diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-interactions-sunburst-slice-clicks-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-interactions-sunburst-slice-clicks-visually-looks-correct-1-snap.png index 935721b773..0fa260a630 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-interactions-sunburst-slice-clicks-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-interactions-sunburst-slice-clicks-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/interactions-test-ts-interactions-tooltips-should-show-tooltip-on-sunburst-1-snap.png b/integration/tests/__image_snapshots__/interactions-test-ts-interactions-tooltips-should-show-tooltip-on-sunburst-1-snap.png index 28a4f3d1ce..0c4adc8de9 100644 Binary files a/integration/tests/__image_snapshots__/interactions-test-ts-interactions-tooltips-should-show-tooltip-on-sunburst-1-snap.png and b/integration/tests/__image_snapshots__/interactions-test-ts-interactions-tooltips-should-show-tooltip-on-sunburst-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/legend-stories-test-ts-legend-stories-extra-values-should-display-flat-legend-extra-values-on-sunburst-1-snap.png b/integration/tests/__image_snapshots__/legend-stories-test-ts-legend-stories-extra-values-should-display-flat-legend-extra-values-on-sunburst-1-snap.png new file mode 100644 index 0000000000..0cb2651345 Binary files /dev/null and b/integration/tests/__image_snapshots__/legend-stories-test-ts-legend-stories-extra-values-should-display-flat-legend-extra-values-on-sunburst-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/legend-stories-test-ts-legend-stories-extra-values-should-display-flat-legend-extra-values-on-treemap-1-snap.png b/integration/tests/__image_snapshots__/legend-stories-test-ts-legend-stories-extra-values-should-display-flat-legend-extra-values-on-treemap-1-snap.png new file mode 100644 index 0000000000..223042fc1f Binary files /dev/null and b/integration/tests/__image_snapshots__/legend-stories-test-ts-legend-stories-extra-values-should-display-flat-legend-extra-values-on-treemap-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/legend-stories-test-ts-legend-stories-extra-values-should-display-nested-legend-extra-values-on-sunburst-1-snap.png b/integration/tests/__image_snapshots__/legend-stories-test-ts-legend-stories-extra-values-should-display-nested-legend-extra-values-on-sunburst-1-snap.png new file mode 100644 index 0000000000..f08f7f9f90 Binary files /dev/null and b/integration/tests/__image_snapshots__/legend-stories-test-ts-legend-stories-extra-values-should-display-nested-legend-extra-values-on-sunburst-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/legend-stories-test-ts-legend-stories-extra-values-should-display-nested-legend-extra-values-on-treemap-1-snap.png b/integration/tests/__image_snapshots__/legend-stories-test-ts-legend-stories-extra-values-should-display-nested-legend-extra-values-on-treemap-1-snap.png new file mode 100644 index 0000000000..cdc0f7c84c Binary files /dev/null and b/integration/tests/__image_snapshots__/legend-stories-test-ts-legend-stories-extra-values-should-display-nested-legend-extra-values-on-treemap-1-snap.png differ diff --git a/integration/tests/legend_stories.test.ts b/integration/tests/legend_stories.test.ts index a8c3713045..859f0a9b44 100644 --- a/integration/tests/legend_stories.test.ts +++ b/integration/tests/legend_stories.test.ts @@ -17,6 +17,7 @@ * under the License. */ +import { PartitionLayout } from '../../src'; import { common } from '../page_objects'; describe('Legend stories', () => { @@ -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`, + ); + }, + ); + }); }); diff --git a/src/chart_types/partition_chart/state/chart_state.tsx b/src/chart_types/partition_chart/state/chart_state.tsx index e2c6327756..eafc5bf9d6 100644 --- a/src/chart_types/partition_chart/state/chart_state.tsx +++ b/src/chart_types/partition_chart/state/chart_state.tsx @@ -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'; @@ -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; @@ -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) { diff --git a/src/chart_types/partition_chart/state/selectors/__snapshots__/get_legend_items_extra.test.ts.snap b/src/chart_types/partition_chart/state/selectors/__snapshots__/get_legend_items_extra.test.ts.snap new file mode 100644 index 0000000000..7b7af03930 --- /dev/null +++ b/src/chart_types/partition_chart/state/selectors/__snapshots__/get_legend_items_extra.test.ts.snap @@ -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 {}`; diff --git a/src/chart_types/partition_chart/state/selectors/get_legend_items_extra.test.ts b/src/chart_types/partition_chart/state/selectors/get_legend_items_extra.test.ts new file mode 100644 index 0000000000..97a8b40d7c --- /dev/null +++ b/src/chart_types/partition_chart/state/selectors/get_legend_items_extra.test.ts @@ -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; + + 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([]); + }); +}); diff --git a/src/chart_types/partition_chart/state/selectors/get_legend_items_extra.ts b/src/chart_types/partition_chart/state/selectors/get_legend_items_extra.ts new file mode 100644 index 0000000000..e24c4b0c3b --- /dev/null +++ b/src/chart_types/partition_chart/state/selectors/get_legend_items_extra.ts @@ -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 => { + const legendExtraValues = new Map(); + + 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, + tree: HierarchyOfArrays, + maxDepth: number, + depth: number = 0, + keys: Map = new Map(), +): Map { + 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; +} diff --git a/src/common/legend.ts b/src/common/legend.ts index 9c856408b7..73784c3da5 100644 --- a/src/common/legend.ts +++ b/src/common/legend.ts @@ -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; @@ -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; diff --git a/src/components/legend/legend_item.tsx b/src/components/legend/legend_item.tsx index e540669356..b0db6a049c 100644 --- a/src/components/legend/legend_item.tsx +++ b/src/components/legend/legend_item.tsx @@ -211,7 +211,7 @@ export class LegendListItem extends Component '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), @@ -241,7 +241,7 @@ export class LegendListItem extends Component onClick={this.handleLabelClick(seriesIdentifier)} isSeriesHidden={isSeriesHidden} /> - {showExtra && extra && renderExtra(extra, isSeriesHidden)} + {extra && renderExtra(extra, isSeriesHidden)} {Action && (
diff --git a/src/components/legend/utils.ts b/src/components/legend/utils.ts index d903ba99a0..79e1767bcd 100644 --- a/src/components/legend/utils.ts +++ b/src/components/legend/utils.ts @@ -24,11 +24,13 @@ export function getExtra(extraValues: Map, 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) { diff --git a/stories/legend/10_sunburst.tsx b/stories/legend/10_sunburst.tsx index cadce40c4f..a546ccbee4 100644 --- a/stories/legend/10_sunburst.tsx +++ b/stories/legend/10_sunburst.tsx @@ -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, @@ -46,6 +55,7 @@ export const Example = () => { { }, ]} config={{ - partitionLayout: PartitionLayout.sunburst, + partitionLayout, linkLabel: { maxCount: 0, fontSize: 14,