@kahwee/sf-map-svg 1.4.0 → 1.5.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 +10 -0
- package/README.md +23 -2
- package/SOURCES.md +10 -0
- package/dist/src/explorer-core.js +21 -3
- package/dist/src/types.d.ts +6 -1
- package/docs/EXAMPLES.md +87 -0
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,16 @@ User-visible changes are recorded here. Unreleased entries describe changes on `
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 1.5.0 — 2026-09-26
|
|
8
|
+
|
|
9
|
+
- Add `controls.neighborhoodPicker`, `controls.markerPicker`, `controls.help`, and `controls.status` so compact embeds can drop redundant chrome. Hidden help remains the map's accessible description; a hidden status line remains a polite live region; source attribution stays visible.
|
|
10
|
+
- Keep a configured `strings.chooseMarker` label after `setMarkers()` updates instead of reverting to “Choose marker”.
|
|
11
|
+
|
|
12
|
+
## 1.4.1 — 2026-09-26
|
|
13
|
+
|
|
14
|
+
- Add a task-based example gallery and code recipes, with clearer routes among the Pages atlas, local measures, district history, transit, and lightweight map.
|
|
15
|
+
- Add a California proposition explorer using certified November 2024 Yes and No votes for all eleven San Francisco supervisorial districts; publish its sourced JSON and reproducible import script.
|
|
16
|
+
|
|
7
17
|
## 1.4.0 — 2026-09-26
|
|
8
18
|
|
|
9
19
|
- Split geographic convenience exports into independent data modules, so metadata search and SFAR-only lookup avoid unrelated JSON.
|
package/README.md
CHANGED
|
@@ -2,12 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@kahwee/sf-map-svg)
|
|
4
4
|
|
|
5
|
-
[
|
|
5
|
+
[Live atlas](https://kahwee.github.io/sf-map-svg/) · [Example gallery](https://kahwee.github.io/sf-map-svg/examples.html) · [Code recipes](docs/EXAMPLES.md) · [Data guide](data/README.md) · [Sources](SOURCES.md) · [Contributing](CONTRIBUTING.md)
|
|
6
6
|
|
|
7
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
8
|
|
|
9
9
|

|
|
10
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) | Root or `/custom-map` |
|
|
18
|
+
| Animate a route | [BART journey](https://kahwee.github.io/sf-map-svg/transit.html) | `/transit` or overlays |
|
|
19
|
+
|
|
11
20
|
## Install
|
|
12
21
|
|
|
13
22
|
```sh
|
|
@@ -246,7 +255,19 @@ aliases. Import types including `InteractiveSFMapOptions`, `InteractiveSFMapElem
|
|
|
246
255
|
| `overlays` | `[]` | GeoJSON line or polygon overlays with stable IDs and optional SVG styles |
|
|
247
256
|
| `style` | Built-in tokens | Explorer CSS tokens: `ink`, `surface`, `accent`, `border`, `focus`, `controlGap`, `font` |
|
|
248
257
|
| `strings` | English defaults | Replace visible map labels and gesture help for localization |
|
|
249
|
-
| `controls` | All enabled | Independently hide `zoom`, `pan`, `reset`, `labels`, `touch`, or `
|
|
258
|
+
| `controls` | All enabled | Independently hide `zoom`, `pan`, `reset`, `labels`, `touch`, `legend`, `neighborhoodPicker`, `markerPicker`, `help`, or `status`; source attribution remains visible |
|
|
259
|
+
|
|
260
|
+
For a compact embed, hide the native choosers only when the page already lists every
|
|
261
|
+
marker or area as an accessible control. Hidden help remains the map's accessible
|
|
262
|
+
description, and a hidden status line remains a polite live region:
|
|
263
|
+
|
|
264
|
+
```js
|
|
265
|
+
const map = createGuideMap({
|
|
266
|
+
interface: 'map',
|
|
267
|
+
controls: { labels: false, neighborhoodPicker: false, markerPicker: false, help: false, status: false },
|
|
268
|
+
strings: { chooseMarker: 'Place on map' }, // also used after setMarkers() updates
|
|
269
|
+
});
|
|
270
|
+
```
|
|
250
271
|
|
|
251
272
|
Explicit layer options override mode defaults even after `setMode()`. District fills, outlines,
|
|
252
273
|
and badges can therefore be composed with neighborhood names without requiring district
|
package/SOURCES.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Geometry and provenance
|
|
2
2
|
|
|
3
|
+
## California propositions in San Francisco
|
|
4
|
+
|
|
5
|
+
Downloaded September 26, 2026 from the San Francisco Department of Elections' [final November 5, 2024 results](https://sfelections.org/results/20241105w/detail.html):
|
|
6
|
+
|
|
7
|
+
- [Certified district statement of vote workbook](https://www.sfelections.org/results/20241105/data/20241203/dsov.xlsx), SHA-256 `9bf9c7d767273e73b3cdf5b98086412cbe354b0928ee6abb0e379deb0353c403`
|
|
8
|
+
- [Certification letter](https://www.sfelections.org/results/20241105/data/20241203/CertificationLetterNov52024.pdf)
|
|
9
|
+
- [California Secretary of State statewide Statement of Vote](https://www.sos.ca.gov/elections/prior-elections/statewide-election-results/general-election-nov-5-2024/statement-vote) for statewide context
|
|
10
|
+
|
|
11
|
+
`data/propositions/2024-11-05.json` contains Yes and No counts for all ten state propositions, grouped by San Francisco supervisorial district. The short titles are editorial navigation labels. `scripts/import-2024-propositions.py` reads the final workbook's proposition sheets and checks that all eleven district totals sum exactly to its San Francisco citywide totals. Yes share divides Yes by Yes plus No, excluding undervotes and overvotes. The data describes where ballots were cast within San Francisco, not individual voters or statewide outcomes. Its 2022 district geography matches the election's district vintage.
|
|
12
|
+
|
|
3
13
|
## Map extraction
|
|
4
14
|
|
|
5
15
|
District geometry, coast, label anchors, highway geometry, and the pastel district palette were extracted from the San Francisco District Map on September 25, 2026. Only map rendering and public geographic data are distributed. Site account identifiers, original application code, and ballot overlays are excluded.
|
|
@@ -30,6 +30,7 @@ function svgElement(tag, attributes = {}) {
|
|
|
30
30
|
node.setAttribute(key, String(value));
|
|
31
31
|
return node;
|
|
32
32
|
}
|
|
33
|
+
let explorerCount = 0;
|
|
33
34
|
/** Create an offline, browser-only neighborhood explorer. Call destroy() before disposal. */
|
|
34
35
|
export function createNeighborhoodExplorerCore({ source = 'realtor', mode = 'neighborhoods', labels = true, neighborhood, year = 2022, theme = 'transit', interface: chrome = 'explorer', layers = {}, selectableNeighborhoods = true, labelSize = {}, fitPadding = 24, markers: initialMarkers = [], markerRadius = 6, markerHitSize = 44, markerColor = '#245b61', selectedMarkerColor = '#f04f32', onMarkerActivate, overlays: initialOverlays = [], onOverlayActivate, style: styleOptions = {}, strings = {}, controls = {}, } = {}, data) {
|
|
35
36
|
if (!['neighborhoods', 'districts', 'basemap'].includes(mode))
|
|
@@ -141,6 +142,7 @@ export function createNeighborhoodExplorerCore({ source = 'realtor', mode = 'nei
|
|
|
141
142
|
.sf-explorer-hint{padding:6px 16px 10px;font-size:12px;color:#586f80;background:#fff}
|
|
142
143
|
.sf-explorer text{pointer-events:none}
|
|
143
144
|
.sf-explorer-status{padding:0 16px 14px;font-size:12px;background:#fff;color:#496578}
|
|
145
|
+
.sf-explorer-visually-hidden{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}
|
|
144
146
|
.sf-explorer[data-interface=map] .sf-explorer-body{display:block}
|
|
145
147
|
.sf-explorer[data-interface=map] .sf-explorer-header,.sf-explorer[data-interface=map] .sf-explorer-panel,.sf-explorer[data-interface=map] .sf-explorer-detail{display:none}
|
|
146
148
|
.sf-explorer-feature-controls{display:flex;flex-wrap:wrap;gap:8px;padding:10px;background:#fff}
|
|
@@ -222,6 +224,7 @@ export function createNeighborhoodExplorerCore({ source = 'realtor', mode = 'nei
|
|
|
222
224
|
const canvas = element('div', '', 'sf-explorer-canvas');
|
|
223
225
|
const hint = element('p', strings.gestureHelp ??
|
|
224
226
|
'Drag to pan · Ctrl/⌘ + scroll to zoom · Focus map: arrows pan, +/− zoom, Home resets. Touch: scroll the page, or enable Touch navigation to pan and pinch the map. Escape exits.', 'sf-explorer-hint');
|
|
227
|
+
hint.id = `sf-explorer-help-${++explorerCount}`;
|
|
225
228
|
const status = element('p', '', 'sf-explorer-status');
|
|
226
229
|
status.setAttribute('aria-live', 'polite');
|
|
227
230
|
const legend = element('div', '', 'sf-explorer-legend');
|
|
@@ -764,12 +767,19 @@ export function createNeighborhoodExplorerCore({ source = 'realtor', mode = 'nei
|
|
|
764
767
|
? `${year} supervisorial districts. Numbers identify each district.`
|
|
765
768
|
: `${formatSourceLabel(source)}. Select a neighborhood to begin.`;
|
|
766
769
|
}
|
|
770
|
+
function syncFeatureControls() {
|
|
771
|
+
featureControls.hidden = neighborhoodLabel.hidden && markerLabel.hidden;
|
|
772
|
+
}
|
|
767
773
|
function updateComposition() {
|
|
768
774
|
const neighborhoodsVisible = enabled('neighborhoodLines') || (labels && enabled('neighborhoodLabels'));
|
|
769
775
|
areas.style.display = neighborhoodsVisible ? '' : 'none';
|
|
770
776
|
areas.style.pointerEvents = selectableNeighborhoods ? '' : 'none';
|
|
771
777
|
neighborhoodLabel.hidden =
|
|
772
|
-
!neighborhoodsVisible ||
|
|
778
|
+
!neighborhoodsVisible ||
|
|
779
|
+
!selectableNeighborhoods ||
|
|
780
|
+
chrome === 'explorer' ||
|
|
781
|
+
controls.neighborhoodPicker === false;
|
|
782
|
+
syncFeatureControls();
|
|
773
783
|
for (const item of items) {
|
|
774
784
|
item.node.setAttribute('stroke-width', item.feature === selected ? '2.2' : enabled('neighborhoodLines') ? '.55' : '0');
|
|
775
785
|
item.node.setAttribute('tabindex', selectableNeighborhoods && item === items[0] ? '0' : '-1');
|
|
@@ -881,9 +891,10 @@ export function createNeighborhoodExplorerCore({ source = 'realtor', mode = 'nei
|
|
|
881
891
|
markerSelect.append(option);
|
|
882
892
|
return { marker, point, node, dot, hit };
|
|
883
893
|
});
|
|
884
|
-
markerLabel.hidden = !markerItems.length;
|
|
894
|
+
markerLabel.hidden = !markerItems.length || controls.markerPicker === false;
|
|
885
895
|
if (markerLabel.firstChild)
|
|
886
|
-
markerLabel.firstChild.textContent =
|
|
896
|
+
markerLabel.firstChild.textContent = `${strings.chooseMarker ?? 'Choose marker'} (${markerItems.length})`;
|
|
897
|
+
syncFeatureControls();
|
|
887
898
|
const nextSelection = previous && ids.has(previous)
|
|
888
899
|
? previous
|
|
889
900
|
: (markers.find((marker) => marker.selected)?.id ?? null);
|
|
@@ -902,6 +913,13 @@ export function createNeighborhoodExplorerCore({ source = 'realtor', mode = 'nei
|
|
|
902
913
|
labelsButton.hidden = true;
|
|
903
914
|
if (controls.legend === false)
|
|
904
915
|
legend.hidden = true;
|
|
916
|
+
if (controls.help === false) {
|
|
917
|
+
// The instructions stay available to assistive technology as the map's description.
|
|
918
|
+
hint.hidden = true;
|
|
919
|
+
svg.setAttribute('aria-describedby', hint.id);
|
|
920
|
+
}
|
|
921
|
+
if (controls.status === false)
|
|
922
|
+
status.classList.add('sf-explorer-visually-hidden');
|
|
905
923
|
function setMode(next) {
|
|
906
924
|
if (destroyed)
|
|
907
925
|
return;
|
package/dist/src/types.d.ts
CHANGED
|
@@ -101,7 +101,12 @@ export interface NeighborhoodExplorerOptions {
|
|
|
101
101
|
/** Stable theme tokens consumed by the explorer chrome. */
|
|
102
102
|
style?: Partial<Record<'ink' | 'surface' | 'accent' | 'border' | 'focus' | 'controlGap' | 'font', string>>;
|
|
103
103
|
strings?: Partial<Record<'title' | 'mode' | 'source' | 'search' | 'chooseNeighborhood' | 'chooseMarker' | 'touchNavigation' | 'reset' | 'emptyResults' | 'gestureHelp', string>>;
|
|
104
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Independently hide chrome. `neighborhoodPicker` and `markerPicker` hide the native
|
|
106
|
+
* choosers (supply your own accessible list); `help` keeps the gesture help as the map's
|
|
107
|
+
* accessible description; `status` keeps a visually hidden live region. Attribution stays.
|
|
108
|
+
*/
|
|
109
|
+
controls?: Partial<Record<'zoom' | 'pan' | 'reset' | 'labels' | 'touch' | 'legend' | 'neighborhoodPicker' | 'markerPicker' | 'help' | 'status', boolean>>;
|
|
105
110
|
}
|
|
106
111
|
export interface NeighborhoodExplorerElement extends HTMLElement {
|
|
107
112
|
selectNeighborhood(name: string | null, options?: {
|
package/docs/EXAMPLES.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
[Live example gallery](https://kahwee.github.io/sf-map-svg/examples.html) · [California propositions](https://kahwee.github.io/sf-map-svg/propositions.html) · [Local measures](https://kahwee.github.io/sf-map-svg/measures.html) · [Source data](../SOURCES.md)
|
|
4
|
+
|
|
5
|
+
Choose by task. All snippets use the public package API; browser examples need a DOM and a bundler that supports JSON imports.
|
|
6
|
+
|
|
7
|
+
| I want to… | Start here |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| Render an SVG on a server | [Static map](#static-svg) |
|
|
10
|
+
| Add a small map to a browser | [Lightweight guide](#lightweight-interactive-guide) |
|
|
11
|
+
| Bundle only selected geography | [Selected data](#selected-geography) |
|
|
12
|
+
| Draw a route over the city | [Route overlay](#route-overlay) |
|
|
13
|
+
| Explore real election votes | [California propositions](#california-propositions-by-sf-district) |
|
|
14
|
+
|
|
15
|
+
## Static SVG
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { writeFile } from 'node:fs/promises';
|
|
19
|
+
import { renderSFMap } from '@kahwee/sf-map-svg';
|
|
20
|
+
|
|
21
|
+
const svg = renderSFMap({
|
|
22
|
+
year: 2022,
|
|
23
|
+
landmarks: true,
|
|
24
|
+
bartStations: true,
|
|
25
|
+
idPrefix: 'example',
|
|
26
|
+
});
|
|
27
|
+
await writeFile('districts.svg', svg);
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The root entry includes the built-in geography for convenience. See the [full option list](../README.md#static-options).
|
|
31
|
+
|
|
32
|
+
## Lightweight interactive guide
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { createGuideMap } from '@kahwee/sf-map-svg/guide';
|
|
36
|
+
|
|
37
|
+
const map = createGuideMap({ layers: { roadLabels: false } });
|
|
38
|
+
document.querySelector('#map')?.append(map);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The guide includes selected coast, SFAR neighborhoods, parks, roads, and stations. Detailed geography loads only when explicitly requested; see the [guide recipe](../README.md#render-a-static-map).
|
|
42
|
+
|
|
43
|
+
## Selected geography
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { createInteractiveSFMapWithData } from '@kahwee/sf-map-svg/interactive-data';
|
|
47
|
+
import coast from '@kahwee/sf-map-svg/data/coast.json' with { type: 'json' };
|
|
48
|
+
import realtor from '@kahwee/sf-map-svg/data/neighborhoods-realtor.json' with { type: 'json' };
|
|
49
|
+
|
|
50
|
+
const map = createInteractiveSFMapWithData(
|
|
51
|
+
{
|
|
52
|
+
map: { coast: coast.features[0].geometry },
|
|
53
|
+
neighborhoods: { realtor },
|
|
54
|
+
},
|
|
55
|
+
{ mode: 'neighborhoods', layers: { highways: false, keyRoads: false } },
|
|
56
|
+
);
|
|
57
|
+
document.querySelector('#map')?.append(map);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
This imports one neighborhood definition source. To omit its geometry too, import catalog metadata alone from `/data/catalog`.
|
|
61
|
+
|
|
62
|
+
## Route overlay
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { createGuideMap } from '@kahwee/sf-map-svg/guide';
|
|
66
|
+
|
|
67
|
+
const map = createGuideMap();
|
|
68
|
+
document.querySelector('#map')?.append(map);
|
|
69
|
+
map.setOverlays([{
|
|
70
|
+
id: 'trip',
|
|
71
|
+
label: 'Example route',
|
|
72
|
+
geometry: {
|
|
73
|
+
type: 'LineString',
|
|
74
|
+
coordinates: [[-122.4194, 37.7749], [-122.3981, 37.7936]],
|
|
75
|
+
},
|
|
76
|
+
stroke: '#a85036',
|
|
77
|
+
strokeWidth: 3,
|
|
78
|
+
}]);
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Coordinates are WGS84 `[longitude, latitude]`. The overlay follows pan and zoom. The [BART journey](https://kahwee.github.io/sf-map-svg/transit.html) is a separate schematic motion example.
|
|
82
|
+
|
|
83
|
+
## California propositions by SF district
|
|
84
|
+
|
|
85
|
+
[Open the interactive explorer](https://kahwee.github.io/sf-map-svg/propositions.html). It uses the public `custom-map` renderer with only the 2022 district and coast datasets and colors each district from the [certified results JSON](../data/propositions/2024-11-05.json). The JSON includes all ten statewide propositions on the November 2024 ballot, with Yes and No counts for each of San Francisco's eleven supervisorial districts. Its scope is SF votes, not statewide totals or voter demographics.
|
|
86
|
+
|
|
87
|
+
The [import script](../scripts/import-2024-propositions.py) checks each district sum against the official citywide count. [Geographic and election sources](../SOURCES.md) explain the provenance.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kahwee/sf-map-svg",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.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",
|
|
@@ -102,6 +102,7 @@
|
|
|
102
102
|
},
|
|
103
103
|
"files": [
|
|
104
104
|
"dist",
|
|
105
|
+
"docs/EXAMPLES.md",
|
|
105
106
|
"README.md",
|
|
106
107
|
"SOURCES.md",
|
|
107
108
|
"LICENSE"
|