diff --git a/packages/ez-core/src/utils/types.ts b/packages/ez-core/src/utils/types.ts index b2c76da..5e8f9d2 100644 --- a/packages/ez-core/src/utils/types.ts +++ b/packages/ez-core/src/utils/types.ts @@ -120,6 +120,7 @@ type CurveConfig = { export type AreaConfig = CurveConfig & { curve?: AreaCurve; fill?: string; + opacity?: number; }; export type LineConfig = CurveConfig & { diff --git a/packages/ez-dev/jest/snapshots/recipes/area/MultiAreaChart.spec.tsx.snap b/packages/ez-dev/jest/snapshots/recipes/area/MultiAreaChart.spec.tsx.snap new file mode 100644 index 0000000..2f5e8de --- /dev/null +++ b/packages/ez-dev/jest/snapshots/recipes/area/MultiAreaChart.spec.tsx.snap @@ -0,0 +1,655 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MultiAreaChart renders a multiarea chart 1`] = ` +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + + + + + 2 + + + + + + + 4 + + + + + + + 6 + + + + + + + 8 + + + + + + + 10 + + + + + + + 12 + + + + + + + 14 + + + + + + + + + + + 20 + + + + + + + 40 + + + + + + + 60 + + + + + + + 80 + + + + + + + 100 + + + + + + + 120 + + + + + + + 140 + + + + + + + 160 + + + + + + + 180 + + + + + + + 200 + + + + + + + 220 + + + + + + + 240 + + + + + + + 260 + + + + + + + 280 + + + + + + + 300 + + + + + + +
+
+
+
+
+ + yValue + +
+
+
+
+ + zValue + +
+
+
+
+
+
+
+`; diff --git a/packages/ez-dev/storybook/data.ts b/packages/ez-dev/storybook/data.ts index a51f0f3..4b07c4d 100644 --- a/packages/ez-dev/storybook/data.ts +++ b/packages/ez-dev/storybook/data.ts @@ -9,30 +9,35 @@ export const dimensions = { export const rawData: RawData = [ { value: 9, + value1: 20, name: 'Alpha', id: '1', v: 2, }, { value: 45, + value1: 20, name: 'Beta', id: '2', v: 5, }, { value: 29, + value1: 20, name: 'Gamma', id: '3', v: 10, }, { value: 30, + value1: 20, name: 'Delta', id: '4', v: 4, }, { value: 50, + value1: 20, name: 'Epsilon', id: '5', v: 8, diff --git a/packages/ez-react/src/components/Area.tsx b/packages/ez-react/src/components/Area.tsx new file mode 100644 index 0000000..7d942b6 --- /dev/null +++ b/packages/ez-react/src/components/Area.tsx @@ -0,0 +1,84 @@ +import React, { FC, SVGAttributes, useMemo } from 'react'; +import { AreaConfig, AreaData, MarkerConfig } from 'eazychart-core/src/types'; +import { Point } from '@/components/shapes/Point'; +import { Points } from '@/components/Points'; +import { useColorScale } from '@/components/scales/ColorScale'; +import { AreaPath } from './shapes/Area'; +import { LinePath } from './shapes/LinePath'; + +export interface AreaProps extends SVGAttributes { + xDomainKey: string; + yDomainKey: string; + area?: AreaConfig; + marker?: MarkerConfig; +} + +export const Area: FC = ({ + xDomainKey, + yDomainKey, + area = { + stroke: '#339999', + strokeWidth: 2, + curve: 'curveLinear', + }, + marker = { + hidden: true, + radius: 5, + color: '#FFF', + }, +}) => { + const { colorScale } = useColorScale(); + + const color = useMemo( + () => (colorScale.isDefined() ? colorScale.scale(yDomainKey) : area.fill), + [area.fill, colorScale, yDomainKey] + ); + return ( + { + const lineAreaData: AreaData = shapeData.map((d) => { + return { + x: d.x, + y0: chartDimensions.height, + y1: d.y, + }; + }); + return ( + + + + {!marker.hidden && + shapeData.map((shapeDatum) => { + 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..e693a84 --- /dev/null +++ b/packages/ez-react/src/components/StackedBars.tsx @@ -0,0 +1,87 @@ +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 { RectangleDatum } from 'eazychart-core/src/types'; + +export interface StackedBarsProps extends SVGAttributes { + xDomainKey: string; + yDomainKeys: string[]; +} + +export const StackedBars: FC = ({ + xDomainKey, + yDomainKeys, + ...rest +}) => { + const { data, dimensions, isRTL } = useChart(); + const { xScale, yScale } = useCartesianScales(); + const { colorScale } = useColorScale(); + + const scaledData = useMemo(() => { + return yDomainKeys.reduce((acc, yDomainKey, index) => { + if (index === 0) { + // @ts-ignore + acc[yDomainKey] = scaleRectangleData( + data, + xDomainKey, + yDomainKey, + xScale, + yScale, + colorScale, + dimensions, + isRTL + ); + return acc; + } else { + // @ts-ignore + acc[yDomainKey] = scaleRectangleData( + data, + xDomainKey, + yDomainKey, + xScale, + yScale, + colorScale, + dimensions, + isRTL + ).map((datum, id) => { + // @ts-ignore + const height0 = acc[yDomainKeys[index - 1]][id].height; + return datum.height > height0 + ? { ...datum, height: datum.height - height0 } + : datum; + }); + return acc; + } + }, {}); + }, [ + data, + yDomainKeys, + xDomainKey, + xScale, + yScale, + colorScale, + dimensions, + isRTL, + ]); + + return ( + + {yDomainKeys.map((yDomainKey) => { + // @ts-ignore + const shapeData = scaledData[yDomainKey] as RectangleDatum[]; + const color = colorScale.scale(yDomainKey); + return shapeData.map((shapeDatum: RectangleDatum, index: number) => { + return ( + + ); + }); + })} + + ); +}; diff --git a/packages/ez-react/src/components/shapes/Area.tsx b/packages/ez-react/src/components/shapes/Area.tsx index b92ada2..d92faa9 100644 --- a/packages/ez-react/src/components/shapes/Area.tsx +++ b/packages/ez-react/src/components/shapes/Area.tsx @@ -4,13 +4,13 @@ import { defaultColor, generateAreaPath } from 'eazychart-core/src'; import { useAnimation } from '../../lib/use-animation'; import { useChart } from '@/lib/use-chart'; -export interface AreaProps extends SVGAttributes { +export interface AreaPathProps extends SVGAttributes { shapeData?: AreaData; curve?: AreaCurve; beta?: number; } -export const Area: FC = ({ +export const AreaPath: FC = ({ shapeData = [], curve = 'curveLinear', beta, diff --git a/packages/ez-react/src/lib/useToggableDomainKey.ts b/packages/ez-react/src/lib/useToggableDomainKey.ts index 57e41dc..851e67b 100644 --- a/packages/ez-react/src/lib/useToggableDomainKey.ts +++ b/packages/ez-react/src/lib/useToggableDomainKey.ts @@ -1,11 +1,12 @@ import { useCallback, useMemo, useState } from 'react'; import { RawData } from 'eazychart-core/src/types'; -import { getDomainByKeys } from 'eazychart-core/src'; +import { getDomainByKeys } from 'eazychart-core'; export const useToggableDomainKey = (data: RawData, domainKeys: string[]) => { // Setup a state for the domain keys to make them toggable const [activeDomainKeys, setActiveDomainKeys] = useState(domainKeys); + // Toggle Y axis domain keys whenever a legend key is clicked const toggleDomainKey = useCallback( (key: string, isActive: boolean, _color: string) => { @@ -19,6 +20,7 @@ export const useToggableDomainKey = (data: RawData, domainKeys: string[]) => { }, [activeDomainKeys, setActiveDomainKeys] ); + // Re-scale the Y axis const activeDomain = useMemo( () => getDomainByKeys(activeDomainKeys, data), diff --git a/packages/ez-react/src/recipes/area/AreaChart.stories.tsx b/packages/ez-react/src/recipes/area/AreaChart.stories.tsx index 7ac0dd4..a898d6d 100644 --- a/packages/ez-react/src/recipes/area/AreaChart.stories.tsx +++ b/packages/ez-react/src/recipes/area/AreaChart.stories.tsx @@ -3,6 +3,7 @@ import { Meta, Story } from '@storybook/react'; import { AreaChart, AreaChartProps } from '@/recipes/area/AreaChart'; import { baseChartArgTypes, ChartWrapper } from '../../lib/storybook-utils'; import { colors, evolutionData } from 'eazychart-dev/storybook/data'; +import { MultiAreaChart, MultiAreaChartProps } from './MultiAreaChart'; const meta: Meta = { id: '2', @@ -24,11 +25,19 @@ const Template: Story = (args) => { ); }; +const MultiAreaTemplate: 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 = Template.bind({}); -Default.args = { +const defaultArguments = { area: { stroke: colors[0], strokeWidth: 2, @@ -52,3 +61,16 @@ Default.args = { }, data: evolutionData, }; + +Default.args = defaultArguments; + +export const MultiArea = MultiAreaTemplate.bind({}); + +MultiArea.args = { + ...defaultArguments, + yAxis: { + domainKeys: ['yValue', 'yValue1', 'yValue2'], + title: 'Temperature', + tickFormat: (d: number) => `${d}°`, + }, +}; diff --git a/packages/ez-react/src/recipes/area/AreaChart.tsx b/packages/ez-react/src/recipes/area/AreaChart.tsx index f0b7416..e0bba73 100644 --- a/packages/ez-react/src/recipes/area/AreaChart.tsx +++ b/packages/ez-react/src/recipes/area/AreaChart.tsx @@ -11,17 +11,13 @@ import { Dimensions, AreaConfig, MarkerConfig, - AreaData, } from 'eazychart-core/src/types'; import { TooltipProps, Tooltip } from '@/components/addons/tooltip/Tooltip'; import { Chart } from '@/components/Chart'; -import { Points } from '@/components/Points'; import { Axis } from '@/components/scales/Axis'; import { Grid } from '@/components/scales/grid/Grid'; -import { LinePath } from '@/components/shapes/LinePath'; -import { Point } from '@/components/shapes/Point'; -import { Area } from '@/components/shapes/Area'; import { CartesianScale } from '@/components/scales/CartesianScale'; +import { Area } from '@/components/Area'; export interface AreaChartProps extends SVGAttributes { data: RawData; @@ -107,50 +103,11 @@ export const AreaChart: FC = ({ }} > - { - const lineAreaData: AreaData = shapeData.map((d) => { - return { - x: d.x, - y0: chartDimensions.height, - y1: d.y, - }; - }); - return ( - - - - {!marker.hidden && - shapeData.map((shapeDatum) => { - return ( - - ); - })} - - ); - }, - }} + area={area} + marker={marker} /> { + data: RawData; + colors?: string[]; + area?: AreaConfig; + marker?: MarkerConfig; + animationOptions?: AnimationOptions; + padding?: ChartPadding; + grid?: GridConfig; + isRTL?: boolean; + xAxis?: AxisConfig; + yAxis?: AxisConfigMulti; + dimensions?: Partial; + scopedSlots?: { + TooltipComponent: FC; + LegendComponent: FC; + }; + onResize?: (dimensions: Dimensions) => void; +} + +export const MultiAreaChart: FC = ({ + data, + colors = ['#339999', '#993399', '#333399'], + area = { + stroke: '#339999', + strokeWidth: 2, + curve: 'curveLinear', + }, + marker = { + hidden: true, + radius: 5, + color: '#FFF', + }, + animationOptions = { + easing: 'easeBack', + duration: 400, + delay: 0, + }, + padding = { + left: 100, + bottom: 100, + right: 100, + top: 100, + }, + grid = { + directions: [Direction.HORIZONTAL, Direction.VERTICAL], + color: '#a8a8a8', + }, + xAxis = { + domainKey: 'xValue', + }, + yAxis = { + domainKeys: ['yValue', 'yValue1', 'yValue2'], + }, + isRTL = false, + dimensions = {}, + scopedSlots = { + TooltipComponent: Tooltip, + LegendComponent: Legend, + }, +}) => { + const { activeDomainKeys, activeDomain, toggleDomainKey } = + useToggableDomainKey(data, yAxis.domainKeys); + return ( + + + + + {activeDomainKeys.map((yDomainKey) => { + 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..8da296e 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'], + 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..8967f48 --- /dev/null +++ b/packages/ez-react/src/recipes/column/StackedColumnChart.tsx @@ -0,0 +1,122 @@ +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/src/recipes/line/LineErrorMarginChart.tsx b/packages/ez-react/src/recipes/line/LineErrorMarginChart.tsx index c940b71..9dd6f13 100644 --- a/packages/ez-react/src/recipes/line/LineErrorMarginChart.tsx +++ b/packages/ez-react/src/recipes/line/LineErrorMarginChart.tsx @@ -23,7 +23,7 @@ import { Axis } from '@/components/scales/Axis'; import { Grid } from '@/components/scales/grid/Grid'; import { LinePath } from '@/components/shapes/LinePath'; import { Point } from '@/components/shapes/Point'; -import { Area } from '@/components/shapes/Area'; +import { AreaPath } from '@/components/shapes/Area'; import { CartesianScale } from '@/components/scales/CartesianScale'; export interface LineErrorMarginChartProps extends SVGAttributes { @@ -153,7 +153,7 @@ export const LineErrorMarginChart: FC = ({ }); return ( - { + it('renders a multiarea chart', async () => { + let wrapper: RenderResult; + act(() => { + wrapper = render( + + ); + }); + + await waitFor(() => { + expect(wrapper.container.innerHTML).toMatchSnapshot(); + }); + }); +});