@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 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.
@@ -105,7 +105,8 @@ const transitColors = {
105
105
  selected: '#0073ae',
106
106
  };
107
107
  export function resolveMapColors(theme, colors = {}) {
108
- return { ...defaults, ...(theme === 'transit' ? transitColors : {}), ...colors };
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>San Francisco supervisorial district boundaries (${year}).${neighborhoodLines ? ' Dashed lines show SFAR realtor neighborhood areas, defined in August 2010.' : ''}${landmarks ? ' Highlighted areas show six parks and landmarks.' : ''}${keyRoads ? ' Thin gray lines show selected road corridors.' : ''}${bartStations ? ' Rings mark the eight San Francisco BART stations.' : ''} Geometry from DataSF${bartStations ? ' and BART' : ''}. See package SOURCES.md.</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"/>`,
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)
@@ -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 { assertOptions } from './validation.js';
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
- return createSFMapWithData(prepared, staticInput(data, prepared));
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
- ...data.map,
33
- neighborhoods: collection.features.map(({ geometry, properties }) => ({
34
- name: properties.sourceName,
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 grouped keys are rejected. Supplied grouped keys override flat compatibility keys; omitted or `undefined` grouped keys preserve the flat value or renderer default. The two renderers retain their existing defaults. Marker arrays accept readonly inputs.
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.0 · local working tree. Node >=24.0.0 for server rendering; browsers need a DOM and a JSON-capable bundler.
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.0 · local working tree. -->
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.0 · local working tree. -->
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.0 · local working tree. -->
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 grouped keys are rejected. Supplied grouped keys override flat compatibility keys; omitted or `undefined` grouped keys preserve the flat value or renderer default. The two renderers retain their existing defaults. Marker arrays accept readonly inputs.
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.0 · local working tree. -->
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.0 · local working tree. -->
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.0 · local working tree. -->
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.0 · local working tree. -->
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.0 · local working tree. -->
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.0 · 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.
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.0 · local working tree. Node >=24.0.0 for server rendering; browsers need a DOM and a JSON-capable bundler.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kahwee/sf-map-svg",
3
- "version": "4.3.0",
3
+ "version": "4.3.1",
4
4
  "description": "Offline SVG maps of San Francisco with district boundaries, parks, landmarks, and BART stations.",
5
5
  "type": "module",
6
6
  "main": "./dist/src/api.js",