Skip to content

Commit

Permalink
feat(legend): display series value (dependent on hover) & sort in leg…
Browse files Browse the repository at this point in the history
…end (#155)
  • Loading branch information
emmacunningham authored Apr 10, 2019
1 parent 3221b14 commit 78af858
Show file tree
Hide file tree
Showing 14 changed files with 365 additions and 31 deletions.
19 changes: 16 additions & 3 deletions src/components/_legend.scss
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,13 @@ $elasticChartsLegendMaxHeight: $euiSize * 4;
overflow: hidden;
flex-shrink: 1;
flex-grow: 0;
max-width: 100%;
}
.elasticChartsLegendList {
overflow-y: auto;
overflow-x: hidden;
height: 100%;
max-width: 100%;
@include euiScrollBar;
}

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

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

&.elasticChartsLegendListItem__title--hasDisplayValue {
width: $elasticChartsLegendMaxWidth - 6 * $euiSize;
max-width: $elasticChartsLegendMaxWidth - 6 * $euiSize;
}
}

.elasticChartsLegendListItem__displayValue {
text-align: right;

&.elasticChartsLegendListItem__displayValue--hidden {
display: none;
}
}

Expand Down
16 changes: 12 additions & 4 deletions src/components/legend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ class LegendComponent extends React.Component<ReactiveChartProps> {
responsive={false}
>
{[...legendItems.values()].map((item) => {
const { color, label, isSeriesVisible, isLegendItemVisible } = item;
const { isLegendItemVisible } = item;

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

return (
<EuiFlexItem {...legendItemProps}>
{this.renderLegendElement({ color, label, isSeriesVisible }, item.key)}
{this.renderLegendElement(item, item.key)}
</EuiFlexItem>
);
})}
Expand All @@ -100,10 +100,18 @@ class LegendComponent extends React.Component<ReactiveChartProps> {
}

private renderLegendElement = (
{ color, label, isSeriesVisible }: Partial<LegendItem>,
{ color, label, isSeriesVisible, displayValue }: LegendItem,
legendItemKey: string,
) => {
const props = { color, label, isSeriesVisible, legendItemKey };
const tooltipValues = this.props.chartStore!.legendItemTooltipValues.get();
let tooltipValue;

if (tooltipValues && tooltipValues.get(legendItemKey)) {
tooltipValue = tooltipValues.get(legendItemKey);
}

const display = tooltipValue != null ? tooltipValue : displayValue.formatted;
const props = { color, label, isSeriesVisible, legendItemKey, displayValue: display };

return <LegendElement {...props} />;
}
Expand Down
38 changes: 34 additions & 4 deletions src/components/legend_element.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ interface LegendElementProps {
color: string | undefined;
label: string | undefined;
isSeriesVisible?: boolean;
displayValue: string;
}

interface LegendElementState {
Expand Down Expand Up @@ -50,18 +51,37 @@ class LegendElementComponent extends React.Component<LegendElementProps, LegendE
});
}

renderDisplayValue(displayValue: string, show: boolean) {
if (!show) {
return;
}

return (
<EuiText
size="xs"
className="eui-textTruncate elasticChartsLegendListItem__displayValue"
title={displayValue}
>
{displayValue}
</EuiText>
);
}

