@opendata-ai/openchart-core 7.11.0 → 8.0.0-rc.10

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 (59) hide show
  1. package/dist/index.d.ts +1348 -132
  2. package/dist/index.js +556 -48
  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 +9086 -0
  8. package/schema/table.schema.json +861 -0
  9. package/schema/vizspec.schema.json +17922 -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/breakpoints.ts +7 -2
  29. package/src/responsive/index.ts +2 -0
  30. package/src/responsive/metrics.ts +77 -10
  31. package/src/schema/__tests__/generated-artifacts.test.ts +42 -0
  32. package/src/schema/__tests__/schema.test.ts +179 -0
  33. package/src/styles/__tests__/reduced-motion-selectors.test.ts +28 -0
  34. package/src/styles/animation.css +211 -168
  35. package/src/styles/base.css +89 -62
  36. package/src/styles/chrome.css +173 -179
  37. package/src/styles/dark.css +38 -35
  38. package/src/styles/graph.css +116 -111
  39. package/src/styles/index.css +5 -0
  40. package/src/styles/keyframes.css +130 -99
  41. package/src/styles/legend.css +14 -12
  42. package/src/styles/reduced-motion.css +61 -40
  43. package/src/styles/series-search.css +153 -0
  44. package/src/styles/sparkline.css +14 -12
  45. package/src/styles/table-animation.css +82 -80
  46. package/src/styles/table.css +314 -302
  47. package/src/styles/token-definitions.ts +199 -0
  48. package/src/styles/tokens.css +132 -128
  49. package/src/styles/tooltip.css +109 -115
  50. package/src/styles/you-draw-it.css +126 -0
  51. package/src/types/__tests__/encoding.test.ts +86 -17
  52. package/src/types/__tests__/spec.test.ts +16 -12
  53. package/src/types/encoding.ts +69 -25
  54. package/src/types/events.ts +107 -13
  55. package/src/types/index.ts +42 -3
  56. package/src/types/layout.ts +484 -11
  57. package/src/types/schema-roots.ts +32 -0
  58. package/src/types/spec.ts +718 -87
  59. 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"] },
