diff --git a/src/lib/series/domains/y_domain.test.ts b/src/lib/series/domains/y_domain.test.ts index c083110311..0f8eb71459 100644 --- a/src/lib/series/domains/y_domain.test.ts +++ b/src/lib/series/domains/y_domain.test.ts @@ -691,4 +691,109 @@ describe('Y Domain', () => { const errorMessage = 'custom yDomain for a is invalid, computed min is greater than custom max'; expect(attemptToMerge).toThrowError(errorMessage); }); + test('Should merge Y domain with stacked as percentage', () => { + const dataSeries1: RawDataSeries[] = [ + { + specId: getSpecId('a'), + key: [''], + seriesColorKey: '', + data: [{ x: 1, y1: 2 }, { x: 2, y1: 2 }, { x: 3, y1: 2 }, { x: 4, y1: 5 }], + }, + { + specId: getSpecId('a'), + key: [''], + seriesColorKey: '', + data: [{ x: 1, y1: 2 }, { x: 4, y1: 7 }], + }, + ]; + const dataSeries2: RawDataSeries[] = [ + { + specId: getSpecId('a'), + key: [''], + seriesColorKey: '', + data: [{ x: 1, y1: 10 }, { x: 2, y1: 10 }, { x: 3, y1: 2 }, { x: 4, y1: 5 }], + }, + ]; + const specDataSeries = new Map(); + specDataSeries.set(getSpecId('a'), dataSeries1); + specDataSeries.set(getSpecId('b'), dataSeries2); + const mergedDomain = mergeYDomain( + specDataSeries, + [ + { + seriesType: 'area', + yScaleType: ScaleType.Linear, + groupId: getGroupId('a'), + id: getSpecId('a'), + stackAccessors: ['a'], + yScaleToDataExtent: true, + stackAsPercentage: true, + }, + { + seriesType: 'area', + yScaleType: ScaleType.Log, + groupId: getGroupId('a'), + id: getSpecId('b'), + yScaleToDataExtent: true, + }, + ], + new Map(), + ); + expect(mergedDomain).toEqual([ + { + groupId: 'a', + domain: [0, 1], + scaleType: ScaleType.Linear, + isBandScale: false, + type: 'yDomain', + }, + ]); + }); + test('Should merge Y domain with as percentage regadless of custom domains', () => { + const groupId = getGroupId('a'); + + const dataSeries: RawDataSeries[] = [ + { + specId: getSpecId('a'), + key: [''], + seriesColorKey: '', + data: [{ x: 1, y1: 2 }, { x: 2, y1: 2 }, { x: 3, y1: 2 }, { x: 4, y1: 5 }], + }, + { + specId: getSpecId('a'), + key: [''], + seriesColorKey: '', + data: [{ x: 1, y1: 2 }, { x: 4, y1: 7 }], + }, + ]; + const specDataSeries = new Map(); + specDataSeries.set(getSpecId('a'), dataSeries); + const domainsByGroupId = new Map(); + domainsByGroupId.set(groupId, { min: 2, max: 20 }); + + const mergedDomain = mergeYDomain( + specDataSeries, + [ + { + seriesType: 'area', + yScaleType: ScaleType.Linear, + groupId, + id: getSpecId('a'), + stackAccessors: ['a'], + yScaleToDataExtent: true, + stackAsPercentage: true, + }, + ], + domainsByGroupId, + ); + expect(mergedDomain).toEqual([ + { + type: 'yDomain', + groupId, + domain: [0, 1], + scaleType: ScaleType.Linear, + isBandScale: false, + }, + ]); + }); }); diff --git a/src/lib/series/domains/y_domain.ts b/src/lib/series/domains/y_domain.ts index 0cf36d95cf..a0d87175af 100644 --- a/src/lib/series/domains/y_domain.ts +++ b/src/lib/series/domains/y_domain.ts @@ -17,7 +17,7 @@ export type YDomain = BaseDomain & { export type YBasicSeriesSpec = Pick< BasicSeriesSpec, 'id' | 'seriesType' | 'yScaleType' | 'groupId' | 'stackAccessors' | 'yScaleToDataExtent' | 'colorAccessors' ->; +> & { stackAsPercentage?: boolean }; export function mergeYDomain( dataSeries: Map, @@ -32,50 +32,54 @@ export function mergeYDomain( const yDomains = specsByGroupIdsEntries.map( ([groupId, groupSpecs]): YDomain => { const groupYScaleType = coerceYScaleTypes([...groupSpecs.stacked, ...groupSpecs.nonStacked]); - - // compute stacked domain - const isStackedScaleToExtent = groupSpecs.stacked.some((spec) => { - return spec.yScaleToDataExtent; - }); - const stackedDataSeries = getDataSeriesOnGroup(dataSeries, groupSpecs.stacked); - const stackedDomain = computeYStackedDomain(stackedDataSeries, isStackedScaleToExtent); - - // compute non stacked domain - const isNonStackedScaleToExtent = groupSpecs.nonStacked.some((spec) => { - return spec.yScaleToDataExtent; - }); - const nonStackedDataSeries = getDataSeriesOnGroup(dataSeries, groupSpecs.nonStacked); - const nonStackedDomain = computeYNonStackedDomain(nonStackedDataSeries, isNonStackedScaleToExtent); - - // merge stacked and non stacked domain together - const groupDomain = computeContinuousDataDomain( - [...stackedDomain, ...nonStackedDomain], - identity, - isStackedScaleToExtent || isNonStackedScaleToExtent, - ); - - const [computedDomainMin, computedDomainMax] = groupDomain; - let domain = groupDomain; - - const customDomain = domainsByGroupId.get(groupId); - - if (customDomain && isCompleteBound(customDomain)) { - // Don't need to check min > max because this has been validated on axis domain merge - domain = [customDomain.min, customDomain.max]; - } else if (customDomain && isLowerBound(customDomain)) { - if (customDomain.min > computedDomainMax) { - throw new Error(`custom yDomain for ${groupId} is invalid, custom min is greater than computed max`); - } - - domain = [customDomain.min, computedDomainMax]; - } else if (customDomain && isUpperBound(customDomain)) { - if (computedDomainMin > customDomain.max) { - throw new Error(`custom yDomain for ${groupId} is invalid, computed min is greater than custom max`); + const { isPercentageStack } = groupSpecs; + + let domain: number[]; + if (isPercentageStack) { + domain = computeContinuousDataDomain([0, 1], identity); + } else { + // compute stacked domain + const isStackedScaleToExtent = groupSpecs.stacked.some((spec) => { + return spec.yScaleToDataExtent; + }); + const stackedDataSeries = getDataSeriesOnGroup(dataSeries, groupSpecs.stacked); + const stackedDomain = computeYStackedDomain(stackedDataSeries, isStackedScaleToExtent); + + // compute non stacked domain + const isNonStackedScaleToExtent = groupSpecs.nonStacked.some((spec) => { + return spec.yScaleToDataExtent; + }); + const nonStackedDataSeries = getDataSeriesOnGroup(dataSeries, groupSpecs.nonStacked); + const nonStackedDomain = computeYNonStackedDomain(nonStackedDataSeries, isNonStackedScaleToExtent); + + // merge stacked and non stacked domain together + domain = computeContinuousDataDomain( + [...stackedDomain, ...nonStackedDomain], + identity, + isStackedScaleToExtent || isNonStackedScaleToExtent, + ); + + const [computedDomainMin, computedDomainMax] = domain; + + const customDomain = domainsByGroupId.get(groupId); + + if (customDomain && isCompleteBound(customDomain)) { + // Don't need to check min > max because this has been validated on axis domain merge + domain = [customDomain.min, customDomain.max]; + } else if (customDomain && isLowerBound(customDomain)) { + if (customDomain.min > computedDomainMax) { + throw new Error(`custom yDomain for ${groupId} is invalid, custom min is greater than computed max`); + } + + domain = [customDomain.min, computedDomainMax]; + } else if (customDomain && isUpperBound(customDomain)) { + if (computedDomainMin > customDomain.max) { + throw new Error(`custom yDomain for ${groupId} is invalid, computed min is greater than custom max`); + } + + domain = [computedDomainMin, customDomain.max]; } - - domain = [computedDomainMin, customDomain.max]; } - return { type: 'yDomain', isBandScale: false, @@ -139,10 +143,14 @@ function computeYNonStackedDomain(dataseries: RawDataSeries[], scaleToExtent: bo return computeContinuousDataDomain([...yValues.values()], identity, scaleToExtent); } export function splitSpecsByGroupId(specs: YBasicSeriesSpec[]) { - const specsByGroupIds = new Map(); + const specsByGroupIds = new Map< + GroupId, + { isPercentageStack: boolean; stacked: YBasicSeriesSpec[]; nonStacked: YBasicSeriesSpec[] } + >(); // split each specs by groupId and by stacked or not specs.forEach((spec) => { const group = specsByGroupIds.get(spec.groupId) || { + isPercentageStack: false, stacked: [], nonStacked: [], }; @@ -151,6 +159,9 @@ export function splitSpecsByGroupId(specs: YBasicSeriesSpec[]) { } else { group.nonStacked.push(spec); } + if (spec.stackAsPercentage === true) { + group.isPercentageStack = true; + } specsByGroupIds.set(spec.groupId, group); }); return specsByGroupIds; diff --git a/src/lib/series/legend.ts b/src/lib/series/legend.ts index dec81ecbd6..0c24de0cb5 100644 --- a/src/lib/series/legend.ts +++ b/src/lib/series/legend.ts @@ -56,7 +56,7 @@ export function computeLegend( isLegendItemVisible: !hideInLegend, displayValue: { raw: series.lastValue, - formatted: formatter(series.lastValue), + formatted: isSeriesVisible ? formatter(series.lastValue) : undefined, }, }); }); diff --git a/src/lib/series/series.ts b/src/lib/series/series.ts index 14e5558758..6f6d6da3ea 100644 --- a/src/lib/series/series.ts +++ b/src/lib/series/series.ts @@ -219,12 +219,18 @@ export function getFormattedDataseries( }[] = []; specsByGroupIdsEntries.forEach(([groupId, groupSpecs]) => { + const { isPercentageStack } = groupSpecs; // format stacked data series const stackedDataSeries = getRawDataSeries(groupSpecs.stacked, dataSeries); + const stackedDataSeriesValues = formatStackedDataSeriesValues( + stackedDataSeries.rawDataSeries, + false, + isPercentageStack, + ); stackedFormattedDataSeries.push({ groupId, counts: stackedDataSeries.counts, - dataSeries: formatStackedDataSeriesValues(stackedDataSeries.rawDataSeries, false), + dataSeries: stackedDataSeriesValues, }); // format non stacked data series diff --git a/src/lib/series/specs.ts b/src/lib/series/specs.ts index 993ae85ae6..5a88632944 100644 --- a/src/lib/series/specs.ts +++ b/src/lib/series/specs.ts @@ -136,6 +136,10 @@ export type BarSeriesSpec = BasicSeriesSpec & { /** If true, will stack all BarSeries and align bars to ticks (instead of centered on ticks) */ enableHistogramMode?: boolean; barSeriesStyle?: CustomBarSeriesStyle; + /** + * Stack each series in percentage for each point. + */ + stackAsPercentage?: boolean; }; /** @@ -167,6 +171,10 @@ export type AreaSeriesSpec = BasicSeriesSpec & /** The type of interpolator to be used to interpolate values between points */ curve?: CurveType; areaSeriesStyle?: AreaSeriesStyle; + /** + * Stack each series in percentage for each point. + */ + stackAsPercentage?: boolean; }; interface HistogramConfig { diff --git a/src/lib/series/stacked_percent_series_utils.test.ts b/src/lib/series/stacked_percent_series_utils.test.ts new file mode 100644 index 0000000000..4d639d2226 --- /dev/null +++ b/src/lib/series/stacked_percent_series_utils.test.ts @@ -0,0 +1,293 @@ +import { getSpecId } from '../utils/ids'; +import { RawDataSeries } from './series'; +import { formatStackedDataSeriesValues } from './stacked_series_utils'; + +describe('Stacked Series Utils', () => { + const STANDARD_DATA_SET: RawDataSeries[] = [ + { + data: [ + { + x: 0, + y1: 10, + }, + ], + key: [], + seriesColorKey: 'color-key', + specId: getSpecId('spec1'), + }, + { + data: [ + { + x: 0, + y1: 20, + }, + ], + key: [], + seriesColorKey: 'color-key', + specId: getSpecId('spec2'), + }, + { + data: [ + { + x: 0, + y1: 70, + }, + ], + key: [], + seriesColorKey: 'color-key', + specId: getSpecId('spec3'), + }, + ]; + const WITH_NULL_DATASET: RawDataSeries[] = [ + { + data: [ + { + x: 0, + y1: 10, + }, + ], + key: [], + seriesColorKey: 'color-key', + specId: getSpecId('spec1'), + }, + { + data: [ + { + x: 0, + y1: null, + }, + ], + key: [], + seriesColorKey: 'color-key', + specId: getSpecId('spec2'), + }, + { + data: [ + { + x: 0, + y1: 30, + }, + ], + key: [], + seriesColorKey: 'color-key', + specId: getSpecId('spec3'), + }, + ]; + const STANDARD_DATA_SET_WY0: RawDataSeries[] = [ + { + data: [ + { + x: 0, + y0: 2, + y1: 10, + }, + ], + key: [], + seriesColorKey: 'color-key', + specId: getSpecId('spec1'), + }, + { + data: [ + { + x: 0, + y0: 4, + y1: 20, + }, + ], + key: [], + seriesColorKey: 'color-key', + specId: getSpecId('spec2'), + }, + { + data: [ + { + x: 0, + y0: 6, + y1: 70, + }, + ], + key: [], + seriesColorKey: 'color-key', + specId: getSpecId('spec3'), + }, + ]; + const WITH_NULL_DATASET_WY0: RawDataSeries[] = [ + { + data: [ + { + x: 0, + y0: 2, + y1: 10, + }, + ], + key: [], + seriesColorKey: 'color-key', + specId: getSpecId('spec1'), + }, + { + data: [ + { + x: 0, + y1: null, + }, + ], + key: [], + seriesColorKey: 'color-key', + specId: getSpecId('spec2'), + }, + { + data: [ + { + x: 0, + y0: 6, + y1: 90, + }, + ], + key: [], + seriesColorKey: 'color-key', + specId: getSpecId('spec3'), + }, + ]; + const DATA_SET_WITH_NULL_2: RawDataSeries[] = [ + { + specId: getSpecId('spec1'), + key: ['a'], + seriesColorKey: 'a', + data: [{ x: 1, y1: 10 }, { x: 2, y1: 20 }, { x: 4, y1: 40 }], + }, + { + specId: getSpecId('spec1'), + key: ['b'], + seriesColorKey: 'b', + data: [{ x: 1, y1: 90 }, { x: 3, y1: 30 }], + }, + ]; + + describe.only('Format stacked dataset', () => { + test('format data without nulls', () => { + const formattedData = formatStackedDataSeriesValues(STANDARD_DATA_SET, false, true); + const data0 = formattedData[0].data[0]; + expect(data0.initialY1).toBe(0.1); + expect(data0.y0).toBe(0); + expect(data0.y1).toBe(0.1); + + const data1 = formattedData[1].data[0]; + expect(data1.initialY1).toBe(0.2); + expect(data1.y0).toBe(0.1); + expect(data1.y1).toBeCloseTo(0.3); + + const data2 = formattedData[2].data[0]; + expect(data2.initialY1).toBe(0.7); + expect(data2.y0).toBe(0.3); + expect(data2.y1).toBe(1); + }); + test('format data with nulls', () => { + const formattedData = formatStackedDataSeriesValues(WITH_NULL_DATASET, false, true); + const data0 = formattedData[0].data[0]; + expect(data0.initialY1).toBe(0.25); + expect(data0.y0).toBe(0); + expect(data0.y1).toBe(0.25); + + expect(formattedData[1].data[0]).toEqual({ + datum: undefined, + initialY0: null, + initialY1: null, + x: 0, + y1: null, + y0: 0.25, + }); + + const data2 = formattedData[2].data[0]; + expect(data2.initialY1).toBe(0.75); + expect(data2.y0).toBe(0.25); + expect(data2.y1).toBe(1); + }); + test('format data without nulls with y0 values', () => { + const formattedData = formatStackedDataSeriesValues(STANDARD_DATA_SET_WY0, false, true); + const data0 = formattedData[0].data[0]; + expect(data0.initialY0).toBe(0.02); + expect(data0.initialY1).toBe(0.1); + expect(data0.y0).toBe(0.02); + expect(data0.y1).toBe(0.1); + + const data1 = formattedData[1].data[0]; + expect(data1.initialY0).toBe(0.04); + expect(data1.initialY1).toBe(0.2); + expect(data1.y0).toBe(0.14); + expect(data1.y1).toBeCloseTo(0.3, 5); + + const data2 = formattedData[2].data[0]; + expect(data2.initialY0).toBe(0.06); + expect(data2.initialY1).toBe(0.7); + expect(data2.y0).toBe(0.36); + expect(data2.y1).toBe(1); + }); + test('format data with nulls', () => { + const formattedData = formatStackedDataSeriesValues(WITH_NULL_DATASET_WY0, false, true); + const data0 = formattedData[0].data[0]; + expect(data0.initialY0).toBe(0.02); + expect(data0.initialY1).toBe(0.1); + expect(data0.y0).toBe(0.02); + expect(data0.y1).toBe(0.1); + + const data1 = formattedData[1].data[0]; + expect(data1.initialY0).toBe(null); + expect(data1.initialY1).toBe(null); + expect(data1.y0).toBe(0.1); + expect(data1.y1).toBe(null); + + const data2 = formattedData[2].data[0]; + expect(data2.initialY0).toBe(0.06); + expect(data2.initialY1).toBe(0.9); + expect(data2.y0).toBe(0.16); + expect(data2.y1).toBe(1); + }); + test('format data without nulls on second series', () => { + const formattedData = formatStackedDataSeriesValues(DATA_SET_WITH_NULL_2, false, true); + expect(formattedData.length).toBe(2); + expect(formattedData[0].data.length).toBe(3); + expect(formattedData[1].data.length).toBe(2); + + expect(formattedData[0].data[0]).toEqual({ + datum: undefined, + initialY0: null, + initialY1: 0.1, + x: 1, + y0: 0, + y1: 0.1, + }); + expect(formattedData[0].data[1]).toEqual({ + datum: undefined, + initialY0: null, + initialY1: 1, + x: 2, + y0: 0, + y1: 1, + }); + expect(formattedData[0].data[2]).toEqual({ + datum: undefined, + initialY0: null, + initialY1: 1, + x: 4, + y0: 0, + y1: 1, + }); + expect(formattedData[1].data[0]).toEqual({ + datum: undefined, + initialY0: null, + initialY1: 0.9, + x: 1, + y0: 0.1, + y1: 1, + }); + expect(formattedData[1].data[1]).toEqual({ + datum: undefined, + initialY0: null, + initialY1: 1, + x: 3, + y0: 0, + y1: 1, + }); + }); + }); +}); diff --git a/src/lib/series/stacked_series_utils.test.ts b/src/lib/series/stacked_series_utils.test.ts index 3da6e6c22f..b8621c4b62 100644 --- a/src/lib/series/stacked_series_utils.test.ts +++ b/src/lib/series/stacked_series_utils.test.ts @@ -182,6 +182,7 @@ describe('Stacked Series Utils', () => { expect(x0StackArray).toBeDefined(); expect(x0StackArray.length).toBe(3); expect(x0StackArray).toEqual([10, 20, 30]); + // expect(x0StackArray).toEqual([10, 20, 30]); }); test('with values with nulls', () => { const stackedMap = getYValueStackMap(WITH_NULL_DATASET); @@ -206,7 +207,9 @@ describe('Stacked Series Utils', () => { expect(computedStackedMap.size).toBe(1); const x0Array = computedStackedMap.get(0)!; expect(x0Array).toBeDefined(); - expect(x0Array).toEqual([0, 10, 30, 60]); + expect(x0Array.values).toEqual([0, 10, 30, 60]); + expect(x0Array.percent).toEqual([0, 0.16666666666666666, 0.5, 1]); + expect(x0Array.total).toBe(60); }); test('with null values', () => { const stackedMap = getYValueStackMap(WITH_NULL_DATASET); @@ -214,7 +217,9 @@ describe('Stacked Series Utils', () => { expect(computedStackedMap.size).toBe(1); const x0Array = computedStackedMap.get(0)!; expect(x0Array).toBeDefined(); - expect(x0Array).toEqual([0, 10, 10, 40]); + expect(x0Array.values).toEqual([0, 10, 10, 40]); + expect(x0Array.percent).toEqual([0, 0.25, 0.25, 1]); + expect(x0Array.total).toBe(40); }); }); describe('Format stacked dataset', () => { diff --git a/src/lib/series/stacked_series_utils.ts b/src/lib/series/stacked_series_utils.ts index 17cb1a5fa4..d2da96382d 100644 --- a/src/lib/series/stacked_series_utils.ts +++ b/src/lib/series/stacked_series_utils.ts @@ -1,5 +1,11 @@ import { DataSeries, DataSeriesDatum, RawDataSeries } from './series'; +interface StackedValues { + values: number[]; + percent: number[]; + total: number; +} + /** * Map each y value from a RawDataSeries on it's specific x value into, * ordering the stack based on the dataseries index. @@ -26,28 +32,51 @@ export function getYValueStackMap(dataseries: RawDataSeries[]): Map, scaleToExtent: boolean, -): Map { - const stackedValues = new Map(); +): Map { + const stackedValues = new Map(); yValueStackMap.forEach((yStackArray, xValue) => { const stackArray = yStackArray.reduce( - (stackedValue, currentValue, index) => { - if (stackedValue.length === 0) { + (acc, currentValue, index) => { + if (acc.values.length === 0) { if (scaleToExtent) { - return [currentValue, currentValue]; + return { + values: [currentValue, currentValue], + total: currentValue, + }; } - return [0, currentValue]; + return { + values: [0, currentValue], + total: currentValue, + }; } - return [...stackedValue, stackedValue[index] + currentValue]; + return { + values: [...acc.values, acc.values[index] + currentValue], + total: acc.total + currentValue, + }; + }, + { + values: [] as number[], + total: 0, }, - [] as number[], ); - stackedValues.set(xValue, stackArray); + const percent = stackArray.values.map((value) => { + return value / stackArray.total; + }); + stackedValues.set(xValue, { + values: stackArray.values, + percent, + total: stackArray.total, + }); }); return stackedValues; } -export function formatStackedDataSeriesValues(dataseries: RawDataSeries[], scaleToExtent: boolean): DataSeries[] { +export function formatStackedDataSeriesValues( + dataseries: RawDataSeries[], + scaleToExtent: boolean, + isPercentageMode: boolean = false, +): DataSeries[] { const yValueStackMap = getYValueStackMap(dataseries); const stackedValues = computeYStackedMapValues(yValueStackMap, scaleToExtent); @@ -55,17 +84,25 @@ export function formatStackedDataSeriesValues(dataseries: RawDataSeries[], scale const stackedDataSeries: DataSeries[] = dataseries.map((ds, seriesIndex) => { const newData: DataSeriesDatum[] = []; ds.data.forEach((data) => { - const { x, y1, datum } = data; - if (stackedValues.get(x) === 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 { + y1 = data.y1; + } + let y0 = isPercentageMode && data.y0 != null ? data.y0 / stack.total : data.y0; let computedY0: number | null; if (scaleToExtent) { - computedY0 = data.y0 ? data.y0 : y1; + computedY0 = y0 ? y0 : y1; } else { - computedY0 = data.y0 ? data.y0 : 0; + computedY0 = y0 ? y0 : 0; } - const initialY0 = data.y0 == null ? null : data.y0; + const initialY0 = y0 == null ? null : y0; if (seriesIndex === 0) { newData.push({ x, @@ -76,17 +113,20 @@ export function formatStackedDataSeriesValues(dataseries: RawDataSeries[], scale datum, }); } else { - const stack = stackedValues.get(x); - if (!stack) { - return; - } - const stackY = stack[seriesIndex]; - const stackedY1 = y1 !== null ? stackY + y1 : null; - let stackedY0: number | null = data.y0 == null ? stackY : stackY + data.y0; - // 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 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; + } } newData.push({ x, @@ -98,6 +138,7 @@ export function formatStackedDataSeriesValues(dataseries: RawDataSeries[], scale }); } }); + return { specId: ds.specId, key: ds.key, diff --git a/src/specs/bar_series.tsx b/src/specs/bar_series.tsx index 6785ee479e..b2e85330ce 100644 --- a/src/specs/bar_series.tsx +++ b/src/specs/bar_series.tsx @@ -18,6 +18,7 @@ export class BarSeriesSpecComponent extends PureComponent { yScaleToDataExtent: false, hideInLegend: false, enableHistogramMode: false, + stackAsPercentage: false, }; componentDidMount() { const { chartStore, children, ...config } = this.props; diff --git a/src/state/utils.ts b/src/state/utils.ts index 3c6c763586..b296d4b9e5 100644 --- a/src/state/utils.ts +++ b/src/state/utils.ts @@ -122,14 +122,34 @@ export function computeSeriesDomains( const formattedDataSeries = getFormattedDataseries(specsArray, splittedSeries); // tslint:disable-next-line:no-console - // console.log({ formattedDataSeries, xDomain, yDomain }); - + // console.log({ formattedDataSeries, xDomain, yDomain });\ + const lastValues = new Map(); + + formattedDataSeries.stacked.forEach((ds) => { + ds.dataSeries.forEach((series) => { + if (series.data.length > 0) { + const last = series.data[series.data.length - 1]; + if (last !== null && last.initialY1 !== null) { + lastValues.set(series.seriesColorKey, last.initialY1); + } + } + }); + }); + const updatedSeriesColors = new Map(); + seriesColors.forEach((value, key) => { + const lastValue = lastValues.get(key); + const updatedColorSet = { + ...value, + lastValue, + }; + updatedSeriesColors.set(key, updatedColorSet); + }); return { xDomain, yDomain, splittedDataSeries, formattedDataSeries, - seriesColors, + seriesColors: updatedSeriesColors, }; } diff --git a/stories/area_chart.tsx b/stories/area_chart.tsx index 9280e6e88d..706805a4af 100644 --- a/stories/area_chart.tsx +++ b/stories/area_chart.tsx @@ -228,6 +228,42 @@ storiesOf('Area Chart', module) ); }) + .add('stacked as percentage', () => { + const stackedAsPercentage = boolean('stacked as percentage', true); + return ( + + + + `${Number(d * 100).toFixed(0)} %`} + /> + + + + ); + }) .add('stacked with separated specs', () => { return ( diff --git a/stories/bar_chart.tsx b/stories/bar_chart.tsx index c7f5700431..64095bd411 100644 --- a/stories/bar_chart.tsx +++ b/stories/bar_chart.tsx @@ -475,6 +475,43 @@ storiesOf('Bar Chart', module) ); }) + .add('stacked as percentage', () => { + const stackedAsPercentage = boolean('stacked as percentage', true); + const clusterBars = boolean('cluster', true); + return ( + + + + (stackedAsPercentage && clusterBars ? `${Number(d * 100).toFixed(0)} %` : d)} + /> + + + + ); + }) .add('clustered with axis and legend', () => { const chartRotation = select( 'chartRotation',