@opendata-ai/openchart-core 7.10.0 → 8.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/index.d.ts +1154 -119
  2. package/dist/index.js +530 -41
  3. package/dist/index.js.map +1 -1
  4. package/dist/styles.css +2 -1
  5. package/dist/styles.css.map +1 -0
  6. package/package.json +9 -3
  7. package/schema/chart.schema.json +8957 -0
  8. package/schema/table.schema.json +861 -0
  9. package/schema/vizspec.schema.json +17694 -0
  10. package/src/accessibility/alt-text.ts +39 -1
  11. package/src/accessibility/aria.ts +4 -2
  12. package/src/colors/__tests__/colorblind.test.ts +12 -0
  13. package/src/colors/colorblind.ts +11 -2
  14. package/src/colors/contrast.ts +11 -0
  15. package/src/colors/index.ts +9 -1
  16. package/src/colors/palettes.ts +47 -0
  17. package/src/helpers/__tests__/deep-merge.test.ts +54 -0
  18. package/src/helpers/__tests__/spec-builders.test.ts +3 -3
  19. package/src/helpers/deep-merge.ts +29 -0
  20. package/src/helpers/spec-builders.ts +7 -7
  21. package/src/index.ts +29 -1
  22. package/src/labels/collision.ts +1 -0
  23. package/src/layout/chrome.ts +24 -0
  24. package/src/layout/index.ts +1 -1
  25. package/src/locale/__tests__/format.test.ts +347 -11
  26. package/src/locale/format.ts +243 -18
  27. package/src/locale/index.ts +10 -1
  28. package/src/responsive/index.ts +2 -0
  29. package/src/responsive/metrics.ts +74 -9
  30. package/src/schema/__tests__/generated-artifacts.test.ts +42 -0
  31. package/src/schema/__tests__/schema.test.ts +179 -0
  32. package/src/styles/__tests__/reduced-motion-selectors.test.ts +28 -0
  33. package/src/styles/animation.css +197 -168
  34. package/src/styles/base.css +89 -62
  35. package/src/styles/chrome.css +173 -179
  36. package/src/styles/dark.css +38 -35
  37. package/src/styles/graph.css +116 -111
  38. package/src/styles/index.css +5 -0
  39. package/src/styles/keyframes.css +115 -100
  40. package/src/styles/legend.css +14 -12
  41. package/src/styles/reduced-motion.css +61 -40
  42. package/src/styles/series-search.css +153 -0
  43. package/src/styles/sparkline.css +14 -12
  44. package/src/styles/table-animation.css +82 -80
  45. package/src/styles/table.css +314 -302
  46. package/src/styles/token-definitions.ts +199 -0
  47. package/src/styles/tokens.css +132 -128
  48. package/src/styles/tooltip.css +109 -115
  49. package/src/styles/you-draw-it.css +126 -0
  50. package/src/types/__tests__/encoding.test.ts +86 -17
  51. package/src/types/__tests__/spec.test.ts +16 -12
  52. package/src/types/encoding.ts +69 -25
  53. package/src/types/events.ts +12 -0
  54. package/src/types/index.ts +39 -3
  55. package/src/types/layout.ts +414 -8
  56. package/src/types/schema-roots.ts +32 -0
  57. package/src/types/spec.ts +643 -84
  58. package/src/types/table.ts +3 -2
package/dist/index.js CHANGED
@@ -15,8 +15,6 @@ var MARK_ENCODING_RULES = {
15
15
  size: optional(),
16
16
  opacity: optional("quantitative"),
17
17
  tooltip: optional(),
18
- href: optional(),
19
- order: optional("quantitative", "ordinal"),
20
18
  detail: optional("nominal")
21
19
  },
