@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
|
@@ -1,776 +0,0 @@
|
|
|
1
|
-
// ============================================================
|
|
2
|
-
// Centralized legend layout engine
|
|
3
|
-
// Pure function: config + state + width → positions
|
|
4
|
-
// ============================================================
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
LEGEND_HEIGHT,
|
|
8
|
-
LEGEND_PILL_PAD,
|
|
9
|
-
LEGEND_PILL_FONT_SIZE,
|
|
10
|
-
LEGEND_CAPSULE_PAD,
|
|
11
|
-
LEGEND_DOT_R,
|
|
12
|
-
LEGEND_ENTRY_FONT_SIZE,
|
|
13
|
-
LEGEND_ENTRY_DOT_GAP,
|
|
14
|
-
LEGEND_ENTRY_TRAIL,
|
|
15
|
-
LEGEND_GROUP_GAP,
|
|
16
|
-
LEGEND_MAX_ENTRY_ROWS,
|
|
17
|
-
LEGEND_GEAR_PILL_W,
|
|
18
|
-
LEGEND_TOGGLE_DOT_R,
|
|
19
|
-
measureLegendText,
|
|
20
|
-
truncateLegendText,
|
|
21
|
-
} from './legend-constants';
|
|
22
|
-
|
|
23
|
-
import { compactNumber } from './number-format';
|
|
24
|
-
import type { LegendGroupData } from './legend-types';
|
|
25
|
-
import type {
|
|
26
|
-
LegendConfig,
|
|
27
|
-
LegendState,
|
|
28
|
-
LegendLayout,
|
|
29
|
-
LegendPillLayout,
|
|
30
|
-
LegendCapsuleLayout,
|
|
31
|
-
LegendControlLayout,
|
|
32
|
-
LegendEntryLayout,
|
|
33
|
-
LegendControl,
|
|
34
|
-
ControlsGroupLayout,
|
|
35
|
-
ControlsGroupToggleLayout,
|
|
36
|
-
} from './legend-types';
|
|
37
|
-
|
|
38
|
-
// ── Constants ───────────────────────────────────────────────
|
|
39
|
-
|
|
40
|
-
const CONTROL_PILL_PAD = 16;
|
|
41
|
-
const CONTROL_FONT_SIZE = 11;
|
|
42
|
-
const CONTROL_ICON_GAP = 4;
|
|
43
|
-
const CONTROL_GAP = 8;
|
|
44
|
-
|
|
45
|
-
// Continuous-ramp swatch dims (choropleth groups) — match the legacy map ramp
|
|
46
|
-
// block so the gradient reads the same now that it lives in the top legend.
|
|
47
|
-
const RAMP_LEGEND_W = 80;
|
|
48
|
-
const RAMP_LEGEND_H = 8;
|
|
49
|
-
const RAMP_LABEL_GAP = 6;
|
|
50
|
-
|
|
51
|
-
/** Compact numeric label for a ramp end — shared with the map's on-region value
|
|
52
|
-
* labels (`compactNumber`) so the gradient ends and region values read the same
|
|
53
|
-
* (`40M` legend end ↔ a `39.5M` region). */
|
|
54
|
-
function fmtRamp(n: number): string {
|
|
55
|
-
return compactNumber(n);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Width of a gradient group's capsule: pill + min label + ramp + max label. */
|
|
59
|
-
function gradientCapsuleWidth(
|
|
60
|
-
name: string,
|
|
61
|
-
gradient: NonNullable<LegendGroupData['gradient']>
|
|
62
|
-
): number {
|
|
63
|
-
const pw = pillWidth(name);
|
|
64
|
-
const minW = measureLegendText(fmtRamp(gradient.min), LEGEND_ENTRY_FONT_SIZE);
|
|
65
|
-
const maxW = measureLegendText(fmtRamp(gradient.max), LEGEND_ENTRY_FONT_SIZE);
|
|
66
|
-
return (
|
|
67
|
-
LEGEND_CAPSULE_PAD +
|
|
68
|
-
pw +
|
|
69
|
-
4 +
|
|
70
|
-
minW +
|
|
71
|
-
RAMP_LABEL_GAP +
|
|
72
|
-
RAMP_LEGEND_W +
|
|
73
|
-
RAMP_LABEL_GAP +
|
|
74
|
-
maxW +
|
|
75
|
-
LEGEND_CAPSULE_PAD
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Active capsule for a continuous-ramp (choropleth) group. */
|
|
80
|
-
function buildGradientCapsuleLayout(
|
|
81
|
-
group: LegendGroupData,
|
|
82
|
-
gradient: NonNullable<LegendGroupData['gradient']>
|
|
83
|
-
): LegendCapsuleLayout {
|
|
84
|
-
const pw = pillWidth(group.name);
|
|
85
|
-
const minText = fmtRamp(gradient.min);
|
|
86
|
-
const maxText = fmtRamp(gradient.max);
|
|
87
|
-
const minW = measureLegendText(minText, LEGEND_ENTRY_FONT_SIZE);
|
|
88
|
-
const gx = LEGEND_CAPSULE_PAD + pw + 4;
|
|
89
|
-
const minX = gx;
|
|
90
|
-
const rampX = gx + minW + RAMP_LABEL_GAP;
|
|
91
|
-
const maxX = rampX + RAMP_LEGEND_W + RAMP_LABEL_GAP;
|
|
92
|
-
const width = gradientCapsuleWidth(group.name, gradient);
|
|
93
|
-
return {
|
|
94
|
-
groupName: group.name,
|
|
95
|
-
x: 0,
|
|
96
|
-
y: 0,
|
|
97
|
-
width,
|
|
98
|
-
height: LEGEND_HEIGHT,
|
|
99
|
-
pill: {
|
|
100
|
-
groupName: group.name,
|
|
101
|
-
x: LEGEND_CAPSULE_PAD,
|
|
102
|
-
y: LEGEND_CAPSULE_PAD,
|
|
103
|
-
width: pw,
|
|
104
|
-
height: LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2,
|
|
105
|
-
isActive: true,
|
|
106
|
-
},
|
|
107
|
-
entries: [],
|
|
108
|
-
gradient: {
|
|
109
|
-
rampX,
|
|
110
|
-
rampY: (LEGEND_HEIGHT - RAMP_LEGEND_H) / 2,
|
|
111
|
-
rampW: RAMP_LEGEND_W,
|
|
112
|
-
rampH: RAMP_LEGEND_H,
|
|
113
|
-
min: gradient.min,
|
|
114
|
-
max: gradient.max,
|
|
115
|
-
minText,
|
|
116
|
-
minX,
|
|
117
|
-
maxText,
|
|
118
|
-
maxX,
|
|
119
|
-
textY: LEGEND_HEIGHT / 2,
|
|
120
|
-
low: gradient.low,
|
|
121
|
-
high: gradient.high,
|
|
122
|
-
},
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// ── Measurement helpers ─────────────────────────────────────
|
|
127
|
-
|
|
128
|
-
export function pillWidth(name: string): number {
|
|
129
|
-
return measureLegendText(name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function entriesWidth(
|
|
133
|
-
entries: ReadonlyArray<{ readonly value: string }>
|
|
134
|
-
): number {
|
|
135
|
-
let w = 0;
|
|
136
|
-
for (const e of entries) {
|
|
137
|
-
w +=
|
|
138
|
-
LEGEND_DOT_R * 2 +
|
|
139
|
-
LEGEND_ENTRY_DOT_GAP +
|
|
140
|
-
measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) +
|
|
141
|
-
LEGEND_ENTRY_TRAIL;
|
|
142
|
-
}
|
|
143
|
-
return w;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function entryWidth(value: string): number {
|
|
147
|
-
return (
|
|
148
|
-
LEGEND_DOT_R * 2 +
|
|
149
|
-
LEGEND_ENTRY_DOT_GAP +
|
|
150
|
-
measureLegendText(value, LEGEND_ENTRY_FONT_SIZE) +
|
|
151
|
-
LEGEND_ENTRY_TRAIL
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function controlWidth(control: LegendControl): number {
|
|
156
|
-
let w = CONTROL_PILL_PAD;
|
|
157
|
-
if (control.label) {
|
|
158
|
-
w += measureLegendText(control.label, CONTROL_FONT_SIZE);
|
|
159
|
-
if (control.icon) w += CONTROL_ICON_GAP;
|
|
160
|
-
}
|
|
161
|
-
// Icon space (approximate — icons are typically ~14px)
|
|
162
|
-
if (control.icon) w += 14;
|
|
163
|
-
if (control.children) {
|
|
164
|
-
for (const child of control.children) {
|
|
165
|
-
w += measureLegendText(child.label, CONTROL_FONT_SIZE) + 12;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
return w;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function capsuleWidth(
|
|
172
|
-
name: string,
|
|
173
|
-
entries: ReadonlyArray<{ readonly value: string }>,
|
|
174
|
-
containerWidth: number,
|
|
175
|
-
addonWidth = 0
|
|
176
|
-
): {
|
|
177
|
-
width: number;
|
|
178
|
-
entryRows: number;
|
|
179
|
-
moreCount: number;
|
|
180
|
-
visibleEntries: number;
|
|
181
|
-
} {
|
|
182
|
-
const pw = pillWidth(name);
|
|
183
|
-
const maxCapsuleW = containerWidth;
|
|
184
|
-
const baseW = LEGEND_CAPSULE_PAD * 2 + pw + 4 + addonWidth;
|
|
185
|
-
|
|
186
|
-
// Try single row first. Tolerate sub-pixel float drift: the caller
|
|
187
|
-
// typically sizes its container to exactly match singleRowW, and FP
|
|
188
|
-
// associativity in intermediate sums can leave maxCapsuleW short by
|
|
189
|
-
// ~1e-14 (same family of bug fixed in 069a327 for the per-entry pass).
|
|
190
|
-
const ew = entriesWidth(entries);
|
|
191
|
-
const singleRowW = baseW + ew;
|
|
192
|
-
if (singleRowW <= maxCapsuleW + 0.5) {
|
|
193
|
-
return {
|
|
194
|
-
width: singleRowW,
|
|
195
|
-
entryRows: 1,
|
|
196
|
-
moreCount: 0,
|
|
197
|
-
visibleEntries: entries.length,
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Multi-row: compute how many entries fit per row
|
|
202
|
-
// Right boundary leaves one LEGEND_CAPSULE_PAD for right padding;
|
|
203
|
-
// left padding is already baked into the starting rowX.
|
|
204
|
-
const rowWidth = maxCapsuleW - LEGEND_CAPSULE_PAD;
|
|
205
|
-
let row = 1;
|
|
206
|
-
let rowX = LEGEND_CAPSULE_PAD + pw + 4 + addonWidth;
|
|
207
|
-
let visible = 0;
|
|
208
|
-
|
|
209
|
-
for (let i = 0; i < entries.length; i++) {
|
|
210
|
-
let ew2 = entryWidth(entries[i]!.value);
|
|
211
|
-
const availW = rowWidth - rowX;
|
|
212
|
-
if (ew2 > availW && i > 0) {
|
|
213
|
-
row++;
|
|
214
|
-
rowX = LEGEND_CAPSULE_PAD + pw + 4 + addonWidth;
|
|
215
|
-
if (row > LEGEND_MAX_ENTRY_ROWS) {
|
|
216
|
-
return {
|
|
217
|
-
width: maxCapsuleW,
|
|
218
|
-
entryRows: LEGEND_MAX_ENTRY_ROWS,
|
|
219
|
-
moreCount: entries.length - visible,
|
|
220
|
-
visibleEntries: visible,
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
// Cap to available width (mirrors truncation in buildCapsuleLayout)
|
|
225
|
-
ew2 = Math.min(ew2, rowWidth - rowX);
|
|
226
|
-
rowX += ew2;
|
|
227
|
-
visible++;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
return {
|
|
231
|
-
width: maxCapsuleW,
|
|
232
|
-
entryRows: row,
|
|
233
|
-
moreCount: 0,
|
|
234
|
-
visibleEntries: entries.length,
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// ── Controls group layout helpers ───────────────────────────
|
|
239
|
-
|
|
240
|
-
export function controlsGroupCapsuleWidth(
|
|
241
|
-
toggles: Array<{ label: string }>
|
|
242
|
-
): number {
|
|
243
|
-
let w = LEGEND_CAPSULE_PAD * 2 + LEGEND_GEAR_PILL_W + 4;
|
|
244
|
-
for (const t of toggles) {
|
|
245
|
-
w +=
|
|
246
|
-
LEGEND_TOGGLE_DOT_R * 2 +
|
|
247
|
-
LEGEND_ENTRY_DOT_GAP +
|
|
248
|
-
measureLegendText(t.label, LEGEND_ENTRY_FONT_SIZE) +
|
|
249
|
-
LEGEND_ENTRY_TRAIL;
|
|
250
|
-
}
|
|
251
|
-
return w;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/** True when the controlsGroup should be hosted by the app overlay strip
|
|
255
|
-
* (gear suppressed, controls row + anchor reserved) rather than drawn inline.
|
|
256
|
-
* Never gates on the export path. */
|
|
257
|
-
function isAppHostedControls(config: LegendConfig, isExport: boolean): boolean {
|
|
258
|
-
return (
|
|
259
|
-
!isExport &&
|
|
260
|
-
config.controlsHost === 'app' &&
|
|
261
|
-
!!config.controlsGroup &&
|
|
262
|
-
config.controlsGroup.toggles.length > 0
|
|
263
|
-
);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function buildControlsGroupLayout(
|
|
267
|
-
config: LegendConfig,
|
|
268
|
-
state: LegendState
|
|
269
|
-
): ControlsGroupLayout | undefined {
|
|
270
|
-
const cg = config.controlsGroup;
|
|
271
|
-
if (!cg || cg.toggles.length === 0) return undefined;
|
|
272
|
-
|
|
273
|
-
const expanded = !!state.controlsExpanded;
|
|
274
|
-
const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
|
|
275
|
-
|
|
276
|
-
if (!expanded) {
|
|
277
|
-
// Collapsed: just a gear pill
|
|
278
|
-
return {
|
|
279
|
-
x: 0,
|
|
280
|
-
y: 0,
|
|
281
|
-
width: LEGEND_GEAR_PILL_W,
|
|
282
|
-
height: LEGEND_HEIGHT,
|
|
283
|
-
expanded: false,
|
|
284
|
-
pill: { x: 0, y: 0, width: LEGEND_GEAR_PILL_W, height: LEGEND_HEIGHT },
|
|
285
|
-
toggles: [],
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Expanded capsule
|
|
290
|
-
const capsuleW = controlsGroupCapsuleWidth(cg.toggles);
|
|
291
|
-
const toggleLayouts: ControlsGroupToggleLayout[] = [];
|
|
292
|
-
let tx = LEGEND_CAPSULE_PAD + LEGEND_GEAR_PILL_W + 4;
|
|
293
|
-
|
|
294
|
-
for (const toggle of cg.toggles) {
|
|
295
|
-
const dotCx = tx + LEGEND_TOGGLE_DOT_R;
|
|
296
|
-
const dotCy = LEGEND_HEIGHT / 2;
|
|
297
|
-
const textX = tx + LEGEND_TOGGLE_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
|
|
298
|
-
const textY = LEGEND_HEIGHT / 2;
|
|
299
|
-
|
|
300
|
-
toggleLayouts.push({
|
|
301
|
-
id: toggle.id,
|
|
302
|
-
label: toggle.label,
|
|
303
|
-
active: toggle.active,
|
|
304
|
-
dotCx,
|
|
305
|
-
dotCy,
|
|
306
|
-
textX,
|
|
307
|
-
textY,
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
tx +=
|
|
311
|
-
LEGEND_TOGGLE_DOT_R * 2 +
|
|
312
|
-
LEGEND_ENTRY_DOT_GAP +
|
|
313
|
-
measureLegendText(toggle.label, LEGEND_ENTRY_FONT_SIZE) +
|
|
314
|
-
LEGEND_ENTRY_TRAIL;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
return {
|
|
318
|
-
x: 0,
|
|
319
|
-
y: 0,
|
|
320
|
-
width: capsuleW,
|
|
321
|
-
height: LEGEND_HEIGHT,
|
|
322
|
-
expanded: true,
|
|
323
|
-
pill: {
|
|
324
|
-
x: LEGEND_CAPSULE_PAD,
|
|
325
|
-
y: LEGEND_CAPSULE_PAD,
|
|
326
|
-
width: LEGEND_GEAR_PILL_W - LEGEND_CAPSULE_PAD * 2,
|
|
327
|
-
height: pillH,
|
|
328
|
-
},
|
|
329
|
-
toggles: toggleLayouts,
|
|
330
|
-
};
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// ── Main layout computation ─────────────────────────────────
|
|
334
|
-
|
|
335
|
-
export function computeLegendLayout(
|
|
336
|
-
config: LegendConfig,
|
|
337
|
-
state: LegendState,
|
|
338
|
-
containerWidth: number
|
|
339
|
-
): LegendLayout {
|
|
340
|
-
const { groups, controls: configControls, mode } = config;
|
|
341
|
-
const isExport = mode === 'export';
|
|
342
|
-
|
|
343
|
-
// App-hosted controls: suppress the inline gear, reserve a header row + anchor.
|
|
344
|
-
const gated = isAppHostedControls(config, isExport);
|
|
345
|
-
|
|
346
|
-
// Filter groups for export: only active group shown
|
|
347
|
-
const activeGroupName = state.activeGroup?.toLowerCase() ?? null;
|
|
348
|
-
|
|
349
|
-
// In export mode with no active group and no groups, no legend
|
|
350
|
-
if (isExport && !activeGroupName) {
|
|
351
|
-
return {
|
|
352
|
-
height: 0,
|
|
353
|
-
width: 0,
|
|
354
|
-
rows: [],
|
|
355
|
-
controls: [],
|
|
356
|
-
pills: [],
|
|
357
|
-
};
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// Controls group (gear): suppressed in export and when app-hosted.
|
|
361
|
-
const controlsGroupLayout =
|
|
362
|
-
isExport || gated ? undefined : buildControlsGroupLayout(config, state);
|
|
363
|
-
|
|
364
|
-
const visibleGroups = config.showEmptyGroups
|
|
365
|
-
? groups
|
|
366
|
-
: groups.filter((g) => g.entries.length > 0 || !!g.gradient);
|
|
367
|
-
if (
|
|
368
|
-
visibleGroups.length === 0 &&
|
|
369
|
-
(!configControls || configControls.length === 0) &&
|
|
370
|
-
!controlsGroupLayout
|
|
371
|
-
) {
|
|
372
|
-
return {
|
|
373
|
-
height: 0,
|
|
374
|
-
width: 0,
|
|
375
|
-
rows: [],
|
|
376
|
-
controls: [],
|
|
377
|
-
pills: [],
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// Compute control layouts (right-aligned)
|
|
382
|
-
const controlLayouts: LegendControlLayout[] = [];
|
|
383
|
-
let totalControlsW = 0;
|
|
384
|
-
|
|
385
|
-
if (configControls && !isExport) {
|
|
386
|
-
for (const ctrl of configControls) {
|
|
387
|
-
const w = controlWidth(ctrl);
|
|
388
|
-
const children = ctrl.children?.map((c) => ({
|
|
389
|
-
id: c.id,
|
|
390
|
-
label: c.label,
|
|
391
|
-
x: 0,
|
|
392
|
-
y: 0,
|
|
393
|
-
width: measureLegendText(c.label, CONTROL_FONT_SIZE) + 12,
|
|
394
|
-
...(c.isActive !== undefined && { isActive: c.isActive }),
|
|
395
|
-
}));
|
|
396
|
-
controlLayouts.push({
|
|
397
|
-
id: ctrl.id,
|
|
398
|
-
x: 0, // positioned later
|
|
399
|
-
y: 0,
|
|
400
|
-
width: w,
|
|
401
|
-
height: LEGEND_HEIGHT,
|
|
402
|
-
icon: ctrl.icon,
|
|
403
|
-
...(ctrl.label !== undefined && { label: ctrl.label }),
|
|
404
|
-
exportBehavior: ctrl.exportBehavior,
|
|
405
|
-
...(children !== undefined && { children }),
|
|
406
|
-
});
|
|
407
|
-
totalControlsW += w + CONTROL_GAP;
|
|
408
|
-
}
|
|
409
|
-
if (totalControlsW > 0) totalControlsW -= CONTROL_GAP; // remove trailing gap
|
|
410
|
-
} else if (configControls && isExport) {
|
|
411
|
-
// In export, include controls with exportBehavior 'include' or 'static'
|
|
412
|
-
for (const ctrl of configControls) {
|
|
413
|
-
if (ctrl.exportBehavior === 'strip') continue;
|
|
414
|
-
const w = controlWidth(ctrl);
|
|
415
|
-
const children = ctrl.children?.map((c) => ({
|
|
416
|
-
id: c.id,
|
|
417
|
-
label: c.label,
|
|
418
|
-
x: 0,
|
|
419
|
-
y: 0,
|
|
420
|
-
width: measureLegendText(c.label, CONTROL_FONT_SIZE) + 12,
|
|
421
|
-
...(c.isActive !== undefined && { isActive: c.isActive }),
|
|
422
|
-
}));
|
|
423
|
-
controlLayouts.push({
|
|
424
|
-
id: ctrl.id,
|
|
425
|
-
x: 0,
|
|
426
|
-
y: 0,
|
|
427
|
-
width: w,
|
|
428
|
-
height: LEGEND_HEIGHT,
|
|
429
|
-
icon: ctrl.icon,
|
|
430
|
-
...(ctrl.label !== undefined && { label: ctrl.label }),
|
|
431
|
-
exportBehavior: ctrl.exportBehavior,
|
|
432
|
-
...(children !== undefined && { children }),
|
|
433
|
-
});
|
|
434
|
-
totalControlsW += w + CONTROL_GAP;
|
|
435
|
-
}
|
|
436
|
-
if (totalControlsW > 0) totalControlsW -= CONTROL_GAP;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// Available width for tag groups (controls anchor right, gear pill at end of pills)
|
|
440
|
-
const controlsSpace =
|
|
441
|
-
totalControlsW > 0 ? totalControlsW + LEGEND_GROUP_GAP * 2 : 0;
|
|
442
|
-
const gearSpace = controlsGroupLayout
|
|
443
|
-
? controlsGroupLayout.width + LEGEND_GROUP_GAP
|
|
444
|
-
: 0;
|
|
445
|
-
const groupAvailW = containerWidth - controlsSpace - gearSpace;
|
|
446
|
-
|
|
447
|
-
// Build pill/capsule layouts
|
|
448
|
-
const pills: LegendPillLayout[] = [];
|
|
449
|
-
let activeCapsule: LegendCapsuleLayout | undefined;
|
|
450
|
-
|
|
451
|
-
for (const g of visibleGroups) {
|
|
452
|
-
const isActive = activeGroupName === g.name.toLowerCase();
|
|
453
|
-
|
|
454
|
-
// In export mode, skip non-active groups
|
|
455
|
-
if (isExport && !isActive) continue;
|
|
456
|
-
|
|
457
|
-
if (isActive) {
|
|
458
|
-
activeCapsule = buildCapsuleLayout(
|
|
459
|
-
g,
|
|
460
|
-
groupAvailW,
|
|
461
|
-
config.capsulePillAddonWidth ?? 0
|
|
462
|
-
);
|
|
463
|
-
} else if (!activeGroupName || config.showInactivePills) {
|
|
464
|
-
const pw = pillWidth(g.name);
|
|
465
|
-
pills.push({
|
|
466
|
-
groupName: g.name,
|
|
467
|
-
x: 0,
|
|
468
|
-
y: 0,
|
|
469
|
-
width: pw,
|
|
470
|
-
height: LEGEND_HEIGHT,
|
|
471
|
-
isActive: false,
|
|
472
|
-
});
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
// Position elements in rows
|
|
477
|
-
const alignLeft = config.position.titleRelation === 'inline-with-title';
|
|
478
|
-
const rows = layoutRows(
|
|
479
|
-
activeCapsule,
|
|
480
|
-
pills,
|
|
481
|
-
controlLayouts,
|
|
482
|
-
groupAvailW,
|
|
483
|
-
containerWidth,
|
|
484
|
-
totalControlsW,
|
|
485
|
-
alignLeft,
|
|
486
|
-
controlsGroupLayout
|
|
487
|
-
);
|
|
488
|
-
|
|
489
|
-
// App-hosted controls: the controlsGroup was dropped above (no gear), and the
|
|
490
|
-
// app overlay strip pins itself to the top edge of the preview — dgmo reserves
|
|
491
|
-
// no row and emits no anchor, so the chart reclaims that space.
|
|
492
|
-
const height = rows.length * LEGEND_HEIGHT;
|
|
493
|
-
const width = containerWidth;
|
|
494
|
-
|
|
495
|
-
return {
|
|
496
|
-
height,
|
|
497
|
-
width,
|
|
498
|
-
rows,
|
|
499
|
-
...(activeCapsule !== undefined && { activeCapsule }),
|
|
500
|
-
controls: controlLayouts,
|
|
501
|
-
pills,
|
|
502
|
-
...(controlsGroupLayout !== undefined && {
|
|
503
|
-
controlsGroup: controlsGroupLayout,
|
|
504
|
-
}),
|
|
505
|
-
};
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// ── Build capsule for active group ──────────────────────────
|
|
509
|
-
|
|
510
|
-
function buildCapsuleLayout(
|
|
511
|
-
group: LegendGroupData,
|
|
512
|
-
containerWidth: number,
|
|
513
|
-
addonWidth = 0
|
|
514
|
-
): LegendCapsuleLayout {
|
|
515
|
-
if (group.gradient) return buildGradientCapsuleLayout(group, group.gradient);
|
|
516
|
-
const pw = pillWidth(group.name);
|
|
517
|
-
const info = capsuleWidth(
|
|
518
|
-
group.name,
|
|
519
|
-
group.entries,
|
|
520
|
-
containerWidth,
|
|
521
|
-
addonWidth
|
|
522
|
-
);
|
|
523
|
-
|
|
524
|
-
const pill: LegendPillLayout = {
|
|
525
|
-
groupName: group.name,
|
|
526
|
-
x: LEGEND_CAPSULE_PAD,
|
|
527
|
-
y: LEGEND_CAPSULE_PAD,
|
|
528
|
-
width: pw,
|
|
529
|
-
height: LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2,
|
|
530
|
-
isActive: true,
|
|
531
|
-
};
|
|
532
|
-
|
|
533
|
-
const entries: LegendEntryLayout[] = [];
|
|
534
|
-
const ex = LEGEND_CAPSULE_PAD + pw + 4 + addonWidth;
|
|
535
|
-
let ey = 0;
|
|
536
|
-
let rowX = ex;
|
|
537
|
-
// Right boundary: one LEGEND_CAPSULE_PAD for right padding.
|
|
538
|
-
// Left padding is already in ex/rowX starting position.
|
|
539
|
-
const maxRowW = containerWidth - LEGEND_CAPSULE_PAD;
|
|
540
|
-
let currentRow = 0;
|
|
541
|
-
|
|
542
|
-
for (let i = 0; i < info.visibleEntries; i++) {
|
|
543
|
-
const entry = group.entries[i]!;
|
|
544
|
-
let ew = entryWidth(entry.value);
|
|
545
|
-
|
|
546
|
-
if (info.entryRows > 1 && rowX + ew > maxRowW && rowX > ex && i > 0) {
|
|
547
|
-
currentRow++;
|
|
548
|
-
rowX = ex;
|
|
549
|
-
ey = currentRow * LEGEND_HEIGHT;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
let displayValue: string | undefined;
|
|
553
|
-
const availW = maxRowW - rowX;
|
|
554
|
-
if (ew > availW) {
|
|
555
|
-
const textAvail =
|
|
556
|
-
availW - (LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + LEGEND_ENTRY_TRAIL);
|
|
557
|
-
if (textAvail > 0) {
|
|
558
|
-
displayValue = truncateLegendText(
|
|
559
|
-
entry.value,
|
|
560
|
-
LEGEND_ENTRY_FONT_SIZE,
|
|
561
|
-
textAvail
|
|
562
|
-
);
|
|
563
|
-
ew =
|
|
564
|
-
LEGEND_DOT_R * 2 +
|
|
565
|
-
LEGEND_ENTRY_DOT_GAP +
|
|
566
|
-
measureLegendText(displayValue, LEGEND_ENTRY_FONT_SIZE) +
|
|
567
|
-
LEGEND_ENTRY_TRAIL;
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
const dotCx = rowX + LEGEND_DOT_R;
|
|
572
|
-
const dotCy = ey + LEGEND_HEIGHT / 2;
|
|
573
|
-
const textX = rowX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
|
|
574
|
-
const textY = ey + LEGEND_HEIGHT / 2;
|
|
575
|
-
|
|
576
|
-
entries.push({
|
|
577
|
-
value: entry.value,
|
|
578
|
-
color: entry.color,
|
|
579
|
-
x: rowX,
|
|
580
|
-
y: ey,
|
|
581
|
-
width: ew,
|
|
582
|
-
dotCx,
|
|
583
|
-
dotCy,
|
|
584
|
-
textX,
|
|
585
|
-
textY,
|
|
586
|
-
...(displayValue !== undefined && { displayValue }),
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
rowX += ew;
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
const totalRows = info.entryRows;
|
|
593
|
-
const capsuleH = totalRows * LEGEND_HEIGHT;
|
|
594
|
-
|
|
595
|
-
return {
|
|
596
|
-
groupName: group.name,
|
|
597
|
-
x: 0,
|
|
598
|
-
y: 0,
|
|
599
|
-
width: info.width,
|
|
600
|
-
height: capsuleH,
|
|
601
|
-
pill,
|
|
602
|
-
entries,
|
|
603
|
-
...(info.moreCount > 0 && { moreCount: info.moreCount }),
|
|
604
|
-
...(addonWidth > 0 && { addonX: LEGEND_CAPSULE_PAD + pw + 4 }),
|
|
605
|
-
};
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
// ── Row layout ──────────────────────────────────────────────
|
|
609
|
-
|
|
610
|
-
function layoutRows(
|
|
611
|
-
activeCapsule: LegendCapsuleLayout | undefined,
|
|
612
|
-
pills: LegendPillLayout[],
|
|
613
|
-
controls: LegendControlLayout[],
|
|
614
|
-
groupAvailW: number,
|
|
615
|
-
containerWidth: number,
|
|
616
|
-
totalControlsW: number,
|
|
617
|
-
alignLeft = false,
|
|
618
|
-
controlsGroup?: ControlsGroupLayout
|
|
619
|
-
): Array<{
|
|
620
|
-
y: number;
|
|
621
|
-
items: Array<LegendPillLayout | LegendCapsuleLayout | LegendControlLayout>;
|
|
622
|
-
}> {
|
|
623
|
-
const rows: Array<{
|
|
624
|
-
y: number;
|
|
625
|
-
items: Array<LegendPillLayout | LegendCapsuleLayout | LegendControlLayout>;
|
|
626
|
-
}> = [];
|
|
627
|
-
|
|
628
|
-
// Collect all group-side items in display order: active capsule first, then collapsed pills
|
|
629
|
-
const groupItems: Array<LegendPillLayout | LegendCapsuleLayout> = [];
|
|
630
|
-
if (activeCapsule) groupItems.push(activeCapsule);
|
|
631
|
-
groupItems.push(...pills);
|
|
632
|
-
|
|
633
|
-
// Controls group width for centering offset
|
|
634
|
-
const gearW = controlsGroup ? controlsGroup.width + LEGEND_GROUP_GAP : 0;
|
|
635
|
-
|
|
636
|
-
// Compute total group items width
|
|
637
|
-
let currentRowItems: Array<
|
|
638
|
-
LegendPillLayout | LegendCapsuleLayout | LegendControlLayout
|
|
639
|
-
> = [];
|
|
640
|
-
let currentRowW = 0;
|
|
641
|
-
let rowY = 0;
|
|
642
|
-
|
|
643
|
-
for (const item of groupItems) {
|
|
644
|
-
const itemW = item.width + LEGEND_GROUP_GAP;
|
|
645
|
-
if (currentRowW + item.width > groupAvailW && currentRowItems.length > 0) {
|
|
646
|
-
// Commit current row (row 0 needs gear space deducted for centering)
|
|
647
|
-
if (!alignLeft) {
|
|
648
|
-
const rowGearW = rows.length === 0 ? gearW : 0;
|
|
649
|
-
centerRowItems(
|
|
650
|
-
currentRowItems,
|
|
651
|
-
containerWidth,
|
|
652
|
-
totalControlsW,
|
|
653
|
-
rowGearW
|
|
654
|
-
);
|
|
655
|
-
}
|
|
656
|
-
rows.push({ y: rowY, items: currentRowItems });
|
|
657
|
-
rowY += LEGEND_HEIGHT;
|
|
658
|
-
currentRowItems = [];
|
|
659
|
-
currentRowW = 0;
|
|
660
|
-
}
|
|
661
|
-
item.x = currentRowW;
|
|
662
|
-
item.y = rowY;
|
|
663
|
-
currentRowItems.push(item);
|
|
664
|
-
currentRowW += itemW;
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
// Add controls to first row (right-aligned)
|
|
668
|
-
if (controls.length > 0) {
|
|
669
|
-
let cx = containerWidth;
|
|
670
|
-
for (let i = controls.length - 1; i >= 0; i--) {
|
|
671
|
-
const ctrl = controls[i]!;
|
|
672
|
-
cx -= ctrl.width;
|
|
673
|
-
ctrl.x = cx;
|
|
674
|
-
ctrl.y = 0;
|
|
675
|
-
cx -= CONTROL_GAP;
|
|
676
|
-
}
|
|
677
|
-
// Controls go on first row
|
|
678
|
-
if (rows.length > 0) {
|
|
679
|
-
rows[0]!.items.push(...controls);
|
|
680
|
-
} else if (currentRowItems.length > 0) {
|
|
681
|
-
currentRowItems.push(...controls);
|
|
682
|
-
} else {
|
|
683
|
-
// Only controls, no groups
|
|
684
|
-
currentRowItems.push(...controls);
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
// Commit last row
|
|
689
|
-
if (currentRowItems.length > 0) {
|
|
690
|
-
if (!alignLeft) {
|
|
691
|
-
centerRowItems(currentRowItems, containerWidth, totalControlsW, gearW);
|
|
692
|
-
}
|
|
693
|
-
rows.push({ y: rowY, items: currentRowItems });
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
// Position controls group AFTER centering so it follows the shifted items
|
|
697
|
-
if (controlsGroup) {
|
|
698
|
-
const row0Items = rows[0]?.items ?? [];
|
|
699
|
-
const groupItemsInRow0 = row0Items.filter(
|
|
700
|
-
(it) => 'groupName' in it
|
|
701
|
-
) as Array<LegendPillLayout | LegendCapsuleLayout>;
|
|
702
|
-
if (groupItemsInRow0.length > 0) {
|
|
703
|
-
const last = groupItemsInRow0[groupItemsInRow0.length - 1]!;
|
|
704
|
-
controlsGroup.x = last.x + last.width + LEGEND_GROUP_GAP;
|
|
705
|
-
} else {
|
|
706
|
-
// No group items — center the controls group
|
|
707
|
-
controlsGroup.x = (containerWidth - controlsGroup.width) / 2;
|
|
708
|
-
}
|
|
709
|
-
controlsGroup.y = 0;
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
// Ensure at least one row height
|
|
713
|
-
if (rows.length === 0) {
|
|
714
|
-
rows.push({ y: 0, items: [] });
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
return rows;
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
function centerRowItems(
|
|
721
|
-
items: Array<LegendPillLayout | LegendCapsuleLayout | LegendControlLayout>,
|
|
722
|
-
containerWidth: number,
|
|
723
|
-
totalControlsW: number,
|
|
724
|
-
controlsGroupW = 0
|
|
725
|
-
): void {
|
|
726
|
-
// Only center group items (pills and capsules), not controls
|
|
727
|
-
const groupItems = items.filter((it) => 'groupName' in it) as Array<
|
|
728
|
-
LegendPillLayout | LegendCapsuleLayout
|
|
729
|
-
>;
|
|
730
|
-
|
|
731
|
-
if (groupItems.length === 0) return;
|
|
732
|
-
|
|
733
|
-
const totalGroupW =
|
|
734
|
-
groupItems.reduce((s, it) => s + it.width, 0) +
|
|
735
|
-
(groupItems.length - 1) * LEGEND_GROUP_GAP;
|
|
736
|
-
|
|
737
|
-
const availW =
|
|
738
|
-
containerWidth -
|
|
739
|
-
(totalControlsW > 0 ? totalControlsW + LEGEND_GROUP_GAP * 2 : 0) -
|
|
740
|
-
controlsGroupW;
|
|
741
|
-
const offset = Math.max(0, (availW - totalGroupW) / 2);
|
|
742
|
-
|
|
743
|
-
let x = offset;
|
|
744
|
-
for (const item of groupItems) {
|
|
745
|
-
item.x = x;
|
|
746
|
-
x += item.width + LEGEND_GROUP_GAP;
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
// ── Public: height reservation ──────────────────────────────
|
|
751
|
-
|
|
752
|
-
export function getLegendReservedHeight(
|
|
753
|
-
config: LegendConfig,
|
|
754
|
-
state: LegendState,
|
|
755
|
-
containerWidth: number
|
|
756
|
-
): number {
|
|
757
|
-
const layout = computeLegendLayout(config, state, containerWidth);
|
|
758
|
-
return layout.height;
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
export function getMaxLegendReservedHeight(
|
|
762
|
-
config: LegendConfig,
|
|
763
|
-
containerWidth: number
|
|
764
|
-
): number {
|
|
765
|
-
const groups = config.groups;
|
|
766
|
-
if (groups.length === 0) return LEGEND_HEIGHT;
|
|
767
|
-
const addon = config.capsulePillAddonWidth ?? 0;
|
|
768
|
-
let max = LEGEND_HEIGHT;
|
|
769
|
-
for (const g of groups) {
|
|
770
|
-
if (g.entries.length === 0) continue;
|
|
771
|
-
const info = capsuleWidth(g.name, g.entries, containerWidth, addon);
|
|
772
|
-
const h = info.entryRows * LEGEND_HEIGHT;
|
|
773
|
-
if (h > max) max = h;
|
|
774
|
-
}
|
|
775
|
-
return max;
|
|
776
|
-
}
|