diff --git a/packages/polaris-viz/src/components/BarChart/BarChart.tsx b/packages/polaris-viz/src/components/BarChart/BarChart.tsx index 6fe748d86..7beef1b72 100644 --- a/packages/polaris-viz/src/components/BarChart/BarChart.tsx +++ b/packages/polaris-viz/src/components/BarChart/BarChart.tsx @@ -30,6 +30,7 @@ import {useRenderTooltipContent} from '../../hooks'; import {HorizontalBarChart} from '../HorizontalBarChart'; import {VerticalBarChart} from '../VerticalBarChart'; import {ChartSkeleton} from '../../components/ChartSkeleton'; +import {fillMissingDataPoints} from '../../utilities/fillMissingDataPoints'; export type BarChartProps = { errorText?: string; @@ -51,7 +52,7 @@ export function BarChart(props: BarChartProps) { const { annotations = [], - data, + data: dataSeries, state, errorText, direction = 'vertical', @@ -71,6 +72,8 @@ export function BarChart(props: BarChartProps) { ...props, }; + const data = fillMissingDataPoints(dataSeries); + const skipLinkAnchorId = useRef(uniqueId('BarChart')); const emptyState = data.length === 0; diff --git a/packages/polaris-viz/src/components/BarChart/stories/playground/MisMatchedData.stories.tsx b/packages/polaris-viz/src/components/BarChart/stories/playground/MisMatchedData.stories.tsx new file mode 100644 index 000000000..c0312967a --- /dev/null +++ b/packages/polaris-viz/src/components/BarChart/stories/playground/MisMatchedData.stories.tsx @@ -0,0 +1,45 @@ +import type {Story} from '@storybook/react'; + +import {BarChart, BarChartProps} from '../../../../components'; +import {META} from '../meta'; + +export default { + ...META, + title: `${META.title}/Playground`, +}; + +const DATA = [ + { + name: 'Canada', + data: [ + {key: 'Mice', value: 13.28}, + {key: 'Dogs', value: 23.43}, + {key: 'Cats', value: 6.64}, + {key: 'Birds', value: 54.47}, + ], + }, + { + name: 'United States', + data: [ + {key: 'Lizards', value: 350.13}, + {key: 'Turtles', value: 223.43}, + {key: 'Mice', value: 15.38}, + {key: 'Snakes', value: 122.68}, + {key: 'Dogs', value: 31.54}, + {key: 'Birds', value: 94.84}, + ], + }, + { + name: 'China', + data: [ + {key: 'Snakes', value: 0}, + {key: 'Dogs', value: 0}, + ], + }, +]; + +const Template: Story = () => { + return ; +}; + +export const MisMatchedData = Template.bind({}); diff --git a/packages/polaris-viz/src/components/SimpleBarChart/stories/playground/MisMatchedData.stories.tsx b/packages/polaris-viz/src/components/SimpleBarChart/stories/playground/MisMatchedData.stories.tsx new file mode 100644 index 000000000..9ea5757ac --- /dev/null +++ b/packages/polaris-viz/src/components/SimpleBarChart/stories/playground/MisMatchedData.stories.tsx @@ -0,0 +1,49 @@ +import type {Story} from '@storybook/react'; + +import {SimpleBarChart, SimpleBarChartProps} from '../../../../components'; +import {META} from '../meta'; + +export default { + ...META, + title: `${META.title}/Playground`, +}; + +const DATA = [ + { + name: 'Canada', + data: [ + {key: 'Mice', value: 13.28}, + {key: 'Dogs', value: 23.43}, + {key: 'Cats', value: 6.64}, + {key: 'Birds', value: 54.47}, + ], + }, + { + name: 'United States', + data: [ + {key: 'Lizards', value: 350.13}, + {key: 'Turtles', value: 223.43}, + {key: 'Mice', value: 15.38}, + {key: 'Snakes', value: 122.68}, + {key: 'Dogs', value: 31.54}, + {key: 'Birds', value: 94.84}, + ], + }, + { + name: 'China', + data: [ + {key: 'Snakes', value: 0}, + {key: 'Dogs', value: 0}, + ], + }, +]; + +const Template: Story = () => { + return ( +
+ +
+ ); +}; + +export const MisMatchedData = Template.bind({}); diff --git a/packages/polaris-viz/src/utilities/fillMissingDataPoints.ts b/packages/polaris-viz/src/utilities/fillMissingDataPoints.ts new file mode 100644 index 000000000..b56f2a26e --- /dev/null +++ b/packages/polaris-viz/src/utilities/fillMissingDataPoints.ts @@ -0,0 +1,28 @@ +import type {DataSeries} from '@shopify/polaris-viz-core'; + +export function fillMissingDataPoints(dataSeries: DataSeries[]) { + const allKeys = new Set(); + const dataValueMap: {[key: number]: {[key: string]: number | null}} = {}; + + for (const [index, {data}] of dataSeries.entries()) { + for (const {key, value} of data) { + allKeys.add(`${key}`); + + if (dataValueMap[index] == null) { + dataValueMap[index] = {}; + } + + dataValueMap[index][key] = value; + } + } + + return dataSeries.map(({name}, index) => { + const newData = [...allKeys].map((key) => { + return { + key, + value: dataValueMap[index][key] ?? null, + }; + }); + return {name, data: newData}; + }); +} diff --git a/packages/polaris-viz/src/utilities/tests/fillMissingDataPoints.test.ts b/packages/polaris-viz/src/utilities/tests/fillMissingDataPoints.test.ts new file mode 100644 index 000000000..8b1e94856 --- /dev/null +++ b/packages/polaris-viz/src/utilities/tests/fillMissingDataPoints.test.ts @@ -0,0 +1,146 @@ +import type {DataPoint, DataSeries} from '@shopify/polaris-viz-core'; + +import {fillMissingDataPoints} from '../fillMissingDataPoints'; + +describe('fillMissingDataPoints', () => { + it('returns original data when each array length matches', () => { + const mockData = [ + { + name: 'Canada', + data: [ + {key: 'Snakes', value: 122.68}, + {key: 'Dogs', value: 31.54}, + ], + }, + { + name: 'United States', + data: [ + {key: 'Snakes', value: 122.68}, + {key: 'Dogs', value: 31.54}, + ], + }, + { + name: 'China', + data: [ + {key: 'Snakes', value: 0}, + {key: 'Dogs', value: 0}, + ], + }, + ]; + const result = fillMissingDataPoints(mockData); + expect(result).toMatchObject(mockData); + }); + + it('fills data so all arrays contain all items', () => { + const mockData = [ + { + name: 'Canada', + data: [ + {key: 'Mice', value: 13.28}, + {key: 'Dogs', value: 23.43}, + {key: 'Cats', value: 6.64}, + {key: 'Birds', value: 54.47}, + ], + }, + { + name: 'United States', + data: [ + {key: 'Lizards', value: 350.13}, + {key: 'Turtles', value: 223.43}, + {key: 'Mice', value: 15.38}, + {key: 'Snakes', value: 122.68}, + {key: 'Dogs', value: 31.54}, + {key: 'Birds', value: 94.84}, + ], + }, + { + name: 'China', + data: [ + {key: 'Snakes', value: 0}, + {key: 'Dogs', value: 0}, + ], + }, + ]; + + const result = fillMissingDataPoints(mockData); + + expect(result).toMatchObject([ + { + name: 'Canada', + data: [ + {key: 'Mice', value: 13.28}, + {key: 'Dogs', value: 23.43}, + {key: 'Cats', value: 6.64}, + {key: 'Birds', value: 54.47}, + {key: 'Lizards', value: null}, + {key: 'Turtles', value: null}, + {key: 'Snakes', value: null}, + ], + }, + { + name: 'United States', + data: [ + {key: 'Mice', value: 15.38}, + {key: 'Dogs', value: 31.54}, + {key: 'Cats', value: null}, + {key: 'Birds', value: 94.84}, + {key: 'Lizards', value: 350.13}, + {key: 'Turtles', value: 223.43}, + {key: 'Snakes', value: 122.68}, + ], + }, + { + name: 'China', + data: [ + {key: 'Mice', value: null}, + {key: 'Dogs', value: 0}, + {key: 'Cats', value: null}, + {key: 'Birds', value: null}, + {key: 'Lizards', value: null}, + {key: 'Turtles', value: null}, + {key: 'Snakes', value: 0}, + ], + }, + ]); + }); + + it('loops through a large data set in less than 15ms', () => { + const data = getData(); + + const start = Date.now(); + fillMissingDataPoints(data); + const elapsed = Date.now() - start; + + expect(elapsed).toBeLessThan(15); + }); +}); + +const DATA_SERIES_COUNT = 10; +const DATA_POINTS_COUNT = 500; + +function getData() { + const largestArray: DataSeries[] = []; + + for (let i = 1; i <= DATA_SERIES_COUNT; i++) { + const dataItems: DataPoint[] = []; + const randomOffset = getRandomNumber(0, DATA_POINTS_COUNT / 6); + + for (let j = 1; j <= DATA_POINTS_COUNT - randomOffset; j++) { + const key = getRandomKey(); + const value = getRandomNumber(0, 100); + dataItems.push({key, value}); + } + + largestArray.push({name: `Array ${i}`, data: dataItems}); + } + + return largestArray; + + function getRandomNumber(min, max) { + return Math.random() * (max - min) + min; + } + + function getRandomKey() { + return Math.random().toString(36).substring(7); + } +}