@opendata-ai/openchart-engine 6.25.4 → 6.26.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",
@@ -6543,9 +6553,146 @@ import {
6543
6553
  isLayerSpec,
6544
6554
  isSankeySpec,
6545
6555
  isTableSpec,
6556
+ isTileMapSpec,
6546
6557
  resolveMarkDef,
6547
6558
  resolveMarkType
6548
6559
  } from "@opendata-ai/openchart-core";
6560
+
6561
+ // src/tilemap/layout.ts
6562
+ var US_STATE_TILES = [
6563
+ // Row 0
6564
+ { state: "ME", col: 10, row: 0 },
6565
+ // Row 1
6566
+ { state: "VT", col: 9, row: 1 },
6567
+ { state: "NH", col: 10, row: 1 },
6568
+ // Row 2
6569
+ { state: "WA", col: 0, row: 2 },
6570
+ { state: "ID", col: 1, row: 2 },
6571
+ { state: "MT", col: 2, row: 2 },
6572
+ { state: "ND", col: 3, row: 2 },
6573
+ { state: "MN", col: 4, row: 2 },
6574
+ { state: "WI", col: 5, row: 2 },
6575
+ { state: "MI", col: 6, row: 2 },
6576
+ { state: "NY", col: 8, row: 2 },
6577
+ { state: "MA", col: 9, row: 2 },
6578
+ // Row 3
6579
+ { state: "OR", col: 0, row: 3 },
6580
+ { state: "NV", col: 1, row: 3 },
6581
+ { state: "WY", col: 2, row: 3 },
6582
+ { state: "SD", col: 3, row: 3 },
6583
+ { state: "IA", col: 4, row: 3 },
6584
+ { state: "IL", col: 5, row: 3 },
6585
+ { state: "IN", col: 6, row: 3 },
6586
+ { state: "OH", col: 7, row: 3 },
6587
+ { state: "PA", col: 8, row: 3 },
6588
+ { state: "NJ", col: 9, row: 3 },
6589
+ { state: "CT", col: 10, row: 3 },
6590
+ // Row 4
6591
+ { state: "CA", col: 0, row: 4 },
6592
+ { state: "UT", col: 1, row: 4 },
6593
+ { state: "CO", col: 2, row: 4 },
6594
+ { state: "NE", col: 3, row: 4 },
6595
+ { state: "MO", col: 4, row: 4 },
6596
+ { state: "KY", col: 5, row: 4 },
6597
+ { state: "WV", col: 6, row: 4 },
6598
+ { state: "VA", col: 7, row: 4 },
6599
+ { state: "MD", col: 8, row: 4 },
6600
+ { state: "DE", col: 9, row: 4 },
6601
+ { state: "RI", col: 10, row: 4 },
6602
+ // Row 5
6603
+ { state: "AZ", col: 1, row: 5 },
6604
+ { state: "NM", col: 2, row: 5 },
6605
+ { state: "KS", col: 3, row: 5 },
6606
+ { state: "AR", col: 4, row: 5 },
6607
+ { state: "TN", col: 5, row: 5 },
6608
+ { state: "NC", col: 6, row: 5 },
6609
+ { state: "SC", col: 7, row: 5 },
6610
+ { state: "DC", col: 8, row: 5 },
6611
+ // Row 6
6612
+ { state: "AK", col: 0, row: 6 },
6613
+ { state: "OK", col: 3, row: 6 },
6614
+ { state: "LA", col: 4, row: 6 },
6615
+ { state: "MS", col: 5, row: 6 },
6616
+ { state: "AL", col: 6, row: 6 },
6617
+ { state: "GA", col: 7, row: 6 },
6618
+ // Row 7
6619
+ { state: "HI", col: 1, row: 7 },
6620
+ { state: "TX", col: 3, row: 7 },
6621
+ { state: "FL", col: 7, row: 7 }
6622
+ ];
6623
+ var STATE_CODE_SET = new Set(US_STATE_TILES.map((t) => t.state));
6624
+ var STATE_NAMES = {
6625
+ AL: "Alabama",
6626
+ AK: "Alaska",
6627
+ AZ: "Arizona",
6628
+ AR: "Arkansas",
6629
+ CA: "California",
6630
+ CO: "Colorado",
6631
+ CT: "Connecticut",
6632
+ DE: "Delaware",
6633
+ DC: "District of Columbia",
6634
+ FL: "Florida",
6635
+ GA: "Georgia",
6636
+ HI: "Hawaii",
6637
+ ID: "Idaho",
6638
+ IL: "Illinois",
6639
+ IN: "Indiana",
6640
+ IA: "Iowa",
6641
+ KS: "Kansas",
6642
+ KY: "Kentucky",
6643
+ LA: "Louisiana",
6644
+ ME: "Maine",
6645
+ MD: "Maryland",
6646
+ MA: "Massachusetts",
6647
+ MI: "Michigan",
6648
+ MN: "Minnesota",
6649
+ MS: "Mississippi",
6650
+ MO: "Missouri",
6651
+ MT: "Montana",
6652
+ NE: "Nebraska",
6653
+ NV: "Nevada",
6654
+ NH: "New Hampshire",
6655
+ NJ: "New Jersey",
6656
+ NM: "New Mexico",
6657
+ NY: "New York",
6658
+ NC: "North Carolina",
6659
+ ND: "North Dakota",
6660
+ OH: "Ohio",
6661
+ OK: "Oklahoma",
6662
+ OR: "Oregon",
6663
+ PA: "Pennsylvania",
6664
+ RI: "Rhode Island",
6665
+ SC: "South Carolina",
6666
+ SD: "South Dakota",
6667
+ TN: "Tennessee",
6668
+ TX: "Texas",
6669
+ UT: "Utah",
6670
+ VT: "Vermont",
6671
+ VA: "Virginia",
6672
+ WA: "Washington",
6673
+ WV: "West Virginia",
6674
+ WI: "Wisconsin",
6675
+ WY: "Wyoming"
6676
+ };
6677
+ var GRID_COLS = 12;
6678
+ var GRID_ROWS = 8;
6679
+ function computeTilePositions(availableWidth, availableHeight, gap = 4) {
6680
+ const maxTileW = (availableWidth - gap * (GRID_COLS - 1)) / GRID_COLS;
6681
+ const maxTileH = (availableHeight - gap * (GRID_ROWS - 1)) / GRID_ROWS;
6682
+ const tileSize = Math.max(1, Math.floor(Math.min(maxTileW, maxTileH)));
6683
+ const gridWidth = tileSize * GRID_COLS + gap * (GRID_COLS - 1);
6684
+ const gridHeight = tileSize * GRID_ROWS + gap * (GRID_ROWS - 1);
6685
+ const positions = /* @__PURE__ */ new Map();
6686
+ for (const { state, col, row } of US_STATE_TILES) {
6687
+ positions.set(state, {
6688
+ x: col * (tileSize + gap),
6689
+ y: row * (tileSize + gap)
6690
+ });
6691
+ }
6692
+ return { tileSize, gap, positions, gridWidth, gridHeight };
6693
+ }
6694
+
6695
+ // src/compiler/normalize.ts
6549
6696
  function normalizeChromeField(value2) {
6550
6697
  if (value2 === void 0) return void 0;
6551
6698
  if (typeof value2 === "string") return { text: value2 };
@@ -6746,6 +6893,45 @@ function normalizeGraphSpec(spec, _warnings) {
6746
6893
  watermark: spec.watermark ?? true
6747
6894
  };
6748
6895
  }
