spb/cancerindex
Public
TypeScript 97.2%
SQL 1.5%
CSS 0.6%
JavaScript 0.5%
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