@opendata-ai/openchart-engine 6.25.4 → 6.27.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.
package/dist/index.js CHANGED
@@ -1,17 +1,17 @@
1
1
  // src/compile.ts
2
2
  import {
3
3
  AXIS_TITLE_TRAILING_PAD as AXIS_TITLE_TRAILING_PAD2,
4
- adaptTheme as adaptTheme3,
4
+ adaptTheme as adaptTheme4,
5
5
  BREAKPOINT_COMPACT_MAX as BREAKPOINT_COMPACT_MAX2,
6
6
  computeLabelBounds,
7
- estimateTextWidth as estimateTextWidth14,
7
+ estimateTextWidth as estimateTextWidth15,
8
8
  generateAltText,
9
9
  generateDataTable,
10
10
  getAxisTitleOffset as getAxisTitleOffset2,
11
11
  getBreakpoint,
12
12
  getHeightClass,
13
13
  getLayoutStrategy,
14
- resolveTheme as resolveTheme3,
14
+ resolveTheme as resolveTheme4,
15
15
  TICK_LABEL_OFFSET
16
16
  } from "@opendata-ai/openchart-core";
17
17
 
@@ -4115,6 +4115,12 @@ import { isGradientDef } from "@opendata-ai/openchart-core";
4115
4115
  function isFieldPredicate(pred) {
4116
4116
  return "field" in pred;
4117
4117
  }
