@diagrammo/dgmo 0.19.0 → 0.20.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 (48) hide show
  1. package/dist/advanced.cjs +948 -321
  2. package/dist/advanced.d.cts +148 -54
  3. package/dist/advanced.d.ts +148 -54
  4. package/dist/advanced.js +949 -321
  5. package/dist/auto.cjs +930 -317
  6. package/dist/auto.js +117 -117
  7. package/dist/auto.mjs +934 -318
  8. package/dist/cli.cjs +160 -160
  9. package/dist/index.cjs +929 -316
  10. package/dist/index.js +933 -317
  11. package/dist/internal.cjs +948 -321
  12. package/dist/internal.d.cts +148 -54
  13. package/dist/internal.d.ts +148 -54
  14. package/dist/internal.js +949 -321
  15. package/dist/map-data/PROVENANCE.json +1 -1
  16. package/dist/map-data/lakes.json +1 -0
  17. package/dist/map-data/na-lakes.json +1 -0
  18. package/dist/map-data/na-land.json +1 -0
  19. package/dist/map-data/rivers.json +1 -0
  20. package/docs/language-reference.md +12 -7
  21. package/gallery/fixtures/map-region-scope.dgmo +15 -0
  22. package/package.json +4 -4
  23. package/src/advanced.ts +7 -6
  24. package/src/c4/parser.ts +6 -6
  25. package/src/completion.ts +6 -2
  26. package/src/echarts.ts +1 -1
  27. package/src/infra/parser.ts +10 -10
  28. package/src/journey-map/parser.ts +1 -1
  29. package/src/label-layout.ts +36 -0
  30. package/src/map/data/PROVENANCE.json +1 -1
  31. package/src/map/data/README.md +2 -0
  32. package/src/map/data/lakes.json +1 -0
  33. package/src/map/data/na-lakes.json +1 -0
  34. package/src/map/data/na-land.json +1 -0
  35. package/src/map/data/rivers.json +1 -0
  36. package/src/map/layout.ts +1022 -205
  37. package/src/map/load-data.ts +73 -17
  38. package/src/map/parser.ts +22 -13
  39. package/src/map/renderer.ts +200 -219
  40. package/src/map/resolved-types.ts +18 -1
  41. package/src/map/resolver.ts +79 -7
  42. package/src/map/types.ts +4 -0
  43. package/src/mindmap/parser.ts +1 -1
  44. package/src/sitemap/parser.ts +1 -1
  45. package/src/utils/legend-d3.ts +42 -0
  46. package/src/utils/legend-layout.ts +83 -3
  47. package/src/utils/legend-svg.ts +1 -8
  48. package/src/utils/legend-types.ts +44 -1
@@ -17,7 +17,7 @@ import type { LegendConfig, LegendState } from '../utils/legend-types';
17
17
  import type { PaletteColors } from '../palettes/types';
18
18
  import type { D3ExportDimensions } from '../utils/d3-types';
19
19
  import type { MapData, ResolvedMap } from './resolved-types';
20
- import { layoutMap, type MapLayout, type PlacedLabel } from './layout';
20
+ import { layoutMap, type MapLayoutRegion, type PlacedLabel } from './layout';
21
21
 
22
22
  const LABEL_FONT = 11;
23
23
 
