From 55fd383de9fca0a1ab7f8b7f1a68fac5c038d8be Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Mon, 7 Oct 2019 22:31:22 +0200 Subject: [PATCH 1/2] feat: fill multi-series with missing x values data points --- .playground/index.html | 2 +- .playground/playgroud.tsx | 133 +++++++++++++----- src/chart_types/xy_chart/domains/x_domain.ts | 11 +- src/chart_types/xy_chart/store/utils.ts | 2 +- .../utils/__snapshots__/series.test.ts.snap | 110 ++++++++++++++- src/chart_types/xy_chart/utils/series.test.ts | 32 +++-- src/chart_types/xy_chart/utils/series.ts | 20 ++- .../stacked_percent_series_utils.test.ts | 46 ++++-- .../utils/stacked_series_utils.test.ts | 52 +++++-- .../xy_chart/utils/stacked_series_utils.ts | 112 +++++++++++---- src/utils/commons.ts | 4 + 11 files changed, 408 insertions(+), 116 deletions(-) diff --git a/.playground/index.html b/.playground/index.html index 6a204f4f99..7bdda72199 100644 --- a/.playground/index.html +++ b/.playground/index.html @@ -25,7 +25,7 @@ background: white; position: relative; width: 800px; - height: 150px; + height: 450px; margin: 10px; } diff --git a/.playground/playgroud.tsx b/.playground/playgroud.tsx index 6fcfbb7567..c93015f245 100644 --- a/.playground/playgroud.tsx +++ b/.playground/playgroud.tsx @@ -1,16 +1,5 @@ import React, { Fragment } from 'react'; -import { - Axis, - Chart, - getAxisId, - getSpecId, - Position, - ScaleType, - Settings, - BarSeries, - LineSeries, - AreaSeries, -} from '../src'; +import { Axis, Chart, getAxisId, getSpecId, Position, ScaleType, Settings, AreaSeries } from '../src'; export class Playground extends React.Component { render() { @@ -26,9 +15,6 @@ export class Playground extends React.Component { }, }, }} - xDomain={{ - max: 3.8, - }} /> - - - + {/* - - */} + - diff --git a/src/chart_types/xy_chart/domains/x_domain.ts b/src/chart_types/xy_chart/domains/x_domain.ts index e81e7e363b..f70d8b2358 100644 --- a/src/chart_types/xy_chart/domains/x_domain.ts +++ b/src/chart_types/xy_chart/domains/x_domain.ts @@ -1,5 +1,5 @@ import { isCompleteBound, isLowerBound, isUpperBound } from '../utils/axis_utils'; -import { compareByValueAsc, identity } from '../../../utils/commons'; +import { compareByValueAsc, identity, isNumberArray } from '../../../utils/commons'; import { computeContinuousDataDomain, computeOrdinalDataDomain, Domain } from '../../../utils/domain'; import { ScaleType } from '../../../utils/scales/scales'; import { BasicSeriesSpec, DomainRange } from '../utils/specs'; @@ -22,7 +22,7 @@ export type XDomain = BaseDomain & { */ export function mergeXDomain( specs: Pick[], - xValues: Set, + xValues: Set, customXDomain?: DomainRange | Domain, ): XDomain { const mainXScaleType = convertXScaleTypes(specs); @@ -46,7 +46,11 @@ export function mergeXDomain( } else { seriesXComputedDomains = computeContinuousDataDomain(values, identity, true); let customMinInterval: undefined | number; - + if (!isNumberArray(values)) { + throw new Error( + `Each X value in a ${mainXScaleType.scaleType} x scale needs be be a number. String or objects are not allowed`, + ); + } if (customXDomain) { if (Array.isArray(customXDomain)) { throw new Error('xDomain for continuous scale should be a DomainRange object, not an array'); @@ -78,7 +82,6 @@ export function mergeXDomain( } } } - const computedMinInterval = findMinInterval(values); if (customMinInterval != null) { // Allow greater custom min iff xValues has 1 member. diff --git a/src/chart_types/xy_chart/store/utils.ts b/src/chart_types/xy_chart/store/utils.ts index 71bec6f9a6..1ed63ae942 100644 --- a/src/chart_types/xy_chart/store/utils.ts +++ b/src/chart_types/xy_chart/store/utils.ts @@ -165,7 +165,7 @@ export function computeSeriesDomains( const xDomain = mergeXDomain(specsArray, xValues, customXDomain); const yDomain = mergeYDomain(splittedSeries, specsArray, customYDomainsByGroupId); - const formattedDataSeries = getFormattedDataseries(specsArray, splittedSeries); + const formattedDataSeries = getFormattedDataseries(specsArray, splittedSeries, xValues, xDomain.scaleType); // we need to get the last values from the formatted dataseries // because we change the format if we are on percentage mode 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 5f6629088a..89969a1e2f 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 @@ -17650,6 +17650,14 @@ Array [ "y0": null, "y1": 2, }, + Object { + "datum": undefined, + "initialY0": null, + "initialY1": 0, + "x": 3, + "y0": 0, + "y1": 0, + }, Object { "datum": undefined, "initialY0": null, @@ -17675,6 +17683,14 @@ Array [ "y0": 1, "y1": 22, }, + Object { + "datum": undefined, + "initialY0": null, + "initialY1": 0, + "x": 2, + "y0": 2, + "y1": 2, + }, Object { "datum": undefined, "initialY0": null, @@ -17683,6 +17699,14 @@ Array [ "y0": 0, "y1": 23, }, + Object { + "datum": undefined, + "initialY0": null, + "initialY1": 0, + "x": 4, + "y0": 4, + "y1": 4, + }, ], "key": Array [ "b", @@ -17713,6 +17737,14 @@ Array [ "y0": 2, "y1": 2, }, + Object { + "datum": undefined, + "initialY0": null, + "initialY1": 0, + "x": 3, + "y0": 0, + "y1": 0, + }, Object { "datum": undefined, "initialY0": null, @@ -17738,6 +17770,14 @@ Array [ "y0": 1, "y1": 22, }, + Object { + "datum": undefined, + "initialY0": null, + "initialY1": 0, + "x": 2, + "y0": 2, + "y1": 2, + }, Object { "datum": undefined, "initialY0": null, @@ -17746,6 +17786,14 @@ Array [ "y0": 0, "y1": 23, }, + Object { + "datum": undefined, + "initialY0": null, + "initialY1": 0, + "x": 4, + "y0": 4, + "y1": 4, + }, ], "key": Array [ "b", @@ -17776,6 +17824,14 @@ Array [ "y0": 2, "y1": 3, }, + Object { + "datum": undefined, + "initialY0": null, + "initialY1": 0, + "x": 3, + "y0": 0, + "y1": 0, + }, Object { "datum": undefined, "initialY0": 3, @@ -17855,6 +17911,14 @@ Array [ "y0": 2, "y1": 3, }, + Object { + "datum": undefined, + "initialY0": null, + "initialY1": 0, + "x": 3, + "y0": 0, + "y1": 0, + }, Object { "datum": undefined, "initialY0": 3, @@ -17929,18 +17993,40 @@ Array [ Object { "datum": undefined, "initialY0": null, +<<<<<<< HEAD "initialY1": 4, "x": 4, "y0": null, "y1": 4, +======= + "initialY1": 2, + "x": 2, + "y0": 0, + "y1": 2, +>>>>>>> feat: fill multi-series with missing x values data points }, Object { "datum": undefined, "initialY0": null, +<<<<<<< HEAD "initialY1": 2, "x": 2, "y0": null, "y1": 2, +======= + "initialY1": 0, + "x": 3, + "y0": 0, + "y1": 0, + }, + Object { + "datum": undefined, + "initialY0": null, + "initialY1": 4, + "x": 4, + "y0": 0, + "y1": 4, +>>>>>>> feat: fill multi-series with missing x values data points }, ], "key": Array [ @@ -17951,6 +18037,22 @@ Array [ }, Object { "data": Array [ + Object { + "datum": undefined, + "initialY0": null, + "initialY1": 21, + "x": 1, + "y0": 1, + "y1": 22, + }, + Object { + "datum": undefined, + "initialY0": null, + "initialY1": 0, + "x": 2, + "y0": 2, + "y1": 2, + }, Object { "datum": undefined, "initialY0": null, @@ -17962,10 +18064,10 @@ Array [ Object { "datum": undefined, "initialY0": null, - "initialY1": 21, - "x": 1, - "y0": 1, - "y1": 22, + "initialY1": 0, + "x": 4, + "y0": 4, + "y1": 4, }, ], "key": Array [ diff --git a/src/chart_types/xy_chart/utils/series.test.ts b/src/chart_types/xy_chart/utils/series.test.ts index 76ffd19189..9e6787ef1f 100644 --- a/src/chart_types/xy_chart/utils/series.test.ts +++ b/src/chart_types/xy_chart/utils/series.test.ts @@ -100,7 +100,8 @@ describe('Series', () => { data: [{ x: 1, y1: 21 }, { x: 3, y1: 23 }], }, ]; - const stackedValues = formatStackedDataSeriesValues(dataSeries, false); + const xValues = new Set([1, 2, 3, 4]); + const stackedValues = formatStackedDataSeriesValues(dataSeries, false, false, xValues, ScaleType.Linear); expect(stackedValues).toMatchSnapshot(); }); test('Can stack multiple dataseries', () => { @@ -130,7 +131,8 @@ describe('Series', () => { data: [{ x: 1, y1: 1 }, { x: 2, y1: 2 }, { x: 3, y1: 3 }, { x: 4, y1: 4 }], }, ]; - const stackedValues = formatStackedDataSeriesValues(dataSeries, false); + const xValues = new Set([1, 2, 3, 4]); + const stackedValues = formatStackedDataSeriesValues(dataSeries, false, false, xValues, ScaleType.Linear); expect(stackedValues).toMatchSnapshot(); }); test('Can stack unsorted dataseries', () => { @@ -148,7 +150,8 @@ describe('Series', () => { data: [{ x: 3, y1: 23 }, { x: 1, y1: 21 }], }, ]; - const stackedValues = formatStackedDataSeriesValues(dataSeries, false); + const xValues = new Set([1, 2, 3, 4]); + const stackedValues = formatStackedDataSeriesValues(dataSeries, false, false, xValues, ScaleType.Linear); expect(stackedValues).toMatchSnapshot(); }); test('Can stack high volume of dataseries', () => { @@ -167,7 +170,8 @@ describe('Series', () => { data: new Array(maxArrayItems).fill(0).map((d, i) => ({ x: i, y1: i })), }, ]; - const stackedValues = formatStackedDataSeriesValues(dataSeries, false); + const xValues = new Set(new Array(maxArrayItems).fill(0).map((d, i) => i)); + const stackedValues = formatStackedDataSeriesValues(dataSeries, false, false, xValues, ScaleType.Linear); expect(stackedValues).toMatchSnapshot(); }); test('Can stack simple dataseries with scale to extent', () => { @@ -185,7 +189,8 @@ describe('Series', () => { data: [{ x: 1, y1: 21 }, { x: 3, y1: 23 }], }, ]; - const stackedValues = formatStackedDataSeriesValues(dataSeries, true); + const xValues = new Set([1, 2, 3, 4]); + const stackedValues = formatStackedDataSeriesValues(dataSeries, true, false, xValues, ScaleType.Linear); // the datum on the snapshots is undefined because we are not adding it to // the test raw dataseries expect(stackedValues).toMatchSnapshot(); @@ -217,7 +222,8 @@ describe('Series', () => { data: [{ x: 1, y1: 1 }, { x: 2, y1: 2 }, { x: 3, y1: 3 }, { x: 4, y1: 4 }], }, ]; - const stackedValues = formatStackedDataSeriesValues(dataSeries, true); + const xValues = new Set([1, 2, 3, 4]); + const stackedValues = formatStackedDataSeriesValues(dataSeries, true, false, xValues, ScaleType.Linear); // the datum on the snapshots is undefined because we are not adding it to // the test raw dataseries expect(stackedValues).toMatchSnapshot(); @@ -237,7 +243,8 @@ describe('Series', () => { data: [{ x: 1, y1: 2, y0: 1 }, { x: 2, y1: 3, y0: 1 }, { x: 3, y1: 23, y0: 4 }, { x: 4, y1: 4, y0: 1 }], }, ]; - const stackedValues = formatStackedDataSeriesValues(dataSeries, true); + const xValues = new Set([1, 2, 3, 4]); + const stackedValues = formatStackedDataSeriesValues(dataSeries, true, false, xValues, ScaleType.Linear); // the datum on the snapshots is undefined because we are not adding it to // the test raw dataseries @@ -268,7 +275,8 @@ describe('Series', () => { data: [{ x: 1, y1: 2, y0: 1 }, { x: 2, y1: 3, y0: 1 }, { x: 3, y1: 23, y0: 4 }, { x: 4, y1: 4, y0: 1 }], }, ]; - const stackedValues = formatStackedDataSeriesValues(dataSeries, true); + const xValues = new Set([1, 2, 3, 4]); + const stackedValues = formatStackedDataSeriesValues(dataSeries, true, false, xValues, ScaleType.Linear); // the datum on the snapshots is undefined because we are not adding it to // the test raw dataseries expect(stackedValues[0].data[0].y0).toBe(1); @@ -343,10 +351,16 @@ describe('Series', () => { data: TestDataset.BARCHART_2Y0G, hideInLegend: false, }; + const xValues = new Set([0, 1, 2, 3]); seriesSpecs.set(spec1.id, spec1); seriesSpecs.set(spec2.id, spec2); const splittedDataSeries = getSplittedSeries(seriesSpecs); - const stackedDataSeries = getFormattedDataseries([spec1, spec2], splittedDataSeries.splittedSeries); + const stackedDataSeries = getFormattedDataseries( + [spec1, spec2], + splittedDataSeries.splittedSeries, + xValues, + ScaleType.Linear, + ); expect(stackedDataSeries.stacked).toMatchSnapshot(); }); test('should get series color map', () => { diff --git a/src/chart_types/xy_chart/utils/series.ts b/src/chart_types/xy_chart/utils/series.ts index 862753e109..1dabeefc42 100644 --- a/src/chart_types/xy_chart/utils/series.ts +++ b/src/chart_types/xy_chart/utils/series.ts @@ -7,6 +7,7 @@ import { isEqualSeriesKey } from './series_utils'; import { BasicSeriesSpec, Datum, SeriesAccessors } from './specs'; import { formatStackedDataSeriesValues } from './stacked_series_utils'; import { LastValues } from '../store/utils'; +import { ScaleType } from '../../../utils/scales/scales'; export interface RawDataSeriesDatum { /** the x value */ @@ -91,13 +92,13 @@ export function splitSeries( ): { rawDataSeries: RawDataSeries[]; colorsValues: Map; - xValues: Set; + xValues: Set; } { const { xAccessor, yAccessors, y0Accessors, splitSeriesAccessors = [] } = accessors; const isMultipleY = yAccessors && yAccessors.length > 1; const series = new Map(); const colorsValues = new Map(); - const xValues = new Set(); + const xValues = new Set(); data.forEach((datum) => { const seriesKey = getAccessorsValues(datum, splitSeriesAccessors); @@ -196,6 +197,8 @@ function cleanDatum(datum: Datum, xAccessor: Accessor, yAccessor: Accessor, y0Ac export function getFormattedDataseries( specs: YBasicSeriesSpec[], dataSeries: Map, + xValues: Set, + xScaleType: ScaleType, ): { stacked: FormattedDataSeries[]; nonStacked: FormattedDataSeries[]; @@ -222,6 +225,8 @@ export function getFormattedDataseries( stackedDataSeries.rawDataSeries, false, isPercentageStack, + xValues, + xScaleType, ); stackedFormattedDataSeries.push({ groupId, @@ -294,12 +299,16 @@ export function getSplittedSeries( ): { splittedSeries: Map; seriesColors: Map; - xValues: Set; + xValues: Set; } { const splittedSeries = new Map(); const seriesColors = new Map(); - const xValues: Set = new Set(); + let xValues: Set = new Set(); + let isOrdinalScale = false; for (const [specId, spec] of seriesSpecs) { + if (spec.xScaleType === ScaleType.Ordinal) { + isOrdinalScale = true; + } const dataSeries = splitSeries(spec.data, spec, specId); let currentRawDataSeries = dataSeries.rawDataSeries; if (deselectedDataSeries) { @@ -332,6 +341,9 @@ export function getSplittedSeries( xValues.add(xValue); } } + if (!isOrdinalScale) { + xValues = new Set([...xValues].sort()); + } return { splittedSeries, seriesColors, 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 a85cabed82..b0135a5b07 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 @@ -1,6 +1,7 @@ import { getSpecId } from '../../../utils/ids'; import { RawDataSeries } from './series'; import { formatStackedDataSeriesValues } from './stacked_series_utils'; +import { ScaleType } from '../../../utils/scales/scales'; describe('Stacked Series Utils', () => { const STANDARD_DATA_SET: RawDataSeries[] = [ @@ -162,10 +163,12 @@ describe('Stacked Series Utils', () => { data: [{ x: 1, y1: 90 }, { x: 3, y1: 30 }], }, ]; + const xValues = new Set([0]); + const with2NullsXValues = new Set([1, 2, 3, 4]); describe('Format stacked dataset', () => { test('format data without nulls', () => { - const formattedData = formatStackedDataSeriesValues(STANDARD_DATA_SET, false, true); + const formattedData = formatStackedDataSeriesValues(STANDARD_DATA_SET, false, true, xValues, ScaleType.Linear); const data0 = formattedData[0].data[0]; expect(data0.initialY1).toBe(0.1); expect(data0.y0).toBeNull(); @@ -182,7 +185,7 @@ describe('Stacked Series Utils', () => { expect(data2.y1).toBe(1); }); test('format data with nulls', () => { - const formattedData = formatStackedDataSeriesValues(WITH_NULL_DATASET, false, true); + const formattedData = formatStackedDataSeriesValues(WITH_NULL_DATASET, false, true, xValues, ScaleType.Linear); const data0 = formattedData[0].data[0]; expect(data0.initialY1).toBe(0.25); expect(data0.y0).toBeNull(); @@ -203,7 +206,13 @@ describe('Stacked Series Utils', () => { expect(data2.y1).toBe(1); }); test('format data without nulls with y0 values', () => { - const formattedData = formatStackedDataSeriesValues(STANDARD_DATA_SET_WY0, false, true); + const formattedData = formatStackedDataSeriesValues( + STANDARD_DATA_SET_WY0, + false, + true, + xValues, + ScaleType.Linear, + ); const data0 = formattedData[0].data[0]; expect(data0.initialY0).toBe(0.02); expect(data0.initialY1).toBe(0.1); @@ -223,7 +232,13 @@ describe('Stacked Series Utils', () => { expect(data2.y1).toBe(1); }); test('format data with nulls', () => { - const formattedData = formatStackedDataSeriesValues(WITH_NULL_DATASET_WY0, false, true); + const formattedData = formatStackedDataSeriesValues( + WITH_NULL_DATASET_WY0, + false, + true, + xValues, + ScaleType.Linear, + ); const data0 = formattedData[0].data[0]; expect(data0.initialY0).toBe(0.02); expect(data0.initialY1).toBe(0.1); @@ -243,11 +258,16 @@ describe('Stacked Series Utils', () => { expect(data2.y1).toBe(1); }); test('format data without nulls on second series', () => { - const formattedData = formatStackedDataSeriesValues(DATA_SET_WITH_NULL_2, false, true); + const formattedData = formatStackedDataSeriesValues( + DATA_SET_WITH_NULL_2, + false, + true, + with2NullsXValues, + ScaleType.Linear, + ); expect(formattedData.length).toBe(2); - expect(formattedData[0].data.length).toBe(3); - expect(formattedData[1].data.length).toBe(2); - + expect(formattedData[0].data.length).toBe(4); + expect(formattedData[1].data.length).toBe(4); expect(formattedData[0].data[0]).toEqual({ datum: undefined, initialY0: null, @@ -264,7 +284,7 @@ describe('Stacked Series Utils', () => { y0: null, y1: 1, }); - expect(formattedData[0].data[2]).toEqual({ + expect(formattedData[0].data[3]).toEqual({ datum: undefined, initialY0: null, initialY1: 1, @@ -281,6 +301,14 @@ describe('Stacked Series Utils', () => { y1: 1, }); expect(formattedData[1].data[1]).toEqual({ + datum: undefined, + initialY0: null, + initialY1: 0, + x: 2, + y0: 1, + y1: 1, + }); + expect(formattedData[1].data[2]).toEqual({ datum: undefined, initialY0: null, initialY1: 1, 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 236730d128..0747bc2921 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 @@ -1,6 +1,7 @@ import { getSpecId } from '../../../utils/ids'; import { RawDataSeries } from './series'; import { computeYStackedMapValues, formatStackedDataSeriesValues, getYValueStackMap } from './stacked_series_utils'; +import { ScaleType } from '../../../utils/scales/scales'; describe('Stacked Series Utils', () => { const EMPTY_DATA_SET: RawDataSeries[] = [ @@ -170,13 +171,16 @@ describe('Stacked Series Utils', () => { data: [{ x: 1, y1: 21 }, { x: 3, y1: 23 }], }, ]; + const xValues = new Set([0]); + const emptyXValues: Set = new Set(); + const with2NullsXValues = new Set([1, 2, 3, 4]); describe('create stacked maps', () => { test('with empty values', () => { - const stackedMap = getYValueStackMap(EMPTY_DATA_SET); + const stackedMap = getYValueStackMap(EMPTY_DATA_SET, emptyXValues); expect(stackedMap.size).toBe(0); }); test('with basic values', () => { - const stackedMap = getYValueStackMap(STANDARD_DATA_SET); + const stackedMap = getYValueStackMap(STANDARD_DATA_SET, xValues); expect(stackedMap.size).toBe(1); const x0StackArray = stackedMap.get(0)!; expect(x0StackArray).toBeDefined(); @@ -185,7 +189,7 @@ describe('Stacked Series Utils', () => { // expect(x0StackArray).toEqual([10, 20, 30]); }); test('with values with nulls', () => { - const stackedMap = getYValueStackMap(WITH_NULL_DATASET); + const stackedMap = getYValueStackMap(WITH_NULL_DATASET, xValues); expect(stackedMap.size).toBe(1); const x0StackArray = stackedMap.get(0)!; expect(x0StackArray).toBeDefined(); @@ -195,14 +199,14 @@ describe('Stacked Series Utils', () => { }); describe('compute stacked arrays', () => { test('with empty values', () => { - const stackedMap = getYValueStackMap(EMPTY_DATA_SET); + const stackedMap = getYValueStackMap(EMPTY_DATA_SET, emptyXValues); let computedStackedMap = computeYStackedMapValues(stackedMap, false); expect(computedStackedMap.size).toBe(0); computedStackedMap = computeYStackedMapValues(stackedMap, true); expect(computedStackedMap.size).toBe(0); }); test('with basic values', () => { - const stackedMap = getYValueStackMap(STANDARD_DATA_SET); + const stackedMap = getYValueStackMap(STANDARD_DATA_SET, xValues); const computedStackedMap = computeYStackedMapValues(stackedMap, false); expect(computedStackedMap.size).toBe(1); const x0Array = computedStackedMap.get(0)!; @@ -212,7 +216,7 @@ describe('Stacked Series Utils', () => { expect(x0Array.total).toBe(60); }); test('with null values', () => { - const stackedMap = getYValueStackMap(WITH_NULL_DATASET); + const stackedMap = getYValueStackMap(WITH_NULL_DATASET, xValues); const computedStackedMap = computeYStackedMapValues(stackedMap, false); expect(computedStackedMap.size).toBe(1); const x0Array = computedStackedMap.get(0)!; @@ -224,7 +228,7 @@ describe('Stacked Series Utils', () => { }); describe('Format stacked dataset', () => { test('format data without nulls', () => { - const formattedData = formatStackedDataSeriesValues(STANDARD_DATA_SET, false); + const formattedData = formatStackedDataSeriesValues(STANDARD_DATA_SET, false, false, xValues, ScaleType.Linear); expect(formattedData[0].data[0]).toEqual({ datum: undefined, initialY0: null, @@ -251,7 +255,7 @@ describe('Stacked Series Utils', () => { }); }); test('format data with nulls', () => { - const formattedData = formatStackedDataSeriesValues(WITH_NULL_DATASET, false); + const formattedData = formatStackedDataSeriesValues(WITH_NULL_DATASET, false, false, xValues, ScaleType.Linear); expect(formattedData[1].data[0]).toEqual({ datum: undefined, initialY0: null, @@ -262,7 +266,13 @@ describe('Stacked Series Utils', () => { }); }); test('format data without nulls with y0 values', () => { - const formattedData = formatStackedDataSeriesValues(STANDARD_DATA_SET_WY0, false); + const formattedData = formatStackedDataSeriesValues( + STANDARD_DATA_SET_WY0, + false, + false, + xValues, + ScaleType.Linear, + ); expect(formattedData[0].data[0]).toEqual({ datum: undefined, initialY0: 2, @@ -289,7 +299,13 @@ describe('Stacked Series Utils', () => { }); }); test('format data with nulls', () => { - const formattedData = formatStackedDataSeriesValues(WITH_NULL_DATASET_WY0, false); + const formattedData = formatStackedDataSeriesValues( + WITH_NULL_DATASET_WY0, + false, + false, + xValues, + ScaleType.Linear, + ); expect(formattedData[0].data[0]).toEqual({ datum: undefined, initialY0: 2, @@ -316,10 +332,16 @@ describe('Stacked Series Utils', () => { }); }); test('format data without nulls on second series', () => { - const formattedData = formatStackedDataSeriesValues(DATA_SET_WITH_NULL_2, false); + const formattedData = formatStackedDataSeriesValues( + DATA_SET_WITH_NULL_2, + false, + false, + with2NullsXValues, + ScaleType.Linear, + ); expect(formattedData.length).toBe(2); - expect(formattedData[0].data.length).toBe(3); - expect(formattedData[1].data.length).toBe(2); + expect(formattedData[0].data.length).toBe(4); + expect(formattedData[1].data.length).toBe(4); expect(formattedData[0].data[0]).toEqual({ datum: undefined, @@ -337,7 +359,7 @@ describe('Stacked Series Utils', () => { y0: null, y1: 2, }); - expect(formattedData[0].data[2]).toEqual({ + expect(formattedData[0].data[3]).toEqual({ datum: undefined, initialY0: null, initialY1: 4, @@ -353,7 +375,7 @@ describe('Stacked Series Utils', () => { y0: 1, y1: 22, }); - expect(formattedData[1].data[1]).toEqual({ + expect(formattedData[1].data[2]).toEqual({ datum: undefined, initialY0: null, initialY1: 23, 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 003b6a4d19..c3cb941cd6 100644 --- a/src/chart_types/xy_chart/utils/stacked_series_utils.ts +++ b/src/chart_types/xy_chart/utils/stacked_series_utils.ts @@ -1,4 +1,5 @@ -import { DataSeries, DataSeriesDatum, RawDataSeries } from './series'; +import { DataSeries, DataSeriesDatum, RawDataSeries, RawDataSeriesDatum } from './series'; +import { ScaleType } from '../../../utils/scales/scales'; interface StackedValues { values: number[]; @@ -11,14 +12,23 @@ interface StackedValues { * ordering the stack based on the dataseries index. * @param dataseries */ -export function getYValueStackMap(dataseries: RawDataSeries[]): Map { +export function getYValueStackMap(dataseries: RawDataSeries[], xValues: Set): Map { const stackMap = new Map(); + const missingXValues = new Set([...xValues]); dataseries.forEach((ds, index) => { ds.data.forEach((datum) => { const stack = stackMap.get(datum.x) || new Array(dataseries.length).fill(0); stack[index] = datum.y1; stackMap.set(datum.x, stack); + if (xValues.has(datum.x)) { + missingXValues.delete(datum.x); + } }); + for (let x of missingXValues.values()) { + const stack = stackMap.get(x) || new Array(dataseries.length).fill(0); + stack[index] = 0; + stackMap.set(x, stack); + } }); return stackMap; } @@ -75,21 +85,75 @@ export function computeYStackedMapValues( export function formatStackedDataSeriesValues( dataseries: RawDataSeries[], scaleToExtent: boolean, - isPercentageMode: boolean = false, + isPercentageMode: boolean, + xValues: Set, + xScaleType: ScaleType, ): DataSeries[] { - const yValueStackMap = getYValueStackMap(dataseries); - + const yValueStackMap = getYValueStackMap(dataseries, xValues); const stackedValues = computeYStackedMapValues(yValueStackMap, scaleToExtent); const stackedDataSeries: DataSeries[] = dataseries.map((ds, seriesIndex) => { const newData: DataSeriesDatum[] = []; + const missingXValues = new Set([...xValues]); ds.data.forEach((data) => { - const { x, datum } = data; - const stack = stackedValues.get(x); - if (!stack) { + const formattedSeriesDatum = getStackedFormattedSeriesDatum( + data, + stackedValues, + seriesIndex, + scaleToExtent, + isPercentageMode, + ); + if (formattedSeriesDatum === undefined) { return; } - let y1: number | null = null; + missingXValues.delete(data.x); + newData.push(formattedSeriesDatum); + }); + for (let x of missingXValues.values()) { + const filledSeriesDatum = getStackedFormattedSeriesDatum( + { + x, + y1: 0, + }, + stackedValues, + seriesIndex, + scaleToExtent, + isPercentageMode, + ); + if (filledSeriesDatum) { + newData.push(filledSeriesDatum); + } + } + newData.sort((a, b) => { + if (xScaleType === ScaleType.Ordinal || typeof a.x === 'string' || typeof b.x === 'string') { + return 0; + } + return a.x - b.x; + }); + return { + specId: ds.specId, + key: ds.key, + seriesColorKey: ds.seriesColorKey, + data: newData, + }; + }); + + return stackedDataSeries; +} + +function getStackedFormattedSeriesDatum( + data: RawDataSeriesDatum, + stackedValues: Map, + seriesIndex: number, + scaleToExtent: boolean, + isPercentageMode: boolean = false, +): DataSeriesDatum | undefined { + const { x, datum } = data; + const stack = stackedValues.get(x); + if (!stack) { + return; + } + let y1: number | null = null; if (isPercentageMode) { y1 = data.y1 != null ? data.y1 / stack.total : null; } else { @@ -105,14 +169,14 @@ export function formatStackedDataSeriesValues( const initialY0 = y0 == null ? null : y0; if (seriesIndex === 0) { - newData.push({ + return { x, y1, y0: computedY0, initialY1: y1, initialY0, datum, - }); + }; } else { const stackY = isPercentageMode ? stack.percent[seriesIndex] : stack.values[seriesIndex]; let stackedY1: number | null = null; @@ -129,25 +193,13 @@ export function formatStackedDataSeriesValues( stackedY0 = null; } } - - newData.push({ - x, - y1: stackedY1, - y0: stackedY0, - initialY1: y1, - initialY0, - datum, - }); - } - }); - return { - specId: ds.specId, - key: ds.key, - seriesColorKey: ds.seriesColorKey, - data: newData, + x, + y1: stackedY1, + y0: stackedY0, + initialY1: y1, + initialY0, + datum, }; - }); - - return stackedDataSeries; + } } diff --git a/src/utils/commons.ts b/src/utils/commons.ts index 1cd46d4946..84201a1296 100644 --- a/src/utils/commons.ts +++ b/src/utils/commons.ts @@ -123,3 +123,7 @@ export function mergePartial( return getPartialValue(base, partial, additionalPartials); } + +export function isNumberArray(value: unknown): value is number[] { + return Array.isArray(value) && value.every((element) => typeof element === 'number'); +} From 64a3c9e730a0ee74a06d4f96c7c5733aa1054965 Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Tue, 8 Oct 2019 19:23:52 +0200 Subject: [PATCH 2/2] feat(data): fill datasets with zeros with missing points when stacked When displaying a stacked area/line chart with multiple data sets we need to fill the dataset with zeros on missing data points. The rendering is affected also: it will hide the filled missing point but will continue to show the area beneath it. fix #388 --- .playground/playgroud.tsx | 147 +++++------------- .../xy_chart/rendering/rendering.ts | 10 +- src/chart_types/xy_chart/store/utils.ts | 2 +- .../utils/__snapshots__/series.test.ts.snap | 66 +++++--- src/chart_types/xy_chart/utils/series.ts | 13 ++ .../stacked_percent_series_utils.test.ts | 4 + .../xy_chart/utils/stacked_series_utils.ts | 96 +++++++----- src/components/legend/legend.tsx | 34 ++-- .../react_canvas/reactive_chart.tsx | 8 +- stories/bar_chart.tsx | 8 +- 10 files changed, 192 insertions(+), 196 deletions(-) diff --git a/.playground/playgroud.tsx b/.playground/playgroud.tsx index c93015f245..dc99fde61d 100644 --- a/.playground/playgroud.tsx +++ b/.playground/playgroud.tsx @@ -6,125 +6,64 @@ export class Playground extends React.Component { return (
- - + + - - {/* */} + Number(d).toFixed(2)} + />
diff --git a/src/chart_types/xy_chart/rendering/rendering.ts b/src/chart_types/xy_chart/rendering/rendering.ts index 0991b29774..362d62511f 100644 --- a/src/chart_types/xy_chart/rendering/rendering.ts +++ b/src/chart_types/xy_chart/rendering/rendering.ts @@ -197,9 +197,9 @@ export function renderPoints( const isLogScale = isLogarithmicScale(yScale); const pointGeometries = dataset.reduce( (acc, datum) => { - const { x: xValue, y0, y1, initialY0, initialY1 } = datum; - // don't create the point if not within the xScale domain - if (!xScale.isValueInDomain(xValue)) { + const { x: xValue, y0, y1, initialY0, initialY1, filled } = datum; + // don't create the point if not within the xScale domain or it that point was filled + if (!xScale.isValueInDomain(xValue) || (filled && filled.y1 !== undefined)) { return acc; } const x = xScale.scale(xValue); @@ -286,9 +286,9 @@ export function renderBars( const fontFamily = sharedSeriesStyle.displayValue.fontFamily; dataset.forEach((datum) => { - const { y0, y1, initialY1 } = datum; + const { y0, y1, initialY1, filled } = datum; // don't create a bar if the initialY1 value is null. - if (initialY1 === null) { + if (initialY1 === null || (filled && filled.y1 !== undefined)) { return; } // don't create a bar if not within the xScale domain diff --git a/src/chart_types/xy_chart/store/utils.ts b/src/chart_types/xy_chart/store/utils.ts index 1ed63ae942..4dc27913c8 100644 --- a/src/chart_types/xy_chart/store/utils.ts +++ b/src/chart_types/xy_chart/store/utils.ts @@ -118,7 +118,7 @@ export function getLastValues(formattedDataSeries: { if (last !== null) { const { initialY1: y1, initialY0: y0 } = last; - if (y1 !== null || y0 !== null) { + if (!last.filled && (y1 !== null || y0 !== null)) { lastValues.set(series.seriesColorKey, { y0, y1 }); } } 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 89969a1e2f..e383d6b4c4 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 @@ -17652,10 +17652,14 @@ Array [ }, Object { "datum": undefined, + "filled": Object { + "x": 3, + "y1": 0, + }, "initialY0": null, "initialY1": 0, "x": 3, - "y0": 0, + "y0": null, "y1": 0, }, Object { @@ -17685,6 +17689,10 @@ Array [ }, Object { "datum": undefined, + "filled": Object { + "x": 2, + "y1": 0, + }, "initialY0": null, "initialY1": 0, "x": 2, @@ -17701,6 +17709,10 @@ Array [ }, Object { "datum": undefined, + "filled": Object { + "x": 4, + "y1": 0, + }, "initialY0": null, "initialY1": 0, "x": 4, @@ -17739,6 +17751,10 @@ Array [ }, Object { "datum": undefined, + "filled": Object { + "x": 3, + "y1": 0, + }, "initialY0": null, "initialY1": 0, "x": 3, @@ -17772,6 +17788,10 @@ Array [ }, Object { "datum": undefined, + "filled": Object { + "x": 2, + "y1": 0, + }, "initialY0": null, "initialY1": 0, "x": 2, @@ -17788,6 +17808,10 @@ Array [ }, Object { "datum": undefined, + "filled": Object { + "x": 4, + "y1": 0, + }, "initialY0": null, "initialY1": 0, "x": 4, @@ -17826,6 +17850,10 @@ Array [ }, Object { "datum": undefined, + "filled": Object { + "x": 3, + "y1": 0, + }, "initialY0": null, "initialY1": 0, "x": 3, @@ -17913,6 +17941,10 @@ Array [ }, Object { "datum": undefined, + "filled": Object { + "x": 3, + "y1": 0, + }, "initialY0": null, "initialY1": 0, "x": 3, @@ -17993,30 +18025,21 @@ Array [ Object { "datum": undefined, "initialY0": null, -<<<<<<< HEAD - "initialY1": 4, - "x": 4, - "y0": null, - "y1": 4, -======= "initialY1": 2, "x": 2, - "y0": 0, + "y0": null, "y1": 2, ->>>>>>> feat: fill multi-series with missing x values data points }, Object { "datum": undefined, + "filled": Object { + "x": 3, + "y1": 0, + }, "initialY0": null, -<<<<<<< HEAD - "initialY1": 2, - "x": 2, - "y0": null, - "y1": 2, -======= "initialY1": 0, "x": 3, - "y0": 0, + "y0": null, "y1": 0, }, Object { @@ -18024,9 +18047,8 @@ Array [ "initialY0": null, "initialY1": 4, "x": 4, - "y0": 0, + "y0": null, "y1": 4, ->>>>>>> feat: fill multi-series with missing x values data points }, ], "key": Array [ @@ -18047,6 +18069,10 @@ Array [ }, Object { "datum": undefined, + "filled": Object { + "x": 2, + "y1": 0, + }, "initialY0": null, "initialY1": 0, "x": 2, @@ -18063,6 +18089,10 @@ Array [ }, Object { "datum": undefined, + "filled": Object { + "x": 4, + "y1": 0, + }, "initialY0": null, "initialY1": 0, "x": 4, diff --git a/src/chart_types/xy_chart/utils/series.ts b/src/chart_types/xy_chart/utils/series.ts index 1dabeefc42..741e29f100 100644 --- a/src/chart_types/xy_chart/utils/series.ts +++ b/src/chart_types/xy_chart/utils/series.ts @@ -9,6 +9,15 @@ import { formatStackedDataSeriesValues } from './stacked_series_utils'; import { LastValues } from '../store/utils'; import { ScaleType } from '../../../utils/scales/scales'; +export interface FilledValues { + /** the x value */ + x: number | string; + /** the max y value */ + y1: number | null; + /** the minimum y value */ + y0: number | null; +} + export interface RawDataSeriesDatum { /** the x value */ x: number | string; @@ -21,6 +30,7 @@ export interface RawDataSeriesDatum { } export interface DataSeriesDatum { + /** the x value */ x: number | string; /** the max y value */ y1: number | null; @@ -32,6 +42,8 @@ export interface DataSeriesDatum { initialY0: number | null; /** the datum */ datum?: any; + /** the list of filled values because missing or nulls */ + filled?: Partial; } export interface DataSeries { @@ -341,6 +353,7 @@ export function getSplittedSeries( xValues.add(xValue); } } + // keep the user order for ordinal scales if (!isOrdinalScale) { xValues = new Set([...xValues].sort()); } 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 b0135a5b07..348c42645d 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 @@ -307,6 +307,10 @@ describe('Stacked Series Utils', () => { x: 2, y0: 1, y1: 1, + filled: { + x: 2, + y1: 0, + }, }); expect(formattedData[1].data[2]).toEqual({ datum: undefined, 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 c3cb941cd6..a9a97731a4 100644 --- a/src/chart_types/xy_chart/utils/stacked_series_utils.ts +++ b/src/chart_types/xy_chart/utils/stacked_series_utils.ts @@ -1,4 +1,4 @@ -import { DataSeries, DataSeriesDatum, RawDataSeries, RawDataSeriesDatum } from './series'; +import { DataSeries, DataSeriesDatum, RawDataSeries, RawDataSeriesDatum, FilledValues } from './series'; import { ScaleType } from '../../../utils/scales/scales'; interface StackedValues { @@ -12,8 +12,11 @@ interface StackedValues { * ordering the stack based on the dataseries index. * @param dataseries */ -export function getYValueStackMap(dataseries: RawDataSeries[], xValues: Set): Map { - const stackMap = new Map(); +export function getYValueStackMap( + dataseries: RawDataSeries[], + xValues: Set, +): Map { + const stackMap = new Map(); const missingXValues = new Set([...xValues]); dataseries.forEach((ds, index) => { ds.data.forEach((datum) => { @@ -26,6 +29,7 @@ export function getYValueStackMap(dataseries: RawDataSeries[], xValues: Set, seriesIndex: number, scaleToExtent: boolean, - isPercentageMode: boolean = false, + isPercentageMode = false, + filled?: Partial, ): DataSeriesDatum | undefined { const { x, datum } = data; const stack = stackedValues.get(x); @@ -154,45 +165,47 @@ function getStackedFormattedSeriesDatum( return; } let y1: number | null = null; - if (isPercentageMode) { - y1 = data.y1 != null ? data.y1 / stack.total : null; - } else { - y1 = data.y1; - } - let y0 = isPercentageMode && data.y0 != null ? data.y0 / stack.total : data.y0; - let computedY0: number | null; - if (scaleToExtent) { - computedY0 = y0 ? y0 : y1; - } else { - computedY0 = y0 ? y0 : null; + if (isPercentageMode) { + y1 = data.y1 != null ? data.y1 / stack.total : null; + } else { + y1 = data.y1; + } + let y0 = isPercentageMode && data.y0 != null ? data.y0 / stack.total : data.y0; + let computedY0: number | null; + if (scaleToExtent) { + computedY0 = y0 ? y0 : y1; + } else { + computedY0 = y0 ? y0 : null; + } + const initialY0 = y0 == null ? null : y0; + + if (seriesIndex === 0) { + return { + x, + y1, + y0: computedY0, + initialY1: y1, + initialY0, + datum, + ...(filled && { filled }), + }; + } else { + const stackY = isPercentageMode ? stack.percent[seriesIndex] : stack.values[seriesIndex]; + let stackedY1: number | null = null; + let stackedY0: number | null = null; + if (isPercentageMode) { + stackedY1 = y1 !== null ? stackY + y1 : null; + stackedY0 = y0 != null ? stackY + y0 : stackY; + } else { + stackedY1 = y1 !== null ? stackY + y1 : null; + stackedY0 = y0 != null ? stackY + y0 : stackY; + // configure null y0 if y1 is null + // it's semantically right to say y0 is null if y1 is null + if (stackedY1 === null) { + stackedY0 = null; } - const initialY0 = y0 == null ? null : y0; + } - if (seriesIndex === 0) { - return { - x, - y1, - y0: computedY0, - initialY1: y1, - initialY0, - datum, - }; - } else { - const stackY = isPercentageMode ? stack.percent[seriesIndex] : stack.values[seriesIndex]; - let stackedY1: number | null = null; - let stackedY0: number | null = null; - if (isPercentageMode) { - stackedY1 = y1 !== null ? stackY + y1 : null; - stackedY0 = y0 != null ? stackY + y0 : stackY; - } else { - stackedY1 = y1 !== null ? stackY + y1 : null; - stackedY0 = y0 != null ? stackY + y0 : stackY; - // configure null y0 if y1 is null - // it's semantically right to say y0 is null if y1 is null - if (stackedY1 === null) { - stackedY0 = null; - } - } return { x, y1: stackedY1, @@ -200,6 +213,7 @@ function getStackedFormattedSeriesDatum( initialY1: y1, initialY0, datum, + ...(filled && { filled }), }; } } diff --git a/src/components/legend/legend.tsx b/src/components/legend/legend.tsx index 3595e3924c..e37340671e 100644 --- a/src/components/legend/legend.tsx +++ b/src/components/legend/legend.tsx @@ -169,25 +169,21 @@ class LegendComponent extends React.Component { const tooltipValues = legendItemTooltipValues.get(); const legendValues = this.getLegendValues(tooltipValues, key, banded); - return ( - <> - {legendValues.map((value, index) => { - const yAccessor: AccessorType = index === 0 ? AccessorType.Y1 : AccessorType.Y0; - return ( - - ); - })} - - ); + return legendValues.map((value, index) => { + const yAccessor: AccessorType = index === 0 ? AccessorType.Y1 : AccessorType.Y0; + return ( + + ); + }); }; } diff --git a/src/components/react_canvas/reactive_chart.tsx b/src/components/react_canvas/reactive_chart.tsx index 00a124cd97..7dbe422a31 100644 --- a/src/components/react_canvas/reactive_chart.tsx +++ b/src/components/react_canvas/reactive_chart.tsx @@ -355,10 +355,10 @@ class Chart extends React.Component { sortAndRenderElements() { const { chartRotation, chartDimensions } = this.props.chartStore!; const clippings = { - clipX: 0, - clipY: 0, - clipWidth: [90, -90].includes(chartRotation) ? chartDimensions.height : chartDimensions.width, - clipHeight: [90, -90].includes(chartRotation) ? chartDimensions.width : chartDimensions.height, + clipX: -1, + clipY: -1, + clipWidth: ([90, -90].includes(chartRotation) ? chartDimensions.height : chartDimensions.width) + 1, + clipHeight: ([90, -90].includes(chartRotation) ? chartDimensions.width : chartDimensions.height) + 1, }; const bars = this.renderBarSeries(clippings); diff --git a/stories/bar_chart.tsx b/stories/bar_chart.tsx index 7b47ba005e..de24e461a5 100644 --- a/stories/bar_chart.tsx +++ b/stories/bar_chart.tsx @@ -88,7 +88,7 @@ storiesOf('Bar Chart', module) hideClippedValue, }; - const debug = boolean('debug', true); + const debug = boolean('debug', false); const chartRotation = select( 'chartRotation', { @@ -133,7 +133,7 @@ storiesOf('Bar Chart', module) return ( - + Number(d).toFixed(2)} /> - -