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): display pie chart legend extra #939

Merged
merged 12 commits into from
Jan 20, 2021
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions integration/tests/legend_stories.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
* under the License.
*/

import { PartitionLayout } from '../../src';
import { common } from '../page_objects';

describe('Legend stories', () => {
Expand Down Expand Up @@ -143,4 +144,24 @@ describe('Legend stories', () => {
expect(hiddenResults).toEqual([1]);
});
});

describe('Extra values', () => {
it.each([PartitionLayout.sunburst, PartitionLayout.treemap])(
'should display flat legend extra values on %s',
async (layout) => {
await common.expectChartAtUrlToMatchScreenshot(
`http://localhost:9001/?path=/story/legend--piechart&knob-Partition Layout=${layout}&knob-flatLegend=true&knob-showLegendExtra=true&knob-legendMaxDepth=2`,
);
},
);

it.each([PartitionLayout.sunburst, PartitionLayout.treemap])(
'should display nested legend extra values on %s',
async (layout) => {
await common.expectChartAtUrlToMatchScreenshot(
`http://localhost:9001/?path=/story/legend--piechart&knob-Partition Layout=${layout}&knob-flatLegend=false&knob-showLegendExtra=true&knob-legendMaxDepth=2`,
);
},
);
});
});
7 changes: 3 additions & 4 deletions src/chart_types/partition_chart/state/chart_state.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { Partition } from '../renderer/canvas/partition';
import { HighlighterFromHover } from '../renderer/dom/highlighter_hover';
import { HighlighterFromLegend } from '../renderer/dom/highlighter_legend';
import { computeLegendSelector } from './selectors/compute_legend';
import { getLegendItemsExtra } from './selectors/get_legend_items_extra';
import { getLegendItemsLabels } from './selectors/get_legend_items_labels';
import { isTooltipVisibleSelector } from './selectors/is_tooltip_visible';
import { createOnElementClickCaller } from './selectors/on_element_click_caller';
Expand All @@ -37,8 +38,6 @@ import { createOnElementOverCaller } from './selectors/on_element_over_caller';
import { getPieSpec } from './selectors/pie_spec';
import { getTooltipInfoSelector } from './selectors/tooltip';

const EMPTY_MAP = new Map();

