Skip to content

Commit 0aa7366

Browse files
committed
www/map: revert circle-mode picker override — circle is a pure render swap
Prior commit `aca0fcd5c9e` had circle mode coarsen the picker (r10→r9, r11→r10 etc) to save bandwidth. That conflates two orthogonal concerns: viz rendering vs. wire-format optimization. The original circle-mode design was superficial — draw each cell as a circular column instead of a hex tessellation, everything else identical. Reverting the coarsest-fit override; circle mode now uses identical picker → identical res → identical cell fetch as hex mode. Bandwidth optimizations for the ~1 MB responses at coarse zoom still wanted — but they belong elsewhere: - Worker-side `maxCells` adaptive coarsening on the combo path - Parquet transport (drop JSON overhead per row) Neither of those changes what the user sees — only what fills the wire. Tests updated accordingly: circle-mode picker now asserted equal to hex-mode picker across the zoom sweep. 32 passing, was 46. Refs #121.
1 parent aca0fcd commit 0aa7366

2 files changed

Lines changed: 27 additions & 91 deletions

File tree

www/src/map/picker.test.ts

Lines changed: 20 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -48,82 +48,30 @@ describe("picker: hex mode auto res per zoom", () => {
4848
}
4949
})
5050

51-
describe("picker: circle mode coarsest-fit override", () => {
52-
// Below the rasterized threshold (target dot < 1.5px), circle mode
53-
// MUST fall through to hex-mode auto to preserve statewide density
54-
// paint. Above it, circle can go coarser than hex mode.
55-
it("z=8.5 (target < 1.5px): matches hex auto (r9, rasterized)", () => {
56-
expect(circleRadiusPx(8.5)).toBeLessThan(1.5)
57-
expect(pickRes("circle", 8.5, NJ_LAT)).toBe(pickRes("hex", 8.5, NJ_LAT))
58-
})
59-
60-
// Anchor circle-mode picks at every zoom in a table so any drift
61-
// shows up loud.
62-
const cases: Array<[number, number, number]> = [
63-
// [zoom, hex_res, circle_res]
64-
[8.5, 9, 9], // rasterized preserved (target < 1.5)
65-
[8.7, 9, 8], // one-step jump at threshold crossing (target ≈ 1.53)
66-
[9.0, 9, 8],
67-
[10.94, 10, 9], // Bergen — the vp that flagged the picker over-fetch
68-
[11.0, 10, 9],
69-
[12.0, 11, 9],
70-
[13.81, 11, 10], // mid-zoom (DTJC)
71-
[14.0, 12, 11],
72-
[16.91, 12, 12], // deep: auto wins (coarsestFit fits inside auto)
73-
[17.0, 13, 12], // circle stays r12; hex bumps to r13
74-
[18.0, 14, 13],
75-
[19.0, 15, 13], // circle strictly coarser at very deep zoom
76-
[20.0, 15, 14],
77-
]
78-
for (const [zoom, hex, circle] of cases) {
79-
it(`z=${zoom}: hex=r${hex}, circle=r${circle}`, () => {
80-
expect(pickRes("hex", zoom, NJ_LAT)).toBe(hex)
81-
expect(pickRes("circle", zoom, NJ_LAT)).toBe(circle)
82-
})
83-
}
84-
})
85-
86-
describe("picker: hex mode monotone (never coarser as we zoom in)", () => {
87-
// Hex-mode tessellation should never drop res as zoom increases.
88-
// Circle mode is intentionally *not* monotone at the boundary
89-
// crossing (z≈8.5 target=1.49→1.55 wants slightly-coarser cells so
90-
// 1.5px dots fit inscribed instead of clamping to sub-px on r9).
91-
it("res non-decreasing across z=7 → 20 (hex)", () => {
92-
let prev = -1
93-
for (let z = 7; z <= 20; z += 0.1) {
94-
const r = pickRes("hex", z, NJ_LAT)
95-
expect(r).toBeGreaterThanOrEqual(prev)
96-
prev = r
97-
}
98-
})
99-
})
100-
101-
describe("picker: circle mode piecewise monotone", () => {
102-
// Circle mode has ONE legit dropdown at the rasterized→discrete-dot
103-
// threshold crossing (z≈8.6 for the current curve). Everywhere else
104-
// it should stay non-decreasing as z increases.
105-
it("at most one res-drop across z=7 → 20", () => {
106-
let prev = -1
107-
let drops = 0
108-
for (let z = 7; z <= 20; z += 0.1) {
109-
const r = pickRes("circle", z, NJ_LAT)
110-
if (r < prev) drops++
111-
prev = r
51+
describe("picker: circle mode == hex mode (pure rendering swap)", () => {
52+
// Circle mode is a rendering swap only — same picker, same res,
53+
// same cell fetch. Anything else conflates two concerns (viz
54+
// rendering vs. bandwidth optimization). Bandwidth wins live
55+
// elsewhere (worker-side coarsening, parquet transport).
56+
it("hex and circle pick identical res across z=7 → 20", () => {
57+
for (let z = 7; z <= 20; z += 0.25) {
58+
expect(pickRes("circle", z, NJ_LAT)).toBe(pickRes("hex", z, NJ_LAT))
11259
}
113-
expect(drops).toBeLessThanOrEqual(1)
11460
})
11561
})
11662

117-
describe("picker: circle mode never picks a res finer than hex mode", () => {
118-
// Corollary of the "Math.max with auto" guard — circle-mode's data
119-
// reduction should never accidentally fetch finer cells than hex.
120-
it("across z=7 → 20", () => {
121-
for (let z = 7; z <= 20; z += 0.25) {
122-
const hexRes = pickRes("hex", z, NJ_LAT)
123-
const circleRes = pickRes("circle", z, NJ_LAT)
124-
expect(circleRes).toBeLessThanOrEqual(hexRes)
125-
}
126-
})
63+
describe("picker: monotone in zoom (never coarser as we zoom in)", () => {
64+
// Both modes: as zoom deepens, res never decreases.
65+
for (const viz of ["hex", "circle"] as const) {
66+
it(`${viz} mode: res non-decreasing across z=7 → 20`, () => {
67+
let prev = -1
68+
for (let z = 7; z <= 20; z += 0.1) {
69+
const r = pickRes(viz, z, NJ_LAT)
70+
expect(r).toBeGreaterThanOrEqual(prev)
71+
prev = r
72+
}
73+
})
74+
}
12775
})
12876

12977
describe("picker: circleRadiusPx curve", () => {

www/src/map/picker.ts

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -39,32 +39,20 @@ export function autoHexPxTarget(
3939
return Math.min(30, Math.max(1.0, diaPx * 0.99))
4040
}
4141

42-
/** Threshold (px) below which we stay in the *rasterized-heatmap*
43-
* regime — target dot < ~1.5px, cells blend into density paint. Above
44-
* this we're in the *discrete-dot* regime where we can safely drop to
45-
* a coarser resolution whose inscribed circle still fits the target
46-
* dot, saving bandwidth without changing the visual. */
47-
const CIRCLE_RASTERIZED_THRESHOLD_PX = 1.5
48-
4942
/** Effective `hexPxTarget` fed into `pickHexResolutionForPixels`.
50-
* Layer over the auto target with viz-mode overrides. */
43+
* Circle mode uses the identical picker as hex mode — same res, same
44+
* cell count, same fetch. Circle is a pure rendering swap: each cell
45+
* is drawn as a circular column clamped to the cell's inscribed
46+
* radius. Bandwidth optimizations belong elsewhere (worker-side
47+
* aggregation, parquet transport). */
5148
export function hexPxTargetFor(
5249
viz: VizMode,
5350
zoom: number,
5451
lat: number,
5552
autoOverrides: Record<number, number> = {},
5653
): number {
57-
const auto = autoHexPxTarget(zoom, lat, autoOverrides)
58-
if (viz !== "circle") return auto
59-
const target = circleRadiusPx(zoom)
60-
if (target < CIRCLE_RASTERIZED_THRESHOLD_PX) return auto
61-
// inscribed_radius ≥ target_radius ⇔ hex_diameter ≥ 4·target/√3.
62-
// Accept up to ~15% clamp (dot rendered slightly smaller than
63-
// target) rather than jump two H3 levels for a strict fit — the
64-
// difference between "1.5px" and "1.3px" dots is invisible; the
65-
// difference between r7 and r8 cell counts is 7×.
66-
const coarsestFit = ((4 * target) / Math.sqrt(3)) / 1.15
67-
return Math.max(auto, coarsestFit)
54+
void viz // parity with hex mode
55+
return autoHexPxTarget(zoom, lat, autoOverrides)
6856
}
6957

7058
/** End-to-end: given (viz, zoom, lat, autoOverrides), return the H3

0 commit comments

Comments
 (0)