@opendata-ai/openchart-engine 6.12.0 → 6.15.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.
Files changed (43) hide show
  1. package/dist/index.js +1022 -648
  2. package/dist/index.js.map +1 -1
  3. package/package.json +2 -2
  4. package/src/__tests__/axes.test.ts +12 -30
  5. package/src/__tests__/compile-chart.test.ts +4 -4
  6. package/src/__tests__/dimensions.test.ts +2 -2
  7. package/src/__tests__/encoding-sugar.test.ts +390 -0
  8. package/src/annotations/collisions.ts +268 -0
  9. package/src/annotations/compute.ts +9 -912
  10. package/src/annotations/constants.ts +32 -0
  11. package/src/annotations/geometry.ts +167 -0
  12. package/src/annotations/position.ts +95 -0
  13. package/src/annotations/resolve-range.ts +98 -0
  14. package/src/annotations/resolve-refline.ts +148 -0
  15. package/src/annotations/resolve-text.ts +134 -0
  16. package/src/charts/__tests__/post-process.test.ts +258 -0
  17. package/src/charts/bar/__tests__/labels.test.ts +31 -0
  18. package/src/charts/bar/compute.ts +27 -6
  19. package/src/charts/bar/index.ts +3 -0
  20. package/src/charts/bar/labels.ts +38 -14
  21. package/src/charts/column/__tests__/compute.test.ts +99 -0
  22. package/src/charts/column/compute.ts +27 -6
  23. package/src/charts/column/index.ts +3 -0
  24. package/src/charts/column/labels.ts +35 -13
  25. package/src/charts/dot/index.ts +10 -1
  26. package/src/charts/dot/labels.ts +37 -6
  27. package/src/charts/line/area.ts +31 -6
  28. package/src/charts/line/compute.ts +7 -2
  29. package/src/charts/line/index.ts +33 -2
  30. package/src/charts/post-process.ts +215 -0
  31. package/src/compile.ts +91 -158
  32. package/src/compiler/normalize.ts +2 -2
  33. package/src/layout/axes.ts +12 -15
  34. package/src/layout/dimensions.ts +3 -3
  35. package/src/layout/scales.ts +116 -36
  36. package/src/legend/compute.ts +2 -4
  37. package/src/tooltips/__tests__/compute.test.ts +188 -0
  38. package/src/tooltips/compute.ts +54 -12
  39. package/src/transforms/__tests__/aggregate.test.ts +159 -0
  40. package/src/transforms/__tests__/fold.test.ts +79 -0
  41. package/src/transforms/aggregate.ts +130 -0
  42. package/src/transforms/fold.ts +49 -0
  43. 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/compute.ts
15
- import { detectCollision, estimateTextWidth } from "@opendata-ai/openchart-core";
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,76 +26,11 @@ 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;
27
- function interpolateInDomain(numValue, domain, positionOf) {
28
- if (domain.length < 2) return null;
29
- const nums = domain.map(Number);
30
- if (!nums.every(Number.isFinite)) return null;
31
- const sorted = nums.map((n, i) => ({ n, i })).sort((a, b) => a.n - b.n);
32
- let lower = 0;
33
- let upper = sorted.length - 1;
34
- for (let i = 0; i < sorted.length; i++) {
35
- if (sorted[i].n <= numValue) lower = i;
36
- if (sorted[i].n >= numValue) {
37
- upper = i;
38
- break;
39
- }
40
- }
41
- const lowerPos = positionOf(domain[sorted[lower].i]);
42
- const upperPos = positionOf(domain[sorted[upper].i]);
43
- if (lower === upper) return lowerPos;
44
- const t = (numValue - sorted[lower].n) / (sorted[upper].n - sorted[lower].n);
45
- return lowerPos + t * (upperPos - lowerPos);
46
- }
47
- function resolvePosition(value2, scale) {
48
- if (!scale) return null;
49
- const s = scale.scale;
50
- const type = scale.type;
51
- if (type === "time") {
52
- const date2 = new Date(String(value2));
53
- if (Number.isNaN(date2.getTime())) return null;
54
- return s(date2);
55
- }
56
- if (type === "linear" || type === "log") {
57
- const num = typeof value2 === "number" ? value2 : Number(value2);
58
- if (!Number.isFinite(num)) return null;
59
- return s(num);
60
- }
61
- if (type === "band") {
62
- const bandScale = s;
63
- const strValue2 = String(value2);
64
- const pos = bandScale(strValue2);
65
- if (pos !== void 0) return pos + (bandScale.bandwidth?.() ?? 0) / 2;
66
- const bw = bandScale.bandwidth?.() ?? 0;
67
- return interpolateInDomain(
68
- Number(strValue2),
69
- bandScale.domain(),
70
- (entry) => (bandScale(entry) ?? 0) + bw / 2
71
- );
72
- }
73
- const strValue = String(value2);
74
- const directResult = s(strValue);
75
- if (directResult !== void 0) return directResult;
76
- if (type === "point" || type === "ordinal") {
77
- const domain = s.domain();
78
- return interpolateInDomain(
79
- Number(strValue),
80
- domain,
81
- (entry) => s(entry) ?? 0
82
- );
83
- }
84
- return null;
85
- }
86
- function makeAnnotationLabelStyle(fontSize, fontWeight, fill, isDark) {
87
- const defaultFill = isDark ? DARK_TEXT_FILL : LIGHT_TEXT_FILL;
88
- return {
89
- fontFamily: "Inter, system-ui, sans-serif",
90
- fontSize: fontSize ?? DEFAULT_ANNOTATION_FONT_SIZE,
91
- fontWeight: fontWeight ?? DEFAULT_ANNOTATION_FONT_WEIGHT,
92
- fill: fill ?? defaultFill,
93
- lineHeight: DEFAULT_LINE_HEIGHT,
94
- textAnchor: "start"
95
- };
96
- }
29
+ var NUDGE_PADDING = 6;
30
+ var CLAMP_MARGIN = 4;
31
+
32
+ // src/annotations/geometry.ts
33
+ import { estimateTextWidth } from "@opendata-ai/openchart-core";
97
34
  function computeTextBounds(labelX, labelY, text, fontSize, fontWeight) {
98
35
  const lines = text.split("\n");
99
36
  const isMultiLine = lines.length > 1;
@@ -149,230 +86,92 @@ function computeConnectorOrigin(labelX, labelY, text, fontSize, fontWeight, targ
149
86
  }
150
87
  return ndx < 0 ? { x: box.x, y: boxCenterY } : { x: box.x + box.width, y: boxCenterY };
151
88
  }
152
- function resolveTextAnnotation(annotation, scales, chartArea, isDark) {
153
- const px = resolvePosition(annotation.x, scales.x);
154
- const py = resolvePosition(annotation.y, scales.y);
155
- if (px === null || py === null) return null;
156
- const defaultTextFill = isDark ? DARK_TEXT_FILL : LIGHT_TEXT_FILL;
157
- const labelStyle = makeAnnotationLabelStyle(
158
- annotation.fontSize,
159
- annotation.fontWeight,
160
- annotation.fill ?? defaultTextFill,
161
- isDark
162
- );
163
- const anchorDelta = computeAnchorOffset(annotation.anchor, px, py, chartArea);
164
- const finalDelta = applyOffset(anchorDelta, annotation.offset);
165
- const labelX = px + finalDelta.dx;
166
- const labelY = py + finalDelta.dy;
167
- const showConnector = annotation.connector !== false;
168
- const connectorStyle = annotation.connector === "curve" ? "curve" : "straight";
169
- const fontSize = annotation.fontSize ?? DEFAULT_ANNOTATION_FONT_SIZE;
170
- const fontWeight = annotation.fontWeight ?? DEFAULT_ANNOTATION_FONT_WEIGHT;
171
- const { x: connectorFromX, y: connectorFromY } = computeConnectorOrigin(
172
- labelX,
173
- labelY,
174
- annotation.text,
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,
175
104
  fontSize,
176
105
  fontWeight,
177
- px,
178
- py,
179
- connectorStyle
106
+ targetX,
107
+ targetY,
108
+ connStyle
180
109
  );
181
- const baseFrom = { x: connectorFromX, y: connectorFromY };
182
- const baseTo = { x: px, y: py };
183
- const adjustedFrom = {
184
- x: baseFrom.x + (annotation.connectorOffset?.from?.dx ?? 0),
185
- y: baseFrom.y + (annotation.connectorOffset?.from?.dy ?? 0)
186
- };
187
- const adjustedToRaw = {
188
- x: baseTo.x + (annotation.connectorOffset?.to?.dx ?? 0),
189
- y: baseTo.y + (annotation.connectorOffset?.to?.dy ?? 0)
190
- };
191
- const GAP = 4;
192
- const cdx = adjustedToRaw.x - adjustedFrom.x;
193
- const cdy = adjustedToRaw.y - adjustedFrom.y;
194
- const dist = Math.sqrt(cdx * cdx + cdy * cdy);
195
- const adjustedTo = dist > GAP * 2 ? { x: adjustedToRaw.x - cdx / dist * GAP, y: adjustedToRaw.y - cdy / dist * GAP } : adjustedToRaw;
196
- const label = {
197
- text: annotation.text,
198
- x: labelX,
199
- y: labelY,
200
- style: labelStyle,
201
- visible: true,
202
- connector: showConnector ? {
203
- from: adjustedFrom,
204
- to: adjustedTo,
205
- stroke: annotation.stroke ?? "#999999",
206
- style: connectorStyle
207
- } : void 0,
208
- background: annotation.background
209
- };
210
- return {
211
- type: "text",
212
- id: annotation.id,
213
- label,
214
- stroke: annotation.stroke,
215
- fill: annotation.fill,
216
- opacity: annotation.opacity,
217
- zIndex: annotation.zIndex
218
- };
110
+ return { ...connector, from: newFrom };
219
111
  }
220
- function resolveRangeAnnotation(annotation, scales, chartArea, isDark) {
221
- let x2 = chartArea.x;
222
- let y2 = chartArea.y;
223
- let width = chartArea.width;
224
- let height = chartArea.height;
225
- if (annotation.x1 !== void 0 && annotation.x2 !== void 0) {
226
- const x1px = resolvePosition(annotation.x1, scales.x);
227
- const x2px = resolvePosition(annotation.x2, scales.x);
228
- if (x1px === null || x2px === null) return null;
229
- x2 = Math.min(x1px, x2px);
230
- width = Math.abs(x2px - x1px);
231
- }
232
- if (annotation.y1 !== void 0 && annotation.y2 !== void 0) {
233
- const y1px = resolvePosition(annotation.y1, scales.y);
234
- const y2px = resolvePosition(annotation.y2, scales.y);
235
- if (y1px === null || y2px === null) return null;
236
- y2 = Math.min(y1px, y2px);
237
- height = Math.abs(y2px - y1px);
238
- }
239
- const rect = { x: x2, y: y2, width, height };
240
- let label;
241
- if (annotation.label) {
242
- const anchor = annotation.labelAnchor ?? "top";
243
- const centered = anchor === "top" || anchor === "bottom" || anchor === "auto";
244
- const baseDx = centered ? 0 : anchor === "right" ? -4 : 4;
245
- const baseDy = 14;
246
- const labelDelta = applyOffset({ dx: baseDx, dy: baseDy }, annotation.labelOffset);
247
- const style = makeAnnotationLabelStyle(11, 500, void 0, isDark);
248
- if (centered) {
249
- style.textAnchor = "middle";
250
- } else if (anchor === "right") {
251
- style.textAnchor = "end";
112
+
113
+ // src/annotations/position.ts
114
+ function interpolateInDomain(numValue, domain, positionOf) {
115
+ if (domain.length < 2) return null;
116
+ const nums = domain.map(Number);
117
+ if (!nums.every(Number.isFinite)) return null;
118
+ const sorted = nums.map((n, i) => ({ n, i })).sort((a, b) => a.n - b.n);
119
+ let lower = 0;
120
+ let upper = sorted.length - 1;
121
+ for (let i = 0; i < sorted.length; i++) {
122
+ if (sorted[i].n <= numValue) lower = i;
123
+ if (sorted[i].n >= numValue) {
124
+ upper = i;
125
+ break;
252
126
  }
253
- const baseX = centered ? x2 + width / 2 : anchor === "right" ? x2 + width : x2;
254
- label = {
255
- text: annotation.label,
256
- x: baseX + labelDelta.dx,
257
- y: y2 + labelDelta.dy,
258
- style,
259
- visible: true
260
- };
261
127
  }
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
- };
128
+ const lowerPos = positionOf(domain[sorted[lower].i]);
129
+ const upperPos = positionOf(domain[sorted[upper].i]);
130
+ if (lower === upper) return lowerPos;
131
+ const t = (numValue - sorted[lower].n) / (sorted[upper].n - sorted[lower].n);
132
+ return lowerPos + t * (upperPos - lowerPos);
273
133
  }
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;
134
+ function resolvePosition(value2, scale) {
135
+ if (!scale) return null;
136
+ const s = scale.scale;
137
+ const type = scale.type;
138
+ if (type === "time") {
139
+ const date2 = new Date(String(value2));
140
+ if (Number.isNaN(date2.getTime())) return null;
141
+ return s(date2);
289
142
  }
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";
143
+ if (type === "linear" || type === "log") {
144
+ const num = typeof value2 === "number" ? value2 : Number(value2);
145
+ if (!Number.isFinite(num)) return null;
146
+ return s(num);
295
147
  }
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
- };
148
+ if (type === "band") {
149
+ const bandScale = s;
150
+ const strValue2 = String(value2);
151
+ const pos = bandScale(strValue2);
152
+ if (pos !== void 0) return pos + (bandScale.bandwidth?.() ?? 0) / 2;
153
+ const bw = bandScale.bandwidth?.() ?? 0;
154
+ return interpolateInDomain(
155
+ Number(strValue2),
156
+ bandScale.domain(),
157
+ (entry) => (bandScale(entry) ?? 0) + bw / 2
158
+ );
357
159
  }
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);
160
+ const strValue = String(value2);
161
+ const directResult = s(strValue);
162
+ if (directResult !== void 0) return directResult;
163
+ if (type === "point" || type === "ordinal") {
164
+ const domain = s.domain();
165
+ return interpolateInDomain(
166
+ Number(strValue),
167
+ domain,
168
+ (entry) => s(entry) ?? 0
169
+ );
170
+ }
171
+ return null;
374
172
  }
