@kahwee/sf-map-svg 4.3.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 +6 -0
- package/dist/src/map-core.js +24 -2
- package/dist/src/static.js +59 -23
- package/docs/API.md +3 -1
- package/llms-full.txt +19 -11
- package/llms.txt +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,12 @@ 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
|
+
|
|
7
13
|
## 4.3.0 — 2026-10-02
|
|
8
14
|
|
|
9
15
|
- Add `selectFeature` with typed marker, neighborhood source/ID and district year/ID references. Missing or mismatched identities preserve current geography and selection.
|
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/static.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
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);
|
|
8
|
-
|
|
9
|
+
const { map, source } = staticInput(data, prepared);
|
|
10
|
+
return createSFMapWithData({ ...prepared, source }, map);
|
|
9
11
|
}
|
|
10
12
|
/** Project canonical layer geometry without constructing an SVG string. */
|
|
11
13
|
export function getLayerPaths(data, options = {}) {
|
|
12
14
|
const prepared = expandStaticOptions(options);
|
|
13
|
-
return getLayerPathsWithData(prepared, staticInput(data, prepared));
|
|
15
|
+
return getLayerPathsWithData(prepared, staticInput(data, prepared).map);
|
|
14
16
|
}
|
|
15
17
|
/** Borrow immutable geometry, deriving only the renderer's lightweight neighborhood rows. */
|
|
16
18
|
function staticInput(data, options) {
|
|
@@ -19,45 +21,79 @@ function staticInput(data, options) {
|
|
|
19
21
|
if (!('map' in data)) {
|
|
20
22
|
if (options.source !== undefined)
|
|
21
23
|
throw new TypeError('source requires source-aware MapData; compact StaticMapData already chooses its geography.');
|
|
22
|
-
return data;
|
|
24
|
+
return { map: data, source: undefined };
|
|
23
25
|
}
|
|
24
26
|
const source = options.source ??
|
|
25
27
|
['realtor', 'sf-find', 'analysis'].find((candidate) => !!data.neighborhoods[candidate]);
|
|
26
28
|
if (source === undefined)
|
|
27
|
-
return data.map;
|
|
29
|
+
return { map: data.map, source: undefined };
|
|
28
30
|
const collection = data.neighborhoods[source];
|
|
29
31
|
if (!collection)
|
|
30
32
|
throw new RangeError(`No ${source} neighborhood dataset was supplied.`);
|
|
31
33
|
return {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
geometry,
|
|
36
|
-
|
|
34
|
+
source,
|
|
35
|
+
map: {
|
|
36
|
+
...data.map,
|
|
37
|
+
neighborhoods: collection.features.map(({ geometry, properties }) => ({
|
|
38
|
+
name: properties.sourceName,
|
|
39
|
+
geometry,
|
|
40
|
+
})),
|
|
41
|
+
},
|
|
37
42
|
};
|
|
38
43
|
}
|
|
44
|
+
const staticLayerKeys = [
|
|
45
|
+
'districtFills',
|
|
46
|
+
'districtLines',
|
|
47
|
+
'districtLabels',
|
|
48
|
+
'neighborhoodLines',
|
|
49
|
+
'landmarks',
|
|
50
|
+
'bartStations',
|
|
51
|
+
'highways',
|
|
52
|
+
'keyRoads',
|
|
53
|
+
'roadLabels',
|
|
54
|
+
];
|
|
39
55
|
/** Grouped keys override flat compatibility keys; omitted keys preserve renderer defaults. */
|
|
40
56
|
function expandStaticOptions(options) {
|
|
41
|
-
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);
|
|
42
87
|
if (options.layers !== undefined)
|
|
43
|
-
validateSwitchPatch(options.layers,
|
|
44
|
-
'districtFills',
|
|
45
|
-
'districtLines',
|
|
46
|
-
'districtLabels',
|
|
47
|
-
'neighborhoodLines',
|
|
48
|
-
'landmarks',
|
|
49
|
-
'bartStations',
|
|
50
|
-
'highways',
|
|
51
|
-
'keyRoads',
|
|
52
|
-
'roadLabels',
|
|
53
|
-
]);
|
|
88
|
+
validateSwitchPatch(options.layers, staticLayerKeys);
|
|
54
89
|
if (options.appearance !== undefined) {
|
|
55
90
|
assertOptions(options.appearance, 'static appearance', ['theme', 'colors', 'districtStyle']);
|
|
56
91
|
}
|
|
57
|
-
const { layers, appearance, ...flat } = options;
|
|
92
|
+
const { layers, appearance, theme, colors, districtStyle, ...flat } = options;
|
|
58
93
|
const supplied = (group = {}) => Object.fromEntries(Object.entries(group).filter(([, value]) => value !== undefined));
|
|
59
94
|
return {
|
|
60
95
|
...flat,
|
|
96
|
+
...prepareAppearance({}, { theme, colors, districtStyle }),
|
|
61
97
|
...supplied(layers),
|
|
62
98
|
...supplied(appearance === undefined ? undefined : prepareAppearance({}, appearance)),
|
|
63
99
|
};
|
package/docs/API.md
CHANGED
|
@@ -19,12 +19,14 @@ const { svg } = renderMap(staticMapData, presentation);
|
|
|
19
19
|
const map = createMap(fullMapData, presentation);
|
|
20
20
|
```
|
|
21
21
|
|
|
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
|
|
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.
|
|
23
23
|
|
|
24
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.
|
|
25
25
|
|
|
26
26
|
Custom district IDs must be finite numbers. An empty `labelPoints` array uses the district's primary `label` position.
|
|
27
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
|
+
|
|
28
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.
|
|
29
31
|
|
|
30
32
|
## Interactive maps
|
package/llms-full.txt
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
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.3.
|
|
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.3.
|
|
11
|
+
<!-- Generated from README.md; @kahwee/sf-map-svg 4.3.1 · local working tree. -->
|
|
12
12
|
|
|
13
13
|
# SF Map SVG
|
|
14
14
|
|
|
@@ -87,7 +87,7 @@ Install with `pnpm install --frozen-lockfile`. See the [contributing guide](http
|
|
|
87
87
|
|
|
88
88
|
---
|
|
89
89
|
|
|
90
|
-
<!-- Generated from docs/developer-guide.md; @kahwee/sf-map-svg 4.3.
|
|
90
|
+
<!-- Generated from docs/developer-guide.md; @kahwee/sf-map-svg 4.3.1 · local working tree. -->
|
|
91
91
|
|
|
92
92
|
# Developer guide
|
|
93
93
|
|
|
@@ -231,7 +231,7 @@ See [API options](https://github.com/kahwee/sf-map-svg/blob/main/docs/API.md), [
|
|
|
231
231
|
|
|
232
232
|
---
|
|
233
233
|
|
|
234
|
-
<!-- Generated from docs/API.md; @kahwee/sf-map-svg 4.3.
|
|
234
|
+
<!-- Generated from docs/API.md; @kahwee/sf-map-svg 4.3.1 · local working tree. -->
|
|
235
235
|
|
|
236
236
|
# Map options and lifecycle
|
|
237
237
|
|
|
@@ -254,12 +254,14 @@ const { svg } = renderMap(staticMapData, presentation);
|
|
|
254
254
|
const map = createMap(fullMapData, presentation);
|
|
255
255
|
```
|
|
256
256
|
|
|
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
|
|
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.
|
|
258
258
|
|
|
259
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.
|
|
260
260
|
|
|
261
261
|
Custom district IDs must be finite numbers. An empty `labelPoints` array uses the district's primary `label` position.
|
|
262
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
|
+
|
|
263
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.
|
|
264
266
|
|
|
265
267
|
## Interactive maps
|
|
@@ -335,7 +337,7 @@ Explore presentation options in the [live map design playground](https://kahwee.
|
|
|
335
337
|
|
|
336
338
|
---
|
|
337
339
|
|
|
338
|
-
<!-- Generated from docs/EXAMPLES.md; @kahwee/sf-map-svg 4.3.
|
|
340
|
+
<!-- Generated from docs/EXAMPLES.md; @kahwee/sf-map-svg 4.3.1 · local working tree. -->
|
|
339
341
|
|
|
340
342
|
# Examples
|
|
341
343
|
|
|
@@ -446,7 +448,7 @@ For the controller and explicit data imports, see [the v3 migration guide](https
|
|
|
446
448
|
|
|
447
449
|
---
|
|
448
450
|
|
|
449
|
-
<!-- Generated from docs/consumer-integration.md; @kahwee/sf-map-svg 4.3.
|
|
451
|
+
<!-- Generated from docs/consumer-integration.md; @kahwee/sf-map-svg 4.3.1 · local working tree. -->
|
|
450
452
|
|
|
451
453
|
# Embedding and progressive enhancement
|
|
452
454
|
|
|
@@ -498,7 +500,7 @@ See the [exact API contract](https://github.com/kahwee/sf-map-svg/blob/main/docs
|
|
|
498
500
|
|
|
499
501
|
---
|
|
500
502
|
|
|
501
|
-
<!-- Generated from docs/migration-v3.md; @kahwee/sf-map-svg 4.3.
|
|
503
|
+
<!-- Generated from docs/migration-v3.md; @kahwee/sf-map-svg 4.3.1 · local working tree. -->
|
|
502
504
|
|
|
503
505
|
# Migrate to version 3
|
|
504
506
|
|
|
@@ -539,7 +541,7 @@ District styles are now prepared once per district on construction and style/yea
|
|
|
539
541
|
|
|
540
542
|
---
|
|
541
543
|
|
|
542
|
-
<!-- Generated from SOURCES.md; @kahwee/sf-map-svg 4.3.
|
|
544
|
+
<!-- Generated from SOURCES.md; @kahwee/sf-map-svg 4.3.1 · local working tree. -->
|
|
543
545
|
|
|
544
546
|
# Geometry and provenance
|
|
545
547
|
|
|
@@ -723,7 +725,7 @@ display geometry, and no precinct polygons or raw workbook data are shipped.
|
|
|
723
725
|
|
|
724
726
|
---
|
|
725
727
|
|
|
726
|
-
<!-- Generated from CHANGELOG.md; @kahwee/sf-map-svg 4.3.
|
|
728
|
+
<!-- Generated from CHANGELOG.md; @kahwee/sf-map-svg 4.3.1 · local working tree. -->
|
|
727
729
|
|
|
728
730
|
# Changelog
|
|
729
731
|
|
|
@@ -731,6 +733,12 @@ User-visible changes are recorded here. Unreleased entries describe changes on `
|
|
|
731
733
|
|
|
732
734
|
## Unreleased
|
|
733
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
|
+
|
|
734
742
|
## 4.3.0 — 2026-10-02
|
|
735
743
|
|
|
736
744
|
- Add `selectFeature` with typed marker, neighborhood source/ID and district year/ID references. Missing or mismatched identities preserve current geography and selection.
|
|
@@ -1043,7 +1051,7 @@ consistent home, and makes animation and subscription ownership predictable.
|
|
|
1043
1051
|
|
|
1044
1052
|
# Published type contracts
|
|
1045
1053
|
|
|
1046
|
-
@kahwee/sf-map-svg 4.3.
|
|
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.
|
|
1047
1055
|
|
|
1048
1056
|
| Public import | Declaration |
|
|
1049
1057
|
| --- | --- |
|
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.3.
|
|
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