@kahwee/sf-map-svg 4.3.0 → 4.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/api-audit.md CHANGED
@@ -16,8 +16,9 @@ unbounded geographic data can never fail.
16
16
  | A new camera request during animation | The new request cancels the prior generation. A callback cannot revive it after cancellation/disposal. |
17
17
  | Selection callback selecting another item | Newer selection wins. The old selection cannot subsequently move the camera or send a stale activation callback. |
18
18
  | `selectNeighborhood(..., {fit:false})` in basemap mode | Selection can change presentation, but preserves the camera. Explicit layer overrides remain authoritative. |
19
- | Clustering and selected/focused markers | Selected/focused pins remain individually reachable. Coincident alternatives remain available through the chooser. |
20
- | Marker entrance and reduced motion | Entrances are optional, independent of camera motion, and cancel on preference changes or disposal. Stable IDs avoid repeated entrances on filtering. |
19
+ | Clustering and selected/focused markers | Selected/focused pins remain individually reachable. Coincident pins and clusters that cannot separate at maximum zoom open a place chooser, including compact shells with hidden native pickers. Visible pins and clusters share one tab stop with bracket-key navigation. |
20
+ | Viewport marker rendering | Individual pin visuals allocate on first visibility or selection; offscreen pins and cluster symbols detach with screen-space overscan. Uncreated records receive current labels/styles on first materialization. Full catalog/picker and cached node identities remain available; selected/focused pins stay mounted. Pan reuses global cluster membership and nodes until zoom, data, selection, focus, or clustering changes. |
21
+ | Marker entrance and reduced motion | Entrances begin on first visibility, are independent of camera motion, and cancel on preference changes or disposal, including pending entrances. Cached pins do not restart entrances on reentry. Stable IDs avoid repeated entrances on filtering. |
21
22
  | Hiding touch control while gestures are enabled | Returns touch scrolling to the page. Other toolbar controls remain independent. |
22
23
  | Convenience mode/source setters | Share the atomic configuration path. `{ resetView: false }` preserves the camera; omitted options retain the historic reset. Invalid options and unavailable geography leave state unchanged. |
23
24
  | Runtime patch omitted/false/undefined | Omitted retains; false disables boolean switches; undefined removes group/key overrides. Top-level mode/source/year/labels set to undefined retain their current values. Nested feature objects replace, rather than deep-merge. |
@@ -45,7 +46,7 @@ Live appearance patches prepare values, district styles and source replacement b
45
46
  | Accessibility | Feature round trips, keyboard activation, focus recovery after removal/clustering, independent controls, touch names, and reduced motion have browser regressions. Hiding every alternative chooser is a consumer accessibility decision. |
46
47
  | Output safety | SVG text and attributes are escaped. This is not a security sandbox for CSS tokens or caller-provided DOM. Keep untrusted callout content in `textContent`; the consumer controls CSS/resource policy. |
47
48
  | Packaging | Clean tarball install exercises entrypoints and emitted declarations, including `LineString` overlays. No runtime dependencies or embedded geographic coordinate blobs were added. |
48
- | Performance | Production bundle checks enforce the guide and renderer ceilings and inspect dataset inclusion. Grouping has a 2,000-marker regression; millions of markers are outside this guide-oriented design. |
49
+ | Performance | Production bundle checks enforce the guide and renderer ceilings and inspect dataset inclusion. Viewport culling, focus, reattachment, and cluster reuse have 2,000-marker regressions across three engines and two widths; millions of markers are outside this guide-oriented design. |
49
50
 
50
51
  Regression sources: `test/api-contract.test.js`, `test/camera.test.js`,
51
52
  `test/viewport.test.js`, `stories/Robustness.stories.ts`,
@@ -61,7 +62,7 @@ Static grouped layers and appearance reject unknown keys, while flat compatibili
61
62
  interactive option-key validation is not a claim that all static options share the
62
63
  same schema. Host layout, delayed images, fonts, and application cards need their
63
64
  own CLS tests. The scale bar is an approximate local Mercator scale, not survey
64
- instrumentation. Clustering does not spiderfy coincident locations. Geographic
65
+ instrumentation. Coincident locations use a place chooser rather than spiderfying. Geographic
65
66
  source correctness remains governed by SOURCES.md and existing topology tests.
66
67
 
67
68
  ## Extending the contract
@@ -1,12 +1,12 @@
1
1
  # Guide bundle size report
2
2
 
3
- Generated 2026-10-01 by `pnpm report:guide` with Vite production minification and gzip compression. Each emitted JS chunk is compressed independently. The report measures the explicit-data root and the optional `@kahwee/sf-map-svg/guide` preset.
3
+ Generated 2026-10-11 by `pnpm report:guide` with Vite production minification and gzip compression. Each emitted JS chunk is compressed independently. The report measures the explicit-data root and the optional `@kahwee/sf-map-svg/guide` preset.
4
4
 
5
5
  | Entry | Initial JS, raw | Initial JS, gzip | Explicit detail JS, gzip |
6
6
  | --- | ---: | ---: | ---: |
