fix(admin): restore LineChart component
LineChart is used by the dashboard page to render DAU trend charts. It was accidentally removed during metrics cleanup — restore it.
This commit is contained in:
parent
ae601774df
commit
22b5eab769
169
admin/src/components/admin/LineChart.tsx
Normal file
169
admin/src/components/admin/LineChart.tsx
Normal file
@ -0,0 +1,169 @@
|
||||
"use client";
|
||||
|
||||
import { format, parseISO } from "date-fns";
|
||||
|
||||
interface DataPoint {
|
||||
date: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
interface LineChartProps {
|
||||
data: DataPoint[];
|
||||
width?: number;
|
||||
height?: number;
|
||||
color?: string;
|
||||
label: string;
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
export default function LineChart({
|
||||
data,
|
||||
width = 600,
|
||||
height = 200,
|
||||
color = "#6366f1",
|
||||
label,
|
||||
unit = "",
|
||||
}: LineChartProps) {
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<div style={{ color: "#737373", fontSize: "13px", textAlign: "center", padding: "32px" }}>
|
||||
暂无数据
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const padding = { top: 16, right: 16, bottom: 36, left: 44 };
|
||||
const chartWidth = width - padding.left - padding.right;
|
||||
const chartHeight = height - padding.top - padding.bottom;
|
||||
|
||||
const values = data.map((d) => d.value);
|
||||
const maxValue = Math.max(...values, 1);
|
||||
const minValue = 0;
|
||||
|
||||
function xScale(i: number) {
|
||||
return padding.left + (i / (data.length - 1 || 1)) * chartWidth;
|
||||
}
|
||||
|
||||
function yScale(v: number) {
|
||||
return padding.top + chartHeight - ((v - minValue) / (maxValue - minValue || 1)) * chartHeight;
|
||||
}
|
||||
|
||||
// Build SVG path
|
||||
const pathD = data
|
||||
.map((d, i) => `${i === 0 ? "M" : "L"} ${xScale(i).toFixed(1)} ${yScale(d.value).toFixed(1)}`)
|
||||
.join(" ");
|
||||
|
||||
// Area path (fill under the line)
|
||||
const areaD =
|
||||
pathD +
|
||||
` L ${xScale(data.length - 1).toFixed(1)} ${(padding.top + chartHeight).toFixed(1)}` +
|
||||
` L ${padding.left} ${(padding.top + chartHeight).toFixed(1)} Z`;
|
||||
|
||||
// Y-axis ticks (5 ticks)
|
||||
const yTicks = Array.from({ length: 5 }, (_, i) => {
|
||||
const v = minValue + ((maxValue - minValue) * i) / 4;
|
||||
return { v, y: yScale(v) };
|
||||
});
|
||||
|
||||
// X-axis ticks (show every nth label based on data length)
|
||||
const step = Math.max(1, Math.floor(data.length / 6));
|
||||
const xTicks = data.filter((_, i) => i % step === 0 || i === data.length - 1);
|
||||
|
||||
const gradientId = `grad-${label.replace(/\s/g, "-")}`;
|
||||
|
||||
return (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
style={{ display: "block", maxWidth: "100%" }}
|
||||
aria-label={`${label} 趋势图`}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor={color} stopOpacity={0.2} />
|
||||
<stop offset="100%" stopColor={color} stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
{/* Y-axis gridlines */}
|
||||
{yTicks.map((t, i) => (
|
||||
<line
|
||||
key={i}
|
||||
x1={padding.left}
|
||||
y1={t.y}
|
||||
x2={width - padding.right}
|
||||
y2={t.y}
|
||||
stroke="#e5e5e5"
|
||||
strokeWidth={1}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Y-axis labels */}
|
||||
{yTicks.map((t, i) => (
|
||||
<text
|
||||
key={i}
|
||||
x={padding.left - 6}
|
||||
y={t.y + 4}
|
||||
textAnchor="end"
|
||||
fontSize={11}
|
||||
fill="#737373"
|
||||
>
|
||||
{Math.round(t.v)}
|
||||
</text>
|
||||
))}
|
||||
|
||||
{/* X-axis labels */}
|
||||
{xTicks.map((d, i) => {
|
||||
const idx = data.findIndex((x) => x.date === d.date);
|
||||
return (
|
||||
<text
|
||||
key={i}
|
||||
x={xScale(idx)}
|
||||
y={padding.top + chartHeight + 16}
|
||||
textAnchor="middle"
|
||||
fontSize={11}
|
||||
fill="#737373"
|
||||
>
|
||||
{(() => {
|
||||
try {
|
||||
return format(parseISO(d.date), "MM-dd");
|
||||
} catch {
|
||||
return d.date.slice(5);
|
||||
}
|
||||
})()}
|
||||
</text>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Area fill */}
|
||||
<path d={areaD} fill={`url(#${gradientId})`} />
|
||||
|
||||
{/* Line */}
|
||||
<path d={pathD} fill="none" stroke={color} strokeWidth={2} strokeLinejoin="round" />
|
||||
|
||||
{/* Data points (dots) */}
|
||||
{data.map((d, i) => (
|
||||
<circle
|
||||
key={i}
|
||||
cx={xScale(i)}
|
||||
cy={yScale(d.value)}
|
||||
r={3}
|
||||
fill={color}
|
||||
stroke="white"
|
||||
strokeWidth={1.5}
|
||||
style={{ cursor: "default" }}
|
||||
>
|
||||
<title>
|
||||
{(() => {
|
||||
try {
|
||||
return `${format(parseISO(d.date), "yyyy-MM-dd")}: ${d.value}${unit}`;
|
||||
} catch {
|
||||
return `${d.date}: ${d.value}${unit}`;
|
||||
}
|
||||
})()}
|
||||
</title>
|
||||
</circle>
|
||||
))}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user