@inflexa-ai/harness 0.42.2 → 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/sandbox/shared.d.ts +1 -6
- package/dist/agents/sandbox/shared.js +0 -15
- package/dist/app/chat-turn.js +13 -1
- package/dist/config/environment-stores.d.ts +4 -2
- package/dist/contracts/chat-events.d.ts +9 -1
- package/dist/contracts/chat-frame.d.ts +3 -3
- package/dist/contracts/chat-frame.js +8 -3
- package/dist/contracts/chat-parts.d.ts +4 -1
- package/dist/contracts/data-profile.d.ts +4 -6
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/report-blocks.d.ts +1364 -75
- package/dist/contracts/report-blocks.js +221 -29
- package/dist/contracts/report-reference.js +1 -1
- package/dist/contracts/schemas/chat-events.d.ts +13 -0
- package/dist/contracts/schemas/chat-events.js +5 -0
- package/dist/contracts/schemas/chat-parts.d.ts +2 -2
- package/dist/contracts/schemas/index.d.ts +1 -1
- package/dist/contracts/schemas/index.js +1 -1
- package/dist/execution/input-signature.d.ts +7 -13
- package/dist/execution/input-signature.js +0 -0
- package/dist/execution/run-canceler.d.ts +3 -2
- package/dist/execution/run-canceler.js +3 -2
- package/dist/execution/run-event-stream.d.ts +16 -2
- package/dist/execution/run-event-stream.js +54 -3
- package/dist/index.d.ts +1 -8
- package/dist/index.js +5 -16
- package/dist/input-scan/enrich.d.ts +0 -2
- package/dist/input-scan/enrich.js +0 -1
- package/dist/lib/metrics.js +6 -7
- package/dist/lib/otel-spans.d.ts +4 -9
- package/dist/lib/otel-spans.js +3 -8
- package/dist/loop/run-agent.d.ts +4 -2
- package/dist/loop/run-agent.js +38 -23
- package/dist/memory/thread-history.d.ts +1 -1
- package/dist/prompts/report-session.d.ts +1 -1
- package/dist/prompts/report-session.js +17 -0
- package/dist/provenance/exec-frame.d.ts +1 -1
- package/dist/report-model/authoring-grammar.d.ts +416 -17
- package/dist/report-model/authoring-grammar.js +25 -4
- package/dist/report-model/block-walk.d.ts +20 -4
- package/dist/report-model/block-walk.js +64 -7
- package/dist/report-model/draft-finish.d.ts +6 -2
- package/dist/report-model/draft-finish.js +6 -1
- package/dist/report-model/production-resolver.d.ts +8 -0
- package/dist/report-model/production-resolver.js +1 -1
- package/dist/report-model/validate.d.ts +20 -10
- package/dist/report-model/validate.js +29 -13
- package/dist/report-render/chart-export.d.ts +52 -0
- package/dist/report-render/chart-export.js +192 -0
- package/dist/report-render/chart-presets.d.ts +46 -47
- package/dist/report-render/chart-presets.js +31 -101
- package/dist/report-render/chart-renderers.d.ts +163 -0
- package/dist/report-render/chart-renderers.js +1055 -0
- package/dist/report-render/chart-toolbox.d.ts +124 -0
- package/dist/report-render/chart-toolbox.js +595 -0
- package/dist/report-render/chart.d.ts +60 -27
- package/dist/report-render/chart.js +2323 -524
- package/dist/report-render/design.d.ts +476 -16
- package/dist/report-render/design.js +390 -56
- package/dist/report-render/figures/common.d.ts +321 -0
- package/dist/report-render/figures/common.js +759 -0
- package/dist/report-render/figures/dendrogram.d.ts +36 -0
- package/dist/report-render/figures/dendrogram.js +130 -0
- package/dist/report-render/figures/dense.d.ts +111 -0
- package/dist/report-render/figures/dense.js +204 -0
- package/dist/report-render/figures/dotplot.d.ts +28 -0
- package/dist/report-render/figures/dotplot.js +234 -0
- package/dist/report-render/figures/embedding.d.ts +43 -0
- package/dist/report-render/figures/embedding.js +360 -0
- package/dist/report-render/figures/equal-units.d.ts +101 -0
- package/dist/report-render/figures/equal-units.js +215 -0
- package/dist/report-render/figures/forest.d.ts +20 -0
- package/dist/report-render/figures/forest.js +398 -0
- package/dist/report-render/figures/gsea.d.ts +30 -0
- package/dist/report-render/figures/gsea.js +391 -0
- package/dist/report-render/figures/heatmap.d.ts +40 -0
- package/dist/report-render/figures/heatmap.js +395 -0
- package/dist/report-render/figures/index.d.ts +135 -0
- package/dist/report-render/figures/index.js +100 -0
- package/dist/report-render/figures/km.d.ts +16 -0
- package/dist/report-render/figures/km.js +406 -0
- package/dist/report-render/figures/label-room.d.ts +130 -0
- package/dist/report-render/figures/label-room.js +341 -0
- package/dist/report-render/figures/locuszoom.d.ts +50 -0
- package/dist/report-render/figures/locuszoom.js +675 -0
- package/dist/report-render/figures/lollipop.d.ts +16 -0
- package/dist/report-render/figures/lollipop.js +247 -0
- package/dist/report-render/figures/ma.d.ts +20 -0
- package/dist/report-render/figures/ma.js +114 -0
- package/dist/report-render/figures/manhattan.d.ts +60 -0
- package/dist/report-render/figures/manhattan.js +343 -0
- package/dist/report-render/figures/oncoprint.d.ts +41 -0
- package/dist/report-render/figures/oncoprint.js +420 -0
- package/dist/report-render/figures/pca.d.ts +37 -0
- package/dist/report-render/figures/pca.js +241 -0
- package/dist/report-render/figures/qq.d.ts +30 -0
- package/dist/report-render/figures/qq.js +182 -0
- package/dist/report-render/figures/roc.d.ts +17 -0
- package/dist/report-render/figures/roc.js +116 -0
- package/dist/report-render/figures/sankey.d.ts +48 -0
- package/dist/report-render/figures/sankey.js +353 -0
- package/dist/report-render/figures/upset.d.ts +60 -0
- package/dist/report-render/figures/upset.js +316 -0
- package/dist/report-render/figures/volcano.d.ts +25 -0
- package/dist/report-render/figures/volcano.js +166 -0
- package/dist/report-render/hybrid-svg.d.ts +75 -0
- package/dist/report-render/hybrid-svg.js +504 -0
- package/dist/report-render/number-format.d.ts +19 -0
- package/dist/report-render/number-format.js +25 -1
- package/dist/report-render/page.d.ts +41 -4
- package/dist/report-render/page.js +247 -12
- package/dist/report-render/render.d.ts +13 -4
- package/dist/report-render/render.js +37 -6
- package/dist/report-render/types.d.ts +23 -2
- package/dist/report-render/value-bridge.d.ts +38 -3
- package/dist/report-render/value-bridge.js +137 -6
- package/dist/report-render/views/chart-view.d.ts +42 -6
- package/dist/report-render/views/chart-view.js +70 -9
- package/dist/report-render/views/page-view.js +9 -10
- package/dist/runtime/assemble.js +2 -2
- package/dist/sandbox/client.d.ts +23 -32
- package/dist/sandbox/client.js +3 -11
- package/dist/sandbox/create-sandbox.d.ts +9 -24
- package/dist/sandbox/create-sandbox.js +15 -30
- package/dist/sandbox/docker-client.d.ts +12 -22
- package/dist/sandbox/docker-client.js +29 -43
- package/dist/sandbox/exec.d.ts +58 -0
- package/dist/sandbox/exec.js +197 -0
- package/dist/sandbox/identity.d.ts +5 -5
- package/dist/sandbox/identity.js +7 -10
- package/dist/sandbox/k8s-client.d.ts +4 -10
- package/dist/sandbox/k8s-client.js +5 -12
- package/dist/sandbox/liveness.d.ts +5 -12
- package/dist/sandbox/liveness.js +5 -12
- package/dist/sandbox/notification-sweep.d.ts +4 -7
- package/dist/sandbox/notification-sweep.js +4 -7
- package/dist/sandbox/reaper.d.ts +3 -3
- package/dist/sandbox/reaper.js +3 -3
- package/dist/sandbox/sandbox-error.d.ts +10 -11
- package/dist/sandbox/sandbox-error.js +10 -11
- package/dist/sandbox/sandbox-step-translate.d.ts +1 -1
- package/dist/sandbox/sandbox-step-translate.js +1 -1
- package/dist/sandbox/types.d.ts +31 -99
- package/dist/sandbox/types.js +17 -84
- package/dist/state/active-sandboxes.d.ts +7 -32
- package/dist/state/active-sandboxes.js +11 -46
- package/dist/state/index.d.ts +1 -2
- package/dist/state/index.js +1 -1
- package/dist/state/migrations/20261005120000_drop_the_active_exec_tracking.d.ts +7 -0
- package/dist/state/migrations/20261005120000_drop_the_active_exec_tracking.js +12 -0
- package/dist/state/migrations/index.js +2 -0
- package/dist/state/schema.d.ts +1 -5
- package/dist/state/schema.js +1 -6
- package/dist/state/step-executions.js +1 -2
- package/dist/tasks/data-profile.js +7 -20
- package/dist/tasks/derive-table-exec.d.ts +4 -14
- package/dist/tasks/derive-table-exec.js +9 -24
- package/dist/tasks/extract-values.d.ts +2 -2
- package/dist/tasks/extract-values.js +4 -7
- package/dist/tools/define-tool.d.ts +56 -13
- package/dist/tools/define-tool.js +28 -1
- package/dist/tools/execute-analysis.js +3 -3
- package/dist/tools/report-session/derive-table.d.ts +8 -9
- package/dist/tools/report-session/derive-table.js +3 -4
- package/dist/tools/report-session/preview-report.js +4 -49
- package/dist/tools/sandbox/list-available-packages.d.ts +1 -1
- package/dist/tools/sandbox/list-available-packages.js +5 -4
- package/dist/tools/workspace/edit-file.d.ts +5 -4
- package/dist/tools/workspace/edit-file.js +10 -13
- package/dist/tools/workspace/execute-command.d.ts +27 -42
- package/dist/tools/workspace/execute-command.js +42 -38
- package/dist/tools/workspace/index.d.ts +5 -6
- package/dist/tools/workspace/index.js +5 -6
- package/dist/tools/workspace/mutator.d.ts +43 -8
- package/dist/tools/workspace/mutator.js +45 -34
- package/dist/tools/workspace/run-exec.d.ts +3 -12
- package/dist/tools/workspace/run-exec.js +7 -24
- package/dist/tools/workspace/scan-inputs.d.ts +0 -3
- package/dist/tools/workspace/scan-inputs.js +0 -1
- package/dist/tools/workspace/write-file.d.ts +12 -3
- package/dist/tools/workspace/write-file.js +17 -13
- package/dist/workflows/sandbox-step.d.ts +7 -11
- package/dist/workflows/sandbox-step.js +24 -27
- package/package.json +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,675 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The regional association plot, as LocusZoom draws it (Pruim et al. 2010): the variants of one region of one
|
|
3
|
+
* chromosome, with the position on x and the transformed p on y.
|
|
4
|
+
*
|
|
5
|
+
* - Each point takes the LocusZoom bin of its r² with the lead variant, and a point of no r² draws gray under
|
|
6
|
+
* the others. One legend over the plot states the bins and the gray.
|
|
7
|
+
* - The lead variant is the row with the largest transformed p. It draws as a purple diamond with its name on
|
|
8
|
+
* top. A tie keeps the row at the smaller position, then the first row.
|
|
9
|
+
* - The recombination rate draws as a blue line on a right axis in cM/Mb, from the rows in position order.
|
|
10
|
+
* - A guide line marks the genome-wide significance at 5 × 10⁻⁸. Its name sits over the line at the first free
|
|
11
|
+
* spot from the left end, where no variant and no stretch of the recombination line meets the name.
|
|
12
|
+
* - The x axis reads in megabases, with round ticks, and it ends at the first and the last position of the rows.
|
|
13
|
+
* - The genes of the track draw under the plot, each one as a line with its name under it. The lane of a gene
|
|
14
|
+
* is the first lane where its line and its name overlap no gene before it, in the order of the gene starts.
|
|
15
|
+
* - A variant whose stored p is 0 draws an upward triangle at the top of the plotted range, and it leads.
|
|
16
|
+
*
|
|
17
|
+
* The points and the recombination line come from the composition machinery, thus a window of many thousand
|
|
18
|
+
* variants reads the shared payload. The r² rides the points as a continuous color, and a piecewise map bins
|
|
19
|
+
* it. The composition drops a row whose color is not numeric, thus the points of no r² draw in a series of
|
|
20
|
+
* their own after the composition, with their rows inline. The tick names, the lead, and the genes follow them.
|
|
21
|
+
*
|
|
22
|
+
* The chart runtime takes no function in the option, thus the axis cannot print a position divided by 10⁶.
|
|
23
|
+
* The axis hides its own ticks and labels, and a series of named points draws each tick in megabases, as the
|
|
24
|
+
* Manhattan figure prints its chromosome names.
|
|
25
|
+
*/
|
|
26
|
+
import { err, ok } from "neverthrow";
|
|
27
|
+
import { channelColumn, channelOrder, channelTransform } from "../../contracts/report-blocks.js";
|
|
28
|
+
import { declaredForColumn } from "../../contracts/report-reference.js";
|
|
29
|
+
import { MANHATTAN_P_THRESHOLD } from "../chart-presets.js";
|
|
30
|
+
import { CHART_BODY_MAX_PX, CHART_BODY_PX, CHART_EXPORT_SIZES, CHART_INK, exportSizeFor } from "../design.js";
|
|
31
|
+
import { formatNumberCell, typographicExponent } from "../number-format.js";
|
|
32
|
+
import { categoryName, chartProblem, columnPresent, firstFreeLanes, toNumber, transformColumn, valueAxis, valueAxisTitle, withFigureBody } from "./common.js";
|
|
33
|
+
import { axisOf, BELOW_RESOLUTION_SYMBOL, BELOW_RESOLUTION_SYMBOL_PX, BELOW_RESOLUTION_TOOLTIP, belowResolutionHeight, belowResolutionRows, DENSE_NULL_Z, demandedChannel, largest, niceCeiling, seriesOf, } from "./dense.js";
|
|
34
|
+
import { COLUMN_PLOT_FRAME, textWidthPx } from "./label-room.js";
|
|
35
|
+
/** The five r² bins of LocusZoom, in ascending order, with its colors: navy, sky blue, green, orange, and red. */
|
|
36
|
+
export const LD_BINS = [
|
|
37
|
+
{ low: 0, high: 0.2, color: "#000080" },
|
|
38
|
+
{ low: 0.2, high: 0.4, color: "#87cefa" },
|
|
39
|
+
{ low: 0.4, high: 0.6, color: "#00a000" },
|
|
40
|
+
{ low: 0.6, high: 0.8, color: "#ffa500" },
|
|
41
|
+
{ low: 0.8, high: 1, color: "#ff0000" },
|
|
42
|
+
];
|
|
43
|
+
/** The gray of a variant with no r², and the name of its series and its legend entry. */
|
|
44
|
+
export const LD_MISSING_COLOR = "#8c8c8c";
|
|
45
|
+
export const LD_MISSING_NAME = "No r²";
|
|
46
|
+
/** The purple of the lead variant, as LocusZoom draws it, and the name of its series. */
|
|
47
|
+
export const LEAD_VARIANT_COLOR = "#9632b8";
|
|
48
|
+
export const LEAD_VARIANT_NAME = "Lead variant";
|
|
49
|
+
/** The blue of the recombination line, and the name of its series. */
|
|
50
|
+
export const RECOMBINATION_COLOR = "#1f4fd8";
|
|
51
|
+
export const RECOMBINATION_NAME = "Recombination rate";
|
|
52
|
+
/** The name of the series that prints the tick names in megabases, and of the series of the genes. */
|
|
53
|
+
export const POSITION_TICKS = "Position ticks";
|
|
54
|
+
export const GENE_NAMES = "Genes";
|
|
55
|
+
/** The name of the series that prints the name of the genome-wide line. */
|
|
56
|
+
export const GUIDE_LABEL = "Genome-wide line";
|
|
57
|
+
/** The title of the legend, of the p axis, and of the recombination axis where its column declares no label. */
|
|
58
|
+
const LEGEND_TITLE = "r²";
|
|
59
|
+
const NEG_LOG10_P_TITLE = "−log10(p)";
|
|
60
|
+
const RECOMBINATION_TITLE = "Recombination rate (cM/Mb)";
|
|
61
|
+
/** The members of a locuszoom block that the figure reads. */
|
|
62
|
+
const READS = new Set(["x", "y", "color", "label", "metric", "group", "track"]);
|
|
63
|
+
/** The size in pixels of a variant point, of the lead diamond, and the width of the point outline. */
|
|
64
|
+
const POINT_PX = 6;
|
|
65
|
+
const LEAD_PX = 12;
|
|
66
|
+
const POINT_OUTLINE_PX = 0.4;
|
|
67
|
+
/** The width of the recombination line in pixels, and its opacity. The points read over it. */
|
|
68
|
+
const RECOMBINATION_WIDTH_PX = 1;
|
|
69
|
+
const RECOMBINATION_OPACITY = 0.7;
|
|
70
|
+
/**
|
|
71
|
+
* The name of the genome-wide line: its gap over the line, its gap from the left end and from each variant or
|
|
72
|
+
* stretch of the recombination line that it clears, and the halo of the page color around its letters, in
|
|
73
|
+
* pixels. The height of its line of text is a share of the text size.
|
|
74
|
+
*/
|
|
75
|
+
const GUIDE_GAP_PX = 2;
|
|
76
|
+
const GUIDE_INSET_PX = 4;
|
|
77
|
+
const GUIDE_HALO_PX = 2;
|
|
78
|
+
const GUIDE_HALO_COLOR = "#ffffff";
|
|
79
|
+
const GUIDE_LINE_SHARE = 1.25;
|
|
80
|
+
/** The width of a gene line in pixels. */
|
|
81
|
+
const GENE_WIDTH_PX = 2;
|
|
82
|
+
/** The icon size in pixels of the legend, and the gaps between its items and between an icon and its text. */
|
|
83
|
+
const LEGEND_ICON_PX = 8;
|
|
84
|
+
const LEGEND_ITEM_GAP_PX = 6;
|
|
85
|
+
const LEGEND_TEXT_GAP_PX = 3;
|
|
86
|
+
/** The width and the length in pixels of a tick mark of the position axis. */
|
|
87
|
+
const TICK_WIDTH_PX = 1;
|
|
88
|
+
const TICK_LENGTH_PX = 5;
|
|
89
|
+
/** The count of base pairs in one megabase. */
|
|
90
|
+
const BASES_PER_MB = 1_000_000;
|
|
91
|
+
/** The most tick steps that the position axis draws. Six names of four digits fit the single journal column. */
|
|
92
|
+
const POSITION_TICK_COUNT = 5;
|
|
93
|
+
/** The round tick steps of the position axis, in base pairs, in ascending order. */
|
|
94
|
+
const POSITION_STEPS = [1, 2, 2.5, 5].flatMap((coefficient) => [1e3, 1e4, 1e5, 1e6, 1e7].map((power) => coefficient * power)).sort((a, b) => a - b);
|
|
95
|
+
/**
|
|
96
|
+
* The layout of the plot and of the gene band, in percent of the default chart body.
|
|
97
|
+
*
|
|
98
|
+
* The axis band under the plot holds the tick names and the title of the position axis. Each lane of genes
|
|
99
|
+
* takes one band of the height of a line and a name. A band of more lanes than the body holds grows the body.
|
|
100
|
+
*/
|
|
101
|
+
const PLOT_TOP = 12;
|
|
102
|
+
const PLOT_LEFT = 10;
|
|
103
|
+
const PLOT_RIGHT = 5;
|
|
104
|
+
const PLOT_RIGHT_WITH_RATE = 10;
|
|
105
|
+
const AXIS_BAND = 17;
|
|
106
|
+
const GENE_LANE = 6;
|
|
107
|
+
const TRACK_BOTTOM = 3;
|
|
108
|
+
const LANES_IN_BODY = 3;
|
|
109
|
+
/** The place of a gene line inside its lane, in lanes from the top of the lane. The name sits under the line. */
|
|
110
|
+
const GENE_LINE_PLACE = 0.25;
|
|
111
|
+
/**
|
|
112
|
+
* The width of one character of a gene name, as a share of the text size, and the gap in characters between two
|
|
113
|
+
* genes of one lane. A name reads at the single journal column, thus its width reads that frame.
|
|
114
|
+
*/
|
|
115
|
+
const GENE_CHARACTER_SHARE = 0.6;
|
|
116
|
+
const GENE_GAP_CHARACTERS = 1;
|
|
117
|
+
/** The trailing unit of a declared position label, which the megabase axis replaces. */
|
|
118
|
+
const POSITION_UNIT = /\s*\((bp|base pairs|kb|Mb)\)\s*$/i;
|
|
119
|
+
function deriveLocusZoom(block, rows, context) {
|
|
120
|
+
const columns = locusColumns(block, rows, context);
|
|
121
|
+
if (columns.isErr())
|
|
122
|
+
return err(columns.error);
|
|
123
|
+
const { position, p, r2, label, rate, chromosome } = columns.value;
|
|
124
|
+
const xs = rows.map((row) => toNumber(row[position]));
|
|
125
|
+
const faulty = xs.findIndex((x) => x === null);
|
|
126
|
+
if (faulty >= 0) {
|
|
127
|
+
return err(chartProblem(context.blockId, `The locuszoom reads the "${position}" column as a position in base pairs, and row ${faulty + 1} holds "${String(rows[faulty][position] ?? "")}", which is not a number.`));
|
|
128
|
+
}
|
|
129
|
+
const chromosomes = chromosome === undefined ? [] : distinctNames(rows, chromosome);
|
|
130
|
+
if (chromosomes.length > 1) {
|
|
131
|
+
return err(chartProblem(context.blockId, `The locuszoom draws one region of one chromosome, and the "${chromosome}" column holds "${chromosomes[0]}" and "${chromosomes[1]}".`));
|
|
132
|
+
}
|
|
133
|
+
const r2s = rows.map((row) => toNumber(row[r2]));
|
|
134
|
+
const outside = r2s.findIndex((value) => value !== null && (value < 0 || value > 1));
|
|
135
|
+
if (outside >= 0) {
|
|
136
|
+
return err(chartProblem(context.blockId, `The locuszoom reads the "${r2}" column as the r² with the lead variant, and row ${outside + 1} holds ${r2s[outside]}, which is outside 0 to 1.`));
|
|
137
|
+
}
|
|
138
|
+
const line = -Math.log10(MANHATTAN_P_THRESHOLD);
|
|
139
|
+
const finite = transformColumn(rows, p, "neg_log10");
|
|
140
|
+
const peak = largest(finite.flatMap((value) => (value !== null ? [value] : [])));
|
|
141
|
+
const zeroRows = belowResolutionRows(rows, p, xs);
|
|
142
|
+
const zeros = new Set(zeroRows);
|
|
143
|
+
const zeroY = belowResolutionHeight(peak, line);
|
|
144
|
+
const ys = finite.map((value, index) => (zeros.has(index) ? zeroY : value));
|
|
145
|
+
const lead = leadRow(xs, finite, zeros);
|
|
146
|
+
const top = niceCeiling(Math.max(peak ?? 0, line, zeroRows.length > 0 ? zeroY : 0));
|
|
147
|
+
const window = positionWindow(xs);
|
|
148
|
+
const genes = context.track === undefined ? ok(undefined) : windowGenes(context.blockId, context.track, window);
|
|
149
|
+
if (genes.isErr())
|
|
150
|
+
return err(genes.error);
|
|
151
|
+
const lanes = genes.value === undefined ? 0 : Math.max(1, (largest(genes.value.map((gene) => gene.lane)) ?? 0) + 1);
|
|
152
|
+
const composition = {
|
|
153
|
+
series: [
|
|
154
|
+
{ form: "scatter", encoding: { x: position, y: { column: p, transform: "neg_log10" }, color: r2, ...(label !== undefined ? { label } : {}) } },
|
|
155
|
+
...(rate !== undefined ? [{ form: "line", name: RECOMBINATION_NAME, encoding: { x: position, y: rate } }] : []),
|
|
156
|
+
],
|
|
157
|
+
annotations: [{ kind: "reference-line", axis: "y", value: line }],
|
|
158
|
+
};
|
|
159
|
+
const xTitle = positionTitle(context, position, chromosomes[0]);
|
|
160
|
+
const composed = context.compose(composition, { preset: { x: xTitle, y: NEG_LOG10_P_TITLE }, colorOnTop: true });
|
|
161
|
+
if (composed.isErr())
|
|
162
|
+
return err(composed.error);
|
|
163
|
+
const option = composed.value;
|
|
164
|
+
const [points, recombination] = seriesOf(option);
|
|
165
|
+
const nameOf = (index) => {
|
|
166
|
+
const cell = label === undefined ? undefined : rows[index][label];
|
|
167
|
+
return cell === undefined || cell === null || String(cell).trim() === "" ? undefined : String(cell);
|
|
168
|
+
};
|
|
169
|
+
const item = (index, y) => {
|
|
170
|
+
const name = nameOf(index);
|
|
171
|
+
return { ...(name !== undefined ? { name } : {}), value: [xs[index] ?? 0, y] };
|
|
172
|
+
};
|
|
173
|
+
const missing = [];
|
|
174
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
175
|
+
const y = finite[index];
|
|
176
|
+
if (r2s[index] === null && y !== null)
|
|
177
|
+
missing.push(item(index, y));
|
|
178
|
+
}
|
|
179
|
+
const rated = rate === undefined ? [] : ratesInOrder(rows, xs, rate);
|
|
180
|
+
const rateTop = niceCeiling(largest(rated.map(([, value]) => value)) ?? 0);
|
|
181
|
+
const layout = bandLayout(lanes, rate !== undefined);
|
|
182
|
+
const plotAxis = {
|
|
183
|
+
...axisOf(option, "xAxis"),
|
|
184
|
+
...valueAxis("x", xTitle, { min: window.min, max: window.max }),
|
|
185
|
+
axisLabel: { show: false },
|
|
186
|
+
axisTick: { show: false },
|
|
187
|
+
splitLine: { show: false },
|
|
188
|
+
};
|
|
189
|
+
const pAxis = { ...axisOf(option, "yAxis"), ...valueAxis("y", NEG_LOG10_P_TITLE, { min: 0, max: top }) };
|
|
190
|
+
const rateAxis = rate === undefined
|
|
191
|
+
? []
|
|
192
|
+
: [
|
|
193
|
+
{
|
|
194
|
+
...valueAxis("y", valueAxisTitle(context.labels, rate, RECOMBINATION_TITLE), { min: 0, max: rateTop }),
|
|
195
|
+
position: "right",
|
|
196
|
+
nameRotate: -90,
|
|
197
|
+
splitLine: { show: false },
|
|
198
|
+
},
|
|
199
|
+
];
|
|
200
|
+
const series = [
|
|
201
|
+
{ ...points, symbolSize: POINT_PX, itemStyle: { borderColor: CHART_INK, borderWidth: POINT_OUTLINE_PX } },
|
|
202
|
+
...(recombination !== undefined
|
|
203
|
+
? [
|
|
204
|
+
{
|
|
205
|
+
...recombination,
|
|
206
|
+
yAxisIndex: 1,
|
|
207
|
+
showSymbol: false,
|
|
208
|
+
silent: true,
|
|
209
|
+
z: DENSE_NULL_Z,
|
|
210
|
+
lineStyle: { color: RECOMBINATION_COLOR, width: RECOMBINATION_WIDTH_PX, opacity: RECOMBINATION_OPACITY },
|
|
211
|
+
itemStyle: { color: RECOMBINATION_COLOR },
|
|
212
|
+
},
|
|
213
|
+
]
|
|
214
|
+
: []),
|
|
215
|
+
...(missing.length > 0 ? [missingSeries(missing)] : []),
|
|
216
|
+
...(zeroRows.length > 0 ? [zeroSeries(zeroRows.map((index) => ({ ...item(index, zeroY), itemStyle: { color: binColor(r2s[index]) } })))] : []),
|
|
217
|
+
tickSeries(window),
|
|
218
|
+
guideSeries(guideText(MANHATTAN_P_THRESHOLD), line, { ys, xs, rated, top, rateTop, window, layout }),
|
|
219
|
+
...(lead !== undefined ? [leadSeries(item(lead, ys[lead] ?? 0))] : []),
|
|
220
|
+
...(genes.value !== undefined ? [geneSeries(genes.value, rate !== undefined ? 2 : 1)] : []),
|
|
221
|
+
];
|
|
222
|
+
const plotGrid = { top: pct(layout.top), bottom: pct(layout.plotBottom), left: pct(PLOT_LEFT), right: pct(layout.right) };
|
|
223
|
+
const geneGrid = { bottom: pct(layout.trackBottom), height: pct(layout.band), left: pct(PLOT_LEFT), right: pct(layout.right) };
|
|
224
|
+
const derived = {
|
|
225
|
+
...option,
|
|
226
|
+
legend: { show: false },
|
|
227
|
+
visualMap: [ldLegend()],
|
|
228
|
+
grid: genes.value === undefined ? plotGrid : [plotGrid, geneGrid],
|
|
229
|
+
xAxis: genes.value === undefined ? plotAxis : [plotAxis, { type: "value", gridIndex: 1, min: window.min, max: window.max, show: false }],
|
|
230
|
+
yAxis: [pAxis, ...rateAxis, ...(genes.value === undefined ? [] : [{ type: "value", gridIndex: 1, min: 0, max: lanes, inverse: true, show: false }])],
|
|
231
|
+
series,
|
|
232
|
+
};
|
|
233
|
+
return ok(layout.bodyPx > CHART_BODY_PX ? withFigureBody(derived, layout.bodyPx) : derived);
|
|
234
|
+
}
|
|
235
|
+
/** The columns of the channels. An absent position, p, or r², a transform on one of them, and an absent column each refuse. */
|
|
236
|
+
function locusColumns(block, rows, context) {
|
|
237
|
+
const encoding = block.encoding ?? {};
|
|
238
|
+
const x = demandedChannel(context.blockId, "locuszoom", encoding, "x");
|
|
239
|
+
if (x.isErr())
|
|
240
|
+
return err(x.error);
|
|
241
|
+
const y = demandedChannel(context.blockId, "locuszoom", encoding, "y");
|
|
242
|
+
if (y.isErr())
|
|
243
|
+
return err(y.error);
|
|
244
|
+
if (encoding.color === undefined) {
|
|
245
|
+
return err(chartProblem(context.blockId, 'The locuszoom chart needs a column for the "color" channel: the r² of each variant with the lead variant.'));
|
|
246
|
+
}
|
|
247
|
+
const plain = { x: x.value, color: encoding.color, metric: encoding.metric, group: encoding.group };
|
|
248
|
+
for (const [name, channel] of Object.entries(plain)) {
|
|
249
|
+
if (channel !== undefined && (channelTransform(channel) !== undefined || channelOrder(channel) !== undefined)) {
|
|
250
|
+
return err(chartProblem(context.blockId, `The locuszoom reads the "${name}" column as it stands, thus the channel takes no transform and no "orderBy".`));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const found = {
|
|
254
|
+
position: channelColumn(x.value),
|
|
255
|
+
// The figure applies the transform of the p column itself, as the Manhattan figure does.
|
|
256
|
+
p: channelColumn(y.value),
|
|
257
|
+
r2: channelColumn(encoding.color),
|
|
258
|
+
...(encoding.label !== undefined ? { label: encoding.label } : {}),
|
|
259
|
+
...(encoding.metric !== undefined ? { rate: channelColumn(encoding.metric) } : {}),
|
|
260
|
+
...(encoding.group !== undefined ? { chromosome: channelColumn(encoding.group) } : {}),
|
|
261
|
+
};
|
|
262
|
+
for (const column of Object.values(found)) {
|
|
263
|
+
if (rows.length > 0 && !columnPresent(column, rows, context.columns)) {
|
|
264
|
+
return err(chartProblem(context.blockId, `The column "${column}" is absent from every row.`));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return ok(found);
|
|
268
|
+
}
|
|
269
|
+
/** The distinct names of one column, in the order of the rows. An empty cell names nothing. */
|
|
270
|
+
function distinctNames(rows, column) {
|
|
271
|
+
const names = [];
|
|
272
|
+
for (const row of rows) {
|
|
273
|
+
const cell = row[column];
|
|
274
|
+
if (cell === undefined || cell === null || String(cell).trim() === "")
|
|
275
|
+
continue;
|
|
276
|
+
const name = categoryName(cell);
|
|
277
|
+
if (!names.includes(name))
|
|
278
|
+
names.push(name);
|
|
279
|
+
if (names.length > 1)
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
return names;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* The lead row: the largest transformed p, a stored zero before each finite p. A tie keeps the row at the
|
|
286
|
+
* smaller position, then the first row. A table with no p gives no lead.
|
|
287
|
+
*/
|
|
288
|
+
function leadRow(xs, finite, zeros) {
|
|
289
|
+
let lead;
|
|
290
|
+
let best = Number.NEGATIVE_INFINITY;
|
|
291
|
+
for (let index = 0; index < finite.length; index += 1) {
|
|
292
|
+
const rank = zeros.has(index) ? Number.POSITIVE_INFINITY : finite[index];
|
|
293
|
+
if (rank === null)
|
|
294
|
+
continue;
|
|
295
|
+
if (lead === undefined || rank > best || (rank === best && (xs[index] ?? 0) < (xs[lead] ?? 0))) {
|
|
296
|
+
lead = index;
|
|
297
|
+
best = rank;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return lead;
|
|
301
|
+
}
|
|
302
|
+
/** The text of a guide p-value: the p in the scientific form of the number helper, as a power of ten. */
|
|
303
|
+
function guideText(value) {
|
|
304
|
+
return `p ${typographicExponent(formatNumberCell(value, "scientific").text)}`;
|
|
305
|
+
}
|
|
306
|
+
/** The pairs of the position and the recombination rate of each row with both, in position order. */
|
|
307
|
+
function ratesInOrder(rows, xs, rate) {
|
|
308
|
+
const pairs = [];
|
|
309
|
+
for (const [index, row] of rows.entries()) {
|
|
310
|
+
const x = xs[index];
|
|
311
|
+
const value = toNumber(row[rate]);
|
|
312
|
+
if (x !== null && value !== null)
|
|
313
|
+
pairs.push([x, value]);
|
|
314
|
+
}
|
|
315
|
+
return pairs.sort((a, b) => a[0] - b[0]);
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* The position where the name of the genome-wide line starts: the first spot from the left end where the box
|
|
319
|
+
* of the name, over the line, holds no variant and no stretch of the recombination line. The right end sits at
|
|
320
|
+
* the recombination axis, thus the search starts at the left. A plot with no free spot takes the first spot
|
|
321
|
+
* where the fewest of those marks meet the box, and the halo keeps the name legible over them.
|
|
322
|
+
*
|
|
323
|
+
* The box reads the single-column export. There the name takes the largest share of the width and of the
|
|
324
|
+
* height of the plot, thus a spot that is free there is free at each size.
|
|
325
|
+
*/
|
|
326
|
+
function guideStart(text, line, room) {
|
|
327
|
+
const { window, layout } = room;
|
|
328
|
+
const frame = exportSizeFor(CHART_EXPORT_SIZES.single, layout.bodyPx);
|
|
329
|
+
const plotWidthPx = (frame.widthPx * (100 - PLOT_LEFT - layout.right)) / 100;
|
|
330
|
+
const plotHeightPx = (frame.heightPx * (100 - layout.top - layout.plotBottom)) / 100;
|
|
331
|
+
const xPerPx = (window.max - window.min) / plotWidthPx;
|
|
332
|
+
const yPerPx = room.top / plotHeightPx;
|
|
333
|
+
const width = (textWidthPx(text, frame.textPx) + 2 * GUIDE_HALO_PX) * xPerPx;
|
|
334
|
+
const inset = GUIDE_INSET_PX * xPerPx;
|
|
335
|
+
const low = line;
|
|
336
|
+
const high = line + (GUIDE_GAP_PX + frame.textPx * GUIDE_LINE_SHARE) * yPerPx;
|
|
337
|
+
const reach = (POINT_PX / 2) * yPerPx;
|
|
338
|
+
const half = (POINT_PX / 2) * xPerPx;
|
|
339
|
+
const blocked = [];
|
|
340
|
+
for (const [index, x] of room.xs.entries()) {
|
|
341
|
+
const y = room.ys[index];
|
|
342
|
+
if (x !== null && y !== null && y + reach >= low && y - reach <= high)
|
|
343
|
+
blocked.push([x - half, x + half]);
|
|
344
|
+
}
|
|
345
|
+
if (room.rateTop > 0) {
|
|
346
|
+
const rateLow = (low / room.top) * room.rateTop;
|
|
347
|
+
const rateHigh = (high / room.top) * room.rateTop;
|
|
348
|
+
for (let index = 1; index < room.rated.length; index += 1) {
|
|
349
|
+
const [x1, r1] = room.rated[index - 1];
|
|
350
|
+
const [x2, r2] = room.rated[index];
|
|
351
|
+
if (Math.max(r1, r2) >= rateLow && Math.min(r1, r2) <= rateHigh)
|
|
352
|
+
blocked.push([x1, x2]);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
const marks = union(blocked);
|
|
356
|
+
const covered = coverage(marks);
|
|
357
|
+
// A spot starts at the left end or past the end of a mark, because a start inside a free stretch covers no
|
|
358
|
+
// less than the start of that stretch.
|
|
359
|
+
let best = window.min + inset;
|
|
360
|
+
let least = covered(best, best + width + inset);
|
|
361
|
+
for (const [, end] of marks) {
|
|
362
|
+
const from = end + inset;
|
|
363
|
+
if (from + width > window.max - inset || least === 0)
|
|
364
|
+
break;
|
|
365
|
+
const share = covered(from, from + width + inset);
|
|
366
|
+
if (share < least) {
|
|
367
|
+
best = from;
|
|
368
|
+
least = share;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return best;
|
|
372
|
+
}
|
|
373
|
+
/** The union of some intervals, as disjoint intervals in ascending order. */
|
|
374
|
+
function union(intervals) {
|
|
375
|
+
const sorted = [...intervals].sort((a, b) => a[0] - b[0]);
|
|
376
|
+
const merged = [];
|
|
377
|
+
for (const [start, end] of sorted) {
|
|
378
|
+
const last = merged[merged.length - 1];
|
|
379
|
+
if (last !== undefined && start <= last[1])
|
|
380
|
+
last[1] = Math.max(last[1], end);
|
|
381
|
+
else
|
|
382
|
+
merged.push([start, end]);
|
|
383
|
+
}
|
|
384
|
+
return merged;
|
|
385
|
+
}
|
|
386
|
+
/** The length that some disjoint intervals in ascending order cover inside a span, read with two binary searches. */
|
|
387
|
+
function coverage(marks) {
|
|
388
|
+
const before = [0];
|
|
389
|
+
for (const [start, end] of marks)
|
|
390
|
+
before.push(before[before.length - 1] + end - start);
|
|
391
|
+
const firstEndingAfter = (x) => {
|
|
392
|
+
let low = 0;
|
|
393
|
+
let high = marks.length;
|
|
394
|
+
while (low < high) {
|
|
395
|
+
const middle = (low + high) >> 1;
|
|
396
|
+
if (marks[middle][1] > x)
|
|
397
|
+
high = middle;
|
|
398
|
+
else
|
|
399
|
+
low = middle + 1;
|
|
400
|
+
}
|
|
401
|
+
return low;
|
|
402
|
+
};
|
|
403
|
+
return (from, to) => {
|
|
404
|
+
const first = firstEndingAfter(from);
|
|
405
|
+
let last = firstEndingAfter(to);
|
|
406
|
+
if (last < marks.length && marks[last][0] < to)
|
|
407
|
+
last += 1;
|
|
408
|
+
if (first >= last)
|
|
409
|
+
return 0;
|
|
410
|
+
return before[last] - before[first] - Math.max(0, from - marks[first][0]) - Math.max(0, marks[last - 1][1] - to);
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* The series of the name of the genome-wide line: one point with no symbol on the line, whose label starts at
|
|
415
|
+
* the point and sits over the line. The halo of the page color keeps each letter clear of a mark under it.
|
|
416
|
+
*/
|
|
417
|
+
function guideSeries(text, line, room) {
|
|
418
|
+
return {
|
|
419
|
+
type: "scatter",
|
|
420
|
+
name: GUIDE_LABEL,
|
|
421
|
+
silent: true,
|
|
422
|
+
symbolSize: 0,
|
|
423
|
+
z: 10,
|
|
424
|
+
tooltip: { show: false },
|
|
425
|
+
label: {
|
|
426
|
+
show: true,
|
|
427
|
+
formatter: "{b}",
|
|
428
|
+
position: [0, -GUIDE_GAP_PX],
|
|
429
|
+
align: "left",
|
|
430
|
+
verticalAlign: "bottom",
|
|
431
|
+
color: CHART_INK,
|
|
432
|
+
textBorderColor: GUIDE_HALO_COLOR,
|
|
433
|
+
textBorderWidth: GUIDE_HALO_PX,
|
|
434
|
+
},
|
|
435
|
+
data: [{ name: text, value: [guideStart(text, line, room), line] }],
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* The window of the positions: the smallest and the largest position, and the smallest round step that draws
|
|
440
|
+
* at most `POSITION_TICK_COUNT` steps across them. The axis ends at the rows, as the window of the run states
|
|
441
|
+
* the region, and the ticks sit at the multiples of the step inside it.
|
|
442
|
+
*/
|
|
443
|
+
function positionWindow(xs) {
|
|
444
|
+
let low;
|
|
445
|
+
let high;
|
|
446
|
+
for (const x of xs) {
|
|
447
|
+
if (x === null)
|
|
448
|
+
continue;
|
|
449
|
+
low = low === undefined ? x : Math.min(low, x);
|
|
450
|
+
high = high === undefined ? x : Math.max(high, x);
|
|
451
|
+
}
|
|
452
|
+
const from = low ?? 0;
|
|
453
|
+
const to = high ?? 0;
|
|
454
|
+
const span = Math.max(to - from, POSITION_STEPS[0]);
|
|
455
|
+
const step = POSITION_STEPS.find((candidate) => span / candidate <= POSITION_TICK_COUNT) ?? POSITION_STEPS[POSITION_STEPS.length - 1];
|
|
456
|
+
return { min: from, max: Math.max(to, from + 1), step };
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* The title of the position axis in megabases: the declared label of the position column with its unit
|
|
460
|
+
* replaced, else the chromosome of the rows, else the bare position.
|
|
461
|
+
*/
|
|
462
|
+
function positionTitle(context, column, chromosome) {
|
|
463
|
+
const declared = declaredForColumn(context.labels, column);
|
|
464
|
+
if (declared !== undefined)
|
|
465
|
+
return `${declared.replace(POSITION_UNIT, "")} (Mb)`;
|
|
466
|
+
if (chromosome === undefined)
|
|
467
|
+
return "Position (Mb)";
|
|
468
|
+
const name = chromosome.replace(/^chr/i, "");
|
|
469
|
+
return `Position on chr${name} (Mb)`;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* The series of the ticks: one tick mark under the x axis at each multiple of the step inside the window, with
|
|
473
|
+
* the position in megabases as its label. The runtime counts the ticks of an axis from its minimum, and the
|
|
474
|
+
* window starts at the first row, thus the axis draws no tick of its own. The count of decimals is the count of the step, thus each name reads
|
|
475
|
+
* alike.
|
|
476
|
+
*/
|
|
477
|
+
function tickSeries(window) {
|
|
478
|
+
const decimals = decimalsOf(window.step / BASES_PER_MB);
|
|
479
|
+
const data = [];
|
|
480
|
+
for (let place = Math.ceil(window.min / window.step); place * window.step <= window.max; place += 1) {
|
|
481
|
+
const at = Math.round(place * window.step);
|
|
482
|
+
data.push({ name: (at / BASES_PER_MB).toFixed(decimals), value: [at, 0] });
|
|
483
|
+
}
|
|
484
|
+
return {
|
|
485
|
+
type: "scatter",
|
|
486
|
+
name: POSITION_TICKS,
|
|
487
|
+
silent: true,
|
|
488
|
+
symbol: "rect",
|
|
489
|
+
symbolSize: [TICK_WIDTH_PX, TICK_LENGTH_PX],
|
|
490
|
+
symbolOffset: [0, TICK_LENGTH_PX / 2],
|
|
491
|
+
itemStyle: { color: CHART_INK },
|
|
492
|
+
tooltip: { show: false },
|
|
493
|
+
label: { show: true, position: "bottom", distance: 3, color: CHART_INK, formatter: "{b}" },
|
|
494
|
+
data,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
/** The count of decimals of a round step, for example 1 for 0.1 and 2 for 0.25. */
|
|
498
|
+
function decimalsOf(step) {
|
|
499
|
+
const text = String(Number(step.toPrecision(6)));
|
|
500
|
+
const point = text.indexOf(".");
|
|
501
|
+
return point < 0 ? 0 : text.length - point - 1;
|
|
502
|
+
}
|
|
503
|
+
/** The color of the bin of one r², or the gray of a missing r². */
|
|
504
|
+
function binColor(r2) {
|
|
505
|
+
if (r2 === null)
|
|
506
|
+
return LD_MISSING_COLOR;
|
|
507
|
+
return (LD_BINS.find((bin, place) => r2 >= bin.low && (r2 < bin.high || place === LD_BINS.length - 1)) ?? LD_BINS[0]).color;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* The legend of the LD bins: a piecewise map over the r² of the points, highest bin first, with the gray entry
|
|
511
|
+
* of a missing r² last. The title leads the row.
|
|
512
|
+
*
|
|
513
|
+
* The points of no r² draw in their own series, and the map colors the point series alone. The gray entry
|
|
514
|
+
* holds no r² between 0 and 1, and the figure refuses any other r², thus the entry colors no point. A click
|
|
515
|
+
* selects no bin, because the gray entry would hide nothing.
|
|
516
|
+
*/
|
|
517
|
+
function ldLegend() {
|
|
518
|
+
const bins = [...LD_BINS].reverse().map((bin, place) => ({
|
|
519
|
+
gte: bin.low,
|
|
520
|
+
...(place === 0 ? { lte: bin.high } : { lt: bin.high }),
|
|
521
|
+
label: `${bin.low}–${bin.high}`,
|
|
522
|
+
color: bin.color,
|
|
523
|
+
}));
|
|
524
|
+
return {
|
|
525
|
+
type: "piecewise",
|
|
526
|
+
seriesIndex: [0],
|
|
527
|
+
dimension: 2,
|
|
528
|
+
orient: "horizontal",
|
|
529
|
+
top: 0,
|
|
530
|
+
left: "center",
|
|
531
|
+
selectedMode: false,
|
|
532
|
+
showLabel: true,
|
|
533
|
+
text: [LEGEND_TITLE, ""],
|
|
534
|
+
itemSymbol: "circle",
|
|
535
|
+
itemWidth: LEGEND_ICON_PX,
|
|
536
|
+
itemHeight: LEGEND_ICON_PX,
|
|
537
|
+
itemGap: LEGEND_ITEM_GAP_PX,
|
|
538
|
+
textGap: LEGEND_TEXT_GAP_PX,
|
|
539
|
+
inverse: true,
|
|
540
|
+
pieces: [...bins, { lt: 0, label: LD_MISSING_NAME, color: LD_MISSING_COLOR }],
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
/** The series of the points of no r²: gray, under the colored points. */
|
|
544
|
+
function missingSeries(data) {
|
|
545
|
+
return {
|
|
546
|
+
type: "scatter",
|
|
547
|
+
name: LD_MISSING_NAME,
|
|
548
|
+
symbolSize: POINT_PX,
|
|
549
|
+
z: DENSE_NULL_Z,
|
|
550
|
+
itemStyle: { color: LD_MISSING_COLOR, borderColor: CHART_INK, borderWidth: POINT_OUTLINE_PX },
|
|
551
|
+
tooltip: { formatter: "{b}<br/>{a}" },
|
|
552
|
+
data: [...data],
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
/** The series of the stored zeros: the upward triangle at the drawn height, in the color of the bin of each. */
|
|
556
|
+
function zeroSeries(data) {
|
|
557
|
+
return {
|
|
558
|
+
type: "scatter",
|
|
559
|
+
name: "p = 0",
|
|
560
|
+
symbol: BELOW_RESOLUTION_SYMBOL,
|
|
561
|
+
symbolSize: BELOW_RESOLUTION_SYMBOL_PX,
|
|
562
|
+
itemStyle: { borderColor: CHART_INK, borderWidth: POINT_OUTLINE_PX },
|
|
563
|
+
tooltip: BELOW_RESOLUTION_TOOLTIP,
|
|
564
|
+
data: [...data],
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
/** The series of the lead variant: one purple diamond over the points, with its name on top. */
|
|
568
|
+
function leadSeries(data) {
|
|
569
|
+
return {
|
|
570
|
+
type: "scatter",
|
|
571
|
+
name: LEAD_VARIANT_NAME,
|
|
572
|
+
symbol: "diamond",
|
|
573
|
+
symbolSize: LEAD_PX,
|
|
574
|
+
z: 10,
|
|
575
|
+
itemStyle: { color: LEAD_VARIANT_COLOR, borderColor: CHART_INK, borderWidth: POINT_OUTLINE_PX },
|
|
576
|
+
label: { show: true, position: "top", distance: 4, color: CHART_INK, formatter: "{b}" },
|
|
577
|
+
tooltip: { formatter: "{b}<br/>{a}" },
|
|
578
|
+
data: [data],
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* The genes of the track inside the window, each one clipped to it, in the order of their starts. A row whose
|
|
583
|
+
* start or end is not numeric draws no gene.
|
|
584
|
+
*
|
|
585
|
+
* A gene takes the room of its line and of its name under the line. The name centers under the line and stays
|
|
586
|
+
* inside the window. The lane of a gene is the first lane whose last gene ends a gap before that room starts.
|
|
587
|
+
*/
|
|
588
|
+
function windowGenes(blockId, track, window) {
|
|
589
|
+
for (const column of [track.start, track.end, track.label]) {
|
|
590
|
+
if (track.rows.length > 0 && !columnPresent(column, track.rows, track.columns)) {
|
|
591
|
+
return err(chartProblem(blockId, `The track table holds no "${column}" column.`));
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
const perCharacter = ((GENE_CHARACTER_SHARE * COLUMN_PLOT_FRAME.textPx) / COLUMN_PLOT_FRAME.widthPx) * (window.max - window.min);
|
|
595
|
+
const spans = [];
|
|
596
|
+
for (const [place, row] of track.rows.entries()) {
|
|
597
|
+
const a = toNumber(row[track.start]);
|
|
598
|
+
const b = toNumber(row[track.end]);
|
|
599
|
+
if (a === null || b === null)
|
|
600
|
+
continue;
|
|
601
|
+
const start = Math.max(Math.min(a, b), window.min);
|
|
602
|
+
const end = Math.min(Math.max(a, b), window.max);
|
|
603
|
+
if (start > end)
|
|
604
|
+
continue;
|
|
605
|
+
spans.push({ start, end, name: String(row[track.label] ?? ""), place });
|
|
606
|
+
}
|
|
607
|
+
spans.sort((a, b) => a.start - b.start || a.place - b.place);
|
|
608
|
+
const laneOf = firstFreeLanes(spans.length);
|
|
609
|
+
const genes = [];
|
|
610
|
+
for (const span of spans) {
|
|
611
|
+
const half = (span.name.length * perCharacter) / 2;
|
|
612
|
+
const center = Math.min(Math.max((span.start + span.end) / 2, window.min + half), window.max - half);
|
|
613
|
+
const from = Math.min(span.start, center - half);
|
|
614
|
+
const to = Math.max(span.end, center + half) + GENE_GAP_CHARACTERS * perCharacter;
|
|
615
|
+
genes.push({ start: span.start, end: span.end, name: span.name, center, lane: laneOf(from, to) });
|
|
616
|
+
}
|
|
617
|
+
return ok(genes);
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* The series of the genes: one point with no symbol under the middle of each gene with its name as the label,
|
|
621
|
+
* and one line for each gene. A gene symbol prints in italics, as the nomenclature asks.
|
|
622
|
+
*/
|
|
623
|
+
function geneSeries(genes, yAxisIndex) {
|
|
624
|
+
return {
|
|
625
|
+
type: "scatter",
|
|
626
|
+
name: GENE_NAMES,
|
|
627
|
+
xAxisIndex: 1,
|
|
628
|
+
yAxisIndex,
|
|
629
|
+
silent: true,
|
|
630
|
+
symbolSize: 0,
|
|
631
|
+
tooltip: { show: false },
|
|
632
|
+
label: { show: true, position: "bottom", distance: 2, color: CHART_INK, fontStyle: "italic", formatter: "{b}" },
|
|
633
|
+
data: genes.map((gene) => ({ name: gene.name, value: [gene.center, gene.lane + GENE_LINE_PLACE] })),
|
|
634
|
+
markLine: {
|
|
635
|
+
silent: false,
|
|
636
|
+
symbol: "none",
|
|
637
|
+
label: { show: false },
|
|
638
|
+
tooltip: { formatter: "{b}" },
|
|
639
|
+
lineStyle: { color: CHART_INK, width: GENE_WIDTH_PX, type: "solid" },
|
|
640
|
+
data: genes.map((gene) => [
|
|
641
|
+
{ name: gene.name, coord: [gene.start, gene.lane + GENE_LINE_PLACE] },
|
|
642
|
+
{ coord: [gene.end, gene.lane + GENE_LINE_PLACE] },
|
|
643
|
+
]),
|
|
644
|
+
},
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* The bands of one chart. A band past `LANES_IN_BODY` lanes grows the body by one lane of the default body for
|
|
649
|
+
* each further lane, and each band keeps its height in pixels. The body stops at the largest body, and past it
|
|
650
|
+
* the plot keeps its height and the lanes share the rest of the gene band.
|
|
651
|
+
*/
|
|
652
|
+
function bandLayout(lanes, withRate) {
|
|
653
|
+
const grown = Math.max(0, lanes - LANES_IN_BODY) * GENE_LANE;
|
|
654
|
+
const bodyPx = Math.min(CHART_BODY_MAX_PX, (CHART_BODY_PX * (100 + grown)) / 100);
|
|
655
|
+
const share = CHART_BODY_PX / bodyPx;
|
|
656
|
+
const trackBottom = lanes > 0 ? TRACK_BOTTOM * share : 0;
|
|
657
|
+
const top = PLOT_TOP * share;
|
|
658
|
+
const axisBand = AXIS_BAND * share;
|
|
659
|
+
const plot = (100 - PLOT_TOP - AXIS_BAND - (lanes > 0 ? TRACK_BOTTOM : 0) - Math.min(lanes, LANES_IN_BODY) * GENE_LANE) * share;
|
|
660
|
+
const band = 100 - top - axisBand - trackBottom - plot;
|
|
661
|
+
return {
|
|
662
|
+
top,
|
|
663
|
+
plotBottom: trackBottom + band + axisBand,
|
|
664
|
+
trackBottom,
|
|
665
|
+
band,
|
|
666
|
+
right: withRate ? PLOT_RIGHT_WITH_RATE : PLOT_RIGHT,
|
|
667
|
+
bodyPx,
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
/** One share of the chart as a percent string. */
|
|
671
|
+
function pct(share) {
|
|
672
|
+
return `${Number(share.toFixed(2))}%`;
|
|
673
|
+
}
|
|
674
|
+
/** The locuszoom figure module. */
|
|
675
|
+
export const LOCUSZOOM_FIGURE = { reads: READS, derive: deriveLocusZoom };
|