@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.
- package/README.md +2 -2
- package/dist/agents/sandbox/shared.d.ts +1 -6
- package/dist/agents/sandbox/shared.js +0 -15
- package/dist/app/chat-turn.js +13 -1
- package/dist/config/environment-stores.d.ts +4 -2
- package/dist/contracts/chat-events.d.ts +9 -1
- package/dist/contracts/chat-frame.d.ts +3 -3
- package/dist/contracts/chat-frame.js +8 -3
- package/dist/contracts/chat-parts.d.ts +4 -1
- package/dist/contracts/data-profile.d.ts +4 -6
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/report-blocks.d.ts +1364 -75
- package/dist/contracts/report-blocks.js +221 -29
- package/dist/contracts/report-reference.js +1 -1
- package/dist/contracts/schemas/chat-events.d.ts +13 -0
- package/dist/contracts/schemas/chat-events.js +5 -0
- package/dist/contracts/schemas/chat-parts.d.ts +2 -2
- package/dist/contracts/schemas/index.d.ts +1 -1
- package/dist/contracts/schemas/index.js +1 -1
- package/dist/execution/input-signature.d.ts +7 -13
- package/dist/execution/input-signature.js +0 -0
- package/dist/execution/run-canceler.d.ts +3 -2
- package/dist/execution/run-canceler.js +3 -2
- package/dist/execution/run-event-stream.d.ts +16 -2
- package/dist/execution/run-event-stream.js +54 -3
- package/dist/index.d.ts +1 -8
- package/dist/index.js +5 -16
- package/dist/input-scan/enrich.d.ts +0 -2
- package/dist/input-scan/enrich.js +0 -1
- package/dist/lib/metrics.js +6 -7
- package/dist/lib/otel-spans.d.ts +4 -9
- package/dist/lib/otel-spans.js +3 -8
- package/dist/loop/run-agent.d.ts +4 -2
- package/dist/loop/run-agent.js +38 -23
- package/dist/memory/thread-history.d.ts +1 -1
- package/dist/prompts/report-session.d.ts +1 -1
- package/dist/prompts/report-session.js +17 -0
- package/dist/provenance/exec-frame.d.ts +1 -1
- package/dist/report-model/authoring-grammar.d.ts +416 -17
- package/dist/report-model/authoring-grammar.js +25 -4
- package/dist/report-model/block-walk.d.ts +20 -4
- package/dist/report-model/block-walk.js +64 -7
- package/dist/report-model/draft-finish.d.ts +6 -2
- package/dist/report-model/draft-finish.js +6 -1
- package/dist/report-model/production-resolver.d.ts +8 -0
- package/dist/report-model/production-resolver.js +1 -1
- package/dist/report-model/validate.d.ts +20 -10
- package/dist/report-model/validate.js +29 -13
- package/dist/report-render/chart-export.d.ts +52 -0
- package/dist/report-render/chart-export.js +192 -0
- package/dist/report-render/chart-presets.d.ts +46 -47
- package/dist/report-render/chart-presets.js +31 -101
- package/dist/report-render/chart-renderers.d.ts +163 -0
- package/dist/report-render/chart-renderers.js +1055 -0
- package/dist/report-render/chart-toolbox.d.ts +124 -0
- package/dist/report-render/chart-toolbox.js +595 -0
- package/dist/report-render/chart.d.ts +60 -27
- package/dist/report-render/chart.js +2323 -524
- package/dist/report-render/design.d.ts +476 -16
- package/dist/report-render/design.js +390 -56
- package/dist/report-render/figures/common.d.ts +321 -0
- package/dist/report-render/figures/common.js +759 -0
- package/dist/report-render/figures/dendrogram.d.ts +36 -0
- package/dist/report-render/figures/dendrogram.js +130 -0
- package/dist/report-render/figures/dense.d.ts +111 -0
- package/dist/report-render/figures/dense.js +204 -0
- package/dist/report-render/figures/dotplot.d.ts +28 -0
- package/dist/report-render/figures/dotplot.js +234 -0
- package/dist/report-render/figures/embedding.d.ts +43 -0
- package/dist/report-render/figures/embedding.js +360 -0
- package/dist/report-render/figures/equal-units.d.ts +101 -0
- package/dist/report-render/figures/equal-units.js +215 -0
- package/dist/report-render/figures/forest.d.ts +20 -0
- package/dist/report-render/figures/forest.js +398 -0
- package/dist/report-render/figures/gsea.d.ts +30 -0
- package/dist/report-render/figures/gsea.js +391 -0
- package/dist/report-render/figures/heatmap.d.ts +40 -0
- package/dist/report-render/figures/heatmap.js +395 -0
- package/dist/report-render/figures/index.d.ts +135 -0
- package/dist/report-render/figures/index.js +100 -0
- package/dist/report-render/figures/km.d.ts +16 -0
- package/dist/report-render/figures/km.js +406 -0
- package/dist/report-render/figures/label-room.d.ts +130 -0
- package/dist/report-render/figures/label-room.js +341 -0
- package/dist/report-render/figures/locuszoom.d.ts +50 -0
- package/dist/report-render/figures/locuszoom.js +675 -0
- package/dist/report-render/figures/lollipop.d.ts +16 -0
- package/dist/report-render/figures/lollipop.js +247 -0
- package/dist/report-render/figures/ma.d.ts +20 -0
- package/dist/report-render/figures/ma.js +114 -0
- package/dist/report-render/figures/manhattan.d.ts +60 -0
- package/dist/report-render/figures/manhattan.js +343 -0
- package/dist/report-render/figures/oncoprint.d.ts +41 -0
- package/dist/report-render/figures/oncoprint.js +420 -0
- package/dist/report-render/figures/pca.d.ts +37 -0
- package/dist/report-render/figures/pca.js +241 -0
- package/dist/report-render/figures/qq.d.ts +30 -0
- package/dist/report-render/figures/qq.js +182 -0
- package/dist/report-render/figures/roc.d.ts +17 -0
- package/dist/report-render/figures/roc.js +116 -0
- package/dist/report-render/figures/sankey.d.ts +48 -0
- package/dist/report-render/figures/sankey.js +353 -0
- package/dist/report-render/figures/upset.d.ts +60 -0
- package/dist/report-render/figures/upset.js +316 -0
- package/dist/report-render/figures/volcano.d.ts +25 -0
- package/dist/report-render/figures/volcano.js +166 -0
- package/dist/report-render/hybrid-svg.d.ts +75 -0
- package/dist/report-render/hybrid-svg.js +504 -0
- package/dist/report-render/number-format.d.ts +19 -0
- package/dist/report-render/number-format.js +25 -1
- package/dist/report-render/page.d.ts +41 -4
- package/dist/report-render/page.js +247 -12
- package/dist/report-render/render.d.ts +13 -4
- package/dist/report-render/render.js +37 -6
- package/dist/report-render/types.d.ts +23 -2
- package/dist/report-render/value-bridge.d.ts +38 -3
- package/dist/report-render/value-bridge.js +137 -6
- package/dist/report-render/views/chart-view.d.ts +42 -6
- package/dist/report-render/views/chart-view.js +70 -9
- package/dist/report-render/views/page-view.js +9 -10
- package/dist/runtime/assemble.js +2 -2
- package/dist/sandbox/client.d.ts +23 -32
- package/dist/sandbox/client.js +3 -11
- package/dist/sandbox/create-sandbox.d.ts +9 -24
- package/dist/sandbox/create-sandbox.js +15 -30
- package/dist/sandbox/docker-client.d.ts +12 -22
- package/dist/sandbox/docker-client.js +29 -43
- package/dist/sandbox/exec.d.ts +58 -0
- package/dist/sandbox/exec.js +197 -0
- package/dist/sandbox/identity.d.ts +5 -5
- package/dist/sandbox/identity.js +7 -10
- package/dist/sandbox/k8s-client.d.ts +4 -10
- package/dist/sandbox/k8s-client.js +5 -12
- package/dist/sandbox/liveness.d.ts +5 -12
- package/dist/sandbox/liveness.js +5 -12
- package/dist/sandbox/notification-sweep.d.ts +4 -7
- package/dist/sandbox/notification-sweep.js +4 -7
- package/dist/sandbox/reaper.d.ts +3 -3
- package/dist/sandbox/reaper.js +3 -3
- package/dist/sandbox/sandbox-error.d.ts +10 -11
- package/dist/sandbox/sandbox-error.js +10 -11
- package/dist/sandbox/sandbox-step-translate.d.ts +1 -1
- package/dist/sandbox/sandbox-step-translate.js +1 -1
- package/dist/sandbox/types.d.ts +31 -99
- package/dist/sandbox/types.js +17 -84
- package/dist/state/active-sandboxes.d.ts +7 -32
- package/dist/state/active-sandboxes.js +11 -46
- package/dist/state/index.d.ts +1 -2
- package/dist/state/index.js +1 -1
- package/dist/state/migrations/20261005120000_drop_the_active_exec_tracking.d.ts +7 -0
- package/dist/state/migrations/20261005120000_drop_the_active_exec_tracking.js +12 -0
- package/dist/state/migrations/index.js +2 -0
- package/dist/state/schema.d.ts +1 -5
- package/dist/state/schema.js +1 -6
- package/dist/state/step-executions.js +1 -2
- package/dist/tasks/data-profile.js +7 -20
- package/dist/tasks/derive-table-exec.d.ts +4 -14
- package/dist/tasks/derive-table-exec.js +9 -24
- package/dist/tasks/extract-values.d.ts +2 -2
- package/dist/tasks/extract-values.js +4 -7
- package/dist/tools/define-tool.d.ts +56 -13
- package/dist/tools/define-tool.js +28 -1
- package/dist/tools/execute-analysis.js +3 -3
- package/dist/tools/report-session/derive-table.d.ts +8 -9
- package/dist/tools/report-session/derive-table.js +3 -4
- package/dist/tools/report-session/preview-report.js +4 -49
- package/dist/tools/sandbox/list-available-packages.d.ts +1 -1
- package/dist/tools/sandbox/list-available-packages.js +5 -4
- package/dist/tools/workspace/edit-file.d.ts +5 -4
- package/dist/tools/workspace/edit-file.js +10 -13
- package/dist/tools/workspace/execute-command.d.ts +27 -42
- package/dist/tools/workspace/execute-command.js +42 -38
- package/dist/tools/workspace/index.d.ts +5 -6
- package/dist/tools/workspace/index.js +5 -6
- package/dist/tools/workspace/mutator.d.ts +43 -8
- package/dist/tools/workspace/mutator.js +45 -34
- package/dist/tools/workspace/run-exec.d.ts +3 -12
- package/dist/tools/workspace/run-exec.js +7 -24
- package/dist/tools/workspace/scan-inputs.d.ts +0 -3
- package/dist/tools/workspace/scan-inputs.js +0 -1
- package/dist/tools/workspace/write-file.d.ts +12 -3
- package/dist/tools/workspace/write-file.js +17 -13
- package/dist/workflows/sandbox-step.d.ts +7 -11
- package/dist/workflows/sandbox-step.js +24 -27
- package/package.json +5 -3
- package/dist/sandbox/await-exec.d.ts +0 -143
- package/dist/sandbox/await-exec.js +0 -417
- package/dist/sandbox/deliver-exec-event.d.ts +0 -25
- package/dist/sandbox/deliver-exec-event.js +0 -29
- package/dist/sandbox/digest.d.ts +0 -14
- package/dist/sandbox/digest.js +0 -17
- package/dist/sandbox/exec-id.d.ts +0 -12
- package/dist/sandbox/exec-id.js +0 -20
- package/dist/sandbox/hmac.d.ts +0 -62
- package/dist/sandbox/hmac.js +0 -72
- package/dist/sandbox/submit-exec.d.ts +0 -38
- package/dist/sandbox/submit-exec.js +0 -80
- package/dist/sandbox/watchdog.d.ts +0 -71
- package/dist/sandbox/watchdog.js +0 -161
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The forest figure: one row for each term, top-down, with the estimate as a square, its interval as a line,
|
|
3
|
+
* the line of no effect, and a text column at the right that prints each estimate with its interval, and a
|
|
4
|
+
* second text column that prints the p value.
|
|
5
|
+
*
|
|
6
|
+
* A ratio reads on a log axis, thus the axis is logarithmic when each value is positive, and its line of no
|
|
7
|
+
* effect sits at 1. A table with a value at or under zero holds differences, thus its axis is linear and the
|
|
8
|
+
* line sits at 0. The log axis ends at the nice ratios just past the data and 1, thus it spans no empty decade.
|
|
9
|
+
* An interval past the widest log axis ends at the axis with an arrow, as `forestplot` clips it, and its text
|
|
10
|
+
* prints the bound of the table.
|
|
11
|
+
*/
|
|
12
|
+
import { err, ok } from "neverthrow";
|
|
13
|
+
import { channelColumn, channelOrder, channelTransform } from "../../contracts/report-blocks.js";
|
|
14
|
+
import { INTERVAL_RENDERER } from "../chart-renderers.js";
|
|
15
|
+
import { TOOLBOX_BAND_PX } from "../chart-toolbox.js";
|
|
16
|
+
import { CHART_EXPORT_SIZES, CHART_INK, CHART_PAGE_TEXT_PX, GUIDE_LINE_COLOR, GUIDE_LINE_WIDTH_PX } from "../design.js";
|
|
17
|
+
import { formatNumberCell, selectNumberKind, shownMinus, smallestPositiveValue, typographicExponent } from "../number-format.js";
|
|
18
|
+
import { categoryAxis, categoryAxisTitle, chartProblem, columnPresent, demandedColumn, orderCategories, plainColumn, toNumber, valueAxis, valueAxisTitle, } from "./common.js";
|
|
19
|
+
/** The members of a forest block that the figure reads. */
|
|
20
|
+
const FOREST_READS = new Set(["x", "y", "low", "high", "p", "size"]);
|
|
21
|
+
/** The forest figure of a table of ratios or differences. */
|
|
22
|
+
export const FOREST_FIGURE = { reads: FOREST_READS, derive: deriveForest };
|
|
23
|
+
/** The nice ratios of one decade. The ends of the log axis sit at one of them times a power of ten. */
|
|
24
|
+
const NICE_RATIOS = [1, 2, 2.5, 4, 5];
|
|
25
|
+
/**
|
|
26
|
+
* The widest span of the log axis, in decades. A separated covariate gives a Wald interval of many decades, and
|
|
27
|
+
* an axis that holds it squeezes each other row into one narrow band.
|
|
28
|
+
*/
|
|
29
|
+
const LOG_AXIS_DECADES = 4;
|
|
30
|
+
/** The clip marks of an interval item: its low end, and its high end, at an end of the axis. */
|
|
31
|
+
const CLIPPED_LOW = 1;
|
|
32
|
+
const CLIPPED_HIGH = 2;
|
|
33
|
+
/** The side of a square in pixels where the block names no size, and the least and the most side of a sized square. */
|
|
34
|
+
const SQUARE_PX = 9;
|
|
35
|
+
const SQUARE_MIN_PX = 5;
|
|
36
|
+
const SQUARE_MAX_PX = 16;
|
|
37
|
+
/** The drawing levels of the interval line and of the square. The square covers the line through it. */
|
|
38
|
+
const INTERVAL_Z = 3;
|
|
39
|
+
const SQUARE_Z = 4;
|
|
40
|
+
/**
|
|
41
|
+
* The width of one character of a text column as a share of the text size, and the gap in pixels between the
|
|
42
|
+
* two columns. The p column sits past the widest text of the estimate column.
|
|
43
|
+
*/
|
|
44
|
+
const CHARACTER_SHARE = 0.6;
|
|
45
|
+
const COLUMN_GAP_PX = 16;
|
|
46
|
+
/** The gap in pixels between the plot and the estimate column. */
|
|
47
|
+
const TEXT_MARGIN_PX = 12;
|
|
48
|
+
/**
|
|
49
|
+
* The characters of one line of the term column, and its most lines. A longer term breaks at a space onto the
|
|
50
|
+
* next line, and a term past the last line ends with an ellipsis. The square of the row names the whole term in
|
|
51
|
+
* its tooltip. The break counts characters, thus the column holds the same lines at each text size.
|
|
52
|
+
*/
|
|
53
|
+
const TERM_LINE_CHARACTERS = 18;
|
|
54
|
+
const TERM_LINES = 2;
|
|
55
|
+
/**
|
|
56
|
+
* The decimals of a ratio from 0.1 up, as `forestplot` and the journals print it, and the magnitude under which
|
|
57
|
+
* a row takes more decimals.
|
|
58
|
+
*/
|
|
59
|
+
const ESTIMATE_DECIMALS = 2;
|
|
60
|
+
const ESTIMATE_DECIMAL_FLOOR = 0.1;
|
|
61
|
+
/**
|
|
62
|
+
* The magnitude under which a value of the estimate column prints as a power of ten, the same floor as the
|
|
63
|
+
* compact-scientific form of a table cell. A separated covariate gives a bound such as `1.2e-150`, and no
|
|
64
|
+
* fixed count of decimals holds it: `toFixed` refuses more than 100.
|
|
65
|
+
*/
|
|
66
|
+
const ESTIMATE_SCIENTIFIC_FLOOR = 1e-3;
|
|
67
|
+
/**
|
|
68
|
+
* The edges of the chart in percent. The grid holds its tick labels, its axis names, and each text column
|
|
69
|
+
* inside these edges, thus a long term and the p column both stay inside the chart. On the page the top edge
|
|
70
|
+
* sits under the band of the toolbox, because the titles of the text columns sit at the top right corner. An
|
|
71
|
+
* export has no toolbox, thus a rule for each export size puts the top edge back.
|
|
72
|
+
*/
|
|
73
|
+
const CHART_EDGE = "2%";
|
|
74
|
+
/** Derive the figure of one forest block. */
|
|
75
|
+
function deriveForest(block, rows, context) {
|
|
76
|
+
const columns = forestColumns(block, rows, context);
|
|
77
|
+
if (columns.isErr())
|
|
78
|
+
return err(columns.error);
|
|
79
|
+
const read = forestRows(rows, columns.value, context.blockId);
|
|
80
|
+
if (read.isErr())
|
|
81
|
+
return err(read.error);
|
|
82
|
+
const order = channelOrder(block.encoding?.y ?? columns.value.term);
|
|
83
|
+
const ordered = orderCategories(context.blockId, read.value.length, (index) => read.value[index].term, read.value.map((entry) => entry.term), order, (index) => (order === undefined ? undefined : read.value[index].row[order.by]));
|
|
84
|
+
if (ordered.isErr())
|
|
85
|
+
return err(ordered.error);
|
|
86
|
+
const byTerm = new Map(read.value.map((entry) => [String(entry.term), entry]));
|
|
87
|
+
const forest = ordered.value.flatMap((term) => byTerm.get(String(term)) ?? []);
|
|
88
|
+
const positive = forest.every((entry) => entry.estimate > 0 && (entry.low === null || entry.low > 0) && (entry.high === null || entry.high > 0));
|
|
89
|
+
const reference = positive ? 1 : 0;
|
|
90
|
+
const xTitle = valueAxisTitle(context.labels, columns.value.estimate);
|
|
91
|
+
// A linear axis without `scale` holds zero in its range, thus the line of no effect always shows.
|
|
92
|
+
const range = positive ? logRange(forest) : undefined;
|
|
93
|
+
const xAxis = range !== undefined ? valueAxis("x", xTitle, { log: true, ...range }) : valueAxis("x", xTitle);
|
|
94
|
+
const estimates = forest.map((entry) => estimateText(entry));
|
|
95
|
+
const textAxes = [textAxis(estimates, xTitle, TEXT_MARGIN_PX)];
|
|
96
|
+
if (columns.value.p !== undefined) {
|
|
97
|
+
const pColumn = columns.value.p;
|
|
98
|
+
const bound = smallestPositiveValue(forest.map((entry) => entry.row[pColumn]));
|
|
99
|
+
const pTexts = forest.map((entry) => pText(entry.row[pColumn], pColumn, bound));
|
|
100
|
+
textAxes.push(textAxis(pTexts, valueAxisTitle(context.labels, pColumn), TEXT_MARGIN_PX + widestText(estimates) + COLUMN_GAP_PX));
|
|
101
|
+
}
|
|
102
|
+
const series = [squareSeries(forest, columns.value.size, reference)];
|
|
103
|
+
if (columns.value.low !== undefined && columns.value.high !== undefined)
|
|
104
|
+
series.push(intervalSeries(forest, range));
|
|
105
|
+
return ok({
|
|
106
|
+
legend: { show: false },
|
|
107
|
+
grid: { top: TOOLBOX_BAND_PX, bottom: CHART_EDGE, left: CHART_EDGE, right: CHART_EDGE, outerBoundsMode: "same", outerBoundsContain: "all" },
|
|
108
|
+
xAxis,
|
|
109
|
+
yAxis: [
|
|
110
|
+
{
|
|
111
|
+
...categoryAxis("y", forest.map((entry) => termLines(String(entry.term))), { title: categoryAxisTitle(context.labels, columns.value.term), topDown: true }),
|
|
112
|
+
// A forest reads as a table of rows, thus the term column draws no axis line and no tick.
|
|
113
|
+
axisLine: { show: false },
|
|
114
|
+
axisTick: { show: false },
|
|
115
|
+
},
|
|
116
|
+
...textAxes,
|
|
117
|
+
],
|
|
118
|
+
series,
|
|
119
|
+
media: Object.values(CHART_EXPORT_SIZES).map((size) => ({
|
|
120
|
+
query: { minWidth: size.widthPx, maxWidth: size.widthPx, minHeight: size.heightPx, maxHeight: size.heightPx },
|
|
121
|
+
option: { grid: { top: CHART_EDGE } },
|
|
122
|
+
})),
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The columns of one forest block. The term and the estimate are demanded, and each other channel is
|
|
127
|
+
* optional. The term channel can carry an order, because it draws the category axis. Every other channel
|
|
128
|
+
* reads a plain column.
|
|
129
|
+
*/
|
|
130
|
+
function forestColumns(block, rows, context) {
|
|
131
|
+
const encoding = block.encoding ?? {};
|
|
132
|
+
const source = { blockId: context.blockId, rows, columns: context.columns };
|
|
133
|
+
const term = encoding.y;
|
|
134
|
+
if (term === undefined)
|
|
135
|
+
return err(chartProblem(context.blockId, 'The forest figure needs a column for the "y" channel.'));
|
|
136
|
+
if (channelTransform(term) !== undefined) {
|
|
137
|
+
return err(chartProblem(context.blockId, 'The forest figure reads the "y" channel as a plain column, thus it takes no transform.'));
|
|
138
|
+
}
|
|
139
|
+
for (const column of [channelColumn(term), channelOrder(term)?.by]) {
|
|
140
|
+
if (column !== undefined && rows.length > 0 && !columnPresent(column, rows, context.columns)) {
|
|
141
|
+
return err(chartProblem(context.blockId, `The column "${column}" is absent from every row.`));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const estimate = demandedColumn("forest", encoding, "x", source);
|
|
145
|
+
if (estimate.isErr())
|
|
146
|
+
return err(estimate.error);
|
|
147
|
+
const optional = { low: undefined, high: undefined, p: undefined, size: undefined };
|
|
148
|
+
for (const channel of ["low", "high", "p", "size"]) {
|
|
149
|
+
const column = plainColumn("forest", encoding, channel, source);
|
|
150
|
+
if (column.isErr())
|
|
151
|
+
return err(column.error);
|
|
152
|
+
optional[channel] = column.value;
|
|
153
|
+
}
|
|
154
|
+
return ok({
|
|
155
|
+
term: channelColumn(term),
|
|
156
|
+
estimate: estimate.value,
|
|
157
|
+
...(optional.low !== undefined ? { low: optional.low } : {}),
|
|
158
|
+
...(optional.high !== undefined ? { high: optional.high } : {}),
|
|
159
|
+
...(optional.p !== undefined ? { p: optional.p } : {}),
|
|
160
|
+
...(optional.size !== undefined ? { size: optional.size } : {}),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* The rows of the figure in table order.
|
|
165
|
+
*
|
|
166
|
+
* A row whose term is empty or whose estimate is not numeric draws nothing. A term names one row, thus a term
|
|
167
|
+
* of two rows refuses, because its two squares would share one line. A bound on the wrong side of its
|
|
168
|
+
* estimate refuses too.
|
|
169
|
+
*/
|
|
170
|
+
function forestRows(rows, columns, blockId) {
|
|
171
|
+
const seen = new Set();
|
|
172
|
+
const forest = [];
|
|
173
|
+
for (const [index, row] of rows.entries()) {
|
|
174
|
+
const term = row[columns.term];
|
|
175
|
+
const estimate = toNumber(row[columns.estimate]);
|
|
176
|
+
if (term === undefined || String(term) === "" || estimate === null)
|
|
177
|
+
continue;
|
|
178
|
+
if (seen.has(String(term))) {
|
|
179
|
+
return err(chartProblem(blockId, `The forest figure draws one row for each term, and the term "${String(term)}" holds two rows.`));
|
|
180
|
+
}
|
|
181
|
+
seen.add(String(term));
|
|
182
|
+
const low = columns.low === undefined ? null : toNumber(row[columns.low]);
|
|
183
|
+
const high = columns.high === undefined ? null : toNumber(row[columns.high]);
|
|
184
|
+
if (low !== null && high !== null && (low > estimate || high < estimate)) {
|
|
185
|
+
return err(chartProblem(blockId, `The row ${index + 1} holds the interval from ${low} to ${high} around the estimate ${estimate}. ` +
|
|
186
|
+
'The "low" bound sits at or under the estimate, and the "high" bound sits at or over it.'));
|
|
187
|
+
}
|
|
188
|
+
forest.push({ term, estimate, low, high, row });
|
|
189
|
+
}
|
|
190
|
+
return ok(forest);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* The ends of the log axis: the largest nice ratio at or under the smallest value, and the smallest nice ratio
|
|
194
|
+
* at or over the largest value. Both ends reach 1, thus the line of no effect always shows.
|
|
195
|
+
*
|
|
196
|
+
* The axis spans `LOG_AXIS_DECADES` at most, and it always holds each estimate and 1. Where the bounds need a
|
|
197
|
+
* wider axis, each side takes half of the free decades, and a side that needs less gives the rest to the other
|
|
198
|
+
* side.
|
|
199
|
+
*/
|
|
200
|
+
function logRange(forest) {
|
|
201
|
+
let least = 1;
|
|
202
|
+
let most = 1;
|
|
203
|
+
let lowest = 1;
|
|
204
|
+
let highest = 1;
|
|
205
|
+
for (const entry of forest) {
|
|
206
|
+
least = Math.min(least, entry.estimate);
|
|
207
|
+
most = Math.max(most, entry.estimate);
|
|
208
|
+
for (const value of [entry.estimate, entry.low, entry.high]) {
|
|
209
|
+
if (value === null)
|
|
210
|
+
continue;
|
|
211
|
+
lowest = Math.min(lowest, value);
|
|
212
|
+
highest = Math.max(highest, value);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const full = { min: niceRatio(lowest, "under"), max: niceRatio(highest, "over") };
|
|
216
|
+
if (Math.log10(full.max / full.min) <= LOG_AXIS_DECADES)
|
|
217
|
+
return full;
|
|
218
|
+
const core = { min: niceRatio(least, "under"), max: niceRatio(most, "over") };
|
|
219
|
+
const room = Math.max(0, LOG_AXIS_DECADES - Math.log10(core.max / core.min));
|
|
220
|
+
const below = Math.log10(core.min / full.min);
|
|
221
|
+
const above = Math.log10(full.max / core.max);
|
|
222
|
+
const under = Math.min(below, Math.max(room / 2, room - above));
|
|
223
|
+
const over = Math.min(above, room - under);
|
|
224
|
+
return {
|
|
225
|
+
min: under >= below ? full.min : niceRatio(core.min / 10 ** under, "over"),
|
|
226
|
+
max: over >= above ? full.max : niceRatio(core.max * 10 ** over, "under"),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/** The nearest nice ratio at or under, or at or over, one positive value. */
|
|
230
|
+
function niceRatio(value, side) {
|
|
231
|
+
const decade = Math.floor(Math.log10(value));
|
|
232
|
+
const candidates = [];
|
|
233
|
+
for (const power of [decade - 1, decade, decade + 1]) {
|
|
234
|
+
for (const nice of NICE_RATIOS)
|
|
235
|
+
candidates.push(roundRatio(nice * 10 ** power));
|
|
236
|
+
}
|
|
237
|
+
return side === "under"
|
|
238
|
+
? Math.max(...candidates.filter((candidate) => candidate <= value))
|
|
239
|
+
: Math.min(...candidates.filter((candidate) => candidate >= value));
|
|
240
|
+
}
|
|
241
|
+
/** One nice ratio rounded to its own digits, thus `2.5 × 10^-1` reads `0.25` and never `0.25000000000000006`. */
|
|
242
|
+
function roundRatio(value) {
|
|
243
|
+
return Number(value.toPrecision(6));
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* The shown text of one cell of the p column, as a power of ten, or the cell as it is where it is empty.
|
|
247
|
+
*
|
|
248
|
+
* The `p` channel names a p-value column whatever its name, thus the cell takes the kind of a p-value. A stored
|
|
249
|
+
* zero prints the bound under the smallest positive p of the column, and never a bare zero.
|
|
250
|
+
*/
|
|
251
|
+
function pText(cell, column, bound) {
|
|
252
|
+
if (cell === undefined || cell === "")
|
|
253
|
+
return "";
|
|
254
|
+
return typographicExponent(formatNumberCell(cell, selectNumberKind(column, cell, "p-value"), bound).text);
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* The text of one estimate with its interval, each value with the decimals of the row, for example
|
|
258
|
+
* `0.53 (0.38–0.75)`. A negative bound joins the interval with the word `to`, because a dash beside a minus
|
|
259
|
+
* reads as a range of the wrong sign. A row with no interval prints its estimate.
|
|
260
|
+
*/
|
|
261
|
+
function estimateText(entry) {
|
|
262
|
+
const bounds = entry.low !== null && entry.high !== null ? { low: entry.low, high: entry.high } : undefined;
|
|
263
|
+
const decimals = rowDecimals(bounds === undefined ? [entry.estimate] : [entry.estimate, bounds.low, bounds.high]);
|
|
264
|
+
const estimate = fixedText(entry.estimate, decimals);
|
|
265
|
+
if (bounds === undefined)
|
|
266
|
+
return estimate;
|
|
267
|
+
const joint = bounds.low < 0 || bounds.high < 0 ? " to " : "–";
|
|
268
|
+
return `${estimate} (${fixedText(bounds.low, decimals)}${joint}${fixedText(bounds.high, decimals)})`;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* The decimals of one row: two where each value that is not zero sits at 0.1 or more, else the decimals that
|
|
272
|
+
* give two significant digits to the smallest value. Thus the estimate and its bounds read at one precision.
|
|
273
|
+
* A value under `ESTIMATE_SCIENTIFIC_FLOOR` prints as a power of ten, thus it sets no decimals.
|
|
274
|
+
*/
|
|
275
|
+
function rowDecimals(values) {
|
|
276
|
+
let least = Number.POSITIVE_INFINITY;
|
|
277
|
+
for (const value of values) {
|
|
278
|
+
if (Math.abs(value) >= ESTIMATE_SCIENTIFIC_FLOOR)
|
|
279
|
+
least = Math.min(least, Math.abs(value));
|
|
280
|
+
}
|
|
281
|
+
return least < ESTIMATE_DECIMAL_FLOOR ? 1 - Math.floor(Math.log10(least)) : ESTIMATE_DECIMALS;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* One value at a fixed count of decimals, with the typographic minus. A value that rounds to zero prints no
|
|
285
|
+
* sign, and a value under `ESTIMATE_SCIENTIFIC_FLOOR` that is not zero prints as a power of ten.
|
|
286
|
+
*/
|
|
287
|
+
function fixedText(value, decimals) {
|
|
288
|
+
if (value !== 0 && Math.abs(value) < ESTIMATE_SCIENTIFIC_FLOOR)
|
|
289
|
+
return typographicExponent(formatNumberCell(value, "scientific").text);
|
|
290
|
+
const text = value.toFixed(decimals);
|
|
291
|
+
return Number(text) === 0 ? text.replace("-", "") : shownMinus(text);
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* One term broken at its spaces into lines of `TERM_LINE_CHARACTERS` at most, and `TERM_LINES` lines at most.
|
|
295
|
+
* A word longer than a line stays whole on its own line. A term that needs more lines keeps the first lines,
|
|
296
|
+
* and its last line ends with an ellipsis.
|
|
297
|
+
*/
|
|
298
|
+
export function termLines(term) {
|
|
299
|
+
const lines = [];
|
|
300
|
+
let line = "";
|
|
301
|
+
for (const word of term.split(" ").filter((part) => part !== "")) {
|
|
302
|
+
if (line === "")
|
|
303
|
+
line = word;
|
|
304
|
+
else if (line.length + 1 + word.length <= TERM_LINE_CHARACTERS)
|
|
305
|
+
line = `${line} ${word}`;
|
|
306
|
+
else {
|
|
307
|
+
lines.push(line);
|
|
308
|
+
line = word;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (line !== "")
|
|
312
|
+
lines.push(line);
|
|
313
|
+
if (lines.length <= TERM_LINES)
|
|
314
|
+
return lines.join("\n");
|
|
315
|
+
const kept = lines.slice(0, TERM_LINES);
|
|
316
|
+
const last = kept[TERM_LINES - 1];
|
|
317
|
+
kept[TERM_LINES - 1] = `${last.length < TERM_LINE_CHARACTERS ? last : last.slice(0, TERM_LINE_CHARACTERS - 1)}…`;
|
|
318
|
+
return kept.join("\n");
|
|
319
|
+
}
|
|
320
|
+
/** The estimated width in pixels of the widest text of one column at the page text size. */
|
|
321
|
+
function widestText(texts) {
|
|
322
|
+
let longest = 0;
|
|
323
|
+
for (const text of texts)
|
|
324
|
+
longest = Math.max(longest, text.length);
|
|
325
|
+
return Math.ceil(longest * CHARACTER_SHARE * CHART_PAGE_TEXT_PX);
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* One text column at the right of the plot: a category axis of the rows with the text of each row as its
|
|
329
|
+
* label, and the column title over it. The axis draws no line and no tick, thus only the text shows.
|
|
330
|
+
*/
|
|
331
|
+
function textAxis(texts, title, offset) {
|
|
332
|
+
return {
|
|
333
|
+
type: "category",
|
|
334
|
+
position: "right",
|
|
335
|
+
inverse: true,
|
|
336
|
+
offset,
|
|
337
|
+
data: [...texts],
|
|
338
|
+
name: title,
|
|
339
|
+
nameLocation: "start",
|
|
340
|
+
nameTextStyle: { align: "left", fontWeight: "bold" },
|
|
341
|
+
axisLine: { show: false },
|
|
342
|
+
axisTick: { show: false },
|
|
343
|
+
axisLabel: { align: "left", margin: 0 },
|
|
344
|
+
splitLine: { show: false },
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* The squares of the estimates, with the line of no effect.
|
|
349
|
+
*
|
|
350
|
+
* A size channel sets the area of each square, thus the side grows with the square root of the value, and the
|
|
351
|
+
* largest value takes the most side. A square too small to see takes the least side.
|
|
352
|
+
*/
|
|
353
|
+
function squareSeries(forest, sizeColumn, reference) {
|
|
354
|
+
let largest = 0;
|
|
355
|
+
if (sizeColumn !== undefined) {
|
|
356
|
+
for (const entry of forest)
|
|
357
|
+
largest = Math.max(largest, toNumber(entry.row[sizeColumn]) ?? 0);
|
|
358
|
+
}
|
|
359
|
+
const sideOf = (entry) => {
|
|
360
|
+
if (sizeColumn === undefined || largest <= 0)
|
|
361
|
+
return SQUARE_PX;
|
|
362
|
+
const size = toNumber(entry.row[sizeColumn]);
|
|
363
|
+
if (size === null || size <= 0)
|
|
364
|
+
return SQUARE_MIN_PX;
|
|
365
|
+
return Math.max(SQUARE_MIN_PX, Math.round(SQUARE_MAX_PX * Math.sqrt(size / largest)));
|
|
366
|
+
};
|
|
367
|
+
return {
|
|
368
|
+
type: "scatter",
|
|
369
|
+
symbol: "rect",
|
|
370
|
+
itemStyle: { color: CHART_INK },
|
|
371
|
+
z: SQUARE_Z,
|
|
372
|
+
data: forest.map((entry, place) => ({ name: String(entry.term), value: [entry.estimate, place], symbolSize: sideOf(entry) })),
|
|
373
|
+
markLine: {
|
|
374
|
+
silent: true,
|
|
375
|
+
symbol: "none",
|
|
376
|
+
label: { show: false },
|
|
377
|
+
lineStyle: { color: GUIDE_LINE_COLOR, width: GUIDE_LINE_WIDTH_PX, type: "solid" },
|
|
378
|
+
data: [{ xAxis: reference }],
|
|
379
|
+
},
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* The interval of each row as a line along the x axis, through the named interval renderer. A bound past an end
|
|
384
|
+
* of `range` ends at that end, and the item marks the clipped end.
|
|
385
|
+
*/
|
|
386
|
+
function intervalSeries(forest, range) {
|
|
387
|
+
const items = [];
|
|
388
|
+
for (const [place, entry] of forest.entries()) {
|
|
389
|
+
if (entry.low === null || entry.high === null)
|
|
390
|
+
continue;
|
|
391
|
+
const clipsLow = range !== undefined && entry.low < range.min;
|
|
392
|
+
const clipsHigh = range !== undefined && entry.high > range.max;
|
|
393
|
+
const low = clipsLow ? range.min : entry.low;
|
|
394
|
+
const high = clipsHigh ? range.max : entry.high;
|
|
395
|
+
items.push([entry.estimate, place, low, high, 0, 0, 0, (clipsLow ? CLIPPED_LOW : 0) + (clipsHigh ? CLIPPED_HIGH : 0)]);
|
|
396
|
+
}
|
|
397
|
+
return { type: "custom", renderItem: INTERVAL_RENDERER, silent: true, z: INTERVAL_Z, encode: { x: [0, 2, 3], y: 1 }, data: items };
|
|
398
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
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 type { FigureModule } from "./index.js";
|
|
21
|
+
/**
|
|
22
|
+
* The count of vertices that the thinning aims at for one curve, before it adds the vertices of shape.
|
|
23
|
+
*
|
|
24
|
+
* A wide page draws the rank axis over about one thousand pixels, thus one vertex for each pixel keeps the
|
|
25
|
+
* drawn line as the full table draws it. Three sets and the metric then stay under the coordinate bound of the
|
|
26
|
+
* SVG export.
|
|
27
|
+
*/
|
|
28
|
+
export declare const GSEA_STRIDE_TARGET = 1000;
|
|
29
|
+
/** The GSEA running enrichment score figure. */
|
|
30
|
+
export declare const GSEA_FIGURE: FigureModule;
|