From c3d4f9cdcddbf5dd96b49b030c65014ce39632dc Mon Sep 17 00:00:00 2001 From: mbauchet Date: Sun, 23 Jul 2023 10:13:46 +0200 Subject: [PATCH 1/4] add forecast line(s) for LineChart --- .../chart-elements/LineChart/LineChart.tsx | 423 +++++++++++------- .../chart-elements/common/ChartLegend.tsx | 7 +- .../chart-elements/common/ChartTooltip.tsx | 38 +- .../chart-elements/LineChart.stories.tsx | 22 +- .../chart-elements/helpers/testData.tsx | 145 ++++++ 5 files changed, 456 insertions(+), 179 deletions(-) diff --git a/src/components/chart-elements/LineChart/LineChart.tsx b/src/components/chart-elements/LineChart/LineChart.tsx index 5c1e6819c..e6a0d258b 100644 --- a/src/components/chart-elements/LineChart/LineChart.tsx +++ b/src/components/chart-elements/LineChart/LineChart.tsx @@ -1,14 +1,14 @@ "use client"; import React, { useState } from "react"; import { - CartesianGrid, - Legend, - Line, - LineChart as ReChartsLineChart, - ResponsiveContainer, - Tooltip, - XAxis, - YAxis, + CartesianGrid, + Legend, + Line, + LineChart as ReChartsLineChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, } from "recharts"; import { AxisDomain } from "recharts/types/util/types"; @@ -19,176 +19,263 @@ import ChartLegend from "components/chart-elements/common/ChartLegend"; import ChartTooltip from "../common/ChartTooltip"; import { - BaseColors, - colorPalette, - defaultValueFormatter, - getColorClassNames, - themeColorRange, - tremorTwMerge, + BaseColors, + colorPalette, + defaultValueFormatter, + getColorClassNames, + themeColorRange, + tremorTwMerge, } from "lib"; import { CurveType } from "../../../lib/inputTypes"; export interface LineChartProps extends BaseChartProps { - curveType?: CurveType; - connectNulls?: boolean; + curveType?: CurveType; + connectNulls?: boolean; + forecastCategories?: string[] | string[][]; } const LineChart = React.forwardRef((props, ref) => { - const { - data = [], - categories = [], - index, - colors = themeColorRange, - valueFormatter = defaultValueFormatter, - startEndOnly = false, - showXAxis = true, - showYAxis = true, - yAxisWidth = 56, - animationDuration = 900, - showAnimation = true, - showTooltip = true, - showLegend = true, - showGridLines = true, - autoMinValue = false, - curveType = "linear", - minValue, - maxValue, - connectNulls = false, - allowDecimals = true, - noDataText, - className, - ...other - } = props; - const [legendHeight, setLegendHeight] = useState(60); - const categoryColors = constructCategoryColors(categories, colors); + const { + data = [], + categories = [], + forecastCategories, + index, + colors = themeColorRange, + valueFormatter = defaultValueFormatter, + startEndOnly = false, + showXAxis = true, + showYAxis = true, + yAxisWidth = 56, + animationDuration = 900, + showAnimation = true, + showTooltip = true, + showLegend = true, + showGridLines = true, + autoMinValue = false, + curveType = "linear", + minValue, + maxValue, + connectNulls = false, + allowDecimals = true, + noDataText, + className, + ...other + } = props; + const [legendHeight, setLegendHeight] = useState(60); + const categoryColors = constructCategoryColors(categories, colors); - const yAxisDomain = getYAxisDomain(autoMinValue, minValue, maxValue); + const yAxisDomain = getYAxisDomain(autoMinValue, minValue, maxValue); - return ( -
- - {data?.length ? ( - - {showGridLines ? ( - + + {data?.length ? ( + + {showGridLines ? ( + + ) : null} + + + {showTooltip ? ( + ( + + )} + position={{ y: 0 }} + /> + ) : null} + {showLegend ? ( + ChartLegend({ payload }, categoryColors, setLegendHeight, forecastCategories)} + /> + ) : null} + {categories.map((category) => ( + + ))} + {forecastCategories ? ( + forecastCategories.map((category, idX) => ( + <> + {Array.isArray(category) ? ( + <> + {category.map((subCategory) => ( + + ))} + + ) : ( + + )} + + )) + ) : ( + null + )} + + ) : ( + )} - strokeDasharray="3 3" - horizontal={true} - vertical={false} - /> - ) : null} - - - {showTooltip ? ( - ( - - )} - position={{ y: 0 }} - /> - ) : null} - {showLegend ? ( - ChartLegend({ payload }, categoryColors, setLegendHeight)} - /> - ) : null} - {categories.map((category) => ( - - ))} - - ) : ( - - )} - -
- ); + + + ); }); LineChart.displayName = "LineChart"; diff --git a/src/components/chart-elements/common/ChartLegend.tsx b/src/components/chart-elements/common/ChartLegend.tsx index fa0e2d37a..050d0566c 100644 --- a/src/components/chart-elements/common/ChartLegend.tsx +++ b/src/components/chart-elements/common/ChartLegend.tsx @@ -9,7 +9,8 @@ const ChartLegend = ( { payload }: any, categoryColors: Map, setLegendHeight: React.Dispatch>, -) => { + forecastCategories?: string[] | string[][] + ) => { const legendRef = useRef(null); useOnWindowResize(() => { @@ -23,8 +24,8 @@ const ChartLegend = ( return (
entry.value)} - colors={payload.map((entry: any) => categoryColors.get(entry.value))} + categories={payload.filter((e: any) => !forecastCategories?.flat()?.includes(e.value)).map((entry: any) => entry.value)} + colors={payload.filter((e: any) => !forecastCategories?.flat()?.includes(e.value)).map((entry: any) => categoryColors.get(entry.value))} />
); diff --git a/src/components/chart-elements/common/ChartTooltip.tsx b/src/components/chart-elements/common/ChartTooltip.tsx index d0126ab34..0b276e55c 100644 --- a/src/components/chart-elements/common/ChartTooltip.tsx +++ b/src/components/chart-elements/common/ChartTooltip.tsx @@ -78,6 +78,8 @@ export interface ChartTooltipProps { label: string; categoryColors: Map; valueFormatter: ValueFormatter; + categories: string[]; + forecastCategories?: string[] | string[][]; } const ChartTooltip = ({ @@ -86,6 +88,8 @@ const ChartTooltip = ({ label, categoryColors, valueFormatter, + categories, + forecastCategories, }: ChartTooltipProps) => { if (active && payload) { return ( @@ -117,13 +121,33 @@ const ChartTooltip = ({
{payload.map(({ value, name }: { value: number; name: string }, idx: number) => ( - - ))} + <> + { + forecastCategories?.flat()?.includes(name) ? ( + <> + {(!categories.includes(name) && (payload.length !== (categories.length + (forecastCategories?.flat()?.length ?? 0)))) ? ( + subArray.indexOf(name) !== -1)] ) ?? BaseColors.Blue} + /> + ) : ( + null + )} + + ) : ( + + + ) + } + + ))}
); diff --git a/src/stories/chart-elements/LineChart.stories.tsx b/src/stories/chart-elements/LineChart.stories.tsx index 7dfa4b300..76e9e0dbc 100644 --- a/src/stories/chart-elements/LineChart.stories.tsx +++ b/src/stories/chart-elements/LineChart.stories.tsx @@ -3,7 +3,7 @@ import React from "react"; import { ComponentMeta, ComponentStory } from "@storybook/react"; import { Card, LineChart, Title } from "components"; -import { simpleBaseChartData as data, simpleBaseChartDataWithNulls } from "./helpers/testData"; +import { simpleBaseChartData as data, simpleBaseChartDataWithNulls, simpleBaseChartDataWithForecast } from "./helpers/testData"; import { valueFormatter } from "stories/chart-elements/helpers/utils"; // More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export @@ -186,3 +186,23 @@ WithShortAnimationDuration.args = { categories: ["Sales", "Successful Payments"], index: "month", }; + +export const WithSingleForecastLine = DefaultTemplate.bind({}); +WithSingleForecastLine.args = { + data: simpleBaseChartDataWithForecast, + showAnimation: true, + animationDuration: 100, + categories: ["Sales", "Successful Payments"], + forecastCategories: ["Sales Forecast", "Successful Payments Forecast"], + index: "month", +}; + +export const WithMultipleForecastLines = DefaultTemplate.bind({}); +WithMultipleForecastLines.args = { + data: simpleBaseChartDataWithForecast, + showAnimation: true, + animationDuration: 100, + categories: ["Sales", "Successful Payments"], + forecastCategories: [["Sales Forecast Max", "Sales Forecast Min"], ["Successful Payments Forecast Max", "Successful Payments Forecast Min"]], + index: "month", +}; diff --git a/src/stories/chart-elements/helpers/testData.tsx b/src/stories/chart-elements/helpers/testData.tsx index dbebab104..b8cb46a6d 100644 --- a/src/stories/chart-elements/helpers/testData.tsx +++ b/src/stories/chart-elements/helpers/testData.tsx @@ -138,3 +138,148 @@ export const simpleSingleCategoryData = [ deltaType: "moderateIncrease", }, ]; + +export const simpleBaseChartDataWithForecast = [ + { + month: "Jan 21'", + Sales: 4000, + "Successful Payments": 3000, + "This is an edge case": 100000000, + Test: 5000, + }, + { + month: "Feb 21'", + Sales: 3000, + "Successful Payments": 2000, + "This is an edge case": 100000000, + Test: 5000, + }, + { + month: "Mar 21'", + Sales: 2000, + "Successful Payments": 1700, + "This is an edge case": 100000000, + Test: 5000, + }, + { + month: "Apr 21'", + Sales: 2780, + "Successful Payments": 2500, + "This is an edge case": 100000000, + Test: 5000, + }, + { + month: "May 21'", + Sales: 1890, + "Successful Payments": 1000, + "This is an edge case": 100000000, + Test: 5000, + }, + { + month: "Jun 21'", + Sales: 2390, + "Successful Payments": 2000, + "This is an edge case": 100000000, + Test: 5000, + }, + { + month: "Jul 21'", + Sales: 3490, + "Successful Payments": 3000, + "This is an edge case": 100000000, + Test: 5000, + }, + { + month: "Aug 21'", + Sales: 4100, + "Successful Payments": 3100, + "This is an edge case": 100000000, + Test: 5000, + "Sales Forecast": 4100, + "Sales Forecast Min" : 4100, + "Sales Forecast Max" : 4100, + "Successful Payments Forecast": 3100, + "Successful Payments Forecast Min" : 3100, + "Successful Payments Forecast Max" : 3100 + }, + { + month: "Sept 21'", + Sales: null, + "Successful Payments": null, + "This is an edge case": 100000000, + Test: 5000, + "Sales Forecast": 5600, + "Sales Forecast Min" : 5200, + "Sales Forecast Max" : 6000, + "Successful Payments Forecast": 3200, + "Successful Payments Forecast Min" : 2900, + "Successful Payments Forecast Max" : 3700 + }, + { + month: "Oct 21'", + Sales: null, + "Successful Payments": null, + "This is an edge case": 100000000, + Test: 5000, + "Sales Forecast": 5300, + "Sales Forecast Min" : 5000, + "Sales Forecast Max" : 6700, + "Successful Payments Forecast": 3600, + "Successful Payments Forecast Min" : 3100, + "Successful Payments Forecast Max" : 4000 + }, + { + month: "Nov 21'", + Sales: null, + "Successful Payments": null, + "This is an edge case": 100000000, + Test: 5000, + "Sales Forecast": 5000, + "Sales Forecast Min" : 4800, + "Sales Forecast Max" : 6900, + "Successful Payments Forecast": 3400, + "Successful Payments Forecast Min" : 3000, + "Successful Payments Forecast Max" : 4100 + }, + { + month: "Dec 21'", + Sales: null, + "Successful Payments": null, + "This is an edge case": 100000000, + Test: 5000, + "Sales Forecast": 5900, + "Sales Forecast Min" : 5000, + "Sales Forecast Max" : 7200, + "Successful Payments Forecast": 3500, + "Successful Payments Forecast Min" : 2900, + "Successful Payments Forecast Max" : 4500 + }, + { + month: "Jan 22'", + Sales: null, + "Successful Payments": null, + "This is an edge case": 100000000, + Test: 5000, + "Sales Forecast": 6000, + "Sales Forecast Min" : 5900, + "Sales Forecast Max" : 7000, + "Successful Payments Forecast": 4000, + "Successful Payments Forecast Min" : 3200, + "Successful Payments Forecast Max" : 4800 + }, + { + month: "Feb 22'", + Sales: null, + "Successful Payments": null, + "This is an edge case": 100000000, + Test: 5000, + "Sales Forecast": 6100, + "Sales Forecast Min" : 6100, + "Sales Forecast Max" : 7400, + "Successful Payments Forecast": 4700, + "Successful Payments Forecast Min" : 4000, + "Successful Payments Forecast Max" : 6000 + }, + + ]; + From 2437905f50023f858c8a66a72665541a3e00dc1a Mon Sep 17 00:00:00 2001 From: mbauchet Date: Sun, 23 Jul 2023 11:28:48 +0200 Subject: [PATCH 2/4] add forecast to line chart --- .../chart-elements/LineChart/LineChart.tsx | 34 ++++++++++++------- .../chart-elements/common/ChartTooltip.tsx | 13 ++++--- src/components/chart-elements/common/utils.ts | 26 ++++++++++++++ .../chart-elements/LineChart.stories.tsx | 21 ++++++++++-- .../chart-elements/helpers/testData.tsx | 2 +- 5 files changed, 73 insertions(+), 23 deletions(-) diff --git a/src/components/chart-elements/LineChart/LineChart.tsx b/src/components/chart-elements/LineChart/LineChart.tsx index e6a0d258b..c5b1ae6c6 100644 --- a/src/components/chart-elements/LineChart/LineChart.tsx +++ b/src/components/chart-elements/LineChart/LineChart.tsx @@ -1,5 +1,5 @@ "use client"; -import React, { useState } from "react"; +import React, { Fragment, useState } from "react"; import { CartesianGrid, Legend, @@ -12,7 +12,7 @@ import { } from "recharts"; import { AxisDomain } from "recharts/types/util/types"; -import { constructCategoryColors, getYAxisDomain } from "../common/utils"; +import { constructCategoryColors, getPercentageWithCategories, getYAxisDomain } from "../common/utils"; import NoData from "../common/NoData"; import BaseChartProps from "../common/BaseChartProps"; import ChartLegend from "components/chart-elements/common/ChartLegend"; @@ -32,6 +32,7 @@ export interface LineChartProps extends BaseChartProps { curveType?: CurveType; connectNulls?: boolean; forecastCategories?: string[] | string[][]; + forecastAnimationDelay?: number; } const LineChart = React.forwardRef((props, ref) => { @@ -65,7 +66,14 @@ const LineChart = React.forwardRef((props, ref) const categoryColors = constructCategoryColors(categories, colors); const yAxisDomain = getYAxisDomain(autoMinValue, minValue, maxValue); + + const percentageOfRealDatas = forecastCategories ? getPercentageWithCategories(data, categories) : 1; + const percentageOfForecastedDatas = forecastCategories ? getPercentageWithCategories(data, forecastCategories) : 0; + const animationDurationPercentage = animationDuration * percentageOfRealDatas + const forecastAnimationDurationPercentage = animationDuration * percentageOfForecastedDatas + const forecastAnimationDelay = animationDurationPercentage - (animationDurationPercentage / 2.5) + return (
@@ -182,13 +190,13 @@ const LineChart = React.forwardRef((props, ref) strokeLinejoin="round" strokeLinecap="round" isAnimationActive={showAnimation} - animationDuration={animationDuration} + animationDuration={animationDurationPercentage} connectNulls={connectNulls} /> ))} {forecastCategories ? ( - forecastCategories.map((category, idX) => ( - <> + forecastCategories.map((category, idx) => ( + {Array.isArray(category) ? ( <> {category.map((subCategory) => ( @@ -197,7 +205,7 @@ const LineChart = React.forwardRef((props, ref) tremorTwMerge( "opacity-50", getColorClassNames( - categoryColors.get(categories[idX]) ?? BaseColors.Gray, + categoryColors.get(categories[idx]) ?? BaseColors.Gray, colorPalette.text, ).strokeColor ) @@ -207,7 +215,7 @@ const LineChart = React.forwardRef((props, ref) className: tremorTwMerge( "stroke-tremor-background dark:stroke-dark-tremor-background", getColorClassNames( - categoryColors.get(categories[idX]) ?? BaseColors.Gray, + categoryColors.get(categories[idx]) ?? BaseColors.Gray, colorPalette.text, ).fillColor, ), @@ -223,7 +231,8 @@ const LineChart = React.forwardRef((props, ref) strokeLinecap="round" strokeDasharray="5 5" isAnimationActive={showAnimation} - animationDuration={animationDuration} + animationDuration={forecastAnimationDurationPercentage} + animationBegin={forecastAnimationDelay} connectNulls={connectNulls} /> ))} @@ -234,7 +243,7 @@ const LineChart = React.forwardRef((props, ref) tremorTwMerge( "opacity-50", getColorClassNames( - categoryColors.get(categories[idX]) ?? BaseColors.Gray, + categoryColors.get(categories[idx]) ?? BaseColors.Gray, colorPalette.text, ).strokeColor ) @@ -244,7 +253,7 @@ const LineChart = React.forwardRef((props, ref) className: tremorTwMerge( "stroke-tremor-background dark:stroke-dark-tremor-background", getColorClassNames( - categoryColors.get(categories[idX]) ?? BaseColors.Gray, + categoryColors.get(categories[idx]) ?? BaseColors.Gray, colorPalette.text, ).fillColor, ), @@ -260,11 +269,12 @@ const LineChart = React.forwardRef((props, ref) strokeLinecap="round" strokeDasharray="5 5" isAnimationActive={showAnimation} - animationDuration={animationDuration} + animationDuration={forecastAnimationDurationPercentage} + animationBegin={forecastAnimationDelay} connectNulls={connectNulls} /> )} - + )) ) : ( null diff --git a/src/components/chart-elements/common/ChartTooltip.tsx b/src/components/chart-elements/common/ChartTooltip.tsx index 0b276e55c..1105d8a9c 100644 --- a/src/components/chart-elements/common/ChartTooltip.tsx +++ b/src/components/chart-elements/common/ChartTooltip.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { Fragment } from "react"; import { tremorTwMerge } from "../../../lib"; import { Color, ValueFormatter } from "../../../lib"; @@ -78,7 +78,7 @@ export interface ChartTooltipProps { label: string; categoryColors: Map; valueFormatter: ValueFormatter; - categories: string[]; + categories?: string[]; forecastCategories?: string[] | string[][]; } @@ -121,23 +121,22 @@ const ChartTooltip = ({
{payload.map(({ value, name }: { value: number; name: string }, idx: number) => ( - <> + { forecastCategories?.flat()?.includes(name) ? ( <> - {(!categories.includes(name) && (payload.length !== (categories.length + (forecastCategories?.flat()?.length ?? 0)))) ? ( + {(!categories?.includes(name) && (payload.length !== ((categories?.length ?? 0) + (forecastCategories?.flat()?.length ?? 0)))) ? ( subArray.indexOf(name) !== -1)] ) ?? BaseColors.Blue} + color={categoryColors.get(categories?.[forecastCategories.findIndex(subArray => subArray.indexOf(name) !== -1)] ?? "") ?? BaseColors.Blue} /> ) : ( null )} ) : ( - ) } - + ))}
diff --git a/src/components/chart-elements/common/utils.ts b/src/components/chart-elements/common/utils.ts index 55c602959..f38813e24 100644 --- a/src/components/chart-elements/common/utils.ts +++ b/src/components/chart-elements/common/utils.ts @@ -20,3 +20,29 @@ export const getYAxisDomain = ( const maxDomain = maxValue ?? "auto"; return [minDomain, maxDomain]; }; + +export const getPercentageWithCategories = (data: any[], categories?: string[] | string [][]): number => { + if(!categories) + return 0; + + const totalObjects = data.length + 1; + let objectsWithCategories = 0; + + for (const obj of data) { + let hasCategoryValue = false; + for (const category of categories.flat()) { + if (obj.hasOwnProperty(category) && obj[category] !== null && obj[category] !== undefined) { + hasCategoryValue = true; + break; + } + } + + if (hasCategoryValue) { + objectsWithCategories++; + } + } + + const percentageWithCategories = (objectsWithCategories / totalObjects); + return percentageWithCategories; + +}; diff --git a/src/stories/chart-elements/LineChart.stories.tsx b/src/stories/chart-elements/LineChart.stories.tsx index 76e9e0dbc..14a5b9894 100644 --- a/src/stories/chart-elements/LineChart.stories.tsx +++ b/src/stories/chart-elements/LineChart.stories.tsx @@ -190,18 +190,33 @@ WithShortAnimationDuration.args = { export const WithSingleForecastLine = DefaultTemplate.bind({}); WithSingleForecastLine.args = { data: simpleBaseChartDataWithForecast, - showAnimation: true, + categories: ["Sales", "Successful Payments"], + forecastCategories: ["Sales Forecast", "Successful Payments Forecast"], + index: "month", +}; + +export const WithSingleForecastLineWithShortAnimationDuration = DefaultTemplate.bind({}); +WithSingleForecastLineWithShortAnimationDuration.args = { + data: simpleBaseChartDataWithForecast, animationDuration: 100, categories: ["Sales", "Successful Payments"], forecastCategories: ["Sales Forecast", "Successful Payments Forecast"], index: "month", }; +export const WithSingleForecastLineWithLongAnimationDuration = DefaultTemplate.bind({}); +WithSingleForecastLineWithLongAnimationDuration.args = { + data: simpleBaseChartDataWithForecast, + animationDuration: 5000, + categories: ["Sales", "Successful Payments"], + forecastCategories: ["Sales Forecast", "Successful Payments Forecast"], + index: "month", +}; + + export const WithMultipleForecastLines = DefaultTemplate.bind({}); WithMultipleForecastLines.args = { data: simpleBaseChartDataWithForecast, - showAnimation: true, - animationDuration: 100, categories: ["Sales", "Successful Payments"], forecastCategories: [["Sales Forecast Max", "Sales Forecast Min"], ["Successful Payments Forecast Max", "Successful Payments Forecast Min"]], index: "month", diff --git a/src/stories/chart-elements/helpers/testData.tsx b/src/stories/chart-elements/helpers/testData.tsx index b8cb46a6d..9e7041f96 100644 --- a/src/stories/chart-elements/helpers/testData.tsx +++ b/src/stories/chart-elements/helpers/testData.tsx @@ -281,5 +281,5 @@ export const simpleBaseChartDataWithForecast = [ "Successful Payments Forecast Max" : 6000 }, - ]; +]; From 053475c4191a47ba460e9c142fc77b6f6ebda940 Mon Sep 17 00:00:00 2001 From: mbauchet Date: Sun, 23 Jul 2023 16:13:07 +0200 Subject: [PATCH 3/4] add custome line style for forecast line --- .../chart-elements/LineChart/LineChart.tsx | 13 ++++++++----- src/components/chart-elements/common/utils.ts | 16 ++++++++++++++-- src/lib/inputTypes.ts | 2 ++ 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/components/chart-elements/LineChart/LineChart.tsx b/src/components/chart-elements/LineChart/LineChart.tsx index c5b1ae6c6..dacccd31e 100644 --- a/src/components/chart-elements/LineChart/LineChart.tsx +++ b/src/components/chart-elements/LineChart/LineChart.tsx @@ -12,7 +12,7 @@ import { } from "recharts"; import { AxisDomain } from "recharts/types/util/types"; -import { constructCategoryColors, getPercentageWithCategories, getYAxisDomain } from "../common/utils"; +import { constructCategoryColors, getForecastStrokeDasharray, getPercentageWithCategories, getYAxisDomain } from "../common/utils"; import NoData from "../common/NoData"; import BaseChartProps from "../common/BaseChartProps"; import ChartLegend from "components/chart-elements/common/ChartLegend"; @@ -26,13 +26,13 @@ import { themeColorRange, tremorTwMerge, } from "lib"; -import { CurveType } from "../../../lib/inputTypes"; +import { CurveType, LineStyle } from "../../../lib/inputTypes"; export interface LineChartProps extends BaseChartProps { curveType?: CurveType; connectNulls?: boolean; forecastCategories?: string[] | string[][]; - forecastAnimationDelay?: number; + forecastLineStyle?: LineStyle; } const LineChart = React.forwardRef((props, ref) => { @@ -40,6 +40,7 @@ const LineChart = React.forwardRef((props, ref) data = [], categories = [], forecastCategories, + forecastLineStyle = "dashed", index, colors = themeColorRange, valueFormatter = defaultValueFormatter, @@ -73,6 +74,8 @@ const LineChart = React.forwardRef((props, ref) const animationDurationPercentage = animationDuration * percentageOfRealDatas const forecastAnimationDurationPercentage = animationDuration * percentageOfForecastedDatas const forecastAnimationDelay = animationDurationPercentage - (animationDurationPercentage / 2.5) + + const forecastStrokeDasharray = getForecastStrokeDasharray(forecastLineStyle); return (
@@ -229,7 +232,7 @@ const LineChart = React.forwardRef((props, ref) strokeWidth={2} strokeLinejoin="round" strokeLinecap="round" - strokeDasharray="5 5" + strokeDasharray={forecastStrokeDasharray} isAnimationActive={showAnimation} animationDuration={forecastAnimationDurationPercentage} animationBegin={forecastAnimationDelay} @@ -267,7 +270,7 @@ const LineChart = React.forwardRef((props, ref) strokeWidth={2} strokeLinejoin="round" strokeLinecap="round" - strokeDasharray="5 5" + strokeDasharray={forecastStrokeDasharray} isAnimationActive={showAnimation} animationDuration={forecastAnimationDurationPercentage} animationBegin={forecastAnimationDelay} diff --git a/src/components/chart-elements/common/utils.ts b/src/components/chart-elements/common/utils.ts index f38813e24..af542e3c0 100644 --- a/src/components/chart-elements/common/utils.ts +++ b/src/components/chart-elements/common/utils.ts @@ -1,4 +1,4 @@ -import { Color } from "../../../lib/inputTypes"; +import { Color, LineStyle } from "../../../lib/inputTypes"; export const constructCategoryColors = ( categories: string[], @@ -44,5 +44,17 @@ export const getPercentageWithCategories = (data: any[], categories?: string[] | const percentageWithCategories = (objectsWithCategories / totalObjects); return percentageWithCategories; - }; + +export const getForecastStrokeDasharray = (forecastLineStyle: LineStyle): string => { + switch (forecastLineStyle) { + case "solid": + return "1"; + case "dashed": + return "5 5"; + case "dotted": + return "0.5 5"; + default: + return "1"; + } +} diff --git a/src/lib/inputTypes.ts b/src/lib/inputTypes.ts index dc93fcd85..35ad46f19 100644 --- a/src/lib/inputTypes.ts +++ b/src/lib/inputTypes.ts @@ -62,3 +62,5 @@ const alignItemsValues = ["start", "end", "center", "baseline", "stretch"] as co export type AlignItems = (typeof alignItemsValues)[number]; export type FlexDirection = "row" | "col" | "row-reverse" | "col-reverse"; + +export type LineStyle = "solid" | "dashed" | "dotted"; From 954d9562b739fb5a9f5e457e3f3cf02f4d05568b Mon Sep 17 00:00:00 2001 From: mbauchet Date: Sun, 23 Jul 2023 16:24:50 +0200 Subject: [PATCH 4/4] add forecast to area chart (to discuss) --- .../chart-elements/AreaChart/AreaChart.tsx | 524 +++++++++++------- .../chart-elements/AreaChart.stories.tsx | 36 +- 2 files changed, 344 insertions(+), 216 deletions(-) diff --git a/src/components/chart-elements/AreaChart/AreaChart.tsx b/src/components/chart-elements/AreaChart/AreaChart.tsx index 27a3c4027..120406046 100644 --- a/src/components/chart-elements/AreaChart/AreaChart.tsx +++ b/src/components/chart-elements/AreaChart/AreaChart.tsx @@ -1,238 +1,332 @@ "use client"; -import React, { useState } from "react"; +import React, { Fragment, useState } from "react"; import { - Area, - CartesianGrid, - Legend, - AreaChart as ReChartsAreaChart, - ResponsiveContainer, - Tooltip, - XAxis, - YAxis, + Area, + CartesianGrid, + Legend, + AreaChart as ReChartsAreaChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, } from "recharts"; import { AxisDomain } from "recharts/types/util/types"; -import { constructCategoryColors, getYAxisDomain } from "../common/utils"; +import { constructCategoryColors, getForecastStrokeDasharray, getYAxisDomain } from "../common/utils"; import BaseChartProps from "../common/BaseChartProps"; import ChartLegend from "../common/ChartLegend"; import ChartTooltip from "../common/ChartTooltip"; import NoData from "../common/NoData"; import { - BaseColors, - defaultValueFormatter, - themeColorRange, - colorPalette, - getColorClassNames, - tremorTwMerge, + BaseColors, + defaultValueFormatter, + themeColorRange, + colorPalette, + getColorClassNames, + tremorTwMerge, } from "lib"; -import { CurveType } from "../../../lib/inputTypes"; +import { CurveType, LineStyle } from "../../../lib/inputTypes"; export interface AreaChartProps extends BaseChartProps { - stack?: boolean; - curveType?: CurveType; - connectNulls?: boolean; + stack?: boolean; + curveType?: CurveType; + connectNulls?: boolean; + forecastCategories?: string[] | string[][]; + forecastLineStyle?: LineStyle; } const AreaChart = React.forwardRef((props, ref) => { - const { - data = [], - categories = [], - index, - stack = false, - colors = themeColorRange, - valueFormatter = defaultValueFormatter, - startEndOnly = false, - showXAxis = true, - showYAxis = true, - yAxisWidth = 56, - showAnimation = true, - animationDuration = 900, - showTooltip = true, - showLegend = true, - showGridLines = true, - showGradient = true, - autoMinValue = false, - curveType = "linear", - minValue, - maxValue, - connectNulls = false, - allowDecimals = true, - noDataText, - className, - ...other - } = props; - const [legendHeight, setLegendHeight] = useState(60); - const categoryColors = constructCategoryColors(categories, colors); + const { + data = [], + categories = [], + forecastCategories, + forecastLineStyle = "dashed", + index, + stack = false, + colors = themeColorRange, + valueFormatter = defaultValueFormatter, + startEndOnly = false, + showXAxis = true, + showYAxis = true, + yAxisWidth = 56, + showAnimation = true, + animationDuration = 900, + showTooltip = true, + showLegend = true, + showGridLines = true, + showGradient = true, + autoMinValue = false, + curveType = "linear", + minValue, + maxValue, + connectNulls = false, + allowDecimals = true, + noDataText, + className, + ...other + } = props; + const [legendHeight, setLegendHeight] = useState(60); + const categoryColors = constructCategoryColors(categories, colors); - const yAxisDomain = getYAxisDomain(autoMinValue, minValue, maxValue); + const yAxisDomain = getYAxisDomain(autoMinValue, minValue, maxValue); - return ( -
- - {data?.length ? ( - - {showGridLines ? ( - - ) : null} - - - {showTooltip ? ( - ( - + const forecastStrokeDasharray = getForecastStrokeDasharray(forecastLineStyle); + + return ( +
+ + {data?.length ? ( + + {showGridLines ? ( + + ) : null} + + + {showTooltip ? ( + ( + + )} + position={{ y: 0 }} + /> + ) : null} + {showLegend ? ( + ChartLegend({ payload }, categoryColors, setLegendHeight, forecastCategories)} + /> + ) : null} + {categories.map((category) => { + return ( + + {showGradient ? ( + + + + + ) : ( + + + + )} + + ); + })} + {categories.map((category) => ( + + ))} + + {forecastCategories ? ( + forecastCategories.map((category, idx) => ( + + {Array.isArray(category) ? ( + <> + {category.map((subCategory) => ( + + ))} + + ) : ( + + )} + + )) + ) : ( + null + )} + + ) : ( + )} - position={{ y: 0 }} - /> - ) : null} - {showLegend ? ( - ChartLegend({ payload }, categoryColors, setLegendHeight)} - /> - ) : null} - {categories.map((category) => { - return ( - - {showGradient ? ( - - - - - ) : ( - - - - )} - - ); - })} - {categories.map((category) => ( - - ))} - - ) : ( - - )} - -
- ); +
+
+ ); }); AreaChart.displayName = "AreaChart"; diff --git a/src/stories/chart-elements/AreaChart.stories.tsx b/src/stories/chart-elements/AreaChart.stories.tsx index 4feb4c27b..086c53978 100644 --- a/src/stories/chart-elements/AreaChart.stories.tsx +++ b/src/stories/chart-elements/AreaChart.stories.tsx @@ -3,7 +3,7 @@ import React from "react"; import { ComponentMeta, ComponentStory } from "@storybook/react"; import { AreaChart, Card, Title } from "components"; -import { simpleBaseChartData as data, simpleBaseChartDataWithNulls } from "./helpers/testData"; +import { simpleBaseChartData as data, simpleBaseChartDataWithForecast, simpleBaseChartDataWithNulls } from "./helpers/testData"; import { valueFormatter } from "./helpers/utils"; // More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export @@ -200,3 +200,37 @@ WithShortAnimationDuration.args = { categories: ["Sales", "Successful Payments"], index: "month", }; + +export const WithSingleForecastArea = DefaultTemplate.bind({}); +WithSingleForecastArea.args = { + data: simpleBaseChartDataWithForecast, + categories: ["Sales", "Successful Payments"], + forecastCategories: ["Sales Forecast", "Successful Payments Forecast"], + index: "month", +}; + +export const WithSingleForecastAreaWithShortAnimationDuration = DefaultTemplate.bind({}); +WithSingleForecastAreaWithShortAnimationDuration.args = { + data: simpleBaseChartDataWithForecast, + animationDuration: 100, + categories: ["Sales", "Successful Payments"], + forecastCategories: ["Sales Forecast", "Successful Payments Forecast"], + index: "month", +}; + +export const WithSingleForecastAreaWithLongAnimationDuration = DefaultTemplate.bind({}); +WithSingleForecastAreaWithLongAnimationDuration.args = { + data: simpleBaseChartDataWithForecast, + animationDuration: 5000, + categories: ["Sales", "Successful Payments"], + forecastCategories: ["Sales Forecast", "Successful Payments Forecast"], + index: "month", +}; + +export const WithMultipleForecastAreas = DefaultTemplate.bind({}); +WithMultipleForecastAreas.args = { + data: simpleBaseChartDataWithForecast, + categories: ["Sales", "Successful Payments"], + forecastCategories: [["Sales Forecast Max", "Sales Forecast Min"], ["Successful Payments Forecast Max", "Successful Payments Forecast Min"]], + index: "month", +};