@kahwee/sf-map-svg 2.2.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +37 -624
- package/dist/data/README.md +3 -2
- package/dist/src/full-data.d.ts +3 -0
- package/dist/src/full-data.js +9 -0
- package/dist/src/guide-map.d.ts +1 -1
- package/dist/src/guide-map.js +4 -3
- package/dist/src/guide.d.ts +1 -2
- package/dist/src/map.d.ts +1 -1
- package/dist/src/map.js +5 -3
- package/dist/src/static-data.d.ts +3 -0
- package/dist/src/static-data.js +23 -0
- package/docs/EXAMPLES.md +11 -9
- package/docs/api-audit.md +5 -14
- package/docs/consumer-integration.md +12 -159
- package/docs/guide-bundle-report.md +5 -7
- package/docs/migration-v3.md +30 -0
- package/package.json +10 -22
- package/dist/src/custom-map.d.ts +0 -3
- package/dist/src/custom-map.js +0 -1
- package/dist/src/explorer.d.ts +0 -4
- package/dist/src/explorer.js +0 -30
- package/dist/src/index.d.ts +0 -12
- package/dist/src/index.js +0 -21
- package/dist/src/interactive-data.d.ts +0 -6
- package/dist/src/interactive-data.js +0 -7
- package/dist/src/interactive.d.ts +0 -4
- package/dist/src/interactive.js +0 -7
- package/docs/migration-v2.md +0 -238
package/README.md
CHANGED
|
@@ -1,21 +1,8 @@
|
|
|
1
|
-
#
|
|
1
|
+
# SF Map SVG
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
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.
|
|
4
4
|
|
|
5
|
-
[Live
|
|
6
|
-
|
|
7
|
-
Self-contained SVG maps of San Francisco, with precise coastlines, soft district colors, parks, roads, BART stations, and searchable neighborhoods. Render static SVGs in Node or add an interactive map to a browser. All geometry is bundled; there are no runtime dependencies, map tiles, API keys, or external data requests.
|
|
8
|
-
|
|
9
|
-

|
|
10
|
-
|
|
11
|
-
## Choose a starting point
|
|
12
|
-
|
|
13
|
-
| Goal | Example | API |
|
|
14
|
-
| --- | --- | --- |
|
|
15
|
-
| Explore real election data | [California propositions by SF district](https://kahwee.github.io/sf-map-svg/propositions.html) or [local measures](https://kahwee.github.io/sf-map-svg/measures.html) | `custom-map` |
|
|
16
|
-
| Make a small interactive city map | [Neighborhood guide](https://kahwee.github.io/sf-map-svg/#explore-more-title) | `/guide` |
|
|
17
|
-
| Render a static or custom SVG | [Code recipes](docs/EXAMPLES.md) | `/static` or `/custom-map` |
|
|
18
|
-
| Animate a route | [BART journey](https://kahwee.github.io/sf-map-svg/transit.html) | `/transit` or overlays |
|
|
5
|
+
[Live examples](https://kahwee.github.io/sf-map-svg/examples.html) · [Storybook source](stories/) · [Geographic sources](SOURCES.md) · [v3 migration](docs/migration-v3.md)
|
|
19
6
|
|
|
20
7
|
## Install
|
|
21
8
|
|
|
@@ -23,639 +10,65 @@ Self-contained SVG maps of San Francisco, with precise coastlines, soft district
|
|
|
23
10
|
pnpm add @kahwee/sf-map-svg
|
|
24
11
|
```
|
|
25
12
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
| Start with | Entry point | What you get |
|
|
29
|
-
| --- | --- | --- |
|
|
30
|
-
| v2 controller | `@kahwee/sf-map-svg` or `/map` | Explicit data, grouped options, managed events and camera |
|
|
31
|
-
| v2 static SVG | `@kahwee/sf-map-svg/static` | Server-safe rendering with explicit data |
|
|
32
|
-
| Compatibility static SVG | `@kahwee/sf-map-svg/legacy` | Original renderer with bundled geography |
|
|
33
|
-
| Data-injected SVG | `@kahwee/sf-map-svg/custom-map` | Tree-shakeable renderer core with only the geographic data you provide |
|
|
34
|
-
| Neighborhood explorer | `@kahwee/sf-map-svg/explorer` | Search, source selection, map controls, GeoJSON downloads |
|
|
35
|
-
| Interactive map | `@kahwee/sf-map-svg/interactive` | Embeddable map and controls without the explorer sidebar |
|
|
36
|
-
| Data-injected interactive map | `@kahwee/sf-map-svg/interactive-data` | Interactive shell without bundled geographic JSON |
|
|
37
|
-
| Lightweight guide map | `@kahwee/sf-map-svg/guide` | Curated overview geography, with detailed data loaded explicitly |
|
|
38
|
-
| Animated transit demo | `@kahwee/sf-map-svg/transit` | Optional, schematic BART journey with playback controls |
|
|
39
|
-
| Geographic data | `@kahwee/sf-map-svg/data` | Source-aware lookup and canonical GeoJSON |
|
|
40
|
-
| Metadata search | `@kahwee/sf-map-svg/data/catalog` | Search names without polygon geometry |
|
|
41
|
-
| SFAR lookup | `@kahwee/sf-map-svg/data/realtor` | Default neighborhoods without alternative sources |
|
|
13
|
+
Node 22.12+ is required for server rendering. Browser maps need a DOM and a bundler that supports JSON imports.
|
|
42
14
|
|
|
43
|
-
##
|
|
15
|
+
## Static SVG
|
|
44
16
|
|
|
45
|
-
```
|
|
46
|
-
import {
|
|
47
|
-
import {
|
|
48
|
-
import { guideOptions } from '@kahwee/sf-map-svg/presets';
|
|
49
|
-
|
|
50
|
-
const map = createMap(guideMapData, {
|
|
51
|
-
...guideOptions,
|
|
52
|
-
features: { motion: true, markerEntrance: true, clustering: true },
|
|
53
|
-
appearance: { colors: { water: '#e6f1f5' } },
|
|
54
|
-
});
|
|
55
|
-
document.querySelector('#map').append(map.element);
|
|
56
|
-
map.configure({ features: { motion: false }, controls: { pan: false } });
|
|
57
|
-
const unsubscribe = map.on('markerchange', ({ marker }) => console.log(marker?.id));
|
|
58
|
-
map.camera.reset({ animate: false });
|
|
59
|
-
// On component disposal: map.destroy();
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
The root and `/map` include no geography; `/static` includes no interactive runtime.
|
|
63
|
-
`guide/data` supplies only overview geography; `/presets` supplies configuration only.
|
|
64
|
-
Camera methods share `{ animate, duration }`. `configure()` accepts the same feature,
|
|
65
|
-
layer and control groups as construction and validates the entire patch before applying
|
|
66
|
-
it. Appearance is construction-only. The controller's `on()` returns an unsubscribe
|
|
67
|
-
function and disposal removes all its subscriptions. Operations after disposal throw;
|
|
68
|
-
`destroy()` itself is idempotent.
|
|
69
|
-
|
|
70
|
-
`renderMap(data, options)` from `/static` returns `{ svg, project, ... }` and needs only
|
|
71
|
-
`StaticMapData` (for example, `guideMapData.map`). Static options retain their SVG-unit
|
|
72
|
-
semantics; camera/motion/control options belong exclusively to the browser controller.
|
|
73
|
-
`getLayerPaths(data, options)` returns the same fitted projection and canonical coast,
|
|
74
|
-
district, neighborhood, highway, landmark, road, and station paths without making SVG markup.
|
|
75
|
-
`DistrictYear`, `DistrictRowData`, and `DistrictStyle` are exported types from the root and `/static`.
|
|
76
|
-
See [v2 migration and architecture](docs/migration-v2.md) for breaking changes,
|
|
77
|
-
configuration resets, and bundle boundaries. Existing subpaths remain compatibility APIs.
|
|
78
|
-
|
|
79
|
-
## Render a static map
|
|
80
|
-
|
|
81
|
-
```js
|
|
82
|
-
import { writeFile } from 'node:fs/promises';
|
|
83
|
-
import { renderSFMap } from '@kahwee/sf-map-svg/legacy';
|
|
17
|
+
```ts
|
|
18
|
+
import { renderMap } from '@kahwee/sf-map-svg';
|
|
19
|
+
import { fullMapData } from '@kahwee/sf-map-svg/data/full';
|
|
84
20
|
|
|
85
|
-
const svg =
|
|
21
|
+
const { svg, project, viewBox } = renderMap(fullMapData.map, {
|
|
22
|
+
year: 2022,
|
|
86
23
|
landmarks: true,
|
|
87
24
|
bartStations: true,
|
|
88
|
-
highways: true,
|
|
89
|
-
neighborhoodLines: true,
|
|
90
|
-
markers: [{ id: 'dolores', lng: -122.4269, lat: 37.7596, label: 'Dolores Park' }],
|
|
91
25
|
});
|
|
92
|
-
|
|
93
|
-
await writeFile('san-francisco.svg', svg);
|
|
94
26
|
```
|
|
95
27
|
|
|
96
|
-
For
|
|
97
|
-
`@kahwee/sf-map-svg/custom-map` and pass only the geographic assets your map uses.
|
|
98
|
-
That entry point does not import the package's built-in JSON collections. Its `SFMapData`
|
|
99
|
-
requires the coast and accepts selected district vintages plus optional neighborhood,
|
|
100
|
-
road, park, and station arrays. Use the canonical JSON subpaths documented in
|
|
101
|
-
[`data/README.md`](data/README.md) as source; map each feature collection to the
|
|
102
|
-
corresponding `SFMapData` records. The `/legacy` entry includes the built-in datasets for migration. The v2 root
|
|
103
|
-
imports no geographic JSON.
|
|
28
|
+
`renderMap(data, options)` returns SVG markup and matching projection helpers. Import `/data/full` only when all packaged geography is needed. For static rendering without interactive lookup collections, import `staticMapData` from `/data/static`. Small maps can compose selected JSON exports from `/data/*` and pass them as `StaticMapData`. `getLayerPaths(data, options)` returns fitted geographic paths without SVG markup.
|
|
104
29
|
|
|
105
|
-
|
|
106
|
-
modules that symbol uses.
|
|
107
|
-
`searchNeighborhoods` needs catalog metadata but no polygon geometry. The synchronous
|
|
108
|
-
source-switching `getNeighborhood` still needs all three neighborhood collections;
|
|
109
|
-
use `/data/realtor` when only the default SFAR definitions are needed. Optional coast,
|
|
110
|
-
district, road, park, and station collections also have independent `/data/*` entries.
|
|
111
|
-
See [the measured bundle report](docs/module-bundle-report.md).
|
|
30
|
+
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`, and `colors`. Each optional layer is independent. User-supplied text and attributes are escaped in SVG output.
|
|
112
31
|
|
|
113
|
-
|
|
114
|
-
`@kahwee/sf-map-svg/interactive-data`. Pass an `InteractiveSFMapData` object containing
|
|
115
|
-
`map` (the `SFMapData` used by the static renderer) and only the neighborhood collections
|
|
116
|
-
you want available. This keeps alternative neighborhood sources, unused district vintages,
|
|
117
|
-
and optional layers out of that entry's bundle. The normal `/interactive` entry retains its
|
|
118
|
-
built-in datasets and synchronous source switching.
|
|
119
|
-
|
|
120
|
-
For the curated guide map, import `@kahwee/sf-map-svg/guide`. Its overview preset contains
|
|
121
|
-
only the simplified coastline, SFAR areas, major parks, BART points, US 101 / I-280 /
|
|
122
|
-
Highway 1, and Market, Van Ness, Geary, Lombard, 19th Avenue, and the Embarcadero. It does
|
|
123
|
-
not import historical districts, other neighborhood sources, or the full street network.
|
|
124
|
-
Collision-filtered neighborhood labels are enabled by default. Road geometry and road labels
|
|
125
|
-
have independent `layers.keyRoads` and `layers.roadLabels` controls. Lombard geometry and its
|
|
126
|
-
label appear after 1.8× zoom; major street labels can appear at city scale in a smaller type size.
|
|
32
|
+
## Interactive map
|
|
127
33
|
|
|
128
34
|
```ts
|
|
129
|
-
import {
|
|
130
|
-
import {
|
|
131
|
-
import { createInteractiveSFMapWithData } from '@kahwee/sf-map-svg/interactive-data';
|
|
132
|
-
|
|
133
|
-
const map = createGuideMap({ layers: { roadLabels: false } });
|
|
134
|
-
document.querySelector('#map')!.append(map);
|
|
35
|
+
import { createMap } from '@kahwee/sf-map-svg';
|
|
36
|
+
import { fullMapData } from '@kahwee/sf-map-svg/data/full';
|
|
135
37
|
|
|
136
|
-
|
|
137
|
-
const detailedData = await loadGuideDetailedData();
|
|
138
|
-
const detailedMap = createInteractiveSFMapWithData(detailedData, {
|
|
38
|
+
const map = createMap(fullMapData, {
|
|
139
39
|
mode: 'neighborhoods',
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
`docs/guide-bundle-report.md` records compressed bundle sizes and verifies the included data.
|
|
146
|
-
The detailed loader is also re-exported from `/guide` for compatibility; importing only
|
|
147
|
-
that function no longer puts overview geography in the initial bundle.
|
|
148
|
-
|
|
149
|
-
```js
|
|
150
|
-
import coast from '@kahwee/sf-map-svg/data/coast.json' with { type: 'json' };
|
|
151
|
-
import realtor from '@kahwee/sf-map-svg/data/neighborhoods-realtor.json' with { type: 'json' };
|
|
152
|
-
import { createInteractiveSFMapWithData } from '@kahwee/sf-map-svg/interactive-data';
|
|
153
|
-
|
|
154
|
-
const map = createInteractiveSFMapWithData(
|
|
155
|
-
{
|
|
156
|
-
map: { coast: coast.features[0].geometry },
|
|
157
|
-
neighborhoods: { realtor },
|
|
158
|
-
},
|
|
159
|
-
{
|
|
160
|
-
layers: {
|
|
161
|
-
districtFills: false,
|
|
162
|
-
districtLines: false,
|
|
163
|
-
districtLabels: false,
|
|
164
|
-
landmarks: false,
|
|
165
|
-
bartStations: false,
|
|
166
|
-
highways: false,
|
|
167
|
-
keyRoads: false,
|
|
168
|
-
},
|
|
169
|
-
},
|
|
170
|
-
);
|
|
171
|
-
document.querySelector('#map').append(map);
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
Embed the returned SVG markup directly in a page; in Astro, use `<div set:html={svg} />`. User-supplied text and attributes are XML escaped. Use a distinct `idPrefix` for each map when combining independently rendered SVGs.
|
|
175
|
-
|
|
176
|
-
## Static options
|
|
177
|
-
|
|
178
|
-
| Option | Default | Purpose |
|
|
179
|
-
| --- | --- | --- |
|
|
180
|
-
| `year` | `2022` | District boundaries: `2002`, `2012`, or `2022` |
|
|
181
|
-
| `districtLines` | `true` | Supervisorial district outlines |
|
|
182
|
-
| `neighborhoodLines` | `false` | Dashed SFAR realtor neighborhood outlines |
|
|
183
|
-
| `theme` | `'districts'` | Use `'transit'` for pale blue water, ivory land, green parks, and blue BART symbols; custom `colors` still take precedence |
|
|
184
|
-
| `districtFills` | `true` | Original Site’s eleven muted district colors |
|
|
185
|
-
| `districtStyle` | — | Per-district callback returning optional `fill`, `stroke`, and `opacity` (0–1); applies to both static SVG and the interactive map |
|
|
186
|
-
| `labels` | `true` | Master switch for visible map text; symbols and accessible titles remain |
|
|
187
|
-
| `districtLabels` | `true` | District number badges |
|
|
188
|
-
| `highways` | `false` | Original Site’s highway geometry |
|
|
189
|
-
| `landmarks` | `false` | Golden Gate Park, Presidio, Lincoln Park, Twin Peaks, Dolores Park, and McLaren Park |
|
|
190
|
-
| `bartStations` | `false` | Eight San Francisco BART stations with blue rings and names |
|
|
191
|
-
| `width`, `height` | `800`, `800` | SVG viewBox and intrinsic size |
|
|
192
|
-
| `padding` | `28` | Space around the coast |
|
|
193
|
-
| `markers` | `[]` | Points with `id`, `lng`, `lat`, optional `label`, `color`, `selected` |
|
|
194
|
-
| `keyRoads` | `false` | Six selected street corridors: Market, Van Ness, Geary, Lombard, 19th Avenue, and the Embarcadero |
|
|
195
|
-
| `roadLabels` | Same as `keyRoads` | Road labels independently of their geometry |
|
|
196
|
-
| `colors` | Built-in palette | Override `water`, `land`, `district`, `neighborhood`, `highway`, `road`, `park`, `landmark`, `bart`, `label`, `marker`, `selected` |
|
|
197
|
-
| `title` | `San Francisco map` | Accessible SVG title |
|
|
198
|
-
| `idPrefix` | Unique per process | Set explicitly for deterministic output or independent server renders |
|
|
199
|
-
|
|
200
|
-
For a plain outline map, set `districtFills: false`. Neighborhood areas are **August 2010 SFAR realtor areas**, not legal boundaries or a historical layer matched to the district year. See [SOURCES.md](SOURCES.md).
|
|
201
|
-
|
|
202
|
-
`createSFMap(options)` returns `{ svg, project, viewBox }`. `project([longitude, latitude])` gives matching SVG coordinates for custom overlays. Named exports also include `districtYears`, `districtColors`, and `neighborhoodNames`.
|
|
203
|
-
|
|
204
|
-
### Election district API
|
|
205
|
-
|
|
206
|
-
Supply canonical district rows for each boundary year and return a style for each district:
|
|
207
|
-
|
|
208
|
-
```ts
|
|
209
|
-
import { createMap, getLayerPaths, renderMap, type StaticMapData } from '@kahwee/sf-map-svg';
|
|
210
|
-
import { districtMaps } from '@kahwee/sf-map-svg/data/districts';
|
|
211
|
-
import coast from '@kahwee/sf-map-svg/data/coast.json' with { type: 'json' };
|
|
212
|
-
|
|
213
|
-
const districts = Object.fromEntries(
|
|
214
|
-
Object.entries(districtMaps).map(([year, collection]) => [year, collection.features.map(({ geometry, properties }) => ({
|
|
215
|
-
id: properties.district,
|
|
216
|
-
label: properties.label,
|
|
217
|
-
labelPoints: properties.labelPoints,
|
|
218
|
-
geometry,
|
|
219
|
-
extras: properties.displayExtras,
|
|
220
|
-
}))]),
|
|
221
|
-
) as StaticMapData['districts'];
|
|
222
|
-
const data: StaticMapData = { coast: coast.features[0].geometry as StaticMapData['coast'], districts };
|
|
223
|
-
const shares = new Map([[1, 0.62], [2, 0.48]]); // Replace with your vote data.
|
|
224
|
-
const districtStyle = (district: { id: number }) => ({
|
|
225
|
-
fill: (shares.get(district.id) ?? 0) >= 0.5 ? '#498c79' : '#cfdfd6',
|
|
226
|
-
stroke: '#49665f',
|
|
227
|
-
});
|
|
228
|
-
const { svg } = renderMap(data, { year: 2022, districtStyle });
|
|
229
|
-
const paths = getLayerPaths(data, { year: 2022 }); // paths.districts[0].geometry / .extras / .path
|
|
230
|
-
|
|
231
|
-
const map = createMap({ map: data, districts: districtMaps, neighborhoods: {} }, {
|
|
232
|
-
mode: 'districts', year: 2022, appearance: { districtStyle },
|
|
40
|
+
source: 'realtor',
|
|
41
|
+
neighborhood: 'Inner Mission',
|
|
42
|
+
layers: { bartStations: true },
|
|
43
|
+
features: { motion: true },
|
|
44
|
+
appearance: { theme: 'districts' },
|
|
233
45
|
});
|
|
234
46
|
document.querySelector('#map')?.append(map.element);
|
|
235
|
-
map.on('
|
|
236
|
-
map.
|
|
237
|
-
|
|
238
|
-
map.
|
|
239
|
-
map.selectDistrict(1, { fit: true });
|
|
240
|
-
map.setDistrictStyle(districtStyle); // Recompute colors after your vote data changes.
|
|
241
|
-
// On component disposal: map.destroy();
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
District paths are keyboard buttons: Tab enters the district layer, brackets move among districts,
|
|
245
|
-
and Enter or Space selects and activates one. `getSelectedDistrict()` returns a detached
|
|
246
|
-
`{ id, year, district }` snapshot. `setDistrictYear()` preserves the camera and an existing
|
|
247
|
-
district selection, changes labels and paths in place, and emits `districtyearchange`.
|
|
248
|
-
It requires both district rows and label features for the requested year; unavailable years
|
|
249
|
-
throw without changing the map. The `animate` option crossfades boundary sets rather than
|
|
250
|
-
morphing polygons with different topology.
|
|
251
|
-
|
|
252
|
-
Use `theme: 'transit'` for pale water, ivory land, green parks, and blue BART symbols. Custom `colors` override the preset. Park and station overlays represent current source geography, independently of the district year; stations outside San Francisco, including Daly City, are excluded.
|
|
253
|
-
|
|
254
|
-
`keyRoads: true` adds the six curated orientation streets using DataSF centerlines. These are orientation features, not routing guidance. The lightweight guide shows primary corridors at city scale and Lombard after zooming.
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
## Neighborhood explorer
|
|
258
|
-
|
|
259
|
-
The browser explorer includes canonical-name and alias search, source selection, neighborhood outlines, zoom controls, and GeoJSON downloads. SFAR realtor definitions are selected by default; SF Find and analysis neighborhoods remain separate choices.
|
|
260
|
-
|
|
261
|
-
```js
|
|
262
|
-
import { createNeighborhoodExplorer } from '@kahwee/sf-map-svg/explorer';
|
|
263
|
-
|
|
264
|
-
const explorer = createNeighborhoodExplorer({ source: 'realtor' });
|
|
265
|
-
document.querySelector('#map').append(explorer);
|
|
266
|
-
explorer.selectNeighborhood('NoPa');
|
|
267
|
-
|
|
268
|
-
// Before removing the component, release its observers and event listeners.
|
|
269
|
-
// explorer.destroy();
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
Call this browser-only factory after a DOM is available. Importing it does not mount anything. Options include `source`, an optional initial `neighborhood` name or alias, and district `year`. The returned element also exposes `setSource(source)`, `zoomBy(factor)`, and `resetView()`.
|
|
273
|
-
|
|
274
|
-
Selected downloads are one-feature GeoJSON FeatureCollections retaining source attribution and boundary-processing metadata.
|
|
275
|
-
|
|
276
|
-
At city scale, labels stay sparse. Zooming reveals neighborhood and BART names, with label sizing and collision checks based on the visible viewport. Station points remain visible. The static `renderSFMap` API keeps its existing labels and defaults.
|
|
277
|
-
|
|
278
|
-
### Map modes and labels
|
|
279
|
-
|
|
280
|
-
The interactive explorer includes a map-mode selector and a Labels toggle. `mode: 'districts'` shows numbered supervisorial districts; `mode: 'neighborhoods'` shows names from the selected neighborhood source (SFAR realtor by default). Labels are collision-filtered and remain about 12 screen pixels through map zoom and resize; more names fit as you zoom in. Road labels use 11 pixels.
|
|
281
|
-
|
|
282
|
-
```js
|
|
283
|
-
const explorer = createNeighborhoodExplorer({ mode: 'districts', labels: true });
|
|
284
|
-
explorer.setLabels(false);
|
|
285
|
-
explorer.setMode('neighborhoods');
|
|
286
|
-
explorer.setLabels(true);
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
`renderSFMap({ labels: false })` also hides all visible text while retaining station symbols and accessible titles. Standalone SVGs are static images; the interactive explorer provides the constant-size labels during map zoom.
|
|
290
|
-
|
|
291
|
-
## Reusable interactive map
|
|
292
|
-
|
|
293
|
-
The `@kahwee/sf-map-svg/interactive` entry point provides a map, accessible controls, and
|
|
294
|
-
attribution without the explorer's search sidebar or detail panel. It does not change URLs,
|
|
295
|
-
load articles, apply editorial filters, or navigate. Importing the static entry point does not
|
|
296
|
-
import this interactive runtime. Both paths retain zero runtime dependencies.
|
|
297
|
-
|
|
298
|
-
```js
|
|
299
|
-
import { createInteractiveSFMap } from '@kahwee/sf-map-svg/interactive';
|
|
300
|
-
|
|
301
|
-
const map = createInteractiveSFMap({
|
|
302
|
-
mode: 'neighborhoods',
|
|
303
|
-
source: 'analysis', // 'sf-find' and 'realtor' are also available
|
|
304
|
-
theme: 'transit',
|
|
305
|
-
labelSize: { min: 12, max: 15 },
|
|
306
|
-
layers: { districtFills: false, districtLines: false, districtLabels: false },
|
|
307
|
-
});
|
|
308
|
-
document.querySelector('#map').append(map);
|
|
309
|
-
map.addEventListener('neighborhoodchange', ({ detail }) => {
|
|
310
|
-
// On clear: id, name, and feature are null. source always identifies the dataset.
|
|
311
|
-
console.log(detail.id, detail.name, detail.source);
|
|
312
|
-
});
|
|
313
|
-
map.selectNeighborhood('Mission', { fit: false });
|
|
314
|
-
const selection = map.getSelection(); // { id, name, source, feature } or null
|
|
315
|
-
map.selectNeighborhood(null);
|
|
316
|
-
```
|
|
317
|
-
|
|
318
|
-
`createInteractiveSFMap()` defaults to `mode: 'basemap'`: no district or neighborhood layers.
|
|
319
|
-
Parks, roads, and stations are initially enabled and can be disabled independently. The existing
|
|
320
|
-
`createNeighborhoodExplorer()` and static APIs retain their realtor/district defaults. The
|
|
321
|
-
source option only chooses a definition collection; it does not make that collection visible
|
|
322
|
-
in basemap mode. Editorial groupings belong to the consumer and are never treated as geographic
|
|
323
|
-
aliases. Import types including `InteractiveSFMapOptions`, `InteractiveSFMapElement`,
|
|
324
|
-
`MapViewport`, `MapPadding`, and `NeighborhoodSelection` from the interactive entry point.
|
|
325
|
-
|
|
326
|
-
| Option | Default | Behavior |
|
|
327
|
-
| --- | --- | --- |
|
|
328
|
-
| `mode` | `basemap` (interactive), `neighborhoods` (explorer) | `basemap`, `districts`, or `neighborhoods`; establishes layer defaults |
|
|
329
|
-
| `source` | `realtor` | `realtor`, `sf-find`, or `analysis`; explicit source recommended for reusable integrations |
|
|
330
|
-
| `theme` | `transit` | `transit` or `districts` |
|
|
331
|
-
| `year` | `2022` | District vintage: 2002, 2012, or 2022 |
|
|
332
|
-
| `layers` | Mode defaults | Independent booleans: `districtFills`, `districtLines`, `districtLabels`, `neighborhoodLines`, `neighborhoodLabels`, `landmarks`, `bartStations`, `highways`, `keyRoads` (geometry), `roadLabels` |
|
|
333
|
-
| `labels` | `true` | Master visible-text switch; accessible descriptions and station symbols remain |
|
|
334
|
-
| `labelSize` | `{ min: 11, max: 12 }` | Neighborhood and other labels use this screen-pixel range (8–32 allowed); road labels stay smaller at 9px, capped by `max`. Sizes never grow with zoom |
|
|
335
|
-
| `selectableNeighborhoods` | `true` | Enables pointer/keyboard selection; false retains geography and labels |
|
|
336
|
-
| `neighborhood` | Unset | Initial name, alias, or ID in the selected source |
|
|
337
|
-
| `fitPadding` | `24` | Screen pixels, number or `{ top, right, bottom, left }`, used by selection and geometry fitting after mounting |
|
|
338
|
-
| `markers` | `[]` | Supplied `MapMarker` records with unique, nonempty IDs; no content fetching |
|
|
339
|
-
| `markerRadius` / `markerHitSize` | `6` / `44` | Screen-pixel visible radius and tap-target diameter, independent of zoom; selected radius grows by 2px |
|
|
340
|
-
| `markerColor` / `selectedMarkerColor` | `#245b61` / `#f04f32` | Default marker colors; individual `marker.color` overrides the unselected color |
|
|
341
|
-
| `onMarkerActivate` | Unset | Called when a non-null marker selection changes, including programmatic changes |
|
|
342
|
-
| `overlays` | `[]` | GeoJSON line or polygon overlays with stable IDs and optional SVG styles |
|
|
343
|
-
| `style` | Built-in tokens | Explorer CSS tokens: `ink`, `surface`, `accent`, `border`, `focus`, `controlGap`, `font` |
|
|
344
|
-
| `strings` | English defaults | Replace visible map labels and gesture help for localization |
|
|
345
|
-
| `controls` | All enabled | Independently hide `zoom`, `pan`, `reset`, `labels`, `touch`, `legend`, `neighborhoodPicker`, `markerPicker`, `help`, or `status`; source attribution remains visible |
|
|
346
|
-
|
|
347
|
-
For a compact embed, hide the native choosers only when the page already lists every
|
|
348
|
-
marker or area as an accessible control. Hidden help remains the map's accessible
|
|
349
|
-
description, and a hidden status line remains a polite live region:
|
|
350
|
-
|
|
351
|
-
```js
|
|
352
|
-
const map = createGuideMap({
|
|
353
|
-
interface: 'map',
|
|
354
|
-
controls: { labels: false, neighborhoodPicker: false, markerPicker: false, help: false, status: false },
|
|
355
|
-
strings: { chooseMarker: 'Place on map' }, // also used after setMarkers() updates
|
|
356
|
-
});
|
|
47
|
+
map.on('neighborhoodchange', ({ name }) => console.log(name));
|
|
48
|
+
map.camera.zoom(1.5);
|
|
49
|
+
// When the view is removed:
|
|
50
|
+
map.destroy();
|
|
357
51
|
```
|
|
358
52
|
|
|
359
|
-
|
|
360
|
-
and badges can therefore be composed with neighborhood names without requiring district
|
|
361
|
-
labels. Descriptions identify only displayed boundary layers, their source and available
|
|
362
|
-
vintage; analysis data is described as census-tract-based reporting areas, without inventing a
|
|
363
|
-
boundary year. The downloadable source metadata remains available in the full explorer.
|
|
364
|
-
|
|
365
|
-
Labels use measured screen-space collision boxes. Selected marker and neighborhood names
|
|
366
|
-
come first, then district labels, stations, parks, roads, and neighborhoods in descending area
|
|
367
|
-
order with stable ID tie-breaking. Station and marker symbols reserve space. Roads and station
|
|
368
|
-
names appear at 1.8× zoom; city view keeps park labels sparse. Labels outside the viewport or
|
|
369
|
-
without room are suppressed, including a selected label too wide to fit. Selection names remain
|
|
370
|
-
available in the native chooser and through events. The label range is separate from SVG user
|
|
371
|
-
units used by the static renderer.
|
|
53
|
+
`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.
|
|
372
54
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
```js
|
|
376
|
-
const saved = map.getViewport(); // copied [x, y, size] in the 800×800 projected map
|
|
377
|
-
map.zoomBy(2);
|
|
378
|
-
map.panBy(30, 0); // move view east by 30 screen pixels
|
|
379
|
-
map.setViewport(saved);
|
|
380
|
-
map.resetView();
|
|
381
|
-
map.fitGeometry({
|
|
382
|
-
type: 'MultiPoint',
|
|
383
|
-
coordinates: places.map(({ lng, lat }) => [lng, lat]),
|
|
384
|
-
}, { top: 24, right: 24, bottom: 80, left: 24 });
|
|
385
|
-
map.addEventListener('viewportchange', ({ detail }) => saveInYourState(detail.viewport));
|
|
386
|
-
```
|
|
55
|
+
Construction options include `mode` (`basemap`, `neighborhoods`, or `districts`), `source`, `neighborhood`, `year`, `labels`, `layers`, `controls`, `features`, `appearance`, `markers`, `overlays`, `legend`, `strings`, `attribution`, `fitPadding`, and touch navigation settings. `features` holds motion, marker entrances, selected marker rings, clustering, north arrow, and scale bar. `appearance` holds theme, color tokens, label and area styles, marker colors and sizes, and district styling. `layers` controls district fill/line/labels, neighborhood lines/labels, landmarks, BART, highways, key roads, and road labels. See the exported `MapOptions` type for exact values and the [Storybook examples](stories/) for live controls.
|
|
387
56
|
|
|
388
|
-
|
|
389
|
-
including `Point`, `MultiPoint`, and `GeometryCollection`, and rejects empty geometry or
|
|
390
|
-
impossible padding. Views are constrained to the city extent and 1–12× zoom. Consequently,
|
|
391
|
-
padding is best-effort near city edges or for extents larger than the map; a single point fits
|
|
392
|
-
at maximum zoom. This is an SF map, not a world map. Resizing keeps the projected view and
|
|
393
|
-
recomputes screen sizes; call `fitGeometry` again if a new container size needs different fitting.
|
|
394
|
-
Initial neighborhood fitting before mounting uses the explorer's proportional padding.
|
|
57
|
+
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.
|
|
395
58
|
|
|
396
|
-
|
|
397
|
-
allow external state to drive selection without moving the viewport. They return false for
|
|
398
|
-
unknown identities. `getSelection()` and `getSelectedMarker()` read current selection.
|
|
399
|
-
`setSource(source)` clears neighborhood selection and resets the view, emitting a clear event
|
|
400
|
-
when needed. `setMode(mode)` resets the viewport. Repeating the same viewport or selection
|
|
401
|
-
emits no change event, avoiding state feedback loops. All events bubble. Call `destroy()`
|
|
402
|
-
before removing the element to release listeners, observers, frames, and download URLs.
|
|
403
|
-
`setOverlays(overlays)` replaces all consumer overlays; each overlay has a unique `id`,
|
|
404
|
-
WGS84 `LineString`, `MultiLineString`, `Polygon`, or `MultiPolygon` geometry, optional
|
|
405
|
-
`stroke`, `strokeWidth`, `fill`, `fillOpacity`, `visible`, and accessible `label`. Overlays
|
|
406
|
-
track every pan, zoom, resize, and source change and render above geography but below markers.
|
|
407
|
-
They are decorative and do not participate in label collision layout.
|
|
408
|
-
|
|
409
|
-
### Dense markers
|
|
410
|
-
|
|
411
|
-
```js
|
|
412
|
-
const map = createInteractiveSFMap({ markers: places });
|
|
413
|
-
document.querySelector('#map').append(map);
|
|
414
|
-
map.addEventListener('markerchange', ({ detail }) => {
|
|
415
|
-
// { id, marker }, both null when cleared. Render your own content panel here.
|
|
416
|
-
renderSelection(detail.marker);
|
|
417
|
-
});
|
|
418
|
-
map.setMarkers(updatedPlaces);
|
|
419
|
-
map.selectMarker('place-id');
|
|
420
|
-
```
|
|
421
|
-
|
|
422
|
-
Every supplied marker remains in the native chooser, even when positions coincide or markers
|
|
423
|
-
are outside the current view. Pointer and keyboard activation select and fit a marker; markers
|
|
424
|
-
also have individual keyboard stops. This is the accessible-choice alternative to clustering:
|
|
425
|
-
no counts are estimated and no supplied markers are silently dropped. The chooser count is
|
|
426
|
-
the supplied collection size. Replacing markers retains the selected ID when present, otherwise
|
|
427
|
-
uses an explicitly selected marker or clears selection. Large hit targets can overlap; use the
|
|
428
|
-
chooser to reach obscured markers. Automated clustering is not included in this release.
|
|
429
|
-
|
|
430
|
-
### Gesture policy and keyboard access
|
|
431
|
-
|
|
432
|
-
- **Default touch:** one finger scrolls the page; pinch zooms the browser. Map buttons and
|
|
433
|
-
native choosers work without engaging map gestures.
|
|
434
|
-
- **Touch navigation:** explicitly enable the visible button (or `setTouchNavigation(true)`).
|
|
435
|
-
One finger pans the map; two fingers pan and pinch around their midpoint. The button becomes
|
|
436
|
-
**Done: page scrolling**. Use it or Escape to return to page gestures. The page remains
|
|
437
|
-
scrollable outside the canvas, and Tab can leave it. Changing mode during an active gesture
|
|
438
|
-
takes effect after fingers are lifted.
|
|
439
|
-
- **Mouse:** drag pans; Ctrl/⌘ + wheel zooms. Ordinary wheel scrolling remains page scrolling.
|
|
440
|
-
- **Keyboard:** focus the map, then arrows pan, +/− zoom, Home resets, and Escape exits touch
|
|
441
|
-
navigation. Tab reaches controls, the neighborhood chooser, one neighborhood path, and markers.
|
|
442
|
-
On a neighborhood path, `[` / `]` moves through source features and Enter/Space selects.
|
|
443
|
-
The native chooser is also available for areas outside the current view.
|
|
444
|
-
- Pointer cancellation, loss of capture, window blur, and resizing cancel active gestures.
|
|
445
|
-
There is no animated camera or inertia. Button transitions are disabled with reduced motion.
|
|
446
|
-
|
|
447
|
-
## Geographic data and lookup
|
|
448
|
-
|
|
449
|
-
All map geometry is available through stable JSON package exports. There are three district files (2002, 2012, 2022), the full 117 SF Find neighborhoods, 41 analysis neighborhoods, 92 realtor-defined areas, and separate coastline, highway, landmark, and BART files. Neighborhood records include a canonical display name, exact source name, stable ID, aliases where documented, source definition, and full polygon geometry.
|
|
450
|
-
|
|
451
|
-
```js
|
|
452
|
-
import { getRealtorNeighborhood } from '@kahwee/sf-map-svg/data/realtor';
|
|
453
|
-
import { searchNeighborhoods } from '@kahwee/sf-map-svg/data/catalog';
|
|
454
|
-
|
|
455
|
-
const mission = getRealtorNeighborhood('Inner Mission');
|
|
456
|
-
const outerMission = getRealtorNeighborhood('Outer Mission');
|
|
457
|
-
const nopa = getRealtorNeighborhood('NoPa');
|
|
458
|
-
const matchingDefinitions = searchNeighborhoods('mission');
|
|
459
|
-
```
|
|
59
|
+
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](docs/EXAMPLES.md) and [consumer integration](docs/consumer-integration.md).
|
|
460
60
|
|
|
461
|
-
|
|
61
|
+
## Data and development
|
|
462
62
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
Requires Node 22.12+ and pnpm 12. The library uses strict TypeScript; the build emits JavaScript and declarations to `dist/`.
|
|
63
|
+
Canonical geography is in [`data/`](data/README.md), with provenance in [`SOURCES.md`](SOURCES.md). The public `/data` entry exposes lookup, catalog, district maps, and source-specific neighborhood collections. These are deeply frozen; clone before editing.
|
|
466
64
|
|
|
467
65
|
```sh
|
|
468
66
|
pnpm install --frozen-lockfile
|
|
469
|
-
pnpm check
|
|
470
|
-
pnpm demo
|
|
471
|
-
pnpm build-storybook
|
|
472
|
-
pnpm test:stories
|
|
473
|
-
pnpm test:
|
|
474
|
-
pnpm test:package # install and check the packed package
|
|
475
|
-
```
|
|
476
|
-
|
|
477
|
-
Use `pnpm format` to apply Biome formatting and safe lint fixes. Install Chromium once with `pnpm exec playwright install chromium` before local Storybook tests. Run `pnpm storybook` for interactive component examples at http://127.0.0.1:6006. The coverage command writes `coverage/storybook/coverage-summary.json` and `lcov.info` for `src/` TypeScript only. CI runs package and Chromium Storybook checks on Node 26 and uploads the coverage report. See [CONTRIBUTING.md](CONTRIBUTING.md) for source structure and release instructions.
|
|
478
|
-
|
|
479
|
-
In Storybook, start with **Start here / V2 interactive map** and **Start here / V2 static SVG** for copyable public imports and live layer controls. The phone stories use a 390 px Storybook viewport; the toolbar also offers a 1280 px desktop viewport. **Maps** shows the guide and transit components, **Data** compares source definitions, **Legacy** documents compatibility entrypoints, and **Checks** contains deeper controller regressions. Accessibility violations fail Storybook browser tests by default.
|
|
480
|
-
|
|
481
|
-
### Browser verification
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
Run `pnpm demo` and serve the repository root. `examples/generated/index.html` covers static
|
|
485
|
-
maps, `explorer.html` covers the full explorer, and `interactive.html` covers independently
|
|
486
|
-
controlled neighborhood selection, 36 overlapping sample markers, and fit/save/restore hooks.
|
|
487
|
-
Storybook **Legacy / Interactive map** includes both themes, selectable neighborhoods,
|
|
488
|
-
independent layers, dense markers, and a 390px example.
|
|
489
|
-
|
|
490
|
-
With that server running, the browser regression checks can be run through the installed CLI:
|
|
491
|
-
|
|
492
|
-
```sh
|
|
493
|
-
agent-browser skills get core --full
|
|
494
|
-
agent-browser --session sf-map-check open http://127.0.0.1:8765/examples/generated/interactive.html
|
|
495
|
-
agent-browser --session sf-map-check eval "import('/scripts/check-interactive-browser.mjs').then(m => m.checkInteractiveBrowser())"
|
|
496
|
-
agent-browser --session sf-map-check close
|
|
497
|
-
```
|
|
498
|
-
|
|
499
|
-
These checks cover actual browser layout, both themes, narrow/wide containers, label collisions,
|
|
500
|
-
zoom extremes, keyboard selection, cancellation logic, viewport state, marker reachability, and
|
|
501
|
-
teardown. Physical iOS Safari and Android Chrome verification remains required before a release:
|
|
502
|
-
check page scroll and browser pinch in default mode; map pan and pinch in engaged mode; lift one
|
|
503
|
-
finger; interrupt/cancel; rotate; use Done; verify scrolling resumes. Desktop automation and
|
|
504
|
-
synthetic pointer tests do not establish physical-device compatibility.
|
|
505
|
-
|
|
506
|
-
## GitHub Pages
|
|
507
|
-
|
|
508
|
-
The [one spot, three San Franciscos map](https://kahwee.github.io/sf-map-svg/spot.html) lets visitors pick a point and compare its SFAR, SF Find, and analysis neighborhood definitions, then inspect its supervisorial district on the 2002, 2012, and 2022 display maps. The same comparison appears in Storybook under **Data / One spot, three San Franciscos**. A source outline is drawn from that source's polygon; clicking the map runs point-in-polygon lookup against the canonical geographic collections. The page makes no address or legal-boundary claim.
|
|
509
|
-
|
|
510
|
-
The [civic atlas](https://kahwee.github.io/sf-map-svg/) leads with certified June 2026 ballot measure results. Visitors can select a measure and district, switch Yes/No shading, compare the official 2002, 2012, and 2022 district maps, and play a schematic BART journey. The boundary animation morphs matched district outlines between dated SVGs. Intermediate shapes illustrate the change; each completed year uses its exact published geometry. Playback is user initiated, pauses when the page is hidden, and switches instantly when reduced motion is requested. The lightweight neighborhood guide loads on demand. The full [ballot measures explorer](https://kahwee.github.io/sf-map-svg/measures.html) spans all three district map years with four separately sourced election snapshots.
|
|
511
|
-
|
|
512
|
-
The [candidate vote explorer](https://kahwee.github.io/sf-map-svg/candidates.html) maps
|
|
513
|
-
56 certified federal, statewide, and state legislative contests across six
|
|
514
|
-
San Francisco elections from 2016 to 2026. It loads one election JSON on
|
|
515
|
-
demand, marks districts only partly eligible for House or legislative races,
|
|
516
|
-
and keeps the 2012 and 2022 supervisorial map vintages distinct. Applications
|
|
517
|
-
can import a snapshot explicitly from npm without adding it to the default
|
|
518
|
-
renderer:
|
|
519
|
-
|
|
520
|
-
```js
|
|
521
|
-
import election from '@kahwee/sf-map-svg/data/candidates/2024-11-05.json' with { type: 'json' };
|
|
522
|
-
```
|
|
523
|
-
|
|
524
|
-
See the [candidate schema](data/candidates/README.md), [source records](SOURCES.md),
|
|
525
|
-
and [next dataset ideas](docs/data-opportunities.md).
|
|
526
|
-
|
|
527
|
-
```sh
|
|
528
|
-
pnpm build:pages # local preview
|
|
529
|
-
pnpm build:pages --released # use the current npm release
|
|
530
|
-
python3 -m http.server 8765 --directory pages-dist
|
|
531
|
-
# Open http://localhost:8765
|
|
67
|
+
pnpm check
|
|
68
|
+
pnpm demo
|
|
69
|
+
pnpm build-storybook
|
|
70
|
+
pnpm test:stories:coverage
|
|
71
|
+
pnpm test:package
|
|
532
72
|
```
|
|
533
73
|
|
|
534
|
-
The
|
|
535
|
-
|
|
536
|
-
`.github/workflows/pages.yml` validates and deploys on pushes to `main` and after successful npm publishing. Release-triggered builds wait for registry processing before using the newly published version. Pages deployment does not publish npm packages or releases.
|
|
537
|
-
|
|
538
|
-
## License and attribution
|
|
539
|
-
|
|
540
|
-
MIT-licensed software, originally extracted from KahWee’s San Francisco District Map. Geographic data retains its source terms and attribution requirements; see [LICENSE](LICENSE) and [SOURCES.md](SOURCES.md). Neighborhood definitions vary by source and are not legal boundaries or a claim of universal consensus.
|
|
541
|
-
|
|
542
|
-
### Schematic transit animation
|
|
543
|
-
|
|
544
|
-
```js
|
|
545
|
-
import { createTransitAnimation } from '@kahwee/sf-map-svg/transit';
|
|
546
|
-
const animation = createTransitAnimation();
|
|
547
|
-
document.querySelector('#transit').append(animation);
|
|
548
|
-
// On removal: animation.destroy();
|
|
549
|
-
```
|
|
550
|
-
|
|
551
|
-
This optional browser component starts paused, with Play/Pause and a keyboard-accessible journey slider. A loop lasts 28 seconds; timing is illustrative. It connects the bundled official BART station centroids with straight segments, not actual tracks or live service. It pauses when the page is hidden. No autoplay means reduced-motion users can inspect the static map or scrub manually. Existing map defaults are unchanged.
|
|
552
|
-
|
|
553
|
-
Embed the Pages demo with `<iframe src="https://kahwee.github.io/sf-map-svg/transit.html" title="Schematic BART journey" loading="lazy" style="width:100%;height:clamp(650px, calc(100vw + 240px), 930px);border:0"></iframe>`.
|
|
554
|
-
|
|
555
|
-
## Ballot measures explorer
|
|
556
|
-
|
|
557
|
-
[Explore local ballot measures](https://kahwee.github.io/sf-map-svg/measures.html): 44 measures from the November 2002, November 2012, November 2022, and June 2026 elections, across all three supported district map vintages. Pick an election, search its measures, inspect a district, compare two measures, and download an SVG, CSV, or the sourced JSON. The page includes touch pan/zoom, keyboard district selection, a sortable district table, and shareable year-specific views. Each election's results and district map load on demand. This is an archive of four elections, not every intervening election or a live results service. See [the election data schema](data/elections/README.md) and [SOURCES.md](SOURCES.md) for methods and source links.
|
|
558
|
-
|
|
559
|
-
### Motion, styling, and progressive embeds
|
|
560
|
-
|
|
561
|
-
The guide now exposes the same `colors` palette as the static renderer. Existing
|
|
562
|
-
appearance and immediate camera movement remain the defaults. See
|
|
563
|
-
[consumer integration recommendations](docs/consumer-integration.md) for the
|
|
564
|
-
complete example, bundle choices, progressive shell, and testing contract.
|
|
565
|
-
|
|
566
|
-
```js
|
|
567
|
-
import { createGuideMap } from '@kahwee/sf-map-svg/guide/map';
|
|
568
|
-
|
|
569
|
-
const map = createGuideMap({
|
|
570
|
-
colors: { water: '#202d38', land: '#34434a', park: '#42624d',
|
|
571
|
-
road: '#728080', neighborhood: '#64767e', label: '#f1f3ee' },
|
|
572
|
-
labelStyle: { fontFamily: 'DM Sans, system-ui, sans-serif', fontWeight: 550,
|
|
573
|
-
haloColor: '#34434a' },
|
|
574
|
-
motion: { duration: 400 },
|
|
575
|
-
markerEntrance: { duration: 450, stagger: 35 },
|
|
576
|
-
selectedMarkerRing: { color: '#f1f3ee', width: 2, gap: 3 },
|
|
577
|
-
clustering: { radius: 32 },
|
|
578
|
-
attribution: 'compact',
|
|
579
|
-
legend: { items: [{ label: 'Places', color: '#cf8757' }] },
|
|
580
|
-
northArrow: true,
|
|
581
|
-
scaleBar: true,
|
|
582
|
-
strings: { touchNavigation: 'Touch pan', touchNavigationLabel: 'Enable touch pan',
|
|
583
|
-
touchNavigationDone: 'Done', touchNavigationExitLabel: 'Restore page scrolling' },
|
|
584
|
-
});
|
|
585
|
-
document.querySelector('#map').append(map);
|
|
586
|
-
```
|
|
587
|
-
|
|
588
|
-
| Option | Contract |
|
|
589
|
-
| --- | --- |
|
|
590
|
-
| `colors` | Static palette keys: water, land, district, neighborhood, highway, road, park, landmark, BART (`bart`), label, marker, selected |
|
|
591
|
-
| `labelStyle` | `fontFamily`, numeric `fontWeight`, `haloColor`; the application loads any custom font |
|
|
592
|
-
| `areaStyle` | `selectedFill`, `selectedStroke`, `hoverFill`, `hoverStroke`; applies to selectable neighborhoods |
|
|
593
|
-
| `motion` | `false` by default; `true` uses 320ms, or `{ duration }` in milliseconds |
|
|
594
|
-
| `markerEntrance` | `false` by default; `true` uses 420ms and 35ms stagger, or `{ duration, stagger }`; only newly introduced IDs animate, delay capped at 1s |
|
|
595
|
-
| `MapMarker.radius` | Per-marker visible radius; interactive units are CSS pixels, static units are SVG units |
|
|
596
|
-
| `selectedMarkerRing` | Optional `{ color, width, gap }` in CSS pixels; preserves the hit target |
|
|
597
|
-
| `clustering` | `false` by default; `true` or `{ radius }` groups nearby screen positions; selected pin remains independent |
|
|
598
|
-
| `legend` | `{ builtins: false, items: [{ label, color }] }` replaces built-ins; omit `builtins` to append custom entries; `hidden` hides selected built-ins (`bart`, `park`, `highway`, `road`) |
|
|
599
|
-
| `attribution` | `'full'` (default) or `'compact'`; compact keeps full provenance in a native disclosure |
|
|
600
|
-
| `northArrow`, `scaleBar` | Optional canvas furniture; scale is approximate at central SF latitude, in metric units |
|
|
601
|
-
|
|
602
|
-
`setViewport(view, { animate, duration })`, `fitGeometry(geometry, padding,
|
|
603
|
-
{ animate, duration })`, `selectMarker(id, { fit, animate, duration })`, and
|
|
604
|
-
`selectNeighborhood(name, { fit, animate, duration })` accept per-call motion
|
|
605
|
-
controls. `animate: true` opts in even when global motion is disabled.
|
|
606
|
-
`stopAnimation()` freezes the camera at its current viewport. A new camera
|
|
607
|
-
operation replaces the previous transition; pointer gestures interrupt it.
|
|
608
|
-
Reduced-motion preference overrides all animation requests. `destroy()` cancels
|
|
609
|
-
camera frames, marker animations, listeners, and resize observation.
|
|
610
|
-
|
|
611
|
-
`map.overlayElement` is a public HTML overlay slot. `map.projectToScreen(lng, lat)`
|
|
612
|
-
returns `{ x, y, visible }` in CSS pixels relative to that slot. Call it after
|
|
613
|
-
mounting; reposition your callout on `viewportchange` and `mapresize`.
|
|
614
|
-
The slot ignores pointer events; interactive children can set `pointer-events:auto`.
|
|
615
|
-
`clusteractivate` emits `{ markers }` and fits their geographic extent. Coincident
|
|
616
|
-
pins cannot separate through zoom; retain the full native marker chooser or an
|
|
617
|
-
accessible external list. `--sf-marker-index` on each marker is a stable entrance
|
|
618
|
-
index hook, but the built-in animation avoids the need to style internal SVG.
|
|
619
|
-
|
|
620
|
-
For server rendering, `createGuideSVG(options)` from `@kahwee/sf-map-svg/guide/static`
|
|
621
|
-
uses the same simplified geography as the browser guide with one import.
|
|
622
|
-
`createGuideShell(options)` also reserves compact toolbar, legend, and attribution
|
|
623
|
-
rows; pass its `.sf-guide-shell` element to `mountGuideMap(shell, options)` from
|
|
624
|
-
`@kahwee/sf-map-svg/guide/map`. By default, this compact layout hides the native
|
|
625
|
-
pickers, pan buttons, label switch, help, and visible status; explicit control overrides are honored. Provide an accessible
|
|
626
|
-
external place list. The toolbar and legend scroll horizontally if needed.
|
|
627
|
-
|
|
628
|
-
### Reconfigure without rebuilding
|
|
629
|
-
|
|
630
|
-
Construction options remain backward-compatible. Use these atomic patches for
|
|
631
|
-
runtime switches; they preserve the map element, viewport, markers, and selection:
|
|
632
|
-
|
|
633
|
-
```js
|
|
634
|
-
map.setFeatures({ motion: false, clustering: true, selectedMarkerRing: true });
|
|
635
|
-
map.setLayers({ landmarks: false, bartStations: true, roadLabels: false });
|
|
636
|
-
map.setControls({ zoom: false, reset: true, touch: true });
|
|
637
|
-
```
|
|
638
|
-
|
|
639
|
-
`setFeatures` accepts the exported `MapFeatures` interface: `motion`,
|
|
640
|
-
`markerEntrance`, `clustering`, `selectedMarkerRing`, `northArrow`, and `scaleBar`.
|
|
641
|
-
Every feature accepts `false` to disable; the first four also accept `true` for
|
|
642
|
-
built-in settings or an options object. Omitted patch keys retain their settings;
|
|
643
|
-
`undefined` resets that key to the default. Options objects **replace** the previous
|
|
644
|
-
object for that feature; they do not deep-merge. `getFeatures()` returns a detached,
|
|
645
|
-
normalized snapshot. Changing motion cancels the current transition; changing
|
|
646
|
-
entrance settings cancels active entrances and applies to future new marker IDs.
|
|
647
|
-
|
|
648
|
-
`setLayers` accepts `InteractiveLayers`; `undefined` restores the mode default.
|
|
649
|
-
Geometry, associated labels, built-in legend entries, and attribution update
|
|
650
|
-
together. Roads and road labels remain independent switches. Toggling does not
|
|
651
|
-
remove geography already imported into a client bundle.
|
|
652
|
-
|
|
653
|
-
`setControls` accepts the same keys as `controls`. Hiding zoom no longer hides
|
|
654
|
-
Reset, Labels, or Touch. Hiding the touch button disengages touch navigation so
|
|
655
|
-
page scrolling stays recoverable. Unknown switch names and invalid values throw
|
|
656
|
-
before modifying state. After `destroy()`, setters are no-ops and disposal can be
|
|
657
|
-
repeated safely. Getters return the last state; screen projection still requires
|
|
658
|
-
a mounted, visible canvas.
|
|
659
|
-
|
|
660
|
-
For configuration precedence, malformed inputs, callback reentrancy, and lifecycle
|
|
661
|
-
ownership, see the [maintainer API audit](docs/api-audit.md).
|
|
74
|
+
The [contributing guide](CONTRIBUTING.md) explains the geographic and release checks. Version 3 removes the five old compatibility entry points; [migrate before upgrading](docs/migration-v3.md).
|