@diagrammo/dgmo 0.25.5 → 0.27.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/README.md +3 -3
- package/dist/advanced.cjs +4255 -2756
- package/dist/advanced.d.cts +285 -59
- package/dist/advanced.d.ts +285 -59
- package/dist/advanced.js +4253 -2750
- package/dist/auto.cjs +4051 -2589
- package/dist/auto.js +124 -122
- package/dist/auto.mjs +4051 -2589
- package/dist/cli.cjs +172 -170
- package/dist/editor.cjs +4 -0
- package/dist/editor.js +4 -0
- package/dist/highlight.cjs +4 -0
- package/dist/highlight.js +4 -0
- package/dist/index.cjs +4076 -2591
- package/dist/index.d.cts +33 -8
- package/dist/index.d.ts +33 -8
- package/dist/index.js +4076 -2591
- package/dist/internal.cjs +4255 -2756
- package/dist/internal.d.cts +285 -59
- package/dist/internal.d.ts +285 -59
- package/dist/internal.js +4253 -2750
- package/dist/map-data/PROVENANCE.json +1 -1
- package/dist/map-data/airport-collisions.json +1 -0
- package/dist/map-data/airports.json +1 -0
- package/docs/language-reference.md +68 -18
- package/gallery/fixtures/boxes-and-lines-diverging.dgmo +15 -0
- package/gallery/fixtures/map-choropleth-diverging.dgmo +9 -0
- package/gallery/fixtures/map-region-values.dgmo +13 -0
- package/gallery/fixtures/map-subnational-zoom.dgmo +12 -0
- package/gallery/fixtures/map-tagged-legs.dgmo +16 -0
- package/gallery/fixtures/map-undirected-edges.dgmo +12 -0
- package/package.json +1 -1
- package/src/advanced.ts +3 -6
- package/src/auto/index.ts +1 -1
- package/src/boxes-and-lines/layout.ts +146 -26
- package/src/boxes-and-lines/parser.ts +43 -8
- package/src/boxes-and-lines/renderer.ts +223 -96
- package/src/boxes-and-lines/types.ts +9 -2
- package/src/c4/layout.ts +14 -32
- package/src/c4/parser.ts +9 -5
- package/src/c4/renderer.ts +34 -39
- package/src/class/layout.ts +118 -18
- package/src/class/parser.ts +35 -1
- package/src/class/renderer.ts +58 -2
- package/src/class/types.ts +3 -0
- package/src/cli.ts +4 -4
- package/src/completion-types.ts +0 -1
- package/src/completion.ts +106 -51
- package/src/cycle/layout.ts +55 -72
- package/src/cycle/renderer.ts +11 -6
- package/src/d3.ts +78 -117
- package/src/diagnostics.ts +16 -0
- package/src/echarts.ts +46 -33
- package/src/editor/keywords.ts +4 -0
- package/src/er/layout.ts +114 -22
- package/src/er/parser.ts +28 -1
- package/src/er/renderer.ts +55 -2
- package/src/er/types.ts +3 -0
- package/src/gantt/renderer.ts +46 -38
- package/src/gantt/resolver.ts +9 -2
- package/src/graph/edge-spline.ts +29 -0
- package/src/graph/flowchart-parser.ts +35 -2
- package/src/graph/flowchart-renderer.ts +80 -52
- package/src/graph/layout.ts +206 -23
- package/src/graph/notes.ts +21 -0
- package/src/graph/state-parser.ts +26 -1
- package/src/graph/state-renderer.ts +80 -52
- package/src/graph/types.ts +13 -0
- package/src/index.ts +1 -1
- package/src/infra/layout.ts +46 -26
- package/src/infra/parser.ts +1 -1
- package/src/infra/renderer.ts +16 -7
- package/src/journey-map/layout.ts +38 -49
- package/src/journey-map/renderer.ts +22 -45
- package/src/kanban/renderer.ts +15 -6
- package/src/label-layout.ts +3 -3
- package/src/map/completion.ts +77 -22
- package/src/map/context-labels.ts +57 -12
- package/src/map/data/PROVENANCE.json +1 -1
- package/src/map/data/airport-collisions.json +1 -0
- package/src/map/data/airports.json +1 -0
- package/src/map/data/types.ts +19 -0
- package/src/map/layout.ts +1196 -90
- package/src/map/legend-band.ts +2 -2
- package/src/map/load-data.ts +10 -1
- package/src/map/parser.ts +61 -32
- package/src/map/renderer.ts +284 -12
- package/src/map/resolved-types.ts +15 -1
- package/src/map/resolver.ts +132 -12
- package/src/map/types.ts +28 -8
- package/src/migrate/embedded.ts +9 -7
- package/src/mindmap/text-wrap.ts +13 -14
- package/src/org/layout.ts +19 -17
- package/src/org/renderer.ts +11 -4
- package/src/palettes/color-utils.ts +82 -21
- package/src/palettes/index.ts +0 -19
- package/src/palettes/registry.ts +1 -1
- package/src/palettes/types.ts +2 -2
- package/src/pert/layout.ts +48 -40
- package/src/pert/parser.ts +0 -14
- package/src/pert/renderer.ts +30 -43
- package/src/pyramid/renderer.ts +4 -5
- package/src/raci/renderer.ts +42 -70
- package/src/render.ts +1 -1
- package/src/ring/renderer.ts +1 -2
- package/src/sequence/parser.ts +100 -22
- package/src/sequence/renderer.ts +75 -50
- package/src/sitemap/layout.ts +27 -19
- package/src/sitemap/renderer.ts +12 -5
- package/src/tech-radar/renderer.ts +11 -35
- package/src/utils/arrow-markers.ts +51 -0
- package/src/utils/fit-canvas.ts +64 -0
- package/src/utils/legend-constants.ts +8 -54
- package/src/utils/legend-d3.ts +10 -7
- package/src/utils/legend-layout.ts +7 -4
- package/src/utils/legend-types.ts +10 -4
- package/src/utils/note-box/constants.ts +25 -0
- package/src/utils/note-box/index.ts +11 -0
- package/src/utils/note-box/metrics.ts +90 -0
- package/src/utils/note-box/svg.ts +331 -0
- package/src/utils/notes/bounds.ts +30 -0
- package/src/utils/notes/build.ts +131 -0
- package/src/utils/notes/index.ts +18 -0
- package/src/utils/notes/model.ts +19 -0
- package/src/utils/notes/parse.ts +131 -0
- package/src/utils/notes/place.ts +177 -0
- package/src/utils/notes/resolve.ts +88 -0
- package/src/utils/number-format.ts +36 -0
- package/src/utils/parsing.ts +41 -0
- package/src/utils/reserved-key-registry.ts +4 -0
- package/src/utils/text-measure.ts +122 -0
- package/src/wireframe/layout.ts +4 -2
- package/src/wireframe/renderer.ts +8 -6
- package/src/palettes/dracula.ts +0 -68
- package/src/palettes/gruvbox.ts +0 -85
- package/src/palettes/monokai.ts +0 -68
- package/src/palettes/one-dark.ts +0 -70
- package/src/palettes/rose-pine.ts +0 -84
- package/src/palettes/solarized.ts +0 -77
package/src/map/renderer.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
TITLE_Y,
|
|
13
13
|
} from '../utils/title-constants';
|
|
14
14
|
import { mix } from '../palettes/color-utils';
|
|
15
|
+
import { measureText } from '../utils/text-measure';
|
|
15
16
|
import { renderLegendD3 } from '../utils/legend-d3';
|
|
16
17
|
import type { LegendConfig, LegendState } from '../utils/legend-types';
|
|
17
18
|
import { mapLegendConfig, mapLegendGroups } from './legend-band';
|
|
@@ -185,22 +186,32 @@ function coastlineOuterRings(
|
|
|
185
186
|
* the accepted cross-region overdraw artifact behave exactly as before. */
|
|
186
187
|
function appendWaterLines(
|
|
187
188
|
g: Sel,
|
|
189
|
+
defs: Sel,
|
|
190
|
+
coastId: string,
|
|
188
191
|
outerRings: readonly string[],
|
|
189
192
|
style: MapLayoutCoastlineStyle,
|
|
190
193
|
flatWater: string
|
|
191
194
|
): void {
|
|
192
195
|
const d = outerRings.join(' ');
|
|
196
|
+
if (!d) return;
|
|
197
|
+
// The coast geometry is byte-for-byte identical across every line-level and
|
|
198
|
+
// both passes (only stroke colour/width differ). On a world map that compound
|
|
199
|
+
// path is ~200KB, so emitting it ~12× inline ballooned the SVG to multi-MB and
|
|
200
|
+
// overflowed the SSG HTML reparse. Define it once and `<use>` it: same pixels,
|
|
201
|
+
// a fraction of the bytes.
|
|
202
|
+
defs.append('path').attr('id', coastId).attr('d', d).attr('fill', 'none');
|
|
203
|
+
const ref = `#${coastId}`;
|
|
193
204
|
const linesOuterFirst = [...style.lines].sort((a, b) => b.d - a.d);
|
|
194
205
|
for (const line of linesOuterFirst) {
|
|
195
|
-
g.append('
|
|
196
|
-
.attr('
|
|
206
|
+
g.append('use')
|
|
207
|
+
.attr('href', ref)
|
|
197
208
|
.attr('stroke', style.color)
|
|
198
209
|
.attr('stroke-width', 2 * (line.d + line.thickness))
|
|
199
210
|
.attr('stroke-opacity', line.opacity)
|
|
200
211
|
.attr('stroke-linejoin', 'round')
|
|
201
212
|
.attr('stroke-linecap', 'round');
|
|
202
|
-
g.append('
|
|
203
|
-
.attr('
|
|
213
|
+
g.append('use')
|
|
214
|
+
.attr('href', ref)
|
|
204
215
|
.attr('stroke', flatWater)
|
|
205
216
|
.attr('stroke-width', 2 * line.d)
|
|
206
217
|
.attr('stroke-linejoin', 'round')
|
|
@@ -460,6 +471,8 @@ export function renderMap(
|
|
|
460
471
|
.attr('mask', `url(#${maskId})`);
|
|
461
472
|
appendWaterLines(
|
|
462
473
|
gWater,
|
|
474
|
+
defs,
|
|
475
|
+
'dgmo-map-coast',
|
|
463
476
|
// Pass the canvas frame so edges collinear with it (the antimeridian on a
|
|
464
477
|
// world map, regional clipExtent cuts) don't get ringed as fake coast —
|
|
465
478
|
// land runs cleanly to the render-area edge.
|
|
@@ -513,6 +526,193 @@ export function renderMap(
|
|
|
513
526
|
}
|
|
514
527
|
}
|
|
515
528
|
|
|
529
|
+
// ── City dots — a faint gazetteer scatter for geographic orientation, over
|
|
530
|
+
// the basemap but under connectors/POIs/labels. Decorative (no pointer events,
|
|
531
|
+
// no data attributes); muted ink at low opacity so it reads as texture, not
|
|
532
|
+
// data. Suppressed entirely by the `no-cities` directive (empty array). ──
|
|
533
|
+
if (layout.cityDots.length) {
|
|
534
|
+
const gCities = svg
|
|
535
|
+
.append('g')
|
|
536
|
+
.attr('class', 'dgmo-map-cities')
|
|
537
|
+
.attr('fill', palette.textMuted)
|
|
538
|
+
.attr('fill-opacity', 0.28)
|
|
539
|
+
.style('pointer-events', 'none');
|
|
540
|
+
for (const c of layout.cityDots) {
|
|
541
|
+
gCities.append('circle').attr('cx', c.cx).attr('cy', c.cy).attr('r', c.r);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// ── Label clarity patch — repaint the clean region FILL under each POI label
|
|
546
|
+
// so the basemap line work beneath it (region borders, relief hachure, coast +
|
|
547
|
+
// river lines) dissolves where it would clutter the text. Crucially this is NOT
|
|
548
|
+
// a halo: the patch IS the same fill that already surrounds the label, so over
|
|
549
|
+
// plain land it is invisible and ONLY the crossing lines disappear — no white
|
|
550
|
+
// pad over flat colour. A blurred-blob mask feathers each patch into the map
|
|
551
|
+
// (soft edges, no hard rectangle). Sits above all basemap line work, below the
|
|
552
|
+
// dots / legs / labels. POI labels only; context/water labels keep their lines
|
|
553
|
+
// (they read as cartographic texture, not clutter).
|
|
554
|
+
//
|
|
555
|
+
// Collapsed cluster members are HIDDEN at rest, so their labels aren't on screen
|
|
556
|
+
// — patching under them would fade the basemap beneath an invisible label. So
|
|
557
|
+
// each cluster's member patches go in a per-cluster group tagged
|
|
558
|
+
// `data-cluster-deco`, which the app's spiderfy controller toggles in lockstep
|
|
559
|
+
// with the legs / member dots: the fade only appears once the fan is expanded.
|
|
560
|
+
// Non-clustered POI labels (and ALL labels in export / `no-cluster-pois`, where
|
|
561
|
+
// the fan is permanently open) keep the always-on patch so the static image
|
|
562
|
+
// matches what's on screen.
|
|
563
|
+
const clusterUi = !exportDims && !resolved.directives.noClusterPois;
|
|
564
|
+
const poiLabels = layout.labels.filter(
|
|
565
|
+
(l) => l.poiId !== undefined && !l.hidden
|
|
566
|
+
);
|
|
567
|
+
if (poiLabels.length) {
|
|
568
|
+
const patchBlurId = 'dgmo-map-label-patch-blur';
|
|
569
|
+
// Soft falloff so the patch dissolves into the surrounding basemap instead of
|
|
570
|
+
// ending on a hard edge. Tuned at the 11px label font. One shared filter for
|
|
571
|
+
// every patch group.
|
|
572
|
+
defs
|
|
573
|
+
.append('filter')
|
|
574
|
+
.attr('id', patchBlurId)
|
|
575
|
+
.attr('x', '-50%')
|
|
576
|
+
.attr('y', '-50%')
|
|
577
|
+
.attr('width', '200%')
|
|
578
|
+
.attr('height', '200%')
|
|
579
|
+
.append('feGaussianBlur')
|
|
580
|
+
.attr('in', 'SourceGraphic')
|
|
581
|
+
.attr('stdDeviation', 2.5);
|
|
582
|
+
const PAD = 8; // ≳ blur reach so culled regions can't lose a fringe
|
|
583
|
+
|
|
584
|
+
// Build one patch (mask of blurred blobs + the region fills they cross) for a
|
|
585
|
+
// set of labels. `decoCluster` tags the group so the app toggles it with the
|
|
586
|
+
// rest of the cluster's spiderfy decoration.
|
|
587
|
+
const buildPatch = (
|
|
588
|
+
labels: typeof poiLabels,
|
|
589
|
+
maskId: string,
|
|
590
|
+
decoCluster?: string
|
|
591
|
+
): void => {
|
|
592
|
+
if (!labels.length) return;
|
|
593
|
+
const patchMask = defs
|
|
594
|
+
.append('mask')
|
|
595
|
+
.attr('id', maskId)
|
|
596
|
+
// userSpaceOnUse: the patch group's bbox is the whole basemap, but keep
|
|
597
|
+
// the mask region pinned to the canvas (parity with the water masks).
|
|
598
|
+
.attr('maskUnits', 'userSpaceOnUse')
|
|
599
|
+
.attr('x', 0)
|
|
600
|
+
.attr('y', 0)
|
|
601
|
+
.attr('width', width)
|
|
602
|
+
.attr('height', height);
|
|
603
|
+
// White blobs = where the patch shows; the surrounding black hides it. Each
|
|
604
|
+
// blob is a SOLID rounded rect sized to the text bbox so the core fully
|
|
605
|
+
// hides the line work behind the glyphs; the blur only feathers the outer
|
|
606
|
+
// edge. Solid core → if an engine drops the in-mask blur (some WebKit
|
|
607
|
+
// builds), it degrades to a clean stadium patch, never a halo.
|
|
608
|
+
const blobs = patchMask
|
|
609
|
+
.append('g')
|
|
610
|
+
.attr('filter', `url(#${patchBlurId})`);
|
|
611
|
+
// Padded rects (incl. the blur reach) drive both the mask AND the region
|
|
612
|
+
// cull below, so the two never disagree.
|
|
613
|
+
const blobRects: Array<{
|
|
614
|
+
x0: number;
|
|
615
|
+
y0: number;
|
|
616
|
+
x1: number;
|
|
617
|
+
y1: number;
|
|
618
|
+
}> = [];
|
|
619
|
+
for (const l of labels) {
|
|
620
|
+
const multi = l.lines && l.lines.length > 1;
|
|
621
|
+
const tw = multi
|
|
622
|
+
? Math.max(...l.lines!.map((s) => measureText(s, LABEL_FONT)))
|
|
623
|
+
: measureText(l.text, LABEL_FONT);
|
|
624
|
+
const th = multi ? l.lines!.length * (LABEL_FONT + 2) : LABEL_FONT;
|
|
625
|
+
// Anchor → blob centre x; cy nudged up from the text baseline to the
|
|
626
|
+
// glyph visual centre.
|
|
627
|
+
const cx =
|
|
628
|
+
l.anchor === 'start'
|
|
629
|
+
? l.x + tw / 2
|
|
630
|
+
: l.anchor === 'end'
|
|
631
|
+
? l.x - tw / 2
|
|
632
|
+
: l.x;
|
|
633
|
+
const cy = l.y - LABEL_FONT * 0.3;
|
|
634
|
+
const w = tw + 8;
|
|
635
|
+
const h = th + 6;
|
|
636
|
+
blobs
|
|
637
|
+
.append('rect')
|
|
638
|
+
.attr('x', cx - w / 2)
|
|
639
|
+
.attr('y', cy - h / 2)
|
|
640
|
+
.attr('width', w)
|
|
641
|
+
.attr('height', h)
|
|
642
|
+
.attr('rx', h / 2)
|
|
643
|
+
.attr('fill', 'white');
|
|
644
|
+
blobRects.push({
|
|
645
|
+
x0: cx - w / 2 - PAD,
|
|
646
|
+
y0: cy - h / 2 - PAD,
|
|
647
|
+
x1: cx + w / 2 + PAD,
|
|
648
|
+
y1: cy + h / 2 + PAD,
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
const gPatch = svg
|
|
652
|
+
.append('g')
|
|
653
|
+
.attr('class', 'dgmo-map-label-patch')
|
|
654
|
+
.attr('mask', `url(#${maskId})`)
|
|
655
|
+
// Decorative cover — never a pointer target so region hover / name-on-
|
|
656
|
+
// hover reaches the real region paths beneath (WebKit hit-tests masked
|
|
657
|
+
// content).
|
|
658
|
+
.style('pointer-events', 'none');
|
|
659
|
+
// Per-cluster patch: tag as cluster decoration so the spiderfy controller
|
|
660
|
+
// shows/hides it in step with the fan. (The app collapses all `[data-
|
|
661
|
+
// cluster-deco]` to opacity 0 at rest, pre-paint, so there's no flash.)
|
|
662
|
+
if (decoCluster !== undefined)
|
|
663
|
+
gPatch.attr('data-cluster-deco', decoCluster);
|
|
664
|
+
// Redraw fills only (no stroke), in document order, but ONLY for regions
|
|
665
|
+
// whose bbox overlaps a label blob — the mask already hides everything
|
|
666
|
+
// else, so emitting the rest is pure SVG weight (matters on dense world
|
|
667
|
+
// maps). The kept regions reproduce the exact composite colour under each
|
|
668
|
+
// label, so the patch is seamless and the only visible change is the
|
|
669
|
+
// vanished line work.
|
|
670
|
+
for (const r of layout.regions) {
|
|
671
|
+
let minX = Infinity,
|
|
672
|
+
minY = Infinity,
|
|
673
|
+
maxX = -Infinity,
|
|
674
|
+
maxY = -Infinity;
|
|
675
|
+
for (const ring of parsePathRings(r.d))
|
|
676
|
+
for (const [px, py] of ring) {
|
|
677
|
+
if (px < minX) minX = px;
|
|
678
|
+
if (px > maxX) maxX = px;
|
|
679
|
+
if (py < minY) minY = py;
|
|
680
|
+
if (py > maxY) maxY = py;
|
|
681
|
+
}
|
|
682
|
+
const hit = blobRects.some(
|
|
683
|
+
(b) => minX <= b.x1 && maxX >= b.x0 && minY <= b.y1 && maxY >= b.y0
|
|
684
|
+
);
|
|
685
|
+
if (hit) gPatch.append('path').attr('d', r.d).attr('fill', r.fill);
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
if (clusterUi) {
|
|
690
|
+
// Always-on patch for labels that are visible at rest (non-cluster members).
|
|
691
|
+
buildPatch(
|
|
692
|
+
poiLabels.filter((l) => l.clusterMember === undefined),
|
|
693
|
+
'dgmo-map-label-patch'
|
|
694
|
+
);
|
|
695
|
+
// Per-cluster patches, hidden until the fan expands.
|
|
696
|
+
const byCluster = new Map<string, typeof poiLabels>();
|
|
697
|
+
for (const l of poiLabels) {
|
|
698
|
+
if (l.clusterMember === undefined) continue;
|
|
699
|
+
const arr = byCluster.get(l.clusterMember);
|
|
700
|
+
if (arr) arr.push(l);
|
|
701
|
+
else byCluster.set(l.clusterMember, [l]);
|
|
702
|
+
}
|
|
703
|
+
// Safe mask id from the iteration index — cluster ids can be coord-based
|
|
704
|
+
// (`@lat,lon`) and aren't `url(#…)`-safe; the raw id rides on the
|
|
705
|
+
// data-cluster-deco attribute instead.
|
|
706
|
+
let ci = 0;
|
|
707
|
+
for (const [cid, labs] of byCluster)
|
|
708
|
+
buildPatch(labs, `dgmo-map-label-patch-c${ci++}`, cid);
|
|
709
|
+
} else {
|
|
710
|
+
// Export / `no-cluster-pois`: fan is permanently open → one always-on patch
|
|
711
|
+
// over every POI label.
|
|
712
|
+
buildPatch(poiLabels, 'dgmo-map-label-patch');
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
516
716
|
// ── AK / HI insets (albers-usa) — drawn in the FOREGROUND so the opaque ocean
|
|
517
717
|
// box hides the main-map neighbour land (Mexico's Baja) behind it; the state
|
|
518
718
|
// then draws on top, framed by the box border. ──
|
|
@@ -560,11 +760,23 @@ export function renderMap(
|
|
|
560
760
|
.attr('y', 0)
|
|
561
761
|
.attr('width', width)
|
|
562
762
|
.attr('height', height);
|
|
763
|
+
// White reveal = the box interior, but eroded inward by `reach` (a black
|
|
764
|
+
// border stroke) so the seaward rings fade out before the frame instead of
|
|
765
|
+
// being hard-clipped at it — clipped ring fragments otherwise read as
|
|
766
|
+
// artifacts bumping into the crisp 1px border. Mirrors the main-map moat,
|
|
767
|
+
// applied inward.
|
|
768
|
+
const reach = Math.max(0, ...cs.lines.map((l) => l.d + l.thickness));
|
|
563
769
|
for (const box of layout.insets) {
|
|
564
770
|
const d =
|
|
565
771
|
box.points.map((p, i) => `${i ? 'L' : 'M'}${p[0]},${p[1]}`).join('') +
|
|
566
772
|
'Z';
|
|
567
|
-
mask
|
|
773
|
+
mask
|
|
774
|
+
.append('path')
|
|
775
|
+
.attr('d', d)
|
|
776
|
+
.attr('fill', 'white')
|
|
777
|
+
.attr('stroke', 'black')
|
|
778
|
+
.attr('stroke-width', 2 * reach)
|
|
779
|
+
.attr('stroke-linejoin', 'round');
|
|
568
780
|
}
|
|
569
781
|
// Neighbour land masks as land too — clipped to its box so it can't darken
|
|
570
782
|
// an adjacent inset — keeping the AK/Canada land border free of rings.
|
|
@@ -607,6 +819,8 @@ export function renderMap(
|
|
|
607
819
|
.attr('mask', `url(#${maskId})`);
|
|
608
820
|
appendWaterLines(
|
|
609
821
|
gInsetWater,
|
|
822
|
+
defs,
|
|
823
|
+
'dgmo-map-inset-coast',
|
|
610
824
|
coastlineOuterRings(layout.insetRegions, cs.minExtent),
|
|
611
825
|
cs,
|
|
612
826
|
layout.background
|
|
@@ -620,6 +834,25 @@ export function renderMap(
|
|
|
620
834
|
.attr('stroke-width', 0.5)
|
|
621
835
|
.attr('stroke-linejoin', 'round');
|
|
622
836
|
}
|
|
837
|
+
|
|
838
|
+
// Re-stroke each inset frame ON TOP (stroke-only, no fill) as the final
|
|
839
|
+
// inset layer — the early border path sits under the contextLand fill,
|
|
840
|
+
// region fills, and water-lines, so its inner half gets covered near the
|
|
841
|
+
// top/right edges and the frame reads as thin/fuzzy. This overlay keeps a
|
|
842
|
+
// crisp full-width border on every side.
|
|
843
|
+
for (const box of layout.insets) {
|
|
844
|
+
const d =
|
|
845
|
+
box.points.map((p, i) => `${i ? 'L' : 'M'}${p[0]},${p[1]}`).join('') +
|
|
846
|
+
'Z';
|
|
847
|
+
insetG
|
|
848
|
+
.append('path')
|
|
849
|
+
.attr('d', d)
|
|
850
|
+
.attr('fill', 'none')
|
|
851
|
+
.attr('stroke', mix(palette.text, palette.bg, 55))
|
|
852
|
+
.attr('stroke-width', 1)
|
|
853
|
+
.attr('stroke-linejoin', 'round')
|
|
854
|
+
.style('pointer-events', 'none');
|
|
855
|
+
}
|
|
623
856
|
}
|
|
624
857
|
|
|
625
858
|
// Code↔diagram sync: tag a synced element with its 1-based source line and,
|
|
@@ -647,7 +880,17 @@ export function renderMap(
|
|
|
647
880
|
.attr('d', leg.d)
|
|
648
881
|
.attr('stroke', leg.color)
|
|
649
882
|
.attr('stroke-width', leg.width)
|
|
650
|
-
.attr('stroke-linecap', 'round')
|
|
883
|
+
.attr('stroke-linecap', 'round')
|
|
884
|
+
// Endpoint POI ids so a focused leg can co-highlight its two POIs (§17 sync).
|
|
885
|
+
.attr('data-from-id', leg.fromId)
|
|
886
|
+
.attr('data-to-id', leg.toId);
|
|
887
|
+
// Tag the line per tag value (lowercased, matching the legend-entry attrs) so
|
|
888
|
+
// a legend hover spotlights only the matching lines (§24B.6) — mirrors POIs.
|
|
889
|
+
if (leg.tags) {
|
|
890
|
+
for (const [group, value] of Object.entries(leg.tags)) {
|
|
891
|
+
p.attr(`data-tag-${group.toLowerCase()}`, value.toLowerCase());
|
|
892
|
+
}
|
|
893
|
+
}
|
|
651
894
|
// A 0-width invisible leg path is hard to hit; pointer-events on the visible
|
|
652
895
|
// stroke is enough for the line widths legs use.
|
|
653
896
|
wireSync(p, leg.lineNumber);
|
|
@@ -698,7 +941,6 @@ export function renderMap(
|
|
|
698
941
|
// the legs/hub/members visible but emit NO hit-area + NO badge, so there is
|
|
699
942
|
// nothing for the app's spiderfy controller to collapse — the map reads the
|
|
700
943
|
// same on screen as on paper.
|
|
701
|
-
const clusterUi = !exportDims && !resolved.directives.noClusterPois;
|
|
702
944
|
const gSpider = svg.append('g').attr('class', 'dgmo-map-spider');
|
|
703
945
|
for (const cl of layout.clusters) {
|
|
704
946
|
// Pointer hit-area — bottom of the stack so member dots still take their own
|
|
@@ -755,6 +997,7 @@ export function renderMap(
|
|
|
755
997
|
.attr('cy', poi.cy)
|
|
756
998
|
.attr('r', poi.r)
|
|
757
999
|
.attr('fill', poi.fill)
|
|
1000
|
+
.attr('fill-opacity', poi.fillOpacity)
|
|
758
1001
|
.attr('stroke', poi.stroke)
|
|
759
1002
|
.attr('stroke-width', 1)
|
|
760
1003
|
.attr('data-line-number', poi.lineNumber)
|
|
@@ -807,7 +1050,7 @@ export function renderMap(
|
|
|
807
1050
|
lab.color,
|
|
808
1051
|
lab.haloColor,
|
|
809
1052
|
lab.halo,
|
|
810
|
-
LABEL_FONT,
|
|
1053
|
+
lab.fontSize ?? LABEL_FONT,
|
|
811
1054
|
lab.italic,
|
|
812
1055
|
lab.letterSpacing
|
|
813
1056
|
)
|
|
@@ -836,6 +1079,18 @@ export function renderMap(
|
|
|
836
1079
|
line.attr('data-cluster-member', lab.clusterMember);
|
|
837
1080
|
wireSync(line, lab.lineNumber);
|
|
838
1081
|
}
|
|
1082
|
+
// Region callout anchor dot: a small disc at the (too-small) region's true
|
|
1083
|
+
// centroid, in the region's fill, so the leader visibly originates on the map.
|
|
1084
|
+
if (lab.calloutDot) {
|
|
1085
|
+
gLabels
|
|
1086
|
+
.append('circle')
|
|
1087
|
+
.attr('cx', lab.calloutDot.x)
|
|
1088
|
+
.attr('cy', lab.calloutDot.y)
|
|
1089
|
+
.attr('r', 2.5)
|
|
1090
|
+
.attr('fill', lab.calloutDot.color)
|
|
1091
|
+
.attr('stroke', mix(palette.text, palette.bg, 55))
|
|
1092
|
+
.attr('stroke-width', 0.75);
|
|
1093
|
+
}
|
|
839
1094
|
const t = emitText(
|
|
840
1095
|
gLabels,
|
|
841
1096
|
lab.x,
|
|
@@ -845,10 +1100,11 @@ export function renderMap(
|
|
|
845
1100
|
lab.color,
|
|
846
1101
|
lab.haloColor,
|
|
847
1102
|
lab.halo,
|
|
848
|
-
LABEL_FONT,
|
|
1103
|
+
lab.fontSize ?? LABEL_FONT,
|
|
849
1104
|
lab.italic,
|
|
850
1105
|
lab.letterSpacing,
|
|
851
|
-
lab.lines
|
|
1106
|
+
lab.lines,
|
|
1107
|
+
lab.valueLine
|
|
852
1108
|
);
|
|
853
1109
|
// POI labels are spotlightable: tag with the POI id and make the text the
|
|
854
1110
|
// hover target (the app dims the other dots/labels on enter).
|
|
@@ -955,7 +1211,7 @@ export function renderMap(
|
|
|
955
1211
|
// ── Title / subtitle / caption (foreground — drawn last so they sit above the
|
|
956
1212
|
// basemap, POIs, and labels; layout reserves top padding so POIs clear them) ──
|
|
957
1213
|
// Soft bg halo so the banner stays legible over busy land/water (the muted
|
|
958
|
-
// subtitle/caption otherwise wash out on mid-toned palettes like
|
|
1214
|
+
// subtitle/caption otherwise wash out on mid-toned palettes like nord).
|
|
959
1215
|
if (layout.title) {
|
|
960
1216
|
svg
|
|
961
1217
|
.append('text')
|
|
@@ -1032,7 +1288,8 @@ function emitText(
|
|
|
1032
1288
|
fontSize: number,
|
|
1033
1289
|
italic?: boolean,
|
|
1034
1290
|
letterSpacing?: number,
|
|
1035
|
-
lines?: readonly string[]
|
|
1291
|
+
lines?: readonly string[],
|
|
1292
|
+
valueLine?: string
|
|
1036
1293
|
): d3Selection.Selection<SVGTextElement, unknown, null, undefined> {
|
|
1037
1294
|
const t = g
|
|
1038
1295
|
.append('text')
|
|
@@ -1053,6 +1310,21 @@ function emitText(
|
|
|
1053
1310
|
.attr('dy', i === 0 ? startDy : lineHeight)
|
|
1054
1311
|
.text(ln);
|
|
1055
1312
|
});
|
|
1313
|
+
} else if (valueLine !== undefined) {
|
|
1314
|
+
// A choropleth region's name over its metric value: name on top, value below
|
|
1315
|
+
// in a smaller, dimmer line (same fill, reduced opacity). Raise the block so
|
|
1316
|
+
// its centre sits on the placement `y`. The halo (set below) wraps both lines.
|
|
1317
|
+
const valueSize = Math.round(fontSize * 0.82);
|
|
1318
|
+
t.append('tspan')
|
|
1319
|
+
.attr('x', x)
|
|
1320
|
+
.attr('dy', -fontSize * 0.28)
|
|
1321
|
+
.text(text);
|
|
1322
|
+
t.append('tspan')
|
|
1323
|
+
.attr('x', x)
|
|
1324
|
+
.attr('dy', fontSize * 0.92)
|
|
1325
|
+
.attr('font-size', valueSize)
|
|
1326
|
+
.attr('fill-opacity', 0.72)
|
|
1327
|
+
.text(valueLine);
|
|
1056
1328
|
} else {
|
|
1057
1329
|
t.text(text);
|
|
1058
1330
|
}
|
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
import type { DgmoError } from '../diagnostics';
|
|
5
5
|
import type { TagGroup } from '../utils/tag-groups';
|
|
6
6
|
import type { MapDirectives } from './types';
|
|
7
|
-
import type {
|
|
7
|
+
import type {
|
|
8
|
+
Gazetteer,
|
|
9
|
+
BoundaryTopology,
|
|
10
|
+
WaterBodies,
|
|
11
|
+
AirportData,
|
|
12
|
+
} from './data/types';
|
|
8
13
|
|
|
9
14
|
/** The four static assets, injected into the pure resolver (DI). */
|
|
10
15
|
export interface MapData {
|
|
@@ -32,6 +37,10 @@ export interface MapData {
|
|
|
32
37
|
* `context-labels` directive is on — oceans/seas/gulfs/bays/etc. Optional, so
|
|
33
38
|
* hand-built test fixtures and older bundles need not supply it. */
|
|
34
39
|
waterBodies?: WaterBodies;
|
|
40
|
+
/** IATA-coded airports (`airports.json`) — lets `poi JFK` / `route JFK -> LAX`
|
|
41
|
+
* resolve. Optional so hand-built fixtures and older DI bundles need not supply
|
|
42
|
+
* it; the resolver guards `data.airports?.…` everywhere. */
|
|
43
|
+
airports?: AirportData;
|
|
35
44
|
gazetteer: Gazetteer;
|
|
36
45
|
}
|
|
37
46
|
|
|
@@ -40,6 +49,7 @@ export type ProjectionFamily =
|
|
|
40
49
|
| 'natural-earth'
|
|
41
50
|
| 'equirectangular'
|
|
42
51
|
| 'albers-usa'
|
|
52
|
+
| 'conic-equal-area'
|
|
43
53
|
| 'mercator';
|
|
44
54
|
|
|
45
55
|
/** Which geometry layers the renderer draws. */
|
|
@@ -89,6 +99,8 @@ export interface ResolvedEdge {
|
|
|
89
99
|
readonly directed: boolean;
|
|
90
100
|
readonly style: 'straight' | 'arc';
|
|
91
101
|
readonly meta: Readonly<Record<string, string>>;
|
|
102
|
+
/** Tag(s) on the edge line → colour the LINE (§24B.6). */
|
|
103
|
+
readonly tags: Readonly<Record<string, string>>;
|
|
92
104
|
readonly lineNumber: number;
|
|
93
105
|
}
|
|
94
106
|
|
|
@@ -98,6 +110,8 @@ export interface ResolvedRouteLeg {
|
|
|
98
110
|
readonly label?: string;
|
|
99
111
|
readonly style: 'straight' | 'arc';
|
|
100
112
|
readonly value?: string; // leg thickness
|
|
113
|
+
/** Tag(s) on the leg line → colour the LINE (§24B.6). */
|
|
114
|
+
readonly tags: Readonly<Record<string, string>>;
|
|
101
115
|
readonly lineNumber: number;
|
|
102
116
|
}
|
|
103
117
|
|