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

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 +533 -42
  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 +77 -10
  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,14 +1336,41 @@ 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
1250
1342
  );
1251
1343
  return input.hasTitle ? rotatedHeight + X_AXIS_TITLE_BAND_ROTATED : rotatedHeight;
1252
1344
  }
1253
- return input.hasTitle ? baseHeight + X_AXIS_TITLE_BAND : baseHeight;
1345
+ const tickExtent = 4 + input.tickFontSize + Math.max(11, Math.ceil(input.tickFontSize * 0.7));
1346
+ const flatHeight = Math.max(baseHeight, tickExtent);
1347
+ return input.hasTitle ? flatHeight + X_AXIS_TITLE_BAND : flatHeight;
1348
+ }
1349
+ function maxRotatedLabelWidth(tickAngle, tickFontSize) {
1350
+ const angleRad = Math.abs(tickAngle) * (Math.PI / 180);
1351
+ const sin = Math.sin(angleRad);
1352
+ if (sin <= 1e-3) return Number.POSITIVE_INFINITY;
1353
+ const lineHeight = tickFontSize * TICK_LINE_HEIGHT_FACTOR;
1354
+ const usable = X_AXIS_ROTATED_EXTENT_CAP - lineHeight * Math.cos(angleRad) - AXIS_TO_LABEL_GAP;
1355
+ return Math.max(0, usable / sin);
1356
+ }
1357
+ function truncateRotatedLabel(label, tickAngle, tickFontSize, tickFontWeight, measure = estimateTextWidth) {
1358
+ const budget = maxRotatedLabelWidth(tickAngle, tickFontSize);
1359
+ if (!Number.isFinite(budget)) return label;
1360
+ if (measure(label, tickFontSize, tickFontWeight) <= budget) return label;
1361
+ const ELLIPSIS = "\u2026";
1362
+ let lo = 0;
1363
+ let hi = label.length;
1364
+ while (lo < hi) {
1365
+ const mid = Math.ceil((lo + hi) / 2);
1366
+ const candidate = label.slice(0, mid) + ELLIPSIS;
1367
+ if (measure(candidate, tickFontSize, tickFontWeight) <= budget) {
1368
+ lo = mid;
1369
+ } else {
1370
+ hi = mid - 1;
1371
+ }
1372
+ }
1373
+ return lo > 0 ? label.slice(0, lo).trimEnd() + ELLIPSIS : ELLIPSIS;
1254
1374
  }
1255
1375
 
1256
1376
  // src/theme/defaults.ts
@@ -1790,6 +1910,12 @@ function estimateLineCount(text, style, maxWidth, measureText) {
1790
1910
  }
1791
1911
  return lines;
1792
1912
  }
1913
+ var FOOTNOTE_LINE_HEIGHT = 1.3;
1914
+ var FOOTNOTE_BAND_GAP = 4;
1915
+ function footnoteBandHeight(count, theme) {
1916
+ if (count <= 0) return 0;
1917
+ return count * theme.fonts.sizes.small * FOOTNOTE_LINE_HEIGHT + FOOTNOTE_BAND_GAP;
1918
+ }
1793
1919
  function computeChrome(chrome, theme, width, measureText, chromeMode = "full", padding, watermark = true, bottomLegendHeight = 0) {
1794
1920
  if (!chrome || chromeMode === "hidden") {
1795
1921
  return { topHeight: 0, bottomHeight: 0 };
@@ -2118,7 +2244,8 @@ function resolveCollisions(labels, strategies = OFFSET_STRATEGIES) {
2118
2244
  from: { x: bestX, y: bestY },
2119
2245
  to: { x: label.anchorX, y: label.anchorY },
2120
2246
  stroke: label.style.fill,
2121
- style: "straight"
2247
+ style: "straight",
2248
+ arrow: false
2122
2249
  } : void 0
2123
2250
  });