@@ -149,14 +187,31 @@ var GRAPH_ENCODING_RULES = {
149
187
 
150
188
  // src/types/events.ts
151
189
  var elementRef = {
152
- annotation: (index, id) => ({ type: "annotation", index, id }),
153
- chrome: (key) => ({ type: "chrome", key }),
154
- seriesLabel: (series) => ({ type: "series-label", series }),
155
- legend: () => ({ type: "legend" }),
156
- legendEntry: (series, index) => ({
190
+ annotation: (index, id, path) => ({
191
+ type: "annotation",
192
+ index,
193
+ ...id !== void 0 ? { id } : {},
194
+ ...path !== void 0 ? { path } : {}
195
+ }),
196
+ chrome: (key, path) => ({
197
+ type: "chrome",
198
+ key,
199
+ ...path !== void 0 ? { path } : {}
200
+ }),
201
+ seriesLabel: (series, path) => ({
202
+ type: "series-label",
203
+ series,
204
+ ...path !== void 0 ? { path } : {}
205
+ }),
206
+ legend: (path) => ({
207
+ type: "legend",
208
+ ...path !== void 0 ? { path } : {}
209
+ }),
210
+ legendEntry: (series, index, path) => ({
157
211
  type: "legend-entry",
158
212
  series,
159
- index
213
+ index,
214
+ ...path !== void 0 ? { path } : {}
160
215
  })
161
216
  };
162
217
 
@@ -177,6 +232,10 @@ function isConditionalDef(def) {
177
232
  if (!def) return false;
178
233
  return "condition" in def;
179
234
  }
235
+ function isValueDef(def) {
236
+ if (!def) return false;
237
+ return "value" in def && !("condition" in def) && !("field" in def);
238
+ }
180
239
  var MARK_TYPES = /* @__PURE__ */ new Set([
181
240
  "bar",
182
241
  "line",
@@ -188,9 +247,16 @@ var MARK_TYPES = /* @__PURE__ */ new Set([
188
247
  "rule",
189
248
  "tick",
190
249
  "rect",
191
- "lollipop"
250
+ "lollipop",
251
+ "beeswarm",
252
+ "range",
253
+ "waffle",
254
+ "calendar",
255
+ "parliament"
192
256
  ]);
193
- var CHART_TYPES = MARK_TYPES;
257
+ function isAxislessMark(markType) {
258
+ return markType === "arc" || markType === "waffle" || markType === "calendar" || markType === "parliament";
259
+ }
194
260
  function resolveMarkType(mark) {
195
261
  return typeof mark === "string" ? mark : mark.type;
196
262
  }
@@ -218,6 +284,9 @@ function isTileMapSpec(spec) {
218
284
  function isBarListSpec(spec) {
219
285
  return "type" in spec && spec.type === "barlist";
220
286
  }
287
+ function isMapSpec(spec) {
288
+ return "type" in spec && spec.type === "map";
289
+ }
221
290
  function isTextAnnotation(annotation) {
222
291
  return annotation.type === "text";
223
292
  }
@@ -225,7 +294,8 @@ function isRangeAnnotation(annotation) {
225
294
  return annotation.type === "range";
226
295
  }
227
296
  function isRefLineAnnotation(annotation) {
228
- return annotation.type === "refline" || annotation.type === "rule";
297
+ const type = annotation.type;
298
+ return type === "refline" || type === "rule";
229
299
  }
230
300
  var MARK_DISPLAY_NAMES = {
231
301
  bar: "Bar chart",
@@ -239,7 +309,12 @@ var MARK_DISPLAY_NAMES = {
239
309
  rule: "Rule chart",
240
310
  tick: "Tick plot",
241
311
  rect: "Heatmap",
242
- lollipop: "Lollipop chart"
312
+ lollipop: "Lollipop chart",
313
+ beeswarm: "Beeswarm chart",
314
+ range: "Range plot",
315
+ waffle: "Waffle chart",
316
+ calendar: "Calendar heatmap",
317
+ parliament: "Parliament chart"
243
318
  };
244
319
 
245
320
  // ../../node_modules/.bun/d3-color@3.1.0/node_modules/d3-color/src/define.js
@@ -606,10 +681,16 @@ var TRITAN_MATRIX = [
606
681
  [0, 0.433, 0.567],
607
682
  [0, 0.475, 0.525]
608
683
  ];
684
+ var ACHROMAT_MATRIX = [
685
+ [0.2126, 0.7152, 0.0722],
686
+ [0.2126, 0.7152, 0.0722],
687
+ [0.2126, 0.7152, 0.0722]
688
+ ];
609
689
  var MATRICES = {
610
690
  protanopia: PROTAN_MATRIX,
611
691
  deuteranopia: DEUTAN_MATRIX,
612
- tritanopia: TRITAN_MATRIX
692
+ tritanopia: TRITAN_MATRIX,
693
+ achromatopsia: ACHROMAT_MATRIX
613
694
  };
614
695
  function linearize(v) {
615
696
  const s = v / 255;
@@ -654,6 +735,10 @@ function relativeLuminance(color2) {
654
735
  const linear = srgb.map((v) => v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4);
655
736
  return 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2];
656
737
  }
738
+ function isOpaqueColor(color2) {
739
+ const c = rgb(color2);
740
+ return c != null && !Number.isNaN(c.r) && c.opacity === 1;
741
+ }
657
742
  function contrastRatio(fg, bg) {
658
743
  const l1 = relativeLuminance(fg);
659
744
  const l2 = relativeLuminance(bg);
@@ -812,6 +897,30 @@ var DIVERGING_PALETTES = {
812
897
  redBlue: [...DIVERGING_RED_BLUE.stops],
813
898
  brownTeal: [...DIVERGING_BROWN_TEAL.stops]
814
899
  };
900
+ var VL_SCHEME_ALIASES = {
901
+ blues: "blue",
902
+ greens: "green",
903
+ oranges: "orange",
904
+ purples: "purple",
905
+ teals: "teal",
906
+ redblue: "redBlue",
907
+ brownteal: "brownTeal",
908
+ // ColorBrewer BrBG, which brownTeal mirrors
909
+ brownbluegreen: "brownTeal",
910
+ category10: "categorical",
911
+ tableau10: "categorical"
912
+ };
913
+ function resolveSchemeName(name) {
914
+ const canonical = SEQUENTIAL_PALETTES[name] || DIVERGING_PALETTES[name] ? name : VL_SCHEME_ALIASES[name.toLowerCase()] ?? name.toLowerCase();
915
+ if (canonical === "categorical") return [...CATEGORICAL_PALETTE];
916
+ return SEQUENTIAL_PALETTES[canonical] ?? DIVERGING_PALETTES[canonical];
917
+ }
918
+ var SUPPORTED_SCHEME_NAMES = [
919
+ ...Object.keys(SEQUENTIAL_PALETTES),
920
+ ...Object.keys(DIVERGING_PALETTES),
921
+ "categorical",
922
+ ...Object.keys(VL_SCHEME_ALIASES)
923
+ ];
815
924
 
816
925
  // src/layout/text-measure.ts
817
926
  var CHAR_WIDTHS_400 = {
@@ -1232,6 +1341,7 @@ var X_AXIS_TITLE_BAND = 22;
1232
1341
  var X_AXIS_TITLE_BAND_ROTATED = 20;
1233
1342
  var X_AXIS_ROTATED_EXTENT_CAP = 120;
1234
1343
  var TICK_LINE_HEIGHT_FACTOR = 1.2;
1344
+ var AXIS_TO_LABEL_GAP = 3;
1235
1345
  function computeXAxisExtentFromLabels(input) {
1236
1346
  const measure = input.measure ?? estimateTextWidth;
1237
1347
  const baseHeight = input.xAxisHeight ?? X_AXIS_BAND_HEIGHT;
@@ -1243,14 +1353,41 @@ function computeXAxisExtentFromLabels(input) {
1243
1353
  if (w > maxLabelWidth) maxLabelWidth = w;
1244
1354
  }
1245
1355
  const lineHeight = input.tickFontSize * TICK_LINE_HEIGHT_FACTOR;
1246
- const AXIS_TO_LABEL_GAP = 3;
1247
1356
  const rotatedHeight = Math.min(
1248
1357
  maxLabelWidth * Math.sin(angleRad) + lineHeight * Math.cos(angleRad) + AXIS_TO_LABEL_GAP,
1249
1358
  X_AXIS_ROTATED_EXTENT_CAP
1250
1359
  );
1251
1360
  return input.hasTitle ? rotatedHeight + X_AXIS_TITLE_BAND_ROTATED : rotatedHeight;
1252
1361
  }
1253
- return input.hasTitle ? baseHeight + X_AXIS_TITLE_BAND : baseHeight;
1362
+ const tickExtent = 4 + input.tickFontSize + Math.max(11, Math.ceil(input.tickFontSize * 0.7));
1363
+ const flatHeight = Math.max(baseHeight, tickExtent);
1364
+ return input.hasTitle ? flatHeight + X_AXIS_TITLE_BAND : flatHeight;
1365
+ }
1366
+ function maxRotatedLabelWidth(tickAngle, tickFontSize) {
1367
+ const angleRad = Math.abs(tickAngle) * (Math.PI / 180);
1368
+ const sin = Math.sin(angleRad);
1369
+ if (sin <= 1e-3) return Number.POSITIVE_INFINITY;
1370
+ const lineHeight = tickFontSize * TICK_LINE_HEIGHT_FACTOR;
1371
+ const usable = X_AXIS_ROTATED_EXTENT_CAP - lineHeight * Math.cos(angleRad) - AXIS_TO_LABEL_GAP;
1372
+ return Math.max(0, usable / sin);
1373
+ }
1374
+ function truncateRotatedLabel(label, tickAngle, tickFontSize, tickFontWeight, measure = estimateTextWidth) {
1375
+ const budget = maxRotatedLabelWidth(tickAngle, tickFontSize);
1376
+ if (!Number.isFinite(budget)) return label;
1377
+ if (measure(label, tickFontSize, tickFontWeight) <= budget) return label;
1378
+ const ELLIPSIS = "\u2026";
1379
+ let lo = 0;
1380
+ let hi = label.length;
1381
+ while (lo < hi) {
1382
+ const mid = Math.ceil((lo + hi) / 2);
1383
+ const candidate = label.slice(0, mid) + ELLIPSIS;
1384
+ if (measure(candidate, tickFontSize, tickFontWeight) <= budget) {
1385
+ lo = mid;
1386
+ } else {
1387
+ hi = mid - 1;
1388
+ }
1389
+ }
1390
+ return lo > 0 ? label.slice(0, lo).trimEnd() + ELLIPSIS : ELLIPSIS;
1254
1391
  }
1255
1392
 
1256
1393
  // src/theme/defaults.ts
@@ -1790,6 +1927,12 @@ function estimateLineCount(text, style, maxWidth, measureText) {
1790
1927
  }
1791
1928
  return lines;
1792
1929
  }
1930
+ var FOOTNOTE_LINE_HEIGHT = 1.3;
1931
+ var FOOTNOTE_BAND_GAP = 4;
1932
+ function footnoteBandHeight(count, theme) {
1933
+ if (count <= 0) return 0;
1934
+ return count * theme.fonts.sizes.small * FOOTNOTE_LINE_HEIGHT + FOOTNOTE_BAND_GAP;
1935
+ }
1793
1936
  function computeChrome(chrome, theme, width, measureText, chromeMode = "full", padding, watermark = true, bottomLegendHeight = 0) {
1794
1937
  if (!chrome || chromeMode === "hidden") {
1795
1938
  return { topHeight: 0, bottomHeight: 0 };
@@ -2118,7 +2261,8 @@ function resolveCollisions(labels, strategies = OFFSET_STRATEGIES) {
2118
2261
  from: { x: bestX, y: bestY },
2119
2262
  to: { x: label.anchorX, y: label.anchorY },
2120
2263
  stroke: label.style.fill,
2121
- style: "straight"
2264
+ style: "straight",
2265
+ arrow: false
2122
2266
  } : void 0
2123
2267
  });
2124
2268
  } else {
@@ -3145,30 +3289,65 @@ function formatNumber(value, _locale) {
3145
3289
  if (Number.isInteger(value)) {
3146
3290
  return format(",")(value);
3147
3291
  }
3292
+ if (Math.abs(value) < 5e-3) {
3293
+ return format(".2~r")(value);
3294
+ }
3148
3295
  return format(",.2f")(value);
3149
3296
  }
3150
- var ABBREVIATIONS = [
3297
+ var UNIT_TABLE = [
3151
3298
  { threshold: 1e12, suffix: "T", divisor: 1e12 },
3152
3299
  { threshold: 1e9, suffix: "B", divisor: 1e9 },
3153
3300
  { threshold: 1e6, suffix: "M", divisor: 1e6 },
3154
- { threshold: 1e3, suffix: "K", divisor: 1e3 }
3301
+ { threshold: 1e3, suffix: "k", divisor: 1e3 }
3155
3302
  ];
3303
+ function unitFor(abs) {
3304
+ for (const entry of UNIT_TABLE) {
3305
+ if (abs >= entry.threshold) return entry;
3306
+ }
3307
+ return { suffix: "", divisor: 1 };
3308
+ }
3309
+ function fractionDigits(x) {
3310
+ const a = Math.abs(x);
3311
+ for (let p = 0; p <= 3; p++) {
3312
+ const scaled = a * 10 ** p;
3313
+ if (Math.abs(scaled - Math.round(scaled)) < 1e-9) return p;
3314
+ }
3315
+ return 4;
3316
+ }
3156
3317
  function abbreviateNumber(value) {
3157
3318
  if (!Number.isFinite(value)) return String(value);
3158
3319
  const absValue = Math.abs(value);
3159
3320
  const sign = value < 0 ? "-" : "";
3160
- for (const { threshold, suffix, divisor } of ABBREVIATIONS) {
3321
+ for (let i = 0; i < UNIT_TABLE.length; i++) {
3322
+ const { threshold, suffix, divisor } = UNIT_TABLE[i];
3161
3323
  if (absValue >= threshold) {
3162
- const abbreviated = absValue / divisor;
3163
- const formatted = abbreviated % 1 === 0 ? String(abbreviated) : format(".1f")(abbreviated);
3164
- return `${sign}${formatted}${suffix}`;
3324
+ const mantissa = format(".1~f")(absValue / divisor);
3325
+ if (mantissa === "1000" && i > 0) {
3326
+ return `${sign}1${UNIT_TABLE[i - 1].suffix}`;
3327
+ }
3328
+ return `${sign}${mantissa}${suffix}`;
3165
3329
  }
3166
3330
  }
3167
3331
  return formatNumber(value);
3168
3332
  }
3333
+ function formatOrdinal(value) {
3334
+ if (!Number.isFinite(value)) return String(value);
3335
+ const n = Math.round(value);
3336
+ const abs = Math.abs(n);
3337
+ const rem100 = abs % 100;
3338
+ const rem10 = abs % 10;
3339
+ let suffix;
3340
+ if (rem100 >= 11 && rem100 <= 13) suffix = "th";
3341
+ else if (rem10 === 1) suffix = "st";
3342
+ else if (rem10 === 2) suffix = "nd";
3343
+ else if (rem10 === 3) suffix = "rd";
3344
+ else suffix = "th";
3345
+ return `${n}${suffix}`;
3346
+ }
3169
3347
  var D3_FORMAT_SUFFIX_RE = /^(.*~?[efgsrdxXobcnp%])(.+)$/;
3170
3348
  function buildD3Formatter(formatStr) {
3171
3349
  if (!formatStr) return null;
3350
+ if (formatStr === "ordinal") return formatOrdinal;
3172
3351
  try {
3173
3352
  const fmt = format(formatStr);
3174
3353
  if (formatStr.includes("s")) {
@@ -3188,6 +3367,111 @@ function buildD3Formatter(formatStr) {
3188
3367
  }
3189
3368
  return null;
3190
3369
  }
3370
+ function formatPercent(value, options) {
3371
+ const fraction = options?.fraction ?? true;
3372
+ if (fraction) {
3373
+ return format(".1~%")(value);
3374
+ }
3375
+ return `${format(",.1~f")(value)}%`;
3376
+ }
3377
+ function formatCurrency(value, options) {
3378
+ const compact = options?.compact ?? false;
3379
+ const abs = Math.abs(value);
3380
+ const sign = value < 0 ? "-" : "";
3381
+ if (compact) {
3382
+ if (abs >= 1e3) {
3383
+ return `${sign}$${abbreviateNumber(abs)}`;
3384
+ }
3385
+ return `${sign}$${formatNumber(abs)}`;
3386
+ }
3387
+ if (Number.isInteger(value)) {
3388
+ return sign + format("$,")(abs);
3389
+ }
3390
+ return sign + format("$,.2f")(abs);
3391
+ }
3392
+ function computeFieldFormatContext(values, surface) {
3393
+ let min = Infinity;
3394
+ let max = -Infinity;
3395
+ let allIntegers = true;
3396
+ let hasFinite = false;
3397
+ for (const raw of values) {
3398
+ const v = typeof raw === "number" ? raw : typeof raw === "string" ? Number(raw) : NaN;
3399
+ if (!Number.isFinite(v)) continue;
3400
+ hasFinite = true;
3401
+ if (v < min) min = v;
3402
+ if (v > max) max = v;
3403
+ if (allIntegers && !Number.isInteger(v)) allIntegers = false;
3404
+ }
3405
+ const ctx = {};
3406
+ if (hasFinite) {
3407
+ ctx.extent = [min, max];
3408
+ ctx.allIntegers = allIntegers;
3409
+ }
3410
+ if (surface) ctx.surface = surface;
3411
+ return ctx;
3412
+ }
3413
+ function isYearContext(ctx) {
3414
+ if (!ctx?.extent || !ctx.allIntegers) return false;
3415
+ const [min, max] = ctx.extent;
3416
+ return min >= 1500 && max <= 2500;
3417
+ }
3418
+ function resolveNumberFormatter(formatStr, ctx) {
3419
+ if (!formatStr) return null;
3420
+ if (formatStr === "ordinal") return formatOrdinal;
3421
+ if (formatStr === "percent") {
3422
+ const fraction = ctx?.extent ? Math.max(Math.abs(ctx.extent[0]), Math.abs(ctx.extent[1])) <= 1 : true;
3423
+ return (v) => formatPercent(v, { fraction });
3424
+ }
3425
+ if (formatStr === "currency") {
3426
+ const isTable = ctx?.surface === "table";
3427
+ if (!isTable && ctx?.step != null && (ctx.stepReference ?? 0) >= 1e3) {
3428
+ const stepFmt = buildCompactStepFormatter(ctx.step);
3429
+ return (v) => {
3430
+ const sign = v < 0 ? "-" : "";
3431
+ return `${sign}$${stepFmt(Math.abs(v))}`;
3432
+ };
3433
+ }
3434
+ return (v) => formatCurrency(v, { compact: !isTable });
3435
+ }
3436
+ return buildD3Formatter(formatStr);
3437
+ }
3438
+ function defaultNumberFormatter(ctx) {
3439
+ if (isYearContext(ctx)) {
3440
+ return (v) => String(Math.round(v));
3441
+ }
3442
+ if (ctx?.surface === "table") {
3443
+ return formatNumber;
3444
+ }
3445
+ if (ctx?.step != null && (ctx.stepReference ?? maxAbs(ctx.extent)) >= 1e3) {
3446
+ return buildCompactStepFormatter(ctx.step);
3447
+ }
3448
+ return (v) => {
3449
+ if (!Number.isFinite(v)) return String(v);
3450
+ if (ctx?.extent === void 0 && ctx?.allIntegers === void 0) {
3451
+ if (Number.isInteger(v) && v >= 1500 && v <= 2500) return String(v);
3452
+ }
3453
+ return Math.abs(v) >= 1e3 ? abbreviateNumber(v) : formatNumber(v);
3454
+ };
3455
+ }
3456
+ function maxAbs(extent) {
3457
+ if (!extent) return 0;
3458
+ return Math.max(Math.abs(extent[0]), Math.abs(extent[1]));
3459
+ }
3460
+ function buildCompactStepFormatter(step) {
3461
+ return (v) => {
3462
+ if (v === 0) return "0";
3463
+ if (!Number.isFinite(v)) return String(v);
3464
+ const abs = Math.abs(v);
3465
+ if (abs < 1e3) return formatNumber(v);
3466
+ const { suffix, divisor } = unitFor(abs);
3467
+ const p = fractionDigits(step / divisor);
3468
+ if (p > 2) {
3469
+ return Number.isInteger(v) ? format(",")(v) : format(",.2f")(v);
3470
+ }
3471
+ const sign = v < 0 ? "-" : "";
3472
+ return sign + format(`.${p}~f`)(abs / divisor) + suffix;
3473
+ };
3474
+ }
3191
3475
  var GRANULARITY_FORMATS = {
3192
3476
  year: "%Y",
3193
3477
  quarter: "",
@@ -3262,7 +3546,34 @@ function generateAltText(spec, data) {
3262
3546
  const titleText = typeof title === "string" ? title : title.text;
3263
3547
  parts.push(`showing ${titleText}`);
3264
3548
  }
3265
- if (spec.encoding.x && data.length > 0) {
3549
+ if (markType === "waffle") {
3550
+ const units = markDef.units ?? 100;
3551
+ parts.push(`dividing ${units} units`);
3552
+ }
3553
+ if (markType === "parliament") {
3554
+ const valueField = spec.encoding.theta?.field ?? spec.encoding.y?.field;
3555
+ if (valueField) {
3556
+ const totalSeats = data.reduce((sum, d) => {
3557
+ const v = Number(d[valueField]);
3558
+ return sum + (Number.isFinite(v) && v > 0 ? v : 0);
3559
+ }, 0);
3560
+ if (totalSeats > 0) {
3561
+ const majority = Math.floor(totalSeats / 2) + 1;
3562
+ parts.push(`with ${totalSeats} seats, ${majority} needed for a majority`);
3563
+ }
3564
+ }
3565
+ }
3566
+ if (markType === "beeswarm") {
3567
+ const xIsValue = spec.encoding.x?.type === "quantitative";
3568
+ const valueField = xIsValue ? spec.encoding.x?.field : spec.encoding.y?.field;
3569
+ const laneField = xIsValue ? spec.encoding.y?.field : spec.encoding.x?.field;
3570
+ if (valueField) {
3571
+ parts.push(
3572
+ `plotting the distribution of ${valueField}${laneField ? ` by ${laneField}` : ""}`
3573
+ );
3574
+ }
3575
+ }
3576
+ if (spec.encoding.x && markType !== "beeswarm" && data.length > 0) {
3266
3577
  const field = spec.encoding.x.field;
3267
3578
  const values = data.map((d) => d[field]).filter((v) => v != null);
3268
3579
  if (values.length > 0) {
@@ -3347,11 +3658,12 @@ function generateAriaLabels(marks) {
3347
3658
  }
3348
3659
  return labels;
3349
3660
  }
3661
+ var ariaNumberFormatter = defaultNumberFormatter();
3350
3662
  function formatValue(value) {
3351
3663
  if (value == null) return "N/A";
3352
3664
  if (value instanceof Date) return value.toISOString().slice(0, 10);
3353
3665
  if (typeof value === "number") {
3354
- return Number.isInteger(value) ? String(value) : value.toFixed(2);
3666
+ return ariaNumberFormatter(value);
3355
3667
  }
3356
3668
  return String(value);
3357
3669
  }
@@ -3431,7 +3743,7 @@ function pieChart(data, category, value, options) {
3431
3743
  const categoryChannel = resolveField(category, data);
3432
3744
  const valueChannel = resolveField(value, data);
3433
3745
  const encoding = {
3434
- y: valueChannel,
3746
+ theta: valueChannel,
3435
3747
  color: categoryChannel
3436
3748
  };
3437
3749
  if (options?.size && data) {
@@ -3449,7 +3761,7 @@ function donutChart(data, category, value, options) {
3449
3761
  const categoryChannel = resolveField(category, data);
3450
3762
  const valueChannel = resolveField(value, data);
3451
3763
  const encoding = {
3452
- y: valueChannel,
3764
+ theta: valueChannel,
3453
3765
  color: categoryChannel
3454
3766
  };
3455
3767
  if (options?.size && data) {
@@ -3514,6 +3826,182 @@ function tileMap(data, options) {
3514
3826
  ...options?.valueFormat && { valueFormat: options.valueFormat }
3515
3827
  };
3516
3828
  }
3829
+
3830
+ // src/helpers/deep-merge.ts
3831
+ function deepMergeSpec(target, source) {
3832
+ if (source === void 0) return target;
3833
+ if (source === null) return source;
3834
+ if (typeof source !== "object" || Array.isArray(source) || typeof target !== "object" || target === null || Array.isArray(target)) {
3835
+ return source;
3836
+ }
3837
+ const result = { ...target };
3838
+ for (const key of Object.keys(source)) {
3839
+ result[key] = deepMergeSpec(target[key], source[key]);
3840
+ }
3841
+ return result;
3842
+ }
3843
+
3844
+ // src/styles/token-definitions.ts
3845
+ var CSS_TOKEN_ROOT_SELECTORS = [
3846
+ ".oc-root",
3847
+ ".oc-chart-root",
3848
+ ".oc-table-wrapper",
3849
+ ".oc-table-root",
3850
+ ".oc-graph-wrapper",
3851
+ ".oc-graph-root",
3852
+ ".oc-sankey-root",
3853
+ ".oc-tilemap-root",
3854
+ ".oc-barlist-root",
3855
+ ".oc-map-root"
3856
+ ];
3857
+ var CSS_TOKENS = [
3858
+ {
3859
+ name: "--oc-font-family",
3860
+ light: '\n "Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
3861
+ },
3862
+ {
3863
+ name: "--oc-font-mono",
3864
+ light: '"JetBrains Mono", "Fira Code", "Cascadia Code", monospace'
3865
+ },
3866
+ {
3867
+ section: "Animation easing presets via CSS linear()",
3868
+ name: "--oc-ease-smooth",
3869
+ 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 )"
3870
+ },
3871
+ {
3872
+ name: "--oc-ease-snappy",
3873
+ 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 )",
3874
+ consumers: "EASE_VAR_MAP in vanilla renderers"
3875
+ },
3876
+ { section: "Animation timing defaults", name: "--oc-animation-duration", light: "500ms" },
3877
+ { name: "--oc-animation-stagger", light: "80ms" },
3878
+ { name: "--oc-annotation-delay", light: "200ms" },
3879
+ { section: "Typography scale (editorial design system)", name: "--oc-title-size", light: "26px" },
3880
+ { name: "--oc-title-weight", light: "590" },
3881
+ { name: "--oc-title-tracking", light: "-0.022em" },
3882
+ { name: "--oc-subtitle-size", light: "14px" },
3883
+ { name: "--oc-subtitle-weight", light: "450" },
3884
+ { name: "--oc-source-size", light: "11px" },
3885
+ { name: "--oc-source-weight", light: "450" },
3886
+ { name: "--oc-body-size", light: "13px" },
3887
+ { name: "--oc-eyebrow-size", light: "11px" },
3888
+ { name: "--oc-eyebrow-weight", light: "550" },
3889
+ { name: "--oc-eyebrow-tracking", light: "0.08em" },
3890
+ { section: "Surfaces (light mode defaults)", name: "--oc-bg", light: "#ffffff", dark: "#09090b" },
3891
+ { name: "--oc-card", light: "#ffffff", dark: "#111113" },
3892
+ {
3893
+ name: "--oc-secondary",
3894
+ light: "#f4f4f5",
3895
+ dark: "#27272a",
3896
+ comment: "zinc-100, raised surface"
3897
+ },
3898
+ {
3899
+ section: `Text levels. Names invert across modes: in light mode "muted" sits at
3900
+ * a lighter zinc step than "subtle" because both are picked relative to
3901
+ * the active background, not from a fixed lightness ladder. The intent
3902
+ * is "muted = first step away from primary text"; "subtle = next step
3903
+ * down"; etc.
3904
+ *
3905
+ * Light mode: text=zinc-950, secondary=zinc-700, muted=zinc-500,
3906
+ * subtle=zinc-400, faint=zinc-300
3907
+ * Dark mode (dark.css): inverts the surface tokens but keeps the same
3908
+ * muted -> subtle -> faint progression away from primary.`,
3909
+ name: "--oc-text",
3910
+ light: "#09090b",
3911
+ dark: "#f7f8f8"
3912
+ },
3913
+ { name: "--oc-text-secondary", light: "#3f3f46", dark: "#d0d6e0" },
3914
+ { name: "--oc-text-muted", light: "#71717a", dark: "#a1a1aa" },
3915
+ { name: "--oc-text-faint", light: "#d4d4d8", dark: "#52525b" },
3916
+ {
3917
+ section: "Lines",
3918
+ name: "--oc-gridline",
3919
+ light: "rgba(0, 0, 0, 0.1)",
3920
+ dark: "rgba(255, 255, 255, 0.05)"
3921
+ },
3922
+ {
3923
+ name: "--oc-axis",
3924
+ light: "rgba(0, 0, 0, 0.1)",
3925
+ dark: "rgba(255, 255, 255, 0.1)"
3926
+ },
3927
+ {
3928
+ name: "--oc-border",
3929
+ light: "rgba(0, 0, 0, 0.08)",
3930
+ dark: "rgba(255, 255, 255, 0.1)"
3931
+ },
3932
+ { name: "--oc-border-radius", light: "2px" },
3933
+ {
3934
+ section: "Brand and semantic",
3935
+ name: "--oc-accent",
3936
+ light: "#06b6d4",
3937
+ dark: "#06b6d4"
3938
+ },
3939
+ {
3940
+ name: "--oc-accent-strong",
3941
+ light: "#0891b2",
3942
+ dark: "#06b6d4",
3943
+ comment: "darker cyan for line strokes on light bg",
3944
+ consumers: "stamped via adaptForLightLineStroke"
3945
+ },
3946
+ { name: "--oc-positive", light: "#10b981", dark: "#34d399" },
3947
+ { name: "--oc-negative", light: "#e11d48", dark: "#fb7185" },
3948
+ { name: "--oc-focus", light: "#3b82f6", dark: "#60a5fa" },
3949
+ {
3950
+ name: "--oc-focus-ring",
3951
+ light: "rgba(59, 130, 246, 0.1)",
3952
+ comment: "static, intentionally NOT derived from --oc-focus and NOT flipped in dark mode"
3953
+ },
3954
+ {
3955
+ name: "--oc-focus-ring-strong",
3956
+ light: "rgba(59, 130, 246, 0.25)"
3957
+ },
3958
+ {
3959
+ name: "--oc-editable-hover",
3960
+ light: "rgba(79, 70, 229, 0.35)",
3961
+ comment: "edit-mode hover outline, indigo one-off"
3962
+ },
3963
+ {
3964
+ section: "Spacing scale (4px base) \u2014 only --oc-space-2/4 are consumed (JS-stamped)",
3965
+ name: "--oc-space-2",
3966
+ light: "8px",
3967
+ consumers: "stamped at mount from theme.spacing (theme-tokens.ts); component CSS must NOT consume these"
3968
+ },
3969
+ {
3970
+ name: "--oc-space-4",
3971
+ light: "16px",
3972
+ consumers: "stamped at mount from theme.spacing (theme-tokens.ts); component CSS must NOT consume these"
3973
+ },
3974
+ {
3975
+ section: "Interactive states",
3976
+ name: "--oc-hover-bg",
3977
+ light: "rgba(0, 0, 0, 0.025)",
3978
+ dark: "rgba(255, 255, 255, 0.05)"
3979
+ },
3980
+ {
3981
+ name: "--oc-tooltip-bg",
3982
+ light: "rgba(255, 255, 255, 0.88)",
3983
+ dark: "rgba(17, 17, 19, 0.92)"
3984
+ },
3985
+ {
3986
+ name: "--oc-tooltip-border",
3987
+ light: "rgba(0, 0, 0, 0.08)",
3988
+ dark: "rgba(255, 255, 255, 0.08)"
3989
+ },
3990
+ {
3991
+ name: "--oc-tooltip-shadow",
3992
+ light: "0 2px 8px rgba(0, 0, 0, 0.08), 0 0 1px rgba(0, 0, 0, 0.12)",
3993
+ dark: "0 2px 8px rgba(0, 0, 0, 0.3), 0 0 1px rgba(0, 0, 0, 0.4)"
3994
+ },
3995
+ { name: "--oc-tooltip-text", light: "#09090b", dark: "#f7f8f8" },
3996
+ { name: "--oc-legend-text", light: "#3f3f46", dark: "#d0d6e0" }
3997
+ ];
3998
+ var TOKEN_MAP = new Map(CSS_TOKENS.map((t) => [t.name, t]));
3999
+ function cssTokenDefault(name, mode) {
4000
+ const token = TOKEN_MAP.get(name);
4001
+ if (!token) throw new Error(`Unknown CSS token: ${name}`);
4002
+ if (mode === "dark" && token.dark !== void 0) return token.dark;
4003
+ return token.light;
4004
+ }
3517
4005
  export {
3518
4006
  ACHROMATIC_RAMP,
3519
4007
  AXIS_TITLE_GAP,
@@ -3526,12 +4014,13 @@ export {
3526
4014
  BREAKPOINT_COMPACT_MAX,
3527
4015
  BREAKPOINT_MEDIUM_MAX,
3528
4016
  CATEGORICAL_PALETTE,
3529
- CHART_ENCODING_RULES,
3530
- CHART_TYPES,
3531
4017
  COMPACT_WIDTH,
4018
+ CSS_TOKENS,
4019
+ CSS_TOKEN_ROOT_SELECTORS,
3532
4020
  DEFAULT_THEME,
3533
4021
  DIVERGING_PALETTES,
3534
4022
  EXTENDED_OFFSET_STRATEGIES,
4023
+ FOOTNOTE_LINE_HEIGHT,
3535
4024
  GRAPH_ENCODING_RULES,
3536
4025
  HEIGHT_CRAMPED_MAX,
3537
4026
  HEIGHT_SHORT_MAX,
@@ -3550,6 +4039,7 @@ export {
3550
4039
  NARROW_VIEWPORT_MAX,
3551
4040
  OFFSET_STRATEGIES,
3552
4041
  SEQUENTIAL_PALETTES,
4042
+ SUPPORTED_SCHEME_NAMES,
3553
4043
  TICK_LABEL_OFFSET,
3554
4044
  TICK_LINE_HEIGHT_FACTOR,
3555
4045
  TOP_PAD_EXTRA_NARROW,
@@ -3565,15 +4055,20 @@ export {
3565
4055
  areaChart,
3566
4056
  axisTitleOffset,
3567
4057
  barChart,
4058
+ buildCompactStepFormatter,
3568
4059
  buildD3Formatter,
3569
4060
  buildTemporalFormatter,
3570
4061
  checkPaletteDistinguishability,
3571
4062
  columnChart,
3572
4063
  computeChrome,
4064
+ computeFieldFormatContext,
3573
4065
  computeLabelBounds,
3574
4066
  computeXAxisExtentFromLabels,
3575
4067
  contrastRatio,
4068
+ cssTokenDefault,
3576
4069
  dataTable,
4070
+ deepMergeSpec,
4071
+ defaultNumberFormatter,
3577
4072
  detectCollision,
3578
4073
  donutChart,
3579
4074
  dotChart,
@@ -3582,8 +4077,12 @@ export {
3582
4077
  essay,
3583
4078
  estimateTextWidth,
3584
4079
  findAccessibleColor,
4080
+ footnoteBandHeight,
4081
+ formatCurrency,
3585
4082
  formatDate,
3586
4083
  formatNumber,
4084
+ formatOrdinal,
4085
+ formatPercent,
3587
4086
  generateAltText,
3588
4087
  generateAriaLabels,
3589
4088
  generateDataTable,
@@ -3594,6 +4093,7 @@ export {
3594
4093
  getRepresentativeColor,
3595
4094
  heuristicMeasure,
3596
4095
  inferFieldType,
4096
+ isAxislessMark,
3597
4097
  isBarListSpec,
3598
4098
  isChartSpec,
3599
4099
  isConditionalDef,
@@ -3601,13 +4101,18 @@ export {
3601
4101
  isGradientDef,
3602
4102
  isGraphSpec,
3603
4103
  isLayerSpec,
4104
+ isMapSpec,
4105
+ isOpaqueColor,
3604
4106
  isRangeAnnotation,
3605
4107
  isRefLineAnnotation,
3606
4108
  isSankeySpec,
3607
4109
  isTableSpec,
3608
4110
  isTextAnnotation,
3609
4111
  isTileMapSpec,
4112
+ isValueDef,
4113
+ isYearContext,
3610
4114
  lineChart,
4115
+ maxRotatedLabelWidth,
3611
4116
  meetsAA,
3612
4117
  overlapArea,
3613
4118
  pickLabelColor,
@@ -3616,11 +4121,14 @@ export {
3616
4121
  resolveMarkDef,
3617
4122
  resolveMarkType,
3618
4123
  resolveMeasurer,
4124
+ resolveNumberFormatter,
4125
+ resolveSchemeName,
3619
4126
  resolveTheme,
3620
4127
  scatterChart,
3621
4128
  simulateColorBlindness,
3622
4129
  textAscent,
3623
4130
  tileMap,
4131
+ truncateRotatedLabel,
3624
4132
  wire,
3625
4133
  wrapText
3626
4134
  };