SPB Git forge

spb/cancerindex

Public
37commits 1branches 0releases
2.9 MBsize
maindefault branch
10 days agolast push
TypeScript 97.2% SQL 1.5% CSS 0.6% JavaScript 0.5%
5.5 KB · 100 lines typescript
Raw Blame History
1import { describe, it, expect } from 'vitest';2import { groupComparable, explainSplit, differingDimensions, splitIntoMultiples, shouldUseMultiples, groupKey, latestPoint, type ComparableObs } from '@/lib/explorer-series';34function obs(over: Partial<ComparableObs> & { year: number; value: number }): ComparableObs {5  return {6    cancer_slug: 'lung',7    cancer_name: 'Lung',8    geography_slug: 'united-states',9    geography_name: 'United States',10    sex: 'all',11    age_group: 'all',12    metric: 'as_mortality_rate',13    unit: 'per_100k',14    standard_population: 'US 2000 standard',15    estimate_type: 'observed',16    source_slug: 'cdc-uscs',17    source_name: 'USCS',18    provenance_id: 1,19    ...over,20  };21}2223describe('groupComparable', () => {24  it('overlays observations that share metric, unit, geography, source, standard population and age group', () => {25    const rows = [obs({ year: 2000, value: 50 }), obs({ year: 2001, value: 48 }), obs({ cancer_slug: 'breast', cancer_name: 'Breast', year: 2000, value: 20 }), obs({ cancer_slug: 'breast', cancer_name: 'Breast', year: 2001, value: 19 })];26    const g = groupComparable(rows);27    expect(g).toHaveLength(1);28    expect(g[0]!.series.map((s) => s.name)).toEqual(['Breast', 'Lung']); // sex omitted when every series shares it29    expect(g[0]!.series[1]!.points.map((p) => p.x)).toEqual([2000, 2001]);30    expect(g[0]!.n_obs).toBe(4);31    expect([g[0]!.year_min, g[0]!.year_max]).toEqual([2000, 2001]);32    expect(g[0]!.y_max).toBe(50);33  });34  it('separates different standard populations and different sources — never overlaid', () => {35    const rows = [obs({ year: 2000, value: 50 }), obs({ year: 2000, value: 52, standard_population: 'World (Segi)' }), obs({ year: 2000, value: 51, source_slug: 'cdc-wonder', standard_population: 'US 2000 Std. Population' })];36    const g = groupComparable(rows);37    expect(g).toHaveLength(3);38    const dims = differingDimensions(g);39    expect(dims).toEqual(['source_slug', 'standard_population']);40    const why = explainSplit(g)!;41    expect(why).toMatch(/3 separate charts/);42    expect(why).toMatch(/standard population \(.*US 2000 standard.*vs.*World \(Segi\)/);43    expect(why).toMatch(/source \(cdc-uscs vs cdc-wonder\)/);44  });45  it('separates age groups and metrics/units; counts get "no standard population" in the caption', () => {46    const rows = [obs({ year: 2000, value: 50 }), obs({ year: 2000, value: 5, age_group: '65+' }), obs({ year: 2000, value: 1000, metric: 'mortality_count', unit: 'count', standard_population: null })];47    const g = groupComparable(rows);48    expect(g).toHaveLength(3);49    expect(explainSplit(g)).toMatch(/no standard population \(crude or count\)/);50    expect(explainSplit(g)).toMatch(/age group \(all ages vs 65\+\)/);51  });52  it('returns null explanation for a single group', () => {53    expect(explainSplit(groupComparable([obs({ year: 2000, value: 1 })]))).toBeNull();54  });55  it('makes cancer × sex series, dashed when estimated, and names them accordingly', () => {56    const rows = [obs({ year: 2000, value: 50, sex: 'male' }), obs({ year: 2000, value: 40, sex: 'female' }), obs({ year: 2001, value: 39, sex: 'female', estimate_type: 'estimated' })];57    const g = groupComparable(rows);58    expect(g).toHaveLength(1);59    const names = g[0]!.series.map((s) => `${s.name}${s.dashed ? '*' : ''}`);60    expect(names).toEqual(['Lung · male', 'Lung · female', 'Lung · female · estimated*']);61  });62  it('disambiguates two site definitions of one cancer inside one source', () => {63    const rows = [obs({ year: 2000, value: 50, site_definition: 'C33-C34' }), obs({ year: 2000, value: 45, site_definition: 'C34' })];64    const g = groupComparable(rows);65    expect(g[0]!.series.map((s) => s.name).sort()).toEqual(['Lung · C33-C34', 'Lung · C34']);66  });67  it('orders groups by size and skips non-finite values', () => {68    const rows = [obs({ year: 2000, value: 1, source_slug: 'x' }), obs({ year: 2000, value: 2 }), obs({ year: 2001, value: 3 }), obs({ year: 2002, value: Number.NaN })];69    const g = groupComparable(rows);70    expect(g.map((x) => x.source_slug)).toEqual(['cdc-uscs', 'x']);71    expect(g[0]!.n_obs).toBe(2);72  });73  it('groupKey is stable and null-safe', () => {74    expect(groupKey(obs({ year: 1, value: 1, standard_population: null }))).toBe('as_mortality_rate|per_100k|united-states|cdc-uscs||all');75  });76});7778describe('small multiples', () => {79  const rows = ['a', 'b', 'c', 'd', 'e'].flatMap((c) => [obs({ cancer_slug: c, cancer_name: c.toUpperCase(), year: 2000, value: 10 }), obs({ cancer_slug: c, cancer_name: c.toUpperCase(), year: 2001, value: c === 'e' ? 99 : 12 })]);80  const g = groupComparable(rows)[0]!;81  it('forces multiples beyond 4 overlaid series, honours the explicit view otherwise', () => {82    expect(shouldUseMultiples(g, 'lines')).toBe(true);83    const small = groupComparable(rows.slice(0, 4))[0]!;84    expect(shouldUseMultiples(small, 'lines')).toBe(false);85    expect(shouldUseMultiples(small, 'multiples')).toBe(true);86    const single = groupComparable(rows.slice(0, 2))[0]!;87    expect(shouldUseMultiples(single, 'multiples')).toBe(false); // one cancer → nothing to split88  });89  it('splits per cancer and keeps the shared y_max', () => {90    const panels = splitIntoMultiples(g);91    expect(panels).toHaveLength(5);92    expect(panels.every((p) => p.y_max === 99)).toBe(true);93    expect(panels.map((p) => p.series[0]!.cancer_slug)).toEqual(['a', 'b', 'c', 'd', 'e']);94    expect(panels[0]!.n_obs).toBe(2);95  });96  it('latestPoint returns the last chronological point', () => {97    expect(latestPoint(g.series[4]!)).toEqual({ x: 2001, y: 99 });98  });99});100