improvement: some files replaced, lots of them i shall say. AGENT.MD file created. some rules and skills added for cursor agent.
This commit is contained in:
168
frontend/src/components/ui/today/TodayDonutChart.tsx
Normal file
168
frontend/src/components/ui/today/TodayDonutChart.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
'use client';
|
||||
|
||||
import type { CSSProperties } from 'react';
|
||||
import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from 'recharts';
|
||||
import type { TodayChartBucket } from '@/types/today';
|
||||
import { TodayChartFrame } from '@/components/ui/today/TodayChartFrame';
|
||||
import {
|
||||
chartRankColor,
|
||||
TODAY_CHART_TOOLTIP_STYLE,
|
||||
} from '@/components/today/chart-theme';
|
||||
|
||||
interface TodayDonutChartBaseProps {
|
||||
data: TodayChartBucket[];
|
||||
labelForCode: (code: string) => string;
|
||||
colorForCode?: (code: string, index: number) => string;
|
||||
swatchStyleForCode?: (code: string, index: number) => CSSProperties;
|
||||
}
|
||||
|
||||
interface TodayDonutChartProps extends TodayDonutChartBaseProps {
|
||||
variant?: 'donut' | 'pie';
|
||||
/** Inline legend + chart row (legacy). Prefer TodayDonutChartLegend + sidePanelLayout. */
|
||||
sideLegend?: boolean;
|
||||
}
|
||||
|
||||
function useDonutChartModel({
|
||||
data,
|
||||
labelForCode,
|
||||
colorForCode,
|
||||
swatchStyleForCode,
|
||||
}: TodayDonutChartBaseProps) {
|
||||
const chartData = data.map((item) => ({
|
||||
...item,
|
||||
displayLabel: labelForCode(item.code),
|
||||
}));
|
||||
|
||||
const resolveColor = (code: string, index: number) =>
|
||||
colorForCode?.(code, index) ?? chartRankColor(index);
|
||||
|
||||
const resolveSwatchStyle = (code: string, index: number): CSSProperties =>
|
||||
swatchStyleForCode?.(code, index) ?? {
|
||||
backgroundColor: resolveColor(code, index),
|
||||
borderColor: 'rgba(0, 0, 0, 0.18)',
|
||||
};
|
||||
|
||||
return { chartData, resolveColor, resolveSwatchStyle };
|
||||
}
|
||||
|
||||
export function TodayDonutChartLegend({
|
||||
data,
|
||||
labelForCode,
|
||||
colorForCode,
|
||||
swatchStyleForCode,
|
||||
}: TodayDonutChartBaseProps) {
|
||||
const { chartData, resolveSwatchStyle } = useDonutChartModel({
|
||||
data,
|
||||
labelForCode,
|
||||
colorForCode,
|
||||
swatchStyleForCode,
|
||||
});
|
||||
|
||||
const rowClass = 'flex h-4 items-center text-xs leading-none';
|
||||
|
||||
return (
|
||||
<div className="flex min-w-0 items-start overflow-hidden">
|
||||
<div className="flex max-h-full min-w-0 flex-col items-start gap-1.5 overflow-y-auto">
|
||||
{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 min-w-0 flex-col items-start gap-1.5 overflow-hidden">
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
||||
export function TodayDonutChart({
|
||||
data,
|
||||
labelForCode,
|
||||
colorForCode,
|
||||
swatchStyleForCode,
|
||||
variant = 'donut',
|
||||
sideLegend = false,
|
||||
}: TodayDonutChartProps) {
|
||||
const { chartData, resolveColor } = useDonutChartModel({
|
||||
data,
|
||||
labelForCode,
|
||||
colorForCode,
|
||||
swatchStyleForCode,
|
||||
});
|
||||
|
||||
const innerRadius = variant === 'pie' ? 0 : '62%';
|
||||
const outerRadius = variant === 'pie' ? '88%' : 92;
|
||||
|
||||
const pieChart = (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<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 (
|
||||
<div className="flex h-full min-h-0 w-full items-center gap-3 overflow-hidden sm:gap-4">
|
||||
<div className="flex min-h-0 min-w-0 flex-1 items-center overflow-hidden">
|
||||
<TodayDonutChartLegend
|
||||
data={data}
|
||||
labelForCode={labelForCode}
|
||||
colorForCode={colorForCode}
|
||||
swatchStyleForCode={swatchStyleForCode}
|
||||
/>
|
||||
</div>
|
||||
<div className="aspect-square h-[min(100%,9.5rem)] w-[min(100%,9.5rem)] shrink-0">
|
||||
{pieChart}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <TodayChartFrame>{pieChart}</TodayChartFrame>;
|
||||
}
|
||||
Reference in New Issue
Block a user