2124
2251
  } else {
@@ -3145,30 +3272,65 @@ function formatNumber(value, _locale) {
3145
3272
  if (Number.isInteger(value)) {
3146
3273
  return format(",")(value);
3147
3274
  }
3275
+ if (Math.abs(value) < 5e-3) {
3276
+ return format(".2~r")(value);
3277
+ }
3148
3278
  return format(",.2f")(value);
3149
3279
  }
3150
- var ABBREVIATIONS = [
3280
+ var UNIT_TABLE = [
3151
3281
  { threshold: 1e12, suffix: "T", divisor: 1e12 },
3152
3282
  { threshold: 1e9, suffix: "B", divisor: 1e9 },
3153
3283
  { threshold: 1e6, suffix: "M", divisor: 1e6 },
3154
- { threshold: 1e3, suffix: "K", divisor: 1e3 }
3284
+ { threshold: 1e3, suffix: "k", divisor: 1e3 }
3155
3285
  ];
3286
+ function unitFor(abs) {
3287
+ for (const entry of UNIT_TABLE) {
3288
+ if (abs >= entry.threshold) return entry;
3289
+ }
3290
+ return { suffix: "", divisor: 1 };
3291
+ }
3292
+ function fractionDigits(x) {
3293
+ const a = Math.abs(x);
3294
+ for (let p = 0; p <= 3; p++) {
3295
+ const scaled = a * 10 ** p;
3296
+ if (Math.abs(scaled - Math.round(scaled)) < 1e-9) return p;
3297
+ }
3298
+ return 4;
3299
+ }
3156
3300
  function abbreviateNumber(value) {
3157
3301
  if (!Number.isFinite(value)) return String(value);
3158
3302
  const absValue = Math.abs(value);
3159
3303
  const sign = value < 0 ? "-" : "";
3160
- for (const { threshold, suffix, divisor } of ABBREVIATIONS) {
3304
+ for (let i = 0; i < UNIT_TABLE.length; i++) {
3305
+ const { threshold, suffix, divisor } = UNIT_TABLE[i];
3161
3306
  if (absValue >= threshold) {
3162
- const abbreviated = absValue / divisor;
3163
- const formatted = abbreviated % 1 === 0 ? String(abbreviated) : format(".1f")(abbreviated);
3164
- return `${sign}${formatted}${suffix}`;
3307
+ const mantissa = format(".1~f")(absValue / divisor);
3308
+ if (mantissa === "1000" && i > 0) {
3309
+ return `${sign}1${UNIT_TABLE[i - 1].suffix}`;
3310
+ }
3311
+ return `${sign}${mantissa}${suffix}`;
3165
3312
  }
3166
3313
  }
3167
3314
  return formatNumber(value);
3168
3315
  }
3316
+ function formatOrdinal(value) {
3317
+ if (!Number.isFinite(value)) return String(value);
3318
+ const n = Math.round(value);
3319
+ const abs = Math.abs(n);
3320
+ const rem100 = abs % 100;
3321
+ const rem10 = abs % 10;
3322
+ let suffix;
3323
+ if (rem100 >= 11 && rem100 <= 13) suffix = "th";
3324
+ else if (rem10 === 1) suffix = "st";
3325
+ else if (rem10 === 2) suffix = "nd";
3326
+ else if (rem10 === 3) suffix = "rd";
3327
+ else suffix = "th";
3328
+ return `${n}${suffix}`;
3329
+ }
3169
3330
  var D3_FORMAT_SUFFIX_RE = /^(.*~?[efgsrdxXobcnp%])(.+)$/;
3170
3331
  function buildD3Formatter(formatStr) {
3171
3332
  if (!formatStr) return null;
3333
+ if (formatStr === "ordinal") return formatOrdinal;
3172
3334
  try {
3173
3335
  const fmt = format(formatStr);
3174
3336
  if (formatStr.includes("s")) {
@@ -3188,6 +3350,111 @@ function buildD3Formatter(formatStr) {
3188
3350
  }
3189
3351
  return null;
3190
3352
  }
3353
+ function formatPercent(value, options) {
3354
+ const fraction = options?.fraction ?? true;
3355
+ if (fraction) {
3356
+ return format(".1~%")(value);
3357
+ }
3358
+ return `${format(",.1~f")(value)}%`;
3359
+ }
3360
+ function formatCurrency(value, options) {
3361
+ const compact = options?.compact ?? false;
3362
+ const abs = Math.abs(value);
3363
+ const sign = value < 0 ? "-" : "";
3364
+ if (compact) {
3365
+ if (abs >= 1e3) {
3366
+ return `${sign}$${abbreviateNumber(abs)}`;
3367
+ }
3368
+ return `${sign}$${formatNumber(abs)}`;
3369
+ }
3370
+ if (Number.isInteger(value)) {
3371
+ return sign + format("$,")(abs);
3372
+ }
3373
+ return sign + format("$,.2f")(abs);
3374
+ }
3375
+ function computeFieldFormatContext(values, surface) {
3376
+ let min = Infinity;
3377
+ let max = -Infinity;
3378
+ let allIntegers = true;
3379
+ let hasFinite = false;
3380
+ for (const raw of values) {
3381
+ const v = typeof raw === "number" ? raw : typeof raw === "string" ? Number(raw) : NaN;
3382
+ if (!Number.isFinite(v)) continue;
3383
+ hasFinite = true;
3384
+ if (v < min) min = v;
3385
+ if (v > max) max = v;
3386
+ if (allIntegers && !Number.isInteger(v)) allIntegers = false;
3387
+ }
3388
+ const ctx = {};
3389
+ if (hasFinite) {
3390
+ ctx.extent = [min, max];
3391
+ ctx.allIntegers = allIntegers;
3392
+ }
3393
+ if (surface) ctx.surface = surface;
3394
+ return ctx;
3395
+ }
3396
+ function isYearContext(ctx) {
3397
+ if (!ctx?.extent || !ctx.allIntegers) return false;
3398
+ const [min, max] = ctx.extent;
3399
+ return min >= 1500 && max <= 2500;
3400
+ }
3401
+ function resolveNumberFormatter(formatStr, ctx) {
3402
+ if (!formatStr) return null;
3403
+ if (formatStr === "ordinal") return formatOrdinal;
3404
+ if (formatStr === "percent") {
3405
+ const fraction = ctx?.extent ? Math.max(Math.abs(ctx.extent[0]), Math.abs(ctx.extent[1])) <= 1 : true;
3406
+ return (v) => formatPercent(v, { fraction });
3407
+ }
3408
+ if (formatStr === "currency") {
3409
+ const isTable = ctx?.surface === "table";
3410
+ if (!isTable && ctx?.step != null && (ctx.stepReference ?? 0) >= 1e3) {
3411
+ const stepFmt = buildCompactStepFormatter(ctx.step);
3412
+ return (v) => {
3413
+ const sign = v < 0 ? "-" : "";
3414
+ return `${sign}$${stepFmt(Math.abs(v))}`;
3415
+ };
3416
+ }
3417
+ return (v) => formatCurrency(v, { compact: !isTable });
3418
+ }
3419
+ return buildD3Formatter(formatStr);
3420
+ }
3421
+ function defaultNumberFormatter(ctx) {
3422
+ if (isYearContext(ctx)) {
3423
+ return (v) => String(Math.round(v));
3424
+ }
3425
+ if (ctx?.surface === "table") {
3426
+ return formatNumber;
3427
+ }
3428
+ if (ctx?.step != null && (ctx.stepReference ?? maxAbs(ctx.extent)) >= 1e3) {
3429
+ return buildCompactStepFormatter(ctx.step);
3430
+ }
3431
+ return (v) => {
3432
+ if (!Number.isFinite(v)) return String(v);
3433
+ if (ctx?.extent === void 0 && ctx?.allIntegers === void 0) {
3434
+ if (Number.isInteger(v) && v >= 1500 && v <= 2500) return String(v);
3435
+ }
3436
+ return Math.abs(v) >= 1e3 ? abbreviateNumber(v) : formatNumber(v);
3437
+ };
3438
+ }
3439
+ function maxAbs(extent) {
3440
+ if (!extent) return 0;
3441
+ return Math.max(Math.abs(extent[0]), Math.abs(extent[1]));
3442
+ }
3443
+ function buildCompactStepFormatter(step) {
3444
+ return (v) => {
3445
+ if (v === 0) return "0";
3446
+ if (!Number.isFinite(v)) return String(v);
3447
+ const abs = Math.abs(v);
3448
+ if (abs < 1e3) return formatNumber(v);
3449
+ const { suffix, divisor } = unitFor(abs);
3450
+ const p = fractionDigits(step / divisor);
3451
+ if (p > 2) {
3452
+ return Number.isInteger(v) ? format(",")(v) : format(",.2f")(v);
3453
+ }
3454
+ const sign = v < 0 ? "-" : "";
3455
+ return sign + format(`.${p}~f`)(abs / divisor) + suffix;
3456
+ };
3457
+ }
3191
3458
  var GRANULARITY_FORMATS = {
3192
3459
  year: "%Y",
3193
3460
  quarter: "",
@@ -3262,7 +3529,34 @@ function generateAltText(spec, data) {
3262
3529
  const titleText = typeof title === "string" ? title : title.text;
3263
3530
  parts.push(`showing ${titleText}`);
3264
3531
  }
3265
- if (spec.encoding.x && data.length > 0) {
3532
+ if (markType === "waffle") {
3533
+ const units = markDef.units ?? 100;
3534
+ parts.push(`dividing ${units} units`);
3535
+ }
3536
+ if (markType === "parliament") {
3537
+ const valueField = spec.encoding.theta?.field ?? spec.encoding.y?.field;
3538
+ if (valueField) {
3539
+ const totalSeats = data.reduce((sum, d) => {
3540
+ const v = Number(d[valueField]);
3541
+ return sum + (Number.isFinite(v) && v > 0 ? v : 0);
3542
+ }, 0);
3543
+ if (totalSeats > 0) {
3544
+ const majority = Math.floor(totalSeats / 2) + 1;
3545
+ parts.push(`with ${totalSeats} seats, ${majority} needed for a majority`);
3546
+ }
3547
+ }
3548
+ }
3549
+ if (markType === "beeswarm") {
3550
+ const xIsValue = spec.encoding.x?.type === "quantitative";
3551
+ const valueField = xIsValue ? spec.encoding.x?.field : spec.encoding.y?.field;
3552
+ const laneField = xIsValue ? spec.encoding.y?.field : spec.encoding.x?.field;
3553
+ if (valueField) {
3554
+ parts.push(
3555
+ `plotting the distribution of ${valueField}${laneField ? ` by ${laneField}` : ""}`
3556
+ );
3557
+ }
3558
+ }
3559
+ if (spec.encoding.x && markType !== "beeswarm" && data.length > 0) {
3266
3560
  const field = spec.encoding.x.field;
3267
3561
  const values = data.map((d) => d[field]).filter((v) => v != null);
3268
3562
  if (values.length > 0) {
@@ -3347,11 +3641,12 @@ function generateAriaLabels(marks) {
3347
3641
  }
3348
3642
  return labels;
3349
3643
  }
3644
+ var ariaNumberFormatter = defaultNumberFormatter();
3350
3645
  function formatValue(value) {
3351
3646
  if (value == null) return "N/A";
3352
3647
  if (value instanceof Date) return value.toISOString().slice(0, 10);
3353
3648
  if (typeof value === "number") {
3354
- return Number.isInteger(value) ? String(value) : value.toFixed(2);
3649
+ return ariaNumberFormatter(value);
3355
3650
  }
3356
3651
  return String(value);
3357
3652
  }
@@ -3431,7 +3726,7 @@ function pieChart(data, category, value, options) {
3431
3726
  const categoryChannel = resolveField(category, data);
3432
3727
  const valueChannel = resolveField(value, data);
3433
3728
  const encoding = {
3434
- y: valueChannel,
3729
+ theta: valueChannel,
3435
3730
  color: categoryChannel
3436
3731
  };
3437
3732
  if (options?.size && data) {
@@ -3449,7 +3744,7 @@ function donutChart(data, category, value, options) {
3449
3744
  const categoryChannel = resolveField(category, data);
3450
3745
  const valueChannel = resolveField(value, data);
3451
3746
  const encoding = {
3452
- y: valueChannel,
3747
+ theta: valueChannel,
3453
3748
  color: categoryChannel
3454
3749
  };
3455
3750
  if (options?.size && data) {
@@ -3514,6 +3809,182 @@ function tileMap(data, options) {
3514
3809
  ...options?.valueFormat && { valueFormat: options.valueFormat }
3515
3810
  };
3516
3811
  }
3812
+
3813
+ // src/helpers/deep-merge.ts
3814
+ function deepMergeSpec(target, source) {
3815
+ if (source === void 0) return target;
3816
+ if (source === null) return source;
3817
+ if (typeof source !== "object" || Array.isArray(source) || typeof target !== "object" || target === null || Array.isArray(target)) {
3818
+ return source;
3819
+ }
3820
+ const result = { ...target };
3821
+ for (const key of Object.keys(source)) {
3822
+ result[key] = deepMergeSpec(target[key], source[key]);
3823
+ }
3824
+ return result;
3825
+ }
3826
+
3827
+ // src/styles/token-definitions.ts
3828
+ var CSS_TOKEN_ROOT_SELECTORS = [
3829
+ ".oc-root",
3830
+ ".oc-chart-root",
3831
+ ".oc-table-wrapper",
3832
+ ".oc-table-root",
3833
+ ".oc-graph-wrapper",
3834
+ ".oc-graph-root",
3835
+ ".oc-sankey-root",
3836
+ ".oc-tilemap-root",
3837
+ ".oc-barlist-root",
3838
+ ".oc-map-root"
3839
+ ];
3840
+ var CSS_TOKENS = [
3841
+ {
3842
+ name: "--oc-font-family",
3843
+ light: '\n "Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
3844
+ },
3845
+ {
3846
+ name: "--oc-font-mono",
3847
+ light: '"JetBrains Mono", "Fira Code", "Cascadia Code", monospace'
3848
+ },
3849
+ {
3850
+ section: "Animation easing presets via CSS linear()",
3851
+ name: "--oc-ease-smooth",
3852
+ 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 )"
3853
+ },
3854
+ {
3855
+ name: "--oc-ease-snappy",
3856
+ 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 )",
3857
+ consumers: "EASE_VAR_MAP in vanilla renderers"
3858
+ },
3859
+ { section: "Animation timing defaults", name: "--oc-animation-duration", light: "500ms" },
3860
+ { name: "--oc-animation-stagger", light: "80ms" },
3861
+ { name: "--oc-annotation-delay", light: "200ms" },
3862
+ { section: "Typography scale (editorial design system)", name: "--oc-title-size", light: "26px" },
3863
+ { name: "--oc-title-weight", light: "590" },
3864
+ { name: "--oc-title-tracking", light: "-0.022em" },
3865
+ { name: "--oc-subtitle-size", light: "14px" },
3866
+ { name: "--oc-subtitle-weight", light: "450" },
3867
+ { name: "--oc-source-size", light: "11px" },
3868
+ { name: "--oc-source-weight", light: "450" },
3869
+ { name: "--oc-body-size", light: "13px" },
3870
+ { name: "--oc-eyebrow-size", light: "11px" },
3871
+ { name: "--oc-eyebrow-weight", light: "550" },
3872
+ { name: "--oc-eyebrow-tracking", light: "0.08em" },
3873
+ { section: "Surfaces (light mode defaults)", name: "--oc-bg", light: "#ffffff", dark: "#09090b" },
3874
+ { name: "--oc-card", light: "#ffffff", dark: "#111113" },
3875
+ {
3876
+ name: "--oc-secondary",
3877
+ light: "#f4f4f5",
3878
+ dark: "#27272a",
3879
+ comment: "zinc-100, raised surface"
3880
+ },
3881
+ {
3882
+ section: `Text levels. Names invert across modes: in light mode "muted" sits at
3883
+ * a lighter zinc step than "subtle" because both are picked relative to
3884
+ * the active background, not from a fixed lightness ladder. The intent
3885
+ * is "muted = first step away from primary text"; "subtle = next step
3886
+ * down"; etc.
3887
+ *
3888
+ * Light mode: text=zinc-950, secondary=zinc-700, muted=zinc-500,
3889
+ * subtle=zinc-400, faint=zinc-300
3890
+ * Dark mode (dark.css): inverts the surface tokens but keeps the same
3891
+ * muted -> subtle -> faint progression away from primary.`,
3892
+ name: "--oc-text",
3893
+ light: "#09090b",
3894
+ dark: "#f7f8f8"
3895
+ },
3896
+ { name: "--oc-text-secondary", light: "#3f3f46", dark: "#d0d6e0" },
3897
+ { name: "--oc-text-muted", light: "#71717a", dark: "#a1a1aa" },
3898
+ { name: "--oc-text-faint", light: "#d4d4d8", dark: "#52525b" },
3899
+ {
3900
+ section: "Lines",
3901
+ name: "--oc-gridline",
3902
+ light: "rgba(0, 0, 0, 0.1)",
3903
+ dark: "rgba(255, 255, 255, 0.05)"
3904
+ },
3905
+ {
3906
+ name: "--oc-axis",
3907
+ light: "rgba(0, 0, 0, 0.1)",
3908
+ dark: "rgba(255, 255, 255, 0.1)"
3909
+ },
3910
+ {
3911
+ name: "--oc-border",
3912
+ light: "rgba(0, 0, 0, 0.08)",
3913
+ dark: "rgba(255, 255, 255, 0.1)"
3914
+ },
3915
+ { name: "--oc-border-radius", light: "2px" },
3916
+ {
3917
+ section: "Brand and semantic",
3918
+ name: "--oc-accent",
3919
+ light: "#06b6d4",
3920
+ dark: "#06b6d4"
3921
+ },
3922
+ {
3923
+ name: "--oc-accent-strong",
3924
+ light: "#0891b2",
3925
+ dark: "#06b6d4",
3926
+ comment: "darker cyan for line strokes on light bg",
3927
+ consumers: "stamped via adaptForLightLineStroke"
3928
+ },
3929
+ { name: "--oc-positive", light: "#10b981", dark: "#34d399" },
3930
+ { name: "--oc-negative", light: "#e11d48", dark: "#fb7185" },
3931
+ { name: "--oc-focus", light: "#3b82f6", dark: "#60a5fa" },
3932
+ {
3933
+ name: "--oc-focus-ring",
3934
+ light: "rgba(59, 130, 246, 0.1)",
3935
+ comment: "static, intentionally NOT derived from --oc-focus and NOT flipped in dark mode"
3936
+ },
3937
+ {
3938
+ name: "--oc-focus-ring-strong",
3939
+ light: "rgba(59, 130, 246, 0.25)"
3940
+ },
3941
+ {
3942
+ name: "--oc-editable-hover",
3943
+ light: "rgba(79, 70, 229, 0.35)",
3944
+ comment: "edit-mode hover outline, indigo one-off"
3945
+ },
3946
+ {
3947
+ section: "Spacing scale (4px base) \u2014 only --oc-space-2/4 are consumed (JS-stamped)",
3948
+ name: "--oc-space-2",
3949
+ light: "8px",
3950
+ consumers: "stamped at mount from theme.spacing (theme-tokens.ts); component CSS must NOT consume these"
3951
+ },
3952
+ {
3953
+ name: "--oc-space-4",
3954
+ light: "16px",
3955
+ consumers: "stamped at mount from theme.spacing (theme-tokens.ts); component CSS must NOT consume these"
3956
+ },
3957
+ {
3958
+ section: "Interactive states",
3959
+ name: "--oc-hover-bg",
3960
+ light: "rgba(0, 0, 0, 0.025)",
3961
+ dark: "rgba(255, 255, 255, 0.05)"
3962
+ },
3963
+ {
3964
+ name: "--oc-tooltip-bg",
3965
+ light: "rgba(255, 255, 255, 0.88)",
3966
+ dark: "rgba(17, 17, 19, 0.92)"
3967
+ },
3968
+ {
3969
+ name: "--oc-tooltip-border",
3970
+ light: "rgba(0, 0, 0, 0.08)",
3971
+ dark: "rgba(255, 255, 255, 0.08)"
3972
+ },
3973
+ {
3974
+ name: "--oc-tooltip-shadow",
3975
+ light: "0 2px 8px rgba(0, 0, 0, 0.08), 0 0 1px rgba(0, 0, 0, 0.12)",
3976
+ dark: "0 2px 8px rgba(0, 0, 0, 0.3), 0 0 1px rgba(0, 0, 0, 0.4)"
3977
+ },
3978
+ { name: "--oc-tooltip-text", light: "#09090b", dark: "#f7f8f8" },
3979
+ { name: "--oc-legend-text", light: "#3f3f46", dark: "#d0d6e0" }
3980
+ ];
3981
+ var TOKEN_MAP = new Map(CSS_TOKENS.map((t) => [t.name, t]));
3982
+ function cssTokenDefault(name, mode) {
3983
+ const token = TOKEN_MAP.get(name);
3984
+ if (!token) throw new Error(`Unknown CSS token: ${name}`);
3985
+ if (mode === "dark" && token.dark !== void 0) return token.dark;
3986
+ return token.light;
3987
+ }
3517
3988
  export {
3518
3989
  ACHROMATIC_RAMP,
3519
3990
  AXIS_TITLE_GAP,
@@ -3526,12 +3997,13 @@ export {
3526
3997
  BREAKPOINT_COMPACT_MAX,
3527
3998
  BREAKPOINT_MEDIUM_MAX,
3528
3999
  CATEGORICAL_PALETTE,
3529
- CHART_ENCODING_RULES,
3530
- CHART_TYPES,
3531
4000
  COMPACT_WIDTH,
4001
+ CSS_TOKENS,
4002
+ CSS_TOKEN_ROOT_SELECTORS,
3532
4003
  DEFAULT_THEME,
3533
4004
  DIVERGING_PALETTES,
3534
4005
  EXTENDED_OFFSET_STRATEGIES,
4006
+ FOOTNOTE_LINE_HEIGHT,
3535
4007
  GRAPH_ENCODING_RULES,
3536
4008
  HEIGHT_CRAMPED_MAX,
3537
4009
  HEIGHT_SHORT_MAX,
@@ -3550,6 +4022,7 @@ export {
3550
4022
  NARROW_VIEWPORT_MAX,
3551
4023
  OFFSET_STRATEGIES,
3552
4024
  SEQUENTIAL_PALETTES,
4025
+ SUPPORTED_SCHEME_NAMES,
3553
4026
  TICK_LABEL_OFFSET,
3554
4027
  TICK_LINE_HEIGHT_FACTOR,
3555
4028
  TOP_PAD_EXTRA_NARROW,
@@ -3565,15 +4038,20 @@ export {
3565
4038
  areaChart,
3566
4039
  axisTitleOffset,
3567
4040
  barChart,
4041
+ buildCompactStepFormatter,
3568
4042
  buildD3Formatter,
3569
4043
  buildTemporalFormatter,
3570
4044
  checkPaletteDistinguishability,
3571
4045
  columnChart,
3572
4046
  computeChrome,
4047
+ computeFieldFormatContext,
3573
4048
  computeLabelBounds,
3574
4049
  computeXAxisExtentFromLabels,
3575
4050
  contrastRatio,
4051
+ cssTokenDefault,
3576
4052
  dataTable,
4053
+ deepMergeSpec,
4054
+ defaultNumberFormatter,
3577
4055
  detectCollision,
3578
4056
  donutChart,
3579
4057
  dotChart,
@@ -3582,8 +4060,12 @@ export {
3582
4060
  essay,
3583
4061
  estimateTextWidth,
3584
4062
  findAccessibleColor,
4063
+ footnoteBandHeight,
4064
+ formatCurrency,
3585
4065
  formatDate,
3586
4066
  formatNumber,
4067
+ formatOrdinal,
4068
+ formatPercent,
3587
4069
  generateAltText,
3588
4070
  generateAriaLabels,
3589
4071
  generateDataTable,
@@ -3594,6 +4076,7 @@ export {
3594
4076
  getRepresentativeColor,
3595
4077
  heuristicMeasure,
3596
4078
  inferFieldType,
4079
+ isAxislessMark,
3597
4080
  isBarListSpec,
3598
4081
  isChartSpec,
3599
4082
  isConditionalDef,
@@ -3601,13 +4084,18 @@ export {
3601
4084
  isGradientDef,
3602
4085
  isGraphSpec,
3603
4086
  isLayerSpec,
4087
+ isMapSpec,
4088
+ isOpaqueColor,
3604
4089
  isRangeAnnotation,
3605
4090
  isRefLineAnnotation,
3606
4091
  isSankeySpec,
3607
4092
  isTableSpec,
3608
4093
  isTextAnnotation,
3609
4094
  isTileMapSpec,
4095
+ isValueDef,
4096
+ isYearContext,
3610
4097
  lineChart,
4098
+ maxRotatedLabelWidth,
3611
4099
  meetsAA,
3612
4100
  overlapArea,
3613
4101
  pickLabelColor,
@@ -3616,11 +4104,14 @@ export {
3616
4104
  resolveMarkDef,
3617
4105
  resolveMarkType,
3618
4106
  resolveMeasurer,
4107
+ resolveNumberFormatter,
4108
+ resolveSchemeName,
3619
4109
  resolveTheme,
3620
4110
  scatterChart,
3621
4111
  simulateColorBlindness,
3622
4112
  textAscent,
3623
4113
  tileMap,
4114
+ truncateRotatedLabel,
3624
4115
  wire,
3625
4116
  wrapText
3626
4117
  };