@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.
- package/dist/advanced.cjs +948 -321
- package/dist/advanced.d.cts +148 -54
- package/dist/advanced.d.ts +148 -54
- package/dist/advanced.js +949 -321
- package/dist/auto.cjs +930 -317
- package/dist/auto.js +117 -117
- package/dist/auto.mjs +934 -318
- package/dist/cli.cjs +160 -160
- package/dist/index.cjs +929 -316
- package/dist/index.js +933 -317
- package/dist/internal.cjs +948 -321
- package/dist/internal.d.cts +148 -54
- package/dist/internal.d.ts +148 -54
- package/dist/internal.js +949 -321
- package/dist/map-data/PROVENANCE.json +1 -1
- package/dist/map-data/lakes.json +1 -0
- package/dist/map-data/na-lakes.json +1 -0
- package/dist/map-data/na-land.json +1 -0
- package/dist/map-data/rivers.json +1 -0
- package/docs/language-reference.md +12 -7
- package/gallery/fixtures/map-region-scope.dgmo +15 -0
- package/package.json +4 -4
- package/src/advanced.ts +7 -6
- package/src/c4/parser.ts +6 -6
- package/src/completion.ts +6 -2
- package/src/echarts.ts +1 -1
- package/src/infra/parser.ts +10 -10
- package/src/journey-map/parser.ts +1 -1
- package/src/label-layout.ts +36 -0
- package/src/map/data/PROVENANCE.json +1 -1
- package/src/map/data/README.md +2 -0
- package/src/map/data/lakes.json +1 -0
- package/src/map/data/na-lakes.json +1 -0
- package/src/map/data/na-land.json +1 -0
- package/src/map/data/rivers.json +1 -0
- package/src/map/layout.ts +1022 -205
- package/src/map/load-data.ts +73 -17
- package/src/map/parser.ts +22 -13
- package/src/map/renderer.ts +200 -219
- package/src/map/resolved-types.ts +18 -1
- package/src/map/resolver.ts +79 -7
- package/src/map/types.ts +4 -0
- package/src/mindmap/parser.ts +1 -1
- package/src/sitemap/parser.ts +1 -1
- package/src/utils/legend-d3.ts +42 -0
- package/src/utils/legend-layout.ts +83 -3
- package/src/utils/legend-svg.ts +1 -8
- package/src/utils/legend-types.ts +44 -1
package/src/map/renderer.ts
CHANGED
|
@@ -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
|
|
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
|
|
66
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
84
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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',
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
-
|
|
216
|
-
.attr(
|
|
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
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
):
|
|
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 =
|
|
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 {
|