7
- | Renderer root (explicit data) | 99.3 KB | 29.1 KB | — |
8
- | Static renderer | 18.2 KB | 5.8 KB | — |
9
- | Guide preset | 489.1 KB | 116.8 KB | 467.0 KB |
7
+ | Renderer root (explicit data) | 118.8 KB | 34.6 KB | — |
8
+ | Static renderer | 25.8 KB | 7.9 KB | — |
9
+ | Guide preset | 507.8 KB | 122.0 KB | 467.0 KB |
10
10
 
11
11
  **500 KB target:** met.
12
12
 
package/llms-full.txt CHANGED
@@ -2,13 +2,13 @@
2
2
 
3
3
  > Offline San Francisco SVG maps with explicit geographic imports, no runtime dependencies, static rendering, and a browser controller.
4
4
 
5
- @kahwee/sf-map-svg 4.3.0 · local working tree. Node >=24.0.0 for server rendering; browsers need a DOM and a JSON-capable bundler.
5
+ @kahwee/sf-map-svg 4.3.2 · local working tree. Node >=24.0.0 for server rendering; browsers need a DOM and a JSON-capable bundler.
6
6
 
7
7
  These are working-tree docs. Unreleased changelog entries may describe APIs absent from npm; verify the installed version before copying them.
8
8
 
9
9
  Use renderMap(data, options) for SVG strings; createMap(data, options) returns a controller. Mount map.element and destroy it on unmount. Choose mode explicitly: createMap defaults to basemap. Geography is separate; staticMapData and fullMapData have different shapes.
10
10
 
11
- <!-- Generated from README.md; @kahwee/sf-map-svg 4.3.0 · local working tree. -->
11
+ <!-- Generated from README.md; @kahwee/sf-map-svg 4.3.2 · local working tree. -->
12
12
 
13
13
  # SF Map SVG
14
14
 
@@ -16,6 +16,8 @@ Offline, self-contained San Francisco SVG maps. The package has no runtime depen
16
16
 
