@kahwee/sf-map-svg 4.0.0 → 4.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +28 -1
  2. package/README.md +7 -9
  3. package/dist/data/guide/coast.json +1 -1
  4. package/dist/data/guide/landmarks.json +1 -1
  5. package/dist/data/guide/neighborhoods-realtor.json +1 -1
  6. package/dist/data/stations.d.ts +2 -2
  7. package/dist/data/types.d.ts +8 -1
  8. package/dist/src/api.d.ts +1 -1
  9. package/dist/src/api.js +1 -1
  10. package/dist/src/configuration.js +18 -2
  11. package/dist/src/controller-types.d.ts +1 -0
  12. package/dist/src/district-morph.d.ts +17 -0
  13. package/dist/src/district-morph.js +148 -0
  14. package/dist/src/explorer-core.js +143 -46
  15. package/dist/src/features.d.ts +6 -0
  16. package/dist/src/features.js +11 -1
  17. package/dist/src/frame-scheduler.d.ts +11 -0
  18. package/dist/src/frame-scheduler.js +64 -0
  19. package/dist/src/layer-transition.d.ts +18 -0
  20. package/dist/src/layer-transition.js +102 -0
  21. package/dist/src/layers.d.ts +9 -7
  22. package/dist/src/layers.js +28 -18
  23. package/dist/src/map-core.js +7 -4
  24. package/dist/src/marker-layer.d.ts +3 -0
  25. package/dist/src/marker-layer.js +27 -5
  26. package/dist/src/overlays.d.ts +3 -4
  27. package/dist/src/overlays.js +5 -1
  28. package/dist/src/static-animation.d.ts +13 -0
  29. package/dist/src/static-animation.js +36 -0
  30. package/dist/src/transit.js +2 -1
  31. package/dist/src/types.d.ts +26 -0
  32. package/dist/src/validation.js +2 -0
  33. package/docs/API.md +33 -0
  34. package/docs/api-audit.md +2 -0
  35. package/docs/consumer-integration.md +1 -1
  36. package/docs/developer-guide.md +137 -0
  37. package/docs/guide-bundle-report.md +4 -4
  38. package/llms-full.txt +1844 -0
  39. package/llms.txt +25 -0
  40. package/package.json +10 -4
@@ -55,6 +55,16 @@ export interface SFMapOptions {
55
55
  overlays?: readonly MapOverlay[];
56
56
  title?: string;
57
57
  idPrefix?: string;
58
+ /**
59
+ * Opt-in, self-contained CSS choreography for static SVGs: land fades in, the coast and
60
+ * lines draw, districts grow, then labels and points appear. It plays when the SVG is
61
+ * inserted into a page or loaded as an image, and stays still under reduced motion.
62
+ * `duration` (default 2400 ms) scales the whole sequence; `delay` offsets it.
63
+ */
64
+ animation?: boolean | {
65
+ duration?: number;
66
+ delay?: number;
67
+ };
58
68
  colors?: Partial<Record<'water' | 'land' | 'district' | 'neighborhood' | 'highway' | 'road' | 'park' | 'landmark' | 'bart' | 'label' | 'marker' | 'selected', string>>;
59
69
  }
60
70
  /** Serializable viewport in the interactive map's fixed 800 × 800 projected space. */
@@ -104,6 +114,21 @@ export interface MapFeatures {
104
114
  };
105
115
  northArrow?: boolean;
106
116
  scaleBar?: boolean;
117
+ /**
118
+ * Fade layers as `setLayers` and `setMode` switch them, crossfade neighborhood boundaries
119
+ * when `setSource` changes the definition, and crossfade district fills on
120
+ * `setDistrictStyle`. Reduced motion takes precedence.
121
+ */
122
+ layerTransitions?: boolean | {
123
+ duration?: number;
124
+ };
125
+ /**
126
+ * Morph district outlines from one map year to the next on `setDistrictYear`; pass
127
+ * `{ animate: false }` for an instant change. Reduced motion takes precedence.
128
+ */
129
+ districtMorph?: boolean | {
130
+ duration?: number;
131
+ };
107
132
  }
