modules/monochrome/src/metric-card/rich-metric-chart.js (167 lines of code) (raw):
// Copyright (c) 2019 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {withTheme} from '../shared/theme';
import {ExpandedIcon, CollapsedIcon, CheckAltIcon} from '../shared/icons';
import MetricChart from './metric-chart';
import {FilterContainer, FilterToggle, FilterItem, FilterLegend} from './styled-components';
import memoize from '../utils/memoize';
import {scaleOrdinal} from 'd3-scale';
import {extent} from 'd3-array';
const DEFAULT_COLORS = scaleOrdinal().range([
'#12939A',
'#DDB27C',
'#88572C',
'#FF991F',
'#F15C17',
'#223F9A',
'#DA70BF',
'#125C77',
'#4DC19C',
'#776E57',
'#17B8BE',
'#F6D18A',
'#B7885E',
'#FFCB99',
'#F89570',
'#829AE3',
'#E79FD5',
'#1E96BE',
'#89DAC1',
'#B3AD9E'
]);
/**
* A component that visualizes the multiple data series. Features:
* Each data series is shown as a line series
* Clicking on the legend toggles the visibility of that data series
* Legends are sorted by prominence (maximum value in the look ahead window)
* A show all/show less button to toggle only showing the top 5 data series by value
*/
class MetricChartWithLegends extends PureComponent {
static propTypes = Object.assign({}, MetricChart.propTypes, {
topSeriesCount: PropTypes.number
});
static defaultProps = Object.assign({}, MetricChart.defaultProps, {
topSeriesCount: 5,
getColor: DEFAULT_COLORS
});
constructor(props) {
super(props);
this.state = {
dataVisibility: {},
showTopSeriesOnly: true,
hoveredSeriesName: null
};
this.extractDataSeries = memoize(this._extractDataSeries);
}
_getColor(key) {
const {getColor} = this.props;
switch (typeof getColor) {
case 'object':
return getColor[key];
case 'function':
return getColor(key);
default:
return getColor;
}
}
// Extract subset of streams from all variable streams
// Format stream data for render
_extractDataSeries = data => {
const {formatTitle, getY} = this.props;
const series = [];
for (const key in data) {
const value = data[key];
if (Array.isArray(value)) {
const displayName = formatTitle(key);
const yExtent = extent(value, getY);
series.push({
key,
displayName,
color: this._getColor(key),
data: value,
extent: yExtent,
max: Math.max(Math.abs(yExtent[0]), Math.abs(yExtent[1]))
});
}
}
// Sort data series by max value
series.sort((s1, s2) => s2.max - s1.max);
return series;
};
// Check if a certain data series is turned on by user settings
_isDataVisible = key => {
const {showTopSeriesOnly, dataVisibility} = this.state;
const dataSeries = this.extractDataSeries(this.props.data);
if (dataVisibility[key] === false) {
// turned of by the user
return false;
}
if (showTopSeriesOnly) {
const {topSeriesCount} = this.props;
return dataSeries.findIndex(s => s.key === key) < topSeriesCount;
}
return true;
};
_setHoveredDataName = key => {
this.setState({hoveredSeriesName: key});
};
_toggleDataVisibility = key => {
const {dataVisibility} = this.state;
this.setState({
dataVisibility: {
...dataVisibility,
// at start, all streams have undefined which is treated as visible
[key]: dataVisibility[key] === false
}
});
};
// Legends (also as visibility toggle) of the data streams
_renderDataFilters() {
const {showTopSeriesOnly, hoveredSeriesName} = this.state;
const {theme, style, topSeriesCount} = this.props;
const dataSeries = this.extractDataSeries(this.props.data);
const series = showTopSeriesOnly ? dataSeries.slice(0, topSeriesCount) : dataSeries;
return (
<FilterContainer theme={theme} userStyle={style.filter} isExpanded={!showTopSeriesOnly}>
{dataSeries.length > topSeriesCount && (
<FilterToggle
theme={theme}
userStyle={style.filterToggle}
isExpanded={!showTopSeriesOnly}
onClick={() => this.setState({showTopSeriesOnly: !showTopSeriesOnly})}
>
{showTopSeriesOnly
? style.iconCollapsed || <CollapsedIcon />
: style.iconExpanded || <ExpandedIcon />}
</FilterToggle>
)}
{series.map(s => {
const styleProps = {
theme,
name: s.key,
displayName: s.displayName,
color: s.color,
isHovered: hoveredSeriesName === s.key,
isActive: this._isDataVisible(s.key)
};
return (
<FilterItem
userStyle={style.filterItem}
{...styleProps}
key={`multiplot-${s.key}`}
onMouseOver={() => this._setHoveredDataName(s.key)}
onMouseOut={() => this._setHoveredDataName(null)}
onClick={() => this._toggleDataVisibility(s.key)}
>
<FilterLegend {...styleProps} userStyle={style.filterLegend}>
{styleProps.isActive ? style.iconOn || <CheckAltIcon /> : style.iconOff}
</FilterLegend>
<span>{s.displayName}</span>
</FilterItem>
);
})}
</FilterContainer>
);
}
render() {
return (
<div>
<MetricChart
{...this.props}
highlightSeries={this.state.hoveredSeriesName}
onSeriesMouseOver={key => this._setHoveredDataName(key)}
onMouseLeave={() => this._setHoveredDataName(null)}
dataFilter={this._isDataVisible}
/>
{this._renderDataFilters()}
</div>
);
}
}
export default withTheme(MetricChartWithLegends);