compasso 0.2.0 → 0.4.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 +126 -7
- package/dist/chunk-F47C6ZEB.js +1041 -0
- package/dist/chunk-F47C6ZEB.js.map +1 -0
- package/dist/chunk-JP4N42AY.js +497 -0
- package/dist/chunk-JP4N42AY.js.map +1 -0
- package/dist/{chunk-P2S7AUOL.js → chunk-LRHHUJFZ.js} +3 -3
- package/dist/{chunk-P2S7AUOL.js.map → chunk-LRHHUJFZ.js.map} +1 -1
- package/dist/{chunk-5B453C4P.js → chunk-O3BT2O42.js} +32 -3
- package/dist/chunk-O3BT2O42.js.map +1 -0
- package/dist/{chunk-EHQMKVDM.js → chunk-Q6DVTCXD.js} +9 -24
- package/dist/chunk-Q6DVTCXD.js.map +1 -0
- package/dist/{chunk-5PGOL2KR.js → chunk-RWPGGWO5.js} +9 -28
- package/dist/chunk-RWPGGWO5.js.map +1 -0
- package/dist/chunk-UJVU7B44.js +764 -0
- package/dist/chunk-UJVU7B44.js.map +1 -0
- package/dist/{chunk-TP3JOOJW.js → chunk-ZBDABVIO.js} +3 -3
- package/dist/{chunk-TP3JOOJW.js.map → chunk-ZBDABVIO.js.map} +1 -1
- package/dist/core/index.cjs +30 -0
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +5 -1
- package/dist/core/index.d.ts +5 -1
- package/dist/core/index.js +1 -1
- package/dist/ecomap/index.cjs +32 -21
- package/dist/ecomap/index.cjs.map +1 -1
- package/dist/ecomap/index.js +2 -2
- package/dist/fault-tree/index.js +2 -2
- package/dist/fishbone/index.js +2 -2
- package/dist/genogram/index.cjs +36 -25
- package/dist/genogram/index.cjs.map +1 -1
- package/dist/genogram/index.d.cts +4 -2
- package/dist/genogram/index.d.ts +4 -2
- package/dist/genogram/index.js +2 -2
- package/dist/index.cjs +2397 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -2
- package/dist/index.d.ts +9 -2
- package/dist/index.js +8 -5
- package/dist/kinship-DqEklrDN.d.ts +51 -0
- package/dist/kinship-Dy_ijjJV.d.cts +51 -0
- package/dist/labels-CBQ_3Ec9.d.cts +123 -0
- package/dist/labels-DNqRkWuI.d.ts +123 -0
- package/dist/labels-RtFw9tX1.d.cts +91 -0
- package/dist/labels-RtFw9tX1.d.ts +91 -0
- package/dist/labels-iZjijjtK.d.cts +64 -0
- package/dist/labels-iZjijjtK.d.ts +64 -0
- package/dist/locales/pt-br.cjs +77 -0
- package/dist/locales/pt-br.cjs.map +1 -1
- package/dist/locales/pt-br.d.cts +12 -2
- package/dist/locales/pt-br.d.ts +12 -2
- package/dist/locales/pt-br.js +72 -1
- package/dist/locales/pt-br.js.map +1 -1
- package/dist/org-chart/index.cjs +853 -0
- package/dist/org-chart/index.cjs.map +1 -0
- package/dist/org-chart/index.d.cts +168 -0
- package/dist/org-chart/index.d.ts +168 -0
- package/dist/org-chart/index.js +4 -0
- package/dist/org-chart/index.js.map +1 -0
- package/dist/pedigree/index.cjs +1151 -0
- package/dist/pedigree/index.cjs.map +1 -0
- package/dist/pedigree/index.d.cts +155 -0
- package/dist/pedigree/index.d.ts +155 -0
- package/dist/pedigree/index.js +4 -0
- package/dist/pedigree/index.js.map +1 -0
- package/dist/phylo/index.cjs +553 -0
- package/dist/phylo/index.cjs.map +1 -0
- package/dist/phylo/index.d.cts +158 -0
- package/dist/phylo/index.d.ts +158 -0
- package/dist/phylo/index.js +4 -0
- package/dist/phylo/index.js.map +1 -0
- package/dist/types-BnMG7TCd.d.cts +66 -0
- package/dist/types-BnMG7TCd.d.ts +66 -0
- package/package.json +42 -3
- package/dist/chunk-5B453C4P.js.map +0 -1
- package/dist/chunk-5PGOL2KR.js.map +0 -1
- package/dist/chunk-EHQMKVDM.js.map +0 -1
- package/dist/kinship-BARO5-qz.d.cts +0 -115
- package/dist/kinship-Bkf87Jhu.d.ts +0 -115
|
@@ -0,0 +1,853 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/org-chart/types.ts
|
|
4
|
+
var ORG_REPORT_KINDS = ["line", "assistant", "dotted"];
|
|
5
|
+
var ORG_VACANCIES = ["filled", "vacant"];
|
|
6
|
+
|
|
7
|
+
// src/org-chart/labels.ts
|
|
8
|
+
var ORG_CHART_TITLE_LABELS_EN = {
|
|
9
|
+
reportKinds: { line: "Reports to", assistant: "Assistant to", dotted: "Dotted-line report to" },
|
|
10
|
+
vacant: "(vacant)"
|
|
11
|
+
};
|
|
12
|
+
var ORG_CHART_SVG_LABELS_EN = {
|
|
13
|
+
legend: { line: "Reports to", assistant: "Assistant", dotted: "Dotted-line / matrix", vacant: "Vacant position" },
|
|
14
|
+
ariaLabel: "Organizational chart"
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/org-chart/validate.ts
|
|
18
|
+
var ORG_MAX_MATRIX_EDGES_PER_NODE = 5;
|
|
19
|
+
var OrgChartValidationError = class extends Error {
|
|
20
|
+
issues;
|
|
21
|
+
constructor(issues) {
|
|
22
|
+
super(`invalid org chart: ${issues.map((i) => i.message).join("; ")}`);
|
|
23
|
+
this.name = "OrgChartValidationError";
|
|
24
|
+
this.issues = issues;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
function listIds(ids) {
|
|
28
|
+
if (ids.length === 1) return String(ids[0]);
|
|
29
|
+
return `${ids.slice(0, -1).join(", ")} and ${ids[ids.length - 1]}`;
|
|
30
|
+
}
|
|
31
|
+
function sortIssues(issues) {
|
|
32
|
+
const unique = /* @__PURE__ */ new Map();
|
|
33
|
+
for (const issue of issues) unique.set(`${issue.code} ${issue.message}`, issue);
|
|
34
|
+
return [...unique.values()].sort(
|
|
35
|
+
(a, b) => a.code !== b.code ? a.code < b.code ? -1 : 1 : a.message < b.message ? -1 : a.message > b.message ? 1 : 0
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
var GRAPH_BLOCKING = /* @__PURE__ */ new Set([
|
|
39
|
+
"duplicate-id",
|
|
40
|
+
"unknown-manager",
|
|
41
|
+
"unknown-report",
|
|
42
|
+
"multiple-managers"
|
|
43
|
+
]);
|
|
44
|
+
function orgChartIssues(input) {
|
|
45
|
+
if (input.positions.length === 0 && input.reports.length === 0) return [];
|
|
46
|
+
const issues = [];
|
|
47
|
+
const push = (code, message) => {
|
|
48
|
+
issues.push({ code, message });
|
|
49
|
+
};
|
|
50
|
+
const positionById = /* @__PURE__ */ new Map();
|
|
51
|
+
const dupPositionIds = /* @__PURE__ */ new Set();
|
|
52
|
+
for (const p of input.positions) {
|
|
53
|
+
if (positionById.has(p.id)) dupPositionIds.add(p.id);
|
|
54
|
+
else positionById.set(p.id, p);
|
|
55
|
+
}
|
|
56
|
+
for (const id of [...dupPositionIds].sort((a, b) => a - b)) {
|
|
57
|
+
push("duplicate-id", `duplicate position id ${id}`);
|
|
58
|
+
}
|
|
59
|
+
const reportById = /* @__PURE__ */ new Map();
|
|
60
|
+
const dupReportIds = /* @__PURE__ */ new Set();
|
|
61
|
+
for (const r of input.reports) {
|
|
62
|
+
if (reportById.has(r.id)) dupReportIds.add(r.id);
|
|
63
|
+
else reportById.set(r.id, r);
|
|
64
|
+
}
|
|
65
|
+
for (const id of [...dupReportIds].sort((a, b) => a - b)) {
|
|
66
|
+
push("duplicate-id", `duplicate report id ${id}`);
|
|
67
|
+
}
|
|
68
|
+
if (issues.length > 0) {
|
|
69
|
+
return sortIssues(issues);
|
|
70
|
+
}
|
|
71
|
+
const reports = [...reportById.values()].sort((a, b) => a.id - b.id);
|
|
72
|
+
const solidParents = /* @__PURE__ */ new Map();
|
|
73
|
+
const assistantReportEdge = /* @__PURE__ */ new Map();
|
|
74
|
+
const solidManagedEdges = /* @__PURE__ */ new Map();
|
|
75
|
+
const dottedDegree = /* @__PURE__ */ new Map();
|
|
76
|
+
for (const r of reports) {
|
|
77
|
+
const hasManager = positionById.has(r.managerId);
|
|
78
|
+
const hasReport = positionById.has(r.reportId);
|
|
79
|
+
if (!hasManager) {
|
|
80
|
+
push("unknown-manager", `report ${r.id} managerId ${r.managerId} is not a declared position`);
|
|
81
|
+
}
|
|
82
|
+
if (!hasReport) {
|
|
83
|
+
push("unknown-report", `report ${r.id} reportId ${r.reportId} is not a declared position`);
|
|
84
|
+
}
|
|
85
|
+
if (r.managerId === r.reportId) {
|
|
86
|
+
push("self-report", `report ${r.id} has managerId === reportId (${r.managerId})`);
|
|
87
|
+
}
|
|
88
|
+
if (r.kind !== "dotted") {
|
|
89
|
+
const arr = solidParents.get(r.reportId) ?? [];
|
|
90
|
+
arr.push(r.id);
|
|
91
|
+
solidParents.set(r.reportId, arr);
|
|
92
|
+
const managed = solidManagedEdges.get(r.managerId) ?? [];
|
|
93
|
+
managed.push(r.id);
|
|
94
|
+
solidManagedEdges.set(r.managerId, managed);
|
|
95
|
+
}
|
|
96
|
+
if (r.kind === "assistant" && r.managerId !== r.reportId && !assistantReportEdge.has(r.reportId)) {
|
|
97
|
+
assistantReportEdge.set(r.reportId, r.id);
|
|
98
|
+
}
|
|
99
|
+
if (r.kind === "dotted" && r.managerId !== r.reportId) {
|
|
100
|
+
if (positionById.has(r.managerId)) dottedDegree.set(r.managerId, (dottedDegree.get(r.managerId) ?? 0) + 1);
|
|
101
|
+
if (positionById.has(r.reportId)) dottedDegree.set(r.reportId, (dottedDegree.get(r.reportId) ?? 0) + 1);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
for (const [reportId, edgeIds] of [...solidParents.entries()].sort((a, b) => a[0] - b[0])) {
|
|
105
|
+
if (edgeIds.length > 1) {
|
|
106
|
+
push(
|
|
107
|
+
"multiple-managers",
|
|
108
|
+
`position ${reportId} is the report of solid edges ${listIds(edgeIds.sort((a, b) => a - b))} \u2014 keep one solid line, make the rest kind "dotted"`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for (const [positionId, assistEdgeId] of [...assistantReportEdge.entries()].sort((a, b) => a[0] - b[0])) {
|
|
113
|
+
const managed = solidManagedEdges.get(positionId);
|
|
114
|
+
if (managed !== void 0 && managed.length > 0) {
|
|
115
|
+
const firstManaged = Math.min(...managed);
|
|
116
|
+
push(
|
|
117
|
+
"assistant-has-reports",
|
|
118
|
+
`position ${positionId} is an assistant (report of edge ${assistEdgeId}) but manages solid edge ${firstManaged} \u2014 assistants are leaves`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
for (const [positionId, count] of [...dottedDegree.entries()].sort((a, b) => a[0] - b[0])) {
|
|
123
|
+
if (count > ORG_MAX_MATRIX_EDGES_PER_NODE) {
|
|
124
|
+
push(
|
|
125
|
+
"too-many-matrix-edges",
|
|
126
|
+
`position ${positionId} has ${count} dotted (matrix) connections \u2014 at most ${ORG_MAX_MATRIX_EDGES_PER_NODE} are supported per position`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (!issues.some((i) => GRAPH_BLOCKING.has(i.code))) {
|
|
131
|
+
const solidChildren = /* @__PURE__ */ new Map();
|
|
132
|
+
for (const r of reports) {
|
|
133
|
+
if (r.kind === "dotted") continue;
|
|
134
|
+
if (r.managerId === r.reportId) continue;
|
|
135
|
+
const arr = solidChildren.get(r.managerId) ?? [];
|
|
136
|
+
arr.push(r.reportId);
|
|
137
|
+
solidChildren.set(r.managerId, arr);
|
|
138
|
+
}
|
|
139
|
+
for (const arr of solidChildren.values()) arr.sort((a, b) => a - b);
|
|
140
|
+
const color = /* @__PURE__ */ new Map();
|
|
141
|
+
const seenCycles = /* @__PURE__ */ new Set();
|
|
142
|
+
const dfs = (start) => {
|
|
143
|
+
const stack = [{ id: start, nextChild: 0 }];
|
|
144
|
+
color.set(start, 1);
|
|
145
|
+
while (stack.length > 0) {
|
|
146
|
+
const frame = stack[stack.length - 1];
|
|
147
|
+
const children = solidChildren.get(frame.id) ?? [];
|
|
148
|
+
if (frame.nextChild >= children.length) {
|
|
149
|
+
color.set(frame.id, 2);
|
|
150
|
+
stack.pop();
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const child = children[frame.nextChild];
|
|
154
|
+
frame.nextChild += 1;
|
|
155
|
+
const c = color.get(child) ?? 0;
|
|
156
|
+
if (c === 1) {
|
|
157
|
+
const from = stack.findIndex((f) => f.id === child);
|
|
158
|
+
const cycle = stack.slice(from).map((f) => f.id);
|
|
159
|
+
const minIdx = cycle.indexOf(Math.min(...cycle));
|
|
160
|
+
const rotated = [...cycle.slice(minIdx), ...cycle.slice(0, minIdx)];
|
|
161
|
+
const key = rotated.join(">");
|
|
162
|
+
if (!seenCycles.has(key)) {
|
|
163
|
+
seenCycles.add(key);
|
|
164
|
+
push("cycle", `cycle: ${[...rotated, rotated[0]].join(" \u2192 ")}`);
|
|
165
|
+
}
|
|
166
|
+
} else if (c === 0) {
|
|
167
|
+
color.set(child, 1);
|
|
168
|
+
stack.push({ id: child, nextChild: 0 });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
for (const p of [...positionById.values()].sort((a, b) => a.id - b.id)) {
|
|
173
|
+
if ((color.get(p.id) ?? 0) === 0) dfs(p.id);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return sortIssues(issues);
|
|
177
|
+
}
|
|
178
|
+
function validateOrgChart(input) {
|
|
179
|
+
const issues = orgChartIssues(input);
|
|
180
|
+
if (issues.length > 0) throw new OrgChartValidationError(issues);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// src/core/xml.ts
|
|
184
|
+
function xmlEscape(text) {
|
|
185
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/core/geometry.ts
|
|
189
|
+
function pathData(points) {
|
|
190
|
+
return points.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x} ${p.y}`).join(" ");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/core/text.ts
|
|
194
|
+
var CHAR_W = 0.6;
|
|
195
|
+
function estimateTextWidth(text, fontPx) {
|
|
196
|
+
return text.length * fontPx * CHAR_W;
|
|
197
|
+
}
|
|
198
|
+
function wrapLabel(label, perLine, maxLines = 2) {
|
|
199
|
+
if (label.length <= perLine) return [label];
|
|
200
|
+
const cap = (s) => s.length > perLine ? s.slice(0, Math.max(1, perLine - 1)) + "\u2026" : s;
|
|
201
|
+
const lines = [""];
|
|
202
|
+
for (const word of label.split(/\s+/)) {
|
|
203
|
+
const last = lines.length - 1;
|
|
204
|
+
const current = lines[last];
|
|
205
|
+
if (current === "" || (current + " " + word).length <= perLine) {
|
|
206
|
+
lines[last] = current === "" ? word : `${current} ${word}`;
|
|
207
|
+
} else if (lines.length < maxLines) {
|
|
208
|
+
lines.push(word);
|
|
209
|
+
} else {
|
|
210
|
+
lines[last] = `${current} ${word}`;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
214
|
+
return lines.map(cap);
|
|
215
|
+
}
|
|
216
|
+
function wrapLabelBalanced(label, maxLines) {
|
|
217
|
+
const perLine = Math.min(26, Math.max(14, Math.ceil(label.length / 2) + 2));
|
|
218
|
+
return wrapLabel(label, perLine, maxLines);
|
|
219
|
+
}
|
|
220
|
+
function clampLabel(label, maxChars) {
|
|
221
|
+
if (maxChars === void 0 || label.length <= maxChars) return label;
|
|
222
|
+
return label.slice(0, Math.max(1, maxChars - 1)) + "\u2026";
|
|
223
|
+
}
|
|
224
|
+
var FONT_FAMILY = "Helvetica, Arial, sans-serif";
|
|
225
|
+
|
|
226
|
+
// src/core/legend.ts
|
|
227
|
+
var LEGEND_ROW_H = 18;
|
|
228
|
+
var LEGEND_PAD = 16;
|
|
229
|
+
var LEGEND_SWATCH_W = 22;
|
|
230
|
+
var LEGEND_GAP = 14;
|
|
231
|
+
var LEGEND_FONT = 11;
|
|
232
|
+
var LEGEND_TEXT_FILL = "#52525b";
|
|
233
|
+
function legendBlock(entries, startY) {
|
|
234
|
+
if (entries.length === 0) return { svg: "", width: 0, height: startY };
|
|
235
|
+
const rows = entries.map((entry, i) => {
|
|
236
|
+
const rowCenterY = startY + i * LEGEND_ROW_H + LEGEND_ROW_H / 2;
|
|
237
|
+
const textX = LEGEND_PAD + LEGEND_SWATCH_W + LEGEND_GAP;
|
|
238
|
+
return entry.swatch(LEGEND_PAD, rowCenterY) + `<text x="${textX}" y="${rowCenterY + LEGEND_FONT * 0.32}" font-family="${FONT_FAMILY}" font-size="${LEGEND_FONT}" fill="${LEGEND_TEXT_FILL}">${xmlEscape(entry.label)}</text>`;
|
|
239
|
+
});
|
|
240
|
+
const widestLabel = entries.reduce((m, e) => Math.max(m, estimateTextWidth(e.label, LEGEND_FONT)), 0);
|
|
241
|
+
return {
|
|
242
|
+
svg: `<g data-compasso-legend="true">${rows.join("")}</g>`,
|
|
243
|
+
width: LEGEND_PAD + LEGEND_SWATCH_W + LEGEND_GAP + widestLabel + LEGEND_PAD,
|
|
244
|
+
height: startY + entries.length * LEGEND_ROW_H + LEGEND_PAD / 2
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// src/core/stroke.ts
|
|
249
|
+
var EDGE_STROKE = {
|
|
250
|
+
distant: { width: 1.5, dash: [4, 4], opacity: 0.55 }};
|
|
251
|
+
|
|
252
|
+
// src/org-chart/layout.ts
|
|
253
|
+
var ORG_LABEL_FONT = 12;
|
|
254
|
+
var ORG_TITLE_FONT = 10;
|
|
255
|
+
var ORG_LABEL_LINE_H = 14;
|
|
256
|
+
var PADDING = 32;
|
|
257
|
+
var ORG_SIBLING_GAP = 28;
|
|
258
|
+
var ORG_TREE_GAP = 56;
|
|
259
|
+
var ORG_CORRIDOR = 30;
|
|
260
|
+
var ORG_MIN_BOX_W = 120;
|
|
261
|
+
var ORG_BOX_PAD_X = 12;
|
|
262
|
+
var ORG_BOX_PAD_Y = 9;
|
|
263
|
+
var ORG_ASSIST_GAP = 24;
|
|
264
|
+
var ORG_ASSIST_BAND_DROP = 14;
|
|
265
|
+
var ORG_MATRIX_GUTTER_GAP = 28;
|
|
266
|
+
var ORG_MATRIX_LANE_PITCH = 14;
|
|
267
|
+
var ORG_MATRIX_ESCAPE_STEP = 2;
|
|
268
|
+
var ORG_STEM_ID_BASE = 1e6;
|
|
269
|
+
var ORG_BUS_ID_BASE = 2e6;
|
|
270
|
+
var ORG_DROP_ID_BASE = 3e6;
|
|
271
|
+
var ORG_ASSIST_ID_BASE = 4e6;
|
|
272
|
+
var ORG_DOTTED_ID_BASE = 5e6;
|
|
273
|
+
var round = (n) => Math.round(n * 100) / 100;
|
|
274
|
+
var drawnLeftEdge = (cx, boxW) => round(round(cx) - round(boxW) / 2);
|
|
275
|
+
var drawnRightEdge = (cx, boxW) => drawnLeftEdge(cx, boxW) + round(boxW);
|
|
276
|
+
function packSubtree(node, gaps) {
|
|
277
|
+
const { ownHalfL, ownHalfR, children } = node;
|
|
278
|
+
if (children.length === 0) {
|
|
279
|
+
return { halfL: ownHalfL, halfR: ownHalfR, offsets: [] };
|
|
280
|
+
}
|
|
281
|
+
const xs = [0];
|
|
282
|
+
for (let i = 1; i < children.length; i++) {
|
|
283
|
+
xs.push(xs[i - 1] + children[i - 1].halfR + gaps.siblingGap + children[i].halfL);
|
|
284
|
+
}
|
|
285
|
+
const first = children[0];
|
|
286
|
+
const last = children[children.length - 1];
|
|
287
|
+
const axis = (xs[0] + xs[xs.length - 1]) / 2;
|
|
288
|
+
const offsets = xs.map((x) => x - axis);
|
|
289
|
+
const halfL = Math.max(ownHalfL, axis - (xs[0] - first.halfL));
|
|
290
|
+
const halfR = Math.max(ownHalfR, xs[xs.length - 1] + last.halfR - axis);
|
|
291
|
+
return { halfL, halfR, offsets };
|
|
292
|
+
}
|
|
293
|
+
function measurePosition(position, maxLabelChars, vacantWord) {
|
|
294
|
+
const nameLines = position.name === "" ? [] : wrapLabelBalanced(clampLabel(position.name, maxLabelChars), 2);
|
|
295
|
+
const titleLines = position.title === null ? [] : wrapLabelBalanced(clampLabel(position.title, maxLabelChars), 2);
|
|
296
|
+
const subtitleLines = position.subtitle === null ? [] : wrapLabelBalanced(clampLabel(position.subtitle, maxLabelChars), 1);
|
|
297
|
+
const vacantMarker = position.vacancy === "vacant" ? vacantWord : null;
|
|
298
|
+
let innerW = 0;
|
|
299
|
+
for (const l of nameLines) innerW = Math.max(innerW, estimateTextWidth(l, ORG_LABEL_FONT));
|
|
300
|
+
for (const l of titleLines) innerW = Math.max(innerW, estimateTextWidth(l, ORG_TITLE_FONT));
|
|
301
|
+
for (const l of subtitleLines) innerW = Math.max(innerW, estimateTextWidth(l, ORG_TITLE_FONT));
|
|
302
|
+
if (vacantMarker !== null) innerW = Math.max(innerW, estimateTextWidth(vacantMarker, ORG_TITLE_FONT));
|
|
303
|
+
const boxW = Math.max(ORG_MIN_BOX_W, innerW + ORG_BOX_PAD_X * 2);
|
|
304
|
+
const lineCount = nameLines.length + titleLines.length + subtitleLines.length + (vacantMarker !== null ? 1 : 0);
|
|
305
|
+
const boxH = ORG_BOX_PAD_Y * 2 + lineCount * ORG_LABEL_LINE_H;
|
|
306
|
+
return { boxW, boxH, nameLines, titleLines, subtitleLines, vacantMarker };
|
|
307
|
+
}
|
|
308
|
+
function positionTitle(position, vacantWord) {
|
|
309
|
+
if (position.hover !== void 0) return position.hover;
|
|
310
|
+
if (position.title !== null) {
|
|
311
|
+
return position.name === "" ? position.title : `${position.name} \u2014 ${position.title}`;
|
|
312
|
+
}
|
|
313
|
+
if (position.name !== "") return position.name;
|
|
314
|
+
return vacantWord;
|
|
315
|
+
}
|
|
316
|
+
function computeOrgChartLayout(input, opts = {}) {
|
|
317
|
+
if (input.positions.length === 0 && input.reports.length === 0) {
|
|
318
|
+
return { width: PADDING * 2, height: PADDING * 2, nodes: [], elements: [], rootPositionIds: [] };
|
|
319
|
+
}
|
|
320
|
+
validateOrgChart(input);
|
|
321
|
+
const titleLabels = opts.titleLabels ?? ORG_CHART_TITLE_LABELS_EN;
|
|
322
|
+
const vacantWord = titleLabels.vacant;
|
|
323
|
+
const positionById = new Map(input.positions.map((p) => [p.id, p]));
|
|
324
|
+
const reports = [...input.reports].sort((a, b) => a.id - b.id);
|
|
325
|
+
const solidReports = reports.filter((r) => r.kind !== "dotted");
|
|
326
|
+
const lineChildIds = /* @__PURE__ */ new Map();
|
|
327
|
+
const assistChildIds = /* @__PURE__ */ new Map();
|
|
328
|
+
const solidParentReportId = /* @__PURE__ */ new Set();
|
|
329
|
+
for (const r of solidReports) {
|
|
330
|
+
solidParentReportId.add(r.reportId);
|
|
331
|
+
const bucket = r.kind === "assistant" ? assistChildIds : lineChildIds;
|
|
332
|
+
const arr = bucket.get(r.managerId) ?? [];
|
|
333
|
+
arr.push(r);
|
|
334
|
+
bucket.set(r.managerId, arr);
|
|
335
|
+
}
|
|
336
|
+
for (const arr of lineChildIds.values()) arr.sort((a, b) => a.reportId - b.reportId);
|
|
337
|
+
for (const arr of assistChildIds.values()) arr.sort((a, b) => a.reportId - b.reportId);
|
|
338
|
+
const rootPositionIds = input.positions.map((p) => p.id).filter((id) => !solidParentReportId.has(id)).sort((a, b) => a - b);
|
|
339
|
+
const newInst = (position, depth, assistReport) => {
|
|
340
|
+
const m = measurePosition(position, opts.maxLabelChars, vacantWord);
|
|
341
|
+
return {
|
|
342
|
+
position,
|
|
343
|
+
m,
|
|
344
|
+
style: position.vacancy === "vacant" ? "dashed" : "solid",
|
|
345
|
+
title: positionTitle(position, vacantWord),
|
|
346
|
+
depth,
|
|
347
|
+
lineChildren: [],
|
|
348
|
+
assistants: [],
|
|
349
|
+
ownHalfL: m.boxW / 2,
|
|
350
|
+
ownHalfR: m.boxW / 2,
|
|
351
|
+
spanL: 0,
|
|
352
|
+
spanR: 0,
|
|
353
|
+
offsets: [],
|
|
354
|
+
cx: 0,
|
|
355
|
+
assistReport,
|
|
356
|
+
assistTop: 0
|
|
357
|
+
};
|
|
358
|
+
};
|
|
359
|
+
const allInsts = [];
|
|
360
|
+
const build = (positionId, depth, assistReport) => {
|
|
361
|
+
const position = positionById.get(positionId);
|
|
362
|
+
const inst = newInst(position, depth, assistReport);
|
|
363
|
+
allInsts.push(inst);
|
|
364
|
+
for (const r of assistChildIds.get(positionId) ?? []) {
|
|
365
|
+
const a = build(r.reportId, depth + 1, r);
|
|
366
|
+
inst.assistants.push({ inst: a, report: r });
|
|
367
|
+
}
|
|
368
|
+
for (const r of lineChildIds.get(positionId) ?? []) {
|
|
369
|
+
inst.lineChildren.push(build(r.reportId, depth + 1, null));
|
|
370
|
+
}
|
|
371
|
+
return inst;
|
|
372
|
+
};
|
|
373
|
+
const roots = rootPositionIds.map((id) => build(id, 0, null));
|
|
374
|
+
for (const inst of allInsts) {
|
|
375
|
+
if (inst.assistants.length > 0) {
|
|
376
|
+
const widest = inst.assistants.reduce((m, a) => Math.max(m, a.inst.m.boxW), 0);
|
|
377
|
+
inst.ownHalfL = inst.m.boxW / 2 + ORG_ASSIST_GAP + widest;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
const gaps = { siblingGap: ORG_SIBLING_GAP };
|
|
381
|
+
const pack = (inst) => {
|
|
382
|
+
for (const c of inst.lineChildren) pack(c);
|
|
383
|
+
const result = packSubtree(
|
|
384
|
+
{
|
|
385
|
+
ownHalfL: inst.ownHalfL,
|
|
386
|
+
ownHalfR: inst.ownHalfR,
|
|
387
|
+
children: inst.lineChildren.map((c) => ({ halfL: c.spanL, halfR: c.spanR }))
|
|
388
|
+
},
|
|
389
|
+
gaps
|
|
390
|
+
);
|
|
391
|
+
inst.spanL = result.halfL;
|
|
392
|
+
inst.spanR = result.halfR;
|
|
393
|
+
inst.offsets = result.offsets;
|
|
394
|
+
};
|
|
395
|
+
for (const root of roots) pack(root);
|
|
396
|
+
let cursor = PADDING;
|
|
397
|
+
for (const root of roots) {
|
|
398
|
+
root.cx = cursor + root.spanL;
|
|
399
|
+
const placeX = (inst) => {
|
|
400
|
+
inst.lineChildren.forEach((c, i) => {
|
|
401
|
+
c.cx = inst.cx + inst.offsets[i];
|
|
402
|
+
placeX(c);
|
|
403
|
+
});
|
|
404
|
+
};
|
|
405
|
+
placeX(root);
|
|
406
|
+
cursor = root.cx + root.spanR + ORG_TREE_GAP;
|
|
407
|
+
}
|
|
408
|
+
const canvasRight = roots.length === 0 ? PADDING : cursor - ORG_TREE_GAP;
|
|
409
|
+
const rowInsts = [];
|
|
410
|
+
for (const inst of allInsts) (rowInsts[inst.depth] ??= []).push(inst);
|
|
411
|
+
const depthCount = rowInsts.length;
|
|
412
|
+
const assistStackH = (inst) => {
|
|
413
|
+
if (inst.assistants.length === 0) return 0;
|
|
414
|
+
return inst.assistants.reduce((s, a) => s + a.inst.m.boxH + ORG_ASSIST_BAND_DROP, 0) + ORG_ASSIST_BAND_DROP;
|
|
415
|
+
};
|
|
416
|
+
const rowH = [];
|
|
417
|
+
for (let d = 0; d < depthCount; d++) {
|
|
418
|
+
const lineBoxes = (rowInsts[d] ?? []).filter((i) => i.assistReport === null);
|
|
419
|
+
rowH.push(lineBoxes.reduce((m, i) => Math.max(m, i.m.boxH), 0));
|
|
420
|
+
}
|
|
421
|
+
const assistZone = [];
|
|
422
|
+
for (let d = 0; d < depthCount; d++) {
|
|
423
|
+
let tallest = 0;
|
|
424
|
+
for (const inst of rowInsts[d] ?? []) tallest = Math.max(tallest, assistStackH(inst));
|
|
425
|
+
assistZone.push(tallest);
|
|
426
|
+
}
|
|
427
|
+
const rowTop = [PADDING];
|
|
428
|
+
for (let d = 0; d < depthCount - 1; d++) {
|
|
429
|
+
rowTop.push(rowTop[d] + rowH[d] + assistZone[d] + ORG_CORRIDOR);
|
|
430
|
+
}
|
|
431
|
+
const busY = (d) => rowTop[d + 1] - ORG_CORRIDOR / 2;
|
|
432
|
+
const last = depthCount - 1;
|
|
433
|
+
const height = Math.ceil(rowTop[last] + rowH[last] + assistZone[last] + PADDING);
|
|
434
|
+
const nodes = [];
|
|
435
|
+
const elements = [];
|
|
436
|
+
const reportTitle = (kind, label) => {
|
|
437
|
+
const word = titleLabels.reportKinds[kind];
|
|
438
|
+
return label !== null && label !== void 0 ? `${word} \xB7 ${label}` : word;
|
|
439
|
+
};
|
|
440
|
+
const pushNode = (inst) => {
|
|
441
|
+
nodes.push({
|
|
442
|
+
positionId: inst.position.id,
|
|
443
|
+
cx: round(inst.cx),
|
|
444
|
+
top: round(rowTop[inst.depth]),
|
|
445
|
+
boxW: round(inst.m.boxW),
|
|
446
|
+
boxH: round(inst.m.boxH),
|
|
447
|
+
style: inst.style,
|
|
448
|
+
nameLines: inst.m.nameLines,
|
|
449
|
+
titleLines: inst.m.titleLines,
|
|
450
|
+
subtitleLines: inst.m.subtitleLines,
|
|
451
|
+
vacantMarker: inst.m.vacantMarker,
|
|
452
|
+
isAssistant: inst.assistReport !== null,
|
|
453
|
+
depth: inst.depth,
|
|
454
|
+
title: inst.title
|
|
455
|
+
});
|
|
456
|
+
};
|
|
457
|
+
const emit = (inst) => {
|
|
458
|
+
const d = inst.depth;
|
|
459
|
+
let bandY = rowTop[d] + rowH[d] + ORG_ASSIST_BAND_DROP;
|
|
460
|
+
for (const { inst: a, report } of inst.assistants) {
|
|
461
|
+
const assistRight = inst.cx - ORG_ASSIST_GAP;
|
|
462
|
+
a.cx = assistRight - a.m.boxW / 2;
|
|
463
|
+
const assistTop = bandY;
|
|
464
|
+
a.assistTop = assistTop;
|
|
465
|
+
const assistMidY = assistTop + a.m.boxH / 2;
|
|
466
|
+
nodes.push({
|
|
467
|
+
positionId: a.position.id,
|
|
468
|
+
cx: round(a.cx),
|
|
469
|
+
top: round(assistTop),
|
|
470
|
+
boxW: round(a.m.boxW),
|
|
471
|
+
boxH: round(a.m.boxH),
|
|
472
|
+
style: a.style,
|
|
473
|
+
nameLines: a.m.nameLines,
|
|
474
|
+
titleLines: a.m.titleLines,
|
|
475
|
+
subtitleLines: a.m.subtitleLines,
|
|
476
|
+
vacantMarker: a.m.vacantMarker,
|
|
477
|
+
isAssistant: true,
|
|
478
|
+
depth: a.depth,
|
|
479
|
+
title: a.title
|
|
480
|
+
});
|
|
481
|
+
elements.push({
|
|
482
|
+
edgeId: ORG_ASSIST_ID_BASE + report.reportId,
|
|
483
|
+
kind: "assist",
|
|
484
|
+
points: [
|
|
485
|
+
{ x: round(inst.cx), y: round(assistMidY) },
|
|
486
|
+
{ x: drawnRightEdge(a.cx, a.m.boxW), y: round(assistMidY) }
|
|
487
|
+
],
|
|
488
|
+
dashed: false,
|
|
489
|
+
title: reportTitle("assistant", report.label)
|
|
490
|
+
});
|
|
491
|
+
bandY += a.m.boxH + ORG_ASSIST_BAND_DROP;
|
|
492
|
+
}
|
|
493
|
+
pushNode(inst);
|
|
494
|
+
if (inst.lineChildren.length === 0) return;
|
|
495
|
+
const by = busY(d);
|
|
496
|
+
const boxBottom = rowTop[d] + inst.m.boxH;
|
|
497
|
+
elements.push({
|
|
498
|
+
edgeId: ORG_STEM_ID_BASE + inst.position.id,
|
|
499
|
+
kind: "stem",
|
|
500
|
+
points: [
|
|
501
|
+
{ x: round(inst.cx), y: round(boxBottom) },
|
|
502
|
+
{ x: round(inst.cx), y: round(by) }
|
|
503
|
+
],
|
|
504
|
+
dashed: false,
|
|
505
|
+
title: reportTitle("line", null)
|
|
506
|
+
});
|
|
507
|
+
const childCxs = inst.lineChildren.map((c) => c.cx);
|
|
508
|
+
if (inst.lineChildren.length > 1) {
|
|
509
|
+
elements.push({
|
|
510
|
+
edgeId: ORG_BUS_ID_BASE + inst.position.id,
|
|
511
|
+
kind: "bus",
|
|
512
|
+
points: [
|
|
513
|
+
{ x: round(Math.min(...childCxs)), y: round(by) },
|
|
514
|
+
{ x: round(Math.max(...childCxs)), y: round(by) }
|
|
515
|
+
],
|
|
516
|
+
dashed: false,
|
|
517
|
+
title: reportTitle("line", null)
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
for (const c of inst.lineChildren) {
|
|
521
|
+
elements.push({
|
|
522
|
+
edgeId: ORG_DROP_ID_BASE + c.position.id,
|
|
523
|
+
kind: "drop",
|
|
524
|
+
points: [
|
|
525
|
+
{ x: round(c.cx), y: round(by) },
|
|
526
|
+
{ x: round(c.cx), y: round(rowTop[c.depth]) }
|
|
527
|
+
],
|
|
528
|
+
dashed: false,
|
|
529
|
+
title: reportTitle("line", null)
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
for (const c of inst.lineChildren) emit(c);
|
|
533
|
+
};
|
|
534
|
+
for (const root of roots) emit(root);
|
|
535
|
+
const instByPos = new Map(allInsts.map((i) => [i.position.id, i]));
|
|
536
|
+
const escapeByPos = /* @__PURE__ */ new Map();
|
|
537
|
+
for (const inst of allInsts) {
|
|
538
|
+
const isAssist = inst.assistReport !== null;
|
|
539
|
+
const boxTop = isAssist ? inst.assistTop : rowTop[inst.depth];
|
|
540
|
+
const leftEdge = drawnLeftEdge(inst.cx, inst.m.boxW);
|
|
541
|
+
const rightEdge = drawnRightEdge(inst.cx, inst.m.boxW);
|
|
542
|
+
if (isAssist) {
|
|
543
|
+
const managerInst = instByPos.get(inst.assistReport.managerId);
|
|
544
|
+
escapeByPos.set(inst.position.id, {
|
|
545
|
+
side: -1,
|
|
546
|
+
boxEdge: leftEdge,
|
|
547
|
+
escCol: managerInst.cx - managerInst.spanL,
|
|
548
|
+
midY: boxTop + inst.m.boxH / 2,
|
|
549
|
+
boxH: inst.m.boxH
|
|
550
|
+
});
|
|
551
|
+
} else {
|
|
552
|
+
escapeByPos.set(inst.position.id, {
|
|
553
|
+
side: 1,
|
|
554
|
+
boxEdge: rightEdge,
|
|
555
|
+
escCol: inst.cx + inst.spanR,
|
|
556
|
+
midY: boxTop + inst.m.boxH / 2,
|
|
557
|
+
boxH: inst.m.boxH
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
const nodeById = new Map(nodes.map((n) => [n.positionId, n]));
|
|
562
|
+
const dottedReports = reports.filter(
|
|
563
|
+
(r) => r.kind === "dotted" && nodeById.has(r.managerId) && nodeById.has(r.reportId) && r.managerId !== r.reportId
|
|
564
|
+
);
|
|
565
|
+
let maxBoxBottom = PADDING;
|
|
566
|
+
for (const n of nodes) maxBoxBottom = Math.max(maxBoxBottom, n.top + n.boxH);
|
|
567
|
+
let laneCount = 0;
|
|
568
|
+
let channelLanes = 0;
|
|
569
|
+
if (dottedReports.length > 0) {
|
|
570
|
+
const laneOf = /* @__PURE__ */ new Map();
|
|
571
|
+
laneCount = allocateLanes(
|
|
572
|
+
dottedReports.map((r) => {
|
|
573
|
+
const rep = escapeByPos.get(r.reportId);
|
|
574
|
+
const mgr = escapeByPos.get(r.managerId);
|
|
575
|
+
return {
|
|
576
|
+
lo: Math.min(rep.midY, mgr.midY),
|
|
577
|
+
hi: Math.max(rep.midY, mgr.midY),
|
|
578
|
+
set: (lane) => laneOf.set(r.id, lane)
|
|
579
|
+
};
|
|
580
|
+
})
|
|
581
|
+
);
|
|
582
|
+
channelLanes = dottedReports.length * 2;
|
|
583
|
+
const channelTop = maxBoxBottom + ORG_MATRIX_GUTTER_GAP;
|
|
584
|
+
const exitSlot = /* @__PURE__ */ new Map();
|
|
585
|
+
const nextExitY = (posId, mid, boxH) => {
|
|
586
|
+
const slot = exitSlot.get(posId) ?? 0;
|
|
587
|
+
exitSlot.set(posId, slot + 1);
|
|
588
|
+
if (slot === 0) return mid;
|
|
589
|
+
const step = Math.min(6, Math.max(2, (boxH / 2 - 6) / Math.max(1, Math.ceil(slot / 2))));
|
|
590
|
+
const sign = slot % 2 === 1 ? -1 : 1;
|
|
591
|
+
return mid + sign * Math.ceil(slot / 2) * step;
|
|
592
|
+
};
|
|
593
|
+
const geoms = dottedReports.map((r, i) => {
|
|
594
|
+
const rep = escapeByPos.get(r.reportId);
|
|
595
|
+
const mgr = escapeByPos.get(r.managerId);
|
|
596
|
+
return {
|
|
597
|
+
r,
|
|
598
|
+
i,
|
|
599
|
+
repMidY: round(nextExitY(r.reportId, rep.midY, rep.boxH)),
|
|
600
|
+
mgrMidY: round(nextExitY(r.managerId, mgr.midY, mgr.boxH)),
|
|
601
|
+
repBoxEdge: round(rep.boxEdge),
|
|
602
|
+
mgrBoxEdge: round(mgr.boxEdge),
|
|
603
|
+
repChY: round(channelTop + 2 * i * ORG_MATRIX_LANE_PITCH),
|
|
604
|
+
mgrChY: round(channelTop + (2 * i + 1) * ORG_MATRIX_LANE_PITCH),
|
|
605
|
+
laneX: round(canvasRight + ORG_MATRIX_GUTTER_GAP + laneOf.get(r.id) * ORG_MATRIX_LANE_PITCH),
|
|
606
|
+
repEscX: 0,
|
|
607
|
+
mgrEscX: 0
|
|
608
|
+
};
|
|
609
|
+
});
|
|
610
|
+
const verts = [];
|
|
611
|
+
for (const g of geoms) {
|
|
612
|
+
const rep = escapeByPos.get(g.r.reportId);
|
|
613
|
+
const mgr = escapeByPos.get(g.r.managerId);
|
|
614
|
+
verts.push({
|
|
615
|
+
escCol: rep.escCol,
|
|
616
|
+
side: rep.side,
|
|
617
|
+
lo: Math.min(g.repMidY, g.repChY),
|
|
618
|
+
hi: Math.max(g.repMidY, g.repChY),
|
|
619
|
+
assign: (x) => g.repEscX = x
|
|
620
|
+
});
|
|
621
|
+
verts.push({
|
|
622
|
+
escCol: mgr.escCol,
|
|
623
|
+
side: mgr.side,
|
|
624
|
+
lo: Math.min(g.mgrMidY, g.mgrChY),
|
|
625
|
+
hi: Math.max(g.mgrMidY, g.mgrChY),
|
|
626
|
+
assign: (x) => g.mgrEscX = x
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
const VERT_EPS = 0.01;
|
|
630
|
+
const ordered = verts.map((v, idx) => ({ v, key: geoms[Math.floor(idx / 2)].r.id * 2 + idx % 2 })).sort((a, b) => a.key - b.key).map((e) => e.v);
|
|
631
|
+
const placed = [];
|
|
632
|
+
const yOverlap = (aLo, aHi, bLo, bHi) => Math.min(aHi, bHi) - Math.max(aLo, bLo) > VERT_EPS;
|
|
633
|
+
for (const v of ordered) {
|
|
634
|
+
let band = v.side === 1 ? Math.max(canvasRight - v.escCol, ORG_MATRIX_GUTTER_GAP) : v.escCol;
|
|
635
|
+
for (const n of nodes) {
|
|
636
|
+
const bLeft = n.cx - n.boxW / 2;
|
|
637
|
+
const bRight = n.cx + n.boxW / 2;
|
|
638
|
+
if (n.top + n.boxH <= v.lo + VERT_EPS || n.top >= v.hi - VERT_EPS) continue;
|
|
639
|
+
if (v.side === 1 && bLeft > v.escCol + VERT_EPS) band = Math.min(band, bLeft - v.escCol);
|
|
640
|
+
else if (v.side === -1 && bRight < v.escCol - VERT_EPS) band = Math.min(band, v.escCol - bRight);
|
|
641
|
+
}
|
|
642
|
+
let lane = 0;
|
|
643
|
+
let candidate = round(v.escCol);
|
|
644
|
+
while (placed.some((p) => Math.abs(p.x - candidate) < VERT_EPS && yOverlap(p.lo, p.hi, v.lo, v.hi))) {
|
|
645
|
+
lane += 1;
|
|
646
|
+
candidate = round(v.escCol + v.side * lane * ORG_MATRIX_ESCAPE_STEP);
|
|
647
|
+
}
|
|
648
|
+
if (lane > 0 && lane * ORG_MATRIX_ESCAPE_STEP >= band - VERT_EPS) {
|
|
649
|
+
throw new Error(
|
|
650
|
+
`org-chart: escape-column-overflow \u2014 escape vertical nudged ${lane * ORG_MATRIX_ESCAPE_STEP}px from its base column exceeds the ${round(band)}px box-free band`
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
placed.push({ x: candidate, lo: v.lo, hi: v.hi });
|
|
654
|
+
v.assign(candidate);
|
|
655
|
+
}
|
|
656
|
+
for (const g of geoms) {
|
|
657
|
+
elements.push({
|
|
658
|
+
edgeId: ORG_DOTTED_ID_BASE + g.r.id,
|
|
659
|
+
kind: "dotted",
|
|
660
|
+
points: collapseDegenerate([
|
|
661
|
+
{ x: g.repBoxEdge, y: g.repMidY },
|
|
662
|
+
// out the report box's edge
|
|
663
|
+
{ x: g.repEscX, y: g.repMidY },
|
|
664
|
+
// → box-free escape column (in own gutter lane)
|
|
665
|
+
{ x: g.repEscX, y: g.repChY },
|
|
666
|
+
// → down into the bottom traverse channel
|
|
667
|
+
{ x: g.laneX, y: g.repChY },
|
|
668
|
+
// → across (below all boxes) to the right gutter lane
|
|
669
|
+
{ x: g.laneX, y: g.mgrChY },
|
|
670
|
+
// → down the right gutter (right of every box)
|
|
671
|
+
{ x: g.mgrEscX, y: g.mgrChY },
|
|
672
|
+
// → back across (below all boxes) to the manager column
|
|
673
|
+
{ x: g.mgrEscX, y: g.mgrMidY },
|
|
674
|
+
// → up the box-free escape column
|
|
675
|
+
{ x: g.mgrBoxEdge, y: g.mgrMidY }
|
|
676
|
+
// → into the manager box's edge
|
|
677
|
+
]),
|
|
678
|
+
dashed: true,
|
|
679
|
+
title: reportTitle("dotted", g.r.label)
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
const width = dottedReports.length > 0 ? Math.ceil(canvasRight + ORG_MATRIX_GUTTER_GAP + (laneCount - 1) * ORG_MATRIX_LANE_PITCH + ORG_MATRIX_GUTTER_GAP + PADDING) : Math.ceil(canvasRight + PADDING);
|
|
684
|
+
const finalHeight = channelLanes > 0 ? Math.ceil(maxBoxBottom + ORG_MATRIX_GUTTER_GAP + (channelLanes - 1) * ORG_MATRIX_LANE_PITCH + PADDING) : height;
|
|
685
|
+
return { width, height: finalHeight, nodes, elements, rootPositionIds };
|
|
686
|
+
}
|
|
687
|
+
function collapseDegenerate(points) {
|
|
688
|
+
const out = [];
|
|
689
|
+
for (const p of points) {
|
|
690
|
+
const prev = out[out.length - 1];
|
|
691
|
+
if (prev === void 0 || prev.x !== p.x || prev.y !== p.y) out.push(p);
|
|
692
|
+
}
|
|
693
|
+
return out;
|
|
694
|
+
}
|
|
695
|
+
function allocateLanes(items) {
|
|
696
|
+
const lanes = [];
|
|
697
|
+
for (const it of items) {
|
|
698
|
+
let chosen = -1;
|
|
699
|
+
for (let l = 0; l < lanes.length; l++) {
|
|
700
|
+
if (lanes[l].every((o) => it.hi <= o.lo || it.lo >= o.hi)) {
|
|
701
|
+
chosen = l;
|
|
702
|
+
break;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (chosen === -1) {
|
|
706
|
+
chosen = lanes.length;
|
|
707
|
+
lanes.push([]);
|
|
708
|
+
}
|
|
709
|
+
lanes[chosen].push({ lo: it.lo, hi: it.hi });
|
|
710
|
+
it.set(chosen);
|
|
711
|
+
}
|
|
712
|
+
return lanes.length;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// src/org-chart/svg.ts
|
|
716
|
+
var GLYPH_STROKE = "#52525b";
|
|
717
|
+
var LABEL_FILL = "#3f3f46";
|
|
718
|
+
var EDGE_INK = "#71717a";
|
|
719
|
+
var GLYPH_ATTRS = `fill="transparent" stroke="${GLYPH_STROKE}" stroke-width="2"`;
|
|
720
|
+
var VACANT_DASH = `stroke-dasharray="6,4"`;
|
|
721
|
+
var DOTTED = EDGE_STROKE.distant;
|
|
722
|
+
var round2 = (n) => Math.round(n * 100) / 100;
|
|
723
|
+
var ORG_BOX_PAD_Y2 = 9;
|
|
724
|
+
function boxSvg(n) {
|
|
725
|
+
const left = round2(n.cx - n.boxW / 2);
|
|
726
|
+
const pieces = [`<title>${xmlEscape(n.title)}</title>`];
|
|
727
|
+
if (n.style === "dashed") {
|
|
728
|
+
pieces.push(
|
|
729
|
+
`<rect x="${left}" y="${n.top}" width="${n.boxW}" height="${n.boxH}" rx="2" ${GLYPH_ATTRS} ${VACANT_DASH}/>`
|
|
730
|
+
);
|
|
731
|
+
const ix = round2(left + 3);
|
|
732
|
+
const iy = round2(n.top + 3);
|
|
733
|
+
const iw = round2(n.boxW - 6);
|
|
734
|
+
const ih = round2(n.boxH - 6);
|
|
735
|
+
pieces.push(`<rect x="${ix}" y="${iy}" width="${iw}" height="${ih}" rx="2" ${GLYPH_ATTRS} ${VACANT_DASH}/>`);
|
|
736
|
+
} else {
|
|
737
|
+
pieces.push(`<rect x="${left}" y="${n.top}" width="${n.boxW}" height="${n.boxH}" rx="2" ${GLYPH_ATTRS}/>`);
|
|
738
|
+
}
|
|
739
|
+
let lineIndex = 0;
|
|
740
|
+
const firstBaseline = (i) => round2(n.top + ORG_BOX_PAD_Y2 + ORG_LABEL_LINE_H / 2 + i * ORG_LABEL_LINE_H + ORG_LABEL_FONT * 0.32);
|
|
741
|
+
const textBlock = (lines, font) => {
|
|
742
|
+
if (lines.length === 0) return "";
|
|
743
|
+
const tspans = lines.map((line, i) => `<tspan x="${n.cx}" y="${firstBaseline(lineIndex + i)}">${xmlEscape(line)}</tspan>`).join("");
|
|
744
|
+
lineIndex += lines.length;
|
|
745
|
+
return `<text text-anchor="middle" font-family="${FONT_FAMILY}" font-size="${font}" fill="${LABEL_FILL}">${tspans}</text>`;
|
|
746
|
+
};
|
|
747
|
+
pieces.push(textBlock(n.nameLines, ORG_LABEL_FONT));
|
|
748
|
+
pieces.push(textBlock(n.titleLines, ORG_TITLE_FONT));
|
|
749
|
+
pieces.push(textBlock(n.subtitleLines, ORG_TITLE_FONT));
|
|
750
|
+
if (n.vacantMarker !== null) pieces.push(textBlock([n.vacantMarker], ORG_TITLE_FONT));
|
|
751
|
+
return `<g data-node-id="p${n.positionId}">${pieces.filter((p) => p !== "").join("")}</g>`;
|
|
752
|
+
}
|
|
753
|
+
function elementSvg(el) {
|
|
754
|
+
const head = `<g data-edge-id="${el.edgeId}"><title>${xmlEscape(el.title)}</title>`;
|
|
755
|
+
if (el.dashed) {
|
|
756
|
+
const dash = DOTTED.dash === null ? "" : ` stroke-dasharray="${DOTTED.dash[0]},${DOTTED.dash[1]}"`;
|
|
757
|
+
return head + `<path d="${pathData(el.points)}" fill="none" stroke="${EDGE_INK}" stroke-width="${DOTTED.width}" stroke-opacity="${DOTTED.opacity}"${dash}/></g>`;
|
|
758
|
+
}
|
|
759
|
+
if (el.points.length === 2) {
|
|
760
|
+
const a = el.points[0];
|
|
761
|
+
const b = el.points[1];
|
|
762
|
+
return head + `<line x1="${a.x}" y1="${a.y}" x2="${b.x}" y2="${b.y}" stroke="${EDGE_INK}" stroke-width="1.5" stroke-opacity="0.75"/></g>`;
|
|
763
|
+
}
|
|
764
|
+
return head + `<path d="${pathData(el.points)}" fill="none" stroke="${EDGE_INK}" stroke-width="1.5" stroke-opacity="0.75"/></g>`;
|
|
765
|
+
}
|
|
766
|
+
var MINI_ATTRS = `fill="transparent" stroke="${GLYPH_STROKE}" stroke-width="1.5"`;
|
|
767
|
+
function lineSwatch(x, y) {
|
|
768
|
+
const x1 = round2(x + 2);
|
|
769
|
+
const x2 = round2(x + LEGEND_SWATCH_W - 2);
|
|
770
|
+
return `<line x1="${x1}" y1="${y}" x2="${x2}" y2="${y}" stroke="${EDGE_INK}" stroke-width="1.5" stroke-opacity="0.75"/>`;
|
|
771
|
+
}
|
|
772
|
+
function assistantSwatch(x, y) {
|
|
773
|
+
const stemX = round2(x + LEGEND_SWATCH_W - 4);
|
|
774
|
+
const boxX = round2(x + 1);
|
|
775
|
+
return `<line x1="${stemX}" y1="${round2(y - 4)}" x2="${stemX}" y2="${y}" stroke="${EDGE_INK}" stroke-width="1.5" stroke-opacity="0.75"/><line x1="${stemX}" y1="${y}" x2="${round2(boxX + 8)}" y2="${y}" stroke="${EDGE_INK}" stroke-width="1.5" stroke-opacity="0.75"/><rect x="${boxX}" y="${round2(y - 3.5)}" width="8" height="7" rx="1" ${MINI_ATTRS}/>`;
|
|
776
|
+
}
|
|
777
|
+
function dottedSwatch(x, y) {
|
|
778
|
+
const x1 = round2(x + 2);
|
|
779
|
+
const x2 = round2(x + LEGEND_SWATCH_W - 2);
|
|
780
|
+
const dash = DOTTED.dash === null ? "" : ` stroke-dasharray="${DOTTED.dash[0]},${DOTTED.dash[1]}"`;
|
|
781
|
+
return `<line x1="${x1}" y1="${y}" x2="${x2}" y2="${y}" stroke="${EDGE_INK}" stroke-width="${DOTTED.width}" stroke-opacity="${DOTTED.opacity}"${dash}/>`;
|
|
782
|
+
}
|
|
783
|
+
function vacantSwatch(x, y) {
|
|
784
|
+
const cx = round2(x + LEGEND_SWATCH_W / 2);
|
|
785
|
+
const bx = round2(cx - 8);
|
|
786
|
+
const by = round2(y - 5);
|
|
787
|
+
return `<rect x="${bx}" y="${by}" width="16" height="10" rx="1" ${MINI_ATTRS} stroke-dasharray="3,2"/><rect x="${round2(bx + 2)}" y="${round2(by + 2)}" width="12" height="6" rx="1" ${MINI_ATTRS} stroke-dasharray="3,2"/>`;
|
|
788
|
+
}
|
|
789
|
+
function orgChartLayoutSvg(layout, opts = {}) {
|
|
790
|
+
const labels = opts.labels ?? ORG_CHART_SVG_LABELS_EN;
|
|
791
|
+
const parts = [];
|
|
792
|
+
for (const el of layout.elements) parts.push(elementSvg(el));
|
|
793
|
+
for (const n of layout.nodes) parts.push(boxSvg(n));
|
|
794
|
+
let width = layout.width;
|
|
795
|
+
let height = layout.height;
|
|
796
|
+
if (opts.legend !== false && layout.nodes.length > 0) {
|
|
797
|
+
const hasSolid = layout.elements.some((e) => e.kind === "stem" || e.kind === "bus" || e.kind === "drop");
|
|
798
|
+
const hasAssistant = layout.nodes.some((n) => n.isAssistant);
|
|
799
|
+
const hasDotted = layout.elements.some((e) => e.kind === "dotted");
|
|
800
|
+
const hasVacant = layout.nodes.some((n) => n.style === "dashed");
|
|
801
|
+
const entries = [];
|
|
802
|
+
if (hasSolid) entries.push({ swatch: lineSwatch, label: labels.legend.line });
|
|
803
|
+
if (hasAssistant) entries.push({ swatch: assistantSwatch, label: labels.legend.assistant });
|
|
804
|
+
if (hasDotted) entries.push({ swatch: dottedSwatch, label: labels.legend.dotted });
|
|
805
|
+
if (hasVacant) entries.push({ swatch: vacantSwatch, label: labels.legend.vacant });
|
|
806
|
+
const block = legendBlock(entries, layout.height);
|
|
807
|
+
if (block.svg !== "") {
|
|
808
|
+
parts.push(block.svg);
|
|
809
|
+
width = Math.max(width, block.width);
|
|
810
|
+
height = block.height;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
const w = Math.ceil(width);
|
|
814
|
+
const h = Math.ceil(height);
|
|
815
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${w} ${h}" width="${w}" height="${h}" role="img" aria-label="${xmlEscape(labels.ariaLabel)}">` + parts.join("") + `</svg>`;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// src/org-chart/render.ts
|
|
819
|
+
function orgChartSvg(input, opts = {}) {
|
|
820
|
+
const layout = computeOrgChartLayout(input, {
|
|
821
|
+
...opts.maxLabelChars !== void 0 ? { maxLabelChars: opts.maxLabelChars } : {},
|
|
822
|
+
...opts.titleLabels !== void 0 ? { titleLabels: opts.titleLabels } : {}
|
|
823
|
+
});
|
|
824
|
+
const svg = orgChartLayoutSvg(layout, {
|
|
825
|
+
...opts.legend === false ? { legend: false } : {},
|
|
826
|
+
...opts.svgLabels !== void 0 ? { labels: opts.svgLabels } : {}
|
|
827
|
+
});
|
|
828
|
+
return { svg, layout };
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
exports.ORG_ASSIST_ID_BASE = ORG_ASSIST_ID_BASE;
|
|
832
|
+
exports.ORG_BUS_ID_BASE = ORG_BUS_ID_BASE;
|
|
833
|
+
exports.ORG_CHART_SVG_LABELS_EN = ORG_CHART_SVG_LABELS_EN;
|
|
834
|
+
exports.ORG_CHART_TITLE_LABELS_EN = ORG_CHART_TITLE_LABELS_EN;
|
|
835
|
+
exports.ORG_DOTTED_ID_BASE = ORG_DOTTED_ID_BASE;
|
|
836
|
+
exports.ORG_DROP_ID_BASE = ORG_DROP_ID_BASE;
|
|
837
|
+
exports.ORG_LABEL_FONT = ORG_LABEL_FONT;
|
|
838
|
+
exports.ORG_LABEL_LINE_H = ORG_LABEL_LINE_H;
|
|
839
|
+
exports.ORG_MAX_MATRIX_EDGES_PER_NODE = ORG_MAX_MATRIX_EDGES_PER_NODE;
|
|
840
|
+
exports.ORG_REPORT_KINDS = ORG_REPORT_KINDS;
|
|
841
|
+
exports.ORG_STEM_ID_BASE = ORG_STEM_ID_BASE;
|
|
842
|
+
exports.ORG_TITLE_FONT = ORG_TITLE_FONT;
|
|
843
|
+
exports.ORG_VACANCIES = ORG_VACANCIES;
|
|
844
|
+
exports.OrgChartValidationError = OrgChartValidationError;
|
|
845
|
+
exports.computeOrgChartLayout = computeOrgChartLayout;
|
|
846
|
+
exports.estimateTextWidth = estimateTextWidth;
|
|
847
|
+
exports.orgChartIssues = orgChartIssues;
|
|
848
|
+
exports.orgChartLayoutSvg = orgChartLayoutSvg;
|
|
849
|
+
exports.orgChartSvg = orgChartSvg;
|
|
850
|
+
exports.packSubtree = packSubtree;
|
|
851
|
+
exports.validateOrgChart = validateOrgChart;
|
|
852
|
+
//# sourceMappingURL=index.cjs.map
|
|
853
|
+
//# sourceMappingURL=index.cjs.map
|