Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(legend/click): add click interations on legend titles #51

Merged
merged 49 commits into from
Mar 6, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
455a033
feat(legend/series): add hover interaction on legend items
emmacunningham Feb 4, 2019
a063717
feat(legend/series): set highlighted legend item to null on mouseout
emmacunningham Feb 4, 2019
94d6508
feat(legend/series): compare highlightedLegendItemIndex with null
emmacunningham Feb 4, 2019
3db7f00
feat(legend/series): add legend hover interaction to line series
emmacunningham Feb 4, 2019
ae5dcaf
feat(legend/series): add legend hover interaction to area chart
emmacunningham Feb 4, 2019
d6d88a9
refactor(geometry): allow all geometries to have specId & seriesKey
emmacunningham Feb 6, 2019
c062b2e
refactor(geometries): share common styles with different geometries
emmacunningham Feb 7, 2019
409097e
refactor(geometry): add GEOMETRY_STYLES constant
emmacunningham Feb 7, 2019
65b59f8
docs(legend): add interaction stories for each chart type
emmacunningham Feb 8, 2019
eec61b3
refactor(legend): move getter to store
emmacunningham Feb 8, 2019
98d5cec
refactor: move shared geometry style to theme
emmacunningham Feb 8, 2019
7d12db7
refactor(legend): use observable for legend item state
emmacunningham Feb 8, 2019
61a2443
refactor: use for loop for series key array comparison
emmacunningham Feb 8, 2019
77e101f
refactor(geometry): move bar hover opacity logic to shared logic
emmacunningham Feb 8, 2019
4e4dafd
refactor: use computed for current highlighted legend item
emmacunningham Feb 8, 2019
26ff575
feat(legend): add mouse over/out listeners from store
emmacunningham Feb 8, 2019
407fc94
refactor: pass only the necessary values to legend mouseover listener
emmacunningham Feb 9, 2019
af649f0
feat(legend): add scaffolding for legend title click interaction
emmacunningham Feb 4, 2019
b929d29
feat(legend): update state store on legend item click
emmacunningham Feb 5, 2019
69c164a
feat(legend/series): filter raw dataseries by current legend item
emmacunningham Feb 8, 2019
d28d3d1
feat(legend): update highlighted and selected dependent on state
emmacunningham Feb 8, 2019
40e6ed5
feat(legend): add style for selected item title
emmacunningham Feb 8, 2019
c5aebdc
fix(legend): remove direct filtering of legend click with data
emmacunningham Feb 8, 2019
e2070ea
refactor(legend): set up legend item click listeners
emmacunningham Feb 8, 2019
08c051f
feat(legend/click): add listener logic and interactions story
emmacunningham Feb 9, 2019
2a696f4
feat(legend): add plus/minus buttons to legend item panel
emmacunningham Feb 9, 2019
34c7602
chore: merge master into feature/legend-click
emmacunningham Feb 14, 2019
bc0eedd
feat(legend): add placeholders for legend panel
emmacunningham Feb 15, 2019
7155647
feat(legend): add legend item +/- listeners
emmacunningham Feb 15, 2019
0e95b6b
chore: merge master into feature/legend-click
emmacunningham Feb 19, 2019
ae2bb81
fix(legend): fix legend item hover style
emmacunningham Feb 19, 2019
0ee4860
fix(legend): fix hover & selected style for legend item title
emmacunningham Feb 19, 2019
44d1da1
feat(series): use selected data series to show/hide series
emmacunningham Feb 19, 2019
99f3793
feat(legend): add color picker state for legend item
emmacunningham Feb 20, 2019
370f8b4
feat(legend/series): toggle visibility of series from legend
emmacunningham Feb 26, 2019
c052f36
feat(legend/series): init selectedDateSeries & set legend item selection
emmacunningham Feb 26, 2019
e252333
feat(legend): set legend item icon based on item status
emmacunningham Feb 26, 2019
5857a63
refactor(legend): change property to better clarify use
emmacunningham Feb 26, 2019
3c081f4
feat(legend/series): toggle individual series visibility
emmacunningham Feb 26, 2019
b60553c
chore: merge branch 'master' into feature/legend-click
emmacunningham Feb 26, 2019
1d9a5d1
test(chart_state): add tests for new legend interactions
emmacunningham Feb 26, 2019
28a811b
feat(legend/series): update series color on color picker change
emmacunningham Feb 27, 2019
82e597e
test(chart_state): test for selecting & customizing data series
emmacunningham Feb 27, 2019
e6ba6ea
test(series): add test for splitting series for only selected
emmacunningham Feb 28, 2019
46a8ea7
fix(series): reset selectedDataSeries on spec update
emmacunningham Feb 28, 2019
4b3fe49
test(specs_parser): add tests for lifecycle methods
emmacunningham Feb 28, 2019
b9d7140
style(legend): remove unused import
emmacunningham Feb 28, 2019
f7f8d93
style(chart_state): add comments around series logic
emmacunningham Mar 1, 2019
58f7e3d
fix(line_geometries): use opacity for spring animation
emmacunningham Mar 1, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/components/_legend.scss
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,24 @@ $elasticChartsLegendMaxHeight: $euiSize * 4;
}

