Files
plane/packages/propel/src/charts/bar-chart/root.tsx
chuan 8ebde8aa05
Some checks failed
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Admin Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Codespell / Check for spelling errors (push) Has been cancelled
Sync Repositories / sync_changes (push) Has been cancelled
Initial commit: Plane
Synced from upstream: 8853637e981ed7d8a6cff32bd98e7afe20f54362
2025-11-07 00:00:52 +08:00

210 lines
6.3 KiB
TypeScript

/* eslint-disable @typescript-eslint/no-explicit-any */
"use client";
import React, { useCallback, useMemo, useState } from "react";
import {
BarChart as CoreBarChart,
Bar,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
Legend,
CartesianGrid,
} from "recharts";
// plane imports
import { AXIS_LABEL_CLASSNAME } from "@plane/constants";
import { TBarChartProps } from "@plane/types";
// local components
import { getLegendProps } from "../components/legend";
import { CustomXAxisTick, CustomYAxisTick } from "../components/tick";
import { CustomTooltip } from "../components/tooltip";
import { barShapeVariants } from "./bar";
const DEFAULT_BAR_FILL_COLOR = "#000000";
export const BarChart = React.memo(<K extends string, T extends string>(props: TBarChartProps<K, T>) => {
const {
data,
bars,
xAxis,
yAxis,
barSize = 40,
className,
legend,
margin,
tickCount = {
x: undefined,
y: 10,
},
customTicks,
showTooltip = true,
customTooltipContent,
} = props;
// states
const [activeBar, setActiveBar] = useState<string | null>(null);
const [activeLegend, setActiveLegend] = useState<string | null>(null);
// derived values
const { stackKeys, stackLabels } = useMemo(() => {
const keys: string[] = [];
const labels: Record<string, string> = {};
for (const bar of bars) {
keys.push(bar.key);
labels[bar.key] = bar.label;
}
return { stackKeys: keys, stackLabels: labels };
}, [bars]);
// get bar color dynamically based on payload
const getBarColor = useCallback(
(payload: Record<string, string>[], barKey: string) => {
const bar = bars.find((b) => b.key === barKey);
if (!bar) return DEFAULT_BAR_FILL_COLOR;
if (typeof bar.fill === "function") {
const payloadItem = payload?.find((item) => item.dataKey === barKey);
if (payloadItem?.payload) {
try {
return bar.fill(payloadItem.payload);
} catch (error) {
console.error(error);
return DEFAULT_BAR_FILL_COLOR;
}
} else {
return DEFAULT_BAR_FILL_COLOR; // fallback color when no payload data
}
} else {
return bar.fill;
}
},
[bars]
);
// get all bar colors
const getAllBarColors = useCallback(
(payload: any[]) => {
const colors: Record<string, string> = {};
for (const bar of bars) {
colors[bar.key] = getBarColor(payload, bar.key);
}
return colors;
},
[bars, getBarColor]
);
const renderBars = useMemo(
() =>
bars.map((bar) => (
<Bar
key={bar.key}
dataKey={bar.key}
stackId={bar.stackId}
opacity={!!activeLegend && activeLegend !== bar.key ? 0.1 : 1}
shape={(shapeProps: any) => {
const shapeVariant = barShapeVariants[bar.shapeVariant ?? "bar"];
const node = shapeVariant(shapeProps, bar, stackKeys);
return React.isValidElement(node) ? node : <>{node}</>;
}}
className="[&_path]:transition-opacity [&_path]:duration-200"
onMouseEnter={() => setActiveBar(bar.key)}
onMouseLeave={() => setActiveBar(null)}
fill={getBarColor(data, bar.key)}
/>
)),
[activeLegend, stackKeys, bars, getBarColor, data]
);
return (
<div className={className}>
<ResponsiveContainer width="100%" height="100%">
<CoreBarChart
data={data}
margin={{
top: margin?.top === undefined ? 5 : margin.top,
right: margin?.right === undefined ? 30 : margin.right,
bottom: margin?.bottom === undefined ? 5 : margin.bottom,
left: margin?.left === undefined ? 20 : margin.left,
}}
barSize={barSize}
className="recharts-wrapper"
>
<CartesianGrid stroke="rgba(var(--color-border-100), 0.8)" vertical={false} />
<XAxis
dataKey={xAxis.key}
tick={(props) => {
const TickComponent = customTicks?.x || CustomXAxisTick;
return <TickComponent {...props} />;
}}
tickLine={false}
axisLine={false}
label={{
value: xAxis.label,
dy: xAxis.dy ?? 28,
className: AXIS_LABEL_CLASSNAME,
}}
tickCount={tickCount.x}
/>
<YAxis
domain={yAxis.domain}
tickLine={false}
axisLine={false}
label={{
value: yAxis.label,
angle: -90,
position: "bottom",
offset: yAxis.offset ?? -24,
dx: yAxis.dx ?? -16,
className: AXIS_LABEL_CLASSNAME,
}}
tick={(props) => {
const TickComponent = customTicks?.y || CustomYAxisTick;
return <TickComponent {...props} />;
}}
tickCount={tickCount.y}
allowDecimals={!!yAxis.allowDecimals}
/>
{legend && (
// @ts-expect-error recharts types are not up to date
<Legend
onMouseEnter={(payload) => setActiveLegend(payload.value)}
onMouseLeave={() => setActiveLegend(null)}
formatter={(value) => stackLabels[value]}
{...getLegendProps(legend)}
/>
)}
{showTooltip && (
<Tooltip
cursor={{
fill: "currentColor",
className: "text-custom-background-90/80 cursor-pointer",
}}
wrapperStyle={{
pointerEvents: "auto",
}}
content={({ active, label, payload }) => {
if (customTooltipContent) return customTooltipContent({ active, label, payload });
return (
<CustomTooltip
active={active}
label={label}
payload={payload}
activeKey={activeBar}
itemKeys={stackKeys}
itemLabels={stackLabels}
itemDotColors={getAllBarColors(payload || [])}
/>
);
}}
/>
)}
{renderBars}
</CoreBarChart>
</ResponsiveContainer>
</div>
);
});
BarChart.displayName = "BarChart";