@kahwee/sf-map-svg 2.1.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +38 -569
- package/dist/data/README.md +3 -2
- package/dist/src/configuration.js +1 -0
- package/dist/src/controller-types.d.ts +23 -2
- package/dist/src/explorer-core.js +230 -4
- package/dist/src/full-data.d.ts +3 -0
- package/dist/src/full-data.js +9 -0
- package/dist/src/guide-map.d.ts +1 -1
- package/dist/src/guide-map.js +4 -3
- package/dist/src/guide.d.ts +1 -2
- package/dist/src/layers.d.ts +2 -0
- package/dist/src/layers.js +2 -2
- package/dist/src/map-core.d.ts +33 -0
- package/dist/src/map-core.js +90 -28
- package/dist/src/map.d.ts +2 -2
- package/dist/src/map.js +13 -3
- package/dist/src/static-data.d.ts +3 -0
- package/dist/src/static-data.js +23 -0
- package/dist/src/static.d.ts +35 -2
- package/dist/src/static.js +5 -1
- package/dist/src/types.d.ts +19 -0
- package/dist/src/validation.js +3 -0
- package/docs/EXAMPLES.md +18 -9
- package/docs/api-audit.md +5 -14
- package/docs/consumer-integration.md +12 -159
- package/docs/guide-bundle-report.md +5 -7
- package/docs/migration-v3.md +30 -0
- package/package.json +13 -23
- package/dist/src/custom-map.d.ts +0 -3
- package/dist/src/custom-map.js +0 -1
- package/dist/src/explorer.d.ts +0 -4
- package/dist/src/explorer.js +0 -30
- package/dist/src/index.d.ts +0 -12
- package/dist/src/index.js +0 -21
- package/dist/src/interactive-data.d.ts +0 -6
- package/dist/src/interactive-data.js +0 -7
- package/dist/src/interactive.d.ts +0 -4
- package/dist/src/interactive.js +0 -7
- package/docs/migration-v2.md +0 -238
package/dist/src/map.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { MapController, MapOptions } from './controller-types.js';
|
|
2
|
-
import {
|
|
2
|
+
import type { InteractiveSFMapData } from './explorer-data.js';
|
|
3
3
|
export type * from './controller-types.js';
|
|
4
4
|
export type { InteractiveSFMapData as MapData } from './explorer-data.js';
|
|
5
|
-
export type { CameraOptions, InteractiveLayers, MapFeatures, MapMarker, MapOverlay, MapPadding, MapViewport, NeighborhoodSelection, } from './types.js';
|
|
5
|
+
export type { CameraOptions, DistrictSelection, DistrictStyle, DistrictYear, InteractiveLayers, MapFeatures, MapMarker, MapOverlay, MapPadding, MapViewport, NeighborhoodSelection, } from './types.js';
|
|
6
6
|
/** No geography is imported. Supply a preset or your own immutable data. */
|
|
7
7
|
export declare function createMap(data: InteractiveSFMapData, options?: MapOptions): MapController;
|
package/dist/src/map.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { validateCameraOptions } from './camera.js';
|
|
2
2
|
import { expandMapOptions, prepareConfiguration } from './configuration.js';
|
|
3
|
+
import { createNeighborhoodExplorerCore } from './explorer-core.js';
|
|
3
4
|
import { controlKeys, layerKeys } from './features.js';
|
|
4
|
-
import { createInteractiveSFMapWithData } from './interactive-data.js';
|
|
5
5
|
import { assertOptions } from './validation.js';
|
|
6
6
|
/** No geography is imported. Supply a preset or your own immutable data. */
|
|
7
7
|
export function createMap(data, options = {}) {
|
|
@@ -11,10 +11,12 @@ export function createMap(data, options = {}) {
|
|
|
11
11
|
layers: options.layers,
|
|
12
12
|
controls: options.controls,
|
|
13
13
|
});
|
|
14
|
-
const element =
|
|
14
|
+
const element = createNeighborhoodExplorerCore({
|
|
15
|
+
mode: 'basemap',
|
|
15
16
|
...expanded,
|
|
17
|
+
interface: 'map',
|
|
16
18
|
onOverlayActivate: () => { },
|
|
17
|
-
});
|
|
19
|
+
}, data);
|
|
18
20
|
const subscriptions = new Set();
|
|
19
21
|
let destroyed = false;
|
|
20
22
|
function active() {
|
|
@@ -63,6 +65,10 @@ export function createMap(data, options = {}) {
|
|
|
63
65
|
on: use((type, listener) => {
|
|
64
66
|
if (![
|
|
65
67
|
'markerchange',
|
|
68
|
+
'districtchange',
|
|
69
|
+
'districthover',
|
|
70
|
+
'districtactivate',
|
|
71
|
+
'districtyearchange',
|
|
66
72
|
'neighborhoodchange',
|
|
67
73
|
'overlayactivate',
|
|
68
74
|
'clusteractivate',
|
|
@@ -89,6 +95,10 @@ export function createMap(data, options = {}) {
|
|
|
89
95
|
getSelectedMarker: use(element.getSelectedMarker),
|
|
90
96
|
selectNeighborhood: use(element.selectNeighborhood),
|
|
91
97
|
getSelectedNeighborhood: use(element.getSelection),
|
|
98
|
+
selectDistrict: use(element.selectDistrict),
|
|
99
|
+
getSelectedDistrict: use(element.getSelectedDistrict),
|
|
100
|
+
setDistrictYear: use(element.setDistrictYear),
|
|
101
|
+
setDistrictStyle: use(element.setDistrictStyle),
|
|
92
102
|
setSource: use(element.setSource),
|
|
93
103
|
setMode: use(element.setMode),
|
|
94
104
|
setLabels: use(element.setLabels),
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { bartStations, keyRoads, landmarks } from '../data/index.js';
|
|
2
|
+
import data from './data.js';
|
|
3
|
+
import { deepFreeze } from './immutable.js';
|
|
4
|
+
/** Complete data for static rendering, without interactive lookup collections. */
|
|
5
|
+
export const staticMapData = deepFreeze({
|
|
6
|
+
coast: data.coast,
|
|
7
|
+
districts: data.districts,
|
|
8
|
+
neighborhoods: data.neighborhoods,
|
|
9
|
+
highways: data.highways,
|
|
10
|
+
landmarks: landmarks.features.map(({ id, properties, geometry }) => ({
|
|
11
|
+
id,
|
|
12
|
+
...properties,
|
|
13
|
+
geometry,
|
|
14
|
+
})),
|
|
15
|
+
keyRoads: keyRoads.features.map(({ id, properties, geometry }) => ({
|
|
16
|
+
id,
|
|
17
|
+
...properties,
|
|
18
|
+
geometry,
|
|
19
|
+
})),
|
|
20
|
+
bartStations: bartStations.features.flatMap(({ id, properties, geometry }) => geometry.type === 'Point'
|
|
21
|
+
? [{ id, name: properties.name, coordinates: geometry.coordinates }]
|
|
22
|
+
: []),
|
|
23
|
+
});
|
package/dist/src/static.d.ts
CHANGED
|
@@ -1,10 +1,43 @@
|
|
|
1
1
|
import { type SFMapData } from './map-core.js';
|
|
2
2
|
import type { SFMapOptions } from './types.js';
|
|
3
|
-
export type { SFMapData as StaticMapData } from './map-core.js';
|
|
4
|
-
export type { SFMapOptions as StaticMapOptions } from './types.js';
|
|
3
|
+
export type { DistrictRowData, SFMapData as StaticMapData } from './map-core.js';
|
|
4
|
+
export type { DistrictStyle, DistrictYear, SFMapOptions as StaticMapOptions } from './types.js';
|
|
5
5
|
/** Server-safe SVG rendering with explicit data; returns SVG plus projection helpers. */
|
|
6
6
|
export declare function renderMap(data: SFMapData, options?: SFMapOptions): {
|
|
7
7
|
svg: string;
|
|
8
8
|
project: (coordinates: import("../data/types.js").Position) => [number, number];
|
|
9
9
|
viewBox: [number, number, number, number];
|
|
10
10
|
};
|
|
11
|
+
/** Project canonical layer geometry without constructing an SVG string. */
|
|
12
|
+
export declare function getLayerPaths(data: SFMapData, options?: SFMapOptions): {
|
|
13
|
+
year: import("./types.js").DistrictYear;
|
|
14
|
+
viewBox: [number, number, number, number];
|
|
15
|
+
project: (coordinates: import("../data/types.js").Position) => [number, number];
|
|
16
|
+
coast: string;
|
|
17
|
+
districts: {
|
|
18
|
+
id: number;
|
|
19
|
+
geometry: string;
|
|
20
|
+
extras: string;
|
|
21
|
+
path: string;
|
|
22
|
+
}[];
|
|
23
|
+
neighborhoods: {
|
|
24
|
+
name: string;
|
|
25
|
+
path: string;
|
|
26
|
+
}[];
|
|
27
|
+
highways: {
|
|
28
|
+
route: string;
|
|
29
|
+
path: string;
|
|
30
|
+
}[];
|
|
31
|
+
landmarks: {
|
|
32
|
+
id: string;
|
|
33
|
+
path: string;
|
|
34
|
+
}[];
|
|
35
|
+
keyRoads: {
|
|
36
|
+
id: string;
|
|
37
|
+
path: string;
|
|
38
|
+
}[];
|
|
39
|
+
bartStations: {
|
|
40
|
+
id: string;
|
|
41
|
+
point: [number, number];
|
|
42
|
+
}[];
|
|
43
|
+
};
|
package/dist/src/static.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import { createSFMapWithData } from './map-core.js';
|
|
1
|
+
import { createSFMapWithData, getLayerPathsWithData } from './map-core.js';
|
|
2
2
|
/** Server-safe SVG rendering with explicit data; returns SVG plus projection helpers. */
|
|
3
3
|
export function renderMap(data, options = {}) {
|
|
4
4
|
return createSFMapWithData(options, data);
|
|
5
5
|
}
|
|
6
|
+
/** Project canonical layer geometry without constructing an SVG string. */
|
|
7
|
+
export function getLayerPaths(data, options = {}) {
|
|
8
|
+
return getLayerPathsWithData(options, data);
|
|
9
|
+
}
|
package/dist/src/types.d.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import type { Geometry, NeighborhoodFeature, NeighborhoodSource } from '../data/types.js';
|
|
2
2
|
export type DistrictYear = 2002 | 2012 | 2022;
|
|
3
|
+
export interface DistrictSelection {
|
|
4
|
+
id: number;
|
|
5
|
+
year: DistrictYear;
|
|
6
|
+
district: import('./map-core.js').DistrictRowData;
|
|
7
|
+
}
|
|
8
|
+
export interface DistrictStyle {
|
|
9
|
+
fill?: string;
|
|
10
|
+
stroke?: string;
|
|
11
|
+
opacity?: number;
|
|
12
|
+
}
|
|
3
13
|
export interface MapMarker {
|
|
4
14
|
id: string;
|
|
5
15
|
lng: number;
|
|
@@ -31,6 +41,8 @@ export interface SFMapOptions {
|
|
|
31
41
|
districtLines?: boolean;
|
|
32
42
|
neighborhoodLines?: boolean;
|
|
33
43
|
districtFills?: boolean;
|
|
44
|
+
/** Style each supervisorial district without taking over SVG rendering. */
|
|
45
|
+
districtStyle?: (district: import('./map-core.js').DistrictRowData) => DistrictStyle;
|
|
34
46
|
districtLabels?: boolean;
|
|
35
47
|
/** Hide all visible text labels while retaining geographic symbols and accessible titles. */
|
|
36
48
|
labels?: boolean;
|
|
@@ -112,6 +124,7 @@ export interface NeighborhoodExplorerOptions extends MapFeatures {
|
|
|
112
124
|
hoverFill?: string;
|
|
113
125
|
hoverStroke?: string;
|
|
114
126
|
};
|
|
127
|
+
districtStyle?: SFMapOptions['districtStyle'];
|
|
115
128
|
legend?: {
|
|
116
129
|
builtins?: boolean;
|
|
117
130
|
hidden?: readonly ('bart' | 'park' | 'highway' | 'road')[];
|
|
@@ -176,6 +189,12 @@ export interface NeighborhoodExplorerElement extends HTMLElement {
|
|
|
176
189
|
fit?: boolean;
|
|
177
190
|
} & CameraOptions): boolean;
|
|
178
191
|
getSelection(): NeighborhoodSelection | null;
|
|
192
|
+
selectDistrict(id: number | null, options?: {
|
|
193
|
+
fit?: boolean;
|
|
194
|
+
} & CameraOptions): boolean;
|
|
195
|
+
getSelectedDistrict(): DistrictSelection | null;
|
|
196
|
+
setDistrictYear(year: DistrictYear, options?: CameraOptions): void;
|
|
197
|
+
setDistrictStyle(style: SFMapOptions['districtStyle']): void;
|
|
179
198
|
setSource(source: NeighborhoodSource): void;
|
|
180
199
|
setMode(mode: ExplorerMode): void;
|
|
181
200
|
setLabels(visible: boolean): void;
|
package/dist/src/validation.js
CHANGED
|
@@ -33,6 +33,7 @@ const optionKeys = [
|
|
|
33
33
|
'colors',
|
|
34
34
|
'labelStyle',
|
|
35
35
|
'areaStyle',
|
|
36
|
+
'districtStyle',
|
|
36
37
|
'motion',
|
|
37
38
|
'markerEntrance',
|
|
38
39
|
'selectedMarkerRing',
|
|
@@ -60,6 +61,8 @@ const optionKeys = [
|
|
|
60
61
|
];
|
|
61
62
|
export function validateExplorerOptions(options) {
|
|
62
63
|
assertOptions(options, 'map', optionKeys);
|
|
64
|
+
if (options.districtStyle !== undefined && typeof options.districtStyle !== 'function')
|
|
65
|
+
throw new TypeError('districtStyle must be a function.');
|
|
63
66
|
for (const key of ['labels', 'selectableNeighborhoods'])
|
|
64
67
|
if (options[key] !== undefined && typeof options[key] !== 'boolean')
|
|
65
68
|
throw new TypeError(`${key} must be boolean.`);
|
package/docs/EXAMPLES.md
CHANGED
|
@@ -16,18 +16,26 @@ Choose by task. All snippets use the public package API; browser examples need a
|
|
|
16
16
|
|
|
17
17
|
```ts
|
|
18
18
|
import { writeFile } from 'node:fs/promises';
|
|
19
|
-
import {
|
|
19
|
+
import { renderMap } from '@kahwee/sf-map-svg';
|
|
20
|
+
import { fullMapData } from '@kahwee/sf-map-svg/data/full';
|
|
20
21
|
|
|
21
|
-
const svg =
|
|
22
|
+
const svg = renderMap(fullMapData.map, {
|
|
22
23
|
year: 2022,
|
|
23
24
|
landmarks: true,
|
|
24
25
|
bartStations: true,
|
|
25
26
|
idPrefix: 'example',
|
|
26
|
-
});
|
|
27
|
+
}).svg;
|
|
27
28
|
await writeFile('districts.svg', svg);
|
|
28
29
|
```
|
|
29
30
|
|
|
30
|
-
The
|
|
31
|
+
The full preset imports all packaged geography. For smaller bundles, pass selected data to the root or `/static` renderer. See the [static recipe](../README.md#static-svg).
|
|
32
|
+
|
|
33
|
+
For an election choropleth, use `renderMap(data, { year, districtStyle })` or
|
|
34
|
+
`createMap({ map: data, districts: districtMaps, neighborhoods: {} }, options)`.
|
|
35
|
+
The controller exposes `setDistrictYear`, `setDistrictStyle`, `selectDistrict`, and typed
|
|
36
|
+
district events; `getLayerPaths(data, { year })` returns fitted paths without SVG markup.
|
|
37
|
+
See the [complete election recipe](../README.md#election-district-api) and the Storybook
|
|
38
|
+
“Election choropleth” example.
|
|
31
39
|
|
|
32
40
|
## Lightweight interactive guide
|
|
33
41
|
|
|
@@ -43,18 +51,19 @@ The guide includes selected coast, SFAR neighborhoods, parks, roads, and station
|
|
|
43
51
|
## Selected geography
|
|
44
52
|
|
|
45
53
|
```ts
|
|
46
|
-
import {
|
|
54
|
+
import { createMap } from '@kahwee/sf-map-svg';
|
|
47
55
|
import coast from '@kahwee/sf-map-svg/data/coast.json' with { type: 'json' };
|
|
48
56
|
import realtor from '@kahwee/sf-map-svg/data/neighborhoods-realtor.json' with { type: 'json' };
|
|
49
57
|
|
|
50
|
-
const map =
|
|
58
|
+
const map = createMap(
|
|
51
59
|
{
|
|
52
60
|
map: { coast: coast.features[0].geometry },
|
|
53
61
|
neighborhoods: { realtor },
|
|
54
62
|
},
|
|
55
63
|
{ mode: 'neighborhoods', layers: { highways: false, keyRoads: false } },
|
|
56
64
|
);
|
|
57
|
-
document.querySelector('#map')?.append(map);
|
|
65
|
+
document.querySelector('#map')?.append(map.element);
|
|
66
|
+
// On unmount: map.destroy();
|
|
58
67
|
```
|
|
59
68
|
|
|
60
69
|
This imports one neighborhood definition source. To omit its geometry too, import catalog metadata alone from `/data/catalog`.
|
|
@@ -82,7 +91,7 @@ Coordinates are WGS84 `[longitude, latitude]`. The overlay follows pan and zoom.
|
|
|
82
91
|
|
|
83
92
|
## California propositions by SF district
|
|
84
93
|
|
|
85
|
-
[Open the interactive explorer](https://kahwee.github.io/sf-map-svg/propositions.html). It uses the public
|
|
94
|
+
[Open the interactive explorer](https://kahwee.github.io/sf-map-svg/propositions.html). It uses the public static renderer with only the 2022 district and coast datasets and colors each district from the [certified results JSON](../data/propositions/2024-11-05.json). The JSON includes all ten statewide propositions on the November 2024 ballot, with Yes and No counts for each of San Francisco's eleven supervisorial districts. Its scope is SF votes, not statewide totals or voter demographics.
|
|
86
95
|
|
|
87
96
|
The [import script](../scripts/import-2024-propositions.py) checks each district sum against the official citywide count. [Geographic and election sources](../SOURCES.md) explain the provenance.
|
|
88
97
|
|
|
@@ -94,4 +103,4 @@ compact sources, a north arrow, and a metric scale. Storybook's **Checks / Consu
|
|
|
94
103
|
API** includes executable motion, reduced-motion, and progressive-shell checks.
|
|
95
104
|
See [consumer integration](consumer-integration.md) for server and browser recipes.
|
|
96
105
|
|
|
97
|
-
For the
|
|
106
|
+
For the controller and explicit data imports, see [the v3 migration guide](migration-v3.md).
|
package/docs/api-audit.md
CHANGED
|
@@ -63,18 +63,9 @@ Exercise enable/disable round trips and callbacks that synchronously request a
|
|
|
63
63
|
second update. Add published-type and bundle regressions when the import graph or
|
|
64
64
|
public types change. Update this matrix when introducing a new interaction.
|
|
65
65
|
|
|
66
|
-
## Verification for
|
|
66
|
+
## Verification for version 3
|
|
67
67
|
|
|
68
|
-
- `pnpm check
|
|
69
|
-
- `pnpm test:stories:coverage
|
|
70
|
-
- `pnpm demo`, `pnpm build-storybook`, and
|
|
71
|
-
-
|
|
72
|
-
`pnpm report:guide` (the report script uses 1024-byte units).
|
|
73
|
-
- Isolated agent-browser inspection of generated static SVG examples and the
|
|
74
|
-
interactive example at desktop and 390px found no page overflow or browser
|
|
75
|
-
errors. Interactive labels remained collision-filtered at both widths. Browser
|
|
76
|
-
screenshots were kept outside the repository and the session was closed.
|
|
77
|
-
|
|
78
|
-
The independent pre-release review reproduced and closed stale cluster activation
|
|
79
|
-
and checked the v2 overlay event bridge. See the controller and robustness stories
|
|
80
|
-
for pointer/keyboard activation, reentrant replacement, and revoked old nodes.
|
|
68
|
+
- `pnpm check` covers Node regressions, types, lint, catalog topology, and bundle budgets.
|
|
69
|
+
- `pnpm test:stories:coverage` runs Chromium stories and reports browser coverage.
|
|
70
|
+
- `pnpm demo`, `pnpm build-storybook`, `pnpm test:package`, and `pnpm test:visual` cover examples, rendered stories, packed consumer imports, and reviewed map screenshots.
|
|
71
|
+
- The packed consumer asserts removed compatibility entry points are absent.
|
|
@@ -1,165 +1,18 @@
|
|
|
1
|
-
# Consumer integration
|
|
1
|
+
# Consumer integration
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
recipes below remain supported for existing guide and progressive-shell consumers.
|
|
3
|
+
Use the version 3 root controller and explicit geographic data. The [migration guide](migration-v3.md) maps removed imports to supported calls.
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
For a compact browser guide:
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
geometry from a bundle.
|
|
7
|
+
```ts
|
|
8
|
+
import { createMap } from '@kahwee/sf-map-svg';
|
|
9
|
+
import { guideMapData } from '@kahwee/sf-map-svg/guide/data';
|
|
10
|
+
import { guideOptions } from '@kahwee/sf-map-svg/presets';
|
|
13
11
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
with compression and normal long-lived versioned cache headers. Avoid root or
|
|
19
|
-
`data` barrel imports in client code when only one collection is needed.
|
|
20
|
-
|
|
21
|
-
Load `guide/detailed` only after a user requests higher detail. Dynamically import
|
|
22
|
-
the browser map when its container approaches the viewport. Keep the static
|
|
23
|
-
renderer in server/build code; do not import it in a client loader. The overview
|
|
24
|
-
SVG still contains geography, so both static and interactive representations cost
|
|
25
|
-
bytes. Measure HTML and JS separately, including compressed bytes and lazy chunks.
|
|
26
|
-
Do not inline full-detail data into every page.
|
|
27
|
-
|
|
28
|
-
`pnpm report:guide` records measured raw/gzip sizes and audits included datasets.
|
|
29
|
-
`pnpm check` enforces 125 KiB initial guide and 35 KiB data-free renderer budgets.
|
|
30
|
-
These are regression ceilings, not claims about every application's final bundle.
|
|
31
|
-
Current measurements are in [the bundle report](guide-bundle-report.md).
|
|
32
|
-
|
|
33
|
-
## Progressive compact layout
|
|
34
|
-
|
|
35
|
-
Build/server code:
|
|
36
|
-
|
|
37
|
-
```js
|
|
38
|
-
import { createGuideShell } from '@kahwee/sf-map-svg/guide/static';
|
|
39
|
-
const html = createGuideShell({ markers: places, labels: false });
|
|
40
|
-
// Put html into the page alongside an accessible place list.
|
|
12
|
+
const map = createMap(guideMapData, { ...guideOptions });
|
|
13
|
+
document.querySelector('#map')?.append(map.element);
|
|
14
|
+
// Dispose when the containing view unmounts.
|
|
15
|
+
map.destroy();
|
|
41
16
|
```
|
|
42
17
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
```js
|
|
46
|
-
import { mountGuideMap } from '@kahwee/sf-map-svg/guide/map';
|
|
47
|
-
const map = mountGuideMap(document.querySelector('.sf-guide-shell'), {
|
|
48
|
-
markers: places,
|
|
49
|
-
labels: false,
|
|
50
|
-
motion: { duration: 400 },
|
|
51
|
-
markerEntrance: true,
|
|
52
|
-
clustering: true,
|
|
53
|
-
selectedMarkerRing: { color: '#163d61' },
|
|
54
|
-
strings: { touchNavigation: 'Touch pan', touchNavigationDone: 'Done' },
|
|
55
|
-
});
|
|
56
|
-
// In your place list's activation handler:
|
|
57
|
-
map.selectMarker(places[0].id);
|
|
58
|
-
// On component disposal:
|
|
59
|
-
// map.destroy();
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
Pass the same palette, markers, and layer choices to both sides. `labels:false`
|
|
63
|
-
avoids a label-placement change during enhancement: static labels and interactive
|
|
64
|
-
collision placement are different algorithms. Shell placeholders are not working
|
|
65
|
-
controls, and the SVG remains available without JavaScript. The compact shell
|
|
66
|
-
reserves its own rows at all widths; it is not a general serializer for arbitrary
|
|
67
|
-
explorer chrome. Keep external image/card aspect ratios and list heights stable as
|
|
68
|
-
well: a library shell cannot prevent shifts elsewhere in your page.
|
|
69
|
-
|
|
70
|
-
## Callouts without private DOM selectors
|
|
71
|
-
|
|
72
|
-
```js
|
|
73
|
-
const callout = document.createElement('div');
|
|
74
|
-
callout.textContent = 'Selected place';
|
|
75
|
-
callout.style.position = 'absolute';
|
|
76
|
-
map.overlayElement.append(callout);
|
|
77
|
-
const placeCallout = () => {
|
|
78
|
-
const pin = map.getSelectedMarker();
|
|
79
|
-
if (!pin) { callout.hidden = true; return; }
|
|
80
|
-
const point = map.projectToScreen(pin.lng, pin.lat);
|
|
81
|
-
callout.hidden = !point.visible;
|
|
82
|
-
callout.style.transform = `translate(${point.x}px, ${point.y}px)`;
|
|
83
|
-
};
|
|
84
|
-
for (const event of ['markerchange', 'viewportchange', 'mapresize'])
|
|
85
|
-
map.addEventListener(event, placeCallout);
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
These coordinates are relative to the canvas, not the page. Callout size,
|
|
89
|
-
collision avoidance, and focus belong to the application. Use `textContent` for
|
|
90
|
-
untrusted text. Consumer-added listeners should be removed on disposal (or use
|
|
91
|
-
an application-owned AbortSignal).
|
|
92
|
-
|
|
93
|
-
## Motion and dense places
|
|
94
|
-
|
|
95
|
-
Camera motion and marker entrances are independent and optional. Keep stable pin
|
|
96
|
-
IDs across filtering so existing places do not repeatedly drop in. A subsequent
|
|
97
|
-
camera request supersedes an earlier one. Reduced motion wins over application
|
|
98
|
-
options; changing the preference during motion stops at the current position.
|
|
99
|
-
|
|
100
|
-
Clustering uses a deterministic screen-distance grouping in stable marker-ID order using a spatial index. It is
|
|
101
|
-
intended for guide-sized lists, not millions of records. Clicking or pressing Enter
|
|
102
|
-
on a cluster fits its extent and emits `clusteractivate`. Exactly coincident places
|
|
103
|
-
remain clustered even at maximum zoom. Always provide a chooser/list; selecting a
|
|
104
|
-
place removes it from its cluster and draws it above clusters. Avoid silently
|
|
105
|
-
hiding all alternate access when setting `controls.markerPicker:false`.
|
|
106
|
-
|
|
107
|
-
## Verification expectations
|
|
108
|
-
|
|
109
|
-
Run the existing Node and browser suites plus bundle checks. The Consumer API
|
|
110
|
-
stories exercise transitions, supersession, teardown, reduced motion, clustered
|
|
111
|
-
selection, touch names, and 390px shell replacement. Also inspect the example page
|
|
112
|
-
at desktop/tablet/390px: keyboard focus, drag interruption, labels and marker
|
|
113
|
-
collisions, overflow, console errors, and sources disclosure. Test your own
|
|
114
|
-
container/font/CSS and delayed image loading before claiming page-wide zero CLS.
|
|
115
|
-
|
|
116
|
-
## Runtime configuration and failure handling
|
|
117
|
-
|
|
118
|
-
Prefer `setFeatures`, `setLayers`, and `setControls` over recreating a map for each
|
|
119
|
-
checkbox. Patches leave omitted keys alone; explicit `false` disables and
|
|
120
|
-
`undefined` resets. Feature option objects replace the previous object, so
|
|
121
|
-
`setFeatures({ motion: { duration: 200 } })` is predictable without hidden retained
|
|
122
|
-
fields. `getFeatures()` is a detached normalized snapshot, safe for consumer reads.
|
|
123
|
-
|
|
124
|
-
Invalid feature/control/layer updates preserve the previous state. Invalid marker
|
|
125
|
-
and overlay replacements preserve the previous list. Removed overlays lose their
|
|
126
|
-
listeners immediately. Repeated disposal cancels all asynchronous work and returns
|
|
127
|
-
touch gestures to the page. Focused markers stay reachable through filtering;
|
|
128
|
-
when a focused marker disappears, focus returns to the map rather than the page
|
|
129
|
-
body. A focused cluster that dissolves also returns focus to the map.
|
|
130
|
-
|
|
131
|
-
The compact mount uses defaults for its controls, then honors caller overrides.
|
|
132
|
-
Enabling native pickers deliberately adds their row. The default shell replacement
|
|
133
|
-
has stable geometry; custom controls, fonts, and application CSS still require
|
|
134
|
-
consumer verification. Shells reject custom SVG dimensions/padding because the
|
|
135
|
-
interactive projection is fixed. Use `createGuideSVG` for custom static sizes.
|
|
136
|
-
|
|
137
|
-
Adversarial checks cover reentrant camera callbacks, invalid/atomic patches,
|
|
138
|
-
feature round trips, control independence, overlay replacement/disposal, detached
|
|
139
|
-
configuration snapshots, keyboard focus, and 2,000-marker grouping. The camera,
|
|
140
|
-
feature normalization, and spatial grouping modules are separate from DOM rendering
|
|
141
|
-
so future options can extend those contracts without rebuilding their lifecycles.
|
|
142
|
-
|
|
143
|
-
### Adding another optional feature
|
|
144
|
-
|
|
145
|
-
Add its public type and default to the configuration boundary first. Validate and
|
|
146
|
-
copy the full next configuration before touching the current map. Implement enable
|
|
147
|
-
and disable symmetrically, and define what happens to in-flight work when it changes.
|
|
148
|
-
Avoid hiding unrelated controls through a shared parent. Any window/document/media
|
|
149
|
-
listener, observer, animation, or replaceable interactive subtree needs an explicit
|
|
150
|
-
owner and cleanup path. Keep camera timing and geographic grouping independently
|
|
151
|
-
testable rather than embedding more asynchronous state inside renderer callbacks.
|
|
152
|
-
|
|
153
|
-
For each addition, test disabled construction, enabling, disabling, repeated round
|
|
154
|
-
trips, invalid updates, reduced motion where relevant, and disposal. Verify that the
|
|
155
|
-
viewport, selection, keyboard focus, accessible descriptions, and layer legend stay
|
|
156
|
-
consistent. Runtime toggles are not a substitute for selective data imports or
|
|
157
|
-
production bundle measurement. New palette/font/layout choices still need rendered
|
|
158
|
-
consumer checks; passing these tests does not guarantee every arbitrary host CSS or
|
|
159
|
-
data combination.
|
|
160
|
-
|
|
161
|
-
The [maintainer audit matrix](api-audit.md) records option precedence, invalid-input
|
|
162
|
-
behavior, callback reentrancy, ownership, and explicit limits. Interactive options
|
|
163
|
-
reject unknown keys, including nested typos. Shared preset data is immutable;
|
|
164
|
-
clone it before deriving custom collections. The compact mount rejects full
|
|
165
|
-
attribution rather than silently overriding it.
|
|
18
|
+
For server rendering, import `renderMap` from the root and pass `guideMapData.map` or another explicit `StaticMapData`. Import `/data/full` only if the complete packaged geography is required. All rendering stays offline and has zero runtime dependencies. The guide's detailed geography loads only when `loadGuideDetailedData()` is called.
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
# Guide bundle size report
|
|
2
2
|
|
|
3
|
-
Generated 2026-09-27 by `pnpm report:guide` with Vite production minification and gzip compression. Each emitted JS chunk is compressed independently. The
|
|
3
|
+
Generated 2026-09-27 by `pnpm report:guide` with Vite production minification and gzip compression. Each emitted JS chunk is compressed independently. The report measures the explicit-data v3 root and the optional `@kahwee/sf-map-svg/guide` preset.
|
|
4
4
|
|
|
5
5
|
| Entry | Initial JS, raw | Initial JS, gzip | Explicit detail JS, gzip |
|
|
6
6
|
| --- | ---: | ---: | ---: |
|
|
7
|
-
|
|
|
8
|
-
|
|
|
9
|
-
|
|
|
10
|
-
| Data-free interactive renderer | 72.5 KB | 21.8 KB | — |
|
|
11
|
-
| Guide preset (after) | 461.7 KB | 110.2 KB | 473.7 KB |
|
|
7
|
+
| v3 root (explicit data) | 84.5 KB | 24.6 KB | — |
|
|
8
|
+
| v3 static renderer | 14.8 KB | 4.9 KB | — |
|
|
9
|
+
| Guide preset | 469.8 KB | 112.0 KB | 473.7 KB |
|
|
12
10
|
|
|
13
|
-
**
|
|
11
|
+
**500 KB target:** met.
|
|
14
12
|
|
|
15
13
|
The initial guide chunk graph includes only the overview coast, SFAR realtor neighborhoods, major parks, selected highways, six selected streets, and BART points. It excludes historical districts, SF Find neighborhoods, analysis neighborhoods, and the full catalog. Detailed coast, selected SFAR boundaries, parks, selected highway routes, streets, and BART data are in dynamic chunks and load only when `loadGuideDetailedData()` is called. The data inclusion assertions run as part of this report command.
|
|
16
14
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Migrate to version 3
|
|
2
|
+
|
|
3
|
+
Version 3 removes the deprecated compatibility entry points and their bundled-data factories. This is a breaking major release. Version 2 remains available if you need time to migrate; removed imports fail to resolve in version 3. The root API introduced in version 2 remains the supported API.
|
|
4
|
+
|
|
5
|
+
| Version 2 import or call | Version 3 replacement |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| `/legacy` `renderSFMap(options)` | `renderMap(fullMapData.map, options).svg` |
|
|
8
|
+
| `/legacy` `createSFMap(options)` | `renderMap(fullMapData.map, options)` |
|
|
9
|
+
| `/custom-map` `createSFMapWithData(options, data)` | `renderMap(data, options)` |
|
|
10
|
+
| `/explorer` `createNeighborhoodExplorer(options)` | `createMap(fullMapData, options)`; mount `.element` |
|
|
11
|
+
| `/interactive` `createInteractiveSFMap(options)` | `createMap(fullMapData, { mode: 'basemap', ...options })`; mount `.element` |
|
|
12
|
+
| `/interactive-data` `createInteractiveSFMapWithData(data, options)` | `createMap(data, options)`; mount `.element` |
|
|
13
|
+
|
|
14
|
+
`fullMapData` comes from `@kahwee/sf-map-svg/data/full`. It is an explicit import of all packaged geography. For smaller bundles, use `staticMapData` from `/data/static` for SVGs, construct data from individual `/data/*` modules, or use `guideMapData` from `/guide/data`. There is no implicit geographic data in the root import.
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { createMap, renderMap } from '@kahwee/sf-map-svg';
|
|
18
|
+
import { fullMapData } from '@kahwee/sf-map-svg/data/full';
|
|
19
|
+
|
|
20
|
+
const svg = renderMap(fullMapData.map, { landmarks: true }).svg;
|
|
21
|
+
const map = createMap(fullMapData, { mode: 'neighborhoods' });
|
|
22
|
+
document.querySelector('#map')?.append(map.element);
|
|
23
|
+
map.camera.zoom(2);
|
|
24
|
+
map.configure({ layers: { landmarks: false } });
|
|
25
|
+
map.destroy();
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The controller owns subscriptions and cleanup. Replace old element calls with controller methods (`camera.get/set/pan/zoom/reset/fit/stop`, `selectNeighborhood`, `setSource`, `setMode`, `setLabels`, `setMarkers`, and `setOverlays`). Group feature flags under `features` and styling under `appearance`; `configure` updates runtime features, layers, and controls. For code that needs the guide shell, `/guide`, `/guide/data`, `/guide/static`, and `/guide/map` remain supported. `/transit` also remains supported.
|
|
29
|
+
|
|
30
|
+
Review tree-shaking after migrating: `/data/full` intentionally includes every packaged layer, while the root and `/static` stay data free. Test server rendering, browser mounting, and map disposal in your application before upgrading production.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kahwee/sf-map-svg",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Offline SVG maps of San Francisco with district boundaries, parks, landmarks, and BART stations.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/src/api.js",
|
|
@@ -10,10 +10,6 @@
|
|
|
10
10
|
"types": "./dist/src/api.d.ts",
|
|
11
11
|
"import": "./dist/src/api.js"
|
|
12
12
|
},
|
|
13
|
-
"./custom-map": {
|
|
14
|
-
"types": "./dist/src/custom-map.d.ts",
|
|
15
|
-
"import": "./dist/src/custom-map.js"
|
|
16
|
-
},
|
|
17
13
|
"./data": {
|
|
18
14
|
"types": "./dist/data/index.d.ts",
|
|
19
15
|
"import": "./dist/data/index.js"
|
|
@@ -26,6 +22,14 @@
|
|
|
26
22
|
"types": "./dist/data/catalog.d.ts",
|
|
27
23
|
"import": "./dist/data/catalog.js"
|
|
28
24
|
},
|
|
25
|
+
"./data/full": {
|
|
26
|
+
"types": "./dist/src/full-data.d.ts",
|
|
27
|
+
"import": "./dist/src/full-data.js"
|
|
28
|
+
},
|
|
29
|
+
"./data/static": {
|
|
30
|
+
"types": "./dist/src/static-data.d.ts",
|
|
31
|
+
"import": "./dist/src/static-data.js"
|
|
32
|
+
},
|
|
29
33
|
"./data/coast": {
|
|
30
34
|
"types": "./dist/data/coast.d.ts",
|
|
31
35
|
"import": "./dist/data/coast.js"
|
|
@@ -68,18 +72,6 @@
|
|
|
68
72
|
"types": "./dist/src/geometry.d.ts",
|
|
69
73
|
"import": "./dist/src/geometry.js"
|
|
70
74
|
},
|
|
71
|
-
"./explorer": {
|
|
72
|
-
"types": "./dist/src/explorer.d.ts",
|
|
73
|
-
"import": "./dist/src/explorer.js"
|
|
74
|
-
},
|
|
75
|
-
"./interactive": {
|
|
76
|
-
"types": "./dist/src/interactive.d.ts",
|
|
77
|
-
"import": "./dist/src/interactive.js"
|
|
78
|
-
},
|
|
79
|
-
"./interactive-data": {
|
|
80
|
-
"types": "./dist/src/interactive-data.d.ts",
|
|
81
|
-
"import": "./dist/src/interactive-data.js"
|
|
82
|
-
},
|
|
83
75
|
"./guide": {
|
|
84
76
|
"types": "./dist/src/guide.d.ts",
|
|
85
77
|
"import": "./dist/src/guide.js"
|
|
@@ -104,10 +96,6 @@
|
|
|
104
96
|
"types": "./dist/src/guide-static.d.ts",
|
|
105
97
|
"import": "./dist/src/guide-static.js"
|
|
106
98
|
},
|
|
107
|
-
"./legacy": {
|
|
108
|
-
"types": "./dist/src/index.d.ts",
|
|
109
|
-
"import": "./dist/src/index.js"
|
|
110
|
-
},
|
|
111
99
|
"./map": {
|
|
112
100
|
"types": "./dist/src/map.d.ts",
|
|
113
101
|
"import": "./dist/src/map.js"
|
|
@@ -130,7 +118,7 @@
|
|
|
130
118
|
"docs/consumer-integration.md",
|
|
131
119
|
"docs/api-audit.md",
|
|
132
120
|
"docs/guide-bundle-report.md",
|
|
133
|
-
"docs/migration-
|
|
121
|
+
"docs/migration-v3.md",
|
|
134
122
|
"CHANGELOG.md"
|
|
135
123
|
],
|
|
136
124
|
"sideEffects": false,
|
|
@@ -179,15 +167,17 @@
|
|
|
179
167
|
"test": "pnpm build && node --test",
|
|
180
168
|
"test:stories": "pnpm build && vitest run --project=storybook",
|
|
181
169
|
"test:stories:coverage": "pnpm build && vitest run --project=storybook --coverage",
|
|
170
|
+
"test:visual": "playwright test --config=playwright.visual.config.mjs",
|
|
182
171
|
"demo": "pnpm build && node examples/build.mjs",
|
|
183
172
|
"format": "biome check --write .",
|
|
184
173
|
"format:check": "biome format .",
|
|
185
|
-
"check": "pnpm peers check && pnpm lint && pnpm data:check && pnpm typecheck && pnpm test && pnpm test:bundle",
|
|
174
|
+
"check": "pnpm peers check && pnpm lint && pnpm data:check && pnpm typecheck && pnpm typecheck:stories && pnpm test && pnpm test:bundle",
|
|
186
175
|
"storybook": "pnpm build && storybook dev -p 6006 --host 127.0.0.1 --no-open",
|
|
187
176
|
"build-storybook": "pnpm build && storybook build",
|
|
188
177
|
"data:catalog": "node scripts/build-data-catalog.mjs",
|
|
189
178
|
"data:check": "node scripts/build-data-catalog.mjs --check",
|
|
190
179
|
"typecheck": "pnpm build && tsc -p tsconfig.json",
|
|
180
|
+
"typecheck:stories": "tsc -p tsconfig.stories.json",
|
|
191
181
|
"data:normalize-realtor": "node scripts/normalize-realtor.mjs",
|
|
192
182
|
"data:guide": "node scripts/build-guide-data.mjs",
|
|
193
183
|
"report:guide": "node scripts/report-guide-bundle.mjs",
|
package/dist/src/custom-map.d.ts
DELETED
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
export type { BartStationData, DistrictRowData, KeyRoadData, LandmarkData, SFMapData, } from './map-core.js';
|
|
2
|
-
export { createSFMapWithData, districtColors, districtYears, } from './map-core.js';
|
|
3
|
-
export type { DistrictYear, MapMarker, MapOverlay, SFMapOptions } from './types.js';
|
package/dist/src/custom-map.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { createSFMapWithData, districtColors, districtYears, } from './map-core.js';
|
package/dist/src/explorer.d.ts
DELETED
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import type { NeighborhoodExplorerElement, NeighborhoodExplorerOptions } from './types.js';
|
|
2
|
-
export type { ExplorerMode, NeighborhoodExplorerElement, NeighborhoodExplorerOptions, } from './types.js';
|
|
3
|
-
/** Create an offline, browser-only neighborhood explorer with package datasets. */
|
|
4
|
-
export declare function createNeighborhoodExplorer(options?: NeighborhoodExplorerOptions): NeighborhoodExplorerElement;
|