Skip to content

Commit 78af858

Browse files
feat(legend): display series value (dependent on hover) & sort in legend (#155)
1 parent 3221b14 commit 78af858

File tree

14 files changed

+365
-31
lines changed

14 files changed

+365
-31
lines changed

src/components/_legend.scss

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,13 @@ $elasticChartsLegendMaxHeight: $euiSize * 4;
8181
overflow: hidden;
8282
flex-shrink: 1;
8383
flex-grow: 0;
84+
max-width: 100%;
8485
}
8586
.elasticChartsLegendList {
8687
overflow-y: auto;
8788
overflow-x: hidden;
8889
height: 100%;
90+
max-width: 100%;
8991
@include euiScrollBar;
9092
}
9193

@@ -108,9 +110,20 @@ $elasticChartsLegendMaxHeight: $euiSize * 4;
108110
max-width: $elasticChartsLegendMaxWidth - 4 * $euiSize;
109111

110112
&.elasticChartsLegendListItem__title--selected {
111-
.elasticChartsLegendListItem__title {
112-
text-decoration: underline;
113-
}
113+
text-decoration: underline;
114+
}
115+
116+
&.elasticChartsLegendListItem__title--hasDisplayValue {
117+
width: $elasticChartsLegendMaxWidth - 6 * $euiSize;
118+
max-width: $elasticChartsLegendMaxWidth - 6 * $euiSize;
119+
}
120+
}
121+
122+
.elasticChartsLegendListItem__displayValue {
123+
text-align: right;
124+
125+
&.elasticChartsLegendListItem__displayValue--hidden {
126+
display: none;
114127
}
115128
}
116129

