@kahwee/sf-map-svg 4.2.0 → 4.3.1
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 +15 -0
- package/README.md +10 -14
- package/dist/src/configuration.d.ts +3 -1
- package/dist/src/controller-types.d.ts +28 -2
- package/dist/src/explorer-core.js +6 -1
- package/dist/src/map-core.js +24 -2
- package/dist/src/map.js +53 -12
- package/dist/src/static.d.ts +5 -2
- package/dist/src/static.js +78 -16
- package/dist/src/types.d.ts +3 -1
- package/docs/API.md +19 -3
- package/docs/api-audit.md +6 -8
- package/docs/consumer-integration.md +35 -11
- package/docs/developer-guide.md +5 -5
- package/llms-full.txt +130 -48
- package/llms.txt +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@ User-visible changes are recorded here. Unreleased entries describe changes on `
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 4.3.1 — 2026-10-04
|
|
8
|
+
|
|
9
|
+
- Reject unknown flat static options and invalid values before rendering or invoking district styling callbacks, matching grouped-option validation.
|
|
10
|
+
- Ignore undefined static color overrides so SVG attributes retain the selected theme's defaults; preserve grouped-option precedence.
|
|
11
|
+
- Describe the actual neighborhood source, enabled and supplied layers, and park and BART station counts in static SVG descriptions. Compact geography uses a source-neutral description.
|
|
12
|
+
|
|
13
|
+
## 4.3.0 — 2026-10-02
|
|
14
|
+
|
|
15
|
+
- Add `selectFeature` with typed marker, neighborhood source/ID and district year/ID references. Missing or mismatched identities preserve current geography and selection.
|
|
16
|
+
- Accept the same source-aware map data in static and interactive rendering. Static maps can explicitly select a supplied neighborhood definition without manually rebuilding renderer rows; compact static inputs remain supported.
|
|
17
|
+
- Include current mode, source, year and labels in configuration snapshots. Route controller mode/source/label setters through atomic configuration and support explicit camera preservation with `{ resetView: false }`.
|
|
18
|
+
- Refine the Pages atlas opening with concise copy, a map ahead of statistics on phones, coordinate captions and three direct paths into designing, exploring and building. Improve shared theme-button touch targets and stack API reference records on phones while retaining table semantics.
|
|
19
|
+
|
|
20
|
+
- Clarify the documentation paths, progressive enhancement and cleanup examples; consolidate browser validation and release instructions into one maintenance runbook. Check README and integration recipes against the packed package.
|
|
21
|
+
|
|
7
22
|
## 4.2.0 — 2026-10-01
|
|
8
23
|
|
|
9
24
|
- Reuse one native dropdown treatment across Pages, with an inset chevron, a reserved end gutter, and preserved labels, keyboard behavior, and mobile pickers.
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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
|
-
[Design playground](https://kahwee.github.io/sf-map-svg/playground.html) · [Live examples](https://kahwee.github.io/sf-map-svg/examples.html) · [Storybook source](stories/) · [Geographic sources](SOURCES.md) · [v3 migration](docs/migration-v3.md)
|
|
5
|
+
[Getting started](docs/developer-guide.md) · [API reference](docs/API.md) · [Design playground](https://kahwee.github.io/sf-map-svg/playground.html) · [Live examples](https://kahwee.github.io/sf-map-svg/examples.html) · [Storybook source](stories/) · [Geographic sources](SOURCES.md) · [v3 migration](docs/migration-v3.md)
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -45,11 +45,16 @@ const map = createMap(fullMapData, {
|
|
|
45
45
|
features: { motion: true },
|
|
46
46
|
appearance: { theme: 'districts' },
|
|
47
47
|
});
|
|
48
|
-
document.querySelector('#map')
|
|
48
|
+
const host = document.querySelector('#map');
|
|
49
|
+
if (!host) throw new Error('Missing #map container');
|
|
50
|
+
host.append(map.element);
|
|
49
51
|
map.on('neighborhoodchange', ({ name }) => console.log(name));
|
|
50
52
|
map.camera.zoom(1.5);
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
+
// Call when your application removes this view.
|
|
54
|
+
function disposeMap() {
|
|
55
|
+
map.destroy();
|
|
56
|
+
map.element.remove();
|
|
57
|
+
}
|
|
53
58
|
```
|
|
54
59
|
|
|
55
60
|
`createMap(data, options)` returns a controller. Use `map.element` for mounting, `map.configure({ features, layers, controls, appearance, mode, source, year, labels })` for runtime switches, `map.camera` for pan/zoom/fit/reset, and `map.on()` for typed events. Appearance can change live while preserving camera, selection, and focus. `getResolvedConfiguration()` explains effective layers; `getCapabilities()` reports supplied geography. 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.
|
|
@@ -66,13 +71,4 @@ Call `destroy()` when removing an interactive map.
|
|
|
66
71
|
|
|
67
72
|
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.
|
|
68
73
|
|
|
69
|
-
|
|
70
|
-
pnpm install --frozen-lockfile
|
|
71
|
-
pnpm check
|
|
72
|
-
pnpm demo
|
|
73
|
-
pnpm build-storybook
|
|
74
|
-
pnpm test:stories:coverage
|
|
75
|
-
pnpm test:package
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
The [contributing guide](CONTRIBUTING.md) explains the geographic and release checks. If you are upgrading from v2, follow the [v3 migration guide](docs/migration-v3.md).
|
|
74
|
+
Install with `pnpm install --frozen-lockfile`. See the [contributing guide](CONTRIBUTING.md) for architecture and the [validation and release runbook](https://github.com/kahwee/sf-map-svg/blob/main/docs/maintenance.md) for checks, browser inspection, and publishing. If you are upgrading from v2, follow the [v3 migration guide](docs/migration-v3.md).
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { MapConfiguration, MapConfigurationSnapshot, MapOptions } from './controller-types.js';
|
|
2
2
|
import type { NeighborhoodExplorerOptions } from './types.js';
|
|
3
3
|
export declare function expandMapOptions(options: MapOptions): NeighborhoodExplorerOptions;
|
|
4
|
+
type PresentationSnapshot = Pick<MapConfigurationSnapshot, 'features' | 'layers' | 'controls' | 'appearance'>;
|
|
4
5
|
/** Prepare the entire configuration before any DOM mutation. */
|
|
5
|
-
export declare function prepareConfiguration(current:
|
|
6
|
+
export declare function prepareConfiguration(current: PresentationSnapshot, patch: MapConfiguration): PresentationSnapshot;
|
|
7
|
+
export {};
|
|
@@ -24,7 +24,12 @@ export interface MapOptions extends Omit<NeighborhoodExplorerOptions, keyof MapF
|
|
|
24
24
|
features?: MapFeatures;
|
|
25
25
|
appearance?: MapAppearance | undefined;
|
|
26
26
|
}
|
|
27
|
+
/** Current geographic choices and detached presentation overrides; excludes camera and selection. */
|
|
27
28
|
export interface MapConfigurationSnapshot {
|
|
29
|
+
mode: NonNullable<MapOptions['mode']>;
|
|
30
|
+
source: NeighborhoodSource;
|
|
31
|
+
year: DistrictYear;
|
|
32
|
+
labels: boolean;
|
|
28
33
|
features: MapFeatures;
|
|
29
34
|
/** Explicit overrides; absent values continue to follow the current mode. */
|
|
30
35
|
layers: InteractiveLayers;
|
|
@@ -58,6 +63,19 @@ export type MapSelectionChange = {
|
|
|
58
63
|
current: DistrictSelection | null;
|
|
59
64
|
previous: DistrictSelection | null;
|
|
60
65
|
};
|
|
66
|
+
/** Dataset identity is explicit; selection never switches geography implicitly. */
|
|
67
|
+
export type MapFeatureReference = {
|
|
68
|
+
kind: 'marker';
|
|
69
|
+
id: string | null;
|
|
70
|
+
} | {
|
|
71
|
+
kind: 'neighborhood';
|
|
72
|
+
source: NeighborhoodSource;
|
|
73
|
+
id: string | null;
|
|
74
|
+
} | {
|
|
75
|
+
kind: 'district';
|
|
76
|
+
year: DistrictYear;
|
|
77
|
+
id: number | null;
|
|
78
|
+
};
|
|
61
79
|
export interface MapEvents {
|
|
62
80
|
/** Common selection envelope; existing change events retain their payloads. */
|
|
63
81
|
selectionchange: MapSelectionChange;
|
|
@@ -97,6 +115,10 @@ export interface MapEvents {
|
|
|
97
115
|
};
|
|
98
116
|
mapresize: undefined;
|
|
99
117
|
}
|
|
118
|
+
/** Convenience setters retain their historic reset default; false preserves the camera. */
|
|
119
|
+
export interface MapViewUpdateOptions {
|
|
120
|
+
resetView?: boolean;
|
|
121
|
+
}
|
|
100
122
|
export interface MapCamera {
|
|
101
123
|
/** Fixed 800 × 800 projected space, not longitude/latitude. */
|
|
102
124
|
get(): MapViewport;
|
|
@@ -124,6 +146,10 @@ export interface MapController {
|
|
|
124
146
|
/** Reconcile stable IDs; retained markers preserve nodes, focus, and entrance animations. */
|
|
125
147
|
setMarkers(markers: readonly MapMarker[]): void;
|
|
126
148
|
setOverlays(overlays: readonly MapOverlay[]): void;
|
|
149
|
+
/** False for missing IDs or a source/year other than the current geography. */
|
|
150
|
+
selectFeature(reference: MapFeatureReference, options?: CameraOptions & {
|
|
151
|
+
fit?: boolean;
|
|
152
|
+
}): boolean;
|
|
127
153
|
selectMarker(id: string | null, options?: CameraOptions & {
|
|
128
154
|
fit?: boolean;
|
|
129
155
|
}): boolean;
|
|
@@ -138,8 +164,8 @@ export interface MapController {
|
|
|
138
164
|
getSelectedDistrict(): DistrictSelection | null;
|
|
139
165
|
setDistrictYear(year: DistrictYear, options?: CameraOptions): void;
|
|
140
166
|
setDistrictStyle(style: ((district: DistrictSelection['district']) => DistrictStyle) | undefined): void;
|
|
141
|
-
setSource(source: NeighborhoodSource): void;
|
|
142
|
-
setMode(mode: NonNullable<MapOptions['mode']
|
|
167
|
+
setSource(source: NeighborhoodSource, options?: MapViewUpdateOptions): void;
|
|
168
|
+
setMode(mode: NonNullable<MapOptions['mode']>, options?: MapViewUpdateOptions): void;
|
|
143
169
|
setLabels(visible: boolean): void;
|
|
144
170
|
setTouchNavigation(enabled: boolean): void;
|
|
145
171
|
projectToScreen(lng: number, lat: number): {
|
|
@@ -1064,7 +1064,10 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
1064
1064
|
function selectNeighborhood(name, options = {}) {
|
|
1065
1065
|
if (destroyed)
|
|
1066
1066
|
return false;
|
|
1067
|
-
const feature = name === null
|
|
1067
|
+
const feature = name === null
|
|
1068
|
+
? undefined
|
|
1069
|
+
: (collections[source]?.features.find((entry) => entry.id === name) ??
|
|
1070
|
+
getNeighborhood(name, source));
|
|
1068
1071
|
if (name !== null && !feature)
|
|
1069
1072
|
return false;
|
|
1070
1073
|
validateCameraOptions(options);
|
|
@@ -1668,6 +1671,8 @@ export function createNeighborhoodExplorerCore(options = {}, data) {
|
|
|
1668
1671
|
throw new RangeError(`No ${nextYear} district dataset was supplied.`);
|
|
1669
1672
|
if (nextMode === 'neighborhoods' && !sources.length)
|
|
1670
1673
|
throw new RangeError('No neighborhood dataset was supplied.');
|
|
1674
|
+
if (patch.source !== undefined && !Object.hasOwn(collections, patch.source))
|
|
1675
|
+
throw new RangeError(`Unknown neighborhood source: ${patch.source}`);
|
|
1671
1676
|
const commitSource = patch.source !== undefined && patch.source !== source
|
|
1672
1677
|
? prepareSource(patch.source, false)
|
|
1673
1678
|
: undefined;
|
package/dist/src/map-core.js
CHANGED
|
@@ -105,7 +105,8 @@ const transitColors = {
|
|
|
105
105
|
selected: '#0073ae',
|
|
106
106
|
};
|
|
107
107
|
export function resolveMapColors(theme, colors = {}) {
|
|
108
|
-
|
|
108
|
+
const supplied = Object.fromEntries(Object.entries(colors).filter(([, value]) => value !== undefined));
|
|
109
|
+
return { ...defaults, ...(theme === 'transit' ? transitColors : {}), ...supplied };
|
|
109
110
|
}
|
|
110
111
|
let sequence = 0;
|
|
111
112
|
/** Make an offline SVG and the matching longitude/latitude projection. */
|
|
@@ -143,8 +144,29 @@ export function createSFMapWithData(options, data) {
|
|
|
143
144
|
};
|
|
144
145
|
})
|
|
145
146
|
: [];
|
|
147
|
+
const neighborhoodDescription = options.source === 'realtor'
|
|
148
|
+
? 'SFAR realtor neighborhood areas, defined in August 2010'
|
|
149
|
+
: options.source === 'sf-find'
|
|
150
|
+
? 'SF Find neighborhood areas'
|
|
151
|
+
: options.source === 'analysis'
|
|
152
|
+
? 'DataSF analysis neighborhood areas'
|
|
153
|
+
: 'the supplied neighborhood areas';
|
|
154
|
+
const description = [
|
|
155
|
+
districts.length && (districtFills || districtLines || (labels && districtLabels))
|
|
156
|
+
? `San Francisco district boundaries (${year}).`
|
|
157
|
+
: 'San Francisco map.',
|
|
158
|
+
neighborhoodLines && data.neighborhoods?.length
|
|
159
|
+
? `Dashed lines show ${neighborhoodDescription}.`
|
|
160
|
+
: '',
|
|
161
|
+
landmarks && landmarkData.length ? `Park and landmark features: ${landmarkData.length}.` : '',
|
|
162
|
+
keyRoads && roadData.length ? 'Selected road corridors.' : '',
|
|
163
|
+
bartStations && stationData.length ? `BART stations: ${stationData.length}.` : '',
|
|
164
|
+
'See SOURCES.md for geographic sources.',
|
|
165
|
+
]
|
|
166
|
+
.filter(Boolean)
|
|
167
|
+
.join(' ');
|
|
146
168
|
const parts = [
|
|
147
|
-
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}" role="img" aria-labelledby="${idPrefix}-title" data-sf-map="" data-year="${year}"${animated ? ` data-sf-animate="${idPrefix}"` : ''} style="max-width:100%;height:auto"><title id="${idPrefix}-title">${escapeXml(title)}</title><desc
|
|
169
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}" role="img" aria-labelledby="${idPrefix}-title" data-sf-map="" data-year="${year}"${animated ? ` data-sf-animate="${idPrefix}"` : ''} style="max-width:100%;height:auto"><title id="${idPrefix}-title">${escapeXml(title)}</title><desc>${escapeXml(description)}</desc>${animation ? staticAnimationStyle(idPrefix, animation) : ''}<defs><clipPath id="${idPrefix}-coast"><path d="${coastPath}" fill-rule="evenodd" clip-rule="evenodd"/></clipPath></defs><rect width="${width}" height="${height}" fill="${escapeXml(colors.water)}"/><g data-layer="geography"><path data-layer="coast" d="${coastPath}" fill="${escapeXml(colors.land)}" fill-rule="evenodd"/>`,
|
|
148
170
|
];
|
|
149
171
|
// Explicit drawing order keeps optional overlays and user markers predictable.
|
|
150
172
|
if (districtFills)
|
package/dist/src/map.js
CHANGED
|
@@ -39,6 +39,7 @@ export function createMap(data, options = {}) {
|
|
|
39
39
|
])),
|
|
40
40
|
});
|
|
41
41
|
const snapshot = () => ({
|
|
42
|
+
...element.getMapState(),
|
|
42
43
|
...structuredClone({
|
|
43
44
|
features: config.features,
|
|
44
45
|
layers: config.layers,
|
|
@@ -129,7 +130,9 @@ export function createMap(data, options = {}) {
|
|
|
129
130
|
key,
|
|
130
131
|
next.features[key],
|
|
131
132
|
]));
|
|
133
|
+
let committed = false;
|
|
132
134
|
element.applyPresentation({ ...patch, ...('appearance' in patch ? { appearance: next.appearance } : {}) }, () => {
|
|
135
|
+
committed = true;
|
|
133
136
|
config = next;
|
|
134
137
|
if (hasFeatures)
|
|
135
138
|
element.setFeatures(features);
|
|
@@ -138,6 +141,14 @@ export function createMap(data, options = {}) {
|
|
|
138
141
|
if (hasControls)
|
|
139
142
|
element.setControls(controls);
|
|
140
143
|
}, () => !destroyed && revision === configurationRevision);
|
|
144
|
+
return committed && !destroyed && revision === configurationRevision;
|
|
145
|
+
}
|
|
146
|
+
function updateView(patch, options = {}) {
|
|
147
|
+
assertOptions(options, 'view update', ['resetView']);
|
|
148
|
+
if (options.resetView !== undefined && typeof options.resetView !== 'boolean')
|
|
149
|
+
throw new TypeError('resetView must be a boolean.');
|
|
150
|
+
if (configure(patch) && options.resetView !== false)
|
|
151
|
+
element.resetView();
|
|
141
152
|
}
|
|
142
153
|
return Object.freeze({
|
|
143
154
|
element,
|
|
@@ -146,7 +157,9 @@ export function createMap(data, options = {}) {
|
|
|
146
157
|
get destroyed() {
|
|
147
158
|
return destroyed;
|
|
148
159
|
},
|
|
149
|
-
configure: use(
|
|
160
|
+
configure: use((patch) => {
|
|
161
|
+
configure(patch);
|
|
162
|
+
}),
|
|
150
163
|
getConfiguration: use(snapshot),
|
|
151
164
|
getResolvedConfiguration: use(resolved),
|
|
152
165
|
getCapabilities: use(capabilities),
|
|
@@ -180,6 +193,41 @@ export function createMap(data, options = {}) {
|
|
|
180
193
|
}),
|
|
181
194
|
setMarkers: use(element.setMarkers),
|
|
182
195
|
setOverlays: use(element.setOverlays),
|
|
196
|
+
selectFeature: use((reference, options) => {
|
|
197
|
+
assertOptions(reference, 'feature reference');
|
|
198
|
+
if (!['marker', 'neighborhood', 'district'].includes(reference.kind))
|
|
199
|
+
throw new TypeError('Unknown feature kind.');
|
|
200
|
+
assertOptions(reference, 'feature reference', reference.kind === 'marker'
|
|
201
|
+
? ['kind', 'id']
|
|
202
|
+
: reference.kind === 'neighborhood'
|
|
203
|
+
? ['kind', 'id', 'source']
|
|
204
|
+
: ['kind', 'id', 'year']);
|
|
205
|
+
if (reference.kind === 'district') {
|
|
206
|
+
if (reference.id !== null &&
|
|
207
|
+
(typeof reference.id !== 'number' || !Number.isFinite(reference.id)))
|
|
208
|
+
throw new TypeError('District IDs must be finite numbers or null.');
|
|
209
|
+
if (![2002, 2012, 2022].includes(reference.year))
|
|
210
|
+
throw new RangeError('Unknown district year.');
|
|
211
|
+
if (reference.year !== element.getMapState().year || !yearList.includes(reference.year))
|
|
212
|
+
return false;
|
|
213
|
+
return element.selectDistrict(reference.id, options);
|
|
214
|
+
}
|
|
215
|
+
if (reference.id !== null && (typeof reference.id !== 'string' || !reference.id))
|
|
216
|
+
throw new TypeError('Feature IDs must be nonempty strings or null.');
|
|
217
|
+
if (reference.kind === 'marker')
|
|
218
|
+
return element.selectMarker(reference.id, options);
|
|
219
|
+
if (!['realtor', 'sf-find', 'analysis'].includes(reference.source))
|
|
220
|
+
throw new RangeError('Unknown neighborhood source.');
|
|
221
|
+
if (reference.source !== element.getMapState().source ||
|
|
222
|
+
!sourceList.includes(reference.source))
|
|
223
|
+
return false;
|
|
224
|
+
const feature = reference.id === null
|
|
225
|
+
? null
|
|
226
|
+
: data.neighborhoods[reference.source]?.features.find((entry) => entry.id === reference.id);
|
|
227
|
+
if (reference.id !== null && !feature)
|
|
228
|
+
return false;
|
|
229
|
+
return element.selectNeighborhood(feature?.id ?? null, options);
|
|
230
|
+
}),
|
|
183
231
|
selectMarker: use(element.selectMarker),
|
|
184
232
|
getSelectedMarker: use(element.getSelectedMarker),
|
|
185
233
|
selectNeighborhood: use(element.selectNeighborhood),
|
|
@@ -193,17 +241,10 @@ export function createMap(data, options = {}) {
|
|
|
193
241
|
setDistrictStyle: use((style) => {
|
|
194
242
|
configure({ appearance: { districtStyle: style } });
|
|
195
243
|
}),
|
|
196
|
-
setSource: use((
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
setMode: use((...args) => {
|
|
201
|
-
configurationRevision++;
|
|
202
|
-
element.setMode(...args);
|
|
203
|
-
}),
|
|
204
|
-
setLabels: use((...args) => {
|
|
205
|
-
configurationRevision++;
|
|
206
|
-
element.setLabels(...args);
|
|
244
|
+
setSource: use((source, options) => updateView({ source }, options)),
|
|
245
|
+
setMode: use((mode, options) => updateView({ mode }, options)),
|
|
246
|
+
setLabels: use((labels) => {
|
|
247
|
+
configure({ labels });
|
|
207
248
|
}),
|
|
208
249
|
setTouchNavigation: use(element.setTouchNavigation),
|
|
209
250
|
projectToScreen: use(element.projectToScreen),
|
package/dist/src/static.d.ts
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
|
+
import type { InteractiveSFMapData } from './explorer-data.js';
|
|
1
2
|
import { type SFMapData } from './map-core.js';
|
|
2
3
|
import type { SFMapOptions } from './types.js';
|
|
4
|
+
/** Either compact static geography or the same source-aware geography used by createMap. */
|
|
5
|
+
export type StaticMapInput = SFMapData | InteractiveSFMapData;
|
|
3
6
|
export type { DistrictRowData, SFMapData as StaticMapData } from './map-core.js';
|
|
4
7
|
export type { DistrictStyle, DistrictYear, SFMapOptions as StaticMapOptions, StaticMapAppearance, StaticMapLayers, } from './types.js';
|
|
5
8
|
/** Server-safe SVG rendering with explicit data; returns SVG plus projection helpers. */
|
|
6
|
-
export declare function renderMap(data:
|
|
9
|
+
export declare function renderMap(data: StaticMapInput, options?: SFMapOptions): {
|
|
7
10
|
svg: string;
|
|
8
11
|
project: (coordinates: import("../data/types.js").Position) => [number, number];
|
|
9
12
|
viewBox: [number, number, number, number];
|
|
10
13
|
};
|
|
11
14
|
/** Project canonical layer geometry without constructing an SVG string. */
|
|
12
|
-
export declare function getLayerPaths(data:
|
|
15
|
+
export declare function getLayerPaths(data: StaticMapInput, options?: SFMapOptions): {
|
|
13
16
|
year: import("./types.js").DistrictYear;
|
|
14
17
|
viewBox: [number, number, number, number];
|
|
15
18
|
project: (coordinates: import("../data/types.js").Position) => [number, number];
|
package/dist/src/static.js
CHANGED
|
@@ -1,37 +1,99 @@
|
|
|
1
1
|
import { prepareAppearance } from './appearance.js';
|
|
2
2
|
import { validateSwitchPatch } from './features.js';
|
|
3
3
|
import { createSFMapWithData, getLayerPathsWithData } from './map-core.js';
|
|
4
|
-
import {
|
|
4
|
+
import { normalizeStaticAnimation } from './static-animation.js';
|
|
5
|
+
import { assertOptions, validateMarkers, validateOverlays } from './validation.js';
|
|
5
6
|
/** Server-safe SVG rendering with explicit data; returns SVG plus projection helpers. */
|
|
6
7
|
export function renderMap(data, options = {}) {
|
|
7
|
-
|
|
8
|
+
const prepared = expandStaticOptions(options);
|
|
9
|
+
const { map, source } = staticInput(data, prepared);
|
|
10
|
+
return createSFMapWithData({ ...prepared, source }, map);
|
|
8
11
|
}
|
|
9
12
|
/** Project canonical layer geometry without constructing an SVG string. */
|
|
10
13
|
export function getLayerPaths(data, options = {}) {
|
|
11
|
-
|
|
14
|
+
const prepared = expandStaticOptions(options);
|
|
15
|
+
return getLayerPathsWithData(prepared, staticInput(data, prepared).map);
|
|
12
16
|
}
|
|
17
|
+
/** Borrow immutable geometry, deriving only the renderer's lightweight neighborhood rows. */
|
|
18
|
+
function staticInput(data, options) {
|
|
19
|
+
if (options.source !== undefined && !['realtor', 'sf-find', 'analysis'].includes(options.source))
|
|
20
|
+
throw new RangeError('Unknown neighborhood source.');
|
|
21
|
+
if (!('map' in data)) {
|
|
22
|
+
if (options.source !== undefined)
|
|
23
|
+
throw new TypeError('source requires source-aware MapData; compact StaticMapData already chooses its geography.');
|
|
24
|
+
return { map: data, source: undefined };
|
|
25
|
+
}
|
|
26
|
+
const source = options.source ??
|
|
27
|
+
['realtor', 'sf-find', 'analysis'].find((candidate) => !!data.neighborhoods[candidate]);
|
|
28
|
+
if (source === undefined)
|
|
29
|
+
return { map: data.map, source: undefined };
|
|
30
|
+
const collection = data.neighborhoods[source];
|
|
31
|
+
if (!collection)
|
|
32
|
+
throw new RangeError(`No ${source} neighborhood dataset was supplied.`);
|
|
33
|
+
return {
|
|
34
|
+
source,
|
|
35
|
+
map: {
|
|
36
|
+
...data.map,
|
|
37
|
+
neighborhoods: collection.features.map(({ geometry, properties }) => ({
|
|
38
|
+
name: properties.sourceName,
|
|
39
|
+
geometry,
|
|
40
|
+
})),
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const staticLayerKeys = [
|
|
45
|
+
'districtFills',
|
|
46
|
+
'districtLines',
|
|
47
|
+
'districtLabels',
|
|
48
|
+
'neighborhoodLines',
|
|
49
|
+
'landmarks',
|
|
50
|
+
'bartStations',
|
|
51
|
+
'highways',
|
|
52
|
+
'keyRoads',
|
|
53
|
+
'roadLabels',
|
|
54
|
+
];
|
|
13
55
|
/** Grouped keys override flat compatibility keys; omitted keys preserve renderer defaults. */
|
|
14
56
|
function expandStaticOptions(options) {
|
|
15
|
-
assertOptions(options, 'static map'
|
|
57
|
+
assertOptions(options, 'static map', [
|
|
58
|
+
...staticLayerKeys,
|
|
59
|
+
'layers',
|
|
60
|
+
'appearance',
|
|
61
|
+
'theme',
|
|
62
|
+
'colors',
|
|
63
|
+
'districtStyle',
|
|
64
|
+
'width',
|
|
65
|
+
'height',
|
|
66
|
+
'padding',
|
|
67
|
+
'year',
|
|
68
|
+
'source',
|
|
69
|
+
'labels',
|
|
70
|
+
'markers',
|
|
71
|
+
'overlays',
|
|
72
|
+
'title',
|
|
73
|
+
'idPrefix',
|
|
74
|
+
'animation',
|
|
75
|
+
]);
|
|
76
|
+
for (const key of [...staticLayerKeys, 'labels'])
|
|
77
|
+
if (options[key] !== undefined && typeof options[key] !== 'boolean')
|
|
78
|
+
throw new TypeError(`${key} must be boolean.`);
|
|
79
|
+
for (const key of ['title', 'idPrefix'])
|
|
80
|
+
if (options[key] !== undefined && typeof options[key] !== 'string')
|
|
81
|
+
throw new TypeError(`${key} must be a string.`);
|
|
82
|
+
if (options.markers !== undefined)
|
|
83
|
+
validateMarkers(options.markers);
|
|
84
|
+
if (options.overlays !== undefined)
|
|
85
|
+
validateOverlays(options.overlays);
|
|
86
|
+
normalizeStaticAnimation(options.animation);
|
|
16
87
|
if (options.layers !== undefined)
|
|
17
|
-
validateSwitchPatch(options.layers,
|
|
18
|
-
'districtFills',
|
|
19
|
-
'districtLines',
|
|
20
|
-
'districtLabels',
|
|
21
|
-
'neighborhoodLines',
|
|
22
|
-
'landmarks',
|
|
23
|
-
'bartStations',
|
|
24
|
-
'highways',
|
|
25
|
-
'keyRoads',
|
|
26
|
-
'roadLabels',
|
|
27
|
-
]);
|
|
88
|
+
validateSwitchPatch(options.layers, staticLayerKeys);
|
|
28
89
|
if (options.appearance !== undefined) {
|
|
29
90
|
assertOptions(options.appearance, 'static appearance', ['theme', 'colors', 'districtStyle']);
|
|
30
91
|
}
|
|
31
|
-
const { layers, appearance, ...flat } = options;
|
|
92
|
+
const { layers, appearance, theme, colors, districtStyle, ...flat } = options;
|
|
32
93
|
const supplied = (group = {}) => Object.fromEntries(Object.entries(group).filter(([, value]) => value !== undefined));
|
|
33
94
|
return {
|
|
34
95
|
...flat,
|
|
96
|
+
...prepareAppearance({}, { theme, colors, districtStyle }),
|
|
35
97
|
...supplied(layers),
|
|
36
98
|
...supplied(appearance === undefined ? undefined : prepareAppearance({}, appearance)),
|
|
37
99
|
};
|
package/dist/src/types.d.ts
CHANGED
|
@@ -41,6 +41,8 @@ export interface SFMapOptions {
|
|
|
41
41
|
height?: number;
|
|
42
42
|
padding?: number;
|
|
43
43
|
year?: DistrictYear;
|
|
44
|
+
/** Select a supplied neighborhood definition when rendering source-aware MapData. */
|
|
45
|
+
source?: NeighborhoodSource;
|
|
44
46
|
districtLines?: boolean;
|
|
45
47
|
neighborhoodLines?: boolean;
|
|
46
48
|
districtFills?: boolean;
|
|
@@ -251,4 +253,4 @@ export interface NeighborhoodExplorerElement extends HTMLElement {
|
|
|
251
253
|
export interface TransitAnimationElement extends HTMLElement {
|
|
252
254
|
destroy(): void;
|
|
253
255
|
}
|
|
254
|
-
export type { MapAppearance, MapCamera, MapCapabilities, MapConfiguration, MapConfigurationSnapshot, MapController, MapControls, MapEvents, MapOptions, MapSelectionChange, ResolvedMapConfiguration, } from './controller-types.js';
|
|
256
|
+
export type { MapAppearance, MapCamera, MapCapabilities, MapConfiguration, MapConfigurationSnapshot, MapController, MapControls, MapEvents, MapFeatureReference, MapOptions, MapSelectionChange, MapViewUpdateOptions, ResolvedMapConfiguration, } from './controller-types.js';
|
package/docs/API.md
CHANGED
|
@@ -6,6 +6,8 @@ and event types are exported by [src/api.ts](../src/api.ts) and defined in
|
|
|
6
6
|
|
|
7
7
|
## Static maps
|
|
8
8
|
|
|
9
|
+
Static maps accept compact `StaticMapData` or the same source-aware `MapData` as interactive maps. With `MapData`, `source` selects a supplied neighborhood definition; SFAR realtor is preferred by default, followed by SF Find and analysis when available. A requested unavailable source throws before styling callbacks run. Compact `StaticMapData` already chooses its neighborhoods, so it rejects a `source` option. Both data shapes retain the static renderer defaults.
|
|
10
|
+
|
|
9
11
|
Static maps accept the same grouped presentation vocabulary as interactive maps:
|
|
10
12
|
|
|
11
13
|
```ts
|
|
@@ -17,12 +19,14 @@ const { svg } = renderMap(staticMapData, presentation);
|
|
|
17
19
|
const map = createMap(fullMapData, presentation);
|
|
18
20
|
```
|
|
19
21
|
|
|
20
|
-
Static `layers` supports every interactive layer switch except `neighborhoodLabels`. Static `appearance` supports `theme`, `colors`, and `districtStyle`; browser typography, area interaction styles, and screen-space marker sizing remain interactive settings. Unknown
|
|
22
|
+
Static `layers` supports every interactive layer switch except `neighborhoodLabels`. Static `appearance` supports `theme`, `colors`, and `districtStyle`; browser typography, area interaction styles, and screen-space marker sizing remain interactive settings. Unknown keys and invalid values are rejected for both flat and grouped options. Supplied grouped keys override flat compatibility keys; omitted or `undefined` grouped keys preserve the flat value or renderer default. Undefined color tokens use the theme default instead of entering SVG attributes. The two renderers retain their existing defaults. Marker arrays accept readonly inputs.
|
|
21
23
|
|
|
22
24
|
Flat compatibility options include `theme`, `width`, `height`, `padding`, `year` (2002, 2012, 2022), `districtLines`, `districtFills`, `districtStyle`, `districtLabels`, `neighborhoodLines`, `labels`, `highways`, `keyRoads`, `roadLabels`, `landmarks`, `bartStations`, `markers`, `overlays`, `title`, `idPrefix`, `colors`, and `animation`. Each optional layer is independent. User-supplied text and attributes are escaped in SVG output.
|
|
23
25
|
|
|
24
26
|
Custom district IDs must be finite numbers. An empty `labelPoints` array uses the district's primary `label` position.
|
|
25
27
|
|
|
28
|
+
The SVG description names the selected neighborhood source for source-aware data and uses a generic description for compact inputs. It describes enabled layers only when their data is supplied, using actual park and station counts.
|
|
29
|
+
|
|
26
30
|
`animation: true` (or `{ duration, delay }`) makes a static map draw itself with self-contained, scoped CSS: land fades in, the coast and lines draw, districts grow, then labels and points appear. It plays when the SVG is inserted into a page or loaded as an image, needs no JavaScript, and stays still under `prefers-reduced-motion`. Re-insert the markup to replay it. Default output is unchanged.
|
|
27
31
|
|
|
28
32
|
## Interactive maps
|
|
@@ -47,7 +51,7 @@ District style callbacks are evaluated once per district at construction and on
|
|
|
47
51
|
|
|
48
52
|
## Live configuration and inspection
|
|
49
53
|
|
|
50
|
-
`map.configure({ appearance, mode, source, year, labels, layers, features, controls })` updates an existing map. Appearance updates preserve the camera, selections, retained marker nodes, and keyboard focus. Mode/source changes through `configure` preserve the camera;
|
|
54
|
+
`map.configure({ appearance, mode, source, year, labels, layers, features, controls })` updates an existing map. Appearance updates preserve the camera, selections, retained marker nodes, and keyboard focus. Mode/source changes through `configure` preserve the camera; the `setMode(mode, { resetView: false })` and `setSource(source, { resetView: false })` convenience methods use the same atomic update path and preserve the camera. Omit the second argument to retain their existing reset behavior. Invalid view-update options fail before changing configuration. Switching neighborhood source clears the neighborhood selection. Changing district year retains a selected district when it exists in the new year. Unsupported datasets, invalid appearance values, and invalid district-style callback results fail during preparation before commit. A reentrant styling callback can supersede the pending patch.
|
|
51
55
|
|
|
52
56
|
Appearance token objects (`colors`, `style`, `labelStyle`, `labelSize`, `areaStyle`) merge supplied keys; other appearance properties replace. Omitted keys retain their value, an explicit `undefined` property removes that override, and `appearance: undefined` resets the whole appearance group. Feature objects retain their existing replacement semantics. Mode/source/year/labels ignore `undefined`; reset them with an explicit value. Patch types explicitly permit these resets with TypeScript’s `exactOptionalPropertyTypes` enabled.
|
|
53
57
|
|
|
@@ -63,7 +67,7 @@ const effective = map.getResolvedConfiguration();
|
|
|
63
67
|
const available = map.getCapabilities();
|
|
64
68
|
```
|
|
65
69
|
|
|
66
|
-
`getConfiguration()` returns detached feature, layer, control, and appearance overrides; district style callbacks retain their function identity. `getResolvedConfiguration()` adds the current mode, source, year and master labels and resolves **layer switches** through mode defaults, available data, and the master label switch. Appearance/control fields remain overrides. It does not claim that every label is visible: zoom and collision filtering still apply. `getCapabilities()` returns supplied `sources`, usable district `years`, and layer data availability for the current source/year, independent of visibility overrides. These reads throw after destruction.
|
|
70
|
+
`getConfiguration()` returns the current mode, source, year and master labels, together with detached feature, layer, control, and appearance overrides; district style callbacks retain their function identity. `getResolvedConfiguration()` adds the current mode, source, year and master labels and resolves **layer switches** through mode defaults, available data, and the master label switch. Appearance/control fields remain overrides. It does not claim that every label is visible: zoom and collision filtering still apply. `getCapabilities()` returns supplied `sources`, usable district `years`, and layer data availability for the current source/year, independent of visibility overrides. These reads throw after destruction.
|
|
67
71
|
|
|
68
72
|
## Common selection event
|
|
69
73
|
|
|
@@ -78,6 +82,18 @@ map.on('selectionchange', (event) => {
|
|
|
78
82
|
|
|
79
83
|
`kind` is `marker`, `neighborhood`, or `district`. `current` and `previous` are detached selection snapshots or `null`. The event follows committed selection changes, including clearing and a changed district vintage; it does not represent every activation. Existing `markerchange`, `neighborhoodchange`, and `districtchange` events remain supported.
|
|
80
84
|
|
|
85
|
+
## Source-aware feature selection
|
|
86
|
+
|
|
87
|
+
`selectFeature(reference, options)` accepts one typed identity for each selectable kind:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
map.selectFeature({ kind: 'marker', id: 'ferry' }, { fit: false });
|
|
91
|
+
map.selectFeature({ kind: 'neighborhood', source: 'realtor', id: 'inner-mission' });
|
|
92
|
+
map.selectFeature({ kind: 'district', year: 2022, id: 3 });
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Neighborhood IDs are exact source-scoped feature IDs, rather than display names or aliases. District identity includes the boundary year. This operation returns `false` for a missing feature or a source/year other than the map's current supplied geography; it never silently switches definitions or years. Choose geography through `configure` first. An `id` of `null` clears that kind in the referenced current geography. Malformed references throw. Existing `selectMarker`, `selectNeighborhood`, and `selectDistrict` shortcuts remain supported.
|
|
96
|
+
|
|
81
97
|
## Coordinates and units
|
|
82
98
|
|
|
83
99
|
GeoJSON and static projection inputs use `[longitude, latitude]` in WGS84; markers use `{ lng, lat }`. Static `project` returns SVG units. `projectToScreen(lng, lat)` returns pixels relative to the mounted canvas. Camera viewports and pan deltas use the fixed 800 × 800 projected map space, independent of canvas size. Marker radius uses screen pixels in browser maps and SVG units in static output.
|
package/docs/api-audit.md
CHANGED
|
@@ -19,7 +19,8 @@ unbounded geographic data can never fail.
|
|
|
19
19
|
| Clustering and selected/focused markers | Selected/focused pins remain individually reachable. Coincident alternatives remain available through the chooser. |
|
|
20
20
|
| Marker entrance and reduced motion | Entrances are optional, independent of camera motion, and cancel on preference changes or disposal. Stable IDs avoid repeated entrances on filtering. |
|
|
21
21
|
| Hiding touch control while gestures are enabled | Returns touch scrolling to the page. Other toolbar controls remain independent. |
|
|
22
|
-
|
|
|
22
|
+
| Convenience mode/source setters | Share the atomic configuration path. `{ resetView: false }` preserves the camera; omitted options retain the historic reset. Invalid options and unavailable geography leave state unchanged. |
|
|
23
|
+
| Runtime patch omitted/false/undefined | Omitted retains; false disables boolean switches; undefined removes group/key overrides. Top-level mode/source/year/labels set to undefined retain their current values. Nested feature objects replace, rather than deep-merge. |
|
|
23
24
|
| Compact shell and full attribution | Rejected before replacement. Use `createGuideController` for full attribution. Control overrides are honored and may deliberately alter layout. |
|
|
24
25
|
| District style callbacks | Prepare and copy every style before committing. Hover/selection reuse the results; call `setDistrictStyle()` to refresh changed external data. Reentrant updates or destruction supersede pending work. |
|
|
25
26
|
| Rapid district year changes | Both outgoing layers are tracked, inert, and removed on interruption, completion, reduced-motion changes, or destruction. Zero duration creates no transition copies. |
|
|
@@ -29,6 +30,8 @@ unbounded geographic data can never fail.
|
|
|
29
30
|
| Guide controllers and compatibility factories | `createGuideController`/`mountGuideController` share grouped options and lifecycle with `createMap`. Existing element factories retain flat options and their return types. |
|
|
30
31
|
| Layer hidden and bundle cost | Visibility never unloads imported geography. Use narrow entrypoints or data injection to save bytes. |
|
|
31
32
|
|
|
33
|
+
Live appearance patches prepare values, district styles and source replacement before committing; token objects merge by key. Styling callbacks that issue a newer presentation update supersede the pending patch. Resolved layer reads distinguish visibility settings from data availability and from zoom/collision label filtering. The additive selection envelope preserves existing event contracts.
|
|
34
|
+
|
|
32
35
|
## Failure boundaries and regression evidence
|
|
33
36
|
|
|
34
37
|
| Surface | Adversarial checks and behavior |
|
|
@@ -70,11 +73,6 @@ Exercise enable/disable round trips and callbacks that synchronously request a
|
|
|
70
73
|
second update. Add published-type and bundle regressions when the import graph or
|
|
71
74
|
public types change. Update this matrix when introducing a new interaction.
|
|
72
75
|
|
|
73
|
-
## Verification
|
|
76
|
+
## Verification
|
|
74
77
|
|
|
75
|
-
-
|
|
76
|
-
- `pnpm test:stories:coverage` runs Chromium stories and reports browser coverage.
|
|
77
|
-
- `pnpm demo`, `pnpm build-storybook`, `pnpm test:package`, and `pnpm test:visual` cover examples, rendered stories, packed consumer imports, and reviewed map screenshots.
|
|
78
|
-
- The packed consumer asserts removed compatibility entry points are absent.
|
|
79
|
-
|
|
80
|
-
Live appearance patches prepare values, district styles and source replacement before committing; token objects merge by key. Styling callbacks that issue a newer presentation update supersede the pending patch. Resolved layer reads distinguish visibility settings from data availability and from zoom/collision label filtering. The additive selection envelope preserves existing event contracts.
|
|
78
|
+
Follow the [validation and release runbook](https://github.com/kahwee/sf-map-svg/blob/main/docs/maintenance.md#validation-by-change). The packed consumer also asserts that removed compatibility entry points remain absent.
|
|
@@ -1,23 +1,47 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Embedding and progressive enhancement
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
For basic mounting, events and cleanup, start with the [developer guide](developer-guide.md). The root controller takes explicit geography; the guide entry offers a smaller preset with the same grouped `configure()`, `camera`, `on()`, and `destroy()` contract. See the [migration guide](migration-v3.md) for removed v2 imports.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Render a guide before JavaScript loads
|
|
6
|
+
|
|
7
|
+
On the server or in a build step, render a compact frame:
|
|
6
8
|
|
|
7
9
|
```ts
|
|
8
|
-
import {
|
|
10
|
+
import { createGuideShell } from '@kahwee/sf-map-svg/guide/static';
|
|
11
|
+
|
|
12
|
+
const markup = createGuideShell({ title: 'Explore San Francisco' });
|
|
13
|
+
// Insert markup into your server-rendered HTML.
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The shell uses an 800 × 800 projection with 28 units of padding, matching the interactive map. Use `createGuideSVG()` for custom dimensions. Rendering is offline, with zero runtime dependencies.
|
|
17
|
+
|
|
18
|
+
## Enhance the existing frame
|
|
19
|
+
|
|
20
|
+
In your browser entry, mount the controller into that shell:
|
|
9
21
|
|
|
10
|
-
|
|
22
|
+
```ts
|
|
23
|
+
import { mountGuideController } from '@kahwee/sf-map-svg/guide';
|
|
24
|
+
|
|
25
|
+
const shell = document.querySelector<HTMLElement>('.sf-guide-shell');
|
|
26
|
+
if (!shell) throw new Error('Missing guide shell');
|
|
27
|
+
const guide = mountGuideController(shell, {
|
|
28
|
+
attribution: 'compact',
|
|
11
29
|
features: { motion: { duration: 400 }, markerEntrance: true },
|
|
12
30
|
appearance: { theme: 'transit' },
|
|
13
31
|
});
|
|
14
|
-
|
|
15
|
-
//
|
|
16
|
-
|
|
32
|
+
|
|
33
|
+
// Call before your application removes the containing view.
|
|
34
|
+
function disposeGuide() {
|
|
35
|
+
guide.destroy();
|
|
36
|
+
}
|
|
17
37
|
```
|
|
18
38
|
|
|
19
|
-
|
|
39
|
+
Compact attribution is required for an existing shell. Invalid configuration leaves its static frame intact. `destroy()` disposes listeners and animation work; your application owns removal of the containing view. Motion is opt-in and follows reduced-motion preferences.
|
|
40
|
+
|
|
41
|
+
For a new browser-only guide, use `createGuideController()` and mount its `.element` as shown in the developer guide. Existing `createGuideMap()` and `mountGuideMap()` retain their augmented-element return types and flat options for compatibility.
|
|
42
|
+
|
|
43
|
+
## Choose the data boundary
|
|
20
44
|
|
|
21
|
-
|
|
45
|
+
`createMap(guideMapData, guideOptions)` remains available through `/guide/data` and `/presets`. For server rendering, pass `guideMapData.map` or `/data/static` to `renderMap()`. Use `/data/full` when interactive lookup collections are needed. The guide's detailed geography loads only on an explicit `loadGuideDetailedData()` call; enabling a layer does not download it.
|
|
22
46
|
|
|
23
|
-
|
|
47
|
+
See the [exact API contract](API.md), [worked examples](EXAMPLES.md), and [bundle measurements](guide-bundle-report.md).
|
package/docs/developer-guide.md
CHANGED
|
@@ -80,7 +80,7 @@ function disposeMap() {
|
|
|
80
80
|
| Visible labels | On | On, subject to zoom/collisions |
|
|
81
81
|
| Motion features | Static animation off | Off; opt in via `features` |
|
|
82
82
|
|
|
83
|
-
A layer switch does not download missing geography. `fullMapData` has `.map` for static rendering and separate lookup collections for interactive use. `staticMapData` is already a static data object. Use `renderMap(guideMapData
|
|
83
|
+
A layer switch does not download missing geography. `fullMapData` has `.map` for static rendering and separate lookup collections for interactive use. `staticMapData` is already a static data object. Use `renderMap(guideMapData, options)` or `renderMap(fullMapData, { source: 'sf-find' })` to share source-aware geography with the browser controller. Passing `.map` remains supported as compact static geography; it already chooses its neighborhoods and does not accept a `source` option.
|
|
84
84
|
|
|
85
85
|
SFAR realtor neighborhoods are the default when supplied. If only an alternative collection is supplied, the controller selects that available source. Choose `source` explicitly when comparing definitions. Sources retain separate identities; they are not interchangeable polygons.
|
|
86
86
|
|
|
@@ -88,7 +88,7 @@ SFAR realtor neighborhoods are the default when supplied. If only an alternative
|
|
|
88
88
|
|
|
89
89
|
| Change | Call |
|
|
90
90
|
| --- | --- |
|
|
91
|
-
|
|
|
91
|
+
| Presentation, layers, controls, behaviors | `map.configure({ layers, controls, features, appearance, mode, source, year, labels })` |
|
|
92
92
|
| Map mode or neighborhood source | `map.setMode(mode)`, `map.setSource(source)` |
|
|
93
93
|
| District boundaries or choropleth | `map.setDistrictYear(year)`, `map.setDistrictStyle(callback)` |
|
|
94
94
|
| Master text visibility | `map.setLabels(visible)` |
|
|
@@ -96,6 +96,8 @@ SFAR realtor neighborhoods are the default when supplied. If only an alternative
|
|
|
96
96
|
| Camera | `map.camera.set/pan/zoom/fit/reset(...)` |
|
|
97
97
|
| Palette, typography, control styling | `map.configure({ appearance })`; retains camera, selection and focus |
|
|
98
98
|
|
|
99
|
+
Appearance token objects merge by key; `undefined` removes an override and `appearance: undefined` resets the group. Static maps accept grouped `layers` and the shared `appearance` keys (`theme`, `colors`, `districtStyle`) too. See [the API contract](API.md#live-configuration-and-inspection) and [the design playground](https://kahwee.github.io/sf-map-svg/playground.html).
|
|
100
|
+
|
|
99
101
|
`configure` merges supplied keys; omitted keys keep their existing values. Set a layer or control key to `undefined` to remove its override. Set a whole group to `undefined` to reset that group. Set a feature to `false` to disable it. Explicit layer overrides keep winning after mode changes.
|
|
100
102
|
|
|
101
103
|
```ts
|
|
@@ -105,7 +107,7 @@ map.configure({ features: { motion: false } });
|
|
|
105
107
|
map.configure({ layers: undefined }); // clear all explicit layer overrides
|
|
106
108
|
```
|
|
107
109
|
|
|
108
|
-
`getConfiguration()` returns a detached snapshot of
|
|
110
|
+
`getConfiguration()` returns current mode/source/year/labels and a detached snapshot of the presentation groups, including explicit layer and appearance overrides. Camera and selection remain separate runtime state. Use `getResolvedConfiguration()` for current mode/source/year/labels and effective layer switches, and `getCapabilities()` for supplied geography. Invalid configuration patches leave the current map unchanged. Convenience methods use the same atomic update path: pass `map.setMode(mode, { resetView: false })` or `map.setSource(source, { resetView: false })` to keep the camera. Their default still resets the view.
|
|
109
111
|
|
|
110
112
|
## Markers, routes and camera coordinates
|
|
111
113
|
|
|
@@ -135,5 +137,3 @@ The package declarations are the exact type contract for your installed version.
|
|
|
135
137
|
The generated `llms.txt` index links to Markdown documentation; `llms-full.txt` combines the guides and type contracts into one file. Both are built from the same package selected for the site. Local previews are labeled as working-tree docs. Released builds use the installed npm package's docs and declarations.
|
|
136
138
|
|
|
137
139
|
See [API options](API.md), [worked examples](EXAMPLES.md), [consumer integration](consumer-integration.md), and [v3 migration](migration-v3.md).
|
|
138
|
-
|
|
139
|
-
Appearance token objects merge by key; `undefined` removes an override and `appearance: undefined` resets the group. Static maps accept grouped `layers` and the shared `appearance` keys (`theme`, `colors`, `districtStyle`) too. See [the API contract](API.md#live-configuration-and-inspection) and [the design playground](https://kahwee.github.io/sf-map-svg/playground.html).
|
package/llms-full.txt
CHANGED
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
> Offline San Francisco SVG maps with explicit geographic imports, no runtime dependencies, static rendering, and a browser controller.
|
|
4
4
|
|
|
5
|
-
@kahwee/sf-map-svg 4.
|
|
5
|
+
@kahwee/sf-map-svg 4.3.1 · local working tree. Node >=24.0.0 for server rendering; browsers need a DOM and a JSON-capable bundler.
|
|
6
6
|
|
|
7
7
|
These are working-tree docs. Unreleased changelog entries may describe APIs absent from npm; verify the installed version before copying them.
|
|
8
8
|
|
|
9
9
|
Use renderMap(data, options) for SVG strings; createMap(data, options) returns a controller. Mount map.element and destroy it on unmount. Choose mode explicitly: createMap defaults to basemap. Geography is separate; staticMapData and fullMapData have different shapes.
|
|
10
10
|
|
|
11
|
-
<!-- Generated from README.md; @kahwee/sf-map-svg 4.
|
|
11
|
+
<!-- Generated from README.md; @kahwee/sf-map-svg 4.3.1 · local working tree. -->
|
|
12
12
|
|
|
13
13
|
# SF Map SVG
|
|
14
14
|
|
|
15
15
|
Offline, self-contained San Francisco SVG maps. The package has no runtime dependencies. Geography is always an explicit import; the root entry does not bundle data.
|
|
16
16
|
|
|
17
|
-
[Design playground](https://kahwee.github.io/sf-map-svg/playground.html) · [Live examples](https://kahwee.github.io/sf-map-svg/examples.html) · [Storybook source](https://github.com/kahwee/sf-map-svg/blob/main/stories/) · [Geographic sources](https://github.com/kahwee/sf-map-svg/blob/main/SOURCES.md) · [v3 migration](https://github.com/kahwee/sf-map-svg/blob/main/docs/migration-v3.md)
|
|
17
|
+
[Getting started](https://github.com/kahwee/sf-map-svg/blob/main/docs/developer-guide.md) · [API reference](https://github.com/kahwee/sf-map-svg/blob/main/docs/API.md) · [Design playground](https://kahwee.github.io/sf-map-svg/playground.html) · [Live examples](https://kahwee.github.io/sf-map-svg/examples.html) · [Storybook source](https://github.com/kahwee/sf-map-svg/blob/main/stories/) · [Geographic sources](https://github.com/kahwee/sf-map-svg/blob/main/SOURCES.md) · [v3 migration](https://github.com/kahwee/sf-map-svg/blob/main/docs/migration-v3.md)
|
|
18
18
|
|
|
19
19
|
## Install
|
|
20
20
|
|
|
@@ -57,11 +57,16 @@ const map = createMap(fullMapData, {
|
|
|
57
57
|
features: { motion: true },
|
|
58
58
|
appearance: { theme: 'districts' },
|
|
59
59
|
});
|
|
60
|
-
document.querySelector('#map')
|
|
60
|
+
const host = document.querySelector('#map');
|
|
61
|
+
if (!host) throw new Error('Missing #map container');
|
|
62
|
+
host.append(map.element);
|
|
61
63
|
map.on('neighborhoodchange', ({ name }) => console.log(name));
|
|
62
64
|
map.camera.zoom(1.5);
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
+
// Call when your application removes this view.
|
|
66
|
+
function disposeMap() {
|
|
67
|
+
map.destroy();
|
|
68
|
+
map.element.remove();
|
|
69
|
+
}
|
|
65
70
|
```
|
|
66
71
|
|
|
67
72
|
`createMap(data, options)` returns a controller. Use `map.element` for mounting, `map.configure({ features, layers, controls, appearance, mode, source, year, labels })` for runtime switches, `map.camera` for pan/zoom/fit/reset, and `map.on()` for typed events. Appearance can change live while preserving camera, selection, and focus. `getResolvedConfiguration()` explains effective layers; `getCapabilities()` reports supplied geography. 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.
|
|
@@ -78,20 +83,11 @@ Call `destroy()` when removing an interactive map.
|
|
|
78
83
|
|
|
79
84
|
Canonical geography is in [`data/`](https://github.com/kahwee/sf-map-svg/blob/main/data/README.md), with provenance in [`SOURCES.md`](https://github.com/kahwee/sf-map-svg/blob/main/SOURCES.md). The public `/data` entry exposes lookup, catalog, district maps, and source-specific neighborhood collections. These are deeply frozen; clone before editing.
|
|
80
85
|
|
|
81
|
-
|
|
82
|
-
pnpm install --frozen-lockfile
|
|
83
|
-
pnpm check
|
|
84
|
-
pnpm demo
|
|
85
|
-
pnpm build-storybook
|
|
86
|
-
pnpm test:stories:coverage
|
|
87
|
-
pnpm test:package
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
The [contributing guide](https://github.com/kahwee/sf-map-svg/blob/main/CONTRIBUTING.md) explains the geographic and release checks. If you are upgrading from v2, follow the [v3 migration guide](https://github.com/kahwee/sf-map-svg/blob/main/docs/migration-v3.md).
|
|
86
|
+
Install with `pnpm install --frozen-lockfile`. See the [contributing guide](https://github.com/kahwee/sf-map-svg/blob/main/CONTRIBUTING.md) for architecture and the [validation and release runbook](https://github.com/kahwee/sf-map-svg/blob/main/docs/maintenance.md) for checks, browser inspection, and publishing. If you are upgrading from v2, follow the [v3 migration guide](https://github.com/kahwee/sf-map-svg/blob/main/docs/migration-v3.md).
|
|
91
87
|
|
|
92
88
|
---
|
|
93
89
|
|
|
94
|
-
<!-- Generated from docs/developer-guide.md; @kahwee/sf-map-svg 4.
|
|
90
|
+
<!-- Generated from docs/developer-guide.md; @kahwee/sf-map-svg 4.3.1 · local working tree. -->
|
|
95
91
|
|
|
96
92
|
# Developer guide
|
|
97
93
|
|
|
@@ -175,7 +171,7 @@ function disposeMap() {
|
|
|
175
171
|
| Visible labels | On | On, subject to zoom/collisions |
|
|
176
172
|
| Motion features | Static animation off | Off; opt in via `features` |
|
|
177
173
|
|
|
178
|
-
A layer switch does not download missing geography. `fullMapData` has `.map` for static rendering and separate lookup collections for interactive use. `staticMapData` is already a static data object. Use `renderMap(guideMapData
|
|
174
|
+
A layer switch does not download missing geography. `fullMapData` has `.map` for static rendering and separate lookup collections for interactive use. `staticMapData` is already a static data object. Use `renderMap(guideMapData, options)` or `renderMap(fullMapData, { source: 'sf-find' })` to share source-aware geography with the browser controller. Passing `.map` remains supported as compact static geography; it already chooses its neighborhoods and does not accept a `source` option.
|
|
179
175
|
|
|
180
176
|
SFAR realtor neighborhoods are the default when supplied. If only an alternative collection is supplied, the controller selects that available source. Choose `source` explicitly when comparing definitions. Sources retain separate identities; they are not interchangeable polygons.
|
|
181
177
|
|
|
@@ -183,7 +179,7 @@ SFAR realtor neighborhoods are the default when supplied. If only an alternative
|
|
|
183
179
|
|
|
184
180
|
| Change | Call |
|
|
185
181
|
| --- | --- |
|
|
186
|
-
|
|
|
182
|
+
| Presentation, layers, controls, behaviors | `map.configure({ layers, controls, features, appearance, mode, source, year, labels })` |
|
|
187
183
|
| Map mode or neighborhood source | `map.setMode(mode)`, `map.setSource(source)` |
|
|
188
184
|
| District boundaries or choropleth | `map.setDistrictYear(year)`, `map.setDistrictStyle(callback)` |
|
|
189
185
|
| Master text visibility | `map.setLabels(visible)` |
|
|
@@ -191,6 +187,8 @@ SFAR realtor neighborhoods are the default when supplied. If only an alternative
|
|
|
191
187
|
| Camera | `map.camera.set/pan/zoom/fit/reset(...)` |
|
|
192
188
|
| Palette, typography, control styling | `map.configure({ appearance })`; retains camera, selection and focus |
|
|
193
189
|
|
|
190
|
+
Appearance token objects merge by key; `undefined` removes an override and `appearance: undefined` resets the group. Static maps accept grouped `layers` and the shared `appearance` keys (`theme`, `colors`, `districtStyle`) too. See [the API contract](https://github.com/kahwee/sf-map-svg/blob/main/docs/API.md#live-configuration-and-inspection) and [the design playground](https://kahwee.github.io/sf-map-svg/playground.html).
|
|
191
|
+
|
|
194
192
|
`configure` merges supplied keys; omitted keys keep their existing values. Set a layer or control key to `undefined` to remove its override. Set a whole group to `undefined` to reset that group. Set a feature to `false` to disable it. Explicit layer overrides keep winning after mode changes.
|
|
195
193
|
|
|
196
194
|
```ts
|
|
@@ -200,7 +198,7 @@ map.configure({ features: { motion: false } });
|
|
|
200
198
|
map.configure({ layers: undefined }); // clear all explicit layer overrides
|
|
201
199
|
```
|
|
202
200
|
|
|
203
|
-
`getConfiguration()` returns a detached snapshot of
|
|
201
|
+
`getConfiguration()` returns current mode/source/year/labels and a detached snapshot of the presentation groups, including explicit layer and appearance overrides. Camera and selection remain separate runtime state. Use `getResolvedConfiguration()` for current mode/source/year/labels and effective layer switches, and `getCapabilities()` for supplied geography. Invalid configuration patches leave the current map unchanged. Convenience methods use the same atomic update path: pass `map.setMode(mode, { resetView: false })` or `map.setSource(source, { resetView: false })` to keep the camera. Their default still resets the view.
|
|
204
202
|
|
|
205
203
|
## Markers, routes and camera coordinates
|
|
206
204
|
|
|
@@ -231,11 +229,9 @@ The generated `llms.txt` index links to Markdown documentation; `llms-full.txt`
|
|
|
231
229
|
|
|
232
230
|
See [API options](https://github.com/kahwee/sf-map-svg/blob/main/docs/API.md), [worked examples](https://github.com/kahwee/sf-map-svg/blob/main/docs/EXAMPLES.md), [consumer integration](https://github.com/kahwee/sf-map-svg/blob/main/docs/consumer-integration.md), and [v3 migration](https://github.com/kahwee/sf-map-svg/blob/main/docs/migration-v3.md).
|
|
233
231
|
|
|
234
|
-
Appearance token objects merge by key; `undefined` removes an override and `appearance: undefined` resets the group. Static maps accept grouped `layers` and the shared `appearance` keys (`theme`, `colors`, `districtStyle`) too. See [the API contract](https://github.com/kahwee/sf-map-svg/blob/main/docs/API.md#live-configuration-and-inspection) and [the design playground](https://kahwee.github.io/sf-map-svg/playground.html).
|
|
235
|
-
|
|
236
232
|
---
|
|
237
233
|
|
|
238
|
-
<!-- Generated from docs/API.md; @kahwee/sf-map-svg 4.
|
|
234
|
+
<!-- Generated from docs/API.md; @kahwee/sf-map-svg 4.3.1 · local working tree. -->
|
|
239
235
|
|
|
240
236
|
# Map options and lifecycle
|
|
241
237
|
|
|
@@ -245,6 +241,8 @@ and event types are exported by [src/api.ts](https://github.com/kahwee/sf-map-sv
|
|
|
245
241
|
|
|
246
242
|
## Static maps
|
|
247
243
|
|
|
244
|
+
Static maps accept compact `StaticMapData` or the same source-aware `MapData` as interactive maps. With `MapData`, `source` selects a supplied neighborhood definition; SFAR realtor is preferred by default, followed by SF Find and analysis when available. A requested unavailable source throws before styling callbacks run. Compact `StaticMapData` already chooses its neighborhoods, so it rejects a `source` option. Both data shapes retain the static renderer defaults.
|
|
245
|
+
|
|
248
246
|
Static maps accept the same grouped presentation vocabulary as interactive maps:
|
|
249
247
|
|
|
250
248
|
```ts
|
|
@@ -256,12 +254,14 @@ const { svg } = renderMap(staticMapData, presentation);
|
|
|
256
254
|
const map = createMap(fullMapData, presentation);
|
|
257
255
|
```
|
|
258
256
|
|
|
259
|
-
Static `layers` supports every interactive layer switch except `neighborhoodLabels`. Static `appearance` supports `theme`, `colors`, and `districtStyle`; browser typography, area interaction styles, and screen-space marker sizing remain interactive settings. Unknown
|
|
257
|
+
Static `layers` supports every interactive layer switch except `neighborhoodLabels`. Static `appearance` supports `theme`, `colors`, and `districtStyle`; browser typography, area interaction styles, and screen-space marker sizing remain interactive settings. Unknown keys and invalid values are rejected for both flat and grouped options. Supplied grouped keys override flat compatibility keys; omitted or `undefined` grouped keys preserve the flat value or renderer default. Undefined color tokens use the theme default instead of entering SVG attributes. The two renderers retain their existing defaults. Marker arrays accept readonly inputs.
|
|
260
258
|
|
|
261
259
|
Flat compatibility options include `theme`, `width`, `height`, `padding`, `year` (2002, 2012, 2022), `districtLines`, `districtFills`, `districtStyle`, `districtLabels`, `neighborhoodLines`, `labels`, `highways`, `keyRoads`, `roadLabels`, `landmarks`, `bartStations`, `markers`, `overlays`, `title`, `idPrefix`, `colors`, and `animation`. Each optional layer is independent. User-supplied text and attributes are escaped in SVG output.
|
|
262
260
|
|
|
263
261
|
Custom district IDs must be finite numbers. An empty `labelPoints` array uses the district's primary `label` position.
|
|
264
262
|
|
|
263
|
+
The SVG description names the selected neighborhood source for source-aware data and uses a generic description for compact inputs. It describes enabled layers only when their data is supplied, using actual park and station counts.
|
|
264
|
+
|
|
265
265
|
`animation: true` (or `{ duration, delay }`) makes a static map draw itself with self-contained, scoped CSS: land fades in, the coast and lines draw, districts grow, then labels and points appear. It plays when the SVG is inserted into a page or loaded as an image, needs no JavaScript, and stays still under `prefers-reduced-motion`. Re-insert the markup to replay it. Default output is unchanged.
|
|
266
266
|
|
|
267
267
|
## Interactive maps
|
|
@@ -286,7 +286,7 @@ District style callbacks are evaluated once per district at construction and on
|
|
|
286
286
|
|
|
287
287
|
## Live configuration and inspection
|
|
288
288
|
|
|
289
|
-
`map.configure({ appearance, mode, source, year, labels, layers, features, controls })` updates an existing map. Appearance updates preserve the camera, selections, retained marker nodes, and keyboard focus. Mode/source changes through `configure` preserve the camera;
|
|
289
|
+
`map.configure({ appearance, mode, source, year, labels, layers, features, controls })` updates an existing map. Appearance updates preserve the camera, selections, retained marker nodes, and keyboard focus. Mode/source changes through `configure` preserve the camera; the `setMode(mode, { resetView: false })` and `setSource(source, { resetView: false })` convenience methods use the same atomic update path and preserve the camera. Omit the second argument to retain their existing reset behavior. Invalid view-update options fail before changing configuration. Switching neighborhood source clears the neighborhood selection. Changing district year retains a selected district when it exists in the new year. Unsupported datasets, invalid appearance values, and invalid district-style callback results fail during preparation before commit. A reentrant styling callback can supersede the pending patch.
|
|
290
290
|
|
|
291
291
|
Appearance token objects (`colors`, `style`, `labelStyle`, `labelSize`, `areaStyle`) merge supplied keys; other appearance properties replace. Omitted keys retain their value, an explicit `undefined` property removes that override, and `appearance: undefined` resets the whole appearance group. Feature objects retain their existing replacement semantics. Mode/source/year/labels ignore `undefined`; reset them with an explicit value. Patch types explicitly permit these resets with TypeScript’s `exactOptionalPropertyTypes` enabled.
|
|
292
292
|
|
|
@@ -302,7 +302,7 @@ const effective = map.getResolvedConfiguration();
|
|
|
302
302
|
const available = map.getCapabilities();
|
|
303
303
|
```
|
|
304
304
|
|
|
305
|
-
`getConfiguration()` returns detached feature, layer, control, and appearance overrides; district style callbacks retain their function identity. `getResolvedConfiguration()` adds the current mode, source, year and master labels and resolves **layer switches** through mode defaults, available data, and the master label switch. Appearance/control fields remain overrides. It does not claim that every label is visible: zoom and collision filtering still apply. `getCapabilities()` returns supplied `sources`, usable district `years`, and layer data availability for the current source/year, independent of visibility overrides. These reads throw after destruction.
|
|
305
|
+
`getConfiguration()` returns the current mode, source, year and master labels, together with detached feature, layer, control, and appearance overrides; district style callbacks retain their function identity. `getResolvedConfiguration()` adds the current mode, source, year and master labels and resolves **layer switches** through mode defaults, available data, and the master label switch. Appearance/control fields remain overrides. It does not claim that every label is visible: zoom and collision filtering still apply. `getCapabilities()` returns supplied `sources`, usable district `years`, and layer data availability for the current source/year, independent of visibility overrides. These reads throw after destruction.
|
|
306
306
|
|
|
307
307
|
## Common selection event
|
|
308
308
|
|
|
@@ -317,6 +317,18 @@ map.on('selectionchange', (event) => {
|
|
|
317
317
|
|
|
318
318
|
`kind` is `marker`, `neighborhood`, or `district`. `current` and `previous` are detached selection snapshots or `null`. The event follows committed selection changes, including clearing and a changed district vintage; it does not represent every activation. Existing `markerchange`, `neighborhoodchange`, and `districtchange` events remain supported.
|
|
319
319
|
|
|
320
|
+
## Source-aware feature selection
|
|
321
|
+
|
|
322
|
+
`selectFeature(reference, options)` accepts one typed identity for each selectable kind:
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
map.selectFeature({ kind: 'marker', id: 'ferry' }, { fit: false });
|
|
326
|
+
map.selectFeature({ kind: 'neighborhood', source: 'realtor', id: 'inner-mission' });
|
|
327
|
+
map.selectFeature({ kind: 'district', year: 2022, id: 3 });
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Neighborhood IDs are exact source-scoped feature IDs, rather than display names or aliases. District identity includes the boundary year. This operation returns `false` for a missing feature or a source/year other than the map's current supplied geography; it never silently switches definitions or years. Choose geography through `configure` first. An `id` of `null` clears that kind in the referenced current geography. Malformed references throw. Existing `selectMarker`, `selectNeighborhood`, and `selectDistrict` shortcuts remain supported.
|
|
331
|
+
|
|
320
332
|
## Coordinates and units
|
|
321
333
|
|
|
322
334
|
GeoJSON and static projection inputs use `[longitude, latitude]` in WGS84; markers use `{ lng, lat }`. Static `project` returns SVG units. `projectToScreen(lng, lat)` returns pixels relative to the mounted canvas. Camera viewports and pan deltas use the fixed 800 × 800 projected map space, independent of canvas size. Marker radius uses screen pixels in browser maps and SVG units in static output.
|
|
@@ -325,7 +337,7 @@ Explore presentation options in the [live map design playground](https://kahwee.
|
|
|
325
337
|
|
|
326
338
|
---
|
|
327
339
|
|
|
328
|
-
<!-- Generated from docs/EXAMPLES.md; @kahwee/sf-map-svg 4.
|
|
340
|
+
<!-- Generated from docs/EXAMPLES.md; @kahwee/sf-map-svg 4.3.1 · local working tree. -->
|
|
329
341
|
|
|
330
342
|
# Examples
|
|
331
343
|
|
|
@@ -436,35 +448,59 @@ For the controller and explicit data imports, see [the v3 migration guide](https
|
|
|
436
448
|
|
|
437
449
|
---
|
|
438
450
|
|
|
439
|
-
<!-- Generated from docs/consumer-integration.md; @kahwee/sf-map-svg 4.
|
|
451
|
+
<!-- Generated from docs/consumer-integration.md; @kahwee/sf-map-svg 4.3.1 · local working tree. -->
|
|
452
|
+
|
|
453
|
+
# Embedding and progressive enhancement
|
|
440
454
|
|
|
441
|
-
|
|
455
|
+
For basic mounting, events and cleanup, start with the [developer guide](https://github.com/kahwee/sf-map-svg/blob/main/docs/developer-guide.md). The root controller takes explicit geography; the guide entry offers a smaller preset with the same grouped `configure()`, `camera`, `on()`, and `destroy()` contract. See the [migration guide](https://github.com/kahwee/sf-map-svg/blob/main/docs/migration-v3.md) for removed v2 imports.
|
|
442
456
|
|
|
443
|
-
|
|
457
|
+
## Render a guide before JavaScript loads
|
|
444
458
|
|
|
445
|
-
|
|
459
|
+
On the server or in a build step, render a compact frame:
|
|
446
460
|
|
|
447
461
|
```ts
|
|
448
|
-
import {
|
|
462
|
+
import { createGuideShell } from '@kahwee/sf-map-svg/guide/static';
|
|
463
|
+
|
|
464
|
+
const markup = createGuideShell({ title: 'Explore San Francisco' });
|
|
465
|
+
// Insert markup into your server-rendered HTML.
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
The shell uses an 800 × 800 projection with 28 units of padding, matching the interactive map. Use `createGuideSVG()` for custom dimensions. Rendering is offline, with zero runtime dependencies.
|
|
469
|
+
|
|
470
|
+
## Enhance the existing frame
|
|
471
|
+
|
|
472
|
+
In your browser entry, mount the controller into that shell:
|
|
473
|
+
|
|
474
|
+
```ts
|
|
475
|
+
import { mountGuideController } from '@kahwee/sf-map-svg/guide';
|
|
449
476
|
|
|
450
|
-
const
|
|
477
|
+
const shell = document.querySelector<HTMLElement>('.sf-guide-shell');
|
|
478
|
+
if (!shell) throw new Error('Missing guide shell');
|
|
479
|
+
const guide = mountGuideController(shell, {
|
|
480
|
+
attribution: 'compact',
|
|
451
481
|
features: { motion: { duration: 400 }, markerEntrance: true },
|
|
452
482
|
appearance: { theme: 'transit' },
|
|
453
483
|
});
|
|
454
|
-
|
|
455
|
-
//
|
|
456
|
-
|
|
484
|
+
|
|
485
|
+
// Call before your application removes the containing view.
|
|
486
|
+
function disposeGuide() {
|
|
487
|
+
guide.destroy();
|
|
488
|
+
}
|
|
457
489
|
```
|
|
458
490
|
|
|
459
|
-
|
|
491
|
+
Compact attribution is required for an existing shell. Invalid configuration leaves its static frame intact. `destroy()` disposes listeners and animation work; your application owns removal of the containing view. Motion is opt-in and follows reduced-motion preferences.
|
|
492
|
+
|
|
493
|
+
For a new browser-only guide, use `createGuideController()` and mount its `.element` as shown in the developer guide. Existing `createGuideMap()` and `mountGuideMap()` retain their augmented-element return types and flat options for compatibility.
|
|
460
494
|
|
|
461
|
-
|
|
495
|
+
## Choose the data boundary
|
|
462
496
|
|
|
463
|
-
|
|
497
|
+
`createMap(guideMapData, guideOptions)` remains available through `/guide/data` and `/presets`. For server rendering, pass `guideMapData.map` or `/data/static` to `renderMap()`. Use `/data/full` when interactive lookup collections are needed. The guide's detailed geography loads only on an explicit `loadGuideDetailedData()` call; enabling a layer does not download it.
|
|
498
|
+
|
|
499
|
+
See the [exact API contract](https://github.com/kahwee/sf-map-svg/blob/main/docs/API.md), [worked examples](https://github.com/kahwee/sf-map-svg/blob/main/docs/EXAMPLES.md), and [bundle measurements](https://github.com/kahwee/sf-map-svg/blob/main/docs/guide-bundle-report.md).
|
|
464
500
|
|
|
465
501
|
---
|
|
466
502
|
|
|
467
|
-
<!-- Generated from docs/migration-v3.md; @kahwee/sf-map-svg 4.
|
|
503
|
+
<!-- Generated from docs/migration-v3.md; @kahwee/sf-map-svg 4.3.1 · local working tree. -->
|
|
468
504
|
|
|
469
505
|
# Migrate to version 3
|
|
470
506
|
|
|
@@ -505,7 +541,7 @@ District styles are now prepared once per district on construction and style/yea
|
|
|
505
541
|
|
|
506
542
|
---
|
|
507
543
|
|
|
508
|
-
<!-- Generated from SOURCES.md; @kahwee/sf-map-svg 4.
|
|
544
|
+
<!-- Generated from SOURCES.md; @kahwee/sf-map-svg 4.3.1 · local working tree. -->
|
|
509
545
|
|
|
510
546
|
# Geometry and provenance
|
|
511
547
|
|
|
@@ -689,7 +725,7 @@ display geometry, and no precinct polygons or raw workbook data are shipped.
|
|
|
689
725
|
|
|
690
726
|
---
|
|
691
727
|
|
|
692
|
-
<!-- Generated from CHANGELOG.md; @kahwee/sf-map-svg 4.
|
|
728
|
+
<!-- Generated from CHANGELOG.md; @kahwee/sf-map-svg 4.3.1 · local working tree. -->
|
|
693
729
|
|
|
694
730
|
# Changelog
|
|
695
731
|
|
|
@@ -697,6 +733,21 @@ User-visible changes are recorded here. Unreleased entries describe changes on `
|
|
|
697
733
|
|
|
698
734
|
## Unreleased
|
|
699
735
|
|
|
736
|
+
## 4.3.1 — 2026-10-04
|
|
737
|
+
|
|
738
|
+
- Reject unknown flat static options and invalid values before rendering or invoking district styling callbacks, matching grouped-option validation.
|
|
739
|
+
- Ignore undefined static color overrides so SVG attributes retain the selected theme's defaults; preserve grouped-option precedence.
|
|
740
|
+
- Describe the actual neighborhood source, enabled and supplied layers, and park and BART station counts in static SVG descriptions. Compact geography uses a source-neutral description.
|
|
741
|
+
|
|
742
|
+
## 4.3.0 — 2026-10-02
|
|
743
|
+
|
|
744
|
+
- Add `selectFeature` with typed marker, neighborhood source/ID and district year/ID references. Missing or mismatched identities preserve current geography and selection.
|
|
745
|
+
- Accept the same source-aware map data in static and interactive rendering. Static maps can explicitly select a supplied neighborhood definition without manually rebuilding renderer rows; compact static inputs remain supported.
|
|
746
|
+
- Include current mode, source, year and labels in configuration snapshots. Route controller mode/source/label setters through atomic configuration and support explicit camera preservation with `{ resetView: false }`.
|
|
747
|
+
- Refine the Pages atlas opening with concise copy, a map ahead of statistics on phones, coordinate captions and three direct paths into designing, exploring and building. Improve shared theme-button touch targets and stack API reference records on phones while retaining table semantics.
|
|
748
|
+
|
|
749
|
+
- Clarify the documentation paths, progressive enhancement and cleanup examples; consolidate browser validation and release instructions into one maintenance runbook. Check README and integration recipes against the packed package.
|
|
750
|
+
|
|
700
751
|
## 4.2.0 — 2026-10-01
|
|
701
752
|
|
|
702
753
|
- Reuse one native dropdown treatment across Pages, with an inset chevron, a reserved end gutter, and preserved labels, keyboard behavior, and mobile pickers.
|
|
@@ -1000,7 +1051,7 @@ consistent home, and makes animation and subscription ownership predictable.
|
|
|
1000
1051
|
|
|
1001
1052
|
# Published type contracts
|
|
1002
1053
|
|
|
1003
|
-
@kahwee/sf-map-svg 4.
|
|
1054
|
+
@kahwee/sf-map-svg 4.3.1 · local working tree. These declarations are generated by TypeScript. Use package export paths, not the internal filenames below. Imported relative contracts are included for context, not as additional public entry points.
|
|
1004
1055
|
|
|
1005
1056
|
| Public import | Declaration |
|
|
1006
1057
|
| --- | --- |
|
|
@@ -1079,7 +1130,12 @@ export interface MapOptions extends Omit<NeighborhoodExplorerOptions, keyof MapF
|
|
|
1079
1130
|
features?: MapFeatures;
|
|
1080
1131
|
appearance?: MapAppearance | undefined;
|
|
1081
1132
|
}
|
|
1133
|
+
/** Current geographic choices and detached presentation overrides; excludes camera and selection. */
|
|
1082
1134
|
export interface MapConfigurationSnapshot {
|
|
1135
|
+
mode: NonNullable<MapOptions['mode']>;
|
|
1136
|
+
source: NeighborhoodSource;
|
|
1137
|
+
year: DistrictYear;
|
|
1138
|
+
labels: boolean;
|
|
1083
1139
|
features: MapFeatures;
|
|
1084
1140
|
/** Explicit overrides; absent values continue to follow the current mode. */
|
|
1085
1141
|
layers: InteractiveLayers;
|
|
@@ -1113,6 +1169,19 @@ export type MapSelectionChange = {
|
|
|
1113
1169
|
current: DistrictSelection | null;
|
|
1114
1170
|
previous: DistrictSelection | null;
|
|
1115
1171
|
};
|
|
1172
|
+
/** Dataset identity is explicit; selection never switches geography implicitly. */
|
|
1173
|
+
export type MapFeatureReference = {
|
|
1174
|
+
kind: 'marker';
|
|
1175
|
+
id: string | null;
|
|
1176
|
+
} | {
|
|
1177
|
+
kind: 'neighborhood';
|
|
1178
|
+
source: NeighborhoodSource;
|
|
1179
|
+
id: string | null;
|
|
1180
|
+
} | {
|
|
1181
|
+
kind: 'district';
|
|
1182
|
+
year: DistrictYear;
|
|
1183
|
+
id: number | null;
|
|
1184
|
+
};
|
|
1116
1185
|
export interface MapEvents {
|
|
1117
1186
|
/** Common selection envelope; existing change events retain their payloads. */
|
|
1118
1187
|
selectionchange: MapSelectionChange;
|
|
@@ -1152,6 +1221,10 @@ export interface MapEvents {
|
|
|
1152
1221
|
};
|
|
1153
1222
|
mapresize: undefined;
|
|
1154
1223
|
}
|
|
1224
|
+
/** Convenience setters retain their historic reset default; false preserves the camera. */
|
|
1225
|
+
export interface MapViewUpdateOptions {
|
|
1226
|
+
resetView?: boolean;
|
|
1227
|
+
}
|
|
1155
1228
|
export interface MapCamera {
|
|
1156
1229
|
/** Fixed 800 × 800 projected space, not longitude/latitude. */
|
|
1157
1230
|
get(): MapViewport;
|
|
@@ -1179,6 +1252,10 @@ export interface MapController {
|
|
|
1179
1252
|
/** Reconcile stable IDs; retained markers preserve nodes, focus, and entrance animations. */
|
|
1180
1253
|
setMarkers(markers: readonly MapMarker[]): void;
|
|
1181
1254
|
setOverlays(overlays: readonly MapOverlay[]): void;
|
|
1255
|
+
/** False for missing IDs or a source/year other than the current geography. */
|
|
1256
|
+
selectFeature(reference: MapFeatureReference, options?: CameraOptions & {
|
|
1257
|
+
fit?: boolean;
|
|
1258
|
+
}): boolean;
|
|
1182
1259
|
selectMarker(id: string | null, options?: CameraOptions & {
|
|
1183
1260
|
fit?: boolean;
|
|
1184
1261
|
}): boolean;
|
|
@@ -1193,8 +1270,8 @@ export interface MapController {
|
|
|
1193
1270
|
getSelectedDistrict(): DistrictSelection | null;
|
|
1194
1271
|
setDistrictYear(year: DistrictYear, options?: CameraOptions): void;
|
|
1195
1272
|
setDistrictStyle(style: ((district: DistrictSelection['district']) => DistrictStyle) | undefined): void;
|
|
1196
|
-
setSource(source: NeighborhoodSource): void;
|
|
1197
|
-
setMode(mode: NonNullable<MapOptions['mode']
|
|
1273
|
+
setSource(source: NeighborhoodSource, options?: MapViewUpdateOptions): void;
|
|
1274
|
+
setMode(mode: NonNullable<MapOptions['mode']>, options?: MapViewUpdateOptions): void;
|
|
1198
1275
|
setLabels(visible: boolean): void;
|
|
1199
1276
|
setTouchNavigation(enabled: boolean): void;
|
|
1200
1277
|
projectToScreen(lng: number, lat: number): {
|
|
@@ -1387,6 +1464,8 @@ export interface SFMapOptions {
|
|
|
1387
1464
|
height?: number;
|
|
1388
1465
|
padding?: number;
|
|
1389
1466
|
year?: DistrictYear;
|
|
1467
|
+
/** Select a supplied neighborhood definition when rendering source-aware MapData. */
|
|
1468
|
+
source?: NeighborhoodSource;
|
|
1390
1469
|
districtLines?: boolean;
|
|
1391
1470
|
neighborhoodLines?: boolean;
|
|
1392
1471
|
districtFills?: boolean;
|
|
@@ -1597,7 +1676,7 @@ export interface NeighborhoodExplorerElement extends HTMLElement {
|
|
|
1597
1676
|
export interface TransitAnimationElement extends HTMLElement {
|
|
1598
1677
|
destroy(): void;
|
|
1599
1678
|
}
|
|
1600
|
-
export type { MapAppearance, MapCamera, MapCapabilities, MapConfiguration, MapConfigurationSnapshot, MapController, MapControls, MapEvents, MapOptions, MapSelectionChange, ResolvedMapConfiguration, } from './controller-types.js';
|
|
1679
|
+
export type { MapAppearance, MapCamera, MapCapabilities, MapConfiguration, MapConfigurationSnapshot, MapController, MapControls, MapEvents, MapFeatureReference, MapOptions, MapSelectionChange, MapViewUpdateOptions, ResolvedMapConfiguration, } from './controller-types.js';
|
|
1601
1680
|
```
|
|
1602
1681
|
|
|
1603
1682
|
### dist/src/explorer-data.d.ts
|
|
@@ -1724,18 +1803,21 @@ export declare function createSFMapWithData(options: SFMapOptions, data: SFMapDa
|
|
|
1724
1803
|
### dist/src/static.d.ts
|
|
1725
1804
|
|
|
1726
1805
|
```ts
|
|
1806
|
+
import type { InteractiveSFMapData } from './explorer-data.js';
|
|
1727
1807
|
import { type SFMapData } from './map-core.js';
|
|
1728
1808
|
import type { SFMapOptions } from './types.js';
|
|
1809
|
+
/** Either compact static geography or the same source-aware geography used by createMap. */
|
|
1810
|
+
export type StaticMapInput = SFMapData | InteractiveSFMapData;
|
|
1729
1811
|
export type { DistrictRowData, SFMapData as StaticMapData } from './map-core.js';
|
|
1730
1812
|
export type { DistrictStyle, DistrictYear, SFMapOptions as StaticMapOptions, StaticMapAppearance, StaticMapLayers, } from './types.js';
|
|
1731
1813
|
/** Server-safe SVG rendering with explicit data; returns SVG plus projection helpers. */
|
|
1732
|
-
export declare function renderMap(data:
|
|
1814
|
+
export declare function renderMap(data: StaticMapInput, options?: SFMapOptions): {
|
|
1733
1815
|
svg: string;
|
|
1734
1816
|
project: (coordinates: import("../data/types.js").Position) => [number, number];
|
|
1735
1817
|
viewBox: [number, number, number, number];
|
|
1736
1818
|
};
|
|
1737
1819
|
/** Project canonical layer geometry without constructing an SVG string. */
|
|
1738
|
-
export declare function getLayerPaths(data:
|
|
1820
|
+
export declare function getLayerPaths(data: StaticMapInput, options?: SFMapOptions): {
|
|
1739
1821
|
year: import("./types.js").DistrictYear;
|
|
1740
1822
|
viewBox: [number, number, number, number];
|
|
1741
1823
|
project: (coordinates: import("../data/types.js").Position) => [number, number];
|
package/llms.txt
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> Offline San Francisco SVG maps with explicit geographic imports, no runtime dependencies, static rendering, and a browser controller.
|
|
4
4
|
|
|
5
|
-
@kahwee/sf-map-svg 4.
|
|
5
|
+
@kahwee/sf-map-svg 4.3.1 · local working tree. Node >=24.0.0 for server rendering; browsers need a DOM and a JSON-capable bundler.
|
|
6
6
|
|
|
7
7
|
These are working-tree docs. Unreleased changelog entries may describe APIs absent from npm; verify the installed version before copying them.
|
|
8
8
|
|
package/package.json
CHANGED