@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,406 @@
1
+ /**
2
+ * The Kaplan-Meier figure: one step curve for each group, the confidence band as a step band, a tick at each
3
+ * censored row, the number-at-risk table under the plot, the median lines, and the statistics text.
4
+ *
5
+ * The figure plots the precomputed survival of the table, and it fits no curve. It computes two lookups from
6
+ * the rows, and each one reads the rows of one group alone:
7
+ *
8
+ * - The number at risk at each tick of the time axis is the risk of the first row at or after the tick, and 0
9
+ * past the last row of the group. A risk table that does not sit at the axis ticks misleads, thus the figure
10
+ * sets the ticks itself and reads the table at them.
11
+ * - The median of a group is the time of the first row whose survival is at or under 0.5. A curve that never
12
+ * reaches 0.5 has no median, and it takes no median line.
13
+ */
14
+ import { err, ok } from "neverthrow";
15
+ import { CHART_BODY_MAX_PX, CHART_BODY_PX, CHART_FONT_STACK, CHART_INK, CHART_PAGE_TEXT_PX } from "../design.js";
16
+ import { categoricalPalette, categoryAxis, categoryName, chartProblem, demandedColumn, firstAppearance, GUIDE_LINE_STYLE, plainColumn, statisticsGraphic, toNumber, valueAxis, valueAxisTitle, withFigureBody, } from "./common.js";
17
+ /** The members of a km block that the figure reads. */
18
+ const KM_READS = new Set(["x", "y", "group", "low", "high", "censor", "risk", "statistics"]);
19
+ /** The Kaplan-Meier survival figure. */
20
+ export const KM_FIGURE = { reads: KM_READS, derive: deriveKm };
21
+ /** The survival at which the median line sits. */
22
+ const MEDIAN_SURVIVAL = 0.5;
23
+ /**
24
+ * The most ticks of the time axis. The step of the axis is the smallest nice step at or over the last time over
25
+ * `MAX_TIME_TICKS - 1`, thus the span from 0 to the last time holds five steps at most.
26
+ */
27
+ const MAX_TIME_TICKS = 6;
28
+ /** The nice steps of the time axis in one decade. */
29
+ const NICE_STEPS = [1, 2, 2.5, 5];
30
+ /**
31
+ * The tolerance, in steps, under which the end of the time axis counts as a tick. A product such as `3 × 0.2`
32
+ * gives `0.6000000000000001`, one unit in the last place past the end `0.6`.
33
+ */
34
+ const TICK_TOLERANCE = 1e-9;
35
+ /** The significant digits of a tick, thus a tick such as `3 × 0.2` reads `0.6`. */
36
+ const TICK_DIGITS = 12;
37
+ /** The opacity of a confidence band. The curve and the band of the other group stay readable through it. */
38
+ const BAND_OPACITY = 0.2;
39
+ /** The stroke width of a survival curve, in pixels. */
40
+ const CURVE_WIDTH_PX = 1.5;
41
+ /** The size of a censor tick in pixels: a thin upright bar across the curve. */
42
+ const CENSOR_TICK_PX = [1.5, 8];
43
+ /**
44
+ * The layout of the plot and of the number-at-risk table, in percent of the chart.
45
+ *
46
+ * The two grids share one left edge and one right edge, thus each count sits under its tick. The axis band
47
+ * under the plot holds the tick labels and the title of the time axis, and the header band holds the title of
48
+ * the table. Each group takes one row of the table. A table of more rows than the default body holds grows the
49
+ * body.
50
+ */
51
+ const PLOT_TOP = 12;
52
+ const GRID_RIGHT = 5;
53
+ const AXIS_BAND = 14;
54
+ const HEADER_BAND = 7;
55
+ const RISK_ROW = 6;
56
+ const TABLE_BOTTOM = 3;
57
+ const RISK_ROWS_IN_BODY = 3;
58
+ /** The left inset of the table title, in percent of the chart. The title sits over the group names. */
59
+ const HEADER_LEFT = 1;
60
+ /** The title of the number-at-risk table. */
61
+ const TABLE_TITLE = "Number at risk";
62
+ /**
63
+ * The gap in pixels between a group name and the plot edge of the table. A count at the first tick centers on
64
+ * the edge, thus the gap holds half of a count of three digits.
65
+ */
66
+ const TABLE_NAME_GAP_PX = 16;
67
+ /**
68
+ * The left edge of both grids, in percent of the chart: the least edge, the most edge, the pad, and the share
69
+ * of one character of a group name.
70
+ *
71
+ * The share and the pad read the single journal column, the narrowest place that the figure draws in: one
72
+ * character of the export text, and the name gap with a margin. Thus the longest group name of the table fits
73
+ * left of the plot there.
74
+ */
75
+ const GRID_LEFT_MIN = 10;
76
+ const GRID_LEFT_MAX = 30;
77
+ const GRID_LEFT_PAD = 7;
78
+ const NAME_CHARACTER_PCT = 1.6;
79
+ /** Derive the figure of one km block. */
80
+ function deriveKm(block, rows, context) {
81
+ const columns = kmColumns(block, rows, context);
82
+ if (columns.isErr())
83
+ return err(columns.error);
84
+ const curves = kmCurves(rows, columns.value, context.blockId);
85
+ if (curves.isErr())
86
+ return err(curves.error);
87
+ const palette = categoricalPalette(curves.value.length);
88
+ const colorOf = (index) => palette[index % palette.length];
89
+ const end = timeEnd(curves.value);
90
+ const step = timeStep(end);
91
+ const ticks = timeTicks(end, step);
92
+ const table = columns.value.risk !== undefined;
93
+ const left = gridLeft(curves.value);
94
+ const layout = kmLayout(table ? curves.value.length : 0);
95
+ const legend = curves.value.length > 1;
96
+ const series = [];
97
+ for (const [index, curve] of curves.value.entries()) {
98
+ series.push(...curveSeries(curve, index, colorOf(index), columns.value));
99
+ }
100
+ const median = medianSeries(curves.value);
101
+ if (median !== undefined)
102
+ series.push(median);
103
+ if (table) {
104
+ for (const [index, curve] of curves.value.entries())
105
+ series.push(riskSeries(curve, index, ticks));
106
+ }
107
+ // The time axis ends at the last time of the table. A label at an end that is no tick crowds the last tick, thus it hides.
108
+ const timeAxis = {
109
+ ...valueAxis("x", valueAxisTitle(context.labels, columns.value.time), { min: 0, max: end }),
110
+ interval: step,
111
+ ...(onTick(end, step) ? {} : { axisLabel: { showMaxLabel: false } }),
112
+ };
113
+ const survivalAxis = valueAxis("y", valueAxisTitle(context.labels, columns.value.survival), { min: 0, max: 1 });
114
+ const plotGrid = { top: pct(layout.top), bottom: pct(layout.plotBottom), left: pct(left), right: pct(GRID_RIGHT) };
115
+ // The statistics text sits at the top of the plot, and the top of the plot moves with the body.
116
+ const statistics = statisticsGraphic(context.statistics, "top-right").map((text) => ({ ...text, top: pct(layout.top) }));
117
+ const graphic = [...statistics, ...(table ? [tableTitle(layout)] : [])];
118
+ const option = {
119
+ legend: legend ? { top: 0 } : { show: false },
120
+ grid: table ? [plotGrid, tableGrid(layout, left)] : plotGrid,
121
+ xAxis: table ? [timeAxis, tableTimeAxis(end, step)] : timeAxis,
122
+ yAxis: table ? [survivalAxis, tableGroupAxis(curves.value, colorOf)] : survivalAxis,
123
+ series,
124
+ ...(graphic.length > 0 ? { graphic } : {}),
125
+ };
126
+ return ok(layout.bodyPx > CHART_BODY_PX ? withFigureBody(option, layout.bodyPx) : option);
127
+ }
128
+ /** A number in percent of the chart, rounded, thus a float residue never reaches the option. */
129
+ function pct(value) {
130
+ return `${Math.round(value * 1e4) / 1e4}%`;
131
+ }
132
+ /**
133
+ * The bands of a chart whose table holds some rows. A table past `RISK_ROWS_IN_BODY` rows grows the body by one
134
+ * row of the default body for each further row, and each band keeps its height in pixels. The body stops at the
135
+ * largest body, and past it the plot keeps its height and the rows share the rest of the table band.
136
+ */
137
+ function kmLayout(rows) {
138
+ const grown = Math.max(0, rows - RISK_ROWS_IN_BODY) * RISK_ROW;
139
+ const bodyPx = Math.min(CHART_BODY_MAX_PX, (CHART_BODY_PX * (100 + grown)) / 100);
140
+ const share = CHART_BODY_PX / bodyPx;
141
+ const fixed = rows > 0 ? PLOT_TOP + AXIS_BAND + HEADER_BAND + TABLE_BOTTOM : PLOT_TOP + AXIS_BAND;
142
+ const plot = (100 - fixed - Math.min(rows, RISK_ROWS_IN_BODY) * RISK_ROW) * share;
143
+ return {
144
+ top: PLOT_TOP * share,
145
+ plotBottom: 100 - PLOT_TOP * share - plot,
146
+ tableBottom: TABLE_BOTTOM * share,
147
+ rows: 100 - fixed * share - plot,
148
+ header: HEADER_BAND * share,
149
+ bodyPx,
150
+ };
151
+ }
152
+ /** The columns of one km block. The time and the survival are demanded, and each other channel is optional. */
153
+ function kmColumns(block, rows, context) {
154
+ const encoding = block.encoding ?? {};
155
+ const source = { blockId: context.blockId, rows, columns: context.columns };
156
+ const time = demandedColumn("km", encoding, "x", source);
157
+ if (time.isErr())
158
+ return err(time.error);
159
+ const survival = demandedColumn("km", encoding, "y", source);
160
+ if (survival.isErr())
161
+ return err(survival.error);
162
+ const optional = {
163
+ group: undefined,
164
+ low: undefined,
165
+ high: undefined,
166
+ censor: undefined,
167
+ risk: undefined,
168
+ };
169
+ for (const channel of ["group", "low", "high", "censor", "risk"]) {
170
+ const column = plainColumn("km", encoding, channel, source);
171
+ if (column.isErr())
172
+ return err(column.error);
173
+ optional[channel] = column.value;
174
+ }
175
+ return ok({
176
+ time: time.value,
177
+ survival: survival.value,
178
+ ...(optional.group !== undefined ? { group: optional.group } : {}),
179
+ ...(optional.low !== undefined ? { low: optional.low } : {}),
180
+ ...(optional.high !== undefined ? { high: optional.high } : {}),
181
+ ...(optional.censor !== undefined ? { censor: optional.censor } : {}),
182
+ ...(optional.risk !== undefined ? { risk: optional.risk } : {}),
183
+ });
184
+ }
185
+ /**
186
+ * The curves of one table: one for each group in the order of its first row, each in time order.
187
+ *
188
+ * A row whose time or survival is not numeric draws nothing. A survival outside 0 to 1 is no probability,
189
+ * thus it refuses, because the axis holds 0 to 1 and a clip would hide the fault. A bound on the wrong side of
190
+ * the survival refuses too. The sort is stable, thus two rows at one time keep the order of the table.
191
+ */
192
+ function kmCurves(rows, columns, blockId) {
193
+ const groupOf = (row) => (columns.group === undefined ? "" : (row[columns.group] ?? ""));
194
+ const names = firstAppearance(rows.map(groupOf).map(String));
195
+ const byName = new Map(names.map((name) => [name, []]));
196
+ for (const [index, row] of rows.entries()) {
197
+ const time = toNumber(row[columns.time]);
198
+ const survival = toNumber(row[columns.survival]);
199
+ if (time === null || survival === null)
200
+ continue;
201
+ if (survival < 0 || survival > 1) {
202
+ return err(chartProblem(blockId, `The km figure reads a survival between 0 and 1, and the row ${index + 1} holds ${survival} in the "${columns.survival}" column.`));
203
+ }
204
+ const low = columns.low === undefined ? null : toNumber(row[columns.low]);
205
+ const high = columns.high === undefined ? null : toNumber(row[columns.high]);
206
+ if (low !== null && high !== null && (low > survival || high < survival)) {
207
+ return err(chartProblem(blockId, `The row ${index + 1} holds the interval from ${low} to ${high} around the survival ${survival}. ` +
208
+ 'The "low" bound sits at or under the survival, and the "high" bound sits at or over it.'));
209
+ }
210
+ byName.get(String(groupOf(row)))?.push({
211
+ time,
212
+ survival,
213
+ low,
214
+ high,
215
+ censor: columns.censor === undefined ? null : toNumber(row[columns.censor]),
216
+ risk: columns.risk === undefined ? null : toNumber(row[columns.risk]),
217
+ });
218
+ }
219
+ return ok(names.map((name) => ({ name, points: (byName.get(name) ?? []).sort((a, b) => a.time - b.time) })));
220
+ }
221
+ /** The last time of the table, or 1 for a table with no time past zero. */
222
+ function timeEnd(curves) {
223
+ let end = 0;
224
+ for (const curve of curves) {
225
+ for (const point of curve.points)
226
+ end = Math.max(end, point.time);
227
+ }
228
+ return end > 0 ? end : 1;
229
+ }
230
+ /** The smallest nice step of the time axis at or over the end over `MAX_TIME_TICKS - 1`. */
231
+ function timeStep(end) {
232
+ const least = end / (MAX_TIME_TICKS - 1);
233
+ let decade = 10 ** Math.floor(Math.log10(least));
234
+ for (;;) {
235
+ for (const nice of NICE_STEPS) {
236
+ if (nice * decade >= least)
237
+ return nice * decade;
238
+ }
239
+ decade *= 10;
240
+ }
241
+ }
242
+ /** The ticks of the time axis: each multiple of the step from 0 to the end. */
243
+ function timeTicks(end, step) {
244
+ const count = Math.floor(end / step + TICK_TOLERANCE);
245
+ const ticks = [];
246
+ for (let place = 0; place <= count; place += 1)
247
+ ticks.push(Number((place * step).toPrecision(TICK_DIGITS)));
248
+ return ticks;
249
+ }
250
+ /** Whether the end of the time axis sits at a multiple of the step. */
251
+ function onTick(end, step) {
252
+ return Math.abs(end / step - Math.round(end / step)) < TICK_TOLERANCE;
253
+ }
254
+ /** The left edge of both grids, wide enough for the longest group name in the table column. */
255
+ function gridLeft(curves) {
256
+ let longest = 0;
257
+ for (const curve of curves)
258
+ longest = Math.max(longest, categoryName(curve.name).length);
259
+ return Math.min(GRID_LEFT_MAX, Math.max(GRID_LEFT_MIN, Math.ceil(GRID_LEFT_PAD + longest * NAME_CHARACTER_PCT)));
260
+ }
261
+ /**
262
+ * The series of one curve: the step line, the two halves of the step band, and the censor ticks.
263
+ *
264
+ * Each series carries the name of the group, thus the legend entry of the group shows and hides all of them.
265
+ * The band and the ticks are silent, thus the legend takes its icon from the line.
266
+ */
267
+ function curveSeries(curve, index, color, columns) {
268
+ const name = categoryName(curve.name);
269
+ const points = curve.points;
270
+ const line = points.map((point) => [point.time, point.survival]);
271
+ if (points.length === 0 || points[0].time > 0)
272
+ line.unshift([0, 1]);
273
+ const series = [
274
+ {
275
+ type: "line",
276
+ name,
277
+ step: "end",
278
+ showSymbol: false,
279
+ itemStyle: { color },
280
+ lineStyle: { color, width: CURVE_WIDTH_PX },
281
+ z: 3,
282
+ data: line,
283
+ },
284
+ ];
285
+ if (columns.low !== undefined && columns.high !== undefined) {
286
+ const lower = [];
287
+ const span = [];
288
+ for (const point of points) {
289
+ if (point.low === null || point.high === null)
290
+ continue;
291
+ lower.push([point.time, point.low]);
292
+ span.push([point.time, point.high - point.low]);
293
+ }
294
+ const band = {
295
+ type: "line",
296
+ name,
297
+ stack: `km-band-${index}`,
298
+ step: "end",
299
+ showSymbol: false,
300
+ silent: true,
301
+ tooltip: { show: false },
302
+ lineStyle: { opacity: 0 },
303
+ };
304
+ series.push({ ...band, data: lower }, { ...band, areaStyle: { color, opacity: BAND_OPACITY }, data: span });
305
+ }
306
+ if (columns.censor !== undefined) {
307
+ series.push({
308
+ type: "scatter",
309
+ name,
310
+ symbol: "rect",
311
+ symbolSize: [...CENSOR_TICK_PX],
312
+ silent: true,
313
+ itemStyle: { color },
314
+ z: 4,
315
+ data: points.filter((point) => point.censor !== null && point.censor > 0).map((point) => [point.time, point.survival]),
316
+ });
317
+ }
318
+ return series;
319
+ }
320
+ /** The median of one curve: the time of its first row at or under 0.5, or `undefined` where none reaches it. */
321
+ function medianOf(curve) {
322
+ return curve.points.find((point) => point.survival <= MEDIAN_SURVIVAL)?.time;
323
+ }
324
+ /**
325
+ * The median lines as one silent series: a line at 0.5 from time 0 to the latest median, and one drop from 0.5
326
+ * to the time axis at the median of each curve that crosses 0.5. No curve that crosses gives no series.
327
+ */
328
+ function medianSeries(curves) {
329
+ const medians = curves.map(medianOf).filter((median) => median !== undefined);
330
+ if (medians.length === 0)
331
+ return undefined;
332
+ const latest = Math.max(...medians);
333
+ const lines = [
334
+ [{ coord: [0, MEDIAN_SURVIVAL] }, { coord: [latest, MEDIAN_SURVIVAL] }],
335
+ ...medians.map((median) => [{ coord: [median, MEDIAN_SURVIVAL] }, { coord: [median, 0] }]),
336
+ ];
337
+ return {
338
+ type: "line",
339
+ silent: true,
340
+ tooltip: { show: false },
341
+ data: [],
342
+ markLine: { silent: true, symbol: "none", label: { show: false }, lineStyle: { ...GUIDE_LINE_STYLE }, data: lines },
343
+ };
344
+ }
345
+ /** The number at risk of one curve at one tick: the risk of its first row at or after the tick, else 0. */
346
+ function riskAt(curve, tick) {
347
+ for (const point of curve.points) {
348
+ if (point.time >= tick && point.risk !== null)
349
+ return point.risk;
350
+ }
351
+ return 0;
352
+ }
353
+ /** The counts of one curve in its row of the table: one text at each tick, in the ink of the chart. */
354
+ function riskSeries(curve, index, ticks) {
355
+ return {
356
+ type: "scatter",
357
+ xAxisIndex: 1,
358
+ yAxisIndex: 1,
359
+ silent: true,
360
+ tooltip: { show: false },
361
+ symbolSize: 0,
362
+ label: { show: true, position: "inside", formatter: "{b}", color: CHART_INK },
363
+ data: ticks.map((tick) => ({ name: String(riskAt(curve, tick)), value: [tick, index] })),
364
+ };
365
+ }
366
+ /** The grid of the table: one row for each group at the bottom of the chart, with the left edge of the plot. */
367
+ function tableGrid(layout, left) {
368
+ return { bottom: pct(layout.tableBottom), height: pct(layout.rows), left: pct(left), right: pct(GRID_RIGHT) };
369
+ }
370
+ /** The title of the table: one text over the group names, at the left edge of the chart. */
371
+ function tableTitle(layout) {
372
+ return {
373
+ type: "text",
374
+ left: pct(HEADER_LEFT),
375
+ bottom: pct(layout.tableBottom + layout.rows + layout.header / 4),
376
+ silent: true,
377
+ style: { text: TABLE_TITLE, fill: CHART_INK, fontFamily: CHART_FONT_STACK, fontSize: CHART_PAGE_TEXT_PX },
378
+ };
379
+ }
380
+ /** The time axis of the table: the range and the ticks of the plot, with no line and no label. */
381
+ function tableTimeAxis(end, step) {
382
+ return {
383
+ type: "value",
384
+ gridIndex: 1,
385
+ min: 0,
386
+ max: end,
387
+ interval: step,
388
+ axisLine: { show: false },
389
+ axisTick: { show: false },
390
+ axisLabel: { show: false },
391
+ splitLine: { show: false },
392
+ };
393
+ }
394
+ /** The group axis of the table: one row for each group from the top, each name in the color of its curve. */
395
+ function tableGroupAxis(curves, colorOf) {
396
+ const names = curves.map((curve, index) => ({ value: categoryName(curve.name), textStyle: { color: colorOf(index) } }));
397
+ return {
398
+ ...categoryAxis("y", [], { topDown: true }),
399
+ data: names,
400
+ gridIndex: 1,
401
+ axisLine: { show: false },
402
+ axisTick: { show: false },
403
+ axisLabel: { margin: TABLE_NAME_GAP_PX },
404
+ splitLine: { show: false },
405
+ };
406
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * The place of the names that a figure of one unit draws on its data: the sample names of a PCA, and the
3
+ * category names of an embedding.
4
+ *
5
+ * The chart runtime moves a label that overlaps another one only where the label states its own absolute
6
+ * place, and the option holds no function that could state one. Thus the derivation places each name itself,
7
+ * before the draw, in the units of the data. The square of the figure spans one length of data on each axis,
8
+ * thus one estimate of the square size converts a text size in pixels to a size in data units on both axes.
9
+ *
10
+ * The estimate reads a square smaller than the square of the page, because the column export draws a smaller
11
+ * square at a text size that shrinks less. A name that clears its neighbor at the estimate clears it at each
12
+ * size. The place is a pure function of the points and the names, thus the same rows give the same places.
13
+ */
14
+ import { CHART_EXPORT_SIZES } from "../design.js";
15
+ /** The gap between a point and its name, in pixels. */
16
+ export declare const NAME_GAP_PX = 4;
17
+ /** One box in data units, or in pixels. */
18
+ export interface Box {
19
+ readonly left: number;
20
+ readonly right: number;
21
+ readonly bottom: number;
22
+ readonly top: number;
23
+ }
24
+ /** True when two boxes share an area. */
25
+ export declare function overlaps(a: Box, b: Box): boolean;
26
+ /** The range of the two axes of a plot, in data units. */
27
+ export interface PlotRange {
28
+ readonly x: {
29
+ readonly min: number;
30
+ readonly max: number;
31
+ };
32
+ readonly y: {
33
+ readonly min: number;
34
+ readonly max: number;
35
+ };
36
+ }
37
+ /** One named point: its place in data units and its name. */
38
+ export interface NamedPoint {
39
+ readonly x: number;
40
+ readonly y: number;
41
+ readonly text: string;
42
+ }
43
+ /** The four sides of a point where its name can sit, in the order that the figure tries them. */
44
+ export declare const NAME_SIDES: readonly ["right", "left", "top", "bottom"];
45
+ /** One side of a point. */
46
+ export type NameSide = (typeof NAME_SIDES)[number];
47
+ /**
48
+ * The side of each point where its name sits, in the order of the points.
49
+ *
50
+ * Each name takes the first side, in the order right, left, top, bottom, where it stays inside the plot on that
51
+ * side and covers no earlier name and no point. A name that finds no free side sits at the right. `plot` is the range of
52
+ * the two axes in data units, and `pointPx` is the diameter of a point in pixels.
53
+ */
54
+ export declare function pointNameSides(points: readonly NamedPoint[], plot: PlotRange, pointPx: number): NameSide[];
55
+ /**
56
+ * The place of each category name on the data, in the order of the names, or `undefined` for a name that
57
+ * finds no free place.
58
+ *
59
+ * A name sits centered on its anchor, for example the median of its cells. A heavier name places first, thus
60
+ * the name of a large cluster keeps its anchor. A name that covers an earlier name moves along y by whole
61
+ * lines, up first, while its box stays inside the y range of `plot`, until it covers none. A name that covers
62
+ * an earlier name at each such place draws nothing, thus no two names overlap. `weights` gives the weight of
63
+ * each name, for example its cell count, and the x range of `plot` gives the length that sizes a name.
64
+ */
65
+ export declare function nameAnchors(names: readonly NamedPoint[], weights: readonly number[], plot: PlotRange): Array<{
66
+ readonly x: number;
67
+ readonly y: number;
68
+ } | undefined>;
69
+ /**
70
+ * The plot of one render, where a dense figure measures its names: the plot size in pixels and the text size of
71
+ * the render.
72
+ *
73
+ * Each frame is a little smaller than the plot that the chart runtime draws for a figure of one grid at that
74
+ * size. A smaller frame puts the names nearer to each other than the drawn plot does, thus a name that clears
75
+ * its neighbors in the frame clears them in the drawn plot.
76
+ */
77
+ export interface NameFrame {
78
+ readonly widthPx: number;
79
+ readonly heightPx: number;
80
+ readonly textPx: number;
81
+ }
82
+ /** The width of one text in pixels at one text size: the estimate of each figure that places its own text. */
83
+ export declare function textWidthPx(text: string, textPx: number): number;
84
+ /** The frame of the page: the plot inside the 900 by 400 pixel chart body of a window 1280 pixels wide. */
85
+ export declare const PAGE_PLOT_FRAME: NameFrame;
86
+ /** The frame of the single-column export: the plot inside the 336 by 253 pixel chart, and the 7 pt text. */
87
+ export declare const COLUMN_PLOT_FRAME: NameFrame;
88
+ /** The frame of the double-column export: the plot inside the 692 by 348 pixel chart, and the 7 pt text. */
89
+ export declare const DOUBLE_COLUMN_PLOT_FRAME: NameFrame;
90
+ /** The frame of the slide export: the plot inside the 1920 by 1080 pixel chart, and the slide text. */
91
+ export declare const SLIDE_PLOT_FRAME: NameFrame;
92
+ /** The frame of each export size. A new export size joins with the frame of its plot. */
93
+ export declare const EXPORT_PLOT_FRAMES: Readonly<Record<keyof typeof CHART_EXPORT_SIZES, NameFrame>>;
94
+ /**
95
+ * The three places of a name beside its anchor: the text starts at the anchor, the text ends at the anchor,
96
+ * or the text centers over the anchor.
97
+ */
98
+ export type LeaderSide = "right" | "left" | "top";
99
+ /** The anchor of one name in data units, and the side of the anchor where the text sits. */
100
+ export interface LeaderName {
101
+ readonly x: number;
102
+ readonly y: number;
103
+ readonly side: LeaderSide;
104
+ }
105
+ /** The points that no name can cover: the plotted pair of each row, and the diameter of the largest point. */
106
+ export interface NameObstacles {
107
+ readonly xs: readonly (number | null)[];
108
+ readonly ys: readonly (number | null)[];
109
+ readonly pointPx: number;
110
+ }
111
+ /**
112
+ * The anchor of each name, in the order of the names, or `undefined` for a name that finds no free place.
113
+ *
114
+ * The names come in the order of their weight, thus the most important name places first. Each name tries the
115
+ * sides in the given order, first beside its point, then one line further away at each step, up before down.
116
+ * A leader line joins each point to the anchor of its name. A place is free when the name stays inside the
117
+ * plot, when the name and its leader line cross no earlier name and no earlier leader line, and when they cover
118
+ * no point. Where no place is free, the name takes the first place clear of the earlier names and leader lines
119
+ * that covers the fewest points. A name that finds no place clear of them draws nothing.
120
+ *
121
+ * The geometry runs in pixels of `frame`, and each point enters a grid of cells one line high. Thus a test of
122
+ * one place reads the points of a few cells alone, and a table of many thousand points costs one pass to index.
123
+ * `plot` is the range of the two axes in data units. `top` is the highest y in data units that the text of a
124
+ * name can reach, because the chart runtime can end the y axis at the largest point. `reserved` holds the boxes
125
+ * in data units that other text of the figure takes, for example the label of a guide line, and no name covers
126
+ * one. A leader line can pass one, because a point can lie under the text.
127
+ */
128
+ export declare function placeLeaderNames(names: readonly NamedPoint[], obstacles: NameObstacles, plot: PlotRange, sides: readonly LeaderSide[], top: number, frame?: NameFrame, reserved?: readonly Box[]): Array<LeaderName | undefined>;
129
+ /** The box of one placed name in data units, for a test that a name covers no neighbor and no point. */
130
+ export declare function leaderNameBox(name: LeaderName, text: string, plot: PlotRange, frame?: NameFrame): Box;