feat(public): render responsive character guides
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user