Merge pull request #688 from influxdata/highlight-series

Highlight series
pull/695/head
Andrew Watkins 2016-12-15 10:22:15 -08:00 committed by GitHub
commit b885f5725f
11 changed files with 124 additions and 64 deletions

View File

@ -1,4 +1,7 @@
import timeSeriesToDygraph from 'src/utils/timeSeriesToDygraph';
import {STROKE_WIDTH} from 'src/shared/constants';
const {light: strokeWidth} = STROKE_WIDTH;
describe('timeSeriesToDygraph', () => {
it('parses a raw InfluxDB response into a dygraph friendly data format', () => {
@ -46,9 +49,11 @@ describe('timeSeriesToDygraph', () => {
dygraphSeries: {
'm1.f1': {
axis: 'y',
strokeWidth,
},
'm1.f2': {
axis: 'y',
strokeWidth,
},
},
};
@ -156,12 +161,15 @@ describe('timeSeriesToDygraph', () => {
dygraphSeries: {
'm1.f1': {
axis: 'y',
strokeWidth,
},
'm1.f2': {
axis: 'y',
strokeWidth,
},
'm3.f3': {
axis: 'y2',
strokeWidth,
},
},
};

View File

@ -5,40 +5,38 @@ import QueryTabItem from './QueryTabItem';
import RenamePanelModal from './RenamePanelModal';
import SimpleDropdown from 'src/shared/components/SimpleDropdown';
const {shape, func, bool, arrayOf} = PropTypes;
const Panel = React.createClass({
propTypes: {
panel: shape({}).isRequired,
queries: arrayOf(shape({})).isRequired,
panel: PropTypes.shape({
id: PropTypes.string.isRequired,
}).isRequired,
queries: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
timeRange: PropTypes.shape({
upper: PropTypes.string,
lower: PropTypes.string,
}).isRequired,
isExpanded: bool.isRequired,
onTogglePanel: func.isRequired,
actions: shape({
chooseNamespace: func.isRequired,
chooseMeasurement: func.isRequired,
chooseTag: func.isRequired,
groupByTag: func.isRequired,
addQuery: func.isRequired,
deleteQuery: func.isRequired,
toggleField: func.isRequired,
groupByTime: func.isRequired,
toggleTagAcceptance: func.isRequired,
applyFuncsToField: func.isRequired,
deletePanel: func.isRequired,
isExpanded: PropTypes.bool.isRequired,
onTogglePanel: PropTypes.func.isRequired,
actions: PropTypes.shape({
chooseNamespace: PropTypes.func.isRequired,
chooseMeasurement: PropTypes.func.isRequired,
chooseTag: PropTypes.func.isRequired,
groupByTag: PropTypes.func.isRequired,
addQuery: PropTypes.func.isRequired,
deleteQuery: PropTypes.func.isRequired,
toggleField: PropTypes.func.isRequired,
groupByTime: PropTypes.func.isRequired,
toggleTagAcceptance: PropTypes.func.isRequired,
applyFuncsToField: PropTypes.func.isRequired,
deletePanel: PropTypes.func.isRequired,
renamePanel: PropTypes.func.isRequired,
}).isRequired,
},
getInitialState() {
return {
activeQueryId: null,
};
setActiveQuery: PropTypes.func.isRequired,
activeQueryID: PropTypes.string,
},
handleSetActiveQuery(query) {
this.setState({activeQueryId: query.id});
this.props.setActiveQuery(query.id);
},
handleAddQuery() {
@ -63,8 +61,8 @@ const Panel = React.createClass({
},
getActiveQuery() {
const {queries} = this.props;
const activeQuery = queries.find((query) => query.id === this.state.activeQueryId);
const {queries, activeQueryID} = this.props;
const activeQuery = queries.find((query) => query.id === activeQueryID);
const defaultQuery = queries[0];
return activeQuery || defaultQuery;

View File

@ -24,7 +24,9 @@ const PanelBuilder = React.createClass({
deletePanel: func.isRequired,
}).isRequired,
setActivePanel: func.isRequired,
setActiveQuery: func.isRequired,
activePanelID: string,
activeQueryID: string,
},
handleCreateExploer() {
@ -32,7 +34,7 @@ const PanelBuilder = React.createClass({
},
render() {
const {activePanelID, width, actions, setActivePanel} = this.props;
const {width, actions, setActivePanel, setActiveQuery, activePanelID, activeQueryID} = this.props;
return (
<div className="panel-builder" style={{width}}>
@ -40,7 +42,9 @@ const PanelBuilder = React.createClass({
<PanelList
actions={actions}
setActivePanel={setActivePanel}
setActiveQuery={setActiveQuery}
activePanelID={activePanelID}
activeQueryID={activeQueryID}
/>
</div>
);

View File

@ -15,7 +15,9 @@ const PanelList = React.createClass({
queryConfigs: PropTypes.shape({}),
actions: shape({}).isRequired,
setActivePanel: func.isRequired,
setActiveQuery: func.isRequired,
activePanelID: string,
activeQueryID: string,
},
handleTogglePanel(panel) {
@ -25,12 +27,12 @@ const PanelList = React.createClass({
null : panel.id;
this.props.setActivePanel(activePanelID);
// Reset the activeQueryID when toggling Exporations
this.props.setActiveQuery(null);
},
render() {
const {actions, panels, timeRange, queryConfigs} = this.props;
const activePanelID = this.props.activePanelID;
const {actions, panels, timeRange, queryConfigs, setActiveQuery, activeQueryID, activePanelID} = this.props;
return (
<div>
@ -51,8 +53,10 @@ const PanelList = React.createClass({
queries={queries}
timeRange={timeRange}
onTogglePanel={this.handleTogglePanel}
setActiveQuery={setActiveQuery}
isExpanded={panelID === activePanelID}
actions={allActions}
activeQueryID={activeQueryID}
/>
);
})}

View File

@ -6,22 +6,22 @@ import LineGraph from 'shared/components/LineGraph';
import MultiTable from './MultiTable';
const RefreshingLineGraph = AutoRefresh(LineGraph);
const {bool, shape, string, arrayOf} = PropTypes;
const Visualization = React.createClass({
propTypes: {
timeRange: shape({
upper: string,
lower: string,
timeRange: PropTypes.shape({
upper: PropTypes.string,
lower: PropTypes.string,
}).isRequired,
queryConfigs: arrayOf(shape({})).isRequired,
isActive: bool.isRequired,
name: string,
queryConfigs: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
isActive: PropTypes.bool.isRequired,
name: PropTypes.string,
activeQueryIndex: PropTypes.number,
},
contextTypes: {
source: shape({
links: shape({
proxy: string.isRequired,
source: PropTypes.shape({
links: PropTypes.shape({
proxy: PropTypes.string.isRequired,
}).isRequired,
}).isRequired,
},
@ -45,7 +45,7 @@ const Visualization = React.createClass({
},
render() {
const {queryConfigs, timeRange, isActive, name} = this.props;
const {queryConfigs, timeRange, isActive, name, activeQueryIndex} = this.props;
const {source} = this.context;
const proxyLink = source.links.proxy;
@ -77,6 +77,7 @@ const Visualization = React.createClass({
<RefreshingLineGraph
queries={queries}
autoRefresh={autoRefreshMs}
activeQueryIndex={activeQueryIndex}
/>
) : <MultiTable queries={queries} />}
</div>

View File

@ -14,6 +14,7 @@ const Visualizations = React.createClass({
queryConfigs: shape({}).isRequired,
width: string,
activePanelID: string,
activeQueryID: string,
},
render() {
@ -22,7 +23,9 @@ const Visualizations = React.createClass({
const visualizations = Object.keys(panels).map((panelID) => {
const panel = panels[panelID];
const queries = panel.queryIds.map((id) => queryConfigs[id]);
return <Visualization name={panel.name} key={panelID} queryConfigs={queries} timeRange={timeRange} isActive={panelID === activePanelID} />;
const isActive = panelID === activePanelID;
return <Visualization activeQueryIndex={this.getActiveQueryIndex(panelID)} name={panel.name} key={panelID} queryConfigs={queries} timeRange={timeRange} isActive={isActive} />;
});
return (
@ -31,6 +34,21 @@ const Visualizations = React.createClass({
</div>
);
},
getActiveQueryIndex(panelID) {
const {activeQueryID, activePanelID, panels} = this.props;
const isPanelActive = panelID === activePanelID;
if (!isPanelActive) {
return -1;
}
if (activeQueryID === null) {
return 0;
}
return panels[panelID].queryIds.indexOf(activeQueryID);
},
});
function mapStateToProps(state) {

View File

@ -49,14 +49,17 @@ const DataExplorer = React.createClass({
getInitialState() {
return {
activePanelId: null,
activePanelID: null,
activeQueryID: null,
};
},
handleSetActivePanel(id) {
this.setState({
activePanelID: id,
});
this.setState({activePanelID: id});
},
handleSetActiveQuery(id) {
this.setState({activeQueryID: id});
},
render() {
@ -76,8 +79,18 @@ const DataExplorer = React.createClass({
explorerID={explorerID}
/>
<ResizeContainer>
<PanelBuilder timeRange={timeRange} activePanelID={this.state.activePanelID} setActivePanel={this.handleSetActivePanel} />
<Visualizations timeRange={timeRange} activePanelID={this.state.activePanelID} />
<PanelBuilder
timeRange={timeRange}
activePanelID={this.state.activePanelID}
activeQueryID={this.state.activeQueryID}
setActiveQuery={this.handleSetActiveQuery}
setActivePanel={this.handleSetActivePanel}
/>
<Visualizations
timeRange={timeRange}
activePanelID={this.state.activePanelID}
activeQueryID={this.state.activeQueryID}
/>
</ResizeContainer>
</div>
);

View File

@ -77,7 +77,6 @@ export default React.createClass({
fillGraph: this.props.isGraphFilled,
axisLineWidth: 2,
gridLineWidth: 1,
strokeWidth: 1.5,
highlightCircleSize: 3,
colors: finalLineColors,
series: dygraphSeries,
@ -143,7 +142,7 @@ export default React.createClass({
}
const timeSeries = this.getTimeSeries();
const {labels, ranges} = this.props;
const {labels, ranges, options, dygraphSeries} = this.props;
dygraph.updateOptions({
labels,
@ -156,7 +155,8 @@ export default React.createClass({
valueRange: getRange(timeSeries, ranges.y2),
},
},
underlayCallback: this.props.options.underlayCallback,
underlayCallback: options.underlayCallback,
series: dygraphSeries,
});
dygraph.resize();

View File

@ -7,20 +7,19 @@ import _ from 'lodash';
import timeSeriesToDygraph from 'utils/timeSeriesToDygraph';
import lastValues from 'src/shared/parsing/lastValues';
const {array, string, arrayOf, bool, shape} = PropTypes;
export default React.createClass({
displayName: 'LineGraph',
propTypes: {
data: arrayOf(shape({}).isRequired).isRequired,
title: string,
data: PropTypes.arrayOf(PropTypes.shape({}).isRequired).isRequired,
title: PropTypes.string,
isFetchingInitially: PropTypes.bool,
isRefreshing: PropTypes.bool,
underlayCallback: PropTypes.func,
isGraphFilled: bool,
overrideLineColors: array,
queries: arrayOf(shape({}).isRequired).isRequired,
showSingleStat: bool,
isGraphFilled: PropTypes.bool,
overrideLineColors: PropTypes.array,
queries: PropTypes.arrayOf(PropTypes.shape({}).isRequired).isRequired,
showSingleStat: PropTypes.bool,
activeQueryIndex: PropTypes.number,
},
getDefaultProps() {
@ -36,12 +35,13 @@ export default React.createClass({
},
componentWillMount() {
this._timeSeries = timeSeriesToDygraph(this.props.data);
this._timeSeries = timeSeriesToDygraph(this.props.data, this.props.activeQueryIndex);
},
componentWillUpdate(nextProps) {
if (this.props.data !== nextProps.data) {
this._timeSeries = timeSeriesToDygraph(nextProps.data);
const {data, activeQueryIndex} = this.props;
if (data !== nextProps.data || activeQueryIndex !== nextProps.activeQueryIndex) {
this._timeSeries = timeSeriesToDygraph(nextProps.data, nextProps.activeQueryIndex);
}
},

View File

@ -462,3 +462,8 @@ export const DEFAULT_LINE_COLORS = [
],
],
];
export const STROKE_WIDTH = {
heavy: 3.5,
light: 1.5,
};

View File

@ -1,9 +1,12 @@
import {STROKE_WIDTH} from 'src/shared/constants';
/**
* Accepts an array of raw influxdb responses and returns a format
* that Dygraph understands.
*/
export default function timeSeriesToDygraph(raw = []) {
// activeQueryIndex is an optional argument that indicated which query's series
// we want highlighted.
export default function timeSeriesToDygraph(raw = [], activeQueryIndex) {
const labels = ['time']; // all of the effective field names (i.e. <measurement>.<field>)
const fieldToIndex = {}; // see parseSeries
const dates = {}; // map of date as string to date value to minimize string coercion
@ -90,7 +93,13 @@ export default function timeSeriesToDygraph(raw = []) {
// ex given this timeSeries [Date, 10, 20, 30] field index at 2 would correspond to value 20
fieldToIndex[effectiveFieldName] = labels.length;
labels.push(effectiveFieldName);
dygraphSeries[effectiveFieldName] = {axis: queryIndex === 0 ? 'y' : 'y2'};
const {light, heavy} = STROKE_WIDTH;
dygraphSeries[effectiveFieldName] = {
axis: queryIndex === 0 ? 'y' : 'y2',
strokeWidth: queryIndex === activeQueryIndex ? heavy : light,
};
});
(series.values || []).forEach(parseRow);