@kahwee/sf-map-svg 1.3.8 → 1.4.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 +12 -0
- package/README.md +29 -8
- package/SOURCES.md +10 -0
- package/dist/data/README.md +23 -2
- package/dist/data/analysis.d.ts +2 -0
- package/dist/data/analysis.js +3 -0
- package/dist/data/catalog.d.ts +6 -0
- package/dist/data/catalog.js +14 -0
- package/dist/data/coast.d.ts +4 -0
- package/dist/data/coast.js +3 -0
- package/dist/data/districts.d.ts +2 -0
- package/dist/data/districts.js +9 -0
- package/dist/data/highways.d.ts +4 -0
- package/dist/data/highways.js +3 -0
- package/dist/data/index.d.ts +9 -25
- package/dist/data/index.js +9 -67
- package/dist/data/landmarks.d.ts +2 -0
- package/dist/data/landmarks.js +3 -0
- package/dist/data/lookup-utils.d.ts +3 -0
- package/dist/data/lookup-utils.js +14 -0
- package/dist/data/lookup.d.ts +7 -0
- package/dist/data/lookup.js +16 -0
- package/dist/data/realtor.d.ts +4 -0
- package/dist/data/realtor.js +8 -0
- package/dist/data/roads.d.ts +2 -0
- package/dist/data/roads.js +3 -0
- package/dist/data/sf-find.d.ts +2 -0
- package/dist/data/sf-find.js +3 -0
- package/dist/data/stations.d.ts +4 -0
- package/dist/data/stations.js +3 -0
- package/dist/src/guide-data.d.ts +3 -0
- package/dist/src/guide-data.js +41 -0
- package/dist/src/guide-detailed.d.ts +3 -0
- package/dist/src/guide-detailed.js +45 -0
- package/dist/src/guide-map.d.ts +3 -0
- package/dist/src/guide-map.js +22 -0
- package/dist/src/guide.d.ts +4 -9
- package/dist/src/guide.js +3 -107
- package/dist/src/transit.js +24 -2
- package/docs/EXAMPLES.md +87 -0
- package/package.json +58 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@ User-visible changes are recorded here. Unreleased entries describe changes on `
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 1.4.1 — 2026-09-26
|
|
8
|
+
|
|
9
|
+
- Add a task-based example gallery and code recipes, with clearer routes among the Pages atlas, local measures, district history, transit, and lightweight map.
|
|
10
|
+
- Add a California proposition explorer using certified November 2024 Yes and No votes for all eleven San Francisco supervisorial districts; publish its sourced JSON and reproducible import script.
|
|
11
|
+
|
|
12
|
+
## 1.4.0 — 2026-09-26
|
|
13
|
+
|
|
14
|
+
- Split geographic convenience exports into independent data modules, so metadata search and SFAR-only lookup avoid unrelated JSON.
|
|
15
|
+
- Separate the guide's detailed loader from overview geography; keep the existing `/guide` exports and add explicit `/guide/detailed`, `/guide/data`, and `/guide/map` paths.
|
|
16
|
+
- Make the optional transit animation include only its 2022 map geography, preserving the rendered route while cutting its consumer bundle size.
|
|
17
|
+
- Publish a before-and-after tree-shaking report for representative consumer imports.
|
|
18
|
+
|
|
7
19
|
## 1.3.8 — 2026-09-26
|
|
8
20
|
|
|
9
21
|
- Give the district map a clear, dedicated viewing area on mobile by shrinking the election and measure controls and moving the map legend below the map.
|
package/README.md
CHANGED
|
@@ -2,12 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@kahwee/sf-map-svg)
|
|
4
4
|
|
|
5
|
-
[
|
|
5
|
+
[Live atlas](https://kahwee.github.io/sf-map-svg/) · [Example gallery](https://kahwee.github.io/sf-map-svg/examples.html) · [Code recipes](docs/EXAMPLES.md) · [Data guide](data/README.md) · [Sources](SOURCES.md) · [Contributing](CONTRIBUTING.md)
|
|
6
6
|
|
|
7
7
|
Self-contained SVG maps of San Francisco, with precise coastlines, soft district colors, parks, roads, BART stations, and searchable neighborhoods. Render static SVGs in Node or add an interactive map to a browser. All geometry is bundled; there are no runtime dependencies, map tiles, API keys, or external data requests.
|
|
8
8
|
|
|
9
9
|

|
|
10
10
|
|
|
11
|
+
## Choose a starting point
|
|
12
|
+
|
|
13
|
+
| Goal | Example | API |
|
|
14
|
+
| --- | --- | --- |
|
|
15
|
+
| Explore real election data | [California propositions by SF district](https://kahwee.github.io/sf-map-svg/propositions.html) or [local measures](https://kahwee.github.io/sf-map-svg/measures.html) | `custom-map` |
|
|
16
|
+
| Make a small interactive city map | [Neighborhood guide](https://kahwee.github.io/sf-map-svg/#explore-more-title) | `/guide` |
|
|
17
|
+
| Render a static or custom SVG | [Code recipes](docs/EXAMPLES.md) | Root or `/custom-map` |
|
|
18
|
+
| Animate a route | [BART journey](https://kahwee.github.io/sf-map-svg/transit.html) | `/transit` or overlays |
|
|
19
|
+
|
|
11
20
|
## Install
|
|
12
21
|
|
|
13
22
|
```sh
|
|
@@ -26,6 +35,8 @@ Use Node 22.12+ for server-side rendering. Browser components need a DOM and a b
|
|
|
26
35
|
| Lightweight guide map | `@kahwee/sf-map-svg/guide` | Curated overview geography, with detailed data loaded explicitly |
|
|
27
36
|
| Animated transit demo | `@kahwee/sf-map-svg/transit` | Optional, schematic BART journey with playback controls |
|
|
28
37
|
| Geographic data | `@kahwee/sf-map-svg/data` | Source-aware lookup and canonical GeoJSON |
|
|
38
|
+
| Metadata search | `@kahwee/sf-map-svg/data/catalog` | Search names without polygon geometry |
|
|
39
|
+
| SFAR lookup | `@kahwee/sf-map-svg/data/realtor` | Default neighborhoods without alternative sources |
|
|
29
40
|
|
|
30
41
|
## Render a static map
|
|
31
42
|
|
|
@@ -53,6 +64,14 @@ road, park, and station arrays. Use the canonical JSON subpaths documented in
|
|
|
53
64
|
corresponding `SFMapData` records. The root entry remains convenient and includes the
|
|
54
65
|
built-in datasets for backward compatibility.
|
|
55
66
|
|
|
67
|
+
In tree-shaking bundlers, importing a single symbol from `/data` retains only the
|
|
68
|
+
modules that symbol uses.
|
|
69
|
+
`searchNeighborhoods` needs catalog metadata but no polygon geometry. The synchronous
|
|
70
|
+
source-switching `getNeighborhood` still needs all three neighborhood collections;
|
|
71
|
+
use `/data/realtor` when only the default SFAR definitions are needed. Optional coast,
|
|
72
|
+
district, road, park, and station collections also have independent `/data/*` entries.
|
|
73
|
+
See [the measured bundle report](docs/module-bundle-report.md).
|
|
74
|
+
|
|
56
75
|
The same split is available for browser controls with
|
|
57
76
|
`@kahwee/sf-map-svg/interactive-data`. Pass an `InteractiveSFMapData` object containing
|
|
58
77
|
`map` (the `SFMapData` used by the static renderer) and only the neighborhood collections
|
|
@@ -69,7 +88,8 @@ have independent `layers.keyRoads` and `layers.roadLabels` controls. Lombard geo
|
|
|
69
88
|
label appear after 1.8× zoom; major street labels can appear at city scale in a smaller type size.
|
|
70
89
|
|
|
71
90
|
```ts
|
|
72
|
-
import { createGuideMap
|
|
91
|
+
import { createGuideMap } from '@kahwee/sf-map-svg/guide';
|
|
92
|
+
import { loadGuideDetailedData } from '@kahwee/sf-map-svg/guide/detailed';
|
|
73
93
|
import { createInteractiveSFMapWithData } from '@kahwee/sf-map-svg/interactive-data';
|
|
74
94
|
|
|
75
95
|
const map = createGuideMap({ layers: { roadLabels: false } });
|
|
@@ -85,6 +105,8 @@ const detailedMap = createInteractiveSFMapWithData(detailedData, {
|
|
|
85
105
|
|
|
86
106
|
Run `pnpm data:guide` to rebuild the subpixel overview geometries from canonical files.
|
|
87
107
|
`docs/guide-bundle-report.md` records compressed bundle sizes and verifies the included data.
|
|
108
|
+
The detailed loader is also re-exported from `/guide` for compatibility; importing only
|
|
109
|
+
that function no longer puts overview geography in the initial bundle.
|
|
88
110
|
|
|
89
111
|
```js
|
|
90
112
|
import coast from '@kahwee/sf-map-svg/data/coast.json' with { type: 'json' };
|
|
@@ -328,13 +350,12 @@ chooser to reach obscured markers. Automated clustering is not included in this
|
|
|
328
350
|
All map geometry is available through stable JSON package exports. There are three district files (2002, 2012, 2022), the full 117 SF Find neighborhoods, 41 analysis neighborhoods, 92 realtor-defined areas, and separate coastline, highway, landmark, and BART files. Neighborhood records include a canonical display name, exact source name, stable ID, aliases where documented, source definition, and full polygon geometry.
|
|
329
351
|
|
|
330
352
|
```js
|
|
331
|
-
import
|
|
332
|
-
import
|
|
333
|
-
import { getNeighborhood, searchNeighborhoods } from '@kahwee/sf-map-svg/data';
|
|
353
|
+
import { getRealtorNeighborhood } from '@kahwee/sf-map-svg/data/realtor';
|
|
354
|
+
import { searchNeighborhoods } from '@kahwee/sf-map-svg/data/catalog';
|
|
334
355
|
|
|
335
|
-
const mission =
|
|
336
|
-
const outerMission =
|
|
337
|
-
const nopa =
|
|
356
|
+
const mission = getRealtorNeighborhood('Inner Mission');
|
|
357
|
+
const outerMission = getRealtorNeighborhood('Outer Mission');
|
|
358
|
+
const nopa = getRealtorNeighborhood('NoPa');
|
|
338
359
|
const matchingDefinitions = searchNeighborhoods('mission');
|
|
339
360
|
```
|
|
340
361
|
|
package/SOURCES.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Geometry and provenance
|
|
2
2
|
|
|
3
|
+
## California propositions in San Francisco
|
|
4
|
+
|
|
5
|
+
Downloaded September 26, 2026 from the San Francisco Department of Elections' [final November 5, 2024 results](https://sfelections.org/results/20241105w/detail.html):
|
|
6
|
+
|
|
7
|
+
- [Certified district statement of vote workbook](https://www.sfelections.org/results/20241105/data/20241203/dsov.xlsx), SHA-256 `9bf9c7d767273e73b3cdf5b98086412cbe354b0928ee6abb0e379deb0353c403`
|
|
8
|
+
- [Certification letter](https://www.sfelections.org/results/20241105/data/20241203/CertificationLetterNov52024.pdf)
|
|
9
|
+
- [California Secretary of State statewide Statement of Vote](https://www.sos.ca.gov/elections/prior-elections/statewide-election-results/general-election-nov-5-2024/statement-vote) for statewide context
|
|
10
|
+
|
|
11
|
+
`data/propositions/2024-11-05.json` contains Yes and No counts for all ten state propositions, grouped by San Francisco supervisorial district. The short titles are editorial navigation labels. `scripts/import-2024-propositions.py` reads the final workbook's proposition sheets and checks that all eleven district totals sum exactly to its San Francisco citywide totals. Yes share divides Yes by Yes plus No, excluding undervotes and overvotes. The data describes where ballots were cast within San Francisco, not individual voters or statewide outcomes. Its 2022 district geography matches the election's district vintage.
|
|
12
|
+
|
|
3
13
|
## Map extraction
|
|
4
14
|
|
|
5
15
|
District geometry, coast, label anchors, highway geometry, and the pastel district palette were extracted from the San Francisco District Map on September 25, 2026. Only map rendering and public geographic data are distributed. Site account identifiers, original application code, and ballot overlays are excluded.
|
package/dist/data/README.md
CHANGED
|
@@ -33,7 +33,28 @@ const mission = neighborhoods.features.find((f) => f.id === 'inner-mission');
|
|
|
33
33
|
console.log(mission.properties.canonicalName, mission.geometry);
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
Node 22.12+ supports this syntax. Bundlers may also support JSON imports without the import attribute. Files are included in the package archive; non-JavaScript consumers can parse the same JSON files directly. These are package entry points, not a hosted API. The public repository and GitHub Pages explorer also provide access to the source data. Consumers who need only one dataset
|
|
36
|
+
Node 22.12+ supports this syntax. Bundlers may also support JSON imports without the import attribute. Files are included in the package archive; non-JavaScript consumers can parse the same JSON files directly. These are package entry points, not a hosted API. The public repository and GitHub Pages explorer also provide access to the source data. Consumers who need only one dataset can import its JSON subpath or a typed, frozen leaf module.
|
|
37
|
+
|
|
38
|
+
## Import only the data you need
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
import { searchNeighborhoods } from '@kahwee/sf-map-svg/data/catalog';
|
|
42
|
+
import { getRealtorNeighborhood } from '@kahwee/sf-map-svg/data/realtor';
|
|
43
|
+
import { coast } from '@kahwee/sf-map-svg/data/coast';
|
|
44
|
+
|
|
45
|
+
const candidates = searchNeighborhoods('mission'); // metadata, no polygons
|
|
46
|
+
const mission = getRealtorNeighborhood('Inner Mission'); // SFAR only
|
|
47
|
+
const coastline = coast.features[0].geometry;
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The `/data` entry remains compatible and re-exports these helpers and collections.
|
|
51
|
+
Its `getNeighborhood(name, { source })` imports all three neighborhood definitions
|
|
52
|
+
because source switching is synchronous. Importing only `searchNeighborhoods` from
|
|
53
|
+
`/data` now includes catalog metadata only in tree-shaking bundles. Other independent modules are
|
|
54
|
+
`/data/sf-find`, `/data/analysis`, `/data/districts`, `/data/highways`,
|
|
55
|
+
`/data/landmarks`, `/data/roads`, and `/data/stations`. Districts still groups all
|
|
56
|
+
three supported years; import `districts-2022.json` directly when only one year is
|
|
57
|
+
needed. See [the consumer bundle report](../docs/module-bundle-report.md).
|
|
37
58
|
|
|
38
59
|
## Names and definitions
|
|
39
60
|
|
|
@@ -69,7 +90,7 @@ const allAnalysisPolygons = neighborhoodCollections.analysis;
|
|
|
69
90
|
const historicalDistricts = districtMaps[2012];
|
|
70
91
|
```
|
|
71
92
|
|
|
72
|
-
Lookup is exact after case/punctuation normalization, returns `undefined` for an unknown name, and throws for an unknown source. Search matches substrings and preserves every source-specific result. The convenience
|
|
93
|
+
Lookup is exact after case/punctuation normalization, returns `undefined` for an unknown name, and throws for an unknown source. Search matches substrings and preserves every source-specific result. The convenience and leaf modules' shared data is deeply frozen; use `structuredClone(feature)` if you need an editable copy. Do not mutate imported datasets used by the renderer.
|
|
73
94
|
|
|
74
95
|
## Draw a neighborhood with the existing projection
|
|
75
96
|
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Catalog, NeighborhoodSource } from './types.js';
|
|
2
|
+
export declare const catalog: Catalog;
|
|
3
|
+
/** Search name metadata across sources without importing polygon geometry. */
|
|
4
|
+
export declare function searchNeighborhoods(query?: string, { source }?: {
|
|
5
|
+
source?: NeighborhoodSource;
|
|
6
|
+
}): import("./types.js").NeighborhoodEntry[];
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { deepFreeze } from '../src/immutable.js';
|
|
2
|
+
import catalogData from './catalog.json' with { type: 'json' };
|
|
3
|
+
import { normalizeNeighborhoodName } from './lookup-utils.js';
|
|
4
|
+
export const catalog = deepFreeze(catalogData);
|
|
5
|
+
/** Search name metadata across sources without importing polygon geometry. */
|
|
6
|
+
export function searchNeighborhoods(query = '', { source } = {}) {
|
|
7
|
+
if (source !== undefined && !['sf-find', 'analysis', 'realtor'].includes(source))
|
|
8
|
+
throw new RangeError(`Unknown neighborhood source: ${source}`);
|
|
9
|
+
if (typeof query !== 'string')
|
|
10
|
+
throw new TypeError('Neighborhood query must be a string.');
|
|
11
|
+
const term = normalizeNeighborhoodName(query);
|
|
12
|
+
return catalog.neighborhoods.filter((entry) => (source === undefined || entry.source === source) &&
|
|
13
|
+
[entry.id, entry.canonicalName, entry.sourceName, ...entry.aliases].some((value) => normalizeNeighborhoodName(value).includes(term)));
|
|
14
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { deepFreeze } from '../src/immutable.js';
|
|
2
|
+
import districts2002 from './districts-2002.json' with { type: 'json' };
|
|
3
|
+
import districts2012 from './districts-2012.json' with { type: 'json' };
|
|
4
|
+
import districts2022 from './districts-2022.json' with { type: 'json' };
|
|
5
|
+
export const districtMaps = deepFreeze({
|
|
6
|
+
2002: districts2002,
|
|
7
|
+
2012: districts2012,
|
|
8
|
+
2022: districts2022,
|
|
9
|
+
});
|
package/dist/data/index.d.ts
CHANGED
|
@@ -1,26 +1,10 @@
|
|
|
1
|
+
export { catalog, searchNeighborhoods } from './catalog.js';
|
|
2
|
+
export { coast } from './coast.js';
|
|
3
|
+
export { districtMaps } from './districts.js';
|
|
4
|
+
export { highways } from './highways.js';
|
|
5
|
+
export { landmarks } from './landmarks.js';
|
|
6
|
+
export { getNeighborhood, neighborhoodCollections, neighborhoodSources } from './lookup.js';
|
|
7
|
+
export { getRealtorNeighborhood, neighborhoods } from './realtor.js';
|
|
8
|
+
export { keyRoads } from './roads.js';
|
|
9
|
+
export { bartStations } from './stations.js';
|
|
1
10
|
export type * from './types.js';
|
|
2
|
-
import type { Catalog, DistrictProperties, Feature, FeatureCollection, KeyRoadProperties, LandmarkProperties, NeighborhoodProperties, NeighborhoodSource } from './types.js';
|
|
3
|
-
export declare const coast: FeatureCollection<{
|
|
4
|
-
readonly name: string;
|
|
5
|
-
}>;
|
|
6
|
-
export declare const districtMaps: Readonly<Record<2002 | 2012 | 2022, FeatureCollection<DistrictProperties>>>;
|
|
7
|
-
export declare const neighborhoods: FeatureCollection<NeighborhoodProperties>;
|
|
8
|
-
export declare const neighborhoodCollections: Readonly<Record<NeighborhoodSource, FeatureCollection<NeighborhoodProperties>>>;
|
|
9
|
-
export declare const neighborhoodSources: readonly NeighborhoodSource[];
|
|
10
|
-
export declare const highways: FeatureCollection<{
|
|
11
|
-
readonly route: string;
|
|
12
|
-
}>;
|
|
13
|
-
export declare const landmarks: FeatureCollection<LandmarkProperties>;
|
|
14
|
-
export declare const bartStations: FeatureCollection<{
|
|
15
|
-
readonly name: string;
|
|
16
|
-
}>;
|
|
17
|
-
export declare const catalog: Catalog;
|
|
18
|
-
/** Exact ID, canonical name, source name, or alias lookup within one definition set. */
|
|
19
|
-
export declare function getNeighborhood(name: string, { source }?: {
|
|
20
|
-
source?: NeighborhoodSource;
|
|
21
|
-
}): Feature<NeighborhoodProperties> | undefined;
|
|
22
|
-
/** Search names across sources; results keep their source identity and omit geometry. */
|
|
23
|
-
export declare function searchNeighborhoods(query?: string, { source }?: {
|
|
24
|
-
source?: NeighborhoodSource;
|
|
25
|
-
}): import("./types.js").NeighborhoodEntry[];
|
|
26
|
-
export declare const keyRoads: FeatureCollection<KeyRoadProperties>;
|
package/dist/data/index.js
CHANGED
|
@@ -1,67 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import landmarkData from './landmarks.json' with { type: 'json' };
|
|
11
|
-
import sfFind from './neighborhoods.json' with { type: 'json' };
|
|
12
|
-
import analysis from './neighborhoods-analysis.json' with { type: 'json' };
|
|
13
|
-
import realtor from './neighborhoods-realtor.json' with { type: 'json' };
|
|
14
|
-
export const coast = deepFreeze(coastData);
|
|
15
|
-
export const districtMaps = deepFreeze({
|
|
16
|
-
2002: districts2002,
|
|
17
|
-
2012: districts2012,
|
|
18
|
-
2022: districts2022,
|
|
19
|
-
});
|
|
20
|
-
export const neighborhoods = deepFreeze(realtor);
|
|
21
|
-
export const neighborhoodCollections = deepFreeze({
|
|
22
|
-
'sf-find': sfFind,
|
|
23
|
-
analysis,
|
|
24
|
-
realtor,
|
|
25
|
-
});
|
|
26
|
-
export const neighborhoodSources = Object.freeze(Object.keys(neighborhoodCollections));
|
|
27
|
-
export const highways = deepFreeze(highwayData);
|
|
28
|
-
export const landmarks = deepFreeze(landmarkData);
|
|
29
|
-
export const bartStations = deepFreeze(stationData);
|
|
30
|
-
export const catalog = deepFreeze(catalogData);
|
|
31
|
-
const normalize = (value) => value
|
|
32
|
-
.normalize('NFKD')
|
|
33
|
-
.toLowerCase()
|
|
34
|
-
.replace(/[\u0300-\u036f'’]/g, '')
|
|
35
|
-
.replace(/[^a-z0-9]+/g, ' ')
|
|
36
|
-
.trim();
|
|
37
|
-
function checkSource(source) {
|
|
38
|
-
if (!Object.hasOwn(neighborhoodCollections, source))
|
|
39
|
-
throw new RangeError(`Unknown neighborhood source: ${source}`);
|
|
40
|
-
}
|
|
41
|
-
const names = (feature) => [
|
|
42
|
-
feature.id,
|
|
43
|
-
feature.properties.canonicalName,
|
|
44
|
-
feature.properties.sourceName,
|
|
45
|
-
...feature.properties.aliases,
|
|
46
|
-
];
|
|
47
|
-
/** Exact ID, canonical name, source name, or alias lookup within one definition set. */
|
|
48
|
-
export function getNeighborhood(name, { source = 'realtor' } = {}) {
|
|
49
|
-
checkSource(source);
|
|
50
|
-
if (typeof name !== 'string')
|
|
51
|
-
throw new TypeError('Neighborhood name must be a string.');
|
|
52
|
-
const query = normalize(name);
|
|
53
|
-
if (!query)
|
|
54
|
-
return undefined;
|
|
55
|
-
return neighborhoodCollections[source].features.find((feature) => names(feature).some((value) => normalize(value) === query));
|
|
56
|
-
}
|
|
57
|
-
/** Search names across sources; results keep their source identity and omit geometry. */
|
|
58
|
-
export function searchNeighborhoods(query = '', { source } = {}) {
|
|
59
|
-
if (source !== undefined)
|
|
60
|
-
checkSource(source);
|
|
61
|
-
if (typeof query !== 'string')
|
|
62
|
-
throw new TypeError('Neighborhood query must be a string.');
|
|
63
|
-
const term = normalize(query);
|
|
64
|
-
return catalog.neighborhoods.filter((entry) => (source === undefined || entry.source === source) &&
|
|
65
|
-
[entry.id, entry.canonicalName, entry.sourceName, ...entry.aliases].some((value) => normalize(value).includes(term)));
|
|
66
|
-
}
|
|
67
|
-
export const keyRoads = deepFreeze(roadData);
|
|
1
|
+
export { catalog, searchNeighborhoods } from './catalog.js';
|
|
2
|
+
export { coast } from './coast.js';
|
|
3
|
+
export { districtMaps } from './districts.js';
|
|
4
|
+
export { highways } from './highways.js';
|
|
5
|
+
export { landmarks } from './landmarks.js';
|
|
6
|
+
export { getNeighborhood, neighborhoodCollections, neighborhoodSources } from './lookup.js';
|
|
7
|
+
export { getRealtorNeighborhood, neighborhoods } from './realtor.js';
|
|
8
|
+
export { keyRoads } from './roads.js';
|
|
9
|
+
export { bartStations } from './stations.js';
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { Feature, FeatureCollection, NeighborhoodProperties } from './types.js';
|
|
2
|
+
export declare const normalizeNeighborhoodName: (value: string) => string;
|
|
3
|
+
export declare function findNeighborhood(name: string, collection: FeatureCollection<NeighborhoodProperties>): Feature<NeighborhoodProperties> | undefined;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const normalizeNeighborhoodName = (value) => value
|
|
2
|
+
.normalize('NFKD')
|
|
3
|
+
.toLowerCase()
|
|
4
|
+
.replace(/[\u0300-\u036f'’]/g, '')
|
|
5
|
+
.replace(/[^a-z0-9]+/g, ' ')
|
|
6
|
+
.trim();
|
|
7
|
+
export function findNeighborhood(name, collection) {
|
|
8
|
+
if (typeof name !== 'string')
|
|
9
|
+
throw new TypeError('Neighborhood name must be a string.');
|
|
10
|
+
const query = normalizeNeighborhoodName(name);
|
|
11
|
+
if (!query)
|
|
12
|
+
return undefined;
|
|
13
|
+
return collection.features.find(({ id, properties }) => [id, properties.canonicalName, properties.sourceName, ...properties.aliases].some((value) => normalizeNeighborhoodName(value) === query));
|
|
14
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FeatureCollection, NeighborhoodProperties, NeighborhoodSource } from './types.js';
|
|
2
|
+
export declare const neighborhoodCollections: Readonly<Record<NeighborhoodSource, FeatureCollection<NeighborhoodProperties>>>;
|
|
3
|
+
export declare const neighborhoodSources: readonly NeighborhoodSource[];
|
|
4
|
+
/** Exact ID, canonical name, source name, or alias lookup within one definition set. */
|
|
5
|
+
export declare function getNeighborhood(name: string, { source }?: {
|
|
6
|
+
source?: NeighborhoodSource;
|
|
7
|
+
}): import("./types.js").Feature<NeighborhoodProperties> | undefined;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { analysisNeighborhoods } from './analysis.js';
|
|
2
|
+
import { findNeighborhood } from './lookup-utils.js';
|
|
3
|
+
import { neighborhoods } from './realtor.js';
|
|
4
|
+
import { sfFindNeighborhoods } from './sf-find.js';
|
|
5
|
+
export const neighborhoodCollections = Object.freeze({
|
|
6
|
+
'sf-find': sfFindNeighborhoods,
|
|
7
|
+
analysis: analysisNeighborhoods,
|
|
8
|
+
realtor: neighborhoods,
|
|
9
|
+
});
|
|
10
|
+
export const neighborhoodSources = Object.freeze(Object.keys(neighborhoodCollections));
|
|
11
|
+
/** Exact ID, canonical name, source name, or alias lookup within one definition set. */
|
|
12
|
+
export function getNeighborhood(name, { source = 'realtor' } = {}) {
|
|
13
|
+
if (!Object.hasOwn(neighborhoodCollections, source))
|
|
14
|
+
throw new RangeError(`Unknown neighborhood source: ${source}`);
|
|
15
|
+
return findNeighborhood(name, neighborhoodCollections[source]);
|
|
16
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { FeatureCollection, NeighborhoodProperties } from './types.js';
|
|
2
|
+
export declare const neighborhoods: FeatureCollection<NeighborhoodProperties>;
|
|
3
|
+
/** Look up one SFAR area without importing the other neighborhood definitions. */
|
|
4
|
+
export declare function getRealtorNeighborhood(name: string): import("./types.js").Feature<NeighborhoodProperties> | undefined;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { deepFreeze } from '../src/immutable.js';
|
|
2
|
+
import { findNeighborhood } from './lookup-utils.js';
|
|
3
|
+
import realtorData from './neighborhoods-realtor.json' with { type: 'json' };
|
|
4
|
+
export const neighborhoods = deepFreeze(realtorData);
|
|
5
|
+
/** Look up one SFAR area without importing the other neighborhood definitions. */
|
|
6
|
+
export function getRealtorNeighborhood(name) {
|
|
7
|
+
return findNeighborhood(name, neighborhoods);
|
|
8
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import bart from '../data/guide/bart-stations.json' with { type: 'json' };
|
|
2
|
+
import coast from '../data/guide/coast.json' with { type: 'json' };
|
|
3
|
+
import highways from '../data/guide/highways.json' with { type: 'json' };
|
|
4
|
+
import roads from '../data/guide/key-roads.json' with { type: 'json' };
|
|
5
|
+
import landmarks from '../data/guide/landmarks.json' with { type: 'json' };
|
|
6
|
+
import neighborhoods from '../data/guide/neighborhoods-realtor.json' with { type: 'json' };
|
|
7
|
+
const neighborhoodData = neighborhoods;
|
|
8
|
+
const highwayData = highways;
|
|
9
|
+
const parkData = landmarks;
|
|
10
|
+
const roadData = roads;
|
|
11
|
+
const stationData = bart;
|
|
12
|
+
/** Compact, source-aware geography with no district or alternate-neighborhood files. */
|
|
13
|
+
export const guideMapData = {
|
|
14
|
+
map: {
|
|
15
|
+
coast: coast.features[0].geometry,
|
|
16
|
+
neighborhoods: neighborhoodData.features.map((feature) => ({
|
|
17
|
+
name: feature.properties.canonicalName,
|
|
18
|
+
geometry: feature.geometry,
|
|
19
|
+
})),
|
|
20
|
+
highways: highwayData.features.map((feature) => ({
|
|
21
|
+
route: String(feature.properties.route),
|
|
22
|
+
geometry: feature.geometry,
|
|
23
|
+
})),
|
|
24
|
+
landmarks: parkData.features.map(({ id, properties, geometry }) => ({
|
|
25
|
+
id,
|
|
26
|
+
...properties,
|
|
27
|
+
geometry,
|
|
28
|
+
})),
|
|
29
|
+
keyRoads: roadData.features.map(({ id, properties, geometry }) => ({
|
|
30
|
+
id,
|
|
31
|
+
...properties,
|
|
32
|
+
sourceNames: properties.sourceNames ?? [],
|
|
33
|
+
segmentIds: properties.segmentIds ?? [],
|
|
34
|
+
geometry,
|
|
35
|
+
})),
|
|
36
|
+
bartStations: stationData.features.flatMap(({ id, properties, geometry }) => geometry.type === 'Point'
|
|
37
|
+
? [{ id, name: properties.name, coordinates: geometry.coordinates }]
|
|
38
|
+
: []),
|
|
39
|
+
},
|
|
40
|
+
neighborhoods: { realtor: neighborhoodData },
|
|
41
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Load detailed selected geography only after the caller requests it. */
|
|
2
|
+
export async function loadGuideDetailedData() {
|
|
3
|
+
const [coastData, neighborhoodData, parkData, highwayData, roadData, bartData] = await Promise.all([
|
|
4
|
+
import('../data/coast.json', { with: { type: 'json' } }),
|
|
5
|
+
import('../data/neighborhoods-realtor.json', { with: { type: 'json' } }),
|
|
6
|
+
import('../data/landmarks.json', { with: { type: 'json' } }),
|
|
7
|
+
import('../data/guide/highways-detailed.json', { with: { type: 'json' } }),
|
|
8
|
+
import('../data/key-roads.json', { with: { type: 'json' } }),
|
|
9
|
+
import('../data/bart-stations.json', { with: { type: 'json' } }),
|
|
10
|
+
]);
|
|
11
|
+
const detailedNeighborhoods = neighborhoodData.default;
|
|
12
|
+
const detailedRoads = roadData.default;
|
|
13
|
+
const detailedHighways = highwayData.default;
|
|
14
|
+
const detailedParks = parkData.default;
|
|
15
|
+
const detailedStations = bartData.default;
|
|
16
|
+
return {
|
|
17
|
+
map: {
|
|
18
|
+
coast: coastData.default.features[0].geometry,
|
|
19
|
+
neighborhoods: detailedNeighborhoods.features.map((feature) => ({
|
|
20
|
+
name: feature.properties.canonicalName,
|
|
21
|
+
geometry: feature.geometry,
|
|
22
|
+
})),
|
|
23
|
+
highways: detailedHighways.features
|
|
24
|
+
.filter((feature) => ['1', '101', '280'].includes(String(feature.properties.route)))
|
|
25
|
+
.map((feature) => ({
|
|
26
|
+
route: String(feature.properties.route),
|
|
27
|
+
geometry: feature.geometry,
|
|
28
|
+
})),
|
|
29
|
+
landmarks: detailedParks.features.map(({ id, properties, geometry }) => ({
|
|
30
|
+
id,
|
|
31
|
+
...properties,
|
|
32
|
+
geometry,
|
|
33
|
+
})),
|
|
34
|
+
keyRoads: detailedRoads.features.map(({ id, properties, geometry }) => ({
|
|
35
|
+
id,
|
|
36
|
+
...properties,
|
|
37
|
+
geometry,
|
|
38
|
+
})),
|
|
39
|
+
bartStations: detailedStations.features.flatMap(({ id, properties, geometry }) => geometry.type === 'Point'
|
|
40
|
+
? [{ id, name: properties.name, coordinates: geometry.coordinates }]
|
|
41
|
+
: []),
|
|
42
|
+
},
|
|
43
|
+
neighborhoods: { realtor: detailedNeighborhoods },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { InteractiveSFMapElement, InteractiveSFMapOptions } from './interactive-data.js';
|
|
2
|
+
/** Guide map preset: SFAR neighborhoods, major parks, BART, and curated roads. */
|
|
3
|
+
export declare function createGuideMap(options?: InteractiveSFMapOptions): InteractiveSFMapElement;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { guideMapData } from './guide-data.js';
|
|
2
|
+
import { createInteractiveSFMapWithData } from './interactive-data.js';
|
|
3
|
+
/** Guide map preset: SFAR neighborhoods, major parks, BART, and curated roads. */
|
|
4
|
+
export function createGuideMap(options = {}) {
|
|
5
|
+
return createInteractiveSFMapWithData(guideMapData, {
|
|
6
|
+
mode: 'neighborhoods',
|
|
7
|
+
...options,
|
|
8
|
+
layers: {
|
|
9
|
+
districtFills: false,
|
|
10
|
+
districtLines: false,
|
|
11
|
+
districtLabels: false,
|
|
12
|
+
neighborhoodLines: true,
|
|
13
|
+
neighborhoodLabels: true,
|
|
14
|
+
landmarks: true,
|
|
15
|
+
bartStations: true,
|
|
16
|
+
highways: true,
|
|
17
|
+
keyRoads: true,
|
|
18
|
+
roadLabels: true,
|
|
19
|
+
...options.layers,
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
}
|
package/dist/src/guide.d.ts
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
import type { InteractiveSFMapData } from './explorer-data.js';
|
|
2
|
-
import type { InteractiveSFMapElement, InteractiveSFMapOptions } from './interactive-data.js';
|
|
3
|
-
/** Compact, source-aware San Francisco guide data. Imports no district or alternate-neighborhood files. */
|
|
4
|
-
export declare const guideMapData: InteractiveSFMapData;
|
|
5
|
-
/** Guide map preset: SFAR neighborhoods, major parks, BART, and curated roads. */
|
|
6
|
-
export declare function createGuideMap(options?: InteractiveSFMapOptions): InteractiveSFMapElement;
|
|
7
|
-
/** Load the detailed versions of the guide's selected geography on explicit request. */
|
|
8
|
-
export declare function loadGuideDetailedData(): Promise<InteractiveSFMapData>;
|
|
9
1
|
export type { InteractiveSFMapData } from './explorer-data.js';
|
|
10
|
-
export
|
|
2
|
+
export { guideMapData } from './guide-data.js';
|
|
3
|
+
export { loadGuideDetailedData } from './guide-detailed.js';
|
|
4
|
+
export { createGuideMap } from './guide-map.js';
|
|
5
|
+
export type { InteractiveSFMapElement, InteractiveSFMapOptions } from './interactive-data.js';
|
package/dist/src/guide.js
CHANGED
|
@@ -1,107 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import roads from '../data/guide/key-roads.json' with { type: 'json' };
|
|
5
|
-
import landmarks from '../data/guide/landmarks.json' with { type: 'json' };
|
|
6
|
-
import neighborhoods from '../data/guide/neighborhoods-realtor.json' with { type: 'json' };
|
|
7
|
-
import { createInteractiveSFMapWithData } from './interactive-data.js';
|
|
8
|
-
const neighborhoodData = neighborhoods;
|
|
9
|
-
const highwayData = highways;
|
|
10
|
-
const parkData = landmarks;
|
|
11
|
-
const roadData = roads;
|
|
12
|
-
const stationData = bart;
|
|
13
|
-
/** Compact, source-aware San Francisco guide data. Imports no district or alternate-neighborhood files. */
|
|
14
|
-
export const guideMapData = {
|
|
15
|
-
map: {
|
|
16
|
-
coast: coast.features[0].geometry,
|
|
17
|
-
neighborhoods: neighborhoodData.features.map((feature) => ({
|
|
18
|
-
name: feature.properties.canonicalName,
|
|
19
|
-
geometry: feature.geometry,
|
|
20
|
-
})),
|
|
21
|
-
highways: highwayData.features.map((feature) => ({
|
|
22
|
-
route: String(feature.properties.route),
|
|
23
|
-
geometry: feature.geometry,
|
|
24
|
-
})),
|
|
25
|
-
landmarks: parkData.features.map(({ id, properties, geometry }) => ({
|
|
26
|
-
id,
|
|
27
|
-
...properties,
|
|
28
|
-
geometry,
|
|
29
|
-
})),
|
|
30
|
-
keyRoads: roadData.features.map(({ id, properties, geometry }) => ({
|
|
31
|
-
id,
|
|
32
|
-
...properties,
|
|
33
|
-
sourceNames: properties.sourceNames ?? [],
|
|
34
|
-
segmentIds: properties.segmentIds ?? [],
|
|
35
|
-
geometry,
|
|
36
|
-
})),
|
|
37
|
-
bartStations: stationData.features.flatMap(({ id, properties, geometry }) => geometry.type === 'Point'
|
|
38
|
-
? [{ id, name: properties.name, coordinates: geometry.coordinates }]
|
|
39
|
-
: []),
|
|
40
|
-
},
|
|
41
|
-
neighborhoods: { realtor: neighborhoodData },
|
|
42
|
-
};
|
|
43
|
-
/** Guide map preset: SFAR neighborhoods, major parks, BART, and curated roads. */
|
|
44
|
-
export function createGuideMap(options = {}) {
|
|
45
|
-
return createInteractiveSFMapWithData(guideMapData, {
|
|
46
|
-
mode: 'neighborhoods',
|
|
47
|
-
...options,
|
|
48
|
-
layers: {
|
|
49
|
-
districtFills: false,
|
|
50
|
-
districtLines: false,
|
|
51
|
-
districtLabels: false,
|
|
52
|
-
neighborhoodLines: true,
|
|
53
|
-
neighborhoodLabels: true,
|
|
54
|
-
landmarks: true,
|
|
55
|
-
bartStations: true,
|
|
56
|
-
highways: true,
|
|
57
|
-
keyRoads: true,
|
|
58
|
-
roadLabels: true,
|
|
59
|
-
...options.layers,
|
|
60
|
-
},
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
/** Load the detailed versions of the guide's selected geography on explicit request. */
|
|
64
|
-
export async function loadGuideDetailedData() {
|
|
65
|
-
const [coastData, neighborhoodData, parkData, highwayData, roadData, bartData] = await Promise.all([
|
|
66
|
-
import('../data/coast.json', { with: { type: 'json' } }),
|
|
67
|
-
import('../data/neighborhoods-realtor.json', { with: { type: 'json' } }),
|
|
68
|
-
import('../data/landmarks.json', { with: { type: 'json' } }),
|
|
69
|
-
import('../data/guide/highways-detailed.json', { with: { type: 'json' } }),
|
|
70
|
-
import('../data/key-roads.json', { with: { type: 'json' } }),
|
|
71
|
-
import('../data/bart-stations.json', { with: { type: 'json' } }),
|
|
72
|
-
]);
|
|
73
|
-
const detailedNeighborhoods = neighborhoodData.default;
|
|
74
|
-
const detailedRoads = roadData.default;
|
|
75
|
-
const detailedHighways = highwayData.default;
|
|
76
|
-
const detailedParks = parkData.default;
|
|
77
|
-
const detailedStations = bartData.default;
|
|
78
|
-
return {
|
|
79
|
-
map: {
|
|
80
|
-
coast: coastData.default.features[0].geometry,
|
|
81
|
-
neighborhoods: detailedNeighborhoods.features.map((feature) => ({
|
|
82
|
-
name: feature.properties.canonicalName,
|
|
83
|
-
geometry: feature.geometry,
|
|
84
|
-
})),
|
|
85
|
-
highways: detailedHighways.features
|
|
86
|
-
.filter((feature) => ['1', '101', '280'].includes(String(feature.properties.route)))
|
|
87
|
-
.map((feature) => ({
|
|
88
|
-
route: String(feature.properties.route),
|
|
89
|
-
geometry: feature.geometry,
|
|
90
|
-
})),
|
|
91
|
-
landmarks: detailedParks.features.map(({ id, properties, geometry }) => ({
|
|
92
|
-
id,
|
|
93
|
-
...properties,
|
|
94
|
-
geometry,
|
|
95
|
-
})),
|
|
96
|
-
keyRoads: detailedRoads.features.map(({ id, properties, geometry }) => ({
|
|
97
|
-
id,
|
|
98
|
-
...properties,
|
|
99
|
-
geometry,
|
|
100
|
-
})),
|
|
101
|
-
bartStations: detailedStations.features.flatMap(({ id, properties, geometry }) => geometry.type === 'Point'
|
|
102
|
-
? [{ id, name: properties.name, coordinates: geometry.coordinates }]
|
|
103
|
-
: []),
|
|
104
|
-
},
|
|
105
|
-
neighborhoods: { realtor: detailedNeighborhoods },
|
|
106
|
-
};
|
|
107
|
-
}
|
|
1
|
+
export { guideMapData } from './guide-data.js';
|
|
2
|
+
export { loadGuideDetailedData } from './guide-detailed.js';
|
|
3
|
+
export { createGuideMap } from './guide-map.js';
|
package/dist/src/transit.js
CHANGED
|
@@ -1,12 +1,34 @@
|
|
|
1
1
|
import stations from '../data/bart-stations.json' with { type: 'json' };
|
|
2
|
-
import
|
|
2
|
+
import coast from '../data/coast.json' with { type: 'json' };
|
|
3
|
+
import districts from '../data/districts-2022.json' with { type: 'json' };
|
|
4
|
+
import parks from '../data/landmarks.json' with { type: 'json' };
|
|
5
|
+
import { createSFMapWithData } from './map-core.js';
|
|
6
|
+
const districtData = districts;
|
|
7
|
+
const parkData = parks;
|
|
8
|
+
const transitMapData = {
|
|
9
|
+
coast: coast.features[0].geometry,
|
|
10
|
+
districts: {
|
|
11
|
+
2022: districtData.features.map(({ geometry, properties }) => ({
|
|
12
|
+
id: properties.district,
|
|
13
|
+
label: properties.label,
|
|
14
|
+
labelPoints: properties.labelPoints,
|
|
15
|
+
geometry,
|
|
16
|
+
extras: properties.displayExtras,
|
|
17
|
+
})),
|
|
18
|
+
},
|
|
19
|
+
landmarks: parkData.features.map(({ id, properties, geometry }) => ({
|
|
20
|
+
id,
|
|
21
|
+
...properties,
|
|
22
|
+
geometry,
|
|
23
|
+
})),
|
|
24
|
+
};
|
|
3
25
|
/** A deliberately schematic, offline station-to-station animation. Starts paused. */
|
|
4
26
|
export function createTransitAnimation() {
|
|
5
27
|
const host = Object.assign(document.createElement('div'), {
|
|
6
28
|
destroy: () => { },
|
|
7
29
|
});
|
|
8
30
|
const root = host.attachShadow({ mode: 'open' });
|
|
9
|
-
const map =
|
|
31
|
+
const map = createSFMapWithData({ districtLabels: false, landmarks: true, labels: false }, transitMapData);
|
|
10
32
|
root.innerHTML = `<style>
|
|
11
33
|
:host{display:block;font:14px system-ui;color:#244747;max-width:800px}
|
|
12
34
|
svg{display:block;width:100%;height:auto} .controls{display:flex;gap:12px;align-items:center;padding:12px;flex-wrap:wrap}
|
package/docs/EXAMPLES.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
[Live example gallery](https://kahwee.github.io/sf-map-svg/examples.html) · [California propositions](https://kahwee.github.io/sf-map-svg/propositions.html) · [Local measures](https://kahwee.github.io/sf-map-svg/measures.html) · [Source data](../SOURCES.md)
|
|
4
|
+
|
|
5
|
+
Choose by task. All snippets use the public package API; browser examples need a DOM and a bundler that supports JSON imports.
|
|
6
|
+
|
|
7
|
+
| I want to… | Start here |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| Render an SVG on a server | [Static map](#static-svg) |
|
|
10
|
+
| Add a small map to a browser | [Lightweight guide](#lightweight-interactive-guide) |
|
|
11
|
+
| Bundle only selected geography | [Selected data](#selected-geography) |
|
|
12
|
+
| Draw a route over the city | [Route overlay](#route-overlay) |
|
|
13
|
+
| Explore real election votes | [California propositions](#california-propositions-by-sf-district) |
|
|
14
|
+
|
|
15
|
+
## Static SVG
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { writeFile } from 'node:fs/promises';
|
|
19
|
+
import { renderSFMap } from '@kahwee/sf-map-svg';
|
|
20
|
+
|
|
21
|
+
const svg = renderSFMap({
|
|
22
|
+
year: 2022,
|
|
23
|
+
landmarks: true,
|
|
24
|
+
bartStations: true,
|
|
25
|
+
idPrefix: 'example',
|
|
26
|
+
});
|
|
27
|
+
await writeFile('districts.svg', svg);
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The root entry includes the built-in geography for convenience. See the [full option list](../README.md#static-options).
|
|
31
|
+
|
|
32
|
+
## Lightweight interactive guide
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { createGuideMap } from '@kahwee/sf-map-svg/guide';
|
|
36
|
+
|
|
37
|
+
const map = createGuideMap({ layers: { roadLabels: false } });
|
|
38
|
+
document.querySelector('#map')?.append(map);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The guide includes selected coast, SFAR neighborhoods, parks, roads, and stations. Detailed geography loads only when explicitly requested; see the [guide recipe](../README.md#render-a-static-map).
|
|
42
|
+
|
|
43
|
+
## Selected geography
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { createInteractiveSFMapWithData } from '@kahwee/sf-map-svg/interactive-data';
|
|
47
|
+
import coast from '@kahwee/sf-map-svg/data/coast.json' with { type: 'json' };
|
|
48
|
+
import realtor from '@kahwee/sf-map-svg/data/neighborhoods-realtor.json' with { type: 'json' };
|
|
49
|
+
|
|
50
|
+
const map = createInteractiveSFMapWithData(
|
|
51
|
+
{
|
|
52
|
+
map: { coast: coast.features[0].geometry },
|
|
53
|
+
neighborhoods: { realtor },
|
|
54
|
+
},
|
|
55
|
+
{ mode: 'neighborhoods', layers: { highways: false, keyRoads: false } },
|
|
56
|
+
);
|
|
57
|
+
document.querySelector('#map')?.append(map);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
This imports one neighborhood definition source. To omit its geometry too, import catalog metadata alone from `/data/catalog`.
|
|
61
|
+
|
|
62
|
+
## Route overlay
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { createGuideMap } from '@kahwee/sf-map-svg/guide';
|
|
66
|
+
|
|
67
|
+
const map = createGuideMap();
|
|
68
|
+
document.querySelector('#map')?.append(map);
|
|
69
|
+
map.setOverlays([{
|
|
70
|
+
id: 'trip',
|
|
71
|
+
label: 'Example route',
|
|
72
|
+
geometry: {
|
|
73
|
+
type: 'LineString',
|
|
74
|
+
coordinates: [[-122.4194, 37.7749], [-122.3981, 37.7936]],
|
|
75
|
+
},
|
|
76
|
+
stroke: '#a85036',
|
|
77
|
+
strokeWidth: 3,
|
|
78
|
+
}]);
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Coordinates are WGS84 `[longitude, latitude]`. The overlay follows pan and zoom. The [BART journey](https://kahwee.github.io/sf-map-svg/transit.html) is a separate schematic motion example.
|
|
82
|
+
|
|
83
|
+
## California propositions by SF district
|
|
84
|
+
|
|
85
|
+
[Open the interactive explorer](https://kahwee.github.io/sf-map-svg/propositions.html). It uses the public `custom-map` 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
|
+
|
|
87
|
+
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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kahwee/sf-map-svg",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.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/index.js",
|
|
@@ -18,6 +18,50 @@
|
|
|
18
18
|
"types": "./dist/data/index.d.ts",
|
|
19
19
|
"import": "./dist/data/index.js"
|
|
20
20
|
},
|
|
21
|
+
"./data/analysis": {
|
|
22
|
+
"types": "./dist/data/analysis.d.ts",
|
|
23
|
+
"import": "./dist/data/analysis.js"
|
|
24
|
+
},
|
|
25
|
+
"./data/catalog": {
|
|
26
|
+
"types": "./dist/data/catalog.d.ts",
|
|
27
|
+
"import": "./dist/data/catalog.js"
|
|
28
|
+
},
|
|
29
|
+
"./data/coast": {
|
|
30
|
+
"types": "./dist/data/coast.d.ts",
|
|
31
|
+
"import": "./dist/data/coast.js"
|
|
32
|
+
},
|
|
33
|
+
"./data/districts": {
|
|
34
|
+
"types": "./dist/data/districts.d.ts",
|
|
35
|
+
"import": "./dist/data/districts.js"
|
|
36
|
+
},
|
|
37
|
+
"./data/highways": {
|
|
38
|
+
"types": "./dist/data/highways.d.ts",
|
|
39
|
+
"import": "./dist/data/highways.js"
|
|
40
|
+
},
|
|
41
|
+
"./data/landmarks": {
|
|
42
|
+
"types": "./dist/data/landmarks.d.ts",
|
|
43
|
+
"import": "./dist/data/landmarks.js"
|
|
44
|
+
},
|
|
45
|
+
"./data/lookup": {
|
|
46
|
+
"types": "./dist/data/lookup.d.ts",
|
|
47
|
+
"import": "./dist/data/lookup.js"
|
|
48
|
+
},
|
|
49
|
+
"./data/realtor": {
|
|
50
|
+
"types": "./dist/data/realtor.d.ts",
|
|
51
|
+
"import": "./dist/data/realtor.js"
|
|
52
|
+
},
|
|
53
|
+
"./data/roads": {
|
|
54
|
+
"types": "./dist/data/roads.d.ts",
|
|
55
|
+
"import": "./dist/data/roads.js"
|
|
56
|
+
},
|
|
57
|
+
"./data/sf-find": {
|
|
58
|
+
"types": "./dist/data/sf-find.d.ts",
|
|
59
|
+
"import": "./dist/data/sf-find.js"
|
|
60
|
+
},
|
|
61
|
+
"./data/stations": {
|
|
62
|
+
"types": "./dist/data/stations.d.ts",
|
|
63
|
+
"import": "./dist/data/stations.js"
|
|
64
|
+
},
|
|
21
65
|
"./data/*.json": "./dist/data/*.json",
|
|
22
66
|
"./geometry": {
|
|
23
67
|
"types": "./dist/src/geometry.d.ts",
|
|
@@ -39,6 +83,18 @@
|
|
|
39
83
|
"types": "./dist/src/guide.d.ts",
|
|
40
84
|
"import": "./dist/src/guide.js"
|
|
41
85
|
},
|
|
86
|
+
"./guide/data": {
|
|
87
|
+
"types": "./dist/src/guide-data.d.ts",
|
|
88
|
+
"import": "./dist/src/guide-data.js"
|
|
89
|
+
},
|
|
90
|
+
"./guide/detailed": {
|
|
91
|
+
"types": "./dist/src/guide-detailed.d.ts",
|
|
92
|
+
"import": "./dist/src/guide-detailed.js"
|
|
93
|
+
},
|
|
94
|
+
"./guide/map": {
|
|
95
|
+
"types": "./dist/src/guide-map.d.ts",
|
|
96
|
+
"import": "./dist/src/guide-map.js"
|
|
97
|
+
},
|
|
42
98
|
"./transit": {
|
|
43
99
|
"types": "./dist/src/transit.d.ts",
|
|
44
100
|
"import": "./dist/src/transit.js"
|
|
@@ -46,6 +102,7 @@
|
|
|
46
102
|
},
|
|
47
103
|
"files": [
|
|
48
104
|
"dist",
|
|
105
|
+
"docs/EXAMPLES.md",
|
|
49
106
|
"README.md",
|
|
50
107
|
"SOURCES.md",
|
|
51
108
|
"LICENSE"
|