From 590f342f01ec1fcc9b1dadf99942712b657526b3 Mon Sep 17 00:00:00 2001 From: Irina Kuzmina Date: Wed, 24 Jan 2024 10:01:01 +0200 Subject: [PATCH] feat(D3 plugin): Stacked percentage charts (bar-x, bar-y, area) (#391) * feat(D3 plugin): percent stacking * fix review(1) --- src/i18n/keysets/en.json | 3 +- src/i18n/keysets/ru.json | 3 +- .../d3/__stories__/Showcase.stories.tsx | 19 ++++- .../d3/__stories__/area/Basic.stories.tsx | 8 +++ .../d3/__stories__/bar-x/BarX.stories.tsx | 8 +++ .../d3/__stories__/bar-y/BarY.stories.tsx | 8 +++ .../d3/examples/area/PercentStacking.tsx | 69 +++++++++++++++++++ .../d3/examples/bar-x/PercentStack.tsx | 61 ++++++++++++++++ .../d3/examples/bar-y/PercentStacking.tsx | 63 +++++++++++++++++ .../d3/renderer/hooks/useSeries/types.ts | 2 + .../d3/renderer/hooks/useSeries/utils.ts | 2 +- .../hooks/useShapes/area/prepare-data.ts | 23 +++++++ .../hooks/useShapes/bar-x/prepare-data.ts | 21 +++++- .../hooks/useShapes/bar-y/prepare-data.ts | 22 ++++-- .../validation/__tests__/validation.test.ts | 19 +++++ src/plugins/d3/renderer/validation/index.ts | 24 ++++++- src/types/widget-data/area.ts | 4 +- src/types/widget-data/bar-x.ts | 1 - src/types/widget-data/bar-y.ts | 1 - 19 files changed, 343 insertions(+), 18 deletions(-) create mode 100644 src/plugins/d3/examples/area/PercentStacking.tsx create mode 100644 src/plugins/d3/examples/bar-x/PercentStack.tsx create mode 100644 src/plugins/d3/examples/bar-y/PercentStacking.tsx diff --git a/src/i18n/keysets/en.json b/src/i18n/keysets/en.json index 1b6eadf5..34c273c2 100644 --- a/src/i18n/keysets/en.json +++ b/src/i18n/keysets/en.json @@ -33,7 +33,8 @@ "label_invalid-axis-datetime-data-point": "It seems you are trying to use inappropriate data type for \"{{key}}\" value in series \"{{seriesName}}\" for axis with type \"datetime\". Only numbers are allowed.", "label_invalid-axis-linear-data-point": "It seems you are trying to use inappropriate data type for \"{{key}}\" value in series \"{{seriesName}}\" for axis with type \"linear\". Numbers and nulls are allowed.", "label_invalid-pie-data-value": "It seems you are trying to use inappropriate data type for \"value\" value. Only numbers are allowed.", - "label_invalid-series-type": "It seems you haven't defined \"series.type\" property, or defined it incorrectly. Available values: [{{types}}]." + "label_invalid-series-type": "It seems you haven't defined \"series.type\" property, or defined it incorrectly. Available values: [{{types}}].", + "label_invalid-series-property": "It seems you are trying to use inappropriate value for \"{{key}}\", or defined it incorrectly. Available values: [{{values}}]." }, "highcharts": { "reset-zoom-title": "Reset zoom", diff --git a/src/i18n/keysets/ru.json b/src/i18n/keysets/ru.json index 59693f8c..b0e4e6f5 100644 --- a/src/i18n/keysets/ru.json +++ b/src/i18n/keysets/ru.json @@ -35,7 +35,8 @@ "label_invalid-axis-datetime-data-point": "Похоже, что вы пытаетесь использовать недопустимый тип данных для значения \"{{key}}\" в серии \"{{seriesName}}\" для оси с типом \"datetime\". Допускается только использование чисел.", "label_invalid-axis-linear-data-point": "Похоже, что вы пытаетесь использовать недопустимый тип данных для значения \"{{key}}\" в серии \"{{seriesName}}\" для оси с типом \"linear\". Допускается использование чисел и значений null.", "label_invalid-pie-data-value": "Похоже, что вы пытаетесь использовать недопустимый тип данных для значения \"value\". Допускается только использование чисел.", - "label_invalid-series-type": "Похоже, что вы не указали значение \"series.type\" или указали его неверно. Доступные значения: [{{types}}]." + "label_invalid-series-type": "Похоже, что вы не указали значение \"series.type\" или указали его неверно. Доступные значения: [{{types}}].", + "label_invalid-series-property": "Похоже, что вы пытаетесь использовать недопустимое значение для \"{{key}}\", или указали его неверно. Доступные значения: [{{values}}]." }, "highcharts": { "reset-zoom-title": "Сбросить увеличение", diff --git a/src/plugins/d3/__stories__/Showcase.stories.tsx b/src/plugins/d3/__stories__/Showcase.stories.tsx index 935e8a5c..c6f12920 100644 --- a/src/plugins/d3/__stories__/Showcase.stories.tsx +++ b/src/plugins/d3/__stories__/Showcase.stories.tsx @@ -9,6 +9,7 @@ import {D3Plugin} from '../index'; import {BasicBarXChart} from '../examples/bar-x/Basic'; import {GroupedColumns} from '../examples/bar-x/GroupedColumns'; import {StackedColumns} from '../examples/bar-x/StackedColumns'; +import {PercentStackColumns} from '../examples/bar-x/PercentStack'; import {DataLabels as BarXDataLabels} from '../examples/bar-x/DataLabels'; import {Basic as BasicBarY} from '../examples/bar-y/Basic'; import {GroupedColumns as GroupedColumnsBarY} from '../examples/bar-y/GroupedColumns'; @@ -23,6 +24,8 @@ import {Donut} from '../examples/pie/Donut'; import {LineAndBarXCombinedChart} from '../examples/combined/LineAndBarX'; import {LineWithMarkers} from '../examples/line/LineWithMarkers'; import {StackedArea} from '../examples/area/StackedArea'; +import {PercentStackingBars} from '../examples/bar-y/PercentStacking'; +import {PercentStackingArea} from '../examples/area/PercentStacking'; const ShowcaseStory = () => { const [loading, setLoading] = React.useState(true); @@ -71,6 +74,10 @@ const ShowcaseStory = () => { Stacked area + + Stacked percentage areas + + Bar-x charts @@ -85,9 +92,13 @@ const ShowcaseStory = () => { - Stacked columns + Stacked columns(normal) + + Stacked percentage column + + Bar-x chart with data labels @@ -105,10 +116,14 @@ const ShowcaseStory = () => { Grouped bars - + Stacked bars + + Stacked percentage bars + + Pie charts diff --git a/src/plugins/d3/__stories__/area/Basic.stories.tsx b/src/plugins/d3/__stories__/area/Basic.stories.tsx index 4b3fc707..e9eb3dc2 100644 --- a/src/plugins/d3/__stories__/area/Basic.stories.tsx +++ b/src/plugins/d3/__stories__/area/Basic.stories.tsx @@ -6,6 +6,7 @@ import {settings} from '../../../../libs'; import {D3Plugin} from '../..'; import {Basic} from '../../examples/area/Basic'; import {StackedArea} from '../../examples/area/StackedArea'; +import {PercentStackingArea} from '../../examples/area/PercentStacking'; const ChartStory = ({Chart}: {Chart: React.FC}) => { const [shown, setShown] = React.useState(false); @@ -41,6 +42,13 @@ export const StackedAreaChartStory: StoryObj = { }, }; +export const PercentStackingAreaChartStory: StoryObj = { + name: 'Stacked percentage areas', + args: { + Chart: PercentStackingArea, + }, +}; + export default { title: 'Plugins/D3/Area', decorators: [withKnobs], diff --git a/src/plugins/d3/__stories__/bar-x/BarX.stories.tsx b/src/plugins/d3/__stories__/bar-x/BarX.stories.tsx index 91098eea..4462a806 100644 --- a/src/plugins/d3/__stories__/bar-x/BarX.stories.tsx +++ b/src/plugins/d3/__stories__/bar-x/BarX.stories.tsx @@ -11,6 +11,7 @@ import { } from '../../examples/bar-x/Basic'; import {GroupedColumns} from '../../examples/bar-x/GroupedColumns'; import {StackedColumns} from '../../examples/bar-x/StackedColumns'; +import {PercentStackColumns} from '../../examples/bar-x/PercentStack'; const ChartStory = ({Chart}: {Chart: React.FC}) => { const [shown, setShown] = React.useState(false); @@ -67,6 +68,13 @@ export const StackedBarXChartStory: StoryObj = { }, }; +export const PercentStackBarXChartStory: StoryObj = { + name: 'Stacked percentage columns', + args: { + Chart: PercentStackColumns, + }, +}; + export default { title: 'Plugins/D3/Bar-X', decorators: [withKnobs], diff --git a/src/plugins/d3/__stories__/bar-y/BarY.stories.tsx b/src/plugins/d3/__stories__/bar-y/BarY.stories.tsx index 776e3c15..9ab97b68 100644 --- a/src/plugins/d3/__stories__/bar-y/BarY.stories.tsx +++ b/src/plugins/d3/__stories__/bar-y/BarY.stories.tsx @@ -6,6 +6,7 @@ import {D3Plugin} from '../..'; import {Basic} from '../../examples/bar-y/Basic'; import {GroupedColumns} from '../../examples/bar-y/GroupedColumns'; import {StackedColumns} from '../../examples/bar-y/StackedColumns'; +import {PercentStackingBars} from '../../examples/bar-y/PercentStacking'; const ChartStory = ({Chart}: {Chart: React.FC}) => { const [shown, setShown] = React.useState(false); @@ -48,6 +49,13 @@ export const StackedBarYChartStory: StoryObj = { }, }; +export const PercentStackingBarYChartStory: StoryObj = { + name: 'Stacked percentage bars', + args: { + Chart: PercentStackingBars, + }, +}; + export default { title: 'Plugins/D3/Bar-Y', component: ChartStory, diff --git a/src/plugins/d3/examples/area/PercentStacking.tsx b/src/plugins/d3/examples/area/PercentStacking.tsx new file mode 100644 index 00000000..e8b6505d --- /dev/null +++ b/src/plugins/d3/examples/area/PercentStacking.tsx @@ -0,0 +1,69 @@ +import {groups} from 'd3'; +import React from 'react'; +import {ChartKit} from '../../../../components/ChartKit'; +import type {ChartKitWidgetData, AreaSeries, AreaSeriesData} from '../../../../types'; +import {ExampleWrapper} from '../ExampleWrapper'; +import nintendoGames from '../nintendoGames'; + +const years = Array.from( + new Set( + nintendoGames.map((d) => + d.date ? String(new Date(d.date as number).getFullYear()) : 'unknown', + ), + ), +).sort(); + +function prepareData() { + const grouped = groups( + nintendoGames, + (d) => d.platform, + (d) => (d.date ? String(new Date(d.date as number).getFullYear()) : 'unknown'), + ); + const series = grouped.map(([platform, gamesByYear]) => { + const platformGames = Object.fromEntries(gamesByYear) || {}; + return { + name: platform, + data: years.reduce((acc, year) => { + if (year in platformGames) { + acc.push({ + x: year, + y: platformGames[year].length, + }); + } + + return acc; + }, []), + }; + }); + + return {series}; +} + +export const PercentStackingArea = () => { + const {series} = prepareData(); + + const data = series.map((s) => { + return { + type: 'area', + stacking: 'percent', + name: s.name, + data: s.data, + } as AreaSeries; + }); + + const widgetData: ChartKitWidgetData = { + series: { + data: data, + }, + xAxis: { + type: 'category', + categories: years, + }, + }; + + return ( + + + + ); +}; diff --git a/src/plugins/d3/examples/bar-x/PercentStack.tsx b/src/plugins/d3/examples/bar-x/PercentStack.tsx new file mode 100644 index 00000000..10b1ed5b --- /dev/null +++ b/src/plugins/d3/examples/bar-x/PercentStack.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import {groups} from 'd3'; +import {ChartKit} from '../../../../components/ChartKit'; +import type {BarXSeries, ChartKitWidgetData} from '../../../../types'; +import {ExampleWrapper} from '../ExampleWrapper'; +import nintendoGames from '../nintendoGames'; + +function prepareData() { + const grouped = groups( + nintendoGames, + (d) => d.platform, + (d) => (d.date ? new Date(d.date as number).getFullYear() : 'unknown'), + ); + const categories: string[] = []; + const series = grouped.map(([platform, years]) => { + return { + name: platform, + data: years.map(([year, list]) => { + categories.push(String(year)); + + return { + x: String(year), + y: list.length, + }; + }), + }; + }); + + return {categories, series}; +} + +export const PercentStackColumns = () => { + const {series, categories} = prepareData(); + const data = series.map((s) => { + return { + type: 'bar-x', + stacking: 'percent', + name: s.name, + data: s.data, + } as BarXSeries; + }); + + const widgetData: ChartKitWidgetData = { + series: { + data: data, + }, + xAxis: { + type: 'category', + categories: categories.sort(), + title: { + text: 'Release year', + }, + }, + }; + + return ( + + + + ); +}; diff --git a/src/plugins/d3/examples/bar-y/PercentStacking.tsx b/src/plugins/d3/examples/bar-y/PercentStacking.tsx new file mode 100644 index 00000000..0a334fcb --- /dev/null +++ b/src/plugins/d3/examples/bar-y/PercentStacking.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import {groups} from 'd3'; +import {ChartKit} from '../../../../components/ChartKit'; +import type {BarYSeries, ChartKitWidgetData} from '../../../../types'; +import {ExampleWrapper} from '../ExampleWrapper'; +import nintendoGames from '../nintendoGames'; + +function prepareData() { + const grouped = groups( + nintendoGames, + (d) => d.platform, + (d) => (d.date ? new Date(d.date as number).getFullYear() : 'unknown'), + ); + const categories: string[] = []; + const series = grouped.map(([platform, years]) => { + return { + name: platform, + data: years.map(([year, list]) => { + categories.push(String(year)); + + return { + y: String(year), + x: list.length, + }; + }), + }; + }); + + return {categories, series}; +} + +export const PercentStackingBars = () => { + const {series, categories} = prepareData(); + const data = series.map((s) => { + return { + type: 'bar-y', + stacking: 'percent', + name: s.name, + data: s.data, + } as BarYSeries; + }); + + const widgetData: ChartKitWidgetData = { + series: { + data: data, + }, + yAxis: [ + { + type: 'category', + categories: categories.sort(), + title: { + text: 'Release year', + }, + }, + ], + }; + + return ( + + + + ); +}; diff --git a/src/plugins/d3/renderer/hooks/useSeries/types.ts b/src/plugins/d3/renderer/hooks/useSeries/types.ts index 41cf1c19..94a35d8e 100644 --- a/src/plugins/d3/renderer/hooks/useSeries/types.ts +++ b/src/plugins/d3/renderer/hooks/useSeries/types.ts @@ -92,6 +92,7 @@ export type PreparedBarXSeries = { type: BarXSeries['type']; data: BarXSeriesData[]; stackId: string; + stacking: BarXSeries['stacking']; dataLabels: { enabled: boolean; inside: boolean; @@ -105,6 +106,7 @@ export type PreparedBarYSeries = { type: BarYSeries['type']; data: BarYSeriesData[]; stackId: string; + stacking: BarYSeries['stacking']; dataLabels: { enabled: boolean; inside: boolean; diff --git a/src/plugins/d3/renderer/hooks/useSeries/utils.ts b/src/plugins/d3/renderer/hooks/useSeries/utils.ts index dcb85456..d962176b 100644 --- a/src/plugins/d3/renderer/hooks/useSeries/utils.ts +++ b/src/plugins/d3/renderer/hooks/useSeries/utils.ts @@ -39,7 +39,7 @@ export function getSeriesStackId(series: StackedSeries) { let stackId = series.stackId; if (!stackId) { - stackId = series.stacking === 'normal' ? getCommonStackId() : getRandomCKId(); + stackId = series.stacking ? getCommonStackId() : getRandomCKId(); } return stackId; diff --git a/src/plugins/d3/renderer/hooks/useShapes/area/prepare-data.ts b/src/plugins/d3/renderer/hooks/useShapes/area/prepare-data.ts index 5f35288e..d730a4b8 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/area/prepare-data.ts +++ b/src/plugins/d3/renderer/hooks/useShapes/area/prepare-data.ts @@ -1,3 +1,4 @@ +import type {ScaleLinear} from 'd3'; import {group, sort} from 'd3'; import type {PreparedAreaSeries} from '../../useSeries/types'; import type {PreparedAxis} from '../../useChartOptions/types'; @@ -70,6 +71,8 @@ export const prepareAreaData = (args: { yScale: ChartScale; }): PreparedAreaData[] => { const {series, xAxis, xScale, yScale} = args; + const yLinearScale = yScale as ScaleLinear; + const plotHeight = yLinearScale(yLinearScale.domain()[0]); const yAxis = args.yAxis[0]; const [_xMin, xRangeMax] = xScale.range(); const xMax = xRangeMax / (1 - xAxis.maxPadding); @@ -140,6 +143,26 @@ export const prepareAreaData = (args: { return acc; }, []); + if (series.some((s) => s.stacking === 'percent')) { + xValues.forEach(([x], index) => { + const stackHeight = accumulatedYValues.get(x) || 0; + let acc = 0; + const ratio = plotHeight / stackHeight; + + seriesStackData.forEach((item) => { + const point = item.points[index]; + + if (point) { + const height = (point.y0 - point.y) * ratio; + point.y0 = plotHeight - height - acc; + point.y = point.y0 + height; + + acc += height; + } + }); + }); + } + return result.concat(seriesStackData); }, [], diff --git a/src/plugins/d3/renderer/hooks/useShapes/bar-x/prepare-data.ts b/src/plugins/d3/renderer/hooks/useShapes/bar-x/prepare-data.ts index 2bf8690f..507c7b9a 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/bar-x/prepare-data.ts +++ b/src/plugins/d3/renderer/hooks/useShapes/bar-x/prepare-data.ts @@ -46,6 +46,8 @@ export const prepareBarXData = (args: { yScale: ChartScale; }): PreparedBarXData[] => { const {series, seriesOptions, xAxis, xScale, yScale} = args; + const yLinearScale = yScale as ScaleLinear; + const plotHeight = yLinearScale(yLinearScale.domain()[0]); const categories = get(xAxis, 'categories', [] as string[]); const barMaxWidth = get(seriesOptions, 'bar-x.barMaxWidth'); const barPadding = get(seriesOptions, 'bar-x.barPadding'); @@ -131,6 +133,7 @@ export const prepareBarXData = (args: { const currentGroupWidth = rectWidth * stacks.length + rectGap * (stacks.length - 1); stacks.forEach((yValues, groupItemIndex) => { let stackHeight = 0; + const stackItems: PreparedBarXData[] = []; const sortedData = sortKey ? sort(yValues, (a, b) => comparator(get(a, sortKey), get(b, sortKey))) @@ -149,9 +152,8 @@ export const prepareBarXData = (args: { } const x = xCenter - currentGroupWidth / 2 + (rectWidth + rectGap) * groupItemIndex; - const yLinearScale = yScale as ScaleLinear; const y = yLinearScale(yValue.data.y as number); - const height = yLinearScale(yLinearScale.domain()[0]) - y; + const height = plotHeight - y; const barData: PreparedBarXData = { x, @@ -164,10 +166,23 @@ export const prepareBarXData = (args: { barData.label = getLabelData(barData); - result.push(barData); + stackItems.push(barData); stackHeight += height + 1; }); + + if (series.some((s) => s.stacking === 'percent')) { + let acc = 0; + const ratio = plotHeight / (stackHeight - stackItems.length); + stackItems.forEach((item) => { + item.height = item.height * ratio; + item.y = plotHeight - item.height - acc; + + acc += item.height + 1; + }); + } + + result.push(...stackItems); }); }); diff --git a/src/plugins/d3/renderer/hooks/useShapes/bar-y/prepare-data.ts b/src/plugins/d3/renderer/hooks/useShapes/bar-y/prepare-data.ts index 29fcb6ae..2d2d24e4 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/bar-y/prepare-data.ts +++ b/src/plugins/d3/renderer/hooks/useShapes/bar-y/prepare-data.ts @@ -77,6 +77,8 @@ export const prepareBarYData = (args: { yScale: ChartScale; }): PreparedBarYData[] => { const {series, seriesOptions, yAxis, xScale, yScale} = args; + const xLinearScale = xScale as ScaleLinear; + const plotWidth = xLinearScale(xLinearScale.domain()[1]); const barMaxWidth = get(seriesOptions, 'bar-y.barMaxWidth'); const barPadding = get(seriesOptions, 'bar-y.barPadding'); const groupPadding = get(seriesOptions, 'bar-y.groupPadding'); @@ -116,6 +118,7 @@ export const prepareBarYData = (args: { stacks.forEach((measureValues, groupItemIndex) => { let stackSum = 0; + const stackItems: PreparedBarYData[] = []; const sortedData = sortKey ? sort(measureValues, (a, b) => comparator(get(a, sortKey), get(b, sortKey))) : measureValues; @@ -131,11 +134,9 @@ export const prepareBarYData = (args: { } const y = center - currentBarHeight / 2 + (barHeight + rectGap) * groupItemIndex; - const xLinearScale = xScale as ScaleLinear; - const x = xLinearScale(data.x as number); - const width = x - xLinearScale(xLinearScale.domain()[0]); + const width = xLinearScale(data.x as number); - result.push({ + stackItems.push({ x: stackSum, y, width, @@ -147,6 +148,19 @@ export const prepareBarYData = (args: { stackSum += width + 1; }); + + if (series.some((s) => s.stacking === 'percent')) { + let acc = 0; + const ratio = plotWidth / (stackSum - stackItems.length); + stackItems.forEach((item) => { + item.width = item.width * ratio; + item.x = acc; + + acc += item.width; + }); + } + + result.push(...stackItems); }); }); diff --git a/src/plugins/d3/renderer/validation/__tests__/validation.test.ts b/src/plugins/d3/renderer/validation/__tests__/validation.test.ts index 1db9c2b2..1c93ba19 100644 --- a/src/plugins/d3/renderer/validation/__tests__/validation.test.ts +++ b/src/plugins/d3/renderer/validation/__tests__/validation.test.ts @@ -70,4 +70,23 @@ describe('plugins/d3/validation', () => { expect(error?.code).toEqual(CHARTKIT_ERROR_CODE.INVALID_DATA); }, ); + + test.each([ + {series: {data: [{type: 'area', stacking: 'notNormal', data: [{x: 1, y: 1}]}]}}, + {series: {data: [{type: 'bar-x', stacking: 'notNormal', data: [{x: 1, y: 1}]}]}}, + {series: {data: [{type: 'bar-y', stacking: 'notNormal', data: [{x: 1, y: 1}]}]}}, + ])( + 'validateData should throw an error in case of invalid stacking value (data: %j)', + (data) => { + let error: ChartKitError | null = null; + + try { + validateData(data as ChartKitWidgetData); + } catch (e) { + error = e as ChartKitError; + } + + expect(error?.code).toEqual(CHARTKIT_ERROR_CODE.INVALID_DATA); + }, + ); }); diff --git a/src/plugins/d3/renderer/validation/index.ts b/src/plugins/d3/renderer/validation/index.ts index ac152834..f8d18df6 100644 --- a/src/plugins/d3/renderer/validation/index.ts +++ b/src/plugins/d3/renderer/validation/index.ts @@ -4,6 +4,7 @@ import isEmpty from 'lodash/isEmpty'; import {SeriesType} from '../../../../constants'; import {ChartKitError, CHARTKIT_ERROR_CODE} from '../../../../libs'; import { + AreaSeries, BarXSeries, BarYSeries, ChartKitWidgetAxis, @@ -17,7 +18,7 @@ import {i18n} from '../../../../i18n'; import {DEFAULT_AXIS_TYPE} from '../constants'; -type XYSeries = ScatterSeries | BarXSeries | BarYSeries | LineSeries; +type XYSeries = ScatterSeries | BarXSeries | BarYSeries | LineSeries | AreaSeries; const AVAILABLE_SERIES_TYPES = Object.values(SeriesType); @@ -122,6 +123,20 @@ const validatePieSeries = ({series}: {series: PieSeries}) => { }); }; +const validateStacking = ({series}: {series: AreaSeries | BarXSeries | BarYSeries}) => { + const availableStackingValues = ['normal', 'percent']; + + if (series.stacking && !availableStackingValues.includes(series.stacking)) { + throw new ChartKitError({ + code: CHARTKIT_ERROR_CODE.INVALID_DATA, + message: i18n('error', 'label_invalid-series-property', { + key: 'stacking', + values: availableStackingValues, + }), + }); + } +}; + const validateSeries = (args: { series: ChartKitWidgetSeries; xAxis?: ChartKitWidgetAxis; @@ -139,8 +154,13 @@ const validateSeries = (args: { } switch (series.type) { - case 'bar-x': + case 'area': case 'bar-y': + case 'bar-x': { + validateXYSeries({series, xAxis, yAxis}); + validateStacking({series}); + break; + } case 'line': case 'scatter': { validateXYSeries({series, xAxis, yAxis}); diff --git a/src/types/widget-data/area.ts b/src/types/widget-data/area.ts index f9ae413c..2509fea5 100644 --- a/src/types/widget-data/area.ts +++ b/src/types/widget-data/area.ts @@ -34,11 +34,11 @@ export type AreaSeries = BaseSeries & { /** The name of the series (used in legend, tooltip etc) */ name: string; /** Whether to stack the values of each series on top of each other. - * Possible values are undefined to disable, "normal" to stack by value + * Possible values are undefined to disable, "normal" to stack by value or "percent" * * @default undefined * */ - stacking?: 'normal'; + stacking?: 'normal' | 'percent'; /** This option allows grouping series in a stacked chart */ stackId?: string; /** The main color of the series (hex, rgba) */ diff --git a/src/types/widget-data/bar-x.ts b/src/types/widget-data/bar-x.ts index ca5aa657..e85cfaec 100644 --- a/src/types/widget-data/bar-x.ts +++ b/src/types/widget-data/bar-x.ts @@ -35,7 +35,6 @@ export type BarXSeries = BaseSeries & { name: string; /** The main color of the series (hex, rgba) */ color?: string; - // FIXME 'percent' (https://github.com/gravity-ui/chartkit/issues/329) /** Whether to stack the values of each series on top of each other. * Possible values are undefined to disable, "normal" to stack by value or "percent" * diff --git a/src/types/widget-data/bar-y.ts b/src/types/widget-data/bar-y.ts index 3815c5b1..d20b92a3 100644 --- a/src/types/widget-data/bar-y.ts +++ b/src/types/widget-data/bar-y.ts @@ -29,7 +29,6 @@ export type BarYSeries = BaseSeries & { name: string; /** The main color of the series (hex, rgba) */ color?: string; - // fixme 'percent' /** Whether to stack the values of each series on top of each other. * Possible values are undefined to disable, "normal" to stack by value or "percent" *