@inflexa-ai/harness 0.42.2 → 0.43.1

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 +5 -3
  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,420 @@
1
+ /**
2
+ * The oncoprint: the alteration matrix of a cohort, genes by samples.
3
+ *
4
+ * The canonical design is the one of cBioPortal, ComplexHeatmap, and maftools. Each cell draws a gray ground,
5
+ * and an altered cell adds one glyph in the color of its class. A bar over the matrix draws the alteration
6
+ * count of each sample, stacked by class, and a bar at the right draws the share of altered samples of each
7
+ * gene with its percent text. A cohort holds too many samples for their names, thus the names hide and the
8
+ * axis title states the count.
9
+ *
10
+ * The figure computes two named summaries, and each one reads the rows of the bound table alone:
11
+ *
12
+ * - The alteration count of a sample: the count of its rows with a class, split by class.
13
+ * - The share of a gene: the count of samples with a class in that gene, over the count of every sample of
14
+ * the table. A row with an empty class names a sample with no alteration, thus that sample counts in the
15
+ * denominator and draws the ground alone.
16
+ *
17
+ * Each gene name and each percent text prints. Thus a figure of more genes than the default body holds at one
18
+ * line of text each states a taller body, and the grids keep their shares of it. A taller body grows the
19
+ * column exports in the same ratio.
20
+ */
21
+ import { err, ok } from "neverthrow";
22
+ import { channelColumn, channelOrder, channelTransform } from "../../contracts/report-blocks.js";
23
+ import { declaredForColumn } from "../../contracts/report-reference.js";
24
+ import { CELL_GLYPH_RENDERER } from "../chart-renderers.js";
25
+ import { CHART_BODY_MAX_PX, CHART_BODY_PX, CHART_EXPORT_SIZES, CHART_PAGE_TEXT_PX, CHART_PAGE_WIDTH_PX, CHART_SLOT_LIMIT, CHART_WIDE_PALETTE, MUTED_CHART_COLOR, } from "../design.js";
26
+ import { categoryAxis, categoryName, chartProblem, columnPresent, firstAppearance, legendLineCount, legendLinePx, orderQuickCategories, valueAxis, withFigureBody, } from "./common.js";
27
+ /**
28
+ * The color of each common alteration class of the MAF standard, in the order of the legend.
29
+ *
30
+ * The first seven classes take the Okabe-Ito hues, and `Multi_Hit` takes its black, as maftools draws it. The
31
+ * two rare classes take the first two hues of the wide palette past the Okabe-Ito set.
32
+ */
33
+ export const ALTERATION_CLASS_COLORS = {
34
+ Missense_Mutation: "#009e73",
35
+ Nonsense_Mutation: "#d55e00",
36
+ Frame_Shift_Del: "#0072b2",
37
+ Frame_Shift_Ins: "#cc79a7",
38
+ In_Frame_Del: "#e69f00",
39
+ In_Frame_Ins: "#56b4e9",
40
+ Splice_Site: "#f0e442",
41
+ Translation_Start_Site: "#875692",
42
+ Nonstop_Mutation: "#8db600",
43
+ Multi_Hit: "#000000",
44
+ };
45
+ /** The colors that an unknown class takes, in order: the wide palette less each color of the fixed map. */
46
+ const UNKNOWN_CLASS_COLORS = CHART_WIDE_PALETTE.filter((color) => !Object.values(ALTERATION_CLASS_COLORS).includes(color));
47
+ /** The gray ground of each cell. It is lighter than the muted color, thus a glyph of any class reads on it. */
48
+ export const CELL_GROUND_COLOR = "#e0e0e0";
49
+ /** The stack name of the count bars. Each class series names it, thus the classes of one sample stack. */
50
+ const COUNT_STACK = "alterations";
51
+ /** The title of the count axis over the matrix. */
52
+ const COUNT_TITLE = "Alterations";
53
+ /**
54
+ * The places of the three grids, in percent of the chart: the matrix, the count bar over it, and the share bar
55
+ * at its right. The count bar shares the left and the right edge of the matrix, and the share bar shares its
56
+ * top and its bottom, thus each bar lines up with its row or its column. The left band holds the gene names,
57
+ * and the band at the right of the share bar holds its percent text.
58
+ */
59
+ const MATRIX_TOP_PCT = 22;
60
+ const MATRIX_BOTTOM_PCT = 28;
61
+ const MATRIX_GRID = { left: "14%", right: "17%", top: `${MATRIX_TOP_PCT}%`, bottom: `${MATRIX_BOTTOM_PCT}%` };
62
+ /** The height of one gene row on the page, in pixels: one line of the page text with a gap. */
63
+ export const ONCOPRINT_ROW_PX = 16;
64
+ /** The height of one annotation strip under the matrix, and the gap between the matrix and the strips, in percent of the chart. */
65
+ const STRIP_BAND_PCT = 3.5;
66
+ const STRIP_GAP_PCT = 1;
67
+ const COUNT_GRID = { left: "14%", right: "17%", top: "5%", height: "14%" };
68
+ const SHARE_GRID = { left: "84%", right: "8%", top: "22%", bottom: "28%" };
69
+ /**
70
+ * The icon size and the gap of the class legend, in pixels. A cohort states some classes, and the smaller icon
71
+ * keeps the legend to few lines at the width of a single journal column.
72
+ */
73
+ const LEGEND_ICON_PX = 10;
74
+ const LEGEND_GAP_PX = 8;
75
+ /** The gap between the matrix and the sample title. The sample names hide, thus the title sits close to the cells. */
76
+ const SAMPLE_TITLE_GAP = 8;
77
+ /** The height of the sample title line, as a share of the text size. */
78
+ const TITLE_LINE_SHARE = 1.6;
79
+ /** The oncoprint module: the sample, the gene, the class, and the annotation tracks. */
80
+ export const ONCOPRINT_FIGURE = { reads: new Set(["x", "y", "value", "tracks"]), derive: deriveOncoprint };
81
+ function deriveOncoprint(block, rows, context) {
82
+ const sample = categoryChannel(block, rows, context, "x");
83
+ if (sample.isErr())
84
+ return err(sample.error);
85
+ const gene = categoryChannel(block, rows, context, "y");
86
+ if (gene.isErr())
87
+ return err(gene.error);
88
+ const klass = categoryChannel(block, rows, context, "value");
89
+ if (klass.isErr())
90
+ return err(klass.error);
91
+ const samples = orderQuickCategories(context.blockId, rows, sample.value.column, channelOrder(sample.value.channel));
92
+ if (samples.isErr())
93
+ return err(samples.error);
94
+ const genes = orderQuickCategories(context.blockId, rows, gene.value.column, channelOrder(gene.value.channel));
95
+ if (genes.isErr())
96
+ return err(genes.error);
97
+ const cellCount = genes.value.length * samples.value.length;
98
+ if (cellCount > CHART_SLOT_LIMIT) {
99
+ // The matrix draws one cell for each gene and each sample, whether or not a row names the pair.
100
+ return err(chartProblem(context.blockId, `The oncoprint holds ${genes.value.length} genes and ${samples.value.length} samples, thus ${cellCount} cells. A chart holds ${CHART_SLOT_LIMIT} slots at most, thus a table of fewer genes or fewer samples serves the reader.`));
101
+ }
102
+ const cells = classCells(context.blockId, rows, sample.value.column, gene.value.column, klass.value.column);
103
+ if (cells.isErr())
104
+ return err(cells.error);
105
+ const tracks = sampleTracks(context, rows, sample.value.column, block.encoding?.tracks ?? []);
106
+ if (tracks.isErr())
107
+ return err(tracks.error);
108
+ const classes = legendClasses(rows, klass.value.column);
109
+ const trackValues = firstAppearance(tracks.value.flatMap((track) => samples.value.flatMap((name) => track.values.get(String(name)) ?? [])));
110
+ const legendColors = classColors([...classes, ...trackValues]);
111
+ const colors = legendColors.slice(0, classes.length);
112
+ const places = new Map(classes.map((name, place) => [name, place]));
113
+ const data = [];
114
+ for (const [geneIndex, geneName] of genes.value.entries()) {
115
+ for (const [sampleIndex, sampleName] of samples.value.entries()) {
116
+ const name = cells.value.get(cellKey(sampleName, geneName));
117
+ if (name === undefined) {
118
+ data.push([sampleIndex, geneIndex, -1]);
119
+ continue;
120
+ }
121
+ data.push({ name: `${String(geneName)}, ${String(sampleName)}: ${categoryName(name)}`, value: [sampleIndex, geneIndex, places.get(name) ?? -1] });
122
+ }
123
+ }
124
+ const sampleTitle = `${categoryAxisName(context.labels, sample.value.column)} (n = ${samples.value.length})`;
125
+ const sampleAxis = { nameGap: SAMPLE_TITLE_GAP, axisLabel: { show: false }, axisTick: { show: false }, axisLine: { show: false } };
126
+ const stripBand = tracks.value.length * STRIP_BAND_PCT;
127
+ const stripShare = stripBand === 0 ? 0 : stripBand + STRIP_GAP_PCT;
128
+ const legendNames = [...classes, ...trackValues].map((name) => categoryName(name));
129
+ const band = legendBandPx(legendNames);
130
+ const body = (genes.value.length * ONCOPRINT_ROW_PX + band) / (1 - (MATRIX_TOP_PCT + stripShare) / 100);
131
+ const tall = body > CHART_BODY_PX;
132
+ const bottom = tall ? (band / Math.min(body, CHART_BODY_MAX_PX)) * 100 : MATRIX_BOTTOM_PCT;
133
+ const layout = { ...MATRIX_GRID, bottom: percentOf(bottom + stripShare) };
134
+ const strips = stripMembers(tracks.value, samples.value, trackValues, legendColors.slice(classes.length), stripBand, bottom, sampleTitle, context.labels);
135
+ const option = {
136
+ tooltip: { trigger: "item" },
137
+ grid: [layout, { ...COUNT_GRID }, { ...SHARE_GRID, bottom: layout.bottom }, ...strips.grid],
138
+ xAxis: [
139
+ { ...categoryAxis("x", samples.value, { title: strips.grid.length === 0 ? sampleTitle : undefined }), gridIndex: 0, ...sampleAxis },
140
+ { type: "category", gridIndex: 1, data: [...samples.value], axisLabel: { show: false }, axisTick: { show: false } },
141
+ { type: "value", gridIndex: 2, min: 0, max: 1, show: false },
142
+ ...strips.xAxis.map((axis) => ({ ...axis, ...sampleAxis })),
143
+ ],
144
+ yAxis: [
145
+ {
146
+ ...categoryAxis("y", genes.value, { topDown: true }),
147
+ gridIndex: 0,
148
+ axisTick: { show: false },
149
+ axisLine: { show: false },
150
+ // A single journal column gives a row less than one line of text. Each gene name that would
151
+ // cover its neighbor hides, and the share text of its row hides with it.
152
+ axisLabel: { hideOverlap: true },
153
+ },
154
+ { ...valueAxis("y", COUNT_TITLE), gridIndex: 1, minInterval: 1, splitNumber: 2 },
155
+ { type: "category", gridIndex: 2, inverse: true, data: [...genes.value], show: false },
156
+ ...strips.yAxis,
157
+ ],
158
+ series: [
159
+ {
160
+ type: "custom",
161
+ renderItem: CELL_GLYPH_RENDERER,
162
+ itemPayload: { colors, ground: CELL_GROUND_COLOR },
163
+ xAxisIndex: 0,
164
+ yAxisIndex: 0,
165
+ encode: { x: 0, y: 1 },
166
+ tooltip: { formatter: "{b}" },
167
+ data,
168
+ },
169
+ ...countSeries(rows, samples.value, sample.value.column, klass.value.column, classes, colors),
170
+ shareSeries(rows, samples.value, genes.value, sample.value.column, gene.value.column, klass.value.column),
171
+ ...strips.series,
172
+ ],
173
+ legend: {
174
+ // A taller body holds the legend lines of the single column export, thus the page legend starts
175
+ // under the sample title and the free part of the band sits under it.
176
+ ...(tall
177
+ ? {
178
+ top: percentOf(100 - bottom + ((CHART_PAGE_TEXT_PX * (1 + TITLE_LINE_SHARE) + SAMPLE_TITLE_GAP) / Math.min(body, CHART_BODY_MAX_PX)) * 100),
179
+ }
180
+ : { bottom: 0 }),
181
+ itemWidth: LEGEND_ICON_PX,
182
+ itemHeight: LEGEND_ICON_PX,
183
+ itemGap: LEGEND_GAP_PX,
184
+ data: legendNames,
185
+ },
186
+ };
187
+ return ok(tall ? withFigureBody(option, body) : option);
188
+ }
189
+ /**
190
+ * The band under the matrix and the strips of a taller body, in page pixels: the sample title, and the lines of
191
+ * the legend. The band holds the legend lines of the page and of the single column export, whose narrow width
192
+ * wraps the legend into more lines. A column export keeps the share of the band, thus the band of the column
193
+ * converts to page pixels in the ratio of the page body to the column height.
194
+ */
195
+ function legendBandPx(names) {
196
+ const bandAt = (widthPx, textPx) => legendLineCount(names, widthPx, textPx) * legendLinePx(textPx, LEGEND_GAP_PX) + textPx * (1 + TITLE_LINE_SHARE) + SAMPLE_TITLE_GAP;
197
+ const single = CHART_EXPORT_SIZES.single;
198
+ return Math.ceil(Math.max(bandAt(CHART_PAGE_WIDTH_PX, CHART_PAGE_TEXT_PX), (bandAt(single.widthPx, single.textPx) * CHART_BODY_PX) / single.heightPx));
199
+ }
200
+ /**
201
+ * The column of one category channel. An absent channel, a transform, and a column that no row holds each
202
+ * refuse, because the figure reads the cells of the column as names.
203
+ */
204
+ function categoryChannel(block, rows, context, name) {
205
+ const channel = block.encoding?.[name];
206
+ if (channel === undefined) {
207
+ return err(chartProblem(context.blockId, `The oncoprint figure needs a column for the "${name}" channel.`));
208
+ }
209
+ if (channelTransform(channel) !== undefined) {
210
+ return err(chartProblem(context.blockId, `The oncoprint reads the "${name}" column as categories, thus the channel takes no transform.`));
211
+ }
212
+ if (name === "value" && channelOrder(channel) !== undefined) {
213
+ return err(chartProblem(context.blockId, 'The "value" channel draws no category axis, thus it takes no "orderBy".'));
214
+ }
215
+ const column = channelColumn(channel);
216
+ if (rows.length > 0 && !columnPresent(column, rows, context.columns)) {
217
+ return err(chartProblem(context.blockId, `The column "${column}" is absent from every row.`));
218
+ }
219
+ return ok({ column, channel });
220
+ }
221
+ /** The class of one row, or `undefined` for a row that names a sample with no alteration. */
222
+ export function classOf(row, column) {
223
+ const cell = row[column];
224
+ if (cell === undefined)
225
+ return undefined;
226
+ const text = String(cell).trim();
227
+ return text === "" ? undefined : text;
228
+ }
229
+ /** The key of one cell of the matrix. Each cell keeps its type, thus the number `1` and the text `"1"` differ. */
230
+ function cellKey(sample, gene) {
231
+ return JSON.stringify([typeof sample, String(sample), typeof gene, String(gene)]);
232
+ }
233
+ /**
234
+ * The class of each altered cell. A cell that two rows name with a class has no one glyph, thus it refuses:
235
+ * the MAF standard states `Multi_Hit` for it.
236
+ */
237
+ function classCells(blockId, rows, sample, gene, klass) {
238
+ const cells = new Map();
239
+ for (const row of rows) {
240
+ const name = classOf(row, klass);
241
+ if (name === undefined)
242
+ continue;
243
+ const key = cellKey(row[sample], row[gene]);
244
+ if (cells.has(key)) {
245
+ return err(chartProblem(blockId, `The oncoprint holds the gene "${String(row[gene])}" and the sample "${String(row[sample])}" with two classes. One cell draws one class, thus the table states "Multi_Hit" for a gene with more than one class in a sample.`));
246
+ }
247
+ cells.set(key, name);
248
+ }
249
+ return ok(cells);
250
+ }
251
+ /** The classes of the table in the order of the legend: the common classes in their fixed order, then each other class as it first appears. */
252
+ export function legendClasses(rows, column) {
253
+ const present = new Set();
254
+ const unknown = [];
255
+ for (const row of rows) {
256
+ const name = classOf(row, column);
257
+ if (name === undefined || present.has(name))
258
+ continue;
259
+ present.add(name);
260
+ if (declaredForColumn(ALTERATION_CLASS_COLORS, name) === undefined)
261
+ unknown.push(name);
262
+ }
263
+ return [...Object.keys(ALTERATION_CLASS_COLORS).filter((name) => present.has(name)), ...unknown];
264
+ }
265
+ /** The color of each class in legend order. An unknown class takes the next color of its own list, and past that list the muted color. */
266
+ export function classColors(classes) {
267
+ let next = 0;
268
+ return classes.map((name) => {
269
+ const fixed = declaredForColumn(ALTERATION_CLASS_COLORS, name);
270
+ if (fixed !== undefined)
271
+ return fixed;
272
+ const color = UNKNOWN_CLASS_COLORS[next] ?? MUTED_CHART_COLOR;
273
+ next += 1;
274
+ return color;
275
+ });
276
+ }
277
+ /** The title of a category axis: the declared label of the column, else the column name as words. */
278
+ function categoryAxisName(labels, column) {
279
+ return declaredForColumn(labels, column) ?? categoryName(column);
280
+ }
281
+ /** The count bars over the matrix: one series for each class, stacked, with the count of the class in each sample. */
282
+ function countSeries(rows, samples, sample, klass, classes, colors) {
283
+ const places = new Map(samples.map((name, place) => [String(name), place]));
284
+ const counts = classes.map(() => samples.map(() => null));
285
+ const classPlaces = new Map(classes.map((name, place) => [name, place]));
286
+ for (const row of rows) {
287
+ const name = classOf(row, klass);
288
+ const place = places.get(String(row[sample]));
289
+ if (name === undefined || place === undefined)
290
+ continue;
291
+ const series = counts[classPlaces.get(name) ?? -1];
292
+ if (series !== undefined)
293
+ series[place] = (series[place] ?? 0) + 1;
294
+ }
295
+ return classes.map((name, index) => ({
296
+ type: "bar",
297
+ name: categoryName(name),
298
+ stack: COUNT_STACK,
299
+ xAxisIndex: 1,
300
+ yAxisIndex: 1,
301
+ barCategoryGap: "10%",
302
+ itemStyle: { color: colors[index] },
303
+ tooltip: { formatter: "{b}<br/>{a}: {c}" },
304
+ data: counts[index],
305
+ }));
306
+ }
307
+ /**
308
+ * The share bar at the right of the matrix: for each gene, the count of its altered samples over the count of
309
+ * every sample of the table, with its percent text at the end of the bar.
310
+ */
311
+ function shareSeries(rows, samples, genes, sample, gene, klass) {
312
+ const altered = new Map();
313
+ for (const row of rows) {
314
+ if (classOf(row, klass) === undefined)
315
+ continue;
316
+ const key = String(row[gene]);
317
+ const set = altered.get(key) ?? new Set();
318
+ set.add(String(row[sample]));
319
+ altered.set(key, set);
320
+ }
321
+ const total = samples.length;
322
+ return {
323
+ type: "bar",
324
+ xAxisIndex: 2,
325
+ yAxisIndex: 2,
326
+ silent: true,
327
+ barCategoryGap: "30%",
328
+ itemStyle: { color: MUTED_CHART_COLOR },
329
+ label: { show: true, position: "right" },
330
+ labelLayout: { hideOverlap: true },
331
+ data: genes.map((name) => {
332
+ const share = total === 0 ? 0 : (altered.get(String(name))?.size ?? 0) / total;
333
+ return { value: share, label: { formatter: percentText(share) } };
334
+ }),
335
+ };
336
+ }
337
+ /** The percent text of one share: a whole percent, and `<1%` for a share above zero that rounds to zero. */
338
+ function percentText(share) {
339
+ const percent = Math.round(share * 100);
340
+ return percent === 0 && share > 0 ? "<1%" : `${percent}%`;
341
+ }
342
+ /**
343
+ * The value of each sample in each track column. A track draws one value for each sample, thus a sample whose
344
+ * rows hold two values refuses. An empty cell gives no value, and its sample draws no strip cell.
345
+ */
346
+ function sampleTracks(context, rows, sample, columns) {
347
+ const tracks = [];
348
+ for (const column of columns) {
349
+ if (rows.length > 0 && !columnPresent(column, rows, context.columns)) {
350
+ return err(chartProblem(context.blockId, `The column "${column}" is absent from every row.`));
351
+ }
352
+ const values = new Map();
353
+ for (const row of rows) {
354
+ const cell = row[column];
355
+ const value = cell === undefined ? "" : String(cell).trim();
356
+ if (value === "")
357
+ continue;
358
+ const key = String(row[sample]);
359
+ const held = values.get(key);
360
+ if (held !== undefined && held !== value) {
361
+ return err(chartProblem(context.blockId, `The track column "${column}" holds two values for the sample "${key}". A track draws one value for each sample.`));
362
+ }
363
+ values.set(key, value);
364
+ }
365
+ tracks.push({ column, values });
366
+ }
367
+ return ok(tracks);
368
+ }
369
+ /**
370
+ * The annotation strips under the matrix: one row of cells for each track, in one grid that shares the sample
371
+ * axis of the matrix. Each value of a track draws its cells through the cell-glyph renderer as a ground of the
372
+ * value color alone, thus one series of each value names its legend entry. The sample title sits under the
373
+ * strips.
374
+ */
375
+ function stripMembers(tracks, samples, values, colors, band, bottom, sampleTitle, labels) {
376
+ if (tracks.length === 0)
377
+ return { grid: [], xAxis: [], yAxis: [], series: [] };
378
+ const cellsOf = new Map(values.map((value) => [value, []]));
379
+ for (const [trackPlace, track] of tracks.entries()) {
380
+ for (const [samplePlace, name] of samples.entries()) {
381
+ const value = track.values.get(String(name));
382
+ if (value !== undefined)
383
+ cellsOf.get(value)?.push([samplePlace, trackPlace, -1]);
384
+ }
385
+ }
386
+ const series = [];
387
+ for (const [valuePlace, value] of values.entries()) {
388
+ const data = cellsOf.get(value) ?? [];
389
+ series.push({
390
+ type: "custom",
391
+ name: categoryName(value),
392
+ renderItem: CELL_GLYPH_RENDERER,
393
+ itemPayload: { colors: [], ground: colors[valuePlace] },
394
+ // The legend reads the color of the series, and the renderer reads the ground.
395
+ itemStyle: { color: colors[valuePlace] },
396
+ xAxisIndex: 3,
397
+ yAxisIndex: 3,
398
+ encode: { x: 0, y: 1 },
399
+ tooltip: { formatter: "{a}" },
400
+ data,
401
+ });
402
+ }
403
+ return {
404
+ grid: [{ left: MATRIX_GRID.left, right: MATRIX_GRID.right, bottom: percentOf(bottom), height: percentOf(band) }],
405
+ xAxis: [{ ...categoryAxis("x", samples, { title: sampleTitle }), gridIndex: 3 }],
406
+ yAxis: [
407
+ {
408
+ ...categoryAxis("y", tracks.map((track) => categoryAxisName(labels, track.column)), { topDown: true }),
409
+ gridIndex: 3,
410
+ axisTick: { show: false },
411
+ axisLine: { show: false },
412
+ },
413
+ ],
414
+ series,
415
+ };
416
+ }
417
+ /** One layout percentage, rounded, thus a float residue never reaches the option. */
418
+ function percentOf(value) {
419
+ return `${Math.round(value * 1e4) / 1e4}%`;
420
+ }
@@ -0,0 +1,37 @@
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 type { FigureModule } from "./index.js";
19
+ /**
20
+ * The symbol of each shape category, in order: the circle, the triangle, the square, the diamond, the
21
+ * inverted triangle, and the cross. Each one keeps a clear outline at a small size, and past six a reader
22
+ * no longer tells the symbols apart.
23
+ */
24
+ export declare const SHAPE_SYMBOLS: readonly ["circle", "triangle", "rect", "diamond", "path://M0,0L10,0L5,10Z", "path://M3.5,0H6.5V3.5H10V6.5H6.5V10H3.5V6.5H0V3.5H3.5Z"];
25
+ /** 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. */
26
+ export declare const SHAPED_POINT_PX = 10;
27
+ /** The largest row count of a PCA whose points each carry their name. The DESeq2 workflow labels a small study. */
28
+ export declare const PCA_LABEL_ROWS = 20;
29
+ /** The PCA of samples. */
30
+ export declare const PCA_FIGURE: FigureModule;
31
+ /**
32
+ * The plain scatter with a `shape` channel.
33
+ *
34
+ * The module reads every member of a scatter, thus the refusal of a member that the shape draw cannot draw
35
+ * names the shape as the cause, and never states that a scatter reads no such member.
36
+ */
37
+ export declare const SHAPED_SCATTER_FIGURE: FigureModule;