feat(report): PDF SVG chart components (trend line + horizontal bar)
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
import React from 'react';
|
||||
import { Svg, Line, Polyline, Rect, Text as SvgText, G } from '@react-pdf/renderer';
|
||||
|
||||
interface TrendChartProps {
|
||||
labels: string[];
|
||||
current: number[];
|
||||
previous: number[] | null;
|
||||
color: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export function PdfTrendChart({ labels, current, previous, color, width = 460, height = 140 }: TrendChartProps) {
|
||||
const allValues = [...current, ...(previous ?? [])].filter(v => v > 0);
|
||||
const max = allValues.length > 0 ? Math.max(...allValues) : 1;
|
||||
const padL = 8, padR = 8, padT = 8, padB = 8;
|
||||
const w = width - padL - padR;
|
||||
const h = height - padT - padB;
|
||||
|
||||
const sx = (i: number) => padL + (labels.length > 1 ? (i / (labels.length - 1)) * w : w / 2);
|
||||
const sy = (v: number) => padT + h - (v / max) * h;
|
||||
|
||||
const toPoints = (data: number[]) =>
|
||||
data.map((v, i) => `${sx(i).toFixed(1)},${sy(v).toFixed(1)}`).join(' ');
|
||||
|
||||
const gridLines = [0.25, 0.5, 0.75, 1.0];
|
||||
|
||||
return (
|
||||
<Svg width={width} height={height}>
|
||||
{gridLines.map(f => (
|
||||
<Line key={f}
|
||||
x1={padL} y1={sy(max * f).toFixed(1)}
|
||||
x2={width - padR} y2={sy(max * f).toFixed(1)}
|
||||
stroke="#e2e8f0" strokeWidth={0.5} />
|
||||
))}
|
||||
{previous && previous.some(v => v > 0) && (
|
||||
<Polyline points={toPoints(previous)}
|
||||
stroke="#94a3b8" strokeWidth={1.5} strokeDasharray="4 3" fill="none" />
|
||||
)}
|
||||
{current.some(v => v > 0) && (
|
||||
<Polyline points={toPoints(current)}
|
||||
stroke={color} strokeWidth={2.5} fill="none" />
|
||||
)}
|
||||
{labels
|
||||
.filter((_, i) => labels.length <= 8 || i % Math.ceil(labels.length / 8) === 0)
|
||||
.map((label) => {
|
||||
const origIdx = labels.indexOf(label);
|
||||
return (
|
||||
<SvgText key={label}
|
||||
x={sx(origIdx).toFixed(1)} y={height - 1}
|
||||
fill="#94a3b8"
|
||||
textAnchor="middle"
|
||||
style={{ fontSize: 7 }}>
|
||||
{label}
|
||||
</SvgText>
|
||||
);
|
||||
})}
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
interface HBarChartProps {
|
||||
items: Array<{ name: string; value: number }>;
|
||||
color: string;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export function PdfHBarChart({ items, color, width = 460 }: HBarChartProps) {
|
||||
const barH = 16;
|
||||
const gap = 10;
|
||||
const labelW = 150;
|
||||
const valueW = 70;
|
||||
const barAreaW = width - labelW - valueW - 8;
|
||||
const max = Math.max(...items.map(i => i.value), 1);
|
||||
const totalH = items.length * (barH + gap);
|
||||
|
||||
return (
|
||||
<Svg width={width} height={totalH}>
|
||||
{items.map((item, i) => {
|
||||
const y = i * (barH + gap);
|
||||
const bw = Math.max((item.value / max) * barAreaW, 2);
|
||||
const shortName = item.name.length > 22 ? item.name.slice(0, 22) + '…' : item.name;
|
||||
const valueStr = item.value.toLocaleString('en-SA', { maximumFractionDigits: 0 });
|
||||
return (
|
||||
<G key={item.name}>
|
||||
<SvgText x={0} y={y + barH - 4} fill="#334155" style={{ fontSize: 8 }}>{shortName}</SvgText>
|
||||
<Rect x={labelW} y={y} width={bw} height={barH} fill={color} rx={3} />
|
||||
<SvgText x={labelW + bw + 4} y={y + barH - 4} fill="#64748b" style={{ fontSize: 8 }}>{valueStr}</SvgText>
|
||||
</G>
|
||||
);
|
||||
})}
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user