@diagrammo/dgmo 0.30.0 → 0.31.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 +21 -3
- package/dist/advanced.cjs +560 -269
- package/dist/advanced.d.cts +27 -2
- package/dist/advanced.d.ts +27 -2
- package/dist/advanced.js +559 -269
- package/dist/auto.cjs +558 -270
- package/dist/auto.js +93 -93
- package/dist/auto.mjs +558 -270
- package/dist/cli.cjs +144 -143
- package/dist/index.cjs +557 -269
- package/dist/index.js +557 -269
- package/package.json +1 -1
- package/src/advanced.ts +3 -0
- package/src/boxes-and-lines/layout-search.ts +214 -0
- package/src/boxes-and-lines/layout.ts +4 -0
- package/src/boxes-and-lines/parser.ts +78 -0
- package/src/boxes-and-lines/renderer.ts +57 -5
- package/src/boxes-and-lines/types.ts +9 -0
- package/src/c4/renderer.ts +7 -5
- package/src/chart-types.ts +2 -2
- package/src/class/renderer.ts +4 -2
- package/src/cli-banner.ts +107 -0
- package/src/cli.ts +13 -0
- package/src/colors.ts +22 -0
- package/src/er/renderer.ts +4 -2
- package/src/graph/flowchart-renderer.ts +4 -2
- package/src/graph/state-renderer.ts +4 -2
- package/src/infra/renderer.ts +8 -4
- package/src/journey-map/parser.ts +15 -1
- package/src/journey-map/renderer.ts +1 -1
- package/src/kanban/renderer.ts +1 -1
- package/src/map/renderer.ts +27 -14
- package/src/mindmap/renderer.ts +5 -3
- package/src/org/renderer.ts +67 -120
- package/src/palettes/color-utils.ts +7 -2
- package/src/pert/renderer.ts +13 -8
- package/src/raci/renderer.ts +1 -1
- package/src/sitemap/renderer.ts +35 -37
- package/src/utils/card.ts +183 -0
- package/src/utils/tag-groups.ts +10 -10
- package/src/utils/visual-conventions.ts +61 -0
package/src/colors.ts
CHANGED
|
@@ -60,6 +60,28 @@ export const RECOGNIZED_COLOR_NAMES = Object.freeze([
|
|
|
60
60
|
'white',
|
|
61
61
|
] as const);
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* The canonical order in which the categorical (non-neutral) color names are
|
|
65
|
+
* auto-assigned: tag/group swatches (`autoTagColorCycle`) and data-chart
|
|
66
|
+
* series colors (`getSeriesColors`) both derive their rotation from this list.
|
|
67
|
+
*
|
|
68
|
+
* Seeded with the RGB primaries (`red, green, blue`) for an unmistakable first
|
|
69
|
+
* three, then each subsequent hue is chosen to fill the widest remaining gap on
|
|
70
|
+
* the color wheel — maximizing contrast between adjacent swatches. Neutrals
|
|
71
|
+
* (`gray`/`black`/`white`) are intentionally excluded so auto-picked colors
|
|
72
|
+
* always read as distinct legend swatches.
|
|
73
|
+
*/
|
|
74
|
+
export const CATEGORICAL_COLOR_ORDER = Object.freeze([
|
|
75
|
+
'red',
|
|
76
|
+
'green',
|
|
77
|
+
'blue',
|
|
78
|
+
'yellow',
|
|
79
|
+
'teal',
|
|
80
|
+
'purple',
|
|
81
|
+
'orange',
|
|
82
|
+
'cyan',
|
|
83
|
+
] as const);
|
|
84
|
+
|
|
63
85
|
/**
|
|
64
86
|
* Returns true iff `name` is one of the 11 recognized DGMO color names.
|
|
65
87
|
*/
|
package/src/er/renderer.ts
CHANGED
|
@@ -46,8 +46,10 @@ const MAX_SCALE = 3;
|
|
|
46
46
|
const TABLE_FONT_SIZE = 13;
|
|
47
47
|
const COLUMN_FONT_SIZE = 11;
|
|
48
48
|
const EDGE_LABEL_FONT_SIZE = 11;
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
import {
|
|
50
|
+
EDGE_STROKE_WIDTH,
|
|
51
|
+
NODE_STROKE_WIDTH,
|
|
52
|
+
} from '../utils/visual-conventions'; // shared (Story 111.1)
|
|
51
53
|
const MEMBER_LINE_HEIGHT = 18;
|
|
52
54
|
const COMPARTMENT_PADDING_Y = 8;
|
|
53
55
|
const MEMBER_PADDING_X = 10;
|
|
@@ -36,8 +36,10 @@ const DIAGRAM_PADDING = 20;
|
|
|
36
36
|
const MAX_SCALE = 3;
|
|
37
37
|
const NODE_FONT_SIZE = 13;
|
|
38
38
|
const EDGE_LABEL_FONT_SIZE = 11;
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
import {
|
|
40
|
+
EDGE_STROKE_WIDTH,
|
|
41
|
+
NODE_STROKE_WIDTH,
|
|
42
|
+
} from '../utils/visual-conventions'; // shared (Story 111.1)
|
|
41
43
|
const ARROWHEAD_W = 10;
|
|
42
44
|
const ARROWHEAD_H = 7;
|
|
43
45
|
const IO_SKEW = 15;
|
|
@@ -37,8 +37,10 @@ const MAX_SCALE = 3;
|
|
|
37
37
|
const NODE_FONT_SIZE = 13;
|
|
38
38
|
const EDGE_LABEL_FONT_SIZE = 11;
|
|
39
39
|
const GROUP_LABEL_FONT_SIZE = 11;
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
import {
|
|
41
|
+
EDGE_STROKE_WIDTH,
|
|
42
|
+
NODE_STROKE_WIDTH,
|
|
43
|
+
} from '../utils/visual-conventions'; // shared (Story 111.1)
|
|
42
44
|
const ARROWHEAD_W = 10;
|
|
43
45
|
const ARROWHEAD_H = 7;
|
|
44
46
|
const PSEUDOSTATE_RADIUS = 10;
|
package/src/infra/renderer.ts
CHANGED
|
@@ -57,20 +57,24 @@ import { ScaleContext } from '../utils/scaling';
|
|
|
57
57
|
// ============================================================
|
|
58
58
|
|
|
59
59
|
const NODE_FONT_SIZE = 13;
|
|
60
|
+
// Intentional deviation (conventions §1): infra uses denser meta rows
|
|
61
|
+
// (10px font / 14px line height) than the 11/16 default.
|
|
60
62
|
const META_FONT_SIZE = 10;
|
|
61
63
|
const META_LINE_HEIGHT = 14;
|
|
62
64
|
const EDGE_LABEL_FONT_SIZE = 11;
|
|
63
65
|
const GROUP_LABEL_FONT_SIZE = 14;
|
|
64
66
|
const NODE_BORDER_RADIUS = 8;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
import {
|
|
68
|
+
EDGE_STROKE_WIDTH,
|
|
69
|
+
NODE_STROKE_WIDTH,
|
|
70
|
+
COLLAPSE_BAR_HEIGHT,
|
|
71
|
+
COLLAPSE_BAR_INSET,
|
|
72
|
+
} from '../utils/visual-conventions'; // shared (Story 111.1)
|
|
67
73
|
const OVERLOAD_STROKE_WIDTH = 3;
|
|
68
74
|
const ROLE_DOT_RADIUS = 3;
|
|
69
75
|
const NODE_HEADER_HEIGHT = 28;
|
|
70
76
|
const NODE_SEPARATOR_GAP = 4;
|
|
71
77
|
const NODE_PAD_BOTTOM = 10;
|
|
72
|
-
const COLLAPSE_BAR_HEIGHT = 6;
|
|
73
|
-
const COLLAPSE_BAR_INSET = 0;
|
|
74
78
|
|
|
75
79
|
const LEGEND_FIXED_GAP = 16; // gap between fixed legend and scaled diagram — local, not shared
|
|
76
80
|
const SPEED_BADGE_H_PAD = 5; // horizontal padding inside active speed badge
|
|
@@ -184,7 +184,21 @@ export function parseJourneyMap(
|
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
186
|
} else {
|
|
187
|
-
|
|
187
|
+
// Same-line form (pipes removed in 0.18.0): peel a trailing
|
|
188
|
+
// `color: <token>` off the rest-of-line persona name.
|
|
189
|
+
const colorMatch = afterKeyword.match(/^(.+?)\s+color:\s*(\S+)$/i);
|
|
190
|
+
if (colorMatch) {
|
|
191
|
+
personaName = colorMatch[1]!.trim();
|
|
192
|
+
personaColor =
|
|
193
|
+
resolveColorWithDiagnostic(
|
|
194
|
+
colorMatch[2]!,
|
|
195
|
+
lineNumber,
|
|
196
|
+
result.diagnostics,
|
|
197
|
+
palette
|
|
198
|
+
) ?? undefined;
|
|
199
|
+
} else {
|
|
200
|
+
personaName = afterKeyword;
|
|
201
|
+
}
|
|
188
202
|
}
|
|
189
203
|
|
|
190
204
|
if (!personaName) {
|
|
@@ -52,7 +52,7 @@ export interface JourneyMapInteractiveOptions {
|
|
|
52
52
|
// Match kanban styling constants
|
|
53
53
|
const DIAGRAM_PADDING = 20;
|
|
54
54
|
const PADDING = DIAGRAM_PADDING;
|
|
55
|
-
|
|
55
|
+
import { CARD_RADIUS } from '../utils/visual-conventions'; // shared (Story 111.1)
|
|
56
56
|
const CARD_PADDING_X = 10;
|
|
57
57
|
const CARD_PADDING_Y = 6;
|
|
58
58
|
const CARD_HEADER_HEIGHT = 24;
|
package/src/kanban/renderer.ts
CHANGED
|
@@ -55,7 +55,7 @@ const CARD_HEADER_HEIGHT = 24;
|
|
|
55
55
|
const CARD_META_LINE_HEIGHT = 14;
|
|
56
56
|
const CARD_SEPARATOR_GAP = 4;
|
|
57
57
|
const CARD_GAP = 8;
|
|
58
|
-
|
|
58
|
+
import { CARD_RADIUS } from '../utils/visual-conventions'; // shared (Story 111.1)
|
|
59
59
|
const CARD_PADDING_X = 10;
|
|
60
60
|
const CARD_PADDING_Y = 6;
|
|
61
61
|
const CARD_STROKE_WIDTH = 1.5;
|
package/src/map/renderer.ts
CHANGED
|
@@ -219,6 +219,15 @@ function appendWaterLines(
|
|
|
219
219
|
}
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
// Per-render namespace for SVG def ids (clipPaths, masks, filters, markers, the
|
|
223
|
+
// `<use>`-shared coast path). SVG `url(#id)` / `href="#id"` resolve document-
|
|
224
|
+
// globally to the FIRST matching id, so when several maps are inlined on one
|
|
225
|
+
// page (the docs gallery, an MDX page) shared constant ids made every later map
|
|
226
|
+
// reference the first map's defs — its coast `<use>` ghosted through. A
|
|
227
|
+
// monotonic per-render suffix makes every render's ids unique. NOT reset between
|
|
228
|
+
// renders, so re-renders (legend flips) and same-page siblings never collide.
|
|
229
|
+
let mapInstanceCounter = 0;
|
|
230
|
+
|
|
222
231
|
/** Render a resolved map into `container` (d3-selection appends an `<svg>`). */
|
|
223
232
|
export function renderMap(
|
|
224
233
|
container: HTMLDivElement,
|
|
@@ -283,6 +292,10 @@ export function renderMap(
|
|
|
283
292
|
// arrowhead up to a giant wedge. The size grows gently with the line width —
|
|
284
293
|
// enough to stay distinct from the stroke — but is firmly capped.
|
|
285
294
|
const defs = svg.append('defs');
|
|
295
|
+
// Namespace every def id minted below so multiple maps on one page don't share
|
|
296
|
+
// `url(#…)` targets (see mapInstanceCounter).
|
|
297
|
+
const uid = mapInstanceCounter++;
|
|
298
|
+
const nid = (base: string): string => `${base}__m${uid}`;
|
|
286
299
|
// Dampened: ~8px at the thinnest leg, easing toward a 15px cap as legs widen.
|
|
287
300
|
const arrowSize = (w: number): number => Math.min(15, 7 + w * 0.95);
|
|
288
301
|
|
|
@@ -356,8 +369,8 @@ export function renderMap(
|
|
|
356
369
|
// sub-pixel + low-contrast so the texture stays faint. Decorative — no data attrs.
|
|
357
370
|
if (layout.relief.length && layout.reliefHatch) {
|
|
358
371
|
const h = layout.reliefHatch;
|
|
359
|
-
const rangeClipId = 'dgmo-relief-clip';
|
|
360
|
-
const landClipId = 'dgmo-relief-land';
|
|
372
|
+
const rangeClipId = nid('dgmo-relief-clip');
|
|
373
|
+
const landClipId = nid('dgmo-relief-land');
|
|
361
374
|
const rangeClip = defs.append('clipPath').attr('id', rangeClipId);
|
|
362
375
|
for (const s of layout.relief) rangeClip.append('path').attr('d', s.d);
|
|
363
376
|
const landClip = defs.append('clipPath').attr('id', landClipId);
|
|
@@ -402,7 +415,7 @@ export function renderMap(
|
|
|
402
415
|
// §24B.2, ADR-1/3/6.
|
|
403
416
|
if (layout.coastlineStyle) {
|
|
404
417
|
const cs = layout.coastlineStyle;
|
|
405
|
-
const maskId = 'dgmo-map-water-mask';
|
|
418
|
+
const maskId = nid('dgmo-map-water-mask');
|
|
406
419
|
const mask = defs
|
|
407
420
|
.append('mask')
|
|
408
421
|
.attr('id', maskId)
|
|
@@ -472,7 +485,7 @@ export function renderMap(
|
|
|
472
485
|
appendWaterLines(
|
|
473
486
|
gWater,
|
|
474
487
|
defs,
|
|
475
|
-
'dgmo-map-coast',
|
|
488
|
+
nid('dgmo-map-coast'),
|
|
476
489
|
// Pass the canvas frame so edges collinear with it (the antimeridian on a
|
|
477
490
|
// world map, regional clipExtent cuts) don't get ringed as fake coast —
|
|
478
491
|
// land runs cleanly to the render-area edge.
|
|
@@ -565,7 +578,7 @@ export function renderMap(
|
|
|
565
578
|
(l) => l.poiId !== undefined && !l.hidden
|
|
566
579
|
);
|
|
567
580
|
if (poiLabels.length) {
|
|
568
|
-
const patchBlurId = 'dgmo-map-label-patch-blur';
|
|
581
|
+
const patchBlurId = nid('dgmo-map-label-patch-blur');
|
|
569
582
|
// Soft falloff so the patch dissolves into the surrounding basemap instead of
|
|
570
583
|
// ending on a hard edge. Tuned at the 11px label font. One shared filter for
|
|
571
584
|
// every patch group.
|
|
@@ -690,7 +703,7 @@ export function renderMap(
|
|
|
690
703
|
// Always-on patch for labels that are visible at rest (non-cluster members).
|
|
691
704
|
buildPatch(
|
|
692
705
|
poiLabels.filter((l) => l.clusterMember === undefined),
|
|
693
|
-
'dgmo-map-label-patch'
|
|
706
|
+
nid('dgmo-map-label-patch')
|
|
694
707
|
);
|
|
695
708
|
// Per-cluster patches, hidden until the fan expands.
|
|
696
709
|
const byCluster = new Map<string, typeof poiLabels>();
|
|
@@ -705,11 +718,11 @@ export function renderMap(
|
|
|
705
718
|
// data-cluster-deco attribute instead.
|
|
706
719
|
let ci = 0;
|
|
707
720
|
for (const [cid, labs] of byCluster)
|
|
708
|
-
buildPatch(labs, `dgmo-map-label-patch-c${ci++}
|
|
721
|
+
buildPatch(labs, nid(`dgmo-map-label-patch-c${ci++}`), cid);
|
|
709
722
|
} else {
|
|
710
723
|
// Export / `no-cluster-pois`: fan is permanently open → one always-on patch
|
|
711
724
|
// over every POI label.
|
|
712
|
-
buildPatch(poiLabels, 'dgmo-map-label-patch');
|
|
725
|
+
buildPatch(poiLabels, nid('dgmo-map-label-patch'));
|
|
713
726
|
}
|
|
714
727
|
}
|
|
715
728
|
|
|
@@ -734,7 +747,7 @@ export function renderMap(
|
|
|
734
747
|
// Neighbour land (Canada beside Alaska) clipped to this box, behind the
|
|
735
748
|
// state — so a land border reads as land rather than sprouting coast rings.
|
|
736
749
|
if (box.contextLand) {
|
|
737
|
-
const clipId = `dgmo-map-inset-clip-${bi}
|
|
750
|
+
const clipId = nid(`dgmo-map-inset-clip-${bi}`);
|
|
738
751
|
defs.append('clipPath').attr('id', clipId).append('path').attr('d', d);
|
|
739
752
|
insetG
|
|
740
753
|
.append('path')
|
|
@@ -751,7 +764,7 @@ export function renderMap(
|
|
|
751
764
|
// same way. Inside the inset group so it composites over the box fills.
|
|
752
765
|
if (layout.coastlineStyle) {
|
|
753
766
|
const cs = layout.coastlineStyle;
|
|
754
|
-
const maskId = 'dgmo-map-inset-water-mask';
|
|
767
|
+
const maskId = nid('dgmo-map-inset-water-mask');
|
|
755
768
|
const mask = defs
|
|
756
769
|
.append('mask')
|
|
757
770
|
.attr('id', maskId)
|
|
@@ -786,7 +799,7 @@ export function renderMap(
|
|
|
786
799
|
.append('path')
|
|
787
800
|
.attr('d', box.contextLand.d)
|
|
788
801
|
.attr('fill', 'black')
|
|
789
|
-
.attr('clip-path', `url(
|
|
802
|
+
.attr('clip-path', `url(#${nid(`dgmo-map-inset-clip-${bi}`)})`);
|
|
790
803
|
});
|
|
791
804
|
for (const r of layout.insetRegions)
|
|
792
805
|
if (r.id !== 'lake')
|
|
@@ -798,7 +811,7 @@ export function renderMap(
|
|
|
798
811
|
// which side reads as water, but SVG strokes still extend stroke-width/2
|
|
799
812
|
// past their path, so without this the seaward rings bleed over the box
|
|
800
813
|
// border. Union of all inset quads = one clipPath shared by the group.
|
|
801
|
-
const clipId = 'dgmo-map-inset-water-clip';
|
|
814
|
+
const clipId = nid('dgmo-map-inset-water-clip');
|
|
802
815
|
const clip = defs.append('clipPath').attr('id', clipId);
|
|
803
816
|
for (const box of layout.insets) {
|
|
804
817
|
const d =
|
|
@@ -820,7 +833,7 @@ export function renderMap(
|
|
|
820
833
|
appendWaterLines(
|
|
821
834
|
gInsetWater,
|
|
822
835
|
defs,
|
|
823
|
-
'dgmo-map-inset-coast',
|
|
836
|
+
nid('dgmo-map-inset-coast'),
|
|
824
837
|
coastlineOuterRings(layout.insetRegions, cs.minExtent),
|
|
825
838
|
cs,
|
|
826
839
|
layout.background
|
|
@@ -895,7 +908,7 @@ export function renderMap(
|
|
|
895
908
|
// stroke is enough for the line widths legs use.
|
|
896
909
|
wireSync(p, leg.lineNumber);
|
|
897
910
|
if (leg.arrow) {
|
|
898
|
-
const id = `dgmo-map-arrow-${i}
|
|
911
|
+
const id = nid(`dgmo-map-arrow-${i}`);
|
|
899
912
|
const s = arrowSize(leg.width);
|
|
900
913
|
defs
|
|
901
914
|
.append('marker')
|
package/src/mindmap/renderer.ts
CHANGED
|
@@ -35,9 +35,11 @@ const LABEL_LINE_HEIGHT = 18;
|
|
|
35
35
|
const DESC_LINE_HEIGHT = 14;
|
|
36
36
|
const NODE_RADIUS = 6;
|
|
37
37
|
const ROOT_STROKE_WIDTH = 2.5;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
import {
|
|
39
|
+
NODE_STROKE_WIDTH,
|
|
40
|
+
EDGE_STROKE_WIDTH,
|
|
41
|
+
COLLAPSE_BAR_HEIGHT,
|
|
42
|
+
} from '../utils/visual-conventions'; // shared (Story 111.1)
|
|
41
43
|
|
|
42
44
|
function nodeFill(
|
|
43
45
|
palette: PaletteColors,
|
package/src/org/renderer.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
EYE_CLOSED_PATH,
|
|
27
27
|
} from '../utils/legend-constants';
|
|
28
28
|
import { renderIntegratedLegend } from '../utils/legend-integration';
|
|
29
|
+
import { renderNodeCard, renderCollapseBar } from '../utils/card';
|
|
29
30
|
import { measureText } from '../utils/text-measure';
|
|
30
31
|
import { getMaxLegendReservedHeight } from '../utils/legend-layout';
|
|
31
32
|
import type { LegendConfig } from '../utils/legend-types';
|
|
@@ -38,23 +39,25 @@ const DIAGRAM_PADDING = 20;
|
|
|
38
39
|
const MAX_SCALE = 3;
|
|
39
40
|
import { TITLE_FONT_SIZE, TITLE_FONT_WEIGHT } from '../utils/title-constants';
|
|
40
41
|
const TITLE_HEIGHT = 30;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
42
|
+
// Shared card / group / collapse constants (Story 111.1). org matches every
|
|
43
|
+
// convention default, so it imports the full set.
|
|
44
|
+
import {
|
|
45
|
+
LABEL_FONT_SIZE,
|
|
46
|
+
META_FONT_SIZE,
|
|
47
|
+
META_LINE_HEIGHT,
|
|
48
|
+
HEADER_HEIGHT,
|
|
49
|
+
SEPARATOR_GAP,
|
|
50
|
+
EDGE_STROKE_WIDTH,
|
|
51
|
+
NODE_STROKE_WIDTH,
|
|
52
|
+
CARD_RADIUS,
|
|
53
|
+
CONTAINER_RADIUS,
|
|
54
|
+
CONTAINER_LABEL_FONT_SIZE,
|
|
55
|
+
CONTAINER_META_FONT_SIZE,
|
|
56
|
+
CONTAINER_META_LINE_HEIGHT,
|
|
57
|
+
CONTAINER_HEADER_HEIGHT,
|
|
58
|
+
COLLAPSE_BAR_HEIGHT,
|
|
59
|
+
COLLAPSE_BAR_INSET,
|
|
60
|
+
} from '../utils/visual-conventions';
|
|
58
61
|
|
|
59
62
|
// Ancestor breadcrumb trail (focus mode)
|
|
60
63
|
const ANCESTOR_DOT_R = 4;
|
|
@@ -360,21 +363,16 @@ export function renderOrg(
|
|
|
360
363
|
}
|
|
361
364
|
|
|
362
365
|
if (!exportDims && c.hiddenCount && c.hiddenCount > 0) {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
.
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
.attr('width', c.width - sCollapseBarInset * 2)
|
|
374
|
-
.attr('height', sCollapseBarHeight)
|
|
375
|
-
.attr('fill', containerStroke(palette, colorOff ? undefined : c.color))
|
|
376
|
-
.attr('clip-path', `url(#${clipId})`)
|
|
377
|
-
.attr('class', 'org-collapse-bar');
|
|
366
|
+
renderCollapseBar(cG, {
|
|
367
|
+
width: c.width,
|
|
368
|
+
height: c.height,
|
|
369
|
+
barHeight: sCollapseBarHeight,
|
|
370
|
+
inset: sCollapseBarInset,
|
|
371
|
+
rx: sContainerRadius,
|
|
372
|
+
fill: containerStroke(palette, colorOff ? undefined : c.color),
|
|
373
|
+
clipId: `clip-${c.nodeId}`,
|
|
374
|
+
className: 'org-collapse-bar',
|
|
375
|
+
});
|
|
378
376
|
}
|
|
379
377
|
|
|
380
378
|
// Focus icon (hover-reveal, interactive only) — for non-root containers with children
|
|
@@ -489,103 +487,52 @@ export function renderOrg(
|
|
|
489
487
|
);
|
|
490
488
|
const stroke = nodeStroke(palette, colorOff ? undefined : node.color);
|
|
491
489
|
|
|
492
|
-
const rect = nodeG
|
|
493
|
-
.append('rect')
|
|
494
|
-
.attr('x', 0)
|
|
495
|
-
.attr('y', 0)
|
|
496
|
-
.attr('width', node.width)
|
|
497
|
-
.attr('height', node.height)
|
|
498
|
-
.attr('rx', sCardRadius)
|
|
499
|
-
.attr('fill', fill)
|
|
500
|
-
.attr('stroke', stroke)
|
|
501
|
-
.attr('stroke-width', sNodeStrokeWidth);
|
|
502
|
-
|
|
503
|
-
if (node.isContainer) {
|
|
504
|
-
rect.attr('stroke-dasharray', '6 3');
|
|
505
|
-
}
|
|
506
|
-
|
|
507
490
|
const labelColor = contrastText(
|
|
508
491
|
fill,
|
|
509
492
|
palette.textOnFillLight,
|
|
510
493
|
palette.textOnFillDark
|
|
511
494
|
);
|
|
512
|
-
nodeG
|
|
513
|
-
.append('text')
|
|
514
|
-
.attr('x', node.width / 2)
|
|
515
|
-
.attr('y', sHeaderHeight / 2 + sLabelFontSize / 2 - 2)
|
|
516
|
-
.attr('text-anchor', 'middle')
|
|
517
|
-
.attr('fill', labelColor)
|
|
518
|
-
.attr('font-size', sLabelFontSize)
|
|
519
|
-
.attr('font-weight', 'bold')
|
|
520
|
-
.text(node.label);
|
|
521
495
|
|
|
522
496
|
const metaEntries = Object.entries(node.metadata);
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
nodeG
|
|
549
|
-
.append('text')
|
|
550
|
-
.attr('x', 10)
|
|
551
|
-
.attr('y', rowY)
|
|
552
|
-
.attr('fill', labelColor)
|
|
553
|
-
.attr('font-size', sMetaFontSize)
|
|
554
|
-
.text(`${displayKey}: `);
|
|
555
|
-
|
|
556
|
-
nodeG
|
|
557
|
-
.append('text')
|
|
558
|
-
.attr('x', valueX)
|
|
559
|
-
.attr('y', rowY)
|
|
560
|
-
.attr('fill', labelColor)
|
|
561
|
-
.attr('font-size', sMetaFontSize)
|
|
562
|
-
.text(value);
|
|
563
|
-
}
|
|
564
|
-
}
|
|
497
|
+
renderNodeCard(nodeG, {
|
|
498
|
+
width: node.width,
|
|
499
|
+
height: node.height,
|
|
500
|
+
rx: sCardRadius,
|
|
501
|
+
fill,
|
|
502
|
+
stroke,
|
|
503
|
+
strokeWidth: sNodeStrokeWidth,
|
|
504
|
+
...(node.isContainer && { dashed: true }),
|
|
505
|
+
label: node.label,
|
|
506
|
+
labelColor,
|
|
507
|
+
labelFontSize: sLabelFontSize,
|
|
508
|
+
headerHeight: sHeaderHeight,
|
|
509
|
+
...(metaEntries.length > 0 && {
|
|
510
|
+
meta: {
|
|
511
|
+
rows: metaEntries.map(
|
|
512
|
+
([k, value]) => [displayNames.get(k) ?? k, value] as const
|
|
513
|
+
),
|
|
514
|
+
fontSize: sMetaFontSize,
|
|
515
|
+
lineHeight: sMetaLineHeight,
|
|
516
|
+
separatorGap: sSeparatorGap,
|
|
517
|
+
separatorColor: solid ? labelColor : stroke,
|
|
518
|
+
textColor: labelColor,
|
|
519
|
+
},
|
|
520
|
+
}),
|
|
521
|
+
});
|
|
565
522
|
|
|
566
523
|
if (!exportDims && node.hiddenCount && node.hiddenCount > 0) {
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
.
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
.
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
.attr('width', node.width - sCollapseBarInset * 2)
|
|
580
|
-
.attr('height', sCollapseBarHeight)
|
|
581
|
-
.attr(
|
|
582
|
-
'fill',
|
|
583
|
-
solid
|
|
584
|
-
? labelColor
|
|
585
|
-
: nodeStroke(palette, colorOff ? undefined : node.color)
|
|
586
|
-
)
|
|
587
|
-
.attr('clip-path', `url(#${clipId})`)
|
|
588
|
-
.attr('class', 'org-collapse-bar');
|
|
524
|
+
renderCollapseBar(nodeG, {
|
|
525
|
+
width: node.width,
|
|
526
|
+
height: node.height,
|
|
527
|
+
barHeight: sCollapseBarHeight,
|
|
528
|
+
inset: sCollapseBarInset,
|
|
529
|
+
rx: sCardRadius,
|
|
530
|
+
fill: solid
|
|
531
|
+
? labelColor
|
|
532
|
+
: nodeStroke(palette, colorOff ? undefined : node.color),
|
|
533
|
+
clipId: `clip-${node.id}`,
|
|
534
|
+
className: 'org-collapse-bar',
|
|
535
|
+
});
|
|
589
536
|
}
|
|
590
537
|
|
|
591
538
|
// Focus icon (hover-reveal, interactive only) — for non-root nodes with children
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CATEGORICAL_COLOR_ORDER } from '../colors';
|
|
1
2
|
import type { PaletteColors } from './types';
|
|
2
3
|
|
|
3
4
|
// ============================================================
|
|
@@ -318,10 +319,14 @@ export function shapeFill(
|
|
|
318
319
|
// Series Colors
|
|
319
320
|
// ============================================================
|
|
320
321
|
|
|
321
|
-
/**
|
|
322
|
+
/**
|
|
323
|
+
* Derive the 8-color series rotation from a palette's named colors, in the
|
|
324
|
+
* shared {@link CATEGORICAL_COLOR_ORDER} (RGB-seeded, max-contrast). Tag
|
|
325
|
+
* swatches and chart series colors thus share one canonical rotation.
|
|
326
|
+
*/
|
|
322
327
|
export function getSeriesColors(palette: PaletteColors): string[] {
|
|
323
328
|
const c = palette.colors;
|
|
324
|
-
return
|
|
329
|
+
return CATEGORICAL_COLOR_ORDER.map((name) => c[name]!);
|
|
325
330
|
}
|
|
326
331
|
|
|
327
332
|
/**
|
package/src/pert/renderer.ts
CHANGED
|
@@ -87,7 +87,16 @@ const NODE_CELL_FONT_SIZE = 11;
|
|
|
87
87
|
// row holds the name and is taller than the corner cells so the name
|
|
88
88
|
// reads as the primary label (mirroring textbook proportions).
|
|
89
89
|
const NODE_RADIUS = 6;
|
|
90
|
-
|
|
90
|
+
// Shared card / group / collapse constants (Story 111.1). The explanatory
|
|
91
|
+
// comments below stay with the renderer; the values now live in one module.
|
|
92
|
+
import {
|
|
93
|
+
NODE_STROKE_WIDTH,
|
|
94
|
+
EDGE_STROKE_WIDTH,
|
|
95
|
+
CONTAINER_RADIUS,
|
|
96
|
+
CONTAINER_LABEL_FONT_SIZE,
|
|
97
|
+
CONTAINER_HEADER_HEIGHT,
|
|
98
|
+
COLLAPSE_BAR_HEIGHT,
|
|
99
|
+
} from '../utils/visual-conventions';
|
|
91
100
|
|
|
92
101
|
// Analysis-block chrome (Summary / Activity Risk / Completion / Field
|
|
93
102
|
// labels). These sit BELOW the diagram and shouldn't compete with it
|
|
@@ -113,18 +122,14 @@ const NODE_BOTTOM_ROW_HEIGHT = 26;
|
|
|
113
122
|
// stroke and a matching red arrowhead because the critical path is the
|
|
114
123
|
// central concept of a PERT chart, and a binary `data-critical` attr
|
|
115
124
|
// alone left it visually invisible to readers.
|
|
116
|
-
const EDGE_STROKE_WIDTH = 1.5;
|
|
117
125
|
const ARROWHEAD_W = 10;
|
|
118
126
|
const ARROWHEAD_H = 7;
|
|
119
127
|
// Group-rect treatment per §2: neutral surface fill on textMuted stroke,
|
|
120
128
|
// solid border, rx=8, top-center 13pt 'bold' label inside a reserved
|
|
121
129
|
// 28px header band — exactly matching org's container recipe.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
// Collapse-bar height — see conventions doc §3 Pattern A/B (matches
|
|
126
|
-
// org's `COLLAPSE_BAR_HEIGHT`). Universal "this is collapsed" signal.
|
|
127
|
-
const COLLAPSE_BAR_HEIGHT = 6;
|
|
130
|
+
// CONTAINER_RADIUS/LABEL_FONT_SIZE/HEADER_HEIGHT + COLLAPSE_BAR_HEIGHT now
|
|
131
|
+
// imported from utils/visual-conventions (Story 111.1). Group-rect treatment
|
|
132
|
+
// per §2; collapse-bar height matches org per §3 Pattern A/B.
|
|
128
133
|
// Always-on fade applied to bottom-20% (by duration) activity nodes
|
|
129
134
|
// so the eye is drawn to the longer, schedule-dominating work first.
|
|
130
135
|
// Less aggressive than FADE_OPACITY because these cards still need to
|
package/src/raci/renderer.ts
CHANGED
|
@@ -267,7 +267,7 @@ const TINT_PCT = 25;
|
|
|
267
267
|
* kanban `CARD_RADIUS = 6`). Keeps RACI markers visually consistent
|
|
268
268
|
* with nodes elsewhere in the diagram language.
|
|
269
269
|
*/
|
|
270
|
-
|
|
270
|
+
import { NODE_STROKE_WIDTH } from '../utils/visual-conventions'; // shared (Story 111.1)
|
|
271
271
|
const NODE_RADIUS = 6;
|
|
272
272
|
|
|
273
273
|
/**
|