import { test } from 'node:test' import assert from 'node:assert/strict' import { estimateTextWidth, layoutPeakLabels } from './labelLayout.js' const BOUNDS = { min: 0, max: 400, fontPx: 10 } test('estimateTextWidth: grows with text length and font size', () => { assert.ok(estimateTextWidth('AB', 10) > estimateTextWidth('A', 10)) assert.ok(estimateTextWidth('ABC', 20) > estimateTextWidth('ABC', 10)) assert.ok(estimateTextWidth('', 10) >= 0) }) test('layoutPeakLabels: well-separated labels all sit on lane 0', () => { const out = layoutPeakLabels( [{ key: 'a', x: 20, text: 'A' }, { key: 'b', x: 200, text: 'B' }, { key: 'c', x: 380, text: 'C' }], BOUNDS, ) assert.deepEqual(out.labels.map((l) => l.lane), [0, 0, 0]) assert.equal(out.lanes, 1) }) test('layoutPeakLabels: overlapping labels are pushed to separate lanes', () => { // This is the failing case from the live site: three densities peaking at // nearly the same rate rendered as "HiWhite:F: Black F". const out = layoutPeakLabels( [ { key: 'hi', x: 100, text: 'Hispanic F' }, { key: 'wh', x: 104, text: 'White F' }, { key: 'bl', x: 108, text: 'Black F' }, ], BOUNDS, ) const lanes = out.labels.map((l) => l.lane).sort() assert.deepEqual(lanes, [0, 1, 2]) assert.equal(out.lanes, 3) }) test('layoutPeakLabels: identical positions never share a lane', () => { const out = layoutPeakLabels( [ { key: 'a', x: 200, text: 'Hispanic F' }, { key: 'b', x: 200, text: 'Hispanic M' }, ], BOUNDS, ) assert.notEqual(out.labels[0].lane, out.labels[1].lane) }) test('layoutPeakLabels: a lane is reused once there is horizontal room', () => { const out = layoutPeakLabels( [ { key: 'a', x: 20, text: 'A' }, { key: 'b', x: 24, text: 'B' }, { key: 'c', x: 380, text: 'C' }, ], BOUNDS, ) const byKey = Object.fromEntries(out.labels.map((l) => [l.key, l.lane])) assert.equal(byKey.a, 0) assert.equal(byKey.b, 1) // 'c' is far away, so it drops back to the first lane rather than stacking. assert.equal(byKey.c, 0) }) test('layoutPeakLabels: anchors outward at the edges so text stays in frame', () => { const out = layoutPeakLabels( [{ key: 'l', x: 0, text: 'Hispanic F' }, { key: 'r', x: 400, text: 'Hispanic F' }], BOUNDS, ) const byKey = Object.fromEntries(out.labels.map((l) => [l.key, l])) assert.equal(byKey.l.anchor, 'start') assert.equal(byKey.r.anchor, 'end') }) test('layoutPeakLabels: a mid-plot label stays centred on its peak', () => { const out = layoutPeakLabels([{ key: 'm', x: 200, text: 'Black F' }], BOUNDS) assert.equal(out.labels[0].anchor, 'middle') assert.equal(out.labels[0].x, 200) }) test('layoutPeakLabels: no label extends outside the plot bounds', () => { const out = layoutPeakLabels( [ { key: 'l', x: -50, text: 'American Indian / Alaska Native' }, { key: 'r', x: 900, text: 'American Indian / Alaska Native' }, ], BOUNDS, ) for (const l of out.labels) { assert.ok(l.left >= BOUNDS.min - 0.01, `${l.key} left ${l.left} < ${BOUNDS.min}`) assert.ok(l.right <= BOUNDS.max + 0.01, `${l.key} right ${l.right} > ${BOUNDS.max}`) } }) test('layoutPeakLabels: preserves input order in the output', () => { // Lane assignment sorts internally; callers still key off their own order. const out = layoutPeakLabels( [{ key: 'z', x: 300, text: 'Z' }, { key: 'a', x: 10, text: 'A' }], BOUNDS, ) assert.deepEqual(out.labels.map((l) => l.key), ['z', 'a']) }) test('layoutPeakLabels: empty input yields zero lanes', () => { const out = layoutPeakLabels([], BOUNDS) assert.deepEqual(out.labels, []) assert.equal(out.lanes, 0) }) test('layoutPeakLabels: wider gap forces more lanes', () => { const items = [{ key: 'a', x: 100, text: 'A' }, { key: 'b', x: 130, text: 'B' }] const tight = layoutPeakLabels(items, { ...BOUNDS, gap: 0 }) const loose = layoutPeakLabels(items, { ...BOUNDS, gap: 40 }) assert.ok(loose.lanes > tight.lanes) })