@diagrammo/dgmo 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +16 -6
  2. package/dist/advanced.cjs +2521 -623
  3. package/dist/advanced.d.cts +917 -534
  4. package/dist/advanced.d.ts +917 -534
  5. package/dist/advanced.js +2516 -623
  6. package/dist/auto.cjs +2333 -608
  7. package/dist/auto.js +119 -119
  8. package/dist/auto.mjs +2335 -609
  9. package/dist/cli.cjs +168 -168
  10. package/dist/editor.cjs +13 -15
  11. package/dist/editor.js +13 -15
  12. package/dist/highlight.cjs +15 -12
  13. package/dist/highlight.js +15 -12
  14. package/dist/index.cjs +2317 -595
  15. package/dist/index.d.cts +4 -1
  16. package/dist/index.d.ts +4 -1
  17. package/dist/index.js +2319 -596
  18. package/dist/internal.cjs +2521 -623
  19. package/dist/internal.d.cts +917 -534
  20. package/dist/internal.d.ts +917 -534
  21. package/dist/internal.js +2516 -623
  22. package/dist/map-data/PROVENANCE.json +1 -1
  23. package/dist/map-data/mountain-ranges.json +1 -0
  24. package/dist/map-data/water-bodies.json +1 -0
  25. package/docs/language-reference.md +44 -31
  26. package/gallery/fixtures/map-categorical-world.dgmo +16 -0
  27. package/gallery/fixtures/map-categorical.dgmo +0 -1
  28. package/gallery/fixtures/map-choropleth.dgmo +0 -1
  29. package/gallery/fixtures/map-coastline.dgmo +7 -0
  30. package/gallery/fixtures/map-colorize.dgmo +11 -0
  31. package/gallery/fixtures/map-direct-color.dgmo +9 -0
  32. package/gallery/fixtures/map-reference-world.dgmo +11 -0
  33. package/gallery/fixtures/map-region-scope.dgmo +0 -3
  34. package/gallery/fixtures/map-route.dgmo +0 -1
  35. package/package.json +1 -1
  36. package/src/advanced.ts +26 -1
  37. package/src/boxes-and-lines/renderer.ts +39 -12
  38. package/src/cli.ts +1 -1
  39. package/src/completion.ts +32 -24
  40. package/src/cycle/renderer.ts +14 -1
  41. package/src/d3.ts +23 -11
  42. package/src/editor/highlight-api.ts +4 -0
  43. package/src/editor/keywords.ts +13 -15
  44. package/src/infra/renderer.ts +35 -7
  45. package/src/map/colorize.ts +54 -0
  46. package/src/map/context-labels.ts +429 -0
  47. package/src/map/data/PROVENANCE.json +1 -1
  48. package/src/map/data/mountain-ranges.json +1 -0
  49. package/src/map/data/types.ts +34 -0
  50. package/src/map/data/water-bodies.json +1 -0
  51. package/src/map/dimensions.ts +117 -0
  52. package/src/map/geo-query.ts +295 -0
  53. package/src/map/geo.ts +305 -2
  54. package/src/map/invert.ts +111 -0
  55. package/src/map/layout.ts +1504 -335
  56. package/src/map/load-data.ts +16 -2
  57. package/src/map/parser.ts +57 -111
  58. package/src/map/renderer.ts +556 -13
  59. package/src/map/resolved-types.ts +24 -2
  60. package/src/map/resolver.ts +237 -67
  61. package/src/map/types.ts +39 -23
  62. package/src/mindmap/renderer.ts +10 -1
  63. package/src/palettes/atlas.ts +77 -0
  64. package/src/palettes/blueprint.ts +73 -0
  65. package/src/palettes/color-utils.ts +58 -1
  66. package/src/palettes/index.ts +12 -3
  67. package/src/palettes/slate.ts +73 -0
  68. package/src/palettes/tidewater.ts +73 -0
  69. package/src/render.ts +8 -1
  70. package/src/tech-radar/renderer.ts +3 -0
  71. package/src/tech-radar/types.ts +3 -0
  72. package/src/utils/d3-types.ts +5 -0
  73. package/src/utils/legend-layout.ts +21 -4
  74. package/src/utils/legend-types.ts +7 -0
  75. package/src/utils/reserved-key-registry.ts +3 -0
  76. package/src/palettes/bold.ts +0 -67
@@ -18,7 +18,7 @@
18
18
  // `await import('jsdom')` seam in render.ts. The web build injects `MapData` via
19
19
  // DI and never calls `loadMapData`, so the dynamic import only runs in Node.
20
20
  import type { MapData } from './resolved-types';
21
- import type { BoundaryTopology, Gazetteer } from './data/types';
21
+ import type { BoundaryTopology, Gazetteer, WaterBodies } from './data/types';
22
22
 