108
133
  export interface NeighborhoodExplorerOptions extends MapFeatures {
109
134
  mode?: ExplorerMode;
@@ -207,6 +232,7 @@ export interface NeighborhoodExplorerElement extends HTMLElement {
207
232
  fitGeometry(geometry: Geometry, padding?: number | MapPadding, options?: CameraOptions): void;
208
233
  /** Explicitly engage map touch gestures; false restores page gestures. */
209
234
  setTouchNavigation(enabled: boolean): void;
235
+ /** Reconcile stable IDs; retained markers preserve nodes, focus, and entrance animations. */
210
236
  setMarkers(markers: readonly MapMarker[]): void;
211
237
  setOverlays(overlays: readonly MapOverlay[]): void;
212
238
  selectMarker(id: string | null, options?: {
@@ -42,6 +42,8 @@ const optionKeys = [
42
42
  'attribution',
43
43
  'northArrow',
44
44
  'scaleBar',
45
+ 'layerTransitions',
46
+ 'districtMorph',
45
47
  'interface',
46
48
  'layers',
47
49
  'selectableNeighborhoods',
package/docs/API.md ADDED
@@ -0,0 +1,33 @@
1
+ # Map options and lifecycle
2
+
3
+ Start with the [static and interactive examples](../README.md). Exact option
4
+ and event types are exported by [src/api.ts](../src/api.ts) and defined in
5
+ [src/types.ts](../src/types.ts) and [src/controller-types.ts](../src/controller-types.ts).
6
+
7
+ ## Static maps
8
+
9
+ Static options include `theme`, `width`, `height`, `padding`, `year` (2002, 2012, 2022), `districtLines`, `districtFills`, `districtStyle`, `districtLabels`, `neighborhoodLines`, `labels`, `highways`, `keyRoads`, `roadLabels`, `landmarks`, `bartStations`, `markers`, `overlays`, `title`, `idPrefix`, `colors`, and `animation`. Each optional layer is independent. User-supplied text and attributes are escaped in SVG output.
10
+
11
+ Custom district IDs must be finite numbers. An empty `labelPoints` array uses the district's primary `label` position.
12
+
13
+ `animation: true` (or `{ duration, delay }`) makes a static map draw itself with self-contained, scoped CSS: land fades in, the coast and lines draw, districts grow, then labels and points appear. It plays when the SVG is inserted into a page or loaded as an image, needs no JavaScript, and stays still under `prefers-reduced-motion`. Re-insert the markup to replay it. Default output is unchanged.
14
+
15
+ ## Interactive maps
16
+
17
+ `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](developer-guide.md) compares static and interactive defaults and explains configuration resets, coordinates, and cleanup.
18
+
19
+ Construction options include `mode` (`basemap`, `neighborhoods`, or `districts`), `source`, `neighborhood`, `year`, `labels`, `layers`, `controls`, `features`, `appearance`, `markers`, `overlays`, `legend`, `strings`, `attribution`, and `fitPadding`. Enable map touch gestures with the touch control or `map.setTouchNavigation(true)`. `features` holds motion, marker entrances, selected marker rings, clustering, north arrow, scale bar, layer transitions, and district morphs. `appearance` holds theme, color tokens, label and area styles, marker colors and sizes, and district styling. `layers` controls district fill/line/labels, neighborhood lines/labels, landmarks, BART, highways, key roads, and road labels. See the exported `MapOptions` type for exact values and the [Storybook examples](../stories/) for live controls.
20
+
21
+ The controller also supports marker, neighborhood, and district selection; district year and style changes; source and mode changes; labels and touch navigation; screen projection; and typed `markerchange`, `neighborhoodchange`, `districtchange`, `districthover`, `districtactivate`, `districtyearchange`, `overlayactivate`, `clusteractivate`, `viewportchange`, and `mapresize` events. `destroy()` releases browser resources; operations after destruction throw.
22
+
23
+ If a district callback destroys the map or selects a newer district, the interrupted activation stops without moving keyboard focus or sending a stale event.
24
+
25
+ `setMarkers()` reconciles by stable marker `id`: retained markers keep their DOM nodes, focus, and in-progress entrance animations; only new IDs animate in. Identical ordered marker updates are a visual no-op. Camera state and `viewportchange` events remain synchronous, while animated camera steps and their dependent label/marker layout commit in the same browser frame.
26
+
27
+ For a small guide, import `guideMapData` from `/guide/data` and pass it to `createMap`. The optional `/guide` entry also provides `createGuideMap`, `mountGuideMap`, and detailed-data loading for existing guide layouts. `/transit` provides the standalone schematic transit animation. See [examples](EXAMPLES.md) and [consumer integration](consumer-integration.md).
28
+
29
+ For the guide preset with the same controller API, use `createGuideController(options)` from `/guide` (or `/guide/map`). It accepts grouped `MapOptions` and returns `MapController`; `mountGuideController(shell, options)` enhances a `createGuideShell()` container. Both use the lightweight guide geography. Existing `createGuideMap` and `mountGuideMap` calls retain their element-based API.
30
+
31
+ Two opt-in features animate the map's lines and layers. `features.layerTransitions` fades layers as `configure({ layers })` and `setMode()` switch them, crossfades neighborhood boundaries when `setSource()` changes the definition, and crossfades district fills on `setDistrictStyle()`. `features.districtMorph` moves district outlines from one map year to the next on `setDistrictYear()`, then settles on the exact published geometry; pass `{ animate: false }` for an instant change. Both default off, own and cancel their animations, and yield to reduced motion.
32
+
33
+ District style callbacks are evaluated once per district at construction and on style or year updates. Hover, selection, and layer toggles reuse the prepared styles; call `setDistrictStyle()` again when external styling data changes. Invalid styles leave the current map unchanged. District fades respect reduced motion and are removed on interruption or destruction.
package/docs/api-audit.md CHANGED
@@ -24,6 +24,8 @@ unbounded geographic data can never fail.
24
24
  | District style callbacks | Prepare and copy every style before committing. Hover/selection reuse the results; call `setDistrictStyle()` to refresh changed external data. Reentrant updates or destruction supersede pending work. |
25
25
  | Rapid district year changes | Both outgoing layers are tracked, inert, and removed on interruption, completion, reduced-motion changes, or destruction. Zero duration creates no transition copies. |
26
26
  | Label metrics and camera movement | Reuse screen-space text measurements and nodes across frames. Font loading invalidates measurements; hidden or removed candidates do not remain in the visible layer. |
27
+ | Marker replacement | Reconcile stable IDs, preserve retained nodes/picker options and entrance animations, cancel removed entrances, and restore focus after reordering. Identical ordered values do not mutate the DOM or restart work. |
28
+ | Camera and label scheduling | A shared frame advances the camera before rendering dependent labels, marker sizes, and clusters. State/events remain synchronous; cancellation preserves pending layout, and destruction cancels both kinds of work. |
27
29
  | Guide controllers and compatibility factories | `createGuideController`/`mountGuideController` share grouped options and lifecycle with `createMap`. Existing element factories retain flat options and their return types. |
28
30
  | Layer hidden and bundle cost | Visibility never unloads imported geography. Use narrow entrypoints or data injection to save bytes. |
29
31
 
@@ -1,6 +1,6 @@
1
1
  # Consumer integration
2
2
 
3
- Use the version 3 root controller and explicit geographic data. The [migration guide](migration-v3.md) maps removed imports to supported calls.
3
+ Use the root controller and import geographic data explicitly. The [migration guide](migration-v3.md) maps removed v2 imports to supported calls.
4
4
 
5
5
  For a compact browser guide:
6
6
 
@@ -0,0 +1,137 @@
1
+ # Developer guide
2
+
3
+ Choose the renderer and geography separately. `renderMap` produces a string without a DOM; `createMap` produces a browser controller whose `.element` you mount. Neither root function imports geography for you.
4
+
5
+ ## Choose an entry point
6
+
7
+ | Task | Renderer | Data |
8
+ | --- | --- | --- |
9
+ | Generate an SVG file or server-rendered image | `/static`: `renderMap` | `/data/static`: `staticMapData` |
10
+ | Explore districts or several neighborhood definitions | `/map`: `createMap` | `/data/full`: `fullMapData` |
11
+ | Embed a smaller neighborhood guide | `/guide/map`: `createGuideController` | Included lightweight guide geography |
12
+ | Use the guide data with the general controller | `/map`: `createMap` | `/guide/data`: `guideMapData` |
13
+ | Look up names without mounting a map | `/data/lookup` | Source-aware lookup helpers |
14
+
15
+ 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.
16
+
17
+ ## Write a static SVG
18
+
19
+ ```ts
20
+ import { writeFile } from 'node:fs/promises';
21
+ import { renderMap } from '@kahwee/sf-map-svg/static';
22
+ import { staticMapData } from '@kahwee/sf-map-svg/data/static';
23
+
24
+ const { svg } = renderMap(staticMapData, {
25
+ year: 2022,
26
+ landmarks: true,
27
+ bartStations: true,
28
+ title: 'San Francisco districts, 2022',
29
+ });
30
+ await writeFile('san-francisco.svg', svg);
31
+ ```
32
+
33
+ The SVG is self-contained. `project([lng, lat])` returns SVG coordinates; `viewBox` is `[0, 0, width, height]`. Use a different `idPrefix` for each inline SVG on the same page, or let the renderer generate one.
34
+
35
+ ## Mount and dispose an interactive map
36
+
37
+ Provide a host such as `<div id="map"></div>` before running this code. Run browser construction after mounting, not during server rendering.
38
+
39
+ ```ts
40
+ import { createMap } from '@kahwee/sf-map-svg/map';
41
+ import { fullMapData } from '@kahwee/sf-map-svg/data/full';
42
+
43
+ const host = document.querySelector<HTMLElement>('#map');
44
+ if (!host) throw new Error('Missing #map host');
45
+
46
+ const map = createMap(fullMapData, {
47
+ mode: 'neighborhoods',
48
+ source: 'realtor',
49
+ layers: { bartStations: true },
50
+ features: { motion: true },
51
+ appearance: { theme: 'districts' },
52
+ });
53
+ host.append(map.element);
54
+ const unsubscribe = map.on('neighborhoodchange', ({ name }) => {
55
+ // name is null when the selection is cleared.
56
+ console.log(name);
57
+ });
58
+
59
+ // Call from your framework's unmount hook or when removing the view.
60
+ function disposeMap() {
61
+ unsubscribe();
62
+ map.destroy();
63
+ map.element.remove();
64
+ }
65
+ ```
66
+
67
+ `destroy()` is idempotent, cancels owned work and subscriptions, and leaves host-owned DOM in place. Other controller operations after destruction throw. In React, construct inside an effect and return cleanup; in Vue or Svelte, use their mount/unmount hooks. Do not create a new map on every render: update the existing controller instead.
68
+
69
+ ## Know the defaults
70
+
71
+ | Setting | `renderMap` | `createMap` |
72
+ | --- | --- | --- |
73
+ | Mode | No mode option | `basemap`; set `neighborhoods` or `districts` explicitly |
74
+ | Theme | `districts` | `transit`; use `appearance.theme` |
75
+ | District layers | On | Follow `districts` mode |
76
+ | Neighborhood layers | Lines off | Follow `neighborhoods` mode |
77
+ | Parks, highways, key roads | Off | On when supplied |
78
+ | Road labels | Follow `keyRoads` | On when supplied, subject to zoom/collisions |
79
+ | BART stations | Off | On when supplied |
80
+ | Visible labels | On | On, subject to zoom/collisions |
81
+ | Motion features | Static animation off | Off; opt in via `features` |
82
+
83
+ A layer switch does not download missing geography. `fullMapData` has `.map` for static rendering and separate lookup collections for interactive use. `staticMapData` is already a static data object. Use `renderMap(guideMapData.map, options)`, not `renderMap(guideMapData, options)`.
84
+
85
+ SFAR realtor neighborhoods are the default when supplied. If only an alternative collection is supplied, the controller selects that available source. Choose `source` explicitly when comparing definitions. Sources retain separate identities; they are not interchangeable polygons.
86
+
87
+ ## Construction settings versus runtime updates
88
+
89
+ | Change | Call |
90
+ | --- | --- |
91
+ | Layers, controls, behaviors | `map.configure({ layers, controls, features })` |
92
+ | Map mode or neighborhood source | `map.setMode(mode)`, `map.setSource(source)` |
93
+ | District boundaries or choropleth | `map.setDistrictYear(year)`, `map.setDistrictStyle(callback)` |
94
+ | Master text visibility | `map.setLabels(visible)` |
95
+ | Markers or route overlays | `map.setMarkers(items)`, `map.setOverlays(items)` |
96
+ | Camera | `map.camera.set/pan/zoom/fit/reset(...)` |
97
+ | Palette, typography, control styling | Construction-only `appearance`; recreate the map to change it |
98
+
99
+ `configure` merges supplied keys; omitted keys keep their existing values. Set a layer or control key to `undefined` to remove its override. Set a whole group to `undefined` to reset that group. Set a feature to `false` to disable it. Explicit layer overrides keep winning after mode changes.
100
+
101
+ ```ts
102
+ map.configure({ layers: { neighborhoodLabels: false } });
103
+ map.configure({ layers: { neighborhoodLabels: undefined } }); // follow mode again
104
+ map.configure({ features: { motion: false } });
105
+ map.configure({ layers: undefined }); // clear all explicit layer overrides
106
+ ```
107
+
108
+ `getConfiguration()` returns a detached snapshot of these groups, including explicit layer overrides. It is not a full serialization of mode, source, year, camera, selection, or construction appearance. Invalid configuration patches leave the current map unchanged.
109
+
110
+ ## Markers, routes and camera coordinates
111
+
112
+ Marker positions are `{ lng, lat }`. 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` uses projected units. `projectToScreen(lng, lat)` returns pixels relative to the map canvas, plus visibility.
113
+
114
+ 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.
115
+
116
+ Reduced motion takes precedence over animations. Map touch gestures are opt-in; hiding a chooser requires an accessible alternative in your application.
117
+
118
+ ## Common integration errors
119
+
120
+ | Symptom | Check |
121
+ | --- | --- |
122
+ | No neighborhoods or district fills | Set `mode` explicitly; the default is `basemap` |
123
+ | An enabled layer does not appear | Supply its dataset; check master labels, zoom and collision filtering |
124
+ | A theme or motion option throws | Put styling in `appearance` and behaviors in `features`; flat interactive options are rejected |
125
+ | Source switch throws | Supply that source collection, or use `/data/full` |
126
+ | A geographic coordinate produces a surprising camera view | Use `camera.fit(geometry)`; `camera.set` takes projected coordinates |
127
+ | An event handler breaks after clearing selection | Handle null `id`, `name`, `marker`, or `district` |
128
+ | Updates throw after navigating away | Dispose once and stop calling the destroyed controller |
129
+ | An import from `/legacy`, `/custom-map`, `/explorer`, `/interactive` or `/interactive-data` fails | Those entries were removed in v3; follow the migration guide |
130
+
131
+ ## Versions and assistant-readable docs
132
+
133
+ The package declarations are the exact type contract for your installed version. Check the changelog before using features shown on a local preview: its Unreleased section can include functionality absent from npm.
134
+
135
+ The generated `llms.txt` index links to Markdown documentation; `llms-full.txt` combines the guides and type contracts into one file. Both are built from the same package selected for the site. Local previews are labeled as working-tree docs. Released builds use the installed npm package's docs and declarations.
136
+
137
+ See [API options](API.md), [worked examples](EXAMPLES.md), [consumer integration](consumer-integration.md), and [v3 migration](migration-v3.md).
@@ -1,12 +1,12 @@
1
1
  # Guide bundle size report
2
2
 
3
- Generated 2026-09-28 by `pnpm report:guide` with Vite production minification and gzip compression. Each emitted JS chunk is compressed independently. The report measures the explicit-data v3 root and the optional `@kahwee/sf-map-svg/guide` preset.
3
+ Generated 2026-10-01 by `pnpm report:guide` with Vite production minification and gzip compression. Each emitted JS chunk is compressed independently. The report measures the explicit-data root and the optional `@kahwee/sf-map-svg/guide` preset.
4
4
 
5
5
  | Entry | Initial JS, raw | Initial JS, gzip | Explicit detail JS, gzip |
6
6
  | --- | ---: | ---: | ---: |
7
- | v3 root (explicit data) | 87.4 KB | 25.5 KB | — |
8
- | v3 static renderer | 15.3 KB | 5.0 KB | — |
9
- | Guide preset | 477.1 KB | 114.1 KB | 473.7 KB |
7
+ | Renderer root (explicit data) | 99.3 KB | 29.1 KB | — |
8
+ | Static renderer | 18.2 KB | 5.8 KB | — |
9
+ | Guide preset | 489.1 KB | 116.8 KB | 467.0 KB |
10
10
 
11
11
  **500 KB target:** met.
12
12