6896
+ function normalizeTileMapSpec(spec, warnings) {
6897
+ let data = Array.isArray(spec.data) ? spec.data : [];
6898
+ if (!Array.isArray(spec.data)) {
6899
+ data = Object.entries(spec.data).map(([state, value2]) => ({ state, value: value2 }));
6900
+ }
6901
+ let encoding = spec.encoding;
6902
+ if (!encoding) {
6903
+ encoding = {
6904
+ state: { field: "state", type: "nominal" },
6905
+ value: { field: "value", type: "quantitative" }
6906
+ };
6907
+ }
6908
+ let matchedCount = 0;
6909
+ for (const row of data) {
6910
+ const stateCode = String(row[encoding.state.field]);
6911
+ if (STATE_CODE_SET.has(stateCode)) {
6912
+ matchedCount++;
6913
+ }
6914
+ }
6915
+ const matchRatio = data.length > 0 ? matchedCount / data.length : 0;
6916
+ if (matchRatio < 0.5 && data.length > 0) {
6917
+ warnings.push(
6918
+ `TileMap data: only ${matchedCount} of ${data.length} rows have valid US state codes (expected \u226550%)`
6919
+ );
6920
+ }
6921
+ return {
6922
+ type: "tilemap",
6923
+ data,
6924
+ encoding,
6925
+ palette: spec.palette ?? "blue",
6926
+ chrome: normalizeChrome(spec.chrome),
6927
+ legend: spec.legend,
6928
+ theme: spec.theme ?? {},
6929
+ darkMode: spec.darkMode ?? "off",
6930
+ watermark: spec.watermark ?? true,
6931
+ animation: spec.animation,
6932
+ valueFormat: spec.valueFormat
6933
+ };
6934
+ }
6749
6935
  function normalizeSpec(spec, warnings = []) {
6750
6936
  if (isLayerSpec(spec)) {
6751
6937
  const leaves = flattenLayers(spec);
@@ -6766,8 +6952,11 @@ function normalizeSpec(spec, warnings = []) {
6766
6952
  if (isSankeySpec(spec)) {
6767
6953
  return normalizeSankeySpec(spec, warnings);
6768
6954
  }
6955
+ if (isTileMapSpec(spec)) {
6956
+ return normalizeTileMapSpec(spec, warnings);
6957
+ }
6769
6958
  throw new Error(
6770
- `Unknown spec shape. Expected mark (chart), layer, type: 'table', type: 'graph', or type: 'sankey'.`
6959
+ `Unknown spec shape. Expected mark (chart), layer, type: 'table', type: 'graph', type: 'sankey', or type: 'tilemap'.`
6771
6960
  );
6772
6961
  }
6773
6962
  function flattenLayers(spec, parentData, parentEncoding, parentTransforms, parentWatermark) {
@@ -7317,6 +7506,96 @@ function validateSankeySpec(spec, errors) {
7317
7506
  });
7318
7507
  }
7319
7508
  }
