@kahwee/sf-map-svg 1.0.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 CHANGED
@@ -1,16 +1,34 @@
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
+
3
24
  ## 1.0.0
4
25
 
5
26
  - Publish the stable 1.0 API with public npm access and a public source repository.
6
27
  - Add the reusable interactive map entrypoint, controlled viewport and selection APIs, keyboard/touch navigation, and marker selection.
7
28
  - Add interactive examples, Storybook stories, and viewport/navigation regression checks.
8
-
9
29
  - Add district/neighborhood explorer modes and a labels toggle, with fixed screen-size labels while zooming.
10
30
  - Add a master visible-label switch for standalone SVGs.
11
-
12
31
  - Add nine optional key-road landmarks from DataSF centerlines, public JSON, and zoom-aware explorer labels.
13
-
14
32
  - Add a transit-inspired map theme with pale water, quiet land, green parks, and blue station symbols.
15
33
  - Refine the neighborhood explorer presentation and map legend.
16
34
  - Add a transit example and Storybook preset.
@@ -29,11 +47,8 @@
29
47
  ## 0.3.0
30
48
 
31
49
  - Enable public npm distribution with explicit public registry access.
32
-
33
50
  - Remove realtor boundary sliver overlaps while preserving all 92 neighborhoods and their combined footprint; enforce disjoint interiors in regression tests.
34
-
35
51
  - Use the 92 SFAR realtor neighborhoods by default for map outlines, data lookup, and Storybook. Other source collections remain available.
36
-
37
52
  - Move all geographic data to exported canonical GeoJSON files, preserving district display extras.
38
53
  - Expose complete SF Find (117), analysis (41), and realtor (92) neighborhood collections with source-specific canonical names and documented aliases.
39
54
  - Add immutable data helpers, exact-name lookup, source-aware search, and a typed geometry export.
package/README.md CHANGED
@@ -1,143 +1,77 @@
1
1
  # San Francisco SVG maps
2
2
 
3
- ![District fills, optional neighborhood boundaries, and plain outlines](docs/map-preview.png)
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
- An MIT-licensed package extracted from KahWee’s **San Francisco District Map** Site. Draws a self-contained SVG with bundled geometry and no runtime dependencies, tiles, WebGL, or network requests.
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
6
 
7
- ```js
8
- import { renderSFMap, createSFMap } from '@kahwee/sf-map-svg';
9
-
10
- const svg = renderSFMap({
11
- year: 2022,
12
- districtLines: true,
13
- landmarks: true, // parks with labels
14
- bartStations: true, // all eight SF stations
15
- highways: true,
16
- neighborhoodLines: true, // optional; off by default
17
- markers: [{ id: 'dolores', lng: -122.4269, lat: 37.7596, label: 'Dolores Park' }],
18
- });
19
- ```
20
-
21
- Write `svg` to a `.svg` file or embed it in your page. For Astro, render it with `<div set:html={svg} />`. Text and attribute values are XML escaped.
22
-
23
- ## Layers and options
24
-
25
- | Option | Default | Purpose |
26
- | ------------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------- |
27
- | `year` | `2022` | District boundaries: `2002`, `2012`, or `2022` |
28
- | `districtLines` | `true` | Supervisorial district outlines |
29
- | `neighborhoodLines` | `false` | Dashed SFAR realtor neighborhood outlines |
30
- | `theme` | `'districts'` | Use `'transit'` for pale blue water, ivory land, green parks, and blue BART symbols; custom `colors` still take precedence |
31
- | `districtFills` | `true` | Original Site’s eleven muted district colors |
32
- | `labels` | `true` | Master switch for visible map text; symbols and accessible titles remain |
33
- | `districtLabels` | `true` | District number badges |
34
- | `highways` | `false` | Original Site’s highway geometry |
35
- | `landmarks` | `false` | Golden Gate Park, Presidio, Lincoln Park, Twin Peaks, Dolores Park, and McLaren Park |
36
- | `bartStations` | `false` | Eight San Francisco BART stations with blue rings and names |
37
- | `width`, `height` | `800`, `800` | SVG viewBox and intrinsic size |
38
- | `padding` | `28` | Space around the coast |
39
- | `markers` | `[]` | Points with `id`, `lng`, `lat`, optional `label`, `color`, `selected` |
40
- | `keyRoads` | `false` | Nine selected road corridors and names for orientation |
41
- | `colors` | Built-in palette | Override `water`, `land`, `district`, `neighborhood`, `highway`, `road`, `park`, `landmark`, `bart`, `label`, `marker`, `selected` |
42
- | `title` | `San Francisco map` | Accessible SVG title |
43
- | `idPrefix` | Unique per process | Set explicitly for deterministic output or independent server renders |
44
-
45
- 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).
46
-
47
- `createSFMap(options)` returns `{ svg, project, viewBox }`. `project([longitude, latitude])` gives matching SVG coordinates for custom overlays. Named exports also include `districtYears`, `districtColors`, and `neighborhoodNames`.
7
+ ![San Francisco district maps with optional neighborhood boundaries](docs/map-preview.png)
48
8
 
