@diagrammo/dgmo 0.21.1 → 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 (73) hide show
  1. package/README.md +16 -6
  2. package/dist/advanced.cjs +2003 -466
  3. package/dist/advanced.d.cts +5714 -0
  4. package/dist/advanced.d.ts +5714 -0
  5. package/dist/advanced.js +1999 -466
  6. package/dist/auto.cjs +2048 -449
  7. package/dist/auto.d.cts +39 -0
  8. package/dist/auto.d.ts +39 -0
  9. package/dist/auto.js +121 -121
  10. package/dist/auto.mjs +2050 -450
  11. package/dist/cli.cjs +170 -170
  12. package/dist/editor.cjs +13 -16
  13. package/dist/editor.js +13 -16
  14. package/dist/highlight.cjs +15 -13
  15. package/dist/highlight.js +15 -13
  16. package/dist/index.cjs +2032 -435
  17. package/dist/index.d.cts +339 -0
  18. package/dist/index.d.ts +339 -0
  19. package/dist/index.js +2034 -436
  20. package/dist/internal.cjs +2003 -466
  21. package/dist/internal.d.cts +5714 -0
  22. package/dist/internal.d.ts +5714 -0
  23. package/dist/internal.js +1999 -466
  24. package/dist/map-data/water-bodies.json +1 -0
  25. package/docs/language-reference.md +20 -9
  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 +0 -1
  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 +12 -1
  37. package/src/boxes-and-lines/renderer.ts +39 -12
  38. package/src/cli.ts +1 -1
  39. package/src/completion.ts +32 -25
  40. package/src/cycle/renderer.ts +14 -1
  41. package/src/d3.ts +8 -2
  42. package/src/editor/highlight-api.ts +4 -0
  43. package/src/editor/keywords.ts +13 -16
  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/types.ts +34 -0
  48. package/src/map/data/water-bodies.json +1 -0
  49. package/src/map/dimensions.ts +117 -0
  50. package/src/map/geo-query.ts +21 -3
  51. package/src/map/geo.ts +47 -1
  52. package/src/map/layout.ts +1300 -251
  53. package/src/map/load-data.ts +10 -2
  54. package/src/map/parser.ts +42 -116
  55. package/src/map/renderer.ts +512 -13
  56. package/src/map/resolved-types.ts +16 -2
  57. package/src/map/resolver.ts +208 -59
  58. package/src/map/types.ts +30 -32
  59. package/src/mindmap/renderer.ts +10 -1
  60. package/src/palettes/atlas.ts +77 -0
  61. package/src/palettes/blueprint.ts +73 -0
  62. package/src/palettes/color-utils.ts +58 -1
  63. package/src/palettes/index.ts +12 -3
  64. package/src/palettes/slate.ts +73 -0
  65. package/src/palettes/tidewater.ts +73 -0
  66. package/src/render.ts +8 -1
  67. package/src/tech-radar/renderer.ts +3 -0
  68. package/src/tech-radar/types.ts +3 -0
  69. package/src/utils/d3-types.ts +5 -0
  70. package/src/utils/legend-layout.ts +21 -4
  71. package/src/utils/legend-types.ts +7 -0
  72. package/src/utils/reserved-key-registry.ts +3 -0
  73. package/src/palettes/bold.ts +0 -67
