@kahwee/sf-map-svg 4.4.0 → 5.0.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 +25 -0
- package/README.md +32 -0
- package/dist/data/README.md +7 -0
- package/dist/src/cards.d.ts +15 -0
- package/dist/src/cards.js +138 -0
- package/dist/src/cluster-layer.d.ts +2 -0
- package/dist/src/cluster-layer.js +2 -0
- 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-controls.d.ts +26 -0
- package/dist/src/explorer-controls.js +120 -0
- package/dist/src/explorer-core.d.ts +6 -1
- package/dist/src/explorer-core.js +156 -667
- package/dist/src/explorer-shell.d.ts +62 -0
- package/dist/src/explorer-shell.js +228 -0
- package/dist/src/explorer-sidebar.d.ts +20 -0
- package/dist/src/explorer-sidebar.js +79 -0
- package/dist/src/geometry.d.ts +6 -1
- package/dist/src/geometry.js +80 -0
- package/dist/src/guide-map.js +2 -1
- package/dist/src/guide-static.d.ts +1 -1
- package/dist/src/label-spread.d.ts +5 -0
- package/dist/src/label-spread.js +36 -0
- package/dist/src/layers.js +1 -1
- package/dist/src/map.d.ts +1 -1
- package/dist/src/map.js +24 -13
- package/dist/src/marker-catalog.d.ts +1 -1
- package/dist/src/marker-catalog.js +11 -5
- package/dist/src/neighborhood-layer.d.ts +67 -0
- package/dist/src/neighborhood-layer.js +256 -0
- package/dist/src/overlay-layer.d.ts +13 -0
- package/dist/src/overlay-layer.js +68 -0
- package/dist/src/palettes.d.ts +48 -0
- package/dist/src/palettes.js +40 -0
- package/dist/src/presets.d.ts +1 -0
- package/dist/src/presets.js +1 -0
- package/dist/src/static-labels.js +19 -4
- package/dist/src/static.d.ts +2 -2
- package/dist/src/static.js +21 -2
- package/dist/src/types.d.ts +21 -9
- package/dist/src/validation.js +7 -5
- package/docs/API.md +75 -5
- package/docs/EXAMPLES.md +29 -0
- package/docs/api-audit.md +22 -5
- package/docs/consumer-integration.md +86 -2
- 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 +508 -53
- package/llms.txt +2 -1
- package/package.json +17 -8
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.0.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.0.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
|
|
@@ -94,6 +99,33 @@ See [controller options, events, and lifecycle](https://github.com/kahwee/sf-map
|
|
|
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.
|
|
@@ -220,7 +252,7 @@ pnpm benchmark:markers --counts=2000 --mode=neighborhoods \
|
|
|
220
252
|
|
|
221
253
|
---
|
|
222
254
|
|
|
223
|
-
<!-- Generated from docs/developer-guide.md; @kahwee/sf-map-svg
|
|
255
|
+
<!-- Generated from docs/developer-guide.md; @kahwee/sf-map-svg 5.0.0 · local working tree. -->
|
|
224
256
|
|
|
225
257
|
# Developer guide
|
|
226
258
|
|
|
@@ -234,6 +266,7 @@ Choose the renderer and geography separately. `renderMap` produces a string with
|
|
|
234
266
|
| Explore districts or several neighborhood definitions | `/map`: `createMap` | `/data/full`: `fullMapData` |
|
|
235
267
|
| Embed a smaller neighborhood guide | `/guide/map`: `createGuideController` | Included lightweight guide geography |
|
|
236
268
|
| Use the guide data with the general controller | `/map`: `createMap` | `/guide/data`: `guideMapData` |
|
|
269
|
+
| Query bounds or polygon containment without a DOM | `/geometry` | Caller-supplied geometry or explicit dataset imports |
|
|
237
270
|
| Look up names without mounting a map | `/data/lookup` | Source-aware lookup helpers |
|
|
238
271
|
|
|
239
272
|
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 +353,43 @@ SFAR realtor neighborhoods are the default when supplied. If only an alternative
|
|
|
320
353
|
| Camera | `map.camera.set/pan/zoom/fit/reset(...)` |
|
|
321
354
|
| Palette, typography, control styling | `map.configure({ appearance })`; retains camera, selection and focus |
|
|
322
355
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
`
|
|
356
|
+
Controller patches merge nested token and feature records. Omitted and `undefined`
|
|
357
|
+
values retain existing values. Use `false` to disable a boolean switch and
|
|
358
|
+
`resetConfiguration()` to clear presentation overrides explicitly:
|
|
326
359
|
|
|
327
360
|
```ts
|
|
328
361
|
map.configure({ layers: { neighborhoodLabels: false } });
|
|
329
|
-
map.configure({ layers: { neighborhoodLabels: undefined } }); // follow mode again
|
|
330
362
|
map.configure({ features: { motion: false } });
|
|
331
|
-
map.
|
|
363
|
+
map.resetConfiguration(['layers']); // follow mode-derived defaults again
|
|
364
|
+
map.resetConfiguration(['appearance']); // built-in palette and typography
|
|
332
365
|
```
|
|
333
366
|
|
|
334
|
-
|
|
367
|
+
Reset groups are `features`, `layers`, `controls`, and `appearance`; omitting
|
|
368
|
+
the argument resets all four. Resets preserve geographic choices and camera,
|
|
369
|
+
and do not restore constructor overrides. Static maps accept grouped layers and
|
|
370
|
+
the shared appearance keys (`theme`, `colors`, `districtStyle`) at construction.
|
|
371
|
+
See [the API contract](https://github.com/kahwee/sf-map-svg/blob/main/docs/API.md#live-configuration-and-inspection).
|
|
372
|
+
|
|
373
|
+
`getConfiguration()` returns a detached snapshot of geographic choices and
|
|
374
|
+
presentation overrides. Use `getResolvedConfiguration()` for effective layers
|
|
375
|
+
and `getCapabilities()` for supplied geography. Invalid patches preserve state.
|
|
376
|
+
`setMode`, `setSource`, and `configure` all preserve the camera; request movement
|
|
377
|
+
with `camera.reset()` or `camera.fit(geometry)`. See the
|
|
378
|
+
[v5 migration guide](https://github.com/kahwee/sf-map-svg/blob/main/docs/migration-v5.md) for changed call forms.
|
|
379
|
+
|
|
380
|
+
## Geographic queries
|
|
381
|
+
|
|
382
|
+
The v5 `/geometry` helpers `geometryBounds(geometry)` and
|
|
383
|
+
`geometryContainsPoint(geometry, position)` work without a DOM or bundled
|
|
384
|
+
geography. Import `Bounds`, `Geometry`, and `Position` types from the same entry.
|
|
385
|
+
Use containment for area lookup and bounds for geographic extents; pass the
|
|
386
|
+
original geometry to `camera.fit`, rather than geographic bounds to `camera.set`.
|
|
387
|
+
See the [worked example](https://github.com/kahwee/sf-map-svg/blob/main/docs/EXAMPLES.md#find-areas-containing-a-location) and
|
|
388
|
+
[full query semantics](https://github.com/kahwee/sf-map-svg/blob/main/docs/API.md#geographic-queries).
|
|
335
389
|
|
|
336
390
|
## Markers, routes and camera coordinates
|
|
337
391
|
|
|
338
|
-
|
|
392
|
+
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
393
|
|
|
340
394
|
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
395
|
|
|
@@ -364,7 +418,7 @@ See [API options](https://github.com/kahwee/sf-map-svg/blob/main/docs/API.md), [
|
|
|
364
418
|
|
|
365
419
|
---
|
|
366
420
|
|
|
367
|
-
<!-- Generated from docs/API.md; @kahwee/sf-map-svg
|
|
421
|
+
<!-- Generated from docs/API.md; @kahwee/sf-map-svg 5.0.0 · local working tree. -->
|
|
368
422
|
|
|
369
423
|
# Map options and lifecycle
|
|
370
424
|
|
|
@@ -372,6 +426,41 @@ Start with the [static and interactive examples](https://github.com/kahwee/sf-ma
|
|
|
372
426
|
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
427
|
[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
428
|
|
|
429
|
+
## Geographic queries
|
|
430
|
+
|
|
431
|
+
New in v5. See the [migration guide](https://github.com/kahwee/sf-map-svg/blob/main/docs/migration-v5.md) for upgrade steps.
|
|
432
|
+
|
|
433
|
+
The data-free `@kahwee/sf-map-svg/geometry` entry exports `geometryBounds(geometry)`
|
|
434
|
+
and `geometryContainsPoint(geometry, [longitude, latitude])`. They work offline
|
|
435
|
+
with the same readonly GeoJSON geometries used by the renderer, without a DOM.
|
|
436
|
+
The same entry exports the `Geometry`, `Position`, and `Bounds` types, so geometry
|
|
437
|
+
consumers need no second import path for their function input and result types.
|
|
438
|
+
|
|
439
|
+
```ts
|
|
440
|
+
import { geometryBounds, geometryContainsPoint } from '@kahwee/sf-map-svg/geometry';
|
|
441
|
+
import { neighborhoods } from '@kahwee/sf-map-svg/data/realtor';
|
|
442
|
+
|
|
443
|
+
const location = [-122.42, 37.76] as const;
|
|
444
|
+
const matches = neighborhoods.features.filter((feature) =>
|
|
445
|
+
geometryContainsPoint(feature.geometry, location),
|
|
446
|
+
);
|
|
447
|
+
const bounds = matches[0] ? geometryBounds(matches[0].geometry) : null;
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
Bounds are `[west, south, east, north]` in degrees, computed from coordinates,
|
|
451
|
+
not cached feature metadata. Empty or absent geometry returns `null`.
|
|
452
|
+
Hit testing includes Polygon, MultiPolygon, and polygon members of nested
|
|
453
|
+
GeometryCollections. Hole interiors are excluded; outer and hole boundaries
|
|
454
|
+
are included, so a shared boundary may match multiple features. Ring winding
|
|
455
|
+
does not matter; open rings are closed implicitly. Other geometry types do
|
|
456
|
+
not produce polygon hits. Empty or absent geometry returns `false`.
|
|
457
|
+
|
|
458
|
+
Both helpers reject nonfinite or out-of-range longitude/latitude with
|
|
459
|
+
`RangeError`; altitude is ignored. They assume valid planar GeoJSON topology
|
|
460
|
+
and straight segments in longitude/latitude, suitable for these SF datasets.
|
|
461
|
+
They do not repair polygons, apply a boundary tolerance, or unwrap antimeridian
|
|
462
|
+
crossings. A bounds match alone does not establish polygon containment.
|
|
463
|
+
|
|
375
464
|
## Static maps
|
|
376
465
|
|
|
377
466
|
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.
|
|
@@ -399,8 +488,32 @@ The SVG description names the selected neighborhood source for source-aware data
|
|
|
399
488
|
|
|
400
489
|
Static `labelLayout: { width: 390 }` opts into shared interactive **overview** label priorities, sizes, anchors and collision rules. `width` must be a positive finite CSS canvas width; it does not alter the SVG viewBox. Set `neighborhoodLabels: true` (flat or under `layers`) to add area names in this layout. Guide SVGs and shells enable neighborhood names by default only when opting into `labelLayout`. `labels: false` remains the master switch. Default static output is unchanged.
|
|
401
490
|
|
|
491
|
+
For editorial previews, `labelLayout` also accepts `maxLabels` (a nonnegative safe integer), `haloColor` (an SVG color string), and `compact: { width, maxLabels }`. The compact width must be positive, finite, and smaller than the main layout width. Each layout places labels around pins independently, then chooses up to its budget for spatial coverage and nearby-pin context (selected marker first when it fits). Without a budget the existing overview order is unchanged. Equal spatial scores retain the original priority order. A zero budget hides text, never pins. The default halo remains white; use the land color for a quieter dark map.
|
|
492
|
+
|
|
493
|
+
Compact mode embeds two label groups and a media query in the same SVG. At or below the compact width it shows the compact group; above it shows the main group. In an `<img>` or standalone SVG, the breakpoint follows the SVG viewport. In inline SVG it follows the document viewport, so use `<img>` for cards whose width differs from the page. No JavaScript, duplicated geometry, external fonts, or extra image requests are needed. The compact width is also its font-layout hint; very narrow images still scale the text. The interactive renderer continues to use its live collision layout after enhancement.
|
|
494
|
+
|
|
495
|
+
```js
|
|
496
|
+
createGuideSVG({
|
|
497
|
+
labelLayout: {
|
|
498
|
+
width: 800,
|
|
499
|
+
haloColor: '#1b2c28',
|
|
500
|
+
compact: { width: 480, maxLabels: 4 },
|
|
501
|
+
},
|
|
502
|
+
colors: { water: '#0a1412', land: '#1b2c28', label: '#d5e3e8' },
|
|
503
|
+
markers: places,
|
|
504
|
+
});
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
See the [field-guide preview recipe](https://github.com/kahwee/sf-map-svg/blob/main/docs/consumer-integration.md#editorial-map-previews) for a complete build-time example.
|
|
508
|
+
|
|
402
509
|
Static placement uses estimated system-font metrics without browser globals. Interactive placement measures actual fonts and includes live selection and HTML furniture, so exact glyph positions and the visible label set can differ. See the [integration recipe](https://github.com/kahwee/sf-map-svg/blob/main/docs/consumer-integration.md#keep-overview-labels-consistent) for width and progressive-enhancement guidance.
|
|
403
510
|
|
|
511
|
+
## Editorial palettes
|
|
512
|
+
|
|
513
|
+
Import `mapPalettes` from `@kahwee/sf-map-svg/presets`. `nightForest` and `warmPaper` are deeply frozen, opt-in token sets. Each supplies `colors`, `haloColor`, `markerColor`, `selectedMarkerColor`, and `selectedMarkerRingColor`. They work with any supplied geography and do not change the existing themes. Text clears 4.5:1 and pins/rings clear 3:1 against the palette's land, park, and water surfaces; consumers must check their own overrides and surrounding UI.
|
|
514
|
+
|
|
515
|
+
For static maps pass `colors: palette.colors`, `labelLayout: { width: 800, haloColor: palette.haloColor }`, and the marker color on each marker. For interactive maps pass `appearance: { colors: palette.colors, labelStyle: { haloColor: palette.haloColor }, markerColor: palette.markerColor, selectedMarkerColor: palette.selectedMarkerColor }` and `features: { selectedMarkerRing: { color: palette.selectedMarkerRingColor } }`. Choose the palette in the consuming application; these tokens do not read system preferences or install global CSS.
|
|
516
|
+
|
|
404
517
|
## Interactive maps
|
|
405
518
|
|
|
406
519
|
`createMap` defaults to `mode: 'basemap'` and `appearance.theme: 'transit'`. Set the mode explicitly for neighborhood or district exploration. District and neighborhood layer defaults follow the mode; other supplied layers default on. The [developer guide](https://github.com/kahwee/sf-map-svg/blob/main/docs/developer-guide.md) compares static and interactive defaults and explains configuration resets, coordinates, and cleanup.
|
|
@@ -429,16 +542,16 @@ District style callbacks are evaluated once per district at construction and on
|
|
|
429
542
|
|
|
430
543
|
## Live configuration and inspection
|
|
431
544
|
|
|
432
|
-
`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
|
|
545
|
+
`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.
|
|
433
546
|
|
|
434
|
-
|
|
547
|
+
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.
|
|
435
548
|
|
|
436
549
|
```ts
|
|
437
550
|
map.configure({
|
|
438
551
|
appearance: { colors: { water: '#142d45' }, labelStyle: { fontFamily: 'Georgia,serif' } },
|
|
439
552
|
layers: { bartStations: false },
|
|
440
553
|
});
|
|
441
|
-
map.
|
|
554
|
+
map.resetConfiguration(['appearance']); // restore built-in defaults
|
|
442
555
|
|
|
443
556
|
const overrides = map.getConfiguration();
|
|
444
557
|
const effective = map.getResolvedConfiguration();
|
|
@@ -470,17 +583,28 @@ map.selectFeature({ kind: 'neighborhood', source: 'realtor', id: 'inner-mission'
|
|
|
470
583
|
map.selectFeature({ kind: 'district', year: 2022, id: 3 });
|
|
471
584
|
```
|
|
472
585
|
|
|
473
|
-
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.
|
|
586
|
+
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.
|
|
474
587
|
|
|
475
588
|
## Coordinates and units
|
|
476
589
|
|
|
477
|
-
|
|
590
|
+
Version 5 uses tuple inputs and exports the named `ScreenPoint` /
|
|
591
|
+
`ProjectToScreen` types. The former two-number call is removed.
|
|
592
|
+
|
|
593
|
+
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.
|
|
478
594
|
|
|
479
595
|
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.
|
|
480
596
|
|
|
597
|
+
## Linked place cards
|
|
598
|
+
|
|
599
|
+
`bindMapCards(map, root, options)` from `@kahwee/sf-map-svg/cards` connects consumer-owned cards to a `MapController`. Each card uses `data-map-marker="stable-marker-id"`; native `<button type="button" data-map-select>` controls select that place without panning. Article links keep their ordinary navigation. `options.markers` is the same catalog supplied to the map; `highlightColor` optionally changes the preview ring and `scrollOnSelect: false` disables automatic card reveal.
|
|
600
|
+
|
|
601
|
+
Pointer hover and keyboard focus draw a temporary ring through `projectToScreen()`. They do not call `selectMarker`, change the camera, or emit selection events. Focused cards take precedence over hovered cards. Offscreen points do not draw a ring. Touch users use the selection button. Selected cards receive `data-map-selected` and their buttons receive `aria-pressed`; style the card attribute in your own CSS. Selecting a pin reveals its card with immediate native scrolling, without moving focus. Filtering/reordering/removing cards is safe; IDs are compared directly, not inserted into CSS selectors.
|
|
602
|
+
|
|
603
|
+
Call `binding.setMarkers(next)` after `map.setMarkers(next)` when the catalog changes. Call `binding.destroy()` before `map.destroy()` on unmount; cleanup is idempotent and restores attributes that the binding owned. The module does not create cards, fetch data, change URLs, install global CSS, or import geography. See the [linked-card recipe](https://github.com/kahwee/sf-map-svg/blob/main/docs/consumer-integration.md#link-place-cards-to-pins).
|
|
604
|
+
|
|
481
605
|
---
|
|
482
606
|
|
|
483
|
-
<!-- Generated from docs/EXAMPLES.md; @kahwee/sf-map-svg
|
|
607
|
+
<!-- Generated from docs/EXAMPLES.md; @kahwee/sf-map-svg 5.0.0 · local working tree. -->
|
|
484
608
|
|
|
485
609
|
# Examples
|
|
486
610
|
|
|
@@ -590,9 +714,38 @@ See [consumer integration](https://github.com/kahwee/sf-map-svg/blob/main/docs/c
|
|
|
590
714
|
|
|
591
715
|
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).
|
|
592
716
|
|
|
717
|
+
## Find areas containing a location
|
|
718
|
+
|
|
719
|
+
New in v5. This example runs in
|
|
720
|
+
Node or a browser without mounting a map and imports only the SFAR definition:
|
|
721
|
+
|
|
722
|
+
```ts
|
|
723
|
+
import { geometryBounds, geometryContainsPoint } from '@kahwee/sf-map-svg/geometry';
|
|
724
|
+
import { neighborhoods } from '@kahwee/sf-map-svg/data/realtor';
|
|
725
|
+
|
|
726
|
+
const location = [-122.42, 37.76] as const;
|
|
727
|
+
const matches = neighborhoods.features.filter((feature) =>
|
|
728
|
+
geometryContainsPoint(feature.geometry, location),
|
|
729
|
+
);
|
|
730
|
+
for (const feature of matches) {
|
|
731
|
+
console.log(feature.id, feature.properties.canonicalName, geometryBounds(feature.geometry));
|
|
732
|
+
}
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
Keep all matches when the location lies on a shared boundary. An empty result
|
|
736
|
+
means no supplied polygon contains the location; it does not trigger a switch
|
|
737
|
+
to another neighborhood definition. For an existing map using the same realtor
|
|
738
|
+
source, pass a chosen match to
|
|
739
|
+
`map.selectFeature({ kind: 'neighborhood', source: 'realtor', id: feature.id })`.
|
|
740
|
+
To fit it, use `map.camera.fit(feature.geometry)`. Geographic bounds cannot be
|
|
741
|
+
passed directly to `camera.set`, which takes projected `[x, y, size]`.
|
|
742
|
+
|
|
743
|
+
See [query semantics and limitations](https://github.com/kahwee/sf-map-svg/blob/main/docs/API.md#geographic-queries) and the
|
|
744
|
+
[HTML overlay recipe](https://github.com/kahwee/sf-map-svg/blob/main/docs/consumer-integration.md#position-html-overlays).
|
|
745
|
+
|
|
593
746
|
---
|
|
594
747
|
|
|
595
|
-
<!-- Generated from docs/consumer-integration.md; @kahwee/sf-map-svg
|
|
748
|
+
<!-- Generated from docs/consumer-integration.md; @kahwee/sf-map-svg 5.0.0 · local working tree. -->
|
|
596
749
|
|
|
597
750
|
# Embedding and progressive enhancement
|
|
598
751
|
|
|
@@ -631,7 +784,7 @@ For a fixed-width embed, use shared inputs on the server and in the browser:
|
|
|
631
784
|
```ts
|
|
632
785
|
// Shared inputs: coordinates, IDs, radii, colors and layer choices must match.
|
|
633
786
|
const guideMarkers = [
|
|
634
|
-
{ id: 'ferry', label: 'Ferry Building',
|
|
787
|
+
{ id: 'ferry', label: 'Ferry Building', position: [-122.3937, 37.7955] as const, radius: 7 },
|
|
635
788
|
];
|
|
636
789
|
const guideLayers = { neighborhoodLabels: true, roadLabels: true };
|
|
637
790
|
const guideColors = { label: '#183e58', landmark: '#426641' };
|
|
@@ -647,7 +800,7 @@ const labeledPlacesMarkup = createGuideShell({
|
|
|
647
800
|
|
|
648
801
|
After inserting that markup, pass `markers: guideMarkers`, `layers: guideLayers`, and `appearance: { colors: guideColors }` to `mountGuideController()` as shown below. The static-only `labelLayout` option is not a browser option: the controller measures the actual canvas and font. The shell border is outside the canvas, so a default shell with a 390 px outer width has a 388 px canvas.
|
|
649
802
|
|
|
650
|
-
For responsive layouts whose canvas width is unknown at build time, choose a representative width and accept text scaling until enhancement, or serve independently rendered SVG variants through your application's responsive markup. The package does not detect a device on the server. Keep `labels: false` if your design requires no visible text change at enhancement.
|
|
803
|
+
For responsive layouts whose canvas width is unknown at build time, use `labelLayout.compact` for an embedded image with two static label layouts (see [editorial previews](#editorial-map-previews)), choose a representative width and accept text scaling until enhancement, or serve independently rendered SVG variants through your application's responsive markup. The package does not detect a device on the server. Keep `labels: false` if your design requires no visible text change at enhancement.
|
|
651
804
|
|
|
652
805
|
For `renderMap()`, also enable `neighborhoodLabels` explicitly; it defaults off outside the guide preset. `labels: false` hides all text in either layout while preserving symbols and accessible titles.
|
|
653
806
|
|
|
@@ -684,9 +837,198 @@ For a new browser-only guide, use `createGuideController()` and mount its `.elem
|
|
|
684
837
|
|
|
685
838
|
See the [exact API contract](https://github.com/kahwee/sf-map-svg/blob/main/docs/API.md), [worked examples](https://github.com/kahwee/sf-map-svg/blob/main/docs/EXAMPLES.md), and [bundle measurements](https://github.com/kahwee/sf-map-svg/blob/main/docs/guide-bundle-report.md).
|
|
686
839
|
|
|
840
|
+
|
|
841
|
+
## Editorial map previews
|
|
842
|
+
|
|
843
|
+
YorkSF uses one city overview to connect its field notes and compact locator maps to orient individual articles. These patterns work for any local guide: keep the place catalog in the consuming application, generate SVG images at build time, and link each preview to the application's map or article route.
|
|
844
|
+
|
|
845
|
+
Run `pnpm demo` and open `examples/generated/field-guide.html` for a self-contained light/dark example. Its overview uses `labelLayout.compact` to limit mobile text without removing pins, and `haloColor` to match the night palette. The locator cards share those colors but omit labels at small sizes. Images reserve their aspect ratio and load lazily; the links work without JavaScript. The example derives its sample park coordinates from the package's existing landmark data rather than maintaining another geographic dataset.
|
|
846
|
+
|
|
847
|
+
Use stable marker IDs for deep links. Keep addresses, visit dates, historical-status caveats, and editorial descriptions in your own content model. The package supplies map graphics; it does not imply that a place is currently open or that an author visited it. Keep a visible list beside the overview so the image is not the only way to find a place. SVG alt text should describe its purpose; link text should name the destination.
|
|
848
|
+
|
|
849
|
+
For an image card, `compact.width` follows the image's rendered viewport. For inline SVG it follows the outer document viewport; do not assume it behaves like a container query. Avoid embedding scripts in exported images. Interactive maps remain a separate progressive enhancement.
|
|
850
|
+
|
|
851
|
+
## Link place cards to pins
|
|
852
|
+
|
|
853
|
+
Keep ordinary article links and add a separate map-selection button. Native buttons give keyboard and touch users the same action as pointer users:
|
|
854
|
+
|
|
855
|
+
```html
|
|
856
|
+
<article data-map-marker="park">
|
|
857
|
+
<h2><a href="/places/park/">A place in the park</a></h2>
|
|
858
|
+
<button type="button" data-map-select>Show on map</button>
|
|
859
|
+
</article>
|
|
860
|
+
```
|
|
861
|
+
|
|
862
|
+
```js
|
|
863
|
+
import { bindMapCards } from '@kahwee/sf-map-svg/cards';
|
|
864
|
+
import { mapPalettes } from '@kahwee/sf-map-svg/presets';
|
|
865
|
+
|
|
866
|
+
const binding = bindMapCards(map, document.querySelector('#place-cards'), {
|
|
867
|
+
markers: places,
|
|
868
|
+
highlightColor: mapPalettes.nightForest.selectedMarkerRingColor,
|
|
869
|
+
});
|
|
870
|
+
// On catalog change: map.setMarkers(next); binding.setMarkers(next);
|
|
871
|
+
// On unmount: binding.destroy(); map.destroy();
|
|
872
|
+
```
|
|
873
|
+
|
|
874
|
+
Style `[data-map-selected]` with an outline or background change that remains legible in both themes. Use a visible focus outline on card buttons and links. Hover/focus only previews the location: it preserves selection and camera. Map selection reveals the card without shifting keyboard focus; set `scrollOnSelect: false` if your application owns scrolling. Match cards and markers by stable IDs, and keep hidden/filtered places out of both catalogs when that is your intended filter behavior.
|
|
875
|
+
|
|
876
|
+
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.
|
|
877
|
+
|
|
878
|
+
## Position HTML overlays
|
|
879
|
+
|
|
880
|
+
In v5, pass the same readonly `[longitude, latitude]` position used by static
|
|
881
|
+
projection. The former two-number call is removed; see the
|
|
882
|
+
[migration guide](https://github.com/kahwee/sf-map-svg/blob/main/docs/migration-v5.md).
|
|
883
|
+
|
|
884
|
+
Call this helper after mounting the map in a container with nonzero width:
|
|
885
|
+
|
|
886
|
+
```ts
|
|
887
|
+
import type { MapController } from '@kahwee/sf-map-svg';
|
|
888
|
+
import type { Position } from '@kahwee/sf-map-svg/geometry';
|
|
889
|
+
|
|
890
|
+
function attachCallout(map: MapController, location: Position, label: string) {
|
|
891
|
+
const callout = document.createElement('span');
|
|
892
|
+
callout.textContent = label;
|
|
893
|
+
callout.style.cssText = 'position:absolute;pointer-events:none;transform:translate(-50%,-100%)';
|
|
894
|
+
const update = () => {
|
|
895
|
+
const point = map.projectToScreen(location);
|
|
896
|
+
callout.hidden = !point.visible;
|
|
897
|
+
callout.style.left = `${point.x}px`;
|
|
898
|
+
callout.style.top = `${point.y}px`;
|
|
899
|
+
};
|
|
900
|
+
update();
|
|
901
|
+
map.overlayElement.append(callout);
|
|
902
|
+
const stopViewport = map.on('viewportchange', update);
|
|
903
|
+
const stopResize = map.on('mapresize', update);
|
|
904
|
+
return () => {
|
|
905
|
+
stopViewport();
|
|
906
|
+
stopResize();
|
|
907
|
+
callout.remove();
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
Use `const detach = attachCallout(map, [-122.3937, 37.7955], 'Ferry Building')`
|
|
913
|
+
with your mounted controller. Call `detach()` before `map.destroy()` on unmount.
|
|
914
|
+
Keep the container visible while projecting; an unmounted or zero-width canvas
|
|
915
|
+
throws. Controller calls after destruction also throw. Camera moves and resize
|
|
916
|
+
both require repositioning; a one-time projection becomes stale.
|
|
917
|
+
|
|
918
|
+
`x` and `y` are CSS pixels relative to `overlayElement`, not page coordinates or
|
|
919
|
+
SVG units. `visible` tests inclusion in the map viewport; it does not account
|
|
920
|
+
for occlusion by other content. This noninteractive label uses `textContent`
|
|
921
|
+
and leaves pointer input to the map. Interactive callouts need their own
|
|
922
|
+
keyboard, focus, and dismissal behavior. See [coordinates and units](https://github.com/kahwee/sf-map-svg/blob/main/docs/API.md#coordinates-and-units).
|
|
923
|
+
|
|
924
|
+
---
|
|
925
|
+
|
|
926
|
+
<!-- Generated from docs/migration-v5.md; @kahwee/sf-map-svg 5.0.0 · local working tree. -->
|
|
927
|
+
|
|
928
|
+
# Migrating to v5
|
|
929
|
+
|
|
930
|
+
Version 5.0.0 makes coordinate inputs, controller updates, and reset behavior
|
|
931
|
+
consistent. It requires Node 24+ and retains offline rendering, explicit data
|
|
932
|
+
imports, zero runtime dependencies, and the existing default map appearance.
|
|
933
|
+
The version bump prepares the next release; it does not publish the package.
|
|
934
|
+
|
|
935
|
+
## Coordinates use GeoJSON positions
|
|
936
|
+
|
|
937
|
+
Markers now require `position: [longitude, latitude]`. The old `lng` and `lat`
|
|
938
|
+
properties are rejected. `projectToScreen` takes one readonly position; its
|
|
939
|
+
old two-number form is removed. Altitude is accepted and ignored by projection.
|
|
940
|
+
|
|
941
|
+
```ts
|
|
942
|
+
// v4
|
|
943
|
+
const oldMarker = { id: 'ferry', lng: -122.3937, lat: 37.7955 };
|
|
944
|
+
// v5
|
|
945
|
+
const marker = { id: 'ferry', position: [-122.3937, 37.7955] as const };
|
|
946
|
+
map.setMarkers([marker]);
|
|
947
|
+
const point = map.projectToScreen(marker.position);
|
|
948
|
+
```
|
|
949
|
+
|
|
950
|
+
Use `Position` from `/geometry` for reusable positions and `ScreenPoint` or
|
|
951
|
+
`ProjectToScreen` from the root or `/map` for screen-projection contracts.
|
|
952
|
+
`x` and `y` remain canvas-relative CSS pixels; `visible` means inside the
|
|
953
|
+
viewport, not unobscured. Mount the map in a nonzero-width container first.
|
|
954
|
+
Camera viewports still use projected `[x, y, size]`; camera pan deltas are CSS
|
|
955
|
+
pixels. Geographic bounds are not camera viewports.
|
|
956
|
+
|
|
957
|
+
## Controller updates preserve the camera
|
|
958
|
+
|
|
959
|
+
`configure`, `setMode`, and `setSource` preserve the camera. The second
|
|
960
|
+
`{ resetView }` argument to the controller convenience setters is removed.
|
|
961
|
+
To reproduce an old automatic reset, request it explicitly:
|
|
962
|
+
|
|
963
|
+
```ts
|
|
964
|
+
map.setMode('neighborhoods');
|
|
965
|
+
map.camera.reset({ animate: false });
|
|
966
|
+
```
|
|
967
|
+
|
|
968
|
+
Selection still fits by default. Use `{ fit: false }` to change selection
|
|
969
|
+
without moving the camera, or `camera.fit(geometry)` for an explicit fit.
|
|
970
|
+
|
|
971
|
+
## Reset presentation explicitly
|
|
972
|
+
|
|
973
|
+
In controller `configure` patches, omitted and `undefined` values are no-ops at
|
|
974
|
+
every record depth. Nested token and feature records merge; an explicit boolean
|
|
975
|
+
replaces a feature record and `false` disables a boolean switch.
|
|
976
|
+
|
|
977
|
+
```ts
|
|
978
|
+
map.configure({ appearance: { colors: { water: '#142d45' } } });
|
|
979
|
+
map.configure({ appearance: undefined }); // keeps the current appearance
|
|
980
|
+
map.resetConfiguration(['appearance']); // restores built-in appearance defaults
|
|
981
|
+
map.resetConfiguration(['layers', 'controls']); // removes explicit overrides
|
|
982
|
+
map.resetConfiguration(); // resets all four presentation groups
|
|
983
|
+
```
|
|
984
|
+
|
|
985
|
+
The reset groups are `features`, `layers`, `controls`, and `appearance`.
|
|
986
|
+
Resetting a group does not restore initial constructor overrides, change the
|
|
987
|
+
camera, or change mode/source/year/labels. It restores built-in defaults and
|
|
988
|
+
mode-derived layer choices. Supply geographic choices explicitly through
|
|
989
|
+
`configure`. To remove a single token while retaining other overrides, build
|
|
990
|
+
the desired group, reset that group, then configure the retained values.
|
|
991
|
+
Each call commits atomically; the two calls are separate updates.
|
|
992
|
+
|
|
993
|
+
These rules describe `MapController` from `createMap`, `createGuideController`,
|
|
994
|
+
and `mountGuideController`. Low-level element factories retain their separate
|
|
995
|
+
`setFeatures` / `setLayers` APIs; use controllers for the grouped update contract.
|
|
996
|
+
|
|
997
|
+
## Select neighborhoods by exact identity
|
|
998
|
+
|
|
999
|
+
Controller `selectNeighborhood(id)` accepts an exact ID in the current source,
|
|
1000
|
+
rather than a display name or alias. Unknown IDs return `false` without changing
|
|
1001
|
+
selection. Resolve a human-entered name using the source-specific lookup first.
|
|
1002
|
+
For integrations, prefer the source-qualified identity:
|
|
1003
|
+
|
|
1004
|
+
```ts
|
|
1005
|
+
import { getRealtorNeighborhood } from '@kahwee/sf-map-svg/data/realtor';
|
|
1006
|
+
|
|
1007
|
+
const feature = getRealtorNeighborhood('Inner Mission');
|
|
1008
|
+
if (feature) {
|
|
1009
|
+
map.selectFeature({ kind: 'neighborhood', source: 'realtor', id: feature.id }, { fit: false });
|
|
1010
|
+
}
|
|
1011
|
+
```
|
|
1012
|
+
|
|
1013
|
+
`selectFeature` requires the current supplied source/year and never switches
|
|
1014
|
+
geography implicitly. Marker IDs and numeric district IDs remain unchanged.
|
|
1015
|
+
The construction-time `neighborhood` option and low-level element lookup APIs
|
|
1016
|
+
continue to resolve names; the stricter ID contract applies to controller
|
|
1017
|
+
selection methods. Existing selection event names remain available alongside
|
|
1018
|
+
`selectionchange`.
|
|
1019
|
+
|
|
1020
|
+
## Offline geometry queries
|
|
1021
|
+
|
|
1022
|
+
`geometryBounds(geometry)` and `geometryContainsPoint(geometry, position)` are
|
|
1023
|
+
new in v5. They accept readonly GeoJSON without a DOM or geographic imports.
|
|
1024
|
+
Bounds are `[west, south, east, north]` or `null` for empty geometry. Containment
|
|
1025
|
+
respects holes, includes polygon boundaries, and supports islands and nested
|
|
1026
|
+
collections. Shared edges can match more than one feature. See the
|
|
1027
|
+
[query contract](https://github.com/kahwee/sf-map-svg/blob/main/docs/API.md#geographic-queries) for validation and planar limitations.
|
|
1028
|
+
|
|
687
1029
|
---
|
|
688
1030
|
|
|
689
|
-
<!-- Generated from docs/migration-v3.md; @kahwee/sf-map-svg
|
|
1031
|
+
<!-- Generated from docs/migration-v3.md; @kahwee/sf-map-svg 5.0.0 · local working tree. -->
|
|
690
1032
|
|
|
691
1033
|
# Migrate to version 3
|
|
692
1034
|
|
|
@@ -727,7 +1069,7 @@ District styles are now prepared once per district on construction and style/yea
|
|
|
727
1069
|
|
|
728
1070
|
---
|
|
729
1071
|
|
|
730
|
-
<!-- Generated from SOURCES.md; @kahwee/sf-map-svg
|
|
1072
|
+
<!-- Generated from SOURCES.md; @kahwee/sf-map-svg 5.0.0 · local working tree. -->
|
|
731
1073
|
|
|
732
1074
|
# Geometry and provenance
|
|
733
1075
|
|
|
@@ -911,7 +1253,7 @@ display geometry, and no precinct polygons or raw workbook data are shipped.
|
|
|
911
1253
|
|
|
912
1254
|
---
|
|
913
1255
|
|
|
914
|
-
<!-- Generated from CHANGELOG.md; @kahwee/sf-map-svg
|
|
1256
|
+
<!-- Generated from CHANGELOG.md; @kahwee/sf-map-svg 5.0.0 · local working tree. -->
|
|
915
1257
|
|
|
916
1258
|
# Changelog
|
|
917
1259
|
|
|
@@ -919,6 +1261,31 @@ User-visible changes are recorded here. Unreleased entries describe changes on `
|
|
|
919
1261
|
|
|
920
1262
|
## Unreleased
|
|
921
1263
|
|
|
1264
|
+
## 5.0.0 — 2026-10-19
|
|
1265
|
+
|
|
1266
|
+
- Breaking: controller `setMode` and `setSource` preserve the camera; remove their `{ resetView }` argument. Call `camera.reset()` or `camera.fit()` explicitly.
|
|
1267
|
+
- 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.
|
|
1268
|
+
- Breaking: controller `selectNeighborhood` selects exact IDs in the current source; resolve names through source-specific data lookup or use source/year-qualified `selectFeature`.
|
|
1269
|
+
- 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.
|
|
1270
|
+
|
|
1271
|
+
- 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.
|
|
1272
|
+
- 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.
|
|
1273
|
+
- 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.
|
|
1274
|
+
|
|
1275
|
+
## 4.5.0 — 2026-10-14
|
|
1276
|
+
|
|
1277
|
+
- 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.
|
|
1278
|
+
- Spread budgeted static labels across the map while favoring nearby-pin context and retaining a selected-place label when it fits. Keep collision avoidance, all pins, unbudgeted labels, and default output unchanged.
|
|
1279
|
+
|
|
1280
|
+
- Add opt-in `mapPalettes.nightForest` and `mapPalettes.warmPaper` tokens for consistent static/interactive map colors, label halos, markers, and selection rings, with contrast checks.
|
|
1281
|
+
|
|
1282
|
+
- Port YorkSF’s editorial map-preview pattern into static rendering: opt-in compact SVG label layouts, spatial label budgets, and configurable halos for dark palettes. Add a build-time field-guide example and light/dark Storybook preview without runtime dependencies or changes to default output.
|
|
1283
|
+
|
|
1284
|
+
- Separate interactive neighborhood state, controls, sidebar downloads and overlays into lifecycle-owned modules; revoke neighborhood hover listeners when replacing sources and keep optional explorer sidebar code out of normal map bundles.
|
|
1285
|
+
- Build the package once during `pnpm check`, while keeping standalone test and typecheck commands self-contained.
|
|
1286
|
+
|
|
1287
|
+
- Include compatible development-tool updates to Vite 8.3.4 and Playwright 1.64.0. Keep Storybook/addons 10.6.1 and aligned Vitest/browser/coverage 5.0.3; their newer 10.6.2 and 5.0.4 releases remain inside pnpm's release-age window. Make the consumer examples' custom touch-button accessible names include their visible text. Remove the obsolete Storybook 10.6.0 peer overrides; retain Node 24+ and zero runtime dependencies. These tooling changes were previously prepared as 4.4.1 but not published separately.
|
|
1288
|
+
|
|
922
1289
|
## 4.4.0 — 2026-10-12
|
|
923
1290
|
|
|
924
1291
|
- Add opt-in static `labelLayout: { width }` to `renderMap`, `createGuideSVG`, and `createGuideShell`. Static and interactive maps now share overview priorities, neighborhood anchors, font sizes, and collision rules; centered labels retain their anchors during enhancement.
|
|
@@ -1263,7 +1630,7 @@ consistent home, and makes animation and subscription ownership predictable.
|
|
|
1263
1630
|
|
|
1264
1631
|
# Published type contracts
|
|
1265
1632
|
|
|
1266
|
-
@kahwee/sf-map-svg
|
|
1633
|
+
@kahwee/sf-map-svg 5.0.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.
|
|
1267
1634
|
|
|
1268
1635
|
| Public import | Declaration |
|
|
1269
1636
|
| --- | --- |
|
|
@@ -1292,6 +1659,7 @@ consistent home, and makes animation and subscription ownership predictable.
|
|
|
1292
1659
|
| @kahwee/sf-map-svg/map | ./dist/src/map.d.ts |
|
|
1293
1660
|
| @kahwee/sf-map-svg/static | ./dist/src/static.d.ts |
|
|
1294
1661
|
| @kahwee/sf-map-svg/presets | ./dist/src/presets.d.ts |
|
|
1662
|
+
| @kahwee/sf-map-svg/cards | ./dist/src/cards.d.ts |
|
|
1295
1663
|
|
|
1296
1664
|
### ./dist/src/api.d.ts
|
|
1297
1665
|
|
|
@@ -1308,7 +1676,7 @@ import type { MapController, MapOptions } from './controller-types.js';
|
|
|
1308
1676
|
import type { InteractiveSFMapData } from './explorer-data.js';
|
|
1309
1677
|
export type * from './controller-types.js';
|
|
1310
1678
|
export type { InteractiveSFMapData as MapData } from './explorer-data.js';
|
|
1311
|
-
export type { CameraOptions, DistrictSelection, DistrictStyle, DistrictYear, InteractiveLayers, MapFeatures, MapMarker, MapOverlay, MapPadding, MapViewport, NeighborhoodSelection, } from './types.js';
|
|
1679
|
+
export type { CameraOptions, DistrictSelection, DistrictStyle, DistrictYear, InteractiveLayers, MapFeatures, MapMarker, MapOverlay, MapPadding, MapViewport, NeighborhoodSelection, ProjectToScreen, ScreenPoint, } from './types.js';
|
|
1312
1680
|
/** No geography is imported. Supply a preset or your own immutable data. */
|
|
1313
1681
|
export declare function createMap(data: InteractiveSFMapData, options?: MapOptions): MapController;
|
|
1314
1682
|
```
|
|
@@ -1317,8 +1685,8 @@ export declare function createMap(data: InteractiveSFMapData, options?: MapOptio
|
|
|
1317
1685
|
|
|
1318
1686
|
```ts
|
|
1319
1687
|
import type { Geometry, NeighborhoodSource } from '../data/types.js';
|
|
1320
|
-
import type { CameraOptions, DistrictSelection, DistrictStyle, DistrictYear, InteractiveLayers, MapFeatures, MapMarker, MapOverlay, MapPadding, MapViewport, NeighborhoodExplorerOptions, NeighborhoodSelection } from './types.js';
|
|
1321
|
-
/**
|
|
1688
|
+
import type { CameraOptions, DistrictSelection, DistrictStyle, DistrictYear, InteractiveLayers, MapFeatures, MapMarker, MapOverlay, MapPadding, MapViewport, NeighborhoodExplorerOptions, NeighborhoodSelection, ProjectToScreen } from './types.js';
|
|
1689
|
+
/** Explicit undefined is accepted as a no-op, including with exactOptionalPropertyTypes. */
|
|
1322
1690
|
type Resettable<T> = {
|
|
1323
1691
|
[K in keyof T]?: T[K] | undefined;
|
|
1324
1692
|
};
|
|
@@ -1327,7 +1695,7 @@ export type MapAppearance = {
|
|
|
1327
1695
|
[K in keyof AppearanceOptions]?: K extends 'colors' | 'style' | 'labelStyle' | 'labelSize' | 'areaStyle' ? Resettable<NonNullable<AppearanceOptions[K]>> | undefined : AppearanceOptions[K] | undefined;
|
|
1328
1696
|
};
|
|
1329
1697
|
export type MapControls = NonNullable<NeighborhoodExplorerOptions['controls']>;
|
|
1330
|
-
/** Omitted
|
|
1698
|
+
/** Omitted and undefined values retain current values. Use resetConfiguration to clear overrides. */
|
|
1331
1699
|
export interface MapConfiguration {
|
|
1332
1700
|
features?: Resettable<MapFeatures> | undefined;
|
|
1333
1701
|
layers?: Resettable<InteractiveLayers> | undefined;
|
|
@@ -1433,10 +1801,7 @@ export interface MapEvents {
|
|
|
1433
1801
|
};
|
|
1434
1802
|
mapresize: undefined;
|
|
1435
1803
|
}
|
|
1436
|
-
|
|
1437
|
-
export interface MapViewUpdateOptions {
|
|
1438
|
-
resetView?: boolean;
|
|
1439
|
-
}
|
|
1804
|
+
export type MapConfigurationGroup = 'features' | 'layers' | 'controls' | 'appearance';
|
|
1440
1805
|
export interface MapCamera {
|
|
1441
1806
|
/** Fixed 800 × 800 projected space, not longitude/latitude. */
|
|
1442
1807
|
get(): MapViewport;
|
|
@@ -1457,6 +1822,8 @@ export interface MapController {
|
|
|
1457
1822
|
readonly camera: MapCamera;
|
|
1458
1823
|
readonly destroyed: boolean;
|
|
1459
1824
|
configure(patch: MapConfiguration): void;
|
|
1825
|
+
/** Clear presentation overrides to built-in defaults, preserving camera and geographic choices. */
|
|
1826
|
+
resetConfiguration(groups?: readonly MapConfigurationGroup[]): void;
|
|
1460
1827
|
getConfiguration(): MapConfigurationSnapshot;
|
|
1461
1828
|
getResolvedConfiguration(): ResolvedMapConfiguration;
|
|
1462
1829
|
getCapabilities(): MapCapabilities;
|
|
@@ -1472,7 +1839,7 @@ export interface MapController {
|
|
|
1472
1839
|
fit?: boolean;
|
|
1473
1840
|
}): boolean;
|
|
1474
1841
|
getSelectedMarker(): MapMarker | null;
|
|
1475
|
-
selectNeighborhood(
|
|
1842
|
+
selectNeighborhood(id: string | null, options?: CameraOptions & {
|
|
1476
1843
|
fit?: boolean;
|
|
1477
1844
|
}): boolean;
|
|
1478
1845
|
getSelectedNeighborhood(): NeighborhoodSelection | null;
|
|
@@ -1482,15 +1849,12 @@ export interface MapController {
|
|
|
1482
1849
|
getSelectedDistrict(): DistrictSelection | null;
|
|
1483
1850
|
setDistrictYear(year: DistrictYear, options?: CameraOptions): void;
|
|
1484
1851
|
setDistrictStyle(style: ((district: DistrictSelection['district']) => DistrictStyle) | undefined): void;
|
|
1485
|
-
setSource(source: NeighborhoodSource
|
|
1486
|
-
setMode(mode: NonNullable<MapOptions['mode']
|
|
1852
|
+
setSource(source: NeighborhoodSource): void;
|
|
1853
|
+
setMode(mode: NonNullable<MapOptions['mode']>): void;
|
|
1487
1854
|
setLabels(visible: boolean): void;
|
|
1488
1855
|
setTouchNavigation(enabled: boolean): void;
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
y: number;
|
|
1492
|
-
visible: boolean;
|
|
1493
|
-
};
|
|
1856
|
+
/** Accepts a GeoJSON position; returns canvas-relative CSS pixels. */
|
|
1857
|
+
projectToScreen: ProjectToScreen;
|
|
1494
1858
|
/** Idempotent; stops work and managed subscriptions. Does not remove host-owned DOM. */
|
|
1495
1859
|
destroy(): void;
|
|
1496
1860
|
}
|
|
@@ -1633,7 +1997,15 @@ export interface KeyRoadProperties {
|
|
|
1633
1997
|
### dist/src/types.d.ts
|
|
1634
1998
|
|
|
1635
1999
|
```ts
|
|
1636
|
-
import type { Geometry, NeighborhoodFeature, NeighborhoodSource } from '../data/types.js';
|
|
2000
|
+
import type { Geometry, NeighborhoodFeature, NeighborhoodSource, Position } from '../data/types.js';
|
|
2001
|
+
/** Canvas-relative CSS pixels; visibility means inside the current square map viewport. */
|
|
2002
|
+
export interface ScreenPoint {
|
|
2003
|
+
x: number;
|
|
2004
|
+
y: number;
|
|
2005
|
+
visible: boolean;
|
|
2006
|
+
}
|
|
2007
|
+
/** WGS84 longitude/latitude; altitude, if supplied in a position, is ignored. */
|
|
2008
|
+
export type ProjectToScreen = (position: Position) => ScreenPoint;
|
|
1637
2009
|
export type DistrictYear = 2002 | 2012 | 2022;
|
|
1638
2010
|
export interface DistrictSelection {
|
|
1639
2011
|
id: number;
|
|
@@ -1647,8 +2019,7 @@ export interface DistrictStyle {
|
|
|
1647
2019
|
}
|
|
1648
2020
|
export interface MapMarker {
|
|
1649
2021
|
id: string;
|
|
1650
|
-
|
|
1651
|
-
lat: number;
|
|
2022
|
+
position: Position;
|
|
1652
2023
|
label?: string;
|
|
1653
2024
|
selected?: boolean;
|
|
1654
2025
|
color?: string;
|
|
@@ -1691,6 +2062,15 @@ export interface SFMapOptions {
|
|
|
1691
2062
|
/** Opt into interactive overview label priorities/collisions at this CSS display width. Uses estimated system-font metrics without a DOM. */
|
|
1692
2063
|
labelLayout?: {
|
|
1693
2064
|
width: number;
|
|
2065
|
+
/** Maximum placed labels, spread across the map with nearby-pin context; zero hides text only. */
|
|
2066
|
+
maxLabels?: number;
|
|
2067
|
+
/** Label outline color, for example the land color on a dark map. */
|
|
2068
|
+
haloColor?: string;
|
|
2069
|
+
/** Alternate layout below this SVG viewport width, without browser JavaScript. */
|
|
2070
|
+
compact?: {
|
|
2071
|
+
width: number;
|
|
2072
|
+
maxLabels: number;
|
|
2073
|
+
};
|
|
1694
2074
|
};
|
|
1695
2075
|
highways?: boolean;
|
|
1696
2076
|
keyRoads?: boolean;
|
|
@@ -1846,11 +2226,7 @@ export interface CameraOptions {
|
|
|
1846
2226
|
export interface NeighborhoodExplorerElement extends HTMLElement {
|
|
1847
2227
|
/** Append positioned HTML children here; coordinates are relative to this layer. */
|
|
1848
2228
|
readonly overlayElement: HTMLDivElement;
|
|
1849
|
-
projectToScreen
|
|
1850
|
-
x: number;
|
|
1851
|
-
y: number;
|
|
1852
|
-
visible: boolean;
|
|
1853
|
-
};
|
|
2229
|
+
projectToScreen: ProjectToScreen;
|
|
1854
2230
|
stopAnimation(): void;
|
|
1855
2231
|
/** Atomic patch; false disables, undefined resets a feature to its default. */
|
|
1856
2232
|
setFeatures(patch: MapFeatures): void;
|
|
@@ -1894,7 +2270,7 @@ export interface NeighborhoodExplorerElement extends HTMLElement {
|
|
|
1894
2270
|
export interface TransitAnimationElement extends HTMLElement {
|
|
1895
2271
|
destroy(): void;
|
|
1896
2272
|
}
|
|
1897
|
-
export type { MapAppearance, MapCamera, MapCapabilities, MapConfiguration, MapConfigurationSnapshot, MapController, MapControls, MapEvents, MapFeatureReference, MapOptions, MapSelectionChange,
|
|
2273
|
+
export type { MapAppearance, MapCamera, MapCapabilities, MapConfiguration, MapConfigurationGroup, MapConfigurationSnapshot, MapController, MapControls, MapEvents, MapFeatureReference, MapOptions, MapSelectionChange, ResolvedMapConfiguration, } from './controller-types.js';
|
|
1898
2274
|
```
|
|
1899
2275
|
|
|
1900
2276
|
### dist/src/explorer-data.d.ts
|
|
@@ -2023,11 +2399,16 @@ export declare function createSFMapWithData(options: SFMapOptions, data: SFMapDa
|
|
|
2023
2399
|
### dist/src/geometry.d.ts
|
|
2024
2400
|
|
|
2025
2401
|
```ts
|
|
2026
|
-
import type { Geometry, Position } from '../data/types.js';
|
|
2402
|
+
import type { Bounds, Geometry, Position } from '../data/types.js';
|
|
2403
|
+
export type { Bounds, Geometry, Position } from '../data/types.js';
|
|
2027
2404
|
export type Project = (position: Position) => [number, number];
|
|
2028
2405
|
export declare function rawProject([longitude, latitude]: Position): [number, number];
|
|
2029
2406
|
export declare function positions(geometry: Geometry | null | undefined): Position[];
|
|
2030
2407
|
export declare function geometryPath(geometry: Geometry | null | undefined, project: Project): string;
|
|
2408
|
+
/** Geographic [west, south, east, north], or null for empty geometry. */
|
|
2409
|
+
export declare function geometryBounds(geometry: Geometry | null | undefined): Bounds | null;
|
|
2410
|
+
/** Polygon hit testing in longitude/latitude; boundaries (including hole edges) count as hits. */
|
|
2411
|
+
export declare function geometryContainsPoint(geometry: Geometry | null | undefined, point: Position): boolean;
|
|
2031
2412
|
```
|
|
2032
2413
|
|
|
2033
2414
|
### dist/src/static.d.ts
|
|
@@ -2043,14 +2424,14 @@ export type { DistrictStyle, DistrictYear, SFMapOptions as StaticMapOptions, Sta
|
|
|
2043
2424
|
/** Server-safe SVG rendering with explicit data; returns SVG plus projection helpers. */
|
|
2044
2425
|
export declare function renderMap(data: StaticMapInput, options?: SFMapOptions): {
|
|
2045
2426
|
svg: string;
|
|
2046
|
-
project: (coordinates: import("
|
|
2427
|
+
project: (coordinates: import("./geometry.js").Position) => [number, number];
|
|
2047
2428
|
viewBox: [number, number, number, number];
|
|
2048
2429
|
};
|
|
2049
2430
|
/** Project canonical layer geometry without constructing an SVG string. */
|
|
2050
2431
|
export declare function getLayerPaths(data: StaticMapInput, options?: SFMapOptions): {
|
|
2051
2432
|
year: import("./types.js").DistrictYear;
|
|
2052
2433
|
viewBox: [number, number, number, number];
|
|
2053
|
-
project: (coordinates: import("
|
|
2434
|
+
project: (coordinates: import("./geometry.js").Position) => [number, number];
|
|
2054
2435
|
coast: string;
|
|
2055
2436
|
districts: {
|
|
2056
2437
|
id: number;
|
|
@@ -2265,7 +2646,7 @@ import type { SFMapOptions } from './types.js';
|
|
|
2265
2646
|
/** Server-safe overview using exactly the guide browser geography. */
|
|
2266
2647
|
export declare function createGuideSVG(options?: SFMapOptions): {
|
|
2267
2648
|
svg: string;
|
|
2268
|
-
project: (coordinates: import("
|
|
2649
|
+
project: (coordinates: import("./geometry.js").Position) => [number, number];
|
|
2269
2650
|
viewBox: [number, number, number, number];
|
|
2270
2651
|
};
|
|
2271
2652
|
/** Stable compact frame, progressively enhanced with mountGuideMap(). No browser globals. */
|
|
@@ -2278,4 +2659,78 @@ export declare function createGuideShell(options?: SFMapOptions): string;
|
|
|
2278
2659
|
import type { MapOptions } from './controller-types.js';
|
|
2279
2660
|
/** Configuration only: pair with guide/data or a compatible caller-supplied dataset. */
|
|
2280
2661
|
export declare const guideOptions: Readonly<MapOptions>;
|
|
2662
|
+
export { type MapPalette, mapPalettes } from './palettes.js';
|
|
2663
|
+
```
|
|
2664
|
+
|
|
2665
|
+
### dist/src/palettes.d.ts
|
|
2666
|
+
|
|
2667
|
+
```ts
|
|
2668
|
+
import type { SFMapOptions } from './types.js';
|
|
2669
|
+
/** Shared tokens for static SVGs, interactive maps, and consumer-owned place cards. */
|
|
2670
|
+
export interface MapPalette {
|
|
2671
|
+
readonly colors: NonNullable<SFMapOptions['colors']>;
|
|
2672
|
+
readonly haloColor: string;
|
|
2673
|
+
readonly markerColor: string;
|
|
2674
|
+
readonly selectedMarkerColor: string;
|
|
2675
|
+
readonly selectedMarkerRingColor: string;
|
|
2676
|
+
}
|
|
2677
|
+
/** Opt-in editorial palettes inspired by YorkSF. Existing renderer themes stay unchanged. */
|
|
2678
|
+
export declare const mapPalettes: {
|
|
2679
|
+
nightForest: {
|
|
2680
|
+
colors: {
|
|
2681
|
+
water: string;
|
|
2682
|
+
land: string;
|
|
2683
|
+
park: string;
|
|
2684
|
+
neighborhood: string;
|
|
2685
|
+
district: string;
|
|
2686
|
+
road: string;
|
|
2687
|
+
highway: string;
|
|
2688
|
+
landmark: string;
|
|
2689
|
+
bart: string;
|
|
2690
|
+
label: string;
|
|
2691
|
+
};
|
|
2692
|
+
haloColor: string;
|
|
2693
|
+
markerColor: string;
|
|
2694
|
+
selectedMarkerColor: string;
|
|
2695
|
+
selectedMarkerRingColor: string;
|
|
2696
|
+
};
|
|
2697
|
+
warmPaper: {
|
|
2698
|
+
colors: {
|
|
2699
|
+
water: string;
|
|
2700
|
+
land: string;
|
|
2701
|
+
park: string;
|
|
2702
|
+
neighborhood: string;
|
|
2703
|
+
district: string;
|
|
2704
|
+
road: string;
|
|
2705
|
+
highway: string;
|
|
2706
|
+
landmark: string;
|
|
2707
|
+
bart: string;
|
|
2708
|
+
label: string;
|
|
2709
|
+
};
|
|
2710
|
+
haloColor: string;
|
|
2711
|
+
markerColor: string;
|
|
2712
|
+
selectedMarkerColor: string;
|
|
2713
|
+
selectedMarkerRingColor: string;
|
|
2714
|
+
};
|
|
2715
|
+
};
|
|
2716
|
+
```
|
|
2717
|
+
|
|
2718
|
+
### ./dist/src/cards.d.ts
|
|
2719
|
+
|
|
2720
|
+
```ts
|
|
2721
|
+
import type { MapController } from './controller-types.js';
|
|
2722
|
+
import type { MapMarker } from './types.js';
|
|
2723
|
+
export interface MapCardsOptions {
|
|
2724
|
+
markers: readonly MapMarker[];
|
|
2725
|
+
highlightColor?: string;
|
|
2726
|
+
/** Reveal selected cards with native scrolling, without moving keyboard focus. Default true. */
|
|
2727
|
+
scrollOnSelect?: boolean;
|
|
2728
|
+
}
|
|
2729
|
+
/** Connect consumer-owned [data-map-marker] cards and [data-map-select] buttons to a map. */
|
|
2730
|
+
export declare function bindMapCards(map: MapController, root: HTMLElement, options: MapCardsOptions): {
|
|
2731
|
+
/** Call after map.setMarkers() when your place catalog changes. */
|
|
2732
|
+
setMarkers(next: readonly MapMarker[]): void;
|
|
2733
|
+
/** Remove owned listeners/decoration and restore card attributes. Call before map.destroy(). */
|
|
2734
|
+
destroy(): void;
|
|
2735
|
+
};
|
|
2281
2736
|
```
|