@inflexa-ai/harness 0.42.1 → 0.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/agents/conversation-agent.js +5 -1
- package/dist/agents/sandbox/drug-repurposing-agent.js +1 -0
- package/dist/agents/sandbox/shared.d.ts +1 -6
- package/dist/agents/sandbox/shared.js +3 -16
- package/dist/agents/sandbox/types.d.ts +1 -1
- 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/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
- package/dist/prompts/sandbox/drug-repurposing-agent.js +7 -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/bio/index.d.ts +2 -0
- package/dist/tools/bio/index.js +3 -0
- package/dist/tools/bio/keys.d.ts +11 -0
- package/dist/tools/bio/keys.js +15 -0
- package/dist/tools/bio/search-clinical-trials.d.ts +1 -1
- package/dist/tools/bio/search-patents.d.ts +46 -0
- package/dist/tools/bio/search-patents.js +134 -0
- package/dist/tools/bio/search-regulatory-approvals.d.ts +49 -0
- package/dist/tools/bio/search-regulatory-approvals.js +158 -0
- 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/lib/amass-client.d.ts +586 -0
- package/dist/tools/lib/amass-client.js +492 -0
- package/dist/tools/lib/amass-config.d.ts +2 -0
- package/dist/tools/lib/amass-config.js +10 -0
- 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 +3 -1
- 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,395 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The heatmap: a matrix of one value over the pairs of two category columns, with annotation strips.
|
|
3
|
+
*
|
|
4
|
+
* The canonical expression heatmap is the pheatmap and ComplexHeatmap figure: the genes down the y axis in the
|
|
5
|
+
* order of the run, the samples along x, a z-score on a diverging ramp symmetric at zero, and one strip of
|
|
6
|
+
* category colors over the matrix for each covariate of the samples, with a legend for each strip. The cells
|
|
7
|
+
* stand apart by a thin white gap, and the axes draw no line. A matrix of many samples hides its sample names,
|
|
8
|
+
* and the axis title states the count of samples in their place.
|
|
9
|
+
*
|
|
10
|
+
* A distance matrix draws on the sequential ramp, whose dark end is the low value. Thus the diagonal of zero
|
|
11
|
+
* distance draws dark, and two far samples draw light, as the DESeq2 sample-distance figure draws them.
|
|
12
|
+
*
|
|
13
|
+
* The run gives the order of each axis through the `orderBy` of its channel, or through the tree of the axis.
|
|
14
|
+
* A tree is the edge table of a clustering. The axis takes its depth-first leaf order, and the figure draws it
|
|
15
|
+
* as a dendrogram of elbows: the tree of x above the strips, and the tree of y at the left of the row names.
|
|
16
|
+
*
|
|
17
|
+
* A pair of categories holds one value. A repeated pair is a refusal, and never a silent sum. A track column
|
|
18
|
+
* holds one value for each x category, and a category with two values is a refusal.
|
|
19
|
+
*
|
|
20
|
+
* pheatmap names each row. Thus a matrix of more rows than the default body holds at one line of text each
|
|
21
|
+
* states a taller body, up to the largest body. A taller body grows the column exports in the same ratio.
|
|
22
|
+
* Past the largest body, and in a column export whose rows are shorter than a line of text, a row name that
|
|
23
|
+
* overlaps its neighbor hides.
|
|
24
|
+
*/
|
|
25
|
+
import { err, ok } from "neverthrow";
|
|
26
|
+
import { channelColumn, channelOrder, channelTransform } from "../../contracts/report-blocks.js";
|
|
27
|
+
import { declaredForColumn } from "../../contracts/report-reference.js";
|
|
28
|
+
import { CHART_BODY_PX, CHART_BODY_MAX_PX, CHART_INK, CHART_PAGE_TEXT_PX, CHART_SLOT_LIMIT, COLOR_SCALE_BAND_PCT, MUTED_CHART_COLOR } from "../design.js";
|
|
29
|
+
import { axisNameFields, categoricalPalette, categoryAxisTitle, categoryName, chartProblem, colorScale, columnPresent, continuousScale, orderQuickCategories, toNumber, transformColumn, valueAxisTitle, withFigureBody, Y_AXIS_NAME_GAP, } from "./common.js";
|
|
30
|
+
import { checkTree } from "./dendrogram.js";
|
|
31
|
+
/** The count of x categories past which the matrix hides the x labels and states the count in the axis title. */
|
|
32
|
+
export const HEATMAP_LABEL_LIMIT = 40;
|
|
33
|
+
/** The white gap between two cells, in pixels. */
|
|
34
|
+
export const HEATMAP_CELL_GAP_PX = 1;
|
|
35
|
+
/** The height of one annotation strip, in pixels. */
|
|
36
|
+
export const HEATMAP_STRIP_PX = 12;
|
|
37
|
+
/** The height of one line of the strip legends, and the gap between the band of the legends and the strips, in pixels. */
|
|
38
|
+
export const TRACK_LEGEND_LINE_PX = 24;
|
|
39
|
+
export const TRACK_BAND_GAP_PX = 14;
|
|
40
|
+
/** The gap over the legends, and between the strips and the matrix, in pixels. */
|
|
41
|
+
const TRACK_GAP_PX = 6;
|
|
42
|
+
/** The height of one matrix row on the page, in pixels: one line of the page text with a gap. */
|
|
43
|
+
export const HEATMAP_ROW_PX = 16;
|
|
44
|
+
/** The top margin of a matrix with no strip, in pixels: the top margin of the layout discipline on the default body. */
|
|
45
|
+
const PLAIN_TOP_PX = 32;
|
|
46
|
+
/**
|
|
47
|
+
* The band under a matrix in a taller body, in pixels: the axis title and its gap, and one line of level
|
|
48
|
+
* labels. A turned label adds its turned length.
|
|
49
|
+
*/
|
|
50
|
+
const TITLE_BAND_PX = 44;
|
|
51
|
+
const LEVEL_LABEL_PX = 18;
|
|
52
|
+
/** The count of x categories past which the layout discipline turns the x labels 45 degrees, and past which 90. */
|
|
53
|
+
const TURN_45_COUNT = 10;
|
|
54
|
+
const TURN_90_COUNT = 20;
|
|
55
|
+
/** The right margin of the grid beside the color scale. */
|
|
56
|
+
const COLOR_SCALE_GRID_RIGHT = `${COLOR_SCALE_BAND_PCT}%`;
|
|
57
|
+
/**
|
|
58
|
+
* The width of one character of a row label, as a share of the text size. The left margin of a matrix with
|
|
59
|
+
* strips holds its longest row label, thus the strips and the matrix start at one edge.
|
|
60
|
+
*/
|
|
61
|
+
const LABEL_CHARACTER_SHARE = 0.6;
|
|
62
|
+
const LABEL_MARGIN_MIN_PX = 40;
|
|
63
|
+
const LABEL_MARGIN_PAD_PX = 16;
|
|
64
|
+
/** The size of one entry of a strip legend, in pixels. */
|
|
65
|
+
const TRACK_LEGEND_ITEM_PX = 10;
|
|
66
|
+
/** The depth of the band of one dendrogram, in pixels: the height of the tree of x, and the width of the tree of y. */
|
|
67
|
+
export const DENDROGRAM_BAND_PX = 48;
|
|
68
|
+
/** The gap between the leaves of a dendrogram and the strips, the matrix, or the row names, in pixels. */
|
|
69
|
+
const DENDROGRAM_GAP_PX = 4;
|
|
70
|
+
/** The top margin over the tree of x where no strip legend sits above it, and the margin at the left of the tree of y, in pixels. */
|
|
71
|
+
const DENDROGRAM_TOP_PX = 12;
|
|
72
|
+
const DENDROGRAM_SIDE_PX = 8;
|
|
73
|
+
/** The width of a dendrogram branch, in pixels. */
|
|
74
|
+
const DENDROGRAM_LINE_PX = 1;
|
|
75
|
+
/** The axis parts that the matrix hides: the line and the ticks. */
|
|
76
|
+
const LINELESS_AXIS = { axisLine: { show: false }, axisTick: { show: false } };
|
|
77
|
+
/** The heatmap. */
|
|
78
|
+
export const HEATMAP_FIGURE = {
|
|
79
|
+
reads: new Set(["x", "y", "value", "tracks", "trees"]),
|
|
80
|
+
derive: deriveHeatmap,
|
|
81
|
+
};
|
|
82
|
+
/** The plain column of one category channel of the matrix. A transform would make its categories numbers, thus it refuses. */
|
|
83
|
+
function categoryColumn(block, rows, context, member, channel) {
|
|
84
|
+
if (channelTransform(channel) !== undefined) {
|
|
85
|
+
return err(chartProblem(block.id, `The "${member}" channel of the heatmap names categories, thus it takes no transform.`));
|
|
86
|
+
}
|
|
87
|
+
return present(block.id, rows, context, channelColumn(channel));
|
|
88
|
+
}
|
|
89
|
+
/** The column, where one row at least holds it. A table with no row holds each column. */
|
|
90
|
+
function present(blockId, rows, context, column) {
|
|
91
|
+
if (rows.length > 0 && !columnPresent(column, rows, context.columns)) {
|
|
92
|
+
return err(chartProblem(blockId, `The column "${column}" is absent from every row.`));
|
|
93
|
+
}
|
|
94
|
+
return ok(column);
|
|
95
|
+
}
|
|
96
|
+
/** The key of one pair. The key carries the type of each cell, thus the number `1` and the text `"1"` stay two categories. */
|
|
97
|
+
function pairKey(xCell, yCell) {
|
|
98
|
+
return `${cellKey(xCell)} ${cellKey(yCell)}`;
|
|
99
|
+
}
|
|
100
|
+
/** The key of one cell. */
|
|
101
|
+
function cellKey(cell) {
|
|
102
|
+
return `${typeof cell}:${String(cell)}`;
|
|
103
|
+
}
|
|
104
|
+
function deriveHeatmap(block, rows, context) {
|
|
105
|
+
const encoding = block.encoding ?? {};
|
|
106
|
+
for (const member of ["x", "y", "value"]) {
|
|
107
|
+
if (encoding[member] === undefined) {
|
|
108
|
+
return err(chartProblem(block.id, `The heatmap chart needs a column for the "${member}" channel.`));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const xChannel = encoding.x;
|
|
112
|
+
const yChannel = encoding.y;
|
|
113
|
+
const valueChannel = encoding.value;
|
|
114
|
+
if (channelOrder(valueChannel) !== undefined) {
|
|
115
|
+
return err(chartProblem(block.id, 'The "value" channel draws no category axis, thus it takes no "orderBy".'));
|
|
116
|
+
}
|
|
117
|
+
const x = categoryColumn(block, rows, context, "x", xChannel);
|
|
118
|
+
if (x.isErr())
|
|
119
|
+
return err(x.error);
|
|
120
|
+
const y = categoryColumn(block, rows, context, "y", yChannel);
|
|
121
|
+
if (y.isErr())
|
|
122
|
+
return err(y.error);
|
|
123
|
+
const valueColumn = present(block.id, rows, context, channelColumn(valueChannel));
|
|
124
|
+
if (valueColumn.isErr())
|
|
125
|
+
return err(valueColumn.error);
|
|
126
|
+
const transform = channelTransform(valueChannel);
|
|
127
|
+
const valueName = transform === undefined ? valueColumn.value : `${transform}(${valueColumn.value})`;
|
|
128
|
+
const values = transform === undefined ? rows.map((row) => toNumber(row[valueColumn.value])) : transformColumn(rows, valueColumn.value, transform);
|
|
129
|
+
const xOrdered = axisOrder(block, rows, context, "x", x.value, xChannel);
|
|
130
|
+
if (xOrdered.isErr())
|
|
131
|
+
return err(xOrdered.error);
|
|
132
|
+
const yOrdered = axisOrder(block, rows, context, "y", y.value, yChannel);
|
|
133
|
+
if (yOrdered.isErr())
|
|
134
|
+
return err(yOrdered.error);
|
|
135
|
+
const xCategories = xOrdered.value.order;
|
|
136
|
+
const yCategories = yOrdered.value.order;
|
|
137
|
+
const xTree = xOrdered.value.tree;
|
|
138
|
+
const yTree = yOrdered.value.tree;
|
|
139
|
+
const slots = xCategories.length * yCategories.length;
|
|
140
|
+
if (slots > CHART_SLOT_LIMIT) {
|
|
141
|
+
return err(chartProblem(block.id, `The chart holds ${xCategories.length} categories and ${yCategories.length} groups, thus ${slots} slots. A chart holds ${CHART_SLOT_LIMIT} slots at most, thus a table of fewer categories or fewer groups serves the reader.`));
|
|
142
|
+
}
|
|
143
|
+
const cells = new Map();
|
|
144
|
+
for (const [index, row] of rows.entries()) {
|
|
145
|
+
const key = pairKey(row[x.value], row[y.value]);
|
|
146
|
+
if (cells.has(key)) {
|
|
147
|
+
return err(chartProblem(block.id, `The heatmap holds the pair (${String(row[x.value])}, ${String(row[y.value])}) more than one time.`));
|
|
148
|
+
}
|
|
149
|
+
cells.set(key, values[index]);
|
|
150
|
+
}
|
|
151
|
+
const data = [];
|
|
152
|
+
const finite = [];
|
|
153
|
+
for (let xi = 0; xi < xCategories.length; xi++) {
|
|
154
|
+
for (let yi = 0; yi < yCategories.length; yi++) {
|
|
155
|
+
const value = cells.get(pairKey(xCategories[xi], yCategories[yi])) ?? null;
|
|
156
|
+
data.push([xi, yi, value]);
|
|
157
|
+
if (value !== null)
|
|
158
|
+
finite.push(value);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const hidden = xCategories.length > HEATMAP_LABEL_LIMIT;
|
|
162
|
+
const xTitle = hidden
|
|
163
|
+
? `${categoryAxisTitle(context.labels, x.value) ?? categoryName(x.value)} (n = ${xCategories.length})`
|
|
164
|
+
: categoryAxisTitle(context.labels, x.value);
|
|
165
|
+
const xAxis = {
|
|
166
|
+
type: "category",
|
|
167
|
+
data: xCategories.map(String),
|
|
168
|
+
...axisNameFields("x", xTitle),
|
|
169
|
+
...LINELESS_AXIS,
|
|
170
|
+
...(hidden ? { axisLabel: { show: false } } : {}),
|
|
171
|
+
};
|
|
172
|
+
const yAxis = {
|
|
173
|
+
type: "category",
|
|
174
|
+
// The runtime draws the first category of a y axis at the origin. The inverted axis puts the first row
|
|
175
|
+
// of the order at the top, thus the matrix reads down as the run ordered it.
|
|
176
|
+
inverse: true,
|
|
177
|
+
data: yCategories.map(String),
|
|
178
|
+
...axisNameFields("y", categoryAxisTitle(context.labels, y.value)),
|
|
179
|
+
...LINELESS_AXIS,
|
|
180
|
+
// A matrix of many rows holds more names than its height fits. The runtime then shows each name that
|
|
181
|
+
// clears its neighbor, and never draws two names over each other.
|
|
182
|
+
axisLabel: { hideOverlap: true },
|
|
183
|
+
};
|
|
184
|
+
const scaleMap = {
|
|
185
|
+
type: "continuous",
|
|
186
|
+
...colorScale(continuousScale(finite), valueAxisTitle(context.labels, valueName), valueName, declaredForColumn(context.meanings, valueColumn.value)),
|
|
187
|
+
};
|
|
188
|
+
const matrix = { type: "heatmap", itemStyle: { borderColor: "#ffffff", borderWidth: HEATMAP_CELL_GAP_PX }, data };
|
|
189
|
+
const tracks = encoding.tracks ?? [];
|
|
190
|
+
const band = underBand(xCategories, hidden);
|
|
191
|
+
if (tracks.length === 0 && xTree === undefined && yTree === undefined) {
|
|
192
|
+
const body = PLAIN_TOP_PX + yCategories.length * HEATMAP_ROW_PX + band;
|
|
193
|
+
if (body <= CHART_BODY_PX) {
|
|
194
|
+
return ok({ xAxis, yAxis, visualMap: scaleMap, series: [matrix], grid: { right: COLOR_SCALE_GRID_RIGHT } });
|
|
195
|
+
}
|
|
196
|
+
return ok(withFigureBody({
|
|
197
|
+
xAxis,
|
|
198
|
+
yAxis,
|
|
199
|
+
visualMap: scaleMap,
|
|
200
|
+
series: [matrix],
|
|
201
|
+
grid: { right: COLOR_SCALE_GRID_RIGHT, top: PLAIN_TOP_PX, bottom: bandShare(band, body) },
|
|
202
|
+
}, body));
|
|
203
|
+
}
|
|
204
|
+
const strips = trackStrips(block, rows, context, x.value, xCategories, tracks);
|
|
205
|
+
if (strips.isErr())
|
|
206
|
+
return err(strips.error);
|
|
207
|
+
const titles = strips.value.map((strip) => strip.title);
|
|
208
|
+
let longest = 0;
|
|
209
|
+
for (const name of [...yCategories.map(String), ...titles])
|
|
210
|
+
longest = Math.max(longest, name.length);
|
|
211
|
+
const labelMargin = Math.max(LABEL_MARGIN_MIN_PX, Math.round(longest * CHART_PAGE_TEXT_PX * LABEL_CHARACTER_SHARE) + LABEL_MARGIN_PAD_PX);
|
|
212
|
+
// The tree of y sits at the left of the row names, and a y title sits between the tree and the names.
|
|
213
|
+
const yTitled = yAxis.name !== undefined;
|
|
214
|
+
const yTreeBand = yTree === undefined ? 0 : DENDROGRAM_SIDE_PX + DENDROGRAM_BAND_PX + DENDROGRAM_GAP_PX + (yTitled ? CHART_PAGE_TEXT_PX + Y_AXIS_NAME_GAP : 0);
|
|
215
|
+
const left = yTreeBand + labelMargin;
|
|
216
|
+
const headTop = tracks.length > 0 ? TRACK_GAP_PX + tracks.length * TRACK_LEGEND_LINE_PX + TRACK_BAND_GAP_PX : xTree !== undefined ? DENDROGRAM_TOP_PX : PLAIN_TOP_PX;
|
|
217
|
+
const stripTop = headTop + (xTree === undefined ? 0 : DENDROGRAM_BAND_PX + DENDROGRAM_GAP_PX);
|
|
218
|
+
const matrixTop = tracks.length > 0 ? stripTop + tracks.length * HEATMAP_STRIP_PX + TRACK_GAP_PX : stripTop;
|
|
219
|
+
const body = matrixTop + yCategories.length * HEATMAP_ROW_PX + band;
|
|
220
|
+
const tall = body > CHART_BODY_PX;
|
|
221
|
+
const bottom = tall ? bandShare(band, body) : hidden ? "12%" : xCategories.length > TURN_45_COUNT ? "25%" : "20%";
|
|
222
|
+
const palette = categoricalPalette(strips.value.reduce((sum, strip) => sum + strip.values.length, 0));
|
|
223
|
+
const stripSeries = [];
|
|
224
|
+
const legends = [];
|
|
225
|
+
let offset = 0;
|
|
226
|
+
for (const [place, strip] of strips.value.entries()) {
|
|
227
|
+
stripSeries.push({
|
|
228
|
+
type: "heatmap",
|
|
229
|
+
name: strip.title,
|
|
230
|
+
xAxisIndex: 1,
|
|
231
|
+
yAxisIndex: 1,
|
|
232
|
+
itemStyle: { borderColor: "#ffffff", borderWidth: HEATMAP_CELL_GAP_PX },
|
|
233
|
+
data: strip.codes.flatMap((code, xi) => (code === null ? [] : [[xi, place, code]])),
|
|
234
|
+
});
|
|
235
|
+
legends.push({
|
|
236
|
+
type: "piecewise",
|
|
237
|
+
seriesIndex: 1 + place,
|
|
238
|
+
dimension: 2,
|
|
239
|
+
orient: "horizontal",
|
|
240
|
+
// Beside a tree of y, the legends start over the row names, thus a column export holds each entry.
|
|
241
|
+
left: yTree === undefined ? left : yTreeBand,
|
|
242
|
+
top: TRACK_GAP_PX + place * TRACK_LEGEND_LINE_PX,
|
|
243
|
+
itemWidth: TRACK_LEGEND_ITEM_PX,
|
|
244
|
+
itemHeight: TRACK_LEGEND_ITEM_PX,
|
|
245
|
+
selectedMode: false,
|
|
246
|
+
showLabel: true,
|
|
247
|
+
text: [strip.title],
|
|
248
|
+
// The strips share one palette, and a value past its end takes the muted color.
|
|
249
|
+
pieces: strip.values.map((value, code) => ({ value: code, label: categoryName(value), color: palette[offset + code] ?? MUTED_CHART_COLOR })),
|
|
250
|
+
});
|
|
251
|
+
offset += strip.values.length;
|
|
252
|
+
}
|
|
253
|
+
// Beside a tree of y, each row name starts at the leaves. An export draws its text smaller than the page,
|
|
254
|
+
// thus a name that ends at the matrix would leave a gap between the leaves and the names.
|
|
255
|
+
const leafLabels = yTree === undefined ? {} : { axisLabel: { hideOverlap: true, align: "left", margin: labelMargin } };
|
|
256
|
+
const grid = [{ left, right: COLOR_SCALE_GRID_RIGHT, top: matrixTop, bottom }];
|
|
257
|
+
const xAxes = [xAxis];
|
|
258
|
+
const yAxes = [{ ...yAxis, ...leafLabels }];
|
|
259
|
+
if (tracks.length > 0) {
|
|
260
|
+
grid.push({ left, right: COLOR_SCALE_GRID_RIGHT, top: stripTop, height: tracks.length * HEATMAP_STRIP_PX });
|
|
261
|
+
xAxes.push({ type: "category", gridIndex: 1, data: xCategories.map(String), ...LINELESS_AXIS, axisLabel: { show: false } });
|
|
262
|
+
yAxes.push({ type: "category", gridIndex: 1, inverse: true, data: titles, ...LINELESS_AXIS, ...leafLabels });
|
|
263
|
+
}
|
|
264
|
+
const treeSeries = [];
|
|
265
|
+
if (xTree !== undefined) {
|
|
266
|
+
const index = grid.length;
|
|
267
|
+
grid.push({ left, right: COLOR_SCALE_GRID_RIGHT, top: headTop, height: DENDROGRAM_BAND_PX });
|
|
268
|
+
xAxes.push(leafAxis(index, xCategories.length, false));
|
|
269
|
+
yAxes.push(heightAxis(index, xTree.height, false));
|
|
270
|
+
treeSeries.push(dendrogramSeries(index, xTree, "x"));
|
|
271
|
+
}
|
|
272
|
+
if (yTree !== undefined) {
|
|
273
|
+
const index = grid.length;
|
|
274
|
+
grid.push({ left: DENDROGRAM_SIDE_PX, width: DENDROGRAM_BAND_PX, top: matrixTop, bottom });
|
|
275
|
+
xAxes.push(heightAxis(index, yTree.height, true));
|
|
276
|
+
yAxes.push(leafAxis(index, yCategories.length, true));
|
|
277
|
+
treeSeries.push(dendrogramSeries(index, yTree, "y"));
|
|
278
|
+
}
|
|
279
|
+
const framed = {
|
|
280
|
+
grid,
|
|
281
|
+
xAxis: xAxes,
|
|
282
|
+
yAxis: yAxes,
|
|
283
|
+
visualMap: [{ ...scaleMap, seriesIndex: 0 }, ...legends],
|
|
284
|
+
series: [matrix, ...stripSeries, ...treeSeries],
|
|
285
|
+
legend: { show: false },
|
|
286
|
+
};
|
|
287
|
+
return ok(tall ? withFigureBody(framed, body) : framed);
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* The categories of one axis in their order, and the checked tree of the axis where the block binds one.
|
|
291
|
+
*
|
|
292
|
+
* A tree gives the leaf order, thus an `orderBy` on the same axis names a second order, and it refuses.
|
|
293
|
+
*/
|
|
294
|
+
function axisOrder(block, rows, context, axis, column, channel) {
|
|
295
|
+
const tree = context.trees?.[axis];
|
|
296
|
+
const declared = channelOrder(channel);
|
|
297
|
+
if (tree !== undefined && declared !== undefined) {
|
|
298
|
+
return err(chartProblem(block.id, `The ${axis} axis takes the leaf order of its tree, thus the "${axis}" channel takes no "orderBy".`));
|
|
299
|
+
}
|
|
300
|
+
const categories = orderQuickCategories(block.id, rows, column, declared);
|
|
301
|
+
if (categories.isErr())
|
|
302
|
+
return err(categories.error);
|
|
303
|
+
if (tree === undefined)
|
|
304
|
+
return ok({ order: categories.value });
|
|
305
|
+
return checkTree(block.id, axis, tree, categories.value).map((checked) => ({ order: checked.order, tree: checked }));
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* The hidden value axis of a dendrogram along the categories of the matrix. A category axis draws category `i`
|
|
309
|
+
* at the middle of band `i`, thus a range of half a band past each end puts each leaf under its column or
|
|
310
|
+
* beside its row. The tree of y reads down, as the matrix reads down.
|
|
311
|
+
*/
|
|
312
|
+
function leafAxis(gridIndex, count, inverse) {
|
|
313
|
+
return { type: "value", gridIndex, min: -0.5, max: count - 0.5, ...(inverse ? { inverse: true } : {}), show: false };
|
|
314
|
+
}
|
|
315
|
+
/** The hidden value axis of the heights of a dendrogram. The tree of y turns it, thus the root sits at the left and the leaves face the row names. */
|
|
316
|
+
function heightAxis(gridIndex, height, inverse) {
|
|
317
|
+
return { type: "value", gridIndex, min: 0, max: height > 0 ? height : 1, ...(inverse ? { inverse: true } : {}), show: false };
|
|
318
|
+
}
|
|
319
|
+
/** The branches of one dendrogram: one elbow of lines for each edge, in the order of the tree table. */
|
|
320
|
+
function dendrogramSeries(gridIndex, tree, axis) {
|
|
321
|
+
return {
|
|
322
|
+
type: "lines",
|
|
323
|
+
coordinateSystem: "cartesian2d",
|
|
324
|
+
xAxisIndex: gridIndex,
|
|
325
|
+
yAxisIndex: gridIndex,
|
|
326
|
+
polyline: true,
|
|
327
|
+
silent: true,
|
|
328
|
+
clip: false,
|
|
329
|
+
lineStyle: { color: CHART_INK, width: DENDROGRAM_LINE_PX, opacity: 1 },
|
|
330
|
+
data: tree.elbows.map((elbow) => ({ coords: axis === "x" ? elbow.map((point) => [point[0], point[1]]) : elbow.map((point) => [point[1], point[0]]) })),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* The band under the matrix of a taller body, in pixels: the axis title, and the x labels as the layout
|
|
335
|
+
* discipline turns them. A hidden label takes no room.
|
|
336
|
+
*/
|
|
337
|
+
function underBand(xCategories, hidden) {
|
|
338
|
+
if (hidden)
|
|
339
|
+
return TITLE_BAND_PX;
|
|
340
|
+
if (xCategories.length <= TURN_45_COUNT)
|
|
341
|
+
return TITLE_BAND_PX + LEVEL_LABEL_PX;
|
|
342
|
+
let longest = 0;
|
|
343
|
+
for (const category of xCategories)
|
|
344
|
+
longest = Math.max(longest, String(category).length);
|
|
345
|
+
const length = longest * CHART_PAGE_TEXT_PX * LABEL_CHARACTER_SHARE;
|
|
346
|
+
return TITLE_BAND_PX + Math.ceil(xCategories.length <= TURN_90_COUNT ? length * Math.SQRT1_2 : length);
|
|
347
|
+
}
|
|
348
|
+
/** The band under the matrix as a share of the body, which stops at the largest body. */
|
|
349
|
+
function bandShare(band, body) {
|
|
350
|
+
return `${Math.round((band / Math.min(body, CHART_BODY_MAX_PX)) * 1e6) / 1e4}%`;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* The strip of each track column. The categories of a strip take the order in which the x axis meets them.
|
|
354
|
+
* An x category that no row gives a value draws no cell in the strip. A track with no value for any x category
|
|
355
|
+
* refuses, because its legend would hold no entry, and the runtime then draws a legend of number ranges.
|
|
356
|
+
*/
|
|
357
|
+
function trackStrips(block, rows, context, xColumn, xCategories, tracks) {
|
|
358
|
+
const strips = [];
|
|
359
|
+
for (const track of tracks) {
|
|
360
|
+
const column = present(block.id, rows, context, track);
|
|
361
|
+
if (column.isErr())
|
|
362
|
+
return err(column.error);
|
|
363
|
+
const held = new Map();
|
|
364
|
+
for (const row of rows) {
|
|
365
|
+
const category = row[xColumn];
|
|
366
|
+
const value = row[track];
|
|
367
|
+
if (category === undefined || value === undefined || value === "")
|
|
368
|
+
continue;
|
|
369
|
+
const key = cellKey(category);
|
|
370
|
+
const prior = held.get(key);
|
|
371
|
+
if (prior !== undefined && cellKey(prior) !== cellKey(value)) {
|
|
372
|
+
return err(chartProblem(block.id, `The track column "${track}" holds two values, "${String(prior)}" and "${String(value)}", for the x category "${String(category)}". A track holds one value for each x category.`));
|
|
373
|
+
}
|
|
374
|
+
held.set(key, value);
|
|
375
|
+
}
|
|
376
|
+
const along = xCategories.map((category) => held.get(cellKey(category)) ?? null);
|
|
377
|
+
const values = [];
|
|
378
|
+
const codeOf = new Map();
|
|
379
|
+
for (const value of along) {
|
|
380
|
+
if (value === null || codeOf.has(cellKey(value)))
|
|
381
|
+
continue;
|
|
382
|
+
codeOf.set(cellKey(value), values.length);
|
|
383
|
+
values.push(value);
|
|
384
|
+
}
|
|
385
|
+
if (values.length === 0 && xCategories.length > 0) {
|
|
386
|
+
return err(chartProblem(block.id, `The track column "${track}" holds no value for any x category.`));
|
|
387
|
+
}
|
|
388
|
+
strips.push({
|
|
389
|
+
title: declaredForColumn(context.labels, track) ?? categoryName(track),
|
|
390
|
+
values,
|
|
391
|
+
codes: along.map((value) => (value === null ? null : (codeOf.get(cellKey(value)) ?? null))),
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
return ok(strips);
|
|
395
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The figure registry: one module for each chart type that draws the canonical figure of its field.
|
|
3
|
+
*
|
|
4
|
+
* A canonical figure often draws more than the series of one grid over one table: a table under the plot, a
|
|
5
|
+
* text column beside it, stacked panels, a glyph in a cell, or a track from a second table. Thus a figure is a
|
|
6
|
+
* module of its own, and `deriveRaw` in `chart.ts` asks this registry before every other path. A base type
|
|
7
|
+
* with no module keeps its fixed rule. A preset with no module refuses, because no other path draws it.
|
|
8
|
+
*
|
|
9
|
+
* A module states the members of the block that it reads. The dispatch refuses each other member before the
|
|
10
|
+
* module runs, thus a module never meets a channel that it would drop in silence.
|
|
11
|
+
*
|
|
12
|
+
* A module is a pure function of the block, the rows, and the context. It reads no clock, no random value, and
|
|
13
|
+
* no locale, and its option holds no function, because the option rides to the page as inline JSON.
|
|
14
|
+
*/
|
|
15
|
+
import type { Result } from "neverthrow";
|
|
16
|
+
import type { ChartBlock, ChartComposition, ChartEncoding, ChartType } from "../../contracts/report-blocks.js";
|
|
17
|
+
import type { PresetAxisTitles, PresetClassification } from "../chart-presets.js";
|
|
18
|
+
import type { ChartRow, EchartOption } from "../chart.js";
|
|
19
|
+
import type { RenderProblem } from "../types.js";
|
|
20
|
+
import type { ColumnLabels, ColumnMeanings, FigureStatistic } from "./common.js";
|
|
21
|
+
/**
|
|
22
|
+
* The presets that exist as a figure module alone. No composition expands them, thus each one draws only
|
|
23
|
+
* through its module.
|
|
24
|
+
*/
|
|
25
|
+
export declare const FIGURE_PRESET_TYPES: readonly ["pca", "embedding", "dotplot", "forest", "roc", "qq", "gsea", "oncoprint", "lollipop", "upset", "sankey", "locuszoom"];
|
|
26
|
+
/** One preset that exists as a figure module alone. */
|
|
27
|
+
export type FigurePresetType = (typeof FIGURE_PRESET_TYPES)[number];
|
|
28
|
+
/** True when the chart type exists as a figure module alone. */
|
|
29
|
+
export declare function isFigurePresetType(chartType: ChartType): chartType is FigurePresetType;
|
|
30
|
+
/**
|
|
31
|
+
* The chart types that draw through a figure module alone: each figure preset, the heatmap, and the four
|
|
32
|
+
* presets of the first grammar. No base rule draws the annotation strips of a heatmap, thus the heatmap keeps
|
|
33
|
+
* no base rule beside its module. The volcano, the MA plot, the Manhattan plot, and the Kaplan-Meier curve
|
|
34
|
+
* each draw elements that no composition holds, thus none of them keeps an expansion beside its module.
|
|
35
|
+
*/
|
|
36
|
+
export declare const MODULE_ONLY_TYPES: readonly ["pca", "embedding", "dotplot", "forest", "roc", "qq", "gsea", "oncoprint", "lollipop", "upset", "sankey", "locuszoom", "heatmap", "volcano", "ma", "manhattan", "km"];
|
|
37
|
+
/** One chart type that draws through a figure module alone. */
|
|
38
|
+
export type ModuleOnlyType = (typeof MODULE_ONLY_TYPES)[number];
|
|
39
|
+
/** True when the chart type draws through a figure module alone. */
|
|
40
|
+
export declare function isModuleOnlyType(chartType: ChartType): chartType is ModuleOnlyType;
|
|
41
|
+
/**
|
|
42
|
+
* One member of a chart block that a figure can read: a channel of the quick-path encoding, the statistics,
|
|
43
|
+
* the track, the trees, or the focus.
|
|
44
|
+
*/
|
|
45
|
+
export type FigureMember = keyof ChartEncoding | "statistics" | "track" | "trees" | "focus";
|
|
46
|
+
/**
|
|
47
|
+
* The resolved track of a figure: the rows of the second table, its declared column order, and the columns
|
|
48
|
+
* that the block names in it. The labels and the meanings are the declarations of the track binding.
|
|
49
|
+
*/
|
|
50
|
+
export interface FigureTrack {
|
|
51
|
+
readonly rows: readonly ChartRow[];
|
|
52
|
+
readonly columns?: readonly string[];
|
|
53
|
+
readonly start: string;
|
|
54
|
+
readonly end: string;
|
|
55
|
+
readonly label: string;
|
|
56
|
+
readonly length?: string;
|
|
57
|
+
readonly labels: ColumnLabels;
|
|
58
|
+
readonly meanings: ColumnMeanings;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The resolved tree of one category axis: the edge rows of the tree table, its declared column order, and the
|
|
62
|
+
* three columns that the block names in it. The labels and the meanings are the declarations of the tree
|
|
63
|
+
* binding.
|
|
64
|
+
*/
|
|
65
|
+
export interface FigureTree {
|
|
66
|
+
readonly rows: readonly ChartRow[];
|
|
67
|
+
readonly columns?: readonly string[];
|
|
68
|
+
readonly parent: string;
|
|
69
|
+
readonly child: string;
|
|
70
|
+
readonly height: string;
|
|
71
|
+
readonly labels: ColumnLabels;
|
|
72
|
+
readonly meanings: ColumnMeanings;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The parts of a preset that a figure hands to the composition machinery: the semantic axis titles, and the
|
|
76
|
+
* per-row classification where the figure splits the rows itself. `colorOnTop` draws the points of a
|
|
77
|
+
* continuous color in the ascending order of the color, inline and on the page, thus the high values draw on
|
|
78
|
+
* top.
|
|
79
|
+
*
|
|
80
|
+
* `keepsRowsInline` keeps the rows of the composition inline whatever their size. A figure that rewrites the
|
|
81
|
+
* items of the composition output states it, because the page builds each item from the cells and never
|
|
82
|
+
* from the rewrite.
|
|
83
|
+
*/
|
|
84
|
+
export interface FigureComposeExtras {
|
|
85
|
+
readonly preset?: PresetAxisTitles;
|
|
86
|
+
readonly classification?: PresetClassification;
|
|
87
|
+
readonly colorOnTop?: boolean;
|
|
88
|
+
readonly keepsRowsInline?: boolean;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* What one figure reads beside its block and its rows.
|
|
92
|
+
*
|
|
93
|
+
* - `labels` and `meanings` are the declarations of the bound table, and `columns` is its declared column
|
|
94
|
+
* order.
|
|
95
|
+
* - `statistics` holds each statistic of the block in block order, with its resolved value and its shown text.
|
|
96
|
+
* A block with no statistic gives an empty list.
|
|
97
|
+
* - `track` is the resolved second table, where the block binds one.
|
|
98
|
+
* - `trees` holds the resolved tree of each axis that the block binds one for.
|
|
99
|
+
* - `textPx` is the text size of the page. The export scales each `graphic` text from it.
|
|
100
|
+
* - `compose` derives one composition over the bound rows through the shared machinery of `chart.ts`. A dense
|
|
101
|
+
* figure builds its primary series here, thus the series reads the shared payload of its artifact past the
|
|
102
|
+
* inline bound. A figure can add series after the result, and each such series keeps its rows inline. A
|
|
103
|
+
* figure that puts a series before the result keeps the whole chart inline, because the page fills the
|
|
104
|
+
* leading series from the descriptors of the composition.
|
|
105
|
+
*/
|
|
106
|
+
export interface FigureContext {
|
|
107
|
+
readonly blockId: string;
|
|
108
|
+
readonly labels: ColumnLabels;
|
|
109
|
+
readonly meanings: ColumnMeanings;
|
|
110
|
+
readonly columns?: readonly string[];
|
|
111
|
+
readonly statistics: readonly FigureStatistic[];
|
|
112
|
+
readonly track?: FigureTrack;
|
|
113
|
+
readonly trees?: {
|
|
114
|
+
readonly x?: FigureTree;
|
|
115
|
+
readonly y?: FigureTree;
|
|
116
|
+
};
|
|
117
|
+
readonly textPx: number;
|
|
118
|
+
readonly compose: (composition: ChartComposition, extras?: FigureComposeExtras) => Result<EchartOption, RenderProblem>;
|
|
119
|
+
}
|
|
120
|
+
/** The derivation of one figure: the raw option, before the layout discipline and the figure rules. */
|
|
121
|
+
export type FigureDerivation = (block: ChartBlock, rows: readonly ChartRow[], context: FigureContext) => Result<EchartOption, RenderProblem>;
|
|
122
|
+
/** One figure module: the members of the block that it reads, and its derivation. */
|
|
123
|
+
export interface FigureModule {
|
|
124
|
+
readonly reads: ReadonlySet<FigureMember>;
|
|
125
|
+
readonly derive: FigureDerivation;
|
|
126
|
+
}
|
|
127
|
+
/** The figure module of each chart type that has one. */
|
|
128
|
+
export type FigureRegistry = Readonly<Partial<Record<ChartType, FigureModule>>>;
|
|
129
|
+
/** The registered figure modules. A module joins by one entry here, keyed by its chart type. */
|
|
130
|
+
export declare const FIGURE_MODULES: FigureRegistry;
|
|
131
|
+
/**
|
|
132
|
+
* The chart types that read each member that the canonical figures add. The refusal of such a member names
|
|
133
|
+
* these types, and the teaching text of the member names the same ones.
|
|
134
|
+
*/
|
|
135
|
+
export declare const FIGURE_MEMBER_READERS: Readonly<Record<"shape" | "p" | "censor" | "risk" | "hit" | "metric" | "tracks" | "statistics" | "track" | "trees", readonly ChartType[]>>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The figure registry: one module for each chart type that draws the canonical figure of its field.
|
|
3
|
+
*
|
|
4
|
+
* A canonical figure often draws more than the series of one grid over one table: a table under the plot, a
|
|
5
|
+
* text column beside it, stacked panels, a glyph in a cell, or a track from a second table. Thus a figure is a
|
|
6
|
+
* module of its own, and `deriveRaw` in `chart.ts` asks this registry before every other path. A base type
|
|
7
|
+
* with no module keeps its fixed rule. A preset with no module refuses, because no other path draws it.
|
|
8
|
+
*
|
|
9
|
+
* A module states the members of the block that it reads. The dispatch refuses each other member before the
|
|
10
|
+
* module runs, thus a module never meets a channel that it would drop in silence.
|
|
11
|
+
*
|
|
12
|
+
* A module is a pure function of the block, the rows, and the context. It reads no clock, no random value, and
|
|
13
|
+
* no locale, and its option holds no function, because the option rides to the page as inline JSON.
|
|
14
|
+
*/
|
|
15
|
+
import { GSEA_FIGURE } from "./gsea.js";
|
|
16
|
+
import { LOLLIPOP_FIGURE } from "./lollipop.js";
|
|
17
|
+
import { ONCOPRINT_FIGURE } from "./oncoprint.js";
|
|
18
|
+
import { FOREST_FIGURE } from "./forest.js";
|
|
19
|
+
import { KM_FIGURE } from "./km.js";
|
|
20
|
+
import { ROC_FIGURE } from "./roc.js";
|
|
21
|
+
import { MA_FIGURE } from "./ma.js";
|
|
22
|
+
import { MANHATTAN_FIGURE } from "./manhattan.js";
|
|
23
|
+
import { QQ_FIGURE } from "./qq.js";
|
|
24
|
+
import { VOLCANO_FIGURE } from "./volcano.js";
|
|
25
|
+
import { DOTPLOT_FIGURE } from "./dotplot.js";
|
|
26
|
+
import { EMBEDDING_FIGURE } from "./embedding.js";
|
|
27
|
+
import { HEATMAP_FIGURE } from "./heatmap.js";
|
|
28
|
+
import { PCA_FIGURE } from "./pca.js";
|
|
29
|
+
import { UPSET_FIGURE } from "./upset.js";
|
|
30
|
+
import { SANKEY_FIGURE } from "./sankey.js";
|
|
31
|
+
import { LOCUSZOOM_FIGURE } from "./locuszoom.js";
|
|
32
|
+
/**
|
|
33
|
+
* The presets that exist as a figure module alone. No composition expands them, thus each one draws only
|
|
34
|
+
* through its module.
|
|
35
|
+
*/
|
|
36
|
+
export const FIGURE_PRESET_TYPES = [
|
|
37
|
+
"pca",
|
|
38
|
+
"embedding",
|
|
39
|
+
"dotplot",
|
|
40
|
+
"forest",
|
|
41
|
+
"roc",
|
|
42
|
+
"qq",
|
|
43
|
+
"gsea",
|
|
44
|
+
"oncoprint",
|
|
45
|
+
"lollipop",
|
|
46
|
+
"upset",
|
|
47
|
+
"sankey",
|
|
48
|
+
"locuszoom",
|
|
49
|
+
];
|
|
50
|
+
/** True when the chart type exists as a figure module alone. */
|
|
51
|
+
export function isFigurePresetType(chartType) {
|
|
52
|
+
return FIGURE_PRESET_TYPES.includes(chartType);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The chart types that draw through a figure module alone: each figure preset, the heatmap, and the four
|
|
56
|
+
* presets of the first grammar. No base rule draws the annotation strips of a heatmap, thus the heatmap keeps
|
|
57
|
+
* no base rule beside its module. The volcano, the MA plot, the Manhattan plot, and the Kaplan-Meier curve
|
|
58
|
+
* each draw elements that no composition holds, thus none of them keeps an expansion beside its module.
|
|
59
|
+
*/
|
|
60
|
+
export const MODULE_ONLY_TYPES = [...FIGURE_PRESET_TYPES, "heatmap", "volcano", "ma", "manhattan", "km"];
|
|
61
|
+
/** True when the chart type draws through a figure module alone. */
|
|
62
|
+
export function isModuleOnlyType(chartType) {
|
|
63
|
+
return MODULE_ONLY_TYPES.includes(chartType);
|
|
64
|
+
}
|
|
65
|
+
/** The registered figure modules. A module joins by one entry here, keyed by its chart type. */
|
|
66
|
+
export const FIGURE_MODULES = {
|
|
67
|
+
km: KM_FIGURE,
|
|
68
|
+
forest: FOREST_FIGURE,
|
|
69
|
+
roc: ROC_FIGURE,
|
|
70
|
+
gsea: GSEA_FIGURE,
|
|
71
|
+
oncoprint: ONCOPRINT_FIGURE,
|
|
72
|
+
lollipop: LOLLIPOP_FIGURE,
|
|
73
|
+
volcano: VOLCANO_FIGURE,
|
|
74
|
+
ma: MA_FIGURE,
|
|
75
|
+
manhattan: MANHATTAN_FIGURE,
|
|
76
|
+
qq: QQ_FIGURE,
|
|
77
|
+
pca: PCA_FIGURE,
|
|
78
|
+
embedding: EMBEDDING_FIGURE,
|
|
79
|
+
dotplot: DOTPLOT_FIGURE,
|
|
80
|
+
heatmap: HEATMAP_FIGURE,
|
|
81
|
+
upset: UPSET_FIGURE,
|
|
82
|
+
sankey: SANKEY_FIGURE,
|
|
83
|
+
locuszoom: LOCUSZOOM_FIGURE,
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* The chart types that read each member that the canonical figures add. The refusal of such a member names
|
|
87
|
+
* these types, and the teaching text of the member names the same ones.
|
|
88
|
+
*/
|
|
89
|
+
export const FIGURE_MEMBER_READERS = {
|
|
90
|
+
shape: ["pca", "scatter"],
|
|
91
|
+
p: ["ma", "forest"],
|
|
92
|
+
censor: ["km"],
|
|
93
|
+
risk: ["km"],
|
|
94
|
+
hit: ["gsea"],
|
|
95
|
+
metric: ["gsea", "locuszoom"],
|
|
96
|
+
tracks: ["heatmap", "oncoprint"],
|
|
97
|
+
statistics: ["km", "roc", "qq", "gsea"],
|
|
98
|
+
track: ["lollipop", "locuszoom"],
|
|
99
|
+
trees: ["heatmap"],
|
|
100
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
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 type { FigureModule } from "./index.js";
|
|
15
|
+
/** The Kaplan-Meier survival figure. */
|
|
16
|
+
export declare const KM_FIGURE: FigureModule;
|