@diagrammo/dgmo 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/advanced.cjs +238 -48
- package/dist/advanced.d.cts +17 -0
- package/dist/advanced.d.ts +17 -0
- package/dist/advanced.js +238 -48
- package/dist/auto.cjs +236 -42
- package/dist/auto.js +115 -115
- package/dist/auto.mjs +236 -42
- package/dist/cli.cjs +153 -153
- package/dist/editor.cjs +3 -0
- package/dist/editor.js +3 -0
- package/dist/highlight.cjs +3 -0
- package/dist/highlight.js +3 -0
- package/dist/index.cjs +232 -41
- package/dist/index.js +232 -41
- package/dist/internal.cjs +238 -48
- package/dist/internal.d.cts +17 -0
- package/dist/internal.d.ts +17 -0
- package/dist/internal.js +238 -48
- package/dist/map-data/PROVENANCE.json +1 -1
- package/dist/map-data/gazetteer.json +1 -1
- package/dist/map-data/mountain-ranges.json +1 -1
- package/dist/map-data/water-bodies.json +1 -1
- package/dist/map-data/world-coarse.json +1 -1
- package/dist/map-data/world-detail.json +1 -1
- package/docs/language-reference.md +35 -0
- package/gallery/fixtures/boxes-and-lines.dgmo +6 -4
- package/package.json +1 -1
- package/src/boxes-and-lines/parser.ts +39 -0
- package/src/boxes-and-lines/renderer.ts +171 -13
- package/src/boxes-and-lines/types.ts +9 -0
- package/src/completion.ts +4 -5
- package/src/d3.ts +12 -4
- package/src/editor/keywords.ts +3 -0
- package/src/map/data/PROVENANCE.json +1 -1
- package/src/map/data/README.md +6 -0
- package/src/map/data/gazetteer.json +1 -1
- package/src/map/data/mountain-ranges.json +1 -1
- package/src/map/data/water-bodies.json +1 -1
- package/src/map/data/world-coarse.json +1 -1
- package/src/map/data/world-detail.json +1 -1
- package/src/map/layout.ts +111 -18
- package/src/map/renderer.ts +95 -4
- package/src/utils/reserved-key-registry.ts +5 -3
package/src/map/layout.ts
CHANGED
|
@@ -69,6 +69,16 @@ const R_MAX = 22;
|
|
|
69
69
|
const W_MIN = 1.25; // edge stroke width
|
|
70
70
|
const W_MAX = 8;
|
|
71
71
|
const FONT = 11; // on-map label font px
|
|
72
|
+
|
|
73
|
+
// A few countries have far-flung territory that drags the area-weighted centroid
|
|
74
|
+
// off the mainland (US → Alaska pulls it up into Canada). Anchor their world-layer
|
|
75
|
+
// label/hover point to a mainland [lon, lat] instead. Antimeridian crossers whose
|
|
76
|
+
// body dominates by area (Russia) are NOT listed — their area-weighted centroid
|
|
77
|
+
// already lands on the mainland; only the naive bounding-box centre (which the app
|
|
78
|
+
// previously used for hover) mistook the wrapped sliver for half the shape.
|
|
79
|
+
const WORLD_LABEL_ANCHORS: Record<string, [number, number]> = {
|
|
80
|
+
US: [-98.5, 39.5], // CONUS geographic centre (near Lebanon, Kansas)
|
|
81
|
+
};
|
|
72
82
|
// POI-cluster hover-only gate (Decision #1). A ≥2-member cluster's callout
|
|
73
83
|
// column falls back to hover-only labels when it would sprawl or overflow:
|
|
74
84
|
// - MAX_CLUSTER_EXTENT_FACTOR × min(width,height) = the px diagonal beyond which
|
|
@@ -112,16 +122,21 @@ const RELIEF_MIN_DIM = 2; // px
|
|
|
112
122
|
// Relief = horizontal hachure lines clipped to each range: a subtle
|
|
113
123
|
// dark-on-light / light-on-dark texture that reads as "mountains here". Spacing
|
|
114
124
|
// is SCREEN-space so density is constant regardless of zoom (geo-space spacing
|
|
115
|
-
// would collapse a small range to 1–2 lines and read as a glitch).
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
125
|
+
// would collapse a small range to 1–2 lines and read as a glitch). Drawn with a
|
|
126
|
+
// non-scaling stroke (constant device width at any zoom/DPR) and a low-contrast
|
|
127
|
+
// colour so it reads as faint, fine terrain hachure — dense thin lines that are
|
|
128
|
+
// almost indistinguishable as individual strokes (a whisper of texture, not
|
|
129
|
+
// stripes). NOT crispEdges — that snaps the stroke to a solid ~1px in WebKit and
|
|
130
|
+
// reads too heavy; plain AA keeps the lines soft. The width is kept just ABOVE
|
|
131
|
+
// sub-pixel: at ~0.15px the AA fuzz spreads each line to ~1px and tight spacing
|
|
132
|
+
// merges them into a flat grey wash (a "blob"). 0.25px every 1.5px stays a fine,
|
|
133
|
+
// faint hatch on both zoomed-out world maps and zoomed-in regional views.
|
|
134
|
+
const RELIEF_HATCH_SPACING = 1.5; // px between lines
|
|
135
|
+
const RELIEF_HATCH_WIDTH = 0.2; // px stroke
|
|
121
136
|
// % of the DARK reference (palette.bg on dark themes, palette.text on light)
|
|
122
137
|
// blended into the land colour — so the lines read DARKER than the land in both
|
|
123
138
|
// themes (palette.text alone flips to light on dark themes).
|
|
124
|
-
const RELIEF_HATCH_STRENGTH =
|
|
139
|
+
const RELIEF_HATCH_STRENGTH = 26;
|
|
125
140
|
// Coastline water-lines (opt-in `coastline`, §24B.2). N equal-width coast-parallel
|
|
126
141
|
// rings on the water side, evenly spaced and FADING seaward — the antique
|
|
127
142
|
// nautical-chart depth-contour look. Offshore distances + thickness are
|
|
@@ -191,6 +206,14 @@ export interface MapLayoutRegion {
|
|
|
191
206
|
/** The region's tag values keyed by group (lowercased) — emitted as
|
|
192
207
|
* `data-tag-<group>` so the app can highlight on legend-entry hover. */
|
|
193
208
|
readonly tags?: Readonly<Record<string, string>>;
|
|
209
|
+
/** Area-weighted screen centroid (px) of the DRAWN geometry — emitted as
|
|
210
|
+
* `data-label-x`/`data-label-y` so the app can anchor the hover label here
|
|
211
|
+
* instead of the path's bounding-box centre. The bbox centre breaks for
|
|
212
|
+
* antimeridian crossers (Russia's wrapped Chukotka sliver pins the box's left
|
|
213
|
+
* edge to the far side of the map, dropping the centre into the Atlantic); the
|
|
214
|
+
* area-weighted centroid stays on the body. Honours WORLD_LABEL_ANCHORS. */
|
|
215
|
+
readonly labelX?: number;
|
|
216
|
+
readonly labelY?: number;
|
|
194
217
|
}
|
|
195
218
|
|
|
196
219
|
/** A framed inset "cutout" (albers-usa AK/HI), in screen px. The frame is a
|
|
@@ -836,6 +859,60 @@ export function parsePathRings(d: string): Array<Array<[number, number]>> {
|
|
|
836
859
|
return rings;
|
|
837
860
|
}
|
|
838
861
|
|
|
862
|
+
/** Drop antimeridian wrap-slivers from a GLOBAL-view region path. A landmass that
|
|
863
|
+
* crosses ±180° (Russia's Chukotka, the western Aleutians, Fiji…) is clipped into
|
|
864
|
+
* fragments; the far one is a small sliver pinned to the OPPOSITE vertical frame
|
|
865
|
+
* edge — it reads as a stray island floating beside its true continent (e.g. the
|
|
866
|
+
* "island left of Alaska"). We drop any ring that (a) has an edge collinear with
|
|
867
|
+
* the LEFT or RIGHT canvas edge AND (b) is small AND (c) isn't the region's
|
|
868
|
+
* largest ring. The mainland (large, on its own edge) and interior islands (not
|
|
869
|
+
* frame-cut) are kept. Vertical edges only — a ring cut by the top/bottom lat
|
|
870
|
+
* crop is real content, not a wrap. Global-only: regional clipExtent cuts ARE
|
|
871
|
+
* real land at the viewport edge and must survive. */
|
|
872
|
+
function dropAntimeridianWrapSlivers(
|
|
873
|
+
d: string,
|
|
874
|
+
width: number,
|
|
875
|
+
height: number
|
|
876
|
+
): string {
|
|
877
|
+
const rings = parsePathRings(d);
|
|
878
|
+
if (rings.length <= 1) return d;
|
|
879
|
+
const eps = 0.75;
|
|
880
|
+
const minArea = 0.003 * width * height; // 0.3% of canvas
|
|
881
|
+
const ringArea = (r: ReadonlyArray<[number, number]>): number => {
|
|
882
|
+
let s = 0;
|
|
883
|
+
for (let i = 0; i < r.length; i++) {
|
|
884
|
+
const a = r[i]!;
|
|
885
|
+
const b = r[(i + 1) % r.length]!;
|
|
886
|
+
s += a[0] * b[1] - b[0] * a[1];
|
|
887
|
+
}
|
|
888
|
+
return Math.abs(s) / 2;
|
|
889
|
+
};
|
|
890
|
+
const areas = rings.map(ringArea);
|
|
891
|
+
const maxArea = Math.max(...areas);
|
|
892
|
+
const onVEdge = (
|
|
893
|
+
a: readonly [number, number],
|
|
894
|
+
b: readonly [number, number]
|
|
895
|
+
): boolean =>
|
|
896
|
+
(Math.abs(a[0]) <= eps && Math.abs(b[0]) <= eps) ||
|
|
897
|
+
(Math.abs(a[0] - width) <= eps && Math.abs(b[0] - width) <= eps);
|
|
898
|
+
let dropped = false;
|
|
899
|
+
const kept = rings.filter((r, idx) => {
|
|
900
|
+
if (areas[idx]! >= maxArea || areas[idx]! >= minArea) return true;
|
|
901
|
+
const touches = r.some((p, i) => onVEdge(p, r[(i + 1) % r.length]!));
|
|
902
|
+
if (touches) {
|
|
903
|
+
dropped = true;
|
|
904
|
+
return false;
|
|
905
|
+
}
|
|
906
|
+
return true;
|
|
907
|
+
});
|
|
908
|
+
if (!dropped) return d;
|
|
909
|
+
return kept
|
|
910
|
+
.map(
|
|
911
|
+
(r) => r.map((p, i) => (i ? 'L' : 'M') + p[0] + ',' + p[1]).join('') + 'Z'
|
|
912
|
+
)
|
|
913
|
+
.join('');
|
|
914
|
+
}
|
|
915
|
+
|
|
839
916
|
export function layoutMap(
|
|
840
917
|
resolved: ResolvedMap,
|
|
841
918
|
data: MapData,
|
|
@@ -1140,10 +1217,17 @@ export function layoutMap(
|
|
|
1140
1217
|
const by0 = cb[0][1];
|
|
1141
1218
|
const cw = cb[1][0] - bx0;
|
|
1142
1219
|
const ch = cb[1][1] - by0;
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1220
|
+
// A global stretch-fill runs the world to EVERY edge of the canvas — no
|
|
1221
|
+
// FIT_PAD inset. The equirectangular rectangle is the map, so its edges ARE
|
|
1222
|
+
// the render-area edges (the antimeridian sits exactly on the left/right
|
|
1223
|
+
// edge, not 24px short of it with a coastline ringing the gap). The title
|
|
1224
|
+
// overlays the top; we reserve a top band only when POIs are present (so
|
|
1225
|
+
// their markers don't project up under the foreground title banner).
|
|
1226
|
+
const topReserve = resolved.title && resolved.pois.length > 0 ? topPad : 0;
|
|
1227
|
+
const ox = 0;
|
|
1228
|
+
const oy = topReserve;
|
|
1229
|
+
const sx = cw > 0 ? width / cw : 1;
|
|
1230
|
+
const sy = ch > 0 ? (height - topReserve) / ch : 1;
|
|
1147
1231
|
stretchParams = { sx, sy, ox, oy, bx0, by0 };
|
|
1148
1232
|
const stretch = (x: number, y: number): [number, number] => [
|
|
1149
1233
|
ox + (x - bx0) * sx,
|
|
@@ -1588,7 +1672,12 @@ export function layoutMap(
|
|
|
1588
1672
|
// but still drop antimeridian frame-fillers (Fiji et al.).
|
|
1589
1673
|
const viewF = shouldCull ? cullFeatureToView(f) : dropFrameFillers(f);
|
|
1590
1674
|
if (!viewF) continue;
|
|
1591
|
-
const
|
|
1675
|
+
const raw = path(viewF as never) ?? '';
|
|
1676
|
+
// Global views: strip the wrap-sliver a crossing landmass leaves pinned to
|
|
1677
|
+
// the far edge (Russia's Chukotka beside Alaska). Regional cuts are real.
|
|
1678
|
+
const d = fitIsGlobal
|
|
1679
|
+
? dropAntimeridianWrapSlivers(raw, width, height)
|
|
1680
|
+
: raw;
|
|
1592
1681
|
if (!d) continue;
|
|
1593
1682
|
const isThisLayer = r?.layer === layerKind;
|
|
1594
1683
|
// Non-US neighbour land in a US view is gray context, not yellow land.
|
|
@@ -1614,6 +1703,15 @@ export function layoutMap(
|
|
|
1614
1703
|
// (the same source the resolver/inset/context-label layers read).
|
|
1615
1704
|
label = (f.properties as { name?: string } | null)?.name;
|
|
1616
1705
|
}
|
|
1706
|
+
// Label/hover anchor: a hardcoded mainland anchor when far-flung territory
|
|
1707
|
+
// would skew it, else the area-weighted screen centroid of the drawn shape.
|
|
1708
|
+
// The latter (unlike a bounding-box centre) survives antimeridian crossers.
|
|
1709
|
+
const labelAnchor = WORLD_LABEL_ANCHORS[iso];
|
|
1710
|
+
const c = labelAnchor
|
|
1711
|
+
? project(labelAnchor[0], labelAnchor[1])
|
|
1712
|
+
: path.centroid(viewF as never);
|
|
1713
|
+
const hasCentroid =
|
|
1714
|
+
c != null && Number.isFinite(c[0]) && Number.isFinite(c[1]);
|
|
1617
1715
|
regions.push({
|
|
1618
1716
|
id: iso,
|
|
1619
1717
|
d,
|
|
@@ -1622,6 +1720,7 @@ export function layoutMap(
|
|
|
1622
1720
|
lineNumber,
|
|
1623
1721
|
layer,
|
|
1624
1722
|
...(label !== undefined && { label }),
|
|
1723
|
+
...(hasCentroid && { labelX: c[0], labelY: c[1] }),
|
|
1625
1724
|
...(isThisLayer && r.value !== undefined && { value: r.value }),
|
|
1626
1725
|
...(isThisLayer && Object.keys(r.tags).length > 0 && { tags: r.tags }),
|
|
1627
1726
|
});
|
|
@@ -2284,12 +2383,6 @@ export function layoutMap(
|
|
|
2284
2383
|
lineNumber,
|
|
2285
2384
|
});
|
|
2286
2385
|
};
|
|
2287
|
-
// A few countries have far-flung territory that drags the area-weighted
|
|
2288
|
-
// centroid off the mainland (US → Alaska pulls it up into Canada). Anchor
|
|
2289
|
-
// their world-layer label to a mainland [lon, lat] instead.
|
|
2290
|
-
const WORLD_LABEL_ANCHORS: Record<string, [number, number]> = {
|
|
2291
|
-
US: [-98.5, 39.5], // CONUS geographic centre (near Lebanon, Kansas)
|
|
2292
|
-
};
|
|
2293
2386
|
// A region label's screen footprint, middle-anchored on its centroid, used to
|
|
2294
2387
|
// keep two region labels from overlapping (a small gap adds breathing room).
|
|
2295
2388
|
const REGION_LABEL_GAP = 2;
|
package/src/map/renderer.ts
CHANGED
|
@@ -57,6 +57,70 @@ function ringToPath(ring: ReadonlyArray<[number, number]>): string {
|
|
|
57
57
|
return d + 'Z';
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/** Open SVG polyline (`M…L…`, no `Z`) from a run of points. */
|
|
61
|
+
function polylineToPath(pts: ReadonlyArray<[number, number]>): string {
|
|
62
|
+
let d = '';
|
|
63
|
+
for (let i = 0; i < pts.length; i++)
|
|
64
|
+
d += (i ? 'L' : 'M') + pts[i]![0] + ',' + pts[i]![1];
|
|
65
|
+
return d;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Coast subpaths for one ring, dropping any edge that runs ALONG a canvas edge.
|
|
69
|
+
* A region clipped to the viewport (the antimeridian on a world map, or a
|
|
70
|
+
* regional clipExtent cut) gains a synthetic straight edge collinear with the
|
|
71
|
+
* frame — that edge is NOT a real coast and must not be buffered into a coast
|
|
72
|
+
* band (which would ring the cut with water-lines short of the edge). Without a
|
|
73
|
+
* `frame` the ring is returned closed (`M…Z`) as before. With one, the ring is
|
|
74
|
+
* split at every frame-collinear edge into open coast arcs (`M…L…`), so the land
|
|
75
|
+
* runs cleanly to the edge and only true coastline gets a water-line. */
|
|
76
|
+
function ringToCoastPaths(
|
|
77
|
+
ring: ReadonlyArray<[number, number]>,
|
|
78
|
+
frame?: { w: number; h: number }
|
|
79
|
+
): string[] {
|
|
80
|
+
if (!frame) return [ringToPath(ring)];
|
|
81
|
+
const n = ring.length;
|
|
82
|
+
const eps = 0.75;
|
|
83
|
+
const onL = (x: number): boolean => Math.abs(x) <= eps;
|
|
84
|
+
const onR = (x: number): boolean => Math.abs(x - frame.w) <= eps;
|
|
85
|
+
const onT = (y: number): boolean => Math.abs(y) <= eps;
|
|
86
|
+
const onB = (y: number): boolean => Math.abs(y - frame.h) <= eps;
|
|
87
|
+
const isFrameEdge = (
|
|
88
|
+
a: readonly [number, number],
|
|
89
|
+
b: readonly [number, number]
|
|
90
|
+
): boolean =>
|
|
91
|
+
(onL(a[0]) && onL(b[0])) ||
|
|
92
|
+
(onR(a[0]) && onR(b[0])) ||
|
|
93
|
+
(onT(a[1]) && onT(b[1])) ||
|
|
94
|
+
(onB(a[1]) && onB(b[1]));
|
|
95
|
+
// No frame-collinear edge anywhere → ordinary interior coastline (closed).
|
|
96
|
+
let firstBreak = -1;
|
|
97
|
+
for (let i = 0; i < n; i++)
|
|
98
|
+
if (isFrameEdge(ring[i]!, ring[(i + 1) % n]!)) {
|
|
99
|
+
firstBreak = i;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
if (firstBreak === -1) return [ringToPath(ring)];
|
|
103
|
+
// Walk the loop from just after the first cut, accumulating runs of real-coast
|
|
104
|
+
// edges into open polylines and breaking at each frame-collinear edge.
|
|
105
|
+
const paths: string[] = [];
|
|
106
|
+
let cur: Array<[number, number]> = [];
|
|
107
|
+
const start = (firstBreak + 1) % n;
|
|
108
|
+
for (let k = 0; k < n; k++) {
|
|
109
|
+
const i = (start + k) % n;
|
|
110
|
+
const a = ring[i]!;
|
|
111
|
+
const b = ring[(i + 1) % n]!;
|
|
112
|
+
if (isFrameEdge(a, b)) {
|
|
113
|
+
if (cur.length >= 2) paths.push(polylineToPath(cur));
|
|
114
|
+
cur = [];
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (cur.length === 0) cur.push(a);
|
|
118
|
+
cur.push(b);
|
|
119
|
+
}
|
|
120
|
+
if (cur.length >= 2) paths.push(polylineToPath(cur));
|
|
121
|
+
return paths;
|
|
122
|
+
}
|
|
123
|
+
|
|
60
124
|
/** Coast outlines to buffer: every region's OUTER rings whose bbox extent clears
|
|
61
125
|
* `minExtent`. Holes/enclaves are skipped via containment depth (even depth =
|
|
62
126
|
* outer landmass boundary, odd = a hole) so an enclave (Lesotho) or a lake-hole
|
|
@@ -64,7 +128,8 @@ function ringToPath(ring: ReadonlyArray<[number, number]>): string {
|
|
|
64
128
|
* degenerate-ring floor now — every island, however small, grows coast rings. */
|
|
65
129
|
function coastlineOuterRings(
|
|
66
130
|
regions: readonly MapLayoutRegion[],
|
|
67
|
-
minExtent: number
|
|
131
|
+
minExtent: number,
|
|
132
|
+
frame?: { w: number; h: number }
|
|
68
133
|
): string[] {
|
|
69
134
|
const paths: string[] = [];
|
|
70
135
|
for (const r of regions) {
|
|
@@ -88,7 +153,7 @@ function coastlineOuterRings(
|
|
|
88
153
|
for (let j = 0; j < rings.length; j++)
|
|
89
154
|
if (j !== i && pointInRing(fx, fy, rings[j]!)) depth++;
|
|
90
155
|
if (depth % 2 === 1) continue; // hole/enclave — skip
|
|
91
|
-
paths.push(
|
|
156
|
+
paths.push(...ringToCoastPaths(ring, frame));
|
|
92
157
|
}
|
|
93
158
|
}
|
|
94
159
|
return paths;
|
|
@@ -229,6 +294,16 @@ export function renderMap(
|
|
|
229
294
|
// Display name on EVERY region (authored + base/context) so the app can
|
|
230
295
|
// surface it on hover — decorative metadata, no visible text drawn here.
|
|
231
296
|
if (r.label) p.attr('data-region-name', r.label);
|
|
297
|
+
// ISO id on EVERY political region (authored + base/context + inset states),
|
|
298
|
+
// so the Inspect tool can resolve a reverse-geocoded `{iso}` to its drawn path
|
|
299
|
+
// and outline it. Distinct from `data-region` (data-layer-only, legend hover):
|
|
300
|
+
// this lands on base/context land too. Lakes carry no iso (not a place).
|
|
301
|
+
if (r.id && r.id !== 'lake') p.attr('data-iso', r.id);
|
|
302
|
+
// Area-weighted centroid (px) the app anchors the hover label to — robust to
|
|
303
|
+
// antimeridian crossers where a bounding-box centre lands in open ocean.
|
|
304
|
+
if (r.labelX !== undefined && r.labelY !== undefined) {
|
|
305
|
+
p.attr('data-label-x', r.labelX).attr('data-label-y', r.labelY);
|
|
306
|
+
}
|
|
232
307
|
// Data layer? Tag it so the app can highlight on legend hover / gradient
|
|
233
308
|
// scrub. `data-value` for ramp-proximity, `data-tag-<group>` per tag value
|
|
234
309
|
// (both lowercased to match the lowercased legend-entry attributes).
|
|
@@ -276,6 +351,10 @@ export function renderMap(
|
|
|
276
351
|
const gRelief = svg
|
|
277
352
|
.append('g')
|
|
278
353
|
.attr('clip-path', `url(#${landClipId})`) // outer: land only
|
|
354
|
+
// Decorative texture — never a pointer target, so region hover (the app's
|
|
355
|
+
// name-on-hover) always reaches the region path beneath. WebKit hit-tests
|
|
356
|
+
// masked/clipped overlays unlike Chromium, so this must be explicit.
|
|
357
|
+
.style('pointer-events', 'none')
|
|
279
358
|
.append('g')
|
|
280
359
|
.attr('class', 'dgmo-map-relief')
|
|
281
360
|
.attr('clip-path', `url(#${rangeClipId})`) // inner: ∩ ranges
|
|
@@ -370,10 +449,20 @@ export function renderMap(
|
|
|
370
449
|
.append('g')
|
|
371
450
|
.attr('class', 'dgmo-map-water-lines')
|
|
372
451
|
.attr('fill', 'none')
|
|
452
|
+
// Decorative nautical lines — never a pointer target. Without this the wide
|
|
453
|
+
// pre-mask coastal ring bands swallow region hover over coastal countries in
|
|
454
|
+
// WebKit (which hit-tests masked content unlike Chromium); e.g. Portugal.
|
|
455
|
+
.style('pointer-events', 'none')
|
|
373
456
|
.attr('mask', `url(#${maskId})`);
|
|
374
457
|
appendWaterLines(
|
|
375
458
|
gWater,
|
|
376
|
-
|
|
459
|
+
// Pass the canvas frame so edges collinear with it (the antimeridian on a
|
|
460
|
+
// world map, regional clipExtent cuts) don't get ringed as fake coast —
|
|
461
|
+
// land runs cleanly to the render-area edge.
|
|
462
|
+
coastlineOuterRings(layout.regions, cs.minExtent, {
|
|
463
|
+
w: width,
|
|
464
|
+
h: height,
|
|
465
|
+
}),
|
|
377
466
|
cs,
|
|
378
467
|
layout.background
|
|
379
468
|
);
|
|
@@ -407,7 +496,8 @@ export function renderMap(
|
|
|
407
496
|
const gRivers = svg
|
|
408
497
|
.append('g')
|
|
409
498
|
.attr('class', 'dgmo-map-rivers')
|
|
410
|
-
.attr('fill', 'none')
|
|
499
|
+
.attr('fill', 'none')
|
|
500
|
+
.style('pointer-events', 'none'); // decorative — pass hover to regions below
|
|
411
501
|
for (const r of layout.rivers) {
|
|
412
502
|
gRivers
|
|
413
503
|
.append('path')
|
|
@@ -509,6 +599,7 @@ export function renderMap(
|
|
|
509
599
|
.append('g')
|
|
510
600
|
.attr('class', 'dgmo-map-inset-water-lines')
|
|
511
601
|
.attr('fill', 'none')
|
|
602
|
+
.style('pointer-events', 'none') // decorative — pass hover to inset regions
|
|
512
603
|
.attr('mask', `url(#${maskId})`);
|
|
513
604
|
appendWaterLines(
|
|
514
605
|
gInsetWater,
|
|
@@ -148,12 +148,14 @@ export const PERT_REGISTRY: ReservedKeyRegistry = staticRegistry([
|
|
|
148
148
|
'collapsed',
|
|
149
149
|
]);
|
|
150
150
|
|
|
151
|
+
// `width`/`split`/`fanout` were copy-pasted from an infra-flavored template
|
|
152
|
+
// during the §1.4 metadata migration but boxes-and-lines never read them
|
|
153
|
+
// (split/fanout are infra-only edge-flow keys, consumed in src/infra/*). Removed
|
|
154
|
+
// 2026-06-03 — only `value` (the numeric ramp) is a real BL data channel.
|
|
151
155
|
export const BOXES_AND_LINES_REGISTRY: ReservedKeyRegistry = staticRegistry([
|
|
152
156
|
'color',
|
|
153
157
|
'description',
|
|
154
|
-
'
|
|
155
|
-
'split',
|
|
156
|
-
'fanout',
|
|
158
|
+
'value',
|
|
157
159
|
]);
|
|
158
160
|
|
|
159
161
|
export const TIMELINE_REGISTRY: ReservedKeyRegistry = staticRegistry([
|