@opendata-ai/openchart-engine 6.12.0 → 6.13.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/dist/index.js +878 -606
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/axes.test.ts +12 -30
- package/src/__tests__/compile-chart.test.ts +4 -4
- package/src/__tests__/dimensions.test.ts +2 -2
- package/src/__tests__/encoding-sugar.test.ts +389 -0
- package/src/annotations/collisions.ts +268 -0
- package/src/annotations/compute.ts +9 -912
- package/src/annotations/constants.ts +32 -0
- package/src/annotations/geometry.ts +167 -0
- package/src/annotations/position.ts +95 -0
- package/src/annotations/resolve-range.ts +98 -0
- package/src/annotations/resolve-refline.ts +148 -0
- package/src/annotations/resolve-text.ts +134 -0
- package/src/charts/__tests__/post-process.test.ts +258 -0
- package/src/charts/bar/__tests__/labels.test.ts +31 -0
- package/src/charts/bar/compute.ts +27 -6
- package/src/charts/bar/labels.ts +7 -1
- package/src/charts/column/__tests__/compute.test.ts +99 -0
- package/src/charts/column/compute.ts +27 -6
- package/src/charts/line/area.ts +19 -2
- package/src/charts/post-process.ts +215 -0
- package/src/compile.ts +90 -158
- package/src/compiler/normalize.ts +2 -2
- package/src/layout/axes.ts +10 -13
- package/src/layout/dimensions.ts +3 -3
- package/src/layout/scales.ts +106 -29
- package/src/tooltips/__tests__/compute.test.ts +188 -0
- package/src/tooltips/compute.ts +25 -11
- package/src/transforms/__tests__/aggregate.test.ts +159 -0
- package/src/transforms/__tests__/fold.test.ts +79 -0
- package/src/transforms/aggregate.ts +130 -0
- package/src/transforms/fold.ts +49 -0
- package/src/transforms/index.ts +8 -0
package/dist/index.js
CHANGED
|
@@ -11,8 +11,10 @@ import {
|
|
|
11
11
|
resolveTheme as resolveTheme3
|
|
12
12
|
} from "@opendata-ai/openchart-core";
|
|
13
13
|
|
|
14
|
-
// src/annotations/
|
|
15
|
-
import { detectCollision
|
|
14
|
+
// src/annotations/collisions.ts
|
|
15
|
+
import { detectCollision } from "@opendata-ai/openchart-core";
|
|
16
|
+
|
|
17
|
+
// src/annotations/constants.ts
|
|
16
18
|
var DEFAULT_ANNOTATION_FONT_SIZE = 12;
|
|
17
19
|
var DEFAULT_ANNOTATION_FONT_WEIGHT = 400;
|
|
18
20
|
var DEFAULT_LINE_HEIGHT = 1.3;
|
|
@@ -24,6 +26,91 @@ var DARK_TEXT_FILL = "#d1d5db";
|
|
|
24
26
|
var LIGHT_REFLINE_STROKE = "#888888";
|
|
25
27
|
var DARK_REFLINE_STROKE = "#9ca3af";
|
|
26
28
|
var ANCHOR_OFFSET = 8;
|
|
29
|
+
var NUDGE_PADDING = 6;
|
|
30
|
+
var CLAMP_MARGIN = 4;
|
|
31
|
+
|
|
32
|
+
// src/annotations/geometry.ts
|
|
33
|
+
import { estimateTextWidth } from "@opendata-ai/openchart-core";
|
|
34
|
+
function computeTextBounds(labelX, labelY, text, fontSize, fontWeight) {
|
|
35
|
+
const lines = text.split("\n");
|
|
36
|
+
const isMultiLine = lines.length > 1;
|
|
37
|
+
const maxWidth = Math.max(...lines.map((line) => estimateTextWidth(line, fontSize, fontWeight)));
|
|
38
|
+
const totalHeight = lines.length * fontSize * DEFAULT_LINE_HEIGHT;
|
|
39
|
+
const x2 = isMultiLine ? labelX - maxWidth / 2 : labelX;
|
|
40
|
+
return {
|
|
41
|
+
x: x2,
|
|
42
|
+
y: labelY - fontSize,
|
|
43
|
+
width: maxWidth,
|
|
44
|
+
height: totalHeight
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function computeAnchorOffset(anchor, _px, py, chartArea) {
|
|
48
|
+
if (!anchor || anchor === "auto") {
|
|
49
|
+
const isUpperHalf = py < chartArea.y + chartArea.height / 2;
|
|
50
|
+
return isUpperHalf ? { dx: ANCHOR_OFFSET, dy: ANCHOR_OFFSET } : { dx: ANCHOR_OFFSET, dy: -ANCHOR_OFFSET };
|
|
51
|
+
}
|
|
52
|
+
switch (anchor) {
|
|
53
|
+
case "top":
|
|
54
|
+
return { dx: 0, dy: -ANCHOR_OFFSET };
|
|
55
|
+
case "bottom":
|
|
56
|
+
return { dx: 0, dy: ANCHOR_OFFSET };
|
|
57
|
+
case "left":
|
|
58
|
+
return { dx: -ANCHOR_OFFSET, dy: 0 };
|
|
59
|
+
case "right":
|
|
60
|
+
return { dx: ANCHOR_OFFSET, dy: 0 };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function applyOffset(base, offset) {
|
|
64
|
+
if (!offset) return base;
|
|
65
|
+
return {
|
|
66
|
+
dx: base.dx + (offset.dx ?? 0),
|
|
67
|
+
dy: base.dy + (offset.dy ?? 0)
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function computeConnectorOrigin(labelX, labelY, text, fontSize, fontWeight, targetX, targetY, connectorStyle) {
|
|
71
|
+
const box = computeTextBounds(labelX, labelY, text, fontSize, fontWeight);
|
|
72
|
+
const boxCenterX = box.x + box.width / 2;
|
|
73
|
+
const boxCenterY = box.y + box.height / 2;
|
|
74
|
+
if (connectorStyle === "curve") {
|
|
75
|
+
return {
|
|
76
|
+
x: box.x + box.width,
|
|
77
|
+
y: boxCenterY
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const halfW = box.width / 2 || 1;
|
|
81
|
+
const halfH = box.height / 2 || 1;
|
|
82
|
+
const ndx = (targetX - boxCenterX) / halfW;
|
|
83
|
+
const ndy = (targetY - boxCenterY) / halfH;
|
|
84
|
+
if (Math.abs(ndy) >= Math.abs(ndx)) {
|
|
85
|
+
return ndy < 0 ? { x: boxCenterX, y: box.y } : { x: boxCenterX, y: box.y + box.height };
|
|
86
|
+
}
|
|
87
|
+
return ndx < 0 ? { x: box.x, y: boxCenterY } : { x: box.x + box.width, y: boxCenterY };
|
|
88
|
+
}
|
|
89
|
+
function estimateLabelBounds(label) {
|
|
90
|
+
const fontSize = label.style.fontSize ?? DEFAULT_ANNOTATION_FONT_SIZE;
|
|
91
|
+
const fontWeight = label.style.fontWeight ?? DEFAULT_ANNOTATION_FONT_WEIGHT;
|
|
92
|
+
return computeTextBounds(label.x, label.y, label.text, fontSize, fontWeight);
|
|
93
|
+
}
|
|
94
|
+
function recomputeConnector(label, targetX, targetY) {
|
|
95
|
+
const connector = label.connector;
|
|
96
|
+
if (!connector) return connector;
|
|
97
|
+
const fontSize = label.style.fontSize ?? DEFAULT_ANNOTATION_FONT_SIZE;
|
|
98
|
+
const fontWeight = label.style.fontWeight ?? DEFAULT_ANNOTATION_FONT_WEIGHT;
|
|
99
|
+
const connStyle = connector.style === "curve" ? "curve" : "straight";
|
|
100
|
+
const newFrom = computeConnectorOrigin(
|
|
101
|
+
label.x,
|
|
102
|
+
label.y,
|
|
103
|
+
label.text,
|
|
104
|
+
fontSize,
|
|
105
|
+
fontWeight,
|
|
106
|
+
targetX,
|
|
107
|
+
targetY,
|
|
108
|
+
connStyle
|
|
109
|
+
);
|
|
110
|
+
return { ...connector, from: newFrom };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/annotations/position.ts
|
|
27
114
|
function interpolateInDomain(numValue, domain, positionOf) {
|
|
28
115
|
if (domain.length < 2) return null;
|
|
29
116
|
const nums = domain.map(Number);
|
|
@@ -83,6 +170,150 @@ function resolvePosition(value2, scale) {
|
|
|
83
170
|
}
|
|
84
171
|
return null;
|
|
85
172
|
}
|
|
173
|
+
|
|
174
|
+
// src/annotations/collisions.ts
|
|
175
|
+
function generateNudgeCandidates(selfBounds, obstacles, padding) {
|
|
176
|
+
const candidates = [];
|
|
177
|
+
for (const obs of obstacles) {
|
|
178
|
+
const belowDy = obs.y + obs.height + padding - selfBounds.y;
|
|
179
|
+
candidates.push({ dx: 0, dy: belowDy, distance: Math.abs(belowDy) });
|
|
180
|
+
const aboveDy = obs.y - padding - (selfBounds.y + selfBounds.height);
|
|
181
|
+
candidates.push({ dx: 0, dy: aboveDy, distance: Math.abs(aboveDy) });
|
|
182
|
+
const leftDx = obs.x - padding - (selfBounds.x + selfBounds.width);
|
|
183
|
+
candidates.push({ dx: leftDx, dy: 0, distance: Math.abs(leftDx) });
|
|
184
|
+
const rightDx = obs.x + obs.width + padding - selfBounds.x;
|
|
185
|
+
candidates.push({ dx: rightDx, dy: 0, distance: Math.abs(rightDx) });
|
|
186
|
+
}
|
|
187
|
+
candidates.sort((a, b) => a.distance - b.distance);
|
|
188
|
+
return candidates;
|
|
189
|
+
}
|
|
190
|
+
function nudgeAnnotationFromObstacles(annotation, originalAnnotation, scales, chartArea, obstacles) {
|
|
191
|
+
if (annotation.type !== "text" || !annotation.label) return false;
|
|
192
|
+
const labelBounds = estimateLabelBounds(annotation.label);
|
|
193
|
+
const collidingObs = obstacles.filter(
|
|
194
|
+
(obs) => obs.width > 0 && obs.height > 0 && detectCollision(labelBounds, obs)
|
|
195
|
+
);
|
|
196
|
+
if (collidingObs.length === 0) return false;
|
|
197
|
+
const px = resolvePosition(originalAnnotation.x, scales.x);
|
|
198
|
+
const py = resolvePosition(originalAnnotation.y, scales.y);
|
|
199
|
+
if (px === null || py === null) return false;
|
|
200
|
+
const candidates = generateNudgeCandidates(labelBounds, collidingObs, NUDGE_PADDING);
|
|
201
|
+
const fontSize = labelBounds.height / Math.max(1, annotation.label.text.split("\n").length);
|
|
202
|
+
for (const { dx, dy } of candidates) {
|
|
203
|
+
const newLabelX = annotation.label.x + dx;
|
|
204
|
+
const newLabelY = annotation.label.y + dy;
|
|
205
|
+
const candidateLabel = {
|
|
206
|
+
...annotation.label,
|
|
207
|
+
x: newLabelX,
|
|
208
|
+
y: newLabelY,
|
|
209
|
+
connector: recomputeConnector({ ...annotation.label, x: newLabelX, y: newLabelY }, px, py)
|
|
210
|
+
};
|
|
211
|
+
const candidateBounds = estimateLabelBounds(candidateLabel);
|
|
212
|
+
const stillCollides = obstacles.some(
|
|
213
|
+
(obs) => obs.width > 0 && obs.height > 0 && detectCollision(candidateBounds, obs)
|
|
214
|
+
);
|
|
215
|
+
if (stillCollides) continue;
|
|
216
|
+
const labelCenterX = candidateBounds.x + candidateBounds.width / 2;
|
|
217
|
+
const labelCenterY = candidateBounds.y + candidateBounds.height / 2;
|
|
218
|
+
const inBounds = labelCenterX >= chartArea.x && labelCenterX <= chartArea.x + chartArea.width + 10 && labelCenterY >= chartArea.y - fontSize && labelCenterY <= chartArea.y + chartArea.height + fontSize * 3;
|
|
219
|
+
if (inBounds) {
|
|
220
|
+
annotation.label = candidateLabel;
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
function resolveAnnotationCollisions(annotations, originalSpecs, scales, chartArea) {
|
|
227
|
+
const placedBounds = [];
|
|
228
|
+
for (let i = 0; i < annotations.length; i++) {
|
|
229
|
+
const annotation = annotations[i];
|
|
230
|
+
if (annotation.type !== "text" || !annotation.label) {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const bounds = estimateLabelBounds(annotation.label);
|
|
234
|
+
const collidingBounds = placedBounds.filter(
|
|
235
|
+
(pb) => pb.width > 0 && pb.height > 0 && detectCollision(bounds, pb)
|
|
236
|
+
);
|
|
237
|
+
if (collidingBounds.length > 0) {
|
|
238
|
+
const originalSpec = originalSpecs[i];
|
|
239
|
+
if (originalSpec?.type === "text") {
|
|
240
|
+
const px = resolvePosition(originalSpec.x, scales.x);
|
|
241
|
+
const py = resolvePosition(originalSpec.y, scales.y);
|
|
242
|
+
if (px !== null && py !== null) {
|
|
243
|
+
const candidates = generateNudgeCandidates(bounds, collidingBounds, NUDGE_PADDING);
|
|
244
|
+
const fontSize = bounds.height / Math.max(1, annotation.label.text.split("\n").length);
|
|
245
|
+
for (const { dx, dy } of candidates) {
|
|
246
|
+
const newLabelX = annotation.label.x + dx;
|
|
247
|
+
const newLabelY = annotation.label.y + dy;
|
|
248
|
+
const candidateLabel = {
|
|
249
|
+
...annotation.label,
|
|
250
|
+
x: newLabelX,
|
|
251
|
+
y: newLabelY
|
|
252
|
+
};
|
|
253
|
+
const candidateBounds = estimateLabelBounds(candidateLabel);
|
|
254
|
+
const stillCollides = placedBounds.some(
|
|
255
|
+
(pb) => pb.width > 0 && pb.height > 0 && detectCollision(candidateBounds, pb)
|
|
256
|
+
);
|
|
257
|
+
if (stillCollides) continue;
|
|
258
|
+
const labelCenterX = candidateBounds.x + candidateBounds.width / 2;
|
|
259
|
+
const labelCenterY = candidateBounds.y + candidateBounds.height / 2;
|
|
260
|
+
const inBounds = labelCenterX >= chartArea.x && labelCenterX <= chartArea.x + chartArea.width + 10 && labelCenterY >= chartArea.y - fontSize && labelCenterY <= chartArea.y + chartArea.height + fontSize;
|
|
261
|
+
if (inBounds) {
|
|
262
|
+
annotation.label = {
|
|
263
|
+
...annotation.label,
|
|
264
|
+
x: newLabelX,
|
|
265
|
+
y: newLabelY,
|
|
266
|
+
connector: recomputeConnector(
|
|
267
|
+
{ ...annotation.label, x: newLabelX, y: newLabelY },
|
|
268
|
+
px,
|
|
269
|
+
py
|
|
270
|
+
)
|
|
271
|
+
};
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
placedBounds.push(estimateLabelBounds(annotation.label));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function clampAnnotationsToBounds(annotations, svgWidth, svgHeight) {
|
|
282
|
+
for (const annotation of annotations) {
|
|
283
|
+
if (annotation.type !== "text" || !annotation.label) continue;
|
|
284
|
+
const bounds = estimateLabelBounds(annotation.label);
|
|
285
|
+
let dx = 0;
|
|
286
|
+
let dy = 0;
|
|
287
|
+
if (bounds.x + bounds.width > svgWidth - CLAMP_MARGIN) {
|
|
288
|
+
dx = svgWidth - CLAMP_MARGIN - (bounds.x + bounds.width);
|
|
289
|
+
}
|
|
290
|
+
if (bounds.x + dx < CLAMP_MARGIN) {
|
|
291
|
+
dx = CLAMP_MARGIN - bounds.x;
|
|
292
|
+
}
|
|
293
|
+
if (bounds.y < CLAMP_MARGIN) {
|
|
294
|
+
dy = CLAMP_MARGIN - bounds.y;
|
|
295
|
+
}
|
|
296
|
+
if (bounds.y + bounds.height + dy > svgHeight - CLAMP_MARGIN) {
|
|
297
|
+
dy = svgHeight - CLAMP_MARGIN - (bounds.y + bounds.height);
|
|
298
|
+
}
|
|
299
|
+
if (dx === 0 && dy === 0) continue;
|
|
300
|
+
const newX = annotation.label.x + dx;
|
|
301
|
+
const newY = annotation.label.y + dy;
|
|
302
|
+
const connector = annotation.label.connector;
|
|
303
|
+
annotation.label = {
|
|
304
|
+
...annotation.label,
|
|
305
|
+
x: newX,
|
|
306
|
+
y: newY,
|
|
307
|
+
connector: connector ? recomputeConnector(
|
|
308
|
+
{ ...annotation.label, x: newX, y: newY },
|
|
309
|
+
connector.to.x,
|
|
310
|
+
connector.to.y
|
|
311
|
+
) : void 0
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// src/annotations/resolve-text.ts
|
|
86
317
|
function makeAnnotationLabelStyle(fontSize, fontWeight, fill, isDark) {
|
|
87
318
|
const defaultFill = isDark ? DARK_TEXT_FILL : LIGHT_TEXT_FILL;
|
|
88
319
|
return {
|
|
@@ -94,61 +325,6 @@ function makeAnnotationLabelStyle(fontSize, fontWeight, fill, isDark) {
|
|
|
94
325
|
textAnchor: "start"
|
|
95
326
|
};
|
|
96
327
|
}
|
|
97
|
-
function computeTextBounds(labelX, labelY, text, fontSize, fontWeight) {
|
|
98
|
-
const lines = text.split("\n");
|
|
99
|
-
const isMultiLine = lines.length > 1;
|
|
100
|
-
const maxWidth = Math.max(...lines.map((line) => estimateTextWidth(line, fontSize, fontWeight)));
|
|
101
|
-
const totalHeight = lines.length * fontSize * DEFAULT_LINE_HEIGHT;
|
|
102
|
-
const x2 = isMultiLine ? labelX - maxWidth / 2 : labelX;
|
|
103
|
-
return {
|
|
104
|
-
x: x2,
|
|
105
|
-
y: labelY - fontSize,
|
|
106
|
-
width: maxWidth,
|
|
107
|
-
height: totalHeight
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
function computeAnchorOffset(anchor, _px, py, chartArea) {
|
|
111
|
-
if (!anchor || anchor === "auto") {
|
|
112
|
-
const isUpperHalf = py < chartArea.y + chartArea.height / 2;
|
|
113
|
-
return isUpperHalf ? { dx: ANCHOR_OFFSET, dy: ANCHOR_OFFSET } : { dx: ANCHOR_OFFSET, dy: -ANCHOR_OFFSET };
|
|
114
|
-
}
|
|
115
|
-
switch (anchor) {
|
|
116
|
-
case "top":
|
|
117
|
-
return { dx: 0, dy: -ANCHOR_OFFSET };
|
|
118
|
-
case "bottom":
|
|
119
|
-
return { dx: 0, dy: ANCHOR_OFFSET };
|
|
120
|
-
case "left":
|
|
121
|
-
return { dx: -ANCHOR_OFFSET, dy: 0 };
|
|
122
|
-
case "right":
|
|
123
|
-
return { dx: ANCHOR_OFFSET, dy: 0 };
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
function applyOffset(base, offset) {
|
|
127
|
-
if (!offset) return base;
|
|
128
|
-
return {
|
|
129
|
-
dx: base.dx + (offset.dx ?? 0),
|
|
130
|
-
dy: base.dy + (offset.dy ?? 0)
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
function computeConnectorOrigin(labelX, labelY, text, fontSize, fontWeight, targetX, targetY, connectorStyle) {
|
|
134
|
-
const box = computeTextBounds(labelX, labelY, text, fontSize, fontWeight);
|
|
135
|
-
const boxCenterX = box.x + box.width / 2;
|
|
136
|
-
const boxCenterY = box.y + box.height / 2;
|
|
137
|
-
if (connectorStyle === "curve") {
|
|
138
|
-
return {
|
|
139
|
-
x: box.x + box.width,
|
|
140
|
-
y: boxCenterY
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
const halfW = box.width / 2 || 1;
|
|
144
|
-
const halfH = box.height / 2 || 1;
|
|
145
|
-
const ndx = (targetX - boxCenterX) / halfW;
|
|
146
|
-
const ndy = (targetY - boxCenterY) / halfH;
|
|
147
|
-
if (Math.abs(ndy) >= Math.abs(ndx)) {
|
|
148
|
-
return ndy < 0 ? { x: boxCenterX, y: box.y } : { x: boxCenterX, y: box.y + box.height };
|
|
149
|
-
}
|
|
150
|
-
return ndx < 0 ? { x: box.x, y: boxCenterY } : { x: box.x + box.width, y: boxCenterY };
|
|
151
|
-
}
|
|
152
328
|
function resolveTextAnnotation(annotation, scales, chartArea, isDark) {
|
|
153
329
|
const px = resolvePosition(annotation.x, scales.x);
|
|
154
330
|
const py = resolvePosition(annotation.y, scales.y);
|
|
@@ -217,6 +393,8 @@ function resolveTextAnnotation(annotation, scales, chartArea, isDark) {
|
|
|
217
393
|
zIndex: annotation.zIndex
|
|
218
394
|
};
|
|
219
395
|
}
|
|
396
|
+
|
|
397
|
+
// src/annotations/resolve-range.ts
|
|
220
398
|
function resolveRangeAnnotation(annotation, scales, chartArea, isDark) {
|
|
221
399
|
let x2 = chartArea.x;
|
|
222
400
|
let y2 = chartArea.y;
|
|
@@ -252,310 +430,125 @@ function resolveRangeAnnotation(annotation, scales, chartArea, isDark) {
|
|
|
252
430
|
}
|
|
253
431
|
const baseX = centered ? x2 + width / 2 : anchor === "right" ? x2 + width : x2;
|
|
254
432
|
label = {
|
|
255
|
-
text: annotation.label,
|
|
256
|
-
x: baseX + labelDelta.dx,
|
|
257
|
-
y: y2 + labelDelta.dy,
|
|
258
|
-
style,
|
|
259
|
-
visible: true
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
const defaultOpacity = isDark ? 0.2 : DEFAULT_RANGE_OPACITY;
|
|
263
|
-
return {
|
|
264
|
-
type: "range",
|
|
265
|
-
id: annotation.id,
|
|
266
|
-
rect,
|
|
267
|
-
label,
|
|
268
|
-
fill: annotation.fill ?? DEFAULT_RANGE_FILL,
|
|
269
|
-
opacity: annotation.opacity ?? defaultOpacity,
|
|
270
|
-
stroke: annotation.stroke,
|
|
271
|
-
zIndex: annotation.zIndex
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
function resolveRefLineAnnotation(annotation, scales, chartArea, isDark) {
|
|
275
|
-
let start;
|
|
276
|
-
let end;
|
|
277
|
-
if (annotation.y !== void 0) {
|
|
278
|
-
const yPx = resolvePosition(annotation.y, scales.y);
|
|
279
|
-
if (yPx === null) return null;
|
|
280
|
-
start = { x: chartArea.x, y: yPx };
|
|
281
|
-
end = { x: chartArea.x + chartArea.width, y: yPx };
|
|
282
|
-
} else if (annotation.x !== void 0) {
|
|
283
|
-
const xPx = resolvePosition(annotation.x, scales.x);
|
|
284
|
-
if (xPx === null) return null;
|
|
285
|
-
start = { x: xPx, y: chartArea.y };
|
|
286
|
-
end = { x: xPx, y: chartArea.y + chartArea.height };
|
|
287
|
-
} else {
|
|
288
|
-
return null;
|
|
289
|
-
}
|
|
290
|
-
let strokeDasharray;
|
|
291
|
-
if (annotation.style === "dashed" || annotation.style === void 0) {
|
|
292
|
-
strokeDasharray = DEFAULT_REFLINE_DASH;
|
|
293
|
-
} else if (annotation.style === "dotted") {
|
|
294
|
-
strokeDasharray = "2 2";
|
|
295
|
-
}
|
|
296
|
-
let label;
|
|
297
|
-
if (annotation.label) {
|
|
298
|
-
const isHorizontal = annotation.y !== void 0;
|
|
299
|
-
const anchor = annotation.labelAnchor ?? (isHorizontal ? "top" : "left");
|
|
300
|
-
let baseDx;
|
|
301
|
-
let baseDy;
|
|
302
|
-
let labelX;
|
|
303
|
-
let labelY;
|
|
304
|
-
let textAnchor;
|
|
305
|
-
if (isHorizontal) {
|
|
306
|
-
if (anchor === "left") {
|
|
307
|
-
baseDx = 4;
|
|
308
|
-
baseDy = -4;
|
|
309
|
-
labelX = start.x;
|
|
310
|
-
labelY = start.y;
|
|
311
|
-
textAnchor = "start";
|
|
312
|
-
} else if (anchor === "bottom") {
|
|
313
|
-
baseDx = -4;
|
|
314
|
-
baseDy = 14;
|
|
315
|
-
labelX = end.x;
|
|
316
|
-
labelY = end.y;
|
|
317
|
-
textAnchor = "end";
|
|
318
|
-
} else {
|
|
319
|
-
baseDx = -4;
|
|
320
|
-
baseDy = -4;
|
|
321
|
-
labelX = end.x;
|
|
322
|
-
labelY = end.y;
|
|
323
|
-
textAnchor = "end";
|
|
324
|
-
}
|
|
325
|
-
} else {
|
|
326
|
-
if (anchor === "right") {
|
|
327
|
-
baseDx = -4;
|
|
328
|
-
baseDy = 14;
|
|
329
|
-
labelX = start.x;
|
|
330
|
-
labelY = start.y;
|
|
331
|
-
textAnchor = "end";
|
|
332
|
-
} else if (anchor === "bottom") {
|
|
333
|
-
baseDx = 4;
|
|
334
|
-
baseDy = -4;
|
|
335
|
-
labelX = start.x;
|
|
336
|
-
labelY = end.y;
|
|
337
|
-
textAnchor = "start";
|
|
338
|
-
} else {
|
|
339
|
-
baseDx = 4;
|
|
340
|
-
baseDy = 14;
|
|
341
|
-
labelX = start.x;
|
|
342
|
-
labelY = start.y;
|
|
343
|
-
textAnchor = "start";
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
const labelDelta = applyOffset({ dx: baseDx, dy: baseDy }, annotation.labelOffset);
|
|
347
|
-
const defaultStroke2 = isDark ? DARK_REFLINE_STROKE : LIGHT_REFLINE_STROKE;
|
|
348
|
-
const style = makeAnnotationLabelStyle(11, 400, annotation.stroke ?? defaultStroke2, isDark);
|
|
349
|
-
style.textAnchor = textAnchor;
|
|
350
|
-
label = {
|
|
351
|
-
text: annotation.label,
|
|
352
|
-
x: labelX + labelDelta.dx,
|
|
353
|
-
y: labelY + labelDelta.dy,
|
|
354
|
-
style,
|
|
355
|
-
visible: true
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
const defaultStroke = isDark ? DARK_REFLINE_STROKE : LIGHT_REFLINE_STROKE;
|
|
359
|
-
return {
|
|
360
|
-
type: "refline",
|
|
361
|
-
id: annotation.id,
|
|
362
|
-
line: { start, end },
|
|
363
|
-
label,
|
|
364
|
-
stroke: annotation.stroke ?? defaultStroke,
|
|
365
|
-
strokeDasharray,
|
|
366
|
-
strokeWidth: annotation.strokeWidth ?? 1,
|
|
367
|
-
zIndex: annotation.zIndex
|
|
368
|
-
};
|
|
369
|
-
}
|
|
370
|
-
function estimateLabelBounds(label) {
|
|
371
|
-
const fontSize = label.style.fontSize ?? DEFAULT_ANNOTATION_FONT_SIZE;
|
|
372
|
-
const fontWeight = label.style.fontWeight ?? DEFAULT_ANNOTATION_FONT_WEIGHT;
|
|
373
|
-
return computeTextBounds(label.x, label.y, label.text, fontSize, fontWeight);
|
|
374
|
-
}
|
|
375
|
-
var NUDGE_PADDING = 6;
|
|
376
|
-
function generateNudgeCandidates(selfBounds, obstacles, padding) {
|
|
377
|
-
const candidates = [];
|
|
378
|
-
for (const obs of obstacles) {
|
|
379
|
-
const belowDy = obs.y + obs.height + padding - selfBounds.y;
|
|
380
|
-
candidates.push({ dx: 0, dy: belowDy, distance: Math.abs(belowDy) });
|
|
381
|
-
const aboveDy = obs.y - padding - (selfBounds.y + selfBounds.height);
|
|
382
|
-
candidates.push({ dx: 0, dy: aboveDy, distance: Math.abs(aboveDy) });
|
|
383
|
-
const leftDx = obs.x - padding - (selfBounds.x + selfBounds.width);
|
|
384
|
-
candidates.push({ dx: leftDx, dy: 0, distance: Math.abs(leftDx) });
|
|
385
|
-
const rightDx = obs.x + obs.width + padding - selfBounds.x;
|
|
386
|
-
candidates.push({ dx: rightDx, dy: 0, distance: Math.abs(rightDx) });
|
|
387
|
-
}
|
|
388
|
-
candidates.sort((a, b) => a.distance - b.distance);
|
|
389
|
-
return candidates;
|
|
390
|
-
}
|
|
391
|
-
function nudgeAnnotationFromObstacles(annotation, originalAnnotation, scales, chartArea, obstacles) {
|
|
392
|
-
if (annotation.type !== "text" || !annotation.label) return false;
|
|
393
|
-
const labelBounds = estimateLabelBounds(annotation.label);
|
|
394
|
-
const collidingObs = obstacles.filter(
|
|
395
|
-
(obs) => obs.width > 0 && obs.height > 0 && detectCollision(labelBounds, obs)
|
|
396
|
-
);
|
|
397
|
-
if (collidingObs.length === 0) return false;
|
|
398
|
-
const px = resolvePosition(originalAnnotation.x, scales.x);
|
|
399
|
-
const py = resolvePosition(originalAnnotation.y, scales.y);
|
|
400
|
-
if (px === null || py === null) return false;
|
|
401
|
-
const candidates = generateNudgeCandidates(labelBounds, collidingObs, NUDGE_PADDING);
|
|
402
|
-
const fontSize = labelBounds.height / Math.max(1, annotation.label.text.split("\n").length);
|
|
403
|
-
for (const { dx, dy } of candidates) {
|
|
404
|
-
const newLabelX = annotation.label.x + dx;
|
|
405
|
-
const newLabelY = annotation.label.y + dy;
|
|
406
|
-
let newConnector = annotation.label.connector;
|
|
407
|
-
if (newConnector) {
|
|
408
|
-
const annFontSize = annotation.label.style.fontSize ?? DEFAULT_ANNOTATION_FONT_SIZE;
|
|
409
|
-
const annFontWeight = annotation.label.style.fontWeight ?? DEFAULT_ANNOTATION_FONT_WEIGHT;
|
|
410
|
-
const connStyle = newConnector.style === "curve" ? "curve" : "straight";
|
|
411
|
-
const newFrom = computeConnectorOrigin(
|
|
412
|
-
newLabelX,
|
|
413
|
-
newLabelY,
|
|
414
|
-
annotation.label.text,
|
|
415
|
-
annFontSize,
|
|
416
|
-
annFontWeight,
|
|
417
|
-
px,
|
|
418
|
-
py,
|
|
419
|
-
connStyle
|
|
420
|
-
);
|
|
421
|
-
newConnector = { ...newConnector, from: newFrom };
|
|
422
|
-
}
|
|
423
|
-
const candidateLabel = {
|
|
424
|
-
...annotation.label,
|
|
425
|
-
x: newLabelX,
|
|
426
|
-
y: newLabelY,
|
|
427
|
-
connector: newConnector
|
|
428
|
-
};
|
|
429
|
-
const candidateBounds = estimateLabelBounds(candidateLabel);
|
|
430
|
-
const stillCollides = obstacles.some(
|
|
431
|
-
(obs) => obs.width > 0 && obs.height > 0 && detectCollision(candidateBounds, obs)
|
|
432
|
-
);
|
|
433
|
-
if (stillCollides) continue;
|
|
434
|
-
const labelCenterX = candidateBounds.x + candidateBounds.width / 2;
|
|
435
|
-
const labelCenterY = candidateBounds.y + candidateBounds.height / 2;
|
|
436
|
-
const inBounds = labelCenterX >= chartArea.x && labelCenterX <= chartArea.x + chartArea.width + 10 && labelCenterY >= chartArea.y - fontSize && labelCenterY <= chartArea.y + chartArea.height + fontSize * 3;
|
|
437
|
-
if (inBounds) {
|
|
438
|
-
annotation.label = candidateLabel;
|
|
439
|
-
return true;
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
return false;
|
|
443
|
-
}
|
|
444
|
-
function resolveAnnotationCollisions(annotations, originalSpecs, scales, chartArea) {
|
|
445
|
-
const placedBounds = [];
|
|
446
|
-
for (let i = 0; i < annotations.length; i++) {
|
|
447
|
-
const annotation = annotations[i];
|
|
448
|
-
if (annotation.type !== "text" || !annotation.label) {
|
|
449
|
-
continue;
|
|
450
|
-
}
|
|
451
|
-
const bounds = estimateLabelBounds(annotation.label);
|
|
452
|
-
const collidingBounds = placedBounds.filter(
|
|
453
|
-
(pb) => pb.width > 0 && pb.height > 0 && detectCollision(bounds, pb)
|
|
454
|
-
);
|
|
455
|
-
if (collidingBounds.length > 0) {
|
|
456
|
-
const originalSpec = originalSpecs[i];
|
|
457
|
-
if (originalSpec?.type === "text") {
|
|
458
|
-
const px = resolvePosition(originalSpec.x, scales.x);
|
|
459
|
-
const py = resolvePosition(originalSpec.y, scales.y);
|
|
460
|
-
if (px !== null && py !== null) {
|
|
461
|
-
const candidates = generateNudgeCandidates(bounds, collidingBounds, NUDGE_PADDING);
|
|
462
|
-
const fontSize = bounds.height / Math.max(1, annotation.label.text.split("\n").length);
|
|
463
|
-
for (const { dx, dy } of candidates) {
|
|
464
|
-
const newLabelX = annotation.label.x + dx;
|
|
465
|
-
const newLabelY = annotation.label.y + dy;
|
|
466
|
-
const candidateLabel = {
|
|
467
|
-
...annotation.label,
|
|
468
|
-
x: newLabelX,
|
|
469
|
-
y: newLabelY
|
|
470
|
-
};
|
|
471
|
-
const candidateBounds = estimateLabelBounds(candidateLabel);
|
|
472
|
-
const stillCollides = placedBounds.some(
|
|
473
|
-
(pb) => pb.width > 0 && pb.height > 0 && detectCollision(candidateBounds, pb)
|
|
474
|
-
);
|
|
475
|
-
if (stillCollides) continue;
|
|
476
|
-
const labelCenterX = candidateBounds.x + candidateBounds.width / 2;
|
|
477
|
-
const labelCenterY = candidateBounds.y + candidateBounds.height / 2;
|
|
478
|
-
const inBounds = labelCenterX >= chartArea.x && labelCenterX <= chartArea.x + chartArea.width + 10 && labelCenterY >= chartArea.y - fontSize && labelCenterY <= chartArea.y + chartArea.height + fontSize;
|
|
479
|
-
if (inBounds) {
|
|
480
|
-
let newConnector = annotation.label.connector;
|
|
481
|
-
if (newConnector) {
|
|
482
|
-
const annFontSize = annotation.label.style.fontSize ?? DEFAULT_ANNOTATION_FONT_SIZE;
|
|
483
|
-
const annFontWeight = annotation.label.style.fontWeight ?? DEFAULT_ANNOTATION_FONT_WEIGHT;
|
|
484
|
-
const connStyle = newConnector.style === "curve" ? "curve" : "straight";
|
|
485
|
-
const newFrom = computeConnectorOrigin(
|
|
486
|
-
newLabelX,
|
|
487
|
-
newLabelY,
|
|
488
|
-
annotation.label.text,
|
|
489
|
-
annFontSize,
|
|
490
|
-
annFontWeight,
|
|
491
|
-
px,
|
|
492
|
-
py,
|
|
493
|
-
connStyle
|
|
494
|
-
);
|
|
495
|
-
newConnector = { ...newConnector, from: newFrom };
|
|
496
|
-
}
|
|
497
|
-
annotation.label = {
|
|
498
|
-
...annotation.label,
|
|
499
|
-
x: newLabelX,
|
|
500
|
-
y: newLabelY,
|
|
501
|
-
connector: newConnector
|
|
502
|
-
};
|
|
503
|
-
break;
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
placedBounds.push(estimateLabelBounds(annotation.label));
|
|
433
|
+
text: annotation.label,
|
|
434
|
+
x: baseX + labelDelta.dx,
|
|
435
|
+
y: y2 + labelDelta.dy,
|
|
436
|
+
style,
|
|
437
|
+
visible: true
|
|
438
|
+
};
|
|
510
439
|
}
|
|
440
|
+
const defaultOpacity = isDark ? 0.2 : DEFAULT_RANGE_OPACITY;
|
|
441
|
+
return {
|
|
442
|
+
type: "range",
|
|
443
|
+
id: annotation.id,
|
|
444
|
+
rect,
|
|
445
|
+
label,
|
|
446
|
+
fill: annotation.fill ?? DEFAULT_RANGE_FILL,
|
|
447
|
+
opacity: annotation.opacity ?? defaultOpacity,
|
|
448
|
+
stroke: annotation.stroke,
|
|
449
|
+
zIndex: annotation.zIndex
|
|
450
|
+
};
|
|
511
451
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
if (
|
|
520
|
-
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
452
|
+
|
|
453
|
+
// src/annotations/resolve-refline.ts
|
|
454
|
+
function resolveRefLineAnnotation(annotation, scales, chartArea, isDark) {
|
|
455
|
+
let start;
|
|
456
|
+
let end;
|
|
457
|
+
if (annotation.y !== void 0) {
|
|
458
|
+
const yPx = resolvePosition(annotation.y, scales.y);
|
|
459
|
+
if (yPx === null) return null;
|
|
460
|
+
start = { x: chartArea.x, y: yPx };
|
|
461
|
+
end = { x: chartArea.x + chartArea.width, y: yPx };
|
|
462
|
+
} else if (annotation.x !== void 0) {
|
|
463
|
+
const xPx = resolvePosition(annotation.x, scales.x);
|
|
464
|
+
if (xPx === null) return null;
|
|
465
|
+
start = { x: xPx, y: chartArea.y };
|
|
466
|
+
end = { x: xPx, y: chartArea.y + chartArea.height };
|
|
467
|
+
} else {
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
let strokeDasharray;
|
|
471
|
+
if (annotation.style === "dashed" || annotation.style === void 0) {
|
|
472
|
+
strokeDasharray = DEFAULT_REFLINE_DASH;
|
|
473
|
+
} else if (annotation.style === "dotted") {
|
|
474
|
+
strokeDasharray = "2 2";
|
|
475
|
+
}
|
|
476
|
+
let label;
|
|
477
|
+
if (annotation.label) {
|
|
478
|
+
const isHorizontal = annotation.y !== void 0;
|
|
479
|
+
const anchor = annotation.labelAnchor ?? (isHorizontal ? "top" : "left");
|
|
480
|
+
let baseDx;
|
|
481
|
+
let baseDy;
|
|
482
|
+
let labelX;
|
|
483
|
+
let labelY;
|
|
484
|
+
let textAnchor;
|
|
485
|
+
if (isHorizontal) {
|
|
486
|
+
if (anchor === "left") {
|
|
487
|
+
baseDx = 4;
|
|
488
|
+
baseDy = -4;
|
|
489
|
+
labelX = start.x;
|
|
490
|
+
labelY = start.y;
|
|
491
|
+
textAnchor = "start";
|
|
492
|
+
} else if (anchor === "bottom") {
|
|
493
|
+
baseDx = -4;
|
|
494
|
+
baseDy = 14;
|
|
495
|
+
labelX = end.x;
|
|
496
|
+
labelY = end.y;
|
|
497
|
+
textAnchor = "end";
|
|
498
|
+
} else {
|
|
499
|
+
baseDx = -4;
|
|
500
|
+
baseDy = -4;
|
|
501
|
+
labelX = end.x;
|
|
502
|
+
labelY = end.y;
|
|
503
|
+
textAnchor = "end";
|
|
504
|
+
}
|
|
505
|
+
} else {
|
|
506
|
+
if (anchor === "right") {
|
|
507
|
+
baseDx = -4;
|
|
508
|
+
baseDy = 14;
|
|
509
|
+
labelX = start.x;
|
|
510
|
+
labelY = start.y;
|
|
511
|
+
textAnchor = "end";
|
|
512
|
+
} else if (anchor === "bottom") {
|
|
513
|
+
baseDx = 4;
|
|
514
|
+
baseDy = -4;
|
|
515
|
+
labelX = start.x;
|
|
516
|
+
labelY = end.y;
|
|
517
|
+
textAnchor = "start";
|
|
518
|
+
} else {
|
|
519
|
+
baseDx = 4;
|
|
520
|
+
baseDy = 14;
|
|
521
|
+
labelX = start.x;
|
|
522
|
+
labelY = start.y;
|
|
523
|
+
textAnchor = "start";
|
|
524
|
+
}
|
|
550
525
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
526
|
+
const labelDelta = applyOffset({ dx: baseDx, dy: baseDy }, annotation.labelOffset);
|
|
527
|
+
const defaultStroke2 = isDark ? DARK_REFLINE_STROKE : LIGHT_REFLINE_STROKE;
|
|
528
|
+
const style = makeAnnotationLabelStyle(11, 400, annotation.stroke ?? defaultStroke2, isDark);
|
|
529
|
+
style.textAnchor = textAnchor;
|
|
530
|
+
label = {
|
|
531
|
+
text: annotation.label,
|
|
532
|
+
x: labelX + labelDelta.dx,
|
|
533
|
+
y: labelY + labelDelta.dy,
|
|
534
|
+
style,
|
|
535
|
+
visible: true
|
|
556
536
|
};
|
|
557
537
|
}
|
|
538
|
+
const defaultStroke = isDark ? DARK_REFLINE_STROKE : LIGHT_REFLINE_STROKE;
|
|
539
|
+
return {
|
|
540
|
+
type: "refline",
|
|
541
|
+
id: annotation.id,
|
|
542
|
+
line: { start, end },
|
|
543
|
+
label,
|
|
544
|
+
stroke: annotation.stroke ?? defaultStroke,
|
|
545
|
+
strokeDasharray,
|
|
546
|
+
strokeWidth: annotation.strokeWidth ?? 1,
|
|
547
|
+
zIndex: annotation.zIndex
|
|
548
|
+
};
|
|
558
549
|
}
|
|
550
|
+
|
|
551
|
+
// src/annotations/compute.ts
|
|
559
552
|
function computeAnnotations(spec, scales, chartArea, strategy, isDark = false, obstacles = [], svgDimensions) {
|
|
560
553
|
if (strategy.annotationPosition === "tooltip-only") {
|
|
561
554
|
return [];
|
|
@@ -788,6 +781,7 @@ function computeBarMarks(spec, scales, _chartArea, _strategy) {
|
|
|
788
781
|
scales
|
|
789
782
|
);
|
|
790
783
|
}
|
|
784
|
+
const stackMode = xChannel.stack === "normalize" ? "normalize" : xChannel.stack === "center" ? "center" : "zero";
|
|
791
785
|
return computeStackedBars(
|
|
792
786
|
spec.data,
|
|
793
787
|
xChannel.field,
|
|
@@ -797,7 +791,8 @@ function computeBarMarks(spec, scales, _chartArea, _strategy) {
|
|
|
797
791
|
yScale,
|
|
798
792
|
bandwidth,
|
|
799
793
|
baseline,
|
|
800
|
-
scales
|
|
794
|
+
scales,
|
|
795
|
+
stackMode
|
|
801
796
|
);
|
|
802
797
|
}
|
|
803
798
|
return computeColoredBars(
|
|
@@ -812,27 +807,33 @@ function computeBarMarks(spec, scales, _chartArea, _strategy) {
|
|
|
812
807
|
scales
|
|
813
808
|
);
|
|
814
809
|
}
|
|
815
|
-
function computeStackedBars(data, valueField, categoryField, colorField, xScale, yScale, bandwidth, _baseline, scales) {
|
|
810
|
+
function computeStackedBars(data, valueField, categoryField, colorField, xScale, yScale, bandwidth, _baseline, scales, stackMode = "zero") {
|
|
816
811
|
const marks = [];
|
|
817
812
|
const categoryGroups = groupByField(data, categoryField);
|
|
818
813
|
for (const [category, rows] of categoryGroups) {
|
|
819
814
|
const bandY = yScale(category);
|
|
820
815
|
if (bandY === void 0) continue;
|
|
821
|
-
let
|
|
816
|
+
let categoryTotal = 0;
|
|
822
817
|
for (const row of rows) {
|
|
823
|
-
const
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
818
|
+
const v = Number(row[valueField] ?? 0);
|
|
819
|
+
if (Number.isFinite(v) && v > 0) categoryTotal += v;
|
|
820
|
+
}
|
|
821
|
+
let cumulativeValue = stackMode === "center" ? -categoryTotal / 2 : 0;
|
|
822
|
+
for (const row of rows) {
|
|
823
|
+
const groupKey2 = String(row[colorField] ?? "");
|
|
824
|
+
const rawValue = Number(row[valueField] ?? 0);
|
|
825
|
+
if (!Number.isFinite(rawValue) || rawValue <= 0) continue;
|
|
826
|
+
const value2 = stackMode === "normalize" && categoryTotal > 0 ? rawValue / categoryTotal : rawValue;
|
|
827
|
+
const color2 = getColor(scales, groupKey2);
|
|
827
828
|
const xLeft = xScale(cumulativeValue);
|
|
828
829
|
const xRight = xScale(cumulativeValue + value2);
|
|
829
830
|
const barWidth = Math.max(Math.abs(xRight - xLeft), MIN_BAR_WIDTH);
|
|
830
831
|
const aria = {
|
|
831
|
-
label: `${category}, ${
|
|
832
|
+
label: `${category}, ${groupKey2}: ${formatBarValue(rawValue)}`
|
|
832
833
|
};
|
|
833
834
|
marks.push({
|
|
834
835
|
type: "rect",
|
|
835
|
-
x: xLeft,
|
|
836
|
+
x: Math.min(xLeft, xRight),
|
|
836
837
|
y: bandY,
|
|
837
838
|
width: barWidth,
|
|
838
839
|
height: bandwidth,
|
|
@@ -866,16 +867,16 @@ function computeGroupedBars(data, valueField, categoryField, colorField, xScale,
|
|
|
866
867
|
const bandY = yScale(category);
|
|
867
868
|
if (bandY === void 0) continue;
|
|
868
869
|
for (const row of rows) {
|
|
869
|
-
const
|
|
870
|
+
const groupKey2 = String(row[colorField] ?? "");
|
|
870
871
|
const value2 = Number(row[valueField] ?? 0);
|
|
871
872
|
if (!Number.isFinite(value2)) continue;
|
|
872
|
-
const groupIndex = groupIndexMap.get(
|
|
873
|
-
const color2 = getColor(scales,
|
|
873
|
+
const groupIndex = groupIndexMap.get(groupKey2) ?? 0;
|
|
874
|
+
const color2 = getColor(scales, groupKey2);
|
|
874
875
|
const xPos = value2 >= 0 ? baseline : xScale(value2);
|
|
875
876
|
const barWidth = Math.max(Math.abs(xScale(value2) - baseline), MIN_BAR_WIDTH);
|
|
876
877
|
const subY = bandY + groupIndex * (subBandHeight + gap);
|
|
877
878
|
const aria = {
|
|
878
|
-
label: `${category}, ${
|
|
879
|
+
label: `${category}, ${groupKey2}: ${formatBarValue(value2)}`
|
|
879
880
|
};
|
|
880
881
|
marks.push({
|
|
881
882
|
type: "rect",
|
|
@@ -901,12 +902,12 @@ function computeColoredBars(data, valueField, categoryField, colorField, xScale,
|
|
|
901
902
|
if (!Number.isFinite(value2)) continue;
|
|
902
903
|
const bandY = yScale(category);
|
|
903
904
|
if (bandY === void 0) continue;
|
|
904
|
-
const
|
|
905
|
-
const color2 = getColor(scales,
|
|
905
|
+
const groupKey2 = String(row[colorField] ?? "");
|
|
906
|
+
const color2 = getColor(scales, groupKey2);
|
|
906
907
|
const xPos = value2 >= 0 ? baseline : xScale(value2);
|
|
907
908
|
const barWidth = Math.max(Math.abs(xScale(value2) - baseline), MIN_BAR_WIDTH);
|
|
908
909
|
const aria = {
|
|
909
|
-
label: `${category}, ${
|
|
910
|
+
label: `${category}, ${groupKey2}: ${formatBarValue(value2)}`
|
|
910
911
|
};
|
|
911
912
|
marks.push({
|
|
912
913
|
type: "rect",
|
|
@@ -979,7 +980,7 @@ var SUFFIX_MULTIPLIERS = {
|
|
|
979
980
|
T: 1e12
|
|
980
981
|
};
|
|
981
982
|
function parseDisplayNumber(raw) {
|
|
982
|
-
const trimmed = raw.trim();
|
|
983
|
+
const trimmed = raw.trim().replace(/\u2212/g, "-");
|
|
983
984
|
if (!trimmed) return NaN;
|
|
984
985
|
const last = trimmed[trimmed.length - 1].toUpperCase();
|
|
985
986
|
const multiplier = SUFFIX_MULTIPLIERS[last];
|
|
@@ -988,6 +989,9 @@ function parseDisplayNumber(raw) {
|
|
|
988
989
|
const n = Number(numPart);
|
|
989
990
|
return Number.isNaN(n) ? NaN : n * multiplier;
|
|
990
991
|
}
|
|
992
|
+
if (last === "%") {
|
|
993
|
+
return Number(trimmed.slice(0, -1).replace(/,/g, ""));
|
|
994
|
+
}
|
|
991
995
|
return Number(trimmed.replace(/,/g, ""));
|
|
992
996
|
}
|
|
993
997
|
var LABEL_FONT_SIZE = 11;
|
|
@@ -1149,6 +1153,7 @@ function computeColumnMarks(spec, scales, _chartArea, _strategy) {
|
|
|
1149
1153
|
scales
|
|
1150
1154
|
);
|
|
1151
1155
|
}
|
|
1156
|
+
const stackMode = yChannel.stack === "normalize" ? "normalize" : yChannel.stack === "center" ? "center" : "zero";
|
|
1152
1157
|
return computeStackedColumns(
|
|
1153
1158
|
spec.data,
|
|
1154
1159
|
xChannel.field,
|
|
@@ -1158,7 +1163,8 @@ function computeColumnMarks(spec, scales, _chartArea, _strategy) {
|
|
|
1158
1163
|
yScale,
|
|
1159
1164
|
bandwidth,
|
|
1160
1165
|
baseline,
|
|
1161
|
-
scales
|
|
1166
|
+
scales,
|
|
1167
|
+
stackMode
|
|
1162
1168
|
);
|
|
1163
1169
|
}
|
|
1164
1170
|
return computeColoredColumns(
|
|
@@ -1236,13 +1242,13 @@ function computeColoredColumns(data, categoryField, valueField, colorField, xSca
|
|
|
1236
1242
|
if (!Number.isFinite(value2)) continue;
|
|
1237
1243
|
const bandX = xScale(category);
|
|
1238
1244
|
if (bandX === void 0) continue;
|
|
1239
|
-
const
|
|
1240
|
-
const color2 = getColor(scales,
|
|
1245
|
+
const groupKey2 = String(row[colorField] ?? "");
|
|
1246
|
+
const color2 = getColor(scales, groupKey2);
|
|
1241
1247
|
const yPos = yScale(value2);
|
|
1242
1248
|
const columnHeight = Math.max(Math.abs(baseline - yPos), MIN_COLUMN_HEIGHT);
|
|
1243
1249
|
const y2 = value2 >= 0 ? yPos : baseline;
|
|
1244
1250
|
const aria = {
|
|
1245
|
-
label: `${category}, ${
|
|
1251
|
+
label: `${category}, ${groupKey2}: ${formatColumnValue(value2)}`
|
|
1246
1252
|
};
|
|
1247
1253
|
marks.push({
|
|
1248
1254
|
type: "rect",
|
|
@@ -1280,17 +1286,17 @@ function computeGroupedColumns(data, categoryField, valueField, colorField, xSca
|
|
|
1280
1286
|
const bandX = xScale(category);
|
|
1281
1287
|
if (bandX === void 0) continue;
|
|
1282
1288
|
for (const row of rows) {
|
|
1283
|
-
const
|
|
1289
|
+
const groupKey2 = String(row[colorField] ?? "");
|
|
1284
1290
|
const value2 = Number(row[valueField] ?? 0);
|
|
1285
1291
|
if (!Number.isFinite(value2)) continue;
|
|
1286
|
-
const groupIndex = groupIndexMap.get(
|
|
1287
|
-
const color2 = getColor(scales,
|
|
1292
|
+
const groupIndex = groupIndexMap.get(groupKey2) ?? 0;
|
|
1293
|
+
const color2 = getColor(scales, groupKey2);
|
|
1288
1294
|
const yPos = yScale(value2);
|
|
1289
1295
|
const columnHeight = Math.max(Math.abs(baseline - yPos), MIN_COLUMN_HEIGHT);
|
|
1290
1296
|
const y2 = value2 >= 0 ? yPos : baseline;
|
|
1291
1297
|
const subX = bandX + groupIndex * (subBandWidth + gap);
|
|
1292
1298
|
const aria = {
|
|
1293
|
-
label: `${category}, ${
|
|
1299
|
+
label: `${category}, ${groupKey2}: ${formatColumnValue(value2)}`
|
|
1294
1300
|
};
|
|
1295
1301
|
marks.push({
|
|
1296
1302
|
type: "rect",
|
|
@@ -1308,28 +1314,34 @@ function computeGroupedColumns(data, categoryField, valueField, colorField, xSca
|
|
|
1308
1314
|
}
|
|
1309
1315
|
return marks;
|
|
1310
1316
|
}
|
|
1311
|
-
function computeStackedColumns(data, categoryField, valueField, colorField, xScale, yScale, bandwidth, _baseline, scales) {
|
|
1317
|
+
function computeStackedColumns(data, categoryField, valueField, colorField, xScale, yScale, bandwidth, _baseline, scales, stackMode = "zero") {
|
|
1312
1318
|
const marks = [];
|
|
1313
1319
|
const categoryGroups = groupByField(data, categoryField);
|
|
1314
1320
|
for (const [category, rows] of categoryGroups) {
|
|
1315
1321
|
const bandX = xScale(category);
|
|
1316
1322
|
if (bandX === void 0) continue;
|
|
1317
|
-
let
|
|
1323
|
+
let categoryTotal = 0;
|
|
1318
1324
|
for (const row of rows) {
|
|
1319
|
-
const
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1325
|
+
const v = Number(row[valueField] ?? 0);
|
|
1326
|
+
if (Number.isFinite(v) && v > 0) categoryTotal += v;
|
|
1327
|
+
}
|
|
1328
|
+
let cumulativeValue = stackMode === "center" ? -categoryTotal / 2 : 0;
|
|
1329
|
+
for (const row of rows) {
|
|
1330
|
+
const groupKey2 = String(row[colorField] ?? "");
|
|
1331
|
+
const rawValue = Number(row[valueField] ?? 0);
|
|
1332
|
+
if (!Number.isFinite(rawValue) || rawValue <= 0) continue;
|
|
1333
|
+
const value2 = stackMode === "normalize" && categoryTotal > 0 ? rawValue / categoryTotal : rawValue;
|
|
1334
|
+
const color2 = getColor(scales, groupKey2);
|
|
1323
1335
|
const yTop = yScale(cumulativeValue + value2);
|
|
1324
1336
|
const yBottom = yScale(cumulativeValue);
|
|
1325
1337
|
const columnHeight = Math.max(Math.abs(yBottom - yTop), MIN_COLUMN_HEIGHT);
|
|
1326
1338
|
const aria = {
|
|
1327
|
-
label: `${category}, ${
|
|
1339
|
+
label: `${category}, ${groupKey2}: ${formatColumnValue(rawValue)}`
|
|
1328
1340
|
};
|
|
1329
1341
|
marks.push({
|
|
1330
1342
|
type: "rect",
|
|
1331
1343
|
x: bandX,
|
|
1332
|
-
y: yTop,
|
|
1344
|
+
y: Math.min(yTop, yBottom),
|
|
1333
1345
|
width: bandwidth,
|
|
1334
1346
|
height: columnHeight,
|
|
1335
1347
|
fill: color2,
|
|
@@ -2521,6 +2533,26 @@ function stack_default() {
|
|
|
2521
2533
|
return stack;
|
|
2522
2534
|
}
|
|
2523
2535
|
|
|
2536
|
+
// ../../node_modules/.bun/d3-shape@3.2.0/node_modules/d3-shape/src/offset/expand.js
|
|
2537
|
+
function expand_default(series, order) {
|
|
2538
|
+
if (!((n = series.length) > 0)) return;
|
|
2539
|
+
for (var i, n, j = 0, m = series[0].length, y2; j < m; ++j) {
|
|
2540
|
+
for (y2 = i = 0; i < n; ++i) y2 += series[i][j][1] || 0;
|
|
2541
|
+
if (y2) for (i = 0; i < n; ++i) series[i][j][1] /= y2;
|
|
2542
|
+
}
|
|
2543
|
+
none_default(series, order);
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
// ../../node_modules/.bun/d3-shape@3.2.0/node_modules/d3-shape/src/offset/silhouette.js
|
|
2547
|
+
function silhouette_default(series, order) {
|
|
2548
|
+
if (!((n = series.length) > 0)) return;
|
|
2549
|
+
for (var j = 0, s0 = series[order[0]], n, m = s0.length; j < m; ++j) {
|
|
2550
|
+
for (var i = 0, y2 = 0; i < n; ++i) y2 += series[i][j][1] || 0;
|
|
2551
|
+
s0[j][1] += s0[j][0] = -y2 / 2;
|
|
2552
|
+
}
|
|
2553
|
+
none_default(series, order);
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2524
2556
|
// src/charts/line/curves.ts
|
|
2525
2557
|
var CURVE_MAP = {
|
|
2526
2558
|
linear: linear_default,
|
|
@@ -2648,7 +2680,9 @@ function computeStackedArea(spec, scales, chartArea) {
|
|
|
2648
2680
|
}
|
|
2649
2681
|
return pivot;
|
|
2650
2682
|
});
|
|
2651
|
-
const
|
|
2683
|
+
const stackProp = yChannel.stack;
|
|
2684
|
+
const offsetFn = stackProp === "normalize" ? expand_default : stackProp === "center" ? silhouette_default : none_default;
|
|
2685
|
+
const stackGenerator = stack_default().keys(keys).order(none_default2).offset(offsetFn);
|
|
2652
2686
|
const stackedData = stackGenerator(pivotData);
|
|
2653
2687
|
const yScale = scales.y.scale;
|
|
2654
2688
|
const marks = [];
|
|
@@ -3102,53 +3136,185 @@ function computePieLabels(marks, _chartArea, density = "auto", _textFill = "#333
|
|
|
3102
3136
|
dominantBaseline: "central"
|
|
3103
3137
|
}
|
|
3104
3138
|
});
|
|
3105
|
-
targetMarkIndices.push(mi);
|
|
3106
|
-
}
|
|
3107
|
-
if (candidates.length === 0) return [];
|
|
3108
|
-
let resolved;
|
|
3109
|
-
if (density === "all") {
|
|
3110
|
-
resolved = candidates.map((c) => ({
|
|
3111
|
-
text: c.text,
|
|
3112
|
-
x: c.anchorX,
|
|
3113
|
-
y: c.anchorY,
|
|
3114
|
-
style: c.style,
|
|
3115
|
-
visible: true
|
|
3116
|
-
}));
|
|
3117
|
-
} else {
|
|
3118
|
-
resolved = resolveCollisions5(candidates);
|
|
3139
|
+
targetMarkIndices.push(mi);
|
|
3140
|
+
}
|
|
3141
|
+
if (candidates.length === 0) return [];
|
|
3142
|
+
let resolved;
|
|
3143
|
+
if (density === "all") {
|
|
3144
|
+
resolved = candidates.map((c) => ({
|
|
3145
|
+
text: c.text,
|
|
3146
|
+
x: c.anchorX,
|
|
3147
|
+
y: c.anchorY,
|
|
3148
|
+
style: c.style,
|
|
3149
|
+
visible: true
|
|
3150
|
+
}));
|
|
3151
|
+
} else {
|
|
3152
|
+
resolved = resolveCollisions5(candidates);
|
|
3153
|
+
}
|
|
3154
|
+
for (let i = 0; i < resolved.length && i < targetMarks.length; i++) {
|
|
3155
|
+
const label = resolved[i];
|
|
3156
|
+
const mark = targetMarks[i];
|
|
3157
|
+
if (label.visible) {
|
|
3158
|
+
label.connector = {
|
|
3159
|
+
from: { x: label.x, y: label.y },
|
|
3160
|
+
to: { x: mark.centroid.x, y: mark.centroid.y },
|
|
3161
|
+
stroke: _textFill,
|
|
3162
|
+
style: "straight"
|
|
3163
|
+
};
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
return resolved;
|
|
3167
|
+
}
|
|
3168
|
+
|
|
3169
|
+
// src/charts/pie/index.ts
|
|
3170
|
+
var pieRenderer = (spec, scales, chartArea, strategy, theme) => {
|
|
3171
|
+
const marks = computePieMarks(spec, scales, chartArea, strategy, false);
|
|
3172
|
+
const labels = computePieLabels(marks, chartArea, spec.labels.density, theme.colors.text);
|
|
3173
|
+
for (let i = 0; i < marks.length && i < labels.length; i++) {
|
|
3174
|
+
marks[i].label = labels[i];
|
|
3175
|
+
}
|
|
3176
|
+
return marks;
|
|
3177
|
+
};
|
|
3178
|
+
var donutRenderer = (spec, scales, chartArea, strategy, theme) => {
|
|
3179
|
+
const marks = computePieMarks(spec, scales, chartArea, strategy, true);
|
|
3180
|
+
const labels = computePieLabels(marks, chartArea, spec.labels.density, theme.colors.text);
|
|
3181
|
+
for (let i = 0; i < marks.length && i < labels.length; i++) {
|
|
3182
|
+
marks[i].label = labels[i];
|
|
3183
|
+
}
|
|
3184
|
+
return marks;
|
|
3185
|
+
};
|
|
3186
|
+
|
|
3187
|
+
// src/charts/post-process.ts
|
|
3188
|
+
function computeMarkObstacles(marks, scales) {
|
|
3189
|
+
if (scales.y?.type === "band") {
|
|
3190
|
+
return computeBandRowObstacles(marks, scales);
|
|
3191
|
+
}
|
|
3192
|
+
const obstacles = [];
|
|
3193
|
+
for (const mark of marks) {
|
|
3194
|
+
if (mark.type === "rect") {
|
|
3195
|
+
const rm = mark;
|
|
3196
|
+
obstacles.push({ x: rm.x, y: rm.y, width: rm.width, height: rm.height });
|
|
3197
|
+
} else if (mark.type === "point") {
|
|
3198
|
+
const pm = mark;
|
|
3199
|
+
obstacles.push({
|
|
3200
|
+
x: pm.cx - pm.r,
|
|
3201
|
+
y: pm.cy - pm.r,
|
|
3202
|
+
width: pm.r * 2,
|
|
3203
|
+
height: pm.r * 2
|
|
3204
|
+
});
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
return obstacles;
|
|
3208
|
+
}
|
|
3209
|
+
function computeBandRowObstacles(marks, scales) {
|
|
3210
|
+
const rows = /* @__PURE__ */ new Map();
|
|
3211
|
+
for (const mark of marks) {
|
|
3212
|
+
let cy;
|
|
3213
|
+
let left2;
|
|
3214
|
+
let right2;
|
|
3215
|
+
if (mark.type === "point") {
|
|
3216
|
+
const pm = mark;
|
|
3217
|
+
cy = pm.cy;
|
|
3218
|
+
left2 = pm.cx - pm.r;
|
|
3219
|
+
right2 = pm.cx + pm.r;
|
|
3220
|
+
} else if (mark.type === "rect") {
|
|
3221
|
+
const rm = mark;
|
|
3222
|
+
cy = rm.y + rm.height / 2;
|
|
3223
|
+
left2 = rm.x;
|
|
3224
|
+
right2 = rm.x + rm.width;
|
|
3225
|
+
} else {
|
|
3226
|
+
continue;
|
|
3227
|
+
}
|
|
3228
|
+
const key = Math.round(cy);
|
|
3229
|
+
const existing = rows.get(key);
|
|
3230
|
+
if (existing) {
|
|
3231
|
+
existing.minX = Math.min(existing.minX, left2);
|
|
3232
|
+
existing.maxX = Math.max(existing.maxX, right2);
|
|
3233
|
+
} else {
|
|
3234
|
+
rows.set(key, { minX: left2, maxX: right2, bandY: cy });
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
const bandScale = scales.y.scale;
|
|
3238
|
+
const bandwidth = bandScale.bandwidth?.() ?? 0;
|
|
3239
|
+
if (bandwidth === 0) return [];
|
|
3240
|
+
const obstacles = [];
|
|
3241
|
+
for (const { minX, maxX, bandY } of rows.values()) {
|
|
3242
|
+
obstacles.push({
|
|
3243
|
+
x: minX,
|
|
3244
|
+
y: bandY - bandwidth / 2,
|
|
3245
|
+
width: maxX - minX,
|
|
3246
|
+
height: bandwidth
|
|
3247
|
+
});
|
|
3119
3248
|
}
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3249
|
+
return obstacles;
|
|
3250
|
+
}
|
|
3251
|
+
function resolveRendererKey(markType, encoding, markDef) {
|
|
3252
|
+
if (markType === "bar") {
|
|
3253
|
+
const xType = encoding.x?.type;
|
|
3254
|
+
const yType = encoding.y?.type;
|
|
3255
|
+
const isVertical = (xType === "nominal" || xType === "ordinal" || xType === "temporal") && yType === "quantitative";
|
|
3256
|
+
if (isVertical) {
|
|
3257
|
+
return "bar:vertical";
|
|
3258
|
+
}
|
|
3259
|
+
} else if (markType === "arc") {
|
|
3260
|
+
const innerRadius = markDef.innerRadius;
|
|
3261
|
+
if (innerRadius && innerRadius > 0) {
|
|
3262
|
+
return "arc:donut";
|
|
3130
3263
|
}
|
|
3131
3264
|
}
|
|
3132
|
-
return
|
|
3265
|
+
return markType;
|
|
3133
3266
|
}
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3267
|
+
function getMarkPrimaryValue(mark) {
|
|
3268
|
+
switch (mark.type) {
|
|
3269
|
+
case "rect":
|
|
3270
|
+
return mark.height;
|
|
3271
|
+
// bar height is the primary value encoding
|
|
3272
|
+
case "point":
|
|
3273
|
+
return mark.cy;
|
|
3274
|
+
// y position for scatter
|
|
3275
|
+
case "arc":
|
|
3276
|
+
return mark.endAngle - mark.startAngle;
|
|
3277
|
+
// arc angle extent
|
|
3278
|
+
case "line":
|
|
3279
|
+
case "area":
|
|
3280
|
+
return 0;
|
|
3281
|
+
// series marks don't have individual values
|
|
3282
|
+
default:
|
|
3283
|
+
return 0;
|
|
3141
3284
|
}
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3285
|
+
}
|
|
3286
|
+
function assignAnimationIndices(marks, animation) {
|
|
3287
|
+
if (!animation?.enabled) return;
|
|
3288
|
+
if (animation.staggerOrder === "value") {
|
|
3289
|
+
const indexed = marks.map((m, i) => ({ mark: m, idx: i }));
|
|
3290
|
+
indexed.sort((a, b) => {
|
|
3291
|
+
const av = getMarkPrimaryValue(a.mark);
|
|
3292
|
+
const bv = getMarkPrimaryValue(b.mark);
|
|
3293
|
+
return av - bv;
|
|
3294
|
+
});
|
|
3295
|
+
for (let i = 0; i < indexed.length; i++) {
|
|
3296
|
+
const m = indexed[i].mark;
|
|
3297
|
+
if (m.type === "rect" && m.stackGroup) continue;
|
|
3298
|
+
m.animationIndex = i;
|
|
3299
|
+
}
|
|
3149
3300
|
}
|
|
3150
|
-
|
|
3151
|
-
|
|
3301
|
+
const groupIndexMap = /* @__PURE__ */ new Map();
|
|
3302
|
+
const groupStackPos = /* @__PURE__ */ new Map();
|
|
3303
|
+
let nextGroupIndex = 0;
|
|
3304
|
+
for (const mark of marks) {
|
|
3305
|
+
if (mark.type === "rect" && mark.stackGroup) {
|
|
3306
|
+
const rect = mark;
|
|
3307
|
+
const group = rect.stackGroup;
|
|
3308
|
+
if (!groupIndexMap.has(group)) {
|
|
3309
|
+
groupIndexMap.set(group, nextGroupIndex++);
|
|
3310
|
+
}
|
|
3311
|
+
rect.animationIndex = groupIndexMap.get(group);
|
|
3312
|
+
const pos = groupStackPos.get(group) ?? 0;
|
|
3313
|
+
rect.stackPos = pos;
|
|
3314
|
+
groupStackPos.set(group, pos + 1);
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3152
3318
|
|
|
3153
3319
|
// src/charts/registry.ts
|
|
3154
3320
|
var renderers = /* @__PURE__ */ new Map();
|
|
@@ -6135,7 +6301,7 @@ function normalizeChrome(chrome) {
|
|
|
6135
6301
|
};
|
|
6136
6302
|
}
|
|
6137
6303
|
function inferFieldType(data, field) {
|
|
6138
|
-
const sampleSize = Math.min(
|
|
6304
|
+
const sampleSize = Math.min(50, data.length);
|
|
6139
6305
|
let numericCount = 0;
|
|
6140
6306
|
let dateCount = 0;
|
|
6141
6307
|
let totalNonNull = 0;
|
|
@@ -7714,7 +7880,7 @@ function computeAxes(scales, chartArea, strategy, theme, measureText) {
|
|
|
7714
7880
|
}));
|
|
7715
7881
|
const shouldThin = scales.x.type !== "band" && !axisConfig?.tickCount && !axisConfig?.values;
|
|
7716
7882
|
const ticks2 = shouldThin ? thinTicksUntilFit(allTicks, fontSize, fontWeight, measureText) : allTicks;
|
|
7717
|
-
let tickAngle = axisConfig?.labelAngle
|
|
7883
|
+
let tickAngle = axisConfig?.labelAngle;
|
|
7718
7884
|
if (tickAngle === void 0 && scales.x.type === "band" && ticks2.length > 1) {
|
|
7719
7885
|
const bandwidth = scales.x.scale.bandwidth();
|
|
7720
7886
|
let maxLabelWidth = 0;
|
|
@@ -7726,7 +7892,7 @@ function computeAxes(scales, chartArea, strategy, theme, measureText) {
|
|
|
7726
7892
|
tickAngle = -45;
|
|
7727
7893
|
}
|
|
7728
7894
|
}
|
|
7729
|
-
const axisTitle = axisConfig?.title
|
|
7895
|
+
const axisTitle = axisConfig?.title;
|
|
7730
7896
|
result.x = {
|
|
7731
7897
|
ticks: ticks2,
|
|
7732
7898
|
gridlines: axisConfig?.grid ? gridlines : [],
|
|
@@ -7756,14 +7922,14 @@ function computeAxes(scales, chartArea, strategy, theme, measureText) {
|
|
|
7756
7922
|
} else {
|
|
7757
7923
|
allTicks = continuousTicks(scales.y, yDensity);
|
|
7758
7924
|
}
|
|
7759
|
-
const
|
|
7925
|
+
const shouldThin = scales.y.type !== "band" && !axisConfig?.tickCount && !axisConfig?.values;
|
|
7926
|
+
const ticks2 = shouldThin ? thinTicksUntilFit(allTicks, fontSize, fontWeight, measureText) : allTicks;
|
|
7927
|
+
const gridlines = ticks2.map((t) => ({
|
|
7760
7928
|
position: t.position,
|
|
7761
7929
|
major: true
|
|
7762
7930
|
}));
|
|
7763
|
-
const
|
|
7764
|
-
const
|
|
7765
|
-
const axisTitle = axisConfig?.title ?? axisConfig?.label;
|
|
7766
|
-
const tickAngle = axisConfig?.labelAngle ?? axisConfig?.tickAngle;
|
|
7931
|
+
const axisTitle = axisConfig?.title;
|
|
7932
|
+
const tickAngle = axisConfig?.labelAngle;
|
|
7767
7933
|
result.y = {
|
|
7768
7934
|
ticks: ticks2,
|
|
7769
7935
|
// Y-axis gridlines are shown by default (standard editorial practice)
|
|
@@ -7825,8 +7991,8 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
7825
7991
|
const isRadial = spec.markType === "arc";
|
|
7826
7992
|
const encoding = spec.encoding;
|
|
7827
7993
|
const xAxis = encoding.x?.axis;
|
|
7828
|
-
const hasXAxisLabel = !!xAxis?.
|
|
7829
|
-
const xTickAngle = xAxis?.
|
|
7994
|
+
const hasXAxisLabel = !!xAxis?.title;
|
|
7995
|
+
const xTickAngle = xAxis?.labelAngle;
|
|
7830
7996
|
let xAxisHeight;
|
|
7831
7997
|
if (isRadial) {
|
|
7832
7998
|
xAxisHeight = 0;
|
|
@@ -8051,6 +8217,12 @@ function uniqueStrings(values) {
|
|
|
8051
8217
|
}
|
|
8052
8218
|
return result;
|
|
8053
8219
|
}
|
|
8220
|
+
function applyCategoricalSort(values, sort) {
|
|
8221
|
+
if (sort === null) return values;
|
|
8222
|
+
const sorted = [...values].sort((a, b) => a.localeCompare(b, void 0, { numeric: true }));
|
|
8223
|
+
if (sort === "descending") sorted.reverse();
|
|
8224
|
+
return sorted;
|
|
8225
|
+
}
|
|
8054
8226
|
function applyContinuousConfig(scale, channel) {
|
|
8055
8227
|
if (channel.scale?.clamp) {
|
|
8056
8228
|
scale.clamp(true);
|
|
@@ -8212,7 +8384,7 @@ function evenRange(start, end, count) {
|
|
|
8212
8384
|
return Array.from({ length: count }, (_, i) => start + step * i);
|
|
8213
8385
|
}
|
|
8214
8386
|
function buildBandScale(channel, data, rangeStart, rangeEnd) {
|
|
8215
|
-
const values = channel.scale?.domain ? channel.scale.domain : uniqueStrings(fieldValues(data, channel.field));
|
|
8387
|
+
const values = channel.scale?.domain ? channel.scale.domain : applyCategoricalSort(uniqueStrings(fieldValues(data, channel.field)), channel.sort);
|
|
8216
8388
|
const padding = channel.scale?.padding ?? 0.35;
|
|
8217
8389
|
const scale = band().domain(values).range([rangeStart, rangeEnd]).padding(padding);
|
|
8218
8390
|
if (channel.scale?.paddingInner !== void 0) {
|
|
@@ -8228,7 +8400,7 @@ function buildBandScale(channel, data, rangeStart, rangeEnd) {
|
|
|
8228
8400
|
return { scale, type: "band", channel };
|
|
8229
8401
|
}
|
|
8230
8402
|
function buildPointScale(channel, data, rangeStart, rangeEnd) {
|
|
8231
|
-
const values = channel.scale?.domain ? channel.scale.domain : uniqueStrings(fieldValues(data, channel.field));
|
|
8403
|
+
const values = channel.scale?.domain ? channel.scale.domain : applyCategoricalSort(uniqueStrings(fieldValues(data, channel.field)), channel.sort);
|
|
8232
8404
|
const padding = channel.scale?.padding ?? 0.5;
|
|
8233
8405
|
const scale = point4().domain(values).range([rangeStart, rangeEnd]).padding(padding);
|
|
8234
8406
|
if (channel.scale?.reverse) {
|
|
@@ -8238,7 +8410,8 @@ function buildPointScale(channel, data, rangeStart, rangeEnd) {
|
|
|
8238
8410
|
return { scale, type: "point", channel };
|
|
8239
8411
|
}
|
|
8240
8412
|
function buildOrdinalColorScale(channel, data, palette) {
|
|
8241
|
-
const
|
|
8413
|
+
const explicitDomain = channel.scale?.domain;
|
|
8414
|
+
const values = explicitDomain ? explicitDomain.map(String) : applyCategoricalSort(uniqueStrings(fieldValues(data, channel.field)), channel.sort);
|
|
8242
8415
|
const scale = ordinal().domain(values).range(palette);
|
|
8243
8416
|
return { scale, type: "ordinal", channel };
|
|
8244
8417
|
}
|
|
@@ -8316,25 +8489,49 @@ function computeScales(spec, chartArea, data) {
|
|
|
8316
8489
|
}
|
|
8317
8490
|
if (encoding.x) {
|
|
8318
8491
|
let xData = data;
|
|
8492
|
+
let xChannel = encoding.x;
|
|
8319
8493
|
const xStackDisabled = encoding.x.stack === null || encoding.x.stack === false;
|
|
8320
8494
|
if (spec.markType === "bar" && encoding.color && encoding.x.type === "quantitative" && !xStackDisabled) {
|
|
8321
|
-
|
|
8322
|
-
|
|
8323
|
-
if (
|
|
8324
|
-
const
|
|
8325
|
-
|
|
8326
|
-
|
|
8327
|
-
const
|
|
8328
|
-
|
|
8329
|
-
|
|
8495
|
+
if (encoding.x.stack === "normalize") {
|
|
8496
|
+
xChannel = { ...encoding.x, scale: { ...encoding.x.scale, domain: [0, 1], nice: false } };
|
|
8497
|
+
} else if (encoding.x.stack === "center") {
|
|
8498
|
+
const yField = encoding.y?.field;
|
|
8499
|
+
const xField = encoding.x.field;
|
|
8500
|
+
if (yField) {
|
|
8501
|
+
const sums = /* @__PURE__ */ new Map();
|
|
8502
|
+
for (const row of data) {
|
|
8503
|
+
const cat = String(row[yField] ?? "");
|
|
8504
|
+
const val = Number(row[xField] ?? 0);
|
|
8505
|
+
if (Number.isFinite(val) && val > 0) {
|
|
8506
|
+
sums.set(cat, (sums.get(cat) ?? 0) + val);
|
|
8507
|
+
}
|
|
8508
|
+
}
|
|
8509
|
+
const maxSum = Math.max(...sums.values(), 0);
|
|
8510
|
+
const half = maxSum / 2;
|
|
8511
|
+
xChannel = {
|
|
8512
|
+
...encoding.x,
|
|
8513
|
+
scale: { ...encoding.x.scale, domain: [-half, half], zero: true }
|
|
8514
|
+
};
|
|
8515
|
+
}
|
|
8516
|
+
} else {
|
|
8517
|
+
const yField = encoding.y?.field;
|
|
8518
|
+
const xField = encoding.x.field;
|
|
8519
|
+
if (yField) {
|
|
8520
|
+
const sums = /* @__PURE__ */ new Map();
|
|
8521
|
+
for (const row of data) {
|
|
8522
|
+
const cat = String(row[yField] ?? "");
|
|
8523
|
+
const val = Number(row[xField] ?? 0);
|
|
8524
|
+
if (Number.isFinite(val) && val > 0) {
|
|
8525
|
+
sums.set(cat, (sums.get(cat) ?? 0) + val);
|
|
8526
|
+
}
|
|
8330
8527
|
}
|
|
8528
|
+
const maxSum = Math.max(...sums.values(), 0);
|
|
8529
|
+
xData = [...data, { [xField]: maxSum }];
|
|
8331
8530
|
}
|
|
8332
|
-
const maxSum = Math.max(...sums.values(), 0);
|
|
8333
|
-
xData = [...data, { [xField]: maxSum }];
|
|
8334
8531
|
}
|
|
8335
8532
|
}
|
|
8336
8533
|
result.x = buildPositionalScale(
|
|
8337
|
-
|
|
8534
|
+
xChannel,
|
|
8338
8535
|
xData,
|
|
8339
8536
|
chartArea.x,
|
|
8340
8537
|
chartArea.x + chartArea.width,
|
|
@@ -8344,26 +8541,50 @@ function computeScales(spec, chartArea, data) {
|
|
|
8344
8541
|
}
|
|
8345
8542
|
if (encoding.y) {
|
|
8346
8543
|
let yData = data;
|
|
8544
|
+
let yChannel = encoding.y;
|
|
8347
8545
|
const isVerticalBar = spec.markType === "bar" && (encoding.x?.type === "nominal" || encoding.x?.type === "ordinal") && encoding.y.type === "quantitative";
|
|
8348
8546
|
const yStackDisabled = encoding.y.stack === null || encoding.y.stack === false;
|
|
8349
8547
|
if ((isVerticalBar || spec.markType === "area") && encoding.color && encoding.y.type === "quantitative" && !yStackDisabled) {
|
|
8350
|
-
|
|
8351
|
-
|
|
8352
|
-
if (
|
|
8353
|
-
const
|
|
8354
|
-
|
|
8355
|
-
|
|
8356
|
-
const
|
|
8357
|
-
|
|
8358
|
-
|
|
8548
|
+
if (encoding.y.stack === "normalize") {
|
|
8549
|
+
yChannel = { ...encoding.y, scale: { ...encoding.y.scale, domain: [0, 1], nice: false } };
|
|
8550
|
+
} else if (encoding.y.stack === "center") {
|
|
8551
|
+
const xField = encoding.x?.field;
|
|
8552
|
+
const yField = encoding.y.field;
|
|
8553
|
+
if (xField) {
|
|
8554
|
+
const sums = /* @__PURE__ */ new Map();
|
|
8555
|
+
for (const row of data) {
|
|
8556
|
+
const cat = String(row[xField] ?? "");
|
|
8557
|
+
const val = Number(row[yField] ?? 0);
|
|
8558
|
+
if (Number.isFinite(val) && val > 0) {
|
|
8559
|
+
sums.set(cat, (sums.get(cat) ?? 0) + val);
|
|
8560
|
+
}
|
|
8359
8561
|
}
|
|
8562
|
+
const maxSum = Math.max(...sums.values(), 0);
|
|
8563
|
+
const half = maxSum / 2;
|
|
8564
|
+
yChannel = {
|
|
8565
|
+
...encoding.y,
|
|
8566
|
+
scale: { ...encoding.y.scale, domain: [-half, half], zero: true }
|
|
8567
|
+
};
|
|
8568
|
+
}
|
|
8569
|
+
} else {
|
|
8570
|
+
const xField = encoding.x?.field;
|
|
8571
|
+
const yField = encoding.y.field;
|
|
8572
|
+
if (xField) {
|
|
8573
|
+
const sums = /* @__PURE__ */ new Map();
|
|
8574
|
+
for (const row of data) {
|
|
8575
|
+
const cat = String(row[xField] ?? "");
|
|
8576
|
+
const val = Number(row[yField] ?? 0);
|
|
8577
|
+
if (Number.isFinite(val) && val > 0) {
|
|
8578
|
+
sums.set(cat, (sums.get(cat) ?? 0) + val);
|
|
8579
|
+
}
|
|
8580
|
+
}
|
|
8581
|
+
const maxSum = Math.max(...sums.values(), 0);
|
|
8582
|
+
yData = [...data, { [yField]: maxSum }];
|
|
8360
8583
|
}
|
|
8361
|
-
const maxSum = Math.max(...sums.values(), 0);
|
|
8362
|
-
yData = [...data, { [yField]: maxSum }];
|
|
8363
8584
|
}
|
|
8364
8585
|
}
|
|
8365
8586
|
result.y = buildPositionalScale(
|
|
8366
|
-
|
|
8587
|
+
yChannel,
|
|
8367
8588
|
yData,
|
|
8368
8589
|
chartArea.y + chartArea.height,
|
|
8369
8590
|
chartArea.y,
|
|
@@ -10183,10 +10404,16 @@ function formatValue(value2, fieldType, format2) {
|
|
|
10183
10404
|
}
|
|
10184
10405
|
return String(value2);
|
|
10185
10406
|
}
|
|
10407
|
+
function resolveLabel(ch) {
|
|
10408
|
+
return ch.title ?? ch.axis?.title ?? ch.field;
|
|
10409
|
+
}
|
|
10410
|
+
function resolveFormat(ch) {
|
|
10411
|
+
return ch.format ?? ch.axis?.format;
|
|
10412
|
+
}
|
|
10186
10413
|
function buildExplicitTooltipFields(row, channels) {
|
|
10187
10414
|
return channels.map((ch) => ({
|
|
10188
|
-
label: ch
|
|
10189
|
-
value: formatValue(row[ch.field], ch.type, ch
|
|
10415
|
+
label: resolveLabel(ch),
|
|
10416
|
+
value: formatValue(row[ch.field], ch.type, resolveFormat(ch))
|
|
10190
10417
|
}));
|
|
10191
10418
|
}
|
|
10192
10419
|
function buildFields(row, encoding, color2) {
|
|
@@ -10197,21 +10424,25 @@ function buildFields(row, encoding, color2) {
|
|
|
10197
10424
|
const fields = [];
|
|
10198
10425
|
if (encoding.y) {
|
|
10199
10426
|
fields.push({
|
|
10200
|
-
label: encoding.y
|
|
10201
|
-
value: formatValue(row[encoding.y.field], encoding.y.type, encoding.y
|
|
10427
|
+
label: resolveLabel(encoding.y),
|
|
10428
|
+
value: formatValue(row[encoding.y.field], encoding.y.type, resolveFormat(encoding.y)),
|
|
10202
10429
|
color: color2
|
|
10203
10430
|
});
|
|
10204
10431
|
}
|
|
10205
10432
|
if (encoding.x) {
|
|
10206
10433
|
fields.push({
|
|
10207
|
-
label: encoding.x
|
|
10208
|
-
value: formatValue(row[encoding.x.field], encoding.x.type, encoding.x
|
|
10434
|
+
label: resolveLabel(encoding.x),
|
|
10435
|
+
value: formatValue(row[encoding.x.field], encoding.x.type, resolveFormat(encoding.x))
|
|
10209
10436
|
});
|
|
10210
10437
|
}
|
|
10211
10438
|
if (encoding.size && "field" in encoding.size) {
|
|
10212
10439
|
fields.push({
|
|
10213
|
-
label: encoding.size
|
|
10214
|
-
value: formatValue(
|
|
10440
|
+
label: resolveLabel(encoding.size),
|
|
10441
|
+
value: formatValue(
|
|
10442
|
+
row[encoding.size.field],
|
|
10443
|
+
encoding.size.type,
|
|
10444
|
+
resolveFormat(encoding.size)
|
|
10445
|
+
)
|
|
10215
10446
|
});
|
|
10216
10447
|
}
|
|
10217
10448
|
return fields;
|
|
@@ -10264,14 +10495,14 @@ function tooltipsForArc(mark, encoding, markIndex) {
|
|
|
10264
10495
|
if (encoding.y) {
|
|
10265
10496
|
fields.push({
|
|
10266
10497
|
label: categoryName,
|
|
10267
|
-
value: formatValue(row[encoding.y.field], encoding.y.type, encoding.y
|
|
10498
|
+
value: formatValue(row[encoding.y.field], encoding.y.type, resolveFormat(encoding.y)),
|
|
10268
10499
|
color: getRepresentativeColor9(mark.fill)
|
|
10269
10500
|
});
|
|
10270
10501
|
}
|
|
10271
10502
|
} else if (encoding.y) {
|
|
10272
10503
|
fields.push({
|
|
10273
|
-
label: encoding.y
|
|
10274
|
-
value: formatValue(row[encoding.y.field], encoding.y.type, encoding.y
|
|
10504
|
+
label: resolveLabel(encoding.y),
|
|
10505
|
+
value: formatValue(row[encoding.y.field], encoding.y.type, resolveFormat(encoding.y)),
|
|
10275
10506
|
color: getRepresentativeColor9(mark.fill)
|
|
10276
10507
|
});
|
|
10277
10508
|
}
|
|
@@ -10319,6 +10550,93 @@ function computeTooltipDescriptors(spec, marks) {
|
|
|
10319
10550
|
return descriptors;
|
|
10320
10551
|
}
|
|
10321
10552
|
|
|
10553
|
+
// src/transforms/aggregate.ts
|
|
10554
|
+
function computeAggregate(op, values) {
|
|
10555
|
+
if (values.length === 0) return 0;
|
|
10556
|
+
switch (op) {
|
|
10557
|
+
case "count":
|
|
10558
|
+
return values.length;
|
|
10559
|
+
case "sum":
|
|
10560
|
+
return values.reduce((a, b) => a + b, 0);
|
|
10561
|
+
case "mean": {
|
|
10562
|
+
const sum2 = values.reduce((a, b) => a + b, 0);
|
|
10563
|
+
return sum2 / values.length;
|
|
10564
|
+
}
|
|
10565
|
+
case "median": {
|
|
10566
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
10567
|
+
const mid = Math.floor(sorted.length / 2);
|
|
10568
|
+
return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
|
|
10569
|
+
}
|
|
10570
|
+
case "min":
|
|
10571
|
+
return Math.min(...values);
|
|
10572
|
+
case "max":
|
|
10573
|
+
return Math.max(...values);
|
|
10574
|
+
case "variance": {
|
|
10575
|
+
if (values.length < 2) return 0;
|
|
10576
|
+
const mean = values.reduce((a, b) => a + b, 0) / values.length;
|
|
10577
|
+
return values.reduce((a, v) => a + (v - mean) ** 2, 0) / values.length;
|
|
10578
|
+
}
|
|
10579
|
+
case "stdev": {
|
|
10580
|
+
if (values.length < 2) return 0;
|
|
10581
|
+
const m = values.reduce((a, b) => a + b, 0) / values.length;
|
|
10582
|
+
return Math.sqrt(values.reduce((a, v) => a + (v - m) ** 2, 0) / values.length);
|
|
10583
|
+
}
|
|
10584
|
+
case "q1": {
|
|
10585
|
+
const s = [...values].sort((a, b) => a - b);
|
|
10586
|
+
const i = (s.length - 1) * 0.25;
|
|
10587
|
+
const lo = Math.floor(i);
|
|
10588
|
+
const frac = i - lo;
|
|
10589
|
+
return s[lo] + frac * ((s[lo + 1] ?? s[lo]) - s[lo]);
|
|
10590
|
+
}
|
|
10591
|
+
case "q3": {
|
|
10592
|
+
const s = [...values].sort((a, b) => a - b);
|
|
10593
|
+
const i = (s.length - 1) * 0.75;
|
|
10594
|
+
const lo = Math.floor(i);
|
|
10595
|
+
const frac = i - lo;
|
|
10596
|
+
return s[lo] + frac * ((s[lo + 1] ?? s[lo]) - s[lo]);
|
|
10597
|
+
}
|
|
10598
|
+
default:
|
|
10599
|
+
return 0;
|
|
10600
|
+
}
|
|
10601
|
+
}
|
|
10602
|
+
function groupKey(row, groupby) {
|
|
10603
|
+
return groupby.map((f) => String(row[f] ?? "")).join("\0");
|
|
10604
|
+
}
|
|
10605
|
+
function runAggregate(data, transform) {
|
|
10606
|
+
const { aggregate, groupby } = transform;
|
|
10607
|
+
const groups = /* @__PURE__ */ new Map();
|
|
10608
|
+
for (const row of data) {
|
|
10609
|
+
const key = groupKey(row, groupby);
|
|
10610
|
+
const existing = groups.get(key);
|
|
10611
|
+
if (existing) {
|
|
10612
|
+
existing.push(row);
|
|
10613
|
+
} else {
|
|
10614
|
+
groups.set(key, [row]);
|
|
10615
|
+
}
|
|
10616
|
+
}
|
|
10617
|
+
const result = [];
|
|
10618
|
+
for (const rows of groups.values()) {
|
|
10619
|
+
const outRow = {};
|
|
10620
|
+
for (const field of groupby) {
|
|
10621
|
+
outRow[field] = rows[0][field];
|
|
10622
|
+
}
|
|
10623
|
+
for (const agg of aggregate) {
|
|
10624
|
+
if (agg.op === "distinct") {
|
|
10625
|
+
outRow[agg.as] = new Set(rows.map((r) => r[agg.field])).size;
|
|
10626
|
+
continue;
|
|
10627
|
+
}
|
|
10628
|
+
const values = rows.map((r) => {
|
|
10629
|
+
if (agg.op === "count") return 1;
|
|
10630
|
+
const v = Number(r[agg.field]);
|
|
10631
|
+
return Number.isFinite(v) ? v : NaN;
|
|
10632
|
+
}).filter((v) => !Number.isNaN(v));
|
|
10633
|
+
outRow[agg.as] = computeAggregate(agg.op, values);
|
|
10634
|
+
}
|
|
10635
|
+
result.push(outRow);
|
|
10636
|
+
}
|
|
10637
|
+
return result;
|
|
10638
|
+
}
|
|
10639
|
+
|
|
10322
10640
|
// src/transforms/bin.ts
|
|
10323
10641
|
function computeStep(extent2, maxbins, nice2) {
|
|
10324
10642
|
const span = extent2[1] - extent2[0];
|
|
@@ -10415,6 +10733,30 @@ function runFilter(data, predicate) {
|
|
|
10415
10733
|
return data.filter((datum) => evaluatePredicate(datum, predicate));
|
|
10416
10734
|
}
|
|
10417
10735
|
|
|
10736
|
+
// src/transforms/fold.ts
|
|
10737
|
+
function runFold(data, transform) {
|
|
10738
|
+
const { fold } = transform;
|
|
10739
|
+
const [keyAs, valueAs] = transform.as ?? ["key", "value"];
|
|
10740
|
+
const foldSet = new Set(fold);
|
|
10741
|
+
const result = [];
|
|
10742
|
+
for (const row of data) {
|
|
10743
|
+
const base = {};
|
|
10744
|
+
for (const [k, v] of Object.entries(row)) {
|
|
10745
|
+
if (!foldSet.has(k)) {
|
|
10746
|
+
base[k] = v;
|
|
10747
|
+
}
|
|
10748
|
+
}
|
|
10749
|
+
for (const field of fold) {
|
|
10750
|
+
result.push({
|
|
10751
|
+
...base,
|
|
10752
|
+
[keyAs]: field,
|
|
10753
|
+
[valueAs]: row[field]
|
|
10754
|
+
});
|
|
10755
|
+
}
|
|
10756
|
+
}
|
|
10757
|
+
return result;
|
|
10758
|
+
}
|
|
10759
|
+
|
|
10418
10760
|
// src/transforms/timeunit.ts
|
|
10419
10761
|
function extractTimeUnit(date2, unit2) {
|
|
10420
10762
|
switch (unit2) {
|
|
@@ -10492,6 +10834,10 @@ function runTransforms(data, transforms) {
|
|
|
10492
10834
|
result = runCalculate(result, transform);
|
|
10493
10835
|
} else if ("timeUnit" in transform) {
|
|
10494
10836
|
result = runTimeUnit(result, transform);
|
|
10837
|
+
} else if ("aggregate" in transform) {
|
|
10838
|
+
result = runAggregate(result, transform);
|
|
10839
|
+
} else if ("fold" in transform) {
|
|
10840
|
+
result = runFold(result, transform);
|
|
10495
10841
|
}
|
|
10496
10842
|
}
|
|
10497
10843
|
return result;
|
|
@@ -10524,71 +10870,55 @@ var builtinRenderers = {
|
|
|
10524
10870
|
for (const [type, renderer] of Object.entries(builtinRenderers)) {
|
|
10525
10871
|
registerChartRenderer(type, renderer);
|
|
10526
10872
|
}
|
|
10527
|
-
function
|
|
10528
|
-
|
|
10529
|
-
|
|
10530
|
-
|
|
10531
|
-
const
|
|
10532
|
-
|
|
10533
|
-
|
|
10534
|
-
|
|
10535
|
-
|
|
10536
|
-
|
|
10537
|
-
const
|
|
10538
|
-
|
|
10539
|
-
|
|
10540
|
-
|
|
10541
|
-
|
|
10542
|
-
|
|
10543
|
-
}
|
|
10544
|
-
|
|
10545
|
-
|
|
10546
|
-
|
|
10547
|
-
|
|
10548
|
-
|
|
10549
|
-
|
|
10550
|
-
|
|
10551
|
-
|
|
10552
|
-
|
|
10553
|
-
|
|
10554
|
-
|
|
10555
|
-
|
|
10556
|
-
|
|
10557
|
-
|
|
10558
|
-
|
|
10559
|
-
|
|
10560
|
-
const
|
|
10561
|
-
|
|
10562
|
-
|
|
10563
|
-
right2 = rm.x + rm.width;
|
|
10564
|
-
} else {
|
|
10565
|
-
continue;
|
|
10566
|
-
}
|
|
10567
|
-
const key = Math.round(cy);
|
|
10568
|
-
const existing = rows.get(key);
|
|
10569
|
-
if (existing) {
|
|
10570
|
-
existing.minX = Math.min(existing.minX, left2);
|
|
10571
|
-
existing.maxX = Math.max(existing.maxX, right2);
|
|
10572
|
-
} else {
|
|
10573
|
-
rows.set(key, { minX: left2, maxX: right2, bandY: cy });
|
|
10873
|
+
function expandEncodingSugar(spec) {
|
|
10874
|
+
const encoding = spec.encoding;
|
|
10875
|
+
if (!encoding) return spec;
|
|
10876
|
+
const generatedTransforms = [];
|
|
10877
|
+
const updatedEncoding = { ...encoding };
|
|
10878
|
+
let changed = false;
|
|
10879
|
+
for (const channel of Object.keys(encoding)) {
|
|
10880
|
+
const ch = encoding[channel];
|
|
10881
|
+
if (!ch || !ch.field) continue;
|
|
10882
|
+
if (ch.bin != null && ch.bin !== false) {
|
|
10883
|
+
const field = ch.field;
|
|
10884
|
+
const outputField = `bin_${field}`;
|
|
10885
|
+
const binTransform = {
|
|
10886
|
+
bin: ch.bin === true ? true : ch.bin,
|
|
10887
|
+
field,
|
|
10888
|
+
as: outputField
|
|
10889
|
+
};
|
|
10890
|
+
generatedTransforms.push(binTransform);
|
|
10891
|
+
const { bin: _bin, ...rest } = ch;
|
|
10892
|
+
updatedEncoding[channel] = { ...rest, field: outputField };
|
|
10893
|
+
changed = true;
|
|
10894
|
+
}
|
|
10895
|
+
const current = updatedEncoding[channel] ?? ch;
|
|
10896
|
+
if (current.timeUnit) {
|
|
10897
|
+
const field = current.field;
|
|
10898
|
+
const unit2 = current.timeUnit;
|
|
10899
|
+
const outputField = `${unit2}_${field}`;
|
|
10900
|
+
const timeUnitTransform = {
|
|
10901
|
+
timeUnit: unit2,
|
|
10902
|
+
field,
|
|
10903
|
+
as: outputField
|
|
10904
|
+
};
|
|
10905
|
+
generatedTransforms.push(timeUnitTransform);
|
|
10906
|
+
const { timeUnit: _tu, ...rest } = current;
|
|
10907
|
+
updatedEncoding[channel] = { ...rest, field: outputField };
|
|
10908
|
+
changed = true;
|
|
10574
10909
|
}
|
|
10575
10910
|
}
|
|
10576
|
-
|
|
10577
|
-
const
|
|
10578
|
-
|
|
10579
|
-
|
|
10580
|
-
|
|
10581
|
-
|
|
10582
|
-
|
|
10583
|
-
y: bandY - bandwidth / 2,
|
|
10584
|
-
width: maxX - minX,
|
|
10585
|
-
height: bandwidth
|
|
10586
|
-
});
|
|
10587
|
-
}
|
|
10588
|
-
return obstacles;
|
|
10911
|
+
if (!changed) return spec;
|
|
10912
|
+
const existingTransforms = spec.transform ?? [];
|
|
10913
|
+
return {
|
|
10914
|
+
...spec,
|
|
10915
|
+
encoding: updatedEncoding,
|
|
10916
|
+
transform: [...generatedTransforms, ...existingTransforms]
|
|
10917
|
+
};
|
|
10589
10918
|
}
|
|
10590
10919
|
function compileChart(spec, options) {
|
|
10591
|
-
const
|
|
10920
|
+
const expandedSpec = spec && typeof spec === "object" && !Array.isArray(spec) ? expandEncodingSugar(spec) : spec;
|
|
10921
|
+
const { spec: normalized } = compile(expandedSpec);
|
|
10592
10922
|
if ("type" in normalized && normalized.type === "table") {
|
|
10593
10923
|
throw new Error("compileChart received a table spec. Use compileTable instead.");
|
|
10594
10924
|
}
|
|
@@ -10599,16 +10929,16 @@ function compileChart(spec, options) {
|
|
|
10599
10929
|
throw new Error("compileChart received a sankey spec. Use compileSankey instead.");
|
|
10600
10930
|
}
|
|
10601
10931
|
let chartSpec = normalized;
|
|
10602
|
-
const rawWatermark =
|
|
10932
|
+
const rawWatermark = expandedSpec.watermark;
|
|
10603
10933
|
const watermark = rawWatermark !== void 0 ? chartSpec.watermark : options.watermark ?? true;
|
|
10604
|
-
const rawTransforms =
|
|
10934
|
+
const rawTransforms = expandedSpec.transform;
|
|
10605
10935
|
if (rawTransforms && rawTransforms.length > 0) {
|
|
10606
10936
|
chartSpec = { ...chartSpec, data: runTransforms(chartSpec.data, rawTransforms) };
|
|
10607
10937
|
}
|
|
10608
10938
|
const breakpoint = getBreakpoint(options.width);
|
|
10609
10939
|
const heightClass = getHeightClass(options.height);
|
|
10610
10940
|
const strategy = getLayoutStrategy(breakpoint, heightClass);
|
|
10611
|
-
const rawSpec =
|
|
10941
|
+
const rawSpec = expandedSpec;
|
|
10612
10942
|
const overrides = rawSpec.overrides;
|
|
10613
10943
|
if (overrides?.[breakpoint]) {
|
|
10614
10944
|
const bp = overrides[breakpoint];
|
|
@@ -10719,20 +11049,11 @@ function compileChart(spec, options) {
|
|
|
10719
11049
|
if (!isRadial) {
|
|
10720
11050
|
computeGridlines(axes, chartArea);
|
|
10721
11051
|
}
|
|
10722
|
-
|
|
10723
|
-
|
|
10724
|
-
|
|
10725
|
-
|
|
10726
|
-
|
|
10727
|
-
if (isVertical) {
|
|
10728
|
-
rendererKey = "bar:vertical";
|
|
10729
|
-
}
|
|
10730
|
-
} else if (rendererKey === "arc") {
|
|
10731
|
-
const innerRadius = renderSpec.markDef.innerRadius;
|
|
10732
|
-
if (innerRadius && innerRadius > 0) {
|
|
10733
|
-
rendererKey = "arc:donut";
|
|
10734
|
-
}
|
|
10735
|
-
}
|
|
11052
|
+
const rendererKey = resolveRendererKey(
|
|
11053
|
+
renderSpec.markType,
|
|
11054
|
+
renderSpec.encoding,
|
|
11055
|
+
renderSpec.markDef
|
|
11056
|
+
);
|
|
10736
11057
|
const renderer = getChartRenderer(rendererKey);
|
|
10737
11058
|
const marks = renderer ? renderer(renderSpec, scales, chartArea, strategy, theme) : [];
|
|
10738
11059
|
const obstacles = [];
|
|
@@ -10780,37 +11101,7 @@ function compileChart(spec, options) {
|
|
|
10780
11101
|
},
|
|
10781
11102
|
chartSpec.data
|
|
10782
11103
|
);
|
|
10783
|
-
|
|
10784
|
-
const indexed = marks.map((m, i) => ({ mark: m, idx: i }));
|
|
10785
|
-
indexed.sort((a, b) => {
|
|
10786
|
-
const av = getMarkPrimaryValue(a.mark);
|
|
10787
|
-
const bv = getMarkPrimaryValue(b.mark);
|
|
10788
|
-
return av - bv;
|
|
10789
|
-
});
|
|
10790
|
-
for (let i = 0; i < indexed.length; i++) {
|
|
10791
|
-
const m = indexed[i].mark;
|
|
10792
|
-
if (m.type === "rect" && m.stackGroup) continue;
|
|
10793
|
-
m.animationIndex = i;
|
|
10794
|
-
}
|
|
10795
|
-
}
|
|
10796
|
-
if (resolvedAnimation?.enabled) {
|
|
10797
|
-
const groupIndexMap = /* @__PURE__ */ new Map();
|
|
10798
|
-
const groupStackPos = /* @__PURE__ */ new Map();
|
|
10799
|
-
let nextGroupIndex = 0;
|
|
10800
|
-
for (const mark of marks) {
|
|
10801
|
-
if (mark.type === "rect" && mark.stackGroup) {
|
|
10802
|
-
const rect = mark;
|
|
10803
|
-
const group = rect.stackGroup;
|
|
10804
|
-
if (!groupIndexMap.has(group)) {
|
|
10805
|
-
groupIndexMap.set(group, nextGroupIndex++);
|
|
10806
|
-
}
|
|
10807
|
-
rect.animationIndex = groupIndexMap.get(group);
|
|
10808
|
-
const pos = groupStackPos.get(group) ?? 0;
|
|
10809
|
-
rect.stackPos = pos;
|
|
10810
|
-
groupStackPos.set(group, pos + 1);
|
|
10811
|
-
}
|
|
10812
|
-
}
|
|
10813
|
-
}
|
|
11104
|
+
assignAnimationIndices(marks, resolvedAnimation);
|
|
10814
11105
|
return {
|
|
10815
11106
|
area: chartArea,
|
|
10816
11107
|
chrome: dims.chrome,
|
|
@@ -10837,25 +11128,6 @@ function compileChart(spec, options) {
|
|
|
10837
11128
|
watermark
|
|
10838
11129
|
};
|
|
10839
11130
|
}
|
|
10840
|
-
function getMarkPrimaryValue(mark) {
|
|
10841
|
-
switch (mark.type) {
|
|
10842
|
-
case "rect":
|
|
10843
|
-
return mark.height;
|
|
10844
|
-
// bar height is the primary value encoding
|
|
10845
|
-
case "point":
|
|
10846
|
-
return mark.cy;
|
|
10847
|
-
// y position for scatter
|
|
10848
|
-
case "arc":
|
|
10849
|
-
return mark.endAngle - mark.startAngle;
|
|
10850
|
-
// arc angle extent
|
|
10851
|
-
case "line":
|
|
10852
|
-
case "area":
|
|
10853
|
-
return 0;
|
|
10854
|
-
// series marks don't have individual values
|
|
10855
|
-
default:
|
|
10856
|
-
return 0;
|
|
10857
|
-
}
|
|
10858
|
-
}
|
|
10859
11131
|
function compileLayer(spec, options) {
|
|
10860
11132
|
const leaves = flattenLayers(spec);
|
|
10861
11133
|
if (leaves.length === 0) {
|