-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: introduced stacked bar chart and tree map chart. (#6305)
- Loading branch information
1 parent
873e433
commit d6bcd8d
Showing
10 changed files
with
460 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
export type TStackItem<T extends string> = { | ||
key: T; | ||
fillClassName: string; | ||
textClassName: string; | ||
dotClassName?: string; | ||
showPercentage?: boolean; | ||
}; | ||
|
||
export type TStackChartData<K extends string, T extends string> = { | ||
[key in K]: string | number; | ||
} & Record<T, any>; | ||
|
||
export type TStackedBarChartProps<K extends string, T extends string> = { | ||
data: TStackChartData<K, T>[]; | ||
stacks: TStackItem<T>[]; | ||
xAxis: { | ||
key: keyof TStackChartData<K, T>; | ||
label: string; | ||
}; | ||
yAxis: { | ||
key: keyof TStackChartData<K, T>; | ||
label: string; | ||
domain?: [number, number]; | ||
allowDecimals?: boolean; | ||
}; | ||
barSize?: number; | ||
className?: string; | ||
tickCount?: { | ||
x?: number; | ||
y?: number; | ||
}; | ||
showTooltip?: boolean; | ||
}; | ||
|
||
export type TreeMapItem = { | ||
name: string; | ||
value: number; | ||
textClassName?: string; | ||
icon?: React.ReactElement; | ||
} & ( | ||
| { | ||
fillColor: string; | ||
} | ||
| { | ||
fillClassName: string; | ||
} | ||
); | ||
|
||
export type TreeMapChartProps = { | ||
data: TreeMapItem[]; | ||
className?: string; | ||
isAnimationActive?: boolean; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
import React from "react"; | ||
// plane imports | ||
import { TStackChartData } from "@plane/types"; | ||
import { cn } from "@plane/utils"; | ||
|
||
// Helper to calculate percentage | ||
const calculatePercentage = <K extends string, T extends string>( | ||
data: TStackChartData<K, T>, | ||
stackKeys: T[], | ||
currentKey: T | ||
): number => { | ||
const total = stackKeys.reduce((sum, key) => sum + data[key], 0); | ||
return total === 0 ? 0 : Math.round((data[currentKey] / total) * 100); | ||
}; | ||
|
||
export const CustomStackBar = React.memo<any>((props: any) => { | ||
const { fill, x, y, width, height, dataKey, stackKeys, payload, textClassName, showPercentage } = props; | ||
// Calculate text position | ||
const MIN_BAR_HEIGHT_FOR_INTERNAL = 14; // Minimum height needed to show text inside | ||
const TEXT_PADDING = Math.min(6, Math.abs(MIN_BAR_HEIGHT_FOR_INTERNAL - height / 2)); | ||
const textY = y + height - TEXT_PADDING; // Position inside bar if tall enough | ||
// derived values | ||
const RADIUS = 2; | ||
const currentBarPercentage = calculatePercentage(payload, stackKeys, dataKey); | ||
|
||
if (!height) return null; | ||
return ( | ||
<g> | ||
<path | ||
d={` | ||
M${x + RADIUS},${y + height} | ||
L${x + RADIUS},${y} | ||
Q${x},${y} ${x},${y + RADIUS} | ||
L${x},${y + height - RADIUS} | ||
Q${x},${y + height} ${x + RADIUS},${y + height} | ||
L${x + width - RADIUS},${y + height} | ||
Q${x + width},${y + height} ${x + width},${y + height - RADIUS} | ||
L${x + width},${y + RADIUS} | ||
Q${x + width},${y} ${x + width - RADIUS},${y} | ||
L${x + RADIUS},${y} | ||
`} | ||
className={cn("transition-colors duration-200", fill)} | ||
fill="currentColor" | ||
/> | ||
{showPercentage && | ||
height >= MIN_BAR_HEIGHT_FOR_INTERNAL && | ||
currentBarPercentage !== undefined && | ||
!Number.isNaN(currentBarPercentage) && ( | ||
<text | ||
x={x + width / 2} | ||
y={textY} | ||
textAnchor="middle" | ||
className={cn("text-xs font-medium", textClassName)} | ||
fill="currentColor" | ||
> | ||
{currentBarPercentage}% | ||
</text> | ||
)} | ||
</g> | ||
); | ||
}); | ||
CustomStackBar.displayName = "CustomStackBar"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./root"; |
130 changes: 130 additions & 0 deletions
130
web/core/components/core/charts/stacked-bar-chart/root.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
"use client"; | ||
|
||
import React from "react"; | ||
import { BarChart, Bar, XAxis, YAxis, ResponsiveContainer, Tooltip } from "recharts"; | ||
// plane imports | ||
import { TStackedBarChartProps } from "@plane/types"; | ||
import { cn } from "@plane/utils"; | ||
// local components | ||
import { CustomStackBar } from "./bar"; | ||
import { CustomXAxisTick, CustomYAxisTick } from "./tick"; | ||
import { CustomTooltip } from "./tooltip"; | ||
|
||
// Common classnames | ||
const LABEL_CLASSNAME = "uppercase text-custom-text-300/60 text-sm tracking-wide"; | ||
const AXIS_LINE_CLASSNAME = "text-custom-text-400/70"; | ||
|
||
export const StackedBarChart = React.memo( | ||
<K extends string, T extends string>({ | ||
data, | ||
stacks, | ||
xAxis, | ||
yAxis, | ||
barSize = 40, | ||
className = "w-full h-96", | ||
tickCount = { | ||
x: undefined, | ||
y: 10, | ||
}, | ||
showTooltip = true, | ||
}: TStackedBarChartProps<K, T>) => { | ||
// derived values | ||
const stackKeys = React.useMemo(() => stacks.map((stack) => stack.key), [stacks]); | ||
const stackDotClassNames = React.useMemo( | ||
() => stacks.reduce((acc, stack) => ({ ...acc, [stack.key]: stack.dotClassName }), {}), | ||
[stacks] | ||
); | ||
|
||
const renderBars = React.useMemo( | ||
() => | ||
stacks.map((stack) => ( | ||
<Bar | ||
key={stack.key} | ||
dataKey={stack.key} | ||
stackId="a" | ||
fill={stack.fillClassName} | ||
shape={(props: any) => ( | ||
<CustomStackBar | ||
{...props} | ||
stackKeys={stackKeys} | ||
textClassName={stack.textClassName} | ||
showPercentage={stack.showPercentage} | ||
/> | ||
)} | ||
/> | ||
)), | ||
[stackKeys, stacks] | ||
); | ||
|
||
return ( | ||
<div className={cn(className)}> | ||
<ResponsiveContainer width="100%" height="100%"> | ||
<BarChart | ||
data={data} | ||
margin={{ top: 10, right: 10, left: 10, bottom: 40 }} | ||
barSize={barSize} | ||
className="recharts-wrapper" | ||
> | ||
<XAxis | ||
dataKey={xAxis.key} | ||
tick={(props) => <CustomXAxisTick {...props} />} | ||
tickLine={{ | ||
stroke: "currentColor", | ||
className: AXIS_LINE_CLASSNAME, | ||
}} | ||
axisLine={{ | ||
stroke: "currentColor", | ||
className: AXIS_LINE_CLASSNAME, | ||
}} | ||
label={{ | ||
value: xAxis.label, | ||
dy: 28, | ||
className: LABEL_CLASSNAME, | ||
}} | ||
tickCount={tickCount.x} | ||
/> | ||
<YAxis | ||
domain={yAxis.domain} | ||
tickLine={{ | ||
stroke: "currentColor", | ||
className: AXIS_LINE_CLASSNAME, | ||
}} | ||
axisLine={{ | ||
stroke: "currentColor", | ||
className: AXIS_LINE_CLASSNAME, | ||
}} | ||
label={{ | ||
value: yAxis.label, | ||
angle: -90, | ||
position: "bottom", | ||
offset: -24, | ||
dx: -16, | ||
className: LABEL_CLASSNAME, | ||
}} | ||
tick={(props) => <CustomYAxisTick {...props} />} | ||
tickCount={tickCount.y} | ||
allowDecimals={yAxis.allowDecimals ?? false} | ||
/> | ||
{showTooltip && ( | ||
<Tooltip | ||
cursor={{ fill: "currentColor", className: "text-custom-background-90/80 cursor-pointer" }} | ||
content={({ active, label, payload }) => ( | ||
<CustomTooltip | ||
active={active} | ||
label={label} | ||
payload={payload} | ||
stackKeys={stackKeys} | ||
stackDotClassNames={stackDotClassNames} | ||
/> | ||
)} | ||
/> | ||
)} | ||
{renderBars} | ||
</BarChart> | ||
</ResponsiveContainer> | ||
</div> | ||
); | ||
} | ||
); | ||
StackedBarChart.displayName = "StackedBarChart"; |
23 changes: 23 additions & 0 deletions
23
web/core/components/core/charts/stacked-bar-chart/tick.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
import React from "react"; | ||
|
||
// Common classnames | ||
const AXIS_TICK_CLASSNAME = "fill-custom-text-400 text-sm capitalize"; | ||
|
||
export const CustomXAxisTick = React.memo<any>(({ x, y, payload }: any) => ( | ||
<g transform={`translate(${x},${y})`}> | ||
<text y={0} dy={16} textAnchor="middle" className={AXIS_TICK_CLASSNAME}> | ||
{payload.value} | ||
</text> | ||
</g> | ||
)); | ||
CustomXAxisTick.displayName = "CustomXAxisTick"; | ||
|
||
export const CustomYAxisTick = React.memo<any>(({ x, y, payload }: any) => ( | ||
<g transform={`translate(${x},${y})`}> | ||
<text dx={-10} textAnchor="middle" className={AXIS_TICK_CLASSNAME}> | ||
{payload.value} | ||
</text> | ||
</g> | ||
)); | ||
CustomYAxisTick.displayName = "CustomYAxisTick"; |
39 changes: 39 additions & 0 deletions
39
web/core/components/core/charts/stacked-bar-chart/tooltip.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
import React from "react"; | ||
// plane imports | ||
import { Card, ECardSpacing } from "@plane/ui"; | ||
import { cn } from "@plane/utils"; | ||
|
||
type TStackedBarChartProps = { | ||
active: boolean | undefined; | ||
label: string | undefined; | ||
payload: any[] | undefined; | ||
stackKeys: string[]; | ||
stackDotClassNames: Record<string, string>; | ||
}; | ||
|
||
export const CustomTooltip = React.memo( | ||
({ active, label, payload, stackKeys, stackDotClassNames }: TStackedBarChartProps) => { | ||
// derived values | ||
const filteredPayload = payload?.filter((item: any) => item.dataKey && stackKeys.includes(item.dataKey)); | ||
|
||
if (!active || !filteredPayload || !filteredPayload.length) return null; | ||
return ( | ||
<Card className="flex flex-col" spacing={ECardSpacing.SM}> | ||
<p className="text-xs text-custom-text-100 font-medium border-b border-custom-border-200 pb-2 capitalize"> | ||
{label} | ||
</p> | ||
{filteredPayload.map((item: any) => ( | ||
<div key={item?.dataKey} className="flex items-center gap-2 text-xs capitalize"> | ||
{stackDotClassNames[item?.dataKey] && ( | ||
<div className={cn("size-2 rounded-full", stackDotClassNames[item?.dataKey])} /> | ||
)} | ||
<span className="text-custom-text-300">{item?.name}:</span> | ||
<span className="font-medium text-custom-text-200">{item?.value}</span> | ||
</div> | ||
))} | ||
</Card> | ||
); | ||
} | ||
); | ||
CustomTooltip.displayName = "CustomTooltip"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./root"; |
Oops, something went wrong.