/** * Dashboard charts — bar (تعداد بیماران) + line/area (میزان درآمد). * * The source (clinic-pro-tauri) draws these with @mui/x-charts. MUI is not used * in clinicpro, so they are reproduced with plain DOM + inline SVG, matching the * source visuals: bars #5559CE, dashed horizontal grid, y-axis ticks #858D9D, * x-axis labels #7E7E7E, and a line #5559CE over a #3A6FF8 gradient area. */ import React from 'react'; export interface ChartPoint { label: string; value: number; } /** ~5 rounded gridline ticks covering [0, max], top → bottom. Deduped so tiny * integer ranges (e.g. max=1) never render repeated labels. */ function niceTicks(max: number, count = 4): number[] { const safeMax = max > 0 ? max : 1; const rawStep = safeMax / count; const mag = Math.pow(10, Math.floor(Math.log10(rawStep))); const norm = rawStep / mag; const niceNorm = norm <= 1 ? 1 : norm <= 2 ? 2 : norm <= 5 ? 5 : 10; const step = niceNorm * mag; const ticks: number[] = []; for (let i = count; i >= 0; i--) ticks.push(Math.round(step * i)); // collapse duplicate rounded labels (keeps gridlines proportional via ticks[0]) const deduped = ticks.filter((t, i) => i === 0 || t !== ticks[i - 1]); return deduped.length >= 2 ? deduped : [ticks[0], 0]; } const faNum = new Intl.NumberFormat('fa-IR'); /** * Shared plot frame: left y-axis tick column + dashed gridlines + bottom x-axis * labels. `render(top, bottom)` receives the plot-area vertical bounds (px kept * implicit via fl/percentages) and returns the plot content. */ function ChartFrame({ ticks, labels, yWidth, children, }: { ticks: number[]; labels: string[]; yWidth: number; children: React.ReactNode; }) { return (
{/* y-axis ticks, aligned to gridlines */}
{ticks.map((t, i) => ( {faNum.format(t)} ))}
{/* plot area */}
{ticks.map((_, i) => (
))} {children}
{/* x-axis labels */}
{labels.map((l, i) => ( {l} ))}
); } /** Bar chart — thin #5559CE columns (source: BarPlot, categoryGapRatio 0.7). */ export function TauriBarChart({ data }: { data: ChartPoint[] }) { if (!data.length || !data.some((d) => d.value > 0)) { return ; } const max = Math.max(...data.map((d) => d.value), 1); const ticks = niceTicks(max); const top = ticks[0] || 1; return ( d.label)} yWidth={42}>
{data.map((d, i) => (
0 ? 4 : 0, animation: `tdgrowcol .9s ${i * 0.06}s cubic-bezier(.22,.61,.36,1) both`, }} />
))}
); } /** Line + area chart — #5559CE line over a #3A6FF8 gradient (source: LinePlot). */ export function TauriLineChart({ data }: { data: ChartPoint[] }) { if (data.length < 2 || !data.some((d) => d.value > 0)) { return ; } const values = data.map((d) => d.value); const max = Math.max(...values, 1); const ticks = niceTicks(max); const top = ticks[0] || 1; // SVG plot: 0..100 in both axes, non-uniform scaling (path has no text). const W = 100; const H = 100; const stepX = data.length > 1 ? W / (data.length - 1) : W; const pts = values.map((v, i) => [i * stepX, H - (v / top) * H] as [number, number]); const d = pts .map((p, i) => { if (i === 0) return `M${p[0]},${p[1]}`; const prev = pts[i - 1]; const cx = (prev[0] + p[0]) / 2; return `C${cx},${prev[1]} ${cx},${p[1]} ${p[0]},${p[1]}`; }) .join(' '); const area = `${d} L${pts[pts.length - 1][0]},${H} L${pts[0][0]},${H} Z`; return ( x.label)} yWidth={64}> ); } function EmptyChart() { return (
داده‌ای برای نمایش نیست
); }