spb/countryatlas
Public
TypeScript 57%
Python 38.6%
JavaScript 3.6%
CSS 0.6%
1import { line as d3Line } from 'd3-shape';2import { scaleLinear } from 'd3-scale';3import { CHART, MARK } from './palette';4import { extent, periodToX, splitForecast, type SeriesPoint } from './scales';56/**7 * Tiny server-renderable line (no axes) with an end dot. Fixed viewBox so it scales with its box;8 * strokes use `vector-effect: non-scaling-stroke` so lines stay 1.5 px at any size.9 */10export function Sparkline({11 points,12 width = 96,13 height = 28,14 color = CHART.accent,15 className,16 ariaLabel,17 direction,18}: {19 points: SeriesPoint[];20 width?: number;21 height?: number;22 color?: string;23 className?: string;24 ariaLabel?: string;25 /** Colour the end dot by direction (up/down) — the line stays neutral. */26 direction?: 'up' | 'down' | 'flat' | null;27}) {28 const clean = points.filter((p) => p.value != null && Number.isFinite(p.value));29 if (clean.length < 2) return <span className={className} style={{ display: 'inline-block', width, height }} aria-hidden />;30 const xs = clean.map(periodToX);31 const xDom = extent(xs)!;32 const yDom = extent(clean.map((p) => p.value))!;33 const pad = 3;34 const x = scaleLinear().domain(xDom).range([pad, width - pad]);35 const y = scaleLinear()36 .domain(yDom[0] === yDom[1] ? [yDom[0] - 1, yDom[1] + 1] : yDom)37 .range([height - pad, pad]);38 const gen = d3Line<SeriesPoint>()39 .x((p) => x(periodToX(p)))40 .y((p) => y(p.value!));41 const runs = splitForecast(clean);42 const last = clean.filter((p) => !p.is_forecast).at(-1) ?? clean.at(-1)!;43 const dotColor = direction === 'up' ? CHART.up : direction === 'down' ? CHART.down : color;44 return (45 <svg className={className} width={width} height={height} viewBox={`0 0 ${width} ${height}`} role={ariaLabel ? 'img' : undefined} aria-label={ariaLabel} aria-hidden={ariaLabel ? undefined : true} preserveAspectRatio="none">46 {runs.map((r, i) => (47 <path key={i} d={gen(r.points) ?? ''} fill="none" stroke={r.forecast ? CHART.forecast : color} strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" strokeDasharray={r.forecast ? '3 3' : undefined} vectorEffect="non-scaling-stroke" opacity={r.forecast ? 0.8 : 0.9} />48 ))}49 <circle cx={x(periodToX(last))} cy={y(last.value!)} r={MARK.dotR - 1} fill={dotColor} stroke={CHART.surface} strokeWidth={1.5} vectorEffect="non-scaling-stroke" />50 </svg>51 );52}53