4118
+ function isRelativeTimeRef(value2) {
4119
+ return typeof value2 === "object" && value2 !== null && "anchor" in value2 && "offset" in value2 && "unit" in value2;
4120
+ }
4121
+ function toNum(v) {
4122
+ return typeof v === "number" ? v : NaN;
4123
+ }
4118
4124
  function evaluateFieldPredicate(datum, pred) {
4119
4125
  const value2 = datum[pred.field];
4120
4126
  if (pred.valid !== void 0) {
@@ -4124,22 +4130,26 @@ function evaluateFieldPredicate(datum, pred) {
4124
4130
  if (pred.equal !== void 0) {
4125
4131
  return value2 == pred.equal;
4126
4132
  }
4127
- const numValue = Number(value2);
4133
+ let numValue = Number(value2);
4134
+ if (Number.isNaN(numValue) && value2 != null) {
4135
+ const ms = new Date(value2).getTime();
4136
+ if (!Number.isNaN(ms)) numValue = ms;
4137
+ }
4128
4138
  if (pred.lt !== void 0) {
4129
- return numValue < pred.lt;
4139
+ return numValue < toNum(pred.lt);
4130
4140
  }
4131
4141
  if (pred.lte !== void 0) {
4132
- return numValue <= pred.lte;
4142
+ return numValue <= toNum(pred.lte);
4133
4143
  }
4134
4144
  if (pred.gt !== void 0) {
4135
- return numValue > pred.gt;
4145
+ return numValue > toNum(pred.gt);
4136
4146
  }
4137
4147
  if (pred.gte !== void 0) {
4138
- return numValue >= pred.gte;
4148
+ return numValue >= toNum(pred.gte);
4139
4149
  }
4140
4150
  if (pred.range !== void 0) {
4141
- const [min4, max4] = pred.range;
4142
- return numValue >= min4 && numValue <= max4;
4151
+ const [lo, hi] = pred.range;
4152
+ return numValue >= toNum(lo) && numValue <= toNum(hi);
4143
4153
  }
4144
4154
  if (pred.oneOf !== void 0) {
4145
4155
  return pred.oneOf.some((v) => v == value2);
@@ -4356,16 +4366,16 @@ function computeStackedBars(data, valueField, categoryField, colorField, xScale,
4356
4366
  }
4357
4367
  let cumulativeValue = stackMode === "center" ? -categoryTotal / 2 : 0;
4358
4368
  for (const row of rows) {
4359
- const groupKey2 = String(row[colorField] ?? "");
4369
+ const groupKey3 = String(row[colorField] ?? "");
4360
4370
  const rawValue = Number(row[valueField] ?? 0);
4361
4371
  if (!Number.isFinite(rawValue) || rawValue <= 0) continue;
4362
4372
  const value2 = stackMode === "normalize" && categoryTotal > 0 ? rawValue / categoryTotal : rawValue;
4363
- const color2 = getColor(scales, groupKey2);
4373
+ const color2 = getColor(scales, groupKey3);
4364
4374
  const xLeft = xScale(cumulativeValue);
4365
4375
  const xRight = xScale(cumulativeValue + value2);
4366
4376
  const barWidth = Math.max(Math.abs(xRight - xLeft), MIN_BAR_WIDTH);
4367
4377
  const aria = {
4368
- label: `${category}, ${groupKey2}: ${formatLabelValue(rawValue)}`
4378
+ label: `${category}, ${groupKey3}: ${formatLabelValue(rawValue)}`
4369
4379
  };
4370
4380
  marks.push({
4371
4381
  type: "rect",
@@ -4403,16 +4413,16 @@ function computeGroupedBars(data, valueField, categoryField, colorField, xScale,
4403
4413
  const bandY = yScale(category);
4404
4414
  if (bandY === void 0) continue;
4405
4415
  for (const row of rows) {
4406
- const groupKey2 = String(row[colorField] ?? "");
4416
+ const groupKey3 = String(row[colorField] ?? "");
4407
4417
  const value2 = Number(row[valueField] ?? 0);
4408
4418
  if (!Number.isFinite(value2)) continue;
4409
- const groupIndex = groupIndexMap.get(groupKey2) ?? 0;
4410
- const color2 = getColor(scales, groupKey2);
4419
+ const groupIndex = groupIndexMap.get(groupKey3) ?? 0;
4420
+ const color2 = getColor(scales, groupKey3);
4411
4421
  const xPos = value2 >= 0 ? baseline : xScale(value2);
4412
4422
  const barWidth = Math.max(Math.abs(xScale(value2) - baseline), MIN_BAR_WIDTH);
4413
4423
  const subY = bandY + groupIndex * (subBandHeight + gap);
4414
4424
  const aria = {
4415
- label: `${category}, ${groupKey2}: ${formatLabelValue(value2)}`
4425
+ label: `${category}, ${groupKey3}: ${formatLabelValue(value2)}`
4416
4426
  };
4417
4427
  marks.push({
4418
4428
  type: "rect",
@@ -4438,12 +4448,12 @@ function computeColoredBars(data, valueField, categoryField, colorField, xScale,
4438
4448
  if (!Number.isFinite(value2)) continue;
4439
4449
  const bandY = yScale(category);
4440
4450
  if (bandY === void 0) continue;
4441
- const groupKey2 = String(row[colorField] ?? "");
4442
- const color2 = getColor(scales, groupKey2);
4451
+ const groupKey3 = String(row[colorField] ?? "");
4452
+ const color2 = getColor(scales, groupKey3);
4443
4453
  const xPos = value2 >= 0 ? baseline : xScale(value2);
4444
4454
  const barWidth = Math.max(Math.abs(xScale(value2) - baseline), MIN_BAR_WIDTH);
4445
4455
  const aria = {
4446
- label: `${category}, ${groupKey2}: ${formatLabelValue(value2)}`
4456
+ label: `${category}, ${groupKey3}: ${formatLabelValue(value2)}`
4447
4457
  };
4448
4458
  marks.push({
4449
4459
  type: "rect",
@@ -4810,13 +4820,13 @@ function computeColoredColumns(data, categoryField, valueField, colorField, xSca
4810
4820
  if (!Number.isFinite(value2)) continue;
4811
4821
  const bandX = xScale(category);
4812
4822
  if (bandX === void 0) continue;
4813
- const groupKey2 = String(row[colorField] ?? "");
4814
- const color2 = getColor(scales, groupKey2);
4823
+ const groupKey3 = String(row[colorField] ?? "");
4824
+ const color2 = getColor(scales, groupKey3);
4815
4825
  const yPos = yScale(value2);
4816
4826
  const columnHeight = Math.max(Math.abs(baseline - yPos), MIN_COLUMN_HEIGHT);
4817
4827
  const y2 = value2 >= 0 ? yPos : baseline;
4818
4828
  const aria = {
4819
- label: `${category}, ${groupKey2}: ${formatLabelValue(value2)}`
4829
+ label: `${category}, ${groupKey3}: ${formatLabelValue(value2)}`
4820
4830
  };
4821
4831
  marks.push({
4822
4832
  type: "rect",
@@ -4854,17 +4864,17 @@ function computeGroupedColumns(data, categoryField, valueField, colorField, xSca
4854
4864
  const bandX = xScale(category);
4855
4865
  if (bandX === void 0) continue;
4856
4866
  for (const row of rows) {
4857
- const groupKey2 = String(row[colorField] ?? "");
4867
+ const groupKey3 = String(row[colorField] ?? "");
4858
4868
  const value2 = Number(row[valueField] ?? 0);
4859
4869
  if (!Number.isFinite(value2)) continue;
4860
- const groupIndex = groupIndexMap.get(groupKey2) ?? 0;
4861
- const color2 = getColor(scales, groupKey2);
4870
+ const groupIndex = groupIndexMap.get(groupKey3) ?? 0;
4871
+ const color2 = getColor(scales, groupKey3);
4862
4872
  const yPos = yScale(value2);
4863
4873
  const columnHeight = Math.max(Math.abs(baseline - yPos), MIN_COLUMN_HEIGHT);
4864
4874
  const y2 = value2 >= 0 ? yPos : baseline;
4865
4875
  const subX = bandX + groupIndex * (subBandWidth + gap);
4866
4876
  const aria = {
4867
- label: `${category}, ${groupKey2}: ${formatLabelValue(value2)}`
4877
+ label: `${category}, ${groupKey3}: ${formatLabelValue(value2)}`
4868
4878
  };
4869
4879
  marks.push({
4870
4880
  type: "rect",
@@ -4895,16 +4905,16 @@ function computeStackedColumns(data, categoryField, valueField, colorField, xSca
4895
4905
  }
4896
4906
  let cumulativeValue = stackMode === "center" ? -categoryTotal / 2 : 0;
4897
4907
  for (const row of rows) {
4898
- const groupKey2 = String(row[colorField] ?? "");
4908
+ const groupKey3 = String(row[colorField] ?? "");
4899
4909
  const rawValue = Number(row[valueField] ?? 0);
4900
4910
  if (!Number.isFinite(rawValue) || rawValue <= 0) continue;
4901
4911
  const value2 = stackMode === "normalize" && categoryTotal > 0 ? rawValue / categoryTotal : rawValue;
4902
- const color2 = getColor(scales, groupKey2);
4912
+ const color2 = getColor(scales, groupKey3);
4903
4913
  const yTop = yScale(cumulativeValue + value2);
4904
4914
  const yBottom = yScale(cumulativeValue);
4905
4915
  const columnHeight = Math.max(Math.abs(yBottom - yTop), MIN_COLUMN_HEIGHT);
4906
4916
  const aria = {
4907
- label: `${category}, ${groupKey2}: ${formatLabelValue(rawValue)}`
4917
+ label: `${category}, ${groupKey3}: ${formatLabelValue(rawValue)}`
4908
4918
  };
4909
4919
  marks.push({
4910
4920
  type: "rect",
@@ -5341,7 +5351,7 @@ function computeSingleArea(spec, scales, _chartArea) {
5341
5351
  fill: fillValue,
5342
5352
  fillOpacity,
5343
5353
  stroke: getRepresentativeColor4(isGradientDef3(fillValue) ? color2 : fillValue),
5344
- strokeWidth: 2,
5354
+ strokeWidth: spec.display === "sparkline" ? 1.25 : 2,
5345
5355
  seriesKey: seriesKey === "__default__" ? void 0 : seriesKey,
5346
5356
  data: validPoints.map((p) => p.row),
5347
5357
  dataPoints: validPoints.map((p) => ({ x: p.x, y: p.yTop, datum: p.row })),
@@ -5458,6 +5468,7 @@ function computeAreaMarks(spec, scales, chartArea) {
5458
5468
  // src/charts/line/compute.ts
5459
5469
  import { getRepresentativeColor as getRepresentativeColor5 } from "@opendata-ai/openchart-core";
5460
5470
  var DEFAULT_STROKE_WIDTH = 2.5;
5471
+ var SPARKLINE_STROKE_WIDTH = 1.25;
5461
5472
  var DEFAULT_POINT_RADIUS = 3;
5462
5473
  function computeLineMarks(spec, scales, _chartArea, _strategy) {
5463
5474
  const encoding = spec.encoding;
@@ -5524,7 +5535,7 @@ function computeLineMarks(spec, scales, _chartArea, _strategy) {
5524
5535
  points: allPoints,
5525
5536
  path: combinedPath,
5526
5537
  stroke: strokeColor,
5527
- strokeWidth: styleOverride?.strokeWidth ?? DEFAULT_STROKE_WIDTH,
5538
+ strokeWidth: styleOverride?.strokeWidth ?? (spec.display === "sparkline" ? SPARKLINE_STROKE_WIDTH : DEFAULT_STROKE_WIDTH),
5528
5539
  strokeDasharray,
5529
5540
  opacity: styleOverride?.opacity,
5530
5541
  seriesKey: seriesStyleKey,
@@ -6543,9 +6554,146 @@ import {
6543
6554
  isLayerSpec,
6544
6555
  isSankeySpec,
6545
6556
  isTableSpec,
6557
+ isTileMapSpec,
6546
6558
  resolveMarkDef,
6547
6559
  resolveMarkType
6548
6560
  } from "@opendata-ai/openchart-core";
6561
+
6562
+ // src/tilemap/layout.ts
6563
+ var US_STATE_TILES = [
6564
+ // Row 0
6565
+ { state: "ME", col: 10, row: 0 },
6566
+ // Row 1
6567
+ { state: "VT", col: 9, row: 1 },
6568
+ { state: "NH", col: 10, row: 1 },
6569
+ // Row 2
6570
+ { state: "WA", col: 0, row: 2 },
6571
+ { state: "ID", col: 1, row: 2 },
6572
+ { state: "MT", col: 2, row: 2 },
6573
+ { state: "ND", col: 3, row: 2 },
6574
+ { state: "MN", col: 4, row: 2 },
6575
+ { state: "WI", col: 5, row: 2 },
6576
+ { state: "MI", col: 6, row: 2 },
6577
+ { state: "NY", col: 8, row: 2 },
6578
+ { state: "MA", col: 9, row: 2 },
6579
+ // Row 3
6580
+ { state: "OR", col: 0, row: 3 },
6581
+ { state: "NV", col: 1, row: 3 },
6582
+ { state: "WY", col: 2, row: 3 },
6583
+ { state: "SD", col: 3, row: 3 },
6584
+ { state: "IA", col: 4, row: 3 },
6585
+ { state: "IL", col: 5, row: 3 },
6586
+ { state: "IN", col: 6, row: 3 },
6587
+ { state: "OH", col: 7, row: 3 },
6588
+ { state: "PA", col: 8, row: 3 },
6589
+ { state: "NJ", col: 9, row: 3 },
6590
+ { state: "CT", col: 10, row: 3 },
6591
+ // Row 4
6592
+ { state: "CA", col: 0, row: 4 },
6593
+ { state: "UT", col: 1, row: 4 },
6594
+ { state: "CO", col: 2, row: 4 },
6595
+ { state: "NE", col: 3, row: 4 },
6596
+ { state: "MO", col: 4, row: 4 },
6597
+ { state: "KY", col: 5, row: 4 },
6598
+ { state: "WV", col: 6, row: 4 },
6599
+ { state: "VA", col: 7, row: 4 },
6600
+ { state: "MD", col: 8, row: 4 },
6601
+ { state: "DE", col: 9, row: 4 },
6602
+ { state: "RI", col: 10, row: 4 },
6603
+ // Row 5
6604
+ { state: "AZ", col: 1, row: 5 },
6605
+ { state: "NM", col: 2, row: 5 },
6606
+ { state: "KS", col: 3, row: 5 },
6607
+ { state: "AR", col: 4, row: 5 },
6608
+ { state: "TN", col: 5, row: 5 },
6609
+ { state: "NC", col: 6, row: 5 },
6610
+ { state: "SC", col: 7, row: 5 },
6611
+ { state: "DC", col: 8, row: 5 },
6612
+ // Row 6
6613
+ { state: "AK", col: 0, row: 6 },
6614
+ { state: "OK", col: 3, row: 6 },
6615
+ { state: "LA", col: 4, row: 6 },
6616
+ { state: "MS", col: 5, row: 6 },
6617
+ { state: "AL", col: 6, row: 6 },
6618
+ { state: "GA", col: 7, row: 6 },
6619
+ // Row 7
6620
+ { state: "HI", col: 1, row: 7 },
6621
+ { state: "TX", col: 3, row: 7 },
6622
+ { state: "FL", col: 7, row: 7 }
6623
+ ];
6624
+ var STATE_CODE_SET = new Set(US_STATE_TILES.map((t) => t.state));
6625
+ var STATE_NAMES = {
6626
+ AL: "Alabama",
6627
+ AK: "Alaska",
6628
+ AZ: "Arizona",
6629
+ AR: "Arkansas",
6630
+ CA: "California",
6631
+ CO: "Colorado",
6632
+ CT: "Connecticut",
6633
+ DE: "Delaware",
6634
+ DC: "District of Columbia",
6635
+ FL: "Florida",
6636
+ GA: "Georgia",
6637
+ HI: "Hawaii",
6638
+ ID: "Idaho",
6639
+ IL: "Illinois",
6640
+ IN: "Indiana",
6641
+ IA: "Iowa",
6642
+ KS: "Kansas",
6643
+ KY: "Kentucky",
6644
+ LA: "Louisiana",
6645
+ ME: "Maine",
6646
+ MD: "Maryland",
6647
+ MA: "Massachusetts",
6648
+ MI: "Michigan",
6649
+ MN: "Minnesota",
6650
+ MS: "Mississippi",
6651
+ MO: "Missouri",
6652
+ MT: "Montana",
6653
+ NE: "Nebraska",
6654
+ NV: "Nevada",
6655
+ NH: "New Hampshire",
6656
+ NJ: "New Jersey",
6657
+ NM: "New Mexico",
6658
+ NY: "New York",
6659
+ NC: "North Carolina",
6660
+ ND: "North Dakota",
6661
+ OH: "Ohio",
6662
+ OK: "Oklahoma",
6663
+ OR: "Oregon",
6664
+ PA: "Pennsylvania",
6665
+ RI: "Rhode Island",
6666
+ SC: "South Carolina",
6667
+ SD: "South Dakota",
6668
+ TN: "Tennessee",
6669
+ TX: "Texas",
6670
+ UT: "Utah",
6671
+ VT: "Vermont",
6672
+ VA: "Virginia",
6673
+ WA: "Washington",
6674
+ WV: "West Virginia",
6675
+ WI: "Wisconsin",
6676
+ WY: "Wyoming"
6677
+ };
6678
+ var GRID_COLS = 12;
6679
+ var GRID_ROWS = 8;
6680
+ function computeTilePositions(availableWidth, availableHeight, gap = 4) {
6681
+ const maxTileW = (availableWidth - gap * (GRID_COLS - 1)) / GRID_COLS;
6682
+ const maxTileH = (availableHeight - gap * (GRID_ROWS - 1)) / GRID_ROWS;
6683
+ const tileSize = Math.max(1, Math.floor(Math.min(maxTileW, maxTileH)));
6684
+ const gridWidth = tileSize * GRID_COLS + gap * (GRID_COLS - 1);
6685
+ const gridHeight = tileSize * GRID_ROWS + gap * (GRID_ROWS - 1);
6686
+ const positions = /* @__PURE__ */ new Map();
6687
+ for (const { state, col, row } of US_STATE_TILES) {
6688
+ positions.set(state, {
6689
+ x: col * (tileSize + gap),
6690
+ y: row * (tileSize + gap)
6691
+ });
6692
+ }
6693
+ return { tileSize, gap, positions, gridWidth, gridHeight };
6694
+ }
6695
+
6696
+ // src/compiler/normalize.ts
6549
6697
  function normalizeChromeField(value2) {
6550
6698
  if (value2 === void 0) return void 0;
6551
6699
  if (typeof value2 === "string") return { text: value2 };
@@ -6665,6 +6813,12 @@ function normalizeChartSpec(spec, warnings) {
6665
6813
  const encoding = inferEncodingTypes(spec.encoding, spec.data, warnings);
6666
6814
  const markType = resolveMarkType(spec.mark);
6667
6815
  const markDef = resolveMarkDef(spec.mark);
6816
+ const display = spec.display ?? "full";
6817
+ if (display === "sparkline" && markType !== "line" && markType !== "area" && markType !== "bar" && markType !== "point") {
6818
+ warnings.push(
6819
+ `[openchart] display: 'sparkline' works best with mark: 'line' | 'area' | 'bar' | 'point'. Got mark: '${markType}' \u2014 rendering may degrade.`
6820
+ );
6821
+ }
6668
6822
  return {
6669
6823
  markType,
6670
6824
  markDef,
@@ -6679,7 +6833,20 @@ function normalizeChartSpec(spec, warnings) {
6679
6833
  darkMode: spec.darkMode ?? "off",
6680
6834
  hiddenSeries: spec.hiddenSeries ?? [],
6681
6835
  seriesStyles: spec.seriesStyles ?? {},
6682
- watermark: spec.watermark ?? true
6836
+ watermark: spec.watermark ?? true,
6837
+ display,
6838
+ // Default empty userExplicit; compileChart overwrites this with the real
6839
+ // descriptor built from the raw expanded spec before normalize runs.
6840
+ userExplicit: {
6841
+ chrome: false,
6842
+ legend: false,
6843
+ xAxis: false,
6844
+ yAxis: false,
6845
+ labels: false,
6846
+ animation: false,
6847
+ watermark: false,
6848
+ crosshair: false
6849
+ }
6683
6850
  };
6684
6851
  }
6685
6852
  function normalizeTableSpec(spec, _warnings) {
@@ -6746,6 +6913,45 @@ function normalizeGraphSpec(spec, _warnings) {
6746
6913
  watermark: spec.watermark ?? true
6747
6914
  };
6748
6915
  }
6916
+ function normalizeTileMapSpec(spec, warnings) {
6917
+ let data = Array.isArray(spec.data) ? spec.data : [];
6918
+ if (!Array.isArray(spec.data)) {
6919
+ data = Object.entries(spec.data).map(([state, value2]) => ({ state, value: value2 }));
6920
+ }
6921
+ let encoding = spec.encoding;
6922
+ if (!encoding) {
6923
+ encoding = {
6924
+ state: { field: "state", type: "nominal" },
6925
+ value: { field: "value", type: "quantitative" }
6926
+ };
6927
+ }
6928
+ let matchedCount = 0;
6929
+ for (const row of data) {
6930
+ const stateCode = String(row[encoding.state.field]);
6931
+ if (STATE_CODE_SET.has(stateCode)) {
6932
+ matchedCount++;
6933
+ }
6934
+ }
6935
+ const matchRatio = data.length > 0 ? matchedCount / data.length : 0;
6936
+ if (matchRatio < 0.5 && data.length > 0) {
6937
+ warnings.push(
6938
+ `TileMap data: only ${matchedCount} of ${data.length} rows have valid US state codes (expected \u226550%)`
6939
+ );
6940
+ }
6941
+ return {
6942
+ type: "tilemap",
6943
+ data,
6944
+ encoding,
6945
+ palette: spec.palette ?? "blue",
6946
+ chrome: normalizeChrome(spec.chrome),
6947
+ legend: spec.legend,
6948
+ theme: spec.theme ?? {},
6949
+ darkMode: spec.darkMode ?? "off",
6950
+ watermark: spec.watermark ?? true,
6951
+ animation: spec.animation,
6952
+ valueFormat: spec.valueFormat
6953
+ };
6954
+ }
6749
6955
  function normalizeSpec(spec, warnings = []) {
6750
6956
  if (isLayerSpec(spec)) {
6751
6957
  const leaves = flattenLayers(spec);
@@ -6766,8 +6972,11 @@ function normalizeSpec(spec, warnings = []) {
6766
6972
  if (isSankeySpec(spec)) {
6767
6973
  return normalizeSankeySpec(spec, warnings);
6768
6974
  }
6975
+ if (isTileMapSpec(spec)) {
6976
+ return normalizeTileMapSpec(spec, warnings);
6977
+ }
6769
6978
  throw new Error(
6770
- `Unknown spec shape. Expected mark (chart), layer, type: 'table', type: 'graph', or type: 'sankey'.`
6979
+ `Unknown spec shape. Expected mark (chart), layer, type: 'table', type: 'graph', type: 'sankey', or type: 'tilemap'.`
6771
6980
  );
6772
6981
  }
6773
6982
  function flattenLayers(spec, parentData, parentEncoding, parentTransforms, parentWatermark) {
@@ -7317,6 +7526,96 @@ function validateSankeySpec(spec, errors) {
7317
7526
  });
7318
7527
  }
7319
7528
  }
7529
+ function validateTileMapSpec(spec, errors) {
7530
+ if (!spec.data || typeof spec.data !== "object") {
7531
+ errors.push({
7532
+ message: 'Spec error: tilemap spec requires a "data" field (record or array)',
7533
+ path: "data",
7534
+ code: "INVALID_TYPE",
7535
+ suggestion: 'Provide data as either a record mapping state codes to values (e.g. { "CA": 12000, "TX": 8500 }) or an array of objects with state and value fields'
7536
+ });
7537
+ return;
7538
+ }
7539
+ if (!Array.isArray(spec.data) && Object.keys(spec.data).length === 0) {
7540
+ errors.push({
7541
+ message: 'Spec error: "data" must have at least one entry',
7542
+ path: "data",
7543
+ code: "EMPTY_DATA",
7544
+ suggestion: 'Add at least one state-value pair, e.g. { "CA": 12000 }'
7545
+ });
7546
+ return;
7547
+ }
7548
+ if (Array.isArray(spec.data)) {
7549
+ if (spec.data.length === 0) {
7550
+ errors.push({
7551
+ message: 'Spec error: "data" array must be non-empty',
7552
+ path: "data",
7553
+ code: "EMPTY_DATA",
7554
+ suggestion: "Add at least one data row"
7555
+ });
7556
+ return;
7557
+ }
7558
+ const firstRow = spec.data[0];
7559
+ if (typeof firstRow !== "object" || firstRow === null || Array.isArray(firstRow)) {
7560
+ errors.push({
7561
+ message: 'Spec error: each item in "data" must be a plain object',
7562
+ path: "data[0]",
7563
+ code: "INVALID_TYPE",
7564
+ suggestion: 'Each data item should be an object, e.g. { state: "CA", value: 12000 }'
7565
+ });
7566
+ return;
7567
+ }
7568
+ if (!spec.encoding || typeof spec.encoding !== "object") {
7569
+ errors.push({
7570
+ message: 'Spec error: tilemap spec with array data requires an "encoding" object with state and value channels',
7571
+ path: "encoding",
7572
+ code: "MISSING_FIELD",
7573
+ suggestion: 'Add an encoding object, e.g. encoding: { state: { field: "state", type: "nominal" }, value: { field: "value", type: "quantitative" } }'
7574
+ });
7575
+ return;
7576
+ }
7577
+ const encoding = spec.encoding;
7578
+ const dataColumns = new Set(Object.keys(firstRow));
7579
+ const availableColumns = [...dataColumns].join(", ");
7580
+ for (const channel of ["state", "value"]) {
7581
+ const ch = encoding[channel];
7582
+ if (!ch || typeof ch !== "object") {
7583
+ errors.push({
7584
+ message: `Spec error: tilemap encoding requires "${channel}" channel`,
7585
+ path: `encoding.${channel}`,
7586
+ code: "MISSING_FIELD",
7587
+ suggestion: `Add encoding.${channel} with a field from your data (${availableColumns}). Example: ${channel}: { field: "${[...dataColumns][0] ?? "myField"}", type: "${channel === "value" ? "quantitative" : "nominal"}" }`
7588
+ });
7589
+ continue;
7590
+ }
7591
+ if (!ch.field || typeof ch.field !== "string") {
7592
+ errors.push({
7593
+ message: `Spec error: encoding.${channel} must have a "field" string`,
7594
+ path: `encoding.${channel}.field`,
7595
+ code: "MISSING_FIELD",
7596
+ suggestion: `Add a field name from your data columns: ${availableColumns}`
7597
+ });
7598
+ continue;
7599
+ }
7600
+ if (!dataColumns.has(ch.field)) {
7601
+ errors.push({
7602
+ message: `Spec error: encoding.${channel}.field "${ch.field}" does not exist in data. Available columns: ${availableColumns}`,
7603
+ path: `encoding.${channel}.field`,
7604
+ code: "DATA_FIELD_MISSING",
7605
+ suggestion: `Use one of the available data columns: ${availableColumns}`
7606
+ });
7607
+ }
7608
+ }
7609
+ }
7610
+ if (spec.darkMode !== void 0 && !VALID_DARK_MODES.has(spec.darkMode)) {
7611
+ errors.push({
7612
+ message: 'Spec error: darkMode must be "auto", "force", or "off"',
7613
+ path: "darkMode",
7614
+ code: "INVALID_VALUE",
7615
+ suggestion: 'Use one of: "auto" (system preference), "force" (always dark), or "off" (always light)'
7616
+ });
7617
+ }
7618
+ }
7320
7619
  function validateLayerSpec(spec, errors) {
7321
7620
  const layer = spec.layer;
7322
7621
  if (layer.length === 0) {
@@ -7411,17 +7710,18 @@ function validateSpec(spec) {
7411
7710
  const isTable = obj.type === "table";
7412
7711
  const isGraph = obj.type === "graph";
7413
7712
  const isSankey = obj.type === "sankey";
7414
- const isLayer = hasLayer && !isTable && !isGraph && !isSankey;
7415
- const isChart = hasMark && !hasLayer && !isTable && !isGraph && !isSankey;
7416
- if (!isChart && !isTable && !isGraph && !isSankey && !isLayer) {
7713
+ const isTileMap = obj.type === "tilemap";
7714
+ const isLayer = hasLayer && !isTable && !isGraph && !isSankey && !isTileMap;
7715
+ const isChart = hasMark && !hasLayer && !isTable && !isGraph && !isSankey && !isTileMap;
7716
+ if (!isChart && !isTable && !isGraph && !isSankey && !isTileMap && !isLayer) {
7417
7717
  return {
7418
7718
  valid: false,
7419
7719
  errors: [
7420
7720
  {
7421
- message: 'Spec error: spec must have a "mark" field for charts, a "layer" array for layered charts, or a "type" field for tables/graphs/sankey',
7721
+ message: 'Spec error: spec must have a "mark" field for charts, a "layer" array for layered charts, or a "type" field for tables/graphs/sankey/tilemap',
7422
7722
  path: "mark",
7423
7723
  code: "MISSING_FIELD",
7424
- suggestion: `Add a "mark" field for charts (e.g. mark: "bar"), a "layer" array for layered charts, or a "type" field for tables/graphs/sankey (type: "table", type: "graph", or type: "sankey"). Valid mark types: ${[...MARK_TYPES].join(", ")}`
7724
+ suggestion: `Add a "mark" field for charts (e.g. mark: "bar"), a "layer" array for layered charts, or a "type" field for tables/graphs/sankey/tilemap (type: "table", type: "graph", type: "sankey", or type: "tilemap"). Valid mark types: ${[...MARK_TYPES].join(", ")}`
7425
7725
  }
7426
7726
  ],
7427
7727
  normalized: null
@@ -7459,6 +7759,8 @@ function validateSpec(spec) {
7459
7759
  validateGraphSpec(obj, errors);
7460
7760
  } else if (isSankey) {
7461
7761
  validateSankeySpec(obj, errors);
7762
+ } else if (isTileMap) {
7763
+ validateTileMapSpec(obj, errors);
7462
7764
  }
7463
7765
  if (errors.length > 0) {
7464
7766
  return { valid: false, errors, normalized: null };
@@ -8021,7 +8323,7 @@ function scaleSupportsTickCount(resolvedScale) {
8021
8323
  const scale = resolvedScale.scale;
8022
8324
  return "ticks" in scale && typeof scale.ticks === "function";
8023
8325
  }
8024
- function categoricalTicks(resolvedScale, density, orientation = "horizontal", bandwidth, labelAngle, fontSize, fontWeight, measureText) {
8326
+ function categoricalTicks(resolvedScale, density, orientation = "horizontal", bandwidth, labelAngle, fontSize, fontWeight, measureText, subtitleContext) {
8025
8327
  const scale = resolvedScale.scale;
8026
8328
  const domain = scale.domain();
8027
8329
  const explicitTickCount = resolvedScale.channel.axis?.tickCount;
@@ -8057,14 +8359,37 @@ function categoricalTicks(resolvedScale, density, orientation = "horizontal", ba
8057
8359
  selectedValues = domain.filter((_, i) => i % step === 0);
8058
8360
  }
8059
8361
  }
8362
+ let subtitleMap;
8363
+ if (subtitleContext) {
8364
+ const { data, fieldName, labelField } = subtitleContext;
8365
+ if (data.length > 0) {
8366
+ subtitleMap = /* @__PURE__ */ new Map();
8367
+ for (const row of data) {
8368
+ const key = String(row[fieldName] ?? "");
8369
+ if (!subtitleMap.has(key)) {
8370
+ const val = row[labelField];
8371
+ if (val != null) {
8372
+ subtitleMap.set(key, String(val));
8373
+ }
8374
+ }
8375
+ }
8376
+ }
8377
+ }
8060
8378
  const ticks2 = selectedValues.map((value2) => {
8061
8379
  const bandScale = resolvedScale.type === "band" ? scale : null;
8062
8380
  const pos = bandScale ? (bandScale(value2) ?? 0) + bandScale.bandwidth() / 2 : scale(value2) ?? 0;
8063
- return {
8381
+ const tick = {
8064
8382
  value: value2,
8065
8383
  position: pos,
8066
8384
  label: value2
8067
8385
  };
8386
+ if (subtitleMap) {
8387
+ const subtitle = subtitleMap.get(value2);
8388
+ if (subtitle !== void 0) {
8389
+ tick.subtitle = subtitle;
8390
+ }
8391
+ }
8392
+ return tick;
8068
8393
  });
8069
8394
  return ticks2;
8070
8395
  }
@@ -8136,7 +8461,7 @@ function fitContinuousTicks(scale, initialTicks, initialCount, fontSize, fontWei
8136
8461
  const fallback = bestWithinFloor ?? buildContinuousTicks(scale, floor);
8137
8462
  return thinTicksUntilFit(fallback, fontSize, fontWeight, measureText, orientation);
8138
8463
  }
8139
- function computeAxes(scales, chartArea, strategy, theme, measureText) {
8464
+ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContext) {
8140
8465
  const result = {};
8141
8466
  const baseDensity = strategy.axisLabelDensity;
8142
8467
  const yDensity = effectiveDensity(
@@ -8168,7 +8493,7 @@ function computeAxes(scales, chartArea, strategy, theme, measureText) {
8168
8493
  };
8169
8494
  const { fontSize } = tickLabelStyle;
8170
8495
  const { fontWeight } = tickLabelStyle;
8171
- if (scales.x) {
8496
+ if (scales.x && !dataContext?.skipX) {
8172
8497
  const axisConfig = scales.x.channel.axis;
8173
8498
  const isContinuousX = scales.x.type !== "band" && scales.x.type !== "point" && scales.x.type !== "ordinal";
8174
8499
  const xTargetCount = isContinuousX ? targetTickCount(chartArea.width, xDensity, "x") : void 0;
@@ -8246,7 +8571,7 @@ function computeAxes(scales, chartArea, strategy, theme, measureText) {
8246
8571
  labelFlush: axisConfig?.labelFlush
8247
8572
  };
8248
8573
  }
8249
- if (scales.y) {
8574
+ if (scales.y && !dataContext?.skipY) {
8250
8575
  const axisConfig = scales.y.channel.axis;
8251
8576
  const isContinuousY = scales.y.type !== "band" && scales.y.type !== "point" && scales.y.type !== "ordinal";
8252
8577
  const yTargetCount = isContinuousY ? targetTickCount(chartArea.height, yDensity, "y") : void 0;
@@ -8254,7 +8579,19 @@ function computeAxes(scales, chartArea, strategy, theme, measureText) {
8254
8579
  if (axisConfig?.values) {
8255
8580
  allTicks = resolveExplicitTicks(axisConfig.values, scales.y);
8256
8581
  } else if (!isContinuousY) {
8257
- allTicks = categoricalTicks(scales.y, yDensity, "vertical");
8582
+ const yFieldName = dataContext?.encoding.y?.field;
8583
+ const yLabelField = axisConfig?.labelField;
8584
+ allTicks = categoricalTicks(
8585
+ scales.y,
8586
+ yDensity,
8587
+ "vertical",
8588
+ void 0,
8589
+ void 0,
8590
+ void 0,
8591
+ void 0,
8592
+ void 0,
8593
+ yFieldName && yLabelField && dataContext ? { data: dataContext.data, fieldName: yFieldName, labelField: yLabelField } : void 0
8594
+ );
8258
8595
  } else {
8259
8596
  allTicks = continuousTicks(scales.y, yDensity, yTargetCount);
8260
8597
  }
@@ -8386,12 +8723,24 @@ function scalePadding(basePadding, width, height) {
8386
8723
  }
8387
8724
  var MIN_CHART_WIDTH = 60;
8388
8725
  var MIN_CHART_HEIGHT = 40;
8726
+ function getMinChartDims(display) {
8727
+ return display === "sparkline" ? { width: 30, height: 20 } : { width: MIN_CHART_WIDTH, height: MIN_CHART_HEIGHT };
8728
+ }
8729
+ function getSparklinePad(spec) {
8730
+ const strokeWidth = spec.markDef.strokeWidth ?? 2;
8731
+ return Math.max(strokeWidth / 2 + 1, 2);
8732
+ }
8389
8733
  function computeDimensions(spec, options, legendLayout, theme, strategy, watermark = true) {
8390
8734
  const { width, height } = options;
8391
8735
  const padding = scalePadding(theme.spacing.padding, width, height);
8392
8736
  const hPad = width < BREAKPOINT_COMPACT_MAX ? Math.max(Math.round(padding * HPAD_COMPACT_FRACTION), HPAD_COMPACT_MIN) : padding;
8393
8737
  const axisMargin = theme.spacing.axisMargin;
8394
- const chromeMode = strategy?.chromeMode ?? "full";
8738
+ const userExplicit = spec.userExplicit;
8739
+ const isSparkline = spec.display === "sparkline";
8740
+ let chromeMode = strategy?.chromeMode ?? "full";
8741
+ if (isSparkline && !userExplicit.chrome) {
8742
+ chromeMode = "hidden";
8743
+ }
8395
8744
  const chrome = computeChrome2(
8396
8745
  chromeToInput(spec.chrome),
8397
8746
  theme,
@@ -8401,6 +8750,35 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
8401
8750
  padding,
8402
8751
  watermark
8403
8752
  );
8753
+ if (isSparkline) {
8754
+ const total2 = { x: 0, y: 0, width, height };
8755
+ const sparkPad = getSparklinePad(spec);
8756
+ const xAxisSpace = userExplicit.xAxis ? 26 : 0;
8757
+ const yAxisSpace = userExplicit.yAxis ? 30 : 0;
8758
+ const margins2 = {
8759
+ top: chrome.topHeight + sparkPad,
8760
+ right: sparkPad,
8761
+ bottom: chrome.bottomHeight + sparkPad + xAxisSpace,
8762
+ left: sparkPad + yAxisSpace
8763
+ };
8764
+ if (userExplicit.legend && "entries" in legendLayout && legendLayout.entries.length > 0) {
8765
+ const gap = legendGap(width);
8766
+ if (legendLayout.position === "right" || legendLayout.position === "bottom-right") {
8767
+ margins2.right += legendLayout.bounds.width + 8;
8768
+ } else if (legendLayout.position === "top") {
8769
+ margins2.top += legendLayout.bounds.height + gap;
8770
+ } else if (legendLayout.position === "bottom") {
8771
+ margins2.bottom += legendLayout.bounds.height + gap;
8772
+ }
8773
+ }
8774
+ const chartArea2 = {
8775
+ x: margins2.left,
8776
+ y: margins2.top,
8777
+ width: Math.max(0, width - margins2.left - margins2.right),
8778
+ height: Math.max(0, height - margins2.top - margins2.bottom)
8779
+ };
8780
+ return { total: total2, chrome, chartArea: chartArea2, margins: margins2, theme };
8781
+ }
8404
8782
  const total = { x: 0, y: 0, width, height };
8405
8783
  const isRadial = spec.markType === "arc";
8406
8784
  const encoding = spec.encoding;
@@ -8488,10 +8866,23 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
8488
8866
  if (encoding.y && !isRadial) {
8489
8867
  if (spec.markType === "bar" || spec.markType === "circle" || spec.markType === "lollipop" || encoding.y.type === "nominal" || encoding.y.type === "ordinal") {
8490
8868
  const yField = encoding.y.field;
8869
+ const yLabelField = encoding.y.axis?.labelField;
8491
8870
  let maxLabelWidth = 0;
8492
8871
  for (const row of spec.data) {
8493
8872
  const label = String(row[yField] ?? "");
8494
- const w = estimateTextWidth10(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
8873
+ let w = estimateTextWidth10(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
8874
+ if (yLabelField) {
8875
+ const subtitle = String(row[yLabelField] ?? "");
8876
+ if (subtitle) {
8877
+ const gap = theme.fonts.sizes.axisTick * 0.6;
8878
+ const subtitleWidth = estimateTextWidth10(
8879
+ subtitle,
8880
+ theme.fonts.sizes.axisTick,
8881
+ theme.fonts.weights.normal
8882
+ );
8883
+ w += gap + subtitleWidth;
8884
+ }
8885
+ }
8495
8886
  if (w > maxLabelWidth) maxLabelWidth = w;
8496
8887
  }
8497
8888
  if (maxLabelWidth > 0) {
@@ -8545,7 +8936,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
8545
8936
  if (options.rightAxisReserve && options.rightAxisReserve > 0) {
8546
8937
  margins.right = Math.max(margins.right, hPad + options.rightAxisReserve);
8547
8938
  }
8548
- if (legendLayout.entries.length > 0) {
8939
+ if ("entries" in legendLayout && legendLayout.entries.length > 0) {
8549
8940
  const gap = legendGap(width);
8550
8941
  if (legendLayout.position === "right" || legendLayout.position === "bottom-right") {
8551
8942
  margins.right += legendLayout.bounds.width + 8;
@@ -8561,7 +8952,8 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
8561
8952
  width: Math.max(0, width - margins.left - margins.right),
8562
8953
  height: Math.max(0, height - margins.top - margins.bottom)
8563
8954
  };
8564
- if ((chartArea.width < MIN_CHART_WIDTH || chartArea.height < MIN_CHART_HEIGHT) && chromeMode !== "hidden") {
8955
+ const minDims = getMinChartDims(spec.display);
8956
+ if ((chartArea.width < minDims.width || chartArea.height < minDims.height) && chromeMode !== "hidden") {
8565
8957
  const fallbackMode = chromeMode === "full" ? "compact" : "hidden";
8566
8958
  const fallbackChrome = computeChrome2(
8567
8959
  chromeToInput(spec.chrome),
@@ -8579,7 +8971,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
8579
8971
  const bottomDelta = margins.bottom - newBottom;
8580
8972
  if (topDelta > 0 || bottomDelta > 0) {
8581
8973
  const gap = legendGap(width);
8582
- margins.top = newTop + (legendLayout.entries.length > 0 && legendLayout.position === "top" ? legendLayout.bounds.height + gap : 0);
8974
+ margins.top = newTop + ("entries" in legendLayout && legendLayout.entries.length > 0 && legendLayout.position === "top" ? legendLayout.bounds.height + gap : 0);
8583
8975
  margins.bottom = newBottom;
8584
8976
  chartArea = {
8585
8977
  x: margins.left,
@@ -9108,7 +9500,8 @@ function truncateEntries(entries, maxCount) {
9108
9500
  return truncated;
9109
9501
  }
9110
9502
  function computeLegend(spec, strategy, theme, chartArea, watermark = true) {
9111
- if (spec.legend?.show === false || strategy.legendMaxHeight === 0) {
9503
+ const sparklineHidden = spec.display === "sparkline" && !spec.userExplicit.legend;
9504
+ if (sparklineHidden || spec.legend?.show === false || strategy.legendMaxHeight === 0) {
9112
9505
  return {
9113
9506
  position: "top",
9114
9507
  entries: [],
@@ -9904,7 +10297,7 @@ function compileSankey(spec, options) {
9904
10297
  theme,
9905
10298
  fullArea
9906
10299
  );
9907
- const legendGap2 = legend.entries.length > 0 ? 4 : 0;
10300
+ const legendGap2 = "entries" in legend && legend.entries.length > 0 ? 4 : 0;
9908
10301
  const area = {
9909
10302
  x: fullArea.x,
9910
10303
  y: fullArea.y + legend.bounds.height + legendGap2,
@@ -10847,11 +11240,266 @@ function compileTableLayout(spec, options, theme) {
10847
11240
  };
10848
11241
  }
10849
11242
 
11243
+ // src/tilemap/compile-tilemap.ts
11244
+ import {
11245
+ adaptTheme as adaptTheme3,
11246
+ buildD3Formatter as buildD3Formatter7,
11247
+ computeChrome as computeChrome5,
11248
+ estimateTextWidth as estimateTextWidth14,
11249
+ formatNumber as formatNumber5,
11250
+ resolveTheme as resolveTheme3,
11251
+ SEQUENTIAL_PALETTES
11252
+ } from "@opendata-ai/openchart-core";
11253
+ var TILE_CORNER_RADIUS = 2;
11254
+ var TILE_STROKE_WIDTH = 0;
11255
+ function compileTileMap(spec, options) {
11256
+ const { spec: normalized } = compile(spec);
11257
+ if (!("type" in normalized) || normalized.type !== "tilemap") {
11258
+ throw new Error(
11259
+ "compileTileMap received a non-tilemap spec. Use compileChart, compileTable, compileGraph, or compileSankey instead."
11260
+ );
11261
+ }
11262
+ const tilemapSpec = normalized;
11263
+ const rawWatermark = spec.watermark;
11264
+ const watermark = rawWatermark !== void 0 ? tilemapSpec.watermark : options.watermark ?? true;
11265
+ const mergedThemeConfig = options.theme ? { ...tilemapSpec.theme, ...options.theme } : tilemapSpec.theme;
11266
+ const lightTheme = resolveTheme3(mergedThemeConfig);
11267
+ let theme = lightTheme;
11268
+ if (options.darkMode) {
11269
+ theme = adaptTheme3(theme);
11270
+ }
11271
+ const isDarkMode = options.darkMode;
11272
+ const chrome = computeChrome5(
11273
+ {
11274
+ title: tilemapSpec.chrome.title,
11275
+ subtitle: tilemapSpec.chrome.subtitle,
11276
+ source: tilemapSpec.chrome.source,
11277
+ byline: tilemapSpec.chrome.byline,
11278
+ footer: tilemapSpec.chrome.footer
11279
+ },
11280
+ theme,
11281
+ options.width,
11282
+ options.measureText,
11283
+ "full",
11284
+ void 0,
11285
+ watermark
11286
+ );
11287
+ const padding = theme.spacing.padding;
11288
+ const fullArea = {
11289
+ x: padding,
11290
+ y: padding + chrome.topHeight,
11291
+ width: options.width - padding * 2,
11292
+ height: options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
11293
+ };
11294
+ if (fullArea.width <= 0 || fullArea.height <= 0) {
11295
+ return emptyLayout2(chrome, theme, options, watermark);
11296
+ }
11297
+ const stateField = tilemapSpec.encoding.state.field;
11298
+ const valueField = tilemapSpec.encoding.value.field;
11299
+ const stateValueMap = /* @__PURE__ */ new Map();
11300
+ for (const row of tilemapSpec.data) {
11301
+ const stateCode = String(row[stateField]);
11302
+ const raw = row[valueField];
11303
+ if (STATE_CODE_SET.has(stateCode) && raw !== null && raw !== void 0) {
11304
+ const value2 = Number(raw);
11305
+ if (!Number.isNaN(value2)) {
11306
+ stateValueMap.set(stateCode, value2);
11307
+ }
11308
+ }
11309
+ }
11310
+ const values = Array.from(stateValueMap.values());
11311
+ const min4 = values.length > 0 ? Math.min(...values) : 0;
11312
+ const max4 = values.length > 0 ? Math.max(...values) : 100;
11313
+ const paletteStops = [...SEQUENTIAL_PALETTES[tilemapSpec.palette] ?? SEQUENTIAL_PALETTES.blue];
11314
+ if (isDarkMode) paletteStops.reverse();
11315
+ const domain = paletteStops.map((_, i) => min4 + i / (paletteStops.length - 1) * (max4 - min4));
11316
+ const colorScale = linear2().domain(domain).range(paletteStops).clamp(true);
11317
+ const showLegend = tilemapSpec.legend?.show !== false;
11318
+ const legendBarHeight = 12;
11319
+ const legendLabelGap = 4;
11320
+ const legendTotalHeight = showLegend ? legendBarHeight + legendLabelGap + 14 : 0;
11321
+ const legendGap2 = showLegend ? 8 : 0;
11322
+ const tileAreaHeight = fullArea.height - legendTotalHeight - legendGap2;
11323
+ const tilePositions = computeTilePositions(fullArea.width, tileAreaHeight, 4);
11324
+ const tileGridOffsetX = fullArea.x + (fullArea.width - tilePositions.gridWidth) / 2;
11325
+ const tileGridOffsetY = fullArea.y;
11326
+ const legendX = tileGridOffsetX;
11327
+ const legendY = tileGridOffsetY + tilePositions.gridHeight + legendGap2;
11328
+ const legendWidth = tilePositions.gridWidth;
11329
+ const formatter = buildD3Formatter7(tilemapSpec.valueFormat) ?? formatNumber5;
11330
+ const neutralFillLight = "#e0e0e0";
11331
+ const neutralFillDark = "#2a2a3e";
11332
+ const neutralStrokeLight = "#d0d0d0";
11333
+ const neutralStrokeDark = "#3a3a50";
11334
+ const neutralFill = isDarkMode ? neutralFillDark : neutralFillLight;
11335
+ const neutralStroke = isDarkMode ? neutralStrokeDark : neutralStrokeLight;
11336
+ const tiles = [];
11337
+ for (const { state: stateCode } of US_STATE_TILES) {
11338
+ const pos = tilePositions.positions.get(stateCode);
11339
+ if (!pos) continue;
11340
+ const hasData = stateValueMap.has(stateCode);
11341
+ const value2 = hasData ? stateValueMap.get(stateCode) : null;
11342
+ const fill = hasData ? colorScale(value2) : neutralFill;
11343
+ const formattedValue = hasData ? formatter(value2) : "\u2013";
11344
+ const labelStyle = {
11345
+ fontFamily: theme.fonts.family,
11346
+ fontSize: tilePositions.tileSize > 24 ? 14 : 11,
11347
+ fontWeight: 700,
11348
+ fill: "#ffffff",
11349
+ lineHeight: 1.2
11350
+ };
11351
+ const valueLabelStyle = {
11352
+ fontFamily: theme.fonts.family,
11353
+ fontSize: tilePositions.tileSize > 24 ? 12 : 10,
11354
+ fontWeight: 400,
11355
+ fill: "#ffffff",
11356
+ lineHeight: 1.2
11357
+ };
11358
+ const valueLabel = tilePositions.tileSize < 24 ? { text: "", x: 0, y: 0, style: valueLabelStyle, visible: false } : {
11359
+ text: formattedValue,
11360
+ x: tileGridOffsetX + pos.x + tilePositions.tileSize / 2,
11361
+ y: tileGridOffsetY + pos.y + tilePositions.tileSize / 2 + 8,
11362
+ style: valueLabelStyle,
11363
+ visible: true
11364
+ };
11365
+ const tile = {
11366
+ type: "tile",
11367
+ stateCode,
11368
+ x: tileGridOffsetX + pos.x,
11369
+ y: tileGridOffsetY + pos.y,
11370
+ size: tilePositions.tileSize,
11371
+ fill,
11372
+ stroke: neutralStroke,
11373
+ strokeWidth: TILE_STROKE_WIDTH,
11374
+ cornerRadius: TILE_CORNER_RADIUS,
11375
+ value: value2 ?? null,
11376
+ formattedValue,
11377
+ hasData,
11378
+ label: {
11379
+ text: stateCode,
11380
+ x: tileGridOffsetX + pos.x + tilePositions.tileSize / 2,
11381
+ y: tileGridOffsetY + pos.y + tilePositions.tileSize / 2 - 4,
11382
+ style: labelStyle,
11383
+ visible: true
11384
+ },
11385
+ valueLabel,
11386
+ data: { state: stateCode, value: value2, stateName: STATE_NAMES[stateCode] ?? stateCode },
11387
+ aria: {
11388
+ role: "img",
11389
+ label: `${STATE_NAMES[stateCode] ?? stateCode}: ${formattedValue}`
11390
+ },
11391
+ animationIndex: 0
11392
+ };
11393
+ tiles.push(tile);
11394
+ }
11395
+ const indices = Array.from({ length: tiles.length }, (_, i) => i);
11396
+ let seed = 42;
11397
+ for (let i = indices.length - 1; i > 0; i--) {
11398
+ seed = seed * 1103515245 + 12345 & 2147483647;
11399
+ const j = seed % (i + 1);
11400
+ [indices[i], indices[j]] = [indices[j], indices[i]];
11401
+ }
11402
+ for (let i = 0; i < tiles.length; i++) {
11403
+ tiles[i].animationIndex = indices[i];
11404
+ }
11405
+ let gradientLegend = null;
11406
+ if (showLegend) {
11407
+ const gradientColorStops = paletteStops.map((color2, i) => ({
11408
+ offset: i / (paletteStops.length - 1),
11409
+ color: color2
11410
+ }));
11411
+ gradientLegend = {
11412
+ type: "gradient",
11413
+ position: "bottom",
11414
+ bounds: { x: legendX, y: legendY, width: legendWidth, height: legendBarHeight },
11415
+ labelStyle: {
11416
+ fontFamily: theme.fonts.family,
11417
+ fontSize: 11,
11418
+ fontWeight: 400,
11419
+ fill: theme.colors.text,
11420
+ lineHeight: 1.2
11421
+ },
11422
+ colorStops: gradientColorStops,
11423
+ minLabel: formatter(min4),
11424
+ maxLabel: formatter(max4)
11425
+ };
11426
+ }
11427
+ const tooltipDescriptors = /* @__PURE__ */ new Map();
11428
+ for (const tile of tiles) {
11429
+ const fields = [
11430
+ {
11431
+ label: "Value",
11432
+ value: tile.formattedValue
11433
+ }
11434
+ ];
11435
+ tooltipDescriptors.set(tile.stateCode, {
11436
+ title: STATE_NAMES[tile.stateCode] ?? tile.stateCode,
11437
+ fields
11438
+ });
11439
+ }
11440
+ const a11y = {
11441
+ altText: `Tile map of US states showing values from ${formatter(min4)} to ${formatter(max4)}`,
11442
+ dataTableFallback: tiles.map((t) => [t.stateCode, t.formattedValue]),
11443
+ role: "img",
11444
+ keyboardNavigable: tiles.length > 0
11445
+ };
11446
+ const resolvedAnimation = resolveAnimation(tilemapSpec.animation);
11447
+ return {
11448
+ area: fullArea,
11449
+ chrome,
11450
+ tiles,
11451
+ gradientLegend,
11452
+ tooltipDescriptors,
11453
+ a11y,
11454
+ theme,
11455
+ width: options.width,
11456
+ height: options.height,
11457
+ animation: resolvedAnimation,
11458
+ watermark,
11459
+ measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth14(text, fontSize), height: fontSize }))
11460
+ };
11461
+ }
11462
+ function emptyLayout2(chrome, theme, options, watermark) {
11463
+ return {
11464
+ area: { x: 0, y: 0, width: 0, height: 0 },
11465
+ chrome,
11466
+ tiles: [],
11467
+ gradientLegend: {
11468
+ type: "gradient",
11469
+ position: "bottom",
11470
+ bounds: { x: 0, y: 0, width: 0, height: 0 },
11471
+ labelStyle: {
11472
+ fontFamily: theme.fonts.family,
11473
+ fontSize: 11,
11474
+ fontWeight: 400,
11475
+ fill: theme.colors.text,
11476
+ lineHeight: 1.2
11477
+ },
11478
+ colorStops: [],
11479
+ minLabel: "0",
11480
+ maxLabel: "0"
11481
+ },
11482
+ tooltipDescriptors: /* @__PURE__ */ new Map(),
11483
+ a11y: {
11484
+ altText: "Empty tile map",
11485
+ dataTableFallback: [],
11486
+ role: "img",
11487
+ keyboardNavigable: false
11488
+ },
11489
+ theme,
11490
+ width: options.width,
11491
+ height: options.height,
11492
+ watermark,
11493
+ animation: void 0,
11494
+ measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth14(text, fontSize), height: fontSize }))
11495
+ };
11496
+ }
11497
+
10850
11498
  // src/tooltips/compute.ts
10851
11499
  import {
10852
11500
  buildTemporalFormatter as buildTemporalFormatter2,
10853
11501
  formatDate as formatDate3,
10854
- formatNumber as formatNumber5,
11502
+ formatNumber as formatNumber6,
10855
11503
  getRepresentativeColor as getRepresentativeColor10
10856
11504
  } from "@opendata-ai/openchart-core";
10857
11505
  function formatValue(value2, fieldType, format2) {
@@ -10866,10 +11514,10 @@ function formatValue(value2, fieldType, format2) {
10866
11514
  try {
10867
11515
  return format(format2)(value2);
10868
11516
  } catch {
10869
- return formatNumber5(value2);
11517
+ return formatNumber6(value2);
10870
11518
  }
10871
11519
  }
10872
- return formatNumber5(value2);
11520
+ return formatNumber6(value2);
10873
11521
  }
10874
11522
  return String(value2);
10875
11523
  }
@@ -11219,8 +11867,78 @@ function runCalculate(data, transform) {
11219
11867
  }
11220
11868
 
11221
11869
  // src/transforms/filter.ts
11870
+ function applyOffset2(anchor, offset, unit2) {
11871
+ const d = new Date(anchor.getTime());
11872
+ switch (unit2) {
11873
+ case "year":
11874
+ d.setFullYear(d.getFullYear() + offset);
11875
+ break;
11876
+ case "quarter":
11877
+ d.setMonth(d.getMonth() + offset * 3);
11878
+ break;
11879
+ case "month":
11880
+ d.setMonth(d.getMonth() + offset);
11881
+ break;
11882
+ case "week":
11883
+ d.setDate(d.getDate() + offset * 7);
11884
+ break;
11885
+ case "day":
11886
+ d.setDate(d.getDate() + offset);
11887
+ break;
11888
+ }
11889
+ return d.getTime();
11890
+ }
11891
+ function resolveRef(data, field, ref) {
11892
+ let anchorMs = ref.anchor === "max" ? -Infinity : Infinity;
11893
+ for (const row of data) {
11894
+ const val = row[field];
11895
+ if (val == null) continue;
11896
+ const ms = new Date(val).getTime();
11897
+ if (Number.isNaN(ms)) continue;
11898
+ if (ref.anchor === "max" && ms > anchorMs) anchorMs = ms;
11899
+ if (ref.anchor === "min" && ms < anchorMs) anchorMs = ms;
11900
+ }
11901
+ if (!Number.isFinite(anchorMs)) return 0;
11902
+ return applyOffset2(new Date(anchorMs), ref.offset, ref.unit);
11903
+ }
11904
+ function resolveRelativeRefs(data, predicate) {
11905
+ if ("and" in predicate) {
11906
+ return { and: predicate.and.map((p) => resolveRelativeRefs(data, p)) };
11907
+ }
11908
+ if ("or" in predicate) {
11909
+ return { or: predicate.or.map((p) => resolveRelativeRefs(data, p)) };
11910
+ }
11911
+ if ("not" in predicate) {
11912
+ return { not: resolveRelativeRefs(data, predicate.not) };
11913
+ }
11914
+ if ("field" in predicate) {
11915
+ const fp = predicate;
11916
+ let needsCopy = false;
11917
+ const resolved = {};
11918
+ for (const prop of ["lt", "lte", "gt", "gte"]) {
11919
+ if (isRelativeTimeRef(fp[prop])) {
11920
+ resolved[prop] = resolveRef(data, fp.field, fp[prop]);
11921
+ needsCopy = true;
11922
+ }
11923
+ }
11924
+ if (fp.range) {
11925
+ const [lo, hi] = fp.range;
11926
+ const loResolved = isRelativeTimeRef(lo) ? resolveRef(data, fp.field, lo) : lo;
11927
+ const hiResolved = isRelativeTimeRef(hi) ? resolveRef(data, fp.field, hi) : hi;
11928
+ if (isRelativeTimeRef(lo) || isRelativeTimeRef(hi)) {
11929
+ resolved.range = [loResolved, hiResolved];
11930
+ needsCopy = true;
11931
+ }
11932
+ }
11933
+ if (needsCopy) {
11934
+ return { ...fp, ...resolved };
11935
+ }
11936
+ }
11937
+ return predicate;
11938
+ }
11222
11939
  function runFilter(data, predicate) {
11223
- return data.filter((datum) => evaluatePredicate(datum, predicate));
11940
+ const resolved = resolveRelativeRefs(data, predicate);
11941
+ return data.filter((datum) => evaluatePredicate(datum, resolved));
11224
11942
  }
11225
11943
 
11226
11944
  // src/transforms/fold.ts
@@ -11312,6 +12030,131 @@ function runTimeUnit(data, transform) {
11312
12030
  });
11313
12031
  }
11314
12032
 
12033
+ // src/transforms/window.ts
12034
+ function groupKey2(row, groupby) {
12035
+ return groupby.map((f) => String(row[f] ?? "")).join("\0");
12036
+ }
12037
+ function tryParseDate(val) {
12038
+ if (val == null) return NaN;
12039
+ if (typeof val === "number") return new Date(val).getTime();
12040
+ if (typeof val === "string" && (val.includes("-") || val.includes("T"))) {
12041
+ const ms = new Date(val).getTime();
12042
+ return ms;
12043
+ }
12044
+ return NaN;
12045
+ }
12046
+ function compareValues(a, b, order) {
12047
+ const dir = order === "descending" ? -1 : 1;
12048
+ const dateA = tryParseDate(a);
12049
+ const dateB = tryParseDate(b);
12050
+ if (!Number.isNaN(dateA) && !Number.isNaN(dateB)) {
12051
+ return dir * (dateA - dateB);
12052
+ }
12053
+ const numA = Number(a);
12054
+ const numB = Number(b);
12055
+ if (Number.isFinite(numA) && Number.isFinite(numB)) {
12056
+ return dir * (numA - numB);
12057
+ }
12058
+ return dir * String(a ?? "").localeCompare(String(b ?? ""));
12059
+ }
12060
+ function runWindow(data, transform) {
12061
+ if (data.length === 0) return [];
12062
+ const { window: windowDefs, sort, groupby = [] } = transform;
12063
+ const indexed = data.map((row, i) => ({ row, originalIndex: i }));
12064
+ const groups = /* @__PURE__ */ new Map();
12065
+ for (const entry of indexed) {
12066
+ const key = groupby.length > 0 ? groupKey2(entry.row, groupby) : "";
12067
+ const existing = groups.get(key);
12068
+ if (existing) {
12069
+ existing.push(entry);
12070
+ } else {
12071
+ groups.set(key, [entry]);
12072
+ }
12073
+ }
12074
+ const result = new Array(data.length);
12075
+ for (const groupEntries of groups.values()) {
12076
+ const sorted = [...groupEntries].sort((a, b) => {
12077
+ for (const s of sort) {
12078
+ const cmp = compareValues(a.row[s.field], b.row[s.field], s.order ?? "ascending");
12079
+ if (cmp !== 0) return cmp;
12080
+ }
12081
+ return 0;
12082
+ });
12083
+ for (let i = 0; i < sorted.length; i++) {
12084
+ const entry = sorted[i];
12085
+ const outRow = { ...entry.row };
12086
+ for (const def of windowDefs) {
12087
+ const offset = def.offset ?? 1;
12088
+ let computed = null;
12089
+ switch (def.op) {
12090
+ case "lag": {
12091
+ const lagIdx = i - offset;
12092
+ computed = lagIdx >= 0 ? sorted[lagIdx].row[def.field] ?? null : null;
12093
+ break;
12094
+ }
12095
+ case "lead": {
12096
+ const leadIdx = i + offset;
12097
+ computed = leadIdx < sorted.length ? sorted[leadIdx].row[def.field] ?? null : null;
12098
+ break;
12099
+ }
12100
+ case "diff": {
12101
+ const lagIdx = i - offset;
12102
+ if (lagIdx >= 0) {
12103
+ const current = Number(entry.row[def.field]);
12104
+ const lagged = Number(sorted[lagIdx].row[def.field]);
12105
+ computed = Number.isFinite(current) && Number.isFinite(lagged) ? current - lagged : null;
12106
+ }
12107
+ break;
12108
+ }
12109
+ case "pct_change": {
12110
+ const lagIdx = i - offset;
12111
+ if (lagIdx >= 0) {
12112
+ const current = Number(entry.row[def.field]);
12113
+ const lagged = Number(sorted[lagIdx].row[def.field]);
12114
+ if (Number.isFinite(current) && Number.isFinite(lagged) && lagged !== 0) {
12115
+ computed = (current - lagged) / lagged;
12116
+ }
12117
+ }
12118
+ break;
12119
+ }
12120
+ case "cumsum": {
12121
+ const val = Number(entry.row[def.field]);
12122
+ const addend = Number.isFinite(val) ? val : 0;
12123
+ if (i === 0) {
12124
+ computed = addend;
12125
+ } else {
12126
+ const prev = Number(result[sorted[i - 1].originalIndex]?.[def.as] ?? 0);
12127
+ computed = prev + addend;
12128
+ }
12129
+ break;
12130
+ }
12131
+ case "rank": {
12132
+ let rank = i + 1;
12133
+ for (let j = 0; j < i; j++) {
12134
+ const isTie = sort.every(
12135
+ (s) => String(sorted[j].row[s.field]) === String(entry.row[s.field])
12136
+ );
12137
+ if (isTie) {
12138
+ rank = result[sorted[j].originalIndex]?.[def.as];
12139
+ break;
12140
+ }
12141
+ }
12142
+ computed = rank;
12143
+ break;
12144
+ }
12145
+ case "first_value": {
12146
+ computed = sorted[0].row[def.field] ?? null;
12147
+ break;
12148
+ }
12149
+ }
12150
+ outRow[def.as] = computed;
12151
+ }
12152
+ result[entry.originalIndex] = outRow;
12153
+ }
12154
+ }
12155
+ return result;
12156
+ }
12157
+
11315
12158
  // src/transforms/index.ts
11316
12159
  function runTransforms(data, transforms) {
11317
12160
  let result = data;
@@ -11328,6 +12171,8 @@ function runTransforms(data, transforms) {
11328
12171
  result = runAggregate(result, transform);
11329
12172
  } else if ("fold" in transform) {
11330
12173
  result = runFold(result, transform);
12174
+ } else if ("window" in transform) {
12175
+ result = runWindow(result, transform);
11331
12176
  }
11332
12177
  }
11333
12178
  return result;
@@ -11394,7 +12239,7 @@ function compileChart(spec, options) {
11394
12239
  }
11395
12240
  let chartSpec = normalized;
11396
12241
  const rawWatermark = expandedSpec.watermark;
11397
- const watermark = rawWatermark !== void 0 ? chartSpec.watermark : options.watermark ?? true;
12242
+ let watermark = rawWatermark !== void 0 ? chartSpec.watermark : options.watermark ?? true;
11398
12243
  const rawTransforms = expandedSpec.transform;
11399
12244
  if (rawTransforms && rawTransforms.length > 0) {
11400
12245
  chartSpec = { ...chartSpec, data: runTransforms(chartSpec.data, rawTransforms) };
@@ -11404,6 +12249,21 @@ function compileChart(spec, options) {
11404
12249
  let strategy = getLayoutStrategy(breakpoint, heightClass);
11405
12250
  const rawSpec = expandedSpec;
11406
12251
  const overrides = rawSpec.overrides;
12252
+ const rawEncoding = rawSpec.encoding;
12253
+ const bpForExplicit = overrides?.[breakpoint];
12254
+ const bpEncoding = bpForExplicit?.encoding;
12255
+ const hasChromeKeys = (v) => !!v && typeof v === "object" && Object.keys(v).length > 0;
12256
+ const userExplicit = {
12257
+ chrome: hasChromeKeys(rawSpec.chrome) || hasChromeKeys(bpForExplicit?.chrome),
12258
+ legend: rawSpec.legend !== void 0 || bpForExplicit?.legend !== void 0,
12259
+ xAxis: rawEncoding?.x?.axis !== void 0 || bpEncoding?.x?.axis !== void 0,
12260
+ yAxis: rawEncoding?.y?.axis !== void 0 || bpEncoding?.y?.axis !== void 0,
12261
+ labels: rawSpec.labels !== void 0 || bpForExplicit?.labels !== void 0,
12262
+ animation: rawSpec.animation !== void 0 || bpForExplicit?.animation !== void 0,
12263
+ watermark: rawSpec.watermark !== void 0 || bpForExplicit?.watermark !== void 0,
12264
+ crosshair: rawSpec.crosshair !== void 0 || bpForExplicit?.crosshair !== void 0
12265
+ };
12266
+ chartSpec = { ...chartSpec, userExplicit };
11407
12267
  if (overrides?.[breakpoint]) {
11408
12268
  const bp = overrides[breakpoint];
11409
12269
  if (bp.chrome) {
@@ -11447,13 +12307,84 @@ function compileChart(spec, options) {
11447
12307
  };
11448
12308
  strategy = { ...strategy, annotationPosition: "inline" };
11449
12309
  }
12310
+ if (bp.display !== void 0) {
12311
+ chartSpec = {
12312
+ ...chartSpec,
12313
+ display: bp.display
12314
+ };
12315
+ }
12316
+ if (bp.encoding !== void 0) {
12317
+ const bpEnc = bp.encoding;
12318
+ const mergedEncoding = { ...chartSpec.encoding };
12319
+ const NESTED_CHANNEL_KEYS = ["axis", "scale"];
12320
+ for (const channel of Object.keys(bpEnc)) {
12321
+ const baseCh = mergedEncoding[channel];
12322
+ const bpCh = bpEnc[channel];
12323
+ if (bpCh && baseCh) {
12324
+ const merged = { ...baseCh, ...bpCh };
12325
+ for (const key of NESTED_CHANNEL_KEYS) {
12326
+ const baseNested = baseCh[key];
12327
+ const bpNested = bpCh[key];
12328
+ if (baseNested && bpNested && typeof baseNested === "object" && typeof bpNested === "object" && !Array.isArray(baseNested) && !Array.isArray(bpNested)) {
12329
+ merged[key] = { ...baseNested, ...bpNested };
12330
+ }
12331
+ }
12332
+ mergedEncoding[channel] = merged;
12333
+ } else if (bpCh) {
12334
+ mergedEncoding[channel] = bpCh;
12335
+ }
12336
+ }
12337
+ chartSpec = {
12338
+ ...chartSpec,
12339
+ encoding: mergedEncoding
12340
+ };
12341
+ }
12342
+ if (typeof bp.watermark === "boolean") {
12343
+ watermark = bp.watermark;
12344
+ chartSpec = { ...chartSpec, watermark };
12345
+ }
12346
+ }
12347
+ if (chartSpec.display === "sparkline" && !chartSpec.userExplicit.labels) {
12348
+ chartSpec = {
12349
+ ...chartSpec,
12350
+ labels: { ...chartSpec.labels, density: "none" }
12351
+ };
12352
+ }
12353
+ let rawAnimationSpec = overrides?.[breakpoint]?.animation ?? rawSpec.animation;
12354
+ if (rawAnimationSpec === void 0 && chartSpec.display === "sparkline") {
12355
+ rawAnimationSpec = false;
12356
+ }
12357
+ if (chartSpec.display === "sparkline" && rawAnimationSpec !== false && rawAnimationSpec !== void 0) {
12358
+ const SPARK_DURATION = 1100;
12359
+ if (rawAnimationSpec === true) {
12360
+ rawAnimationSpec = { enter: { duration: SPARK_DURATION } };
12361
+ } else if (typeof rawAnimationSpec === "object") {
12362
+ const cfg = rawAnimationSpec;
12363
+ const enter = cfg.enter;
12364
+ if (enter === void 0 || enter === true) {
12365
+ rawAnimationSpec = {
12366
+ ...cfg,
12367
+ enter: { duration: SPARK_DURATION }
12368
+ };
12369
+ } else if (typeof enter === "object" && enter !== null && enter.duration === void 0) {
12370
+ rawAnimationSpec = {
12371
+ ...cfg,
12372
+ enter: { ...enter, duration: SPARK_DURATION }
12373
+ };
12374
+ }
12375
+ }
11450
12376
  }
11451
- const rawAnimationSpec = overrides?.[breakpoint]?.animation ?? rawSpec.animation;
11452
12377
  const resolvedAnimation = resolveAnimation(rawAnimationSpec);
12378
+ const rawCrosshair = bpForExplicit?.crosshair ?? rawSpec.crosshair;
12379
+ const crosshair = chartSpec.display === "sparkline" && !chartSpec.userExplicit.crosshair ? false : rawCrosshair === true;
12380
+ if (chartSpec.display === "sparkline" && !chartSpec.userExplicit.watermark) {
12381
+ watermark = false;
12382
+ chartSpec = { ...chartSpec, watermark: false };
12383
+ }
11453
12384
  const mergedThemeConfig = options.theme ? { ...chartSpec.theme, ...options.theme } : chartSpec.theme;
11454
- let theme = resolveTheme3(mergedThemeConfig);
12385
+ let theme = resolveTheme4(mergedThemeConfig);
11455
12386
  if (options.darkMode) {
11456
- theme = adaptTheme3(theme);
12387
+ theme = adaptTheme4(theme);
11457
12388
  }
11458
12389
  const preliminaryArea = {
11459
12390
  x: 0,
@@ -11465,7 +12396,7 @@ function compileChart(spec, options) {
11465
12396
  const dims = computeDimensions(chartSpec, options, legendLayout, theme, strategy, watermark);
11466
12397
  const chartArea = dims.chartArea;
11467
12398
  const legendArea = { ...chartArea };
11468
- if (legendLayout.entries.length > 0) {
12399
+ if ("entries" in legendLayout && legendLayout.entries.length > 0) {
11469
12400
  const gap = legendGap(options.width);
11470
12401
  switch (legendLayout.position) {
11471
12402
  case "top":
@@ -11494,7 +12425,14 @@ function compileChart(spec, options) {
11494
12425
  applyColorScaleRange(scales, renderSpec.encoding, theme);
11495
12426
  scales.defaultColor = chartSpec.markDef.fill ?? chartSpec.markDef.stroke ?? theme.colors.categorical[0];
11496
12427
  const isRadial = chartSpec.markType === "arc";
11497
- const axes = isRadial ? { x: void 0, y: void 0 } : computeAxes(scales, chartArea, strategy, theme, options.measureText);
12428
+ const skipX = chartSpec.display === "sparkline" && !chartSpec.userExplicit.xAxis;
12429
+ const skipY = chartSpec.display === "sparkline" && !chartSpec.userExplicit.yAxis;
12430
+ const axes = isRadial ? { x: void 0, y: void 0 } : computeAxes(scales, chartArea, strategy, theme, options.measureText, {
12431
+ data: renderSpec.data,
12432
+ encoding: renderSpec.encoding,
12433
+ skipX,
12434
+ skipY
12435
+ });
11498
12436
  if (!isRadial) {
11499
12437
  computeGridlines(axes, chartArea);
11500
12438
  }
@@ -11569,6 +12507,8 @@ function compileChart(spec, options) {
11569
12507
  },
11570
12508
  animation: resolvedAnimation,
11571
12509
  watermark,
12510
+ display: chartSpec.display,
12511
+ crosshair,
11572
12512
  measureText: options.measureText
11573
12513
  };
11574
12514
  }
@@ -11588,7 +12528,8 @@ function compileLayer(spec, options) {
11588
12528
  const primaryLayout = compileChart(primarySpec, options);
11589
12529
  const allMarks = [];
11590
12530
  const seenLabels = /* @__PURE__ */ new Set();
11591
- const mergedLegendEntries = [...primaryLayout.legend.entries];
12531
+ const pLegend = primaryLayout.legend;
12532
+ const mergedLegendEntries = "entries" in pLegend ? [...pLegend.entries] : [];
11592
12533
  for (const entry of mergedLegendEntries) {
11593
12534
  seenLabels.add(entry.label);
11594
12535
  }
@@ -11600,10 +12541,13 @@ function compileLayer(spec, options) {
11600
12541
  for (const { leaf } of indexedLeaves) {
11601
12542
  const leafLayout = compileChart(leaf, options);
11602
12543
  allMarks.push(...leafLayout.marks);
11603
- for (const entry of leafLayout.legend.entries) {
11604
- if (!seenLabels.has(entry.label)) {
11605
- seenLabels.add(entry.label);
11606
- mergedLegendEntries.push(entry);
12544
+ const leafLeg = leafLayout.legend;
12545
+ if ("entries" in leafLeg) {
12546
+ for (const entry of leafLeg.entries) {
12547
+ if (!seenLabels.has(entry.label)) {
12548
+ seenLabels.add(entry.label);
12549
+ mergedLegendEntries.push(entry);
12550
+ }
11607
12551
  }
11608
12552
  }
11609
12553
  }
@@ -11612,7 +12556,7 @@ function compileLayer(spec, options) {
11612
12556
  marks: allMarks,
11613
12557
  legend: {
11614
12558
  ...primaryLayout.legend,
11615
- entries: mergedLegendEntries
12559
+ ..."entries" in pLegend ? { entries: mergedLegendEntries } : {}
11616
12560
  }
11617
12561
  };
11618
12562
  }
@@ -11626,7 +12570,7 @@ function estimateYAxisLabelWidth(data, encoding, baseFontSize) {
11626
12570
  let maxWidth = 0;
11627
12571
  for (const row of data) {
11628
12572
  const label = String(row[yField] ?? "");
11629
- const w = estimateTextWidth14(label, baseFontSize, 400);
12573
+ const w = estimateTextWidth15(label, baseFontSize, 400);
11630
12574
  if (w > maxWidth) maxWidth = w;
11631
12575
  }
11632
12576
  return maxWidth > 0 ? maxWidth + 10 : 40;
@@ -11655,7 +12599,7 @@ function estimateYAxisLabelWidth(data, encoding, baseFontSize) {
11655
12599
  }
11656
12600
  const hasNeg = data.some((r) => Number(r[yField]) < 0);
11657
12601
  const labelEst = (hasNeg ? "-" : "") + sampleLabel;
11658
- return estimateTextWidth14(labelEst, baseFontSize, 400) + 10;
12602
+ return estimateTextWidth15(labelEst, baseFontSize, 400) + 10;
11659
12603
  }
11660
12604
  function compileLayerIndependent(leaves, layerSpec, options) {
11661
12605
  if (leaves.length > 2) {
@@ -11672,7 +12616,7 @@ function compileLayerIndependent(leaves, layerSpec, options) {
11672
12616
  `Dual-axis charts require matching x-field types across layers. Layer 0 has '${xType0}', layer 1 has '${xType1}'.`
11673
12617
  );
11674
12618
  }
11675
- const theme = resolveTheme3(layerSpec.theme ?? leaf1.theme);
12619
+ const theme = resolveTheme4(layerSpec.theme ?? leaf1.theme);
11676
12620
  const axisFontSize = theme.fonts?.sizes?.axisTick ?? 11;
11677
12621
  const rightAxisWidth = estimateYAxisLabelWidth(leaf1.data, leaf1.encoding, axisFontSize);
11678
12622
  const hasRightAxisTitle = !!leaf1.encoding?.y?.axis?.title;
@@ -11789,9 +12733,12 @@ function compileLayerIndependent(leaves, layerSpec, options) {
11789
12733
  return tagged;
11790
12734
  });
11791
12735
  const seenLabels = /* @__PURE__ */ new Set();
11792
- const mergedLegendEntries = [...layout0.legend.entries];
12736
+ const l0Legend = layout0.legend;
12737
+ const l1Legend = layout1.legend;
12738
+ const mergedLegendEntries = "entries" in l0Legend ? [...l0Legend.entries] : [];
11793
12739
  for (const entry of mergedLegendEntries) seenLabels.add(entry.label);
11794
- for (const entry of layout1.legend.entries) {
12740
+ const l1Entries = "entries" in l1Legend ? l1Legend.entries : [];
12741
+ for (const entry of l1Entries) {
11795
12742
  if (!seenLabels.has(entry.label)) {
11796
12743
  seenLabels.add(entry.label);
11797
12744
  mergedLegendEntries.push(entry);
@@ -11821,7 +12768,7 @@ function compileLayerIndependent(leaves, layerSpec, options) {
11821
12768
  marks,
11822
12769
  legend: {
11823
12770
  ...layout0.legend,
11824
- entries: mergedLegendEntries
12771
+ ..."entries" in l0Legend ? { entries: mergedLegendEntries } : {}
11825
12772
  },
11826
12773
  tooltipDescriptors: mergedTooltips
11827
12774
  };
@@ -11934,9 +12881,9 @@ function compileTable(spec, options) {
11934
12881
  }
11935
12882
  const tableSpec = normalized;
11936
12883
  const mergedThemeConfig = options.theme ? { ...tableSpec.theme, ...options.theme } : tableSpec.theme;
11937
- let theme = resolveTheme3(mergedThemeConfig);
12884
+ let theme = resolveTheme4(mergedThemeConfig);
11938
12885
  if (options.darkMode) {
11939
- theme = adaptTheme3(theme);
12886
+ theme = adaptTheme4(theme);
11940
12887
  }
11941
12888
  const rawWatermark = spec.watermark;
11942
12889
  const watermark = rawWatermark !== void 0 ? tableSpec.watermark : options.watermark ?? true;
@@ -11948,6 +12895,9 @@ function compileGraph2(spec, options) {
11948
12895
  function compileSankey2(spec, options) {
11949
12896
  return compileSankey(spec, options);
11950
12897
  }
12898
+ function compileTileMap2(spec, options) {
12899
+ return compileTileMap(spec, options);
12900
+ }
11951
12901
  export {
11952
12902
  clampStaggerDelay,
11953
12903
  clearRenderers,
@@ -11957,6 +12907,7 @@ export {
11957
12907
  compileLayer,
11958
12908
  compileSankey2 as compileSankey,
11959
12909
  compileTable,
12910
+ compileTileMap2 as compileTileMap,
11960
12911
  evaluatePredicate,
11961
12912
  getChartRenderer,
11962
12913
  isConditionalValueDef,