@diagrammo/dgmo 0.47.0 → 0.48.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.js +11 -11
- package/dist/auto.js +16 -16
- package/dist/auto.mjs +41 -12
- package/dist/block.js +4 -4
- package/dist/{chunk-FNB3PNGF.js → chunk-7DTWEFUE.js} +3 -3
- package/dist/{chunk-2DXMZRWJ.js → chunk-B65G5DNL.js} +8 -1
- package/dist/{chunk-RZCF2VVM.js → chunk-B7JUCQUA.js} +3 -1
- package/dist/{chunk-DU7W62N5.js → chunk-FANX6UJR.js} +3 -3
- package/dist/{chunk-6FZYCKXB.js → chunk-HSH73DP6.js} +2 -2
- package/dist/{chunk-P4RH4OSP.js → chunk-ILKORZ3H.js} +2 -2
- package/dist/{chunk-Y5QGVTVC.js → chunk-JTSMEYIZ.js} +7 -3
- package/dist/{chunk-RCERWOJR.js → chunk-ODZVHU3R.js} +1 -2
- package/dist/{chunk-DZAQBW35.js → chunk-SGZYXELT.js} +3 -7
- package/dist/{chunk-CUVHQHZ7.js → chunk-THXCFRTU.js} +3 -3
- package/dist/{chunk-IH6ZMVQ5.js → chunk-UC7TFHUM.js} +29 -11
- package/dist/{chunk-EMS2SK26.js → chunk-ZBWMSD5N.js} +3 -1
- package/dist/cli.cjs +15 -15
- package/dist/{dgmo-router-DZCIDUO5.js → dgmo-router-X35VLZLM.js} +3 -3
- package/dist/element.js +16 -16
- package/dist/element.mjs +41 -12
- package/dist/{flowchart-renderer-ZAEOL4DX.js → flowchart-renderer-SNDSII4D.js} +2 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -4
- package/dist/{parser-FUSKFGZN.js → parser-2EWECDSA.js} +2 -2
- package/dist/{renderer-UWA5XS74.js → renderer-4JHRUBE6.js} +3 -3
- package/dist/{renderer-WZRREQ2P.js → renderer-5MYBMBYO.js} +2 -2
- package/dist/{renderer-TOCOVLHS.js → renderer-B2OMQIQG.js} +3 -3
- package/dist/{renderer-2XQQQ6QR.js → renderer-N43WR5GV.js} +3 -3
- package/dist/{renderer-T7YKC5VP.js → renderer-ZRNG54CW.js} +3 -3
- package/dist/{state-renderer-RFYHSP4K.js → state-renderer-B4DQBM6M.js} +2 -2
- package/package.json +23 -71
- package/dist/advanced.cjs +0 -71648
- package/dist/advanced.d.cts +0 -5719
- package/dist/auto.cjs +0 -70648
- package/dist/auto.d.cts +0 -54
- package/dist/block.cjs +0 -70208
- package/dist/block.d.cts +0 -115
- package/dist/editor.cjs +0 -526
- package/dist/editor.d.cts +0 -26
- package/dist/element.cjs +0 -70530
- package/dist/element.d.cts +0 -62
- package/dist/highlight.cjs +0 -852
- package/dist/highlight.d.cts +0 -49
- package/dist/index.cjs +0 -70491
- package/dist/index.d.cts +0 -160
- package/dist/pert.cjs +0 -325
- package/dist/pert.d.cts +0 -559
- package/dist/themes-1-CuKpeH.d.cts +0 -908
- package/src/advanced.ts +0 -873
- package/src/arc/parser.ts +0 -15
- package/src/arc/renderer.ts +0 -633
- package/src/auto/index.ts +0 -621
- package/src/auto/shared.ts +0 -370
- package/src/auto/styles.ts +0 -73
- package/src/block/layout.ts +0 -198
- package/src/block/parser.ts +0 -427
- package/src/block/renderer.ts +0 -484
- package/src/block/types.ts +0 -67
- package/src/boxes-and-lines/collapse.ts +0 -96
- package/src/boxes-and-lines/focus.ts +0 -257
- package/src/boxes-and-lines/label-placement.ts +0 -339
- package/src/boxes-and-lines/layout-grouped.ts +0 -912
- package/src/boxes-and-lines/layout-layered.ts +0 -722
- package/src/boxes-and-lines/layout-search.ts +0 -1678
- package/src/boxes-and-lines/layout.ts +0 -398
- package/src/boxes-and-lines/parser.ts +0 -1181
- package/src/boxes-and-lines/renderer.ts +0 -1465
- package/src/boxes-and-lines/types.ts +0 -66
- package/src/c4/layout.ts +0 -2441
- package/src/c4/parser.ts +0 -1158
- package/src/c4/renderer.ts +0 -2008
- package/src/c4/types.ts +0 -89
- package/src/chart-type-registry.ts +0 -529
- package/src/chart-types.ts +0 -225
- package/src/chart.ts +0 -685
- package/src/charts-d3/bar.ts +0 -338
- package/src/charts-d3/chord.ts +0 -140
- package/src/charts-d3/function.ts +0 -147
- package/src/charts-d3/funnel.ts +0 -104
- package/src/charts-d3/heatmap.ts +0 -145
- package/src/charts-d3/index.ts +0 -337
- package/src/charts-d3/interactions.ts +0 -477
- package/src/charts-d3/line.ts +0 -344
- package/src/charts-d3/mount.ts +0 -151
- package/src/charts-d3/pie.ts +0 -117
- package/src/charts-d3/polar.ts +0 -89
- package/src/charts-d3/radar.ts +0 -0
- package/src/charts-d3/sankey.ts +0 -0
- package/src/charts-d3/scatter.ts +0 -185
- package/src/charts-d3/shared.ts +0 -208
- package/src/class/layout.ts +0 -326
- package/src/class/parser.ts +0 -635
- package/src/class/renderer.ts +0 -793
- package/src/class/types.ts +0 -58
- package/src/cli-banner.ts +0 -106
- package/src/cli.ts +0 -1080
- package/src/colors-diagnostics.ts +0 -35
- package/src/colors.ts +0 -379
- package/src/completion-types.ts +0 -27
- package/src/completion.ts +0 -2668
- package/src/cycle/layout.ts +0 -1026
- package/src/cycle/parser.ts +0 -387
- package/src/cycle/renderer.ts +0 -653
- package/src/cycle/types.ts +0 -101
- package/src/d3.ts +0 -1434
- package/src/data-chart-parser.ts +0 -1007
- package/src/dgmo-router.ts +0 -296
- package/src/diagnostics-registry.ts +0 -108
- package/src/diagnostics.ts +0 -0
- package/src/dimensions.ts +0 -20
- package/src/directives-registry.ts +0 -396
- package/src/editor/dgmo.grammar +0 -76
- package/src/editor/dgmo.grammar.d.ts +0 -2
- package/src/editor/dgmo.grammar.js +0 -18
- package/src/editor/dgmo.grammar.terms.d.ts +0 -5
- package/src/editor/dgmo.grammar.terms.js +0 -35
- package/src/editor/highlight-api.ts +0 -596
- package/src/editor/highlight.ts +0 -36
- package/src/editor/index.ts +0 -27
- package/src/editor/keywords.ts +0 -128
- package/src/editor/tokens.ts +0 -30
- package/src/element/index.ts +0 -432
- package/src/embed/css.ts +0 -312
- package/src/embed/escape.ts +0 -15
- package/src/embed/index.ts +0 -280
- package/src/er/classify.ts +0 -228
- package/src/er/layout.ts +0 -530
- package/src/er/parser.ts +0 -750
- package/src/er/renderer.ts +0 -725
- package/src/er/types.ts +0 -52
- package/src/error-card.ts +0 -269
- package/src/event-line/diagnostics.ts +0 -117
- package/src/event-line/parser.ts +0 -643
- package/src/event-line/renderer.ts +0 -1742
- package/src/event-line/types.ts +0 -74
- package/src/fonts.ts +0 -3
- package/src/gantt/calculator.ts +0 -979
- package/src/gantt/parser.ts +0 -1641
- package/src/gantt/renderer.ts +0 -3787
- package/src/gantt/resolver.ts +0 -172
- package/src/gantt/types.ts +0 -206
- package/src/graph/diagnostics.ts +0 -64
- package/src/graph/edge-spline.ts +0 -29
- package/src/graph/flowchart-parser.ts +0 -759
- package/src/graph/flowchart-renderer.ts +0 -817
- package/src/graph/layout.ts +0 -472
- package/src/graph/notes.ts +0 -21
- package/src/graph/state-collapse.ts +0 -78
- package/src/graph/state-parser.ts +0 -574
- package/src/graph/state-renderer.ts +0 -653
- package/src/graph/types.ts +0 -65
- package/src/index.ts +0 -261
- package/src/infra/compute.ts +0 -1385
- package/src/infra/layout.ts +0 -888
- package/src/infra/parser.ts +0 -1234
- package/src/infra/renderer.ts +0 -2534
- package/src/infra/roles.ts +0 -75
- package/src/infra/types.ts +0 -216
- package/src/infra/validation.ts +0 -201
- package/src/journey-map/layout.ts +0 -490
- package/src/journey-map/parser.ts +0 -557
- package/src/journey-map/renderer.ts +0 -1580
- package/src/journey-map/types.ts +0 -47
- package/src/kanban/mutations.ts +0 -175
- package/src/kanban/parser.ts +0 -509
- package/src/kanban/renderer.ts +0 -1449
- package/src/kanban/types.ts +0 -39
- package/src/label-layout.ts +0 -368
- package/src/map/colorize.ts +0 -54
- package/src/map/completion.ts +0 -316
- package/src/map/context-labels.ts +0 -609
- package/src/map/data/PROVENANCE.json +0 -1
- package/src/map/data/README.md +0 -40
- package/src/map/data/airport-collisions.json +0 -1
- package/src/map/data/airports.json +0 -1
- package/src/map/data/gazetteer.json +0 -1
- package/src/map/data/lakes.json +0 -1
- package/src/map/data/mountain-ranges.json +0 -1
- package/src/map/data/na-lakes.json +0 -1
- package/src/map/data/na-land.json +0 -1
- package/src/map/data/region-names.json +0 -1
- package/src/map/data/rivers.json +0 -1
- package/src/map/data/types.ts +0 -121
- package/src/map/data/us-states.json +0 -1
- package/src/map/data/water-bodies.json +0 -1
- package/src/map/data/world-coarse.json +0 -1
- package/src/map/data/world-detail.json +0 -1
- package/src/map/diagnostics.ts +0 -134
- package/src/map/dimensions.ts +0 -150
- package/src/map/geo-query.ts +0 -295
- package/src/map/geo.ts +0 -438
- package/src/map/invert.ts +0 -111
- package/src/map/layout.ts +0 -5069
- package/src/map/legend-band.ts +0 -127
- package/src/map/load-data.ts +0 -186
- package/src/map/parser.ts +0 -783
- package/src/map/renderer.ts +0 -1445
- package/src/map/resolved-types.ts +0 -160
- package/src/map/resolver.ts +0 -1146
- package/src/map/types.ts +0 -194
- package/src/mindmap/collapse.ts +0 -67
- package/src/mindmap/layout.ts +0 -649
- package/src/mindmap/parser.ts +0 -430
- package/src/mindmap/renderer.ts +0 -637
- package/src/mindmap/text-wrap.ts +0 -218
- package/src/mindmap/types.ts +0 -56
- package/src/org/collapse.ts +0 -151
- package/src/org/layout.ts +0 -1262
- package/src/org/parser.ts +0 -519
- package/src/org/renderer.ts +0 -866
- package/src/org/resolver.ts +0 -636
- package/src/palettes/atlas.ts +0 -77
- package/src/palettes/blueprint.ts +0 -73
- package/src/palettes/catppuccin.ts +0 -84
- package/src/palettes/color-utils.ts +0 -443
- package/src/palettes/index.ts +0 -67
- package/src/palettes/nord.ts +0 -67
- package/src/palettes/registry.ts +0 -114
- package/src/palettes/slate.ts +0 -73
- package/src/palettes/tidewater.ts +0 -73
- package/src/palettes/tokyo-night.ts +0 -86
- package/src/palettes/types.ts +0 -88
- package/src/pert/analyzer.ts +0 -1183
- package/src/pert/diagnostics.ts +0 -110
- package/src/pert/index.ts +0 -43
- package/src/pert/internal.ts +0 -252
- package/src/pert/layout.ts +0 -971
- package/src/pert/monte-carlo.ts +0 -446
- package/src/pert/parser.ts +0 -1606
- package/src/pert/renderer.ts +0 -4009
- package/src/pert/share-normalize.ts +0 -62
- package/src/pert/types.ts +0 -604
- package/src/pyramid/parser.ts +0 -185
- package/src/pyramid/renderer.ts +0 -768
- package/src/pyramid/types.ts +0 -28
- package/src/quadrant/parser.ts +0 -18
- package/src/quadrant/renderer.ts +0 -638
- package/src/raci/diagnostics.ts +0 -213
- package/src/raci/index.ts +0 -23
- package/src/raci/mutations.ts +0 -321
- package/src/raci/parser.ts +0 -699
- package/src/raci/renderer.ts +0 -1627
- package/src/raci/types.ts +0 -82
- package/src/raci/variants.ts +0 -320
- package/src/render.ts +0 -268
- package/src/ring/parser.ts +0 -212
- package/src/ring/renderer.ts +0 -490
- package/src/ring/types.ts +0 -27
- package/src/sequence/collapse.ts +0 -174
- package/src/sequence/parser.ts +0 -1596
- package/src/sequence/participant-inference.ts +0 -152
- package/src/sequence/renderer.ts +0 -3168
- package/src/sequence/tag-resolution.ts +0 -164
- package/src/sharing.ts +0 -211
- package/src/sitemap/collapse.ts +0 -100
- package/src/sitemap/layout.ts +0 -925
- package/src/sitemap/parser.ts +0 -755
- package/src/sitemap/renderer.ts +0 -828
- package/src/sitemap/types.ts +0 -42
- package/src/slope/parser.ts +0 -18
- package/src/slope/renderer.ts +0 -378
- package/src/swimlane/diagnostics.ts +0 -94
- package/src/swimlane/layout.ts +0 -597
- package/src/swimlane/parser.ts +0 -613
- package/src/swimlane/renderer.ts +0 -681
- package/src/swimlane/types.ts +0 -151
- package/src/tech-radar/interactive.ts +0 -1129
- package/src/tech-radar/layout.ts +0 -197
- package/src/tech-radar/parser.ts +0 -436
- package/src/tech-radar/renderer.ts +0 -1239
- package/src/tech-radar/shared.ts +0 -151
- package/src/tech-radar/types.ts +0 -86
- package/src/themes.ts +0 -22
- package/src/timeline/parser.ts +0 -472
- package/src/timeline/renderer.ts +0 -3024
- package/src/timeline/types.ts +0 -33
- package/src/timeline/viz-parser.ts +0 -21
- package/src/treemap/diagnostics.ts +0 -62
- package/src/treemap/layout-radial.ts +0 -233
- package/src/treemap/layout.ts +0 -189
- package/src/treemap/parser.ts +0 -463
- package/src/treemap/renderer-radial.ts +0 -438
- package/src/treemap/renderer.ts +0 -572
- package/src/treemap/treemap-shared.ts +0 -309
- package/src/treemap/types.ts +0 -60
- package/src/utils/arrow-markers.ts +0 -51
- package/src/utils/arrows-diagnostics.ts +0 -62
- package/src/utils/arrows.ts +0 -227
- package/src/utils/brand.ts +0 -61
- package/src/utils/card.ts +0 -183
- package/src/utils/collapse-engine/tree-edges.ts +0 -115
- package/src/utils/collapse-engine/tree.ts +0 -117
- package/src/utils/d3-helpers.ts +0 -219
- package/src/utils/d3-types.ts +0 -9
- package/src/utils/description-helpers.ts +0 -38
- package/src/utils/duration.ts +0 -312
- package/src/utils/export-container.ts +0 -41
- package/src/utils/expr-eval.ts +0 -27
- package/src/utils/fit-canvas.ts +0 -64
- package/src/utils/hover-styles.ts +0 -673
- package/src/utils/inline-markdown.ts +0 -96
- package/src/utils/legend-constants.ts +0 -48
- package/src/utils/legend-d3.ts +0 -618
- package/src/utils/legend-integration.ts +0 -107
- package/src/utils/legend-layout.ts +0 -776
- package/src/utils/legend-svg.ts +0 -214
- package/src/utils/legend-types.ts +0 -272
- package/src/utils/name-normalize.ts +0 -163
- package/src/utils/note-box/constants.ts +0 -25
- package/src/utils/note-box/index.ts +0 -11
- package/src/utils/note-box/metrics.ts +0 -90
- package/src/utils/note-box/svg.ts +0 -331
- package/src/utils/notes/bounds.ts +0 -30
- package/src/utils/notes/build.ts +0 -131
- package/src/utils/notes/index.ts +0 -18
- package/src/utils/notes/model.ts +0 -19
- package/src/utils/notes/parse.ts +0 -131
- package/src/utils/notes/place.ts +0 -177
- package/src/utils/notes/resolve.ts +0 -88
- package/src/utils/number-format.ts +0 -36
- package/src/utils/parsing.ts +0 -953
- package/src/utils/reserved-key-registry.ts +0 -239
- package/src/utils/safe-href.ts +0 -38
- package/src/utils/scaling.ts +0 -97
- package/src/utils/svg-embed.ts +0 -251
- package/src/utils/tag-groups.ts +0 -607
- package/src/utils/text-measure.ts +0 -122
- package/src/utils/time-ticks.ts +0 -253
- package/src/utils/title-constants.ts +0 -24
- package/src/utils/visual-conventions.ts +0 -61
- package/src/utils/wrapped-desc.ts +0 -87
- package/src/venn/parser.ts +0 -18
- package/src/venn/renderer.ts +0 -1047
- package/src/version-control/diagnostics.ts +0 -67
- package/src/version-control/parser.ts +0 -544
- package/src/version-control/renderer.ts +0 -724
- package/src/version-control/types.ts +0 -100
- package/src/visualizations/parse.ts +0 -1560
- package/src/visualizations/types.ts +0 -262
- package/src/wireframe/layout.ts +0 -479
- package/src/wireframe/parser.ts +0 -1114
- package/src/wireframe/renderer.ts +0 -1318
- package/src/wireframe/types.ts +0 -110
- package/src/wordcloud/parser.ts +0 -18
- package/src/wordcloud/renderer.ts +0 -357
package/src/infra/layout.ts
DELETED
|
@@ -1,888 +0,0 @@
|
|
|
1
|
-
// ============================================================
|
|
2
|
-
// Infra Chart Layout Engine
|
|
3
|
-
// ============================================================
|
|
4
|
-
//
|
|
5
|
-
// Uses dagre for LR/TB DAG layout. Groups are implemented as
|
|
6
|
-
// post-layout bounding box wrappers around their children.
|
|
7
|
-
|
|
8
|
-
import dagre from '@dagrejs/dagre';
|
|
9
|
-
import type { Writable } from '../utils/brand';
|
|
10
|
-
import type { ComputedInfraModel, ComputedInfraNode } from './types';
|
|
11
|
-
import {
|
|
12
|
-
measureText,
|
|
13
|
-
truncateText,
|
|
14
|
-
CHAR_WIDTH_RATIO,
|
|
15
|
-
} from '../utils/text-measure';
|
|
16
|
-
|
|
17
|
-
// ============================================================
|
|
18
|
-
// Layout types
|
|
19
|
-
// ============================================================
|
|
20
|
-
|
|
21
|
-
export interface InfraLayoutNode {
|
|
22
|
-
readonly id: string;
|
|
23
|
-
readonly label: string;
|
|
24
|
-
readonly x: number;
|
|
25
|
-
readonly y: number;
|
|
26
|
-
readonly width: number;
|
|
27
|
-
readonly height: number;
|
|
28
|
-
readonly computedRps: number;
|
|
29
|
-
readonly overloaded: boolean;
|
|
30
|
-
readonly rateLimited: boolean;
|
|
31
|
-
readonly isEdge: boolean;
|
|
32
|
-
readonly groupId: string | null;
|
|
33
|
-
readonly computedLatencyMs: number;
|
|
34
|
-
readonly computedLatencyPercentiles: ComputedInfraNode['computedLatencyPercentiles'];
|
|
35
|
-
readonly computedUptime: number;
|
|
36
|
-
readonly computedAvailability: number;
|
|
37
|
-
readonly computedAvailabilityPercentiles: ComputedInfraNode['computedAvailabilityPercentiles'];
|
|
38
|
-
readonly computedInstances: number;
|
|
39
|
-
readonly computedConcurrentInvocations: number;
|
|
40
|
-
readonly computedCbState: ComputedInfraNode['computedCbState'];
|
|
41
|
-
readonly childHealthState?: ComputedInfraNode['childHealthState'];
|
|
42
|
-
readonly properties: ComputedInfraNode['properties'];
|
|
43
|
-
readonly queueMetrics?: ComputedInfraNode['queueMetrics'];
|
|
44
|
-
readonly tags: Readonly<Record<string, string>>;
|
|
45
|
-
readonly description?: readonly string[];
|
|
46
|
-
readonly lineNumber: number;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface InfraLayoutEdge {
|
|
50
|
-
readonly sourceId: string;
|
|
51
|
-
readonly targetId: string;
|
|
52
|
-
readonly label: string;
|
|
53
|
-
readonly async: boolean;
|
|
54
|
-
readonly computedRps: number;
|
|
55
|
-
readonly split: number;
|
|
56
|
-
readonly fanout: number | null;
|
|
57
|
-
readonly points: ReadonlyArray<{ readonly x: number; readonly y: number }>;
|
|
58
|
-
readonly lineNumber: number;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export interface InfraLayoutGroup {
|
|
62
|
-
readonly id: string;
|
|
63
|
-
readonly label: string;
|
|
64
|
-
readonly x: number;
|
|
65
|
-
readonly y: number;
|
|
66
|
-
readonly width: number;
|
|
67
|
-
readonly height: number;
|
|
68
|
-
readonly instances?: number | string;
|
|
69
|
-
readonly lineNumber: number;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export interface InfraLayoutResult {
|
|
73
|
-
readonly nodes: readonly InfraLayoutNode[];
|
|
74
|
-
readonly edges: readonly InfraLayoutEdge[];
|
|
75
|
-
readonly groups: readonly InfraLayoutGroup[];
|
|
76
|
-
/** Diagram-level options (e.g., default-latency-ms, default-uptime). */
|
|
77
|
-
readonly options: Readonly<Record<string, string>>;
|
|
78
|
-
readonly direction: 'LR' | 'TB';
|
|
79
|
-
readonly width: number;
|
|
80
|
-
readonly height: number;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// ============================================================
|
|
84
|
-
// Sizing constants
|
|
85
|
-
// ============================================================
|
|
86
|
-
|
|
87
|
-
const MIN_NODE_WIDTH = 140;
|
|
88
|
-
const NODE_HEADER_HEIGHT = 28;
|
|
89
|
-
const META_LINE_HEIGHT = 14;
|
|
90
|
-
const NODE_SEPARATOR_GAP = 4;
|
|
91
|
-
const NODE_PAD_BOTTOM = 10;
|
|
92
|
-
const ROLE_DOT_ROW = 12;
|
|
93
|
-
const COLLAPSE_BAR_HEIGHT = 6;
|
|
94
|
-
// Font sizes used when sizing nodes from text — must match renderer.ts
|
|
95
|
-
// (NODE_FONT_SIZE / META_FONT_SIZE) so measured widths agree.
|
|
96
|
-
const NODE_FONT_SIZE = 13;
|
|
97
|
-
const META_FONT_SIZE = 10;
|
|
98
|
-
const EDGE_LABEL_FONT_SIZE = 11;
|
|
99
|
-
const PADDING_X = 24;
|
|
100
|
-
const GROUP_PADDING = 20;
|
|
101
|
-
const GROUP_HEADER_HEIGHT = 24;
|
|
102
|
-
const EDGE_MARGIN = 60;
|
|
103
|
-
|
|
104
|
-
// ============================================================
|
|
105
|
-
// Node sizing
|
|
106
|
-
// ============================================================
|
|
107
|
-
|
|
108
|
-
/** Display property keys shown as key: value rows. */
|
|
109
|
-
const DISPLAY_KEYS = new Set([
|
|
110
|
-
'cache-hit',
|
|
111
|
-
'firewall-block',
|
|
112
|
-
'ratelimit-rps',
|
|
113
|
-
'latency-ms',
|
|
114
|
-
'uptime',
|
|
115
|
-
'instances',
|
|
116
|
-
'max-rps',
|
|
117
|
-
'cb-error-threshold',
|
|
118
|
-
'cb-latency-threshold-ms',
|
|
119
|
-
'concurrency',
|
|
120
|
-
'duration-ms',
|
|
121
|
-
'cold-start-ms',
|
|
122
|
-
'buffer',
|
|
123
|
-
'drain-rate',
|
|
124
|
-
'retention-hours',
|
|
125
|
-
'partitions',
|
|
126
|
-
]);
|
|
127
|
-
|
|
128
|
-
/** Display names for width estimation. */
|
|
129
|
-
const DISPLAY_NAMES: Record<string, string> = {
|
|
130
|
-
'cache-hit': 'cache hit',
|
|
131
|
-
'firewall-block': 'firewall block',
|
|
132
|
-
'ratelimit-rps': 'rate limit RPS',
|
|
133
|
-
'latency-ms': 'latency',
|
|
134
|
-
uptime: 'uptime',
|
|
135
|
-
instances: 'instances',
|
|
136
|
-
'max-rps': 'max RPS',
|
|
137
|
-
'cb-error-threshold': 'CB error threshold',
|
|
138
|
-
'cb-latency-threshold-ms': 'CB latency threshold',
|
|
139
|
-
concurrency: 'concurrency',
|
|
140
|
-
'duration-ms': 'duration',
|
|
141
|
-
'cold-start-ms': 'cold start',
|
|
142
|
-
buffer: 'buffer',
|
|
143
|
-
'drain-rate': 'drain rate',
|
|
144
|
-
'retention-hours': 'retention',
|
|
145
|
-
partitions: 'partitions',
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
function countDisplayProps(
|
|
149
|
-
node: ComputedInfraNode,
|
|
150
|
-
expanded: boolean,
|
|
151
|
-
options?: Record<string, string>
|
|
152
|
-
): number {
|
|
153
|
-
// Declared properties are only shown when the node is selected (expanded)
|
|
154
|
-
if (!expanded) return 0;
|
|
155
|
-
let count = node.properties.filter((p) => DISPLAY_KEYS.has(p.key)).length;
|
|
156
|
-
// Count diagram-level default rows for properties the node doesn't explicitly declare
|
|
157
|
-
if (options) {
|
|
158
|
-
const hasLatency = node.properties.some((p) => p.key === 'latency-ms');
|
|
159
|
-
const hasUptime = node.properties.some((p) => p.key === 'uptime');
|
|
160
|
-
const isServerless = node.properties.some((p) => p.key === 'concurrency');
|
|
161
|
-
const defaultLatency = parseFloat(options['default-latency-ms'] ?? '') || 0;
|
|
162
|
-
const defaultUptime = parseFloat(options['default-uptime'] ?? '') || 0;
|
|
163
|
-
if (!hasLatency && !isServerless && defaultLatency > 0) count++;
|
|
164
|
-
if (!hasUptime && defaultUptime > 0 && defaultUptime < 100) count++;
|
|
165
|
-
}
|
|
166
|
-
return count;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/** Count computed rows shown below declared props. When expanded, shows p50/p90/p99; otherwise just p90. */
|
|
170
|
-
function countComputedRows(node: ComputedInfraNode, expanded: boolean): number {
|
|
171
|
-
let count = 0;
|
|
172
|
-
// Serverless instances row
|
|
173
|
-
if (node.computedConcurrentInvocations > 0) count += 1;
|
|
174
|
-
const p = node.computedLatencyPercentiles;
|
|
175
|
-
if (p.p50 > 0 || p.p90 > 0 || p.p99 > 0) count += expanded ? 3 : 1; // all percentiles or just p90
|
|
176
|
-
if (node.computedUptime < 1) {
|
|
177
|
-
const declaredUptime = node.properties.find((p) => p.key === 'uptime');
|
|
178
|
-
const declaredVal = declaredUptime ? Number(declaredUptime.value) / 100 : 1;
|
|
179
|
-
const differs = Math.abs(node.computedUptime - declaredVal) > 0.000001;
|
|
180
|
-
if (differs || node.isEdge) count += 1;
|
|
181
|
-
}
|
|
182
|
-
if (node.computedAvailability < 1) count += 1;
|
|
183
|
-
// CB state row when circuit breaker is open
|
|
184
|
-
if (node.computedCbState === 'open') count += 1;
|
|
185
|
-
// Queue computed rows: lag + overflow
|
|
186
|
-
if (node.queueMetrics) {
|
|
187
|
-
if (node.queueMetrics.fillRate > 0) count += 1; // lag row
|
|
188
|
-
if (
|
|
189
|
-
node.queueMetrics.fillRate > 0 &&
|
|
190
|
-
node.queueMetrics.timeToOverflow < Infinity
|
|
191
|
-
)
|
|
192
|
-
count += 1; // overflow row
|
|
193
|
-
}
|
|
194
|
-
return count;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function hasRoles(node: ComputedInfraNode): boolean {
|
|
198
|
-
if (node.isEdge) return false;
|
|
199
|
-
return node.properties.some((p) => DISPLAY_KEYS.has(p.key));
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function computeNodeWidth(
|
|
203
|
-
node: ComputedInfraNode,
|
|
204
|
-
expanded: boolean,
|
|
205
|
-
options?: Record<string, string>
|
|
206
|
-
): number {
|
|
207
|
-
// Account for badge text (e.g., "3x") in header width — serverless nodes no longer show a badge
|
|
208
|
-
const badgeVal =
|
|
209
|
-
node.computedConcurrentInvocations === 0 && node.computedInstances > 1
|
|
210
|
-
? node.computedInstances
|
|
211
|
-
: 0;
|
|
212
|
-
// Badge ("3x") renders at META_FONT_SIZE at the header's right edge; reserve
|
|
213
|
-
// its width plus a small gap alongside the bold label at NODE_FONT_SIZE.
|
|
214
|
-
const badgeWidth =
|
|
215
|
-
badgeVal > 0
|
|
216
|
-
? measureText(`${badgeVal}x`, META_FONT_SIZE) +
|
|
217
|
-
2 * CHAR_WIDTH_RATIO * NODE_FONT_SIZE
|
|
218
|
-
: 0;
|
|
219
|
-
const labelWidth =
|
|
220
|
-
measureText(node.label, NODE_FONT_SIZE) + badgeWidth + PADDING_X;
|
|
221
|
-
|
|
222
|
-
// Collect all key names (including "RPS" and computed rows) to compute aligned value column
|
|
223
|
-
const allKeys: string[] = [];
|
|
224
|
-
if (node.computedRps > 0) allKeys.push('RPS');
|
|
225
|
-
// Declared property keys only included when expanded
|
|
226
|
-
if (expanded) {
|
|
227
|
-
for (const p of node.properties) {
|
|
228
|
-
const dk = DISPLAY_NAMES[p.key];
|
|
229
|
-
if (dk) allKeys.push(dk);
|
|
230
|
-
}
|
|
231
|
-
// Default property keys
|
|
232
|
-
if (options) {
|
|
233
|
-
const hasLatency = node.properties.some((p) => p.key === 'latency-ms');
|
|
234
|
-
const hasUptime = node.properties.some((p) => p.key === 'uptime');
|
|
235
|
-
const isServerless = node.properties.some((p) => p.key === 'concurrency');
|
|
236
|
-
if (
|
|
237
|
-
!hasLatency &&
|
|
238
|
-
!isServerless &&
|
|
239
|
-
(parseFloat(options['default-latency-ms'] ?? '') || 0) > 0
|
|
240
|
-
)
|
|
241
|
-
allKeys.push('latency');
|
|
242
|
-
if (
|
|
243
|
-
!hasUptime &&
|
|
244
|
-
(parseFloat(options['default-uptime'] ?? '') || 0) > 0 &&
|
|
245
|
-
parseFloat(options['default-uptime'] ?? '') < 100
|
|
246
|
-
)
|
|
247
|
-
allKeys.push('uptime');
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
// Computed rows
|
|
251
|
-
const computedRows = countComputedRows(node, expanded);
|
|
252
|
-
if (computedRows > 0) {
|
|
253
|
-
if (node.computedConcurrentInvocations > 0) allKeys.push('instances');
|
|
254
|
-
const perc = node.computedLatencyPercentiles;
|
|
255
|
-
if (perc.p50 > 0 || perc.p90 > 0 || perc.p99 > 0) {
|
|
256
|
-
if (expanded) {
|
|
257
|
-
allKeys.push('p50', 'p90', 'p99');
|
|
258
|
-
} else {
|
|
259
|
-
allKeys.push('p90');
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
if (node.computedUptime < 1) {
|
|
263
|
-
const declaredUptime = node.properties.find((p) => p.key === 'uptime');
|
|
264
|
-
const declaredVal = declaredUptime
|
|
265
|
-
? Number(declaredUptime.value) / 100
|
|
266
|
-
: 1;
|
|
267
|
-
if (
|
|
268
|
-
Math.abs(node.computedUptime - declaredVal) > 0.000001 ||
|
|
269
|
-
node.isEdge
|
|
270
|
-
) {
|
|
271
|
-
allKeys.push('eff. uptime');
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
if (node.computedAvailability < 1) allKeys.push('availability');
|
|
275
|
-
if (node.computedCbState === 'open') allKeys.push('CB');
|
|
276
|
-
if (node.queueMetrics) {
|
|
277
|
-
if (node.queueMetrics.fillRate > 0) allKeys.push('lag');
|
|
278
|
-
if (
|
|
279
|
-
node.queueMetrics.fillRate > 0 &&
|
|
280
|
-
node.queueMetrics.timeToOverflow < Infinity
|
|
281
|
-
)
|
|
282
|
-
allKeys.push('overflow');
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
if (allKeys.length === 0) return Math.max(MIN_NODE_WIDTH, labelWidth);
|
|
286
|
-
|
|
287
|
-
// Width of the aligned key column ("key: "), measured in pixels at the meta
|
|
288
|
-
// font size — mirrors renderer.ts's valueX = x + 10 + measureText("key: ").
|
|
289
|
-
const keyColWidth = Math.max(
|
|
290
|
-
...allKeys.map((k) => measureText(`${k}: `, META_FONT_SIZE))
|
|
291
|
-
);
|
|
292
|
-
// keyColWidth + measured value width
|
|
293
|
-
let maxRowWidth = 0;
|
|
294
|
-
if (node.computedRps > 0) {
|
|
295
|
-
// RPS row may show "29.3k / 50k" when an effective cap exists
|
|
296
|
-
const nodeMaxRps = getNumProp(node, 'max-rps', 0);
|
|
297
|
-
const nodeRateLimit = getNumProp(node, 'ratelimit-rps', 0);
|
|
298
|
-
const nodeConcurrency = getNumProp(node, 'concurrency', 0);
|
|
299
|
-
const nodeDurationMs = getNumProp(node, 'duration-ms', 100);
|
|
300
|
-
const serverlessCap =
|
|
301
|
-
nodeConcurrency > 0 ? nodeConcurrency / (nodeDurationMs / 1000) : 0;
|
|
302
|
-
const effectiveCap =
|
|
303
|
-
serverlessCap > 0
|
|
304
|
-
? serverlessCap
|
|
305
|
-
: nodeMaxRps > 0 && nodeRateLimit > 0
|
|
306
|
-
? Math.min(nodeMaxRps * node.computedInstances, nodeRateLimit)
|
|
307
|
-
: nodeMaxRps > 0
|
|
308
|
-
? nodeMaxRps * node.computedInstances
|
|
309
|
-
: nodeRateLimit > 0
|
|
310
|
-
? nodeRateLimit
|
|
311
|
-
: 0;
|
|
312
|
-
const rpsVal =
|
|
313
|
-
effectiveCap > 0 && !node.isEdge
|
|
314
|
-
? `${formatRpsShort(node.computedRps)} / ${formatRpsShort(effectiveCap)}`
|
|
315
|
-
: formatRps(node.computedRps);
|
|
316
|
-
maxRowWidth = Math.max(
|
|
317
|
-
maxRowWidth,
|
|
318
|
-
keyColWidth + measureText(rpsVal, META_FONT_SIZE)
|
|
319
|
-
);
|
|
320
|
-
}
|
|
321
|
-
// Declared property value widths only when expanded
|
|
322
|
-
if (expanded) {
|
|
323
|
-
for (const p of node.properties) {
|
|
324
|
-
const dk = DISPLAY_NAMES[p.key];
|
|
325
|
-
if (!dk) continue;
|
|
326
|
-
const numVal =
|
|
327
|
-
typeof p.value === 'number'
|
|
328
|
-
? p.value
|
|
329
|
-
: parseFloat(String(p.value)) || 0;
|
|
330
|
-
const PCT_KEYS = [
|
|
331
|
-
'cache-hit',
|
|
332
|
-
'firewall-block',
|
|
333
|
-
'uptime',
|
|
334
|
-
'cb-error-threshold',
|
|
335
|
-
];
|
|
336
|
-
const valStr =
|
|
337
|
-
p.key === 'max-rps' || p.key === 'ratelimit-rps'
|
|
338
|
-
? formatRpsShort(numVal)
|
|
339
|
-
: p.key === 'latency-ms' ||
|
|
340
|
-
p.key === 'cb-latency-threshold-ms' ||
|
|
341
|
-
p.key === 'duration-ms' ||
|
|
342
|
-
p.key === 'cold-start-ms'
|
|
343
|
-
? formatMs(numVal)
|
|
344
|
-
: PCT_KEYS.includes(p.key)
|
|
345
|
-
? `${numVal}%`
|
|
346
|
-
: String(p.value);
|
|
347
|
-
maxRowWidth = Math.max(
|
|
348
|
-
maxRowWidth,
|
|
349
|
-
keyColWidth + measureText(valStr, META_FONT_SIZE)
|
|
350
|
-
);
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
// Computed row widths (e.g., "p90: 520ms" or "p90: 520ms / 500ms" when SLO configured)
|
|
354
|
-
if (computedRows > 0) {
|
|
355
|
-
const perc = node.computedLatencyPercentiles;
|
|
356
|
-
const msValues = expanded ? [perc.p50, perc.p90, perc.p99] : [perc.p90];
|
|
357
|
-
for (const ms of msValues) {
|
|
358
|
-
if (ms > 0) {
|
|
359
|
-
maxRowWidth = Math.max(
|
|
360
|
-
maxRowWidth,
|
|
361
|
-
keyColWidth + measureText(formatMs(ms), META_FONT_SIZE)
|
|
362
|
-
);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
// p90 may show "<current> / <threshold>" when non-green. Always reserve combined width
|
|
366
|
-
// so node width doesn't reflow when SLO state transitions from green to warning/overloaded.
|
|
367
|
-
if (perc.p90 > 0) {
|
|
368
|
-
const rawThreshold =
|
|
369
|
-
node.properties.find((p) => p.key === 'slo-p90-latency-ms')?.value ??
|
|
370
|
-
options?.['slo-p90-latency-ms'];
|
|
371
|
-
const threshold =
|
|
372
|
-
rawThreshold != null ? parseFloat(String(rawThreshold)) : NaN;
|
|
373
|
-
if (!isNaN(threshold) && threshold > 0) {
|
|
374
|
-
// formatMs here must produce the same string as formatMsShort in renderer.ts — both are identical.
|
|
375
|
-
// If either changes, the reserved width and the rendered text will diverge.
|
|
376
|
-
const combinedVal = `${formatMs(perc.p90)} / ${formatMs(threshold)}`;
|
|
377
|
-
maxRowWidth = Math.max(
|
|
378
|
-
maxRowWidth,
|
|
379
|
-
keyColWidth + measureText(combinedVal, META_FONT_SIZE)
|
|
380
|
-
);
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
if (node.computedUptime < 1) {
|
|
384
|
-
maxRowWidth = Math.max(
|
|
385
|
-
maxRowWidth,
|
|
386
|
-
keyColWidth +
|
|
387
|
-
measureText(formatUptime(node.computedUptime), META_FONT_SIZE)
|
|
388
|
-
);
|
|
389
|
-
}
|
|
390
|
-
if (node.computedAvailability < 1) {
|
|
391
|
-
maxRowWidth = Math.max(
|
|
392
|
-
maxRowWidth,
|
|
393
|
-
keyColWidth +
|
|
394
|
-
measureText(formatUptime(node.computedAvailability), META_FONT_SIZE)
|
|
395
|
-
);
|
|
396
|
-
}
|
|
397
|
-
// CB state row ("CB: OPEN") — inverted pill, use full text width
|
|
398
|
-
if (node.computedCbState === 'open') {
|
|
399
|
-
maxRowWidth = Math.max(
|
|
400
|
-
maxRowWidth,
|
|
401
|
-
measureText('CB: OPEN', META_FONT_SIZE) + 8
|
|
402
|
-
);
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// Pixel-width cap for description lines, derived from the legacy 120-char cap
|
|
407
|
-
// at META_FONT_SIZE — must match renderer.ts's DESC_MAX_WIDTH.
|
|
408
|
-
const DESC_MAX_WIDTH = 120 * CHAR_WIDTH_RATIO * META_FONT_SIZE;
|
|
409
|
-
const descLines =
|
|
410
|
-
expanded && node.description && !node.isEdge ? node.description : [];
|
|
411
|
-
let descWidth = 0;
|
|
412
|
-
for (const dl of descLines) {
|
|
413
|
-
const truncated = truncateText(dl, META_FONT_SIZE, DESC_MAX_WIDTH);
|
|
414
|
-
descWidth = Math.max(
|
|
415
|
-
descWidth,
|
|
416
|
-
measureText(truncated, META_FONT_SIZE) + PADDING_X
|
|
417
|
-
);
|
|
418
|
-
}
|
|
419
|
-
return Math.max(MIN_NODE_WIDTH, labelWidth, maxRowWidth + 20, descWidth);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
function computeNodeHeight(
|
|
423
|
-
node: ComputedInfraNode,
|
|
424
|
-
expanded: boolean,
|
|
425
|
-
options?: Record<string, string>
|
|
426
|
-
): number {
|
|
427
|
-
const propCount = countDisplayProps(node, expanded, options);
|
|
428
|
-
const computedCount = countComputedRows(node, expanded);
|
|
429
|
-
const hasRps = node.computedRps > 0;
|
|
430
|
-
const descLineCount =
|
|
431
|
-
expanded && node.description && !node.isEdge ? node.description.length : 0;
|
|
432
|
-
const descH = descLineCount * META_LINE_HEIGHT;
|
|
433
|
-
if (propCount === 0 && computedCount === 0 && !hasRps)
|
|
434
|
-
return NODE_HEADER_HEIGHT + descH + NODE_PAD_BOTTOM;
|
|
435
|
-
|
|
436
|
-
let h = NODE_HEADER_HEIGHT + descH + NODE_SEPARATOR_GAP;
|
|
437
|
-
// Computed section: RPS + computed rows
|
|
438
|
-
const computedSectionCount = (hasRps ? 1 : 0) + computedCount;
|
|
439
|
-
h += computedSectionCount * META_LINE_HEIGHT;
|
|
440
|
-
// Separator between computed and declared sections
|
|
441
|
-
if (computedSectionCount > 0 && propCount > 0) h += NODE_SEPARATOR_GAP;
|
|
442
|
-
// Declared property rows
|
|
443
|
-
h += propCount * META_LINE_HEIGHT;
|
|
444
|
-
// Role dots row
|
|
445
|
-
if (hasRoles(node)) h += ROLE_DOT_ROW;
|
|
446
|
-
h += NODE_PAD_BOTTOM;
|
|
447
|
-
// Collapsed group nodes have a collapse bar at the bottom — add space so dots aren't obscured
|
|
448
|
-
if (node.id.startsWith('[')) h += COLLAPSE_BAR_HEIGHT;
|
|
449
|
-
return h;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
function formatRps(rps: number): string {
|
|
453
|
-
if (rps >= 1000) return `${(rps / 1000).toFixed(1)}k rps`;
|
|
454
|
-
return `${Math.round(rps)} rps`;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
function formatRpsShort(rps: number): string {
|
|
458
|
-
if (rps >= 1000) return `${(rps / 1000).toFixed(1)}k`;
|
|
459
|
-
return `${Math.round(rps)}`;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
function getNumProp(
|
|
463
|
-
node: ComputedInfraNode,
|
|
464
|
-
key: string,
|
|
465
|
-
fallback: number
|
|
466
|
-
): number {
|
|
467
|
-
const p = node.properties.find((pr) => pr.key === key);
|
|
468
|
-
if (!p) return fallback;
|
|
469
|
-
return typeof p.value === 'number'
|
|
470
|
-
? p.value
|
|
471
|
-
: parseFloat(String(p.value)) || fallback;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
function formatMs(ms: number): string {
|
|
475
|
-
if (ms >= 1000) return `${(ms / 1000).toFixed(1)}s`;
|
|
476
|
-
return `${Math.round(ms)}ms`;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
function formatUptime(fraction: number): string {
|
|
480
|
-
const pct = fraction * 100;
|
|
481
|
-
if (pct >= 99.99) return '99.99%';
|
|
482
|
-
if (pct >= 99.9) return `${pct.toFixed(2)}%`;
|
|
483
|
-
if (pct >= 99) return `${pct.toFixed(1)}%`;
|
|
484
|
-
return `${pct.toFixed(1)}%`;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
// ============================================================
|
|
488
|
-
// Group separation pass
|
|
489
|
-
// ============================================================
|
|
490
|
-
|
|
491
|
-
const GROUP_GAP = GROUP_PADDING * 2 + GROUP_HEADER_HEIGHT; // min clear gap between group boxes
|
|
492
|
-
|
|
493
|
-
export function separateGroups(
|
|
494
|
-
groups: Writable<InfraLayoutGroup>[],
|
|
495
|
-
nodes: Writable<InfraLayoutNode>[],
|
|
496
|
-
isLR: boolean,
|
|
497
|
-
maxIterations = 20
|
|
498
|
-
): Map<string, { dx: number; dy: number }> {
|
|
499
|
-
// Symmetric 2D rectangle intersection — no sorting needed, handles all
|
|
500
|
-
// relative positions correctly, stable after mid-pass shifts.
|
|
501
|
-
// Endpoint edge routing is not affected: renderer.ts recomputes border
|
|
502
|
-
// connection points from node x/y at render time via nodeBorderPoint().
|
|
503
|
-
const groupDeltas = new Map<string, { dx: number; dy: number }>();
|
|
504
|
-
let converged = false;
|
|
505
|
-
for (let iter = 0; iter < maxIterations; iter++) {
|
|
506
|
-
let anyOverlap = false;
|
|
507
|
-
for (let i = 0; i < groups.length; i++) {
|
|
508
|
-
for (let j = i + 1; j < groups.length; j++) {
|
|
509
|
-
// In-bounds by loop guard.
|
|
510
|
-
const ga = groups[i]!;
|
|
511
|
-
const gb = groups[j]!;
|
|
512
|
-
|
|
513
|
-
// Symmetric primary-axis overlap (Y for LR, X for TB)
|
|
514
|
-
const primaryOverlap = isLR
|
|
515
|
-
? Math.min(ga.y + ga.height, gb.y + gb.height) - Math.max(ga.y, gb.y)
|
|
516
|
-
: Math.min(ga.x + ga.width, gb.x + gb.width) - Math.max(ga.x, gb.x);
|
|
517
|
-
if (primaryOverlap <= 0) continue;
|
|
518
|
-
|
|
519
|
-
// Symmetric cross-axis overlap — boxes must intersect in 2D
|
|
520
|
-
const crossOverlap = isLR
|
|
521
|
-
? Math.min(ga.x + ga.width, gb.x + gb.width) - Math.max(ga.x, gb.x)
|
|
522
|
-
: Math.min(ga.y + ga.height, gb.y + gb.height) - Math.max(ga.y, gb.y);
|
|
523
|
-
if (crossOverlap <= 0) continue;
|
|
524
|
-
|
|
525
|
-
anyOverlap = true;
|
|
526
|
-
const shift = primaryOverlap + GROUP_GAP;
|
|
527
|
-
|
|
528
|
-
// Shift the group with the larger primary-axis center (deterministic)
|
|
529
|
-
const aCenter = isLR ? ga.y + ga.height / 2 : ga.x + ga.width / 2;
|
|
530
|
-
const bCenter = isLR ? gb.y + gb.height / 2 : gb.x + gb.width / 2;
|
|
531
|
-
const groupToShift = aCenter <= bCenter ? gb : ga;
|
|
532
|
-
|
|
533
|
-
if (isLR) groupToShift.y += shift;
|
|
534
|
-
else groupToShift.x += shift;
|
|
535
|
-
|
|
536
|
-
// Accumulate the total delta for this group (used by fixEdgeWaypoints)
|
|
537
|
-
const prev = groupDeltas.get(groupToShift.id) ?? { dx: 0, dy: 0 };
|
|
538
|
-
if (isLR)
|
|
539
|
-
groupDeltas.set(groupToShift.id, {
|
|
540
|
-
dx: prev.dx,
|
|
541
|
-
dy: prev.dy + shift,
|
|
542
|
-
});
|
|
543
|
-
else
|
|
544
|
-
groupDeltas.set(groupToShift.id, {
|
|
545
|
-
dx: prev.dx + shift,
|
|
546
|
-
dy: prev.dy,
|
|
547
|
-
});
|
|
548
|
-
|
|
549
|
-
for (const node of nodes) {
|
|
550
|
-
if (node.groupId === groupToShift.id) {
|
|
551
|
-
if (isLR) node.y += shift;
|
|
552
|
-
else node.x += shift;
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
if (!anyOverlap) {
|
|
558
|
-
converged = true;
|
|
559
|
-
break;
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
if (!converged && maxIterations > 0) {
|
|
563
|
-
console.warn(
|
|
564
|
-
`separateGroups: hit maxIterations (${maxIterations}) without fully resolving all group overlaps`
|
|
565
|
-
);
|
|
566
|
-
}
|
|
567
|
-
return groupDeltas;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
export function fixEdgeWaypoints(
|
|
571
|
-
edges: Writable<InfraLayoutEdge>[],
|
|
572
|
-
nodes: Writable<InfraLayoutNode>[],
|
|
573
|
-
groupDeltas: Map<string, { dx: number; dy: number }>
|
|
574
|
-
): void {
|
|
575
|
-
if (groupDeltas.size === 0) return;
|
|
576
|
-
const nodeToGroup = new Map<string, string | null>();
|
|
577
|
-
for (const node of nodes) nodeToGroup.set(node.id, node.groupId);
|
|
578
|
-
|
|
579
|
-
for (const edge of edges) {
|
|
580
|
-
const srcGroup = nodeToGroup.get(edge.sourceId) ?? null;
|
|
581
|
-
// Group-targeting edges (targetId is a group ID, not a node) return undefined from the map → null →
|
|
582
|
-
// treated as "ungrouped target", which is the correct approximation.
|
|
583
|
-
const tgtGroup = nodeToGroup.get(edge.targetId) ?? null;
|
|
584
|
-
const srcDelta = srcGroup ? groupDeltas.get(srcGroup) : undefined;
|
|
585
|
-
const tgtDelta = tgtGroup ? groupDeltas.get(tgtGroup) : undefined;
|
|
586
|
-
|
|
587
|
-
if (!srcDelta && !tgtDelta) continue; // neither side shifted
|
|
588
|
-
|
|
589
|
-
if (srcDelta && tgtDelta && srcGroup !== tgtGroup) {
|
|
590
|
-
// both sides in different shifted groups — discard, renderer draws a straight line
|
|
591
|
-
edge.points = [];
|
|
592
|
-
continue;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
const delta = srcDelta ?? tgtDelta!;
|
|
596
|
-
for (const pt of edge.points as Writable<{ x: number; y: number }>[]) {
|
|
597
|
-
pt.x += delta.dx;
|
|
598
|
-
pt.y += delta.dy;
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
// ============================================================
|
|
604
|
-
// Layout engine
|
|
605
|
-
// ============================================================
|
|
606
|
-
|
|
607
|
-
export function layoutInfra(
|
|
608
|
-
computed: ComputedInfraModel,
|
|
609
|
-
expandedNodeIds?: Set<string> | null,
|
|
610
|
-
collapsedNodes?: Set<string> | null
|
|
611
|
-
): InfraLayoutResult {
|
|
612
|
-
if (computed.nodes.length === 0) {
|
|
613
|
-
return {
|
|
614
|
-
nodes: [],
|
|
615
|
-
edges: [],
|
|
616
|
-
groups: [],
|
|
617
|
-
options: {},
|
|
618
|
-
direction: computed.direction,
|
|
619
|
-
width: 0,
|
|
620
|
-
height: 0,
|
|
621
|
-
};
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
const isLR = computed.direction !== 'TB';
|
|
625
|
-
const g = new dagre.graphlib.Graph();
|
|
626
|
-
g.setGraph({
|
|
627
|
-
rankdir: computed.direction === 'TB' ? 'TB' : 'LR',
|
|
628
|
-
nodesep: isLR ? 70 : 60,
|
|
629
|
-
ranksep: isLR ? 150 : 120,
|
|
630
|
-
edgesep: 30,
|
|
631
|
-
});
|
|
632
|
-
g.setDefaultEdgeLabel(() => ({}));
|
|
633
|
-
|
|
634
|
-
// Build set of grouped node IDs for inflating dimensions
|
|
635
|
-
const groupedNodeIds = new Set<string>();
|
|
636
|
-
for (const node of computed.nodes) {
|
|
637
|
-
if (node.groupId) groupedNodeIds.add(node.id);
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
// Extra space dagre must reserve for the group bounding box
|
|
641
|
-
const GROUP_INFLATE = GROUP_PADDING * 2 + GROUP_HEADER_HEIGHT;
|
|
642
|
-
|
|
643
|
-
// Add nodes — inflate grouped nodes so dagre accounts for group boxes
|
|
644
|
-
const widthMap = new Map<string, number>();
|
|
645
|
-
const heightMap = new Map<string, number>();
|
|
646
|
-
for (const node of computed.nodes) {
|
|
647
|
-
const isNodeCollapsed = collapsedNodes?.has(node.id) ?? false;
|
|
648
|
-
const expanded =
|
|
649
|
-
!isNodeCollapsed && (expandedNodeIds?.has(node.id) ?? false);
|
|
650
|
-
const width = computeNodeWidth(node, expanded, computed.options);
|
|
651
|
-
const height = isNodeCollapsed
|
|
652
|
-
? NODE_HEADER_HEIGHT + NODE_PAD_BOTTOM
|
|
653
|
-
: computeNodeHeight(node, expanded, computed.options);
|
|
654
|
-
widthMap.set(node.id, width);
|
|
655
|
-
heightMap.set(node.id, height);
|
|
656
|
-
const inGroup = groupedNodeIds.has(node.id);
|
|
657
|
-
g.setNode(node.id, {
|
|
658
|
-
label: node.label,
|
|
659
|
-
width: inGroup && !isLR ? width + GROUP_INFLATE : width,
|
|
660
|
-
height: inGroup && isLR ? height + GROUP_INFLATE : height,
|
|
661
|
-
});
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// Add edges — skip edges targeting groups (resolve to children instead)
|
|
665
|
-
const groupChildIds = new Set<string>();
|
|
666
|
-
for (const node of computed.nodes) {
|
|
667
|
-
if (node.groupId) groupChildIds.add(node.id);
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
// Build group child lookup
|
|
671
|
-
const groupChildren = new Map<string, string[]>();
|
|
672
|
-
for (const node of computed.nodes) {
|
|
673
|
-
if (node.groupId) {
|
|
674
|
-
const list = groupChildren.get(node.groupId) ?? [];
|
|
675
|
-
list.push(node.id);
|
|
676
|
-
groupChildren.set(node.groupId, list);
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
for (const edge of computed.edges) {
|
|
681
|
-
// If target is a group, add edges to all children of that group
|
|
682
|
-
const children = groupChildren.get(edge.targetId);
|
|
683
|
-
if (children && children.length > 0) {
|
|
684
|
-
for (const childId of children) {
|
|
685
|
-
g.setEdge(edge.sourceId, childId, { label: edge.label });
|
|
686
|
-
}
|
|
687
|
-
} else {
|
|
688
|
-
g.setEdge(edge.sourceId, edge.targetId, { label: edge.label });
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
// Run layout
|
|
693
|
-
dagre.layout(g);
|
|
694
|
-
|
|
695
|
-
// Extract positioned nodes
|
|
696
|
-
const layoutNodes: Writable<InfraLayoutNode>[] = computed.nodes.map(
|
|
697
|
-
(node): Writable<InfraLayoutNode> => {
|
|
698
|
-
const pos = g.node(node.id);
|
|
699
|
-
return {
|
|
700
|
-
id: node.id,
|
|
701
|
-
label: node.label,
|
|
702
|
-
x: pos.x,
|
|
703
|
-
y: pos.y,
|
|
704
|
-
width: widthMap.get(node.id) ?? MIN_NODE_WIDTH,
|
|
705
|
-
height: heightMap.get(node.id) ?? NODE_HEADER_HEIGHT + NODE_PAD_BOTTOM,
|
|
706
|
-
computedRps: node.computedRps,
|
|
707
|
-
overloaded: node.overloaded,
|
|
708
|
-
rateLimited: node.rateLimited,
|
|
709
|
-
isEdge: node.isEdge,
|
|
710
|
-
groupId: node.groupId,
|
|
711
|
-
computedLatencyMs: node.computedLatencyMs,
|
|
712
|
-
computedLatencyPercentiles: node.computedLatencyPercentiles,
|
|
713
|
-
computedUptime: node.computedUptime,
|
|
714
|
-
computedAvailability: node.computedAvailability,
|
|
715
|
-
computedAvailabilityPercentiles: node.computedAvailabilityPercentiles,
|
|
716
|
-
computedInstances: node.computedInstances,
|
|
717
|
-
computedConcurrentInvocations: node.computedConcurrentInvocations,
|
|
718
|
-
computedCbState: node.computedCbState,
|
|
719
|
-
...(node.childHealthState !== undefined && {
|
|
720
|
-
childHealthState: node.childHealthState,
|
|
721
|
-
}),
|
|
722
|
-
...(node.queueMetrics !== undefined && {
|
|
723
|
-
queueMetrics: node.queueMetrics,
|
|
724
|
-
}),
|
|
725
|
-
properties: node.properties,
|
|
726
|
-
tags: node.tags,
|
|
727
|
-
...(node.description !== undefined && {
|
|
728
|
-
description: node.description,
|
|
729
|
-
}),
|
|
730
|
-
lineNumber: node.lineNumber,
|
|
731
|
-
};
|
|
732
|
-
}
|
|
733
|
-
);
|
|
734
|
-
|
|
735
|
-
// Extract edge waypoints
|
|
736
|
-
const layoutEdges: Writable<InfraLayoutEdge>[] = [];
|
|
737
|
-
for (const edge of computed.edges) {
|
|
738
|
-
const children = groupChildren.get(edge.targetId);
|
|
739
|
-
if (children && children.length > 0) {
|
|
740
|
-
// Use the first child's edge points as representative
|
|
741
|
-
// In-bounds: children.length > 0 guarded above.
|
|
742
|
-
const edgeData = g.edge(edge.sourceId, children[0]!);
|
|
743
|
-
layoutEdges.push({
|
|
744
|
-
sourceId: edge.sourceId,
|
|
745
|
-
targetId: edge.targetId,
|
|
746
|
-
label: edge.label,
|
|
747
|
-
async: edge.async,
|
|
748
|
-
computedRps: edge.computedRps,
|
|
749
|
-
split: edge.split,
|
|
750
|
-
fanout: edge.fanout,
|
|
751
|
-
points: edgeData?.points ?? [],
|
|
752
|
-
lineNumber: edge.lineNumber,
|
|
753
|
-
});
|
|
754
|
-
} else {
|
|
755
|
-
const edgeData = g.edge(edge.sourceId, edge.targetId);
|
|
756
|
-
layoutEdges.push({
|
|
757
|
-
sourceId: edge.sourceId,
|
|
758
|
-
targetId: edge.targetId,
|
|
759
|
-
label: edge.label,
|
|
760
|
-
async: edge.async,
|
|
761
|
-
computedRps: edge.computedRps,
|
|
762
|
-
split: edge.split,
|
|
763
|
-
fanout: edge.fanout,
|
|
764
|
-
points: edgeData?.points ?? [],
|
|
765
|
-
lineNumber: edge.lineNumber,
|
|
766
|
-
});
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
// Compute group bounding boxes from children
|
|
771
|
-
const layoutGroups: Writable<InfraLayoutGroup>[] = computed.groups.map(
|
|
772
|
-
(group): Writable<InfraLayoutGroup> => {
|
|
773
|
-
const childNodes = layoutNodes.filter((n) => n.groupId === group.id);
|
|
774
|
-
if (childNodes.length === 0) {
|
|
775
|
-
return {
|
|
776
|
-
id: group.id,
|
|
777
|
-
label: group.label,
|
|
778
|
-
x: 0,
|
|
779
|
-
y: 0,
|
|
780
|
-
width: MIN_NODE_WIDTH,
|
|
781
|
-
height: NODE_HEADER_HEIGHT + NODE_PAD_BOTTOM,
|
|
782
|
-
...(group.instances !== undefined && { instances: group.instances }),
|
|
783
|
-
lineNumber: group.lineNumber,
|
|
784
|
-
};
|
|
785
|
-
}
|
|
786
|
-
let minX = Infinity,
|
|
787
|
-
minY = Infinity,
|
|
788
|
-
maxX = -Infinity,
|
|
789
|
-
maxY = -Infinity;
|
|
790
|
-
for (const child of childNodes) {
|
|
791
|
-
const left = child.x - child.width / 2;
|
|
792
|
-
const right = child.x + child.width / 2;
|
|
793
|
-
const top = child.y - child.height / 2;
|
|
794
|
-
const bottom = child.y + child.height / 2;
|
|
795
|
-
if (left < minX) minX = left;
|
|
796
|
-
if (right > maxX) maxX = right;
|
|
797
|
-
if (top < minY) minY = top;
|
|
798
|
-
if (bottom > maxY) maxY = bottom;
|
|
799
|
-
}
|
|
800
|
-
return {
|
|
801
|
-
id: group.id,
|
|
802
|
-
label: group.label,
|
|
803
|
-
x: minX - GROUP_PADDING,
|
|
804
|
-
y: minY - GROUP_PADDING - GROUP_HEADER_HEIGHT,
|
|
805
|
-
width: maxX - minX + GROUP_PADDING * 2,
|
|
806
|
-
height: maxY - minY + GROUP_PADDING * 2 + GROUP_HEADER_HEIGHT,
|
|
807
|
-
...(group.instances !== undefined && { instances: group.instances }),
|
|
808
|
-
lineNumber: group.lineNumber,
|
|
809
|
-
};
|
|
810
|
-
}
|
|
811
|
-
);
|
|
812
|
-
|
|
813
|
-
// Separate overlapping groups (post-layout pass) and fix stale edge waypoints
|
|
814
|
-
const groupDeltas = separateGroups(layoutGroups, layoutNodes, isLR);
|
|
815
|
-
fixEdgeWaypoints(layoutEdges, layoutNodes, groupDeltas);
|
|
816
|
-
|
|
817
|
-
// Compute total dimensions
|
|
818
|
-
let minX = Infinity,
|
|
819
|
-
minY = Infinity,
|
|
820
|
-
maxX = -Infinity,
|
|
821
|
-
maxY = -Infinity;
|
|
822
|
-
for (const node of layoutNodes) {
|
|
823
|
-
const left = node.x - node.width / 2;
|
|
824
|
-
const right = node.x + node.width / 2;
|
|
825
|
-
const top = node.y - node.height / 2;
|
|
826
|
-
const bottom = node.y + node.height / 2;
|
|
827
|
-
if (left < minX) minX = left;
|
|
828
|
-
if (right > maxX) maxX = right;
|
|
829
|
-
if (top < minY) minY = top;
|
|
830
|
-
if (bottom > maxY) maxY = bottom;
|
|
831
|
-
}
|
|
832
|
-
for (const group of layoutGroups) {
|
|
833
|
-
if (group.x < minX) minX = group.x;
|
|
834
|
-
if (group.x + group.width > maxX) maxX = group.x + group.width;
|
|
835
|
-
if (group.y < minY) minY = group.y;
|
|
836
|
-
if (group.y + group.height > maxY) maxY = group.y + group.height;
|
|
837
|
-
}
|
|
838
|
-
for (const edge of layoutEdges) {
|
|
839
|
-
for (const pt of edge.points) {
|
|
840
|
-
if (pt.x < minX) minX = pt.x;
|
|
841
|
-
if (pt.x > maxX) maxX = pt.x;
|
|
842
|
-
if (pt.y < minY) minY = pt.y;
|
|
843
|
-
if (pt.y > maxY) maxY = pt.y;
|
|
844
|
-
}
|
|
845
|
-
// Account for edge label width at midpoint
|
|
846
|
-
if (edge.label) {
|
|
847
|
-
const midIdx = Math.floor(edge.points.length / 2);
|
|
848
|
-
const midPt = edge.points[midIdx];
|
|
849
|
-
if (midPt) {
|
|
850
|
-
const halfWidth =
|
|
851
|
-
(measureText(edge.label, EDGE_LABEL_FONT_SIZE) + 8) / 2;
|
|
852
|
-
if (midPt.x - halfWidth < minX) minX = midPt.x - halfWidth;
|
|
853
|
-
if (midPt.x + halfWidth > maxX) maxX = midPt.x + halfWidth;
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
// Shift everything to start at EDGE_MARGIN
|
|
859
|
-
const shiftX = -minX + EDGE_MARGIN;
|
|
860
|
-
const shiftY = -minY + EDGE_MARGIN;
|
|
861
|
-
for (const node of layoutNodes) {
|
|
862
|
-
node.x += shiftX;
|
|
863
|
-
node.y += shiftY;
|
|
864
|
-
}
|
|
865
|
-
for (const edge of layoutEdges) {
|
|
866
|
-
for (const pt of edge.points as Writable<{ x: number; y: number }>[]) {
|
|
867
|
-
pt.x += shiftX;
|
|
868
|
-
pt.y += shiftY;
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
for (const group of layoutGroups) {
|
|
872
|
-
group.x += shiftX;
|
|
873
|
-
group.y += shiftY;
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
const totalWidth = maxX + shiftX + EDGE_MARGIN;
|
|
877
|
-
const totalHeight = maxY + shiftY + EDGE_MARGIN;
|
|
878
|
-
|
|
879
|
-
return {
|
|
880
|
-
nodes: layoutNodes,
|
|
881
|
-
edges: layoutEdges,
|
|
882
|
-
groups: layoutGroups,
|
|
883
|
-
options: computed.options,
|
|
884
|
-
direction: computed.direction,
|
|
885
|
-
width: totalWidth,
|
|
886
|
-
height: totalHeight,
|
|
887
|
-
};
|
|
888
|
-
}
|