@opendata-ai/openchart-engine 6.11.0 → 6.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/index.d.ts +7 -0
  2. package/dist/index.js +944 -629
  3. package/dist/index.js.map +1 -1
  4. package/package.json +2 -2
  5. package/src/__test-fixtures__/specs.ts +3 -0
  6. package/src/__tests__/axes.test.ts +12 -30
  7. package/src/__tests__/compile-chart.test.ts +4 -4
  8. package/src/__tests__/dimensions.test.ts +2 -2
  9. package/src/__tests__/encoding-sugar.test.ts +389 -0
  10. package/src/annotations/collisions.ts +268 -0
  11. package/src/annotations/compute.ts +9 -912
  12. package/src/annotations/constants.ts +32 -0
  13. package/src/annotations/geometry.ts +167 -0
  14. package/src/annotations/position.ts +95 -0
  15. package/src/annotations/resolve-range.ts +98 -0
  16. package/src/annotations/resolve-refline.ts +148 -0
  17. package/src/annotations/resolve-text.ts +134 -0
  18. package/src/charts/__tests__/post-process.test.ts +258 -0
  19. package/src/charts/bar/__tests__/labels.test.ts +31 -0
  20. package/src/charts/bar/compute.ts +27 -6
  21. package/src/charts/bar/labels.ts +7 -1
  22. package/src/charts/column/__tests__/compute.test.ts +99 -0
  23. package/src/charts/column/compute.ts +27 -6
  24. package/src/charts/line/area.ts +19 -2
  25. package/src/charts/post-process.ts +215 -0
  26. package/src/compile.ts +113 -169
  27. package/src/compiler/__tests__/normalize.test.ts +110 -0
  28. package/src/compiler/normalize.ts +22 -3
  29. package/src/compiler/types.ts +4 -0
  30. package/src/graphs/compile-graph.ts +8 -0
  31. package/src/graphs/types.ts +2 -0
  32. package/src/layout/axes.ts +10 -13
  33. package/src/layout/dimensions.ts +6 -3
  34. package/src/layout/scales.ts +106 -29
  35. package/src/legend/compute.ts +3 -1
  36. package/src/sankey/compile-sankey.ts +12 -2
  37. package/src/sankey/types.ts +1 -0
  38. package/src/tables/compile-table.ts +5 -0
  39. package/src/tooltips/__tests__/compute.test.ts +188 -0
  40. package/src/tooltips/compute.ts +25 -11
  41. package/src/transforms/__tests__/aggregate.test.ts +159 -0
  42. package/src/transforms/__tests__/fold.test.ts +79 -0
  43. package/src/transforms/aggregate.ts +130 -0
  44. package/src/transforms/fold.ts +49 -0
  45. 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",
@@ -979,7 +980,7 @@ var SUFFIX_MULTIPLIERS = {
979
980
  T: 1e12
980
981
  };
981
982
  function parseDisplayNumber(raw) {
982
- const trimmed = raw.trim();
983
+ const trimmed = raw.trim().replace(/\u2212/g, "-");
983
984
  if (!trimmed) return NaN;
984
985
  const last = trimmed[trimmed.length - 1].toUpperCase();
985
986
  const multiplier = SUFFIX_MULTIPLIERS[last];
@@ -988,6 +989,9 @@ function parseDisplayNumber(raw) {
988
989
  const n = Number(numPart);
989
990
  return Number.isNaN(n) ? NaN : n * multiplier;
990
991
  }
992
+ if (last === "%") {
993
+ return Number(trimmed.slice(0, -1).replace(/,/g, ""));
994
+ }
991
995
  return Number(trimmed.replace(/,/g, ""));
992
996
  }
993
997
  var LABEL_FONT_SIZE = 11;
@@ -1149,6 +1153,7 @@ function computeColumnMarks(spec, scales, _chartArea, _strategy) {
1149
1153
  scales
1150
1154
  );
1151
1155
  }
1156
+ const stackMode = yChannel.stack === "normalize" ? "normalize" : yChannel.stack === "center" ? "center" : "zero";
1152
1157
  return computeStackedColumns(
1153
1158
  spec.data,
1154
1159
  xChannel.field,
@@ -1158,7 +1163,8 @@ function computeColumnMarks(spec, scales, _chartArea, _strategy) {
1158
1163
  yScale,
1159
1164
  bandwidth,
1160
1165
  baseline,
1161
- scales
1166
+ scales,
1167
+ stackMode
1162
1168
  );
1163
1169
  }
