Files
dyolink/frontend/src/components/today/TodayDonutChart.tsx

126 lines
3.7 KiB
TypeScript
Raw Normal View History

'use client';
import type { CSSProperties } from 'react';
import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from 'recharts';
import type { TodayChartBucket } from '@/types/today';
import {
TODAY_CHART_COLORS,
TODAY_CHART_TOOLTIP_STYLE,
} from '@/components/today/chart-theme';
interface TodayDonutChartProps {
data: TodayChartBucket[];
labelForCode: (code: string) => string;
colorForCode?: (code: string, index: number) => string;
swatchStyleForCode?: (code: string, index: number) => CSSProperties;
variant?: 'donut' | 'pie';
sideLegend?: boolean;
}
export function TodayDonutChart({
data,
labelForCode,
colorForCode,
swatchStyleForCode,
variant = 'donut',
sideLegend = false,
}: TodayDonutChartProps) {
const chartData = data.map((item) => ({
...item,
displayLabel: labelForCode(item.code),
}));
const resolveColor = (code: string, index: number) =>
colorForCode?.(code, index) ??
TODAY_CHART_COLORS[index % TODAY_CHART_COLORS.length];
const resolveSwatchStyle = (code: string, index: number): CSSProperties =>
swatchStyleForCode?.(code, index) ?? {
backgroundColor: resolveColor(code, index),
borderColor: 'rgba(0, 0, 0, 0.18)',
};
const innerRadius = variant === 'pie' ? 0 : 62;
const outerRadius = sideLegend ? 100 : 92;
const chart = (
<ResponsiveContainer width="100%" height={240}>
<PieChart margin={{ top: 0, right: 0, bottom: 0, left: 0 }}>
<Pie
data={chartData}
dataKey="count"
nameKey="displayLabel"
cx="50%"
cy="50%"
innerRadius={innerRadius}
outerRadius={outerRadius}
paddingAngle={variant === 'pie' ? 1 : 2}
stroke="transparent"
>
{chartData.map((entry, index) => (
<Cell key={entry.code} fill={resolveColor(entry.code, index)} />
))}
</Pie>
<Tooltip
contentStyle={TODAY_CHART_TOOLTIP_STYLE}
formatter={(value, _name, item) => {
const row = item?.payload as TodayChartBucket | undefined;
return [value, row ? labelForCode(row.code) : ''];
}}
/>
</PieChart>
</ResponsiveContainer>
);
if (!sideLegend) {
return chart;
}
const rowClass = 'flex h-4 items-center text-xs leading-none';
const legendInset = 'px-12';
return (
<div className={`flex h-full min-h-[220px] items-center ${legendInset}`}>
<div className="flex min-w-0 flex-1 items-center overflow-y-auto max-h-full py-0.5">
<div className="flex flex-col items-start gap-1.5 shrink-0">
{chartData.map((entry, index) => (
<span key={entry.code} className={rowClass}>
<span
className="inline-block h-3 w-3 rounded-sm border"
style={resolveSwatchStyle(entry.code, index)}
aria-hidden
/>
</span>
))}
</div>
<div className="ml-2 flex flex-col items-start gap-1.5">
{chartData.map((entry) => (
<span
key={entry.code}
className={`${rowClass} max-w-full truncate text-left text-text-primary`}
>
{entry.displayLabel}
</span>
))}
</div>
<div className="ml-3 flex shrink-0 flex-col items-end gap-1.5">
{chartData.map((entry) => (
<span
key={entry.code}
className={`${rowClass} tabular-nums text-right text-text-muted`}
>
{entry.count}
</span>
))}
</div>
</div>
<div className="ml-4 flex h-[240px] w-[min(100%,220px)] max-w-[48%] shrink-0 items-center justify-center">
{chart}
</div>
</div>
);
}