17
17
  [Getting started](https://github.com/kahwee/sf-map-svg/blob/main/docs/developer-guide.md) · [API reference](https://github.com/kahwee/sf-map-svg/blob/main/docs/API.md) · [Design playground](https://kahwee.github.io/sf-map-svg/playground.html) · [Live examples](https://kahwee.github.io/sf-map-svg/examples.html) · [Storybook source](https://github.com/kahwee/sf-map-svg/blob/main/stories/) · [Geographic sources](https://github.com/kahwee/sf-map-svg/blob/main/SOURCES.md) · [v3 migration](https://github.com/kahwee/sf-map-svg/blob/main/docs/migration-v3.md)
18
18
 
19
+ [Map preview](https://github.com/kahwee/sf-map-svg/blob/main/docs/map-preview.png) · 2,000-marker screencasts: [desktop](https://github.com/kahwee/sf-map-svg/blob/main/docs/media/markers-2000-desktop.webm), [390 px phone](https://github.com/kahwee/sf-map-svg/blob/main/docs/media/markers-2000-phone.webm). [Screenshots and capture instructions](https://github.com/kahwee/sf-map-svg/blob/main/docs/media/README.md) show zoom, viewport culling, clustering, label updates, and selection from the complete catalog. These recordings illustrate behavior; frame timings are reported below.
20
+
19
21
  ## Install
20
22
 
21
23
  ```sh
@@ -83,11 +85,125 @@ Call `destroy()` when removing an interactive map.
83
85
 
84
86
  Canonical geography is in [`data/`](https://github.com/kahwee/sf-map-svg/blob/main/data/README.md), with provenance in [`SOURCES.md`](https://github.com/kahwee/sf-map-svg/blob/main/SOURCES.md). The public `/data` entry exposes lookup, catalog, district maps, and source-specific neighborhood collections. These are deeply frozen; clone before editing.
85
87
 
86
- Install with `pnpm install --frozen-lockfile`. See the [contributing guide](https://github.com/kahwee/sf-map-svg/blob/main/CONTRIBUTING.md) for architecture and the [validation and release runbook](https://github.com/kahwee/sf-map-svg/blob/main/docs/maintenance.md) for checks, browser inspection, and publishing. If you are upgrading from v2, follow the [v3 migration guide](https://github.com/kahwee/sf-map-svg/blob/main/docs/migration-v3.md).
88
+ Install with `pnpm install --frozen-lockfile`. Bun 1.4.3 is pinned in `.bun-version`; `pnpm test:bun` checks compiled-package imports and static rendering. Strict TypeScript 7.0.2 declarations and packed consumers remain part of the Node/pnpm checks. See the [contributing guide](https://github.com/kahwee/sf-map-svg/blob/main/CONTRIBUTING.md) for architecture and the [validation and release runbook](https://github.com/kahwee/sf-map-svg/blob/main/docs/maintenance.md) for checks, browser inspection, and publishing. If you are upgrading from v2, follow the [v3 migration guide](https://github.com/kahwee/sf-map-svg/blob/main/docs/migration-v3.md).
89
+
90
+ ## Browser checks and marker performance
91
+
92
+ The focused interaction suite checks a production bundle in Chromium, Firefox, and WebKit at 1440 px and 390 px. It covers 2,000-marker viewport culling and reattachment, overlapping-place selection, marker keyboard navigation and focus recovery, camera input, resizing, touch-mode scroll policy, reduced motion, and disposal. Phone widths check responsive layout in these engines; they do not replace physical-device gesture testing. The full Storybook and screenshot suites continue to use Chromium.
93
+
94
+ ```sh
95
+ pnpm exec playwright install --with-deps chromium firefox webkit
96
+ pnpm test:browsers
97
+ pnpm benchmark:markers --browsers=chromium,firefox,webkit
98
+ ```
99
+
100
+ The benchmark uses headless engines and a deterministic city grid with 500 and 2,000 labeled markers at 800 px and 390 px viewport widths. It loads the production bundle and fonts before timing, disables motion, and starts each operation from a consistent camera and clustering state. Each operation has three warmups and 20 measured samples. Results include mount, pan, zoom, clustering changes, retained-ID updates, replacement, and identical updates. Run benchmarks alone on an otherwise idle machine. Use `--labels=false` to compare label cost and `--output=test-results/another-run.json` to keep separate results.
101
+
102
+ `test-results/marker-benchmark.json` records browser versions, machine details, and p50/p95/max timings. Synchronous times include the API call and forced layout; settled times include two animation frames and are not GPU-paint measurements. Six continuous workloads record 60 frame intervals each: pan, rapid zoom, and resize with clustering on and off. Four initial frame callbacks precede the measurements. An untimed allocation probe also counts newly created pin groups, including detached nodes. These measurements describe the test machine, rather than a universal performance limit or a mobile-device guarantee. The report also records mounted pin/cluster counts at overview and two zoom levels, with clustering on and off; the picker retains all records. CI saves a shorter 2,000-marker Chromium benchmark for comparison without timing thresholds.
103
+
104
+ Measured October 10–11, 2026 against renderer commit `1d523d9`, in a Linux x64 container on an Intel Xeon Platinum 8573C with a four-CPU quota and 16 GiB memory limit, using Node 26.11.1 and Playwright 1.63.0. All 36 focused browser checks passed. The 2,000-marker results below use clustering and labels. Mount, update (retained IDs), and replacement (all new IDs) show median synchronous milliseconds; pan shows the 95th-percentile frame interval in milliseconds.
105
+
106
+ | Engine | Viewport | Mount | Update | Replace | Pan frame p95 |
107
+ | --- | ---: | ---: | ---: | ---: | ---: |
108
+ | Chromium 153 | 800 px | 147.2 | 28.3 | 92.0 | 166.7 |
109
+ | Chromium 153 | 390 px | 180.6 | 24.4 | 104.4 | 183.3 |
110
+ | Firefox 155 | 800 px | 351.0 | 55.0 | 222.0 | 200.3 |
111
+ | Firefox 155 | 390 px | 386.0 | 211.0 | 161.0 | 116.8 |
112
+ | WebKit 26.6 | 800 px | 468.0 | 105.0 | 896.0 | 302.0 |
113
+ | WebKit 26.6 | 390 px | 235.0 | 107.0 | 889.0 | 118.0 |
114
+
115
+ The baseline retained all supplied pin nodes in the SVG, including clustered pins. These frame intervals exceeded the 16.7 ms budget for 60 Hz interaction. Prefer smaller marker lists and stable IDs, and avoid repeatedly replacing every marker during interaction. Identical 2,000-marker updates took 0.7–2.0 ms at the median across these runs; full replacement was especially costly in WebKit. Keep camera-call timings separate from frame timings: a quick synchronous call does not imply a smooth rendered frame.
116
+
117
+ In that baseline, a separate 20-sample Chromium run with 2,000 markers at 800 px and `labels: false` reduced the pan frame p95 from 166.7 ms to 50.0 ms and median mount time from 147.2 ms to 115.6 ms. Disabling labels helps this workload but still does not establish 60 Hz performance. Reproduce the comparison with `pnpm benchmark:markers --counts=2000 --widths=800 --labels=false --output=test-results/marker-benchmark-no-labels.json`.
118
+
119
+ ### Viewport renderer results
120
+
121
+ The viewport renderer detaches offscreen pins and clustered members, reuses their nodes when they return, caches cluster groups during pan, and skips unchanged SVG writes. Selected and focused pins stay mounted; every supplied record remains selectable. Cluster membership uses the full catalog, so panning does not change counts at the edges.
122
+
123
+ The same 20-sample, 2,000-marker benchmark was rerun October 11, 2026 on the same host, browser versions, and label settings. Columns use the same units as the baseline above.
124
+
125
+ | Engine | Viewport | Mount | Update | Replace | Pan frame p95 |
126
+ | --- | ---: | ---: | ---: | ---: | ---: |
127
+ | Chromium 153 | 800 px | 92.3 | 17.0 | 52.8 | 16.8 |
128
+ | Chromium 153 | 390 px | 97.6 | 12.9 | 63.6 | 16.7 |
129
+ | Firefox 155 | 800 px | 106.0 | 21.0 | 54.0 | 49.4 |
130
+ | Firefox 155 | 390 px | 123.0 | 45.0 | 78.0 | 17.1 |
131
+ | WebKit 26.6 | 800 px | 160.0 | 32.0 | 805.0 | 49.0 |
132
+ | WebKit 26.6 | 390 px | 164.0 | 20.0 | 781.0 | 36.0 |
133
+
134
+ Desktop pan frame p95 improved from 166.7 → 16.8 ms in Chromium, 200.3 → 49.4 ms in Firefox, and 302.0 → 49.0 ms in WebKit. Firefox and WebKit still exceed a 60 Hz frame budget in this run. Prefer stable IDs, clustering, and fewer enabled label layers for dense interactive views; full replacement still creates all new records and picker options. These headless measurements do not establish smoothness on a physical phone.
135
+
136
+ With clustering off at 800 px, the mounted pin count fell from 2,000 in city view to 228 at the sampled 4× zoom; all 2,000 records stayed in the picker. **Checks / 2000 markers / Desktop** and **Phone** in Storybook show live counts and test zoom, pan, retained-ID updates, offscreen selection, and reset. The [plan and storyboard](https://github.com/kahwee/sf-map-svg/blob/main/docs/marker-performance-plan.md) describe the regression contract. [Saved baseline and updated JSON](https://github.com/kahwee/sf-map-svg/blob/main/docs/benchmarks/markers-2026-10-11.json) include all timing percentiles and viewport counts.
137
+
138
+ ```sh
139
+ pnpm benchmark:markers --counts=2000 --browsers=chromium,firefox,webkit \
140
+ --output=test-results/marker-benchmark-viewport.json
141
+ ```
142
+
143
+ ### Lazy pin creation and expanded workloads
144
+
145
+ Individual SVG pins now allocate on first visibility or selection, then retain their nodes for reuse. With 2,000 clustered records, an untimed probe counted zero pin groups at mount, zero after replacing every ID, and one after selecting a pin. The previous renderer created 2,000 groups at mount and another 2,000 on replacement. The complete picker, selection, keyboard focus, and deferred entrance animations remain available.
146
+
147
+ October 11, 2026 results with the same engines, labeled grid, and 20 operation samples (median synchronous milliseconds):
148
+
149
+ | Engine | Viewport | Mount | Replace all IDs |
150
+ | --- | ---: | ---: | ---: |
151
+ | chromium | 800 px | 78.1 | 39.3 |
152
+ | chromium | 390 px | 87.9 | 47.9 |
153
+ | firefox | 800 px | 88.0 | 24.0 |
154
+ | firefox | 390 px | 95.0 | 46.0 |
155
+ | webkit | 800 px | 128.0 | 717.0 |
156
+ | webkit | 390 px | 132.0 | 710.0 |
157
+
158
+ A separate run of the same expanded harness against `e41aa2b` measured Chromium mount medians of 115.1/123.6 ms and replacement medians of 57.1/89.8 ms at 800/390 px, versus 78.1/87.9 ms and 39.3/47.9 ms after lazy allocation. This paired control covers Chromium only. WebKit replacement remains expensive at 717/710 ms; stable IDs remain preferable.
159
+
160
+ The expanded stress cases reveal work that culling alone does not remove. Unclustered frame interval p95 values, in milliseconds:
161
+
162
+ | Engine | Viewport | Pan | Rapid zoom | Resize |
163
+ | --- | ---: | ---: | ---: | ---: |
164
+ | chromium | 800 px | 16.7 | 133.4 | 83.3 |
165
+ | chromium | 390 px | 33.3 | 33.4 | 50.0 |
166
+ | firefox | 800 px | 33.8 | 34.2 | 100.2 |
167
+ | firefox | 390 px | 34.2 | 49.4 | 50.7 |
168
+ | webkit | 800 px | 60.0 | 106.0 | 342.0 |
169
+ | webkit | 390 px | 114.0 | 124.0 | 192.0 |
170
+
171
+ These runs do not demonstrate a consistent frame-rate gain: desktop Chromium clustered pan p95 was 50.1 ms in the control and 66.8 ms after lazy allocation, compared with 16.8 ms in the earlier run. Rapid zoom alternates reciprocal 1.2× factors; resize alternates host widths by 80 px. Each workload runs with clustering both on and off, samples counts outside the timed loop, and retains 60 measured intervals. Headless frame timings vary and do not establish physical-device smoothness. [Saved lazy and control results](https://github.com/kahwee/sf-map-svg/blob/main/docs/benchmarks/markers-lazy-2026-10-11.json) preserve every scenario and allocation count.
172
+
173
+ ```sh
174
+ pnpm benchmark:markers --counts=2000 --browsers=chromium,firefox,webkit \
175
+ --output=test-results/marker-benchmark-lazy.json
176
+ ```
177
+
178
+ ### Cached label preparation and renderer components
179
+
180
+ Neighborhood priority is sorted once per source. Styled label candidates are reused until selection, catalog, source, layers, appearance, or zoom/width eligibility changes. Overlay bounds are measured before marker and cluster SVG updates; secondary-road visibility and scale furniture skip unchanged writes. Marker catalog reconciliation and cluster rendering now have separate modules, reducing the explorer controller from 2,316 to 2,151 lines. The interactive renderer is 34.6 KiB gzip, up from 34.2 KiB, within the unchanged 35 KiB limit; static rendering remains 7.9 KiB.
181
+
182
+ A fresh Chromium comparison against `e774fbd` used 2,000 labeled markers, 20 operation samples, both widths, and all six frame workloads in basemap and neighborhood modes. Neighborhood-mode frame interval p95 values (milliseconds):
183
+
184
+ | Workload | 800 px before | 800 px after | 390 px before | 390 px after |
185
+ | --- | ---: | ---: | ---: | ---: |
186
+ | clustered pan | 66.7 | 50.0 | 16.7 | 16.8 |
187
+ | clustered rapid zoom | 66.7 | 83.4 | 33.4 | 16.8 |
188
+ | clustered resize | 16.7 | 66.7 | 66.6 | 16.8 |
189
+ | unclustered pan | 16.7 | 49.9 | 33.3 | 66.6 |
190
+ | unclustered rapid zoom | 116.6 | 33.4 | 66.7 | 83.4 |
191
+ | unclustered resize | 66.7 | 33.3 | 50.0 | 50.1 |
192
+
193
+ Results are mixed rather than a consistent frame-rate gain. Basemap desktop clustered pan p95 changed from 50.0 to 16.8 ms, while phone unclustered pan changed from 16.8 to 50.0 ms. Basemap mount medians were 72.1 → 88.8 ms at 800 px and 88.3 → 89.1 ms at 390 px; neighborhood-mode mount medians were 81.5 → 73.3 ms and 93.9 → 89.0 ms. Viewport counts and allocation probes match before and after. [Saved comparisons](https://github.com/kahwee/sf-map-svg/blob/main/docs/benchmarks/markers-refactor-2026-10-11.json) include every operation and scenario; these headless runs do not establish physical-device smoothness.
194
+
195
+ ```sh
196
+ pnpm benchmark:markers --counts=2000 --mode=neighborhoods \
197
+ --output=test-results/marker-benchmark-neighborhoods.json
198
+ ```
199
+
200
+ ## CI maintenance
201
+
202
+ [GitHub Actions maintenance](https://github.com/kahwee/sf-map-svg/blob/main/.github/ACTIONS.md) covers workflows, parallel checks, action versions, and weekly updates.
87
203
 
88
204
  ---
89
205
 
90
- <!-- Generated from docs/developer-guide.md; @kahwee/sf-map-svg 4.3.0 · local working tree. -->
206
+ <!-- Generated from docs/developer-guide.md; @kahwee/sf-map-svg 4.3.2 · local working tree. -->
91
207
 
92
208
  # Developer guide
93
209
 
@@ -231,7 +347,7 @@ See [API options](https://github.com/kahwee/sf-map-svg/blob/main/docs/API.md), [
231
347
 
232
348
  ---
233
349
 
234
- <!-- Generated from docs/API.md; @kahwee/sf-map-svg 4.3.0 · local working tree. -->
350
+ <!-- Generated from docs/API.md; @kahwee/sf-map-svg 4.3.2 · local working tree. -->
235
351
 
236
352
  # Map options and lifecycle
237
353
 
@@ -254,12 +370,14 @@ const { svg } = renderMap(staticMapData, presentation);
254
370
  const map = createMap(fullMapData, presentation);
255
371
  ```
256
372
 
257
- Static `layers` supports every interactive layer switch except `neighborhoodLabels`. Static `appearance` supports `theme`, `colors`, and `districtStyle`; browser typography, area interaction styles, and screen-space marker sizing remain interactive settings. Unknown grouped keys are rejected. Supplied grouped keys override flat compatibility keys; omitted or `undefined` grouped keys preserve the flat value or renderer default. The two renderers retain their existing defaults. Marker arrays accept readonly inputs.
373
+ Static `layers` supports every interactive layer switch except `neighborhoodLabels`. Static `appearance` supports `theme`, `colors`, and `districtStyle`; browser typography, area interaction styles, and screen-space marker sizing remain interactive settings. Unknown keys and invalid values are rejected for both flat and grouped options. Supplied grouped keys override flat compatibility keys; omitted or `undefined` grouped keys preserve the flat value or renderer default. Undefined color tokens use the theme default instead of entering SVG attributes. The two renderers retain their existing defaults. Marker arrays accept readonly inputs.
258
374
 
259
375
  Flat compatibility options include `theme`, `width`, `height`, `padding`, `year` (2002, 2012, 2022), `districtLines`, `districtFills`, `districtStyle`, `districtLabels`, `neighborhoodLines`, `labels`, `highways`, `keyRoads`, `roadLabels`, `landmarks`, `bartStations`, `markers`, `overlays`, `title`, `idPrefix`, `colors`, and `animation`. Each optional layer is independent. User-supplied text and attributes are escaped in SVG output.
260
376
 
261
377
  Custom district IDs must be finite numbers. An empty `labelPoints` array uses the district's primary `label` position.
262
378
 
379
+ The SVG description names the selected neighborhood source for source-aware data and uses a generic description for compact inputs. It describes enabled layers only when their data is supplied, using actual park and station counts.
380
+
263
381
  `animation: true` (or `{ duration, delay }`) makes a static map draw itself with self-contained, scoped CSS: land fades in, the coast and lines draw, districts grow, then labels and points appear. It plays when the SVG is inserted into a page or loaded as an image, needs no JavaScript, and stays still under `prefers-reduced-motion`. Re-insert the markup to replay it. Default output is unchanged.
264
382
 
265
383
  ## Interactive maps
@@ -272,7 +390,13 @@ The controller also supports marker, neighborhood, and district selection; distr
272
390
 
273
391
  If a district callback destroys the map or selects a newer district, the interrupted activation stops without moving keyboard focus or sending a stale event.
274
392
 
275
- `setMarkers()` reconciles by stable marker `id`: retained markers keep their DOM nodes, focus, and in-progress entrance animations; only new IDs animate in. Identical ordered marker updates are a visual no-op. Camera state and `viewportchange` events remain synchronous, while animated camera steps and their dependent label/marker layout commit in the same browser frame.
393
+ `setMarkers()` reconciles by stable marker `id`: retained markers keep any previously created nodes, focus, and in-progress entrance animations. New pin entrances start when the pin first becomes visible; a cached pin does not animate again merely because it returns to the viewport. Disabling entrances, reduced-motion changes, and destruction cancel pending and active entrances. Identical ordered marker updates are a visual no-op. Camera state and `viewportchange` events remain synchronous, while animated camera steps and their dependent label/marker layout commit in the same browser frame.
394
+
395
+ Interactive maps create individual pin nodes only on first visibility or selection, then cache them for reuse. Record and style updates before first visibility apply to the eventual node. They mount only pins and cluster symbols intersecting a padded viewport; clustered member pins detach. All records remain in the native picker and selectable through the API. Selected and keyboard-focused pins stay mounted, even outside the viewport, and retained nodes return when panning back. Cluster membership uses the complete catalog and stays stable during pan at the same zoom. Static SVG exports are unaffected.
396
+
397
+ Visible markers and clusters share one tab stop. Use `[` and `]` to move focus, Enter or Space to activate, and Tab to leave the group. Arrow keys retain map panning. Marker replacement preserves the focused ID when available and moves focus to a surviving marker when it is removed.
398
+
399
+ Activating coincident pins opens a place chooser, including when compact embeds hide the native marker picker. Clusters zoom when their members can separate at maximum zoom; otherwise they open the chooser. Escape or Close returns focus to the originating pin or cluster when it remains visible. Choosing a place selects its marker; replacing markers or destroying the map closes the chooser. `clusteractivate` still reports the activated cluster's markers.
276
400
 
277
401
  For a small guide, import `guideMapData` from `/guide/data` and pass it to `createMap`. The optional `/guide` entry also provides `createGuideMap`, `mountGuideMap`, and detailed-data loading for existing guide layouts. `/transit` provides the standalone schematic transit animation. See [examples](https://github.com/kahwee/sf-map-svg/blob/main/docs/EXAMPLES.md) and [consumer integration](https://github.com/kahwee/sf-map-svg/blob/main/docs/consumer-integration.md).
278
402
 
@@ -329,13 +453,13 @@ Neighborhood IDs are exact source-scoped feature IDs, rather than display names
329
453
 
330
454
  ## Coordinates and units
331
455
 
332
- GeoJSON and static projection inputs use `[longitude, latitude]` in WGS84; markers use `{ lng, lat }`. Static `project` returns SVG units. `projectToScreen(lng, lat)` returns pixels relative to the mounted canvas. Camera viewports and pan deltas use the fixed 800 × 800 projected map space, independent of canvas size. Marker radius uses screen pixels in browser maps and SVG units in static output.
456
+ GeoJSON and static projection inputs use `[longitude, latitude]` in WGS84; markers use `{ lng, lat }`. Static `project` returns SVG units. `projectToScreen(lng, lat)` returns pixels relative to the mounted canvas. Camera viewports use the fixed 800 × 800 projected map space, independent of canvas size. Pan deltas use screen pixels, converted using the mounted canvas width. Marker radius uses screen pixels in browser maps and SVG units in static output.
333
457
 
334
458
  Explore presentation options in the [live map design playground](https://kahwee.github.io/sf-map-svg/playground.html). The page identifies its package version; released builds use compatible options for that installed version.
335
459
 
336
460
  ---
337
461
 
338
- <!-- Generated from docs/EXAMPLES.md; @kahwee/sf-map-svg 4.3.0 · local working tree. -->
462
+ <!-- Generated from docs/EXAMPLES.md; @kahwee/sf-map-svg 4.3.2 · local working tree. -->
339
463
 
340
464
  # Examples
341
465
 
@@ -446,7 +570,7 @@ For the controller and explicit data imports, see [the v3 migration guide](https
446
570
 
447
571
  ---
448
572
 
449
- <!-- Generated from docs/consumer-integration.md; @kahwee/sf-map-svg 4.3.0 · local working tree. -->
573
+ <!-- Generated from docs/consumer-integration.md; @kahwee/sf-map-svg 4.3.2 · local working tree. -->
450
574
 
451
575
  # Embedding and progressive enhancement
452
576
 
@@ -498,7 +622,7 @@ See the [exact API contract](https://github.com/kahwee/sf-map-svg/blob/main/docs
498
622
 
499
623
  ---
500
624
 
501
- <!-- Generated from docs/migration-v3.md; @kahwee/sf-map-svg 4.3.0 · local working tree. -->
625
+ <!-- Generated from docs/migration-v3.md; @kahwee/sf-map-svg 4.3.2 · local working tree. -->
502
626
 
503
627
  # Migrate to version 3
504
628
 
@@ -539,7 +663,7 @@ District styles are now prepared once per district on construction and style/yea
539
663
 
540
664
  ---
541
665
 
542
- <!-- Generated from SOURCES.md; @kahwee/sf-map-svg 4.3.0 · local working tree. -->
666
+ <!-- Generated from SOURCES.md; @kahwee/sf-map-svg 4.3.2 · local working tree. -->
543
667
 
544
668
  # Geometry and provenance
545
669
 
@@ -723,7 +847,7 @@ display geometry, and no precinct polygons or raw workbook data are shipped.
723
847
 
724
848
  ---
725
849
 
726
- <!-- Generated from CHANGELOG.md; @kahwee/sf-map-svg 4.3.0 · local working tree. -->
850
+ <!-- Generated from CHANGELOG.md; @kahwee/sf-map-svg 4.3.2 · local working tree. -->
727
851
 
728
852
  # Changelog
729
853
 
@@ -731,6 +855,30 @@ User-visible changes are recorded here. Unreleased entries describe changes on `
731
855
 
732
856
  ## Unreleased
733
857
 
858
+ ## 4.3.2 — 2026-10-10
859
+
860
+ - Update compatible development tooling and refresh dependency security fixes.
861
+
862
+ - Cache label candidates across camera frames, prepare neighborhood priority once per source, and measure overlays before SVG updates.
863
+ - Extract marker catalog reconciliation and cluster rendering with explicit cache and listener ownership; add label refresh checks across catalog, appearance, layers, and source changes.
864
+
865
+ - Create individual marker visuals only on first visibility or selection, keeping the full catalog and retained nodes. Defer new pin entrances and cancel pending entrances with active animations.
866
+ - Benchmark clustered and unclustered continuous pan, rapid zoom, and resize, with separate allocation probes.
867
+
868
+ - Cull offscreen pins and cluster symbols while preserving the full catalog, cached nodes, selection, and keyboard focus. Cache cluster groups during pan and skip unchanged SVG attributes.
869
+ - Add interactive 2,000-marker desktop and phone stories, viewport count benchmarks, and a Bun 1.4.3 compatibility gate.
870
+
871
+ - Add production-bundle interaction checks in Chromium, Firefox, and WebKit, plus reproducible marker benchmarks and CI performance artifacts.
872
+ - Add optimized desktop and phone marker screencasts to the Pages examples, with captions and playback size checks.
873
+ - Add a place chooser for coincident pins and clusters that cannot separate at maximum zoom, including compact embeds with hidden native pickers.
874
+ - Give visible markers and clusters one tab stop with bracket-key navigation, preserving focus across marker reordering and recovering focus when a marker is removed.
875
+
876
+ ## 4.3.1 — 2026-10-04
877
+
878
+ - Reject unknown flat static options and invalid values before rendering or invoking district styling callbacks, matching grouped-option validation.
879
+ - Ignore undefined static color overrides so SVG attributes retain the selected theme's defaults; preserve grouped-option precedence.
880
+ - Describe the actual neighborhood source, enabled and supplied layers, and park and BART station counts in static SVG descriptions. Compact geography uses a source-neutral description.
881
+
734
882
  ## 4.3.0 — 2026-10-02
735
883
 
736
884
  - Add `selectFeature` with typed marker, neighborhood source/ID and district year/ID references. Missing or mismatched identities preserve current geography and selection.
@@ -1043,7 +1191,7 @@ consistent home, and makes animation and subscription ownership predictable.
1043
1191
 
1044
1192
  # Published type contracts
1045
1193
 
1046
- @kahwee/sf-map-svg 4.3.0 · local working tree. These declarations are generated by TypeScript. Use package export paths, not the internal filenames below. Imported relative contracts are included for context, not as additional public entry points.
1194
+ @kahwee/sf-map-svg 4.3.2 · local working tree. These declarations are generated by TypeScript. Use package export paths, not the internal filenames below. Imported relative contracts are included for context, not as additional public entry points.
1047
1195
 
1048
1196
  | Public import | Declaration |
1049
1197
  | --- | --- |
@@ -1224,7 +1372,7 @@ export interface MapCamera {
1224
1372
  fit(geometry: Geometry, options?: CameraOptions & {
1225
1373
  padding?: number | MapPadding;
1226
1374
  }): void;
1227
- /** Delta in projected map units. */
1375
+ /** Delta in screen pixels, converted using the mounted canvas width. */
1228
1376
  pan(x: number, y: number, options?: CameraOptions): void;
1229
1377
  zoom(factor: number, options?: CameraOptions): void;
1230
1378
  reset(options?: CameraOptions): void;
@@ -1241,7 +1389,7 @@ export interface MapController {
1241
1389
  getResolvedConfiguration(): ResolvedMapConfiguration;
1242
1390
  getCapabilities(): MapCapabilities;
1243
1391
  on<K extends keyof MapEvents>(type: K, listener: (detail: MapEvents[K]) => void): () => void;
1244
- /** Reconcile stable IDs; retained markers preserve nodes, focus, and entrance animations. */
1392
+ /** Reconcile stable IDs; preserve existing nodes and focus. Pin visuals allocate on first visibility/selection. */
1245
1393
  setMarkers(markers: readonly MapMarker[]): void;
1246
1394
  setOverlays(overlays: readonly MapOverlay[]): void;
1247
1395
  /** False for missing IDs or a source/year other than the current geography. */
@@ -1655,7 +1803,7 @@ export interface NeighborhoodExplorerElement extends HTMLElement {
1655
1803
  fitGeometry(geometry: Geometry, padding?: number | MapPadding, options?: CameraOptions): void;
1656
1804
  /** Explicitly engage map touch gestures; false restores page gestures. */
1657
1805
  setTouchNavigation(enabled: boolean): void;
1658
- /** Reconcile stable IDs; retained markers preserve nodes, focus, and entrance animations. */
1806
+ /** Reconcile stable IDs; preserve existing nodes and focus. Pin visuals allocate on first visibility/selection. */
1659
1807
  setMarkers(markers: readonly MapMarker[]): void;
1660
1808
  setOverlays(overlays: readonly MapOverlay[]): void;
1661
1809
  selectMarker(id: string | null, options?: {
package/llms.txt CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Offline San Francisco SVG maps with explicit geographic imports, no runtime dependencies, static rendering, and a browser controller.
4
4
 
5
- @kahwee/sf-map-svg 4.3.0 · local working tree. Node >=24.0.0 for server rendering; browsers need a DOM and a JSON-capable bundler.
5
+ @kahwee/sf-map-svg 4.3.2 · local working tree. Node >=24.0.0 for server rendering; browsers need a DOM and a JSON-capable bundler.
6
6
 
7
7
  These are working-tree docs. Unreleased changelog entries may describe APIs absent from npm; verify the installed version before copying them.
8
8
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kahwee/sf-map-svg",
3
- "version": "4.3.0",
3
+ "version": "4.3.2",
4
4
  "description": "Offline SVG maps of San Francisco with district boundaries, parks, landmarks, and BART stations.",
5
5
  "type": "module",
6
6
  "main": "./dist/src/api.js",
@@ -135,22 +135,22 @@
135
135
  "node": ">=24.0.0"
136
136
  },
137
137
  "devDependencies": {
138
- "@biomejs/biome": "2.5.14",
139
- "@storybook/addon-a11y": "10.6.0",
140
- "@storybook/addon-docs": "^10.6.0",
141
- "@storybook/addon-vitest": "10.6.0",
142
- "@storybook/html-vite": "^10.6.0",
143
- "@vitest/browser-playwright": "^5.0.2",
144
- "@vitest/coverage-v8": "5.0.2",
138
+ "@biomejs/biome": "2.5.15",
139
+ "@storybook/addon-a11y": "10.6.1",
140
+ "@storybook/addon-docs": "^10.6.1",
141
+ "@storybook/addon-vitest": "10.6.1",
142
+ "@storybook/html-vite": "^10.6.1",
143
+ "@vitest/browser-playwright": "^5.0.3",
144
+ "@vitest/coverage-v8": "5.0.3",
145
145
  "@xmldom/xmldom": "^0.9.12",
146
146
  "playwright": "^1.63.0",
147
147
  "polygon-clipping": "^0.15.7",
148
148
  "react": "^19.3.0",
149
149
  "react-dom": "^19.3.0",
150
- "storybook": "^10.6.0",
150
+ "storybook": "^10.6.1",
151
151
  "typescript": "^7.0.2",
152
- "vite": "^8.3.1",
153
- "vitest": "^5.0.2"
152
+ "vite": "^8.3.3",
153
+ "vitest": "^5.0.3"
154
154
  },
155
155
  "homepage": "https://kahwee.github.io/sf-map-svg/",
156
156
  "bugs": {
@@ -172,6 +172,9 @@
172
172
  "test:stories": "pnpm build && vitest run --project=storybook",
173
173
  "test:stories:coverage": "pnpm build && vitest run --project=storybook --coverage",
174
174
  "test:visual": "playwright test --config=playwright.visual.config.mjs",
175
+ "test:browsers": "playwright test --config=playwright.browser.config.mjs",
176
+ "test:bun": "pnpm build && bun scripts/smoke-bun.mjs",
177
+ "benchmark:markers": "node scripts/benchmark-markers.mjs",
175
178
  "demo": "pnpm build && node examples/build.mjs",
176
179
  "format": "biome check --write .",
177
180
  "format:check": "biome format .",