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%
4.0 KB · 87 lines typescript
Raw Blame History
1import { describe, expect, it } from 'vitest';2import { MAP_NO_DATA_FILL, MAP_RAMP, classIndex, classLabel, fillFor, quantileScale, quantileSorted, rampColor, sqrtRadius } from '@/lib/map-scale';34describe('quantileSorted', () => {5  it('interpolates like R-7 / d3.quantile', () => {6    const s = [1, 2, 3, 4, 5];7    expect(quantileSorted(s, 0)).toBe(1);8    expect(quantileSorted(s, 0.5)).toBe(3);9    expect(quantileSorted(s, 0.25)).toBe(2);10    expect(quantileSorted(s, 1)).toBe(5);11    expect(quantileSorted([10, 20], 0.5)).toBe(15);12    expect(Number.isNaN(quantileSorted([], 0.5))).toBe(true);13  });14});1516describe('quantileScale', () => {17  it('splits skewed counts into 5 equal-count classes with non-overlapping integer bounds', () => {18    const values = [608226, 68847, 68832, 49415, 47021, 44579, 32882, 30740, 28329, 19933, 500, 300, 120, 80, 40, 12, 9, 5, 2, 1];19    const s = quantileScale(values);20    expect(s.method).toBe('quantile');21    expect(s.classes).toHaveLength(5);22    expect(s.breaks).toHaveLength(4);23    // every value lands in exactly one class whose [lo, hi] contains it24    for (const v of values) {25      const c = s.classes[classIndex(v, s.breaks)]!;26      expect(v).toBeGreaterThanOrEqual(c.lo);27      expect(v).toBeLessThanOrEqual(c.hi);28    }29    // classes are ordered and disjoint30    for (let i = 1; i < s.classes.length; i++) expect(s.classes[i]!.lo).toBeGreaterThan(s.classes[i - 1]!.hi);31    // roughly equal counts (20 values → about 4 per class; integer thresholds shift one or two items)32    expect(s.classes.reduce((n, c) => n + c.n, 0)).toBe(20);33    for (const c of s.classes) expect(c.n).toBeGreaterThanOrEqual(3);34    for (const c of s.classes) expect(c.n).toBeLessThanOrEqual(5);35    expect(s.classes[4]!.hi).toBe(608226);36    expect(s.classes[0]!.fill).toBe(MAP_RAMP[0]);37    expect(s.classes[4]!.fill).toBe(MAP_RAMP[4]);38  });39  it('ignores zeros, negatives and non-finite values', () => {40    const s = quantileScale([0, -3, NaN, Infinity, 5, 10]);41    expect(s.classes.reduce((n, c) => n + c.n, 0)).toBe(2);42    expect(fillFor(0, s)).toBe(MAP_NO_DATA_FILL);43    expect(fillFor(null, s)).toBe(MAP_NO_DATA_FILL);44  });45  it('collapses to fewer classes when there are few distinct values', () => {46    expect(quantileScale([]).classes).toEqual([]);47    const one = quantileScale([7, 7, 7]);48    expect(one.classes).toHaveLength(1);49    expect(one.breaks).toEqual([]);50    expect(one.classes[0]).toMatchObject({ lo: 7, hi: 7, n: 3, fill: MAP_RAMP[MAP_RAMP.length - 1] });51    const two = quantileScale([1, 1, 1, 1, 100]);52    expect(two.classes.length).toBeGreaterThanOrEqual(2);53    expect(two.classes.length).toBeLessThanOrEqual(5);54    expect(fillFor(100, two)).toBe(MAP_RAMP[MAP_RAMP.length - 1]);55    expect(fillFor(1, two)).toBe(MAP_RAMP[0]);56  });57});5859describe('helpers', () => {60  it('classIndex uses inclusive upper thresholds', () => {61    expect(classIndex(1, [1, 10, 100])).toBe(0);62    expect(classIndex(2, [1, 10, 100])).toBe(1);63    expect(classIndex(100, [1, 10, 100])).toBe(2);64    expect(classIndex(101, [1, 10, 100])).toBe(3);65    expect(classIndex(5, [])).toBe(0);66  });67  it('rampColor spreads n classes across the ramp', () => {68    expect(rampColor(0, 5)).toBe(MAP_RAMP[0]);69    expect(rampColor(4, 5)).toBe(MAP_RAMP[4]);70    expect(rampColor(0, 2)).toBe(MAP_RAMP[0]);71    expect(rampColor(1, 2)).toBe(MAP_RAMP[4]);72    expect(rampColor(0, 1)).toBe(MAP_RAMP[4]);73  });74  it('classLabel formats ranges and singletons', () => {75    const fmt = (n: number) => n.toLocaleString('en-US');76    expect(classLabel({ index: 0, lo: 1, hi: 12, n: 3, fill: '#000' }, fmt)).toBe('1–12');77    expect(classLabel({ index: 4, lo: 608226, hi: 608226, n: 1, fill: '#000' }, fmt)).toBe('608,226');78  });79  it('sqrtRadius scales area with value and clamps', () => {80    expect(sqrtRadius(100, 100)).toBe(14);81    expect(sqrtRadius(25, 100)).toBe(7);82    expect(sqrtRadius(1, 1_000_000)).toBe(1.5);83    expect(sqrtRadius(0, 100)).toBe(0);84    expect(sqrtRadius(5, 0)).toBe(0);85  });86});87