/* Baby Names Almanac — chart, sparkline, name card, raw pivot. Adapted from the design bundle. Operates on series passed in as props (no global window.BABY_DATA — data is fetched via API and injected by App). */ const PALETTES = { F: ['#a23a4a', '#c97a55', '#7c5e3c', '#5a7159', '#8a4d6b', '#b89455'], M: ['#2f4a6b', '#5a7159', '#7c5e3c', '#8a4d4d', '#3d6b73', '#5e4f7a'], }; function fmt(n) { if (n == null || isNaN(n)) return '—'; if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M'; if (n >= 1000) return (n / 1000).toFixed(n >= 10000 ? 0 : 1) + 'k'; return Math.round(n).toLocaleString(); } function fmtFull(n) { return n == null ? '—' : Math.round(n).toLocaleString(); } function ord(n) { if (n == null) return '—'; const s = ['th','st','nd','rd'], v = n % 100; return n + (s[(v - 20) % 10] || s[v] || s[0]); } // ----- Trend Chart ----- function TrendChart({ series, metric, yearRange, mode, chartStyle }) { const [hover, setHover] = React.useState(null); const wrapRef = React.useRef(null); const W = 1100, H = 360; const PAD = { l: 56, r: 24, t: 18, b: 36 }; const innerW = W - PAD.l - PAD.r; const innerH = H - PAD.t - PAD.b; const [y0, y1] = yearRange; const palette = PALETTES[mode]; const filtered = series.map((s, i) => ({ name: s.name, color: s.color || palette[i % palette.length], points: (s.series || []) .filter(p => p.year >= y0 && p.year <= y1) .map(p => ({ year: p.year, val: p[metric.key] })) .filter(p => p.val != null && !isNaN(p.val)), })); let yMin = Infinity, yMax = -Infinity; filtered.forEach(s => s.points.forEach(p => { if (p.val < yMin) yMin = p.val; if (p.val > yMax) yMax = p.val; })); if (!isFinite(yMin)) { yMin = 0; yMax = 1; } if (metric.key === 'rank') { yMin = Math.max(1, Math.floor(yMin * 0.7)); yMax = Math.ceil(yMax * 1.05); } else { yMin = 0; yMax = yMax * 1.08 || 1; } const xScale = year => PAD.l + ((year - y0) / Math.max(1, (y1 - y0))) * innerW; const yScale = val => { if (metric.key === 'rank') { return PAD.t + ((val - yMin) / (yMax - yMin)) * innerH; } return PAD.t + (1 - (val - yMin) / (yMax - yMin)) * innerH; }; const xTicks = []; const yearSpan = y1 - y0; const step = yearSpan > 100 ? 20 : yearSpan > 50 ? 10 : yearSpan > 20 ? 5 : 2; for (let y = Math.ceil(y0 / step) * step; y <= y1; y += step) xTicks.push(y); const yTicks = []; const yTickCount = 5; for (let i = 0; i <= yTickCount; i++) { const v = yMin + (yMax - yMin) * (i / yTickCount); yTicks.push(metric.key === 'rank' ? Math.round(v) : v); } function pathFor(points) { if (points.length === 0) return ''; return points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${xScale(p.year).toFixed(2)} ${yScale(p.val).toFixed(2)}`).join(' '); } function areaFor(points) { if (points.length === 0) return ''; const baseY = metric.key === 'rank' ? yScale(yMax) : H - PAD.b; return points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${xScale(p.year).toFixed(2)} ${yScale(p.val).toFixed(2)}`).join(' ') + ` L ${xScale(points[points.length - 1].year).toFixed(2)} ${baseY} L ${xScale(points[0].year).toFixed(2)} ${baseY} Z`; } function onMove(e) { const svg = e.currentTarget; const rect = svg.getBoundingClientRect(); const x = (e.clientX - rect.left) * (W / rect.width); if (x < PAD.l || x > W - PAD.r) { setHover(null); return; } const yearF = y0 + ((x - PAD.l) / innerW) * (y1 - y0); const year = Math.round(yearF); setHover({ year, clientX: e.clientX - rect.left, clientY: e.clientY - rect.top, rect }); } function fmtVal(v) { if (v == null) return '—'; if (metric.key === 'count') return fmtFull(v); if (metric.key === 'rank') return '#' + Math.round(v); if (metric.key === 'share') return (v * 100).toFixed(3) + '%'; if (metric.key === 'per10k') return v.toFixed(1) + ' / 10k'; if (metric.key === 'percentile') return v.toFixed(1) + '%'; return v; } const hoverData = hover ? filtered.map(s => { const pt = s.points.find(p => p.year === hover.year); return pt ? { name: s.name, color: s.color, val: pt.val } : { name: s.name, color: s.color, val: null }; }) : null; return (
setHover(null)}> {yTicks.map((t, i) => { const yp = yScale(t); return ( {metric.key === 'count' ? fmt(t) : metric.key === 'rank' ? '#' + Math.round(t) : metric.key === 'share' ? (t * 100).toFixed(2) + '%' : metric.key === 'per10k' ? t.toFixed(1) : metric.key === 'percentile' ? t.toFixed(0) + '%' : t} ); })} {xTicks.map((t, i) => { const xp = xScale(t); return ( {t} ); })} {chartStyle === 'area' && filtered.map((s, i) => ( ))} {filtered.map((s, i) => ( {chartStyle === 'dots' && s.points.map((p, j) => ( ))} ))} {hover && ( {hoverData.map((h, i) => h.val != null ? ( ) : null)} )} {hover && hoverData && (
{hover.year}
{hoverData.map((h, i) => (
{h.name} {fmtVal(h.val)}
))}
)}
); } // ----- Sparkline ----- function Sparkline({ points, color }) { if (!points || points.length === 0) return null; const W = 280, H = 50; let max = -Infinity, min = Infinity; points.forEach(p => { if (p.val > max) max = p.val; if (p.val < min) min = p.val; }); const yr0 = points[0].year, yr1 = points[points.length - 1].year; const xs = y => ((y - yr0) / Math.max(1, yr1 - yr0)) * W; const ys = v => H - 4 - ((v - min) / Math.max(1, (max - min))) * (H - 8); const path = points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${xs(p.year).toFixed(1)} ${ys(p.val).toFixed(1)}`).join(' '); const area = path + ` L ${xs(points[points.length - 1].year).toFixed(1)} ${H} L ${xs(points[0].year).toFixed(1)} ${H} Z`; return ( ); } // ----- Trend label ----- function computeTrendLabel(series) { if (!series || series.length < 2) return { label: 'Rarely used', cls: 'rare' }; const last = series[series.length - 1]; const peakRow = series.reduce((a, b) => (b.count > a.count ? b : a), series[0]); const recent = series.slice(-10); const earlier = series.slice(-25, -10); const recentAvg = recent.reduce((a, r) => a + r.count, 0) / Math.max(1, recent.length); const earlierAvg = earlier.reduce((a, r) => a + r.count, 0) / Math.max(1, earlier.length); if (last.count < 100 && peakRow.count > 5000 && peakRow.year < 1960) { if (recentAvg > earlierAvg * 1.3) return { label: 'Coming back', cls: 'vintage' }; return { label: 'Rarely used', cls: 'rare' }; } if (peakRow.year < 1960 && last.count > peakRow.count * 0.4 && (recentAvg > earlierAvg * 1.4)) { return { label: 'Coming back', cls: 'vintage' }; } if (recentAvg > earlierAvg * 1.25) return { label: 'Getting more popular', cls: 'rising' }; if (recentAvg < earlierAvg * 0.75) return { label: 'Getting less popular', cls: 'falling' }; if (last.count < 200) return { label: 'Rarely used', cls: 'rare' }; return { label: 'Holding steady', cls: 'stable' }; } // ----- Name card ----- function NameOrigin({ name, info }) { if (!info) { return (
Origin & meaning

