@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.
Files changed (43) hide show
  1. package/dist/advanced.cjs +556 -195
  2. package/dist/advanced.js +555 -195
  3. package/dist/auto.cjs +322 -196
  4. package/dist/auto.js +113 -113
  5. package/dist/auto.mjs +322 -196
  6. package/dist/cli.cjs +156 -156
  7. package/dist/editor.cjs +1 -0
  8. package/dist/editor.js +1 -0
  9. package/dist/highlight.cjs +1 -0
  10. package/dist/highlight.js +1 -0
  11. package/dist/index.cjs +320 -195
  12. package/dist/index.js +320 -195
  13. package/dist/internal.cjs +556 -195
  14. package/dist/internal.js +555 -195
  15. package/dist/map-data/PROVENANCE.json +1 -1
  16. package/dist/map-data/mountain-ranges.json +1 -0
  17. package/docs/language-reference.md +27 -25
  18. package/gallery/fixtures/map-direct-color.dgmo +10 -0
  19. package/package.json +1 -1
  20. package/src/advanced.ts +14 -0
  21. package/src/completion.ts +1 -0
  22. package/src/d3.ts +15 -9
  23. package/src/editor/keywords.ts +1 -0
  24. package/src/map/data/PROVENANCE.json +1 -1
  25. package/src/map/data/mountain-ranges.json +1 -0
  26. package/src/map/geo-query.ts +277 -0
  27. package/src/map/geo.ts +258 -1
  28. package/src/map/invert.ts +111 -0
  29. package/src/map/layout.ts +233 -113
  30. package/src/map/load-data.ts +7 -1
  31. package/src/map/parser.ts +22 -2
  32. package/src/map/renderer.ts +44 -0
  33. package/src/map/resolved-types.ts +8 -0
  34. package/src/map/resolver.ts +40 -19
  35. package/src/map/types.ts +18 -0
  36. package/dist/advanced.d.cts +0 -5331
  37. package/dist/advanced.d.ts +0 -5331
  38. package/dist/auto.d.cts +0 -39
  39. package/dist/auto.d.ts +0 -39
  40. package/dist/index.d.cts +0 -336
  41. package/dist/index.d.ts +0 -336
  42. package/dist/internal.d.cts +0 -5331
  43. package/dist/internal.d.ts +0 -5331
@@ -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;
@@ -18,7 +18,13 @@ import type {
18
18
  ProjectionFamily,
19
19
  GeoExtent,
20
20
  } from './resolved-types';
21
- import { featureIndex, featureBbox, unionExtent, fold } from './geo';
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
- const MERCATOR_MAX_SPAN = 25;
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 = featureBbox(data.worldCoarse, r.iso);
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/continental scale: equirectangular fills the frame edge-to-edge and
692
- // never clips the continents at the boundary (naturalEarth's curved sides
693
- // overrun a corner-based fit). `projection natural-earth` opts back into the
694
- // curved look explicitly.
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
- projection = 'equirectangular';
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;