feat(report): PDF SVG chart components (trend line + horizontal bar)

This commit is contained in:
fahed
2026-04-28 14:37:03 +03:00
parent 2f90753f57
commit cf6a4c0b3d
+94
View File
@@ -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>
);
}