@diagrammo/dgmo 0.47.0 → 0.48.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 (345) hide show
  1. package/dist/advanced.js +11 -11
  2. package/dist/auto.js +16 -16
  3. package/dist/auto.mjs +41 -12
  4. package/dist/block.js +4 -4
  5. package/dist/{chunk-FNB3PNGF.js → chunk-7DTWEFUE.js} +3 -3
  6. package/dist/{chunk-2DXMZRWJ.js → chunk-B65G5DNL.js} +8 -1
  7. package/dist/{chunk-RZCF2VVM.js → chunk-B7JUCQUA.js} +3 -1
  8. package/dist/{chunk-DU7W62N5.js → chunk-FANX6UJR.js} +3 -3
  9. package/dist/{chunk-6FZYCKXB.js → chunk-HSH73DP6.js} +2 -2
  10. package/dist/{chunk-P4RH4OSP.js → chunk-ILKORZ3H.js} +2 -2
  11. package/dist/{chunk-Y5QGVTVC.js → chunk-JTSMEYIZ.js} +7 -3
  12. package/dist/{chunk-RCERWOJR.js → chunk-ODZVHU3R.js} +1 -2
  13. package/dist/{chunk-DZAQBW35.js → chunk-SGZYXELT.js} +3 -7
  14. package/dist/{chunk-CUVHQHZ7.js → chunk-THXCFRTU.js} +3 -3
  15. package/dist/{chunk-IH6ZMVQ5.js → chunk-UC7TFHUM.js} +29 -11
  16. package/dist/{chunk-EMS2SK26.js → chunk-ZBWMSD5N.js} +3 -1
  17. package/dist/cli.cjs +15 -15
  18. package/dist/{dgmo-router-DZCIDUO5.js → dgmo-router-X35VLZLM.js} +3 -3
  19. package/dist/element.js +16 -16
  20. package/dist/element.mjs +41 -12
  21. package/dist/{flowchart-renderer-ZAEOL4DX.js → flowchart-renderer-SNDSII4D.js} +2 -2
  22. package/dist/index.d.ts +4 -2
  23. package/dist/index.js +4 -4
  24. package/dist/{parser-FUSKFGZN.js → parser-2EWECDSA.js} +2 -2
  25. package/dist/{renderer-UWA5XS74.js → renderer-4JHRUBE6.js} +3 -3
  26. package/dist/{renderer-WZRREQ2P.js → renderer-5MYBMBYO.js} +2 -2
  27. package/dist/{renderer-TOCOVLHS.js → renderer-B2OMQIQG.js} +3 -3
  28. package/dist/{renderer-2XQQQ6QR.js → renderer-N43WR5GV.js} +3 -3
  29. package/dist/{renderer-T7YKC5VP.js → renderer-ZRNG54CW.js} +3 -3
  30. package/dist/{state-renderer-RFYHSP4K.js → state-renderer-B4DQBM6M.js} +2 -2
  31. package/package.json +23 -71
  32. package/dist/advanced.cjs +0 -71648
  33. package/dist/advanced.d.cts +0 -5719
  34. package/dist/auto.cjs +0 -70648
  35. package/dist/auto.d.cts +0 -54
  36. package/dist/block.cjs +0 -70208
  37. package/dist/block.d.cts +0 -115
  38. package/dist/editor.cjs +0 -526
  39. package/dist/editor.d.cts +0 -26
  40. package/dist/element.cjs +0 -70530
  41. package/dist/element.d.cts +0 -62
  42. package/dist/highlight.cjs +0 -852
  43. package/dist/highlight.d.cts +0 -49
  44. package/dist/index.cjs +0 -70491
  45. package/dist/index.d.cts +0 -160
  46. package/dist/pert.cjs +0 -325
  47. package/dist/pert.d.cts +0 -559
  48. package/dist/themes-1-CuKpeH.d.cts +0 -908
  49. package/src/advanced.ts +0 -873
  50. package/src/arc/parser.ts +0 -15
  51. package/src/arc/renderer.ts +0 -633
  52. package/src/auto/index.ts +0 -621
  53. package/src/auto/shared.ts +0 -370
  54. package/src/auto/styles.ts +0 -73
  55. package/src/block/layout.ts +0 -198
  56. package/src/block/parser.ts +0 -427
  57. package/src/block/renderer.ts +0 -484
  58. package/src/block/types.ts +0 -67
  59. package/src/boxes-and-lines/collapse.ts +0 -96
  60. package/src/boxes-and-lines/focus.ts +0 -257
  61. package/src/boxes-and-lines/label-placement.ts +0 -339
  62. package/src/boxes-and-lines/layout-grouped.ts +0 -912
  63. package/src/boxes-and-lines/layout-layered.ts +0 -722
  64. package/src/boxes-and-lines/layout-search.ts +0 -1678
  65. package/src/boxes-and-lines/layout.ts +0 -398
  66. package/src/boxes-and-lines/parser.ts +0 -1181
  67. package/src/boxes-and-lines/renderer.ts +0 -1465
  68. package/src/boxes-and-lines/types.ts +0 -66
  69. package/src/c4/layout.ts +0 -2441
  70. package/src/c4/parser.ts +0 -1158
  71. package/src/c4/renderer.ts +0 -2008
  72. package/src/c4/types.ts +0 -89
  73. package/src/chart-type-registry.ts +0 -529
  74. package/src/chart-types.ts +0 -225
  75. package/src/chart.ts +0 -685
  76. package/src/charts-d3/bar.ts +0 -338
  77. package/src/charts-d3/chord.ts +0 -140
  78. package/src/charts-d3/function.ts +0 -147
  79. package/src/charts-d3/funnel.ts +0 -104
  80. package/src/charts-d3/heatmap.ts +0 -145
  81. package/src/charts-d3/index.ts +0 -337
  82. package/src/charts-d3/interactions.ts +0 -477
  83. package/src/charts-d3/line.ts +0 -344
  84. package/src/charts-d3/mount.ts +0 -151
  85. package/src/charts-d3/pie.ts +0 -117
  86. package/src/charts-d3/polar.ts +0 -89
  87. package/src/charts-d3/radar.ts +0 -0
  88. package/src/charts-d3/sankey.ts +0 -0
  89. package/src/charts-d3/scatter.ts +0 -185
  90. package/src/charts-d3/shared.ts +0 -208
  91. package/src/class/layout.ts +0 -326
  92. package/src/class/parser.ts +0 -635
  93. package/src/class/renderer.ts +0 -793
  94. package/src/class/types.ts +0 -58
  95. package/src/cli-banner.ts +0 -106
  96. package/src/cli.ts +0 -1080
  97. package/src/colors-diagnostics.ts +0 -35
  98. package/src/colors.ts +0 -379
  99. package/src/completion-types.ts +0 -27
  100. package/src/completion.ts +0 -2668
  101. package/src/cycle/layout.ts +0 -1026
  102. package/src/cycle/parser.ts +0 -387
  103. package/src/cycle/renderer.ts +0 -653
  104. package/src/cycle/types.ts +0 -101
  105. package/src/d3.ts +0 -1434
  106. package/src/data-chart-parser.ts +0 -1007
  107. package/src/dgmo-router.ts +0 -296
  108. package/src/diagnostics-registry.ts +0 -108
  109. package/src/diagnostics.ts +0 -0
  110. package/src/dimensions.ts +0 -20
  111. package/src/directives-registry.ts +0 -396
  112. package/src/editor/dgmo.grammar +0 -76
  113. package/src/editor/dgmo.grammar.d.ts +0 -2
  114. package/src/editor/dgmo.grammar.js +0 -18
  115. package/src/editor/dgmo.grammar.terms.d.ts +0 -5
  116. package/src/editor/dgmo.grammar.terms.js +0 -35
  117. package/src/editor/highlight-api.ts +0 -596
  118. package/src/editor/highlight.ts +0 -36
  119. package/src/editor/index.ts +0 -27
  120. package/src/editor/keywords.ts +0 -128
  121. package/src/editor/tokens.ts +0 -30
  122. package/src/element/index.ts +0 -432
  123. package/src/embed/css.ts +0 -312
  124. package/src/embed/escape.ts +0 -15
  125. package/src/embed/index.ts +0 -280
  126. package/src/er/classify.ts +0 -228
  127. package/src/er/layout.ts +0 -530
  128. package/src/er/parser.ts +0 -750
  129. package/src/er/renderer.ts +0 -725
  130. package/src/er/types.ts +0 -52
  131. package/src/error-card.ts +0 -269
  132. package/src/event-line/diagnostics.ts +0 -117
  133. package/src/event-line/parser.ts +0 -643
  134. package/src/event-line/renderer.ts +0 -1742
  135. package/src/event-line/types.ts +0 -74
  136. package/src/fonts.ts +0 -3
  137. package/src/gantt/calculator.ts +0 -979
  138. package/src/gantt/parser.ts +0 -1641
  139. package/src/gantt/renderer.ts +0 -3787
  140. package/src/gantt/resolver.ts +0 -172
  141. package/src/gantt/types.ts +0 -206
  142. package/src/graph/diagnostics.ts +0 -64
  143. package/src/graph/edge-spline.ts +0 -29
  144. package/src/graph/flowchart-parser.ts +0 -759
  145. package/src/graph/flowchart-renderer.ts +0 -817
  146. package/src/graph/layout.ts +0 -472
  147. package/src/graph/notes.ts +0 -21
  148. package/src/graph/state-collapse.ts +0 -78
  149. package/src/graph/state-parser.ts +0 -574
  150. package/src/graph/state-renderer.ts +0 -653
  151. package/src/graph/types.ts +0 -65
  152. package/src/index.ts +0 -261
  153. package/src/infra/compute.ts +0 -1385
  154. package/src/infra/layout.ts +0 -888
  155. package/src/infra/parser.ts +0 -1234
  156. package/src/infra/renderer.ts +0 -2534
  157. package/src/infra/roles.ts +0 -75
  158. package/src/infra/types.ts +0 -216
  159. package/src/infra/validation.ts +0 -201
  160. package/src/journey-map/layout.ts +0 -490
  161. package/src/journey-map/parser.ts +0 -557
  162. package/src/journey-map/renderer.ts +0 -1580
  163. package/src/journey-map/types.ts +0 -47
  164. package/src/kanban/mutations.ts +0 -175
  165. package/src/kanban/parser.ts +0 -509
  166. package/src/kanban/renderer.ts +0 -1449
  167. package/src/kanban/types.ts +0 -39
  168. package/src/label-layout.ts +0 -368
  169. package/src/map/colorize.ts +0 -54
  170. package/src/map/completion.ts +0 -316
  171. package/src/map/context-labels.ts +0 -609
  172. package/src/map/data/PROVENANCE.json +0 -1
  173. package/src/map/data/README.md +0 -40
  174. package/src/map/data/airport-collisions.json +0 -1
  175. package/src/map/data/airports.json +0 -1
  176. package/src/map/data/gazetteer.json +0 -1
  177. package/src/map/data/lakes.json +0 -1
  178. package/src/map/data/mountain-ranges.json +0 -1
  179. package/src/map/data/na-lakes.json +0 -1
  180. package/src/map/data/na-land.json +0 -1
  181. package/src/map/data/region-names.json +0 -1
  182. package/src/map/data/rivers.json +0 -1
  183. package/src/map/data/types.ts +0 -121
  184. package/src/map/data/us-states.json +0 -1
  185. package/src/map/data/water-bodies.json +0 -1
  186. package/src/map/data/world-coarse.json +0 -1
  187. package/src/map/data/world-detail.json +0 -1
  188. package/src/map/diagnostics.ts +0 -134
  189. package/src/map/dimensions.ts +0 -150
  190. package/src/map/geo-query.ts +0 -295
  191. package/src/map/geo.ts +0 -438
  192. package/src/map/invert.ts +0 -111
  193. package/src/map/layout.ts +0 -5069
  194. package/src/map/legend-band.ts +0 -127
  195. package/src/map/load-data.ts +0 -186
  196. package/src/map/parser.ts +0 -783
  197. package/src/map/renderer.ts +0 -1445
  198. package/src/map/resolved-types.ts +0 -160
  199. package/src/map/resolver.ts +0 -1146
  200. package/src/map/types.ts +0 -194
  201. package/src/mindmap/collapse.ts +0 -67
  202. package/src/mindmap/layout.ts +0 -649
  203. package/src/mindmap/parser.ts +0 -430
  204. package/src/mindmap/renderer.ts +0 -637
  205. package/src/mindmap/text-wrap.ts +0 -218
  206. package/src/mindmap/types.ts +0 -56
  207. package/src/org/collapse.ts +0 -151
  208. package/src/org/layout.ts +0 -1262
  209. package/src/org/parser.ts +0 -519
  210. package/src/org/renderer.ts +0 -866
  211. package/src/org/resolver.ts +0 -636
  212. package/src/palettes/atlas.ts +0 -77
  213. package/src/palettes/blueprint.ts +0 -73
  214. package/src/palettes/catppuccin.ts +0 -84
  215. package/src/palettes/color-utils.ts +0 -443
  216. package/src/palettes/index.ts +0 -67
  217. package/src/palettes/nord.ts +0 -67
  218. package/src/palettes/registry.ts +0 -114
  219. package/src/palettes/slate.ts +0 -73
  220. package/src/palettes/tidewater.ts +0 -73
  221. package/src/palettes/tokyo-night.ts +0 -86
  222. package/src/palettes/types.ts +0 -88
  223. package/src/pert/analyzer.ts +0 -1183
  224. package/src/pert/diagnostics.ts +0 -110
  225. package/src/pert/index.ts +0 -43
  226. package/src/pert/internal.ts +0 -252
  227. package/src/pert/layout.ts +0 -971
  228. package/src/pert/monte-carlo.ts +0 -446
  229. package/src/pert/parser.ts +0 -1606
  230. package/src/pert/renderer.ts +0 -4009
  231. package/src/pert/share-normalize.ts +0 -62
  232. package/src/pert/types.ts +0 -604
  233. package/src/pyramid/parser.ts +0 -185
  234. package/src/pyramid/renderer.ts +0 -768
  235. package/src/pyramid/types.ts +0 -28
  236. package/src/quadrant/parser.ts +0 -18
  237. package/src/quadrant/renderer.ts +0 -638
  238. package/src/raci/diagnostics.ts +0 -213
  239. package/src/raci/index.ts +0 -23
  240. package/src/raci/mutations.ts +0 -321
  241. package/src/raci/parser.ts +0 -699
  242. package/src/raci/renderer.ts +0 -1627
  243. package/src/raci/types.ts +0 -82
  244. package/src/raci/variants.ts +0 -320
  245. package/src/render.ts +0 -268
  246. package/src/ring/parser.ts +0 -212
  247. package/src/ring/renderer.ts +0 -490
  248. package/src/ring/types.ts +0 -27
  249. package/src/sequence/collapse.ts +0 -174
  250. package/src/sequence/parser.ts +0 -1596
  251. package/src/sequence/participant-inference.ts +0 -152
  252. package/src/sequence/renderer.ts +0 -3168
  253. package/src/sequence/tag-resolution.ts +0 -164
  254. package/src/sharing.ts +0 -211
  255. package/src/sitemap/collapse.ts +0 -100
  256. package/src/sitemap/layout.ts +0 -925
  257. package/src/sitemap/parser.ts +0 -755
  258. package/src/sitemap/renderer.ts +0 -828
  259. package/src/sitemap/types.ts +0 -42
  260. package/src/slope/parser.ts +0 -18
  261. package/src/slope/renderer.ts +0 -378
  262. package/src/swimlane/diagnostics.ts +0 -94
  263. package/src/swimlane/layout.ts +0 -597
  264. package/src/swimlane/parser.ts +0 -613
  265. package/src/swimlane/renderer.ts +0 -681
  266. package/src/swimlane/types.ts +0 -151
  267. package/src/tech-radar/interactive.ts +0 -1129
  268. package/src/tech-radar/layout.ts +0 -197
  269. package/src/tech-radar/parser.ts +0 -436
  270. package/src/tech-radar/renderer.ts +0 -1239
  271. package/src/tech-radar/shared.ts +0 -151
  272. package/src/tech-radar/types.ts +0 -86
  273. package/src/themes.ts +0 -22
  274. package/src/timeline/parser.ts +0 -472
  275. package/src/timeline/renderer.ts +0 -3024
  276. package/src/timeline/types.ts +0 -33
  277. package/src/timeline/viz-parser.ts +0 -21
  278. package/src/treemap/diagnostics.ts +0 -62
  279. package/src/treemap/layout-radial.ts +0 -233
  280. package/src/treemap/layout.ts +0 -189
  281. package/src/treemap/parser.ts +0 -463
  282. package/src/treemap/renderer-radial.ts +0 -438
  283. package/src/treemap/renderer.ts +0 -572
  284. package/src/treemap/treemap-shared.ts +0 -309
  285. package/src/treemap/types.ts +0 -60
  286. package/src/utils/arrow-markers.ts +0 -51
  287. package/src/utils/arrows-diagnostics.ts +0 -62
  288. package/src/utils/arrows.ts +0 -227
  289. package/src/utils/brand.ts +0 -61
  290. package/src/utils/card.ts +0 -183
  291. package/src/utils/collapse-engine/tree-edges.ts +0 -115
  292. package/src/utils/collapse-engine/tree.ts +0 -117
  293. package/src/utils/d3-helpers.ts +0 -219
  294. package/src/utils/d3-types.ts +0 -9
  295. package/src/utils/description-helpers.ts +0 -38
  296. package/src/utils/duration.ts +0 -312
  297. package/src/utils/export-container.ts +0 -41
  298. package/src/utils/expr-eval.ts +0 -27
  299. package/src/utils/fit-canvas.ts +0 -64
  300. package/src/utils/hover-styles.ts +0 -673
  301. package/src/utils/inline-markdown.ts +0 -96
  302. package/src/utils/legend-constants.ts +0 -48
  303. package/src/utils/legend-d3.ts +0 -618
  304. package/src/utils/legend-integration.ts +0 -107
  305. package/src/utils/legend-layout.ts +0 -776
  306. package/src/utils/legend-svg.ts +0 -214
  307. package/src/utils/legend-types.ts +0 -272
  308. package/src/utils/name-normalize.ts +0 -163
  309. package/src/utils/note-box/constants.ts +0 -25
  310. package/src/utils/note-box/index.ts +0 -11
  311. package/src/utils/note-box/metrics.ts +0 -90
  312. package/src/utils/note-box/svg.ts +0 -331
  313. package/src/utils/notes/bounds.ts +0 -30
  314. package/src/utils/notes/build.ts +0 -131
  315. package/src/utils/notes/index.ts +0 -18
  316. package/src/utils/notes/model.ts +0 -19
  317. package/src/utils/notes/parse.ts +0 -131
  318. package/src/utils/notes/place.ts +0 -177
  319. package/src/utils/notes/resolve.ts +0 -88
  320. package/src/utils/number-format.ts +0 -36
  321. package/src/utils/parsing.ts +0 -953
  322. package/src/utils/reserved-key-registry.ts +0 -239
  323. package/src/utils/safe-href.ts +0 -38
  324. package/src/utils/scaling.ts +0 -97
  325. package/src/utils/svg-embed.ts +0 -251
  326. package/src/utils/tag-groups.ts +0 -607
  327. package/src/utils/text-measure.ts +0 -122
  328. package/src/utils/time-ticks.ts +0 -253
  329. package/src/utils/title-constants.ts +0 -24
  330. package/src/utils/visual-conventions.ts +0 -61
  331. package/src/utils/wrapped-desc.ts +0 -87
  332. package/src/venn/parser.ts +0 -18
  333. package/src/venn/renderer.ts +0 -1047
  334. package/src/version-control/diagnostics.ts +0 -67
  335. package/src/version-control/parser.ts +0 -544
  336. package/src/version-control/renderer.ts +0 -724
  337. package/src/version-control/types.ts +0 -100
  338. package/src/visualizations/parse.ts +0 -1560
  339. package/src/visualizations/types.ts +0 -262
  340. package/src/wireframe/layout.ts +0 -479
  341. package/src/wireframe/parser.ts +0 -1114
  342. package/src/wireframe/renderer.ts +0 -1318
  343. package/src/wireframe/types.ts +0 -110
  344. package/src/wordcloud/parser.ts +0 -18
  345. package/src/wordcloud/renderer.ts +0 -357
