mirror of
https://github.com/prometheus/prometheus.git
synced 2025-03-05 20:59:13 -08:00
Graph multi series select (#6391)
* adding graph series multi select support Signed-off-by: Boyko Lalov <boiskila@gmail.com> Signed-off-by: blalov <boiskila@gmail.com> * skip inital plot draw from ResizeDetector Signed-off-by: Boyko Lalov <boiskila@gmail.com> Signed-off-by: blalov <boiskila@gmail.com> * fix unit tests Signed-off-by: blalov <boiskila@gmail.com> * extract Legend as component Signed-off-by: blalov <boiskila@gmail.com> * create legend state interface Signed-off-by: blalov <boiskila@gmail.com> * fix click selection issue Signed-off-by: blalov <boiskila@gmail.com> * fix single series select bug Signed-off-by: Boyko Lalov <boiskila@gmail.com> * review changes Signed-off-by: blalov <boiskila@gmail.com>
This commit is contained in:
parent
8ac15703b0
commit
315b28439f
|
@ -139,10 +139,10 @@ div.time-input {
|
||||||
font-size: 0.75em;
|
font-size: 0.75em;
|
||||||
padding: 10px 5px;
|
padding: 10px 5px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.legend-item {
|
.legend-item {
|
||||||
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
|
|
|
@ -3,8 +3,12 @@ import $ from 'jquery';
|
||||||
import { shallow, mount } from 'enzyme';
|
import { shallow, mount } from 'enzyme';
|
||||||
import Graph from './Graph';
|
import Graph from './Graph';
|
||||||
import ReactResizeDetector from 'react-resize-detector';
|
import ReactResizeDetector from 'react-resize-detector';
|
||||||
|
import { Legend } from './Legend';
|
||||||
|
|
||||||
describe('Graph', () => {
|
describe('Graph', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb: any) => cb());
|
||||||
|
});
|
||||||
describe('data is returned', () => {
|
describe('data is returned', () => {
|
||||||
const props: any = {
|
const props: any = {
|
||||||
queryParams: {
|
queryParams: {
|
||||||
|
@ -64,14 +68,16 @@ describe('Graph', () => {
|
||||||
describe('Legend', () => {
|
describe('Legend', () => {
|
||||||
it('renders a legend', () => {
|
it('renders a legend', () => {
|
||||||
const graph = shallow(<Graph {...props} />);
|
const graph = shallow(<Graph {...props} />);
|
||||||
expect(graph.find('.graph-legend .legend-item')).toHaveLength(1);
|
expect(graph.find(Legend)).toHaveLength(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('on component update', () => {
|
describe('on component update', () => {
|
||||||
let graph: any;
|
let graph: any;
|
||||||
|
let spyState: any;
|
||||||
|
let mockPlot: any;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.spyOn($, 'plot').mockImplementation(() => ({} as any));
|
mockPlot = jest.spyOn($, 'plot').mockReturnValue({ setData: jest.fn(), draw: jest.fn(), destroy: jest.fn() } as any);
|
||||||
graph = mount(
|
graph = mount(
|
||||||
<Graph
|
<Graph
|
||||||
{...({
|
{...({
|
||||||
|
@ -85,12 +91,13 @@ describe('Graph', () => {
|
||||||
} as any)}
|
} as any)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
spyState = jest.spyOn(graph.instance(), 'setState');
|
||||||
});
|
});
|
||||||
afterAll(() => {
|
afterEach(() => {
|
||||||
jest.restoreAllMocks();
|
spyState.mockReset();
|
||||||
|
mockPlot.mockReset();
|
||||||
});
|
});
|
||||||
it('should trigger state update when new data is recieved', () => {
|
it('should trigger state update when new data is recieved', () => {
|
||||||
const spyState = jest.spyOn(Graph.prototype, 'setState');
|
|
||||||
graph.setProps({ data: { result: [{ values: [{}], metric: {} }] } });
|
graph.setProps({ data: { result: [{ values: [{}], metric: {} }] } });
|
||||||
expect(spyState).toHaveBeenCalledWith(
|
expect(spyState).toHaveBeenCalledWith(
|
||||||
{
|
{
|
||||||
|
@ -102,25 +109,22 @@ describe('Graph', () => {
|
||||||
labels: {},
|
labels: {},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
selectedSeriesIndex: null,
|
|
||||||
},
|
},
|
||||||
expect.anything()
|
expect.anything()
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('should trigger state update when stacked prop is changed', () => {
|
it('should trigger state update when stacked prop is changed', () => {
|
||||||
const spyState = jest.spyOn(Graph.prototype, 'setState');
|
graph.setProps({ stacked: false });
|
||||||
graph.setProps({ stacked: true });
|
|
||||||
expect(spyState).toHaveBeenCalledWith(
|
expect(spyState).toHaveBeenCalledWith(
|
||||||
{
|
{
|
||||||
chartData: [
|
chartData: [
|
||||||
{
|
{
|
||||||
color: 'rgb(237,194,64)',
|
color: 'rgb(237,194,64)',
|
||||||
data: [[1572128592000, 0]],
|
data: [[1572128592000, null]],
|
||||||
index: 0,
|
index: 0,
|
||||||
labels: {},
|
labels: {},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
selectedSeriesIndex: null,
|
|
||||||
},
|
},
|
||||||
expect.anything()
|
expect.anything()
|
||||||
);
|
);
|
||||||
|
@ -128,7 +132,7 @@ describe('Graph', () => {
|
||||||
});
|
});
|
||||||
describe('on unmount', () => {
|
describe('on unmount', () => {
|
||||||
it('should call destroy plot', () => {
|
it('should call destroy plot', () => {
|
||||||
const wrapper = shallow(
|
const graph = mount(
|
||||||
<Graph
|
<Graph
|
||||||
{...({
|
{...({
|
||||||
stacked: true,
|
stacked: true,
|
||||||
|
@ -141,21 +145,17 @@ describe('Graph', () => {
|
||||||
} as any)}
|
} as any)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
const spyPlotDestroy = jest.spyOn(Graph.prototype, 'componentWillUnmount');
|
const spyPlotDestroy = jest.spyOn(graph.instance(), 'componentWillUnmount');
|
||||||
wrapper.unmount();
|
graph.unmount();
|
||||||
expect(spyPlotDestroy).toHaveBeenCalledTimes(1);
|
expect(spyPlotDestroy).toHaveBeenCalledTimes(1);
|
||||||
|
spyPlotDestroy.mockReset();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('plot', () => {
|
describe('plot', () => {
|
||||||
let spyFlot: any;
|
|
||||||
beforeEach(() => {
|
|
||||||
spyFlot = jest.spyOn($, 'plot').mockImplementation(() => ({} as any));
|
|
||||||
});
|
|
||||||
afterAll(() => {
|
|
||||||
jest.restoreAllMocks();
|
|
||||||
});
|
|
||||||
it('should not call jquery.plot if chartRef not exist', () => {
|
it('should not call jquery.plot if chartRef not exist', () => {
|
||||||
|
const mockSetData = jest.fn();
|
||||||
|
jest.spyOn($, 'plot').mockReturnValue({ setData: mockSetData, draw: jest.fn(), destroy: jest.fn() } as any);
|
||||||
const graph = shallow(
|
const graph = shallow(
|
||||||
<Graph
|
<Graph
|
||||||
{...({
|
{...({
|
||||||
|
@ -170,9 +170,12 @@ describe('Graph', () => {
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
(graph.instance() as any).plot();
|
(graph.instance() as any).plot();
|
||||||
expect(spyFlot).not.toBeCalled();
|
expect(mockSetData).not.toBeCalled();
|
||||||
});
|
});
|
||||||
it('should call jquery.plot if chartRef exist', () => {
|
it('should call jquery.plot if chartRef exist', () => {
|
||||||
|
const mockPlot = jest
|
||||||
|
.spyOn($, 'plot')
|
||||||
|
.mockReturnValue({ setData: jest.fn(), draw: jest.fn(), destroy: jest.fn() } as any);
|
||||||
const graph = mount(
|
const graph = mount(
|
||||||
<Graph
|
<Graph
|
||||||
{...({
|
{...({
|
||||||
|
@ -187,11 +190,11 @@ describe('Graph', () => {
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
(graph.instance() as any).plot();
|
(graph.instance() as any).plot();
|
||||||
expect(spyFlot).toBeCalled();
|
expect(mockPlot).toBeCalled();
|
||||||
});
|
});
|
||||||
it('should destroy plot', () => {
|
it('should destroy plot', () => {
|
||||||
const spyPlotDestroy = jest.fn();
|
const mockDestroy = jest.fn();
|
||||||
jest.spyOn($, 'plot').mockReturnValue({ destroy: spyPlotDestroy } as any);
|
jest.spyOn($, 'plot').mockReturnValue({ setData: jest.fn(), draw: jest.fn(), destroy: mockDestroy } as any);
|
||||||
const graph = mount(
|
const graph = mount(
|
||||||
<Graph
|
<Graph
|
||||||
{...({
|
{...({
|
||||||
|
@ -207,17 +210,12 @@ describe('Graph', () => {
|
||||||
);
|
);
|
||||||
(graph.instance() as any).plot();
|
(graph.instance() as any).plot();
|
||||||
(graph.instance() as any).destroyPlot();
|
(graph.instance() as any).destroyPlot();
|
||||||
expect(spyPlotDestroy).toHaveBeenCalledTimes(1);
|
expect(mockDestroy).toHaveBeenCalledTimes(2);
|
||||||
jest.restoreAllMocks();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('plotSetAndDraw', () => {
|
describe('plotSetAndDraw', () => {
|
||||||
afterEach(() => {
|
|
||||||
jest.restoreAllMocks();
|
|
||||||
});
|
|
||||||
it('should call spyPlotSetAndDraw on legend hover', () => {
|
it('should call spyPlotSetAndDraw on legend hover', () => {
|
||||||
jest.spyOn($, 'plot').mockReturnValue({ setData: jest.fn(), draw: jest.fn() } as any);
|
jest.spyOn($, 'plot').mockReturnValue({ setData: jest.fn(), draw: jest.fn(), destroy: jest.fn() } as any);
|
||||||
jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb: any) => cb());
|
|
||||||
const graph = mount(
|
const graph = mount(
|
||||||
<Graph
|
<Graph
|
||||||
{...({
|
{...({
|
||||||
|
@ -232,7 +230,7 @@ describe('Graph', () => {
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
(graph.instance() as any).plot(); // create chart
|
(graph.instance() as any).plot(); // create chart
|
||||||
const spyPlotSetAndDraw = jest.spyOn(Graph.prototype, 'plotSetAndDraw');
|
const spyPlotSetAndDraw = jest.spyOn(graph.instance() as any, 'plotSetAndDraw');
|
||||||
graph
|
graph
|
||||||
.find('.legend-item')
|
.find('.legend-item')
|
||||||
.at(0)
|
.at(0)
|
||||||
|
@ -240,9 +238,10 @@ describe('Graph', () => {
|
||||||
expect(spyPlotSetAndDraw).toHaveBeenCalledTimes(1);
|
expect(spyPlotSetAndDraw).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
it('should call spyPlotSetAndDraw with chartDate from state as default value', () => {
|
it('should call spyPlotSetAndDraw with chartDate from state as default value', () => {
|
||||||
const spySetData = jest.fn();
|
const mockSetData = jest.fn();
|
||||||
jest.spyOn($, 'plot').mockReturnValue({ setData: spySetData, draw: jest.fn() } as any);
|
const spyPlot = jest
|
||||||
jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb: any) => cb());
|
.spyOn($, 'plot')
|
||||||
|
.mockReturnValue({ setData: mockSetData, draw: jest.fn(), destroy: jest.fn() } as any);
|
||||||
const graph: any = mount(
|
const graph: any = mount(
|
||||||
<Graph
|
<Graph
|
||||||
{...({
|
{...({
|
||||||
|
@ -258,7 +257,8 @@ describe('Graph', () => {
|
||||||
);
|
);
|
||||||
(graph.instance() as any).plot(); // create chart
|
(graph.instance() as any).plot(); // create chart
|
||||||
graph.find('.graph-legend').simulate('mouseout');
|
graph.find('.graph-legend').simulate('mouseout');
|
||||||
expect(spySetData).toHaveBeenCalledWith(graph.state().chartData);
|
expect(mockSetData).toHaveBeenCalledWith(graph.state().chartData);
|
||||||
|
spyPlot.mockReset();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,7 +2,7 @@ import $ from 'jquery';
|
||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import ReactResizeDetector from 'react-resize-detector';
|
import ReactResizeDetector from 'react-resize-detector';
|
||||||
|
|
||||||
import SeriesName from '../SeriesName';
|
import { Legend } from './Legend';
|
||||||
import { Metric, QueryParams } from '../types/types';
|
import { Metric, QueryParams } from '../types/types';
|
||||||
import { isPresent } from '../utils/func';
|
import { isPresent } from '../utils/func';
|
||||||
import { normalizeData, getOptions, toHoverColor } from './GraphHelpers';
|
import { normalizeData, getOptions, toHoverColor } from './GraphHelpers';
|
||||||
|
@ -31,7 +31,6 @@ export interface GraphSeries {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GraphState {
|
interface GraphState {
|
||||||
selectedSeriesIndex: number | null;
|
|
||||||
chartData: GraphSeries[];
|
chartData: GraphSeries[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,30 +38,43 @@ class Graph extends PureComponent<GraphProps, GraphState> {
|
||||||
private chartRef = React.createRef<HTMLDivElement>();
|
private chartRef = React.createRef<HTMLDivElement>();
|
||||||
private $chart?: jquery.flot.plot;
|
private $chart?: jquery.flot.plot;
|
||||||
private rafID = 0;
|
private rafID = 0;
|
||||||
|
private selectedSeriesIndexes: number[] = [];
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
selectedSeriesIndex: null,
|
|
||||||
chartData: normalizeData(this.props),
|
chartData: normalizeData(this.props),
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidUpdate(prevProps: GraphProps) {
|
componentDidUpdate(prevProps: GraphProps) {
|
||||||
const { data, stacked } = this.props;
|
const { data, stacked } = this.props;
|
||||||
if (prevProps.data !== data || prevProps.stacked !== stacked) {
|
if (prevProps.data !== data) {
|
||||||
this.setState({ selectedSeriesIndex: null, chartData: normalizeData(this.props) }, this.plot);
|
this.selectedSeriesIndexes = [];
|
||||||
|
this.setState({ chartData: normalizeData(this.props) }, this.plot);
|
||||||
|
} else if (prevProps.stacked !== stacked) {
|
||||||
|
this.setState({ chartData: normalizeData(this.props) }, () => {
|
||||||
|
if (this.selectedSeriesIndexes.length === 0) {
|
||||||
|
this.plot();
|
||||||
|
} else {
|
||||||
|
this.plot(this.state.chartData.filter((_, i) => this.selectedSeriesIndexes.includes(i)));
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.plot();
|
||||||
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.destroyPlot();
|
this.destroyPlot();
|
||||||
}
|
}
|
||||||
|
|
||||||
plot = () => {
|
plot = (data: GraphSeries[] = this.state.chartData) => {
|
||||||
if (!this.chartRef.current) {
|
if (!this.chartRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.destroyPlot();
|
this.destroyPlot();
|
||||||
|
|
||||||
this.$chart = $.plot($(this.chartRef.current), this.state.chartData, getOptions(this.props.stacked));
|
this.$chart = $.plot($(this.chartRef.current), data, getOptions(this.props.stacked));
|
||||||
};
|
};
|
||||||
|
|
||||||
destroyPlot = () => {
|
destroyPlot = () => {
|
||||||
|
@ -78,14 +90,14 @@ class Graph extends PureComponent<GraphProps, GraphState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSeriesSelect = (index: number) => () => {
|
handleSeriesSelect = (selected: number[], selectedIndex: number) => {
|
||||||
const { selectedSeriesIndex, chartData } = this.state;
|
const { chartData } = this.state;
|
||||||
this.plotSetAndDraw(
|
this.plot(
|
||||||
selectedSeriesIndex === index
|
this.selectedSeriesIndexes.length === 1 && this.selectedSeriesIndexes.includes(selectedIndex)
|
||||||
? chartData.map(toHoverColor(index, this.props.stacked))
|
? chartData.map(toHoverColor(selectedIndex, this.props.stacked))
|
||||||
: chartData.slice(index, index + 1)
|
: chartData.filter((_, i) => selected.includes(i)) // draw only selected
|
||||||
);
|
);
|
||||||
this.setState({ selectedSeriesIndex: selectedSeriesIndex === index ? null : index });
|
this.selectedSeriesIndexes = selected;
|
||||||
};
|
};
|
||||||
|
|
||||||
handleSeriesHover = (index: number) => () => {
|
handleSeriesHover = (index: number) => () => {
|
||||||
|
@ -102,28 +114,25 @@ class Graph extends PureComponent<GraphProps, GraphState> {
|
||||||
this.plotSetAndDraw();
|
this.plotSetAndDraw();
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
handleResize = () => {
|
||||||
const { selectedSeriesIndex, chartData } = this.state;
|
if (isPresent(this.$chart)) {
|
||||||
const canUseHover = chartData.length > 1 && selectedSeriesIndex === null;
|
this.plot(this.$chart.getData() as GraphSeries[]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { chartData } = this.state;
|
||||||
return (
|
return (
|
||||||
<div className="graph">
|
<div className="graph">
|
||||||
<ReactResizeDetector handleWidth onResize={this.plot} />
|
<ReactResizeDetector handleWidth onResize={this.handleResize} skipOnMount />
|
||||||
<div className="graph-chart" ref={this.chartRef} />
|
<div className="graph-chart" ref={this.chartRef} />
|
||||||
<div className="graph-legend" onMouseOut={canUseHover ? this.handleLegendMouseOut : undefined}>
|
<Legend
|
||||||
{chartData.map(({ index, color, labels }) => (
|
shouldReset={this.selectedSeriesIndexes.length === 0}
|
||||||
<div
|
chartData={chartData}
|
||||||
style={{ opacity: selectedSeriesIndex === null || index === selectedSeriesIndex ? 1 : 0.5 }}
|
onHover={this.handleSeriesHover}
|
||||||
onClick={chartData.length > 1 ? this.handleSeriesSelect(index) : undefined}
|
onLegendMouseOut={this.handleLegendMouseOut}
|
||||||
onMouseOver={canUseHover ? this.handleSeriesHover(index) : undefined}
|
onSeriesToggle={this.handleSeriesSelect}
|
||||||
key={index}
|
/>
|
||||||
className="legend-item"
|
|
||||||
>
|
|
||||||
<span className="legend-swatch" style={{ backgroundColor: color }}></span>
|
|
||||||
<SeriesName labels={labels} format />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
77
web/ui/react-app/src/graph/Legend.tsx
Normal file
77
web/ui/react-app/src/graph/Legend.tsx
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
import React, { PureComponent, SyntheticEvent } from 'react';
|
||||||
|
import SeriesName from '../SeriesName';
|
||||||
|
import { GraphSeries } from './Graph';
|
||||||
|
|
||||||
|
interface LegendProps {
|
||||||
|
chartData: GraphSeries[];
|
||||||
|
shouldReset: boolean;
|
||||||
|
onLegendMouseOut: (ev: SyntheticEvent<HTMLDivElement>) => void;
|
||||||
|
onSeriesToggle: (selected: number[], index: number) => void;
|
||||||
|
onHover: (index: number) => (ev: SyntheticEvent<HTMLDivElement>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LegendState {
|
||||||
|
selectedIndexes: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Legend extends PureComponent<LegendProps, LegendState> {
|
||||||
|
state = {
|
||||||
|
selectedIndexes: [] as number[],
|
||||||
|
};
|
||||||
|
componentDidUpdate(prevProps: LegendProps) {
|
||||||
|
if (this.props.shouldReset && prevProps.shouldReset !== this.props.shouldReset) {
|
||||||
|
this.setState({ selectedIndexes: [] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handleSeriesSelect = (index: number) => (ev: any) => {
|
||||||
|
// TODO: add proper event type
|
||||||
|
const { selectedIndexes } = this.state;
|
||||||
|
|
||||||
|
let selected = [index];
|
||||||
|
if (ev.ctrlKey) {
|
||||||
|
const { chartData } = this.props;
|
||||||
|
if (selectedIndexes.includes(index)) {
|
||||||
|
selected = selectedIndexes.filter(idx => idx !== index);
|
||||||
|
} else {
|
||||||
|
selected =
|
||||||
|
// Flip the logic - In case none is selected ctrl + click should deselect clicked series.
|
||||||
|
selectedIndexes.length === 0
|
||||||
|
? chartData.reduce<number[]>((acc, _, i) => (i === index ? acc : [...acc, i]), [])
|
||||||
|
: [...selectedIndexes, index]; // Select multiple.
|
||||||
|
}
|
||||||
|
} else if (selectedIndexes.length === 1 && selectedIndexes.includes(index)) {
|
||||||
|
selected = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ selectedIndexes: selected });
|
||||||
|
this.props.onSeriesToggle(selected, index);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { chartData, onLegendMouseOut, onHover } = this.props;
|
||||||
|
const { selectedIndexes } = this.state;
|
||||||
|
const canUseHover = chartData.length > 1 && selectedIndexes.length === 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="graph-legend" onMouseOut={canUseHover ? onLegendMouseOut : undefined}>
|
||||||
|
{chartData.map(({ index, color, labels }) => (
|
||||||
|
<div
|
||||||
|
style={{ opacity: selectedIndexes.length === 0 || selectedIndexes.includes(index) ? 1 : 0.5 }}
|
||||||
|
onClick={chartData.length > 1 ? this.handleSeriesSelect(index) : undefined}
|
||||||
|
onMouseOver={canUseHover ? onHover(index) : undefined}
|
||||||
|
key={index}
|
||||||
|
className="legend-item"
|
||||||
|
>
|
||||||
|
<span className="legend-swatch" style={{ backgroundColor: color }}></span>
|
||||||
|
<SeriesName labels={labels} format />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{chartData.length > 1 && (
|
||||||
|
<div className="pl-1 mt-1 text-muted" style={{ fontSize: 13 }}>
|
||||||
|
Click: select series, CTRL + click: toggle multiple series
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue