@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.d.ts +46 -4
- package/dist/index.js +862 -66
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/compound-labels.test.ts +147 -0
- package/src/compile.ts +47 -13
- package/src/compiler/normalize.ts +57 -1
- package/src/compiler/types.ts +3 -1
- package/src/compiler/validate.ts +124 -5
- package/src/index.ts +16 -1
- package/src/layout/axes/ticks.ts +34 -2
- package/src/layout/axes.ts +27 -1
- package/src/layout/dimensions.ts +21 -3
- package/src/sankey/compile-sankey.ts +1 -1
- package/src/tilemap/__tests__/compile-tilemap.test.ts +322 -0
- package/src/tilemap/compile-tilemap.ts +383 -0
- package/src/tilemap/layout.ts +172 -0
- package/src/tilemap/types.ts +32 -0
- package/src/transforms/__tests__/filter-relative.test.ts +202 -0
- package/src/transforms/__tests__/window.test.ts +286 -0
- package/src/transforms/filter.ts +108 -3
- package/src/transforms/index.ts +5 -1
- package/src/transforms/predicates.ts +39 -9
- package/src/transforms/window.ts +185 -0
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
|
|
4
|
+
adaptTheme as adaptTheme4,
|
|
5
5
|
BREAKPOINT_COMPACT_MAX as BREAKPOINT_COMPACT_MAX2,
|
|
6
6
|
computeLabelBounds,
|
|
7
|
-
estimateTextWidth as
|
|
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
|
|
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
|
-
|
|
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 [
|
|
4142
|
-
return numValue >=
|
|
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
|
|
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,
|
|
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}, ${
|
|
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
|
|
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(
|
|
4410
|
-
const color2 = getColor(scales,
|
|
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}, ${
|
|
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
|
|
4442
|
-
const color2 = getColor(scales,
|
|
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}, ${
|
|
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
|
|
4814
|
-
const color2 = getColor(scales,
|
|
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}, ${
|
|
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
|
|
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(
|
|
4861
|
-
const color2 = getColor(scales,
|
|
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}, ${
|
|
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
|
|
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,
|
|
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}, ${
|
|
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: '
|
|
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
|
|
7415
|
-
const
|
|
7416
|
-
|
|
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: "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
11454
|
+
return formatNumber6(value2);
|
|
10870
11455
|
}
|
|
10871
11456
|
}
|
|
10872
|
-
return
|
|
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
|
-
|
|
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 =
|
|
12236
|
+
let theme = resolveTheme4(mergedThemeConfig);
|
|
11455
12237
|
if (options.darkMode) {
|
|
11456
|
-
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
|
|
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
|
-
|
|
11604
|
-
|
|
11605
|
-
|
|
11606
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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 =
|
|
12729
|
+
let theme = resolveTheme4(mergedThemeConfig);
|
|
11938
12730
|
if (options.darkMode) {
|
|
11939
|
-
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,
|