@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,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The UpSet plot: the intersections of some sets, as upset.app, UpSetR, and ComplexHeatmap draw them.
|
|
3
|
+
*
|
|
4
|
+
* The figure reads one row for each member of each set: the `x` column names the element, and the `group`
|
|
5
|
+
* column names the set. Three grids line up:
|
|
6
|
+
*
|
|
7
|
+
* - The top grid draws the size of each intersection as a bar, with its count over the bar.
|
|
8
|
+
* - The middle grid draws the dot matrix: one column for each intersection and one row for each set. A dark
|
|
9
|
+
* dot marks each member set of the intersection, a light dot marks each other set, and a line joins the
|
|
10
|
+
* member dots of the column.
|
|
11
|
+
* - The left grid draws the size of each set as a bar that grows to the left, on the rows of the matrix. The
|
|
12
|
+
* set names sit between these bars and the matrix.
|
|
13
|
+
*
|
|
14
|
+
* The figure computes two named summaries, and each one reads the rows of the bound table alone:
|
|
15
|
+
*
|
|
16
|
+
* - The size of a set: the count of its distinct elements.
|
|
17
|
+
* - The exact intersection of an element: the combination of every set that holds it. The size of an
|
|
18
|
+
* intersection is the count of its elements, thus each element counts in one intersection alone.
|
|
19
|
+
*
|
|
20
|
+
* The sets sort by size, the largest at the top, and a tie keeps the first appearance. The intersections sort
|
|
21
|
+
* by size, largest first, then by degree, smallest first, then by the set order of their members. The largest
|
|
22
|
+
* intersections draw alone past the cap, and the title under the matrix states the count of the others.
|
|
23
|
+
*
|
|
24
|
+
* The dots and the count text of one column must fit the column at each export size, thus one media rule for
|
|
25
|
+
* each export size states the dot size and the turn of the count text there.
|
|
26
|
+
*/
|
|
27
|
+
import { err, ok } from "neverthrow";
|
|
28
|
+
import { CHART_BODY_PX, CHART_EXPORT_SIZES, CHART_INK, CHART_PAGE_TEXT_PX, CHART_PAGE_WIDTH_PX } from "../design.js";
|
|
29
|
+
import { axisNameFields, chartProblem, demandedColumn, valueAxis } from "./common.js";
|
|
30
|
+
/**
|
|
31
|
+
* The most sets that one figure draws. Each set takes one row of the matrix, and the rows of twelve sets hold
|
|
32
|
+
* one line of the column-export text each.
|
|
33
|
+
*/
|
|
34
|
+
export const UPSET_SET_LIMIT = 12;
|
|
35
|
+
/** The most intersections that one figure draws. The largest ones draw, and the title states the count of the others. */
|
|
36
|
+
export const UPSET_INTERSECTION_LIMIT = 30;
|
|
37
|
+
/** The titles of the two value axes. */
|
|
38
|
+
const INTERSECTION_TITLE = "Intersection size";
|
|
39
|
+
const SET_TITLE = "Set size";
|
|
40
|
+
/** The light dot of a set that is not a member of the intersection. */
|
|
41
|
+
const ABSENT_DOT_COLOR = "#e0e0e0";
|
|
42
|
+
/** The text between the member names of one intersection. */
|
|
43
|
+
const MEMBER_SEPARATOR = " & ";
|
|
44
|
+
/** The places of the grids, in percent of the chart: the top margin, the gap between the bars and the matrix, and the left and right margins. */
|
|
45
|
+
const TOP_PCT = 9;
|
|
46
|
+
const GAP_PCT = 3;
|
|
47
|
+
const SIDE_PCT = 3;
|
|
48
|
+
/** The width of the set-size bars, in percent of the chart. */
|
|
49
|
+
const SET_BAR_PCT = 16;
|
|
50
|
+
/** The height of one set row, and of the whole matrix, at most, in percent of the chart. */
|
|
51
|
+
const ROW_MAX_PCT = 7;
|
|
52
|
+
const MATRIX_MAX_PCT = 50;
|
|
53
|
+
/** The share of the smaller band of one cell that its dot fills, and the bounds of a dot, in pixels. */
|
|
54
|
+
const DOT_SHARE = 0.7;
|
|
55
|
+
const DOT_MIN_PX = 2;
|
|
56
|
+
const DOT_MAX_PX = 16;
|
|
57
|
+
/** The width of the line that joins the member dots, as a share of the dot size, and its smallest width in pixels. */
|
|
58
|
+
const JOIN_SHARE = 0.25;
|
|
59
|
+
const JOIN_MIN_PX = 1;
|
|
60
|
+
/** The width of one character of the chart text, as a share of the text size. The layout estimates a name width with it. */
|
|
61
|
+
const CHARACTER_SHARE = 0.6;
|
|
62
|
+
/** The gap between a set name and the matrix, and between the name and the set bars, in pixels. */
|
|
63
|
+
const NAME_GAP_PX = 8;
|
|
64
|
+
/** The lines of text under the matrix: the tick labels and the title of the set bars, with their gaps. */
|
|
65
|
+
const BOTTOM_LINES = 3.2;
|
|
66
|
+
/** The UpSet module: the element and the set. */
|
|
67
|
+
export const UPSET_FIGURE = { reads: new Set(["x", "group"]), derive: deriveUpset };
|
|
68
|
+
/** True when a cell holds no text. */
|
|
69
|
+
function emptyCell(cell) {
|
|
70
|
+
return cell === undefined || String(cell).trim() === "";
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The set sizes and the exact intersections of one membership table.
|
|
74
|
+
*
|
|
75
|
+
* A row with an empty set cell names an element in no set, and it adds nothing: the empty intersection draws
|
|
76
|
+
* no column. The error is the detail of the refusal.
|
|
77
|
+
*/
|
|
78
|
+
export function upsetSummary(rows, element, set) {
|
|
79
|
+
const setsOf = new Map();
|
|
80
|
+
const sizes = new Map();
|
|
81
|
+
for (const row of rows) {
|
|
82
|
+
const setCell = row[set];
|
|
83
|
+
if (emptyCell(setCell))
|
|
84
|
+
continue;
|
|
85
|
+
const setName = String(setCell).trim();
|
|
86
|
+
if (emptyCell(row[element])) {
|
|
87
|
+
return err(`A row of the set "${setName}" names no element in the "${element}" column.`);
|
|
88
|
+
}
|
|
89
|
+
const elementName = String(row[element]).trim();
|
|
90
|
+
const held = setsOf.get(elementName) ?? new Set();
|
|
91
|
+
if (held.has(setName)) {
|
|
92
|
+
return err(`The element "${elementName}" is a member of the set "${setName}" in two rows. The table holds one row for each member of each set.`);
|
|
93
|
+
}
|
|
94
|
+
held.add(setName);
|
|
95
|
+
setsOf.set(elementName, held);
|
|
96
|
+
sizes.set(setName, (sizes.get(setName) ?? 0) + 1);
|
|
97
|
+
}
|
|
98
|
+
if (sizes.size === 0) {
|
|
99
|
+
return err(`The table names no set in the "${set}" column, thus the figure has no intersection to draw.`);
|
|
100
|
+
}
|
|
101
|
+
if (sizes.size > UPSET_SET_LIMIT) {
|
|
102
|
+
return err(`The table names ${sizes.size} sets. An upset figure draws ${UPSET_SET_LIMIT} sets at most, thus a table of fewer sets serves the reader.`);
|
|
103
|
+
}
|
|
104
|
+
// The map keeps the first appearance, and the sort is stable, thus a tie keeps that order.
|
|
105
|
+
const sets = [...sizes.entries()].map(([name, size]) => ({ name, size })).sort((a, b) => b.size - a.size);
|
|
106
|
+
const places = new Map(sets.map((entry, place) => [entry.name, place]));
|
|
107
|
+
const counts = new Map();
|
|
108
|
+
for (const held of setsOf.values()) {
|
|
109
|
+
const members = [...held].map((name) => places.get(name) ?? -1).sort((a, b) => a - b);
|
|
110
|
+
const key = members.join(",");
|
|
111
|
+
const entry = counts.get(key) ?? { members, size: 0 };
|
|
112
|
+
entry.size += 1;
|
|
113
|
+
counts.set(key, entry);
|
|
114
|
+
}
|
|
115
|
+
const intersections = [...counts.values()].sort((a, b) => b.size - a.size || a.members.length - b.members.length || compareMembers(a.members, b.members));
|
|
116
|
+
return ok({ sets, intersections });
|
|
117
|
+
}
|
|
118
|
+
/** Compare two member lists of one degree by the set order: the list whose first different set comes first sorts first. */
|
|
119
|
+
function compareMembers(a, b) {
|
|
120
|
+
for (let place = 0; place < Math.min(a.length, b.length); place += 1) {
|
|
121
|
+
if (a[place] !== b[place])
|
|
122
|
+
return a[place] - b[place];
|
|
123
|
+
}
|
|
124
|
+
return a.length - b.length;
|
|
125
|
+
}
|
|
126
|
+
/** The page box and the box of each export size. */
|
|
127
|
+
const PAGE_BOX = { widthPx: CHART_PAGE_WIDTH_PX, heightPx: CHART_BODY_PX, textPx: CHART_PAGE_TEXT_PX };
|
|
128
|
+
const EXPORT_SIZES = [CHART_EXPORT_SIZES.single, CHART_EXPORT_SIZES.double, CHART_EXPORT_SIZES.slide];
|
|
129
|
+
/**
|
|
130
|
+
* The places of the grids for some sets, and set names of some length.
|
|
131
|
+
*
|
|
132
|
+
* The set names take a band between the set bars and the matrix, and the tick labels with the title of the set
|
|
133
|
+
* bars take a band under the matrix. Each band holds its text at the page and at the single column, thus the
|
|
134
|
+
* percent layout serves each export.
|
|
135
|
+
*/
|
|
136
|
+
function upsetLayout(setCount, longestName) {
|
|
137
|
+
const boxes = [PAGE_BOX, CHART_EXPORT_SIZES.single];
|
|
138
|
+
const namePct = Math.max(...boxes.map((box) => ((longestName * box.textPx * CHARACTER_SHARE + 2 * NAME_GAP_PX) / box.widthPx) * 100));
|
|
139
|
+
const bottomPct = Math.max(...boxes.map((box) => ((BOTTOM_LINES * box.textPx + NAME_GAP_PX) / box.heightPx) * 100));
|
|
140
|
+
const rowPct = Math.min(ROW_MAX_PCT, MATRIX_MAX_PCT / setCount);
|
|
141
|
+
return {
|
|
142
|
+
matrixLeft: rounded(SIDE_PCT + SET_BAR_PCT + namePct),
|
|
143
|
+
matrixTop: rounded(100 - bottomPct - rowPct * setCount),
|
|
144
|
+
matrixBottom: rounded(bottomPct),
|
|
145
|
+
rowPct,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/** The dot size of one box: a share of the smaller band of one cell, within the bounds. */
|
|
149
|
+
function dotPx(layout, columns, box) {
|
|
150
|
+
const column = (((100 - layout.matrixLeft - SIDE_PCT) / 100) * box.widthPx) / columns;
|
|
151
|
+
const row = (layout.rowPct / 100) * box.heightPx;
|
|
152
|
+
return rounded(Math.min(DOT_MAX_PX, Math.max(DOT_MIN_PX, DOT_SHARE * Math.min(column, row))));
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The count text over the intersection bars at one box. A text wider than its column turns upright, thus the
|
|
156
|
+
* counts of many narrow columns stand side by side and none hides the next.
|
|
157
|
+
*/
|
|
158
|
+
function countLabel(layout, columns, digits, box) {
|
|
159
|
+
const column = (((100 - layout.matrixLeft - SIDE_PCT) / 100) * box.widthPx) / columns;
|
|
160
|
+
const upright = digits * box.textPx * CHARACTER_SHARE > column;
|
|
161
|
+
return upright
|
|
162
|
+
? { show: true, position: "top", color: CHART_INK, rotate: 90, align: "left", verticalAlign: "middle", distance: 4 }
|
|
163
|
+
: { show: true, position: "top", color: CHART_INK, distance: 2 };
|
|
164
|
+
}
|
|
165
|
+
/** One layout percentage, rounded, thus a float residue never reaches the option. */
|
|
166
|
+
function rounded(value) {
|
|
167
|
+
return Math.round(value * 1e4) / 1e4;
|
|
168
|
+
}
|
|
169
|
+
/** A percent of the chart as the option states it. */
|
|
170
|
+
function pct(value) {
|
|
171
|
+
return `${rounded(value)}%`;
|
|
172
|
+
}
|
|
173
|
+
function deriveUpset(block, rows, context) {
|
|
174
|
+
const source = { blockId: context.blockId, rows, ...(context.columns !== undefined ? { columns: context.columns } : {}) };
|
|
175
|
+
const encoding = block.encoding ?? {};
|
|
176
|
+
const element = demandedColumn("upset", encoding, "x", source);
|
|
177
|
+
if (element.isErr())
|
|
178
|
+
return err(element.error);
|
|
179
|
+
const set = demandedColumn("upset", encoding, "group", source);
|
|
180
|
+
if (set.isErr())
|
|
181
|
+
return err(set.error);
|
|
182
|
+
if (rows.length === 0) {
|
|
183
|
+
return err(chartProblem(context.blockId, "The upset figure has no rows. It needs one row for each member of each set."));
|
|
184
|
+
}
|
|
185
|
+
const summary = upsetSummary(rows, element.value, set.value);
|
|
186
|
+
if (summary.isErr())
|
|
187
|
+
return err(chartProblem(context.blockId, summary.error));
|
|
188
|
+
const { sets, intersections } = summary.value;
|
|
189
|
+
const drawn = intersections.slice(0, UPSET_INTERSECTION_LIMIT);
|
|
190
|
+
const hidden = intersections.length - drawn.length;
|
|
191
|
+
const setNames = sets.map((entry) => entry.name);
|
|
192
|
+
const columnNames = drawn.map((entry) => entry.members.map((place) => setNames[place]).join(MEMBER_SEPARATOR));
|
|
193
|
+
const layout = upsetLayout(sets.length, Math.max(...setNames.map((name) => name.length)));
|
|
194
|
+
const digits = Math.max(...drawn.map((entry) => String(entry.size).length));
|
|
195
|
+
const members = [];
|
|
196
|
+
const absent = [];
|
|
197
|
+
const joins = [];
|
|
198
|
+
for (const [column, entry] of drawn.entries()) {
|
|
199
|
+
const held = new Set(entry.members);
|
|
200
|
+
for (let row = 0; row < sets.length; row += 1) {
|
|
201
|
+
(held.has(row) ? members : absent).push([column, row]);
|
|
202
|
+
}
|
|
203
|
+
if (entry.members.length > 1) {
|
|
204
|
+
joins.push({
|
|
205
|
+
coords: [
|
|
206
|
+
[column, entry.members[0]],
|
|
207
|
+
[column, entry.members[entry.members.length - 1]],
|
|
208
|
+
],
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const pageDot = dotPx(layout, drawn.length, PAGE_BOX);
|
|
213
|
+
const matrixGrid = { left: pct(layout.matrixLeft), right: pct(SIDE_PCT), top: pct(layout.matrixTop), bottom: pct(layout.matrixBottom) };
|
|
214
|
+
const hiddenAxis = { axisLabel: { show: false }, axisTick: { show: false }, axisLine: { show: false } };
|
|
215
|
+
const series = [
|
|
216
|
+
{
|
|
217
|
+
id: "intersections",
|
|
218
|
+
type: "bar",
|
|
219
|
+
xAxisIndex: 0,
|
|
220
|
+
yAxisIndex: 0,
|
|
221
|
+
barCategoryGap: "30%",
|
|
222
|
+
itemStyle: { color: CHART_INK },
|
|
223
|
+
label: countLabel(layout, drawn.length, digits, PAGE_BOX),
|
|
224
|
+
tooltip: { formatter: "{b}: {c}" },
|
|
225
|
+
data: drawn.map((entry) => entry.size),
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
id: "sets",
|
|
229
|
+
type: "bar",
|
|
230
|
+
xAxisIndex: 2,
|
|
231
|
+
yAxisIndex: 2,
|
|
232
|
+
barCategoryGap: "40%",
|
|
233
|
+
itemStyle: { color: CHART_INK },
|
|
234
|
+
tooltip: { formatter: "{b}: {c}" },
|
|
235
|
+
data: sets.map((entry) => entry.size),
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
id: "absent",
|
|
239
|
+
type: "scatter",
|
|
240
|
+
xAxisIndex: 1,
|
|
241
|
+
yAxisIndex: 1,
|
|
242
|
+
silent: true,
|
|
243
|
+
symbolSize: pageDot,
|
|
244
|
+
itemStyle: { color: ABSENT_DOT_COLOR },
|
|
245
|
+
z: 1,
|
|
246
|
+
data: absent,
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
id: "joins",
|
|
250
|
+
type: "lines",
|
|
251
|
+
coordinateSystem: "cartesian2d",
|
|
252
|
+
xAxisIndex: 1,
|
|
253
|
+
yAxisIndex: 1,
|
|
254
|
+
silent: true,
|
|
255
|
+
lineStyle: { color: CHART_INK, width: joinPx(pageDot), opacity: 1 },
|
|
256
|
+
z: 2,
|
|
257
|
+
data: joins,
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
id: "members",
|
|
261
|
+
type: "scatter",
|
|
262
|
+
xAxisIndex: 1,
|
|
263
|
+
yAxisIndex: 1,
|
|
264
|
+
silent: true,
|
|
265
|
+
symbolSize: pageDot,
|
|
266
|
+
itemStyle: { color: CHART_INK, opacity: 1 },
|
|
267
|
+
z: 3,
|
|
268
|
+
data: members,
|
|
269
|
+
},
|
|
270
|
+
];
|
|
271
|
+
return ok({
|
|
272
|
+
tooltip: { trigger: "item" },
|
|
273
|
+
legend: { show: false },
|
|
274
|
+
grid: [
|
|
275
|
+
{ left: matrixGrid.left, right: matrixGrid.right, top: pct(TOP_PCT), bottom: pct(100 - layout.matrixTop + GAP_PCT) },
|
|
276
|
+
matrixGrid,
|
|
277
|
+
{ left: pct(SIDE_PCT), width: pct(SET_BAR_PCT), top: matrixGrid.top, bottom: matrixGrid.bottom },
|
|
278
|
+
],
|
|
279
|
+
xAxis: [
|
|
280
|
+
{ type: "category", gridIndex: 0, data: columnNames, axisLabel: { show: false }, axisTick: { show: false } },
|
|
281
|
+
{
|
|
282
|
+
type: "category",
|
|
283
|
+
gridIndex: 1,
|
|
284
|
+
data: columnNames,
|
|
285
|
+
...hiddenAxis,
|
|
286
|
+
...(hidden > 0 ? { ...axisNameFields("x", `Intersections: ${drawn.length} of ${intersections.length} shown`), nameGap: NAME_GAP_PX } : {}),
|
|
287
|
+
},
|
|
288
|
+
{ ...valueAxis("x", SET_TITLE), gridIndex: 2, inverse: true, splitNumber: 2, minInterval: 1, axisLabel: { rotate: 0 } },
|
|
289
|
+
],
|
|
290
|
+
yAxis: [
|
|
291
|
+
{ ...valueAxis("y", INTERSECTION_TITLE), gridIndex: 0, splitNumber: 3, minInterval: 1 },
|
|
292
|
+
{ type: "category", gridIndex: 1, inverse: true, data: setNames, axisTick: { show: false }, axisLine: { show: false } },
|
|
293
|
+
{ type: "category", gridIndex: 2, inverse: true, data: setNames, show: false },
|
|
294
|
+
],
|
|
295
|
+
series,
|
|
296
|
+
media: EXPORT_SIZES.map((size) => {
|
|
297
|
+
const dot = dotPx(layout, drawn.length, size);
|
|
298
|
+
return {
|
|
299
|
+
query: { minWidth: size.widthPx, maxWidth: size.widthPx, minHeight: size.heightPx, maxHeight: size.heightPx },
|
|
300
|
+
option: {
|
|
301
|
+
series: [
|
|
302
|
+
{ label: countLabel(layout, drawn.length, digits, size) },
|
|
303
|
+
{},
|
|
304
|
+
{ symbolSize: dot },
|
|
305
|
+
{ lineStyle: { width: joinPx(dot) } },
|
|
306
|
+
{ symbolSize: dot },
|
|
307
|
+
],
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
}),
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
/** The width of the line that joins the member dots of one column, at one dot size. */
|
|
314
|
+
function joinPx(dot) {
|
|
315
|
+
return rounded(Math.max(JOIN_MIN_PX, dot * JOIN_SHARE));
|
|
316
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The volcano figure, as DESeq2 papers and EnhancedVolcano draw it: the effect on x, the transformed p on y,
|
|
3
|
+
* the null points in gray under the two signal sides, and the names of the most significant signal genes.
|
|
4
|
+
*
|
|
5
|
+
* - The null points draw under the signal points, in gray and smaller. The down side takes the blue of the
|
|
6
|
+
* palette and the up side takes its vermilion.
|
|
7
|
+
* - The legend names each side with the count of its points.
|
|
8
|
+
* - The ten most significant signal points that carry a name show it. The derivation places each name clear of
|
|
9
|
+
* the other names and of the points, with a leader line to its point. The names place again for the plot and
|
|
10
|
+
* the text size of each export, and a name that finds no place clear of the earlier names does not print
|
|
11
|
+
* there.
|
|
12
|
+
* - The effect axis is symmetric around zero, thus the two sides read at one scale.
|
|
13
|
+
* - A row with no p draws no point and counts on no side.
|
|
14
|
+
* - A row whose stored p is 0 draws an upward triangle at the top of the plotted range, on the side of its
|
|
15
|
+
* effect. It counts on that side, and it takes a name before each finite p.
|
|
16
|
+
*
|
|
17
|
+
* The classification and the guides read one pair of cuts, the declared thresholds or the preset defaults.
|
|
18
|
+
* Thus the color split lands on the drawn lines. The points come from the composition machinery, thus a
|
|
19
|
+
* table of many thousand genes reads the shared payload.
|
|
20
|
+
*/
|
|
21
|
+
import type { FigureModule } from "./index.js";
|
|
22
|
+
/** The count of signal points that show their name. */
|
|
23
|
+
export declare const VOLCANO_LABEL_COUNT = 10;
|
|
24
|
+
/** The volcano figure module. */
|
|
25
|
+
export declare const VOLCANO_FIGURE: FigureModule;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The volcano figure, as DESeq2 papers and EnhancedVolcano draw it: the effect on x, the transformed p on y,
|
|
3
|
+
* the null points in gray under the two signal sides, and the names of the most significant signal genes.
|
|
4
|
+
*
|
|
5
|
+
* - The null points draw under the signal points, in gray and smaller. The down side takes the blue of the
|
|
6
|
+
* palette and the up side takes its vermilion.
|
|
7
|
+
* - The legend names each side with the count of its points.
|
|
8
|
+
* - The ten most significant signal points that carry a name show it. The derivation places each name clear of
|
|
9
|
+
* the other names and of the points, with a leader line to its point. The names place again for the plot and
|
|
10
|
+
* the text size of each export, and a name that finds no place clear of the earlier names does not print
|
|
11
|
+
* there.
|
|
12
|
+
* - The effect axis is symmetric around zero, thus the two sides read at one scale.
|
|
13
|
+
* - A row with no p draws no point and counts on no side.
|
|
14
|
+
* - A row whose stored p is 0 draws an upward triangle at the top of the plotted range, on the side of its
|
|
15
|
+
* effect. It counts on that side, and it takes a name before each finite p.
|
|
16
|
+
*
|
|
17
|
+
* The classification and the guides read one pair of cuts, the declared thresholds or the preset defaults.
|
|
18
|
+
* Thus the color split lands on the drawn lines. The points come from the composition machinery, thus a
|
|
19
|
+
* table of many thousand genes reads the shared payload.
|
|
20
|
+
*/
|
|
21
|
+
import { err, ok } from "neverthrow";
|
|
22
|
+
import { channelColumn } from "../../contracts/report-blocks.js";
|
|
23
|
+
import { expandVolcano, VOLCANO_EFFECT_THRESHOLD, volcanoAxisTitles } from "../chart-presets.js";
|
|
24
|
+
import { CHART_PALETTE, MUTED_CHART_COLOR } from "../design.js";
|
|
25
|
+
import { transformColumn } from "./common.js";
|
|
26
|
+
import { axisOf, BELOW_RESOLUTION_SYMBOL, BELOW_RESOLUTION_SYMBOL_PX, BELOW_RESOLUTION_TOOLTIP, belowResolutionHeight, belowResolutionRows, countText, DENSE_NULL_OPACITY, DENSE_NULL_SYMBOL_PX, DENSE_NULL_Z, DENSE_SIGNAL_OPACITY, DENSE_SIGNAL_SYMBOL_PX, demandedChannel, largest, niceCeiling, plottedValues, pointLayer, POINT_NAMES, pointNameSeries, seriesOf, sizedSeries, symmetricRange, } from "./dense.js";
|
|
27
|
+
import { placeLeaderNames } from "./label-room.js";
|
|
28
|
+
/** The count of signal points that show their name. */
|
|
29
|
+
export const VOLCANO_LABEL_COUNT = 10;
|
|
30
|
+
/** The sides of a point where a volcano name sits, in the order that the figure tries them. */
|
|
31
|
+
const VOLCANO_NAME_SIDES = ["right", "left", "top"];
|
|
32
|
+
/** The color of the down side and of the up side. The null side takes the muted color. */
|
|
33
|
+
const SIDE_COLORS = [CHART_PALETTE[0], CHART_PALETTE[1]];
|
|
34
|
+
/** The members of a volcano block that the figure reads. */
|
|
35
|
+
const READS = new Set(["x", "y", "label"]);
|
|
36
|
+
/**
|
|
37
|
+
* The volcano classification with the count of each category in its name.
|
|
38
|
+
*
|
|
39
|
+
* The counts read the plotted pair of each row through the preset rule, which is the rule that splits the
|
|
40
|
+
* series. Thus each name counts the points of its own series.
|
|
41
|
+
*/
|
|
42
|
+
function countedClassification(base, xs, ys) {
|
|
43
|
+
const counts = new Map(base.categories.map((category) => [category.name, 0]));
|
|
44
|
+
for (let index = 0; index < xs.length; index += 1) {
|
|
45
|
+
const category = base.categoryOf(xs[index], ys[index], index);
|
|
46
|
+
if (category !== undefined)
|
|
47
|
+
counts.set(category, (counts.get(category) ?? 0) + 1);
|
|
48
|
+
}
|
|
49
|
+
const names = new Map(base.categories.map((category) => [category.name, `${category.name} (${countText(counts.get(category.name) ?? 0)})`]));
|
|
50
|
+
return {
|
|
51
|
+
categories: base.categories.map((category) => ({ ...category, name: names.get(category.name) ?? category.name })),
|
|
52
|
+
categoryOf: (x, y, index) => {
|
|
53
|
+
const category = base.categoryOf(x, y, index);
|
|
54
|
+
return category === undefined ? undefined : names.get(category);
|
|
55
|
+
},
|
|
56
|
+
rule: base.rule,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The rows whose point shows its name: the most significant signal rows that carry a name, at most
|
|
61
|
+
* `VOLCANO_LABEL_COUNT`. The order reads the plotted p, a stored zero first, and a tie keeps the order of the
|
|
62
|
+
* rows.
|
|
63
|
+
*/
|
|
64
|
+
function labeledRows(rows, labelColumn, base, xs, ys, zeros) {
|
|
65
|
+
if (labelColumn === undefined)
|
|
66
|
+
return [];
|
|
67
|
+
const signal = new Set(base.categories.filter((category) => !category.muted).map((category) => category.name));
|
|
68
|
+
const candidates = [];
|
|
69
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
70
|
+
const category = base.categoryOf(xs[index], ys[index], index);
|
|
71
|
+
const name = rows[index][labelColumn];
|
|
72
|
+
if (category !== undefined && signal.has(category) && name !== undefined && String(name).trim() !== "")
|
|
73
|
+
candidates.push(index);
|
|
74
|
+
}
|
|
75
|
+
const rank = (index) => (zeros.has(index) ? Number.POSITIVE_INFINITY : (ys[index] ?? 0));
|
|
76
|
+
candidates.sort((a, b) => (rank(a) === rank(b) ? a - b : rank(b) - rank(a)));
|
|
77
|
+
return candidates.slice(0, VOLCANO_LABEL_COUNT);
|
|
78
|
+
}
|
|
79
|
+
function deriveVolcano(block, rows, context) {
|
|
80
|
+
const encoding = block.encoding ?? {};
|
|
81
|
+
const x = demandedChannel(context.blockId, "volcano", encoding, "x");
|
|
82
|
+
if (x.isErr())
|
|
83
|
+
return err(x.error);
|
|
84
|
+
const y = demandedChannel(context.blockId, "volcano", encoding, "y");
|
|
85
|
+
if (y.isErr())
|
|
86
|
+
return err(y.error);
|
|
87
|
+
const expansion = expandVolcano(x.value, y.value, encoding, block.thresholds);
|
|
88
|
+
const base = expansion.classification;
|
|
89
|
+
const xs = plottedValues(rows, x.value);
|
|
90
|
+
const pColumn = channelColumn(y.value);
|
|
91
|
+
const finite = transformColumn(rows, pColumn, "neg_log10");
|
|
92
|
+
const peak = largest(finite.flatMap((value, index) => (value !== null && xs[index] !== null ? [value] : []))) ?? 0;
|
|
93
|
+
const zeroRows = belowResolutionRows(rows, pColumn, xs);
|
|
94
|
+
const zeros = new Set(zeroRows);
|
|
95
|
+
const zeroY = belowResolutionHeight(peak, base.rule.kind === "volcano" ? base.rule.cut : 0);
|
|
96
|
+
// A stored zero draws at the top of the plotted range, thus the counts and the names read it there. The
|
|
97
|
+
// composition reads the transformed column, which drops it, and the figure draws it in a series of its own.
|
|
98
|
+
const ys = finite.map((value, index) => (zeros.has(index) ? zeroY : value));
|
|
99
|
+
const counted = countedClassification(base, xs, ys);
|
|
100
|
+
const composed = context.compose(expansion.composition, { preset: volcanoAxisTitles(x.value), classification: counted });
|
|
101
|
+
if (composed.isErr())
|
|
102
|
+
return err(composed.error);
|
|
103
|
+
const reach = largest(xs.flatMap((value, index) => (value !== null && ys[index] !== null ? [Math.abs(value)] : [])));
|
|
104
|
+
const effect = block.thresholds?.effect ?? VOLCANO_EFFECT_THRESHOLD;
|
|
105
|
+
const end = niceCeiling(Math.max(reach ?? 0, effect));
|
|
106
|
+
const option = composed.value;
|
|
107
|
+
const labelColumn = encoding.label;
|
|
108
|
+
const names = labelColumn === undefined
|
|
109
|
+
? []
|
|
110
|
+
: labeledRows(rows, labelColumn, base, xs, ys, zeros).map((index) => ({
|
|
111
|
+
x: xs[index] ?? 0,
|
|
112
|
+
y: ys[index] ?? 0,
|
|
113
|
+
text: String(rows[index][labelColumn]),
|
|
114
|
+
}));
|
|
115
|
+
const top = zeroRows.length > 0 ? Math.max(peak, zeroY) : peak;
|
|
116
|
+
// The runtime ends the p axis at its own round number at or past the peak. The round number of the figure
|
|
117
|
+
// is at or past it too, thus the names measure against a span at least as long as the drawn one.
|
|
118
|
+
const plot = { x: { min: -end, max: end }, y: { min: 0, max: niceCeiling(top) } };
|
|
119
|
+
const text = sizedSeries([POINT_NAMES], (frame) => pointNameSeries(names, placeLeaderNames(names, { xs, ys, pointPx: DENSE_SIGNAL_SYMBOL_PX }, plot, VOLCANO_NAME_SIDES, top, frame)));
|
|
120
|
+
const legend = typeof option.legend === "object" && option.legend !== null ? option.legend : {};
|
|
121
|
+
return ok({
|
|
122
|
+
...option,
|
|
123
|
+
...text.member,
|
|
124
|
+
// The legend names the three sides alone, and never the series of the point names.
|
|
125
|
+
legend: { ...legend, data: seriesOf(option).map((series) => series.name) },
|
|
126
|
+
xAxis: { ...axisOf(option, "xAxis"), ...symmetricRange(end) },
|
|
127
|
+
yAxis: { ...axisOf(option, "yAxis"), min: 0 },
|
|
128
|
+
series: [
|
|
129
|
+
...seriesOf(option).map((series, place) => ({ ...series, ...sideLayer(place, DENSE_SIGNAL_SYMBOL_PX, DENSE_NULL_SYMBOL_PX) })),
|
|
130
|
+
...zeroSeries(counted, zeroRows, xs, zeroY, labelColumn === undefined ? undefined : (index) => String(rows[index][labelColumn])),
|
|
131
|
+
...text.page,
|
|
132
|
+
],
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/** The style of the points of one side, by the place of the side: the two signal sides, then the null side. */
|
|
136
|
+
function sideLayer(place, signalPx, nullPx) {
|
|
137
|
+
return place < SIDE_COLORS.length
|
|
138
|
+
? pointLayer(SIDE_COLORS[place], signalPx, DENSE_SIGNAL_OPACITY)
|
|
139
|
+
: pointLayer(MUTED_CHART_COLOR, nullPx, DENSE_NULL_OPACITY, DENSE_NULL_Z);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The series of the stored zeros: one series for each side that holds one, under the name and in the style of
|
|
143
|
+
* that side, with the upward triangle at the drawn height. The series follow the points of the composition,
|
|
144
|
+
* thus they keep their rows inline and the points still read the shared payload.
|
|
145
|
+
*/
|
|
146
|
+
function zeroSeries(classification, zeroRows, xs, zeroY, nameOf) {
|
|
147
|
+
return classification.categories.flatMap((category, place) => {
|
|
148
|
+
const data = zeroRows
|
|
149
|
+
.filter((index) => classification.categoryOf(xs[index], zeroY, index) === category.name)
|
|
150
|
+
.map((index) => (nameOf === undefined ? [xs[index] ?? 0, zeroY] : { name: nameOf(index), value: [xs[index] ?? 0, zeroY] }));
|
|
151
|
+
if (data.length === 0)
|
|
152
|
+
return [];
|
|
153
|
+
return [
|
|
154
|
+
{
|
|
155
|
+
type: "scatter",
|
|
156
|
+
name: category.name,
|
|
157
|
+
symbol: BELOW_RESOLUTION_SYMBOL,
|
|
158
|
+
...sideLayer(place, BELOW_RESOLUTION_SYMBOL_PX, BELOW_RESOLUTION_SYMBOL_PX),
|
|
159
|
+
tooltip: BELOW_RESOLUTION_TOOLTIP,
|
|
160
|
+
data,
|
|
161
|
+
},
|
|
162
|
+
];
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
/** The volcano figure module. */
|
|
166
|
+
export const VOLCANO_FIGURE = { reads: READS, derive: deriveVolcano };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The hybrid SVG of a dense chart: the vector axes, text, legend, and guides, over one raster layer of the points.
|
|
3
|
+
*
|
|
4
|
+
* A chart past the point bound of the export gets no staged SVG, because a vector file of many thousands of
|
|
5
|
+
* points is too large. The page builds its SVG on a click instead. It draws the export option with the SVG
|
|
6
|
+
* renderer and the point data emptied, it draws the points alone on a canvas at 300 DPI, and it puts the canvas
|
|
7
|
+
* image into the SVG at the rectangle of the grid, under the vector marks. The server has no canvas, thus only
|
|
8
|
+
* the page builds this file.
|
|
9
|
+
*
|
|
10
|
+
* The rule of a point layer and the composition of the file hold one rule each in two twins: the TypeScript
|
|
11
|
+
* function and the page function. A shared test vector runs both, thus the two cannot drift in silence. The
|
|
12
|
+
* option transforms and the two draws exist on the page alone, because the server never builds the file.
|
|
13
|
+
*/
|
|
14
|
+
/** The box of one export in CSS pixels, and in millimeters for a column export. */
|
|
15
|
+
export interface SvgRootSize {
|
|
16
|
+
readonly widthPx: number;
|
|
17
|
+
readonly heightPx: number;
|
|
18
|
+
readonly widthMm?: number;
|
|
19
|
+
readonly heightMm?: number;
|
|
20
|
+
}
|
|
21
|
+
/** The raster image of the point layer and its rectangle in the pixel space of the SVG. */
|
|
22
|
+
export interface PointLayerImage {
|
|
23
|
+
readonly href: string;
|
|
24
|
+
readonly x: number;
|
|
25
|
+
readonly y: number;
|
|
26
|
+
readonly width: number;
|
|
27
|
+
readonly height: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* True when one series draws a point for each row: a `scatter` whose symbol draws and which shows no series
|
|
31
|
+
* label. A scatter of size zero, or one with a shown label, carries names, and names stay vector text.
|
|
32
|
+
*/
|
|
33
|
+
export declare function isPointLayer(series: unknown): boolean;
|
|
34
|
+
/** True when an option holds a point layer. The page can then build the hybrid SVG of the option. */
|
|
35
|
+
export declare function holdsPointLayer(option: Readonly<Record<string, unknown>>): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* The SVG text with its root at the millimeter size of a column. The view box keeps the pixel space, thus a
|
|
38
|
+
* vector editor opens the file at the column width. A size with no millimeter box leaves the root as it is.
|
|
39
|
+
*/
|
|
40
|
+
export declare function millimeterRoot(svg: string, size: SvgRootSize): string;
|
|
41
|
+
/**
|
|
42
|
+
* The hybrid SVG file: the vector file with one `<image>` element of the point layer, and the root at the
|
|
43
|
+
* millimeter size.
|
|
44
|
+
*
|
|
45
|
+
* The image goes first after the background rectangle, thus each vector mark draws over the points: the axes,
|
|
46
|
+
* the guides, the names, and the legend. The rectangle of the image is the grid rectangle, and the image
|
|
47
|
+
* stretches to it, because the raster drew the grid at that rectangle. A text that holds no SVG root passes
|
|
48
|
+
* through with no image.
|
|
49
|
+
*/
|
|
50
|
+
export declare function composeHybridSvg(svg: string, layer: PointLayerImage, size: SvgRootSize): string;
|
|
51
|
+
/**
|
|
52
|
+
* The page twin of the point-layer rule and of the composition, and the page build of the hybrid file, as
|
|
53
|
+
* browser source text.
|
|
54
|
+
*
|
|
55
|
+
* `reportHybridSvg(runtime, option, size)` is the entry point. It reads the chart runtime and the document of
|
|
56
|
+
* the page, and it gives the SVG text, or `null` when the option holds no point layer or the runtime gives no
|
|
57
|
+
* grid rectangle. The fragment reads `reportExportOption` of the renderer fragment, thus the bootstrap inlines
|
|
58
|
+
* that fragment first.
|
|
59
|
+
*
|
|
60
|
+
* The draws run in this order:
|
|
61
|
+
*
|
|
62
|
+
* 1. The vector draw takes the export option with the rows of each point layer emptied. Each point layer keeps
|
|
63
|
+
* each row that carries its own label, and two anchors with no symbol at the least and the greatest value of
|
|
64
|
+
* each dimension. Thus each axis and each color scale keeps the extent of the full rows.
|
|
65
|
+
* 2. The rectangle of each grid that holds a point layer comes from the runtime: a halving search of each edge
|
|
66
|
+
* with `containPixel` from a point inside the grid. The extent of each continuous axis of such a grid comes
|
|
67
|
+
* from `convertFromPixel` at the two edges.
|
|
68
|
+
* 3. The raster draw takes the export option with the point layers alone. Each grid stands at its measured
|
|
69
|
+
* rectangle, each axis holds its measured extent and draws nothing, and the legend, the graphics, and the
|
|
70
|
+
* color scales draw nothing. Thus each point lands on the pixel where the vector axes place it.
|
|
71
|
+
* 4. The composition crops the raster to the union of the grid rectangles at 300 DPI, and it puts the PNG into
|
|
72
|
+
* the vector file. The crop reaches past each grid edge by half of the largest symbol of a layer or of a
|
|
73
|
+
* visual map of a layer, thus a point on an edge keeps its whole symbol, as it does on the page.
|
|
74
|
+
*/
|
|
75
|
+
export declare const HYBRID_SVG_SOURCE: string;
|