@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
package/src/cycle/renderer.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
} from './types';
|
|
32
32
|
import { computeCycleLayout, wrapEdgeLabelText } from './layout';
|
|
33
33
|
import { ScaleContext } from '../utils/scaling';
|
|
34
|
+
import { measureText } from '../utils/text-measure';
|
|
34
35
|
|
|
35
36
|
// ── Constants ────────────────────────────────────────────────
|
|
36
37
|
const NODE_FONT_SIZE = 13;
|
|
@@ -502,12 +503,16 @@ export function renderCycle(
|
|
|
502
503
|
const anchor = isRight ? 'start' : isLeft ? 'end' : 'middle';
|
|
503
504
|
|
|
504
505
|
const lineCount = labelLines.length + descLines.length;
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
506
|
+
// Measure the widest rendered line in pixels at the scaled edge-label font
|
|
507
|
+
// so the background box matches the actual ink (same measurer the layout
|
|
508
|
+
// uses to size + place the label).
|
|
509
|
+
let maxLineW = 0;
|
|
510
|
+
for (const l of labelLines)
|
|
511
|
+
maxLineW = Math.max(maxLineW, measureText(l, scaledEdgeLabelFont));
|
|
512
|
+
for (const l of descLines)
|
|
513
|
+
maxLineW = Math.max(maxLineW, measureText(l, scaledDescFont));
|
|
514
|
+
|
|
515
|
+
const bgW = maxLineW + 12;
|
|
511
516
|
const bgH = lineCount * scaledEdgeLineH + 6;
|
|
512
517
|
const bgX = isRight
|
|
513
518
|
? le.labelX - 4
|
package/src/d3.ts
CHANGED
|
@@ -6,6 +6,7 @@ import cloud from 'd3-cloud';
|
|
|
6
6
|
import { FONT_FAMILY } from './fonts';
|
|
7
7
|
import { computeQuadrantPointLabels, type LabelRect } from './label-layout';
|
|
8
8
|
import { MONTH_ABBR, computeTimeTicks } from './utils/time-ticks';
|
|
9
|
+
import { measureText, wrapTextToWidth } from './utils/text-measure';
|
|
9
10
|
import type { D3ExportDimensions } from './utils/d3-types';
|
|
10
11
|
import { ScaleContext } from './utils/scaling';
|
|
11
12
|
|
|
@@ -140,6 +141,8 @@ export interface ParsedVisualization {
|
|
|
140
141
|
timelineDefaultSwimlaneTG?: string;
|
|
141
142
|
timelineScale: boolean;
|
|
142
143
|
timelineSwimlanes: boolean;
|
|
144
|
+
/** Authored `active-tag <group|none|metric>` directive (§15.6); resolved at render. */
|
|
145
|
+
timelineActiveTag?: string;
|
|
143
146
|
vennSets: VennSet[];
|
|
144
147
|
vennOverlaps: VennOverlap[];
|
|
145
148
|
// Quadrant chart fields
|
|
@@ -181,6 +184,7 @@ import {
|
|
|
181
184
|
import {
|
|
182
185
|
collectIndentedValues,
|
|
183
186
|
extractColor,
|
|
187
|
+
measureIndent,
|
|
184
188
|
normalizeNumericToken,
|
|
185
189
|
parseFirstLine,
|
|
186
190
|
peelTrailingColorName,
|
|
@@ -396,7 +400,7 @@ export function parseVisualization(
|
|
|
396
400
|
// In-bounds by loop guard.
|
|
397
401
|
const rawLine = lines[i]!;
|
|
398
402
|
const line = rawLine.trim();
|
|
399
|
-
const indent = rawLine
|
|
403
|
+
const indent = measureIndent(rawLine);
|
|
400
404
|
const lineNumber = i + 1;
|
|
401
405
|
|
|
402
406
|
// Skip empty lines
|
|
@@ -800,6 +804,27 @@ export function parseVisualization(
|
|
|
800
804
|
}
|
|
801
805
|
}
|
|
802
806
|
|
|
807
|
+
// Timeline display directives (§15.6, space-separated, no colon):
|
|
808
|
+
// no-scale — drop the date axis/scale
|
|
809
|
+
// swimlanes — force lane backgrounds
|
|
810
|
+
// active-tag <X> — authored active tag group / `none` / value-ramp label
|
|
811
|
+
if (result.type === 'timeline') {
|
|
812
|
+
const lower = line.toLowerCase();
|
|
813
|
+
if (lower === 'no-scale') {
|
|
814
|
+
result.timelineScale = false;
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
if (lower === 'swimlanes') {
|
|
818
|
+
result.timelineSwimlanes = true;
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
const activeTagMatch = line.match(/^active-tag\s+(.+)$/i);
|
|
822
|
+
if (activeTagMatch) {
|
|
823
|
+
result.timelineActiveTag = activeTagMatch[1]!.trim();
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
803
828
|
// Timeline event lines: date-first syntax
|
|
804
829
|
if (result.type === 'timeline') {
|
|
805
830
|
const tlRegistry = withTagAliases(
|
|
@@ -1821,7 +1846,6 @@ export function resolveVerticalCollisions(
|
|
|
1821
1846
|
|
|
1822
1847
|
const SLOPE_MARGIN = { top: 80, bottom: 40, left: 80 };
|
|
1823
1848
|
const SLOPE_LABEL_FONT_SIZE = 14;
|
|
1824
|
-
const SLOPE_CHAR_WIDTH = 8; // approximate px per character at 14px
|
|
1825
1849
|
|
|
1826
1850
|
/**
|
|
1827
1851
|
* Renders a slope chart into the given container using D3.
|
|
@@ -1850,7 +1874,6 @@ export function renderSlopeChart(
|
|
|
1850
1874
|
const sMarginTop = ctx.aesthetic(SLOPE_MARGIN.top);
|
|
1851
1875
|
const sMarginBottom = ctx.aesthetic(SLOPE_MARGIN.bottom);
|
|
1852
1876
|
const sMarginLeft = ctx.aesthetic(SLOPE_MARGIN.left);
|
|
1853
|
-
const sCharWidth = ctx.structural(SLOPE_CHAR_WIDTH);
|
|
1854
1877
|
const sLabelFontSize = ctx.text(SLOPE_LABEL_FONT_SIZE);
|
|
1855
1878
|
const sPeriodFont = ctx.text(18);
|
|
1856
1879
|
const sValueFont = ctx.text(16);
|
|
@@ -1873,7 +1896,7 @@ export function renderSlopeChart(
|
|
|
1873
1896
|
const text = `${item.values[item.values.length - 1]} — ${item.label}`;
|
|
1874
1897
|
return text.length > longest.length ? text : longest;
|
|
1875
1898
|
}, '');
|
|
1876
|
-
const estimatedLabelWidth = maxLabelText
|
|
1899
|
+
const estimatedLabelWidth = measureText(maxLabelText, sLabelFontSize);
|
|
1877
1900
|
const maxRightMargin = Math.floor(width * 0.35);
|
|
1878
1901
|
const rightMargin = Math.min(
|
|
1879
1902
|
Math.max(estimatedLabelWidth + ctx.aesthetic(30), ctx.aesthetic(120)),
|
|
@@ -1965,24 +1988,11 @@ export function renderSlopeChart(
|
|
|
1965
1988
|
const lastX = xScale(periods[periods.length - 1]!)!;
|
|
1966
1989
|
const labelText = `${lastVal} — ${item.label}`;
|
|
1967
1990
|
const availableWidth = rightMargin - ctx.aesthetic(15);
|
|
1968
|
-
const maxChars = Math.floor(availableWidth / sCharWidth);
|
|
1969
1991
|
|
|
1970
1992
|
let labelLineCount = 1;
|
|
1971
1993
|
let wrappedLines: string[] | null = null;
|
|
1972
|
-
if (labelText
|
|
1973
|
-
const
|
|
1974
|
-
const lines: string[] = [];
|
|
1975
|
-
let current = '';
|
|
1976
|
-
for (const word of words) {
|
|
1977
|
-
const test = current ? `${current} ${word}` : word;
|
|
1978
|
-
if (test.length > maxChars && current) {
|
|
1979
|
-
lines.push(current);
|
|
1980
|
-
current = word;
|
|
1981
|
-
} else {
|
|
1982
|
-
current = test;
|
|
1983
|
-
}
|
|
1984
|
-
}
|
|
1985
|
-
if (current) lines.push(current);
|
|
1994
|
+
if (measureText(labelText, sLabelFontSize) > availableWidth) {
|
|
1995
|
+
const lines = wrapTextToWidth(labelText, sLabelFontSize, availableWidth);
|
|
1986
1996
|
labelLineCount = lines.length;
|
|
1987
1997
|
wrappedLines = lines;
|
|
1988
1998
|
}
|
|
@@ -1999,7 +2009,6 @@ export function renderSlopeChart(
|
|
|
1999
2009
|
tipHtml,
|
|
2000
2010
|
lastX,
|
|
2001
2011
|
labelText,
|
|
2002
|
-
maxChars,
|
|
2003
2012
|
wrappedLines,
|
|
2004
2013
|
labelHeight,
|
|
2005
2014
|
};
|
|
@@ -2790,7 +2799,7 @@ function renderEras(
|
|
|
2790
2799
|
): void {
|
|
2791
2800
|
const eraColors = palette
|
|
2792
2801
|
? getEraColors(palette)
|
|
2793
|
-
: ['#
|
|
2802
|
+
: ['#3b6ea5', '#5b9357', '#c9a227', '#cc7a33', '#7d5ba6'];
|
|
2794
2803
|
eras.forEach((era, i) => {
|
|
2795
2804
|
const startVal = parseTimelineDate(era.startDate);
|
|
2796
2805
|
const endVal = parseTimelineDate(era.endDate);
|
|
@@ -2907,7 +2916,7 @@ function renderMarkers(
|
|
|
2907
2916
|
reservedLabelY?: number
|
|
2908
2917
|
): void {
|
|
2909
2918
|
// Default marker color - bright orange/red that "pops"
|
|
2910
|
-
const defaultColor = palette?.accent || '#
|
|
2919
|
+
const defaultColor = palette?.accent || '#3a9188';
|
|
2911
2920
|
|
|
2912
2921
|
// Pre-compute marker positions so each can size its label based on the
|
|
2913
2922
|
// distance to its nearest neighbor (or chart edge).
|
|
@@ -3995,6 +4004,15 @@ function renderTimelineTagLegendOverlay(
|
|
|
3995
4004
|
.attr('transform', `translate(${x}, ${y})`)
|
|
3996
4005
|
.style('cursor', 'pointer');
|
|
3997
4006
|
|
|
4007
|
+
// Transparent hit area so the whole icon (not just the 2px bars) is clickable
|
|
4008
|
+
iconG
|
|
4009
|
+
.append('rect')
|
|
4010
|
+
.attr('x', -5)
|
|
4011
|
+
.attr('y', -5)
|
|
4012
|
+
.attr('width', 22)
|
|
4013
|
+
.attr('height', 20)
|
|
4014
|
+
.attr('fill', 'transparent');
|
|
4015
|
+
|
|
3998
4016
|
const barColor = isSwimActive ? palette.primary : palette.textMuted;
|
|
3999
4017
|
const barOpacity = isSwimActive ? 1 : 0.35;
|
|
4000
4018
|
const bars = [
|
|
@@ -5789,12 +5807,8 @@ function hasCanvas2d(): boolean {
|
|
|
5789
5807
|
}
|
|
5790
5808
|
}
|
|
5791
5809
|
|
|
5792
|
-
/** Average glyph advance for Inter as a fraction of the font size. Slightly
|
|
5793
|
-
* generous so estimated boxes err toward not overlapping. */
|
|
5794
|
-
const WORDCLOUD_GLYPH_ADVANCE = 0.62;
|
|
5795
|
-
|
|
5796
5810
|
function estimateWordWidth(text: string, size: number): number {
|
|
5797
|
-
return text
|
|
5811
|
+
return measureText(text, size);
|
|
5798
5812
|
}
|
|
5799
5813
|
|
|
5800
5814
|
type PlacedCloudWord = WordCloudWord & {
|
|
@@ -6290,12 +6304,15 @@ export function renderVenn(
|
|
|
6290
6304
|
const edgePad = ctx.aesthetic(8);
|
|
6291
6305
|
const labelTextPad = ctx.aesthetic(4);
|
|
6292
6306
|
|
|
6293
|
-
const
|
|
6307
|
+
const sSetLabelFont = ctx.text(14);
|
|
6294
6308
|
|
|
6295
6309
|
for (let i = 0; i < n; i++) {
|
|
6296
6310
|
// In-bounds by loop guard (n === vennSets.length === rawCircles.length).
|
|
6297
6311
|
const estimatedWidth =
|
|
6298
|
-
vennSets[i]!.name
|
|
6312
|
+
measureText(vennSets[i]!.name, sSetLabelFont) +
|
|
6313
|
+
stubLen +
|
|
6314
|
+
edgePad +
|
|
6315
|
+
labelTextPad;
|
|
6299
6316
|
const dx = rawCircles[i]!.x - clusterCx;
|
|
6300
6317
|
const dy = rawCircles[i]!.y - clusterCy;
|
|
6301
6318
|
if (Math.abs(dx) >= Math.abs(dy)) {
|
|
@@ -6313,7 +6330,6 @@ export function renderVenn(
|
|
|
6313
6330
|
// to leave readable space outside for leader+text. Wrap target scales
|
|
6314
6331
|
// with the canvas so labels stay narrow on small windows.
|
|
6315
6332
|
const OVERLAP_FONT = ctx.text(13);
|
|
6316
|
-
const OVERLAP_CH_W = ctx.structural(7);
|
|
6317
6333
|
const OVERLAP_LINE_H = ctx.structural(16);
|
|
6318
6334
|
const OVERLAP_LEADER_PAD = ctx.structural(18);
|
|
6319
6335
|
const OVERLAP_TEXT_GAP = ctx.aesthetic(6);
|
|
@@ -6322,27 +6338,6 @@ export function renderVenn(
|
|
|
6322
6338
|
ctx.structural(80),
|
|
6323
6339
|
Math.min(ctx.structural(170), width * 0.18)
|
|
6324
6340
|
);
|
|
6325
|
-
const MAX_WRAP_CHARS = Math.max(
|
|
6326
|
-
8,
|
|
6327
|
-
Math.floor(OVERLAP_WRAP_TARGET_W / OVERLAP_CH_W)
|
|
6328
|
-
);
|
|
6329
|
-
|
|
6330
|
-
function wrapLabel(text: string, maxChars: number): string[] {
|
|
6331
|
-
const words = text.split(/\s+/).filter(Boolean);
|
|
6332
|
-
const lines: string[] = [];
|
|
6333
|
-
let cur = '';
|
|
6334
|
-
for (const w of words) {
|
|
6335
|
-
const cand = cur ? cur + ' ' + w : w;
|
|
6336
|
-
if (cand.length > maxChars && cur) {
|
|
6337
|
-
lines.push(cur);
|
|
6338
|
-
cur = w;
|
|
6339
|
-
} else {
|
|
6340
|
-
cur = cand;
|
|
6341
|
-
}
|
|
6342
|
-
}
|
|
6343
|
-
if (cur) lines.push(cur);
|
|
6344
|
-
return lines.length ? lines : [text];
|
|
6345
|
-
}
|
|
6346
6341
|
|
|
6347
6342
|
function predictOverlapDirRaw(idxs: number[]): { x: number; y: number } {
|
|
6348
6343
|
const excluded = rawCircles
|
|
@@ -6381,12 +6376,18 @@ export function renderVenn(
|
|
|
6381
6376
|
if (!ov.label) continue;
|
|
6382
6377
|
const idxs = ov.sets.map((s) => vennSets.findIndex((vs) => vs.name === s));
|
|
6383
6378
|
if (idxs.some((idx) => idx < 0)) continue;
|
|
6384
|
-
const lines =
|
|
6379
|
+
const lines = wrapTextToWidth(
|
|
6380
|
+
ov.label,
|
|
6381
|
+
OVERLAP_FONT,
|
|
6382
|
+
OVERLAP_WRAP_TARGET_W
|
|
6383
|
+
);
|
|
6385
6384
|
wrappedOverlapLabels.set(ov, lines);
|
|
6386
6385
|
|
|
6387
6386
|
const dir = predictOverlapDirRaw(idxs);
|
|
6388
|
-
const
|
|
6389
|
-
|
|
6387
|
+
const labelW = lines.reduce(
|
|
6388
|
+
(m, l) => Math.max(m, measureText(l, OVERLAP_FONT)),
|
|
6389
|
+
0
|
|
6390
|
+
);
|
|
6390
6391
|
const labelH = lines.length * OVERLAP_LINE_H;
|
|
6391
6392
|
const baseLeader =
|
|
6392
6393
|
OVERLAP_LEADER_PAD + OVERLAP_TEXT_GAP + OVERLAP_MARGIN_PAD;
|
|
@@ -6651,7 +6652,6 @@ export function renderVenn(
|
|
|
6651
6652
|
return Math.max(0, right - left);
|
|
6652
6653
|
}
|
|
6653
6654
|
|
|
6654
|
-
const CH_RATIO = 0.6;
|
|
6655
6655
|
const MIN_FONT = ctx.text(10);
|
|
6656
6656
|
const MAX_FONT = ctx.text(22);
|
|
6657
6657
|
const INTERNAL_PAD = ctx.aesthetic(12);
|
|
@@ -6671,11 +6671,13 @@ export function renderVenn(
|
|
|
6671
6671
|
const centroid = regionCentroid(circles, inside);
|
|
6672
6672
|
|
|
6673
6673
|
const availW = exclusiveHSpan(centroid.x, centroid.y, i);
|
|
6674
|
+
// Width of `text` at fontSize 1; scale to solve for the largest fitting font.
|
|
6675
|
+
const textWidthPerPx = measureText(text, 1);
|
|
6674
6676
|
const fitFont = Math.min(
|
|
6675
6677
|
MAX_FONT,
|
|
6676
|
-
Math.max(MIN_FONT, (availW - INTERNAL_PAD * 2) /
|
|
6678
|
+
Math.max(MIN_FONT, (availW - INTERNAL_PAD * 2) / textWidthPerPx)
|
|
6677
6679
|
);
|
|
6678
|
-
const estTextW = text
|
|
6680
|
+
const estTextW = measureText(text, fitFont);
|
|
6679
6681
|
|
|
6680
6682
|
const fitsInside =
|
|
6681
6683
|
estTextW + INTERNAL_PAD * 2 < availW &&
|
|
@@ -6739,8 +6741,7 @@ export function renderVenn(
|
|
|
6739
6741
|
const textAnchor = isRight ? 'start' : 'end';
|
|
6740
6742
|
let textX = stubEndX + (isRight ? labelTextPad : -labelTextPad);
|
|
6741
6743
|
const textY = stubEndY;
|
|
6742
|
-
const
|
|
6743
|
-
const estW = text.length * sCharW;
|
|
6744
|
+
const estW = measureText(text, sSetLabelFont);
|
|
6744
6745
|
if (isRight) textX = Math.min(textX, width - estW - 4);
|
|
6745
6746
|
else textX = Math.max(textX, estW + 4);
|
|
6746
6747
|
|
|
@@ -6758,7 +6759,7 @@ export function renderVenn(
|
|
|
6758
6759
|
.attr('font-size', `${sSetLabelFont}px`)
|
|
6759
6760
|
.attr('font-weight', 'bold')
|
|
6760
6761
|
.text(text);
|
|
6761
|
-
const externalEstW = text
|
|
6762
|
+
const externalEstW = measureText(text, sSetLabelFont);
|
|
6762
6763
|
setLabelBBoxes[i] = {
|
|
6763
6764
|
x: isRight ? textX : textX - externalEstW,
|
|
6764
6765
|
y: renderedTextY - sSetLabelFont / 2,
|
|
@@ -6968,8 +6969,10 @@ export function renderVenn(
|
|
|
6968
6969
|
textY = stubEndY + sign * OVERLAP_TEXT_GAP;
|
|
6969
6970
|
}
|
|
6970
6971
|
|
|
6971
|
-
const
|
|
6972
|
-
|
|
6972
|
+
const blockW = lines.reduce(
|
|
6973
|
+
(m, l) => Math.max(m, measureText(l, OVERLAP_FONT)),
|
|
6974
|
+
0
|
|
6975
|
+
);
|
|
6973
6976
|
const blockH = lines.length * OVERLAP_LINE_H;
|
|
6974
6977
|
|
|
6975
6978
|
if (textAnchor === 'start') textX = Math.min(textX, width - blockW - 4);
|
|
@@ -7238,29 +7241,6 @@ export function renderQuadrant(
|
|
|
7238
7241
|
.append('g')
|
|
7239
7242
|
.attr('transform', `translate(${margin.left}, ${margin.top})`);
|
|
7240
7243
|
|
|
7241
|
-
// Mix two hex colors: pct=100 → all `a`, pct=0 → all `b`
|
|
7242
|
-
const mixHex = (a: string, b: string, pct: number): string => {
|
|
7243
|
-
const parse = (h: string): [number, number, number] => {
|
|
7244
|
-
const r = h.replace('#', '');
|
|
7245
|
-
// In-bounds: 3-char path indexes [0],[1],[2].
|
|
7246
|
-
const f =
|
|
7247
|
-
r.length === 3 ? r[0]! + r[0]! + r[1]! + r[1]! + r[2]! + r[2]! : r;
|
|
7248
|
-
return [
|
|
7249
|
-
parseInt(f.substring(0, 2), 16),
|
|
7250
|
-
parseInt(f.substring(2, 4), 16),
|
|
7251
|
-
parseInt(f.substring(4, 6), 16),
|
|
7252
|
-
];
|
|
7253
|
-
};
|
|
7254
|
-
const [ar, ag, ab] = parse(a),
|
|
7255
|
-
[br, bg, bb] = parse(b),
|
|
7256
|
-
t = pct / 100;
|
|
7257
|
-
const c = (x: number, y: number) =>
|
|
7258
|
-
Math.round(x * t + y * (1 - t))
|
|
7259
|
-
.toString(16)
|
|
7260
|
-
.padStart(2, '0');
|
|
7261
|
-
return `#${c(ar, br)}${c(ag, bg)}${c(ab, bb)}`;
|
|
7262
|
-
};
|
|
7263
|
-
|
|
7264
7244
|
const bg = isDark ? palette.surface : palette.bg;
|
|
7265
7245
|
|
|
7266
7246
|
// Full palette color for a quadrant (used for border and label tinting)
|
|
@@ -7277,7 +7257,7 @@ export function renderQuadrant(
|
|
|
7277
7257
|
label: QuadrantLabel | null,
|
|
7278
7258
|
defaultIdx: number
|
|
7279
7259
|
): string => {
|
|
7280
|
-
return
|
|
7260
|
+
return mix(getQuadrantColor(label, defaultIdx), bg, 30);
|
|
7281
7261
|
};
|
|
7282
7262
|
|
|
7283
7263
|
// Quadrant definitions: position, rect bounds, label position
|
|
@@ -7357,16 +7337,12 @@ export function renderQuadrant(
|
|
|
7357
7337
|
const shadowColor = 'rgba(0,0,0,0.4)';
|
|
7358
7338
|
|
|
7359
7339
|
// Single muted shade of textColor — watermark-style, readable against any quadrant fill
|
|
7360
|
-
const quadrantLabelColor =
|
|
7340
|
+
const quadrantLabelColor = mix(textColor, bg, 35);
|
|
7361
7341
|
|
|
7362
7342
|
// Scale label font size to fit within quadrant bounds, wrapping into multiple lines if needed
|
|
7363
7343
|
const LABEL_MAX_FONT = ctx.text(48);
|
|
7364
7344
|
const LABEL_MIN_FONT = ctx.text(14);
|
|
7365
7345
|
const LABEL_PAD = ctx.aesthetic(40);
|
|
7366
|
-
const CHAR_WIDTH_RATIO = 0.6;
|
|
7367
|
-
|
|
7368
|
-
const estTextWidth = (text: string, fontSize: number): number =>
|
|
7369
|
-
text.length * fontSize * CHAR_WIDTH_RATIO;
|
|
7370
7346
|
|
|
7371
7347
|
interface QuadrantLabelLayout {
|
|
7372
7348
|
lines: string[];
|
|
@@ -7380,10 +7356,9 @@ export function renderQuadrant(
|
|
|
7380
7356
|
): QuadrantLabelLayout => {
|
|
7381
7357
|
const availW = qw - LABEL_PAD;
|
|
7382
7358
|
const availH = qh - LABEL_PAD;
|
|
7383
|
-
const words = text.split(/\s+/);
|
|
7384
7359
|
|
|
7385
7360
|
// Try single line first
|
|
7386
|
-
if (
|
|
7361
|
+
if (measureText(text, LABEL_MAX_FONT) <= availW) {
|
|
7387
7362
|
const fs = Math.min(LABEL_MAX_FONT, availH);
|
|
7388
7363
|
return {
|
|
7389
7364
|
lines: [text],
|
|
@@ -7392,21 +7367,8 @@ export function renderQuadrant(
|
|
|
7392
7367
|
}
|
|
7393
7368
|
|
|
7394
7369
|
// Try wrapping into 2+ lines: greedily pack words so each line fits availW
|
|
7395
|
-
const wrapLines = (fs: number): string[] =>
|
|
7396
|
-
|
|
7397
|
-
let cur = '';
|
|
7398
|
-
for (const w of words) {
|
|
7399
|
-
const trial = cur ? `${cur} ${w}` : w;
|
|
7400
|
-
if (estTextWidth(trial, fs) > availW && cur) {
|
|
7401
|
-
result.push(cur);
|
|
7402
|
-
cur = w;
|
|
7403
|
-
} else {
|
|
7404
|
-
cur = trial;
|
|
7405
|
-
}
|
|
7406
|
-
}
|
|
7407
|
-
if (cur) result.push(cur);
|
|
7408
|
-
return result;
|
|
7409
|
-
};
|
|
7370
|
+
const wrapLines = (fs: number): string[] =>
|
|
7371
|
+
wrapTextToWidth(text, fs, availW);
|
|
7410
7372
|
|
|
7411
7373
|
// Binary-search for largest font size where wrapped text fits both width and height
|
|
7412
7374
|
let lo = LABEL_MIN_FONT;
|
|
@@ -7417,7 +7379,7 @@ export function renderQuadrant(
|
|
|
7417
7379
|
const mid = Math.round((lo + hi) / 2);
|
|
7418
7380
|
const lines = wrapLines(mid);
|
|
7419
7381
|
const totalH = lines.length * mid * 1.2; // line height ~1.2em
|
|
7420
|
-
const maxLineW = Math.max(...lines.map((l) =>
|
|
7382
|
+
const maxLineW = Math.max(...lines.map((l) => measureText(l, mid)));
|
|
7421
7383
|
if (maxLineW <= availW && totalH <= availH) {
|
|
7422
7384
|
bestFs = mid;
|
|
7423
7385
|
bestLines = lines;
|
|
@@ -7649,10 +7611,9 @@ export function renderQuadrant(
|
|
|
7649
7611
|
const POINT_LABEL_FONT_SIZE = ctx.text(12);
|
|
7650
7612
|
const quadrantLabelObstacles: LabelRect[] = quadrantDefsWithLabel.map((d) => {
|
|
7651
7613
|
const layout = labelLayouts.get(d.label!.text)!;
|
|
7652
|
-
const totalW =
|
|
7653
|
-
|
|
7654
|
-
|
|
7655
|
-
CHAR_WIDTH_RATIO;
|
|
7614
|
+
const totalW = Math.max(
|
|
7615
|
+
...layout.lines.map((l) => measureText(l, layout.fontSize))
|
|
7616
|
+
);
|
|
7656
7617
|
const totalH = layout.lines.length * layout.fontSize * 1.2;
|
|
7657
7618
|
return {
|
|
7658
7619
|
x: d.labelX - totalW / 2,
|
|
@@ -8117,7 +8078,7 @@ export async function renderForExport(
|
|
|
8117
8078
|
if (detectedType === 'boxes-and-lines') {
|
|
8118
8079
|
const { parseBoxesAndLines } = await import('./boxes-and-lines/parser');
|
|
8119
8080
|
const effectivePalette = await resolveExportPalette(theme, palette);
|
|
8120
|
-
const blParsed = parseBoxesAndLines(content);
|
|
8081
|
+
const blParsed = parseBoxesAndLines(content, effectivePalette);
|
|
8121
8082
|
if (blParsed.error || blParsed.nodes.length === 0) return '';
|
|
8122
8083
|
|
|
8123
8084
|
// Convert viewState.htv (Record<string, string[]>) to Map<string, Set<string>>
|
|
@@ -8611,7 +8572,7 @@ export async function renderForExport(
|
|
|
8611
8572
|
const { mapExportDimensions } = await import('./map/dimensions');
|
|
8612
8573
|
|
|
8613
8574
|
const effectivePalette = await resolveExportPalette(theme, palette);
|
|
8614
|
-
const mapParsed = parseMap(content);
|
|
8575
|
+
const mapParsed = parseMap(content, effectivePalette);
|
|
8615
8576
|
// Always render — an empty or partially-resolved map still draws the
|
|
8616
8577
|
// inferred base map (§24B.10 / layout AC23); diagnostics surface separately.
|
|
8617
8578
|
// Prefer injected `mapData` (browser bundles it; the fs loader can't run
|
|
@@ -8741,7 +8702,7 @@ export async function renderForExport(
|
|
|
8741
8702
|
if (parsed.type === 'sequence') {
|
|
8742
8703
|
const { parseSequenceDgmo } = await import('./sequence/parser');
|
|
8743
8704
|
const { renderSequenceDiagram } = await import('./sequence/renderer');
|
|
8744
|
-
const seqParsed = parseSequenceDgmo(content);
|
|
8705
|
+
const seqParsed = parseSequenceDgmo(content, effectivePalette);
|
|
8745
8706
|
if (seqParsed.error || seqParsed.participants.length === 0) return '';
|
|
8746
8707
|
// Apply interactive view state from share links (read from unified viewState).
|
|
8747
8708
|
// Sequences key both sections and groups by source line number; `cg` is the
|
|
@@ -8793,7 +8754,7 @@ export async function renderForExport(
|
|
|
8793
8754
|
dims,
|
|
8794
8755
|
resolveActiveTagGroup(
|
|
8795
8756
|
parsed.timelineTagGroups,
|
|
8796
|
-
|
|
8757
|
+
parsed.timelineActiveTag,
|
|
8797
8758
|
viewState?.tag ?? options?.tagGroup
|
|
8798
8759
|
),
|
|
8799
8760
|
viewState?.swim,
|
package/src/diagnostics.ts
CHANGED
|
@@ -277,6 +277,12 @@ export const METADATA_DIAGNOSTIC_CODES = {
|
|
|
277
277
|
* after a layer name. Replaced by the `description: <text>` key.
|
|
278
278
|
*/
|
|
279
279
|
RING_BARE_DESCRIPTION_REMOVED: 'E_RING_BARE_DESCRIPTION_REMOVED',
|
|
280
|
+
/**
|
|
281
|
+
* Error: legacy sequence bare-keyword `position N` participant
|
|
282
|
+
* ordering shorthand. Replaced by the colon-keyed `position: <N>`
|
|
283
|
+
* metadata form, consistent with §1.4 same-line metadata.
|
|
284
|
+
*/
|
|
285
|
+
SEQUENCE_BARE_POSITION_REMOVED: 'E_SEQUENCE_BARE_POSITION_REMOVED',
|
|
280
286
|
/**
|
|
281
287
|
* Error: a `tag` declaration appears after the first non-tag
|
|
282
288
|
* content line. The reserved-key registry is finalized before
|
|
@@ -347,6 +353,16 @@ export function bareDescriptionRemovedMessage(args: {
|
|
|
347
353
|
return `'|' description shorthand removed in ${args.chartType} — use 'description: ${quoted}'`;
|
|
348
354
|
}
|
|
349
355
|
|
|
356
|
+
/**
|
|
357
|
+
* Canonical message for `E_SEQUENCE_BARE_POSITION_REMOVED`. Emitted
|
|
358
|
+
* when a sequence participant uses the legacy bare-keyword
|
|
359
|
+
* `position N` ordering shorthand instead of the colon-keyed
|
|
360
|
+
* `position: N` metadata form.
|
|
361
|
+
*/
|
|
362
|
+
export function sequenceBarePositionRemovedMessage(n: string): string {
|
|
363
|
+
return `Bare 'position ${n}' removed — use 'position: ${n}' (colon required, per §1.4 metadata)`;
|
|
364
|
+
}
|
|
365
|
+
|
|
350
366
|
/**
|
|
351
367
|
* Canonical message for `W_EMPTY_METADATA_VALUE`. Emitted when a
|
|
352
368
|
* `key:` token has no value following the colon.
|
package/src/echarts.ts
CHANGED
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
rectCircleOverlap,
|
|
53
53
|
} from './label-layout';
|
|
54
54
|
import { ScaleContext } from './utils/scaling';
|
|
55
|
+
import { measureText, wrapTextToWidth } from './utils/text-measure';
|
|
55
56
|
|
|
56
57
|
// ============================================================
|
|
57
58
|
// Types
|
|
@@ -1458,7 +1459,7 @@ export function computeScatterLabelGraphics(
|
|
|
1458
1459
|
const pt = points[i]!;
|
|
1459
1460
|
const ptSize = pt.size ?? symbolSize;
|
|
1460
1461
|
const minGap = ptSize / 2 + 4;
|
|
1461
|
-
const labelWidth = pt.name
|
|
1462
|
+
const labelWidth = measureText(pt.name, fontSize) + 8;
|
|
1462
1463
|
const labelX = pt.px - labelWidth / 2; // centered horizontally
|
|
1463
1464
|
|
|
1464
1465
|
// Try both directions, pick whichever keeps the label closest to the point
|
|
@@ -2037,15 +2038,18 @@ function buildHeatmapOption(
|
|
|
2037
2038
|
});
|
|
2038
2039
|
|
|
2039
2040
|
// Rotate column labels only when they'd overlap at the default font size.
|
|
2040
|
-
//
|
|
2041
|
-
// an even share of a ~900px-wide chart.
|
|
2042
|
-
const CHAR_WIDTH = 7;
|
|
2041
|
+
// Measure the widest label at the actual 12px axis font; rotate if it
|
|
2042
|
+
// exceeds an even share of a ~900px-wide chart.
|
|
2043
2043
|
const ESTIMATED_CHART_WIDTH = 900;
|
|
2044
2044
|
const scaledChartWidth = sc.structural(ESTIMATED_CHART_WIDTH);
|
|
2045
|
-
const
|
|
2045
|
+
const colLabelFontSize = sc.text(12);
|
|
2046
|
+
const longestColWidth = Math.max(
|
|
2047
|
+
0,
|
|
2048
|
+
...columns.map((c) => measureText(c, colLabelFontSize))
|
|
2049
|
+
);
|
|
2046
2050
|
const slotWidth =
|
|
2047
2051
|
columns.length > 0 ? scaledChartWidth / columns.length : Infinity;
|
|
2048
|
-
const needsRotation =
|
|
2052
|
+
const needsRotation = longestColWidth > slotWidth * 0.85;
|
|
2049
2053
|
|
|
2050
2054
|
const ESTIMATED_PLOT_W = 770;
|
|
2051
2055
|
const ESTIMATED_PLOT_H = 380;
|
|
@@ -2054,11 +2058,15 @@ function buildHeatmapOption(
|
|
|
2054
2058
|
const cellW = columns.length > 0 ? scaledPlotW / columns.length : scaledPlotW;
|
|
2055
2059
|
const cellH =
|
|
2056
2060
|
heatmapRows.length > 0 ? scaledPlotH / heatmapRows.length : scaledPlotH;
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2061
|
+
// Width (in fontSize units) of the widest cell value string, measured with
|
|
2062
|
+
// accurate glyph widths so the fitted font size matches what renders.
|
|
2063
|
+
const maxValueWidthPerUnit = Math.max(
|
|
2064
|
+
measureText('0', 1),
|
|
2065
|
+
...heatmapRows.flatMap((r) =>
|
|
2066
|
+
r.values.map((v) => measureText(String(v), 1))
|
|
2067
|
+
)
|
|
2060
2068
|
);
|
|
2061
|
-
const fontFromWidth = (cellW * 0.75) /
|
|
2069
|
+
const fontFromWidth = (cellW * 0.75) / maxValueWidthPerUnit;
|
|
2062
2070
|
const fontFromHeight = (cellH * 0.75) / 0.72;
|
|
2063
2071
|
const cellFontFloor = sc.text(16);
|
|
2064
2072
|
const labelFontSize = Math.max(
|
|
@@ -2540,21 +2548,9 @@ function makeChartGrid(options: {
|
|
|
2540
2548
|
};
|
|
2541
2549
|
}
|
|
2542
2550
|
|
|
2543
|
-
/** Wrap a label string at word boundaries to fit within `
|
|
2544
|
-
function wrapLabel(text: string,
|
|
2545
|
-
|
|
2546
|
-
const lines: string[] = [];
|
|
2547
|
-
let current = '';
|
|
2548
|
-
for (const word of words) {
|
|
2549
|
-
if (current && current.length + 1 + word.length > maxChars) {
|
|
2550
|
-
lines.push(current);
|
|
2551
|
-
current = word;
|
|
2552
|
-
} else {
|
|
2553
|
-
current = current ? current + ' ' + word : word;
|
|
2554
|
-
}
|
|
2555
|
-
}
|
|
2556
|
-
if (current) lines.push(current);
|
|
2557
|
-
return lines.join('\n');
|
|
2551
|
+
/** Wrap a label string at word boundaries to fit within `maxWidthPx` per line. */
|
|
2552
|
+
function wrapLabel(text: string, fontSize: number, maxWidthPx: number): string {
|
|
2553
|
+
return wrapTextToWidth(text, fontSize, maxWidthPx).join('\n');
|
|
2558
2554
|
}
|
|
2559
2555
|
|
|
2560
2556
|
// ── Bar ──────────────────────────────────────────────────────
|
|
@@ -2592,15 +2588,26 @@ function buildBarOption(
|
|
|
2592
2588
|
});
|
|
2593
2589
|
|
|
2594
2590
|
// For horizontal bars, wrap long category labels at word boundaries so they
|
|
2595
|
-
// don't consume too much horizontal space on the y-axis.
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
const
|
|
2600
|
-
|
|
2591
|
+
// don't consume too much horizontal space on the y-axis. The wrap budget is
|
|
2592
|
+
// a ~12-char line measured at the actual category-label font (16px) so
|
|
2593
|
+
// narrow/wide glyphs wrap by real rendered width, not raw char count.
|
|
2594
|
+
const catLabelFontSize = (sc ?? ScaleContext.identity()).text(16);
|
|
2595
|
+
const catWrapWidth = measureText('0'.repeat(12), catLabelFontSize);
|
|
2596
|
+
const catLabels = isHorizontal
|
|
2597
|
+
? labels.map((l) => wrapLabel(l, catLabelFontSize, catWrapWidth))
|
|
2598
|
+
: labels;
|
|
2599
|
+
|
|
2600
|
+
// Compute the max visible line width (px) after wrapping for nameGap.
|
|
2601
|
+
const maxVisibleWidth = Math.max(
|
|
2602
|
+
0,
|
|
2603
|
+
...catLabels.map((l) =>
|
|
2604
|
+
Math.max(
|
|
2605
|
+
...l.split('\n').map((seg) => measureText(seg, catLabelFontSize))
|
|
2606
|
+
)
|
|
2607
|
+
)
|
|
2601
2608
|
);
|
|
2602
2609
|
const hCatGap =
|
|
2603
|
-
isHorizontal && yLabel ? Math.max(40,
|
|
2610
|
+
isHorizontal && yLabel ? Math.max(40, maxVisibleWidth + 16) : undefined;
|
|
2604
2611
|
const categoryAxis = makeGridAxis(
|
|
2605
2612
|
'category',
|
|
2606
2613
|
textColor,
|
|
@@ -3309,9 +3316,15 @@ function buildBarStackedOption(
|
|
|
3309
3316
|
};
|
|
3310
3317
|
});
|
|
3311
3318
|
|
|
3319
|
+
const stackCatLabelFontSize = s.text(16);
|
|
3312
3320
|
const hCatGap =
|
|
3313
3321
|
isHorizontal && yLabel
|
|
3314
|
-
? Math.max(
|
|
3322
|
+
? Math.max(
|
|
3323
|
+
40,
|
|
3324
|
+
Math.max(
|
|
3325
|
+
...labels.map((l) => measureText(l, stackCatLabelFontSize))
|
|
3326
|
+
) + 16
|
|
3327
|
+
)
|
|
3315
3328
|
: undefined;
|
|
3316
3329
|
const categoryAxis = makeGridAxis(
|
|
3317
3330
|
'category',
|
package/src/editor/keywords.ts
CHANGED
|
@@ -164,8 +164,10 @@ export const DIRECTIVE_KEYWORDS = new Set([
|
|
|
164
164
|
'no-relief',
|
|
165
165
|
'no-context-labels',
|
|
166
166
|
'no-region-labels',
|
|
167
|
+
'no-region-value',
|
|
167
168
|
'no-poi-labels',
|
|
168
169
|
'no-colorize',
|
|
170
|
+
'no-cities',
|
|
169
171
|
'no-cluster-pois',
|
|
170
172
|
'poi',
|
|
171
173
|
'route',
|
|
@@ -207,6 +209,8 @@ export const DIRECTIVE_KEYWORDS = new Set([
|
|
|
207
209
|
'color',
|
|
208
210
|
// Title suppression (cross-chart-type)
|
|
209
211
|
'no-title',
|
|
212
|
+
// Note suppression (cross-chart-type — graph notes)
|
|
213
|
+
'no-notes',
|
|
210
214
|
// Flowchart layout
|
|
211
215
|
'orientation-vertical',
|
|
212
216
|
// RACI
|