render() {
const { legendItemKey } = this.props;
const { color, label, isSeriesVisible } = this.props;
const { color, label, isSeriesVisible, displayValue } = this.props;

const onTitleClick = this.onLegendTitleClick(legendItemKey);

const showLegendDisplayValue = this.props.chartStore!.showLegendDisplayValue.get();
const isSelected = legendItemKey === this.props.chartStore!.selectedLegendItemKey.get();
const titleClassNames = classNames(
'eui-textTruncate',
'elasticChartsLegendListItem__title',
{
['elasticChartsLegendListItem__title--selected']: isSelected,
['elasticChartsLegendListItem__title--hasDisplayValue']: this.props.chartStore!.showLegendDisplayValue.get(),
},
'elasticChartsLegendListItem__title',
);

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

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

const displayValueClassNames = classNames(
'elasticChartsLegendListItem__displayValue',
{
['elasticChartsLegendListItem__displayValue--hidden']: !isSeriesVisible,
},
);

return (
<EuiFlexGroup gutterSize="xs" alignItems="center" responsive={false}>
<EuiFlexItem grow={false}>
Expand All @@ -90,11 +117,11 @@ class LegendElementComponent extends React.Component<LegendElementProps, LegendE
<EuiFlexItem grow={false}>
{this.renderVisibilityButton(legendItemKey, isSeriesVisible)}
</EuiFlexItem>
<EuiFlexItem grow={false} className={titleClassNames} onClick={onTitleClick}>
<EuiFlexItem grow={false} onClick={onTitleClick}>
<EuiPopover
id="contentPanel"
button={
<EuiText size="xs" className="eui-textTruncate elasticChartsLegendListItem__title">
<EuiText size="xs" className={titleClassNames}>
{label}
</EuiText>
}
Expand All @@ -111,6 +138,9 @@ class LegendElementComponent extends React.Component<LegendElementProps, LegendE
</EuiContextMenuPanel>
</EuiPopover>
</EuiFlexItem>
<EuiFlexItem grow={true} className={displayValueClassNames}>
{this.renderDisplayValue(displayValue, showLegendDisplayValue)}
</EuiFlexItem>
</EuiFlexGroup>
);
}
Expand Down
40 changes: 31 additions & 9 deletions src/lib/series/legend.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { getGroupId, getSpecId, SpecId } from '../utils/ids';
import { AxisId, getAxisId, getGroupId, getSpecId, SpecId } from '../utils/ids';
import { ScaleType } from '../utils/scales/scales';
import { computeLegend, getSeriesColorLabel } from './legend';
import { DataSeriesColorsValues } from './series';
import { BasicSeriesSpec } from './specs';
import { AxisSpec, BasicSeriesSpec, Position } from './specs';

