import { Box } from "@mantine/core";
import {
overviewAccentGradients,
type OverviewAccent,
} from "@/components/overview/overview.styles";
/**
* Tiny inline-SVG mini graphs shared by every KPI / stat card across the app.
* No charting dependency — crisp and cheap to render in long card grids.
*
* - `MiniSparkline` (variant "area" | "line") draws a smooth decorative trend.
* - `MiniRing` draws a circular percentage gauge.
*
* The sparkline series is deterministic (seeded from a string) so a given card
* always renders the same shape — purely decorative, not a real time-series.
*/
/** Deterministic, gently-rising series seeded by a string (purely decorative). */
export function sparkHeights(seed: string, baseline: number, count = 9): number[] {
let h = 2166136261;
for (let i = 0; i < seed.length; i += 1) {
h ^= seed.charCodeAt(i);
h = Math.imul(h, 16777619) >>> 0;
}
const out: number[] = [];
let v = 0.35 + (baseline > 0 ? Math.min(0.35, baseline * 0.35) : 0.15);
for (let i = 0; i < count; i += 1) {
h = (Math.imul(h, 1103515245) + 12345) >>> 0;
const delta = ((h % 1000) / 1000 - 0.42) * 0.28;
v = Math.min(1, Math.max(0.22, v + delta));
out.push(v);
}
// Nudge the final point up so the series reads as an upward trend.
out[out.length - 1] = Math.min(1, out[out.length - 1] + 0.15);
return out;
}
/** Build a smooth (Catmull-Rom → bezier) path string through y-points in [0,1]. */
function smoothPath(values: number[], width: number, height: number, pad = 2): string {
const n = values.length;
if (n === 0) return "";
const innerW = width - pad * 2;
const innerH = height - pad * 2;
const pts = values.map((v, i) => ({
x: pad + (n === 1 ? 0 : (i / (n - 1)) * innerW),
y: pad + (1 - v) * innerH,
}));
if (n === 1) return `M ${pts[0].x} ${pts[0].y}`;
let d = `M ${pts[0].x} ${pts[0].y}`;
for (let i = 0; i < n - 1; i += 1) {
const p0 = pts[i - 1] ?? pts[i];
const p1 = pts[i];
const p2 = pts[i + 1];
const p3 = pts[i + 2] ?? p2;
const c1x = p1.x + (p2.x - p0.x) / 6;
const c1y = p1.y + (p2.y - p0.y) / 6;
const c2x = p2.x - (p3.x - p1.x) / 6;
const c2y = p2.y - (p3.y - p1.y) / 6;
d += ` C ${c1x} ${c1y} ${c2x} ${c2y} ${p2.x} ${p2.y}`;
}
return d;
}
export type SparklineVariant = "area" | "line";
/**
* Smooth decorative trend. `area` fills under the curve with an accent gradient;
* `line` is a clean stroke with a highlighted dot at the latest point.
*/
export function MiniSparkline({
variant,
accent = "default",
seed,
baseline = 0,
height = 38,
}: {
variant: SparklineVariant;
accent?: OverviewAccent;
seed: string;
baseline?: number;
height?: number;
}) {
const [light, deep] = overviewAccentGradients[accent] ?? overviewAccentGradients.default;
const values = sparkHeights(seed, baseline);
const width = 120;
const pad = 3;
const line = smoothPath(values, width, height, pad);
const last = values[values.length - 1];
const lastX = pad + ((values.length - 1) / (values.length - 1 || 1)) * (width - pad * 2);
const lastY = pad + (1 - last) * (height - pad * 2);
// Unique gradient id per seed+accent so multiple cards don't collide.
const gid = `spark-${variant}-${accent}-${seed.replace(/[^a-zA-Z0-9]/g, "")}`;
return (
);
}
/** Circular percentage gauge with an accent stroke. */
export function MiniRing({
pct,
accent = "default",
size = 52,
stroke = 5,
children,
}: {
pct: number | null | undefined;
accent?: OverviewAccent;
size?: number;
stroke?: number;
children?: React.ReactNode;
}) {
const [, deep] = overviewAccentGradients[accent] ?? overviewAccentGradients.default;
const radius = (size - stroke) / 2;
const circumference = 2 * Math.PI * radius;
const clamped = pct != null ? Math.min(100, Math.max(0, Math.round(pct))) : null;
const dash = clamped != null ? (clamped / 100) * circumference : 0;
return (
{children}
);
}