23
23
  type NodeBuiltins = {
24
24
  readFile: typeof import('node:fs/promises').readFile;
@@ -43,8 +43,10 @@ const FILES = {
43
43
  usStates: 'us-states.json',
44
44
  lakes: 'lakes.json',
45
45
  rivers: 'rivers.json',
46
+ mountainRanges: 'mountain-ranges.json',
46
47
  naLand: 'na-land.json',
47
48
  naLakes: 'na-lakes.json',
49
+ waterBodies: 'water-bodies.json',
48
50
  gazetteer: 'gazetteer.json',
49
51
  } as const;
50
52
 
@@ -131,18 +133,28 @@ export function loadMapData(): Promise<MapData> {
131
133
  usStates,
132
134
  lakes,
133
135
  rivers,
136
+ mountainRanges,
134
137
  naLand,
135
138
  naLakes,
139
+ waterBodies,
136
140
  gazetteer,
137
141
  ] = await Promise.all([
142
+ // worldCoarse (110m) is LOAD-BEARING but NOT a render source: the world
143
+ // basemap renders from worldDetail (50m) at all scales (resolver pins
144
+ // basemaps.world = 'detail'). Coarse stays as the authoritative region
145
+ // name index + dominant-landmass bbox source in resolver.ts. Do not drop it.
138
146
  readJson<BoundaryTopology>(nb, dir, FILES.worldCoarse),
139
147
  readJson<BoundaryTopology>(nb, dir, FILES.worldDetail),
140
148
  readJson<BoundaryTopology>(nb, dir, FILES.usStates),
141
- // Lakes/rivers/NA assets are optional — older bundles may predate them.
149
+ // Lakes/rivers/mountain/NA/water assets are optional — older bundles may predate them.
142
150
  readJson<BoundaryTopology>(nb, dir, FILES.lakes).catch(() => undefined),
143
151
  readJson<BoundaryTopology>(nb, dir, FILES.rivers).catch(() => undefined),
152
+ readJson<BoundaryTopology>(nb, dir, FILES.mountainRanges).catch(
153
+ () => undefined
154
+ ),
144
155
  readJson<BoundaryTopology>(nb, dir, FILES.naLand).catch(() => undefined),
145
156
  readJson<BoundaryTopology>(nb, dir, FILES.naLakes).catch(() => undefined),
157
+ readJson<WaterBodies>(nb, dir, FILES.waterBodies).catch(() => undefined),
146
158
  readJson<Gazetteer>(nb, dir, FILES.gazetteer),
147
159
  ]);
148
160
  return validate({
@@ -152,8 +164,10 @@ export function loadMapData(): Promise<MapData> {
152
164
  gazetteer,
153
165
  ...(lakes && { lakes }),
154
166
  ...(rivers && { rivers }),
167
+ ...(mountainRanges && { mountainRanges }),
155
168
  ...(naLand && { naLand }),
156
169
  ...(naLakes && { naLakes }),
170
+ ...(waterBodies && { waterBodies }),
157
171
  });
158
172
  })().catch((e: unknown) => {
159
173
  cache = undefined; // don't poison future calls with a rejected promise
package/src/map/parser.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  measureIndent,
10
10
  splitNameAndMeta,
11
11
  extractColor,
12
+ peelTrailingColorName,
12
13
  } from '../utils/parsing';
13
14
  import {
14
15
  MAP_REGISTRY,
@@ -31,7 +32,6 @@ import type {
31
32
  MapRouteLeg,
32
33
  MapEdge,
33
34
  PoiPos,
34
- MapScale,
35
35
  } from './types';
36
36
  import type { TagGroup, TagEntry } from '../utils/tag-groups';
37
37
 
@@ -46,21 +46,22 @@ const HUB_RE = /^(->|~>)\s+(.+)$/;
46
46
  const LEG_ARROW_RE = /^(-[^>]*?->|->|~[^>]*?~>|~>|--)\s+(.+)$/;
47
47
  const AT_RE = /(^|[\s,])at\s*:/i; // the removed `at:` coord form (§24B.9)
48
48
 
49
+ // Final 13 (§24B.2): 6 irreducible-intent directives + 7 `no-*` cosmetic
50
+ // opt-outs. Every cosmetic is on by default; its `no-*` flag is the only switch.
49
51
  const DIRECTIVE_SET: ReadonlySet<string> = new Set([
50
- 'region',
51
- 'projection',
52
52
  'region-metric',
53
53
  'poi-metric',
54
54
  'flow-metric',
55
- 'scale',
56
- 'region-labels',
57
- 'poi-labels',
58
- 'default-country',
59
- 'default-state',
55
+ 'locale',
60
56
  'active-tag',
61
- 'no-legend',
62
- 'subtitle',
63
57
  'caption',
58
+ 'no-legend',
59
+ 'no-coastline',
60
+ 'no-relief',
61
+ 'no-context-labels',
62
+ 'no-region-labels',
63
+ 'no-poi-labels',
64
+ 'no-colorize',
64
65
  ]);
65
66
 
66
67
  /** True when the first non-blank/non-comment line declares `map`. */
@@ -221,15 +222,6 @@ export function parseMap(content: string): ParsedMap {
221
222
  handleTag(trimmed, lineNumber);
222
223
  continue;
223
224
  }
224
- // Bare-flag directives (no value) — only when the line is exactly the flag,
225
- // so a region named e.g. "Natural Bridge" still parses as a region.
226
- if (
227
- (firstWord === 'muted' || firstWord === 'natural') &&
228
- trimmed === firstWord
229
- ) {
230
- handleDirective(firstWord, '', lineNumber);
231
- continue;
232
- }
233
225
  if (
234
226
  DIRECTIVE_SET.has(firstWord) &&
235
227
  !trimmed.slice(firstWord.length).trimStart().startsWith(':')
@@ -289,31 +281,16 @@ export function parseMap(content: string): ParsedMap {
289
281
  pushWarning(line, `Duplicate directive "${key}" — last value wins.`);
290
282
  };
291
283
  switch (key) {
292
- case 'region':
293
- dup(d.region);
294
- d.region = value;
295
- break;
296
- case 'projection':
297
- dup(d.projection);
298
- if (
299
- value &&
300
- ![
301
- 'equirectangular',
302
- 'natural-earth',
303
- 'albers-usa',
304
- 'mercator',
305
- ].includes(value)
306
- )
307
- pushWarning(
308
- line,
309
- `Unknown projection "${value}" (expected equirectangular | natural-earth | albers-usa | mercator).`
310
- );
311
- d.projection = value;
312
- break;
313
- case 'region-metric':
284
+ case 'region-metric': {
314
285
  dup(d.regionMetric);
315
- d.regionMetric = value;
286
+ // A trailing color names the choropleth ramp hue (§24B.3): the
287
+ // label keeps the rest. `region-metric Sales ($M) blue` → blue ramp.
288
+ const { label: rmLabel, colorName: rmColor } =
289
+ peelTrailingColorName(value);
290
+ d.regionMetric = rmLabel;
291
+ if (rmColor) d.regionMetricColor = rmColor;
316
292
  break;
293
+ }
317
294
  case 'poi-metric':
318
295
  dup(d.poiMetric);
319
296
  d.poiMetric = value;
@@ -322,87 +299,44 @@ export function parseMap(content: string): ParsedMap {
322
299
  dup(d.flowMetric);
323
300
  d.flowMetric = value;
324
301
  break;
325
- case 'scale':
326
- dup(d.scale);
327
- {
328
- const s = parseScale(value, line);
329
- if (s) d.scale = s;
330
- }
331
- break;
332
- case 'region-labels':
333
- dup(d.regionLabels);
334
- if (value && !['full', 'abbrev', 'off'].includes(value))
335
- pushWarning(
336
- line,
337
- `Unknown region-labels "${value}" (expected full | abbrev | off).`
338
- );
339
- d.regionLabels = value;
340
- break;
341
- case 'poi-labels':
342
- dup(d.poiLabels);
343
- if (value && !['off', 'auto', 'all'].includes(value))
344
- pushWarning(
345
- line,
346
- `Unknown poi-labels "${value}" (expected off | auto | all).`
347
- );
348
- d.poiLabels = value;
349
- break;
350
- case 'default-country':
351
- dup(d.defaultCountry);
352
- d.defaultCountry = value;
353
- break;
354
- case 'default-state':
355
- dup(d.defaultState);
356
- d.defaultState = value;
302
+ case 'locale':
303
+ dup(d.locale);
304
+ d.locale = value;
357
305
  break;
358
306
  case 'active-tag':
359
307
  dup(d.activeTag);
360
308
  d.activeTag = value;
361
309
  break;
310
+ case 'caption':
311
+ dup(d.caption);
312
+ d.caption = value;
313
+ break;
314
+ // ── Cosmetic `no-*` opt-outs: bare flags, idempotent (mirror `no-legend`,
315
+ // no dup warning); each defaults the feature ON when absent. ──
362
316
  case 'no-legend':
363
317
  d.noLegend = true;
364
318
  break;
365
- case 'muted':
366
- case 'natural':
367
- if (d.basemapStyle !== undefined && d.basemapStyle !== key)
368
- pushWarning(
369
- line,
370
- `Conflicting basemap dress — "${d.basemapStyle}" then "${key}"; last wins.`
371
- );
372
- d.basemapStyle = key;
319
+ case 'no-coastline':
320
+ d.noCoastline = true;
373
321
  break;
374
- case 'subtitle':
375
- dup(d.subtitle);
376
- d.subtitle = value;
322
+ case 'no-relief':
323
+ d.noRelief = true;
377
324
  break;
378
- case 'caption':
379
- dup(d.caption);
380
- d.caption = value;
325
+ case 'no-context-labels':
326
+ d.noContextLabels = true;
327
+ break;
328
+ case 'no-region-labels':
329
+ d.noRegionLabels = true;
330
+ break;
331
+ case 'no-poi-labels':
332
+ d.noPoiLabels = true;
333
+ break;
334
+ case 'no-colorize':
335
+ d.noColorize = true;
381
336
  break;
382
337
  }
383
338
  }
384
339
 
385
- function parseScale(value: string, line: number): MapScale | null {
386
- const toks = value.split(/\s+/).filter(Boolean);
387
- const min = Number(toks[0]);
388
- const max = Number(toks[1]);
389
- if (!Number.isFinite(min) || !Number.isFinite(max)) {
390
- pushError(line, `scale requires numeric <min> <max> (got "${value}").`);
391
- return null;
392
- }
393
- const scale: Writable<MapScale> = { min, max };
394
- if (toks[2] === 'center') {
395
- const c = Number(toks[3]);
396
- if (Number.isFinite(c)) scale.center = c;
397
- else
398
- pushError(
399
- line,
400
- `scale center requires a number (got "${toks[3] ?? ''}").`
401
- );
402
- }
403
- return scale;
404
- }
405
-
406
340
  function handleTag(trimmed: string, line: number): void {
407
341
  const m = matchTagBlockHeading(trimmed);
408
342
  if (!m) {
@@ -488,6 +422,8 @@ export function parseMap(content: string): ParsedMap {
488
422
  };
489
423
  if (regionScope !== undefined) region.scope = regionScope;
490
424
  if (valueNum !== undefined) region.value = valueNum;
425
+ // §1.5 trailing color → flat categorical override fill (§24B.4).
426
+ if (split.color) region.color = split.color;
491
427
  regions.push(region);
492
428
  }
493
429
 
@@ -513,6 +449,8 @@ export function parseMap(content: string): ParsedMap {
513
449
  const poi: Writable<MapPoi> = { pos, tags, meta, lineNumber: line };
514
450
  if (split.alias) poi.alias = split.alias;
515
451
  if (label !== undefined) poi.label = label;
452
+ // §1.5 trailing color → flat marker fill (§24B.5); wins over a tag color.
453
+ if (split.color) poi.color = split.color;
516
454
  pois.push(poi);
517
455
  open.poi = { poi, indent };
518
456
  }
@@ -539,6 +477,8 @@ export function parseMap(content: string): ParsedMap {
539
477
  const { tags, meta } = partitionMeta(split.meta, tagGroupNames());
540
478
  const originLabel = meta['label'];
541
479
  const originValue = meta['value'];
480
+ // Leg shape comes only from `style: arc` / arrow style (surface parsing
481
+ // removed — a leg no longer bows just because it crosses water).
542
482
  const style: 'straight' | 'arc' =
543
483
  meta['style'] === 'arc' ? 'arc' : 'straight';
544
484
  const route: Writable<MapRoute> = {
@@ -588,6 +528,8 @@ export function parseMap(content: string): ParsedMap {
588
528
  const { tags, meta } = partitionMeta(split.meta, tagGroupNames());
589
529
  const value = meta['value'];
590
530
  const destLabel = meta['label'];
531
+ // Leg shape comes only from the arrow style or the route header `style: arc`
532
+ // (surface parsing removed — no implied bow).
591
533
  const style: 'straight' | 'arc' =
592
534
  arrowStyle === 'arc' || headerStyle === 'arc' ? 'arc' : 'straight';
593
535
  return {
@@ -634,13 +576,17 @@ export function parseMap(content: string): ParsedMap {
634
576
  pushError(line, `Edge has an empty endpoint: "${trimmed}".`);
635
577
  continue;
636
578
  }
637
- const meta = k === links.length - 1 ? lastSplit.meta : {};
579
+ const isLast = k === links.length - 1;
580
+ const meta = isLast ? lastSplit.meta : {};
581
+ // Edge shape comes only from the arrow token (surface parsing removed).
582
+ const style: 'straight' | 'arc' =
583
+ links[k]!.style === 'arc' ? 'arc' : 'straight';
638
584
  edges.push({
639
585
  from,
640
586
  to,
641
587
  ...(links[k]!.label !== undefined && { label: links[k]!.label }),
642
588
  directed: links[k]!.directed,
643
- style: links[k]!.style,
589
+ style,
644
590
  meta,
645
591
  lineNumber: line,
646
592
  });