From 51e00e2fe9bf25c754c3cf3e8b8fc96279ae0d94 Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Mon, 11 Mar 2024 11:01:38 +0100 Subject: [PATCH 01/15] chore: refactor LegendItem type --- .../heatmap/state/selectors/compute_legend.ts | 1 + .../selectors/get_legend_items_labels.ts | 10 +- .../__snapshots__/partition.test.tsx.snap | 143 ++++++++++-------- .../state/selectors/compute_legend.ts | 11 +- .../xy_chart/legend/legend.test.ts | 14 +- .../src/chart_types/xy_chart/legend/legend.ts | 30 ++-- .../xy_chart/rendering/rendering.test.ts | 6 +- .../selectors/get_legend_items_labels.ts | 13 +- .../xy_chart/state/utils/common.test.ts | 8 +- packages/charts/src/common/legend.ts | 7 +- .../charts/src/components/legend/utils.ts | 8 +- 11 files changed, 131 insertions(+), 120 deletions(-) diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/compute_legend.ts b/packages/charts/src/chart_types/heatmap/state/selectors/compute_legend.ts index 11a47a60cc..fbbe50b052 100644 --- a/packages/charts/src/chart_types/heatmap/state/selectors/compute_legend.ts +++ b/packages/charts/src/chart_types/heatmap/state/selectors/compute_legend.ts @@ -32,6 +32,7 @@ export const computeLegendSelector = createCustomCachedSelector( isToggleable: true, path: [{ index: 0, value: label }], keys: [], + values: [], }; }); }, diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/get_legend_items_labels.ts b/packages/charts/src/chart_types/heatmap/state/selectors/get_legend_items_labels.ts index dae202929f..26b7331efe 100644 --- a/packages/charts/src/chart_types/heatmap/state/selectors/get_legend_items_labels.ts +++ b/packages/charts/src/chart_types/heatmap/state/selectors/get_legend_items_labels.ts @@ -15,8 +15,10 @@ import { getSettingsSpecSelector } from '../../../../state/selectors/get_setting export const getLegendItemsLabelsSelector = createCustomCachedSelector( [computeLegendSelector, getSettingsSpecSelector], (legendItems, { showLegendExtra }): LegendItemLabel[] => - legendItems.map(({ label, defaultExtra }) => ({ - label: `${label}${showLegendExtra ? defaultExtra?.formatted ?? '' : ''}`, - depth: 0, - })), + legendItems.map(({ label, values }) => { + return { + label: `${label}${showLegendExtra ? values[0]?.formatted ?? '' : ''}`, + depth: 0, + }; + }), ); diff --git a/packages/charts/src/chart_types/partition_chart/__snapshots__/partition.test.tsx.snap b/packages/charts/src/chart_types/partition_chart/__snapshots__/partition.test.tsx.snap index 11a5d6ef39..c7ed6222d7 100644 --- a/packages/charts/src/chart_types/partition_chart/__snapshots__/partition.test.tsx.snap +++ b/packages/charts/src/chart_types/partition_chart/__snapshots__/partition.test.tsx.snap @@ -5,11 +5,6 @@ exports[`Retain hierarchy even with arbitrary names getLegendItems all distinct { "childId": "A", "color": "rgba(128, 0, 0, 0.5)", - "defaultExtra": { - "formatted": "2", - "legendSizingLabel": "2", - "raw": 2, - }, "depth": 0, "keys": [], "label": "A", @@ -33,15 +28,16 @@ exports[`Retain hierarchy even with arbitrary names getLegendItems all distinct "specId": "spec1", }, ], + "values": [ + { + "formatted": "2", + "raw": 2, + }, + ], }, { "childId": "A", "color": "rgba(128, 0, 0, 0.5)", - "defaultExtra": { - "formatted": "1", - "legendSizingLabel": "1", - "raw": 1, - }, "depth": 1, "keys": [], "label": "A", @@ -69,15 +65,16 @@ exports[`Retain hierarchy even with arbitrary names getLegendItems all distinct "specId": "spec1", }, ], + "values": [ + { + "formatted": "1", + "raw": 1, + }, + ], }, { "childId": "B", "color": "rgba(128, 0, 0, 0.5)", - "defaultExtra": { - "formatted": "1", - "legendSizingLabel": "1", - "raw": 1, - }, "depth": 1, "keys": [], "label": "B", @@ -105,15 +102,16 @@ exports[`Retain hierarchy even with arbitrary names getLegendItems all distinct "specId": "spec1", }, ], + "values": [ + { + "formatted": "1", + "raw": 1, + }, + ], }, { "childId": "B", "color": "rgba(128, 0, 0, 0.5)", - "defaultExtra": { - "formatted": "2", - "legendSizingLabel": "2", - "raw": 2, - }, "depth": 0, "keys": [], "label": "B", @@ -137,15 +135,16 @@ exports[`Retain hierarchy even with arbitrary names getLegendItems all distinct "specId": "spec1", }, ], + "values": [ + { + "formatted": "2", + "raw": 2, + }, + ], }, { "childId": "A", "color": "rgba(128, 0, 0, 0.5)", - "defaultExtra": { - "formatted": "1", - "legendSizingLabel": "1", - "raw": 1, - }, "depth": 1, "keys": [], "label": "A", @@ -173,15 +172,16 @@ exports[`Retain hierarchy even with arbitrary names getLegendItems all distinct "specId": "spec1", }, ], + "values": [ + { + "formatted": "1", + "raw": 1, + }, + ], }, { "childId": "B", "color": "rgba(128, 0, 0, 0.5)", - "defaultExtra": { - "formatted": "1", - "legendSizingLabel": "1", - "raw": 1, - }, "depth": 1, "keys": [], "label": "B", @@ -209,15 +209,16 @@ exports[`Retain hierarchy even with arbitrary names getLegendItems all distinct "specId": "spec1", }, ], + "values": [ + { + "formatted": "1", + "raw": 1, + }, + ], }, { "childId": "C", "color": "rgba(128, 0, 0, 0.5)", - "defaultExtra": { - "formatted": "2", - "legendSizingLabel": "2", - "raw": 2, - }, "depth": 0, "keys": [], "label": "C", @@ -241,15 +242,16 @@ exports[`Retain hierarchy even with arbitrary names getLegendItems all distinct "specId": "spec1", }, ], + "values": [ + { + "formatted": "2", + "raw": 2, + }, + ], }, { "childId": "A", "color": "rgba(128, 0, 0, 0.5)", - "defaultExtra": { - "formatted": "1", - "legendSizingLabel": "1", - "raw": 1, - }, "depth": 1, "keys": [], "label": "A", @@ -277,15 +279,16 @@ exports[`Retain hierarchy even with arbitrary names getLegendItems all distinct "specId": "spec1", }, ], + "values": [ + { + "formatted": "1", + "raw": 1, + }, + ], }, { "childId": "B", "color": "rgba(128, 0, 0, 0.5)", - "defaultExtra": { - "formatted": "1", - "legendSizingLabel": "1", - "raw": 1, - }, "depth": 1, "keys": [], "label": "B", @@ -313,6 +316,12 @@ exports[`Retain hierarchy even with arbitrary names getLegendItems all distinct "specId": "spec1", }, ], + "values": [ + { + "formatted": "1", + "raw": 1, + }, + ], }, ] `; @@ -322,11 +331,6 @@ exports[`Retain hierarchy even with arbitrary names getLegendItems special case: { "childId": "A", "color": "rgba(128, 0, 0, 0.5)", - "defaultExtra": { - "formatted": "1", - "legendSizingLabel": "1", - "raw": 1, - }, "depth": 0, "keys": [], "label": "A", @@ -350,15 +354,16 @@ exports[`Retain hierarchy even with arbitrary names getLegendItems special case: "specId": "spec1", }, ], + "values": [ + { + "formatted": "1", + "raw": 1, + }, + ], }, { "childId": "A", "color": "rgba(128, 0, 0, 0.5)", - "defaultExtra": { - "formatted": "1", - "legendSizingLabel": "1", - "raw": 1, - }, "depth": 1, "keys": [], "label": "A", @@ -386,6 +391,12 @@ exports[`Retain hierarchy even with arbitrary names getLegendItems special case: "specId": "spec1", }, ], + "values": [ + { + "formatted": "1", + "raw": 1, + }, + ], }, ] `; @@ -395,11 +406,6 @@ exports[`Retain hierarchy even with arbitrary names getLegendItems special case: { "childId": "C", "color": "rgba(128, 0, 0, 0.5)", - "defaultExtra": { - "formatted": "1", - "legendSizingLabel": "1", - "raw": 1, - }, "depth": 0, "keys": [], "label": "C", @@ -423,15 +429,16 @@ exports[`Retain hierarchy even with arbitrary names getLegendItems special case: "specId": "spec1", }, ], + "values": [ + { + "formatted": "1", + "raw": 1, + }, + ], }, { "childId": "B", "color": "rgba(128, 0, 0, 0.5)", - "defaultExtra": { - "formatted": "1", - "legendSizingLabel": "1", - "raw": 1, - }, "depth": 1, "keys": [], "label": "B", @@ -459,6 +466,12 @@ exports[`Retain hierarchy even with arbitrary names getLegendItems special case: "specId": "spec1", }, ], + "values": [ + { + "formatted": "1", + "raw": 1, + }, + ], }, ] `; diff --git a/packages/charts/src/chart_types/partition_chart/state/selectors/compute_legend.ts b/packages/charts/src/chart_types/partition_chart/state/selectors/compute_legend.ts index 3d70692861..bbaeb02fc1 100644 --- a/packages/charts/src/chart_types/partition_chart/state/selectors/compute_legend.ts +++ b/packages/charts/src/chart_types/partition_chart/state/selectors/compute_legend.ts @@ -123,11 +123,12 @@ function walkTree( depth: node[DEPTH_KEY] - 1, seriesIdentifiers: [{ key, specId }], keys: [], - defaultExtra: { - raw: node[AGGREGATE_KEY], - formatted: valueFormatter(node[AGGREGATE_KEY]), - legendSizingLabel: `${node[AGGREGATE_KEY]}`, - }, + values: [ + { + raw: node[AGGREGATE_KEY], + formatted: valueFormatter(node[AGGREGATE_KEY]), + }, + ], }, node, }); diff --git a/packages/charts/src/chart_types/xy_chart/legend/legend.test.ts b/packages/charts/src/chart_types/xy_chart/legend/legend.test.ts index 7b169e54da..0b8f5ff001 100644 --- a/packages/charts/src/chart_types/xy_chart/legend/legend.test.ts +++ b/packages/charts/src/chart_types/xy_chart/legend/legend.test.ts @@ -22,8 +22,6 @@ import { computeSeriesDomainsSelector } from '../state/selectors/compute_series_ import { getSeriesName } from '../utils/series'; import { AxisSpec, BasicSeriesSpec, SeriesType } from '../utils/specs'; -const nullDisplayValue = undefined; - const spec1: BasicSeriesSpec = { chartType: ChartType.XYAxis, specType: SpecType.Series, @@ -100,7 +98,7 @@ describe('Legends', () => { isItemHidden: false, isSeriesHidden: false, isToggleable: true, - defaultExtra: nullDisplayValue, + values: [], path: [{ index: 0, value: 'groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{}' }], }; expect(legend[0]).toMatchObject(expected); @@ -143,7 +141,7 @@ describe('Legends', () => { isItemHidden: false, isSeriesHidden: false, isToggleable: true, - defaultExtra: nullDisplayValue, + values: [], path: [{ index: 0, value: 'groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g-a}' }], }, { @@ -153,7 +151,7 @@ describe('Legends', () => { isItemHidden: false, isSeriesHidden: false, isToggleable: true, - defaultExtra: nullDisplayValue, + values: [], path: [{ index: 0, value: 'groupId{__global__}spec{spec1}yAccessor{y2}splitAccessors{g-a}' }], }, { @@ -163,7 +161,7 @@ describe('Legends', () => { isItemHidden: false, isSeriesHidden: false, isToggleable: true, - defaultExtra: nullDisplayValue, + values: [], path: [{ index: 0, value: 'groupId{__global__}spec{spec1}yAccessor{y1}splitAccessors{g-b}' }], }, { @@ -173,7 +171,7 @@ describe('Legends', () => { isItemHidden: false, isSeriesHidden: false, isToggleable: true, - defaultExtra: nullDisplayValue, + values: [], path: [{ index: 0, value: 'groupId{__global__}spec{spec1}yAccessor{y2}splitAccessors{g-b}' }], }, ]; @@ -222,7 +220,7 @@ describe('Legends', () => { isItemHidden: false, isSeriesHidden: false, isToggleable: true, - defaultExtra: nullDisplayValue, + values: [], path: [{ index: 0, value: 'groupId{__global__}spec{spec2}yAccessor{y}splitAccessors{}' }], }, ]; diff --git a/packages/charts/src/chart_types/xy_chart/legend/legend.ts b/packages/charts/src/chart_types/xy_chart/legend/legend.ts index fa3d6c68ec..a49dbcd5e5 100644 --- a/packages/charts/src/chart_types/xy_chart/legend/legend.ts +++ b/packages/charts/src/chart_types/xy_chart/legend/legend.ts @@ -135,14 +135,15 @@ export function computeLegend( isSeriesHidden, isItemHidden: hideInLegend, isToggleable: true, - defaultExtra: + values: itemValue !== null - ? { - raw: itemValue, - formatted: formattedItemValue, - legendSizingLabel: formattedItemValue, - } - : undefined, + ? [ + { + raw: itemValue, + formatted: formattedItemValue, + }, + ] + : [], path: [{ index: 0, value: seriesIdentifier.key }], keys: [specId, spec.groupId, yAccessor, ...series.splitAccessors.values()], pointStyle, @@ -162,14 +163,15 @@ export function computeLegend( isSeriesHidden, isItemHidden: hideInLegend, isToggleable: true, - defaultExtra: + values: bandedItemValue !== null - ? { - raw: bandedItemValue, - formatted: bandedFormattedItemValue, - legendSizingLabel: bandedFormattedItemValue, - } - : undefined, + ? [ + { + raw: bandedItemValue, + formatted: bandedFormattedItemValue, + }, + ] + : [], path: [{ index: 0, value: seriesIdentifier.key }], keys: [specId, spec.groupId, yAccessor, ...series.splitAccessors.values()], pointStyle, diff --git a/packages/charts/src/chart_types/xy_chart/rendering/rendering.test.ts b/packages/charts/src/chart_types/xy_chart/rendering/rendering.test.ts index c132e7f886..175e25bf31 100644 --- a/packages/charts/src/chart_types/xy_chart/rendering/rendering.test.ts +++ b/packages/charts/src/chart_types/xy_chart/rendering/rendering.test.ts @@ -128,11 +128,7 @@ describe('Rendering utils', () => { label: '', seriesIdentifiers: [seriesIdentifier], isSeriesHidden: false, - defaultExtra: { - formatted: null, - raw: null, - legendSizingLabel: null, - }, + values: [], path: [], keys: [], }; diff --git a/packages/charts/src/chart_types/xy_chart/state/selectors/get_legend_items_labels.ts b/packages/charts/src/chart_types/xy_chart/state/selectors/get_legend_items_labels.ts index f70c3384e1..4ac49ea456 100644 --- a/packages/charts/src/chart_types/xy_chart/state/selectors/get_legend_items_labels.ts +++ b/packages/charts/src/chart_types/xy_chart/state/selectors/get_legend_items_labels.ts @@ -15,11 +15,10 @@ import { getSettingsSpecSelector } from '../../../../state/selectors/get_setting export const getLegendItemsLabelsSelector = createCustomCachedSelector( [computeLegendSelector, getSettingsSpecSelector], (legendItems, { showLegendExtra }): LegendItemLabel[] => - legendItems.map(({ label, defaultExtra }) => ({ - label: - defaultExtra && (defaultExtra.legendSizingLabel ?? null) !== null - ? `${label}${showLegendExtra ? defaultExtra.legendSizingLabel : ''}` - : label, - depth: 0, - })), + legendItems.map(({ label, values }) => { + return { + label: `${label}${showLegendExtra ? values[0]?.formatted : ''}`, + depth: 0, + }; + }), ); diff --git a/packages/charts/src/chart_types/xy_chart/state/utils/common.test.ts b/packages/charts/src/chart_types/xy_chart/state/utils/common.test.ts index a2ecbf2385..307a657e2b 100644 --- a/packages/charts/src/chart_types/xy_chart/state/utils/common.test.ts +++ b/packages/charts/src/chart_types/xy_chart/state/utils/common.test.ts @@ -133,7 +133,7 @@ describe('Type Checks', () => { specId: 'bars', }, ], - defaultExtra: { raw: 6, formatted: '6.00', legendSizingLabel: '6.00' }, + values: [{ raw: 6, formatted: '6.00' }], isSeriesHidden: true, path: [], keys: [], @@ -147,7 +147,7 @@ describe('Type Checks', () => { specId: 'bars', }, ], - defaultExtra: { raw: 2, formatted: '2.00', legendSizingLabel: '2.00' }, + values: [{ raw: 2, formatted: '2.00' }], isSeriesHidden: true, path: [], keys: [], @@ -166,7 +166,7 @@ describe('Type Checks', () => { specId: 'bars', }, ], - defaultExtra: { raw: 6, formatted: '6.00', legendSizingLabel: '6.00' }, + values: [{ raw: 6, formatted: '6.00' }], isSeriesHidden: false, path: [], keys: [], @@ -180,7 +180,7 @@ describe('Type Checks', () => { specId: 'bars', }, ], - defaultExtra: { raw: 2, formatted: '2.00', legendSizingLabel: '2.00' }, + values: [{ raw: 2, formatted: '2.00' }], isSeriesHidden: true, path: [], keys: [], diff --git a/packages/charts/src/common/legend.ts b/packages/charts/src/common/legend.ts index 91308df8e8..ec08170d81 100644 --- a/packages/charts/src/common/legend.ts +++ b/packages/charts/src/common/legend.ts @@ -30,11 +30,10 @@ export type LegendItem = { label: CategoryLabel; isSeriesHidden?: boolean; isItemHidden?: boolean; - defaultExtra?: { + values: Array<{ raw: number | null; - formatted: number | string | null; - legendSizingLabel: number | string | null; - }; + formatted: string; + }>; // TODO: Remove when partition layers are toggleable isToggleable?: boolean; keys: Array; diff --git a/packages/charts/src/components/legend/utils.ts b/packages/charts/src/components/legend/utils.ts index b7575c1d98..14d2c84fa1 100644 --- a/packages/charts/src/components/legend/utils.ts +++ b/packages/charts/src/components/legend/utils.ts @@ -15,10 +15,10 @@ export function getExtra( item: LegendItem, totalItems: number, ): { raw: PrimitiveValue; formatted: string } | null { - const { seriesIdentifiers, defaultExtra, childId, path } = item; + const { seriesIdentifiers, values, childId, path } = item; // don't show extra if the legend item is associated with multiple series if (extraValues.size === 0 || seriesIdentifiers.length > 1 || !seriesIdentifiers[0]) { - return defaultExtra ? { formatted: `${defaultExtra.formatted ?? ''}`, raw: defaultExtra.raw } : null; + return values.length > 0 ? { formatted: `${values[0]?.formatted ?? ''}`, raw: values[0]?.raw ?? null } : null; } const [{ key }] = seriesIdentifiers; const extraValueKey = path.map(({ index }) => index).join('__'); @@ -26,7 +26,7 @@ export function getExtra( const actionExtra = childId !== undefined ? itemExtraValues?.get(childId) : undefined; return actionExtra ? actionExtra - : extraValues.size === totalItems && defaultExtra - ? { formatted: `${defaultExtra.formatted ?? ''}`, raw: defaultExtra.raw } + : extraValues.size === totalItems && values.length > 0 + ? { formatted: `${values[0]?.formatted ?? ''}`, raw: values[0]?.raw ?? null } : null; } From 3d196b8c7c11ecd5eac7666da20390e8eff17c30 Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Mon, 11 Mar 2024 11:15:46 +0100 Subject: [PATCH 02/15] chore: remaned legendItemValue props --- packages/charts/api/charts.api.md | 9 ++-- .../selectors/get_legend_items_labels.ts | 2 +- .../__snapshots__/partition.test.tsx.snap | 52 +++++++++---------- .../layout/viewmodel/hierarchy_of_arrays.ts | 4 +- .../state/selectors/compute_legend.ts | 4 +- .../src/chart_types/xy_chart/legend/legend.ts | 8 +-- .../selectors/get_legend_items_labels.ts | 2 +- .../xy_chart/state/utils/common.test.ts | 8 +-- .../chart_types/xy_chart/tooltip/tooltip.ts | 2 +- packages/charts/src/common/legend.ts | 10 ++-- .../charts/src/components/legend/extra.tsx | 22 -------- .../src/components/legend/legend_item.tsx | 7 ++- .../charts/src/components/legend/utils.ts | 9 ++-- packages/charts/src/specs/settings.tsx | 3 +- 14 files changed, 62 insertions(+), 80 deletions(-) delete mode 100644 packages/charts/src/components/legend/extra.tsx diff --git a/packages/charts/api/charts.api.md b/packages/charts/api/charts.api.md index 7e7db396f1..280aa06b29 100644 --- a/packages/charts/api/charts.api.md +++ b/packages/charts/api/charts.api.md @@ -854,10 +854,7 @@ export interface CustomLegendProps { label: CategoryLabel; seriesType?: SeriesType; pointStyle?: PointStyle; - extraValue?: { - raw: PrimitiveValue; - formatted: string; - }; + extraValue?: LegendItemValue; isSeriesHidden?: boolean; onItemOverActon: () => void; onItemOutAction: () => void; @@ -3497,6 +3494,10 @@ export interface YDomainBase { // @public (undocumented) export type YDomainRange = YDomainBase & DomainRange & LogScaleOptions; +// Warnings were encountered during analysis: +// +// src/specs/settings.tsx:394:5 - (ae-forgotten-export) The symbol "LegendItemValue" needs to be exported by the entry point index.d.ts + // (No @packageDocumentation comment for this package) ``` diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/get_legend_items_labels.ts b/packages/charts/src/chart_types/heatmap/state/selectors/get_legend_items_labels.ts index 26b7331efe..9ea1b27656 100644 --- a/packages/charts/src/chart_types/heatmap/state/selectors/get_legend_items_labels.ts +++ b/packages/charts/src/chart_types/heatmap/state/selectors/get_legend_items_labels.ts @@ -17,7 +17,7 @@ export const getLegendItemsLabelsSelector = createCustomCachedSelector( (legendItems, { showLegendExtra }): LegendItemLabel[] => legendItems.map(({ label, values }) => { return { - label: `${label}${showLegendExtra ? values[0]?.formatted ?? '' : ''}`, + label: `${label}${showLegendExtra ? values[0]?.label ?? '' : ''}`, depth: 0, }; }), diff --git a/packages/charts/src/chart_types/partition_chart/__snapshots__/partition.test.tsx.snap b/packages/charts/src/chart_types/partition_chart/__snapshots__/partition.test.tsx.snap index c7ed6222d7..ceb5f5d1f1 100644 --- a/packages/charts/src/chart_types/partition_chart/__snapshots__/partition.test.tsx.snap +++ b/packages/charts/src/chart_types/partition_chart/__snapshots__/partition.test.tsx.snap @@ -30,8 +30,8 @@ exports[`Retain hierarchy even with arbitrary names getLegendItems all distinct ], "values": [ { - "formatted": "2", - "raw": 2, + "label": "2", + "value": 2, }, ], }, @@ -67,8 +67,8 @@ exports[`Retain hierarchy even with arbitrary names getLegendItems all distinct ], "values": [ { - "formatted": "1", - "raw": 1, + "label": "1", + "value": 1, }, ], }, @@ -104,8 +104,8 @@ exports[`Retain hierarchy even with arbitrary names getLegendItems all distinct ], "values": [ { - "formatted": "1", - "raw": 1, + "label": "1", + "value": 1, }, ], }, @@ -137,8 +137,8 @@ exports[`Retain hierarchy even with arbitrary names getLegendItems all distinct ], "values": [ { - "formatted": "2", - "raw": 2, + "label": "2", + "value": 2, }, ], }, @@ -174,8 +174,8 @@ exports[`Retain hierarchy even with arbitrary names getLegendItems all distinct ], "values": [ { - "formatted": "1", - "raw": 1, + "label": "1", + "value": 1, }, ], }, @@ -211,8 +211,8 @@ exports[`Retain hierarchy even with arbitrary names getLegendItems all distinct ], "values": [ { - "formatted": "1", - "raw": 1, + "label": "1", + "value": 1, }, ], }, @@ -244,8 +244,8 @@ exports[`Retain hierarchy even with arbitrary names getLegendItems all distinct ], "values": [ { - "formatted": "2", - "raw": 2, + "label": "2", + "value": 2, }, ], }, @@ -281,8 +281,8 @@ exports[`Retain hierarchy even with arbitrary names getLegendItems all distinct ], "values": [ { - "formatted": "1", - "raw": 1, + "label": "1", + "value": 1, }, ], }, @@ -318,8 +318,8 @@ exports[`Retain hierarchy even with arbitrary names getLegendItems all distinct ], "values": [ { - "formatted": "1", - "raw": 1, + "label": "1", + "value": 1, }, ], }, @@ -356,8 +356,8 @@ exports[`Retain hierarchy even with arbitrary names getLegendItems special case: ], "values": [ { - "formatted": "1", - "raw": 1, + "label": "1", + "value": 1, }, ], }, @@ -393,8 +393,8 @@ exports[`Retain hierarchy even with arbitrary names getLegendItems special case: ], "values": [ { - "formatted": "1", - "raw": 1, + "label": "1", + "value": 1, }, ], }, @@ -431,8 +431,8 @@ exports[`Retain hierarchy even with arbitrary names getLegendItems special case: ], "values": [ { - "formatted": "1", - "raw": 1, + "label": "1", + "value": 1, }, ], }, @@ -468,8 +468,8 @@ exports[`Retain hierarchy even with arbitrary names getLegendItems special case: ], "values": [ { - "formatted": "1", - "raw": 1, + "label": "1", + "value": 1, }, ], }, diff --git a/packages/charts/src/chart_types/partition_chart/layout/viewmodel/hierarchy_of_arrays.ts b/packages/charts/src/chart_types/partition_chart/layout/viewmodel/hierarchy_of_arrays.ts index 40e2fdcf08..86c7a20649 100644 --- a/packages/charts/src/chart_types/partition_chart/layout/viewmodel/hierarchy_of_arrays.ts +++ b/packages/charts/src/chart_types/partition_chart/layout/viewmodel/hierarchy_of_arrays.ts @@ -117,8 +117,8 @@ export function getExtraValueMap( const [key, arrayNode] = branch; const { value, path, [CHILDREN_KEY]: children } = arrayNode; const values: LegendItemExtraValues = new Map(); - const formattedValue = valueFormatter ? valueFormatter(value) : `${value}`; - values.set(key, { formatted: formattedValue, raw: value }); + const label = valueFormatter ? valueFormatter(value) : `${value}`; + values.set(key, { label, value }); keys.set(path.map(({ index }) => index).join('__'), values); if (depth < maxDepth) getExtraValueMap(layers, valueFormatter, children, maxDepth, depth + 1, keys); } diff --git a/packages/charts/src/chart_types/partition_chart/state/selectors/compute_legend.ts b/packages/charts/src/chart_types/partition_chart/state/selectors/compute_legend.ts index bbaeb02fc1..06dcd513d8 100644 --- a/packages/charts/src/chart_types/partition_chart/state/selectors/compute_legend.ts +++ b/packages/charts/src/chart_types/partition_chart/state/selectors/compute_legend.ts @@ -125,8 +125,8 @@ function walkTree( keys: [], values: [ { - raw: node[AGGREGATE_KEY], - formatted: valueFormatter(node[AGGREGATE_KEY]), + value: node[AGGREGATE_KEY], + label: valueFormatter(node[AGGREGATE_KEY]), }, ], }, diff --git a/packages/charts/src/chart_types/xy_chart/legend/legend.ts b/packages/charts/src/chart_types/xy_chart/legend/legend.ts index a49dbcd5e5..f2925e2e30 100644 --- a/packages/charts/src/chart_types/xy_chart/legend/legend.ts +++ b/packages/charts/src/chart_types/xy_chart/legend/legend.ts @@ -139,8 +139,8 @@ export function computeLegend( itemValue !== null ? [ { - raw: itemValue, - formatted: formattedItemValue, + value: itemValue, + label: formattedItemValue, }, ] : [], @@ -167,8 +167,8 @@ export function computeLegend( bandedItemValue !== null ? [ { - raw: bandedItemValue, - formatted: bandedFormattedItemValue, + value: bandedItemValue, + label: bandedFormattedItemValue, }, ] : [], diff --git a/packages/charts/src/chart_types/xy_chart/state/selectors/get_legend_items_labels.ts b/packages/charts/src/chart_types/xy_chart/state/selectors/get_legend_items_labels.ts index 4ac49ea456..d0cce65a20 100644 --- a/packages/charts/src/chart_types/xy_chart/state/selectors/get_legend_items_labels.ts +++ b/packages/charts/src/chart_types/xy_chart/state/selectors/get_legend_items_labels.ts @@ -17,7 +17,7 @@ export const getLegendItemsLabelsSelector = createCustomCachedSelector( (legendItems, { showLegendExtra }): LegendItemLabel[] => legendItems.map(({ label, values }) => { return { - label: `${label}${showLegendExtra ? values[0]?.formatted : ''}`, + label: `${label}${showLegendExtra ? values[0]?.label : ''}`, depth: 0, }; }), diff --git a/packages/charts/src/chart_types/xy_chart/state/utils/common.test.ts b/packages/charts/src/chart_types/xy_chart/state/utils/common.test.ts index 307a657e2b..a6b1522471 100644 --- a/packages/charts/src/chart_types/xy_chart/state/utils/common.test.ts +++ b/packages/charts/src/chart_types/xy_chart/state/utils/common.test.ts @@ -133,7 +133,7 @@ describe('Type Checks', () => { specId: 'bars', }, ], - values: [{ raw: 6, formatted: '6.00' }], + values: [{ value: 6, label: '6.00' }], isSeriesHidden: true, path: [], keys: [], @@ -147,7 +147,7 @@ describe('Type Checks', () => { specId: 'bars', }, ], - values: [{ raw: 2, formatted: '2.00' }], + values: [{ value: 2, label: '2.00' }], isSeriesHidden: true, path: [], keys: [], @@ -166,7 +166,7 @@ describe('Type Checks', () => { specId: 'bars', }, ], - values: [{ raw: 6, formatted: '6.00' }], + values: [{ value: 6, label: '6.00' }], isSeriesHidden: false, path: [], keys: [], @@ -180,7 +180,7 @@ describe('Type Checks', () => { specId: 'bars', }, ], - values: [{ raw: 2, formatted: '2.00' }], + values: [{ value: 2, label: '2.00' }], isSeriesHidden: true, path: [], keys: [], diff --git a/packages/charts/src/chart_types/xy_chart/tooltip/tooltip.ts b/packages/charts/src/chart_types/xy_chart/tooltip/tooltip.ts index c079823e65..b837242c95 100644 --- a/packages/charts/src/chart_types/xy_chart/tooltip/tooltip.ts +++ b/packages/charts/src/chart_types/xy_chart/tooltip/tooltip.ts @@ -29,7 +29,7 @@ export function getLegendItemExtraValues(tooltipValues: TooltipValue[]): Map { const current: LegendItemExtraValues = seriesTooltipValues.get(seriesIdentifier.key) ?? new Map(); if (valueAccessor === BandedAccessorType.Y0 || valueAccessor === BandedAccessorType.Y1) { - current.set(valueAccessor, { formatted: formattedValue, raw: value }); + current.set(valueAccessor, { label: formattedValue, value }); } seriesTooltipValues.set(seriesIdentifier.key, current); }); diff --git a/packages/charts/src/common/legend.ts b/packages/charts/src/common/legend.ts index ec08170d81..0eea3d1e34 100644 --- a/packages/charts/src/common/legend.ts +++ b/packages/charts/src/common/legend.ts @@ -17,6 +17,9 @@ import { PointStyle } from '../utils/themes/theme'; /** @internal */ export type LegendItemChildId = CategoryKey; +/** @public */ +export type LegendItemValue = { value: PrimitiveValue; label: string }; + /** @internal */ export type LegendItem = { seriesIdentifiers: SeriesIdentifier[]; @@ -30,10 +33,7 @@ export type LegendItem = { label: CategoryLabel; isSeriesHidden?: boolean; isItemHidden?: boolean; - values: Array<{ - raw: number | null; - formatted: string; - }>; + values: Array; // TODO: Remove when partition layers are toggleable isToggleable?: boolean; keys: Array; @@ -42,4 +42,4 @@ export type LegendItem = { }; /** @internal */ -export type LegendItemExtraValues = Map; +export type LegendItemExtraValues = Map; diff --git a/packages/charts/src/components/legend/extra.tsx b/packages/charts/src/components/legend/extra.tsx deleted file mode 100644 index cad6e492b5..0000000000 --- a/packages/charts/src/components/legend/extra.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; - -/** - * @internal - * @param extra - * @param isSeriesHidden - */ -export function renderExtra(extra: string) { - return ( -
- {extra} -
- ); -} diff --git a/packages/charts/src/components/legend/legend_item.tsx b/packages/charts/src/components/legend/legend_item.tsx index 99c4811596..5a050d2620 100644 --- a/packages/charts/src/components/legend/legend_item.tsx +++ b/packages/charts/src/components/legend/legend_item.tsx @@ -10,7 +10,6 @@ import classNames from 'classnames'; import React, { Component, createRef, MouseEventHandler, CSSProperties } from 'react'; import { Color as ItemColor } from './color'; -import { renderExtra } from './extra'; import { Label as ItemLabel } from './label'; import { getExtra } from './utils'; import { Color } from '../../common/colors'; @@ -225,7 +224,11 @@ export class LegendListItem extends Component onToggle={this.onLabelToggle(seriesIdentifiers)} isSeriesHidden={isSeriesHidden} /> - {extra && !isSeriesHidden && renderExtra(extra.formatted)} + {extra && !isSeriesHidden && ( +
+ {extra.label} +
+ )} {Action && (
diff --git a/packages/charts/src/components/legend/utils.ts b/packages/charts/src/components/legend/utils.ts index 14d2c84fa1..e3680066b4 100644 --- a/packages/charts/src/components/legend/utils.ts +++ b/packages/charts/src/components/legend/utils.ts @@ -6,19 +6,18 @@ * Side Public License, v 1. */ -import { PrimitiveValue } from '../../chart_types/partition_chart/layout/utils/group_by_rollup'; -import { LegendItemExtraValues, LegendItem } from '../../common/legend'; +import { LegendItemExtraValues, LegendItem, LegendItemValue } from '../../common/legend'; /** @internal */ export function getExtra( extraValues: Map, item: LegendItem, totalItems: number, -): { raw: PrimitiveValue; formatted: string } | null { +): LegendItemValue | null { const { seriesIdentifiers, values, childId, path } = item; // don't show extra if the legend item is associated with multiple series if (extraValues.size === 0 || seriesIdentifiers.length > 1 || !seriesIdentifiers[0]) { - return values.length > 0 ? { formatted: `${values[0]?.formatted ?? ''}`, raw: values[0]?.raw ?? null } : null; + return values.length > 0 ? { label: `${values[0]?.label ?? ''}`, value: values[0]?.value ?? null } : null; } const [{ key }] = seriesIdentifiers; const extraValueKey = path.map(({ index }) => index).join('__'); @@ -27,6 +26,6 @@ export function getExtra( return actionExtra ? actionExtra : extraValues.size === totalItems && values.length > 0 - ? { formatted: `${values[0]?.formatted ?? ''}`, raw: values[0]?.raw ?? null } + ? { label: `${values[0]?.label ?? ''}`, value: values[0]?.value ?? null } : null; } diff --git a/packages/charts/src/specs/settings.tsx b/packages/charts/src/specs/settings.tsx index e1e305e994..fa2d3677e4 100644 --- a/packages/charts/src/specs/settings.tsx +++ b/packages/charts/src/specs/settings.tsx @@ -18,6 +18,7 @@ import { WordModel } from '../chart_types/wordcloud/layout/types/viewmodel_types import { XYChartSeriesIdentifier } from '../chart_types/xy_chart/utils/series'; import { CategoryLabel } from '../common/category'; import { Color } from '../common/colors'; +import { LegendItemValue } from '../common/legend'; import { SmallMultiplesDatum } from '../common/panel_utils'; import { SeriesIdentifier } from '../common/series_id'; import { TooltipPortalSettings } from '../components'; @@ -390,7 +391,7 @@ export interface CustomLegendProps { label: CategoryLabel; seriesType?: SeriesType; pointStyle?: PointStyle; - extraValue?: { raw: PrimitiveValue; formatted: string }; + extraValue?: LegendItemValue; isSeriesHidden?: boolean; onItemOverActon: () => void; onItemOutAction: () => void; From 5c928ba093db34da8a414a27ec91c7729de42d63 Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Mon, 11 Mar 2024 12:38:44 +0100 Subject: [PATCH 03/15] chore: improve type and fixed legend sizing --- .../heatmap/state/selectors/compute_legend.ts | 1 + .../src/chart_types/xy_chart/legend/legend.ts | 2 ++ .../xy_chart/rendering/rendering.test.ts | 1 + .../selectors/get_legend_items_labels.ts | 13 +------ .../xy_chart/state/utils/common.test.ts | 4 +++ packages/charts/src/common/legend.ts | 3 +- .../src/state/selectors/get_legend_size.ts | 36 +++++++++++-------- .../6a_different_tooltip_formatter.story.tsx | 5 +-- 8 files changed, 35 insertions(+), 30 deletions(-) diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/compute_legend.ts b/packages/charts/src/chart_types/heatmap/state/selectors/compute_legend.ts index fbbe50b052..b553be8e52 100644 --- a/packages/charts/src/chart_types/heatmap/state/selectors/compute_legend.ts +++ b/packages/charts/src/chart_types/heatmap/state/selectors/compute_legend.ts @@ -26,6 +26,7 @@ export const computeLegendSelector = createCustomCachedSelector( return { // the band label is considered unique by construction seriesIdentifiers: [{ key: label, specId: label }], + depth: 0, color, label, isSeriesHidden: deselectedDataSeries.some(({ key }) => key === label), diff --git a/packages/charts/src/chart_types/xy_chart/legend/legend.ts b/packages/charts/src/chart_types/xy_chart/legend/legend.ts index f2925e2e30..8ba182e20c 100644 --- a/packages/charts/src/chart_types/xy_chart/legend/legend.ts +++ b/packages/charts/src/chart_types/xy_chart/legend/legend.ts @@ -128,6 +128,7 @@ export function computeLegend( const formattedItemValue = itemValue !== null ? formatter(itemValue) : ''; legendItems.push({ + depth: 0, color, label: banded ? getBandedLegendItemLabel(name, BandedAccessorType.Y1, postFixes) : name, seriesIdentifiers: [seriesIdentifier], @@ -156,6 +157,7 @@ export function computeLegend( const labelY0 = getBandedLegendItemLabel(name, BandedAccessorType.Y0, postFixes); legendItems.push({ + depth: 0, color, label: labelY0, seriesIdentifiers: [seriesIdentifier], diff --git a/packages/charts/src/chart_types/xy_chart/rendering/rendering.test.ts b/packages/charts/src/chart_types/xy_chart/rendering/rendering.test.ts index 175e25bf31..aea0204cd0 100644 --- a/packages/charts/src/chart_types/xy_chart/rendering/rendering.test.ts +++ b/packages/charts/src/chart_types/xy_chart/rendering/rendering.test.ts @@ -126,6 +126,7 @@ describe('Rendering utils', () => { const highlightedLegendItem: LegendItem = { color: '', label: '', + depth: 0, seriesIdentifiers: [seriesIdentifier], isSeriesHidden: false, values: [], diff --git a/packages/charts/src/chart_types/xy_chart/state/selectors/get_legend_items_labels.ts b/packages/charts/src/chart_types/xy_chart/state/selectors/get_legend_items_labels.ts index d0cce65a20..dcaba2338a 100644 --- a/packages/charts/src/chart_types/xy_chart/state/selectors/get_legend_items_labels.ts +++ b/packages/charts/src/chart_types/xy_chart/state/selectors/get_legend_items_labels.ts @@ -6,19 +6,8 @@ * Side Public License, v 1. */ -import { computeLegendSelector } from './compute_legend'; import { createCustomCachedSelector } from '../../../../state/create_selector'; import { LegendItemLabel } from '../../../../state/selectors/get_legend_items_labels'; -import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_spec'; /** @internal */ -export const getLegendItemsLabelsSelector = createCustomCachedSelector( - [computeLegendSelector, getSettingsSpecSelector], - (legendItems, { showLegendExtra }): LegendItemLabel[] => - legendItems.map(({ label, values }) => { - return { - label: `${label}${showLegendExtra ? values[0]?.label : ''}`, - depth: 0, - }; - }), -); +export const getLegendItemsLabelsSelector = createCustomCachedSelector([], (): LegendItemLabel[] => []); diff --git a/packages/charts/src/chart_types/xy_chart/state/utils/common.test.ts b/packages/charts/src/chart_types/xy_chart/state/utils/common.test.ts index a6b1522471..fe4ff09579 100644 --- a/packages/charts/src/chart_types/xy_chart/state/utils/common.test.ts +++ b/packages/charts/src/chart_types/xy_chart/state/utils/common.test.ts @@ -127,6 +127,7 @@ describe('Type Checks', () => { { color: '#1EA593', label: 'a', + depth: 0, seriesIdentifiers: [ { key: 'specId:{bars},colors:{a}', @@ -141,6 +142,7 @@ describe('Type Checks', () => { { color: '#2B70F7', label: 'b', + depth: 0, seriesIdentifiers: [ { key: 'specId:{bars},colors:{b}', @@ -160,6 +162,7 @@ describe('Type Checks', () => { { color: '#1EA593', label: 'a', + depth: 0, seriesIdentifiers: [ { key: 'specId:{bars},colors:{a}', @@ -174,6 +177,7 @@ describe('Type Checks', () => { { color: '#2B70F7', label: 'b', + depth: 0, seriesIdentifiers: [ { key: 'specId:{bars},colors:{b}', diff --git a/packages/charts/src/common/legend.ts b/packages/charts/src/common/legend.ts index 0eea3d1e34..ae9dbc11a7 100644 --- a/packages/charts/src/common/legend.ts +++ b/packages/charts/src/common/legend.ts @@ -24,7 +24,8 @@ export type LegendItemValue = { value: PrimitiveValue; label: string }; export type LegendItem = { seriesIdentifiers: SeriesIdentifier[]; childId?: LegendItemChildId; - depth?: number; + // zero indexed + depth: number; /** * Path to iterm in hierarchical legend */ diff --git a/packages/charts/src/state/selectors/get_legend_size.ts b/packages/charts/src/state/selectors/get_legend_size.ts index 8ab81b5653..e1af911412 100644 --- a/packages/charts/src/state/selectors/get_legend_size.ts +++ b/packages/charts/src/state/selectors/get_legend_size.ts @@ -8,7 +8,7 @@ import { getChartThemeSelector } from './get_chart_theme'; import { getLegendConfigSelector } from './get_legend_config_selector'; -import { getLegendItemsLabelsSelector } from './get_legend_items_labels'; +import { getLegendItemsSelector } from './get_legend_items'; import { DEFAULT_FONT_FAMILY } from '../../common/default_theme_attributes'; import { LEGEND_HIERARCHY_MARGIN } from '../../components/legend/legend_item'; import { LEGEND_TO_FULL_CONFIG } from '../../components/legend/position_style'; @@ -35,17 +35,23 @@ export type LegendSizing = Size & { /** @internal */ export const getLegendSizeSelector = createCustomCachedSelector( - [getLegendConfigSelector, getChartThemeSelector, getParentDimensionSelector, getLegendItemsLabelsSelector], - (legendConfig, theme, parentDimensions, labels): LegendSizing => { - if (!legendConfig.showLegend) { + [getLegendConfigSelector, getChartThemeSelector, getParentDimensionSelector, getLegendItemsSelector], + ( + { showLegend, legendSize, showLegendExtra, legendPosition, legendAction }, + theme, + parentDimensions, + items, + ): LegendSizing => { + if (!showLegend) { return { width: 0, height: 0, margin: 0, position: LEGEND_TO_FULL_CONFIG[Position.Right] }; } const bbox = withTextMeasure((textMeasure) => - labels.reduce( - (acc, { label, depth }) => { + items.reduce( + (acc, { label, depth, values }) => { + const itemLabel = `${label}${showLegendExtra ? values[0]?.label ?? '' : ''}`; const { width, height } = textMeasure( - label, + itemLabel, { fontFamily: DEFAULT_FONT_FAMILY, fontVariant: 'normal', fontWeight: 400, fontStyle: 'normal' }, 12, 1.5, @@ -58,22 +64,22 @@ export const getLegendSizeSelector = createCustomCachedSelector( ), ); - const { showLegendExtra: showLegendDisplayValue, legendPosition, legendAction } = legendConfig; const { legend: { verticalWidth, spacingBuffer, margin }, } = theme; const actionDimension = isDefined(legendAction) ? 24 : 0; // max width plus margin - const legendItemWidth = MARKER_WIDTH + SHARED_MARGIN + bbox.width + (showLegendDisplayValue ? SHARED_MARGIN : 0); + const showExtraMargin = showLegendExtra; // && items.every(({ values }) => values.length > 0); // remove unnecessary margin + const legendItemWidth = MARKER_WIDTH + SHARED_MARGIN + bbox.width + (showExtraMargin ? SHARED_MARGIN : 0); if (legendPosition.direction === LayoutDirection.Vertical) { const legendItemHeight = bbox.height + VERTICAL_PADDING * 2; - const legendHeight = legendItemHeight * labels.length + TOP_MARGIN; + const legendHeight = legendItemHeight * items.length + TOP_MARGIN; const scrollBarDimension = legendHeight > parentDimensions.height ? SCROLL_BAR_WIDTH : 0; const staticWidth = spacingBuffer + actionDimension + scrollBarDimension; - const width = Number.isFinite(legendConfig.legendSize) - ? Math.min(Math.max(legendConfig.legendSize, legendItemWidth * 0.3 + staticWidth), parentDimensions.width * 0.7) + const width = Number.isFinite(legendSize) + ? Math.min(Math.max(legendSize, legendItemWidth * 0.3 + staticWidth), parentDimensions.width * 0.7) : Math.floor(Math.min(legendItemWidth + staticWidth, verticalWidth)); return { @@ -83,9 +89,9 @@ export const getLegendSizeSelector = createCustomCachedSelector( position: legendPosition, }; } - const isSingleLine = (parentDimensions.width - 20) / 200 > labels.length; - const height = Number.isFinite(legendConfig.legendSize) - ? Math.min(legendConfig.legendSize, parentDimensions.height * 0.7) + const isSingleLine = (parentDimensions.width - 20) / 200 > items.length; + const height = Number.isFinite(legendSize) + ? Math.min(legendSize, parentDimensions.height * 0.7) : isSingleLine ? bbox.height + 16 : bbox.height * 2 + 24; diff --git a/storybook/stories/axes/6a_different_tooltip_formatter.story.tsx b/storybook/stories/axes/6a_different_tooltip_formatter.story.tsx index 14eaf56fe4..6b52cce0d7 100644 --- a/storybook/stories/axes/6a_different_tooltip_formatter.story.tsx +++ b/storybook/stories/axes/6a_different_tooltip_formatter.story.tsx @@ -16,7 +16,8 @@ import { ChartsStory } from '../../types'; import { useBaseTheme } from '../../use_base_theme'; export const Example: ChartsStory = (_, { title, description }) => { - const showLegend = boolean('Show legend', true, 'Y axis'); + const showLegend = boolean('Show legend', true); + const showLegendExtra = boolean('Show legend values', true); const disableYAxisFormat = boolean('Disable Axis tickFormat', false, 'Y axis'); const yAxisFormat = text('Axis value format', '0[.]0', 'Y axis'); const yAxisUnit = text('Axis unit', 'pets', 'Y axis'); @@ -31,7 +32,7 @@ export const Example: ChartsStory = (_, { title, description }) => { return ( - + `${value}${headerUnit ? ` ${headerUnit}` : ''}` From 464d50e473d237a0734beacbe92bd3d3fda1539f Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Mon, 11 Mar 2024 18:42:20 +0100 Subject: [PATCH 04/15] feat: add aggregation functions --- .../xy_chart/state/utils/get_last_value.ts | 96 ++++++++-- .../charts/src/common/aggregations.test.ts | 131 ++++++++++++++ packages/charts/src/common/aggregations.ts | 165 ++++++++++++++++++ 3 files changed, 379 insertions(+), 13 deletions(-) create mode 100644 packages/charts/src/common/aggregations.test.ts create mode 100644 packages/charts/src/common/aggregations.ts diff --git a/packages/charts/src/chart_types/xy_chart/state/utils/get_last_value.ts b/packages/charts/src/chart_types/xy_chart/state/utils/get_last_value.ts index b8e7db1b62..070dca6702 100644 --- a/packages/charts/src/chart_types/xy_chart/state/utils/get_last_value.ts +++ b/packages/charts/src/chart_types/xy_chart/state/utils/get_last_value.ts @@ -8,18 +8,67 @@ import { $Values } from 'utility-types'; +import { + firstNonNull, + lastNonNull, + median, + average, + min, + max, + sum, + countNonNull, + distinctCount, + variance, + stdDeviation, + range, + difference, + differencePercent, +} from '../../../../common/aggregations'; import { ScaleType } from '../../../../scales/constants'; import { XDomain } from '../../domains/types'; -import { isDatumFilled } from '../../rendering/utils'; import { DataSeries, DataSeriesDatum } from '../../utils/series'; -/** @internal */ +/** @public */ export const LegendValue = Object.freeze({ None: 'none' as const, + /** Last value considering all data points in the chart */ LastValue: 'lastValue' as const, + /** Last non-null value */ LastNonNullValue: 'lastNonNullValue' as const, + /** Average value considering all data points in the chart */ + Average: 'average' as const, + /** Median value considering all data points in the chart */ + Median: 'median' as const, + /** Maximum value considering all data points in the chart */ + Max: 'max' as const, + /** Minimum value considering all data points in the chart */ + Min: 'min' as const, + /** First value considering all data points in the chart */ + FirstValue: 'firstValue' as const, + /** First non-null value */ + FirstNonNullValue: 'firstNonNullValue' as const, + /** Sum of al values plotted in the chart */ + Total: 'total' as const, + /** number of data points plotted in the chart */ + Count: 'count' as const, + /** number of data points with different values plotted in the chart */ + DistinctCount: 'distinctCount' as const, + /** Variance of all data points plotted in the chart */ + Variance: 'variance' as const, + /** Standard deviation of all data points plotted in the chart */ + StdDeviation: 'stdDeviation' as const, + /** Difference between min and max values */ + Range: 'range' as const, + /** Difference between first and last values */ + Difference: 'difference' as const, + /** % difference between first and last values */ + DifferencePercent: 'differencePercent' as const, + /** Partition section value */ + Value: 'value' as const, + /** Partition section value in percent */ + Percent: 'percent' as const, }); -/** @internal */ +/** @public */ export type LegendValue = $Values; /** @@ -38,18 +87,39 @@ export function getLegendValue( if (xDomain.type === ScaleType.Ordinal) { return null; } - switch (type) { - case LegendValue.LastNonNullValue: { - const last = series.data.findLast((d) => d.x === xDomain.dataDomain[1] && valueAccessor(d) !== null); - return last ? valueAccessor(last) : null; - } + case LegendValue.FirstNonNullValue: + return firstNonNull(series.data, valueAccessor); + case LegendValue.FirstValue: + return series.data.length === 0 ? null : valueAccessor(series.data.at(1)!); + case LegendValue.LastNonNullValue: + return lastNonNull(series.data, valueAccessor); case LegendValue.LastValue: - const last = series.data.findLast((d) => d.x === xDomain.dataDomain[1]); - if (last && !isDatumFilled(last)) { - return valueAccessor(last); - } - return null; + return series.data.length === 0 ? null : valueAccessor(series.data.at(-1)!); + case LegendValue.Average: + return average(series.data, valueAccessor); + case LegendValue.Median: + return median(series.data, valueAccessor); + case LegendValue.Min: + return min(series.data, valueAccessor); + case LegendValue.Max: + return max(series.data, valueAccessor); + case LegendValue.Total: + return sum(series.data, valueAccessor).sum; + case LegendValue.Count: + return countNonNull(series.data, valueAccessor); + case LegendValue.DistinctCount: + return distinctCount(series.data, valueAccessor); + case LegendValue.Variance: + return variance(series.data, valueAccessor); + case LegendValue.StdDeviation: + return stdDeviation(series.data, valueAccessor); + case LegendValue.Range: + return range(series.data, valueAccessor); + case LegendValue.Difference: + return difference(series.data, valueAccessor); + case LegendValue.DifferencePercent: + return differencePercent(series.data, valueAccessor); default: case LegendValue.None: return null; diff --git a/packages/charts/src/common/aggregations.test.ts b/packages/charts/src/common/aggregations.test.ts new file mode 100644 index 0000000000..fcb0101d48 --- /dev/null +++ b/packages/charts/src/common/aggregations.test.ts @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + countNonNull, + sum, + average, + median, + min, + max, + range, + distinctCount, + stdDeviation, + variance, + difference, + differencePercent, +} from './aggregations'; +const numericArray = [3, 4, 5, 6, 6, 7, 7.5, 8, 1, 2, 9]; +const numericAccessor = (d: number) => d; +const numericArrayWithNull = [3, null, 5, 6, null, 7, 7.5, 8, 1, 2, 9]; +const numericAccessorWithNull = (d: number | null) => d; +const objectArray = [ + { y: 3 }, + { y: 4 }, + { y: 5 }, + { y: 6 }, + { y: 6 }, + { y: 7 }, + { y: 7.5 }, + { y: 8 }, + { y: 1 }, + { y: 2 }, + { y: 9 }, +]; +const objArrayAccessor = (d: { y: number | null }) => d.y; + +const objectArrayWithNull = [ + { y: 3 }, + { y: null }, + { y: 5 }, + { y: 6 }, + { y: null }, + { y: 7 }, + { y: 7.5 }, + { y: 8 }, + { y: 1 }, + { y: 2 }, + { y: 9 }, +]; + +describe('Aggregations', () => { + it('sum', () => { + expect(sum(numericArray, numericAccessor).sum).toBe(58.5); + expect(sum(objectArray, objArrayAccessor).sum).toBe(58.5); + expect(sum(numericArrayWithNull, numericAccessorWithNull).sum).toBe(48.5); + expect(sum(objectArrayWithNull, objArrayAccessor).sum).toBe(48.5); + expect(sum(numericArrayWithNull, numericAccessorWithNull).validCount).toBe(9); + expect(sum(objectArrayWithNull, objArrayAccessor).validCount).toBe(9); + }); + it('count', () => { + expect(countNonNull(numericArray, numericAccessor)).toBe(11); + expect(countNonNull(objectArray, objArrayAccessor)).toBe(11); + expect(countNonNull(numericArrayWithNull, numericAccessorWithNull)).toBe(9); + expect(countNonNull(objectArrayWithNull, objArrayAccessor)).toBe(9); + }); + it('distinct count', () => { + expect(distinctCount(numericArray, numericAccessorWithNull)).toBe(10); + expect(distinctCount(objectArray, objArrayAccessor)).toBe(10); + expect(distinctCount(numericArrayWithNull, numericAccessorWithNull)).toBe(9); + expect(distinctCount(objectArrayWithNull, objArrayAccessor)).toBe(9); + }); + it('average', () => { + expect(average(numericArray, numericAccessorWithNull)).toBeCloseTo(5.318); + expect(average(objectArray, objArrayAccessor)).toBeCloseTo(5.318); + expect(average(numericArrayWithNull, numericAccessorWithNull)).toBeCloseTo(5.388); + expect(average(objectArrayWithNull, objArrayAccessor)).toBeCloseTo(5.388); + }); + it('median', () => { + expect(median(numericArray, numericAccessorWithNull)).toBe(6); + expect(median(objectArray, objArrayAccessor)).toBe(6); + expect(median(numericArrayWithNull, numericAccessorWithNull)).toBe(6); + expect(median(objectArrayWithNull, objArrayAccessor)).toBe(6); + }); + it('min', () => { + expect(min(numericArray, numericAccessorWithNull)).toBe(1); + expect(min(objectArray, objArrayAccessor)).toBe(1); + expect(min(numericArrayWithNull, numericAccessorWithNull)).toBe(1); + expect(min(objectArrayWithNull, objArrayAccessor)).toBe(1); + }); + it('max', () => { + expect(max(numericArray, numericAccessorWithNull)).toBe(9); + expect(max(objectArray, objArrayAccessor)).toBe(9); + expect(max(numericArrayWithNull, numericAccessorWithNull)).toBe(9); + expect(max(objectArrayWithNull, objArrayAccessor)).toBe(9); + }); + it('range', () => { + expect(range(numericArray, numericAccessorWithNull)).toBe(8); + expect(range(objectArray, objArrayAccessor)).toBe(8); + expect(range(numericArrayWithNull, numericAccessorWithNull)).toBe(8); + expect(range(objectArrayWithNull, objArrayAccessor)).toBe(8); + }); + it('diff', () => { + expect(difference(numericArray, numericAccessorWithNull)).toBe(6); + expect(difference(objectArray, objArrayAccessor)).toBe(6); + expect(difference(numericArrayWithNull, numericAccessorWithNull)).toBe(6); + expect(difference(objectArrayWithNull, objArrayAccessor)).toBe(6); + }); + it('diff percent', () => { + expect(differencePercent(numericArray, numericAccessorWithNull)).toBeCloseTo(0.666); + expect(differencePercent(objectArray, objArrayAccessor)).toBeCloseTo(0.666); + expect(differencePercent(numericArrayWithNull, numericAccessorWithNull)).toBeCloseTo(0.666); + expect(differencePercent(objectArrayWithNull, objArrayAccessor)).toBeCloseTo(0.666); + }); + it('variance', () => { + expect(variance(numericArray, numericAccessorWithNull)).toBeCloseTo(6.613); + expect(variance(objectArray, objArrayAccessor)).toBeCloseTo(6.613); + expect(variance(numericArrayWithNull, numericAccessorWithNull)).toBeCloseTo(7.986); + expect(variance(objectArrayWithNull, objArrayAccessor)).toBeCloseTo(7.986); + }); + it('std deviation', () => { + expect(stdDeviation(numericArray, numericAccessorWithNull)).toBeCloseTo(2.5716); + expect(stdDeviation(objectArray, objArrayAccessor)).toBeCloseTo(2.5716); + expect(stdDeviation(numericArrayWithNull, numericAccessorWithNull)).toBeCloseTo(2.8259); + expect(stdDeviation(objectArrayWithNull, objArrayAccessor)).toBeCloseTo(2.8259); + }); +}); diff --git a/packages/charts/src/common/aggregations.ts b/packages/charts/src/common/aggregations.ts new file mode 100644 index 0000000000..3ced5c613e --- /dev/null +++ b/packages/charts/src/common/aggregations.ts @@ -0,0 +1,165 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/** @internal */ +export function sum(arr: D[], accessor: (d: D) => number | null): { sum: number; validCount: number } { + return arr.reduce<{ sum: number; validCount: number }>( + (acc, d) => { + const value = accessor(d); + return { + sum: acc.sum + (value !== null ? value : 0), + validCount: value !== null ? acc.validCount + 1 : acc.validCount, + }; + }, + { sum: 0, validCount: 0 }, + ); +} + +/** @internal */ +export function average(arr: D[], accessor: (d: D) => number | null): number { + const total = sum(arr, accessor); + if (total.validCount === 0) return NaN; + return total.sum / total.validCount; +} + +/** @internal */ +export function median(input: D[], accessor: (d: D) => number | null): number { + const arr: number[] = input.reduce((acc, d) => { + const value = accessor(d); + if (value !== null) { + acc.push(value); + } + return acc; + }, []); + if (!arr.length) return NaN; + const s = arr.toSorted((a, b) => a - b); + const mid = Math.floor(s.length / 2); + if (s.length % 2) { + return s[mid] ?? NaN; + } + return ((s[mid - 1] ?? NaN) + (s[mid] ?? NaN)) / 2; +} + +/** @internal */ +export function min(arr: D[], accessor: (d: D) => number | null): number { + if (arr.length === 0) return NaN; + return arr.reduce((m, d) => { + const value = accessor(d); + return value !== null ? Math.min(m, value) : m; + }, +Infinity); +} +/** @internal */ +export function max(arr: D[], accessor: (d: D) => number | null): number { + if (arr.length === 0) return NaN; + return arr.reduce((m, d) => { + const value = accessor(d); + return value !== null ? Math.max(m, value) : m; + }, -Infinity); +} + +/** @internal */ +export function range(arr: D[], accessor: (d: D) => number | null): number { + if (arr.length === 0) return NaN; + const minMax = arr.reduce<{ min: number; max: number }>( + (m, d) => { + const value = accessor(d); + return { + max: value !== null ? Math.max(m.max, value) : m.max, + min: value !== null ? Math.min(m.min, value) : m.min, + }; + }, + { min: +Infinity, max: -Infinity }, + ); + return minMax.max - minMax.min; +} + +/** @internal */ +export function countNonNull(arr: D[], accessor: (d: D) => number | null): number { + return arr.filter((d) => accessor(d) !== null).length; +} +/** @internal */ +export function distinctCount(arr: D[], accessor: (d: D) => number | null): number { + return new Set(nonNullArray(arr, accessor)).size; +} + +/** @internal */ +export function nonNullArray(arr: D[], accessor: (d: D) => number | null): number[] { + return arr.reduce((acc, d) => { + const value = accessor(d); + if (value !== null) { + acc.push(value); + } + return acc; + }, []); +} + +/** @internal */ +export function variance(arr: D[], accessor: (d: D) => number | null): number { + const nonNullArr = nonNullArray(arr, accessor); + const ddof = 1; + const total = sum(nonNullArr, (d) => d); + const avg = total.sum / total.validCount; + const squareDiffs = nonNullArr.map((d) => { + return Math.abs(Math.pow(d - avg, 2)); + }); + const totalSumOfSquareDiffs = sum(squareDiffs, (d: number) => d); + return totalSumOfSquareDiffs.sum / (totalSumOfSquareDiffs.validCount - ddof); +} + +/** @internal */ +export function stdDeviation(arr: D[], accessor: (d: D) => number | null): number { + const v = variance(arr, accessor); + const std = Math.sqrt(v); + return std; +} + +/** @internal */ +export function firstNonNull(arr: D[], accessor: (d: D) => number | null): number | null { + for (let i = 0; i < arr.length; i++) { + const item = arr[i]; + if (!item) continue; + const value = accessor(item); + if (value) { + return value; + } + } + return null; +} + +/** @internal */ +export function lastNonNull(arr: D[], accessor: (d: D) => number | null): number | null { + for (let i = arr.length - 1; i >= 0; i--) { + const item = arr[i]; + if (!item) continue; + const value = accessor(item); + if (value) { + return value; + } + } + return null; +} + +/** @internal */ +export function difference(arr: D[], accessor: (d: D) => number | null): number | null { + const first = firstNonNull(arr, accessor); + const last = lastNonNull(arr, accessor); + if (first !== null && last !== null) { + return last - first; + } + return null; +} + +/** @internal */ +export function differencePercent(arr: D[], accessor: (d: D) => number | null): number | null { + const first = firstNonNull(arr, accessor); + const last = lastNonNull(arr, accessor); + if (first !== null && last !== null) { + return (last - first) / last; + } + return null; +} From 04186a4c0f934db44ba1e317b5de06c0227103af Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Mon, 11 Mar 2024 18:46:04 +0100 Subject: [PATCH 05/15] fix: story --- storybook/stories/legend/16_custom_legend.story.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/storybook/stories/legend/16_custom_legend.story.tsx b/storybook/stories/legend/16_custom_legend.story.tsx index f633261963..6579216ee7 100644 --- a/storybook/stories/legend/16_custom_legend.story.tsx +++ b/storybook/stories/legend/16_custom_legend.story.tsx @@ -52,7 +52,7 @@ export const Example: ChartsStory = (_, { title, description }) => { onClick={() => i.onItemClickAction(false)} style={{ display: 'block', color: i.isSeriesHidden ? 'gray' : i.color }} > - {i.label} {i.extraValue?.formatted} + {i.label} {i.extraValue?.label} ))}
From 08db6b250e76990b12cdfab198be1f777077b28f Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Tue, 12 Mar 2024 09:25:33 +0100 Subject: [PATCH 06/15] fix: discard filled datum in legend stats --- .../charts/src/chart_types/xy_chart/legend/legend.ts | 5 +++++ .../xy_chart/state/utils/get_last_value.ts | 12 +++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/charts/src/chart_types/xy_chart/legend/legend.ts b/packages/charts/src/chart_types/xy_chart/legend/legend.ts index 8ba182e20c..87546e8769 100644 --- a/packages/charts/src/chart_types/xy_chart/legend/legend.ts +++ b/packages/charts/src/chart_types/xy_chart/legend/legend.ts @@ -15,6 +15,7 @@ import { BandedAccessorType } from '../../../utils/geometry'; import { getLegendCompareFn, SeriesCompareFn } from '../../../utils/series_sort'; import { PointStyle, Theme } from '../../../utils/themes/theme'; import { XDomain } from '../domains/types'; +import { isDatumFilled } from '../rendering/utils'; import { LegendValue, getLegendValue } from '../state/utils/get_last_value'; import { getAxesSpecForSpecId, getSpecsById } from '../state/utils/spec'; import { Y0_ACCESSOR_POSTFIX, Y1_ACCESSOR_POSTFIX } from '../tooltip/tooltip'; @@ -119,6 +120,10 @@ export function computeLegend( const pointStyle = getPointStyle(spec, theme); const itemValue = getLegendValue(series, xDomain, legendValueMode, (d) => { + // don't consider filled in data in the calculations + if (isDatumFilled(d)) { + return null; + } return series.stackMode === StackMode.Percentage ? d.y1 === null || d.y0 === null ? null diff --git a/packages/charts/src/chart_types/xy_chart/state/utils/get_last_value.ts b/packages/charts/src/chart_types/xy_chart/state/utils/get_last_value.ts index 070dca6702..333674cc14 100644 --- a/packages/charts/src/chart_types/xy_chart/state/utils/get_last_value.ts +++ b/packages/charts/src/chart_types/xy_chart/state/utils/get_last_value.ts @@ -89,13 +89,15 @@ export function getLegendValue( } switch (type) { case LegendValue.FirstNonNullValue: - return firstNonNull(series.data, valueAccessor); - case LegendValue.FirstValue: - return series.data.length === 0 ? null : valueAccessor(series.data.at(1)!); + return firstNonNull(series.data, (d) => (d.x === xDomain.dataDomain[0] ? valueAccessor(d) : null)); case LegendValue.LastNonNullValue: - return lastNonNull(series.data, valueAccessor); + return lastNonNull(series.data, (d) => (d.x === xDomain.dataDomain[1] ? valueAccessor(d) : null)); + case LegendValue.FirstValue: + const first = series.data.at(0); + return first ? valueAccessor(first) : null; case LegendValue.LastValue: - return series.data.length === 0 ? null : valueAccessor(series.data.at(-1)!); + const last = series.data.at(-1); + return last ? valueAccessor(last) : null; case LegendValue.Average: return average(series.data, valueAccessor); case LegendValue.Median: From a14ff42fdd6318e58cca1cfe861f3ed32e846891 Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Tue, 12 Mar 2024 09:46:58 +0100 Subject: [PATCH 07/15] chore(API): expose missing type --- packages/charts/api/charts.api.md | 10 ++++++---- packages/charts/src/index.ts | 1 + 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/charts/api/charts.api.md b/packages/charts/api/charts.api.md index 280aa06b29..fe6f47530e 100644 --- a/packages/charts/api/charts.api.md +++ b/packages/charts/api/charts.api.md @@ -1802,6 +1802,12 @@ export interface LegendColorPickerProps { // @public (undocumented) export type LegendItemListener = (series: SeriesIdentifier[]) => void; +// @public (undocumented) +export type LegendItemValue = { + value: PrimitiveValue; + label: string; +}; + // @public (undocumented) export interface LegendLabelOptions { maxLines: number; @@ -3494,10 +3500,6 @@ export interface YDomainBase { // @public (undocumented) export type YDomainRange = YDomainBase & DomainRange & LogScaleOptions; -// Warnings were encountered during analysis: -// -// src/specs/settings.tsx:394:5 - (ae-forgotten-export) The symbol "LegendItemValue" needs to be exported by the entry point index.d.ts - // (No @packageDocumentation comment for this package) ``` diff --git a/packages/charts/src/index.ts b/packages/charts/src/index.ts index 6e27f1589b..b9ad39b0b4 100644 --- a/packages/charts/src/index.ts +++ b/packages/charts/src/index.ts @@ -46,6 +46,7 @@ export { } from './chart_types/xy_chart/annotations/types'; export { GeometryValue, BandedAccessorType } from './utils/geometry'; export { LegendPath, LegendPathElement } from './state/actions/legend'; +export { LegendItemValue } from './common/legend'; export { CategoryKey, CategoryLabel } from './common/category'; export { Layer as PartitionLayer, PartitionProps } from './chart_types/partition_chart/specs/index'; export { FillLabelConfig as PartitionFillLabel, PartitionStyle } from './utils/themes/partition'; From 29d32464ebaf49b15aa198dcfa99349335bc77df Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Tue, 12 Mar 2024 09:50:46 +0100 Subject: [PATCH 08/15] use slice.sort instead of toSorted --- packages/charts/src/common/aggregations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/charts/src/common/aggregations.ts b/packages/charts/src/common/aggregations.ts index 3ced5c613e..b704567af3 100644 --- a/packages/charts/src/common/aggregations.ts +++ b/packages/charts/src/common/aggregations.ts @@ -37,7 +37,7 @@ export function median(input: D[], accessor: (d: D) => number | null): number return acc; }, []); if (!arr.length) return NaN; - const s = arr.toSorted((a, b) => a - b); + const s = arr.slice().sort((a, b) => a - b); const mid = Math.floor(s.length / 2); if (s.length % 2) { return s[mid] ?? NaN; From 029552f9d025f4a5e136626632734d7152516a0a Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Tue, 12 Mar 2024 11:10:06 +0100 Subject: [PATCH 09/15] fix: first/last correct behaviour --- .../src/chart_types/xy_chart/legend/legend.ts | 37 ++++++++++++------- .../xy_chart/state/utils/get_last_value.ts | 8 ++-- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/packages/charts/src/chart_types/xy_chart/legend/legend.ts b/packages/charts/src/chart_types/xy_chart/legend/legend.ts index 87546e8769..5d6f8fbe53 100644 --- a/packages/charts/src/chart_types/xy_chart/legend/legend.ts +++ b/packages/charts/src/chart_types/xy_chart/legend/legend.ts @@ -29,6 +29,7 @@ import { getSeriesKey, getSeriesIdentifierFromDataSeries, isBandedSpec, + DataSeriesDatum, } from '../utils/series'; import { AxisSpec, @@ -73,6 +74,26 @@ function getPointStyle(spec: BasicSeriesSpec, theme: Theme): PointStyle | undefi } } +const y1Accessor = + (stackMode?: StackMode) => + (d: DataSeriesDatum): number | null => { + // don't consider filled in data in the calculations + if (isDatumFilled(d)) { + return null; + } + return stackMode === StackMode.Percentage ? (d.y1 === null || d.y0 === null ? null : d.y1 - d.y0) : d.initialY1; + }; + +const y0Accessor = + (stackMode?: StackMode) => + (d: DataSeriesDatum): number | null => { + // don't consider filled in data in the calculations + if (isDatumFilled(d)) { + return null; + } + return stackMode === StackMode.Percentage ? d.y0 : d.initialY0; + }; + /** @internal */ export function computeLegend( xDomain: XDomain, @@ -119,17 +140,7 @@ export function computeLegend( const pointStyle = getPointStyle(spec, theme); - const itemValue = getLegendValue(series, xDomain, legendValueMode, (d) => { - // don't consider filled in data in the calculations - if (isDatumFilled(d)) { - return null; - } - return series.stackMode === StackMode.Percentage - ? d.y1 === null || d.y0 === null - ? null - : d.y1 - d.y0 - : d.initialY1; - }); + const itemValue = getLegendValue(series, xDomain, legendValueMode, y1Accessor(series.stackMode)); const formattedItemValue = itemValue !== null ? formatter(itemValue) : ''; legendItems.push({ @@ -155,9 +166,7 @@ export function computeLegend( pointStyle, }); if (banded) { - const bandedItemValue = getLegendValue(series, xDomain, legendValueMode, (d) => { - return series.stackMode === StackMode.Percentage ? d.y0 : d.initialY0; - }); + const bandedItemValue = getLegendValue(series, xDomain, legendValueMode, y0Accessor(series.stackMode)); const bandedFormattedItemValue = bandedItemValue !== null ? formatter(bandedItemValue) : ''; const labelY0 = getBandedLegendItemLabel(name, BandedAccessorType.Y0, postFixes); diff --git a/packages/charts/src/chart_types/xy_chart/state/utils/get_last_value.ts b/packages/charts/src/chart_types/xy_chart/state/utils/get_last_value.ts index 333674cc14..a5aea62db4 100644 --- a/packages/charts/src/chart_types/xy_chart/state/utils/get_last_value.ts +++ b/packages/charts/src/chart_types/xy_chart/state/utils/get_last_value.ts @@ -89,14 +89,14 @@ export function getLegendValue( } switch (type) { case LegendValue.FirstNonNullValue: - return firstNonNull(series.data, (d) => (d.x === xDomain.dataDomain[0] ? valueAccessor(d) : null)); + return firstNonNull(series.data, valueAccessor); case LegendValue.LastNonNullValue: - return lastNonNull(series.data, (d) => (d.x === xDomain.dataDomain[1] ? valueAccessor(d) : null)); + return lastNonNull(series.data, valueAccessor); case LegendValue.FirstValue: - const first = series.data.at(0); + const first = series.data.find((d) => d.x === xDomain.dataDomain[0]); return first ? valueAccessor(first) : null; case LegendValue.LastValue: - const last = series.data.at(-1); + const last = series.data.findLast((d) => d.x === xDomain.dataDomain[1]); return last ? valueAccessor(last) : null; case LegendValue.Average: return average(series.data, valueAccessor); From ec7d75cfb743ff13edec0f5ace2e96ce4eadb1ea Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Tue, 12 Mar 2024 11:35:09 +0100 Subject: [PATCH 10/15] feat: specify the preferred legend stat --- packages/charts/api/charts.api.md | 30 +++++++++++- .../selectors/get_legend_items_labels.ts | 4 +- .../selectors/get_legend_items_labels.ts | 4 +- .../src/chart_types/xy_chart/legend/legend.ts | 6 +-- .../xy_chart/state/utils/get_last_value.ts | 46 +------------------ packages/charts/src/common/legend.ts | 45 ++++++++++++++++++ .../__snapshots__/chart.test.tsx.snap | 2 +- .../legend/__snapshots__/legend.test.tsx.snap | 24 +++++----- .../src/components/legend/legend.test.tsx | 16 +++++-- .../charts/src/components/legend/legend.tsx | 2 +- .../components/legend/legend_icon.test.tsx | 5 +- .../src/components/legend/legend_item.tsx | 8 ++-- packages/charts/src/index.ts | 2 +- packages/charts/src/specs/constants.ts | 2 +- packages/charts/src/specs/settings.test.tsx | 4 +- packages/charts/src/specs/settings.tsx | 8 ++-- .../selectors/get_legend_config_selector.ts | 4 +- .../src/state/selectors/get_legend_size.ts | 6 +-- .../lines/1_x_continuous.story.tsx | 9 +++- .../area/10_stacked_same_naming.story.tsx | 9 +++- storybook/stories/area/13_band_area.story.tsx | 8 +++- .../area/6_with_axis_and_legend.story.tsx | 9 +++- storybook/stories/area/7_stacked.story.tsx | 9 +++- .../area/8_stacked_percentage.story.tsx | 19 +++++++- .../area/8_stacked_percentage_zeros.story.tsx | 19 +++++++- .../area/9_stacked_separate_specs.story.tsx | 9 +++- .../6a_different_tooltip_formatter.story.tsx | 8 +++- .../stories/bar/10_axis_and_legend.story.tsx | 9 +++- .../11_stacked_with_axis_and_legend.story.tsx | 9 +++- storybook/stories/bar/13_clustered.story.tsx | 4 +- .../bar/14_clustered_multiple.story.tsx | 9 +++- .../stories/bar/18_bar_chart_1y0g.story.tsx | 9 +++- .../stories/bar/19_bar_chart_1y1g.story.tsx | 9 +++- .../stories/bar/20_bar_chart_1y2g.story.tsx | 9 +++- .../stories/bar/21_bar_chart_2y0g.story.tsx | 9 +++- .../stories/bar/22_barchart_2y1g.story.tsx | 9 +++- .../stories/bar/23_bar_chart_2y2g.story.tsx | 9 +++- .../bar/24_tooltip_visibility.story.tsx | 9 +++- storybook/stories/bar/2_label_value.story.tsx | 3 +- .../bar/50_order_bins_by_sum.story.tsx | 4 +- .../bar/51_label_value_advanced.story.tsx | 3 +- storybook/stories/debug/1_basic.story.tsx | 20 +++++++- .../stories/debug/2_debug_state.story.tsx | 3 +- .../interactions/15_render_change.story.tsx | 4 +- .../16_cursor_update_action.story.tsx | 5 +- .../interactions/17_png_export.story.tsx | 3 +- .../interactions/1_bar_clicks.story.tsx | 3 +- .../2_area_point_clicks.story.tsx | 4 +- .../3_line_point_clicks.story.tsx | 4 +- .../4_line_area_bar_clicks.story.tsx | 14 +++++- .../4_sunburst_slice_clicks.story.tsx | 4 +- .../5_clicks_legend_items_bar.story.tsx | 4 +- .../6_clicks_legend_items_area.story.tsx | 4 +- .../7_clicks_legend_items_line.story.tsx | 4 +- .../8_clicks_legend_items_mixed.story.tsx | 4 +- .../stories/legend/10_sunburst.story.tsx | 3 +- .../legend/11_legend_actions.story.tsx | 13 +++++- .../stories/legend/13_inside_chart.story.tsx | 3 +- .../stories/legend/14_single_series.story.tsx | 4 +- .../stories/legend/16_custom_legend.story.tsx | 3 +- .../legend/1_legend_positioning.story.tsx | 4 +- .../stories/legend/5_changing_specs.story.tsx | 9 +++- .../stories/legend/6_hide_legend.story.tsx | 9 +++- .../stories/legend/8_spacing_buffer.story.tsx | 10 +++- .../11_discontinuous_data_points.story.tsx | 20 +++++++- .../stories/line/14_point_shapes.story.tsx | 3 +- .../line/5_w_axis_and_legend.story.tsx | 8 +++- storybook/stories/line/6_curved.story.tsx | 8 +++- storybook/stories/line/7_multiple.story.tsx | 8 +++- storybook/stories/line/8_stacked.story.tsx | 8 +++- .../stories/line/9_multi_series.story.tsx | 8 +++- .../stories/mixed/1_bars_and_lines.story.tsx | 9 +++- .../stories/mixed/2_lines_and_areas.story.tsx | 9 +++- .../stories/mixed/3_areas_and_bars.story.tsx | 19 +++++++- storybook/stories/mixed/4_test_bar.story.tsx | 9 +++- .../stories/mixed/5_test_bar_time.story.tsx | 9 +++- storybook/stories/mixed/6_fitting.story.tsx | 3 +- .../stories/mixed/6_fitting_stacked.story.tsx | 4 +- .../mixed/8_polarized_stacked.story.tsx | 8 +++- .../stories/mosaic/10_mosaic_simple.story.tsx | 3 +- .../stories/rotations/1_ordinal.story.tsx | 4 +- .../rotations/2_negative_ordinal.story.tsx | 10 +++- .../rotations/3_rotations_ordinal.story.tsx | 10 +++- .../stories/rotations/4_90_ordinal.story.tsx | 10 +++- .../stories/rotations/5_180_ordinal.story.tsx | 10 +++- .../rotations/6_negative_linear.story.tsx | 10 +++- .../rotations/7_rotations_linear.story.tsx | 10 +++- .../rotations/8_90_deg_linear.story.tsx | 10 +++- .../rotations/9_180_deg_linear.story.tsx | 10 +++- .../small_multiples/7_sunbursts.story.tsx | 3 +- .../stories/stylings/10_custom_bars.story.tsx | 10 +++- .../stylings/11_custom_lines.story.tsx | 3 +- .../stories/stylings/12_custom_area.story.tsx | 3 +- .../stylings/13_custom_series_name.story.tsx | 9 +++- .../13_custom_series_name_config.story.tsx | 18 +++++++- ...14_custom_series_name_formatting.story.tsx | 9 +++- .../17_bar_series_color_variant.story.tsx | 4 +- .../18_line_series_color_variant.story.tsx | 4 +- .../19_area_series_color_variant.story.tsx | 4 +- .../stories/stylings/22_dark_theme.story.tsx | 8 +++- .../stylings/26_highlighter_style.story.tsx | 3 +- .../stories/stylings/2_margins.story.tsx | 4 +- .../stylings/4_theme_styling.story.tsx | 3 +- .../stylings/5_partial_custom_theme.story.tsx | 4 +- .../stylings/6_partial_and_base.story.tsx | 4 +- .../stylings/7_multiple_custom.story.tsx | 4 +- .../8_custom_series_colors_array.story.tsx | 9 +++- .../9_custom_series_colors_function.story.tsx | 8 +++- .../stories/sunburst/14_full_zero.story.tsx | 12 ++++- .../test_cases/21_domain_edges.story.tsx | 7 +-- .../stories/test_cases/7_rtl_text.story.tsx | 3 +- .../stories/treemap/9_zero_values.story.tsx | 12 ++++- storybook/stories/waffle/1_simple.story.tsx | 12 ++++- storybook/stories/waffle/2_test.story.tsx | 4 +- 114 files changed, 685 insertions(+), 258 deletions(-) diff --git a/packages/charts/api/charts.api.md b/packages/charts/api/charts.api.md index fe6f47530e..1d5fece385 100644 --- a/packages/charts/api/charts.api.md +++ b/packages/charts/api/charts.api.md @@ -1843,6 +1843,7 @@ export interface LegendSpec { legendSize: number; legendSort?: SeriesCompareFn; legendStrategy?: LegendStrategy; + legendValues: Array; // (undocumented) onLegendItemClick?: LegendItemListener; // (undocumented) @@ -1854,7 +1855,6 @@ export interface LegendSpec { // (undocumented) onLegendItemPlusClick?: LegendItemListener; showLegend: boolean; - showLegendExtra: boolean; } // @public (undocumented) @@ -1879,6 +1879,32 @@ export interface LegendStyle { verticalWidth: number; } +// @public (undocumented) +export const LegendValue: Readonly<{ + None: "none"; + LastValue: "lastValue"; + LastNonNullValue: "lastNonNullValue"; + Average: "average"; + Median: "median"; + Max: "max"; + Min: "min"; + FirstValue: "firstValue"; + FirstNonNullValue: "firstNonNullValue"; + Total: "total"; + Count: "count"; + DistinctCount: "distinctCount"; + Variance: "variance"; + StdDeviation: "stdDeviation"; + Range: "range"; + Difference: "difference"; + DifferencePercent: "differencePercent"; + Value: "value"; + Percent: "percent"; +}>; + +// @public (undocumented) +export type LegendValue = $Values; + // @public (undocumented) export const LIGHT_BASE_COLORS: ChartBaseColors; @@ -2695,7 +2721,7 @@ export const Settings: (props: SFProps; +export const settingsBuildProps: BuildProps; // @public (undocumented) export type SettingsProps = ComponentProps; diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/get_legend_items_labels.ts b/packages/charts/src/chart_types/heatmap/state/selectors/get_legend_items_labels.ts index 9ea1b27656..2e758de3c7 100644 --- a/packages/charts/src/chart_types/heatmap/state/selectors/get_legend_items_labels.ts +++ b/packages/charts/src/chart_types/heatmap/state/selectors/get_legend_items_labels.ts @@ -14,10 +14,10 @@ import { getSettingsSpecSelector } from '../../../../state/selectors/get_setting /** @internal */ export const getLegendItemsLabelsSelector = createCustomCachedSelector( [computeLegendSelector, getSettingsSpecSelector], - (legendItems, { showLegendExtra }): LegendItemLabel[] => + (legendItems, { legendValues }): LegendItemLabel[] => legendItems.map(({ label, values }) => { return { - label: `${label}${showLegendExtra ? values[0]?.label ?? '' : ''}`, + label: `${label}${legendValues.length > 0 ? values[0]?.label ?? '' : ''}`, depth: 0, }; }), diff --git a/packages/charts/src/chart_types/partition_chart/state/selectors/get_legend_items_labels.ts b/packages/charts/src/chart_types/partition_chart/state/selectors/get_legend_items_labels.ts index a52bed4f60..83b13d7e8d 100644 --- a/packages/charts/src/chart_types/partition_chart/state/selectors/get_legend_items_labels.ts +++ b/packages/charts/src/chart_types/partition_chart/state/selectors/get_legend_items_labels.ts @@ -16,11 +16,11 @@ import { getLegendLabelsAndValue } from '../../layout/utils/legend_labels'; /** @internal */ export const getLegendItemsLabels = createCustomCachedSelector( [getPartitionSpecs, getSettingsSpecSelector, getTrees], - (specs, { legendMaxDepth, showLegend, showLegendExtra }, trees): LegendItemLabel[] => + (specs, { legendMaxDepth, showLegend, legendValues }, trees): LegendItemLabel[] => specs.flatMap(({ layers, valueFormatter }) => showLegend ? trees.flatMap(({ tree }) => - getLegendLabelsAndValue(layers, tree, legendMaxDepth, showLegendExtra ? valueFormatter : () => ''), + getLegendLabelsAndValue(layers, tree, legendMaxDepth, legendValues.length > 0 ? valueFormatter : () => ''), ) : [], ), diff --git a/packages/charts/src/chart_types/xy_chart/legend/legend.ts b/packages/charts/src/chart_types/xy_chart/legend/legend.ts index 5d6f8fbe53..98bc2144e5 100644 --- a/packages/charts/src/chart_types/xy_chart/legend/legend.ts +++ b/packages/charts/src/chart_types/xy_chart/legend/legend.ts @@ -7,7 +7,7 @@ */ import { Color } from '../../../common/colors'; -import { LegendItem } from '../../../common/legend'; +import { LegendItem, LegendValue } from '../../../common/legend'; import { SeriesKey, SeriesIdentifier } from '../../../common/series_id'; import { SettingsSpec } from '../../../specs'; import { isDefined, mergePartial } from '../../../utils/common'; @@ -16,7 +16,7 @@ import { getLegendCompareFn, SeriesCompareFn } from '../../../utils/series_sort' import { PointStyle, Theme } from '../../../utils/themes/theme'; import { XDomain } from '../domains/types'; import { isDatumFilled } from '../rendering/utils'; -import { LegendValue, getLegendValue } from '../state/utils/get_last_value'; +import { getLegendValue } from '../state/utils/get_last_value'; import { getAxesSpecForSpecId, getSpecsById } from '../state/utils/spec'; import { Y0_ACCESSOR_POSTFIX, Y1_ACCESSOR_POSTFIX } from '../tooltip/tooltip'; import { defaultTickFormatter } from '../utils/axis_utils'; @@ -109,7 +109,7 @@ export function computeLegend( const legendItems: LegendItem[] = []; const defaultColor = theme.colors.defaultVizColor; - const legendValueMode = LegendValue.LastValue; + const legendValueMode = settingsSpec.legendValues[0] ?? LegendValue.None; dataSeries.forEach((series) => { const { specId, yAccessor } = series; diff --git a/packages/charts/src/chart_types/xy_chart/state/utils/get_last_value.ts b/packages/charts/src/chart_types/xy_chart/state/utils/get_last_value.ts index a5aea62db4..d4ef0e02f2 100644 --- a/packages/charts/src/chart_types/xy_chart/state/utils/get_last_value.ts +++ b/packages/charts/src/chart_types/xy_chart/state/utils/get_last_value.ts @@ -6,8 +6,6 @@ * Side Public License, v 1. */ -import { $Values } from 'utility-types'; - import { firstNonNull, lastNonNull, @@ -24,53 +22,11 @@ import { difference, differencePercent, } from '../../../../common/aggregations'; +import { LegendValue } from '../../../../common/legend'; import { ScaleType } from '../../../../scales/constants'; import { XDomain } from '../../domains/types'; import { DataSeries, DataSeriesDatum } from '../../utils/series'; -/** @public */ -export const LegendValue = Object.freeze({ - None: 'none' as const, - /** Last value considering all data points in the chart */ - LastValue: 'lastValue' as const, - /** Last non-null value */ - LastNonNullValue: 'lastNonNullValue' as const, - /** Average value considering all data points in the chart */ - Average: 'average' as const, - /** Median value considering all data points in the chart */ - Median: 'median' as const, - /** Maximum value considering all data points in the chart */ - Max: 'max' as const, - /** Minimum value considering all data points in the chart */ - Min: 'min' as const, - /** First value considering all data points in the chart */ - FirstValue: 'firstValue' as const, - /** First non-null value */ - FirstNonNullValue: 'firstNonNullValue' as const, - /** Sum of al values plotted in the chart */ - Total: 'total' as const, - /** number of data points plotted in the chart */ - Count: 'count' as const, - /** number of data points with different values plotted in the chart */ - DistinctCount: 'distinctCount' as const, - /** Variance of all data points plotted in the chart */ - Variance: 'variance' as const, - /** Standard deviation of all data points plotted in the chart */ - StdDeviation: 'stdDeviation' as const, - /** Difference between min and max values */ - Range: 'range' as const, - /** Difference between first and last values */ - Difference: 'difference' as const, - /** % difference between first and last values */ - DifferencePercent: 'differencePercent' as const, - /** Partition section value */ - Value: 'value' as const, - /** Partition section value in percent */ - Percent: 'percent' as const, -}); -/** @public */ -export type LegendValue = $Values; - /** * This method return a value from a DataSeries that correspond to the type of value requested. * It in general compute the last, min, max, avg, sum of the value in a series. diff --git a/packages/charts/src/common/legend.ts b/packages/charts/src/common/legend.ts index ae9dbc11a7..d74b5e8750 100644 --- a/packages/charts/src/common/legend.ts +++ b/packages/charts/src/common/legend.ts @@ -6,6 +6,8 @@ * Side Public License, v 1. */ +import { $Values } from 'utility-types'; + import { CategoryKey, CategoryLabel } from './category'; import { Color } from './colors'; import { SeriesIdentifier } from './series_id'; @@ -20,6 +22,49 @@ export type LegendItemChildId = CategoryKey; /** @public */ export type LegendItemValue = { value: PrimitiveValue; label: string }; +/** @public */ +export const LegendValue = Object.freeze({ + None: 'none' as const, + /** Last value considering all data points in the chart */ + LastValue: 'lastValue' as const, + /** Last non-null value */ + LastNonNullValue: 'lastNonNullValue' as const, + /** Average value considering all data points in the chart */ + Average: 'average' as const, + /** Median value considering all data points in the chart */ + Median: 'median' as const, + /** Maximum value considering all data points in the chart */ + Max: 'max' as const, + /** Minimum value considering all data points in the chart */ + Min: 'min' as const, + /** First value considering all data points in the chart */ + FirstValue: 'firstValue' as const, + /** First non-null value */ + FirstNonNullValue: 'firstNonNullValue' as const, + /** Sum of al values plotted in the chart */ + Total: 'total' as const, + /** number of data points plotted in the chart */ + Count: 'count' as const, + /** number of data points with different values plotted in the chart */ + DistinctCount: 'distinctCount' as const, + /** Variance of all data points plotted in the chart */ + Variance: 'variance' as const, + /** Standard deviation of all data points plotted in the chart */ + StdDeviation: 'stdDeviation' as const, + /** Difference between min and max values */ + Range: 'range' as const, + /** Difference between first and last values */ + Difference: 'difference' as const, + /** % difference between first and last values */ + DifferencePercent: 'differencePercent' as const, + /** Partition section value */ + Value: 'value' as const, + /** Partition section value in percent */ + Percent: 'percent' as const, +}); +/** @public */ +export type LegendValue = $Values; + /** @internal */ export type LegendItem = { seriesIdentifiers: SeriesIdentifier[]; diff --git a/packages/charts/src/components/__snapshots__/chart.test.tsx.snap b/packages/charts/src/components/__snapshots__/chart.test.tsx.snap index 060963eb45..3695910d87 100644 --- a/packages/charts/src/components/__snapshots__/chart.test.tsx.snap +++ b/packages/charts/src/components/__snapshots__/chart.test.tsx.snap @@ -26,7 +26,7 @@ exports[`Chart should render the legend name test 1`] = `