SPB Git forge

spb/countryatlas

Public
20commits 1branches 0releases
268.3 MBsize
maindefault branch
12 days agolast push
TypeScript 57% Python 38.6% JavaScript 3.6% CSS 0.6%
2.6 KB · 59 lines tsx
Raw Blame History
1import { t } from '@/i18n';2import { fixed, isNum } from '@/lib/format';3import { seriesVar } from './palette';45/**6 * Age structure as a single stacked bar (0–14 / 15–64 / 65+ shares, % of population). Ordinal colouring7 * (one hue, three lightness steps via the sequential ramp) because the categories are ordered.8 * A 2 px surface gap separates segments; labels inside only when the segment is wide enough.9 */10export function PopulationPyramid({11  shares,12  name,13  year,14  className,15}: {16  shares: { young: number | null; working: number | null; old: number | null };17  name: string;18  year?: number | null;19  className?: string;20}) {21  const parts = [22    { key: 'young', label: '0–14', value: shares.young, color: 'var(--seq-2)' },23    { key: 'working', label: '15–64', value: shares.working, color: 'var(--seq-4)' },24    { key: 'old', label: '65+', value: shares.old, color: 'var(--seq-6)' },25  ];26  const valid = parts.filter((p) => isNum(p.value));27  if (valid.length === 0) return <p className="text-sm text-ink-3">{t('chart.noData')}</p>;28  const total = valid.reduce((a, p) => a + (p.value as number), 0) || 100;29  const summary = t('chart.summary.pyramid', { name, parts: valid.map((p) => `${p.label}: ${fixed(p.value as number, 1)} %`).join(', ') });30  return (31    <figure className={className}>32      <div className="flex h-6 w-full overflow-hidden rounded-xs" role="img" aria-label={summary} style={{ gap: 2 }}>33        {valid.map((p) => {34          const pct = ((p.value as number) / total) * 100;35          return (36            <div key={p.key} className="relative h-full" style={{ width: `${pct}%`, background: p.color }} title={`${p.label}: ${fixed(p.value as number, 1)} %`}>37              {pct > 14 ? (38                <span className="tnum absolute inset-0 grid place-items-center text-2xs font-medium" style={{ color: p.key === 'young' ? 'var(--ink)' : '#fff' }}>39                  {fixed(p.value as number, 0)} %40                </span>41              ) : null}42            </div>43          );44        })}45      </div>46      <figcaption className="mt-1.5 flex flex-wrap gap-x-4 gap-y-1 text-xs text-ink-2">47        {valid.map((p) => (48          <span key={p.key} className="inline-flex items-center gap-1.5">49            <span aria-hidden className="inline-block h-2.5 w-2.5 rounded-xs" style={{ background: p.color }} />50            {p.label} <span className="tnum text-ink">{fixed(p.value as number, 1)} %</span>51          </span>52        ))}53        {year ? <span className="text-ink-3">{year}</span> : null}54      </figcaption>55      {seriesVar(0) ? null : null}56    </figure>57  );58}59