Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add forecast #579

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
524 changes: 309 additions & 215 deletions src/components/chart-elements/AreaChart/AreaChart.tsx

Large diffs are not rendered by default.

442 changes: 271 additions & 171 deletions src/components/chart-elements/LineChart/LineChart.tsx

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions src/components/chart-elements/common/ChartLegend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ const ChartLegend = (
{ payload }: any,
categoryColors: Map<string, Color>,
setLegendHeight: React.Dispatch<React.SetStateAction<number>>,
) => {
forecastCategories?: string[] | string[][]
) => {
const legendRef = useRef<HTMLDivElement>(null);

useOnWindowResize(() => {
Expand All @@ -23,8 +24,8 @@ const ChartLegend = (
return (
<div ref={legendRef} className="flex items-center justify-end">
<Legend
categories={payload.map((entry: any) => 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))}
/>
</div>
);
Expand Down
39 changes: 31 additions & 8 deletions src/components/chart-elements/common/ChartTooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react";
import React, { Fragment } from "react";
import { tremorTwMerge } from "../../../lib";

import { Color, ValueFormatter } from "../../../lib";
Expand Down Expand Up @@ -78,6 +78,8 @@ export interface ChartTooltipProps {
label: string;
categoryColors: Map<string, Color>;
valueFormatter: ValueFormatter;
categories?: string[];
forecastCategories?: string[] | string[][];
}

const ChartTooltip = ({
Expand All @@ -86,6 +88,8 @@ const ChartTooltip = ({
label,
categoryColors,
valueFormatter,
categories,
forecastCategories,
}: ChartTooltipProps) => {
if (active && payload) {
return (
Expand Down Expand Up @@ -117,13 +121,32 @@ const ChartTooltip = ({

<div className={tremorTwMerge(spacing.twoXl.paddingX, spacing.sm.paddingY, "space-y-1")}>
{payload.map(({ value, name }: { value: number; name: string }, idx: number) => (
<ChartTooltipRow
key={`id-${idx}`}
value={valueFormatter(value)}
name={name}
color={categoryColors.get(name) ?? BaseColors.Blue}
/>
))}
<Fragment key={idx}>
{
forecastCategories?.flat()?.includes(name) ? (
<>
{(!categories?.includes(name) && (payload.length !== ((categories?.length ?? 0) + (forecastCategories?.flat()?.length ?? 0)))) ? (
<ChartTooltipRow
key={`id-${idx}`}
value={valueFormatter(value)}
name={name}
color={categoryColors.get(categories?.[forecastCategories.findIndex(subArray => subArray.indexOf(name) !== -1)] ?? "") ?? BaseColors.Blue}
/>
) : (
null
)}
</>
) : (
<ChartTooltipRow
key={`id-${idx}`}
value={valueFormatter(value)}
name={name}
color={categoryColors.get(name) ?? BaseColors.Blue}
/>
)
}
</Fragment>
))}
</div>
</ChartTooltipFrame>
);
Expand Down
40 changes: 39 additions & 1 deletion src/components/chart-elements/common/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Color } from "../../../lib/inputTypes";
import { Color, LineStyle } from "../../../lib/inputTypes";

export const constructCategoryColors = (
categories: string[],
Expand All @@ -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;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain why you are adding 1 here? Maybe I am missing something obvious but it looks incorrect? 🤔


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++;
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that you can write the above code (from line 29) as follows:

const objectsWithCategories = data.reduce((count, obj) => 
  count + categories.flat().some(c => obj.hasOwnProperty(c) && obj[c] !== null && obj[c] !== undefined), 
  0
)

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";
}
}
2 changes: 2 additions & 0 deletions src/lib/inputTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
36 changes: 35 additions & 1 deletion src/stories/chart-elements/AreaChart.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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",
};
37 changes: 36 additions & 1 deletion src/stories/chart-elements/LineChart.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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",
};
145 changes: 145 additions & 0 deletions src/stories/chart-elements/helpers/testData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
},

];

Loading