@diagrammo/dgmo 0.47.0 → 0.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auto.js +1 -1
- package/dist/auto.mjs +1 -1
- package/dist/element.js +1 -1
- package/dist/element.mjs +1 -1
- package/package.json +19 -70
- 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/treemap/renderer.ts
DELETED
|
@@ -1,572 +0,0 @@
|
|
|
1
|
-
// ============================================================
|
|
2
|
-
// Treemap — D3 SVG Renderer
|
|
3
|
-
// ============================================================
|
|
4
|
-
//
|
|
5
|
-
// Emits a static, full-tree treemap. Interactive chrome (drill breadcrumb,
|
|
6
|
-
// tooltips, the runtime color-mode switcher) lives app-side; the only
|
|
7
|
-
// interactive affordance drawn here is the scope/target focus icon, marked
|
|
8
|
-
// `data-export-ignore` so `finalizeSvgExport` strips it on export.
|
|
9
|
-
|
|
10
|
-
import * as d3Selection from 'd3-selection';
|
|
11
|
-
import { FONT_FAMILY } from '../fonts';
|
|
12
|
-
import { contrastText, getSeriesColors, mix } from '../palettes/color-utils';
|
|
13
|
-
import { tagAttrKey } from '../utils/tag-groups';
|
|
14
|
-
import { measureText } from '../utils/text-measure';
|
|
15
|
-
import { renderIntegratedLegend } from '../utils/legend-integration';
|
|
16
|
-
import { getMaxLegendReservedHeight } from '../utils/legend-layout';
|
|
17
|
-
import { LEGEND_GROUP_GAP } from '../utils/legend-constants';
|
|
18
|
-
import type { LegendPosition } from '../utils/legend-types';
|
|
19
|
-
import {
|
|
20
|
-
TITLE_FONT_SIZE,
|
|
21
|
-
TITLE_FONT_WEIGHT,
|
|
22
|
-
TITLE_Y,
|
|
23
|
-
} from '../utils/title-constants';
|
|
24
|
-
import type { PaletteColors } from '../palettes';
|
|
25
|
-
import type { D3ExportDimensions } from '../utils/d3-types';
|
|
26
|
-
import type { ParsedTreemap, TreemapColorMode } from './types';
|
|
27
|
-
import { layoutTreemap } from './layout';
|
|
28
|
-
import {
|
|
29
|
-
buildHeatScale,
|
|
30
|
-
buildLegend,
|
|
31
|
-
compactNumber,
|
|
32
|
-
formatPct,
|
|
33
|
-
resolveCellColor,
|
|
34
|
-
resolveColorMode,
|
|
35
|
-
type CellColorContext,
|
|
36
|
-
} from './treemap-shared';
|
|
37
|
-
|
|
38
|
-
const PADDING = 12;
|
|
39
|
-
const HEADER_H = 18;
|
|
40
|
-
const TITLE_BAND = 36;
|
|
41
|
-
/** Percent of a leaf's own color kept when muting it toward the background. */
|
|
42
|
-
const LEAF_MUTE_PCT = 50;
|
|
43
|
-
|
|
44
|
-
/** Standard placement every dgmo legend uses: centered, under the title. */
|
|
45
|
-
const LEGEND_POSITION: LegendPosition = {
|
|
46
|
-
placement: 'top-center',
|
|
47
|
-
titleRelation: 'below-title',
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
export interface TreemapRenderOptions {
|
|
51
|
-
/** Color mode override (app's runtime switcher). Defaults to source. */
|
|
52
|
-
colorMode?: TreemapColorMode;
|
|
53
|
-
/** Render budget (interactive only). Export omits it → full tree. */
|
|
54
|
-
maxDepth?: number;
|
|
55
|
-
/** Shift the branch-hue index so a drilled-into view keeps the color it had
|
|
56
|
-
* when expanded (the re-rooted node would otherwise become index 0 = the
|
|
57
|
-
* first hue). Set to the drilled branch's original top-level index. */
|
|
58
|
-
colorOffset?: number;
|
|
59
|
-
/** Click handler for drillable cells (app interactivity). */
|
|
60
|
-
onClickItem?: (lineNumber: number) => void;
|
|
61
|
-
/** Color-mode switch fired when a legend pill is clicked (app interactivity).
|
|
62
|
-
* The mode switcher is baked into the legend (clickable group pills), so the
|
|
63
|
-
* app no longer renders a separate overlay control. */
|
|
64
|
-
onSelectMode?: (mode: TreemapColorMode) => void;
|
|
65
|
-
exportMode?: boolean;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** Render for CLI/export (full tree, no drill chrome). */
|
|
69
|
-
export function renderTreemapForExport(
|
|
70
|
-
container: HTMLDivElement,
|
|
71
|
-
parsed: ParsedTreemap,
|
|
72
|
-
palette: PaletteColors,
|
|
73
|
-
isDark: boolean,
|
|
74
|
-
exportDims?: D3ExportDimensions,
|
|
75
|
-
options?: TreemapRenderOptions
|
|
76
|
-
): void {
|
|
77
|
-
renderTreemap(container, parsed, palette, isDark, exportDims, {
|
|
78
|
-
exportMode: true,
|
|
79
|
-
...options,
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function renderTreemap(
|
|
84
|
-
container: HTMLDivElement,
|
|
85
|
-
parsed: ParsedTreemap,
|
|
86
|
-
palette: PaletteColors,
|
|
87
|
-
isDark: boolean,
|
|
88
|
-
exportDims?: D3ExportDimensions,
|
|
89
|
-
options: TreemapRenderOptions = {}
|
|
90
|
-
): void {
|
|
91
|
-
if (parsed.error || parsed.roots.length === 0) return;
|
|
92
|
-
void isDark; // colors derive from the palette; kept for handler-signature parity.
|
|
93
|
-
|
|
94
|
-
d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
|
|
95
|
-
const width = exportDims?.width ?? container.clientWidth;
|
|
96
|
-
const height = exportDims?.height ?? container.clientHeight;
|
|
97
|
-
if (width <= 0 || height <= 0) return;
|
|
98
|
-
|
|
99
|
-
const mode = resolveColorMode(parsed, options.colorMode);
|
|
100
|
-
const opts = parsed.options;
|
|
101
|
-
const solid = opts.solidFill;
|
|
102
|
-
const exportMode = options.exportMode ?? false;
|
|
103
|
-
const seriesColorsTop = getSeriesColors(palette);
|
|
104
|
-
const showTitle = !!parsed.title;
|
|
105
|
-
const titleH = showTitle ? TITLE_BAND : 0;
|
|
106
|
-
|
|
107
|
-
const heat = buildHeatScale(parsed, palette);
|
|
108
|
-
const legend = !opts.noLegend
|
|
109
|
-
? buildLegend(mode, parsed, heat, seriesColorsTop, options.colorOffset ?? 0)
|
|
110
|
-
: null;
|
|
111
|
-
|
|
112
|
-
// Reserve a band below the title for the standardized legend, exactly like
|
|
113
|
-
// mindmap/org/map (top-center, below-title). The treemap fills the rest.
|
|
114
|
-
const legendReserve = legend
|
|
115
|
-
? getMaxLegendReservedHeight(
|
|
116
|
-
{ groups: legend.groups, position: LEGEND_POSITION, mode: 'preview' },
|
|
117
|
-
width
|
|
118
|
-
) + LEGEND_GROUP_GAP
|
|
119
|
-
: 0;
|
|
120
|
-
|
|
121
|
-
const headerH = opts.noHeaders ? 0 : HEADER_H;
|
|
122
|
-
const areaX = PADDING;
|
|
123
|
-
const areaY = titleH + legendReserve + PADDING;
|
|
124
|
-
const areaW = Math.max(1, width - PADDING * 2);
|
|
125
|
-
const areaH = Math.max(1, height - areaY - PADDING);
|
|
126
|
-
|
|
127
|
-
const layout = layoutTreemap(parsed.roots, {
|
|
128
|
-
width: areaW,
|
|
129
|
-
height: areaH,
|
|
130
|
-
headerH,
|
|
131
|
-
...(options.maxDepth !== undefined && { maxDepth: options.maxDepth }),
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
// ── SVG root ───────────────────────────────────────────────
|
|
135
|
-
const svg = d3Selection
|
|
136
|
-
.select(container)
|
|
137
|
-
.append('svg')
|
|
138
|
-
.attr('width', width)
|
|
139
|
-
.attr('height', height)
|
|
140
|
-
.attr('viewBox', `0 0 ${width} ${height}`)
|
|
141
|
-
.attr('preserveAspectRatio', 'xMidYMin meet')
|
|
142
|
-
.attr('xmlns', 'http://www.w3.org/2000/svg')
|
|
143
|
-
.attr('class', 'dgmo-treemap')
|
|
144
|
-
.style('font-family', FONT_FAMILY);
|
|
145
|
-
|
|
146
|
-
// Drill icon: faint by default, a touch clearer when its cell is hovered.
|
|
147
|
-
svg
|
|
148
|
-
.append('style')
|
|
149
|
-
.text(
|
|
150
|
-
'.dgmo-treemap-focus{opacity:.3;transition:opacity .12s}' +
|
|
151
|
-
'.dgmo-treemap-cell:hover .dgmo-treemap-focus{opacity:.7}'
|
|
152
|
-
);
|
|
153
|
-
|
|
154
|
-
svg
|
|
155
|
-
.append('rect')
|
|
156
|
-
.attr('width', width)
|
|
157
|
-
.attr('height', height)
|
|
158
|
-
.attr('fill', palette.bg);
|
|
159
|
-
|
|
160
|
-
if (showTitle) {
|
|
161
|
-
const title = svg
|
|
162
|
-
.append('text')
|
|
163
|
-
.attr('class', 'chart-title')
|
|
164
|
-
.attr('x', width / 2)
|
|
165
|
-
.attr('y', TITLE_Y)
|
|
166
|
-
.attr('text-anchor', 'middle')
|
|
167
|
-
.attr('fill', palette.text)
|
|
168
|
-
.attr('font-family', FONT_FAMILY)
|
|
169
|
-
.attr('font-size', TITLE_FONT_SIZE)
|
|
170
|
-
.attr('font-weight', TITLE_FONT_WEIGHT)
|
|
171
|
-
.text(parsed.title);
|
|
172
|
-
if (parsed.titleLineNumber !== null) {
|
|
173
|
-
title.attr('data-line-number', parsed.titleLineNumber);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const root = svg
|
|
178
|
-
.append('g')
|
|
179
|
-
.attr('class', 'dgmo-treemap-cells')
|
|
180
|
-
.attr('transform', `translate(${areaX},${areaY})`);
|
|
181
|
-
|
|
182
|
-
const seriesColors = seriesColorsTop;
|
|
183
|
-
const colorOffset = options.colorOffset ?? 0;
|
|
184
|
-
const activeGroup =
|
|
185
|
-
parsed.tagGroups.length > 0 ? parsed.tagGroups[0]!.name : null;
|
|
186
|
-
|
|
187
|
-
// Branch hue is keyed off the top-level branch's SOURCE order (not d3's
|
|
188
|
-
// value-sorted order) so the cell colors match the legend, which lists roots
|
|
189
|
-
// in source order. `colorOffset` shifts the index so a drilled-into branch
|
|
190
|
-
// keeps the hue it had at the top level.
|
|
191
|
-
const rootIndexByLabel = new Map(parsed.roots.map((r, i) => [r.label, i]));
|
|
192
|
-
|
|
193
|
-
const colorCtx: CellColorContext = {
|
|
194
|
-
mode,
|
|
195
|
-
heat,
|
|
196
|
-
tagGroups: parsed.tagGroups,
|
|
197
|
-
activeGroup,
|
|
198
|
-
rootIndexByLabel,
|
|
199
|
-
seriesColors,
|
|
200
|
-
colorOffset,
|
|
201
|
-
bg: palette.bg,
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
// ── Cells ──────────────────────────────────────────────────
|
|
205
|
-
for (const cell of layout.cells) {
|
|
206
|
-
const w = Math.max(0, cell.x1 - cell.x0);
|
|
207
|
-
const h = Math.max(0, cell.y1 - cell.y0);
|
|
208
|
-
if (w <= 0 || h <= 0) continue;
|
|
209
|
-
|
|
210
|
-
// Emphasis: the containing box (header band) is pure color; the internal
|
|
211
|
-
// leaf cells are muted (mixed toward the background). Both are opaque so the
|
|
212
|
-
// pure container behind a leaf doesn't bleed through.
|
|
213
|
-
const baseColor = resolveCellColor(cell, colorCtx);
|
|
214
|
-
const fill = cell.isContainer
|
|
215
|
-
? baseColor
|
|
216
|
-
: solid
|
|
217
|
-
? baseColor
|
|
218
|
-
: mix(baseColor, palette.bg, LEAF_MUTE_PCT);
|
|
219
|
-
const drillable = cell.isContainer || cell.isCollapsed;
|
|
220
|
-
|
|
221
|
-
const g = root
|
|
222
|
-
.append('g')
|
|
223
|
-
.attr('class', 'dgmo-treemap-cell')
|
|
224
|
-
.attr('transform', `translate(${cell.x0},${cell.y0})`)
|
|
225
|
-
.attr('data-node-path', cell.path.join(' / '));
|
|
226
|
-
if (cell.lineNumber !== undefined) {
|
|
227
|
-
g.attr('data-line-number', cell.lineNumber);
|
|
228
|
-
}
|
|
229
|
-
if (cell.heat !== undefined) g.attr('data-heat', cell.heat);
|
|
230
|
-
if (cell.node && activeGroup) {
|
|
231
|
-
const key = tagAttrKey(activeGroup);
|
|
232
|
-
const tagVal = cell.node.metadata[key];
|
|
233
|
-
if (tagVal) g.attr(`data-tag-${key}`, tagVal);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const rect = g
|
|
237
|
-
.append('rect')
|
|
238
|
-
.attr(
|
|
239
|
-
'class',
|
|
240
|
-
cell.isContainer ? 'dgmo-treemap-rect branch' : 'dgmo-treemap-rect'
|
|
241
|
-
)
|
|
242
|
-
.attr('width', w)
|
|
243
|
-
.attr('height', h)
|
|
244
|
-
.attr('rx', 2)
|
|
245
|
-
.attr('fill', fill)
|
|
246
|
-
.attr('fill-opacity', 1)
|
|
247
|
-
.attr('stroke', palette.bg)
|
|
248
|
-
// No stroke on internal leaf shapes — the paddingInner gap already
|
|
249
|
-
// separates them; only the containing box keeps a frame.
|
|
250
|
-
.attr('stroke-width', cell.isContainer ? 1 : 0);
|
|
251
|
-
|
|
252
|
-
if (drillable && options.onClickItem && cell.lineNumber !== undefined) {
|
|
253
|
-
const ln = cell.lineNumber;
|
|
254
|
-
rect
|
|
255
|
-
.style('cursor', 'pointer')
|
|
256
|
-
.on('click', () => options.onClickItem!(ln));
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const ink = contrastText(
|
|
260
|
-
fill,
|
|
261
|
-
palette.textOnFillLight,
|
|
262
|
-
palette.textOnFillDark
|
|
263
|
-
);
|
|
264
|
-
|
|
265
|
-
// Header bar: group name (left) + its aggregate value/% (right) so a
|
|
266
|
-
// container's share is readable without drilling.
|
|
267
|
-
if (cell.isContainer && headerH > 0 && w > 34 && h > headerH) {
|
|
268
|
-
const valParts: string[] = [];
|
|
269
|
-
if (!opts.noValues) valParts.push(compactNumber(cell.value));
|
|
270
|
-
if (!opts.noPercent) valParts.push(formatPct(cell.pctOfRoot));
|
|
271
|
-
const valStr = valParts.join(' · ');
|
|
272
|
-
// Only reserve space for the focus icon when one is actually drawn (a
|
|
273
|
-
// non-drillable container's header value sits flush right).
|
|
274
|
-
const ICON_RESERVE = drillable ? 16 : 4;
|
|
275
|
-
const showVal = valStr.length > 0 && w > 110;
|
|
276
|
-
const valW = showVal ? measureText(valStr, 10.5) : 0;
|
|
277
|
-
const labelMax = w - 8 - ICON_RESERVE - (showVal ? valW + 10 : 0);
|
|
278
|
-
|
|
279
|
-
g.append('text')
|
|
280
|
-
.attr('class', 'dgmo-treemap-header')
|
|
281
|
-
.attr('x', 6)
|
|
282
|
-
.attr('y', 13)
|
|
283
|
-
.attr('font-size', 11)
|
|
284
|
-
.attr('font-weight', 700)
|
|
285
|
-
.attr('fill', ink)
|
|
286
|
-
.text(clipLabel(cell.label, Math.max(0, labelMax), 11));
|
|
287
|
-
|
|
288
|
-
if (showVal) {
|
|
289
|
-
g.append('text')
|
|
290
|
-
.attr('class', 'dgmo-treemap-header-value')
|
|
291
|
-
.attr('x', w - ICON_RESERVE - 4)
|
|
292
|
-
.attr('y', 13)
|
|
293
|
-
.attr('text-anchor', 'end')
|
|
294
|
-
.attr('font-size', 10.5)
|
|
295
|
-
.attr('fill', ink)
|
|
296
|
-
.attr('opacity', 0.75)
|
|
297
|
-
.text(valStr);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Leaf / collapsed-block labels. The name scales with the cell so a big
|
|
302
|
-
// shape fills its space; the bigger the label, the more it's muted — a soft
|
|
303
|
-
// watermark on large cells, crisp on small ones. Fit-to-width so the name
|
|
304
|
-
// never has to truncate just because it scaled up.
|
|
305
|
-
// Pad scales down on small cells so a tight label (e.g. a rolled-up
|
|
306
|
-
// "Trivia" leaf) still fits instead of truncating; big cells keep the roomy
|
|
307
|
-
// 14px gutter. MIN_FS lets the name shrink well below the comfortable 12px
|
|
308
|
-
// floor on cramped cells — readability yields to showing the whole word.
|
|
309
|
-
const PAD = Math.max(4, Math.min(14, Math.floor(Math.min(w, h) / 5)));
|
|
310
|
-
const MIN_FS = 7;
|
|
311
|
-
if (!cell.isContainer && w >= 2 * PAD + 8 && h >= 2 * PAD + 8) {
|
|
312
|
-
const maxW = w - 2 * PAD;
|
|
313
|
-
|
|
314
|
-
// Vertical fallback: when the name can't fit horizontally even at the
|
|
315
|
-
// floor font, the cell is clearly taller than wide, and rotating buys
|
|
316
|
-
// real length (height beats the horizontal budget), run the name down the
|
|
317
|
-
// cell instead of truncating it. Value/% are dropped in this mode — a
|
|
318
|
-
// narrow tile has no room for a second column, and showing the whole name
|
|
319
|
-
// is the point. Skips the horizontal block entirely when it fires.
|
|
320
|
-
const horizMinW = measureText(cell.label, MIN_FS) * 1.06;
|
|
321
|
-
if (horizMinW > maxW && h > w * 1.25 && h - 2 * PAD > maxW) {
|
|
322
|
-
drawVerticalLabel(g, cell.label, w, h, PAD, ink);
|
|
323
|
-
if (drillable && w > 30 && h > 22) drawFocusIcon(g, w, ink, 9);
|
|
324
|
-
continue;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
let fs = clamp(Math.round(Math.min(w / 3.6, h / 3.4)), MIN_FS, 72);
|
|
328
|
-
// Safety factor: text-measure under-estimates bold glyph widths, so shrink
|
|
329
|
-
// a touch more to guarantee the PAD gap from the right edge.
|
|
330
|
-
const nameW = measureText(cell.label, fs) * 1.06;
|
|
331
|
-
if (nameW > maxW) fs = Math.max(MIN_FS, Math.floor((fs * maxW) / nameW));
|
|
332
|
-
const tg = clamp((fs - MIN_FS) / (72 - MIN_FS), 0, 1);
|
|
333
|
-
const nameOpacity = 0.95 - tg * 0.5; // 0.95 small → 0.45 large
|
|
334
|
-
const vfs = clamp(Math.round(fs * 0.42), 11, 28);
|
|
335
|
-
const valOpacity = Math.max(0.45, nameOpacity - 0.1);
|
|
336
|
-
|
|
337
|
-
let y = PAD + Math.round(fs * 0.78); // top PAD above the cap height
|
|
338
|
-
g.append('text')
|
|
339
|
-
.attr('class', 'dgmo-treemap-label')
|
|
340
|
-
.attr('x', PAD)
|
|
341
|
-
.attr('y', y)
|
|
342
|
-
.attr('font-size', fs)
|
|
343
|
-
.attr('font-weight', 600)
|
|
344
|
-
.attr('fill', ink)
|
|
345
|
-
.attr('opacity', nameOpacity)
|
|
346
|
-
.text(clipLabel(cell.label, maxW, fs));
|
|
347
|
-
|
|
348
|
-
const valParts = [
|
|
349
|
-
opts.noValues ? '' : compactNumber(cell.value),
|
|
350
|
-
opts.noPercent ? '' : formatPct(cell.pctOfRoot),
|
|
351
|
-
].filter(Boolean);
|
|
352
|
-
const combined = valParts.join(' · ');
|
|
353
|
-
const nameBaseY = y;
|
|
354
|
-
const addLineAt = (
|
|
355
|
-
text: string,
|
|
356
|
-
yy: number,
|
|
357
|
-
size: number,
|
|
358
|
-
opacity: number
|
|
359
|
-
): void => {
|
|
360
|
-
g.append('text')
|
|
361
|
-
.attr('class', 'dgmo-treemap-value')
|
|
362
|
-
.attr('x', PAD)
|
|
363
|
-
.attr('y', yy)
|
|
364
|
-
.attr('font-size', size)
|
|
365
|
-
.attr('fill', ink)
|
|
366
|
-
.attr('opacity', opacity)
|
|
367
|
-
.text(text);
|
|
368
|
-
};
|
|
369
|
-
|
|
370
|
-
if (combined) {
|
|
371
|
-
const fitsW = measureText(combined, vfs) <= maxW;
|
|
372
|
-
const valY = y + lineDrop(fs, vfs);
|
|
373
|
-
const roomBelow = valY + descent(vfs) <= h - PAD;
|
|
374
|
-
if (fitsW && roomBelow) {
|
|
375
|
-
// Preferred: value · % on its own line under the name.
|
|
376
|
-
addLineAt(combined, valY, vfs, valOpacity);
|
|
377
|
-
} else if (fitsW) {
|
|
378
|
-
// Short cell: no room below → trail the value after the name,
|
|
379
|
-
// smaller, on the same line (if it fits the width).
|
|
380
|
-
const ivfs = clamp(Math.round(fs * 0.5), 11, Math.max(11, vfs));
|
|
381
|
-
const rendered = clipLabel(cell.label, maxW, fs);
|
|
382
|
-
// Safety factor: text-measure under-estimates bold glyph width, so pad
|
|
383
|
-
// it to the worst case (and add a real gap) or the value collides with
|
|
384
|
-
// the name's end (e.g. "Hardtack70").
|
|
385
|
-
const nW = measureText(rendered, fs) * 1.12;
|
|
386
|
-
const gap = Math.max(12, Math.round(fs * 0.25));
|
|
387
|
-
if (PAD + nW + gap + measureText(combined, ivfs) <= w - PAD) {
|
|
388
|
-
g.append('text')
|
|
389
|
-
.attr('class', 'dgmo-treemap-value')
|
|
390
|
-
.attr('x', PAD + nW + gap)
|
|
391
|
-
.attr('y', nameBaseY)
|
|
392
|
-
.attr('font-size', ivfs)
|
|
393
|
-
.attr('fill', ink)
|
|
394
|
-
.attr('opacity', valOpacity)
|
|
395
|
-
.text(combined);
|
|
396
|
-
}
|
|
397
|
-
} else if (valParts.length === 2) {
|
|
398
|
-
// Tall-narrow cell: combined too wide → stack value then % below.
|
|
399
|
-
const d1 = lineDrop(fs, vfs);
|
|
400
|
-
if (y + d1 + descent(vfs) <= h - PAD) {
|
|
401
|
-
y += d1;
|
|
402
|
-
addLineAt(valParts[0]!, y, vfs, valOpacity);
|
|
403
|
-
}
|
|
404
|
-
const pfs = Math.max(10, Math.round(vfs * 0.9));
|
|
405
|
-
const d2 = lineDrop(vfs, pfs);
|
|
406
|
-
if (y + d2 + descent(pfs) <= h - PAD) {
|
|
407
|
-
y += d2;
|
|
408
|
-
addLineAt(valParts[1]!, y, pfs, valOpacity);
|
|
409
|
-
}
|
|
410
|
-
} else {
|
|
411
|
-
const onlyY = y + lineDrop(fs, vfs);
|
|
412
|
-
if (onlyY + descent(vfs) <= h - PAD) {
|
|
413
|
-
addLineAt(combined, onlyY, vfs, valOpacity);
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// Scope/target focus icon on drillable cells (interactive-only).
|
|
420
|
-
if (drillable && w > 30 && h > 22) {
|
|
421
|
-
drawFocusIcon(g, w, ink, 9);
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
if (legend) {
|
|
426
|
-
const legendG = svg
|
|
427
|
-
.append('g')
|
|
428
|
-
.attr('class', 'dgmo-treemap-legend')
|
|
429
|
-
.attr('transform', `translate(0, ${titleH})`);
|
|
430
|
-
renderIntegratedLegend(legendG, {
|
|
431
|
-
groups: legend.groups,
|
|
432
|
-
palette: {
|
|
433
|
-
bg: palette.bg,
|
|
434
|
-
surface: palette.surface,
|
|
435
|
-
text: palette.text,
|
|
436
|
-
textMuted: palette.textMuted,
|
|
437
|
-
primary: palette.primary,
|
|
438
|
-
},
|
|
439
|
-
isDark,
|
|
440
|
-
width,
|
|
441
|
-
mode: exportMode ? 'export' : 'preview',
|
|
442
|
-
position: LEGEND_POSITION,
|
|
443
|
-
activeGroup: legend.activeGroup,
|
|
444
|
-
// The color-mode switcher is the legend itself: each applicable mode is a
|
|
445
|
-
// group pill; the active one is the open capsule, the rest are clickable
|
|
446
|
-
// pills that switch mode. Export shows only the active group.
|
|
447
|
-
showInactivePills: !exportMode,
|
|
448
|
-
showEmptyGroups: !exportMode,
|
|
449
|
-
...(options.onSelectMode !== undefined && {
|
|
450
|
-
callbacks: {
|
|
451
|
-
onGroupToggle: (name: string) => {
|
|
452
|
-
const m = legend.modeByName.get(name);
|
|
453
|
-
if (m) options.onSelectMode!(m);
|
|
454
|
-
},
|
|
455
|
-
},
|
|
456
|
-
}),
|
|
457
|
-
});
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// ============================================================
|
|
462
|
-
// Helpers
|
|
463
|
-
// ============================================================
|
|
464
|
-
|
|
465
|
-
function drawFocusIcon(
|
|
466
|
-
g: d3Selection.Selection<SVGGElement, unknown, null, undefined>,
|
|
467
|
-
w: number,
|
|
468
|
-
ink: string,
|
|
469
|
-
cy: number
|
|
470
|
-
): void {
|
|
471
|
-
// Small, subtle scope/target ring centered vertically at `cy` (the header
|
|
472
|
-
// band's middle). Opacity is driven by the SVG <style> (faint by default,
|
|
473
|
-
// clearer on cell hover). A generous transparent rect keeps it easy to click.
|
|
474
|
-
const r = 4.5;
|
|
475
|
-
const cx = w - r - 5;
|
|
476
|
-
const fi = g
|
|
477
|
-
.append('g')
|
|
478
|
-
.attr('class', 'dgmo-treemap-focus')
|
|
479
|
-
.attr('data-export-ignore', 'true');
|
|
480
|
-
fi.append('rect')
|
|
481
|
-
.attr('x', cx - 9)
|
|
482
|
-
.attr('y', cy - 9)
|
|
483
|
-
.attr('width', 18)
|
|
484
|
-
.attr('height', 18)
|
|
485
|
-
.attr('fill', 'transparent');
|
|
486
|
-
fi.append('circle')
|
|
487
|
-
.attr('cx', cx)
|
|
488
|
-
.attr('cy', cy)
|
|
489
|
-
.attr('r', r)
|
|
490
|
-
.attr('fill', 'none')
|
|
491
|
-
.attr('stroke', ink)
|
|
492
|
-
.attr('stroke-width', 1.25);
|
|
493
|
-
fi.append('circle')
|
|
494
|
-
.attr('cx', cx)
|
|
495
|
-
.attr('cy', cy)
|
|
496
|
-
.attr('r', 1.4)
|
|
497
|
-
.attr('fill', ink);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
/**
|
|
501
|
-
* Run a leaf's name vertically (reads bottom-to-top) down a tall-narrow cell.
|
|
502
|
-
* Font is bound by the cell WIDTH (the rotated cap height) and the name length
|
|
503
|
-
* by the cell HEIGHT; the name still clips if even rotated it can't fit. No
|
|
504
|
-
* value/% — the caller only reaches here when the cell is too narrow for them.
|
|
505
|
-
*/
|
|
506
|
-
function drawVerticalLabel(
|
|
507
|
-
g: d3Selection.Selection<SVGGElement, unknown, null, undefined>,
|
|
508
|
-
label: string,
|
|
509
|
-
w: number,
|
|
510
|
-
h: number,
|
|
511
|
-
pad: number,
|
|
512
|
-
ink: string
|
|
513
|
-
): void {
|
|
514
|
-
const MIN_FS = 6;
|
|
515
|
-
const availLen = h - 2 * pad; // length budget, along the cell height
|
|
516
|
-
const availThick = w - 2 * pad; // thickness budget, across the cell width
|
|
517
|
-
// Thickness-bound first: cap the glyph height to the cell width (modest cap so
|
|
518
|
-
// vertical names stay subtle), then shrink to fit the name into the length.
|
|
519
|
-
let fs = clamp(Math.round(Math.min(availThick / 0.8, 14)), MIN_FS, 22);
|
|
520
|
-
const nameW = measureText(label, fs) * 1.06;
|
|
521
|
-
if (nameW > availLen)
|
|
522
|
-
fs = Math.max(MIN_FS, Math.floor((fs * availLen) / nameW));
|
|
523
|
-
const text = clipLabel(label, availLen, fs);
|
|
524
|
-
if (!text) return;
|
|
525
|
-
|
|
526
|
-
g.append('text')
|
|
527
|
-
.attr('class', 'dgmo-treemap-label')
|
|
528
|
-
// Center on the cell, rotate -90 so the string runs upward; the small y
|
|
529
|
-
// offset recenters the glyph's cap height across the cell width (resvg has
|
|
530
|
-
// no reliable dominant-baseline, so it's done by hand like the H labels).
|
|
531
|
-
.attr('transform', `translate(${w / 2},${h / 2}) rotate(-90)`)
|
|
532
|
-
.attr('y', Math.round(fs * 0.28))
|
|
533
|
-
.attr('text-anchor', 'middle')
|
|
534
|
-
.attr('font-size', fs)
|
|
535
|
-
.attr('font-weight', 600)
|
|
536
|
-
.attr('fill', ink)
|
|
537
|
-
.attr('opacity', 0.95)
|
|
538
|
-
.text(text);
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
/** Depth a glyph's descender (p, q, g, y, j) drops below the baseline. Inter's
|
|
542
|
-
* deepest descenders sit at ~0.24em; round up so a big `g`/`y` tail never grazes
|
|
543
|
-
* the line beneath it. */
|
|
544
|
-
function descent(fs: number): number {
|
|
545
|
-
return Math.ceil(fs * 0.26);
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
/**
|
|
549
|
-
* Baseline-to-baseline drop from a line at `prevFs` to the next at `nextFs`:
|
|
550
|
-
* clear the previous line's descender, leave a small gap, then drop by the next
|
|
551
|
-
* line's cap height. Without the descender term, a large name's `p`/`g`/`y`
|
|
552
|
-
* tails collide with the value line beneath it.
|
|
553
|
-
*/
|
|
554
|
-
function lineDrop(prevFs: number, nextFs: number, gap = 4): number {
|
|
555
|
-
return descent(prevFs) + gap + Math.round(nextFs * 0.72);
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
function clamp(x: number, lo: number, hi: number): number {
|
|
559
|
-
return Math.max(lo, Math.min(hi, x));
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
function clipLabel(s: string, maxWidth: number, fs: number): string {
|
|
563
|
-
if (measureText(s, fs) <= maxWidth) return s;
|
|
564
|
-
let lo = 0;
|
|
565
|
-
let hi = s.length;
|
|
566
|
-
while (lo < hi) {
|
|
567
|
-
const mid = Math.ceil((lo + hi) / 2);
|
|
568
|
-
if (measureText(s.slice(0, mid) + '…', fs) <= maxWidth) lo = mid;
|
|
569
|
-
else hi = mid - 1;
|
|
570
|
-
}
|
|
571
|
-
return lo <= 0 ? '' : s.slice(0, lo) + '…';
|
|
572
|
-
}
|