src/components/legend.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ class LegendComponent extends React.Component<ReactiveChartProps> {
6868
responsive={false}
6969
>
7070
{[...legendItems.values()].map((item) => {
71-
const { color, label, isSeriesVisible, isLegendItemVisible } = item;
71+
const { isLegendItemVisible } = item;
7272

7373
const legendItemProps = {
7474
key: item.key,
@@ -81,7 +81,7 @@ class LegendComponent extends React.Component<ReactiveChartProps> {
8181

8282
return (
8383
<EuiFlexItem {...legendItemProps}>
84-
{this.renderLegendElement({ color, label, isSeriesVisible }, item.key)}
84+
{this.renderLegendElement(item, item.key)}
8585
</EuiFlexItem>
8686
);
8787
})}
@@ -100,10 +100,18 @@ class LegendComponent extends React.Component<ReactiveChartProps> {
100100
}
101101

102102
private renderLegendElement = (
103-
{ color, label, isSeriesVisible }: Partial<LegendItem>,
103+
{ color, label, isSeriesVisible, displayValue }: LegendItem,
104104
legendItemKey: string,
105105
) => {
106-
const props = { color, label, isSeriesVisible, legendItemKey };
106+
const tooltipValues = this.props.chartStore!.legendItemTooltipValues.get();
107+
let tooltipValue;
108+
109+
if (tooltipValues && tooltipValues.get(legendItemKey)) {
110+
tooltipValue = tooltipValues.get(legendItemKey);
111+
}
112+
113+
const display = tooltipValue != null ? tooltipValue : displayValue.formatted;
114+
const props = { color, label, isSeriesVisible, legendItemKey, displayValue: display };
107115

108116
return <LegendElement {...props} />;
109117
}

src/components/legend_element.tsx

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ interface LegendElementProps {
2222
color: string | undefined;
2323
label: string | undefined;
2424
isSeriesVisible?: boolean;
25+
displayValue: string;
2526
}
2627

2728
interface LegendElementState {
@@ -50,18 +51,37 @@ class LegendElementComponent extends React.Component<LegendElementProps, LegendE
5051
});
5152
}
5253

54+
renderDisplayValue(displayValue: string, show: boolean) {
55+
if (!show) {
56+
return;
57+
}
58+
59+
return (
60+
<EuiText
61+
size="xs"
62+
className="eui-textTruncate elasticChartsLegendListItem__displayValue"
63+
title={displayValue}
64+
>
65+
{displayValue}
66+
</EuiText>
67+
);
68+
}
69+
5370
render() {
5471
const { legendItemKey } = this.props;
55-
const { color, label, isSeriesVisible } = this.props;
72+
const { color, label, isSeriesVisible, displayValue } = this.props;
5673

5774
const onTitleClick = this.onLegendTitleClick(legendItemKey);
5875

76+
const showLegendDisplayValue = this.props.chartStore!.showLegendDisplayValue.get();
5977
const isSelected = legendItemKey === this.props.chartStore!.selectedLegendItemKey.get();
6078
const titleClassNames = classNames(
79+
'eui-textTruncate',
80+
'elasticChartsLegendListItem__title',
6181
{
6282
['elasticChartsLegendListItem__title--selected']: isSelected,
83+
['elasticChartsLegendListItem__title--hasDisplayValue']: this.props.chartStore!.showLegendDisplayValue.get(),
6384
},
64-
'elasticChartsLegendListItem__title',
6585
);
6686

6787
const colorDotProps = {
@@ -71,6 +91,13 @@ class LegendElementComponent extends React.Component<LegendElementProps, LegendE
7191

7292
const colorDot = <EuiIcon type="dot" {...colorDotProps} />;
7393

94+
const displayValueClassNames = classNames(
95+
'elasticChartsLegendListItem__displayValue',
96+
{
97+
['elasticChartsLegendListItem__displayValue--hidden']: !isSeriesVisible,
98+
},
99+
);
100+
74101
return (
75102
<EuiFlexGroup gutterSize="xs" alignItems="center" responsive={false}>
76103
<EuiFlexItem grow={false}>
@@ -90,11 +117,11 @@ class LegendElementComponent extends React.Component<LegendElementProps, LegendE
90117
<EuiFlexItem grow={false}>
91118
{this.renderVisibilityButton(legendItemKey, isSeriesVisible)}
92119
</EuiFlexItem>
93-
<EuiFlexItem grow={false} className={titleClassNames} onClick={onTitleClick}>
120+
<EuiFlexItem grow={false} onClick={onTitleClick}>
94121
<EuiPopover
95122
id="contentPanel"
96123
button={
97-
<EuiText size="xs" className="eui-textTruncate elasticChartsLegendListItem__title">
124+
<EuiText size="xs" className={titleClassNames}>
98125
{label}
99126
</EuiText>
100127
}
@@ -111,6 +138,9 @@ class LegendElementComponent extends React.Component<LegendElementProps, LegendE
111138
</EuiContextMenuPanel>
112139
</EuiPopover>
113140
</EuiFlexItem>
141+
<EuiFlexItem grow={true} className={displayValueClassNames}>
142+
{this.renderDisplayValue(displayValue, showLegendDisplayValue)}
143+
</EuiFlexItem>
114144
</EuiFlexGroup>
115145
);
116146
}

src/lib/series/legend.test.ts

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { getGroupId, getSpecId, SpecId } from '../utils/ids';
1+
import { AxisId, getAxisId, getGroupId, getSpecId, SpecId } from '../utils/ids';
22
import { ScaleType } from '../utils/scales/scales';
33
import { computeLegend, getSeriesColorLabel } from './legend';
44
import { DataSeriesColorsValues } from './series';
5-
import { BasicSeriesSpec } from './specs';
5+
import { AxisSpec, BasicSeriesSpec, Position } from './specs';
66

77
const colorValues1a = {
88
specId: getSpecId('spec1'),
@@ -46,6 +46,22 @@ const spec2: BasicSeriesSpec = {
4646
hideInLegend: false,
4747
};
4848

49+
const axesSpecs = new Map<AxisId, AxisSpec>();
50+
const axisSpec: AxisSpec = {
51+
id: getAxisId('axis1'),
52+
groupId: getGroupId('group1'),
53+
hide: false,
54+
showOverlappingTicks: false,
55+
showOverlappingLabels: false,
56+
position: Position.Left,
57+
tickSize: 10,
58+
tickPadding: 10,
59+
tickFormat: (value: any) => {
60+
return `${value}`;
61+
},
62+
};
63+
axesSpecs.set(axisSpec.id, axisSpec);
64+
4965
describe('Legends', () => {
5066
const seriesColor = new Map<string, DataSeriesColorsValues>();
5167
const seriesColorMap = new Map<string, string>();
@@ -61,7 +77,7 @@ describe('Legends', () => {
6177
});
6278
it('compute legend for a single series', () => {
6379
seriesColor.set('colorSeries1a', colorValues1a);
64-
const legend = computeLegend(seriesColor, seriesColorMap, specs, 'violet');
80+
const legend = computeLegend(seriesColor, seriesColorMap, specs, 'violet', axesSpecs);
6581
const expected = [
6682
{
6783
color: 'red',
@@ -70,14 +86,15 @@ describe('Legends', () => {
7086
isSeriesVisible: true,
7187
isLegendItemVisible: true,
7288
key: 'colorSeries1a',
89+
displayValue: {},
7390
},
7491
];
7592
expect(Array.from(legend.values())).toEqual(expected);
7693
});
7794
it('compute legend for a single spec but with multiple series', () => {
7895
seriesColor.set('colorSeries1a', colorValues1a);
7996
seriesColor.set('colorSeries1b', colorValues1b);
80-
const legend = computeLegend(seriesColor, seriesColorMap, specs, 'violet');
97+
const legend = computeLegend(seriesColor, seriesColorMap, specs, 'violet', axesSpecs);
8198
const expected = [
8299
{
83100
color: 'red',
@@ -86,6 +103,7 @@ describe('Legends', () => {
86103
isSeriesVisible: true,
87104
isLegendItemVisible: true,
88105
key: 'colorSeries1a',
106+
displayValue: {},
89107
},
90108
{
91109
color: 'blue',
@@ -94,14 +112,15 @@ describe('Legends', () => {
94112
isSeriesVisible: true,
95113
isLegendItemVisible: true,
96114
key: 'colorSeries1b',
115+
displayValue: {},
97116
},
98117
];
99118
expect(Array.from(legend.values())).toEqual(expected);
100119
});
101120
it('compute legend for multiple specs', () => {
102121
seriesColor.set('colorSeries1a', colorValues1a);
103122
seriesColor.set('colorSeries2a', colorValues2a);
104-
const legend = computeLegend(seriesColor, seriesColorMap, specs, 'violet');
123+
const legend = computeLegend(seriesColor, seriesColorMap, specs, 'violet', axesSpecs);
105124
const expected = [
106125
{
107126
color: 'red',
@@ -110,6 +129,7 @@ describe('Legends', () => {
110129
isSeriesVisible: true,
111130
isLegendItemVisible: true,
112131
key: 'colorSeries1a',
132+
displayValue: {},
113133
},
114134
{
115135
color: 'green',
@@ -118,19 +138,20 @@ describe('Legends', () => {
118138
isSeriesVisible: true,
119139
isLegendItemVisible: true,
120140
key: 'colorSeries2a',
141+
displayValue: {},
121142
},
122143
];
123144
expect(Array.from(legend.values())).toEqual(expected);
124145
});
125146
it('empty legend for missing spec', () => {
126147
seriesColor.set('colorSeries2b', colorValues2b);
127-
const legend = computeLegend(seriesColor, seriesColorMap, specs, 'violet');
148+
const legend = computeLegend(seriesColor, seriesColorMap, specs, 'violet', axesSpecs);
128149
expect(legend.size).toEqual(0);
129150
});
130151
it('compute legend with default color for missing series color', () => {
131152
seriesColor.set('colorSeries1a', colorValues1a);
132153
const emptyColorMap = new Map<string, string>();
133-
const legend = computeLegend(seriesColor, emptyColorMap, specs, 'violet');
154+
const legend = computeLegend(seriesColor, emptyColorMap, specs, 'violet', axesSpecs);
134155
const expected = [
135156
{
136157
color: 'violet',
@@ -139,6 +160,7 @@ describe('Legends', () => {
139160
isSeriesVisible: true,
140161
isLegendItemVisible: true,
141162
key: 'colorSeries1a',
163+
displayValue: {},
142164
},
143165
];
144166
expect(Array.from(legend.values())).toEqual(expected);
@@ -152,7 +174,7 @@ describe('Legends', () => {
152174
const emptyColorMap = new Map<string, string>();
153175
const deselectedDataSeries = null;
154176

155-
const legend = computeLegend(seriesColor, emptyColorMap, specs, 'violet', deselectedDataSeries);
177+
const legend = computeLegend(seriesColor, emptyColorMap, specs, 'violet', axesSpecs, deselectedDataSeries);
156178

157179
const visibility = [...legend.values()].map((item) => item.isSeriesVisible);
158180

@@ -167,7 +189,7 @@ describe('Legends', () => {
167189
const emptyColorMap = new Map<string, string>();
168190
const deselectedDataSeries = [colorValues1a, colorValues1b];
169191

170-
const legend = computeLegend(seriesColor, emptyColorMap, specs, 'violet', deselectedDataSeries);
192+
const legend = computeLegend(seriesColor, emptyColorMap, specs, 'violet', axesSpecs, deselectedDataSeries);
171193

172194
const visibility = [...legend.values()].map((item) => item.isSeriesVisible);
173195
expect(visibility).toEqual([false, false, true]);

src/lib/series/legend.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { findDataSeriesByColorValues } from '../../state/utils';
2-
import { SpecId } from '../utils/ids';
3-
import { DataSeriesColorsValues } from './series';
4-
import { BasicSeriesSpec } from './specs';
1+
import { findDataSeriesByColorValues, getAxesSpecForSpecId } from '../../state/utils';
2+
import { identity } from '../utils/commons';
3+
import { AxisId, SpecId } from '../utils/ids';
4+
import { DataSeriesColorsValues, getSortedDataSeriesColorsValuesMap } from './series';
5+
import { AxisSpec, BasicSeriesSpec } from './specs';
56

67
export interface LegendItem {
78
key: string;
@@ -10,16 +11,25 @@ export interface LegendItem {
1011
value: DataSeriesColorsValues;
1112
isSeriesVisible?: boolean;
1213
isLegendItemVisible?: boolean;
14+
displayValue: {
15+
raw: any;
16+
formatted: any;
17+
};
1318
}
19+
1420
export function computeLegend(
1521
seriesColor: Map<string, DataSeriesColorsValues>,
1622
seriesColorMap: Map<string, string>,
1723
specs: Map<SpecId, BasicSeriesSpec>,
1824
defaultColor: string,
25+
axesSpecs: Map<AxisId, AxisSpec>,
1926
deselectedDataSeries?: DataSeriesColorsValues[] | null,
2027
): Map<string, LegendItem> {
2128
const legendItems: Map<string, LegendItem> = new Map();
22-
seriesColor.forEach((series, key) => {
29+
30+
const sortedSeriesColors = getSortedDataSeriesColorsValuesMap(seriesColor);
31+
32+
sortedSeriesColors.forEach((series, key) => {
2333
const spec = specs.get(series.specId);
2434

2535
const color = seriesColorMap.get(key) || defaultColor;
@@ -33,6 +43,10 @@ export function computeLegend(
3343
return;
3444
}
3545

46+
// Use this to get axis spec w/ tick formatter
47+
const { yAxis } = getAxesSpecForSpecId(axesSpecs, spec.groupId);
48+
const formatter = yAxis ? yAxis.tickFormat : identity;
49+
3650
const { hideInLegend } = spec;
3751

3852
legendItems.set(key, {
@@ -42,6 +56,10 @@ export function computeLegend(
4256
value: series,
4357
isSeriesVisible,
4458
isLegendItemVisible: !hideInLegend,
59+
displayValue: {
60+
raw: series.lastValue,
61+
formatted: formatter(series.lastValue),
62+
},
4563
});
4664
});
4765
return legendItems;

0 commit comments

Comments
 (0)