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 radar chart component #567

Open
wants to merge 2 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
191 changes: 191 additions & 0 deletions src/components/chart-elements/RadarChart/RadarChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
"use client";
import React, { useState } from "react";
import {
Area,
CartesianGrid,
Legend,
Radar,
RadarChart as ReChartsRadarChart,
PolarGrid,
PolarAngleAxis,
ResponsiveContainer,
Tooltip,
} from "recharts";


import { constructCategoryColors } from "../common/utils";
import ChartLegend from "../common/ChartLegend";
import ChartTooltip from "../common/ChartTooltip";
import NoData from "../common/NoData";

import {
BaseColors,
defaultValueFormatter,
themeColorRange,
colorPalette,
getColorClassNames,
tremorTwMerge,
} from "lib";

export interface RadarChartProps extends BaseAnimationTimingProps {
data: any[];
category?: string;
index?: string;
colors?: Color[];
valueFormatter?: ValueFormatter;
outerRadius?: string | number;
dashArray?: boolean;
showAnimation?: boolean;
showTooltip?: boolean;
noDataText?: string;
showTooltip?: boolean;
showLegend?: boolean;
showGridLines?: boolean;
showGradient?: boolean;
noDataText?: string;
className?: string;
}

const RadarChart = React.forwardRef<HTMLDivElement, RadarChartProps>((props, ref) => {
const {
data = [],
categories = [],
index,
colors = themeColorRange,
valueFormatter = defaultValueFormatter,
dashArray = false,
outerRadius = "80%",
showAnimation = true,
animationDuration = 900,
showTooltip = true,
showLegend = true,
showGridLines = true,
showGradient = false,
noDataText,
className,
...other
} = props;
const [legendHeight, setLegendHeight] = useState(60);
const categoryColors = constructCategoryColors(categories, colors);

return (
<div ref={ref} className={tremorTwMerge("w-full h-80", className)} {...other}>
<ResponsiveContainer className="h-full w-full">
{data?.length ? (
<ReChartsRadarChart data={data} outerRadius={outerRadius}>
{showGridLines ? (
<PolarGrid
className={tremorTwMerge(
// common
"stroke-1",
// light
"stroke-tremor-content-subtle",
// dark
"dark:stroke-dark-tremor-content-subtle",
)}
strokeDasharray={dashArray ? "3 3" : ""}
horizontal={true}
vertical={false}
/>
) : null}
{showTooltip ? (
<Tooltip
wrapperStyle={{ outline: "none" }}
isAnimationActive={false}
cursor={{ stroke: "#d1d5db", strokeWidth: 1 }} // @achi @severin
content={({ active, payload, label }) => (
<ChartTooltip
active={active}
payload={payload}
label={label}
valueFormatter={valueFormatter}
categoryColors={categoryColors}
/>
)}
position={{ y: 0 }}
/>
) : null}
{showLegend ? (
<Legend
verticalAlign="top"
height={legendHeight}
content={({ payload }) => ChartLegend({ payload }, categoryColors, setLegendHeight)}
/>
) : null}
{
categories.length > 0 ? (
categories.map((category) => {
return(
<>
<defs key={category}>
{showGradient ? (
<linearGradient
className={
getColorClassNames(
categoryColors.get(category) ?? BaseColors.Gray,
colorPalette.text,
).textColor
}
id={categoryColors.get(category)}
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop offset="5%" stopColor="currentColor" stopOpacity={0.4} />
<stop offset="95%" stopColor="currentColor" stopOpacity={0} />
</linearGradient>
) : (
<linearGradient
className={
getColorClassNames(
categoryColors.get(category) ?? BaseColors.Gray,
colorPalette.text,
).textColor
}
id={categoryColors.get(category)}
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop stopColor="currentColor" stopOpacity={0.3} />
</linearGradient>
)}
</defs>
<Radar
className={
getColorClassNames(
categoryColors.get(category) ?? BaseColors.Gray,
colorPalette.text,
).strokeColor
}
dot={false}
key={category}
name={category}
dataKey={category}
stroke=""
fill={`url(#${categoryColors.get(category)})`}
isAnimationActive={showAnimation}
animationDuration={animationDuration}
/>
<PolarAngleAxis dataKey={index} />
</>
)
})
) : (
<Radar/>
)
}
</ReChartsRadarChart>
) : (
<NoData noDataText={noDataText} />
)}
</ResponsiveContainer>
</div>
);
});

