@inflexa-ai/harness 0.42.2 → 0.43.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/agents/sandbox/shared.d.ts +1 -6
- package/dist/agents/sandbox/shared.js +0 -15
- package/dist/app/chat-turn.js +13 -1
- package/dist/config/environment-stores.d.ts +4 -2
- package/dist/contracts/chat-events.d.ts +9 -1
- package/dist/contracts/chat-frame.d.ts +3 -3
- package/dist/contracts/chat-frame.js +8 -3
- package/dist/contracts/chat-parts.d.ts +4 -1
- package/dist/contracts/data-profile.d.ts +4 -6
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/report-blocks.d.ts +1364 -75
- package/dist/contracts/report-blocks.js +221 -29
- package/dist/contracts/report-reference.js +1 -1
- package/dist/contracts/schemas/chat-events.d.ts +13 -0
- package/dist/contracts/schemas/chat-events.js +5 -0
- package/dist/contracts/schemas/chat-parts.d.ts +2 -2
- package/dist/contracts/schemas/index.d.ts +1 -1
- package/dist/contracts/schemas/index.js +1 -1
- package/dist/execution/input-signature.d.ts +7 -13
- package/dist/execution/input-signature.js +0 -0
- package/dist/execution/run-canceler.d.ts +3 -2
- package/dist/execution/run-canceler.js +3 -2
- package/dist/execution/run-event-stream.d.ts +16 -2
- package/dist/execution/run-event-stream.js +54 -3
- package/dist/index.d.ts +1 -8
- package/dist/index.js +5 -16
- package/dist/input-scan/enrich.d.ts +0 -2
- package/dist/input-scan/enrich.js +0 -1
- package/dist/lib/metrics.js +6 -7
- package/dist/lib/otel-spans.d.ts +4 -9
- package/dist/lib/otel-spans.js +3 -8
- package/dist/loop/run-agent.d.ts +4 -2
- package/dist/loop/run-agent.js +38 -23
- package/dist/memory/thread-history.d.ts +1 -1
- package/dist/prompts/report-session.d.ts +1 -1
- package/dist/prompts/report-session.js +17 -0
- package/dist/provenance/exec-frame.d.ts +1 -1
- package/dist/report-model/authoring-grammar.d.ts +416 -17
- package/dist/report-model/authoring-grammar.js +25 -4
- package/dist/report-model/block-walk.d.ts +20 -4
- package/dist/report-model/block-walk.js +64 -7
- package/dist/report-model/draft-finish.d.ts +6 -2
- package/dist/report-model/draft-finish.js +6 -1
- package/dist/report-model/production-resolver.d.ts +8 -0
- package/dist/report-model/production-resolver.js +1 -1
- package/dist/report-model/validate.d.ts +20 -10
- package/dist/report-model/validate.js +29 -13
- package/dist/report-render/chart-export.d.ts +52 -0
- package/dist/report-render/chart-export.js +192 -0
- package/dist/report-render/chart-presets.d.ts +46 -47
- package/dist/report-render/chart-presets.js +31 -101
- package/dist/report-render/chart-renderers.d.ts +163 -0
- package/dist/report-render/chart-renderers.js +1055 -0
- package/dist/report-render/chart-toolbox.d.ts +124 -0
- package/dist/report-render/chart-toolbox.js +595 -0
- package/dist/report-render/chart.d.ts +60 -27
- package/dist/report-render/chart.js +2323 -524
- package/dist/report-render/design.d.ts +476 -16
- package/dist/report-render/design.js +390 -56
- package/dist/report-render/figures/common.d.ts +321 -0
- package/dist/report-render/figures/common.js +759 -0
- package/dist/report-render/figures/dendrogram.d.ts +36 -0
- package/dist/report-render/figures/dendrogram.js +130 -0
- package/dist/report-render/figures/dense.d.ts +111 -0
- package/dist/report-render/figures/dense.js +204 -0
- package/dist/report-render/figures/dotplot.d.ts +28 -0
- package/dist/report-render/figures/dotplot.js +234 -0
- package/dist/report-render/figures/embedding.d.ts +43 -0
- package/dist/report-render/figures/embedding.js +360 -0
- package/dist/report-render/figures/equal-units.d.ts +101 -0
- package/dist/report-render/figures/equal-units.js +215 -0
- package/dist/report-render/figures/forest.d.ts +20 -0
- package/dist/report-render/figures/forest.js +398 -0
- package/dist/report-render/figures/gsea.d.ts +30 -0
- package/dist/report-render/figures/gsea.js +391 -0
- package/dist/report-render/figures/heatmap.d.ts +40 -0
- package/dist/report-render/figures/heatmap.js +395 -0
- package/dist/report-render/figures/index.d.ts +135 -0
- package/dist/report-render/figures/index.js +100 -0
- package/dist/report-render/figures/km.d.ts +16 -0
- package/dist/report-render/figures/km.js +406 -0
- package/dist/report-render/figures/label-room.d.ts +130 -0
- package/dist/report-render/figures/label-room.js +341 -0
- package/dist/report-render/figures/locuszoom.d.ts +50 -0
- package/dist/report-render/figures/locuszoom.js +675 -0
- package/dist/report-render/figures/lollipop.d.ts +16 -0
- package/dist/report-render/figures/lollipop.js +247 -0
- package/dist/report-render/figures/ma.d.ts +20 -0
- package/dist/report-render/figures/ma.js +114 -0
- package/dist/report-render/figures/manhattan.d.ts +60 -0
- package/dist/report-render/figures/manhattan.js +343 -0
- package/dist/report-render/figures/oncoprint.d.ts +41 -0
- package/dist/report-render/figures/oncoprint.js +420 -0
- package/dist/report-render/figures/pca.d.ts +37 -0
- package/dist/report-render/figures/pca.js +241 -0
- package/dist/report-render/figures/qq.d.ts +30 -0
- package/dist/report-render/figures/qq.js +182 -0
- package/dist/report-render/figures/roc.d.ts +17 -0
- package/dist/report-render/figures/roc.js +116 -0
- package/dist/report-render/figures/sankey.d.ts +48 -0
- package/dist/report-render/figures/sankey.js +353 -0
- package/dist/report-render/figures/upset.d.ts +60 -0
- package/dist/report-render/figures/upset.js +316 -0
- package/dist/report-render/figures/volcano.d.ts +25 -0
- package/dist/report-render/figures/volcano.js +166 -0
- package/dist/report-render/hybrid-svg.d.ts +75 -0
- package/dist/report-render/hybrid-svg.js +504 -0
- package/dist/report-render/number-format.d.ts +19 -0
- package/dist/report-render/number-format.js +25 -1
- package/dist/report-render/page.d.ts +41 -4
- package/dist/report-render/page.js +247 -12
- package/dist/report-render/render.d.ts +13 -4
- package/dist/report-render/render.js +37 -6
- package/dist/report-render/types.d.ts +23 -2
- package/dist/report-render/value-bridge.d.ts +38 -3
- package/dist/report-render/value-bridge.js +137 -6
- package/dist/report-render/views/chart-view.d.ts +42 -6
- package/dist/report-render/views/chart-view.js +70 -9
- package/dist/report-render/views/page-view.js +9 -10
- package/dist/runtime/assemble.js +2 -2
- package/dist/sandbox/client.d.ts +23 -32
- package/dist/sandbox/client.js +3 -11
- package/dist/sandbox/create-sandbox.d.ts +9 -24
- package/dist/sandbox/create-sandbox.js +15 -30
- package/dist/sandbox/docker-client.d.ts +12 -22
- package/dist/sandbox/docker-client.js +29 -43
- package/dist/sandbox/exec.d.ts +58 -0
- package/dist/sandbox/exec.js +197 -0
- package/dist/sandbox/identity.d.ts +5 -5
- package/dist/sandbox/identity.js +7 -10
- package/dist/sandbox/k8s-client.d.ts +4 -10
- package/dist/sandbox/k8s-client.js +5 -12
- package/dist/sandbox/liveness.d.ts +5 -12
- package/dist/sandbox/liveness.js +5 -12
- package/dist/sandbox/notification-sweep.d.ts +4 -7
- package/dist/sandbox/notification-sweep.js +4 -7
- package/dist/sandbox/reaper.d.ts +3 -3
- package/dist/sandbox/reaper.js +3 -3
- package/dist/sandbox/sandbox-error.d.ts +10 -11
- package/dist/sandbox/sandbox-error.js +10 -11
- package/dist/sandbox/sandbox-step-translate.d.ts +1 -1
- package/dist/sandbox/sandbox-step-translate.js +1 -1
- package/dist/sandbox/types.d.ts +31 -99
- package/dist/sandbox/types.js +17 -84
- package/dist/state/active-sandboxes.d.ts +7 -32
- package/dist/state/active-sandboxes.js +11 -46
- package/dist/state/index.d.ts +1 -2
- package/dist/state/index.js +1 -1
- package/dist/state/migrations/20261005120000_drop_the_active_exec_tracking.d.ts +7 -0
- package/dist/state/migrations/20261005120000_drop_the_active_exec_tracking.js +12 -0
- package/dist/state/migrations/index.js +2 -0
- package/dist/state/schema.d.ts +1 -5
- package/dist/state/schema.js +1 -6
- package/dist/state/step-executions.js +1 -2
- package/dist/tasks/data-profile.js +7 -20
- package/dist/tasks/derive-table-exec.d.ts +4 -14
- package/dist/tasks/derive-table-exec.js +9 -24
- package/dist/tasks/extract-values.d.ts +2 -2
- package/dist/tasks/extract-values.js +4 -7
- package/dist/tools/define-tool.d.ts +56 -13
- package/dist/tools/define-tool.js +28 -1
- package/dist/tools/execute-analysis.js +3 -3
- package/dist/tools/report-session/derive-table.d.ts +8 -9
- package/dist/tools/report-session/derive-table.js +3 -4
- package/dist/tools/report-session/preview-report.js +4 -49
- package/dist/tools/sandbox/list-available-packages.d.ts +1 -1
- package/dist/tools/sandbox/list-available-packages.js +5 -4
- package/dist/tools/workspace/edit-file.d.ts +5 -4
- package/dist/tools/workspace/edit-file.js +10 -13
- package/dist/tools/workspace/execute-command.d.ts +27 -42
- package/dist/tools/workspace/execute-command.js +42 -38
- package/dist/tools/workspace/index.d.ts +5 -6
- package/dist/tools/workspace/index.js +5 -6
- package/dist/tools/workspace/mutator.d.ts +43 -8
- package/dist/tools/workspace/mutator.js +45 -34
- package/dist/tools/workspace/run-exec.d.ts +3 -12
- package/dist/tools/workspace/run-exec.js +7 -24
- package/dist/tools/workspace/scan-inputs.d.ts +0 -3
- package/dist/tools/workspace/scan-inputs.js +0 -1
- package/dist/tools/workspace/write-file.d.ts +12 -3
- package/dist/tools/workspace/write-file.js +17 -13
- package/dist/workflows/sandbox-step.d.ts +7 -11
- package/dist/workflows/sandbox-step.js +24 -27
- package/package.json +5 -3
- package/dist/sandbox/await-exec.d.ts +0 -143
- package/dist/sandbox/await-exec.js +0 -417
- package/dist/sandbox/deliver-exec-event.d.ts +0 -25
- package/dist/sandbox/deliver-exec-event.js +0 -29
- package/dist/sandbox/digest.d.ts +0 -14
- package/dist/sandbox/digest.js +0 -17
- package/dist/sandbox/exec-id.d.ts +0 -12
- package/dist/sandbox/exec-id.js +0 -20
- package/dist/sandbox/hmac.d.ts +0 -62
- package/dist/sandbox/hmac.js +0 -72
- package/dist/sandbox/submit-exec.d.ts +0 -38
- package/dist/sandbox/submit-exec.js +0 -80
- package/dist/sandbox/watchdog.d.ts +0 -71
- package/dist/sandbox/watchdog.js +0 -161
|
@@ -0,0 +1,1055 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The named renderers of a custom series, in two twins.
|
|
3
|
+
*
|
|
4
|
+
* The chart runtime draws an interval, a violin outline, an oncoprint cell, and a lollipop stem through a
|
|
5
|
+
* `custom` series. The option rides to the page as inline JSON, thus it holds no function. The derivation
|
|
6
|
+
* writes the name of a renderer as the `renderItem` string, and the chart runtime finds the function under
|
|
7
|
+
* that name in its own registry. Each consumer registers each renderer with `registerCustomSeries` before it
|
|
8
|
+
* draws: the page bootstrap through the source text below, and the server export through the TypeScript
|
|
9
|
+
* functions. A series that needs parameters beyond its items carries them as JSON in `itemPayload`, and the
|
|
10
|
+
* renderer reads them from its first argument.
|
|
11
|
+
*
|
|
12
|
+
* The two twins hold one rule each. A shared test vector runs both over one set of items with a stub of the
|
|
13
|
+
* runtime, and it compares the elements that they give. Thus the two cannot drift in silence.
|
|
14
|
+
*
|
|
15
|
+
* A category axis rounds a data coordinate to its category, thus a renderer never adds a fraction of a
|
|
16
|
+
* category to a coordinate. It takes the band of one category in pixels from the runtime, and it shifts the
|
|
17
|
+
* pixel position by a fraction of that band.
|
|
18
|
+
*
|
|
19
|
+
* The export option lives here too, in the same two twins: the server draws the SVG through it, and the page
|
|
20
|
+
* draws each PNG through it.
|
|
21
|
+
*/
|
|
22
|
+
import { CHART_INK, CHART_PAGE_TEXT_PX, COLOR_SCALE_BAND_PCT, GUIDE_LINE_COLOR } from "./design.js";
|
|
23
|
+
/** The renderer name of an interval: an error bar, a confidence interval, or the inner mark of a violin. */
|
|
24
|
+
export const INTERVAL_RENDERER = "interval";
|
|
25
|
+
/** The renderer name of a violin outline. */
|
|
26
|
+
export const OUTLINE_RENDERER = "outline";
|
|
27
|
+
/** The renderer name of one oncoprint cell: the gray ground and the glyph of its alteration class. */
|
|
28
|
+
export const CELL_GLYPH_RENDERER = "cell-glyph";
|
|
29
|
+
/** The renderer name of one lollipop mutation: a stem from zero and a head at its count. */
|
|
30
|
+
export const STEM_RENDERER = "stem";
|
|
31
|
+
/** The gap between two oncoprint cells, in pixels. */
|
|
32
|
+
export const CELL_GAP_PX = 1;
|
|
33
|
+
/**
|
|
34
|
+
* The largest share of one cell band that the gap takes. A cohort of some hundred samples at a column width
|
|
35
|
+
* gives a band near one pixel, and a full pixel of gap would erase each cell.
|
|
36
|
+
*/
|
|
37
|
+
const CELL_GAP_MAX_SHARE = 0.25;
|
|
38
|
+
/** The share of the ground height that the glyph of a class covers. The gray ground shows above and below it. */
|
|
39
|
+
export const CELL_GLYPH_SHARE = 0.6;
|
|
40
|
+
/** The radius of the head of a lollipop stem, in pixels. */
|
|
41
|
+
export const STEM_HEAD_RADIUS_PX = 3.5;
|
|
42
|
+
/** The count of grid points of one violin outline. Each item of an outline holds one pair for each point. */
|
|
43
|
+
export const VIOLIN_GRID_POINTS = 64;
|
|
44
|
+
/** The half length of the cap at each bound of an error bar, in pixels. It is the half width of an arrow too. */
|
|
45
|
+
export const INTERVAL_CAP_PX = 4;
|
|
46
|
+
/** The length of the arrow at a clipped end of an interval, in pixels. */
|
|
47
|
+
export const INTERVAL_ARROW_PX = 6;
|
|
48
|
+
/** The stroke width of an error bar and of its caps, in pixels. */
|
|
49
|
+
const INTERVAL_STROKE_PX = 1.5;
|
|
50
|
+
/** The stroke width of the quartile line of a violin, in pixels. It reads as a thin box inside the outline. */
|
|
51
|
+
const INNER_STROKE_PX = 3;
|
|
52
|
+
/** The radius of the median point of a violin, in pixels. */
|
|
53
|
+
const INNER_POINT_RADIUS_PX = 2.5;
|
|
54
|
+
/** The fill of the median point of a violin. A light point reads on the dark quartile line. */
|
|
55
|
+
const INNER_POINT_FILL = "#ffffff";
|
|
56
|
+
/** The opacity of a violin outline. The quartile line inside it stays readable. */
|
|
57
|
+
const OUTLINE_OPACITY = 0.75;
|
|
58
|
+
/** One straight segment in the ink of the chart. */
|
|
59
|
+
function segment(x1, y1, x2, y2, width) {
|
|
60
|
+
return { type: "line", shape: { x1, y1, x2, y2 }, style: { stroke: CHART_INK, lineWidth: width } };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Draw one interval.
|
|
64
|
+
*
|
|
65
|
+
* An item is `[x, y, low, high, axis, offset, mark, clipped]`. `axis` is `0` for an interval along x and `1`
|
|
66
|
+
* for one along y. `offset` is a fraction of one category band, and it moves the interval over its own bar in a
|
|
67
|
+
* grouped chart. `mark` is `0` for an error bar with two caps, and `1` for the inner mark of a violin: a
|
|
68
|
+
* thick quartile line and one point at the median, which is the anchor `y`. `clipped` is optional: its bit `1`
|
|
69
|
+
* marks a `low` that the axis clipped and its bit `2` a clipped `high`, and a clipped end draws an arrow in
|
|
70
|
+
* place of its cap, as `forestplot` draws it.
|
|
71
|
+
*/
|
|
72
|
+
export function intervalRenderer(_params, api) {
|
|
73
|
+
const x = api.value(0);
|
|
74
|
+
const y = api.value(1);
|
|
75
|
+
const low = api.value(2);
|
|
76
|
+
const high = api.value(3);
|
|
77
|
+
const alongX = api.value(4) === 0;
|
|
78
|
+
const offset = api.value(5);
|
|
79
|
+
const inner = api.value(6) === 1;
|
|
80
|
+
const clipped = Number(api.value(7)) || 0;
|
|
81
|
+
const band = alongX ? api.size([0, 1])[1] : api.size([1, 0])[0];
|
|
82
|
+
const shift = offset * band;
|
|
83
|
+
const from = api.coord(alongX ? [low, y] : [x, low]);
|
|
84
|
+
const to = api.coord(alongX ? [high, y] : [x, high]);
|
|
85
|
+
const anchor = api.coord([x, y]);
|
|
86
|
+
const fromX = alongX ? from[0] : from[0] + shift;
|
|
87
|
+
const fromY = alongX ? from[1] + shift : from[1];
|
|
88
|
+
const toX = alongX ? to[0] : to[0] + shift;
|
|
89
|
+
const toY = alongX ? to[1] + shift : to[1];
|
|
90
|
+
if (inner) {
|
|
91
|
+
return {
|
|
92
|
+
type: "group",
|
|
93
|
+
children: [
|
|
94
|
+
segment(fromX, fromY, toX, toY, INNER_STROKE_PX),
|
|
95
|
+
{
|
|
96
|
+
type: "circle",
|
|
97
|
+
shape: { cx: alongX ? anchor[0] : anchor[0] + shift, cy: alongX ? anchor[1] + shift : anchor[1], r: INNER_POINT_RADIUS_PX },
|
|
98
|
+
style: { fill: INNER_POINT_FILL, stroke: CHART_INK, lineWidth: 1 },
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const children = [segment(fromX, fromY, toX, toY, INTERVAL_STROKE_PX)];
|
|
104
|
+
for (const [bit, endX, endY, otherX, otherY] of [
|
|
105
|
+
[1, fromX, fromY, toX, toY],
|
|
106
|
+
[2, toX, toY, fromX, fromY],
|
|
107
|
+
]) {
|
|
108
|
+
if ((clipped & bit) !== 0) {
|
|
109
|
+
children.push(...arrowHead(endX, endY, otherX, otherY));
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
children.push(alongX
|
|
113
|
+
? segment(endX, endY - INTERVAL_CAP_PX, endX, endY + INTERVAL_CAP_PX, INTERVAL_STROKE_PX)
|
|
114
|
+
: segment(endX - INTERVAL_CAP_PX, endY, endX + INTERVAL_CAP_PX, endY, INTERVAL_STROKE_PX));
|
|
115
|
+
}
|
|
116
|
+
return { type: "group", children };
|
|
117
|
+
}
|
|
118
|
+
/** The two strokes of an arrow at one end of an interval, open toward its other end. */
|
|
119
|
+
function arrowHead(endX, endY, otherX, otherY) {
|
|
120
|
+
const length = Math.hypot(endX - otherX, endY - otherY) || 1;
|
|
121
|
+
const unitX = (endX - otherX) / length;
|
|
122
|
+
const unitY = (endY - otherY) / length;
|
|
123
|
+
const backX = endX - unitX * INTERVAL_ARROW_PX;
|
|
124
|
+
const backY = endY - unitY * INTERVAL_ARROW_PX;
|
|
125
|
+
return [
|
|
126
|
+
segment(backX - unitY * INTERVAL_CAP_PX, backY + unitX * INTERVAL_CAP_PX, endX, endY, INTERVAL_STROKE_PX),
|
|
127
|
+
segment(backX + unitY * INTERVAL_CAP_PX, backY - unitX * INTERVAL_CAP_PX, endX, endY, INTERVAL_STROKE_PX),
|
|
128
|
+
];
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Draw one violin outline.
|
|
132
|
+
*
|
|
133
|
+
* An item is `[c, yMin, yMax, offset, w1, y1, w2, y2, ...]` with one pair for each grid point. `c` is the
|
|
134
|
+
* category index, `yMin` and `yMax` are the extent of the grid, and `offset` is a fraction of one band that
|
|
135
|
+
* moves a grouped violin inside its band. Each pair is a half-width in band fractions and its grid value.
|
|
136
|
+
* The outline runs up the right side and down the left side, thus the polygon closes on itself.
|
|
137
|
+
*/
|
|
138
|
+
export function outlineRenderer(_params, api) {
|
|
139
|
+
const category = api.value(0);
|
|
140
|
+
const band = api.size([1, 0])[0];
|
|
141
|
+
const center = api.value(3) * band;
|
|
142
|
+
const right = [];
|
|
143
|
+
const left = [];
|
|
144
|
+
for (let point = 0; point < VIOLIN_GRID_POINTS; point += 1) {
|
|
145
|
+
const width = api.value(4 + 2 * point) * band;
|
|
146
|
+
const at = api.coord([category, api.value(5 + 2 * point)]);
|
|
147
|
+
right.push([at[0] + center + width, at[1]]);
|
|
148
|
+
left.push([at[0] + center - width, at[1]]);
|
|
149
|
+
}
|
|
150
|
+
const color = api.visual("color");
|
|
151
|
+
return {
|
|
152
|
+
type: "polygon",
|
|
153
|
+
shape: { points: right.concat(left.reverse()) },
|
|
154
|
+
style: { fill: color, stroke: color, lineWidth: 1, opacity: OUTLINE_OPACITY },
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Draw one oncoprint cell.
|
|
159
|
+
*
|
|
160
|
+
* An item is `[x, y, class]`. `x` and `y` are the places of the sample and the gene on the two category axes,
|
|
161
|
+
* and `class` is the place of the alteration class in the `colors` list of the payload, or `-1` for a cell
|
|
162
|
+
* with no alteration. Each cell draws the `ground` color of the payload over its band less one gap. An altered
|
|
163
|
+
* cell adds one glyph in the color of its class: the width of the ground and a share of its height, centered.
|
|
164
|
+
*/
|
|
165
|
+
export function cellGlyphRenderer(params, api) {
|
|
166
|
+
const payload = params.itemPayload ?? {};
|
|
167
|
+
const colors = Array.isArray(payload.colors) ? payload.colors : [];
|
|
168
|
+
const ground = typeof payload.ground === "string" ? payload.ground : CHART_INK;
|
|
169
|
+
const center = api.coord([api.value(0), api.value(1)]);
|
|
170
|
+
const klass = api.value(2);
|
|
171
|
+
const band = api.size([1, 1]);
|
|
172
|
+
const width = band[0] - Math.min(CELL_GAP_PX, band[0] * CELL_GAP_MAX_SHARE);
|
|
173
|
+
const height = band[1] - Math.min(CELL_GAP_PX, band[1] * CELL_GAP_MAX_SHARE);
|
|
174
|
+
const children = [
|
|
175
|
+
{ type: "rect", shape: { x: center[0] - width / 2, y: center[1] - height / 2, width, height }, style: { fill: ground } },
|
|
176
|
+
];
|
|
177
|
+
if (klass >= 0 && klass < colors.length) {
|
|
178
|
+
const glyph = height * CELL_GLYPH_SHARE;
|
|
179
|
+
children.push({ type: "rect", shape: { x: center[0] - width / 2, y: center[1] - glyph / 2, width, height: glyph }, style: { fill: colors[klass] } });
|
|
180
|
+
}
|
|
181
|
+
return { type: "group", children };
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Draw one lollipop mutation.
|
|
185
|
+
*
|
|
186
|
+
* An item is `[x, y]`: the amino-acid position and the count. The stem is a thin gray line from the value `0`
|
|
187
|
+
* to the count, and the head is one circle at the count in the color of the series.
|
|
188
|
+
*/
|
|
189
|
+
export function stemRenderer(_params, api) {
|
|
190
|
+
const x = api.value(0);
|
|
191
|
+
const floor = api.coord([x, 0]);
|
|
192
|
+
const head = api.coord([x, api.value(1)]);
|
|
193
|
+
return {
|
|
194
|
+
type: "group",
|
|
195
|
+
children: [
|
|
196
|
+
{ type: "line", shape: { x1: floor[0], y1: floor[1], x2: head[0], y2: head[1] }, style: { stroke: GUIDE_LINE_COLOR, lineWidth: 1 } },
|
|
197
|
+
{
|
|
198
|
+
type: "circle",
|
|
199
|
+
shape: { cx: head[0], cy: head[1], r: STEM_HEAD_RADIUS_PX },
|
|
200
|
+
style: { fill: api.visual("color"), stroke: CHART_INK, lineWidth: 0.5 },
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* The registered renderers. A renderer joins by one entry here, beside its TypeScript function and its page
|
|
207
|
+
* twin, and each consumer registers it under the same name.
|
|
208
|
+
*/
|
|
209
|
+
export const CHART_RENDERERS = [
|
|
210
|
+
{ name: INTERVAL_RENDERER, render: intervalRenderer, pageFunction: "reportIntervalRenderer" },
|
|
211
|
+
{ name: OUTLINE_RENDERER, render: outlineRenderer, pageFunction: "reportOutlineRenderer" },
|
|
212
|
+
{ name: CELL_GLYPH_RENDERER, render: cellGlyphRenderer, pageFunction: "reportCellGlyphRenderer" },
|
|
213
|
+
{ name: STEM_RENDERER, render: stemRenderer, pageFunction: "reportStemRenderer" },
|
|
214
|
+
];
|
|
215
|
+
/**
|
|
216
|
+
* Register each renderer with the chart runtime. A second registration of one name replaces the first with
|
|
217
|
+
* the same function, thus a caller registers before each draw and the runtime holds one function per name.
|
|
218
|
+
*/
|
|
219
|
+
export function registerChartRenderers(runtime) {
|
|
220
|
+
for (const entry of CHART_RENDERERS) {
|
|
221
|
+
runtime.registerCustomSeries(entry.name, entry.render);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* The member of an option that holds the series of each export width, for a figure whose text the derivation
|
|
226
|
+
* places itself.
|
|
227
|
+
*
|
|
228
|
+
* A name that the derivation places clears its neighbors at one plot size and one text size alone. Thus the
|
|
229
|
+
* figure places its names again for the plot of each export, and the export takes the series of its width in
|
|
230
|
+
* place of the page series of the same names. The member is no field of the chart runtime, and the runtime
|
|
231
|
+
* keeps an unknown member without a draw.
|
|
232
|
+
*/
|
|
233
|
+
export const SIZED_SERIES_MEMBER = "sizedSeries";
|
|
234
|
+
/**
|
|
235
|
+
* The series of an option with the page series of the sized names replaced by the series of one export width.
|
|
236
|
+
* A width that the member does not hold keeps the page series.
|
|
237
|
+
*/
|
|
238
|
+
function sizedSeries(series, sized, widthPx) {
|
|
239
|
+
if (!Array.isArray(series) || typeof sized !== "object" || sized === null)
|
|
240
|
+
return series;
|
|
241
|
+
const fields = sized;
|
|
242
|
+
const widths = fields.widths;
|
|
243
|
+
const chosen = typeof widths === "object" && widths !== null ? widths[String(widthPx)] : undefined;
|
|
244
|
+
if (!Array.isArray(fields.names) || !Array.isArray(chosen))
|
|
245
|
+
return series;
|
|
246
|
+
const names = fields.names;
|
|
247
|
+
const kept = series.filter((entry) => typeof entry !== "object" || entry === null || names.indexOf(entry.name) < 0);
|
|
248
|
+
return kept.concat(chosen);
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* One axis, or one list of axes, with each gap that clears its text scaled to the text size of an export: the
|
|
252
|
+
* name gap, and the offset of an axis past a column of text.
|
|
253
|
+
*/
|
|
254
|
+
function scaledAxes(axes, scale) {
|
|
255
|
+
if (Array.isArray(axes))
|
|
256
|
+
return axes.map((axis) => scaledAxes(axis, scale));
|
|
257
|
+
if (typeof axes !== "object" || axes === null)
|
|
258
|
+
return axes;
|
|
259
|
+
const axis = { ...axes };
|
|
260
|
+
if (typeof axis.nameGap === "number")
|
|
261
|
+
axis.nameGap = axis.nameGap * scale;
|
|
262
|
+
if (typeof axis.offset === "number")
|
|
263
|
+
axis.offset = axis.offset * scale;
|
|
264
|
+
return axis;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* One graphic element with its text scaled to the text size of an export, and the text of each child of a
|
|
268
|
+
* group. A facet panel label is such an element, and the size legend is such a group.
|
|
269
|
+
*/
|
|
270
|
+
function scaledGraphic(element, scale) {
|
|
271
|
+
if (typeof element !== "object" || element === null)
|
|
272
|
+
return element;
|
|
273
|
+
const out = { ...element };
|
|
274
|
+
const style = out.style;
|
|
275
|
+
if (style !== undefined && typeof style.fontSize === "number")
|
|
276
|
+
out.style = { ...style, fontSize: style.fontSize * scale };
|
|
277
|
+
if (Array.isArray(out.children))
|
|
278
|
+
out.children = out.children.map((child) => scaledGraphic(child, scale));
|
|
279
|
+
return out;
|
|
280
|
+
}
|
|
281
|
+
/** The width of one character of the chart text, as a share of the text size. The export guesses a label width with it. */
|
|
282
|
+
const CHARACTER_SHARE = 0.6;
|
|
283
|
+
/** The height of one line of the chart text, as a share of the text size. An upright category label takes one line along its axis. */
|
|
284
|
+
export const LABEL_LINE_SHARE = 1.3;
|
|
285
|
+
/** The share of the export width that the category labels of one x axis can fill. */
|
|
286
|
+
const LABEL_ROOM = 0.8;
|
|
287
|
+
/**
|
|
288
|
+
* The label interval of an axis of upright category labels at one width and one text size: the count of labels that
|
|
289
|
+
* the chart runtime skips after each label that it prints. The width holds one upright label for each line of text.
|
|
290
|
+
* An axis of more categories prints the first label of each run that the width gives, and an axis that fits skips none.
|
|
291
|
+
*/
|
|
292
|
+
export function categoryInterval(count, textPx, widthPx) {
|
|
293
|
+
const fit = Math.max(1, Math.floor(widthPx / (textPx * LABEL_LINE_SHARE)));
|
|
294
|
+
return count > fit ? Math.ceil(count / fit) - 1 : 0;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* One x axis of categories with its labels turned where they do not fit the width of the export, and its upright
|
|
298
|
+
* labels thinned to the count that the width holds.
|
|
299
|
+
*
|
|
300
|
+
* The page turns the labels by their count. A column export is narrower, thus it turns the labels whose
|
|
301
|
+
* longest name, at the export text size, passes the width of one category. A label that needs more than two
|
|
302
|
+
* widths turns upright. An authored turn stays. The page thins its upright labels at the page width, thus the
|
|
303
|
+
* export states the interval again at its own width and text size.
|
|
304
|
+
*/
|
|
305
|
+
function fittedCategories(axis, textPx, widthPx) {
|
|
306
|
+
if (typeof axis !== "object" || axis === null || Array.isArray(axis))
|
|
307
|
+
return axis;
|
|
308
|
+
const fields = axis;
|
|
309
|
+
const data = fields.data;
|
|
310
|
+
if (fields.type !== "category" || !Array.isArray(data) || data.length === 0)
|
|
311
|
+
return axis;
|
|
312
|
+
const label = typeof fields.axisLabel === "object" && fields.axisLabel !== null ? fields.axisLabel : {};
|
|
313
|
+
let rotate = label.rotate;
|
|
314
|
+
if (typeof rotate !== "number" || rotate === 0) {
|
|
315
|
+
let longest = 0;
|
|
316
|
+
for (const name of data)
|
|
317
|
+
longest = Math.max(longest, String(name).length);
|
|
318
|
+
const need = longest * textPx * CHARACTER_SHARE;
|
|
319
|
+
const room = (widthPx * LABEL_ROOM) / data.length;
|
|
320
|
+
if (need <= room)
|
|
321
|
+
return axis;
|
|
322
|
+
rotate = need > 2 * room ? 90 : 45;
|
|
323
|
+
}
|
|
324
|
+
const interval = rotate === 90 ? { interval: categoryInterval(data.length, textPx, widthPx * LABEL_ROOM) } : {};
|
|
325
|
+
return { ...fields, axisLabel: { ...label, rotate, ...interval } };
|
|
326
|
+
}
|
|
327
|
+
/** The share of the scale band that one line of a scale title can fill. The rest keeps a gap to the plot. */
|
|
328
|
+
const SCALE_TITLE_ROOM = 0.8;
|
|
329
|
+
/**
|
|
330
|
+
* The title of a still color scale, wrapped at its spaces into lines that fit the scale band.
|
|
331
|
+
*
|
|
332
|
+
* The chart runtime centers the title over the bar and lays the scale out at the right edge. A title wider
|
|
333
|
+
* than the band pushes the scale box into the plot. The width of a line is the estimate of the export: each
|
|
334
|
+
* character takes a fixed share of the text size. One word longer than a line stays whole.
|
|
335
|
+
*/
|
|
336
|
+
function wrappedTitle(title, textPx, widthPx) {
|
|
337
|
+
const perLine = Math.max(1, Math.floor((widthPx * COLOR_SCALE_BAND_PCT * SCALE_TITLE_ROOM) / 100 / (textPx * CHARACTER_SHARE)));
|
|
338
|
+
const lines = [];
|
|
339
|
+
let line = "";
|
|
340
|
+
for (const word of title.split(" ")) {
|
|
341
|
+
if (line === "") {
|
|
342
|
+
line = word;
|
|
343
|
+
}
|
|
344
|
+
else if (line.length + 1 + word.length <= perLine) {
|
|
345
|
+
line = `${line} ${word}`;
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
lines.push(line);
|
|
349
|
+
line = word;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (line !== "")
|
|
353
|
+
lines.push(line);
|
|
354
|
+
return lines.join("\n");
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* The width in pixels of the grid that one axis draws in. A facet states the width of each grid in percent of
|
|
358
|
+
* the chart, and a chart of one grid spans the export width.
|
|
359
|
+
*/
|
|
360
|
+
function gridWidth(grid, axis, widthPx) {
|
|
361
|
+
const place = typeof axis === "object" && axis !== null && typeof axis.gridIndex === "number"
|
|
362
|
+
? axis.gridIndex
|
|
363
|
+
: 0;
|
|
364
|
+
const entry = Array.isArray(grid) ? grid[place] : grid;
|
|
365
|
+
const width = typeof entry === "object" && entry !== null ? entry.width : undefined;
|
|
366
|
+
if (typeof width === "string" && width.endsWith("%"))
|
|
367
|
+
return (widthPx * Number.parseFloat(width)) / 100;
|
|
368
|
+
if (typeof width === "number")
|
|
369
|
+
return width;
|
|
370
|
+
return widthPx;
|
|
371
|
+
}
|
|
372
|
+
/** One x axis, or each x axis of a facet, with its category labels fitted to the width of its own grid. */
|
|
373
|
+
function fittedAxes(axes, grid, textPx, widthPx) {
|
|
374
|
+
if (!Array.isArray(axes))
|
|
375
|
+
return fittedCategories(axes, textPx, gridWidth(grid, axes, widthPx));
|
|
376
|
+
return axes.map((axis) => fittedCategories(axis, textPx, gridWidth(grid, axis, widthPx)));
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* One continuous color map of an export, with its title wrapped into the band of the scale.
|
|
380
|
+
*
|
|
381
|
+
* The page prints the title and the upper end over the scale, one line each, and the lower end under it. The
|
|
382
|
+
* export keeps the two ends and wraps the title into the band of the scale. The scale starts at the top of the
|
|
383
|
+
* plot and stays short, thus its lower end never meets the labels of the x axis. A map that shows nothing, that
|
|
384
|
+
* prints no end, or that draws pieces and no scale passes through.
|
|
385
|
+
*/
|
|
386
|
+
function stillScale(map, textPx, widthPx) {
|
|
387
|
+
if (typeof map !== "object" || map === null)
|
|
388
|
+
return map;
|
|
389
|
+
const fields = map;
|
|
390
|
+
const text = fields.text;
|
|
391
|
+
if (fields.show === false || fields.type === "piecewise" || !Array.isArray(text) || typeof text[0] !== "string" || typeof text[1] !== "string")
|
|
392
|
+
return map;
|
|
393
|
+
const lines = text[0].split("\n");
|
|
394
|
+
const upper = lines[lines.length - 1];
|
|
395
|
+
const title = lines.slice(0, -1).join(" ");
|
|
396
|
+
return {
|
|
397
|
+
...fields,
|
|
398
|
+
top: STILL_SCALE_TOP,
|
|
399
|
+
itemHeight: STILL_SCALE_LINES * textPx,
|
|
400
|
+
text: [title === "" ? upper : `${wrappedTitle(title, textPx, widthPx)}\n${upper}`, text[1]],
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
/** The top of a still color scale, which is the top margin of the grid, and its length in lines of the text. */
|
|
404
|
+
const STILL_SCALE_TOP = "8%";
|
|
405
|
+
const STILL_SCALE_LINES = 8;
|
|
406
|
+
/** The height of the legend band of an export, in lines of its text. */
|
|
407
|
+
const LEGEND_BAND_LINES = 2.5;
|
|
408
|
+
/** The outer bounds mode of a facet panel: the labels of the panel stay inside the box of its grid. */
|
|
409
|
+
const PANEL_BOUNDS_MODE = "same";
|
|
410
|
+
/** The item gap of the legend in the theme, in pixels. Two lines of a legend that wraps stand one gap apart. */
|
|
411
|
+
const LEGEND_ITEM_GAP_PX = 16;
|
|
412
|
+
/** The line pitch of the legend text, and the height of the x title of a facet, as a share of the text size. */
|
|
413
|
+
const LEGEND_LINE_SHARE = 1.25;
|
|
414
|
+
const FACET_TITLE_SHARE = 1.6;
|
|
415
|
+
/** The padding of the legend box at each side, in pixels: the default of the chart runtime. */
|
|
416
|
+
const LEGEND_PADDING_PX = 5;
|
|
417
|
+
/**
|
|
418
|
+
* The smallest share of its page height that the lowest row of facet panels keeps in an export. The single column
|
|
419
|
+
* wraps a legend of many names into more lines than its height holds, thus such a legend draws in one line that
|
|
420
|
+
* scrolls, and the panels keep their room.
|
|
421
|
+
*/
|
|
422
|
+
const PANEL_MIN_SHARE = 0.5;
|
|
423
|
+
/**
|
|
424
|
+
* The height of the band of a bottom legend of some lines, in pixels: one line pitch and one item gap for each
|
|
425
|
+
* line, and one text size under the lowest line.
|
|
426
|
+
*/
|
|
427
|
+
function legendBand(lines, textPx) {
|
|
428
|
+
return Math.round(lines * (textPx * LEGEND_LINE_SHARE + LEGEND_ITEM_GAP_PX) + textPx);
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* The count of lines of a horizontal legend at one width. An entry is the icon of the text size, the gap of
|
|
432
|
+
* five pixels to its text, and the text. The entries fill each line in order, one item gap apart. The names
|
|
433
|
+
* are the data of the legend, or else the name of each series in order of first appearance.
|
|
434
|
+
*/
|
|
435
|
+
function legendLines(option, textPx, widthPx) {
|
|
436
|
+
const legend = option.legend;
|
|
437
|
+
const names = [];
|
|
438
|
+
if (Array.isArray(legend.data)) {
|
|
439
|
+
for (const entry of legend.data) {
|
|
440
|
+
const name = typeof entry === "object" && entry !== null ? entry.name : entry;
|
|
441
|
+
if (typeof name === "string")
|
|
442
|
+
names.push(name);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
else if (Array.isArray(option.series)) {
|
|
446
|
+
for (const series of option.series) {
|
|
447
|
+
const name = typeof series === "object" && series !== null ? series.name : undefined;
|
|
448
|
+
if (typeof name === "string" && name !== "" && names.indexOf(name) < 0)
|
|
449
|
+
names.push(name);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
const room = widthPx - 2 * LEGEND_PADDING_PX;
|
|
453
|
+
let lines = names.length === 0 ? 0 : 1;
|
|
454
|
+
let used = 0;
|
|
455
|
+
for (const name of names) {
|
|
456
|
+
const entry = textPx + 5 + name.length * textPx * CHARACTER_SHARE;
|
|
457
|
+
if (used > 0 && used + LEGEND_ITEM_GAP_PX + entry > room) {
|
|
458
|
+
lines += 1;
|
|
459
|
+
used = entry;
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
used = used > 0 ? used + LEGEND_ITEM_GAP_PX + entry : entry;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return lines;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* A facet with its lowest row of panels and its x title held above the band of a bottom legend.
|
|
469
|
+
*
|
|
470
|
+
* The page reserves the legend band in percent of its body, and a narrow export wraps the legend into more
|
|
471
|
+
* lines than that band holds. The lowest row of panels then ends its boxes over the x title, and the x title
|
|
472
|
+
* sits on the band. Each row of panels gives an equal part of the lost height, thus each panel keeps one
|
|
473
|
+
* height. A row moves up by the parts of the rows over it, and the label of each panel moves with its row, thus
|
|
474
|
+
* the label keeps its gap over the panel. A legend whose lines leave each panel under `PANEL_MIN_SHARE` of its
|
|
475
|
+
* height draws in one line that scrolls.
|
|
476
|
+
*/
|
|
477
|
+
function facetOverLegend(option, textPx, widthPx, heightPx) {
|
|
478
|
+
const grids = option.grid;
|
|
479
|
+
const title = Math.round(textPx * FACET_TITLE_SHARE);
|
|
480
|
+
const tops = [];
|
|
481
|
+
let lowest = 0;
|
|
482
|
+
let height = 0;
|
|
483
|
+
for (const grid of grids) {
|
|
484
|
+
const bottom = panelBottom(grid);
|
|
485
|
+
if (bottom <= 0)
|
|
486
|
+
continue;
|
|
487
|
+
const fields = grid;
|
|
488
|
+
const top = Number.parseFloat(String(fields.top));
|
|
489
|
+
if (tops.indexOf(top) < 0)
|
|
490
|
+
tops.push(top);
|
|
491
|
+
if (bottom <= lowest)
|
|
492
|
+
continue;
|
|
493
|
+
lowest = bottom;
|
|
494
|
+
height = Number.parseFloat(String(fields.height));
|
|
495
|
+
}
|
|
496
|
+
tops.sort((a, b) => a - b);
|
|
497
|
+
const partOf = (legendPx) => Math.max(0, lowest - ((heightPx - legendPx - title) / heightPx) * 100) / tops.length;
|
|
498
|
+
const lines = legendLines(option, textPx, widthPx);
|
|
499
|
+
let band = legendBand(lines, textPx);
|
|
500
|
+
if (lines > 1 && partOf(band) > height * (1 - PANEL_MIN_SHARE)) {
|
|
501
|
+
option.legend = { ...option.legend, type: "scroll" };
|
|
502
|
+
band = legendBand(1, textPx);
|
|
503
|
+
}
|
|
504
|
+
const part = partOf(band);
|
|
505
|
+
// The row of a panel label is the first row whose panels start at or under the label.
|
|
506
|
+
const rowOf = (top) => tops.findIndex((rowTop) => rowTop >= top);
|
|
507
|
+
const moved = (value, row) => `${Math.round((value - row * part) * 1e4) / 1e4}%`;
|
|
508
|
+
if (part > 0) {
|
|
509
|
+
option.grid = grids.map((grid) => {
|
|
510
|
+
if (panelBottom(grid) <= 0)
|
|
511
|
+
return grid;
|
|
512
|
+
const fields = grid;
|
|
513
|
+
const top = Number.parseFloat(String(fields.top));
|
|
514
|
+
return { ...fields, top: moved(top, tops.indexOf(top)), height: `${Math.round((Number.parseFloat(String(fields.height)) - part) * 1e4) / 1e4}%` };
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
if (Array.isArray(option.graphic)) {
|
|
518
|
+
option.graphic = option.graphic.map((element) => {
|
|
519
|
+
if (typeof element !== "object" || element === null)
|
|
520
|
+
return element;
|
|
521
|
+
const fields = element;
|
|
522
|
+
if (fields.bottom !== undefined)
|
|
523
|
+
return { ...fields, bottom: band };
|
|
524
|
+
const top = typeof fields.top === "string" && fields.top.endsWith("%") ? Number.parseFloat(fields.top) : Number.NaN;
|
|
525
|
+
const row = Number.isFinite(top) && part > 0 ? rowOf(top) : -1;
|
|
526
|
+
return row > 0 ? { ...fields, top: moved(top, row) } : element;
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
/** The bottom edge of one facet panel, in percent of the chart, or 0 for a grid that is no facet panel. */
|
|
531
|
+
function panelBottom(grid) {
|
|
532
|
+
if (typeof grid !== "object" || grid === null)
|
|
533
|
+
return 0;
|
|
534
|
+
const fields = grid;
|
|
535
|
+
if (fields.outerBoundsMode !== PANEL_BOUNDS_MODE || typeof fields.top !== "string" || typeof fields.height !== "string")
|
|
536
|
+
return 0;
|
|
537
|
+
return Number.parseFloat(fields.top) + Number.parseFloat(fields.height);
|
|
538
|
+
}
|
|
539
|
+
/** True when an option lays out facet panels over a bottom legend. */
|
|
540
|
+
function holdsFacetLegend(option) {
|
|
541
|
+
const legend = option.legend;
|
|
542
|
+
const bottom = typeof legend === "object" && legend !== null && legend.show !== false && legend.bottom !== undefined;
|
|
543
|
+
return bottom && Array.isArray(option.grid) && option.grid.some((grid) => panelBottom(grid) > 0);
|
|
544
|
+
}
|
|
545
|
+
/** True when an option draws a legend along the bottom edge of a grid of one panel. */
|
|
546
|
+
function holdsBottomLegend(option) {
|
|
547
|
+
const legend = option.legend;
|
|
548
|
+
const bottom = typeof legend === "object" && legend !== null && legend.show !== false && legend.bottom !== undefined;
|
|
549
|
+
return bottom && option.xAxis !== undefined && !Array.isArray(option.grid);
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* The option of one export at one text size.
|
|
553
|
+
*
|
|
554
|
+
* An export is a still figure. Thus it carries no tooltip, no toolbox, and no animation. The theme of the
|
|
555
|
+
* export sets the text size, and the gap between an axis and its name, and the label of a facet panel, scale
|
|
556
|
+
* with the text. The copy leaves the input as it is, thus the page keeps its own option.
|
|
557
|
+
*
|
|
558
|
+
* A column export is short and narrow. Thus the grid of a chart with a bottom legend holds its labels and its
|
|
559
|
+
* names above a band of the legend height, and the panels of a facet end over the band of the legend lines
|
|
560
|
+
* that the export width gives. The category labels turn where they do not fit the width, and a color scale
|
|
561
|
+
* prints its ends as static text. A grid that contains its labels contains its names too.
|
|
562
|
+
*/
|
|
563
|
+
export function exportOption(option, textPx, widthPx, heightPx) {
|
|
564
|
+
const scale = textPx / CHART_PAGE_TEXT_PX;
|
|
565
|
+
const out = {};
|
|
566
|
+
for (const key of Object.keys(option)) {
|
|
567
|
+
if (key === "tooltip" || key === "toolbox" || key === SIZED_SERIES_MEMBER)
|
|
568
|
+
continue;
|
|
569
|
+
out[key] = option[key];
|
|
570
|
+
}
|
|
571
|
+
if (out.series !== undefined)
|
|
572
|
+
out.series = sizedSeries(out.series, option[SIZED_SERIES_MEMBER], widthPx);
|
|
573
|
+
out.animation = false;
|
|
574
|
+
if (out.xAxis !== undefined)
|
|
575
|
+
out.xAxis = scaledAxes(out.xAxis, scale);
|
|
576
|
+
if (out.yAxis !== undefined)
|
|
577
|
+
out.yAxis = scaledAxes(out.yAxis, scale);
|
|
578
|
+
if (Array.isArray(out.graphic))
|
|
579
|
+
out.graphic = out.graphic.map((element) => scaledGraphic(element, scale));
|
|
580
|
+
if (out.xAxis !== undefined)
|
|
581
|
+
out.xAxis = fittedAxes(out.xAxis, out.grid, textPx, widthPx);
|
|
582
|
+
if (Array.isArray(out.visualMap))
|
|
583
|
+
out.visualMap = out.visualMap.map((map) => stillScale(map, textPx, widthPx));
|
|
584
|
+
else if (out.visualMap !== undefined)
|
|
585
|
+
out.visualMap = stillScale(out.visualMap, textPx, widthPx);
|
|
586
|
+
if (typeof out.grid === "object" && out.grid !== null && !Array.isArray(out.grid) && out.grid.containLabel === true) {
|
|
587
|
+
// The legacy containment holds the labels alone, and a name at the edge of a short export is cut.
|
|
588
|
+
out.grid = { ...out.grid, containLabel: false, outerBoundsMode: "auto", outerBoundsContain: "all" };
|
|
589
|
+
}
|
|
590
|
+
if (holdsBottomLegend(out)) {
|
|
591
|
+
const grid = typeof out.grid === "object" && out.grid !== null ? out.grid : {};
|
|
592
|
+
out.grid = { ...grid, outerBoundsMode: "auto", outerBounds: { left: 0, right: 0, top: 0, bottom: Math.round(textPx * LEGEND_BAND_LINES) } };
|
|
593
|
+
}
|
|
594
|
+
if (holdsFacetLegend(out))
|
|
595
|
+
facetOverLegend(out, textPx, widthPx, heightPx);
|
|
596
|
+
return out;
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* The page twin of each renderer, of the bind step, and of the export option, as browser source text.
|
|
600
|
+
*
|
|
601
|
+
* Each function holds the rule of its TypeScript twin in the same order of operations. Thus the two give
|
|
602
|
+
* the same numbers for one item, and the shared test vector compares them exactly.
|
|
603
|
+
*/
|
|
604
|
+
export const CHART_RENDERERS_SOURCE = `function reportSegment(x1, y1, x2, y2, width) {
|
|
605
|
+
return { type: "line", shape: { x1: x1, y1: y1, x2: x2, y2: y2 }, style: { stroke: ${JSON.stringify(CHART_INK)}, lineWidth: width } };
|
|
606
|
+
}
|
|
607
|
+
function reportIntervalRenderer(params, api) {
|
|
608
|
+
var x = api.value(0);
|
|
609
|
+
var y = api.value(1);
|
|
610
|
+
var low = api.value(2);
|
|
611
|
+
var high = api.value(3);
|
|
612
|
+
var alongX = api.value(4) === 0;
|
|
613
|
+
var offset = api.value(5);
|
|
614
|
+
var inner = api.value(6) === 1;
|
|
615
|
+
var clipped = Number(api.value(7)) || 0;
|
|
616
|
+
var band = alongX ? api.size([0, 1])[1] : api.size([1, 0])[0];
|
|
617
|
+
var shift = offset * band;
|
|
618
|
+
var from = api.coord(alongX ? [low, y] : [x, low]);
|
|
619
|
+
var to = api.coord(alongX ? [high, y] : [x, high]);
|
|
620
|
+
var anchor = api.coord([x, y]);
|
|
621
|
+
var fromX = alongX ? from[0] : from[0] + shift;
|
|
622
|
+
var fromY = alongX ? from[1] + shift : from[1];
|
|
623
|
+
var toX = alongX ? to[0] : to[0] + shift;
|
|
624
|
+
var toY = alongX ? to[1] + shift : to[1];
|
|
625
|
+
if (inner) {
|
|
626
|
+
return {
|
|
627
|
+
type: "group",
|
|
628
|
+
children: [
|
|
629
|
+
reportSegment(fromX, fromY, toX, toY, ${INNER_STROKE_PX}),
|
|
630
|
+
{
|
|
631
|
+
type: "circle",
|
|
632
|
+
shape: { cx: alongX ? anchor[0] : anchor[0] + shift, cy: alongX ? anchor[1] + shift : anchor[1], r: ${INNER_POINT_RADIUS_PX} },
|
|
633
|
+
style: { fill: ${JSON.stringify(INNER_POINT_FILL)}, stroke: ${JSON.stringify(CHART_INK)}, lineWidth: 1 }
|
|
634
|
+
}
|
|
635
|
+
]
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
var children = [reportSegment(fromX, fromY, toX, toY, ${INTERVAL_STROKE_PX})];
|
|
639
|
+
var ends = [[1, fromX, fromY, toX, toY], [2, toX, toY, fromX, fromY]];
|
|
640
|
+
for (var e = 0; e < ends.length; e++) {
|
|
641
|
+
var endX = ends[e][1];
|
|
642
|
+
var endY = ends[e][2];
|
|
643
|
+
if ((clipped & ends[e][0]) !== 0) {
|
|
644
|
+
var arrow = reportArrowHead(endX, endY, ends[e][3], ends[e][4]);
|
|
645
|
+
children.push(arrow[0], arrow[1]);
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
children.push(
|
|
649
|
+
alongX
|
|
650
|
+
? reportSegment(endX, endY - ${INTERVAL_CAP_PX}, endX, endY + ${INTERVAL_CAP_PX}, ${INTERVAL_STROKE_PX})
|
|
651
|
+
: reportSegment(endX - ${INTERVAL_CAP_PX}, endY, endX + ${INTERVAL_CAP_PX}, endY, ${INTERVAL_STROKE_PX})
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
return { type: "group", children: children };
|
|
655
|
+
}
|
|
656
|
+
function reportArrowHead(endX, endY, otherX, otherY) {
|
|
657
|
+
var length = Math.hypot(endX - otherX, endY - otherY) || 1;
|
|
658
|
+
var unitX = (endX - otherX) / length;
|
|
659
|
+
var unitY = (endY - otherY) / length;
|
|
660
|
+
var backX = endX - unitX * ${INTERVAL_ARROW_PX};
|
|
661
|
+
var backY = endY - unitY * ${INTERVAL_ARROW_PX};
|
|
662
|
+
return [
|
|
663
|
+
reportSegment(backX - unitY * ${INTERVAL_CAP_PX}, backY + unitX * ${INTERVAL_CAP_PX}, endX, endY, ${INTERVAL_STROKE_PX}),
|
|
664
|
+
reportSegment(backX + unitY * ${INTERVAL_CAP_PX}, backY - unitX * ${INTERVAL_CAP_PX}, endX, endY, ${INTERVAL_STROKE_PX})
|
|
665
|
+
];
|
|
666
|
+
}
|
|
667
|
+
function reportOutlineRenderer(params, api) {
|
|
668
|
+
var category = api.value(0);
|
|
669
|
+
var band = api.size([1, 0])[0];
|
|
670
|
+
var center = api.value(3) * band;
|
|
671
|
+
var right = [];
|
|
672
|
+
var left = [];
|
|
673
|
+
for (var point = 0; point < ${VIOLIN_GRID_POINTS}; point++) {
|
|
674
|
+
var width = api.value(4 + 2 * point) * band;
|
|
675
|
+
var at = api.coord([category, api.value(5 + 2 * point)]);
|
|
676
|
+
right.push([at[0] + center + width, at[1]]);
|
|
677
|
+
left.push([at[0] + center - width, at[1]]);
|
|
678
|
+
}
|
|
679
|
+
var color = api.visual("color");
|
|
680
|
+
return {
|
|
681
|
+
type: "polygon",
|
|
682
|
+
shape: { points: right.concat(left.reverse()) },
|
|
683
|
+
style: { fill: color, stroke: color, lineWidth: 1, opacity: ${OUTLINE_OPACITY} }
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
function reportCellGlyphRenderer(params, api) {
|
|
687
|
+
var payload = params.itemPayload || {};
|
|
688
|
+
var colors = Array.isArray(payload.colors) ? payload.colors : [];
|
|
689
|
+
var ground = typeof payload.ground === "string" ? payload.ground : ${JSON.stringify(CHART_INK)};
|
|
690
|
+
var center = api.coord([api.value(0), api.value(1)]);
|
|
691
|
+
var klass = api.value(2);
|
|
692
|
+
var band = api.size([1, 1]);
|
|
693
|
+
var width = band[0] - Math.min(${CELL_GAP_PX}, band[0] * ${CELL_GAP_MAX_SHARE});
|
|
694
|
+
var height = band[1] - Math.min(${CELL_GAP_PX}, band[1] * ${CELL_GAP_MAX_SHARE});
|
|
695
|
+
var children = [{ type: "rect", shape: { x: center[0] - width / 2, y: center[1] - height / 2, width: width, height: height }, style: { fill: ground } }];
|
|
696
|
+
if (klass >= 0 && klass < colors.length) {
|
|
697
|
+
var glyph = height * ${CELL_GLYPH_SHARE};
|
|
698
|
+
children.push({ type: "rect", shape: { x: center[0] - width / 2, y: center[1] - glyph / 2, width: width, height: glyph }, style: { fill: colors[klass] } });
|
|
699
|
+
}
|
|
700
|
+
return { type: "group", children: children };
|
|
701
|
+
}
|
|
702
|
+
function reportStemRenderer(params, api) {
|
|
703
|
+
var x = api.value(0);
|
|
704
|
+
var floor = api.coord([x, 0]);
|
|
705
|
+
var head = api.coord([x, api.value(1)]);
|
|
706
|
+
return {
|
|
707
|
+
type: "group",
|
|
708
|
+
children: [
|
|
709
|
+
{ type: "line", shape: { x1: floor[0], y1: floor[1], x2: head[0], y2: head[1] }, style: { stroke: ${JSON.stringify(GUIDE_LINE_COLOR)}, lineWidth: 1 } },
|
|
710
|
+
{ type: "circle", shape: { cx: head[0], cy: head[1], r: ${STEM_HEAD_RADIUS_PX} }, style: { fill: api.visual("color"), stroke: ${JSON.stringify(CHART_INK)}, lineWidth: 0.5 } }
|
|
711
|
+
]
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
function reportRegisterRenderers(runtime) {
|
|
715
|
+
${CHART_RENDERERS.map((entry) => ` runtime.registerCustomSeries(${JSON.stringify(entry.name)}, ${entry.pageFunction});`).join("\n")}
|
|
716
|
+
}
|
|
717
|
+
function reportScaledAxes(axes, scale) {
|
|
718
|
+
if (Array.isArray(axes)) {
|
|
719
|
+
var list = [];
|
|
720
|
+
for (var a = 0; a < axes.length; a++) {
|
|
721
|
+
list.push(reportScaledAxes(axes[a], scale));
|
|
722
|
+
}
|
|
723
|
+
return list;
|
|
724
|
+
}
|
|
725
|
+
if (typeof axes !== "object" || axes === null) {
|
|
726
|
+
return axes;
|
|
727
|
+
}
|
|
728
|
+
var axis = Object.assign({}, axes);
|
|
729
|
+
if (typeof axis.nameGap === "number") {
|
|
730
|
+
axis.nameGap = axis.nameGap * scale;
|
|
731
|
+
}
|
|
732
|
+
if (typeof axis.offset === "number") {
|
|
733
|
+
axis.offset = axis.offset * scale;
|
|
734
|
+
}
|
|
735
|
+
return axis;
|
|
736
|
+
}
|
|
737
|
+
function reportScaledGraphic(element, scale) {
|
|
738
|
+
if (typeof element !== "object" || element === null) {
|
|
739
|
+
return element;
|
|
740
|
+
}
|
|
741
|
+
var out = Object.assign({}, element);
|
|
742
|
+
var style = out.style;
|
|
743
|
+
if (style !== undefined && typeof style.fontSize === "number") {
|
|
744
|
+
out.style = Object.assign({}, style, { fontSize: style.fontSize * scale });
|
|
745
|
+
}
|
|
746
|
+
if (Array.isArray(out.children)) {
|
|
747
|
+
var children = [];
|
|
748
|
+
for (var c = 0; c < out.children.length; c++) {
|
|
749
|
+
children.push(reportScaledGraphic(out.children[c], scale));
|
|
750
|
+
}
|
|
751
|
+
out.children = children;
|
|
752
|
+
}
|
|
753
|
+
return out;
|
|
754
|
+
}
|
|
755
|
+
function reportLegendLines(option, textPx, widthPx) {
|
|
756
|
+
var legend = option.legend;
|
|
757
|
+
var names = [];
|
|
758
|
+
if (Array.isArray(legend.data)) {
|
|
759
|
+
for (var d = 0; d < legend.data.length; d++) {
|
|
760
|
+
var entry = legend.data[d];
|
|
761
|
+
var entryName = typeof entry === "object" && entry !== null ? entry.name : entry;
|
|
762
|
+
if (typeof entryName === "string") {
|
|
763
|
+
names.push(entryName);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
} else if (Array.isArray(option.series)) {
|
|
767
|
+
for (var s = 0; s < option.series.length; s++) {
|
|
768
|
+
var series = option.series[s];
|
|
769
|
+
var seriesName = typeof series === "object" && series !== null ? series.name : undefined;
|
|
770
|
+
if (typeof seriesName === "string" && seriesName !== "" && names.indexOf(seriesName) < 0) {
|
|
771
|
+
names.push(seriesName);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
var room = widthPx - 2 * ${LEGEND_PADDING_PX};
|
|
776
|
+
var lines = names.length === 0 ? 0 : 1;
|
|
777
|
+
var used = 0;
|
|
778
|
+
for (var n = 0; n < names.length; n++) {
|
|
779
|
+
var width = textPx + 5 + names[n].length * textPx * ${CHARACTER_SHARE};
|
|
780
|
+
if (used > 0 && used + ${LEGEND_ITEM_GAP_PX} + width > room) {
|
|
781
|
+
lines += 1;
|
|
782
|
+
used = width;
|
|
783
|
+
} else {
|
|
784
|
+
used = used > 0 ? used + ${LEGEND_ITEM_GAP_PX} + width : width;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
return lines;
|
|
788
|
+
}
|
|
789
|
+
function reportPanelBottom(grid) {
|
|
790
|
+
if (typeof grid !== "object" || grid === null) {
|
|
791
|
+
return 0;
|
|
792
|
+
}
|
|
793
|
+
if (grid.outerBoundsMode !== ${JSON.stringify(PANEL_BOUNDS_MODE)} || typeof grid.top !== "string" || typeof grid.height !== "string") {
|
|
794
|
+
return 0;
|
|
795
|
+
}
|
|
796
|
+
return Number.parseFloat(grid.top) + Number.parseFloat(grid.height);
|
|
797
|
+
}
|
|
798
|
+
function reportHoldsFacetLegend(option) {
|
|
799
|
+
var legend = option.legend;
|
|
800
|
+
var bottom = typeof legend === "object" && legend !== null && legend.show !== false && legend.bottom !== undefined;
|
|
801
|
+
if (!bottom || !Array.isArray(option.grid)) {
|
|
802
|
+
return false;
|
|
803
|
+
}
|
|
804
|
+
for (var g = 0; g < option.grid.length; g++) {
|
|
805
|
+
if (reportPanelBottom(option.grid[g]) > 0) {
|
|
806
|
+
return true;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return false;
|
|
810
|
+
}
|
|
811
|
+
function reportLegendBand(lines, textPx) {
|
|
812
|
+
return Math.round(lines * (textPx * ${LEGEND_LINE_SHARE} + ${LEGEND_ITEM_GAP_PX}) + textPx);
|
|
813
|
+
}
|
|
814
|
+
function reportFacetOverLegend(option, textPx, widthPx, heightPx) {
|
|
815
|
+
var grids = option.grid;
|
|
816
|
+
var title = Math.round(textPx * ${FACET_TITLE_SHARE});
|
|
817
|
+
var tops = [];
|
|
818
|
+
var lowest = 0;
|
|
819
|
+
var height = 0;
|
|
820
|
+
for (var g = 0; g < grids.length; g++) {
|
|
821
|
+
var edge = reportPanelBottom(grids[g]);
|
|
822
|
+
if (edge <= 0) {
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
var gridTop = Number.parseFloat(String(grids[g].top));
|
|
826
|
+
if (tops.indexOf(gridTop) < 0) {
|
|
827
|
+
tops.push(gridTop);
|
|
828
|
+
}
|
|
829
|
+
if (edge <= lowest) {
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
lowest = edge;
|
|
833
|
+
height = Number.parseFloat(String(grids[g].height));
|
|
834
|
+
}
|
|
835
|
+
tops.sort(function (a, b) {
|
|
836
|
+
return a - b;
|
|
837
|
+
});
|
|
838
|
+
function partOf(legendPx) {
|
|
839
|
+
return Math.max(0, lowest - ((heightPx - legendPx - title) / heightPx) * 100) / tops.length;
|
|
840
|
+
}
|
|
841
|
+
var lines = reportLegendLines(option, textPx, widthPx);
|
|
842
|
+
var band = reportLegendBand(lines, textPx);
|
|
843
|
+
if (lines > 1 && partOf(band) > height * (1 - ${PANEL_MIN_SHARE})) {
|
|
844
|
+
option.legend = Object.assign({}, option.legend, { type: "scroll" });
|
|
845
|
+
band = reportLegendBand(1, textPx);
|
|
846
|
+
}
|
|
847
|
+
var part = partOf(band);
|
|
848
|
+
function rowOf(top) {
|
|
849
|
+
for (var r = 0; r < tops.length; r++) {
|
|
850
|
+
if (tops[r] >= top) {
|
|
851
|
+
return r;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
return -1;
|
|
855
|
+
}
|
|
856
|
+
function moved(value, row) {
|
|
857
|
+
return Math.round((value - row * part) * 1e4) / 1e4 + "%";
|
|
858
|
+
}
|
|
859
|
+
if (part > 0) {
|
|
860
|
+
var placed = [];
|
|
861
|
+
for (var p = 0; p < grids.length; p++) {
|
|
862
|
+
if (reportPanelBottom(grids[p]) <= 0) {
|
|
863
|
+
placed.push(grids[p]);
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
var top = Number.parseFloat(String(grids[p].top));
|
|
867
|
+
placed.push(Object.assign({}, grids[p], { top: moved(top, tops.indexOf(top)), height: Math.round((Number.parseFloat(String(grids[p].height)) - part) * 1e4) / 1e4 + "%" }));
|
|
868
|
+
}
|
|
869
|
+
option.grid = placed;
|
|
870
|
+
}
|
|
871
|
+
if (Array.isArray(option.graphic)) {
|
|
872
|
+
var graphic = [];
|
|
873
|
+
for (var e = 0; e < option.graphic.length; e++) {
|
|
874
|
+
var element = option.graphic[e];
|
|
875
|
+
if (typeof element !== "object" || element === null) {
|
|
876
|
+
graphic.push(element);
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
if (element.bottom !== undefined) {
|
|
880
|
+
graphic.push(Object.assign({}, element, { bottom: band }));
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
var labelTop = typeof element.top === "string" && element.top.endsWith("%") ? Number.parseFloat(element.top) : Number.NaN;
|
|
884
|
+
var row = Number.isFinite(labelTop) && part > 0 ? rowOf(labelTop) : -1;
|
|
885
|
+
graphic.push(row > 0 ? Object.assign({}, element, { top: moved(labelTop, row) }) : element);
|
|
886
|
+
}
|
|
887
|
+
option.graphic = graphic;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
function reportHoldsBottomLegend(option) {
|
|
891
|
+
var legend = option.legend;
|
|
892
|
+
var bottom = typeof legend === "object" && legend !== null && legend.show !== false && legend.bottom !== undefined;
|
|
893
|
+
return bottom && option.xAxis !== undefined && !Array.isArray(option.grid);
|
|
894
|
+
}
|
|
895
|
+
function reportCategoryInterval(count, textPx, widthPx) {
|
|
896
|
+
var fit = Math.max(1, Math.floor(widthPx / (textPx * ${LABEL_LINE_SHARE})));
|
|
897
|
+
return count > fit ? Math.ceil(count / fit) - 1 : 0;
|
|
898
|
+
}
|
|
899
|
+
function reportFittedCategories(axis, textPx, widthPx) {
|
|
900
|
+
if (typeof axis !== "object" || axis === null || Array.isArray(axis)) {
|
|
901
|
+
return axis;
|
|
902
|
+
}
|
|
903
|
+
var data = axis.data;
|
|
904
|
+
if (axis.type !== "category" || !Array.isArray(data) || data.length === 0) {
|
|
905
|
+
return axis;
|
|
906
|
+
}
|
|
907
|
+
var label = typeof axis.axisLabel === "object" && axis.axisLabel !== null ? axis.axisLabel : {};
|
|
908
|
+
var rotate = label.rotate;
|
|
909
|
+
if (typeof rotate !== "number" || rotate === 0) {
|
|
910
|
+
var longest = 0;
|
|
911
|
+
for (var d = 0; d < data.length; d++) {
|
|
912
|
+
longest = Math.max(longest, String(data[d]).length);
|
|
913
|
+
}
|
|
914
|
+
var need = longest * textPx * ${CHARACTER_SHARE};
|
|
915
|
+
var room = (widthPx * ${LABEL_ROOM}) / data.length;
|
|
916
|
+
if (need <= room) {
|
|
917
|
+
return axis;
|
|
918
|
+
}
|
|
919
|
+
rotate = need > 2 * room ? 90 : 45;
|
|
920
|
+
}
|
|
921
|
+
var fitted = { rotate: rotate };
|
|
922
|
+
if (rotate === 90) {
|
|
923
|
+
fitted.interval = reportCategoryInterval(data.length, textPx, widthPx * ${LABEL_ROOM});
|
|
924
|
+
}
|
|
925
|
+
return Object.assign({}, axis, { axisLabel: Object.assign({}, label, fitted) });
|
|
926
|
+
}
|
|
927
|
+
function reportWrappedTitle(title, textPx, widthPx) {
|
|
928
|
+
var perLine = Math.max(1, Math.floor((widthPx * ${COLOR_SCALE_BAND_PCT} * ${SCALE_TITLE_ROOM}) / 100 / (textPx * ${CHARACTER_SHARE})));
|
|
929
|
+
var words = title.split(" ");
|
|
930
|
+
var lines = [];
|
|
931
|
+
var line = "";
|
|
932
|
+
for (var w = 0; w < words.length; w++) {
|
|
933
|
+
var word = words[w];
|
|
934
|
+
if (line === "") {
|
|
935
|
+
line = word;
|
|
936
|
+
} else if (line.length + 1 + word.length <= perLine) {
|
|
937
|
+
line = line + " " + word;
|
|
938
|
+
} else {
|
|
939
|
+
lines.push(line);
|
|
940
|
+
line = word;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
if (line !== "") {
|
|
944
|
+
lines.push(line);
|
|
945
|
+
}
|
|
946
|
+
return lines.join("\\n");
|
|
947
|
+
}
|
|
948
|
+
function reportGridWidth(grid, axis, widthPx) {
|
|
949
|
+
var place = typeof axis === "object" && axis !== null && typeof axis.gridIndex === "number" ? axis.gridIndex : 0;
|
|
950
|
+
var entry = Array.isArray(grid) ? grid[place] : grid;
|
|
951
|
+
var width = typeof entry === "object" && entry !== null ? entry.width : undefined;
|
|
952
|
+
if (typeof width === "string" && width.endsWith("%")) {
|
|
953
|
+
return (widthPx * Number.parseFloat(width)) / 100;
|
|
954
|
+
}
|
|
955
|
+
if (typeof width === "number") {
|
|
956
|
+
return width;
|
|
957
|
+
}
|
|
958
|
+
return widthPx;
|
|
959
|
+
}
|
|
960
|
+
function reportFittedAxes(axes, grid, textPx, widthPx) {
|
|
961
|
+
if (!Array.isArray(axes)) {
|
|
962
|
+
return reportFittedCategories(axes, textPx, reportGridWidth(grid, axes, widthPx));
|
|
963
|
+
}
|
|
964
|
+
var fitted = [];
|
|
965
|
+
for (var a = 0; a < axes.length; a++) {
|
|
966
|
+
fitted.push(reportFittedCategories(axes[a], textPx, reportGridWidth(grid, axes[a], widthPx)));
|
|
967
|
+
}
|
|
968
|
+
return fitted;
|
|
969
|
+
}
|
|
970
|
+
function reportStillScale(map, textPx, widthPx) {
|
|
971
|
+
if (typeof map !== "object" || map === null) {
|
|
972
|
+
return map;
|
|
973
|
+
}
|
|
974
|
+
var text = map.text;
|
|
975
|
+
if (map.show === false || map.type === "piecewise" || !Array.isArray(text) || typeof text[0] !== "string" || typeof text[1] !== "string") {
|
|
976
|
+
return map;
|
|
977
|
+
}
|
|
978
|
+
var lines = text[0].split("\\n");
|
|
979
|
+
var upper = lines[lines.length - 1];
|
|
980
|
+
var title = lines.slice(0, -1).join(" ");
|
|
981
|
+
return Object.assign({}, map, {
|
|
982
|
+
top: ${JSON.stringify(STILL_SCALE_TOP)},
|
|
983
|
+
itemHeight: ${STILL_SCALE_LINES} * textPx,
|
|
984
|
+
text: [title === "" ? upper : reportWrappedTitle(title, textPx, widthPx) + "\\n" + upper, text[1]]
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
function reportSizedSeries(series, sized, widthPx) {
|
|
988
|
+
if (!Array.isArray(series) || typeof sized !== "object" || sized === null) {
|
|
989
|
+
return series;
|
|
990
|
+
}
|
|
991
|
+
var widths = sized.widths;
|
|
992
|
+
var chosen = typeof widths === "object" && widths !== null ? widths[String(widthPx)] : undefined;
|
|
993
|
+
if (!Array.isArray(sized.names) || !Array.isArray(chosen)) {
|
|
994
|
+
return series;
|
|
995
|
+
}
|
|
996
|
+
var kept = [];
|
|
997
|
+
for (var s = 0; s < series.length; s++) {
|
|
998
|
+
var entry = series[s];
|
|
999
|
+
if (typeof entry !== "object" || entry === null || sized.names.indexOf(entry.name) < 0) {
|
|
1000
|
+
kept.push(entry);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
return kept.concat(chosen);
|
|
1004
|
+
}
|
|
1005
|
+
function reportExportOption(option, textPx, widthPx, heightPx) {
|
|
1006
|
+
var scale = textPx / ${CHART_PAGE_TEXT_PX};
|
|
1007
|
+
var out = {};
|
|
1008
|
+
var keys = Object.keys(option);
|
|
1009
|
+
for (var k = 0; k < keys.length; k++) {
|
|
1010
|
+
if (keys[k] === "tooltip" || keys[k] === "toolbox" || keys[k] === ${JSON.stringify(SIZED_SERIES_MEMBER)}) {
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
1013
|
+
out[keys[k]] = option[keys[k]];
|
|
1014
|
+
}
|
|
1015
|
+
if (out.series !== undefined) {
|
|
1016
|
+
out.series = reportSizedSeries(out.series, option[${JSON.stringify(SIZED_SERIES_MEMBER)}], widthPx);
|
|
1017
|
+
}
|
|
1018
|
+
out.animation = false;
|
|
1019
|
+
if (out.xAxis !== undefined) {
|
|
1020
|
+
out.xAxis = reportScaledAxes(out.xAxis, scale);
|
|
1021
|
+
}
|
|
1022
|
+
if (out.yAxis !== undefined) {
|
|
1023
|
+
out.yAxis = reportScaledAxes(out.yAxis, scale);
|
|
1024
|
+
}
|
|
1025
|
+
if (Array.isArray(out.graphic)) {
|
|
1026
|
+
var graphic = [];
|
|
1027
|
+
for (var g = 0; g < out.graphic.length; g++) {
|
|
1028
|
+
graphic.push(reportScaledGraphic(out.graphic[g], scale));
|
|
1029
|
+
}
|
|
1030
|
+
out.graphic = graphic;
|
|
1031
|
+
}
|
|
1032
|
+
if (out.xAxis !== undefined) {
|
|
1033
|
+
out.xAxis = reportFittedAxes(out.xAxis, out.grid, textPx, widthPx);
|
|
1034
|
+
}
|
|
1035
|
+
if (Array.isArray(out.visualMap)) {
|
|
1036
|
+
var maps = [];
|
|
1037
|
+
for (var m = 0; m < out.visualMap.length; m++) {
|
|
1038
|
+
maps.push(reportStillScale(out.visualMap[m], textPx, widthPx));
|
|
1039
|
+
}
|
|
1040
|
+
out.visualMap = maps;
|
|
1041
|
+
} else if (out.visualMap !== undefined) {
|
|
1042
|
+
out.visualMap = reportStillScale(out.visualMap, textPx, widthPx);
|
|
1043
|
+
}
|
|
1044
|
+
if (typeof out.grid === "object" && out.grid !== null && !Array.isArray(out.grid) && out.grid.containLabel === true) {
|
|
1045
|
+
out.grid = Object.assign({}, out.grid, { containLabel: false, outerBoundsMode: "auto", outerBoundsContain: "all" });
|
|
1046
|
+
}
|
|
1047
|
+
if (reportHoldsBottomLegend(out)) {
|
|
1048
|
+
var grid = typeof out.grid === "object" && out.grid !== null ? out.grid : {};
|
|
1049
|
+
out.grid = Object.assign({}, grid, { outerBoundsMode: "auto", outerBounds: { left: 0, right: 0, top: 0, bottom: Math.round(textPx * ${LEGEND_BAND_LINES}) } });
|
|
1050
|
+
}
|
|
1051
|
+
if (reportHoldsFacetLegend(out)) {
|
|
1052
|
+
reportFacetOverLegend(out, textPx, widthPx, heightPx);
|
|
1053
|
+
}
|
|
1054
|
+
return out;
|
|
1055
|
+
}`;
|