@@ -0,0 +1 @@
1
+ {"entries":[[85.078,21.558,"Arctic Ocean",0,"ocean"],[-26.746,83.424,"Indian Ocean",0,"ocean"],[39.898,-30.68,"North Atlantic Ocean",0,"ocean",[[40,-55],[30,-45],[50,-25],[15,-45]]],[24.49,-136.445,"North Pacific Ocean",0,"ocean",[[36,-126],[47,-131],[55,-143],[35,160],[15,165]]],[-34.311,-18.311,"South Atlantic Ocean",0,"ocean",[[-25,-35],[-40,-15],[-10,-20]]],[-30.137,-126.822,"South Pacific Ocean",0,"ocean",[[-20,-110],[-40,-95],[-15,170],[-35,170]]],[-65.892,-18.447,"Southern Ocean",0,"ocean"],[13.692,63.675,"Arabian Sea",1,"sea"],[72.951,-66.643,"Baffin Bay",1,"bay"],[13.118,86.757,"Bay of Bengal",1,"bay"],[71.997,-136.388,"Beaufort Sea",1,"sea"],[43.547,31.292,"Black Sea",1,"sea"],[13.754,-78.235,"Caribbean Sea",1,"sea"],[41.954,50.61,"Caspian Sea",1,"sea"],[-18.886,157.126,"Coral Sea",1,"sea"],[58.108,-149.198,"Gulf of Alaska",1,"gulf"],[25.319,-90.053,"Gulf of America",1,"gulf"],[59.246,-85.292,"Hudson Bay",1,"bay"],[55.979,-52.602,"Labrador Sea",1,"sea"],[34.341,17.988,"Mediterranean Sea",1,"sea"],[25.683,52.866,"Persian Gulf",1,"gulf"],[17.263,133.481,"Philippine Sea",1,"sea"],[19.579,38.751,"Red Sea",1,"sea"],[-77.493,-169.96,"Ross Sea",1,"sea"],[40.613,136.423,"Sea of Japan",1,"sea"],[52.851,149.205,"Sea of Okhotsk",1,"sea"],[13.781,114.703,"South China Sea",1,"sea"],[-40.358,160.682,"Tasman Sea",1,"sea"],[-75.66,-53.473,"Weddell Sea",1,"sea"],[-73.979,-106.379,"Amundsen Sea",2,"sea"],[10.878,95.492,"Andaman Sea",2,"sea"],[-9.347,135.278,"Arafura Sea",2,"sea"],[19.67,-93.559,"Bahía de Campeche",2,"bay"],[56.024,19.234,"Baltic Sea",2,"sea"],[-5.658,126.23,"Banda Sea",2,"sea"],[74.604,43.063,"Barents Sea",2,"sea"],[45.627,-4.33,"Bay of Biscay",2,"bay"],[-72.084,-82.745,"Bellingshausen Sea",2,"sea"],[56.349,-171.512,"Bering Sea",2,"sea"],[3.571,122.574,"Celebes Sea",2,"sea"],[68.633,-169.418,"Chukchi Sea",2,"sea"],[64.549,-57.848,"Davis Strait",2,"strait"],[-66.686,-68.017,"Drake Passage",2,"channel"],[28.854,125.37,"East China Sea",2,"sea"],[76.558,-8.516,"Greenland Sea",2,"sea"],[3.294,2.903,"Gulf of Guinea",2,"gulf"],[9.545,101.832,"Gulf of Thailand",2,"gulf"],[53.543,-5.35,"Irish Sea",2,"sea"],[-5.2,112.569,"Java Sea",2,"sea"],[76.92,73.103,"Kara Sea",2,"sea"],[5.871,75.234,"Laccadive Sea",2,"sea"],[75.961,120.379,"Laptev Sea",2,"sea"],[22.073,120.87,"Luzon Strait",2,"strait"],[-20.03,40.414,"Mozambique Channel",2,"channel"],[56.502,2.655,"North Sea",2,"sea"],[66.883,1.329,"Norwegian Sea",2,"sea"],[27.646,-59.716,"Sargasso Sea",2,"sea"],[-61.149,-54.893,"Scotia Sea",2,"sea"],[8.392,120.208,"Sulu Sea",2,"sea"],[-10.978,127.712,"Timor Sea",2,"sea"],[35.164,123.956,"Yellow Sea",2,"sea"],[42.81,15.327,"Adriatic Sea",3,"sea"],[49.707,-2.894,"English Channel",3,"channel"],[26.35,-110.53,"Golfo de California",3,"gulf"],[-35.41,131.682,"Great Australian Bight",3,"gulf"],[12.621,48.305,"Gulf of Aden",3,"gulf"],[61.904,19.763,"Gulf of Bothnia",3,"gulf"],[-14.387,139.215,"Gulf of Carpentaria",3,"gulf"],[16.3,-88.001,"Gulf of Honduras",3,"gulf"],[24.54,58.766,"Gulf of Oman",3,"gulf"],[63.3,-73.224,"Hudson Strait",3,"strait"],[53.746,-80.255,"James Bay",3,"bay"],[39.863,12.144,"Tyrrhenian Sea",3,"sea"],[65.418,38.854,"White Sea",3,"sea"],[38.899,24.994,"Aegean Sea",4,"sea"],[70.531,-120.687,"Amundsen Gulf",4,"gulf"],[40.132,1.619,"Balearic Sea",4,"sea"],[44.909,-66.051,"Bay of Fundy",4,"bay"],[-37.489,176.808,"Bay of Plenty",4,"bay"],[-3.776,147.903,"Bismarck Sea",4,"sea"],[38.756,120.053,"Bo Hai",4,"sea"],[57.651,-160.179,"Bristol Bay",4,"bay"],[51.086,-5.339,"Bristol Channel",4,"channel"],[-2.412,131.169,"Ceram Sea",4,"sea"],[37.829,-76.031,"Chesapeake Bay",4,"bay"],[59.657,-152.312,"Cook Inlet",4,"bay"],[42.981,3.976,"Golfe du Lion",4,"gulf"],[-46.117,-66.587,"Golfo San Jorge",4,"gulf"],[8.18,-79.211,"Golfo de Panamá",4,"gulf"],[59.969,26.987,"Gulf of Finland",4,"gulf"],[22.57,69.333,"Gulf of Kutch",4,"gulf"],[43.368,-68.504,"Gulf of Maine",4,"gulf"],[8.43,78.962,"Gulf of Mannar",4,"gulf"],[48.615,-60.525,"Gulf of Saint Lawrence",4,"gulf"],[19.791,107.611,"Gulf of Tonkin",4,"gulf"],[33.956,132.471,"Inner Sea",4,"sea"],[56.267,-7.03,"Inner Seas",4,"sea"],[38.178,18.626,"Ionian Sea",4,"sea"],[33.799,128.392,"Korea Strait",4,"strait"],[-1.86,118.021,"Makassar Strait",4,"strait"],[75.523,-61.147,"Melville Bay",4,"bay"],[0.591,126.128,"Molucca Sea",4,"sea"],[-35.469,-56.554,"Río de la Plata",4,"gulf"],[59.897,157.821,"Shelikhova Gulf",4,"gulf"],[-9.247,155.069,"Solomon Sea",4,"sea"],[35.783,-5.919,"Strait of Gibraltar",4,"strait"],[5.586,99.038,"Strait of Malacca",4,"strait"],[1.193,103.68,"Strait of Singapore",4,"strait"],[25.571,-79.276,"Straits of Florida",4,"strait"],[24.166,119.387,"Taiwan Strait",4,"strait"],[70.288,-99.272,"The North Western Passages",4,"channel"],[59.341,-67.41,"Ungava Bay",4,"bay"],[73.851,-109.102,"Viscount Melville Sound",4,"sound"]]}
@@ -2644,7 +2644,9 @@ Markers in cells are always **rendered in canonical alphabet order** (`R A C I`,
2644
2644
 
2645
2645
  Geographic concept maps: highlight/shade political subdivisions, drop points of interest (POIs), and connect them with routes or edges. For "share a concept" business maps, not cartography. Renders at a fixed, auto-fit position — no pan/zoom. Basemap and viewport are **inferred from the content you reference** — most maps need no directives. v1 boundaries: world countries + US states.
2646
2646
 
2647
- **How the map type is decided (inference):** the resolver takes the bounding box of everything referenced (valued/tagged regions + POIs + edge endpoints), pads it, and measures its span. Projection: `albers-usa` (US conic + AK/HI insets) when the map is US-only; else `equirectangular` snapped to the full Greenwich world when the span is world-scale (≥ ~90°); else `mercator` for a tight regional cluster; else `equirectangular`. The US-state mesh is added whenever you name a US state. Directives only matter to *override* this: `region us-states` forces the state mesh + US scoping (useful on a POI-only US map, redundant once you name a state); `projection …` forces the projection; **`region world` is currently inert** — world is already the default, so it changes nothing (the frame widens from a world-scale longitude span, not this directive).
2647
+ **The zero-config map is the good-looking map.** Type `map`, name some places, and you're done coastlines, mountain relief (on reference maps), region/POI labels, and orientation labels all render by default. There is no projection, scale, or label directive; the only knobs are the bare `no-*` opt-outs.
2648
+
2649
+ **How the map type is decided (inference):** the resolver takes the bounding box of everything referenced (valued/tagged regions + POIs + edge endpoints), pads it, and measures its span. Projection is **always inferred — never configured**: `albers-usa` (US conic + AK/HI insets) when the map is US-oriented; at world/multi-continent scale a **data** map (any region/POI carries `value:` or a tag) gets **Equal Earth** (equal-area — honest for thematic comparison) while a **dataless reference** map gets **natural-earth** (the prettier curved compromise); `mercator` for a tight regional or single-continent cluster. The US-state mesh is added whenever you name a US state (or the map is US-oriented).
2648
2650
 
2649
2651
  ### Declaration
2650
2652
 
@@ -2660,7 +2662,6 @@ A subdivision name on its own line with a `value:` fills with a single-hue tint
2660
2662
 
2661
2663
  ```
2662
2664
  map US Sales
2663
- region us-states
2664
2665
  region-metric Sales ($M)
2665
2666
 
2666
2667
  California value: 92
@@ -2668,7 +2669,8 @@ Texas value: 78
2668
2669
  Florida value: 51
2669
2670
  ```
2670
2671
 
2671
- - `region-metric <label>` labels the ramp in the legend; a trailing color on it sets the ramp hue (`region-metric Sales ($M) blue` → blue ramp, default red). `scale <min> <max>` overrides the auto anchors.
2672
+ - `region-metric <label>` labels the ramp in the legend; a trailing color on it sets the ramp hue (`region-metric Sales ($M) blue` → blue ramp, default red).
2673
+ - The ramp **auto-fits**: all-non-negative data anchors the low end at **0** (shared baseline); mixed-sign data fits data-min→data-max. There is no `scale` directive.
2672
2674
  - A subdivision with no `value:`/tag renders as the neutral base.
2673
2675
 
2674
2676
  ### Region fill — categorical (tags)
@@ -2677,7 +2679,6 @@ Uses the universal tag model (§1.3): declare a `tag` group and apply its alias
2677
2679
 
2678
2680
  ```
2679
2681
  map Global Presence
2680
- region world
2681
2682
 
2682
2683
  tag Market as m
2683
2684
  HQ blue
@@ -2734,18 +2735,28 @@ dcw # hub/star — indented edges share the source
2734
2735
  -> office-west
2735
2736
  ```
2736
2737
 
2737
- `~>` curves a single edge. No geographic path-finding — legs are straight or arced.
2738
+ `~>` curves a single edge. There is no geographic path-finding and no `surface:` — legs are plain straight or arced geometry (`style: arc` to bow one) and may cross land.
2739
+
2740
+ ```
2741
+ map Caribbean Cruise
2742
+
2743
+ route Miami style: arc
2744
+ -weigh anchor-> Havana
2745
+ -> Kingston
2746
+ -> Cartagena
2747
+ ```
2738
2748
 
2739
2749
  ### Labels, legend & chrome
2740
2750
 
2741
- - Title is the declaration line; `subtitle` / `caption` are directives.
2751
+ - Title is the declaration line; `caption` (data-source attribution, travels with the exported PNG) is the only chrome directive. There is no `subtitle`.
2742
2752
  - Legend auto-composes below the title: the value ramp + `region-metric` and each tag group are **selectable colouring groups** (collapse/activate to flip the fill); POI size (`poi-metric`) and edge thickness (`flow-metric`) are self-evident from scale and carry no legend key in v1. `no-legend` suppresses all of it.
2743
- - `region-labels full | abbrev | off` (default `off`); `poi-labels off | auto | all` (default `auto`). Labels render **on the map** (export-safe), escalating inline → leader line → numbered pin in dense clusters; markers never move.
2753
+ - **Region and POI labels are on by default.** Region labels auto-fit **full abbrev hide** (a US-state 2-letter abbreviation is tried when the full name doesn't fit; other regions degrade full → hide); POI labels are collision-managed. Labels render **on the map** (export-safe), escalating inline → leader line → numbered pin in dense clusters; markers never move. A wide map in a narrow column (< ~480px) prefers abbreviations and drops reference relief, as if zoomed out.
2754
+ - **Cosmetic features are on by default**; the only switches are bare `no-*` opt-outs (no positive opt-in flag): `no-coastline`, `no-relief`, `no-context-labels`, `no-region-labels`, `no-poi-labels`, `no-legend`. A plain look = the four basemap flags together.
2744
2755
 
2745
2756
  ### Name resolution
2746
2757
 
2747
2758
  - Admin units use **ISO 3166** (geometry keyed by code, so "United States" / "USA" / "US" resolve alike); cities use **GeoNames** (alias/accent matching, population ranking, did-you-mean).
2748
- - `default-country` / `default-state <ISO>` scopes bare city resolution (inferred from content if unset).
2759
+ - `locale <ISO>` scopes bare city resolution to a country (`locale US`) or subdivision (`locale US-GA`) — inferred from content if unset.
2749
2760
  - A bare ambiguous, undeclared name → most-populous in scope (info note).
2750
2761
  - **Disambiguate once:** trailing ISO code at first declaration — `San Jose CR` (country) or `Portland US-OR` (subdivision). Thereafter reference the bare name. Two same-named cities → `as <alias>` each.
2751
2762
  - **Region fills disambiguate the country-vs-state collision** (`Georgia` = country `GE` or US state `US-GA`) by ISO code or name + scope — pick whichever reads best:
@@ -2756,7 +2767,7 @@ dcw # hub/star — indented edges share the source
2756
2767
 
2757
2768
  ### Directives & reserved keys
2758
2769
 
2759
- Directives (no colon): `region` (world | us-states), `projection` (equirectangular | natural-earth | albers-usa | mercator), `region-metric`, `poi-metric`, `flow-metric`, `scale`, `region-labels`, `poi-labels`, `default-country`, `default-state`, `active-tag`, `no-legend`, `subtitle`, `caption`. Reserved metadata keys (need colons): `value`, `label`, `style` (`value` = the one numeric channel: region shade / POI size / edge thickness). A bare US state postal code resolves to that state (`poi Portland OR` → Oregon; `CA` = California). Coordinates are positional (no `at:` key). Projection is auto-picked by extent span (world equirectangular, full Greenwich frame; US → albers-usa with Alaska/Hawaii insets; tight regional → mercator) unless overridden.
2770
+ The directive set is **12, all colon-free**: six naming intent the renderer can't infer `region-metric`, `poi-metric`, `flow-metric`, `locale`, `active-tag`, `caption` — and six `no-*` cosmetic opt-outs — `no-legend`, `no-coastline`, `no-relief`, `no-context-labels`, `no-region-labels`, `no-poi-labels`. There is **no** `projection`, `scale`, `subtitle`, `surface`, `region`, or label-enum directive, and cosmetics have no positive opt-in form. Reserved metadata keys (need colons): `value`, `label`, `style` (`value` = the one numeric channel: region shade / POI size / edge thickness); `surface:` is no longer recognized. A bare US state postal code resolves to that state (`poi Portland OR` → Oregon; `CA` = California). Coordinates are positional (no `at:` key). Projection is inferred from extent + whether the map carries data (US → albers-usa; world data → Equal Earth; world reference → natural-earth; regional → mercator) and cannot be overridden.
2760
2771
 
2761
2772
  ---
2762
2773
 
@@ -0,0 +1,16 @@
1
+ map Global Market Presence
2
+
3
+ tag Market as m
4
+ HQ blue
5
+ Region teal
6
+ Prospect orange
7
+ active-tag Market
8
+
9
+ United States m: HQ
10
+ Germany m: Region
11
+ Japan m: Region
12
+ Brazil m: Prospect
13
+ India m: Region
14
+ Australia m: Prospect
15
+ South Africa m: Prospect
16
+ Indonesia m: Prospect
@@ -1,5 +1,4 @@
1
1
  map Global Market Presence
2
- region world
3
2
 
4
3
  tag Market as m
5
4
  HQ blue
@@ -1,5 +1,4 @@
1
1
  map US Sales by State
2
- region us-states
3
2
  region-metric Sales ($M)
4
3
 
5
4
  California value: 92
@@ -0,0 +1,7 @@
1
+ map Smuggler's Run
2
+
3
+ poi Havana value: 90
4
+ poi Kingston value: 120
5
+
6
+ route Havana style: arc
7
+ -run the blockade-> Kingston
@@ -0,0 +1,11 @@
1
+ map Privateer's Atlas
2
+
3
+ // Regions named, no value: or tags → colorize is the inferred dress: every
4
+ // state/country gets a distinct pastel and no two neighbours share a hue
5
+ // (4-colour political look). The named states aren't special — the whole drawn
6
+ // mesh colours, so it reads as one chart. `no-colorize` would force green-land.
7
+ California
8
+ Texas
9
+ Florida
10
+ New York
11
+ Washington
@@ -1,5 +1,4 @@
1
1
  map Regional Focus
2
- region us-states
3
2
  region-metric Sales ($M) blue
4
3
 
5
4
  California value: 92
@@ -0,0 +1,11 @@
1
+ map World Reference
2
+
3
+ // A dataless reference map (regions named, no value: or tags) — so the
4
+ // zero-config basemap shows its full dress: coastline water-lines, mountain
5
+ // relief, and orientation labels, on a natural-earth projection.
6
+ United States
7
+ Brazil
8
+ Egypt
9
+ India
10
+ Japan
11
+ Australia
@@ -1,8 +1,5 @@
1
1
  map Region Scope Disambiguation
2
- region us-states
3
2
  region-metric Sales ($M)
4
- region-labels abbrev
5
- subtitle Pin a country/state name clash by ISO code (US-GA) or name + scope (Georgia US)
6
3
 
7
4
  California value: 92
8
5
  Texas value: 78
@@ -1,5 +1,4 @@
1
1
  map Caribbean Cruise
2
- projection mercator
3
2
 
4
3
  route Miami style: arc
5
4
  -weigh anchor-> Havana
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diagrammo/dgmo",
3
- "version": "0.21.1",
3
+ "version": "0.22.0",
4
4
  "description": "DGMO diagram markup language — parser, renderer, and color system",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/advanced.ts CHANGED
@@ -540,6 +540,14 @@ export type {
540
540
  MapLayoutLegend,
541
541
  } from './map/layout';
542
542
  export { renderMap, renderMapForExport } from './map/renderer';
543
+ // Content-aware export dimensions — derive the canvas height from a map's intrinsic
544
+ // projected aspect so exports/embeds match the content's natural shape (no vertical
545
+ // stretch). Used by the CLI/MCP/SSG export path and by Obsidian's DI render.
546
+ export {
547
+ mapContentAspect,
548
+ mapExportDimensions,
549
+ type MapExportDimensions,
550
+ } from './map/dimensions';
543
551
  // Map geo-query (step-5 coordinate/location inspector) — a SEPARATE entry from
544
552
  // the renderer; takes `MapData` by DI so it's browser-safe (never calls the
545
553
  // Node-only `loadMapData`).
@@ -734,7 +742,10 @@ export {
734
742
  gruvboxPalette,
735
743
  tokyoNightPalette,
736
744
  oneDarkPalette,
737
- boldPalette,
745
+ atlasPalette,
746
+ blueprintPalette,
747
+ slatePalette,
748
+ tidewaterPalette,
738
749
  draculaPalette,
739
750
  monokaiPalette,
740
751
  } from './palettes';
@@ -324,6 +324,9 @@ interface BLRenderOptions {
324
324
  onToggleDescriptions?: (active: boolean) => void;
325
325
  onToggleControlsExpand?: () => void;
326
326
  exportMode?: boolean;
327
+ /** When 'app', the description toggle is hosted by the app overlay strip
328
+ * (inline gear suppressed, controls row + anchor reserved). */
329
+ controlsHost?: 'app' | 'inline';
327
330
  }
328
331
 
329
332
  export function renderBoxesAndLines(
@@ -344,6 +347,7 @@ export function renderBoxesAndLines(
344
347
  onToggleDescriptions,
345
348
  onToggleControlsExpand,
346
349
  exportMode = false,
350
+ controlsHost,
347
351
  } = options ?? {};
348
352
  d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
349
353
 
@@ -364,16 +368,27 @@ export function renderBoxesAndLines(
364
368
  const sGroupLabelZone = sctx.structural(GROUP_LABEL_ZONE);
365
369
  const sTitleFontSize = sctx.text(TITLE_FONT_SIZE);
366
370
  const sTitleY = sctx.structural(TITLE_Y);
367
- const sLegendHeight = sctx.structural(
368
- getMaxLegendReservedHeight(
369
- {
370
- groups: parsed.tagGroups,
371
- position: { placement: 'top-center', titleRelation: 'below-title' },
372
- mode: exportMode ? 'export' : 'preview',
373
- },
374
- width
375
- )
371
+ // Reserve legend height only when a legend will actually render. App-hosted
372
+ // controls move the Descriptions toggle to the app overlay, so a
373
+ // descriptions-only chart (no tag groups) reserves nothing.
374
+ const reserveHasDescriptions = parsed.nodes.some(
375
+ (n) => n.description && n.description.length > 0
376
376
  );
377
+ const willRenderLegend =
378
+ parsed.tagGroups.length > 0 ||
379
+ (reserveHasDescriptions && controlsHost !== 'app');
380
+ const sLegendHeight = willRenderLegend
381
+ ? sctx.structural(
382
+ getMaxLegendReservedHeight(
383
+ {
384
+ groups: parsed.tagGroups,
385
+ position: { placement: 'top-center', titleRelation: 'below-title' },
386
+ mode: exportMode ? 'export' : 'preview',
387
+ },
388
+ width
389
+ )
390
+ )
391
+ : 0;
377
392
 
378
393
  const activeGroup = resolveActiveTagGroup(
379
394
  parsed.tagGroups,
@@ -995,12 +1010,17 @@ export function renderBoxesAndLines(
995
1010
  const hasDescriptions = parsed.nodes.some(
996
1011
  (n) => n.description && n.description.length > 0
997
1012
  );
998
- const hasLegend = parsed.tagGroups.length > 0 || hasDescriptions;
1013
+ // App-hosted: the Descriptions control moves to the app overlay, so a
1014
+ // descriptions-only legend (no tag groups) has nothing left to render.
1015
+ const hasLegend =
1016
+ parsed.tagGroups.length > 0 || (hasDescriptions && controlsHost !== 'app');
999
1017
 
1000
1018
  if (hasLegend) {
1001
- // Build controls group for description toggle
1019
+ // Build controls group for description toggle. App-hosted controls own the
1020
+ // toggling, so the group is built (to gate + size the row) even without the
1021
+ // inline-gear callback.
1002
1022
  let controlsGroup: { toggles: ControlsGroupToggle[] } | undefined;
1003
- if (hasDescriptions && onToggleDescriptions) {
1023
+ if (hasDescriptions && (onToggleDescriptions || controlsHost === 'app')) {
1004
1024
  controlsGroup = {
1005
1025
  toggles: [
1006
1026
  {
@@ -1018,7 +1038,14 @@ export function renderBoxesAndLines(
1018
1038
  groups: parsed.tagGroups,
1019
1039
  position: { placement: 'top-center', titleRelation: 'below-title' },
1020
1040
  mode: exportMode ? 'export' : 'preview',
1041
+ // Keep inactive sibling tag groups visible as collapsed pills so the user
1042
+ // can click one to flip the active colouring dimension (preview only —
1043
+ // export shows just the active group). Without this, declaring a second
1044
+ // tag group (e.g. Team) leaves it invisible whenever another group is
1045
+ // active. The app's BoxesAndLinesPreview already wires pill clicks.
1046
+ showInactivePills: true,
1021
1047
  ...(controlsGroup !== undefined && { controlsGroup }),
1048
+ ...(controlsHost !== undefined && { controlsHost }),
1022
1049
  };
1023
1050
  const legendState: LegendState = {
1024
1051
  activeGroup,
package/src/cli.ts CHANGED
@@ -136,7 +136,7 @@ Key options:
136
136
  - \`-o <file>\` — output file; format inferred from extension (\`.svg\` → SVG, else PNG)
137
137
  - \`-o url\` — output a shareable diagrammo.app URL
138
138
  - \`--theme <theme>\` — \`light\` (default), \`dark\`, \`transparent\`
139
- - \`--palette <name>\` — \`nord\` (default), \`solarized\`, \`catppuccin\`, \`rose-pine\`, \`gruvbox\`, \`tokyo-night\`, \`one-dark\`, \`bold\`
139
+ - \`--palette <name>\` — \`nord\` (default), \`atlas\`, \`blueprint\`, \`slate\`, \`tidewater\`, \`solarized\`, \`catppuccin\`, \`rose-pine\`, \`gruvbox\`, \`tokyo-night\`, \`one-dark\`, \`dracula\`, \`monokai\`
140
140
  - \`--copy\` — copy the URL to clipboard (use with \`-o url\`)
141
141
  - \`--chart-types\` — list all supported chart types
142
142
 
package/src/completion.ts CHANGED
@@ -98,9 +98,12 @@ const GLOBAL_DIRECTIVES: Record<string, DirectiveValueSpec> = {
98
98
  'gruvbox',
99
99
  'tokyo-night',
100
100
  'one-dark',
101
- 'bold',
102
101
  'dracula',
103
102
  'monokai',
103
+ 'atlas',
104
+ 'blueprint',
105
+ 'slate',
106
+ 'tidewater',
104
107
  ],
105
108
  },
106
109
  theme: {
@@ -508,19 +511,12 @@ export const COMPLETION_REGISTRY = new Map<string, DirectiveSpec>([
508
511
  ],
509
512
  [
510
513
  'map',
511
- // Geographic map directives (§24B.2/.7). `poi`/`route` are content
512
- // keywords, not directives; metadata keys (value/label/style) live in the
513
- // reserved-key registry.
514
+ // Geographic map directives (§24B.2/.7). Cosmetics are ON by default — the
515
+ // only switches are bare `no-*` opt-outs, surfaced proactively so a
516
+ // zero-config map still hints at what can be turned off. `poi`/`route` are
517
+ // content keywords, not directives; metadata keys (value/label/style) live
518
+ // in the reserved-key registry.
514
519
  withGlobals({
515
- region: {
516
- description:
517
- 'Basemap: us-states (force US state mesh + scoping) | world (inert — already the default)',
518
- values: ['us-states', 'world'],
519
- },
520
- projection: {
521
- description: 'Override the auto projection',
522
- values: ['equirectangular', 'natural-earth', 'albers-usa', 'mercator'],
523
- },
524
520
  'region-metric': { description: 'Label for the region value ramp' },
525
521
  'poi-metric': {
526
522
  description: 'Label for the POI value (marker size) channel',
@@ -528,21 +524,32 @@ export const COMPLETION_REGISTRY = new Map<string, DirectiveSpec>([
528
524
  'flow-metric': {
529
525
  description: 'Label for the edge/leg value (thickness) channel',
530
526
  },
531
- scale: { description: 'Override value ramp anchors: scale <min> <max>' },
532
- 'region-labels': {
533
- description: 'Subdivision name labels',
534
- values: ['full', 'abbrev', 'off'],
527
+ locale: {
528
+ description:
529
+ 'Default country/state for bare place names, e.g. locale US-GA',
535
530
  },
536
- 'poi-labels': {
537
- description: 'POI labels/values',
538
- values: ['off', 'auto', 'all'],
531
+ 'active-tag': {
532
+ description: 'Which tag group leads when several are present',
539
533
  },
540
- 'default-country': { description: 'ISO scope for bare city resolution' },
541
- 'default-state': { description: 'ISO subdivision scope' },
534
+ caption: { description: 'Caption line (data-source attribution)' },
542
535
  'no-legend': { description: 'Suppress the legend' },
543
- relief: { description: 'Subtle mountain-range relief shading' },
544
- subtitle: { description: 'Subtitle line' },
545
- caption: { description: 'Caption line' },
536
+ 'no-coastline': {
537
+ description: 'Turn off coastal water-lines (on by default)',
538
+ },
539
+ 'no-relief': {
540
+ description: 'Turn off mountain-range relief shading (on by default)',
541
+ },
542
+ 'no-context-labels': {
543
+ description: 'Turn off orientation labels for water + nearby countries',
544
+ },
545
+ 'no-region-labels': {
546
+ description: 'Turn off subdivision name labels (on by default)',
547
+ },
548
+ 'no-poi-labels': { description: 'Turn off POI labels (on by default)' },
549
+ 'no-colorize': {
550
+ description:
551
+ 'Force plain green-land reference dress (regions are auto-coloured by default)',
552
+ },
546
553
  }),
547
554
  ],
548
555
  ]);
@@ -48,6 +48,10 @@ export interface CycleRenderOptions {
48
48
  onToggleDescriptions?: (active: boolean) => void;
49
49
  onToggleControlsExpand?: () => void;
50
50
  exportMode?: boolean;
51
+ /** When 'app', the description toggle is hosted by the app overlay strip:
52
+ * the inline gear is suppressed and a controls row + anchor are reserved.
53
+ * Default (inline) renders the gear as before. */
54
+ controlsHost?: 'app' | 'inline';
51
55
  }
52
56
 
53
57
  /**
@@ -92,7 +96,13 @@ export function renderCycle(
92
96
  const hasDescriptions =
93
97
  parsed.nodes.some((n) => n.description.length > 0) ||
94
98
  parsed.edges.some((e) => e.description.length > 0);
95
- const hasLegend = hasDescriptions && !!renderOptions?.onToggleDescriptions;
99
+ // App-hosted: controls live in the app overlay strip. Cycle has no tag groups,
100
+ // so there's no in-SVG legend left to render — don't reserve a legend band.
101
+ const appHostedControls = renderOptions?.controlsHost === 'app';
102
+ const hasLegend =
103
+ !appHostedControls &&
104
+ hasDescriptions &&
105
+ !!renderOptions?.onToggleDescriptions;
96
106
 
97
107
  const showTitle = !!parsed.title && parsed.options['no-title'] !== 'on';
98
108
  const legendOffset = hasLegend ? sLegendHeight : 0;
@@ -160,6 +170,9 @@ export function renderCycle(
160
170
  position: { placement: 'top-center', titleRelation: 'below-title' },
161
171
  mode: renderOptions?.exportMode ? 'export' : 'preview',
162
172
  controlsGroup,
173
+ ...(renderOptions?.controlsHost !== undefined && {
174
+ controlsHost: renderOptions.controlsHost,
175
+ }),
163
176
  };
164
177
  const legendState: LegendState = {
165
178
  activeGroup: null,
package/src/d3.ts CHANGED
@@ -8459,6 +8459,7 @@ export async function renderForExport(
8459
8459
  const { parseMap } = await import('./map/parser');
8460
8460
  const { resolveMap } = await import('./map/resolver');
8461
8461
  const { renderMapForExport } = await import('./map/renderer');
8462
+ const { mapExportDimensions } = await import('./map/dimensions');
8462
8463
 
8463
8464
  const effectivePalette = await resolveExportPalette(theme, palette);
8464
8465
  const mapParsed = parseMap(content);
@@ -8478,14 +8479,19 @@ export async function renderForExport(
8478
8479
  }
8479
8480
  const mapResolved = resolveMap(mapParsed, mapData);
8480
8481
 
8481
- const container = createExportContainer(EXPORT_WIDTH, EXPORT_HEIGHT);
8482
+ // Content-aware canvas: derive the height from the map's intrinsic projected
8483
+ // aspect (world ~2.3:1, a region taller, etc.) instead of the fixed 800, so the
8484
+ // export matches the content's natural shape — no vertical stretch, no
8485
+ // letterbox bands. `preferContain` rides along to the renderer.
8486
+ const dims = mapExportDimensions(mapResolved, mapData, EXPORT_WIDTH);
8487
+ const container = createExportContainer(dims.width, dims.height);
8482
8488
  renderMapForExport(
8483
8489
  container,
8484
8490
  mapResolved,
8485
8491
  mapData,
8486
8492
  effectivePalette,
8487
8493
  theme === 'dark',
8488
- { width: EXPORT_WIDTH, height: EXPORT_HEIGHT }
8494
+ dims
8489
8495
  );
8490
8496
  return finalizeSvgExport(container, theme, effectivePalette);
8491
8497
  }
@@ -200,6 +200,10 @@ const ATTRIBUTE_KEYS = new Set([
200
200
  'tech',
201
201
  'span',
202
202
  'split',
203
+ // Map (§24B) reserved keys
204
+ 'value',
205
+ 'label',
206
+ 'style',
203
207
  ]);
204
208
 
205
209
  /**
@@ -80,11 +80,10 @@ export const METADATA_KEYS = new Set([
80
80
  'quadrant',
81
81
  'ring',
82
82
  'trend',
83
- // Map (§24B) metadata keys
84
- 'score',
83
+ // Map (§24B) reserved metadata keys
84
+ 'value',
85
85
  'label',
86
- 'description',
87
- 'weight',
86
+ 'style',
88
87
  ]);
89
88
 
90
89
  /** Tag declaration keyword. */
@@ -150,22 +149,20 @@ export const DIRECTIVE_KEYWORDS = new Set([
150
149
  // Sequence
151
150
  'activations',
152
151
  'no-activations',
153
- // Map (§24B) directives
154
- 'region',
155
- 'projection',
152
+ // Map (§24B) directives — cosmetics on by default, bare `no-*` opt-outs
156
153
  'region-metric',
157
154
  'poi-metric',
158
155
  'flow-metric',
159
- 'region-labels',
160
- 'poi-labels',
161
- 'default-country',
162
- 'default-state',
163
- 'no-legend',
164
- 'no-insets',
165
- 'muted',
166
- 'natural',
167
- 'subtitle',
156
+ 'locale',
157
+ 'active-tag',
168
158
  'caption',
159
+ 'no-legend',
160
+ 'no-coastline',
161
+ 'no-relief',
162
+ 'no-context-labels',
163
+ 'no-region-labels',
164
+ 'no-poi-labels',
165
+ 'no-colorize',
169
166
  'poi',
170
167
  'route',
171
168
  // Data charts
@@ -2080,9 +2080,13 @@ function renderLegend(
2080
2080
  isDark: boolean,
2081
2081
  activeGroup: string | null,
2082
2082
  playback?: InfraPlaybackState,
2083
- exportMode = false
2083
+ exportMode = false,
2084
+ controlsHost?: 'app' | 'inline'
2084
2085
  ) {
2085
2086
  if (legendGroups.length === 0 && !playback) return;
2087
+ // App-hosted playback: the play/pause + speed UI lives in the app overlay
2088
+ // strip, so suppress the in-SVG Playback pill and emit the controls anchor.
2089
+ const appHostedPlayback = controlsHost === 'app' && !!playback;
2086
2090
 
2087
2091
  const legendG = rootSvg
2088
2092
  .append('g')
@@ -2097,8 +2101,9 @@ function renderLegend(
2097
2101
  name: g.name,
2098
2102
  entries: g.entries.map((e) => ({ value: e.value, color: e.color })),
2099
2103
  }));
2100
- // Add Playback as a group with empty entries (collapsed pill) or dummy entries (expanded)
2101
- if (playback) {
2104
+ // Add Playback as a group with empty entries (collapsed pill) or dummy entries
2105
+ // (expanded) — unless the app hosts it, in which case it's suppressed.
2106
+ if (playback && !appHostedPlayback) {
2102
2107
  allGroups.push({ name: 'Playback', entries: [] });
2103
2108
  }
2104
2109
 
@@ -2107,6 +2112,20 @@ function renderLegend(
2107
2112
  position: { placement: 'top-center', titleRelation: 'below-title' },
2108
2113
  mode: exportMode ? 'export' : 'preview',
2109
2114
  showEmptyGroups: true,
2115
+ ...(appHostedPlayback && {
2116
+ controlsHost: 'app' as const,
2117
+ controlsGroup: {
2118
+ toggles: [
2119
+ {
2120
+ id: 'playback',
2121
+ type: 'toggle' as const,
2122
+ label: 'Playback',
2123
+ active: true,
2124
+ onToggle: () => {},
2125
+ },
2126
+ ],
2127
+ },
2128
+ }),
2110
2129
  };
2111
2130
  const legendState: LegendState = { activeGroup };
2112
2131
  renderLegendD3(
@@ -2233,9 +2252,13 @@ export function renderInfra(
2233
2252
  playback?: InfraPlaybackState | null,
2234
2253
  expandedNodeIds?: Set<string> | null,
2235
2254
  exportMode?: boolean,
2236
- collapsedNodes?: Set<string> | null
2255
+ collapsedNodes?: Set<string> | null,
2256
+ /** When 'app', the playback pill is suppressed and a controls row + anchor are
2257
+ * reserved for the app overlay strip (play/pause + speed live there). */
2258
+ controlsHost?: 'app' | 'inline'
2237
2259
  ) {
2238
2260
  d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
2261
+ const appHostedPlayback = controlsHost === 'app' && !!playback;
2239
2262
 
2240
2263
  const ctx = ScaleContext.identity();
2241
2264
  const sc = buildScaledConstants(ctx);
@@ -2246,7 +2269,10 @@ export function renderInfra(
2246
2269
  palette,
2247
2270
  layout.edges
2248
2271
  );
2249
- const hasLegend = legendGroups.length > 0 || !!playback;
2272
+ // App-hosted: the playback pill moves to the app overlay, so a playback-only
2273
+ // legend (no tag groups) has nothing left to render.
2274
+ const hasLegend =
2275
+ legendGroups.length > 0 || (!!playback && !appHostedPlayback);
2250
2276
  const fixedLegend = !exportMode && hasLegend;
2251
2277
  const legendDynamicH = hasLegend
2252
2278
  ? getMaxLegendReservedHeight(
@@ -2461,7 +2487,8 @@ export function renderInfra(
2461
2487
  isDark,
2462
2488
  activeGroup ?? null,
2463
2489
  playback ?? undefined,
2464
- exportMode
2490
+ exportMode,
2491
+ controlsHost
2465
2492
  );
2466
2493
  // Re-enable pointer events on interactive legend elements
2467
2494
  legendSvg
@@ -2478,7 +2505,8 @@ export function renderInfra(
2478
2505
  isDark,
2479
2506
  activeGroup ?? null,
2480
2507
  playback ?? undefined,
2481
- exportMode
2508
+ exportMode,
2509
+ controlsHost
2482
2510
  );
2483
2511
  }
2484
2512
  }