@inflexa-ai/harness 0.42.2 → 0.43.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.
Files changed (199) hide show
  1. package/README.md +2 -2
  2. package/dist/agents/sandbox/shared.d.ts +1 -6
  3. package/dist/agents/sandbox/shared.js +0 -15
  4. package/dist/app/chat-turn.js +13 -1
  5. package/dist/config/environment-stores.d.ts +4 -2
  6. package/dist/contracts/chat-events.d.ts +9 -1
  7. package/dist/contracts/chat-frame.d.ts +3 -3
  8. package/dist/contracts/chat-frame.js +8 -3
  9. package/dist/contracts/chat-parts.d.ts +4 -1
  10. package/dist/contracts/data-profile.d.ts +4 -6
  11. package/dist/contracts/index.d.ts +1 -1
  12. package/dist/contracts/report-blocks.d.ts +1364 -75
  13. package/dist/contracts/report-blocks.js +221 -29
  14. package/dist/contracts/report-reference.js +1 -1
  15. package/dist/contracts/schemas/chat-events.d.ts +13 -0
  16. package/dist/contracts/schemas/chat-events.js +5 -0
  17. package/dist/contracts/schemas/chat-parts.d.ts +2 -2
  18. package/dist/contracts/schemas/index.d.ts +1 -1
  19. package/dist/contracts/schemas/index.js +1 -1
  20. package/dist/execution/input-signature.d.ts +7 -13
  21. package/dist/execution/input-signature.js +0 -0
  22. package/dist/execution/run-canceler.d.ts +3 -2
  23. package/dist/execution/run-canceler.js +3 -2
  24. package/dist/execution/run-event-stream.d.ts +16 -2
  25. package/dist/execution/run-event-stream.js +54 -3
  26. package/dist/index.d.ts +1 -8
  27. package/dist/index.js +5 -16
  28. package/dist/input-scan/enrich.d.ts +0 -2
  29. package/dist/input-scan/enrich.js +0 -1
  30. package/dist/lib/metrics.js +6 -7
  31. package/dist/lib/otel-spans.d.ts +4 -9
  32. package/dist/lib/otel-spans.js +3 -8
  33. package/dist/loop/run-agent.d.ts +4 -2
  34. package/dist/loop/run-agent.js +38 -23
  35. package/dist/memory/thread-history.d.ts +1 -1
  36. package/dist/prompts/report-session.d.ts +1 -1
  37. package/dist/prompts/report-session.js +17 -0
  38. package/dist/provenance/exec-frame.d.ts +1 -1
  39. package/dist/report-model/authoring-grammar.d.ts +416 -17
  40. package/dist/report-model/authoring-grammar.js +25 -4
  41. package/dist/report-model/block-walk.d.ts +20 -4
  42. package/dist/report-model/block-walk.js +64 -7
  43. package/dist/report-model/draft-finish.d.ts +6 -2
  44. package/dist/report-model/draft-finish.js +6 -1
  45. package/dist/report-model/production-resolver.d.ts +8 -0
  46. package/dist/report-model/production-resolver.js +1 -1
  47. package/dist/report-model/validate.d.ts +20 -10
  48. package/dist/report-model/validate.js +29 -13
  49. package/dist/report-render/chart-export.d.ts +52 -0
  50. package/dist/report-render/chart-export.js +192 -0
  51. package/dist/report-render/chart-presets.d.ts +46 -47
  52. package/dist/report-render/chart-presets.js +31 -101
  53. package/dist/report-render/chart-renderers.d.ts +163 -0
  54. package/dist/report-render/chart-renderers.js +1055 -0
  55. package/dist/report-render/chart-toolbox.d.ts +124 -0
  56. package/dist/report-render/chart-toolbox.js +595 -0
  57. package/dist/report-render/chart.d.ts +60 -27
  58. package/dist/report-render/chart.js +2323 -524
  59. package/dist/report-render/design.d.ts +476 -16
  60. package/dist/report-render/design.js +390 -56
  61. package/dist/report-render/figures/common.d.ts +321 -0
  62. package/dist/report-render/figures/common.js +759 -0
  63. package/dist/report-render/figures/dendrogram.d.ts +36 -0
  64. package/dist/report-render/figures/dendrogram.js +130 -0
  65. package/dist/report-render/figures/dense.d.ts +111 -0
  66. package/dist/report-render/figures/dense.js +204 -0
  67. package/dist/report-render/figures/dotplot.d.ts +28 -0
  68. package/dist/report-render/figures/dotplot.js +234 -0
  69. package/dist/report-render/figures/embedding.d.ts +43 -0
  70. package/dist/report-render/figures/embedding.js +360 -0
  71. package/dist/report-render/figures/equal-units.d.ts +101 -0
  72. package/dist/report-render/figures/equal-units.js +215 -0
  73. package/dist/report-render/figures/forest.d.ts +20 -0
  74. package/dist/report-render/figures/forest.js +398 -0
  75. package/dist/report-render/figures/gsea.d.ts +30 -0
  76. package/dist/report-render/figures/gsea.js +391 -0
  77. package/dist/report-render/figures/heatmap.d.ts +40 -0
  78. package/dist/report-render/figures/heatmap.js +395 -0
  79. package/dist/report-render/figures/index.d.ts +135 -0
  80. package/dist/report-render/figures/index.js +100 -0
  81. package/dist/report-render/figures/km.d.ts +16 -0
  82. package/dist/report-render/figures/km.js +406 -0
  83. package/dist/report-render/figures/label-room.d.ts +130 -0
  84. package/dist/report-render/figures/label-room.js +341 -0
  85. package/dist/report-render/figures/locuszoom.d.ts +50 -0
  86. package/dist/report-render/figures/locuszoom.js +675 -0
  87. package/dist/report-render/figures/lollipop.d.ts +16 -0
  88. package/dist/report-render/figures/lollipop.js +247 -0
  89. package/dist/report-render/figures/ma.d.ts +20 -0
  90. package/dist/report-render/figures/ma.js +114 -0
  91. package/dist/report-render/figures/manhattan.d.ts +60 -0
  92. package/dist/report-render/figures/manhattan.js +343 -0
  93. package/dist/report-render/figures/oncoprint.d.ts +41 -0
  94. package/dist/report-render/figures/oncoprint.js +420 -0
  95. package/dist/report-render/figures/pca.d.ts +37 -0
  96. package/dist/report-render/figures/pca.js +241 -0
  97. package/dist/report-render/figures/qq.d.ts +30 -0
  98. package/dist/report-render/figures/qq.js +182 -0
  99. package/dist/report-render/figures/roc.d.ts +17 -0
  100. package/dist/report-render/figures/roc.js +116 -0
  101. package/dist/report-render/figures/sankey.d.ts +48 -0
  102. package/dist/report-render/figures/sankey.js +353 -0
  103. package/dist/report-render/figures/upset.d.ts +60 -0
  104. package/dist/report-render/figures/upset.js +316 -0
  105. package/dist/report-render/figures/volcano.d.ts +25 -0
  106. package/dist/report-render/figures/volcano.js +166 -0
  107. package/dist/report-render/hybrid-svg.d.ts +75 -0
  108. package/dist/report-render/hybrid-svg.js +504 -0
  109. package/dist/report-render/number-format.d.ts +19 -0
  110. package/dist/report-render/number-format.js +25 -1
  111. package/dist/report-render/page.d.ts +41 -4
  112. package/dist/report-render/page.js +247 -12
  113. package/dist/report-render/render.d.ts +13 -4
  114. package/dist/report-render/render.js +37 -6
  115. package/dist/report-render/types.d.ts +23 -2
  116. package/dist/report-render/value-bridge.d.ts +38 -3
  117. package/dist/report-render/value-bridge.js +137 -6
  118. package/dist/report-render/views/chart-view.d.ts +42 -6
  119. package/dist/report-render/views/chart-view.js +70 -9
  120. package/dist/report-render/views/page-view.js +9 -10
  121. package/dist/runtime/assemble.js +2 -2
  122. package/dist/sandbox/client.d.ts +23 -32
  123. package/dist/sandbox/client.js +3 -11
  124. package/dist/sandbox/create-sandbox.d.ts +9 -24
  125. package/dist/sandbox/create-sandbox.js +15 -30
  126. package/dist/sandbox/docker-client.d.ts +12 -22
  127. package/dist/sandbox/docker-client.js +29 -43
  128. package/dist/sandbox/exec.d.ts +58 -0
  129. package/dist/sandbox/exec.js +197 -0
  130. package/dist/sandbox/identity.d.ts +5 -5
  131. package/dist/sandbox/identity.js +7 -10
  132. package/dist/sandbox/k8s-client.d.ts +4 -10
  133. package/dist/sandbox/k8s-client.js +5 -12
  134. package/dist/sandbox/liveness.d.ts +5 -12
  135. package/dist/sandbox/liveness.js +5 -12
  136. package/dist/sandbox/notification-sweep.d.ts +4 -7
  137. package/dist/sandbox/notification-sweep.js +4 -7
  138. package/dist/sandbox/reaper.d.ts +3 -3
  139. package/dist/sandbox/reaper.js +3 -3
  140. package/dist/sandbox/sandbox-error.d.ts +10 -11
  141. package/dist/sandbox/sandbox-error.js +10 -11
  142. package/dist/sandbox/sandbox-step-translate.d.ts +1 -1
  143. package/dist/sandbox/sandbox-step-translate.js +1 -1
  144. package/dist/sandbox/types.d.ts +31 -99
  145. package/dist/sandbox/types.js +17 -84
  146. package/dist/state/active-sandboxes.d.ts +7 -32
  147. package/dist/state/active-sandboxes.js +11 -46
  148. package/dist/state/index.d.ts +1 -2
  149. package/dist/state/index.js +1 -1
  150. package/dist/state/migrations/20261005120000_drop_the_active_exec_tracking.d.ts +7 -0
  151. package/dist/state/migrations/20261005120000_drop_the_active_exec_tracking.js +12 -0
  152. package/dist/state/migrations/index.js +2 -0
  153. package/dist/state/schema.d.ts +1 -5
  154. package/dist/state/schema.js +1 -6
  155. package/dist/state/step-executions.js +1 -2
  156. package/dist/tasks/data-profile.js +7 -20
  157. package/dist/tasks/derive-table-exec.d.ts +4 -14
  158. package/dist/tasks/derive-table-exec.js +9 -24
  159. package/dist/tasks/extract-values.d.ts +2 -2
  160. package/dist/tasks/extract-values.js +4 -7
  161. package/dist/tools/define-tool.d.ts +56 -13
  162. package/dist/tools/define-tool.js +28 -1
  163. package/dist/tools/execute-analysis.js +3 -3
  164. package/dist/tools/report-session/derive-table.d.ts +8 -9
  165. package/dist/tools/report-session/derive-table.js +3 -4
  166. package/dist/tools/report-session/preview-report.js +4 -49
  167. package/dist/tools/sandbox/list-available-packages.d.ts +1 -1
  168. package/dist/tools/sandbox/list-available-packages.js +5 -4
  169. package/dist/tools/workspace/edit-file.d.ts +5 -4
  170. package/dist/tools/workspace/edit-file.js +10 -13
  171. package/dist/tools/workspace/execute-command.d.ts +27 -42
  172. package/dist/tools/workspace/execute-command.js +42 -38
  173. package/dist/tools/workspace/index.d.ts +5 -6
  174. package/dist/tools/workspace/index.js +5 -6
  175. package/dist/tools/workspace/mutator.d.ts +43 -8
  176. package/dist/tools/workspace/mutator.js +45 -34
  177. package/dist/tools/workspace/run-exec.d.ts +3 -12
  178. package/dist/tools/workspace/run-exec.js +7 -24
  179. package/dist/tools/workspace/scan-inputs.d.ts +0 -3
  180. package/dist/tools/workspace/scan-inputs.js +0 -1
  181. package/dist/tools/workspace/write-file.d.ts +12 -3
  182. package/dist/tools/workspace/write-file.js +17 -13
  183. package/dist/workflows/sandbox-step.d.ts +7 -11
  184. package/dist/workflows/sandbox-step.js +24 -27
  185. package/package.json +3 -1
  186. package/dist/sandbox/await-exec.d.ts +0 -143
  187. package/dist/sandbox/await-exec.js +0 -417
  188. package/dist/sandbox/deliver-exec-event.d.ts +0 -25
  189. package/dist/sandbox/deliver-exec-event.js +0 -29
  190. package/dist/sandbox/digest.d.ts +0 -14
  191. package/dist/sandbox/digest.js +0 -17
  192. package/dist/sandbox/exec-id.d.ts +0 -12
  193. package/dist/sandbox/exec-id.js +0 -20
  194. package/dist/sandbox/hmac.d.ts +0 -62
  195. package/dist/sandbox/hmac.js +0 -72
  196. package/dist/sandbox/submit-exec.d.ts +0 -38
  197. package/dist/sandbox/submit-exec.js +0 -80
  198. package/dist/sandbox/watchdog.d.ts +0 -71
  199. package/dist/sandbox/watchdog.js +0 -161
