@inflexa-ai/harness 0.42.1 → 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 (217) hide show
  1. package/README.md +2 -2
  2. package/dist/agents/conversation-agent.js +5 -1
  3. package/dist/agents/sandbox/drug-repurposing-agent.js +1 -0
  4. package/dist/agents/sandbox/shared.d.ts +1 -6
  5. package/dist/agents/sandbox/shared.js +3 -16
  6. package/dist/agents/sandbox/types.d.ts +1 -1
  7. package/dist/app/chat-turn.js +13 -1
  8. package/dist/config/environment-stores.d.ts +4 -2
  9. package/dist/contracts/chat-events.d.ts +9 -1
  10. package/dist/contracts/chat-frame.d.ts +3 -3
  11. package/dist/contracts/chat-frame.js +8 -3
  12. package/dist/contracts/chat-parts.d.ts +4 -1
  13. package/dist/contracts/data-profile.d.ts +4 -6
  14. package/dist/contracts/index.d.ts +1 -1
  15. package/dist/contracts/report-blocks.d.ts +1364 -75
  16. package/dist/contracts/report-blocks.js +221 -29
  17. package/dist/contracts/report-reference.js +1 -1
  18. package/dist/contracts/schemas/chat-events.d.ts +13 -0
  19. package/dist/contracts/schemas/chat-events.js +5 -0
  20. package/dist/contracts/schemas/chat-parts.d.ts +2 -2
  21. package/dist/contracts/schemas/index.d.ts +1 -1
  22. package/dist/contracts/schemas/index.js +1 -1
  23. package/dist/execution/input-signature.d.ts +7 -13
  24. package/dist/execution/input-signature.js +0 -0
  25. package/dist/execution/run-canceler.d.ts +3 -2
  26. package/dist/execution/run-canceler.js +3 -2
  27. package/dist/execution/run-event-stream.d.ts +16 -2
  28. package/dist/execution/run-event-stream.js +54 -3
  29. package/dist/index.d.ts +1 -8
  30. package/dist/index.js +5 -16
  31. package/dist/input-scan/enrich.d.ts +0 -2
  32. package/dist/input-scan/enrich.js +0 -1
  33. package/dist/lib/metrics.js +6 -7
  34. package/dist/lib/otel-spans.d.ts +4 -9
  35. package/dist/lib/otel-spans.js +3 -8
  36. package/dist/loop/run-agent.d.ts +4 -2
  37. package/dist/loop/run-agent.js +38 -23
  38. package/dist/memory/thread-history.d.ts +1 -1
  39. package/dist/prompts/report-session.d.ts +1 -1
  40. package/dist/prompts/report-session.js +17 -0
  41. package/dist/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
  42. package/dist/prompts/sandbox/drug-repurposing-agent.js +7 -0
  43. package/dist/provenance/exec-frame.d.ts +1 -1
  44. package/dist/report-model/authoring-grammar.d.ts +416 -17
  45. package/dist/report-model/authoring-grammar.js +25 -4
  46. package/dist/report-model/block-walk.d.ts +20 -4
  47. package/dist/report-model/block-walk.js +64 -7
  48. package/dist/report-model/draft-finish.d.ts +6 -2
  49. package/dist/report-model/draft-finish.js +6 -1
  50. package/dist/report-model/production-resolver.d.ts +8 -0
  51. package/dist/report-model/production-resolver.js +1 -1
  52. package/dist/report-model/validate.d.ts +20 -10
  53. package/dist/report-model/validate.js +29 -13
  54. package/dist/report-render/chart-export.d.ts +52 -0
  55. package/dist/report-render/chart-export.js +192 -0
  56. package/dist/report-render/chart-presets.d.ts +46 -47
  57. package/dist/report-render/chart-presets.js +31 -101
  58. package/dist/report-render/chart-renderers.d.ts +163 -0
  59. package/dist/report-render/chart-renderers.js +1055 -0
  60. package/dist/report-render/chart-toolbox.d.ts +124 -0
  61. package/dist/report-render/chart-toolbox.js +595 -0
  62. package/dist/report-render/chart.d.ts +60 -27
  63. package/dist/report-render/chart.js +2323 -524
  64. package/dist/report-render/design.d.ts +476 -16
  65. package/dist/report-render/design.js +390 -56
  66. package/dist/report-render/figures/common.d.ts +321 -0
  67. package/dist/report-render/figures/common.js +759 -0
  68. package/dist/report-render/figures/dendrogram.d.ts +36 -0
  69. package/dist/report-render/figures/dendrogram.js +130 -0
  70. package/dist/report-render/figures/dense.d.ts +111 -0
  71. package/dist/report-render/figures/dense.js +204 -0
  72. package/dist/report-render/figures/dotplot.d.ts +28 -0
  73. package/dist/report-render/figures/dotplot.js +234 -0
  74. package/dist/report-render/figures/embedding.d.ts +43 -0
  75. package/dist/report-render/figures/embedding.js +360 -0
  76. package/dist/report-render/figures/equal-units.d.ts +101 -0
  77. package/dist/report-render/figures/equal-units.js +215 -0
  78. package/dist/report-render/figures/forest.d.ts +20 -0
  79. package/dist/report-render/figures/forest.js +398 -0
  80. package/dist/report-render/figures/gsea.d.ts +30 -0
  81. package/dist/report-render/figures/gsea.js +391 -0
  82. package/dist/report-render/figures/heatmap.d.ts +40 -0
  83. package/dist/report-render/figures/heatmap.js +395 -0
  84. package/dist/report-render/figures/index.d.ts +135 -0
  85. package/dist/report-render/figures/index.js +100 -0
  86. package/dist/report-render/figures/km.d.ts +16 -0
  87. package/dist/report-render/figures/km.js +406 -0
  88. package/dist/report-render/figures/label-room.d.ts +130 -0
  89. package/dist/report-render/figures/label-room.js +341 -0
  90. package/dist/report-render/figures/locuszoom.d.ts +50 -0
  91. package/dist/report-render/figures/locuszoom.js +675 -0
  92. package/dist/report-render/figures/lollipop.d.ts +16 -0
  93. package/dist/report-render/figures/lollipop.js +247 -0
  94. package/dist/report-render/figures/ma.d.ts +20 -0
  95. package/dist/report-render/figures/ma.js +114 -0
  96. package/dist/report-render/figures/manhattan.d.ts +60 -0
  97. package/dist/report-render/figures/manhattan.js +343 -0
  98. package/dist/report-render/figures/oncoprint.d.ts +41 -0
  99. package/dist/report-render/figures/oncoprint.js +420 -0
  100. package/dist/report-render/figures/pca.d.ts +37 -0
  101. package/dist/report-render/figures/pca.js +241 -0
  102. package/dist/report-render/figures/qq.d.ts +30 -0
  103. package/dist/report-render/figures/qq.js +182 -0
  104. package/dist/report-render/figures/roc.d.ts +17 -0
  105. package/dist/report-render/figures/roc.js +116 -0
  106. package/dist/report-render/figures/sankey.d.ts +48 -0
  107. package/dist/report-render/figures/sankey.js +353 -0
  108. package/dist/report-render/figures/upset.d.ts +60 -0
  109. package/dist/report-render/figures/upset.js +316 -0
  110. package/dist/report-render/figures/volcano.d.ts +25 -0
  111. package/dist/report-render/figures/volcano.js +166 -0
  112. package/dist/report-render/hybrid-svg.d.ts +75 -0
  113. package/dist/report-render/hybrid-svg.js +504 -0
  114. package/dist/report-render/number-format.d.ts +19 -0
  115. package/dist/report-render/number-format.js +25 -1
  116. package/dist/report-render/page.d.ts +41 -4
  117. package/dist/report-render/page.js +247 -12
  118. package/dist/report-render/render.d.ts +13 -4
  119. package/dist/report-render/render.js +37 -6
  120. package/dist/report-render/types.d.ts +23 -2
  121. package/dist/report-render/value-bridge.d.ts +38 -3
  122. package/dist/report-render/value-bridge.js +137 -6
  123. package/dist/report-render/views/chart-view.d.ts +42 -6
  124. package/dist/report-render/views/chart-view.js +70 -9
  125. package/dist/report-render/views/page-view.js +9 -10
  126. package/dist/runtime/assemble.js +2 -2
  127. package/dist/sandbox/client.d.ts +23 -32
  128. package/dist/sandbox/client.js +3 -11
  129. package/dist/sandbox/create-sandbox.d.ts +9 -24
  130. package/dist/sandbox/create-sandbox.js +15 -30
  131. package/dist/sandbox/docker-client.d.ts +12 -22
  132. package/dist/sandbox/docker-client.js +29 -43
  133. package/dist/sandbox/exec.d.ts +58 -0
  134. package/dist/sandbox/exec.js +197 -0
  135. package/dist/sandbox/identity.d.ts +5 -5
  136. package/dist/sandbox/identity.js +7 -10
  137. package/dist/sandbox/k8s-client.d.ts +4 -10
  138. package/dist/sandbox/k8s-client.js +5 -12
  139. package/dist/sandbox/liveness.d.ts +5 -12
  140. package/dist/sandbox/liveness.js +5 -12
  141. package/dist/sandbox/notification-sweep.d.ts +4 -7
  142. package/dist/sandbox/notification-sweep.js +4 -7
  143. package/dist/sandbox/reaper.d.ts +3 -3
  144. package/dist/sandbox/reaper.js +3 -3
  145. package/dist/sandbox/sandbox-error.d.ts +10 -11
  146. package/dist/sandbox/sandbox-error.js +10 -11
  147. package/dist/sandbox/sandbox-step-translate.d.ts +1 -1
  148. package/dist/sandbox/sandbox-step-translate.js +1 -1
  149. package/dist/sandbox/types.d.ts +31 -99
  150. package/dist/sandbox/types.js +17 -84
  151. package/dist/state/active-sandboxes.d.ts +7 -32
  152. package/dist/state/active-sandboxes.js +11 -46
  153. package/dist/state/index.d.ts +1 -2
  154. package/dist/state/index.js +1 -1
  155. package/dist/state/migrations/20261005120000_drop_the_active_exec_tracking.d.ts +7 -0
  156. package/dist/state/migrations/20261005120000_drop_the_active_exec_tracking.js +12 -0
  157. package/dist/state/migrations/index.js +2 -0
  158. package/dist/state/schema.d.ts +1 -5
  159. package/dist/state/schema.js +1 -6
  160. package/dist/state/step-executions.js +1 -2
  161. package/dist/tasks/data-profile.js +7 -20
  162. package/dist/tasks/derive-table-exec.d.ts +4 -14
  163. package/dist/tasks/derive-table-exec.js +9 -24
  164. package/dist/tasks/extract-values.d.ts +2 -2
  165. package/dist/tasks/extract-values.js +4 -7
  166. package/dist/tools/bio/index.d.ts +2 -0
  167. package/dist/tools/bio/index.js +3 -0
  168. package/dist/tools/bio/keys.d.ts +11 -0
  169. package/dist/tools/bio/keys.js +15 -0
  170. package/dist/tools/bio/search-clinical-trials.d.ts +1 -1
  171. package/dist/tools/bio/search-patents.d.ts +46 -0
  172. package/dist/tools/bio/search-patents.js +134 -0
  173. package/dist/tools/bio/search-regulatory-approvals.d.ts +49 -0
  174. package/dist/tools/bio/search-regulatory-approvals.js +158 -0
  175. package/dist/tools/define-tool.d.ts +56 -13
  176. package/dist/tools/define-tool.js +28 -1
  177. package/dist/tools/execute-analysis.js +3 -3
  178. package/dist/tools/lib/amass-client.d.ts +586 -0
  179. package/dist/tools/lib/amass-client.js +492 -0
  180. package/dist/tools/lib/amass-config.d.ts +2 -0
  181. package/dist/tools/lib/amass-config.js +10 -0
  182. package/dist/tools/report-session/derive-table.d.ts +8 -9
  183. package/dist/tools/report-session/derive-table.js +3 -4
  184. package/dist/tools/report-session/preview-report.js +4 -49
  185. package/dist/tools/sandbox/list-available-packages.d.ts +1 -1
  186. package/dist/tools/sandbox/list-available-packages.js +5 -4
  187. package/dist/tools/workspace/edit-file.d.ts +5 -4
  188. package/dist/tools/workspace/edit-file.js +10 -13
  189. package/dist/tools/workspace/execute-command.d.ts +27 -42
  190. package/dist/tools/workspace/execute-command.js +42 -38
  191. package/dist/tools/workspace/index.d.ts +5 -6
  192. package/dist/tools/workspace/index.js +5 -6
  193. package/dist/tools/workspace/mutator.d.ts +43 -8
  194. package/dist/tools/workspace/mutator.js +45 -34
  195. package/dist/tools/workspace/run-exec.d.ts +3 -12
  196. package/dist/tools/workspace/run-exec.js +7 -24
  197. package/dist/tools/workspace/scan-inputs.d.ts +0 -3
  198. package/dist/tools/workspace/scan-inputs.js +0 -1
  199. package/dist/tools/workspace/write-file.d.ts +12 -3
  200. package/dist/tools/workspace/write-file.js +17 -13
  201. package/dist/workflows/sandbox-step.d.ts +7 -11
  202. package/dist/workflows/sandbox-step.js +24 -27
  203. package/package.json +3 -1
  204. package/dist/sandbox/await-exec.d.ts +0 -143
  205. package/dist/sandbox/await-exec.js +0 -417
  206. package/dist/sandbox/deliver-exec-event.d.ts +0 -25
  207. package/dist/sandbox/deliver-exec-event.js +0 -29
  208. package/dist/sandbox/digest.d.ts +0 -14
  209. package/dist/sandbox/digest.js +0 -17
  210. package/dist/sandbox/exec-id.d.ts +0 -12
  211. package/dist/sandbox/exec-id.js +0 -20
  212. package/dist/sandbox/hmac.d.ts +0 -62
  213. package/dist/sandbox/hmac.js +0 -72
  214. package/dist/sandbox/submit-exec.d.ts +0 -38
  215. package/dist/sandbox/submit-exec.js +0 -80
  216. package/dist/sandbox/watchdog.d.ts +0 -71
  217. package/dist/sandbox/watchdog.js +0 -161