/** @internal */
export class PartitionState implements InternalChartState {
chartType = ChartTypes.Partition;
Expand Down Expand Up @@ -80,8 +79,8 @@ export class PartitionState implements InternalChartState {
return computeLegendSelector(globalState);
}

getLegendExtraValues() {
return EMPTY_MAP;
getLegendExtraValues(globalState: GlobalChartState) {
return getLegendItemsExtra(globalState);
}

chartRenderer(containerRef: BackwardRef, forwardStageRef: RefObject<HTMLCanvasElement>) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Partition - Legend item extra values should return all extra values in nested legend 1`] = `Object {}`;

exports[`Partition - Legend item extra values should return extra values in nested legend within max depth of 1 1`] = `Object {}`;

exports[`Partition - Legend item extra values should return extra values in nested legend within max depth of 2 1`] = `Object {}`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { Store } from 'redux';

import { MockSeriesSpec, MockGlobalSpec } from '../../../../mocks/specs';
import { MockStore } from '../../../../mocks/store';
import { GlobalChartState } from '../../../../state/chart_state';
import { PrimitiveValue } from '../../layout/utils/group_by_rollup';
import { getLegendItemsExtra } from './get_legend_items_extra';

describe('Partition - Legend item extra values', () => {
type TestDatum = [string, string, string, number];
const spec = MockSeriesSpec.sunburst({
data: [
['aaa', 'aa', '1', 1],
['aaa', 'aa', '1', 2],
['aaa', 'aa', '3', 1],
['aaa', 'bb', '4', 1],
['aaa', 'bb', '5', 1],
['aaa', 'bb', '6', 1],
['bbb', 'aa', '7', 1],
['bbb', 'aa', '8', 1],
['bbb', 'bb', '9', 1],
['bbb', 'bb', '10', 1],
['bbb', 'cc', '11', 1],
['bbb', 'cc', '12', 1],
],
valueAccessor: (d: TestDatum) => d[3],
layers: [
{
groupByRollup: (datum: TestDatum) => datum[0],
nodeLabel: (d: PrimitiveValue) => String(d),
},
{
groupByRollup: (datum: TestDatum) => datum[1],
nodeLabel: (d: PrimitiveValue) => String(d),
},
{
groupByRollup: (datum: TestDatum) => datum[2],
nodeLabel: (d: PrimitiveValue) => String(d),
},
],
});
let store: Store<GlobalChartState>;

beforeEach(() => {
store = MockStore.default();
});

it('should return all extra values in nested legend', () => {
MockStore.addSpecs([spec], store);

const extraValues = getLegendItemsExtra(store.getState());
expect([...extraValues.keys()]).toEqual([
'0',
'0__0',
'0__0__0',
'0__0__0__0',
'0__0__0__1',
'0__0__1',
'0__0__1__0',
'0__0__1__1',
'0__0__1__2',
'0__1',
'0__1__0',
'0__1__0__0',
'0__1__0__1',
'0__1__1',
'0__1__1__0',
'0__1__1__1',
'0__1__2',
'0__1__2__0',
'0__1__2__1',
]);
expect(extraValues.values()).toMatchSnapshot();
});

it('should return extra values in nested legend within max depth of 1', () => {
const settings = MockGlobalSpec.settings({ legendMaxDepth: 1 });
MockStore.addSpecs([settings, spec], store);

const extraValues = getLegendItemsExtra(store.getState());
expect([...extraValues.keys()]).toEqual(['0', '0__0', '0__1']);
expect(extraValues.values()).toMatchSnapshot();
});

it('should return extra values in nested legend within max depth of 2', () => {
const settings = MockGlobalSpec.settings({ legendMaxDepth: 2 });
MockStore.addSpecs([settings, spec], store);

const extraValues = getLegendItemsExtra(store.getState());
expect([...extraValues.keys()]).toEqual([
'0',
'0__0',
'0__0__0',
'0__0__1',
'0__1',
'0__1__0',
'0__1__1',
'0__1__2',
]);
expect(extraValues.values()).toMatchSnapshot();
});

it('filters all extraValues is depth is 0', () => {
const settings = MockGlobalSpec.settings({ legendMaxDepth: 0 });
MockStore.addSpecs([settings, spec], store);

const extraValues = getLegendItemsExtra(store.getState());
expect([...extraValues.keys()]).toEqual([]);
});

it('filters all extraValues is depth is NaN', () => {
const settings = MockGlobalSpec.settings({ legendMaxDepth: NaN });
MockStore.addSpecs([settings, spec], store);

const extraValues = getLegendItemsExtra(store.getState());
expect([...extraValues.keys()]).toEqual([]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import createCachedSelector from 're-reselect';

import { LegendItemExtraValues } from '../../../../common/legend';
import { SeriesKey } from '../../../../common/series_id';
import { SettingsSpec } from '../../../../specs';
import { getChartIdSelector } from '../../../../state/selectors/get_chart_id';
import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs';
import { HierarchyOfArrays, CHILDREN_KEY } from '../../layout/utils/group_by_rollup';
import { PartitionSpec } from '../../specs';
import { getPieSpec } from './pie_spec';
import { getTree } from './tree';

/** @internal */
export const getLegendItemsExtra = createCachedSelector(
[getPieSpec, getSettingsSpecSelector, getTree],
(pieSpec, { legendMaxDepth }, tree): Map<SeriesKey, LegendItemExtraValues> => {
const legendExtraValues = new Map<SeriesKey, LegendItemExtraValues>();

return pieSpec && isValidLegendMaxDepth(legendMaxDepth)
? getExtraValueMap(pieSpec, tree, legendMaxDepth)
: legendExtraValues;
},
)(getChartIdSelector);

/**
* Check if the legendMaxDepth from settings is a valid number (NaN or <=0)
*
* @param legendMaxDepth - SettingsSpec['legendMaxDepth']
*/
function isValidLegendMaxDepth(legendMaxDepth: SettingsSpec['legendMaxDepth']): boolean {
return typeof legendMaxDepth === 'number' && !Number.isNaN(legendMaxDepth) && legendMaxDepth > 0;
}

/**
* Creates flat extra value map from nested key path
*/
function getExtraValueMap(
{ layers, valueFormatter }: Pick<PartitionSpec, 'layers' | 'valueFormatter'>,
tree: HierarchyOfArrays,
maxDepth: number,
depth: number = 0,
keys: Map<SeriesKey, LegendItemExtraValues> = new Map(),
): Map<SeriesKey, LegendItemExtraValues> {
for (let i = 0; i < tree.length; i++) {
const branch = tree[i];
const [key, arrayNode] = branch;
const { value, path, [CHILDREN_KEY]: children } = arrayNode;

if (key != null) {
const values: LegendItemExtraValues = new Map();
const formattedValue = valueFormatter ? valueFormatter(value) : value;

values.set(key, formattedValue);
keys.set(path.map(({ index }) => index).join('__'), values);
}

if (depth < maxDepth) {
getExtraValueMap({ layers, valueFormatter }, children, maxDepth, depth + 1, keys);
}
}
return keys;
}
4 changes: 4 additions & 0 deletions src/common/legend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { LegendPath } from '../state/actions/legend';
import { Color } from '../utils/common';
import { CategoryKey, CategoryLabel } from './category';
import { SeriesIdentifier } from './series_id';

/** @internal */
export type LegendItemChildId = CategoryKey;

Expand All @@ -30,6 +31,9 @@ export type LegendItem = {
seriesIdentifier: SeriesIdentifier;
childId?: LegendItemChildId;
depth?: number;
/**
* Path to iterm in hierarchical legend
*/
path: LegendPath;
color: Color;
label: CategoryLabel;
Expand Down
4 changes: 2 additions & 2 deletions src/components/legend/legend_item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ export class LegendListItem extends Component<LegendItemProps, LegendItemState>
'echLegendItem__extra--hidden': isItemHidden,
});
const hasColorPicker = Boolean(colorPicker);
const extra = getExtra(extraValues, item, totalItems);
const extra = showExtra && getExtra(extraValues, item, totalItems);
const style = item.depth
? {
marginLeft: LEGEND_HIERARCHY_MARGIN * (item.depth ?? 0),
Expand Down Expand Up @@ -241,7 +241,7 @@ export class LegendListItem extends Component<LegendItemProps, LegendItemState>
onClick={this.handleLabelClick(seriesIdentifier)}
isSeriesHidden={isSeriesHidden}
/>
{showExtra && extra && renderExtra(extra, isSeriesHidden)}
{extra && renderExtra(extra, isSeriesHidden)}
{Action && (
<div className="echLegendItem__action">
<Action series={seriesIdentifier} color={color} label={label} />
Expand Down
4 changes: 3 additions & 1 deletion src/components/legend/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ export function getExtra(extraValues: Map<string, LegendItemExtraValues>, item:
seriesIdentifier: { key },
defaultExtra,
childId,
path,
} = item;
if (extraValues.size === 0) {
return defaultExtra?.formatted ?? '';
}
const itemExtraValues = extraValues.get(key);
const extraValueKey = path.map(({ index }) => index).join('__');
const itemExtraValues = extraValues.has(extraValueKey) ? extraValues.get(extraValueKey) : extraValues.get(key);
const actionExtra = (childId && itemExtraValues?.get(childId)) ?? null;
if (extraValues.size !== totalItems) {
if (actionExtra != null) {
Expand Down
12 changes: 11 additions & 1 deletion stories/legend/10_sunburst.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,16 @@ import {
} from '../utils/utils';

export const Example = () => {
const partitionLayout = select(
'Partition Layout',
{
treemap: PartitionLayout.treemap,
sunburst: PartitionLayout.sunburst,
},
PartitionLayout.sunburst,
);
const flatLegend = boolean('flatLegend', true);
const showLegendExtra = boolean('showLegendExtra', false);
const legendMaxDepth = number('legendMaxDepth', 2, {
min: 0,
max: 3,
Expand All @@ -46,6 +55,7 @@ export const Example = () => {
<Chart className="story-chart">
<Settings
showLegend
showLegendExtra={showLegendExtra}
flatLegend={flatLegend}
legendStrategy={legendStrategy}
legendMaxDepth={legendMaxDepth}
Expand Down Expand Up @@ -81,7 +91,7 @@ export const Example = () => {
},
]}
config={{
partitionLayout: PartitionLayout.sunburst,
partitionLayout,
linkLabel: {
maxCount: 0,
fontSize: 14,
Expand Down