@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
|
@@ -13,8 +13,16 @@
|
|
|
13
13
|
* The walk resolves nothing and it opens no file. It reports what the tree holds, and a caller decides
|
|
14
14
|
* which part it needs.
|
|
15
15
|
*/
|
|
16
|
-
import { channelColumn } from "../contracts/report-blocks.js";
|
|
16
|
+
import { channelColumn, channelOrder } from "../contracts/report-blocks.js";
|
|
17
17
|
import { holdsADriftedExponent } from "../report-render/number-format.js";
|
|
18
|
+
/** The slot of the tree of one axis. */
|
|
19
|
+
export function treeSlot(axis) {
|
|
20
|
+
return `tree:${axis}`;
|
|
21
|
+
}
|
|
22
|
+
/** The slot of the statistic at one index of a chart block. */
|
|
23
|
+
export function statisticSlot(index) {
|
|
24
|
+
return `statistic:${index}`;
|
|
25
|
+
}
|
|
18
26
|
/**
|
|
19
27
|
* A free-standing token that looks like a number, with an optional decimal part and an optional percent
|
|
20
28
|
* sign.
|
|
@@ -69,9 +77,10 @@ function proseWarnings(blockId, prose) {
|
|
|
69
77
|
/**
|
|
70
78
|
* Each column that the grammar of one chart names.
|
|
71
79
|
*
|
|
72
|
-
* The quick path names its
|
|
73
|
-
*
|
|
74
|
-
* beside its column, thus a transformed channel names the same column as a plain one.
|
|
80
|
+
* The quick path names its channels and its label. A composition names the channels of each series, the
|
|
81
|
+
* lower bound of a band, the label of each series, the column of each rank rule, and its facet. A transform
|
|
82
|
+
* rides beside its column, thus a transformed channel names the same column as a plain one. A channel that
|
|
83
|
+
* sorts its categories names its sort column right after its own column.
|
|
75
84
|
*
|
|
76
85
|
* A name comes back one time, in the order that the grammar states it. Thus a refusal names each absent
|
|
77
86
|
* column one time.
|
|
@@ -83,8 +92,10 @@ function chartColumns(block) {
|
|
|
83
92
|
columns.push(column);
|
|
84
93
|
};
|
|
85
94
|
const addChannel = (channel) => {
|
|
86
|
-
if (channel
|
|
87
|
-
|
|
95
|
+
if (channel === undefined)
|
|
96
|
+
return;
|
|
97
|
+
add(channelColumn(channel));
|
|
98
|
+
add(channelOrder(channel)?.by);
|
|
88
99
|
};
|
|
89
100
|
const encoding = block.encoding;
|
|
90
101
|
if (encoding !== undefined) {
|
|
@@ -93,6 +104,19 @@ function chartColumns(block) {
|
|
|
93
104
|
addChannel(encoding.group);
|
|
94
105
|
addChannel(encoding.value);
|
|
95
106
|
add(encoding.label);
|
|
107
|
+
addChannel(encoding.color);
|
|
108
|
+
addChannel(encoding.size);
|
|
109
|
+
addChannel(encoding.low);
|
|
110
|
+
addChannel(encoding.high);
|
|
111
|
+
addChannel(encoding.facet);
|
|
112
|
+
addChannel(encoding.shape);
|
|
113
|
+
addChannel(encoding.p);
|
|
114
|
+
addChannel(encoding.censor);
|
|
115
|
+
addChannel(encoding.risk);
|
|
116
|
+
addChannel(encoding.hit);
|
|
117
|
+
addChannel(encoding.metric);
|
|
118
|
+
for (const column of encoding.tracks ?? [])
|
|
119
|
+
add(column);
|
|
96
120
|
}
|
|
97
121
|
const composition = block.composition;
|
|
98
122
|
if (composition !== undefined) {
|
|
@@ -102,14 +126,47 @@ function chartColumns(block) {
|
|
|
102
126
|
addChannel(series.encoding.y0);
|
|
103
127
|
addChannel(series.encoding.group);
|
|
104
128
|
add(series.encoding.label);
|
|
129
|
+
addChannel(series.encoding.color);
|
|
130
|
+
addChannel(series.encoding.size);
|
|
131
|
+
addChannel(series.encoding.low);
|
|
132
|
+
addChannel(series.encoding.high);
|
|
105
133
|
}
|
|
106
134
|
for (const annotation of composition.annotations ?? []) {
|
|
107
135
|
if (annotation.kind === "point-labels")
|
|
108
136
|
add(annotation.column);
|
|
109
137
|
}
|
|
138
|
+
addChannel(composition.facet);
|
|
110
139
|
}
|
|
111
140
|
return columns;
|
|
112
141
|
}
|
|
142
|
+
/** Each column of its own table that the track of a chart names: the start, the end, the label, and the length. */
|
|
143
|
+
function trackColumns(track) {
|
|
144
|
+
return [track.start, track.end, track.label, ...(track.length !== undefined ? [track.length] : [])];
|
|
145
|
+
}
|
|
146
|
+
/** Each column of its own table that the tree of one axis names: the parent, the child, and the height. */
|
|
147
|
+
function treeColumns(tree) {
|
|
148
|
+
return [tree.parent, tree.child, tree.height];
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* The references of one chart block, in the order of the block: the binding, the track, the tree of `x`, the
|
|
152
|
+
* tree of `y`, and each statistic. Each one carries its slot.
|
|
153
|
+
*/
|
|
154
|
+
function chartReferences(block) {
|
|
155
|
+
const references = [{ blockId: block.id, reference: block.binding, slot: "binding", encodingColumns: chartColumns(block) }];
|
|
156
|
+
if (block.track !== undefined) {
|
|
157
|
+
references.push({ blockId: block.id, reference: block.track.binding, slot: "track", encodingColumns: trackColumns(block.track) });
|
|
158
|
+
}
|
|
159
|
+
for (const axis of ["x", "y"]) {
|
|
160
|
+
const tree = block.trees?.[axis];
|
|
161
|
+
if (tree !== undefined) {
|
|
162
|
+
references.push({ blockId: block.id, reference: tree.binding, slot: treeSlot(axis), encodingColumns: treeColumns(tree) });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
for (const [index, statistic] of (block.statistics ?? []).entries()) {
|
|
166
|
+
references.push({ blockId: block.id, reference: statistic.value, slot: statisticSlot(index) });
|
|
167
|
+
}
|
|
168
|
+
return references;
|
|
169
|
+
}
|
|
113
170
|
/**
|
|
114
171
|
* Walk a block tree, and report each reference, each repeated id, and each free numeral.
|
|
115
172
|
*
|
|
@@ -152,7 +209,7 @@ export function walkBlocks(blocks) {
|
|
|
152
209
|
references.push({ blockId: block.id, reference: block.value });
|
|
153
210
|
return;
|
|
154
211
|
case "chart":
|
|
155
|
-
references.push(
|
|
212
|
+
references.push(...chartReferences(block));
|
|
156
213
|
return;
|
|
157
214
|
case "table":
|
|
158
215
|
case "figure":
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
*/
|
|
28
28
|
import { type ReportDocument } from "../contracts/report-blocks.js";
|
|
29
29
|
import type { UnresolvedReference } from "../contracts/report-reference.js";
|
|
30
|
-
import { type ReportWarning } from "./block-walk.js";
|
|
30
|
+
import { type ChartSlot, type ReportWarning } from "./block-walk.js";
|
|
31
31
|
import type { DraftDocument } from "./draft.js";
|
|
32
32
|
import type { ReportSnapshot } from "./reference-resolver.js";
|
|
33
33
|
/** A schema conformance gap, reduced to a path and a message. */
|
|
@@ -41,10 +41,14 @@ export interface DuplicateIdGap {
|
|
|
41
41
|
kind: "duplicate-id";
|
|
42
42
|
id: string;
|
|
43
43
|
}
|
|
44
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* An unresolved reference gap. It ties the block that carries the reference to the reason it did not resolve.
|
|
46
|
+
* `slot` names the place of the reference inside a chart block: the binding, the track, a tree, or one statistic.
|
|
47
|
+
*/
|
|
45
48
|
export interface UnresolvedReferenceGap {
|
|
46
49
|
kind: "unresolved-reference";
|
|
47
50
|
blockId: string;
|
|
51
|
+
slot?: ChartSlot;
|
|
48
52
|
failure: UnresolvedReference;
|
|
49
53
|
}
|
|
50
54
|
/** One completeness gap of a draft. The kind set is closed. */
|
|
@@ -76,7 +76,12 @@ export function finishDraft(draft, snapshot, derivations = []) {
|
|
|
76
76
|
for (const entry of references) {
|
|
77
77
|
const result = validateReferenceStructure(entry.reference, snapshot, entry.encodingColumns);
|
|
78
78
|
if (result.isErr()) {
|
|
79
|
-
gaps.push({
|
|
79
|
+
gaps.push({
|
|
80
|
+
kind: "unresolved-reference",
|
|
81
|
+
blockId: entry.blockId,
|
|
82
|
+
...(entry.slot !== undefined ? { slot: entry.slot } : {}),
|
|
83
|
+
failure: result.error,
|
|
84
|
+
});
|
|
80
85
|
}
|
|
81
86
|
}
|
|
82
87
|
// A schema failure always pushes a gap. Thus an empty gap list means the parse passed. The
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
import { type Result } from "neverthrow";
|
|
21
21
|
import type { Suspension } from "../workflows/suspension.js";
|
|
22
22
|
import { type ReferenceResolver } from "./reference-resolver.js";
|
|
23
|
+
/** One artifact row, as plain cells. A text-backed format holds a numeric column as a string. */
|
|
24
|
+
type Row = Record<string, string | number>;
|
|
23
25
|
/**
|
|
24
26
|
* One artifact that the host fast path could not read, addressed for the extraction arm. The `hash` pins
|
|
25
27
|
* the exact bytes, thus the arm reads the same artifact that the identity layer verified.
|
|
@@ -116,6 +118,11 @@ export type TabularFormat = "csv" | "tsv" | "json" | "parquet";
|
|
|
116
118
|
* drops the same value to `None`, thus the host arm and the sandbox arm read the same bytes the same way.
|
|
117
119
|
*/
|
|
118
120
|
export declare function coerceCell(value: unknown): string | number | undefined;
|
|
121
|
+
/**
|
|
122
|
+
* Parse a delimited file into rows. The first record is the header. A ragged record and a repeated header
|
|
123
|
+
* name are structural doubt, thus each gives back `undefined` and the file falls through.
|
|
124
|
+
*/
|
|
125
|
+
export declare function parseDelimited(text: string, delimiter: string): Row[] | undefined;
|
|
119
126
|
/**
|
|
120
127
|
* Make the production resolver over the workspace root and the optional extraction arm.
|
|
121
128
|
*
|
|
@@ -125,3 +132,4 @@ export declare function coerceCell(value: unknown): string | number | undefined;
|
|
|
125
132
|
* artifact on both paths, and never one read for each reference.
|
|
126
133
|
*/
|
|
127
134
|
export declare function createProductionResolver(deps: ProductionResolverDeps): ReferenceResolver;
|
|
135
|
+
export {};
|
|
@@ -236,7 +236,7 @@ function parseRecords(text, delimiter) {
|
|
|
236
236
|
* Parse a delimited file into rows. The first record is the header. A ragged record and a repeated header
|
|
237
237
|
* name are structural doubt, thus each gives back `undefined` and the file falls through.
|
|
238
238
|
*/
|
|
239
|
-
function parseDelimited(text, delimiter) {
|
|
239
|
+
export function parseDelimited(text, delimiter) {
|
|
240
240
|
const records = parseRecords(text, delimiter);
|
|
241
241
|
if (records === undefined) {
|
|
242
242
|
return undefined;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* each reference against the pinned evidence, and it warns about the prose of each block.
|
|
9
9
|
*/
|
|
10
10
|
import type { UnresolvedReference } from "../contracts/report-reference.js";
|
|
11
|
-
import { type AnyBlock, type CollectedReference, type ReportWarning } from "./block-walk.js";
|
|
11
|
+
import { type AnyBlock, type ChartSlot, type CollectedReference, type ReportWarning } from "./block-walk.js";
|
|
12
12
|
import { type ReferenceResolver, type ReportSnapshot, type ResolvedValue } from "./reference-resolver.js";
|
|
13
13
|
export type { ReportWarning };
|
|
14
14
|
/**
|
|
@@ -23,9 +23,13 @@ export interface SchemaIssue {
|
|
|
23
23
|
path: string;
|
|
24
24
|
message: string;
|
|
25
25
|
}
|
|
26
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* One reference that did not resolve, tied to the block that carries it. `slot` names the place of the
|
|
28
|
+
* reference inside a chart block, thus a reviewer tells a failed track or statistic from a failed binding.
|
|
29
|
+
*/
|
|
27
30
|
export interface ResolutionFailure {
|
|
28
31
|
blockId: string;
|
|
32
|
+
slot?: ChartSlot;
|
|
29
33
|
failure: UnresolvedReference;
|
|
30
34
|
}
|
|
31
35
|
/**
|
|
@@ -44,17 +48,22 @@ export type ReportValidation = {
|
|
|
44
48
|
warnings: ReportWarning[];
|
|
45
49
|
};
|
|
46
50
|
/**
|
|
47
|
-
* Match each column that a chart
|
|
51
|
+
* Match each column that a chart names against the table that its binding, its track, or a tree resolved to.
|
|
48
52
|
*
|
|
49
|
-
* The binding schema
|
|
50
|
-
* bound resolver broke its own contract. That is reported and never ignored, because a chart with no
|
|
53
|
+
* The binding, the track, and the tree schema admit a table reference only, thus a resolved value of another type means
|
|
54
|
+
* that the bound resolver broke its own contract. That is reported and never ignored, because a chart with no
|
|
51
55
|
* table behind it renders nothing and a silent skip would pass it as grounded.
|
|
52
56
|
*/
|
|
53
57
|
export declare function checkChartEncoding(entry: CollectedReference, value: ResolvedValue): UnresolvedReference | undefined;
|
|
54
58
|
/** The resolved values and the failures of one resolution pass over the references of a block tree. */
|
|
55
59
|
export interface ReferenceResolution {
|
|
56
|
-
/** The resolved value of each block whose
|
|
60
|
+
/** The resolved value of the binding of each block whose binding resolved, keyed by its block id. */
|
|
57
61
|
resolvedByBlock: Map<string, ResolvedValue>;
|
|
62
|
+
/**
|
|
63
|
+
* The resolved value of each other slot of a chart, keyed by the block id and then by the slot: the track,
|
|
64
|
+
* each tree, and each statistic. A chart that binds none of them holds no entry.
|
|
65
|
+
*/
|
|
66
|
+
resolvedBySlot: Map<string, Map<ChartSlot, ResolvedValue>>;
|
|
58
67
|
/** Each reference that did not resolve, or whose chart encoding named an absent column. */
|
|
59
68
|
failures: ResolutionFailure[];
|
|
60
69
|
}
|
|
@@ -62,10 +71,11 @@ export interface ReferenceResolution {
|
|
|
62
71
|
* Resolve each reference of a block tree under the concurrency bound, and run the chart-encoding match.
|
|
63
72
|
*
|
|
64
73
|
* The walk collects each reference one time, `allWithConcurrency` bounds the fan-out, and
|
|
65
|
-
* `checkChartEncoding` catches a chart that plots a column which the bound table does not hold.
|
|
66
|
-
* gate and the preview share this one pass, thus the two refuse the same references. A
|
|
67
|
-
* whose
|
|
68
|
-
*
|
|
74
|
+
* `checkChartEncoding` catches a chart that plots a column which the bound table or the track does not hold.
|
|
75
|
+
* The record gate and the preview share this one pass, thus the two refuse the same references. A
|
|
76
|
+
* value-bearing block whose binding resolved carries its value in the map, and the track and each statistic
|
|
77
|
+
* of a chart carry their values under their slots. A claim or a citation reference reads no value from the
|
|
78
|
+
* map. The walk collects every failure, thus a reviewer sees each block and each slot that broke.
|
|
69
79
|
*/
|
|
70
80
|
export declare function resolveDocumentReferences(sections: readonly AnyBlock[], snapshot: ReportSnapshot, resolver: ReferenceResolver): Promise<ReferenceResolution>;
|
|
71
81
|
/** Resolve each reference, collect the schema conformance and the resolution outcomes, and warn on prose. */
|
|
@@ -18,11 +18,17 @@ import { columnsHeldByNoRow } from "./reference-resolver.js";
|
|
|
18
18
|
* unbounded fan-out would open one read for each reference at the same moment.
|
|
19
19
|
*/
|
|
20
20
|
export const RESOLUTION_CONCURRENCY = 8;
|
|
21
|
+
/** The role of one chart slot whose columns name a table: the binding, the track, or a tree. */
|
|
22
|
+
function tableRole(slot) {
|
|
23
|
+
if (slot === "track")
|
|
24
|
+
return "track";
|
|
25
|
+
return slot === "tree:x" || slot === "tree:y" ? "tree" : "binding";
|
|
26
|
+
}
|
|
21
27
|
/**
|
|
22
|
-
* Match each column that a chart
|
|
28
|
+
* Match each column that a chart names against the table that its binding, its track, or a tree resolved to.
|
|
23
29
|
*
|
|
24
|
-
* The binding schema
|
|
25
|
-
* bound resolver broke its own contract. That is reported and never ignored, because a chart with no
|
|
30
|
+
* The binding, the track, and the tree schema admit a table reference only, thus a resolved value of another type means
|
|
31
|
+
* that the bound resolver broke its own contract. That is reported and never ignored, because a chart with no
|
|
26
32
|
* table behind it renders nothing and a silent skip would pass it as grounded.
|
|
27
33
|
*/
|
|
28
34
|
export function checkChartEncoding(entry, value) {
|
|
@@ -30,8 +36,9 @@ export function checkChartEncoding(entry, value) {
|
|
|
30
36
|
if (encodingColumns === undefined || encodingColumns.length === 0) {
|
|
31
37
|
return undefined;
|
|
32
38
|
}
|
|
39
|
+
const role = tableRole(entry.slot);
|
|
33
40
|
if (value.type !== "table") {
|
|
34
|
-
return { reference: entry.reference, reason: "locator-out-of-range", detail: `the chart
|
|
41
|
+
return { reference: entry.reference, reason: "locator-out-of-range", detail: `the chart ${role} resolved to a ${value.type} and not to a table` };
|
|
35
42
|
}
|
|
36
43
|
const absent = columnsHeldByNoRow(value.rows, encodingColumns);
|
|
37
44
|
if (absent.length === 0) {
|
|
@@ -40,17 +47,18 @@ export function checkChartEncoding(entry, value) {
|
|
|
40
47
|
return {
|
|
41
48
|
reference: entry.reference,
|
|
42
49
|
reason: "locator-out-of-range",
|
|
43
|
-
detail: `the encoding names column ${absent.join(", ")}, which the bound table does not hold`,
|
|
50
|
+
detail: `the ${role === "binding" ? "encoding" : role} names column ${absent.join(", ")}, which the ${role === "binding" ? "bound" : role} table does not hold`,
|
|
44
51
|
};
|
|
45
52
|
}
|
|
46
53
|
/**
|
|
47
54
|
* Resolve each reference of a block tree under the concurrency bound, and run the chart-encoding match.
|
|
48
55
|
*
|
|
49
56
|
* The walk collects each reference one time, `allWithConcurrency` bounds the fan-out, and
|
|
50
|
-
* `checkChartEncoding` catches a chart that plots a column which the bound table does not hold.
|
|
51
|
-
* gate and the preview share this one pass, thus the two refuse the same references. A
|
|
52
|
-
* whose
|
|
53
|
-
*
|
|
57
|
+
* `checkChartEncoding` catches a chart that plots a column which the bound table or the track does not hold.
|
|
58
|
+
* The record gate and the preview share this one pass, thus the two refuse the same references. A
|
|
59
|
+
* value-bearing block whose binding resolved carries its value in the map, and the track and each statistic
|
|
60
|
+
* of a chart carry their values under their slots. A claim or a citation reference reads no value from the
|
|
61
|
+
* map. The walk collects every failure, thus a reviewer sees each block and each slot that broke.
|
|
54
62
|
*/
|
|
55
63
|
export async function resolveDocumentReferences(sections, snapshot, resolver) {
|
|
56
64
|
const { references } = walkBlocks(sections);
|
|
@@ -62,19 +70,27 @@ export async function resolveDocumentReferences(sections, snapshot, resolver) {
|
|
|
62
70
|
const resolved = await allWithConcurrency(references.map((entry) => () => resolver.resolve(entry.reference, snapshot).then((result) => ({ entry, result }))), RESOLUTION_CONCURRENCY);
|
|
63
71
|
const failures = [];
|
|
64
72
|
const resolvedByBlock = new Map();
|
|
73
|
+
const resolvedBySlot = new Map();
|
|
65
74
|
for (const { entry, result } of resolved) {
|
|
75
|
+
const slot = entry.slot !== undefined ? { slot: entry.slot } : {};
|
|
66
76
|
if (result.isErr()) {
|
|
67
|
-
failures.push({ blockId: entry.blockId, failure: result.error });
|
|
77
|
+
failures.push({ blockId: entry.blockId, ...slot, failure: result.error });
|
|
68
78
|
continue;
|
|
69
79
|
}
|
|
70
80
|
const encodingFailure = checkChartEncoding(entry, result.value);
|
|
71
81
|
if (encodingFailure !== undefined) {
|
|
72
|
-
failures.push({ blockId: entry.blockId, failure: encodingFailure });
|
|
82
|
+
failures.push({ blockId: entry.blockId, ...slot, failure: encodingFailure });
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (entry.slot === undefined || entry.slot === "binding") {
|
|
86
|
+
resolvedByBlock.set(entry.blockId, result.value);
|
|
73
87
|
continue;
|
|
74
88
|
}
|
|
75
|
-
|
|
89
|
+
const slots = resolvedBySlot.get(entry.blockId) ?? new Map();
|
|
90
|
+
slots.set(entry.slot, result.value);
|
|
91
|
+
resolvedBySlot.set(entry.blockId, slots);
|
|
76
92
|
}
|
|
77
|
-
return { resolvedByBlock, failures };
|
|
93
|
+
return { resolvedByBlock, resolvedBySlot, failures };
|
|
78
94
|
}
|
|
79
95
|
/** Resolve each reference, collect the schema conformance and the resolution outcomes, and warn on prose. */
|
|
80
96
|
export async function validateReport(document, snapshot, resolver) {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SVG export of one chart: the server-side render of the chart runtime at a journal column width.
|
|
3
|
+
*
|
|
4
|
+
* The export renders the option of the page with every row inline, in the print theme, through the same
|
|
5
|
+
* named renderers as the page. Thus the chart that a reader sees is the chart that the paper gets.
|
|
6
|
+
*
|
|
7
|
+
* The bytes are a pure function of the option. The chart runtime numbers its instances and its style
|
|
8
|
+
* classes with counters of the whole process, thus two renders of one option differ in those tokens alone.
|
|
9
|
+
* The export renumbers each token in order of first appearance, and two renders give one byte sequence.
|
|
10
|
+
*/
|
|
11
|
+
import type * as Echarts from "echarts";
|
|
12
|
+
import { type Result } from "neverthrow";
|
|
13
|
+
import type { EchartOption } from "./chart.js";
|
|
14
|
+
import { type ChartExportSize } from "./design.js";
|
|
15
|
+
import type { DataAsset } from "./table-data.js";
|
|
16
|
+
import type { RenderProblem } from "./types.js";
|
|
17
|
+
/** The module of the chart runtime. The SVG export draws with it on the server. */
|
|
18
|
+
export type ChartRuntime = typeof Echarts;
|
|
19
|
+
/**
|
|
20
|
+
* Load the chart runtime.
|
|
21
|
+
*
|
|
22
|
+
* The runtime loads on the first export and not with this module. The harness entry reaches this module, thus
|
|
23
|
+
* a static import makes each process that loads the harness pay for the runtime, also a process that exports
|
|
24
|
+
* no chart. The module cache keeps the first load, thus a later call costs nothing.
|
|
25
|
+
*/
|
|
26
|
+
export declare function loadChartRuntime(): Promise<ChartRuntime>;
|
|
27
|
+
/** The two SVG files of one chart: the single journal column and the double journal column. */
|
|
28
|
+
export interface ChartSvgs {
|
|
29
|
+
readonly single: DataAsset;
|
|
30
|
+
readonly double: DataAsset;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Render one option to SVG text at one column size.
|
|
34
|
+
*
|
|
35
|
+
* The export registers the print theme and each named renderer before it draws, and each registration of
|
|
36
|
+
* one name gives the same theme or the same function. The chart runtime is code outside the harness, thus its
|
|
37
|
+
* throw becomes the message of an `err`. The instance is disposed in every case, thus no render leaves a timer
|
|
38
|
+
* behind.
|
|
39
|
+
*/
|
|
40
|
+
export declare function renderChartSvg(echarts: ChartRuntime, option: EchartOption, size: ChartExportSize): Result<string, string>;
|
|
41
|
+
/**
|
|
42
|
+
* The two SVG files of one chart, or `undefined` for a chart past the export bound.
|
|
43
|
+
*
|
|
44
|
+
* The option is the chart with every row inline. A chart whose plotted point count passes the crowd row
|
|
45
|
+
* count gets no staged SVG. The page builds the hybrid SVG of such a chart, or the menu states that the PNG
|
|
46
|
+
* serves a chart that holds no point layer. An option that the chart runtime cannot
|
|
47
|
+
* draw refuses, and the refusal names the block.
|
|
48
|
+
*
|
|
49
|
+
* `bodyPx` is the height of the page chart body. A taller body draws each column at a taller height, thus a
|
|
50
|
+
* row of the figure keeps its share of the height.
|
|
51
|
+
*/
|
|
52
|
+
export declare function chartSvgAssets(echarts: ChartRuntime, blockId: string, option: EchartOption, bodyPx?: number): Result<ChartSvgs | undefined, RenderProblem>;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SVG export of one chart: the server-side render of the chart runtime at a journal column width.
|
|
3
|
+
*
|
|
4
|
+
* The export renders the option of the page with every row inline, in the print theme, through the same
|
|
5
|
+
* named renderers as the page. Thus the chart that a reader sees is the chart that the paper gets.
|
|
6
|
+
*
|
|
7
|
+
* The bytes are a pure function of the option. The chart runtime numbers its instances and its style
|
|
8
|
+
* classes with counters of the whole process, thus two renders of one option differ in those tokens alone.
|
|
9
|
+
* The export renumbers each token in order of first appearance, and two renders give one byte sequence.
|
|
10
|
+
*/
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import { err, ok } from "neverthrow";
|
|
13
|
+
import { exportOption, OUTLINE_RENDERER, registerChartRenderers } from "./chart-renderers.js";
|
|
14
|
+
import { CHART_BODY_PX, CHART_EXPORT_SIZES, CHART_PRINT_THEME_NAME, CHART_PRINT_TEXT_PX, chartTheme, exportSizeFor, SCATTER_CROWD_ROWS, } from "./design.js";
|
|
15
|
+
import { millimeterRoot } from "./hybrid-svg.js";
|
|
16
|
+
/** One instance token of the chart runtime: a class, a clip path, or a gradient, for example `zr1-cls-2`. */
|
|
17
|
+
const INSTANCE_TOKEN = /zr\d+-[a-z]+-?\d+/g;
|
|
18
|
+
/**
|
|
19
|
+
* The places where the chart runtime writes an instance token: the value of an `id` or a `class` attribute,
|
|
20
|
+
* a `url(#...)` reference, and the style block. A text node is none of them, thus a category name that
|
|
21
|
+
* reads as a token keeps its text.
|
|
22
|
+
*/
|
|
23
|
+
const TOKEN_PLACES = /\s(?:id|class)="[^"]*"|url\(#[^)]*\)|<style[^>]*>[\s\S]*?<\/style>/g;
|
|
24
|
+
/** The count of hash characters in an asset name. It matches the name of a table asset. */
|
|
25
|
+
const HASH_CHARS = 12;
|
|
26
|
+
/**
|
|
27
|
+
* Load the chart runtime.
|
|
28
|
+
*
|
|
29
|
+
* The runtime loads on the first export and not with this module. The harness entry reaches this module, thus
|
|
30
|
+
* a static import makes each process that loads the harness pay for the runtime, also a process that exports
|
|
31
|
+
* no chart. The module cache keeps the first load, thus a later call costs nothing.
|
|
32
|
+
*/
|
|
33
|
+
export function loadChartRuntime() {
|
|
34
|
+
return import("echarts");
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Render one option to SVG text at one column size.
|
|
38
|
+
*
|
|
39
|
+
* The export registers the print theme and each named renderer before it draws, and each registration of
|
|
40
|
+
* one name gives the same theme or the same function. The chart runtime is code outside the harness, thus its
|
|
41
|
+
* throw becomes the message of an `err`. The instance is disposed in every case, thus no render leaves a timer
|
|
42
|
+
* behind.
|
|
43
|
+
*/
|
|
44
|
+
export function renderChartSvg(echarts, option, size) {
|
|
45
|
+
echarts.registerTheme(CHART_PRINT_THEME_NAME, chartTheme(CHART_PRINT_TEXT_PX));
|
|
46
|
+
registerChartRenderers({
|
|
47
|
+
// The runtime types each item dimension as a string or a number. Each item that the derivation writes
|
|
48
|
+
// for a named renderer holds numbers alone, thus the renderer reads a number at each dimension.
|
|
49
|
+
registerCustomSeries: (name, render) => echarts.registerCustomSeries(name, render),
|
|
50
|
+
});
|
|
51
|
+
let chart;
|
|
52
|
+
// The axis jitter of the chart runtime places a point that finds no free place with `Math.random`. The
|
|
53
|
+
// render below runs to its end in one synchronous call, thus the seeded sequence serves this render alone,
|
|
54
|
+
// and the same option gives the same points and the same bytes. The finally block puts the global back.
|
|
55
|
+
const random = Math.random;
|
|
56
|
+
Math.random = seededRandom(EXPORT_RANDOM_SEED);
|
|
57
|
+
try {
|
|
58
|
+
chart = echarts.init(null, CHART_PRINT_THEME_NAME, { renderer: "svg", ssr: true, width: size.widthPx, height: size.heightPx });
|
|
59
|
+
chart.setOption(exportOption(option, CHART_PRINT_TEXT_PX, size.widthPx, size.heightPx));
|
|
60
|
+
return ok(finishedSvg(chart.renderToSVGString(), size));
|
|
61
|
+
}
|
|
62
|
+
catch (cause) {
|
|
63
|
+
return err(cause instanceof Error ? cause.message : String(cause));
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
Math.random = random;
|
|
67
|
+
chart?.dispose();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** The seed of the sequence that stands for `Math.random` during one export. Any fixed value serves. */
|
|
71
|
+
const EXPORT_RANDOM_SEED = 0x2f6b1e35;
|
|
72
|
+
/**
|
|
73
|
+
* A deterministic sequence of numbers in `[0, 1)` from one seed: the mulberry32 generator. Each call of the
|
|
74
|
+
* returned function gives the next number.
|
|
75
|
+
*/
|
|
76
|
+
function seededRandom(seed) {
|
|
77
|
+
let state = seed >>> 0;
|
|
78
|
+
return () => {
|
|
79
|
+
state = (state + 0x6d2b79f5) >>> 0;
|
|
80
|
+
let t = state;
|
|
81
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
82
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
83
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The SVG text with its root at the millimeter size, and with each instance token renumbered.
|
|
88
|
+
*
|
|
89
|
+
* The root states the width and the height of the column in millimeters, and the view box keeps the pixel
|
|
90
|
+
* space of the render. Thus a vector editor opens the file at the column width. The renumber reads the
|
|
91
|
+
* places of the tokens in document order in one pass, thus the style block, each clip path, each gradient,
|
|
92
|
+
* and each element that names one of them agree.
|
|
93
|
+
*/
|
|
94
|
+
function finishedSvg(svg, size) {
|
|
95
|
+
const tokens = new Map();
|
|
96
|
+
const renumber = (token) => {
|
|
97
|
+
let next = tokens.get(token);
|
|
98
|
+
if (next === undefined) {
|
|
99
|
+
const family = /^zr\d+-([a-z]+)/.exec(token)?.[1] ?? "t";
|
|
100
|
+
next = `zr0-${family}-${tokens.size}`;
|
|
101
|
+
tokens.set(token, next);
|
|
102
|
+
}
|
|
103
|
+
return next;
|
|
104
|
+
};
|
|
105
|
+
return millimeterRoot(svg.replace(TOKEN_PLACES, (place) => place.replace(INSTANCE_TOKEN, renumber)), size);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The count of coordinates that one option draws, summed over its series.
|
|
109
|
+
*
|
|
110
|
+
* The count bounds the export. A cloud of many thousands of points gives an SVG of many megabytes, two times,
|
|
111
|
+
* thus the page builds the SVG of such a chart on a click, with the points as one raster layer. An empty slot draws nothing, thus it counts nothing. A radar
|
|
112
|
+
* polygon draws one coordinate for each indicator, and a violin outline draws two vertices for each grid
|
|
113
|
+
* point, thus each counts every coordinate that it draws.
|
|
114
|
+
*/
|
|
115
|
+
function plottedPoints(option) {
|
|
116
|
+
const series = option.series;
|
|
117
|
+
if (!Array.isArray(series))
|
|
118
|
+
return 0;
|
|
119
|
+
let count = 0;
|
|
120
|
+
for (const entry of series) {
|
|
121
|
+
if (typeof entry !== "object" || entry === null)
|
|
122
|
+
continue;
|
|
123
|
+
const fields = entry;
|
|
124
|
+
if (!Array.isArray(fields.data))
|
|
125
|
+
continue;
|
|
126
|
+
for (const item of fields.data) {
|
|
127
|
+
if (fields.type === "radar")
|
|
128
|
+
count += polygonCoordinates(item);
|
|
129
|
+
else if (fields.renderItem === OUTLINE_RENDERER)
|
|
130
|
+
count += outlineVertices(item);
|
|
131
|
+
else
|
|
132
|
+
count += drawnItem(item) ? 1 : 0;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return count;
|
|
136
|
+
}
|
|
137
|
+
/** True when one item draws a coordinate: a number, or a value whose members hold no empty slot. */
|
|
138
|
+
function drawnItem(item) {
|
|
139
|
+
const value = typeof item === "object" && item !== null && !Array.isArray(item) ? item.value : item;
|
|
140
|
+
if (typeof value === "number")
|
|
141
|
+
return true;
|
|
142
|
+
return Array.isArray(value) && value.every((member) => member !== null && member !== undefined && member !== "-");
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* The count of vertices that one violin outline draws. The item leads with the category, the extent, and the
|
|
146
|
+
* offset, then holds one pair for each grid point, and the outline draws each point on its right side and on
|
|
147
|
+
* its left side.
|
|
148
|
+
*/
|
|
149
|
+
function outlineVertices(item) {
|
|
150
|
+
return Array.isArray(item) ? Math.max(0, item.length - OUTLINE_LEAD_MEMBERS) : 0;
|
|
151
|
+
}
|
|
152
|
+
/** The members of an outline item before its grid pairs: the category, the two ends of the extent, and the offset. */
|
|
153
|
+
const OUTLINE_LEAD_MEMBERS = 4;
|
|
154
|
+
/** The count of coordinates that one radar polygon draws: each member of its value that is not empty. */
|
|
155
|
+
function polygonCoordinates(item) {
|
|
156
|
+
const value = typeof item === "object" && item !== null ? item.value : undefined;
|
|
157
|
+
return Array.isArray(value) ? value.filter((member) => member !== null && member !== undefined).length : 0;
|
|
158
|
+
}
|
|
159
|
+
/** One SVG asset: the content-addressed name, with the column in millimeters, and the text. */
|
|
160
|
+
function svgAsset(bytes, size) {
|
|
161
|
+
const hash = createHash("sha256").update(bytes).digest("hex").slice(0, HASH_CHARS);
|
|
162
|
+
return { name: `c-${hash}-${size.widthMm}mm.svg`, bytes };
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* The two SVG files of one chart, or `undefined` for a chart past the export bound.
|
|
166
|
+
*
|
|
167
|
+
* The option is the chart with every row inline. A chart whose plotted point count passes the crowd row
|
|
168
|
+
* count gets no staged SVG. The page builds the hybrid SVG of such a chart, or the menu states that the PNG
|
|
169
|
+
* serves a chart that holds no point layer. An option that the chart runtime cannot
|
|
170
|
+
* draw refuses, and the refusal names the block.
|
|
171
|
+
*
|
|
172
|
+
* `bodyPx` is the height of the page chart body. A taller body draws each column at a taller height, thus a
|
|
173
|
+
* row of the figure keeps its share of the height.
|
|
174
|
+
*/
|
|
175
|
+
export function chartSvgAssets(echarts, blockId, option, bodyPx = CHART_BODY_PX) {
|
|
176
|
+
if (plottedPoints(option) > SCATTER_CROWD_ROWS) {
|
|
177
|
+
return ok(undefined);
|
|
178
|
+
}
|
|
179
|
+
const singleSize = exportSizeFor(CHART_EXPORT_SIZES.single, bodyPx);
|
|
180
|
+
const doubleSize = exportSizeFor(CHART_EXPORT_SIZES.double, bodyPx);
|
|
181
|
+
const single = renderChartSvg(echarts, option, singleSize);
|
|
182
|
+
if (single.isErr())
|
|
183
|
+
return err(exportProblem(blockId, single.error));
|
|
184
|
+
const double = renderChartSvg(echarts, option, doubleSize);
|
|
185
|
+
if (double.isErr())
|
|
186
|
+
return err(exportProblem(blockId, double.error));
|
|
187
|
+
return ok({ single: svgAsset(single.value, singleSize), double: svgAsset(double.value, doubleSize) });
|
|
188
|
+
}
|
|
189
|
+
/** The refusal of an option that the chart runtime refused to draw. */
|
|
190
|
+
function exportProblem(blockId, cause) {
|
|
191
|
+
return { blockId, kind: "invalid-chart-input", detail: `The chart runtime refused to draw the SVG export of the chart: ${cause}` };
|
|
192
|
+
}
|