1164
1170
  return computeColoredColumns(
@@ -1236,13 +1242,13 @@ function computeColoredColumns(data, categoryField, valueField, colorField, xSca
1236
1242
  if (!Number.isFinite(value2)) continue;
1237
1243
  const bandX = xScale(category);
1238
1244
  if (bandX === void 0) continue;
1239
- const groupKey = String(row[colorField] ?? "");
1240
- const color2 = getColor(scales, groupKey);
1245
+ const groupKey2 = String(row[colorField] ?? "");
1246
+ const color2 = getColor(scales, groupKey2);
1241
1247
  const yPos = yScale(value2);
1242
1248
  const columnHeight = Math.max(Math.abs(baseline - yPos), MIN_COLUMN_HEIGHT);
1243
1249
  const y2 = value2 >= 0 ? yPos : baseline;
1244
1250
  const aria = {
1245
- label: `${category}, ${groupKey}: ${formatColumnValue(value2)}`
1251
+ label: `${category}, ${groupKey2}: ${formatColumnValue(value2)}`
1246
1252
  };
1247
1253
  marks.push({
1248
1254
  type: "rect",
@@ -1280,17 +1286,17 @@ function computeGroupedColumns(data, categoryField, valueField, colorField, xSca
1280
1286
  const bandX = xScale(category);
1281
1287
  if (bandX === void 0) continue;
1282
1288
  for (const row of rows) {
1283
- const groupKey = String(row[colorField] ?? "");
1289
+ const groupKey2 = String(row[colorField] ?? "");
1284
1290
  const value2 = Number(row[valueField] ?? 0);
1285
1291
  if (!Number.isFinite(value2)) continue;
1286
- const groupIndex = groupIndexMap.get(groupKey) ?? 0;
1287
- const color2 = getColor(scales, groupKey);
1292
+ const groupIndex = groupIndexMap.get(groupKey2) ?? 0;
1293
+ const color2 = getColor(scales, groupKey2);
1288
1294
  const yPos = yScale(value2);
1289
1295
  const columnHeight = Math.max(Math.abs(baseline - yPos), MIN_COLUMN_HEIGHT);
1290
1296
  const y2 = value2 >= 0 ? yPos : baseline;
1291
1297
  const subX = bandX + groupIndex * (subBandWidth + gap);
1292
1298
  const aria = {
1293
- label: `${category}, ${groupKey}: ${formatColumnValue(value2)}`
1299
+ label: `${category}, ${groupKey2}: ${formatColumnValue(value2)}`
1294
1300
  };
1295
1301
  marks.push({
1296
1302
  type: "rect",
@@ -1308,28 +1314,34 @@ function computeGroupedColumns(data, categoryField, valueField, colorField, xSca
1308
1314
  }
1309
1315
  return marks;
1310
1316
  }
1311
- function computeStackedColumns(data, categoryField, valueField, colorField, xScale, yScale, bandwidth, _baseline, scales) {
1317
+ function computeStackedColumns(data, categoryField, valueField, colorField, xScale, yScale, bandwidth, _baseline, scales, stackMode = "zero") {
1312
1318
  const marks = [];
1313
1319
  const categoryGroups = groupByField(data, categoryField);
1314
1320
  for (const [category, rows] of categoryGroups) {
1315
1321
  const bandX = xScale(category);
1316
1322
  if (bandX === void 0) continue;
1317
- let cumulativeValue = 0;
1323
+ let categoryTotal = 0;
1318
1324
  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);
1325
+ const v = Number(row[valueField] ?? 0);
1326
+ if (Number.isFinite(v) && v > 0) categoryTotal += v;
1327
+ }
1328
+ let cumulativeValue = stackMode === "center" ? -categoryTotal / 2 : 0;
1329
+ for (const row of rows) {
1330
+ const groupKey2 = String(row[colorField] ?? "");
1331
+ const rawValue = Number(row[valueField] ?? 0);
1332
+ if (!Number.isFinite(rawValue) || rawValue <= 0) continue;
1333
+ const value2 = stackMode === "normalize" && categoryTotal > 0 ? rawValue / categoryTotal : rawValue;
1334
+ const color2 = getColor(scales, groupKey2);
1323
1335
  const yTop = yScale(cumulativeValue + value2);
1324
1336
  const yBottom = yScale(cumulativeValue);
1325
1337
  const columnHeight = Math.max(Math.abs(yBottom - yTop), MIN_COLUMN_HEIGHT);
1326
1338
  const aria = {
1327
- label: `${category}, ${groupKey}: ${formatColumnValue(value2)}`
1339
+ label: `${category}, ${groupKey2}: ${formatColumnValue(rawValue)}`
1328
1340
  };
1329
1341
  marks.push({
1330
1342
  type: "rect",
1331
1343
  x: bandX,
1332
- y: yTop,
1344
+ y: Math.min(yTop, yBottom),
1333
1345
  width: bandwidth,
1334
1346
  height: columnHeight,
1335
1347
  fill: color2,
@@ -2521,6 +2533,26 @@ function stack_default() {
2521
2533
  return stack;
2522
2534
  }
2523
2535
 
2536
+ // ../../node_modules/.bun/d3-shape@3.2.0/node_modules/d3-shape/src/offset/expand.js
2537
+ function expand_default(series, order) {
2538
+ if (!((n = series.length) > 0)) return;
2539
+ for (var i, n, j = 0, m = series[0].length, y2; j < m; ++j) {
2540
+ for (y2 = i = 0; i < n; ++i) y2 += series[i][j][1] || 0;
2541
+ if (y2) for (i = 0; i < n; ++i) series[i][j][1] /= y2;
2542
+ }
2543
+ none_default(series, order);
2544
+ }
2545
+
2546
+ // ../../node_modules/.bun/d3-shape@3.2.0/node_modules/d3-shape/src/offset/silhouette.js
2547
+ function silhouette_default(series, order) {
2548
+ if (!((n = series.length) > 0)) return;
2549
+ for (var j = 0, s0 = series[order[0]], n, m = s0.length; j < m; ++j) {
2550
+ for (var i = 0, y2 = 0; i < n; ++i) y2 += series[i][j][1] || 0;
2551
+ s0[j][1] += s0[j][0] = -y2 / 2;
2552
+ }
2553
+ none_default(series, order);
2554
+ }
2555
+
2524
2556
  // src/charts/line/curves.ts
2525
2557
  var CURVE_MAP = {
2526
2558
  linear: linear_default,
@@ -2648,7 +2680,9 @@ function computeStackedArea(spec, scales, chartArea) {
2648
2680
  }
2649
2681
  return pivot;
2650
2682
  });
2651
- const stackGenerator = stack_default().keys(keys).order(none_default2).offset(none_default);
2683
+ const stackProp = yChannel.stack;
2684
+ const offsetFn = stackProp === "normalize" ? expand_default : stackProp === "center" ? silhouette_default : none_default;
2685
+ const stackGenerator = stack_default().keys(keys).order(none_default2).offset(offsetFn);
2652
2686
  const stackedData = stackGenerator(pivotData);
2653
2687
  const yScale = scales.y.scale;
2654
2688
  const marks = [];
@@ -3102,53 +3136,185 @@ function computePieLabels(marks, _chartArea, density = "auto", _textFill = "#333
3102
3136
  dominantBaseline: "central"
3103
3137
  }
3104
3138
  });
3105
- targetMarkIndices.push(mi);
3106
- }
3107
- if (candidates.length === 0) return [];
3108
- let resolved;
3109
- if (density === "all") {
3110
- resolved = candidates.map((c) => ({
3111
- text: c.text,
3112
- x: c.anchorX,
3113
- y: c.anchorY,
3114
- style: c.style,
3115
- visible: true
3116
- }));
3117
- } else {
3118
- resolved = resolveCollisions5(candidates);
3139
+ targetMarkIndices.push(mi);
3140
+ }
3141
+ if (candidates.length === 0) return [];
3142
+ let resolved;
3143
+ if (density === "all") {
3144
+ resolved = candidates.map((c) => ({
3145
+ text: c.text,
3146
+ x: c.anchorX,
3147
+ y: c.anchorY,
3148
+ style: c.style,
3149
+ visible: true
3150
+ }));
3151
+ } else {
3152
+ resolved = resolveCollisions5(candidates);
3153
+ }
3154
+ for (let i = 0; i < resolved.length && i < targetMarks.length; i++) {
3155
+ const label = resolved[i];
3156
+ const mark = targetMarks[i];
3157
+ if (label.visible) {
3158
+ label.connector = {
3159
+ from: { x: label.x, y: label.y },
3160
+ to: { x: mark.centroid.x, y: mark.centroid.y },
3161
+ stroke: _textFill,
3162
+ style: "straight"
3163
+ };
3164
+ }
3165
+ }
3166
+ return resolved;
3167
+ }
3168
+
3169
+ // src/charts/pie/index.ts
3170
+ var pieRenderer = (spec, scales, chartArea, strategy, theme) => {
3171
+ const marks = computePieMarks(spec, scales, chartArea, strategy, false);
3172
+ const labels = computePieLabels(marks, chartArea, spec.labels.density, theme.colors.text);
3173
+ for (let i = 0; i < marks.length && i < labels.length; i++) {
3174
+ marks[i].label = labels[i];
3175
+ }
3176
+ return marks;
3177
+ };
3178
+ var donutRenderer = (spec, scales, chartArea, strategy, theme) => {
3179
+ const marks = computePieMarks(spec, scales, chartArea, strategy, true);
3180
+ const labels = computePieLabels(marks, chartArea, spec.labels.density, theme.colors.text);
3181
+ for (let i = 0; i < marks.length && i < labels.length; i++) {
3182
+ marks[i].label = labels[i];
3183
+ }
3184
+ return marks;
3185
+ };
3186
+
3187
+ // src/charts/post-process.ts
3188
+ function computeMarkObstacles(marks, scales) {
3189
+ if (scales.y?.type === "band") {
3190
+ return computeBandRowObstacles(marks, scales);
3191
+ }
3192
+ const obstacles = [];
3193
+ for (const mark of marks) {
3194
+ if (mark.type === "rect") {
3195
+ const rm = mark;
3196
+ obstacles.push({ x: rm.x, y: rm.y, width: rm.width, height: rm.height });
3197
+ } else if (mark.type === "point") {
3198
+ const pm = mark;
3199
+ obstacles.push({
3200
+ x: pm.cx - pm.r,
3201
+ y: pm.cy - pm.r,
3202
+ width: pm.r * 2,
3203
+ height: pm.r * 2
3204
+ });
3205
+ }
3206
+ }
3207
+ return obstacles;
3208
+ }
3209
+ function computeBandRowObstacles(marks, scales) {
3210
+ const rows = /* @__PURE__ */ new Map();
3211
+ for (const mark of marks) {
3212
+ let cy;
3213
+ let left2;
3214
+ let right2;
3215
+ if (mark.type === "point") {
3216
+ const pm = mark;
3217
+ cy = pm.cy;
3218
+ left2 = pm.cx - pm.r;
3219
+ right2 = pm.cx + pm.r;
3220
+ } else if (mark.type === "rect") {
3221
+ const rm = mark;
3222
+ cy = rm.y + rm.height / 2;
3223
+ left2 = rm.x;
3224
+ right2 = rm.x + rm.width;
3225
+ } else {
3226
+ continue;
3227
+ }
3228
+ const key = Math.round(cy);
3229
+ const existing = rows.get(key);
3230
+ if (existing) {
3231
+ existing.minX = Math.min(existing.minX, left2);
3232
+ existing.maxX = Math.max(existing.maxX, right2);
3233
+ } else {
3234
+ rows.set(key, { minX: left2, maxX: right2, bandY: cy });
3235
+ }
3236
+ }
3237
+ const bandScale = scales.y.scale;
3238
+ const bandwidth = bandScale.bandwidth?.() ?? 0;
3239
+ if (bandwidth === 0) return [];
3240
+ const obstacles = [];
3241
+ for (const { minX, maxX, bandY } of rows.values()) {
3242
+ obstacles.push({
3243
+ x: minX,
3244
+ y: bandY - bandwidth / 2,
3245
+ width: maxX - minX,
3246
+ height: bandwidth
3247
+ });
3119
3248
  }
3120
- for (let i = 0; i < resolved.length && i < targetMarks.length; i++) {
3121
- const label = resolved[i];
3122
- const mark = targetMarks[i];
3123
- if (label.visible) {
3124
- label.connector = {
3125
- from: { x: label.x, y: label.y },
3126
- to: { x: mark.centroid.x, y: mark.centroid.y },
3127
- stroke: _textFill,
3128
- style: "straight"
3129
- };
3249
+ return obstacles;
3250
+ }
3251
+ function resolveRendererKey(markType, encoding, markDef) {
3252
+ if (markType === "bar") {
3253
+ const xType = encoding.x?.type;
3254
+ const yType = encoding.y?.type;
3255
+ const isVertical = (xType === "nominal" || xType === "ordinal" || xType === "temporal") && yType === "quantitative";
3256
+ if (isVertical) {
3257
+ return "bar:vertical";
3258
+ }
3259
+ } else if (markType === "arc") {
3260
+ const innerRadius = markDef.innerRadius;
3261
+ if (innerRadius && innerRadius > 0) {
3262
+ return "arc:donut";
3130
3263
  }
3131
3264
  }
3132
- return resolved;
3265
+ return markType;
3133
3266
  }
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];
3267
+ function getMarkPrimaryValue(mark) {
3268
+ switch (mark.type) {
3269
+ case "rect":
3270
+ return mark.height;
3271
+ // bar height is the primary value encoding
3272
+ case "point":
3273
+ return mark.cy;
3274
+ // y position for scatter
3275
+ case "arc":
3276
+ return mark.endAngle - mark.startAngle;
3277
+ // arc angle extent
3278
+ case "line":
3279
+ case "area":
3280
+ return 0;
3281
+ // series marks don't have individual values
3282
+ default:
3283
+ return 0;
3141
3284
  }
3142
- 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];
3285
+ }
3286
+ function assignAnimationIndices(marks, animation) {
3287
+ if (!animation?.enabled) return;
3288
+ if (animation.staggerOrder === "value") {
3289
+ const indexed = marks.map((m, i) => ({ mark: m, idx: i }));
3290
+ indexed.sort((a, b) => {
3291
+ const av = getMarkPrimaryValue(a.mark);
3292
+ const bv = getMarkPrimaryValue(b.mark);
3293
+ return av - bv;
3294
+ });
3295
+ for (let i = 0; i < indexed.length; i++) {
3296
+ const m = indexed[i].mark;
3297
+ if (m.type === "rect" && m.stackGroup) continue;
3298
+ m.animationIndex = i;
3299
+ }
3149
3300
  }
3150
- return marks;
3151
- };
3301
+ const groupIndexMap = /* @__PURE__ */ new Map();
3302
+ const groupStackPos = /* @__PURE__ */ new Map();
3303
+ let nextGroupIndex = 0;
3304
+ for (const mark of marks) {
3305
+ if (mark.type === "rect" && mark.stackGroup) {
3306
+ const rect = mark;
3307
+ const group = rect.stackGroup;
3308
+ if (!groupIndexMap.has(group)) {
3309
+ groupIndexMap.set(group, nextGroupIndex++);
3310
+ }
3311
+ rect.animationIndex = groupIndexMap.get(group);
3312
+ const pos = groupStackPos.get(group) ?? 0;
3313
+ rect.stackPos = pos;
3314
+ groupStackPos.set(group, pos + 1);
3315
+ }
3316
+ }
3317
+ }
3152
3318
 
3153
3319
  // src/charts/registry.ts
3154
3320
  var renderers = /* @__PURE__ */ new Map();
@@ -6135,7 +6301,7 @@ function normalizeChrome(chrome) {
6135
6301
  };
6136
6302
  }
6137
6303
  function inferFieldType(data, field) {
6138
- const sampleSize = Math.min(10, data.length);
6304
+ const sampleSize = Math.min(50, data.length);
6139
6305
  let numericCount = 0;
6140
6306
  let dateCount = 0;
6141
6307
  let totalNonNull = 0;
@@ -6244,7 +6410,8 @@ function normalizeChartSpec(spec, warnings) {
6244
6410
  theme: spec.theme ?? {},
6245
6411
  darkMode: spec.darkMode ?? "off",
6246
6412
  hiddenSeries: spec.hiddenSeries ?? [],
6247
- seriesStyles: spec.seriesStyles ?? {}
6413
+ seriesStyles: spec.seriesStyles ?? {},
6414
+ watermark: spec.watermark ?? true
6248
6415
  };
6249
6416
  }
6250
6417
  function normalizeTableSpec(spec, _warnings) {
@@ -6261,7 +6428,8 @@ function normalizeTableSpec(spec, _warnings) {
6261
6428
  stickyFirstColumn: spec.stickyFirstColumn ?? false,
6262
6429
  compact: spec.compact ?? false,
6263
6430
  responsive: spec.responsive ?? true,
6264
- animation: spec.animation
6431
+ animation: spec.animation,
6432
+ watermark: spec.watermark ?? true
6265
6433
  };
6266
6434
  }
6267
6435
  function normalizeSankeySpec(spec, _warnings) {
@@ -6281,7 +6449,8 @@ function normalizeSankeySpec(spec, _warnings) {
6281
6449
  darkMode: spec.darkMode ?? "off",
6282
6450
  animation: spec.animation,
6283
6451
  valueFormat: spec.valueFormat,
6284
- linkOpacity: spec.linkOpacity
6452
+ linkOpacity: spec.linkOpacity,
6453
+ watermark: spec.watermark ?? true
6285
6454
  };
6286
6455
  }
6287
6456
  function normalizeGraphSpec(spec, _warnings) {
@@ -6304,7 +6473,8 @@ function normalizeGraphSpec(spec, _warnings) {
6304
6473
  chrome: normalizeChrome(spec.chrome),
6305
6474
  annotations: normalizeAnnotations(spec.annotations),
6306
6475
  theme: spec.theme ?? {},
6307
- darkMode: spec.darkMode ?? "off"
6476
+ darkMode: spec.darkMode ?? "off",
6477
+ watermark: spec.watermark ?? true
6308
6478
  };
6309
6479
  }
6310
6480
  function normalizeSpec(spec, warnings = []) {
@@ -6331,14 +6501,23 @@ function normalizeSpec(spec, warnings = []) {
6331
6501
  `Unknown spec shape. Expected mark (chart), layer, type: 'table', type: 'graph', or type: 'sankey'.`
6332
6502
  );
6333
6503
  }
6334
- function flattenLayers(spec, parentData, parentEncoding, parentTransforms) {
6504
+ function flattenLayers(spec, parentData, parentEncoding, parentTransforms, parentWatermark) {
6335
6505
  const resolvedData = spec.data ?? parentData;
6336
6506
  const resolvedEncoding = parentEncoding && spec.encoding ? { ...parentEncoding, ...spec.encoding } : spec.encoding ?? parentEncoding;
6337
6507
  const resolvedTransforms = [...parentTransforms ?? [], ...spec.transform ?? []];
6508
+ const resolvedWatermark = spec.watermark ?? parentWatermark;
6338
6509
  const leaves = [];
6339
6510
  for (const child of spec.layer) {
6340
6511
  if (isLayerSpec(child)) {
6341
- leaves.push(...flattenLayers(child, resolvedData, resolvedEncoding, resolvedTransforms));
6512
+ leaves.push(
6513
+ ...flattenLayers(
6514
+ child,
6515
+ resolvedData,
6516
+ resolvedEncoding,
6517
+ resolvedTransforms,
6518
+ resolvedWatermark
6519
+ )
6520
+ );
6342
6521
  } else {
6343
6522
  const mergedData = child.data ?? resolvedData ?? [];
6344
6523
  const mergedEncoding = resolvedEncoding ? { ...resolvedEncoding, ...child.encoding } : child.encoding;
@@ -6347,7 +6526,9 @@ function flattenLayers(spec, parentData, parentEncoding, parentTransforms) {
6347
6526
  ...child,
6348
6527
  data: mergedData,
6349
6528
  encoding: mergedEncoding,
6350
- transform: mergedTransforms.length > 0 ? mergedTransforms : void 0
6529
+ transform: mergedTransforms.length > 0 ? mergedTransforms : void 0,
6530
+ // Inherit parent watermark if child doesn't explicitly set one
6531
+ ...child.watermark === void 0 && resolvedWatermark !== void 0 ? { watermark: resolvedWatermark } : {}
6351
6532
  });
6352
6533
  }
6353
6534
  }
@@ -7406,6 +7587,8 @@ function compileGraph(spec, options) {
7406
7587
  );
7407
7588
  }
7408
7589
  const graphSpec = normalized;
7590
+ const rawWatermark = spec.watermark;
7591
+ const watermark = rawWatermark !== void 0 ? graphSpec.watermark : options.watermark ?? true;
7409
7592
  const mergedThemeConfig = options.theme ? { ...graphSpec.theme, ...options.theme } : graphSpec.theme;
7410
7593
  let theme = resolveTheme(mergedThemeConfig);
7411
7594
  if (options.darkMode) {
@@ -7473,7 +7656,10 @@ function compileGraph(spec, options) {
7473
7656
  },
7474
7657
  theme,
7475
7658
  options.width,
7476
- options.measureText
7659
+ options.measureText,
7660
+ "full",
7661
+ void 0,
7662
+ watermark
7477
7663
  );
7478
7664
  return {
7479
7665
  nodes: compiledNodes,
@@ -7487,7 +7673,8 @@ function compileGraph(spec, options) {
7487
7673
  width: options.width,
7488
7674
  height: options.height
7489
7675
  },
7490
- simulationConfig
7676
+ simulationConfig,
7677
+ watermark
7491
7678
  };
7492
7679
  }
7493
7680
  var DEFAULT_COLLISION_PADDING = 5;
@@ -7693,7 +7880,7 @@ function computeAxes(scales, chartArea, strategy, theme, measureText) {
7693
7880
  }));
7694
7881
  const shouldThin = scales.x.type !== "band" && !axisConfig?.tickCount && !axisConfig?.values;
7695
7882
  const ticks2 = shouldThin ? thinTicksUntilFit(allTicks, fontSize, fontWeight, measureText) : allTicks;
7696
- let tickAngle = axisConfig?.labelAngle ?? axisConfig?.tickAngle;
7883
+ let tickAngle = axisConfig?.labelAngle;
7697
7884
  if (tickAngle === void 0 && scales.x.type === "band" && ticks2.length > 1) {
7698
7885
  const bandwidth = scales.x.scale.bandwidth();
7699
7886
  let maxLabelWidth = 0;
@@ -7705,7 +7892,7 @@ function computeAxes(scales, chartArea, strategy, theme, measureText) {
7705
7892
  tickAngle = -45;
7706
7893
  }
7707
7894
  }
7708
- const axisTitle = axisConfig?.title ?? axisConfig?.label;
7895
+ const axisTitle = axisConfig?.title;
7709
7896
  result.x = {
7710
7897
  ticks: ticks2,
7711
7898
  gridlines: axisConfig?.grid ? gridlines : [],
@@ -7735,14 +7922,14 @@ function computeAxes(scales, chartArea, strategy, theme, measureText) {
7735
7922
  } else {
7736
7923
  allTicks = continuousTicks(scales.y, yDensity);
7737
7924
  }
7738
- const gridlines = allTicks.map((t) => ({
7925
+ const shouldThin = scales.y.type !== "band" && !axisConfig?.tickCount && !axisConfig?.values;
7926
+ const ticks2 = shouldThin ? thinTicksUntilFit(allTicks, fontSize, fontWeight, measureText) : allTicks;
7927
+ const gridlines = ticks2.map((t) => ({
7739
7928
  position: t.position,
7740
7929
  major: true
7741
7930
  }));
7742
- const shouldThin = scales.y.type !== "band" && !axisConfig?.tickCount && !axisConfig?.values;
7743
- const ticks2 = shouldThin ? thinTicksUntilFit(allTicks, fontSize, fontWeight, measureText) : allTicks;
7744
- const axisTitle = axisConfig?.title ?? axisConfig?.label;
7745
- const tickAngle = axisConfig?.labelAngle ?? axisConfig?.tickAngle;
7931
+ const axisTitle = axisConfig?.title;
7932
+ const tickAngle = axisConfig?.labelAngle;
7746
7933
  result.y = {
7747
7934
  ticks: ticks2,
7748
7935
  // Y-axis gridlines are shown by default (standard editorial practice)
@@ -7786,7 +7973,7 @@ function scalePadding(basePadding, width, height) {
7786
7973
  }
7787
7974
  var MIN_CHART_WIDTH = 60;
7788
7975
  var MIN_CHART_HEIGHT = 40;
7789
- function computeDimensions(spec, options, legendLayout, theme, strategy) {
7976
+ function computeDimensions(spec, options, legendLayout, theme, strategy, watermark = true) {
7790
7977
  const { width, height } = options;
7791
7978
  const padding = scalePadding(theme.spacing.padding, width, height);
7792
7979
  const axisMargin = theme.spacing.axisMargin;
@@ -7797,14 +7984,15 @@ function computeDimensions(spec, options, legendLayout, theme, strategy) {
7797
7984
  width,
7798
7985
  options.measureText,
7799
7986
  chromeMode,
7800
- padding
7987
+ padding,
7988
+ watermark
7801
7989
  );
7802
7990
  const total = { x: 0, y: 0, width, height };
7803
7991
  const isRadial = spec.markType === "arc";
7804
7992
  const encoding = spec.encoding;
7805
7993
  const xAxis = encoding.x?.axis;
7806
- const hasXAxisLabel = !!xAxis?.label;
7807
- const xTickAngle = xAxis?.tickAngle;
7994
+ const hasXAxisLabel = !!xAxis?.title;
7995
+ const xTickAngle = xAxis?.labelAngle;
7808
7996
  let xAxisHeight;
7809
7997
  if (isRadial) {
7810
7998
  xAxisHeight = 0;
@@ -7955,7 +8143,8 @@ function computeDimensions(spec, options, legendLayout, theme, strategy) {
7955
8143
  width,
7956
8144
  options.measureText,
7957
8145
  fallbackMode,
7958
- padding
8146
+ padding,
8147
+ watermark
7959
8148
  );
7960
8149
  const fallbackTopAxisGap = isRadial && fallbackChrome.topHeight === 0 ? 0 : axisMargin;
7961
8150
  const newTop = padding + fallbackChrome.topHeight + fallbackTopAxisGap;
@@ -8028,6 +8217,12 @@ function uniqueStrings(values) {
8028
8217
  }
8029
8218
  return result;
8030
8219
  }
8220
+ function applyCategoricalSort(values, sort) {
8221
+ if (sort === null) return values;
8222
+ const sorted = [...values].sort((a, b) => a.localeCompare(b, void 0, { numeric: true }));
8223
+ if (sort === "descending") sorted.reverse();
8224
+ return sorted;
8225
+ }
8031
8226
  function applyContinuousConfig(scale, channel) {
8032
8227
  if (channel.scale?.clamp) {
8033
8228
  scale.clamp(true);
@@ -8189,7 +8384,7 @@ function evenRange(start, end, count) {
8189
8384
  return Array.from({ length: count }, (_, i) => start + step * i);
8190
8385
  }
8191
8386
  function buildBandScale(channel, data, rangeStart, rangeEnd) {
8192
- const values = channel.scale?.domain ? channel.scale.domain : uniqueStrings(fieldValues(data, channel.field));
8387
+ const values = channel.scale?.domain ? channel.scale.domain : applyCategoricalSort(uniqueStrings(fieldValues(data, channel.field)), channel.sort);
8193
8388
  const padding = channel.scale?.padding ?? 0.35;
8194
8389
  const scale = band().domain(values).range([rangeStart, rangeEnd]).padding(padding);
8195
8390
  if (channel.scale?.paddingInner !== void 0) {
@@ -8205,7 +8400,7 @@ function buildBandScale(channel, data, rangeStart, rangeEnd) {
8205
8400
  return { scale, type: "band", channel };
8206
8401
  }
8207
8402
  function buildPointScale(channel, data, rangeStart, rangeEnd) {
8208
- const values = channel.scale?.domain ? channel.scale.domain : uniqueStrings(fieldValues(data, channel.field));
8403
+ const values = channel.scale?.domain ? channel.scale.domain : applyCategoricalSort(uniqueStrings(fieldValues(data, channel.field)), channel.sort);
8209
8404
  const padding = channel.scale?.padding ?? 0.5;
8210
8405
  const scale = point4().domain(values).range([rangeStart, rangeEnd]).padding(padding);
8211
8406
  if (channel.scale?.reverse) {
@@ -8215,7 +8410,8 @@ function buildPointScale(channel, data, rangeStart, rangeEnd) {
8215
8410
  return { scale, type: "point", channel };
8216
8411
  }
8217
8412
  function buildOrdinalColorScale(channel, data, palette) {
8218
- const values = uniqueStrings(fieldValues(data, channel.field));
8413
+ const explicitDomain = channel.scale?.domain;
8414
+ const values = explicitDomain ? explicitDomain.map(String) : applyCategoricalSort(uniqueStrings(fieldValues(data, channel.field)), channel.sort);
8219
8415
  const scale = ordinal().domain(values).range(palette);
8220
8416
  return { scale, type: "ordinal", channel };
8221
8417
  }
@@ -8293,25 +8489,49 @@ function computeScales(spec, chartArea, data) {
8293
8489
  }
8294
8490
  if (encoding.x) {
8295
8491
  let xData = data;
8492
+ let xChannel = encoding.x;
8296
8493
  const xStackDisabled = encoding.x.stack === null || encoding.x.stack === false;
8297
8494
  if (spec.markType === "bar" && encoding.color && encoding.x.type === "quantitative" && !xStackDisabled) {
8298
- const yField = encoding.y?.field;
8299
- const xField = encoding.x.field;
8300
- if (yField) {
8301
- const sums = /* @__PURE__ */ new Map();
8302
- for (const row of data) {
8303
- const cat = String(row[yField] ?? "");
8304
- const val = Number(row[xField] ?? 0);
8305
- if (Number.isFinite(val) && val > 0) {
8306
- sums.set(cat, (sums.get(cat) ?? 0) + val);
8495
+ if (encoding.x.stack === "normalize") {
8496
+ xChannel = { ...encoding.x, scale: { ...encoding.x.scale, domain: [0, 1], nice: false } };
8497
+ } else if (encoding.x.stack === "center") {
8498
+ const yField = encoding.y?.field;
8499
+ const xField = encoding.x.field;
8500
+ if (yField) {
8501
+ const sums = /* @__PURE__ */ new Map();
8502
+ for (const row of data) {
8503
+ const cat = String(row[yField] ?? "");
8504
+ const val = Number(row[xField] ?? 0);
8505
+ if (Number.isFinite(val) && val > 0) {
8506
+ sums.set(cat, (sums.get(cat) ?? 0) + val);
8507
+ }
8508
+ }
8509
+ const maxSum = Math.max(...sums.values(), 0);
8510
+ const half = maxSum / 2;
8511
+ xChannel = {
8512
+ ...encoding.x,
8513
+ scale: { ...encoding.x.scale, domain: [-half, half], zero: true }
8514
+ };
8515
+ }
8516
+ } else {
8517
+ const yField = encoding.y?.field;
8518
+ const xField = encoding.x.field;
8519
+ if (yField) {
8520
+ const sums = /* @__PURE__ */ new Map();
8521
+ for (const row of data) {
8522
+ const cat = String(row[yField] ?? "");
8523
+ const val = Number(row[xField] ?? 0);
8524
+ if (Number.isFinite(val) && val > 0) {
8525
+ sums.set(cat, (sums.get(cat) ?? 0) + val);
8526
+ }
8307
8527
  }
8528
+ const maxSum = Math.max(...sums.values(), 0);
8529
+ xData = [...data, { [xField]: maxSum }];
8308
8530
  }
8309
- const maxSum = Math.max(...sums.values(), 0);
8310
- xData = [...data, { [xField]: maxSum }];
8311
8531
  }
8312
8532
  }
8313
8533
  result.x = buildPositionalScale(
8314
- encoding.x,
8534
+ xChannel,
8315
8535
  xData,
8316
8536
  chartArea.x,
8317
8537
  chartArea.x + chartArea.width,
@@ -8321,26 +8541,50 @@ function computeScales(spec, chartArea, data) {
8321
8541
  }
8322
8542
  if (encoding.y) {
8323
8543
  let yData = data;
8544
+ let yChannel = encoding.y;
8324
8545
  const isVerticalBar = spec.markType === "bar" && (encoding.x?.type === "nominal" || encoding.x?.type === "ordinal") && encoding.y.type === "quantitative";
8325
8546
  const yStackDisabled = encoding.y.stack === null || encoding.y.stack === false;
8326
8547
  if ((isVerticalBar || spec.markType === "area") && encoding.color && encoding.y.type === "quantitative" && !yStackDisabled) {
8327
- const xField = encoding.x?.field;
8328
- const yField = encoding.y.field;
8329
- if (xField) {
8330
- const sums = /* @__PURE__ */ new Map();
8331
- for (const row of data) {
8332
- const cat = String(row[xField] ?? "");
8333
- const val = Number(row[yField] ?? 0);
8334
- if (Number.isFinite(val) && val > 0) {
8335
- sums.set(cat, (sums.get(cat) ?? 0) + val);
8548
+ if (encoding.y.stack === "normalize") {
8549
+ yChannel = { ...encoding.y, scale: { ...encoding.y.scale, domain: [0, 1], nice: false } };
8550
+ } else if (encoding.y.stack === "center") {
8551
+ const xField = encoding.x?.field;
8552
+ const yField = encoding.y.field;
8553
+ if (xField) {
8554
+ const sums = /* @__PURE__ */ new Map();
8555
+ for (const row of data) {
8556
+ const cat = String(row[xField] ?? "");
8557
+ const val = Number(row[yField] ?? 0);
8558
+ if (Number.isFinite(val) && val > 0) {
8559
+ sums.set(cat, (sums.get(cat) ?? 0) + val);
8560
+ }
8561
+ }
8562
+ const maxSum = Math.max(...sums.values(), 0);
8563
+ const half = maxSum / 2;
8564
+ yChannel = {
8565
+ ...encoding.y,
8566
+ scale: { ...encoding.y.scale, domain: [-half, half], zero: true }
8567
+ };
8568
+ }
8569
+ } else {
8570
+ const xField = encoding.x?.field;
8571
+ const yField = encoding.y.field;
8572
+ if (xField) {
8573
+ const sums = /* @__PURE__ */ new Map();
8574
+ for (const row of data) {
8575
+ const cat = String(row[xField] ?? "");
8576
+ const val = Number(row[yField] ?? 0);
8577
+ if (Number.isFinite(val) && val > 0) {
8578
+ sums.set(cat, (sums.get(cat) ?? 0) + val);
8579
+ }
8336
8580
  }
8581
+ const maxSum = Math.max(...sums.values(), 0);
8582
+ yData = [...data, { [yField]: maxSum }];
8337
8583
  }
8338
- const maxSum = Math.max(...sums.values(), 0);
8339
- yData = [...data, { [yField]: maxSum }];
8340
8584
  }
8341
8585
  }
8342
8586
  result.y = buildPositionalScale(
8343
- encoding.y,
8587
+ yChannel,
8344
8588
  yData,
8345
8589
  chartArea.y + chartArea.height,
8346
8590
  chartArea.y,
@@ -8441,7 +8685,7 @@ function truncateEntries(entries, maxCount) {
8441
8685
  });
8442
8686
  return truncated;
8443
8687
  }
8444
- function computeLegend(spec, strategy, theme, chartArea) {
8688
+ function computeLegend(spec, strategy, theme, chartArea, watermark = true) {
8445
8689
  if (spec.legend?.show === false || strategy.legendMaxHeight === 0) {
8446
8690
  return {
8447
8691
  position: "top",
@@ -8518,7 +8762,7 @@ function computeLegend(spec, strategy, theme, chartArea) {
8518
8762
  entryGap: 4
8519
8763
  };
8520
8764
  }
8521
- const availableWidth = chartArea.width - LEGEND_PADDING * 2 - BRAND_RESERVE_WIDTH;
8765
+ const availableWidth = chartArea.width - LEGEND_PADDING * 2 - (watermark ? BRAND_RESERVE_WIDTH : 0);
8522
8766
  if (spec.legend?.symbolLimit != null) {
8523
8767
  const limit = Math.max(1, spec.legend.symbolLimit);
8524
8768
  if (limit < entries.length) {
@@ -9137,6 +9381,8 @@ function compileSankey(spec, options) {
9137
9381
  );
9138
9382
  }
9139
9383
  const sankeySpec = normalized;
9384
+ const rawWatermark = spec.watermark;
9385
+ const watermark = rawWatermark !== void 0 ? sankeySpec.watermark : options.watermark ?? true;
9140
9386
  const mergedThemeConfig = options.theme ? { ...sankeySpec.theme, ...options.theme } : sankeySpec.theme;
9141
9387
  const lightTheme = resolveTheme2(mergedThemeConfig);
9142
9388
  let theme = lightTheme;
@@ -9157,7 +9403,10 @@ function compileSankey(spec, options) {
9157
9403
  },
9158
9404
  theme,
9159
9405
  options.width,
9160
- options.measureText
9406
+ options.measureText,
9407
+ "full",
9408
+ void 0,
9409
+ watermark
9161
9410
  );
9162
9411
  const padding = theme.spacing.padding;
9163
9412
  const fullArea = {
@@ -9167,7 +9416,7 @@ function compileSankey(spec, options) {
9167
9416
  height: options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
9168
9417
  };
9169
9418
  if (fullArea.width <= 0 || fullArea.height <= 0) {
9170
- return emptyLayout(fullArea, chrome, theme, options);
9419
+ return emptyLayout(fullArea, chrome, theme, options, watermark);
9171
9420
  }
9172
9421
  const sourceField = sankeySpec.encoding.source.field;
9173
9422
  const targetField = sankeySpec.encoding.target.field;
@@ -9203,7 +9452,7 @@ function compileSankey(spec, options) {
9203
9452
  height: fullArea.height - legend.bounds.height - legendGap
9204
9453
  };
9205
9454
  if (area.height <= 0) {
9206
- return emptyLayout(area, chrome, theme, options);
9455
+ return emptyLayout(area, chrome, theme, options, watermark);
9207
9456
  }
9208
9457
  const labelFontSize = theme.fonts.sizes.small;
9209
9458
  const labelFontWeight = theme.fonts.weights.normal;
@@ -9347,7 +9596,8 @@ function compileSankey(spec, options) {
9347
9596
  width: options.width,
9348
9597
  height: options.height
9349
9598
  },
9350
- animation: resolvedAnimation
9599
+ animation: resolvedAnimation,
9600
+ watermark
9351
9601
  };
9352
9602
  }
9353
9603
  function buildSankeyLegend(nodeColorMap, colorField, data, sourceField, targetField, theme, area) {
@@ -9454,7 +9704,7 @@ function buildTooltipDescriptors(nodes, links, valueFormat) {
9454
9704
  }
9455
9705
  return descriptors;
9456
9706
  }
9457
- function emptyLayout(area, chrome, theme, options) {
9707
+ function emptyLayout(area, chrome, theme, options, watermark) {
9458
9708
  return {
9459
9709
  area,
9460
9710
  chrome,
@@ -9486,7 +9736,8 @@ function emptyLayout(area, chrome, theme, options) {
9486
9736
  dimensions: {
9487
9737
  width: options.width,
9488
9738
  height: options.height
9489
- }
9739
+ },
9740
+ watermark
9490
9741
  };
9491
9742
  }
9492
9743
 
@@ -10086,6 +10337,7 @@ function compileTableLayout(spec, options, theme) {
10086
10337
  });
10087
10338
  return { id: rowId, cells, data: row };
10088
10339
  });
10340
+ const watermark = spec.watermark;
10089
10341
  const chrome = computeChrome4(
10090
10342
  {
10091
10343
  title: spec.chrome.title,
@@ -10096,7 +10348,10 @@ function compileTableLayout(spec, options, theme) {
10096
10348
  },
10097
10349
  theme,
10098
10350
  options.width,
10099
- options.measureText
10351
+ options.measureText,
10352
+ "full",
10353
+ void 0,
10354
+ watermark
10100
10355
  );
10101
10356
  const titleText = spec.chrome.title?.text ?? "";
10102
10357
  const caption = titleText ? `Table: ${titleText}` : `Data table with ${data.length} rows`;
@@ -10118,7 +10373,8 @@ function compileTableLayout(spec, options, theme) {
10118
10373
  summary: `${resolvedColumns.length} columns, ${totalFiltered} rows`
10119
10374
  },
10120
10375
  theme,
10121
- animation: resolveAnimation(spec.animation)
10376
+ animation: resolveAnimation(spec.animation),
10377
+ watermark
10122
10378
  };
10123
10379
  }
10124
10380
 
@@ -10148,10 +10404,16 @@ function formatValue(value2, fieldType, format2) {
10148
10404
  }
10149
10405
  return String(value2);
10150
10406
  }
10407
+ function resolveLabel(ch) {
10408
+ return ch.title ?? ch.axis?.title ?? ch.field;
10409
+ }
10410
+ function resolveFormat(ch) {
10411
+ return ch.format ?? ch.axis?.format;
10412
+ }
10151
10413
  function buildExplicitTooltipFields(row, channels) {
10152
10414
  return channels.map((ch) => ({
10153
- label: ch.axis?.label ?? ch.field,
10154
- value: formatValue(row[ch.field], ch.type, ch.axis?.format)
10415
+ label: resolveLabel(ch),
10416
+ value: formatValue(row[ch.field], ch.type, resolveFormat(ch))
10155
10417
  }));
10156
10418
  }
10157
10419
  function buildFields(row, encoding, color2) {
@@ -10162,21 +10424,25 @@ function buildFields(row, encoding, color2) {
10162
10424
  const fields = [];
10163
10425
  if (encoding.y) {
10164
10426
  fields.push({
10165
- label: encoding.y.axis?.label ?? encoding.y.field,
10166
- value: formatValue(row[encoding.y.field], encoding.y.type, encoding.y.axis?.format),
10427
+ label: resolveLabel(encoding.y),
10428
+ value: formatValue(row[encoding.y.field], encoding.y.type, resolveFormat(encoding.y)),
10167
10429
  color: color2
10168
10430
  });
10169
10431
  }
10170
10432
  if (encoding.x) {
10171
10433
  fields.push({
10172
- label: encoding.x.axis?.label ?? encoding.x.field,
10173
- value: formatValue(row[encoding.x.field], encoding.x.type, encoding.x.axis?.format)
10434
+ label: resolveLabel(encoding.x),
10435
+ value: formatValue(row[encoding.x.field], encoding.x.type, resolveFormat(encoding.x))
10174
10436
  });
10175
10437
  }
10176
10438
  if (encoding.size && "field" in encoding.size) {
10177
10439
  fields.push({
10178
- label: encoding.size.axis?.label ?? encoding.size.field,
10179
- value: formatValue(row[encoding.size.field], encoding.size.type, encoding.size.axis?.format)
10440
+ label: resolveLabel(encoding.size),
10441
+ value: formatValue(
10442
+ row[encoding.size.field],
10443
+ encoding.size.type,
10444
+ resolveFormat(encoding.size)
10445
+ )
10180
10446
  });
10181
10447
  }
10182
10448
  return fields;
@@ -10229,14 +10495,14 @@ function tooltipsForArc(mark, encoding, markIndex) {
10229
10495
  if (encoding.y) {
10230
10496
  fields.push({
10231
10497
  label: categoryName,
10232
- value: formatValue(row[encoding.y.field], encoding.y.type, encoding.y.axis?.format),
10498
+ value: formatValue(row[encoding.y.field], encoding.y.type, resolveFormat(encoding.y)),
10233
10499
  color: getRepresentativeColor9(mark.fill)
10234
10500
  });
10235
10501
  }
10236
10502
  } else if (encoding.y) {
10237
10503
  fields.push({
10238
- label: encoding.y.field,
10239
- value: formatValue(row[encoding.y.field], encoding.y.type, encoding.y.axis?.format),
10504
+ label: resolveLabel(encoding.y),
10505
+ value: formatValue(row[encoding.y.field], encoding.y.type, resolveFormat(encoding.y)),
10240
10506
  color: getRepresentativeColor9(mark.fill)
10241
10507
  });
10242
10508
  }
@@ -10284,6 +10550,93 @@ function computeTooltipDescriptors(spec, marks) {
10284
10550
  return descriptors;
10285
10551
  }
10286
10552
 
10553
+ // src/transforms/aggregate.ts
10554
+ function computeAggregate(op, values) {
10555
+ if (values.length === 0) return 0;
10556
+ switch (op) {
10557
+ case "count":
10558
+ return values.length;
10559
+ case "sum":
10560
+ return values.reduce((a, b) => a + b, 0);
10561
+ case "mean": {
10562
+ const sum2 = values.reduce((a, b) => a + b, 0);
10563
+ return sum2 / values.length;
10564
+ }
10565
+ case "median": {
10566
+ const sorted = [...values].sort((a, b) => a - b);
10567
+ const mid = Math.floor(sorted.length / 2);
10568
+ return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
10569
+ }
10570
+ case "min":
10571
+ return Math.min(...values);
10572
+ case "max":
10573
+ return Math.max(...values);
10574
+ case "variance": {
10575
+ if (values.length < 2) return 0;
10576
+ const mean = values.reduce((a, b) => a + b, 0) / values.length;
10577
+ return values.reduce((a, v) => a + (v - mean) ** 2, 0) / values.length;
10578
+ }
10579
+ case "stdev": {
10580
+ if (values.length < 2) return 0;
10581
+ const m = values.reduce((a, b) => a + b, 0) / values.length;
10582
+ return Math.sqrt(values.reduce((a, v) => a + (v - m) ** 2, 0) / values.length);
10583
+ }
10584
+ case "q1": {
10585
+ const s = [...values].sort((a, b) => a - b);
10586
+ const i = (s.length - 1) * 0.25;
10587
+ const lo = Math.floor(i);
10588
+ const frac = i - lo;
10589
+ return s[lo] + frac * ((s[lo + 1] ?? s[lo]) - s[lo]);
10590
+ }
10591
+ case "q3": {
10592
+ const s = [...values].sort((a, b) => a - b);
10593
+ const i = (s.length - 1) * 0.75;
10594
+ const lo = Math.floor(i);
10595
+ const frac = i - lo;
10596
+ return s[lo] + frac * ((s[lo + 1] ?? s[lo]) - s[lo]);
10597
+ }
10598
+ default:
10599
+ return 0;
10600
+ }
10601
+ }
10602
+ function groupKey(row, groupby) {
10603
+ return groupby.map((f) => String(row[f] ?? "")).join("\0");
10604
+ }
10605
+ function runAggregate(data, transform) {
10606
+ const { aggregate, groupby } = transform;
10607
+ const groups = /* @__PURE__ */ new Map();
10608
+ for (const row of data) {
10609
+ const key = groupKey(row, groupby);
10610
+ const existing = groups.get(key);
10611
+ if (existing) {
10612
+ existing.push(row);
10613
+ } else {
10614
+ groups.set(key, [row]);
10615
+ }
10616
+ }
10617
+ const result = [];
10618
+ for (const rows of groups.values()) {
10619
+ const outRow = {};
10620
+ for (const field of groupby) {
10621
+ outRow[field] = rows[0][field];
10622
+ }
10623
+ for (const agg of aggregate) {
10624
+ if (agg.op === "distinct") {
10625
+ outRow[agg.as] = new Set(rows.map((r) => r[agg.field])).size;
10626
+ continue;
10627
+ }
10628
+ const values = rows.map((r) => {
10629
+ if (agg.op === "count") return 1;
10630
+ const v = Number(r[agg.field]);
10631
+ return Number.isFinite(v) ? v : NaN;
10632
+ }).filter((v) => !Number.isNaN(v));
10633
+ outRow[agg.as] = computeAggregate(agg.op, values);
10634
+ }
10635
+ result.push(outRow);
10636
+ }
10637
+ return result;
10638
+ }
10639
+
10287
10640
  // src/transforms/bin.ts
10288
10641
  function computeStep(extent2, maxbins, nice2) {
10289
10642
  const span = extent2[1] - extent2[0];
@@ -10380,6 +10733,30 @@ function runFilter(data, predicate) {
10380
10733
  return data.filter((datum) => evaluatePredicate(datum, predicate));
10381
10734
  }
10382
10735
 
10736
+ // src/transforms/fold.ts
10737
+ function runFold(data, transform) {
10738
+ const { fold } = transform;
10739
+ const [keyAs, valueAs] = transform.as ?? ["key", "value"];
10740
+ const foldSet = new Set(fold);
10741
+ const result = [];
10742
+ for (const row of data) {
10743
+ const base = {};
10744
+ for (const [k, v] of Object.entries(row)) {
10745
+ if (!foldSet.has(k)) {
10746
+ base[k] = v;
10747
+ }
10748
+ }
10749
+ for (const field of fold) {
10750
+ result.push({
10751
+ ...base,
10752
+ [keyAs]: field,
10753
+ [valueAs]: row[field]
10754
+ });
10755
+ }
10756
+ }
10757
+ return result;
10758
+ }
10759
+
10383
10760
  // src/transforms/timeunit.ts
10384
10761
  function extractTimeUnit(date2, unit2) {
10385
10762
  switch (unit2) {
@@ -10457,6 +10834,10 @@ function runTransforms(data, transforms) {
10457
10834
  result = runCalculate(result, transform);
10458
10835
  } else if ("timeUnit" in transform) {
10459
10836
  result = runTimeUnit(result, transform);
10837
+ } else if ("aggregate" in transform) {
10838
+ result = runAggregate(result, transform);
10839
+ } else if ("fold" in transform) {
10840
+ result = runFold(result, transform);
10460
10841
  }
10461
10842
  }
10462
10843
  return result;
@@ -10489,71 +10870,55 @@ var builtinRenderers = {
10489
10870
  for (const [type, renderer] of Object.entries(builtinRenderers)) {
10490
10871
  registerChartRenderer(type, renderer);
10491
10872
  }
10492
- function computeMarkObstacles(marks, scales) {
10493
- if (scales.y?.type === "band") {
10494
- return computeBandRowObstacles(marks, scales);
10495
- }
10496
- const obstacles = [];
10497
- for (const mark of marks) {
10498
- if (mark.type === "rect") {
10499
- const rm = mark;
10500
- obstacles.push({ x: rm.x, y: rm.y, width: rm.width, height: rm.height });
10501
- } else if (mark.type === "point") {
10502
- const pm = mark;
10503
- obstacles.push({
10504
- x: pm.cx - pm.r,
10505
- y: pm.cy - pm.r,
10506
- width: pm.r * 2,
10507
- height: pm.r * 2
10508
- });
10509
- }
10510
- }
10511
- return obstacles;
10512
- }
10513
- function computeBandRowObstacles(marks, scales) {
10514
- const rows = /* @__PURE__ */ new Map();
10515
- for (const mark of marks) {
10516
- let cy;
10517
- let left2;
10518
- let right2;
10519
- if (mark.type === "point") {
10520
- const pm = mark;
10521
- cy = pm.cy;
10522
- left2 = pm.cx - pm.r;
10523
- right2 = pm.cx + pm.r;
10524
- } else if (mark.type === "rect") {
10525
- const rm = mark;
10526
- cy = rm.y + rm.height / 2;
10527
- left2 = rm.x;
10528
- right2 = rm.x + rm.width;
10529
- } else {
10530
- continue;
10531
- }
10532
- const key = Math.round(cy);
10533
- const existing = rows.get(key);
10534
- if (existing) {
10535
- existing.minX = Math.min(existing.minX, left2);
10536
- existing.maxX = Math.max(existing.maxX, right2);
10537
- } else {
10538
- rows.set(key, { minX: left2, maxX: right2, bandY: cy });
10873
+ function expandEncodingSugar(spec) {
10874
+ const encoding = spec.encoding;
10875
+ if (!encoding) return spec;
10876
+ const generatedTransforms = [];
10877
+ const updatedEncoding = { ...encoding };
10878
+ let changed = false;
10879
+ for (const channel of Object.keys(encoding)) {
10880
+ const ch = encoding[channel];
10881
+ if (!ch || !ch.field) continue;
10882
+ if (ch.bin != null && ch.bin !== false) {
10883
+ const field = ch.field;
10884
+ const outputField = `bin_${field}`;
10885
+ const binTransform = {
10886
+ bin: ch.bin === true ? true : ch.bin,
10887
+ field,
10888
+ as: outputField
10889
+ };
10890
+ generatedTransforms.push(binTransform);
10891
+ const { bin: _bin, ...rest } = ch;
10892
+ updatedEncoding[channel] = { ...rest, field: outputField };
10893
+ changed = true;
10894
+ }
10895
+ const current = updatedEncoding[channel] ?? ch;
10896
+ if (current.timeUnit) {
10897
+ const field = current.field;
10898
+ const unit2 = current.timeUnit;
10899
+ const outputField = `${unit2}_${field}`;
10900
+ const timeUnitTransform = {
10901
+ timeUnit: unit2,
10902
+ field,
10903
+ as: outputField
10904
+ };
10905
+ generatedTransforms.push(timeUnitTransform);
10906
+ const { timeUnit: _tu, ...rest } = current;
10907
+ updatedEncoding[channel] = { ...rest, field: outputField };
10908
+ changed = true;
10539
10909
  }
10540
10910
  }
10541
- const bandScale = scales.y.scale;
10542
- const bandwidth = bandScale.bandwidth?.() ?? 0;
10543
- if (bandwidth === 0) return [];
10544
- const obstacles = [];
10545
- for (const { minX, maxX, bandY } of rows.values()) {
10546
- obstacles.push({
10547
- x: minX,
10548
- y: bandY - bandwidth / 2,
10549
- width: maxX - minX,
10550
- height: bandwidth
10551
- });
10552
- }
10553
- return obstacles;
10911
+ if (!changed) return spec;
10912
+ const existingTransforms = spec.transform ?? [];
10913
+ return {
10914
+ ...spec,
10915
+ encoding: updatedEncoding,
10916
+ transform: [...generatedTransforms, ...existingTransforms]
10917
+ };
10554
10918
  }
10555
10919
  function compileChart(spec, options) {
10556
- const { spec: normalized } = compile(spec);
10920
+ const expandedSpec = spec && typeof spec === "object" && !Array.isArray(spec) ? expandEncodingSugar(spec) : spec;
10921
+ const { spec: normalized } = compile(expandedSpec);
10557
10922
  if ("type" in normalized && normalized.type === "table") {
10558
10923
  throw new Error("compileChart received a table spec. Use compileTable instead.");
10559
10924
  }
@@ -10564,14 +10929,16 @@ function compileChart(spec, options) {
10564
10929
  throw new Error("compileChart received a sankey spec. Use compileSankey instead.");
10565
10930
  }
10566
10931
  let chartSpec = normalized;
10567
- const rawTransforms = spec.transform;
10932
+ const rawWatermark = expandedSpec.watermark;
10933
+ const watermark = rawWatermark !== void 0 ? chartSpec.watermark : options.watermark ?? true;
10934
+ const rawTransforms = expandedSpec.transform;
10568
10935
  if (rawTransforms && rawTransforms.length > 0) {
10569
10936
  chartSpec = { ...chartSpec, data: runTransforms(chartSpec.data, rawTransforms) };
10570
10937
  }
10571
10938
  const breakpoint = getBreakpoint(options.width);
10572
10939
  const heightClass = getHeightClass(options.height);
10573
10940
  const strategy = getLayoutStrategy(breakpoint, heightClass);
10574
- const rawSpec = spec;
10941
+ const rawSpec = expandedSpec;
10575
10942
  const overrides = rawSpec.overrides;
10576
10943
  if (overrides?.[breakpoint]) {
10577
10944
  const bp = overrides[breakpoint];
@@ -10622,8 +10989,8 @@ function compileChart(spec, options) {
10622
10989
  width: options.width,
10623
10990
  height: options.height
10624
10991
  };
10625
- const legendLayout = computeLegend(chartSpec, strategy, theme, preliminaryArea);
10626
- const dims = computeDimensions(chartSpec, options, legendLayout, theme, strategy);
10992
+ const legendLayout = computeLegend(chartSpec, strategy, theme, preliminaryArea, watermark);
10993
+ const dims = computeDimensions(chartSpec, options, legendLayout, theme, strategy, watermark);
10627
10994
  const chartArea = dims.chartArea;
10628
10995
  const legendArea = { ...chartArea };
10629
10996
  if (legendLayout.entries.length > 0) {
@@ -10641,7 +11008,7 @@ function compileChart(spec, options) {
10641
11008
  break;
10642
11009
  }
10643
11010
  }
10644
- const finalLegend = computeLegend(chartSpec, strategy, theme, legendArea);
11011
+ const finalLegend = computeLegend(chartSpec, strategy, theme, legendArea, watermark);
10645
11012
  let renderData = chartSpec.data;
10646
11013
  if (chartSpec.hiddenSeries.length > 0 && chartSpec.encoding.color && "field" in chartSpec.encoding.color) {
10647
11014
  const colorField = chartSpec.encoding.color.field;
@@ -10682,20 +11049,11 @@ function compileChart(spec, options) {
10682
11049
  if (!isRadial) {
10683
11050
  computeGridlines(axes, chartArea);
10684
11051
  }
10685
- let rendererKey = renderSpec.markType;
10686
- if (rendererKey === "bar") {
10687
- const xType = renderSpec.encoding.x?.type;
10688
- const yType = renderSpec.encoding.y?.type;
10689
- const isVertical = (xType === "nominal" || xType === "ordinal" || xType === "temporal") && yType === "quantitative";
10690
- if (isVertical) {
10691
- rendererKey = "bar:vertical";
10692
- }
10693
- } else if (rendererKey === "arc") {
10694
- const innerRadius = renderSpec.markDef.innerRadius;
10695
- if (innerRadius && innerRadius > 0) {
10696
- rendererKey = "arc:donut";
10697
- }
10698
- }
11052
+ const rendererKey = resolveRendererKey(
11053
+ renderSpec.markType,
11054
+ renderSpec.encoding,
11055
+ renderSpec.markDef
11056
+ );
10699
11057
  const renderer = getChartRenderer(rendererKey);
10700
11058
  const marks = renderer ? renderer(renderSpec, scales, chartArea, strategy, theme) : [];
10701
11059
  const obstacles = [];
@@ -10708,12 +11066,14 @@ function compileChart(spec, options) {
10708
11066
  obstacles.push(computeLabelBounds(mark.label));
10709
11067
  }
10710
11068
  }
10711
- const brandPadding = theme.spacing.padding;
10712
- const brandX = dims.total.width - brandPadding - BRAND_RESERVE_WIDTH2;
10713
- const xAxisExtent = axes.x?.label ? 48 : axes.x ? 26 : 0;
10714
- const firstBottomChrome = dims.chrome.source ?? dims.chrome.byline ?? dims.chrome.footer;
10715
- const brandY = firstBottomChrome ? chartArea.y + chartArea.height + xAxisExtent + firstBottomChrome.y : chartArea.y + chartArea.height + xAxisExtent + theme.spacing.chartToFooter;
10716
- obstacles.push({ x: brandX, y: brandY, width: BRAND_RESERVE_WIDTH2, height: 30 });
11069
+ if (watermark) {
11070
+ const brandPadding = theme.spacing.padding;
11071
+ const brandX = dims.total.width - brandPadding - BRAND_RESERVE_WIDTH2;
11072
+ const xAxisExtent = axes.x?.label ? 48 : axes.x ? 26 : 0;
11073
+ const firstBottomChrome = dims.chrome.source ?? dims.chrome.byline ?? dims.chrome.footer;
11074
+ const brandY = firstBottomChrome ? chartArea.y + chartArea.height + xAxisExtent + firstBottomChrome.y : chartArea.y + chartArea.height + xAxisExtent + theme.spacing.chartToFooter;
11075
+ obstacles.push({ x: brandX, y: brandY, width: BRAND_RESERVE_WIDTH2, height: 30 });
11076
+ }
10717
11077
  const annotations = computeAnnotations(
10718
11078
  chartSpec,
10719
11079
  scales,
@@ -10741,37 +11101,7 @@ function compileChart(spec, options) {
10741
11101
  },
10742
11102
  chartSpec.data
10743
11103
  );
10744
- if (resolvedAnimation?.enabled && resolvedAnimation.staggerOrder === "value") {
10745
- const indexed = marks.map((m, i) => ({ mark: m, idx: i }));
10746
- indexed.sort((a, b) => {
10747
- const av = getMarkPrimaryValue(a.mark);
10748
- const bv = getMarkPrimaryValue(b.mark);
10749
- return av - bv;
10750
- });
10751
- for (let i = 0; i < indexed.length; i++) {
10752
- const m = indexed[i].mark;
10753
- if (m.type === "rect" && m.stackGroup) continue;
10754
- m.animationIndex = i;
10755
- }
10756
- }
10757
- if (resolvedAnimation?.enabled) {
10758
- const groupIndexMap = /* @__PURE__ */ new Map();
10759
- const groupStackPos = /* @__PURE__ */ new Map();
10760
- let nextGroupIndex = 0;
10761
- for (const mark of marks) {
10762
- if (mark.type === "rect" && mark.stackGroup) {
10763
- const rect = mark;
10764
- const group = rect.stackGroup;
10765
- if (!groupIndexMap.has(group)) {
10766
- groupIndexMap.set(group, nextGroupIndex++);
10767
- }
10768
- rect.animationIndex = groupIndexMap.get(group);
10769
- const pos = groupStackPos.get(group) ?? 0;
10770
- rect.stackPos = pos;
10771
- groupStackPos.set(group, pos + 1);
10772
- }
10773
- }
10774
- }
11104
+ assignAnimationIndices(marks, resolvedAnimation);
10775
11105
  return {
10776
11106
  area: chartArea,
10777
11107
  chrome: dims.chrome,
@@ -10794,28 +11124,10 @@ function compileChart(spec, options) {
10794
11124
  width: options.width,
10795
11125
  height: options.height
10796
11126
  },
10797
- animation: resolvedAnimation
11127
+ animation: resolvedAnimation,
11128
+ watermark
10798
11129
  };
10799
11130
  }
10800
- function getMarkPrimaryValue(mark) {
10801
- switch (mark.type) {
10802
- case "rect":
10803
- return mark.height;
10804
- // bar height is the primary value encoding
10805
- case "point":
10806
- return mark.cy;
10807
- // y position for scatter
10808
- case "arc":
10809
- return mark.endAngle - mark.startAngle;
10810
- // arc angle extent
10811
- case "line":
10812
- case "area":
10813
- return 0;
10814
- // series marks don't have individual values
10815
- default:
10816
- return 0;
10817
- }
10818
- }
10819
11131
  function compileLayer(spec, options) {
10820
11132
  const leaves = flattenLayers(spec);
10821
11133
  if (leaves.length === 0) {
@@ -10864,6 +11176,7 @@ function buildPrimarySpec(leaves, layerSpec) {
10864
11176
  responsive: layerSpec.responsive ?? leaves[0].responsive,
10865
11177
  theme: layerSpec.theme ?? leaves[0].theme,
10866
11178
  darkMode: layerSpec.darkMode ?? leaves[0].darkMode,
11179
+ watermark: layerSpec.watermark ?? leaves[0].watermark,
10867
11180
  hiddenSeries: layerSpec.hiddenSeries ?? leaves[0].hiddenSeries
10868
11181
  };
10869
11182
  return primary;
@@ -10880,7 +11193,9 @@ function compileTable(spec, options) {
10880
11193
  if (options.darkMode) {
10881
11194
  theme = adaptTheme3(theme);
10882
11195
  }
10883
- return compileTableLayout(tableSpec, options, theme);
11196
+ const rawWatermark = spec.watermark;
11197
+ const watermark = rawWatermark !== void 0 ? tableSpec.watermark : options.watermark ?? true;
11198
+ return compileTableLayout({ ...tableSpec, watermark }, options, theme);
10884
11199
  }
10885
11200
  function compileGraph2(spec, options) {
10886
11201
  return compileGraph(spec, options);