@opendata-ai/openchart-engine 6.0.0 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/index.d.ts +155 -19
  2. package/dist/index.js +1513 -164
  3. package/dist/index.js.map +1 -1
  4. package/package.json +2 -2
  5. package/src/__test-fixtures__/specs.ts +6 -3
  6. package/src/__tests__/axes.test.ts +168 -4
  7. package/src/__tests__/compile-chart.test.ts +23 -12
  8. package/src/__tests__/compile-layer.test.ts +386 -0
  9. package/src/__tests__/dimensions.test.ts +6 -3
  10. package/src/__tests__/legend.test.ts +6 -3
  11. package/src/__tests__/scales.test.ts +176 -2
  12. package/src/annotations/__tests__/compute.test.ts +8 -4
  13. package/src/charts/bar/__tests__/compute.test.ts +12 -6
  14. package/src/charts/bar/compute.ts +21 -5
  15. package/src/charts/column/__tests__/compute.test.ts +14 -7
  16. package/src/charts/column/compute.ts +21 -6
  17. package/src/charts/dot/__tests__/compute.test.ts +10 -5
  18. package/src/charts/dot/compute.ts +10 -4
  19. package/src/charts/line/__tests__/compute.test.ts +102 -11
  20. package/src/charts/line/__tests__/curves.test.ts +51 -0
  21. package/src/charts/line/__tests__/labels.test.ts +2 -1
  22. package/src/charts/line/__tests__/mark-options.test.ts +175 -0
  23. package/src/charts/line/area.ts +19 -8
  24. package/src/charts/line/compute.ts +64 -25
  25. package/src/charts/line/curves.ts +40 -0
  26. package/src/charts/pie/__tests__/compute.test.ts +10 -5
  27. package/src/charts/pie/compute.ts +2 -1
  28. package/src/charts/rule/index.ts +127 -0
  29. package/src/charts/scatter/__tests__/compute.test.ts +10 -5
  30. package/src/charts/scatter/compute.ts +15 -5
  31. package/src/charts/text/index.ts +92 -0
  32. package/src/charts/tick/index.ts +84 -0
  33. package/src/charts/utils.ts +1 -1
  34. package/src/compile.ts +175 -23
  35. package/src/compiler/__tests__/compile.test.ts +4 -4
  36. package/src/compiler/__tests__/normalize.test.ts +4 -4
  37. package/src/compiler/__tests__/validate.test.ts +25 -26
  38. package/src/compiler/index.ts +1 -1
  39. package/src/compiler/normalize.ts +77 -4
  40. package/src/compiler/types.ts +6 -2
  41. package/src/compiler/validate.ts +167 -35
  42. package/src/graphs/__tests__/compile-graph.test.ts +2 -2
  43. package/src/graphs/compile-graph.ts +2 -2
  44. package/src/index.ts +17 -1
  45. package/src/layout/axes.ts +122 -20
  46. package/src/layout/dimensions.ts +15 -9
  47. package/src/layout/scales.ts +320 -31
  48. package/src/legend/compute.ts +9 -6
  49. package/src/tables/__tests__/compile-table.test.ts +1 -1
  50. package/src/tooltips/__tests__/compute.test.ts +10 -5
  51. package/src/tooltips/compute.ts +32 -14
  52. package/src/transforms/__tests__/bin.test.ts +88 -0
  53. package/src/transforms/__tests__/calculate.test.ts +146 -0
  54. package/src/transforms/__tests__/conditional.test.ts +109 -0
  55. package/src/transforms/__tests__/filter.test.ts +59 -0
  56. package/src/transforms/__tests__/index.test.ts +93 -0
  57. package/src/transforms/__tests__/predicates.test.ts +176 -0
  58. package/src/transforms/__tests__/timeunit.test.ts +129 -0
  59. package/src/transforms/bin.ts +87 -0
  60. package/src/transforms/calculate.ts +60 -0
  61. package/src/transforms/conditional.ts +46 -0
  62. package/src/transforms/filter.ts +17 -0
  63. package/src/transforms/index.ts +48 -0
  64. package/src/transforms/predicates.ts +90 -0
  65. package/src/transforms/timeunit.ts +88 -0