22
20
  line: {
@@ -27,8 +25,6 @@ var MARK_ENCODING_RULES = {
27
25
  opacity: optional("quantitative"),
28
26
  strokeDash: optional("nominal", "ordinal"),
29
27
  tooltip: optional(),
30
- href: optional(),
31
- order: optional("quantitative", "ordinal"),
32
28
  detail: optional("nominal")
33
29
  },
34
30
  area: {
@@ -39,8 +35,6 @@ var MARK_ENCODING_RULES = {
39
35
  size: optional(),
40
36
  opacity: optional("quantitative"),
41
37
  tooltip: optional(),
42
- href: optional(),
43
- order: optional("quantitative", "ordinal"),
44
38
  detail: optional("nominal")
45
39
  },
46
40
  point: {
@@ -48,11 +42,8 @@ var MARK_ENCODING_RULES = {
48
42
  y: required("quantitative", "temporal", "nominal", "ordinal"),
49
43
  color: optional("nominal", "ordinal", "quantitative"),
50
44
  size: optional("quantitative"),
51
- shape: optional("nominal", "ordinal"),
52
45
  opacity: optional("quantitative"),
53
46
  tooltip: optional(),
54
- href: optional(),
55
- order: optional("quantitative", "ordinal"),
56
47
  detail: optional("nominal")
57
48
  },
58
49
  circle: {
@@ -62,8 +53,6 @@ var MARK_ENCODING_RULES = {
62
53
  size: optional("quantitative"),
63
54
  opacity: optional("quantitative"),
64
55
  tooltip: optional(),
65
- href: optional(),
66
- order: optional("quantitative", "ordinal"),
67
56
  detail: optional("nominal")
68
57
  },
69
58
  lollipop: {
@@ -73,21 +62,63 @@ var MARK_ENCODING_RULES = {
73
62
  size: optional("quantitative"),
74
63
  opacity: optional("quantitative"),
75
64
  tooltip: optional(),
76
- href: optional(),
77
- order: optional("quantitative", "ordinal"),
65
+ detail: optional("nominal")
66
+ },
67
+ beeswarm: {
68
+ x: optional("quantitative", "nominal", "ordinal"),
69
+ y: optional("quantitative", "nominal", "ordinal"),
70
+ color: optional("nominal", "ordinal", "quantitative"),
71
+ size: optional("quantitative"),
72
+ opacity: optional("quantitative"),
73
+ tooltip: optional(),
74
+ detail: optional("nominal")
75
+ },
76
+ // Range: x+x2 with nominal y (horizontal) or y+y2 with nominal x (vertical).
77
+ // The orientation-dependent x2/y2 requirement is enforced by a dedicated
78
+ // check in the validator; the static table can only mark them optional.
79
+ range: {
80
+ x: required("quantitative", "nominal", "ordinal"),
81
+ y: required("quantitative", "nominal", "ordinal"),
82
+ x2: optional("quantitative"),
83
+ y2: optional("quantitative"),
84
+ color: optional("nominal", "ordinal"),
85
+ size: optional(),
86
+ opacity: optional("quantitative"),
87
+ tooltip: optional(),
78
88
  detail: optional("nominal")
79
89
  },
80
90
  arc: {
91
+ x: optional(),
92
+ y: optional("quantitative"),
93
+ theta: required("quantitative"),
94
+ color: required("nominal", "ordinal"),
95
+ size: optional(),
96
+ opacity: optional("quantitative"),
97
+ tooltip: optional(),
98
+ detail: optional("nominal")
99
+ },
100
+ waffle: {
101
+ x: optional(),
102
+ y: required("quantitative"),
103
+ color: required("nominal", "ordinal"),
104
+ size: optional(),
105
+ opacity: optional("quantitative"),
106
+ tooltip: optional(),
107
+ theta: optional("quantitative"),
108
+ detail: optional("nominal")
109
+ },
110
+ // parliament: hemicycle seat chart. Like arc/waffle, no positional axes;
111
+ // color is the party (required), theta the seat count (aliased to y in the
112
+ // sugar pass). y is accepted so the theta alias resolves before the rules
113
+ // check runs.
114
+ parliament: {
81
115
  x: optional(),
82
116
  y: required("quantitative"),
83
117
  color: required("nominal", "ordinal"),
84
118
  size: optional(),
85
119
  opacity: optional("quantitative"),
86
120
  tooltip: optional(),
87
- href: optional(),
88
- order: optional("quantitative", "ordinal"),
89
121
  theta: optional("quantitative"),
90
- radius: optional("quantitative"),
91
122
  detail: optional("nominal")
92
123
  },
93
124
  text: {
@@ -98,7 +129,6 @@ var MARK_ENCODING_RULES = {
98
129
  opacity: optional("quantitative"),
99
130
  text: required(),
100
131
  tooltip: optional(),
101
- href: optional(),
102
132
  detail: optional("nominal")
103
133
  },
104
134
  rule: {
@@ -111,7 +141,6 @@ var MARK_ENCODING_RULES = {
111
141
  opacity: optional("quantitative"),
112
142
  strokeDash: optional("nominal", "ordinal"),
113
143
  tooltip: optional(),
114
- href: optional(),
115
144
  detail: optional("nominal")
116
145
  },
117
146
  tick: {
@@ -121,7 +150,6 @@ var MARK_ENCODING_RULES = {
121
150
  size: optional(),
122
151
  opacity: optional("quantitative"),
123
152
  tooltip: optional(),
124
- href: optional(),
125
153
  detail: optional("nominal")
126
154
  },
127
155
  rect: {
@@ -133,12 +161,22 @@ var MARK_ENCODING_RULES = {
133
161
  size: optional(),
134
162
  opacity: optional("quantitative"),
135
163
  tooltip: optional(),
136
- href: optional(),
137
- order: optional("quantitative", "ordinal"),
164
+ detail: optional("nominal")
165
+ },
166
+ // calendar: x is the daily date (temporal), color the per-day value
167
+ // (quantitative). No y channel: the calendar computes its own
168
+ // weeks-x-weekdays geometry, so y is left optional here and the dedicated
169
+ // validator rejects it with a pointed message.
170
+ calendar: {
171
+ x: required("temporal"),
172
+ y: optional(),
173
+ color: required("quantitative"),
174
+ size: optional(),
175
+ opacity: optional("quantitative"),
176
+ tooltip: optional(),
138
177
  detail: optional("nominal")
139
178
  }
140
179
  };
141
- var CHART_ENCODING_RULES = MARK_ENCODING_RULES;
142
180
  var GRAPH_ENCODING_RULES = {
143
181
  nodeColor: { required: false, allowedTypes: ["nominal", "ordinal"] },
144
182
  nodeSize: { required: false, allowedTypes: ["quantitative"] },
@@ -177,6 +215,10 @@ function isConditionalDef(def) {
177
215
  if (!def) return false;
178
216
  return "condition" in def;
179
217
  }
218
+ function isValueDef(def) {
219
+ if (!def) return false;
220
+ return "value" in def && !("condition" in def) && !("field" in def);
221
+ }
180
222
  var MARK_TYPES = /* @__PURE__ */ new Set([
181
223
  "bar",
182
224
  "line",
@@ -188,9 +230,16 @@ var MARK_TYPES = /* @__PURE__ */ new Set([
188
230
  "rule",
189
231
  "tick",
190
232
  "rect",
191
- "lollipop"
233
+ "lollipop",
234
+ "beeswarm",
235
+ "range",
236
+ "waffle",
237
+ "calendar",
238
+ "parliament"
192
239
  ]);
193
- var CHART_TYPES = MARK_TYPES;
240
+ function isAxislessMark(markType) {
241
+ return markType === "arc" || markType === "waffle" || markType === "calendar" || markType === "parliament";
242
+ }
194
243
  function resolveMarkType(mark) {
195
244
  return typeof mark === "string" ? mark : mark.type;
196
245
  }
@@ -218,6 +267,9 @@ function isTileMapSpec(spec) {
218
267
  function isBarListSpec(spec) {
219
268
  return "type" in spec && spec.type === "barlist";
220
269
  }
270
+ function isMapSpec(spec) {
271
+ return "type" in spec && spec.type === "map";
272
+ }
221
273
  function isTextAnnotation(annotation) {
222
274
  return annotation.type === "text";
223
275
  }
@@ -225,7 +277,8 @@ function isRangeAnnotation(annotation) {
225
277
  return annotation.type === "range";
226
278
  }
227
279
  function isRefLineAnnotation(annotation) {
228
- return annotation.type === "refline" || annotation.type === "rule";
280
+ const type = annotation.type;
281
+ return type === "refline" || type === "rule";
229
282
  }
230
283
  var MARK_DISPLAY_NAMES = {
231
284
  bar: "Bar chart",
@@ -239,7 +292,12 @@ var MARK_DISPLAY_NAMES = {
239
292
  rule: "Rule chart",
240
293
  tick: "Tick plot",
241
294
  rect: "Heatmap",
242
- lollipop: "Lollipop chart"
295
+ lollipop: "Lollipop chart",
296
+ beeswarm: "Beeswarm chart",
297
+ range: "Range plot",
298
+ waffle: "Waffle chart",
299
+ calendar: "Calendar heatmap",
300
+ parliament: "Parliament chart"
243
301
  };
244
302
 
245
303
  // ../../node_modules/.bun/d3-color@3.1.0/node_modules/d3-color/src/define.js
@@ -606,10 +664,16 @@ var TRITAN_MATRIX = [
606
664
  [0, 0.433, 0.567],
607
665
  [0, 0.475, 0.525]
608
666
  ];
667
+ var ACHROMAT_MATRIX = [
668
+ [0.2126, 0.7152, 0.0722],
669
+ [0.2126, 0.7152, 0.0722],
670
+ [0.2126, 0.7152, 0.0722]
671
+ ];
609
672
  var MATRICES = {
610
673
  protanopia: PROTAN_MATRIX,
611
674
  deuteranopia: DEUTAN_MATRIX,
612
- tritanopia: TRITAN_MATRIX
675
+ tritanopia: TRITAN_MATRIX,
676
+ achromatopsia: ACHROMAT_MATRIX
613
677
  };
614
678
  function linearize(v) {
615
679
  const s = v / 255;
@@ -654,6 +718,10 @@ function relativeLuminance(color2) {
654
718
  const linear = srgb.map((v) => v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4);
655
719
  return 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2];
656
720
  }
721
+ function isOpaqueColor(color2) {
722
+ const c = rgb(color2);
723
+ return c != null && !Number.isNaN(c.r) && c.opacity === 1;
724
+ }
657
725
  function contrastRatio(fg, bg) {
658
726
  const l1 = relativeLuminance(fg);
659
727
  const l2 = relativeLuminance(bg);
@@ -812,6 +880,30 @@ var DIVERGING_PALETTES = {
812
880
  redBlue: [...DIVERGING_RED_BLUE.stops],
813
881
  brownTeal: [...DIVERGING_BROWN_TEAL.stops]
814
882
  };
883
+ var VL_SCHEME_ALIASES = {
884
+ blues: "blue",
885
+ greens: "green",
886
+ oranges: "orange",
887
+ purples: "purple",
888
+ teals: "teal",
889
+ redblue: "redBlue",
890
+ brownteal: "brownTeal",
891
+ // ColorBrewer BrBG, which brownTeal mirrors
892
+ brownbluegreen: "brownTeal",
893
+ category10: "categorical",
894
+ tableau10: "categorical"
895
+ };
896
+ function resolveSchemeName(name) {
897
+ const canonical = SEQUENTIAL_PALETTES[name] || DIVERGING_PALETTES[name] ? name : VL_SCHEME_ALIASES[name.toLowerCase()] ?? name.toLowerCase();
898
+ if (canonical === "categorical") return [...CATEGORICAL_PALETTE];
899
+ return SEQUENTIAL_PALETTES[canonical] ?? DIVERGING_PALETTES[canonical];
900
+ }
901
+ var SUPPORTED_SCHEME_NAMES = [
902
+ ...Object.keys(SEQUENTIAL_PALETTES),
903
+ ...Object.keys(DIVERGING_PALETTES),
904
+ "categorical",
905
+ ...Object.keys(VL_SCHEME_ALIASES)
906
+ ];
815
907
 
816
908
  // src/layout/text-measure.ts
817
909
  var CHAR_WIDTHS_400 = {
@@ -1232,6 +1324,7 @@ var X_AXIS_TITLE_BAND = 22;
1232
1324
  var X_AXIS_TITLE_BAND_ROTATED = 20;
1233
1325
  var X_AXIS_ROTATED_EXTENT_CAP = 120;
1234
1326
  var TICK_LINE_HEIGHT_FACTOR = 1.2;
1327
+ var AXIS_TO_LABEL_GAP = 3;
1235
1328
  function computeXAxisExtentFromLabels(input) {
1236
1329
  const measure = input.measure ?? estimateTextWidth;
1237
1330
  const baseHeight = input.xAxisHeight ?? X_AXIS_BAND_HEIGHT;
@@ -1243,7 +1336,6 @@ function computeXAxisExtentFromLabels(input) {
1243
1336
  if (w > maxLabelWidth) maxLabelWidth = w;
1244
1337
  }
1245
1338
  const lineHeight = input.tickFontSize * TICK_LINE_HEIGHT_FACTOR;
1246
- const AXIS_TO_LABEL_GAP = 3;
1247
1339
  const rotatedHeight = Math.min(
1248
1340
  maxLabelWidth * Math.sin(angleRad) + lineHeight * Math.cos(angleRad) + AXIS_TO_LABEL_GAP,
1249
1341
  X_AXIS_ROTATED_EXTENT_CAP
@@ -1252,6 +1344,32 @@ function computeXAxisExtentFromLabels(input) {
1252
1344
  }
1253
1345
  return input.hasTitle ? baseHeight + X_AXIS_TITLE_BAND : baseHeight;
1254
1346
  }
1347
+ function maxRotatedLabelWidth(tickAngle, tickFontSize) {
1348
+ const angleRad = Math.abs(tickAngle) * (Math.PI / 180);
1349
+ const sin = Math.sin(angleRad);
1350
+ if (sin <= 1e-3) return Number.POSITIVE_INFINITY;
1351
+ const lineHeight = tickFontSize * TICK_LINE_HEIGHT_FACTOR;
1352
+ const usable = X_AXIS_ROTATED_EXTENT_CAP - lineHeight * Math.cos(angleRad) - AXIS_TO_LABEL_GAP;
1353
+ return Math.max(0, usable / sin);
1354
+ }
1355
+ function truncateRotatedLabel(label, tickAngle, tickFontSize, tickFontWeight, measure = estimateTextWidth) {
1356
+ const budget = maxRotatedLabelWidth(tickAngle, tickFontSize);
1357
+ if (!Number.isFinite(budget)) return label;
1358
+ if (measure(label, tickFontSize, tickFontWeight) <= budget) return label;
1359
+ const ELLIPSIS = "\u2026";
1360
+ let lo = 0;
1361
+ let hi = label.length;
1362
+ while (lo < hi) {
1363
+ const mid = Math.ceil((lo + hi) / 2);
1364
+ const candidate = label.slice(0, mid) + ELLIPSIS;
1365
+ if (measure(candidate, tickFontSize, tickFontWeight) <= budget) {
1366
+ lo = mid;
1367
+ } else {
1368
+ hi = mid - 1;
1369
+ }
1370
+ }
1371
+ return lo > 0 ? label.slice(0, lo).trimEnd() + ELLIPSIS : ELLIPSIS;
1372
+ }
1255
1373
 
1256
1374
  // src/theme/defaults.ts
1257
1375
  var DEFAULT_THEME = {
@@ -1790,6 +1908,12 @@ function estimateLineCount(text, style, maxWidth, measureText) {
1790
1908
  }
1791
1909
  return lines;
1792
1910
  }
1911
+ var FOOTNOTE_LINE_HEIGHT = 1.3;
1912
+ var FOOTNOTE_BAND_GAP = 4;
1913
+ function footnoteBandHeight(count, theme) {
1914
+ if (count <= 0) return 0;
1915
+ return count * theme.fonts.sizes.small * FOOTNOTE_LINE_HEIGHT + FOOTNOTE_BAND_GAP;
1916
+ }
1793
1917
  function computeChrome(chrome, theme, width, measureText, chromeMode = "full", padding, watermark = true, bottomLegendHeight = 0) {
1794
1918
  if (!chrome || chromeMode === "hidden") {
1795
1919
  return { topHeight: 0, bottomHeight: 0 };
@@ -2118,7 +2242,8 @@ function resolveCollisions(labels, strategies = OFFSET_STRATEGIES) {
2118
2242
  from: { x: bestX, y: bestY },
2119
2243
  to: { x: label.anchorX, y: label.anchorY },
2120
2244
  stroke: label.style.fill,
2121
- style: "straight"
2245
+ style: "straight",
2246
+ arrow: false
2122
2247
  } : void 0
2123
2248
  });
2124
2249
  } else {
@@ -3145,30 +3270,65 @@ function formatNumber(value, _locale) {
3145
3270
  if (Number.isInteger(value)) {
3146
3271
  return format(",")(value);
3147
3272
  }
3273
+ if (Math.abs(value) < 5e-3) {
3274
+ return format(".2~r")(value);
3275
+ }
3148
3276
  return format(",.2f")(value);
3149
3277
  }
3150
- var ABBREVIATIONS = [
3278
+ var UNIT_TABLE = [
3151
3279
  { threshold: 1e12, suffix: "T", divisor: 1e12 },
3152
3280
  { threshold: 1e9, suffix: "B", divisor: 1e9 },
3153
3281
  { threshold: 1e6, suffix: "M", divisor: 1e6 },
3154
- { threshold: 1e3, suffix: "K", divisor: 1e3 }
3282
+ { threshold: 1e3, suffix: "k", divisor: 1e3 }
3155
3283
  ];
3284
+ function unitFor(abs) {
3285
+ for (const entry of UNIT_TABLE) {
3286
+ if (abs >= entry.threshold) return entry;
3287
+ }
3288
+ return { suffix: "", divisor: 1 };
3289
+ }
3290
+ function fractionDigits(x) {
3291
+ const a = Math.abs(x);
3292
+ for (let p = 0; p <= 3; p++) {
3293
+ const scaled = a * 10 ** p;
3294
+ if (Math.abs(scaled - Math.round(scaled)) < 1e-9) return p;
3295
+ }
3296
+ return 4;
3297
+ }
3156
3298
  function abbreviateNumber(value) {
3157
3299
  if (!Number.isFinite(value)) return String(value);
3158
3300
  const absValue = Math.abs(value);
3159
3301
  const sign = value < 0 ? "-" : "";
3160
- for (const { threshold, suffix, divisor } of ABBREVIATIONS) {
3302
+ for (let i = 0; i < UNIT_TABLE.length; i++) {
3303
+ const { threshold, suffix, divisor } = UNIT_TABLE[i];
3161
3304
  if (absValue >= threshold) {
3162
- const abbreviated = absValue / divisor;
3163
- const formatted = abbreviated % 1 === 0 ? String(abbreviated) : format(".1f")(abbreviated);
3164
- return `${sign}${formatted}${suffix}`;
3305
+ const mantissa = format(".1~f")(absValue / divisor);
3306
+ if (mantissa === "1000" && i > 0) {
3307
+ return `${sign}1${UNIT_TABLE[i - 1].suffix}`;
3308
+ }
3309
+ return `${sign}${mantissa}${suffix}`;
3165
3310
  }
3166
3311
  }
3167
3312
  return formatNumber(value);
3168
3313
  }
3314
+ function formatOrdinal(value) {
3315
+ if (!Number.isFinite(value)) return String(value);
3316
+ const n = Math.round(value);
3317
+ const abs = Math.abs(n);
3318
+ const rem100 = abs % 100;
3319
+ const rem10 = abs % 10;
3320
+ let suffix;
3321
+ if (rem100 >= 11 && rem100 <= 13) suffix = "th";
3322
+ else if (rem10 === 1) suffix = "st";
3323
+ else if (rem10 === 2) suffix = "nd";
3324
+ else if (rem10 === 3) suffix = "rd";
3325
+ else suffix = "th";
3326
+ return `${n}${suffix}`;
3327
+ }
3169
3328
  var D3_FORMAT_SUFFIX_RE = /^(.*~?[efgsrdxXobcnp%])(.+)$/;
3170
3329
  function buildD3Formatter(formatStr) {
3171
3330
  if (!formatStr) return null;
3331
+ if (formatStr === "ordinal") return formatOrdinal;
3172
3332
  try {
3173
3333
  const fmt = format(formatStr);
3174
3334
  if (formatStr.includes("s")) {
@@ -3188,6 +3348,111 @@ function buildD3Formatter(formatStr) {
3188
3348
  }
3189
3349
  return null;
3190
3350
  }
3351
+ function formatPercent(value, options) {
3352
+ const fraction = options?.fraction ?? true;
3353
+ if (fraction) {
3354
+ return format(".1~%")(value);
3355
+ }
3356
+ return `${format(",.1~f")(value)}%`;
3357
+ }
3358
+ function formatCurrency(value, options) {
3359
+ const compact = options?.compact ?? false;
3360
+ const abs = Math.abs(value);
3361
+ const sign = value < 0 ? "-" : "";
3362
+ if (compact) {
3363
+ if (abs >= 1e3) {
3364
+ return `${sign}$${abbreviateNumber(abs)}`;
3365
+ }
3366
+ return `${sign}$${formatNumber(abs)}`;
3367
+ }
3368
+ if (Number.isInteger(value)) {
3369
+ return sign + format("$,")(abs);
3370
+ }
3371
+ return sign + format("$,.2f")(abs);
3372
+ }
3373
+ function computeFieldFormatContext(values, surface) {
3374
+ let min = Infinity;
3375
+ let max = -Infinity;
3376
+ let allIntegers = true;
3377
+ let hasFinite = false;
3378
+ for (const raw of values) {
3379
+ const v = typeof raw === "number" ? raw : typeof raw === "string" ? Number(raw) : NaN;
3380
+ if (!Number.isFinite(v)) continue;
3381
+ hasFinite = true;
3382
+ if (v < min) min = v;
3383
+ if (v > max) max = v;
3384
+ if (allIntegers && !Number.isInteger(v)) allIntegers = false;
3385
+ }
3386
+ const ctx = {};
3387
+ if (hasFinite) {
3388
+ ctx.extent = [min, max];
3389
+ ctx.allIntegers = allIntegers;
3390
+ }
3391
+ if (surface) ctx.surface = surface;
3392
+ return ctx;
3393
+ }
3394
+ function isYearContext(ctx) {
3395
+ if (!ctx?.extent || !ctx.allIntegers) return false;
3396
+ const [min, max] = ctx.extent;
3397
+ return min >= 1500 && max <= 2500;
3398
+ }
3399
+ function resolveNumberFormatter(formatStr, ctx) {
3400
+ if (!formatStr) return null;
3401
+ if (formatStr === "ordinal") return formatOrdinal;
3402
+ if (formatStr === "percent") {
3403
+ const fraction = ctx?.extent ? Math.max(Math.abs(ctx.extent[0]), Math.abs(ctx.extent[1])) <= 1 : true;
3404
+ return (v) => formatPercent(v, { fraction });
3405
+ }
3406
+ if (formatStr === "currency") {
3407
+ const isTable = ctx?.surface === "table";
3408
+ if (!isTable && ctx?.step != null && (ctx.stepReference ?? 0) >= 1e3) {
3409
+ const stepFmt = buildCompactStepFormatter(ctx.step);
3410
+ return (v) => {
3411
+ const sign = v < 0 ? "-" : "";
3412
+ return `${sign}$${stepFmt(Math.abs(v))}`;
3413
+ };
3414
+ }
3415
+ return (v) => formatCurrency(v, { compact: !isTable });
3416
+ }
3417
+ return buildD3Formatter(formatStr);
3418
+ }
3419
+ function defaultNumberFormatter(ctx) {
3420
+ if (isYearContext(ctx)) {
3421
+ return (v) => String(Math.round(v));
3422
+ }
3423
+ if (ctx?.surface === "table") {
3424
+ return formatNumber;
3425
+ }
3426
+ if (ctx?.step != null && (ctx.stepReference ?? maxAbs(ctx.extent)) >= 1e3) {
3427
+ return buildCompactStepFormatter(ctx.step);
3428
+ }
3429
+ return (v) => {
3430
+ if (!Number.isFinite(v)) return String(v);
3431
+ if (ctx?.extent === void 0 && ctx?.allIntegers === void 0) {
3432
+ if (Number.isInteger(v) && v >= 1500 && v <= 2500) return String(v);
3433
+ }
3434
+ return Math.abs(v) >= 1e3 ? abbreviateNumber(v) : formatNumber(v);
3435
+ };
3436
+ }
3437
+ function maxAbs(extent) {
3438
+ if (!extent) return 0;
3439
+ return Math.max(Math.abs(extent[0]), Math.abs(extent[1]));
3440
+ }
3441
+ function buildCompactStepFormatter(step) {
3442
+ return (v) => {
3443
+ if (v === 0) return "0";
3444
+ if (!Number.isFinite(v)) return String(v);
3445
+ const abs = Math.abs(v);
3446
+ if (abs < 1e3) return formatNumber(v);
3447
+ const { suffix, divisor } = unitFor(abs);
3448
+ const p = fractionDigits(step / divisor);
3449
+ if (p > 2) {
3450
+ return Number.isInteger(v) ? format(",")(v) : format(",.2f")(v);
3451
+ }
3452
+ const sign = v < 0 ? "-" : "";
3453
+ return sign + format(`.${p}~f`)(abs / divisor) + suffix;
3454
+ };
3455
+ }
3191
3456
  var GRANULARITY_FORMATS = {
3192
3457
  year: "%Y",
3193
3458
  quarter: "",
@@ -3262,7 +3527,34 @@ function generateAltText(spec, data) {
3262
3527
  const titleText = typeof title === "string" ? title : title.text;
3263
3528
  parts.push(`showing ${titleText}`);
3264
3529
  }
3265
- if (spec.encoding.x && data.length > 0) {
3530
+ if (markType === "waffle") {
3531
+ const units = markDef.units ?? 100;
3532
+ parts.push(`dividing ${units} units`);
3533
+ }
3534
+ if (markType === "parliament") {
3535
+ const valueField = spec.encoding.theta?.field ?? spec.encoding.y?.field;
3536
+ if (valueField) {
3537
+ const totalSeats = data.reduce((sum, d) => {
3538
+ const v = Number(d[valueField]);
3539
+ return sum + (Number.isFinite(v) && v > 0 ? v : 0);
3540
+ }, 0);
3541
+ if (totalSeats > 0) {
3542
+ const majority = Math.floor(totalSeats / 2) + 1;
3543
+ parts.push(`with ${totalSeats} seats, ${majority} needed for a majority`);
3544
+ }
3545
+ }
3546
+ }
3547
+ if (markType === "beeswarm") {
3548
+ const xIsValue = spec.encoding.x?.type === "quantitative";
3549
+ const valueField = xIsValue ? spec.encoding.x?.field : spec.encoding.y?.field;
3550
+ const laneField = xIsValue ? spec.encoding.y?.field : spec.encoding.x?.field;
3551
+ if (valueField) {
3552
+ parts.push(
3553
+ `plotting the distribution of ${valueField}${laneField ? ` by ${laneField}` : ""}`
3554
+ );
3555
+ }
3556
+ }
3557
+ if (spec.encoding.x && markType !== "beeswarm" && data.length > 0) {
3266
3558
  const field = spec.encoding.x.field;
3267
3559
  const values = data.map((d) => d[field]).filter((v) => v != null);
3268
3560
  if (values.length > 0) {
@@ -3347,11 +3639,12 @@ function generateAriaLabels(marks) {
3347
3639
  }
3348
3640
  return labels;
3349
3641
  }
3642
+ var ariaNumberFormatter = defaultNumberFormatter();
3350
3643
  function formatValue(value) {
3351
3644
  if (value == null) return "N/A";
3352
3645
  if (value instanceof Date) return value.toISOString().slice(0, 10);
3353
3646
  if (typeof value === "number") {
3354
- return Number.isInteger(value) ? String(value) : value.toFixed(2);
3647
+ return ariaNumberFormatter(value);
3355
3648
  }
3356
3649
  return String(value);
3357
3650
  }
@@ -3431,7 +3724,7 @@ function pieChart(data, category, value, options) {
3431
3724
  const categoryChannel = resolveField(category, data);
3432
3725
  const valueChannel = resolveField(value, data);
3433
3726
  const encoding = {
3434
- y: valueChannel,
3727
+ theta: valueChannel,
3435
3728
  color: categoryChannel
3436
3729
  };
3437
3730
  if (options?.size && data) {
@@ -3449,7 +3742,7 @@ function donutChart(data, category, value, options) {
3449
3742
  const categoryChannel = resolveField(category, data);
3450
3743
  const valueChannel = resolveField(value, data);
3451
3744
  const encoding = {
3452
- y: valueChannel,
3745
+ theta: valueChannel,
3453
3746
  color: categoryChannel
3454
3747
  };
3455
3748
  if (options?.size && data) {
@@ -3514,6 +3807,182 @@ function tileMap(data, options) {
3514
3807
  ...options?.valueFormat && { valueFormat: options.valueFormat }
3515
3808
  };
3516
3809
  }
3810
+
3811
+ // src/helpers/deep-merge.ts
3812
+ function deepMergeSpec(target, source) {
3813
+ if (source === void 0) return target;
3814
+ if (source === null) return source;
3815
+ if (typeof source !== "object" || Array.isArray(source) || typeof target !== "object" || target === null || Array.isArray(target)) {
3816
+ return source;
3817
+ }
3818
+ const result = { ...target };
3819
+ for (const key of Object.keys(source)) {
3820
+ result[key] = deepMergeSpec(target[key], source[key]);
3821
+ }
3822
+ return result;
3823
+ }
3824
+
3825
+ // src/styles/token-definitions.ts
3826
+ var CSS_TOKEN_ROOT_SELECTORS = [
3827
+ ".oc-root",
3828
+ ".oc-chart-root",
3829
+ ".oc-table-wrapper",
3830
+ ".oc-table-root",
3831
+ ".oc-graph-wrapper",
3832
+ ".oc-graph-root",
3833
+ ".oc-sankey-root",
3834
+ ".oc-tilemap-root",
3835
+ ".oc-barlist-root",
3836
+ ".oc-map-root"
3837
+ ];
3838
+ var CSS_TOKENS = [
3839
+ {
3840
+ name: "--oc-font-family",
3841
+ light: '\n "Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
3842
+ },
3843
+ {
3844
+ name: "--oc-font-mono",
3845
+ light: '"JetBrains Mono", "Fira Code", "Cascadia Code", monospace'
3846
+ },
3847
+ {
3848
+ section: "Animation easing presets via CSS linear()",
3849
+ name: "--oc-ease-smooth",
3850
+ light: "linear(\n 0,\n 0.157,\n 0.438,\n 0.64,\n 0.766,\n 0.85,\n 0.906,\n 0.941,\n 0.964,\n 0.978,\n 0.988,\n 0.994,\n 0.998,\n 1\n )"
3851
+ },
3852
+ {
3853
+ name: "--oc-ease-snappy",
3854
+ light: "linear(\n 0,\n 0.012,\n 0.048,\n 0.108,\n 0.194,\n 0.302,\n 0.426,\n 0.559,\n 0.69,\n 0.808,\n 0.905,\n 0.973,\n 1.013,\n 1.028,\n 1.023,\n 1.006,\n 0.984,\n 0.966,\n 0.957,\n 0.957,\n 0.964,\n 0.975,\n 0.986,\n 0.995,\n 1,\n 1.003,\n 1.002,\n 1,\n 0.998,\n 0.998,\n 0.999,\n 1\n )",
3855
+ consumers: "EASE_VAR_MAP in vanilla renderers"
3856
+ },
3857
+ { section: "Animation timing defaults", name: "--oc-animation-duration", light: "500ms" },
3858
+ { name: "--oc-animation-stagger", light: "80ms" },
3859
+ { name: "--oc-annotation-delay", light: "200ms" },
3860
+ { section: "Typography scale (editorial design system)", name: "--oc-title-size", light: "26px" },
3861
+ { name: "--oc-title-weight", light: "590" },
3862
+ { name: "--oc-title-tracking", light: "-0.022em" },
3863
+ { name: "--oc-subtitle-size", light: "14px" },
3864
+ { name: "--oc-subtitle-weight", light: "450" },
3865
+ { name: "--oc-source-size", light: "11px" },
3866
+ { name: "--oc-source-weight", light: "450" },
3867
+ { name: "--oc-body-size", light: "13px" },
3868
+ { name: "--oc-eyebrow-size", light: "11px" },
3869
+ { name: "--oc-eyebrow-weight", light: "550" },
3870
+ { name: "--oc-eyebrow-tracking", light: "0.08em" },
3871
+ { section: "Surfaces (light mode defaults)", name: "--oc-bg", light: "#ffffff", dark: "#09090b" },
3872
+ { name: "--oc-card", light: "#ffffff", dark: "#111113" },
3873
+ {
3874
+ name: "--oc-secondary",
3875
+ light: "#f4f4f5",
3876
+ dark: "#27272a",
3877
+ comment: "zinc-100, raised surface"
3878
+ },
3879
+ {
3880
+ section: `Text levels. Names invert across modes: in light mode "muted" sits at
3881
+ * a lighter zinc step than "subtle" because both are picked relative to
3882
+ * the active background, not from a fixed lightness ladder. The intent
3883
+ * is "muted = first step away from primary text"; "subtle = next step
3884
+ * down"; etc.
3885
+ *
3886
+ * Light mode: text=zinc-950, secondary=zinc-700, muted=zinc-500,
3887
+ * subtle=zinc-400, faint=zinc-300
3888
+ * Dark mode (dark.css): inverts the surface tokens but keeps the same
3889
+ * muted -> subtle -> faint progression away from primary.`,
3890
+ name: "--oc-text",
3891
+ light: "#09090b",
3892
+ dark: "#f7f8f8"
3893
+ },
3894
+ { name: "--oc-text-secondary", light: "#3f3f46", dark: "#d0d6e0" },
3895
+ { name: "--oc-text-muted", light: "#71717a", dark: "#a1a1aa" },
3896
+ { name: "--oc-text-faint", light: "#d4d4d8", dark: "#52525b" },
3897
+ {
3898
+ section: "Lines",
3899
+ name: "--oc-gridline",
3900
+ light: "rgba(0, 0, 0, 0.1)",
3901
+ dark: "rgba(255, 255, 255, 0.05)"
3902
+ },
3903
+ {
3904
+ name: "--oc-axis",
3905
+ light: "rgba(0, 0, 0, 0.1)",
3906
+ dark: "rgba(255, 255, 255, 0.1)"
3907
+ },
3908
+ {
3909
+ name: "--oc-border",
3910
+ light: "rgba(0, 0, 0, 0.08)",
3911
+ dark: "rgba(255, 255, 255, 0.1)"
3912
+ },
3913
+ { name: "--oc-border-radius", light: "2px" },
3914
+ {
3915
+ section: "Brand and semantic",
3916
+ name: "--oc-accent",
3917
+ light: "#06b6d4",
3918
+ dark: "#06b6d4"
3919
+ },
3920
+ {
3921
+ name: "--oc-accent-strong",
3922
+ light: "#0891b2",
3923
+ dark: "#06b6d4",
3924
+ comment: "darker cyan for line strokes on light bg",
3925
+ consumers: "stamped via adaptForLightLineStroke"
3926
+ },
3927
+ { name: "--oc-positive", light: "#10b981", dark: "#34d399" },
3928
+ { name: "--oc-negative", light: "#e11d48", dark: "#fb7185" },
3929
+ { name: "--oc-focus", light: "#3b82f6", dark: "#60a5fa" },
3930
+ {
3931
+ name: "--oc-focus-ring",
3932
+ light: "rgba(59, 130, 246, 0.1)",
3933
+ comment: "static, intentionally NOT derived from --oc-focus and NOT flipped in dark mode"
3934
+ },
3935
+ {
3936
+ name: "--oc-focus-ring-strong",
3937
+ light: "rgba(59, 130, 246, 0.25)"
3938
+ },
3939
+ {
3940
+ name: "--oc-editable-hover",
3941
+ light: "rgba(79, 70, 229, 0.35)",
3942
+ comment: "edit-mode hover outline, indigo one-off"
3943
+ },
3944
+ {
3945
+ section: "Spacing scale (4px base) \u2014 only --oc-space-2/4 are consumed (JS-stamped)",
3946
+ name: "--oc-space-2",
3947
+ light: "8px",
3948
+ consumers: "stamped at mount from theme.spacing (theme-tokens.ts); component CSS must NOT consume these"
3949
+ },
3950
+ {
3951
+ name: "--oc-space-4",
3952
+ light: "16px",
3953
+ consumers: "stamped at mount from theme.spacing (theme-tokens.ts); component CSS must NOT consume these"
3954
+ },
3955
+ {
3956
+ section: "Interactive states",
3957
+ name: "--oc-hover-bg",
3958
+ light: "rgba(0, 0, 0, 0.025)",
3959
+ dark: "rgba(255, 255, 255, 0.05)"
3960
+ },
3961
+ {
3962
+ name: "--oc-tooltip-bg",
3963
+ light: "rgba(255, 255, 255, 0.88)",
3964
+ dark: "rgba(17, 17, 19, 0.92)"
3965
+ },
3966
+ {
3967
+ name: "--oc-tooltip-border",
3968
+ light: "rgba(0, 0, 0, 0.08)",
3969
+ dark: "rgba(255, 255, 255, 0.08)"
3970
+ },
3971
+ {
3972
+ name: "--oc-tooltip-shadow",
3973
+ light: "0 2px 8px rgba(0, 0, 0, 0.08), 0 0 1px rgba(0, 0, 0, 0.12)",
3974
+ dark: "0 2px 8px rgba(0, 0, 0, 0.3), 0 0 1px rgba(0, 0, 0, 0.4)"
3975
+ },
3976
+ { name: "--oc-tooltip-text", light: "#09090b", dark: "#f7f8f8" },
3977
+ { name: "--oc-legend-text", light: "#3f3f46", dark: "#d0d6e0" }
3978
+ ];
3979
+ var TOKEN_MAP = new Map(CSS_TOKENS.map((t) => [t.name, t]));
3980
+ function cssTokenDefault(name, mode) {
3981
+ const token = TOKEN_MAP.get(name);
3982
+ if (!token) throw new Error(`Unknown CSS token: ${name}`);
3983
+ if (mode === "dark" && token.dark !== void 0) return token.dark;
3984
+ return token.light;
3985
+ }
3517
3986
  export {
3518
3987
  ACHROMATIC_RAMP,
3519
3988
  AXIS_TITLE_GAP,
@@ -3526,12 +3995,13 @@ export {
3526
3995
  BREAKPOINT_COMPACT_MAX,
3527
3996
  BREAKPOINT_MEDIUM_MAX,
3528
3997
  CATEGORICAL_PALETTE,
3529
- CHART_ENCODING_RULES,
3530
- CHART_TYPES,
3531
3998
  COMPACT_WIDTH,
3999
+ CSS_TOKENS,
4000
+ CSS_TOKEN_ROOT_SELECTORS,
3532
4001
  DEFAULT_THEME,
3533
4002
  DIVERGING_PALETTES,
3534
4003
  EXTENDED_OFFSET_STRATEGIES,
4004
+ FOOTNOTE_LINE_HEIGHT,
3535
4005
  GRAPH_ENCODING_RULES,
3536
4006
  HEIGHT_CRAMPED_MAX,
3537
4007
  HEIGHT_SHORT_MAX,
@@ -3550,6 +4020,7 @@ export {
3550
4020
  NARROW_VIEWPORT_MAX,
3551
4021
  OFFSET_STRATEGIES,
3552
4022
  SEQUENTIAL_PALETTES,
4023
+ SUPPORTED_SCHEME_NAMES,
3553
4024
  TICK_LABEL_OFFSET,
3554
4025
  TICK_LINE_HEIGHT_FACTOR,
3555
4026
  TOP_PAD_EXTRA_NARROW,
@@ -3565,15 +4036,20 @@ export {
3565
4036
  areaChart,
3566
4037
  axisTitleOffset,
3567
4038
  barChart,
4039
+ buildCompactStepFormatter,
3568
4040
  buildD3Formatter,
3569
4041
  buildTemporalFormatter,
3570
4042
  checkPaletteDistinguishability,
3571
4043
  columnChart,
3572
4044
  computeChrome,
4045
+ computeFieldFormatContext,
3573
4046
  computeLabelBounds,
3574
4047
  computeXAxisExtentFromLabels,
3575
4048
  contrastRatio,
4049
+ cssTokenDefault,
3576
4050
  dataTable,
4051
+ deepMergeSpec,
4052
+ defaultNumberFormatter,
3577
4053
  detectCollision,
3578
4054
  donutChart,
3579
4055
  dotChart,
@@ -3582,8 +4058,12 @@ export {
3582
4058
  essay,
3583
4059
  estimateTextWidth,
3584
4060
  findAccessibleColor,
4061
+ footnoteBandHeight,
4062
+ formatCurrency,
3585
4063
  formatDate,
3586
4064
  formatNumber,
4065
+ formatOrdinal,
4066
+ formatPercent,
3587
4067
  generateAltText,
3588
4068
  generateAriaLabels,
3589
4069
  generateDataTable,
@@ -3594,6 +4074,7 @@ export {
3594
4074
  getRepresentativeColor,
3595
4075
  heuristicMeasure,
3596
4076
  inferFieldType,
4077
+ isAxislessMark,
3597
4078
  isBarListSpec,
3598
4079
  isChartSpec,
3599
4080
  isConditionalDef,
@@ -3601,13 +4082,18 @@ export {
3601
4082
  isGradientDef,
3602
4083
  isGraphSpec,
3603
4084
  isLayerSpec,
4085
+ isMapSpec,
4086
+ isOpaqueColor,
3604
4087
  isRangeAnnotation,
3605
4088
  isRefLineAnnotation,
3606
4089
  isSankeySpec,
3607
4090
  isTableSpec,
3608
4091
  isTextAnnotation,
3609
4092
  isTileMapSpec,
4093
+ isValueDef,
4094
+ isYearContext,
3610
4095
  lineChart,
4096
+ maxRotatedLabelWidth,
3611
4097
  meetsAA,
3612
4098
  overlapArea,
3613
4099
  pickLabelColor,
@@ -3616,11 +4102,14 @@ export {
3616
4102
  resolveMarkDef,
3617
4103
  resolveMarkType,
3618
4104
  resolveMeasurer,
4105
+ resolveNumberFormatter,
4106
+ resolveSchemeName,
3619
4107
  resolveTheme,
3620
4108
  scatterChart,
3621
4109
  simulateColorBlindness,
3622
4110
  textAscent,
3623
4111
  tileMap,
4112
+ truncateRotatedLabel,
3624
4113
  wire,
3625
4114
  wrapText
3626
4115
  };