spb/company-atlas
Public
Python 66.3%
TypeScript 22.7%
JavaScript 8.6%
HTML 1.4%
CSS 0.7%
1import { extent, max, min } from 'd3-array';2import { scaleLinear } from 'd3-scale';3import { area as d3area, curveMonotoneX, line as d3line } from 'd3-shape';4import { cn } from '@/lib/cn';56/**7 * Inline sparkline (SVG, server-renderable). Values are plotted left→right; nulls break the line. `baseline` draws a8 * dashed reference (e.g. index 100). `tone` colours by trend when 'auto'. Never fabricates: empty → a flat hairline.9 */10export function Sparkline({11 values,12 width = 96,13 height = 24,14 className,15 tone = 'auto',16 fill = true,17 baseline,18 strokeWidth = 1.4,19 ariaLabel,20}: {21 values: Array<number | null | undefined> | undefined | null;22 width?: number;23 height?: number;24 className?: string;25 tone?: 'auto' | 'accent' | 'positive' | 'negative' | 'muted' | 'ink';26 fill?: boolean;27 baseline?: number;28 strokeWidth?: number;29 ariaLabel?: string;30}) {31 const pts = (values ?? []).map((v, i) => [i, typeof v === 'number' && Number.isFinite(v) ? v : null] as [number, number | null]);32 const valid = pts.filter((p): p is [number, number] => p[1] !== null);33 if (valid.length < 2) {34 return (35 <svg width={width} height={height} className={cn('block shrink-0', className)} aria-hidden>36 <line x1={0} x2={width} y1={height / 2} y2={height / 2} stroke="var(--rule-strong)" strokeDasharray="2 3" />37 </svg>38 );39 }40 const first = valid[0]?.[1] ?? 0;41 const last = valid[valid.length - 1]?.[1] ?? 0;42 const color =43 tone === 'auto' ? (last > first ? 'var(--positive)' : last < first ? 'var(--danger)' : 'var(--ink-3)') : tone === 'accent' ? 'var(--accent)' : tone === 'positive' ? 'var(--positive)' : tone === 'negative' ? 'var(--danger)' : tone === 'ink' ? 'var(--ink)' : 'var(--ink-3)';44 const x = scaleLinear()45 .domain([0, pts.length - 1])46 .range([1, width - 1]);47 const ys = valid.map((p) => p[1]);48 let [lo, hi] = extent([...ys, ...(baseline !== undefined ? [baseline] : [])]) as [number, number];49 if (lo === hi) {50 lo -= 1;51 hi += 1;52 }53 const y = scaleLinear()54 .domain([lo, hi])55 .range([height - 2, 2]);56 const l = d3line<[number, number | null]>()57 .defined((d) => d[1] !== null)58 .x((d) => x(d[0]))59 .y((d) => y(d[1] as number))60 .curve(curveMonotoneX);61 const a = d3area<[number, number | null]>()62 .defined((d) => d[1] !== null)63 .x((d) => x(d[0]))64 .y0(height)65 .y1((d) => y(d[1] as number))66 .curve(curveMonotoneX);67 const lastPt = valid[valid.length - 1] as [number, number];68 return (69 <svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} className={cn('block shrink-0 overflow-visible', className)} role={ariaLabel ? 'img' : undefined} aria-label={ariaLabel} aria-hidden={ariaLabel ? undefined : true}>70 {fill && <path d={a(pts) ?? ''} fill={color} opacity={0.1} />}71 {baseline !== undefined && <line x1={0} x2={width} y1={y(baseline)} y2={y(baseline)} stroke="var(--rule-strong)" strokeDasharray="2 3" />}72 <path d={l(pts) ?? ''} fill="none" stroke={color} strokeWidth={strokeWidth} strokeLinejoin="round" strokeLinecap="round" />73 <circle cx={x(lastPt[0])} cy={y(lastPt[1])} r={1.8} fill={color} />74 </svg>75 );76}7778/** Delta helper: first vs last of a series (percentage), null when not computable. */79export function sparkDelta(values: Array<number | null | undefined> | undefined | null): number | null {80 const v = (values ?? []).filter((x): x is number => typeof x === 'number' && Number.isFinite(x));81 if (v.length < 2) return null;82 const a = v[0] as number;83 const b = v[v.length - 1] as number;84 if (a === 0) return null;85 return ((b - a) / Math.abs(a)) * 100;86}8788export function seriesMinMax(values: number[]): [number, number] | null {89 if (!values.length) return null;90 return [min(values) ?? 0, max(values) ?? 0];91}92