Loading origin details…

); } if (!info.available) { return (
Origin & meaning

Origin and meaning are not available yet.

); } const originLabel = (info.origins || []).join(', ') || 'Origin uncertain'; return (
Origin & meaning

{info.description || `${name} is commonly linked to ${originLabel}.`}

Origin{originLabel} Meaning{info.meaning || 'Meaning uncertain'}
LLM-generated note · {info.confidence || 'medium'} confidence
); } function NameCard({ name, series, color, ord: idx, nameInfo, showSparkline }) { if (!series || series.length === 0) { return (
No. {String(idx + 1).padStart(2, '0')}
{name}
Not in dataset
); } const last = series[series.length - 1]; const peak = series.reduce((a, b) => (b.count > a.count ? b : a), series[0]); const bestRank = series.reduce((a, b) => (b.rank < a.rank ? b : a), series[0]); const trend = computeTrendLabel(series); const sparkPts = series.map(s => ({ year: s.year, val: s.count })); return (
No. {String(idx + 1).padStart(2, '0')} · {last.year}
{trend.label}
{name}
Current popularity
{last.rank ? '#' + last.rank : '—'}
Babies named {name}
{fmt(last.count)} babies
Biggest year
{peak.year}
{fmt(peak.count)} babies
Best popularity#{bestRank.rank} {bestRank.year}
More popular than{last.percentile != null ? last.percentile.toFixed(1) + '%' : '—'}
{showSparkline !== false && (
)}
); } // ----- Raw data pivot (selected names only) ----- function RawTable({ series, mode, density }) { const valid = series.filter(s => s.series && s.series.length); const [search, setSearch] = React.useState(''); const [yearStart, setYearStart] = React.useState(2000); const [yearEnd, setYearEnd] = React.useState(2025); const [sortBy, setSortBy] = React.useState('recent'); const years = []; for (let y = yearEnd; y >= yearStart; y--) years.push(y); const allRows = React.useMemo(() => { return valid.map(s => { const counts = {}; let total = 0, peak = 0, peakYear = null, bestRank = Infinity, latestCount = 0; s.series.forEach(p => { if (p.year >= yearStart && p.year <= yearEnd) { counts[p.year] = p.count; total += p.count; if (p.count > peak) { peak = p.count; peakYear = p.year; } if (p.rank < bestRank) bestRank = p.rank; if (p.year === yearEnd) latestCount = p.count; } }); return { name: s.name, counts, total, peak, peakYear, bestRank, latestCount }; }); }, [valid, yearStart, yearEnd]); const filtered = React.useMemo(() => { let r = allRows; if (search.trim()) { const q = search.toLowerCase(); r = r.filter(x => x.name.toLowerCase().includes(q)); } r = [...r]; if (sortBy === 'recent') r.sort((a, b) => b.latestCount - a.latestCount); else if (sortBy === 'peak') r.sort((a, b) => b.peak - a.peak); else if (sortBy === 'best-rank') r.sort((a, b) => a.bestRank - b.bestRank); else if (sortBy === 'total') r.sort((a, b) => b.total - a.total); else if (sortBy === 'name') r.sort((a, b) => a.name.localeCompare(b.name)); return r; }, [allRows, search, sortBy]); const maxCount = React.useMemo(() => { let m = 0; filtered.forEach(r => Object.values(r.counts).forEach(v => { if (v > m) m = v; })); return m; }, [filtered]); const accent = mode === 'F' ? '162, 58, 74' : '47, 74, 107'; function exportCSV() { const head = ['name', ...years].join(','); const lines = filtered.map(r => [r.name, ...years.map(y => r.counts[y] || '')].join(',')); const csv = [head, ...lines].join('\n'); const blob = new Blob([csv], { type: 'text/csv' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `baby-names-${mode === 'F' ? 'girls' : 'boys'}-${yearStart}-${yearEnd}.csv`; a.click(); URL.revokeObjectURL(url); } return (
{filtered.length} of {allRows.length} selected names
{valid.length === 0 ? (
Add a name above to see yearly counts.
) : (
{years.map(y => )} {filtered.map(row => ( {years.map(y => { const c = row.counts[y]; const intensity = c ? Math.min(1, c / (maxCount * 0.5)) : 0; return ( ); })} ))}
Name{y}
{row.name} {c ? c.toLocaleString() : '·'}
)}

The Social Security list does not include names given to fewer than 5 babies in a year. Dots mean the name was not listed for that year.

); } Object.assign(window, { TrendChart, Sparkline, NameCard, RawTable, computeTrendLabel, fmt, fmtFull, ord, PALETTES });