49
- ## Installation
9
+ ## Install
50
10
 
51
11
  ```sh
52
12
  pnpm add @kahwee/sf-map-svg
53
13
  ```
54
14
 
55
- 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.
56
-
57
- ## Development
58
-
59
- ```sh
60
- pnpm install --frozen-lockfile
61
- pnpm test
62
- pnpm demo
63
- ```
64
-
65
- Open `examples/generated/index.html` to compare district and neighborhood maps. Generated SVG files are there too. See [CONTRIBUTING.md](CONTRIBUTING.md) for source structure, checks and release steps. GitHub Actions runs validation; npm releases use public access.
66
-
67
- The package uses a small Mercator SVG renderer while retaining the Site’s boundary geometry, coastline, palette, district labels, and highway data.
15
+ Use Node 22.12+ for server-side rendering. Browser components need a DOM and a bundler that supports the package’s JSON imports.
68
16
 
69
- The `theme: 'transit'` preset borrows the clear visual hierarchy of [BART’s system map](https://www.bart.gov/system-map), retaining geographic positions. It uses a quiet, single-color land fill instead of district colors. The neighborhood explorer uses this preset.
70
-
71
- Enable `landmarks`, `bartStations`, and `highways` together for the featured example. Park fills use `colors.park`, park labels use `colors.landmark`, and station rings and labels use `colors.bart`. These current geographic overlays are independent of the district year; BART stations are city-only (Daly City is outside the map). Station positions are geographic points, not a route diagram.
72
-
73
- ## Examples
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 |
74
23
 
75
- ### Landmarks, BART stations, and highways
24
+ ## Render a static map
76
25
 
77
26
  ```js
78
- import { renderSFMap } from '@kahwee/sf-map-svg';
79
27
  import { writeFile } from 'node:fs/promises';
28
+ import { renderSFMap } from '@kahwee/sf-map-svg';
80
29
 
81
- await writeFile(
82
- 'san-francisco.svg',
83
- renderSFMap({
84
- landmarks: true,
85
- bartStations: true,
86
- highways: true,
87
- }),
88
- );
89
- ```
90
-
91
- ### Plain map with a selected place
92
-
93
- ```js
94
- const svg = renderSFMap({
95
- districtFills: false,
96
- districtLabels: false,
97
- landmarks: true,
98
- markers: [{ id: 'dolores', lng: -122.4269, lat: 37.7596, label: 'Dolores Park', selected: true }],
99
- });
100
- ```
101
-
102
- ### Match a site's colors
103
-
104
- ```js
105
30
  const svg = renderSFMap({
106
31
  landmarks: true,
107
32
  bartStations: true,
108
- colors: { park: '#c4d4b1', landmark: '#3e6346', bart: '#795285' },
33
+ highways: true,
34
+ neighborhoodLines: true,
35
+ markers: [{ id: 'dolores', lng: -122.4269, lat: 37.7596, label: 'Dolores Park' }],
109
36
  });
110
- ```
111
37
 
112
- ## Storybook
113
-
114
- ```sh
115
- pnpm storybook # http://127.0.0.1:6006
116
- pnpm build-storybook # static output in storybook-static/
38
+ await writeFile('san-francisco.svg', svg);
117
39
  ```
118
40
 
119
- Map stories cover the default map, combined and independent landmark/BART layers, neighborhoods, outlines, historical district years, custom markers, a custom palette, and a narrow map. The Data / Neighborhood explorer adds examples for comparing Mission, Outer Mission, SoMa, and NoPa across source definitions. Controls edit map options live; the Docs tab shows usage examples. Storybook and Vite are development dependencies only and are excluded from the package archive. Development requires Node 22.12+ and pnpm 12. Only esbuild's dependency build script is enabled in `pnpm-workspace.yaml`.
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.
120
42
 
121
- Dependabot checks npm dependencies and GitHub Actions weekly, grouping Storybook updates. CI validates formatting, SVG tests, generated examples, Storybook builds, and package creation on Node 22 and 26. Dependency PRs require review; updates are not merged automatically.
43
+ ## Static options
122
44
 
123
- ## Accessible JSON data and neighborhood lookup
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 |
124
64
 
125
- 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.
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).
126
66
 
