Skip to content

Commit 7d6139d

Browse files
feat(legend/click): add click interations on legend titles (#51)
1 parent 2e4a2f0 commit 7d6139d

File tree

17 files changed

+878
-51
lines changed

17 files changed

+878
-51
lines changed

src/components/_legend.scss

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,14 +90,24 @@ $elasticChartsLegendMaxHeight: $euiSize * 4;
9090
}
9191

9292
.elasticChartsLegendList__item {
93+
cursor: pointer;
94+
9395
&:hover {
94-
text-decoration: underline;
96+
.elasticChartsLegendListItem__title {
97+
text-decoration: underline;
98+
}
9599
}
96100
}
97101

98102
.elasticChartsLegendListItem__title {
99103
width: $elasticChartsLegendMaxWidth - 4 * $euiSize;
100104
max-width: $elasticChartsLegendMaxWidth - 4 * $euiSize;
105+
106+
&.elasticChartsLegendListItem__title--selected {
107+
.elasticChartsLegendListItem__title {
108+
text-decoration: underline;
109+
}
110+
}
101111
}
102112

103113
.elasticChartsLegend__toggle {

src/components/legend.tsx

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1-
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText } from '@elastic/eui';
1+
import {
2+
EuiFlexGroup,
3+
EuiFlexItem,
4+
} from '@elastic/eui';
25
import classNames from 'classnames';
36
import { inject, observer } from 'mobx-react';
47
import React from 'react';
58
import { isVertical } from '../lib/axes/axis_utils';
69
import { LegendItem } from '../lib/series/legend';
710
import { ChartStore } from '../state/chart_state';
11+
import { LegendElement } from './legend_element';
812

913
interface ReactiveChartProps {
1014
chartStore?: ChartStore; // FIX until we find a better way on ts mobx
@@ -74,9 +78,11 @@ class LegendComponent extends React.Component<ReactiveChartProps> {
7478
onMouseLeave: this.onLegendItemMouseout,
7579
};
7680

81+
const { color, label, isVisible } = item;
82+
7783
return (
7884
<EuiFlexItem {...legendItemProps}>
79-
<LegendElement color={item.color} label={item.label} />
85+
{this.renderLegendElement({ color, label, isVisible }, index)}
8086
</EuiFlexItem>
8187
);
8288
})}
@@ -93,22 +99,12 @@ class LegendComponent extends React.Component<ReactiveChartProps> {
9399
private onLegendItemMouseout = () => {
94100
this.props.chartStore!.onLegendItemOut();
95101
}
96-
}
97-
function LegendElement({ color, label }: Partial<LegendItem>) {
98-
return (
99-
<EuiFlexGroup gutterSize="xs" alignItems="center" responsive={false}>
100-
<EuiFlexItem grow={false}>
101-
<EuiIcon type="dot" color={color} />
102-
</EuiFlexItem>
103-
<EuiFlexItem grow={false}>
104-
<EuiFlexItem grow={true} className="elasticChartsLegendListItem__title" title={label}>
105-
<EuiText size="xs" className="eui-textTruncate">
106-
{label}
107-
</EuiText>
108-
</EuiFlexItem>
109-
</EuiFlexItem>
110-
</EuiFlexGroup>
111-
);
102+
103+
private renderLegendElement = ({ color, label, isVisible }: Partial<LegendItem>, legendItemIndex: number) => {
104+
const props = { color, label, isVisible, index: legendItemIndex };
105+
106+
return <LegendElement {...props} />;
107+
}
112108
}
113109

114110
export const Legend = inject('chartStore')(observer(LegendComponent));

