Files
prowler/ui/components/graphs/donut-chart.tsx
T
Alan Buscaglia e5364911bb feat: implement comprehensive light/dark theme support for all chart components
- Add CSS variables to all chart tooltips and components for dynamic theme switching
- Update AlertPill component to use theme-aware colors (--chart-alert-bg, --chart-alert-text)
- Change all alert icons to use red color (--chart-fail) consistently across charts
- Add ScatterDataPoint type to centralized types.ts
- Fix scatter-plot-with-selection component type definitions and imports
- Migrate shadcn Select component to shared folder structure
- Update ThreatMap to use shadcn Select instead of native select element
- Ensure all graph components dynamically update colors when theme toggles

This enables seamless light/dark theme support across:
- ChartTooltip and MultiSeriesChartTooltip
- LineChart, RadarChart, ScatterPlot custom tooltips
- AlertPill and icon coloring
- All form elements now use shadcn components
2025-10-22 10:19:12 +02:00

189 lines
5.2 KiB
TypeScript

"use client";
import { useState } from "react";
import { Cell, Label, Pie, PieChart, Tooltip } from "recharts";
import { ChartConfig, ChartContainer } from "@/components/ui/chart/Chart";
import { ChartLegend } from "./shared/chart-legend";
import { DonutDataPoint } from "./types";
interface DonutChartProps {
data: DonutDataPoint[];
height?: number;
innerRadius?: number;
outerRadius?: number;
showLegend?: boolean;
centerLabel?: {
value: string | number;
label: string;
};
}
const CustomTooltip = ({ active, payload }: any) => {
if (active && payload && payload.length) {
const data = payload[0].payload;
return (
<div
className="rounded-lg border p-3 shadow-lg"
style={{
backgroundColor: "var(--chart-background)",
borderColor: "var(--chart-border-emphasis)",
}}
>
<div className="flex items-center gap-2">
<div
className="h-3 w-3 rounded-sm"
style={{ backgroundColor: data.color }}
/>
<span
className="text-sm font-semibold"
style={{ color: "var(--chart-text-primary)" }}
>
{data.percentage}% {data.name}
</span>
</div>
{data.change !== undefined && (
<p
className="mt-2 text-xs"
style={{ color: "var(--chart-text-secondary)" }}
>
<span className="font-bold">
{data.change > 0 ? "+" : ""}
{data.change}%
</span>{" "}
Since last scan
</p>
)}
</div>
);
}
return null;
};
const CustomLegend = ({ payload }: any) => {
const items = payload.map((entry: any) => ({
label: `${entry.value} (${entry.payload.percentage}%)`,
color: entry.color,
}));
return <ChartLegend items={items} />;
};
export function DonutChart({
data,
innerRadius = 80,
outerRadius = 120,
showLegend = true,
centerLabel,
}: DonutChartProps) {
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
const chartConfig = data.reduce(
(config, item) => ({
...config,
[item.name]: {
label: item.name,
color: item.color,
},
}),
{} as ChartConfig,
);
const chartData = data.map((item) => ({
name: item.name,
value: item.value,
fill: item.color,
color: item.color,
percentage: item.percentage,
change: item.change,
}));
const legendPayload = chartData.map((entry) => ({
value: entry.name,
color: entry.color,
payload: {
percentage: entry.percentage,
},
}));
return (
<div>
<ChartContainer
config={chartConfig}
className="mx-auto aspect-square max-h-[350px]"
>
<PieChart>
<Tooltip content={<CustomTooltip />} />
<Pie
data={chartData}
dataKey="value"
nameKey="name"
innerRadius={innerRadius}
outerRadius={outerRadius}
strokeWidth={0}
paddingAngle={2}
>
{chartData.map((entry, index) => {
const opacity =
hoveredIndex === null ? 1 : hoveredIndex === index ? 1 : 0.5;
return (
<Cell
key={`cell-${index}`}
fill={entry.fill}
opacity={opacity}
style={{ transition: "opacity 0.2s" }}
onMouseEnter={() => setHoveredIndex(index)}
onMouseLeave={() => setHoveredIndex(null)}
/>
);
})}
{centerLabel && (
<Label
content={({ viewBox }) => {
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
const formattedValue =
typeof centerLabel.value === "number"
? centerLabel.value.toLocaleString()
: centerLabel.value;
return (
<text
x={viewBox.cx}
y={viewBox.cy}
textAnchor="middle"
dominantBaseline="middle"
>
<tspan
x={viewBox.cx}
y={viewBox.cy}
className="text-3xl font-bold"
style={{
fill: "var(--chart-text-primary)",
}}
>
{formattedValue}
</tspan>
<tspan
x={viewBox.cx}
y={(viewBox.cy || 0) + 24}
style={{
fill: "var(--chart-text-secondary)",
}}
>
{centerLabel.label}
</tspan>
</text>
);
}
}}
/>
)}
</Pie>
</PieChart>
</ChartContainer>
{showLegend && <CustomLegend payload={legendPayload} />}
</div>
);
}