schematex 0.1.0 → 0.2.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 +229 -40
- package/dist/api-bQZ98gkJ.d.cts +22 -0
- package/dist/api-bQZ98gkJ.d.ts +22 -0
- package/dist/browser.cjs +46 -0
- package/dist/browser.cjs.map +1 -0
- package/dist/browser.d.cts +27 -0
- package/dist/browser.d.ts +27 -0
- package/dist/browser.js +40 -0
- package/dist/browser.js.map +1 -0
- package/dist/{chunk-XXU36667.js → chunk-2OIW3MAE.js} +4 -3
- package/dist/chunk-2OIW3MAE.js.map +1 -0
- package/dist/{chunk-3FTUWAXK.cjs → chunk-3M7QWADF.cjs} +5 -4
- package/dist/chunk-3M7QWADF.cjs.map +1 -0
- package/dist/{chunk-BE5HNDA5.cjs → chunk-4HPT4BOI.cjs} +5 -4
- package/dist/{chunk-BE5HNDA5.cjs.map → chunk-4HPT4BOI.cjs.map} +1 -1
- package/dist/{chunk-CZRM7LT7.js → chunk-4TS5NB7L.js} +4 -3
- package/dist/chunk-4TS5NB7L.js.map +1 -0
- package/dist/{chunk-4G7ZIBHN.js → chunk-5SH5NUDW.js} +3 -2
- package/dist/{chunk-4G7ZIBHN.js.map → chunk-5SH5NUDW.js.map} +1 -1
- package/dist/{chunk-47ZC6EMJ.js → chunk-7WXAAVR3.js} +4 -3
- package/dist/{chunk-47ZC6EMJ.js.map → chunk-7WXAAVR3.js.map} +1 -1
- package/dist/{chunk-5C7DPDHQ.js → chunk-A74ZCP5I.js} +4 -3
- package/dist/{chunk-5C7DPDHQ.js.map → chunk-A74ZCP5I.js.map} +1 -1
- package/dist/{chunk-DS47NTWZ.cjs → chunk-AMP2FFES.cjs} +11 -10
- package/dist/chunk-AMP2FFES.cjs.map +1 -0
- package/dist/{chunk-2UKC6ZCY.cjs → chunk-CEV3GZA3.cjs} +12 -11
- package/dist/chunk-CEV3GZA3.cjs.map +1 -0
- package/dist/{chunk-NYCIK4SU.cjs → chunk-DTMCQXXC.cjs} +13 -12
- package/dist/chunk-DTMCQXXC.cjs.map +1 -0
- package/dist/{chunk-FDLZEKEB.js → chunk-GEPBET4L.js} +3 -2
- package/dist/chunk-GEPBET4L.js.map +1 -0
- package/dist/chunk-HAIBAF6J.cjs +1880 -0
- package/dist/chunk-HAIBAF6J.cjs.map +1 -0
- package/dist/{chunk-FGPTCDUT.cjs → chunk-HKRYKEOV.cjs} +5 -4
- package/dist/chunk-HKRYKEOV.cjs.map +1 -0
- package/dist/{chunk-D4JTSPOL.js → chunk-HLYA4QBB.js} +4 -3
- package/dist/chunk-HLYA4QBB.js.map +1 -0
- package/dist/{chunk-URSKIHSY.cjs → chunk-IMHR3S5H.cjs} +6 -5
- package/dist/chunk-IMHR3S5H.cjs.map +1 -0
- package/dist/{chunk-U4I37IBN.js → chunk-IQIJ6WW6.js} +4 -3
- package/dist/chunk-IQIJ6WW6.js.map +1 -0
- package/dist/{chunk-UHLYS3W5.cjs → chunk-IY52OWPG.cjs} +5 -4
- package/dist/{chunk-UHLYS3W5.cjs.map → chunk-IY52OWPG.cjs.map} +1 -1
- package/dist/chunk-JZGFSRVT.js +1873 -0
- package/dist/chunk-JZGFSRVT.js.map +1 -0
- package/dist/{chunk-3J7TFUOC.js → chunk-L6IHSTPP.js} +4 -3
- package/dist/{chunk-3J7TFUOC.js.map → chunk-L6IHSTPP.js.map} +1 -1
- package/dist/{chunk-4DBRNOPA.cjs → chunk-LKHWBDWZ.cjs} +5 -4
- package/dist/{chunk-4DBRNOPA.cjs.map → chunk-LKHWBDWZ.cjs.map} +1 -1
- package/dist/{chunk-34X3ZJ6E.cjs → chunk-LXNFVHDT.cjs} +3 -2
- package/dist/{chunk-34X3ZJ6E.cjs.map → chunk-LXNFVHDT.cjs.map} +1 -1
- package/dist/chunk-M6AMNXQ7.js +4539 -0
- package/dist/chunk-M6AMNXQ7.js.map +1 -0
- package/dist/{chunk-XX4BKS7Y.js → chunk-MRGS54WN.js} +4 -3
- package/dist/chunk-MRGS54WN.js.map +1 -0
- package/dist/{chunk-ROFLJ74T.js → chunk-MXJ6FHSY.js} +4 -3
- package/dist/chunk-MXJ6FHSY.js.map +1 -0
- package/dist/{chunk-V6WO7RK7.cjs → chunk-PIQG2Z5N.cjs} +5 -4
- package/dist/chunk-PIQG2Z5N.cjs.map +1 -0
- package/dist/{chunk-U5GGE6PJ.js → chunk-RQX53J6M.js} +4 -3
- package/dist/chunk-RQX53J6M.js.map +1 -0
- package/dist/{chunk-N7KOXOMX.cjs → chunk-S6VPECM3.cjs} +72 -2
- package/dist/chunk-S6VPECM3.cjs.map +1 -0
- package/dist/{chunk-VFQCTXOX.js → chunk-SPIW4VWP.js} +4 -3
- package/dist/chunk-SPIW4VWP.js.map +1 -0
- package/dist/{chunk-IX554O5K.js → chunk-TIGP2OEJ.js} +72 -3
- package/dist/chunk-TIGP2OEJ.js.map +1 -0
- package/dist/{chunk-LMFSHK45.js → chunk-TPA36ULU.js} +4 -3
- package/dist/{chunk-LMFSHK45.js.map → chunk-TPA36ULU.js.map} +1 -1
- package/dist/{chunk-ZX7QKZK2.cjs → chunk-ULERCTGS.cjs} +5 -4
- package/dist/{chunk-ZX7QKZK2.cjs.map → chunk-ULERCTGS.cjs.map} +1 -1
- package/dist/chunk-VP54YPOX.cjs +4544 -0
- package/dist/chunk-VP54YPOX.cjs.map +1 -0
- package/dist/{chunk-XQ52ICHU.cjs → chunk-YKO7DY2F.cjs} +14 -13
- package/dist/chunk-YKO7DY2F.cjs.map +1 -0
- package/dist/{chunk-PDPHRZZT.js → chunk-YO4GU6JX.js} +4 -3
- package/dist/chunk-YO4GU6JX.js.map +1 -0
- package/dist/{chunk-S6BK5DB6.cjs → chunk-ZGKEFVJQ.cjs} +13 -12
- package/dist/chunk-ZGKEFVJQ.cjs.map +1 -0
- package/dist/{chunk-2MQWZ2XY.cjs → chunk-ZO77FHBF.cjs} +3 -2
- package/dist/chunk-ZO77FHBF.cjs.map +1 -0
- package/dist/diagrams/blockdiagram/index.cjs +5 -5
- package/dist/diagrams/blockdiagram/index.d.cts +1 -1
- package/dist/diagrams/blockdiagram/index.d.ts +1 -1
- package/dist/diagrams/blockdiagram/index.js +1 -1
- package/dist/diagrams/circuit/index.cjs +8 -8
- package/dist/diagrams/circuit/index.d.cts +1 -1
- package/dist/diagrams/circuit/index.d.ts +1 -1
- package/dist/diagrams/circuit/index.js +2 -2
- package/dist/diagrams/ecomap/index.cjs +7 -7
- package/dist/diagrams/ecomap/index.d.cts +1 -1
- package/dist/diagrams/ecomap/index.d.ts +1 -1
- package/dist/diagrams/ecomap/index.js +2 -2
- package/dist/diagrams/entity/index.cjs +6 -6
- package/dist/diagrams/entity/index.d.cts +1 -1
- package/dist/diagrams/entity/index.d.ts +1 -1
- package/dist/diagrams/entity/index.js +2 -2
- package/dist/diagrams/fishbone/index.cjs +8 -8
- package/dist/diagrams/fishbone/index.d.cts +9 -9
- package/dist/diagrams/fishbone/index.d.ts +9 -9
- package/dist/diagrams/fishbone/index.js +2 -2
- package/dist/diagrams/flowchart/index.cjs +8 -8
- package/dist/diagrams/flowchart/index.d.cts +2 -2
- package/dist/diagrams/flowchart/index.d.ts +2 -2
- package/dist/diagrams/flowchart/index.js +2 -2
- package/dist/diagrams/genogram/index.cjs +9 -9
- package/dist/diagrams/genogram/index.d.cts +1 -1
- package/dist/diagrams/genogram/index.d.ts +1 -1
- package/dist/diagrams/genogram/index.js +2 -2
- package/dist/diagrams/ladder/index.cjs +6 -6
- package/dist/diagrams/ladder/index.d.cts +1 -1
- package/dist/diagrams/ladder/index.d.ts +1 -1
- package/dist/diagrams/ladder/index.js +2 -2
- package/dist/diagrams/logic/index.cjs +6 -6
- package/dist/diagrams/logic/index.d.cts +1 -1
- package/dist/diagrams/logic/index.d.ts +1 -1
- package/dist/diagrams/logic/index.js +2 -2
- package/dist/diagrams/orgchart/index.cjs +7 -7
- package/dist/diagrams/orgchart/index.d.cts +1 -1
- package/dist/diagrams/orgchart/index.d.ts +1 -1
- package/dist/diagrams/orgchart/index.js +2 -2
- package/dist/diagrams/pedigree/index.cjs +7 -7
- package/dist/diagrams/pedigree/index.d.cts +1 -1
- package/dist/diagrams/pedigree/index.d.ts +1 -1
- package/dist/diagrams/pedigree/index.js +2 -2
- package/dist/diagrams/phylo/index.cjs +7 -7
- package/dist/diagrams/phylo/index.d.cts +1 -1
- package/dist/diagrams/phylo/index.d.ts +1 -1
- package/dist/diagrams/phylo/index.js +2 -2
- package/dist/diagrams/sld/index.cjs +6 -6
- package/dist/diagrams/sld/index.d.cts +1 -1
- package/dist/diagrams/sld/index.d.ts +1 -1
- package/dist/diagrams/sld/index.js +2 -2
- package/dist/diagrams/sociogram/index.cjs +6 -6
- package/dist/diagrams/sociogram/index.d.cts +1 -1
- package/dist/diagrams/sociogram/index.d.ts +1 -1
- package/dist/diagrams/sociogram/index.js +2 -2
- package/dist/diagrams/timing/index.cjs +4 -4
- package/dist/diagrams/timing/index.d.cts +1 -1
- package/dist/diagrams/timing/index.d.ts +1 -1
- package/dist/diagrams/timing/index.js +1 -1
- package/dist/diagrams/venn/index.cjs +9 -9
- package/dist/diagrams/venn/index.d.cts +1 -1
- package/dist/diagrams/venn/index.d.ts +1 -1
- package/dist/diagrams/venn/index.js +2 -2
- package/dist/export.cjs +87 -0
- package/dist/export.cjs.map +1 -0
- package/dist/export.d.cts +38 -0
- package/dist/export.d.ts +38 -0
- package/dist/export.js +83 -0
- package/dist/export.js.map +1 -0
- package/dist/{index-BXefHVce.d.cts → index-SSGpCggE.d.cts} +52 -3
- package/dist/{index-BSlza1YY.d.ts → index-ga04CTBI.d.ts} +52 -3
- package/dist/index.cjs +65 -1948
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -12
- package/dist/index.d.ts +7 -12
- package/dist/index.js +19 -1942
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +56 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +24 -0
- package/dist/react.d.ts +24 -0
- package/dist/react.js +54 -0
- package/dist/react.js.map +1 -0
- package/dist/{types-DqfcYkcY.d.ts → types-BcPhMdHd.d.cts} +6 -2
- package/dist/{types-DqfcYkcY.d.cts → types-BcPhMdHd.d.ts} +6 -2
- package/package.json +31 -2
- package/dist/chunk-2MQWZ2XY.cjs.map +0 -1
- package/dist/chunk-2UKC6ZCY.cjs.map +0 -1
- package/dist/chunk-3FTUWAXK.cjs.map +0 -1
- package/dist/chunk-ADOXGKAK.js +0 -1251
- package/dist/chunk-ADOXGKAK.js.map +0 -1
- package/dist/chunk-CZRM7LT7.js.map +0 -1
- package/dist/chunk-D4JTSPOL.js.map +0 -1
- package/dist/chunk-DS47NTWZ.cjs.map +0 -1
- package/dist/chunk-FDLZEKEB.js.map +0 -1
- package/dist/chunk-FGPTCDUT.cjs.map +0 -1
- package/dist/chunk-IX554O5K.js.map +0 -1
- package/dist/chunk-MDICUK6F.cjs +0 -1258
- package/dist/chunk-MDICUK6F.cjs.map +0 -1
- package/dist/chunk-N7KOXOMX.cjs.map +0 -1
- package/dist/chunk-NYCIK4SU.cjs.map +0 -1
- package/dist/chunk-PDPHRZZT.js.map +0 -1
- package/dist/chunk-ROFLJ74T.js.map +0 -1
- package/dist/chunk-S6BK5DB6.cjs.map +0 -1
- package/dist/chunk-U4I37IBN.js.map +0 -1
- package/dist/chunk-U5GGE6PJ.js.map +0 -1
- package/dist/chunk-URSKIHSY.cjs.map +0 -1
- package/dist/chunk-V6WO7RK7.cjs.map +0 -1
- package/dist/chunk-VFQCTXOX.js.map +0 -1
- package/dist/chunk-XQ52ICHU.cjs.map +0 -1
- package/dist/chunk-XX4BKS7Y.js.map +0 -1
- package/dist/chunk-XXU36667.js.map +0 -1
|
@@ -0,0 +1,4539 @@
|
|
|
1
|
+
import { orgchart } from './chunk-7WXAAVR3.js';
|
|
2
|
+
import { circuit } from './chunk-2OIW3MAE.js';
|
|
3
|
+
import { blockdiagram } from './chunk-5SH5NUDW.js';
|
|
4
|
+
import { ladder } from './chunk-L6IHSTPP.js';
|
|
5
|
+
import { sld } from './chunk-A74ZCP5I.js';
|
|
6
|
+
import { entity } from './chunk-MRGS54WN.js';
|
|
7
|
+
import { fishbone } from './chunk-IQIJ6WW6.js';
|
|
8
|
+
import { venn } from './chunk-MXJ6FHSY.js';
|
|
9
|
+
import { flowchart } from './chunk-JZGFSRVT.js';
|
|
10
|
+
import { genogram } from './chunk-HLYA4QBB.js';
|
|
11
|
+
import { ecomap } from './chunk-RQX53J6M.js';
|
|
12
|
+
import { pedigree } from './chunk-TPA36ULU.js';
|
|
13
|
+
import { phylo } from './chunk-4TS5NB7L.js';
|
|
14
|
+
import { sociogram } from './chunk-YO4GU6JX.js';
|
|
15
|
+
import { timing } from './chunk-GEPBET4L.js';
|
|
16
|
+
import { logic } from './chunk-SPIW4VWP.js';
|
|
17
|
+
import { resolveBaseTheme, resolveTimelineTheme, cssCustomProperties, resolveMindmapTheme } from './chunk-TIGP2OEJ.js';
|
|
18
|
+
import { title, desc, el, text, path, rect, group, svgRoot, escapeXml, defs, circle, polygon, line } from './chunk-KLJEK547.js';
|
|
19
|
+
|
|
20
|
+
// src/diagrams/decisiontree/parser.ts
|
|
21
|
+
var DTreeParseError = class extends Error {
|
|
22
|
+
constructor(message, line2) {
|
|
23
|
+
super(line2 !== void 0 ? `Line ${line2}: ${message}` : message);
|
|
24
|
+
this.line = line2;
|
|
25
|
+
this.name = "DTreeParseError";
|
|
26
|
+
}
|
|
27
|
+
line;
|
|
28
|
+
};
|
|
29
|
+
function preprocess(src) {
|
|
30
|
+
const out = [];
|
|
31
|
+
const lines = src.split(/\r?\n/);
|
|
32
|
+
for (let i = 0; i < lines.length; i++) {
|
|
33
|
+
const raw = lines[i];
|
|
34
|
+
if (raw === void 0) continue;
|
|
35
|
+
if (!raw.trim() || raw.trim().startsWith("#") || raw.trim().startsWith("//")) continue;
|
|
36
|
+
const indentSpaces = raw.length - raw.replace(/^\s+/, "").length;
|
|
37
|
+
out.push({ indent: Math.floor(indentSpaces / 2), text: raw.trim(), line: i + 1 });
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
function tokenize(s) {
|
|
42
|
+
const tokens = [];
|
|
43
|
+
let i = 0;
|
|
44
|
+
while (i < s.length) {
|
|
45
|
+
const ch = s[i];
|
|
46
|
+
if (/\s/.test(ch)) {
|
|
47
|
+
i++;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (ch === '"') {
|
|
51
|
+
const end = s.indexOf('"', i + 1);
|
|
52
|
+
if (end < 0) throw new DTreeParseError(`Unterminated string: ${s}`);
|
|
53
|
+
tokens.push(s.slice(i, end + 1));
|
|
54
|
+
i = end + 1;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
let j = i;
|
|
58
|
+
let bracket = 0;
|
|
59
|
+
while (j < s.length) {
|
|
60
|
+
const c = s[j];
|
|
61
|
+
if (c === "[") bracket++;
|
|
62
|
+
else if (c === "]") bracket--;
|
|
63
|
+
else if (bracket === 0 && /\s/.test(c)) break;
|
|
64
|
+
else if (bracket === 0 && c === '"') break;
|
|
65
|
+
j++;
|
|
66
|
+
}
|
|
67
|
+
tokens.push(s.slice(i, j));
|
|
68
|
+
i = j;
|
|
69
|
+
}
|
|
70
|
+
return tokens;
|
|
71
|
+
}
|
|
72
|
+
function unquote(s) {
|
|
73
|
+
if (s.startsWith('"') && s.endsWith('"')) return s.slice(1, -1);
|
|
74
|
+
return s;
|
|
75
|
+
}
|
|
76
|
+
function parseKV(tokens) {
|
|
77
|
+
const keys = {};
|
|
78
|
+
const labels = [];
|
|
79
|
+
const rest = [];
|
|
80
|
+
for (const tok of tokens) {
|
|
81
|
+
if (tok.startsWith('"') && tok.endsWith('"')) {
|
|
82
|
+
labels.push(unquote(tok));
|
|
83
|
+
} else if (tok.includes("=")) {
|
|
84
|
+
const idx = tok.indexOf("=");
|
|
85
|
+
const k = tok.slice(0, idx);
|
|
86
|
+
const v = tok.slice(idx + 1);
|
|
87
|
+
keys[k] = v;
|
|
88
|
+
} else {
|
|
89
|
+
rest.push(tok);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { keys, labels, rest };
|
|
93
|
+
}
|
|
94
|
+
function parseNumberList(s) {
|
|
95
|
+
const inner = s.replace(/^\[/, "").replace(/\]$/, "");
|
|
96
|
+
return inner.split(",").map((p) => Number(p.trim())).filter((n) => !Number.isNaN(n));
|
|
97
|
+
}
|
|
98
|
+
var idCounter = 0;
|
|
99
|
+
function nextId(prefix = "n") {
|
|
100
|
+
idCounter++;
|
|
101
|
+
return `${prefix}${idCounter}`;
|
|
102
|
+
}
|
|
103
|
+
function parseDecisionLine(tokens, _ctx, lineNum) {
|
|
104
|
+
let idx = 0;
|
|
105
|
+
let incomingChoice;
|
|
106
|
+
let incomingProb;
|
|
107
|
+
if (tokens[idx] === "choice") {
|
|
108
|
+
idx++;
|
|
109
|
+
const next = tokens[idx];
|
|
110
|
+
if (!next || !next.startsWith('"')) throw new DTreeParseError(`"choice" requires a label`, lineNum);
|
|
111
|
+
incomingChoice = unquote(next);
|
|
112
|
+
idx++;
|
|
113
|
+
if (idx >= tokens.length) {
|
|
114
|
+
return {
|
|
115
|
+
id: nextId(),
|
|
116
|
+
kind: "decision",
|
|
117
|
+
// placeholder; will be overridden if sole child exists
|
|
118
|
+
label: "",
|
|
119
|
+
children: [],
|
|
120
|
+
incomingChoice,
|
|
121
|
+
// special marker so parseBody knows this is a wrapper
|
|
122
|
+
_wrapper: "choice"
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
} else if (tokens[idx] === "prob") {
|
|
126
|
+
idx++;
|
|
127
|
+
const p = Number(tokens[idx]);
|
|
128
|
+
if (Number.isNaN(p)) throw new DTreeParseError(`"prob" requires a numeric probability`, lineNum);
|
|
129
|
+
incomingProb = p;
|
|
130
|
+
idx++;
|
|
131
|
+
}
|
|
132
|
+
const kw = tokens[idx];
|
|
133
|
+
if (!kw) throw new DTreeParseError(`Missing node kind`, lineNum);
|
|
134
|
+
const rest = tokens.slice(idx + 1);
|
|
135
|
+
const parsed = parseKV(rest);
|
|
136
|
+
let kind;
|
|
137
|
+
if (kw === "decision") kind = "decision";
|
|
138
|
+
else if (kw === "chance") kind = "chance";
|
|
139
|
+
else if (kw === "end" || kw === "outcome") kind = "end";
|
|
140
|
+
else throw new DTreeParseError(`Unknown node kind "${kw}" in decision mode`, lineNum);
|
|
141
|
+
const label = parsed.labels[0] ?? "";
|
|
142
|
+
const payoffStr = parsed.keys.payoff;
|
|
143
|
+
const payoff = payoffStr !== void 0 ? Number(payoffStr) : void 0;
|
|
144
|
+
return {
|
|
145
|
+
id: nextId(),
|
|
146
|
+
kind,
|
|
147
|
+
label,
|
|
148
|
+
children: [],
|
|
149
|
+
incomingChoice,
|
|
150
|
+
incomingProb,
|
|
151
|
+
payoff
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function parseMlLine(tokens, ctx, lineNum) {
|
|
155
|
+
let idx = 0;
|
|
156
|
+
let mlBranch;
|
|
157
|
+
if (tokens[idx] === "true" || tokens[idx] === "false") {
|
|
158
|
+
mlBranch = tokens[idx];
|
|
159
|
+
idx++;
|
|
160
|
+
}
|
|
161
|
+
const kw = tokens[idx];
|
|
162
|
+
if (!kw) throw new DTreeParseError(`Missing ML node kind`, lineNum);
|
|
163
|
+
idx++;
|
|
164
|
+
const rest = tokens.slice(idx);
|
|
165
|
+
const parsed = parseKV(rest);
|
|
166
|
+
let kind;
|
|
167
|
+
if (kw === "split") kind = "split";
|
|
168
|
+
else if (kw === "leaf") kind = "leaf";
|
|
169
|
+
else throw new DTreeParseError(`Unknown ML node kind "${kw}"`, lineNum);
|
|
170
|
+
const label = parsed.labels[0] ?? "";
|
|
171
|
+
const k = parsed.keys;
|
|
172
|
+
let value;
|
|
173
|
+
if (k.value !== void 0) {
|
|
174
|
+
if (k.value.startsWith("[")) value = parseNumberList(k.value);
|
|
175
|
+
else value = Number(k.value);
|
|
176
|
+
}
|
|
177
|
+
if (typeof value === "number") ctx.regression = true;
|
|
178
|
+
let impurity;
|
|
179
|
+
for (const impKey of ["gini", "entropy", "mse", "gain", "impurity"]) {
|
|
180
|
+
if (k[impKey] !== void 0) {
|
|
181
|
+
impurity = Number(k[impKey]);
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
id: nextId(),
|
|
187
|
+
kind,
|
|
188
|
+
label,
|
|
189
|
+
children: [],
|
|
190
|
+
feature: k.feature,
|
|
191
|
+
op: k.op,
|
|
192
|
+
threshold: k.threshold !== void 0 ? Number.isNaN(Number(k.threshold)) ? k.threshold : Number(k.threshold) : void 0,
|
|
193
|
+
samples: k.samples !== void 0 ? Number(k.samples) : void 0,
|
|
194
|
+
value,
|
|
195
|
+
impurity,
|
|
196
|
+
mlBranch,
|
|
197
|
+
className: k.class
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function parseTaxonomyLine(tokens, _ctx, lineNum) {
|
|
201
|
+
let idx = 0;
|
|
202
|
+
let branchLabel;
|
|
203
|
+
if (tokens[idx] === "yes:" || tokens[idx] === "no:") {
|
|
204
|
+
branchLabel = tokens[idx].replace(":", "");
|
|
205
|
+
idx++;
|
|
206
|
+
} else if (tokens[idx] === "label") {
|
|
207
|
+
idx++;
|
|
208
|
+
const lbl = tokens[idx];
|
|
209
|
+
if (!lbl || !lbl.startsWith('"')) throw new DTreeParseError(`"label" requires a quoted string`, lineNum);
|
|
210
|
+
branchLabel = unquote(lbl.replace(/":?$/, '"'));
|
|
211
|
+
if (lbl.endsWith(':"')) ;
|
|
212
|
+
idx++;
|
|
213
|
+
if (tokens[idx] === ":") idx++;
|
|
214
|
+
}
|
|
215
|
+
const kw = tokens[idx];
|
|
216
|
+
if (!kw) throw new DTreeParseError(`Missing taxonomy node kind`, lineNum);
|
|
217
|
+
idx++;
|
|
218
|
+
const rest = tokens.slice(idx);
|
|
219
|
+
const parsed = parseKV(rest);
|
|
220
|
+
let kind;
|
|
221
|
+
if (kw === "q" || kw === "question") kind = "question";
|
|
222
|
+
else if (kw === "a" || kw === "answer" || kw === "leaf") kind = "answer";
|
|
223
|
+
else throw new DTreeParseError(`Unknown taxonomy node kind "${kw}"`, lineNum);
|
|
224
|
+
return {
|
|
225
|
+
id: nextId(),
|
|
226
|
+
kind,
|
|
227
|
+
label: parsed.labels[0] ?? "",
|
|
228
|
+
children: [],
|
|
229
|
+
branchLabel
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function parseNodeLine(text2, mode, ctx, lineNum) {
|
|
233
|
+
const normalized = text2.replace(/"\s*:\s*/g, '" : ');
|
|
234
|
+
const tokens = tokenize(normalized).filter((t) => t !== ":");
|
|
235
|
+
if (mode === "decision") return parseDecisionLine(tokens, ctx, lineNum);
|
|
236
|
+
if (mode === "ml") return parseMlLine(tokens, ctx, lineNum);
|
|
237
|
+
return parseTaxonomyLine(tokens, ctx, lineNum);
|
|
238
|
+
}
|
|
239
|
+
function buildTree(lines, mode, ctx) {
|
|
240
|
+
if (lines.length === 0) throw new DTreeParseError("No tree body");
|
|
241
|
+
const [first, ...rest] = lines;
|
|
242
|
+
if (!first) throw new DTreeParseError("No tree body");
|
|
243
|
+
const root = parseNodeLine(first.text, mode, ctx, first.line);
|
|
244
|
+
const stack = [{ node: root, indent: first.indent }];
|
|
245
|
+
for (const line2 of rest) {
|
|
246
|
+
const node = parseNodeLine(line2.text, mode, ctx, line2.line);
|
|
247
|
+
while (stack.length > 0 && stack[stack.length - 1].indent >= line2.indent) stack.pop();
|
|
248
|
+
const parent = stack[stack.length - 1];
|
|
249
|
+
if (!parent) throw new DTreeParseError(`Orphan line (bad indent): ${line2.text}`, line2.line);
|
|
250
|
+
parent.node.children.push(node);
|
|
251
|
+
stack.push({ node, indent: line2.indent });
|
|
252
|
+
}
|
|
253
|
+
return collapseWrappers(root);
|
|
254
|
+
}
|
|
255
|
+
function collapseWrappers(node) {
|
|
256
|
+
node.children = node.children.map(collapseWrappers).flatMap((c) => {
|
|
257
|
+
const anyC = c;
|
|
258
|
+
if (anyC._wrapper === "choice") {
|
|
259
|
+
return c.children.map((gc) => ({ ...gc, incomingChoice: c.incomingChoice }));
|
|
260
|
+
}
|
|
261
|
+
return [c];
|
|
262
|
+
});
|
|
263
|
+
return node;
|
|
264
|
+
}
|
|
265
|
+
function computeEV(node) {
|
|
266
|
+
for (const c of node.children) computeEV(c);
|
|
267
|
+
if (node.kind === "end") {
|
|
268
|
+
node.ev = node.payoff ?? 0;
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (node.kind === "chance") {
|
|
272
|
+
let sum = 0;
|
|
273
|
+
for (const c of node.children) sum += (c.incomingProb ?? 0) * (c.ev ?? 0);
|
|
274
|
+
node.ev = sum;
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (node.kind === "decision") {
|
|
278
|
+
let best = -Infinity;
|
|
279
|
+
for (const c of node.children) {
|
|
280
|
+
if ((c.ev ?? -Infinity) > best) best = c.ev ?? -Infinity;
|
|
281
|
+
}
|
|
282
|
+
node.ev = best;
|
|
283
|
+
for (const c of node.children) {
|
|
284
|
+
if ((c.ev ?? -Infinity) === best) c.optimal = true;
|
|
285
|
+
}
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function validateDecision(node, lineMap) {
|
|
290
|
+
if (node.kind === "chance" && node.children.length > 0) {
|
|
291
|
+
const sum = node.children.reduce((a, c) => a + (c.incomingProb ?? 0), 0);
|
|
292
|
+
if (Math.abs(sum - 1) > 0.01) {
|
|
293
|
+
throw new DTreeParseError(
|
|
294
|
+
`chance "${node.label}" probabilities do not sum to 1.0 (got ${sum.toFixed(3)})`
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
for (const c of node.children) validateDecision(c);
|
|
299
|
+
}
|
|
300
|
+
function parseDecisionTree(src) {
|
|
301
|
+
idCounter = 0;
|
|
302
|
+
const lines = preprocess(src);
|
|
303
|
+
if (lines.length === 0) throw new DTreeParseError("Empty input");
|
|
304
|
+
const header = lines.shift();
|
|
305
|
+
const headerMatch = header.text.match(/^decisiontree(?::(\w+))?(?:\s+"([^"]*)")?\s*$/i);
|
|
306
|
+
if (!headerMatch) throw new DTreeParseError(`Invalid header: ${header.text}`, header.line);
|
|
307
|
+
const modeRaw = (headerMatch[1] ?? "taxonomy").toLowerCase();
|
|
308
|
+
const mode = modeRaw === "decision" || modeRaw === "da" ? "decision" : modeRaw === "ml" ? "ml" : "taxonomy";
|
|
309
|
+
const title2 = headerMatch[2];
|
|
310
|
+
const nodeKeywords = /* @__PURE__ */ new Set([
|
|
311
|
+
"decision",
|
|
312
|
+
"chance",
|
|
313
|
+
"end",
|
|
314
|
+
"outcome",
|
|
315
|
+
"choice",
|
|
316
|
+
"prob",
|
|
317
|
+
"split",
|
|
318
|
+
"leaf",
|
|
319
|
+
"true",
|
|
320
|
+
"false",
|
|
321
|
+
"q",
|
|
322
|
+
"question",
|
|
323
|
+
"a",
|
|
324
|
+
"answer",
|
|
325
|
+
"yes:",
|
|
326
|
+
"no:",
|
|
327
|
+
"label"
|
|
328
|
+
]);
|
|
329
|
+
const config = {};
|
|
330
|
+
while (lines.length > 0) {
|
|
331
|
+
const l = lines[0];
|
|
332
|
+
const firstTok = l.text.split(/\s+/)[0];
|
|
333
|
+
if (nodeKeywords.has(firstTok)) break;
|
|
334
|
+
const m = l.text.match(/^([a-zA-Z][\w-]*)\s*:\s*(.+)$/);
|
|
335
|
+
if (!m) break;
|
|
336
|
+
config[m[1]] = m[2].trim();
|
|
337
|
+
lines.shift();
|
|
338
|
+
}
|
|
339
|
+
let direction = "top-down";
|
|
340
|
+
if (config.direction === "left-right" || config.direction === "lr") direction = "left-right";
|
|
341
|
+
if (mode === "decision" && !config.direction) direction = "left-right";
|
|
342
|
+
const classes = config.classes ? config.classes.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
343
|
+
let impurityName;
|
|
344
|
+
if (config.impurity) {
|
|
345
|
+
const v = config.impurity.toLowerCase();
|
|
346
|
+
if (v === "gini" || v === "entropy" || v === "mse" || v === "gain") impurityName = v;
|
|
347
|
+
}
|
|
348
|
+
const branchLabels = config.branchLabels === "relation" || config["branch-labels"] === "relation" ? "relation" : "boolean";
|
|
349
|
+
const branchLengthProb = config.branchLength === "probability" || config["branch-length"] === "probability";
|
|
350
|
+
let edgeStyle;
|
|
351
|
+
const es = (config.edgeStyle || config["edge-style"] || "").toLowerCase();
|
|
352
|
+
if (es === "diagonal" || es === "orthogonal" || es === "bracket") edgeStyle = es;
|
|
353
|
+
const ctx = { regression: false };
|
|
354
|
+
const root = buildTree(lines, mode, ctx);
|
|
355
|
+
if (mode === "decision") {
|
|
356
|
+
validateDecision(root);
|
|
357
|
+
computeEV(root);
|
|
358
|
+
}
|
|
359
|
+
return {
|
|
360
|
+
type: "decisiontree",
|
|
361
|
+
mode,
|
|
362
|
+
title: title2,
|
|
363
|
+
direction,
|
|
364
|
+
classes,
|
|
365
|
+
impurityName: impurityName ?? (mode === "ml" ? "gini" : void 0),
|
|
366
|
+
branchLabels,
|
|
367
|
+
branchLengthProb,
|
|
368
|
+
edgeStyle,
|
|
369
|
+
regression: ctx.regression,
|
|
370
|
+
root
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// src/diagrams/decisiontree/layout.ts
|
|
375
|
+
function sizeOf(node, mode) {
|
|
376
|
+
if (mode === "decision") {
|
|
377
|
+
if (node.kind === "decision") return { w: 58, h: 36 };
|
|
378
|
+
if (node.kind === "chance") return { w: 34, h: 34 };
|
|
379
|
+
return { w: 22, h: 22 };
|
|
380
|
+
}
|
|
381
|
+
if (mode === "ml") {
|
|
382
|
+
if (node.kind === "split") return { w: 200, h: 100 };
|
|
383
|
+
return { w: 200, h: 82 };
|
|
384
|
+
}
|
|
385
|
+
return { w: 150, h: 50 };
|
|
386
|
+
}
|
|
387
|
+
function wrap(node, mode, depth, parent) {
|
|
388
|
+
const w = {
|
|
389
|
+
node,
|
|
390
|
+
size: sizeOf(node, mode),
|
|
391
|
+
depth,
|
|
392
|
+
parent,
|
|
393
|
+
children: [],
|
|
394
|
+
prelim: 0,
|
|
395
|
+
mod: 0,
|
|
396
|
+
xFinal: 0,
|
|
397
|
+
yFinal: 0
|
|
398
|
+
};
|
|
399
|
+
w.children = node.children.map((c) => wrap(c, mode, depth + 1, w));
|
|
400
|
+
return w;
|
|
401
|
+
}
|
|
402
|
+
function collect(w, out) {
|
|
403
|
+
out.push(w);
|
|
404
|
+
for (const c of w.children) collect(c, out);
|
|
405
|
+
}
|
|
406
|
+
function sibExtent(w, sibH) {
|
|
407
|
+
return sibH ? w.size.w : w.size.h;
|
|
408
|
+
}
|
|
409
|
+
function assignLeafPositions(w, sibH, unit, leafGap, cursor) {
|
|
410
|
+
if (w.children.length === 0) {
|
|
411
|
+
w.prelim = cursor.v;
|
|
412
|
+
const extent = sibExtent(w, sibH);
|
|
413
|
+
cursor.v += Math.max(extent, unit) + leafGap;
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
for (const c of w.children) assignLeafPositions(c, sibH, unit, leafGap, cursor);
|
|
417
|
+
const first = w.children[0].prelim;
|
|
418
|
+
const last = w.children[w.children.length - 1].prelim;
|
|
419
|
+
w.prelim = (first + last) / 2;
|
|
420
|
+
}
|
|
421
|
+
function subtreeSibRange(w, sibH) {
|
|
422
|
+
const e = sibExtent(w, sibH);
|
|
423
|
+
let lo = w.prelim - e / 2;
|
|
424
|
+
let hi = w.prelim + e / 2;
|
|
425
|
+
for (const c of w.children) {
|
|
426
|
+
const cr = subtreeSibRange(c, sibH);
|
|
427
|
+
if (cr.lo < lo) lo = cr.lo;
|
|
428
|
+
if (cr.hi > hi) hi = cr.hi;
|
|
429
|
+
}
|
|
430
|
+
return { lo, hi };
|
|
431
|
+
}
|
|
432
|
+
function enforceSibGap(w, sibH, sibGap) {
|
|
433
|
+
for (const c of w.children) enforceSibGap(c, sibH, sibGap);
|
|
434
|
+
for (let i = 1; i < w.children.length; i++) {
|
|
435
|
+
const prev = w.children[i - 1];
|
|
436
|
+
const cur = w.children[i];
|
|
437
|
+
const prevR = subtreeSibRange(prev, sibH);
|
|
438
|
+
const curR = subtreeSibRange(cur, sibH);
|
|
439
|
+
const gap = curR.lo - prevR.hi;
|
|
440
|
+
if (gap < sibGap) {
|
|
441
|
+
shiftSubtree(cur, sibGap - gap);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (w.children.length > 0) {
|
|
445
|
+
const first = w.children[0].prelim;
|
|
446
|
+
const last = w.children[w.children.length - 1].prelim;
|
|
447
|
+
w.prelim = (first + last) / 2;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
function shiftSubtree(w, dx) {
|
|
451
|
+
w.prelim += dx;
|
|
452
|
+
for (const c of w.children) shiftSubtree(c, dx);
|
|
453
|
+
}
|
|
454
|
+
function computeLevelOffsets(root, sibH, levelGap) {
|
|
455
|
+
const depthSizes = [];
|
|
456
|
+
const all = [];
|
|
457
|
+
collect(root, all);
|
|
458
|
+
for (const n of all) {
|
|
459
|
+
const dSize = sibH ? n.size.h : n.size.w;
|
|
460
|
+
depthSizes[n.depth] = Math.max(depthSizes[n.depth] ?? 0, dSize);
|
|
461
|
+
}
|
|
462
|
+
const offsets = [];
|
|
463
|
+
let acc = 0;
|
|
464
|
+
for (let i = 0; i < depthSizes.length; i++) {
|
|
465
|
+
if (i === 0) acc = 0;
|
|
466
|
+
else acc += depthSizes[i - 1] / 2 + levelGap + depthSizes[i] / 2;
|
|
467
|
+
offsets.push(acc);
|
|
468
|
+
}
|
|
469
|
+
return offsets;
|
|
470
|
+
}
|
|
471
|
+
function setFinal(w, sibH, levelOffsets) {
|
|
472
|
+
const d = levelOffsets[w.depth] ?? 0;
|
|
473
|
+
if (sibH) {
|
|
474
|
+
w.xFinal = w.prelim;
|
|
475
|
+
w.yFinal = d;
|
|
476
|
+
} else {
|
|
477
|
+
w.xFinal = d;
|
|
478
|
+
w.yFinal = w.prelim;
|
|
479
|
+
}
|
|
480
|
+
for (const c of w.children) setFinal(c, sibH, levelOffsets);
|
|
481
|
+
}
|
|
482
|
+
function routeEdge(style, sibH, p, c, rail) {
|
|
483
|
+
const px = p.x, py = p.y, cx = c.x, cy = c.y;
|
|
484
|
+
let startX, startY, endX, endY;
|
|
485
|
+
if (sibH) {
|
|
486
|
+
startX = px;
|
|
487
|
+
startY = py + p.height / 2;
|
|
488
|
+
endX = cx;
|
|
489
|
+
endY = cy - c.height / 2;
|
|
490
|
+
} else {
|
|
491
|
+
startX = px + p.width / 2;
|
|
492
|
+
startY = py;
|
|
493
|
+
endX = cx - c.width / 2;
|
|
494
|
+
endY = cy;
|
|
495
|
+
}
|
|
496
|
+
if (style === "diagonal") {
|
|
497
|
+
const tLabel = 0.68;
|
|
498
|
+
const lx = startX + (endX - startX) * tLabel;
|
|
499
|
+
const ly = startY + (endY - startY) * tLabel;
|
|
500
|
+
const dx = endX - startX;
|
|
501
|
+
const dy = endY - startY;
|
|
502
|
+
let angle = Math.atan2(dy, dx) * 180 / Math.PI;
|
|
503
|
+
if (angle > 90) angle -= 180;
|
|
504
|
+
if (angle < -90) angle += 180;
|
|
505
|
+
return {
|
|
506
|
+
path: `M ${startX} ${startY} L ${endX} ${endY}`,
|
|
507
|
+
labelX: lx,
|
|
508
|
+
labelY: ly,
|
|
509
|
+
angle
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
if (style === "bracket") {
|
|
513
|
+
if (sibH) {
|
|
514
|
+
const stub2 = Math.min(18, Math.abs(endY - startY) * 0.25);
|
|
515
|
+
const elbowY = startY + stub2;
|
|
516
|
+
const midX = (startX + endX) / 2;
|
|
517
|
+
const midY = (elbowY + endY) / 2;
|
|
518
|
+
const angle = Math.atan2(endY - elbowY, endX - startX) * 180 / Math.PI;
|
|
519
|
+
return {
|
|
520
|
+
path: `M ${startX} ${startY} L ${startX} ${elbowY} L ${endX} ${endY}`,
|
|
521
|
+
labelX: midX,
|
|
522
|
+
labelY: midY,
|
|
523
|
+
angle: angle > 90 ? angle - 180 : angle < -90 ? angle + 180 : angle
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
const stub = Math.min(18, Math.abs(endX - startX) * 0.25);
|
|
527
|
+
const elbowX = startX + stub;
|
|
528
|
+
return {
|
|
529
|
+
path: `M ${startX} ${startY} L ${elbowX} ${startY} L ${endX} ${endY}`,
|
|
530
|
+
labelX: (elbowX + endX) / 2,
|
|
531
|
+
labelY: (startY + endY) / 2,
|
|
532
|
+
angle: Math.atan2(endY - startY, endX - elbowX) * 180 / Math.PI
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
if (sibH) {
|
|
536
|
+
const railY = rail ?? (startY + endY) / 2;
|
|
537
|
+
return {
|
|
538
|
+
path: `M ${startX} ${startY} L ${startX} ${railY} L ${endX} ${railY} L ${endX} ${endY}`,
|
|
539
|
+
labelX: (startX + endX) / 2,
|
|
540
|
+
labelY: railY,
|
|
541
|
+
angle: 0
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
const railX = rail ?? (startX + endX) / 2;
|
|
545
|
+
return {
|
|
546
|
+
path: `M ${startX} ${startY} L ${railX} ${startY} L ${railX} ${endY} L ${endX} ${endY}`,
|
|
547
|
+
labelX: railX,
|
|
548
|
+
labelY: (startY + endY) / 2,
|
|
549
|
+
angle: 0
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
function routeSnappedEnd(sibH, p, c) {
|
|
553
|
+
if (sibH) {
|
|
554
|
+
const startX2 = p.x, startY2 = p.y + p.height / 2;
|
|
555
|
+
const endX2 = c.x, endY2 = c.y - c.height / 2;
|
|
556
|
+
return {
|
|
557
|
+
path: `M ${startX2} ${startY2} L ${endX2} ${endY2}`,
|
|
558
|
+
labelX: (startX2 + endX2) / 2,
|
|
559
|
+
labelY: (startY2 + endY2) / 2,
|
|
560
|
+
angle: 0
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
const startX = p.x + p.width / 2;
|
|
564
|
+
const startY = p.y;
|
|
565
|
+
const endX = c.x - c.width / 2;
|
|
566
|
+
const endY = c.y;
|
|
567
|
+
const naturalCx = c.naturalX ?? c.x;
|
|
568
|
+
const bendX = Math.max(naturalCx, startX + 20);
|
|
569
|
+
const dx = bendX - startX;
|
|
570
|
+
const dy = endY - startY;
|
|
571
|
+
let angle = Math.atan2(dy, dx) * 180 / Math.PI;
|
|
572
|
+
if (angle > 90) angle -= 180;
|
|
573
|
+
if (angle < -90) angle += 180;
|
|
574
|
+
const t = 0.7;
|
|
575
|
+
const labelX = startX + (bendX - startX) * t;
|
|
576
|
+
const labelY = startY + (endY - startY) * t;
|
|
577
|
+
return {
|
|
578
|
+
path: `M ${startX} ${startY} L ${bendX} ${endY} L ${endX} ${endY}`,
|
|
579
|
+
labelX,
|
|
580
|
+
labelY,
|
|
581
|
+
angle
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
function layoutDecisionTree(ast) {
|
|
585
|
+
const sibH = ast.direction === "top-down";
|
|
586
|
+
const root = wrap(ast.root, ast.mode, 0);
|
|
587
|
+
const all = [];
|
|
588
|
+
collect(root, all);
|
|
589
|
+
const perDepthDepthSize = [];
|
|
590
|
+
for (const w of all) {
|
|
591
|
+
const d = sibH ? w.size.h : w.size.w;
|
|
592
|
+
perDepthDepthSize[w.depth] = Math.max(perDepthDepthSize[w.depth] ?? 0, d);
|
|
593
|
+
}
|
|
594
|
+
for (const w of all) {
|
|
595
|
+
const target = perDepthDepthSize[w.depth];
|
|
596
|
+
if (sibH) w.size.h = target;
|
|
597
|
+
else w.size.w = target;
|
|
598
|
+
}
|
|
599
|
+
const maxSibExtent = Math.max(...all.map((w) => sibExtent(w, sibH)));
|
|
600
|
+
let leafGap, levelGap, sibGap;
|
|
601
|
+
if (ast.mode === "ml") {
|
|
602
|
+
leafGap = 40;
|
|
603
|
+
levelGap = 80;
|
|
604
|
+
sibGap = 40;
|
|
605
|
+
} else if (ast.mode === "decision") {
|
|
606
|
+
leafGap = sibH ? 40 : 36;
|
|
607
|
+
levelGap = sibH ? 90 : 110;
|
|
608
|
+
sibGap = 30;
|
|
609
|
+
} else {
|
|
610
|
+
leafGap = sibH ? 30 : 22;
|
|
611
|
+
levelGap = sibH ? 80 : 90;
|
|
612
|
+
sibGap = 26;
|
|
613
|
+
}
|
|
614
|
+
const unit = Math.max(maxSibExtent, sibH ? 120 : 50);
|
|
615
|
+
const cursor = { v: 0 };
|
|
616
|
+
assignLeafPositions(root, sibH, unit, leafGap, cursor);
|
|
617
|
+
enforceSibGap(root, sibH, sibGap);
|
|
618
|
+
const levelOffsets = computeLevelOffsets(root, sibH, levelGap);
|
|
619
|
+
setFinal(root, sibH, levelOffsets);
|
|
620
|
+
const PADDING2 = 40;
|
|
621
|
+
const extraLeft = ast.mode === "decision" && !sibH ? 110 : 0;
|
|
622
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
623
|
+
for (const n of all) {
|
|
624
|
+
minX = Math.min(minX, n.xFinal - n.size.w / 2);
|
|
625
|
+
minY = Math.min(minY, n.yFinal - n.size.h / 2);
|
|
626
|
+
maxX = Math.max(maxX, n.xFinal + n.size.w / 2);
|
|
627
|
+
maxY = Math.max(maxY, n.yFinal + n.size.h / 2);
|
|
628
|
+
}
|
|
629
|
+
const needsPayoffCol = ast.mode === "decision" && !sibH;
|
|
630
|
+
const payoffColGap = needsPayoffCol ? 110 : 0;
|
|
631
|
+
const extraRight = needsPayoffCol ? 180 : ast.mode === "decision" ? 110 : 20;
|
|
632
|
+
const naturalXMap = /* @__PURE__ */ new Map();
|
|
633
|
+
for (const w of all) naturalXMap.set(w.node.id, w.xFinal);
|
|
634
|
+
if (needsPayoffCol) {
|
|
635
|
+
let endMaxX = -Infinity;
|
|
636
|
+
for (const w of all) if (w.node.kind === "end") endMaxX = Math.max(endMaxX, w.xFinal);
|
|
637
|
+
for (const w of all) if (w.node.kind === "end") w.xFinal = endMaxX;
|
|
638
|
+
minX = Infinity;
|
|
639
|
+
maxX = -Infinity;
|
|
640
|
+
for (const n of all) {
|
|
641
|
+
minX = Math.min(minX, n.xFinal - n.size.w / 2);
|
|
642
|
+
maxX = Math.max(maxX, n.xFinal + n.size.w / 2);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
const offsetX = PADDING2 + extraLeft - minX;
|
|
646
|
+
const offsetY = PADDING2 - minY;
|
|
647
|
+
const layoutNodes = all.map((w) => ({
|
|
648
|
+
node: w.node,
|
|
649
|
+
x: w.xFinal + offsetX,
|
|
650
|
+
y: w.yFinal + offsetY,
|
|
651
|
+
width: w.size.w,
|
|
652
|
+
height: w.size.h,
|
|
653
|
+
depth: w.depth,
|
|
654
|
+
naturalX: (naturalXMap.get(w.node.id) ?? w.xFinal) + offsetX,
|
|
655
|
+
naturalY: w.yFinal + offsetY
|
|
656
|
+
}));
|
|
657
|
+
const nodeById = new Map(layoutNodes.map((n) => [n.node.id, n]));
|
|
658
|
+
const edgeStyle = ast.edgeStyle ?? (ast.mode === "decision" ? "diagonal" : "orthogonal");
|
|
659
|
+
const levelRails = [];
|
|
660
|
+
if (edgeStyle === "orthogonal") {
|
|
661
|
+
for (let d = 1; d < perDepthDepthSize.length; d++) {
|
|
662
|
+
const parentBotY = (levelOffsets[d - 1] ?? 0) + perDepthDepthSize[d - 1] / 2;
|
|
663
|
+
const childTopY = (levelOffsets[d] ?? 0) - perDepthDepthSize[d] / 2;
|
|
664
|
+
levelRails[d] = (parentBotY + childTopY) / 2 + (sibH ? offsetY : offsetX);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
const labelAnchors = {};
|
|
668
|
+
const edges = [];
|
|
669
|
+
for (const w of all) {
|
|
670
|
+
for (const c of w.children) {
|
|
671
|
+
const pn = nodeById.get(w.node.id);
|
|
672
|
+
const cn = nodeById.get(c.node.id);
|
|
673
|
+
const wasSnapped = needsPayoffCol && c.node.kind === "end" && cn.naturalX !== void 0 && Math.abs(cn.naturalX - cn.x) > 1;
|
|
674
|
+
const geom = wasSnapped ? routeSnappedEnd(sibH, pn, cn) : routeEdge(edgeStyle, sibH, pn, cn, levelRails[c.depth]);
|
|
675
|
+
const isOptimal = c.node.optimal === true;
|
|
676
|
+
let strokeWidth = 1.6;
|
|
677
|
+
if (isOptimal) strokeWidth = 3;
|
|
678
|
+
if (ast.branchLengthProb && c.node.incomingProb !== void 0) {
|
|
679
|
+
strokeWidth = 1 + c.node.incomingProb * 2.5;
|
|
680
|
+
}
|
|
681
|
+
let label;
|
|
682
|
+
if (ast.mode === "decision") {
|
|
683
|
+
if (c.node.incomingChoice !== void 0) label = c.node.incomingChoice;
|
|
684
|
+
else if (c.node.incomingProb !== void 0) label = formatProb(c.node.incomingProb);
|
|
685
|
+
} else if (ast.mode === "ml") {
|
|
686
|
+
if (c.node.mlBranch) {
|
|
687
|
+
if (ast.branchLabels === "relation" && w.node.op && w.node.threshold !== void 0) {
|
|
688
|
+
const op = c.node.mlBranch === "true" ? w.node.op : flipOp(w.node.op);
|
|
689
|
+
label = `${op} ${w.node.threshold}`;
|
|
690
|
+
} else {
|
|
691
|
+
label = c.node.mlBranch === "true" ? "True" : "False";
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
} else {
|
|
695
|
+
if (c.node.branchLabel) label = c.node.branchLabel;
|
|
696
|
+
}
|
|
697
|
+
labelAnchors[c.node.id] = { x: geom.labelX, y: geom.labelY, angle: geom.angle };
|
|
698
|
+
edges.push({ from: w.node.id, to: c.node.id, path: geom.path, label, isOptimal, strokeWidth });
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
let payoffColumnX;
|
|
702
|
+
if (needsPayoffCol) {
|
|
703
|
+
let endX = 0;
|
|
704
|
+
for (const n of layoutNodes) if (n.node.kind === "end") endX = Math.max(endX, n.x + n.width / 2);
|
|
705
|
+
payoffColumnX = endX + payoffColGap;
|
|
706
|
+
}
|
|
707
|
+
const width = Math.ceil(maxX - minX + PADDING2 * 2 + extraRight + extraLeft);
|
|
708
|
+
const height = Math.ceil(maxY - minY + PADDING2 * 2);
|
|
709
|
+
return {
|
|
710
|
+
width,
|
|
711
|
+
height,
|
|
712
|
+
nodes: layoutNodes,
|
|
713
|
+
edges,
|
|
714
|
+
levelRails: edgeStyle === "orthogonal" ? levelRails : void 0,
|
|
715
|
+
title: ast.title,
|
|
716
|
+
mode: ast.mode,
|
|
717
|
+
direction: ast.direction,
|
|
718
|
+
edgeStyle,
|
|
719
|
+
labelAnchors,
|
|
720
|
+
payoffColumnX
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
function flipOp(op) {
|
|
724
|
+
if (op === "<=") return ">";
|
|
725
|
+
if (op === "<") return ">=";
|
|
726
|
+
if (op === ">=") return "<";
|
|
727
|
+
if (op === ">") return "<=";
|
|
728
|
+
return op;
|
|
729
|
+
}
|
|
730
|
+
function formatProb(p) {
|
|
731
|
+
if (p >= 0.01 && p <= 0.99) {
|
|
732
|
+
const pct = Math.round(p * 100);
|
|
733
|
+
if (Math.abs(p * 100 - pct) < 0.01) return `p=${pct}%`;
|
|
734
|
+
}
|
|
735
|
+
return `p=${p}`;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// src/diagrams/decisiontree/renderer.ts
|
|
739
|
+
var CLASS_PALETTE = [
|
|
740
|
+
"#0ea5e9",
|
|
741
|
+
"#10b981",
|
|
742
|
+
"#f59e0b",
|
|
743
|
+
"#f43f5e",
|
|
744
|
+
"#8b5cf6",
|
|
745
|
+
"#14b8a6",
|
|
746
|
+
"#ec4899",
|
|
747
|
+
"#84cc16",
|
|
748
|
+
"#06b6d4",
|
|
749
|
+
"#f97316"
|
|
750
|
+
];
|
|
751
|
+
function buildCss(t) {
|
|
752
|
+
return `
|
|
753
|
+
.lt-dtree { background: ${t.bg}; font-family: system-ui, -apple-system, sans-serif; }
|
|
754
|
+
.lt-dtree-title { font: 500 16px sans-serif; fill: ${t.text}; }
|
|
755
|
+
.lt-dtree-edge { fill: none; stroke: ${t.stroke}; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
|
|
756
|
+
.lt-dtree-edge-optimal { fill: none; stroke: ${t.positive}; stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; }
|
|
757
|
+
.lt-dtree-edge-leader { fill: none; stroke: ${t.stroke}; stroke-width: 1; stroke-dasharray: 2 2; opacity: 0.55; }
|
|
758
|
+
.lt-dtree-edge-label { font: 500 11px sans-serif; fill: ${t.text}; text-anchor: middle; dominant-baseline: middle; }
|
|
759
|
+
.lt-dtree-edge-prob { font: italic 400 10px sans-serif; fill: ${t.textMuted}; text-anchor: middle; dominant-baseline: middle; }
|
|
760
|
+
.lt-dtree-edge-label-bg { fill: ${t.bg}; stroke: none; }
|
|
761
|
+
.lt-dtree-decision { fill: #dbeafe; stroke: #1d4ed8; stroke-width: 1.6; }
|
|
762
|
+
.lt-dtree-chance { fill: #fef3c7; stroke: #b45309; stroke-width: 1.6; }
|
|
763
|
+
.lt-dtree-outcome { fill: #f1f5f9; stroke: ${t.stroke}; stroke-width: 1.4; }
|
|
764
|
+
.lt-dtree-node-label { font: 500 12px sans-serif; fill: ${t.text}; }
|
|
765
|
+
.lt-dtree-ev { font: 500 10px "SF Mono", monospace; fill: ${t.textMuted}; }
|
|
766
|
+
.lt-dtree-ev-optimal { font: 600 10px "SF Mono", monospace; fill: ${t.positive}; }
|
|
767
|
+
.lt-dtree-payoff { font: 600 12px "SF Mono", monospace; fill: ${t.text}; }
|
|
768
|
+
.lt-dtree-payoff-neg { font: 600 12px "SF Mono", monospace; fill: ${t.negative ?? "#dc2626"}; }
|
|
769
|
+
.lt-dtree-ml-rect { stroke: ${t.stroke}; stroke-width: 1; }
|
|
770
|
+
.lt-dtree-ml-line-1 { font: 500 12px sans-serif; fill: ${t.text}; }
|
|
771
|
+
.lt-dtree-ml-line-muted { font: 400 10px sans-serif; fill: ${t.textMuted}; }
|
|
772
|
+
.lt-dtree-ml-mono { font: 400 10px "SF Mono", monospace; fill: ${t.textMuted}; }
|
|
773
|
+
.lt-dtree-ml-class { font: 600 11px sans-serif; }
|
|
774
|
+
.lt-dtree-taxon { fill: #eef2ff; stroke: #4f46e5; stroke-width: 1.4; }
|
|
775
|
+
.lt-dtree-taxon-leaf { fill: #ecfdf5; stroke: #059669; stroke-width: 1.4; }
|
|
776
|
+
.lt-dtree-taxon-label { font: 500 12px sans-serif; fill: ${t.text}; text-anchor: middle; }
|
|
777
|
+
`.trim();
|
|
778
|
+
}
|
|
779
|
+
function renderDecisionNode(ln, layout) {
|
|
780
|
+
const n = ln.node;
|
|
781
|
+
const parts = [];
|
|
782
|
+
const sibH = layout.direction === "top-down";
|
|
783
|
+
if (n.kind === "decision") {
|
|
784
|
+
parts.push(rect({
|
|
785
|
+
x: ln.x - ln.width / 2,
|
|
786
|
+
y: ln.y - ln.height / 2,
|
|
787
|
+
width: ln.width,
|
|
788
|
+
height: ln.height,
|
|
789
|
+
rx: 2,
|
|
790
|
+
ry: 2,
|
|
791
|
+
class: "lt-dtree-decision"
|
|
792
|
+
}));
|
|
793
|
+
if (n.label) {
|
|
794
|
+
if (sibH) {
|
|
795
|
+
parts.push(text({
|
|
796
|
+
x: ln.x,
|
|
797
|
+
y: ln.y - ln.height / 2 - 8,
|
|
798
|
+
class: "lt-dtree-node-label",
|
|
799
|
+
"text-anchor": "middle"
|
|
800
|
+
}, n.label));
|
|
801
|
+
} else {
|
|
802
|
+
parts.push(text({
|
|
803
|
+
x: ln.x - ln.width / 2 - 8,
|
|
804
|
+
y: ln.y + 4,
|
|
805
|
+
class: "lt-dtree-node-label",
|
|
806
|
+
"text-anchor": "end"
|
|
807
|
+
}, n.label));
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
if (n.ev !== void 0) {
|
|
811
|
+
parts.push(text({
|
|
812
|
+
x: ln.x,
|
|
813
|
+
y: ln.y + ln.height / 2 + 13,
|
|
814
|
+
class: "lt-dtree-ev",
|
|
815
|
+
"text-anchor": "middle"
|
|
816
|
+
}, `EV=${formatNum(n.ev)}`));
|
|
817
|
+
}
|
|
818
|
+
} else if (n.kind === "chance") {
|
|
819
|
+
parts.push(circle({
|
|
820
|
+
cx: ln.x,
|
|
821
|
+
cy: ln.y,
|
|
822
|
+
r: ln.width / 2,
|
|
823
|
+
class: "lt-dtree-chance"
|
|
824
|
+
}));
|
|
825
|
+
if (n.label) {
|
|
826
|
+
if (sibH) {
|
|
827
|
+
parts.push(text({
|
|
828
|
+
x: ln.x,
|
|
829
|
+
y: ln.y - ln.height / 2 - 8,
|
|
830
|
+
class: "lt-dtree-node-label",
|
|
831
|
+
"text-anchor": "middle"
|
|
832
|
+
}, n.label));
|
|
833
|
+
} else {
|
|
834
|
+
parts.push(text({
|
|
835
|
+
x: ln.x,
|
|
836
|
+
y: ln.y - ln.height / 2 - 6,
|
|
837
|
+
class: "lt-dtree-node-label",
|
|
838
|
+
"text-anchor": "middle"
|
|
839
|
+
}, n.label));
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
if (n.ev !== void 0) {
|
|
843
|
+
parts.push(text({
|
|
844
|
+
x: ln.x,
|
|
845
|
+
y: ln.y + ln.height / 2 + 13,
|
|
846
|
+
class: n.optimal ? "lt-dtree-ev-optimal" : "lt-dtree-ev",
|
|
847
|
+
"text-anchor": "middle"
|
|
848
|
+
}, `EV=${formatNum(n.ev)}`));
|
|
849
|
+
}
|
|
850
|
+
} else if (n.kind === "end") {
|
|
851
|
+
const halfW = ln.width / 2;
|
|
852
|
+
const halfH = ln.height / 2;
|
|
853
|
+
const pts = [
|
|
854
|
+
`${ln.x - halfW},${ln.y - halfH}`,
|
|
855
|
+
`${ln.x + halfW},${ln.y}`,
|
|
856
|
+
`${ln.x - halfW},${ln.y + halfH}`
|
|
857
|
+
].join(" ");
|
|
858
|
+
parts.push(polygon({ points: pts, class: "lt-dtree-outcome" }));
|
|
859
|
+
const colX = layout.payoffColumnX;
|
|
860
|
+
const tipX = ln.x + halfW;
|
|
861
|
+
const textY = ln.y + 4;
|
|
862
|
+
const payoffCls = n.payoff !== void 0 && n.payoff < 0 ? "lt-dtree-payoff-neg" : "lt-dtree-payoff";
|
|
863
|
+
const payoffStr = n.payoff !== void 0 ? formatPayoff(n.payoff) : "";
|
|
864
|
+
if (colX !== void 0 && colX > tipX + 20) {
|
|
865
|
+
parts.push(path({
|
|
866
|
+
d: `M ${tipX + 2} ${ln.y} L ${colX - 6} ${ln.y}`,
|
|
867
|
+
class: "lt-dtree-edge-leader"
|
|
868
|
+
}));
|
|
869
|
+
if (n.label) {
|
|
870
|
+
parts.push(text({
|
|
871
|
+
x: (tipX + colX) / 2,
|
|
872
|
+
y: ln.y - 6,
|
|
873
|
+
class: "lt-dtree-edge-label",
|
|
874
|
+
"text-anchor": "middle"
|
|
875
|
+
}, n.label));
|
|
876
|
+
}
|
|
877
|
+
if (payoffStr) {
|
|
878
|
+
parts.push(text({
|
|
879
|
+
x: colX,
|
|
880
|
+
y: textY,
|
|
881
|
+
class: payoffCls,
|
|
882
|
+
"text-anchor": "start"
|
|
883
|
+
}, payoffStr));
|
|
884
|
+
}
|
|
885
|
+
} else {
|
|
886
|
+
const labelParts = [];
|
|
887
|
+
if (payoffStr) labelParts.push(payoffStr);
|
|
888
|
+
if (n.label) labelParts.push(n.label);
|
|
889
|
+
if (labelParts.length > 0) {
|
|
890
|
+
parts.push(text({
|
|
891
|
+
x: tipX + 8,
|
|
892
|
+
y: textY,
|
|
893
|
+
class: payoffCls
|
|
894
|
+
}, labelParts.join(" \xB7 ")));
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
return group({
|
|
899
|
+
"data-node-id": n.id,
|
|
900
|
+
"data-node-kind": n.kind,
|
|
901
|
+
"data-ev": n.ev !== void 0 ? String(n.ev) : ""
|
|
902
|
+
}, parts);
|
|
903
|
+
}
|
|
904
|
+
function formatNum(n) {
|
|
905
|
+
if (Math.abs(n) >= 1e4) return n.toLocaleString(void 0, { maximumFractionDigits: 0 });
|
|
906
|
+
if (Number.isInteger(n)) return String(n);
|
|
907
|
+
return n.toFixed(2);
|
|
908
|
+
}
|
|
909
|
+
function formatPayoff(n) {
|
|
910
|
+
const abs = Math.abs(n);
|
|
911
|
+
const sign = n < 0 ? "-" : "";
|
|
912
|
+
if (abs >= 1e6) return `${sign}$${(abs / 1e6).toFixed(abs >= 1e7 ? 0 : 1)}M`;
|
|
913
|
+
if (abs >= 1e3) return `${sign}$${abs.toLocaleString()}`;
|
|
914
|
+
return String(n);
|
|
915
|
+
}
|
|
916
|
+
function classColor(i) {
|
|
917
|
+
return CLASS_PALETTE[i % CLASS_PALETTE.length];
|
|
918
|
+
}
|
|
919
|
+
function mlNodeFillColor(n) {
|
|
920
|
+
if (!Array.isArray(n.value)) return { fill: "#f1f5f9" };
|
|
921
|
+
const arr = n.value;
|
|
922
|
+
const sum = arr.reduce((a, b) => a + b, 0);
|
|
923
|
+
if (sum === 0) return { fill: "#f1f5f9" };
|
|
924
|
+
let maxIdx = 0;
|
|
925
|
+
for (let i = 1; i < arr.length; i++) if (arr[i] > arr[maxIdx]) maxIdx = i;
|
|
926
|
+
const purity = arr[maxIdx] / sum;
|
|
927
|
+
const nClasses = arr.length;
|
|
928
|
+
const minPurity = 1 / nClasses;
|
|
929
|
+
const alpha = Math.max(0.12, 0.12 + 0.8 * ((purity - minPurity) / (1 - minPurity)));
|
|
930
|
+
return { fill: hexWithAlpha(classColor(maxIdx), alpha) };
|
|
931
|
+
}
|
|
932
|
+
function hexWithAlpha(hex, alpha) {
|
|
933
|
+
return hex + Math.round(alpha * 255).toString(16).padStart(2, "0");
|
|
934
|
+
}
|
|
935
|
+
function renderMlNode(ln, ast) {
|
|
936
|
+
const n = ln.node;
|
|
937
|
+
const { fill } = mlNodeFillColor(n);
|
|
938
|
+
const parts = [];
|
|
939
|
+
const x = ln.x - ln.width / 2;
|
|
940
|
+
const y = ln.y - ln.height / 2;
|
|
941
|
+
parts.push(rect({
|
|
942
|
+
x,
|
|
943
|
+
y,
|
|
944
|
+
width: ln.width,
|
|
945
|
+
height: ln.height,
|
|
946
|
+
rx: 6,
|
|
947
|
+
ry: 6,
|
|
948
|
+
fill,
|
|
949
|
+
class: "lt-dtree-ml-rect"
|
|
950
|
+
}));
|
|
951
|
+
const textX = ln.x;
|
|
952
|
+
let textY = y + 16;
|
|
953
|
+
const lineH = 14;
|
|
954
|
+
if (n.kind === "split" && n.feature) {
|
|
955
|
+
const thresh = typeof n.threshold === "number" ? formatNum(n.threshold) : n.threshold ?? "";
|
|
956
|
+
parts.push(text(
|
|
957
|
+
{ x: textX, y: textY, class: "lt-dtree-ml-line-1", "text-anchor": "middle" },
|
|
958
|
+
`${n.feature} ${n.op ?? ""} ${thresh}`
|
|
959
|
+
));
|
|
960
|
+
textY += lineH;
|
|
961
|
+
} else if (n.label) {
|
|
962
|
+
parts.push(text({ x: textX, y: textY, class: "lt-dtree-ml-line-1", "text-anchor": "middle" }, n.label));
|
|
963
|
+
textY += lineH;
|
|
964
|
+
}
|
|
965
|
+
if (n.impurity !== void 0) {
|
|
966
|
+
const impName = ast.impurityName ?? "gini";
|
|
967
|
+
parts.push(text(
|
|
968
|
+
{ x: textX, y: textY, class: "lt-dtree-ml-line-muted", "text-anchor": "middle" },
|
|
969
|
+
`${impName} = ${formatNum(n.impurity)}`
|
|
970
|
+
));
|
|
971
|
+
textY += lineH;
|
|
972
|
+
}
|
|
973
|
+
if (n.samples !== void 0) {
|
|
974
|
+
parts.push(text(
|
|
975
|
+
{ x: textX, y: textY, class: "lt-dtree-ml-line-muted", "text-anchor": "middle" },
|
|
976
|
+
`samples = ${n.samples}`
|
|
977
|
+
));
|
|
978
|
+
textY += lineH;
|
|
979
|
+
}
|
|
980
|
+
if (n.value !== void 0) {
|
|
981
|
+
const vStr = Array.isArray(n.value) ? `value = [${n.value.join(", ")}]` : `value = ${formatNum(n.value)}`;
|
|
982
|
+
parts.push(text({ x: textX, y: textY, class: "lt-dtree-ml-mono", "text-anchor": "middle" }, vStr));
|
|
983
|
+
textY += lineH;
|
|
984
|
+
}
|
|
985
|
+
if (n.className) {
|
|
986
|
+
let colorIdx = 0;
|
|
987
|
+
if (ast.classes) {
|
|
988
|
+
const idx = ast.classes.indexOf(n.className);
|
|
989
|
+
if (idx >= 0) colorIdx = idx;
|
|
990
|
+
}
|
|
991
|
+
parts.push(text(
|
|
992
|
+
{ x: textX, y: textY, class: "lt-dtree-ml-class", "text-anchor": "middle", fill: classColor(colorIdx) },
|
|
993
|
+
`class = ${n.className}`
|
|
994
|
+
));
|
|
995
|
+
textY += lineH;
|
|
996
|
+
} else if (typeof n.value === "number") {
|
|
997
|
+
parts.push(text(
|
|
998
|
+
{ x: textX, y: textY, class: "lt-dtree-ml-class", "text-anchor": "middle", fill: "#0f172a" },
|
|
999
|
+
`predicted = ${formatNum(n.value)}`
|
|
1000
|
+
));
|
|
1001
|
+
textY += lineH;
|
|
1002
|
+
}
|
|
1003
|
+
if (Array.isArray(n.value) && ast.classes) {
|
|
1004
|
+
const arr = n.value;
|
|
1005
|
+
const sum = arr.reduce((a, b) => a + b, 0);
|
|
1006
|
+
if (sum > 0) {
|
|
1007
|
+
const barY = y + ln.height - 10;
|
|
1008
|
+
const barX = x + 10;
|
|
1009
|
+
const barW = ln.width - 20;
|
|
1010
|
+
let cursor = barX;
|
|
1011
|
+
for (let i = 0; i < arr.length; i++) {
|
|
1012
|
+
const seg = arr[i] / sum * barW;
|
|
1013
|
+
if (seg > 0) {
|
|
1014
|
+
parts.push(rect({ x: cursor, y: barY, width: seg, height: 5, fill: classColor(i), stroke: "none" }));
|
|
1015
|
+
}
|
|
1016
|
+
cursor += seg;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
return group({
|
|
1021
|
+
"data-node-id": n.id,
|
|
1022
|
+
"data-node-kind": n.kind,
|
|
1023
|
+
"data-samples": n.samples !== void 0 ? String(n.samples) : "",
|
|
1024
|
+
"data-class": n.className ?? ""
|
|
1025
|
+
}, parts);
|
|
1026
|
+
}
|
|
1027
|
+
function renderTaxonomyNode(ln) {
|
|
1028
|
+
const n = ln.node;
|
|
1029
|
+
const isLeaf = n.kind === "answer" || n.children.length === 0;
|
|
1030
|
+
const cls = isLeaf ? "lt-dtree-taxon-leaf" : "lt-dtree-taxon";
|
|
1031
|
+
const parts = [];
|
|
1032
|
+
parts.push(rect({
|
|
1033
|
+
x: ln.x - ln.width / 2,
|
|
1034
|
+
y: ln.y - ln.height / 2,
|
|
1035
|
+
width: ln.width,
|
|
1036
|
+
height: ln.height,
|
|
1037
|
+
rx: 8,
|
|
1038
|
+
ry: 8,
|
|
1039
|
+
class: cls
|
|
1040
|
+
}));
|
|
1041
|
+
const lines = wrapText(n.label, 22);
|
|
1042
|
+
const totalH = lines.length * 14;
|
|
1043
|
+
let ty = ln.y - totalH / 2 + 11;
|
|
1044
|
+
for (const line2 of lines) {
|
|
1045
|
+
parts.push(text({ x: ln.x, y: ty, class: "lt-dtree-taxon-label" }, line2));
|
|
1046
|
+
ty += 14;
|
|
1047
|
+
}
|
|
1048
|
+
return group({
|
|
1049
|
+
"data-node-id": n.id,
|
|
1050
|
+
"data-node-kind": n.kind,
|
|
1051
|
+
"data-leaf": isLeaf ? "true" : "false"
|
|
1052
|
+
}, parts);
|
|
1053
|
+
}
|
|
1054
|
+
function wrapText(text2, maxChars) {
|
|
1055
|
+
if (text2.length <= maxChars) return [text2];
|
|
1056
|
+
const words = text2.split(/\s+/);
|
|
1057
|
+
const lines = [];
|
|
1058
|
+
let cur = "";
|
|
1059
|
+
for (const w of words) {
|
|
1060
|
+
if (!cur) cur = w;
|
|
1061
|
+
else if ((cur + " " + w).length > maxChars) {
|
|
1062
|
+
lines.push(cur);
|
|
1063
|
+
cur = w;
|
|
1064
|
+
} else cur = cur + " " + w;
|
|
1065
|
+
if (lines.length >= 1 && cur.length >= maxChars) {
|
|
1066
|
+
lines.push(cur);
|
|
1067
|
+
cur = "";
|
|
1068
|
+
break;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
if (cur) lines.push(cur);
|
|
1072
|
+
return lines.slice(0, 2);
|
|
1073
|
+
}
|
|
1074
|
+
function renderDecisionTree(ast, config) {
|
|
1075
|
+
const t = resolveBaseTheme(config?.theme ?? "default");
|
|
1076
|
+
const layout = layoutDecisionTree(ast);
|
|
1077
|
+
const titleOffset = ast.title ? 36 : 10;
|
|
1078
|
+
const width = Math.ceil(layout.width);
|
|
1079
|
+
const height = Math.ceil(layout.height + titleOffset);
|
|
1080
|
+
const children = [];
|
|
1081
|
+
children.push(title(ast.title ?? "Decision Tree"));
|
|
1082
|
+
children.push(desc(`Decision tree (${ast.mode} mode) with ${layout.nodes.length} nodes and ${layout.edges.length} edges`));
|
|
1083
|
+
children.push(el("style", {}, buildCss(t)));
|
|
1084
|
+
if (ast.title) {
|
|
1085
|
+
children.push(text({ x: 20, y: 24, class: "lt-dtree-title" }, ast.title));
|
|
1086
|
+
}
|
|
1087
|
+
const inner = [];
|
|
1088
|
+
for (const e of layout.edges) {
|
|
1089
|
+
const cls = e.isOptimal ? "lt-dtree-edge-optimal" : "lt-dtree-edge";
|
|
1090
|
+
const attrs = { d: e.path, class: cls, "data-edge": `${e.from}->${e.to}` };
|
|
1091
|
+
if (e.strokeWidth !== void 0 && !e.isOptimal) attrs["stroke-width"] = e.strokeWidth;
|
|
1092
|
+
inner.push(path(attrs));
|
|
1093
|
+
}
|
|
1094
|
+
const anchors = layout.labelAnchors ?? {};
|
|
1095
|
+
for (const e of layout.edges) {
|
|
1096
|
+
if (!e.label) continue;
|
|
1097
|
+
const a = anchors[e.to];
|
|
1098
|
+
if (!a) continue;
|
|
1099
|
+
const isProb = ast.mode === "decision" && /^p=/i.test(e.label);
|
|
1100
|
+
const labelClass = isProb ? "lt-dtree-edge-prob" : "lt-dtree-edge-label";
|
|
1101
|
+
let lx = a.x;
|
|
1102
|
+
let ly = a.y;
|
|
1103
|
+
const absAngle = Math.abs(a.angle);
|
|
1104
|
+
const perpOffset = layout.edgeStyle === "diagonal" || layout.edgeStyle === "bracket" ? 9 : 0;
|
|
1105
|
+
if (perpOffset > 0 && absAngle > 1 && absAngle < 89) {
|
|
1106
|
+
const rad = a.angle * Math.PI / 180;
|
|
1107
|
+
const nx = -Math.sin(rad);
|
|
1108
|
+
const ny = Math.cos(rad);
|
|
1109
|
+
const flip = ny < 0 ? -1 : 1;
|
|
1110
|
+
lx += nx * perpOffset * flip;
|
|
1111
|
+
ly += ny * perpOffset * flip;
|
|
1112
|
+
}
|
|
1113
|
+
const charW = isProb ? 5.5 : 6.2;
|
|
1114
|
+
const w = Math.max(e.label.length * charW + 10, 18);
|
|
1115
|
+
const h = 14;
|
|
1116
|
+
inner.push(rect({
|
|
1117
|
+
x: lx - w / 2,
|
|
1118
|
+
y: ly - h / 2,
|
|
1119
|
+
width: w,
|
|
1120
|
+
height: h,
|
|
1121
|
+
class: "lt-dtree-edge-label-bg",
|
|
1122
|
+
rx: 3,
|
|
1123
|
+
ry: 3
|
|
1124
|
+
}));
|
|
1125
|
+
inner.push(text({ x: lx, y: ly, class: labelClass }, e.label));
|
|
1126
|
+
}
|
|
1127
|
+
for (const ln of layout.nodes) {
|
|
1128
|
+
if (ast.mode === "decision") inner.push(renderDecisionNode(ln, layout));
|
|
1129
|
+
else if (ast.mode === "ml") inner.push(renderMlNode(ln, ast));
|
|
1130
|
+
else inner.push(renderTaxonomyNode(ln));
|
|
1131
|
+
}
|
|
1132
|
+
children.push(group({ transform: `translate(0, ${titleOffset})`, "data-mode": ast.mode }, inner));
|
|
1133
|
+
return svgRoot({
|
|
1134
|
+
class: "lt-dtree",
|
|
1135
|
+
role: "img",
|
|
1136
|
+
"aria-label": escapeXml(ast.title ?? `Decision tree (${ast.mode})`),
|
|
1137
|
+
width,
|
|
1138
|
+
height,
|
|
1139
|
+
viewBox: `0 0 ${width} ${height}`
|
|
1140
|
+
}, children);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// src/diagrams/decisiontree/index.ts
|
|
1144
|
+
var decisiontree = {
|
|
1145
|
+
type: "decisiontree",
|
|
1146
|
+
detect(text2) {
|
|
1147
|
+
return /^\s*decisiontree\b/i.test(text2);
|
|
1148
|
+
},
|
|
1149
|
+
parse: parseDecisionTree,
|
|
1150
|
+
render(text2, config) {
|
|
1151
|
+
const ast = parseDecisionTree(text2);
|
|
1152
|
+
return renderDecisionTree(ast, config);
|
|
1153
|
+
}
|
|
1154
|
+
};
|
|
1155
|
+
|
|
1156
|
+
// src/diagrams/timeline/dates.ts
|
|
1157
|
+
function parseDate(raw) {
|
|
1158
|
+
const s = raw.trim();
|
|
1159
|
+
const mGeo = /^(-?\d+(?:\.\d+)?)\s*(Ma|Ga|ka)$/i.exec(s);
|
|
1160
|
+
if (mGeo) {
|
|
1161
|
+
const n = parseFloat(mGeo[1]);
|
|
1162
|
+
const unit = mGeo[2].toLowerCase();
|
|
1163
|
+
const mult = unit === "ga" ? 1e9 : unit === "ma" ? 1e6 : 1e3;
|
|
1164
|
+
return {
|
|
1165
|
+
value: 1970 - n * mult,
|
|
1166
|
+
raw: s,
|
|
1167
|
+
precision: "ma"
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
const mBC = /^(\d+)\s*(BC|BCE)$/i.exec(s);
|
|
1171
|
+
if (mBC) {
|
|
1172
|
+
return { value: -parseInt(mBC[1], 10), raw: s, precision: "year" };
|
|
1173
|
+
}
|
|
1174
|
+
const mIso = /^(-?\d{1,5})-(\d{1,2})-(\d{1,2})$/.exec(s);
|
|
1175
|
+
if (mIso) {
|
|
1176
|
+
const y = parseInt(mIso[1], 10);
|
|
1177
|
+
const mo = parseInt(mIso[2], 10);
|
|
1178
|
+
const d = parseInt(mIso[3], 10);
|
|
1179
|
+
return {
|
|
1180
|
+
value: y + (dayOfYear(y, mo, d) - 1) / daysInYear(y),
|
|
1181
|
+
raw: s,
|
|
1182
|
+
precision: "day"
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
const mYm = /^(-?\d{1,5})-(\d{1,2})$/.exec(s);
|
|
1186
|
+
if (mYm) {
|
|
1187
|
+
const y = parseInt(mYm[1], 10);
|
|
1188
|
+
const mo = parseInt(mYm[2], 10);
|
|
1189
|
+
return {
|
|
1190
|
+
value: y + (mo - 1) / 12,
|
|
1191
|
+
raw: s,
|
|
1192
|
+
precision: "month"
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
const mYq = /^(-?\d{1,5})-?Q([1-4])$/i.exec(s);
|
|
1196
|
+
if (mYq) {
|
|
1197
|
+
const y = parseInt(mYq[1], 10);
|
|
1198
|
+
const q = parseInt(mYq[2], 10);
|
|
1199
|
+
return { value: y + (q - 1) * 0.25, raw: s, precision: "month" };
|
|
1200
|
+
}
|
|
1201
|
+
const mY = /^(-?\d+)$/.exec(s);
|
|
1202
|
+
if (mY) {
|
|
1203
|
+
return { value: parseInt(mY[1], 10), raw: s, precision: "year" };
|
|
1204
|
+
}
|
|
1205
|
+
throw new Error(`Cannot parse date: "${raw}"`);
|
|
1206
|
+
}
|
|
1207
|
+
function daysInYear(y) {
|
|
1208
|
+
const leap = y % 4 === 0 && y % 100 !== 0 || y % 400 === 0;
|
|
1209
|
+
return leap ? 366 : 365;
|
|
1210
|
+
}
|
|
1211
|
+
function dayOfYear(y, m, d) {
|
|
1212
|
+
const dim = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
1213
|
+
if (y % 4 === 0 && y % 100 !== 0 || y % 400 === 0) dim[1] = 29;
|
|
1214
|
+
let total = d;
|
|
1215
|
+
for (let i = 0; i < m - 1; i++) total += dim[i];
|
|
1216
|
+
return total;
|
|
1217
|
+
}
|
|
1218
|
+
function formatYear(value, span) {
|
|
1219
|
+
const y = Math.round(value);
|
|
1220
|
+
if (span >= 2e6) return `${Math.round((1970 - value) / 1e6)} Ma`;
|
|
1221
|
+
if (span >= 2e3) return y < 0 ? `${-y} BC` : `${y}`;
|
|
1222
|
+
if (span >= 50) return y < 0 ? `${-y} BC` : `${y}`;
|
|
1223
|
+
return y < 0 ? `${-y} BC` : `${y}`;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// src/diagrams/timeline/parser.ts
|
|
1227
|
+
var TimelineParseError = class extends Error {
|
|
1228
|
+
constructor(message, line2) {
|
|
1229
|
+
super(line2 !== void 0 ? `Line ${line2}: ${message}` : message);
|
|
1230
|
+
this.line = line2;
|
|
1231
|
+
this.name = "TimelineParseError";
|
|
1232
|
+
}
|
|
1233
|
+
line;
|
|
1234
|
+
};
|
|
1235
|
+
function preprocess2(src) {
|
|
1236
|
+
const out = [];
|
|
1237
|
+
const lines = src.split(/\r?\n/);
|
|
1238
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1239
|
+
const raw = lines[i];
|
|
1240
|
+
if (raw === void 0) continue;
|
|
1241
|
+
const trimmed = raw.trim();
|
|
1242
|
+
if (!trimmed) continue;
|
|
1243
|
+
if (trimmed.startsWith("#") || trimmed.startsWith("//")) continue;
|
|
1244
|
+
const spaces = raw.length - raw.replace(/^\s+/, "").length;
|
|
1245
|
+
out.push({ indent: Math.floor(spaces / 2), text: trimmed, line: i + 1 });
|
|
1246
|
+
}
|
|
1247
|
+
return out;
|
|
1248
|
+
}
|
|
1249
|
+
function readQuoted(s, lineNum) {
|
|
1250
|
+
const t = s.trimStart();
|
|
1251
|
+
if (!t.startsWith('"')) {
|
|
1252
|
+
throw new TimelineParseError(`Expected quoted string, got: ${s}`, lineNum);
|
|
1253
|
+
}
|
|
1254
|
+
const end = t.indexOf('"', 1);
|
|
1255
|
+
if (end < 0) throw new TimelineParseError(`Unterminated string: ${s}`, lineNum);
|
|
1256
|
+
return [t.slice(1, end), t.slice(end + 1)];
|
|
1257
|
+
}
|
|
1258
|
+
function parseProperties(s, lineNum) {
|
|
1259
|
+
const t = s.trimEnd();
|
|
1260
|
+
if (!t.endsWith("]")) return { props: {}, rest: s };
|
|
1261
|
+
const open = t.lastIndexOf("[");
|
|
1262
|
+
if (open < 0) return { props: {}, rest: s };
|
|
1263
|
+
const inner = t.slice(open + 1, -1);
|
|
1264
|
+
const props = {};
|
|
1265
|
+
const parts = splitTopLevel(inner, ",");
|
|
1266
|
+
for (const p of parts) {
|
|
1267
|
+
const pt = p.trim();
|
|
1268
|
+
if (!pt) continue;
|
|
1269
|
+
const idx = pt.indexOf(":");
|
|
1270
|
+
if (idx < 0) {
|
|
1271
|
+
throw new TimelineParseError(`Invalid property (missing ':'): ${pt}`, lineNum);
|
|
1272
|
+
}
|
|
1273
|
+
const k = pt.slice(0, idx).trim();
|
|
1274
|
+
let v = pt.slice(idx + 1).trim();
|
|
1275
|
+
if (v.startsWith('"') && v.endsWith('"')) v = v.slice(1, -1);
|
|
1276
|
+
props[k] = v;
|
|
1277
|
+
}
|
|
1278
|
+
return { props, rest: t.slice(0, open).trimEnd() };
|
|
1279
|
+
}
|
|
1280
|
+
function splitTopLevel(s, sep) {
|
|
1281
|
+
const out = [];
|
|
1282
|
+
let depth = 0;
|
|
1283
|
+
let inQuote = false;
|
|
1284
|
+
let start = 0;
|
|
1285
|
+
for (let i = 0; i < s.length; i++) {
|
|
1286
|
+
const ch = s[i];
|
|
1287
|
+
if (ch === '"') inQuote = !inQuote;
|
|
1288
|
+
if (inQuote) continue;
|
|
1289
|
+
if (ch === "[" || ch === "(") depth++;
|
|
1290
|
+
else if (ch === "]" || ch === ")") depth--;
|
|
1291
|
+
else if (ch === sep && depth === 0) {
|
|
1292
|
+
out.push(s.slice(start, i));
|
|
1293
|
+
start = i + 1;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
out.push(s.slice(start));
|
|
1297
|
+
return out;
|
|
1298
|
+
}
|
|
1299
|
+
function splitDateAndBody(s, lineNum) {
|
|
1300
|
+
let inQuote = false;
|
|
1301
|
+
let colon = -1;
|
|
1302
|
+
for (let i = 0; i < s.length; i++) {
|
|
1303
|
+
const c = s[i];
|
|
1304
|
+
if (c === '"') inQuote = !inQuote;
|
|
1305
|
+
if (inQuote) continue;
|
|
1306
|
+
if (c === ":") {
|
|
1307
|
+
colon = i;
|
|
1308
|
+
break;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
if (colon < 0) throw new TimelineParseError(`Expected ':' after date: ${s}`, lineNum);
|
|
1312
|
+
const datePart = s.slice(0, colon).trim();
|
|
1313
|
+
const body = s.slice(colon + 1).trim();
|
|
1314
|
+
const dd = datePart.indexOf("..");
|
|
1315
|
+
if (dd > 0) {
|
|
1316
|
+
return {
|
|
1317
|
+
date: datePart.slice(0, dd).trim(),
|
|
1318
|
+
end: datePart.slice(dd + 2).trim(),
|
|
1319
|
+
body
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
const mDash = / - /.exec(datePart);
|
|
1323
|
+
if (mDash && mDash.index > 0) {
|
|
1324
|
+
return {
|
|
1325
|
+
date: datePart.slice(0, mDash.index).trim(),
|
|
1326
|
+
end: datePart.slice(mDash.index + 3).trim(),
|
|
1327
|
+
body
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
return { date: datePart, body };
|
|
1331
|
+
}
|
|
1332
|
+
function parseTimeline(src) {
|
|
1333
|
+
const lines = preprocess2(src);
|
|
1334
|
+
if (!lines.length) throw new TimelineParseError("Empty timeline");
|
|
1335
|
+
const ast = {
|
|
1336
|
+
type: "timeline",
|
|
1337
|
+
title: void 0,
|
|
1338
|
+
style: "swimlane",
|
|
1339
|
+
orientation: "horizontal",
|
|
1340
|
+
scale: "proportional",
|
|
1341
|
+
axis: "bottom",
|
|
1342
|
+
events: [],
|
|
1343
|
+
eras: [],
|
|
1344
|
+
tracks: []
|
|
1345
|
+
};
|
|
1346
|
+
let i = 0;
|
|
1347
|
+
let autoId = 0;
|
|
1348
|
+
const nextId2 = (prefix) => `${prefix}-${++autoId}`;
|
|
1349
|
+
const first = lines[0];
|
|
1350
|
+
if (/^timeline\b/i.test(first.text)) {
|
|
1351
|
+
const rest = first.text.replace(/^timeline\b/i, "").trim();
|
|
1352
|
+
if (rest) {
|
|
1353
|
+
if (rest.startsWith('"')) {
|
|
1354
|
+
const [title2] = readQuoted(rest, first.line);
|
|
1355
|
+
ast.title = title2;
|
|
1356
|
+
} else {
|
|
1357
|
+
ast.title = rest;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
i = 1;
|
|
1361
|
+
}
|
|
1362
|
+
while (i < lines.length) {
|
|
1363
|
+
const L = lines[i];
|
|
1364
|
+
const text2 = L.text;
|
|
1365
|
+
if (/^config\s*:/i.test(text2)) {
|
|
1366
|
+
const body = text2.replace(/^config\s*:\s*/i, "");
|
|
1367
|
+
const eq = body.indexOf("=");
|
|
1368
|
+
if (eq < 0) throw new TimelineParseError(`Expected 'key = value' in config: ${text2}`, L.line);
|
|
1369
|
+
const k = body.slice(0, eq).trim();
|
|
1370
|
+
const v = body.slice(eq + 1).trim();
|
|
1371
|
+
applyConfig(ast, k, v, L.line);
|
|
1372
|
+
i++;
|
|
1373
|
+
continue;
|
|
1374
|
+
}
|
|
1375
|
+
if (/^era\b/i.test(text2)) {
|
|
1376
|
+
const body = text2.replace(/^era\s+/i, "");
|
|
1377
|
+
const { props, rest } = parseProperties(body, L.line);
|
|
1378
|
+
const { date, end, body: labelPart } = splitDateAndBody(rest, L.line);
|
|
1379
|
+
if (!end) throw new TimelineParseError(`era requires a date range: ${text2}`, L.line);
|
|
1380
|
+
const [label] = readQuoted(labelPart, L.line);
|
|
1381
|
+
const era = {
|
|
1382
|
+
id: nextId2("era"),
|
|
1383
|
+
label,
|
|
1384
|
+
start: safeParseDate(date, L.line),
|
|
1385
|
+
end: safeParseDate(end, L.line),
|
|
1386
|
+
color: props["color"]
|
|
1387
|
+
};
|
|
1388
|
+
ast.eras.push(era);
|
|
1389
|
+
i++;
|
|
1390
|
+
continue;
|
|
1391
|
+
}
|
|
1392
|
+
if (/^track\b/i.test(text2)) {
|
|
1393
|
+
const body = text2.replace(/^track\s+/i, "");
|
|
1394
|
+
const [name, restAfter] = readQuoted(body, L.line);
|
|
1395
|
+
if (!restAfter.trim().startsWith(":")) {
|
|
1396
|
+
throw new TimelineParseError(`Expected ':' after track name`, L.line);
|
|
1397
|
+
}
|
|
1398
|
+
const trackId = nextId2("track");
|
|
1399
|
+
ast.tracks.push({ id: trackId, label: name });
|
|
1400
|
+
i++;
|
|
1401
|
+
while (i < lines.length && lines[i].indent > L.indent) {
|
|
1402
|
+
const child = lines[i];
|
|
1403
|
+
if (/^note\s*:/i.test(child.text)) {
|
|
1404
|
+
i++;
|
|
1405
|
+
continue;
|
|
1406
|
+
}
|
|
1407
|
+
const parsed2 = parseEventLine(child.text, child.line, nextId2);
|
|
1408
|
+
if (!parsed2) throw new TimelineParseError(`Unrecognized line in track: ${child.text}`, child.line);
|
|
1409
|
+
parsed2.event.trackId = trackId;
|
|
1410
|
+
ast.events.push(parsed2.event);
|
|
1411
|
+
i++;
|
|
1412
|
+
if (i < lines.length && /^note\s*:/i.test(lines[i].text) && lines[i].indent > child.indent) {
|
|
1413
|
+
const noteBody = lines[i].text.replace(/^note\s*:\s*/i, "");
|
|
1414
|
+
const [note] = readQuoted(noteBody, lines[i].line);
|
|
1415
|
+
parsed2.event.note = note;
|
|
1416
|
+
i++;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
continue;
|
|
1420
|
+
}
|
|
1421
|
+
const parsed = parseEventLine(text2, L.line, nextId2);
|
|
1422
|
+
if (parsed) {
|
|
1423
|
+
ast.events.push(parsed.event);
|
|
1424
|
+
i++;
|
|
1425
|
+
if (i < lines.length && /^note\s*:/i.test(lines[i].text) && lines[i].indent > L.indent) {
|
|
1426
|
+
const noteBody = lines[i].text.replace(/^note\s*:\s*/i, "");
|
|
1427
|
+
const [note] = readQuoted(noteBody, lines[i].line);
|
|
1428
|
+
parsed.event.note = note;
|
|
1429
|
+
i++;
|
|
1430
|
+
}
|
|
1431
|
+
continue;
|
|
1432
|
+
}
|
|
1433
|
+
throw new TimelineParseError(`Unrecognized line: ${text2}`, L.line);
|
|
1434
|
+
}
|
|
1435
|
+
return ast;
|
|
1436
|
+
}
|
|
1437
|
+
function safeParseDate(raw, line2) {
|
|
1438
|
+
try {
|
|
1439
|
+
return parseDate(raw);
|
|
1440
|
+
} catch (e) {
|
|
1441
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1442
|
+
throw new TimelineParseError(msg, line2);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
function applyConfig(ast, k, v, line2) {
|
|
1446
|
+
switch (k) {
|
|
1447
|
+
case "style": {
|
|
1448
|
+
const normalized = v === "gantt-project" ? "gantt" : v;
|
|
1449
|
+
if (normalized !== "swimlane" && normalized !== "gantt" && normalized !== "lollipop") {
|
|
1450
|
+
throw new TimelineParseError(`Invalid style: ${v}`, line2);
|
|
1451
|
+
}
|
|
1452
|
+
ast.style = normalized;
|
|
1453
|
+
break;
|
|
1454
|
+
}
|
|
1455
|
+
case "orientation":
|
|
1456
|
+
if (v !== "horizontal" && v !== "vertical") {
|
|
1457
|
+
throw new TimelineParseError(`Invalid orientation: ${v}`, line2);
|
|
1458
|
+
}
|
|
1459
|
+
ast.orientation = v;
|
|
1460
|
+
break;
|
|
1461
|
+
case "scale":
|
|
1462
|
+
if (v !== "proportional" && v !== "equidistant" && v !== "log") {
|
|
1463
|
+
throw new TimelineParseError(`Invalid scale: ${v}`, line2);
|
|
1464
|
+
}
|
|
1465
|
+
ast.scale = v;
|
|
1466
|
+
break;
|
|
1467
|
+
case "axis":
|
|
1468
|
+
if (v !== "bottom" && v !== "center") {
|
|
1469
|
+
throw new TimelineParseError(`Invalid axis: ${v}`, line2);
|
|
1470
|
+
}
|
|
1471
|
+
ast.axis = v;
|
|
1472
|
+
break;
|
|
1473
|
+
default:
|
|
1474
|
+
(ast.metadata ??= {})[k] = v;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
function parseEventLine(text2, line2, nextId2) {
|
|
1478
|
+
const { props, rest } = parseProperties(text2, line2);
|
|
1479
|
+
const { date, end, body } = splitDateAndBody(rest, line2);
|
|
1480
|
+
let kind = end ? "range" : "point";
|
|
1481
|
+
let bodyS = body.trim();
|
|
1482
|
+
if (/^milestone\b/i.test(bodyS)) {
|
|
1483
|
+
kind = "milestone";
|
|
1484
|
+
bodyS = bodyS.replace(/^milestone\s+/i, "");
|
|
1485
|
+
}
|
|
1486
|
+
if (!bodyS.startsWith('"')) return null;
|
|
1487
|
+
const [label] = readQuoted(bodyS, line2);
|
|
1488
|
+
const sideRaw = props["side"];
|
|
1489
|
+
const side = sideRaw === "above" || sideRaw === "below" ? sideRaw : void 0;
|
|
1490
|
+
const ev = {
|
|
1491
|
+
id: nextId2("ev"),
|
|
1492
|
+
label,
|
|
1493
|
+
kind,
|
|
1494
|
+
start: safeParseDate(date, line2),
|
|
1495
|
+
end: end ? safeParseDate(end, line2) : void 0,
|
|
1496
|
+
icon: props["icon"],
|
|
1497
|
+
shape: props["shape"],
|
|
1498
|
+
color: props["color"],
|
|
1499
|
+
category: props["category"],
|
|
1500
|
+
side
|
|
1501
|
+
};
|
|
1502
|
+
return { event: ev, hasNote: false };
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
// src/diagrams/timeline/layout.ts
|
|
1506
|
+
var CANVAS_WIDTH = 960;
|
|
1507
|
+
var PAD_LEFT_WITH_TRACKS = 140;
|
|
1508
|
+
var PAD_LEFT_NO_TRACKS = 40;
|
|
1509
|
+
var PAD_RIGHT = 40;
|
|
1510
|
+
var PAD_TOP_BASE = 40;
|
|
1511
|
+
var ERA_BAND_HEIGHT = 16;
|
|
1512
|
+
var AXIS_HEIGHT = 40;
|
|
1513
|
+
var LANE_HEIGHT_RANGE = 36;
|
|
1514
|
+
var LANE_HEIGHT_POINT = 28;
|
|
1515
|
+
var LANE_GAP = 8;
|
|
1516
|
+
var DEFAULT_CATEGORY_PALETTE = [
|
|
1517
|
+
"#2563eb",
|
|
1518
|
+
"#059669",
|
|
1519
|
+
"#d97706",
|
|
1520
|
+
"#7c3aed",
|
|
1521
|
+
"#dc2626",
|
|
1522
|
+
"#0891b2",
|
|
1523
|
+
"#db2777",
|
|
1524
|
+
"#475569"
|
|
1525
|
+
];
|
|
1526
|
+
function layoutTimeline(ast) {
|
|
1527
|
+
switch (ast.style) {
|
|
1528
|
+
case "gantt":
|
|
1529
|
+
return layoutGantt(ast);
|
|
1530
|
+
case "lollipop":
|
|
1531
|
+
return layoutLollipop(ast);
|
|
1532
|
+
case "swimlane":
|
|
1533
|
+
default:
|
|
1534
|
+
return layoutSwimlane(ast);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
function timeExtent(ast) {
|
|
1538
|
+
const starts = [
|
|
1539
|
+
...ast.events.map((e) => e.start.value),
|
|
1540
|
+
...ast.eras.map((e) => e.start.value)
|
|
1541
|
+
];
|
|
1542
|
+
const ends = [
|
|
1543
|
+
...ast.events.map((e) => e.end?.value ?? e.start.value),
|
|
1544
|
+
...ast.eras.map((e) => e.end.value)
|
|
1545
|
+
];
|
|
1546
|
+
if (!starts.length) return { min: 0, max: 1, span: 1 };
|
|
1547
|
+
const min = Math.min(...starts);
|
|
1548
|
+
const max = Math.max(...ends);
|
|
1549
|
+
const span = max - min || 1;
|
|
1550
|
+
return { min, max, span };
|
|
1551
|
+
}
|
|
1552
|
+
function buildScale(mode, events, tMin, tMax, plotX, plotW) {
|
|
1553
|
+
if (mode === "equidistant") {
|
|
1554
|
+
const sorted = events.slice().sort((a, b) => a.start.value - b.start.value);
|
|
1555
|
+
const n = sorted.length || 1;
|
|
1556
|
+
const indexByValue = /* @__PURE__ */ new Map();
|
|
1557
|
+
sorted.forEach((e, i) => indexByValue.set(e.start.value, i));
|
|
1558
|
+
return (v) => {
|
|
1559
|
+
const idx = indexByValue.get(v);
|
|
1560
|
+
if (idx !== void 0) {
|
|
1561
|
+
return plotX + (n === 1 ? plotW / 2 : idx / (n - 1) * plotW);
|
|
1562
|
+
}
|
|
1563
|
+
return plotX + (v - tMin) / (tMax - tMin) * plotW;
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
if (mode === "log") {
|
|
1567
|
+
const now = Math.max(tMax, 0);
|
|
1568
|
+
const toAgo = (v) => Math.max(1, now - v + 1);
|
|
1569
|
+
const minAgo = toAgo(tMax);
|
|
1570
|
+
const maxAgo = toAgo(tMin);
|
|
1571
|
+
const logMin = Math.log10(minAgo);
|
|
1572
|
+
const logMax = Math.log10(maxAgo);
|
|
1573
|
+
const range = logMax - logMin || 1;
|
|
1574
|
+
return (v) => {
|
|
1575
|
+
const frac = (logMax - Math.log10(toAgo(v))) / range;
|
|
1576
|
+
return plotX + frac * plotW;
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1579
|
+
const span = tMax - tMin || 1;
|
|
1580
|
+
return (v) => plotX + (v - tMin) / span * plotW;
|
|
1581
|
+
}
|
|
1582
|
+
function packEraRows(eras) {
|
|
1583
|
+
if (!eras.length) return { rows: 0, rowOf: [] };
|
|
1584
|
+
const rowsEnd = [];
|
|
1585
|
+
const rowOf = [];
|
|
1586
|
+
for (const e of eras) {
|
|
1587
|
+
let placed = -1;
|
|
1588
|
+
for (let i = 0; i < rowsEnd.length; i++) {
|
|
1589
|
+
if (rowsEnd[i] <= e.start.value) {
|
|
1590
|
+
placed = i;
|
|
1591
|
+
break;
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
if (placed < 0) {
|
|
1595
|
+
rowsEnd.push(e.end.value);
|
|
1596
|
+
placed = rowsEnd.length - 1;
|
|
1597
|
+
} else {
|
|
1598
|
+
rowsEnd[placed] = e.end.value;
|
|
1599
|
+
}
|
|
1600
|
+
rowOf.push(placed);
|
|
1601
|
+
}
|
|
1602
|
+
return { rows: rowsEnd.length, rowOf };
|
|
1603
|
+
}
|
|
1604
|
+
function generateTicks(tMin, tMax, span, xScale, mode) {
|
|
1605
|
+
const ticks = [];
|
|
1606
|
+
if (mode === "log") {
|
|
1607
|
+
const now = Math.max(tMax, 0);
|
|
1608
|
+
const minAgo = Math.max(1, now - tMax + 1);
|
|
1609
|
+
const maxAgo = Math.max(1, now - tMin + 1);
|
|
1610
|
+
const logMin = Math.floor(Math.log10(minAgo));
|
|
1611
|
+
const logMax = Math.ceil(Math.log10(maxAgo));
|
|
1612
|
+
for (let p = logMin; p <= logMax; p++) {
|
|
1613
|
+
const ago = Math.pow(10, p);
|
|
1614
|
+
const v = now - ago;
|
|
1615
|
+
if (v < tMin || v > tMax) continue;
|
|
1616
|
+
const label = ago >= 1e6 ? `${ago / 1e6} Ma` : ago >= 1e3 ? `${ago / 1e3} ka` : `${ago} yr`;
|
|
1617
|
+
ticks.push({ value: v, x: xScale(v), label, major: true });
|
|
1618
|
+
}
|
|
1619
|
+
return ticks;
|
|
1620
|
+
}
|
|
1621
|
+
const step = niceStep(span);
|
|
1622
|
+
const start = Math.ceil(tMin / step) * step;
|
|
1623
|
+
for (let v = start; v <= tMax; v += step) {
|
|
1624
|
+
ticks.push({ value: v, x: xScale(v), label: formatYear(v, span), major: true });
|
|
1625
|
+
}
|
|
1626
|
+
return ticks;
|
|
1627
|
+
}
|
|
1628
|
+
function niceStep(span) {
|
|
1629
|
+
const candidates = [
|
|
1630
|
+
0.01,
|
|
1631
|
+
0.02,
|
|
1632
|
+
0.05,
|
|
1633
|
+
0.1,
|
|
1634
|
+
0.25,
|
|
1635
|
+
0.5,
|
|
1636
|
+
1,
|
|
1637
|
+
2,
|
|
1638
|
+
5,
|
|
1639
|
+
10,
|
|
1640
|
+
20,
|
|
1641
|
+
25,
|
|
1642
|
+
50,
|
|
1643
|
+
100,
|
|
1644
|
+
200,
|
|
1645
|
+
500,
|
|
1646
|
+
1e3,
|
|
1647
|
+
2e3,
|
|
1648
|
+
5e3,
|
|
1649
|
+
1e4,
|
|
1650
|
+
5e4,
|
|
1651
|
+
1e5,
|
|
1652
|
+
5e5,
|
|
1653
|
+
1e6
|
|
1654
|
+
];
|
|
1655
|
+
for (const c of candidates) {
|
|
1656
|
+
if (span / c <= 10) return c;
|
|
1657
|
+
}
|
|
1658
|
+
return Math.pow(10, Math.ceil(Math.log10(span / 10)));
|
|
1659
|
+
}
|
|
1660
|
+
function estimateLabelWidth(label, icon) {
|
|
1661
|
+
const iconW = icon ? 14 : 0;
|
|
1662
|
+
return 6.5 * label.length + iconW + 4;
|
|
1663
|
+
}
|
|
1664
|
+
function layoutSwimlane(ast) {
|
|
1665
|
+
const events = ast.events.slice().sort((a, b) => a.start.value - b.start.value);
|
|
1666
|
+
const allTracks = resolveTracks(ast, events);
|
|
1667
|
+
assignAutoTracks(events, allTracks);
|
|
1668
|
+
const { min: startVal, max: endVal, span } = timeExtent(ast);
|
|
1669
|
+
const paddedStart = startVal - span * 0.02;
|
|
1670
|
+
const paddedEnd = endVal + span * 0.02;
|
|
1671
|
+
const hasNamedTracks = ast.tracks.length > 0;
|
|
1672
|
+
const plotX = hasNamedTracks ? PAD_LEFT_WITH_TRACKS : PAD_LEFT_NO_TRACKS;
|
|
1673
|
+
const plotRight = CANVAS_WIDTH - PAD_RIGHT;
|
|
1674
|
+
const plotW = plotRight - plotX;
|
|
1675
|
+
const xScale = buildScale(ast.scale, events, paddedStart, paddedEnd, plotX, plotW);
|
|
1676
|
+
const eraRows = packEraRows(ast.eras);
|
|
1677
|
+
const eraBandTotal = eraRows.rows * (ERA_BAND_HEIGHT + 2);
|
|
1678
|
+
const titleOffset = ast.title ? 28 : 0;
|
|
1679
|
+
const plotY = PAD_TOP_BASE + titleOffset + eraBandTotal;
|
|
1680
|
+
const lanes = [];
|
|
1681
|
+
let cursorY = plotY + 8;
|
|
1682
|
+
for (const t of allTracks) {
|
|
1683
|
+
const hasRange = events.some((e) => e.trackId === t.id && e.kind === "range");
|
|
1684
|
+
const h = hasRange ? LANE_HEIGHT_RANGE : LANE_HEIGHT_POINT;
|
|
1685
|
+
lanes.push({ trackId: t.id, label: t.label, y: cursorY, height: h });
|
|
1686
|
+
cursorY += h + LANE_GAP;
|
|
1687
|
+
}
|
|
1688
|
+
const plotH = cursorY - plotY;
|
|
1689
|
+
const axisY = cursorY + 4;
|
|
1690
|
+
const height = axisY + AXIS_HEIGHT + 20;
|
|
1691
|
+
const eraLayouts = ast.eras.map((e, idx) => ({
|
|
1692
|
+
era: e,
|
|
1693
|
+
x: xScale(e.start.value),
|
|
1694
|
+
width: Math.max(2, xScale(e.end.value) - xScale(e.start.value)),
|
|
1695
|
+
bandRow: eraRows.rowOf[idx]
|
|
1696
|
+
}));
|
|
1697
|
+
const laneByTrack = new Map(lanes.map((l) => [l.trackId, l]));
|
|
1698
|
+
const eventLayouts = [];
|
|
1699
|
+
const labelBoxes = [];
|
|
1700
|
+
for (const ev of events) {
|
|
1701
|
+
const lane = laneByTrack.get(ev.trackId);
|
|
1702
|
+
if (!lane) continue;
|
|
1703
|
+
if (ev.kind === "range") {
|
|
1704
|
+
const x = xScale(ev.start.value);
|
|
1705
|
+
const xe = xScale(ev.end.value);
|
|
1706
|
+
const w = Math.max(4, xe - x);
|
|
1707
|
+
eventLayouts.push({
|
|
1708
|
+
event: ev,
|
|
1709
|
+
x,
|
|
1710
|
+
w,
|
|
1711
|
+
y: lane.y + 6,
|
|
1712
|
+
h: lane.height - 12,
|
|
1713
|
+
labelX: x + w / 2,
|
|
1714
|
+
labelY: lane.y + lane.height / 2 + 4,
|
|
1715
|
+
labelAnchor: "middle"
|
|
1716
|
+
});
|
|
1717
|
+
} else {
|
|
1718
|
+
const x = xScale(ev.start.value);
|
|
1719
|
+
const cy = lane.y + lane.height / 2;
|
|
1720
|
+
const labelW = estimateLabelWidth(ev.label, ev.icon);
|
|
1721
|
+
const candidates = [
|
|
1722
|
+
cy - 14,
|
|
1723
|
+
cy + 18,
|
|
1724
|
+
cy - 28,
|
|
1725
|
+
cy + 32,
|
|
1726
|
+
cy - 42,
|
|
1727
|
+
cy + 46,
|
|
1728
|
+
cy - 56,
|
|
1729
|
+
cy + 60
|
|
1730
|
+
];
|
|
1731
|
+
let labelY = candidates[0];
|
|
1732
|
+
for (const y of candidates) {
|
|
1733
|
+
const box = { x1: x - labelW / 2, x2: x + labelW / 2, y };
|
|
1734
|
+
const collide = labelBoxes.some((b) => Math.abs(b.y - box.y) < 13 && b.x1 < box.x2 && b.x2 > box.x1);
|
|
1735
|
+
if (!collide) {
|
|
1736
|
+
labelY = y;
|
|
1737
|
+
labelBoxes.push(box);
|
|
1738
|
+
break;
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
eventLayouts.push({
|
|
1742
|
+
event: ev,
|
|
1743
|
+
x,
|
|
1744
|
+
y: cy,
|
|
1745
|
+
h: LANE_HEIGHT_POINT,
|
|
1746
|
+
labelX: x,
|
|
1747
|
+
labelY,
|
|
1748
|
+
labelAnchor: "middle",
|
|
1749
|
+
noteX: ev.note ? x + 12 : void 0,
|
|
1750
|
+
noteY: ev.note ? cy + 16 : void 0
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
return {
|
|
1755
|
+
width: CANVAS_WIDTH,
|
|
1756
|
+
height,
|
|
1757
|
+
style: "swimlane",
|
|
1758
|
+
plotX,
|
|
1759
|
+
plotY,
|
|
1760
|
+
plotW,
|
|
1761
|
+
plotH,
|
|
1762
|
+
lanes,
|
|
1763
|
+
events: eventLayouts,
|
|
1764
|
+
eras: eraLayouts,
|
|
1765
|
+
ticks: generateTicks(paddedStart, paddedEnd, span, xScale, ast.scale).filter((t) => t.x >= plotX - 2 && t.x <= plotX + plotW + 2),
|
|
1766
|
+
axisY,
|
|
1767
|
+
title: ast.title
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
function resolveTracks(ast, events) {
|
|
1771
|
+
const namedIds = new Set(ast.tracks.map((t) => t.id));
|
|
1772
|
+
const tracks = ast.tracks.slice();
|
|
1773
|
+
for (const ev of events) {
|
|
1774
|
+
if (ev.trackId && !namedIds.has(ev.trackId)) {
|
|
1775
|
+
ev.trackId = void 0;
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
return tracks;
|
|
1779
|
+
}
|
|
1780
|
+
function assignAutoTracks(events, tracks) {
|
|
1781
|
+
const unassigned = events.filter((e) => !e.trackId);
|
|
1782
|
+
if (unassigned.length === 0) return;
|
|
1783
|
+
const pointsOnly = unassigned.every((e) => e.kind !== "range");
|
|
1784
|
+
if (pointsOnly) {
|
|
1785
|
+
let lane = tracks.find((t) => t.id === "__auto_points__");
|
|
1786
|
+
if (!lane) {
|
|
1787
|
+
lane = { id: "__auto_points__", label: "" };
|
|
1788
|
+
tracks.push(lane);
|
|
1789
|
+
}
|
|
1790
|
+
for (const ev of unassigned) ev.trackId = lane.id;
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
const packed = [];
|
|
1794
|
+
const pointLane = { id: "__auto_points__", label: "" };
|
|
1795
|
+
const pointEvents = [];
|
|
1796
|
+
for (const ev of unassigned) {
|
|
1797
|
+
if (ev.kind !== "range") {
|
|
1798
|
+
pointEvents.push(ev);
|
|
1799
|
+
continue;
|
|
1800
|
+
}
|
|
1801
|
+
let placed = false;
|
|
1802
|
+
for (const lane of packed) {
|
|
1803
|
+
if (ev.start.value >= lane.endX) {
|
|
1804
|
+
ev.trackId = lane.id;
|
|
1805
|
+
lane.endX = ev.end.value;
|
|
1806
|
+
placed = true;
|
|
1807
|
+
break;
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
if (!placed) {
|
|
1811
|
+
const id = `__auto_${packed.length}__`;
|
|
1812
|
+
tracks.push({ id, label: "" });
|
|
1813
|
+
packed.push({ id, endX: ev.end.value });
|
|
1814
|
+
ev.trackId = id;
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
if (pointEvents.length) {
|
|
1818
|
+
tracks.push(pointLane);
|
|
1819
|
+
for (const ev of pointEvents) ev.trackId = pointLane.id;
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
var GANTT_PIN_ZONE = 70;
|
|
1823
|
+
var GANTT_LANE_H = 30;
|
|
1824
|
+
var GANTT_LEGEND_W = 140;
|
|
1825
|
+
function layoutGantt(ast) {
|
|
1826
|
+
const events = ast.events.slice().sort((a, b) => a.start.value - b.start.value);
|
|
1827
|
+
const { min: startVal, max: endVal, span } = timeExtent(ast);
|
|
1828
|
+
const padSpan = span * 0.03;
|
|
1829
|
+
const paddedStart = startVal - padSpan;
|
|
1830
|
+
const paddedEnd = endVal + padSpan;
|
|
1831
|
+
const categories = [];
|
|
1832
|
+
const catSeen = /* @__PURE__ */ new Set();
|
|
1833
|
+
for (const ev of events) {
|
|
1834
|
+
const c = ev.category ?? (ev.kind === "range" ? ast.tracks.find((t) => t.id === ev.trackId)?.label : void 0);
|
|
1835
|
+
if (c && !catSeen.has(c)) {
|
|
1836
|
+
catSeen.add(c);
|
|
1837
|
+
categories.push(c);
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
if (!categories.length && events.some((e) => e.kind === "range")) {
|
|
1841
|
+
categories.push("Tasks");
|
|
1842
|
+
}
|
|
1843
|
+
const hasLegend = categories.length > 0;
|
|
1844
|
+
const plotX = hasLegend ? PAD_LEFT_WITH_TRACKS - 20 : PAD_LEFT_NO_TRACKS;
|
|
1845
|
+
const plotRight = CANVAS_WIDTH - (hasLegend ? GANTT_LEGEND_W + 20 : PAD_RIGHT);
|
|
1846
|
+
const plotW = plotRight - plotX;
|
|
1847
|
+
const xScale = buildScale(ast.scale, events, paddedStart, paddedEnd, plotX, plotW);
|
|
1848
|
+
const eraRows = packEraRows(ast.eras);
|
|
1849
|
+
const eraBandTotal = eraRows.rows * (ERA_BAND_HEIGHT + 2);
|
|
1850
|
+
const titleOffset = ast.title ? 28 : 0;
|
|
1851
|
+
const pinZoneTop = PAD_TOP_BASE + titleOffset + eraBandTotal;
|
|
1852
|
+
const pinZoneBottom = pinZoneTop + GANTT_PIN_ZONE;
|
|
1853
|
+
const axisY = pinZoneBottom;
|
|
1854
|
+
const lanes = categories.map((c, i) => ({
|
|
1855
|
+
trackId: `__cat_${i}__`,
|
|
1856
|
+
label: c,
|
|
1857
|
+
y: axisY + 14 + i * GANTT_LANE_H,
|
|
1858
|
+
height: GANTT_LANE_H - 8
|
|
1859
|
+
}));
|
|
1860
|
+
const plotY = pinZoneTop;
|
|
1861
|
+
const plotH = lanes.length ? lanes[lanes.length - 1].y + lanes[lanes.length - 1].height - plotY : GANTT_PIN_ZONE;
|
|
1862
|
+
const height = lanes.length ? lanes[lanes.length - 1].y + lanes[lanes.length - 1].height + 40 : axisY + 60;
|
|
1863
|
+
const palette = DEFAULT_CATEGORY_PALETTE;
|
|
1864
|
+
const colorFor = (cat) => {
|
|
1865
|
+
if (!cat) return palette[0];
|
|
1866
|
+
const idx = categories.indexOf(cat);
|
|
1867
|
+
return palette[(idx < 0 ? 0 : idx) % palette.length];
|
|
1868
|
+
};
|
|
1869
|
+
const laneByCat = new Map(lanes.map((l) => [l.label, l]));
|
|
1870
|
+
const pins = [];
|
|
1871
|
+
const eventLayouts = [];
|
|
1872
|
+
const pinBoxes = [];
|
|
1873
|
+
for (const ev of events) {
|
|
1874
|
+
if (ev.kind === "range") {
|
|
1875
|
+
const cat = ev.category ?? "Tasks";
|
|
1876
|
+
const lane = laneByCat.get(cat) ?? lanes[0];
|
|
1877
|
+
if (!lane) continue;
|
|
1878
|
+
const x2 = xScale(ev.start.value);
|
|
1879
|
+
const xe = xScale(ev.end.value);
|
|
1880
|
+
const w = Math.max(4, xe - x2);
|
|
1881
|
+
eventLayouts.push({
|
|
1882
|
+
event: ev,
|
|
1883
|
+
x: x2,
|
|
1884
|
+
w,
|
|
1885
|
+
y: lane.y + 4,
|
|
1886
|
+
h: lane.height - 8,
|
|
1887
|
+
labelX: x2 + w / 2,
|
|
1888
|
+
labelY: lane.y + lane.height / 2 + 4,
|
|
1889
|
+
labelAnchor: "middle"
|
|
1890
|
+
});
|
|
1891
|
+
continue;
|
|
1892
|
+
}
|
|
1893
|
+
const x = xScale(ev.start.value);
|
|
1894
|
+
const labelW = estimateLabelWidth(ev.label, ev.icon);
|
|
1895
|
+
let labelY = pinZoneTop + 12;
|
|
1896
|
+
let step = 0;
|
|
1897
|
+
while (step < 4) {
|
|
1898
|
+
const box = { x1: x - labelW / 2, x2: x + labelW / 2, y: labelY };
|
|
1899
|
+
const collide = pinBoxes.some((b) => Math.abs(b.y - box.y) < 14 && b.x1 < box.x2 && b.x2 > box.x1);
|
|
1900
|
+
if (!collide) {
|
|
1901
|
+
pinBoxes.push(box);
|
|
1902
|
+
break;
|
|
1903
|
+
}
|
|
1904
|
+
labelY += 16;
|
|
1905
|
+
step++;
|
|
1906
|
+
}
|
|
1907
|
+
pins.push({
|
|
1908
|
+
event: ev,
|
|
1909
|
+
x,
|
|
1910
|
+
labelY,
|
|
1911
|
+
axisY,
|
|
1912
|
+
color: colorFor(ev.category)
|
|
1913
|
+
});
|
|
1914
|
+
}
|
|
1915
|
+
const eraLayouts = ast.eras.map((e, idx) => ({
|
|
1916
|
+
era: e,
|
|
1917
|
+
x: xScale(e.start.value),
|
|
1918
|
+
width: Math.max(2, xScale(e.end.value) - xScale(e.start.value)),
|
|
1919
|
+
bandRow: eraRows.rowOf[idx]
|
|
1920
|
+
}));
|
|
1921
|
+
const legend = categories.map((c) => ({ label: c, color: colorFor(c) }));
|
|
1922
|
+
return {
|
|
1923
|
+
width: CANVAS_WIDTH,
|
|
1924
|
+
height,
|
|
1925
|
+
style: "gantt",
|
|
1926
|
+
plotX,
|
|
1927
|
+
plotY,
|
|
1928
|
+
plotW,
|
|
1929
|
+
plotH,
|
|
1930
|
+
lanes,
|
|
1931
|
+
events: eventLayouts,
|
|
1932
|
+
eras: eraLayouts,
|
|
1933
|
+
ticks: generateTicks(paddedStart, paddedEnd, span, xScale, ast.scale).filter((t) => t.x >= plotX - 2 && t.x <= plotX + plotW + 2),
|
|
1934
|
+
axisY,
|
|
1935
|
+
title: ast.title,
|
|
1936
|
+
pins,
|
|
1937
|
+
legend: hasLegend ? legend : void 0
|
|
1938
|
+
};
|
|
1939
|
+
}
|
|
1940
|
+
var LOLLIPOP_CARD_W = 180;
|
|
1941
|
+
var LOLLIPOP_CARD_H = 54;
|
|
1942
|
+
var LOLLIPOP_STEM_BASE = 26;
|
|
1943
|
+
var LOLLIPOP_STEM_STACK = 18;
|
|
1944
|
+
function layoutLollipop(ast) {
|
|
1945
|
+
const events = ast.events.slice().sort((a, b) => a.start.value - b.start.value);
|
|
1946
|
+
const eStarts = events.map((e) => e.start.value);
|
|
1947
|
+
const eEnds = events.map((e) => e.end?.value ?? e.start.value);
|
|
1948
|
+
const eStart = eStarts.length ? Math.min(...eStarts) : 0;
|
|
1949
|
+
const eEnd = eEnds.length ? Math.max(...eEnds) : 1;
|
|
1950
|
+
const span = Math.max(1e-9, eEnd - eStart);
|
|
1951
|
+
const padSpan = span * 0.02;
|
|
1952
|
+
const paddedStart = eStart - padSpan;
|
|
1953
|
+
const paddedEnd = eEnd + padSpan;
|
|
1954
|
+
const plotX = PAD_LEFT_NO_TRACKS + 20;
|
|
1955
|
+
const plotRight = CANVAS_WIDTH - PAD_RIGHT - 20;
|
|
1956
|
+
const plotW = plotRight - plotX;
|
|
1957
|
+
const CARD_HALF = LOLLIPOP_CARD_W / 2;
|
|
1958
|
+
const axisInset = CARD_HALF + 8;
|
|
1959
|
+
const axisStart = plotX + axisInset;
|
|
1960
|
+
const axisEnd = plotRight - axisInset;
|
|
1961
|
+
const axisW = Math.max(1, axisEnd - axisStart);
|
|
1962
|
+
const rawScale = buildScale(ast.scale, events, paddedStart, paddedEnd, axisStart, axisW);
|
|
1963
|
+
const xScale = (v) => Math.max(plotX, Math.min(plotRight, rawScale(v)));
|
|
1964
|
+
const eraRows = packEraRows(ast.eras);
|
|
1965
|
+
const eraBandTotal = eraRows.rows * (ERA_BAND_HEIGHT + 2);
|
|
1966
|
+
const titleOffset = ast.title ? 28 : 0;
|
|
1967
|
+
const plotY = PAD_TOP_BASE + titleOffset + eraBandTotal;
|
|
1968
|
+
const cards = [];
|
|
1969
|
+
let alt = "above";
|
|
1970
|
+
const palette = DEFAULT_CATEGORY_PALETTE;
|
|
1971
|
+
const aboveRows = [];
|
|
1972
|
+
const belowRows = [];
|
|
1973
|
+
for (let i = 0; i < events.length; i++) {
|
|
1974
|
+
const ev = events[i];
|
|
1975
|
+
if (ev.kind === "range") continue;
|
|
1976
|
+
const side = ev.side ?? alt;
|
|
1977
|
+
if (!ev.side) alt = alt === "above" ? "below" : "above";
|
|
1978
|
+
const x = xScale(ev.start.value);
|
|
1979
|
+
const cardX = x - LOLLIPOP_CARD_W / 2;
|
|
1980
|
+
const cardRight = cardX + LOLLIPOP_CARD_W;
|
|
1981
|
+
const rows = side === "above" ? aboveRows : belowRows;
|
|
1982
|
+
let row = 0;
|
|
1983
|
+
for (; row < rows.length; row++) {
|
|
1984
|
+
if (rows[row] <= cardX + 2) {
|
|
1985
|
+
rows[row] = cardRight;
|
|
1986
|
+
break;
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
if (row === rows.length) {
|
|
1990
|
+
rows.push(cardRight);
|
|
1991
|
+
}
|
|
1992
|
+
const stemLen = LOLLIPOP_STEM_BASE + row * LOLLIPOP_STEM_STACK;
|
|
1993
|
+
cards.push({
|
|
1994
|
+
event: ev,
|
|
1995
|
+
x,
|
|
1996
|
+
axisY: 0,
|
|
1997
|
+
// filled after we know axisY
|
|
1998
|
+
side,
|
|
1999
|
+
cardX,
|
|
2000
|
+
cardY: 0,
|
|
2001
|
+
cardW: LOLLIPOP_CARD_W,
|
|
2002
|
+
cardH: LOLLIPOP_CARD_H,
|
|
2003
|
+
stemY1: 0,
|
|
2004
|
+
stemY2: 0,
|
|
2005
|
+
color: ev.color ?? palette[i % palette.length],
|
|
2006
|
+
index: row
|
|
2007
|
+
});
|
|
2008
|
+
cards[cards.length - 1]._stemLen = stemLen;
|
|
2009
|
+
}
|
|
2010
|
+
const maxAboveStems = aboveRows.length;
|
|
2011
|
+
const maxBelowStems = belowRows.length;
|
|
2012
|
+
const topExtent = LOLLIPOP_CARD_H + LOLLIPOP_STEM_BASE + Math.max(0, maxAboveStems - 1) * LOLLIPOP_STEM_STACK;
|
|
2013
|
+
const realAxisY = plotY + topExtent + 10;
|
|
2014
|
+
const bottomExtent = LOLLIPOP_CARD_H + LOLLIPOP_STEM_BASE + Math.max(0, maxBelowStems - 1) * LOLLIPOP_STEM_STACK;
|
|
2015
|
+
const height = realAxisY + bottomExtent + 40;
|
|
2016
|
+
for (const c of cards) {
|
|
2017
|
+
const stemLen = c._stemLen ?? LOLLIPOP_STEM_BASE;
|
|
2018
|
+
c.axisY = realAxisY;
|
|
2019
|
+
if (c.side === "above") {
|
|
2020
|
+
c.stemY1 = realAxisY - 3;
|
|
2021
|
+
c.stemY2 = realAxisY - stemLen;
|
|
2022
|
+
c.cardY = c.stemY2 - c.cardH;
|
|
2023
|
+
} else {
|
|
2024
|
+
c.stemY1 = realAxisY + 3;
|
|
2025
|
+
c.stemY2 = realAxisY + stemLen;
|
|
2026
|
+
c.cardY = c.stemY2;
|
|
2027
|
+
}
|
|
2028
|
+
delete c._stemLen;
|
|
2029
|
+
}
|
|
2030
|
+
const eventLayouts = [];
|
|
2031
|
+
for (const ev of events) {
|
|
2032
|
+
if (ev.kind !== "range") continue;
|
|
2033
|
+
const x = xScale(ev.start.value);
|
|
2034
|
+
const xe = xScale(ev.end.value);
|
|
2035
|
+
const w = Math.max(4, xe - x);
|
|
2036
|
+
eventLayouts.push({
|
|
2037
|
+
event: ev,
|
|
2038
|
+
x,
|
|
2039
|
+
w,
|
|
2040
|
+
y: realAxisY - 5,
|
|
2041
|
+
h: 10,
|
|
2042
|
+
labelX: x + w / 2,
|
|
2043
|
+
labelY: realAxisY - 10,
|
|
2044
|
+
labelAnchor: "middle"
|
|
2045
|
+
});
|
|
2046
|
+
}
|
|
2047
|
+
const eraLayouts = ast.eras.map((e, idx) => ({
|
|
2048
|
+
era: e,
|
|
2049
|
+
x: xScale(e.start.value),
|
|
2050
|
+
width: Math.max(2, xScale(e.end.value) - xScale(e.start.value)),
|
|
2051
|
+
bandRow: eraRows.rowOf[idx]
|
|
2052
|
+
}));
|
|
2053
|
+
const leftmostIdxByRow = /* @__PURE__ */ new Map();
|
|
2054
|
+
const rightmostIdxByRow = /* @__PURE__ */ new Map();
|
|
2055
|
+
eraLayouts.forEach((e, i) => {
|
|
2056
|
+
const curL = leftmostIdxByRow.get(e.bandRow);
|
|
2057
|
+
if (curL === void 0 || e.x < eraLayouts[curL].x) leftmostIdxByRow.set(e.bandRow, i);
|
|
2058
|
+
const curR = rightmostIdxByRow.get(e.bandRow);
|
|
2059
|
+
const right = e.x + e.width;
|
|
2060
|
+
if (curR === void 0 || right > eraLayouts[curR].x + eraLayouts[curR].width) {
|
|
2061
|
+
rightmostIdxByRow.set(e.bandRow, i);
|
|
2062
|
+
}
|
|
2063
|
+
});
|
|
2064
|
+
for (const [, i] of leftmostIdxByRow) {
|
|
2065
|
+
const e = eraLayouts[i];
|
|
2066
|
+
const right = e.x + e.width;
|
|
2067
|
+
e.x = Math.min(e.x, plotX);
|
|
2068
|
+
e.width = right - e.x;
|
|
2069
|
+
}
|
|
2070
|
+
for (const [, i] of rightmostIdxByRow) {
|
|
2071
|
+
const e = eraLayouts[i];
|
|
2072
|
+
e.width = Math.max(e.width, plotRight - e.x);
|
|
2073
|
+
}
|
|
2074
|
+
return {
|
|
2075
|
+
width: CANVAS_WIDTH,
|
|
2076
|
+
height,
|
|
2077
|
+
style: "lollipop",
|
|
2078
|
+
plotX,
|
|
2079
|
+
plotY,
|
|
2080
|
+
plotW,
|
|
2081
|
+
plotH: height - plotY - 20,
|
|
2082
|
+
lanes: [],
|
|
2083
|
+
events: eventLayouts,
|
|
2084
|
+
eras: eraLayouts,
|
|
2085
|
+
ticks: generateTicks(paddedStart, paddedEnd, span, xScale, ast.scale).filter((t) => t.x >= plotX - 2 && t.x <= plotX + plotW + 2),
|
|
2086
|
+
axisY: realAxisY,
|
|
2087
|
+
title: ast.title,
|
|
2088
|
+
cards
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
// src/diagrams/timeline/renderer.ts
|
|
2093
|
+
var ERA_BAND_HEIGHT2 = 16;
|
|
2094
|
+
var ERA_ROW_GAP = 2;
|
|
2095
|
+
function renderTimeline(src, config) {
|
|
2096
|
+
const ast = parseTimeline(src);
|
|
2097
|
+
const layout = layoutTimeline(ast);
|
|
2098
|
+
const theme = resolveTimelineTheme(config?.theme ?? "default");
|
|
2099
|
+
const styleBlock = styleForTheme(theme, config?.fontFamily);
|
|
2100
|
+
const children = [
|
|
2101
|
+
title(ast.title ? `Timeline \u2014 ${ast.title}` : "Timeline"),
|
|
2102
|
+
desc("Schematex timeline diagram"),
|
|
2103
|
+
defs([`<style>${styleBlock}</style>`])
|
|
2104
|
+
];
|
|
2105
|
+
if (ast.title) {
|
|
2106
|
+
children.push(
|
|
2107
|
+
text(
|
|
2108
|
+
{ x: layout.width / 2, y: 26, "text-anchor": "middle", class: "st-title" },
|
|
2109
|
+
ast.title
|
|
2110
|
+
)
|
|
2111
|
+
);
|
|
2112
|
+
}
|
|
2113
|
+
children.push(renderEras(layout, theme));
|
|
2114
|
+
switch (layout.style) {
|
|
2115
|
+
case "gantt":
|
|
2116
|
+
children.push(renderGantt(layout, theme));
|
|
2117
|
+
break;
|
|
2118
|
+
case "lollipop":
|
|
2119
|
+
children.push(renderLollipop(layout, theme));
|
|
2120
|
+
break;
|
|
2121
|
+
case "swimlane":
|
|
2122
|
+
default:
|
|
2123
|
+
children.push(renderSwimlane(layout, theme));
|
|
2124
|
+
break;
|
|
2125
|
+
}
|
|
2126
|
+
children.push(renderAxis(layout));
|
|
2127
|
+
return svgRoot(
|
|
2128
|
+
{
|
|
2129
|
+
viewBox: `0 0 ${layout.width} ${layout.height}`,
|
|
2130
|
+
width: layout.width,
|
|
2131
|
+
height: layout.height,
|
|
2132
|
+
class: `st-timeline st-timeline-${layout.style}`,
|
|
2133
|
+
"data-diagram-type": "timeline",
|
|
2134
|
+
"data-style": layout.style,
|
|
2135
|
+
role: "graphics-document"
|
|
2136
|
+
},
|
|
2137
|
+
children
|
|
2138
|
+
);
|
|
2139
|
+
}
|
|
2140
|
+
function styleForTheme(theme, fontFamily) {
|
|
2141
|
+
const font = fontFamily ?? "system-ui, -apple-system, sans-serif";
|
|
2142
|
+
return `
|
|
2143
|
+
.st-timeline { ${cssCustomProperties(theme)}
|
|
2144
|
+
--st-axis: ${theme.axis};
|
|
2145
|
+
--st-axis-label: ${theme.axisLabel};
|
|
2146
|
+
--st-era-label: ${theme.eraLabel};
|
|
2147
|
+
--st-lane-stripe: ${theme.laneStripe};
|
|
2148
|
+
--st-marker-ring: ${theme.markerRing};
|
|
2149
|
+
--st-marker-fill: ${theme.markerFill};
|
|
2150
|
+
--st-milestone-fill: ${theme.milestoneFill};
|
|
2151
|
+
--st-pin-shaft: ${theme.pinShaft};
|
|
2152
|
+
--st-card-bg: ${theme.cardBg};
|
|
2153
|
+
--st-card-stroke: ${theme.cardStroke};
|
|
2154
|
+
--st-card-text: ${theme.cardText};
|
|
2155
|
+
--st-legend-bg: ${theme.legendBg};
|
|
2156
|
+
--st-legend-stroke: ${theme.legendStroke};
|
|
2157
|
+
font-family: ${font}; background: var(--schematex-bg); }
|
|
2158
|
+
.st-title { font-size: 16px; font-weight: 600; fill: var(--schematex-text); }
|
|
2159
|
+
.st-axis-line { stroke: var(--st-axis); stroke-width: 1.5; fill: none; }
|
|
2160
|
+
.st-axis-tick { stroke: var(--st-axis); stroke-width: 1; opacity: 0.55; }
|
|
2161
|
+
.st-axis-label { font-size: 11px; fill: var(--st-axis-label); }
|
|
2162
|
+
.st-era-rect { opacity: ${theme.eraOpacity}; }
|
|
2163
|
+
.st-era-strip { opacity: ${theme.eraPlotOpacity}; }
|
|
2164
|
+
.st-era-label { font-size: 11px; font-weight: 500; fill: var(--st-era-label); }
|
|
2165
|
+
.st-event-dot { stroke: var(--st-marker-fill); stroke-width: 1.5; }
|
|
2166
|
+
.st-event-label { font-size: 12px; fill: var(--schematex-text); }
|
|
2167
|
+
.st-range-bar { opacity: 0.88; }
|
|
2168
|
+
.st-range-label { font-size: 11px; fill: #fff; font-weight: 500; }
|
|
2169
|
+
.st-milestone { stroke: var(--st-marker-fill); stroke-width: 2; }
|
|
2170
|
+
.st-milestone-label { font-size: 12px; fill: var(--schematex-text); font-weight: 600; }
|
|
2171
|
+
.st-track-label { font-size: 12px; font-weight: 600; fill: var(--schematex-text); }
|
|
2172
|
+
.st-lane-stripe { fill: var(--st-lane-stripe); opacity: ${theme.laneStripeOpacity}; }
|
|
2173
|
+
.st-callout-line { stroke: var(--st-axis); stroke-width: 0.8; stroke-dasharray: 2 2; opacity: 0.5; fill: none; }
|
|
2174
|
+
.st-callout-text { font-size: 10.5px; fill: var(--schematex-text-muted); }
|
|
2175
|
+
.st-label-leader { stroke: var(--st-axis); stroke-width: 0.75; opacity: 0.35; fill: none; }
|
|
2176
|
+
.st-icon { font-size: 14px; }
|
|
2177
|
+
/* Gantt */
|
|
2178
|
+
.st-pin-shaft { stroke: var(--st-pin-shaft); stroke-width: 1.25; stroke-dasharray: 3 2; fill: none; }
|
|
2179
|
+
.st-pin-label { font-size: 11.5px; fill: var(--schematex-text); font-weight: 500; }
|
|
2180
|
+
.st-pin-head { stroke: var(--st-marker-fill); stroke-width: 1.5; }
|
|
2181
|
+
.st-lane-label { font-size: 11px; fill: var(--schematex-text-muted); font-weight: 500; }
|
|
2182
|
+
.st-legend-box { fill: var(--st-legend-bg); stroke: var(--st-legend-stroke); stroke-width: 1; }
|
|
2183
|
+
.st-legend-title { font-size: 11px; font-weight: 600; fill: var(--schematex-text-muted); }
|
|
2184
|
+
.st-legend-label { font-size: 11px; fill: var(--schematex-text); }
|
|
2185
|
+
/* Lollipop */
|
|
2186
|
+
.st-card { fill: var(--st-card-bg); stroke: var(--st-card-stroke); stroke-width: 1; }
|
|
2187
|
+
.st-card-title { font-size: 12px; font-weight: 600; fill: var(--st-card-text); }
|
|
2188
|
+
.st-card-date { font-size: 10.5px; fill: var(--schematex-text-muted); }
|
|
2189
|
+
.st-card-icon { font-size: 16px; }
|
|
2190
|
+
.st-stem { stroke: var(--st-axis); stroke-width: 1.25; opacity: 0.55; fill: none; }
|
|
2191
|
+
.st-lp-marker-ring { stroke-width: 2.5; }
|
|
2192
|
+
.st-lp-marker-core { stroke: none; }
|
|
2193
|
+
`;
|
|
2194
|
+
}
|
|
2195
|
+
function renderEras(layout, theme) {
|
|
2196
|
+
if (!layout.eras.length) return "";
|
|
2197
|
+
const topBase = layout.title ? 54 : 40;
|
|
2198
|
+
const palette = theme.palette;
|
|
2199
|
+
const leftmostOnRow = /* @__PURE__ */ new Map();
|
|
2200
|
+
const rightmostOnRow = /* @__PURE__ */ new Map();
|
|
2201
|
+
layout.eras.forEach((e, i) => {
|
|
2202
|
+
const l = leftmostOnRow.get(e.bandRow);
|
|
2203
|
+
if (l === void 0 || e.x < layout.eras[l].x) leftmostOnRow.set(e.bandRow, i);
|
|
2204
|
+
const r = rightmostOnRow.get(e.bandRow);
|
|
2205
|
+
if (r === void 0 || e.x + e.width > layout.eras[r].x + layout.eras[r].width) rightmostOnRow.set(e.bandRow, i);
|
|
2206
|
+
});
|
|
2207
|
+
const plotEnd = layout.plotX + layout.plotW;
|
|
2208
|
+
const items = layout.eras.map((e, i) => {
|
|
2209
|
+
const y = topBase + e.bandRow * (ERA_BAND_HEIGHT2 + ERA_ROW_GAP);
|
|
2210
|
+
const fill = e.era.color ?? palette[i % palette.length];
|
|
2211
|
+
const labelX = e.x + 6;
|
|
2212
|
+
const isLeftmost = leftmostOnRow.get(e.bandRow) === i;
|
|
2213
|
+
const isRightmost = rightmostOnRow.get(e.bandRow) === i;
|
|
2214
|
+
const stripX = isLeftmost ? Math.min(e.x, layout.plotX) : e.x;
|
|
2215
|
+
const stripEnd = isRightmost ? Math.max(e.x + e.width, plotEnd) : e.x + e.width;
|
|
2216
|
+
return [
|
|
2217
|
+
rect({
|
|
2218
|
+
x: e.x,
|
|
2219
|
+
y,
|
|
2220
|
+
width: e.width,
|
|
2221
|
+
height: ERA_BAND_HEIGHT2,
|
|
2222
|
+
fill,
|
|
2223
|
+
class: "st-era-rect",
|
|
2224
|
+
"data-era-id": e.era.id
|
|
2225
|
+
}),
|
|
2226
|
+
rect({
|
|
2227
|
+
x: stripX,
|
|
2228
|
+
y: layout.plotY,
|
|
2229
|
+
width: stripEnd - stripX,
|
|
2230
|
+
height: layout.plotH,
|
|
2231
|
+
fill,
|
|
2232
|
+
class: "st-era-strip"
|
|
2233
|
+
}),
|
|
2234
|
+
text(
|
|
2235
|
+
{ x: labelX, y: y + 12, class: "st-era-label" },
|
|
2236
|
+
truncate(e.era.label, Math.max(4, Math.floor(e.width / 7)))
|
|
2237
|
+
)
|
|
2238
|
+
].join("");
|
|
2239
|
+
});
|
|
2240
|
+
return group({ class: "st-eras" }, items);
|
|
2241
|
+
}
|
|
2242
|
+
function renderAxis(layout) {
|
|
2243
|
+
const ax = layout.axisY;
|
|
2244
|
+
const items = [
|
|
2245
|
+
line({ x1: layout.plotX, y1: ax, x2: layout.plotX + layout.plotW, y2: ax, class: "st-axis-line" })
|
|
2246
|
+
];
|
|
2247
|
+
for (const t of layout.ticks) {
|
|
2248
|
+
items.push(line({ x1: t.x, y1: ax, x2: t.x, y2: ax + 5, class: "st-axis-tick" }));
|
|
2249
|
+
items.push(text({ x: t.x, y: ax + 18, "text-anchor": "middle", class: "st-axis-label" }, t.label));
|
|
2250
|
+
}
|
|
2251
|
+
return group({ class: "st-axis" }, items);
|
|
2252
|
+
}
|
|
2253
|
+
function renderSwimlane(layout, theme) {
|
|
2254
|
+
const parts = [];
|
|
2255
|
+
parts.push(renderTrackLabels(layout));
|
|
2256
|
+
parts.push(renderLaneStripes(layout));
|
|
2257
|
+
parts.push(renderSwimlaneRanges(layout, theme));
|
|
2258
|
+
parts.push(renderSwimlanePoints(layout, theme));
|
|
2259
|
+
parts.push(renderLabels(layout));
|
|
2260
|
+
parts.push(renderNotes(layout));
|
|
2261
|
+
return parts.join("");
|
|
2262
|
+
}
|
|
2263
|
+
function renderTrackLabels(layout) {
|
|
2264
|
+
const items = layout.lanes.filter((l) => l.label).map((l) => text(
|
|
2265
|
+
{ x: layout.plotX - 12, y: l.y + l.height / 2 + 4, "text-anchor": "end", class: "st-track-label" },
|
|
2266
|
+
l.label
|
|
2267
|
+
));
|
|
2268
|
+
return group({ class: "st-track-labels" }, items);
|
|
2269
|
+
}
|
|
2270
|
+
function renderLaneStripes(layout) {
|
|
2271
|
+
const items = layout.lanes.map((l, i) => {
|
|
2272
|
+
if (i % 2 !== 0) return "";
|
|
2273
|
+
return rect({
|
|
2274
|
+
x: layout.plotX,
|
|
2275
|
+
y: l.y,
|
|
2276
|
+
width: layout.plotW,
|
|
2277
|
+
height: l.height,
|
|
2278
|
+
class: "st-lane-stripe"
|
|
2279
|
+
});
|
|
2280
|
+
}).filter(Boolean);
|
|
2281
|
+
return group({ class: "st-lanes" }, items);
|
|
2282
|
+
}
|
|
2283
|
+
function renderSwimlaneRanges(layout, theme) {
|
|
2284
|
+
const items = [];
|
|
2285
|
+
const trackOrder = layout.lanes.map((l) => l.trackId);
|
|
2286
|
+
const palette = theme.categoryPalette;
|
|
2287
|
+
for (const ev of layout.events) {
|
|
2288
|
+
if (ev.event.kind !== "range") continue;
|
|
2289
|
+
const idx = trackOrder.indexOf(ev.event.trackId);
|
|
2290
|
+
const fill = ev.event.color ?? palette[(idx < 0 ? 0 : idx) % palette.length];
|
|
2291
|
+
items.push(rect({
|
|
2292
|
+
x: ev.x,
|
|
2293
|
+
y: ev.y,
|
|
2294
|
+
width: ev.w ?? 4,
|
|
2295
|
+
height: ev.h,
|
|
2296
|
+
rx: 4,
|
|
2297
|
+
ry: 4,
|
|
2298
|
+
fill,
|
|
2299
|
+
class: "st-range-bar",
|
|
2300
|
+
"data-event-id": ev.event.id
|
|
2301
|
+
}));
|
|
2302
|
+
const w = ev.w ?? 0;
|
|
2303
|
+
if (w >= 60) {
|
|
2304
|
+
items.push(text(
|
|
2305
|
+
{ x: ev.labelX, y: ev.labelY, "text-anchor": "middle", class: "st-range-label" },
|
|
2306
|
+
truncate(ev.event.label, Math.floor(w / 6))
|
|
2307
|
+
));
|
|
2308
|
+
} else if (w > 0) {
|
|
2309
|
+
items.push(text(
|
|
2310
|
+
{ x: ev.x + w + 4, y: ev.labelY, "text-anchor": "start", class: "st-event-label" },
|
|
2311
|
+
ev.event.label
|
|
2312
|
+
));
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
return group({ class: "st-ranges" }, items);
|
|
2316
|
+
}
|
|
2317
|
+
function renderSwimlanePoints(layout, theme) {
|
|
2318
|
+
const items = [];
|
|
2319
|
+
for (const ev of layout.events) {
|
|
2320
|
+
if (ev.event.kind === "range") continue;
|
|
2321
|
+
const isMilestone = ev.event.kind === "milestone";
|
|
2322
|
+
const color = ev.event.color ?? (isMilestone ? theme.milestoneFill : theme.markerRing);
|
|
2323
|
+
const shape = ev.event.shape ?? (isMilestone ? "star" : "circle");
|
|
2324
|
+
items.push(renderMarker(ev, color, shape, isMilestone));
|
|
2325
|
+
}
|
|
2326
|
+
return group({ class: "st-points" }, items);
|
|
2327
|
+
}
|
|
2328
|
+
function renderMarker(ev, color, shape, isMilestone) {
|
|
2329
|
+
const x = ev.x;
|
|
2330
|
+
const y = ev.y;
|
|
2331
|
+
const r = isMilestone ? 8 : 5;
|
|
2332
|
+
const klass = isMilestone ? "st-milestone" : "st-event-dot";
|
|
2333
|
+
switch (shape) {
|
|
2334
|
+
case "square":
|
|
2335
|
+
return rect({ x: x - r, y: y - r, width: r * 2, height: r * 2, fill: color, class: klass, "data-event-id": ev.event.id });
|
|
2336
|
+
case "diamond":
|
|
2337
|
+
return path({ d: `M ${x},${y - r} L ${x + r},${y} L ${x},${y + r} L ${x - r},${y} Z`, fill: color, class: klass, "data-event-id": ev.event.id });
|
|
2338
|
+
case "star":
|
|
2339
|
+
return path({ d: starPath(x, y, r + 2, (r + 2) / 2.5, 5), fill: color, class: klass, "data-event-id": ev.event.id });
|
|
2340
|
+
case "flag":
|
|
2341
|
+
return path({ d: `M ${x - r},${y + r} L ${x - r},${y - r - 4} L ${x + r + 4},${y - r - 1} L ${x - r},${y + 2}`, fill: color, class: klass, "data-event-id": ev.event.id });
|
|
2342
|
+
case "circle":
|
|
2343
|
+
default:
|
|
2344
|
+
return circle({ cx: x, cy: y, r, fill: color, class: klass, "data-event-id": ev.event.id });
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
function renderLabels(layout) {
|
|
2348
|
+
const items = [];
|
|
2349
|
+
for (const ev of layout.events) {
|
|
2350
|
+
if (ev.event.kind === "range") continue;
|
|
2351
|
+
const cls = ev.event.kind === "milestone" ? "st-milestone-label" : "st-event-label";
|
|
2352
|
+
const text2 = ev.event.icon ? `${ev.event.icon} ${ev.event.label}` : ev.event.label;
|
|
2353
|
+
const dy = ev.labelY - ev.y;
|
|
2354
|
+
if (Math.abs(dy) > 22) {
|
|
2355
|
+
const y1 = dy < 0 ? ev.y - 6 : ev.y + 6;
|
|
2356
|
+
const y2 = dy < 0 ? ev.labelY + 3 : ev.labelY - 10;
|
|
2357
|
+
items.push(line({ x1: ev.x, y1, x2: ev.x, y2, class: "st-label-leader" }));
|
|
2358
|
+
}
|
|
2359
|
+
items.push(text(
|
|
2360
|
+
{ x: ev.labelX, y: ev.labelY, "text-anchor": ev.labelAnchor, class: cls },
|
|
2361
|
+
text2
|
|
2362
|
+
));
|
|
2363
|
+
}
|
|
2364
|
+
return group({ class: "st-labels" }, items);
|
|
2365
|
+
}
|
|
2366
|
+
function renderNotes(layout) {
|
|
2367
|
+
const items = [];
|
|
2368
|
+
for (const ev of layout.events) {
|
|
2369
|
+
if (!ev.event.note) continue;
|
|
2370
|
+
const x = ev.x;
|
|
2371
|
+
const ny = ev.noteY ?? ev.y + 18;
|
|
2372
|
+
const nx = ev.noteX ?? x + 10;
|
|
2373
|
+
const wrapped = wrapText2(ev.event.note, 46);
|
|
2374
|
+
items.push(path({ d: `M ${x} ${ev.y + 6} Q ${x + 4} ${ny - 4} ${nx} ${ny}`, class: "st-callout-line" }));
|
|
2375
|
+
wrapped.forEach((ln, i) => {
|
|
2376
|
+
items.push(text(
|
|
2377
|
+
{ x: nx, y: ny + i * 13, class: "st-callout-text" },
|
|
2378
|
+
ln
|
|
2379
|
+
));
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
2382
|
+
return group({ class: "st-notes" }, items);
|
|
2383
|
+
}
|
|
2384
|
+
function renderGantt(layout, theme) {
|
|
2385
|
+
const parts = [];
|
|
2386
|
+
parts.push(renderGanttLanes(layout));
|
|
2387
|
+
parts.push(renderGanttBars(layout, theme));
|
|
2388
|
+
parts.push(renderGanttPins(layout));
|
|
2389
|
+
if (layout.legend && layout.legend.length) {
|
|
2390
|
+
parts.push(renderLegend(layout));
|
|
2391
|
+
}
|
|
2392
|
+
return parts.join("");
|
|
2393
|
+
}
|
|
2394
|
+
function renderGanttLanes(layout) {
|
|
2395
|
+
const items = [];
|
|
2396
|
+
layout.lanes.forEach((l, i) => {
|
|
2397
|
+
if (i % 2 === 0) {
|
|
2398
|
+
items.push(rect({
|
|
2399
|
+
x: layout.plotX,
|
|
2400
|
+
y: l.y,
|
|
2401
|
+
width: layout.plotW,
|
|
2402
|
+
height: l.height,
|
|
2403
|
+
class: "st-lane-stripe"
|
|
2404
|
+
}));
|
|
2405
|
+
}
|
|
2406
|
+
items.push(text({
|
|
2407
|
+
x: layout.plotX - 10,
|
|
2408
|
+
y: l.y + l.height / 2 + 4,
|
|
2409
|
+
"text-anchor": "end",
|
|
2410
|
+
class: "st-lane-label"
|
|
2411
|
+
}, l.label));
|
|
2412
|
+
});
|
|
2413
|
+
return group({ class: "st-gantt-lanes" }, items);
|
|
2414
|
+
}
|
|
2415
|
+
function renderGanttBars(layout, theme) {
|
|
2416
|
+
const items = [];
|
|
2417
|
+
const laneByCat = new Map(layout.lanes.map((l) => [l.label, l]));
|
|
2418
|
+
const legendByLabel = new Map((layout.legend ?? []).map((l) => [l.label, l.color]));
|
|
2419
|
+
for (const ev of layout.events) {
|
|
2420
|
+
if (ev.event.kind !== "range") continue;
|
|
2421
|
+
const cat = ev.event.category ?? layout.lanes[0]?.label ?? "";
|
|
2422
|
+
const color = ev.event.color ?? legendByLabel.get(cat) ?? theme.categoryPalette[0];
|
|
2423
|
+
const w = ev.w ?? 4;
|
|
2424
|
+
items.push(rect({
|
|
2425
|
+
x: ev.x,
|
|
2426
|
+
y: ev.y,
|
|
2427
|
+
width: w,
|
|
2428
|
+
height: ev.h,
|
|
2429
|
+
rx: 5,
|
|
2430
|
+
ry: 5,
|
|
2431
|
+
fill: color,
|
|
2432
|
+
class: "st-range-bar",
|
|
2433
|
+
"data-event-id": ev.event.id,
|
|
2434
|
+
"data-category": cat
|
|
2435
|
+
}));
|
|
2436
|
+
if (w >= 60) {
|
|
2437
|
+
items.push(text(
|
|
2438
|
+
{ x: ev.labelX, y: ev.labelY, "text-anchor": "middle", class: "st-range-label" },
|
|
2439
|
+
truncate(ev.event.label, Math.floor(w / 6))
|
|
2440
|
+
));
|
|
2441
|
+
} else if (w > 0) {
|
|
2442
|
+
const lane = laneByCat.get(cat);
|
|
2443
|
+
const ly = lane ? lane.y + lane.height / 2 + 4 : ev.labelY;
|
|
2444
|
+
items.push(text(
|
|
2445
|
+
{ x: ev.x + w + 4, y: ly, "text-anchor": "start", class: "st-event-label" },
|
|
2446
|
+
ev.event.label
|
|
2447
|
+
));
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
return group({ class: "st-gantt-bars" }, items);
|
|
2451
|
+
}
|
|
2452
|
+
function renderGanttPins(layout) {
|
|
2453
|
+
const pins = layout.pins ?? [];
|
|
2454
|
+
if (!pins.length) return "";
|
|
2455
|
+
const items = [];
|
|
2456
|
+
for (const p of pins) {
|
|
2457
|
+
items.push(renderPin(p));
|
|
2458
|
+
}
|
|
2459
|
+
return group({ class: "st-gantt-pins" }, items);
|
|
2460
|
+
}
|
|
2461
|
+
function renderPin(p) {
|
|
2462
|
+
const isMilestone = p.event.kind === "milestone";
|
|
2463
|
+
const shaft = path({
|
|
2464
|
+
d: `M ${p.x} ${p.labelY + 2} L ${p.x} ${p.axisY - 4}`,
|
|
2465
|
+
class: "st-pin-shaft"
|
|
2466
|
+
});
|
|
2467
|
+
const head = isMilestone ? path({
|
|
2468
|
+
d: starPath(p.x, p.axisY - 6, 7, 3, 5),
|
|
2469
|
+
fill: p.color,
|
|
2470
|
+
class: "st-pin-head",
|
|
2471
|
+
"data-event-id": p.event.id
|
|
2472
|
+
}) : circle({
|
|
2473
|
+
cx: p.x,
|
|
2474
|
+
cy: p.axisY - 6,
|
|
2475
|
+
r: 5,
|
|
2476
|
+
fill: p.color,
|
|
2477
|
+
class: "st-pin-head",
|
|
2478
|
+
"data-event-id": p.event.id
|
|
2479
|
+
});
|
|
2480
|
+
const txt = p.event.icon ? `${p.event.icon} ${p.event.label}` : p.event.label;
|
|
2481
|
+
const label = text(
|
|
2482
|
+
{ x: p.x, y: p.labelY, "text-anchor": "middle", class: "st-pin-label" },
|
|
2483
|
+
txt
|
|
2484
|
+
);
|
|
2485
|
+
return shaft + head + label;
|
|
2486
|
+
}
|
|
2487
|
+
function renderLegend(layout) {
|
|
2488
|
+
const legend = layout.legend ?? [];
|
|
2489
|
+
if (!legend.length) return "";
|
|
2490
|
+
const boxW = 130;
|
|
2491
|
+
const padding = 10;
|
|
2492
|
+
const rowH = 18;
|
|
2493
|
+
const h = padding * 2 + 18 + legend.length * rowH;
|
|
2494
|
+
const x = layout.width - boxW - 16;
|
|
2495
|
+
const y = layout.plotY;
|
|
2496
|
+
const items = [
|
|
2497
|
+
rect({ x, y, width: boxW, height: h, rx: 6, ry: 6, class: "st-legend-box" }),
|
|
2498
|
+
text({ x: x + padding, y: y + padding + 12, class: "st-legend-title" }, "Teams")
|
|
2499
|
+
];
|
|
2500
|
+
legend.forEach((it, i) => {
|
|
2501
|
+
const rowY = y + padding + 18 + i * rowH + 8;
|
|
2502
|
+
items.push(rect({ x: x + padding, y: rowY - 7, width: 12, height: 12, rx: 2, ry: 2, fill: it.color }));
|
|
2503
|
+
items.push(text({ x: x + padding + 20, y: rowY + 3, class: "st-legend-label" }, truncate(it.label, 14)));
|
|
2504
|
+
});
|
|
2505
|
+
return group({ class: "st-legend" }, items);
|
|
2506
|
+
}
|
|
2507
|
+
function renderLollipop(layout, theme) {
|
|
2508
|
+
const parts = [];
|
|
2509
|
+
parts.push(renderLollipopRanges(layout, theme));
|
|
2510
|
+
parts.push(renderLollipopCards(layout, theme));
|
|
2511
|
+
return parts.join("");
|
|
2512
|
+
}
|
|
2513
|
+
function renderLollipopRanges(layout, theme) {
|
|
2514
|
+
const items = [];
|
|
2515
|
+
for (const ev of layout.events) {
|
|
2516
|
+
if (ev.event.kind !== "range") continue;
|
|
2517
|
+
const color = ev.event.color ?? theme.categoryPalette[0];
|
|
2518
|
+
items.push(rect({
|
|
2519
|
+
x: ev.x,
|
|
2520
|
+
y: ev.y,
|
|
2521
|
+
width: ev.w ?? 4,
|
|
2522
|
+
height: ev.h,
|
|
2523
|
+
rx: 3,
|
|
2524
|
+
ry: 3,
|
|
2525
|
+
fill: color,
|
|
2526
|
+
class: "st-range-bar",
|
|
2527
|
+
"data-event-id": ev.event.id
|
|
2528
|
+
}));
|
|
2529
|
+
}
|
|
2530
|
+
return group({ class: "st-lp-ranges" }, items);
|
|
2531
|
+
}
|
|
2532
|
+
function renderLollipopCards(layout, theme) {
|
|
2533
|
+
const cards = layout.cards ?? [];
|
|
2534
|
+
if (!cards.length) return "";
|
|
2535
|
+
const items = [];
|
|
2536
|
+
for (const c of cards) {
|
|
2537
|
+
items.push(renderLollipopCard(c, theme));
|
|
2538
|
+
}
|
|
2539
|
+
return group({ class: "st-lp-cards" }, items);
|
|
2540
|
+
}
|
|
2541
|
+
function renderLollipopCard(c, theme) {
|
|
2542
|
+
const parts = [];
|
|
2543
|
+
parts.push(path({ d: `M ${c.x} ${c.stemY1} L ${c.x} ${c.stemY2}`, class: "st-stem" }));
|
|
2544
|
+
parts.push(circle({ cx: c.x, cy: c.axisY, r: 7, fill: theme.markerFill, stroke: c.color, class: "st-lp-marker-ring" }));
|
|
2545
|
+
parts.push(circle({ cx: c.x, cy: c.axisY, r: 3.2, fill: c.color, class: "st-lp-marker-core" }));
|
|
2546
|
+
parts.push(rect({
|
|
2547
|
+
x: c.cardX,
|
|
2548
|
+
y: c.cardY,
|
|
2549
|
+
width: c.cardW,
|
|
2550
|
+
height: c.cardH,
|
|
2551
|
+
rx: 8,
|
|
2552
|
+
ry: 8,
|
|
2553
|
+
class: "st-card"
|
|
2554
|
+
}));
|
|
2555
|
+
parts.push(rect({
|
|
2556
|
+
x: c.cardX,
|
|
2557
|
+
y: c.cardY,
|
|
2558
|
+
width: 4,
|
|
2559
|
+
height: c.cardH,
|
|
2560
|
+
rx: 2,
|
|
2561
|
+
ry: 2,
|
|
2562
|
+
fill: c.color
|
|
2563
|
+
}));
|
|
2564
|
+
const padX = c.cardX + 14;
|
|
2565
|
+
const titleText = c.event.icon ? `${c.event.icon} ${c.event.label}` : c.event.label;
|
|
2566
|
+
parts.push(text(
|
|
2567
|
+
{ x: padX, y: c.cardY + 22, class: "st-card-title" },
|
|
2568
|
+
truncate(titleText, 26)
|
|
2569
|
+
));
|
|
2570
|
+
parts.push(text(
|
|
2571
|
+
{ x: padX, y: c.cardY + 40, class: "st-card-date" },
|
|
2572
|
+
c.event.start.raw
|
|
2573
|
+
));
|
|
2574
|
+
return parts.join("");
|
|
2575
|
+
}
|
|
2576
|
+
function truncate(s, maxChars) {
|
|
2577
|
+
if (s.length <= maxChars) return s;
|
|
2578
|
+
if (maxChars < 4) return "";
|
|
2579
|
+
return s.slice(0, maxChars - 1) + "\u2026";
|
|
2580
|
+
}
|
|
2581
|
+
function wrapText2(s, max) {
|
|
2582
|
+
const words = s.split(/\s+/);
|
|
2583
|
+
const out = [];
|
|
2584
|
+
let cur = "";
|
|
2585
|
+
for (const w of words) {
|
|
2586
|
+
if ((cur + " " + w).trim().length > max) {
|
|
2587
|
+
if (cur) out.push(cur);
|
|
2588
|
+
cur = w;
|
|
2589
|
+
} else {
|
|
2590
|
+
cur = (cur ? cur + " " : "") + w;
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
if (cur) out.push(cur);
|
|
2594
|
+
return out.slice(0, 4);
|
|
2595
|
+
}
|
|
2596
|
+
function starPath(cx, cy, rOuter, rInner, points) {
|
|
2597
|
+
const step = Math.PI / points;
|
|
2598
|
+
let d = "";
|
|
2599
|
+
for (let i = 0; i < points * 2; i++) {
|
|
2600
|
+
const r = i % 2 === 0 ? rOuter : rInner;
|
|
2601
|
+
const a = i * step - Math.PI / 2;
|
|
2602
|
+
const x = cx + r * Math.cos(a);
|
|
2603
|
+
const y = cy + r * Math.sin(a);
|
|
2604
|
+
d += (i === 0 ? "M" : "L") + x.toFixed(2) + "," + y.toFixed(2) + " ";
|
|
2605
|
+
}
|
|
2606
|
+
return d + "Z";
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
// src/diagrams/timeline/index.ts
|
|
2610
|
+
var timeline = {
|
|
2611
|
+
type: "timeline",
|
|
2612
|
+
detect(text2) {
|
|
2613
|
+
return /^\s*timeline\b/i.test(text2);
|
|
2614
|
+
},
|
|
2615
|
+
parse: parseTimeline,
|
|
2616
|
+
render(text2, config) {
|
|
2617
|
+
return renderTimeline(text2, config);
|
|
2618
|
+
}
|
|
2619
|
+
};
|
|
2620
|
+
|
|
2621
|
+
// src/diagrams/mindmap/parser.ts
|
|
2622
|
+
var VALID_STYLES = ["map", "logic-right"];
|
|
2623
|
+
function parseDirective(line2, out) {
|
|
2624
|
+
const body = line2.replace(/^%%\s*/, "").trim();
|
|
2625
|
+
const idx = body.indexOf(":");
|
|
2626
|
+
if (idx < 0) return;
|
|
2627
|
+
const key = body.slice(0, idx).trim().toLowerCase();
|
|
2628
|
+
const val = body.slice(idx + 1).trim();
|
|
2629
|
+
if (key === "style" && VALID_STYLES.includes(val)) {
|
|
2630
|
+
out.style = val;
|
|
2631
|
+
} else if (key === "theme") {
|
|
2632
|
+
out.themeOverride = val;
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
function parseMindmap(text2) {
|
|
2636
|
+
const lines = text2.split(/\r?\n/);
|
|
2637
|
+
if (lines[0]?.trim().toLowerCase() === "mindmap") lines.shift();
|
|
2638
|
+
const directives = { style: "map" };
|
|
2639
|
+
let root = null;
|
|
2640
|
+
let idCounter2 = 0;
|
|
2641
|
+
const nextId2 = () => `n${idCounter2++}`;
|
|
2642
|
+
const stack = [];
|
|
2643
|
+
let lastHeadingDepth = 0;
|
|
2644
|
+
const attach = (node, depth) => {
|
|
2645
|
+
while (stack.length && stack[stack.length - 1].depth >= depth) stack.pop();
|
|
2646
|
+
const parent = stack[stack.length - 1]?.node;
|
|
2647
|
+
if (!parent) throw new Error("Mindmap: orphan node \u2014 expected root # heading first");
|
|
2648
|
+
node.depth = parent.depth + 1;
|
|
2649
|
+
parent.children.push(node);
|
|
2650
|
+
stack.push({ node, depth });
|
|
2651
|
+
};
|
|
2652
|
+
for (const raw of lines) {
|
|
2653
|
+
const line2 = raw.replace(/\s+$/, "");
|
|
2654
|
+
const trimmed = line2.trim();
|
|
2655
|
+
if (!trimmed) continue;
|
|
2656
|
+
if (trimmed.startsWith("%%")) {
|
|
2657
|
+
parseDirective(trimmed, directives);
|
|
2658
|
+
continue;
|
|
2659
|
+
}
|
|
2660
|
+
const heading = line2.match(/^\s*(#{1,6})\s+(.+)$/);
|
|
2661
|
+
if (heading) {
|
|
2662
|
+
const depth = heading[1].length - 1;
|
|
2663
|
+
const label = heading[2].trim();
|
|
2664
|
+
const node = { id: nextId2(), label, depth, children: [] };
|
|
2665
|
+
if (depth === 0) {
|
|
2666
|
+
if (root) throw new Error("Mindmap: multiple `#` center nodes not allowed");
|
|
2667
|
+
root = node;
|
|
2668
|
+
stack.length = 0;
|
|
2669
|
+
stack.push({ node, depth: 0 });
|
|
2670
|
+
} else {
|
|
2671
|
+
attach(node, depth);
|
|
2672
|
+
}
|
|
2673
|
+
lastHeadingDepth = depth;
|
|
2674
|
+
continue;
|
|
2675
|
+
}
|
|
2676
|
+
const bullet = line2.match(/^(\s*)[-*+]\s+(.+)$/);
|
|
2677
|
+
if (bullet) {
|
|
2678
|
+
const indent = bullet[1].length;
|
|
2679
|
+
const depth = lastHeadingDepth + 1 + Math.floor(indent / 2);
|
|
2680
|
+
const label = bullet[2].trim();
|
|
2681
|
+
const node = { id: nextId2(), label, depth, children: [] };
|
|
2682
|
+
attach(node, depth);
|
|
2683
|
+
continue;
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
if (!root) throw new Error("Mindmap: missing central topic \u2014 start with `# Title`");
|
|
2687
|
+
const ast = {
|
|
2688
|
+
type: "mindmap",
|
|
2689
|
+
style: directives.style,
|
|
2690
|
+
root
|
|
2691
|
+
};
|
|
2692
|
+
if (directives.themeOverride) ast.themeOverride = directives.themeOverride;
|
|
2693
|
+
return ast;
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2696
|
+
// src/diagrams/mindmap/layout.ts
|
|
2697
|
+
var PADDING = 40;
|
|
2698
|
+
var SIBLING_GAP = 20;
|
|
2699
|
+
var MAIN_GAP = 44;
|
|
2700
|
+
var ROOT_CAPSULE_PAD_X = 10;
|
|
2701
|
+
var ROOT_CAPSULE_PAD_Y = 10;
|
|
2702
|
+
function bezierGapFor(childDepth) {
|
|
2703
|
+
if (childDepth <= 1) return 90;
|
|
2704
|
+
if (childDepth === 2) return 60;
|
|
2705
|
+
return 45;
|
|
2706
|
+
}
|
|
2707
|
+
var FONT_CENTRAL = 20;
|
|
2708
|
+
var FONT_MAIN = 15;
|
|
2709
|
+
var FONT_SUB = 13;
|
|
2710
|
+
function fontSizeOf(depth) {
|
|
2711
|
+
if (depth === 0) return FONT_CENTRAL;
|
|
2712
|
+
if (depth === 1) return FONT_MAIN;
|
|
2713
|
+
return FONT_SUB;
|
|
2714
|
+
}
|
|
2715
|
+
function estimateLabelWidth2(label, depth) {
|
|
2716
|
+
return Math.max(32, label.length * fontSizeOf(depth) * 0.58);
|
|
2717
|
+
}
|
|
2718
|
+
function rowHeightOf(depth) {
|
|
2719
|
+
return fontSizeOf(depth) + 14;
|
|
2720
|
+
}
|
|
2721
|
+
function computeColumns(subtreeRoots, firstColStartX) {
|
|
2722
|
+
const maxLW = [];
|
|
2723
|
+
const walk = (n) => {
|
|
2724
|
+
const lw = estimateLabelWidth2(n.label, n.depth);
|
|
2725
|
+
if (maxLW[n.depth] === void 0 || lw > maxLW[n.depth]) maxLW[n.depth] = lw;
|
|
2726
|
+
for (const c of n.children) walk(c);
|
|
2727
|
+
};
|
|
2728
|
+
for (const r of subtreeRoots) walk(r);
|
|
2729
|
+
const center = [];
|
|
2730
|
+
if (subtreeRoots.length === 0) return { center };
|
|
2731
|
+
const rootDepth = subtreeRoots[0].depth;
|
|
2732
|
+
let slotLeft = firstColStartX;
|
|
2733
|
+
center[rootDepth] = slotLeft + maxLW[rootDepth] / 2;
|
|
2734
|
+
for (let d = rootDepth + 1; d < maxLW.length; d++) {
|
|
2735
|
+
slotLeft = slotLeft + maxLW[d - 1] + bezierGapFor(d);
|
|
2736
|
+
center[d] = slotLeft + maxLW[d] / 2;
|
|
2737
|
+
}
|
|
2738
|
+
return { center };
|
|
2739
|
+
}
|
|
2740
|
+
function tidyRight(node, yTop, branchIdx, columns, out) {
|
|
2741
|
+
const rowH = rowHeightOf(node.depth);
|
|
2742
|
+
const lw = estimateLabelWidth2(node.label, node.depth);
|
|
2743
|
+
const x = columns.center[node.depth];
|
|
2744
|
+
if (node.children.length === 0) {
|
|
2745
|
+
const ln2 = {
|
|
2746
|
+
node,
|
|
2747
|
+
x,
|
|
2748
|
+
y: yTop + rowH / 2,
|
|
2749
|
+
side: "right",
|
|
2750
|
+
branchIndex: branchIdx,
|
|
2751
|
+
labelWidth: lw,
|
|
2752
|
+
labelHeight: rowH
|
|
2753
|
+
};
|
|
2754
|
+
out.push(ln2);
|
|
2755
|
+
return { layoutNode: ln2, height: rowH };
|
|
2756
|
+
}
|
|
2757
|
+
let cursor = yTop;
|
|
2758
|
+
const childLayouts = [];
|
|
2759
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
2760
|
+
if (i > 0) cursor += SIBLING_GAP;
|
|
2761
|
+
const { layoutNode: cln, height } = tidyRight(
|
|
2762
|
+
node.children[i],
|
|
2763
|
+
cursor,
|
|
2764
|
+
branchIdx,
|
|
2765
|
+
columns,
|
|
2766
|
+
out
|
|
2767
|
+
);
|
|
2768
|
+
childLayouts.push(cln);
|
|
2769
|
+
cursor += height;
|
|
2770
|
+
}
|
|
2771
|
+
const totalH = Math.max(rowH, cursor - yTop);
|
|
2772
|
+
const firstY = childLayouts[0].y;
|
|
2773
|
+
const lastY = childLayouts[childLayouts.length - 1].y;
|
|
2774
|
+
const parentY = (firstY + lastY) / 2;
|
|
2775
|
+
const ln = {
|
|
2776
|
+
node,
|
|
2777
|
+
x,
|
|
2778
|
+
y: parentY,
|
|
2779
|
+
side: "right",
|
|
2780
|
+
branchIndex: branchIdx,
|
|
2781
|
+
labelWidth: lw,
|
|
2782
|
+
labelHeight: rowH
|
|
2783
|
+
};
|
|
2784
|
+
out.push(ln);
|
|
2785
|
+
return { layoutNode: ln, height: totalH };
|
|
2786
|
+
}
|
|
2787
|
+
function labelEdgeX(n, outward) {
|
|
2788
|
+
if (n.side === "center") return n.x;
|
|
2789
|
+
const dir = n.side === "left" ? -1 : 1;
|
|
2790
|
+
return n.x + (outward ? dir : -dir) * n.labelWidth / 2;
|
|
2791
|
+
}
|
|
2792
|
+
function bezierH(x1, y1, x2, y2) {
|
|
2793
|
+
const k = (x2 - x1) * 0.55;
|
|
2794
|
+
return `M ${x1.toFixed(1)} ${y1.toFixed(1)} C ${(x1 + k).toFixed(1)} ${y1.toFixed(1)}, ${(x2 - k).toFixed(1)} ${y2.toFixed(1)}, ${x2.toFixed(1)} ${y2.toFixed(1)}`;
|
|
2795
|
+
}
|
|
2796
|
+
function edgeWidthFor(depth) {
|
|
2797
|
+
return depth <= 1 ? 2.2 : 1.4;
|
|
2798
|
+
}
|
|
2799
|
+
function normalize(nodes) {
|
|
2800
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
2801
|
+
for (const n of nodes) {
|
|
2802
|
+
const lw = n.labelWidth;
|
|
2803
|
+
const lh = n.labelHeight;
|
|
2804
|
+
const leftX = n.x - lw / 2;
|
|
2805
|
+
const rightX = n.x + lw / 2;
|
|
2806
|
+
minX = Math.min(minX, leftX);
|
|
2807
|
+
maxX = Math.max(maxX, rightX);
|
|
2808
|
+
minY = Math.min(minY, n.y - lh);
|
|
2809
|
+
maxY = Math.max(maxY, n.y + lh);
|
|
2810
|
+
}
|
|
2811
|
+
const dx = PADDING - minX;
|
|
2812
|
+
const dy = PADDING - minY;
|
|
2813
|
+
for (const n of nodes) {
|
|
2814
|
+
n.x += dx;
|
|
2815
|
+
n.y += dy;
|
|
2816
|
+
}
|
|
2817
|
+
return { width: maxX - minX + PADDING * 2, height: maxY - minY + PADDING * 2 };
|
|
2818
|
+
}
|
|
2819
|
+
function buildEdges(root, byId) {
|
|
2820
|
+
const edges = [];
|
|
2821
|
+
const walk = (parent) => {
|
|
2822
|
+
const pln = byId.get(parent.id);
|
|
2823
|
+
for (const c of parent.children) {
|
|
2824
|
+
const cln = byId.get(c.id);
|
|
2825
|
+
let fromX;
|
|
2826
|
+
if (pln.side === "center") {
|
|
2827
|
+
const halfW = pln.labelWidth / 2;
|
|
2828
|
+
fromX = cln.side === "left" ? pln.x - halfW : pln.x + halfW;
|
|
2829
|
+
} else {
|
|
2830
|
+
fromX = labelEdgeX(pln, true);
|
|
2831
|
+
}
|
|
2832
|
+
const toX = labelEdgeX(cln, false);
|
|
2833
|
+
edges.push({
|
|
2834
|
+
from: parent.id,
|
|
2835
|
+
to: c.id,
|
|
2836
|
+
path: bezierH(fromX, pln.y, toX, cln.y),
|
|
2837
|
+
color: "",
|
|
2838
|
+
width: edgeWidthFor(c.depth)
|
|
2839
|
+
});
|
|
2840
|
+
walk(c);
|
|
2841
|
+
}
|
|
2842
|
+
};
|
|
2843
|
+
walk(root);
|
|
2844
|
+
return edges;
|
|
2845
|
+
}
|
|
2846
|
+
function layoutMap(ast) {
|
|
2847
|
+
const root = ast.root;
|
|
2848
|
+
const mains = root.children;
|
|
2849
|
+
const rightCount = Math.ceil(mains.length / 2);
|
|
2850
|
+
const rightMains = mains.slice(0, rightCount);
|
|
2851
|
+
const leftMains = mains.slice(rightCount);
|
|
2852
|
+
const nodes = [];
|
|
2853
|
+
const rootLabelW = estimateLabelWidth2(root.label, 0);
|
|
2854
|
+
const rootCapsuleW = rootLabelW + ROOT_CAPSULE_PAD_X * 2;
|
|
2855
|
+
const rootCapsuleH = rowHeightOf(0) + ROOT_CAPSULE_PAD_Y;
|
|
2856
|
+
const firstColLeft = rootCapsuleW / 2 + bezierGapFor(1);
|
|
2857
|
+
const rightCols = computeColumns(rightMains, firstColLeft);
|
|
2858
|
+
let rightCursor = 0;
|
|
2859
|
+
for (let i = 0; i < rightMains.length; i++) {
|
|
2860
|
+
if (i > 0) rightCursor += MAIN_GAP;
|
|
2861
|
+
const { height: height2 } = tidyRight(rightMains[i], rightCursor, i, rightCols, nodes);
|
|
2862
|
+
rightCursor += height2;
|
|
2863
|
+
}
|
|
2864
|
+
const rightHeight = rightCursor;
|
|
2865
|
+
const leftCols = computeColumns(leftMains, firstColLeft);
|
|
2866
|
+
const leftStart = nodes.length;
|
|
2867
|
+
let leftCursor = 0;
|
|
2868
|
+
for (let i = 0; i < leftMains.length; i++) {
|
|
2869
|
+
if (i > 0) leftCursor += MAIN_GAP;
|
|
2870
|
+
const { height: height2 } = tidyRight(leftMains[i], leftCursor, rightCount + i, leftCols, nodes);
|
|
2871
|
+
leftCursor += height2;
|
|
2872
|
+
}
|
|
2873
|
+
const leftHeight = leftCursor;
|
|
2874
|
+
for (let k = leftStart; k < nodes.length; k++) {
|
|
2875
|
+
nodes[k].x = -nodes[k].x;
|
|
2876
|
+
nodes[k].side = "left";
|
|
2877
|
+
}
|
|
2878
|
+
const rootY = Math.max(rightHeight, leftHeight) / 2;
|
|
2879
|
+
const rootNode = {
|
|
2880
|
+
node: root,
|
|
2881
|
+
x: 0,
|
|
2882
|
+
y: rootY,
|
|
2883
|
+
side: "center",
|
|
2884
|
+
branchIndex: -1,
|
|
2885
|
+
labelWidth: rootCapsuleW,
|
|
2886
|
+
labelHeight: rootCapsuleH
|
|
2887
|
+
};
|
|
2888
|
+
nodes.push(rootNode);
|
|
2889
|
+
const { width, height } = normalize(nodes);
|
|
2890
|
+
const byId = new Map(nodes.map((n) => [n.node.id, n]));
|
|
2891
|
+
const edges = buildEdges(root, byId);
|
|
2892
|
+
return { width, height, style: "map", nodes, edges, title: ast.title };
|
|
2893
|
+
}
|
|
2894
|
+
function layoutLogicRight(ast) {
|
|
2895
|
+
const root = ast.root;
|
|
2896
|
+
const nodes = [];
|
|
2897
|
+
const rootLabelW = estimateLabelWidth2(root.label, 0);
|
|
2898
|
+
const rootCapsuleW = rootLabelW + ROOT_CAPSULE_PAD_X * 2;
|
|
2899
|
+
const rootCapsuleH = rowHeightOf(0) + ROOT_CAPSULE_PAD_Y;
|
|
2900
|
+
const firstColLeft = rootCapsuleW / 2 + bezierGapFor(1);
|
|
2901
|
+
const cols = computeColumns(root.children, firstColLeft);
|
|
2902
|
+
let cursor = 0;
|
|
2903
|
+
for (let i = 0; i < root.children.length; i++) {
|
|
2904
|
+
if (i > 0) cursor += MAIN_GAP;
|
|
2905
|
+
const { height: height2 } = tidyRight(root.children[i], cursor, i, cols, nodes);
|
|
2906
|
+
cursor += height2;
|
|
2907
|
+
}
|
|
2908
|
+
const totalHeight = cursor;
|
|
2909
|
+
const rootNode = {
|
|
2910
|
+
node: root,
|
|
2911
|
+
x: 0,
|
|
2912
|
+
y: totalHeight / 2,
|
|
2913
|
+
side: "center",
|
|
2914
|
+
branchIndex: -1,
|
|
2915
|
+
labelWidth: rootCapsuleW,
|
|
2916
|
+
labelHeight: rootCapsuleH
|
|
2917
|
+
};
|
|
2918
|
+
nodes.push(rootNode);
|
|
2919
|
+
const { width, height } = normalize(nodes);
|
|
2920
|
+
const byId = new Map(nodes.map((n) => [n.node.id, n]));
|
|
2921
|
+
const edges = buildEdges(root, byId);
|
|
2922
|
+
return { width, height, style: "logic-right", nodes, edges, title: ast.title };
|
|
2923
|
+
}
|
|
2924
|
+
function layoutMindmap(ast) {
|
|
2925
|
+
const style = ast.style;
|
|
2926
|
+
if (style === "logic-right") return layoutLogicRight(ast);
|
|
2927
|
+
return layoutMap(ast);
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2930
|
+
// src/diagrams/mindmap/renderer.ts
|
|
2931
|
+
var UNDERLINE_MAIN = 2.2;
|
|
2932
|
+
var UNDERLINE_MAIN_MONO = 1.5;
|
|
2933
|
+
function paletteColor(theme, branchIndex) {
|
|
2934
|
+
if (branchIndex < 0) return theme.centralFill;
|
|
2935
|
+
return theme.branchPalette[branchIndex % theme.branchPalette.length];
|
|
2936
|
+
}
|
|
2937
|
+
function underlineMain(theme) {
|
|
2938
|
+
return theme.branchPalette.length <= 1 ? UNDERLINE_MAIN_MONO : UNDERLINE_MAIN;
|
|
2939
|
+
}
|
|
2940
|
+
function renderCentral(n, theme, fontFamily) {
|
|
2941
|
+
const fs = fontSizeOf(0);
|
|
2942
|
+
const pillW = n.labelWidth;
|
|
2943
|
+
const pillH = n.labelHeight;
|
|
2944
|
+
return group(
|
|
2945
|
+
{ class: "schematex-mindmap-central", "data-node-id": n.node.id },
|
|
2946
|
+
[
|
|
2947
|
+
rect({
|
|
2948
|
+
x: n.x - pillW / 2,
|
|
2949
|
+
y: n.y - pillH / 2,
|
|
2950
|
+
width: pillW,
|
|
2951
|
+
height: pillH,
|
|
2952
|
+
rx: pillH / 2,
|
|
2953
|
+
ry: pillH / 2,
|
|
2954
|
+
fill: "none",
|
|
2955
|
+
stroke: theme.textMuted,
|
|
2956
|
+
"stroke-width": underlineMain(theme)
|
|
2957
|
+
}),
|
|
2958
|
+
text(
|
|
2959
|
+
{
|
|
2960
|
+
x: n.x,
|
|
2961
|
+
y: n.y + fs * 0.35,
|
|
2962
|
+
"text-anchor": "middle",
|
|
2963
|
+
"font-family": fontFamily,
|
|
2964
|
+
"font-size": fs,
|
|
2965
|
+
"font-weight": 700,
|
|
2966
|
+
fill: theme.text
|
|
2967
|
+
},
|
|
2968
|
+
n.node.label
|
|
2969
|
+
)
|
|
2970
|
+
]
|
|
2971
|
+
);
|
|
2972
|
+
}
|
|
2973
|
+
function renderBranchNode(n, color, theme, fontFamily) {
|
|
2974
|
+
const isMain = n.node.depth === 1;
|
|
2975
|
+
const fs = fontSizeOf(n.node.depth);
|
|
2976
|
+
const tx = n.x;
|
|
2977
|
+
const ty = isMain ? n.y - 3 : n.y + fs * 0.35;
|
|
2978
|
+
const ux1 = n.x - n.labelWidth / 2;
|
|
2979
|
+
const ux2 = n.x + n.labelWidth / 2;
|
|
2980
|
+
const uy = n.y;
|
|
2981
|
+
const children = [
|
|
2982
|
+
text(
|
|
2983
|
+
{
|
|
2984
|
+
x: tx,
|
|
2985
|
+
y: ty,
|
|
2986
|
+
"text-anchor": "middle",
|
|
2987
|
+
"font-family": fontFamily,
|
|
2988
|
+
"font-size": fs,
|
|
2989
|
+
"font-weight": isMain ? 600 : 400,
|
|
2990
|
+
fill: theme.text
|
|
2991
|
+
},
|
|
2992
|
+
n.node.label
|
|
2993
|
+
)
|
|
2994
|
+
];
|
|
2995
|
+
if (isMain) {
|
|
2996
|
+
children.push(
|
|
2997
|
+
el("line", {
|
|
2998
|
+
x1: ux1,
|
|
2999
|
+
y1: uy,
|
|
3000
|
+
x2: ux2,
|
|
3001
|
+
y2: uy,
|
|
3002
|
+
stroke: color,
|
|
3003
|
+
"stroke-width": underlineMain(theme),
|
|
3004
|
+
"stroke-linecap": "round"
|
|
3005
|
+
})
|
|
3006
|
+
);
|
|
3007
|
+
}
|
|
3008
|
+
return group(
|
|
3009
|
+
{
|
|
3010
|
+
class: isMain ? "schematex-mindmap-main" : "schematex-mindmap-leaf",
|
|
3011
|
+
"data-node-id": n.node.id,
|
|
3012
|
+
"data-depth": n.node.depth,
|
|
3013
|
+
"data-branch-idx": n.branchIndex
|
|
3014
|
+
},
|
|
3015
|
+
children
|
|
3016
|
+
);
|
|
3017
|
+
}
|
|
3018
|
+
function renderMindmapAST(ast, themeName = "default", fontFamily = "system-ui, -apple-system, sans-serif") {
|
|
3019
|
+
const theme = resolveMindmapTheme(ast.themeOverride ?? themeName);
|
|
3020
|
+
const layout = layoutMindmap(ast);
|
|
3021
|
+
const byId = new Map(layout.nodes.map((n) => [n.node.id, n]));
|
|
3022
|
+
const edgeSvgs = [];
|
|
3023
|
+
for (const e of layout.edges) {
|
|
3024
|
+
const target = byId.get(e.to);
|
|
3025
|
+
if (!target) continue;
|
|
3026
|
+
const color = paletteColor(theme, target.branchIndex);
|
|
3027
|
+
edgeSvgs.push(
|
|
3028
|
+
path({
|
|
3029
|
+
d: e.path,
|
|
3030
|
+
fill: "none",
|
|
3031
|
+
stroke: color,
|
|
3032
|
+
"stroke-width": e.width,
|
|
3033
|
+
"stroke-linecap": "round"
|
|
3034
|
+
})
|
|
3035
|
+
);
|
|
3036
|
+
}
|
|
3037
|
+
const nodeSvgs = [];
|
|
3038
|
+
for (const n of layout.nodes) {
|
|
3039
|
+
if (n.node.depth === 0) {
|
|
3040
|
+
nodeSvgs.push(renderCentral(n, theme, fontFamily));
|
|
3041
|
+
} else {
|
|
3042
|
+
nodeSvgs.push(
|
|
3043
|
+
renderBranchNode(n, paletteColor(theme, n.branchIndex), theme, fontFamily)
|
|
3044
|
+
);
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
const title2 = ast.title ?? ast.root.label;
|
|
3048
|
+
return svgRoot(
|
|
3049
|
+
{
|
|
3050
|
+
viewBox: `0 0 ${layout.width.toFixed(1)} ${layout.height.toFixed(1)}`,
|
|
3051
|
+
width: layout.width.toFixed(1),
|
|
3052
|
+
height: layout.height.toFixed(1),
|
|
3053
|
+
role: "graphics-document",
|
|
3054
|
+
"aria-label": `Mindmap: ${escapeXml(title2)}`
|
|
3055
|
+
},
|
|
3056
|
+
[
|
|
3057
|
+
title(title2),
|
|
3058
|
+
desc(`${layout.style} mindmap with ${layout.nodes.length} nodes`),
|
|
3059
|
+
rect({ x: 0, y: 0, width: layout.width, height: layout.height, fill: theme.bg }),
|
|
3060
|
+
group({ class: "schematex-mindmap-edges", "aria-hidden": "true" }, edgeSvgs),
|
|
3061
|
+
group({ class: "schematex-mindmap-nodes" }, nodeSvgs)
|
|
3062
|
+
]
|
|
3063
|
+
);
|
|
3064
|
+
}
|
|
3065
|
+
function renderMindmap(text2, opts) {
|
|
3066
|
+
const ast = parseMindmap(text2);
|
|
3067
|
+
return renderMindmapAST(ast, opts?.theme, opts?.fontFamily);
|
|
3068
|
+
}
|
|
3069
|
+
|
|
3070
|
+
// src/diagrams/mindmap/index.ts
|
|
3071
|
+
var mindmap = {
|
|
3072
|
+
type: "mindmap",
|
|
3073
|
+
detect(text2) {
|
|
3074
|
+
const lines = text2.trim().split("\n");
|
|
3075
|
+
const first = lines[0]?.trim().toLowerCase() ?? "";
|
|
3076
|
+
if (first.startsWith("mindmap")) return true;
|
|
3077
|
+
for (const ln of lines) {
|
|
3078
|
+
const t = ln.trim();
|
|
3079
|
+
if (!t) continue;
|
|
3080
|
+
if (t.startsWith("%%")) continue;
|
|
3081
|
+
return /^#\s+\S/.test(t);
|
|
3082
|
+
}
|
|
3083
|
+
return false;
|
|
3084
|
+
},
|
|
3085
|
+
parse: parseMindmap,
|
|
3086
|
+
render(text2, config) {
|
|
3087
|
+
return renderMindmap(text2, {
|
|
3088
|
+
theme: config?.theme,
|
|
3089
|
+
fontFamily: config?.fontFamily
|
|
3090
|
+
});
|
|
3091
|
+
}
|
|
3092
|
+
};
|
|
3093
|
+
|
|
3094
|
+
// src/diagrams/matrix/templates.ts
|
|
3095
|
+
var TEMPLATES = {
|
|
3096
|
+
"eisenhower": {
|
|
3097
|
+
grid: "2x2",
|
|
3098
|
+
xAxis: { low: "Urgent", high: "Not Urgent" },
|
|
3099
|
+
yAxis: { low: "Not Important", high: "Important" },
|
|
3100
|
+
annotations: [
|
|
3101
|
+
{ q: 1, label: "Schedule" },
|
|
3102
|
+
{ q: 2, label: "Do First" },
|
|
3103
|
+
{ q: 3, label: "Delete" },
|
|
3104
|
+
{ q: 4, label: "Delegate" }
|
|
3105
|
+
]
|
|
3106
|
+
},
|
|
3107
|
+
"impact-effort": {
|
|
3108
|
+
grid: "2x2",
|
|
3109
|
+
xAxis: { low: "Low Effort", high: "High Effort" },
|
|
3110
|
+
yAxis: { low: "Low Impact", high: "High Impact" },
|
|
3111
|
+
annotations: [
|
|
3112
|
+
{ q: 1, label: "Major Projects" },
|
|
3113
|
+
{ q: 2, label: "Quick Wins" },
|
|
3114
|
+
{ q: 3, label: "Fill-ins" },
|
|
3115
|
+
{ q: 4, label: "Thankless" }
|
|
3116
|
+
]
|
|
3117
|
+
},
|
|
3118
|
+
"rice": {
|
|
3119
|
+
grid: "2x2",
|
|
3120
|
+
xAxis: { low: "Low Effort", high: "High Effort" },
|
|
3121
|
+
yAxis: { low: "Low Reach \xD7 Impact", high: "High Reach \xD7 Impact" },
|
|
3122
|
+
annotations: [
|
|
3123
|
+
{ q: 1, label: "Strategic Bets" },
|
|
3124
|
+
{ q: 2, label: "High RICE" },
|
|
3125
|
+
{ q: 3, label: "Backlog" },
|
|
3126
|
+
{ q: 4, label: "Reconsider" }
|
|
3127
|
+
]
|
|
3128
|
+
},
|
|
3129
|
+
"bcg": {
|
|
3130
|
+
grid: "2x2",
|
|
3131
|
+
// BCG convention: high market share on left (x-axis reversed)
|
|
3132
|
+
xAxis: { low: "High Market Share", high: "Low Market Share" },
|
|
3133
|
+
yAxis: { low: "Low Growth", high: "High Growth" },
|
|
3134
|
+
annotations: [
|
|
3135
|
+
{ q: 1, label: "Question Marks" },
|
|
3136
|
+
{ q: 2, label: "Stars" },
|
|
3137
|
+
{ q: 3, label: "Cash Cows" },
|
|
3138
|
+
{ q: 4, label: "Dogs" }
|
|
3139
|
+
]
|
|
3140
|
+
},
|
|
3141
|
+
"ansoff": {
|
|
3142
|
+
grid: "2x2",
|
|
3143
|
+
xAxis: { low: "Existing Products", high: "New Products" },
|
|
3144
|
+
yAxis: { low: "Existing Markets", high: "New Markets" },
|
|
3145
|
+
annotations: [
|
|
3146
|
+
{ q: 1, label: "Diversification" },
|
|
3147
|
+
{ q: 2, label: "Market Development" },
|
|
3148
|
+
{ q: 3, label: "Market Penetration" },
|
|
3149
|
+
{ q: 4, label: "Product Development" }
|
|
3150
|
+
]
|
|
3151
|
+
},
|
|
3152
|
+
"johari": {
|
|
3153
|
+
grid: "2x2",
|
|
3154
|
+
xAxis: { low: "Known to Self", high: "Not Known to Self" },
|
|
3155
|
+
yAxis: { low: "Not Known to Others", high: "Known to Others" },
|
|
3156
|
+
annotations: [
|
|
3157
|
+
{ q: 1, label: "Blind" },
|
|
3158
|
+
{ q: 2, label: "Open / Arena" },
|
|
3159
|
+
{ q: 3, label: "Hidden / Fa\xE7ade" },
|
|
3160
|
+
{ q: 4, label: "Unknown" }
|
|
3161
|
+
]
|
|
3162
|
+
},
|
|
3163
|
+
"9-box": {
|
|
3164
|
+
grid: "3x3",
|
|
3165
|
+
cols: 3,
|
|
3166
|
+
rows: 3,
|
|
3167
|
+
xAxis: { low: "Low Performance", high: "High Performance" },
|
|
3168
|
+
yAxis: { low: "Low Potential", high: "High Potential" },
|
|
3169
|
+
cellLabels: [
|
|
3170
|
+
{ col: 0, row: 2, label: "Enigma" },
|
|
3171
|
+
{ col: 1, row: 2, label: "Growth Employee" },
|
|
3172
|
+
{ col: 2, row: 2, label: "Future Leader" },
|
|
3173
|
+
{ col: 0, row: 1, label: "Dilemma" },
|
|
3174
|
+
{ col: 1, row: 1, label: "Core Player" },
|
|
3175
|
+
{ col: 2, row: 1, label: "High Impact" },
|
|
3176
|
+
{ col: 0, row: 0, label: "Under-performer" },
|
|
3177
|
+
{ col: 1, row: 0, label: "Effective" },
|
|
3178
|
+
{ col: 2, row: 0, label: "Trusted Pro" }
|
|
3179
|
+
]
|
|
3180
|
+
},
|
|
3181
|
+
"risk-matrix": {
|
|
3182
|
+
grid: "NxM",
|
|
3183
|
+
mode: "heatmap",
|
|
3184
|
+
cols: 5,
|
|
3185
|
+
rows: 5,
|
|
3186
|
+
xAxis: { low: "Negligible", high: "Severe" },
|
|
3187
|
+
yAxis: { low: "Rare", high: "Certain" },
|
|
3188
|
+
rowLabels: ["Rare", "Unlikely", "Possible", "Likely", "Certain"],
|
|
3189
|
+
colLabels: ["Negligible", "Minor", "Moderate", "Major", "Severe"]
|
|
3190
|
+
}
|
|
3191
|
+
};
|
|
3192
|
+
function resolveTemplate(name) {
|
|
3193
|
+
if (name in TEMPLATES) return TEMPLATES[name];
|
|
3194
|
+
return void 0;
|
|
3195
|
+
}
|
|
3196
|
+
function applyTemplateDefaults(ast, spec) {
|
|
3197
|
+
if (spec.grid === "3x3") {
|
|
3198
|
+
ast.grid = "3x3";
|
|
3199
|
+
ast.cols = 3;
|
|
3200
|
+
ast.rows = 3;
|
|
3201
|
+
} else if (spec.grid === "NxM") {
|
|
3202
|
+
ast.grid = "NxM";
|
|
3203
|
+
ast.cols = spec.cols ?? 5;
|
|
3204
|
+
ast.rows = spec.rows ?? 5;
|
|
3205
|
+
} else {
|
|
3206
|
+
ast.grid = "2x2";
|
|
3207
|
+
ast.cols = 2;
|
|
3208
|
+
ast.rows = 2;
|
|
3209
|
+
}
|
|
3210
|
+
if (spec.mode) ast.mode = spec.mode;
|
|
3211
|
+
if (!ast.xAxis.low && !ast.xAxis.high) ast.xAxis = { ...spec.xAxis };
|
|
3212
|
+
if (!ast.yAxis.low && !ast.yAxis.high) ast.yAxis = { ...spec.yAxis };
|
|
3213
|
+
if (spec.annotations && ast.annotations.length === 0) {
|
|
3214
|
+
ast.annotations = spec.annotations.map((a) => ({ ...a }));
|
|
3215
|
+
}
|
|
3216
|
+
if (spec.cellLabels && ast.cellLabels.length === 0) {
|
|
3217
|
+
ast.cellLabels = spec.cellLabels.map((c) => ({ ...c }));
|
|
3218
|
+
}
|
|
3219
|
+
if (spec.rowLabels && !ast.rowLabels) ast.rowLabels = [...spec.rowLabels];
|
|
3220
|
+
if (spec.colLabels && !ast.colLabels) ast.colLabels = [...spec.colLabels];
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3223
|
+
// src/diagrams/matrix/parser.ts
|
|
3224
|
+
var TEMPLATE_NAMES = /* @__PURE__ */ new Set([
|
|
3225
|
+
"eisenhower",
|
|
3226
|
+
"impact-effort",
|
|
3227
|
+
"rice",
|
|
3228
|
+
"bcg",
|
|
3229
|
+
"ansoff",
|
|
3230
|
+
"johari",
|
|
3231
|
+
"9-box",
|
|
3232
|
+
"risk-matrix"
|
|
3233
|
+
]);
|
|
3234
|
+
var DEFAULT_CONFIG = {
|
|
3235
|
+
quadrantBg: true,
|
|
3236
|
+
gridLines: true,
|
|
3237
|
+
axisArrows: true,
|
|
3238
|
+
labelCollision: "auto",
|
|
3239
|
+
bubbleScale: "area",
|
|
3240
|
+
quadrantAnnotations: true,
|
|
3241
|
+
legendPosition: "bottom-right",
|
|
3242
|
+
offChartPolicy: "clamp-badge",
|
|
3243
|
+
showAxis: "auto",
|
|
3244
|
+
margins: false
|
|
3245
|
+
};
|
|
3246
|
+
function emptyAxis() {
|
|
3247
|
+
return { low: "", high: "" };
|
|
3248
|
+
}
|
|
3249
|
+
function newAST() {
|
|
3250
|
+
return {
|
|
3251
|
+
type: "matrix",
|
|
3252
|
+
mode: "quadrant",
|
|
3253
|
+
grid: "2x2",
|
|
3254
|
+
cols: 2,
|
|
3255
|
+
rows: 2,
|
|
3256
|
+
xAxis: emptyAxis(),
|
|
3257
|
+
yAxis: emptyAxis(),
|
|
3258
|
+
points: [],
|
|
3259
|
+
cells: [],
|
|
3260
|
+
cellLabels: [],
|
|
3261
|
+
annotations: [],
|
|
3262
|
+
config: { ...DEFAULT_CONFIG }
|
|
3263
|
+
};
|
|
3264
|
+
}
|
|
3265
|
+
function stripQuotes(s) {
|
|
3266
|
+
const t = s.trim();
|
|
3267
|
+
if (t.startsWith('"') && t.endsWith('"') || t.startsWith("'") && t.endsWith("'")) {
|
|
3268
|
+
return t.slice(1, -1);
|
|
3269
|
+
}
|
|
3270
|
+
return t;
|
|
3271
|
+
}
|
|
3272
|
+
function readQuoted2(line2, from) {
|
|
3273
|
+
let i = from;
|
|
3274
|
+
while (i < line2.length && /\s/.test(line2[i])) i++;
|
|
3275
|
+
if (line2[i] !== '"' && line2[i] !== "'") return null;
|
|
3276
|
+
const quote = line2[i];
|
|
3277
|
+
const start = i + 1;
|
|
3278
|
+
let end = start;
|
|
3279
|
+
while (end < line2.length && line2[end] !== quote) end++;
|
|
3280
|
+
if (end >= line2.length) return null;
|
|
3281
|
+
return { text: line2.slice(start, end), next: end + 1 };
|
|
3282
|
+
}
|
|
3283
|
+
function parseAxis(raw) {
|
|
3284
|
+
const arrowMatch = raw.match(/\s*(→|↑|->|>|↓|←|<-|<)\s*/);
|
|
3285
|
+
if (arrowMatch) {
|
|
3286
|
+
const arrow = arrowMatch[1];
|
|
3287
|
+
const idx = arrowMatch.index;
|
|
3288
|
+
const left = raw.slice(0, idx).trim();
|
|
3289
|
+
const right = raw.slice(idx + arrowMatch[0].length).trim();
|
|
3290
|
+
const reversed = arrow === "\u2190" || arrow === "<-" || arrow === "<";
|
|
3291
|
+
if (reversed) {
|
|
3292
|
+
return { low: right, high: left, reversed: true };
|
|
3293
|
+
}
|
|
3294
|
+
return { low: left, high: right };
|
|
3295
|
+
}
|
|
3296
|
+
return { low: "", high: raw.trim() };
|
|
3297
|
+
}
|
|
3298
|
+
function parseNumberList2(raw) {
|
|
3299
|
+
const t = raw.trim();
|
|
3300
|
+
const inner = t.startsWith("[") && t.endsWith("]") ? t.slice(1, -1) : t;
|
|
3301
|
+
return inner.split(",").map((s) => stripQuotes(s.trim())).filter((s) => s.length > 0);
|
|
3302
|
+
}
|
|
3303
|
+
function parseProperties2(raw, point) {
|
|
3304
|
+
let i = 0;
|
|
3305
|
+
while (i < raw.length) {
|
|
3306
|
+
while (i < raw.length && /\s/.test(raw[i])) i++;
|
|
3307
|
+
if (i >= raw.length) break;
|
|
3308
|
+
const keyMatch = raw.slice(i).match(/^([a-zA-Z_-]+)\s*:\s*/);
|
|
3309
|
+
if (!keyMatch) break;
|
|
3310
|
+
const key = keyMatch[1].toLowerCase();
|
|
3311
|
+
i += keyMatch[0].length;
|
|
3312
|
+
if (raw[i] === '"' || raw[i] === "'") {
|
|
3313
|
+
const q = readQuoted2(raw, i);
|
|
3314
|
+
if (!q) break;
|
|
3315
|
+
if (key === "note") point.note = q.text;
|
|
3316
|
+
else if (key === "label") point.label = q.text;
|
|
3317
|
+
i = q.next;
|
|
3318
|
+
} else {
|
|
3319
|
+
const rest = raw.slice(i);
|
|
3320
|
+
const endMatch = rest.match(/\s+[a-zA-Z_-]+\s*:/);
|
|
3321
|
+
const end = endMatch ? endMatch.index : rest.length;
|
|
3322
|
+
const val = rest.slice(0, end).trim();
|
|
3323
|
+
i += end;
|
|
3324
|
+
if (key === "size") {
|
|
3325
|
+
const n = Number(val);
|
|
3326
|
+
if (!Number.isNaN(n)) point.size = n;
|
|
3327
|
+
} else if (key === "category") {
|
|
3328
|
+
point.category = val;
|
|
3329
|
+
} else if (key === "color") {
|
|
3330
|
+
point.color = val;
|
|
3331
|
+
} else if (key === "shape") {
|
|
3332
|
+
if (val === "circle" || val === "square" || val === "triangle" || val === "diamond") {
|
|
3333
|
+
point.shape = val;
|
|
3334
|
+
}
|
|
3335
|
+
} else if (key === "highlight") {
|
|
3336
|
+
point.highlight = val === "true" || val === "1";
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
function parsePointLine(line2, st) {
|
|
3342
|
+
const q = readQuoted2(line2, 0);
|
|
3343
|
+
if (!q) return false;
|
|
3344
|
+
const rest = line2.slice(q.next).trim();
|
|
3345
|
+
const atMatch = rest.match(/^at\s*\(\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\)\s*(.*)$/);
|
|
3346
|
+
if (!atMatch) return false;
|
|
3347
|
+
const x = Number(atMatch[1]);
|
|
3348
|
+
const y = Number(atMatch[2]);
|
|
3349
|
+
const props = atMatch[3];
|
|
3350
|
+
const offChart = x < 0 || x > 1 || y < 0 || y > 1;
|
|
3351
|
+
const clampedX = Math.max(0, Math.min(1, x));
|
|
3352
|
+
const clampedY = Math.max(0, Math.min(1, y));
|
|
3353
|
+
const point = {
|
|
3354
|
+
id: `p${st.pointIdSeq++}`,
|
|
3355
|
+
label: q.text,
|
|
3356
|
+
x: clampedX,
|
|
3357
|
+
y: clampedY,
|
|
3358
|
+
offChart,
|
|
3359
|
+
origX: offChart ? x : void 0,
|
|
3360
|
+
origY: offChart ? y : void 0
|
|
3361
|
+
};
|
|
3362
|
+
if (props) parseProperties2(props, point);
|
|
3363
|
+
st.ast.points.push(point);
|
|
3364
|
+
return true;
|
|
3365
|
+
}
|
|
3366
|
+
function parseCellLine(line2, st) {
|
|
3367
|
+
const m = line2.match(/^cell\s*\(\s*(\d+)\s*,\s*(\d+)\s*\)\s*(.*)$/i);
|
|
3368
|
+
if (!m) return false;
|
|
3369
|
+
const col = Number(m[1]);
|
|
3370
|
+
const row = Number(m[2]);
|
|
3371
|
+
const rest = m[3];
|
|
3372
|
+
const cell = { col, row };
|
|
3373
|
+
const valMatch = rest.match(/value:\s*(-?\d+(?:\.\d+)?)/i);
|
|
3374
|
+
if (valMatch) cell.value = Number(valMatch[1]);
|
|
3375
|
+
const labMatch = rest.match(/label:\s*"([^"]*)"/i);
|
|
3376
|
+
if (labMatch) cell.label = labMatch[1];
|
|
3377
|
+
const lvlMatch = rest.match(/level:\s*(strong|medium|weak)\b/i);
|
|
3378
|
+
if (lvlMatch) {
|
|
3379
|
+
const lv = lvlMatch[1].toLowerCase();
|
|
3380
|
+
cell.level = lv;
|
|
3381
|
+
if (cell.value === void 0) cell.value = lv === "strong" ? 3 : lv === "medium" ? 2 : 1;
|
|
3382
|
+
}
|
|
3383
|
+
st.ast.cells.push(cell);
|
|
3384
|
+
if (cell.label) {
|
|
3385
|
+
st.ast.cellLabels.push({ col, row, label: cell.label });
|
|
3386
|
+
}
|
|
3387
|
+
return true;
|
|
3388
|
+
}
|
|
3389
|
+
function parseConfigLine(key, value, ast) {
|
|
3390
|
+
const k = key.trim().toLowerCase();
|
|
3391
|
+
const v = value.trim();
|
|
3392
|
+
if (k === "quadrantbg") ast.config.quadrantBg = v === "true";
|
|
3393
|
+
else if (k === "gridlines") ast.config.gridLines = v === "true";
|
|
3394
|
+
else if (k === "axisarrows") ast.config.axisArrows = v === "true";
|
|
3395
|
+
else if (k === "labelcollision") ast.config.labelCollision = v.replace(/"/g, "");
|
|
3396
|
+
else if (k === "bubblescale") ast.config.bubbleScale = v === "radius" ? "radius" : "area";
|
|
3397
|
+
else if (k === "quadrantannotations") ast.config.quadrantAnnotations = v === "true";
|
|
3398
|
+
else if (k === "legendposition") {
|
|
3399
|
+
const t = v.replace(/"/g, "");
|
|
3400
|
+
ast.config.legendPosition = t;
|
|
3401
|
+
} else if (k === "offchartpolicy") {
|
|
3402
|
+
const t = v.replace(/"/g, "");
|
|
3403
|
+
ast.config.offChartPolicy = t;
|
|
3404
|
+
}
|
|
3405
|
+
}
|
|
3406
|
+
function parseHeader(line2, ast) {
|
|
3407
|
+
const t = line2.trim();
|
|
3408
|
+
const rest = t.slice("matrix".length).trim();
|
|
3409
|
+
const heatMatch = rest.match(/^heatmap\s+(\d+)\s*x\s*(\d+)\s*(.*)$/i);
|
|
3410
|
+
if (heatMatch) {
|
|
3411
|
+
ast.mode = "heatmap";
|
|
3412
|
+
ast.grid = "NxM";
|
|
3413
|
+
ast.cols = Number(heatMatch[1]);
|
|
3414
|
+
ast.rows = Number(heatMatch[2]);
|
|
3415
|
+
const title2 = heatMatch[3].trim();
|
|
3416
|
+
if (title2) ast.title = stripQuotes(title2);
|
|
3417
|
+
return void 0;
|
|
3418
|
+
}
|
|
3419
|
+
const corrMatch = rest.match(/^correlation\s*(?:(\d+)\s*x\s*(\d+))?\s*(.*)$/i);
|
|
3420
|
+
if (corrMatch) {
|
|
3421
|
+
ast.mode = "correlation";
|
|
3422
|
+
ast.grid = "NxM";
|
|
3423
|
+
if (corrMatch[1] && corrMatch[2]) {
|
|
3424
|
+
ast.cols = Number(corrMatch[1]);
|
|
3425
|
+
ast.rows = Number(corrMatch[2]);
|
|
3426
|
+
}
|
|
3427
|
+
const title2 = corrMatch[3].trim();
|
|
3428
|
+
if (title2) ast.title = stripQuotes(title2);
|
|
3429
|
+
return void 0;
|
|
3430
|
+
}
|
|
3431
|
+
const tokenMatch = rest.match(/^([a-zA-Z0-9_-]+)\s*(.*)$/);
|
|
3432
|
+
if (tokenMatch) {
|
|
3433
|
+
const tok = tokenMatch[1].toLowerCase();
|
|
3434
|
+
const remainder = tokenMatch[2].trim();
|
|
3435
|
+
if (TEMPLATE_NAMES.has(tok)) {
|
|
3436
|
+
if (remainder) ast.title = stripQuotes(remainder);
|
|
3437
|
+
return tok;
|
|
3438
|
+
}
|
|
3439
|
+
if (rest.startsWith('"') || rest.startsWith("'")) {
|
|
3440
|
+
ast.title = stripQuotes(rest);
|
|
3441
|
+
} else if (rest.length > 0) {
|
|
3442
|
+
ast.title = stripQuotes(rest);
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3445
|
+
return void 0;
|
|
3446
|
+
}
|
|
3447
|
+
function parseMatrix(text2) {
|
|
3448
|
+
const st = { ast: newAST(), pointIdSeq: 0 };
|
|
3449
|
+
const lines = text2.split(/\r?\n/);
|
|
3450
|
+
let templateName;
|
|
3451
|
+
let inConfig = false;
|
|
3452
|
+
for (let raw of lines) {
|
|
3453
|
+
let line2 = raw;
|
|
3454
|
+
const hashIdx = findCommentStart(line2);
|
|
3455
|
+
if (hashIdx >= 0) line2 = line2.slice(0, hashIdx);
|
|
3456
|
+
line2 = line2.trim();
|
|
3457
|
+
if (!line2) {
|
|
3458
|
+
inConfig = false;
|
|
3459
|
+
continue;
|
|
3460
|
+
}
|
|
3461
|
+
if (/^matrix\b/i.test(line2)) {
|
|
3462
|
+
templateName = parseHeader(line2, st.ast);
|
|
3463
|
+
continue;
|
|
3464
|
+
}
|
|
3465
|
+
if (/^config\s*:/i.test(line2)) {
|
|
3466
|
+
inConfig = true;
|
|
3467
|
+
continue;
|
|
3468
|
+
}
|
|
3469
|
+
if (inConfig) {
|
|
3470
|
+
const kv = line2.match(/^([a-zA-Z]+)\s*:\s*(.+)$/);
|
|
3471
|
+
if (kv) {
|
|
3472
|
+
parseConfigLine(kv[1], kv[2], st.ast);
|
|
3473
|
+
continue;
|
|
3474
|
+
}
|
|
3475
|
+
inConfig = false;
|
|
3476
|
+
}
|
|
3477
|
+
if (/^title\s*:/i.test(line2)) {
|
|
3478
|
+
st.ast.title = stripQuotes(line2.replace(/^title\s*:\s*/i, ""));
|
|
3479
|
+
continue;
|
|
3480
|
+
}
|
|
3481
|
+
if (/^x-axis\s*:/i.test(line2)) {
|
|
3482
|
+
st.ast.xAxis = parseAxis(line2.replace(/^x-axis\s*:\s*/i, ""));
|
|
3483
|
+
continue;
|
|
3484
|
+
}
|
|
3485
|
+
if (/^y-axis\s*:/i.test(line2)) {
|
|
3486
|
+
st.ast.yAxis = parseAxis(line2.replace(/^y-axis\s*:\s*/i, ""));
|
|
3487
|
+
continue;
|
|
3488
|
+
}
|
|
3489
|
+
if (/^rows\s*:/i.test(line2)) {
|
|
3490
|
+
st.ast.rowLabels = parseNumberList2(line2.replace(/^rows\s*:\s*/i, ""));
|
|
3491
|
+
if (st.ast.mode !== "quadrant") st.ast.rows = st.ast.rowLabels.length;
|
|
3492
|
+
continue;
|
|
3493
|
+
}
|
|
3494
|
+
if (/^cols\s*:/i.test(line2)) {
|
|
3495
|
+
st.ast.colLabels = parseNumberList2(line2.replace(/^cols\s*:\s*/i, ""));
|
|
3496
|
+
if (st.ast.mode !== "quadrant") st.ast.cols = st.ast.colLabels.length;
|
|
3497
|
+
continue;
|
|
3498
|
+
}
|
|
3499
|
+
if (/^grid\s*:/i.test(line2)) {
|
|
3500
|
+
const v = line2.replace(/^grid\s*:\s*/i, "").trim().toLowerCase();
|
|
3501
|
+
const gm = v.match(/^(\d+)\s*x\s*(\d+)$/);
|
|
3502
|
+
if (gm) {
|
|
3503
|
+
const c = Number(gm[1]);
|
|
3504
|
+
const r = Number(gm[2]);
|
|
3505
|
+
st.ast.cols = c;
|
|
3506
|
+
st.ast.rows = r;
|
|
3507
|
+
if (c === 2 && r === 2) st.ast.grid = "2x2";
|
|
3508
|
+
else if (c === 3 && r === 3) st.ast.grid = "3x3";
|
|
3509
|
+
else st.ast.grid = "NxM";
|
|
3510
|
+
}
|
|
3511
|
+
continue;
|
|
3512
|
+
}
|
|
3513
|
+
if (/^quadrant\s+/i.test(line2)) {
|
|
3514
|
+
const m = line2.match(/^quadrant\s+(?:Q)?(\d)\s*"([^"]*)"(.*)$/i);
|
|
3515
|
+
if (m) {
|
|
3516
|
+
const q = Number(m[1]);
|
|
3517
|
+
const rest = m[3].trim();
|
|
3518
|
+
const descMatch = rest.match(/description:\s*"([^"]*)"/i);
|
|
3519
|
+
const description = descMatch ? descMatch[1] : void 0;
|
|
3520
|
+
if (q >= 1 && q <= 4) {
|
|
3521
|
+
const existing = st.ast.annotations.find((a) => a.q === q);
|
|
3522
|
+
if (existing) {
|
|
3523
|
+
existing.label = m[2];
|
|
3524
|
+
if (description) existing.description = description;
|
|
3525
|
+
} else {
|
|
3526
|
+
st.ast.annotations.push({ q, label: m[2], description });
|
|
3527
|
+
}
|
|
3528
|
+
}
|
|
3529
|
+
}
|
|
3530
|
+
continue;
|
|
3531
|
+
}
|
|
3532
|
+
if (/^axis\s*:/i.test(line2)) {
|
|
3533
|
+
const v = line2.replace(/^axis\s*:\s*/i, "").trim().toLowerCase().replace(/"/g, "");
|
|
3534
|
+
if (v === "none" || v === "off" || v === "hidden") st.ast.config.showAxis = "off";
|
|
3535
|
+
else if (v === "on" || v === "show" || v === "visible") st.ast.config.showAxis = "on";
|
|
3536
|
+
else st.ast.config.showAxis = "auto";
|
|
3537
|
+
continue;
|
|
3538
|
+
}
|
|
3539
|
+
if (/^margins\s*:/i.test(line2)) {
|
|
3540
|
+
const v = line2.replace(/^margins\s*:\s*/i, "").trim().toLowerCase();
|
|
3541
|
+
st.ast.config.margins = v === "true" || v === "on" || v === "1";
|
|
3542
|
+
continue;
|
|
3543
|
+
}
|
|
3544
|
+
if (/^cell\s*\(/i.test(line2)) {
|
|
3545
|
+
parseCellLine(line2, st);
|
|
3546
|
+
continue;
|
|
3547
|
+
}
|
|
3548
|
+
if (line2.startsWith('"') || line2.startsWith("'")) {
|
|
3549
|
+
if (parsePointLine(line2, st)) continue;
|
|
3550
|
+
}
|
|
3551
|
+
}
|
|
3552
|
+
if (templateName) {
|
|
3553
|
+
const spec = resolveTemplate(templateName);
|
|
3554
|
+
if (spec) {
|
|
3555
|
+
applyTemplateDefaults(st.ast, spec);
|
|
3556
|
+
st.ast.template = templateName;
|
|
3557
|
+
}
|
|
3558
|
+
}
|
|
3559
|
+
if (st.ast.cols === 3 && st.ast.rows === 3 && st.ast.grid !== "NxM") {
|
|
3560
|
+
st.ast.grid = "3x3";
|
|
3561
|
+
}
|
|
3562
|
+
if (st.ast.mode === "heatmap" || st.ast.mode === "correlation") st.ast.grid = "NxM";
|
|
3563
|
+
return st.ast;
|
|
3564
|
+
}
|
|
3565
|
+
function findCommentStart(line2) {
|
|
3566
|
+
let inQuote = null;
|
|
3567
|
+
for (let i = 0; i < line2.length; i++) {
|
|
3568
|
+
const ch = line2[i];
|
|
3569
|
+
if (inQuote) {
|
|
3570
|
+
if (ch === inQuote) inQuote = null;
|
|
3571
|
+
} else if (ch === '"' || ch === "'") {
|
|
3572
|
+
inQuote = ch;
|
|
3573
|
+
} else if (ch === "#") {
|
|
3574
|
+
return i;
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3577
|
+
return -1;
|
|
3578
|
+
}
|
|
3579
|
+
|
|
3580
|
+
// src/diagrams/matrix/layout.ts
|
|
3581
|
+
var CANVAS_W = 720;
|
|
3582
|
+
var CANVAS_H = 560;
|
|
3583
|
+
var PADDING_X = 110;
|
|
3584
|
+
var PADDING_Y = 90;
|
|
3585
|
+
var CHAR_W = 6.2;
|
|
3586
|
+
var LABEL_H = 14;
|
|
3587
|
+
function estimateWidth(text2) {
|
|
3588
|
+
const cjk = (text2.match(/[\u3000-\u9fff]/g) ?? []).length;
|
|
3589
|
+
return (text2.length - cjk) * CHAR_W + cjk * 12 + 8;
|
|
3590
|
+
}
|
|
3591
|
+
function clamp01(v) {
|
|
3592
|
+
return Math.max(0.02, Math.min(0.98, v));
|
|
3593
|
+
}
|
|
3594
|
+
function placePoint(p, plot) {
|
|
3595
|
+
const nx = clamp01(p.x);
|
|
3596
|
+
const ny = clamp01(p.y);
|
|
3597
|
+
const px = plot.x0 + nx * plot.w;
|
|
3598
|
+
const py = plot.y0 + (1 - ny) * plot.h;
|
|
3599
|
+
return { px, py };
|
|
3600
|
+
}
|
|
3601
|
+
function computeRadius(p, maxSize, plot, scale) {
|
|
3602
|
+
if (p.size === void 0) return 6;
|
|
3603
|
+
const maxRadius = Math.max(14, plot.h * 0.08);
|
|
3604
|
+
const minRadius = 4;
|
|
3605
|
+
if (maxSize <= 0) return 6;
|
|
3606
|
+
const ratio = p.size / maxSize;
|
|
3607
|
+
if (scale === "radius") {
|
|
3608
|
+
return Math.max(minRadius, ratio * maxRadius);
|
|
3609
|
+
}
|
|
3610
|
+
const maxArea = Math.PI * maxRadius * maxRadius;
|
|
3611
|
+
const area = ratio * maxArea;
|
|
3612
|
+
const r = Math.sqrt(area / Math.PI);
|
|
3613
|
+
return Math.max(minRadius, r);
|
|
3614
|
+
}
|
|
3615
|
+
function resolveLabelCollisions(points, plot, mode) {
|
|
3616
|
+
if (mode === "off") {
|
|
3617
|
+
for (const p of points) {
|
|
3618
|
+
p.label.lx = p.px + p.r + 4 + p.label.width / 2;
|
|
3619
|
+
p.label.ly = p.py - p.r - 4;
|
|
3620
|
+
}
|
|
3621
|
+
return;
|
|
3622
|
+
}
|
|
3623
|
+
for (const p of points) {
|
|
3624
|
+
p.label.ax = p.px;
|
|
3625
|
+
p.label.ay = p.py;
|
|
3626
|
+
p.label.lx = p.px + p.r + 4 + p.label.width / 2;
|
|
3627
|
+
p.label.ly = p.py - p.r - 4;
|
|
3628
|
+
p.label.external = false;
|
|
3629
|
+
p.label.textAnchor = "middle";
|
|
3630
|
+
}
|
|
3631
|
+
if (mode === "leader-only") {
|
|
3632
|
+
for (const p of points) {
|
|
3633
|
+
p.label.external = true;
|
|
3634
|
+
p.label.lx = p.px + p.r + 12 + p.label.width / 2;
|
|
3635
|
+
p.label.ly = p.py;
|
|
3636
|
+
p.label.textAnchor = "middle";
|
|
3637
|
+
}
|
|
3638
|
+
return;
|
|
3639
|
+
}
|
|
3640
|
+
const PAD = 3;
|
|
3641
|
+
for (let iter = 0; iter < 30; iter++) {
|
|
3642
|
+
let moved = false;
|
|
3643
|
+
for (let i = 0; i < points.length; i++) {
|
|
3644
|
+
for (let j = i + 1; j < points.length; j++) {
|
|
3645
|
+
const a = points[i].label;
|
|
3646
|
+
const b = points[j].label;
|
|
3647
|
+
const ax0 = a.lx - a.width / 2 - PAD;
|
|
3648
|
+
const ax1 = a.lx + a.width / 2 + PAD;
|
|
3649
|
+
const ay0 = a.ly - a.height / 2 - PAD;
|
|
3650
|
+
const ay1 = a.ly + a.height / 2 + PAD;
|
|
3651
|
+
const bx0 = b.lx - b.width / 2 - PAD;
|
|
3652
|
+
const bx1 = b.lx + b.width / 2 + PAD;
|
|
3653
|
+
const by0 = b.ly - b.height / 2 - PAD;
|
|
3654
|
+
const by1 = b.ly + b.height / 2 + PAD;
|
|
3655
|
+
const overlapX = Math.min(ax1, bx1) - Math.max(ax0, bx0);
|
|
3656
|
+
const overlapY = Math.min(ay1, by1) - Math.max(ay0, by0);
|
|
3657
|
+
if (overlapX > 0 && overlapY > 0) {
|
|
3658
|
+
const dx = a.lx - b.lx || 0.1;
|
|
3659
|
+
const dy = a.ly - b.ly || 0.1;
|
|
3660
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
3661
|
+
const ux = dx / len;
|
|
3662
|
+
const uy = dy / len;
|
|
3663
|
+
const step = Math.min(3, Math.min(overlapX, overlapY) / 2 + 0.5);
|
|
3664
|
+
a.lx += ux * step;
|
|
3665
|
+
a.ly += uy * step;
|
|
3666
|
+
b.lx -= ux * step;
|
|
3667
|
+
b.ly -= uy * step;
|
|
3668
|
+
moved = true;
|
|
3669
|
+
}
|
|
3670
|
+
}
|
|
3671
|
+
const lb = points[i].label;
|
|
3672
|
+
if (lb.lx - lb.width / 2 < plot.x0 + 2) lb.lx = plot.x0 + 2 + lb.width / 2;
|
|
3673
|
+
if (lb.lx + lb.width / 2 > plot.x0 + plot.w - 2) lb.lx = plot.x0 + plot.w - 2 - lb.width / 2;
|
|
3674
|
+
if (lb.ly - lb.height / 2 < plot.y0 + 2) lb.ly = plot.y0 + 2 + lb.height / 2;
|
|
3675
|
+
if (lb.ly + lb.height / 2 > plot.y0 + plot.h - 2) lb.ly = plot.y0 + plot.h - 2 - lb.height / 2;
|
|
3676
|
+
}
|
|
3677
|
+
if (!moved) break;
|
|
3678
|
+
}
|
|
3679
|
+
if (mode === "auto") {
|
|
3680
|
+
for (const p of points) {
|
|
3681
|
+
const dx = p.label.lx - p.px;
|
|
3682
|
+
const dy = p.label.ly - p.py;
|
|
3683
|
+
if (Math.hypot(dx, dy) > 40) {
|
|
3684
|
+
p.label.external = true;
|
|
3685
|
+
}
|
|
3686
|
+
}
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
function layoutMatrix(ast) {
|
|
3690
|
+
const canvasWidth = CANVAS_W;
|
|
3691
|
+
const canvasHeight = CANVAS_H;
|
|
3692
|
+
const plot = {
|
|
3693
|
+
x0: PADDING_X,
|
|
3694
|
+
y0: PADDING_Y - 30,
|
|
3695
|
+
w: canvasWidth - PADDING_X * 2,
|
|
3696
|
+
h: canvasHeight - PADDING_Y * 2
|
|
3697
|
+
};
|
|
3698
|
+
const points = [];
|
|
3699
|
+
const categoriesSet = /* @__PURE__ */ new Set();
|
|
3700
|
+
if (ast.mode === "quadrant") {
|
|
3701
|
+
let maxSize = 0;
|
|
3702
|
+
for (const p of ast.points) {
|
|
3703
|
+
if (p.size !== void 0 && p.size > maxSize) maxSize = p.size;
|
|
3704
|
+
}
|
|
3705
|
+
for (const p of ast.points) {
|
|
3706
|
+
const { px, py } = placePoint(p, plot);
|
|
3707
|
+
const r = computeRadius(p, maxSize, plot, ast.config.bubbleScale);
|
|
3708
|
+
const width = estimateWidth(p.label);
|
|
3709
|
+
const label = {
|
|
3710
|
+
text: p.label,
|
|
3711
|
+
ax: px,
|
|
3712
|
+
ay: py,
|
|
3713
|
+
lx: px + r + 4 + width / 2,
|
|
3714
|
+
ly: py - r - 4,
|
|
3715
|
+
width,
|
|
3716
|
+
height: LABEL_H,
|
|
3717
|
+
external: false,
|
|
3718
|
+
textAnchor: "middle"
|
|
3719
|
+
};
|
|
3720
|
+
points.push({ point: p, px, py, r, label });
|
|
3721
|
+
if (p.category) categoriesSet.add(p.category);
|
|
3722
|
+
}
|
|
3723
|
+
resolveLabelCollisions(points, plot, ast.config.labelCollision);
|
|
3724
|
+
}
|
|
3725
|
+
return {
|
|
3726
|
+
canvasWidth,
|
|
3727
|
+
canvasHeight,
|
|
3728
|
+
plot,
|
|
3729
|
+
points,
|
|
3730
|
+
categories: [...categoriesSet]
|
|
3731
|
+
};
|
|
3732
|
+
}
|
|
3733
|
+
|
|
3734
|
+
// src/diagrams/matrix/renderer.ts
|
|
3735
|
+
var CATEGORY_COLORS = [
|
|
3736
|
+
"#2563eb",
|
|
3737
|
+
"#16a34a",
|
|
3738
|
+
"#dc2626",
|
|
3739
|
+
"#9333ea",
|
|
3740
|
+
"#ea580c",
|
|
3741
|
+
"#0891b2",
|
|
3742
|
+
"#ca8a04",
|
|
3743
|
+
"#db2777"
|
|
3744
|
+
];
|
|
3745
|
+
var QUADRANT_TINTS = [
|
|
3746
|
+
// Q1 TR, Q2 TL, Q3 BL, Q4 BR
|
|
3747
|
+
"#dbeafe",
|
|
3748
|
+
"#dcfce7",
|
|
3749
|
+
"#f3f4f6",
|
|
3750
|
+
"#fed7aa"
|
|
3751
|
+
];
|
|
3752
|
+
var HEAT_RAMP = [
|
|
3753
|
+
"#f0fdf4",
|
|
3754
|
+
"#bbf7d0",
|
|
3755
|
+
"#fde68a",
|
|
3756
|
+
"#fdba74",
|
|
3757
|
+
"#f87171",
|
|
3758
|
+
"#ef4444",
|
|
3759
|
+
"#b91c1c"
|
|
3760
|
+
];
|
|
3761
|
+
var CSS = `
|
|
3762
|
+
.sx-matrix { background: #fff; font-family: system-ui, -apple-system, "Segoe UI", sans-serif; }
|
|
3763
|
+
.sx-matrix-title { font: 600 16px sans-serif; fill: #111; }
|
|
3764
|
+
.sx-matrix-grid { stroke: #e5e7eb; stroke-width: 1; fill: none; }
|
|
3765
|
+
.sx-matrix-mid { stroke: #9ca3af; stroke-width: 1.2; stroke-dasharray: 4 3; fill: none; }
|
|
3766
|
+
.sx-matrix-plot-border { stroke: #374151; stroke-width: 1.2; fill: none; }
|
|
3767
|
+
.sx-matrix-axis-label { font: 500 12px sans-serif; fill: #374151; }
|
|
3768
|
+
.sx-matrix-axis-end { font: 500 11px sans-serif; fill: #6b7280; }
|
|
3769
|
+
.sx-matrix-quad-annot { font: 600 13px sans-serif; fill: #475569; opacity: 0.75; }
|
|
3770
|
+
.sx-matrix-quad-desc { font: 400 10.5px sans-serif; fill: #64748b; opacity: 0.85; }
|
|
3771
|
+
.sx-matrix-corr-header { font: 600 11.5px sans-serif; fill: #1f2937; text-anchor: middle; }
|
|
3772
|
+
.sx-matrix-corr-rowlabel { font: 500 11.5px sans-serif; fill: #1f2937; text-anchor: end; dominant-baseline: central; }
|
|
3773
|
+
.sx-matrix-corr-margin { font: 500 11px sans-serif; fill: #374151; text-anchor: middle; dominant-baseline: central; }
|
|
3774
|
+
.sx-matrix-corr-margin-best { font: 700 11.5px sans-serif; fill: #111; text-anchor: middle; dominant-baseline: central; }
|
|
3775
|
+
.sx-matrix-corr-grid { stroke: #d1d5db; stroke-width: 0.8; fill: none; }
|
|
3776
|
+
.sx-matrix-corr-rowbg-a { fill: #f0fdf4; }
|
|
3777
|
+
.sx-matrix-corr-rowbg-b { fill: #fff; }
|
|
3778
|
+
.sx-matrix-cell-label { font: 500 12px sans-serif; fill: #1f2937; text-anchor: middle; }
|
|
3779
|
+
.sx-matrix-cell-value { font: 600 18px sans-serif; fill: #111; text-anchor: middle; }
|
|
3780
|
+
.sx-matrix-bubble { stroke-width: 1.5; }
|
|
3781
|
+
.sx-matrix-label { font: 500 11px sans-serif; fill: #111827; text-anchor: middle; dominant-baseline: central; pointer-events: none; }
|
|
3782
|
+
.sx-matrix-leader { stroke: #94a3b8; stroke-width: 0.6; opacity: 0.7; fill: none; }
|
|
3783
|
+
.sx-matrix-legend-text { font: 500 11px sans-serif; fill: #374151; }
|
|
3784
|
+
.sx-matrix-offchart { fill: #ea580c; }
|
|
3785
|
+
`.trim();
|
|
3786
|
+
function axisArrow() {
|
|
3787
|
+
return el(
|
|
3788
|
+
"marker",
|
|
3789
|
+
{
|
|
3790
|
+
id: "sx-matrix-arrow",
|
|
3791
|
+
viewBox: "0 0 10 10",
|
|
3792
|
+
refX: 8,
|
|
3793
|
+
refY: 5,
|
|
3794
|
+
markerWidth: 8,
|
|
3795
|
+
markerHeight: 8,
|
|
3796
|
+
orient: "auto-start-reverse"
|
|
3797
|
+
},
|
|
3798
|
+
[el("path", { d: "M0,0 L10,5 L0,10 z", fill: "#374151" })]
|
|
3799
|
+
);
|
|
3800
|
+
}
|
|
3801
|
+
function bubbleFill(p, categories) {
|
|
3802
|
+
if (p.color) return p.color;
|
|
3803
|
+
if (p.category) {
|
|
3804
|
+
const idx = categories.indexOf(p.category);
|
|
3805
|
+
if (idx >= 0) return CATEGORY_COLORS[idx % CATEGORY_COLORS.length];
|
|
3806
|
+
}
|
|
3807
|
+
return "#2563eb";
|
|
3808
|
+
}
|
|
3809
|
+
function renderQuadrantBackground(ast, lay) {
|
|
3810
|
+
if (!ast.config.quadrantBg || ast.grid !== "2x2") return "";
|
|
3811
|
+
const { plot } = lay;
|
|
3812
|
+
const halfW = plot.w / 2;
|
|
3813
|
+
const halfH = plot.h / 2;
|
|
3814
|
+
const rects = [
|
|
3815
|
+
{ x: plot.x0 + halfW, y: plot.y0, w: halfW, h: halfH, fill: QUADRANT_TINTS[0] },
|
|
3816
|
+
{ x: plot.x0, y: plot.y0, w: halfW, h: halfH, fill: QUADRANT_TINTS[1] },
|
|
3817
|
+
{ x: plot.x0, y: plot.y0 + halfH, w: halfW, h: halfH, fill: QUADRANT_TINTS[2] },
|
|
3818
|
+
{ x: plot.x0 + halfW, y: plot.y0 + halfH, w: halfW, h: halfH, fill: QUADRANT_TINTS[3] }
|
|
3819
|
+
];
|
|
3820
|
+
return group(
|
|
3821
|
+
{ id: "sx-matrix-quad-bg" },
|
|
3822
|
+
rects.map(
|
|
3823
|
+
(r) => rect({ x: r.x, y: r.y, width: r.w, height: r.h, fill: r.fill, "fill-opacity": 0.55 })
|
|
3824
|
+
)
|
|
3825
|
+
);
|
|
3826
|
+
}
|
|
3827
|
+
function renderGrid(ast, lay) {
|
|
3828
|
+
if (!ast.config.gridLines) return "";
|
|
3829
|
+
if (ast.mode === "correlation") return "";
|
|
3830
|
+
const { plot } = lay;
|
|
3831
|
+
const lines = [];
|
|
3832
|
+
const cols = ast.cols;
|
|
3833
|
+
const rows = ast.rows;
|
|
3834
|
+
for (let i = 1; i < cols; i++) {
|
|
3835
|
+
const x = plot.x0 + plot.w * i / cols;
|
|
3836
|
+
const cls = cols === 2 && i === 1 ? "sx-matrix-mid" : "sx-matrix-grid";
|
|
3837
|
+
lines.push(line({ x1: x, y1: plot.y0, x2: x, y2: plot.y0 + plot.h, class: cls }));
|
|
3838
|
+
}
|
|
3839
|
+
for (let j = 1; j < rows; j++) {
|
|
3840
|
+
const y = plot.y0 + plot.h * j / rows;
|
|
3841
|
+
const cls = rows === 2 && j === 1 ? "sx-matrix-mid" : "sx-matrix-grid";
|
|
3842
|
+
lines.push(line({ x1: plot.x0, y1: y, x2: plot.x0 + plot.w, y2: y, class: cls }));
|
|
3843
|
+
}
|
|
3844
|
+
lines.push(
|
|
3845
|
+
rect({
|
|
3846
|
+
x: plot.x0,
|
|
3847
|
+
y: plot.y0,
|
|
3848
|
+
width: plot.w,
|
|
3849
|
+
height: plot.h,
|
|
3850
|
+
class: "sx-matrix-plot-border",
|
|
3851
|
+
fill: "none"
|
|
3852
|
+
})
|
|
3853
|
+
);
|
|
3854
|
+
return group({ id: "sx-matrix-grid" }, lines);
|
|
3855
|
+
}
|
|
3856
|
+
function shouldShowAxis(ast) {
|
|
3857
|
+
if (ast.config.showAxis === "on") return true;
|
|
3858
|
+
if (ast.config.showAxis === "off") return false;
|
|
3859
|
+
return ast.mode === "quadrant";
|
|
3860
|
+
}
|
|
3861
|
+
function renderAxes(ast, lay) {
|
|
3862
|
+
if (!shouldShowAxis(ast)) return "";
|
|
3863
|
+
const { plot } = lay;
|
|
3864
|
+
const els = [];
|
|
3865
|
+
const marker = ast.config.axisArrows ? { "marker-end": "url(#sx-matrix-arrow)" } : {};
|
|
3866
|
+
const xy = plot.y0 + plot.h + 14;
|
|
3867
|
+
const yx = plot.x0 - 14;
|
|
3868
|
+
els.push(
|
|
3869
|
+
line({
|
|
3870
|
+
x1: plot.x0,
|
|
3871
|
+
y1: xy,
|
|
3872
|
+
x2: plot.x0 + plot.w,
|
|
3873
|
+
y2: xy,
|
|
3874
|
+
stroke: "#374151",
|
|
3875
|
+
"stroke-width": 1.2,
|
|
3876
|
+
...marker
|
|
3877
|
+
})
|
|
3878
|
+
);
|
|
3879
|
+
els.push(
|
|
3880
|
+
line({
|
|
3881
|
+
x1: yx,
|
|
3882
|
+
y1: plot.y0 + plot.h,
|
|
3883
|
+
x2: yx,
|
|
3884
|
+
y2: plot.y0,
|
|
3885
|
+
stroke: "#374151",
|
|
3886
|
+
"stroke-width": 1.2,
|
|
3887
|
+
...marker
|
|
3888
|
+
})
|
|
3889
|
+
);
|
|
3890
|
+
if (ast.xAxis.low) {
|
|
3891
|
+
els.push(
|
|
3892
|
+
text(
|
|
3893
|
+
{ x: plot.x0, y: xy + 20, class: "sx-matrix-axis-end", "text-anchor": "start" },
|
|
3894
|
+
ast.xAxis.low
|
|
3895
|
+
)
|
|
3896
|
+
);
|
|
3897
|
+
}
|
|
3898
|
+
if (ast.xAxis.high) {
|
|
3899
|
+
els.push(
|
|
3900
|
+
text(
|
|
3901
|
+
{ x: plot.x0 + plot.w, y: xy + 20, class: "sx-matrix-axis-end", "text-anchor": "end" },
|
|
3902
|
+
ast.xAxis.high
|
|
3903
|
+
)
|
|
3904
|
+
);
|
|
3905
|
+
}
|
|
3906
|
+
if (ast.yAxis.low) {
|
|
3907
|
+
els.push(
|
|
3908
|
+
text(
|
|
3909
|
+
{
|
|
3910
|
+
x: yx - 24,
|
|
3911
|
+
y: plot.y0 + plot.h,
|
|
3912
|
+
class: "sx-matrix-axis-end",
|
|
3913
|
+
"text-anchor": "end",
|
|
3914
|
+
transform: `rotate(-90 ${yx - 24} ${plot.y0 + plot.h})`
|
|
3915
|
+
},
|
|
3916
|
+
ast.yAxis.low
|
|
3917
|
+
)
|
|
3918
|
+
);
|
|
3919
|
+
}
|
|
3920
|
+
if (ast.yAxis.high) {
|
|
3921
|
+
els.push(
|
|
3922
|
+
text(
|
|
3923
|
+
{
|
|
3924
|
+
x: yx - 24,
|
|
3925
|
+
y: plot.y0,
|
|
3926
|
+
class: "sx-matrix-axis-end",
|
|
3927
|
+
"text-anchor": "start",
|
|
3928
|
+
transform: `rotate(-90 ${yx - 24} ${plot.y0})`
|
|
3929
|
+
},
|
|
3930
|
+
ast.yAxis.high
|
|
3931
|
+
)
|
|
3932
|
+
);
|
|
3933
|
+
}
|
|
3934
|
+
return group({ id: "sx-matrix-axes" }, els);
|
|
3935
|
+
}
|
|
3936
|
+
function renderQuadAnnotations(ast, lay) {
|
|
3937
|
+
if (!ast.config.quadrantAnnotations || ast.grid !== "2x2" || ast.annotations.length === 0) return "";
|
|
3938
|
+
const { plot } = lay;
|
|
3939
|
+
plot.w / 2;
|
|
3940
|
+
plot.h / 2;
|
|
3941
|
+
const padding = 14;
|
|
3942
|
+
const positions = {
|
|
3943
|
+
1: { x: plot.x0 + plot.w - padding, y: plot.y0 + padding + 14, anchor: "end" },
|
|
3944
|
+
// TR
|
|
3945
|
+
2: { x: plot.x0 + padding, y: plot.y0 + padding + 14, anchor: "start" },
|
|
3946
|
+
// TL
|
|
3947
|
+
3: { x: plot.x0 + padding, y: plot.y0 + plot.h - padding, anchor: "start" },
|
|
3948
|
+
// BL
|
|
3949
|
+
4: { x: plot.x0 + plot.w - padding, y: plot.y0 + plot.h - padding, anchor: "end" }
|
|
3950
|
+
// BR
|
|
3951
|
+
};
|
|
3952
|
+
const nodes = [];
|
|
3953
|
+
for (const a of ast.annotations) {
|
|
3954
|
+
const pos = positions[a.q];
|
|
3955
|
+
const growsUp = a.q === 3 || a.q === 4;
|
|
3956
|
+
const descLines = a.description ? wrapLabel(a.description, 28) : [];
|
|
3957
|
+
const labelY = growsUp && descLines.length > 0 ? pos.y - descLines.length * 12 : pos.y;
|
|
3958
|
+
nodes.push(
|
|
3959
|
+
text(
|
|
3960
|
+
{ x: pos.x, y: labelY, class: "sx-matrix-quad-annot", "text-anchor": pos.anchor },
|
|
3961
|
+
a.label
|
|
3962
|
+
)
|
|
3963
|
+
);
|
|
3964
|
+
for (let i = 0; i < descLines.length; i++) {
|
|
3965
|
+
nodes.push(
|
|
3966
|
+
text(
|
|
3967
|
+
{
|
|
3968
|
+
x: pos.x,
|
|
3969
|
+
y: labelY + 14 + i * 12,
|
|
3970
|
+
class: "sx-matrix-quad-desc",
|
|
3971
|
+
"text-anchor": pos.anchor
|
|
3972
|
+
},
|
|
3973
|
+
descLines[i]
|
|
3974
|
+
)
|
|
3975
|
+
);
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
3978
|
+
return group({ id: "sx-matrix-quad-annot" }, nodes);
|
|
3979
|
+
}
|
|
3980
|
+
function wrapLabel(text2, maxChars) {
|
|
3981
|
+
const words = text2.split(/\s+/);
|
|
3982
|
+
const out = [];
|
|
3983
|
+
let cur = "";
|
|
3984
|
+
for (const w of words) {
|
|
3985
|
+
if ((cur + " " + w).trim().length > maxChars) {
|
|
3986
|
+
if (cur) out.push(cur);
|
|
3987
|
+
cur = w;
|
|
3988
|
+
} else {
|
|
3989
|
+
cur = (cur ? cur + " " : "") + w;
|
|
3990
|
+
}
|
|
3991
|
+
}
|
|
3992
|
+
if (cur) out.push(cur);
|
|
3993
|
+
return out;
|
|
3994
|
+
}
|
|
3995
|
+
function render3x3CellLabels(ast, lay) {
|
|
3996
|
+
if (ast.grid !== "3x3" || ast.cellLabels.length === 0) return "";
|
|
3997
|
+
const { plot } = lay;
|
|
3998
|
+
const cellW = plot.w / ast.cols;
|
|
3999
|
+
const cellH = plot.h / ast.rows;
|
|
4000
|
+
const nodes = ast.cellLabels.map((cl) => {
|
|
4001
|
+
const cx = plot.x0 + cellW * (cl.col + 0.5);
|
|
4002
|
+
const cy = plot.y0 + cellH * (ast.rows - 1 - cl.row + 0.5);
|
|
4003
|
+
return text({ x: cx, y: cy, class: "sx-matrix-cell-label" }, cl.label);
|
|
4004
|
+
});
|
|
4005
|
+
return group({ id: "sx-matrix-cell-labels" }, nodes);
|
|
4006
|
+
}
|
|
4007
|
+
function renderHeatmap(ast, lay) {
|
|
4008
|
+
if (ast.mode !== "heatmap") return "";
|
|
4009
|
+
const { plot } = lay;
|
|
4010
|
+
const cellW = plot.w / ast.cols;
|
|
4011
|
+
const cellH = plot.h / ast.rows;
|
|
4012
|
+
const maxVal = Math.max(
|
|
4013
|
+
1,
|
|
4014
|
+
...ast.cells.map((c) => c.value ?? (c.col + 1) * (c.row + 1))
|
|
4015
|
+
);
|
|
4016
|
+
const cells = [];
|
|
4017
|
+
for (let col = 0; col < ast.cols; col++) {
|
|
4018
|
+
for (let row = 0; row < ast.rows; row++) {
|
|
4019
|
+
const found = ast.cells.find((c) => c.col === col && c.row === row);
|
|
4020
|
+
const value = found?.value ?? (col + 1) * (row + 1);
|
|
4021
|
+
const ratio = Math.min(1, value / maxVal);
|
|
4022
|
+
const idx = Math.min(HEAT_RAMP.length - 1, Math.floor(ratio * HEAT_RAMP.length));
|
|
4023
|
+
const color = HEAT_RAMP[idx];
|
|
4024
|
+
const x = plot.x0 + col * cellW;
|
|
4025
|
+
const y = plot.y0 + (ast.rows - 1 - row) * cellH;
|
|
4026
|
+
cells.push(
|
|
4027
|
+
rect({
|
|
4028
|
+
x,
|
|
4029
|
+
y,
|
|
4030
|
+
width: cellW,
|
|
4031
|
+
height: cellH,
|
|
4032
|
+
fill: color,
|
|
4033
|
+
stroke: "#fff",
|
|
4034
|
+
"stroke-width": 2
|
|
4035
|
+
})
|
|
4036
|
+
);
|
|
4037
|
+
}
|
|
4038
|
+
}
|
|
4039
|
+
if (ast.rowLabels) {
|
|
4040
|
+
for (let row = 0; row < ast.rows; row++) {
|
|
4041
|
+
const lbl = ast.rowLabels[row];
|
|
4042
|
+
if (!lbl) continue;
|
|
4043
|
+
const y = plot.y0 + (ast.rows - 1 - row + 0.5) * cellH;
|
|
4044
|
+
cells.push(
|
|
4045
|
+
text(
|
|
4046
|
+
{
|
|
4047
|
+
x: plot.x0 - 8,
|
|
4048
|
+
y,
|
|
4049
|
+
class: "sx-matrix-axis-end",
|
|
4050
|
+
"text-anchor": "end",
|
|
4051
|
+
"dominant-baseline": "central"
|
|
4052
|
+
},
|
|
4053
|
+
lbl
|
|
4054
|
+
)
|
|
4055
|
+
);
|
|
4056
|
+
}
|
|
4057
|
+
}
|
|
4058
|
+
if (ast.colLabels) {
|
|
4059
|
+
for (let col = 0; col < ast.cols; col++) {
|
|
4060
|
+
const lbl = ast.colLabels[col];
|
|
4061
|
+
if (!lbl) continue;
|
|
4062
|
+
const x = plot.x0 + (col + 0.5) * cellW;
|
|
4063
|
+
cells.push(
|
|
4064
|
+
text(
|
|
4065
|
+
{
|
|
4066
|
+
x,
|
|
4067
|
+
y: plot.y0 + plot.h + 16,
|
|
4068
|
+
class: "sx-matrix-axis-end",
|
|
4069
|
+
"text-anchor": "middle"
|
|
4070
|
+
},
|
|
4071
|
+
lbl
|
|
4072
|
+
)
|
|
4073
|
+
);
|
|
4074
|
+
}
|
|
4075
|
+
}
|
|
4076
|
+
for (const cl of ast.cellLabels) {
|
|
4077
|
+
const cx = plot.x0 + cellW * (cl.col + 0.5);
|
|
4078
|
+
const cy = plot.y0 + cellH * (ast.rows - 1 - cl.row + 0.5);
|
|
4079
|
+
const words = cl.label.split(/\s+/);
|
|
4080
|
+
const lines = [];
|
|
4081
|
+
let cur = "";
|
|
4082
|
+
for (const w of words) {
|
|
4083
|
+
if ((cur + " " + w).trim().length > 14) {
|
|
4084
|
+
if (cur) lines.push(cur);
|
|
4085
|
+
cur = w;
|
|
4086
|
+
} else {
|
|
4087
|
+
cur = (cur ? cur + " " : "") + w;
|
|
4088
|
+
}
|
|
4089
|
+
}
|
|
4090
|
+
if (cur) lines.push(cur);
|
|
4091
|
+
const lineH = 13;
|
|
4092
|
+
const startY = cy - (lines.length - 1) * lineH / 2;
|
|
4093
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4094
|
+
cells.push(
|
|
4095
|
+
text(
|
|
4096
|
+
{
|
|
4097
|
+
x: cx,
|
|
4098
|
+
y: startY + i * lineH,
|
|
4099
|
+
class: "sx-matrix-cell-label",
|
|
4100
|
+
"dominant-baseline": "central"
|
|
4101
|
+
},
|
|
4102
|
+
lines[i]
|
|
4103
|
+
)
|
|
4104
|
+
);
|
|
4105
|
+
}
|
|
4106
|
+
}
|
|
4107
|
+
return group({ id: "sx-matrix-heatmap" }, cells);
|
|
4108
|
+
}
|
|
4109
|
+
var DOT_COLORS = {
|
|
4110
|
+
strong: "#16a34a",
|
|
4111
|
+
medium: "#86efac",
|
|
4112
|
+
weak: "#9ca3af"
|
|
4113
|
+
};
|
|
4114
|
+
function levelFromValue(v) {
|
|
4115
|
+
if (v >= 3) return "strong";
|
|
4116
|
+
if (v >= 2) return "medium";
|
|
4117
|
+
return "weak";
|
|
4118
|
+
}
|
|
4119
|
+
function renderCorrelation(ast, lay) {
|
|
4120
|
+
if (ast.mode !== "correlation") return "";
|
|
4121
|
+
const { plot } = lay;
|
|
4122
|
+
const marginCols = ast.config.margins ? 2 : 0;
|
|
4123
|
+
const marginRows = ast.config.margins ? 2 : 0;
|
|
4124
|
+
const cellW = plot.w / (ast.cols + marginCols);
|
|
4125
|
+
const cellH = plot.h / (ast.rows + marginRows);
|
|
4126
|
+
const gridW = cellW * ast.cols;
|
|
4127
|
+
const gridH = cellH * ast.rows;
|
|
4128
|
+
const nodes = [];
|
|
4129
|
+
for (let row = 0; row < ast.rows; row++) {
|
|
4130
|
+
const y = plot.y0 + (ast.rows - 1 - row) * cellH;
|
|
4131
|
+
nodes.push(
|
|
4132
|
+
rect({
|
|
4133
|
+
x: plot.x0,
|
|
4134
|
+
y,
|
|
4135
|
+
width: gridW,
|
|
4136
|
+
height: cellH,
|
|
4137
|
+
class: row % 2 === 0 ? "sx-matrix-corr-rowbg-a" : "sx-matrix-corr-rowbg-b"
|
|
4138
|
+
})
|
|
4139
|
+
);
|
|
4140
|
+
}
|
|
4141
|
+
for (let i = 0; i <= ast.cols; i++) {
|
|
4142
|
+
const x = plot.x0 + i * cellW;
|
|
4143
|
+
nodes.push(line({ x1: x, y1: plot.y0, x2: x, y2: plot.y0 + gridH, class: "sx-matrix-corr-grid" }));
|
|
4144
|
+
}
|
|
4145
|
+
for (let j = 0; j <= ast.rows; j++) {
|
|
4146
|
+
const y = plot.y0 + j * cellH;
|
|
4147
|
+
nodes.push(line({ x1: plot.x0, y1: y, x2: plot.x0 + gridW, y2: y, class: "sx-matrix-corr-grid" }));
|
|
4148
|
+
}
|
|
4149
|
+
const dotR = Math.max(4, Math.min(cellW, cellH) * 0.28);
|
|
4150
|
+
const rowSums = new Array(ast.rows).fill(0);
|
|
4151
|
+
const colSums = new Array(ast.cols).fill(0);
|
|
4152
|
+
for (const c of ast.cells) {
|
|
4153
|
+
if (c.col < 0 || c.col >= ast.cols || c.row < 0 || c.row >= ast.rows) continue;
|
|
4154
|
+
const v = c.value ?? (c.level ? c.level === "strong" ? 3 : c.level === "medium" ? 2 : 1 : 0);
|
|
4155
|
+
if (v <= 0) continue;
|
|
4156
|
+
rowSums[c.row] += v;
|
|
4157
|
+
colSums[c.col] += v;
|
|
4158
|
+
const lvl = c.level ?? levelFromValue(v);
|
|
4159
|
+
const cx = plot.x0 + (c.col + 0.5) * cellW;
|
|
4160
|
+
const cy = plot.y0 + (ast.rows - 1 - c.row + 0.5) * cellH;
|
|
4161
|
+
nodes.push(
|
|
4162
|
+
circle({
|
|
4163
|
+
cx,
|
|
4164
|
+
cy,
|
|
4165
|
+
r: dotR,
|
|
4166
|
+
fill: DOT_COLORS[lvl],
|
|
4167
|
+
stroke: DOT_COLORS[lvl],
|
|
4168
|
+
"stroke-width": 1
|
|
4169
|
+
})
|
|
4170
|
+
);
|
|
4171
|
+
}
|
|
4172
|
+
if (ast.colLabels) {
|
|
4173
|
+
for (let col = 0; col < ast.cols; col++) {
|
|
4174
|
+
const label = ast.colLabels[col];
|
|
4175
|
+
if (!label) continue;
|
|
4176
|
+
const cx = plot.x0 + (col + 0.5) * cellW;
|
|
4177
|
+
const lines = wrapLabel(label, 10);
|
|
4178
|
+
const startY = plot.y0 - 8 - (lines.length - 1) * 12;
|
|
4179
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4180
|
+
nodes.push(
|
|
4181
|
+
text(
|
|
4182
|
+
{ x: cx, y: startY + i * 12, class: "sx-matrix-corr-header" },
|
|
4183
|
+
lines[i]
|
|
4184
|
+
)
|
|
4185
|
+
);
|
|
4186
|
+
}
|
|
4187
|
+
}
|
|
4188
|
+
}
|
|
4189
|
+
if (ast.rowLabels) {
|
|
4190
|
+
for (let row = 0; row < ast.rows; row++) {
|
|
4191
|
+
const label = ast.rowLabels[row];
|
|
4192
|
+
if (!label) continue;
|
|
4193
|
+
const y = plot.y0 + (ast.rows - 1 - row + 0.5) * cellH;
|
|
4194
|
+
nodes.push(
|
|
4195
|
+
text({ x: plot.x0 - 8, y, class: "sx-matrix-corr-rowlabel" }, label)
|
|
4196
|
+
);
|
|
4197
|
+
}
|
|
4198
|
+
}
|
|
4199
|
+
if (ast.config.margins) {
|
|
4200
|
+
const rowRanks = rankOf(rowSums);
|
|
4201
|
+
const colRanks = rankOf(colSums);
|
|
4202
|
+
const bestRow = rowSums.length > 0 ? Math.max(...rowSums) : 0;
|
|
4203
|
+
const bestCol = colSums.length > 0 ? Math.max(...colSums) : 0;
|
|
4204
|
+
const scoreColX = plot.x0 + gridW + cellW * 0.5;
|
|
4205
|
+
const rankColX = plot.x0 + gridW + cellW * 1.5;
|
|
4206
|
+
nodes.push(text({ x: scoreColX, y: plot.y0 - 8, class: "sx-matrix-corr-header" }, "Score"));
|
|
4207
|
+
nodes.push(text({ x: rankColX, y: plot.y0 - 8, class: "sx-matrix-corr-header" }, "Rank"));
|
|
4208
|
+
for (let row = 0; row < ast.rows; row++) {
|
|
4209
|
+
const y = plot.y0 + (ast.rows - 1 - row + 0.5) * cellH;
|
|
4210
|
+
const sum = rowSums[row];
|
|
4211
|
+
const rank = rowRanks[row];
|
|
4212
|
+
const cls = sum === bestRow && sum > 0 ? "sx-matrix-corr-margin-best" : "sx-matrix-corr-margin";
|
|
4213
|
+
nodes.push(text({ x: scoreColX, y, class: cls }, String(sum)));
|
|
4214
|
+
nodes.push(
|
|
4215
|
+
text({ x: rankColX, y, class: rank === 1 ? "sx-matrix-corr-margin-best" : "sx-matrix-corr-margin" }, String(rank))
|
|
4216
|
+
);
|
|
4217
|
+
}
|
|
4218
|
+
const scoreRowY = plot.y0 + gridH + cellH * 0.5;
|
|
4219
|
+
const rankRowY = plot.y0 + gridH + cellH * 1.5;
|
|
4220
|
+
nodes.push(
|
|
4221
|
+
text(
|
|
4222
|
+
{ x: plot.x0 - 8, y: scoreRowY, class: "sx-matrix-corr-rowlabel" },
|
|
4223
|
+
"Score"
|
|
4224
|
+
)
|
|
4225
|
+
);
|
|
4226
|
+
nodes.push(
|
|
4227
|
+
text(
|
|
4228
|
+
{ x: plot.x0 - 8, y: rankRowY, class: "sx-matrix-corr-rowlabel" },
|
|
4229
|
+
"Rank"
|
|
4230
|
+
)
|
|
4231
|
+
);
|
|
4232
|
+
for (let col = 0; col < ast.cols; col++) {
|
|
4233
|
+
const cx = plot.x0 + (col + 0.5) * cellW;
|
|
4234
|
+
const sum = colSums[col];
|
|
4235
|
+
const rank = colRanks[col];
|
|
4236
|
+
nodes.push(
|
|
4237
|
+
text(
|
|
4238
|
+
{
|
|
4239
|
+
x: cx,
|
|
4240
|
+
y: scoreRowY,
|
|
4241
|
+
class: sum === bestCol && sum > 0 ? "sx-matrix-corr-margin-best" : "sx-matrix-corr-margin"
|
|
4242
|
+
},
|
|
4243
|
+
String(sum)
|
|
4244
|
+
)
|
|
4245
|
+
);
|
|
4246
|
+
nodes.push(
|
|
4247
|
+
text(
|
|
4248
|
+
{
|
|
4249
|
+
x: cx,
|
|
4250
|
+
y: rankRowY,
|
|
4251
|
+
class: rank === 1 ? "sx-matrix-corr-margin-best" : "sx-matrix-corr-margin"
|
|
4252
|
+
},
|
|
4253
|
+
String(rank)
|
|
4254
|
+
)
|
|
4255
|
+
);
|
|
4256
|
+
}
|
|
4257
|
+
}
|
|
4258
|
+
return group({ id: "sx-matrix-correlation" }, nodes);
|
|
4259
|
+
}
|
|
4260
|
+
function rankOf(vals) {
|
|
4261
|
+
const sorted = [...vals].map((v, i) => ({ v, i })).sort((a, b) => b.v - a.v);
|
|
4262
|
+
const ranks = new Array(vals.length).fill(0);
|
|
4263
|
+
let prev = -Infinity;
|
|
4264
|
+
let rank = 0;
|
|
4265
|
+
let seen = 0;
|
|
4266
|
+
for (const e of sorted) {
|
|
4267
|
+
seen++;
|
|
4268
|
+
if (e.v !== prev) {
|
|
4269
|
+
rank = seen;
|
|
4270
|
+
prev = e.v;
|
|
4271
|
+
}
|
|
4272
|
+
ranks[e.i] = rank;
|
|
4273
|
+
}
|
|
4274
|
+
return ranks;
|
|
4275
|
+
}
|
|
4276
|
+
function renderCorrelationLegend(ast, lay) {
|
|
4277
|
+
if (ast.mode !== "correlation") return "";
|
|
4278
|
+
const xBase = lay.plot.x0 + lay.plot.w + 20;
|
|
4279
|
+
const yBase = lay.plot.y0 + 8;
|
|
4280
|
+
const items = [
|
|
4281
|
+
["strong", "Strong (3)"],
|
|
4282
|
+
["medium", "Medium (2)"],
|
|
4283
|
+
["weak", "Weak (1)"]
|
|
4284
|
+
];
|
|
4285
|
+
const rows = items.map(
|
|
4286
|
+
(it, i) => group({ transform: `translate(${xBase}, ${yBase + i * 18})` }, [
|
|
4287
|
+
circle({ cx: 6, cy: 6, r: 5, fill: DOT_COLORS[it[0]], stroke: DOT_COLORS[it[0]] }),
|
|
4288
|
+
text({ x: 18, y: 10, class: "sx-matrix-legend-text" }, it[1])
|
|
4289
|
+
])
|
|
4290
|
+
);
|
|
4291
|
+
return group({ id: "sx-matrix-corr-legend" }, rows);
|
|
4292
|
+
}
|
|
4293
|
+
function renderPoints(ast, lay) {
|
|
4294
|
+
if (ast.mode !== "quadrant") return "";
|
|
4295
|
+
const nodes = [];
|
|
4296
|
+
for (const p of lay.points) {
|
|
4297
|
+
nodes.push(renderOnePoint(p, lay.categories));
|
|
4298
|
+
}
|
|
4299
|
+
return group({ id: "sx-matrix-points" }, nodes);
|
|
4300
|
+
}
|
|
4301
|
+
function renderOnePoint(pl, categories) {
|
|
4302
|
+
const p = pl.point;
|
|
4303
|
+
const color = bubbleFill(p, categories);
|
|
4304
|
+
const shape = p.shape ?? "circle";
|
|
4305
|
+
let shapeEl;
|
|
4306
|
+
const stroke = p.highlight ? "#111" : color;
|
|
4307
|
+
const strokeWidth = p.highlight ? 2.2 : 1.5;
|
|
4308
|
+
const fillOpacity = p.size !== void 0 ? 0.45 : 0.75;
|
|
4309
|
+
if (shape === "circle") {
|
|
4310
|
+
shapeEl = circle({
|
|
4311
|
+
cx: pl.px,
|
|
4312
|
+
cy: pl.py,
|
|
4313
|
+
r: pl.r,
|
|
4314
|
+
fill: color,
|
|
4315
|
+
"fill-opacity": fillOpacity,
|
|
4316
|
+
stroke,
|
|
4317
|
+
"stroke-width": strokeWidth,
|
|
4318
|
+
class: "sx-matrix-bubble"
|
|
4319
|
+
});
|
|
4320
|
+
} else if (shape === "square") {
|
|
4321
|
+
shapeEl = rect({
|
|
4322
|
+
x: pl.px - pl.r,
|
|
4323
|
+
y: pl.py - pl.r,
|
|
4324
|
+
width: pl.r * 2,
|
|
4325
|
+
height: pl.r * 2,
|
|
4326
|
+
fill: color,
|
|
4327
|
+
"fill-opacity": fillOpacity,
|
|
4328
|
+
stroke,
|
|
4329
|
+
"stroke-width": strokeWidth,
|
|
4330
|
+
class: "sx-matrix-bubble"
|
|
4331
|
+
});
|
|
4332
|
+
} else if (shape === "diamond") {
|
|
4333
|
+
const r = pl.r;
|
|
4334
|
+
shapeEl = polygon({
|
|
4335
|
+
points: `${pl.px},${pl.py - r} ${pl.px + r},${pl.py} ${pl.px},${pl.py + r} ${pl.px - r},${pl.py}`,
|
|
4336
|
+
fill: color,
|
|
4337
|
+
"fill-opacity": fillOpacity,
|
|
4338
|
+
stroke,
|
|
4339
|
+
"stroke-width": strokeWidth,
|
|
4340
|
+
class: "sx-matrix-bubble"
|
|
4341
|
+
});
|
|
4342
|
+
} else {
|
|
4343
|
+
const r = pl.r;
|
|
4344
|
+
shapeEl = polygon({
|
|
4345
|
+
points: `${pl.px},${pl.py - r} ${pl.px + r},${pl.py + r * 0.8} ${pl.px - r},${pl.py + r * 0.8}`,
|
|
4346
|
+
fill: color,
|
|
4347
|
+
"fill-opacity": fillOpacity,
|
|
4348
|
+
stroke,
|
|
4349
|
+
"stroke-width": strokeWidth,
|
|
4350
|
+
class: "sx-matrix-bubble"
|
|
4351
|
+
});
|
|
4352
|
+
}
|
|
4353
|
+
const leader = pl.label.external ? line({
|
|
4354
|
+
x1: pl.px,
|
|
4355
|
+
y1: pl.py,
|
|
4356
|
+
x2: pl.label.lx,
|
|
4357
|
+
y2: pl.label.ly,
|
|
4358
|
+
class: "sx-matrix-leader"
|
|
4359
|
+
}) : "";
|
|
4360
|
+
const label = text(
|
|
4361
|
+
{ x: pl.label.lx, y: pl.label.ly, class: "sx-matrix-label" },
|
|
4362
|
+
pl.label.text
|
|
4363
|
+
);
|
|
4364
|
+
let badge = "";
|
|
4365
|
+
if (p.offChart) {
|
|
4366
|
+
const bx = pl.px;
|
|
4367
|
+
const by = pl.py;
|
|
4368
|
+
badge = text(
|
|
4369
|
+
{ x: bx + pl.r + 4, y: by - pl.r - 2, class: "sx-matrix-offchart", "font-size": 14, "font-weight": 700 },
|
|
4370
|
+
"\u2197"
|
|
4371
|
+
);
|
|
4372
|
+
}
|
|
4373
|
+
const titleStr = p.note ? `${p.label} \xB7 (${p.origX ?? p.x}, ${p.origY ?? p.y}) \u2014 ${p.note}` : `${p.label} \xB7 (${(p.origX ?? p.x).toFixed(2)}, ${(p.origY ?? p.y).toFixed(2)})${p.size !== void 0 ? ` \xB7 size ${p.size}` : ""}`;
|
|
4374
|
+
return group(
|
|
4375
|
+
{
|
|
4376
|
+
class: "sx-matrix-point",
|
|
4377
|
+
"data-point-id": p.id,
|
|
4378
|
+
"data-label": p.label,
|
|
4379
|
+
...p.category ? { "data-category": p.category } : {}
|
|
4380
|
+
},
|
|
4381
|
+
[title(titleStr), shapeEl, leader, label, badge].filter((s) => s.length > 0)
|
|
4382
|
+
);
|
|
4383
|
+
}
|
|
4384
|
+
function renderLegend2(ast, lay) {
|
|
4385
|
+
if (ast.config.legendPosition === "none") return "";
|
|
4386
|
+
if (ast.mode === "heatmap") {
|
|
4387
|
+
const x = lay.plot.x0 + lay.plot.w - 220;
|
|
4388
|
+
const y = lay.plot.y0 + lay.plot.h + 40;
|
|
4389
|
+
const w = 210;
|
|
4390
|
+
const h = 10;
|
|
4391
|
+
const stops = HEAT_RAMP.map(
|
|
4392
|
+
(c, i) => el("stop", { offset: `${i / (HEAT_RAMP.length - 1) * 100}%`, "stop-color": c })
|
|
4393
|
+
);
|
|
4394
|
+
const grad = el(
|
|
4395
|
+
"linearGradient",
|
|
4396
|
+
{ id: "sx-matrix-heatgrad", x1: "0%", x2: "100%" },
|
|
4397
|
+
stops
|
|
4398
|
+
);
|
|
4399
|
+
return group({ id: "sx-matrix-legend" }, [
|
|
4400
|
+
el("defs", {}, [grad]),
|
|
4401
|
+
rect({ x, y, width: w, height: h, fill: "url(#sx-matrix-heatgrad)", stroke: "#d1d5db" }),
|
|
4402
|
+
text({ x, y: y - 4, class: "sx-matrix-legend-text", "text-anchor": "start" }, "Low"),
|
|
4403
|
+
text(
|
|
4404
|
+
{ x: x + w, y: y - 4, class: "sx-matrix-legend-text", "text-anchor": "end" },
|
|
4405
|
+
"High"
|
|
4406
|
+
)
|
|
4407
|
+
]);
|
|
4408
|
+
}
|
|
4409
|
+
if (lay.categories.length === 0) return "";
|
|
4410
|
+
const xBase = lay.plot.x0 + lay.plot.w + 12;
|
|
4411
|
+
const yBase = lay.plot.y0 + 8;
|
|
4412
|
+
const rows = lay.categories.map((cat, i) => {
|
|
4413
|
+
const color = CATEGORY_COLORS[i % CATEGORY_COLORS.length];
|
|
4414
|
+
return group({ transform: `translate(${xBase}, ${yBase + i * 18})` }, [
|
|
4415
|
+
circle({ cx: 6, cy: 6, r: 5, fill: color, "fill-opacity": 0.7, stroke: color }),
|
|
4416
|
+
text({ x: 18, y: 10, class: "sx-matrix-legend-text" }, cat)
|
|
4417
|
+
]);
|
|
4418
|
+
});
|
|
4419
|
+
return group({ id: "sx-matrix-legend" }, rows);
|
|
4420
|
+
}
|
|
4421
|
+
function renderTitle(ast, lay) {
|
|
4422
|
+
if (!ast.title) return "";
|
|
4423
|
+
return text(
|
|
4424
|
+
{ x: lay.canvasWidth / 2, y: 28, class: "sx-matrix-title", "text-anchor": "middle" },
|
|
4425
|
+
ast.title
|
|
4426
|
+
);
|
|
4427
|
+
}
|
|
4428
|
+
function renderMatrixAST(ast) {
|
|
4429
|
+
const lay = layoutMatrix(ast);
|
|
4430
|
+
const needsLegendSpace = lay.categories.length > 0 || ast.mode === "correlation";
|
|
4431
|
+
const extraWidth = needsLegendSpace && lay.plot.x0 + lay.plot.w + 140 > lay.canvasWidth ? 160 : 0;
|
|
4432
|
+
const canvasWidth = lay.canvasWidth + extraWidth;
|
|
4433
|
+
const body = [
|
|
4434
|
+
renderTitle(ast, lay),
|
|
4435
|
+
renderQuadrantBackground(ast, lay),
|
|
4436
|
+
renderGrid(ast, lay),
|
|
4437
|
+
renderQuadAnnotations(ast, lay),
|
|
4438
|
+
render3x3CellLabels(ast, lay),
|
|
4439
|
+
renderHeatmap(ast, lay),
|
|
4440
|
+
renderCorrelation(ast, lay),
|
|
4441
|
+
renderAxes(ast, lay),
|
|
4442
|
+
renderPoints(ast, lay),
|
|
4443
|
+
renderLegend2(ast, lay),
|
|
4444
|
+
renderCorrelationLegend(ast, lay)
|
|
4445
|
+
].filter((s) => s.length > 0);
|
|
4446
|
+
return svgRoot(
|
|
4447
|
+
{
|
|
4448
|
+
class: "sx-matrix",
|
|
4449
|
+
"data-diagram-type": "matrix",
|
|
4450
|
+
"data-mode": ast.mode,
|
|
4451
|
+
width: canvasWidth,
|
|
4452
|
+
height: lay.canvasHeight,
|
|
4453
|
+
viewBox: `0 0 ${canvasWidth} ${lay.canvasHeight}`,
|
|
4454
|
+
role: "graphics-document"
|
|
4455
|
+
},
|
|
4456
|
+
[
|
|
4457
|
+
title(ast.title ? `Matrix \u2014 ${escapeXml(ast.title)}` : "Matrix diagram"),
|
|
4458
|
+
desc(
|
|
4459
|
+
`Matrix diagram${ast.template ? ` (${ast.template} template)` : ""}, ${ast.mode} mode, ${ast.points.length} point(s)`
|
|
4460
|
+
),
|
|
4461
|
+
defs([el("style", {}, CSS), axisArrow()]),
|
|
4462
|
+
...body
|
|
4463
|
+
]
|
|
4464
|
+
);
|
|
4465
|
+
}
|
|
4466
|
+
function renderMatrix(text2) {
|
|
4467
|
+
const ast = parseMatrix(text2);
|
|
4468
|
+
return renderMatrixAST(ast);
|
|
4469
|
+
}
|
|
4470
|
+
|
|
4471
|
+
// src/diagrams/matrix/index.ts
|
|
4472
|
+
var matrix = {
|
|
4473
|
+
type: "matrix",
|
|
4474
|
+
detect(text2) {
|
|
4475
|
+
const first = text2.trim().split("\n")[0]?.trim().toLowerCase() ?? "";
|
|
4476
|
+
return first.startsWith("matrix");
|
|
4477
|
+
},
|
|
4478
|
+
parse: parseMatrix,
|
|
4479
|
+
render(text2) {
|
|
4480
|
+
return renderMatrix(text2);
|
|
4481
|
+
}
|
|
4482
|
+
};
|
|
4483
|
+
|
|
4484
|
+
// src/core/api.ts
|
|
4485
|
+
var plugins = [
|
|
4486
|
+
genogram,
|
|
4487
|
+
ecomap,
|
|
4488
|
+
pedigree,
|
|
4489
|
+
phylo,
|
|
4490
|
+
sociogram,
|
|
4491
|
+
timing,
|
|
4492
|
+
logic,
|
|
4493
|
+
circuit,
|
|
4494
|
+
blockdiagram,
|
|
4495
|
+
ladder,
|
|
4496
|
+
sld,
|
|
4497
|
+
entity,
|
|
4498
|
+
fishbone,
|
|
4499
|
+
venn,
|
|
4500
|
+
flowchart,
|
|
4501
|
+
mindmap,
|
|
4502
|
+
matrix,
|
|
4503
|
+
orgchart,
|
|
4504
|
+
decisiontree,
|
|
4505
|
+
timeline
|
|
4506
|
+
];
|
|
4507
|
+
function detectPlugin(text2, config) {
|
|
4508
|
+
if (config?.type) {
|
|
4509
|
+
const plugin = plugins.find((p) => p.type === config.type);
|
|
4510
|
+
if (plugin) return plugin;
|
|
4511
|
+
}
|
|
4512
|
+
for (const plugin of plugins) {
|
|
4513
|
+
if (plugin.detect(text2)) return plugin;
|
|
4514
|
+
}
|
|
4515
|
+
throw new Error(
|
|
4516
|
+
"Cannot detect diagram type. Start your text with 'genogram', 'ecomap', 'pedigree', 'phylo', 'sociogram', 'timing', 'logic', 'circuit', 'blockdiagram', 'ladder', 'sld', 'entity-structure', 'fishbone', 'venn', 'flowchart', 'mindmap', 'matrix', or 'orgchart'."
|
|
4517
|
+
);
|
|
4518
|
+
}
|
|
4519
|
+
function parse(text2, config) {
|
|
4520
|
+
const plugin = detectPlugin(text2, config);
|
|
4521
|
+
if (plugin.parse) return plugin.parse(text2);
|
|
4522
|
+
throw new Error(
|
|
4523
|
+
`Diagram type '${plugin.type}' does not yet expose a parse() method.`
|
|
4524
|
+
);
|
|
4525
|
+
}
|
|
4526
|
+
function render(text2, config) {
|
|
4527
|
+
const plugin = detectPlugin(text2, config);
|
|
4528
|
+
const renderConfig = {
|
|
4529
|
+
fontFamily: config?.fontFamily ?? "system-ui, -apple-system, sans-serif",
|
|
4530
|
+
fontSize: 12,
|
|
4531
|
+
theme: config?.theme ?? "default",
|
|
4532
|
+
padding: config?.padding ?? 20
|
|
4533
|
+
};
|
|
4534
|
+
return plugin.render(text2, renderConfig);
|
|
4535
|
+
}
|
|
4536
|
+
|
|
4537
|
+
export { decisiontree, parse, render, timeline };
|
|
4538
|
+
//# sourceMappingURL=chunk-M6AMNXQ7.js.map
|
|
4539
|
+
//# sourceMappingURL=chunk-M6AMNXQ7.js.map
|