src/components/legend_element.tsx

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import {
2+
EuiButtonIcon,
3+
// TODO: remove ts-ignore below once typings file is included in eui for color picker
4+
// @ts-ignore
5+
EuiColorPicker,
6+
EuiContextMenuPanel,
7+
EuiFlexGroup,
8+
EuiFlexItem,
9+
EuiIcon,
10+
EuiPopover,
11+
EuiText,
12+
} from '@elastic/eui';
13+
import classNames from 'classnames';
14+
import { inject, observer } from 'mobx-react';
15+
import React from 'react';
16+
17+
import { ChartStore } from '../state/chart_state';
18+
19+
interface LegendElementProps {
20+
chartStore?: ChartStore; // FIX until we find a better way on ts mobx
21+
index: number;
22+
color: string | undefined;
23+
label: string | undefined;
24+
isVisible?: boolean;
25+
}
26+
27+
interface LegendElementState {
28+
isColorPickerOpen: boolean;
29+
}
30+
31+
class LegendElementComponent extends React.Component<LegendElementProps, LegendElementState> {
32+
static displayName = 'LegendElement';
33+
34+
constructor(props: LegendElementProps) {
35+
super(props);
36+
this.state = {
37+
isColorPickerOpen: false,
38+
};
39+
}
40+
41+
closeColorPicker = () => {
42+
this.setState({
43+
isColorPickerOpen: false,
44+
});
45+
}
46+
47+
toggleColorPicker = () => {
48+
this.setState({
49+
isColorPickerOpen: !this.state.isColorPickerOpen,
50+
});
51+
}
52+
53+
render() {
54+
const legendItemIndex = this.props.index;
55+
const { color, label, isVisible } = this.props;
56+
57+
const onTitleClick = this.onLegendTitleClick(legendItemIndex);
58+
59+
const isSelected = legendItemIndex === this.props.chartStore!.selectedLegendItemIndex.get();
60+
const titleClassNames = classNames({
61+
['elasticChartsLegendListItem__title--selected']: isSelected,
62+
}, 'elasticChartsLegendListItem__title');
63+
64+
const colorDotProps = {
65+
color,
66+
onClick: this.toggleColorPicker,
67+
};
68+
69+
const colorDot = <EuiIcon type="dot" {...colorDotProps} />;
70+
71+
return (
72+
<EuiFlexGroup gutterSize="xs" alignItems="center" responsive={false}>
73+
<EuiFlexItem grow={false}>
74+
<EuiPopover
75+
id="legendItemColorPicker"
76+
button={colorDot}
77+
isOpen={this.state.isColorPickerOpen}
78+
closePopover={this.closeColorPicker}
79+
panelPaddingSize="s"
80+
anchorPosition="downCenter"
81+
>
82+
<EuiContextMenuPanel>
83+
<EuiColorPicker onChange={this.onColorPickerChange(legendItemIndex)} color={color} />
84+
</EuiContextMenuPanel>
85+
</EuiPopover>
86+
</EuiFlexItem>
87+
<EuiFlexItem grow={false}>
88+
{this.renderVisibilityButton(legendItemIndex, isVisible)}
89+
</EuiFlexItem>
90+
<EuiFlexItem grow={false} className={titleClassNames} onClick={onTitleClick}>
91+
<EuiPopover
92+
id="contentPanel"
93+
button={(<EuiText size="xs" className="eui-textTruncate elasticChartsLegendListItem__title">
94+
{label}
95+
</EuiText>)
96+
}
97+
isOpen={isSelected}
98+
closePopover={this.onLegendItemPanelClose}
99+
panelPaddingSize="s"
100+
anchorPosition="downCenter"
101+
>
102+
<EuiContextMenuPanel>
103+
<EuiFlexGroup gutterSize="xs" alignItems="center" responsive={false}>
104+
<EuiFlexItem>
105+
{this.renderPlusButton()}
106+
</EuiFlexItem>
107+
<EuiFlexItem>
108+
{this.renderMinusButton()}
109+
</EuiFlexItem>
110+
</EuiFlexGroup>
111+
</EuiContextMenuPanel>
112+
</EuiPopover>
113+
</EuiFlexItem>
114+
</EuiFlexGroup>
115+
);
116+
}
117+
118+
private onLegendTitleClick = (legendItemIndex: number) => () => {
119+
this.props.chartStore!.onLegendItemClick(legendItemIndex);
120+
}
121+
122+
private onLegendItemPanelClose = () => {
123+
// tslint:disable-next-line:no-console
124+
console.log('close');
125+
}
126+
127+
private onColorPickerChange = (legendItemIndex: number) => (color: string) => {
128+
this.props.chartStore!.setSeriesColor(legendItemIndex, color);
129+
}
130+
131+
private renderPlusButton = () => {
132+
return (
133+
<EuiButtonIcon
134+
onClick={this.props.chartStore!.onLegendItemPlusClick}
135+
iconType="plusInCircle"
136+
aria-label="minus"
137+
/>);
138+
}
139+
140+
private renderMinusButton = () => {
141+
return (
142+
<EuiButtonIcon
143+
onClick={this.props.chartStore!.onLegendItemMinusClick}
144+
iconType="minusInCircle"
145+
aria-label="minus"
146+
/>);
147+
}
148+
149+
private onVisibilityClick = (legendItemIndex: number) => (event: React.MouseEvent<HTMLElement>) => {
150+
if (event.shiftKey) {
151+
this.props.chartStore!.toggleSingleSeries(legendItemIndex);
152+
} else {
153+
this.props.chartStore!.toggleSeriesVisibility(legendItemIndex);
154+
}
155+
}
156+
157+
private renderVisibilityButton = (legendItemIndex: number, isVisible: boolean = true) => {
158+
const iconType = isVisible ? 'eye' : 'eyeClosed';
159+
160+
return <EuiButtonIcon
161+
onClick={this.onVisibilityClick(legendItemIndex)}
162+
iconType={iconType}
163+
aria-label="toggle visibility"
164+
/>;
165+
}
166+
}
167+
168+
export const LegendElement = inject('chartStore')(observer(LegendElementComponent));

