@opendata-ai/openchart-engine 6.25.3 → 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 };
@@ -6651,6 +6798,16 @@ function normalizeAnnotations(annotations) {
6651
6798
  }
6652
6799
  });
6653
6800
  }
6801
+ function normalizeLabels(labels) {
6802
+ if (labels === false) return { density: "none", format: "", prefix: "" };
6803
+ if (labels === true || labels === void 0) return { density: "auto", format: "", prefix: "" };
6804
+ return {
6805
+ density: labels.density ?? "auto",
6806
+ format: labels.format ?? "",
6807
+ prefix: labels.prefix ?? "",
6808
+ offsets: labels.offsets
6809
+ };
6810
+ }
6654
6811
  function normalizeChartSpec(spec, warnings) {
6655
6812
  const encoding = inferEncodingTypes(spec.encoding, spec.data, warnings);
6656
6813
  const markType = resolveMarkType(spec.mark);
@@ -6662,12 +6819,7 @@ function normalizeChartSpec(spec, warnings) {
6662
6819
  encoding,
6663
6820
  chrome: normalizeChrome(spec.chrome),
6664
6821
  annotations: normalizeAnnotations(spec.annotations),
6665
- labels: {
6666
- density: spec.labels?.density ?? "auto",
6667
- format: spec.labels?.format ?? "",
6668
- prefix: spec.labels?.prefix ?? "",
6669
- offsets: spec.labels?.offsets
6670
- },
6822
+ labels: normalizeLabels(spec.labels),
6671
6823
  legend: spec.legend,
6672
6824
  responsive: spec.responsive ?? true,
6673
6825
  theme: spec.theme ?? {},
@@ -6741,6 +6893,45 @@ function normalizeGraphSpec(spec, _warnings) {
6741
6893
  watermark: spec.watermark ?? true
6742
6894
  };
6743
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
+ }
6744
6935
  function normalizeSpec(spec, warnings = []) {
6745
6936
  if (isLayerSpec(spec)) {
6746
6937
  const leaves = flattenLayers(spec);
@@ -6761,8 +6952,11 @@ function normalizeSpec(spec, warnings = []) {
6761
6952
  if (isSankeySpec(spec)) {
6762
6953
  return normalizeSankeySpec(spec, warnings);
6763
6954
  }
6955
+ if (isTileMapSpec(spec)) {
6956
+ return normalizeTileMapSpec(spec, warnings);
6957
+ }
6764
6958
  throw new Error(
6765
- `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'.`
6766
6960
  );
6767
6961
  }
6768
6962
  function flattenLayers(spec, parentData, parentEncoding, parentTransforms, parentWatermark) {
@@ -7312,6 +7506,96 @@ function validateSankeySpec(spec, errors) {
7312
7506
  });
7313
7507
  }
7314
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
+ }
7315
7599
  function validateLayerSpec(spec, errors) {
7316
7600
  const layer = spec.layer;
7317
7601
  if (layer.length === 0) {
@@ -7406,17 +7690,18 @@ function validateSpec(spec) {
7406
7690
  const isTable = obj.type === "table";
7407
7691
  const isGraph = obj.type === "graph";
7408
7692
  const isSankey = obj.type === "sankey";
7409
- const isLayer = hasLayer && !isTable && !isGraph && !isSankey;
7410
- const isChart = hasMark && !hasLayer && !isTable && !isGraph && !isSankey;
7411
- 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) {
7412
7697
  return {
7413
7698
  valid: false,
7414
7699
  errors: [
7415
7700
  {
7416
- 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',
7417
7702
  path: "mark",
7418
7703
  code: "MISSING_FIELD",
7419
- 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(", ")}`
7420
7705
  }
7421
7706
  ],
7422
7707
  normalized: null
@@ -7454,6 +7739,8 @@ function validateSpec(spec) {
7454
7739
  validateGraphSpec(obj, errors);
7455
7740
  } else if (isSankey) {
7456
7741
  validateSankeySpec(obj, errors);
7742
+ } else if (isTileMap) {
7743
+ validateTileMapSpec(obj, errors);
7457
7744
  }
7458
7745
  if (errors.length > 0) {
7459
7746
  return { valid: false, errors, normalized: null };
@@ -8016,7 +8303,7 @@ function scaleSupportsTickCount(resolvedScale) {
8016
8303
  const scale = resolvedScale.scale;
8017
8304
  return "ticks" in scale && typeof scale.ticks === "function";
8018
8305
  }
8019
- function categoricalTicks(resolvedScale, density, orientation = "horizontal", bandwidth, labelAngle, fontSize, fontWeight, measureText) {
8306
+ function categoricalTicks(resolvedScale, density, orientation = "horizontal", bandwidth, labelAngle, fontSize, fontWeight, measureText, subtitleContext) {
8020
8307
  const scale = resolvedScale.scale;
8021
8308
  const domain = scale.domain();
8022
8309
  const explicitTickCount = resolvedScale.channel.axis?.tickCount;
@@ -8052,14 +8339,37 @@ function categoricalTicks(resolvedScale, density, orientation = "horizontal", ba
8052
8339
  selectedValues = domain.filter((_, i) => i % step === 0);
8053
8340
  }
8054
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
+ }
8055
8358
  const ticks2 = selectedValues.map((value2) => {
8056
8359
  const bandScale = resolvedScale.type === "band" ? scale : null;
8057
8360
  const pos = bandScale ? (bandScale(value2) ?? 0) + bandScale.bandwidth() / 2 : scale(value2) ?? 0;
8058
- return {
8361
+ const tick = {
8059
8362
  value: value2,
8060
8363
  position: pos,
8061
8364
  label: value2
8062
8365
  };
8366
+ if (subtitleMap) {
8367
+ const subtitle = subtitleMap.get(value2);
8368
+ if (subtitle !== void 0) {
8369
+ tick.subtitle = subtitle;
8370
+ }
8371
+ }
8372
+ return tick;
8063
8373
  });
8064
8374
  return ticks2;
8065
8375
  }
@@ -8131,7 +8441,7 @@ function fitContinuousTicks(scale, initialTicks, initialCount, fontSize, fontWei
8131
8441
  const fallback = bestWithinFloor ?? buildContinuousTicks(scale, floor);
8132
8442
  return thinTicksUntilFit(fallback, fontSize, fontWeight, measureText, orientation);
8133
8443
  }
8134
- function computeAxes(scales, chartArea, strategy, theme, measureText) {
8444
+ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContext) {
8135
8445
  const result = {};
8136
8446
  const baseDensity = strategy.axisLabelDensity;
8137
8447
  const yDensity = effectiveDensity(
@@ -8249,7 +8559,19 @@ function computeAxes(scales, chartArea, strategy, theme, measureText) {
8249
8559
  if (axisConfig?.values) {
8250
8560
  allTicks = resolveExplicitTicks(axisConfig.values, scales.y);
8251
8561
  } else if (!isContinuousY) {
8252
- 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
+ );
8253
8575
  } else {
8254
8576
  allTicks = continuousTicks(scales.y, yDensity, yTargetCount);
8255
8577
  }
@@ -8483,10 +8805,23 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
8483
8805
  if (encoding.y && !isRadial) {
8484
8806
  if (spec.markType === "bar" || spec.markType === "circle" || spec.markType === "lollipop" || encoding.y.type === "nominal" || encoding.y.type === "ordinal") {
8485
8807
  const yField = encoding.y.field;
8808
+ const yLabelField = encoding.y.axis?.labelField;
8486
8809
  let maxLabelWidth = 0;
8487
8810
  for (const row of spec.data) {
8488
8811
  const label = String(row[yField] ?? "");
8489
- 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
+ }
8490
8825
  if (w > maxLabelWidth) maxLabelWidth = w;
8491
8826
  }
8492
8827
  if (maxLabelWidth > 0) {
@@ -8540,7 +8875,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
8540
8875
  if (options.rightAxisReserve && options.rightAxisReserve > 0) {
8541
8876
  margins.right = Math.max(margins.right, hPad + options.rightAxisReserve);
8542
8877
  }
8543
- if (legendLayout.entries.length > 0) {
8878
+ if ("entries" in legendLayout && legendLayout.entries.length > 0) {
8544
8879
  const gap = legendGap(width);
8545
8880
  if (legendLayout.position === "right" || legendLayout.position === "bottom-right") {
8546
8881
  margins.right += legendLayout.bounds.width + 8;
@@ -8574,7 +8909,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
8574
8909
  const bottomDelta = margins.bottom - newBottom;
8575
8910
  if (topDelta > 0 || bottomDelta > 0) {
8576
8911
  const gap = legendGap(width);
8577
- 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);
8578
8913
  margins.bottom = newBottom;
8579
8914
  chartArea = {
8580
8915
  x: margins.left,
@@ -9899,7 +10234,7 @@ function compileSankey(spec, options) {
9899
10234
  theme,
9900
10235
  fullArea
9901
10236
  );
9902
- const legendGap2 = legend.entries.length > 0 ? 4 : 0;
10237
+ const legendGap2 = "entries" in legend && legend.entries.length > 0 ? 4 : 0;
9903
10238
  const area = {
9904
10239
  x: fullArea.x,
9905
10240
  y: fullArea.y + legend.bounds.height + legendGap2,
@@ -10842,11 +11177,266 @@ function compileTableLayout(spec, options, theme) {
10842
11177
  };
10843
11178
  }
10844
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
+
10845
11435
  // src/tooltips/compute.ts
10846
11436
  import {
10847
11437
  buildTemporalFormatter as buildTemporalFormatter2,
10848
11438
  formatDate as formatDate3,
10849
- formatNumber as formatNumber5,
11439
+ formatNumber as formatNumber6,
10850
11440
  getRepresentativeColor as getRepresentativeColor10
10851
11441
  } from "@opendata-ai/openchart-core";
10852
11442
  function formatValue(value2, fieldType, format2) {
@@ -10861,10 +11451,10 @@ function formatValue(value2, fieldType, format2) {
10861
11451
  try {
10862
11452
  return format(format2)(value2);
10863
11453
  } catch {
10864
- return formatNumber5(value2);
11454
+ return formatNumber6(value2);
10865
11455
  }
10866
11456
  }
10867
- return formatNumber5(value2);
11457
+ return formatNumber6(value2);
10868
11458
  }
10869
11459
  return String(value2);
10870
11460
  }
@@ -11214,8 +11804,78 @@ function runCalculate(data, transform) {
11214
11804
  }
11215
11805
 
11216
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
+ }
11217
11876
  function runFilter(data, predicate) {
11218
- return data.filter((datum) => evaluatePredicate(datum, predicate));
11877
+ const resolved = resolveRelativeRefs(data, predicate);
11878
+ return data.filter((datum) => evaluatePredicate(datum, resolved));
11219
11879
  }
11220
11880
 
11221
11881
  // src/transforms/fold.ts
@@ -11307,6 +11967,131 @@ function runTimeUnit(data, transform) {
11307
11967
  });
11308
11968
  }
11309
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
+
11310
12095
  // src/transforms/index.ts
11311
12096
  function runTransforms(data, transforms) {
11312
12097
  let result = data;
@@ -11323,6 +12108,8 @@ function runTransforms(data, transforms) {
11323
12108
  result = runAggregate(result, transform);
11324
12109
  } else if ("fold" in transform) {
11325
12110
  result = runFold(result, transform);
12111
+ } else if ("window" in transform) {
12112
+ result = runWindow(result, transform);
11326
12113
  }
11327
12114
  }
11328
12115
  return result;
@@ -11410,14 +12197,21 @@ function compileChart(spec, options) {
11410
12197
  }
11411
12198
  };
11412
12199
  }
11413
- if (bp.labels) {
11414
- chartSpec = {
11415
- ...chartSpec,
11416
- labels: {
11417
- ...chartSpec.labels,
11418
- ...bp.labels
11419
- }
11420
- };
12200
+ if (bp.labels !== void 0) {
12201
+ if (typeof bp.labels === "boolean") {
12202
+ chartSpec = {
12203
+ ...chartSpec,
12204
+ labels: bp.labels ? { density: "auto", format: "", prefix: "" } : { density: "none", format: "", prefix: "" }
12205
+ };
12206
+ } else {
12207
+ chartSpec = {
12208
+ ...chartSpec,
12209
+ labels: {
12210
+ ...chartSpec.labels,
12211
+ ...bp.labels
12212
+ }
12213
+ };
12214
+ }
11421
12215
  }
11422
12216
  if (bp.legend) {
11423
12217
  chartSpec = {
@@ -11439,9 +12233,9 @@ function compileChart(spec, options) {
11439
12233
  const rawAnimationSpec = overrides?.[breakpoint]?.animation ?? rawSpec.animation;
11440
12234
  const resolvedAnimation = resolveAnimation(rawAnimationSpec);
11441
12235
  const mergedThemeConfig = options.theme ? { ...chartSpec.theme, ...options.theme } : chartSpec.theme;
11442
- let theme = resolveTheme3(mergedThemeConfig);
12236
+ let theme = resolveTheme4(mergedThemeConfig);
11443
12237
  if (options.darkMode) {
11444
- theme = adaptTheme3(theme);
12238
+ theme = adaptTheme4(theme);
11445
12239
  }
11446
12240
  const preliminaryArea = {
11447
12241
  x: 0,
@@ -11453,7 +12247,7 @@ function compileChart(spec, options) {
11453
12247
  const dims = computeDimensions(chartSpec, options, legendLayout, theme, strategy, watermark);
11454
12248
  const chartArea = dims.chartArea;
11455
12249
  const legendArea = { ...chartArea };
11456
- if (legendLayout.entries.length > 0) {
12250
+ if ("entries" in legendLayout && legendLayout.entries.length > 0) {
11457
12251
  const gap = legendGap(options.width);
11458
12252
  switch (legendLayout.position) {
11459
12253
  case "top":
@@ -11482,7 +12276,10 @@ function compileChart(spec, options) {
11482
12276
  applyColorScaleRange(scales, renderSpec.encoding, theme);
11483
12277
  scales.defaultColor = chartSpec.markDef.fill ?? chartSpec.markDef.stroke ?? theme.colors.categorical[0];
11484
12278
  const isRadial = chartSpec.markType === "arc";
11485
- 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
+ });
11486
12283
  if (!isRadial) {
11487
12284
  computeGridlines(axes, chartArea);
11488
12285
  }
@@ -11576,7 +12373,8 @@ function compileLayer(spec, options) {
11576
12373
  const primaryLayout = compileChart(primarySpec, options);
11577
12374
  const allMarks = [];
11578
12375
  const seenLabels = /* @__PURE__ */ new Set();
11579
- const mergedLegendEntries = [...primaryLayout.legend.entries];
12376
+ const pLegend = primaryLayout.legend;
12377
+ const mergedLegendEntries = "entries" in pLegend ? [...pLegend.entries] : [];
11580
12378
  for (const entry of mergedLegendEntries) {
11581
12379
  seenLabels.add(entry.label);
11582
12380
  }
@@ -11588,10 +12386,13 @@ function compileLayer(spec, options) {
11588
12386
  for (const { leaf } of indexedLeaves) {
11589
12387
  const leafLayout = compileChart(leaf, options);
11590
12388
  allMarks.push(...leafLayout.marks);
11591
- for (const entry of leafLayout.legend.entries) {
11592
- if (!seenLabels.has(entry.label)) {
11593
- seenLabels.add(entry.label);
11594
- 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
+ }
11595
12396
  }
11596
12397
  }
11597
12398
  }
@@ -11600,7 +12401,7 @@ function compileLayer(spec, options) {
11600
12401
  marks: allMarks,
11601
12402
  legend: {
11602
12403
  ...primaryLayout.legend,
11603
- entries: mergedLegendEntries
12404
+ ..."entries" in pLegend ? { entries: mergedLegendEntries } : {}
11604
12405
  }
11605
12406
  };
11606
12407
  }
@@ -11614,7 +12415,7 @@ function estimateYAxisLabelWidth(data, encoding, baseFontSize) {
11614
12415
  let maxWidth = 0;
11615
12416
  for (const row of data) {
11616
12417
  const label = String(row[yField] ?? "");
11617
- const w = estimateTextWidth14(label, baseFontSize, 400);
12418
+ const w = estimateTextWidth15(label, baseFontSize, 400);
11618
12419
  if (w > maxWidth) maxWidth = w;
11619
12420
  }
11620
12421
  return maxWidth > 0 ? maxWidth + 10 : 40;
@@ -11643,7 +12444,7 @@ function estimateYAxisLabelWidth(data, encoding, baseFontSize) {
11643
12444
  }
11644
12445
  const hasNeg = data.some((r) => Number(r[yField]) < 0);
11645
12446
  const labelEst = (hasNeg ? "-" : "") + sampleLabel;
11646
- return estimateTextWidth14(labelEst, baseFontSize, 400) + 10;
12447
+ return estimateTextWidth15(labelEst, baseFontSize, 400) + 10;
11647
12448
  }
11648
12449
  function compileLayerIndependent(leaves, layerSpec, options) {
11649
12450
  if (leaves.length > 2) {
@@ -11660,7 +12461,7 @@ function compileLayerIndependent(leaves, layerSpec, options) {
11660
12461
  `Dual-axis charts require matching x-field types across layers. Layer 0 has '${xType0}', layer 1 has '${xType1}'.`
11661
12462
  );
11662
12463
  }
11663
- const theme = resolveTheme3(layerSpec.theme ?? leaf1.theme);
12464
+ const theme = resolveTheme4(layerSpec.theme ?? leaf1.theme);
11664
12465
  const axisFontSize = theme.fonts?.sizes?.axisTick ?? 11;
11665
12466
  const rightAxisWidth = estimateYAxisLabelWidth(leaf1.data, leaf1.encoding, axisFontSize);
11666
12467
  const hasRightAxisTitle = !!leaf1.encoding?.y?.axis?.title;
@@ -11777,9 +12578,12 @@ function compileLayerIndependent(leaves, layerSpec, options) {
11777
12578
  return tagged;
11778
12579
  });
11779
12580
  const seenLabels = /* @__PURE__ */ new Set();
11780
- const mergedLegendEntries = [...layout0.legend.entries];
12581
+ const l0Legend = layout0.legend;
12582
+ const l1Legend = layout1.legend;
12583
+ const mergedLegendEntries = "entries" in l0Legend ? [...l0Legend.entries] : [];
11781
12584
  for (const entry of mergedLegendEntries) seenLabels.add(entry.label);
11782
- for (const entry of layout1.legend.entries) {
12585
+ const l1Entries = "entries" in l1Legend ? l1Legend.entries : [];
12586
+ for (const entry of l1Entries) {
11783
12587
  if (!seenLabels.has(entry.label)) {
11784
12588
  seenLabels.add(entry.label);
11785
12589
  mergedLegendEntries.push(entry);
@@ -11809,7 +12613,7 @@ function compileLayerIndependent(leaves, layerSpec, options) {
11809
12613
  marks,
11810
12614
  legend: {
11811
12615
  ...layout0.legend,
11812
- entries: mergedLegendEntries
12616
+ ..."entries" in l0Legend ? { entries: mergedLegendEntries } : {}
11813
12617
  },
11814
12618
  tooltipDescriptors: mergedTooltips
11815
12619
  };
@@ -11922,9 +12726,9 @@ function compileTable(spec, options) {
11922
12726
  }
11923
12727
  const tableSpec = normalized;
11924
12728
  const mergedThemeConfig = options.theme ? { ...tableSpec.theme, ...options.theme } : tableSpec.theme;
11925
- let theme = resolveTheme3(mergedThemeConfig);
12729
+ let theme = resolveTheme4(mergedThemeConfig);
11926
12730
  if (options.darkMode) {
11927
- theme = adaptTheme3(theme);
12731
+ theme = adaptTheme4(theme);
11928
12732
  }
11929
12733
  const rawWatermark = spec.watermark;
11930
12734
  const watermark = rawWatermark !== void 0 ? tableSpec.watermark : options.watermark ?? true;
@@ -11936,6 +12740,9 @@ function compileGraph2(spec, options) {
11936
12740
  function compileSankey2(spec, options) {
11937
12741
  return compileSankey(spec, options);
11938
12742
  }
12743
+ function compileTileMap2(spec, options) {
12744
+ return compileTileMap(spec, options);
12745
+ }
11939
12746
  export {
11940
12747
  clampStaggerDelay,
11941
12748
  clearRenderers,
@@ -11945,6 +12752,7 @@ export {
11945
12752
  compileLayer,
11946
12753
  compileSankey2 as compileSankey,
11947
12754
  compileTable,
12755
+ compileTileMap2 as compileTileMap,
11948
12756
  evaluatePredicate,
11949
12757
  getChartRenderer,
11950
12758
  isConditionalValueDef,