@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/resolver.ts
CHANGED
|
@@ -78,6 +78,36 @@ const CONTAINER_OVERSHOOT_DEG = 8;
|
|
|
78
78
|
// albers-usa composite (CONUS conic + AK/HI insets) instead of regional Mercator.
|
|
79
79
|
// CONUS spans ≈58° lon; 48° is "most of the country". Tunable.
|
|
80
80
|
const US_NATIONAL_LON_SPAN = 48;
|
|
81
|
+
// Sub-national US auto-zoom (map-us-subnational-zoom). A US region/choropleth map
|
|
82
|
+
// whose authored data occupies a geographically COMPACT slice of the country
|
|
83
|
+
// zooms to that slice (conic-equal-area, like every non-US regional view) instead
|
|
84
|
+
// of always framing the whole nation on albers-usa. "Compact" = the raw data bbox
|
|
85
|
+
// covers less than US_SUBNATIONAL_AREA_FRACTION of the CONUS bbox AREA. Area (not
|
|
86
|
+
// lon-span) is used so a near-national diagonal (e.g. WA+FL) stays national while
|
|
87
|
+
// a tight cluster (the Northeast) or a tall corridor zooms. CONUS_BBOX ≈ the
|
|
88
|
+
// contiguous 48 (≈1416 deg²); the fraction bisects the empty gap between a
|
|
89
|
+
// regional cluster (≲0.15) and a national map (≳0.9). Tunable.
|
|
90
|
+
const CONUS_BBOX: GeoExtent = [
|
|
91
|
+
[-125, 25],
|
|
92
|
+
[-66, 49],
|
|
93
|
+
];
|
|
94
|
+
const US_SUBNATIONAL_AREA_FRACTION = 0.4;
|
|
95
|
+
|
|
96
|
+
/** Rectangular (lon×lat) area of a bbox in deg² — NOT d3 `geoArea` (spherical).
|
|
97
|
+
* The gate frames with a lon/lat bbox anyway, so a planar ratio is the honest
|
|
98
|
+
* measure of "how much of the country does this occupy". */
|
|
99
|
+
function bboxArea(b: GeoExtent): number {
|
|
100
|
+
return (b[1][0] - b[0][0]) * (b[1][1] - b[0][1]);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** True when a US data extent is compact enough to zoom to (sub-national) rather
|
|
104
|
+
* than frame the whole nation. Pure + side-effect-free (unit-tested in isolation
|
|
105
|
+
* with no fixtures/render). Pass the RAW unpadded data-union bbox, not the padded
|
|
106
|
+
* framing extent — the fraction is calibrated on raw data bboxes. */
|
|
107
|
+
export function isSubNationalUsExtent(bbox: GeoExtent | null): boolean {
|
|
108
|
+
if (!bbox) return false;
|
|
109
|
+
return bboxArea(bbox) / bboxArea(CONUS_BBOX) < US_SUBNATIONAL_AREA_FRACTION;
|
|
110
|
+
}
|
|
81
111
|
|
|
82
112
|
// Long-form (or common-alias) country name → the folded Natural-Earth display
|
|
83
113
|
// name actually shipped in world-coarse (#6). The NE coarse layer abbreviates a
|
|
@@ -216,6 +246,9 @@ export function resolveMap(parsed: ParsedMap, data: MapData): ResolvedMap {
|
|
|
216
246
|
const warn = (line: number, message: string, code?: string): void => {
|
|
217
247
|
diagnostics.push(makeDgmoError(line, message, 'warning', code));
|
|
218
248
|
};
|
|
249
|
+
// Folded tokens already flagged as "city shadows an airport code" — emit the
|
|
250
|
+
// W_MAP_AIRPORT_SHADOWED_BY_CITY hint at most once per code (ADR-2, F9).
|
|
251
|
+
const shadowedAirports = new Set<string>();
|
|
219
252
|
|
|
220
253
|
const result: Writable<ResolvedMap> = {
|
|
221
254
|
title: parsed.title,
|
|
@@ -286,6 +319,12 @@ export function resolveMap(parsed: ParsedMap, data: MapData): ResolvedMap {
|
|
|
286
319
|
const regions: ResolvedRegion[] = [];
|
|
287
320
|
const seenRegion = new Map<string, number>(); // iso → index in regions
|
|
288
321
|
let usSubdivisionReferenced = false;
|
|
322
|
+
// AK/HI national guard (map-us-subnational-zoom): a choropleth that colors
|
|
323
|
+
// Alaska or Hawaii is inherently a national composite map (the insets only make
|
|
324
|
+
// sense on albers-usa), so it never sub-national auto-zooms. Tracked ISO-based
|
|
325
|
+
// here because `referencedRegionIds` is not in scope at the projection block.
|
|
326
|
+
let hasAlaskaRef = false;
|
|
327
|
+
let hasHawaiiRef = false;
|
|
289
328
|
const referencedRegionIds: { topo: 'us'; id: string }[] = [];
|
|
290
329
|
for (const r of parsed.regions) {
|
|
291
330
|
const f = fold(r.name);
|
|
@@ -356,6 +395,8 @@ export function resolveMap(parsed: ParsedMap, data: MapData): ResolvedMap {
|
|
|
356
395
|
}
|
|
357
396
|
if (chosen.layer === 'us-state') {
|
|
358
397
|
usSubdivisionReferenced = true;
|
|
398
|
+
if (chosen.id === 'US-AK') hasAlaskaRef = true;
|
|
399
|
+
if (chosen.id === 'US-HI') hasHawaiiRef = true;
|
|
359
400
|
referencedRegionIds.push({ topo: 'us', id: chosen.id });
|
|
360
401
|
}
|
|
361
402
|
const resolved: ResolvedRegion = {
|
|
@@ -428,6 +469,28 @@ export function resolveMap(parsed: ParsedMap, data: MapData): ResolvedMap {
|
|
|
428
469
|
if (aliasIdx !== undefined) idxs = [aliasIdx];
|
|
429
470
|
}
|
|
430
471
|
if (!idxs?.length) {
|
|
472
|
+
// Airports are the LOWEST-precedence identifier tier (ADR-2): consulted
|
|
473
|
+
// only after city `byName` + `alt` miss, so a real city always wins a
|
|
474
|
+
// shared token. Exact folded-code match — never defers. Returns just
|
|
475
|
+
// {lat,lon,iso} like a city; the POI renders the user's typed token ("JFK")
|
|
476
|
+
// as its label, so there is no airport-specific label work.
|
|
477
|
+
const airIdx = data.airports?.airportIata?.[f];
|
|
478
|
+
if (airIdx !== undefined) {
|
|
479
|
+
const a = data.airports!.airports[airIdx];
|
|
480
|
+
if (a) return { kind: 'ok', lat: a[0], lon: a[1], iso: a[2] };
|
|
481
|
+
}
|
|
482
|
+
// A 3-letter token that resolved to neither a city nor a bundled airport is
|
|
483
|
+
// almost always an airport code the set doesn't cover — give the targeted
|
|
484
|
+
// hint (with the `as XXX` coords escape hatch) instead of the generic miss.
|
|
485
|
+
if (/^[A-Za-z]{3}$/.test(name)) {
|
|
486
|
+
const code = name.toUpperCase();
|
|
487
|
+
err(
|
|
488
|
+
line,
|
|
489
|
+
`Unknown airport code "${code}" — not in the bundled airport set (large hubs + US commercial). Use coordinates with \`as ${code}\` if you need it.`,
|
|
490
|
+
'E_MAP_UNKNOWN_AIRPORT_CODE'
|
|
491
|
+
);
|
|
492
|
+
return { kind: 'miss' };
|
|
493
|
+
}
|
|
431
494
|
const cityNames = data.gazetteer.cities.map((c) => c[4]);
|
|
432
495
|
const hint = suggest(name, cityNames);
|
|
433
496
|
err(
|
|
@@ -469,6 +532,22 @@ export function resolveMap(parsed: ParsedMap, data: MapData): ResolvedMap {
|
|
|
469
532
|
);
|
|
470
533
|
}
|
|
471
534
|
const c = cands[0]!;
|
|
535
|
+
// Shadow hint (ADR-2/F9): the token resolved to a city but is ALSO a bundled
|
|
536
|
+
// IATA code (e.g. `Ufa` city vs UFA airport). Non-blocking, once per code, and
|
|
537
|
+
// only for the `byName` overlap set (an `alt`-alias hit whose key differs from
|
|
538
|
+
// the IATA fold is not flagged — a documented limit).
|
|
539
|
+
if (
|
|
540
|
+
data.airports?.airportIata?.[f] !== undefined &&
|
|
541
|
+
!shadowedAirports.has(f)
|
|
542
|
+
) {
|
|
543
|
+
shadowedAirports.add(f);
|
|
544
|
+
const code = name.toUpperCase();
|
|
545
|
+
warn(
|
|
546
|
+
line,
|
|
547
|
+
`"${name}" resolved to the city; "${code}" is also an airport code. Use coordinates with \`as ${code}\` for the airport.`,
|
|
548
|
+
'W_MAP_AIRPORT_SHADOWED_BY_CITY'
|
|
549
|
+
);
|
|
550
|
+
}
|
|
472
551
|
return { kind: 'ok', lat: c[0], lon: c[1], iso: c[2] };
|
|
473
552
|
};
|
|
474
553
|
|
|
@@ -614,6 +693,7 @@ export function resolveMap(parsed: ParsedMap, data: MapData): ResolvedMap {
|
|
|
614
693
|
directed: e.directed,
|
|
615
694
|
style: e.style,
|
|
616
695
|
meta: e.meta,
|
|
696
|
+
tags: e.tags,
|
|
617
697
|
lineNumber: e.lineNumber,
|
|
618
698
|
});
|
|
619
699
|
}
|
|
@@ -698,7 +778,7 @@ export function resolveMap(parsed: ParsedMap, data: MapData): ResolvedMap {
|
|
|
698
778
|
leg.dest,
|
|
699
779
|
leg.destAlias,
|
|
700
780
|
leg.destLabel,
|
|
701
|
-
leg.
|
|
781
|
+
{}, // a leg tag colours the LINE (§24B.6), not the destination stop
|
|
702
782
|
undefined, // a leg's `value:` is leg thickness, not the dest's size
|
|
703
783
|
leg.lineNumber
|
|
704
784
|
);
|
|
@@ -709,6 +789,7 @@ export function resolveMap(parsed: ParsedMap, data: MapData): ResolvedMap {
|
|
|
709
789
|
...(leg.label !== undefined && { label: leg.label }),
|
|
710
790
|
style: leg.style,
|
|
711
791
|
...(leg.value !== undefined && { value: leg.value }),
|
|
792
|
+
tags: leg.tags,
|
|
712
793
|
lineNumber: leg.lineNumber,
|
|
713
794
|
});
|
|
714
795
|
if (!stopIds.includes(destId)) stopIds.push(destId); // unique markers (loop-close dedupe)
|
|
@@ -767,6 +848,14 @@ export function resolveMap(parsed: ParsedMap, data: MapData): ResolvedMap {
|
|
|
767
848
|
}
|
|
768
849
|
const points: Array<[number, number]> = pois.map((p) => [p.lon, p.lat]);
|
|
769
850
|
const unioned = unionExtent(regionBoxes, points);
|
|
851
|
+
// Sub-national US auto-zoom gate (map-us-subnational-zoom). Computed from the
|
|
852
|
+
// RAW unpadded union (NOT the padded `extent` below, which inflates area ~1.5×).
|
|
853
|
+
// `usSubNational` decides WHETHER to zoom; the projection-family picker below
|
|
854
|
+
// still keys off span as before. `localeUsForced` = `locale US` with no
|
|
855
|
+
// subdivision → the author asked for the whole-country frame, so stay national
|
|
856
|
+
// even on compact data.
|
|
857
|
+
const usSubNational = isSubNationalUsExtent(unioned);
|
|
858
|
+
const localeUsForced = localeCountry === 'US' && !localeSubdivision;
|
|
770
859
|
const DEFAULT_EXTENT: GeoExtent = [
|
|
771
860
|
[-180, -85],
|
|
772
861
|
[180, 85],
|
|
@@ -821,7 +910,14 @@ export function resolveMap(parsed: ParsedMap, data: MapData): ResolvedMap {
|
|
|
821
910
|
// symmetrically about its centroid until the longer axis reaches
|
|
822
911
|
// POI_ZOOM_FLOOR_DEG so recognizable land always frames the dots. Uniform scale
|
|
823
912
|
// preserves the aspect; the layout's fitExtent letterboxes to canvas.
|
|
824
|
-
|
|
913
|
+
// Also applies to a sub-national US region map (map-us-subnational-zoom) so a
|
|
914
|
+
// single tiny state (e.g. Rhode Island) keeps neighbour context instead of
|
|
915
|
+
// over-zooming into near-blank land. `usSubNational` is derived from the raw
|
|
916
|
+
// union above, so flooring the framing `extent` here doesn't feed back into it.
|
|
917
|
+
// Gated on `usOriented` — `usSubNational` is a pure area-vs-CONUS ratio, true
|
|
918
|
+
// for ANY small bbox, so without this guard a compact non-US region (e.g. a
|
|
919
|
+
// single small European country) would be floored too, regressing AC10.
|
|
920
|
+
if (isPoiOnly || (usSubNational && usOriented)) {
|
|
825
921
|
const cx = (extent[0][0] + extent[1][0]) / 2;
|
|
826
922
|
const cy = (extent[0][1] + extent[1][1]) / 2;
|
|
827
923
|
const lon = extent[1][0] - extent[0][0];
|
|
@@ -850,15 +946,34 @@ export function resolveMap(parsed: ParsedMap, data: MapData): ResolvedMap {
|
|
|
850
946
|
// intentionally snaps a POI-only US city map to the national frame ("show all
|
|
851
947
|
// states") rather than fit-zooming to the cluster on a geographic projection.
|
|
852
948
|
// (§24B.2 — projection is inferred, never configured.)
|
|
949
|
+
// A compact US region/choropleth that should zoom rather than frame the nation
|
|
950
|
+
// (map-us-subnational-zoom): US-oriented, area-compact, not a POI map, and not
|
|
951
|
+
// forced national by an AK/HI inset or `locale US`.
|
|
952
|
+
const usSubNationalZoom =
|
|
953
|
+
usOriented &&
|
|
954
|
+
usSubNational &&
|
|
955
|
+
!isPoiOnly &&
|
|
956
|
+
!hasAlaskaRef &&
|
|
957
|
+
!hasHawaiiRef &&
|
|
958
|
+
!localeUsForced;
|
|
853
959
|
let projection: ProjectionFamily;
|
|
854
|
-
if (
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
//
|
|
960
|
+
if (
|
|
961
|
+
usOriented &&
|
|
962
|
+
lonSpan < US_NATIONAL_LON_SPAN &&
|
|
963
|
+
(isPoiOnly || usSubNationalZoom)
|
|
964
|
+
) {
|
|
965
|
+
// Sub-national US, zoomed: regional Mercator. North is straight up everywhere
|
|
966
|
+
// (vertical meridians) — a conic equal-area fans its meridians toward the pole
|
|
967
|
+
// so an off-centre region visibly tilts, which reads wrong for a "zoom into the
|
|
968
|
+
// US" view. Area distortion across a sub-national frame is negligible, and the
|
|
969
|
+
// POI-cluster path already uses Mercator here. Covers both a POI cluster (fit
|
|
970
|
+
// to the floored extent) and a compact region/choropleth. The us-states mesh
|
|
971
|
+
// is still drawn (subdivisions via usOriented), so neighbours frame the data.
|
|
860
972
|
projection = 'mercator';
|
|
861
973
|
} else if (usOriented) {
|
|
974
|
+
// National US frame: spread-out region data, a wide corridor (lonSpan ≥ the
|
|
975
|
+
// national threshold), a POI map that spans the country, an AK/HI composite,
|
|
976
|
+
// or an explicit `locale US` whole-country request. (§24B.2 — albers-usa.)
|
|
862
977
|
projection = 'albers-usa';
|
|
863
978
|
} else if (span > WORLD_SPAN || maxAbsLat > MERCATOR_MAX_LAT) {
|
|
864
979
|
// World/multi-continent scale (or a polar-reaching frame). Every world map —
|
|
@@ -868,10 +983,15 @@ export function resolveMap(parsed: ParsedMap, data: MapData): ResolvedMap {
|
|
|
868
983
|
// consistent rectangular look over Equal Earth's area honesty.)
|
|
869
984
|
projection = 'equirectangular';
|
|
870
985
|
} else {
|
|
871
|
-
// Tight clusters AND single-continent regional views:
|
|
872
|
-
//
|
|
873
|
-
//
|
|
874
|
-
projection
|
|
986
|
+
// Tight clusters AND single-continent regional views: a conic equal-area
|
|
987
|
+
// (Albers) projection, parameterized per-extent in the layout. This is the
|
|
988
|
+
// conventional atlas projection for a single mid-latitude continent — it
|
|
989
|
+
// keeps familiar country shapes (no world-projection horizontal squash) and,
|
|
990
|
+
// unlike Mercator, does NOT vertically inflate high-latitude land. That
|
|
991
|
+
// inflation is what pushed a Europe choropleth's colored mass into the bottom
|
|
992
|
+
// ~30% of the frame (Scandinavia's stretch wasted the upper half on empty
|
|
993
|
+
// ocean); equal-area framing re-centers the data and scales the land larger.
|
|
994
|
+
projection = 'conic-equal-area';
|
|
875
995
|
}
|
|
876
996
|
|
|
877
997
|
// World-scale framing (R10): a multi-continent spread frames most cleanly as
|
package/src/map/types.ts
CHANGED
|
@@ -19,9 +19,13 @@ export type PoiPos =
|
|
|
19
19
|
export interface MapDirectives {
|
|
20
20
|
/** Legend label for the region value ramp (`region-metric <label>`). */
|
|
21
21
|
regionMetric?: string;
|
|
22
|
-
/** Recognized color NAME for the choropleth ramp
|
|
22
|
+
/** Recognized color NAME for the choropleth ramp HIGH endpoint, peeled off the
|
|
23
23
|
* `region-metric` trailing token (§24B.3). Defaults to red when absent. */
|
|
24
24
|
regionMetricColor?: string;
|
|
25
|
+
/** Recognized color NAME for the choropleth ramp LOW endpoint (the second,
|
|
26
|
+
* left-of-two trailing colors on `region-metric`, §24B.3). Absent ⇒ the low
|
|
27
|
+
* end is the implied floored neutral (today's single-colour behaviour). */
|
|
28
|
+
regionMetricLowColor?: string;
|
|
25
29
|
/** Legend label for the POI value (marker size) channel (`poi-metric`). */
|
|
26
30
|
poiMetric?: string;
|
|
27
31
|
/** Legend label for the edge/leg value (thickness) channel (`flow-metric`). */
|
|
@@ -49,12 +53,20 @@ export interface MapDirectives {
|
|
|
49
53
|
noContextLabels?: boolean;
|
|
50
54
|
/** `no-region-labels` — suppress region labels (default-on, full→abbrev→hide). */
|
|
51
55
|
noRegionLabels?: boolean;
|
|
56
|
+
/** `no-region-value` — suppress the metric VALUE shown under each data region's
|
|
57
|
+
* name on a `region-metric` choropleth (default-on). The region NAME still
|
|
58
|
+
* renders (governed by `no-region-labels`); only the numeric value line goes. */
|
|
59
|
+
noRegionValue?: boolean;
|
|
52
60
|
/** `no-poi-labels` — suppress POI labels (default-on, collision-managed auto). */
|
|
53
61
|
noPoiLabels?: boolean;
|
|
54
62
|
/** `no-colorize` — force the plain green-land reference dress even when regions
|
|
55
63
|
* are referenced (regions are auto-coloured by default; §24B colorize). A
|
|
56
64
|
* no-op under data — the basemap is already gray there. */
|
|
57
65
|
noColorize?: boolean;
|
|
66
|
+
/** `no-cities` — suppress the subtle gazetteer city dots scattered across the
|
|
67
|
+
* basemap for geographic orientation (default-on; population-ranked, spacing-
|
|
68
|
+
* thinned so density adapts to zoom). Explicit POIs always draw regardless. */
|
|
69
|
+
noCities?: boolean;
|
|
58
70
|
/** `no-cluster-pois` — never collapse coincident POI markers into a count badge
|
|
59
71
|
* (clustering/spiderfy is default-on in the interactive preview). With this set
|
|
60
72
|
* the markers always render fanned out with their legs — the same as a static
|
|
@@ -99,8 +111,8 @@ export interface MapPoi {
|
|
|
99
111
|
|
|
100
112
|
/** One leg of a route (§24B.6): an edge from the previous stop to `dest`. Reuses
|
|
101
113
|
* the edge arrow idiom — in-arrow text = leg label, `value:` = leg thickness,
|
|
102
|
-
* `->`/`~>` (or the header `style: arc`) = shape.
|
|
103
|
-
*
|
|
114
|
+
* `->`/`~>` (or the header `style: arc`) = shape. A tag on the leg line colours
|
|
115
|
+
* the LINE (§24B.6); `label:`/`as` still name the DESTINATION stop. */
|
|
104
116
|
export interface MapRouteLeg {
|
|
105
117
|
readonly label?: string; // in-arrow leg label
|
|
106
118
|
readonly style: 'straight' | 'arc';
|
|
@@ -108,7 +120,9 @@ export interface MapRouteLeg {
|
|
|
108
120
|
readonly dest: PoiPos;
|
|
109
121
|
readonly destAlias?: string;
|
|
110
122
|
readonly destLabel?: string;
|
|
111
|
-
|
|
123
|
+
/** Tag(s) on the leg line → colour the LINE itself. To categorise a STOP,
|
|
124
|
+
* tag its own `poi` line. */
|
|
125
|
+
readonly tags: Readonly<Record<string, string>>;
|
|
112
126
|
readonly lineNumber: number;
|
|
113
127
|
}
|
|
114
128
|
|
|
@@ -127,7 +141,9 @@ export interface MapRoute {
|
|
|
127
141
|
}
|
|
128
142
|
|
|
129
143
|
/** A connector (§24B.6). Endpoints are RAW identifier strings (name or alias);
|
|
130
|
-
* binding to POIs/regions is the resolver's job.
|
|
144
|
+
* binding to POIs/regions is the resolver's job. Token = arrowhead iff it ends
|
|
145
|
+
* in `>`, arc iff it starts with `~`: `->` straight, `~>` arc, `--`/`-label-`
|
|
146
|
+
* undirected straight, `~~`/`~label~` undirected arc. */
|
|
131
147
|
export interface MapEdge {
|
|
132
148
|
readonly from: string;
|
|
133
149
|
readonly to: string;
|
|
@@ -135,6 +151,8 @@ export interface MapEdge {
|
|
|
135
151
|
readonly directed: boolean;
|
|
136
152
|
readonly style: 'straight' | 'arc';
|
|
137
153
|
readonly meta: Readonly<Record<string, string>>;
|
|
154
|
+
/** Tag(s) on the edge line → colour the LINE itself (§24B.6). */
|
|
155
|
+
readonly tags: Readonly<Record<string, string>>;
|
|
138
156
|
readonly lineNumber: number;
|
|
139
157
|
}
|
|
140
158
|
|
|
@@ -166,8 +184,10 @@ export interface MapLayoutLegend {
|
|
|
166
184
|
metric?: string;
|
|
167
185
|
min: number;
|
|
168
186
|
max: number;
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
187
|
+
/** Resolved hex of the LOW (t=0) endpoint — the explicit low colour, or the
|
|
188
|
+
* floored neutral the single-colour fills blend up from. */
|
|
189
|
+
low: string;
|
|
190
|
+
/** Resolved hex of the HIGH (t=1) endpoint (the named ramp hue). */
|
|
191
|
+
high: string;
|
|
172
192
|
};
|
|
173
193
|
}
|
package/src/migrate/embedded.ts
CHANGED
|
@@ -12,17 +12,19 @@
|
|
|
12
12
|
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
13
13
|
|
|
14
14
|
import { parseDgmo } from '../dgmo-router';
|
|
15
|
+
import { METADATA_DIAGNOSTIC_CODES } from '../diagnostics';
|
|
15
16
|
import { migrateContent } from './index';
|
|
16
17
|
|
|
17
18
|
// Diagnostic codes the migration tool itself is repairing — these
|
|
18
19
|
// MUST NOT count as "block fails to parse" reasons, otherwise every
|
|
19
|
-
// legacy block would be skipped.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
// legacy block would be skipped. Derived from the canonical registry
|
|
21
|
+
// so a code rename can't silently desync the migrator.
|
|
22
|
+
const MIGRATION_TARGET_CODES = new Set<string>([
|
|
23
|
+
METADATA_DIAGNOSTIC_CODES.PIPE_OPERATOR_REMOVED,
|
|
24
|
+
METADATA_DIAGNOSTIC_CODES.GANTT_BARE_PERCENT_REMOVED,
|
|
25
|
+
METADATA_DIAGNOSTIC_CODES.JOURNEY_BARE_SCORE_REMOVED,
|
|
26
|
+
METADATA_DIAGNOSTIC_CODES.PYRAMID_BARE_DESCRIPTION_REMOVED,
|
|
27
|
+
METADATA_DIAGNOSTIC_CODES.RING_BARE_DESCRIPTION_REMOVED,
|
|
26
28
|
]);
|
|
27
29
|
|
|
28
30
|
const FENCE_RE = /^(\s*)(```+|~~~+)\s*(dgmo|diagrammo)\b([^\n]*)$/i;
|
package/src/mindmap/text-wrap.ts
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
// (for drawing). Ensures both agree on line breaks and font size.
|
|
8
8
|
|
|
9
9
|
import { preprocessDescriptionLine } from '../utils/description-helpers';
|
|
10
|
+
import { measureText, truncateText } from '../utils/text-measure';
|
|
10
11
|
|
|
11
|
-
const CHAR_WIDTH_RATIO = 0.58; // avg char width / fontSize for Helvetica
|
|
12
12
|
const H_PAD = 16; // 8px padding each side
|
|
13
13
|
const MAX_LABEL_LINES = 3;
|
|
14
14
|
const MAX_DESC_LINES = 2;
|
|
@@ -38,8 +38,6 @@ function tryWrap(
|
|
|
38
38
|
maxLines: number
|
|
39
39
|
): string[] | null {
|
|
40
40
|
const availWidth = maxWidth - H_PAD;
|
|
41
|
-
const charWidth = fontSize * CHAR_WIDTH_RATIO;
|
|
42
|
-
const maxChars = Math.max(1, Math.floor(availWidth / charWidth));
|
|
43
41
|
|
|
44
42
|
const lines: string[] = [];
|
|
45
43
|
let currentLine = '';
|
|
@@ -49,7 +47,7 @@ function tryWrap(
|
|
|
49
47
|
const sep = currentLine && !currentLine.endsWith('-') ? ' ' : '';
|
|
50
48
|
const candidate = currentLine + sep + token;
|
|
51
49
|
|
|
52
|
-
if (candidate
|
|
50
|
+
if (measureText(candidate, fontSize) <= availWidth) {
|
|
53
51
|
currentLine = candidate;
|
|
54
52
|
} else if (!currentLine) {
|
|
55
53
|
// Single token exceeds line — force it onto this line (will be truncated later if needed)
|
|
@@ -72,21 +70,19 @@ function tryWrap(
|
|
|
72
70
|
return lines;
|
|
73
71
|
}
|
|
74
72
|
|
|
75
|
-
/** Truncate the last line of a lines array with ellipsis to fit
|
|
73
|
+
/** Truncate the last line of a lines array with ellipsis to fit maxWidth. */
|
|
76
74
|
function truncateLastLine(
|
|
77
75
|
lines: string[],
|
|
78
76
|
maxWidth: number,
|
|
79
77
|
fontSize: number
|
|
80
78
|
): string[] {
|
|
81
79
|
const availWidth = maxWidth - H_PAD;
|
|
82
|
-
const charWidth = fontSize * CHAR_WIDTH_RATIO;
|
|
83
|
-
const maxChars = Math.max(1, Math.floor(availWidth / charWidth));
|
|
84
80
|
|
|
85
81
|
const result = [...lines];
|
|
86
82
|
// In-bounds: caller passes non-empty lines; tryWrap returns at least one line.
|
|
87
83
|
const last = result[result.length - 1]!;
|
|
88
|
-
if (last
|
|
89
|
-
result[result.length - 1] = last
|
|
84
|
+
if (measureText(last, fontSize) > availWidth) {
|
|
85
|
+
result[result.length - 1] = truncateText(last, fontSize, availWidth);
|
|
90
86
|
}
|
|
91
87
|
return result;
|
|
92
88
|
}
|
|
@@ -132,11 +128,14 @@ export function wrapText(
|
|
|
132
128
|
const last = truncated[truncated.length - 1]!;
|
|
133
129
|
if (!last.endsWith('\u2026')) {
|
|
134
130
|
const availWidth = maxWidth - H_PAD;
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (last
|
|
138
|
-
truncated[truncated.length - 1] =
|
|
139
|
-
last
|
|
131
|
+
// If appending an ellipsis would overflow, truncate to fit; otherwise
|
|
132
|
+
// just append it to signal that lines were dropped.
|
|
133
|
+
if (measureText(last + '\u2026', minFontSize) > availWidth) {
|
|
134
|
+
truncated[truncated.length - 1] = truncateText(
|
|
135
|
+
last,
|
|
136
|
+
minFontSize,
|
|
137
|
+
availWidth
|
|
138
|
+
);
|
|
140
139
|
} else {
|
|
141
140
|
truncated[truncated.length - 1] = last + '\u2026';
|
|
142
141
|
}
|
package/src/org/layout.ts
CHANGED
|
@@ -10,8 +10,18 @@ import { resolveTagColor, injectDefaultTagMetadata } from '../utils/tag-groups';
|
|
|
10
10
|
import {
|
|
11
11
|
LEGEND_PILL_FONT_SIZE,
|
|
12
12
|
LEGEND_ENTRY_FONT_SIZE,
|
|
13
|
+
LEGEND_HEIGHT,
|
|
14
|
+
LEGEND_PILL_PAD,
|
|
15
|
+
LEGEND_CAPSULE_PAD,
|
|
16
|
+
LEGEND_DOT_R,
|
|
17
|
+
LEGEND_ENTRY_DOT_GAP,
|
|
18
|
+
LEGEND_ENTRY_TRAIL,
|
|
19
|
+
LEGEND_GROUP_GAP,
|
|
20
|
+
LEGEND_EYE_SIZE,
|
|
21
|
+
LEGEND_EYE_GAP,
|
|
13
22
|
measureLegendText,
|
|
14
23
|
} from '../utils/legend-constants';
|
|
24
|
+
import { measureText } from '../utils/text-measure';
|
|
15
25
|
|
|
16
26
|
// ============================================================
|
|
17
27
|
// Types
|
|
@@ -91,7 +101,10 @@ export interface OrgLayoutResult {
|
|
|
91
101
|
// Constants
|
|
92
102
|
// ============================================================
|
|
93
103
|
|
|
94
|
-
|
|
104
|
+
// Card text font sizes — MUST match the renderer (LABEL_FONT_SIZE / META_FONT_SIZE)
|
|
105
|
+
// so node sizing measures text at the exact size it is drawn.
|
|
106
|
+
const LABEL_FONT_SIZE = 13;
|
|
107
|
+
const META_FONT_SIZE = 11;
|
|
95
108
|
const META_LINE_HEIGHT = 16;
|
|
96
109
|
const HEADER_HEIGHT = 28;
|
|
97
110
|
const SEPARATOR_GAP = 6;
|
|
@@ -108,15 +121,6 @@ const CONTAINER_META_LINE_HEIGHT = 16;
|
|
|
108
121
|
const STACK_V_GAP = 20;
|
|
109
122
|
|
|
110
123
|
// Legend (kanban-style pills)
|
|
111
|
-
const LEGEND_HEIGHT = 28;
|
|
112
|
-
const LEGEND_PILL_PAD = 16;
|
|
113
|
-
const LEGEND_CAPSULE_PAD = 4;
|
|
114
|
-
const LEGEND_DOT_R = 4;
|
|
115
|
-
const LEGEND_ENTRY_DOT_GAP = 4;
|
|
116
|
-
const LEGEND_ENTRY_TRAIL = 8;
|
|
117
|
-
const LEGEND_GROUP_GAP = 12;
|
|
118
|
-
const LEGEND_EYE_SIZE = 14;
|
|
119
|
-
const LEGEND_EYE_GAP = 6;
|
|
120
124
|
|
|
121
125
|
// ============================================================
|
|
122
126
|
// Helpers
|
|
@@ -156,17 +160,15 @@ function filterMetadata(
|
|
|
156
160
|
}
|
|
157
161
|
|
|
158
162
|
function computeCardWidth(label: string, meta: Record<string, string>): number {
|
|
159
|
-
|
|
163
|
+
// Label is drawn bold at LABEL_FONT_SIZE; meta rows at META_FONT_SIZE.
|
|
164
|
+
let maxTextWidth = measureText(label, LABEL_FONT_SIZE);
|
|
160
165
|
|
|
161
166
|
for (const [key, value] of Object.entries(meta)) {
|
|
162
|
-
const
|
|
163
|
-
if (
|
|
167
|
+
const lineWidth = measureText(`${key}: ${value}`, META_FONT_SIZE);
|
|
168
|
+
if (lineWidth > maxTextWidth) maxTextWidth = lineWidth;
|
|
164
169
|
}
|
|
165
170
|
|
|
166
|
-
return Math.max(
|
|
167
|
-
MIN_CARD_WIDTH,
|
|
168
|
-
Math.ceil(maxChars * CHAR_WIDTH) + CARD_H_PAD * 2
|
|
169
|
-
);
|
|
171
|
+
return Math.max(MIN_CARD_WIDTH, Math.ceil(maxTextWidth) + CARD_H_PAD * 2);
|
|
170
172
|
}
|
|
171
173
|
|
|
172
174
|
function computeCardHeight(meta: Record<string, string>): number {
|
package/src/org/renderer.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
EYE_CLOSED_PATH,
|
|
27
27
|
} from '../utils/legend-constants';
|
|
28
28
|
import { renderLegendD3 } from '../utils/legend-d3';
|
|
29
|
+
import { measureText } from '../utils/text-measure';
|
|
29
30
|
import { getMaxLegendReservedHeight } from '../utils/legend-layout';
|
|
30
31
|
import type { LegendConfig, LegendState } from '../utils/legend-types';
|
|
31
32
|
|
|
@@ -329,8 +330,12 @@ export function renderOrg(
|
|
|
329
330
|
const metaDisplayKeys = metaEntries.map(
|
|
330
331
|
([k]) => displayNames.get(k) ?? k
|
|
331
332
|
);
|
|
332
|
-
const
|
|
333
|
-
|
|
333
|
+
const maxKeyWidth = Math.max(
|
|
334
|
+
...metaDisplayKeys.map((k) =>
|
|
335
|
+
measureText(`${k}: `, sContainerMetaFontSize)
|
|
336
|
+
)
|
|
337
|
+
);
|
|
338
|
+
const valueX = 10 + maxKeyWidth;
|
|
334
339
|
|
|
335
340
|
const metaStartY = sContainerHeaderHeight + sContainerMetaFontSize - 2;
|
|
336
341
|
for (let i = 0; i < metaEntries.length; i++) {
|
|
@@ -529,8 +534,10 @@ export function renderOrg(
|
|
|
529
534
|
const metaDisplayKeys = metaEntries.map(
|
|
530
535
|
([k]) => displayNames.get(k) ?? k
|
|
531
536
|
);
|
|
532
|
-
const
|
|
533
|
-
|
|
537
|
+
const maxKeyWidth = Math.max(
|
|
538
|
+
...metaDisplayKeys.map((k) => measureText(`${k}: `, sMetaFontSize))
|
|
539
|
+
);
|
|
540
|
+
const valueX = 10 + maxKeyWidth;
|
|
534
541
|
|
|
535
542
|
const metaStartY = sHeaderHeight + sSeparatorGap + sMetaFontSize;
|
|
536
543
|
for (let i = 0; i < metaEntries.length; i++) {
|