diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-stacked-percentage-with-zeros-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-stacked-percentage-with-zeros-visually-looks-correct-1-snap.png new file mode 100644 index 00000000000..d53744abdfe Binary files /dev/null and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-stacked-percentage-with-zeros-visually-looks-correct-1-snap.png differ 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 21e072e9e3b..b49cd2daa4d 100644 --- a/src/chart_types/xy_chart/rendering/rendering.areas.test.ts +++ b/src/chart_types/xy_chart/rendering/rendering.areas.test.ts @@ -26,6 +26,7 @@ import { computeSeriesDomains } from '../state/utils'; import { renderArea } from './rendering'; import { ChartTypes } from '../..'; import { SpecTypes } from '../../../specs/settings'; +import { MockSeriesSpec } from '../../../mocks/specs'; const SPEC_ID = 'spec_1'; const GROUP_ID = 'group_1'; @@ -1081,4 +1082,115 @@ describe('Rendering points - areas', () => { expect((zeroValueIndexdGeometry[0] as PointGeometry).radius).toBe(0); }); }); + it('Stacked areas with 0 values', () => { + const pointSeriesSpec1: AreaSeriesSpec = MockSeriesSpec.area({ + id: 'spec_1', + data: [ + [1546300800000, 0], + [1546387200000, 5], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Time, + yScaleType: ScaleType.Linear, + stackAccessors: [0], + stackAsPercentage: true, + }); + const pointSeriesSpec2: AreaSeriesSpec = MockSeriesSpec.area({ + id: 'spec_2', + data: [ + [1546300800000, 0], + [1546387200000, 2], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Time, + yScaleType: ScaleType.Linear, + stackAccessors: [0], + stackAsPercentage: true, + }); + const pointSeriesDomains = computeSeriesDomains([pointSeriesSpec1, pointSeriesSpec2]); + expect(pointSeriesDomains.formattedDataSeries.stacked[0].dataSeries[0].data).toEqual([ + { + datum: [1546300800000, 0], + initialY0: null, + initialY1: null, + x: 1546300800000, + y0: null, + y1: null, + }, + { + datum: [1546387200000, 5], + initialY0: null, + initialY1: 0.7142857142857143, + x: 1546387200000, + y0: null, + y1: 0.7142857142857143, + }, + ]); + }); + it('Stacked areas with null values', () => { + const pointSeriesSpec1: AreaSeriesSpec = MockSeriesSpec.area({ + id: 'spec_1', + data: [ + [1546300800000, null], + [1546387200000, 5], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Time, + yScaleType: ScaleType.Linear, + stackAccessors: [0], + }); + const pointSeriesSpec2: AreaSeriesSpec = MockSeriesSpec.area({ + id: 'spec_2', + data: [ + [1546300800000, 3], + [1546387200000, null], + ], + xAccessor: 0, + yAccessors: [1], + xScaleType: ScaleType.Time, + yScaleType: ScaleType.Linear, + stackAccessors: [0], + }); + const pointSeriesDomains = computeSeriesDomains([pointSeriesSpec1, pointSeriesSpec2]); + expect(pointSeriesDomains.formattedDataSeries.stacked[0].dataSeries[0].data).toEqual([ + { + datum: [1546300800000, null], + initialY0: null, + initialY1: null, + x: 1546300800000, + y0: null, + y1: null, + }, + { + datum: [1546387200000, 5], + initialY0: null, + initialY1: 5, + x: 1546387200000, + y0: null, + y1: 5, + }, + ]); + + expect(pointSeriesDomains.formattedDataSeries.stacked[0].dataSeries[1].data).toEqual([ + { + datum: [1546300800000, 3], + initialY0: null, + initialY1: 3, + x: 1546300800000, + y0: null, + y1: 3, + }, + { + datum: [1546387200000, null], + initialY0: null, + initialY1: null, + x: 1546387200000, + y0: null, + y1: null, + }, + ]); + }); }); 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 0d0a75d8369..d8d39344b22 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 @@ -17,7 +17,13 @@ * under the License. */ import { RawDataSeries } from './series'; -import { computeYStackedMapValues, formatStackedDataSeriesValues, getYValueStackMap } from './stacked_series_utils'; +import { + computeYStackedMapValues, + formatStackedDataSeriesValues, + getYValueStackMap, + getStackedFormattedSeriesDatum, + StackedValues, +} from './stacked_series_utils'; import { ScaleType } from '../../../scales'; describe('Stacked Series Utils', () => { @@ -439,4 +445,21 @@ describe('Stacked Series Utils', () => { }); }); }); + test('Correctly handle 0 values on percentage stack', () => { + const stackedValues: Map = new Map(); + stackedValues.set(1, { + values: [0, 0, 0], + percent: [null, null, null], + total: 0, + }); + const formattedDatum = getStackedFormattedSeriesDatum({ x: 1, y1: 0 }, stackedValues, 0, false, true); + expect(formattedDatum).toEqual({ + datum: undefined, + initialY0: null, + initialY1: null, + x: 1, + y0: null, + y1: null, + }); + }); }); 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 37cb66a407f..9a815944064 100644 --- a/src/chart_types/xy_chart/utils/stacked_series_utils.ts +++ b/src/chart_types/xy_chart/utils/stacked_series_utils.ts @@ -19,9 +19,10 @@ import { DataSeries, DataSeriesDatum, RawDataSeries, RawDataSeriesDatum, FilledValues } from './series'; import { ScaleType } from '../../../scales'; -interface StackedValues { +/** @internal */ +export interface StackedValues { values: number[]; - percent: number[]; + percent: Array; total: number; } @@ -103,6 +104,9 @@ export function computeYStackedMapValues( }, ); const percent = stackArray.values.map((value) => { + if (stackArray.total === 0) { + return null; + } return value / stackArray.total; }); stackedValues.set(xValue, { @@ -124,7 +128,6 @@ export function formatStackedDataSeriesValues( ): 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]); @@ -169,11 +172,11 @@ export function formatStackedDataSeriesValues( data: newData, }; }); - return stackedDataSeries; } -function getStackedFormattedSeriesDatum( +/** @internal */ +export function getStackedFormattedSeriesDatum( data: RawDataSeriesDatum, stackedValues: Map, seriesIndex: number, @@ -187,12 +190,19 @@ function getStackedFormattedSeriesDatum( return; } let y1: number | null = null; + let y0: number | null | undefined = null; if (isPercentageMode) { - y1 = data.y1 != null ? data.y1 / stack.total : null; + if (data.y1 != null && stack.total !== 0) { + y1 = data.y1 / stack.total; + } + if (data.y0 != null && stack.total !== 0) { + y0 = data.y0 / stack.total; + } } else { y1 = data.y1; + y0 = data.y0; } - const y0 = isPercentageMode && data.y0 != null ? data.y0 / stack.total : data.y0; + let computedY0: number | null; if (scaleToExtent) { computedY0 = y0 ? y0 : y1; @@ -216,11 +226,16 @@ function getStackedFormattedSeriesDatum( let stackedY1: number | null = null; let stackedY0: number | null = null; if (isPercentageMode) { - stackedY1 = y1 !== null ? stackY + y1 : null; - stackedY0 = y0 != null ? stackY + y0 : stackY; + stackedY1 = y1 !== null && stackY != null ? stackY + y1 : null; + stackedY0 = y0 != null && stackY != null ? stackY + y0 : stackY; } else { - stackedY1 = y1 !== null ? stackY + y1 : null; - stackedY0 = y0 != null ? stackY + y0 : stackY; + if (stackY == null) { + stackedY1 = y1 !== null ? y1 : null; + stackedY0 = y0 != null ? 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 correct to say y0 is null if y1 is null if (stackedY1 === null) { diff --git a/stories/area/8_stacked_percentage_zeros.tsx b/stories/area/8_stacked_percentage_zeros.tsx new file mode 100644 index 00000000000..e52c2342770 --- /dev/null +++ b/stories/area/8_stacked_percentage_zeros.tsx @@ -0,0 +1,242 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ + +import React from 'react'; +import { AreaSeries, Axis, Chart, Position, ScaleType, Settings, niceTimeFormatter } from '../../src'; + +export const example = () => { + return ( + + + + `${Number(d * 100).toFixed(0)} %`} + /> + + + + + + ); +}; + +const DATA = [ + { + id: '61ca57f1-469d-11e7-af02-69e470af7417:200', + label: '200', + color: 'rgb(115, 216, 255)', + data: [ + [1585234800000, 10], + [1585249200000, 6], + [1585263600000, 2], + [1585278000000, 5], + [1585292400000, 70], + [1585306800000, 84], + [1585321200000, 32], + [1585335600000, 3], + [1585350000000, 2], + [1585364400000, 18], + [1585378800000, 66], + [1585393200000, 82], + [1585407600000, 32], + [1585422000000, 4], + [1585436400000, 1], + [1585447200000, 9], + [1585461600000, 58], + [1585476000000, 70], + [1585490400000, 58], + [1585504800000, 9], + [1585519200000, 0], + [1585533600000, 10], + [1585548000000, 46], + [1585562400000, 95], + [1585576800000, 43], + [1585591200000, 10], + [1585605600000, 1], + [1585620000000, 8], + [1585634400000, 56], + [1585648800000, 88], + [1585663200000, 50], + [1585677600000, 7], + [1585692000000, 1], + [1585706400000, 9], + [1585720800000, 59], + [1585735200000, 83], + [1585749600000, 46], + [1585764000000, 7], + [1585778400000, 2], + [1585792800000, 12], + [1585807200000, 44], + [1585821600000, 84], + [1585836000000, 41], + ], + seriesId: '61ca57f1-469d-11e7-af02-69e470af7417', + stack: 'percent', + lines: { show: true, fill: 0.5, lineWidth: 2, steps: false }, + points: { show: false, radius: 1, lineWidth: 5 }, + bars: { show: false, fill: 0.5, lineWidth: 2 }, + }, + { + id: '61ca57f1-469d-11e7-af02-69e470af7417:404', + label: '404', + color: 'rgb(0, 157, 217)', + data: [ + [1585234800000, 0], + [1585249200000, 0], + [1585263600000, 0], + [1585278000000, 1], + [1585292400000, 6], + [1585306800000, 8], + [1585321200000, 3], + [1585335600000, 0], + [1585350000000, 1], + [1585364400000, 1], + [1585378800000, 2], + [1585393200000, 3], + [1585407600000, 1], + [1585422000000, 0], + [1585436400000, 0], + [1585447200000, 1], + [1585461600000, 0], + [1585476000000, 6], + [1585490400000, 3], + [1585504800000, 0], + [1585519200000, 0], + [1585533600000, 1], + [1585548000000, 4], + [1585562400000, 6], + [1585576800000, 1], + [1585591200000, 0], + [1585605600000, 1], + [1585620000000, 0], + [1585634400000, 1], + [1585648800000, 6], + [1585663200000, 4], + [1585677600000, 0], + [1585692000000, 0], + [1585706400000, 0], + [1585720800000, 1], + [1585735200000, 8], + [1585749600000, 2], + [1585764000000, 0], + [1585778400000, 0], + [1585792800000, 0], + [1585807200000, 1], + [1585821600000, 6], + [1585836000000, 3], + ], + seriesId: '61ca57f1-469d-11e7-af02-69e470af7417', + stack: 'percent', + lines: { show: true, fill: 0.5, lineWidth: 2, steps: false }, + points: { show: false, radius: 1, lineWidth: 5 }, + bars: { show: false, fill: 0.5, lineWidth: 2 }, + }, + { + id: '61ca57f1-469d-11e7-af02-69e470af7417:503', + label: '503', + color: 'rgb(0, 46, 64)', + data: [ + [1585234800000, 0], + [1585249200000, 0], + [1585263600000, 0], + [1585278000000, 1], + [1585292400000, 2], + [1585306800000, 3], + [1585321200000, 3], + [1585335600000, 0], + [1585350000000, 0], + [1585364400000, 0], + [1585378800000, 2], + [1585393200000, 2], + [1585407600000, 2], + [1585422000000, 0], + [1585436400000, 0], + [1585447200000, 0], + [1585461600000, 1], + [1585476000000, 3], + [1585490400000, 2], + [1585504800000, 0], + [1585519200000, 0], + [1585533600000, 0], + [1585548000000, 2], + [1585562400000, 1], + [1585576800000, 3], + [1585591200000, 0], + [1585605600000, 0], + [1585620000000, 0], + [1585634400000, 1], + [1585648800000, 1], + [1585663200000, 1], + [1585677600000, 0], + [1585692000000, 0], + [1585706400000, 0], + [1585720800000, 1], + [1585735200000, 3], + [1585749600000, 1], + [1585764000000, 1], + [1585778400000, 0], + [1585792800000, 0], + [1585807200000, 2], + [1585821600000, 1], + [1585836000000, 3], + ], + seriesId: '61ca57f1-469d-11e7-af02-69e470af7417', + stack: 'percent', + lines: { show: true, fill: 0.5, lineWidth: 2, steps: false }, + points: { show: false, radius: 1, lineWidth: 5 }, + bars: { show: false, fill: 0.5, lineWidth: 2 }, + }, +]; diff --git a/stories/area/area.stories.tsx b/stories/area/area.stories.tsx index 762d7b43a18..6debf19144a 100644 --- a/stories/area/area.stories.tsx +++ b/stories/area/area.stories.tsx @@ -33,6 +33,7 @@ export { example as with4Axes } from './5_with_4_axes'; export { example as withAxisAndLegend } from './6_with_axis_and_legend'; export { example as stacked } from './7_stacked'; export { example as stackedPercentage } from './8_stacked_percentage'; +export { example as stackedPercentageWithZeros } from './8_stacked_percentage_zeros'; export { example as stackedSeparateSpecs } from './9_stacked_separate_specs'; export { example as stackedSameNaming } from './10_stacked_same_naming'; export { example as bandArea } from './13_band_area';