.elasticChartsLegendList__item {
cursor: pointer;

&:hover {
text-decoration: underline;
.elasticChartsLegendListItem__title {
text-decoration: underline;
}
}
}

.elasticChartsLegendListItem__title {
width: $elasticChartsLegendMaxWidth - 4 * $euiSize;
max-width: $elasticChartsLegendMaxWidth - 4 * $euiSize;

&.elasticChartsLegendListItem__title--selected {
.elasticChartsLegendListItem__title {
text-decoration: underline;
}
}
}

.elasticChartsLegend__toggle {
Expand Down
32 changes: 14 additions & 18 deletions src/components/legend.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText } from '@elastic/eui';
import {
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
import classNames from 'classnames';
import { inject, observer } from 'mobx-react';
import React from 'react';
import { isVertical } from '../lib/axes/axis_utils';
import { LegendItem } from '../lib/series/legend';
import { ChartStore } from '../state/chart_state';
import { LegendElement } from './legend_element';

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

const { color, label, isVisible } = item;

return (
<EuiFlexItem {...legendItemProps}>
<LegendElement color={item.color} label={item.label} />
{this.renderLegendElement({ color, label, isVisible }, index)}
</EuiFlexItem>
);
})}
Expand All @@ -93,22 +99,12 @@ class LegendComponent extends React.Component<ReactiveChartProps> {
private onLegendItemMouseout = () => {
this.props.chartStore!.onLegendItemOut();
}
}
function LegendElement({ color, label }: Partial<LegendItem>) {
return (
<EuiFlexGroup gutterSize="xs" alignItems="center" responsive={false}>
<EuiFlexItem grow={false}>
<EuiIcon type="dot" color={color} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexItem grow={true} className="elasticChartsLegendListItem__title" title={label}>
<EuiText size="xs" className="eui-textTruncate">
{label}
</EuiText>
</EuiFlexItem>
</EuiFlexItem>
</EuiFlexGroup>
);

private renderLegendElement = ({ color, label, isVisible }: Partial<LegendItem>, legendItemIndex: number) => {
const props = { color, label, isVisible, index: legendItemIndex };

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

export const Legend = inject('chartStore')(observer(LegendComponent));
168 changes: 168 additions & 0 deletions src/components/legend_element.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import {
EuiButtonIcon,
// TODO: remove ts-ignore below once typings file is included in eui for color picker
// @ts-ignore
EuiColorPicker,
EuiContextMenuPanel,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiPopover,
EuiText,
} from '@elastic/eui';
import classNames from 'classnames';
import { inject, observer } from 'mobx-react';
import React from 'react';

import { ChartStore } from '../state/chart_state';

interface LegendElementProps {
chartStore?: ChartStore; // FIX until we find a better way on ts mobx
index: number;
color: string | undefined;
label: string | undefined;
isVisible?: boolean;
}

interface LegendElementState {
isColorPickerOpen: boolean;
}

class LegendElementComponent extends React.Component<LegendElementProps, LegendElementState> {
static displayName = 'LegendElement';

constructor(props: LegendElementProps) {
super(props);
this.state = {
isColorPickerOpen: false,
};
}

closeColorPicker = () => {
this.setState({
isColorPickerOpen: false,
});
}

toggleColorPicker = () => {
this.setState({
isColorPickerOpen: !this.state.isColorPickerOpen,
});
}

render() {
const legendItemIndex = this.props.index;
const { color, label, isVisible } = this.props;

const onTitleClick = this.onLegendTitleClick(legendItemIndex);

const isSelected = legendItemIndex === this.props.chartStore!.selectedLegendItemIndex.get();
const titleClassNames = classNames({
['elasticChartsLegendListItem__title--selected']: isSelected,
}, 'elasticChartsLegendListItem__title');

const colorDotProps = {
color,
onClick: this.toggleColorPicker,
};

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

return (
<EuiFlexGroup gutterSize="xs" alignItems="center" responsive={false}>
<EuiFlexItem grow={false}>
<EuiPopover
id="legendItemColorPicker"
button={colorDot}
isOpen={this.state.isColorPickerOpen}
closePopover={this.closeColorPicker}
panelPaddingSize="s"
anchorPosition="downCenter"
>
<EuiContextMenuPanel>
<EuiColorPicker onChange={this.onColorPickerChange(legendItemIndex)} color={color} />
</EuiContextMenuPanel>
</EuiPopover>
</EuiFlexItem>
<EuiFlexItem grow={false}>
{this.renderVisibilityButton(legendItemIndex, isVisible)}
</EuiFlexItem>
<EuiFlexItem grow={false} className={titleClassNames} onClick={onTitleClick}>
<EuiPopover
id="contentPanel"
button={(<EuiText size="xs" className="eui-textTruncate elasticChartsLegendListItem__title">
{label}
</EuiText>)
}
isOpen={isSelected}
closePopover={this.onLegendItemPanelClose}
panelPaddingSize="s"
anchorPosition="downCenter"
>
<EuiContextMenuPanel>
<EuiFlexGroup gutterSize="xs" alignItems="center" responsive={false}>
<EuiFlexItem>
{this.renderPlusButton()}
</EuiFlexItem>
<EuiFlexItem>
{this.renderMinusButton()}
</EuiFlexItem>
</EuiFlexGroup>
</EuiContextMenuPanel>
</EuiPopover>
</EuiFlexItem>
</EuiFlexGroup>
);
}

private onLegendTitleClick = (legendItemIndex: number) => () => {
this.props.chartStore!.onLegendItemClick(legendItemIndex);
}

private onLegendItemPanelClose = () => {
// tslint:disable-next-line:no-console
console.log('close');
}

private onColorPickerChange = (legendItemIndex: number) => (color: string) => {
this.props.chartStore!.setSeriesColor(legendItemIndex, color);
}

private renderPlusButton = () => {
return (
<EuiButtonIcon
onClick={this.props.chartStore!.onLegendItemPlusClick}
iconType="plusInCircle"
aria-label="minus"
/>);
}

private renderMinusButton = () => {
return (
<EuiButtonIcon
onClick={this.props.chartStore!.onLegendItemMinusClick}
iconType="minusInCircle"
aria-label="minus"
/>);
}

private onVisibilityClick = (legendItemIndex: number) => (event: React.MouseEvent<HTMLElement>) => {
if (event.shiftKey) {
this.props.chartStore!.toggleSingleSeries(legendItemIndex);
} else {
this.props.chartStore!.toggleSeriesVisibility(legendItemIndex);
}
}

private renderVisibilityButton = (legendItemIndex: number, isVisible: boolean = true) => {
const iconType = isVisible ? 'eye' : 'eyeClosed';

return <EuiButtonIcon
onClick={this.onVisibilityClick(legendItemIndex)}
iconType={iconType}
aria-label="toggle visibility"
/>;
}
}

export const LegendElement = inject('chartStore')(observer(LegendElementComponent));
12 changes: 7 additions & 5 deletions src/components/react_canvas/line_geometries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ interface LineGeometriesDataState {
export class LineGeometries extends React.PureComponent<
LineGeometriesDataProps,
LineGeometriesDataState
> {
> {
static defaultProps: Partial<LineGeometriesDataProps> = {
animated: false,
};
Expand All @@ -41,6 +41,7 @@ export class LineGeometries extends React.PureComponent<
overPoint: undefined,
};
}

render() {
return (
<Group ref={this.barSeriesRef} key={'bar_series'}>
Expand Down Expand Up @@ -153,11 +154,12 @@ export class LineGeometries extends React.PureComponent<
if (this.props.animated) {
return (
<Group key={i} x={transform.x}>
<Spring native from={{ line }} to={{ line }}>
{(props: { line: string }) => (
<Spring native reset from={{ opacity: 0 }} to={{ opacity: 1 }}>
{(props: { opacity: number }) => (
<animated.Path
opacity={props.opacity}
key="line"
data={props.line}
data={line}
strokeWidth={strokeWidth}
stroke={color}
listening={false}
Expand All @@ -172,7 +174,7 @@ export class LineGeometries extends React.PureComponent<
} else {
return (
<Path
key="line"
key={i}
data={line}
strokeWidth={strokeWidth}
stroke={color}
Expand Down
41 changes: 35 additions & 6 deletions src/lib/series/legend.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ describe('Legends', () => {
seriesColor.set('colorSeries1a', colorValues1a);
const legend = computeLegend(seriesColor, seriesColorMap, specs, 'violet');
const expected = [
{ color: 'red', label: 'spec1', value: { colorValues: [], specId: 'spec1' } },
{ color: 'red', label: 'spec1', value: { colorValues: [], specId: 'spec1' }, isVisible: true },
];
expect(legend).toEqual(expected);
});
Expand All @@ -69,8 +69,8 @@ describe('Legends', () => {
seriesColor.set('colorSeries1b', colorValues1b);
const legend = computeLegend(seriesColor, seriesColorMap, specs, 'violet');
const expected = [
{ color: 'red', label: 'spec1', value: { colorValues: [], specId: 'spec1' } },
{ color: 'blue', label: 'a - b', value: { colorValues: ['a', 'b'], specId: 'spec1' } },
{ color: 'red', label: 'spec1', value: { colorValues: [], specId: 'spec1' }, isVisible: true },
{ color: 'blue', label: 'a - b', value: { colorValues: ['a', 'b'], specId: 'spec1' }, isVisible: true },
];
expect(legend).toEqual(expected);
});
Expand All @@ -79,8 +79,8 @@ describe('Legends', () => {
seriesColor.set('colorSeries2a', colorValues2a);
const legend = computeLegend(seriesColor, seriesColorMap, specs, 'violet');
const expected = [
{ color: 'red', label: 'spec1', value: { colorValues: [], specId: 'spec1' } },
{ color: 'green', label: 'spec2', value: { colorValues: [], specId: 'spec2' } },
{ color: 'red', label: 'spec1', value: { colorValues: [], specId: 'spec1' }, isVisible: true },
{ color: 'green', label: 'spec2', value: { colorValues: [], specId: 'spec2' }, isVisible: true },
];
expect(legend).toEqual(expected);
});
Expand All @@ -94,8 +94,37 @@ describe('Legends', () => {
const emptyColorMap = new Map<string, string>();
const legend = computeLegend(seriesColor, emptyColorMap, specs, 'violet');
const expected = [
{ color: 'violet', label: 'spec1', value: { colorValues: [], specId: 'spec1' } },
{ color: 'violet', label: 'spec1', value: { colorValues: [], specId: 'spec1' }, isVisible: true },
];
expect(legend).toEqual(expected);
});
it('sets all series legend items to visible when selectedDataSeries is null', () => {
seriesColor.set('colorSeries1a', colorValues1a);
seriesColor.set('colorSeries1b', colorValues1b);
seriesColor.set('colorSeries2a', colorValues2a);
seriesColor.set('colorSeries2b', colorValues2b);

const emptyColorMap = new Map<string, string>();
const selectedDataSeries = null;

const legend = computeLegend(seriesColor, emptyColorMap, specs, 'violet', selectedDataSeries);

const visibility = legend.map((item) => item.isVisible);

expect(visibility).toEqual([true, true, true, true]);
});
it('selectively sets series to visible when there are selectedDataSeries items', () => {
seriesColor.set('colorSeries1a', colorValues1a);
seriesColor.set('colorSeries1b', colorValues1b);
seriesColor.set('colorSeries2a', colorValues2a);
seriesColor.set('colorSeries2b', colorValues2b);

const emptyColorMap = new Map<string, string>();
const selectedDataSeries = [colorValues1a, colorValues1b];

const legend = computeLegend(seriesColor, emptyColorMap, specs, 'violet', selectedDataSeries);

const visibility = legend.map((item) => item.isVisible);
expect(visibility).toEqual([true, true, false, false]);
});
});
Loading