@@ -1,609 +0,0 @@
1
- // Context-label placement (step 4, part of layout). Produces a sparse,
2
- // density-thinned ORIENTATION layer — water-body names + unreferenced notable
3
- // country names — distinct from `region-labels`. PURE + SYNC + DETERMINISTIC.
4
- //
5
- // Design (tech-spec §map-context-labels): a deliberately LOW per-view label
6
- // BUDGET is the primary noise lever; a span-derived TIER BAND orders candidates
7
- // into it; each candidate is committed only if it survives COLLISION against
8
- // every already-placed data/region/POI/route label (the `collides` closure) AND
9
- // the other context labels. Context labels place DEAD LAST and never displace
10
- // data — they only fill leftover space, degrading gracefully to zero. See
11
- // Decisions 6 (budget), 7 (dead-last), 8 (viewport/projection guards).
12
- import { mix } from '../palettes/color-utils';
13
- import type { PaletteColors } from '../palettes/types';
14
- import type { LabelRect } from '../label-layout';
15
- import { measureLegendText } from '../utils/legend-constants';
16
- import type { ProjectionFamily } from './resolved-types';
17
- import type { WaterBodies, WaterKind } from './data/types';
18
- import type { PlacedLabel } from './layout';
19
-
20
- /** A view span band → priority ordering (NOT a hard zoom cutoff, Decision 6). */
21
- export type TierBand = 'world' | 'continental' | 'regional' | 'local';
22
-
23
- /** An unreferenced country, pre-projected by layout (geo work stays in layout;
24
- * area-rank + name-fit + collision live here so the module is unit-testable). */
25
- export interface CountryCandidate {
26
- readonly name: string;
27
- /** Projected screen bbox `[x0, y0, x1, y1]` (from `path.bounds`). */
28
- readonly bbox: readonly [number, number, number, number];
29
- /** Projected screen anchor `[x, y]` (mainland anchor or `path.centroid`), or
30
- * null when the feature doesn't project to a finite point. */
31
- readonly anchor: readonly [number, number] | null;
32
- /** True when `anchor` came from a curated WORLD_LABEL_ANCHORS entry (a trusted
33
- * mainland point) rather than the area-weighted centroid. Such a country
34
- * bypasses the both-axes smear gate (its full-canvas bbox is an antimeridian
35
- * artifact, not a real footprint, so the unreliable-centroid concern is moot). */
36
- readonly curatedAnchor?: boolean;
37
- /** True for a sovereign NATION touching the viewport (Canada, Mexico) — as
38
- * opposed to a state/subdivision. Nations are labeled UNCONDITIONALLY and
39
- * OUTSIDE the budget so orientation is symmetric (a US view shows Canada AND
40
- * Mexico together, never one-arbitrarily). States/subdivisions leave this
41
- * absent and compete for the rationed budget. */
42
- readonly bordering?: boolean;
43
- /** Ordered interior placement positions (screen coords), best-first: the commit
44
- * loop tries each in turn and places at the first that clears all collisions, so
45
- * a country can dodge a data cluster onto open ground on its own land
46
- * (map-context-neighbor-labels, D7/D12). INVARIANT: either ABSENT or NON-EMPTY,
47
- * and `anchor === positions[0]` always (an empty array is illegal — F7).
48
- * Absent ⇒ single-anchor behaviour (`positions ?? [anchor]`). */
49
- readonly positions?: readonly (readonly [number, number])[];
50
- }
51
-
52
- export interface ContextLabelArgs {
53
- readonly projection: ProjectionFamily;
54
- readonly dLonSpan: number;
55
- readonly dLatSpan: number;
56
- readonly width: number;
57
- readonly height: number;
58
- readonly waterBodies?: WaterBodies | undefined;
59
- readonly countries: readonly CountryCandidate[];
60
- /** Muted, contrast-floored land tone for country labels — computed in layout
61
- * against the neutral basemap and shared with the orientation backdrop so a
62
- * context country and an orientation region read identically. Absent ⇒ the
63
- * legacy `palette.textMuted`. */
64
- readonly countryTone?: string | undefined;
65
- /** Muted, contrast-floored tone for water labels (against the water fill).
66
- * Absent ⇒ the legacy `mix(blue, textMuted, 50)`. */
67
- readonly waterTone?: string | undefined;
68
- readonly palette: PaletteColors;
69
- readonly project: (lon: number, lat: number) => [number, number] | null;
70
- /** Collision test against every committed data/region/POI/route obstacle. */
71
- readonly collides: (rect: LabelRect) => boolean;
72
- /** True when the screen point sits over LAND (a country/state fill) rather than
73
- * open water. WATER labels are rejected when their footprint touches land — an
74
- * ocean name belongs over the ocean (they're optional orientation aids, so drop
75
- * rather than misplace). Country labels are exempt (they label land). Optional
76
- * for unit tests; absent ⇒ no land rejection. */
77
- readonly overLand?: (x: number, y: number) => boolean;
78
- }
79
-
80
- const FONT = 11; // matches layout's on-map label font
81
- const LINE_HEIGHT = FONT + 2; // px per wrapped line — MUST match the renderer's
82
- const PADX = 4; // half-padding around a context label rect
83
- const PADY = 3;
84
- const WATER_LETTER_SPACING = 1.5; // px — cartographic spread for water names
85
- const CONTEXT_PAD = 4; // extra gap enforced between two context labels
86
- const EDGE_CLAMP_MARGIN = 8; // px inset for edge-clamped ocean labels
87
- const EDGE_CLAMP_OVERSHOOT = 0.35; // max off-frame overshoot (× dim) to still clamp
88
-
89
- // Country labels scale with their projected footprint: a big landmass (Canada,
90
- // Mexico on a US view) reads as a larger backdrop name; a small one stays at the
91
- // base font. Size metric is the footprint's linear extent (√bbox-area) as a
92
- // fraction of the canvas's linear extent (√canvas-area), so the ramp is
93
- // resolution-independent. Below MIN ⇒ base font; at/above MAX ⇒ max font; linear
94
- // between. Colour does NOT change with size — subordination is by muting (the
95
- // contrast-floored tone) + size only, never by fading toward bg (Phase 1).
96
- const COUNTRY_FONT_MAX = 22; // px ceiling for the largest footprint
97
- const COUNTRY_SIZE_FRAC_MIN = 0.06; // footprint linear-frac at base font
98
- const COUNTRY_SIZE_FRAC_MAX = 0.32; // footprint linear-frac at max font
99
-
100
- // Multi-position country dodging (map-context-neighbor-labels). A country gets
101
- // several interior on-its-own-land positions so its label can dodge a colliding
102
- // data cluster into open space instead of being dropped. Layout generates the
103
- // positions (geo work stays in layout); the commit loop below walks them
104
- // best-first and places at the first that clears all collisions.
105
- export const MAX_COUNTRY_POSITIONS = 4; // ordered positions per country (cap; D7/D15)
106
- export const COUNTRY_POS_GRID = 5; // lon/lat sampling grid resolution N (N×N cells; D9)
107
- export const COUNTRY_POS_TOPN_MARGIN = 3; // generate positions for top budget+margin (D15)
108
-
109
- // Water-kind priority within a tier (oceans first, then seas, then the rest) so
110
- // a thin budget always spends on the highest-orientation-value names.
111
- const KIND_ORDER: Record<WaterKind, number> = {
112
- ocean: 0,
113
- sea: 1,
114
- gulf: 2,
115
- bay: 3,
116
- strait: 4,
117
- channel: 5,
118
- sound: 6,
119
- };
120
-
121
- /** Span band from the larger of the two view spans (Decision 6 — priority, not
122
- * a hard gate). */
123
- export function tierBand(maxSpanDeg: number): TierBand {
124
- if (maxSpanDeg >= 90) return 'world';
125
- if (maxSpanDeg >= 20) return 'continental';
126
- if (maxSpanDeg >= 5) return 'regional';
127
- return 'local';
128
- }
129
-
130
- /** Deliberately-LOW combined label budget = f(canvas area, band). Floors to ~1
131
- * on a thumbnail and 0 on a tiny canvas (Decision 6, ADR-3; AC9). Caps the
132
- * TOTAL context labels (water + country), so `relief`/data don't get extra
133
- * headroom (Decision 13). */
134
- export function labelBudget(
135
- width: number,
136
- height: number,
137
- band: TierBand
138
- ): number {
139
- const bandCap: Record<TierBand, number> = {
140
- world: 10,
141
- continental: 9,
142
- regional: 7,
143
- local: 6,
144
- };
145
- // Area divisor lowered 150→105 (map-context-neighbor-labels, budget knob): the
146
- // old budget was so thin a regional view spent every slot on a couple of giant
147
- // landmasses, leaving the countries that ring the story unlabeled. The lower
148
- // divisor + raised band caps roughly +50% the slots so the proximity rank has
149
- // room to surface the neighbourhood. Still floors to ~1 on a thumbnail / 0 on a
150
- // tiny canvas (AC9).
151
- const area = Math.floor(Math.sqrt(Math.max(0, width * height)) / 105);
152
- return Math.max(0, Math.min(area, bandCap[band]));
153
- }
154
-
155
- /** Which water tiers/kinds are eligible at a band. World view is oceans + major
156
- * seas ONLY (never bays/sounds/minor gulfs, AC3); broader views progressively
157
- * admit smaller features by `scalerank` (AC4). */
158
- function waterEligible(tier: number, kind: WaterKind, band: TierBand): boolean {
159
- switch (band) {
160
- case 'world':
161
- return tier <= 1 && (kind === 'ocean' || kind === 'sea');
162
- case 'continental':
163
- return tier <= 2;
164
- case 'regional':
165
- return tier <= 3;
166
- case 'local':
167
- return tier <= 4;
168
- }
169
- }
170
-
171
- function insideViewport(
172
- p: readonly [number, number] | null,
173
- width: number,
174
- height: number
175
- ): p is [number, number] {
176
- return (
177
- !!p &&
178
- Number.isFinite(p[0]) &&
179
- Number.isFinite(p[1]) &&
180
- p[0] >= 0 &&
181
- p[0] <= width &&
182
- p[1] >= 0 &&
183
- p[1] <= height
184
- );
185
- }
186
-
187
- /** Rendered label width INCLUDING letter-spacing — `measureLegendText` ignores
188
- * the per-gap `letter-spacing` the renderer applies to water names, so without
189
- * this the fit/clamp math under-measures by ~`(len-1)*spacing` and the label
190
- * clips at the canvas edge. */
191
- export function labelWidth(
192
- text: string,
193
- letterSpacing: number,
194
- font: number = FONT
195
- ): number {
196
- const spacing =
197
- letterSpacing > 0 ? Math.max(0, text.length - 1) * letterSpacing : 0;
198
- return measureLegendText(text, font) + spacing + 2 * PADX;
199
- }
200
-
201
- /** Wrap a multi-word name into balanced lines, biased to wrap READILY — water
202
- * names ("North Pacific Ocean") read better stacked, and a narrower footprint
203
- * is far likelier to clear surrounding land (the hard rule below). Of every
204
- * contiguous split into ≤`maxLines`, picks the one minimising the widest line
205
- * (so it shrinks horizontally whenever a break helps); ties break to fewer
206
- * lines, then top-heavy ("North Pacific" / "Ocean", not "North" / "Pacific
207
- * Ocean"). Single-word names pass through unwrapped. */
208
- export function wrapLabel(text: string, letterSpacing: number): string[] {
209
- const words = text.split(/\s+/).filter(Boolean);
210
- if (words.length <= 1) return [text];
211
- const maxLines = words.length >= 4 ? 3 : 2;
212
- const n = words.length;
213
- type Split = { lines: string[]; cost: number; head: number };
214
- let best: Split | null = null;
215
- for (let mask = 0; mask < 1 << (n - 1); mask++) {
216
- const lines: string[] = [];
217
- let cur = [words[0]!];
218
- for (let i = 1; i < n; i++) {
219
- if (mask & (1 << (i - 1))) {
220
- lines.push(cur.join(' '));
221
- cur = [words[i]!];
222
- } else cur.push(words[i]!);
223
- }
224
- lines.push(cur.join(' '));
225
- if (lines.length > maxLines) continue;
226
- const cost = Math.round(
227
- Math.max(...lines.map((l) => labelWidth(l, letterSpacing)))
228
- );
229
- const head = labelWidth(lines[0]!, letterSpacing);
230
- if (
231
- !best ||
232
- cost < best.cost ||
233
- (cost === best.cost && lines.length < best.lines.length) ||
234
- (cost === best.cost &&
235
- lines.length === best.lines.length &&
236
- head > best.head)
237
- )
238
- best = { lines, cost, head };
239
- }
240
- return best?.lines ?? [text];
241
- }
242
-
243
- function rectAround(
244
- cx: number,
245
- cy: number,
246
- lines: readonly string[],
247
- letterSpacing: number,
248
- font: number = FONT
249
- ): LabelRect {
250
- const lineHeight = font + 2; // MUST match the renderer's per-line stride
251
- const w = Math.max(...lines.map((l) => labelWidth(l, letterSpacing, font)));
252
- const h = (lines.length - 1) * lineHeight + font + 2 * PADY;
253
- return { x: cx - w / 2, y: cy - h / 2, w, h };
254
- }
255
-
256
- function rectFits(r: LabelRect, width: number, height: number): boolean {
257
- return r.x >= 0 && r.y >= 0 && r.x + r.w <= width && r.y + r.h <= height;
258
- }
259
-
260
- function overlapsPadded(a: LabelRect, b: LabelRect, pad: number): boolean {
261
- return (
262
- a.x - pad < b.x + b.w &&
263
- a.x + a.w + pad > b.x &&
264
- a.y - pad < b.y + b.h &&
265
- a.y + a.h + pad > b.y
266
- );
267
- }
268
-
269
- /** Place the orientation backdrop. Returns committed labels in priority order;
270
- * caller pushes them onto `labels` LAST so they never displace data. */
271
- export function placeContextLabels(args: ContextLabelArgs): PlacedLabel[] {
272
- const {
273
- projection,
274
- dLonSpan,
275
- dLatSpan,
276
- width,
277
- height,
278
- waterBodies,
279
- countries,
280
- palette,
281
- countryTone,
282
- waterTone,
283
- project,
284
- collides,
285
- overLand,
286
- } = args;
287
-
288
- // albers-usa is supported: the CONUS conic projects CONUS-area water (Gulf of
289
- // America, the Pacific/Atlantic margins) correctly, and the viewport-visibility
290
- // check below drops the off-frame anchors (Gulf of Alaska, mid-Pacific) that the
291
- // AK/HI inset relocation would otherwise mislabel. The caller additionally feeds
292
- // the AK/HI inset frames into `collides` so a label never lands on an inset box.
293
- // (Supersedes the original blanket albers-usa disable — the US map is the
294
- // flagship orientation case.)
295
- void projection;
296
-
297
- const band = tierBand(Math.max(dLonSpan, dLatSpan));
298
- const budget = labelBudget(width, height, band);
299
- if (budget <= 0) return [];
300
-
301
- // Subordinate cartographic colours. Both are muted but CONTRAST-FLOORED in the
302
- // caller (layout) against the substrate they sit on, and shared with the
303
- // orientation backdrop so a context country and an orientation region match.
304
- // Water stays the blue-gray italic convention; country = the muted land tone.
305
- // Fallbacks keep the pre-floor behaviour for unit tests that omit the tones.
306
- const waterColor =
307
- waterTone ?? mix(palette.colors.blue, palette.textMuted, 50);
308
- const countryColor = countryTone ?? palette.textMuted;
309
- const haloColor = palette.bg;
310
-
311
- type Candidate = {
312
- text: string;
313
- lines: string[];
314
- cx: number;
315
- cy: number;
316
- italic: boolean;
317
- letterSpacing: number;
318
- color: string;
319
- fontSize: number;
320
- sort: number; // priority key (lower first)
321
- // A sovereign nation (Canada/Mexico): committed unconditionally, outside the
322
- // budget (Option A symmetric orientation). Absent on water + states.
323
- bordering?: boolean;
324
- // Ordered dodge positions (screen coords), best-first. Absent on water (single
325
- // anchor); on a country, mirrors CountryCandidate.positions (anchor === [0]).
326
- positions?: readonly (readonly [number, number])[];
327
- };
328
- const candidates: Candidate[] = [];
329
-
330
- // -- Water candidates (priority core: oceans → seas → minor water) --
331
- const center: [number, number] = [width / 2, height / 2];
332
- for (const e of waterBodies?.entries ?? []) {
333
- const [lat, lon, name, tier, kind, alt] = e;
334
- if (!waterEligible(tier, kind, band)) continue;
335
- // Wrap eagerly (Decision: water names stack readily) so the clamp/fit math
336
- // below sees the real, narrower wrapped footprint, not the one-line width.
337
- const wlines = wrapLabel(name, WATER_LETTER_SPACING);
338
- // Multi-anchor (Decision 5 / ADR-4): of the anchors that project inside the
339
- // viewport, pick the one nearest the viewport centre.
340
- const anchorsLngLat: Array<[number, number]> = [[lon, lat]];
341
- for (const a of alt ?? []) anchorsLngLat.push([a[1], a[0]]);
342
- let best: [number, number] | null = null;
343
- let bestD = Infinity;
344
- let nearestProj: [number, number] | null = null; // best finite proj (any side)
345
- let nearestProjD = Infinity;
346
- for (const [aLon, aLat] of anchorsLngLat) {
347
- const p = project(aLon, aLat);
348
- if (!p || !Number.isFinite(p[0]) || !Number.isFinite(p[1])) continue;
349
- const d = (p[0] - center[0]) ** 2 + (p[1] - center[1]) ** 2;
350
- if (d < nearestProjD) {
351
- nearestProjD = d;
352
- nearestProj = p;
353
- }
354
- if (!insideViewport(p, width, height)) continue;
355
- if (d < bestD) {
356
- bestD = d;
357
- best = p;
358
- }
359
- }
360
- // Oceans (tier 0) are large enough that a frame-edge label still reads
361
- // correctly, so when their anchor falls off-screen on a zoomed-in view
362
- // (e.g. the mid-Atlantic/Pacific centroid on a US map) we CLAMP it to the
363
- // viewport margin rather than drop it — the standard cartographic "ocean
364
- // name hugs the edge" behaviour. Smaller basins keep the strict drop-if-
365
- // off-screen rule (AC10) to avoid mislabelling an adjacent basin.
366
- if (!best && tier === 0 && nearestProj) {
367
- // Only clamp an ocean ADJACENT to the frame: if its centroid overshoots
368
- // the viewport by more than ~half a dimension it's a distant ocean (e.g.
369
- // the South Atlantic / Arctic relative to a US view) and edge-clamping it
370
- // would mislabel that margin — drop instead. The surviving oceans are the
371
- // ones the frame actually borders (Pacific to the west, Atlantic east).
372
- const overX = Math.max(0, -nearestProj[0], nearestProj[0] - width);
373
- const overY = Math.max(0, -nearestProj[1], nearestProj[1] - height);
374
- if (
375
- overX <= width * EDGE_CLAMP_OVERSHOOT &&
376
- overY <= height * EDGE_CLAMP_OVERSHOOT
377
- ) {
378
- // Clamp the CENTRE inward by half the label so the whole (centre-
379
- // anchored) rect stays on-canvas — clamping to the bare margin would
380
- // overflow a wide name like "North Atlantic Ocean" off the edge.
381
- // letter-spacing IS counted (labelWidth) so the clamp matches render.
382
- const halfW =
383
- Math.max(...wlines.map((l) => labelWidth(l, WATER_LETTER_SPACING))) /
384
- 2;
385
- const halfH = ((wlines.length - 1) * LINE_HEIGHT + FONT + 2 * PADY) / 2;
386
- const m = EDGE_CLAMP_MARGIN;
387
- best = [
388
- Math.min(Math.max(nearestProj[0], halfW + m), width - halfW - m),
389
- Math.min(Math.max(nearestProj[1], halfH + m), height - halfH - m),
390
- ];
391
- }
392
- }
393
- if (!best) continue;
394
- candidates.push({
395
- text: name,
396
- lines: wlines,
397
- cx: best[0],
398
- cy: best[1],
399
- italic: true,
400
- letterSpacing: WATER_LETTER_SPACING,
401
- color: waterColor,
402
- fontSize: FONT, // water names keep the base font (no footprint to scale on)
403
- // Orientation-value bands (lower = earlier): MAJOR water (oceans + major
404
- // seas, tier ≤ 1) leads at `tier*10+kind` (0..~16); MINOR water (tier ≥ 2:
405
- // smaller seas, bays, gulfs, straits) drops to band 2 (2000+) — BELOW the
406
- // country band (1000+), so a big country outranks a minor basin.
407
- sort: (tier <= 1 ? 0 : 2000) + tier * 10 + KIND_ORDER[kind],
408
- });
409
- }
410
-
411
- // -- Country candidates (unreferenced) --
412
- // Rank by biggest-area-first — a deterministic, position-independent order (no
413
- // proximity-to-POI weighting, which read as arbitrary: near-identical neighbours
414
- // like Wyoming vs Texas won or lost a slot on a hair of distance). Nations are
415
- // pulled out of the budget entirely at commit; among the budgeted states, the
416
- // largest in-frame footprints win the rationed slots. Area also drives the
417
- // font/fade ramp and the smear gate below.
418
- const ranked = countries
419
- .map((c) => {
420
- const [x0, y0, x1, y1] = c.bbox;
421
- const w = x1 - x0;
422
- const h = y1 - y0;
423
- return { c, w, h, area: w * h };
424
- })
425
- .filter((r) => Number.isFinite(r.area) && r.area > 0)
426
- .sort((a, b) => b.area - a.area);
427
- // Canvas linear extent — the denominator for the footprint size ramp below.
428
- const canvasLinear = Math.sqrt(Math.max(1, width * height));
429
- let ci = 0;
430
- for (const r of ranked) {
431
- const { c, w, h, area } = r;
432
- // F2: an antimeridian-crossing / global-smear country fills the canvas in
433
- // BOTH dimensions while its real landmass is split — the `path.centroid`
434
- // anchor is then unreliable (mid-map, wrong basin). Reject only that
435
- // both-axes canvas-smear: a country that is merely wide-but-short (Canada
436
- // hugging the top of a US frame) or tall-but-narrow (Chile) is a legitimate
437
- // landmass whose clipExtent-clipped centroid still lands over its own ground,
438
- // so it must NOT be dropped for footprint shape alone. A `curatedAnchor`
439
- // (WORLD_LABEL_ANCHORS, e.g. Russia → European Russia) is trustworthy by
440
- // construction — the smear concern is moot, so it bypasses this gate (the
441
- // `insideViewport` check below still drops an anchor that projects off-frame).
442
- if (!c.curatedAnchor && w > width * 0.66 && h > height * 0.66) continue;
443
- if (!insideViewport(c.anchor, width, height)) continue;
444
- // Footprint-driven scale (Decision: big landmass = large, faded backdrop
445
- // name). t∈[0,1] over the [MIN,MAX] linear-fraction band; font ramps up and
446
- // ink rises in lockstep, so a bigger name reads as a large, softly-inked
447
- // backdrop. A curated-anchor giant (Russia) keeps the standard big-country
448
- // styling — its full-canvas bbox lands at the top of the ramp (font/ink like
449
- // any large in-frame country), which is the intended subdued-backdrop look.
450
- const sizeFrac = Math.sqrt(area) / canvasLinear;
451
- const t = Math.min(
452
- 1,
453
- Math.max(
454
- 0,
455
- (sizeFrac - COUNTRY_SIZE_FRAC_MIN) /
456
- (COUNTRY_SIZE_FRAC_MAX - COUNTRY_SIZE_FRAC_MIN)
457
- )
458
- );
459
- const fontSize = Math.round(FONT + t * (COUNTRY_FONT_MAX - FONT));
460
- // No fade. Subordination is by MUTING (the floored tone) + smaller size, not
461
- // by fading toward bg — fading a floored tone toward the background would
462
- // re-break the contrast it was floored to guarantee (the old behaviour washed
463
- // big names like Canada toward illegibility). Footprint still drives font size.
464
- const color = countryColor;
465
- // Always the full country name — never an ISO abbreviation. If the name
466
- // doesn't fit the footprint, drop the label rather than abbreviate.
467
- const text = c.name;
468
- const tw = labelWidth(text, 0, fontSize);
469
- // Approximate fit (Decision 4): the (scaled) name fits inside the footprint
470
- // bbox. NOT true point-in-polygon — cartographic labels routinely overrun
471
- // coastlines. The bigger the font, the bigger the box it must clear.
472
- if (tw > w || fontSize + 2 * PADY > h) continue;
473
- candidates.push({
474
- text,
475
- lines: [text],
476
- cx: c.anchor[0],
477
- cy: c.anchor[1],
478
- italic: false,
479
- letterSpacing: 0,
480
- color,
481
- fontSize,
482
- ...(c.bordering ? { bordering: true } : {}),
483
- // Multi-position dodging: carry the ordered interior positions through to the
484
- // commit loop. Invariant anchor === positions[0], so `cx/cy` is positions[0].
485
- ...(c.positions ? { positions: c.positions } : {}),
486
- // Band 1 (orientation-value ranking): above MINOR water (band 2, 2000+) but
487
- // below MAJOR water — oceans + major seas (band 0, ≤~16). So a big country
488
- // (US, Canada, Russia) outranks a minor sea/bay (Sargasso, Bahía de
489
- // Campeche) yet never displaces an ocean. Larger area = earlier within the
490
- // band (`ci` is the area-desc rank index).
491
- sort: 1000 + ci++,
492
- });
493
- }
494
-
495
- // -- Commit dead-last, highest-priority-first, into leftover space only --
496
- candidates.sort((a, b) => a.sort - b.sort);
497
- const placed: PlacedLabel[] = [];
498
- const placedRects: LabelRect[] = [];
499
- // Guarantee state room: water can otherwise monopolise a small budget (a coastal
500
- // view borders many oceans/seas), so reserve up to 2 slots for STATES whenever
501
- // any state candidate exists. Nations don't draw from the budget (committed in
502
- // pass 1 below), so they're excluded from this count. No effect on pure-water
503
- // views (`stateCount` 0 ⇒ cap = budget).
504
- const stateCount = candidates.reduce(
505
- (n, c) => n + (!c.italic && !c.bordering ? 1 : 0),
506
- 0
507
- );
508
- const waterCap = budget - Math.min(2, stateCount);
509
- let waterPlaced = 0;
510
- let budgetUsed = 0;
511
- // Test one trial position for a candidate against every gate (fit, water-on-land,
512
- // committed-obstacle collision, context-overlap). Returns the placed rect when the
513
- // position clears, else null. Country candidates carry several ordered positions
514
- // (map-context-neighbor-labels); we walk them best-first and commit the first that
515
- // clears, so a label dodges a colliding cluster instead of being dropped. Water
516
- // candidates have a single position, so behaviour is unchanged for them.
517
- const gateAt = (
518
- cx: number,
519
- cy: number,
520
- cand: Candidate
521
- ): LabelRect | null => {
522
- const rect = rectAround(
523
- cx,
524
- cy,
525
- cand.lines,
526
- cand.letterSpacing,
527
- cand.fontSize
528
- );
529
- if (!rectFits(rect, width, height)) return null;
530
- // Water labels must sit over OPEN WATER and NEVER touch land — sample a grid
531
- // over every wrapped line (each line's own horizontal extent at five points);
532
- // drop the whole label if ANY sample hits land (Decision: optional orientation
533
- // aids, so exclude rather than misplace over a coastline). Country labels are
534
- // exempt — they belong on their country.
535
- if (cand.italic && overLand) {
536
- const inset = 2;
537
- const top = cy - ((cand.lines.length - 1) / 2) * LINE_HEIGHT;
538
- const touchesLand = cand.lines.some((line, li) => {
539
- const lw = labelWidth(line, cand.letterSpacing);
540
- const x0 = cx - lw / 2 + inset;
541
- const x1 = cx + lw / 2 - inset;
542
- const xs = [x0, (x0 + cx) / 2, cx, (cx + x1) / 2, x1];
543
- const base = top + li * LINE_HEIGHT;
544
- // Sample the glyph body top→baseline (text rises above the baseline) so a
545
- // label whose ascenders clip a coastline is rejected, not just one whose
546
- // baseline sits on land.
547
- return [base, base - FONT * 0.4, base - FONT * 0.8].some((y) =>
548
- xs.some((x) => overLand(x, y))
549
- );
550
- });
551
- if (touchesLand) return null;
552
- }
553
- if (collides(rect)) return null;
554
- if (placedRects.some((r) => overlapsPadded(rect, r, CONTEXT_PAD)))
555
- return null;
556
- return rect;
557
- };
558
- // Walk a candidate's ordered positions best-first; commit at the first that
559
- // clears every gate. F8: `positions ?? [[cx,cy]]` keeps single-anchor (water +
560
- // non-top-N country) behaviour identical. If none clears → drop (no halo, no
561
- // overlap — D16). Returns true when the label was placed.
562
- const tryPlace = (cand: Candidate): boolean => {
563
- const positions = cand.positions ?? [[cand.cx, cand.cy]];
564
- for (const [px, py] of positions) {
565
- const rect = gateAt(px!, py!, cand);
566
- if (!rect) continue;
567
- placedRects.push(rect);
568
- placed.push({
569
- x: px!,
570
- y: py!,
571
- text: cand.text,
572
- anchor: 'middle',
573
- color: cand.color,
574
- // No halo: the bg-coloured outline reads as a ghost box behind the text
575
- // over the tinted water/land. Context labels are muted enough to sit
576
- // cleanly on the basemap without one.
577
- halo: false,
578
- haloColor,
579
- fontSize: cand.fontSize,
580
- italic: cand.italic,
581
- letterSpacing: cand.letterSpacing,
582
- ...(cand.lines.length > 1 ? { lines: cand.lines } : {}),
583
- lineNumber: 0,
584
- });
585
- return true;
586
- }
587
- return false;
588
- };
589
-
590
- // Pass 1 — sovereign NATIONS touching the viewport, labeled UNCONDITIONALLY and
591
- // OUTSIDE the budget (Option A: symmetric orientation — a US view shows Canada
592
- // AND Mexico together, never one arbitrarily). Still gated on fit/collision, so a
593
- // nation label never lands on data or off-canvas; it just isn't rationed. Placed
594
- // FIRST so nations claim their ground before states/water fill the leftover.
595
- for (const cand of candidates) if (cand.bordering) tryPlace(cand);
596
-
597
- // Pass 2 — water + states, rationed by the budget (the nations placed above do
598
- // not count against it).
599
- for (const cand of candidates) {
600
- if (cand.bordering) continue;
601
- if (budgetUsed >= budget) break;
602
- if (cand.italic && waterPlaced >= waterCap) continue;
603
- if (tryPlace(cand)) {
604
- budgetUsed++;
605
- if (cand.italic) waterPlaced++;
606
- }
607
- }
608
- return placed;
609
- }
@@ -1 +0,0 @@
1
- {"assets":{"airport-collisions.json":{"bytes":29,"gzBytes":49,"sha256":"a7d0107c5fcc6d787e010ea100ce1946bb724f7ae1a79eb1b862a8cf822ed1d8"},"airports.json":{"bytes":104083,"gzBytes":38159,"sha256":"c17294d4e3fd259e02c63c49c36c02d9ff34f4462103ff54516d29e93667cd76"},"gazetteer.json":{"bytes":130767,"gzBytes":56261,"sha256":"5ad56e5ba0b3a4f9a6dc8bd3bf8b0fda0e7b86cbe4d85d231114f5dd967d65f7"},"lakes.json":{"bytes":6315,"gzBytes":1487,"sha256":"5840ffd49b8dbf30183a9534a72adf80b6e77ceec224665393fa94e956220323"},"mountain-ranges.json":{"bytes":90845,"gzBytes":26493,"sha256":"a698b3f296e61712fb39b3d8d42ec7c4699f8aadecb549367feb7d09f7785580"},"na-lakes.json":{"bytes":39387,"gzBytes":11281,"sha256":"2a41c04969209380d544a09efe354277e12d704458af95955201eb4f698d16c6"},"na-land.json":{"bytes":114082,"gzBytes":32375,"sha256":"7b94c9bb4e809c22813da5ae939e1ff6a781fd77a04d9c1585a9a82d2a195388"},"region-names.json":{"bytes":11667,"gzBytes":2235,"sha256":"059662d30b6ee8572c5943096905e05218e5f337e6973a9d43d6b41b7313a9ac"},"rivers.json":{"bytes":6707,"gzBytes":2158,"sha256":"3912508469099b1c37360c5505ea033c4ffa30ce95f7428e668e9d824cb81407"},"us-states.json":{"bytes":23313,"gzBytes":7413,"sha256":"0fe3a8937bc7566192662439f29a7866e8823d687290bcb003433ad5edd86567"},"water-bodies.json":{"bytes":4854,"gzBytes":2123,"sha256":"6d1a407a376c63518329c52189e2887053c4b61062af0597e060050ae8469635"},"world-coarse.json":{"bytes":55436,"gzBytes":18397,"sha256":"5cb42e3c8975dde56504ca5c68ece0a1e71d0929680b5fc8cdab758c8666dbf8"},"world-detail.json":{"bytes":163562,"gzBytes":46767,"sha256":"39f1736eaabe9e21190972be3157822be22ee84fdc41751237f2b516f09a7586"}},"counts":{"airportCollisions":2,"airports":1565,"countries":175,"gazetteerAliases":8,"gazetteerCities":2119,"mountainRanges":205,"usStates":56,"waterBodies":113},"generatedBy":"scripts/build-map-data.mjs","sourceHashes":{"file:scripts/airports-snapshot.csv":{"bytes":308536,"sha256":"58b6e45bb91045c2897f1e7e14430aecc3b5eca911bcb389855495ff66ab9599"},"https://cdn.jsdelivr.net/gh/martynafford/natural-earth-geojson@master/10m/physical/ne_10m_lakes.json":{"bytes":6648697,"sha256":"93c8fdf0e591e113f449d0d466e15c7a9841b9b6571c7afe41f95ba51b322452"},"https://cdn.jsdelivr.net/gh/martynafford/natural-earth-geojson@master/110m/physical/ne_110m_lakes.json":{"bytes":27711,"sha256":"6f315b60488e0cf5da9c360e3ce593babf64c2f44cc21e2820c536f7a2aff606"},"https://cdn.jsdelivr.net/gh/martynafford/natural-earth-geojson@master/110m/physical/ne_110m_rivers_lake_centerlines.json":{"bytes":54146,"sha256":"959e13128e4eb5a6ee530b8270c5017bcee9149ce48a97f6fe7fee1fce600b5d"},"https://cdn.jsdelivr.net/gh/nvkelso/natural-earth-vector@master/geojson/ne_10m_admin_0_countries.geojson":{"bytes":13287234,"sha256":"239eec57ac17f100a11e2536cffc56752c318b50ae765b0918ff7aab4ce8f255"},"https://cdn.jsdelivr.net/gh/nvkelso/natural-earth-vector@master/geojson/ne_10m_geography_regions_polys.geojson":{"bytes":5583870,"sha256":"b7b26e50ea917d3696aec87f932def2bf5f890f5770e441d59c162c6f4c92a77"},"https://cdn.jsdelivr.net/gh/nvkelso/natural-earth-vector@master/geojson/ne_110m_geography_marine_polys.geojson":{"bytes":534055,"sha256":"b9c3f7f557d0ff5217906adc82b66ecdac14aa7438df7e518cf6675d037bceb8"},"https://cdn.jsdelivr.net/gh/nvkelso/natural-earth-vector@master/geojson/ne_50m_geography_marine_polys.geojson":{"bytes":1163418,"sha256":"6fe58083e0cc5c7fad9e396970e28a8580bbd8770cfa4d1d7b5a34423e912f97"},"https://cdn.jsdelivr.net/npm/us-atlas@3.0.1/states-10m.json":{"bytes":114554,"sha256":"d76b391ccfa8bff601d51e3e3da5d43a89fa46cd5caca72ce731b383be5596d0"},"https://cdn.jsdelivr.net/npm/world-atlas@2.0.2/countries-110m.json":{"bytes":107761,"sha256":"2516c915867c7baf18ddec727aec46c315541a07cfb3d79a6559b05d5e94eee8"},"https://cdn.jsdelivr.net/npm/world-atlas@2.0.2/countries-50m.json":{"bytes":756420,"sha256":"04342cdc1e3016bcd7db1630de95684d67b79fe3c8c460321e87aef469502394"},"https://download.geonames.org/export/dump/cities5000.zip":{"bytes":5549002,"sha256":"d20e28b2f610da34c21fd82ff6a8e4d24ebe67eba2dccf65bd2c4332ff0f380a"}},"sources":{"geonames":{"citiesUrl":"https://download.geonames.org/export/dump/cities5000.zip","license":"CC BY 4.0 — https://creativecommons.org/licenses/by/4.0/","modificationDateRange":"2006-01-17..2026-06-02 (filtered subset)"},"lakes":{"url":"https://cdn.jsdelivr.net/gh/martynafford/natural-earth-geojson@master/110m/physical/ne_110m_lakes.json","version":"natural-earth 110m (martynafford snapshot)"},"marineCoarse":{"url":"https://cdn.jsdelivr.net/gh/nvkelso/natural-earth-vector@master/geojson/ne_110m_geography_marine_polys.geojson","version":"natural-earth 110m (nvkelso vector snapshot)"},"marineDetail":{"url":"https://cdn.jsdelivr.net/gh/nvkelso/natural-earth-vector@master/geojson/ne_50m_geography_marine_polys.geojson","version":"natural-earth 50m (nvkelso vector snapshot)"},"mountainRanges":{"url":"https://cdn.jsdelivr.net/gh/nvkelso/natural-earth-vector@master/geojson/ne_10m_geography_regions_polys.geojson","version":"natural-earth 10m (nvkelso vector snapshot)"},"naLakes":{"url":"https://cdn.jsdelivr.net/gh/martynafford/natural-earth-geojson@master/10m/physical/ne_10m_lakes.json","version":"natural-earth 10m (martynafford snapshot)"},"naLand":{"url":"https://cdn.jsdelivr.net/gh/nvkelso/natural-earth-vector@master/geojson/ne_10m_admin_0_countries.geojson","version":"natural-earth 10m (nvkelso vector snapshot)"},"ourairports":{"license":"Public Domain — https://ourairports.com/data/","note":"Lean committed slice (scheduled_service=yes + 3-letter iata_code). Regenerate is a deliberate, reviewed bump (ADR-4).","snapshot":"scripts/airports-snapshot.csv","source":"https://davidmegginson.github.io/ourairports-data/airports.csv"},"rivers":{"url":"https://cdn.jsdelivr.net/gh/martynafford/natural-earth-geojson@master/110m/physical/ne_110m_rivers_lake_centerlines.json","version":"natural-earth 110m (martynafford snapshot)"},"usAtlas":{"url":"https://cdn.jsdelivr.net/npm/us-atlas@3.0.1/states-10m.json","version":"3.0.1"},"worldCoarse":{"url":"https://cdn.jsdelivr.net/npm/world-atlas@2.0.2/countries-110m.json","version":"2.0.2"},"worldDetail":{"url":"https://cdn.jsdelivr.net/npm/world-atlas@2.0.2/countries-50m.json","version":"2.0.2"}},"tooling":{"mapshaper":"0.7.22"}}
@@ -1,40 +0,0 @@
1
- # map chart-type data assets
2
-
3
- Generated by `scripts/build-map-data.mjs` (run `pnpm build:map-data`). These
4
- committed JSON files are the runtime artifacts for the `map` chart type. Do not
5
- hand-edit — regenerate from source.
6
-
7
- ## Files
8
- - `world-coarse.json` / `world-detail.json` — world country boundaries (TopoJSON),
9
- keyed by ISO 3166-1 alpha-2. Coarse = world-scale; detail = regional/zoom.
10
- - `us-states.json` — US states + DC + territories (TopoJSON), keyed by ISO 3166-2.
11
- - `lakes.json` — major lakes (Natural Earth 110m, TopoJSON), drawn as water over land.
12
- - `rivers.json` — major river centerlines (Natural Earth 110m, TopoJSON), drawn as thin water lines.
13
- - `na-land.json` — NA-clipped 10m country land (TopoJSON, ISO-keyed): crisp neighbour context under the albers-usa US view.
14
- - `na-lakes.json` — NA-clipped 10m major lakes (TopoJSON): the lakes counterpart to `na-land.json` for the US view.
15
- - `mountain-ranges.json` — notable mountain ranges (Natural Earth 50m geography regions, FEATURECLA "Range/mtn", TopoJSON), drawn as a subtle gradient relief cue when the `relief` directive is on. Optional; single tier (no elevation).
16
- - `water-bodies.json` — water-body orientation labels (`{ entries: [lat, lon, name, tier, kind] }`) from Natural Earth 110m+50m geography marine polys (oceans/seas/gulfs/bays/straits/channels/sounds; rivers + reefs excluded). Anchors are mapshaper inner points; `tier` is the NE scalerank. Drawn only when the `context-labels` directive is on. Optional.
17
- - `gazetteer.json` — `{ cities, byName, alt }` city index (see `types.ts`).
18
- `byName`/`alt` reference `cities` by array index (normalized).
19
- - `PROVENANCE.json` — source versions + per-asset sha256/sizes + GeoNames date range.
20
- - `types.ts` — the typed data contract consumed by the parser/resolver/renderer.
21
-
22
- ## Sources & attribution
23
- - **Country boundaries:** Natural Earth via `world-atlas@2.0.2` (public domain).
24
- - **US states:** US Census via `us-atlas@3.0.1` (public domain).
25
- - **Mountain ranges:** Natural Earth 50m `geography_regions_polys` via `nvkelso/natural-earth-vector` (public domain).
26
- - **Water bodies:** Natural Earth 110m+50m `geography_marine_polys` via `nvkelso/natural-earth-vector` (public domain). One editorial override applied (`Gulf of Mexico` → `Gulf of America`).
27
- - **Cities:** Data © **GeoNames**, licensed under **CC BY 4.0**
28
- (https://creativecommons.org/licenses/by/4.0/) — https://www.geonames.org/.
29
-
30
- ## Determinism & GeoNames drift
31
- Geometry assets are byte-reproducible from the pinned atlas versions (same machine
32
- + `mapshaper@0.7.22`). The GeoNames dump is **rebuilt daily and has no pinnable
33
- snapshot**, so the committed `gazetteer.json` is the source of truth; a regenerate
34
- may pick up upstream city changes. `PROVENANCE.json` is the audit trail. The
35
- modification-date range it reports is over the *filtered* (kept) city subset.
36
-
37
- ## Regenerate
38
- ```
39
- pnpm build:map-data
40
- ```
@@ -1 +0,0 @@
1
- {"collisions":["aba","ufa"]}