src/components/react_canvas/line_geometries.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ interface LineGeometriesDataState {
2929
export class LineGeometries extends React.PureComponent<
3030
LineGeometriesDataProps,
3131
LineGeometriesDataState
32-
> {
32+
> {
3333
static defaultProps: Partial<LineGeometriesDataProps> = {
3434
animated: false,
3535
};
@@ -41,6 +41,7 @@ export class LineGeometries extends React.PureComponent<
4141
overPoint: undefined,
4242
};
4343
}
44+
4445
render() {
4546
return (
4647
<Group ref={this.barSeriesRef} key={'bar_series'}>
@@ -153,11 +154,12 @@ export class LineGeometries extends React.PureComponent<
153154
if (this.props.animated) {
154155
return (
155156
<Group key={i} x={transform.x}>
156-
<Spring native from={{ line }} to={{ line }}>
157-
{(props: { line: string }) => (
157+
<Spring native reset from={{ opacity: 0 }} to={{ opacity: 1 }}>
158+
{(props: { opacity: number }) => (
158159
<animated.Path
160+
opacity={props.opacity}
159161
key="line"
160-
data={props.line}
162+
data={line}
161163
strokeWidth={strokeWidth}
162164
stroke={color}
163165
listening={false}
@@ -172,7 +174,7 @@ export class LineGeometries extends React.PureComponent<
172174
} else {
173175
return (
174176
<Path
175-
key="line"
177+
key={i}
176178
data={line}
177179
strokeWidth={strokeWidth}
178180
stroke={color}

src/lib/series/legend.test.ts

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ describe('Legends', () => {
6060
seriesColor.set('colorSeries1a', colorValues1a);
6161
const legend = computeLegend(seriesColor, seriesColorMap, specs, 'violet');
6262
const expected = [
63-
{ color: 'red', label: 'spec1', value: { colorValues: [], specId: 'spec1' } },
63+
{ color: 'red', label: 'spec1', value: { colorValues: [], specId: 'spec1' }, isVisible: true },
6464
];
6565
expect(legend).toEqual(expected);
6666
});
@@ -69,8 +69,8 @@ describe('Legends', () => {
6969
seriesColor.set('colorSeries1b', colorValues1b);
7070
const legend = computeLegend(seriesColor, seriesColorMap, specs, 'violet');
7171
const expected = [
72-
{ color: 'red', label: 'spec1', value: { colorValues: [], specId: 'spec1' } },
73-
{ color: 'blue', label: 'a - b', value: { colorValues: ['a', 'b'], specId: 'spec1' } },
72+
{ color: 'red', label: 'spec1', value: { colorValues: [], specId: 'spec1' }, isVisible: true },
73+
{ color: 'blue', label: 'a - b', value: { colorValues: ['a', 'b'], specId: 'spec1' }, isVisible: true },
7474
];
7575
expect(legend).toEqual(expected);
7676
});
@@ -79,8 +79,8 @@ describe('Legends', () => {
7979
seriesColor.set('colorSeries2a', colorValues2a);
8080
const legend = computeLegend(seriesColor, seriesColorMap, specs, 'violet');
8181
const expected = [
82-
{ color: 'red', label: 'spec1', value: { colorValues: [], specId: 'spec1' } },
83-
{ color: 'green', label: 'spec2', value: { colorValues: [], specId: 'spec2' } },
82+
{ color: 'red', label: 'spec1', value: { colorValues: [], specId: 'spec1' }, isVisible: true },
83+
{ color: 'green', label: 'spec2', value: { colorValues: [], specId: 'spec2' }, isVisible: true },
8484
];
8585
expect(legend).toEqual(expected);
8686
});
@@ -94,8 +94,37 @@ describe('Legends', () => {
9494
const emptyColorMap = new Map<string, string>();
9595
const legend = computeLegend(seriesColor, emptyColorMap, specs, 'violet');
9696
const expected = [
97-
{ color: 'violet', label: 'spec1', value: { colorValues: [], specId: 'spec1' } },
97+
{ color: 'violet', label: 'spec1', value: { colorValues: [], specId: 'spec1' }, isVisible: true },
9898
];
9999
expect(legend).toEqual(expected);
100100
});
101+
it('sets all series legend items to visible when selectedDataSeries is null', () => {
102+
seriesColor.set('colorSeries1a', colorValues1a);
103+
seriesColor.set('colorSeries1b', colorValues1b);
104+
seriesColor.set('colorSeries2a', colorValues2a);
105+
seriesColor.set('colorSeries2b', colorValues2b);
106+
107+
const emptyColorMap = new Map<string, string>();
108+
const selectedDataSeries = null;
109+
110+
const legend = computeLegend(seriesColor, emptyColorMap, specs, 'violet', selectedDataSeries);
111+
112+
const visibility = legend.map((item) => item.isVisible);
113+
114+
expect(visibility).toEqual([true, true, true, true]);
115+
});
116+
it('selectively sets series to visible when there are selectedDataSeries items', () => {
117+
seriesColor.set('colorSeries1a', colorValues1a);
118+
seriesColor.set('colorSeries1b', colorValues1b);
119+
seriesColor.set('colorSeries2a', colorValues2a);
120+
seriesColor.set('colorSeries2b', colorValues2b);
121+
122+
const emptyColorMap = new Map<string, string>();
123+
const selectedDataSeries = [colorValues1a, colorValues1b];
124+
125+
const legend = computeLegend(seriesColor, emptyColorMap, specs, 'violet', selectedDataSeries);
126+
127+
const visibility = legend.map((item) => item.isVisible);
128+
expect(visibility).toEqual([true, true, false, false]);
129+
});
101130
});

0 commit comments

Comments
 (0)