@kahwee/sf-map-svg 4.5.0 → 5.1.0
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/CHANGELOG.md +24 -0
- package/README.md +49 -2
- package/SOURCES.md +53 -0
- package/dist/data/README.md +7 -0
- package/dist/data/index.d.ts +1 -0
- package/dist/data/index.js +1 -0
- package/dist/data/precincts-2022.json +1 -0
- package/dist/data/precincts.d.ts +2 -0
- package/dist/data/precincts.js +5 -0
- package/dist/data/types.d.ts +5 -0
- package/dist/src/cards.js +1 -1
- package/dist/src/configuration.d.ts +2 -0
- package/dist/src/configuration.js +30 -0
- package/dist/src/controller-types.d.ts +11 -15
- package/dist/src/explorer-core.js +10 -5
- package/dist/src/geometry.d.ts +6 -1
- package/dist/src/geometry.js +80 -0
- package/dist/src/guide-static.d.ts +1 -1
- package/dist/src/layers.js +1 -1
- package/dist/src/map.d.ts +1 -1
- package/dist/src/map.js +22 -12
- package/dist/src/marker-catalog.d.ts +1 -1
- package/dist/src/marker-catalog.js +11 -5
- package/dist/src/static-labels.js +3 -3
- package/dist/src/static.d.ts +2 -2
- package/dist/src/types.d.ts +12 -9
- package/dist/src/validation.js +7 -5
- package/docs/API.md +43 -5
- package/docs/EXAMPLES.md +29 -0
- package/docs/api-audit.md +22 -5
- package/docs/consumer-integration.md +47 -1
- package/docs/developer-guide.md +29 -7
- package/docs/guide-bundle-report.md +4 -4
- package/docs/migration-v5.md +100 -0
- package/llms-full.txt +436 -54
- package/llms.txt +2 -1
- package/package.json +38 -24
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
|
|
5
|
+
@kahwee/sf-map-svg 5.1.0 · 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
|
|
11
|
+
<!-- Generated from README.md; @kahwee/sf-map-svg 5.1.0 · local working tree. -->
|
|
12
12
|
|
|
13
13
|
# SF Map SVG
|
|
14
14
|
|
|
@@ -18,6 +18,11 @@ Offline, self-contained San Francisco SVG maps. The package has no runtime depen
|
|
|
18
18
|
|
|
19
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
20
|
|
|
21
|
+
## Upgrading to v5
|
|
22
|
+
|
|
23
|
+
Version 5.0.0 standardizes coordinate tuples and controller reset behavior.
|
|
24
|
+
Read the [migration guide](https://github.com/kahwee/sf-map-svg/blob/main/docs/migration-v5.md) before upgrading from v4.
|
|
25
|
+
|
|
21
26
|
## Install
|
|
22
27
|
|
|
23
28
|
```sh
|
|
@@ -88,12 +93,39 @@ function disposeMap() {
|
|
|
88
93
|
}
|
|
89
94
|
```
|
|
90
95
|
|
|
91
|
-
`createMap(data, options)` returns a controller. Use `map.element` for mounting, `map.configure({ features, layers, controls, appearance, mode, source, year, labels })` for runtime switches, `map.camera` for pan/zoom/fit/reset, and `map.on()` for typed events. Appearance can change live while preserving camera, selection, and focus. `getResolvedConfiguration()` explains effective layers; `getCapabilities()` reports supplied geography. SFAR realtor neighborhoods are the default when supplied; SF Find and analysis are explicit alternate sources. The `/data/full` preset includes all three collections and historical districts.
|
|
96
|
+
`createMap(data, options)` returns a controller. Use `map.element` for mounting, `map.configure({ features, layers, controls, appearance, mode, source, year, labels })` for runtime switches, `map.camera` for pan/zoom/fit/reset, and `map.on()` for typed events. Appearance can change live while preserving camera, selection, and focus. `getResolvedConfiguration()` explains effective layers; `getCapabilities()` reports supplied geography. SFAR realtor neighborhoods are the default when supplied; SF Find and analysis are explicit alternate sources. The `/data/full` preset includes all three collections and historical districts. `precinctMaps[2022]` from `@kahwee/sf-map-svg/data/precincts` holds the 514 Department of Elections precinct boundaries (data only, not a rendered layer).
|
|
92
97
|
|
|
93
98
|
See [controller options, events, and lifecycle](https://github.com/kahwee/sf-map-svg/blob/main/docs/API.md#interactive-maps),
|
|
94
99
|
[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).
|
|
95
100
|
Call `destroy()` when removing an interactive map.
|
|
96
101
|
|
|
102
|
+
## Geographic queries and screen coordinates
|
|
103
|
+
|
|
104
|
+
New in v5. See the [v5 migration guide](https://github.com/kahwee/sf-map-svg/blob/main/docs/migration-v5.md) for breaking changes.
|
|
105
|
+
|
|
106
|
+
Use the data-free `/geometry` entry to calculate geographic bounds or test which
|
|
107
|
+
polygons contain a location, without constructing a map or requiring a browser:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { geometryBounds, geometryContainsPoint } from '@kahwee/sf-map-svg/geometry';
|
|
111
|
+
import { neighborhoods } from '@kahwee/sf-map-svg/data/realtor';
|
|
112
|
+
|
|
113
|
+
const location = [-122.42, 37.76] as const;
|
|
114
|
+
const matches = neighborhoods.features.filter(({ geometry }) =>
|
|
115
|
+
geometryContainsPoint(geometry, location),
|
|
116
|
+
);
|
|
117
|
+
const bounds = matches[0] ? geometryBounds(matches[0].geometry) : null;
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Polygon holes are respected; shared boundaries can match multiple areas.
|
|
121
|
+
Bounds are `[west, south, east, north]` in degrees, not camera coordinates.
|
|
122
|
+
The [query contract](https://github.com/kahwee/sf-map-svg/blob/main/docs/API.md#geographic-queries) covers empty geometry,
|
|
123
|
+
validation, and geographic limitations.
|
|
124
|
+
|
|
125
|
+
For mounted browser maps, `map.projectToScreen(location)` returns canvas-relative
|
|
126
|
+
CSS pixels and viewport visibility. All coordinate inputs now use GeoJSON tuples. See the [overlay recipe](https://github.com/kahwee/sf-map-svg/blob/main/docs/consumer-integration.md#position-html-overlays)
|
|
127
|
+
and [coordinate units](https://github.com/kahwee/sf-map-svg/blob/main/docs/API.md#coordinates-and-units).
|
|
128
|
+
|
|
97
129
|
## Documentation for coding assistants
|
|
98
130
|
|
|
99
131
|
[Developer guide](https://github.com/kahwee/sf-map-svg/blob/main/docs/developer-guide.md) · [llms.txt index](https://github.com/kahwee/sf-map-svg/blob/main/llms.txt) · [Complete text documentation](https://github.com/kahwee/sf-map-svg/blob/main/llms-full.txt). Generated from project docs and TypeScript declarations; run `pnpm docs:llms` after changing those inputs.
|
|
@@ -102,7 +134,7 @@ Call `destroy()` when removing an interactive map.
|
|
|
102
134
|
|
|
103
135
|
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.
|
|
104
136
|
|
|
105
|
-
Install with `pnpm install --frozen-lockfile`. Bun 1.4.
|
|
137
|
+
Install with `pnpm install --frozen-lockfile`. Bun 1.4.4 is pinned in `.bun-version` for development builds and type checks; `pnpm test:bun` also 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).
|
|
106
138
|
|
|
107
139
|
## Browser checks and marker performance
|
|
108
140
|
|
|
@@ -218,9 +250,24 @@ pnpm benchmark:markers --counts=2000 --mode=neighborhoods \
|
|
|
218
250
|
|
|
219
251
|
[GitHub Actions maintenance](https://github.com/kahwee/sf-map-svg/blob/main/.github/ACTIONS.md) covers workflows, parallel checks, action versions, and weekly updates.
|
|
220
252
|
|
|
253
|
+
Project type checks use Bun: `pnpm check:types:built` checks compiled-package
|
|
254
|
+
consumers, Storybook, and the website in parallel after a build. `pnpm check` runs
|
|
255
|
+
the full gate. CI runs `check:node` on Node 24/26 and these Bun checks once in the
|
|
256
|
+
Bun job. TypeScript remains responsible for declaration emission and packed-package
|
|
257
|
+
consumer compatibility tests. Published consumers do not need Bun.
|
|
258
|
+
|
|
259
|
+
On the session’s Linux executor, three-run median project checks measured
|
|
260
|
+
0.402/0.688/0.209 seconds with Bun for consumers/stories/website, versus
|
|
261
|
+
3.156/4.110/2.496 seconds with `pnpm exec tsc -p <project>`. These include command
|
|
262
|
+
startup; they do not predict hosted CI duration.
|
|
263
|
+
|
|
264
|
+
CI reuses completed library output for demos, Storybook, and Pages. Standalone
|
|
265
|
+
commands still build from a clean checkout. See the [toolchain benchmark report](https://github.com/kahwee/sf-map-svg/blob/main/docs/toolchain-benchmarks.md)
|
|
266
|
+
for raw samples, output-equivalence checks, and reproducible commands.
|
|
267
|
+
|
|
221
268
|
---
|
|
222
269
|
|
|
223
|
-
<!-- Generated from docs/developer-guide.md; @kahwee/sf-map-svg
|
|
270
|
+
<!-- Generated from docs/developer-guide.md; @kahwee/sf-map-svg 5.1.0 · local working tree. -->
|
|
224
271
|
|
|
225
272
|
# Developer guide
|
|
226
273
|
|
|
@@ -234,6 +281,7 @@ Choose the renderer and geography separately. `renderMap` produces a string with
|
|
|
234
281
|
| Explore districts or several neighborhood definitions | `/map`: `createMap` | `/data/full`: `fullMapData` |
|
|
235
282
|
| Embed a smaller neighborhood guide | `/guide/map`: `createGuideController` | Included lightweight guide geography |
|
|
236
283
|
| Use the guide data with the general controller | `/map`: `createMap` | `/guide/data`: `guideMapData` |
|
|
284
|
+
| Query bounds or polygon containment without a DOM | `/geometry` | Caller-supplied geometry or explicit dataset imports |
|
|
237
285
|
| Look up names without mounting a map | `/data/lookup` | Source-aware lookup helpers |
|
|
238
286
|
|
|
239
287
|
Imports starting with `/` in this table are package suffixes, such as `@kahwee/sf-map-svg/static`. Node 24+ is required for server rendering. Browser examples need a DOM and a bundler that supports JSON modules. This is an ESM package.
|
|
@@ -320,22 +368,43 @@ SFAR realtor neighborhoods are the default when supplied. If only an alternative
|
|
|
320
368
|
| Camera | `map.camera.set/pan/zoom/fit/reset(...)` |
|
|
321
369
|
| Palette, typography, control styling | `map.configure({ appearance })`; retains camera, selection and focus |
|
|
322
370
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
`
|
|
371
|
+
Controller patches merge nested token and feature records. Omitted and `undefined`
|
|
372
|
+
values retain existing values. Use `false` to disable a boolean switch and
|
|
373
|
+
`resetConfiguration()` to clear presentation overrides explicitly:
|
|
326
374
|
|
|
327
375
|
```ts
|
|
328
376
|
map.configure({ layers: { neighborhoodLabels: false } });
|
|
329
|
-
map.configure({ layers: { neighborhoodLabels: undefined } }); // follow mode again
|
|
330
377
|
map.configure({ features: { motion: false } });
|
|
331
|
-
map.
|
|
378
|
+
map.resetConfiguration(['layers']); // follow mode-derived defaults again
|
|
379
|
+
map.resetConfiguration(['appearance']); // built-in palette and typography
|
|
332
380
|
```
|
|
333
381
|
|
|
334
|
-
|
|
382
|
+
Reset groups are `features`, `layers`, `controls`, and `appearance`; omitting
|
|
383
|
+
the argument resets all four. Resets preserve geographic choices and camera,
|
|
384
|
+
and do not restore constructor overrides. Static maps accept grouped layers and
|
|
385
|
+
the shared appearance keys (`theme`, `colors`, `districtStyle`) at construction.
|
|
386
|
+
See [the API contract](https://github.com/kahwee/sf-map-svg/blob/main/docs/API.md#live-configuration-and-inspection).
|
|
387
|
+
|
|
388
|
+
`getConfiguration()` returns a detached snapshot of geographic choices and
|
|
389
|
+
presentation overrides. Use `getResolvedConfiguration()` for effective layers
|
|
390
|
+
and `getCapabilities()` for supplied geography. Invalid patches preserve state.
|
|
391
|
+
`setMode`, `setSource`, and `configure` all preserve the camera; request movement
|
|
392
|
+
with `camera.reset()` or `camera.fit(geometry)`. See the
|
|
393
|
+
[v5 migration guide](https://github.com/kahwee/sf-map-svg/blob/main/docs/migration-v5.md) for changed call forms.
|
|
394
|
+
|
|
395
|
+
## Geographic queries
|
|
396
|
+
|
|
397
|
+
The v5 `/geometry` helpers `geometryBounds(geometry)` and
|
|
398
|
+
`geometryContainsPoint(geometry, position)` work without a DOM or bundled
|
|
399
|
+
geography. Import `Bounds`, `Geometry`, and `Position` types from the same entry.
|
|
400
|
+
Use containment for area lookup and bounds for geographic extents; pass the
|
|
401
|
+
original geometry to `camera.fit`, rather than geographic bounds to `camera.set`.
|
|
402
|
+
See the [worked example](https://github.com/kahwee/sf-map-svg/blob/main/docs/EXAMPLES.md#find-areas-containing-a-location) and
|
|
403
|
+
[full query semantics](https://github.com/kahwee/sf-map-svg/blob/main/docs/API.md#geographic-queries).
|
|
335
404
|
|
|
336
405
|
## Markers, routes and camera coordinates
|
|
337
406
|
|
|
338
|
-
|
|
407
|
+
Markers use `position: [longitude, latitude]`. GeoJSON coordinates and the static projection use `[longitude, latitude]`, in WGS84. Camera viewports use `[x, y, size]` in the fixed 800 × 800 projected map space; they are not geographic bounds. `camera.pan` takes deltas in CSS pixels and converts them using the mounted canvas width. `projectToScreen([lng, lat])` returns CSS pixels relative to the map canvas, plus viewport visibility. The former two-number call is removed in v5.
|
|
339
408
|
|
|
340
409
|
Marker and overlay IDs must be unique. Keep marker IDs stable across updates so retained markers preserve DOM nodes, focus and entrance animations. Use `null` to clear a selection; selection methods return a boolean. Selection events can contain null values. `map.on()` returns an unsubscribe function.
|
|
341
410
|
|
|
@@ -364,7 +433,7 @@ See [API options](https://github.com/kahwee/sf-map-svg/blob/main/docs/API.md), [
|
|
|
364
433
|
|
|
365
434
|
---
|
|
366
435
|
|
|
367
|
-
<!-- Generated from docs/API.md; @kahwee/sf-map-svg
|
|
436
|
+
<!-- Generated from docs/API.md; @kahwee/sf-map-svg 5.1.0 · local working tree. -->
|
|
368
437
|
|
|
369
438
|
# Map options and lifecycle
|
|
370
439
|
|
|
@@ -372,6 +441,41 @@ Start with the [static and interactive examples](https://github.com/kahwee/sf-ma
|
|
|
372
441
|
and event types are exported by [src/api.ts](https://github.com/kahwee/sf-map-svg/blob/main/src/api.ts) and defined in
|
|
373
442
|
[src/types.ts](https://github.com/kahwee/sf-map-svg/blob/main/src/types.ts) and [src/controller-types.ts](https://github.com/kahwee/sf-map-svg/blob/main/src/controller-types.ts).
|
|
374
443
|
|
|
444
|
+
## Geographic queries
|
|
445
|
+
|
|
446
|
+
New in v5. See the [migration guide](https://github.com/kahwee/sf-map-svg/blob/main/docs/migration-v5.md) for upgrade steps.
|
|
447
|
+
|
|
448
|
+
The data-free `@kahwee/sf-map-svg/geometry` entry exports `geometryBounds(geometry)`
|
|
449
|
+
and `geometryContainsPoint(geometry, [longitude, latitude])`. They work offline
|
|
450
|
+
with the same readonly GeoJSON geometries used by the renderer, without a DOM.
|
|
451
|
+
The same entry exports the `Geometry`, `Position`, and `Bounds` types, so geometry
|
|
452
|
+
consumers need no second import path for their function input and result types.
|
|
453
|
+
|
|
454
|
+
```ts
|
|
455
|
+
import { geometryBounds, geometryContainsPoint } from '@kahwee/sf-map-svg/geometry';
|
|
456
|
+
import { neighborhoods } from '@kahwee/sf-map-svg/data/realtor';
|
|
457
|
+
|
|
458
|
+
const location = [-122.42, 37.76] as const;
|
|
459
|
+
const matches = neighborhoods.features.filter((feature) =>
|
|
460
|
+
geometryContainsPoint(feature.geometry, location),
|
|
461
|
+
);
|
|
462
|
+
const bounds = matches[0] ? geometryBounds(matches[0].geometry) : null;
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
Bounds are `[west, south, east, north]` in degrees, computed from coordinates,
|
|
466
|
+
not cached feature metadata. Empty or absent geometry returns `null`.
|
|
467
|
+
Hit testing includes Polygon, MultiPolygon, and polygon members of nested
|
|
468
|
+
GeometryCollections. Hole interiors are excluded; outer and hole boundaries
|
|
469
|
+
are included, so a shared boundary may match multiple features. Ring winding
|
|
470
|
+
does not matter; open rings are closed implicitly. Other geometry types do
|
|
471
|
+
not produce polygon hits. Empty or absent geometry returns `false`.
|
|
472
|
+
|
|
473
|
+
Both helpers reject nonfinite or out-of-range longitude/latitude with
|
|
474
|
+
`RangeError`; altitude is ignored. They assume valid planar GeoJSON topology
|
|
475
|
+
and straight segments in longitude/latitude, suitable for these SF datasets.
|
|
476
|
+
They do not repair polygons, apply a boundary tolerance, or unwrap antimeridian
|
|
477
|
+
crossings. A bounds match alone does not establish polygon containment.
|
|
478
|
+
|
|
375
479
|
## Static maps
|
|
376
480
|
|
|
377
481
|
Static maps accept compact `StaticMapData` or the same source-aware `MapData` as interactive maps. With `MapData`, `source` selects a supplied neighborhood definition; SFAR realtor is preferred by default, followed by SF Find and analysis when available. A requested unavailable source throws before styling callbacks run. Compact `StaticMapData` already chooses its neighborhoods, so it rejects a `source` option. Both data shapes retain the static renderer defaults.
|
|
@@ -453,16 +557,16 @@ District style callbacks are evaluated once per district at construction and on
|
|
|
453
557
|
|
|
454
558
|
## Live configuration and inspection
|
|
455
559
|
|
|
456
|
-
`map.configure({ appearance, mode, source, year, labels, layers, features, controls })` updates an existing map. Appearance updates preserve the camera, selections, retained marker nodes, and keyboard focus. Mode/source changes through `configure
|
|
560
|
+
`map.configure({ appearance, mode, source, year, labels, layers, features, controls })` updates an existing map. Appearance updates preserve the camera, selections, retained marker nodes, and keyboard focus. Mode/source changes through `configure`, `setMode(mode)`, and `setSource(source)` preserve the camera. Use `map.camera.reset()` or `map.camera.fit(geometry)` when movement is intended. The former `{ resetView }` setter argument is removed in v5. Switching neighborhood source clears the neighborhood selection. Changing district year retains a selected district when it exists in the new year. Unsupported datasets, invalid appearance values, and invalid district-style callback results fail during preparation before commit. A reentrant styling callback can supersede the pending patch.
|
|
457
561
|
|
|
458
|
-
|
|
562
|
+
Controller patches merge nested token and feature records. Omitted and `undefined` values retain current values at every depth; `false` disables boolean switches. Use `resetConfiguration(groups?)` to remove presentation overrides explicitly. The supported groups are `features`, `layers`, `controls`, and `appearance`; omitting the argument resets all four. Resets restore built-in defaults, preserve mode/source/year/labels and camera, and do not restore constructor overrides. Unknown groups fail before changing state. Static construction options and low-level element methods retain their separately documented contracts.
|
|
459
563
|
|
|
460
564
|
```ts
|
|
461
565
|
map.configure({
|
|
462
566
|
appearance: { colors: { water: '#142d45' }, labelStyle: { fontFamily: 'Georgia,serif' } },
|
|
463
567
|
layers: { bartStations: false },
|
|
464
568
|
});
|
|
465
|
-
map.
|
|
569
|
+
map.resetConfiguration(['appearance']); // restore built-in defaults
|
|
466
570
|
|
|
467
571
|
const overrides = map.getConfiguration();
|
|
468
572
|
const effective = map.getResolvedConfiguration();
|
|
@@ -494,11 +598,14 @@ map.selectFeature({ kind: 'neighborhood', source: 'realtor', id: 'inner-mission'
|
|
|
494
598
|
map.selectFeature({ kind: 'district', year: 2022, id: 3 });
|
|
495
599
|
```
|
|
496
600
|
|
|
497
|
-
Neighborhood IDs are exact source-scoped feature IDs, rather than display names or aliases. District identity includes the boundary year. This operation returns `false` for a missing feature or a source/year other than the map's current supplied geography; it never silently switches definitions or years. Choose geography through `configure` first. An `id` of `null` clears that kind in the referenced current geography. Malformed references throw.
|
|
601
|
+
Neighborhood IDs are exact source-scoped feature IDs, rather than display names or aliases. District identity includes the boundary year. This operation returns `false` for a missing feature or a source/year other than the map's current supplied geography; it never silently switches definitions or years. Choose geography through `configure` first. An `id` of `null` clears that kind in the referenced current geography. Malformed references throw. The `selectMarker`, `selectNeighborhood`, and `selectDistrict` shortcuts select exact IDs in the current geography. Controller `selectNeighborhood` no longer resolves display names or aliases; resolve those with a source-specific data lookup first.
|
|
498
602
|
|
|
499
603
|
## Coordinates and units
|
|
500
604
|
|
|
501
|
-
|
|
605
|
+
Version 5 uses tuple inputs and exports the named `ScreenPoint` /
|
|
606
|
+
`ProjectToScreen` types. The former two-number call is removed.
|
|
607
|
+
|
|
608
|
+
GeoJSON and static projection inputs use `[longitude, latitude]` in WGS84; markers use `{ position: [longitude, latitude] }`. Static `project` returns SVG units. `projectToScreen([longitude, latitude])` accepts the same readonly GeoJSON position as static `project` and returns `ScreenPoint` (`{ x, y, visible }`) in CSS pixels relative to the mounted canvas. Altitude is ignored. Mount the map before calling; invalid Mercator coordinates throw, and calls after controller destruction throw. `visible` indicates viewport inclusion, not whether another element covers the point. 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.
|
|
502
609
|
|
|
503
610
|
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.
|
|
504
611
|
|
|
@@ -512,7 +619,7 @@ Call `binding.setMarkers(next)` after `map.setMarkers(next)` when the catalog ch
|
|
|
512
619
|
|
|
513
620
|
---
|
|
514
621
|
|
|
515
|
-
<!-- Generated from docs/EXAMPLES.md; @kahwee/sf-map-svg
|
|
622
|
+
<!-- Generated from docs/EXAMPLES.md; @kahwee/sf-map-svg 5.1.0 · local working tree. -->
|
|
516
623
|
|
|
517
624
|
# Examples
|
|
518
625
|
|
|
@@ -622,9 +729,38 @@ See [consumer integration](https://github.com/kahwee/sf-map-svg/blob/main/docs/c
|
|
|
622
729
|
|
|
623
730
|
For the controller and explicit data imports, see [the v3 migration guide](https://github.com/kahwee/sf-map-svg/blob/main/docs/migration-v3.md).
|
|
624
731
|
|
|
732
|
+
## Find areas containing a location
|
|
733
|
+
|
|
734
|
+
New in v5. This example runs in
|
|
735
|
+
Node or a browser without mounting a map and imports only the SFAR definition:
|
|
736
|
+
|
|
737
|
+
```ts
|
|
738
|
+
import { geometryBounds, geometryContainsPoint } from '@kahwee/sf-map-svg/geometry';
|
|
739
|
+
import { neighborhoods } from '@kahwee/sf-map-svg/data/realtor';
|
|
740
|
+
|
|
741
|
+
const location = [-122.42, 37.76] as const;
|
|
742
|
+
const matches = neighborhoods.features.filter((feature) =>
|
|
743
|
+
geometryContainsPoint(feature.geometry, location),
|
|
744
|
+
);
|
|
745
|
+
for (const feature of matches) {
|
|
746
|
+
console.log(feature.id, feature.properties.canonicalName, geometryBounds(feature.geometry));
|
|
747
|
+
}
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
Keep all matches when the location lies on a shared boundary. An empty result
|
|
751
|
+
means no supplied polygon contains the location; it does not trigger a switch
|
|
752
|
+
to another neighborhood definition. For an existing map using the same realtor
|
|
753
|
+
source, pass a chosen match to
|
|
754
|
+
`map.selectFeature({ kind: 'neighborhood', source: 'realtor', id: feature.id })`.
|
|
755
|
+
To fit it, use `map.camera.fit(feature.geometry)`. Geographic bounds cannot be
|
|
756
|
+
passed directly to `camera.set`, which takes projected `[x, y, size]`.
|
|
757
|
+
|
|
758
|
+
See [query semantics and limitations](https://github.com/kahwee/sf-map-svg/blob/main/docs/API.md#geographic-queries) and the
|
|
759
|
+
[HTML overlay recipe](https://github.com/kahwee/sf-map-svg/blob/main/docs/consumer-integration.md#position-html-overlays).
|
|
760
|
+
|
|
625
761
|
---
|
|
626
762
|
|
|
627
|
-
<!-- Generated from docs/consumer-integration.md; @kahwee/sf-map-svg
|
|
763
|
+
<!-- Generated from docs/consumer-integration.md; @kahwee/sf-map-svg 5.1.0 · local working tree. -->
|
|
628
764
|
|
|
629
765
|
# Embedding and progressive enhancement
|
|
630
766
|
|
|
@@ -663,7 +799,7 @@ For a fixed-width embed, use shared inputs on the server and in the browser:
|
|
|
663
799
|
```ts
|
|
664
800
|
// Shared inputs: coordinates, IDs, radii, colors and layer choices must match.
|
|
665
801
|
const guideMarkers = [
|
|
666
|
-
{ id: 'ferry', label: 'Ferry Building',
|
|
802
|
+
{ id: 'ferry', label: 'Ferry Building', position: [-122.3937, 37.7955] as const, radius: 7 },
|
|
667
803
|
];
|
|
668
804
|
const guideLayers = { neighborhoodLabels: true, roadLabels: true };
|
|
669
805
|
const guideColors = { label: '#183e58', landmark: '#426641' };
|
|
@@ -754,9 +890,160 @@ Style `[data-map-selected]` with an outline or background change that remains le
|
|
|
754
890
|
|
|
755
891
|
Run `pnpm demo` and open `examples/generated/linked-cards.html` for the complete pattern. Storybook **Maps / Linked place cards** checks hover, keyboard selection, card reveal, both palettes, and disposal.
|
|
756
892
|
|
|
893
|
+
## Position HTML overlays
|
|
894
|
+
|
|
895
|
+
In v5, pass the same readonly `[longitude, latitude]` position used by static
|
|
896
|
+
projection. The former two-number call is removed; see the
|
|
897
|
+
[migration guide](https://github.com/kahwee/sf-map-svg/blob/main/docs/migration-v5.md).
|
|
898
|
+
|
|
899
|
+
Call this helper after mounting the map in a container with nonzero width:
|
|
900
|
+
|
|
901
|
+
```ts
|
|
902
|
+
import type { MapController } from '@kahwee/sf-map-svg';
|
|
903
|
+
import type { Position } from '@kahwee/sf-map-svg/geometry';
|
|
904
|
+
|
|
905
|
+
function attachCallout(map: MapController, location: Position, label: string) {
|
|
906
|
+
const callout = document.createElement('span');
|
|
907
|
+
callout.textContent = label;
|
|
908
|
+
callout.style.cssText = 'position:absolute;pointer-events:none;transform:translate(-50%,-100%)';
|
|
909
|
+
const update = () => {
|
|
910
|
+
const point = map.projectToScreen(location);
|
|
911
|
+
callout.hidden = !point.visible;
|
|
912
|
+
callout.style.left = `${point.x}px`;
|
|
913
|
+
callout.style.top = `${point.y}px`;
|
|
914
|
+
};
|
|
915
|
+
update();
|
|
916
|
+
map.overlayElement.append(callout);
|
|
917
|
+
const stopViewport = map.on('viewportchange', update);
|
|
918
|
+
const stopResize = map.on('mapresize', update);
|
|
919
|
+
return () => {
|
|
920
|
+
stopViewport();
|
|
921
|
+
stopResize();
|
|
922
|
+
callout.remove();
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
```
|
|
926
|
+
|
|
927
|
+
Use `const detach = attachCallout(map, [-122.3937, 37.7955], 'Ferry Building')`
|
|
928
|
+
with your mounted controller. Call `detach()` before `map.destroy()` on unmount.
|
|
929
|
+
Keep the container visible while projecting; an unmounted or zero-width canvas
|
|
930
|
+
throws. Controller calls after destruction also throw. Camera moves and resize
|
|
931
|
+
both require repositioning; a one-time projection becomes stale.
|
|
932
|
+
|
|
933
|
+
`x` and `y` are CSS pixels relative to `overlayElement`, not page coordinates or
|
|
934
|
+
SVG units. `visible` tests inclusion in the map viewport; it does not account
|
|
935
|
+
for occlusion by other content. This noninteractive label uses `textContent`
|
|
936
|
+
and leaves pointer input to the map. Interactive callouts need their own
|
|
937
|
+
keyboard, focus, and dismissal behavior. See [coordinates and units](https://github.com/kahwee/sf-map-svg/blob/main/docs/API.md#coordinates-and-units).
|
|
938
|
+
|
|
757
939
|
---
|
|
758
940
|
|
|
759
|
-
<!-- Generated from docs/migration-
|
|
941
|
+
<!-- Generated from docs/migration-v5.md; @kahwee/sf-map-svg 5.1.0 · local working tree. -->
|
|
942
|
+
|
|
943
|
+
# Migrating to v5
|
|
944
|
+
|
|
945
|
+
Version 5.0.0 makes coordinate inputs, controller updates, and reset behavior
|
|
946
|
+
consistent. It requires Node 24+ and retains offline rendering, explicit data
|
|
947
|
+
imports, zero runtime dependencies, and the existing default map appearance.
|
|
948
|
+
The version bump prepares the next release; it does not publish the package.
|
|
949
|
+
|
|
950
|
+
## Coordinates use GeoJSON positions
|
|
951
|
+
|
|
952
|
+
Markers now require `position: [longitude, latitude]`. The old `lng` and `lat`
|
|
953
|
+
properties are rejected. `projectToScreen` takes one readonly position; its
|
|
954
|
+
old two-number form is removed. Altitude is accepted and ignored by projection.
|
|
955
|
+
|
|
956
|
+
```ts
|
|
957
|
+
// v4
|
|
958
|
+
const oldMarker = { id: 'ferry', lng: -122.3937, lat: 37.7955 };
|
|
959
|
+
// v5
|
|
960
|
+
const marker = { id: 'ferry', position: [-122.3937, 37.7955] as const };
|
|
961
|
+
map.setMarkers([marker]);
|
|
962
|
+
const point = map.projectToScreen(marker.position);
|
|
963
|
+
```
|
|
964
|
+
|
|
965
|
+
Use `Position` from `/geometry` for reusable positions and `ScreenPoint` or
|
|
966
|
+
`ProjectToScreen` from the root or `/map` for screen-projection contracts.
|
|
967
|
+
`x` and `y` remain canvas-relative CSS pixels; `visible` means inside the
|
|
968
|
+
viewport, not unobscured. Mount the map in a nonzero-width container first.
|
|
969
|
+
Camera viewports still use projected `[x, y, size]`; camera pan deltas are CSS
|
|
970
|
+
pixels. Geographic bounds are not camera viewports.
|
|
971
|
+
|
|
972
|
+
## Controller updates preserve the camera
|
|
973
|
+
|
|
974
|
+
`configure`, `setMode`, and `setSource` preserve the camera. The second
|
|
975
|
+
`{ resetView }` argument to the controller convenience setters is removed.
|
|
976
|
+
To reproduce an old automatic reset, request it explicitly:
|
|
977
|
+
|
|
978
|
+
```ts
|
|
979
|
+
map.setMode('neighborhoods');
|
|
980
|
+
map.camera.reset({ animate: false });
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
Selection still fits by default. Use `{ fit: false }` to change selection
|
|
984
|
+
without moving the camera, or `camera.fit(geometry)` for an explicit fit.
|
|
985
|
+
|
|
986
|
+
## Reset presentation explicitly
|
|
987
|
+
|
|
988
|
+
In controller `configure` patches, omitted and `undefined` values are no-ops at
|
|
989
|
+
every record depth. Nested token and feature records merge; an explicit boolean
|
|
990
|
+
replaces a feature record and `false` disables a boolean switch.
|
|
991
|
+
|
|
992
|
+
```ts
|
|
993
|
+
map.configure({ appearance: { colors: { water: '#142d45' } } });
|
|
994
|
+
map.configure({ appearance: undefined }); // keeps the current appearance
|
|
995
|
+
map.resetConfiguration(['appearance']); // restores built-in appearance defaults
|
|
996
|
+
map.resetConfiguration(['layers', 'controls']); // removes explicit overrides
|
|
997
|
+
map.resetConfiguration(); // resets all four presentation groups
|
|
998
|
+
```
|
|
999
|
+
|
|
1000
|
+
The reset groups are `features`, `layers`, `controls`, and `appearance`.
|
|
1001
|
+
Resetting a group does not restore initial constructor overrides, change the
|
|
1002
|
+
camera, or change mode/source/year/labels. It restores built-in defaults and
|
|
1003
|
+
mode-derived layer choices. Supply geographic choices explicitly through
|
|
1004
|
+
`configure`. To remove a single token while retaining other overrides, build
|
|
1005
|
+
the desired group, reset that group, then configure the retained values.
|
|
1006
|
+
Each call commits atomically; the two calls are separate updates.
|
|
1007
|
+
|
|
1008
|
+
These rules describe `MapController` from `createMap`, `createGuideController`,
|
|
1009
|
+
and `mountGuideController`. Low-level element factories retain their separate
|
|
1010
|
+
`setFeatures` / `setLayers` APIs; use controllers for the grouped update contract.
|
|
1011
|
+
|
|
1012
|
+
## Select neighborhoods by exact identity
|
|
1013
|
+
|
|
1014
|
+
Controller `selectNeighborhood(id)` accepts an exact ID in the current source,
|
|
1015
|
+
rather than a display name or alias. Unknown IDs return `false` without changing
|
|
1016
|
+
selection. Resolve a human-entered name using the source-specific lookup first.
|
|
1017
|
+
For integrations, prefer the source-qualified identity:
|
|
1018
|
+
|
|
1019
|
+
```ts
|
|
1020
|
+
import { getRealtorNeighborhood } from '@kahwee/sf-map-svg/data/realtor';
|
|
1021
|
+
|
|
1022
|
+
const feature = getRealtorNeighborhood('Inner Mission');
|
|
1023
|
+
if (feature) {
|
|
1024
|
+
map.selectFeature({ kind: 'neighborhood', source: 'realtor', id: feature.id }, { fit: false });
|
|
1025
|
+
}
|
|
1026
|
+
```
|
|
1027
|
+
|
|
1028
|
+
`selectFeature` requires the current supplied source/year and never switches
|
|
1029
|
+
geography implicitly. Marker IDs and numeric district IDs remain unchanged.
|
|
1030
|
+
The construction-time `neighborhood` option and low-level element lookup APIs
|
|
1031
|
+
continue to resolve names; the stricter ID contract applies to controller
|
|
1032
|
+
selection methods. Existing selection event names remain available alongside
|
|
1033
|
+
`selectionchange`.
|
|
1034
|
+
|
|
1035
|
+
## Offline geometry queries
|
|
1036
|
+
|
|
1037
|
+
`geometryBounds(geometry)` and `geometryContainsPoint(geometry, position)` are
|
|
1038
|
+
new in v5. They accept readonly GeoJSON without a DOM or geographic imports.
|
|
1039
|
+
Bounds are `[west, south, east, north]` or `null` for empty geometry. Containment
|
|
1040
|
+
respects holes, includes polygon boundaries, and supports islands and nested
|
|
1041
|
+
collections. Shared edges can match more than one feature. See the
|
|
1042
|
+
[query contract](https://github.com/kahwee/sf-map-svg/blob/main/docs/API.md#geographic-queries) for validation and planar limitations.
|
|
1043
|
+
|
|
1044
|
+
---
|
|
1045
|
+
|
|
1046
|
+
<!-- Generated from docs/migration-v3.md; @kahwee/sf-map-svg 5.1.0 · local working tree. -->
|
|
760
1047
|
|
|
761
1048
|
# Migrate to version 3
|
|
762
1049
|
|
|
@@ -797,7 +1084,7 @@ District styles are now prepared once per district on construction and style/yea
|
|
|
797
1084
|
|
|
798
1085
|
---
|
|
799
1086
|
|
|
800
|
-
<!-- Generated from SOURCES.md; @kahwee/sf-map-svg
|
|
1087
|
+
<!-- Generated from SOURCES.md; @kahwee/sf-map-svg 5.1.0 · local working tree. -->
|
|
801
1088
|
|
|
802
1089
|
# Geometry and provenance
|
|
803
1090
|
|
|
@@ -979,9 +1266,62 @@ reconciles all 2002, 2012, and 2022 district totals. The election JSON files
|
|
|
979
1266
|
are Pages datasets; the historical district maps retain their existing
|
|
980
1267
|
display geometry, and no precinct polygons or raw workbook data are shipped.
|
|
981
1268
|
|
|
1269
|
+
## November 2026 ballot and pending results
|
|
1270
|
+
|
|
1271
|
+
Downloaded November 4, 2026 UTC (November 3 Pacific):
|
|
1272
|
+
https://media.api.sf.gov/documents/N26_EN_VIP.pdf .
|
|
1273
|
+
`elections/2026-11-03.json` records its SHA-256 and verification time. Printed
|
|
1274
|
+
page 248 lists all 14 state, one regional and ten local measures. Pages 24–51
|
|
1275
|
+
supply candidate statement names. The official roster at
|
|
1276
|
+
https://www.sf.gov/candidates-november-3-2026-general-election was verified
|
|
1277
|
+
on November 4 UTC to complete the 15 displayed candidate contests, adding
|
|
1278
|
+
Charles Hoelter, Manuel Noris-Barrera, Philip Louis Wing, Nicholas Berg and
|
|
1279
|
+
Jamo Muhammad. The California Secretary of State certified list, downloaded
|
|
1280
|
+
November 4 UTC from
|
|
1281
|
+
https://elections.cdn.sos.ca.gov/statewide-elections/2026-general/cert-list-candidates.pdf
|
|
1282
|
+
(certified August 27, 2026; SHA-256 in the feed), supplies nine additional
|
|
1283
|
+
state/Board of Equalization contests on printed pages 1–2 and 13 Supreme Court
|
|
1284
|
+
and First Appellate District retention contests on pages 33–34. The latter
|
|
1285
|
+
explicitly includes San Francisco County and matches SF pamphlet pp. 246–247.
|
|
1286
|
+
Other appellate districts are excluded. Printed candidate coverage does not
|
|
1287
|
+
assert independently verified late statewide write-in qualifications.
|
|
1288
|
+
The November results endpoint
|
|
1289
|
+
https://www.sfelections.org/results/20261103w/index.html returned HTTP 403;
|
|
1290
|
+
no November vote totals, report time, winners or geographic results are asserted.
|
|
1291
|
+
The existing 2022 supervisorial boundaries are reused only for ballot district
|
|
1292
|
+
selection, with neutral fills. See `elections/README.md` for the feed contract
|
|
1293
|
+
and the ingestion blocker. Historical snapshots remain separate elections.
|
|
1294
|
+
|
|
1295
|
+
|
|
1296
|
+
The independent California Secretary of State public media index at
|
|
1297
|
+
https://media.sos.ca.gov/media/json-endpoints.csv identifies the November 3,
|
|
1298
|
+
2026 election and links JSON reports. Downloaded November 4 UTC: the reports
|
|
1299
|
+
still carry the November 2, 6:28 p.m. zero-report time. The adapter extracts
|
|
1300
|
+
only exact San Francisco County rows for 40 supported state/federal candidate, proposition and judicial
|
|
1301
|
+
contests. It does not use statewide totals, infer precinct boundaries, or map
|
|
1302
|
+
county totals onto districts. Actual extracted fixtures and their URLs are
|
|
1303
|
+
recorded in `test/fixtures/sos/README.md`. Zero reports stay pending, and report
|
|
1304
|
+
time is preserved verbatim separately from fetch time.
|
|
1305
|
+
|
|
1306
|
+
### November 2026 plain-language ballot descriptions (verified November 3, 2026 Pacific)
|
|
1307
|
+
|
|
1308
|
+
- SF official voter pamphlet: https://media.api.sf.gov/documents/N26_EN_VIP.pdf — printed pp. 52–53 (regional transit), 64 (A), 70–71 (B), 78 (C), 84 (D), 94–95 (E), 100–101 (F), 108 (G), 124–125 (H), 132–133 (I), 144 (J). Summaries use impartial digests, not campaign arguments. PDF viewer page numbers are two higher than printed pages.
|
|
1309
|
+
- California Secretary of State official November 3, 2026 voter guide: https://vig.cdn.sos.ca.gov/2026/general/pdf/complete-vig.pdf, downloaded November 4 UTC; SHA-256 `9509a4e3ac10b09e3c36d8b9b6c5d2f3935af3c7e2473c392b64352811cac1d8`. Official titles/summaries and Legislative Analyst analysis: printed pp. 14 (1), 20–23 (2), 26–27 (3), 30 (4), 34–37 (5), 40–41 (37), 44 (38), 48–51 (39), 54–55 (40), 58–59 (41), 62–63 (42), 66–67 (43), 70–71 (44), 74–77 (45).
|
|
1310
|
+
- `shortTitle`, `summary`, and `summarySource` in the shared election feed preserve official full titles and source page provenance. Descriptions are neutral paraphrases; no endorsements or outcome predictions.
|
|
1311
|
+
|
|
1312
|
+
The November 3 impartiality review checked all 25 descriptions against the official digests and legislative analyses. Clarifications include SF C printed pp. 78–79 (freeze annual increases, not all contributions), F p. 100 (Board objection), E p. 94 (60-day default approval); California 2 pp. 22–23, 38 pp. 44–45, 39 p. 50, 40 pp. 54–55, 41 pp. 58–59, 43 pp. 66–67, 44 pp. 70–71 (operative scope/waivers pp. 153–156), and 45 pp. 75–76. Each feed record carries its revised printed-page reference.
|
|
1313
|
+
|
|
1314
|
+
Report-history points preserve only actual validated source counts and their distinct first observation times. Tests use explicitly synthetic mutations of the official zero-report fixtures; no synthetic counts or history points are in the checked-in ballot seed or deployed as results.
|
|
1315
|
+
|
|
1316
|
+
## November 2026 precinct results and boundaries
|
|
1317
|
+
|
|
1318
|
+
- `data/elections/precincts-<report time>.json`, such as `precincts-2026-11-03-230421.json` (local department clock, `YYYY-MM-DD-HHmmss`), aggregates the San Francisco Department of Elections cast vote record export `CVR_Export_20261103231904.zip` (downloaded November 5, 2026; the department's report time was supplied as 11/3/2026 11:04:21 PM, kept verbatim with no timezone conversion) to per-precinct, per-contest counts in a compact format (`precincts[id].c[contestIndex] = [undervotes, overvotes, ...votes in option order]`, with `contests[]` giving each index's options; schema version 2, about 0.6 MB). Generate it with `scripts/import-cvr-precincts.mjs --zip <export> --report-time "<M/D/YYYY h:mm:ss AM|PM>" --out-dir data/elections`. Each report time keeps its own file, and `data/elections/precincts-index.json` lists them in time order; consumers read `latest`, the newest report time. The export, which is ballot-level, is an ingestion input and is never committed; its SHA-256 is recorded in the file. Counts use each ballot card's current record (`Modified` when adjudicated). Ranked-choice contests count a single rank-1 mark only, matching the department's first-round figures. These are not winners.
|
|
1319
|
+
- Reconciliation: `test/election-precincts.test.mjs` sums the precincts and compares every candidate and option of all 65 contests with the official Election Summary Report 4 (`test/fixtures/sfelections/summary-report-4.json`, a 5 KB extract of the report's option totals made with `scripts/extract-summary-totals.mjs`, with the XML's SHA-256 recorded; 514 of 514 precincts reported). Every total matches exactly. Five precincts have no ballots in the export and are absent from the file.
|
|
1320
|
+
- `data/precincts-2022.json` holds the 514 polygons of DataSF "Election Precincts - Current, Defined 2022" (`d6x4-hefw`, https://data.sfgov.org/resource/d6x4-hefw.geojson, downloaded November 5, 2026), reduced to the precinct ID with coordinates rounded to six decimals. All 514 IDs match the export's precinct manifest exactly.
|
|
1321
|
+
|
|
982
1322
|
---
|
|
983
1323
|
|
|
984
|
-
<!-- Generated from CHANGELOG.md; @kahwee/sf-map-svg
|
|
1324
|
+
<!-- Generated from CHANGELOG.md; @kahwee/sf-map-svg 5.1.0 · local working tree. -->
|
|
985
1325
|
|
|
986
1326
|
# Changelog
|
|
987
1327
|
|
|
@@ -989,6 +1329,30 @@ User-visible changes are recorded here. Unreleased entries describe changes on `
|
|
|
989
1329
|
|
|
990
1330
|
## Unreleased
|
|
991
1331
|
|
|
1332
|
+
## 5.1.0 — 2026-11-05
|
|
1333
|
+
|
|
1334
|
+
- Add official November 2026 precinct aggregates (`data/elections/precincts-<report time>.json` with a time-ordered `precincts-index.json`) and the 2022 precinct boundaries (`data/precincts-2022.json`), reconciled with the department's summary report. Only aggregates are committed, never the ballot-level export. Export the boundaries as `precinctMaps` from `@kahwee/sf-map-svg/data` and `@kahwee/sf-map-svg/data/precincts`; results snapshots stay Pages data.
|
|
1335
|
+
- Update Biome to 2.6.3 and the aligned Storybook packages to 10.6.4.
|
|
1336
|
+
- Gate library output replacement on Bun source checking, then emit JavaScript, declarations, and JSON with TypeScript `--noCheck`; install pinned Bun in every build job.
|
|
1337
|
+
- Align Storybook on 10.6.3 and Vitest/browser/coverage tooling on 5.0.5, with compatible dependency updates for current security fixes.
|
|
1338
|
+
- Record identical-output library build benchmarks alongside the earlier build-reuse measurements.
|
|
1339
|
+
- Reuse completed library builds in CI demo, Storybook, coverage, and Pages steps; retain fresh builds for standalone commands and archive packing.
|
|
1340
|
+
- Add a reproducible compilation-phase benchmark that checks emitted-output equivalence.
|
|
1341
|
+
- Use Bun for consumer, Storybook, and website project checks; run them once in the Bun CI job instead of repeating them in the Node matrix.
|
|
1342
|
+
- Add a Pages precinct results page (`precincts.html`) that shades the 514 precincts by any contest in the latest official snapshot, and compact the committed precinct snapshot to about 0.6 MB with a 5 KB reconciliation fixture.
|
|
1343
|
+
- Fix election page checks on 390 px phones and in macOS Chromium, and wait for the scrollbar gutter in popover checks.
|
|
1344
|
+
|
|
1345
|
+
## 5.0.0 — 2026-10-19
|
|
1346
|
+
|
|
1347
|
+
- Breaking: controller `setMode` and `setSource` preserve the camera; remove their `{ resetView }` argument. Call `camera.reset()` or `camera.fit()` explicitly.
|
|
1348
|
+
- Breaking: controller `configure` treats all omitted and undefined values as no-ops and merges nested feature records. Add `resetConfiguration(groups?)` to clear presentation overrides explicitly.
|
|
1349
|
+
- Breaking: controller `selectNeighborhood` selects exact IDs in the current source; resolve names through source-specific data lookup or use source/year-qualified `selectFeature`.
|
|
1350
|
+
- Include a [v5 migration guide](https://github.com/kahwee/sf-map-svg/blob/main/docs/migration-v5.md), migrated consumer examples, and regression checks. Retain Node 24+, offline rendering, zero runtime dependencies, geographic data, and default appearance.
|
|
1351
|
+
|
|
1352
|
+
- Add offline `geometryBounds()` and `geometryContainsPoint()` helpers to `/geometry`, with readonly inputs, polygon holes and islands, nested collections, explicit boundary semantics, and coordinate validation. Export `Bounds`, `Geometry`, and `Position` from the same entry.
|
|
1353
|
+
- Breaking: markers now require `position: [longitude, latitude]`; remove the numeric `projectToScreen(lng, lat)` form in favor of one readonly GeoJSON position. Export shared `ScreenPoint` and `ProjectToScreen` types and cover camera changes, resizing, invalid coordinates, and controller disposal in browser regressions.
|
|
1354
|
+
- Document geographic queries, screen-coordinate integration, and API consistency follow-ups. Correct the developer guide: camera pan deltas are CSS pixels, not projected map units.
|
|
1355
|
+
|
|
992
1356
|
## 4.5.0 — 2026-10-14
|
|
993
1357
|
|
|
994
1358
|
- Add the optional `/cards` entry with `bindMapCards()`: hover/focus previews a pin without changing selection or camera, native buttons select places, map selection reveals matching cards without stealing focus, and explicit cleanup restores owned DOM attributes.
|
|
@@ -1347,7 +1711,7 @@ consistent home, and makes animation and subscription ownership predictable.
|
|
|
1347
1711
|
|
|
1348
1712
|
# Published type contracts
|
|
1349
1713
|
|
|
1350
|
-
@kahwee/sf-map-svg
|
|
1714
|
+
@kahwee/sf-map-svg 5.1.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.
|
|
1351
1715
|
|
|
1352
1716
|
| Public import | Declaration |
|
|
1353
1717
|
| --- | --- |
|
|
@@ -1362,6 +1726,7 @@ consistent home, and makes animation and subscription ownership predictable.
|
|
|
1362
1726
|
| @kahwee/sf-map-svg/data/highways | ./dist/data/highways.d.ts |
|
|
1363
1727
|
| @kahwee/sf-map-svg/data/landmarks | ./dist/data/landmarks.d.ts |
|
|
1364
1728
|
| @kahwee/sf-map-svg/data/lookup | ./dist/data/lookup.d.ts |
|
|
1729
|
+
| @kahwee/sf-map-svg/data/precincts | ./dist/data/precincts.d.ts |
|
|
1365
1730
|
| @kahwee/sf-map-svg/data/realtor | ./dist/data/realtor.d.ts |
|
|
1366
1731
|
| @kahwee/sf-map-svg/data/roads | ./dist/data/roads.d.ts |
|
|
1367
1732
|
| @kahwee/sf-map-svg/data/sf-find | ./dist/data/sf-find.d.ts |
|
|
@@ -1393,7 +1758,7 @@ import type { MapController, MapOptions } from './controller-types.js';
|
|
|
1393
1758
|
import type { InteractiveSFMapData } from './explorer-data.js';
|
|
1394
1759
|
export type * from './controller-types.js';
|
|
1395
1760
|
export type { InteractiveSFMapData as MapData } from './explorer-data.js';
|
|
1396
|
-
export type { CameraOptions, DistrictSelection, DistrictStyle, DistrictYear, InteractiveLayers, MapFeatures, MapMarker, MapOverlay, MapPadding, MapViewport, NeighborhoodSelection, } from './types.js';
|
|
1761
|
+
export type { CameraOptions, DistrictSelection, DistrictStyle, DistrictYear, InteractiveLayers, MapFeatures, MapMarker, MapOverlay, MapPadding, MapViewport, NeighborhoodSelection, ProjectToScreen, ScreenPoint, } from './types.js';
|
|
1397
1762
|
/** No geography is imported. Supply a preset or your own immutable data. */
|
|
1398
1763
|
export declare function createMap(data: InteractiveSFMapData, options?: MapOptions): MapController;
|
|
1399
1764
|
```
|
|
@@ -1402,8 +1767,8 @@ export declare function createMap(data: InteractiveSFMapData, options?: MapOptio
|
|
|
1402
1767
|
|
|
1403
1768
|
```ts
|
|
1404
1769
|
import type { Geometry, NeighborhoodSource } from '../data/types.js';
|
|
1405
|
-
import type { CameraOptions, DistrictSelection, DistrictStyle, DistrictYear, InteractiveLayers, MapFeatures, MapMarker, MapOverlay, MapPadding, MapViewport, NeighborhoodExplorerOptions, NeighborhoodSelection } from './types.js';
|
|
1406
|
-
/**
|
|
1770
|
+
import type { CameraOptions, DistrictSelection, DistrictStyle, DistrictYear, InteractiveLayers, MapFeatures, MapMarker, MapOverlay, MapPadding, MapViewport, NeighborhoodExplorerOptions, NeighborhoodSelection, ProjectToScreen } from './types.js';
|
|
1771
|
+
/** Explicit undefined is accepted as a no-op, including with exactOptionalPropertyTypes. */
|
|
1407
1772
|
type Resettable<T> = {
|
|
1408
1773
|
[K in keyof T]?: T[K] | undefined;
|
|
1409
1774
|
};
|
|
@@ -1412,7 +1777,7 @@ export type MapAppearance = {
|
|
|
1412
1777
|
[K in keyof AppearanceOptions]?: K extends 'colors' | 'style' | 'labelStyle' | 'labelSize' | 'areaStyle' ? Resettable<NonNullable<AppearanceOptions[K]>> | undefined : AppearanceOptions[K] | undefined;
|
|
1413
1778
|
};
|
|
1414
1779
|
export type MapControls = NonNullable<NeighborhoodExplorerOptions['controls']>;
|
|
1415
|
-
/** Omitted
|
|
1780
|
+
/** Omitted and undefined values retain current values. Use resetConfiguration to clear overrides. */
|
|
1416
1781
|
export interface MapConfiguration {
|
|
1417
1782
|
features?: Resettable<MapFeatures> | undefined;
|
|
1418
1783
|
layers?: Resettable<InteractiveLayers> | undefined;
|
|
@@ -1518,10 +1883,7 @@ export interface MapEvents {
|
|
|
1518
1883
|
};
|
|
1519
1884
|
mapresize: undefined;
|
|
1520
1885
|
}
|
|
1521
|
-
|
|
1522
|
-
export interface MapViewUpdateOptions {
|
|
1523
|
-
resetView?: boolean;
|
|
1524
|
-
}
|
|
1886
|
+
export type MapConfigurationGroup = 'features' | 'layers' | 'controls' | 'appearance';
|
|
1525
1887
|
export interface MapCamera {
|
|
1526
1888
|
/** Fixed 800 × 800 projected space, not longitude/latitude. */
|
|
1527
1889
|
get(): MapViewport;
|
|
@@ -1542,6 +1904,8 @@ export interface MapController {
|
|
|
1542
1904
|
readonly camera: MapCamera;
|
|
1543
1905
|
readonly destroyed: boolean;
|
|
1544
1906
|
configure(patch: MapConfiguration): void;
|
|
1907
|
+
/** Clear presentation overrides to built-in defaults, preserving camera and geographic choices. */
|
|
1908
|
+
resetConfiguration(groups?: readonly MapConfigurationGroup[]): void;
|
|
1545
1909
|
getConfiguration(): MapConfigurationSnapshot;
|
|
1546
1910
|
getResolvedConfiguration(): ResolvedMapConfiguration;
|
|
1547
1911
|
getCapabilities(): MapCapabilities;
|
|
@@ -1557,7 +1921,7 @@ export interface MapController {
|
|
|
1557
1921
|
fit?: boolean;
|
|
1558
1922
|
}): boolean;
|
|
1559
1923
|
getSelectedMarker(): MapMarker | null;
|
|
1560
|
-
selectNeighborhood(
|
|
1924
|
+
selectNeighborhood(id: string | null, options?: CameraOptions & {
|
|
1561
1925
|
fit?: boolean;
|
|
1562
1926
|
}): boolean;
|
|
1563
1927
|
getSelectedNeighborhood(): NeighborhoodSelection | null;
|
|
@@ -1567,15 +1931,12 @@ export interface MapController {
|
|
|
1567
1931
|
getSelectedDistrict(): DistrictSelection | null;
|
|
1568
1932
|
setDistrictYear(year: DistrictYear, options?: CameraOptions): void;
|
|
1569
1933
|
setDistrictStyle(style: ((district: DistrictSelection['district']) => DistrictStyle) | undefined): void;
|
|
1570
|
-
setSource(source: NeighborhoodSource
|
|
1571
|
-
setMode(mode: NonNullable<MapOptions['mode']
|
|
1934
|
+
setSource(source: NeighborhoodSource): void;
|
|
1935
|
+
setMode(mode: NonNullable<MapOptions['mode']>): void;
|
|
1572
1936
|
setLabels(visible: boolean): void;
|
|
1573
1937
|
setTouchNavigation(enabled: boolean): void;
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
y: number;
|
|
1577
|
-
visible: boolean;
|
|
1578
|
-
};
|
|
1938
|
+
/** Accepts a GeoJSON position; returns canvas-relative CSS pixels. */
|
|
1939
|
+
projectToScreen: ProjectToScreen;
|
|
1579
1940
|
/** Idempotent; stops work and managed subscriptions. Does not remove host-owned DOM. */
|
|
1580
1941
|
destroy(): void;
|
|
1581
1942
|
}
|
|
@@ -1680,6 +2041,11 @@ export interface DistrictProperties {
|
|
|
1680
2041
|
readonly labelPoints: readonly Position[];
|
|
1681
2042
|
readonly displayExtras: Geometry | null;
|
|
1682
2043
|
}
|
|
2044
|
+
export interface PrecinctProperties {
|
|
2045
|
+
/** Four-digit Department of Elections precinct number, such as "1101". */
|
|
2046
|
+
readonly precinct: string;
|
|
2047
|
+
readonly year: 2022;
|
|
2048
|
+
}
|
|
1683
2049
|
export interface Catalog {
|
|
1684
2050
|
readonly schemaVersion: 1;
|
|
1685
2051
|
readonly description: string;
|
|
@@ -1718,7 +2084,15 @@ export interface KeyRoadProperties {
|
|
|
1718
2084
|
### dist/src/types.d.ts
|
|
1719
2085
|
|
|
1720
2086
|
```ts
|
|
1721
|
-
import type { Geometry, NeighborhoodFeature, NeighborhoodSource } from '../data/types.js';
|
|
2087
|
+
import type { Geometry, NeighborhoodFeature, NeighborhoodSource, Position } from '../data/types.js';
|
|
2088
|
+
/** Canvas-relative CSS pixels; visibility means inside the current square map viewport. */
|
|
2089
|
+
export interface ScreenPoint {
|
|
2090
|
+
x: number;
|
|
2091
|
+
y: number;
|
|
2092
|
+
visible: boolean;
|
|
2093
|
+
}
|
|
2094
|
+
/** WGS84 longitude/latitude; altitude, if supplied in a position, is ignored. */
|
|
2095
|
+
export type ProjectToScreen = (position: Position) => ScreenPoint;
|
|
1722
2096
|
export type DistrictYear = 2002 | 2012 | 2022;
|
|
1723
2097
|
export interface DistrictSelection {
|
|
1724
2098
|
id: number;
|
|
@@ -1732,8 +2106,7 @@ export interface DistrictStyle {
|
|
|
1732
2106
|
}
|
|
1733
2107
|
export interface MapMarker {
|
|
1734
2108
|
id: string;
|
|
1735
|
-
|
|
1736
|
-
lat: number;
|
|
2109
|
+
position: Position;
|
|
1737
2110
|
label?: string;
|
|
1738
2111
|
selected?: boolean;
|
|
1739
2112
|
color?: string;
|
|
@@ -1940,11 +2313,7 @@ export interface CameraOptions {
|
|
|
1940
2313
|
export interface NeighborhoodExplorerElement extends HTMLElement {
|
|
1941
2314
|
/** Append positioned HTML children here; coordinates are relative to this layer. */
|
|
1942
2315
|
readonly overlayElement: HTMLDivElement;
|
|
1943
|
-
projectToScreen
|
|
1944
|
-
x: number;
|
|
1945
|
-
y: number;
|
|
1946
|
-
visible: boolean;
|
|
1947
|
-
};
|
|
2316
|
+
projectToScreen: ProjectToScreen;
|
|
1948
2317
|
stopAnimation(): void;
|
|
1949
2318
|
/** Atomic patch; false disables, undefined resets a feature to its default. */
|
|
1950
2319
|
setFeatures(patch: MapFeatures): void;
|
|
@@ -1988,7 +2357,7 @@ export interface NeighborhoodExplorerElement extends HTMLElement {
|
|
|
1988
2357
|
export interface TransitAnimationElement extends HTMLElement {
|
|
1989
2358
|
destroy(): void;
|
|
1990
2359
|
}
|
|
1991
|
-
export type { MapAppearance, MapCamera, MapCapabilities, MapConfiguration, MapConfigurationSnapshot, MapController, MapControls, MapEvents, MapFeatureReference, MapOptions, MapSelectionChange,
|
|
2360
|
+
export type { MapAppearance, MapCamera, MapCapabilities, MapConfiguration, MapConfigurationGroup, MapConfigurationSnapshot, MapController, MapControls, MapEvents, MapFeatureReference, MapOptions, MapSelectionChange, ResolvedMapConfiguration, } from './controller-types.js';
|
|
1992
2361
|
```
|
|
1993
2362
|
|
|
1994
2363
|
### dist/src/explorer-data.d.ts
|
|
@@ -2117,11 +2486,16 @@ export declare function createSFMapWithData(options: SFMapOptions, data: SFMapDa
|
|
|
2117
2486
|
### dist/src/geometry.d.ts
|
|
2118
2487
|
|
|
2119
2488
|
```ts
|
|
2120
|
-
import type { Geometry, Position } from '../data/types.js';
|
|
2489
|
+
import type { Bounds, Geometry, Position } from '../data/types.js';
|
|
2490
|
+
export type { Bounds, Geometry, Position } from '../data/types.js';
|
|
2121
2491
|
export type Project = (position: Position) => [number, number];
|
|
2122
2492
|
export declare function rawProject([longitude, latitude]: Position): [number, number];
|
|
2123
2493
|
export declare function positions(geometry: Geometry | null | undefined): Position[];
|
|
2124
2494
|
export declare function geometryPath(geometry: Geometry | null | undefined, project: Project): string;
|
|
2495
|
+
/** Geographic [west, south, east, north], or null for empty geometry. */
|
|
2496
|
+
export declare function geometryBounds(geometry: Geometry | null | undefined): Bounds | null;
|
|
2497
|
+
/** Polygon hit testing in longitude/latitude; boundaries (including hole edges) count as hits. */
|
|
2498
|
+
export declare function geometryContainsPoint(geometry: Geometry | null | undefined, point: Position): boolean;
|
|
2125
2499
|
```
|
|
2126
2500
|
|
|
2127
2501
|
### dist/src/static.d.ts
|
|
@@ -2137,14 +2511,14 @@ export type { DistrictStyle, DistrictYear, SFMapOptions as StaticMapOptions, Sta
|
|
|
2137
2511
|
/** Server-safe SVG rendering with explicit data; returns SVG plus projection helpers. */
|
|
2138
2512
|
export declare function renderMap(data: StaticMapInput, options?: SFMapOptions): {
|
|
2139
2513
|
svg: string;
|
|
2140
|
-
project: (coordinates: import("
|
|
2514
|
+
project: (coordinates: import("./geometry.js").Position) => [number, number];
|
|
2141
2515
|
viewBox: [number, number, number, number];
|
|
2142
2516
|
};
|
|
2143
2517
|
/** Project canonical layer geometry without constructing an SVG string. */
|
|
2144
2518
|
export declare function getLayerPaths(data: StaticMapInput, options?: SFMapOptions): {
|
|
2145
2519
|
year: import("./types.js").DistrictYear;
|
|
2146
2520
|
viewBox: [number, number, number, number];
|
|
2147
|
-
project: (coordinates: import("
|
|
2521
|
+
project: (coordinates: import("./geometry.js").Position) => [number, number];
|
|
2148
2522
|
coast: string;
|
|
2149
2523
|
districts: {
|
|
2150
2524
|
id: number;
|
|
@@ -2184,6 +2558,7 @@ export { districtMaps } from './districts.js';
|
|
|
2184
2558
|
export { highways } from './highways.js';
|
|
2185
2559
|
export { landmarks } from './landmarks.js';
|
|
2186
2560
|
export { getNeighborhood, neighborhoodCollections, neighborhoodSources } from './lookup.js';
|
|
2561
|
+
export { precinctMaps } from './precincts.js';
|
|
2187
2562
|
export { getRealtorNeighborhood, neighborhoods } from './realtor.js';
|
|
2188
2563
|
export { keyRoads } from './roads.js';
|
|
2189
2564
|
export { bartStations } from './stations.js';
|
|
@@ -2245,6 +2620,13 @@ export declare function getNeighborhood(name: string, { source }?: {
|
|
|
2245
2620
|
}): import("./types.js").Feature<NeighborhoodProperties> | undefined;
|
|
2246
2621
|
```
|
|
2247
2622
|
|
|
2623
|
+
### dist/data/precincts.d.ts
|
|
2624
|
+
|
|
2625
|
+
```ts
|
|
2626
|
+
import type { FeatureCollection, PrecinctProperties } from './types.js';
|
|
2627
|
+
export declare const precinctMaps: Readonly<Record<2022, FeatureCollection<PrecinctProperties>>>;
|
|
2628
|
+
```
|
|
2629
|
+
|
|
2248
2630
|
### dist/data/realtor.d.ts
|
|
2249
2631
|
|
|
2250
2632
|
```ts
|
|
@@ -2359,7 +2741,7 @@ import type { SFMapOptions } from './types.js';
|
|
|
2359
2741
|
/** Server-safe overview using exactly the guide browser geography. */
|
|
2360
2742
|
export declare function createGuideSVG(options?: SFMapOptions): {
|
|
2361
2743
|
svg: string;
|
|
2362
|
-
project: (coordinates: import("
|
|
2744
|
+
project: (coordinates: import("./geometry.js").Position) => [number, number];
|
|
2363
2745
|
viewBox: [number, number, number, number];
|
|
2364
2746
|
};
|
|
2365
2747
|
/** Stable compact frame, progressively enhanced with mountGuideMap(). No browser globals. */
|