Skip to content

Commit

Permalink
feat(legend/series): add hover interaction on legend items
Browse files Browse the repository at this point in the history
addresses elastic#24
  • Loading branch information
emmacunningham committed Feb 8, 2019
1 parent c94dd22 commit 455a033
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 6 deletions.
12 changes: 11 additions & 1 deletion src/components/legend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,14 @@ class LegendComponent extends React.Component<ReactiveChartProps> {
responsive={false}
>
{legendItems.map((item, index) => {
const legendItemProps = {
key: index,
className: 'euiChartLegendList__item',
onMouseOver: this.onLegendItemMouseover(index),
};

return (
<EuiFlexItem key={index} className="euiChartLegendList__item">
<EuiFlexItem {...legendItemProps}>
<LegendElement color={item.color} label={item.label} />
</EuiFlexItem>
);
Expand All @@ -97,6 +103,10 @@ class LegendComponent extends React.Component<ReactiveChartProps> {
</div>
);
}

private onLegendItemMouseover = (legendItemIndex: number) => () => {
this.props.chartStore!.updateHighlightedLegendItem(legendItemIndex);
}
}
function LegendElement({ color, label }: Partial<LegendItem>) {
return (
Expand Down
33 changes: 28 additions & 5 deletions src/components/react_canvas/bar_geometries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { IAction } from 'mobx';
import React from 'react';
import { Group, Rect } from 'react-konva';
import { animated, Spring } from 'react-spring/konva';
import { LegendItem } from '../../lib/series/legend';
import { BarGeometry, GeometryValue } from '../../lib/series/rendering';
import { belongsToDataSeries } from '../../lib/series/series_utils';
import { ElementClickListener, TooltipData } from '../../state/chart_state';

interface BarGeometriesDataProps {
Expand All @@ -12,14 +14,15 @@ interface BarGeometriesDataProps {
onElementClick?: ElementClickListener;
onElementOver: ((tooltip: TooltipData) => void) & IAction;
onElementOut: (() => void) & IAction;
highlightedLegendItem: LegendItem | null;
}
interface BarGeometriesDataState {
overBar?: BarGeometry;
}
export class BarGeometries extends React.PureComponent<
BarGeometriesDataProps,
BarGeometriesDataState
> {
> {
static defaultProps: Partial<BarGeometriesDataProps> = {
animated: false,
};
Expand Down Expand Up @@ -70,14 +73,34 @@ export class BarGeometries extends React.PureComponent<
});
onElementOut();
}

private computeBarOpacity = (bar: BarGeometry, overBar: BarGeometry | undefined): number => {
const { highlightedLegendItem } = this.props;

// There are two elements that might be hovered over that could affect this:
// a specific bar element or a legend item; thus, we handle these states as mutually exclusive.
if (overBar) {
if (overBar !== bar) {
return 0.6;
}
return 1;
} else if (highlightedLegendItem != null) {
const isPartOfHighlightedSeries = belongsToDataSeries(bar.value, highlightedLegendItem.value);

if (isPartOfHighlightedSeries) {
return 1;
}

return 0.6;
}
return 1;
}

private renderBarGeoms = (bars: BarGeometry[]): JSX.Element[] => {
const { overBar } = this.state;
return bars.map((bar, i) => {
const { x, y, width, height, color, value } = bar;
let opacity = 1;
if (overBar && overBar !== bar) {
opacity = 0.6;
}
const opacity = this.computeBarOpacity(bar, overBar);
if (this.props.animated) {
return (
<Group key={i}>
Expand Down
5 changes: 5 additions & 0 deletions src/components/react_canvas/reactive_chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,17 +67,22 @@ class Chart extends React.Component<ReactiveChartProps, ReactiveChartState> {
onOverElement,
onOutElement,
onElementClickListener,
highlightedLegendItemIndex,
legendItems,
} = this.props.chartStore!;
if (!geometries) {
return;
}
const highlightedLegendItem = highlightedLegendItemIndex ? legendItems[highlightedLegendItemIndex] : null;

return (
<BarGeometries
animated={canDataBeAnimated}
bars={geometries.bars}
onElementOver={onOverElement}
onElementOut={onOutElement}
onElementClick={onElementClickListener}
highlightedLegendItem={highlightedLegendItem}
/>
);
}
Expand Down
47 changes: 47 additions & 0 deletions src/lib/series/series_utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { getSpecId } from '../utils/ids';
import { GeometryValue } from './rendering';
import { DataSeriesColorsValues } from './series';
import { belongsToDataSeries, isEqualSeriesKey } from './series_utils';

describe('Series utility functions', () => {
test('can compare series keys for identity', () => {
const seriesKeyA = ['a', 'b', 'c'];
const seriesKeyB = ['a', 'b', 'c'];
const seriesKeyC = ['a', 'b', 'd'];
const seriesKeyD = ['d'];
const seriesKeyE = ['b', 'a', 'c'];

expect(isEqualSeriesKey(seriesKeyA, seriesKeyB)).toBe(true);
expect(isEqualSeriesKey(seriesKeyB, seriesKeyC)).toBe(false);
expect(isEqualSeriesKey(seriesKeyA, seriesKeyD)).toBe(false);
expect(isEqualSeriesKey(seriesKeyA, seriesKeyE)).toBe(false);
expect(isEqualSeriesKey(seriesKeyA, [])).toBe(false);
});

test('can determine if a geometry value belongs to a data series', () => {
const geometryValueA: GeometryValue = {
specId: getSpecId('a'),
datum: null,
seriesKey: ['a', 'b', 'c'],
};

const dataSeriesValuesA: DataSeriesColorsValues = {
specId: getSpecId('a'),
colorValues: ['a', 'b', 'c'],
};

const dataSeriesValuesB: DataSeriesColorsValues = {
specId: getSpecId('b'),
colorValues: ['a', 'b', 'c'],
};

const dataSeriesValuesC: DataSeriesColorsValues = {
specId: getSpecId('a'),
colorValues: ['a', 'b', 'd'],
};

expect(belongsToDataSeries(geometryValueA, dataSeriesValuesA)).toBe(true);
expect(belongsToDataSeries(geometryValueA, dataSeriesValuesB)).toBe(false);
expect(belongsToDataSeries(geometryValueA, dataSeriesValuesC)).toBe(false);
});
});
31 changes: 31 additions & 0 deletions src/lib/series/series_utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { GeometryValue } from './rendering';
import { DataSeriesColorsValues } from './series';

export function isEqualSeriesKey(a: any[], b: any[]): boolean {
if (a.length !== b.length) {
return false;
}

let ret = true;

a.forEach((aVal: any, idx: number) => {
if (aVal !== b[idx]) {
ret = false;
}
});

return ret;
}

export function belongsToDataSeries(geometryValue: GeometryValue, dataSeriesValues: DataSeriesColorsValues): boolean {
const legendItemSeriesKey = dataSeriesValues.colorValues;
const legendItemSpecId = dataSeriesValues.specId;

const geometrySeriesKey = geometryValue.seriesKey;
const geometrySpecId = geometryValue.specId;

const hasSameSpecId = legendItemSpecId === geometrySpecId;
const hasSameSeriesKey = isEqualSeriesKey(legendItemSeriesKey, geometrySeriesKey);

return hasSameSpecId && hasSameSeriesKey;
}
8 changes: 8 additions & 0 deletions src/state/chart_state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ export class ChartStore {
yScales?: Map<GroupId, Scale>;

legendItems: LegendItem[] = [];
highlightedLegendItemIndex: number | null = null;

tooltipData = observable.box<Array<[any, any]> | null>(null);
tooltipPosition = observable.box<{ x: number; y: number } | null>();
Expand Down Expand Up @@ -229,6 +230,13 @@ export class ChartStore {
return this.xScale.type !== ScaleType.Ordinal && Boolean(this.onBrushEndListener);
}

updateHighlightedLegendItem(legendItemIndex: number) {
if (legendItemIndex !== this.highlightedLegendItemIndex) {
this.highlightedLegendItemIndex = legendItemIndex;
this.computeChart();
}
}

updateParentDimensions(width: number, height: number, top: number, left: number) {
let isChanged = false;
if (width !== this.parentDimensions.width) {
Expand Down

0 comments on commit 455a033

Please sign in to comment.