diff --git a/.playground/playground.tsx b/.playground/playground.tsx index 489232967a..939747c428 100644 --- a/.playground/playground.tsx +++ b/.playground/playground.tsx @@ -20,8 +20,7 @@ import React from 'react'; -import { Chart, AreaSeries, Axis, Position, ScaleType, Fit, Settings, CurveType, timeFormatter, niceTimeFormatByDay } from '../src'; -import { data } from './data'; +import { Chart, AreaSeries, Axis, Position, Settings } from '../src'; export class Playground extends React.Component { render() { @@ -36,23 +35,34 @@ export class Playground extends React.Component { d.year !== 2006 || d.series !== 'Manufacturing')} + // xAccessor="date" + // yAccessors={['count']} + // // y0Accessors={['metric0']} + // splitSeriesAccessors={['series']} + // stackAccessors={['date']} + // xScaleType={ScaleType.Time} + // fit={Fit.Lookahead} + // curve={CurveType.CURVE_MONOTONE_X} + // // areaSeriesStyle={{ point: { visible: true } }} + // // stackAsPercentage + // data={data.filter((d) => d.year !== 2006 || d.series !== 'Manufacturing')} + + splitSeriesAccessors={['g']} + yAccessors={['y1']} + stackAccessors={['x']} + data={[ + { x: 1, y1: 1, g: 'a' }, + { x: 4, y1: 4, g: 'a' }, + { x: 2, y1: 2, g: 'a' }, + { x: 3, y1: 23, g: 'b' }, + { x: 1, y1: 21, g: 'b' }, + ]} /> {/* { MockStore.addSpecs([ MockSeriesSpec.area({ ...DEMO_AREA_SPEC_1, - stackAsPercentage: true, + stackMode: StackModes.Percentage, }), MockSeriesSpec.area({ ...DEMO_AREA_SPEC_2, @@ -492,7 +492,7 @@ describe('Y Domain', () => { }), MockSeriesSpec.area({ ...DEMO_AREA_SPEC_1, - stackAsPercentage: true, + stackMode: StackModes.Percentage, }), ], store); const { yDomain } = computeSeriesDomainsSelector(store.getState()); diff --git a/src/chart_types/xy_chart/domains/y_domain.ts b/src/chart_types/xy_chart/domains/y_domain.ts index f7e80fbe92..a0a8b1e67c 100644 --- a/src/chart_types/xy_chart/domains/y_domain.ts +++ b/src/chart_types/xy_chart/domains/y_domain.ts @@ -22,18 +22,19 @@ import { ScaleType } from '../../../scales/constants'; import { identity } from '../../../utils/commons'; import { computeContinuousDataDomain } from '../../../utils/domain'; import { GroupId } from '../../../utils/ids'; +import { Logger } from '../../../utils/logger'; import { isCompleteBound, isLowerBound, isUpperBound } from '../utils/axis_type_utils'; import { DataSeries, FormattedDataSeries } from '../utils/series'; -import { BasicSeriesSpec, YDomainRange, DEFAULT_GLOBAL_ID, SeriesTypes } from '../utils/specs'; +import { BasicSeriesSpec, YDomainRange, DEFAULT_GLOBAL_ID, SeriesTypes, StackModes } from '../utils/specs'; import { YDomain } from './types'; export type YBasicSeriesSpec = Pick< BasicSeriesSpec, 'id' | 'seriesType' | 'yScaleType' | 'groupId' | 'stackAccessors' | 'yScaleToDataExtent' | 'useDefaultGroupDomain' -> & { stackAsPercentage?: boolean; enableHistogramMode?: boolean }; +> & { stackMode?: StackModes; enableHistogramMode?: boolean }; interface GroupSpecs { - isPercentageStack: boolean; + stackMode?: StackModes; stacked: YBasicSeriesSpec[]; nonStacked: YBasicSeriesSpec[]; } @@ -91,10 +92,10 @@ function mergeYDomainForGroup( customDomain?: YDomainRange, ): YDomain { const groupYScaleType = coerceYScaleTypes([...groupSpecs.stacked, ...groupSpecs.nonStacked]); - const { isPercentageStack } = groupSpecs; + const { stackMode } = groupSpecs; let domain: number[]; - if (isPercentageStack) { + if (stackMode === StackModes.Percentage) { domain = computeContinuousDataDomain([0, 1], identity, customDomain); } else { // TODO remove when removing yScaleToDataExtent @@ -166,16 +167,19 @@ function computeYDomain(dataseries: DataSeries[], isStacked = false) { export function splitSpecsByGroupId(specs: YBasicSeriesSpec[]) { const specsByGroupIds = new Map< GroupId, - { isPercentageStack: boolean; stacked: YBasicSeriesSpec[]; nonStacked: YBasicSeriesSpec[] } + { stackMode: StackModes | undefined; stacked: YBasicSeriesSpec[]; nonStacked: YBasicSeriesSpec[] } >(); // After mobx->redux https://github.com/elastic/elastic-charts/pull/281 we keep the specs untouched on mount // in MobX version, the stackAccessors was programmatically added to every histogram specs // in ReduX version, we left untouched the specs, so we have to manually check that - const isHistogramEnabled = specs.some(({ seriesType, enableHistogramMode }) => seriesType === SeriesTypes.Bar && enableHistogramMode); - // split each specs by groupId and by stacked or not + const isHistogramEnabled = specs.some( + ({ seriesType, enableHistogramMode }) => + seriesType === SeriesTypes.Bar && enableHistogramMode + ); + // split each specs by groupId and by stacked or not specs.forEach((spec) => { const group = specsByGroupIds.get(spec.groupId) || { - isPercentageStack: false, + stackMode: undefined, stacked: [], nonStacked: [], }; @@ -189,8 +193,12 @@ export function splitSpecsByGroupId(specs: YBasicSeriesSpec[]) { } else { group.nonStacked.push(spec); } - if (spec.stackAsPercentage === true) { - group.isPercentageStack = true; + if (group.stackMode === undefined && spec.stackMode !== undefined) { + group.stackMode = spec.stackMode; + } + if (spec.stackMode !== undefined && group.stackMode !== undefined && group.stackMode !== spec.stackMode) { + Logger.warn(`Is not possible to mix different stackModes, please align all stackMode on the same GroupId + to the same mode. The default behaviour will be to use the first encountered stackMode on the series`); } specsByGroupIds.set(spec.groupId, group); }); diff --git a/src/chart_types/xy_chart/rendering/rendering.areas.test.ts b/src/chart_types/xy_chart/rendering/rendering.areas.test.ts index 7d3112c5ee..2f2c98b862 100644 --- a/src/chart_types/xy_chart/rendering/rendering.areas.test.ts +++ b/src/chart_types/xy_chart/rendering/rendering.areas.test.ts @@ -27,7 +27,7 @@ import { LIGHT_THEME } from '../../../utils/themes/light_theme'; import { computeSeriesDomains } from '../state/utils/utils'; import { IndexedGeometryMap } from '../utils/indexed_geometry_map'; import { computeXScale, computeYScales } from '../utils/scales'; -import { AreaSeriesSpec, SeriesTypes } from '../utils/specs'; +import { AreaSeriesSpec, SeriesTypes, StackModes } from '../utils/specs'; import { renderArea } from './rendering'; const SPEC_ID = 'spec_1'; @@ -1136,7 +1136,7 @@ describe('Rendering points - areas', () => { xScaleType: ScaleType.Time, yScaleType: ScaleType.Linear, stackAccessors: [0], - stackAsPercentage: true, + stackMode: StackModes.Percentage, }); const pointSeriesSpec2: AreaSeriesSpec = MockSeriesSpec.area({ id: 'spec_2', @@ -1149,7 +1149,7 @@ describe('Rendering points - areas', () => { xScaleType: ScaleType.Time, yScaleType: ScaleType.Linear, stackAccessors: [0], - stackAsPercentage: true, + stackMode: StackModes.Percentage, }); const pointSeriesDomains = computeSeriesDomains([pointSeriesSpec1, pointSeriesSpec2]); expect(pointSeriesDomains.formattedDataSeries.stacked[0].dataSeries[0].data).toMatchObject([ diff --git a/src/chart_types/xy_chart/specs/bar_series.tsx b/src/chart_types/xy_chart/specs/bar_series.tsx index ab6933e99e..d8d4a1e5bd 100644 --- a/src/chart_types/xy_chart/specs/bar_series.tsx +++ b/src/chart_types/xy_chart/specs/bar_series.tsx @@ -37,7 +37,6 @@ const defaultProps = { yScaleToDataExtent: false, hideInLegend: false, enableHistogramMode: false, - stackAsPercentage: false, }; type SpecRequiredProps = Pick; @@ -55,6 +54,5 @@ export const BarSeries: React.FunctionComponent(defaultProps), ); diff --git a/src/chart_types/xy_chart/utils/__snapshots__/series.test.ts.snap b/src/chart_types/xy_chart/utils/__snapshots__/series.test.ts.snap index 543fa7eb75..1ed26fce79 100644 --- a/src/chart_types/xy_chart/utils/__snapshots__/series.test.ts.snap +++ b/src/chart_types/xy_chart/utils/__snapshots__/series.test.ts.snap @@ -22095,7 +22095,11 @@ Array [ Object { "data": Array [ Object { - "datum": undefined, + "datum": Object { + "g": "a", + "x": 1, + "y1": 1, + }, "filled": undefined, "initialY0": null, "initialY1": 1, @@ -22105,7 +22109,11 @@ Array [ "y1": 1, }, Object { - "datum": undefined, + "datum": Object { + "g": "a", + "x": 2, + "y1": 2, + }, "filled": undefined, "initialY0": null, "initialY1": 2, @@ -22115,17 +22123,23 @@ Array [ "y1": 2, }, Object { - "datum": null, - "filled": undefined, + "datum": undefined, + "filled": Object { + "x": 3, + }, "initialY0": null, "initialY1": null, "mark": null, "x": 3, - "y0": NaN, - "y1": NaN, + "y0": 0, + "y1": 0, }, Object { - "datum": undefined, + "datum": Object { + "g": "a", + "x": 4, + "y1": 4, + }, "filled": undefined, "initialY0": null, "initialY1": 4, @@ -22135,18 +22149,25 @@ Array [ "y1": 4, }, ], - "key": "a", + "key": "spec{spec1}yAccessor{y1}splitAccessors{g-a}", "seriesKeys": Array [ "a", + "y1", ], "specId": "spec1", - "splitAccessors": Map {}, + "splitAccessors": Map { + "g" => "a", + }, "yAccessor": "y1", }, Object { "data": Array [ Object { - "datum": undefined, + "datum": Object { + "g": "b", + "x": 1, + "y1": 21, + }, "filled": undefined, "initialY0": null, "initialY1": 21, @@ -22156,17 +22177,23 @@ Array [ "y1": 22, }, Object { - "datum": null, - "filled": undefined, + "datum": undefined, + "filled": Object { + "x": 2, + }, "initialY0": null, "initialY1": null, "mark": null, "x": 2, - "y0": NaN, - "y1": NaN, + "y0": 2, + "y1": 2, }, Object { - "datum": undefined, + "datum": Object { + "g": "b", + "x": 3, + "y1": 23, + }, "filled": undefined, "initialY0": null, "initialY1": 23, @@ -22176,22 +22203,27 @@ Array [ "y1": 23, }, Object { - "datum": null, - "filled": undefined, + "datum": undefined, + "filled": Object { + "x": 4, + }, "initialY0": null, "initialY1": null, "mark": null, "x": 4, - "y0": NaN, - "y1": NaN, + "y0": 4, + "y1": 4, }, ], - "key": "b", + "key": "spec{spec1}yAccessor{y1}splitAccessors{g-b}", "seriesKeys": Array [ "b", + "y1", ], "specId": "spec1", - "splitAccessors": Map {}, + "splitAccessors": Map { + "g" => "b", + }, "yAccessor": "y1", }, ] @@ -22641,7 +22673,11 @@ Array [ Object { "data": Array [ Object { - "datum": undefined, + "datum": Object { + "g": "a", + "x": 1, + "y1": 1, + }, "filled": undefined, "initialY0": null, "initialY1": 1, @@ -22651,7 +22687,11 @@ Array [ "y1": 1, }, Object { - "datum": undefined, + "datum": Object { + "g": "a", + "x": 2, + "y1": 2, + }, "filled": undefined, "initialY0": null, "initialY1": 2, @@ -22661,17 +22701,23 @@ Array [ "y1": 2, }, Object { - "datum": null, - "filled": undefined, + "datum": undefined, + "filled": Object { + "x": 3, + }, "initialY0": null, "initialY1": null, "mark": null, "x": 3, - "y0": NaN, - "y1": NaN, + "y0": 0, + "y1": 0, }, Object { - "datum": undefined, + "datum": Object { + "g": "a", + "x": 4, + "y1": 4, + }, "filled": undefined, "initialY0": null, "initialY1": 4, @@ -22681,18 +22727,25 @@ Array [ "y1": 4, }, ], - "key": "a", + "key": "spec{spec1}yAccessor{y1}splitAccessors{g-a}", "seriesKeys": Array [ "a", + "y1", ], "specId": "spec1", - "splitAccessors": Map {}, + "splitAccessors": Map { + "g" => "a", + }, "yAccessor": "y1", }, Object { "data": Array [ Object { - "datum": undefined, + "datum": Object { + "g": "b", + "x": 1, + "y1": 21, + }, "filled": undefined, "initialY0": null, "initialY1": 21, @@ -22702,17 +22755,23 @@ Array [ "y1": 22, }, Object { - "datum": null, - "filled": undefined, + "datum": undefined, + "filled": Object { + "x": 2, + }, "initialY0": null, "initialY1": null, "mark": null, "x": 2, - "y0": NaN, - "y1": NaN, + "y0": 2, + "y1": 2, }, Object { - "datum": undefined, + "datum": Object { + "g": "b", + "x": 3, + "y1": 23, + }, "filled": undefined, "initialY0": null, "initialY1": 23, @@ -22722,22 +22781,27 @@ Array [ "y1": 23, }, Object { - "datum": null, - "filled": undefined, + "datum": undefined, + "filled": Object { + "x": 4, + }, "initialY0": null, "initialY1": null, "mark": null, "x": 4, - "y0": NaN, - "y1": NaN, + "y0": 4, + "y1": 4, }, ], - "key": "b", + "key": "spec{spec1}yAccessor{y1}splitAccessors{g-b}", "seriesKeys": Array [ "b", + "y1", ], "specId": "spec1", - "splitAccessors": Map {}, + "splitAccessors": Map { + "g" => "b", + }, "yAccessor": "y1", }, ] diff --git a/src/chart_types/xy_chart/utils/series.test.ts b/src/chart_types/xy_chart/utils/series.test.ts index 721f9e3fff..7e71bab96f 100644 --- a/src/chart_types/xy_chart/utils/series.test.ts +++ b/src/chart_types/xy_chart/utils/series.test.ts @@ -107,34 +107,25 @@ describe('Series', () => { expect([...splittedSeries.dataSeries.values()]).toMatchSnapshot(); }); test('Can stack simple dataseries', () => { - const dataSeries: DataSeries[] = [ - { - specId: 'spec1', - yAccessor: 'y1', - splitAccessors: new Map(), - seriesKeys: ['a'], - key: 'a', - data: [ - { x: 1, y1: 1, mark: null, y0: null, initialY1: 1, initialY0: null, datum: undefined }, - { x: 2, y1: 2, mark: null, y0: null, initialY1: 2, initialY0: null, datum: undefined }, - { x: 4, y1: 4, mark: null, y0: null, initialY1: 4, initialY0: null, datum: undefined }, - ], - }, - { - specId: 'spec1', - yAccessor: 'y1', - splitAccessors: new Map(), - seriesKeys: ['b'], - key: 'b', - data: [ - { x: 1, y1: 21, mark: null, y0: null, initialY1: 21, initialY0: null, datum: undefined }, - { x: 3, y1: 23, mark: null, y0: null, initialY1: 23, initialY0: null, datum: undefined }, - ], - }, - ]; - const xValues = new Set([1, 2, 3, 4]); - const stackedValues = formatStackedDataSeriesValues(dataSeries, false, xValues); - expect(stackedValues).toMatchSnapshot(); + const store = MockStore.default(); + MockStore.addSpecs(MockSeriesSpec.area({ + id: 'spec1', + splitSeriesAccessors: ['g'], + yAccessors: ['y1'], + stackAccessors: ['x'], + xScaleType: ScaleType.Linear, + data: [ + { x: 1, y1: 1, g: 'a' }, + { x: 2, y1: 2, g: 'a' }, + { x: 4, y1: 4, g: 'a' }, + { x: 1, y1: 21, g: 'b' }, + { x: 3, y1: 23, g: 'b' }, + ], + }), store); + + const { formattedDataSeries: { stacked } } = computeSeriesDomainsSelector(store.getState()); + + expect(stacked[0].dataSeries).toMatchSnapshot(); }); test('Can stack multiple dataseries', () => { const dataSeries: DataSeries[] = [ @@ -192,38 +183,55 @@ describe('Series', () => { }, ]; const xValues = new Set([1, 2, 3, 4]); - const stackedValues = formatStackedDataSeriesValues(dataSeries, false, xValues); + const stackedValues = formatStackedDataSeriesValues(dataSeries, xValues); expect(stackedValues).toMatchSnapshot(); }); test('Can stack unsorted dataseries', () => { - const dataSeries: DataSeries[] = [ - { - specId: 'spec1', - yAccessor: 'y1', - splitAccessors: new Map(), - seriesKeys: ['a'], - key: 'a', - data: [ - { x: 1, y1: 1, mark: null, y0: null, initialY1: 1, initialY0: null, datum: undefined }, - { x: 4, y1: 4, mark: null, y0: null, initialY1: 4, initialY0: null, datum: undefined }, - { x: 2, y1: 2, mark: null, y0: null, initialY1: 2, initialY0: null, datum: undefined }, - ], - }, - { - specId: 'spec1', - yAccessor: 'y1', - splitAccessors: new Map(), - seriesKeys: ['b'], - key: 'b', - data: [ - { x: 3, y1: 23, mark: null, y0: null, initialY1: 23, initialY0: null, datum: undefined }, - { x: 1, y1: 21, mark: null, y0: null, initialY1: 21, initialY0: null, datum: undefined }, - ], - }, - ]; - const xValues = new Set([1, 2, 3, 4]); - const stackedValues = formatStackedDataSeriesValues(dataSeries, false, xValues); - expect(stackedValues).toMatchSnapshot(); + const store = MockStore.default(); + MockStore.addSpecs(MockSeriesSpec.area({ + id: 'spec1', + splitSeriesAccessors: ['g'], + yAccessors: ['y1'], + stackAccessors: ['x'], + xScaleType: ScaleType.Linear, + data: [ + { x: 1, y1: 1, g: 'a' }, + { x: 4, y1: 4, g: 'a' }, + { x: 2, y1: 2, g: 'a' }, + { x: 3, y1: 23, g: 'b' }, + { x: 1, y1: 21, g: 'b' }, + ], + }), store); + const { formattedDataSeries: { stacked } } = computeSeriesDomainsSelector(store.getState()); + // const dataSeries: DataSeries[] = [ + // { + // specId: 'spec1', + // yAccessor: 'y1', + // splitAccessors: new Map(), + // seriesKeys: ['a'], + // key: 'a', + // data: [ + // { x: 1, y1: 1, mark: null, y0: null, initialY1: 1, initialY0: null, datum: undefined }, + // { x: 4, y1: 4, mark: null, y0: null, initialY1: 4, initialY0: null, datum: undefined }, + // { x: 2, y1: 2, mark: null, y0: null, initialY1: 2, initialY0: null, datum: undefined }, + // ], + // }, + // { + // specId: 'spec1', + // yAccessor: 'y1', + // splitAccessors: new Map(), + // seriesKeys: ['b'], + // key: 'b', + // data: [ + // { x: 3, y1: 23, mark: null, y0: null, initialY1: 23, initialY0: null, datum: undefined }, + // { x: 1, y1: 21, mark: null, y0: null, initialY1: 21, initialY0: null, datum: undefined }, + // ], + // }, + // ]; + // const xValues = new Set([1, 2, 3, 4]); + // const stackedValues = formatStackedDataSeriesValues(dataSeries, xValues); + + expect(stacked[0].dataSeries).toMatchSnapshot(); }); test('Can stack high volume of dataseries', () => { const maxArrayItems = 1000; @@ -246,7 +254,7 @@ describe('Series', () => { }, ]; const xValues = new Set(new Array(maxArrayItems).fill(0).map((d, i) => i)); - const stackedValues = formatStackedDataSeriesValues(dataSeries, false, xValues); + const stackedValues = formatStackedDataSeriesValues(dataSeries, xValues); expect(stackedValues).toMatchSnapshot(); }); test('Can stack simple dataseries with scale to extent', () => { diff --git a/src/chart_types/xy_chart/utils/series.ts b/src/chart_types/xy_chart/utils/series.ts index 77aa3bb65a..666c76e431 100644 --- a/src/chart_types/xy_chart/utils/series.ts +++ b/src/chart_types/xy_chart/utils/series.ts @@ -287,14 +287,14 @@ export function getFormattedDataseries( }[] = []; specsByGroupIdsEntries.forEach(([groupId, groupSpecs]) => { - const { isPercentageStack } = groupSpecs; + const { stackMode } = groupSpecs; // format stacked data series const stackedDataSeries = getDataSeriesBySpecGroup(groupSpecs.stacked, availableDataSeries); const fittedDataSeries = applyFitFunctionToDataSeries(stackedDataSeries.dataSeries, seriesSpecs, xScaleType); const fittedAndStackedDataSeries = formatStackedDataSeriesValues( fittedDataSeries, - isPercentageStack, xValues, + stackMode, ); stackedFormattedDataSeries.push({ diff --git a/src/chart_types/xy_chart/utils/specs.ts b/src/chart_types/xy_chart/utils/specs.ts index 889309646e..16a3628105 100644 --- a/src/chart_types/xy_chart/utils/specs.ts +++ b/src/chart_types/xy_chart/utils/specs.ts @@ -49,15 +49,28 @@ export type BarStyleOverride = RecursivePartial | Color | null; /** @public */ export type PointStyleOverride = RecursivePartial | Color | null; +/** @public */ export const SeriesTypes = Object.freeze({ Area: 'area' as const, Bar: 'bar' as const, Line: 'line' as const, Bubble: 'bubble' as const, }); + /** @public */ export type SeriesTypes = $Values; + +/** @public */ +export const StackModes = Object.freeze({ + Percentage: 'percentage' as const, + Wiggle: 'wiggle' as const, + Silhouette: 'silhouette' as const, +}); + +/** @public */ +export type StackModes = $Values; + /** * Override for bar styles per datum * @@ -446,9 +459,10 @@ export type BarSeriesSpec = BasicSeriesSpec & enableHistogramMode?: boolean; barSeriesStyle?: RecursivePartial; /** - * Stack each series in percentage for each point. + * Stack each series using a specific mode: Percentage, Wiggle, Silhouette. + * The last two modes are generally used for stream graphs */ - stackAsPercentage?: boolean; + stackMode?: StackModes; /** * Functional accessor to return custom color or style for bar datum */ @@ -541,9 +555,10 @@ export type AreaSeriesSpec = BasicSeriesSpec & curve?: CurveType; areaSeriesStyle?: RecursivePartial; /** - * Stack each series in percentage for each point. + * Stack each series using a specific mode: Percentage, Wiggle, Silhouette. + * The last two modes are generally used for stream graphs */ - stackAsPercentage?: boolean; + stackMode?: StackModes; /** * An optional functional accessor to return custom color or style for point datum */ diff --git a/src/chart_types/xy_chart/utils/stacked_percent_series_utils.test.ts b/src/chart_types/xy_chart/utils/stacked_percent_series_utils.test.ts index d8bd5a9539..b88f6da40f 100644 --- a/src/chart_types/xy_chart/utils/stacked_percent_series_utils.test.ts +++ b/src/chart_types/xy_chart/utils/stacked_percent_series_utils.test.ts @@ -21,6 +21,7 @@ import { MockSeriesSpec } from '../../../mocks/specs'; import { MockStore } from '../../../mocks/store'; import { ScaleType } from '../../../scales/constants'; import { computeSeriesDomainsSelector } from '../state/selectors/compute_series_domains'; +import { StackModes } from './specs'; describe('Stacked Series Utils', () => { const STANDARD_DATA_SET = [{ x: 0, y1: 10, g: 'a' }, { x: 0, y1: 20, g: 'b' }, { x: 0, y1: 70, g: 'c' }]; @@ -43,14 +44,17 @@ describe('Stacked Series Utils', () => { const store = MockStore.default(); MockStore.addSpecs([ MockSeriesSpec.area({ + xAccessor: 'x', yAccessors: ['y1'], splitSeriesAccessors: ['g'], stackAccessors: ['x'], - stackAsPercentage: true, + stackMode: StackModes.Percentage, data: STANDARD_DATA_SET, }), ], store); - const { formattedDataSeries: { stacked } } = computeSeriesDomainsSelector(store.getState()); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + const { stacked } = formattedDataSeries; + // console.log(JSON.stringify(formattedDataSeries.stacked, null, 2)); const [data0] = stacked[0].dataSeries[0].data; expect(data0.initialY1).toBe(10); expect(data0.y0).toBe(0); @@ -73,7 +77,7 @@ describe('Stacked Series Utils', () => { yAccessors: ['y1'], splitSeriesAccessors: ['g'], stackAccessors: ['x'], - stackAsPercentage: true, + stackMode: StackModes.Percentage, data: WITH_NULL_DATASET, }), ], store); @@ -106,7 +110,7 @@ describe('Stacked Series Utils', () => { y0Accessors: ['y0'], splitSeriesAccessors: ['g'], stackAccessors: ['x'], - stackAsPercentage: true, + stackMode: StackModes.Percentage, data: STANDARD_DATA_SET_WY0, }), ], store); @@ -138,7 +142,7 @@ describe('Stacked Series Utils', () => { y0Accessors: ['y0'], splitSeriesAccessors: ['g'], stackAccessors: ['x'], - stackAsPercentage: true, + stackMode: StackModes.Percentage, data: WITH_NULL_DATASET_WY0, }), ], store); @@ -172,7 +176,7 @@ describe('Stacked Series Utils', () => { y0Accessors: ['y0'], splitSeriesAccessors: ['g'], stackAccessors: ['x'], - stackAsPercentage: true, + stackMode: StackModes.Percentage, data: DATA_SET_WITH_NULL_2, }), ], store); diff --git a/src/chart_types/xy_chart/utils/stacked_series_utils.test.ts b/src/chart_types/xy_chart/utils/stacked_series_utils.test.ts index 11290679ed..da046811e4 100644 --- a/src/chart_types/xy_chart/utils/stacked_series_utils.test.ts +++ b/src/chart_types/xy_chart/utils/stacked_series_utils.test.ts @@ -21,6 +21,7 @@ import { MockSeriesSpec } from '../../../mocks/specs'; import { MockStore } from '../../../mocks/store'; import { ScaleType } from '../../../scales/constants'; import { computeSeriesDomainsSelector } from '../state/selectors/compute_series_domains'; +import { StackModes } from './specs'; describe('Stacked Series Utils', () => { const EMPTY_DATA_SET = MockSeriesSpec.area({ @@ -118,7 +119,7 @@ describe('Stacked Series Utils', () => { MockStore.addSpecs( MockSeriesSpec.area({ ...STANDARD_DATA_SET, - stackAsPercentage: true, + stackMode: StackModes.Percentage, }), store); const { formattedDataSeries: { stacked: [{ dataSeries }] } } = computeSeriesDomainsSelector(store.getState()); @@ -148,7 +149,7 @@ describe('Stacked Series Utils', () => { const store = MockStore.default(); MockStore.addSpecs(MockSeriesSpec.area({ ...WITH_NULL_DATASET, - stackAsPercentage: true, + stackMode: StackModes.Percentage, }), store); const { formattedDataSeries: { stacked: [{ dataSeries }] } } = computeSeriesDomainsSelector(store.getState()); diff --git a/src/chart_types/xy_chart/utils/stacked_series_utils.ts b/src/chart_types/xy_chart/utils/stacked_series_utils.ts index 8e29333236..0205484f26 100644 --- a/src/chart_types/xy_chart/utils/stacked_series_utils.ts +++ b/src/chart_types/xy_chart/utils/stacked_series_utils.ts @@ -21,6 +21,8 @@ import { stack as D3Stack, stackOffsetExpand as D3StackOffsetExpand, stackOffsetNone as D3StackOffsetNone, + stackOffsetSilhouette as D3StackOffsetSilhouette, + stackOffsetWiggle as D3StackOffsetWiggle, stackOrderNone, SeriesPoint, } from 'd3-shape'; @@ -28,6 +30,7 @@ import { import { SeriesKey } from '../../../commons/series_id'; import { ScaleType } from '../../../scales/constants'; import { DataSeries, DataSeriesDatum } from './series'; +import { StackModes } from './specs'; /** @internal */ export interface StackedValues { @@ -66,8 +69,8 @@ type D3UnionStack = Record< /** @internal */ export function formatStackedDataSeriesValues( dataSeries: DataSeries[], - isPercentageMode: boolean, xValues: Set, + stackMode?: StackModes, ): DataSeries[] { const dataSeriesKeys = dataSeries.reduce>((acc, curr) => { acc[curr.key] = curr; @@ -97,10 +100,12 @@ export function formatStackedDataSeriesValues( xValueMap.set(key, dsMap); }); - const stackOffset = isPercentageMode ? D3StackOffsetExpand : D3StackOffsetNone; + const stackOffset = getOffsetBasedOnStackMode(stackMode); + + const keys = Object.keys(dataSeriesKeys).reduce((acc, key) => ([...acc, `${key}-y0`, `${key}-y1`]), []); const stack = D3Stack() - .keys(Object.keys(dataSeriesKeys).reduce((acc, key) => ([...acc, `${key}-y0`, `${key}-y1`]), [])) + .keys(keys) .order(stackOrderNone) .offset(stackOffset)(reorderedArray); @@ -120,14 +125,14 @@ export function formatStackedDataSeriesValues( return acc; }, {}); + return Object.keys(unionedYStacks).map((stackedDataSeriesKey) => { const dataSeriesProps = dataSeriesKeys[stackedDataSeriesKey]; const dsMap = xValueMap.get(stackedDataSeriesKey); const { y0: y0StackArray, y1: y1StackArray } = unionedYStacks[stackedDataSeriesKey]; - const data = y1StackArray.map((y1Stack, index) => { const { x } = y1Stack.data; - if (!x) { + if (x === undefined || x === null) { return null; } const originalData = dsMap?.get(x); @@ -148,10 +153,23 @@ export function formatStackedDataSeriesValues( filled, }; }).filter((d) => d !== null) as DataSeriesDatum[]; - return { ...dataSeriesProps, data, }; }); } + + +function getOffsetBasedOnStackMode(stackMode?: StackModes) { + switch (stackMode) { + case StackModes.Percentage: + return D3StackOffsetExpand; + case StackModes.Silhouette: + return D3StackOffsetSilhouette; + case StackModes.Wiggle: + return D3StackOffsetWiggle; + default: + return D3StackOffsetNone; + } +} diff --git a/src/mocks/specs/specs.ts b/src/mocks/specs/specs.ts index 69ce8abc66..f30ebe6641 100644 --- a/src/mocks/specs/specs.ts +++ b/src/mocks/specs/specs.ts @@ -59,7 +59,6 @@ export class MockSeriesSpec { yAccessors: ['y'], hideInLegend: false, enableHistogramMode: false, - stackAsPercentage: false, data: [] as any[], }; diff --git a/stories/area/8_stacked_percentage.tsx b/stories/area/8_stacked_percentage.tsx index 6c072875ac..9a91634502 100644 --- a/stories/area/8_stacked_percentage.tsx +++ b/stories/area/8_stacked_percentage.tsx @@ -20,7 +20,7 @@ import { boolean } from '@storybook/addon-knobs'; import React from 'react'; -import { AreaSeries, Axis, Chart, Position, ScaleType, Settings } from '../../src'; +import { AreaSeries, Axis, Chart, Position, ScaleType, Settings, StackModes } from '../../src'; export const Example = () => { const stackedAsPercentage = boolean('stacked as percentage', true); @@ -42,7 +42,7 @@ export const Example = () => { xAccessor="x" yAccessors={['y']} stackAccessors={['x']} - stackAsPercentage={stackedAsPercentage} + stackMode={stackedAsPercentage ? StackModes.Percentage : undefined} splitSeriesAccessors={['g']} data={[ { x: 0, y: 2, g: 'a' }, diff --git a/stories/area/8_stacked_percentage_zeros.tsx b/stories/area/8_stacked_percentage_zeros.tsx index 68bfe60239..949ef0fd7f 100644 --- a/stories/area/8_stacked_percentage_zeros.tsx +++ b/stories/area/8_stacked_percentage_zeros.tsx @@ -19,7 +19,7 @@ import React from 'react'; -import { AreaSeries, Axis, Chart, Position, ScaleType, Settings, niceTimeFormatter } from '../../src'; +import { AreaSeries, Axis, Chart, Position, ScaleType, Settings, niceTimeFormatter, StackModes } from '../../src'; export const Example = () => ( @@ -46,7 +46,7 @@ export const Example = () => ( yAccessors={[1]} data={DATA[0].data} stackAccessors={[0]} - stackAsPercentage + stackMode={StackModes.Percentage} /> ( yAccessors={[1]} data={DATA[1].data} stackAccessors={[0]} - stackAsPercentage + stackMode={StackModes.Percentage} /> ( xAccessor={0} yAccessors={[1]} data={DATA[2].data} - stackAsPercentage + stackMode={StackModes.Percentage} stackAccessors={[0]} /> diff --git a/stories/bar/12_stacked_as_percentage.tsx b/stories/bar/12_stacked_as_percentage.tsx index ff95d6031f..e9519049af 100644 --- a/stories/bar/12_stacked_as_percentage.tsx +++ b/stories/bar/12_stacked_as_percentage.tsx @@ -20,7 +20,7 @@ import { boolean } from '@storybook/addon-knobs'; import React from 'react'; -import { Axis, BarSeries, Chart, Position, ScaleType, Settings } from '../../src'; +import { Axis, BarSeries, Chart, Position, ScaleType, Settings, StackModes } from '../../src'; import { SB_SOURCE_PANEL } from '../utils/storybook'; export const Example = () => { @@ -44,7 +44,7 @@ export const Example = () => { xAccessor="x" yAccessors={['y']} stackAccessors={clusterBars ? [] : ['x']} - stackAsPercentage={clusterBars ? false : stackedAsPercentage} + stackMode={clusterBars ? undefined : StackModes.Percentage} splitSeriesAccessors={['g']} data={[ { x: 0, y: 2, g: 'a' },