@@ -0,0 +1,353 @@
1
+ /**
2
+ * The Sankey diagram: the flows between the nodes of some stages, as the ECharts `sankey` series draws them.
3
+ *
4
+ * The figure reads one row for each flow: the `x` column names the source node, the `y` column names the
5
+ * target node, and the `value` column gives the size of the flow. The optional `group` column names the
6
+ * category of each flow, and each flow takes the palette color of its category.
7
+ *
8
+ * - The nodes keep the order of their first appearance in the rows, and the layout runs no iteration. Thus
9
+ * the order of the table is the order of each stage, and the same rows give the same picture.
10
+ * - The palette starts again at each stage, thus each node takes the palette color of its place in its stage.
11
+ * Each flow takes the color of its source at a light opacity. With a group, each flow takes the color of its group, the nodes draw gray, and a legend names
12
+ * each group.
13
+ * - Each label sits beside its node: at the right, and at the left in the last stage, thus each label stays
14
+ * inside the chart. A label wraps into the room between two stages. The runtime draws a series label at a
15
+ * fixed size of its own, thus the page and one media rule for each export size state the text size.
16
+ * - Each label sits on a box of the page color, thus no flow covers it.
17
+ * - The node gap holds one label line where the stage of the most nodes has the room, thus the label of a thin
18
+ * node meets no neighbor. A label that still meets a neighbor hides, and the tooltip of its node names it.
19
+ * - The tooltip of a flow names its two nodes and its value.
20
+ *
21
+ * The figure computes one named summary: the stage of each node. The stage is the longest chain of flows from
22
+ * a node with no inflow, and a node with no outflow takes the last stage, thus each end of the flows lines up
23
+ * at the right. The chart runtime sizes each node by the larger of its inflow and its outflow.
24
+ */
25
+ import { err, ok } from "neverthrow";
26
+ import { declaredForColumn } from "../../contracts/report-reference.js";
27
+ import { CHART_BODY_PX, CHART_EXPORT_SIZES, CHART_PAGE_TEXT_PX, CHART_PAGE_WIDTH_PX, MUTED_CHART_COLOR } from "../design.js";
28
+ import { AREA_LEGEND_ICON, categoricalPalette, chartProblem, demandedColumn, firstAppearance, legendLineCount, legendLinePx, plainColumn, toNumber, } from "./common.js";
29
+ import { largest } from "./dense.js";
30
+ /** The opacity of a flow. A flow reads as the path of its source, and a label reads over it. */
31
+ export const SANKEY_LINK_OPACITY = 0.35;
32
+ /** The margins of the series at each side, in percent of the chart. */
33
+ const SIDE_PCT = 2;
34
+ const TOP_PCT = 6;
35
+ const BOTTOM_PCT = 6;
36
+ /** The width of one node, as a share of the text size. */
37
+ const NODE_WIDTH_SHARE = 1;
38
+ /** The height of one label line, as a share of the text size. */
39
+ const LABEL_LINE_SHARE = 1.25;
40
+ /**
41
+ * The largest share of the height of a stage that its node gaps take. The rest draws the flows, thus a stage of
42
+ * many nodes takes a smaller gap than one label line.
43
+ */
44
+ const GAP_MAX_SHARE = 0.5;
45
+ /** The gap between a node and its label, in pixels at the page text size. */
46
+ const LABEL_DISTANCE_PX = 5;
47
+ /**
48
+ * The box of a label: the color of the page, and the room at each side of the text, in pixels at the page text
49
+ * size. A label sits over the flows of its node, and the flows of a thin node are thinner than the text. Thus a
50
+ * flow edge would cross the letters, and a halo keeps it visible between them. The box hides each flow under
51
+ * the text.
52
+ */
53
+ export const SANKEY_LABEL_BOX_COLOR = "#ffffff";
54
+ const LABEL_BOX_PAD_PX = 2;
55
+ /**
56
+ * The rich style of the label text. A box of the label itself takes the wrap width, thus it would hide the flows
57
+ * across the room between two stages. A rich style boxes each line of the text alone.
58
+ */
59
+ const LABEL_BOX_STYLE = "box";
60
+ /** The icon size and the gap of the group legend, in pixels. */
61
+ const LEGEND_ICON_PX = 10;
62
+ const LEGEND_GAP_PX = 8;
63
+ /** The Sankey module: the source, the target, the flow, and the group. */
64
+ export const SANKEY_FIGURE = { reads: new Set(["x", "y", "value", "group"]), derive: deriveSankey };
65
+ /** The text of one node cell, or `undefined` for an empty cell. */
66
+ function nodeName(cell) {
67
+ if (cell === undefined)
68
+ return undefined;
69
+ const text = String(cell).trim();
70
+ return text === "" ? undefined : text;
71
+ }
72
+ /**
73
+ * The stage of each node, in the order of first appearance: the longest chain of flows from a node with no
74
+ * inflow. A node with no outflow takes the last stage. A cycle has no stage, thus it refuses, and the error
75
+ * names the nodes of the cycle.
76
+ */
77
+ export function sankeyStages(edges) {
78
+ const nodes = firstAppearance(edges.flatMap((edge) => [edge.source, edge.target]));
79
+ const outgoing = new Map(nodes.map((node) => [node, []]));
80
+ for (const edge of edges)
81
+ outgoing.get(edge.source)?.push(edge.target);
82
+ const cycle = findCycle(nodes, outgoing);
83
+ if (cycle !== undefined) {
84
+ return err(`The flows ${cycle.map((node) => `"${node}"`).join(" → ")} close a cycle. A Sankey diagram draws each flow in one direction, thus a cycle refuses.`);
85
+ }
86
+ const stages = new Map(nodes.map((node) => [node, 0]));
87
+ for (const node of topologicalOrder(nodes, outgoing)) {
88
+ const stage = stages.get(node) ?? 0;
89
+ for (const next of outgoing.get(node) ?? []) {
90
+ if ((stages.get(next) ?? 0) < stage + 1)
91
+ stages.set(next, stage + 1);
92
+ }
93
+ }
94
+ const last = largest([...stages.values()]) ?? 0;
95
+ for (const node of nodes) {
96
+ if ((outgoing.get(node) ?? []).length === 0)
97
+ stages.set(node, last);
98
+ }
99
+ return ok(stages);
100
+ }
101
+ /**
102
+ * The first cycle of a depth-first walk over the nodes in order, as its nodes from the first one back to the
103
+ * first one, or `undefined` for a graph with no cycle. The walk keeps its own stack, thus a long chain never
104
+ * spreads onto the call stack.
105
+ */
106
+ function findCycle(nodes, outgoing) {
107
+ const state = new Map();
108
+ for (const root of nodes) {
109
+ if (state.has(root))
110
+ continue;
111
+ const path = [{ node: root, next: 0 }];
112
+ state.set(root, "open");
113
+ while (path.length > 0) {
114
+ const top = path[path.length - 1];
115
+ const targets = outgoing.get(top.node) ?? [];
116
+ if (top.next >= targets.length) {
117
+ state.set(top.node, "done");
118
+ path.pop();
119
+ continue;
120
+ }
121
+ const target = targets[top.next];
122
+ top.next += 1;
123
+ const seen = state.get(target);
124
+ if (seen === "open") {
125
+ const from = path.findIndex((entry) => entry.node === target);
126
+ return [...path.slice(from).map((entry) => entry.node), target];
127
+ }
128
+ if (seen === undefined) {
129
+ state.set(target, "open");
130
+ path.push({ node: target, next: 0 });
131
+ }
132
+ }
133
+ }
134
+ return undefined;
135
+ }
136
+ /** The nodes of a graph with no cycle in an order where each flow runs forward. A tie keeps the first appearance. */
137
+ function topologicalOrder(nodes, outgoing) {
138
+ const inflow = new Map(nodes.map((node) => [node, 0]));
139
+ for (const targets of outgoing.values()) {
140
+ for (const target of targets)
141
+ inflow.set(target, (inflow.get(target) ?? 0) + 1);
142
+ }
143
+ const order = [];
144
+ const ready = nodes.filter((node) => inflow.get(node) === 0);
145
+ for (let next = 0; next < ready.length; next += 1) {
146
+ const node = ready[next];
147
+ order.push(node);
148
+ for (const target of outgoing.get(node) ?? []) {
149
+ const left = (inflow.get(target) ?? 0) - 1;
150
+ inflow.set(target, left);
151
+ if (left === 0)
152
+ ready.push(target);
153
+ }
154
+ }
155
+ return order;
156
+ }
157
+ /** The flows of the table, or the refusal of the first row that states no flow. */
158
+ function readFlows(rows, source, target, value, group) {
159
+ const flows = [];
160
+ const seen = new Set();
161
+ for (const row of rows) {
162
+ const from = nodeName(row[source]);
163
+ const to = nodeName(row[target]);
164
+ if (from === undefined)
165
+ return err(`A flow to "${to ?? ""}" names no source node in the "${source}" column.`);
166
+ if (to === undefined)
167
+ return err(`A flow from "${from}" names no target node in the "${target}" column.`);
168
+ if (from === to)
169
+ return err(`The flow from "${from}" to "${to}" joins a node to itself. A Sankey flow joins two nodes.`);
170
+ const amount = toNumber(row[value]);
171
+ if (amount === null)
172
+ return err(`The flow from "${from}" to "${to}" holds no number in the "${value}" column.`);
173
+ if (amount <= 0)
174
+ return err(`The flow from "${from}" to "${to}" is ${amount}. A Sankey flow is positive.`);
175
+ const groupName = group === undefined ? undefined : (nodeName(row[group]) ?? "");
176
+ const key = JSON.stringify([from, to, groupName ?? null]);
177
+ if (seen.has(key))
178
+ return err(`The table holds two flows from "${from}" to "${to}". It holds one row for each flow.`);
179
+ seen.add(key);
180
+ flows.push({ source: from, target: to, value: amount, ...(groupName !== undefined ? { group: groupName } : {}) });
181
+ }
182
+ return ok(flows);
183
+ }
184
+ const PAGE_BOX = { widthPx: CHART_PAGE_WIDTH_PX, heightPx: CHART_BODY_PX, textPx: CHART_PAGE_TEXT_PX };
185
+ const EXPORT_SIZES = [CHART_EXPORT_SIZES.single, CHART_EXPORT_SIZES.double, CHART_EXPORT_SIZES.slide];
186
+ /**
187
+ * The text and the node size of one box: the label text size, the room of a label between two stages, the node
188
+ * width, and the node gap. A figure of one stage gives its label the half of the width.
189
+ *
190
+ * A label sits at the middle of its node. Thus two labels of one line never overlap where the gap between two
191
+ * nodes holds one label line, however thin the nodes are. The gap takes one line where the stage of the most
192
+ * nodes has the room, and a smaller gap where it does not.
193
+ */
194
+ function boxFields(box, stageCount, stageNodes, bottomPct) {
195
+ const scale = box.textPx / CHART_PAGE_TEXT_PX;
196
+ const nodeWidth = rounded(box.textPx * NODE_WIDTH_SHARE);
197
+ const distance = rounded(LABEL_DISTANCE_PX * scale);
198
+ const span = box.widthPx * (1 - (2 * SIDE_PCT) / 100);
199
+ const room = stageCount > 1 ? (span - nodeWidth * stageCount) / (stageCount - 1) : span / 2;
200
+ return {
201
+ label: {
202
+ fontSize: box.textPx,
203
+ width: rounded(Math.max(box.textPx, room - 2 * distance)),
204
+ distance,
205
+ rich: { [LABEL_BOX_STYLE]: { backgroundColor: SANKEY_LABEL_BOX_COLOR, padding: [0, rounded(LABEL_BOX_PAD_PX * scale)] } },
206
+ },
207
+ nodeWidth,
208
+ nodeGap: rounded(Math.min(box.textPx * LABEL_LINE_SHARE, gapRoom(box, stageNodes, bottomPct))),
209
+ };
210
+ }
211
+ /** The largest node gap of a stage of some nodes in one box, in pixels. */
212
+ function gapRoom(box, stageNodes, bottomPct) {
213
+ if (stageNodes < 2)
214
+ return Number.POSITIVE_INFINITY;
215
+ const height = box.heightPx * (1 - (TOP_PCT + bottomPct) / 100);
216
+ return (height * GAP_MAX_SHARE) / (stageNodes - 1);
217
+ }
218
+ /**
219
+ * The color of each node. The palette starts again at each stage, in stage order, thus the nodes of one stage
220
+ * read as distinct colors and two stages never meet at an arbitrary pair of the long list. A stage of more
221
+ * nodes than the Okabe-Ito palette holds takes the wide palette.
222
+ */
223
+ function nodeColors(nodes, stages) {
224
+ const byStage = new Map();
225
+ for (const node of nodes) {
226
+ const stage = stages.get(node) ?? 0;
227
+ const members = byStage.get(stage);
228
+ if (members === undefined)
229
+ byStage.set(stage, [node]);
230
+ else
231
+ members.push(node);
232
+ }
233
+ const colors = new Map();
234
+ for (const members of byStage.values()) {
235
+ const palette = categoricalPalette(members.length);
236
+ for (const [place, node] of members.entries())
237
+ colors.set(node, palette[place % palette.length]);
238
+ }
239
+ return colors;
240
+ }
241
+ /** One number rounded, thus a float residue never reaches the option. */
242
+ function rounded(value) {
243
+ return Math.round(value * 1e4) / 1e4;
244
+ }
245
+ /**
246
+ * The band of the group legend under the flows, in percent of the chart. The band holds the legend lines of
247
+ * the page and of the single column, whose narrow width wraps the legend into more lines.
248
+ */
249
+ function legendBandPct(names) {
250
+ const single = CHART_EXPORT_SIZES.single;
251
+ const bandAt = (widthPx, heightPx, textPx) => ((legendLineCount(names, widthPx, textPx) * legendLinePx(textPx, LEGEND_GAP_PX) + textPx) / heightPx) * 100;
252
+ return rounded(Math.max(bandAt(CHART_PAGE_WIDTH_PX, CHART_BODY_PX, CHART_PAGE_TEXT_PX), bandAt(single.widthPx, single.heightPx, single.textPx)));
253
+ }
254
+ function deriveSankey(block, rows, context) {
255
+ const source = { blockId: context.blockId, rows, ...(context.columns !== undefined ? { columns: context.columns } : {}) };
256
+ const encoding = block.encoding ?? {};
257
+ const from = demandedColumn("sankey", encoding, "x", source);
258
+ if (from.isErr())
259
+ return err(from.error);
260
+ const to = demandedColumn("sankey", encoding, "y", source);
261
+ if (to.isErr())
262
+ return err(to.error);
263
+ const value = demandedColumn("sankey", encoding, "value", source);
264
+ if (value.isErr())
265
+ return err(value.error);
266
+ const group = plainColumn("sankey", encoding, "group", source);
267
+ if (group.isErr())
268
+ return err(group.error);
269
+ if (rows.length === 0) {
270
+ return err(chartProblem(context.blockId, "The sankey figure has no rows. It needs one row for each flow."));
271
+ }
272
+ const flows = readFlows(rows, from.value, to.value, value.value, group.value);
273
+ if (flows.isErr())
274
+ return err(chartProblem(context.blockId, flows.error));
275
+ const stages = sankeyStages(flows.value);
276
+ if (stages.isErr())
277
+ return err(chartProblem(context.blockId, stages.error));
278
+ const nodes = [...stages.value.keys()];
279
+ const last = largest([...stages.value.values()]) ?? 0;
280
+ const colors = nodeColors(nodes, stages.value);
281
+ const stageSizes = new Map();
282
+ for (const stage of stages.value.values())
283
+ stageSizes.set(stage, (stageSizes.get(stage) ?? 0) + 1);
284
+ const stageNodes = largest([...stageSizes.values()]) ?? 0;
285
+ const groups = group.value === undefined ? [] : firstAppearance(flows.value.map((entry) => entry.group ?? ""));
286
+ const groupPalette = categoricalPalette(groups.length);
287
+ const groupColors = new Map(groups.map((name, place) => [name, groupPalette[place % groupPalette.length]]));
288
+ const grouped = group.value !== undefined;
289
+ const bottom = grouped ? legendBandPct(groups) + BOTTOM_PCT : BOTTOM_PCT;
290
+ const page = boxFields(PAGE_BOX, last + 1, stageNodes, bottom);
291
+ const series = [
292
+ {
293
+ type: "sankey",
294
+ name: declaredForColumn(context.labels, value.value) ?? value.value,
295
+ left: `${SIDE_PCT}%`,
296
+ right: `${SIDE_PCT}%`,
297
+ top: `${TOP_PCT}%`,
298
+ bottom: `${rounded(bottom)}%`,
299
+ layoutIterations: 0,
300
+ nodeAlign: "justify",
301
+ draggable: false,
302
+ nodeWidth: page.nodeWidth,
303
+ nodeGap: page.nodeGap,
304
+ emphasis: { focus: "adjacency" },
305
+ label: { ...page.label, overflow: "break", formatter: `{${LABEL_BOX_STYLE}|{b}}` },
306
+ // A label that still meets a neighbor hides, and the tooltip of its node names it.
307
+ labelLayout: { hideOverlap: true },
308
+ lineStyle: grouped ? { opacity: SANKEY_LINK_OPACITY } : { color: "source", opacity: SANKEY_LINK_OPACITY },
309
+ data: nodes.map((node) => ({
310
+ name: node,
311
+ depth: stages.value.get(node) ?? 0,
312
+ itemStyle: { color: grouped ? MUTED_CHART_COLOR : colors.get(node) },
313
+ label: { position: (stages.value.get(node) ?? 0) === last && last > 0 ? "left" : "right" },
314
+ })),
315
+ links: flows.value.map((entry) => ({
316
+ source: entry.source,
317
+ target: entry.target,
318
+ value: entry.value,
319
+ ...(grouped ? { lineStyle: { color: groupColors.get(entry.group ?? "") } } : {}),
320
+ })),
321
+ },
322
+ ];
323
+ const option = {
324
+ tooltip: { trigger: "item" },
325
+ series,
326
+ media: EXPORT_SIZES.map((size) => {
327
+ const fields = boxFields(size, last + 1, stageNodes, bottom);
328
+ return {
329
+ query: { minWidth: size.widthPx, maxWidth: size.widthPx, minHeight: size.heightPx, maxHeight: size.heightPx },
330
+ option: { series: [{ label: fields.label, nodeWidth: fields.nodeWidth, nodeGap: fields.nodeGap }] },
331
+ };
332
+ }),
333
+ };
334
+ if (!grouped)
335
+ return ok({ ...option, legend: { show: false } });
336
+ // The chart runtime lists no group of a flow in a legend. Each group takes one empty bar series of its
337
+ // color on hidden axes, thus the legend names each group, and a click of an entry changes no flow.
338
+ return ok({
339
+ ...option,
340
+ grid: { show: false, left: 0, right: 0, top: 0, bottom: 0 },
341
+ xAxis: { type: "value", show: false },
342
+ yAxis: { type: "value", show: false },
343
+ series: [...series, ...groups.map((name) => ({ type: "bar", name, itemStyle: { color: groupColors.get(name) }, data: [] }))],
344
+ legend: {
345
+ bottom: 0,
346
+ selectedMode: false,
347
+ itemWidth: LEGEND_ICON_PX,
348
+ itemHeight: LEGEND_ICON_PX,
349
+ itemGap: LEGEND_GAP_PX,
350
+ data: groups.map((name) => ({ name, icon: AREA_LEGEND_ICON })),
351
+ },
352
+ });
353
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * The UpSet plot: the intersections of some sets, as upset.app, UpSetR, and ComplexHeatmap draw them.
3
+ *
4
+ * The figure reads one row for each member of each set: the `x` column names the element, and the `group`
5
+ * column names the set. Three grids line up:
6
+ *
7
+ * - The top grid draws the size of each intersection as a bar, with its count over the bar.
8
+ * - The middle grid draws the dot matrix: one column for each intersection and one row for each set. A dark
9
+ * dot marks each member set of the intersection, a light dot marks each other set, and a line joins the
10
+ * member dots of the column.
11
+ * - The left grid draws the size of each set as a bar that grows to the left, on the rows of the matrix. The
12
+ * set names sit between these bars and the matrix.
13
+ *
14
+ * The figure computes two named summaries, and each one reads the rows of the bound table alone:
15
+ *
16
+ * - The size of a set: the count of its distinct elements.
17
+ * - The exact intersection of an element: the combination of every set that holds it. The size of an
18
+ * intersection is the count of its elements, thus each element counts in one intersection alone.
19
+ *
20
+ * The sets sort by size, the largest at the top, and a tie keeps the first appearance. The intersections sort
21
+ * by size, largest first, then by degree, smallest first, then by the set order of their members. The largest
22
+ * intersections draw alone past the cap, and the title under the matrix states the count of the others.
23
+ *
24
+ * The dots and the count text of one column must fit the column at each export size, thus one media rule for
25
+ * each export size states the dot size and the turn of the count text there.
26
+ */
27
+ import { type Result } from "neverthrow";
28
+ import type { ChartRow } from "../chart.js";
29
+ import type { FigureModule } from "./index.js";
30
+ /**
31
+ * The most sets that one figure draws. Each set takes one row of the matrix, and the rows of twelve sets hold
32
+ * one line of the column-export text each.
33
+ */
34
+ export declare const UPSET_SET_LIMIT = 12;
35
+ /** The most intersections that one figure draws. The largest ones draw, and the title states the count of the others. */
36
+ export declare const UPSET_INTERSECTION_LIMIT = 30;
37
+ /** The UpSet module: the element and the set. */
38
+ export declare const UPSET_FIGURE: FigureModule;
39
+ /** One set: its name and the count of its elements. */
40
+ export interface UpsetSet {
41
+ readonly name: string;
42
+ readonly size: number;
43
+ }
44
+ /** One exact intersection: the places of its member sets in the set order, and the count of its elements. */
45
+ export interface UpsetIntersection {
46
+ readonly members: readonly number[];
47
+ readonly size: number;
48
+ }
49
+ /** The computed summary of the figure: the sets in size order, and each intersection in draw order. */
50
+ export interface UpsetSummary {
51
+ readonly sets: readonly UpsetSet[];
52
+ readonly intersections: readonly UpsetIntersection[];
53
+ }
54
+ /**
55
+ * The set sizes and the exact intersections of one membership table.
56
+ *
57
+ * A row with an empty set cell names an element in no set, and it adds nothing: the empty intersection draws
58
+ * no column. The error is the detail of the refusal.
59
+ */
60
+ export declare function upsetSummary(rows: readonly ChartRow[], element: string, set: string): Result<UpsetSummary, string>;