@diagrammo/dgmo 0.25.5 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/advanced.cjs +4255 -2756
- package/dist/advanced.d.cts +285 -59
- package/dist/advanced.d.ts +285 -59
- package/dist/advanced.js +4253 -2750
- package/dist/auto.cjs +4051 -2589
- package/dist/auto.js +124 -122
- package/dist/auto.mjs +4051 -2589
- package/dist/cli.cjs +172 -170
- package/dist/editor.cjs +4 -0
- package/dist/editor.js +4 -0
- package/dist/highlight.cjs +4 -0
- package/dist/highlight.js +4 -0
- package/dist/index.cjs +4076 -2591
- package/dist/index.d.cts +33 -8
- package/dist/index.d.ts +33 -8
- package/dist/index.js +4076 -2591
- package/dist/internal.cjs +4255 -2756
- package/dist/internal.d.cts +285 -59
- package/dist/internal.d.ts +285 -59
- package/dist/internal.js +4253 -2750
- package/dist/map-data/PROVENANCE.json +1 -1
- package/dist/map-data/airport-collisions.json +1 -0
- package/dist/map-data/airports.json +1 -0
- package/docs/language-reference.md +68 -18
- package/gallery/fixtures/boxes-and-lines-diverging.dgmo +15 -0
- package/gallery/fixtures/map-choropleth-diverging.dgmo +9 -0
- package/gallery/fixtures/map-region-values.dgmo +13 -0
- package/gallery/fixtures/map-subnational-zoom.dgmo +12 -0
- package/gallery/fixtures/map-tagged-legs.dgmo +16 -0
- package/gallery/fixtures/map-undirected-edges.dgmo +12 -0
- package/package.json +1 -1
- package/src/advanced.ts +3 -6
- package/src/auto/index.ts +1 -1
- package/src/boxes-and-lines/layout.ts +146 -26
- package/src/boxes-and-lines/parser.ts +43 -8
- package/src/boxes-and-lines/renderer.ts +223 -96
- package/src/boxes-and-lines/types.ts +9 -2
- package/src/c4/layout.ts +14 -32
- package/src/c4/parser.ts +9 -5
- package/src/c4/renderer.ts +34 -39
- package/src/class/layout.ts +118 -18
- package/src/class/parser.ts +35 -1
- package/src/class/renderer.ts +58 -2
- package/src/class/types.ts +3 -0
- package/src/cli.ts +4 -4
- package/src/completion-types.ts +0 -1
- package/src/completion.ts +106 -51
- package/src/cycle/layout.ts +55 -72
- package/src/cycle/renderer.ts +11 -6
- package/src/d3.ts +78 -117
- package/src/diagnostics.ts +16 -0
- package/src/echarts.ts +46 -33
- package/src/editor/keywords.ts +4 -0
- package/src/er/layout.ts +114 -22
- package/src/er/parser.ts +28 -1
- package/src/er/renderer.ts +55 -2
- package/src/er/types.ts +3 -0
- package/src/gantt/renderer.ts +46 -38
- package/src/gantt/resolver.ts +9 -2
- package/src/graph/edge-spline.ts +29 -0
- package/src/graph/flowchart-parser.ts +35 -2
- package/src/graph/flowchart-renderer.ts +80 -52
- package/src/graph/layout.ts +206 -23
- package/src/graph/notes.ts +21 -0
- package/src/graph/state-parser.ts +26 -1
- package/src/graph/state-renderer.ts +80 -52
- package/src/graph/types.ts +13 -0
- package/src/index.ts +1 -1
- package/src/infra/layout.ts +46 -26
- package/src/infra/parser.ts +1 -1
- package/src/infra/renderer.ts +16 -7
- package/src/journey-map/layout.ts +38 -49
- package/src/journey-map/renderer.ts +22 -45
- package/src/kanban/renderer.ts +15 -6
- package/src/label-layout.ts +3 -3
- package/src/map/completion.ts +77 -22
- package/src/map/context-labels.ts +57 -12
- package/src/map/data/PROVENANCE.json +1 -1
- package/src/map/data/airport-collisions.json +1 -0
- package/src/map/data/airports.json +1 -0
- package/src/map/data/types.ts +19 -0
- package/src/map/layout.ts +1196 -90
- package/src/map/legend-band.ts +2 -2
- package/src/map/load-data.ts +10 -1
- package/src/map/parser.ts +61 -32
- package/src/map/renderer.ts +284 -12
- package/src/map/resolved-types.ts +15 -1
- package/src/map/resolver.ts +132 -12
- package/src/map/types.ts +28 -8
- package/src/migrate/embedded.ts +9 -7
- package/src/mindmap/text-wrap.ts +13 -14
- package/src/org/layout.ts +19 -17
- package/src/org/renderer.ts +11 -4
- package/src/palettes/color-utils.ts +82 -21
- package/src/palettes/index.ts +0 -19
- package/src/palettes/registry.ts +1 -1
- package/src/palettes/types.ts +2 -2
- package/src/pert/layout.ts +48 -40
- package/src/pert/parser.ts +0 -14
- package/src/pert/renderer.ts +30 -43
- package/src/pyramid/renderer.ts +4 -5
- package/src/raci/renderer.ts +42 -70
- package/src/render.ts +1 -1
- package/src/ring/renderer.ts +1 -2
- package/src/sequence/parser.ts +100 -22
- package/src/sequence/renderer.ts +75 -50
- package/src/sitemap/layout.ts +27 -19
- package/src/sitemap/renderer.ts +12 -5
- package/src/tech-radar/renderer.ts +11 -35
- package/src/utils/arrow-markers.ts +51 -0
- package/src/utils/fit-canvas.ts +64 -0
- package/src/utils/legend-constants.ts +8 -54
- package/src/utils/legend-d3.ts +10 -7
- package/src/utils/legend-layout.ts +7 -4
- package/src/utils/legend-types.ts +10 -4
- package/src/utils/note-box/constants.ts +25 -0
- package/src/utils/note-box/index.ts +11 -0
- package/src/utils/note-box/metrics.ts +90 -0
- package/src/utils/note-box/svg.ts +331 -0
- package/src/utils/notes/bounds.ts +30 -0
- package/src/utils/notes/build.ts +131 -0
- package/src/utils/notes/index.ts +18 -0
- package/src/utils/notes/model.ts +19 -0
- package/src/utils/notes/parse.ts +131 -0
- package/src/utils/notes/place.ts +177 -0
- package/src/utils/notes/resolve.ts +88 -0
- package/src/utils/number-format.ts +36 -0
- package/src/utils/parsing.ts +41 -0
- package/src/utils/reserved-key-registry.ts +4 -0
- package/src/utils/text-measure.ts +122 -0
- package/src/wireframe/layout.ts +4 -2
- package/src/wireframe/renderer.ts +8 -6
- package/src/palettes/dracula.ts +0 -68
- package/src/palettes/gruvbox.ts +0 -85
- package/src/palettes/monokai.ts +0 -68
- package/src/palettes/one-dark.ts +0 -70
- package/src/palettes/rose-pine.ts +0 -84
- package/src/palettes/solarized.ts +0 -77
|
@@ -155,6 +155,54 @@ export function mix(a: string, b: string, pct: number): string {
|
|
|
155
155
|
return `#${c(ar, br)}${c(ag, bg)}${c(ab, bb)}`;
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
// ============================================================
|
|
159
|
+
// Value Ramp (the shared `<metric> <low?> <high?>` coloring convention)
|
|
160
|
+
// ============================================================
|
|
161
|
+
//
|
|
162
|
+
// Single source of truth for value-ramp fills across chart types (map
|
|
163
|
+
// `region-metric`, boxes-and-lines `box-metric`, and any future ramp). Callers
|
|
164
|
+
// resolve the two endpoint NAMES to palette hex, then ask for the fill at a
|
|
165
|
+
// normalized position `t∈[0,1]`. The helper owns ONLY the low→high blend; each
|
|
166
|
+
// caller keeps its own RAMP_FLOOR / base remap of `t`.
|
|
167
|
+
//
|
|
168
|
+
// The blend is a straight sRGB fade between the two true palette endpoints — no
|
|
169
|
+
// invented intermediate hue. `t=0` is exactly `low`, `t=1` is exactly `high`,
|
|
170
|
+
// and everything between is a direct interpolation of those two palette colours.
|
|
171
|
+
// (resvg has no `color-mix()`; `mix()` pre-computes the hex.)
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Value-ramp fill at normalized position `t`. PURE and order-respecting:
|
|
175
|
+
* `t=0` → exactly `low`, `t=1` → exactly `high`, no sorting or intent
|
|
176
|
+
* correction. `low`/`high` are resolved hex (the caller maps colour names →
|
|
177
|
+
* palette hex). A straight sRGB fade between the two palette colours — no
|
|
178
|
+
* synthetic midpoint hue. `_opts` is retained for call-site/theme compat.
|
|
179
|
+
*/
|
|
180
|
+
export function valueRampColor(
|
|
181
|
+
low: string,
|
|
182
|
+
high: string,
|
|
183
|
+
t: number,
|
|
184
|
+
_opts: { isDark: boolean }
|
|
185
|
+
): string {
|
|
186
|
+
const tc = Math.max(0, Math.min(1, t));
|
|
187
|
+
return mix(high, low, tc * 100);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Gradient stops that reproduce `valueRampColor` for a legend
|
|
192
|
+
* `<linearGradient>`. A direct two-endpoint fade needs only the two stops; the
|
|
193
|
+
* gradient itself interpolates between the palette colours.
|
|
194
|
+
*/
|
|
195
|
+
export function valueRampStops(
|
|
196
|
+
low: string,
|
|
197
|
+
high: string,
|
|
198
|
+
_opts: { isDark: boolean }
|
|
199
|
+
): ReadonlyArray<{ offset: number; color: string }> {
|
|
200
|
+
return [
|
|
201
|
+
{ offset: 0, color: low },
|
|
202
|
+
{ offset: 1, color: high },
|
|
203
|
+
];
|
|
204
|
+
}
|
|
205
|
+
|
|
158
206
|
// ============================================================
|
|
159
207
|
// Contrast / Accessibility
|
|
160
208
|
// ============================================================
|
|
@@ -204,7 +252,7 @@ export function contrastRatio(a: string, b: string): number {
|
|
|
204
252
|
* tokyo-night green `#9ece6a` min 106, ratio 11.4:1 all correctly pick dark).
|
|
205
253
|
* 3. **Saturated fill (min RGB < 100, luminance ≤ 0.55)** → `lightText`. At least
|
|
206
254
|
* one channel near zero signals true saturation — gruvbox dark green
|
|
207
|
-
* `#b8bb26` (min 38),
|
|
255
|
+
* `#b8bb26` (min 38), blueprint blue `#1f5e8c` (min 31), bold red/blue
|
|
208
256
|
* (min 0), solarized blue `#268bd2` (min 38). The user consistently
|
|
209
257
|
* prefers light text on these for visual punch.
|
|
210
258
|
*
|
|
@@ -280,33 +328,46 @@ export function getSeriesColors(palette: PaletteColors): string[] {
|
|
|
280
328
|
* Generate `count` visually distinct colors for segment-based charts
|
|
281
329
|
* (pie, doughnut, polar-area).
|
|
282
330
|
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
331
|
+
* Stays ON-PALETTE: the first pass is the palette's own series hues at full
|
|
332
|
+
* strength. When a chart needs more segments than the palette has distinct
|
|
333
|
+
* hues, additional passes reuse the SAME hues at shifted lightness — each a
|
|
334
|
+
* tint (mixed toward `bg`) or shade (mixed toward `text`) of a true palette
|
|
335
|
+
* colour. Hue is never rotated or invented; extra segments read as lighter /
|
|
336
|
+
* darker variants of the palette, not as wheel-generated colours.
|
|
286
337
|
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
* perceptually distinct color regardless of segment count.
|
|
338
|
+
* (Several palettes have duplicate hex for different named colours — e.g.
|
|
339
|
+
* teal===cyan — so the first pass is deduped before the lightness bands kick in.)
|
|
290
340
|
*/
|
|
291
341
|
export function getSegmentColors(
|
|
292
342
|
palette: PaletteColors,
|
|
293
343
|
count: number
|
|
294
344
|
): string[] {
|
|
295
|
-
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
const
|
|
305
|
-
const
|
|
345
|
+
if (count <= 0) return [];
|
|
346
|
+
const base = [...new Set(getSeriesColors(palette))];
|
|
347
|
+
if (count <= base.length) return base.slice(0, count);
|
|
348
|
+
|
|
349
|
+
// Lightness bands of the SAME hues — alternating tint/shade, progressively
|
|
350
|
+
// stronger. Mixing toward the neutral bg/text keeps hue, varying only
|
|
351
|
+
// lightness/saturation (a tint/fade). Symmetric across light & dark themes:
|
|
352
|
+
// `bg` is light/dark and `text` is its inverse, so the two directions always
|
|
353
|
+
// diverge.
|
|
354
|
+
const { bg, text } = palette;
|
|
355
|
+
const variants: ReadonlyArray<(c: string) => string> = [
|
|
356
|
+
(c) => mix(c, bg, 55), // lighter
|
|
357
|
+
(c) => mix(c, text, 55), // darker
|
|
358
|
+
(c) => mix(c, bg, 35),
|
|
359
|
+
(c) => mix(c, text, 35),
|
|
360
|
+
];
|
|
306
361
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
362
|
+
const out = [...base];
|
|
363
|
+
for (let w = 0; out.length < count; w++) {
|
|
364
|
+
const variant = variants[w % variants.length]!;
|
|
365
|
+
for (const c of base) {
|
|
366
|
+
if (out.length >= count) break;
|
|
367
|
+
out.push(variant(c));
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return out.slice(0, count);
|
|
310
371
|
}
|
|
311
372
|
|
|
312
373
|
// ============================================================
|
package/src/palettes/index.ts
CHANGED
|
@@ -26,18 +26,11 @@ export {
|
|
|
26
26
|
export { atlasPalette } from './atlas';
|
|
27
27
|
export { blueprintPalette } from './blueprint';
|
|
28
28
|
export { catppuccinPalette } from './catppuccin';
|
|
29
|
-
export { gruvboxPalette } from './gruvbox';
|
|
30
29
|
export { nordPalette } from './nord';
|
|
31
|
-
export { oneDarkPalette } from './one-dark';
|
|
32
|
-
export { rosePinePalette } from './rose-pine';
|
|
33
30
|
export { slatePalette } from './slate';
|
|
34
|
-
export { solarizedPalette } from './solarized';
|
|
35
31
|
export { tidewaterPalette } from './tidewater';
|
|
36
32
|
export { tokyoNightPalette } from './tokyo-night';
|
|
37
33
|
|
|
38
|
-
export { draculaPalette } from './dracula';
|
|
39
|
-
export { monokaiPalette } from './monokai';
|
|
40
|
-
|
|
41
34
|
// ============================================================
|
|
42
35
|
// Public namespace — `palettes` for use with render()
|
|
43
36
|
// ============================================================
|
|
@@ -45,14 +38,8 @@ export { monokaiPalette } from './monokai';
|
|
|
45
38
|
import { atlasPalette } from './atlas';
|
|
46
39
|
import { blueprintPalette } from './blueprint';
|
|
47
40
|
import { catppuccinPalette } from './catppuccin';
|
|
48
|
-
import { draculaPalette } from './dracula';
|
|
49
|
-
import { gruvboxPalette } from './gruvbox';
|
|
50
|
-
import { monokaiPalette } from './monokai';
|
|
51
41
|
import { nordPalette } from './nord';
|
|
52
|
-
import { oneDarkPalette } from './one-dark';
|
|
53
|
-
import { rosePinePalette } from './rose-pine';
|
|
54
42
|
import { slatePalette } from './slate';
|
|
55
|
-
import { solarizedPalette } from './solarized';
|
|
56
43
|
import { tidewaterPalette } from './tidewater';
|
|
57
44
|
import { tokyoNightPalette } from './tokyo-night';
|
|
58
45
|
|
|
@@ -74,11 +61,5 @@ export const palettes = {
|
|
|
74
61
|
tidewater: tidewaterPalette,
|
|
75
62
|
nord: nordPalette,
|
|
76
63
|
catppuccin: catppuccinPalette,
|
|
77
|
-
solarized: solarizedPalette,
|
|
78
|
-
gruvbox: gruvboxPalette,
|
|
79
64
|
tokyoNight: tokyoNightPalette,
|
|
80
|
-
oneDark: oneDarkPalette,
|
|
81
|
-
rosePine: rosePinePalette,
|
|
82
|
-
dracula: draculaPalette,
|
|
83
|
-
monokai: monokaiPalette,
|
|
84
65
|
} as const satisfies Record<string, PaletteConfig>;
|
package/src/palettes/registry.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { PaletteConfig, PaletteColors } from './types';
|
|
|
5
5
|
// ============================================================
|
|
6
6
|
|
|
7
7
|
const PALETTE_REGISTRY = new Map<string, PaletteConfig>();
|
|
8
|
-
const DEFAULT_PALETTE_ID = '
|
|
8
|
+
const DEFAULT_PALETTE_ID = 'slate';
|
|
9
9
|
|
|
10
10
|
// ============================================================
|
|
11
11
|
// Validation
|
package/src/palettes/types.ts
CHANGED
|
@@ -77,9 +77,9 @@ export interface PaletteColors {
|
|
|
77
77
|
* hands out the same frozen-shape object on every `getPalette(id)`.
|
|
78
78
|
*/
|
|
79
79
|
export interface PaletteConfig {
|
|
80
|
-
/** Registry key: 'nord', '
|
|
80
|
+
/** Registry key: 'nord', 'slate', 'catppuccin' */
|
|
81
81
|
readonly id: string;
|
|
82
|
-
/** Display name: 'Nord', '
|
|
82
|
+
/** Display name: 'Nord', 'Slate', 'Catppuccin' */
|
|
83
83
|
readonly name: string;
|
|
84
84
|
/** Light mode color definitions */
|
|
85
85
|
readonly light: PaletteColors;
|
package/src/pert/layout.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
formatSprintCell,
|
|
21
21
|
formatSlackValue,
|
|
22
22
|
} from './internal';
|
|
23
|
+
import { measureText } from '../utils/text-measure';
|
|
23
24
|
|
|
24
25
|
// Textbook 3×3 PERT/CPM box: top row [ES | dur | EF], middle row
|
|
25
26
|
// [name spanning all three columns], bottom row [LS | slack | LF].
|
|
@@ -46,9 +47,8 @@ const SWIMLANE_GAP = 24;
|
|
|
46
47
|
// Cell-sizing constants — see `computeNodeSizing` below.
|
|
47
48
|
const NODE_CELL_FONT_SIZE = 11;
|
|
48
49
|
const NODE_NAME_FONT_SIZE = 13;
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
const CELL_CHAR_WIDTH_RATIO = 0.55;
|
|
50
|
+
// Milestone name renders at 12pt with a leading `◆ ` glyph (see renderer).
|
|
51
|
+
const MILESTONE_NAME_FONT_SIZE = 12;
|
|
52
52
|
const CELL_PAD_X = 8;
|
|
53
53
|
const NAME_PAD_X = 6;
|
|
54
54
|
// Anchor icon + gap (renderer reserves space to the left of the name
|
|
@@ -88,8 +88,6 @@ export function computeNodeSizing(resolved: ResolvedPert): NodeSizing {
|
|
|
88
88
|
const sprintMode = resolved.options.sprintMode;
|
|
89
89
|
const sprintNumber = resolved.options.sprintNumber ?? 1;
|
|
90
90
|
const projectStart = resolved.projectStart;
|
|
91
|
-
const cellCharW = NODE_CELL_FONT_SIZE * CELL_CHAR_WIDTH_RATIO;
|
|
92
|
-
const nameCharW = NODE_NAME_FONT_SIZE * CELL_CHAR_WIDTH_RATIO;
|
|
93
91
|
|
|
94
92
|
const fmtSchedule = (v: number | null, isTbd: boolean): string =>
|
|
95
93
|
sprintMode
|
|
@@ -100,12 +98,15 @@ export function computeNodeSizing(resolved: ResolvedPert): NodeSizing {
|
|
|
100
98
|
const fmtDur = (v: number | null, isTbd: boolean): string =>
|
|
101
99
|
formatDuration(v, unit, isTbd ? '?' : null);
|
|
102
100
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
let
|
|
107
|
-
let
|
|
108
|
-
let
|
|
101
|
+
// Track the widest rendered pixel width per cell category — measured at
|
|
102
|
+
// the exact font sizes the renderer draws with so reserved width
|
|
103
|
+
// matches drawn text.
|
|
104
|
+
let maxOuterW = 0;
|
|
105
|
+
let maxMidW = 0;
|
|
106
|
+
let maxNameW = 0;
|
|
107
|
+
let maxMilestoneTopW = 0;
|
|
108
|
+
let maxMilestoneSlackW = 0;
|
|
109
|
+
let maxMilestoneNameW = 0;
|
|
109
110
|
|
|
110
111
|
for (const r of resolved.activities) {
|
|
111
112
|
const isTbd = r.es === null;
|
|
@@ -113,17 +114,20 @@ export function computeNodeSizing(resolved: ResolvedPert): NodeSizing {
|
|
|
113
114
|
const dateStr = fmtSchedule(r.es, isTbd);
|
|
114
115
|
const slackStr = fmtSlack(r.slack, isTbd);
|
|
115
116
|
const slackHidden = !isTbd && /^0[a-z]?$/.test(slackStr);
|
|
116
|
-
|
|
117
|
+
maxMilestoneTopW = Math.max(
|
|
118
|
+
maxMilestoneTopW,
|
|
119
|
+
measureText(dateStr, NODE_CELL_FONT_SIZE)
|
|
120
|
+
);
|
|
117
121
|
if (!slackHidden) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
slackStr
|
|
122
|
+
maxMilestoneSlackW = Math.max(
|
|
123
|
+
maxMilestoneSlackW,
|
|
124
|
+
measureText(slackStr, NODE_CELL_FONT_SIZE)
|
|
121
125
|
);
|
|
122
126
|
}
|
|
123
|
-
// The milestone glyph prefix `◆ `
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
r.activity.name
|
|
127
|
+
// The milestone glyph prefix `◆ ` precedes the name.
|
|
128
|
+
maxMilestoneNameW = Math.max(
|
|
129
|
+
maxMilestoneNameW,
|
|
130
|
+
measureText(`◆ ${r.activity.name}`, MILESTONE_NAME_FONT_SIZE)
|
|
127
131
|
);
|
|
128
132
|
continue;
|
|
129
133
|
}
|
|
@@ -133,33 +137,43 @@ export function computeNodeSizing(resolved: ResolvedPert): NodeSizing {
|
|
|
133
137
|
const lfStr = fmtSchedule(r.lf, isTbd);
|
|
134
138
|
const durStr = fmtDur(r.mu, isTbd);
|
|
135
139
|
const slackStr = fmtSlack(r.slack, isTbd);
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
esStr
|
|
139
|
-
efStr
|
|
140
|
-
lsStr
|
|
141
|
-
lfStr
|
|
140
|
+
maxOuterW = Math.max(
|
|
141
|
+
maxOuterW,
|
|
142
|
+
measureText(esStr, NODE_CELL_FONT_SIZE),
|
|
143
|
+
measureText(efStr, NODE_CELL_FONT_SIZE),
|
|
144
|
+
measureText(lsStr, NODE_CELL_FONT_SIZE),
|
|
145
|
+
measureText(lfStr, NODE_CELL_FONT_SIZE)
|
|
146
|
+
);
|
|
147
|
+
maxMidW = Math.max(
|
|
148
|
+
maxMidW,
|
|
149
|
+
measureText(durStr, NODE_CELL_FONT_SIZE),
|
|
150
|
+
measureText(slackStr, NODE_CELL_FONT_SIZE)
|
|
151
|
+
);
|
|
152
|
+
maxNameW = Math.max(
|
|
153
|
+
maxNameW,
|
|
154
|
+
measureText(r.activity.name, NODE_NAME_FONT_SIZE)
|
|
142
155
|
);
|
|
143
|
-
maxMidChars = Math.max(maxMidChars, durStr.length, slackStr.length);
|
|
144
|
-
maxNameChars = Math.max(maxNameChars, r.activity.name.length);
|
|
145
156
|
}
|
|
146
157
|
|
|
147
158
|
// Also account for any collapsed-group rolled-up label width — the
|
|
148
159
|
// renderer draws those with the same textbook-card chrome, so their
|
|
149
160
|
// name needs to fit in the shared `activityWidth`.
|
|
150
161
|
for (const rg of resolved.groups) {
|
|
151
|
-
|
|
162
|
+
maxNameW = Math.max(
|
|
163
|
+
maxNameW,
|
|
164
|
+
measureText(rg.group.name, NODE_NAME_FONT_SIZE)
|
|
165
|
+
);
|
|
152
166
|
}
|
|
153
167
|
|
|
154
|
-
const outerCell = Math.ceil(
|
|
155
|
-
const midCell = Math.ceil(
|
|
168
|
+
const outerCell = Math.ceil(maxOuterW) + 2 * CELL_PAD_X;
|
|
169
|
+
const midCell = Math.ceil(maxMidW) + 2 * CELL_PAD_X;
|
|
156
170
|
const outerNeeded = Math.max(MIN_CELL_WIDTH, outerCell);
|
|
157
171
|
const midNeeded = Math.max(MIN_CELL_WIDTH, midCell);
|
|
158
172
|
const cellsTotalW = 2 * outerNeeded + midNeeded;
|
|
159
173
|
|
|
160
174
|
// The name dictates a lower bound too — anchor-pinned cards reserve
|
|
161
175
|
// a NAME_PIN_WIDTH on the left, so size for the worst case.
|
|
162
|
-
const nameTextW = Math.ceil(
|
|
176
|
+
const nameTextW = Math.ceil(maxNameW);
|
|
163
177
|
const nameTotalW = nameTextW + NAME_PIN_WIDTH + 2 * NAME_PAD_X;
|
|
164
178
|
|
|
165
179
|
const activityWidth = Math.max(
|
|
@@ -183,17 +197,11 @@ export function computeNodeSizing(resolved: ResolvedPert): NodeSizing {
|
|
|
183
197
|
|
|
184
198
|
// Milestones are independent: a one-cell-tall date row, a name row,
|
|
185
199
|
// and (optionally) a slack row. Width should fit whichever is widest.
|
|
186
|
-
const mTop = Math.ceil(
|
|
200
|
+
const mTop = Math.ceil(maxMilestoneTopW) + 2 * CELL_PAD_X;
|
|
187
201
|
const mSlack =
|
|
188
|
-
|
|
189
|
-
? Math.ceil(maxMilestoneSlackChars * cellCharW) + 2 * CELL_PAD_X
|
|
190
|
-
: 0;
|
|
202
|
+
maxMilestoneSlackW > 0 ? Math.ceil(maxMilestoneSlackW) + 2 * CELL_PAD_X : 0;
|
|
191
203
|
// Milestone name renders at 12pt with anchor-pin reserve.
|
|
192
|
-
const
|
|
193
|
-
const mName =
|
|
194
|
-
Math.ceil(maxMilestoneNameChars * mNameCharW) +
|
|
195
|
-
NAME_PIN_WIDTH +
|
|
196
|
-
2 * NAME_PAD_X;
|
|
204
|
+
const mName = Math.ceil(maxMilestoneNameW) + NAME_PIN_WIDTH + 2 * NAME_PAD_X;
|
|
197
205
|
const milestoneWidth = Math.max(
|
|
198
206
|
MIN_MILESTONE_WIDTH,
|
|
199
207
|
Math.min(MAX_MILESTONE_WIDTH, Math.max(mTop, mSlack, mName))
|
package/src/pert/parser.ts
CHANGED
|
@@ -1639,20 +1639,6 @@ export function extractPertSymbols(docText: string): DiagramSymbols {
|
|
|
1639
1639
|
return {
|
|
1640
1640
|
kind: 'pert',
|
|
1641
1641
|
entities,
|
|
1642
|
-
keywords: [
|
|
1643
|
-
'time-unit',
|
|
1644
|
-
'default-confidence',
|
|
1645
|
-
'direction',
|
|
1646
|
-
'node-detail',
|
|
1647
|
-
'trials',
|
|
1648
|
-
'seed',
|
|
1649
|
-
'scrubber-trials',
|
|
1650
|
-
'start-date',
|
|
1651
|
-
'end-date',
|
|
1652
|
-
'active-tag',
|
|
1653
|
-
'tag',
|
|
1654
|
-
'as',
|
|
1655
|
-
],
|
|
1656
1642
|
};
|
|
1657
1643
|
}
|
|
1658
1644
|
|
package/src/pert/renderer.ts
CHANGED
|
@@ -35,6 +35,11 @@ import { FONT_FAMILY } from '../fonts';
|
|
|
35
35
|
import type { PaletteColors } from '../palettes';
|
|
36
36
|
import { contrastText, mix, shapeFill } from '../palettes/color-utils';
|
|
37
37
|
import { ScaleContext } from '../utils/scaling';
|
|
38
|
+
import {
|
|
39
|
+
measureText,
|
|
40
|
+
truncateText,
|
|
41
|
+
wrapTextToWidth,
|
|
42
|
+
} from '../utils/text-measure';
|
|
38
43
|
import {
|
|
39
44
|
TITLE_FONT_SIZE,
|
|
40
45
|
TITLE_FONT_WEIGHT,
|
|
@@ -164,21 +169,23 @@ function fieldLegendRowHeight(maxDescLines: number): number {
|
|
|
164
169
|
maxDescLines * FIELD_LEGEND_DESC_LINE_HEIGHT
|
|
165
170
|
);
|
|
166
171
|
}
|
|
167
|
-
//
|
|
168
|
-
// width
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const charW = FIELD_LEGEND_DESC_FONT_SIZE * 0.55;
|
|
172
|
-
return Math.max(10, Math.floor((colW - 8) / charW));
|
|
172
|
+
// Pixel width the description text wraps within for the given column
|
|
173
|
+
// width (column minus 8px of horizontal padding).
|
|
174
|
+
function fieldLegendDescWidth(colW: number): number {
|
|
175
|
+
return colW - 8;
|
|
173
176
|
}
|
|
174
177
|
// Total height the field-legend block needs at the given outer width.
|
|
175
178
|
// Accounts for the worst-case description wrap across all 6 cells.
|
|
176
179
|
function fieldLegendHeightFor(width: number): number {
|
|
177
180
|
const colW = width / 3;
|
|
178
|
-
const
|
|
181
|
+
const wrapW = fieldLegendDescWidth(colW);
|
|
179
182
|
let maxLines = 1;
|
|
180
183
|
for (const cell of FIELD_LEGEND_CELLS) {
|
|
181
|
-
const n =
|
|
184
|
+
const n = wrapTextToWidth(
|
|
185
|
+
cell.desc,
|
|
186
|
+
FIELD_LEGEND_DESC_FONT_SIZE,
|
|
187
|
+
wrapW
|
|
188
|
+
).length;
|
|
182
189
|
if (n > maxLines) maxLines = n;
|
|
183
190
|
}
|
|
184
191
|
return (
|
|
@@ -223,24 +230,6 @@ const FIELD_LEGEND_CELLS: readonly { label: string; desc: string }[] = [
|
|
|
223
230
|
},
|
|
224
231
|
];
|
|
225
232
|
|
|
226
|
-
function wrapTextByChars(text: string, maxChars: number): string[] {
|
|
227
|
-
const words = text.split(/\s+/);
|
|
228
|
-
const lines: string[] = [];
|
|
229
|
-
let line = '';
|
|
230
|
-
for (const word of words) {
|
|
231
|
-
if (line.length === 0) {
|
|
232
|
-
line = word;
|
|
233
|
-
} else if (line.length + 1 + word.length <= maxChars) {
|
|
234
|
-
line += ` ${word}`;
|
|
235
|
-
} else {
|
|
236
|
-
lines.push(line);
|
|
237
|
-
line = word;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
if (line) lines.push(line);
|
|
241
|
-
return lines;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
233
|
interface ScaledNodeConstants {
|
|
245
234
|
nodeRadius?: number;
|
|
246
235
|
nodeStrokeWidth?: number;
|
|
@@ -2172,14 +2161,9 @@ function drawTextbookCard(g: AnySel, a: TextbookCardArgs): void {
|
|
|
2172
2161
|
const midCenterY = midRowTop + midRowH / 2;
|
|
2173
2162
|
const NAME_PAD_X = 6;
|
|
2174
2163
|
const NAME_PIN_GAP = 4;
|
|
2175
|
-
const charW = sNFS * 0.62;
|
|
2176
2164
|
const pinReserve = a.pinned ? sPIW + NAME_PIN_GAP : 0;
|
|
2177
2165
|
const availTextW = Math.max(0, w - 2 * NAME_PAD_X - pinReserve);
|
|
2178
|
-
const
|
|
2179
|
-
const displayName =
|
|
2180
|
-
a.name.length > maxChars
|
|
2181
|
-
? a.name.slice(0, Math.max(1, maxChars - 1)) + '…'
|
|
2182
|
-
: a.name;
|
|
2166
|
+
const displayName = truncateText(a.name, sNFS, availTextW);
|
|
2183
2167
|
const nameColor = a.midBandLabelColor ?? a.labelColor;
|
|
2184
2168
|
if (a.pinned) {
|
|
2185
2169
|
drawAnchorPin(g, x + NAME_PAD_X, midCenterY, nameColor, sPIW, sPIH);
|
|
@@ -2342,7 +2326,6 @@ function drawMilestonePill(g: AnySel, a: MilestonePillArgs): void {
|
|
|
2342
2326
|
const NAME_PAD_X = 6;
|
|
2343
2327
|
const NAME_PIN_GAP = 4;
|
|
2344
2328
|
const NAME_LINE_HEIGHT = 14;
|
|
2345
|
-
const charW = nameSize * 0.62;
|
|
2346
2329
|
|
|
2347
2330
|
let textAreaLeft = x + NAME_PAD_X;
|
|
2348
2331
|
const textAreaRight = x + w - NAME_PAD_X;
|
|
@@ -2352,20 +2335,20 @@ function drawMilestonePill(g: AnySel, a: MilestonePillArgs): void {
|
|
|
2352
2335
|
}
|
|
2353
2336
|
const textCx = (textAreaLeft + textAreaRight) / 2;
|
|
2354
2337
|
const availW = textAreaRight - textAreaLeft;
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
? line.slice(0, Math.max(1, maxChars - 1)) + '…'
|
|
2359
|
-
: line
|
|
2360
|
-
);
|
|
2338
|
+
// Hard-break over-long words so a single long token still fits the
|
|
2339
|
+
// narrow milestone pill rather than overflowing.
|
|
2340
|
+
const lines = wrapTextToWidth(a.name, nameSize, availW, { hardBreak: true });
|
|
2361
2341
|
const maxLines = Math.max(1, Math.floor(midRowH / NAME_LINE_HEIGHT));
|
|
2362
2342
|
const visibleLines = lines.slice(0, maxLines);
|
|
2363
2343
|
if (lines.length > maxLines && visibleLines.length > 0) {
|
|
2344
|
+
// More lines than fit — force a trailing ellipsis on the last
|
|
2345
|
+
// visible line to signal the truncation, fitting it to the text area.
|
|
2364
2346
|
const last = visibleLines[visibleLines.length - 1]!;
|
|
2347
|
+
const withEllipsis = `${last}…`;
|
|
2365
2348
|
visibleLines[visibleLines.length - 1] =
|
|
2366
|
-
|
|
2367
|
-
?
|
|
2368
|
-
: last
|
|
2349
|
+
measureText(withEllipsis, nameSize) <= availW
|
|
2350
|
+
? withEllipsis
|
|
2351
|
+
: truncateText(last, nameSize, availW);
|
|
2369
2352
|
}
|
|
2370
2353
|
const startCy =
|
|
2371
2354
|
midCenterY - ((visibleLines.length - 1) * NAME_LINE_HEIGHT) / 2;
|
|
@@ -2853,7 +2836,11 @@ function renderFieldLegendBlock(
|
|
|
2853
2836
|
.attr('fill-opacity', 0)
|
|
2854
2837
|
.attr('pointer-events', 'all');
|
|
2855
2838
|
|
|
2856
|
-
const descLines =
|
|
2839
|
+
const descLines = wrapTextToWidth(
|
|
2840
|
+
cell.desc,
|
|
2841
|
+
FIELD_LEGEND_DESC_FONT_SIZE,
|
|
2842
|
+
fieldLegendDescWidth(colW)
|
|
2843
|
+
);
|
|
2857
2844
|
const descBlockHeight =
|
|
2858
2845
|
FIELD_LEGEND_DESC_FONT_SIZE +
|
|
2859
2846
|
Math.max(descLines.length - 1, 0) * FIELD_LEGEND_DESC_LINE_HEIGHT;
|
package/src/pyramid/renderer.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from '../palettes/color-utils';
|
|
17
17
|
import { resolveColor } from '../colors';
|
|
18
18
|
import { renderInlineText } from '../utils/inline-markdown';
|
|
19
|
+
import { CHAR_WIDTH_RATIO, measureText } from '../utils/text-measure';
|
|
19
20
|
import {
|
|
20
21
|
wrapDescriptionLines,
|
|
21
22
|
type WrappedDescLine,
|
|
@@ -45,8 +46,6 @@ const DESC_GAP = 28;
|
|
|
45
46
|
const DESC_ACCENT_WIDTH = 3;
|
|
46
47
|
/** Gap between accent bar and description text. */
|
|
47
48
|
const DESC_ACCENT_GAP = 12;
|
|
48
|
-
/** Approximate ratio of average glyph width to font size (sans-serif). */
|
|
49
|
-
const CHAR_WIDTH_RATIO = 0.55;
|
|
50
49
|
/** Pixel offset between bullet glyph column and body-text column. */
|
|
51
50
|
const BULLET_BODY_INDENT = 10;
|
|
52
51
|
|
|
@@ -250,7 +249,8 @@ export function renderPyramid(
|
|
|
250
249
|
// portion stays readable. Only when needed — wide segments don't need
|
|
251
250
|
// the visual noise of a stroke.
|
|
252
251
|
const labelFitsInside =
|
|
253
|
-
Math.min(topHalf, botHalf) * 2 >
|
|
252
|
+
Math.min(topHalf, botHalf) * 2 >
|
|
253
|
+
measureText(layer.label, layout.labelFont);
|
|
254
254
|
const haloColor =
|
|
255
255
|
textColor === palette.textOnFillLight
|
|
256
256
|
? palette.textOnFillDark
|
|
@@ -496,11 +496,10 @@ function renderLayerDescriptions(
|
|
|
496
496
|
const bulletColRightEdge = layout.leftAccentX - DESC_ACCENT_GAP;
|
|
497
497
|
const computeBulletColLeftX = (lines: WrappedDescLine[]): number => {
|
|
498
498
|
if (side === 'right') return layout.rightTextX;
|
|
499
|
-
const charW = descFont * CHAR_WIDTH_RATIO;
|
|
500
499
|
let maxBodyW = 0;
|
|
501
500
|
for (const l of lines) {
|
|
502
501
|
if (l.kind === 'bullet-first' || l.kind === 'bullet-cont') {
|
|
503
|
-
maxBodyW = Math.max(maxBodyW, l.text
|
|
502
|
+
maxBodyW = Math.max(maxBodyW, measureText(l.text, descFont));
|
|
504
503
|
}
|
|
505
504
|
}
|
|
506
505
|
return bulletColRightEdge - maxBodyW - BULLET_BODY_INDENT;
|