375
- var NUDGE_PADDING = 6;
173
+
174
+ // src/annotations/collisions.ts
376
175
  function generateNudgeCandidates(selfBounds, obstacles, padding) {
377
176
  const candidates = [];
378
177
  for (const obs of obstacles) {
@@ -403,28 +202,11 @@ function nudgeAnnotationFromObstacles(annotation, originalAnnotation, scales, ch
403
202
  for (const { dx, dy } of candidates) {
404
203
  const newLabelX = annotation.label.x + dx;
405
204
  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
205
  const candidateLabel = {
424
206
  ...annotation.label,
425
207
  x: newLabelX,
426
208
  y: newLabelY,
427
- connector: newConnector
209
+ connector: recomputeConnector({ ...annotation.label, x: newLabelX, y: newLabelY }, px, py)
428
210
  };
429
211
  const candidateBounds = estimateLabelBounds(candidateLabel);
430
212
  const stillCollides = obstacles.some(
@@ -477,28 +259,15 @@ function resolveAnnotationCollisions(annotations, originalSpecs, scales, chartAr
477
259
  const labelCenterY = candidateBounds.y + candidateBounds.height / 2;
478
260
  const inBounds = labelCenterX >= chartArea.x && labelCenterX <= chartArea.x + chartArea.width + 10 && labelCenterY >= chartArea.y - fontSize && labelCenterY <= chartArea.y + chartArea.height + fontSize;
479
261
  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
262
  annotation.label = {
498
263
  ...annotation.label,
499
264
  x: newLabelX,
500
265
  y: newLabelY,
501
- connector: newConnector
266
+ connector: recomputeConnector(
267
+ { ...annotation.label, x: newLabelX, y: newLabelY },
268
+ px,
269
+ py
270
+ )
502
271
  };
503
272
  break;
504
273
  }
@@ -509,7 +278,6 @@ function resolveAnnotationCollisions(annotations, originalSpecs, scales, chartAr
509
278
  placedBounds.push(estimateLabelBounds(annotation.label));
510
279
  }
511
280
  }
512
- var CLAMP_MARGIN = 4;
513
281
  function clampAnnotationsToBounds(annotations, svgWidth, svgHeight) {
514
282
  for (const annotation of annotations) {
515
283
  if (annotation.type !== "text" || !annotation.label) continue;
@@ -528,34 +296,259 @@ function clampAnnotationsToBounds(annotations, svgWidth, svgHeight) {
528
296
  if (bounds.y + bounds.height + dy > svgHeight - CLAMP_MARGIN) {
529
297
  dy = svgHeight - CLAMP_MARGIN - (bounds.y + bounds.height);
530
298
  }
531
- if (dx === 0 && dy === 0) continue;
532
- const newX = annotation.label.x + dx;
533
- const newY = annotation.label.y + dy;
534
- let newConnector = annotation.label.connector;
535
- if (newConnector) {
536
- const fontSize = annotation.label.style.fontSize ?? DEFAULT_ANNOTATION_FONT_SIZE;
537
- const fontWeight = annotation.label.style.fontWeight ?? DEFAULT_ANNOTATION_FONT_WEIGHT;
538
- const connStyle = newConnector.style === "curve" ? "curve" : "straight";
539
- const newFrom = computeConnectorOrigin(
540
- newX,
541
- newY,
542
- annotation.label.text,
543
- fontSize,
544
- fontWeight,
545
- newConnector.to.x,
546
- newConnector.to.y,
547
- connStyle
548
- );
549
- newConnector = { ...newConnector, from: newFrom };
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
317
+ function makeAnnotationLabelStyle(fontSize, fontWeight, fill, isDark) {
318
+ const defaultFill = isDark ? DARK_TEXT_FILL : LIGHT_TEXT_FILL;
319
+ return {
320
+ fontFamily: "Inter, system-ui, sans-serif",
321
+ fontSize: fontSize ?? DEFAULT_ANNOTATION_FONT_SIZE,
322
+ fontWeight: fontWeight ?? DEFAULT_ANNOTATION_FONT_WEIGHT,
323
+ fill: fill ?? defaultFill,
324
+ lineHeight: DEFAULT_LINE_HEIGHT,
325
+ textAnchor: "start"
326
+ };
327
+ }
328
+ function resolveTextAnnotation(annotation, scales, chartArea, isDark) {
329
+ const px = resolvePosition(annotation.x, scales.x);
330
+ const py = resolvePosition(annotation.y, scales.y);
331
+ if (px === null || py === null) return null;
332
+ const defaultTextFill = isDark ? DARK_TEXT_FILL : LIGHT_TEXT_FILL;
333
+ const labelStyle = makeAnnotationLabelStyle(
334
+ annotation.fontSize,
335
+ annotation.fontWeight,
336
+ annotation.fill ?? defaultTextFill,
337
+ isDark
338
+ );
339
+ const anchorDelta = computeAnchorOffset(annotation.anchor, px, py, chartArea);
340
+ const finalDelta = applyOffset(anchorDelta, annotation.offset);
341
+ const labelX = px + finalDelta.dx;
342
+ const labelY = py + finalDelta.dy;
343
+ const showConnector = annotation.connector !== false;
344
+ const connectorStyle = annotation.connector === "curve" ? "curve" : "straight";
345
+ const fontSize = annotation.fontSize ?? DEFAULT_ANNOTATION_FONT_SIZE;
346
+ const fontWeight = annotation.fontWeight ?? DEFAULT_ANNOTATION_FONT_WEIGHT;
347
+ const { x: connectorFromX, y: connectorFromY } = computeConnectorOrigin(
348
+ labelX,
349
+ labelY,
350
+ annotation.text,
351
+ fontSize,
352
+ fontWeight,
353
+ px,
354
+ py,
355
+ connectorStyle
356
+ );
357
+ const baseFrom = { x: connectorFromX, y: connectorFromY };
358
+ const baseTo = { x: px, y: py };
359
+ const adjustedFrom = {
360
+ x: baseFrom.x + (annotation.connectorOffset?.from?.dx ?? 0),
361
+ y: baseFrom.y + (annotation.connectorOffset?.from?.dy ?? 0)
362
+ };
363
+ const adjustedToRaw = {
364
+ x: baseTo.x + (annotation.connectorOffset?.to?.dx ?? 0),
365
+ y: baseTo.y + (annotation.connectorOffset?.to?.dy ?? 0)
366
+ };
367
+ const GAP = 4;
368
+ const cdx = adjustedToRaw.x - adjustedFrom.x;
369
+ const cdy = adjustedToRaw.y - adjustedFrom.y;
370
+ const dist = Math.sqrt(cdx * cdx + cdy * cdy);
371
+ const adjustedTo = dist > GAP * 2 ? { x: adjustedToRaw.x - cdx / dist * GAP, y: adjustedToRaw.y - cdy / dist * GAP } : adjustedToRaw;
372
+ const label = {
373
+ text: annotation.text,
374
+ x: labelX,
375
+ y: labelY,
376
+ style: labelStyle,
377
+ visible: true,
378
+ connector: showConnector ? {
379
+ from: adjustedFrom,
380
+ to: adjustedTo,
381
+ stroke: annotation.stroke ?? "#999999",
382
+ style: connectorStyle
383
+ } : void 0,
384
+ background: annotation.background
385
+ };
386
+ return {
387
+ type: "text",
388
+ id: annotation.id,
389
+ label,
390
+ stroke: annotation.stroke,
391
+ fill: annotation.fill,
392
+ opacity: annotation.opacity,
393
+ zIndex: annotation.zIndex
394
+ };
395
+ }
396
+
397
+ // src/annotations/resolve-range.ts
398
+ function resolveRangeAnnotation(annotation, scales, chartArea, isDark) {
399
+ let x2 = chartArea.x;
400
+ let y2 = chartArea.y;
401
+ let width = chartArea.width;
402
+ let height = chartArea.height;
403
+ if (annotation.x1 !== void 0 && annotation.x2 !== void 0) {
404
+ const x1px = resolvePosition(annotation.x1, scales.x);
405
+ const x2px = resolvePosition(annotation.x2, scales.x);
406
+ if (x1px === null || x2px === null) return null;
407
+ x2 = Math.min(x1px, x2px);
408
+ width = Math.abs(x2px - x1px);
409
+ }
410
+ if (annotation.y1 !== void 0 && annotation.y2 !== void 0) {
411
+ const y1px = resolvePosition(annotation.y1, scales.y);
412
+ const y2px = resolvePosition(annotation.y2, scales.y);
413
+ if (y1px === null || y2px === null) return null;
414
+ y2 = Math.min(y1px, y2px);
415
+ height = Math.abs(y2px - y1px);
416
+ }
417
+ const rect = { x: x2, y: y2, width, height };
418
+ let label;
419
+ if (annotation.label) {
420
+ const anchor = annotation.labelAnchor ?? "top";
421
+ const centered = anchor === "top" || anchor === "bottom" || anchor === "auto";
422
+ const baseDx = centered ? 0 : anchor === "right" ? -4 : 4;
423
+ const baseDy = 14;
424
+ const labelDelta = applyOffset({ dx: baseDx, dy: baseDy }, annotation.labelOffset);
425
+ const style = makeAnnotationLabelStyle(11, 500, void 0, isDark);
426
+ if (centered) {
427
+ style.textAnchor = "middle";
428
+ } else if (anchor === "right") {
429
+ style.textAnchor = "end";
430
+ }
431
+ const baseX = centered ? x2 + width / 2 : anchor === "right" ? x2 + width : x2;
432
+ label = {
433
+ text: annotation.label,
434
+ x: baseX + labelDelta.dx,
435
+ y: y2 + labelDelta.dy,
436
+ style,
437
+ visible: true
438
+ };
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
+ };
451
+ }
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
- annotation.label = {
552
- ...annotation.label,
553
- x: newX,
554
- y: newY,
555
- connector: newConnector
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 cumulativeValue = 0;
816
+ let categoryTotal = 0;
822
817
  for (const row of rows) {
823
- const groupKey = String(row[colorField] ?? "");
824
- const value2 = Number(row[valueField] ?? 0);
825
- if (!Number.isFinite(value2) || value2 <= 0) continue;
826
- const color2 = getColor(scales, groupKey);
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}, ${groupKey}: ${formatBarValue(value2)}`
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 groupKey = String(row[colorField] ?? "");
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(groupKey) ?? 0;
873
- const color2 = getColor(scales, groupKey);
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}, ${groupKey}: ${formatBarValue(value2)}`
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 groupKey = String(row[colorField] ?? "");
905
- const color2 = getColor(scales, groupKey);
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}, ${groupKey}: ${formatBarValue(value2)}`
910
+ label: `${category}, ${groupKey2}: ${formatBarValue(value2)}`
910
911
  };
911
912
  marks.push({
912
913
  type: "rect",
@@ -967,11 +968,17 @@ function computeSimpleBars(data, valueField, categoryField, xScale, yScale, band
967
968
 
968
969
  // src/charts/bar/labels.ts
969
970
  import {
971
+ abbreviateNumber as abbreviateNumber2,
970
972
  buildD3Formatter,
971
973
  estimateTextWidth as estimateTextWidth2,
974
+ formatNumber as formatNumber2,
972
975
  getRepresentativeColor,
973
976
  resolveCollisions
974
977
  } from "@opendata-ai/openchart-core";
978
+ function formatBarValue2(value2) {
979
+ if (Math.abs(value2) >= 1e3) return abbreviateNumber2(value2);
980
+ return formatNumber2(value2);
981
+ }
975
982
  var SUFFIX_MULTIPLIERS = {
976
983
  K: 1e3,
977
984
  M: 1e6,
@@ -979,7 +986,7 @@ var SUFFIX_MULTIPLIERS = {
979
986
  T: 1e12
980
987
  };
981
988
  function parseDisplayNumber(raw) {
982
- const trimmed = raw.trim();
989
+ const trimmed = raw.trim().replace(/\u2212/g, "-");
983
990
  if (!trimmed) return NaN;
984
991
  const last = trimmed[trimmed.length - 1].toUpperCase();
985
992
  const multiplier = SUFFIX_MULTIPLIERS[last];
@@ -988,27 +995,39 @@ function parseDisplayNumber(raw) {
988
995
  const n = Number(numPart);
989
996
  return Number.isNaN(n) ? NaN : n * multiplier;
990
997
  }
998
+ if (last === "%") {
999
+ return Number(trimmed.slice(0, -1).replace(/,/g, ""));
1000
+ }
991
1001
  return Number(trimmed.replace(/,/g, ""));
992
1002
  }
993
1003
  var LABEL_FONT_SIZE = 11;
994
1004
  var LABEL_FONT_WEIGHT = 600;
995
1005
  var LABEL_PADDING = 6;
996
1006
  var MIN_WIDTH_FOR_INSIDE_LABEL = 40;
997
- function computeBarLabels(marks, _chartArea, density = "auto", labelFormat, labelPrefix) {
1007
+ function computeBarLabels(marks, _chartArea, density = "auto", labelFormat, labelPrefix, valueField) {
998
1008
  if (density === "none") return [];
999
1009
  const targetMarks = density === "endpoints" && marks.length > 1 ? [marks[0], marks[marks.length - 1]] : marks;
1000
1010
  const candidates = [];
1001
1011
  const fitsInSegment = [];
1002
1012
  const formatter = buildD3Formatter(labelFormat);
1003
1013
  for (const mark of targetMarks) {
1004
- const ariaLabel = mark.aria.label;
1005
- const lastColon = ariaLabel.lastIndexOf(":");
1006
- const rawValue = lastColon >= 0 ? ariaLabel.slice(lastColon + 1).trim() : "";
1007
- if (!rawValue) continue;
1008
- let valuePart = rawValue;
1009
- if (formatter) {
1010
- const num = parseDisplayNumber(rawValue);
1011
- if (!Number.isNaN(num)) valuePart = formatter(num);
1014
+ let valuePart;
1015
+ const rawNum = valueField != null ? Number(mark.data[valueField]) : NaN;
1016
+ if (formatter && Number.isFinite(rawNum)) {
1017
+ valuePart = formatter(rawNum);
1018
+ } else if (Number.isFinite(rawNum)) {
1019
+ valuePart = formatBarValue2(rawNum);
1020
+ } else {
1021
+ const ariaLabel = mark.aria.label;
1022
+ const lastColon = ariaLabel.lastIndexOf(":");
1023
+ const rawValue = lastColon >= 0 ? ariaLabel.slice(lastColon + 1).trim() : "";
1024
+ if (!rawValue) continue;
1025
+ if (formatter) {
1026
+ const num = parseDisplayNumber(rawValue);
1027
+ valuePart = !Number.isNaN(num) ? formatter(num) : rawValue;
1028
+ } else {
1029
+ valuePart = rawValue;
1030
+ }
1012
1031
  }
1013
1032
  if (labelPrefix) valuePart = labelPrefix + valuePart;
1014
1033
  const textWidth = estimateTextWidth2(valuePart, LABEL_FONT_SIZE, LABEL_FONT_WEIGHT);
@@ -1093,12 +1112,14 @@ function computeBarLabels(marks, _chartArea, density = "auto", labelFormat, labe
1093
1112
  // src/charts/bar/index.ts
1094
1113
  var barRenderer = (spec, scales, chartArea, strategy, _theme) => {
1095
1114
  const marks = computeBarMarks(spec, scales, chartArea, strategy);
1115
+ const valueField = spec.encoding?.x && "field" in spec.encoding.x ? spec.encoding.x.field : void 0;
1096
1116
  const labels = computeBarLabels(
1097
1117
  marks,
1098
1118
  chartArea,
1099
1119
  spec.labels.density,
1100
1120
  spec.labels.format,
1101
- spec.labels.prefix
1121
+ spec.labels.prefix,
1122
+ valueField
1102
1123
  );
1103
1124
  for (let i = 0; i < marks.length && i < labels.length; i++) {
1104
1125
  marks[i].label = labels[i];
@@ -1107,11 +1128,11 @@ var barRenderer = (spec, scales, chartArea, strategy, _theme) => {
1107
1128
  };
1108
1129
 
1109
1130
  // src/charts/column/compute.ts
1110
- import { abbreviateNumber as abbreviateNumber2, formatNumber as formatNumber2, isGradientDef as isGradientDef2 } from "@opendata-ai/openchart-core";
1131
+ import { abbreviateNumber as abbreviateNumber3, formatNumber as formatNumber3, isGradientDef as isGradientDef2 } from "@opendata-ai/openchart-core";
1111
1132
  var MIN_COLUMN_HEIGHT = 1;
1112
1133
  function formatColumnValue(value2) {
1113
- if (Math.abs(value2) >= 1e3) return abbreviateNumber2(value2);
1114
- return formatNumber2(value2);
1134
+ if (Math.abs(value2) >= 1e3) return abbreviateNumber3(value2);
1135
+ return formatNumber3(value2);
1115
1136
  }
1116
1137
  function computeColumnMarks(spec, scales, _chartArea, _strategy) {
1117
1138
  const encoding = spec.encoding;
@@ -1149,6 +1170,7 @@ function computeColumnMarks(spec, scales, _chartArea, _strategy) {
1149
1170
  scales
1150
1171
  );
1151
1172
  }
1173
+ const stackMode = yChannel.stack === "normalize" ? "normalize" : yChannel.stack === "center" ? "center" : "zero";
1152
1174
  return computeStackedColumns(
1153
1175
  spec.data,
1154
1176
  xChannel.field,
@@ -1158,7 +1180,8 @@ function computeColumnMarks(spec, scales, _chartArea, _strategy) {
1158
1180
  yScale,
1159
1181
  bandwidth,
1160
1182
  baseline,
1161
- scales
1183
+ scales,
1184
+ stackMode
1162
1185
  );
1163
1186
  }
1164
1187
  return computeColoredColumns(
@@ -1236,13 +1259,13 @@ function computeColoredColumns(data, categoryField, valueField, colorField, xSca
1236
1259
  if (!Number.isFinite(value2)) continue;
1237
1260
  const bandX = xScale(category);
1238
1261
  if (bandX === void 0) continue;
1239
- const groupKey = String(row[colorField] ?? "");
1240
- const color2 = getColor(scales, groupKey);
1262
+ const groupKey2 = String(row[colorField] ?? "");
1263
+ const color2 = getColor(scales, groupKey2);
1241
1264
  const yPos = yScale(value2);
1242
1265
  const columnHeight = Math.max(Math.abs(baseline - yPos), MIN_COLUMN_HEIGHT);
1243
1266
  const y2 = value2 >= 0 ? yPos : baseline;
1244
1267
  const aria = {
1245
- label: `${category}, ${groupKey}: ${formatColumnValue(value2)}`
1268
+ label: `${category}, ${groupKey2}: ${formatColumnValue(value2)}`
1246
1269
  };
1247
1270
  marks.push({
1248
1271
  type: "rect",
@@ -1280,17 +1303,17 @@ function computeGroupedColumns(data, categoryField, valueField, colorField, xSca
1280
1303
  const bandX = xScale(category);
1281
1304
  if (bandX === void 0) continue;
1282
1305
  for (const row of rows) {
1283
- const groupKey = String(row[colorField] ?? "");
1306
+ const groupKey2 = String(row[colorField] ?? "");
1284
1307
  const value2 = Number(row[valueField] ?? 0);
1285
1308
  if (!Number.isFinite(value2)) continue;
1286
- const groupIndex = groupIndexMap.get(groupKey) ?? 0;
1287
- const color2 = getColor(scales, groupKey);
1309
+ const groupIndex = groupIndexMap.get(groupKey2) ?? 0;
1310
+ const color2 = getColor(scales, groupKey2);
1288
1311
  const yPos = yScale(value2);
1289
1312
  const columnHeight = Math.max(Math.abs(baseline - yPos), MIN_COLUMN_HEIGHT);
1290
1313
  const y2 = value2 >= 0 ? yPos : baseline;
1291
1314
  const subX = bandX + groupIndex * (subBandWidth + gap);
1292
1315
  const aria = {
1293
- label: `${category}, ${groupKey}: ${formatColumnValue(value2)}`
1316
+ label: `${category}, ${groupKey2}: ${formatColumnValue(value2)}`
1294
1317
  };
1295
1318
  marks.push({
1296
1319
  type: "rect",
@@ -1308,28 +1331,34 @@ function computeGroupedColumns(data, categoryField, valueField, colorField, xSca
1308
1331
  }
1309
1332
  return marks;
1310
1333
  }
1311
- function computeStackedColumns(data, categoryField, valueField, colorField, xScale, yScale, bandwidth, _baseline, scales) {
1334
+ function computeStackedColumns(data, categoryField, valueField, colorField, xScale, yScale, bandwidth, _baseline, scales, stackMode = "zero") {
1312
1335
  const marks = [];
1313
1336
  const categoryGroups = groupByField(data, categoryField);
1314
1337
  for (const [category, rows] of categoryGroups) {
1315
1338
  const bandX = xScale(category);
1316
1339
  if (bandX === void 0) continue;
1317
- let cumulativeValue = 0;
1340
+ let categoryTotal = 0;
1318
1341
  for (const row of rows) {
1319
- const groupKey = String(row[colorField] ?? "");
1320
- const value2 = Number(row[valueField] ?? 0);
1321
- if (!Number.isFinite(value2) || value2 <= 0) continue;
1322
- const color2 = getColor(scales, groupKey);
1342
+ const v = Number(row[valueField] ?? 0);
1343
+ if (Number.isFinite(v) && v > 0) categoryTotal += v;
1344
+ }
1345
+ let cumulativeValue = stackMode === "center" ? -categoryTotal / 2 : 0;
1346
+ for (const row of rows) {
1347
+ const groupKey2 = String(row[colorField] ?? "");
1348
+ const rawValue = Number(row[valueField] ?? 0);
1349
+ if (!Number.isFinite(rawValue) || rawValue <= 0) continue;
1350
+ const value2 = stackMode === "normalize" && categoryTotal > 0 ? rawValue / categoryTotal : rawValue;
1351
+ const color2 = getColor(scales, groupKey2);
1323
1352
  const yTop = yScale(cumulativeValue + value2);
1324
1353
  const yBottom = yScale(cumulativeValue);
1325
1354
  const columnHeight = Math.max(Math.abs(yBottom - yTop), MIN_COLUMN_HEIGHT);
1326
1355
  const aria = {
1327
- label: `${category}, ${groupKey}: ${formatColumnValue(value2)}`
1356
+ label: `${category}, ${groupKey2}: ${formatColumnValue(rawValue)}`
1328
1357
  };
1329
1358
  marks.push({
1330
1359
  type: "rect",
1331
1360
  x: bandX,
1332
- y: yTop,
1361
+ y: Math.min(yTop, yBottom),
1333
1362
  width: bandwidth,
1334
1363
  height: columnHeight,
1335
1364
  fill: color2,
@@ -1347,28 +1376,43 @@ function computeStackedColumns(data, categoryField, valueField, colorField, xSca
1347
1376
 
1348
1377
  // src/charts/column/labels.ts
1349
1378
  import {
1379
+ abbreviateNumber as abbreviateNumber4,
1350
1380
  buildD3Formatter as buildD3Formatter2,
1351
1381
  estimateTextWidth as estimateTextWidth3,
1382
+ formatNumber as formatNumber4,
1352
1383
  getRepresentativeColor as getRepresentativeColor2,
1353
1384
  resolveCollisions as resolveCollisions2
1354
1385
  } from "@opendata-ai/openchart-core";
1386
+ function formatColumnValue2(value2) {
1387
+ if (Math.abs(value2) >= 1e3) return abbreviateNumber4(value2);
1388
+ return formatNumber4(value2);
1389
+ }
1355
1390
  var LABEL_FONT_SIZE2 = 10;
1356
1391
  var LABEL_FONT_WEIGHT2 = 600;
1357
1392
  var LABEL_OFFSET_Y = 6;
1358
- function computeColumnLabels(marks, _chartArea, density = "auto", labelFormat, labelPrefix) {
1393
+ function computeColumnLabels(marks, _chartArea, density = "auto", labelFormat, labelPrefix, valueField) {
1359
1394
  if (density === "none") return [];
1360
1395
  const targetMarks = density === "endpoints" && marks.length > 1 ? [marks[0], marks[marks.length - 1]] : marks;
1361
1396
  const formatter = buildD3Formatter2(labelFormat);
1362
1397
  const candidates = [];
1363
1398
  for (const mark of targetMarks) {
1364
- const ariaLabel = mark.aria.label;
1365
- const lastColon = ariaLabel.lastIndexOf(":");
1366
- const rawValue = lastColon >= 0 ? ariaLabel.slice(lastColon + 1).trim() : "";
1367
- if (!rawValue) continue;
1368
- let valuePart = rawValue;
1369
- if (formatter) {
1370
- const num = Number(rawValue.replace(/[^0-9.-]/g, ""));
1371
- if (!Number.isNaN(num)) valuePart = formatter(num);
1399
+ let valuePart;
1400
+ const rawNum = valueField != null ? Number(mark.data[valueField]) : NaN;
1401
+ if (formatter && Number.isFinite(rawNum)) {
1402
+ valuePart = formatter(rawNum);
1403
+ } else if (Number.isFinite(rawNum)) {
1404
+ valuePart = formatColumnValue2(rawNum);
1405
+ } else {
1406
+ const ariaLabel = mark.aria.label;
1407
+ const lastColon = ariaLabel.lastIndexOf(":");
1408
+ const rawValue = lastColon >= 0 ? ariaLabel.slice(lastColon + 1).trim() : "";
1409
+ if (!rawValue) continue;
1410
+ if (formatter) {
1411
+ const num = Number(rawValue.replace(/[^0-9.-]/g, ""));
1412
+ valuePart = !Number.isNaN(num) ? formatter(num) : rawValue;
1413
+ } else {
1414
+ valuePart = rawValue;
1415
+ }
1372
1416
  }
1373
1417
  if (labelPrefix) valuePart = labelPrefix + valuePart;
1374
1418
  const numericValue = parseFloat(valuePart);
@@ -1411,12 +1455,14 @@ function computeColumnLabels(marks, _chartArea, density = "auto", labelFormat, l
1411
1455
  // src/charts/column/index.ts
1412
1456
  var columnRenderer = (spec, scales, chartArea, strategy, _theme) => {
1413
1457
  const marks = computeColumnMarks(spec, scales, chartArea, strategy);
1458
+ const valueField = spec.encoding?.y && "field" in spec.encoding.y ? spec.encoding.y.field : void 0;
1414
1459
  const labels = computeColumnLabels(
1415
1460
  marks,
1416
1461
  chartArea,
1417
1462
  spec.labels.density,
1418
1463
  spec.labels.format,
1419
- spec.labels.prefix
1464
+ spec.labels.prefix,
1465
+ valueField
1420
1466
  );
1421
1467
  for (let i = 0; i < marks.length && i < labels.length; i++) {
1422
1468
  marks[i].label = labels[i];
@@ -1574,22 +1620,42 @@ function computeLollipopMarks(data, valueField, categoryField, xScale, yScale, b
1574
1620
 
1575
1621
  // src/charts/dot/labels.ts
1576
1622
  import {
1623
+ abbreviateNumber as abbreviateNumber5,
1624
+ buildD3Formatter as buildD3Formatter3,
1577
1625
  estimateTextWidth as estimateTextWidth4,
1626
+ formatNumber as formatNumber5,
1578
1627
  getRepresentativeColor as getRepresentativeColor3,
1579
1628
  resolveCollisions as resolveCollisions3
1580
1629
  } from "@opendata-ai/openchart-core";
1630
+ function formatDotValue(value2) {
1631
+ if (Math.abs(value2) >= 1e3) return abbreviateNumber5(value2);
1632
+ return formatNumber5(value2);
1633
+ }
1581
1634
  var LABEL_FONT_SIZE3 = 11;
1582
1635
  var LABEL_FONT_WEIGHT3 = 600;
1583
1636
  var LABEL_OFFSET_X = 10;
1584
- function computeDotLabels(marks, _chartArea, density = "auto", labelPrefix) {
1637
+ function computeDotLabels(marks, _chartArea, density = "auto", labelPrefix, labelFormat, valueField) {
1585
1638
  if (density === "none") return [];
1586
1639
  const targetMarks = density === "endpoints" && marks.length > 1 ? [marks[0], marks[marks.length - 1]] : marks;
1640
+ const formatter = buildD3Formatter3(labelFormat);
1587
1641
  const candidates = [];
1588
1642
  for (const mark of targetMarks) {
1589
- const ariaLabel = mark.aria.label;
1590
- const lastColon = ariaLabel.lastIndexOf(":");
1591
- let valuePart = lastColon >= 0 ? ariaLabel.slice(lastColon + 1).trim() : "";
1592
- if (!valuePart) continue;
1643
+ let valuePart;
1644
+ const rawNum = valueField != null ? Number(mark.data[valueField]) : NaN;
1645
+ if (formatter && Number.isFinite(rawNum)) {
1646
+ valuePart = formatter(rawNum);
1647
+ } else if (Number.isFinite(rawNum)) {
1648
+ valuePart = formatDotValue(rawNum);
1649
+ } else {
1650
+ const ariaLabel = mark.aria.label;
1651
+ const lastColon = ariaLabel.lastIndexOf(":");
1652
+ valuePart = lastColon >= 0 ? ariaLabel.slice(lastColon + 1).trim() : "";
1653
+ if (!valuePart) continue;
1654
+ if (formatter) {
1655
+ const num = Number(valuePart.replace(/[^0-9.-]/g, ""));
1656
+ if (!Number.isNaN(num)) valuePart = formatter(num);
1657
+ }
1658
+ }
1593
1659
  if (labelPrefix) valuePart = labelPrefix + valuePart;
1594
1660
  const textWidth = estimateTextWidth4(valuePart, LABEL_FONT_SIZE3, LABEL_FONT_WEIGHT3);
1595
1661
  const textHeight = LABEL_FONT_SIZE3 * 1.2;
@@ -1628,7 +1694,15 @@ function computeDotLabels(marks, _chartArea, density = "auto", labelPrefix) {
1628
1694
  var dotRenderer = (spec, scales, chartArea, strategy, _theme) => {
1629
1695
  const marks = computeDotMarks(spec, scales, chartArea, strategy);
1630
1696
  const pointMarks = marks.filter((m) => m.type === "point");
1631
- const labels = computeDotLabels(pointMarks, chartArea, spec.labels.density, spec.labels.prefix);
1697
+ const valueField = spec.encoding?.x && "field" in spec.encoding.x ? spec.encoding.x.field : void 0;
1698
+ const labels = computeDotLabels(
1699
+ pointMarks,
1700
+ chartArea,
1701
+ spec.labels.density,
1702
+ spec.labels.prefix,
1703
+ spec.labels.format,
1704
+ valueField
1705
+ );
1632
1706
  let labelIdx = 0;
1633
1707
  for (const mark of marks) {
1634
1708
  if (mark.type === "point" && labelIdx < labels.length) {
@@ -1639,6 +1713,9 @@ var dotRenderer = (spec, scales, chartArea, strategy, _theme) => {
1639
1713
  return marks;
1640
1714
  };
1641
1715
 
1716
+ // src/charts/line/index.ts
1717
+ import { getRepresentativeColor as getRepresentativeColor6 } from "@opendata-ai/openchart-core";
1718
+
1642
1719
  // src/charts/line/area.ts
1643
1720
  import { getRepresentativeColor as getRepresentativeColor4 } from "@opendata-ai/openchart-core";
1644
1721
 
@@ -2521,6 +2598,26 @@ function stack_default() {
2521
2598
  return stack;
2522
2599
  }
2523
2600
 
2601
+ // ../../node_modules/.bun/d3-shape@3.2.0/node_modules/d3-shape/src/offset/expand.js
2602
+ function expand_default(series, order) {
2603
+ if (!((n = series.length) > 0)) return;
2604
+ for (var i, n, j = 0, m = series[0].length, y2; j < m; ++j) {
2605
+ for (y2 = i = 0; i < n; ++i) y2 += series[i][j][1] || 0;
2606
+ if (y2) for (i = 0; i < n; ++i) series[i][j][1] /= y2;
2607
+ }
2608
+ none_default(series, order);
2609
+ }
2610
+
2611
+ // ../../node_modules/.bun/d3-shape@3.2.0/node_modules/d3-shape/src/offset/silhouette.js
2612
+ function silhouette_default(series, order) {
2613
+ if (!((n = series.length) > 0)) return;
2614
+ for (var j = 0, s0 = series[order[0]], n, m = s0.length; j < m; ++j) {
2615
+ for (var i = 0, y2 = 0; i < n; ++i) y2 += series[i][j][1] || 0;
2616
+ s0[j][1] += s0[j][0] = -y2 / 2;
2617
+ }
2618
+ none_default(series, order);
2619
+ }
2620
+
2524
2621
  // src/charts/line/curves.ts
2525
2622
  var CURVE_MAP = {
2526
2623
  linear: linear_default,
@@ -2565,7 +2662,7 @@ function computeSingleArea(spec, scales, _chartArea) {
2565
2662
  const marks = [];
2566
2663
  for (const [seriesKey, rows] of groups) {
2567
2664
  const color2 = getColor(scales, seriesKey);
2568
- const sortedRows = sortByField(rows, xChannel.field);
2665
+ const sortedRows = xChannel.type === "nominal" || xChannel.type === "ordinal" ? rows : sortByField(rows, xChannel.field);
2569
2666
  const validPoints = [];
2570
2667
  for (const row of sortedRows) {
2571
2668
  const xVal = scaleValue(scales.x.scale, scales.x.type, row[xChannel.field]);
@@ -2614,7 +2711,7 @@ function computeStackedArea(spec, scales, chartArea) {
2614
2711
  if (!xChannel || !yChannel || !scales.x || !scales.y || !colorField) {
2615
2712
  return computeSingleArea(spec, scales, chartArea);
2616
2713
  }
2617
- const sortedData = sortByField(spec.data, xChannel.field);
2714
+ const sortedData = xChannel.type === "nominal" || xChannel.type === "ordinal" ? spec.data : sortByField(spec.data, xChannel.field);
2618
2715
  const seriesKeys = /* @__PURE__ */ new Set();
2619
2716
  const xValueSet = /* @__PURE__ */ new Set();
2620
2717
  const rowsByXSeries = /* @__PURE__ */ new Map();
@@ -2648,7 +2745,9 @@ function computeStackedArea(spec, scales, chartArea) {
2648
2745
  }
2649
2746
  return pivot;
2650
2747
  });
2651
- const stackGenerator = stack_default().keys(keys).order(none_default2).offset(none_default);
2748
+ const stackProp = yChannel.stack;
2749
+ const offsetFn = stackProp === "normalize" ? expand_default : stackProp === "center" ? silhouette_default : none_default;
2750
+ const stackGenerator = stack_default().keys(keys).order(none_default2).offset(offsetFn);
2652
2751
  const stackedData = stackGenerator(pivotData);
2653
2752
  const yScale = scales.y.scale;
2654
2753
  const marks = [];
@@ -2729,7 +2828,7 @@ function computeLineMarks(spec, scales, _chartArea, _strategy) {
2729
2828
  for (const [seriesKey, rows] of groups) {
2730
2829
  const color2 = isSequentialColor ? getSequentialColor(scales, _getMidValue(rows, sequentialColorField)) : getColor(scales, seriesKey);
2731
2830
  const strokeColor = getRepresentativeColor5(color2);
2732
- const sortedRows = sortByField(rows, xChannel.field);
2831
+ const sortedRows = xChannel.type === "nominal" || xChannel.type === "ordinal" ? rows : sortByField(rows, xChannel.field);
2733
2832
  const pointsWithData = [];
2734
2833
  const segments = [];
2735
2834
  let currentSegment = [];
@@ -2917,9 +3016,24 @@ var lineRenderer = (spec, scales, chartArea, strategy, _theme) => {
2917
3016
  };
2918
3017
  var areaRenderer = (spec, scales, chartArea, strategy, _theme) => {
2919
3018
  const areas = computeAreaMarks(spec, scales, chartArea);
2920
- const lines = computeLineMarks(spec, scales, chartArea, strategy);
3019
+ const encoding = spec.encoding;
3020
+ const hasColor = !!(encoding.color && "field" in encoding.color);
3021
+ const lines = hasColor ? linesFromAreas(areas) : computeLineMarks(spec, scales, chartArea, strategy);
2921
3022
  return [...areas, ...lines];
2922
3023
  };
3024
+ function linesFromAreas(areas) {
3025
+ return areas.map((a) => ({
3026
+ type: "line",
3027
+ points: a.topPoints,
3028
+ path: a.topPath,
3029
+ stroke: getRepresentativeColor6(a.fill),
3030
+ strokeWidth: a.strokeWidth ?? 1,
3031
+ seriesKey: a.seriesKey,
3032
+ data: a.data,
3033
+ dataPoints: a.dataPoints,
3034
+ aria: { label: `${a.seriesKey ?? "Series"}: line with ${a.topPoints.length} data points` }
3035
+ }));
3036
+ }
2923
3037
 
2924
3038
  // src/charts/pie/compute.ts
2925
3039
  import { isConditionalDef, isGradientDef as isGradientDef3 } from "@opendata-ai/openchart-core";
@@ -3129,26 +3243,158 @@ function computePieLabels(marks, _chartArea, density = "auto", _textFill = "#333
3129
3243
  };
3130
3244
  }
3131
3245
  }
3132
- return resolved;
3246
+ return resolved;
3247
+ }
3248
+
3249
+ // src/charts/pie/index.ts
3250
+ var pieRenderer = (spec, scales, chartArea, strategy, theme) => {
3251
+ const marks = computePieMarks(spec, scales, chartArea, strategy, false);
3252
+ const labels = computePieLabels(marks, chartArea, spec.labels.density, theme.colors.text);
3253
+ for (let i = 0; i < marks.length && i < labels.length; i++) {
3254
+ marks[i].label = labels[i];
3255
+ }
3256
+ return marks;
3257
+ };
3258
+ var donutRenderer = (spec, scales, chartArea, strategy, theme) => {
3259
+ const marks = computePieMarks(spec, scales, chartArea, strategy, true);
3260
+ const labels = computePieLabels(marks, chartArea, spec.labels.density, theme.colors.text);
3261
+ for (let i = 0; i < marks.length && i < labels.length; i++) {
3262
+ marks[i].label = labels[i];
3263
+ }
3264
+ return marks;
3265
+ };
3266
+
3267
+ // src/charts/post-process.ts
3268
+ function computeMarkObstacles(marks, scales) {
3269
+ if (scales.y?.type === "band") {
3270
+ return computeBandRowObstacles(marks, scales);
3271
+ }
3272
+ const obstacles = [];
3273
+ for (const mark of marks) {
3274
+ if (mark.type === "rect") {
3275
+ const rm = mark;
3276
+ obstacles.push({ x: rm.x, y: rm.y, width: rm.width, height: rm.height });
3277
+ } else if (mark.type === "point") {
3278
+ const pm = mark;
3279
+ obstacles.push({
3280
+ x: pm.cx - pm.r,
3281
+ y: pm.cy - pm.r,
3282
+ width: pm.r * 2,
3283
+ height: pm.r * 2
3284
+ });
3285
+ }
3286
+ }
3287
+ return obstacles;
3288
+ }
3289
+ function computeBandRowObstacles(marks, scales) {
3290
+ const rows = /* @__PURE__ */ new Map();
3291
+ for (const mark of marks) {
3292
+ let cy;
3293
+ let left2;
3294
+ let right2;
3295
+ if (mark.type === "point") {
3296
+ const pm = mark;
3297
+ cy = pm.cy;
3298
+ left2 = pm.cx - pm.r;
3299
+ right2 = pm.cx + pm.r;
3300
+ } else if (mark.type === "rect") {
3301
+ const rm = mark;
3302
+ cy = rm.y + rm.height / 2;
3303
+ left2 = rm.x;
3304
+ right2 = rm.x + rm.width;
3305
+ } else {
3306
+ continue;
3307
+ }
3308
+ const key = Math.round(cy);
3309
+ const existing = rows.get(key);
3310
+ if (existing) {
3311
+ existing.minX = Math.min(existing.minX, left2);
3312
+ existing.maxX = Math.max(existing.maxX, right2);
3313
+ } else {
3314
+ rows.set(key, { minX: left2, maxX: right2, bandY: cy });
3315
+ }
3316
+ }
3317
+ const bandScale = scales.y.scale;
3318
+ const bandwidth = bandScale.bandwidth?.() ?? 0;
3319
+ if (bandwidth === 0) return [];
3320
+ const obstacles = [];
3321
+ for (const { minX, maxX, bandY } of rows.values()) {
3322
+ obstacles.push({
3323
+ x: minX,
3324
+ y: bandY - bandwidth / 2,
3325
+ width: maxX - minX,
3326
+ height: bandwidth
3327
+ });
3328
+ }
3329
+ return obstacles;
3330
+ }
3331
+ function resolveRendererKey(markType, encoding, markDef) {
3332
+ if (markType === "bar") {
3333
+ const xType = encoding.x?.type;
3334
+ const yType = encoding.y?.type;
3335
+ const isVertical = (xType === "nominal" || xType === "ordinal" || xType === "temporal") && yType === "quantitative";
3336
+ if (isVertical) {
3337
+ return "bar:vertical";
3338
+ }
3339
+ } else if (markType === "arc") {
3340
+ const innerRadius = markDef.innerRadius;
3341
+ if (innerRadius && innerRadius > 0) {
3342
+ return "arc:donut";
3343
+ }
3344
+ }
3345
+ return markType;
3133
3346
  }
3134
-
3135
- // src/charts/pie/index.ts
3136
- var pieRenderer = (spec, scales, chartArea, strategy, theme) => {
3137
- const marks = computePieMarks(spec, scales, chartArea, strategy, false);
3138
- const labels = computePieLabels(marks, chartArea, spec.labels.density, theme.colors.text);
3139
- for (let i = 0; i < marks.length && i < labels.length; i++) {
3140
- marks[i].label = labels[i];
3347
+ function getMarkPrimaryValue(mark) {
3348
+ switch (mark.type) {
3349
+ case "rect":
3350
+ return mark.height;
3351
+ // bar height is the primary value encoding
3352
+ case "point":
3353
+ return mark.cy;
3354
+ // y position for scatter
3355
+ case "arc":
3356
+ return mark.endAngle - mark.startAngle;
3357
+ // arc angle extent
3358
+ case "line":
3359
+ case "area":
3360
+ return 0;
3361
+ // series marks don't have individual values
3362
+ default:
3363
+ return 0;
3141
3364
  }
3142
- return marks;
3143
- };
3144
- var donutRenderer = (spec, scales, chartArea, strategy, theme) => {
3145
- const marks = computePieMarks(spec, scales, chartArea, strategy, true);
3146
- const labels = computePieLabels(marks, chartArea, spec.labels.density, theme.colors.text);
3147
- for (let i = 0; i < marks.length && i < labels.length; i++) {
3148
- marks[i].label = labels[i];
3365
+ }
3366
+ function assignAnimationIndices(marks, animation) {
3367
+ if (!animation?.enabled) return;
3368
+ if (animation.staggerOrder === "value") {
3369
+ const indexed = marks.map((m, i) => ({ mark: m, idx: i }));
3370
+ indexed.sort((a, b) => {
3371
+ const av = getMarkPrimaryValue(a.mark);
3372
+ const bv = getMarkPrimaryValue(b.mark);
3373
+ return av - bv;
3374
+ });
3375
+ for (let i = 0; i < indexed.length; i++) {
3376
+ const m = indexed[i].mark;
3377
+ if (m.type === "rect" && m.stackGroup) continue;
3378
+ m.animationIndex = i;
3379
+ }
3149
3380
  }
3150
- return marks;
3151
- };
3381
+ const groupIndexMap = /* @__PURE__ */ new Map();
3382
+ const groupStackPos = /* @__PURE__ */ new Map();
3383
+ let nextGroupIndex = 0;
3384
+ for (const mark of marks) {
3385
+ if (mark.type === "rect" && mark.stackGroup) {
3386
+ const rect = mark;
3387
+ const group = rect.stackGroup;
3388
+ if (!groupIndexMap.has(group)) {
3389
+ groupIndexMap.set(group, nextGroupIndex++);
3390
+ }
3391
+ rect.animationIndex = groupIndexMap.get(group);
3392
+ const pos = groupStackPos.get(group) ?? 0;
3393
+ rect.stackPos = pos;
3394
+ groupStackPos.set(group, pos + 1);
3395
+ }
3396
+ }
3397
+ }
3152
3398
 
3153
3399
  // src/charts/registry.ts
3154
3400
  var renderers = /* @__PURE__ */ new Map();
@@ -3163,7 +3409,7 @@ function clearRenderers() {
3163
3409
  }
3164
3410
 
3165
3411
  // src/charts/rule/index.ts
3166
- import { getRepresentativeColor as getRepresentativeColor6 } from "@opendata-ai/openchart-core";
3412
+ import { getRepresentativeColor as getRepresentativeColor7 } from "@opendata-ai/openchart-core";
3167
3413
  function computeRuleMarks(spec, scales, chartArea) {
3168
3414
  const encoding = spec.encoding;
3169
3415
  const xChannel = encoding.x;
@@ -3206,7 +3452,7 @@ function computeRuleMarks(spec, scales, chartArea) {
3206
3452
  const y2Val = scaleValue(scales.y.scale, scales.y.type, row[y2Channel.field]);
3207
3453
  if (y2Val != null) y2 = y2Val;
3208
3454
  }
3209
- const color2 = getRepresentativeColor6(
3455
+ const color2 = getRepresentativeColor7(
3210
3456
  colorField ? getColor(scales, String(row[colorField] ?? "__default__")) : getColor(scales, "__default__")
3211
3457
  );
3212
3458
  const strokeDashEncoding = encoding.strokeDash && "field" in encoding.strokeDash ? encoding.strokeDash : void 0;
@@ -6015,7 +6261,7 @@ var scatterRenderer = (spec, scales, chartArea, strategy, _theme) => {
6015
6261
  };
6016
6262
 
6017
6263
  // src/charts/text/index.ts
6018
- import { getRepresentativeColor as getRepresentativeColor7 } from "@opendata-ai/openchart-core";
6264
+ import { getRepresentativeColor as getRepresentativeColor8 } from "@opendata-ai/openchart-core";
6019
6265
  function computeTextMarks(spec, scales) {
6020
6266
  const encoding = spec.encoding;
6021
6267
  const xChannel = encoding.x;
@@ -6041,7 +6287,7 @@ function computeTextMarks(spec, scales) {
6041
6287
  }
6042
6288
  const text = String(row[textChannel.field] ?? "");
6043
6289
  if (!text) continue;
6044
- const color2 = getRepresentativeColor7(
6290
+ const color2 = getRepresentativeColor8(
6045
6291
  colorField ? getColor(scales, String(row[colorField] ?? "__default__")) : getColor(scales, "__default__")
6046
6292
  );
6047
6293
  const fontSize = sizeEncoding ? Math.max(8, Math.min(48, Number(row[sizeEncoding.field]) || 12)) : 12;
@@ -6068,7 +6314,7 @@ var textRenderer = (spec, scales, _chartArea, _strategy, _theme) => {
6068
6314
  };
6069
6315
 
6070
6316
  // src/charts/tick/index.ts
6071
- import { getRepresentativeColor as getRepresentativeColor8 } from "@opendata-ai/openchart-core";
6317
+ import { getRepresentativeColor as getRepresentativeColor9 } from "@opendata-ai/openchart-core";
6072
6318
  var DEFAULT_TICK_LENGTH = 18;
6073
6319
  function computeTickMarks(spec, scales, _chartArea) {
6074
6320
  const encoding = spec.encoding;
@@ -6084,7 +6330,7 @@ function computeTickMarks(spec, scales, _chartArea) {
6084
6330
  const xVal = scaleValue(scales.x.scale, scales.x.type, row[xChannel.field]);
6085
6331
  const yVal = scaleValue(scales.y.scale, scales.y.type, row[yChannel.field]);
6086
6332
  if (xVal == null || yVal == null) continue;
6087
- const color2 = getRepresentativeColor8(
6333
+ const color2 = getRepresentativeColor9(
6088
6334
  colorField ? getColor(scales, String(row[colorField] ?? "__default__")) : getColor(scales, "__default__")
6089
6335
  );
6090
6336
  const aria = {
@@ -6135,7 +6381,7 @@ function normalizeChrome(chrome) {
6135
6381
  };
6136
6382
  }
6137
6383
  function inferFieldType(data, field) {
6138
- const sampleSize = Math.min(10, data.length);
6384
+ const sampleSize = Math.min(50, data.length);
6139
6385
  let numericCount = 0;
6140
6386
  let dateCount = 0;
6141
6387
  let totalNonNull = 0;
@@ -7515,16 +7761,16 @@ var DEFAULT_COLLISION_PADDING = 5;
7515
7761
 
7516
7762
  // src/layout/axes.ts
7517
7763
  import {
7518
- abbreviateNumber as abbreviateNumber3,
7519
- buildD3Formatter as buildD3Formatter3,
7764
+ abbreviateNumber as abbreviateNumber6,
7765
+ buildD3Formatter as buildD3Formatter4,
7520
7766
  buildTemporalFormatter,
7521
7767
  estimateTextWidth as estimateTextWidth7,
7522
7768
  formatDate,
7523
- formatNumber as formatNumber3
7769
+ formatNumber as formatNumber6
7524
7770
  } from "@opendata-ai/openchart-core";
7525
7771
  var TICK_COUNTS = {
7526
- full: 8,
7527
- reduced: 5,
7772
+ full: 10,
7773
+ reduced: 7,
7528
7774
  minimal: 3
7529
7775
  };
7530
7776
  var HEIGHT_MINIMAL_THRESHOLD = 120;
@@ -7637,11 +7883,11 @@ function formatTickLabel(value2, resolvedScale) {
7637
7883
  if (NUMERIC_SCALE_TYPES.has(resolvedScale.type)) {
7638
7884
  const num = value2;
7639
7885
  if (formatStr) {
7640
- const fmt = buildD3Formatter3(formatStr);
7886
+ const fmt = buildD3Formatter4(formatStr);
7641
7887
  if (fmt) return fmt(num);
7642
7888
  }
7643
- if (Math.abs(num) >= 1e3) return abbreviateNumber3(num);
7644
- return formatNumber3(num);
7889
+ if (Math.abs(num) >= 1e3) return abbreviateNumber6(num);
7890
+ return formatNumber6(num);
7645
7891
  }
7646
7892
  return String(value2);
7647
7893
  }
@@ -7714,7 +7960,7 @@ function computeAxes(scales, chartArea, strategy, theme, measureText) {
7714
7960
  }));
7715
7961
  const shouldThin = scales.x.type !== "band" && !axisConfig?.tickCount && !axisConfig?.values;
7716
7962
  const ticks2 = shouldThin ? thinTicksUntilFit(allTicks, fontSize, fontWeight, measureText) : allTicks;
7717
- let tickAngle = axisConfig?.labelAngle ?? axisConfig?.tickAngle;
7963
+ let tickAngle = axisConfig?.labelAngle;
7718
7964
  if (tickAngle === void 0 && scales.x.type === "band" && ticks2.length > 1) {
7719
7965
  const bandwidth = scales.x.scale.bandwidth();
7720
7966
  let maxLabelWidth = 0;
@@ -7726,7 +7972,7 @@ function computeAxes(scales, chartArea, strategy, theme, measureText) {
7726
7972
  tickAngle = -45;
7727
7973
  }
7728
7974
  }
7729
- const axisTitle = axisConfig?.title ?? axisConfig?.label;
7975
+ const axisTitle = axisConfig?.title;
7730
7976
  result.x = {
7731
7977
  ticks: ticks2,
7732
7978
  gridlines: axisConfig?.grid ? gridlines : [],
@@ -7756,14 +8002,14 @@ function computeAxes(scales, chartArea, strategy, theme, measureText) {
7756
8002
  } else {
7757
8003
  allTicks = continuousTicks(scales.y, yDensity);
7758
8004
  }
7759
- const gridlines = allTicks.map((t) => ({
8005
+ const shouldThin = scales.y.type !== "band" && !axisConfig?.tickCount && !axisConfig?.values;
8006
+ const ticks2 = shouldThin ? thinTicksUntilFit(allTicks, fontSize, fontWeight, measureText) : allTicks;
8007
+ const gridlines = ticks2.map((t) => ({
7760
8008
  position: t.position,
7761
8009
  major: true
7762
8010
  }));
7763
- const shouldThin = scales.y.type !== "band" && !axisConfig?.tickCount && !axisConfig?.values;
7764
- const ticks2 = shouldThin ? thinTicksUntilFit(allTicks, fontSize, fontWeight, measureText) : allTicks;
7765
- const axisTitle = axisConfig?.title ?? axisConfig?.label;
7766
- const tickAngle = axisConfig?.labelAngle ?? axisConfig?.tickAngle;
8011
+ const axisTitle = axisConfig?.title;
8012
+ const tickAngle = axisConfig?.labelAngle;
7767
8013
  result.y = {
7768
8014
  ticks: ticks2,
7769
8015
  // Y-axis gridlines are shown by default (standard editorial practice)
@@ -7825,8 +8071,8 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
7825
8071
  const isRadial = spec.markType === "arc";
7826
8072
  const encoding = spec.encoding;
7827
8073
  const xAxis = encoding.x?.axis;
7828
- const hasXAxisLabel = !!xAxis?.label;
7829
- const xTickAngle = xAxis?.tickAngle;
8074
+ const hasXAxisLabel = !!xAxis?.title;
8075
+ const xTickAngle = xAxis?.labelAngle;
7830
8076
  let xAxisHeight;
7831
8077
  if (isRadial) {
7832
8078
  xAxisHeight = 0;
@@ -8051,6 +8297,12 @@ function uniqueStrings(values) {
8051
8297
  }
8052
8298
  return result;
8053
8299
  }
8300
+ function applyCategoricalSort(values, sort) {
8301
+ if (!sort) return values;
8302
+ const sorted = [...values].sort((a, b) => a.localeCompare(b, void 0, { numeric: true }));
8303
+ if (sort === "descending") sorted.reverse();
8304
+ return sorted;
8305
+ }
8054
8306
  function applyContinuousConfig(scale, channel) {
8055
8307
  if (channel.scale?.clamp) {
8056
8308
  scale.clamp(true);
@@ -8064,7 +8316,7 @@ function buildTimeScale(channel, data, rangeStart, rangeEnd) {
8064
8316
  const values = parseDates(fieldValues(data, channel.field));
8065
8317
  const domain = channel.scale?.domain ? [new Date(channel.scale.domain[0]), new Date(channel.scale.domain[1])] : extent(values);
8066
8318
  const scale = time().domain(domain).range([rangeStart, rangeEnd]);
8067
- if (channel.scale?.nice !== false) {
8319
+ if (!channel.scale?.domain && channel.scale?.nice === true) {
8068
8320
  scale.nice();
8069
8321
  }
8070
8322
  applyContinuousConfig(scale, channel);
@@ -8074,7 +8326,7 @@ function buildUtcScale(channel, data, rangeStart, rangeEnd) {
8074
8326
  const values = parseDates(fieldValues(data, channel.field));
8075
8327
  const domain = channel.scale?.domain ? [new Date(channel.scale.domain[0]), new Date(channel.scale.domain[1])] : extent(values);
8076
8328
  const scale = utcTime().domain(domain).range([rangeStart, rangeEnd]);
8077
- if (channel.scale?.nice !== false) {
8329
+ if (!channel.scale?.domain && channel.scale?.nice === true) {
8078
8330
  scale.nice();
8079
8331
  }
8080
8332
  applyContinuousConfig(scale, channel);
@@ -8097,7 +8349,7 @@ function buildLinearScale(channel, data, rangeStart, rangeEnd) {
8097
8349
  }
8098
8350
  }
8099
8351
  const scale = linear2().domain([domainMin, domainMax]).range([rangeStart, rangeEnd]);
8100
- if (channel.scale?.nice !== false) {
8352
+ if (!channel.scale?.domain && channel.scale?.nice !== false) {
8101
8353
  scale.nice();
8102
8354
  }
8103
8355
  applyContinuousConfig(scale, channel);
@@ -8111,7 +8363,7 @@ function buildLogScale(channel, data, rangeStart, rangeEnd) {
8111
8363
  if (channel.scale?.base !== void 0) {
8112
8364
  scale.base(channel.scale.base);
8113
8365
  }
8114
- if (channel.scale?.nice !== false) {
8366
+ if (!channel.scale?.domain && channel.scale?.nice !== false) {
8115
8367
  scale.nice();
8116
8368
  }
8117
8369
  applyContinuousConfig(scale, channel);
@@ -8135,7 +8387,7 @@ function buildPowScale(channel, data, rangeStart, rangeEnd) {
8135
8387
  if (channel.scale?.exponent !== void 0) {
8136
8388
  scale.exponent(channel.scale.exponent);
8137
8389
  }
8138
- if (channel.scale?.nice !== false) {
8390
+ if (!channel.scale?.domain && channel.scale?.nice !== false) {
8139
8391
  scale.nice();
8140
8392
  }
8141
8393
  applyContinuousConfig(scale, channel);
@@ -8156,7 +8408,7 @@ function buildSqrtScale(channel, data, rangeStart, rangeEnd) {
8156
8408
  }
8157
8409
  }
8158
8410
  const scale = sqrt2().domain([domainMin, domainMax]).range([rangeStart, rangeEnd]);
8159
- if (channel.scale?.nice !== false) {
8411
+ if (!channel.scale?.domain && channel.scale?.nice !== false) {
8160
8412
  scale.nice();
8161
8413
  }
8162
8414
  applyContinuousConfig(scale, channel);
@@ -8180,7 +8432,7 @@ function buildSymlogScale(channel, data, rangeStart, rangeEnd) {
8180
8432
  if (channel.scale?.constant !== void 0) {
8181
8433
  scale.constant(channel.scale.constant);
8182
8434
  }
8183
- if (channel.scale?.nice !== false) {
8435
+ if (!channel.scale?.domain && channel.scale?.nice !== false) {
8184
8436
  scale.nice();
8185
8437
  }
8186
8438
  applyContinuousConfig(scale, channel);
@@ -8212,7 +8464,7 @@ function evenRange(start, end, count) {
8212
8464
  return Array.from({ length: count }, (_, i) => start + step * i);
8213
8465
  }
8214
8466
  function buildBandScale(channel, data, rangeStart, rangeEnd) {
8215
- const values = channel.scale?.domain ? channel.scale.domain : uniqueStrings(fieldValues(data, channel.field));
8467
+ const values = channel.scale?.domain ? channel.scale.domain : applyCategoricalSort(uniqueStrings(fieldValues(data, channel.field)), channel.sort);
8216
8468
  const padding = channel.scale?.padding ?? 0.35;
8217
8469
  const scale = band().domain(values).range([rangeStart, rangeEnd]).padding(padding);
8218
8470
  if (channel.scale?.paddingInner !== void 0) {
@@ -8228,7 +8480,7 @@ function buildBandScale(channel, data, rangeStart, rangeEnd) {
8228
8480
  return { scale, type: "band", channel };
8229
8481
  }
8230
8482
  function buildPointScale(channel, data, rangeStart, rangeEnd) {
8231
- const values = channel.scale?.domain ? channel.scale.domain : uniqueStrings(fieldValues(data, channel.field));
8483
+ const values = channel.scale?.domain ? channel.scale.domain : applyCategoricalSort(uniqueStrings(fieldValues(data, channel.field)), channel.sort);
8232
8484
  const padding = channel.scale?.padding ?? 0.5;
8233
8485
  const scale = point4().domain(values).range([rangeStart, rangeEnd]).padding(padding);
8234
8486
  if (channel.scale?.reverse) {
@@ -8238,7 +8490,8 @@ function buildPointScale(channel, data, rangeStart, rangeEnd) {
8238
8490
  return { scale, type: "point", channel };
8239
8491
  }
8240
8492
  function buildOrdinalColorScale(channel, data, palette) {
8241
- const values = uniqueStrings(fieldValues(data, channel.field));
8493
+ const explicitDomain = channel.scale?.domain;
8494
+ const values = explicitDomain ? explicitDomain.map(String) : applyCategoricalSort(uniqueStrings(fieldValues(data, channel.field)), channel.sort);
8242
8495
  const scale = ordinal().domain(values).range(palette);
8243
8496
  return { scale, type: "ordinal", channel };
8244
8497
  }
@@ -8316,25 +8569,49 @@ function computeScales(spec, chartArea, data) {
8316
8569
  }
8317
8570
  if (encoding.x) {
8318
8571
  let xData = data;
8572
+ let xChannel = encoding.x;
8319
8573
  const xStackDisabled = encoding.x.stack === null || encoding.x.stack === false;
8320
8574
  if (spec.markType === "bar" && encoding.color && encoding.x.type === "quantitative" && !xStackDisabled) {
8321
- const yField = encoding.y?.field;
8322
- const xField = encoding.x.field;
8323
- if (yField) {
8324
- const sums = /* @__PURE__ */ new Map();
8325
- for (const row of data) {
8326
- const cat = String(row[yField] ?? "");
8327
- const val = Number(row[xField] ?? 0);
8328
- if (Number.isFinite(val) && val > 0) {
8329
- sums.set(cat, (sums.get(cat) ?? 0) + val);
8575
+ if (encoding.x.stack === "normalize") {
8576
+ xChannel = { ...encoding.x, scale: { ...encoding.x.scale, domain: [0, 1], nice: false } };
8577
+ } else if (encoding.x.stack === "center") {
8578
+ const yField = encoding.y?.field;
8579
+ const xField = encoding.x.field;
8580
+ if (yField) {
8581
+ const sums = /* @__PURE__ */ new Map();
8582
+ for (const row of data) {
8583
+ const cat = String(row[yField] ?? "");
8584
+ const val = Number(row[xField] ?? 0);
8585
+ if (Number.isFinite(val) && val > 0) {
8586
+ sums.set(cat, (sums.get(cat) ?? 0) + val);
8587
+ }
8588
+ }
8589
+ const maxSum = Math.max(...sums.values(), 0);
8590
+ const half = maxSum / 2;
8591
+ xChannel = {
8592
+ ...encoding.x,
8593
+ scale: { ...encoding.x.scale, domain: [-half, half], zero: true }
8594
+ };
8595
+ }
8596
+ } else {
8597
+ const yField = encoding.y?.field;
8598
+ const xField = encoding.x.field;
8599
+ if (yField) {
8600
+ const sums = /* @__PURE__ */ new Map();
8601
+ for (const row of data) {
8602
+ const cat = String(row[yField] ?? "");
8603
+ const val = Number(row[xField] ?? 0);
8604
+ if (Number.isFinite(val) && val > 0) {
8605
+ sums.set(cat, (sums.get(cat) ?? 0) + val);
8606
+ }
8330
8607
  }
8608
+ const maxSum = Math.max(...sums.values(), 0);
8609
+ xData = [...data, { [xField]: maxSum }];
8331
8610
  }
8332
- const maxSum = Math.max(...sums.values(), 0);
8333
- xData = [...data, { [xField]: maxSum }];
8334
8611
  }
8335
8612
  }
8336
8613
  result.x = buildPositionalScale(
8337
- encoding.x,
8614
+ xChannel,
8338
8615
  xData,
8339
8616
  chartArea.x,
8340
8617
  chartArea.x + chartArea.width,
@@ -8344,26 +8621,50 @@ function computeScales(spec, chartArea, data) {
8344
8621
  }
8345
8622
  if (encoding.y) {
8346
8623
  let yData = data;
8624
+ let yChannel = encoding.y;
8347
8625
  const isVerticalBar = spec.markType === "bar" && (encoding.x?.type === "nominal" || encoding.x?.type === "ordinal") && encoding.y.type === "quantitative";
8348
8626
  const yStackDisabled = encoding.y.stack === null || encoding.y.stack === false;
8349
8627
  if ((isVerticalBar || spec.markType === "area") && encoding.color && encoding.y.type === "quantitative" && !yStackDisabled) {
8350
- const xField = encoding.x?.field;
8351
- const yField = encoding.y.field;
8352
- if (xField) {
8353
- const sums = /* @__PURE__ */ new Map();
8354
- for (const row of data) {
8355
- const cat = String(row[xField] ?? "");
8356
- const val = Number(row[yField] ?? 0);
8357
- if (Number.isFinite(val) && val > 0) {
8358
- sums.set(cat, (sums.get(cat) ?? 0) + val);
8628
+ if (encoding.y.stack === "normalize") {
8629
+ yChannel = { ...encoding.y, scale: { ...encoding.y.scale, domain: [0, 1], nice: false } };
8630
+ } else if (encoding.y.stack === "center") {
8631
+ const xField = encoding.x?.field;
8632
+ const yField = encoding.y.field;
8633
+ if (xField) {
8634
+ const sums = /* @__PURE__ */ new Map();
8635
+ for (const row of data) {
8636
+ const cat = String(row[xField] ?? "");
8637
+ const val = Number(row[yField] ?? 0);
8638
+ if (Number.isFinite(val) && val > 0) {
8639
+ sums.set(cat, (sums.get(cat) ?? 0) + val);
8640
+ }
8641
+ }
8642
+ const maxSum = Math.max(...sums.values(), 0);
8643
+ const half = maxSum / 2;
8644
+ yChannel = {
8645
+ ...encoding.y,
8646
+ scale: { ...encoding.y.scale, domain: [-half, half], zero: true }
8647
+ };
8648
+ }
8649
+ } else {
8650
+ const xField = encoding.x?.field;
8651
+ const yField = encoding.y.field;
8652
+ if (xField) {
8653
+ const sums = /* @__PURE__ */ new Map();
8654
+ for (const row of data) {
8655
+ const cat = String(row[xField] ?? "");
8656
+ const val = Number(row[yField] ?? 0);
8657
+ if (Number.isFinite(val) && val > 0) {
8658
+ sums.set(cat, (sums.get(cat) ?? 0) + val);
8659
+ }
8359
8660
  }
8661
+ const maxSum = Math.max(...sums.values(), 0);
8662
+ yData = [...data, { [yField]: maxSum }];
8360
8663
  }
8361
- const maxSum = Math.max(...sums.values(), 0);
8362
- yData = [...data, { [yField]: maxSum }];
8363
8664
  }
8364
8665
  }
8365
8666
  result.y = buildPositionalScale(
8366
- encoding.y,
8667
+ yChannel,
8367
8668
  yData,
8368
8669
  chartArea.y + chartArea.height,
8369
8670
  chartArea.y,
@@ -8517,7 +8818,7 @@ function computeLegend(spec, strategy, theme, chartArea, watermark = true) {
8517
8818
  1,
8518
8819
  Math.floor((maxLegendHeight - LEGEND_PADDING * 2) / (entryHeight + 4))
8519
8820
  );
8520
- const maxEntries = spec.legend?.symbolLimit != null ? Math.min(Math.max(1, spec.legend.symbolLimit), maxFromSpace) : maxFromSpace;
8821
+ const maxEntries = spec.legend?.symbolLimit != null ? Math.max(1, spec.legend.symbolLimit) : maxFromSpace;
8521
8822
  if (entries.length > maxEntries) {
8522
8823
  entries = truncateEntries(entries, maxEntries);
8523
8824
  }
@@ -8592,10 +8893,10 @@ function computeLegend(spec, strategy, theme, chartArea, watermark = true) {
8592
8893
  // src/sankey/compile-sankey.ts
8593
8894
  import {
8594
8895
  adaptTheme as adaptTheme2,
8595
- buildD3Formatter as buildD3Formatter4,
8896
+ buildD3Formatter as buildD3Formatter5,
8596
8897
  computeChrome as computeChrome3,
8597
8898
  estimateTextWidth as estimateTextWidth10,
8598
- formatNumber as formatNumber4,
8899
+ formatNumber as formatNumber7,
8599
8900
  resolveTheme as resolveTheme2
8600
8901
  } from "@opendata-ai/openchart-core";
8601
8902
 
@@ -9449,10 +9750,10 @@ function buildSankeyLegend(nodeColorMap, colorField, data, sourceField, targetFi
9449
9750
  }
9450
9751
  function formatFlowValue(value2, valueFormat) {
9451
9752
  if (valueFormat) {
9452
- const fmt = buildD3Formatter4(valueFormat);
9753
+ const fmt = buildD3Formatter5(valueFormat);
9453
9754
  if (fmt) return fmt(value2);
9454
9755
  }
9455
- return formatNumber4(value2);
9756
+ return formatNumber7(value2);
9456
9757
  }
9457
9758
  function buildTooltipDescriptors(nodes, links, valueFormat) {
9458
9759
  const descriptors = /* @__PURE__ */ new Map();
@@ -9629,7 +9930,7 @@ function computeCategoryColors(data, column, theme, darkMode) {
9629
9930
  }
9630
9931
 
9631
9932
  // src/tables/format-cells.ts
9632
- import { buildD3Formatter as buildD3Formatter5, formatDate as formatDate2, formatNumber as formatNumber5 } from "@opendata-ai/openchart-core";
9933
+ import { buildD3Formatter as buildD3Formatter6, formatDate as formatDate2, formatNumber as formatNumber8 } from "@opendata-ai/openchart-core";
9633
9934
  function isNumericValue(value2) {
9634
9935
  if (typeof value2 === "number") return Number.isFinite(value2);
9635
9936
  return false;
@@ -9648,7 +9949,7 @@ function formatCell(value2, column) {
9648
9949
  };
9649
9950
  }
9650
9951
  if (column.format && isNumericValue(value2)) {
9651
- const formatter = buildD3Formatter5(column.format);
9952
+ const formatter = buildD3Formatter6(column.format);
9652
9953
  if (formatter) {
9653
9954
  return {
9654
9955
  value: value2,
@@ -9660,7 +9961,7 @@ function formatCell(value2, column) {
9660
9961
  if (isNumericValue(value2)) {
9661
9962
  return {
9662
9963
  value: value2,
9663
- formattedValue: formatNumber5(value2),
9964
+ formattedValue: formatNumber8(value2),
9664
9965
  style
9665
9966
  };
9666
9967
  }
@@ -9680,13 +9981,13 @@ function formatCell(value2, column) {
9680
9981
  function formatValueForSearch(value2, column) {
9681
9982
  if (value2 == null) return "";
9682
9983
  if (column.format && isNumericValue(value2)) {
9683
- const formatter = buildD3Formatter5(column.format);
9984
+ const formatter = buildD3Formatter6(column.format);
9684
9985
  if (formatter) {
9685
9986
  return formatter(value2);
9686
9987
  }
9687
9988
  }
9688
9989
  if (isNumericValue(value2)) {
9689
- return formatNumber5(value2);
9990
+ return formatNumber8(value2);
9690
9991
  }
9691
9992
  return String(value2);
9692
9993
  }
@@ -10161,8 +10462,8 @@ function compileTableLayout(spec, options, theme) {
10161
10462
  import {
10162
10463
  buildTemporalFormatter as buildTemporalFormatter2,
10163
10464
  formatDate as formatDate3,
10164
- formatNumber as formatNumber6,
10165
- getRepresentativeColor as getRepresentativeColor9
10465
+ formatNumber as formatNumber9,
10466
+ getRepresentativeColor as getRepresentativeColor10
10166
10467
  } from "@opendata-ai/openchart-core";
10167
10468
  function formatValue(value2, fieldType, format2) {
10168
10469
  if (value2 == null) return "";
@@ -10176,17 +10477,23 @@ function formatValue(value2, fieldType, format2) {
10176
10477
  try {
10177
10478
  return format(format2)(value2);
10178
10479
  } catch {
10179
- return formatNumber6(value2);
10480
+ return formatNumber9(value2);
10180
10481
  }
10181
10482
  }
10182
- return formatNumber6(value2);
10483
+ return formatNumber9(value2);
10183
10484
  }
10184
10485
  return String(value2);
10185
10486
  }
10487
+ function resolveLabel(ch) {
10488
+ return ch.title ?? ch.axis?.title ?? ch.field;
10489
+ }
10490
+ function resolveFormat(ch) {
10491
+ return ch.format ?? ch.axis?.format;
10492
+ }
10186
10493
  function buildExplicitTooltipFields(row, channels) {
10187
10494
  return channels.map((ch) => ({
10188
- label: ch.axis?.label ?? ch.field,
10189
- value: formatValue(row[ch.field], ch.type, ch.axis?.format)
10495
+ label: resolveLabel(ch),
10496
+ value: formatValue(row[ch.field], ch.type, resolveFormat(ch))
10190
10497
  }));
10191
10498
  }
10192
10499
  function buildFields(row, encoding, color2) {
@@ -10195,23 +10502,38 @@ function buildFields(row, encoding, color2) {
10195
10502
  return buildExplicitTooltipFields(row, channels);
10196
10503
  }
10197
10504
  const fields = [];
10198
- if (encoding.y) {
10505
+ if (encoding.color && "field" in encoding.color) {
10199
10506
  fields.push({
10200
- label: encoding.y.axis?.label ?? encoding.y.field,
10201
- value: formatValue(row[encoding.y.field], encoding.y.type, encoding.y.axis?.format),
10507
+ label: resolveLabel(encoding.color),
10508
+ value: formatValue(
10509
+ row[encoding.color.field],
10510
+ encoding.color.type,
10511
+ resolveFormat(encoding.color)
10512
+ ),
10202
10513
  color: color2
10203
10514
  });
10204
10515
  }
10516
+ if (encoding.y) {
10517
+ fields.push({
10518
+ label: resolveLabel(encoding.y),
10519
+ value: formatValue(row[encoding.y.field], encoding.y.type, resolveFormat(encoding.y)),
10520
+ color: encoding.color ? void 0 : color2
10521
+ });
10522
+ }
10205
10523
  if (encoding.x) {
10206
10524
  fields.push({
10207
- label: encoding.x.axis?.label ?? encoding.x.field,
10208
- value: formatValue(row[encoding.x.field], encoding.x.type, encoding.x.axis?.format)
10525
+ label: resolveLabel(encoding.x),
10526
+ value: formatValue(row[encoding.x.field], encoding.x.type, resolveFormat(encoding.x))
10209
10527
  });
10210
10528
  }
10211
10529
  if (encoding.size && "field" in encoding.size) {
10212
10530
  fields.push({
10213
- label: encoding.size.axis?.label ?? encoding.size.field,
10214
- value: formatValue(row[encoding.size.field], encoding.size.type, encoding.size.axis?.format)
10531
+ label: resolveLabel(encoding.size),
10532
+ value: formatValue(
10533
+ row[encoding.size.field],
10534
+ encoding.size.type,
10535
+ resolveFormat(encoding.size)
10536
+ )
10215
10537
  });
10216
10538
  }
10217
10539
  return fields;
@@ -10229,6 +10551,16 @@ function getTooltipTitle(row, encoding) {
10229
10551
  if (encoding.y?.type === "nominal" || encoding.y?.type === "ordinal") {
10230
10552
  return String(row[encoding.y.field] ?? "");
10231
10553
  }
10554
+ if (encoding.x?.type === "quantitative" && encoding.y?.type === "quantitative") {
10555
+ const encodedFields = new Set(
10556
+ [encoding.x, encoding.y, encoding.color, encoding.size, encoding.detail].filter((ch) => !!ch && "field" in ch).map((ch) => ch.field)
10557
+ );
10558
+ for (const [key, value2] of Object.entries(row)) {
10559
+ if (!encodedFields.has(key) && typeof value2 === "string") {
10560
+ return value2;
10561
+ }
10562
+ }
10563
+ }
10232
10564
  if (encoding.color && "field" in encoding.color) {
10233
10565
  return String(row[encoding.color.field] ?? "");
10234
10566
  }
@@ -10247,12 +10579,12 @@ function tooltipsForLine(mark, encoding, _markIndex) {
10247
10579
  }
10248
10580
  function tooltipsForPoint(mark, encoding, markIndex) {
10249
10581
  const title = getTooltipTitle(mark.data, encoding);
10250
- const fields = buildFields(mark.data, encoding, getRepresentativeColor9(mark.fill));
10582
+ const fields = buildFields(mark.data, encoding, getRepresentativeColor10(mark.fill));
10251
10583
  return [[`point-${markIndex}`, { title, fields }]];
10252
10584
  }
10253
10585
  function tooltipsForRect(mark, encoding, markIndex) {
10254
10586
  const title = getTooltipTitle(mark.data, encoding);
10255
- const fields = buildFields(mark.data, encoding, getRepresentativeColor9(mark.fill));
10587
+ const fields = buildFields(mark.data, encoding, getRepresentativeColor10(mark.fill));
10256
10588
  return [[`rect-${markIndex}`, { title, fields }]];
10257
10589
  }
10258
10590
  function tooltipsForArc(mark, encoding, markIndex) {
@@ -10264,15 +10596,15 @@ function tooltipsForArc(mark, encoding, markIndex) {
10264
10596
  if (encoding.y) {
10265
10597
  fields.push({
10266
10598
  label: categoryName,
10267
- value: formatValue(row[encoding.y.field], encoding.y.type, encoding.y.axis?.format),
10268
- color: getRepresentativeColor9(mark.fill)
10599
+ value: formatValue(row[encoding.y.field], encoding.y.type, resolveFormat(encoding.y)),
10600
+ color: getRepresentativeColor10(mark.fill)
10269
10601
  });
10270
10602
  }
10271
10603
  } else if (encoding.y) {
10272
10604
  fields.push({
10273
- label: encoding.y.field,
10274
- value: formatValue(row[encoding.y.field], encoding.y.type, encoding.y.axis?.format),
10275
- color: getRepresentativeColor9(mark.fill)
10605
+ label: resolveLabel(encoding.y),
10606
+ value: formatValue(row[encoding.y.field], encoding.y.type, resolveFormat(encoding.y)),
10607
+ color: getRepresentativeColor10(mark.fill)
10276
10608
  });
10277
10609
  }
10278
10610
  const title = colorEnc ? String(row[colorEnc.field] ?? "") : void 0;
@@ -10283,7 +10615,7 @@ function tooltipsForArea(mark, encoding, _markIndex) {
10283
10615
  for (const dp of mark.dataPoints) {
10284
10616
  dp.tooltip = {
10285
10617
  title: getTooltipTitle(dp.datum, encoding),
10286
- fields: buildFields(dp.datum, encoding, getRepresentativeColor9(mark.fill))
10618
+ fields: buildFields(dp.datum, encoding, getRepresentativeColor10(mark.fill))
10287
10619
  };
10288
10620
  }
10289
10621
  }
@@ -10319,6 +10651,93 @@ function computeTooltipDescriptors(spec, marks) {
10319
10651
  return descriptors;
10320
10652
  }
10321
10653
 
10654
+ // src/transforms/aggregate.ts
10655
+ function computeAggregate(op, values) {
10656
+ if (values.length === 0) return 0;
10657
+ switch (op) {
10658
+ case "count":
10659
+ return values.length;
10660
+ case "sum":
10661
+ return values.reduce((a, b) => a + b, 0);
10662
+ case "mean": {
10663
+ const sum2 = values.reduce((a, b) => a + b, 0);
10664
+ return sum2 / values.length;
10665
+ }
10666
+ case "median": {
10667
+ const sorted = [...values].sort((a, b) => a - b);
10668
+ const mid = Math.floor(sorted.length / 2);
10669
+ return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
10670
+ }
10671
+ case "min":
10672
+ return Math.min(...values);
10673
+ case "max":
10674
+ return Math.max(...values);
10675
+ case "variance": {
10676
+ if (values.length < 2) return 0;
10677
+ const mean = values.reduce((a, b) => a + b, 0) / values.length;
10678
+ return values.reduce((a, v) => a + (v - mean) ** 2, 0) / values.length;
10679
+ }
10680
+ case "stdev": {
10681
+ if (values.length < 2) return 0;
10682
+ const m = values.reduce((a, b) => a + b, 0) / values.length;
10683
+ return Math.sqrt(values.reduce((a, v) => a + (v - m) ** 2, 0) / values.length);
10684
+ }
10685
+ case "q1": {
10686
+ const s = [...values].sort((a, b) => a - b);
10687
+ const i = (s.length - 1) * 0.25;
10688
+ const lo = Math.floor(i);
10689
+ const frac = i - lo;
10690
+ return s[lo] + frac * ((s[lo + 1] ?? s[lo]) - s[lo]);
10691
+ }
10692
+ case "q3": {
10693
+ const s = [...values].sort((a, b) => a - b);
10694
+ const i = (s.length - 1) * 0.75;
10695
+ const lo = Math.floor(i);
10696
+ const frac = i - lo;
10697
+ return s[lo] + frac * ((s[lo + 1] ?? s[lo]) - s[lo]);
10698
+ }
10699
+ default:
10700
+ return 0;
10701
+ }
10702
+ }
10703
+ function groupKey(row, groupby) {
10704
+ return groupby.map((f) => String(row[f] ?? "")).join("\0");
10705
+ }
10706
+ function runAggregate(data, transform) {
10707
+ const { aggregate, groupby } = transform;
10708
+ const groups = /* @__PURE__ */ new Map();
10709
+ for (const row of data) {
10710
+ const key = groupKey(row, groupby);
10711
+ const existing = groups.get(key);
10712
+ if (existing) {
10713
+ existing.push(row);
10714
+ } else {
10715
+ groups.set(key, [row]);
10716
+ }
10717
+ }
10718
+ const result = [];
10719
+ for (const rows of groups.values()) {
10720
+ const outRow = {};
10721
+ for (const field of groupby) {
10722
+ outRow[field] = rows[0][field];
10723
+ }
10724
+ for (const agg of aggregate) {
10725
+ if (agg.op === "distinct") {
10726
+ outRow[agg.as] = new Set(rows.map((r) => r[agg.field])).size;
10727
+ continue;
10728
+ }
10729
+ const values = rows.map((r) => {
10730
+ if (agg.op === "count") return 1;
10731
+ const v = Number(r[agg.field]);
10732
+ return Number.isFinite(v) ? v : NaN;
10733
+ }).filter((v) => !Number.isNaN(v));
10734
+ outRow[agg.as] = computeAggregate(agg.op, values);
10735
+ }
10736
+ result.push(outRow);
10737
+ }
10738
+ return result;
10739
+ }
10740
+
10322
10741
  // src/transforms/bin.ts
10323
10742
  function computeStep(extent2, maxbins, nice2) {
10324
10743
  const span = extent2[1] - extent2[0];
@@ -10415,6 +10834,30 @@ function runFilter(data, predicate) {
10415
10834
  return data.filter((datum) => evaluatePredicate(datum, predicate));
10416
10835
  }
10417
10836
 
10837
+ // src/transforms/fold.ts
10838
+ function runFold(data, transform) {
10839
+ const { fold } = transform;
10840
+ const [keyAs, valueAs] = transform.as ?? ["key", "value"];
10841
+ const foldSet = new Set(fold);
10842
+ const result = [];
10843
+ for (const row of data) {
10844
+ const base = {};
10845
+ for (const [k, v] of Object.entries(row)) {
10846
+ if (!foldSet.has(k)) {
10847
+ base[k] = v;
10848
+ }
10849
+ }
10850
+ for (const field of fold) {
10851
+ result.push({
10852
+ ...base,
10853
+ [keyAs]: field,
10854
+ [valueAs]: row[field]
10855
+ });
10856
+ }
10857
+ }
10858
+ return result;
10859
+ }
10860
+
10418
10861
  // src/transforms/timeunit.ts
10419
10862
  function extractTimeUnit(date2, unit2) {
10420
10863
  switch (unit2) {
@@ -10492,6 +10935,10 @@ function runTransforms(data, transforms) {
10492
10935
  result = runCalculate(result, transform);
10493
10936
  } else if ("timeUnit" in transform) {
10494
10937
  result = runTimeUnit(result, transform);
10938
+ } else if ("aggregate" in transform) {
10939
+ result = runAggregate(result, transform);
10940
+ } else if ("fold" in transform) {
10941
+ result = runFold(result, transform);
10495
10942
  }
10496
10943
  }
10497
10944
  return result;
@@ -10524,71 +10971,55 @@ var builtinRenderers = {
10524
10971
  for (const [type, renderer] of Object.entries(builtinRenderers)) {
10525
10972
  registerChartRenderer(type, renderer);
10526
10973
  }
10527
- function computeMarkObstacles(marks, scales) {
10528
- if (scales.y?.type === "band") {
10529
- return computeBandRowObstacles(marks, scales);
10530
- }
10531
- const obstacles = [];
10532
- for (const mark of marks) {
10533
- if (mark.type === "rect") {
10534
- const rm = mark;
10535
- obstacles.push({ x: rm.x, y: rm.y, width: rm.width, height: rm.height });
10536
- } else if (mark.type === "point") {
10537
- const pm = mark;
10538
- obstacles.push({
10539
- x: pm.cx - pm.r,
10540
- y: pm.cy - pm.r,
10541
- width: pm.r * 2,
10542
- height: pm.r * 2
10543
- });
10544
- }
10545
- }
10546
- return obstacles;
10547
- }
10548
- function computeBandRowObstacles(marks, scales) {
10549
- const rows = /* @__PURE__ */ new Map();
10550
- for (const mark of marks) {
10551
- let cy;
10552
- let left2;
10553
- let right2;
10554
- if (mark.type === "point") {
10555
- const pm = mark;
10556
- cy = pm.cy;
10557
- left2 = pm.cx - pm.r;
10558
- right2 = pm.cx + pm.r;
10559
- } else if (mark.type === "rect") {
10560
- const rm = mark;
10561
- cy = rm.y + rm.height / 2;
10562
- left2 = rm.x;
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 });
10974
+ function expandEncodingSugar(spec) {
10975
+ const encoding = spec.encoding;
10976
+ if (!encoding) return spec;
10977
+ const generatedTransforms = [];
10978
+ const updatedEncoding = { ...encoding };
10979
+ let changed = false;
10980
+ for (const channel of Object.keys(encoding)) {
10981
+ const ch = encoding[channel];
10982
+ if (!ch || !ch.field) continue;
10983
+ if (ch.bin != null && ch.bin !== false) {
10984
+ const field = ch.field;
10985
+ const outputField = `bin_${field}`;
10986
+ const binTransform = {
10987
+ bin: ch.bin === true ? true : ch.bin,
10988
+ field,
10989
+ as: outputField
10990
+ };
10991
+ generatedTransforms.push(binTransform);
10992
+ const { bin: _bin, ...rest } = ch;
10993
+ updatedEncoding[channel] = { ...rest, field: outputField };
10994
+ changed = true;
10995
+ }
10996
+ const current = updatedEncoding[channel] ?? ch;
10997
+ if (current.timeUnit) {
10998
+ const field = current.field;
10999
+ const unit2 = current.timeUnit;
11000
+ const outputField = `${unit2}_${field}`;
11001
+ const timeUnitTransform = {
11002
+ timeUnit: unit2,
11003
+ field,
11004
+ as: outputField
11005
+ };
11006
+ generatedTransforms.push(timeUnitTransform);
11007
+ const { timeUnit: _tu, ...rest } = current;
11008
+ updatedEncoding[channel] = { ...rest, field: outputField };
11009
+ changed = true;
10574
11010
  }
10575
11011
  }
10576
- const bandScale = scales.y.scale;
10577
- const bandwidth = bandScale.bandwidth?.() ?? 0;
10578
- if (bandwidth === 0) return [];
10579
- const obstacles = [];
10580
- for (const { minX, maxX, bandY } of rows.values()) {
10581
- obstacles.push({
10582
- x: minX,
10583
- y: bandY - bandwidth / 2,
10584
- width: maxX - minX,
10585
- height: bandwidth
10586
- });
10587
- }
10588
- return obstacles;
11012
+ if (!changed) return spec;
11013
+ const existingTransforms = spec.transform ?? [];
11014
+ return {
11015
+ ...spec,
11016
+ encoding: updatedEncoding,
11017
+ transform: [...generatedTransforms, ...existingTransforms]
11018
+ };
10589
11019
  }
10590
11020
  function compileChart(spec, options) {
10591
- const { spec: normalized } = compile(spec);
11021
+ const expandedSpec = spec && typeof spec === "object" && !Array.isArray(spec) ? expandEncodingSugar(spec) : spec;
11022
+ const { spec: normalized } = compile(expandedSpec);
10592
11023
  if ("type" in normalized && normalized.type === "table") {
10593
11024
  throw new Error("compileChart received a table spec. Use compileTable instead.");
10594
11025
  }
@@ -10599,16 +11030,16 @@ function compileChart(spec, options) {
10599
11030
  throw new Error("compileChart received a sankey spec. Use compileSankey instead.");
10600
11031
  }
10601
11032
  let chartSpec = normalized;
10602
- const rawWatermark = spec.watermark;
11033
+ const rawWatermark = expandedSpec.watermark;
10603
11034
  const watermark = rawWatermark !== void 0 ? chartSpec.watermark : options.watermark ?? true;
10604
- const rawTransforms = spec.transform;
11035
+ const rawTransforms = expandedSpec.transform;
10605
11036
  if (rawTransforms && rawTransforms.length > 0) {
10606
11037
  chartSpec = { ...chartSpec, data: runTransforms(chartSpec.data, rawTransforms) };
10607
11038
  }
10608
11039
  const breakpoint = getBreakpoint(options.width);
10609
11040
  const heightClass = getHeightClass(options.height);
10610
11041
  const strategy = getLayoutStrategy(breakpoint, heightClass);
10611
- const rawSpec = spec;
11042
+ const rawSpec = expandedSpec;
10612
11043
  const overrides = rawSpec.overrides;
10613
11044
  if (overrides?.[breakpoint]) {
10614
11045
  const bp = overrides[breakpoint];
@@ -10719,20 +11150,11 @@ function compileChart(spec, options) {
10719
11150
  if (!isRadial) {
10720
11151
  computeGridlines(axes, chartArea);
10721
11152
  }
10722
- let rendererKey = renderSpec.markType;
10723
- if (rendererKey === "bar") {
10724
- const xType = renderSpec.encoding.x?.type;
10725
- const yType = renderSpec.encoding.y?.type;
10726
- const isVertical = (xType === "nominal" || xType === "ordinal" || xType === "temporal") && yType === "quantitative";
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
- }
11153
+ const rendererKey = resolveRendererKey(
11154
+ renderSpec.markType,
11155
+ renderSpec.encoding,
11156
+ renderSpec.markDef
11157
+ );
10736
11158
  const renderer = getChartRenderer(rendererKey);
10737
11159
  const marks = renderer ? renderer(renderSpec, scales, chartArea, strategy, theme) : [];
10738
11160
  const obstacles = [];
@@ -10780,37 +11202,7 @@ function compileChart(spec, options) {
10780
11202
  },
10781
11203
  chartSpec.data
10782
11204
  );
10783
- if (resolvedAnimation?.enabled && resolvedAnimation.staggerOrder === "value") {
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
- }
11205
+ assignAnimationIndices(marks, resolvedAnimation);
10814
11206
  return {
10815
11207
  area: chartArea,
10816
11208
  chrome: dims.chrome,
@@ -10834,28 +11226,10 @@ function compileChart(spec, options) {
10834
11226
  height: options.height
10835
11227
  },
10836
11228
  animation: resolvedAnimation,
10837
- watermark
11229
+ watermark,
11230
+ measureText: options.measureText
10838
11231
  };
10839
11232
  }
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
11233
  function compileLayer(spec, options) {
10860
11234
  const leaves = flattenLayers(spec);
10861
11235
  if (leaves.length === 0) {