@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,241 @@
1
+ /**
2
+ * The PCA of samples, and the shape draw that it shares with the plain scatter.
3
+ *
4
+ * The canonical PCA of a bulk study is the DESeq2 `plotPCA` figure: the sample scores on two components, the
5
+ * condition as the color, a second covariate such as the batch as the symbol, and one unit on x equal to one
6
+ * unit on y, thus a distance between two samples reads the same in each direction. A small table names each
7
+ * sample beside its point. The run states the variance of each component, and the declared label of each
8
+ * column carries it to the axis title.
9
+ *
10
+ * The shape draw splits the rows by the pair of their group and their shape. Each split is one series, named
11
+ * by its group, thus the palette gives the series of one group one color. Each series carries the symbol of
12
+ * its shape. The legend names each group with a circle, and each shape with its symbol in the ink color, thus
13
+ * the two keys read apart. A symbol is redundant coding beside the color, thus the figure survives a print in
14
+ * gray.
15
+ *
16
+ * The draw keeps its rows inline. A shape reads for a small table, and a page-side build draws no symbol.
17
+ */
18
+ import { err, ok } from "neverthrow";
19
+ import { channelColumn, channelOrder, channelTransform } from "../../contracts/report-blocks.js";
20
+ import { CHART_INK, CHART_PALETTE } from "../design.js";
21
+ import { categoricalPalette, categoryName, chartProblem, columnPresent, firstAppearance, POINT_LEGEND_ICON, toNumber, transformColumn, valueAxis, valueAxisTitle, } from "./common.js";
22
+ import { equalRanges, squareLayout } from "./equal-units.js";
23
+ import { NAME_GAP_PX, pointNameSides } from "./label-room.js";
24
+ /**
25
+ * The symbol of each shape category, in order: the circle, the triangle, the square, the diamond, the
26
+ * inverted triangle, and the cross. Each one keeps a clear outline at a small size, and past six a reader
27
+ * no longer tells the symbols apart.
28
+ */
29
+ export const SHAPE_SYMBOLS = [
30
+ "circle",
31
+ "triangle",
32
+ "rect",
33
+ "diamond",
34
+ "path://M0,0L10,0L5,10Z",
35
+ "path://M3.5,0H6.5V3.5H10V6.5H6.5V10H3.5V6.5H0V3.5H3.5Z",
36
+ ];
37
+ /** The symbol size of a point of the shape draw, in pixels. A sample of a PCA is one of few, thus it draws large. */
38
+ export const SHAPED_POINT_PX = 10;
39
+ /** The largest row count of a PCA whose points each carry their name. The DESeq2 workflow labels a small study. */
40
+ export const PCA_LABEL_ROWS = 20;
41
+ /** The share of the longer span that pads each side of a PCA, before the ends round to a tick. */
42
+ const PCA_PAD = 0.04;
43
+ /** The margins of the square of a PCA, in pixels: the tick labels and the title of each axis. */
44
+ const PCA_MARGINS = { top: 16, bottom: 56, left: 64, right: 12 };
45
+ /** The width of the legend band at the right of the square of a PCA, in pixels. */
46
+ const PCA_LEGEND_BAND_PX = 120;
47
+ /** The tooltip of a point with a name, and of a point with none. `{b}` is the name and `{c}` the pair. */
48
+ const NAMED_POINT_TOOLTIP = { trigger: "item", formatter: "{b}<br/>{a}: {c}" };
49
+ const PLAIN_POINT_TOOLTIP = { trigger: "item", formatter: "{a}: {c}" };
50
+ /** The label of each named point of a small PCA. Each point states its own side, and the gap is the one of the place estimate. */
51
+ const POINT_LABEL_FIELDS = { label: { show: true, formatter: "{b}", position: "right", distance: NAME_GAP_PX, color: CHART_INK } };
52
+ const PCA_DRAW = { figure: "pca", equalUnits: true, labelRows: PCA_LABEL_ROWS };
53
+ const SCATTER_DRAW = { figure: "scatter", equalUnits: false, labelRows: 0 };
54
+ /** The PCA of samples. */
55
+ export const PCA_FIGURE = {
56
+ reads: new Set(["x", "y", "group", "shape", "label"]),
57
+ derive: (block, rows, context) => deriveShapeDraw(block, rows, context, PCA_DRAW),
58
+ };
59
+ /** The members of a plain scatter that the shape draw does not read beside a shape. */
60
+ const UNSHAPED_MEMBERS = ["color", "size", "low", "high", "facet", "focus"];
61
+ /**
62
+ * The plain scatter with a `shape` channel.
63
+ *
64
+ * The module reads every member of a scatter, thus the refusal of a member that the shape draw cannot draw
65
+ * names the shape as the cause, and never states that a scatter reads no such member.
66
+ */
67
+ export const SHAPED_SCATTER_FIGURE = {
68
+ reads: new Set(["x", "y", "group", "shape", "label", ...UNSHAPED_MEMBERS]),
69
+ derive: (block, rows, context) => {
70
+ const encoding = block.encoding ?? {};
71
+ for (const member of UNSHAPED_MEMBERS) {
72
+ const declared = member === "focus" ? block.focus : encoding[member];
73
+ if (declared !== undefined) {
74
+ const noun = member === "focus" ? "focus" : `"${member}" channel`;
75
+ return err(chartProblem(block.id, `A scatter with a "shape" channel draws x, y, group, and label alone, thus it takes no ${noun} beside the shape.`));
76
+ }
77
+ }
78
+ return deriveShapeDraw(block, rows, context, SCATTER_DRAW);
79
+ },
80
+ };
81
+ /**
82
+ * Resolve one channel of the shape draw. A transform applies to a value channel. An order sorts no axis of
83
+ * this draw, thus it refuses.
84
+ */
85
+ function resolve(context, rows, draw, member, channel, numeric) {
86
+ const column = channelColumn(channel);
87
+ if (channelOrder(channel) !== undefined) {
88
+ const reason = numeric ? "draws a value axis" : "draws no category axis";
89
+ return err(chartProblem(context.blockId, `The "${member}" channel of the ${draw.figure} ${reason}, thus it takes no "orderBy".`));
90
+ }
91
+ if (rows.length > 0 && !columnPresent(column, rows, context.columns)) {
92
+ return err(chartProblem(context.blockId, `The column "${column}" is absent from every row.`));
93
+ }
94
+ const transform = channelTransform(channel);
95
+ if (!numeric && transform !== undefined) {
96
+ return err(chartProblem(context.blockId, `The "${member}" channel of the ${draw.figure} names categories, thus it takes no transform.`));
97
+ }
98
+ if (transform !== undefined) {
99
+ return ok({ name: `${transform}(${column})`, values: transformColumn(rows, column, transform) });
100
+ }
101
+ if (numeric) {
102
+ for (const row of rows) {
103
+ const cell = row[column];
104
+ if (typeof cell === "string" && cell.trim() !== "" && toNumber(cell) === null) {
105
+ return err(chartProblem(context.blockId, `The column "${column}" holds the text "${cell}", and the ${draw.figure} draws "${member}" on a value axis.`));
106
+ }
107
+ }
108
+ }
109
+ return ok({ name: column, values: rows.map((row) => row[column] ?? null) });
110
+ }
111
+ /** Derive the shape draw of a PCA or of a plain scatter. */
112
+ function deriveShapeDraw(block, rows, context, draw) {
113
+ const encoding = block.encoding ?? {};
114
+ if (encoding.x === undefined || encoding.y === undefined) {
115
+ const missing = encoding.x === undefined ? "x" : "y";
116
+ return err(chartProblem(block.id, `The ${draw.figure} chart needs a column for the "${missing}" channel.`));
117
+ }
118
+ const x = resolve(context, rows, draw, "x", encoding.x, true);
119
+ if (x.isErr())
120
+ return err(x.error);
121
+ const y = resolve(context, rows, draw, "y", encoding.y, true);
122
+ if (y.isErr())
123
+ return err(y.error);
124
+ let groups;
125
+ if (encoding.group !== undefined) {
126
+ const group = resolve(context, rows, draw, "group", encoding.group, false);
127
+ if (group.isErr())
128
+ return err(group.error);
129
+ groups = group.value;
130
+ }
131
+ let shapes;
132
+ if (encoding.shape !== undefined) {
133
+ const shape = resolve(context, rows, draw, "shape", encoding.shape, false);
134
+ if (shape.isErr())
135
+ return err(shape.error);
136
+ shapes = shape.value;
137
+ }
138
+ const labelColumn = encoding.label;
139
+ if (labelColumn !== undefined && rows.length > 0 && !columnPresent(labelColumn, rows, context.columns)) {
140
+ return err(chartProblem(block.id, `The column "${labelColumn}" is absent from every row.`));
141
+ }
142
+ const points = [];
143
+ for (let index = 0; index < rows.length; index += 1) {
144
+ const px = toNumber(x.value.values[index]);
145
+ const py = toNumber(y.value.values[index]);
146
+ const pg = groups?.values[index] ?? null;
147
+ const ps = shapes?.values[index] ?? null;
148
+ if (px === null || py === null || (groups !== undefined && pg === null) || (shapes !== undefined && ps === null))
149
+ continue;
150
+ points.push({ index, x: px, y: py, ...(pg !== null ? { group: categoryName(pg) } : {}), ...(ps !== null ? { shape: categoryName(ps) } : {}) });
151
+ }
152
+ const groupNames = firstAppearance(points.flatMap((point) => (point.group === undefined ? [] : [point.group])));
153
+ const shapeNames = firstAppearance(points.flatMap((point) => (point.shape === undefined ? [] : [point.shape])));
154
+ if (shapeNames.length > SHAPE_SYMBOLS.length) {
155
+ return err(chartProblem(block.id, `The "shape" channel holds ${shapeNames.length} categories, and a symbol reads for ${SHAPE_SYMBOLS.length} at most. Draw that column as the "group" channel.`));
156
+ }
157
+ const shared = groups !== undefined && shapes !== undefined ? groupNames.find((name) => shapeNames.includes(name)) : undefined;
158
+ if (shared !== undefined) {
159
+ return err(chartProblem(block.id, `The value "${shared}" names a group and a shape, thus the legend cannot tell the two keys apart.`));
160
+ }
161
+ const ranges = equalRanges(points.map((point) => point.x), points.map((point) => point.y), { pad: PCA_PAD, round: true });
162
+ const palette = categoricalPalette(groupNames.length);
163
+ const yTitle = valueAxisTitle(context.labels, y.value.name);
164
+ const named = labelColumn !== undefined;
165
+ const labeled = named && draw.labelRows > 0 && points.length <= draw.labelRows;
166
+ const sides = new Map();
167
+ if (labeled && labelColumn !== undefined) {
168
+ const placed = pointNameSides(points.map((point) => ({ x: point.x, y: point.y, text: String(rows[point.index][labelColumn] ?? "") })), ranges, SHAPED_POINT_PX);
169
+ for (const [place, point] of points.entries())
170
+ sides.set(point.index, placed[place]);
171
+ }
172
+ const buckets = new Map();
173
+ for (const point of points) {
174
+ const key = bucketKey(point.group, point.shape);
175
+ const bucket = buckets.get(key);
176
+ if (bucket === undefined)
177
+ buckets.set(key, [point]);
178
+ else
179
+ bucket.push(point);
180
+ }
181
+ const series = [];
182
+ const groupKeys = groups === undefined ? [undefined] : groupNames;
183
+ const shapeKeys = shapes === undefined ? [undefined] : shapeNames;
184
+ for (const [groupPlace, groupKey] of groupKeys.entries()) {
185
+ for (const [shapePlace, shapeKey] of shapeKeys.entries()) {
186
+ const members = buckets.get(bucketKey(groupKey, shapeKey));
187
+ if (members === undefined)
188
+ continue;
189
+ series.push({
190
+ type: "scatter",
191
+ name: groupKey ?? shapeKey ?? yTitle,
192
+ ...(shapeKey !== undefined ? { symbol: SHAPE_SYMBOLS[shapePlace] } : {}),
193
+ symbolSize: SHAPED_POINT_PX,
194
+ itemStyle: { color: groupKey !== undefined ? palette[groupPlace] : CHART_PALETTE[0] },
195
+ ...(labeled ? POINT_LABEL_FIELDS : {}),
196
+ data: members.map((point) => {
197
+ const pair = [point.x, point.y];
198
+ const name = labelColumn === undefined ? undefined : rows[point.index][labelColumn];
199
+ if (name === undefined)
200
+ return pair;
201
+ const side = sides.get(point.index);
202
+ return { value: pair, name: String(name), ...(side !== undefined && side !== "right" ? { label: { position: side } } : {}) };
203
+ }),
204
+ });
205
+ }
206
+ }
207
+ // A shape beside a group keys its symbols in a series of no data, thus the legend draws the symbol in ink.
208
+ if (groups !== undefined && shapes !== undefined) {
209
+ for (const [place, name] of shapeNames.entries()) {
210
+ series.push({ type: "scatter", name, symbol: SHAPE_SYMBOLS[place], symbolSize: SHAPED_POINT_PX, itemStyle: { color: CHART_INK }, data: [] });
211
+ }
212
+ }
213
+ const entries = [
214
+ ...(groups !== undefined ? groupNames.map((name) => ({ name, icon: POINT_LEGEND_ICON })) : []),
215
+ ...(shapes !== undefined ? shapeNames.map((name, place) => ({ name, icon: SHAPE_SYMBOLS[place] })) : []),
216
+ ];
217
+ const keyed = entries.length > 1;
218
+ const xTitle = valueAxisTitle(context.labels, x.value.name);
219
+ const option = {
220
+ tooltip: named ? { ...NAMED_POINT_TOOLTIP } : { ...PLAIN_POINT_TOOLTIP },
221
+ series,
222
+ legend: keyed ? { orient: draw.equalUnits ? "vertical" : "horizontal", ...(draw.equalUnits ? {} : { bottom: 0 }), data: entries } : { show: false },
223
+ };
224
+ if (!draw.equalUnits) {
225
+ return ok({ ...option, xAxis: valueAxis("x", xTitle, { scale: true }), yAxis: valueAxis("y", yTitle, { scale: true }) });
226
+ }
227
+ const layout = squareLayout({ panels: 1, margins: PCA_MARGINS, side: keyed ? PCA_LEGEND_BAND_PX : 0, legend: keyed });
228
+ const interval = ranges.step !== undefined ? { interval: ranges.step } : {};
229
+ return ok({
230
+ ...option,
231
+ grid: layout.grid,
232
+ xAxis: { ...valueAxis("x", xTitle, ranges.x), ...interval },
233
+ yAxis: { ...valueAxis("y", yTitle, ranges.y), ...interval },
234
+ ...(keyed && layout.legend !== undefined ? { legend: { ...option.legend, ...layout.legend } } : {}),
235
+ media: layout.media,
236
+ });
237
+ }
238
+ /** The key of the split of one group and one shape. An absent member names no channel. */
239
+ function bucketKey(group, shape) {
240
+ return JSON.stringify([group ?? null, shape ?? null]);
241
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The QQ figure of a GWAS, as qqman and the GWAS quality-control tools draw it: the expected −log10(p) on x,
3
+ * the observed −log10(p) on y, and the diagonal where the two agree under the null.
4
+ *
5
+ * - The points draw small, in the ink of the page.
6
+ * - The band between the `low` and the `high` columns draws in light gray under the points. It states the
7
+ * confidence band of the null expectation that the run computed.
8
+ * - An identity line runs from zero to the largest expected value, where the null expectation ends.
9
+ * - Each axis spans from zero to its own round end, as qqman draws it: x to the largest expected value, and y
10
+ * to the largest observed value. A strong signal can reach a hundred times the expected range, and one
11
+ * shared range would squeeze the points into a thin strip at the left edge.
12
+ * - The statistics, for example the genomic inflation λ, print in the top left corner.
13
+ * - Where the y channel transforms a p column, a stored zero draws an upward triangle at the top of the observed
14
+ * range.
15
+ *
16
+ * The points come from the composition machinery, thus a table of many thousand p-values reads the shared
17
+ * payload. The band draws after the points in the series list, and it keeps its rows inline: its upper half
18
+ * holds the width of the band, which no cell gives. The band reads at most `QQ_BAND_POINTS` rows, spaced
19
+ * along the x axis, thus it stays small beside a payload of any size.
20
+ */
21
+ import type { FigureModule } from "./index.js";
22
+ /** The symbol size in pixels of a QQ point. */
23
+ export declare const QQ_POINT_PX = 3;
24
+ /**
25
+ * The largest count of rows that the band reads. One row for each two pixels of a wide plot keeps the drawn
26
+ * edge of the band as the full table draws it.
27
+ */
28
+ export declare const QQ_BAND_POINTS = 400;
29
+ /** The QQ figure module. */
30
+ export declare const QQ_FIGURE: FigureModule;
@@ -0,0 +1,182 @@
1
+ /**
2
+ * The QQ figure of a GWAS, as qqman and the GWAS quality-control tools draw it: the expected −log10(p) on x,
3
+ * the observed −log10(p) on y, and the diagonal where the two agree under the null.
4
+ *
5
+ * - The points draw small, in the ink of the page.
6
+ * - The band between the `low` and the `high` columns draws in light gray under the points. It states the
7
+ * confidence band of the null expectation that the run computed.
8
+ * - An identity line runs from zero to the largest expected value, where the null expectation ends.
9
+ * - Each axis spans from zero to its own round end, as qqman draws it: x to the largest expected value, and y
10
+ * to the largest observed value. A strong signal can reach a hundred times the expected range, and one
11
+ * shared range would squeeze the points into a thin strip at the left edge.
12
+ * - The statistics, for example the genomic inflation λ, print in the top left corner.
13
+ * - Where the y channel transforms a p column, a stored zero draws an upward triangle at the top of the observed
14
+ * range.
15
+ *
16
+ * The points come from the composition machinery, thus a table of many thousand p-values reads the shared
17
+ * payload. The band draws after the points in the series list, and it keeps its rows inline: its upper half
18
+ * holds the width of the band, which no cell gives. The band reads at most `QQ_BAND_POINTS` rows, spaced
19
+ * along the x axis, thus it stays small beside a payload of any size.
20
+ */
21
+ import { err, ok } from "neverthrow";
22
+ import { channelColumn, channelTransform } from "../../contracts/report-blocks.js";
23
+ import { CHART_INK, GUIDE_LINE_COLOR, GUIDE_LINE_WIDTH_PX, MUTED_CHART_COLOR } from "../design.js";
24
+ import { chartProblem, plainColumn, statisticsGraphic, toNumber } from "./common.js";
25
+ import { axisOf, BELOW_RESOLUTION_NAME, BELOW_RESOLUTION_SYMBOL, BELOW_RESOLUTION_SYMBOL_PX, BELOW_RESOLUTION_TOOLTIP, belowResolutionHeight, belowResolutionRows, demandedChannel, largest, niceCeiling, plottedValues, seriesOf, } from "./dense.js";
26
+ /** The symbol size in pixels of a QQ point. */
27
+ export const QQ_POINT_PX = 3;
28
+ /**
29
+ * The largest count of rows that the band reads. One row for each two pixels of a wide plot keeps the drawn
30
+ * edge of the band as the full table draws it.
31
+ */
32
+ export const QQ_BAND_POINTS = 400;
33
+ /** The fill of the band: the muted gray, light. */
34
+ const BAND_OPACITY = 0.3;
35
+ /** The drawing order of the band. The runtime draws a scatter at the order 2, thus the band sits under it. */
36
+ const BAND_Z = 1;
37
+ /** The titles of the two axes where the binding declares no label. The minus sign is the character. */
38
+ const EXPECTED_TITLE = "Expected −log10(p)";
39
+ const OBSERVED_TITLE = "Observed −log10(p)";
40
+ /** The name of the two band series. The legend of the figure hides, thus the name rides the tooltip alone. */
41
+ const BAND_NAME = "Null band";
42
+ /** The members of a QQ block that the figure reads. */
43
+ const READS = new Set(["x", "y", "low", "high", "statistics"]);
44
+ /**
45
+ * The rows that the band reads: each row with a number in the three columns, in ascending order of x, thinned
46
+ * to at most `QQ_BAND_POINTS` rows spaced along the x axis, or spaced by place where each row holds one x. The
47
+ * first and the last row always stay, thus the band spans the whole axis.
48
+ *
49
+ * The band stacks its width on its lower bound, and a stack takes no negative width. Thus a row whose `high`
50
+ * value sits under its `low` value refuses, and the refusal names the row.
51
+ */
52
+ function bandRows(blockId, rows, xs, lowColumn, highColumn) {
53
+ const complete = [];
54
+ for (let index = 0; index < rows.length; index += 1) {
55
+ const x = xs[index];
56
+ const low = toNumber(rows[index][lowColumn]);
57
+ const high = toNumber(rows[index][highColumn]);
58
+ if (x === null || low === null || high === null)
59
+ continue;
60
+ if (high < low) {
61
+ return err(chartProblem(blockId, `The row ${index + 1} holds the band from ${low} in "${lowColumn}" to ${high} in "${highColumn}". ` +
62
+ 'The "low" bound sits at or under the "high" bound.'));
63
+ }
64
+ complete.push({ x, low, high, index });
65
+ }
66
+ complete.sort((a, b) => a.x - b.x || a.index - b.index);
67
+ if (complete.length <= QQ_BAND_POINTS)
68
+ return ok(complete);
69
+ const last = complete[complete.length - 1];
70
+ const step = (last.x - complete[0].x) / (QQ_BAND_POINTS - 1);
71
+ // Each row sits zero steps from the next one, thus the rule of the step keeps each row.
72
+ if (!(step > 0)) {
73
+ return ok(Array.from({ length: QQ_BAND_POINTS }, (_row, place) => complete[Math.round((place * (complete.length - 1)) / (QQ_BAND_POINTS - 1))]));
74
+ }
75
+ // The kept rows sit at least one step apart, and a kept row between the ends sits at least one step before
76
+ // the last row. Thus the span holds at most `QQ_BAND_POINTS - 1` steps, and the band at most
77
+ // `QQ_BAND_POINTS` rows.
78
+ const kept = [complete[0]];
79
+ for (let place = 1; place < complete.length - 1; place += 1) {
80
+ const row = complete[place];
81
+ if (row.x - kept[kept.length - 1].x >= step && last.x - row.x >= step)
82
+ kept.push(row);
83
+ }
84
+ kept.push(last);
85
+ return ok(kept);
86
+ }
87
+ /**
88
+ * The two series of the band: a lower series that carries the lower bound with no ink, and an upper series
89
+ * that stacks the width of the band on it and fills the area between.
90
+ */
91
+ function bandSeries(band) {
92
+ const shared = { type: "line", name: BAND_NAME, stack: "qq-band", showSymbol: false, silent: true, tooltip: { show: false }, z: BAND_Z };
93
+ return [
94
+ { ...shared, lineStyle: { opacity: 0 }, data: band.map((row) => [row.x, row.low]) },
95
+ {
96
+ ...shared,
97
+ lineStyle: { opacity: 0 },
98
+ areaStyle: { color: MUTED_CHART_COLOR, opacity: BAND_OPACITY },
99
+ data: band.map((row) => [row.x, row.high - row.low]),
100
+ },
101
+ ];
102
+ }
103
+ function deriveQq(block, rows, context) {
104
+ const encoding = block.encoding ?? {};
105
+ const x = demandedChannel(context.blockId, "qq", encoding, "x");
106
+ if (x.isErr())
107
+ return err(x.error);
108
+ const y = demandedChannel(context.blockId, "qq", encoding, "y");
109
+ if (y.isErr())
110
+ return err(y.error);
111
+ // The band draws each bound as its cell gives it, in the unit of the plotted y, thus a bound takes no transform.
112
+ const source = { blockId: context.blockId, rows, columns: context.columns };
113
+ const low = plainColumn("qq", encoding, "low", source);
114
+ if (low.isErr())
115
+ return err(low.error);
116
+ const high = plainColumn("qq", encoding, "high", source);
117
+ if (high.isErr())
118
+ return err(high.error);
119
+ const composition = { series: [{ form: "scatter", encoding: { x: x.value, y: y.value } }] };
120
+ const composed = context.compose(composition, { preset: { x: EXPECTED_TITLE, y: OBSERVED_TITLE } });
121
+ if (composed.isErr())
122
+ return err(composed.error);
123
+ const xs = plottedValues(rows, x.value);
124
+ const ys = plottedValues(rows, y.value);
125
+ const banded = low.value !== undefined && high.value !== undefined ? bandRows(context.blockId, rows, xs, low.value, high.value) : ok([]);
126
+ if (banded.isErr())
127
+ return err(banded.error);
128
+ const band = banded.value;
129
+ const expectedValues = [];
130
+ const observedValues = [];
131
+ for (let index = 0; index < rows.length; index += 1) {
132
+ const expected = xs[index];
133
+ const observed = ys[index];
134
+ if (expected === null || observed === null)
135
+ continue;
136
+ expectedValues.push(expected);
137
+ observedValues.push(observed);
138
+ }
139
+ const top = largest(expectedValues) ?? 0;
140
+ const peak = largest(observedValues);
141
+ // A stored zero of a p that the channel transforms draws at the top of the observed range.
142
+ const zeroRows = channelTransform(y.value) === "neg_log10" ? belowResolutionRows(rows, channelColumn(y.value), xs) : [];
143
+ const zeroY = belowResolutionHeight(peak, 0);
144
+ const xEnd = niceCeiling(Math.max(top, largest(band.map((row) => row.x)) ?? 0));
145
+ // The y range holds the identity line and the band as well as the points.
146
+ const yEnd = niceCeiling(Math.max(peak ?? 0, top, largest(band.map((row) => row.high)) ?? 0, zeroRows.length > 0 ? zeroY : 0));
147
+ const identity = {
148
+ silent: true,
149
+ symbol: "none",
150
+ lineStyle: { color: GUIDE_LINE_COLOR, width: GUIDE_LINE_WIDTH_PX, type: "solid" },
151
+ label: { show: false },
152
+ data: [[{ coord: [0, 0] }, { coord: [top, top] }]],
153
+ };
154
+ const option = composed.value;
155
+ const statistics = statisticsGraphic(context.statistics, "top-left");
156
+ return ok({
157
+ ...option,
158
+ legend: { show: false },
159
+ xAxis: { ...axisOf(option, "xAxis"), min: 0, max: xEnd },
160
+ yAxis: { ...axisOf(option, "yAxis"), min: 0, max: yEnd },
161
+ series: [
162
+ ...seriesOf(option).map((series) => ({ ...series, symbolSize: QQ_POINT_PX, itemStyle: { color: CHART_INK }, markLine: identity })),
163
+ ...(band.length > 0 ? bandSeries(band) : []),
164
+ ...(zeroRows.length > 0
165
+ ? [
166
+ {
167
+ type: "scatter",
168
+ name: BELOW_RESOLUTION_NAME,
169
+ symbol: BELOW_RESOLUTION_SYMBOL,
170
+ symbolSize: BELOW_RESOLUTION_SYMBOL_PX,
171
+ itemStyle: { color: CHART_INK },
172
+ tooltip: BELOW_RESOLUTION_TOOLTIP,
173
+ data: zeroRows.map((index) => [xs[index] ?? 0, zeroY]),
174
+ },
175
+ ]
176
+ : []),
177
+ ],
178
+ ...(statistics.length > 0 ? { graphic: statistics } : {}),
179
+ });
180
+ }
181
+ /** The QQ figure module. */
182
+ export const QQ_FIGURE = { reads: READS, derive: deriveQq };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The ROC figure: the empirical curve of each group, the dashed diagonal of chance, and the statistics text.
3
+ *
4
+ * A row of the table is one operating point of a classifier: its false positive rate and its true positive
5
+ * rate at one threshold. The curve runs through the points of its group in order of the false positive rate,
6
+ * then of the true positive rate, thus the steps of an empirical curve read as steps, and a tie of scores
7
+ * reads as the diagonal segment that the field draws for it. The figure adds no point that the table does not
8
+ * hold.
9
+ *
10
+ * The two axes hold one quantity, a rate from 0 to 1, thus they share the range, the tick step, and the unit:
11
+ * the plot is a square, and the diagonal of chance runs at 45 degrees. The legend sits in a band under the
12
+ * square, thus a narrow column export keeps the square large. The statistics text is the label of a point at
13
+ * the corner (1, 0) of the data, thus it stays in the corner of the square in each container.
14
+ */
15
+ import type { FigureModule } from "./index.js";
16
+ /** The ROC curve figure. */
17
+ export declare const ROC_FIGURE: FigureModule;
@@ -0,0 +1,116 @@
1
+ /**
2
+ * The ROC figure: the empirical curve of each group, the dashed diagonal of chance, and the statistics text.
3
+ *
4
+ * A row of the table is one operating point of a classifier: its false positive rate and its true positive
5
+ * rate at one threshold. The curve runs through the points of its group in order of the false positive rate,
6
+ * then of the true positive rate, thus the steps of an empirical curve read as steps, and a tie of scores
7
+ * reads as the diagonal segment that the field draws for it. The figure adds no point that the table does not
8
+ * hold.
9
+ *
10
+ * The two axes hold one quantity, a rate from 0 to 1, thus they share the range, the tick step, and the unit:
11
+ * the plot is a square, and the diagonal of chance runs at 45 degrees. The legend sits in a band under the
12
+ * square, thus a narrow column export keeps the square large. The statistics text is the label of a point at
13
+ * the corner (1, 0) of the data, thus it stays in the corner of the square in each container.
14
+ */
15
+ import { err, ok } from "neverthrow";
16
+ import { categoricalPalette, categoryName, chartProblem, demandedColumn, firstAppearance, GUIDE_LINE_STYLE, plainColumn, STATISTICS_POINT_INSET_PX, statisticsSeries, toNumber, valueAxis, valueAxisTitle, } from "./common.js";
17
+ import { squareLayout } from "./equal-units.js";
18
+ /** The members of a roc block that the figure reads. */
19
+ const ROC_READS = new Set(["x", "y", "group", "statistics"]);
20
+ /** The ROC curve figure. */
21
+ export const ROC_FIGURE = { reads: ROC_READS, derive: deriveRoc };
22
+ /** The tick step of both rate axes. */
23
+ const RATE_STEP = 0.2;
24
+ /** The margins of the square of a ROC curve, in pixels: the tick labels and the title of each axis. */
25
+ const ROC_MARGINS = { top: 16, bottom: 56, left: 64, right: 12 };
26
+ /** The stroke width of a curve, in pixels. */
27
+ const CURVE_WIDTH_PX = 1.5;
28
+ /** Derive the figure of one roc block. */
29
+ function deriveRoc(block, rows, context) {
30
+ const encoding = block.encoding ?? {};
31
+ const source = { blockId: context.blockId, rows, columns: context.columns };
32
+ const fpr = demandedColumn("roc", encoding, "x", source);
33
+ if (fpr.isErr())
34
+ return err(fpr.error);
35
+ const tpr = demandedColumn("roc", encoding, "y", source);
36
+ if (tpr.isErr())
37
+ return err(tpr.error);
38
+ const group = plainColumn("roc", encoding, "group", source);
39
+ if (group.isErr())
40
+ return err(group.error);
41
+ const curves = rocCurves(rows, fpr.value, tpr.value, group.value, context.blockId);
42
+ if (curves.isErr())
43
+ return err(curves.error);
44
+ const palette = categoricalPalette(curves.value.length);
45
+ const series = curves.value.map((curve, index) => {
46
+ const color = palette[index % palette.length];
47
+ return {
48
+ type: "line",
49
+ name: categoryName(curve.name),
50
+ showSymbol: false,
51
+ itemStyle: { color },
52
+ lineStyle: { color, width: CURVE_WIDTH_PX },
53
+ data: curve.points,
54
+ };
55
+ });
56
+ series.push({
57
+ type: "line",
58
+ silent: true,
59
+ tooltip: { show: false },
60
+ data: [],
61
+ markLine: {
62
+ silent: true,
63
+ symbol: "none",
64
+ label: { show: false },
65
+ lineStyle: { ...GUIDE_LINE_STYLE },
66
+ data: [[{ coord: [0, 0] }, { coord: [1, 1] }]],
67
+ },
68
+ });
69
+ const statistics = statisticsSeries(context.statistics, [1, 0], "bottom-right");
70
+ const squareLabel = statistics.length > 0 ? { series: series.length, insetPx: STATISTICS_POINT_INSET_PX } : undefined;
71
+ series.push(...statistics);
72
+ const keyed = curves.value.length > 1;
73
+ const names = curves.value.map((curve) => categoryName(curve.name));
74
+ const layout = squareLayout({
75
+ panels: 1,
76
+ margins: ROC_MARGINS,
77
+ side: 0,
78
+ legend: keyed,
79
+ legendBand: { place: "bottom", entries: names },
80
+ ...(squareLabel !== undefined ? { squareLabel } : {}),
81
+ });
82
+ return ok({
83
+ grid: layout.grid,
84
+ legend: keyed && layout.legend !== undefined ? { ...layout.legend, data: names } : { show: false },
85
+ xAxis: { ...valueAxis("x", valueAxisTitle(context.labels, fpr.value), { min: 0, max: 1 }), interval: RATE_STEP },
86
+ yAxis: { ...valueAxis("y", valueAxisTitle(context.labels, tpr.value), { min: 0, max: 1 }), interval: RATE_STEP },
87
+ series,
88
+ media: layout.media,
89
+ });
90
+ }
91
+ /**
92
+ * The curves of one table: one for each group in the order of its first row. A row whose rate is not numeric
93
+ * draws nothing, and a rate outside 0 to 1 refuses, because the axes hold 0 to 1 and a clip would hide it.
94
+ * The sort is stable, thus two rows at one point keep the order of the table.
95
+ */
96
+ function rocCurves(rows, fpr, tpr, group, blockId) {
97
+ const groupOf = (row) => (group === undefined ? "" : (row[group] ?? ""));
98
+ const names = firstAppearance(rows.map((row) => String(groupOf(row))));
99
+ const byName = new Map(names.map((name) => [name, []]));
100
+ for (const [index, row] of rows.entries()) {
101
+ const x = toNumber(row[fpr]);
102
+ const y = toNumber(row[tpr]);
103
+ if (x === null || y === null)
104
+ continue;
105
+ for (const [value, column] of [
106
+ [x, fpr],
107
+ [y, tpr],
108
+ ]) {
109
+ if (value < 0 || value > 1) {
110
+ return err(chartProblem(blockId, `The roc figure reads rates between 0 and 1, and the row ${index + 1} holds ${value} in the "${column}" column.`));
111
+ }
112
+ }
113
+ byName.get(String(groupOf(row)))?.push([x, y]);
114
+ }
115
+ return ok(names.map((name) => ({ name, points: (byName.get(name) ?? []).sort((a, b) => a[0] - b[0] || a[1] - b[1]) })));
116
+ }
@@ -0,0 +1,48 @@
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 { type Result } from "neverthrow";
26
+ import type { FigureModule } from "./index.js";
27
+ /** The opacity of a flow. A flow reads as the path of its source, and a label reads over it. */
28
+ export declare const SANKEY_LINK_OPACITY = 0.35;
29
+ /**
30
+ * 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
31
+ * size. A label sits over the flows of its node, and the flows of a thin node are thinner than the text. Thus a
32
+ * flow edge would cross the letters, and a halo keeps it visible between them. The box hides each flow under
33
+ * the text.
34
+ */
35
+ export declare const SANKEY_LABEL_BOX_COLOR = "#ffffff";
36
+ /** The Sankey module: the source, the target, the flow, and the group. */
37
+ export declare const SANKEY_FIGURE: FigureModule;
38
+ /** One flow of the table: its two nodes. */
39
+ export interface SankeyEdge {
40
+ readonly source: string;
41
+ readonly target: string;
42
+ }
43
+ /**
44
+ * The stage of each node, in the order of first appearance: the longest chain of flows from a node with no
45
+ * inflow. A node with no outflow takes the last stage. A cycle has no stage, thus it refuses, and the error
46
+ * names the nodes of the cycle.
47
+ */
48
+ export declare function sankeyStages(edges: readonly SankeyEdge[]): Result<Map<string, number>, string>;