@kahwee/sf-map-svg 2.0.0 → 2.2.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 +14 -0
- package/README.md +72 -1
- package/SOURCES.md +34 -0
- package/dist/data/candidates/2016-11-08.json +1 -0
- package/dist/data/candidates/2018-11-06.json +1 -0
- package/dist/data/candidates/2020-11-03.json +1 -0
- package/dist/data/candidates/2022-11-08.json +1 -0
- package/dist/data/candidates/2024-11-05.json +1 -0
- package/dist/data/candidates/2026-06-02.json +1 -0
- package/dist/data/candidates/README.md +43 -0
- package/dist/data/candidates/catalog.json +40 -0
- 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/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 +1 -1
- package/dist/src/map.js +8 -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 +8 -1
- package/package.json +5 -2
package/dist/src/map-core.d.ts
CHANGED
|
@@ -47,6 +47,39 @@ export interface BartStationData {
|
|
|
47
47
|
}
|
|
48
48
|
export declare const districtYears: readonly DistrictYear[];
|
|
49
49
|
export declare const districtColors: readonly string[];
|
|
50
|
+
/** Canonical geometry in the same fitted coordinate space used by renderMap. */
|
|
51
|
+
export declare function getLayerPathsWithData(options: SFMapOptions, data: SFMapData, complete?: boolean): {
|
|
52
|
+
year: DistrictYear;
|
|
53
|
+
viewBox: [number, number, number, number];
|
|
54
|
+
project: (coordinates: Position) => [number, number];
|
|
55
|
+
coast: string;
|
|
56
|
+
districts: {
|
|
57
|
+
id: number;
|
|
58
|
+
geometry: string;
|
|
59
|
+
extras: string;
|
|
60
|
+
path: string;
|
|
61
|
+
}[];
|
|
62
|
+
neighborhoods: {
|
|
63
|
+
name: string;
|
|
64
|
+
path: string;
|
|
65
|
+
}[];
|
|
66
|
+
highways: {
|
|
67
|
+
route: string;
|
|
68
|
+
path: string;
|
|
69
|
+
}[];
|
|
70
|
+
landmarks: {
|
|
71
|
+
id: string;
|
|
72
|
+
path: string;
|
|
73
|
+
}[];
|
|
74
|
+
keyRoads: {
|
|
75
|
+
id: string;
|
|
76
|
+
path: string;
|
|
77
|
+
}[];
|
|
78
|
+
bartStations: {
|
|
79
|
+
id: string;
|
|
80
|
+
point: [number, number];
|
|
81
|
+
}[];
|
|
82
|
+
};
|
|
50
83
|
/** Make an offline SVG and the matching longitude/latitude projection. */
|
|
51
84
|
export declare function createSFMapWithData(options: SFMapOptions, data: SFMapData): {
|
|
52
85
|
svg: string;
|
package/dist/src/map-core.js
CHANGED
|
@@ -16,6 +16,66 @@ export const districtColors = Object.freeze([
|
|
|
16
16
|
'#cbdcd1',
|
|
17
17
|
'#dfddc9',
|
|
18
18
|
]);
|
|
19
|
+
/** Canonical geometry in the same fitted coordinate space used by renderMap. */
|
|
20
|
+
export function getLayerPathsWithData(options, data, complete = true) {
|
|
21
|
+
const { width = 800, height = 800, padding = 28, year = 2022 } = options;
|
|
22
|
+
if (![width, height, padding].every(Number.isFinite) ||
|
|
23
|
+
width <= 0 ||
|
|
24
|
+
height <= 0 ||
|
|
25
|
+
padding < 0 ||
|
|
26
|
+
padding * 2 >= Math.min(width, height))
|
|
27
|
+
throw new RangeError('Use positive dimensions and padding smaller than half the map.');
|
|
28
|
+
if (!districtYears.includes(year))
|
|
29
|
+
throw new RangeError('District year must be 2002, 2012, or 2022.');
|
|
30
|
+
const coastPoints = positions(data.coast).map(rawProject);
|
|
31
|
+
const [minX, minY, maxX, maxY] = coastPoints.reduce((b, [x, y]) => [Math.min(b[0], x), Math.min(b[1], y), Math.max(b[2], x), Math.max(b[3], y)], [Infinity, Infinity, -Infinity, -Infinity]);
|
|
32
|
+
const scale = Math.min((width - padding * 2) / (maxX - minX), (height - padding * 2) / (maxY - minY));
|
|
33
|
+
const project = (coordinates) => {
|
|
34
|
+
const [x, y] = rawProject(coordinates);
|
|
35
|
+
const point = [
|
|
36
|
+
(x - (minX + maxX) / 2) * scale + width / 2,
|
|
37
|
+
(y - (minY + maxY) / 2) * scale + height / 2,
|
|
38
|
+
];
|
|
39
|
+
if (!point.every(Number.isFinite))
|
|
40
|
+
throw new RangeError('Projected coordinates must be finite; use smaller map dimensions.');
|
|
41
|
+
return point;
|
|
42
|
+
};
|
|
43
|
+
const path = (geometry) => geometryPath(geometry, project);
|
|
44
|
+
return {
|
|
45
|
+
year,
|
|
46
|
+
viewBox: [0, 0, width, height],
|
|
47
|
+
project,
|
|
48
|
+
coast: path(data.coast),
|
|
49
|
+
districts: (complete || options.districtFills !== false || options.districtLines !== false
|
|
50
|
+
? (data.districts?.[year] ?? [])
|
|
51
|
+
: []).map((district) => ({
|
|
52
|
+
id: district.id,
|
|
53
|
+
geometry: path(district.geometry),
|
|
54
|
+
extras: path(district.extras),
|
|
55
|
+
path: path(district.geometry) + path(district.extras),
|
|
56
|
+
})),
|
|
57
|
+
neighborhoods: (complete ? (data.neighborhoods ?? []) : []).map((item) => ({
|
|
58
|
+
name: item.name,
|
|
59
|
+
path: path(item.geometry),
|
|
60
|
+
})),
|
|
61
|
+
highways: (complete ? (data.highways ?? []) : []).map((item) => ({
|
|
62
|
+
route: item.route,
|
|
63
|
+
path: path(item.geometry),
|
|
64
|
+
})),
|
|
65
|
+
landmarks: (complete ? (data.landmarks ?? []) : []).map((item) => ({
|
|
66
|
+
id: item.id,
|
|
67
|
+
path: path(item.geometry),
|
|
68
|
+
})),
|
|
69
|
+
keyRoads: (complete ? (data.keyRoads ?? []) : []).map((item) => ({
|
|
70
|
+
id: item.id,
|
|
71
|
+
path: path(item.geometry),
|
|
72
|
+
})),
|
|
73
|
+
bartStations: (complete ? (data.bartStations ?? []) : []).map((item) => ({
|
|
74
|
+
id: item.id,
|
|
75
|
+
point: project(item.coordinates),
|
|
76
|
+
})),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
19
79
|
const defaults = {
|
|
20
80
|
water: '#e7f0f3',
|
|
21
81
|
land: '#f1f3ee',
|
|
@@ -46,49 +106,35 @@ const transitColors = {
|
|
|
46
106
|
let sequence = 0;
|
|
47
107
|
/** Make an offline SVG and the matching longitude/latitude projection. */
|
|
48
108
|
export function createSFMapWithData(options, data) {
|
|
49
|
-
const { theme = 'districts', width = 800, height = 800,
|
|
109
|
+
const { theme = 'districts', width = 800, height = 800, year = 2022, districtLines = true, neighborhoodLines = false, districtFills = true, districtLabels = true, labels = true, highways = false, keyRoads = false, landmarks = false, bartStations = false, markers = [], title = 'San Francisco map', idPrefix = `sf-map-${++sequence}`, } = options;
|
|
50
110
|
validateMarkers(markers);
|
|
51
111
|
validateOverlays(options.overlays ?? []);
|
|
52
112
|
const roadLabels = options.roadLabels ?? keyRoads;
|
|
53
|
-
if (![width, height, padding].every(Number.isFinite) ||
|
|
54
|
-
width <= 0 ||
|
|
55
|
-
height <= 0 ||
|
|
56
|
-
padding < 0 ||
|
|
57
|
-
padding * 2 >= Math.min(width, height))
|
|
58
|
-
throw new RangeError('Use positive dimensions and padding smaller than half the map.');
|
|
59
|
-
if (!districtYears.includes(year))
|
|
60
|
-
throw new RangeError('District year must be 2002, 2012, or 2022.');
|
|
61
113
|
if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(idPrefix))
|
|
62
114
|
throw new TypeError('idPrefix must start with a letter and contain only letters, numbers, underscores, or hyphens.');
|
|
63
115
|
if (theme !== 'districts' && theme !== 'transit')
|
|
64
116
|
throw new TypeError('Theme must be districts or transit.');
|
|
65
117
|
const colors = { ...defaults, ...(theme === 'transit' ? transitColors : {}), ...options.colors };
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
const scale = Math.min((width - padding * 2) / (maxX - minX), (height - padding * 2) / (maxY - minY));
|
|
69
|
-
const project = (coordinates) => {
|
|
70
|
-
const [x, y] = rawProject(coordinates);
|
|
71
|
-
const point = [
|
|
72
|
-
(x - (minX + maxX) / 2) * scale + width / 2,
|
|
73
|
-
(y - (minY + maxY) / 2) * scale + height / 2,
|
|
74
|
-
];
|
|
75
|
-
if (!point.every(Number.isFinite))
|
|
76
|
-
throw new RangeError('Projected coordinates must be finite; use smaller map dimensions.');
|
|
77
|
-
return point;
|
|
78
|
-
};
|
|
118
|
+
const geometry = getLayerPathsWithData(options, data, false);
|
|
119
|
+
const { project } = geometry;
|
|
79
120
|
const path = (geometry) => geometryPath(geometry, project);
|
|
80
|
-
const coastPath =
|
|
121
|
+
const coastPath = geometry.coast;
|
|
81
122
|
const districts = data.districts?.[year] ?? [];
|
|
82
123
|
const landmarkData = data.landmarks ?? [];
|
|
83
124
|
const roadData = data.keyRoads ?? [];
|
|
84
125
|
const stationData = data.bartStations ?? [];
|
|
85
126
|
const context = { project, path, colors, idPrefix, theme, labels };
|
|
86
127
|
const districtPaths = districtFills || districtLines
|
|
87
|
-
? districts.map((d) =>
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
128
|
+
? districts.map((d, index) => {
|
|
129
|
+
const style = validateDistrictStyle(options.districtStyle?.(d));
|
|
130
|
+
return {
|
|
131
|
+
id: d.id,
|
|
132
|
+
path: geometry.districts[index]?.path ?? '',
|
|
133
|
+
stroke: style.stroke,
|
|
134
|
+
opacity: style.opacity,
|
|
135
|
+
color: style.fill ?? (theme === 'transit' ? colors.land : districtColors[d.id - 1]),
|
|
136
|
+
};
|
|
137
|
+
})
|
|
92
138
|
: [];
|
|
93
139
|
const parts = [
|
|
94
140
|
`<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}" 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><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"/>`,
|
|
@@ -125,3 +171,19 @@ export function createSFMapWithData(options, data) {
|
|
|
125
171
|
viewBox: [0, 0, width, height],
|
|
126
172
|
};
|
|
127
173
|
}
|
|
174
|
+
function validateDistrictStyle(style) {
|
|
175
|
+
if (style === undefined)
|
|
176
|
+
return {};
|
|
177
|
+
if (!style || typeof style !== 'object' || Array.isArray(style))
|
|
178
|
+
throw new TypeError('districtStyle must return a style object.');
|
|
179
|
+
for (const key of Object.keys(style))
|
|
180
|
+
if (!['fill', 'stroke', 'opacity'].includes(key))
|
|
181
|
+
throw new TypeError(`Unknown district style: ${key}`);
|
|
182
|
+
for (const key of ['fill', 'stroke'])
|
|
183
|
+
if (style[key] !== undefined && typeof style[key] !== 'string')
|
|
184
|
+
throw new TypeError(`${key} must be a string.`);
|
|
185
|
+
if (style.opacity !== undefined &&
|
|
186
|
+
(!Number.isFinite(style.opacity) || style.opacity < 0 || style.opacity > 1))
|
|
187
|
+
throw new RangeError('District opacity must be between 0 and 1.');
|
|
188
|
+
return style;
|
|
189
|
+
}
|
package/dist/src/map.d.ts
CHANGED
|
@@ -2,6 +2,6 @@ import type { MapController, MapOptions } from './controller-types.js';
|
|
|
2
2
|
import { type InteractiveSFMapData } from './interactive-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
|
@@ -63,6 +63,10 @@ export function createMap(data, options = {}) {
|
|
|
63
63
|
on: use((type, listener) => {
|
|
64
64
|
if (![
|
|
65
65
|
'markerchange',
|
|
66
|
+
'districtchange',
|
|
67
|
+
'districthover',
|
|
68
|
+
'districtactivate',
|
|
69
|
+
'districtyearchange',
|
|
66
70
|
'neighborhoodchange',
|
|
67
71
|
'overlayactivate',
|
|
68
72
|
'clusteractivate',
|
|
@@ -89,6 +93,10 @@ export function createMap(data, options = {}) {
|
|
|
89
93
|
getSelectedMarker: use(element.getSelectedMarker),
|
|
90
94
|
selectNeighborhood: use(element.selectNeighborhood),
|
|
91
95
|
getSelectedNeighborhood: use(element.getSelection),
|
|
96
|
+
selectDistrict: use(element.selectDistrict),
|
|
97
|
+
getSelectedDistrict: use(element.getSelectedDistrict),
|
|
98
|
+
setDistrictYear: use(element.setDistrictYear),
|
|
99
|
+
setDistrictStyle: use(element.setDistrictStyle),
|
|
92
100
|
setSource: use(element.setSource),
|
|
93
101
|
setMode: use(element.setMode),
|
|
94
102
|
setLabels: use(element.setLabels),
|
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
|
@@ -27,7 +27,14 @@ const svg = renderSFMap({
|
|
|
27
27
|
await writeFile('districts.svg', svg);
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
The
|
|
30
|
+
The legacy entry includes built-in geography for convenience. The modern root and `/static` accept explicit geography. See the [full option list](../README.md#static-options).
|
|
31
|
+
|
|
32
|
+
For an election choropleth, use `renderMap(data, { year, districtStyle })` or
|
|
33
|
+
`createMap({ map: data, districts: districtMaps, neighborhoods: {} }, options)`.
|
|
34
|
+
The controller exposes `setDistrictYear`, `setDistrictStyle`, `selectDistrict`, and typed
|
|
35
|
+
district events; `getLayerPaths(data, { year })` returns fitted paths without SVG markup.
|
|
36
|
+
See the [complete election recipe](../README.md#election-district-api) and the Storybook
|
|
37
|
+
“Election choropleth” example.
|
|
31
38
|
|
|
32
39
|
## Lightweight interactive guide
|
|
33
40
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kahwee/sf-map-svg",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.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",
|
|
@@ -179,15 +179,17 @@
|
|
|
179
179
|
"test": "pnpm build && node --test",
|
|
180
180
|
"test:stories": "pnpm build && vitest run --project=storybook",
|
|
181
181
|
"test:stories:coverage": "pnpm build && vitest run --project=storybook --coverage",
|
|
182
|
+
"test:visual": "playwright test --config=playwright.visual.config.mjs",
|
|
182
183
|
"demo": "pnpm build && node examples/build.mjs",
|
|
183
184
|
"format": "biome check --write .",
|
|
184
185
|
"format:check": "biome format .",
|
|
185
|
-
"check": "pnpm peers check && pnpm lint && pnpm data:check && pnpm typecheck && pnpm test && pnpm test:bundle",
|
|
186
|
+
"check": "pnpm peers check && pnpm lint && pnpm data:check && pnpm typecheck && pnpm typecheck:stories && pnpm test && pnpm test:bundle",
|
|
186
187
|
"storybook": "pnpm build && storybook dev -p 6006 --host 127.0.0.1 --no-open",
|
|
187
188
|
"build-storybook": "pnpm build && storybook build",
|
|
188
189
|
"data:catalog": "node scripts/build-data-catalog.mjs",
|
|
189
190
|
"data:check": "node scripts/build-data-catalog.mjs --check",
|
|
190
191
|
"typecheck": "pnpm build && tsc -p tsconfig.json",
|
|
192
|
+
"typecheck:stories": "tsc -p tsconfig.stories.json",
|
|
191
193
|
"data:normalize-realtor": "node scripts/normalize-realtor.mjs",
|
|
192
194
|
"data:guide": "node scripts/build-guide-data.mjs",
|
|
193
195
|
"report:guide": "node scripts/report-guide-bundle.mjs",
|
|
@@ -197,6 +199,7 @@
|
|
|
197
199
|
"audit": "pnpm audit --audit-level high",
|
|
198
200
|
"build:pages": "pnpm build && node scripts/build-pages.mjs",
|
|
199
201
|
"test:bundle": "node scripts/report-guide-bundle.mjs --check",
|
|
202
|
+
"test:pages": "node scripts/check-pages.mjs",
|
|
200
203
|
"release:notes": "node scripts/release-notes.mjs"
|
|
201
204
|
}
|
|
202
205
|
}
|