Skip to content

Commit

Permalink
Small tracking UI improvements (mlflow#263)
Browse files Browse the repository at this point in the history
This PR makes a few small fixes:

1) Got rid of NaNs shown on the Compare Runs page for run IDs that didn't have a specific metric.
2) Changed the collapse/expand button for opening the Experiments sidebar to have `cursor: pointer` so it looks like a clickable object.
3) Added a breadcrumb link back to each experiment on the Run, Compare Runs and Metrics pages (the "Default > ..." in the headings below):

![screen shot 2018-08-07 at 3 52 05 pm](https://user-images.githubusercontent.com/228859/43806814-63bbf14c-9a5a-11e8-960e-4a24eee1aefc.png)

![screen shot 2018-08-07 at 3 51 47 pm](https://user-images.githubusercontent.com/228859/43806820-66ddad70-9a5a-11e8-997a-4d39ffdb4503.png)

![screen shot 2018-08-07 at 6 52 16 pm](https://user-images.githubusercontent.com/228859/43811765-173593b4-9a73-11e8-9398-32a19dfee5ba.png)
  • Loading branch information
mateiz committed Aug 11, 2018
1 parent 7d3004f commit 5f66de1
Show file tree
Hide file tree
Showing 19 changed files with 258 additions and 113 deletions.
15 changes: 11 additions & 4 deletions mlflow/server/js/src/Routes.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
class Routes {
static rootRoute = "/";

static getExperimentPageRoute(experimentId) {
return `/experiments/${experimentId}`;
}

static experimentPageRoute = "/experiments/:experimentId";

static getRunPageRoute(experimentId, runUuid) {
return `/experiments/${experimentId}/runs/${runUuid}`;
}

static runPageRoute = "/experiments/:experimentId/runs/:runUuid";
static getMetricPageRoute(runUuids, metricKey) {
return `/metric/${metricKey}?runs=${JSON.stringify(runUuids)}`;

static getMetricPageRoute(runUuids, metricKey, experimentId) {
return `/metric/${metricKey}?runs=${JSON.stringify(runUuids)}&experiment=${experimentId}`;
}

static metricPageRoute = "/metric/:metricKey";

static getCompareRunPageRoute(runUuids) {
return `/compare-runs?runs=${JSON.stringify(runUuids)}`
static getCompareRunPageRoute(runUuids, experimentId) {
return `/compare-runs?runs=${JSON.stringify(runUuids)}&experiment=${experimentId}`;
}

static compareRunPageRoute = "/compare-runs"
}

Expand Down
10 changes: 10 additions & 0 deletions mlflow/server/js/src/components/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ h1 {
color: #333;
}

h1 a, h1 a:hover, h1 a:active, h1 a:visited {
color: #333;
}

h2 {
font-size: 18px;
font-weight: normal;
Expand Down Expand Up @@ -149,3 +153,9 @@ table th {
color: #888888;
font-weight: 500;
}

.breadcrumb-chevron {
font-size: 75%;
margin-left: 10px;
margin-right: 8px;
}
48 changes: 48 additions & 0 deletions mlflow/server/js/src/components/BreadcrumbTitle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import { Experiment } from "../sdk/MlflowMessages";
import { Link } from 'react-router-dom';
import Routes from "../Routes";

/**
* A title component that creates a <h1> with breadcrumbs pointing to an experiment and optionally
* a run or a run comparison page.
*/
export default class BreadcrumbTitle extends Component {
static propTypes = {
experiment: PropTypes.instanceOf(Experiment).isRequired,
runUuids: PropTypes.arrayOf(String), // Optional because not all pages are nested under runs
title: PropTypes.any.isRequired,
};

render() {
const {experiment, runUuids, title} = this.props;
const experimentId = experiment.getExperimentId();
const experimentLink = (
<Link to={Routes.getExperimentPageRoute(experimentId)}>
{experiment.getName()}
</Link>
);
let runsLink = null;
if (runUuids) {
runsLink = (runUuids.length === 1 ?
<Link to={Routes.getRunPageRoute(experimentId, runUuids[0])} key="link">
Run {runUuids[0]}
</Link>
:
<Link to={Routes.getCompareRunPageRoute(runUuids, experimentId)} key="link">
Comparing {runUuids.length} Runs
</Link>
);
}
let chevron = <i className="fas fa-chevron-right breadcrumb-chevron" key="chevron"/>;
return (
<h1>
{experimentLink}
{chevron}
{ runsLink ? [runsLink, chevron] : [] }
{title}
</h1>
);
}
}
11 changes: 8 additions & 3 deletions mlflow/server/js/src/components/CompareRunPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,21 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import qs from 'qs';
import { connect } from 'react-redux';
import { getRunApi, getUUID } from '../Actions';
import { getExperimentApi, getRunApi, getUUID } from '../Actions';
import RequestStateWrapper from './RequestStateWrapper';
import CompareRunView from './CompareRunView';

class CompareRunPage extends Component {
static propTypes = {
experimentId: PropTypes.number.isRequired,
runUuids: PropTypes.arrayOf(String).isRequired,
};

componentWillMount() {
this.requestIds = [];
const experimentRequestId = getUUID();
this.props.dispatch(getExperimentApi(this.props.experimentId, experimentRequestId));
this.requestIds.push(experimentRequestId);
this.props.runUuids.forEach((runUuid) => {
const requestId = getUUID();
this.requestIds.push(requestId);
Expand All @@ -23,7 +27,7 @@ class CompareRunPage extends Component {
render() {
return (
<RequestStateWrapper requestIds={this.requestIds}>
<CompareRunView runUuids={this.props.runUuids}/>
<CompareRunView runUuids={this.props.runUuids} experimentId={this.props.experimentId}/>
</RequestStateWrapper>
);
}
Expand All @@ -33,7 +37,8 @@ const mapStateToProps = (state, ownProps) => {
const { location } = ownProps;
const searchValues = qs.parse(location.search);
const runUuids = JSON.parse(searchValues["?runs"]);
return { runUuids };
const experimentId = parseInt(searchValues["experiment"], 10);
return { experimentId, runUuids };
};

export default connect(mapStateToProps)(CompareRunPage);
17 changes: 11 additions & 6 deletions mlflow/server/js/src/components/CompareRunScatter.css
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
.scatter-tooltip {
width: 300px;
font-size: 90%;
}

.scatter-tooltip h3 {
font-size: 105%;
color: #888;
}

.scatter-tooltip h4 {
font-size: 110%;
margin: 4px 0 0 0;
font-size: 105%;
color: #888;
margin: 0;
}

.scatter-tooltip ul {
Expand All @@ -16,8 +21,8 @@
.scatter-tooltip ul li {
display: block;
margin: 0;
padding: 0 0 0 16px;
text-indent: -13px;
padding: 0 0 0 0;
text-indent: 0;
}

.scatter-tooltip ul li .value {
Expand All @@ -28,4 +33,4 @@
overflow: hidden;
text-indent: 0;
vertical-align: top;
}
}
25 changes: 19 additions & 6 deletions mlflow/server/js/src/components/CompareRunScatter.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,16 @@ import './CompareRunView.css';
import { RunInfo } from '../sdk/MlflowMessages';
import Utils from '../utils/Utils';
import { getLatestMetrics } from '../reducers/MetricReducer';
import {ScatterChart, Scatter, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Label} from 'recharts';
import {
ScatterChart,
Scatter,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
Label,
} from 'recharts';
import './CompareRunScatter.css';

class CompareRunScatter extends Component {
Expand All @@ -19,6 +28,8 @@ class CompareRunScatter extends Component {
constructor(props) {
super(props);

this.renderTooltip = this.renderTooltip.bind(this);

this.metricKeys = CompareRunScatter.getKeys(this.props.metricLists);
this.paramKeys = CompareRunScatter.getKeys(this.props.paramLists);

Expand Down Expand Up @@ -126,15 +137,17 @@ class CompareRunScatter extends Component {
<YAxis type="number" dataKey='y' name='y'>
{this.renderAxisLabel('y')}
</YAxis>
<CartesianGrid />
<Tooltip
<CartesianGrid/>
<Tooltip
isAnimationActive={false}
cursor={{strokeDasharray: '3 3'}}
content={this.renderTooltip.bind(this)}/>
content={this.renderTooltip}
/>
<Scatter
data={scatterData}
fill='#8884d8'
isAnimationActive={false} />
fill='#AE76A6'
isAnimationActive={false}
/>
</ScatterChart>
</ResponsiveContainer>
</div>
Expand Down
5 changes: 3 additions & 2 deletions mlflow/server/js/src/components/CompareRunView.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

.run-metadata-label {
display: inline-block;
width: 500px;
width: 250px;
font-size: 16px;
color: #888;
}
Expand All @@ -26,4 +26,5 @@
flex: 1;
padding-left: 8px;
font-size: 16px;
}
}

49 changes: 34 additions & 15 deletions mlflow/server/js/src/components/CompareRunView.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { getParams, getRunInfo } from '../reducers/Reducers';
import { getExperiment, getParams, getRunInfo } from '../reducers/Reducers';
import { connect } from 'react-redux';
import './CompareRunView.css';
import { RunInfo } from '../sdk/MlflowMessages';
import { Experiment, RunInfo } from '../sdk/MlflowMessages';
import HtmlTableView from './HtmlTableView';
import CompareRunScatter from './CompareRunScatter';
import Routes from '../Routes';
import { Link } from 'react-router-dom';
import Utils from '../utils/Utils';
import { getLatestMetrics } from '../reducers/MetricReducer';
import BreadcrumbTitle from "./BreadcrumbTitle";

class CompareRunView extends Component {
static propTypes = {
experiment: PropTypes.instanceOf(Experiment).isRequired,
runInfos: PropTypes.arrayOf(RunInfo).isRequired,
metricLists: PropTypes.arrayOf(Array).isRequired,
paramLists: PropTypes.arrayOf(Array).isRequired,
Expand All @@ -31,22 +33,29 @@ class CompareRunView extends Component {
flex: '1',
},
'td-first': {
width: '500px',
width: '250px',
},
'th-first': {
width: '500px',
width: '250px',
},
};

const experiment = this.props.experiment;
const experimentId = experiment.getExperimentId();
return (
<div className="CompareRunView">
<h1>Comparing {this.props.runInfos.length} Runs</h1>
<BreadcrumbTitle
experiment={experiment}
title={"Comparing " + this.props.runInfos.length + " Runs"}
/>
<div className="run-metadata-container">
<div className="run-metadata-label">Run UUID:</div>
<div className="run-metadata-label">Run ID:</div>
<div className="run-metadata-row">
{this.props.runInfos.map((r, idx) =>
{this.props.runInfos.map(r =>
<div className="run-metadata-item" key={r.run_uuid}>
{r.getRunUuid()}
<Link to={Routes.getRunPageRoute(r.getExperimentId(), r.getRunUuid())}>
{r.getRunUuid()}
</Link>
</div>
)}
</div>
Expand All @@ -70,7 +79,7 @@ class CompareRunView extends Component {
<h2>Metrics</h2>
<HtmlTableView
columns={["Name", "", ""]}
values={Private.getLatestMetricRows(this.props.runInfos, this.props.metricLists)}
values={Private.getLatestMetricRows(this.props.runInfos, this.props.metricLists, experimentId)}
styles={tableStyles}
/>
<CompareRunScatter runUuids={this.props.runUuids}/>
Expand All @@ -83,13 +92,14 @@ const mapStateToProps = (state, ownProps) => {
const runInfos = [];
const metricLists = [];
const paramLists = [];
const { runUuids } = ownProps;
const { experimentId, runUuids } = ownProps;
const experiment = getExperiment(experimentId, state);
runUuids.forEach((runUuid) => {
runInfos.push(getRunInfo(runUuid, state));
metricLists.push(Object.values(getLatestMetrics(runUuid, state)));
paramLists.push(Object.values(getParams(runUuid, state)));
});
return { runInfos, metricLists, paramLists };
return { experiment, runInfos, metricLists, paramLists };
};

export default connect(mapStateToProps)(CompareRunView);
Expand Down Expand Up @@ -121,14 +131,14 @@ class Private {
return rows;
}

static getLatestMetricRows(runInfos, metricLists) {
static getLatestMetricRows(runInfos, metricLists, experimentId) {
const rows = [];
// Map of parameter key to a map of (runUuid -> value)
const metricKeyValueList = [];
metricLists.forEach((metricList) => {
const curKeyValueObj = {};
metricList.forEach((metric) => {
curKeyValueObj[metric.key] = Utils.formatMetric(metric.value);
curKeyValueObj[metric.key] = metric.value;
});
metricKeyValueList.push(curKeyValueObj);
});
Expand All @@ -138,10 +148,19 @@ class Private {
// Figure out which runUuids actually have this metric.
const runUuidsWithMetric = Object.keys(mergedMetrics[metricKey]);
const curRow = [];
curRow.push(<Link to={Routes.getMetricPageRoute(runUuidsWithMetric, metricKey)}>{metricKey}</Link>);
curRow.push(
<Link to={Routes.getMetricPageRoute(runUuidsWithMetric, metricKey, experimentId)} title="Plot chart">
{metricKey}
<i className="fas fa-chart-line" style={{paddingLeft: "6px"}}/>
</Link>
);
runInfos.forEach((r) => {
const curUuid = r.run_uuid;
curRow.push(Math.round(mergedMetrics[metricKey][curUuid] * 1e4)/1e4);
if (mergedMetrics[metricKey].hasOwnProperty(curUuid)) {
curRow.push(Utils.formatMetric(mergedMetrics[metricKey][curUuid]));
} else {
curRow.push("");
}
});
rows.push(curRow)
});
Expand Down
1 change: 1 addition & 0 deletions mlflow/server/js/src/components/ExperimentListView.css
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,5 @@
height: 24px;
text-align: center;
margin-left: 68px;
cursor: pointer;
}
4 changes: 3 additions & 1 deletion mlflow/server/js/src/components/ExperimentListView.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ class ExperimentListView extends Component {
<div>
<h1 className="experiments-header">Experiments</h1>
<div className="collapser-container">
<i onClick={this.props.onClickListExperiments} className="collapser fa fa-chevron-left login-icon"/>
<i onClick={this.props.onClickListExperiments}
title="Hide experiment list"
className="collapser fa fa-chevron-left login-icon"/>
</div>
<div className="experiment-list-container" style={{ height: experimentListHeight }}>
{this.props.experiments.map((e) => {
Expand Down
Loading

0 comments on commit 5f66de1

Please sign in to comment.