@diagrammo/dgmo 0.20.3 → 0.21.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/dist/advanced.cjs +867 -286
- package/dist/advanced.js +866 -286
- package/dist/auto.cjs +635 -284
- package/dist/auto.js +113 -113
- package/dist/auto.mjs +635 -284
- package/dist/cli.cjs +156 -156
- package/dist/editor.cjs +6 -2
- package/dist/editor.js +6 -2
- package/dist/highlight.cjs +6 -2
- package/dist/highlight.js +6 -2
- package/dist/index.cjs +628 -281
- package/dist/index.js +628 -281
- package/dist/internal.cjs +867 -286
- package/dist/internal.js +866 -286
- package/dist/map-data/PROVENANCE.json +1 -1
- package/dist/map-data/mountain-ranges.json +1 -0
- package/docs/language-reference.md +27 -25
- package/gallery/fixtures/map-choropleth.dgmo +7 -7
- package/gallery/fixtures/map-direct-color.dgmo +10 -0
- package/gallery/fixtures/map-pois.dgmo +4 -4
- package/gallery/fixtures/map-region-scope.dgmo +8 -8
- package/gallery/fixtures/map-route.dgmo +5 -6
- package/package.json +1 -1
- package/src/advanced.ts +14 -0
- package/src/completion.ts +10 -4
- package/src/d3.ts +15 -9
- package/src/editor/keywords.ts +6 -2
- package/src/map/data/PROVENANCE.json +1 -1
- package/src/map/data/mountain-ranges.json +1 -0
- package/src/map/geo-query.ts +277 -0
- package/src/map/geo.ts +258 -1
- package/src/map/invert.ts +111 -0
- package/src/map/layout.ts +333 -139
- package/src/map/load-data.ts +7 -1
- package/src/map/parser.ts +142 -33
- package/src/map/renderer.ts +57 -6
- package/src/map/resolved-types.ts +21 -2
- package/src/map/resolver.ts +219 -53
- package/src/map/types.ts +57 -14
- package/src/utils/reserved-key-registry.ts +7 -7
- package/dist/advanced.d.cts +0 -5290
- package/dist/advanced.d.ts +0 -5290
- package/dist/auto.d.cts +0 -39
- package/dist/auto.d.ts +0 -39
- package/dist/index.d.cts +0 -336
- package/dist/index.d.ts +0 -336
- package/dist/internal.d.cts +0 -5290
- package/dist/internal.d.ts +0 -5290
package/src/map/resolver.ts
CHANGED
|
@@ -14,10 +14,17 @@ import type {
|
|
|
14
14
|
ResolvedPoi,
|
|
15
15
|
ResolvedEdge,
|
|
16
16
|
ResolvedRoute,
|
|
17
|
+
ResolvedRouteLeg,
|
|
17
18
|
ProjectionFamily,
|
|
18
19
|
GeoExtent,
|
|
19
20
|
} from './resolved-types';
|
|
20
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
featureIndex,
|
|
23
|
+
featureBbox,
|
|
24
|
+
featureBboxPrimary,
|
|
25
|
+
unionExtent,
|
|
26
|
+
fold,
|
|
27
|
+
} from './geo';
|
|
21
28
|
|
|
22
29
|
/** Discriminated result of a gazetteer name lookup (#5): `defer` is "ambiguous,
|
|
23
30
|
* retry in pass B with inferred scope" — distinct from `miss` (errored, drop) so
|
|
@@ -29,7 +36,13 @@ type LookupResult =
|
|
|
29
36
|
|
|
30
37
|
// Projection / tier thresholds (degrees of span) — tunable (R10).
|
|
31
38
|
const WORLD_SPAN = 90;
|
|
32
|
-
|
|
39
|
+
// Mercator is used for everything sub-world (tight clusters AND single-continent
|
|
40
|
+
// regional views — a mid-latitude continent reads with its familiar conventional
|
|
41
|
+
// shape, where equirectangular squashes it). Two guards push back to
|
|
42
|
+
// equirectangular: a world/multi-continent `span` (> WORLD_SPAN), or a frame that
|
|
43
|
+
// reaches into polar latitudes (> MERCATOR_MAX_LAT) where Mercator's sec(φ) area
|
|
44
|
+
// blow-up turns gross. Europe (≈71°N) and East Asia stay comfortably on Mercator.
|
|
45
|
+
const MERCATOR_MAX_LAT = 80;
|
|
33
46
|
const PAD_FRACTION = 0.05;
|
|
34
47
|
// Latitude band for a snapped world view — Tierra del Fuego (≈ −55°) to northern
|
|
35
48
|
// Russia/Canada (≈ +78°). Excludes most of Antarctica + the high Arctic so the
|
|
@@ -64,6 +77,75 @@ const REGION_ALIASES: Readonly<Record<string, string>> = {
|
|
|
64
77
|
'czech republic': 'czechia',
|
|
65
78
|
};
|
|
66
79
|
|
|
80
|
+
// US state (+ DC) postal abbreviations. A bare trailing two-letter scope token
|
|
81
|
+
// that is one of these resolves to the US STATE `US-XX` (not the colliding
|
|
82
|
+
// ISO 3166-1 country code — e.g. `CA` = California, not Canada) and signals US
|
|
83
|
+
// scope (§24B.8, 2026-06-01). Non-state two-letter tokens (`CR`, `FR`) stay
|
|
84
|
+
// country codes. Trade-off: a foreign country whose code collides with a state
|
|
85
|
+
// (DE/IN/AR/CO/LA/PA/…) can't be picked by bare code — use the city name alone
|
|
86
|
+
// (most-populous) or coordinates.
|
|
87
|
+
const US_STATE_POSTAL: ReadonlySet<string> = new Set([
|
|
88
|
+
'AL',
|
|
89
|
+
'AK',
|
|
90
|
+
'AZ',
|
|
91
|
+
'AR',
|
|
92
|
+
'CA',
|
|
93
|
+
'CO',
|
|
94
|
+
'CT',
|
|
95
|
+
'DE',
|
|
96
|
+
'FL',
|
|
97
|
+
'GA',
|
|
98
|
+
'HI',
|
|
99
|
+
'ID',
|
|
100
|
+
'IL',
|
|
101
|
+
'IN',
|
|
102
|
+
'IA',
|
|
103
|
+
'KS',
|
|
104
|
+
'KY',
|
|
105
|
+
'LA',
|
|
106
|
+
'ME',
|
|
107
|
+
'MD',
|
|
108
|
+
'MA',
|
|
109
|
+
'MI',
|
|
110
|
+
'MN',
|
|
111
|
+
'MS',
|
|
112
|
+
'MO',
|
|
113
|
+
'MT',
|
|
114
|
+
'NE',
|
|
115
|
+
'NV',
|
|
116
|
+
'NH',
|
|
117
|
+
'NJ',
|
|
118
|
+
'NM',
|
|
119
|
+
'NY',
|
|
120
|
+
'NC',
|
|
121
|
+
'ND',
|
|
122
|
+
'OH',
|
|
123
|
+
'OK',
|
|
124
|
+
'OR',
|
|
125
|
+
'PA',
|
|
126
|
+
'RI',
|
|
127
|
+
'SC',
|
|
128
|
+
'SD',
|
|
129
|
+
'TN',
|
|
130
|
+
'TX',
|
|
131
|
+
'UT',
|
|
132
|
+
'VT',
|
|
133
|
+
'VA',
|
|
134
|
+
'WA',
|
|
135
|
+
'WV',
|
|
136
|
+
'WI',
|
|
137
|
+
'WY',
|
|
138
|
+
'DC',
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
/** A bare two-letter scope token that names a US state → its `US-XX` 3166-2 id
|
|
142
|
+
* (`OR` → `US-OR`), else null. Case-insensitive. */
|
|
143
|
+
function usStateFromBareScope(scope: string | undefined): string | null {
|
|
144
|
+
if (!scope) return null;
|
|
145
|
+
const up = scope.toUpperCase();
|
|
146
|
+
return US_STATE_POSTAL.has(up) ? `US-${up}` : null;
|
|
147
|
+
}
|
|
148
|
+
|
|
67
149
|
/** Rough US bounding box (incl. AK across the dateline, HI, PR) for classifying
|
|
68
150
|
* bare coordinate POIs as US-or-not when deciding `albers-usa` (#13). */
|
|
69
151
|
function looksUS(lat: number, lon: number): boolean {
|
|
@@ -128,10 +210,16 @@ export function resolveMap(parsed: ParsedMap, data: MapData): ResolvedMap {
|
|
|
128
210
|
return usStateIndex.has(f) && !countryIndex.has(f);
|
|
129
211
|
}) ||
|
|
130
212
|
parsed.regions.some(
|
|
131
|
-
(r) =>
|
|
213
|
+
(r) =>
|
|
214
|
+
r.scope === 'US' ||
|
|
215
|
+
r.scope?.startsWith('US-') ||
|
|
216
|
+
usStateFromBareScope(r.scope) !== null
|
|
132
217
|
) ||
|
|
133
218
|
parsed.pois.some(
|
|
134
|
-
(p) =>
|
|
219
|
+
(p) =>
|
|
220
|
+
p.pos.kind === 'name' &&
|
|
221
|
+
(p.pos.scope?.startsWith('US-') ||
|
|
222
|
+
usStateFromBareScope(p.pos.scope) !== null)
|
|
135
223
|
);
|
|
136
224
|
|
|
137
225
|
// ── Regions (R2/R12) ──
|
|
@@ -179,17 +267,19 @@ export function resolveMap(parsed: ParsedMap, data: MapData): ResolvedMap {
|
|
|
179
267
|
}
|
|
180
268
|
} else if (inCountry && inState) {
|
|
181
269
|
if (usScoped) {
|
|
270
|
+
// A US scope (e.g. `region us-states`) makes the state the unambiguous
|
|
271
|
+
// intent — resolve silently, no disambiguation warning needed.
|
|
182
272
|
chosen = { ...inState, layer: 'us-state' };
|
|
183
273
|
} else {
|
|
184
274
|
chosen = { ...inCountry, layer: 'country' };
|
|
275
|
+
// Teach the disambiguation syntax so the author can pin it explicitly.
|
|
276
|
+
// Suggest the non-redundant forms: a bare ISO code, or name + scope.
|
|
277
|
+
warn(
|
|
278
|
+
r.lineNumber,
|
|
279
|
+
`"${r.name}" is both a country and a US state — resolved as ${chosen.layer} (${chosen.id}). Pin it with an ISO code (${inState.id} / ${inCountry.id}) or name + scope ("${r.name} US" / "${r.name} ${inCountry.id}").`,
|
|
280
|
+
'W_MAP_REGION_AMBIGUOUS'
|
|
281
|
+
);
|
|
185
282
|
}
|
|
186
|
-
// Teach the disambiguation syntax so the author can pin it explicitly.
|
|
187
|
-
// Suggest the non-redundant forms: a bare ISO code, or name + scope.
|
|
188
|
-
warn(
|
|
189
|
-
r.lineNumber,
|
|
190
|
-
`"${r.name}" is both a country and a US state — resolved as ${chosen.layer} (${chosen.id}). Pin it with an ISO code (${inState.id} / ${inCountry.id}) or name + scope ("${r.name} US" / "${r.name} ${inCountry.id}").`,
|
|
191
|
-
'W_MAP_REGION_AMBIGUOUS'
|
|
192
|
-
);
|
|
193
283
|
} else if (inState) {
|
|
194
284
|
chosen = { ...inState, layer: 'us-state' };
|
|
195
285
|
} else if (inCountry) {
|
|
@@ -212,7 +302,8 @@ export function resolveMap(parsed: ParsedMap, data: MapData): ResolvedMap {
|
|
|
212
302
|
iso: chosen.id,
|
|
213
303
|
name: chosen.name,
|
|
214
304
|
layer: chosen.layer,
|
|
215
|
-
...(r.
|
|
305
|
+
...(r.value !== undefined && { value: r.value }),
|
|
306
|
+
...(r.color !== undefined && { color: r.color }),
|
|
216
307
|
tags: r.tags,
|
|
217
308
|
meta: r.meta,
|
|
218
309
|
lineNumber: r.lineNumber,
|
|
@@ -289,11 +380,14 @@ export function resolveMap(parsed: ParsedMap, data: MapData): ResolvedMap {
|
|
|
289
380
|
let cands = idxs.map((i) => data.gazetteer.cities[i]!);
|
|
290
381
|
const scopeUse = scope ?? scopeHint;
|
|
291
382
|
if (scopeUse) {
|
|
292
|
-
// ISO 3166-2 subdivision scope is `XX-…` (two letters + dash)
|
|
293
|
-
//
|
|
294
|
-
|
|
383
|
+
// ISO 3166-2 subdivision scope is `XX-…` (two letters + dash). A bare
|
|
384
|
+
// two-letter token that names a US state resolves as the `US-XX`
|
|
385
|
+
// subdivision (`CA` = California, §24B.8) — NOT the colliding country
|
|
386
|
+
// code; any other bare two-letter token is a 3166-1 country code.
|
|
387
|
+
const bareState = usStateFromBareScope(scopeUse);
|
|
388
|
+
const subScope = /^[A-Za-z]{2}-/.test(scopeUse) ? scopeUse : bareState;
|
|
295
389
|
const filtered = cands.filter((c) =>
|
|
296
|
-
|
|
390
|
+
subScope ? c[5] === subScope : c[2] === scopeUse
|
|
297
391
|
);
|
|
298
392
|
if (filtered.length) cands = filtered;
|
|
299
393
|
else if (scope) {
|
|
@@ -397,6 +491,7 @@ export function resolveMap(parsed: ParsedMap, data: MapData): ResolvedMap {
|
|
|
397
491
|
lat,
|
|
398
492
|
lon,
|
|
399
493
|
...(p.label !== undefined && { label: p.label }),
|
|
494
|
+
...(p.color !== undefined && { color: p.color }),
|
|
400
495
|
tags: p.tags,
|
|
401
496
|
meta: p.meta,
|
|
402
497
|
lineNumber: p.lineNumber,
|
|
@@ -455,36 +550,102 @@ export function resolveMap(parsed: ParsedMap, data: MapData): ResolvedMap {
|
|
|
455
550
|
});
|
|
456
551
|
}
|
|
457
552
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
553
|
+
// Resolve a route stop to a POI id, registering it (with its inline tags/label/
|
|
554
|
+
// size value) or binding to an already-declared POI. Returns null if a named
|
|
555
|
+
// stop can't be geocoded. Unlike the old code, stop metadata is NO LONGER
|
|
556
|
+
// dropped — it rides onto the created POI (fixes the named-stop meta bug).
|
|
557
|
+
const resolveStop = (
|
|
558
|
+
pos: PoiPos,
|
|
559
|
+
alias: string | undefined,
|
|
560
|
+
label: string | undefined,
|
|
561
|
+
tags: Readonly<Record<string, string>>,
|
|
562
|
+
sizeValue: string | undefined,
|
|
563
|
+
line: number
|
|
564
|
+
): string | null => {
|
|
565
|
+
const meta: Record<string, string> =
|
|
566
|
+
sizeValue !== undefined ? { value: sizeValue } : {};
|
|
567
|
+
if (pos.kind === 'coords') {
|
|
568
|
+
const id = alias ? fold(alias) : `@${pos.lat},${pos.lon}`;
|
|
569
|
+
if (!looksUS(pos.lat, pos.lon)) anyNonUsPoi = true;
|
|
570
|
+
if (!registry.has(id)) {
|
|
571
|
+
registerPoi(
|
|
572
|
+
id,
|
|
573
|
+
{
|
|
468
574
|
id,
|
|
469
|
-
...(
|
|
470
|
-
lat:
|
|
471
|
-
lon:
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
} else {
|
|
480
|
-
id =
|
|
481
|
-
stop.alias && registry.has(fold(stop.alias))
|
|
482
|
-
? fold(stop.alias)
|
|
483
|
-
: resolveEndpoint(stop.ref.name, stop.lineNumber);
|
|
575
|
+
...(alias !== undefined && { name: alias }),
|
|
576
|
+
lat: pos.lat,
|
|
577
|
+
lon: pos.lon,
|
|
578
|
+
...(label !== undefined && { label }),
|
|
579
|
+
tags,
|
|
580
|
+
meta,
|
|
581
|
+
lineNumber: line,
|
|
582
|
+
},
|
|
583
|
+
line
|
|
584
|
+
);
|
|
484
585
|
}
|
|
485
|
-
|
|
586
|
+
return id;
|
|
587
|
+
}
|
|
588
|
+
// Named stop: bind to an existing declared POI if present, else geocode + create.
|
|
589
|
+
const f = fold(pos.name);
|
|
590
|
+
if (registry.has(f)) return f;
|
|
591
|
+
const aliased = declaredByName.get(f);
|
|
592
|
+
if (aliased) return aliased;
|
|
593
|
+
const got = lookupName(pos.name, pos.scope, line, inferredCountry, true);
|
|
594
|
+
if (got.kind !== 'ok') return null;
|
|
595
|
+
noteCountry(got.iso);
|
|
596
|
+
registerPoi(
|
|
597
|
+
f,
|
|
598
|
+
{
|
|
599
|
+
id: f,
|
|
600
|
+
name: pos.name,
|
|
601
|
+
lat: got.lat,
|
|
602
|
+
lon: got.lon,
|
|
603
|
+
...(label !== undefined && { label }),
|
|
604
|
+
tags,
|
|
605
|
+
meta,
|
|
606
|
+
lineNumber: line,
|
|
607
|
+
},
|
|
608
|
+
line
|
|
609
|
+
);
|
|
610
|
+
return f;
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
const routes: ResolvedRoute[] = [];
|
|
614
|
+
for (const rt of parsed.routes) {
|
|
615
|
+
const originId = resolveStop(
|
|
616
|
+
rt.origin,
|
|
617
|
+
rt.originAlias,
|
|
618
|
+
rt.originLabel,
|
|
619
|
+
rt.originTags,
|
|
620
|
+
rt.originValue,
|
|
621
|
+
rt.lineNumber
|
|
622
|
+
);
|
|
623
|
+
if (!originId) continue; // can't anchor the route → drop (error already pushed)
|
|
624
|
+
const stopIds: string[] = [originId];
|
|
625
|
+
const legs: ResolvedRouteLeg[] = [];
|
|
626
|
+
let prevId = originId;
|
|
627
|
+
for (const leg of rt.legs) {
|
|
628
|
+
const destId = resolveStop(
|
|
629
|
+
leg.dest,
|
|
630
|
+
leg.destAlias,
|
|
631
|
+
leg.destLabel,
|
|
632
|
+
leg.destTags,
|
|
633
|
+
undefined, // a leg's `value:` is leg thickness, not the dest's size
|
|
634
|
+
leg.lineNumber
|
|
635
|
+
);
|
|
636
|
+
if (!destId) continue; // ungeocodable destination → skip this leg
|
|
637
|
+
legs.push({
|
|
638
|
+
fromId: prevId,
|
|
639
|
+
toId: destId,
|
|
640
|
+
...(leg.label !== undefined && { label: leg.label }),
|
|
641
|
+
style: leg.style,
|
|
642
|
+
...(leg.value !== undefined && { value: leg.value }),
|
|
643
|
+
lineNumber: leg.lineNumber,
|
|
644
|
+
});
|
|
645
|
+
if (!stopIds.includes(destId)) stopIds.push(destId); // unique markers (loop-close dedupe)
|
|
646
|
+
prevId = destId;
|
|
486
647
|
}
|
|
487
|
-
routes.push({ stopIds,
|
|
648
|
+
routes.push({ stopIds, legs, lineNumber: rt.lineNumber });
|
|
488
649
|
}
|
|
489
650
|
|
|
490
651
|
// ── Basemaps + scope ──
|
|
@@ -498,10 +659,12 @@ export function resolveMap(parsed: ParsedMap, data: MapData): ResolvedMap {
|
|
|
498
659
|
const bb = featureBbox(data.usStates, ref.id);
|
|
499
660
|
if (bb) regionBoxes.push(bb);
|
|
500
661
|
}
|
|
501
|
-
// country regions contribute their country bbox
|
|
662
|
+
// country regions contribute their country bbox — but framed on the dominant
|
|
663
|
+
// landmass, ignoring far-detached minor territories (e.g. French Guiana) so a
|
|
664
|
+
// Europe map naming France doesn't auto-fit across the Atlantic (R5).
|
|
502
665
|
for (const r of regions) {
|
|
503
666
|
if (r.layer === 'country') {
|
|
504
|
-
const bb =
|
|
667
|
+
const bb = featureBboxPrimary(data.worldCoarse, r.iso);
|
|
505
668
|
if (bb) regionBoxes.push(bb);
|
|
506
669
|
}
|
|
507
670
|
}
|
|
@@ -516,6 +679,7 @@ export function resolveMap(parsed: ParsedMap, data: MapData): ResolvedMap {
|
|
|
516
679
|
const lonSpan = extent[1][0] - extent[0][0];
|
|
517
680
|
const latSpan = extent[1][1] - extent[0][1];
|
|
518
681
|
const span = Math.max(lonSpan, latSpan);
|
|
682
|
+
const maxAbsLat = Math.max(Math.abs(extent[0][1]), Math.abs(extent[1][1]));
|
|
519
683
|
// albers-usa only covers US territory: choose it only when the map is truly
|
|
520
684
|
// US-only — no non-US country region AND no POI outside the US (#13). Without
|
|
521
685
|
// the POI guard a `default-country US` + Tokyo map projected to garbage.
|
|
@@ -542,16 +706,18 @@ export function resolveMap(parsed: ParsedMap, data: MapData): ResolvedMap {
|
|
|
542
706
|
projection = override;
|
|
543
707
|
} else if (usDominant) {
|
|
544
708
|
projection = 'albers-usa';
|
|
545
|
-
} else if (span > WORLD_SPAN) {
|
|
546
|
-
// World/
|
|
547
|
-
// never clips the continents at the boundary
|
|
548
|
-
// overrun a corner-based fit)
|
|
549
|
-
//
|
|
709
|
+
} else if (span > WORLD_SPAN || maxAbsLat > MERCATOR_MAX_LAT) {
|
|
710
|
+
// World/multi-continent scale (or a polar-reaching frame): equirectangular
|
|
711
|
+
// fills the frame edge-to-edge, never clips the continents at the boundary
|
|
712
|
+
// (naturalEarth's curved sides overrun a corner-based fit), and avoids
|
|
713
|
+
// Mercator's gross sec(φ) area blow-up near the poles. `projection
|
|
714
|
+
// natural-earth` opts back into the curved look explicitly.
|
|
550
715
|
projection = 'equirectangular';
|
|
551
|
-
} else if (span < MERCATOR_MAX_SPAN) {
|
|
552
|
-
projection = 'mercator';
|
|
553
716
|
} else {
|
|
554
|
-
|
|
717
|
+
// Tight clusters AND single-continent regional views: Mercator gives every
|
|
718
|
+
// mid-latitude landmass its familiar conventional shape (equirectangular
|
|
719
|
+
// squashes a continent like Europe horizontally).
|
|
720
|
+
projection = 'mercator';
|
|
555
721
|
}
|
|
556
722
|
|
|
557
723
|
// World-scale framing (R10): a multi-continent spread frames most cleanly as
|
package/src/map/types.ts
CHANGED
|
@@ -21,8 +21,15 @@ export interface MapScale {
|
|
|
21
21
|
export interface MapDirectives {
|
|
22
22
|
region?: string;
|
|
23
23
|
projection?: string;
|
|
24
|
-
metric
|
|
25
|
-
|
|
24
|
+
/** Legend label for the region value ramp (`region-metric <label>`). */
|
|
25
|
+
regionMetric?: string;
|
|
26
|
+
/** Recognized color NAME for the choropleth ramp hue, peeled off the
|
|
27
|
+
* `region-metric` trailing token (§24B.3). Defaults to red when absent. */
|
|
28
|
+
regionMetricColor?: string;
|
|
29
|
+
/** Legend label for the POI value (marker size) channel (`poi-metric`). */
|
|
30
|
+
poiMetric?: string;
|
|
31
|
+
/** Legend label for the edge/leg value (thickness) channel (`flow-metric`). */
|
|
32
|
+
flowMetric?: string;
|
|
26
33
|
scale?: MapScale;
|
|
27
34
|
regionLabels?: string; // full | abbrev | off
|
|
28
35
|
poiLabels?: string; // off | auto | all
|
|
@@ -30,8 +37,23 @@ export interface MapDirectives {
|
|
|
30
37
|
defaultState?: string;
|
|
31
38
|
activeTag?: string;
|
|
32
39
|
noLegend?: boolean;
|
|
40
|
+
/** Suppress the Alaska & Hawaii inset boxes drawn under the `albers-usa`
|
|
41
|
+
* projection (bare flag `no-insets`). Only meaningful for the US states
|
|
42
|
+
* basemap; silently ignored under any other projection. */
|
|
43
|
+
noInsets?: boolean;
|
|
33
44
|
subtitle?: string;
|
|
34
45
|
caption?: string;
|
|
46
|
+
/** Basemap dress override (bare flags `muted` / `natural`). Forces the
|
|
47
|
+
* land/water styling regardless of whether a colouring dimension is active —
|
|
48
|
+
* `muted` recedes to neutral grays, `natural` keeps the green/blue reference
|
|
49
|
+
* dress. Absent → auto (muted iff a score/tag dimension is active). Lets two
|
|
50
|
+
* maps in one deck share a look. */
|
|
51
|
+
basemapStyle?: 'muted' | 'natural';
|
|
52
|
+
/** Opt-in subtle mountain-range relief shading (bare flag `relief`, §24B.2).
|
|
53
|
+
* Draws a shared directional gradient ("degenerate hillshade") clipped to
|
|
54
|
+
* each notable mountain-range polygon, over base land and under data fills.
|
|
55
|
+
* Off by default; needs the optional `mountain-ranges.json` asset. */
|
|
56
|
+
relief?: boolean;
|
|
35
57
|
}
|
|
36
58
|
|
|
37
59
|
/** A region-fill: a subdivision name with an optional score and/or tag values
|
|
@@ -42,37 +64,58 @@ export interface MapRegion {
|
|
|
42
64
|
* (`Georgia US` → US context) or 3166-2 subdivision (`Georgia US-GA`).
|
|
43
65
|
* Forces the country-vs-state interpretation and silences the ambiguity warning. */
|
|
44
66
|
readonly scope?: string;
|
|
45
|
-
|
|
67
|
+
/** Numeric value → choropleth shade (§24B.3). Lifted out of `meta`. */
|
|
68
|
+
readonly value?: number;
|
|
69
|
+
/** §1.5 trailing-token color NAME → flat categorical override fill (§24B.4);
|
|
70
|
+
* painted regardless of the active colouring dimension, no legend entry. */
|
|
71
|
+
readonly color?: string;
|
|
46
72
|
/** Tag values keyed by lowercased tag GROUP name (alias is resolved away). */
|
|
47
73
|
readonly tags: Readonly<Record<string, string>>;
|
|
48
|
-
/**
|
|
74
|
+
/** Any remaining reserved keys captured verbatim (`label`/`style`/…). */
|
|
49
75
|
readonly meta: Readonly<Record<string, string>>;
|
|
50
76
|
readonly lineNumber: number;
|
|
51
77
|
}
|
|
52
78
|
|
|
53
|
-
/** A point of interest (§24B.5). `meta` holds
|
|
54
|
-
*
|
|
79
|
+
/** A point of interest (§24B.5). `meta` holds the numeric `value` (→ marker
|
|
80
|
+
* size) and `style` verbatim; `label` is lifted out. */
|
|
55
81
|
export interface MapPoi {
|
|
56
82
|
readonly pos: PoiPos;
|
|
57
83
|
readonly alias?: string;
|
|
58
84
|
readonly label?: string;
|
|
85
|
+
/** §1.5 trailing-token color NAME → flat marker fill (§24B.5); wins over a
|
|
86
|
+
* tag color and the default orange. */
|
|
87
|
+
readonly color?: string;
|
|
59
88
|
readonly tags: Readonly<Record<string, string>>;
|
|
60
89
|
readonly meta: Readonly<Record<string, string>>;
|
|
61
90
|
readonly lineNumber: number;
|
|
62
91
|
}
|
|
63
92
|
|
|
64
|
-
/**
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
93
|
+
/** One leg of a route (§24B.6): an edge from the previous stop to `dest`. Reuses
|
|
94
|
+
* the edge arrow idiom — in-arrow text = leg label, `value:` = leg thickness,
|
|
95
|
+
* `->`/`~>` (or the header `style: arc`) = shape. Stop-targeted keys on the leg
|
|
96
|
+
* line (`tag`, `label:`) decorate the DESTINATION point. */
|
|
97
|
+
export interface MapRouteLeg {
|
|
98
|
+
readonly label?: string; // in-arrow leg label
|
|
99
|
+
readonly style: 'straight' | 'arc';
|
|
100
|
+
readonly value?: string; // leg thickness (numeric string, like an edge)
|
|
101
|
+
readonly dest: PoiPos;
|
|
102
|
+
readonly destAlias?: string;
|
|
103
|
+
readonly destLabel?: string;
|
|
104
|
+
readonly destTags: Readonly<Record<string, string>>;
|
|
69
105
|
readonly lineNumber: number;
|
|
70
106
|
}
|
|
71
107
|
|
|
72
|
-
/** An ordered, auto-numbered route (§24B.6)
|
|
108
|
+
/** An ordered, auto-numbered route (§24B.6): `route <origin> [style: arc]` + a
|
|
109
|
+
* sequence of indented arrow legs, each continuing from the previous stop.
|
|
110
|
+
* Repeat the origin as a leg's destination to close a loop. */
|
|
73
111
|
export interface MapRoute {
|
|
74
|
-
readonly
|
|
75
|
-
readonly
|
|
112
|
+
readonly origin: PoiPos;
|
|
113
|
+
readonly originAlias?: string;
|
|
114
|
+
readonly originLabel?: string;
|
|
115
|
+
readonly originValue?: string; // header value → origin marker size
|
|
116
|
+
readonly originTags: Readonly<Record<string, string>>;
|
|
117
|
+
readonly style: 'straight' | 'arc'; // header default leg shape
|
|
118
|
+
readonly legs: readonly MapRouteLeg[];
|
|
76
119
|
readonly lineNumber: number;
|
|
77
120
|
}
|
|
78
121
|
|
|
@@ -75,16 +75,16 @@ export const INFRA_REGISTRY: ReservedKeyRegistry = staticRegistry([
|
|
|
75
75
|
// NOTE: `color` is deliberately OMITTED (unlike sibling registries) — per
|
|
76
76
|
// §24B.9 the map chart type carries color as a trailing token (peeled by
|
|
77
77
|
// `peelTrailingColorName`) / via the tag system, not a `color:` metadata key.
|
|
78
|
-
// `
|
|
79
|
-
//
|
|
78
|
+
// `value` is the single numeric data channel (§24B): it renders as region shade,
|
|
79
|
+
// POI marker size, or edge/leg thickness depending on the element. `style` is the
|
|
80
|
+
// route/edge shape key. `description`/`date`/`score`/`size`/`weight` were removed
|
|
81
|
+
// in the 2026-06-01 syntax review — `value` collapses the old three numeric keys,
|
|
82
|
+
// and description/date had no v1 surface (they now raise an unknown-key error
|
|
83
|
+
// rather than silently no-op).
|
|
80
84
|
export const MAP_REGISTRY: ReservedKeyRegistry = staticRegistry([
|
|
81
|
-
'
|
|
85
|
+
'value',
|
|
82
86
|
'label',
|
|
83
|
-
'size',
|
|
84
|
-
'description',
|
|
85
|
-
'weight',
|
|
86
87
|
'style',
|
|
87
|
-
'date',
|
|
88
88
|
]);
|
|
89
89
|
|
|
90
90
|
export const ORG_REGISTRY: ReservedKeyRegistry = staticRegistry([
|