@kahwee/sf-map-svg 0.3.0 → 1.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 +44 -3
- package/LICENSE +23 -4
- package/README.md +267 -74
- package/SOURCES.md +4 -0
- package/{data → dist/data}/README.md +2 -1
- package/dist/data/bart-stations.json +134 -0
- package/dist/data/catalog.json +4718 -0
- package/dist/data/coast.json +36 -0
- package/dist/data/districts-2002.json +325 -0
- package/dist/data/districts-2012.json +325 -0
- package/dist/data/districts-2022.json +325 -0
- package/dist/data/highways.json +4138 -0
- package/dist/data/index.d.ts +26 -0
- package/{data → dist/data}/index.js +37 -39
- package/dist/data/key-roads.json +12662 -0
- package/dist/data/landmarks.json +167 -0
- package/dist/data/neighborhoods-analysis.json +817 -0
- package/dist/data/neighborhoods-realtor.json +1978 -0
- package/dist/data/neighborhoods.json +2267 -0
- package/dist/data/types.d.ts +117 -0
- package/dist/data/types.js +1 -0
- package/dist/src/data.d.ts +25 -0
- package/{src → dist/src}/data.js +17 -20
- package/dist/src/explorer-layout.d.ts +23 -0
- package/dist/src/explorer-layout.js +72 -0
- package/dist/src/explorer.d.ts +4 -0
- package/dist/src/explorer.js +961 -0
- package/dist/src/geometry.d.ts +5 -0
- package/dist/src/geometry.js +53 -0
- package/dist/src/immutable.d.ts +2 -0
- package/dist/src/immutable.js +9 -0
- package/dist/src/index.d.ts +13 -0
- package/dist/src/index.js +125 -0
- package/dist/src/interactive.d.ts +4 -0
- package/dist/src/interactive.js +5 -0
- package/dist/src/layers.d.ts +36 -0
- package/dist/src/layers.js +64 -0
- package/dist/src/navigation.d.ts +6 -0
- package/dist/src/navigation.js +145 -0
- package/dist/src/overlays.d.ts +25 -0
- package/dist/src/overlays.js +15 -0
- package/dist/src/svg.d.ts +4 -0
- package/dist/src/svg.js +7 -0
- package/dist/src/transit.d.ts +3 -0
- package/dist/src/transit.js +105 -0
- package/dist/src/types.d.ts +112 -0
- package/dist/src/types.js +1 -0
- package/dist/src/viewport.d.ts +5 -0
- package/dist/src/viewport.js +27 -0
- package/package.json +40 -25
- package/data/bart-stations.json +0 -134
- package/data/catalog.json +0 -4699
- package/data/coast.json +0 -36
- package/data/districts-2002.json +0 -325
- package/data/districts-2012.json +0 -325
- package/data/districts-2022.json +0 -325
- package/data/highways.json +0 -4138
- package/data/index.d.ts +0 -117
- package/data/landmarks.json +0 -167
- package/data/neighborhoods-analysis.json +0 -817
- package/data/neighborhoods-realtor.json +0 -1978
- package/data/neighborhoods.json +0 -2267
- package/src/geometry.d.ts +0 -8
- package/src/geometry.js +0 -51
- package/src/immutable.js +0 -8
- package/src/index.d.ts +0 -51
- package/src/index.js +0 -122
- package/src/layers.js +0 -54
- package/src/overlays.js +0 -18
- package/src/svg.js +0 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,13 +1,54 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
User-visible changes are recorded here. Unreleased entries describe changes on `main` that are not part of a tagged package release.
|
|
4
|
+
|
|
5
|
+
## Unreleased
|
|
6
|
+
|
|
7
|
+
## 1.1.0
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Public [GitHub Pages explorer](https://kahwee.github.io/sf-map-svg/) with neighborhood search, map modes, usage tips, SVG examples, and GeoJSON downloads.
|
|
12
|
+
- Automatic Pages builds and deployment from `main`, plus `pnpm build:pages` for local previews.
|
|
13
|
+
- 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.
|
|
14
|
+
- Play/pause controls, a keyboard-accessible journey slider, pause-on-hidden behavior, and a Storybook transit example. Animation starts paused.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Reorganize the README around installation, choosing an API, static and interactive options, geographic data, development, and Pages deployment.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- Release map listeners, observers, animation frames, and download URLs when explorer initialization fails. Add browser regression coverage for failed initialization.
|
|
23
|
+
|
|
24
|
+
## 1.0.0
|
|
25
|
+
|
|
26
|
+
- Publish the stable 1.0 API with public npm access and a public source repository.
|
|
27
|
+
- Add the reusable interactive map entrypoint, controlled viewport and selection APIs, keyboard/touch navigation, and marker selection.
|
|
28
|
+
- Add interactive examples, Storybook stories, and viewport/navigation regression checks.
|
|
29
|
+
- Add district/neighborhood explorer modes and a labels toggle, with fixed screen-size labels while zooming.
|
|
30
|
+
- Add a master visible-label switch for standalone SVGs.
|
|
31
|
+
- Add nine optional key-road landmarks from DataSF centerlines, public JSON, and zoom-aware explorer labels.
|
|
32
|
+
- Add a transit-inspired map theme with pale water, quiet land, green parks, and blue station symbols.
|
|
33
|
+
- Refine the neighborhood explorer presentation and map legend.
|
|
34
|
+
- Add a transit example and Storybook preset.
|
|
35
|
+
|
|
36
|
+
## 0.4.1
|
|
37
|
+
|
|
38
|
+
- Migrate library source to strict TypeScript 7 with generated JavaScript and declarations.
|
|
39
|
+
- Replace Prettier with Biome formatting, import organization, and recommended lint rules.
|
|
40
|
+
|
|
41
|
+
## 0.4.0
|
|
42
|
+
|
|
43
|
+
- Add a browser neighborhood explorer with alias search, selection, boundary zoom, and GeoJSON downloads.
|
|
44
|
+
- Adapt explorer labels to zoom and viewport size while preserving static SVG defaults.
|
|
45
|
+
- License software under MIT and add npm release automation and packaged-consumer checks.
|
|
46
|
+
|
|
3
47
|
## 0.3.0
|
|
4
48
|
|
|
5
49
|
- Enable public npm distribution with explicit public registry access.
|
|
6
|
-
|
|
7
50
|
- Remove realtor boundary sliver overlaps while preserving all 92 neighborhoods and their combined footprint; enforce disjoint interiors in regression tests.
|
|
8
|
-
|
|
9
51
|
- Use the 92 SFAR realtor neighborhoods by default for map outlines, data lookup, and Storybook. Other source collections remain available.
|
|
10
|
-
|
|
11
52
|
- Move all geographic data to exported canonical GeoJSON files, preserving district display extras.
|
|
12
53
|
- Expose complete SF Find (117), analysis (41), and realtor (92) neighborhood collections with source-specific canonical names and documented aliases.
|
|
13
54
|
- Add immutable data helpers, exact-name lookup, source-aware search, and a typed geometry export.
|
package/LICENSE
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
|
-
|
|
1
|
+
MIT License
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Copyright (c) 2026 KahWee Teng
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
Geographic data retains its source terms and attribution requirements;
|
|
24
|
+
see SOURCES.md. This software license does not replace those terms.
|
package/README.md
CHANGED
|
@@ -1,121 +1,246 @@
|
|
|
1
1
|
# San Francisco SVG maps
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[Explore the live map](https://kahwee.github.io/sf-map-svg/) · [Data guide](data/README.md) · [Geographic sources](SOURCES.md) · [Contributing](CONTRIBUTING.md)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
pnpm add @kahwee/sf-map-svg
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Use Node 22.12+ for server-side rendering. Browser components need a DOM and a bundler that supports the package’s JSON imports.
|
|
16
|
+
|
|
17
|
+
| Start with | Entry point | What you get |
|
|
18
|
+
| --- | --- | --- |
|
|
19
|
+
| Static SVG | `@kahwee/sf-map-svg` | SVG markup, projection helpers, optional layers |
|
|
20
|
+
| Neighborhood explorer | `@kahwee/sf-map-svg/explorer` | Search, source selection, map controls, GeoJSON downloads |
|
|
21
|
+
| Interactive map | `@kahwee/sf-map-svg/interactive` | Embeddable map and controls without the explorer sidebar |
|
|
22
|
+
| Geographic data | `@kahwee/sf-map-svg/data` | Source-aware lookup and canonical GeoJSON |
|
|
23
|
+
|
|
24
|
+
## Render a static map
|
|
6
25
|
|
|
7
26
|
```js
|
|
8
|
-
import {
|
|
27
|
+
import { writeFile } from 'node:fs/promises';
|
|
28
|
+
import { renderSFMap } from '@kahwee/sf-map-svg';
|
|
9
29
|
|
|
10
30
|
const svg = renderSFMap({
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
landmarks: true, // parks with labels
|
|
14
|
-
bartStations: true, // all eight SF stations
|
|
31
|
+
landmarks: true,
|
|
32
|
+
bartStations: true,
|
|
15
33
|
highways: true,
|
|
16
|
-
neighborhoodLines: true,
|
|
34
|
+
neighborhoodLines: true,
|
|
17
35
|
markers: [{ id: 'dolores', lng: -122.4269, lat: 37.7596, label: 'Dolores Park' }],
|
|
18
36
|
});
|
|
37
|
+
|
|
38
|
+
await writeFile('san-francisco.svg', svg);
|
|
19
39
|
```
|
|
20
40
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
##
|
|
24
|
-
|
|
25
|
-
| Option
|
|
26
|
-
|
|
|
27
|
-
| `year`
|
|
28
|
-
| `districtLines`
|
|
29
|
-
| `neighborhoodLines` | `false`
|
|
30
|
-
| `
|
|
31
|
-
| `
|
|
32
|
-
| `
|
|
33
|
-
| `
|
|
34
|
-
| `
|
|
35
|
-
| `
|
|
36
|
-
| `
|
|
37
|
-
| `
|
|
38
|
-
| `
|
|
39
|
-
| `
|
|
40
|
-
| `
|
|
41
|
+
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.
|
|
42
|
+
|
|
43
|
+
## Static options
|
|
44
|
+
|
|
45
|
+
| Option | Default | Purpose |
|
|
46
|
+
| --- | --- | --- |
|
|
47
|
+
| `year` | `2022` | District boundaries: `2002`, `2012`, or `2022` |
|
|
48
|
+
| `districtLines` | `true` | Supervisorial district outlines |
|
|
49
|
+
| `neighborhoodLines` | `false` | Dashed SFAR realtor neighborhood outlines |
|
|
50
|
+
| `theme` | `'districts'` | Use `'transit'` for pale blue water, ivory land, green parks, and blue BART symbols; custom `colors` still take precedence |
|
|
51
|
+
| `districtFills` | `true` | Original Site’s eleven muted district colors |
|
|
52
|
+
| `labels` | `true` | Master switch for visible map text; symbols and accessible titles remain |
|
|
53
|
+
| `districtLabels` | `true` | District number badges |
|
|
54
|
+
| `highways` | `false` | Original Site’s highway geometry |
|
|
55
|
+
| `landmarks` | `false` | Golden Gate Park, Presidio, Lincoln Park, Twin Peaks, Dolores Park, and McLaren Park |
|
|
56
|
+
| `bartStations` | `false` | Eight San Francisco BART stations with blue rings and names |
|
|
57
|
+
| `width`, `height` | `800`, `800` | SVG viewBox and intrinsic size |
|
|
58
|
+
| `padding` | `28` | Space around the coast |
|
|
59
|
+
| `markers` | `[]` | Points with `id`, `lng`, `lat`, optional `label`, `color`, `selected` |
|
|
60
|
+
| `keyRoads` | `false` | Nine selected road corridors and names for orientation |
|
|
61
|
+
| `colors` | Built-in palette | Override `water`, `land`, `district`, `neighborhood`, `highway`, `road`, `park`, `landmark`, `bart`, `label`, `marker`, `selected` |
|
|
62
|
+
| `title` | `San Francisco map` | Accessible SVG title |
|
|
63
|
+
| `idPrefix` | Unique per process | Set explicitly for deterministic output or independent server renders |
|
|
41
64
|
|
|
42
65
|
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).
|
|
43
66
|
|
|
44
67
|
`createSFMap(options)` returns `{ svg, project, viewBox }`. `project([longitude, latitude])` gives matching SVG coordinates for custom overlays. Named exports also include `districtYears`, `districtColors`, and `neighborhoodNames`.
|
|
45
68
|
|
|
46
|
-
|
|
69
|
+
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.
|
|
47
70
|
|
|
48
|
-
|
|
49
|
-
pnpm add @kahwee/sf-map-svg
|
|
50
|
-
```
|
|
71
|
+
`keyRoads: true` adds Market, Mission, Geary, Van Ness, 19th Avenue, Sunset, The Embarcadero, Columbus, and Divisadero using DataSF centerlines. These are orientation features, not routing guidance.
|
|
51
72
|
|
|
52
|
-
The package is published publicly on npm. Geographic JSON files are included in the package. See `LICENSE` and `SOURCES.md` for software and source-data rights.
|
|
53
73
|
|
|
54
|
-
##
|
|
74
|
+
## Neighborhood explorer
|
|
55
75
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
76
|
+
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.
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
import { createNeighborhoodExplorer } from '@kahwee/sf-map-svg/explorer';
|
|
80
|
+
|
|
81
|
+
const explorer = createNeighborhoodExplorer({ source: 'realtor' });
|
|
82
|
+
document.querySelector('#map').append(explorer);
|
|
83
|
+
explorer.selectNeighborhood('NoPa');
|
|
84
|
+
|
|
85
|
+
// Before removing the component, release its observers and event listeners.
|
|
86
|
+
// explorer.destroy();
|
|
60
87
|
```
|
|
61
88
|
|
|
62
|
-
|
|
89
|
+
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()`.
|
|
63
90
|
|
|
64
|
-
|
|
91
|
+
Selected downloads are one-feature GeoJSON FeatureCollections retaining source attribution and boundary-processing metadata.
|
|
65
92
|
|
|
66
|
-
|
|
93
|
+
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.
|
|
67
94
|
|
|
68
|
-
|
|
95
|
+
### Map modes and labels
|
|
69
96
|
|
|
70
|
-
|
|
97
|
+
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.
|
|
71
98
|
|
|
72
99
|
```js
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
'san-francisco.svg',
|
|
78
|
-
renderSFMap({
|
|
79
|
-
landmarks: true,
|
|
80
|
-
bartStations: true,
|
|
81
|
-
highways: true,
|
|
82
|
-
}),
|
|
83
|
-
);
|
|
100
|
+
const explorer = createNeighborhoodExplorer({ mode: 'districts', labels: true });
|
|
101
|
+
explorer.setLabels(false);
|
|
102
|
+
explorer.setMode('neighborhoods');
|
|
103
|
+
explorer.setLabels(true);
|
|
84
104
|
```
|
|
85
105
|
|
|
86
|
-
|
|
106
|
+
`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.
|
|
107
|
+
|
|
108
|
+
## Reusable interactive map
|
|
109
|
+
|
|
110
|
+
The `@kahwee/sf-map-svg/interactive` entry point provides a map, accessible controls, and
|
|
111
|
+
attribution without the explorer's search sidebar or detail panel. It does not change URLs,
|
|
112
|
+
load articles, apply editorial filters, or navigate. Importing the static entry point does not
|
|
113
|
+
import this interactive runtime. Both paths retain zero runtime dependencies.
|
|
87
114
|
|
|
88
115
|
```js
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
116
|
+
import { createInteractiveSFMap } from '@kahwee/sf-map-svg/interactive';
|
|
117
|
+
|
|
118
|
+
const map = createInteractiveSFMap({
|
|
119
|
+
mode: 'neighborhoods',
|
|
120
|
+
source: 'analysis', // 'sf-find' and 'realtor' are also available
|
|
121
|
+
theme: 'transit',
|
|
122
|
+
labelSize: { min: 12, max: 15 },
|
|
123
|
+
layers: { districtFills: false, districtLines: false, districtLabels: false },
|
|
124
|
+
});
|
|
125
|
+
document.querySelector('#map').append(map);
|
|
126
|
+
map.addEventListener('neighborhoodchange', ({ detail }) => {
|
|
127
|
+
// On clear: id, name, and feature are null. source always identifies the dataset.
|
|
128
|
+
console.log(detail.id, detail.name, detail.source);
|
|
94
129
|
});
|
|
130
|
+
map.selectNeighborhood('Mission', { fit: false });
|
|
131
|
+
const selection = map.getSelection(); // { id, name, source, feature } or null
|
|
132
|
+
map.selectNeighborhood(null);
|
|
95
133
|
```
|
|
96
134
|
|
|
97
|
-
|
|
135
|
+
`createInteractiveSFMap()` defaults to `mode: 'basemap'`: no district or neighborhood layers.
|
|
136
|
+
Parks, roads, and stations are initially enabled and can be disabled independently. The existing
|
|
137
|
+
`createNeighborhoodExplorer()` and static APIs retain their realtor/district defaults. The
|
|
138
|
+
source option only chooses a definition collection; it does not make that collection visible
|
|
139
|
+
in basemap mode. Editorial groupings belong to the consumer and are never treated as geographic
|
|
140
|
+
aliases. Import types including `InteractiveSFMapOptions`, `InteractiveSFMapElement`,
|
|
141
|
+
`MapViewport`, `MapPadding`, and `NeighborhoodSelection` from the interactive entry point.
|
|
142
|
+
|
|
143
|
+
| Option | Default | Behavior |
|
|
144
|
+
| --- | --- | --- |
|
|
145
|
+
| `mode` | `basemap` (interactive), `neighborhoods` (explorer) | `basemap`, `districts`, or `neighborhoods`; establishes layer defaults |
|
|
146
|
+
| `source` | `realtor` | `realtor`, `sf-find`, or `analysis`; explicit source recommended for reusable integrations |
|
|
147
|
+
| `theme` | `transit` | `transit` or `districts` |
|
|
148
|
+
| `year` | `2022` | District vintage: 2002, 2012, or 2022 |
|
|
149
|
+
| `layers` | Mode defaults | Independent booleans: `districtFills`, `districtLines`, `districtLabels`, `neighborhoodLines`, `neighborhoodLabels`, `landmarks`, `bartStations`, `highways`, `keyRoads` |
|
|
150
|
+
| `labels` | `true` | Master visible-text switch; accessible descriptions and station symbols remain |
|
|
151
|
+
| `labelSize` | `{ min: 11, max: 12 }` | Screen-pixel range, 8–32 allowed. Nominal sizes are 11 for roads and 12 otherwise, clamped to this range; sizes never grow with zoom |
|
|
152
|
+
| `selectableNeighborhoods` | `true` | Enables pointer/keyboard selection; false retains geography and labels |
|
|
153
|
+
| `neighborhood` | Unset | Initial name, alias, or ID in the selected source |
|
|
154
|
+
| `fitPadding` | `24` | Screen pixels, number or `{ top, right, bottom, left }`, used by selection and geometry fitting after mounting |
|
|
155
|
+
| `markers` | `[]` | Supplied `MapMarker` records with unique, nonempty IDs; no content fetching |
|
|
156
|
+
| `markerRadius` / `markerHitSize` | `6` / `44` | Screen-pixel visible radius and tap-target diameter, independent of zoom; selected radius grows by 2px |
|
|
157
|
+
| `markerColor` / `selectedMarkerColor` | `#245b61` / `#f04f32` | Default marker colors; individual `marker.color` overrides the unselected color |
|
|
158
|
+
| `onMarkerActivate` | Unset | Called when a non-null marker selection changes, including programmatic changes |
|
|
159
|
+
|
|
160
|
+
Explicit layer options override mode defaults even after `setMode()`. District fills, outlines,
|
|
161
|
+
and badges can therefore be composed with neighborhood names without requiring district
|
|
162
|
+
labels. Descriptions identify only displayed boundary layers, their source and available
|
|
163
|
+
vintage; analysis data is described as census-tract-based reporting areas, without inventing a
|
|
164
|
+
boundary year. The downloadable source metadata remains available in the full explorer.
|
|
165
|
+
|
|
166
|
+
Labels use measured screen-space collision boxes. Selected marker and neighborhood names
|
|
167
|
+
come first, then district labels, stations, parks, roads, and neighborhoods in descending area
|
|
168
|
+
order with stable ID tie-breaking. Station and marker symbols reserve space. Roads and station
|
|
169
|
+
names appear at 1.8× zoom; city view keeps park labels sparse. Labels outside the viewport or
|
|
170
|
+
without room are suppressed, including a selected label too wide to fit. Selection names remain
|
|
171
|
+
available in the native chooser and through events. The label range is separate from SVG user
|
|
172
|
+
units used by the static renderer.
|
|
173
|
+
|
|
174
|
+
### Viewport and controlled selection
|
|
98
175
|
|
|
99
176
|
```js
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
177
|
+
const saved = map.getViewport(); // copied [x, y, size] in the 800×800 projected map
|
|
178
|
+
map.zoomBy(2);
|
|
179
|
+
map.panBy(30, 0); // move view east by 30 screen pixels
|
|
180
|
+
map.setViewport(saved);
|
|
181
|
+
map.resetView();
|
|
182
|
+
map.fitGeometry({
|
|
183
|
+
type: 'MultiPoint',
|
|
184
|
+
coordinates: places.map(({ lng, lat }) => [lng, lat]),
|
|
185
|
+
}, { top: 24, right: 24, bottom: 80, left: 24 });
|
|
186
|
+
map.addEventListener('viewportchange', ({ detail }) => saveInYourState(detail.viewport));
|
|
105
187
|
```
|
|
106
188
|
|
|
107
|
-
|
|
189
|
+
Call `fitGeometry` after mounting in a visible container. It accepts WGS84 GeoJSON geometry,
|
|
190
|
+
including `Point`, `MultiPoint`, and `GeometryCollection`, and rejects empty geometry or
|
|
191
|
+
impossible padding. Views are constrained to the city extent and 1–12× zoom. Consequently,
|
|
192
|
+
padding is best-effort near city edges or for extents larger than the map; a single point fits
|
|
193
|
+
at maximum zoom. This is an SF map, not a world map. Resizing keeps the projected view and
|
|
194
|
+
recomputes screen sizes; call `fitGeometry` again if a new container size needs different fitting.
|
|
195
|
+
Initial neighborhood fitting before mounting uses the explorer's proportional padding.
|
|
108
196
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
197
|
+
`selectNeighborhood(nameOrIdOrNull, { fit: false })` and `selectMarker(idOrNull, { fit: false })`
|
|
198
|
+
allow external state to drive selection without moving the viewport. They return false for
|
|
199
|
+
unknown identities. `getSelection()` and `getSelectedMarker()` read current selection.
|
|
200
|
+
`setSource(source)` clears neighborhood selection and resets the view, emitting a clear event
|
|
201
|
+
when needed. `setMode(mode)` resets the viewport. Repeating the same viewport or selection
|
|
202
|
+
emits no change event, avoiding state feedback loops. All events bubble. Call `destroy()`
|
|
203
|
+
before removing the element to release listeners, observers, frames, and download URLs.
|
|
113
204
|
|
|
114
|
-
|
|
205
|
+
### Dense markers
|
|
115
206
|
|
|
116
|
-
|
|
207
|
+
```js
|
|
208
|
+
const map = createInteractiveSFMap({ markers: places });
|
|
209
|
+
document.querySelector('#map').append(map);
|
|
210
|
+
map.addEventListener('markerchange', ({ detail }) => {
|
|
211
|
+
// { id, marker }, both null when cleared. Render your own content panel here.
|
|
212
|
+
renderSelection(detail.marker);
|
|
213
|
+
});
|
|
214
|
+
map.setMarkers(updatedPlaces);
|
|
215
|
+
map.selectMarker('place-id');
|
|
216
|
+
```
|
|
117
217
|
|
|
118
|
-
|
|
218
|
+
Every supplied marker remains in the native chooser, even when positions coincide or markers
|
|
219
|
+
are outside the current view. Pointer and keyboard activation select and fit a marker; markers
|
|
220
|
+
also have individual keyboard stops. This is the accessible-choice alternative to clustering:
|
|
221
|
+
no counts are estimated and no supplied markers are silently dropped. The chooser count is
|
|
222
|
+
the supplied collection size. Replacing markers retains the selected ID when present, otherwise
|
|
223
|
+
uses an explicitly selected marker or clears selection. Large hit targets can overlap; use the
|
|
224
|
+
chooser to reach obscured markers. Automated clustering is not included in this release.
|
|
225
|
+
|
|
226
|
+
### Gesture policy and keyboard access
|
|
227
|
+
|
|
228
|
+
- **Default touch:** one finger scrolls the page; pinch zooms the browser. Map buttons and
|
|
229
|
+
native choosers work without engaging map gestures.
|
|
230
|
+
- **Touch navigation:** explicitly enable the visible button (or `setTouchNavigation(true)`).
|
|
231
|
+
One finger pans the map; two fingers pan and pinch around their midpoint. The button becomes
|
|
232
|
+
**Done: page scrolling**. Use it or Escape to return to page gestures. The page remains
|
|
233
|
+
scrollable outside the canvas, and Tab can leave it. Changing mode during an active gesture
|
|
234
|
+
takes effect after fingers are lifted.
|
|
235
|
+
- **Mouse:** drag pans; Ctrl/⌘ + wheel zooms. Ordinary wheel scrolling remains page scrolling.
|
|
236
|
+
- **Keyboard:** focus the map, then arrows pan, +/− zoom, Home resets, and Escape exits touch
|
|
237
|
+
navigation. Tab reaches controls, the neighborhood chooser, one neighborhood path, and markers.
|
|
238
|
+
On a neighborhood path, `[` / `]` moves through source features and Enter/Space selects.
|
|
239
|
+
The native chooser is also available for areas outside the current view.
|
|
240
|
+
- Pointer cancellation, loss of capture, window blur, and resizing cancel active gestures.
|
|
241
|
+
There is no animated camera or inertia. Button transitions are disabled with reduced motion.
|
|
242
|
+
|
|
243
|
+
## Geographic data and lookup
|
|
119
244
|
|
|
120
245
|
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.
|
|
121
246
|
|
|
@@ -131,3 +256,71 @@ const matchingDefinitions = searchNeighborhoods('mission');
|
|
|
131
256
|
```
|
|
132
257
|
|
|
133
258
|
These are 250 **source-specific definitions**, not 250 distinct neighborhoods. Canonical names are package display names, and boundaries reflect each documented source rather than a claimed universal consensus. Mission and Outer Mission remain distinct. JSON files are the source of truth used by the renderer; the default map and lookup use the 92 SFAR realtor neighborhoods. See [the data API guide](data/README.md) for all filenames, schema, lookup rules, source comparisons, and custom SVG overlays. Storybook provides downloadable JSON files beside its neighborhood examples.
|
|
259
|
+
|
|
260
|
+
## Development
|
|
261
|
+
|
|
262
|
+
Requires Node 22.12+ and pnpm 12. The library uses strict TypeScript; the build emits JavaScript and declarations to `dist/`.
|
|
263
|
+
|
|
264
|
+
```sh
|
|
265
|
+
pnpm install --frozen-lockfile
|
|
266
|
+
pnpm check # formatting, data catalog, types, and tests
|
|
267
|
+
pnpm demo # generated SVGs and example pages
|
|
268
|
+
pnpm build-storybook # static component documentation
|
|
269
|
+
pnpm test:package # install and check the packed package
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Use `pnpm format` to apply Biome formatting and safe lint fixes. Run `pnpm storybook` for interactive component examples at http://127.0.0.1:6006. CI runs checks on Node 22, 24, and 26. See [CONTRIBUTING.md](CONTRIBUTING.md) for source structure and release instructions.
|
|
273
|
+
|
|
274
|
+
### Browser verification
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
Run `pnpm demo` and serve the repository root. `examples/generated/index.html` covers static
|
|
278
|
+
maps, `explorer.html` covers the full explorer, and `interactive.html` covers independently
|
|
279
|
+
controlled neighborhood selection, 36 overlapping sample markers, and fit/save/restore hooks.
|
|
280
|
+
Storybook **Maps / Reusable interactive map** includes both themes, selectable neighborhoods,
|
|
281
|
+
independent layers, dense markers, and a 390px example.
|
|
282
|
+
|
|
283
|
+
With that server running, the browser regression checks can be run through the installed CLI:
|
|
284
|
+
|
|
285
|
+
```sh
|
|
286
|
+
agent-browser skills get core --full
|
|
287
|
+
agent-browser --session sf-map-check open http://127.0.0.1:8765/examples/generated/interactive.html
|
|
288
|
+
agent-browser --session sf-map-check eval "import('/scripts/check-interactive-browser.mjs').then(m => m.checkInteractiveBrowser())"
|
|
289
|
+
agent-browser --session sf-map-check close
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
These checks cover actual browser layout, both themes, narrow/wide containers, label collisions,
|
|
293
|
+
zoom extremes, keyboard selection, cancellation logic, viewport state, marker reachability, and
|
|
294
|
+
teardown. Physical iOS Safari and Android Chrome verification remains required before a release:
|
|
295
|
+
check page scroll and browser pinch in default mode; map pan and pinch in engaged mode; lift one
|
|
296
|
+
finger; interrupt/cancel; rotate; use Done; verify scrolling resumes. Desktop automation and
|
|
297
|
+
synthetic pointer tests do not establish physical-device compatibility.
|
|
298
|
+
|
|
299
|
+
## GitHub Pages
|
|
300
|
+
|
|
301
|
+
The [live explorer](https://kahwee.github.io/sf-map-svg/) provides neighborhood search, 2022 district views, SVG examples, and GeoJSON downloads. Its source is in `website/` and uses the public explorer API.
|
|
302
|
+
|
|
303
|
+
```sh
|
|
304
|
+
pnpm build:pages
|
|
305
|
+
python3 -m http.server 8765 --directory pages-dist
|
|
306
|
+
# Open http://localhost:8765
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
The build bundles local assets into `pages-dist/` with relative URLs for GitHub’s project path. `.github/workflows/pages.yml` validates and deploys pushes to `main` using GitHub Actions. It does not publish npm packages or releases.
|
|
310
|
+
|
|
311
|
+
## License and attribution
|
|
312
|
+
|
|
313
|
+
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.
|
|
314
|
+
|
|
315
|
+
### Schematic transit animation
|
|
316
|
+
|
|
317
|
+
```js
|
|
318
|
+
import { createTransitAnimation } from '@kahwee/sf-map-svg/transit';
|
|
319
|
+
const animation = createTransitAnimation();
|
|
320
|
+
document.querySelector('#transit').append(animation);
|
|
321
|
+
// On removal: animation.destroy();
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
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.
|
|
325
|
+
|
|
326
|
+
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>`.
|
package/SOURCES.md
CHANGED
|
@@ -75,3 +75,7 @@ No survey of resident consensus is claimed. Composite areas are not converted to
|
|
|
75
75
|
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.
|
|
76
76
|
|
|
77
77
|
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.
|
|
78
|
+
|
|
79
|
+
## Key road landmarks
|
|
80
|
+
|
|
81
|
+
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 724 source segments into nine named corridors, preserving every source coordinate and CNN segment ID. 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.
|
|
@@ -16,6 +16,7 @@ The renderer, `neighborhoods` convenience export, and `getNeighborhood` default
|
|
|
16
16
|
| `neighborhoods-realtor.json` | All 92 SFAR areas in the August 2010 dataset |
|
|
17
17
|
| `coast.json` | The renderer's common display coastline |
|
|
18
18
|
| `highways.json` | The original map's highway geometry |
|
|
19
|
+
| `key-roads.json` | Nine selected road corridors with source segment IDs and label anchors |
|
|
19
20
|
| `landmarks.json` | Six selected park/landmark property areas |
|
|
20
21
|
| `bart-stations.json` | Eight San Francisco station points |
|
|
21
22
|
| `catalog.json` | Dataset index and searchable neighborhood metadata without geometry |
|
|
@@ -32,7 +33,7 @@ const mission = neighborhoods.features.find((f) => f.id === 'inner-mission');
|
|
|
32
33
|
console.log(mission.properties.canonicalName, mission.geometry);
|
|
33
34
|
```
|
|
34
35
|
|
|
35
|
-
Node 22.12+ supports this syntax. Bundlers may also support JSON imports without the import attribute. Files are included in the package archive; non-JavaScript consumers can parse the same JSON files directly.
|
|
36
|
+
Node 22.12+ supports this syntax. Bundlers may also support JSON imports without the import attribute. Files are included in the package archive; non-JavaScript consumers can parse the same JSON files directly. These are package entry points, not a hosted API. The public repository and GitHub Pages explorer also provide access to the source data. Consumers who need only one dataset should import its JSON subpath rather than the convenience module, which loads all collections.
|
|
36
37
|
|
|
37
38
|
## Names and definitions
|
|
38
39
|
|