RadarChart.displayName = "RadarChart";

export default RadarChart;
2 changes: 2 additions & 0 deletions src/components/chart-elements/RadarChart/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as RadarChart } from "./RadarChart";
export type { RadarChartProps } from "./RadarChart";
1 change: 1 addition & 0 deletions src/components/chart-elements/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from "./AreaChart";
export * from "./BarChart";
export * from "./LineChart";
export * from "./DonutChart";
export * from "./RadarChart"
152 changes: 152 additions & 0 deletions src/stories/chart-elements/RadarChart.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import React from "react";

import { ComponentMeta, ComponentStory } from "@storybook/react";

import { RadarChart, Card, Title } from "components";
import { simpleBaseChartData as data } from "./helpers/testData";
import { valueFormatter } from "./helpers/utils";

// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
export default {
title: "Tremor/ChartElements/RadarChart",
component: RadarChart,
} as ComponentMeta<typeof RadarChart>;
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args

const ResponsiveTemplate: ComponentStory<typeof RadarChart> = (args) => (
<>
<Title>Mobile</Title>
<div className="w-64">
<Card>
<RadarChart {...args} />
</Card>
</div>
<Title className="mt-5">Desktop</Title>
<Card>
<RadarChart {...args} />
</Card>
</>
);

const DefaultTemplate: ComponentStory<typeof RadarChart> = ({ ...args }) => (
<Card>
<RadarChart {...args} />
</Card>
);

const args = { categories: ["Sales", "Successful Payments"], index: "month" };

export const DefaultResponsive = ResponsiveTemplate.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args
DefaultResponsive.args = {
...args,
data,
};

export const WithValueFormatter = ResponsiveTemplate.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args
WithValueFormatter.args = {
...args,
data,
valueFormatter: valueFormatter,
};

export const WithDashArray = ResponsiveTemplate.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args
WithDashArray.args = {
...args,
data,
dashArray: true
};

export const WithCustomOuterRadius = ResponsiveTemplate.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args
WithCustomOuterRadius.args = {
...args,
data,
outerRadius: "30%",
};

export const WithCustomColors = DefaultTemplate.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args
WithCustomColors.args = {
...args,
data,
colors: ["blue", "green"],
};

export const WithGradient = DefaultTemplate.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args
WithGradient.args = {
...args,
data,
showGradient: true,
};

export const WithLessColorsThanCategories = DefaultTemplate.bind({});
WithLessColorsThanCategories.args = {
...args,
data,
colors: ["green"],
};

export const WithLongValues = ResponsiveTemplate.bind({});
WithLongValues.args = {
...args,
data,
categories: ["This is an edge case"],
valueFormatter: valueFormatter,
};

export const WithMultipleCategories = ResponsiveTemplate.bind({});
WithMultipleCategories.args = {
...args,
data,
categories: ["Sales", "Successful Payments", "This is an edge case", "Test"],
valueFormatter: valueFormatter,
};

export const WithNoData = DefaultTemplate.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args
WithNoData.args = {
...args,
};

export const WithNoDataText = DefaultTemplate.bind({});
WithNoDataText.args = {
...args,
noDataText: "No data, try again later.",
};

export const WithNoCategories = DefaultTemplate.bind({});
WithNoCategories.args = {
data,
index: "month",
};

export const WithNoIndex = DefaultTemplate.bind({});
WithNoIndex.args = {
data,
categories: ["Sales", "Successful Payments"],
};

export const WithNoAnimation = DefaultTemplate.bind({});
WithNoAnimation.args = {
...args,
data,
showAnimation: false
};

export const WithLongAnimationDuration = DefaultTemplate.bind({});
WithLongAnimationDuration.args = {
...args,
data,
animationDuration: 5000,
};

export const WithShortAnimationDuration = DefaultTemplate.bind({});
WithShortAnimationDuration.args = {
...args,
data,
animationDuration: 100,
};