199 lines
6.4 KiB
TypeScript
199 lines
6.4 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
Bar,
|
|
BarChart,
|
|
CartesianGrid,
|
|
LabelList,
|
|
Line,
|
|
LineChart,
|
|
XAxis,
|
|
YAxis,
|
|
} from "recharts";
|
|
|
|
import {
|
|
ChartContainer,
|
|
ChartLegend,
|
|
ChartLegendContent,
|
|
ChartTooltip,
|
|
ChartTooltipContent,
|
|
type ChartConfig,
|
|
} from "@/components/ui/chart";
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCaption,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/components/ui/table";
|
|
import type { PreparedComparisonChart } from "@/lib/charts/comparison";
|
|
|
|
interface ComparisonChartProps {
|
|
prepared: PreparedComparisonChart;
|
|
}
|
|
|
|
function suffix(normalization: PreparedComparisonChart["config"]["normalization"]) {
|
|
return normalization === "percent-of-total" ? "%" : "";
|
|
}
|
|
|
|
export function ComparisonChart({ prepared }: ComparisonChartProps) {
|
|
const { config, rows } = prepared;
|
|
const chartConfig: ChartConfig = Object.fromEntries(
|
|
config.series.map((series) => [
|
|
series.field,
|
|
{ label: series.label, color: `var(--${series.color})` },
|
|
]),
|
|
);
|
|
const data = rows.map((row) => ({
|
|
category: row.category,
|
|
...row.values,
|
|
}));
|
|
const valueSuffix = suffix(config.normalization);
|
|
const chartHeight = Math.max(
|
|
config.presentation === "horizontal-bar" ? rows.length * 54 : 320,
|
|
280,
|
|
);
|
|
const common = {
|
|
data,
|
|
accessibilityLayer: true,
|
|
margin: { top: 20, right: 28, left: 8, bottom: 12 },
|
|
} as const;
|
|
|
|
return (
|
|
<div className="space-y-5">
|
|
<ChartContainer
|
|
config={chartConfig}
|
|
className="min-h-72 w-full aspect-auto"
|
|
style={{ height: chartHeight }}
|
|
aria-label={config.title ?? "กราฟเปรียบเทียบ"}
|
|
>
|
|
{config.presentation === "line" ? (
|
|
<LineChart {...common}>
|
|
<CartesianGrid vertical={false} />
|
|
<XAxis dataKey="category" tickLine={false} axisLine={false} minTickGap={16} />
|
|
<YAxis tickLine={false} axisLine={false} width={48} />
|
|
<ChartTooltip content={<ChartTooltipContent />} />
|
|
{config.showLegend ? (
|
|
<ChartLegend content={<ChartLegendContent />} />
|
|
) : null}
|
|
{config.series.map((series) => (
|
|
<Line
|
|
key={series.field}
|
|
dataKey={series.field}
|
|
type="monotone"
|
|
stroke={`var(--color-${series.field})`}
|
|
strokeWidth={2}
|
|
dot={{ r: 4, fill: `var(--color-${series.field})` }}
|
|
activeDot={{ r: 6 }}
|
|
>
|
|
{config.showValues ? (
|
|
<LabelList
|
|
position="top"
|
|
className="fill-foreground font-mono text-[10px]"
|
|
formatter={(value: unknown) => `${String(value)}${valueSuffix}`}
|
|
/>
|
|
) : null}
|
|
</Line>
|
|
))}
|
|
</LineChart>
|
|
) : config.presentation === "horizontal-bar" ? (
|
|
<BarChart {...common} layout="vertical">
|
|
<CartesianGrid horizontal={false} />
|
|
<XAxis type="number" tickLine={false} axisLine={false} />
|
|
<YAxis
|
|
type="category"
|
|
dataKey="category"
|
|
tickLine={false}
|
|
axisLine={false}
|
|
width={112}
|
|
/>
|
|
<ChartTooltip content={<ChartTooltipContent />} />
|
|
{config.showLegend ? (
|
|
<ChartLegend content={<ChartLegendContent />} />
|
|
) : null}
|
|
{config.series.map((series) => (
|
|
<Bar
|
|
key={series.field}
|
|
dataKey={series.field}
|
|
fill={`var(--color-${series.field})`}
|
|
radius={4}
|
|
>
|
|
{config.showValues ? (
|
|
<LabelList
|
|
position="right"
|
|
className="fill-foreground font-mono text-[10px]"
|
|
formatter={(value: unknown) => `${String(value)}${valueSuffix}`}
|
|
/>
|
|
) : null}
|
|
</Bar>
|
|
))}
|
|
</BarChart>
|
|
) : (
|
|
<BarChart {...common}>
|
|
<CartesianGrid vertical={false} />
|
|
<XAxis dataKey="category" tickLine={false} axisLine={false} minTickGap={16} />
|
|
<YAxis tickLine={false} axisLine={false} width={48} />
|
|
<ChartTooltip content={<ChartTooltipContent />} />
|
|
{config.showLegend ? (
|
|
<ChartLegend content={<ChartLegendContent />} />
|
|
) : null}
|
|
{config.series.map((series) => (
|
|
<Bar
|
|
key={series.field}
|
|
dataKey={series.field}
|
|
stackId={config.presentation === "stacked-bar" ? "total" : undefined}
|
|
fill={`var(--color-${series.field})`}
|
|
radius={config.presentation === "stacked-bar" ? 0 : 4}
|
|
>
|
|
{config.showValues ? (
|
|
<LabelList
|
|
position="top"
|
|
className="fill-foreground font-mono text-[10px]"
|
|
formatter={(value: unknown) => `${String(value)}${valueSuffix}`}
|
|
/>
|
|
) : null}
|
|
</Bar>
|
|
))}
|
|
</BarChart>
|
|
)}
|
|
</ChartContainer>
|
|
|
|
<Table>
|
|
<TableCaption className="sr-only">
|
|
ค่าที่ใช้ในกราฟ {config.title ?? "เปรียบเทียบ"}
|
|
</TableCaption>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead scope="col">หมวดหมู่</TableHead>
|
|
{config.series.map((series) => (
|
|
<TableHead key={series.field} scope="col" className="text-right">
|
|
{series.label}
|
|
</TableHead>
|
|
))}
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{rows.map((row) => (
|
|
<TableRow key={row.id}>
|
|
<TableHead scope="row">{row.category}</TableHead>
|
|
{config.series.map((series) => (
|
|
<TableCell key={series.field} className="text-right font-mono tabular-nums">
|
|
{row.exactValues[series.field]}
|
|
{config.normalization !== "raw" ? (
|
|
<span className="ml-2 text-muted-foreground">
|
|
({row.normalizedValues[series.field]}
|
|
{valueSuffix})
|
|
</span>
|
|
) : null}
|
|
</TableCell>
|
|
))}
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
);
|
|
}
|