@diagrammo/dgmo 0.21.0 → 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 +556 -195
- package/dist/advanced.js +555 -195
- package/dist/auto.cjs +322 -196
- package/dist/auto.js +113 -113
- package/dist/auto.mjs +322 -196
- package/dist/cli.cjs +156 -156
- package/dist/editor.cjs +1 -0
- package/dist/editor.js +1 -0
- package/dist/highlight.cjs +1 -0
- package/dist/highlight.js +1 -0
- package/dist/index.cjs +320 -195
- package/dist/index.js +320 -195
- package/dist/internal.cjs +556 -195
- package/dist/internal.js +555 -195
- 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-direct-color.dgmo +10 -0
- package/package.json +1 -1
- package/src/advanced.ts +14 -0
- package/src/completion.ts +1 -0
- package/src/d3.ts +15 -9
- package/src/editor/keywords.ts +1 -0
- 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 +233 -113
- package/src/map/load-data.ts +7 -1
- package/src/map/parser.ts +22 -2
- package/src/map/renderer.ts +44 -0
- package/src/map/resolved-types.ts +8 -0
- package/src/map/resolver.ts +40 -19
- package/src/map/types.ts +18 -0
- package/dist/advanced.d.cts +0 -5331
- package/dist/advanced.d.ts +0 -5331
- 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 -5331
- package/dist/internal.d.ts +0 -5331
package/src/map/renderer.ts
CHANGED
|
@@ -123,6 +123,49 @@ export function renderMap(
|
|
|
123
123
|
};
|
|
124
124
|
for (const r of layout.regions) drawRegion(gRegions, r, 0.5);
|
|
125
125
|
|
|
126
|
+
// ── Relief (mountain-range hachure over ALL land, under rivers/POIs/labels) ──
|
|
127
|
+
// Rule horizontal lines across the whole canvas, clipped to the INTERSECTION
|
|
128
|
+
// of (a) the union of range polygons and (b) the land — nested clipPaths, so
|
|
129
|
+
// the hachure never bleeds onto water (coarse range polygons overrun the
|
|
130
|
+
// coast, and horizontal lines on the sea read as the water convention). The
|
|
131
|
+
// land clip is every drawn region except lakes — INCLUDING value-/tag-coloured
|
|
132
|
+
// regions, so the relief texture sits ATOP the choropleth/tag fills (a range
|
|
133
|
+
// crossing a valued state still reads as mountains there). It stays below
|
|
134
|
+
// rivers, POIs, and labels. Explicit <line>s in a <clipPath> (not a tiled
|
|
135
|
+
// <pattern>) dodge WKWebView/resvg pattern quirks. A non-scaling stroke keeps
|
|
136
|
+
// the width constant in device px at any zoom/DPR (uniform, no moire); kept
|
|
137
|
+
// sub-pixel + low-contrast so the texture stays faint. Decorative — no data attrs.
|
|
138
|
+
if (layout.relief.length && layout.reliefHatch) {
|
|
139
|
+
const h = layout.reliefHatch;
|
|
140
|
+
const rangeClipId = 'dgmo-relief-clip';
|
|
141
|
+
const landClipId = 'dgmo-relief-land';
|
|
142
|
+
const rangeClip = defs.append('clipPath').attr('id', rangeClipId);
|
|
143
|
+
for (const s of layout.relief) rangeClip.append('path').attr('d', s.d);
|
|
144
|
+
const landClip = defs.append('clipPath').attr('id', landClipId);
|
|
145
|
+
for (const r of layout.regions)
|
|
146
|
+
if (r.id !== 'lake') landClip.append('path').attr('d', r.d);
|
|
147
|
+
const gRelief = svg
|
|
148
|
+
.append('g')
|
|
149
|
+
.attr('clip-path', `url(#${landClipId})`) // outer: land only
|
|
150
|
+
.append('g')
|
|
151
|
+
.attr('class', 'dgmo-map-relief')
|
|
152
|
+
.attr('clip-path', `url(#${rangeClipId})`) // inner: ∩ ranges
|
|
153
|
+
.attr('stroke', h.color)
|
|
154
|
+
.attr('stroke-width', h.width)
|
|
155
|
+
// Non-scaling stroke = constant device width at any zoom/DPR (uniform,
|
|
156
|
+
// no moire). NOT crispEdges — that snaps to a solid ~1px in WebKit and
|
|
157
|
+
// reads far too heavy; plain AA keeps the sub-pixel lines whisper-thin.
|
|
158
|
+
.attr('vector-effect', 'non-scaling-stroke');
|
|
159
|
+
for (let y = h.spacing; y < height; y += h.spacing) {
|
|
160
|
+
gRelief
|
|
161
|
+
.append('line')
|
|
162
|
+
.attr('x1', 0)
|
|
163
|
+
.attr('y1', y)
|
|
164
|
+
.attr('x2', width)
|
|
165
|
+
.attr('y2', y);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
126
169
|
// ── Rivers (thin water centerlines over the land, under POIs/edges) ──
|
|
127
170
|
if (layout.rivers.length) {
|
|
128
171
|
const gRivers = svg
|
|
@@ -347,6 +390,7 @@ export function renderMap(
|
|
|
347
390
|
if (layout.title) {
|
|
348
391
|
svg
|
|
349
392
|
.append('text')
|
|
393
|
+
.attr('class', 'dgmo-map-title')
|
|
350
394
|
.attr('x', width / 2)
|
|
351
395
|
.attr('y', TITLE_Y)
|
|
352
396
|
.attr('text-anchor', 'middle')
|
|
@@ -17,6 +17,10 @@ export interface MapData {
|
|
|
17
17
|
/** Major river centerlines (Natural Earth 110m) drawn as thin water lines over
|
|
18
18
|
* land — e.g. the Amazon, Nile, Mississippi. Optional, like `lakes`. */
|
|
19
19
|
rivers?: BoundaryTopology;
|
|
20
|
+
/** Notable mountain-range polygons (Natural Earth 50m geography regions) drawn
|
|
21
|
+
* as a subtle gradient relief cue over base land when the `relief` directive
|
|
22
|
+
* is on — e.g. the Rockies, Andes, Himalayas. Optional, like `lakes`. */
|
|
23
|
+
mountainRanges?: BoundaryTopology;
|
|
20
24
|
/** North-America-clipped 10m country land, used as crisp neighbour context
|
|
21
25
|
* under the albers-usa US view so Canada/Mexico match the 10m states instead
|
|
22
26
|
* of the coarser world tiers. Optional, like `lakes`. */
|
|
@@ -47,6 +51,8 @@ export interface ResolvedRegion {
|
|
|
47
51
|
readonly name: string; // display name
|
|
48
52
|
readonly layer: 'country' | 'us-state';
|
|
49
53
|
readonly value?: number;
|
|
54
|
+
/** §1.5 trailing-token color NAME → flat override fill (§24B.4). */
|
|
55
|
+
readonly color?: string;
|
|
50
56
|
readonly tags: Readonly<Record<string, string>>;
|
|
51
57
|
readonly meta: Readonly<Record<string, string>>;
|
|
52
58
|
readonly lineNumber: number;
|
|
@@ -62,6 +68,8 @@ export interface ResolvedPoi {
|
|
|
62
68
|
readonly lat: number;
|
|
63
69
|
readonly lon: number;
|
|
64
70
|
readonly label?: string;
|
|
71
|
+
/** §1.5 trailing-token color NAME → flat marker fill (§24B.5). */
|
|
72
|
+
readonly color?: string;
|
|
65
73
|
readonly tags: Readonly<Record<string, string>>;
|
|
66
74
|
readonly meta: Readonly<Record<string, string>>;
|
|
67
75
|
readonly lineNumber: number;
|
package/src/map/resolver.ts
CHANGED
|
@@ -18,7 +18,13 @@ import type {
|
|
|
18
18
|
ProjectionFamily,
|
|
19
19
|
GeoExtent,
|
|
20
20
|
} from './resolved-types';
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
featureIndex,
|
|
23
|
+
featureBbox,
|
|
24
|
+
featureBboxPrimary,
|
|
25
|
+
unionExtent,
|
|
26
|
+
fold,
|
|
27
|
+
} from './geo';
|
|
22
28
|
|
|
23
29
|
/** Discriminated result of a gazetteer name lookup (#5): `defer` is "ambiguous,
|
|
24
30
|
* retry in pass B with inferred scope" — distinct from `miss` (errored, drop) so
|
|
@@ -30,7 +36,13 @@ type LookupResult =
|
|
|
30
36
|
|
|
31
37
|
// Projection / tier thresholds (degrees of span) — tunable (R10).
|
|
32
38
|
const WORLD_SPAN = 90;
|
|
33
|
-
|
|
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;
|
|
34
46
|
const PAD_FRACTION = 0.05;
|
|
35
47
|
// Latitude band for a snapped world view — Tierra del Fuego (≈ −55°) to northern
|
|
36
48
|
// Russia/Canada (≈ +78°). Excludes most of Antarctica + the high Arctic so the
|
|
@@ -255,17 +267,19 @@ export function resolveMap(parsed: ParsedMap, data: MapData): ResolvedMap {
|
|
|
255
267
|
}
|
|
256
268
|
} else if (inCountry && inState) {
|
|
257
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.
|
|
258
272
|
chosen = { ...inState, layer: 'us-state' };
|
|
259
273
|
} else {
|
|
260
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
|
+
);
|
|
261
282
|
}
|
|
262
|
-
// Teach the disambiguation syntax so the author can pin it explicitly.
|
|
263
|
-
// Suggest the non-redundant forms: a bare ISO code, or name + scope.
|
|
264
|
-
warn(
|
|
265
|
-
r.lineNumber,
|
|
266
|
-
`"${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}").`,
|
|
267
|
-
'W_MAP_REGION_AMBIGUOUS'
|
|
268
|
-
);
|
|
269
283
|
} else if (inState) {
|
|
270
284
|
chosen = { ...inState, layer: 'us-state' };
|
|
271
285
|
} else if (inCountry) {
|
|
@@ -289,6 +303,7 @@ export function resolveMap(parsed: ParsedMap, data: MapData): ResolvedMap {
|
|
|
289
303
|
name: chosen.name,
|
|
290
304
|
layer: chosen.layer,
|
|
291
305
|
...(r.value !== undefined && { value: r.value }),
|
|
306
|
+
...(r.color !== undefined && { color: r.color }),
|
|
292
307
|
tags: r.tags,
|
|
293
308
|
meta: r.meta,
|
|
294
309
|
lineNumber: r.lineNumber,
|
|
@@ -476,6 +491,7 @@ export function resolveMap(parsed: ParsedMap, data: MapData): ResolvedMap {
|
|
|
476
491
|
lat,
|
|
477
492
|
lon,
|
|
478
493
|
...(p.label !== undefined && { label: p.label }),
|
|
494
|
+
...(p.color !== undefined && { color: p.color }),
|
|
479
495
|
tags: p.tags,
|
|
480
496
|
meta: p.meta,
|
|
481
497
|
lineNumber: p.lineNumber,
|
|
@@ -643,10 +659,12 @@ export function resolveMap(parsed: ParsedMap, data: MapData): ResolvedMap {
|
|
|
643
659
|
const bb = featureBbox(data.usStates, ref.id);
|
|
644
660
|
if (bb) regionBoxes.push(bb);
|
|
645
661
|
}
|
|
646
|
-
// 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).
|
|
647
665
|
for (const r of regions) {
|
|
648
666
|
if (r.layer === 'country') {
|
|
649
|
-
const bb =
|
|
667
|
+
const bb = featureBboxPrimary(data.worldCoarse, r.iso);
|
|
650
668
|
if (bb) regionBoxes.push(bb);
|
|
651
669
|
}
|
|
652
670
|
}
|
|
@@ -661,6 +679,7 @@ export function resolveMap(parsed: ParsedMap, data: MapData): ResolvedMap {
|
|
|
661
679
|
const lonSpan = extent[1][0] - extent[0][0];
|
|
662
680
|
const latSpan = extent[1][1] - extent[0][1];
|
|
663
681
|
const span = Math.max(lonSpan, latSpan);
|
|
682
|
+
const maxAbsLat = Math.max(Math.abs(extent[0][1]), Math.abs(extent[1][1]));
|
|
664
683
|
// albers-usa only covers US territory: choose it only when the map is truly
|
|
665
684
|
// US-only — no non-US country region AND no POI outside the US (#13). Without
|
|
666
685
|
// the POI guard a `default-country US` + Tokyo map projected to garbage.
|
|
@@ -687,16 +706,18 @@ export function resolveMap(parsed: ParsedMap, data: MapData): ResolvedMap {
|
|
|
687
706
|
projection = override;
|
|
688
707
|
} else if (usDominant) {
|
|
689
708
|
projection = 'albers-usa';
|
|
690
|
-
} else if (span > WORLD_SPAN) {
|
|
691
|
-
// World/
|
|
692
|
-
// never clips the continents at the boundary
|
|
693
|
-
// overrun a corner-based fit)
|
|
694
|
-
//
|
|
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.
|
|
695
715
|
projection = 'equirectangular';
|
|
696
|
-
} else if (span < MERCATOR_MAX_SPAN) {
|
|
697
|
-
projection = 'mercator';
|
|
698
716
|
} else {
|
|
699
|
-
|
|
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';
|
|
700
721
|
}
|
|
701
722
|
|
|
702
723
|
// World-scale framing (R10): a multi-continent spread frames most cleanly as
|
package/src/map/types.ts
CHANGED
|
@@ -23,6 +23,9 @@ export interface MapDirectives {
|
|
|
23
23
|
projection?: string;
|
|
24
24
|
/** Legend label for the region value ramp (`region-metric <label>`). */
|
|
25
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;
|
|
26
29
|
/** Legend label for the POI value (marker size) channel (`poi-metric`). */
|
|
27
30
|
poiMetric?: string;
|
|
28
31
|
/** Legend label for the edge/leg value (thickness) channel (`flow-metric`). */
|
|
@@ -34,6 +37,10 @@ export interface MapDirectives {
|
|
|
34
37
|
defaultState?: string;
|
|
35
38
|
activeTag?: string;
|
|
36
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;
|
|
37
44
|
subtitle?: string;
|
|
38
45
|
caption?: string;
|
|
39
46
|
/** Basemap dress override (bare flags `muted` / `natural`). Forces the
|
|
@@ -42,6 +49,11 @@ export interface MapDirectives {
|
|
|
42
49
|
* dress. Absent → auto (muted iff a score/tag dimension is active). Lets two
|
|
43
50
|
* maps in one deck share a look. */
|
|
44
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;
|
|
45
57
|
}
|
|
46
58
|
|
|
47
59
|
/** A region-fill: a subdivision name with an optional score and/or tag values
|
|
@@ -54,6 +66,9 @@ export interface MapRegion {
|
|
|
54
66
|
readonly scope?: string;
|
|
55
67
|
/** Numeric value → choropleth shade (§24B.3). Lifted out of `meta`. */
|
|
56
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;
|
|
57
72
|
/** Tag values keyed by lowercased tag GROUP name (alias is resolved away). */
|
|
58
73
|
readonly tags: Readonly<Record<string, string>>;
|
|
59
74
|
/** Any remaining reserved keys captured verbatim (`label`/`style`/…). */
|
|
@@ -67,6 +82,9 @@ export interface MapPoi {
|
|
|
67
82
|
readonly pos: PoiPos;
|
|
68
83
|
readonly alias?: string;
|
|
69
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;
|
|
70
88
|
readonly tags: Readonly<Record<string, string>>;
|
|
71
89
|
readonly meta: Readonly<Record<string, string>>;
|
|
72
90
|
readonly lineNumber: number;
|