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/components/chart-elements/LineChart/LineChart.tsx b/src/components/chart-elements/LineChart/LineChart.tsx index 5c1e6819c..dacccd31e 100644 --- a/src/components/chart-elements/LineChart/LineChart.tsx +++ b/src/components/chart-elements/LineChart/LineChart.tsx @@ -1,194 +1,294 @@ "use client"; -import React, { useState } from "react"; +import React, { Fragment, 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"; -import { constructCategoryColors, 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"; 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"; +import { CurveType, LineStyle } from "../../../lib/inputTypes"; export interface LineChartProps extends BaseChartProps { - curveType?: CurveType; - connectNulls?: boolean; + curveType?: CurveType; + connectNulls?: boolean; + forecastCategories?: string[] | string[][]; + forecastLineStyle?: LineStyle; } 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, + forecastLineStyle = "dashed", + 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); + + const percentageOfRealDatas = forecastCategories ? getPercentageWithCategories(data, categories) : 1; + const percentageOfForecastedDatas = forecastCategories ? getPercentageWithCategories(data, forecastCategories) : 0; - return ( -
- - {data?.length ? ( - - {showGridLines ? ( - - ) : null} - - - {showTooltip ? ( - ( - + const animationDurationPercentage = animationDuration * percentageOfRealDatas + const forecastAnimationDurationPercentage = animationDuration * percentageOfForecastedDatas + const forecastAnimationDelay = animationDurationPercentage - (animationDurationPercentage / 2.5) + + const forecastStrokeDasharray = getForecastStrokeDasharray(forecastLineStyle); + + return ( +
+ + {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 + )} + + ) : ( + )} - 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..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,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,32 @@ const ChartTooltip = ({
{payload.map(({ value, name }: { value: number; name: string }, idx: number) => ( - - ))} + + { + forecastCategories?.flat()?.includes(name) ? ( + <> + {(!categories?.includes(name) && (payload.length !== ((categories?.length ?? 0) + (forecastCategories?.flat()?.length ?? 0)))) ? ( + 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..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[], @@ -20,3 +20,41 @@ 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; +}; + +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"; 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", +}; diff --git a/src/stories/chart-elements/LineChart.stories.tsx b/src/stories/chart-elements/LineChart.stories.tsx index 7dfa4b300..14a5b9894 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,38 @@ WithShortAnimationDuration.args = { categories: ["Sales", "Successful Payments"], index: "month", }; + +export const WithSingleForecastLine = DefaultTemplate.bind({}); +WithSingleForecastLine.args = { + data: simpleBaseChartDataWithForecast, + 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, + 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..9e7041f96 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 + }, + +]; +