@@ -0,0 +1,391 @@
1
+ /**
2
+ * The GSEA running enrichment score figure, as the GSEA paper (Subramanian et al. 2005, figure 1) and the
3
+ * `gseaplot2` of enrichplot draw it: three stacked panels over one shared axis of the rank in the ordered
4
+ * gene list.
5
+ *
6
+ * - The top panel draws the running score of each set, a line at zero, and a dashed mark from zero to the
7
+ * maximum deviation of each set, which is the enrichment score of the set.
8
+ * - The middle panel draws one thin tick at each hit of each set, one row for each set, in the color of the
9
+ * set.
10
+ * - The bottom panel draws the ranked metric as a gray area, with a line at zero.
11
+ *
12
+ * The panel heights keep the ratio 1.5 : 0.5 : 1 of `gseaplot2`. The statistics print inside the top panel,
13
+ * in the corner that the curve of the first set leaves empty.
14
+ *
15
+ * The table holds one row for each rank of each set, thus a whole ranked list gives many thousand rows. The
16
+ * figure adds panels around the curve, thus it keeps its rows inline, and it thins the vertices that it draws
17
+ * (`drawnVertices`). The thinning keeps each vertex where the shape of a curve changes, thus the drawn line
18
+ * follows the table.
19
+ */
20
+ import { err, ok } from "neverthrow";
21
+ import { channelColumn, channelOrder, channelTransform } from "../../contracts/report-blocks.js";
22
+ import { TOOLBOX_BAND_PX } from "../chart-toolbox.js";
23
+ import { CHART_BODY_PX, GUIDE_LINE_WIDTH_PX, MUTED_CHART_COLOR } from "../design.js";
24
+ import { categoricalPalette, categoryName, chartProblem, columnPresent, firstAppearance, guideLine, guideMarkLine, statisticsGraphic, toNumber, valueAxis, valueAxisTitle, } from "./common.js";
25
+ /**
26
+ * The count of vertices that the thinning aims at for one curve, before it adds the vertices of shape.
27
+ *
28
+ * A wide page draws the rank axis over about one thousand pixels, thus one vertex for each pixel keeps the
29
+ * drawn line as the full table draws it. Three sets and the metric then stay under the coordinate bound of the
30
+ * SVG export.
31
+ */
32
+ export const GSEA_STRIDE_TARGET = 1000;
33
+ /** The titles of the figure where the binding declares no label: the three titles of `gseaplot2`. */
34
+ const RANK_TITLE = "Rank in ordered dataset";
35
+ const SCORE_TITLE = "Enrichment score";
36
+ const METRIC_TITLE = "Ranked metric";
37
+ /**
38
+ * The layout of the three panels, in percent of the chart.
39
+ *
40
+ * The three grids share one left edge and one right edge, thus the three panels align on the rank axis. The
41
+ * left band holds the tick labels and the turned title of the two value axes. The bottom band holds the tick
42
+ * labels and the title of the rank axis, and the legend under them where the figure draws more than one set.
43
+ * The top band is the band of the toolbox on the page body, because the statistics text sits at the top right
44
+ * corner of the top panel. The graphic of a media rule draws from the rule alone and an export scales only the
45
+ * text of the base graphic, thus an export keeps the same top band.
46
+ */
47
+ const GRID_LEFT = 12;
48
+ const GRID_RIGHT = 4;
49
+ const GRID_TOP = (100 * TOOLBOX_BAND_PX) / CHART_BODY_PX;
50
+ const AXIS_BAND = 15;
51
+ const PANEL_GAP = 2;
52
+ /**
53
+ * The legend lists one set on each line, under the rank axis. A set name is often long, and a column export
54
+ * cannot measure it, thus a row of names would wrap into the panels. One set on each line takes one line of
55
+ * height whatever its width, thus the band holds a fixed share for each set.
56
+ */
57
+ const LEGEND_LINE = 5.5;
58
+ const LEGEND_GAP_PX = 4;
59
+ /** The relative heights of the top, the middle, and the bottom panel, as `gseaplot2` gives them. */
60
+ const PANEL_SHARES = [1.5, 0.5, 1];
61
+ /** The inset of the statistics text from the edges of the top panel, in percent of the chart. */
62
+ const STATISTICS_INSET = 2;
63
+ /** The share of one row of the middle panel that each tick leaves free over it and under it. */
64
+ const TICK_INSET = 0.1;
65
+ /** The stroke widths of the running score, of a hit tick, and of the edge of the metric area, in pixels. */
66
+ const SCORE_LINE_PX = 1.5;
67
+ const TICK_LINE_PX = 1;
68
+ const METRIC_LINE_PX = 0.5;
69
+ /** The members of a gsea block that the figure reads. */
70
+ const GSEA_READS = new Set(["x", "y", "group", "hit", "metric", "statistics"]);
71
+ /** The channels that the figure demands, and the one that it reads where the block names it. */
72
+ const DEMANDED_CHANNELS = ["x", "y", "hit", "metric"];
73
+ const PLAIN_CHANNELS = ["x", "y", "group", "hit", "metric"];
74
+ /** The GSEA running enrichment score figure. */
75
+ export const GSEA_FIGURE = { reads: GSEA_READS, derive: deriveGsea };
76
+ /** Derive the three panels of one gsea block. */
77
+ function deriveGsea(block, rows, context) {
78
+ const columns = gseaColumns(block.encoding ?? {}, rows, context);
79
+ if (columns.isErr())
80
+ return err(columns.error);
81
+ const table = gseaTable(rows, columns.value, context.blockId);
82
+ if (table.isErr())
83
+ return err(table.error);
84
+ const { sets, metric } = table.value;
85
+ const palette = categoricalPalette(sets.length);
86
+ const colorOf = (index) => palette[index % palette.length];
87
+ const legendLines = sets.length > 1 ? sets.length : 0;
88
+ const grids = panelGrids(legendLines);
89
+ const range = rankRange(sets, metric);
90
+ const metricTitle = valueAxisTitle(context.labels, columns.value.metric, METRIC_TITLE);
91
+ const deviations = sets.map((set) => maximumDeviation(set.score));
92
+ const top = sets.map((set, index) => scoreSeries(set, index, colorOf(index), deviations[index]));
93
+ const middle = sets.map((set, index) => tickSeries(set, index, colorOf(index)));
94
+ const bottom = metricSeries(metric, metricTitle);
95
+ const hiddenRankAxis = (gridIndex) => ({
96
+ type: "value",
97
+ gridIndex,
98
+ ...range,
99
+ axisLabel: { show: false },
100
+ axisTick: { show: false },
101
+ axisLine: { show: false },
102
+ });
103
+ const firstDeviation = deviations[0];
104
+ const corner = firstDeviation === undefined || firstDeviation.value >= 0 ? "top-right" : "bottom-left";
105
+ return ok({
106
+ tooltip: { trigger: "axis" },
107
+ axisPointer: { link: [{ xAxisIndex: "all" }] },
108
+ grid: grids,
109
+ xAxis: [
110
+ hiddenRankAxis(0),
111
+ hiddenRankAxis(1),
112
+ { ...valueAxis("x", valueAxisTitle(context.labels, columns.value.rank, RANK_TITLE), range), gridIndex: 2 },
113
+ ],
114
+ yAxis: [
115
+ { ...valueAxis("y", valueAxisTitle(context.labels, columns.value.score, SCORE_TITLE)), gridIndex: 0 },
116
+ { type: "value", gridIndex: 1, min: 0, max: Math.max(1, sets.length), inverse: true, show: false },
117
+ { ...valueAxis("y", metricTitle), gridIndex: 2, splitNumber: 3 },
118
+ ],
119
+ series: [...top, ...middle, bottom],
120
+ graphic: statisticsText(context, grids[0], corner),
121
+ legend: legendLines > 0
122
+ ? { data: sets.map((set) => set.name), orient: "vertical", left: percent(GRID_LEFT), bottom: 0, itemGap: LEGEND_GAP_PX }
123
+ : { show: false },
124
+ });
125
+ }
126
+ /**
127
+ * The columns of the channels of one block. A demanded channel that the block omits refuses, and so does a
128
+ * channel with a transform or an order, because the figure reads each channel as it stands in the table.
129
+ */
130
+ function gseaColumns(encoding, rows, context) {
131
+ for (const channel of DEMANDED_CHANNELS) {
132
+ if (encoding[channel] === undefined) {
133
+ return err(chartProblem(context.blockId, `The gsea figure needs a column for the "${channel}" channel.`));
134
+ }
135
+ }
136
+ for (const channel of PLAIN_CHANNELS) {
137
+ const declared = encoding[channel];
138
+ if (declared === undefined)
139
+ continue;
140
+ if (channelTransform(declared) !== undefined || channelOrder(declared) !== undefined) {
141
+ return err(chartProblem(context.blockId, `The gsea figure reads the "${channel}" channel as a plain column, thus it takes no transform and no order.`));
142
+ }
143
+ const column = channelColumn(declared);
144
+ if (rows.length > 0 && !columnPresent(column, rows, context.columns)) {
145
+ return err(chartProblem(context.blockId, `The column "${column}" is absent from every row.`));
146
+ }
147
+ }
148
+ const column = (channel) => {
149
+ const declared = encoding[channel];
150
+ return declared === undefined ? "" : channelColumn(declared);
151
+ };
152
+ return ok({
153
+ rank: column("x"),
154
+ score: column("y"),
155
+ hit: column("hit"),
156
+ metric: column("metric"),
157
+ ...(encoding.group !== undefined ? { set: channelColumn(encoding.group) } : {}),
158
+ });
159
+ }
160
+ /**
161
+ * The sets and the shared metric of one table.
162
+ *
163
+ * A row whose rank is not numeric draws nothing, and a row whose score or metric is not numeric draws no
164
+ * vertex on that curve. A hit is 1 or 0, and every other hit cell refuses, because a guess would move a tick.
165
+ * The sets of one figure share one ranked list, thus two metric values at one rank refuse. A set holds one row
166
+ * for each rank, thus a second row at one rank of one set refuses.
167
+ */
168
+ function gseaTable(rows, columns, blockId) {
169
+ const keys = firstAppearance(rows.map((row) => setKey(row, columns)));
170
+ const sets = new Map();
171
+ for (const key of keys) {
172
+ sets.set(key, { key, name: categoryName(key), score: [], hits: [], ranks: new Set() });
173
+ }
174
+ const metric = new Map();
175
+ for (const row of rows) {
176
+ const rank = toNumber(row[columns.rank]);
177
+ if (rank === null)
178
+ continue;
179
+ const set = sets.get(setKey(row, columns));
180
+ if (set === undefined)
181
+ continue;
182
+ if (set.ranks.has(rank)) {
183
+ return err(chartProblem(blockId, `The set "${set.name}" holds two rows at rank ${rank}. A set holds one row for each rank.`));
184
+ }
185
+ set.ranks.add(rank);
186
+ const hit = hitOf(row[columns.hit]);
187
+ if (hit === undefined) {
188
+ return err(chartProblem(blockId, `The "hit" column "${columns.hit}" holds "${String(row[columns.hit] ?? "")}" at rank ${rank}. A hit is 1 or 0.`));
189
+ }
190
+ if (hit)
191
+ set.hits.push(rank);
192
+ const score = toNumber(row[columns.score]);
193
+ if (score !== null)
194
+ set.score.push({ rank, value: score });
195
+ const value = toNumber(row[columns.metric]);
196
+ if (value === null)
197
+ continue;
198
+ const known = metric.get(rank);
199
+ if (known !== undefined && known !== value) {
200
+ return err(chartProblem(blockId, `The "metric" column "${columns.metric}" holds two values at rank ${rank}. The sets of one figure share one ranked list, thus each rank holds one metric value.`));
201
+ }
202
+ metric.set(rank, value);
203
+ }
204
+ const byRank = (a, b) => a.rank - b.rank;
205
+ return ok({
206
+ sets: [...sets.values()].map(({ key, name, score, hits }) => ({ key, name, score: score.sort(byRank), hits: hits.sort((a, b) => a - b) })),
207
+ metric: [...metric.entries()].map(([rank, value]) => ({ rank, value })).sort(byRank),
208
+ });
209
+ }
210
+ /** The set of one row. A block with no set column draws one set. */
211
+ function setKey(row, columns) {
212
+ return columns.set === undefined ? "" : String(row[columns.set] ?? "");
213
+ }
214
+ /** The hit of one cell: true for 1, false for 0, and `undefined` for every other cell. */
215
+ function hitOf(cell) {
216
+ const value = toNumber(cell);
217
+ if (value === 1)
218
+ return true;
219
+ if (value === 0)
220
+ return false;
221
+ return undefined;
222
+ }
223
+ /** The range of the rank axis: the smallest and the largest rank of the table, which the three panels share. */
224
+ function rankRange(sets, metric) {
225
+ let min = Number.POSITIVE_INFINITY;
226
+ let max = Number.NEGATIVE_INFINITY;
227
+ const visit = (rank) => {
228
+ if (rank < min)
229
+ min = rank;
230
+ if (rank > max)
231
+ max = rank;
232
+ };
233
+ for (const set of sets) {
234
+ for (const vertex of set.score)
235
+ visit(vertex.rank);
236
+ for (const rank of set.hits)
237
+ visit(rank);
238
+ }
239
+ for (const vertex of metric)
240
+ visit(vertex.rank);
241
+ return Number.isFinite(min) ? { min, max } : {};
242
+ }
243
+ /**
244
+ * The vertex of the largest distance from zero, which is the enrichment score of the set. The first vertex
245
+ * of that distance answers, thus a tie gives one answer.
246
+ */
247
+ function maximumDeviation(score) {
248
+ let peak;
249
+ for (const vertex of score) {
250
+ if (peak === undefined || Math.abs(vertex.value) > Math.abs(peak.value))
251
+ peak = vertex;
252
+ }
253
+ return peak;
254
+ }
255
+ /**
256
+ * The vertices of one curve that the figure draws.
257
+ *
258
+ * A curve of `GSEA_STRIDE_TARGET` vertices or fewer draws each one. A longer curve keeps these vertices, and
259
+ * it drops each other one:
260
+ *
261
+ * - the first and the last vertex
262
+ * - each vertex at a stride of `ceil(n / GSEA_STRIDE_TARGET)` places
263
+ * - each turn, where the curve changes from a rise to a fall or from a fall to a rise
264
+ * - each vertex beside a crossing of zero
265
+ * - each vertex at a rank of `kept`: each hit and the maximum deviation of a running score.
266
+ *
267
+ * A running score falls in a straight line between two hits and rises at each hit, thus the turns and the
268
+ * hits hold each corner of the curve, and the line between two kept vertices covers the dropped ones.
269
+ */
270
+ function drawnVertices(curve, kept) {
271
+ const count = curve.length;
272
+ if (count <= GSEA_STRIDE_TARGET)
273
+ return [...curve];
274
+ const stride = Math.ceil(count / GSEA_STRIDE_TARGET);
275
+ return curve.filter((vertex, index) => index === 0 || index === count - 1 || index % stride === 0 || kept.has(vertex.rank) || turns(curve, index) || crossesZero(curve, index));
276
+ }
277
+ /** True when the curve turns at one inner vertex. */
278
+ function turns(curve, index) {
279
+ const before = curve[index].value - curve[index - 1].value;
280
+ const after = curve[index + 1].value - curve[index].value;
281
+ return before * after < 0;
282
+ }
283
+ /** True when the sign of the curve changes between one inner vertex and a neighbor. */
284
+ function crossesZero(curve, index) {
285
+ const sign = Math.sign(curve[index].value);
286
+ return sign !== Math.sign(curve[index - 1].value) || sign !== Math.sign(curve[index + 1].value);
287
+ }
288
+ /**
289
+ * The running score of one set in the top panel, with its dashed mark from zero to its maximum deviation. The
290
+ * first set also carries the line at zero.
291
+ */
292
+ function scoreSeries(set, index, color, deviation) {
293
+ const kept = new Set(set.hits);
294
+ if (deviation !== undefined)
295
+ kept.add(deviation.rank);
296
+ const zero = index === 0 ? [guideLine("y", 0)] : [];
297
+ const mark = deviation === undefined
298
+ ? []
299
+ : [
300
+ [
301
+ { coord: [deviation.rank, 0], lineStyle: { color, type: "dashed", width: GUIDE_LINE_WIDTH_PX }, label: { show: false } },
302
+ { coord: [deviation.rank, deviation.value] },
303
+ ],
304
+ ];
305
+ return {
306
+ type: "line",
307
+ name: set.name,
308
+ xAxisIndex: 0,
309
+ yAxisIndex: 0,
310
+ showSymbol: false,
311
+ symbol: "none",
312
+ itemStyle: { color },
313
+ lineStyle: { color, width: SCORE_LINE_PX },
314
+ emphasis: { disabled: true },
315
+ data: drawnVertices(set.score, kept).map((vertex) => [vertex.rank, vertex.value]),
316
+ markLine: { ...guideMarkLine(zero), data: [...zero, ...mark] },
317
+ };
318
+ }
319
+ /**
320
+ * The hit ticks of one set in the middle panel: one vertical segment at each hit, inside the row of the set.
321
+ *
322
+ * The segments share one line series, and an empty item between two segments breaks the line. Thus each tick
323
+ * stands alone, and it scales with the panel on the page and in the export.
324
+ */
325
+ function tickSeries(set, index, color) {
326
+ return {
327
+ type: "line",
328
+ name: set.name,
329
+ xAxisIndex: 1,
330
+ yAxisIndex: 1,
331
+ silent: true,
332
+ tooltip: { show: false },
333
+ showSymbol: false,
334
+ symbol: "none",
335
+ connectNulls: false,
336
+ itemStyle: { color },
337
+ lineStyle: { color, width: TICK_LINE_PX },
338
+ emphasis: { disabled: true },
339
+ data: set.hits.flatMap((rank) => [
340
+ [rank, index + TICK_INSET],
341
+ [rank, index + 1 - TICK_INSET],
342
+ [rank, "-"],
343
+ ]),
344
+ };
345
+ }
346
+ /** The ranked metric in the bottom panel: a gray area from zero, and the line at zero. */
347
+ function metricSeries(metric, title) {
348
+ return {
349
+ type: "line",
350
+ name: title,
351
+ xAxisIndex: 2,
352
+ yAxisIndex: 2,
353
+ showSymbol: false,
354
+ symbol: "none",
355
+ itemStyle: { color: MUTED_CHART_COLOR },
356
+ lineStyle: { color: MUTED_CHART_COLOR, width: METRIC_LINE_PX },
357
+ areaStyle: { color: MUTED_CHART_COLOR, opacity: 1 },
358
+ emphasis: { disabled: true },
359
+ data: drawnVertices(metric, new Set()).map((vertex) => [vertex.rank, vertex.value]),
360
+ markLine: guideMarkLine([guideLine("y", 0)]),
361
+ };
362
+ }
363
+ /** The three grids: one left edge and one right edge, and the heights in the shares of `gseaplot2`. */
364
+ function panelGrids(legendLines) {
365
+ const shares = PANEL_SHARES[0] + PANEL_SHARES[1] + PANEL_SHARES[2];
366
+ const legendBand = legendLines > 0 ? 1 + legendLines * LEGEND_LINE : 0;
367
+ const room = 100 - GRID_TOP - AXIS_BAND - legendBand - PANEL_GAP * (PANEL_SHARES.length - 1);
368
+ const grids = [];
369
+ let top = GRID_TOP;
370
+ for (const share of PANEL_SHARES) {
371
+ const height = (room * share) / shares;
372
+ grids.push({ left: percent(GRID_LEFT), right: percent(GRID_RIGHT), top: percent(top), height: percent(height) });
373
+ top += height + PANEL_GAP;
374
+ }
375
+ return grids;
376
+ }
377
+ /**
378
+ * The statistics text inside the top panel. A set that peaks above zero leaves the top right of the panel
379
+ * empty, and a set that dips under zero leaves the bottom left empty.
380
+ */
381
+ function statisticsText(context, panel, corner) {
382
+ const top = Number.parseFloat(String(panel.top));
383
+ const height = Number.parseFloat(String(panel.height));
384
+ return statisticsGraphic(context.statistics, corner).map((element) => corner === "top-right"
385
+ ? { ...element, right: percent(GRID_RIGHT + STATISTICS_INSET), top: percent(top + STATISTICS_INSET) }
386
+ : { ...element, left: percent(GRID_LEFT + STATISTICS_INSET), bottom: percent(100 - top - height + STATISTICS_INSET) });
387
+ }
388
+ /** One layout percentage, rounded, thus a float residue of the layout never reaches the option. */
389
+ function percent(value) {
390
+ return `${Math.round(value * 1e4) / 1e4}%`;
391
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The heatmap: a matrix of one value over the pairs of two category columns, with annotation strips.
3
+ *
4
+ * The canonical expression heatmap is the pheatmap and ComplexHeatmap figure: the genes down the y axis in the
5
+ * order of the run, the samples along x, a z-score on a diverging ramp symmetric at zero, and one strip of
6
+ * category colors over the matrix for each covariate of the samples, with a legend for each strip. The cells
7
+ * stand apart by a thin white gap, and the axes draw no line. A matrix of many samples hides its sample names,
8
+ * and the axis title states the count of samples in their place.
9
+ *
10
+ * A distance matrix draws on the sequential ramp, whose dark end is the low value. Thus the diagonal of zero
11
+ * distance draws dark, and two far samples draw light, as the DESeq2 sample-distance figure draws them.
12
+ *
13
+ * The run gives the order of each axis through the `orderBy` of its channel, or through the tree of the axis.
14
+ * A tree is the edge table of a clustering. The axis takes its depth-first leaf order, and the figure draws it
15
+ * as a dendrogram of elbows: the tree of x above the strips, and the tree of y at the left of the row names.
16
+ *
17
+ * A pair of categories holds one value. A repeated pair is a refusal, and never a silent sum. A track column
18
+ * holds one value for each x category, and a category with two values is a refusal.
19
+ *
20
+ * pheatmap names each row. Thus a matrix of more rows than the default body holds at one line of text each
21
+ * states a taller body, up to the largest body. A taller body grows the column exports in the same ratio.
22
+ * Past the largest body, and in a column export whose rows are shorter than a line of text, a row name that
23
+ * overlaps its neighbor hides.
24
+ */
25
+ import type { FigureModule } from "./index.js";
26
+ /** The count of x categories past which the matrix hides the x labels and states the count in the axis title. */
27
+ export declare const HEATMAP_LABEL_LIMIT = 40;
28
+ /** The white gap between two cells, in pixels. */
29
+ export declare const HEATMAP_CELL_GAP_PX = 1;
30
+ /** The height of one annotation strip, in pixels. */
31
+ export declare const HEATMAP_STRIP_PX = 12;
32
+ /** The height of one line of the strip legends, and the gap between the band of the legends and the strips, in pixels. */
33
+ export declare const TRACK_LEGEND_LINE_PX = 24;
34
+ export declare const TRACK_BAND_GAP_PX = 14;
35
+ /** The height of one matrix row on the page, in pixels: one line of the page text with a gap. */
36
+ export declare const HEATMAP_ROW_PX = 16;
37
+ /** The depth of the band of one dendrogram, in pixels: the height of the tree of x, and the width of the tree of y. */
38
+ export declare const DENDROGRAM_BAND_PX = 48;
39
+ /** The heatmap. */
40
+ export declare const HEATMAP_FIGURE: FigureModule;