127
- ```js
128
- import neighborhoods from '@kahwee/sf-map-svg/data/neighborhoods-realtor.json' with { type: 'json' };
129
- import districts2022 from '@kahwee/sf-map-svg/data/districts-2022.json' with { type: 'json' };
130
- import { getNeighborhood, searchNeighborhoods } from '@kahwee/sf-map-svg/data';
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`.
131
68
 
132
- const mission = getNeighborhood('Inner Mission');
133
- const outerMission = getNeighborhood('Outer Mission');
134
- const nopa = getNeighborhood('NoPa', { source: 'realtor' });
135
- const matchingDefinitions = searchNeighborhoods('mission');
136
- ```
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.
70
+
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.
137
72
 
138
- 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.
139
73
 
140
- ## Interactive neighborhood explorer
74
+ ## Neighborhood explorer
141
75
 
142
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.
143
77
 
@@ -158,19 +92,6 @@ Selected downloads are one-feature GeoJSON FeatureCollections retaining source a
158
92
 
159
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.
160
94
 
161
- Run `pnpm demo`, serve the repository root over HTTP, and open `examples/generated/explorer.html`. Storybook includes city, selected neighborhood, alternative-source, and mobile examples.
162
-
163
- ## License
164
-
165
- Software is licensed under MIT. Geographic datasets retain their source terms and attribution requirements; see [SOURCES.md](SOURCES.md).
166
-
167
-
168
- ## TypeScript development
169
-
170
- The library is authored in strict TypeScript 7. Run `pnpm build` to compile JavaScript and declarations into `dist/`. JavaScript consumers require no TypeScript runtime. `pnpm format` applies Biome formatting and safe lint fixes; `pnpm check` checks Biome, data, source and consumer types, and tests.
171
-
172
- Enable `keyRoads: true` for Market, Mission, Geary, Van Ness, 19th Avenue, Sunset, The Embarcadero, Columbus, and Divisadero. These use active DataSF centerlines, not invented routes. The explorer reveals road names as you zoom. Import `keyRoads` from the data entry point or `data/key-roads.json` for geometry, source segment IDs, and label anchors.
173
-
174
95
  ### Map modes and labels
175
96
 
176
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.
@@ -186,7 +107,7 @@ explorer.setLabels(true);
186
107
 
187
108
  ## Reusable interactive map
188
109
 
189
- The new `@kahwee/sf-map-svg/interactive` entry point provides a map, accessible controls, and
110
+ The `@kahwee/sf-map-svg/interactive` entry point provides a map, accessible controls, and
190
111
  attribution without the explorer's search sidebar or detail panel. It does not change URLs,
191
112
  load articles, apply editorial filters, or navigate. Importing the static entry point does not
192
113
  import this interactive runtime. Both paths retain zero runtime dependencies.
@@ -319,7 +240,39 @@ chooser to reach obscured markers. Automated clustering is not included in this
319
240
  - Pointer cancellation, loss of capture, window blur, and resizing cancel active gestures.
320
241
  There is no animated camera or inertia. Button transitions are disabled with reduced motion.
321
242
 
322
- ### Examples and verification
243
+ ## Geographic data and lookup
244
+
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.
246
+
247
+ ```js
248
+ import neighborhoods from '@kahwee/sf-map-svg/data/neighborhoods-realtor.json' with { type: 'json' };
249
+ import districts2022 from '@kahwee/sf-map-svg/data/districts-2022.json' with { type: 'json' };
250
+ import { getNeighborhood, searchNeighborhoods } from '@kahwee/sf-map-svg/data';
251
+
252
+ const mission = getNeighborhood('Inner Mission');
253
+ const outerMission = getNeighborhood('Outer Mission');
254
+ const nopa = getNeighborhood('NoPa', { source: 'realtor' });
255
+ const matchingDefinitions = searchNeighborhoods('mission');
256
+ ```
257
+
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
+
323
276
 
324
277
  Run `pnpm demo` and serve the repository root. `examples/generated/index.html` covers static
325
278
  maps, `explorer.html` covers the full explorer, and `interactive.html` covers independently
@@ -342,3 +295,32 @@ teardown. Physical iOS Safari and Android Chrome verification remains required b
342
295
  check page scroll and browser pinch in default mode; map pan and pinch in engaged mode; lift one
343
296
  finger; interrupt/cancel; rotate; use Done; verify scrolling resumes. Desktop automation and
344
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>`.
@@ -33,7 +33,7 @@ const mission = neighborhoods.features.find((f) => f.id === 'inner-mission');
33
33
  console.log(mission.properties.canonicalName, mission.geometry);
