diff --git a/packages/ez-core/src/sample-data.ts b/packages/ez-core/src/sample-data.ts index 093a627..febde9f 100644 --- a/packages/ez-core/src/sample-data.ts +++ b/packages/ez-core/src/sample-data.ts @@ -23,18 +23,21 @@ export const rawData: RawData = [ id: '1', label: 'Alpha', value: 50, + value1: 65, amount: 10, }, { id: '2', label: 'Beta', value: 100, + value1: 120, amount: 20, }, { id: '3', label: 'Gamma', value: 75, + value1: 90, amount: 30, }, ]; diff --git a/packages/ez-dev/jest/snapshots/components/Chart.spec.tsx.snap b/packages/ez-dev/jest/snapshots/components/Chart.spec.tsx.snap index 53c324d..df633e6 100644 --- a/packages/ez-dev/jest/snapshots/components/Chart.spec.tsx.snap +++ b/packages/ez-dev/jest/snapshots/components/Chart.spec.tsx.snap @@ -44,7 +44,7 @@ exports[`Chart should provide the chart data to the children components 1`] = ` >
- [{"id":"1","label":"Alpha","value":50,"amount":10},{"id":"2","label":"Beta","value":100,"amount":20},{"id":"3","label":"Gamma","value":75,"amount":30}] + [{"id":"1","label":"Alpha","value":50,"value1":65,"amount":10},{"id":"2","label":"Beta","value":100,"value1":120,"amount":20},{"id":"3","label":"Gamma","value":75,"value1":90,"amount":30}]
diff --git a/packages/ez-dev/jest/snapshots/components/addons/Tooltip.spec.tsx.snap b/packages/ez-dev/jest/snapshots/components/addons/Tooltip.spec.tsx.snap index be39a80..9ef5be7 100644 --- a/packages/ez-dev/jest/snapshots/components/addons/Tooltip.spec.tsx.snap +++ b/packages/ez-dev/jest/snapshots/components/addons/Tooltip.spec.tsx.snap @@ -27,6 +27,14 @@ exports[`Tooltip renders the tooltip when a shape is hovered 1`] = ` 50 +
+
+ value1 : +
+
+ 65 +
+
amount : diff --git a/packages/ez-dev/jest/snapshots/recipes/column/StackedColumnChart.spec.tsx.snap b/packages/ez-dev/jest/snapshots/recipes/column/StackedColumnChart.spec.tsx.snap new file mode 100644 index 0000000..a67b105 --- /dev/null +++ b/packages/ez-dev/jest/snapshots/recipes/column/StackedColumnChart.spec.tsx.snap @@ -0,0 +1,509 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`StackedColumnChart renders a stacked column chart 1`] = ` +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Alpha + + + + + + + Beta + + + + + + + Gamma + + + + + + + + + + + 50 + + + + + + + 55 + + + + + + + 60 + + + + + + + 65 + + + + + + + 70 + + + + + + + 75 + + + + + + + 80 + + + + + + + 85 + + + + + + + 90 + + + + + + + 95 + + + + + + + 100 + + + + + + + 105 + + + + + + + 110 + + + + + + + 115 + + + + + + + 120 + + + + + + +
+
+
+
+
+ + value + +
+
+
+
+ + value1 + +
+
+
+
+
+
+
+`; diff --git a/packages/ez-dev/storybook/data.ts b/packages/ez-dev/storybook/data.ts index a51f0f3..789b21e 100644 --- a/packages/ez-dev/storybook/data.ts +++ b/packages/ez-dev/storybook/data.ts @@ -1,5 +1,8 @@ -import { AnimationOptions, ChartPadding, RawData } from 'eazychart-core/src/types'; - +import { + AnimationOptions, + ChartPadding, + RawData, +} from 'eazychart-core/src/types'; export const dimensions = { width: 800, @@ -8,31 +11,41 @@ export const dimensions = { export const rawData: RawData = [ { - value: 9, + value: 5, + value1: 9, + value2: 15, name: 'Alpha', id: '1', v: 2, }, { - value: 45, + value: 20, + value1: 45, + value2: 56, name: 'Beta', id: '2', v: 5, }, { - value: 29, + value: 15, + value1: 29, + value2: 33, name: 'Gamma', id: '3', v: 10, }, { - value: 30, + value: 18, + value1: 30, + value2: 40, name: 'Delta', id: '4', v: 4, }, { - value: 50, + value: 30, + value1: 50, + value2: 70, name: 'Epsilon', id: '5', v: 8, diff --git a/packages/ez-react/src/components/Bars.tsx b/packages/ez-react/src/components/Bars.tsx index 72abebf..d248629 100644 --- a/packages/ez-react/src/components/Bars.tsx +++ b/packages/ez-react/src/components/Bars.tsx @@ -1,16 +1,41 @@ import React, { FC, SVGAttributes, useMemo } from 'react'; -import { scaleRectangleData } from 'eazychart-core/src'; +import { ScaleOrdinal, scaleRectangleData } from 'eazychart-core/src'; import { Bar } from '@/components/shapes/Bar'; import { useChart } from '@/lib/use-chart'; import { useCartesianScales } from '@/components/scales/CartesianScale'; import { useColorScale } from './scales/ColorScale'; +import { + RectangleDatum, + ScaleLinearOrBand, + Dimensions, +} from 'eazychart-core/src/types'; export interface BarsProps extends SVGAttributes { xDomainKey: string; yDomainKey: string; + scopedSlots?: { + default: ({ + shapeData, + scales, + dimensions, + }: { + shapeData: RectangleDatum[]; + scales: { + xScale: ScaleLinearOrBand; + yScale: ScaleLinearOrBand; + colorScale: ScaleOrdinal; + }; + dimensions: Dimensions; + }) => JSX.Element[]; + }; } -export const Bars: FC = ({ xDomainKey, yDomainKey, ...rest }) => { +export const Bars: FC = ({ + xDomainKey, + yDomainKey, + scopedSlots, + ...rest +}) => { const { data, dimensions, isRTL } = useChart(); const { xScale, yScale } = useCartesianScales(); const { colorScale } = useColorScale(); @@ -39,9 +64,15 @@ export const Bars: FC = ({ xDomainKey, yDomainKey, ...rest }) => { return ( - {scaledData.map((rectDatum) => { - return ; - })} + {scopedSlots && scopedSlots.default + ? scopedSlots.default({ + shapeData: scaledData, + scales: { xScale, yScale, colorScale }, + dimensions, + }) + : scaledData.map((rectDatum) => { + return ; + })} ); }; diff --git a/packages/ez-react/src/components/StackedBars.tsx b/packages/ez-react/src/components/StackedBars.tsx new file mode 100644 index 0000000..3453d97 --- /dev/null +++ b/packages/ez-react/src/components/StackedBars.tsx @@ -0,0 +1,100 @@ +import React, { FC, SVGAttributes, useMemo } from 'react'; +import { scaleRectangleData } from 'eazychart-core/src'; +import { Bar } from '@/components/shapes/Bar'; +import { useChart } from '@/lib/use-chart'; +import { useCartesianScales } from '@/components/scales/CartesianScale'; +import { useColorScale } from './scales/ColorScale'; +import { Direction, RectangleDatum } from 'eazychart-core/src/types'; + +export interface StackedBarsProps extends SVGAttributes { + domainKey: string; + stackDomainKeys: string[]; + direction: Direction.HORIZONTAL | Direction.VERTICAL; +} + +export const StackedBars: FC = ({ + domainKey, + stackDomainKeys, + direction = Direction.VERTICAL, + ...rest +}) => { + const { data, dimensions, isRTL } = useChart(); + const { xScale, yScale } = useCartesianScales(); + const { colorScale } = useColorScale(); + + const scaledDataDict = useMemo(() => { + return stackDomainKeys.reduce( + (acc: { [key: string]: RectangleDatum[] }, yDomainKey) => { + acc[yDomainKey] = scaleRectangleData( + data, + domainKey, + yDomainKey, + xScale, + yScale, + colorScale, + dimensions, + isRTL + ); + return acc; + }, + {} + ); + }, [ + data, + stackDomainKeys, + domainKey, + xScale, + yScale, + colorScale, + dimensions, + isRTL, + ]); + + return ( + + {data.map((_datum, idx) => { + return ( + // The Domain keys still needs to be sorted. + // We create a bar for every data row + // Each bar is a stack bar where every element is a domain key. + + {stackDomainKeys.map((yDomainKey, domainIdx) => { + const color = colorScale.scale(yDomainKey); + const scaledData = scaledDataDict[yDomainKey][idx]; + // The first domain key will not be affected. + const previousRectWidth = + domainIdx !== 0 + ? scaledDataDict[stackDomainKeys[domainIdx - 1]][idx].width + : 0; + const previousRectHeight = + domainIdx !== 0 + ? scaledDataDict[stackDomainKeys[domainIdx - 1]][idx].height + : 0; + const shapeDatum = + // The height or the width of the current bar will be computed depending to the orientaion + // the height will be currentDKHeight - previousDKHeight (same for the width) + { + ...scaledData, + width: + direction === Direction.HORIZONTAL + ? scaledData.width - previousRectWidth + : scaledData.width, + height: + direction === Direction.VERTICAL + ? scaledData.height - previousRectHeight + : scaledData.height, + }; + + return ( + + ); + })} + + ); + })} + + ); +}; diff --git a/packages/ez-react/src/recipes/column/ColumnChart.stories.tsx b/packages/ez-react/src/recipes/column/ColumnChart.stories.tsx index 3cc8395..047b004 100644 --- a/packages/ez-react/src/recipes/column/ColumnChart.stories.tsx +++ b/packages/ez-react/src/recipes/column/ColumnChart.stories.tsx @@ -7,6 +7,10 @@ import { } from '@/recipes/column/LineColumnChart'; import { baseChartArgTypes, ChartWrapper } from '@/lib/storybook-utils'; import { colors, rawData } from 'eazychart-dev/storybook/data'; +import { + StackedColumnChart, + StackedColumnChartProps, +} from './StackedColumnChart'; const meta: Meta = { id: '4', @@ -36,6 +40,14 @@ const LineColumnTemplate: Story = (args) => { ); }; +const StackedColumnTemplate: Story = (args) => { + return ( + + + + ); +}; + // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test // https://storybook.js.org/docs/react/workflows/unit-testing export const Default = DefaultTemplate.bind({}); @@ -78,3 +90,16 @@ const lineColumnArguments = { }; LineColumn.args = lineColumnArguments; + +export const StackedColumn = StackedColumnTemplate.bind({}); + +const StackedColumnArguments = { + ...defaultArguments, + yAxis: { + domainKeys: ['value', 'value1', 'value2'], + title: 'Temperature', + tickFormat: (d: number) => `${d}°`, + }, +}; + +StackedColumn.args = StackedColumnArguments; diff --git a/packages/ez-react/src/recipes/column/StackedColumnChart.tsx b/packages/ez-react/src/recipes/column/StackedColumnChart.tsx new file mode 100644 index 0000000..8af9448 --- /dev/null +++ b/packages/ez-react/src/recipes/column/StackedColumnChart.tsx @@ -0,0 +1,123 @@ +import { Legend, LegendProps } from '@/components/addons/legend/Legend'; +import { Tooltip, TooltipProps } from '@/components/addons/tooltip/Tooltip'; +import { Chart } from '@/components/Chart'; +import { Axis } from '@/components/scales/Axis'; +import { CartesianScale } from '@/components/scales/CartesianScale'; +import { ColorScale } from '@/components/scales/ColorScale'; +import { StackedBars } from '@/components/StackedBars'; +import { useToggableDomainKey } from '@/lib/useToggableDomainKey'; +import { ScaleBand, ScaleLinear } from 'eazychart-core/src/scales'; +import { + RawData, + AnimationOptions, + ChartPadding, + AxisConfig, + Position, + Dimensions, + Direction, + AxisConfigMulti, +} from 'eazychart-core/src/types'; +import React, { FC, SVGAttributes } from 'react'; + +export interface StackedColumnChartProps extends SVGAttributes { + data: RawData; + colors?: string[]; + animationOptions?: AnimationOptions; + padding?: ChartPadding; + isRTL?: boolean; + xAxis?: AxisConfig; + yAxis?: AxisConfigMulti; + dimensions?: Partial; + scopedSlots?: { + LegendComponent: React.FC; + TooltipComponent: React.FC; + }; +} + +export const StackedColumnChart: FC = ({ + data, + colors = ['#339999', '#993399', '#333399'], + animationOptions = { + easing: 'easeBack', + duration: 400, + delay: 0, + }, + padding = { + left: 150, + bottom: 100, + right: 150, + top: 100, + }, + xAxis = { + domainKey: 'name', + position: Position.BOTTOM, + }, + yAxis = { + domainKeys: ['value', 'value1'], + position: Position.LEFT, + }, + isRTL = false, + dimensions = {}, + scopedSlots = { + LegendComponent: Legend, + TooltipComponent: Tooltip, + }, +}) => { + const { activeDomainKeys, activeDomain, toggleDomainKey } = + useToggableDomainKey(data, yAxis.domainKeys); + + return ( + + + + + + + + + + ); +}; diff --git a/packages/ez-react/tests/unit/recipes/column/StackedColumnChart.spec.tsx b/packages/ez-react/tests/unit/recipes/column/StackedColumnChart.spec.tsx new file mode 100644 index 0000000..76a0bf5 --- /dev/null +++ b/packages/ez-react/tests/unit/recipes/column/StackedColumnChart.spec.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { dimensions, rawData } from 'eazychart-core/src/sample-data'; +import { act, render, RenderResult, waitFor } from '@testing-library/react'; +import 'tests/mocks/ResizeObserver'; +import { StackedColumnChart } from '@/recipes/column/StackedColumnChart'; + +describe('StackedColumnChart', () => { + it('renders a stacked column chart', async () => { + let wrapper: RenderResult; + act(() => { + wrapper = render( + + ); + }); + + await waitFor(() => { + expect(wrapper.container.innerHTML).toMatchSnapshot(); + }); + }); +});