@@ -29,7 +29,9 @@ export function renderMap(
29
29
  palette: PaletteColors,
30
30
  isDark: boolean,
31
31
  onClickItem?: (lineNumber: number) => void,
32
- exportDims?: D3ExportDimensions
32
+ exportDims?: D3ExportDimensions,
33
+ /** Live override of the active colouring group (interactive legend flip). */
34
+ activeGroupOverride?: string | null
33
35
  ): void {
34
36
  d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
35
37
  const width = exportDims?.width ?? container.clientWidth;
@@ -43,6 +45,9 @@ export function renderMap(
43
45
  {
44
46
  palette,
45
47
  isDark,
48
+ ...(activeGroupOverride !== undefined && {
49
+ activeGroup: activeGroupOverride,
50
+ }),
46
51
  }
47
52
  );
48
53
 
@@ -54,7 +59,12 @@ export function renderMap(
54
59
  .attr('viewBox', `0 0 ${width} ${height}`)
55
60
  .attr('preserveAspectRatio', 'xMidYMin meet')
56
61
  .attr('xmlns', 'http://www.w3.org/2000/svg')
57
- .style('font-family', FONT_FAMILY);
62
+ .style('font-family', FONT_FAMILY)
63
+ // Match the SVG element background to the water rect so any letterboxing
64
+ // (when the host container's aspect differs from the viewBox) shows water,
65
+ // not the gray palette bg that finalizeSvgExport would otherwise apply —
66
+ // i.e. no stray band above/below the map.
67
+ .style('background', layout.background);
58
68
 
59
69
  svg
60
70
  .append('rect')
@@ -62,66 +72,46 @@ export function renderMap(
62
72
  .attr('height', height)
63
73
  .attr('fill', layout.background);
64
74
 
65
- // Arrowhead marker for directed legs.
66
- const arrowColor = mix(palette.text, palette.bg, 50);
75
+ // Arrowhead markers for directed legs. Sized in user-space (NOT the SVG
76
+ // default of stroke-width units) so a heavy weighted line doesn't blow the
77
+ // arrowhead up to a giant wedge. The size grows gently with the line width —
78
+ // enough to stay distinct from the stroke — but is firmly capped.
67
79
  const defs = svg.append('defs');
68
- defs
69
- .append('marker')
70
- .attr('id', 'dgmo-map-arrow')
71
- .attr('viewBox', '0 0 10 10')
72
- .attr('refX', 9)
73
- .attr('refY', 5)
74
- .attr('markerWidth', 7)
75
- .attr('markerHeight', 7)
76
- .attr('orient', 'auto-start-reverse')
77
- .append('path')
78
- .attr('d', 'M0,0L10,5L0,10z')
79
- .attr('fill', arrowColor);
80
+ // Dampened: ~8px at the thinnest leg, easing toward a 15px cap as legs widen.
81
+ const arrowSize = (w: number): number => Math.min(15, 7 + w * 0.95);
80
82
 
81
- const haloColor = layout.background;
83
+ // Neutral bg (not the water-tinted backdrop) so label halos read over both
84
+ // land and ocean.
85
+ const haloColor = palette.bg;
82
86
 
83
- // ── Title / subtitle ──
84
- if (layout.title) {
85
- svg
86
- .append('text')
87
- .attr('x', width / 2)
88
- .attr('y', TITLE_Y)
89
- .attr('text-anchor', 'middle')
90
- .attr('font-size', TITLE_FONT_SIZE)
91
- .attr('font-weight', TITLE_FONT_WEIGHT)
92
- .attr('fill', palette.text)
93
- .text(layout.title);
94
- }
95
- if (layout.subtitle) {
96
- svg
97
- .append('text')
98
- .attr('x', width / 2)
99
- .attr('y', TITLE_Y + TITLE_FONT_SIZE)
100
- .attr('text-anchor', 'middle')
101
- .attr('font-size', LABEL_FONT + 1)
102
- .attr('fill', palette.textMuted)
103
- .text(layout.subtitle);
104
- }
105
- if (layout.caption) {
106
- svg
107
- .append('text')
108
- .attr('x', width / 2)
109
- .attr('y', height - 8)
110
- .attr('text-anchor', 'middle')
111
- .attr('font-size', LABEL_FONT)
112
- .attr('fill', palette.textMuted)
113
- .text(layout.caption);
114
- }
87
+ // Title / subtitle / caption are rendered LAST (see end of function) so they
88
+ // sit in the foreground above the basemap, POIs, and labels.
115
89
 
116
90
  // ── Regions ──
117
91
  const gRegions = svg.append('g').attr('class', 'dgmo-map-regions');
118
- for (const r of layout.regions) {
119
- const p = gRegions
92
+ const drawRegion = (
93
+ g: Sel,
94
+ r: MapLayoutRegion,
95
+ strokeWidth: number
96
+ ): void => {
97
+ const p = g
120
98
  .append('path')
121
99
  .attr('d', r.d)
122
100
  .attr('fill', r.fill)
123
101
  .attr('stroke', r.stroke)
124
- .attr('stroke-width', 0.5);
102
+ .attr('stroke-width', strokeWidth);
103
+ // Data layer? Tag it so the app can highlight on legend hover / gradient
104
+ // scrub. `data-score` for ramp-proximity, `data-tag-<group>` per tag value
105
+ // (both lowercased to match the lowercased legend-entry attributes).
106
+ if (r.layer !== 'base') {
107
+ p.classed('dgmo-map-region', true).attr('data-region', r.id);
108
+ if (r.score !== undefined) p.attr('data-score', r.score);
109
+ if (r.tags) {
110
+ for (const [group, value] of Object.entries(r.tags)) {
111
+ p.attr(`data-tag-${group.toLowerCase()}`, value.toLowerCase());
112
+ }
113
+ }
114
+ }
125
115
  if (r.lineNumber >= 0) {
126
116
  p.attr('data-line-number', r.lineNumber);
127
117
  if (onClickItem) {
@@ -130,6 +120,46 @@ export function renderMap(
130
120
  );
131
121
  }
132
122
  }
123
+ };
124
+ for (const r of layout.regions) drawRegion(gRegions, r, 0.5);
125
+
126
+ // ── Rivers (thin water centerlines over the land, under POIs/edges) ──
127
+ if (layout.rivers.length) {
128
+ const gRivers = svg
129
+ .append('g')
130
+ .attr('class', 'dgmo-map-rivers')
131
+ .attr('fill', 'none');
132
+ for (const r of layout.rivers) {
133
+ gRivers
134
+ .append('path')
135
+ .attr('d', r.d)
136
+ .attr('stroke', r.color)
137
+ .attr('stroke-width', r.width)
138
+ .attr('stroke-linecap', 'round')
139
+ .attr('stroke-linejoin', 'round');
140
+ }
141
+ }
142
+
143
+ // ── AK / HI insets (albers-usa) — drawn in the FOREGROUND so the opaque ocean
144
+ // box hides the main-map neighbour land (Mexico's Baja) behind it; the state
145
+ // then draws on top, framed by the box border. ──
146
+ if (layout.insets.length) {
147
+ const insetG = svg.append('g').attr('class', 'dgmo-map-insets');
148
+ for (const box of layout.insets) {
149
+ // Angled-top quad frame — rides under the conus coast so it never covers
150
+ // neighbouring states. Closed path from the four corners.
151
+ const d =
152
+ box.points.map((p, i) => `${i ? 'L' : 'M'}${p[0]},${p[1]}`).join('') +
153
+ 'Z';
154
+ insetG
155
+ .append('path')
156
+ .attr('d', d)
157
+ .attr('fill', layout.background)
158
+ .attr('stroke', mix(palette.text, palette.bg, 55))
159
+ .attr('stroke-width', 1)
160
+ .attr('stroke-linejoin', 'round');
161
+ }
162
+ for (const r of layout.insetRegions) drawRegion(insetG, r, 0.5);
133
163
  }
134
164
 
135
165
  // ── Legs (edges + route legs) ──
@@ -137,14 +167,31 @@ export function renderMap(
137
167
  .append('g')
138
168
  .attr('class', 'dgmo-map-legs')
139
169
  .attr('fill', 'none');
140
- for (const leg of layout.legs) {
170
+ layout.legs.forEach((leg, i) => {
141
171
  const p = gLegs
142
172
  .append('path')
143
173
  .attr('d', leg.d)
144
174
  .attr('stroke', leg.color)
145
175
  .attr('stroke-width', leg.width)
146
176
  .attr('stroke-linecap', 'round');
147
- if (leg.arrow) p.attr('marker-end', 'url(#dgmo-map-arrow)');
177
+ if (leg.arrow) {
178
+ const id = `dgmo-map-arrow-${i}`;
179
+ const s = arrowSize(leg.width);
180
+ defs
181
+ .append('marker')
182
+ .attr('id', id)
183
+ .attr('viewBox', '0 0 10 10')
184
+ .attr('refX', 10)
185
+ .attr('refY', 5)
186
+ .attr('markerUnits', 'userSpaceOnUse')
187
+ .attr('markerWidth', s)
188
+ .attr('markerHeight', s)
189
+ .attr('orient', 'auto-start-reverse')
190
+ .append('path')
191
+ .attr('d', 'M0,0L10,5L0,10z')
192
+ .attr('fill', leg.color);
193
+ p.attr('marker-end', `url(#${id})`);
194
+ }
148
195
  if (leg.label !== undefined && leg.labelX !== undefined) {
149
196
  emitText(
150
197
  gLegs,
@@ -158,7 +205,7 @@ export function renderMap(
158
205
  LABEL_FONT - 1
159
206
  );
160
207
  }
161
- }
208
+ });
162
209
 
163
210
  // ── POIs ──
164
211
  const gPois = svg.append('g').attr('class', 'dgmo-map-pois');
@@ -181,7 +228,8 @@ export function renderMap(
181
228
  .attr('fill', poi.fill)
182
229
  .attr('stroke', poi.stroke)
183
230
  .attr('stroke-width', 1)
184
- .attr('data-line-number', poi.lineNumber);
231
+ .attr('data-line-number', poi.lineNumber)
232
+ .attr('data-poi', poi.id);
185
233
  if (onClickItem) {
186
234
  c.style('cursor', 'pointer').on('click', () =>
187
235
  onClickItem(poi.lineNumber)
@@ -202,61 +250,40 @@ export function renderMap(
202
250
  }
203
251
  }
204
252
 
205
- // ── Labels (leaders, halo text, numbered pins) ──
253
+ // ── Labels (leaders + halo text) ──
206
254
  const gLabels = svg.append('g').attr('class', 'dgmo-map-labels');
207
255
  for (const lab of layout.labels) {
208
256
  if (lab.leader) {
209
- gLabels
257
+ const line = gLabels
210
258
  .append('line')
211
259
  .attr('x1', lab.leader.x1)
212
260
  .attr('y1', lab.leader.y1)
213
261
  .attr('x2', lab.leader.x2)
214
262
  .attr('y2', lab.leader.y2)
215
- .attr('stroke', mix(palette.textMuted, palette.bg, 60))
216
- .attr('stroke-width', 0.75);
263
+ // Tie the leader to its dot by colour; neutral grey when it has none.
264
+ .attr(
265
+ 'stroke',
266
+ lab.leaderColor ?? mix(palette.textMuted, palette.bg, 60)
267
+ )
268
+ .attr('stroke-width', lab.leaderColor ? 1 : 0.75);
269
+ if (lab.poiId !== undefined) line.attr('data-poi', lab.poiId);
217
270
  }
218
- if (lab.pin !== undefined) {
219
- gLabels
220
- .append('rect')
221
- .attr('x', lab.x - 1)
222
- .attr('y', lab.y - LABEL_FONT)
223
- .attr('width', LABEL_FONT * 1.3)
224
- .attr('height', LABEL_FONT * 1.3)
225
- .attr('rx', 2)
226
- .attr('fill', palette.surface)
227
- .attr('stroke', palette.border);
228
- }
229
- emitText(
271
+ const t = emitText(
230
272
  gLabels,
231
273
  lab.x,
232
274
  lab.y,
233
275
  lab.text,
234
276
  lab.anchor,
235
277
  lab.color,
236
- haloColor,
278
+ lab.haloColor,
237
279
  lab.halo,
238
280
  LABEL_FONT
239
281
  );
240
- }
241
-
242
- // ── Pin legend list ──
243
- if (layout.pinList.length > 0) {
244
- const gPins = svg
245
- .append('g')
246
- .attr('class', 'dgmo-map-pin-list')
247
- .attr(
248
- 'transform',
249
- `translate(12, ${height - layout.pinList.length * 14 - 8})`
250
- );
251
- layout.pinList.forEach((entry, i) => {
252
- gPins
253
- .append('text')
254
- .attr('x', 0)
255
- .attr('y', i * 14)
256
- .attr('font-size', LABEL_FONT - 1)
257
- .attr('fill', palette.textMuted)
258
- .text(`${entry.pin} — ${entry.label}`);
259
- });
282
+ // POI labels are spotlightable: tag with the POI id and make the text the
283
+ // hover target (the app dims the other dots/labels on enter).
284
+ if (lab.poiId !== undefined) {
285
+ t.attr('data-poi', lab.poiId).style('cursor', 'default');
286
+ }
260
287
  }
261
288
 
262
289
  // ── Legend (categorical via renderLegendD3 + ramp/size/weight blocks; AR1) ──
@@ -269,21 +296,92 @@ export function renderMap(
269
296
  .append('g')
270
297
  .attr('class', 'dgmo-map-legend')
271
298
  .attr('transform', `translate(0, ${legendY})`);
272
- const groups = layout.legend.tagGroups.filter((g) => g.entries.length > 0);
299
+ // The score ramp is a selectable colouring group alongside the tag groups
300
+ // (the user flips between them); its capsule renders the gradient inline.
301
+ // Reserved name "Score" when no metric label is set — must match SCORE_NAME
302
+ // in layout.ts so the resolved activeGroup selects it.
303
+ const ramp = layout.legend.ramp;
304
+ const scoreGroup = ramp
305
+ ? {
306
+ name: ramp.metric?.trim() || 'Score',
307
+ entries: [],
308
+ gradient: {
309
+ min: ramp.min,
310
+ max: ramp.max,
311
+ hue: ramp.hue,
312
+ base: ramp.base,
313
+ },
314
+ }
315
+ : null;
316
+ const tagGroups = layout.legend.tagGroups
317
+ .filter((g) => g.entries.length > 0)
318
+ .map((g) => ({ name: g.name, entries: [...g.entries] }));
319
+ const groups = [...(scoreGroup ? [scoreGroup] : []), ...tagGroups];
273
320
  if (groups.length > 0) {
274
321
  const config: LegendConfig = {
275
- groups: groups.map((g) => ({ name: g.name, entries: [...g.entries] })),
322
+ groups,
276
323
  position: { placement: 'top-center', titleRelation: 'below-title' },
277
324
  mode: exportDims ? 'export' : 'preview',
278
325
  showEmptyGroups: false,
326
+ // Keep inactive siblings visible as pills so the user can click to flip
327
+ // the active colouring dimension (preview only — export shows just the
328
+ // active group).
329
+ showInactivePills: true,
279
330
  };
280
331
  const state: LegendState = { activeGroup: layout.legend.activeGroup };
281
332
  renderLegendD3(legendG, config, state, palette, isDark, undefined, width);
282
333
  }
283
- // Custom keys (ramp / size / weight), stacked above the pin list so they
284
- // never overlap it (#3).
285
- const pinGap = layout.pinList.length ? layout.pinList.length * 14 + 14 : 0;
286
- emitExtraLegend(svg, layout, palette, height, pinGap);
334
+ }
335
+
336
+ // ── Title / subtitle / caption (foreground drawn last so they sit above the
337
+ // basemap, POIs, and labels; layout reserves top padding so POIs clear them) ──
338
+ // Soft bg halo so the banner stays legible over busy land/water (the muted
339
+ // subtitle/caption otherwise wash out on mid-toned palettes like gruvbox).
340
+ if (layout.title) {
341
+ svg
342
+ .append('text')
343
+ .attr('x', width / 2)
344
+ .attr('y', TITLE_Y)
345
+ .attr('text-anchor', 'middle')
346
+ .attr('font-size', TITLE_FONT_SIZE)
347
+ .attr('font-weight', TITLE_FONT_WEIGHT)
348
+ .attr('fill', palette.text)
349
+ .attr('paint-order', 'stroke fill')
350
+ .attr('stroke', palette.bg)
351
+ .attr('stroke-width', 4)
352
+ .attr('stroke-linejoin', 'round')
353
+ .attr('stroke-opacity', 0.7)
354
+ .text(layout.title);
355
+ }
356
+ if (layout.subtitle) {
357
+ svg
358
+ .append('text')
359
+ .attr('x', width / 2)
360
+ .attr('y', TITLE_Y + TITLE_FONT_SIZE)
361
+ .attr('text-anchor', 'middle')
362
+ .attr('font-size', LABEL_FONT + 1)
363
+ .attr('fill', palette.textMuted)
364
+ .attr('paint-order', 'stroke fill')
365
+ .attr('stroke', palette.bg)
366
+ .attr('stroke-width', 3)
367
+ .attr('stroke-linejoin', 'round')
368
+ .attr('stroke-opacity', 0.7)
369
+ .text(layout.subtitle);
370
+ }
371
+ if (layout.caption) {
372
+ svg
373
+ .append('text')
374
+ .attr('x', width / 2)
375
+ .attr('y', height - 8)
376
+ .attr('text-anchor', 'middle')
377
+ .attr('font-size', LABEL_FONT)
378
+ .attr('fill', palette.textMuted)
379
+ .attr('paint-order', 'stroke fill')
380
+ .attr('stroke', palette.bg)
381
+ .attr('stroke-width', 3)
382
+ .attr('stroke-linejoin', 'round')
383
+ .attr('stroke-opacity', 0.7)
384
+ .text(layout.caption);
287
385
  }
288
386
  }
289
387
 
@@ -300,7 +398,6 @@ export function renderMapForExport(
300
398
  }
301
399
 
302
400
  type Sel = d3Selection.Selection<SVGGElement, unknown, null, undefined>;
303
- type SvgSel = d3Selection.Selection<SVGSVGElement, unknown, null, undefined>;
304
401
 
305
402
  function emitText(
306
403
  g: Sel,
@@ -312,7 +409,7 @@ function emitText(
312
409
  halo: string,
313
410
  withHalo: boolean,
314
411
  fontSize: number
315
- ): void {
412
+ ): d3Selection.Selection<SVGTextElement, unknown, null, undefined> {
316
413
  const t = g
317
414
  .append('text')
318
415
  .attr('x', x)
@@ -328,121 +425,5 @@ function emitText(
328
425
  .attr('stroke-linejoin', 'round')
329
426
  .attr('stroke-opacity', 0.7);
330
427
  }
331
- }
332
-
333
- /** Ramp gradient bar + graduated size/weight keys (not legend swatch groups). */
334
- function emitExtraLegend(
335
- svg: SvgSel,
336
- layout: MapLayout,
337
- palette: PaletteColors,
338
- height: number,
339
- bottomGap: number
340
- ): void {
341
- const { legend } = layout;
342
- if (!legend) return;
343
- // Nothing to draw if there are only categorical swatches (#4).
344
- if (!legend.ramp && !legend.size && !legend.weight) return;
345
- const blocks: Array<() => void> = [];
346
- const g = svg
347
- .append('g')
348
- .attr('class', 'dgmo-map-legend-keys')
349
- .attr('transform', `translate(12, ${height - 56 - bottomGap})`);
350
- let xCursor = 0;
351
-
352
- if (legend.ramp) {
353
- const ramp = legend.ramp;
354
- blocks.push(() => {
355
- const block = g.append('g').attr('transform', `translate(${xCursor},0)`);
356
- const gradId = 'dgmo-map-ramp';
357
- const grad = block
358
- .append('defs')
359
- .append('linearGradient')
360
- .attr('id', gradId)
361
- .attr('x1', '0%')
362
- .attr('x2', '100%');
363
- grad
364
- .append('stop')
365
- .attr('offset', '0%')
366
- .attr('stop-color', mix(ramp.hue, palette.bg, 15));
367
- grad.append('stop').attr('offset', '100%').attr('stop-color', ramp.hue);
368
- block
369
- .append('rect')
370
- .attr('width', 80)
371
- .attr('height', 8)
372
- .attr('fill', `url(#${gradId})`);
373
- block
374
- .append('text')
375
- .attr('x', 0)
376
- .attr('y', 22)
377
- .attr('font-size', 9)
378
- .attr('fill', palette.textMuted)
379
- .text(String(ramp.min));
380
- block
381
- .append('text')
382
- .attr('x', 80)
383
- .attr('y', 22)
384
- .attr('text-anchor', 'end')
385
- .attr('font-size', 9)
386
- .attr('fill', palette.textMuted)
387
- .text(String(ramp.max));
388
- if (ramp.metric) {
389
- block
390
- .append('text')
391
- .attr('x', 0)
392
- .attr('y', -4)
393
- .attr('font-size', 9)
394
- .attr('fill', palette.textMuted)
395
- .text(ramp.metric);
396
- }
397
- xCursor += 110;
398
- });
399
- }
400
- if (legend.size) {
401
- const sz = legend.size;
402
- blocks.push(() => {
403
- const block = g.append('g').attr('transform', `translate(${xCursor},0)`);
404
- [3, 6, 10].forEach((r, i) => {
405
- block
406
- .append('circle')
407
- .attr('cx', i * 26 + r)
408
- .attr('cy', 8)
409
- .attr('r', r)
410
- .attr('fill', 'none')
411
- .attr('stroke', palette.textMuted);
412
- });
413
- block
414
- .append('text')
415
- .attr('x', 0)
416
- .attr('y', -4)
417
- .attr('font-size', 9)
418
- .attr('fill', palette.textMuted)
419
- .text(sz.metric ?? 'size');
420
- xCursor += 110;
421
- });
422
- }
423
- if (legend.weight) {
424
- const wt = legend.weight;
425
- blocks.push(() => {
426
- const block = g.append('g').attr('transform', `translate(${xCursor},0)`);
427
- [1, 3, 6].forEach((w, i) => {
428
- block
429
- .append('line')
430
- .attr('x1', i * 26)
431
- .attr('y1', 8)
432
- .attr('x2', i * 26 + 20)
433
- .attr('y2', 8)
434
- .attr('stroke', palette.textMuted)
435
- .attr('stroke-width', w);
436
- });
437
- block
438
- .append('text')
439
- .attr('x', 0)
440
- .attr('y', -4)
441
- .attr('font-size', 9)
442
- .attr('fill', palette.textMuted)
443
- .text(wt.metric ?? 'weight');
444
- xCursor += 110;
445
- });
446
- }
447
- for (const draw of blocks) draw();
428
+ return t;
448
429
  }
@@ -11,10 +11,27 @@ export interface MapData {
11
11
  worldCoarse: BoundaryTopology;
12
12
  worldDetail: BoundaryTopology;
13
13
  usStates: BoundaryTopology;
14
+ /** Major lakes (Natural Earth 110m) drawn as water over land — e.g. the Great
15
+ * Lakes. Optional so hand-built test fixtures need not supply it. */
16
+ lakes?: BoundaryTopology;
17
+ /** Major river centerlines (Natural Earth 110m) drawn as thin water lines over
18
+ * land — e.g. the Amazon, Nile, Mississippi. Optional, like `lakes`. */
19
+ rivers?: BoundaryTopology;
20
+ /** North-America-clipped 10m country land, used as crisp neighbour context
21
+ * under the albers-usa US view so Canada/Mexico match the 10m states instead
22
+ * of the coarser world tiers. Optional, like `lakes`. */
23
+ naLand?: BoundaryTopology;
24
+ /** North-America-clipped 10m major lakes (Great Lakes etc.), used in place of
25
+ * the coarse `lakes` under the albers-usa US view. Optional. */
26
+ naLakes?: BoundaryTopology;
14
27
  gazetteer: Gazetteer;
15
28
  }
16
29
 
17
- export type ProjectionFamily = 'natural-earth' | 'albers-usa' | 'mercator';
30
+ export type ProjectionFamily =
31
+ | 'equirectangular'
32
+ | 'natural-earth'
33
+ | 'albers-usa'
34
+ | 'mercator';
18
35
 
19
36
  /** Which geometry layers the renderer draws. */
20
37
  export interface Basemaps {