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