34
34
  ```
35
35
 
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. The repository remains private; these are public package entry points, not a publicly hosted API. Consumers who need only one dataset should import its JSON subpath rather than the convenience module, which loads all collections.
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.
37
37
 
38
38
  ## Names and definitions
39
39
 
@@ -938,18 +938,24 @@ export function createNeighborhoodExplorer({ source = 'realtor', mode = 'neighbo
938
938
  releaseDownloads();
939
939
  },
940
940
  });
941
- const initialMode = mode;
942
- setSource(source);
943
- if (neighborhood !== undefined)
944
- selectNeighborhood(neighborhood);
945
- setMode(initialMode);
946
- if (initialMode === 'neighborhoods' && selected) {
947
- const selectedItem = items.find((item) => item.feature === selected);
948
- if (selectedItem)
949
- setView(fitBounds(selectedItem.bounds));
941
+ try {
942
+ const initialMode = mode;
943
+ setSource(source);
944
+ if (neighborhood !== undefined)
945
+ selectNeighborhood(neighborhood);
946
+ setMode(initialMode);
947
+ if (initialMode === 'neighborhoods' && selected) {
948
+ const selectedItem = items.find((item) => item.feature === selected);
949
+ if (selectedItem)
950
+ setView(fitBounds(selectedItem.bounds));
951
+ }
952
+ setLabels(labels);
953
+ setTouchNavigation(false);
954
+ setMarkers(initialMarkers);
955
+ }
956
+ catch (error) {
957
+ explorer.destroy();
958
+ throw error;
950
959
  }
951
- setLabels(labels);
952
- setTouchNavigation(false);
953
- setMarkers(initialMarkers);
954
960
  return explorer;
955
961
  }
@@ -0,0 +1,3 @@
1
+ import type { TransitAnimationElement } from './types.js';
2
+ /** A deliberately schematic, offline station-to-station animation. Starts paused. */
3
+ export declare function createTransitAnimation(): TransitAnimationElement;
@@ -0,0 +1,105 @@
1
+ import stations from '../data/bart-stations.json' with { type: 'json' };
2
+ import { createSFMap } from './index.js';
3
+ /** A deliberately schematic, offline station-to-station animation. Starts paused. */
4
+ export function createTransitAnimation() {
5
+ const host = Object.assign(document.createElement('div'), {
6
+ destroy: () => { },
7
+ });
8
+ const root = host.attachShadow({ mode: 'open' });
9
+ const map = createSFMap({ districtLabels: false, landmarks: true, labels: false });
10
+ root.innerHTML = `<style>
11
+ :host{display:block;font:14px system-ui;color:#244747;max-width:800px}
12
+ svg{display:block;width:100%;height:auto} .controls{display:flex;gap:12px;align-items:center;padding:12px;flex-wrap:wrap}
13
+ button{font:inherit;padding:10px 18px;border:1px solid #244747;border-radius:24px;background:#244747;color:white;cursor:pointer}
14
+ label{display:flex;align-items:center;gap:8px;flex:1} input{width:100%;min-width:100px;accent-color:#24789a}
15
+ p{margin:8px 12px;line-height:1.5} output{display:block;min-height:3em;margin:8px 12px}
16
+ </style>${map.svg}<div class="controls"><button type="button">Play</button><label>Journey <input aria-label="Journey progress" type="range" min="0" max="1000" value="0"></label></div><output aria-live="off"></output><p>Schematic BART journey. Straight connections between official station locations; not track geometry, a timetable, or live trains. One loop takes 28 seconds.</p>`;
17
+ const svg = root.querySelector('svg');
18
+ const ns = 'http://www.w3.org/2000/svg';
19
+ const points = stations.features.map((station) => map.project(station.geometry.coordinates));
20
+ const line = document.createElementNS(ns, 'polyline');
21
+ line.setAttribute('points', points.map((point) => point.join(',')).join(' '));
22
+ line.setAttribute('fill', 'none');
23
+ line.setAttribute('stroke', '#24789a');
24
+ line.setAttribute('stroke-width', '3');
25
+ line.setAttribute('stroke-dasharray', '5 5');
26
+ svg.append(line);
27
+ for (const [x, y] of points) {
28
+ const dot = document.createElementNS(ns, 'circle');
29
+ dot.setAttribute('cx', String(x));
30
+ dot.setAttribute('cy', String(y));
31
+ dot.setAttribute('r', '4');
32
+ dot.setAttribute('fill', 'white');
33
+ dot.setAttribute('stroke', '#24789a');
34
+ dot.setAttribute('stroke-width', '2');
35
+ svg.append(dot);
36
+ }
37
+ const train = document.createElementNS(ns, 'circle');
38
+ train.setAttribute('r', '7');
39
+ train.setAttribute('fill', '#a85036');
40
+ train.setAttribute('stroke', 'white');
41
+ train.setAttribute('stroke-width', '2');
42
+ svg.append(train);
43
+ const button = root.querySelector('button');
44
+ const slider = root.querySelector('input');
45
+ const output = root.querySelector('output');
46
+ let progress = 0;
47
+ let frame = 0;
48
+ let previous = 0;
49
+ let playing = false;
50
+ let destroyed = false;
51
+ function draw() {
52
+ const position = progress * (points.length - 1);
53
+ const index = Math.min(Math.floor(position), points.length - 2);
54
+ const fraction = position - index;
55
+ const from = points[index];
56
+ const to = points[index + 1];
57
+ train.setAttribute('cx', String(from[0] + (to[0] - from[0]) * fraction));
58
+ train.setAttribute('cy', String(from[1] + (to[1] - from[1]) * fraction));
59
+ slider.value = String(Math.round(progress * 1000));
60
+ output.textContent = `${stations.features[index].properties.name} → ${stations.features[index + 1].properties.name}`;
61
+ }
62
+ function tick(now) {
63
+ if (!playing)
64
+ return;
65
+ if (previous)
66
+ progress = (progress + Math.min(now - previous, 100) / 28000) % 1;
67
+ previous = now;
68
+ draw();
69
+ frame = requestAnimationFrame(tick);
70
+ }
71
+ function pause() {
72
+ playing = false;
73
+ cancelAnimationFrame(frame);
74
+ previous = 0;
75
+ button.textContent = 'Play';
76
+ }
77
+ button.addEventListener('click', () => {
78
+ if (destroyed)
79
+ return;
80
+ if (playing)
81
+ pause();
82
+ else {
83
+ playing = true;
84
+ button.textContent = 'Pause';
85
+ frame = requestAnimationFrame(tick);
86
+ }
87
+ });
88
+ slider.addEventListener('input', () => {
89
+ pause();
90
+ progress = Number(slider.value) / 1000;
91
+ draw();
92
+ });
93
+ const visibility = () => {
94
+ if (document.hidden)
95
+ pause();
96
+ };
97
+ document.addEventListener('visibilitychange', visibility);
98
+ host.destroy = () => {
99
+ destroyed = true;
100
+ pause();
101
+ document.removeEventListener('visibilitychange', visibility);
102
+ };
103
+ draw();
104
+ return host;
105
+ }
@@ -106,3 +106,7 @@ export interface NeighborhoodExplorerElement extends HTMLElement {
106
106
  getSelectedMarker(): MapMarker | null;
107
107
  destroy(): void;
108
108
  }
109
+ /** Paused-by-default schematic BART animation with keyboard-operable controls. */
110
+ export interface TransitAnimationElement extends HTMLElement {
111
+ destroy(): void;
112
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kahwee/sf-map-svg",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Offline SVG maps of San Francisco with district boundaries, parks, landmarks, and BART stations.",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",
@@ -26,6 +26,10 @@
26
26
  "./interactive": {
27
27
  "types": "./dist/src/interactive.d.ts",
28
28
  "import": "./dist/src/interactive.js"
29
+ },
30
+ "./transit": {
31
+ "types": "./dist/src/transit.d.ts",
32
+ "import": "./dist/src/transit.js"
29
33
  }
30
34
  },
31
35
  "files": [
@@ -53,7 +57,7 @@
53
57
  "typescript": "^7.0.2",
54
58
  "vite": "^8.3.1"
55
59
  },
56
- "homepage": "https://github.com/kahwee/sf-map-svg#readme",
60
+ "homepage": "https://kahwee.github.io/sf-map-svg/",
57
61
  "bugs": {
58
62
  "url": "https://github.com/kahwee/sf-map-svg/issues"
59
63
  },
@@ -82,6 +86,7 @@
82
86
  "data:normalize-realtor": "node scripts/normalize-realtor.mjs",
83
87
  "test:package": "node scripts/smoke-package.mjs",
84
88
  "build": "node scripts/build.mjs",
85
- "lint": "biome check ."
89
+ "lint": "biome check .",
90
+ "build:pages": "pnpm demo && node scripts/build-pages.mjs"
86
91
  }
87
92
  }