From f7868885ab5c1702dd852d6b6fade08c5bc3ec66 Mon Sep 17 00:00:00 2001 From: Alexandre Balon-Perin Date: Fri, 28 Jun 2024 23:29:13 +0900 Subject: [PATCH 1/2] Feature: enable multiple X-Axis and Y-Axis Implemented for AreaCharts, LineCharts & BarCharts --- .../chart-elements/AreaChart/AreaChart.tsx | 367 ++++++------------ .../chart-elements/BarChart/BarChart.tsx | 339 +++++++--------- .../chart-elements/LineChart/LineChart.tsx | 306 ++++++--------- src/components/chart-elements/common/Area.tsx | 125 ++++++ src/components/chart-elements/common/Bar.tsx | 103 +++++ .../chart-elements/common/BaseChartProps.tsx | 25 +- .../chart-elements/common/ChartLegend.tsx | 3 +- .../chart-elements/common/ChartTooltip.tsx | 102 ++--- src/components/chart-elements/common/Line.tsx | 121 ++++++ .../chart-elements/common/LinearGradient.tsx | 43 ++ .../chart-elements/common/XAxis.tsx | 110 ++++++ .../chart-elements/common/YAxis.tsx | 104 +++++ src/components/chart-elements/common/types.ts | 6 + src/components/chart-elements/common/utils.ts | 3 +- .../chart-elements/AreaChart.stories.tsx | 67 +++- .../chart-elements/BarChart.stories.tsx | 40 ++ .../chart-elements/LineChart.stories.tsx | 58 ++- .../chart-elements/helpers/testData.tsx | 242 ++++++++++++ 18 files changed, 1468 insertions(+), 696 deletions(-) create mode 100644 src/components/chart-elements/common/Area.tsx create mode 100644 src/components/chart-elements/common/Bar.tsx create mode 100644 src/components/chart-elements/common/Line.tsx create mode 100644 src/components/chart-elements/common/LinearGradient.tsx create mode 100644 src/components/chart-elements/common/XAxis.tsx create mode 100644 src/components/chart-elements/common/YAxis.tsx create mode 100644 src/components/chart-elements/common/types.ts diff --git a/src/components/chart-elements/AreaChart/AreaChart.tsx b/src/components/chart-elements/AreaChart/AreaChart.tsx index 57426b026..13d8c673c 100644 --- a/src/components/chart-elements/AreaChart/AreaChart.tsx +++ b/src/components/chart-elements/AreaChart/AreaChart.tsx @@ -1,20 +1,17 @@ "use client"; -import React, { Fragment, useState } from "react"; +import React, { useState } from "react"; import { - Area, AreaChart as ReChartsAreaChart, CartesianGrid, - Dot, Legend, - Line, ResponsiveContainer, Tooltip, - XAxis, - YAxis, - Label, } from "recharts"; import { AxisDomain } from "recharts/types/util/types"; +import { renderArea } from "../common/Area"; +import { renderHorizontalXAxis } from "../common/XAxis"; +import { renderHorizontalYAxis } from "../common/YAxis"; import BaseChartProps from "../common/BaseChartProps"; import ChartLegend from "../common/ChartLegend"; import ChartTooltip from "../common/ChartTooltip"; @@ -25,15 +22,9 @@ import { hasOnlyOneValueForThisKey, } from "../common/utils"; -import { - BaseColors, - colorPalette, - defaultValueFormatter, - getColorClassNames, - themeColorRange, - tremorTwMerge, -} from "lib"; +import { BaseColors, defaultValueFormatter, themeColorRange, tremorTwMerge } from "lib"; import { CurveType } from "../../../lib/inputTypes"; +import { LinearGradient } from "../common/LinearGradient"; export interface AreaChartProps extends BaseChartProps { stack?: boolean; @@ -81,6 +72,8 @@ const AreaChart = React.forwardRef((props, ref) tickGap = 5, xAxisLabel, yAxisLabel, + xAxisConfigs = [{ orientation: "bottom" }], + yAxisConfigs = [{ orientation: "left" }], ...other } = props; const CustomTooltip = customTooltip; @@ -88,9 +81,12 @@ const AreaChart = React.forwardRef((props, ref) const [legendHeight, setLegendHeight] = useState(60); const [activeDot, setActiveDot] = useState(undefined); const [activeLegend, setActiveLegend] = useState(undefined); + const categoryColors = constructCategoryColors(categories, colors); - const yAxisDomain = getYAxisDomain(autoMinValue, minValue, maxValue); + const isSeriesData: boolean = data?.[0]?.data && data[0].data !== undefined; + const seriesData = isSeriesData ? data : [{ data }]; + const hasOnValueChange = !!onValueChange; function onDotClick(itemData: any, event: React.MouseEvent) { @@ -137,12 +133,25 @@ const AreaChart = React.forwardRef((props, ref) } setActiveDot(undefined); } + + const yAxisConfigsValueFormatter = yAxisConfigs.flatMap((yAxisConfig) => { + return ( + yAxisConfig.categories?.map((c) => [c, yAxisConfig.valueFormatter ?? valueFormatter]) ?? [] + ); + }); + const yAxisValueFormatters = Object.fromEntries( + yAxisConfigsValueFormatter.length > 0 + ? yAxisConfigsValueFormatter + : categories.map((c, idx) => [c, yAxisConfigs[idx]?.valueFormatter ?? valueFormatter]), + ); + return (
{data?.length ? ( { @@ -173,73 +182,45 @@ const AreaChart = React.forwardRef((props, ref) vertical={false} /> ) : null} - - {xAxisLabel && ( - - )} - - - {yAxisLabel && ( - - )} - + {xAxisConfigs.map((xAxisConfig, idx) => { + const d = seriesData + .filter((d) => xAxisConfig.series?.includes(d.name)) + .flatMap((d) => d.data); + return renderHorizontalXAxis({ + key: `x-axis-${idx}`, + showAxis: showXAxis, + data: d.length === 0 ? seriesData[idx].data : d, + rotateLabel: rotateLabelX, + label: xAxisLabel, + orientation: xAxisConfig.orientation, + xAxisId: idx, + padding: { left: paddingValue, right: paddingValue }, + intervalType, + startEndOnly, + minTickGap: tickGap, + dataKey: index, + xAxisCount: xAxisConfigs.length, + }); + })} + {yAxisConfigs.map((yAxisConfig, idx) => { + const domain: AxisDomain = getYAxisDomain( + yAxisConfig.autoMinValue ?? autoMinValue, + yAxisConfig.minValue ?? minValue, + yAxisConfig.maxValue ?? maxValue, + ); + return renderHorizontalYAxis({ + key: `y-axis-${idx}`, + width: yAxisWidth, + showAxis: showYAxis, + domain, + valueFormatter: yAxisConfig.valueFormatter ?? valueFormatter, + allowDecimals, + orientation: yAxisConfig.orientation, + yAxisId: idx, + label: yAxisLabel, + xAxisCount: xAxisConfigs.length, + }); + })} ((props, ref) active={active} payload={payload} label={label} - valueFormatter={valueFormatter} + valueFormatters={yAxisValueFormatters} categoryColors={categoryColors} + index={index} + xAxisCount={xAxisConfigs.length} /> ) ) : ( @@ -291,168 +274,63 @@ const AreaChart = React.forwardRef((props, ref) ) : null} {categories.map((category) => { return ( - - {showGradient ? ( - - - - - ) : ( - - - - )} + + ); })} - {categories.map((category) => ( - { - const { cx, cy, stroke, strokeLinecap, strokeLinejoin, strokeWidth, dataKey } = - props; - return ( - onDotClick(props, event)} - /> - ); - }} - dot={(props: any) => { - const { - stroke, - strokeLinecap, - strokeLinejoin, - strokeWidth, - cx, - cy, - dataKey, - index, - } = props; - - if ( - (hasOnlyOneValueForThisKey(data, category) && - !(activeDot || (activeLegend && activeLegend !== category))) || - (activeDot?.index === index && activeDot?.dataKey === category) - ) { - return ( - - ); - } - return ; - }} - key={category} - name={category} - type={curveType} - dataKey={category} - stroke="" - fill={`url(#${categoryColors.get(category)})`} - strokeWidth={2} - strokeLinejoin="round" - strokeLinecap="round" - isAnimationActive={showAnimation} - animationDuration={animationDuration} - stackId={stack ? "a" : undefined} - connectNulls={connectNulls} - /> - ))} - {onValueChange - ? categories.map((category) => ( - { - event.stopPropagation(); - const { name } = props; - onCategoryClick(name); - }} - /> - )) - : null} + {seriesData.map((series, seriesIndex) => { + return ( + <> + {categories.map((category, idx) => { + const yAxisId = + yAxisConfigs.length === 1 + ? 0 + : yAxisConfigs.findIndex((yAxisConfig) => + yAxisConfig.categories?.includes(category), + ); + const xAxisId = + xAxisConfigs.length === 1 + ? 0 + : xAxisConfigs.findIndex((xAxisConfig) => + xAxisConfig.series?.includes(series.name), + ); + const dataRange = series.data + .filter((d: any) => d[category]) + .map((d: any) => ({ + [index]: d[index], + [category]: d[category], + })); + return renderArea({ + key: ["area", series.name, category].filter(Boolean).join("-"), + name: [series.name, category].filter(Boolean).join("-"), + seriesIndex, + category, + color: categoryColors.get(category), + activeDot, + activeLegend, + onValueChange, + onDotClick, + curveType, + stackId: stack ? "a" : undefined, + connectNulls, + yAxisId: yAxisId === -1 ? idx : yAxisId, + xAxisId: xAxisId === -1 ? seriesIndex : xAxisId, + data: dataRange, + showAnimation, + animationDuration, + xAxisCount: xAxisConfigs.length, + }); + })} + + ); + })} ) : ( @@ -461,7 +339,6 @@ const AreaChart = React.forwardRef((props, ref)
); }); - AreaChart.displayName = "AreaChart"; export default AreaChart; diff --git a/src/components/chart-elements/BarChart/BarChart.tsx b/src/components/chart-elements/BarChart/BarChart.tsx index c87df34bd..f4dab96ac 100644 --- a/src/components/chart-elements/BarChart/BarChart.tsx +++ b/src/components/chart-elements/BarChart/BarChart.tsx @@ -1,17 +1,13 @@ "use client"; -import { colorPalette, getColorClassNames, tremorTwMerge } from "lib"; +import { tremorTwMerge } from "lib"; import React, { useState } from "react"; import { - Bar, BarChart as ReChartsBarChart, CartesianGrid, Legend, ResponsiveContainer, Tooltip, - XAxis, - YAxis, - Label, } from "recharts"; import BaseChartProps from "../common/BaseChartProps"; @@ -22,40 +18,9 @@ import { constructCategoryColors, deepEqual, getYAxisDomain } from "../common/ut import { BaseColors, defaultValueFormatter, themeColorRange } from "lib"; import { AxisDomain } from "recharts/types/util/types"; - -const renderShape = ( - props: any, - activeBar: any | undefined, - activeLegend: string | undefined, - layout: string, -) => { - const { fillOpacity, name, payload, value } = props; - let { x, width, y, height } = props; - - if (layout === "horizontal" && height < 0) { - y += height; - height = Math.abs(height); // height must be a positive number - } else if (layout === "vertical" && width < 0) { - x += width; - width = Math.abs(width); // width must be a positive number - } - - return ( - - ); -}; +import { renderHorizontalXAxis, renderVerticalXAxis } from "../common/XAxis"; +import { renderHorizontalYAxis, renderVerticalYAxis } from "../common/YAxis"; +import { renderBar } from "../common/Bar"; export interface BarChartProps extends BaseChartProps { layout?: "vertical" | "horizontal"; @@ -98,6 +63,8 @@ const BarChart = React.forwardRef((props, ref) => xAxisLabel, yAxisLabel, className, + xAxisConfigs = [{ orientation: "bottom" }], + yAxisConfigs = [{ orientation: "left" }], ...other } = props; const CustomTooltip = customTooltip; @@ -108,6 +75,9 @@ const BarChart = React.forwardRef((props, ref) => const [activeLegend, setActiveLegend] = useState(undefined); const hasOnValueChange = !!onValueChange; + const isSeriesData: boolean = data?.[0]?.data && data[0].data !== undefined; + const seriesData = isSeriesData ? data : [{ data }]; + function onBarClick(data: any, idx: number, event: React.MouseEvent) { event.stopPropagation(); if (!onValueChange) return; @@ -143,7 +113,17 @@ const BarChart = React.forwardRef((props, ref) => } setActiveBar(undefined); } - const yAxisDomain = getYAxisDomain(autoMinValue, minValue, maxValue); + + const yAxisConfigsValueFormatter = yAxisConfigs.flatMap((yAxisConfig) => { + return ( + yAxisConfig.categories?.map((c) => [c, yAxisConfig.valueFormatter ?? valueFormatter]) ?? [] + ); + }); + const yAxisValueFormatters = Object.fromEntries( + yAxisConfigsValueFormatter.length > 0 + ? yAxisConfigsValueFormatter + : categories.map((c, idx) => [c, yAxisConfigs[idx]?.valueFormatter ?? valueFormatter]), + ); return (
@@ -151,7 +131,8 @@ const BarChart = React.forwardRef((props, ref) => {data?.length ? ( ((props, ref) => /> ) : null} - {layout !== "vertical" ? ( - - {xAxisLabel && ( - - )} - - ) : ( - - {xAxisLabel && ( - - )} - - )} - {layout !== "vertical" ? ( - `${(value * 100).toString()} %` : valueFormatter - } - allowDecimals={allowDecimals} - > - {yAxisLabel && ( - - )} - - ) : ( - - {yAxisLabel && ( - - )} - - )} + {xAxisConfigs.map((xAxisConfig, idx) => { + const d = seriesData + .filter((d) => xAxisConfig.series?.includes(d.name)) + .flatMap((d) => d.data); + const commonProps = { + key: `x-axis-${idx}`, + showAxis: showXAxis, + data: d.length === 0 ? seriesData[idx].data : d, + rotateLabel: rotateLabelX, + label: xAxisLabel, + orientation: xAxisConfig.orientation, + xAxisId: idx, + layout, + padding: { left: paddingValue, right: paddingValue }, + xAxisCount: xAxisConfigs.length, + }; + return layout && layout === "vertical" + ? renderVerticalXAxis({ + ...commonProps, + relative, + valueFormatter, + width: yAxisWidth, + }) + : renderHorizontalXAxis({ + ...commonProps, + intervalType, + startEndOnly, + minTickGap: tickGap, + dataKey: index, + }); + })} + {yAxisConfigs.map((yAxisConfig, idx) => { + const domain: AxisDomain = getYAxisDomain( + yAxisConfig.autoMinValue ?? autoMinValue, + yAxisConfig.minValue ?? minValue, + yAxisConfig.maxValue ?? maxValue, + ); + const commonProps = { + key: `y-axis-${idx}`, + showAxis: showYAxis, + allowDecimals, + orientation: yAxisConfig.orientation, + yAxisId: idx, + label: yAxisLabel, + layout, + xAxisCount: xAxisConfigs.length, + }; + return layout && layout === "vertical" + ? renderVerticalYAxis({ + ...commonProps, + intervalType, + startEndOnly, + data, + minTickGap: tickGap, + dataKey: index, + }) + : renderHorizontalYAxis({ + ...commonProps, + width: yAxisWidth, + domain, + valueFormatter: yAxisConfig.valueFormatter ?? valueFormatter, + relative, + }); + })} ((props, ref) => active={active} payload={payload} label={label} - valueFormatter={valueFormatter} + valueFormatters={yAxisValueFormatters} categoryColors={categoryColors} + index={index} + xAxisCount={xAxisConfigs.length} /> ) ) : ( @@ -376,27 +281,51 @@ const BarChart = React.forwardRef((props, ref) => } /> ) : null} - {categories.map((category) => ( - renderShape(props, activeBar, activeLegend, layout)} - onClick={onBarClick} - /> - ))} + {seriesData.map((series, seriesIndex) => { + return ( + <> + {categories.map((category, idx) => { + const yAxisId = + yAxisConfigs.length === 1 + ? 0 + : yAxisConfigs.findIndex((yAxisConfig) => + yAxisConfig.categories?.includes(category), + ); + const xAxisId = + xAxisConfigs.length === 1 + ? 0 + : xAxisConfigs.findIndex((xAxisConfig) => + xAxisConfig.series?.includes(series.name), + ); + const dataRange = series.data + .filter((d: any) => d[category]) + .map((d: any) => ({ + [index]: d[index], + [category]: d[category], + })); + return renderBar({ + key: ["bar", series.name, category].filter(Boolean).join("-"), + name: [series.name, category].filter(Boolean).join("-"), + seriesIndex, + category, + color: categoryColors.get(category), + activeBar, + activeLegend, + onValueChange, + onBarClick, + stackId: stack || relative ? "a" : undefined, + yAxisId: yAxisId === -1 ? idx : yAxisId, + xAxisId: xAxisId === -1 ? seriesIndex : xAxisId, + data: dataRange, + showAnimation, + animationDuration, + xAxisCount: xAxisConfigs.length, + layout, + }); + })} + + ); + })} ) : ( diff --git a/src/components/chart-elements/LineChart/LineChart.tsx b/src/components/chart-elements/LineChart/LineChart.tsx index ded14f8df..34ee3d82f 100644 --- a/src/components/chart-elements/LineChart/LineChart.tsx +++ b/src/components/chart-elements/LineChart/LineChart.tsx @@ -1,16 +1,11 @@ "use client"; -import React, { Fragment, useState } from "react"; +import React, { useState } from "react"; import { CartesianGrid, - Dot, Legend, - Line, LineChart as ReChartsLineChart, ResponsiveContainer, Tooltip, - XAxis, - YAxis, - Label, } from "recharts"; import { AxisDomain } from "recharts/types/util/types"; @@ -24,15 +19,11 @@ import { hasOnlyOneValueForThisKey, } from "../common/utils"; -import { - BaseColors, - colorPalette, - defaultValueFormatter, - getColorClassNames, - themeColorRange, - tremorTwMerge, -} from "lib"; +import { BaseColors, defaultValueFormatter, themeColorRange, tremorTwMerge } from "lib"; import { CurveType } from "../../../lib/inputTypes"; +import { renderHorizontalXAxis } from "../common/XAxis"; +import { renderLine } from "../common/Line"; +import { renderHorizontalYAxis } from "../common/YAxis"; export interface LineChartProps extends BaseChartProps { curveType?: CurveType; @@ -76,6 +67,8 @@ const LineChart = React.forwardRef((props, ref) tickGap = 5, xAxisLabel, yAxisLabel, + xAxisConfigs = [{ orientation: "bottom" }], + yAxisConfigs = [{ orientation: "left" }], ...other } = props; const CustomTooltip = customTooltip; @@ -85,7 +78,9 @@ const LineChart = React.forwardRef((props, ref) const [activeLegend, setActiveLegend] = useState(undefined); const categoryColors = constructCategoryColors(categories, colors); - const yAxisDomain = getYAxisDomain(autoMinValue, minValue, maxValue); + const isSeriesData: boolean = data?.[0]?.data && data[0].data !== undefined; + const seriesData = isSeriesData ? data : [{ data }]; + const hasOnValueChange = !!onValueChange; function onDotClick(itemData: any, event: React.MouseEvent) { @@ -133,12 +128,24 @@ const LineChart = React.forwardRef((props, ref) setActiveDot(undefined); } + const yAxisConfigsValueFormatter = yAxisConfigs.flatMap((yAxisConfig) => { + return ( + yAxisConfig.categories?.map((c) => [c, yAxisConfig.valueFormatter ?? valueFormatter]) ?? [] + ); + }); + const yAxisValueFormatters = Object.fromEntries( + yAxisConfigsValueFormatter.length > 0 + ? yAxisConfigsValueFormatter + : categories.map((c, idx) => [c, yAxisConfigs[idx]?.valueFormatter ?? valueFormatter]), + ); + return (
{data?.length ? ( { @@ -169,73 +176,45 @@ const LineChart = React.forwardRef((props, ref) vertical={false} /> ) : null} - - {xAxisLabel && ( - - )} - - - {yAxisLabel && ( - - )} - + {xAxisConfigs.map((xAxisConfig, idx) => { + const d = seriesData + .filter((d) => xAxisConfig.series?.includes(d.name)) + .flatMap((d) => d.data); + return renderHorizontalXAxis({ + key: `x-axis-${idx}`, + padding: { left: paddingValue, right: paddingValue }, + showAxis: showXAxis, + dataKey: index, + startEndOnly, + data: d.length === 0 ? seriesData[idx].data : d, + intervalType, + rotateLabel: rotateLabelX, + label: xAxisLabel, + orientation: xAxisConfig.orientation, + xAxisId: idx, + minTickGap: tickGap, + xAxisCount: xAxisConfigs.length, + }); + })} + {yAxisConfigs.map((yAxisConfig, idx) => { + const domain: AxisDomain = getYAxisDomain( + yAxisConfig.autoMinValue ?? autoMinValue, + yAxisConfig.minValue ?? minValue, + yAxisConfig.maxValue ?? maxValue, + ); + return renderHorizontalYAxis({ + key: `y-axis-${idx}`, + width: yAxisWidth, + showAxis: showYAxis, + domain, + valueFormatter: yAxisConfig.valueFormatter ?? valueFormatter, + allowDecimals, + orientation: yAxisConfig.orientation, + yAxisId: idx, + label: yAxisLabel, + xAxisCount: xAxisConfigs.length, + }); + })} ((props, ref) active={active} payload={payload} label={label} - valueFormatter={valueFormatter} + valueFormatters={yAxisValueFormatters} categoryColors={categoryColors} + index={index} + xAxisCount={xAxisConfigs.length} /> ) ) : ( @@ -286,117 +267,51 @@ const LineChart = React.forwardRef((props, ref) } /> ) : null} - {categories.map((category) => ( - { - const { cx, cy, stroke, strokeLinecap, strokeLinejoin, strokeWidth, dataKey } = - props; - return ( - onDotClick(props, event)} - /> - ); - }} - dot={(props: any) => { - const { - stroke, - strokeLinecap, - strokeLinejoin, - strokeWidth, - cx, - cy, - dataKey, - index, - } = props; - - if ( - (hasOnlyOneValueForThisKey(data, category) && - !(activeDot || (activeLegend && activeLegend !== category))) || - (activeDot?.index === index && activeDot?.dataKey === category) - ) { - return ( - - ); - } - return ; - }} - key={category} - name={category} - type={curveType} - dataKey={category} - stroke="" - strokeWidth={2} - strokeLinejoin="round" - strokeLinecap="round" - isAnimationActive={showAnimation} - animationDuration={animationDuration} - connectNulls={connectNulls} - /> - ))} - {onValueChange - ? categories.map((category) => ( - { - event.stopPropagation(); - const { name } = props; - onCategoryClick(name); - }} - /> - )) - : null} + {seriesData.map((series, seriesIndex) => { + return ( + <> + {categories.map((category, idx) => { + const yAxisId = + yAxisConfigs.length === 1 + ? 0 + : yAxisConfigs.findIndex((yAxisConfig) => + yAxisConfig.categories?.includes(category), + ); + const xAxisId = + xAxisConfigs.length === 1 + ? 0 + : xAxisConfigs.findIndex((xAxisConfig) => + xAxisConfig.series?.includes(series.name), + ); + const dataRange = series.data + .filter((d: any) => d[category]) + .map((d: any) => ({ + [index]: d[index], + [category]: d[category], + })); + return renderLine({ + key: ["line", series.name, category].filter(Boolean).join("-"), + name: [series.name, category].filter(Boolean).join("-"), + seriesIndex, + category, + color: categoryColors.get(category), + activeDot, + activeLegend, + onValueChange, + onDotClick, + curveType, + connectNulls, + yAxisId: yAxisId === -1 ? idx : yAxisId, + xAxisId: xAxisId === -1 ? seriesIndex : xAxisId, + data: dataRange, + showAnimation, + animationDuration, + xAxisCount: xAxisConfigs.length, + }); + })} + + ); + })} ) : ( @@ -405,7 +320,6 @@ const LineChart = React.forwardRef((props, ref)
); }); - LineChart.displayName = "LineChart"; export default LineChart; diff --git a/src/components/chart-elements/common/Area.tsx b/src/components/chart-elements/common/Area.tsx new file mode 100644 index 000000000..daa8e0b7a --- /dev/null +++ b/src/components/chart-elements/common/Area.tsx @@ -0,0 +1,125 @@ +import React, { Fragment } from "react"; +import { BaseColors, Color, colorPalette, getColorClassNames, tremorTwMerge } from "lib"; +import { Area as ReChartsArea, Dot } from "recharts"; +import { EventProps } from "./BaseChartProps"; +import { hasOnlyOneValueForThisKey } from "./utils"; +import { ActiveDot } from "./types"; + +interface AreaProps { + key: string; + name: string; + category: string; + color: Color | string | undefined; + activeDot?: ActiveDot; + activeLegend?: string; + onValueChange?: (value: EventProps) => void; + onDotClick: (itemData: any, event: React.MouseEvent) => void; + curveType: "basis" | "linear" | "monotone" | "natural" | "step" | "stepAfter" | "stepBefore"; + stackId?: string; + connectNulls?: boolean; + yAxisId?: number; + xAxisId?: number; + data: any[]; + showAnimation?: boolean; + animationDuration?: number; + xAxisCount: number; + seriesIndex?: number; +} + +export const renderArea = (props: AreaProps): JSX.Element => { + const { + key, + name, + category, + color, + activeDot, + activeLegend, + onValueChange, + onDotClick, + curveType, + stackId, + connectNulls, + yAxisId, + xAxisId, + data, + showAnimation, + animationDuration, + xAxisCount, + seriesIndex, + } = props; + + const colorClassNames = getColorClassNames(color ?? BaseColors.Gray, colorPalette.text); + return ( + { + const { cx, cy, stroke, strokeLinecap, strokeLinejoin, strokeWidth } = props; + return ( + onDotClick(props, event)} + /> + ); + }} + dot={(props: any) => { + const { stroke, strokeLinecap, strokeLinejoin, strokeWidth, cx, cy, index } = props; + + if ( + (hasOnlyOneValueForThisKey(data, category) && + !(activeDot || (activeLegend && activeLegend !== category))) || + (activeDot?.index === index && activeDot?.dataKey === category) + ) { + return ( + + ); + } + return ; + }} + name={name} + type={curveType} + dataKey={category} + stroke="" + fill={`url(#${color})`} + strokeWidth={2} + strokeLinejoin="round" + strokeLinecap="round" + isAnimationActive={showAnimation} + animationDuration={animationDuration} + stackId={stackId} + connectNulls={connectNulls} + yAxisId={yAxisId} + xAxisId={xAxisId} + data={xAxisCount > 1 ? data : undefined} + strokeDasharray={`${seriesIndex && seriesIndex > 0 ? 5 : ""}`} + /> + ); +}; diff --git a/src/components/chart-elements/common/Bar.tsx b/src/components/chart-elements/common/Bar.tsx new file mode 100644 index 000000000..231183dba --- /dev/null +++ b/src/components/chart-elements/common/Bar.tsx @@ -0,0 +1,103 @@ +import React from "react"; +import { BaseColors, Color, colorPalette, getColorClassNames, tremorTwMerge } from "lib"; +import { deepEqual } from "../common/utils"; +import { Bar as RechartsBar } from "recharts"; +import { EventProps } from "./BaseChartProps"; +import { Category } from "./types"; + +const renderShape = ( + props: any, + activeBar: any | undefined, + activeLegend: string | undefined, + layout: string, +) => { + const { fillOpacity, name, payload, value } = props; + let { x, width, y, height } = props; + + if (layout === "horizontal" && height < 0) { + y += height; + height = Math.abs(height); // height must be a positive number + } else if (layout === "vertical" && width < 0) { + x += width; + width = Math.abs(width); // width must be a positive number + } + + return ( + + ); +}; + +interface BarProps { + key: string; + name: string; + category: Category; + color: string | Color | undefined; + stackId?: string; + showAnimation?: boolean; + animationDuration?: number; + activeBar?: any; + activeLegend?: string; + onValueChange?: (value: EventProps) => void; + onBarClick?: (data: any, idx: number, event: React.MouseEvent) => void; + layout: "vertical" | "horizontal"; + yAxisId?: number; + xAxisId?: number; + data: any[]; + xAxisCount: number; + seriesIndex?: number; +} + +export const renderBar = (props: BarProps): JSX.Element => { + const { + key, + name, + category, + color, + stackId, + showAnimation, + animationDuration, + activeBar, + activeLegend, + onValueChange, + onBarClick, + yAxisId, + xAxisId, + data, + xAxisCount, + seriesIndex, + layout, + } = props; + + const colorClassNames = getColorClassNames(color ?? BaseColors.Gray, colorPalette.background); + return ( + renderShape(props, activeBar, activeLegend, layout)} + onClick={onBarClick} + yAxisId={yAxisId} + xAxisId={xAxisId} + data={xAxisCount > 1 ? data : undefined} + strokeDasharray={`${seriesIndex && seriesIndex > 0 ? 5 : ""}`} + /> + ); +}; diff --git a/src/components/chart-elements/common/BaseChartProps.tsx b/src/components/chart-elements/common/BaseChartProps.tsx index a08969f5a..f9ffbbe61 100644 --- a/src/components/chart-elements/common/BaseChartProps.tsx +++ b/src/components/chart-elements/common/BaseChartProps.tsx @@ -1,4 +1,10 @@ -import { Color, ValueFormatter, IntervalType } from "../../../lib"; +import { + Color, + ValueFormatter, + IntervalType, + HorizontalPosition, + VerticalPosition, +} from "../../../lib"; import type BaseAnimationTimingProps from "./BaseAnimationTimingProps"; import { CustomTooltipProps } from "./CustomTooltipProps"; @@ -13,6 +19,21 @@ type BaseEventProps = FixedProps & { export type EventProps = BaseEventProps | null | undefined; +interface XAxisConfig { + orientation: VerticalPosition; + series?: string[]; + valueFormatter?: ValueFormatter; +} + +export interface YAxisConfig { + autoMinValue?: boolean; + minValue?: number; + maxValue?: number; + orientation: HorizontalPosition; + categories?: string[]; + valueFormatter?: ValueFormatter; +} + interface BaseChartProps extends BaseAnimationTimingProps, React.HTMLAttributes { data: any[]; categories: string[]; @@ -43,6 +64,8 @@ interface BaseChartProps extends BaseAnimationTimingProps, React.HTMLAttributes< tickGap?: number; xAxisLabel?: string; yAxisLabel?: string; + xAxisConfigs?: XAxisConfig[]; + yAxisConfigs?: YAxisConfig[]; } export default BaseChartProps; diff --git a/src/components/chart-elements/common/ChartLegend.tsx b/src/components/chart-elements/common/ChartLegend.tsx index 12ecec78e..6403643b7 100644 --- a/src/components/chart-elements/common/ChartLegend.tsx +++ b/src/components/chart-elements/common/ChartLegend.tsx @@ -24,12 +24,11 @@ const ChartLegend = ( }); const filteredPayload = payload.filter((item: any) => item.type !== "none"); - return (
entry.value)} - colors={filteredPayload.map((entry: any) => categoryColors.get(entry.value))} + colors={filteredPayload.map((entry: any) => categoryColors.get(entry.dataKey))} onClickLegendItem={onClick} activeLegend={activeLegend} enableLegendSlider={enableLegendSlider} diff --git a/src/components/chart-elements/common/ChartTooltip.tsx b/src/components/chart-elements/common/ChartTooltip.tsx index 4596d70f9..4a29f55b2 100644 --- a/src/components/chart-elements/common/ChartTooltip.tsx +++ b/src/components/chart-elements/common/ChartTooltip.tsx @@ -7,6 +7,7 @@ import { ValueFormatter, colorPalette, } from "lib"; +import { Category } from "./types"; export const ChartTooltipFrame = ({ children }: { children: React.ReactNode }) => (
( +export const ChartTooltipRow = ({ value, name, color, xAxisCount }: ChartTooltipRowProps) => (
- + {!xAxisCount || xAxisCount === 1 ? ( + + ) : null}

; - valueFormatter: ValueFormatter; + valueFormatters: Record; + index: string; + xAxisCount: number; } -const ChartTooltip = ({ - active, - payload, - label, - categoryColors, - valueFormatter, -}: ChartTooltipProps) => { +const ChartTooltip = (props: ChartTooltipProps) => { + const { active, payload, label, categoryColors, valueFormatters, index, xAxisCount } = props; if (active && payload) { const filteredPayload = payload.filter((item: any) => item.type !== "none"); return ( -

-

- {label} -

-
+

+ {label} +

+
+ ) : null}
- {filteredPayload.map(({ value, name }: { value: number; name: string }, idx: number) => ( - - ))} + {filteredPayload.map( + ( + { + value, + name, + dataKey, + payload, + }: { value: number; name: string; dataKey: string; payload: any }, + idx: number, + ) => ( + + ), + )}
); diff --git a/src/components/chart-elements/common/Line.tsx b/src/components/chart-elements/common/Line.tsx new file mode 100644 index 000000000..3820a24a8 --- /dev/null +++ b/src/components/chart-elements/common/Line.tsx @@ -0,0 +1,121 @@ +import React, { Fragment } from "react"; +import { BaseColors, Color, colorPalette, getColorClassNames, tremorTwMerge } from "lib"; +import { Line as ReChartsLine, Dot } from "recharts"; +import { EventProps } from "./BaseChartProps"; +import { ActiveDot } from "./types"; +import { hasOnlyOneValueForThisKey } from "./utils"; + +interface LineProps { + key: string; + name: string; + category: string; + color: Color | string | undefined; + activeDot?: ActiveDot; + activeLegend?: string; + onValueChange?: (value: EventProps) => void; + onDotClick: (itemData: any, event: React.MouseEvent) => void; + curveType: "basis" | "linear" | "monotone" | "natural" | "step" | "stepAfter" | "stepBefore"; + connectNulls?: boolean; + yAxisId?: number; + xAxisId?: number; + data: any[]; + showAnimation?: boolean; + animationDuration?: number; + xAxisCount: number; + seriesIndex?: number; +} + +export const renderLine = (props: LineProps): JSX.Element => { + const { + key, + name, + category, + color, + activeDot, + activeLegend, + onValueChange, + onDotClick, + curveType, + connectNulls, + yAxisId, + xAxisId, + data, + showAnimation, + animationDuration, + xAxisCount, + seriesIndex, + } = props; + + const colorClassNames = getColorClassNames(color ?? BaseColors.Gray, colorPalette.text); + return ( + { + const { cx, cy, stroke, strokeLinecap, strokeLinejoin, strokeWidth } = props; + return ( + onDotClick(props, event)} + /> + ); + }} + dot={(props: any) => { + const { stroke, strokeLinecap, strokeLinejoin, strokeWidth, cx, cy, index } = props; + + if ( + (hasOnlyOneValueForThisKey(data, category) && + !(activeDot || (activeLegend && activeLegend !== category))) || + (activeDot?.index === index && activeDot?.dataKey === category) + ) { + return ( + + ); + } + return ; + }} + name={name} + type={curveType} + dataKey={category} + stroke="" + strokeWidth={2} + strokeLinejoin="round" + strokeLinecap="round" + isAnimationActive={showAnimation} + animationDuration={animationDuration} + connectNulls={connectNulls} + yAxisId={yAxisId} + xAxisId={xAxisId} + data={xAxisCount > 1 ? data : undefined} + strokeDasharray={`${seriesIndex && seriesIndex > 0 ? 5 : ""}`} + /> + ); +}; diff --git a/src/components/chart-elements/common/LinearGradient.tsx b/src/components/chart-elements/common/LinearGradient.tsx new file mode 100644 index 000000000..ef09c90a6 --- /dev/null +++ b/src/components/chart-elements/common/LinearGradient.tsx @@ -0,0 +1,43 @@ +import { BaseColors, Color, colorPalette, getColorClassNames } from "lib"; +import React from "react"; +import { ActiveDot } from "./types"; + +interface LinearGradientProps { + category: string; + activeDot?: ActiveDot; + activeLegend?: string; + colors: Map; + showGradient?: boolean; +} + +export const LinearGradient = (props: LinearGradientProps) => { + const { category, activeDot, activeLegend, colors, showGradient } = props; + return ( + + {showGradient ? ( + <> + + + + ) : ( + + )} + + ); +}; diff --git a/src/components/chart-elements/common/XAxis.tsx b/src/components/chart-elements/common/XAxis.tsx new file mode 100644 index 000000000..4d72937e4 --- /dev/null +++ b/src/components/chart-elements/common/XAxis.tsx @@ -0,0 +1,110 @@ +import React from "react"; +import { IntervalType, tremorTwMerge } from "lib"; +import { XAxis as RechartsXAxis, Label } from "recharts"; +import { ValueFormatter } from "lib/inputTypes"; + +interface CommonXAxisProps { + key: string; + showAxis: boolean; + data: any[]; + rotateLabel?: { + angle: number; + verticalShift?: number; + xAxisHeight?: number; + }; + label?: string; + orientation?: "top" | "bottom"; + xAxisId?: number; + layout?: "vertical" | "horizontal"; + padding: { left: number; right: number }; + xAxisCount: number; +} + +export interface HorizontalXAxisProps extends CommonXAxisProps { + intervalType: IntervalType; + startEndOnly: boolean; + minTickGap: number; + dataKey: string; +} + +export interface VerticalXAxisProps extends CommonXAxisProps { + width: number; + valueFormatter?: ValueFormatter; + relative?: boolean; +} + +export interface VerticalXAxisPropsWithExtra extends VerticalXAxisProps { + type: "category" | "number"; + tick: { transform: string }; + tickFormatter: ValueFormatter | undefined; +} + +export interface HorizontalXAxisPropsWithExtra extends HorizontalXAxisProps { + type: "category" | "number"; + tick: { transform: string }; + ticks: any[] | undefined; + interval: IntervalType; + allowDuplicatedCategory: boolean; +} + +export const renderVerticalXAxis = (props: VerticalXAxisProps): JSX.Element => { + const { relative, valueFormatter } = props; + const xAxisProps: VerticalXAxisPropsWithExtra = { + type: "number" as "category" | "number", + tick: { transform: "translate(-3, 0)" }, + tickFormatter: relative ? (value: number) => `${(value * 100).toString()} %` : valueFormatter, + ...props, + }; + return renderXAxis(xAxisProps); +}; + +export const renderHorizontalXAxis = (props: HorizontalXAxisProps): JSX.Element => { + const { data, dataKey, intervalType, startEndOnly, xAxisCount } = props; + const xAxisProps: HorizontalXAxisPropsWithExtra = { + type: "category" as "category" | "number", + tick: { transform: "translate(0, 6)" }, + ticks: startEndOnly ? [data[0][dataKey], data[data.length - 1][dataKey]] : undefined, + interval: startEndOnly ? "preserveStartEnd" : intervalType, + allowDuplicatedCategory: xAxisCount > 1 ? false : true, + ...props, + }; + return renderXAxis(xAxisProps); +}; + +export const renderXAxis = ( + props: HorizontalXAxisPropsWithExtra | VerticalXAxisPropsWithExtra, +): JSX.Element => { + const { label, layout, rotateLabel, showAxis, ...rechartXAxisProps } = props; + + return ( + + {label && ( + + )} + + ); +}; diff --git a/src/components/chart-elements/common/YAxis.tsx b/src/components/chart-elements/common/YAxis.tsx new file mode 100644 index 000000000..70d4dad64 --- /dev/null +++ b/src/components/chart-elements/common/YAxis.tsx @@ -0,0 +1,104 @@ +import { tremorTwMerge } from "lib"; +import React from "react"; +import { Label, YAxis as RechartsYAxis } from "recharts"; +import { AxisDomain } from "recharts/types/util/types"; +import { IntervalType, ValueFormatter } from "lib/inputTypes"; + +interface CommonYAxisProps { + key: string; + showAxis: boolean; + label?: string; + orientation?: "left" | "right"; + yAxisId?: number; + layout?: "vertical" | "horizontal"; + allowDecimals: boolean; + xAxisCount: number; +} + +export interface HorizontalYAxisProps extends CommonYAxisProps { + width: number; + domain: AxisDomain; + valueFormatter?: ValueFormatter; + relative?: boolean; +} + +export interface VerticalYAxisProps extends CommonYAxisProps { + intervalType: IntervalType; + startEndOnly: boolean; + data: any[]; + dataKey: string; + minTickGap: number; +} + +export interface VerticalYAxisPropsWithExtra extends VerticalYAxisProps { + type: "category" | "number"; + tick: { transform: string }; + ticks: any[] | undefined; + interval: IntervalType; + allowDuplicatedCategory: boolean; +} + +export interface HorizontalYAxisPropsWithExtra extends HorizontalYAxisProps { + type: "category" | "number"; + tick: { transform: string }; + tickFormatter: ValueFormatter | undefined; +} + +export const renderVerticalYAxis = (props: VerticalYAxisProps): JSX.Element => { + const { data, dataKey, intervalType, startEndOnly, xAxisCount } = props; + const yAxisProps: VerticalYAxisPropsWithExtra = { + type: "category", + tick: { transform: "translate(0, 6)" }, + ticks: startEndOnly ? [data[0][dataKey], data[data.length - 1][dataKey]] : undefined, + interval: startEndOnly ? "preserveStartEnd" : intervalType, + allowDuplicatedCategory: xAxisCount > 1 ? false : true, + ...props, + }; + return renderYAxis(yAxisProps); +}; + +export const renderHorizontalYAxis = (props: HorizontalYAxisProps): JSX.Element => { + const { relative, valueFormatter } = props; + const yAxisProps: HorizontalYAxisPropsWithExtra = { + type: "number", + tick: { transform: "translate(-3, 0)" }, + tickFormatter: relative ? (value: number) => `${(value * 100).toString()} %` : valueFormatter, + ...props, + }; + return renderYAxis(yAxisProps); +}; + +export const renderYAxis = (props: HorizontalYAxisProps | VerticalYAxisProps): JSX.Element => { + const { label, showAxis, ...rechartYAxisProps } = props; + + return ( + + {label && ( + + )} + + ); +}; diff --git a/src/components/chart-elements/common/types.ts b/src/components/chart-elements/common/types.ts new file mode 100644 index 000000000..722532eb6 --- /dev/null +++ b/src/components/chart-elements/common/types.ts @@ -0,0 +1,6 @@ +export interface ActiveDot { + index?: number; + dataKey?: string; +} + +export type Category = string; diff --git a/src/components/chart-elements/common/utils.ts b/src/components/chart-elements/common/utils.ts index e367cf868..2924169ac 100644 --- a/src/components/chart-elements/common/utils.ts +++ b/src/components/chart-elements/common/utils.ts @@ -1,3 +1,4 @@ +import { AxisDomain } from "recharts/types/util/types"; import { Color } from "../../../lib/inputTypes"; export const constructCategoryColors = ( @@ -15,7 +16,7 @@ export const getYAxisDomain = ( autoMinValue: boolean, minValue: number | undefined, maxValue: number | undefined, -) => { +): AxisDomain => { const minDomain = autoMinValue ? "auto" : minValue ?? 0; const maxDomain = maxValue ?? "auto"; return [minDomain, maxDomain]; diff --git a/src/stories/chart-elements/AreaChart.stories.tsx b/src/stories/chart-elements/AreaChart.stories.tsx index db57ce067..6725571fb 100644 --- a/src/stories/chart-elements/AreaChart.stories.tsx +++ b/src/stories/chart-elements/AreaChart.stories.tsx @@ -12,6 +12,8 @@ import { longBaseChartData, longIndexBaseChartData, simpleBaseChartWithNegativeValues, + multipleXAxisChartData, + multiSeriesMultiCategoriesChartData, } from "./helpers/testData"; import { valueFormatter } from "./helpers/utils"; @@ -75,7 +77,10 @@ export const ChangedCategoriesOrder: Story = { }; export const LongValues: Story = { - args: { categories: ["This is an edge case"] }, + args: { + categories: ["This is an edge case"], + yAxisWidth: 110, + }, }; export const MultipleCategories: Story = { @@ -364,3 +369,63 @@ export const AxisLabels: Story = { yAxisLabel: "Amount (USD)", }, }; + +export const RightOrientation: Story = { + args: { + categories: ["Sales"], + yAxisConfigs: [{ orientation: "right" }], + }, +}; + +export const MultipleYAxes: Story = { + args: { + yAxisConfigs: [ + { orientation: "left", valueFormatter: (value: number) => `${value} S` }, + { orientation: "right", valueFormatter: (value: number) => `${value} P` }, + ], + }, +}; + +export const MultipleYAxesWithExplicitCategories: Story = { + args: { + yAxisConfigs: [ + { + orientation: "left", + categories: ["Successful Payments"], + valueFormatter: (value: number) => `${value} S`, + }, + { + orientation: "right", + categories: ["Sales"], + valueFormatter: (value: number) => `${value} P`, + }, + ], + }, +}; + +export const TopOrientation: Story = { + args: { + categories: ["Sales"], + xAxisConfigs: [{ orientation: "top" }], + }, +}; + +export const MultipleXAxes: Story = { + args: { + categories: ["Sales"], + data: multipleXAxisChartData, + xAxisConfigs: [{ orientation: "bottom" }, { orientation: "top" }], + }, +}; + +export const MultipleXAndYAxes: Story = { + args: { + categories: ["Sales", "Cost"], + data: multiSeriesMultiCategoriesChartData, + yAxisConfigs: [ + { orientation: "left", valueFormatter: (value: number) => `${value} S` }, + { orientation: "right", valueFormatter: (value: number) => `${value} P` }, + ], + xAxisConfigs: [{ orientation: "bottom" }, { orientation: "top" }], + }, +}; diff --git a/src/stories/chart-elements/BarChart.stories.tsx b/src/stories/chart-elements/BarChart.stories.tsx index 664925dcf..41667d54f 100644 --- a/src/stories/chart-elements/BarChart.stories.tsx +++ b/src/stories/chart-elements/BarChart.stories.tsx @@ -387,3 +387,43 @@ export const AxisLabels: Story = { yAxisLabel: "Amount (USD)", }, }; + +export const RightOrientation: Story = { + args: { + categories: ["Sales"], + yAxisConfigs: [{ orientation: "right" }], + }, +}; + +export const MultipleYAxes: Story = { + args: { + yAxisConfigs: [ + { orientation: "left", valueFormatter: (value: number) => `${value} S` }, + { orientation: "right", valueFormatter: (value: number) => `${value} P` }, + ], + }, +}; + +export const MultipleYAxesWithExplicitCategories: Story = { + args: { + yAxisConfigs: [ + { + orientation: "left", + categories: ["Successful Payments"], + valueFormatter: (value: number) => `${value} S`, + }, + { + orientation: "right", + categories: ["Sales"], + valueFormatter: (value: number) => `${value} P`, + }, + ], + }, +}; + +export const TopOrientation: Story = { + args: { + categories: ["Sales"], + xAxisConfigs: [{ orientation: "top" }], + }, +}; diff --git a/src/stories/chart-elements/LineChart.stories.tsx b/src/stories/chart-elements/LineChart.stories.tsx index 11cea2588..a95f06767 100644 --- a/src/stories/chart-elements/LineChart.stories.tsx +++ b/src/stories/chart-elements/LineChart.stories.tsx @@ -12,6 +12,8 @@ import { longBaseChartData, longIndexBaseChartData, simpleBaseChartWithNegativeValues, + multipleXAxisChartData, + multiSeriesMultiCategoriesChartData, } from "./helpers/testData"; import { valueFormatter } from "./helpers/utils"; @@ -131,7 +133,10 @@ export const SingleAndMultipleDataAndOnValueChange: Story = { }; export const LegendSlider: Story = { - args: { enableLegendSlider: true }, + args: { + categories: ["Sales", "Successful Payments", "Test"], + enableLegendSlider: true, + }, }; export const PreserveStartEnd: Story = { args: { intervalType: "preserveStartEnd" }, @@ -324,3 +329,54 @@ export const AxisLabels: Story = { yAxisLabel: "Amount (USD)", }, }; + +export const RightOrientation: Story = { + args: { + categories: ["Sales"], + yAxisConfigs: [{ orientation: "right" }], + }, +}; + +export const MultipleYAxes: Story = { + args: { + yAxisConfigs: [ + { + orientation: "left", + categories: ["Successful Payments"], + valueFormatter: (value: number) => `${value} S`, + }, + { + orientation: "right", + categories: ["Sales"], + valueFormatter: (value: number) => `${value} P`, + }, + ], + }, +}; + +export const TopOrientation: Story = { + args: { + categories: ["Sales"], + xAxisConfigs: [{ orientation: "top" }], + }, +}; + +export const MultipleXAxes: Story = { + args: { + categories: ["Sales"], + data: multipleXAxisChartData, + xAxisConfigs: [{ orientation: "bottom" }, { orientation: "top" }], + }, +}; + +export const MultipleXAndYAxesMultipleCategories: Story = { + args: { + categories: ["Sales", "Cost"], + data: multiSeriesMultiCategoriesChartData, + yAxisConfigs: [ + { orientation: "left", valueFormatter: (value: number) => `${value} S` }, + { orientation: "right", valueFormatter: (value: number) => `${value} P` }, + ], + xAxisConfigs: [{ orientation: "bottom" }, { orientation: "top" }], + }, +}; diff --git a/src/stories/chart-elements/helpers/testData.tsx b/src/stories/chart-elements/helpers/testData.tsx index 879569ad2..d3f4b92c8 100644 --- a/src/stories/chart-elements/helpers/testData.tsx +++ b/src/stories/chart-elements/helpers/testData.tsx @@ -1751,3 +1751,245 @@ export const singleAndMultipleData = [ Sales: 3612, }, ]; + +export const multipleXAxisChartData = [ + { + name: "2021", + data: [ + { + month: "Jan 21", + Sales: 4400, + }, + { + month: "Feb 21", + Sales: 3612, + }, + { + month: "Mar 21", + Sales: 145, + }, + { + month: "Apr 21", + Sales: 5142, + }, + { + month: "May 21", + Sales: 1234, + }, + { + month: "Jun 21", + Sales: 2345, + }, + { + month: "Jul 21", + Sales: 3456, + }, + { + month: "Aug 21", + Sales: 4567, + }, + { + month: "Sep 21", + Sales: 5678, + }, + { + month: "Oct 21", + Sales: 6789, + }, + { + month: "Nov 21", + Sales: 7890, + }, + { + month: "Dec 21", + Sales: 8901, + }, + ], + }, + { + name: "2022", + data: [ + { + month: "Jan 22", + Sales: 571, + }, + { + month: "Feb 22", + Sales: 234, + }, + { + month: "Mar 22", + Sales: 2345, + }, + { + month: "Apr 22", + Sales: 3456, + }, + { + month: "May 22", + Sales: 4567, + }, + { + month: "Jun 22", + Sales: 5678, + }, + { + month: "Jul 22", + Sales: 6789, + }, + { + month: "Aug 22", + Sales: 7890, + }, + { + month: "Sep 22", + Sales: 8901, + }, + { + month: "Oct 22", + Sales: 9012, + }, + { + month: "Nov 22", + Sales: 1234, + }, + { + month: "Dec 22", + Sales: 2345, + }, + ], + }, +]; + +export const multiSeriesMultiCategoriesChartData = [ + { + name: "2021", + data: [ + { + month: "Jan 21", + Sales: 4400, + Cost: 400, + }, + { + month: "Feb 21", + Sales: 3612, + Cost: 310, + }, + { + month: "Mar 21", + Sales: 145, + Cost: 100, + }, + { + month: "Apr 21", + Sales: 5142, + Cost: 500, + }, + { + month: "May 21", + Sales: 1234, + Cost: 100, + }, + { + month: "Jun 21", + Sales: 2345, + Cost: 200, + }, + { + month: "Jul 21", + Sales: 3456, + Cost: 300, + }, + { + month: "Aug 21", + Sales: 4567, + Cost: 400, + }, + { + month: "Sep 21", + Sales: 5678, + Cost: 500, + }, + { + month: "Oct 21", + Sales: 6789, + Cost: 600, + }, + { + month: "Nov 21", + Sales: 7890, + Cost: 700, + }, + { + month: "Dec 21", + Sales: 8901, + Cost: 800, + }, + ], + }, + { + name: "2022", + data: [ + { + month: "Jan 22", + Sales: 571, + Cost: 5, + }, + { + month: "Feb 22", + Sales: 234, + Cost: 2, + }, + { + month: "Mar 22", + Sales: 2345, + Cost: 23, + }, + { + month: "Apr 22", + Sales: 3456, + Cost: 34, + }, + { + month: "May 22", + Sales: 4567, + Cost: 45, + }, + { + month: "Jun 22", + Sales: 5678, + Cost: 56, + }, + { + month: "Jul 22", + Sales: 6789, + Cost: 67, + }, + { + month: "Aug 22", + Sales: 7890, + Cost: 78, + }, + { + month: "Sep 22", + Sales: 8901, + Cost: 89, + }, + { + month: "Oct 22", + Sales: 9012, + Cost: 90, + }, + { + month: "Nov 22", + Sales: 1234, + Cost: 12, + }, + { + month: "Dec 22", + Sales: 2345, + Cost: 23, + }, + ], + }, +]; From 662c2b77d4894d186bb2641ac76fb79e92ef9abb Mon Sep 17 00:00:00 2001 From: Alexandre Balon-Perin Date: Thu, 25 Jul 2024 13:41:00 +0900 Subject: [PATCH 2/2] small improvement --- .../chart-elements/AreaChart/AreaChart.tsx | 14 ++++++-------- src/stories/chart-elements/AreaChart.stories.tsx | 7 +++++++ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/components/chart-elements/AreaChart/AreaChart.tsx b/src/components/chart-elements/AreaChart/AreaChart.tsx index 13d8c673c..18805bbdb 100644 --- a/src/components/chart-elements/AreaChart/AreaChart.tsx +++ b/src/components/chart-elements/AreaChart/AreaChart.tsx @@ -134,15 +134,13 @@ const AreaChart = React.forwardRef((props, ref) setActiveDot(undefined); } - const yAxisConfigsValueFormatter = yAxisConfigs.flatMap((yAxisConfig) => { - return ( - yAxisConfig.categories?.map((c) => [c, yAxisConfig.valueFormatter ?? valueFormatter]) ?? [] - ); - }); const yAxisValueFormatters = Object.fromEntries( - yAxisConfigsValueFormatter.length > 0 - ? yAxisConfigsValueFormatter - : categories.map((c, idx) => [c, yAxisConfigs[idx]?.valueFormatter ?? valueFormatter]), + yAxisConfigs.flatMap((yAxisConfig, i) => { + return ( + yAxisConfig.categories?.map((c) => [c, yAxisConfig.valueFormatter ?? valueFormatter]) ?? + categories.map((c) => [c, yAxisConfigs[i]?.valueFormatter ?? valueFormatter]) + ); + }), ); return ( diff --git a/src/stories/chart-elements/AreaChart.stories.tsx b/src/stories/chart-elements/AreaChart.stories.tsx index 6725571fb..3fd189087 100644 --- a/src/stories/chart-elements/AreaChart.stories.tsx +++ b/src/stories/chart-elements/AreaChart.stories.tsx @@ -386,6 +386,13 @@ export const MultipleYAxes: Story = { }, }; +export const SingleYAxisWithValueFormatter: Story = { + args: { + categories: ["Sales", "Successful Payments"], + yAxisConfigs: [{ orientation: "left", valueFormatter: (value: number) => `${value} S` }], + }, +}; + export const MultipleYAxesWithExplicitCategories: Story = { args: { yAxisConfigs: [