package/dist/index.js CHANGED
@@ -503,11 +503,79 @@ function computeAnnotations(spec, scales, chartArea, strategy, isDark = false, o
503
503
  // src/charts/bar/compute.ts
504
504
  import { abbreviateNumber, formatNumber } from "@opendata-ai/openchart-core";
505
505
 
506
+ // src/transforms/predicates.ts
507
+ function isFieldPredicate(pred) {
508
+ return "field" in pred;
509
+ }
510
+ function evaluateFieldPredicate(datum, pred) {
511
+ const value = datum[pred.field];
512
+ if (pred.valid !== void 0) {
513
+ const isValid = value !== null && value !== void 0 && !Number.isNaN(value);
514
+ return pred.valid ? isValid : !isValid;
515
+ }
516
+ if (pred.equal !== void 0) {
517
+ return value === pred.equal;
518
+ }
519
+ const numValue = Number(value);
520
+ if (pred.lt !== void 0) {
521
+ return numValue < pred.lt;
522
+ }
523
+ if (pred.lte !== void 0) {
524
+ return numValue <= pred.lte;
525
+ }
526
+ if (pred.gt !== void 0) {
527
+ return numValue > pred.gt;
528
+ }
529
+ if (pred.gte !== void 0) {
530
+ return numValue >= pred.gte;
531
+ }
532
+ if (pred.range !== void 0) {
533
+ const [min3, max3] = pred.range;
534
+ return numValue >= min3 && numValue <= max3;
535
+ }
536
+ if (pred.oneOf !== void 0) {
537
+ return pred.oneOf.includes(value);
538
+ }
539
+ return true;
540
+ }
541
+ function evaluatePredicate(datum, predicate) {
542
+ if (isFieldPredicate(predicate)) {
543
+ return evaluateFieldPredicate(datum, predicate);
544
+ }
545
+ if ("and" in predicate) {
546
+ return predicate.and.every((p) => evaluatePredicate(datum, p));
547
+ }
548
+ if ("or" in predicate) {
549
+ return predicate.or.some((p) => evaluatePredicate(datum, p));
550
+ }
551
+ if ("not" in predicate) {
552
+ return !evaluatePredicate(datum, predicate.not);
553
+ }
554
+ return true;
555
+ }
556
+
557
+ // src/transforms/conditional.ts
558
+ function resolveConditionalValue(datum, channelDef) {
559
+ const conditions = Array.isArray(channelDef.condition) ? channelDef.condition : [channelDef.condition];
560
+ for (const cond of conditions) {
561
+ if (evaluatePredicate(datum, cond.test)) {
562
+ if (cond.field !== void 0) {
563
+ return datum[cond.field];
564
+ }
565
+ return cond.value;
566
+ }
567
+ }
568
+ return channelDef.value;
569
+ }
570
+ function isConditionalValueDef(def) {
571
+ return def !== null && typeof def === "object" && "condition" in def;
572
+ }
573
+
506
574
  // src/charts/utils.ts
507
575
  var DEFAULT_COLOR = "#1b7fa3";
508
576
  function scaleValue(scale, scaleType, value) {
509
577
  if (value == null) return null;
510
- if (scaleType === "time") {
578
+ if (scaleType === "time" || scaleType === "utc") {
511
579
  const date2 = value instanceof Date ? value : new Date(String(value));
512
580
  if (Number.isNaN(date2.getTime())) return null;
513
581
  return scale(date2);
@@ -596,8 +664,10 @@ function computeBarMarks(spec, scales, _chartArea, _strategy) {
596
664
  }
597
665
  const bandwidth = yScale.bandwidth();
598
666
  const baseline = xScale(0);
599
- const colorField = encoding.color?.field;
600
- const isSequentialColor = encoding.color?.type === "quantitative";
667
+ const colorEnc = encoding.color && "field" in encoding.color ? encoding.color : void 0;
668
+ const conditionalColor = encoding.color && isConditionalValueDef(encoding.color) ? encoding.color : void 0;
669
+ const colorField = colorEnc?.field;
670
+ const isSequentialColor = colorEnc?.type === "quantitative";
601
671
  if (!colorField || isSequentialColor) {
602
672
  return computeSimpleBars(
603
673
  spec.data,
@@ -608,7 +678,8 @@ function computeBarMarks(spec, scales, _chartArea, _strategy) {
608
678
  bandwidth,
609
679
  baseline,
610
680
  scales,
611
- isSequentialColor
681
+ isSequentialColor,
682
+ conditionalColor
612
683
  );
613
684
  }
614
685
  return computeStackedBars(
@@ -657,7 +728,7 @@ function computeStackedBars(data, valueField, categoryField, colorField, xScale,
657
728
  }
658
729
  return marks;
659
730
  }
660
- function computeSimpleBars(data, valueField, categoryField, xScale, yScale, bandwidth, baseline, scales, sequentialColor = false) {
731
+ function computeSimpleBars(data, valueField, categoryField, xScale, yScale, bandwidth, baseline, scales, sequentialColor = false, conditionalColor) {
661
732
  const marks = [];
662
733
  for (const row of data) {
663
734
  const category = String(row[categoryField] ?? "");
@@ -665,7 +736,16 @@ function computeSimpleBars(data, valueField, categoryField, xScale, yScale, band
665
736
  if (!Number.isFinite(value)) continue;
666
737
  const bandY = yScale(category);
667
738
  if (bandY === void 0) continue;
668
- const color2 = sequentialColor ? getSequentialColor(scales, value) : getColor(scales, "__default__");
739
+ let color2;
740
+ if (conditionalColor) {
741
+ color2 = String(
742
+ resolveConditionalValue(row, conditionalColor) ?? getColor(scales, "__default__")
743
+ );
744
+ } else if (sequentialColor) {
745
+ color2 = getSequentialColor(scales, value);
746
+ } else {
747
+ color2 = getColor(scales, "__default__");
748
+ }
669
749
  const xPos = value >= 0 ? baseline : xScale(value);
670
750
  const barWidth = Math.max(Math.abs(xScale(value) - baseline), MIN_BAR_WIDTH);
671
751
  const aria = {
@@ -794,8 +874,10 @@ function computeColumnMarks(spec, scales, _chartArea, _strategy) {
794
874
  }
795
875
  const bandwidth = xScale.bandwidth();
796
876
  const baseline = yScale(0);
797
- const colorField = encoding.color?.field;
798
- const isSequentialColor = encoding.color?.type === "quantitative";
877
+ const colorEnc = encoding.color && "field" in encoding.color ? encoding.color : void 0;
878
+ const conditionalColor = encoding.color && isConditionalValueDef(encoding.color) ? encoding.color : void 0;
879
+ const colorField = colorEnc?.field;
880
+ const isSequentialColor = colorEnc?.type === "quantitative";
799
881
  if (colorField && !isSequentialColor) {
800
882
  const categoryGroups = groupByField(spec.data, xChannel.field);
801
883
  const needsStacking = Array.from(categoryGroups.values()).some((rows) => rows.length > 1);
@@ -833,10 +915,11 @@ function computeColumnMarks(spec, scales, _chartArea, _strategy) {
833
915
  bandwidth,
834
916
  baseline,
835
917
  scales,
836
- isSequentialColor
918
+ isSequentialColor,
919
+ conditionalColor
837
920
  );
838
921
  }
839
- function computeSimpleColumns(data, categoryField, valueField, xScale, yScale, bandwidth, baseline, scales, sequentialColor = false) {
922
+ function computeSimpleColumns(data, categoryField, valueField, xScale, yScale, bandwidth, baseline, scales, sequentialColor = false, conditionalColor) {
840
923
  const marks = [];
841
924
  for (const row of data) {
842
925
  const category = String(row[categoryField] ?? "");
@@ -844,7 +927,16 @@ function computeSimpleColumns(data, categoryField, valueField, xScale, yScale, b
844
927
  if (!Number.isFinite(value)) continue;
845
928
  const bandX = xScale(category);
846
929
  if (bandX === void 0) continue;
847
- const color2 = sequentialColor ? getSequentialColor(scales, value) : getColor(scales, "__default__");
930
+ let color2;
931
+ if (conditionalColor) {
932
+ color2 = String(
933
+ resolveConditionalValue(row, conditionalColor) ?? getColor(scales, "__default__")
934
+ );
935
+ } else if (sequentialColor) {
936
+ color2 = getSequentialColor(scales, value);
937
+ } else {
938
+ color2 = getColor(scales, "__default__");
939
+ }
848
940
  const yPos = yScale(value);
849
941
  const columnHeight = Math.max(Math.abs(baseline - yPos), MIN_COLUMN_HEIGHT);
850
942
  const y2 = value >= 0 ? yPos : baseline;
@@ -1019,7 +1111,9 @@ function computeDotMarks(spec, scales, _chartArea, _strategy) {
1019
1111
  }
1020
1112
  const bandwidth = yScale.bandwidth();
1021
1113
  const baseline = xScale(0);
1022
- const colorField = encoding.color?.field;
1114
+ const colorEnc = encoding.color && "field" in encoding.color ? encoding.color : void 0;
1115
+ const isSequentialColor = colorEnc?.type === "quantitative";
1116
+ const colorField = isSequentialColor ? void 0 : colorEnc?.field;
1023
1117
  if (colorField) {
1024
1118
  return computeDumbbellMarks(
1025
1119
  spec.data,
@@ -1040,7 +1134,8 @@ function computeDotMarks(spec, scales, _chartArea, _strategy) {
1040
1134
  yScale,
1041
1135
  bandwidth,
1042
1136
  baseline,
1043
- scales
1137
+ scales,
1138
+ isSequentialColor
1044
1139
  );
1045
1140
  }
1046
1141
  function computeDumbbellMarks(data, valueField, categoryField, colorField, xScale, yScale, bandwidth, scales) {
@@ -1100,7 +1195,7 @@ function computeDumbbellMarks(data, valueField, categoryField, colorField, xScal
1100
1195
  }
1101
1196
  return marks;
1102
1197
  }
1103
- function computeLollipopMarks(data, valueField, categoryField, xScale, yScale, bandwidth, baseline, scales) {
1198
+ function computeLollipopMarks(data, valueField, categoryField, xScale, yScale, bandwidth, baseline, scales, isSequentialColor = false) {
1104
1199
  const marks = [];
1105
1200
  for (const row of data) {
1106
1201
  const category = String(row[categoryField] ?? "");
@@ -1110,7 +1205,7 @@ function computeLollipopMarks(data, valueField, categoryField, xScale, yScale, b
1110
1205
  if (bandY === void 0) continue;
1111
1206
  const cx = xScale(value);
1112
1207
  const cy = bandY + bandwidth / 2;
1113
- const color2 = getColor(scales, "__default__");
1208
+ const color2 = isSequentialColor ? getSequentialColor(scales, value) : getColor(scales, "__default__");
1114
1209
  const stemX = Math.min(baseline, cx);
1115
1210
  const stemWidth = Math.abs(cx - baseline);
1116
1211
  if (stemWidth > 0) {
@@ -1694,6 +1789,139 @@ function pie_default() {
1694
1789
  return pie;
1695
1790
  }
1696
1791
 
1792
+ // ../../node_modules/.bun/d3-shape@3.2.0/node_modules/d3-shape/src/curve/basis.js
1793
+ function point(that, x2, y2) {
1794
+ that._context.bezierCurveTo(
1795
+ (2 * that._x0 + that._x1) / 3,
1796
+ (2 * that._y0 + that._y1) / 3,
1797
+ (that._x0 + 2 * that._x1) / 3,
1798
+ (that._y0 + 2 * that._y1) / 3,
1799
+ (that._x0 + 4 * that._x1 + x2) / 6,
1800
+ (that._y0 + 4 * that._y1 + y2) / 6
1801
+ );
1802
+ }
1803
+ function Basis(context) {
1804
+ this._context = context;
1805
+ }
1806
+ Basis.prototype = {
1807
+ areaStart: function() {
1808
+ this._line = 0;
1809
+ },
1810
+ areaEnd: function() {
1811
+ this._line = NaN;
1812
+ },
1813
+ lineStart: function() {
1814
+ this._x0 = this._x1 = this._y0 = this._y1 = NaN;
1815
+ this._point = 0;
1816
+ },
1817
+ lineEnd: function() {
1818
+ switch (this._point) {
1819
+ case 3:
1820
+ point(this, this._x1, this._y1);
1821
+ // falls through
1822
+ case 2:
1823
+ this._context.lineTo(this._x1, this._y1);
1824
+ break;
1825
+ }
1826
+ if (this._line || this._line !== 0 && this._point === 1) this._context.closePath();
1827
+ this._line = 1 - this._line;
1828
+ },
1829
+ point: function(x2, y2) {
1830
+ x2 = +x2, y2 = +y2;
1831
+ switch (this._point) {
1832
+ case 0:
1833
+ this._point = 1;
1834
+ this._line ? this._context.lineTo(x2, y2) : this._context.moveTo(x2, y2);
1835
+ break;
1836
+ case 1:
1837
+ this._point = 2;
1838
+ break;
1839
+ case 2:
1840
+ this._point = 3;
1841
+ this._context.lineTo((5 * this._x0 + this._x1) / 6, (5 * this._y0 + this._y1) / 6);
1842
+ // falls through
1843
+ default:
1844
+ point(this, x2, y2);
1845
+ break;
1846
+ }
1847
+ this._x0 = this._x1, this._x1 = x2;
1848
+ this._y0 = this._y1, this._y1 = y2;
1849
+ }
1850
+ };
1851
+ function basis_default(context) {
1852
+ return new Basis(context);
1853
+ }
1854
+
1855
+ // ../../node_modules/.bun/d3-shape@3.2.0/node_modules/d3-shape/src/curve/cardinal.js
1856
+ function point2(that, x2, y2) {
1857
+ that._context.bezierCurveTo(
1858
+ that._x1 + that._k * (that._x2 - that._x0),
1859
+ that._y1 + that._k * (that._y2 - that._y0),
1860
+ that._x2 + that._k * (that._x1 - x2),
1861
+ that._y2 + that._k * (that._y1 - y2),
1862
+ that._x2,
1863
+ that._y2
1864
+ );
1865
+ }
1866
+ function Cardinal(context, tension) {
1867
+ this._context = context;
1868
+ this._k = (1 - tension) / 6;
1869
+ }
1870
+ Cardinal.prototype = {
1871
+ areaStart: function() {
1872
+ this._line = 0;
1873
+ },
1874
+ areaEnd: function() {
1875
+ this._line = NaN;
1876
+ },
1877
+ lineStart: function() {
1878
+ this._x0 = this._x1 = this._x2 = this._y0 = this._y1 = this._y2 = NaN;
1879
+ this._point = 0;
1880
+ },
1881
+ lineEnd: function() {
1882
+ switch (this._point) {
1883
+ case 2:
1884
+ this._context.lineTo(this._x2, this._y2);
1885
+ break;
1886
+ case 3:
1887
+ point2(this, this._x1, this._y1);
1888
+ break;
1889
+ }
1890
+ if (this._line || this._line !== 0 && this._point === 1) this._context.closePath();
1891
+ this._line = 1 - this._line;
1892
+ },
1893
+ point: function(x2, y2) {
1894
+ x2 = +x2, y2 = +y2;
1895
+ switch (this._point) {
1896
+ case 0:
1897
+ this._point = 1;
1898
+ this._line ? this._context.lineTo(x2, y2) : this._context.moveTo(x2, y2);
1899
+ break;
1900
+ case 1:
1901
+ this._point = 2;
1902
+ this._x1 = x2, this._y1 = y2;
1903
+ break;
1904
+ case 2:
1905
+ this._point = 3;
1906
+ // falls through
1907
+ default:
1908
+ point2(this, x2, y2);
1909
+ break;
1910
+ }
1911
+ this._x0 = this._x1, this._x1 = this._x2, this._x2 = x2;
1912
+ this._y0 = this._y1, this._y1 = this._y2, this._y2 = y2;
1913
+ }
1914
+ };
1915
+ var cardinal_default = (function custom(tension) {
1916
+ function cardinal(context) {
1917
+ return new Cardinal(context, tension);
1918
+ }
1919
+ cardinal.tension = function(tension2) {
1920
+ return custom(+tension2);
1921
+ };
1922
+ return cardinal;
1923
+ })(0);
1924
+
1697
1925
  // ../../node_modules/.bun/d3-shape@3.2.0/node_modules/d3-shape/src/curve/monotone.js
1698
1926
  function sign(x2) {
1699
1927
  return x2 < 0 ? -1 : 1;
@@ -1706,7 +1934,7 @@ function slope2(that, t) {
1706
1934
  var h = that._x1 - that._x0;
1707
1935
  return h ? (3 * (that._y1 - that._y0) / h - t) / 2 : t;
1708
1936
  }
1709
- function point(that, t02, t12) {
1937
+ function point3(that, t02, t12) {
1710
1938
  var x0 = that._x0, y0 = that._y0, x1 = that._x1, y1 = that._y1, dx = (x1 - x0) / 3;
1711
1939
  that._context.bezierCurveTo(x0 + dx, y0 + dx * t02, x1 - dx, y1 - dx * t12, x1, y1);
1712
1940
  }
@@ -1730,7 +1958,7 @@ MonotoneX.prototype = {
1730
1958
  this._context.lineTo(this._x1, this._y1);
1731
1959
  break;
1732
1960
  case 3:
1733
- point(this, this._t0, slope2(this, this._t0));
1961
+ point3(this, this._t0, slope2(this, this._t0));
1734
1962
  break;
1735
1963
  }
1736
1964
  if (this._line || this._line !== 0 && this._point === 1) this._context.closePath();
@@ -1750,10 +1978,10 @@ MonotoneX.prototype = {
1750
1978
  break;
1751
1979
  case 2:
1752
1980
  this._point = 3;
1753
- point(this, slope2(this, t12 = slope3(this, x2, y2)), t12);
1981
+ point3(this, slope2(this, t12 = slope3(this, x2, y2)), t12);
1754
1982
  break;
1755
1983
  default:
1756
- point(this, this._t0, t12 = slope3(this, x2, y2));
1984
+ point3(this, this._t0, t12 = slope3(this, x2, y2));
1757
1985
  break;
1758
1986
  }
1759
1987
  this._x0 = this._x1, this._x1 = x2;
@@ -1788,6 +2016,115 @@ function monotoneX(context) {
1788
2016
  return new MonotoneX(context);
1789
2017
  }
1790
2018
 
2019
+ // ../../node_modules/.bun/d3-shape@3.2.0/node_modules/d3-shape/src/curve/natural.js
2020
+ function Natural(context) {
2021
+ this._context = context;
2022
+ }
2023
+ Natural.prototype = {
2024
+ areaStart: function() {
2025
+ this._line = 0;
2026
+ },
2027
+ areaEnd: function() {
2028
+ this._line = NaN;
2029
+ },
2030
+ lineStart: function() {
2031
+ this._x = [];
2032
+ this._y = [];
2033
+ },
2034
+ lineEnd: function() {
2035
+ var x2 = this._x, y2 = this._y, n = x2.length;
2036
+ if (n) {
2037
+ this._line ? this._context.lineTo(x2[0], y2[0]) : this._context.moveTo(x2[0], y2[0]);
2038
+ if (n === 2) {
2039
+ this._context.lineTo(x2[1], y2[1]);
2040
+ } else {
2041
+ var px = controlPoints(x2), py = controlPoints(y2);
2042
+ for (var i0 = 0, i1 = 1; i1 < n; ++i0, ++i1) {
2043
+ this._context.bezierCurveTo(px[0][i0], py[0][i0], px[1][i0], py[1][i0], x2[i1], y2[i1]);
2044
+ }
2045
+ }
2046
+ }
2047
+ if (this._line || this._line !== 0 && n === 1) this._context.closePath();
2048
+ this._line = 1 - this._line;
2049
+ this._x = this._y = null;
2050
+ },
2051
+ point: function(x2, y2) {
2052
+ this._x.push(+x2);
2053
+ this._y.push(+y2);
2054
+ }
2055
+ };
2056
+ function controlPoints(x2) {
2057
+ var i, n = x2.length - 1, m, a = new Array(n), b = new Array(n), r = new Array(n);
2058
+ a[0] = 0, b[0] = 2, r[0] = x2[0] + 2 * x2[1];
2059
+ for (i = 1; i < n - 1; ++i) a[i] = 1, b[i] = 4, r[i] = 4 * x2[i] + 2 * x2[i + 1];
2060
+ a[n - 1] = 2, b[n - 1] = 7, r[n - 1] = 8 * x2[n - 1] + x2[n];
2061
+ for (i = 1; i < n; ++i) m = a[i] / b[i - 1], b[i] -= m, r[i] -= m * r[i - 1];
2062
+ a[n - 1] = r[n - 1] / b[n - 1];
2063
+ for (i = n - 2; i >= 0; --i) a[i] = (r[i] - a[i + 1]) / b[i];
2064
+ b[n - 1] = (x2[n] + a[n - 1]) / 2;
2065
+ for (i = 0; i < n - 1; ++i) b[i] = 2 * x2[i + 1] - a[i + 1];
2066
+ return [a, b];
2067
+ }
2068
+ function natural_default(context) {
2069
+ return new Natural(context);
2070
+ }
2071
+
2072
+ // ../../node_modules/.bun/d3-shape@3.2.0/node_modules/d3-shape/src/curve/step.js
2073
+ function Step(context, t) {
2074
+ this._context = context;
2075
+ this._t = t;
2076
+ }
2077
+ Step.prototype = {
2078
+ areaStart: function() {
2079
+ this._line = 0;
2080
+ },
2081
+ areaEnd: function() {
2082
+ this._line = NaN;
2083
+ },
2084
+ lineStart: function() {
2085
+ this._x = this._y = NaN;
2086
+ this._point = 0;
2087
+ },
2088
+ lineEnd: function() {
2089
+ if (0 < this._t && this._t < 1 && this._point === 2) this._context.lineTo(this._x, this._y);
2090
+ if (this._line || this._line !== 0 && this._point === 1) this._context.closePath();
2091
+ if (this._line >= 0) this._t = 1 - this._t, this._line = 1 - this._line;
2092
+ },
2093
+ point: function(x2, y2) {
2094
+ x2 = +x2, y2 = +y2;
2095
+ switch (this._point) {
2096
+ case 0:
2097
+ this._point = 1;
2098
+ this._line ? this._context.lineTo(x2, y2) : this._context.moveTo(x2, y2);
2099
+ break;
2100
+ case 1:
2101
+ this._point = 2;
2102
+ // falls through
2103
+ default: {
2104
+ if (this._t <= 0) {
2105
+ this._context.lineTo(this._x, y2);
2106
+ this._context.lineTo(x2, y2);
2107
+ } else {
2108
+ var x1 = this._x * (1 - this._t) + x2 * this._t;
2109
+ this._context.lineTo(x1, this._y);
2110
+ this._context.lineTo(x1, y2);
2111
+ }
2112
+ break;
2113
+ }
2114
+ }
2115
+ this._x = x2, this._y = y2;
2116
+ }
2117
+ };
2118
+ function step_default(context) {
2119
+ return new Step(context, 0.5);
2120
+ }
2121
+ function stepBefore(context) {
2122
+ return new Step(context, 0);
2123
+ }
2124
+ function stepAfter(context) {
2125
+ return new Step(context, 1);
2126
+ }
2127
+
1791
2128
  // ../../node_modules/.bun/d3-shape@3.2.0/node_modules/d3-shape/src/offset/none.js
1792
2129
  function none_default(series, order) {
1793
2130
  if (!((n = series.length) > 1)) return;
@@ -1845,6 +2182,22 @@ function stack_default() {
1845
2182
  return stack;
1846
2183
  }
1847
2184
 
2185
+ // src/charts/line/curves.ts
2186
+ var CURVE_MAP = {
2187
+ linear: linear_default,
2188
+ monotone: monotoneX,
2189
+ step: step_default,
2190
+ "step-before": stepBefore,
2191
+ "step-after": stepAfter,
2192
+ basis: basis_default,
2193
+ cardinal: cardinal_default,
2194
+ natural: natural_default
2195
+ };
2196
+ function resolveCurve(interpolate) {
2197
+ if (!interpolate) return monotoneX;
2198
+ return CURVE_MAP[interpolate] ?? monotoneX;
2199
+ }
2200
+
1848
2201
  // src/charts/line/area.ts
1849
2202
  var DEFAULT_FILL_OPACITY = 0.15;
1850
2203
  function computeSingleArea(spec, scales, _chartArea) {
@@ -1855,7 +2208,7 @@ function computeSingleArea(spec, scales, _chartArea) {
1855
2208
  const yScale = scales.y.scale;
1856
2209
  const domain = yScale.domain();
1857
2210
  const baselineY = yScale(Math.min(domain[0], domain[1]));
1858
- const colorField = encoding.color?.field;
2211
+ const colorField = encoding.color && "field" in encoding.color ? encoding.color.field : void 0;
1859
2212
  const groups = /* @__PURE__ */ new Map();
1860
2213
  if (!colorField) {
1861
2214
  groups.set("__default__", spec.data);
@@ -1887,9 +2240,10 @@ function computeSingleArea(spec, scales, _chartArea) {
1887
2240
  });
1888
2241
  }
1889
2242
  if (validPoints.length === 0) continue;
1890
- const areaGenerator = area_default().x((d) => d.x).y0((d) => d.yBottom).y1((d) => d.yTop).curve(monotoneX);
2243
+ const curve = resolveCurve(spec.markDef.interpolate);
2244
+ const areaGenerator = area_default().x((d) => d.x).y0((d) => d.yBottom).y1((d) => d.yTop).curve(curve);
1891
2245
  const pathStr = areaGenerator(validPoints) ?? "";
1892
- const topLineGenerator = line_default().x((d) => d.x).y((d) => d.yTop).curve(monotoneX);
2246
+ const topLineGenerator = line_default().x((d) => d.x).y((d) => d.yTop).curve(curve);
1893
2247
  const topPathStr = topLineGenerator(validPoints) ?? "";
1894
2248
  const topPoints = validPoints.map((p) => ({ x: p.x, y: p.yTop }));
1895
2249
  const bottomPoints = validPoints.map((p) => ({ x: p.x, y: p.yBottom }));
@@ -1907,6 +2261,7 @@ function computeSingleArea(spec, scales, _chartArea) {
1907
2261
  strokeWidth: 2,
1908
2262
  seriesKey: seriesKey === "__default__" ? void 0 : seriesKey,
1909
2263
  data: validPoints.map((p) => p.row),
2264
+ dataPoints: validPoints.map((p) => ({ x: p.x, y: p.yTop, datum: p.row })),
1910
2265
  aria
1911
2266
  });
1912
2267
  }
@@ -1916,7 +2271,7 @@ function computeStackedArea(spec, scales, chartArea) {
1916
2271
  const encoding = spec.encoding;
1917
2272
  const xChannel = encoding.x;
1918
2273
  const yChannel = encoding.y;
1919
- const colorField = encoding.color?.field;
2274
+ const colorField = encoding.color && "field" in encoding.color ? encoding.color.field : void 0;
1920
2275
  if (!xChannel || !yChannel || !scales.x || !scales.y || !colorField) {
1921
2276
  return computeSingleArea(spec, scales, chartArea);
1922
2277
  }
@@ -1970,9 +2325,10 @@ function computeStackedArea(spec, scales, chartArea) {
1970
2325
  validPoints.push({ x: xVal, yTop, yBottom });
1971
2326
  }
1972
2327
  if (validPoints.length === 0) continue;
1973
- const areaGenerator = area_default().x((p) => p.x).y0((p) => p.yBottom).y1((p) => p.yTop).curve(monotoneX);
2328
+ const stackCurve = resolveCurve(spec.markDef.interpolate);
2329
+ const areaGenerator = area_default().x((p) => p.x).y0((p) => p.yBottom).y1((p) => p.yTop).curve(stackCurve);
1974
2330
  const pathStr = areaGenerator(validPoints) ?? "";
1975
- const topLineGenerator = line_default().x((p) => p.x).y((p) => p.yTop).curve(monotoneX);
2331
+ const topLineGenerator = line_default().x((p) => p.x).y((p) => p.yTop).curve(stackCurve);
1976
2332
  const topPathStr = topLineGenerator(validPoints) ?? "";
1977
2333
  const topPoints = validPoints.map((p) => ({ x: p.x, y: p.yTop }));
1978
2334
  const bottomPoints = validPoints.map((p) => ({ x: p.x, y: p.yBottom }));
@@ -1995,6 +2351,11 @@ function computeStackedArea(spec, scales, chartArea) {
1995
2351
  const xStr = String(d.data.__x__);
1996
2352
  return rowsByXSeries.get(`${xStr}::${seriesKey}`) ?? d.data;
1997
2353
  }),
2354
+ dataPoints: validPoints.map((p, idx) => {
2355
+ const xStr = String(layer[idx]?.data.__x__);
2356
+ const datum = rowsByXSeries.get(`${xStr}::${seriesKey}`) ?? layer[idx]?.data ?? {};
2357
+ return { x: p.x, y: p.yTop, datum };
2358
+ }),
1998
2359
  aria
1999
2360
  });
2000
2361
  }
@@ -2019,11 +2380,14 @@ function computeLineMarks(spec, scales, _chartArea, _strategy) {
2019
2380
  if (!xChannel || !yChannel || !scales.x || !scales.y) {
2020
2381
  return [];
2021
2382
  }
2022
- const colorField = encoding.color?.field;
2383
+ const colorEnc = encoding.color && "field" in encoding.color ? encoding.color : void 0;
2384
+ const isSequentialColor = colorEnc?.type === "quantitative";
2385
+ const colorField = isSequentialColor ? void 0 : colorEnc?.field;
2386
+ const sequentialColorField = isSequentialColor ? colorEnc.field : void 0;
2023
2387
  const groups = groupByField(spec.data, colorField);
2024
2388
  const marks = [];
2025
2389
  for (const [seriesKey, rows] of groups) {
2026
- const color2 = getColor(scales, seriesKey);
2390
+ const color2 = isSequentialColor ? getSequentialColor(scales, _getMidValue(rows, sequentialColorField)) : getColor(scales, seriesKey);
2027
2391
  const sortedRows = sortByField(rows, xChannel.field);
2028
2392
  const pointsWithData = [];
2029
2393
  const segments = [];
@@ -2038,14 +2402,15 @@ function computeLineMarks(spec, scales, _chartArea, _strategy) {
2038
2402
  }
2039
2403
  continue;
2040
2404
  }
2041
- const point3 = { x: xVal, y: yVal };
2042
- currentSegment.push(point3);
2043
- pointsWithData.push({ ...point3, row });
2405
+ const point5 = { x: xVal, y: yVal };
2406
+ currentSegment.push(point5);
2407
+ pointsWithData.push({ ...point5, row });
2044
2408
  }
2045
2409
  if (currentSegment.length > 0) {
2046
2410
  segments.push(currentSegment);
2047
2411
  }
2048
- const lineGenerator = line_default().x((d) => d.x).y((d) => d.y).curve(monotoneX);
2412
+ const curve = resolveCurve(spec.markDef.interpolate);
2413
+ const lineGenerator = line_default().x((d) => d.x).y((d) => d.y).curve(curve);
2049
2414
  const allPoints = [];
2050
2415
  const pathParts = [];
2051
2416
  for (const segment of segments) {
@@ -2077,31 +2442,49 @@ function computeLineMarks(spec, scales, _chartArea, _strategy) {
2077
2442
  opacity: styleOverride?.opacity,
2078
2443
  seriesKey: seriesStyleKey,
2079
2444
  data: pointsWithData.map((p) => p.row),
2445
+ dataPoints: pointsWithData.map((p) => ({ x: p.x, y: p.y, datum: p.row })),
2080
2446
  aria
2081
2447
  };
2082
2448
  marks.push(lineMark);
2083
- const showPoints = styleOverride?.showPoints !== false;
2084
- for (let i = 0; i < pointsWithData.length; i++) {
2085
- const p = pointsWithData[i];
2086
- const pointMark = {
2087
- type: "point",
2088
- cx: p.x,
2089
- cy: p.y,
2090
- r: showPoints ? DEFAULT_POINT_RADIUS : 0,
2091
- fill: color2,
2092
- stroke: showPoints ? "#ffffff" : "transparent",
2093
- strokeWidth: showPoints ? 1.5 : 0,
2094
- fillOpacity: 0,
2095
- data: p.row,
2096
- aria: {
2097
- label: `Data point: ${xChannel.field}=${String(p.row[xChannel.field])}, ${yChannel.field}=${String(p.row[yChannel.field])}`
2449
+ const markPoint = spec.markDef.point;
2450
+ const showPoints = markPoint === true || markPoint === "transparent" || isSequentialColor;
2451
+ if (showPoints) {
2452
+ const isTransparent = markPoint === "transparent";
2453
+ const seriesShowPoints = styleOverride?.showPoints !== false;
2454
+ for (let i = 0; i < pointsWithData.length; i++) {
2455
+ const p = pointsWithData[i];
2456
+ const visible = seriesShowPoints && !isTransparent;
2457
+ let pointColor = color2;
2458
+ if (isSequentialColor) {
2459
+ const val = Number(p.row[sequentialColorField]);
2460
+ pointColor = Number.isFinite(val) ? getSequentialColor(scales, val) : color2;
2098
2461
  }
2099
- };
2100
- marks.push(pointMark);
2462
+ const pointMark = {
2463
+ type: "point",
2464
+ cx: p.x,
2465
+ cy: p.y,
2466
+ r: visible ? DEFAULT_POINT_RADIUS : 0,
2467
+ fill: pointColor,
2468
+ stroke: visible ? "#ffffff" : "transparent",
2469
+ strokeWidth: visible ? 1.5 : 0,
2470
+ fillOpacity: isTransparent ? 0 : 1,
2471
+ data: p.row,
2472
+ aria: {
2473
+ label: `Data point: ${xChannel.field}=${String(p.row[xChannel.field])}, ${yChannel.field}=${String(p.row[yChannel.field])}`
2474
+ }
2475
+ };
2476
+ marks.push(pointMark);
2477
+ }
2101
2478
  }
2102
2479
  }
2103
2480
  return marks;
2104
2481
  }
2482
+ function _getMidValue(rows, field) {
2483
+ const values = rows.map((r) => Number(r[field])).filter(Number.isFinite);
2484
+ if (values.length === 0) return 0;
2485
+ const sorted = values.sort((a, b) => a - b);
2486
+ return sorted[Math.floor(sorted.length / 2)];
2487
+ }
2105
2488
 
2106
2489
  // src/charts/line/labels.ts
2107
2490
  import { estimateTextWidth as estimateTextWidth5, resolveCollisions as resolveCollisions4 } from "@opendata-ai/openchart-core";
@@ -2207,13 +2590,13 @@ var DEFAULT_PALETTE = [
2207
2590
  "#a88f22",
2208
2591
  "#858078"
2209
2592
  ];
2210
- function groupSmallSlices(slices, threshold) {
2593
+ function groupSmallSlices(slices, threshold2) {
2211
2594
  const total = slices.reduce((sum, s) => sum + s.value, 0);
2212
2595
  if (total === 0) return slices;
2213
2596
  const big = [];
2214
2597
  let otherValue = 0;
2215
2598
  for (const slice2 of slices) {
2216
- if (slice2.value / total < threshold) {
2599
+ if (slice2.value / total < threshold2) {
2217
2600
  otherValue += slice2.value;
2218
2601
  } else {
2219
2602
  big.push(slice2);
@@ -2231,7 +2614,7 @@ function groupSmallSlices(slices, threshold) {
2231
2614
  function computePieMarks(spec, scales, chartArea, _strategy, isDonut = false) {
2232
2615
  const encoding = spec.encoding;
2233
2616
  const valueChannel = encoding.y ?? encoding.x;
2234
- const categoryField = encoding.color?.field;
2617
+ const categoryField = encoding.color && "field" in encoding.color ? encoding.color.field : void 0;
2235
2618
  if (!valueChannel) return [];
2236
2619
  let slices = [];
2237
2620
  if (categoryField) {
@@ -2419,6 +2802,75 @@ function clearRenderers() {
2419
2802
  renderers.clear();
2420
2803
  }
2421
2804
 
2805
+ // src/charts/rule/index.ts
2806
+ function computeRuleMarks(spec, scales, chartArea) {
2807
+ const encoding = spec.encoding;
2808
+ const xChannel = encoding.x;
2809
+ const yChannel = encoding.y;
2810
+ const x2Channel = encoding.x2;
2811
+ const y2Channel = encoding.y2;
2812
+ const colorEncoding = encoding.color && "field" in encoding.color ? encoding.color : void 0;
2813
+ const colorField = colorEncoding?.field;
2814
+ const marks = [];
2815
+ for (const row of spec.data) {
2816
+ let x1 = chartArea.x;
2817
+ let y1 = chartArea.y;
2818
+ let x2 = chartArea.x + chartArea.width;
2819
+ let y2 = chartArea.y + chartArea.height;
2820
+ if (xChannel && scales.x) {
2821
+ const xVal = scaleValue(scales.x.scale, scales.x.type, row[xChannel.field]);
2822
+ if (xVal == null) continue;
2823
+ x1 = xVal;
2824
+ x2 = xVal;
2825
+ }
2826
+ if (yChannel && scales.y) {
2827
+ const yVal = scaleValue(scales.y.scale, scales.y.type, row[yChannel.field]);
2828
+ if (yVal == null) continue;
2829
+ y1 = yVal;
2830
+ y2 = yVal;
2831
+ }
2832
+ if (xChannel && !yChannel) {
2833
+ y1 = chartArea.y;
2834
+ y2 = chartArea.y + chartArea.height;
2835
+ }
2836
+ if (yChannel && !xChannel) {
2837
+ x1 = chartArea.x;
2838
+ x2 = chartArea.x + chartArea.width;
2839
+ }
2840
+ if (x2Channel && scales.x) {
2841
+ const x2Val = scaleValue(scales.x.scale, scales.x.type, row[x2Channel.field]);
2842
+ if (x2Val != null) x2 = x2Val;
2843
+ }
2844
+ if (y2Channel && scales.y) {
2845
+ const y2Val = scaleValue(scales.y.scale, scales.y.type, row[y2Channel.field]);
2846
+ if (y2Val != null) y2 = y2Val;
2847
+ }
2848
+ const color2 = colorField ? getColor(scales, String(row[colorField] ?? "__default__")) : getColor(scales, "__default__");
2849
+ const strokeDashEncoding = encoding.strokeDash && "field" in encoding.strokeDash ? encoding.strokeDash : void 0;
2850
+ const strokeDasharray = strokeDashEncoding ? String(row[strokeDashEncoding.field] ?? "") : void 0;
2851
+ const aria = {
2852
+ label: `Rule from (${Math.round(x1)}, ${Math.round(y1)}) to (${Math.round(x2)}, ${Math.round(y2)})`
2853
+ };
2854
+ marks.push({
2855
+ type: "rule",
2856
+ x1,
2857
+ y1,
2858
+ x2,
2859
+ y2,
2860
+ stroke: color2,
2861
+ strokeWidth: 1,
2862
+ strokeDasharray: strokeDasharray || void 0,
2863
+ opacity: encoding.opacity && "field" in encoding.opacity ? Math.max(0, Math.min(1, Number(row[encoding.opacity.field]) || 1)) : void 0,
2864
+ data: row,
2865
+ aria
2866
+ });
2867
+ }
2868
+ return marks;
2869
+ }
2870
+ var ruleRenderer = (spec, scales, chartArea, _strategy, _theme) => {
2871
+ return computeRuleMarks(spec, scales, chartArea);
2872
+ };
2873
+
2422
2874
  // ../../node_modules/.bun/d3-array@3.2.4/node_modules/d3-array/src/ascending.js
2423
2875
  function ascending(a, b) {
2424
2876
  return a == null || b == null ? NaN : a < b ? -1 : a > b ? 1 : a >= b ? 0 : NaN;
@@ -2648,6 +3100,15 @@ function min2(values, valueof) {
2648
3100
  return min3;
2649
3101
  }
2650
3102
 
3103
+ // ../../node_modules/.bun/d3-array@3.2.4/node_modules/d3-array/src/quantile.js
3104
+ function quantileSorted(values, p, valueof = number) {
3105
+ if (!(n = values.length) || isNaN(p = +p)) return;
3106
+ if (p <= 0 || n < 2) return +valueof(values[0], 0, values);
3107
+ if (p >= 1) return +valueof(values[n - 1], n - 1, values);
3108
+ var n, i = (n - 1) * p, i0 = Math.floor(i), value0 = +valueof(values[i0], i0, values), value1 = +valueof(values[i0 + 1], i0 + 1, values);
3109
+ return value0 + (value1 - value0) * (i - i0);
3110
+ }
3111
+
2651
3112
  // ../../node_modules/.bun/d3-array@3.2.4/node_modules/d3-array/src/range.js
2652
3113
  function range(start, stop, step) {
2653
3114
  start = +start, stop = +stop, step = (n = arguments.length) < 2 ? (stop = start, start = 0, 1) : n < 3 ? 1 : +step;
@@ -2786,7 +3247,7 @@ function pointish(scale) {
2786
3247
  };
2787
3248
  return scale;
2788
3249
  }
2789
- function point2() {
3250
+ function point4() {
2790
3251
  return pointish(band.apply(null, arguments).paddingInner(1));
2791
3252
  }
2792
3253
 
@@ -3143,7 +3604,7 @@ function basis(t12, v0, v1, v2, v3) {
3143
3604
  var t2 = t12 * t12, t3 = t2 * t12;
3144
3605
  return ((1 - 3 * t12 + 3 * t2 - t3) * v0 + (4 - 6 * t2 + 3 * t3) * v1 + (1 + 3 * t12 + 3 * t2 - 3 * t3) * v2 + t3 * v3) / 6;
3145
3606
  }
3146
- function basis_default(values) {
3607
+ function basis_default2(values) {
3147
3608
  var n = values.length - 1;
3148
3609
  return function(t) {
3149
3610
  var i = t <= 0 ? t = 0 : t >= 1 ? (t = 1, n - 1) : Math.floor(t * n), v1 = values[i], v2 = values[i + 1], v0 = i > 0 ? values[i - 1] : 2 * v1 - v2, v3 = i < n - 1 ? values[i + 2] : 2 * v2 - v1;
@@ -3221,7 +3682,7 @@ function rgbSpline(spline) {
3221
3682
  };
3222
3683
  };
3223
3684
  }
3224
- var rgbBasis = rgbSpline(basis_default);
3685
+ var rgbBasis = rgbSpline(basis_default2);
3225
3686
  var rgbBasisClosed = rgbSpline(basisClosed_default);
3226
3687
 
3227
3688
  // ../../node_modules/.bun/d3-interpolate@3.0.1/node_modules/d3-interpolate/src/numberArray.js
@@ -3882,6 +4343,32 @@ function log() {
3882
4343
  return scale;
3883
4344
  }
3884
4345
 
4346
+ // ../../node_modules/.bun/d3-scale@4.0.2/node_modules/d3-scale/src/symlog.js
4347
+ function transformSymlog(c) {
4348
+ return function(x2) {
4349
+ return Math.sign(x2) * Math.log1p(Math.abs(x2 / c));
4350
+ };
4351
+ }
4352
+ function transformSymexp(c) {
4353
+ return function(x2) {
4354
+ return Math.sign(x2) * Math.expm1(Math.abs(x2)) * c;
4355
+ };
4356
+ }
4357
+ function symlogish(transform) {
4358
+ var c = 1, scale = transform(transformSymlog(c), transformSymexp(c));
4359
+ scale.constant = function(_) {
4360
+ return arguments.length ? transform(transformSymlog(c = +_), transformSymexp(c)) : c;
4361
+ };
4362
+ return linearish(scale);
4363
+ }
4364
+ function symlog() {
4365
+ var scale = symlogish(transformer());
4366
+ scale.copy = function() {
4367
+ return copy(scale, symlog()).constant(scale.constant());
4368
+ };
4369
+ return initRange.apply(scale, arguments);
4370
+ }
4371
+
3885
4372
  // ../../node_modules/.bun/d3-scale@4.0.2/node_modules/d3-scale/src/pow.js
3886
4373
  function transformPow(exponent) {
3887
4374
  return function(x2) {
@@ -3902,18 +4389,118 @@ function powish(transform) {
3902
4389
  scale.exponent = function(_) {
3903
4390
  return arguments.length ? (exponent = +_, rescale()) : exponent;
3904
4391
  };
3905
- return linearish(scale);
3906
- }
3907
- function pow() {
3908
- var scale = powish(transformer());
4392
+ return linearish(scale);
4393
+ }
4394
+ function pow() {
4395
+ var scale = powish(transformer());
4396
+ scale.copy = function() {
4397
+ return copy(scale, pow()).exponent(scale.exponent());
4398
+ };
4399
+ initRange.apply(scale, arguments);
4400
+ return scale;
4401
+ }
4402
+ function sqrt2() {
4403
+ return pow.apply(null, arguments).exponent(0.5);
4404
+ }
4405
+
4406
+ // ../../node_modules/.bun/d3-scale@4.0.2/node_modules/d3-scale/src/quantile.js
4407
+ function quantile2() {
4408
+ var domain = [], range2 = [], thresholds = [], unknown;
4409
+ function rescale() {
4410
+ var i = 0, n = Math.max(1, range2.length);
4411
+ thresholds = new Array(n - 1);
4412
+ while (++i < n) thresholds[i - 1] = quantileSorted(domain, i / n);
4413
+ return scale;
4414
+ }
4415
+ function scale(x2) {
4416
+ return x2 == null || isNaN(x2 = +x2) ? unknown : range2[bisect_default(thresholds, x2)];
4417
+ }
4418
+ scale.invertExtent = function(y2) {
4419
+ var i = range2.indexOf(y2);
4420
+ return i < 0 ? [NaN, NaN] : [
4421
+ i > 0 ? thresholds[i - 1] : domain[0],
4422
+ i < thresholds.length ? thresholds[i] : domain[domain.length - 1]
4423
+ ];
4424
+ };
4425
+ scale.domain = function(_) {
4426
+ if (!arguments.length) return domain.slice();
4427
+ domain = [];
4428
+ for (let d of _) if (d != null && !isNaN(d = +d)) domain.push(d);
4429
+ domain.sort(ascending);
4430
+ return rescale();
4431
+ };
4432
+ scale.range = function(_) {
4433
+ return arguments.length ? (range2 = Array.from(_), rescale()) : range2.slice();
4434
+ };
4435
+ scale.unknown = function(_) {
4436
+ return arguments.length ? (unknown = _, scale) : unknown;
4437
+ };
4438
+ scale.quantiles = function() {
4439
+ return thresholds.slice();
4440
+ };
4441
+ scale.copy = function() {
4442
+ return quantile2().domain(domain).range(range2).unknown(unknown);
4443
+ };
4444
+ return initRange.apply(scale, arguments);
4445
+ }
4446
+
4447
+ // ../../node_modules/.bun/d3-scale@4.0.2/node_modules/d3-scale/src/quantize.js
4448
+ function quantize() {
4449
+ var x0 = 0, x1 = 1, n = 1, domain = [0.5], range2 = [0, 1], unknown;
4450
+ function scale(x2) {
4451
+ return x2 != null && x2 <= x2 ? range2[bisect_default(domain, x2, 0, n)] : unknown;
4452
+ }
4453
+ function rescale() {
4454
+ var i = -1;
4455
+ domain = new Array(n);
4456
+ while (++i < n) domain[i] = ((i + 1) * x1 - (i - n) * x0) / (n + 1);
4457
+ return scale;
4458
+ }
4459
+ scale.domain = function(_) {
4460
+ return arguments.length ? ([x0, x1] = _, x0 = +x0, x1 = +x1, rescale()) : [x0, x1];
4461
+ };
4462
+ scale.range = function(_) {
4463
+ return arguments.length ? (n = (range2 = Array.from(_)).length - 1, rescale()) : range2.slice();
4464
+ };
4465
+ scale.invertExtent = function(y2) {
4466
+ var i = range2.indexOf(y2);
4467
+ return i < 0 ? [NaN, NaN] : i < 1 ? [x0, domain[0]] : i >= n ? [domain[n - 1], x1] : [domain[i - 1], domain[i]];
4468
+ };
4469
+ scale.unknown = function(_) {
4470
+ return arguments.length ? (unknown = _, scale) : scale;
4471
+ };
4472
+ scale.thresholds = function() {
4473
+ return domain.slice();
4474
+ };
4475
+ scale.copy = function() {
4476
+ return quantize().domain([x0, x1]).range(range2).unknown(unknown);
4477
+ };
4478
+ return initRange.apply(linearish(scale), arguments);
4479
+ }
4480
+
4481
+ // ../../node_modules/.bun/d3-scale@4.0.2/node_modules/d3-scale/src/threshold.js
4482
+ function threshold() {
4483
+ var domain = [0.5], range2 = [0, 1], unknown, n = 1;
4484
+ function scale(x2) {
4485
+ return x2 != null && x2 <= x2 ? range2[bisect_default(domain, x2, 0, n)] : unknown;
4486
+ }
4487
+ scale.domain = function(_) {
4488
+ return arguments.length ? (domain = Array.from(_), n = Math.min(domain.length, range2.length - 1), scale) : domain.slice();
4489
+ };
4490
+ scale.range = function(_) {
4491
+ return arguments.length ? (range2 = Array.from(_), n = Math.min(domain.length, range2.length - 1), scale) : range2.slice();
4492
+ };
4493
+ scale.invertExtent = function(y2) {
4494
+ var i = range2.indexOf(y2);
4495
+ return [domain[i - 1], domain[i]];
4496
+ };
4497
+ scale.unknown = function(_) {
4498
+ return arguments.length ? (unknown = _, scale) : unknown;
4499
+ };
3909
4500
  scale.copy = function() {
3910
- return copy(scale, pow()).exponent(scale.exponent());
4501
+ return threshold().domain(domain).range(range2).unknown(unknown);
3911
4502
  };
3912
- initRange.apply(scale, arguments);
3913
- return scale;
3914
- }
3915
- function sqrt2() {
3916
- return pow.apply(null, arguments).exponent(0.5);
4503
+ return initRange.apply(scale, arguments);
3917
4504
  }
3918
4505
 
3919
4506
  // ../../node_modules/.bun/d3-time@3.1.0/node_modules/d3-time/src/interval.js
@@ -4856,6 +5443,11 @@ function time() {
4856
5443
  return initRange.apply(calendar(timeTicks, timeTickInterval, timeYear, timeMonth, timeSunday, timeDay, timeHour, timeMinute, second, timeFormat).domain([new Date(2e3, 0, 1), new Date(2e3, 0, 2)]), arguments);
4857
5444
  }
4858
5445
 
5446
+ // ../../node_modules/.bun/d3-scale@4.0.2/node_modules/d3-scale/src/utcTime.js
5447
+ function utcTime() {
5448
+ return initRange.apply(calendar(utcTicks, utcTickInterval, utcYear, utcMonth, utcSunday, utcDay, utcHour, utcMinute, second, utcFormat).domain([Date.UTC(2e3, 0, 1), Date.UTC(2e3, 0, 2)]), arguments);
5449
+ }
5450
+
4859
5451
  // ../../node_modules/.bun/d3-scale@4.0.2/node_modules/d3-scale/src/sequential.js
4860
5452
  function transformer2() {
4861
5453
  var x0 = 0, x1 = 1, t02, t12, k10, transform, interpolator = identity, clamp = false, unknown;
@@ -4911,8 +5503,10 @@ function computeScatterMarks(spec, scales, _chartArea, _strategy) {
4911
5503
  }
4912
5504
  const xScale = scales.x.scale;
4913
5505
  const yScale = scales.y.scale;
4914
- const colorField = encoding.color?.field;
4915
- const sizeField = encoding.size?.field;
5506
+ const colorEnc = encoding.color && "field" in encoding.color ? encoding.color : void 0;
5507
+ const isSequentialColor = colorEnc?.type === "quantitative";
5508
+ const colorField = colorEnc?.field;
5509
+ const sizeField = encoding.size && "field" in encoding.size ? encoding.size.field : void 0;
4916
5510
  let sizeScale;
4917
5511
  if (sizeField) {
4918
5512
  const sizeValues = spec.data.map((d) => Number(d[sizeField])).filter((v) => Number.isFinite(v));
@@ -4927,8 +5521,14 @@ function computeScatterMarks(spec, scales, _chartArea, _strategy) {
4927
5521
  if (!Number.isFinite(xVal) || !Number.isFinite(yVal)) continue;
4928
5522
  const cx = xScale(xVal);
4929
5523
  const cy = yScale(yVal);
4930
- const category = colorField ? String(row[colorField] ?? "") : void 0;
4931
- const color2 = getColor(scales, category ?? "__default__");
5524
+ const category = colorField && !isSequentialColor ? String(row[colorField] ?? "") : void 0;
5525
+ let color2;
5526
+ if (isSequentialColor && colorField) {
5527
+ const val = Number(row[colorField]);
5528
+ color2 = Number.isFinite(val) ? getSequentialColor(scales, val) : getColor(scales, "__default__");
5529
+ } else {
5530
+ color2 = getColor(scales, category ?? "__default__");
5531
+ }
4932
5532
  let radius = DEFAULT_POINT_RADIUS2;
4933
5533
  if (sizeScale && sizeField) {
4934
5534
  const sizeVal = Number(row[sizeField]);
@@ -5025,8 +5625,104 @@ var scatterRenderer = (spec, scales, chartArea, strategy, _theme) => {
5025
5625
  return marks;
5026
5626
  };
5027
5627
 
5628
+ // src/charts/text/index.ts
5629
+ function computeTextMarks(spec, scales) {
5630
+ const encoding = spec.encoding;
5631
+ const xChannel = encoding.x;
5632
+ const yChannel = encoding.y;
5633
+ const textChannel = encoding.text;
5634
+ if (!textChannel || !("field" in textChannel)) return [];
5635
+ const marks = [];
5636
+ const colorEncoding = encoding.color && "field" in encoding.color ? encoding.color : void 0;
5637
+ const colorField = colorEncoding?.field;
5638
+ const sizeEncoding = encoding.size && "field" in encoding.size ? encoding.size : void 0;
5639
+ for (const row of spec.data) {
5640
+ let x2 = 0;
5641
+ if (xChannel && scales.x) {
5642
+ const xVal = scaleValue(scales.x.scale, scales.x.type, row[xChannel.field]);
5643
+ if (xVal == null) continue;
5644
+ x2 = xVal;
5645
+ }
5646
+ let y2 = 0;
5647
+ if (yChannel && scales.y) {
5648
+ const yVal = scaleValue(scales.y.scale, scales.y.type, row[yChannel.field]);
5649
+ if (yVal == null) continue;
5650
+ y2 = yVal;
5651
+ }
5652
+ const text = String(row[textChannel.field] ?? "");
5653
+ if (!text) continue;
5654
+ const color2 = colorField ? getColor(scales, String(row[colorField] ?? "__default__")) : getColor(scales, "__default__");
5655
+ const fontSize = sizeEncoding ? Math.max(8, Math.min(48, Number(row[sizeEncoding.field]) || 12)) : 12;
5656
+ const aria = {
5657
+ label: text
5658
+ };
5659
+ marks.push({
5660
+ type: "textMark",
5661
+ x: x2,
5662
+ y: y2,
5663
+ text,
5664
+ fill: color2,
5665
+ fontSize,
5666
+ textAnchor: "middle",
5667
+ angle: encoding.angle && "field" in encoding.angle ? Number(row[encoding.angle.field]) || 0 : void 0,
5668
+ data: row,
5669
+ aria
5670
+ });
5671
+ }
5672
+ return marks;
5673
+ }
5674
+ var textRenderer = (spec, scales, _chartArea, _strategy, _theme) => {
5675
+ return computeTextMarks(spec, scales);
5676
+ };
5677
+
5678
+ // src/charts/tick/index.ts
5679
+ var DEFAULT_TICK_LENGTH = 18;
5680
+ function computeTickMarks(spec, scales, _chartArea) {
5681
+ const encoding = spec.encoding;
5682
+ const xChannel = encoding.x;
5683
+ const yChannel = encoding.y;
5684
+ if (!xChannel || !yChannel || !scales.x || !scales.y) return [];
5685
+ const colorEncoding = encoding.color && "field" in encoding.color ? encoding.color : void 0;
5686
+ const colorField = colorEncoding?.field;
5687
+ const marks = [];
5688
+ const isHorizontal = xChannel.type === "quantitative" && yChannel.type !== "quantitative";
5689
+ const orient = isHorizontal ? "horizontal" : "vertical";
5690
+ for (const row of spec.data) {
5691
+ const xVal = scaleValue(scales.x.scale, scales.x.type, row[xChannel.field]);
5692
+ const yVal = scaleValue(scales.y.scale, scales.y.type, row[yChannel.field]);
5693
+ if (xVal == null || yVal == null) continue;
5694
+ const color2 = colorField ? getColor(scales, String(row[colorField] ?? "__default__")) : getColor(scales, "__default__");
5695
+ const aria = {
5696
+ label: `${row[xChannel.field]}, ${row[yChannel.field]}`
5697
+ };
5698
+ marks.push({
5699
+ type: "tick",
5700
+ x: xVal,
5701
+ y: yVal,
5702
+ length: DEFAULT_TICK_LENGTH,
5703
+ orient,
5704
+ stroke: color2,
5705
+ strokeWidth: 1,
5706
+ opacity: encoding.opacity && "field" in encoding.opacity ? Math.max(0, Math.min(1, Number(row[encoding.opacity.field]) || 1)) : void 0,
5707
+ data: row,
5708
+ aria
5709
+ });
5710
+ }
5711
+ return marks;
5712
+ }
5713
+ var tickRenderer = (spec, scales, chartArea, _strategy, _theme) => {
5714
+ return computeTickMarks(spec, scales, chartArea);
5715
+ };
5716
+
5028
5717
  // src/compiler/normalize.ts
5029
- import { isChartSpec, isGraphSpec, isTableSpec } from "@opendata-ai/openchart-core";
5718
+ import {
5719
+ isChartSpec,
5720
+ isGraphSpec,
5721
+ isLayerSpec,
5722
+ isTableSpec,
5723
+ resolveMarkDef,
5724
+ resolveMarkType
5725
+ } from "@opendata-ai/openchart-core";
5030
5726
  function normalizeChromeField(value) {
5031
5727
  if (value === void 0) return void 0;
5032
5728
  if (typeof value === "string") return { text: value };
@@ -5081,6 +5777,7 @@ function inferEncodingTypes(encoding, data, warnings) {
5081
5777
  for (const channel of ["x", "y", "color", "size", "detail"]) {
5082
5778
  const spec = result[channel];
5083
5779
  if (!spec) continue;
5780
+ if ("condition" in spec) continue;
5084
5781
  if (!spec.type) {
5085
5782
  const inferred = inferFieldType(data, spec.field);
5086
5783
  result[channel] = { ...spec, type: inferred };
@@ -5131,8 +5828,11 @@ function normalizeAnnotations(annotations) {
5131
5828
  }
5132
5829
  function normalizeChartSpec(spec, warnings) {
5133
5830
  const encoding = inferEncodingTypes(spec.encoding, spec.data, warnings);
5831
+ const markType = resolveMarkType(spec.mark);
5832
+ const markDef = resolveMarkDef(spec.mark);
5134
5833
  return {
5135
- type: spec.type,
5834
+ markType,
5835
+ markDef,
5136
5836
  data: spec.data,
5137
5837
  encoding,
5138
5838
  chrome: normalizeChrome(spec.chrome),
@@ -5190,6 +5890,13 @@ function normalizeGraphSpec(spec, _warnings) {
5190
5890
  };
5191
5891
  }
5192
5892
  function normalizeSpec(spec, warnings = []) {
5893
+ if (isLayerSpec(spec)) {
5894
+ const leaves = flattenLayers(spec);
5895
+ if (leaves.length === 0) {
5896
+ throw new Error("LayerSpec has no leaf chart specs after flattening");
5897
+ }
5898
+ return normalizeChartSpec(leaves[0], warnings);
5899
+ }
5193
5900
  if (isChartSpec(spec)) {
5194
5901
  return normalizeChartSpec(spec, warnings);
5195
5902
  }
@@ -5199,13 +5906,37 @@ function normalizeSpec(spec, warnings = []) {
5199
5906
  if (isGraphSpec(spec)) {
5200
5907
  return normalizeGraphSpec(spec, warnings);
5201
5908
  }
5202
- throw new Error(`Unknown spec type: ${spec.type}`);
5909
+ throw new Error(
5910
+ `Unknown spec shape. Expected mark (chart), layer, type: 'table', or type: 'graph'.`
5911
+ );
5912
+ }
5913
+ function flattenLayers(spec, parentData, parentEncoding, parentTransforms) {
5914
+ const resolvedData = spec.data ?? parentData;
5915
+ const resolvedEncoding = parentEncoding && spec.encoding ? { ...parentEncoding, ...spec.encoding } : spec.encoding ?? parentEncoding;
5916
+ const resolvedTransforms = [...parentTransforms ?? [], ...spec.transform ?? []];
5917
+ const leaves = [];
5918
+ for (const child of spec.layer) {
5919
+ if (isLayerSpec(child)) {
5920
+ leaves.push(...flattenLayers(child, resolvedData, resolvedEncoding, resolvedTransforms));
5921
+ } else {
5922
+ const mergedData = child.data ?? resolvedData ?? [];
5923
+ const mergedEncoding = resolvedEncoding ? { ...resolvedEncoding, ...child.encoding } : child.encoding;
5924
+ const mergedTransforms = [...resolvedTransforms, ...child.transform ?? []];
5925
+ leaves.push({
5926
+ ...child,
5927
+ data: mergedData,
5928
+ encoding: mergedEncoding,
5929
+ transform: mergedTransforms.length > 0 ? mergedTransforms : void 0
5930
+ });
5931
+ }
5932
+ }
5933
+ return leaves;
5203
5934
  }
5204
5935
 
5205
5936
  // src/compiler/validate.ts
5206
5937
  import {
5207
- CHART_ENCODING_RULES,
5208
- CHART_TYPES
5938
+ MARK_ENCODING_RULES,
5939
+ MARK_TYPES
5209
5940
  } from "@opendata-ai/openchart-core";
5210
5941
  var VALID_FIELD_TYPES = /* @__PURE__ */ new Set(["quantitative", "temporal", "nominal", "ordinal"]);
5211
5942
  var VALID_DARK_MODES = /* @__PURE__ */ new Set(["auto", "force", "off"]);
@@ -5227,7 +5958,7 @@ function isNumeric(value) {
5227
5958
  return false;
5228
5959
  }
5229
5960
  function validateChartSpec(spec, errors) {
5230
- const chartType = spec.type;
5961
+ const markType = typeof spec.mark === "string" ? spec.mark : spec.mark?.type;
5231
5962
  if (!Array.isArray(spec.data)) {
5232
5963
  errors.push({
5233
5964
  message: 'Spec error: "data" must be an array',
@@ -5257,17 +5988,17 @@ function validateChartSpec(spec, errors) {
5257
5988
  return;
5258
5989
  }
5259
5990
  if (!spec.encoding || typeof spec.encoding !== "object") {
5260
- const rules2 = CHART_ENCODING_RULES[chartType];
5991
+ const rules2 = MARK_ENCODING_RULES[markType];
5261
5992
  const requiredChannels = Object.entries(rules2).filter(([, rule]) => rule.required).map(([ch]) => ch);
5262
5993
  errors.push({
5263
- message: `Spec error: ${chartType} chart requires an "encoding" object`,
5994
+ message: `Spec error: ${markType} chart requires an "encoding" object`,
5264
5995
  path: "encoding",
5265
5996
  code: "MISSING_FIELD",
5266
5997
  suggestion: `Add an encoding object with required channels: ${requiredChannels.join(", ")}. Example: encoding: { ${requiredChannels.map((ch) => `${ch}: { field: "...", type: "..." }`).join(", ")} }`
5267
5998
  });
5268
5999
  return;
5269
6000
  }
5270
- const rules = CHART_ENCODING_RULES[chartType];
6001
+ const rules = MARK_ENCODING_RULES[markType];
5271
6002
  const encoding = spec.encoding;
5272
6003
  const dataColumns = new Set(Object.keys(firstRow));
5273
6004
  const availableColumns = [...dataColumns].join(", ");
@@ -5275,17 +6006,29 @@ function validateChartSpec(spec, errors) {
5275
6006
  if (rule.required && !encoding[channel]) {
5276
6007
  const allowedTypes = rule.allowedTypes.join(" or ");
5277
6008
  errors.push({
5278
- message: `Spec error: ${chartType} chart requires encoding.${channel} but none was provided`,
6009
+ message: `Spec error: ${markType} chart requires encoding.${channel} but none was provided`,
5279
6010
  path: `encoding.${channel}`,
5280
6011
  code: "MISSING_FIELD",
5281
6012
  suggestion: `Add encoding.${channel} with a field from your data (${availableColumns}) and type (${allowedTypes}). Example: ${channel}: { field: "${[...dataColumns][0] ?? "myField"}", type: "${rule.allowedTypes[0]}" }`
5282
6013
  });
5283
6014
  }
5284
6015
  }
6016
+ const transformFields = /* @__PURE__ */ new Set();
6017
+ if (Array.isArray(spec.transform)) {
6018
+ for (const t of spec.transform) {
6019
+ if (typeof t.as === "string") transformFields.add(t.as);
6020
+ if (Array.isArray(t.as)) {
6021
+ for (const f of t.as) {
6022
+ if (typeof f === "string") transformFields.add(f);
6023
+ }
6024
+ }
6025
+ }
6026
+ }
5285
6027
  for (const [channel, channelSpec] of Object.entries(encoding)) {
5286
6028
  if (!channelSpec || typeof channelSpec !== "object") continue;
5287
6029
  const channelObj = channelSpec;
5288
6030
  const channelRule = rules[channel];
6031
+ if ("condition" in channelObj) continue;
5289
6032
  if (!channelObj.field || typeof channelObj.field !== "string") {
5290
6033
  errors.push({
5291
6034
  message: `Spec error: encoding.${channel} must have a "field" string`,
@@ -5295,7 +6038,7 @@ function validateChartSpec(spec, errors) {
5295
6038
  });
5296
6039
  continue;
5297
6040
  }
5298
- if (!dataColumns.has(channelObj.field)) {
6041
+ if (!dataColumns.has(channelObj.field) && !transformFields.has(channelObj.field)) {
5299
6042
  errors.push({
5300
6043
  message: `Spec error: encoding.${channel}.field "${channelObj.field}" does not exist in data. Available columns: ${availableColumns}`,
5301
6044
  path: `encoding.${channel}.field`,
@@ -5314,7 +6057,7 @@ function validateChartSpec(spec, errors) {
5314
6057
  if (channelRule && channelObj.type && channelRule.allowedTypes.length > 0) {
5315
6058
  if (!channelRule.allowedTypes.includes(channelObj.type)) {
5316
6059
  errors.push({
5317
- message: `Spec error: encoding.${channel} for ${chartType} chart does not accept type "${channelObj.type}". Allowed types: ${channelRule.allowedTypes.join(", ")}`,
6060
+ message: `Spec error: encoding.${channel} for ${markType} chart does not accept type "${channelObj.type}". Allowed types: ${channelRule.allowedTypes.join(", ")}`,
5318
6061
  path: `encoding.${channel}.type`,
5319
6062
  code: "ENCODING_MISMATCH",
5320
6063
  suggestion: `Change encoding.${channel}.type to one of: ${channelRule.allowedTypes.join(", ")}`
@@ -5592,6 +6335,79 @@ function validateGraphSpec(spec, errors) {
5592
6335
  }
5593
6336
  }
5594
6337
  }
6338
+ function validateLayerSpec(spec, errors) {
6339
+ const layer = spec.layer;
6340
+ if (layer.length === 0) {
6341
+ errors.push({
6342
+ message: 'Spec error: "layer" must be a non-empty array',
6343
+ path: "layer",
6344
+ code: "EMPTY_DATA",
6345
+ suggestion: "Add at least one layer with a mark and encoding"
6346
+ });
6347
+ return;
6348
+ }
6349
+ for (let i = 0; i < layer.length; i++) {
6350
+ const child = layer[i];
6351
+ if (!child || typeof child !== "object" || Array.isArray(child)) {
6352
+ errors.push({
6353
+ message: `Spec error: layer[${i}] must be an object`,
6354
+ path: `layer[${i}]`,
6355
+ code: "INVALID_TYPE",
6356
+ suggestion: "Each layer must be a chart spec (with mark) or a nested layer spec (with layer)"
6357
+ });
6358
+ continue;
6359
+ }
6360
+ const childObj = child;
6361
+ const isNestedLayer = "layer" in childObj && Array.isArray(childObj.layer);
6362
+ const isChildChart = "mark" in childObj;
6363
+ if (!isNestedLayer && !isChildChart) {
6364
+ errors.push({
6365
+ message: `Spec error: layer[${i}] must have a "mark" field or a "layer" array`,
6366
+ path: `layer[${i}]`,
6367
+ code: "MISSING_FIELD",
6368
+ suggestion: "Each layer must be a chart spec (with mark + encoding) or a nested layer spec (with layer array)"
6369
+ });
6370
+ continue;
6371
+ }
6372
+ if (isNestedLayer) {
6373
+ validateLayerSpec(childObj, errors);
6374
+ } else if (isChildChart) {
6375
+ const mark = childObj.mark;
6376
+ let markValue;
6377
+ if (typeof mark === "string") {
6378
+ markValue = mark;
6379
+ } else if (mark && typeof mark === "object" && !Array.isArray(mark)) {
6380
+ markValue = mark.type;
6381
+ }
6382
+ if (!markValue || !MARK_TYPES.has(markValue)) {
6383
+ errors.push({
6384
+ message: `Spec error: layer[${i}].mark "${markValue ?? String(mark)}" is not a valid mark type`,
6385
+ path: `layer[${i}].mark`,
6386
+ code: "INVALID_VALUE",
6387
+ suggestion: `Change mark to one of: ${[...MARK_TYPES].join(", ")}`
6388
+ });
6389
+ continue;
6390
+ }
6391
+ const hasOwnData = Array.isArray(childObj.data) && childObj.data.length > 0;
6392
+ const parentHasData = Array.isArray(spec.data) && spec.data.length > 0;
6393
+ if (hasOwnData || parentHasData) {
6394
+ const mergedForValidation = { ...childObj };
6395
+ if (!hasOwnData && parentHasData) {
6396
+ mergedForValidation.data = spec.data;
6397
+ }
6398
+ if (spec.encoding && typeof spec.encoding === "object") {
6399
+ mergedForValidation.encoding = {
6400
+ ...spec.encoding,
6401
+ ...childObj.encoding ?? {}
6402
+ };
6403
+ }
6404
+ if (mergedForValidation.data && mergedForValidation.encoding) {
6405
+ validateChartSpec(mergedForValidation, errors);
6406
+ }
6407
+ }
6408
+ }
6409
+ }
6410
+ }
5595
6411
  function validateSpec(spec) {
5596
6412
  const errors = [];
5597
6413
  if (!spec || typeof spec !== "object" || Array.isArray(spec)) {
@@ -5601,45 +6417,58 @@ function validateSpec(spec) {
5601
6417
  {
5602
6418
  message: "Spec error: spec must be a non-null object",
5603
6419
  code: "INVALID_TYPE",
5604
- suggestion: 'Pass a spec object with at least a "type" field, e.g. { type: "line", data: [...], encoding: {...} }'
6420
+ suggestion: 'Pass a spec object with at least a "mark" field for charts, e.g. { mark: "line", data: [...], encoding: {...} }'
5605
6421
  }
5606
6422
  ],
5607
6423
  normalized: null
5608
6424
  };
5609
6425
  }
5610
6426
  const obj = spec;
5611
- if (!obj.type || typeof obj.type !== "string") {
5612
- return {
5613
- valid: false,
5614
- errors: [
5615
- {
5616
- message: 'Spec error: spec must have a "type" field',
5617
- path: "type",
5618
- code: "MISSING_FIELD",
5619
- suggestion: `Add a type field. Valid types: ${[...CHART_TYPES].join(", ")}, table, graph`
5620
- }
5621
- ],
5622
- normalized: null
5623
- };
5624
- }
5625
- const isChart = CHART_TYPES.has(obj.type);
6427
+ const hasLayer = "layer" in obj && Array.isArray(obj.layer);
6428
+ const hasMark = "mark" in obj;
5626
6429
  const isTable = obj.type === "table";
5627
6430
  const isGraph = obj.type === "graph";
5628
- if (!isChart && !isTable && !isGraph) {
6431
+ const isLayer = hasLayer && !isTable && !isGraph;
6432
+ const isChart = hasMark && !hasLayer && !isTable && !isGraph;
6433
+ if (!isChart && !isTable && !isGraph && !isLayer) {
5629
6434
  return {
5630
6435
  valid: false,
5631
6436
  errors: [
5632
6437
  {
5633
- message: `Spec error: "${obj.type}" is not a valid type. Valid types: ${[...CHART_TYPES].join(", ")}, table, graph`,
5634
- path: "type",
5635
- code: "INVALID_VALUE",
5636
- suggestion: `Change type to one of: ${[...CHART_TYPES].join(", ")}, table, graph`
6438
+ message: 'Spec error: spec must have a "mark" field for charts, a "layer" array for layered charts, or a "type" field for tables/graphs',
6439
+ path: "mark",
6440
+ code: "MISSING_FIELD",
6441
+ suggestion: `Add a "mark" field for charts (e.g. mark: "bar"), a "layer" array for layered charts, or a "type" field for tables/graphs (type: "table" or type: "graph"). Valid mark types: ${[...MARK_TYPES].join(", ")}`
5637
6442
  }
5638
6443
  ],
5639
6444
  normalized: null
5640
6445
  };
5641
6446
  }
6447
+ if (isLayer) {
6448
+ validateLayerSpec(obj, errors);
6449
+ }
5642
6450
  if (isChart) {
6451
+ const mark = obj.mark;
6452
+ let markValue;
6453
+ if (typeof mark === "string") {
6454
+ markValue = mark;
6455
+ } else if (mark && typeof mark === "object" && !Array.isArray(mark)) {
6456
+ markValue = mark.type;
6457
+ }
6458
+ if (!markValue || !MARK_TYPES.has(markValue)) {
6459
+ return {
6460
+ valid: false,
6461
+ errors: [
6462
+ {
6463
+ message: `Spec error: "${markValue ?? String(mark)}" is not a valid mark type. Valid mark types: ${[...MARK_TYPES].join(", ")}`,
6464
+ path: "mark",
6465
+ code: "INVALID_VALUE",
6466
+ suggestion: `Change mark to one of: ${[...MARK_TYPES].join(", ")}`
6467
+ }
6468
+ ],
6469
+ normalized: null
6470
+ };
6471
+ }
5643
6472
  validateChartSpec(obj, errors);
5644
6473
  } else if (isTable) {
5645
6474
  validateTableSpec(obj, errors);
@@ -5956,9 +6785,9 @@ function buildGraphTooltips(nodes) {
5956
6785
  }
5957
6786
  function compileGraph(spec, options) {
5958
6787
  const { spec: normalized } = compile(spec);
5959
- if (normalized.type !== "graph") {
6788
+ if (!("type" in normalized) || normalized.type !== "graph") {
5960
6789
  throw new Error(
5961
- `compileGraph received a ${normalized.type} spec. Use compileChart or compileTable instead.`
6790
+ "compileGraph received a non-graph spec. Use compileChart or compileTable instead."
5962
6791
  );
5963
6792
  }
5964
6793
  const graphSpec = normalized;
@@ -6102,6 +6931,14 @@ function thinTicksUntilFit(ticks2, fontSize, fontWeight, measureText) {
6102
6931
  }
6103
6932
  function continuousTicks(resolvedScale, density) {
6104
6933
  const scale = resolvedScale.scale;
6934
+ if (!("ticks" in scale) || typeof scale.ticks !== "function") {
6935
+ const domain = scale.domain();
6936
+ return domain.map((value) => ({
6937
+ value,
6938
+ position: scale(value),
6939
+ label: formatTickLabel(value, resolvedScale)
6940
+ }));
6941
+ }
6105
6942
  const explicitCount = resolvedScale.channel.axis?.tickCount;
6106
6943
  const count = explicitCount ?? TICK_COUNTS[density];
6107
6944
  const rawTicks = scale.ticks(count);
@@ -6133,13 +6970,24 @@ function categoricalTicks(resolvedScale, density) {
6133
6970
  });
6134
6971
  return ticks2;
6135
6972
  }
6973
+ var NUMERIC_SCALE_TYPES = /* @__PURE__ */ new Set([
6974
+ "linear",
6975
+ "log",
6976
+ "pow",
6977
+ "sqrt",
6978
+ "symlog",
6979
+ "quantile",
6980
+ "quantize",
6981
+ "threshold"
6982
+ ]);
6983
+ var TEMPORAL_SCALE_TYPES = /* @__PURE__ */ new Set(["time", "utc"]);
6136
6984
  function formatTickLabel(value, resolvedScale) {
6137
6985
  const formatStr = resolvedScale.channel.axis?.format;
6138
- if (resolvedScale.type === "time") {
6986
+ if (TEMPORAL_SCALE_TYPES.has(resolvedScale.type)) {
6139
6987
  if (formatStr) return String(value);
6140
6988
  return formatDate(value);
6141
6989
  }
6142
- if (resolvedScale.type === "linear" || resolvedScale.type === "log") {
6990
+ if (NUMERIC_SCALE_TYPES.has(resolvedScale.type)) {
6143
6991
  const num = value;
6144
6992
  if (formatStr) {
6145
6993
  const fmt = buildD3Formatter3(formatStr);
@@ -6150,6 +6998,27 @@ function formatTickLabel(value, resolvedScale) {
6150
6998
  }
6151
6999
  return String(value);
6152
7000
  }
7001
+ function resolveExplicitTicks(values, resolvedScale) {
7002
+ const scale = resolvedScale.scale;
7003
+ return values.map((value) => {
7004
+ let position;
7005
+ if (TEMPORAL_SCALE_TYPES.has(resolvedScale.type)) {
7006
+ const d = value instanceof Date ? value : new Date(String(value));
7007
+ position = scale(d);
7008
+ } else if (resolvedScale.type === "band" || resolvedScale.type === "point" || resolvedScale.type === "ordinal") {
7009
+ const s = String(value);
7010
+ const bandScale = resolvedScale.type === "band" ? scale : null;
7011
+ position = bandScale ? (bandScale(s) ?? 0) + bandScale.bandwidth() / 2 : scale(s) ?? 0;
7012
+ } else {
7013
+ position = scale(value);
7014
+ }
7015
+ return {
7016
+ value,
7017
+ position,
7018
+ label: formatTickLabel(value, resolvedScale)
7019
+ };
7020
+ });
7021
+ }
6153
7022
  function computeAxes(scales, chartArea, strategy, theme, measureText) {
6154
7023
  const result = {};
6155
7024
  const baseDensity = strategy.axisLabelDensity;
@@ -6183,14 +7052,22 @@ function computeAxes(scales, chartArea, strategy, theme, measureText) {
6183
7052
  const { fontSize } = tickLabelStyle;
6184
7053
  const { fontWeight } = tickLabelStyle;
6185
7054
  if (scales.x) {
6186
- const allTicks = scales.x.type === "band" || scales.x.type === "point" || scales.x.type === "ordinal" ? categoricalTicks(scales.x, xDensity) : continuousTicks(scales.x, xDensity);
7055
+ const axisConfig = scales.x.channel.axis;
7056
+ let allTicks;
7057
+ if (axisConfig?.values) {
7058
+ allTicks = resolveExplicitTicks(axisConfig.values, scales.x);
7059
+ } else if (scales.x.type === "band" || scales.x.type === "point" || scales.x.type === "ordinal") {
7060
+ allTicks = categoricalTicks(scales.x, xDensity);
7061
+ } else {
7062
+ allTicks = continuousTicks(scales.x, xDensity);
7063
+ }
6187
7064
  const gridlines = allTicks.map((t) => ({
6188
7065
  position: t.position,
6189
7066
  major: true
6190
7067
  }));
6191
- const shouldThin = scales.x.type !== "band" && !scales.x.channel.axis?.tickCount;
7068
+ const shouldThin = scales.x.type !== "band" && !axisConfig?.tickCount && !axisConfig?.values;
6192
7069
  const ticks2 = shouldThin ? thinTicksUntilFit(allTicks, fontSize, fontWeight, measureText) : allTicks;
6193
- let tickAngle = scales.x.channel.axis?.tickAngle;
7070
+ let tickAngle = axisConfig?.labelAngle ?? axisConfig?.tickAngle;
6194
7071
  if (tickAngle === void 0 && scales.x.type === "band" && ticks2.length > 1) {
6195
7072
  const bandwidth = scales.x.scale.bandwidth();
6196
7073
  let maxLabelWidth = 0;
@@ -6202,35 +7079,62 @@ function computeAxes(scales, chartArea, strategy, theme, measureText) {
6202
7079
  tickAngle = -45;
6203
7080
  }
6204
7081
  }
7082
+ const axisTitle = axisConfig?.title ?? axisConfig?.label;
6205
7083
  result.x = {
6206
7084
  ticks: ticks2,
6207
- gridlines: scales.x.channel.axis?.grid ? gridlines : [],
6208
- label: scales.x.channel.axis?.label,
7085
+ gridlines: axisConfig?.grid ? gridlines : [],
7086
+ label: axisTitle,
6209
7087
  labelStyle: axisLabelStyle,
6210
7088
  tickLabelStyle,
6211
7089
  tickAngle,
6212
7090
  start: { x: chartArea.x, y: chartArea.y + chartArea.height },
6213
- end: { x: chartArea.x + chartArea.width, y: chartArea.y + chartArea.height }
7091
+ end: { x: chartArea.x + chartArea.width, y: chartArea.y + chartArea.height },
7092
+ orient: axisConfig?.orient,
7093
+ domainLine: axisConfig?.domain,
7094
+ tickMarks: axisConfig?.ticks,
7095
+ offset: axisConfig?.offset,
7096
+ titlePadding: axisConfig?.titlePadding,
7097
+ labelPadding: axisConfig?.labelPadding,
7098
+ labelOverlap: axisConfig?.labelOverlap,
7099
+ labelFlush: axisConfig?.labelFlush
6214
7100
  };
6215
7101
  }
6216
7102
  if (scales.y) {
6217
- const allTicks = scales.y.type === "band" || scales.y.type === "point" || scales.y.type === "ordinal" ? categoricalTicks(scales.y, yDensity) : continuousTicks(scales.y, yDensity);
7103
+ const axisConfig = scales.y.channel.axis;
7104
+ let allTicks;
7105
+ if (axisConfig?.values) {
7106
+ allTicks = resolveExplicitTicks(axisConfig.values, scales.y);
7107
+ } else if (scales.y.type === "band" || scales.y.type === "point" || scales.y.type === "ordinal") {
7108
+ allTicks = categoricalTicks(scales.y, yDensity);
7109
+ } else {
7110
+ allTicks = continuousTicks(scales.y, yDensity);
7111
+ }
6218
7112
  const gridlines = allTicks.map((t) => ({
6219
7113
  position: t.position,
6220
7114
  major: true
6221
7115
  }));
6222
- const shouldThin = scales.y.type !== "band" && !scales.y.channel.axis?.tickCount;
7116
+ const shouldThin = scales.y.type !== "band" && !axisConfig?.tickCount && !axisConfig?.values;
6223
7117
  const ticks2 = shouldThin ? thinTicksUntilFit(allTicks, fontSize, fontWeight, measureText) : allTicks;
7118
+ const axisTitle = axisConfig?.title ?? axisConfig?.label;
7119
+ const tickAngle = axisConfig?.labelAngle ?? axisConfig?.tickAngle;
6224
7120
  result.y = {
6225
7121
  ticks: ticks2,
6226
7122
  // Y-axis gridlines are shown by default (standard editorial practice)
6227
7123
  gridlines,
6228
- label: scales.y.channel.axis?.label,
7124
+ label: axisTitle,
6229
7125
  labelStyle: axisLabelStyle,
6230
7126
  tickLabelStyle,
6231
- tickAngle: scales.y.channel.axis?.tickAngle,
7127
+ tickAngle,
6232
7128
  start: { x: chartArea.x, y: chartArea.y },
6233
- end: { x: chartArea.x, y: chartArea.y + chartArea.height }
7129
+ end: { x: chartArea.x, y: chartArea.y + chartArea.height },
7130
+ orient: axisConfig?.orient,
7131
+ domainLine: axisConfig?.domain,
7132
+ tickMarks: axisConfig?.ticks,
7133
+ offset: axisConfig?.offset,
7134
+ titlePadding: axisConfig?.titlePadding,
7135
+ labelPadding: axisConfig?.labelPadding,
7136
+ labelOverlap: axisConfig?.labelOverlap,
7137
+ labelFlush: axisConfig?.labelFlush
6234
7138
  };
6235
7139
  }
6236
7140
  return result;
@@ -6270,7 +7174,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy) {
6270
7174
  padding
6271
7175
  );
6272
7176
  const total = { x: 0, y: 0, width, height };
6273
- const isRadial = spec.type === "pie" || spec.type === "donut";
7177
+ const isRadial = spec.markType === "arc";
6274
7178
  const encoding = spec.encoding;
6275
7179
  const xAxis = encoding.x?.axis;
6276
7180
  const hasXAxisLabel = !!xAxis?.label;
@@ -6295,15 +7199,17 @@ function computeDimensions(spec, options, legendLayout, theme, strategy) {
6295
7199
  } else {
6296
7200
  xAxisHeight = hasXAxisLabel ? 48 : 26;
6297
7201
  }
7202
+ const topAxisGap = isRadial && chrome.topHeight === 0 ? 0 : axisMargin;
6298
7203
  const margins = {
6299
- top: padding + chrome.topHeight + axisMargin,
7204
+ top: padding + chrome.topHeight + topAxisGap,
6300
7205
  right: padding + (isRadial ? padding : axisMargin),
6301
7206
  bottom: padding + chrome.bottomHeight + xAxisHeight,
6302
7207
  left: padding + (isRadial ? padding : axisMargin)
6303
7208
  };
6304
7209
  const labelDensity = spec.labels.density;
6305
- if ((spec.type === "line" || spec.type === "area") && labelDensity !== "none") {
6306
- const colorField = encoding.color?.field;
7210
+ if ((spec.markType === "line" || spec.markType === "area") && labelDensity !== "none") {
7211
+ const colorEnc = encoding.color;
7212
+ const colorField = colorEnc && "field" in colorEnc ? colorEnc.field : void 0;
6307
7213
  if (colorField) {
6308
7214
  let maxLabelWidth = 0;
6309
7215
  const seen = /* @__PURE__ */ new Set();
@@ -6321,7 +7227,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy) {
6321
7227
  }
6322
7228
  }
6323
7229
  if (encoding.y && !isRadial) {
6324
- if (spec.type === "bar" || spec.type === "dot" || encoding.y.type === "nominal" || encoding.y.type === "ordinal") {
7230
+ if (spec.markType === "bar" || spec.markType === "circle" || encoding.y.type === "nominal" || encoding.y.type === "ordinal") {
6325
7231
  const yField = encoding.y.field;
6326
7232
  let maxLabelWidth = 0;
6327
7233
  for (const row of spec.data) {
@@ -6385,7 +7291,8 @@ function computeDimensions(spec, options, legendLayout, theme, strategy) {
6385
7291
  fallbackMode,
6386
7292
  padding
6387
7293
  );
6388
- const newTop = padding + fallbackChrome.topHeight + axisMargin;
7294
+ const fallbackTopAxisGap = isRadial && fallbackChrome.topHeight === 0 ? 0 : axisMargin;
7295
+ const newTop = padding + fallbackChrome.topHeight + fallbackTopAxisGap;
6389
7296
  const topDelta = margins.top - newTop;
6390
7297
  const newBottom = padding + fallbackChrome.bottomHeight + xAxisHeight;
6391
7298
  const bottomDelta = margins.bottom - newBottom;
@@ -6455,6 +7362,15 @@ function uniqueStrings(values) {
6455
7362
  }
6456
7363
  return result;
6457
7364
  }
7365
+ function applyContinuousConfig(scale, channel) {
7366
+ if (channel.scale?.clamp) {
7367
+ scale.clamp(true);
7368
+ }
7369
+ if (channel.scale?.reverse) {
7370
+ const [r0, r1] = scale.range();
7371
+ scale.range([r1, r0]);
7372
+ }
7373
+ }
6458
7374
  function buildTimeScale(channel, data, rangeStart, rangeEnd) {
6459
7375
  const values = parseDates(fieldValues(data, channel.field));
6460
7376
  const domain = channel.scale?.domain ? [new Date(channel.scale.domain[0]), new Date(channel.scale.domain[1])] : extent(values);
@@ -6462,8 +7378,19 @@ function buildTimeScale(channel, data, rangeStart, rangeEnd) {
6462
7378
  if (channel.scale?.nice !== false) {
6463
7379
  scale.nice();
6464
7380
  }
7381
+ applyContinuousConfig(scale, channel);
6465
7382
  return { scale, type: "time", channel };
6466
7383
  }
7384
+ function buildUtcScale(channel, data, rangeStart, rangeEnd) {
7385
+ const values = parseDates(fieldValues(data, channel.field));
7386
+ const domain = channel.scale?.domain ? [new Date(channel.scale.domain[0]), new Date(channel.scale.domain[1])] : extent(values);
7387
+ const scale = utcTime().domain(domain).range([rangeStart, rangeEnd]);
7388
+ if (channel.scale?.nice !== false) {
7389
+ scale.nice();
7390
+ }
7391
+ applyContinuousConfig(scale, channel);
7392
+ return { scale, type: "utc", channel };
7393
+ }
6467
7394
  function buildLinearScale(channel, data, rangeStart, rangeEnd) {
6468
7395
  const values = parseNumbers(fieldValues(data, channel.field));
6469
7396
  let domainMin;
@@ -6484,23 +7411,141 @@ function buildLinearScale(channel, data, rangeStart, rangeEnd) {
6484
7411
  if (channel.scale?.nice !== false) {
6485
7412
  scale.nice();
6486
7413
  }
7414
+ applyContinuousConfig(scale, channel);
6487
7415
  return { scale, type: "linear", channel };
6488
7416
  }
6489
7417
  function buildLogScale(channel, data, rangeStart, rangeEnd) {
6490
7418
  const values = parseNumbers(fieldValues(data, channel.field)).filter((v) => v > 0);
6491
- const domainMin = min2(values) ?? 1;
6492
- const domainMax = max2(values) ?? 10;
6493
- const scale = log().domain([domainMin, domainMax]).range([rangeStart, rangeEnd]).nice();
7419
+ const domainMin = channel.scale?.domain ? channel.scale.domain[0] : min2(values) ?? 1;
7420
+ const domainMax = channel.scale?.domain ? channel.scale.domain[1] : max2(values) ?? 10;
7421
+ const scale = log().domain([domainMin, domainMax]).range([rangeStart, rangeEnd]);
7422
+ if (channel.scale?.base !== void 0) {
7423
+ scale.base(channel.scale.base);
7424
+ }
7425
+ if (channel.scale?.nice !== false) {
7426
+ scale.nice();
7427
+ }
7428
+ applyContinuousConfig(scale, channel);
6494
7429
  return { scale, type: "log", channel };
6495
7430
  }
7431
+ function buildPowScale(channel, data, rangeStart, rangeEnd) {
7432
+ const values = parseNumbers(fieldValues(data, channel.field));
7433
+ let domainMin;
7434
+ let domainMax;
7435
+ if (channel.scale?.domain) {
7436
+ [domainMin, domainMax] = channel.scale.domain;
7437
+ } else {
7438
+ domainMin = min2(values) ?? 0;
7439
+ domainMax = max2(values) ?? 1;
7440
+ if (channel.scale?.zero !== false) {
7441
+ domainMin = Math.min(0, domainMin);
7442
+ domainMax = Math.max(0, domainMax);
7443
+ }
7444
+ }
7445
+ const scale = pow().domain([domainMin, domainMax]).range([rangeStart, rangeEnd]);
7446
+ if (channel.scale?.exponent !== void 0) {
7447
+ scale.exponent(channel.scale.exponent);
7448
+ }
7449
+ if (channel.scale?.nice !== false) {
7450
+ scale.nice();
7451
+ }
7452
+ applyContinuousConfig(scale, channel);
7453
+ return { scale, type: "pow", channel };
7454
+ }
7455
+ function buildSqrtScale(channel, data, rangeStart, rangeEnd) {
7456
+ const values = parseNumbers(fieldValues(data, channel.field));
7457
+ let domainMin;
7458
+ let domainMax;
7459
+ if (channel.scale?.domain) {
7460
+ [domainMin, domainMax] = channel.scale.domain;
7461
+ } else {
7462
+ domainMin = min2(values) ?? 0;
7463
+ domainMax = max2(values) ?? 1;
7464
+ if (channel.scale?.zero !== false) {
7465
+ domainMin = Math.min(0, domainMin);
7466
+ domainMax = Math.max(0, domainMax);
7467
+ }
7468
+ }
7469
+ const scale = sqrt2().domain([domainMin, domainMax]).range([rangeStart, rangeEnd]);
7470
+ if (channel.scale?.nice !== false) {
7471
+ scale.nice();
7472
+ }
7473
+ applyContinuousConfig(scale, channel);
7474
+ return { scale, type: "sqrt", channel };
7475
+ }
7476
+ function buildSymlogScale(channel, data, rangeStart, rangeEnd) {
7477
+ const values = parseNumbers(fieldValues(data, channel.field));
7478
+ let domainMin;
7479
+ let domainMax;
7480
+ if (channel.scale?.domain) {
7481
+ [domainMin, domainMax] = channel.scale.domain;
7482
+ } else {
7483
+ domainMin = min2(values) ?? 0;
7484
+ domainMax = max2(values) ?? 1;
7485
+ if (channel.scale?.zero !== false) {
7486
+ domainMin = Math.min(0, domainMin);
7487
+ domainMax = Math.max(0, domainMax);
7488
+ }
7489
+ }
7490
+ const scale = symlog().domain([domainMin, domainMax]).range([rangeStart, rangeEnd]);
7491
+ if (channel.scale?.constant !== void 0) {
7492
+ scale.constant(channel.scale.constant);
7493
+ }
7494
+ if (channel.scale?.nice !== false) {
7495
+ scale.nice();
7496
+ }
7497
+ applyContinuousConfig(scale, channel);
7498
+ return { scale, type: "symlog", channel };
7499
+ }
7500
+ function buildQuantileScale(channel, data, rangeStart, rangeEnd) {
7501
+ const values = parseNumbers(fieldValues(data, channel.field));
7502
+ const range2 = channel.scale?.range ? channel.scale.range : evenRange(rangeStart, rangeEnd, 4);
7503
+ const scale = quantile2().domain(values).range(range2);
7504
+ return { scale, type: "quantile", channel };
7505
+ }
7506
+ function buildQuantizeScale(channel, data, rangeStart, rangeEnd) {
7507
+ const values = parseNumbers(fieldValues(data, channel.field));
7508
+ const domainMin = channel.scale?.domain ? channel.scale.domain[0] : min2(values) ?? 0;
7509
+ const domainMax = channel.scale?.domain ? channel.scale.domain[1] : max2(values) ?? 1;
7510
+ const range2 = channel.scale?.range ? channel.scale.range : evenRange(rangeStart, rangeEnd, 4);
7511
+ const scale = quantize().domain([domainMin, domainMax]).range(range2);
7512
+ return { scale, type: "quantize", channel };
7513
+ }
7514
+ function buildThresholdScale(channel, _data, rangeStart, rangeEnd) {
7515
+ const domainBreaks = channel.scale?.domain ? channel.scale.domain : [0.5];
7516
+ const range2 = channel.scale?.range ? channel.scale.range : evenRange(rangeStart, rangeEnd, domainBreaks.length + 1);
7517
+ const scale = threshold().domain(domainBreaks).range(range2);
7518
+ return { scale, type: "threshold", channel };
7519
+ }
7520
+ function evenRange(start, end, count) {
7521
+ if (count <= 1) return [start];
7522
+ const step = (end - start) / (count - 1);
7523
+ return Array.from({ length: count }, (_, i) => start + step * i);
7524
+ }
6496
7525
  function buildBandScale(channel, data, rangeStart, rangeEnd) {
6497
7526
  const values = channel.scale?.domain ? channel.scale.domain : uniqueStrings(fieldValues(data, channel.field));
6498
- const scale = band().domain(values).range([rangeStart, rangeEnd]).padding(0.35);
7527
+ const padding = channel.scale?.padding ?? 0.35;
7528
+ const scale = band().domain(values).range([rangeStart, rangeEnd]).padding(padding);
7529
+ if (channel.scale?.paddingInner !== void 0) {
7530
+ scale.paddingInner(channel.scale.paddingInner);
7531
+ }
7532
+ if (channel.scale?.paddingOuter !== void 0) {
7533
+ scale.paddingOuter(channel.scale.paddingOuter);
7534
+ }
7535
+ if (channel.scale?.reverse) {
7536
+ const [r0, r1] = scale.range();
7537
+ scale.range([r1, r0]);
7538
+ }
6499
7539
  return { scale, type: "band", channel };
6500
7540
  }
6501
7541
  function buildPointScale(channel, data, rangeStart, rangeEnd) {
6502
7542
  const values = channel.scale?.domain ? channel.scale.domain : uniqueStrings(fieldValues(data, channel.field));
6503
- const scale = point2().domain(values).range([rangeStart, rangeEnd]).padding(0.5);
7543
+ const padding = channel.scale?.padding ?? 0.5;
7544
+ const scale = point4().domain(values).range([rangeStart, rangeEnd]).padding(padding);
7545
+ if (channel.scale?.reverse) {
7546
+ const [r0, r1] = scale.range();
7547
+ scale.range([r1, r0]);
7548
+ }
6504
7549
  return { scale, type: "point", channel };
6505
7550
  }
6506
7551
  function buildOrdinalColorScale(channel, data, palette) {
@@ -6520,10 +7565,24 @@ function buildPositionalScale(channel, data, rangeStart, rangeEnd, chartType, ax
6520
7565
  switch (channel.scale.type) {
6521
7566
  case "time":
6522
7567
  return buildTimeScale(channel, data, rangeStart, rangeEnd);
7568
+ case "utc":
7569
+ return buildUtcScale(channel, data, rangeStart, rangeEnd);
6523
7570
  case "linear":
6524
7571
  return buildLinearScale(channel, data, rangeStart, rangeEnd);
6525
7572
  case "log":
6526
7573
  return buildLogScale(channel, data, rangeStart, rangeEnd);
7574
+ case "pow":
7575
+ return buildPowScale(channel, data, rangeStart, rangeEnd);
7576
+ case "sqrt":
7577
+ return buildSqrtScale(channel, data, rangeStart, rangeEnd);
7578
+ case "symlog":
7579
+ return buildSymlogScale(channel, data, rangeStart, rangeEnd);
7580
+ case "quantile":
7581
+ return buildQuantileScale(channel, data, rangeStart, rangeEnd);
7582
+ case "quantize":
7583
+ return buildQuantizeScale(channel, data, rangeStart, rangeEnd);
7584
+ case "threshold":
7585
+ return buildThresholdScale(channel, data, rangeStart, rangeEnd);
6527
7586
  case "band":
6528
7587
  return buildBandScale(channel, data, rangeStart, rangeEnd);
6529
7588
  case "point":
@@ -6539,7 +7598,7 @@ function buildPositionalScale(channel, data, rangeStart, rangeEnd, chartType, ax
6539
7598
  return buildLinearScale(channel, data, rangeStart, rangeEnd);
6540
7599
  case "nominal":
6541
7600
  case "ordinal":
6542
- if (chartType === "bar" && axis === "y" || chartType === "column" && axis === "x" || chartType === "dot" && axis === "y") {
7601
+ if (chartType === "bar" || chartType === "circle" && axis === "y") {
6543
7602
  return buildBandScale(channel, data, rangeStart, rangeEnd);
6544
7603
  }
6545
7604
  return buildPointScale(channel, data, rangeStart, rangeEnd);
@@ -6550,7 +7609,7 @@ function buildPositionalScale(channel, data, rangeStart, rangeEnd, chartType, ax
6550
7609
  function computeScales(spec, chartArea, data) {
6551
7610
  const result = {};
6552
7611
  const encoding = spec.encoding;
6553
- if (spec.type === "scatter") {
7612
+ if (spec.markType === "point") {
6554
7613
  if (encoding.x?.type === "quantitative" && encoding.x.scale?.zero === void 0) {
6555
7614
  if (!encoding.x.scale) {
6556
7615
  encoding.x.scale = { zero: false };
@@ -6568,7 +7627,7 @@ function computeScales(spec, chartArea, data) {
6568
7627
  }
6569
7628
  if (encoding.x) {
6570
7629
  let xData = data;
6571
- if (spec.type === "bar" && encoding.color && encoding.x.type === "quantitative") {
7630
+ if (spec.markType === "bar" && encoding.color && encoding.x.type === "quantitative") {
6572
7631
  const yField = encoding.y?.field;
6573
7632
  const xField = encoding.x.field;
6574
7633
  if (yField) {
@@ -6589,13 +7648,14 @@ function computeScales(spec, chartArea, data) {
6589
7648
  xData,
6590
7649
  chartArea.x,
6591
7650
  chartArea.x + chartArea.width,
6592
- spec.type,
7651
+ spec.markType,
6593
7652
  "x"
6594
7653
  );
6595
7654
  }
6596
7655
  if (encoding.y) {
6597
7656
  let yData = data;
6598
- if ((spec.type === "column" || spec.type === "area") && encoding.color && encoding.y.type === "quantitative") {
7657
+ const isVerticalBar = spec.markType === "bar" && (encoding.x?.type === "nominal" || encoding.x?.type === "ordinal") && encoding.y.type === "quantitative";
7658
+ if ((isVerticalBar || spec.markType === "area") && encoding.color && encoding.y.type === "quantitative") {
6599
7659
  const xField = encoding.x?.field;
6600
7660
  const yField = encoding.y.field;
6601
7661
  if (xField) {
@@ -6616,7 +7676,7 @@ function computeScales(spec, chartArea, data) {
6616
7676
  yData,
6617
7677
  chartArea.y + chartArea.height,
6618
7678
  chartArea.y,
6619
- spec.type,
7679
+ spec.markType,
6620
7680
  "y"
6621
7681
  );
6622
7682
  }
@@ -6633,10 +7693,12 @@ function computeScales(spec, chartArea, data) {
6633
7693
  "#a88f22",
6634
7694
  "#858078"
6635
7695
  ];
6636
- if (encoding.color.type === "quantitative") {
6637
- result.color = buildSequentialColorScale(encoding.color, data, defaultPalette);
6638
- } else {
6639
- result.color = buildOrdinalColorScale(encoding.color, data, defaultPalette);
7696
+ if ("field" in encoding.color) {
7697
+ if (encoding.color.type === "quantitative") {
7698
+ result.color = buildSequentialColorScale(encoding.color, data, defaultPalette);
7699
+ } else {
7700
+ result.color = buildOrdinalColorScale(encoding.color, data, defaultPalette);
7701
+ }
6640
7702
  }
6641
7703
  }
6642
7704
  return result;
@@ -6651,12 +7713,12 @@ var LEGEND_PADDING = 8;
6651
7713
  var LEGEND_RIGHT_WIDTH = 120;
6652
7714
  var RIGHT_LEGEND_MAX_HEIGHT_RATIO = 0.4;
6653
7715
  var TOP_LEGEND_MAX_ROWS = 2;
6654
- function swatchShapeForType(chartType) {
6655
- switch (chartType) {
7716
+ function swatchShapeForType(markType) {
7717
+ switch (markType) {
6656
7718
  case "line":
6657
7719
  return "line";
6658
- case "scatter":
6659
- case "dot":
7720
+ case "point":
7721
+ case "circle":
6660
7722
  return "circle";
6661
7723
  default:
6662
7724
  return "square";
@@ -6665,10 +7727,11 @@ function swatchShapeForType(chartType) {
6665
7727
  function extractColorEntries(spec, theme) {
6666
7728
  const colorEnc = spec.encoding.color;
6667
7729
  if (!colorEnc) return [];
7730
+ if ("condition" in colorEnc) return [];
6668
7731
  if (colorEnc.type === "quantitative") return [];
6669
7732
  const uniqueValues = [...new Set(spec.data.map((d) => String(d[colorEnc.field])))];
6670
7733
  const palette = theme.colors.categorical;
6671
- const shape = swatchShapeForType(spec.type);
7734
+ const shape = swatchShapeForType(spec.markType);
6672
7735
  return uniqueValues.map((value, i) => ({
6673
7736
  label: value,
6674
7737
  color: palette[i % palette.length],
@@ -7492,7 +8555,7 @@ function buildFields(row, encoding, color2) {
7492
8555
  value: formatValue(row[encoding.x.field], encoding.x.type, encoding.x.axis?.format)
7493
8556
  });
7494
8557
  }
7495
- if (encoding.size) {
8558
+ if (encoding.size && "field" in encoding.size) {
7496
8559
  fields.push({
7497
8560
  label: encoding.size.axis?.label ?? encoding.size.field,
7498
8561
  value: formatValue(row[encoding.size.field], encoding.size.type, encoding.size.axis?.format)
@@ -7513,12 +8576,20 @@ function getTooltipTitle(row, encoding) {
7513
8576
  if (encoding.y?.type === "nominal" || encoding.y?.type === "ordinal") {
7514
8577
  return String(row[encoding.y.field] ?? "");
7515
8578
  }
7516
- if (encoding.color) {
8579
+ if (encoding.color && "field" in encoding.color) {
7517
8580
  return String(row[encoding.color.field] ?? "");
7518
8581
  }
7519
8582
  return void 0;
7520
8583
  }
7521
- function tooltipsForLine(_mark, _encoding, _markIndex) {
8584
+ function tooltipsForLine(mark, encoding, _markIndex) {
8585
+ if (mark.dataPoints) {
8586
+ for (const dp of mark.dataPoints) {
8587
+ dp.tooltip = {
8588
+ title: getTooltipTitle(dp.datum, encoding),
8589
+ fields: buildFields(dp.datum, encoding, mark.stroke)
8590
+ };
8591
+ }
8592
+ }
7522
8593
  return [];
7523
8594
  }
7524
8595
  function tooltipsForPoint(mark, encoding, markIndex) {
@@ -7534,8 +8605,9 @@ function tooltipsForRect(mark, encoding, markIndex) {
7534
8605
  function tooltipsForArc(mark, encoding, markIndex) {
7535
8606
  const row = mark.data;
7536
8607
  const fields = [];
7537
- if (encoding.color) {
7538
- const categoryName = String(row[encoding.color.field] ?? "");
8608
+ const colorEnc = encoding.color && "field" in encoding.color ? encoding.color : void 0;
8609
+ if (colorEnc) {
8610
+ const categoryName = String(row[colorEnc.field] ?? "");
7539
8611
  if (encoding.y) {
7540
8612
  fields.push({
7541
8613
  label: categoryName,
@@ -7550,10 +8622,18 @@ function tooltipsForArc(mark, encoding, markIndex) {
7550
8622
  color: mark.fill
7551
8623
  });
7552
8624
  }
7553
- const title = encoding.color ? String(row[encoding.color.field] ?? "") : void 0;
8625
+ const title = colorEnc ? String(row[colorEnc.field] ?? "") : void 0;
7554
8626
  return [[`arc-${markIndex}`, { title, fields }]];
7555
8627
  }
7556
- function tooltipsForArea(_mark, _encoding, _markIndex) {
8628
+ function tooltipsForArea(mark, encoding, _markIndex) {
8629
+ if (mark.dataPoints) {
8630
+ for (const dp of mark.dataPoints) {
8631
+ dp.tooltip = {
8632
+ title: getTooltipTitle(dp.datum, encoding),
8633
+ fields: buildFields(dp.datum, encoding, mark.fill)
8634
+ };
8635
+ }
8636
+ }
7557
8637
  return [];
7558
8638
  }
7559
8639
  function computeTooltipDescriptors(spec, marks) {
@@ -7586,16 +8666,205 @@ function computeTooltipDescriptors(spec, marks) {
7586
8666
  return descriptors;
7587
8667
  }
7588
8668
 
8669
+ // src/transforms/bin.ts
8670
+ function computeStep(extent2, maxbins, nice2) {
8671
+ const span = extent2[1] - extent2[0];
8672
+ if (span === 0) return 1;
8673
+ let step = span / maxbins;
8674
+ if (nice2) {
8675
+ const magnitude = 10 ** Math.floor(Math.log10(step));
8676
+ const residual = step / magnitude;
8677
+ if (residual <= 1.5) {
8678
+ step = magnitude;
8679
+ } else if (residual <= 3.5) {
8680
+ step = 2 * magnitude;
8681
+ } else if (residual <= 7.5) {
8682
+ step = 5 * magnitude;
8683
+ } else {
8684
+ step = 10 * magnitude;
8685
+ }
8686
+ }
8687
+ return step;
8688
+ }
8689
+ function runBin(data, transform) {
8690
+ const params = transform.bin === true ? {} : transform.bin;
8691
+ const maxbins = params.maxbins ?? 10;
8692
+ const nice2 = params.nice ?? true;
8693
+ const field = transform.field;
8694
+ let extent2 = params.extent;
8695
+ if (!extent2) {
8696
+ let min3 = Infinity;
8697
+ let max3 = -Infinity;
8698
+ for (const row of data) {
8699
+ const v = Number(row[field]);
8700
+ if (Number.isFinite(v)) {
8701
+ if (v < min3) min3 = v;
8702
+ if (v > max3) max3 = v;
8703
+ }
8704
+ }
8705
+ extent2 = [min3 === Infinity ? 0 : min3, max3 === -Infinity ? 0 : max3];
8706
+ }
8707
+ const step = params.step ?? computeStep(extent2, maxbins, nice2);
8708
+ const [startAs, endAs] = Array.isArray(transform.as) ? transform.as : [transform.as, void 0];
8709
+ return data.map((row) => {
8710
+ const v = Number(row[field]);
8711
+ const newRow = { ...row };
8712
+ if (!Number.isFinite(v)) {
8713
+ newRow[startAs] = null;
8714
+ if (endAs) newRow[endAs] = null;
8715
+ } else {
8716
+ const binStart = Math.floor((v - extent2[0]) / step) * step + extent2[0];
8717
+ newRow[startAs] = binStart;
8718
+ if (endAs) newRow[endAs] = binStart + step;
8719
+ }
8720
+ return newRow;
8721
+ });
8722
+ }
8723
+
8724
+ // src/transforms/calculate.ts
8725
+ function evaluateExpression(datum, expr) {
8726
+ const fieldValue = Number(datum[expr.field]);
8727
+ switch (expr.op) {
8728
+ case "abs":
8729
+ return Math.abs(fieldValue);
8730
+ case "round":
8731
+ return Math.round(fieldValue);
8732
+ case "floor":
8733
+ return Math.floor(fieldValue);
8734
+ case "ceil":
8735
+ return Math.ceil(fieldValue);
8736
+ case "log":
8737
+ return Math.log(fieldValue);
8738
+ case "sqrt":
8739
+ return Math.sqrt(fieldValue);
8740
+ }
8741
+ const operand = expr.field2 !== void 0 ? Number(datum[expr.field2]) : expr.value ?? 0;
8742
+ switch (expr.op) {
8743
+ case "+":
8744
+ return fieldValue + operand;
8745
+ case "-":
8746
+ return fieldValue - operand;
8747
+ case "*":
8748
+ return fieldValue * operand;
8749
+ case "/":
8750
+ return operand === 0 ? NaN : fieldValue / operand;
8751
+ }
8752
+ }
8753
+ function runCalculate(data, transform) {
8754
+ return data.map((row) => ({
8755
+ ...row,
8756
+ [transform.as]: evaluateExpression(row, transform.calculate)
8757
+ }));
8758
+ }
8759
+
8760
+ // src/transforms/filter.ts
8761
+ function runFilter(data, predicate) {
8762
+ return data.filter((datum) => evaluatePredicate(datum, predicate));
8763
+ }
8764
+
8765
+ // src/transforms/timeunit.ts
8766
+ function extractTimeUnit(date2, unit2) {
8767
+ switch (unit2) {
8768
+ case "year":
8769
+ return date2.getFullYear();
8770
+ case "quarter":
8771
+ return Math.floor(date2.getMonth() / 3) + 1;
8772
+ case "month":
8773
+ return date2.getMonth();
8774
+ // 0-indexed like JS Date
8775
+ case "week": {
8776
+ const d = new Date(date2.getTime());
8777
+ d.setHours(0, 0, 0, 0);
8778
+ d.setDate(d.getDate() + 3 - (d.getDay() + 6) % 7);
8779
+ const yearStart = new Date(d.getFullYear(), 0, 1);
8780
+ return Math.ceil(((d.getTime() - yearStart.getTime()) / 864e5 + 1) / 7);
8781
+ }
8782
+ case "day":
8783
+ return date2.getDay();
8784
+ // 0 = Sunday
8785
+ case "dayofyear": {
8786
+ const start = new Date(date2.getFullYear(), 0, 0);
8787
+ const diff = date2.getTime() - start.getTime();
8788
+ return Math.floor(diff / 864e5);
8789
+ }
8790
+ case "date":
8791
+ return date2.getDate();
8792
+ // 1-31
8793
+ case "hours":
8794
+ return date2.getHours();
8795
+ case "minutes":
8796
+ return date2.getMinutes();
8797
+ case "seconds":
8798
+ return date2.getSeconds();
8799
+ case "milliseconds":
8800
+ return date2.getMilliseconds();
8801
+ // Compound units
8802
+ case "yearmonth":
8803
+ return `${date2.getFullYear()}-${String(date2.getMonth() + 1).padStart(2, "0")}`;
8804
+ case "yearmonthdate":
8805
+ return `${date2.getFullYear()}-${String(date2.getMonth() + 1).padStart(2, "0")}-${String(date2.getDate()).padStart(2, "0")}`;
8806
+ case "monthdate":
8807
+ return `${String(date2.getMonth() + 1).padStart(2, "0")}-${String(date2.getDate()).padStart(2, "0")}`;
8808
+ case "hoursminutes":
8809
+ return `${String(date2.getHours()).padStart(2, "0")}:${String(date2.getMinutes()).padStart(2, "0")}`;
8810
+ }
8811
+ }
8812
+ function toDate(value) {
8813
+ if (value instanceof Date) return value;
8814
+ if (typeof value === "string" || typeof value === "number") {
8815
+ const d = new Date(value);
8816
+ return Number.isNaN(d.getTime()) ? null : d;
8817
+ }
8818
+ return null;
8819
+ }
8820
+ function runTimeUnit(data, transform) {
8821
+ return data.map((row) => {
8822
+ const date2 = toDate(row[transform.field]);
8823
+ return {
8824
+ ...row,
8825
+ [transform.as]: date2 ? extractTimeUnit(date2, transform.timeUnit) : null
8826
+ };
8827
+ });
8828
+ }
8829
+
8830
+ // src/transforms/index.ts
8831
+ function runTransforms(data, transforms) {
8832
+ let result = data;
8833
+ for (const transform of transforms) {
8834
+ if ("filter" in transform) {
8835
+ result = runFilter(result, transform.filter);
8836
+ } else if ("bin" in transform) {
8837
+ result = runBin(result, transform);
8838
+ } else if ("calculate" in transform) {
8839
+ result = runCalculate(result, transform);
8840
+ } else if ("timeUnit" in transform) {
8841
+ result = runTimeUnit(result, transform);
8842
+ }
8843
+ }
8844
+ return result;
8845
+ }
8846
+
7589
8847
  // src/compile.ts
7590
8848
  var builtinRenderers = {
7591
8849
  line: lineRenderer,
7592
8850
  area: areaRenderer,
7593
8851
  bar: barRenderer,
7594
- column: columnRenderer,
7595
- scatter: scatterRenderer,
7596
- pie: pieRenderer,
7597
- donut: donutRenderer,
7598
- dot: dotRenderer
8852
+ // horizontal bars
8853
+ "bar:vertical": columnRenderer,
8854
+ // vertical bars (old 'column')
8855
+ point: scatterRenderer,
8856
+ // old 'scatter'
8857
+ arc: pieRenderer,
8858
+ // old 'pie' (donut handled via innerRadius)
8859
+ "arc:donut": donutRenderer,
8860
+ // old 'donut'
8861
+ circle: dotRenderer,
8862
+ // old 'dot'
8863
+ text: textRenderer,
8864
+ rule: ruleRenderer,
8865
+ tick: tickRenderer,
8866
+ rect: columnRenderer
8867
+ // rect uses column renderer (RectMark output) as baseline for heatmaps
7599
8868
  };
7600
8869
  for (const [type, renderer] of Object.entries(builtinRenderers)) {
7601
8870
  registerChartRenderer(type, renderer);
@@ -7665,13 +8934,17 @@ function computeBandRowObstacles(marks, scales) {
7665
8934
  }
7666
8935
  function compileChart(spec, options) {
7667
8936
  const { spec: normalized } = compile(spec);
7668
- if (normalized.type === "table") {
8937
+ if ("type" in normalized && normalized.type === "table") {
7669
8938
  throw new Error("compileChart received a table spec. Use compileTable instead.");
7670
8939
  }
7671
- if (normalized.type === "graph") {
8940
+ if ("type" in normalized && normalized.type === "graph") {
7672
8941
  throw new Error("compileChart received a graph spec. Use compileGraph instead.");
7673
8942
  }
7674
8943
  let chartSpec = normalized;
8944
+ const rawTransforms = spec.transform;
8945
+ if (rawTransforms && rawTransforms.length > 0) {
8946
+ chartSpec = { ...chartSpec, data: runTransforms(chartSpec.data, rawTransforms) };
8947
+ }
7675
8948
  const breakpoint = getBreakpoint(options.width);
7676
8949
  const heightClass = getHeightClass(options.height);
7677
8950
  const strategy = getLayoutStrategy(breakpoint, heightClass);
@@ -7745,7 +9018,7 @@ function compileChart(spec, options) {
7745
9018
  }
7746
9019
  const finalLegend = computeLegend(chartSpec, strategy, theme, legendArea);
7747
9020
  let renderData = chartSpec.data;
7748
- if (chartSpec.hiddenSeries.length > 0 && chartSpec.encoding.color) {
9021
+ if (chartSpec.hiddenSeries.length > 0 && chartSpec.encoding.color && "field" in chartSpec.encoding.color) {
7749
9022
  const colorField = chartSpec.encoding.color.field;
7750
9023
  const hiddenSet = new Set(chartSpec.hiddenSeries);
7751
9024
  renderData = renderData.filter((row) => !hiddenSet.has(String(row[colorField])));
@@ -7779,12 +9052,26 @@ function compileChart(spec, options) {
7779
9052
  }
7780
9053
  }
7781
9054
  scales.defaultColor = theme.colors.categorical[0];
7782
- const isRadial = chartSpec.type === "pie" || chartSpec.type === "donut";
9055
+ const isRadial = chartSpec.markType === "arc";
7783
9056
  const axes = isRadial ? { x: void 0, y: void 0 } : computeAxes(scales, chartArea, strategy, theme, options.measureText);
7784
9057
  if (!isRadial) {
7785
9058
  computeGridlines(axes, chartArea);
7786
9059
  }
7787
- const renderer = getChartRenderer(renderSpec.type);
9060
+ let rendererKey = renderSpec.markType;
9061
+ if (rendererKey === "bar") {
9062
+ const xType = renderSpec.encoding.x?.type;
9063
+ const yType = renderSpec.encoding.y?.type;
9064
+ const isVertical = (xType === "nominal" || xType === "ordinal" || xType === "temporal") && yType === "quantitative";
9065
+ if (isVertical) {
9066
+ rendererKey = "bar:vertical";
9067
+ }
9068
+ } else if (rendererKey === "arc") {
9069
+ const innerRadius = renderSpec.markDef.innerRadius;
9070
+ if (innerRadius && innerRadius > 0) {
9071
+ rendererKey = "arc:donut";
9072
+ }
9073
+ }
9074
+ const renderer = getChartRenderer(rendererKey);
7788
9075
  const marks = renderer ? renderer(renderSpec, scales, chartArea, strategy, theme) : [];
7789
9076
  const obstacles = [];
7790
9077
  if (finalLegend.bounds.width > 0) {
@@ -7792,7 +9079,7 @@ function compileChart(spec, options) {
7792
9079
  }
7793
9080
  obstacles.push(...computeMarkObstacles(marks, scales));
7794
9081
  for (const mark of marks) {
7795
- if (mark.type !== "area" && mark.label?.visible) {
9082
+ if ("label" in mark && mark.label?.visible) {
7796
9083
  obstacles.push(computeLabelBounds(mark.label));
7797
9084
  }
7798
9085
  }
@@ -7813,7 +9100,7 @@ function compileChart(spec, options) {
7813
9100
  const tooltipDescriptors = computeTooltipDescriptors(chartSpec, marks);
7814
9101
  const altText = generateAltText(
7815
9102
  {
7816
- type: chartSpec.type,
9103
+ mark: chartSpec.markType,
7817
9104
  data: chartSpec.data,
7818
9105
  encoding: chartSpec.encoding,
7819
9106
  chrome: chartSpec.chrome
@@ -7822,7 +9109,7 @@ function compileChart(spec, options) {
7822
9109
  );
7823
9110
  const dataTableFallback = generateDataTable(
7824
9111
  {
7825
- type: chartSpec.type,
9112
+ mark: chartSpec.markType,
7826
9113
  data: chartSpec.data,
7827
9114
  encoding: chartSpec.encoding
7828
9115
  },
@@ -7852,10 +9139,63 @@ function compileChart(spec, options) {
7852
9139
  }
7853
9140
  };
7854
9141
  }
9142
+ function compileLayer(spec, options) {
9143
+ const leaves = flattenLayers(spec);
9144
+ if (leaves.length === 0) {
9145
+ throw new Error("LayerSpec has no leaf chart specs after flattening");
9146
+ }
9147
+ if (leaves.length === 1) {
9148
+ const singleSpec = buildPrimarySpec(leaves, spec);
9149
+ return compileChart(singleSpec, options);
9150
+ }
9151
+ const primarySpec = buildPrimarySpec(leaves, spec);
9152
+ const primaryLayout = compileChart(primarySpec, options);
9153
+ const allMarks = [];
9154
+ const seenLabels = /* @__PURE__ */ new Set();
9155
+ const mergedLegendEntries = [...primaryLayout.legend.entries];
9156
+ for (const entry of mergedLegendEntries) {
9157
+ seenLabels.add(entry.label);
9158
+ }
9159
+ for (const leaf of leaves) {
9160
+ const leafLayout = compileChart(leaf, options);
9161
+ allMarks.push(...leafLayout.marks);
9162
+ for (const entry of leafLayout.legend.entries) {
9163
+ if (!seenLabels.has(entry.label)) {
9164
+ seenLabels.add(entry.label);
9165
+ mergedLegendEntries.push(entry);
9166
+ }
9167
+ }
9168
+ }
9169
+ return {
9170
+ ...primaryLayout,
9171
+ marks: allMarks,
9172
+ legend: {
9173
+ ...primaryLayout.legend,
9174
+ entries: mergedLegendEntries
9175
+ }
9176
+ };
9177
+ }
9178
+ function buildPrimarySpec(leaves, layerSpec) {
9179
+ const allData = leaves.flatMap((leaf) => leaf.data);
9180
+ const primary = {
9181
+ ...leaves[0],
9182
+ data: allData,
9183
+ // Layer-level chrome overrides leaf chrome
9184
+ chrome: layerSpec.chrome ?? leaves[0].chrome,
9185
+ labels: layerSpec.labels ?? leaves[0].labels,
9186
+ legend: layerSpec.legend ?? leaves[0].legend,
9187
+ responsive: layerSpec.responsive ?? leaves[0].responsive,
9188
+ theme: layerSpec.theme ?? leaves[0].theme,
9189
+ darkMode: layerSpec.darkMode ?? leaves[0].darkMode,
9190
+ hiddenSeries: layerSpec.hiddenSeries ?? leaves[0].hiddenSeries
9191
+ };
9192
+ return primary;
9193
+ }
7855
9194
  function compileTable(spec, options) {
7856
9195
  const { spec: normalized } = compile(spec);
7857
- if (normalized.type !== "table") {
7858
- throw new Error(`compileTable received a ${normalized.type} spec. Use compileChart instead.`);
9196
+ const normType = "type" in normalized ? normalized.type : void 0;
9197
+ if (normType !== "table") {
9198
+ throw new Error(`compileTable received a non-table spec. Use compileChart instead.`);
7859
9199
  }
7860
9200
  const tableSpec = normalized;
7861
9201
  const mergedThemeConfig = options.theme ? { ...tableSpec.theme, ...options.theme } : tableSpec.theme;
@@ -7873,10 +9213,19 @@ export {
7873
9213
  compile,
7874
9214
  compileChart,
7875
9215
  compileGraph2 as compileGraph,
9216
+ compileLayer,
7876
9217
  compileTable,
9218
+ evaluatePredicate,
7877
9219
  getChartRenderer,
9220
+ isConditionalValueDef,
7878
9221
  normalizeSpec,
7879
9222
  registerChartRenderer,
9223
+ resolveConditionalValue,
9224
+ runBin,
9225
+ runCalculate,
9226
+ runFilter,
9227
+ runTimeUnit,
9228
+ runTransforms,
7880
9229
  validateSpec
7881
9230
  };
7882
9231
  //# sourceMappingURL=index.js.map