7509
+ function validateTileMapSpec(spec, errors) {
7510
+ if (!spec.data || typeof spec.data !== "object") {
7511
+ errors.push({
7512
+ message: 'Spec error: tilemap spec requires a "data" field (record or array)',
7513
+ path: "data",
7514
+ code: "INVALID_TYPE",
7515
+ 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'
7516
+ });
7517
+ return;
7518
+ }
7519
+ if (!Array.isArray(spec.data) && Object.keys(spec.data).length === 0) {
7520
+ errors.push({
7521
+ message: 'Spec error: "data" must have at least one entry',
7522
+ path: "data",
7523
+ code: "EMPTY_DATA",
7524
+ suggestion: 'Add at least one state-value pair, e.g. { "CA": 12000 }'
7525
+ });
7526
+ return;
7527
+ }
7528
+ if (Array.isArray(spec.data)) {
7529
+ if (spec.data.length === 0) {
7530
+ errors.push({
7531
+ message: 'Spec error: "data" array must be non-empty',
7532
+ path: "data",
7533
+ code: "EMPTY_DATA",
7534
+ suggestion: "Add at least one data row"
7535
+ });
7536
+ return;
7537
+ }
7538
+ const firstRow = spec.data[0];
7539
+ if (typeof firstRow !== "object" || firstRow === null || Array.isArray(firstRow)) {
7540
+ errors.push({
7541
+ message: 'Spec error: each item in "data" must be a plain object',
7542
+ path: "data[0]",
7543
+ code: "INVALID_TYPE",
7544
+ suggestion: 'Each data item should be an object, e.g. { state: "CA", value: 12000 }'
7545
+ });
7546
+ return;
7547
+ }
7548
+ if (!spec.encoding || typeof spec.encoding !== "object") {
7549
+ errors.push({
7550
+ message: 'Spec error: tilemap spec with array data requires an "encoding" object with state and value channels',
7551
+ path: "encoding",
7552
+ code: "MISSING_FIELD",
7553
+ suggestion: 'Add an encoding object, e.g. encoding: { state: { field: "state", type: "nominal" }, value: { field: "value", type: "quantitative" } }'
7554
+ });
7555
+ return;
7556
+ }
7557
+ const encoding = spec.encoding;
7558
+ const dataColumns = new Set(Object.keys(firstRow));
7559
+ const availableColumns = [...dataColumns].join(", ");
7560
+ for (const channel of ["state", "value"]) {
7561
+ const ch = encoding[channel];
7562
+ if (!ch || typeof ch !== "object") {
7563
+ errors.push({
7564
+ message: `Spec error: tilemap encoding requires "${channel}" channel`,
7565
+ path: `encoding.${channel}`,
7566
+ code: "MISSING_FIELD",
7567
+ suggestion: `Add encoding.${channel} with a field from your data (${availableColumns}). Example: ${channel}: { field: "${[...dataColumns][0] ?? "myField"}", type: "${channel === "value" ? "quantitative" : "nominal"}" }`
7568
+ });
7569
+ continue;
7570
+ }
7571
+ if (!ch.field || typeof ch.field !== "string") {
7572
+ errors.push({
7573
+ message: `Spec error: encoding.${channel} must have a "field" string`,
7574
+ path: `encoding.${channel}.field`,
7575
+ code: "MISSING_FIELD",
7576
+ suggestion: `Add a field name from your data columns: ${availableColumns}`
7577
+ });
7578
+ continue;
7579
+ }
7580
+ if (!dataColumns.has(ch.field)) {
7581
+ errors.push({
7582
+ message: `Spec error: encoding.${channel}.field "${ch.field}" does not exist in data. Available columns: ${availableColumns}`,
7583
+ path: `encoding.${channel}.field`,
7584
+ code: "DATA_FIELD_MISSING",
7585
+ suggestion: `Use one of the available data columns: ${availableColumns}`
7586
+ });
7587
+ }
7588
+ }
7589
+ }
7590
+ if (spec.darkMode !== void 0 && !VALID_DARK_MODES.has(spec.darkMode)) {
7591
+ errors.push({
7592
+ message: 'Spec error: darkMode must be "auto", "force", or "off"',
7593
+ path: "darkMode",
7594
+ code: "INVALID_VALUE",
7595
+ suggestion: 'Use one of: "auto" (system preference), "force" (always dark), or "off" (always light)'
7596
+ });
7597
+ }
7598
+ }
7320
7599
  function validateLayerSpec(spec, errors) {
7321
7600
  const layer = spec.layer;
7322
7601
  if (layer.length === 0) {
@@ -7411,17 +7690,18 @@ function validateSpec(spec) {
7411
7690
  const isTable = obj.type === "table";
7412
7691
  const isGraph = obj.type === "graph";
7413
7692
  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) {
7693
+ const isTileMap = obj.type === "tilemap";
7694
+ const isLayer = hasLayer && !isTable && !isGraph && !isSankey && !isTileMap;
7695
+ const isChart = hasMark && !hasLayer && !isTable && !isGraph && !isSankey && !isTileMap;
7696
+ if (!isChart && !isTable && !isGraph && !isSankey && !isTileMap && !isLayer) {
7417
7697
  return {
7418
7698
  valid: false,
7419
7699
  errors: [
7420
7700
  {
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',
7701
+ 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
7702
  path: "mark",
7423
7703
  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(", ")}`
7704
+ 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
7705
  }
7426
7706
  ],
7427
7707
  normalized: null
@@ -7459,6 +7739,8 @@ function validateSpec(spec) {
7459
7739
  validateGraphSpec(obj, errors);
7460
7740
  } else if (isSankey) {
7461
7741
  validateSankeySpec(obj, errors);
7742
+ } else if (isTileMap) {
7743
+ validateTileMapSpec(obj, errors);
7462
7744
  }
7463
7745
  if (errors.length > 0) {
7464
7746
  return { valid: false, errors, normalized: null };
@@ -8021,7 +8303,7 @@ function scaleSupportsTickCount(resolvedScale) {
8021
8303
  const scale = resolvedScale.scale;
8022
8304
  return "ticks" in scale && typeof scale.ticks === "function";
8023
8305
  }
8024
- function categoricalTicks(resolvedScale, density, orientation = "horizontal", bandwidth, labelAngle, fontSize, fontWeight, measureText) {
8306
+ function categoricalTicks(resolvedScale, density, orientation = "horizontal", bandwidth, labelAngle, fontSize, fontWeight, measureText, subtitleContext) {
8025
8307
  const scale = resolvedScale.scale;
8026
8308
  const domain = scale.domain();
8027
8309
  const explicitTickCount = resolvedScale.channel.axis?.tickCount;
@@ -8057,14 +8339,37 @@ function categoricalTicks(resolvedScale, density, orientation = "horizontal", ba
8057
8339
  selectedValues = domain.filter((_, i) => i % step === 0);
8058
8340
  }
8059
8341
  }
8342
+ let subtitleMap;
8343
+ if (subtitleContext) {
8344
+ const { data, fieldName, labelField } = subtitleContext;
8345
+ if (data.length > 0) {
8346
+ subtitleMap = /* @__PURE__ */ new Map();
8347
+ for (const row of data) {
8348
+ const key = String(row[fieldName] ?? "");
8349
+ if (!subtitleMap.has(key)) {
8350
+ const val = row[labelField];
8351
+ if (val != null) {
8352
+ subtitleMap.set(key, String(val));
8353
+ }
8354
+ }
8355
+ }
8356
+ }
8357
+ }
8060
8358
  const ticks2 = selectedValues.map((value2) => {
8061
8359
  const bandScale = resolvedScale.type === "band" ? scale : null;
8062
8360
  const pos = bandScale ? (bandScale(value2) ?? 0) + bandScale.bandwidth() / 2 : scale(value2) ?? 0;
8063
- return {
8361
+ const tick = {
8064
8362
  value: value2,
8065
8363
  position: pos,
8066
8364
  label: value2
8067
8365
  };
8366
+ if (subtitleMap) {
8367
+ const subtitle = subtitleMap.get(value2);
8368
+ if (subtitle !== void 0) {
8369
+ tick.subtitle = subtitle;
8370
+ }
8371
+ }
8372
+ return tick;
8068
8373
  });
8069
8374
  return ticks2;
8070
8375
  }
@@ -8136,7 +8441,7 @@ function fitContinuousTicks(scale, initialTicks, initialCount, fontSize, fontWei
8136
8441
  const fallback = bestWithinFloor ?? buildContinuousTicks(scale, floor);
8137
8442
  return thinTicksUntilFit(fallback, fontSize, fontWeight, measureText, orientation);
8138
8443
  }
8139
- function computeAxes(scales, chartArea, strategy, theme, measureText) {
8444
+ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContext) {
8140
8445
  const result = {};
8141
8446
  const baseDensity = strategy.axisLabelDensity;
8142
8447
  const yDensity = effectiveDensity(
@@ -8254,7 +8559,19 @@ function computeAxes(scales, chartArea, strategy, theme, measureText) {
8254
8559
  if (axisConfig?.values) {
8255
8560
  allTicks = resolveExplicitTicks(axisConfig.values, scales.y);
8256
8561
  } else if (!isContinuousY) {
8257
- allTicks = categoricalTicks(scales.y, yDensity, "vertical");
8562
+ const yFieldName = dataContext?.encoding.y?.field;
8563
+ const yLabelField = axisConfig?.labelField;
8564
+ allTicks = categoricalTicks(
8565
+ scales.y,
8566
+ yDensity,
8567
+ "vertical",
8568
+ void 0,
8569
+ void 0,
8570
+ void 0,
8571
+ void 0,
8572
+ void 0,
8573
+ yFieldName && yLabelField && dataContext ? { data: dataContext.data, fieldName: yFieldName, labelField: yLabelField } : void 0
8574
+ );
8258
8575
  } else {
8259
8576
  allTicks = continuousTicks(scales.y, yDensity, yTargetCount);
8260
8577
  }
@@ -8488,10 +8805,23 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
8488
8805
  if (encoding.y && !isRadial) {
8489
8806
  if (spec.markType === "bar" || spec.markType === "circle" || spec.markType === "lollipop" || encoding.y.type === "nominal" || encoding.y.type === "ordinal") {
8490
8807
  const yField = encoding.y.field;
8808
+ const yLabelField = encoding.y.axis?.labelField;
8491
8809
  let maxLabelWidth = 0;
8492
8810
  for (const row of spec.data) {
8493
8811
  const label = String(row[yField] ?? "");
8494
- const w = estimateTextWidth10(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
8812
+ let w = estimateTextWidth10(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
8813
+ if (yLabelField) {
8814
+ const subtitle = String(row[yLabelField] ?? "");
8815
+ if (subtitle) {
8816
+ const gap = theme.fonts.sizes.axisTick * 0.6;
8817
+ const subtitleWidth = estimateTextWidth10(
8818
+ subtitle,
8819
+ theme.fonts.sizes.axisTick,
8820
+ theme.fonts.weights.normal
8821
+ );
8822
+ w += gap + subtitleWidth;
8823
+ }
8824
+ }
8495
8825
  if (w > maxLabelWidth) maxLabelWidth = w;
8496
8826
  }
8497
8827
  if (maxLabelWidth > 0) {
@@ -8545,7 +8875,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
8545
8875
  if (options.rightAxisReserve && options.rightAxisReserve > 0) {
8546
8876
  margins.right = Math.max(margins.right, hPad + options.rightAxisReserve);
8547
8877
  }
8548
- if (legendLayout.entries.length > 0) {
8878
+ if ("entries" in legendLayout && legendLayout.entries.length > 0) {
8549
8879
  const gap = legendGap(width);
8550
8880
  if (legendLayout.position === "right" || legendLayout.position === "bottom-right") {
8551
8881
  margins.right += legendLayout.bounds.width + 8;
@@ -8579,7 +8909,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
8579
8909
  const bottomDelta = margins.bottom - newBottom;
8580
8910
  if (topDelta > 0 || bottomDelta > 0) {
8581
8911
  const gap = legendGap(width);
8582
- margins.top = newTop + (legendLayout.entries.length > 0 && legendLayout.position === "top" ? legendLayout.bounds.height + gap : 0);
8912
+ margins.top = newTop + ("entries" in legendLayout && legendLayout.entries.length > 0 && legendLayout.position === "top" ? legendLayout.bounds.height + gap : 0);
8583
8913
  margins.bottom = newBottom;
8584
8914
  chartArea = {
8585
8915
  x: margins.left,
@@ -9904,7 +10234,7 @@ function compileSankey(spec, options) {
9904
10234
  theme,
9905
10235
  fullArea
9906
10236
  );
9907
- const legendGap2 = legend.entries.length > 0 ? 4 : 0;
10237
+ const legendGap2 = "entries" in legend && legend.entries.length > 0 ? 4 : 0;
9908
10238
  const area = {
9909
10239
  x: fullArea.x,
9910
10240
  y: fullArea.y + legend.bounds.height + legendGap2,
@@ -10847,11 +11177,266 @@ function compileTableLayout(spec, options, theme) {
10847
11177
  };
10848
11178
  }
10849
11179
 
11180
+ // src/tilemap/compile-tilemap.ts
11181
+ import {
11182
+ adaptTheme as adaptTheme3,
11183
+ buildD3Formatter as buildD3Formatter7,
11184
+ computeChrome as computeChrome5,
11185
+ estimateTextWidth as estimateTextWidth14,
11186
+ formatNumber as formatNumber5,
11187
+ resolveTheme as resolveTheme3,
11188
+ SEQUENTIAL_PALETTES
11189
+ } from "@opendata-ai/openchart-core";
11190
+ var TILE_CORNER_RADIUS = 2;
11191
+ var TILE_STROKE_WIDTH = 0;
11192
+ function compileTileMap(spec, options) {
11193
+ const { spec: normalized } = compile(spec);
11194
+ if (!("type" in normalized) || normalized.type !== "tilemap") {
11195
+ throw new Error(
11196
+ "compileTileMap received a non-tilemap spec. Use compileChart, compileTable, compileGraph, or compileSankey instead."
11197
+ );
11198
+ }
11199
+ const tilemapSpec = normalized;
11200
+ const rawWatermark = spec.watermark;
11201
+ const watermark = rawWatermark !== void 0 ? tilemapSpec.watermark : options.watermark ?? true;
11202
+ const mergedThemeConfig = options.theme ? { ...tilemapSpec.theme, ...options.theme } : tilemapSpec.theme;
11203
+ const lightTheme = resolveTheme3(mergedThemeConfig);
11204
+ let theme = lightTheme;
11205
+ if (options.darkMode) {
11206
+ theme = adaptTheme3(theme);
11207
+ }
11208
+ const isDarkMode = options.darkMode;
11209
+ const chrome = computeChrome5(
11210
+ {
11211
+ title: tilemapSpec.chrome.title,
11212
+ subtitle: tilemapSpec.chrome.subtitle,
11213
+ source: tilemapSpec.chrome.source,
11214
+ byline: tilemapSpec.chrome.byline,
11215
+ footer: tilemapSpec.chrome.footer
11216
+ },
11217
+ theme,
11218
+ options.width,
11219
+ options.measureText,
11220
+ "full",
11221
+ void 0,
11222
+ watermark
11223
+ );
11224
+ const padding = theme.spacing.padding;
11225
+ const fullArea = {
11226
+ x: padding,
11227
+ y: padding + chrome.topHeight,
11228
+ width: options.width - padding * 2,
11229
+ height: options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
11230
+ };
11231
+ if (fullArea.width <= 0 || fullArea.height <= 0) {
11232
+ return emptyLayout2(chrome, theme, options, watermark);
11233
+ }
11234
+ const stateField = tilemapSpec.encoding.state.field;
11235
+ const valueField = tilemapSpec.encoding.value.field;
11236
+ const stateValueMap = /* @__PURE__ */ new Map();
11237
+ for (const row of tilemapSpec.data) {
11238
+ const stateCode = String(row[stateField]);
11239
+ const raw = row[valueField];
11240
+ if (STATE_CODE_SET.has(stateCode) && raw !== null && raw !== void 0) {
11241
+ const value2 = Number(raw);
11242
+ if (!Number.isNaN(value2)) {
11243
+ stateValueMap.set(stateCode, value2);
11244
+ }
11245
+ }
11246
+ }
11247
+ const values = Array.from(stateValueMap.values());
11248
+ const min4 = values.length > 0 ? Math.min(...values) : 0;
11249
+ const max4 = values.length > 0 ? Math.max(...values) : 100;
11250
+ const paletteStops = [...SEQUENTIAL_PALETTES[tilemapSpec.palette] ?? SEQUENTIAL_PALETTES.blue];
11251
+ if (isDarkMode) paletteStops.reverse();
11252
+ const domain = paletteStops.map((_, i) => min4 + i / (paletteStops.length - 1) * (max4 - min4));
11253
+ const colorScale = linear2().domain(domain).range(paletteStops).clamp(true);
11254
+ const showLegend = tilemapSpec.legend?.show !== false;
11255
+ const legendBarHeight = 12;
11256
+ const legendLabelGap = 4;
11257
+ const legendTotalHeight = showLegend ? legendBarHeight + legendLabelGap + 14 : 0;
11258
+ const legendGap2 = showLegend ? 8 : 0;
11259
+ const tileAreaHeight = fullArea.height - legendTotalHeight - legendGap2;
11260
+ const tilePositions = computeTilePositions(fullArea.width, tileAreaHeight, 4);
11261
+ const tileGridOffsetX = fullArea.x + (fullArea.width - tilePositions.gridWidth) / 2;
11262
+ const tileGridOffsetY = fullArea.y;
11263
+ const legendX = tileGridOffsetX;
11264
+ const legendY = tileGridOffsetY + tilePositions.gridHeight + legendGap2;
11265
+ const legendWidth = tilePositions.gridWidth;
11266
+ const formatter = buildD3Formatter7(tilemapSpec.valueFormat) ?? formatNumber5;
11267
+ const neutralFillLight = "#e0e0e0";
11268
+ const neutralFillDark = "#2a2a3e";
11269
+ const neutralStrokeLight = "#d0d0d0";
11270
+ const neutralStrokeDark = "#3a3a50";
11271
+ const neutralFill = isDarkMode ? neutralFillDark : neutralFillLight;
11272
+ const neutralStroke = isDarkMode ? neutralStrokeDark : neutralStrokeLight;
11273
+ const tiles = [];
11274
+ for (const { state: stateCode } of US_STATE_TILES) {
11275
+ const pos = tilePositions.positions.get(stateCode);
11276
+ if (!pos) continue;
11277
+ const hasData = stateValueMap.has(stateCode);
11278
+ const value2 = hasData ? stateValueMap.get(stateCode) : null;
11279
+ const fill = hasData ? colorScale(value2) : neutralFill;
11280
+ const formattedValue = hasData ? formatter(value2) : "\u2013";
11281
+ const labelStyle = {
11282
+ fontFamily: theme.fonts.family,
11283
+ fontSize: tilePositions.tileSize > 24 ? 14 : 11,
11284
+ fontWeight: 700,
11285
+ fill: "#ffffff",
11286
+ lineHeight: 1.2
11287
+ };
11288
+ const valueLabelStyle = {
11289
+ fontFamily: theme.fonts.family,
11290
+ fontSize: tilePositions.tileSize > 24 ? 12 : 10,
11291
+ fontWeight: 400,
11292
+ fill: "#ffffff",
11293
+ lineHeight: 1.2
11294
+ };
11295
+ const valueLabel = tilePositions.tileSize < 24 ? { text: "", x: 0, y: 0, style: valueLabelStyle, visible: false } : {
11296
+ text: formattedValue,
11297
+ x: tileGridOffsetX + pos.x + tilePositions.tileSize / 2,
11298
+ y: tileGridOffsetY + pos.y + tilePositions.tileSize / 2 + 8,
11299
+ style: valueLabelStyle,
11300
+ visible: true
11301
+ };
11302
+ const tile = {
11303
+ type: "tile",
11304
+ stateCode,
11305
+ x: tileGridOffsetX + pos.x,
11306
+ y: tileGridOffsetY + pos.y,
11307
+ size: tilePositions.tileSize,
11308
+ fill,
11309
+ stroke: neutralStroke,
11310
+ strokeWidth: TILE_STROKE_WIDTH,
11311
+ cornerRadius: TILE_CORNER_RADIUS,
11312
+ value: value2 ?? null,
11313
+ formattedValue,
11314
+ hasData,
11315
+ label: {
11316
+ text: stateCode,
11317
+ x: tileGridOffsetX + pos.x + tilePositions.tileSize / 2,
11318
+ y: tileGridOffsetY + pos.y + tilePositions.tileSize / 2 - 4,
11319
+ style: labelStyle,
11320
+ visible: true
11321
+ },
11322
+ valueLabel,
11323
+ data: { state: stateCode, value: value2, stateName: STATE_NAMES[stateCode] ?? stateCode },
11324
+ aria: {
11325
+ role: "img",
11326
+ label: `${STATE_NAMES[stateCode] ?? stateCode}: ${formattedValue}`
11327
+ },
11328
+ animationIndex: 0
11329
+ };
11330
+ tiles.push(tile);
11331
+ }
11332
+ const indices = Array.from({ length: tiles.length }, (_, i) => i);
11333
+ let seed = 42;
11334
+ for (let i = indices.length - 1; i > 0; i--) {
11335
+ seed = seed * 1103515245 + 12345 & 2147483647;
11336
+ const j = seed % (i + 1);
11337
+ [indices[i], indices[j]] = [indices[j], indices[i]];
11338
+ }
11339
+ for (let i = 0; i < tiles.length; i++) {
11340
+ tiles[i].animationIndex = indices[i];
11341
+ }
11342
+ let gradientLegend = null;
11343
+ if (showLegend) {
11344
+ const gradientColorStops = paletteStops.map((color2, i) => ({
11345
+ offset: i / (paletteStops.length - 1),
11346
+ color: color2
11347
+ }));
11348
+ gradientLegend = {
11349
+ type: "gradient",
11350
+ position: "bottom",
11351
+ bounds: { x: legendX, y: legendY, width: legendWidth, height: legendBarHeight },
11352
+ labelStyle: {
11353
+ fontFamily: theme.fonts.family,
11354
+ fontSize: 11,
11355
+ fontWeight: 400,
11356
+ fill: theme.colors.text,
11357
+ lineHeight: 1.2
11358
+ },
11359
+ colorStops: gradientColorStops,
11360
+ minLabel: formatter(min4),
11361
+ maxLabel: formatter(max4)
11362
+ };
11363
+ }
11364
+ const tooltipDescriptors = /* @__PURE__ */ new Map();
11365
+ for (const tile of tiles) {
11366
+ const fields = [
11367
+ {
11368
+ label: "Value",
11369
+ value: tile.formattedValue
11370
+ }
11371
+ ];
11372
+ tooltipDescriptors.set(tile.stateCode, {
11373
+ title: STATE_NAMES[tile.stateCode] ?? tile.stateCode,
11374
+ fields
11375
+ });
11376
+ }
11377
+ const a11y = {
11378
+ altText: `Tile map of US states showing values from ${formatter(min4)} to ${formatter(max4)}`,
11379
+ dataTableFallback: tiles.map((t) => [t.stateCode, t.formattedValue]),
11380
+ role: "img",
11381
+ keyboardNavigable: tiles.length > 0
11382
+ };
11383
+ const resolvedAnimation = resolveAnimation(tilemapSpec.animation);
11384
+ return {
11385
+ area: fullArea,
11386
+ chrome,
11387
+ tiles,
11388
+ gradientLegend,
11389
+ tooltipDescriptors,
11390
+ a11y,
11391
+ theme,
11392
+ width: options.width,
11393
+ height: options.height,
11394
+ animation: resolvedAnimation,
11395
+ watermark,
11396
+ measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth14(text, fontSize), height: fontSize }))
11397
+ };
11398
+ }
11399
+ function emptyLayout2(chrome, theme, options, watermark) {
11400
+ return {
11401
+ area: { x: 0, y: 0, width: 0, height: 0 },
11402
+ chrome,
11403
+ tiles: [],
11404
+ gradientLegend: {
11405
+ type: "gradient",
11406
+ position: "bottom",
11407
+ bounds: { x: 0, y: 0, width: 0, height: 0 },
11408
+ labelStyle: {
11409
+ fontFamily: theme.fonts.family,
11410
+ fontSize: 11,
11411
+ fontWeight: 400,
11412
+ fill: theme.colors.text,
11413
+ lineHeight: 1.2
11414
+ },
11415
+ colorStops: [],
11416
+ minLabel: "0",
11417
+ maxLabel: "0"
11418
+ },
11419
+ tooltipDescriptors: /* @__PURE__ */ new Map(),
11420
+ a11y: {
11421
+ altText: "Empty tile map",
11422
+ dataTableFallback: [],
11423
+ role: "img",
11424
+ keyboardNavigable: false
11425
+ },
11426
+ theme,
11427
+ width: options.width,
11428
+ height: options.height,
11429
+ watermark,
11430
+ animation: void 0,
11431
+ measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth14(text, fontSize), height: fontSize }))
11432
+ };
11433
+ }
11434
+
10850
11435
  // src/tooltips/compute.ts
10851
11436
  import {
10852
11437
  buildTemporalFormatter as buildTemporalFormatter2,
10853
11438
  formatDate as formatDate3,
10854
- formatNumber as formatNumber5,
11439
+ formatNumber as formatNumber6,
10855
11440
  getRepresentativeColor as getRepresentativeColor10
10856
11441
  } from "@opendata-ai/openchart-core";
10857
11442
  function formatValue(value2, fieldType, format2) {
@@ -10866,10 +11451,10 @@ function formatValue(value2, fieldType, format2) {
10866
11451
  try {
10867
11452
  return format(format2)(value2);
10868
11453
  } catch {
10869
- return formatNumber5(value2);
11454
+ return formatNumber6(value2);
10870
11455
  }
10871
11456
  }
10872
- return formatNumber5(value2);
11457
+ return formatNumber6(value2);
10873
11458
  }
10874
11459
  return String(value2);
10875
11460
  }
@@ -11219,8 +11804,78 @@ function runCalculate(data, transform) {
11219
11804
  }
11220
11805
 
11221
11806
  // src/transforms/filter.ts
11807
+ function applyOffset2(anchor, offset, unit2) {
11808
+ const d = new Date(anchor.getTime());
11809
+ switch (unit2) {
11810
+ case "year":
11811
+ d.setFullYear(d.getFullYear() + offset);
11812
+ break;
11813
+ case "quarter":
11814
+ d.setMonth(d.getMonth() + offset * 3);
11815
+ break;
11816
+ case "month":
11817
+ d.setMonth(d.getMonth() + offset);
11818
+ break;
11819
+ case "week":
11820
+ d.setDate(d.getDate() + offset * 7);
11821
+ break;
11822
+ case "day":
11823
+ d.setDate(d.getDate() + offset);
11824
+ break;
11825
+ }
11826
+ return d.getTime();
11827
+ }
11828
+ function resolveRef(data, field, ref) {
11829
+ let anchorMs = ref.anchor === "max" ? -Infinity : Infinity;
11830
+ for (const row of data) {
11831
+ const val = row[field];
11832
+ if (val == null) continue;
11833
+ const ms = new Date(val).getTime();
11834
+ if (Number.isNaN(ms)) continue;
11835
+ if (ref.anchor === "max" && ms > anchorMs) anchorMs = ms;
11836
+ if (ref.anchor === "min" && ms < anchorMs) anchorMs = ms;
11837
+ }
11838
+ if (!Number.isFinite(anchorMs)) return 0;
11839
+ return applyOffset2(new Date(anchorMs), ref.offset, ref.unit);
11840
+ }
11841
+ function resolveRelativeRefs(data, predicate) {
11842
+ if ("and" in predicate) {
11843
+ return { and: predicate.and.map((p) => resolveRelativeRefs(data, p)) };
11844
+ }
11845
+ if ("or" in predicate) {
11846
+ return { or: predicate.or.map((p) => resolveRelativeRefs(data, p)) };
11847
+ }
11848
+ if ("not" in predicate) {
11849
+ return { not: resolveRelativeRefs(data, predicate.not) };
11850
+ }
11851
+ if ("field" in predicate) {
11852
+ const fp = predicate;
11853
+ let needsCopy = false;
11854
+ const resolved = {};
11855
+ for (const prop of ["lt", "lte", "gt", "gte"]) {
11856
+ if (isRelativeTimeRef(fp[prop])) {
11857
+ resolved[prop] = resolveRef(data, fp.field, fp[prop]);
11858
+ needsCopy = true;
11859
+ }
11860
+ }
11861
+ if (fp.range) {
11862
+ const [lo, hi] = fp.range;
11863
+ const loResolved = isRelativeTimeRef(lo) ? resolveRef(data, fp.field, lo) : lo;
11864
+ const hiResolved = isRelativeTimeRef(hi) ? resolveRef(data, fp.field, hi) : hi;
11865
+ if (isRelativeTimeRef(lo) || isRelativeTimeRef(hi)) {
11866
+ resolved.range = [loResolved, hiResolved];
11867
+ needsCopy = true;
11868
+ }
11869
+ }
11870
+ if (needsCopy) {
11871
+ return { ...fp, ...resolved };
11872
+ }
11873
+ }
11874
+ return predicate;
11875
+ }
11222
11876
  function runFilter(data, predicate) {
11223
- return data.filter((datum) => evaluatePredicate(datum, predicate));
11877
+ const resolved = resolveRelativeRefs(data, predicate);
11878
+ return data.filter((datum) => evaluatePredicate(datum, resolved));
11224
11879
  }
11225
11880
 
11226
11881
  // src/transforms/fold.ts
@@ -11312,6 +11967,131 @@ function runTimeUnit(data, transform) {
11312
11967
  });
11313
11968
  }
11314
11969
 
11970
+ // src/transforms/window.ts
11971
+ function groupKey2(row, groupby) {
11972
+ return groupby.map((f) => String(row[f] ?? "")).join("\0");
11973
+ }
11974
+ function tryParseDate(val) {
11975
+ if (val == null) return NaN;
11976
+ if (typeof val === "number") return new Date(val).getTime();
11977
+ if (typeof val === "string" && (val.includes("-") || val.includes("T"))) {
11978
+ const ms = new Date(val).getTime();
11979
+ return ms;
11980
+ }
11981
+ return NaN;
11982
+ }
11983
+ function compareValues(a, b, order) {
11984
+ const dir = order === "descending" ? -1 : 1;
11985
+ const dateA = tryParseDate(a);
11986
+ const dateB = tryParseDate(b);
11987
+ if (!Number.isNaN(dateA) && !Number.isNaN(dateB)) {
11988
+ return dir * (dateA - dateB);
11989
+ }
11990
+ const numA = Number(a);
11991
+ const numB = Number(b);
11992
+ if (Number.isFinite(numA) && Number.isFinite(numB)) {
11993
+ return dir * (numA - numB);
11994
+ }
11995
+ return dir * String(a ?? "").localeCompare(String(b ?? ""));
11996
+ }
11997
+ function runWindow(data, transform) {
11998
+ if (data.length === 0) return [];
11999
+ const { window: windowDefs, sort, groupby = [] } = transform;
12000
+ const indexed = data.map((row, i) => ({ row, originalIndex: i }));
12001
+ const groups = /* @__PURE__ */ new Map();
12002
+ for (const entry of indexed) {
12003
+ const key = groupby.length > 0 ? groupKey2(entry.row, groupby) : "";
12004
+ const existing = groups.get(key);
12005
+ if (existing) {
12006
+ existing.push(entry);
12007
+ } else {
12008
+ groups.set(key, [entry]);
12009
+ }
12010
+ }
12011
+ const result = new Array(data.length);
12012
+ for (const groupEntries of groups.values()) {
12013
+ const sorted = [...groupEntries].sort((a, b) => {
12014
+ for (const s of sort) {
12015
+ const cmp = compareValues(a.row[s.field], b.row[s.field], s.order ?? "ascending");
12016
+ if (cmp !== 0) return cmp;
12017
+ }
12018
+ return 0;
12019
+ });
12020
+ for (let i = 0; i < sorted.length; i++) {
12021
+ const entry = sorted[i];
12022
+ const outRow = { ...entry.row };
12023
+ for (const def of windowDefs) {
12024
+ const offset = def.offset ?? 1;
12025
+ let computed = null;
12026
+ switch (def.op) {
12027
+ case "lag": {
12028
+ const lagIdx = i - offset;
12029
+ computed = lagIdx >= 0 ? sorted[lagIdx].row[def.field] ?? null : null;
12030
+ break;
12031
+ }
12032
+ case "lead": {
12033
+ const leadIdx = i + offset;
12034
+ computed = leadIdx < sorted.length ? sorted[leadIdx].row[def.field] ?? null : null;
12035
+ break;
12036
+ }
12037
+ case "diff": {
12038
+ const lagIdx = i - offset;
12039
+ if (lagIdx >= 0) {
12040
+ const current = Number(entry.row[def.field]);
12041
+ const lagged = Number(sorted[lagIdx].row[def.field]);
12042
+ computed = Number.isFinite(current) && Number.isFinite(lagged) ? current - lagged : null;
12043
+ }
12044
+ break;
12045
+ }
12046
+ case "pct_change": {
12047
+ const lagIdx = i - offset;
12048
+ if (lagIdx >= 0) {
12049
+ const current = Number(entry.row[def.field]);
12050
+ const lagged = Number(sorted[lagIdx].row[def.field]);
12051
+ if (Number.isFinite(current) && Number.isFinite(lagged) && lagged !== 0) {
12052
+ computed = (current - lagged) / lagged;
12053
+ }
12054
+ }
12055
+ break;
12056
+ }
12057
+ case "cumsum": {
12058
+ const val = Number(entry.row[def.field]);
12059
+ const addend = Number.isFinite(val) ? val : 0;
12060
+ if (i === 0) {
12061
+ computed = addend;
12062
+ } else {
12063
+ const prev = Number(result[sorted[i - 1].originalIndex]?.[def.as] ?? 0);
12064
+ computed = prev + addend;
12065
+ }
12066
+ break;
12067
+ }
12068
+ case "rank": {
12069
+ let rank = i + 1;
12070
+ for (let j = 0; j < i; j++) {
12071
+ const isTie = sort.every(
12072
+ (s) => String(sorted[j].row[s.field]) === String(entry.row[s.field])
12073
+ );
12074
+ if (isTie) {
12075
+ rank = result[sorted[j].originalIndex]?.[def.as];
12076
+ break;
12077
+ }
12078
+ }
12079
+ computed = rank;
12080
+ break;
12081
+ }
12082
+ case "first_value": {
12083
+ computed = sorted[0].row[def.field] ?? null;
12084
+ break;
12085
+ }
12086
+ }
12087
+ outRow[def.as] = computed;
12088
+ }
12089
+ result[entry.originalIndex] = outRow;
12090
+ }
12091
+ }
12092
+ return result;
12093
+ }
12094
+
11315
12095
  // src/transforms/index.ts
11316
12096
  function runTransforms(data, transforms) {
11317
12097
  let result = data;
@@ -11328,6 +12108,8 @@ function runTransforms(data, transforms) {
11328
12108
  result = runAggregate(result, transform);
11329
12109
  } else if ("fold" in transform) {
11330
12110
  result = runFold(result, transform);
12111
+ } else if ("window" in transform) {
12112
+ result = runWindow(result, transform);
11331
12113
  }
11332
12114
  }
11333
12115
  return result;
@@ -11451,9 +12233,9 @@ function compileChart(spec, options) {
11451
12233
  const rawAnimationSpec = overrides?.[breakpoint]?.animation ?? rawSpec.animation;
11452
12234
  const resolvedAnimation = resolveAnimation(rawAnimationSpec);
11453
12235
  const mergedThemeConfig = options.theme ? { ...chartSpec.theme, ...options.theme } : chartSpec.theme;
11454
- let theme = resolveTheme3(mergedThemeConfig);
12236
+ let theme = resolveTheme4(mergedThemeConfig);
11455
12237
  if (options.darkMode) {
11456
- theme = adaptTheme3(theme);
12238
+ theme = adaptTheme4(theme);
11457
12239
  }
11458
12240
  const preliminaryArea = {
11459
12241
  x: 0,
@@ -11465,7 +12247,7 @@ function compileChart(spec, options) {
11465
12247
  const dims = computeDimensions(chartSpec, options, legendLayout, theme, strategy, watermark);
11466
12248
  const chartArea = dims.chartArea;
11467
12249
  const legendArea = { ...chartArea };
11468
- if (legendLayout.entries.length > 0) {
12250
+ if ("entries" in legendLayout && legendLayout.entries.length > 0) {
11469
12251
  const gap = legendGap(options.width);
11470
12252
  switch (legendLayout.position) {
11471
12253
  case "top":
@@ -11494,7 +12276,10 @@ function compileChart(spec, options) {
11494
12276
  applyColorScaleRange(scales, renderSpec.encoding, theme);
11495
12277
  scales.defaultColor = chartSpec.markDef.fill ?? chartSpec.markDef.stroke ?? theme.colors.categorical[0];
11496
12278
  const isRadial = chartSpec.markType === "arc";
11497
- const axes = isRadial ? { x: void 0, y: void 0 } : computeAxes(scales, chartArea, strategy, theme, options.measureText);
12279
+ const axes = isRadial ? { x: void 0, y: void 0 } : computeAxes(scales, chartArea, strategy, theme, options.measureText, {
12280
+ data: renderSpec.data,
12281
+ encoding: renderSpec.encoding
12282
+ });
11498
12283
  if (!isRadial) {
11499
12284
  computeGridlines(axes, chartArea);
11500
12285
  }
@@ -11588,7 +12373,8 @@ function compileLayer(spec, options) {
11588
12373
  const primaryLayout = compileChart(primarySpec, options);
11589
12374
  const allMarks = [];
11590
12375
  const seenLabels = /* @__PURE__ */ new Set();
11591
- const mergedLegendEntries = [...primaryLayout.legend.entries];
12376
+ const pLegend = primaryLayout.legend;
12377
+ const mergedLegendEntries = "entries" in pLegend ? [...pLegend.entries] : [];
11592
12378
  for (const entry of mergedLegendEntries) {
11593
12379
  seenLabels.add(entry.label);
11594
12380
  }
@@ -11600,10 +12386,13 @@ function compileLayer(spec, options) {
11600
12386
  for (const { leaf } of indexedLeaves) {
11601
12387
  const leafLayout = compileChart(leaf, options);
11602
12388
  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);
12389
+ const leafLeg = leafLayout.legend;
12390
+ if ("entries" in leafLeg) {
12391
+ for (const entry of leafLeg.entries) {
12392
+ if (!seenLabels.has(entry.label)) {
12393
+ seenLabels.add(entry.label);
12394
+ mergedLegendEntries.push(entry);
12395
+ }
11607
12396
  }
11608
12397
  }
11609
12398
  }
@@ -11612,7 +12401,7 @@ function compileLayer(spec, options) {
11612
12401
  marks: allMarks,
11613
12402
  legend: {
11614
12403
  ...primaryLayout.legend,
11615
- entries: mergedLegendEntries
12404
+ ..."entries" in pLegend ? { entries: mergedLegendEntries } : {}
11616
12405
  }
11617
12406
  };
11618
12407
  }
@@ -11626,7 +12415,7 @@ function estimateYAxisLabelWidth(data, encoding, baseFontSize) {
11626
12415
  let maxWidth = 0;
11627
12416
  for (const row of data) {
11628
12417
  const label = String(row[yField] ?? "");
11629
- const w = estimateTextWidth14(label, baseFontSize, 400);
12418
+ const w = estimateTextWidth15(label, baseFontSize, 400);
11630
12419
  if (w > maxWidth) maxWidth = w;
11631
12420
  }
11632
12421
  return maxWidth > 0 ? maxWidth + 10 : 40;
@@ -11655,7 +12444,7 @@ function estimateYAxisLabelWidth(data, encoding, baseFontSize) {
11655
12444
  }
11656
12445
  const hasNeg = data.some((r) => Number(r[yField]) < 0);
11657
12446
  const labelEst = (hasNeg ? "-" : "") + sampleLabel;
11658
- return estimateTextWidth14(labelEst, baseFontSize, 400) + 10;
12447
+ return estimateTextWidth15(labelEst, baseFontSize, 400) + 10;
11659
12448
  }
11660
12449
  function compileLayerIndependent(leaves, layerSpec, options) {
11661
12450
  if (leaves.length > 2) {
@@ -11672,7 +12461,7 @@ function compileLayerIndependent(leaves, layerSpec, options) {
11672
12461
  `Dual-axis charts require matching x-field types across layers. Layer 0 has '${xType0}', layer 1 has '${xType1}'.`
11673
12462
  );
11674
12463
  }
11675
- const theme = resolveTheme3(layerSpec.theme ?? leaf1.theme);
12464
+ const theme = resolveTheme4(layerSpec.theme ?? leaf1.theme);
11676
12465
  const axisFontSize = theme.fonts?.sizes?.axisTick ?? 11;
11677
12466
  const rightAxisWidth = estimateYAxisLabelWidth(leaf1.data, leaf1.encoding, axisFontSize);
11678
12467
  const hasRightAxisTitle = !!leaf1.encoding?.y?.axis?.title;
@@ -11789,9 +12578,12 @@ function compileLayerIndependent(leaves, layerSpec, options) {
11789
12578
  return tagged;
11790
12579
  });
11791
12580
  const seenLabels = /* @__PURE__ */ new Set();
11792
- const mergedLegendEntries = [...layout0.legend.entries];
12581
+ const l0Legend = layout0.legend;
12582
+ const l1Legend = layout1.legend;
12583
+ const mergedLegendEntries = "entries" in l0Legend ? [...l0Legend.entries] : [];
11793
12584
  for (const entry of mergedLegendEntries) seenLabels.add(entry.label);
11794
- for (const entry of layout1.legend.entries) {
12585
+ const l1Entries = "entries" in l1Legend ? l1Legend.entries : [];
12586
+ for (const entry of l1Entries) {
11795
12587
  if (!seenLabels.has(entry.label)) {
11796
12588
  seenLabels.add(entry.label);
11797
12589
  mergedLegendEntries.push(entry);
@@ -11821,7 +12613,7 @@ function compileLayerIndependent(leaves, layerSpec, options) {
11821
12613
  marks,
11822
12614
  legend: {
11823
12615
  ...layout0.legend,
11824
- entries: mergedLegendEntries
12616
+ ..."entries" in l0Legend ? { entries: mergedLegendEntries } : {}
11825
12617
  },
11826
12618
  tooltipDescriptors: mergedTooltips
11827
12619
  };
@@ -11934,9 +12726,9 @@ function compileTable(spec, options) {
11934
12726
  }
11935
12727
  const tableSpec = normalized;
11936
12728
  const mergedThemeConfig = options.theme ? { ...tableSpec.theme, ...options.theme } : tableSpec.theme;
11937
- let theme = resolveTheme3(mergedThemeConfig);
12729
+ let theme = resolveTheme4(mergedThemeConfig);
11938
12730
  if (options.darkMode) {
11939
- theme = adaptTheme3(theme);
12731
+ theme = adaptTheme4(theme);
11940
12732
  }
11941
12733
  const rawWatermark = spec.watermark;
11942
12734
  const watermark = rawWatermark !== void 0 ? tableSpec.watermark : options.watermark ?? true;
@@ -11948,6 +12740,9 @@ function compileGraph2(spec, options) {
11948
12740
  function compileSankey2(spec, options) {
11949
12741
  return compileSankey(spec, options);
11950
12742
  }
12743
+ function compileTileMap2(spec, options) {
12744
+ return compileTileMap(spec, options);
12745
+ }
11951
12746
  export {
11952
12747
  clampStaggerDelay,
11953
12748
  clearRenderers,
@@ -11957,6 +12752,7 @@ export {
11957
12752
  compileLayer,
11958
12753
  compileSankey2 as compileSankey,
11959
12754
  compileTable,
12755
+ compileTileMap2 as compileTileMap,
11960
12756
  evaluatePredicate,
11961
12757
  getChartRenderer,
11962
12758
  isConditionalValueDef,