@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.
- package/CHANGELOG.md +28 -1
- package/README.md +7 -9
- package/dist/data/guide/coast.json +1 -1
- package/dist/data/guide/landmarks.json +1 -1
- package/dist/data/guide/neighborhoods-realtor.json +1 -1
- package/dist/data/stations.d.ts +2 -2
- package/dist/data/types.d.ts +8 -1
- package/dist/src/api.d.ts +1 -1
- package/dist/src/api.js +1 -1
- package/dist/src/configuration.js +18 -2
- package/dist/src/controller-types.d.ts +1 -0
- package/dist/src/district-morph.d.ts +17 -0
- package/dist/src/district-morph.js +148 -0
- package/dist/src/explorer-core.js +143 -46
- package/dist/src/features.d.ts +6 -0
- package/dist/src/features.js +11 -1
- package/dist/src/frame-scheduler.d.ts +11 -0
- package/dist/src/frame-scheduler.js +64 -0
- package/dist/src/layer-transition.d.ts +18 -0
- package/dist/src/layer-transition.js +102 -0
- package/dist/src/layers.d.ts +9 -7
- package/dist/src/layers.js +28 -18
- package/dist/src/map-core.js +7 -4
- package/dist/src/marker-layer.d.ts +3 -0
- package/dist/src/marker-layer.js +27 -5
- package/dist/src/overlays.d.ts +3 -4
- package/dist/src/overlays.js +5 -1
- package/dist/src/static-animation.d.ts +13 -0
- package/dist/src/static-animation.js +36 -0
- package/dist/src/transit.js +2 -1
- package/dist/src/types.d.ts +26 -0
- package/dist/src/validation.js +2 -0
- package/docs/API.md +33 -0
- package/docs/api-audit.md +2 -0
- package/docs/consumer-integration.md +1 -1
- package/docs/developer-guide.md +137 -0
- package/docs/guide-bundle-report.md +4 -4
- package/llms-full.txt +1844 -0
- package/llms.txt +25 -0
- package/package.json +10 -4
package/llms-full.txt
ADDED
|
@@ -0,0 +1,1844 @@
|
|
|
1
|
+
# SF Map SVG
|
|
2
|
+
|
|
3
|
+
> Offline San Francisco SVG maps with explicit geographic imports, no runtime dependencies, static rendering, and a browser controller.
|
|
4
|
+
|
|
5
|
+
@kahwee/sf-map-svg 4.1.0 · local working tree. Node >=24.0.0 for server rendering; browsers need a DOM and a JSON-capable bundler.
|
|
6
|
+
|
|
7
|
+
These are working-tree docs. Unreleased changelog entries may describe APIs absent from npm; verify the installed version before copying them.
|
|
8
|
+
|
|
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
|
+
|
|
11
|
+
<!-- Generated from README.md; @kahwee/sf-map-svg 4.1.0 · local working tree. -->
|
|
12
|
+
|
|
13
|
+
# SF Map SVG
|
|
14
|
+
|
|
15
|
+
Offline, self-contained San Francisco SVG maps. The package has no runtime dependencies. Geography is always an explicit import; the root entry does not bundle data.
|
|
16
|
+
|
|
17
|
+
[Live examples](https://kahwee.github.io/sf-map-svg/examples.html) · [Storybook source](https://github.com/kahwee/sf-map-svg/blob/main/stories/) · [Geographic sources](https://github.com/kahwee/sf-map-svg/blob/main/SOURCES.md) · [v3 migration](https://github.com/kahwee/sf-map-svg/blob/main/docs/migration-v3.md)
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
pnpm add @kahwee/sf-map-svg
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Node 24+ is required for server rendering. Browser maps need a DOM and a bundler that supports JSON imports.
|
|
26
|
+
|
|
27
|
+
Upgrading from v3 to v4: update your Node runtime to 24 or newer. The map API, browser requirements, rendering, and animations are unchanged.
|
|
28
|
+
|
|
29
|
+
## Static SVG
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { renderMap } from '@kahwee/sf-map-svg';
|
|
33
|
+
import { staticMapData } from '@kahwee/sf-map-svg/data/static';
|
|
34
|
+
|
|
35
|
+
const { svg, project, viewBox } = renderMap(staticMapData, {
|
|
36
|
+
year: 2022,
|
|
37
|
+
landmarks: true,
|
|
38
|
+
bartStations: true,
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`renderMap(data, options)` returns SVG markup and matching projection helpers. `/data/static` includes the complete static map without interactive lookup collections. Small maps can compose selected JSON exports from `/data/*` and pass them as `StaticMapData`. `getLayerPaths(data, options)` returns fitted geographic paths without SVG markup.
|
|
43
|
+
|
|
44
|
+
See [map options and animations](https://github.com/kahwee/sf-map-svg/blob/main/docs/API.md#static-maps) for optional layers and static animation.
|
|
45
|
+
|
|
46
|
+
## Interactive map
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { createMap } from '@kahwee/sf-map-svg';
|
|
50
|
+
import { fullMapData } from '@kahwee/sf-map-svg/data/full';
|
|
51
|
+
|
|
52
|
+
const map = createMap(fullMapData, {
|
|
53
|
+
mode: 'neighborhoods',
|
|
54
|
+
source: 'realtor',
|
|
55
|
+
neighborhood: 'Inner Mission',
|
|
56
|
+
layers: { bartStations: true },
|
|
57
|
+
features: { motion: true },
|
|
58
|
+
appearance: { theme: 'districts' },
|
|
59
|
+
});
|
|
60
|
+
document.querySelector('#map')?.append(map.element);
|
|
61
|
+
map.on('neighborhoodchange', ({ name }) => console.log(name));
|
|
62
|
+
map.camera.zoom(1.5);
|
|
63
|
+
// When the view is removed:
|
|
64
|
+
map.destroy();
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`createMap(data, options)` returns a controller. Use `map.element` for mounting, `map.configure({ features, layers, controls })` for runtime switches, `map.camera` for pan/zoom/fit/reset, and `map.on()` for typed events. Appearance is set at construction. 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.
|
|
68
|
+
|
|
69
|
+
See [controller options, events, and lifecycle](https://github.com/kahwee/sf-map-svg/blob/main/docs/API.md#interactive-maps),
|
|
70
|
+
[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).
|
|
71
|
+
Call `destroy()` when removing an interactive map.
|
|
72
|
+
|
|
73
|
+
## Documentation for coding assistants
|
|
74
|
+
|
|
75
|
+
[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.
|
|
76
|
+
|
|
77
|
+
## Data and development
|
|
78
|
+
|
|
79
|
+
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.
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
pnpm install --frozen-lockfile
|
|
83
|
+
pnpm check
|
|
84
|
+
pnpm demo
|
|
85
|
+
pnpm build-storybook
|
|
86
|
+
pnpm test:stories:coverage
|
|
87
|
+
pnpm test:package
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The [contributing guide](https://github.com/kahwee/sf-map-svg/blob/main/CONTRIBUTING.md) explains the geographic and release checks. If you are upgrading from v2, follow the [v3 migration guide](https://github.com/kahwee/sf-map-svg/blob/main/docs/migration-v3.md).
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
<!-- Generated from docs/developer-guide.md; @kahwee/sf-map-svg 4.1.0 · local working tree. -->
|
|
95
|
+
|
|
96
|
+
# Developer guide
|
|
97
|
+
|
|
98
|
+
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.
|
|
99
|
+
|
|
100
|
+
## Choose an entry point
|
|
101
|
+
|
|
102
|
+
| Task | Renderer | Data |
|
|
103
|
+
| --- | --- | --- |
|
|
104
|
+
| Generate an SVG file or server-rendered image | `/static`: `renderMap` | `/data/static`: `staticMapData` |
|
|
105
|
+
| Explore districts or several neighborhood definitions | `/map`: `createMap` | `/data/full`: `fullMapData` |
|
|
106
|
+
| Embed a smaller neighborhood guide | `/guide/map`: `createGuideController` | Included lightweight guide geography |
|
|
107
|
+
| Use the guide data with the general controller | `/map`: `createMap` | `/guide/data`: `guideMapData` |
|
|
108
|
+
| Look up names without mounting a map | `/data/lookup` | Source-aware lookup helpers |
|
|
109
|
+
|
|
110
|
+
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.
|
|
111
|
+
|
|
112
|
+
## Write a static SVG
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
import { writeFile } from 'node:fs/promises';
|
|
116
|
+
import { renderMap } from '@kahwee/sf-map-svg/static';
|
|
117
|
+
import { staticMapData } from '@kahwee/sf-map-svg/data/static';
|
|
118
|
+
|
|
119
|
+
const { svg } = renderMap(staticMapData, {
|
|
120
|
+
year: 2022,
|
|
121
|
+
landmarks: true,
|
|
122
|
+
bartStations: true,
|
|
123
|
+
title: 'San Francisco districts, 2022',
|
|
124
|
+
});
|
|
125
|
+
await writeFile('san-francisco.svg', svg);
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
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.
|
|
129
|
+
|
|
130
|
+
## Mount and dispose an interactive map
|
|
131
|
+
|
|
132
|
+
Provide a host such as `<div id="map"></div>` before running this code. Run browser construction after mounting, not during server rendering.
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { createMap } from '@kahwee/sf-map-svg/map';
|
|
136
|
+
import { fullMapData } from '@kahwee/sf-map-svg/data/full';
|
|
137
|
+
|
|
138
|
+
const host = document.querySelector<HTMLElement>('#map');
|
|
139
|
+
if (!host) throw new Error('Missing #map host');
|
|
140
|
+
|
|
141
|
+
const map = createMap(fullMapData, {
|
|
142
|
+
mode: 'neighborhoods',
|
|
143
|
+
source: 'realtor',
|
|
144
|
+
layers: { bartStations: true },
|
|
145
|
+
features: { motion: true },
|
|
146
|
+
appearance: { theme: 'districts' },
|
|
147
|
+
});
|
|
148
|
+
host.append(map.element);
|
|
149
|
+
const unsubscribe = map.on('neighborhoodchange', ({ name }) => {
|
|
150
|
+
// name is null when the selection is cleared.
|
|
151
|
+
console.log(name);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Call from your framework's unmount hook or when removing the view.
|
|
155
|
+
function disposeMap() {
|
|
156
|
+
unsubscribe();
|
|
157
|
+
map.destroy();
|
|
158
|
+
map.element.remove();
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`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.
|
|
163
|
+
|
|
164
|
+
## Know the defaults
|
|
165
|
+
|
|
166
|
+
| Setting | `renderMap` | `createMap` |
|
|
167
|
+
| --- | --- | --- |
|
|
168
|
+
| Mode | No mode option | `basemap`; set `neighborhoods` or `districts` explicitly |
|
|
169
|
+
| Theme | `districts` | `transit`; use `appearance.theme` |
|
|
170
|
+
| District layers | On | Follow `districts` mode |
|
|
171
|
+
| Neighborhood layers | Lines off | Follow `neighborhoods` mode |
|
|
172
|
+
| Parks, highways, key roads | Off | On when supplied |
|
|
173
|
+
| Road labels | Follow `keyRoads` | On when supplied, subject to zoom/collisions |
|
|
174
|
+
| BART stations | Off | On when supplied |
|
|
175
|
+
| Visible labels | On | On, subject to zoom/collisions |
|
|
176
|
+
| Motion features | Static animation off | Off; opt in via `features` |
|
|
177
|
+
|
|
178
|
+
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)`.
|
|
179
|
+
|
|
180
|
+
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.
|
|
181
|
+
|
|
182
|
+
## Construction settings versus runtime updates
|
|
183
|
+
|
|
184
|
+
| Change | Call |
|
|
185
|
+
| --- | --- |
|
|
186
|
+
| Layers, controls, behaviors | `map.configure({ layers, controls, features })` |
|
|
187
|
+
| Map mode or neighborhood source | `map.setMode(mode)`, `map.setSource(source)` |
|
|
188
|
+
| District boundaries or choropleth | `map.setDistrictYear(year)`, `map.setDistrictStyle(callback)` |
|
|
189
|
+
| Master text visibility | `map.setLabels(visible)` |
|
|
190
|
+
| Markers or route overlays | `map.setMarkers(items)`, `map.setOverlays(items)` |
|
|
191
|
+
| Camera | `map.camera.set/pan/zoom/fit/reset(...)` |
|
|
192
|
+
| Palette, typography, control styling | Construction-only `appearance`; recreate the map to change it |
|
|
193
|
+
|
|
194
|
+
`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.
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
map.configure({ layers: { neighborhoodLabels: false } });
|
|
198
|
+
map.configure({ layers: { neighborhoodLabels: undefined } }); // follow mode again
|
|
199
|
+
map.configure({ features: { motion: false } });
|
|
200
|
+
map.configure({ layers: undefined }); // clear all explicit layer overrides
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
`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.
|
|
204
|
+
|
|
205
|
+
## Markers, routes and camera coordinates
|
|
206
|
+
|
|
207
|
+
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.
|
|
208
|
+
|
|
209
|
+
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.
|
|
210
|
+
|
|
211
|
+
Reduced motion takes precedence over animations. Map touch gestures are opt-in; hiding a chooser requires an accessible alternative in your application.
|
|
212
|
+
|
|
213
|
+
## Common integration errors
|
|
214
|
+
|
|
215
|
+
| Symptom | Check |
|
|
216
|
+
| --- | --- |
|
|
217
|
+
| No neighborhoods or district fills | Set `mode` explicitly; the default is `basemap` |
|
|
218
|
+
| An enabled layer does not appear | Supply its dataset; check master labels, zoom and collision filtering |
|
|
219
|
+
| A theme or motion option throws | Put styling in `appearance` and behaviors in `features`; flat interactive options are rejected |
|
|
220
|
+
| Source switch throws | Supply that source collection, or use `/data/full` |
|
|
221
|
+
| A geographic coordinate produces a surprising camera view | Use `camera.fit(geometry)`; `camera.set` takes projected coordinates |
|
|
222
|
+
| An event handler breaks after clearing selection | Handle null `id`, `name`, `marker`, or `district` |
|
|
223
|
+
| Updates throw after navigating away | Dispose once and stop calling the destroyed controller |
|
|
224
|
+
| An import from `/legacy`, `/custom-map`, `/explorer`, `/interactive` or `/interactive-data` fails | Those entries were removed in v3; follow the migration guide |
|
|
225
|
+
|
|
226
|
+
## Versions and assistant-readable docs
|
|
227
|
+
|
|
228
|
+
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.
|
|
229
|
+
|
|
230
|
+
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.
|
|
231
|
+
|
|
232
|
+
See [API options](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), [consumer integration](https://github.com/kahwee/sf-map-svg/blob/main/docs/consumer-integration.md), and [v3 migration](https://github.com/kahwee/sf-map-svg/blob/main/docs/migration-v3.md).
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
<!-- Generated from docs/API.md; @kahwee/sf-map-svg 4.1.0 · local working tree. -->
|
|
237
|
+
|
|
238
|
+
# Map options and lifecycle
|
|
239
|
+
|
|
240
|
+
Start with the [static and interactive examples](https://github.com/kahwee/sf-map-svg/blob/main/README.md). Exact option
|
|
241
|
+
and event types are exported by [src/api.ts](https://github.com/kahwee/sf-map-svg/blob/main/src/api.ts) and defined in
|
|
242
|
+
[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).
|
|
243
|
+
|
|
244
|
+
## Static maps
|
|
245
|
+
|
|
246
|
+
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.
|
|
247
|
+
|
|
248
|
+
Custom district IDs must be finite numbers. An empty `labelPoints` array uses the district's primary `label` position.
|
|
249
|
+
|
|
250
|
+
`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.
|
|
251
|
+
|
|
252
|
+
## Interactive maps
|
|
253
|
+
|
|
254
|
+
`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.
|
|
255
|
+
|
|
256
|
+
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](https://github.com/kahwee/sf-map-svg/blob/main/stories/) for live controls.
|
|
257
|
+
|
|
258
|
+
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.
|
|
259
|
+
|
|
260
|
+
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.
|
|
261
|
+
|
|
262
|
+
`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.
|
|
263
|
+
|
|
264
|
+
For a small guide, import `guideMapData` from `/guide/data` and pass it to `createMap`. The optional `/guide` entry also provides `createGuideMap`, `mountGuideMap`, and detailed-data loading for existing guide layouts. `/transit` provides the standalone schematic transit animation. See [examples](https://github.com/kahwee/sf-map-svg/blob/main/docs/EXAMPLES.md) and [consumer integration](https://github.com/kahwee/sf-map-svg/blob/main/docs/consumer-integration.md).
|
|
265
|
+
|
|
266
|
+
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.
|
|
267
|
+
|
|
268
|
+
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.
|
|
269
|
+
|
|
270
|
+
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.
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
<!-- Generated from docs/EXAMPLES.md; @kahwee/sf-map-svg 4.1.0 · local working tree. -->
|
|
275
|
+
|
|
276
|
+
# Examples
|
|
277
|
+
|
|
278
|
+
[Live example gallery](https://kahwee.github.io/sf-map-svg/examples.html) · [California propositions](https://kahwee.github.io/sf-map-svg/propositions.html) · [Local measures](https://kahwee.github.io/sf-map-svg/measures.html) · [Source data](https://github.com/kahwee/sf-map-svg/blob/main/SOURCES.md)
|
|
279
|
+
|
|
280
|
+
Choose by task. All snippets use the public package API; browser examples need a DOM and a bundler that supports JSON imports.
|
|
281
|
+
|
|
282
|
+
| I want to… | Start here |
|
|
283
|
+
| --- | --- |
|
|
284
|
+
| Render an SVG on a server | [Static map](#static-svg) |
|
|
285
|
+
| Add a small map to a browser | [Lightweight guide](#lightweight-interactive-guide) |
|
|
286
|
+
| Bundle only selected geography | [Selected data](#selected-geography) |
|
|
287
|
+
| Draw a route over the city | [Route overlay](#route-overlay) |
|
|
288
|
+
| Explore real election votes | [California propositions](#california-propositions-by-sf-district) |
|
|
289
|
+
|
|
290
|
+
## Static SVG
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
import { writeFile } from 'node:fs/promises';
|
|
294
|
+
import { renderMap } from '@kahwee/sf-map-svg';
|
|
295
|
+
import { staticMapData } from '@kahwee/sf-map-svg/data/static';
|
|
296
|
+
|
|
297
|
+
const svg = renderMap(staticMapData, {
|
|
298
|
+
year: 2022,
|
|
299
|
+
landmarks: true,
|
|
300
|
+
bartStations: true,
|
|
301
|
+
idPrefix: 'example',
|
|
302
|
+
}).svg;
|
|
303
|
+
await writeFile('districts.svg', svg);
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
The static preset includes the packaged map layers without interactive lookup collections. For smaller bundles, pass selected data to the root or `/static` renderer. See the [static recipe](https://github.com/kahwee/sf-map-svg/blob/main/README.md#static-svg).
|
|
307
|
+
|
|
308
|
+
For an election choropleth, use `renderMap(data, { year, districtStyle })` or
|
|
309
|
+
`createMap({ map: data, districts: districtMaps, neighborhoods: {} }, options)`.
|
|
310
|
+
The controller exposes `setDistrictYear`, `setDistrictStyle`, `selectDistrict`, and typed
|
|
311
|
+
district events; `getLayerPaths(data, { year })` returns fitted paths without SVG markup.
|
|
312
|
+
See the [Storybook election choropleth](https://github.com/kahwee/sf-map-svg/blob/main/stories/ElectionMap.stories.ts) for a working example.
|
|
313
|
+
|
|
314
|
+
## Lightweight interactive guide
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
import { createGuideController } from '@kahwee/sf-map-svg/guide';
|
|
318
|
+
|
|
319
|
+
const map = createGuideController({ layers: { roadLabels: false } });
|
|
320
|
+
document.querySelector('#map')?.append(map.element);
|
|
321
|
+
// On unmount: map.destroy();
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
The guide includes selected coast, SFAR neighborhoods, parks, roads, and stations. Detailed geography loads only when explicitly requested; see the [consumer guide recipe](https://github.com/kahwee/sf-map-svg/blob/main/docs/consumer-integration.md).
|
|
325
|
+
|
|
326
|
+
## Selected geography
|
|
327
|
+
|
|
328
|
+
```ts
|
|
329
|
+
import { createMap } from '@kahwee/sf-map-svg';
|
|
330
|
+
import coast from '@kahwee/sf-map-svg/data/coast.json' with { type: 'json' };
|
|
331
|
+
import realtor from '@kahwee/sf-map-svg/data/neighborhoods-realtor.json' with { type: 'json' };
|
|
332
|
+
|
|
333
|
+
const map = createMap(
|
|
334
|
+
{
|
|
335
|
+
map: { coast: coast.features[0].geometry },
|
|
336
|
+
neighborhoods: { realtor },
|
|
337
|
+
},
|
|
338
|
+
{ mode: 'neighborhoods', layers: { highways: false, keyRoads: false } },
|
|
339
|
+
);
|
|
340
|
+
document.querySelector('#map')?.append(map.element);
|
|
341
|
+
// On unmount: map.destroy();
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
This imports one neighborhood definition source. To omit its geometry too, import catalog metadata alone from `/data/catalog`.
|
|
345
|
+
|
|
346
|
+
## Route overlay
|
|
347
|
+
|
|
348
|
+
```ts
|
|
349
|
+
import { createGuideController } from '@kahwee/sf-map-svg/guide';
|
|
350
|
+
|
|
351
|
+
const map = createGuideController();
|
|
352
|
+
document.querySelector('#map')?.append(map.element);
|
|
353
|
+
map.setOverlays([{
|
|
354
|
+
id: 'trip',
|
|
355
|
+
label: 'Example route',
|
|
356
|
+
geometry: {
|
|
357
|
+
type: 'LineString',
|
|
358
|
+
coordinates: [[-122.4194, 37.7749], [-122.3981, 37.7936]],
|
|
359
|
+
},
|
|
360
|
+
stroke: '#a85036',
|
|
361
|
+
strokeWidth: 3,
|
|
362
|
+
}]);
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
Coordinates are WGS84 `[longitude, latitude]`. The overlay follows pan and zoom. The [BART journey](https://kahwee.github.io/sf-map-svg/transit.html) is a separate schematic motion example.
|
|
366
|
+
|
|
367
|
+
## California propositions by SF district
|
|
368
|
+
|
|
369
|
+
[Open the interactive explorer](https://kahwee.github.io/sf-map-svg/propositions.html). It uses the public static renderer with only the 2022 district and coast datasets and colors each district from the [certified results JSON](https://github.com/kahwee/sf-map-svg/blob/main/data/propositions/2024-11-05.json). The JSON includes all ten statewide propositions on the November 2024 ballot, with Yes and No counts for each of San Francisco's eleven supervisorial districts. Its scope is SF votes, not statewide totals or voter demographics.
|
|
370
|
+
|
|
371
|
+
The [import script](https://github.com/kahwee/sf-map-svg/blob/main/scripts/import-2024-propositions.py) checks each district sum against the official citywide count. [Geographic and election sources](https://github.com/kahwee/sf-map-svg/blob/main/SOURCES.md) explain the provenance.
|
|
372
|
+
|
|
373
|
+
## Motion and compact guide embeds
|
|
374
|
+
|
|
375
|
+
The runnable `examples/generated/interactive.html` now demonstrates camera motion,
|
|
376
|
+
staggered marker entrances, clustering, selected marker rings, a custom legend,
|
|
377
|
+
compact sources, a north arrow, and a metric scale. Storybook's **Checks / Consumer
|
|
378
|
+
API** includes executable motion, reduced-motion, and progressive-shell checks.
|
|
379
|
+
See [consumer integration](https://github.com/kahwee/sf-map-svg/blob/main/docs/consumer-integration.md) for server and browser recipes.
|
|
380
|
+
|
|
381
|
+
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).
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
<!-- Generated from docs/consumer-integration.md; @kahwee/sf-map-svg 4.1.0 · local working tree. -->
|
|
386
|
+
|
|
387
|
+
# Consumer integration
|
|
388
|
+
|
|
389
|
+
Use the root controller and import geographic data explicitly. The [migration guide](https://github.com/kahwee/sf-map-svg/blob/main/docs/migration-v3.md) maps removed v2 imports to supported calls.
|
|
390
|
+
|
|
391
|
+
For a compact browser guide:
|
|
392
|
+
|
|
393
|
+
```ts
|
|
394
|
+
import { createGuideController } from '@kahwee/sf-map-svg/guide';
|
|
395
|
+
|
|
396
|
+
const map = createGuideController({
|
|
397
|
+
features: { motion: { duration: 400 }, markerEntrance: true },
|
|
398
|
+
appearance: { theme: 'transit' },
|
|
399
|
+
});
|
|
400
|
+
document.querySelector('#map')?.append(map.element);
|
|
401
|
+
// Dispose when the containing view unmounts.
|
|
402
|
+
map.destroy();
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
The guide controller uses the same `configure()`, `camera`, `on()`, and `destroy()` contract as `createMap()`. Motion is opt-in and follows the user's reduced-motion preference. For explicit data composition, `createMap(guideMapData, guideOptions)` remains available using `/guide/data` and `/presets`.
|
|
406
|
+
|
|
407
|
+
To progressively enhance server-rendered `createGuideShell()` markup, use `mountGuideController(shell, options)` from `/guide`. It returns the controller and requires compact attribution. Invalid configuration leaves the static frame intact. Destroy the controller when unmounting. Existing `createGuideMap()` and `mountGuideMap()` return their original augmented elements and accept flat options for compatibility.
|
|
408
|
+
|
|
409
|
+
For server rendering, import `renderMap` from the root and pass `guideMapData.map`, `staticMapData` from `/data/static`, or another explicit `StaticMapData`. Import `/data/full` when interactive lookup collections are also required. All rendering stays offline and has zero runtime dependencies. The guide's detailed geography loads only when `loadGuideDetailedData()` is called.
|
|
410
|
+
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
<!-- Generated from docs/migration-v3.md; @kahwee/sf-map-svg 4.1.0 · local working tree. -->
|
|
414
|
+
|
|
415
|
+
# Migrate to version 3
|
|
416
|
+
|
|
417
|
+
Version 3 removes the deprecated compatibility entry points and their bundled-data factories. This is a breaking major release. Version 2 remains available if you need time to migrate; removed imports fail to resolve in version 3. The root API introduced in version 2 remains the supported API.
|
|
418
|
+
|
|
419
|
+
| Version 2 import or call | Version 3 replacement |
|
|
420
|
+
| --- | --- |
|
|
421
|
+
| `/legacy` `renderSFMap(options)` | `renderMap(staticMapData, options).svg` |
|
|
422
|
+
| `/legacy` `createSFMap(options)` | `renderMap(staticMapData, options)` |
|
|
423
|
+
| `/custom-map` `createSFMapWithData(options, data)` | `renderMap(data, options)` |
|
|
424
|
+
| `/explorer` `createNeighborhoodExplorer(options)` | `createMap(fullMapData, options)`; mount `.element` |
|
|
425
|
+
| `/interactive` `createInteractiveSFMap(options)` | `createMap(fullMapData, { mode: 'basemap', ...options })`; mount `.element` |
|
|
426
|
+
| `/interactive-data` `createInteractiveSFMapWithData(data, options)` | `createMap(data, options)`; mount `.element` |
|
|
427
|
+
|
|
428
|
+
`staticMapData` comes from `@kahwee/sf-map-svg/data/static` and includes the complete static map. `fullMapData` comes from `/data/full` and adds the interactive lookup collections. For smaller bundles, construct data from individual `/data/*` modules or use `guideMapData` from `/guide/data`. There is no implicit geographic data in the root import.
|
|
429
|
+
|
|
430
|
+
```ts
|
|
431
|
+
import { createMap, renderMap } from '@kahwee/sf-map-svg';
|
|
432
|
+
import { fullMapData } from '@kahwee/sf-map-svg/data/full';
|
|
433
|
+
|
|
434
|
+
const svg = renderMap(fullMapData.map, { landmarks: true }).svg;
|
|
435
|
+
const map = createMap(fullMapData, { mode: 'neighborhoods' });
|
|
436
|
+
document.querySelector('#map')?.append(map.element);
|
|
437
|
+
map.camera.zoom(2);
|
|
438
|
+
map.configure({ layers: { landmarks: false } });
|
|
439
|
+
map.destroy();
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
The controller owns subscriptions and cleanup. Replace old element calls with controller methods (`camera.get/set/pan/zoom/reset/fit/stop`, `selectNeighborhood`, `setSource`, `setMode`, `setLabels`, `setMarkers`, and `setOverlays`). Group feature flags under `features` and styling under `appearance`; `configure` updates runtime features, layers, and controls. For code that needs the guide shell, `/guide`, `/guide/data`, `/guide/static`, and `/guide/map` remain supported. `/transit` also remains supported.
|
|
443
|
+
|
|
444
|
+
Review tree-shaking after migrating: `/data/full` intentionally includes every packaged layer, while the root and `/static` stay data free. Test server rendering, browser mounting, and map disposal in your application before upgrading production.
|
|
445
|
+
|
|
446
|
+
## Updating from 3.0 to 3.1
|
|
447
|
+
|
|
448
|
+
Existing guide factories remain compatible. New integrations can use `createGuideController(options)` and `mountGuideController(shell, options)` from `/guide` or `/guide/map`. Move flat styling options into `appearance`, animation options into `features`, append `.element`, and use `.camera` and `.on()` as with `createMap`.
|
|
449
|
+
|
|
450
|
+
District styles are now prepared once per district on construction and style/year updates. Hover and selection reuse that snapshot. If a callback reads mutable external data, call `map.setDistrictStyle(callback)` after changing the data; do not rely on hovering to refresh colors. Callback errors leave the previous style intact, and callback-triggered updates or destruction take precedence over the pending update.
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
<!-- Generated from SOURCES.md; @kahwee/sf-map-svg 4.1.0 · local working tree. -->
|
|
455
|
+
|
|
456
|
+
# Geometry and provenance
|
|
457
|
+
|
|
458
|
+
## Candidate votes in San Francisco, 2016–2026
|
|
459
|
+
|
|
460
|
+
Downloaded September 26, 2026 from the San Francisco Department of Elections'
|
|
461
|
+
final district statements of vote. The files under `data/candidates/`
|
|
462
|
+
contain 56 candidate contests across six elections, with candidate vote counts
|
|
463
|
+
for San Francisco citywide and supervisorial districts. Original workbooks are
|
|
464
|
+
ingestion inputs and are not distributed.
|
|
465
|
+
|
|
466
|
+
| Election | Final official workbook | Supervisorial map vintage |
|
|
467
|
+
| --- | --- | --- |
|
|
468
|
+
| November 8, 2016 | https://www.sfelections.org/results/20161108/data/20161206/20161206_sov.xlsx | 2012 |
|
|
469
|
+
| November 6, 2018 | https://www.sfelections.org/results/20181106/data/20181127/20181127_sov.xlsx | 2012 |
|
|
470
|
+
| November 3, 2020 | https://www.sfelections.org/results/20201103/data/20201201/20201201_dsov.xlsx | 2012 |
|
|
471
|
+
| November 8, 2022 | https://www.sfelections.org/results/20221108/data/20221201/dsov.xlsx | 2022 |
|
|
472
|
+
| November 5, 2024 | https://www.sfelections.org/results/20241105/data/20241203/dsov.xlsx | 2022 |
|
|
473
|
+
| June 2, 2026 | https://sfelections.org/results/20260602/data/20260625/dsov.xlsx | 2022 |
|
|
474
|
+
|
|
475
|
+
The [Department's election data catalog](https://sfelections.org/tools/election_data/datasets.php)
|
|
476
|
+
indexes related election materials. `scripts/import-candidate-results.py` records
|
|
477
|
+
each input SHA-256 in the corresponding JSON and reconciles candidate totals,
|
|
478
|
+
under/overvotes, and all eleven supervisorial areas against the official SF
|
|
479
|
+
total. The 2016 and 2018 workbooks contain explicit district and neighborhood
|
|
480
|
+
summary sections. Later workbooks supply dedicated district statements. A
|
|
481
|
+
missing supervisorial row in a congressional or legislative contest means that
|
|
482
|
+
district was outside the eligible contest area; it is not a zero vote share.
|
|
483
|
+
The 2024 Treasurer workbook reports one additional cumulative vote and two
|
|
484
|
+
undervotes outside the supervisorial rows, retained in `unassigned` and the
|
|
485
|
+
citywide total.
|
|
486
|
+
All pages use candidate / total valid contest votes, not a two-party share.
|
|
487
|
+
The archive excludes ranked-choice local races, primary contests before 2026,
|
|
488
|
+
and other election dates; it does not claim to represent all SF elections.
|
|
489
|
+
Historical neighborhood labels in election workbooks are not substituted for
|
|
490
|
+
the independent neighborhood boundary datasets in this repository.
|
|
491
|
+
|
|
492
|
+
## California propositions in San Francisco
|
|
493
|
+
|
|
494
|
+
Downloaded September 26, 2026 from the San Francisco Department of Elections' [final November 5, 2024 results](https://sfelections.org/results/20241105w/detail.html):
|
|
495
|
+
|
|
496
|
+
- [Certified district statement of vote workbook](https://www.sfelections.org/results/20241105/data/20241203/dsov.xlsx), SHA-256 `9bf9c7d767273e73b3cdf5b98086412cbe354b0928ee6abb0e379deb0353c403`
|
|
497
|
+
- [Certification letter](https://www.sfelections.org/results/20241105/data/20241203/CertificationLetterNov52024.pdf)
|
|
498
|
+
- [California Secretary of State statewide Statement of Vote](https://www.sos.ca.gov/elections/prior-elections/statewide-election-results/general-election-nov-5-2024/statement-vote) for statewide context
|
|
499
|
+
|
|
500
|
+
`data/propositions/2024-11-05.json` contains Yes and No counts for all ten state propositions, grouped by San Francisco supervisorial district. The short titles are editorial navigation labels. `scripts/import-2024-propositions.py` reads the final workbook's proposition sheets and checks that all eleven district totals sum exactly to its San Francisco citywide totals. Yes share divides Yes by Yes plus No, excluding undervotes and overvotes. The data describes where ballots were cast within San Francisco, not individual voters or statewide outcomes. Its 2022 district geography matches the election's district vintage.
|
|
501
|
+
|
|
502
|
+
## Map extraction
|
|
503
|
+
|
|
504
|
+
District geometry, coast, label anchors, highway geometry, and the pastel district palette were extracted from the San Francisco District Map on September 25, 2026. Only map rendering and public geographic data are distributed. Site account identifiers, original application code, and ballot overlays are excluded.
|
|
505
|
+
|
|
506
|
+
## Districts and coastline
|
|
507
|
+
|
|
508
|
+
DataSF datasets used by the original Site:
|
|
509
|
+
|
|
510
|
+
| Layer | Source |
|
|
511
|
+
| ---------------------------- | ---------------------------------------------- |
|
|
512
|
+
| 2002 supervisorial districts | https://data.sf.gov/resource/qdm2-fi8r.geojson |
|
|
513
|
+
| 2012 supervisorial districts | https://data.sf.gov/resource/keex-zmn4.geojson |
|
|
514
|
+
| 2022 supervisorial districts | https://data.sf.gov/resource/f2zs-jevy.geojson |
|
|
515
|
+
| Display land mask | https://data.sf.gov/resource/hcgx-vtsb.geojson |
|
|
516
|
+
| Highways | https://data.sf.gov/resource/3psu-pn9h.geojson |
|
|
517
|
+
|
|
518
|
+
The Site prepared a common coastline using polygon intersections. It excludes the 2002 district-zero water area and tiny survey/reclamation differences between years; internal district boundaries remain unchanged. It represents about 99.631% of the current trimmed land mask. This package preserves that display geometry and includes Treasure Island; it is not a cadastral or navigational map and does not include the Farallon Islands.
|
|
519
|
+
|
|
520
|
+
## Optional neighborhoods
|
|
521
|
+
|
|
522
|
+
117 SF Find Neighborhoods downloaded September 25, 2026:
|
|
523
|
+
|
|
524
|
+
- GeoJSON: https://data.sf.gov/resource/gfpk-269f.geojson?$limit=500
|
|
525
|
+
- Catalog: https://data.sf.gov/Geographic-Locations-and-Boundaries/SF-Find-Neighborhoods/pty2-tcw4
|
|
526
|
+
|
|
527
|
+
The Mayor’s Office of Neighborhood Services defined these areas in **2006** for SF Find. They convey approximate neighborhood locations, not hard demarcations. SF Find remains available as an alternative JSON collection. The default renderer now uses the August 2010 SFAR realtor collection, independently of district year, clipped to the Site’s display coastline.
|
|
528
|
+
|
|
529
|
+
## Rights
|
|
530
|
+
|
|
531
|
+
Source geometry is provided by the City and County of San Francisco through DataSF, subject to the source datasets’ terms: https://datasf.org/opendata/terms-of-use/
|
|
532
|
+
|
|
533
|
+
Package licensing does not change rights in the underlying public data. Retain source attribution when redistributing maps or data.
|
|
534
|
+
|
|
535
|
+
## Parks and landmark areas
|
|
536
|
+
|
|
537
|
+
Downloaded September 25, 2026:
|
|
538
|
+
|
|
539
|
+
- Recreation and Parks Properties: https://data.sf.gov/resource/gtr9-ntp6.geojson?$limit=1000
|
|
540
|
+
- Presidio boundary: https://data.sf.gov/resource/jt6f-vx2z.geojson
|
|
541
|
+
|
|
542
|
+
`data/landmarks.json` retains full source coordinates for six selected landmark areas. Golden Gate Park combines property sections 1–7 into one MultiPolygon; section boundaries are not stroked. Lincoln Park, John McLaren Park, Mission Dolores Park, and Twin Peaks use their named RPD properties. The Presidio uses its separate boundary dataset. Label anchors and offsets are hand-positioned for city-scale legibility. These are property areas, not neighborhood approximations, and do not vary with the district year.
|
|
543
|
+
|
|
544
|
+
## BART stations
|
|
545
|
+
|
|
546
|
+
Downloaded September 25, 2026 from BART's [geospatial data page](https://www.bart.gov/schedules/developers/geo):
|
|
547
|
+
|
|
548
|
+
- Official station-centroid KML archive: https://www.bart.gov/sites/default/files/2025-12/BART-Stations-tracks-entrances-121025.kmz_.zip
|
|
549
|
+
|
|
550
|
+
The `BART Station` folder supplies names and unrounded longitude/latitude for the eight San Francisco stations: Embarcadero, Montgomery St, Powell St, Civic Center/UN Plaza, 16th St/Mission, 24th St/Mission, Glen Park, and Balboa Park. Entrances, tracks, and stations outside city limits are excluded. Station locations are a current overlay, not historical station inventories matched to each district year. BART data retains its source rights independently of the package license.
|
|
551
|
+
|
|
552
|
+
## Public JSON collections and neighborhood naming
|
|
553
|
+
|
|
554
|
+
On September 25, 2026, the previously bundled district, coast, highway, park, and station geometry was moved unchanged to `data/*.json`. District `properties.displayExtras` and label points retain the original map's presentation geometry and island badges. The district JSON files describe processed display maps, not untouched source downloads. Regression digests preserve the original district and SF Find coordinate sequences.
|
|
555
|
+
|
|
556
|
+
The SF Find source was fetched again and its 117 geometries matched the bundled geometry exactly. Two additional complete collections were downloaded on September 25, 2026:
|
|
557
|
+
|
|
558
|
+
- Analysis Neighborhoods (41): https://data.sf.gov/resource/j2bu-swwd.geojson?$limit=100
|
|
559
|
+
- Realtor Neighborhoods (92; August 2010 SFAR definitions): https://data.sf.gov/resource/2kjj-ysvr.geojson?$limit=500
|
|
560
|
+
|
|
561
|
+
The [Analysis Neighborhoods metadata](https://catalog.data.gov/dataset/analysis-neighborhoods) explains the census-tract aggregation and explicitly states that these are not official neighborhood boundaries. The [Realtor Neighborhoods dataset](https://data.sf.gov/d/2kjj-ysvr) identifies SFAR as the source of those alternative definitions. Collection counts mean 250 source-specific definitions, not 250 unique neighborhoods or an exhaustive inventory of every locally used name. Same-name polygons are not merged across sources.
|
|
562
|
+
|
|
563
|
+
Canonical names default to original source labels. The package normalizes Haight-Ashbury and Fisherman's Wharf punctuation where the SF Find source differs, preserving the exact labels in `sourceName`. Curated lookup aliases use these naming references; the references support names, not agreement with the GeoJSON boundary:
|
|
564
|
+
|
|
565
|
+
- Mission / Mission District / The Mission: https://www.sftravel.com/neighborhoods/mission-district and https://www.sftravel.com/neighborhoods
|
|
566
|
+
- South of Market / SoMa: https://www.sftravel.com/neighborhoods/soma-yerba-buena
|
|
567
|
+
- North Panhandle / NoPa / North of the Panhandle: https://www.sftravel.com/article/where-to-eat-drink-san-franciscos-nopa
|
|
568
|
+
- Haight-Ashbury and Fisherman's Wharf display spellings: https://www.sftravel.com/neighborhoods
|
|
569
|
+
|
|
570
|
+
No survey of resident consensus is claimed. Composite areas are not converted to aliases of their component neighborhoods. Mission and Outer Mission remain separate identities. Detailed schema and access examples are in `data/README.md`.
|
|
571
|
+
|
|
572
|
+
## Non-overlapping realtor topology
|
|
573
|
+
|
|
574
|
+
On September 25, 2026, pairwise polygon intersection checks found 64 overlapping pairs in the original 92-area realtor dataset. These were boundary slivers totaling approximately 1.05 square meters (local planar estimate). `pnpm data:normalize-realtor` removes shared interior area by assigning it to the lexicographically first stable neighborhood ID and subtracting it from the other feature. This is a deterministic geometric tie-break for source slivers, not a new claim about legal boundaries.
|
|
575
|
+
|
|
576
|
+
The cleanup retains all 92 identities, names, and source codes. It uses no rounding or buffers, recalculates affected bounding boxes, and verifies that the union of all neighborhood areas is unchanged. Shared edges and vertices remain valid. `data/neighborhoods-realtor.json` records the transformation in `topology`; its polygons are normalized derivatives of the cited source. Regression tests require empty pairwise polygon intersections, non-overlapping component polygons, the original union digest, and an idempotent cleanup. Polygon clipping is a development dependency only; rendering remains dependency-free.
|
|
577
|
+
|
|
578
|
+
## Key road landmarks
|
|
579
|
+
|
|
580
|
+
Downloaded September 26, 2026 (UTC; September 25 in San Francisco) from [DataSF Streets – Active and Retired](https://data.sf.gov/resource/3psu-pn9h.geojson), filtering `active = true` and exact source street names. `data/key-roads.json` groups 513 source segments into six explicitly selected streets: Market, Van Ness, Geary, Lombard, 19th Avenue, and the Embarcadero, preserving source coordinates and CNN segment IDs. Geary St and Geary Blvd are grouped under the Geary Blvd display label. These are geographic orientation features, not a complete network or vehicle-access guidance. Label anchors select existing source vertices near editorial targets. Regenerate with `node scripts/import-key-roads.mjs` then `pnpm data:catalog`. The full query and retrieval date are embedded in the JSON. DataSF terms apply.
|
|
581
|
+
|
|
582
|
+
## Lightweight guide geometry
|
|
583
|
+
|
|
584
|
+
On September 26, 2026, `pnpm data:guide` generated `data/guide/*.json` from the canonical coastline, SFAR neighborhood, park, highway, selected-street, and BART files above. The overview omits unused properties and simplifies lines at subpixel tolerance for an approximately 800px city map. Realtor boundaries are simplified as shared arcs and reused on adjacent polygons. Coastline and park overview polygons are simplified independently while preserving ring closure. The guide preset keeps US 101, I-280, Highway 1 and the six named orientation streets. Detailed selected geography loads only when requested. Derived files retain source metadata and source dates; no new geographic source is asserted.
|
|
585
|
+
|
|
586
|
+
## June 2026 ballot measures explorer
|
|
587
|
+
|
|
588
|
+
Downloaded September 26, 2026 from the San Francisco Department of Elections:
|
|
589
|
+
|
|
590
|
+
- Final district workbook: https://sfelections.org/results/20260602/data/20260625/dsov.xlsx
|
|
591
|
+
- Citywide summary, official measure titles, ballot questions, and thresholds: https://sfelections.org/results/20260602/index.html
|
|
592
|
+
- Certification dated June 25, 2026: https://sfelections.org/results/20260602/data/20260625/CertificationLetterJun22026.pdf
|
|
593
|
+
- Final report index: https://sfelections.org/results/20260602w/detail.html
|
|
594
|
+
|
|
595
|
+
`data/elections/2026-06-02.json` contains Measures A–D, their citywide counts, and the 11 `SUP DIST n - Total` rows from workbook sheets 20–23. Each row retains its worksheet row number; metadata retains the workbook SHA-256. `scripts/import-election-results.py` extracts these with openpyxl (ingestion only), checks all district sums including under/overvotes, and cross-checks Yes/No citywide counts against the official HTML summary. Rerun with the downloaded workbook and summary paths. No original Site ballot overlays are reused.
|
|
596
|
+
|
|
597
|
+
The Pages-only explorer calculates Yes / (Yes + No), excluding under/overvotes. Measure A uses the two-thirds threshold; B–D use a strict majority, as stated in the official summary. These are citywide outcomes, not district-level passage decisions. District counts are reported directly by Elections, not spatially assigned to neighborhoods. The existing 2022 district display map is reused unchanged. Election JSON is a separate website dataset, not a new npm package API; no live service or forthcoming-election coverage is claimed.
|
|
598
|
+
|
|
599
|
+
## Historical local measures for the Pages explorer
|
|
600
|
+
|
|
601
|
+
Downloaded September 26, 2026. Each snapshot contains **all local measures in
|
|
602
|
+
the listed election**, not every election in the surrounding decade. All
|
|
603
|
+
eleven district Yes/No totals reconcile to the citywide official totals.
|
|
604
|
+
|
|
605
|
+
| Election | Final results source | Titles and thresholds | District method |
|
|
606
|
+
| --- | --- | --- | --- |
|
|
607
|
+
| November 5, 2002 | https://sfelections.org/results/20021105/SOV021105.xls | https://webbie1.sfpl.org/multimedia/pdf/elections/November5_2002.pdf | Official workbook `PROPOSITIONS` supervisorial rows; under/overvotes unavailable |
|
|
608
|
+
| November 6, 2012 | https://sfelections.org/results/20121106/data/SOV_Nov2012.xls | https://sfelections.org/results/20121106/index.html and https://webbie1.sfpl.org/multimedia/pdf/elections/November6_2012.pdf | 596 official precincts grouped by DataSF's 2012 `supdist` field |
|
|
609
|
+
| November 8, 2022 | https://www.sfelections.org/results/20221108/data/20221201/dsov.xlsx | https://sfelections.org/results/20221108/index.html | Official final district workbook rows |
|
|
610
|
+
|
|
611
|
+
The 2012 historical precinct-to-district source is
|
|
612
|
+
https://data.sfgov.org/resource/bsfq-aeyw.json?$limit=1000 . Its 605 records
|
|
613
|
+
include 596 election precinct identifiers and the `supdist` attribute; joined
|
|
614
|
+
mail-ballot and slash-combined precinct rows map to one district each. This
|
|
615
|
+
2012 collection is used only to group the 2012 official vote rows. The
|
|
616
|
+
crosswalk and workbook checksums are retained in the JSON. The 2002 voter
|
|
617
|
+
pamphlet explicitly discusses the bond thresholds; 2012 A and B require
|
|
618
|
+
two-thirds, as established by the parcel tax and bond measures.
|
|
619
|
+
|
|
620
|
+
For 2022, the independent **final** summary workbook is
|
|
621
|
+
https://www.sfelections.org/results/20221108/data/20221201/summary.xlsx and
|
|
622
|
+
the certification is
|
|
623
|
+
https://www.sfelections.org/results/20221108/data/20221201/N2022_CertificationLetter.pdf .
|
|
624
|
+
The election summary HTML has older counts for some measures, so it is used
|
|
625
|
+
only for titles, ballot questions, and the stated thresholds. The importer
|
|
626
|
+
compares every citywide Yes, No, undervote, and overvote count with the final
|
|
627
|
+
summary workbook. The 2002 official results index is
|
|
628
|
+
https://sfelections.org/results/20021105w/index.html and the 2012 results
|
|
629
|
+
index is https://sfelections.org/results/20121106/detail.php .
|
|
630
|
+
|
|
631
|
+
`scripts/import-historical-measures.py` records input SHA-256 hashes and
|
|
632
|
+
reconciles all 2002, 2012, and 2022 district totals. The election JSON files
|
|
633
|
+
are Pages datasets; the historical district maps retain their existing
|
|
634
|
+
display geometry, and no precinct polygons or raw workbook data are shipped.
|
|
635
|
+
|
|
636
|
+
---
|
|
637
|
+
|
|
638
|
+
<!-- Generated from CHANGELOG.md; @kahwee/sf-map-svg 4.1.0 · local working tree. -->
|
|
639
|
+
|
|
640
|
+
# Changelog
|
|
641
|
+
|
|
642
|
+
User-visible changes are recorded here. Unreleased entries describe changes on `main` that are not part of a tagged package release.
|
|
643
|
+
|
|
644
|
+
## 4.1.0 — 2026-10-01
|
|
645
|
+
|
|
646
|
+
- Validate canonical and generated GeoJSON during data checks, rejecting invalid WGS84 coordinates and malformed polygon rings before release.
|
|
647
|
+
|
|
648
|
+
- Reject nonnumeric or nonfinite custom district IDs before they enter SVG markup or animation CSS; fall back to the primary district label when `labelPoints` is empty.
|
|
649
|
+
- Stop district activation and keyboard focus after selection callbacks destroy the map or replace the selection; share pointer and keyboard activation guards.
|
|
650
|
+
- Keep layers studio controls safe while data loads, honor the chosen rendering mode, and report failed loading without a second error. Narrow the older-release feature fallback to its supported compatibility cases.
|
|
651
|
+
|
|
652
|
+
- Preserve existing HTML entities in generated social titles instead of double-escaping them.
|
|
653
|
+
|
|
654
|
+
- Gate studio interactions until geography loads, keep failed loads safe, disable controls unsupported by static maps or the selected release, and label preview-only APIs on released documentation pages.
|
|
655
|
+
- Make the camera ownership browser check independent of a short animation timing window; clarify that marker-change events report selection rather than every activation.
|
|
656
|
+
|
|
657
|
+
- Make misplaced interactive-option errors name the supported appearance, feature, or event API.
|
|
658
|
+
|
|
659
|
+
- Clarify developer integration defaults, runtime configuration and lifecycle; generate version-aware llms.txt, complete text docs, Markdown guides and TypeScript contracts for npm and Pages.
|
|
660
|
+
|
|
661
|
+
- Fix layers studio copied examples to preserve the selected mode, layers, motion durations, and supported release features; execute generated examples in the regression checks.
|
|
662
|
+
|
|
663
|
+
- Add opt-in `animation` for `renderMap`: a self-contained, scoped CSS draw-in (coast and lines draw, districts grow, labels and points follow) that plays inline or as an image and respects reduced motion. Default output is unchanged.
|
|
664
|
+
- Add `features.layerTransitions`: layer switches fade, `setSource()` crossfades neighborhood definitions, and `setDistrictStyle()` crossfades district fills.
|
|
665
|
+
- Add `features.districtMorph`: district outlines morph between map years on `setDistrictYear()`, settling on the exact geometry; `{ animate: false }` stays instant.
|
|
666
|
+
- Pages site: a classic, editorial redesign with an animated atlas home, a scroll-driven tour of the controller, a layers studio for every layer and neighborhood definition, rebuilt local measures, California propositions and supervisorial votes examples, and new documentation and API reference pages. Example pages now load prebuilt display maps and a display-simplified dataset instead of raw geometry.
|
|
667
|
+
|
|
668
|
+
## 4.0.1 — 2026-09-28
|
|
669
|
+
|
|
670
|
+
- Reconcile marker updates by ID, preserving retained SVG nodes, picker options, keyboard focus, and entrance animations. Identical ordered updates no longer mutate the DOM; only new IDs animate in and removed markers cancel their entrances.
|
|
671
|
+
- Advance animated cameras and dependent label, marker, and cluster layout in one shared browser frame, with regression coverage for coalescing, cancellation, reentrant updates, and destruction.
|
|
672
|
+
|
|
673
|
+
## 4.0.0 — 2026-09-28
|
|
674
|
+
|
|
675
|
+
- Breaking: require Node 24 or newer for server rendering and development. Validate the minimum supported major alongside Node 26 in CI; browser runtime requirements are unchanged.
|
|
676
|
+
- No map API, geographic data, rendering, or animation changes. Existing v3 integrations only need a supported Node runtime to upgrade.
|
|
677
|
+
|
|
678
|
+
## 3.1.0 — 2026-09-28
|
|
679
|
+
|
|
680
|
+
- Add `createGuideController` and `mountGuideController` with the grouped options, camera, subscriptions, and lifecycle of `createMap`; preserve existing element-based guide factories.
|
|
681
|
+
- Reject invalid marker coordinates before static rendering invokes district-style callbacks.
|
|
682
|
+
- Prepare district styles once per update before changing the map, and reuse projected district paths.
|
|
683
|
+
- Track both district crossfades so interrupted transitions, reduced motion, and teardown remove all outgoing layers.
|
|
684
|
+
- Reuse label nodes and screen-space text measurements during camera movement, refreshing metrics when fonts load; separate label rendering and district appearance from the interactive engine.
|
|
685
|
+
- Give marker visuals and entrance animations their own lifecycle, and prevent reentrant district callbacks from overwriting newer state or restarting work after destruction.
|
|
686
|
+
- Document guide-controller integration and district-style refresh semantics; add browser regressions for callback atomicity, interrupted fades, camera motion, marker replacement, label reuse, and progressive shell mounting.
|
|
687
|
+
|
|
688
|
+
## 3.0.1 — 2026-09-27
|
|
689
|
+
|
|
690
|
+
- Narrow the complete-data presets to their direct geographic modules without changing rendered SVGs or public API behavior.
|
|
691
|
+
- Correct stale contributing and example documentation links, add a local Markdown link check to `pnpm check`, and show the v3 controller API on the Pages homepage.
|
|
692
|
+
- Skip the early push-triggered Pages build while a new package version is still processing on npm; the successful release workflow deploys that version from the registry.
|
|
693
|
+
- Serve local visual snapshots with the same streaming Node HTTP approach as the Pages browser check, avoiding intermittent resets while loading large optional map data.
|
|
694
|
+
|
|
695
|
+
## 3.0.0 — 2026-09-27
|
|
696
|
+
|
|
697
|
+
Version 3 removes the deprecated compatibility APIs. Applications must migrate before upgrading. The data-free root API introduced in version 2 remains the supported API.
|
|
698
|
+
|
|
699
|
+
### Breaking changes
|
|
700
|
+
|
|
701
|
+
- Remove the `/legacy`, `/custom-map`, `/explorer`, `/interactive`, and `/interactive-data` package entry points and their bundled-data factories. These imports fail to resolve in v3; they are removed, not merely marked deprecated.
|
|
702
|
+
- Replace `renderSFMap(options)` with `renderMap(fullMapData.map, options).svg`, `createSFMap(options)` with `renderMap(fullMapData.map, options)`, and `createSFMapWithData(options, data)` with `renderMap(data, options)`.
|
|
703
|
+
- Replace `createNeighborhoodExplorer(options)` and `createInteractiveSFMap(options)` with `createMap(fullMapData, options)`; mount the returned controller's `.element` and call `.destroy()` on unmount. Replace `createInteractiveSFMapWithData(data, options)` with `createMap(data, options)`.
|
|
704
|
+
- Import complete packaged geography explicitly from `/data/full`, or compose smaller data from `/data/*`. The root import continues to bundle no geography. Existing `/guide` and `/transit` entries remain available.
|
|
705
|
+
|
|
706
|
+
### Migration and verification
|
|
707
|
+
|
|
708
|
+
- Add a [v3 migration guide](https://github.com/kahwee/sf-map-svg/blob/main/docs/migration-v3.md) with import and method mappings. Update README, runnable examples, Storybook, Pages generation, TypeScript contracts, and packed-consumer checks to use the supported API.
|
|
709
|
+
- Preserve the generated SVG output for the old complete-data preset while making that data an explicit import. Keep the guide's narrow overview and detailed-data loading.
|
|
710
|
+
- Add `/data/static` for the complete static map without interactive lookup collections; use it on the Pages spot explorer so its detailed coast and labels retain their reviewed appearance without pulling in unused collections.
|
|
711
|
+
|
|
712
|
+
## 2.2.0 — 2026-09-27
|
|
713
|
+
|
|
714
|
+
- Add “One spot, three San Franciscos” to Pages and Storybook, comparing the same location across district, neighborhood, and transit views.
|
|
715
|
+
- Modernize the Storybook examples in TypeScript and gate map changes with reviewed desktop and 390 px visual snapshots on macOS and Linux.
|
|
716
|
+
- Add first-class election choropleths: static and interactive `districtStyle`, live district year/style setters, district selection and activation events, keyboard interaction, and a reduced-motion-aware year crossfade.
|
|
717
|
+
- Export `getLayerPaths` for canonical fitted layer geometry and `DistrictYear`, `DistrictRowData`, and `DistrictStyle` from the modern entrypoints.
|
|
718
|
+
- Add an official-election Storybook example covering three boundary years, vote-share coloring, mobile layout, keyboard selection, year changes, and accessibility checks.
|
|
719
|
+
|
|
720
|
+
## 2.1.0 — 2026-09-27
|
|
721
|
+
|
|
722
|
+
- Pages site: add motion and a dark theme. Cross-document View Transitions keep the header still and morph example titles into page titles; the home hero is an interactive dot map of the 2022 districts; entrances, count-ups, growing charts, and hover/focus micro-interactions respect reduced motion and print. The site adds no library.
|
|
723
|
+
- Pages site: load simplified display maps and thumbnails instead of full-precision SVGs and bundled GeoJSON (candidate and proposition explorers drop from about 391 KB to 12 KB of compressed JavaScript and CSS); add dark map thumbnails, a favicon, a 404 page, a sitemap, per-page social tags, theme-colored browser bars, and a browser smoke test with size budgets that gates deployment.
|
|
724
|
+
- Add six optional San Francisco candidate vote snapshots (2016–2026), a Pages explorer, official source records, and a dataset import script.
|
|
725
|
+
|
|
726
|
+
## 2.0.0 — 2026-09-27
|
|
727
|
+
|
|
728
|
+
Version 2 makes geography an explicit dependency and separates the application API
|
|
729
|
+
from its DOM element. This keeps small consumers small, gives configuration one
|
|
730
|
+
consistent home, and makes animation and subscription ownership predictable.
|
|
731
|
+
|
|
732
|
+
**Migration:** [Complete v1 → v2 guide, with before/after examples](https://github.com/kahwee/sf-map-svg/blob/v2.0.0/docs/migration-v2.md).
|
|
733
|
+
**API:** [README](https://github.com/kahwee/sf-map-svg/blob/v2.0.0/README.md).
|
|
734
|
+
**Consumer performance:** [Integration guide](https://github.com/kahwee/sf-map-svg/blob/v2.0.0/docs/consumer-integration.md).
|
|
735
|
+
|
|
736
|
+
### Breaking changes
|
|
737
|
+
|
|
738
|
+
- The root export now provides data-free `createMap(data, options)` and `renderMap(data, options)`. Move old `createSFMap`, `renderSFMap`, `neighborhoodNames`, `districtColors`, `districtYears`, and legacy static type imports to `/legacy` for an incremental migration. Existing named subpaths retain their compatibility APIs.
|
|
739
|
+
- `createMap` returns a controller: mount `map.element`. Features belong in `features`, styling in `appearance`; flat spellings are rejected. Use `configure({ features, layers, controls })`, `camera.*`, and typed `on()` subscriptions. Appearance is construction-only.
|
|
740
|
+
- The controller throws on operations after disposal; `destroy()` and unsubscribe remain idempotent. Configuration rejects unknown/malformed keys and invalid ranges. Marker/overlay IDs must be unique. Shared guide geography is immutable; clone before deriving custom data.
|
|
741
|
+
|
|
742
|
+
### Added
|
|
743
|
+
|
|
744
|
+
- Data-free `/map` and `/static` entrypoints, configuration-only `/presets`, detached configuration snapshots, and all-groups validation before configuration updates. Camera pan, zoom, reset, fit and set accept consistent animation options.
|
|
745
|
+
- Palette, label font/weight/halo, selected/hover neighborhood styling, custom legend entries, compact expandable attribution, and independent visible/accessibility touch labels.
|
|
746
|
+
- Opt-in eased camera motion and staggered marker entrances, respecting reduced motion, interruption, preference changes, and disposal.
|
|
747
|
+
- Per-pin radii, selected rings, deterministic screen-space clustering and accessible marker choice, HTML overlay placement, screen projection, north arrow and approximate scale bar.
|
|
748
|
+
- Server-safe overview `createGuideSVG` / `createGuideShell` and compact `mountGuideMap`, using matching simplified geography and reserved layout rows.
|
|
749
|
+
- Coastline-only basemaps without neighborhood datasets. SFAR remains the default when supplied; alternative-only data selects the available source.
|
|
750
|
+
- Full migration documentation, public examples, adversarial API contract matrix, production import-graph/size budgets, and automatic verification that GitHub release notes contain the complete changelog. The npm archive now includes CHANGELOG.md.
|
|
751
|
+
|
|
752
|
+
### Fixed and hardened
|
|
753
|
+
|
|
754
|
+
- Reentrant camera and selection callbacks cannot revive obsolete animation or overwrite a newer selection. Failed source, marker, overlay, feature, layer and control updates preserve existing state.
|
|
755
|
+
- Runtime controls remain independent, feature toggles preserve camera/selection, and unrelated configuration updates no longer interrupt motion. Hiding touch controls returns gestures to the page.
|
|
756
|
+
- Revoke obsolete cluster handlers immediately when markers, selection or clustering settings change; v2 overlay activation is wired to typed events for mouse and keyboard users.
|
|
757
|
+
- Keyboard focus remains reachable through filtering and clustering; removed overlays lose listeners. Construction failures and repeated destruction clean up observers, listeners and asynchronous work.
|
|
758
|
+
- Detached selection/event snapshots prevent accidental mutation of renderer state. Malformed/sparse viewports, coordinates, padding, overlay geometry and misspelled options are rejected. Corrected public `LineString` overlay typing.
|
|
759
|
+
- Root static imports tree-shake away browser code and all geographic JSON. Renderer, camera, clustering, configuration and validation have separate internal boundaries; rendering remains offline with zero runtime dependencies.
|
|
760
|
+
|
|
761
|
+
### Validation and bundle guidance
|
|
762
|
+
|
|
763
|
+
- Covered by Node regression tests, Chromium Storybook interactions/coverage, declaration tests, clean packed-consumer installation, and desktop/390px browser inspection. Required checks include demo, Storybook and Pages builds.
|
|
764
|
+
- Representative production gzip measurements: v2 root **22.8 KiB without geography**; static-only root import **4.5 KiB**; compatibility guide including overview geography **110.2 KiB**. Consumer output varies. Runtime switches do not remove imported code or data; use narrow entrypoints, explicit datasets and lazy detail loading.
|
|
765
|
+
|
|
766
|
+
## 1.5.2 — 2026-09-26
|
|
767
|
+
|
|
768
|
+
- Run every Storybook story as a Chromium/Vitest browser check and publish an LCOV and JSON coverage artifact for the renderer, with baseline regression thresholds.
|
|
769
|
+
- Require the browser and coverage check before npm publishing or Pages deployment.
|
|
770
|
+
|
|
771
|
+
## 1.5.1 — 2026-09-26
|
|
772
|
+
|
|
773
|
+
- Run focused Storybook 10 browser interactions and accessibility checks in CI and before npm publishing, covering overlapping markers, route overlays, and keyboard selection.
|
|
774
|
+
- Validate certified proposition district sums against city totals in the data test suite.
|
|
775
|
+
- Keep Biome and the development dependency set current after an audit and outdated-package review.
|
|
776
|
+
|
|
777
|
+
## 1.5.0 — 2026-09-26
|
|
778
|
+
|
|
779
|
+
- Add `controls.neighborhoodPicker`, `controls.markerPicker`, `controls.help`, and `controls.status` so compact embeds can drop redundant chrome. Hidden help remains the map's accessible description; a hidden status line remains a polite live region; source attribution stays visible.
|
|
780
|
+
- Keep a configured `strings.chooseMarker` label after `setMarkers()` updates instead of reverting to “Choose marker”.
|
|
781
|
+
|
|
782
|
+
## 1.4.1 — 2026-09-26
|
|
783
|
+
|
|
784
|
+
- Add a task-based example gallery and code recipes, with clearer routes among the Pages atlas, local measures, district history, transit, and lightweight map.
|
|
785
|
+
- Add a California proposition explorer using certified November 2024 Yes and No votes for all eleven San Francisco supervisorial districts; publish its sourced JSON and reproducible import script.
|
|
786
|
+
|
|
787
|
+
## 1.4.0 — 2026-09-26
|
|
788
|
+
|
|
789
|
+
- Split geographic convenience exports into independent data modules, so metadata search and SFAR-only lookup avoid unrelated JSON.
|
|
790
|
+
- Separate the guide's detailed loader from overview geography; keep the existing `/guide` exports and add explicit `/guide/detailed`, `/guide/data`, and `/guide/map` paths.
|
|
791
|
+
- Make the optional transit animation include only its 2022 map geography, preserving the rendered route while cutting its consumer bundle size.
|
|
792
|
+
- Publish a before-and-after tree-shaking report for representative consumer imports.
|
|
793
|
+
|
|
794
|
+
## 1.3.8 — 2026-09-26
|
|
795
|
+
|
|
796
|
+
- Give the district map a clear, dedicated viewing area on mobile by shrinking the election and measure controls and moving the map legend below the map.
|
|
797
|
+
- Replace the oversized mobile focus button with a compact icon while keeping its accessible name and a 44px touch target.
|
|
798
|
+
|
|
799
|
+
## 1.3.7 — 2026-09-26
|
|
800
|
+
|
|
801
|
+
- Expand the GitHub Pages measure explorer to 44 local measures across November 2002, November 2012, November 2022, and June 2026, covering all three supported district map years.
|
|
802
|
+
- Keep election results and district maps in separate, on-demand JSON chunks; add a searchable measure list, election picker, shareable year-specific views, and dated exports.
|
|
803
|
+
- Reconcile historical district vote totals to official citywide results and document source checksums and the 2012 precinct-to-district grouping method.
|
|
804
|
+
|
|
805
|
+
## 1.3.6 — 2026-09-26
|
|
806
|
+
|
|
807
|
+
- Simplify district history playback to one JavaScript morph implementation, removing the larger CSS animation and fallback branch while preserving the moving boundaries and reduced-motion switch.
|
|
808
|
+
|
|
809
|
+
## 1.3.5 — 2026-09-26
|
|
810
|
+
|
|
811
|
+
- Animate district boundary morphs with CSS path transitions and coordinated CSS fades on supported browsers, retaining a JavaScript fallback and reduced-motion instant switch.
|
|
812
|
+
|
|
813
|
+
## 1.3.4 — 2026-09-26
|
|
814
|
+
|
|
815
|
+
- Morph matched district outlines between historical maps on GitHub Pages, restoring moving boundaries in place of the 1.3.3 line trace.
|
|
816
|
+
- Keep each dated SVG exact when motion settles and switch instantly for visitors who request reduced motion.
|
|
817
|
+
|
|
818
|
+
## 1.3.3 — 2026-09-26
|
|
819
|
+
|
|
820
|
+
- Draw each historical district map’s actual boundary lines during the GitHub Pages playback, with a reduced-motion instant switch.
|
|
821
|
+
|
|
822
|
+
## 1.3.2 — 2026-09-26
|
|
823
|
+
|
|
824
|
+
- Rebuild GitHub Pages as a civic atlas led by interactive June 2026 ballot measure results, with district selection and clear citywide outcomes.
|
|
825
|
+
- Showcase 2002, 2012, and 2022 district maps together and add a controllable boundary reveal that respects reduced-motion settings.
|
|
826
|
+
- Bring the schematic BART journey into the homepage with one-click playback and keep the guide map available on demand.
|
|
827
|
+
|
|
828
|
+
## 1.3.1 — 2026-09-26
|
|
829
|
+
|
|
830
|
+
- Rework the GitHub Pages homepage around the lightweight guide map and show the released bundle-size comparison.
|
|
831
|
+
- Load the full neighborhood explorer and schematic BART demo only when visitors request them, keeping unused geography out of the initial page load.
|
|
832
|
+
|
|
833
|
+
## 1.3.0 — 2026-09-26
|
|
834
|
+
|
|
835
|
+
- Add a lightweight guide map preset with subpixel overview geography, lazy detailed datasets, curated highways and streets, independent road labels, and smaller consumer bundle size.
|
|
836
|
+
- Curate six guide streets and prioritize their visibility by zoom while preserving road geometry across park fills.
|
|
837
|
+
- Simplify shared neighborhood boundaries while preserving the combined city footprint and recognizable park, coast, and road-crossing geometry.
|
|
838
|
+
|
|
839
|
+
## 1.2.0
|
|
840
|
+
|
|
841
|
+
### Added
|
|
842
|
+
|
|
843
|
+
- Add `@kahwee/sf-map-svg/custom-map`, a data-injected renderer entry point that lets consumers bundle only the geographic datasets they provide.
|
|
844
|
+
- Add public styled geographic overlays, explorer theme tokens, configurable labels, and optional controls.
|
|
845
|
+
- Validate SVG overlay numeric attributes and add an interactive data-injected entry point so consumers can provide only the geographic datasets they use.
|
|
846
|
+
- Add a Pages ballot-measures explorer for certified June 2026 local results, district comparisons, shareable selections, and official downloads.
|
|
847
|
+
- Build the Pages demos and SVG downloads from the published npm version, with visible release metadata, installation copying, and release links.
|
|
848
|
+
- Make the measures atlas a full-screen map with side-by-side comparisons, touch navigation, overlay tables and sources, and a focus view.
|
|
849
|
+
- Refresh the Pages layout for mobile and add expandable certified election results with SVG/CSV/JSON exports.
|
|
850
|
+
|
|
851
|
+
## 1.1.0
|
|
852
|
+
|
|
853
|
+
### Added
|
|
854
|
+
|
|
855
|
+
- Public [GitHub Pages explorer](https://kahwee.github.io/sf-map-svg/) with neighborhood search, map modes, usage tips, SVG examples, and GeoJSON downloads.
|
|
856
|
+
- Automatic Pages builds and deployment from `main`, plus `pnpm build:pages` for local previews.
|
|
857
|
+
- Optional `@kahwee/sf-map-svg/transit` browser component and an [embeddable transit demo](https://kahwee.github.io/sf-map-svg/transit.html). The schematic BART journey connects official station locations with straight segments; it does not represent actual tracks or live service.
|
|
858
|
+
- Play/pause controls, a keyboard-accessible journey slider, pause-on-hidden behavior, and a Storybook transit example. Animation starts paused.
|
|
859
|
+
|
|
860
|
+
### Changed
|
|
861
|
+
|
|
862
|
+
- Reorganize the README around installation, choosing an API, static and interactive options, geographic data, development, and Pages deployment.
|
|
863
|
+
|
|
864
|
+
### Fixed
|
|
865
|
+
|
|
866
|
+
- Release map listeners, observers, animation frames, and download URLs when explorer initialization fails. Add browser regression coverage for failed initialization.
|
|
867
|
+
|
|
868
|
+
## 1.0.0
|
|
869
|
+
|
|
870
|
+
- Publish the stable 1.0 API with public npm access and a public source repository.
|
|
871
|
+
- Add the reusable interactive map entrypoint, controlled viewport and selection APIs, keyboard/touch navigation, and marker selection.
|
|
872
|
+
- Add interactive examples, Storybook stories, and viewport/navigation regression checks.
|
|
873
|
+
- Add district/neighborhood explorer modes and a labels toggle, with fixed screen-size labels while zooming.
|
|
874
|
+
- Add a master visible-label switch for standalone SVGs.
|
|
875
|
+
- Add nine optional key-road landmarks from DataSF centerlines, public JSON, and zoom-aware explorer labels.
|
|
876
|
+
- Add a transit-inspired map theme with pale water, quiet land, green parks, and blue station symbols.
|
|
877
|
+
- Refine the neighborhood explorer presentation and map legend.
|
|
878
|
+
- Add a transit example and Storybook preset.
|
|
879
|
+
|
|
880
|
+
## 0.4.1
|
|
881
|
+
|
|
882
|
+
- Migrate library source to strict TypeScript 7 with generated JavaScript and declarations.
|
|
883
|
+
- Replace Prettier with Biome formatting, import organization, and recommended lint rules.
|
|
884
|
+
|
|
885
|
+
## 0.4.0
|
|
886
|
+
|
|
887
|
+
- Add a browser neighborhood explorer with alias search, selection, boundary zoom, and GeoJSON downloads.
|
|
888
|
+
- Adapt explorer labels to zoom and viewport size while preserving static SVG defaults.
|
|
889
|
+
- License software under MIT and add npm release automation and packaged-consumer checks.
|
|
890
|
+
|
|
891
|
+
## 0.3.0
|
|
892
|
+
|
|
893
|
+
- Enable public npm distribution with explicit public registry access.
|
|
894
|
+
- Remove realtor boundary sliver overlaps while preserving all 92 neighborhoods and their combined footprint; enforce disjoint interiors in regression tests.
|
|
895
|
+
- Use the 92 SFAR realtor neighborhoods by default for map outlines, data lookup, and Storybook. Other source collections remain available.
|
|
896
|
+
- Move all geographic data to exported canonical GeoJSON files, preserving district display extras.
|
|
897
|
+
- Expose complete SF Find (117), analysis (41), and realtor (92) neighborhood collections with source-specific canonical names and documented aliases.
|
|
898
|
+
- Add immutable data helpers, exact-name lookup, source-aware search, and a typed geometry export.
|
|
899
|
+
- Split SVG layers from orchestration and reuse projected district paths.
|
|
900
|
+
- Reject longitude values that could overflow SVG coordinates.
|
|
901
|
+
- Add neighborhood explorer stories, JSON downloads, independent overlay stories, and data integrity checks.
|
|
902
|
+
|
|
903
|
+
## 0.2.0
|
|
904
|
+
|
|
905
|
+
- Add optional park and landmark highlights using DataSF property boundaries.
|
|
906
|
+
- Add all eight San Francisco BART stations using official station coordinates.
|
|
907
|
+
- Add overlay color options, TypeScript declarations, source records, and SVG checks.
|
|
908
|
+
- Add Storybook 10.6 with eight interactive examples and API controls.
|
|
909
|
+
- Add usage examples, contributor instructions, and AGENTS.md.
|
|
910
|
+
- Update GitHub Actions and validate Storybook and package builds across supported Node versions.
|
|
911
|
+
- Configure weekly Dependabot updates for development dependencies and Actions.
|
|
912
|
+
- Keep existing layer defaults unchanged, runtime dependencies at zero, and distribution private.
|
|
913
|
+
|
|
914
|
+
## 0.1.1
|
|
915
|
+
|
|
916
|
+
- Separate geometry and projection helpers from SVG layer rendering.
|
|
917
|
+
- Cache immutable coast bounds across renders.
|
|
918
|
+
- Validate complete SVG documents as XML in regression tests.
|
|
919
|
+
- Remove invalid XML control characters from user-supplied labels.
|
|
920
|
+
- Specify even-odd clipping for coastline holes.
|
|
921
|
+
- Standardize formatting and document contribution and private release workflows.
|
|
922
|
+
- Distribute through private GitHub releases; disable npm publication.
|
|
923
|
+
- Exclude original Site application code and internal source identifiers.
|
|
924
|
+
|
|
925
|
+
## 0.1.0
|
|
926
|
+
|
|
927
|
+
- Extract Site version 6 into a standalone SVG renderer.
|
|
928
|
+
- Bundle 2002, 2012 and 2022 districts plus optional SF Find neighborhood boundaries.
|
|
929
|
+
- Preserve source provenance, palette, coast, labels and optional highways.
|
|
930
|
+
|
|
931
|
+
---
|
|
932
|
+
|
|
933
|
+
# Published type contracts
|
|
934
|
+
|
|
935
|
+
@kahwee/sf-map-svg 4.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.
|
|
936
|
+
|
|
937
|
+
| Public import | Declaration |
|
|
938
|
+
| --- | --- |
|
|
939
|
+
| @kahwee/sf-map-svg | ./dist/src/api.d.ts |
|
|
940
|
+
| @kahwee/sf-map-svg/data | ./dist/data/index.d.ts |
|
|
941
|
+
| @kahwee/sf-map-svg/data/analysis | ./dist/data/analysis.d.ts |
|
|
942
|
+
| @kahwee/sf-map-svg/data/catalog | ./dist/data/catalog.d.ts |
|
|
943
|
+
| @kahwee/sf-map-svg/data/full | ./dist/src/full-data.d.ts |
|
|
944
|
+
| @kahwee/sf-map-svg/data/static | ./dist/src/static-data.d.ts |
|
|
945
|
+
| @kahwee/sf-map-svg/data/coast | ./dist/data/coast.d.ts |
|
|
946
|
+
| @kahwee/sf-map-svg/data/districts | ./dist/data/districts.d.ts |
|
|
947
|
+
| @kahwee/sf-map-svg/data/highways | ./dist/data/highways.d.ts |
|
|
948
|
+
| @kahwee/sf-map-svg/data/landmarks | ./dist/data/landmarks.d.ts |
|
|
949
|
+
| @kahwee/sf-map-svg/data/lookup | ./dist/data/lookup.d.ts |
|
|
950
|
+
| @kahwee/sf-map-svg/data/realtor | ./dist/data/realtor.d.ts |
|
|
951
|
+
| @kahwee/sf-map-svg/data/roads | ./dist/data/roads.d.ts |
|
|
952
|
+
| @kahwee/sf-map-svg/data/sf-find | ./dist/data/sf-find.d.ts |
|
|
953
|
+
| @kahwee/sf-map-svg/data/stations | ./dist/data/stations.d.ts |
|
|
954
|
+
| @kahwee/sf-map-svg/geometry | ./dist/src/geometry.d.ts |
|
|
955
|
+
| @kahwee/sf-map-svg/guide | ./dist/src/guide.d.ts |
|
|
956
|
+
| @kahwee/sf-map-svg/guide/data | ./dist/src/guide-data.d.ts |
|
|
957
|
+
| @kahwee/sf-map-svg/guide/detailed | ./dist/src/guide-detailed.d.ts |
|
|
958
|
+
| @kahwee/sf-map-svg/guide/map | ./dist/src/guide-map.d.ts |
|
|
959
|
+
| @kahwee/sf-map-svg/transit | ./dist/src/transit.d.ts |
|
|
960
|
+
| @kahwee/sf-map-svg/guide/static | ./dist/src/guide-static.d.ts |
|
|
961
|
+
| @kahwee/sf-map-svg/map | ./dist/src/map.d.ts |
|
|
962
|
+
| @kahwee/sf-map-svg/static | ./dist/src/static.d.ts |
|
|
963
|
+
| @kahwee/sf-map-svg/presets | ./dist/src/presets.d.ts |
|
|
964
|
+
|
|
965
|
+
### ./dist/src/api.d.ts
|
|
966
|
+
|
|
967
|
+
```ts
|
|
968
|
+
/** Runtime and types only. Geography is always an explicit import. */
|
|
969
|
+
export * from './map.js';
|
|
970
|
+
export * from './static.js';
|
|
971
|
+
```
|
|
972
|
+
|
|
973
|
+
### dist/src/map.d.ts
|
|
974
|
+
|
|
975
|
+
```ts
|
|
976
|
+
import type { MapController, MapOptions } from './controller-types.js';
|
|
977
|
+
import type { InteractiveSFMapData } from './explorer-data.js';
|
|
978
|
+
export type * from './controller-types.js';
|
|
979
|
+
export type { InteractiveSFMapData as MapData } from './explorer-data.js';
|
|
980
|
+
export type { CameraOptions, DistrictSelection, DistrictStyle, DistrictYear, InteractiveLayers, MapFeatures, MapMarker, MapOverlay, MapPadding, MapViewport, NeighborhoodSelection, } from './types.js';
|
|
981
|
+
/** No geography is imported. Supply a preset or your own immutable data. */
|
|
982
|
+
export declare function createMap(data: InteractiveSFMapData, options?: MapOptions): MapController;
|
|
983
|
+
```
|
|
984
|
+
|
|
985
|
+
### dist/src/controller-types.d.ts
|
|
986
|
+
|
|
987
|
+
```ts
|
|
988
|
+
import type { Geometry, NeighborhoodSource } from '../data/types.js';
|
|
989
|
+
import type { CameraOptions, DistrictSelection, DistrictStyle, DistrictYear, InteractiveLayers, MapFeatures, MapMarker, MapOverlay, MapPadding, MapViewport, NeighborhoodExplorerOptions, NeighborhoodSelection } from './types.js';
|
|
990
|
+
export type MapAppearance = Pick<NeighborhoodExplorerOptions, 'theme' | 'colors' | 'labelStyle' | 'areaStyle' | 'districtStyle' | 'labelSize' | 'style' | 'markerRadius' | 'markerHitSize' | 'markerColor' | 'selectedMarkerColor'>;
|
|
991
|
+
export type MapControls = NonNullable<NeighborhoodExplorerOptions['controls']>;
|
|
992
|
+
/** Omitted groups/keys retain their values; undefined groups reset all keys in that group. */
|
|
993
|
+
export interface MapConfiguration {
|
|
994
|
+
features?: MapFeatures;
|
|
995
|
+
layers?: InteractiveLayers;
|
|
996
|
+
controls?: MapControls;
|
|
997
|
+
}
|
|
998
|
+
export interface MapOptions extends Omit<NeighborhoodExplorerOptions, keyof MapFeatures | keyof MapAppearance | 'interface' | 'onMarkerActivate' | 'onOverlayActivate'> {
|
|
999
|
+
features?: MapFeatures;
|
|
1000
|
+
appearance?: MapAppearance;
|
|
1001
|
+
}
|
|
1002
|
+
export interface MapConfigurationSnapshot {
|
|
1003
|
+
features: MapFeatures;
|
|
1004
|
+
/** Explicit overrides; absent values continue to follow the current mode. */
|
|
1005
|
+
layers: InteractiveLayers;
|
|
1006
|
+
controls: MapControls;
|
|
1007
|
+
}
|
|
1008
|
+
export interface MapEvents {
|
|
1009
|
+
districtchange: DistrictSelection | {
|
|
1010
|
+
id: null;
|
|
1011
|
+
year: DistrictYear;
|
|
1012
|
+
district: null;
|
|
1013
|
+
};
|
|
1014
|
+
districthover: DistrictSelection | {
|
|
1015
|
+
id: null;
|
|
1016
|
+
year: DistrictYear;
|
|
1017
|
+
district: null;
|
|
1018
|
+
};
|
|
1019
|
+
districtactivate: DistrictSelection;
|
|
1020
|
+
districtyearchange: {
|
|
1021
|
+
year: DistrictYear;
|
|
1022
|
+
previousYear: DistrictYear;
|
|
1023
|
+
};
|
|
1024
|
+
markerchange: {
|
|
1025
|
+
id: string | null;
|
|
1026
|
+
marker: MapMarker | null;
|
|
1027
|
+
};
|
|
1028
|
+
neighborhoodchange: NeighborhoodSelection | {
|
|
1029
|
+
id: null;
|
|
1030
|
+
name: null;
|
|
1031
|
+
source: NeighborhoodSource;
|
|
1032
|
+
feature: null;
|
|
1033
|
+
};
|
|
1034
|
+
overlayactivate: {
|
|
1035
|
+
overlay: MapOverlay;
|
|
1036
|
+
};
|
|
1037
|
+
clusteractivate: {
|
|
1038
|
+
markers: MapMarker[];
|
|
1039
|
+
};
|
|
1040
|
+
viewportchange: {
|
|
1041
|
+
viewport: MapViewport;
|
|
1042
|
+
};
|
|
1043
|
+
mapresize: undefined;
|
|
1044
|
+
}
|
|
1045
|
+
export interface MapCamera {
|
|
1046
|
+
get(): MapViewport;
|
|
1047
|
+
set(view: MapViewport, options?: CameraOptions): void;
|
|
1048
|
+
fit(geometry: Geometry, options?: CameraOptions & {
|
|
1049
|
+
padding?: number | MapPadding;
|
|
1050
|
+
}): void;
|
|
1051
|
+
pan(x: number, y: number, options?: CameraOptions): void;
|
|
1052
|
+
zoom(factor: number, options?: CameraOptions): void;
|
|
1053
|
+
reset(options?: CameraOptions): void;
|
|
1054
|
+
stop(): void;
|
|
1055
|
+
}
|
|
1056
|
+
/** Owns the map lifecycle separately from its mountable DOM element. */
|
|
1057
|
+
export interface MapController {
|
|
1058
|
+
readonly element: HTMLElement;
|
|
1059
|
+
readonly overlayElement: HTMLDivElement;
|
|
1060
|
+
readonly camera: MapCamera;
|
|
1061
|
+
readonly destroyed: boolean;
|
|
1062
|
+
configure(patch: MapConfiguration): void;
|
|
1063
|
+
getConfiguration(): MapConfigurationSnapshot;
|
|
1064
|
+
on<K extends keyof MapEvents>(type: K, listener: (detail: MapEvents[K]) => void): () => void;
|
|
1065
|
+
/** Reconcile stable IDs; retained markers preserve nodes, focus, and entrance animations. */
|
|
1066
|
+
setMarkers(markers: readonly MapMarker[]): void;
|
|
1067
|
+
setOverlays(overlays: readonly MapOverlay[]): void;
|
|
1068
|
+
selectMarker(id: string | null, options?: CameraOptions & {
|
|
1069
|
+
fit?: boolean;
|
|
1070
|
+
}): boolean;
|
|
1071
|
+
getSelectedMarker(): MapMarker | null;
|
|
1072
|
+
selectNeighborhood(name: string | null, options?: CameraOptions & {
|
|
1073
|
+
fit?: boolean;
|
|
1074
|
+
}): boolean;
|
|
1075
|
+
getSelectedNeighborhood(): NeighborhoodSelection | null;
|
|
1076
|
+
selectDistrict(id: number | null, options?: CameraOptions & {
|
|
1077
|
+
fit?: boolean;
|
|
1078
|
+
}): boolean;
|
|
1079
|
+
getSelectedDistrict(): DistrictSelection | null;
|
|
1080
|
+
setDistrictYear(year: DistrictYear, options?: CameraOptions): void;
|
|
1081
|
+
setDistrictStyle(style: ((district: DistrictSelection['district']) => DistrictStyle) | undefined): void;
|
|
1082
|
+
setSource(source: NeighborhoodSource): void;
|
|
1083
|
+
setMode(mode: NonNullable<MapOptions['mode']>): void;
|
|
1084
|
+
setLabels(visible: boolean): void;
|
|
1085
|
+
setTouchNavigation(enabled: boolean): void;
|
|
1086
|
+
projectToScreen(lng: number, lat: number): {
|
|
1087
|
+
x: number;
|
|
1088
|
+
y: number;
|
|
1089
|
+
visible: boolean;
|
|
1090
|
+
};
|
|
1091
|
+
/** Idempotent; stops work and managed subscriptions. Does not remove host-owned DOM. */
|
|
1092
|
+
destroy(): void;
|
|
1093
|
+
}
|
|
1094
|
+
```
|
|
1095
|
+
|
|
1096
|
+
### dist/data/types.d.ts
|
|
1097
|
+
|
|
1098
|
+
```ts
|
|
1099
|
+
export type Position = readonly [number, number, ...number[]];
|
|
1100
|
+
export type Bounds = readonly [number, number, number, number];
|
|
1101
|
+
export type Geometry = {
|
|
1102
|
+
readonly type: 'Point';
|
|
1103
|
+
readonly coordinates: Position;
|
|
1104
|
+
} | {
|
|
1105
|
+
readonly type: 'LineString';
|
|
1106
|
+
readonly coordinates: readonly Position[];
|
|
1107
|
+
} | {
|
|
1108
|
+
readonly type: 'MultiPoint';
|
|
1109
|
+
readonly coordinates: readonly Position[];
|
|
1110
|
+
} | {
|
|
1111
|
+
readonly type: 'Polygon';
|
|
1112
|
+
readonly coordinates: readonly (readonly Position[])[];
|
|
1113
|
+
} | {
|
|
1114
|
+
readonly type: 'MultiLineString';
|
|
1115
|
+
readonly coordinates: readonly (readonly Position[])[];
|
|
1116
|
+
} | {
|
|
1117
|
+
readonly type: 'MultiPolygon';
|
|
1118
|
+
readonly coordinates: readonly (readonly (readonly Position[])[])[];
|
|
1119
|
+
} | {
|
|
1120
|
+
readonly type: 'GeometryCollection';
|
|
1121
|
+
readonly geometries: readonly Geometry[];
|
|
1122
|
+
};
|
|
1123
|
+
export interface Feature<P = Readonly<Record<string, unknown>>> {
|
|
1124
|
+
readonly type: 'Feature';
|
|
1125
|
+
readonly id: string;
|
|
1126
|
+
readonly bbox: Bounds;
|
|
1127
|
+
readonly properties: P;
|
|
1128
|
+
readonly geometry: Geometry;
|
|
1129
|
+
}
|
|
1130
|
+
export interface DataSource {
|
|
1131
|
+
readonly id: string;
|
|
1132
|
+
readonly title: string;
|
|
1133
|
+
readonly url: string;
|
|
1134
|
+
readonly retrievedAt: string;
|
|
1135
|
+
readonly definitionYear?: number;
|
|
1136
|
+
readonly licenseUrl: string;
|
|
1137
|
+
}
|
|
1138
|
+
export interface Definition {
|
|
1139
|
+
readonly kind: string;
|
|
1140
|
+
readonly year?: number;
|
|
1141
|
+
readonly description: string;
|
|
1142
|
+
}
|
|
1143
|
+
export interface FeatureCollection<P = Readonly<Record<string, unknown>>> {
|
|
1144
|
+
readonly type: 'FeatureCollection';
|
|
1145
|
+
readonly schemaVersion: 1;
|
|
1146
|
+
readonly id: string;
|
|
1147
|
+
readonly title: string;
|
|
1148
|
+
readonly coordinateSystem: string;
|
|
1149
|
+
readonly definition: Definition;
|
|
1150
|
+
readonly sources: readonly DataSource[];
|
|
1151
|
+
readonly topology?: {
|
|
1152
|
+
readonly policy: 'disjoint-interiors';
|
|
1153
|
+
readonly sharedBoundariesAllowed: boolean;
|
|
1154
|
+
readonly method: string;
|
|
1155
|
+
readonly tool: string;
|
|
1156
|
+
readonly processedAt: string;
|
|
1157
|
+
readonly sourceOverlapPairs: number;
|
|
1158
|
+
};
|
|
1159
|
+
readonly features: readonly Feature<P>[];
|
|
1160
|
+
}
|
|
1161
|
+
export type PointFeatureCollection<P = Readonly<Record<string, unknown>>> = Omit<FeatureCollection<P>, 'features'> & {
|
|
1162
|
+
readonly features: readonly (Feature<P> & {
|
|
1163
|
+
readonly geometry: Extract<Geometry, {
|
|
1164
|
+
readonly type: 'Point';
|
|
1165
|
+
}>;
|
|
1166
|
+
})[];
|
|
1167
|
+
};
|
|
1168
|
+
export type NeighborhoodSource = 'sf-find' | 'analysis' | 'realtor';
|
|
1169
|
+
export interface NeighborhoodProperties {
|
|
1170
|
+
readonly name: string;
|
|
1171
|
+
readonly canonicalName: string;
|
|
1172
|
+
readonly sourceName: string;
|
|
1173
|
+
readonly aliases: readonly string[];
|
|
1174
|
+
readonly definitionSource: NeighborhoodSource;
|
|
1175
|
+
readonly nameSources: readonly string[];
|
|
1176
|
+
readonly note?: string;
|
|
1177
|
+
readonly sourceCode?: string | null;
|
|
1178
|
+
readonly realtorDistrict?: string;
|
|
1179
|
+
}
|
|
1180
|
+
export interface NeighborhoodEntry extends NeighborhoodProperties {
|
|
1181
|
+
readonly id: string;
|
|
1182
|
+
readonly source: NeighborhoodSource;
|
|
1183
|
+
readonly file: string;
|
|
1184
|
+
readonly bbox: Bounds;
|
|
1185
|
+
}
|
|
1186
|
+
export interface DistrictProperties {
|
|
1187
|
+
readonly district: number;
|
|
1188
|
+
readonly name: string;
|
|
1189
|
+
readonly year: 2002 | 2012 | 2022;
|
|
1190
|
+
readonly label: Position;
|
|
1191
|
+
readonly labelPoints: readonly Position[];
|
|
1192
|
+
readonly displayExtras: Geometry | null;
|
|
1193
|
+
}
|
|
1194
|
+
export interface Catalog {
|
|
1195
|
+
readonly schemaVersion: 1;
|
|
1196
|
+
readonly description: string;
|
|
1197
|
+
readonly datasets: readonly {
|
|
1198
|
+
readonly id: string;
|
|
1199
|
+
readonly file: string;
|
|
1200
|
+
readonly title: string;
|
|
1201
|
+
readonly featureCount: number;
|
|
1202
|
+
readonly definition: Definition;
|
|
1203
|
+
readonly sources: readonly DataSource[];
|
|
1204
|
+
}[];
|
|
1205
|
+
readonly neighborhoods: readonly NeighborhoodEntry[];
|
|
1206
|
+
}
|
|
1207
|
+
export type NeighborhoodFeature = Feature<NeighborhoodProperties>;
|
|
1208
|
+
export type PolygonGeometry = Extract<Geometry, {
|
|
1209
|
+
readonly type: 'Polygon';
|
|
1210
|
+
}>;
|
|
1211
|
+
export type MultiPolygonGeometry = Extract<Geometry, {
|
|
1212
|
+
readonly type: 'MultiPolygon';
|
|
1213
|
+
}>;
|
|
1214
|
+
export interface LandmarkProperties {
|
|
1215
|
+
readonly name: string;
|
|
1216
|
+
readonly label: Position;
|
|
1217
|
+
readonly offset: readonly [number, number];
|
|
1218
|
+
readonly anchor: 'middle' | 'start' | 'end';
|
|
1219
|
+
}
|
|
1220
|
+
export interface KeyRoadProperties {
|
|
1221
|
+
readonly name: string;
|
|
1222
|
+
readonly level?: 'primary' | 'secondary';
|
|
1223
|
+
readonly sourceNames: readonly string[];
|
|
1224
|
+
readonly label: Position;
|
|
1225
|
+
readonly segmentIds: readonly string[];
|
|
1226
|
+
}
|
|
1227
|
+
```
|
|
1228
|
+
|
|
1229
|
+
### dist/src/types.d.ts
|
|
1230
|
+
|
|
1231
|
+
```ts
|
|
1232
|
+
import type { Geometry, NeighborhoodFeature, NeighborhoodSource } from '../data/types.js';
|
|
1233
|
+
export type DistrictYear = 2002 | 2012 | 2022;
|
|
1234
|
+
export interface DistrictSelection {
|
|
1235
|
+
id: number;
|
|
1236
|
+
year: DistrictYear;
|
|
1237
|
+
district: import('./map-core.js').DistrictRowData;
|
|
1238
|
+
}
|
|
1239
|
+
export interface DistrictStyle {
|
|
1240
|
+
fill?: string;
|
|
1241
|
+
stroke?: string;
|
|
1242
|
+
opacity?: number;
|
|
1243
|
+
}
|
|
1244
|
+
export interface MapMarker {
|
|
1245
|
+
id: string;
|
|
1246
|
+
lng: number;
|
|
1247
|
+
lat: number;
|
|
1248
|
+
label?: string;
|
|
1249
|
+
selected?: boolean;
|
|
1250
|
+
color?: string;
|
|
1251
|
+
/** Visible radius in screen pixels (interactive), SVG units (static). */
|
|
1252
|
+
radius?: number;
|
|
1253
|
+
}
|
|
1254
|
+
export interface MapOverlay {
|
|
1255
|
+
id: string;
|
|
1256
|
+
geometry: Extract<Geometry, {
|
|
1257
|
+
type: 'LineString' | 'MultiLineString' | 'Polygon' | 'MultiPolygon';
|
|
1258
|
+
}>;
|
|
1259
|
+
stroke?: string;
|
|
1260
|
+
strokeWidth?: number;
|
|
1261
|
+
fill?: string;
|
|
1262
|
+
fillOpacity?: number;
|
|
1263
|
+
visible?: boolean;
|
|
1264
|
+
label?: string;
|
|
1265
|
+
}
|
|
1266
|
+
export interface SFMapOptions {
|
|
1267
|
+
theme?: 'districts' | 'transit';
|
|
1268
|
+
width?: number;
|
|
1269
|
+
height?: number;
|
|
1270
|
+
padding?: number;
|
|
1271
|
+
year?: DistrictYear;
|
|
1272
|
+
districtLines?: boolean;
|
|
1273
|
+
neighborhoodLines?: boolean;
|
|
1274
|
+
districtFills?: boolean;
|
|
1275
|
+
/** Evaluated once per district at construction/style/year updates; call setDistrictStyle again when external data changes. */
|
|
1276
|
+
districtStyle?: (district: import('./map-core.js').DistrictRowData) => DistrictStyle;
|
|
1277
|
+
districtLabels?: boolean;
|
|
1278
|
+
/** Hide all visible text labels while retaining geographic symbols and accessible titles. */
|
|
1279
|
+
labels?: boolean;
|
|
1280
|
+
highways?: boolean;
|
|
1281
|
+
keyRoads?: boolean;
|
|
1282
|
+
roadLabels?: boolean;
|
|
1283
|
+
landmarks?: boolean;
|
|
1284
|
+
bartStations?: boolean;
|
|
1285
|
+
markers?: MapMarker[];
|
|
1286
|
+
overlays?: readonly MapOverlay[];
|
|
1287
|
+
title?: string;
|
|
1288
|
+
idPrefix?: string;
|
|
1289
|
+
/**
|
|
1290
|
+
* Opt-in, self-contained CSS choreography for static SVGs: land fades in, the coast and
|
|
1291
|
+
* lines draw, districts grow, then labels and points appear. It plays when the SVG is
|
|
1292
|
+
* inserted into a page or loaded as an image, and stays still under reduced motion.
|
|
1293
|
+
* `duration` (default 2400 ms) scales the whole sequence; `delay` offsets it.
|
|
1294
|
+
*/
|
|
1295
|
+
animation?: boolean | {
|
|
1296
|
+
duration?: number;
|
|
1297
|
+
delay?: number;
|
|
1298
|
+
};
|
|
1299
|
+
colors?: Partial<Record<'water' | 'land' | 'district' | 'neighborhood' | 'highway' | 'road' | 'park' | 'landmark' | 'bart' | 'label' | 'marker' | 'selected', string>>;
|
|
1300
|
+
}
|
|
1301
|
+
/** Serializable viewport in the interactive map's fixed 800 × 800 projected space. */
|
|
1302
|
+
export type MapViewport = readonly [x: number, y: number, size: number];
|
|
1303
|
+
export type ExplorerMode = 'neighborhoods' | 'districts' | 'basemap';
|
|
1304
|
+
export interface MapPadding {
|
|
1305
|
+
top?: number;
|
|
1306
|
+
right?: number;
|
|
1307
|
+
bottom?: number;
|
|
1308
|
+
left?: number;
|
|
1309
|
+
}
|
|
1310
|
+
export interface InteractiveLayers {
|
|
1311
|
+
districtFills?: boolean;
|
|
1312
|
+
districtLines?: boolean;
|
|
1313
|
+
districtLabels?: boolean;
|
|
1314
|
+
neighborhoodLines?: boolean;
|
|
1315
|
+
neighborhoodLabels?: boolean;
|
|
1316
|
+
landmarks?: boolean;
|
|
1317
|
+
bartStations?: boolean;
|
|
1318
|
+
highways?: boolean;
|
|
1319
|
+
keyRoads?: boolean;
|
|
1320
|
+
roadLabels?: boolean;
|
|
1321
|
+
}
|
|
1322
|
+
export interface NeighborhoodSelection {
|
|
1323
|
+
id: string;
|
|
1324
|
+
name: string;
|
|
1325
|
+
source: NeighborhoodSource;
|
|
1326
|
+
feature: NeighborhoodFeature;
|
|
1327
|
+
}
|
|
1328
|
+
export interface MapFeatures {
|
|
1329
|
+
/** Opt-in camera motion; reduced-motion always takes precedence. */
|
|
1330
|
+
motion?: boolean | {
|
|
1331
|
+
duration?: number;
|
|
1332
|
+
};
|
|
1333
|
+
markerEntrance?: boolean | {
|
|
1334
|
+
duration?: number;
|
|
1335
|
+
stagger?: number;
|
|
1336
|
+
};
|
|
1337
|
+
selectedMarkerRing?: boolean | {
|
|
1338
|
+
color?: string;
|
|
1339
|
+
width?: number;
|
|
1340
|
+
gap?: number;
|
|
1341
|
+
};
|
|
1342
|
+
/** Screen-space clustering. The selected marker and full chooser remain available. */
|
|
1343
|
+
clustering?: boolean | {
|
|
1344
|
+
radius?: number;
|
|
1345
|
+
};
|
|
1346
|
+
northArrow?: boolean;
|
|
1347
|
+
scaleBar?: boolean;
|
|
1348
|
+
/**
|
|
1349
|
+
* Fade layers as `setLayers` and `setMode` switch them, crossfade neighborhood boundaries
|
|
1350
|
+
* when `setSource` changes the definition, and crossfade district fills on
|
|
1351
|
+
* `setDistrictStyle`. Reduced motion takes precedence.
|
|
1352
|
+
*/
|
|
1353
|
+
layerTransitions?: boolean | {
|
|
1354
|
+
duration?: number;
|
|
1355
|
+
};
|
|
1356
|
+
/**
|
|
1357
|
+
* Morph district outlines from one map year to the next on `setDistrictYear`; pass
|
|
1358
|
+
* `{ animate: false }` for an instant change. Reduced motion takes precedence.
|
|
1359
|
+
*/
|
|
1360
|
+
districtMorph?: boolean | {
|
|
1361
|
+
duration?: number;
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
export interface NeighborhoodExplorerOptions extends MapFeatures {
|
|
1365
|
+
mode?: ExplorerMode;
|
|
1366
|
+
labels?: boolean;
|
|
1367
|
+
source?: NeighborhoodSource;
|
|
1368
|
+
neighborhood?: string;
|
|
1369
|
+
year?: DistrictYear;
|
|
1370
|
+
theme?: SFMapOptions['theme'];
|
|
1371
|
+
colors?: SFMapOptions['colors'];
|
|
1372
|
+
labelStyle?: {
|
|
1373
|
+
fontFamily?: string;
|
|
1374
|
+
fontWeight?: number;
|
|
1375
|
+
haloColor?: string;
|
|
1376
|
+
};
|
|
1377
|
+
areaStyle?: {
|
|
1378
|
+
selectedFill?: string;
|
|
1379
|
+
selectedStroke?: string;
|
|
1380
|
+
hoverFill?: string;
|
|
1381
|
+
hoverStroke?: string;
|
|
1382
|
+
};
|
|
1383
|
+
districtStyle?: SFMapOptions['districtStyle'];
|
|
1384
|
+
legend?: {
|
|
1385
|
+
builtins?: boolean;
|
|
1386
|
+
hidden?: readonly ('bart' | 'park' | 'highway' | 'road')[];
|
|
1387
|
+
items?: readonly {
|
|
1388
|
+
label: string;
|
|
1389
|
+
color: string;
|
|
1390
|
+
}[];
|
|
1391
|
+
};
|
|
1392
|
+
attribution?: 'full' | 'compact';
|
|
1393
|
+
/** Explorer chrome or the reusable map with only controls and attribution. */
|
|
1394
|
+
interface?: 'explorer' | 'map';
|
|
1395
|
+
/** Independent overrides; omitted layers follow mode defaults. */
|
|
1396
|
+
layers?: InteractiveLayers;
|
|
1397
|
+
selectableNeighborhoods?: boolean;
|
|
1398
|
+
/** Screen pixels; defaults preserve 11px roads / 12px other labels. */
|
|
1399
|
+
labelSize?: {
|
|
1400
|
+
min?: number;
|
|
1401
|
+
max?: number;
|
|
1402
|
+
};
|
|
1403
|
+
/** Screen-pixel space reserved when fitting geometry. */
|
|
1404
|
+
fitPadding?: number | MapPadding;
|
|
1405
|
+
markers?: readonly MapMarker[];
|
|
1406
|
+
/** Screen pixels, independent of zoom. */
|
|
1407
|
+
markerRadius?: number;
|
|
1408
|
+
markerHitSize?: number;
|
|
1409
|
+
markerColor?: string;
|
|
1410
|
+
selectedMarkerColor?: string;
|
|
1411
|
+
onMarkerActivate?: (marker: MapMarker) => void;
|
|
1412
|
+
overlays?: readonly MapOverlay[];
|
|
1413
|
+
onOverlayActivate?: (overlay: MapOverlay) => void;
|
|
1414
|
+
/** Stable theme tokens consumed by the explorer chrome. */
|
|
1415
|
+
style?: Partial<Record<'ink' | 'surface' | 'accent' | 'border' | 'focus' | 'controlGap' | 'font', string>>;
|
|
1416
|
+
strings?: Partial<Record<'title' | 'mode' | 'source' | 'search' | 'chooseNeighborhood' | 'chooseMarker' | 'touchNavigation' | 'touchNavigationLabel' | 'touchNavigationExitLabel' | 'touchNavigationDone' | 'reset' | 'emptyResults' | 'gestureHelp', string>>;
|
|
1417
|
+
/**
|
|
1418
|
+
* Independently hide chrome. `neighborhoodPicker` and `markerPicker` hide the native
|
|
1419
|
+
* choosers (supply your own accessible list); `help` keeps the gesture help as the map's
|
|
1420
|
+
* accessible description; `status` keeps a visually hidden live region. Attribution stays.
|
|
1421
|
+
*/
|
|
1422
|
+
controls?: Partial<Record<'zoom' | 'pan' | 'reset' | 'labels' | 'touch' | 'legend' | 'neighborhoodPicker' | 'markerPicker' | 'help' | 'status', boolean>>;
|
|
1423
|
+
}
|
|
1424
|
+
export interface CameraOptions {
|
|
1425
|
+
animate?: boolean;
|
|
1426
|
+
duration?: number;
|
|
1427
|
+
}
|
|
1428
|
+
export interface NeighborhoodExplorerElement extends HTMLElement {
|
|
1429
|
+
/** Append positioned HTML children here; coordinates are relative to this layer. */
|
|
1430
|
+
readonly overlayElement: HTMLDivElement;
|
|
1431
|
+
projectToScreen(lng: number, lat: number): {
|
|
1432
|
+
x: number;
|
|
1433
|
+
y: number;
|
|
1434
|
+
visible: boolean;
|
|
1435
|
+
};
|
|
1436
|
+
stopAnimation(): void;
|
|
1437
|
+
/** Atomic patch; false disables, undefined resets a feature to its default. */
|
|
1438
|
+
setFeatures(patch: MapFeatures): void;
|
|
1439
|
+
getFeatures(): MapFeatures;
|
|
1440
|
+
/** Layer overrides; undefined restores mode defaults. Preserves camera and selection. */
|
|
1441
|
+
setLayers(patch: InteractiveLayers): void;
|
|
1442
|
+
/** Chrome switches are independent. */
|
|
1443
|
+
setControls(patch: NonNullable<NeighborhoodExplorerOptions['controls']>): void;
|
|
1444
|
+
selectNeighborhood(name: string | null, options?: {
|
|
1445
|
+
fit?: boolean;
|
|
1446
|
+
} & CameraOptions): boolean;
|
|
1447
|
+
getSelection(): NeighborhoodSelection | null;
|
|
1448
|
+
selectDistrict(id: number | null, options?: {
|
|
1449
|
+
fit?: boolean;
|
|
1450
|
+
} & CameraOptions): boolean;
|
|
1451
|
+
getSelectedDistrict(): DistrictSelection | null;
|
|
1452
|
+
setDistrictYear(year: DistrictYear, options?: CameraOptions): void;
|
|
1453
|
+
setDistrictStyle(style: SFMapOptions['districtStyle']): void;
|
|
1454
|
+
setSource(source: NeighborhoodSource): void;
|
|
1455
|
+
setMode(mode: ExplorerMode): void;
|
|
1456
|
+
setLabels(visible: boolean): void;
|
|
1457
|
+
resetView(options?: CameraOptions): void;
|
|
1458
|
+
zoomBy(factor: number, options?: CameraOptions): void;
|
|
1459
|
+
panBy(x: number, y: number, options?: CameraOptions): void;
|
|
1460
|
+
getViewport(): MapViewport;
|
|
1461
|
+
setViewport(view: MapViewport, options?: CameraOptions): void;
|
|
1462
|
+
/** Fit WGS84 geometry, including Point, MultiPoint, or a GeometryCollection. */
|
|
1463
|
+
fitGeometry(geometry: Geometry, padding?: number | MapPadding, options?: CameraOptions): void;
|
|
1464
|
+
/** Explicitly engage map touch gestures; false restores page gestures. */
|
|
1465
|
+
setTouchNavigation(enabled: boolean): void;
|
|
1466
|
+
/** Reconcile stable IDs; retained markers preserve nodes, focus, and entrance animations. */
|
|
1467
|
+
setMarkers(markers: readonly MapMarker[]): void;
|
|
1468
|
+
setOverlays(overlays: readonly MapOverlay[]): void;
|
|
1469
|
+
selectMarker(id: string | null, options?: {
|
|
1470
|
+
fit?: boolean;
|
|
1471
|
+
} & CameraOptions): boolean;
|
|
1472
|
+
getSelectedMarker(): MapMarker | null;
|
|
1473
|
+
destroy(): void;
|
|
1474
|
+
}
|
|
1475
|
+
/** Paused-by-default schematic BART animation with keyboard-operable controls. */
|
|
1476
|
+
export interface TransitAnimationElement extends HTMLElement {
|
|
1477
|
+
destroy(): void;
|
|
1478
|
+
}
|
|
1479
|
+
export type { MapAppearance, MapCamera, MapConfiguration, MapController, MapEvents, MapOptions, } from './controller-types.js';
|
|
1480
|
+
```
|
|
1481
|
+
|
|
1482
|
+
### dist/src/explorer-data.d.ts
|
|
1483
|
+
|
|
1484
|
+
```ts
|
|
1485
|
+
import type { DistrictProperties, FeatureCollection, NeighborhoodProperties, NeighborhoodSource } from '../data/types.js';
|
|
1486
|
+
import type { SFMapData } from './map-core.js';
|
|
1487
|
+
import type { DistrictYear } from './types.js';
|
|
1488
|
+
/** Geographic inputs for the data-injected interactive map entry point. */
|
|
1489
|
+
export interface InteractiveSFMapData {
|
|
1490
|
+
map: SFMapData;
|
|
1491
|
+
districts?: Partial<Readonly<Record<DistrictYear, FeatureCollection<DistrictProperties>>>>;
|
|
1492
|
+
neighborhoods: Partial<Readonly<Record<NeighborhoodSource, FeatureCollection<NeighborhoodProperties>>>>;
|
|
1493
|
+
}
|
|
1494
|
+
```
|
|
1495
|
+
|
|
1496
|
+
### dist/src/map-core.d.ts
|
|
1497
|
+
|
|
1498
|
+
```ts
|
|
1499
|
+
import type { Geometry, Position } from '../data/types.js';
|
|
1500
|
+
import type { DistrictYear, SFMapOptions } from './types.js';
|
|
1501
|
+
export type { DistrictYear, MapMarker, MapOverlay, SFMapOptions } from './types.js';
|
|
1502
|
+
export interface SFMapData {
|
|
1503
|
+
coast: Geometry;
|
|
1504
|
+
districts?: Readonly<Partial<Record<DistrictYear, readonly DistrictRowData[]>>>;
|
|
1505
|
+
neighborhoods?: readonly {
|
|
1506
|
+
name: string;
|
|
1507
|
+
geometry: Geometry;
|
|
1508
|
+
}[];
|
|
1509
|
+
highways?: readonly {
|
|
1510
|
+
route: string;
|
|
1511
|
+
geometry: Geometry;
|
|
1512
|
+
}[];
|
|
1513
|
+
landmarks?: readonly LandmarkData[];
|
|
1514
|
+
keyRoads?: readonly KeyRoadData[];
|
|
1515
|
+
bartStations?: readonly BartStationData[];
|
|
1516
|
+
}
|
|
1517
|
+
export interface DistrictRowData {
|
|
1518
|
+
id: number;
|
|
1519
|
+
label: Position;
|
|
1520
|
+
labelPoints: readonly Position[];
|
|
1521
|
+
geometry: Geometry;
|
|
1522
|
+
extras: Geometry | null;
|
|
1523
|
+
}
|
|
1524
|
+
export interface LandmarkData {
|
|
1525
|
+
id: string;
|
|
1526
|
+
name: string;
|
|
1527
|
+
label: Position;
|
|
1528
|
+
offset: readonly [number, number];
|
|
1529
|
+
anchor: 'middle' | 'start' | 'end';
|
|
1530
|
+
geometry: Geometry;
|
|
1531
|
+
}
|
|
1532
|
+
export interface KeyRoadData {
|
|
1533
|
+
id: string;
|
|
1534
|
+
name: string;
|
|
1535
|
+
level?: 'primary' | 'secondary';
|
|
1536
|
+
sourceNames: readonly string[];
|
|
1537
|
+
label: Position;
|
|
1538
|
+
segmentIds: readonly string[];
|
|
1539
|
+
geometry: Geometry;
|
|
1540
|
+
}
|
|
1541
|
+
export interface BartStationData {
|
|
1542
|
+
id: string;
|
|
1543
|
+
name: string;
|
|
1544
|
+
coordinates: Position;
|
|
1545
|
+
}
|
|
1546
|
+
export declare const districtYears: readonly DistrictYear[];
|
|
1547
|
+
export declare const districtColors: readonly string[];
|
|
1548
|
+
/** Canonical geometry in the same fitted coordinate space used by renderMap. */
|
|
1549
|
+
export declare function getLayerPathsWithData(options: SFMapOptions, data: SFMapData, complete?: boolean): {
|
|
1550
|
+
year: DistrictYear;
|
|
1551
|
+
viewBox: [number, number, number, number];
|
|
1552
|
+
project: (coordinates: Position) => [number, number];
|
|
1553
|
+
coast: string;
|
|
1554
|
+
districts: {
|
|
1555
|
+
id: number;
|
|
1556
|
+
geometry: string;
|
|
1557
|
+
extras: string;
|
|
1558
|
+
path: string;
|
|
1559
|
+
}[];
|
|
1560
|
+
neighborhoods: {
|
|
1561
|
+
name: string;
|
|
1562
|
+
path: string;
|
|
1563
|
+
}[];
|
|
1564
|
+
highways: {
|
|
1565
|
+
route: string;
|
|
1566
|
+
path: string;
|
|
1567
|
+
}[];
|
|
1568
|
+
landmarks: {
|
|
1569
|
+
id: string;
|
|
1570
|
+
path: string;
|
|
1571
|
+
}[];
|
|
1572
|
+
keyRoads: {
|
|
1573
|
+
id: string;
|
|
1574
|
+
path: string;
|
|
1575
|
+
}[];
|
|
1576
|
+
bartStations: {
|
|
1577
|
+
id: string;
|
|
1578
|
+
point: [number, number];
|
|
1579
|
+
}[];
|
|
1580
|
+
};
|
|
1581
|
+
/** Make an offline SVG and the matching longitude/latitude projection. */
|
|
1582
|
+
export declare function createSFMapWithData(options: SFMapOptions, data: SFMapData): {
|
|
1583
|
+
svg: string;
|
|
1584
|
+
project: (coordinates: Position) => [number, number];
|
|
1585
|
+
viewBox: [number, number, number, number];
|
|
1586
|
+
};
|
|
1587
|
+
```
|
|
1588
|
+
|
|
1589
|
+
### dist/src/static.d.ts
|
|
1590
|
+
|
|
1591
|
+
```ts
|
|
1592
|
+
import { type SFMapData } from './map-core.js';
|
|
1593
|
+
import type { SFMapOptions } from './types.js';
|
|
1594
|
+
export type { DistrictRowData, SFMapData as StaticMapData } from './map-core.js';
|
|
1595
|
+
export type { DistrictStyle, DistrictYear, SFMapOptions as StaticMapOptions } from './types.js';
|
|
1596
|
+
/** Server-safe SVG rendering with explicit data; returns SVG plus projection helpers. */
|
|
1597
|
+
export declare function renderMap(data: SFMapData, options?: SFMapOptions): {
|
|
1598
|
+
svg: string;
|
|
1599
|
+
project: (coordinates: import("../data/types.js").Position) => [number, number];
|
|
1600
|
+
viewBox: [number, number, number, number];
|
|
1601
|
+
};
|
|
1602
|
+
/** Project canonical layer geometry without constructing an SVG string. */
|
|
1603
|
+
export declare function getLayerPaths(data: SFMapData, options?: SFMapOptions): {
|
|
1604
|
+
year: import("./types.js").DistrictYear;
|
|
1605
|
+
viewBox: [number, number, number, number];
|
|
1606
|
+
project: (coordinates: import("../data/types.js").Position) => [number, number];
|
|
1607
|
+
coast: string;
|
|
1608
|
+
districts: {
|
|
1609
|
+
id: number;
|
|
1610
|
+
geometry: string;
|
|
1611
|
+
extras: string;
|
|
1612
|
+
path: string;
|
|
1613
|
+
}[];
|
|
1614
|
+
neighborhoods: {
|
|
1615
|
+
name: string;
|
|
1616
|
+
path: string;
|
|
1617
|
+
}[];
|
|
1618
|
+
highways: {
|
|
1619
|
+
route: string;
|
|
1620
|
+
path: string;
|
|
1621
|
+
}[];
|
|
1622
|
+
landmarks: {
|
|
1623
|
+
id: string;
|
|
1624
|
+
path: string;
|
|
1625
|
+
}[];
|
|
1626
|
+
keyRoads: {
|
|
1627
|
+
id: string;
|
|
1628
|
+
path: string;
|
|
1629
|
+
}[];
|
|
1630
|
+
bartStations: {
|
|
1631
|
+
id: string;
|
|
1632
|
+
point: [number, number];
|
|
1633
|
+
}[];
|
|
1634
|
+
};
|
|
1635
|
+
```
|
|
1636
|
+
|
|
1637
|
+
### ./dist/data/index.d.ts
|
|
1638
|
+
|
|
1639
|
+
```ts
|
|
1640
|
+
export { catalog, searchNeighborhoods } from './catalog.js';
|
|
1641
|
+
export { coast } from './coast.js';
|
|
1642
|
+
export { districtMaps } from './districts.js';
|
|
1643
|
+
export { highways } from './highways.js';
|
|
1644
|
+
export { landmarks } from './landmarks.js';
|
|
1645
|
+
export { getNeighborhood, neighborhoodCollections, neighborhoodSources } from './lookup.js';
|
|
1646
|
+
export { getRealtorNeighborhood, neighborhoods } from './realtor.js';
|
|
1647
|
+
export { keyRoads } from './roads.js';
|
|
1648
|
+
export { bartStations } from './stations.js';
|
|
1649
|
+
export type * from './types.js';
|
|
1650
|
+
```
|
|
1651
|
+
|
|
1652
|
+
### dist/data/catalog.d.ts
|
|
1653
|
+
|
|
1654
|
+
```ts
|
|
1655
|
+
import type { Catalog, NeighborhoodSource } from './types.js';
|
|
1656
|
+
export declare const catalog: Catalog;
|
|
1657
|
+
/** Search name metadata across sources without importing polygon geometry. */
|
|
1658
|
+
export declare function searchNeighborhoods(query?: string, { source }?: {
|
|
1659
|
+
source?: NeighborhoodSource;
|
|
1660
|
+
}): import("./types.js").NeighborhoodEntry[];
|
|
1661
|
+
```
|
|
1662
|
+
|
|
1663
|
+
### dist/data/coast.d.ts
|
|
1664
|
+
|
|
1665
|
+
```ts
|
|
1666
|
+
import type { FeatureCollection } from './types.js';
|
|
1667
|
+
export declare const coast: FeatureCollection<{
|
|
1668
|
+
readonly name: string;
|
|
1669
|
+
}>;
|
|
1670
|
+
```
|
|
1671
|
+
|
|
1672
|
+
### dist/data/districts.d.ts
|
|
1673
|
+
|
|
1674
|
+
```ts
|
|
1675
|
+
import type { DistrictProperties, FeatureCollection } from './types.js';
|
|
1676
|
+
export declare const districtMaps: Readonly<Record<2002 | 2012 | 2022, FeatureCollection<DistrictProperties>>>;
|
|
1677
|
+
```
|
|
1678
|
+
|
|
1679
|
+
### dist/data/highways.d.ts
|
|
1680
|
+
|
|
1681
|
+
```ts
|
|
1682
|
+
import type { FeatureCollection } from './types.js';
|
|
1683
|
+
export declare const highways: FeatureCollection<{
|
|
1684
|
+
readonly route: string;
|
|
1685
|
+
}>;
|
|
1686
|
+
```
|
|
1687
|
+
|
|
1688
|
+
### dist/data/landmarks.d.ts
|
|
1689
|
+
|
|
1690
|
+
```ts
|
|
1691
|
+
import type { FeatureCollection, LandmarkProperties } from './types.js';
|
|
1692
|
+
export declare const landmarks: FeatureCollection<LandmarkProperties>;
|
|
1693
|
+
```
|
|
1694
|
+
|
|
1695
|
+
### dist/data/lookup.d.ts
|
|
1696
|
+
|
|
1697
|
+
```ts
|
|
1698
|
+
import type { FeatureCollection, NeighborhoodProperties, NeighborhoodSource } from './types.js';
|
|
1699
|
+
export declare const neighborhoodCollections: Readonly<Record<NeighborhoodSource, FeatureCollection<NeighborhoodProperties>>>;
|
|
1700
|
+
export declare const neighborhoodSources: readonly NeighborhoodSource[];
|
|
1701
|
+
/** Exact ID, canonical name, source name, or alias lookup within one definition set. */
|
|
1702
|
+
export declare function getNeighborhood(name: string, { source }?: {
|
|
1703
|
+
source?: NeighborhoodSource;
|
|
1704
|
+
}): import("./types.js").Feature<NeighborhoodProperties> | undefined;
|
|
1705
|
+
```
|
|
1706
|
+
|
|
1707
|
+
### dist/data/realtor.d.ts
|
|
1708
|
+
|
|
1709
|
+
```ts
|
|
1710
|
+
import type { FeatureCollection, NeighborhoodProperties } from './types.js';
|
|
1711
|
+
export declare const neighborhoods: FeatureCollection<NeighborhoodProperties>;
|
|
1712
|
+
/** Look up one SFAR area without importing the other neighborhood definitions. */
|
|
1713
|
+
export declare function getRealtorNeighborhood(name: string): import("./types.js").Feature<NeighborhoodProperties> | undefined;
|
|
1714
|
+
```
|
|
1715
|
+
|
|
1716
|
+
### dist/data/roads.d.ts
|
|
1717
|
+
|
|
1718
|
+
```ts
|
|
1719
|
+
import type { FeatureCollection, KeyRoadProperties } from './types.js';
|
|
1720
|
+
export declare const keyRoads: FeatureCollection<KeyRoadProperties>;
|
|
1721
|
+
```
|
|
1722
|
+
|
|
1723
|
+
### dist/data/stations.d.ts
|
|
1724
|
+
|
|
1725
|
+
```ts
|
|
1726
|
+
import type { PointFeatureCollection } from './types.js';
|
|
1727
|
+
export declare const bartStations: PointFeatureCollection<{
|
|
1728
|
+
readonly name: string;
|
|
1729
|
+
}>;
|
|
1730
|
+
```
|
|
1731
|
+
|
|
1732
|
+
### ./dist/data/analysis.d.ts
|
|
1733
|
+
|
|
1734
|
+
```ts
|
|
1735
|
+
import type { FeatureCollection, NeighborhoodProperties } from './types.js';
|
|
1736
|
+
export declare const analysisNeighborhoods: FeatureCollection<NeighborhoodProperties>;
|
|
1737
|
+
```
|
|
1738
|
+
|
|
1739
|
+
### ./dist/src/full-data.d.ts
|
|
1740
|
+
|
|
1741
|
+
```ts
|
|
1742
|
+
import type { MapData } from './map.js';
|
|
1743
|
+
/** Complete, explicit geographic preset. Import this only when all datasets are needed. */
|
|
1744
|
+
export declare const fullMapData: MapData;
|
|
1745
|
+
```
|
|
1746
|
+
|
|
1747
|
+
### ./dist/src/static-data.d.ts
|
|
1748
|
+
|
|
1749
|
+
```ts
|
|
1750
|
+
import type { StaticMapData } from './static.js';
|
|
1751
|
+
/** Complete data for static rendering, without interactive lookup collections. */
|
|
1752
|
+
export declare const staticMapData: StaticMapData;
|
|
1753
|
+
```
|
|
1754
|
+
|
|
1755
|
+
### ./dist/data/sf-find.d.ts
|
|
1756
|
+
|
|
1757
|
+
```ts
|
|
1758
|
+
import type { FeatureCollection, NeighborhoodProperties } from './types.js';
|
|
1759
|
+
export declare const sfFindNeighborhoods: FeatureCollection<NeighborhoodProperties>;
|
|
1760
|
+
```
|
|
1761
|
+
|
|
1762
|
+
### ./dist/src/geometry.d.ts
|
|
1763
|
+
|
|
1764
|
+
```ts
|
|
1765
|
+
import type { Geometry, Position } from '../data/types.js';
|
|
1766
|
+
export type Project = (position: Position) => [number, number];
|
|
1767
|
+
export declare function rawProject([longitude, latitude]: Position): [number, number];
|
|
1768
|
+
export declare function positions(geometry: Geometry | null | undefined): Position[];
|
|
1769
|
+
export declare function geometryPath(geometry: Geometry | null | undefined, project: Project): string;
|
|
1770
|
+
```
|
|
1771
|
+
|
|
1772
|
+
### ./dist/src/guide.d.ts
|
|
1773
|
+
|
|
1774
|
+
```ts
|
|
1775
|
+
export type { MapController, MapOptions } from './controller-types.js';
|
|
1776
|
+
export type { InteractiveSFMapData } from './explorer-data.js';
|
|
1777
|
+
export { guideMapData } from './guide-data.js';
|
|
1778
|
+
export { loadGuideDetailedData } from './guide-detailed.js';
|
|
1779
|
+
export { createGuideController, createGuideMap, mountGuideController, mountGuideMap, } from './guide-map.js';
|
|
1780
|
+
export type { CameraOptions, MapFeatures, MapMarker, NeighborhoodExplorerElement as InteractiveSFMapElement, NeighborhoodExplorerOptions as InteractiveSFMapOptions, } from './types.js';
|
|
1781
|
+
```
|
|
1782
|
+
|
|
1783
|
+
### dist/src/guide-data.d.ts
|
|
1784
|
+
|
|
1785
|
+
```ts
|
|
1786
|
+
import type { InteractiveSFMapData } from './explorer-data.js';
|
|
1787
|
+
/** Compact, source-aware geography with no district or alternate-neighborhood files. */
|
|
1788
|
+
export declare const guideMapData: InteractiveSFMapData;
|
|
1789
|
+
```
|
|
1790
|
+
|
|
1791
|
+
### dist/src/guide-detailed.d.ts
|
|
1792
|
+
|
|
1793
|
+
```ts
|
|
1794
|
+
import type { InteractiveSFMapData } from './explorer-data.js';
|
|
1795
|
+
/** Load detailed selected geography only after the caller requests it. */
|
|
1796
|
+
export declare function loadGuideDetailedData(): Promise<InteractiveSFMapData>;
|
|
1797
|
+
```
|
|
1798
|
+
|
|
1799
|
+
### dist/src/guide-map.d.ts
|
|
1800
|
+
|
|
1801
|
+
```ts
|
|
1802
|
+
import type { MapController, MapOptions } from './controller-types.js';
|
|
1803
|
+
import type { NeighborhoodExplorerElement as InteractiveSFMapElement, NeighborhoodExplorerOptions as InteractiveSFMapOptions } from './types.js';
|
|
1804
|
+
/** Preferred guide API: the same grouped options, events, camera and lifecycle as createMap. */
|
|
1805
|
+
export declare function createGuideController(options?: MapOptions): MapController;
|
|
1806
|
+
/** Enhance a static guide shell and return its owning controller. */
|
|
1807
|
+
export declare function mountGuideController(shell: HTMLElement, options?: MapOptions): MapController;
|
|
1808
|
+
/** Element-based compatibility API. Prefer createGuideController for new integrations. */
|
|
1809
|
+
export declare function createGuideMap(options?: InteractiveSFMapOptions): InteractiveSFMapElement;
|
|
1810
|
+
/** Enhance createGuideShell() in place with a fixed compact chrome layout.
|
|
1811
|
+
* Keep an accessible external place list when hiding the native pickers.
|
|
1812
|
+
*/
|
|
1813
|
+
export declare function mountGuideMap(shell: HTMLElement, options?: InteractiveSFMapOptions): InteractiveSFMapElement;
|
|
1814
|
+
```
|
|
1815
|
+
|
|
1816
|
+
### ./dist/src/transit.d.ts
|
|
1817
|
+
|
|
1818
|
+
```ts
|
|
1819
|
+
import type { TransitAnimationElement } from './types.js';
|
|
1820
|
+
/** A deliberately schematic, offline station-to-station animation. Starts paused. */
|
|
1821
|
+
export declare function createTransitAnimation(): TransitAnimationElement;
|
|
1822
|
+
```
|
|
1823
|
+
|
|
1824
|
+
### ./dist/src/guide-static.d.ts
|
|
1825
|
+
|
|
1826
|
+
```ts
|
|
1827
|
+
import type { SFMapOptions } from './types.js';
|
|
1828
|
+
/** Server-safe overview using exactly the guide browser geography. */
|
|
1829
|
+
export declare function createGuideSVG(options?: SFMapOptions): {
|
|
1830
|
+
svg: string;
|
|
1831
|
+
project: (coordinates: import("../data/types.js").Position) => [number, number];
|
|
1832
|
+
viewBox: [number, number, number, number];
|
|
1833
|
+
};
|
|
1834
|
+
/** Stable compact frame, progressively enhanced with mountGuideMap(). No browser globals. */
|
|
1835
|
+
export declare function createGuideShell(options?: SFMapOptions): string;
|
|
1836
|
+
```
|
|
1837
|
+
|
|
1838
|
+
### ./dist/src/presets.d.ts
|
|
1839
|
+
|
|
1840
|
+
```ts
|
|
1841
|
+
import type { MapOptions } from './controller-types.js';
|
|
1842
|
+
/** Configuration only: pair with guide/data or a compatible caller-supplied dataset. */
|
|
1843
|
+
export declare const guideOptions: Readonly<MapOptions>;
|
|
1844
|
+
```
|