const colorValues1a = {
specId: getSpecId('spec1'),
Expand Down Expand Up @@ -46,6 +46,22 @@ const spec2: BasicSeriesSpec = {
hideInLegend: false,
};

const axesSpecs = new Map<AxisId, AxisSpec>();
const axisSpec: AxisSpec = {
id: getAxisId('axis1'),
groupId: getGroupId('group1'),
hide: false,
showOverlappingTicks: false,
showOverlappingLabels: false,
position: Position.Left,
tickSize: 10,
tickPadding: 10,
tickFormat: (value: any) => {
return `${value}`;
},
};
axesSpecs.set(axisSpec.id, axisSpec);

describe('Legends', () => {
const seriesColor = new Map<string, DataSeriesColorsValues>();
const seriesColorMap = new Map<string, string>();
Expand All @@ -61,7 +77,7 @@ describe('Legends', () => {
});
it('compute legend for a single series', () => {
seriesColor.set('colorSeries1a', colorValues1a);
const legend = computeLegend(seriesColor, seriesColorMap, specs, 'violet');
const legend = computeLegend(seriesColor, seriesColorMap, specs, 'violet', axesSpecs);
const expected = [
{
color: 'red',
Expand All @@ -70,14 +86,15 @@ describe('Legends', () => {
isSeriesVisible: true,
isLegendItemVisible: true,
key: 'colorSeries1a',
displayValue: {},
},
];
expect(Array.from(legend.values())).toEqual(expected);
});
it('compute legend for a single spec but with multiple series', () => {
seriesColor.set('colorSeries1a', colorValues1a);
seriesColor.set('colorSeries1b', colorValues1b);
const legend = computeLegend(seriesColor, seriesColorMap, specs, 'violet');
const legend = computeLegend(seriesColor, seriesColorMap, specs, 'violet', axesSpecs);
const expected = [
{
color: 'red',
Expand All @@ -86,6 +103,7 @@ describe('Legends', () => {
isSeriesVisible: true,
isLegendItemVisible: true,
key: 'colorSeries1a',
displayValue: {},
},
{
color: 'blue',
Expand All @@ -94,14 +112,15 @@ describe('Legends', () => {
isSeriesVisible: true,
isLegendItemVisible: true,
key: 'colorSeries1b',
displayValue: {},
},
];
expect(Array.from(legend.values())).toEqual(expected);
});
it('compute legend for multiple specs', () => {
seriesColor.set('colorSeries1a', colorValues1a);
seriesColor.set('colorSeries2a', colorValues2a);
const legend = computeLegend(seriesColor, seriesColorMap, specs, 'violet');
const legend = computeLegend(seriesColor, seriesColorMap, specs, 'violet', axesSpecs);
const expected = [
{
color: 'red',
Expand All @@ -110,6 +129,7 @@ describe('Legends', () => {
isSeriesVisible: true,
isLegendItemVisible: true,
key: 'colorSeries1a',
displayValue: {},
},
{
color: 'green',
Expand All @@ -118,19 +138,20 @@ describe('Legends', () => {
isSeriesVisible: true,
isLegendItemVisible: true,
key: 'colorSeries2a',
displayValue: {},
},
];
expect(Array.from(legend.values())).toEqual(expected);
});
it('empty legend for missing spec', () => {
seriesColor.set('colorSeries2b', colorValues2b);
const legend = computeLegend(seriesColor, seriesColorMap, specs, 'violet');
const legend = computeLegend(seriesColor, seriesColorMap, specs, 'violet', axesSpecs);
expect(legend.size).toEqual(0);
});
it('compute legend with default color for missing series color', () => {
seriesColor.set('colorSeries1a', colorValues1a);
const emptyColorMap = new Map<string, string>();
const legend = computeLegend(seriesColor, emptyColorMap, specs, 'violet');
const legend = computeLegend(seriesColor, emptyColorMap, specs, 'violet', axesSpecs);
const expected = [
{
color: 'violet',
Expand All @@ -139,6 +160,7 @@ describe('Legends', () => {
isSeriesVisible: true,
isLegendItemVisible: true,
key: 'colorSeries1a',
displayValue: {},
},
];
expect(Array.from(legend.values())).toEqual(expected);
Expand All @@ -152,7 +174,7 @@ describe('Legends', () => {
const emptyColorMap = new Map<string, string>();
const deselectedDataSeries = null;

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

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

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

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

const visibility = [...legend.values()].map((item) => item.isSeriesVisible);
expect(visibility).toEqual([false, false, true]);
Expand Down
28 changes: 23 additions & 5 deletions src/lib/series/legend.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { findDataSeriesByColorValues } from '../../state/utils';
import { SpecId } from '../utils/ids';
import { DataSeriesColorsValues } from './series';
import { BasicSeriesSpec } from './specs';
import { findDataSeriesByColorValues, getAxesSpecForSpecId } from '../../state/utils';
import { identity } from '../utils/commons';
import { AxisId, SpecId } from '../utils/ids';
import { DataSeriesColorsValues, getSortedDataSeriesColorsValuesMap } from './series';
import { AxisSpec, BasicSeriesSpec } from './specs';

export interface LegendItem {
key: string;
Expand All @@ -10,16 +11,25 @@ export interface LegendItem {
value: DataSeriesColorsValues;
isSeriesVisible?: boolean;
isLegendItemVisible?: boolean;
displayValue: {
raw: any;
formatted: any;
};
}

export function computeLegend(
seriesColor: Map<string, DataSeriesColorsValues>,
seriesColorMap: Map<string, string>,
specs: Map<SpecId, BasicSeriesSpec>,
defaultColor: string,
axesSpecs: Map<AxisId, AxisSpec>,
deselectedDataSeries?: DataSeriesColorsValues[] | null,
): Map<string, LegendItem> {
const legendItems: Map<string, LegendItem> = new Map();
seriesColor.forEach((series, key) => {

const sortedSeriesColors = getSortedDataSeriesColorsValuesMap(seriesColor);

sortedSeriesColors.forEach((series, key) => {
const spec = specs.get(series.specId);

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

// Use this to get axis spec w/ tick formatter
const { yAxis } = getAxesSpecForSpecId(axesSpecs, spec.groupId);
const formatter = yAxis ? yAxis.tickFormat : identity;

const { hideInLegend } = spec;

legendItems.set(key, {
Expand All @@ -42,6 +56,10 @@ export function computeLegend(
value: series,
isSeriesVisible,
isLegendItemVisible: !hideInLegend,
displayValue: {
raw: series.lastValue,
formatted: formatter(series.lastValue),
},
});
});
return legendItems;
Expand Down
Loading

0 comments on commit 78af858

Please sign in to comment.