@opendata-ai/openchart-engine 8.8.0 → 8.9.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 +37 -2
- package/dist/index.js +1090 -382
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,7 +1,210 @@
|
|
|
1
|
+
// src/design/diagnostics.ts
|
|
2
|
+
import { computeLabelBounds, contrastRatio, isOpaqueColor } from "@opendata-ai/openchart-core";
|
|
3
|
+
|
|
4
|
+
// src/design/identity.ts
|
|
5
|
+
function observationKey(value) {
|
|
6
|
+
if (typeof value === "string") return `string:${value}`;
|
|
7
|
+
if (typeof value === "number" && Number.isFinite(value)) return `number:${value}`;
|
|
8
|
+
if (typeof value === "boolean") return `boolean:${value}`;
|
|
9
|
+
if (value instanceof Date && Number.isFinite(value.getTime()))
|
|
10
|
+
return `date:${value.toISOString()}`;
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
function resolveObservationIdentities(data, keyField, seriesField) {
|
|
14
|
+
const keys = data.map((row) => keyField ? observationKey(row[keyField]) : null);
|
|
15
|
+
const counts = /* @__PURE__ */ new Map();
|
|
16
|
+
for (const key of keys) if (key !== null) counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
17
|
+
return data.map((row, index) => ({
|
|
18
|
+
observation: keys[index] !== null && counts.get(keys[index]) === 1 ? keys[index] : `revision-row:${index}`,
|
|
19
|
+
persistent: keys[index] !== null && counts.get(keys[index]) === 1,
|
|
20
|
+
series: seriesField ? observationKey(row[seriesField]) ?? void 0 : void 0
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
function reconcileReaderSelection(selection, rows, keyField, seriesField, visibleKeys) {
|
|
24
|
+
const identities = resolveObservationIdentities(rows, keyField, seriesField);
|
|
25
|
+
const byKey = new Map(identities.filter((id) => id.persistent).map((id) => [id.observation, id]));
|
|
26
|
+
const data = new Map(identities.map((id, i) => [id.observation, rows[i]]));
|
|
27
|
+
let cleared = false;
|
|
28
|
+
const pinned = selection.pinned.flatMap((item) => {
|
|
29
|
+
const id = byKey.get(item.observation);
|
|
30
|
+
if (!item.persistent || !id) {
|
|
31
|
+
cleared = true;
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
const datum = data.get(id.observation);
|
|
35
|
+
return [
|
|
36
|
+
{
|
|
37
|
+
...id,
|
|
38
|
+
datum,
|
|
39
|
+
previous: JSON.stringify(datum) === JSON.stringify(item.datum) ? void 0 : item.datum,
|
|
40
|
+
state: visibleKeys && !visibleKeys.has(id.observation) ? "filtered" : "visible"
|
|
41
|
+
}
|
|
42
|
+
];
|
|
43
|
+
});
|
|
44
|
+
return {
|
|
45
|
+
preview: null,
|
|
46
|
+
pinned,
|
|
47
|
+
announcement: cleared ? "Selection cleared: the observation was removed or no longer has a unique key." : ""
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function compareObservations(before, after, a, b) {
|
|
51
|
+
if (![before, after].every(Number.isFinite) || !a.measure || a.measure !== b.measure || a.unit !== b.unit || a.representation !== b.representation)
|
|
52
|
+
return null;
|
|
53
|
+
const percent = a.representation === "percent" || a.representation === "proportion";
|
|
54
|
+
return {
|
|
55
|
+
absolute: (after - before) * (a.representation === "proportion" ? 100 : 1),
|
|
56
|
+
...before !== 0 ? { relative: (after - before) / Math.abs(before) } : {},
|
|
57
|
+
unit: percent ? "percentage points" : a.unit
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function linkedEntityKeys(row, source, target) {
|
|
61
|
+
if (source.dataset !== target.dataset) return [];
|
|
62
|
+
const value = row[source.entityField];
|
|
63
|
+
if (source.kind === target.kind && source.transform === target.transform) {
|
|
64
|
+
const key = observationKey(value);
|
|
65
|
+
return key ? [key] : [];
|
|
66
|
+
}
|
|
67
|
+
if (source.kind === "group" && target.kind === "row") {
|
|
68
|
+
const group = String(value);
|
|
69
|
+
const members = source.groupMembers && Object.hasOwn(source.groupMembers, group) ? source.groupMembers[group] : [];
|
|
70
|
+
return members.flatMap((member) => {
|
|
71
|
+
const key = observationKey(member);
|
|
72
|
+
return key ? [key] : [];
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/design/diagnostics.ts
|
|
79
|
+
function diagnoseChart(spec, layout) {
|
|
80
|
+
const diagnostics = [];
|
|
81
|
+
const viewport = layout.dimensions;
|
|
82
|
+
const emit = (code, path, message, origin, status = "failed", severity = "warning") => {
|
|
83
|
+
diagnostics.push({ code, path, message, origin, status, severity, viewport });
|
|
84
|
+
};
|
|
85
|
+
const key = spec.encoding.key?.field;
|
|
86
|
+
if (key && resolveObservationIdentities(spec.data, key).some((id) => !id.persistent)) {
|
|
87
|
+
emit(
|
|
88
|
+
"identity.invalid-key",
|
|
89
|
+
"/encoding/key",
|
|
90
|
+
"Observation keys must be present and unique. Ambiguous selections cannot persist across revisions.",
|
|
91
|
+
"author",
|
|
92
|
+
"failed",
|
|
93
|
+
"error"
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
const channels = [spec.encoding.x, spec.encoding.y].filter(
|
|
97
|
+
(channel) => channel && "field" in channel
|
|
98
|
+
);
|
|
99
|
+
for (const channel of channels) {
|
|
100
|
+
if (!channel || !("field" in channel)) continue;
|
|
101
|
+
const missing = spec.data.filter(
|
|
102
|
+
(row) => row[channel.field] == null || row[channel.field] === ""
|
|
103
|
+
).length;
|
|
104
|
+
if (missing)
|
|
105
|
+
emit(
|
|
106
|
+
"data.missing",
|
|
107
|
+
"/data",
|
|
108
|
+
`${missing} observations have no ${channel.field}. Missing values are not zero; explain omissions or gaps in the methodology.`,
|
|
109
|
+
"author"
|
|
110
|
+
);
|
|
111
|
+
const meta = spec.fields?.[channel.field];
|
|
112
|
+
if (meta?.representation === "percent" && channel.format?.includes("%")) {
|
|
113
|
+
emit(
|
|
114
|
+
"format.percent-scale",
|
|
115
|
+
"/encoding",
|
|
116
|
+
"Percent values are already 0\u2013100; a percent formatter multiplies them by 100. Choose a number format or supply proportions.",
|
|
117
|
+
"author"
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const colors = spec.encoding.color;
|
|
122
|
+
if (colors && "field" in colors && colors.type === "nominal") {
|
|
123
|
+
const count = new Set(spec.data.map((row) => row[colors.field])).size;
|
|
124
|
+
if (count > 6)
|
|
125
|
+
emit(
|
|
126
|
+
"color.crowded",
|
|
127
|
+
"/encoding/color",
|
|
128
|
+
`${count} series compete for color. Highlight an authored focus or use small multiples.`,
|
|
129
|
+
"author"
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
const background = layout.theme.colors.background;
|
|
133
|
+
if (!isOpaqueColor(background)) {
|
|
134
|
+
emit(
|
|
135
|
+
"contrast.unknown-background",
|
|
136
|
+
"/theme/colors/background",
|
|
137
|
+
"Contrast cannot be verified against a transparent background. Supply the host surface color for a check.",
|
|
138
|
+
spec.theme?.colors ? "author" : "default",
|
|
139
|
+
"uncheckable",
|
|
140
|
+
"info"
|
|
141
|
+
);
|
|
142
|
+
} else if (contrastRatio(layout.theme.colors.axis, background) < 4.5) {
|
|
143
|
+
emit(
|
|
144
|
+
"contrast.axis",
|
|
145
|
+
"/theme/colors/axis",
|
|
146
|
+
"Axis text contrast is below 4.5:1 against the resolved background.",
|
|
147
|
+
spec.theme?.colors ? "author" : "default"
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
const labels = layout.marks.flatMap(
|
|
151
|
+
(mark) => "label" in mark && mark.label?.visible ? [computeLabelBounds(mark.label)] : []
|
|
152
|
+
);
|
|
153
|
+
let collision = false;
|
|
154
|
+
for (let i = 0; i < labels.length && !collision; i++) {
|
|
155
|
+
for (let j = i + 1; j < labels.length; j++) {
|
|
156
|
+
const a = labels[i];
|
|
157
|
+
const b = labels[j];
|
|
158
|
+
if (a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y) {
|
|
159
|
+
collision = true;
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (collision)
|
|
165
|
+
emit(
|
|
166
|
+
"labels.collision",
|
|
167
|
+
"/labels",
|
|
168
|
+
"Visible data labels overlap at this viewport. Increase space or reduce label density.",
|
|
169
|
+
spec.labels ? "author" : "default"
|
|
170
|
+
);
|
|
171
|
+
if (spec.labels && typeof spec.labels === "object" && spec.labels.density === "all" && layout.marks.some((mark) => "label" in mark && mark.label && !mark.label.visible)) {
|
|
172
|
+
emit(
|
|
173
|
+
"labels.hidden",
|
|
174
|
+
"/labels",
|
|
175
|
+
"Some requested labels cannot fit at this viewport. Inspect the data view or give the figure more room.",
|
|
176
|
+
"author"
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
if (layout.area.width < 80 || layout.area.height < 80)
|
|
180
|
+
emit(
|
|
181
|
+
"layout.constrained",
|
|
182
|
+
"/chromeLayout",
|
|
183
|
+
"Essential chart content has less than 80px of plotting space. Grow the figure or simplify its composition.",
|
|
184
|
+
spec.chromeLayout ? "author" : "default"
|
|
185
|
+
);
|
|
186
|
+
if (layout.theme.fonts.sizes.axisTick < 12) {
|
|
187
|
+
emit(
|
|
188
|
+
"type.small-axis",
|
|
189
|
+
"/theme/fonts/sizes/axisTick",
|
|
190
|
+
"Axis labels are smaller than the 12px editorial target.",
|
|
191
|
+
spec.theme?.fonts?.sizes?.axisTick !== void 0 ? "author" : "default",
|
|
192
|
+
"failed",
|
|
193
|
+
"info"
|
|
194
|
+
);
|
|
195
|
+
diagnostics[diagnostics.length - 1].fix = {
|
|
196
|
+
label: "Use 12px axis labels",
|
|
197
|
+
precondition: JSON.stringify(spec),
|
|
198
|
+
changes: [{ path: "/theme/fonts/sizes/axisTick", value: 12 }]
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
return diagnostics;
|
|
202
|
+
}
|
|
203
|
+
|
|
1
204
|
// src/compile.ts
|
|
2
205
|
import {
|
|
3
206
|
adaptTheme as adaptTheme6,
|
|
4
|
-
computeLabelBounds,
|
|
207
|
+
computeLabelBounds as computeLabelBounds2,
|
|
5
208
|
footnoteBandHeight,
|
|
6
209
|
generateAltText,
|
|
7
210
|
generateDataTable,
|
|
@@ -11,7 +214,8 @@ import {
|
|
|
11
214
|
isAxislessMark as isAxislessMark3,
|
|
12
215
|
MAX_SR_TABLE_ROWS,
|
|
13
216
|
resolveChromeEconomy,
|
|
14
|
-
resolveTheme as resolveTheme7
|
|
217
|
+
resolveTheme as resolveTheme7,
|
|
218
|
+
wrapText as wrapText3
|
|
15
219
|
} from "@opendata-ai/openchart-core";
|
|
16
220
|
|
|
17
221
|
// src/annotations/collisions.ts
|
|
@@ -2298,6 +2502,14 @@ function dedupeKeys(keys) {
|
|
|
2298
2502
|
return result;
|
|
2299
2503
|
}
|
|
2300
2504
|
|
|
2505
|
+
// src/data/finite-number.ts
|
|
2506
|
+
function finiteNumber(value) {
|
|
2507
|
+
if (typeof value !== "number" && typeof value !== "string") return void 0;
|
|
2508
|
+
if (typeof value === "string" && value.trim() === "") return void 0;
|
|
2509
|
+
const number = Number(value);
|
|
2510
|
+
return Number.isFinite(number) ? number : void 0;
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2301
2513
|
// src/transforms/predicates.ts
|
|
2302
2514
|
function isFieldPredicate(pred) {
|
|
2303
2515
|
return "field" in pred;
|
|
@@ -2429,9 +2641,10 @@ function scaleValue(scale, scaleType, value) {
|
|
|
2429
2641
|
const result = scale(String(value));
|
|
2430
2642
|
return result ?? null;
|
|
2431
2643
|
}
|
|
2432
|
-
const
|
|
2433
|
-
if (
|
|
2434
|
-
|
|
2644
|
+
const number = finiteNumber(value);
|
|
2645
|
+
if (number === void 0) return null;
|
|
2646
|
+
const position = scale(number);
|
|
2647
|
+
return Number.isFinite(position) ? position : null;
|
|
2435
2648
|
}
|
|
2436
2649
|
function groupByField(data, field) {
|
|
2437
2650
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -2520,6 +2733,7 @@ function computeBarMarks(spec, scales, _chartArea, _strategy, containerWidth, th
|
|
|
2520
2733
|
if (typeof yScale.bandwidth !== "function") {
|
|
2521
2734
|
return [];
|
|
2522
2735
|
}
|
|
2736
|
+
const measuredData = spec.data.filter((row) => finiteNumber(row[xChannel.field]) !== void 0);
|
|
2523
2737
|
const bandwidth = yScale.bandwidth();
|
|
2524
2738
|
const xDomain = xScale.domain();
|
|
2525
2739
|
const xIncludesZero = xDomain[0] <= 0 && xDomain[1] >= 0;
|
|
@@ -2531,7 +2745,7 @@ function computeBarMarks(spec, scales, _chartArea, _strategy, containerWidth, th
|
|
|
2531
2745
|
let marks;
|
|
2532
2746
|
if (!colorField || isSequentialColor) {
|
|
2533
2747
|
marks = computeSimpleBars(
|
|
2534
|
-
|
|
2748
|
+
measuredData,
|
|
2535
2749
|
xChannel.field,
|
|
2536
2750
|
yChannel.field,
|
|
2537
2751
|
xScale,
|
|
@@ -2544,12 +2758,12 @@ function computeBarMarks(spec, scales, _chartArea, _strategy, containerWidth, th
|
|
|
2544
2758
|
);
|
|
2545
2759
|
} else {
|
|
2546
2760
|
const categoryGroups = groupByField(spec.data, yChannel.field);
|
|
2547
|
-
const needsStacking = Array.from(categoryGroups.values()).some((rows) => rows.length > 1);
|
|
2761
|
+
const needsStacking = xChannel.stack === "normalize" || xChannel.stack === "center" || Array.from(categoryGroups.values()).some((rows) => rows.length > 1);
|
|
2548
2762
|
if (needsStacking) {
|
|
2549
2763
|
const stackEnabled = xChannel.stack !== null && xChannel.stack !== false;
|
|
2550
2764
|
if (!stackEnabled) {
|
|
2551
2765
|
marks = computeGroupedBars(
|
|
2552
|
-
|
|
2766
|
+
measuredData,
|
|
2553
2767
|
xChannel.field,
|
|
2554
2768
|
yChannel.field,
|
|
2555
2769
|
colorField,
|
|
@@ -2563,7 +2777,7 @@ function computeBarMarks(spec, scales, _chartArea, _strategy, containerWidth, th
|
|
|
2563
2777
|
} else {
|
|
2564
2778
|
const stackMode = xChannel.stack === "normalize" ? "normalize" : xChannel.stack === "center" ? "center" : "zero";
|
|
2565
2779
|
marks = computeStackedBars(
|
|
2566
|
-
|
|
2780
|
+
measuredData,
|
|
2567
2781
|
xChannel.field,
|
|
2568
2782
|
yChannel.field,
|
|
2569
2783
|
colorField,
|
|
@@ -2577,7 +2791,7 @@ function computeBarMarks(spec, scales, _chartArea, _strategy, containerWidth, th
|
|
|
2577
2791
|
}
|
|
2578
2792
|
} else {
|
|
2579
2793
|
marks = computeColoredBars(
|
|
2580
|
-
|
|
2794
|
+
measuredData,
|
|
2581
2795
|
xChannel.field,
|
|
2582
2796
|
yChannel.field,
|
|
2583
2797
|
colorField,
|
|
@@ -2618,16 +2832,16 @@ function computeStackedBars(data, valueField, categoryField, colorField, xScale,
|
|
|
2618
2832
|
}
|
|
2619
2833
|
let cumulativeValue = stackMode === "center" ? -categoryTotal / 2 : 0;
|
|
2620
2834
|
for (const row of rows) {
|
|
2621
|
-
const
|
|
2835
|
+
const groupKey2 = String(row[colorField] ?? "");
|
|
2622
2836
|
const rawValue = Number(row[valueField] ?? 0);
|
|
2623
2837
|
if (!Number.isFinite(rawValue) || rawValue <= 0) continue;
|
|
2624
2838
|
const value = stackMode === "normalize" && categoryTotal > 0 ? rawValue / categoryTotal : rawValue;
|
|
2625
|
-
const color = getColor(scales,
|
|
2839
|
+
const color = getColor(scales, groupKey2);
|
|
2626
2840
|
const xLeft = xScale(cumulativeValue);
|
|
2627
2841
|
const xRight = xScale(cumulativeValue + value);
|
|
2628
2842
|
const barWidth = Math.max(Math.abs(xRight - xLeft), MIN_BAR_WIDTH);
|
|
2629
2843
|
const aria = {
|
|
2630
|
-
label: `${category}, ${
|
|
2844
|
+
label: `${category}, ${groupKey2}: ${formatLabelValue(rawValue)}`
|
|
2631
2845
|
};
|
|
2632
2846
|
marks.push({
|
|
2633
2847
|
type: "rect",
|
|
@@ -2637,7 +2851,7 @@ function computeStackedBars(data, valueField, categoryField, colorField, xScale,
|
|
|
2637
2851
|
height: bandwidth,
|
|
2638
2852
|
fill: isGradientDef(color) ? orientGradientForHorizontalBar(color) : color,
|
|
2639
2853
|
cornerRadius: 0,
|
|
2640
|
-
seriesKey:
|
|
2854
|
+
seriesKey: groupKey2,
|
|
2641
2855
|
data: row,
|
|
2642
2856
|
aria,
|
|
2643
2857
|
orient: "horizontal",
|
|
@@ -2676,16 +2890,16 @@ function computeGroupedBars(data, valueField, categoryField, colorField, xScale,
|
|
|
2676
2890
|
const bandY = yScale(category);
|
|
2677
2891
|
if (bandY === void 0) continue;
|
|
2678
2892
|
for (const row of rows) {
|
|
2679
|
-
const
|
|
2893
|
+
const groupKey2 = String(row[colorField] ?? "");
|
|
2680
2894
|
const value = Number(row[valueField] ?? 0);
|
|
2681
2895
|
if (!Number.isFinite(value)) continue;
|
|
2682
|
-
const groupIndex = groupIndexMap.get(
|
|
2683
|
-
const color = getColor(scales,
|
|
2896
|
+
const groupIndex = groupIndexMap.get(groupKey2) ?? 0;
|
|
2897
|
+
const color = getColor(scales, groupKey2);
|
|
2684
2898
|
const xPos = value >= 0 ? baseline : xScale(value);
|
|
2685
2899
|
const barWidth = Math.max(Math.abs(xScale(value) - baseline), MIN_BAR_WIDTH);
|
|
2686
2900
|
const subY = bandY + groupOffset + groupIndex * (subBandHeight + gap);
|
|
2687
2901
|
const aria = {
|
|
2688
|
-
label: `${category}, ${
|
|
2902
|
+
label: `${category}, ${groupKey2}: ${formatLabelValue(value)}`
|
|
2689
2903
|
};
|
|
2690
2904
|
marks.push({
|
|
2691
2905
|
type: "rect",
|
|
@@ -2696,7 +2910,7 @@ function computeGroupedBars(data, valueField, categoryField, colorField, xScale,
|
|
|
2696
2910
|
fill: isGradientDef(color) ? orientGradientForHorizontalBar(color) : color,
|
|
2697
2911
|
cornerRadius: BAR_CORNER_RADIUS,
|
|
2698
2912
|
cornerRadiusSides: valueEndCorners("horizontal", value < 0),
|
|
2699
|
-
seriesKey:
|
|
2913
|
+
seriesKey: groupKey2,
|
|
2700
2914
|
data: row,
|
|
2701
2915
|
aria,
|
|
2702
2916
|
orient: "horizontal"
|
|
@@ -2713,12 +2927,12 @@ function computeColoredBars(data, valueField, categoryField, colorField, xScale,
|
|
|
2713
2927
|
if (!Number.isFinite(value)) continue;
|
|
2714
2928
|
const bandY = yScale(category);
|
|
2715
2929
|
if (bandY === void 0) continue;
|
|
2716
|
-
const
|
|
2717
|
-
const color = getColor(scales,
|
|
2930
|
+
const groupKey2 = String(row[colorField] ?? "");
|
|
2931
|
+
const color = getColor(scales, groupKey2);
|
|
2718
2932
|
const xPos = value >= 0 ? baseline : xScale(value);
|
|
2719
2933
|
const barWidth = Math.max(Math.abs(xScale(value) - baseline), MIN_BAR_WIDTH);
|
|
2720
2934
|
const aria = {
|
|
2721
|
-
label: `${category}, ${
|
|
2935
|
+
label: `${category}, ${groupKey2}: ${formatLabelValue(value)}`
|
|
2722
2936
|
};
|
|
2723
2937
|
marks.push({
|
|
2724
2938
|
type: "rect",
|
|
@@ -2729,7 +2943,7 @@ function computeColoredBars(data, valueField, categoryField, colorField, xScale,
|
|
|
2729
2943
|
fill: isGradientDef(color) ? orientGradientForHorizontalBar(color) : color,
|
|
2730
2944
|
cornerRadius: BAR_CORNER_RADIUS,
|
|
2731
2945
|
cornerRadiusSides: valueEndCorners("horizontal", value < 0),
|
|
2732
|
-
seriesKey:
|
|
2946
|
+
seriesKey: groupKey2,
|
|
2733
2947
|
data: row,
|
|
2734
2948
|
aria,
|
|
2735
2949
|
orient: "horizontal"
|
|
@@ -3062,10 +3276,8 @@ function buildSizeScale(sizeEncoding, data, options) {
|
|
|
3062
3276
|
const field = sizeEncoding.field;
|
|
3063
3277
|
const values = [];
|
|
3064
3278
|
for (const row of data) {
|
|
3065
|
-
const
|
|
3066
|
-
if (
|
|
3067
|
-
const v = Number(raw);
|
|
3068
|
-
if (Number.isFinite(v)) values.push(v);
|
|
3279
|
+
const value = finiteNumber(row[field]);
|
|
3280
|
+
if (value !== void 0) values.push(value);
|
|
3069
3281
|
}
|
|
3070
3282
|
const explicitDomain = sizeEncoding.scale?.domain;
|
|
3071
3283
|
if (!explicitDomain && values.length === 0) return null;
|
|
@@ -3166,10 +3378,8 @@ function computeBeeswarmMarks(spec, scales, chartArea, _strategy, theme) {
|
|
|
3166
3378
|
const sizeScale = resolvedSize?.scale;
|
|
3167
3379
|
const dots = [];
|
|
3168
3380
|
for (const row of spec.data) {
|
|
3169
|
-
const
|
|
3170
|
-
if (
|
|
3171
|
-
const value = Number(raw);
|
|
3172
|
-
if (!Number.isFinite(value)) continue;
|
|
3381
|
+
const value = finiteNumber(row[valueChannel.field]);
|
|
3382
|
+
if (value === void 0) continue;
|
|
3173
3383
|
const position = positionScale(value);
|
|
3174
3384
|
if (!Number.isFinite(position)) continue;
|
|
3175
3385
|
let lane = "__default__";
|
|
@@ -3181,12 +3391,12 @@ function computeBeeswarmMarks(spec, scales, chartArea, _strategy, theme) {
|
|
|
3181
3391
|
laneCenter = bandStart + bandScale.bandwidth() / 2;
|
|
3182
3392
|
}
|
|
3183
3393
|
let radius = DEFAULT_DOT_RADIUS2;
|
|
3184
|
-
if (
|
|
3185
|
-
const
|
|
3186
|
-
if (
|
|
3187
|
-
|
|
3188
|
-
}
|
|
3394
|
+
if (sizeField) {
|
|
3395
|
+
const sizeValue = finiteNumber(row[sizeField]);
|
|
3396
|
+
if (sizeValue === void 0) continue;
|
|
3397
|
+
if (sizeScale) radius = sizeScale(sizeValue);
|
|
3189
3398
|
}
|
|
3399
|
+
if (!Number.isFinite(radius) || radius < 0) continue;
|
|
3190
3400
|
dots.push({ row, value, position, radius, lane, laneCenter });
|
|
3191
3401
|
}
|
|
3192
3402
|
const offsets = dodgeOffsets(
|
|
@@ -3202,8 +3412,8 @@ function computeBeeswarmMarks(spec, scales, chartArea, _strategy, theme) {
|
|
|
3202
3412
|
const cross = dot.laneCenter + offsets[i];
|
|
3203
3413
|
let color;
|
|
3204
3414
|
if (isSequentialColor && colorField) {
|
|
3205
|
-
const
|
|
3206
|
-
color =
|
|
3415
|
+
const value = finiteNumber(dot.row[colorField]);
|
|
3416
|
+
color = value !== void 0 ? getSequentialColor(scales, value) : getColor(scales, "__default__");
|
|
3207
3417
|
} else if (colorField) {
|
|
3208
3418
|
color = getColor(scales, String(dot.row[colorField] ?? "__default__"));
|
|
3209
3419
|
} else {
|
|
@@ -3274,9 +3484,9 @@ function computeBinnedBarMarks(spec, scales, _chartArea, _strategy, theme) {
|
|
|
3274
3484
|
return marks;
|
|
3275
3485
|
}
|
|
3276
3486
|
function binGeometry(row, xField, x2Field, xScale) {
|
|
3277
|
-
const start =
|
|
3278
|
-
const end =
|
|
3279
|
-
if (
|
|
3487
|
+
const start = finiteNumber(row[xField]);
|
|
3488
|
+
const end = finiteNumber(row[x2Field]);
|
|
3489
|
+
if (start === void 0 || end === void 0) return void 0;
|
|
3280
3490
|
const px0 = xScale(start);
|
|
3281
3491
|
const px1 = xScale(end);
|
|
3282
3492
|
const left = Math.min(px0, px1);
|
|
@@ -3304,8 +3514,8 @@ function overlappingBins(groups, spec, scales, xScale, yScale, baseline, colorFi
|
|
|
3304
3514
|
for (const row of rows) {
|
|
3305
3515
|
const geom = binGeometry(row, xField, x2Field, xScale);
|
|
3306
3516
|
if (!geom) continue;
|
|
3307
|
-
const value =
|
|
3308
|
-
if (
|
|
3517
|
+
const value = finiteNumber(row[yField]);
|
|
3518
|
+
if (value === void 0) continue;
|
|
3309
3519
|
const endY = yScale(value);
|
|
3310
3520
|
const top = Math.min(baseline, endY);
|
|
3311
3521
|
const height = Math.max(Math.abs(baseline - endY), MIN_BAR_HEIGHT);
|
|
@@ -3341,8 +3551,8 @@ function stackedBins(groups, spec, scales, xScale, yScale, baseline, theme) {
|
|
|
3341
3551
|
for (const row of rows) {
|
|
3342
3552
|
const geom = binGeometry(row, xField, x2Field, xScale);
|
|
3343
3553
|
if (!geom) continue;
|
|
3344
|
-
const value =
|
|
3345
|
-
if (
|
|
3554
|
+
const value = finiteNumber(row[yField]);
|
|
3555
|
+
if (value === void 0 || value <= 0) continue;
|
|
3346
3556
|
const binKey = serializeKeyValue(row[xField]);
|
|
3347
3557
|
const used = offsets.get(binKey) ?? 0;
|
|
3348
3558
|
const segmentHeight = Math.max(baseline - yScale(value), MIN_BAR_HEIGHT);
|
|
@@ -3441,8 +3651,8 @@ function computeCalendarMarks(spec, scales, chartArea, theme) {
|
|
|
3441
3651
|
const day = parseUtcDay(row[xEnc.field]);
|
|
3442
3652
|
if (!day) continue;
|
|
3443
3653
|
years.add(day.getUTCFullYear());
|
|
3444
|
-
const value =
|
|
3445
|
-
if (
|
|
3654
|
+
const value = finiteNumber(row[colorEnc.field]);
|
|
3655
|
+
if (value === void 0) continue;
|
|
3446
3656
|
dayData.set(isoDayKey(day), { row, value });
|
|
3447
3657
|
}
|
|
3448
3658
|
if (years.size === 0) return [];
|
|
@@ -3603,6 +3813,7 @@ function computeColumnMarks(spec, scales, _chartArea, _strategy, theme) {
|
|
|
3603
3813
|
if (typeof xScale.bandwidth !== "function") {
|
|
3604
3814
|
return [];
|
|
3605
3815
|
}
|
|
3816
|
+
const measuredData = spec.data.filter((row) => finiteNumber(row[yChannel.field]) !== void 0);
|
|
3606
3817
|
const bandwidth = xScale.bandwidth();
|
|
3607
3818
|
const yDomain = yScale.domain();
|
|
3608
3819
|
const yIncludesZero = yDomain[0] <= 0 && yDomain[1] >= 0;
|
|
@@ -3614,12 +3825,12 @@ function computeColumnMarks(spec, scales, _chartArea, _strategy, theme) {
|
|
|
3614
3825
|
let marks;
|
|
3615
3826
|
if (colorField && !isSequentialColor) {
|
|
3616
3827
|
const categoryGroups = groupByField(spec.data, xChannel.field);
|
|
3617
|
-
const needsStacking = Array.from(categoryGroups.values()).some((rows) => rows.length > 1);
|
|
3828
|
+
const needsStacking = yChannel.stack === "normalize" || yChannel.stack === "center" || Array.from(categoryGroups.values()).some((rows) => rows.length > 1);
|
|
3618
3829
|
if (needsStacking) {
|
|
3619
3830
|
const stackEnabled = yChannel.stack !== null && yChannel.stack !== false;
|
|
3620
3831
|
if (!stackEnabled) {
|
|
3621
3832
|
marks = computeGroupedColumns(
|
|
3622
|
-
|
|
3833
|
+
measuredData,
|
|
3623
3834
|
xChannel.field,
|
|
3624
3835
|
yChannel.field,
|
|
3625
3836
|
colorField,
|
|
@@ -3632,7 +3843,7 @@ function computeColumnMarks(spec, scales, _chartArea, _strategy, theme) {
|
|
|
3632
3843
|
} else {
|
|
3633
3844
|
const stackMode = yChannel.stack === "normalize" ? "normalize" : yChannel.stack === "center" ? "center" : "zero";
|
|
3634
3845
|
marks = computeStackedColumns(
|
|
3635
|
-
|
|
3846
|
+
measuredData,
|
|
3636
3847
|
xChannel.field,
|
|
3637
3848
|
yChannel.field,
|
|
3638
3849
|
colorField,
|
|
@@ -3646,7 +3857,7 @@ function computeColumnMarks(spec, scales, _chartArea, _strategy, theme) {
|
|
|
3646
3857
|
}
|
|
3647
3858
|
} else {
|
|
3648
3859
|
marks = computeColoredColumns(
|
|
3649
|
-
|
|
3860
|
+
measuredData,
|
|
3650
3861
|
xChannel.field,
|
|
3651
3862
|
yChannel.field,
|
|
3652
3863
|
colorField,
|
|
@@ -3659,7 +3870,7 @@ function computeColumnMarks(spec, scales, _chartArea, _strategy, theme) {
|
|
|
3659
3870
|
}
|
|
3660
3871
|
} else {
|
|
3661
3872
|
marks = computeSimpleColumns(
|
|
3662
|
-
|
|
3873
|
+
measuredData,
|
|
3663
3874
|
xChannel.field,
|
|
3664
3875
|
yChannel.field,
|
|
3665
3876
|
xScale,
|
|
@@ -3738,13 +3949,13 @@ function computeColoredColumns(data, categoryField, valueField, colorField, xSca
|
|
|
3738
3949
|
if (!Number.isFinite(value)) continue;
|
|
3739
3950
|
const bandX = xScale(category);
|
|
3740
3951
|
if (bandX === void 0) continue;
|
|
3741
|
-
const
|
|
3742
|
-
const color = getColor(scales,
|
|
3952
|
+
const groupKey2 = String(row[colorField] ?? "");
|
|
3953
|
+
const color = getColor(scales, groupKey2);
|
|
3743
3954
|
const yPos = yScale(value);
|
|
3744
3955
|
const columnHeight = Math.max(Math.abs(baseline - yPos), MIN_COLUMN_HEIGHT);
|
|
3745
3956
|
const y = value >= 0 ? yPos : baseline;
|
|
3746
3957
|
const aria = {
|
|
3747
|
-
label: `${category}, ${
|
|
3958
|
+
label: `${category}, ${groupKey2}: ${formatLabelValue(value)}`
|
|
3748
3959
|
};
|
|
3749
3960
|
marks.push({
|
|
3750
3961
|
type: "rect",
|
|
@@ -3755,7 +3966,7 @@ function computeColoredColumns(data, categoryField, valueField, colorField, xSca
|
|
|
3755
3966
|
fill: color,
|
|
3756
3967
|
cornerRadius: BAR_CORNER_RADIUS,
|
|
3757
3968
|
cornerRadiusSides: valueEndCorners("vertical", value < 0),
|
|
3758
|
-
seriesKey:
|
|
3969
|
+
seriesKey: groupKey2,
|
|
3759
3970
|
data: row,
|
|
3760
3971
|
aria,
|
|
3761
3972
|
orient: "vertical"
|
|
@@ -3784,17 +3995,17 @@ function computeGroupedColumns(data, categoryField, valueField, colorField, xSca
|
|
|
3784
3995
|
const bandX = xScale(category);
|
|
3785
3996
|
if (bandX === void 0) continue;
|
|
3786
3997
|
for (const row of rows) {
|
|
3787
|
-
const
|
|
3998
|
+
const groupKey2 = String(row[colorField] ?? "");
|
|
3788
3999
|
const value = Number(row[valueField] ?? 0);
|
|
3789
4000
|
if (!Number.isFinite(value)) continue;
|
|
3790
|
-
const groupIndex = groupIndexMap.get(
|
|
3791
|
-
const color = getColor(scales,
|
|
4001
|
+
const groupIndex = groupIndexMap.get(groupKey2) ?? 0;
|
|
4002
|
+
const color = getColor(scales, groupKey2);
|
|
3792
4003
|
const yPos = yScale(value);
|
|
3793
4004
|
const columnHeight = Math.max(Math.abs(baseline - yPos), MIN_COLUMN_HEIGHT);
|
|
3794
4005
|
const y = value >= 0 ? yPos : baseline;
|
|
3795
4006
|
const subX = bandX + groupIndex * (subBandWidth + gap);
|
|
3796
4007
|
const aria = {
|
|
3797
|
-
label: `${category}, ${
|
|
4008
|
+
label: `${category}, ${groupKey2}: ${formatLabelValue(value)}`
|
|
3798
4009
|
};
|
|
3799
4010
|
marks.push({
|
|
3800
4011
|
type: "rect",
|
|
@@ -3805,7 +4016,7 @@ function computeGroupedColumns(data, categoryField, valueField, colorField, xSca
|
|
|
3805
4016
|
fill: color,
|
|
3806
4017
|
cornerRadius: BAR_CORNER_RADIUS,
|
|
3807
4018
|
cornerRadiusSides: valueEndCorners("vertical", value < 0),
|
|
3808
|
-
seriesKey:
|
|
4019
|
+
seriesKey: groupKey2,
|
|
3809
4020
|
data: row,
|
|
3810
4021
|
aria,
|
|
3811
4022
|
orient: "vertical"
|
|
@@ -3827,16 +4038,16 @@ function computeStackedColumns(data, categoryField, valueField, colorField, xSca
|
|
|
3827
4038
|
}
|
|
3828
4039
|
let cumulativeValue = stackMode === "center" ? -categoryTotal / 2 : 0;
|
|
3829
4040
|
for (const row of rows) {
|
|
3830
|
-
const
|
|
4041
|
+
const groupKey2 = String(row[colorField] ?? "");
|
|
3831
4042
|
const rawValue = Number(row[valueField] ?? 0);
|
|
3832
4043
|
if (!Number.isFinite(rawValue) || rawValue <= 0) continue;
|
|
3833
4044
|
const value = stackMode === "normalize" && categoryTotal > 0 ? rawValue / categoryTotal : rawValue;
|
|
3834
|
-
const color = getColor(scales,
|
|
4045
|
+
const color = getColor(scales, groupKey2);
|
|
3835
4046
|
const yTop = yScale(cumulativeValue + value);
|
|
3836
4047
|
const yBottom = yScale(cumulativeValue);
|
|
3837
4048
|
const columnHeight = Math.max(Math.abs(yBottom - yTop), MIN_COLUMN_HEIGHT);
|
|
3838
4049
|
const aria = {
|
|
3839
|
-
label: `${category}, ${
|
|
4050
|
+
label: `${category}, ${groupKey2}: ${formatLabelValue(rawValue)}`
|
|
3840
4051
|
};
|
|
3841
4052
|
marks.push({
|
|
3842
4053
|
type: "rect",
|
|
@@ -3846,7 +4057,7 @@ function computeStackedColumns(data, categoryField, valueField, colorField, xSca
|
|
|
3846
4057
|
height: columnHeight,
|
|
3847
4058
|
fill: color,
|
|
3848
4059
|
cornerRadius: 0,
|
|
3849
|
-
seriesKey:
|
|
4060
|
+
seriesKey: groupKey2,
|
|
3850
4061
|
data: row,
|
|
3851
4062
|
aria,
|
|
3852
4063
|
orient: "vertical",
|
|
@@ -4117,8 +4328,8 @@ function computeDumbbellMarks(data, valueField, categoryField, colorField, xScal
|
|
|
4117
4328
|
const cy = bandY + bandwidth / 2;
|
|
4118
4329
|
const xValues = [];
|
|
4119
4330
|
for (const row of rows) {
|
|
4120
|
-
const value =
|
|
4121
|
-
if (
|
|
4331
|
+
const value = finiteNumber(row[valueField]);
|
|
4332
|
+
if (value !== void 0) xValues.push(value);
|
|
4122
4333
|
}
|
|
4123
4334
|
if (xValues.length === 0) continue;
|
|
4124
4335
|
const minVal = Math.min(...xValues);
|
|
@@ -4142,8 +4353,8 @@ function computeDumbbellMarks(data, valueField, categoryField, colorField, xScal
|
|
|
4142
4353
|
});
|
|
4143
4354
|
}
|
|
4144
4355
|
for (const row of rows) {
|
|
4145
|
-
const value =
|
|
4146
|
-
if (
|
|
4356
|
+
const value = finiteNumber(row[valueField]);
|
|
4357
|
+
if (value === void 0) continue;
|
|
4147
4358
|
const cx = xScale(value);
|
|
4148
4359
|
const colorCategory = String(row[colorField] ?? "");
|
|
4149
4360
|
const color = getColor(scales, colorCategory);
|
|
@@ -4169,8 +4380,8 @@ function computeLollipopMarks(data, valueField, categoryField, xScale, yScale, b
|
|
|
4169
4380
|
const marks = [];
|
|
4170
4381
|
for (const row of data) {
|
|
4171
4382
|
const category = String(row[categoryField] ?? "");
|
|
4172
|
-
const value =
|
|
4173
|
-
if (
|
|
4383
|
+
const value = finiteNumber(row[valueField]);
|
|
4384
|
+
if (value === void 0) continue;
|
|
4174
4385
|
const bandY = yScale(category);
|
|
4175
4386
|
if (bandY === void 0) continue;
|
|
4176
4387
|
const cx = xScale(value);
|
|
@@ -4504,8 +4715,8 @@ function computeLineMarks(spec, scales, _chartArea, _strategy, theme) {
|
|
|
4504
4715
|
const visible = seriesShowPoints && !isTransparent && (!isEndpoints || isEndpoint);
|
|
4505
4716
|
let pointColor = color;
|
|
4506
4717
|
if (isSequentialColor) {
|
|
4507
|
-
const
|
|
4508
|
-
pointColor =
|
|
4718
|
+
const value = finiteNumber(p.row[sequentialColorField]);
|
|
4719
|
+
pointColor = value !== void 0 ? getSequentialColor(scales, value) : color;
|
|
4509
4720
|
}
|
|
4510
4721
|
const hollow = isEndpoints && visible;
|
|
4511
4722
|
const pointColorStr = getRepresentativeColor5(pointColor);
|
|
@@ -4561,7 +4772,10 @@ function computeLineMarks(spec, scales, _chartArea, _strategy, theme) {
|
|
|
4561
4772
|
return allMarks;
|
|
4562
4773
|
}
|
|
4563
4774
|
function _getMidValue(rows, field) {
|
|
4564
|
-
const values = rows.
|
|
4775
|
+
const values = rows.flatMap((row) => {
|
|
4776
|
+
const value = finiteNumber(row[field]);
|
|
4777
|
+
return value === void 0 ? [] : [value];
|
|
4778
|
+
});
|
|
4565
4779
|
if (values.length === 0) return 0;
|
|
4566
4780
|
const sorted = values.sort((a, b) => a - b);
|
|
4567
4781
|
return sorted[Math.floor(sorted.length / 2)];
|
|
@@ -4621,25 +4835,26 @@ function computeSingleArea(spec, scales, _chartArea, theme) {
|
|
|
4621
4835
|
const color = getColor(scales, seriesKey);
|
|
4622
4836
|
const sortedRows = xChannel.type === "nominal" || xChannel.type === "ordinal" ? rows : sortByField(rows, xChannel.field);
|
|
4623
4837
|
const validPoints = [];
|
|
4838
|
+
const segments = [[]];
|
|
4624
4839
|
const y2Channel = encoding.y2;
|
|
4625
4840
|
for (const row of sortedRows) {
|
|
4626
4841
|
const xVal = scaleValue(scales.x.scale, scales.x.type, row[xChannel.field]);
|
|
4627
4842
|
const yVal = scaleValue(scales.y.scale, scales.y.type, row[yChannel.field]);
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4843
|
+
const yBottomVal = y2Channel ? scaleValue(scales.y.scale, scales.y.type, row[y2Channel.field]) : baselineY;
|
|
4844
|
+
if (xVal === null || yVal === null || yBottomVal === null) {
|
|
4845
|
+
if (segments[segments.length - 1].length > 0) segments.push([]);
|
|
4846
|
+
continue;
|
|
4847
|
+
}
|
|
4848
|
+
const point = { x: xVal, yTop: yVal, yBottom: yBottomVal, row };
|
|
4849
|
+
validPoints.push(point);
|
|
4850
|
+
segments[segments.length - 1].push(point);
|
|
4636
4851
|
}
|
|
4637
4852
|
if (validPoints.length === 0) continue;
|
|
4638
4853
|
const curve = resolveCurve(spec.markDef.interpolate);
|
|
4639
4854
|
const areaGenerator = area2().x((d) => d.x).y0((d) => d.yBottom).y1((d) => d.yTop).curve(curve);
|
|
4640
|
-
const pathStr = areaGenerator(
|
|
4855
|
+
const pathStr = segments.map((segment) => areaGenerator(segment) ?? "").join(" ");
|
|
4641
4856
|
const topLineGenerator = line3().x((d) => d.x).y((d) => d.yTop).curve(curve);
|
|
4642
|
-
const topPathStr = topLineGenerator(
|
|
4857
|
+
const topPathStr = segments.map((segment) => topLineGenerator(segment) ?? "").join(" ");
|
|
4643
4858
|
const topPoints = validPoints.map((p) => ({ x: p.x, y: p.yTop }));
|
|
4644
4859
|
const bottomPoints = validPoints.map((p) => ({ x: p.x, y: p.yBottom }));
|
|
4645
4860
|
const ariaLabel = seriesKey === "__default__" ? `Area with ${validPoints.length} data points` : `${seriesKey}: area with ${validPoints.length} data points`;
|
|
@@ -4715,6 +4930,7 @@ function computeStackedArea(spec, scales, chartArea, theme) {
|
|
|
4715
4930
|
}
|
|
4716
4931
|
const keys = Array.from(seriesKeys);
|
|
4717
4932
|
const xValues = Array.from(xValueSet);
|
|
4933
|
+
const missingBoundaries = /* @__PURE__ */ new Set();
|
|
4718
4934
|
const pivotData = xValues.map((xVal) => {
|
|
4719
4935
|
const pivot = { __x__: xVal };
|
|
4720
4936
|
for (const key of keys) {
|
|
@@ -4724,7 +4940,9 @@ function computeStackedArea(spec, scales, chartArea, theme) {
|
|
|
4724
4940
|
if (xRows) {
|
|
4725
4941
|
for (const row of xRows) {
|
|
4726
4942
|
const series = String(row[colorField]);
|
|
4727
|
-
|
|
4943
|
+
const value = finiteNumber(row[yChannel.field]);
|
|
4944
|
+
if (value === void 0) missingBoundaries.add(xVal);
|
|
4945
|
+
pivot[series] = value ?? 0;
|
|
4728
4946
|
}
|
|
4729
4947
|
}
|
|
4730
4948
|
return pivot;
|
|
@@ -4739,19 +4957,27 @@ function computeStackedArea(spec, scales, chartArea, theme) {
|
|
|
4739
4957
|
const seriesKey = layer.key;
|
|
4740
4958
|
const color = getColor(scales, seriesKey);
|
|
4741
4959
|
const validPoints = [];
|
|
4960
|
+
const segments = [[]];
|
|
4742
4961
|
for (const d of layer) {
|
|
4743
4962
|
const xVal = scaleValue(scales.x.scale, scales.x.type, d.data.__x__);
|
|
4744
|
-
|
|
4963
|
+
const xKey = String(d.data.__x__);
|
|
4745
4964
|
const yTop = yScale(d[1]);
|
|
4746
4965
|
const yBottom = yScale(d[0]);
|
|
4747
|
-
|
|
4966
|
+
if (xVal === null || missingBoundaries.has(xKey) || !Number.isFinite(yTop) || !Number.isFinite(yBottom)) {
|
|
4967
|
+
if (segments[segments.length - 1].length > 0) segments.push([]);
|
|
4968
|
+
continue;
|
|
4969
|
+
}
|
|
4970
|
+
const datum = rowsByXSeries.get(`${xKey}::${seriesKey}`) ?? d.data;
|
|
4971
|
+
const point = { x: xVal, yTop, yBottom, xKey, datum };
|
|
4972
|
+
validPoints.push(point);
|
|
4973
|
+
segments[segments.length - 1].push(point);
|
|
4748
4974
|
}
|
|
4749
4975
|
if (validPoints.length === 0) continue;
|
|
4750
4976
|
const stackCurve = resolveCurve(spec.markDef.interpolate);
|
|
4751
4977
|
const areaGenerator = area2().x((p) => p.x).y0((p) => p.yBottom).y1((p) => p.yTop).curve(stackCurve);
|
|
4752
|
-
const pathStr = areaGenerator(
|
|
4978
|
+
const pathStr = segments.map((segment) => areaGenerator(segment) ?? "").join(" ");
|
|
4753
4979
|
const topLineGenerator = line3().x((p) => p.x).y((p) => p.yTop).curve(stackCurve);
|
|
4754
|
-
const topPathStr = topLineGenerator(
|
|
4980
|
+
const topPathStr = segments.map((segment) => topLineGenerator(segment) ?? "").join(" ");
|
|
4755
4981
|
const topPoints = validPoints.map((p) => ({ x: p.x, y: p.yTop }));
|
|
4756
4982
|
const bottomPoints = validPoints.map((p) => ({ x: p.x, y: p.yBottom }));
|
|
4757
4983
|
const aria = {
|
|
@@ -4767,7 +4993,7 @@ function computeStackedArea(spec, scales, chartArea, theme) {
|
|
|
4767
4993
|
fillValue = getRepresentativeColor6(color);
|
|
4768
4994
|
fillOpacity = spec.markDef.fillOpacity ?? STACKED_FILL_OPACITY;
|
|
4769
4995
|
}
|
|
4770
|
-
const rawPointKeys = validPoints.map((
|
|
4996
|
+
const rawPointKeys = validPoints.map((point) => serializeKeyValue(point.xKey));
|
|
4771
4997
|
marks.push({
|
|
4772
4998
|
type: "area",
|
|
4773
4999
|
key: seriesKey,
|
|
@@ -4784,15 +5010,8 @@ function computeStackedArea(spec, scales, chartArea, theme) {
|
|
|
4784
5010
|
seriesKey,
|
|
4785
5011
|
stacked: true,
|
|
4786
5012
|
stackNormalized: stackProp === "normalize",
|
|
4787
|
-
data:
|
|
4788
|
-
|
|
4789
|
-
return rowsByXSeries.get(`${xStr}::${seriesKey}`) ?? d.data;
|
|
4790
|
-
}),
|
|
4791
|
-
dataPoints: validPoints.map((p, idx) => {
|
|
4792
|
-
const xStr = String(layer[idx]?.data.__x__);
|
|
4793
|
-
const datum = rowsByXSeries.get(`${xStr}::${seriesKey}`) ?? layer[idx]?.data ?? {};
|
|
4794
|
-
return { x: p.x, y: p.yTop, datum };
|
|
4795
|
-
}),
|
|
5013
|
+
data: validPoints.map((point) => point.datum),
|
|
5014
|
+
dataPoints: validPoints.map((point) => ({ x: point.x, y: point.yTop, datum: point.datum })),
|
|
4796
5015
|
aria
|
|
4797
5016
|
});
|
|
4798
5017
|
}
|
|
@@ -5044,8 +5263,8 @@ function computeParliamentMarks(spec, scales, chartArea, _strategy, theme) {
|
|
|
5044
5263
|
const partyRows = /* @__PURE__ */ new Map();
|
|
5045
5264
|
for (const row of spec.data) {
|
|
5046
5265
|
const party = String(row[colorField] ?? "");
|
|
5047
|
-
const val =
|
|
5048
|
-
if (
|
|
5266
|
+
const val = finiteNumber(row[valueChannel.field]);
|
|
5267
|
+
if (val === void 0 || val < 0) continue;
|
|
5049
5268
|
partyTotals.set(party, (partyTotals.get(party) ?? 0) + Math.round(val));
|
|
5050
5269
|
if (!partyRows.has(party)) partyRows.set(party, row);
|
|
5051
5270
|
}
|
|
@@ -5221,8 +5440,8 @@ function computePieMarks(spec, scales, chartArea, _strategy, isDonut = false, th
|
|
|
5221
5440
|
const categoryRows = /* @__PURE__ */ new Map();
|
|
5222
5441
|
for (const row of spec.data) {
|
|
5223
5442
|
const cat = String(row[categoryField] ?? "");
|
|
5224
|
-
const val =
|
|
5225
|
-
if (
|
|
5443
|
+
const val = finiteNumber(row[valueChannel.field]);
|
|
5444
|
+
if (val === void 0 || val < 0) continue;
|
|
5226
5445
|
categoryTotals.set(cat, (categoryTotals.get(cat) ?? 0) + val);
|
|
5227
5446
|
if (!categoryRows.has(cat)) {
|
|
5228
5447
|
categoryRows.set(cat, row);
|
|
@@ -5241,8 +5460,8 @@ function computePieMarks(spec, scales, chartArea, _strategy, isDonut = false, th
|
|
|
5241
5460
|
} else {
|
|
5242
5461
|
for (let i = 0; i < spec.data.length; i++) {
|
|
5243
5462
|
const row = spec.data[i];
|
|
5244
|
-
const val =
|
|
5245
|
-
if (
|
|
5463
|
+
const val = finiteNumber(row[valueChannel.field]);
|
|
5464
|
+
if (val === void 0 || val < 0) continue;
|
|
5246
5465
|
const label = String(row.label ?? row.name ?? row.category ?? `Slice ${i + 1}`);
|
|
5247
5466
|
slices.push({ label, value: val, originalRow: row });
|
|
5248
5467
|
}
|
|
@@ -5544,9 +5763,9 @@ function computeRangeMarks(spec, scales, _chartArea, _strategy, theme) {
|
|
|
5544
5763
|
const startRaw = row[startChannel.field];
|
|
5545
5764
|
const endRaw = row[endChannel.field];
|
|
5546
5765
|
if (startRaw == null || endRaw == null) continue;
|
|
5547
|
-
const startVal =
|
|
5548
|
-
const endVal =
|
|
5549
|
-
if (
|
|
5766
|
+
const startVal = finiteNumber(startRaw);
|
|
5767
|
+
const endVal = finiteNumber(endRaw);
|
|
5768
|
+
if (startVal === void 0 || endVal === void 0) continue;
|
|
5550
5769
|
const center = bandPos + bandwidth / 2;
|
|
5551
5770
|
const pStart = valueScale(startVal);
|
|
5552
5771
|
const pEnd = valueScale(endVal);
|
|
@@ -5818,8 +6037,8 @@ function computeRectMarks(spec, scales, _chartArea) {
|
|
|
5818
6037
|
if (xVal == null || yVal == null) continue;
|
|
5819
6038
|
let fill;
|
|
5820
6039
|
if (colorEnc && isSequential) {
|
|
5821
|
-
const value =
|
|
5822
|
-
if (
|
|
6040
|
+
const value = finiteNumber(row[colorEnc.field]);
|
|
6041
|
+
if (value === void 0) continue;
|
|
5823
6042
|
fill = getSequentialColor(scales, value);
|
|
5824
6043
|
} else if (colorEnc) {
|
|
5825
6044
|
fill = getColor(scales, String(row[colorEnc.field] ?? "__default__"));
|
|
@@ -5950,6 +6169,7 @@ var DEFAULT_POINT_RADIUS2 = 5;
|
|
|
5950
6169
|
var POINT_STROKE_WIDTH = 1.5;
|
|
5951
6170
|
var DENSE_LAYER_POINTS = 200;
|
|
5952
6171
|
function resolvePosition2(value, channelType, scale) {
|
|
6172
|
+
if (value == null) return void 0;
|
|
5953
6173
|
switch (channelType) {
|
|
5954
6174
|
case "nominal":
|
|
5955
6175
|
case "ordinal": {
|
|
@@ -5967,9 +6187,10 @@ function resolvePosition2(value, channelType, scale) {
|
|
|
5967
6187
|
return Number.isNaN(px) ? void 0 : px;
|
|
5968
6188
|
}
|
|
5969
6189
|
default: {
|
|
5970
|
-
const
|
|
5971
|
-
if (
|
|
5972
|
-
|
|
6190
|
+
const number = finiteNumber(value);
|
|
6191
|
+
if (number === void 0) return void 0;
|
|
6192
|
+
const position = scale(number);
|
|
6193
|
+
return Number.isFinite(position) ? position : void 0;
|
|
5973
6194
|
}
|
|
5974
6195
|
}
|
|
5975
6196
|
}
|
|
@@ -6007,20 +6228,20 @@ function computeScatterMarks(spec, scales, _chartArea, _strategy, theme) {
|
|
|
6007
6228
|
const category = colorField && !isSequentialColor ? String(row[colorField] ?? "") : void 0;
|
|
6008
6229
|
let color;
|
|
6009
6230
|
if (isSequentialColor && colorField) {
|
|
6010
|
-
const
|
|
6011
|
-
color =
|
|
6231
|
+
const value = finiteNumber(row[colorField]);
|
|
6232
|
+
color = value !== void 0 ? getSequentialColor(scales, value) : getColor(scales, "__default__");
|
|
6012
6233
|
} else if (colorField) {
|
|
6013
6234
|
color = getColor(scales, category ?? "__default__");
|
|
6014
6235
|
} else {
|
|
6015
6236
|
color = constFill ?? getColor(scales, "__default__");
|
|
6016
6237
|
}
|
|
6017
6238
|
let radius = constSize != null ? constSize : DEFAULT_POINT_RADIUS2;
|
|
6018
|
-
if (
|
|
6019
|
-
const
|
|
6020
|
-
if (
|
|
6021
|
-
|
|
6022
|
-
}
|
|
6239
|
+
if (sizeField) {
|
|
6240
|
+
const sizeValue = finiteNumber(row[sizeField]);
|
|
6241
|
+
if (sizeValue === void 0) continue;
|
|
6242
|
+
if (sizeScale) radius = sizeScale(sizeValue);
|
|
6023
6243
|
}
|
|
6244
|
+
if (!Number.isFinite(radius) || radius < 0) continue;
|
|
6024
6245
|
const labelParts = [`${xChannel.field}=${rawX}`, `${yChannel.field}=${rawY}`];
|
|
6025
6246
|
if (category) labelParts.push(`${colorField}=${category}`);
|
|
6026
6247
|
if (sizeField && row[sizeField] != null) {
|
|
@@ -6291,8 +6512,8 @@ function computeWaffleMarks(spec, scales, chartArea, _strategy) {
|
|
|
6291
6512
|
const categoryRows = /* @__PURE__ */ new Map();
|
|
6292
6513
|
for (const row of spec.data) {
|
|
6293
6514
|
const cat = String(row[colorField] ?? "");
|
|
6294
|
-
const val =
|
|
6295
|
-
if (
|
|
6515
|
+
const val = finiteNumber(row[valueChannel.field]);
|
|
6516
|
+
if (val === void 0 || val < 0) continue;
|
|
6296
6517
|
categoryTotals.set(cat, (categoryTotals.get(cat) ?? 0) + val);
|
|
6297
6518
|
if (!categoryRows.has(cat)) {
|
|
6298
6519
|
categoryRows.set(cat, row);
|
|
@@ -6467,7 +6688,16 @@ function computeAggregate(op, values) {
|
|
|
6467
6688
|
}
|
|
6468
6689
|
}
|
|
6469
6690
|
function groupKey(row, groupby) {
|
|
6470
|
-
return groupby.map((
|
|
6691
|
+
return JSON.stringify(groupby.map((field) => [typeof row[field], row[field] ?? null]));
|
|
6692
|
+
}
|
|
6693
|
+
function aggregateRows(op, field, rows) {
|
|
6694
|
+
if (op === "count") return rows.length;
|
|
6695
|
+
if (op === "distinct") return new Set(rows.map((row) => row[field])).size;
|
|
6696
|
+
const values = rows.flatMap((row) => {
|
|
6697
|
+
const value = finiteNumber(row[field]);
|
|
6698
|
+
return value === void 0 ? [] : [value];
|
|
6699
|
+
});
|
|
6700
|
+
return values.length ? computeAggregate(op, values) : null;
|
|
6471
6701
|
}
|
|
6472
6702
|
function runAggregate(data, transform) {
|
|
6473
6703
|
const { aggregate, groupby } = transform;
|
|
@@ -6488,16 +6718,7 @@ function runAggregate(data, transform) {
|
|
|
6488
6718
|
outRow[field] = rows[0][field];
|
|
6489
6719
|
}
|
|
6490
6720
|
for (const agg of aggregate) {
|
|
6491
|
-
|
|
6492
|
-
outRow[agg.as] = new Set(rows.map((r) => r[agg.field])).size;
|
|
6493
|
-
continue;
|
|
6494
|
-
}
|
|
6495
|
-
const values = rows.map((r) => {
|
|
6496
|
-
if (agg.op === "count") return 1;
|
|
6497
|
-
const v = Number(r[agg.field]);
|
|
6498
|
-
return Number.isFinite(v) ? v : NaN;
|
|
6499
|
-
}).filter((v) => !Number.isNaN(v));
|
|
6500
|
-
outRow[agg.as] = computeAggregate(agg.op, values);
|
|
6721
|
+
outRow[agg.as] = aggregateRows(agg.op, agg.field, rows);
|
|
6501
6722
|
}
|
|
6502
6723
|
result.push(outRow);
|
|
6503
6724
|
}
|
|
@@ -7126,6 +7347,7 @@ function emitSpecWarnings(warnings, sink = defaultWarnSink) {
|
|
|
7126
7347
|
}
|
|
7127
7348
|
|
|
7128
7349
|
// src/compiler/animation.ts
|
|
7350
|
+
import { FLUID_SPRING, springDuration } from "@opendata-ai/openchart-core";
|
|
7129
7351
|
var ENTER_DEFAULTS = {
|
|
7130
7352
|
duration: 450,
|
|
7131
7353
|
ease: "smooth",
|
|
@@ -7156,10 +7378,16 @@ function resolveAnimation(spec) {
|
|
|
7156
7378
|
annotationDelay: DEFAULT_ANNOTATION_DELAY
|
|
7157
7379
|
};
|
|
7158
7380
|
}
|
|
7159
|
-
const config = spec;
|
|
7160
|
-
const
|
|
7161
|
-
const
|
|
7162
|
-
const
|
|
7381
|
+
const config = typeof spec === "string" ? { preset: spec } : spec;
|
|
7382
|
+
const preset = config.preset ?? "legacy";
|
|
7383
|
+
const spring = preset === "fluid" ? { ...FLUID_SPRING } : void 0;
|
|
7384
|
+
const base = (defaults) => preset === "legacy" ? defaults : { ...defaults, duration: spring ? springDuration(spring) : 250, spring, staggerDelay: 0 };
|
|
7385
|
+
const enter = resolvePhase(config.enter, base(ENTER_DEFAULTS));
|
|
7386
|
+
const update = resolvePhase(config.update, base(UPDATE_DEFAULTS));
|
|
7387
|
+
const exit = resolvePhase(
|
|
7388
|
+
config.exit,
|
|
7389
|
+
preset === "legacy" ? EXIT_DEFAULTS : { ...EXIT_DEFAULTS, duration: 160 }
|
|
7390
|
+
);
|
|
7163
7391
|
if (update && config.update && typeof config.update === "object" && !Array.isArray(config.update)) {
|
|
7164
7392
|
const cap = config.update.maxMarks;
|
|
7165
7393
|
if (typeof cap === "number" && Number.isFinite(cap) && cap >= 1) {
|
|
@@ -7171,7 +7399,7 @@ function resolveAnimation(spec) {
|
|
|
7171
7399
|
enter: enter ?? void 0,
|
|
7172
7400
|
update: update ?? void 0,
|
|
7173
7401
|
exit: exit ?? void 0,
|
|
7174
|
-
annotationDelay: config.annotationDelay ?? DEFAULT_ANNOTATION_DELAY
|
|
7402
|
+
annotationDelay: config.annotationDelay ?? (preset === "legacy" ? DEFAULT_ANNOTATION_DELAY : 0)
|
|
7175
7403
|
};
|
|
7176
7404
|
}
|
|
7177
7405
|
function clampStaggerDelay(delay, elementCount) {
|
|
@@ -7184,9 +7412,14 @@ function resolvePhase(phase, defaults) {
|
|
|
7184
7412
|
return { ...defaults };
|
|
7185
7413
|
}
|
|
7186
7414
|
const cfg = phase;
|
|
7415
|
+
if (cfg.spring && (cfg.duration !== void 0 || cfg.ease !== void 0)) {
|
|
7416
|
+
throw new Error("An animation phase cannot combine spring with duration or ease.");
|
|
7417
|
+
}
|
|
7418
|
+
const spring = cfg.spring ?? (cfg.duration !== void 0 || cfg.ease !== void 0 ? void 0 : defaults.spring);
|
|
7187
7419
|
const stagger = resolveStagger(cfg.stagger, defaults);
|
|
7188
7420
|
return {
|
|
7189
|
-
duration: cfg.duration ?? defaults.duration,
|
|
7421
|
+
duration: spring ? springDuration(spring) : cfg.duration ?? defaults.duration,
|
|
7422
|
+
spring,
|
|
7190
7423
|
ease: cfg.ease ?? defaults.ease,
|
|
7191
7424
|
staggerDelay: stagger.delay,
|
|
7192
7425
|
staggerOrder: stagger.order
|
|
@@ -9520,7 +9753,68 @@ function validateLayerSpec(spec, errors) {
|
|
|
9520
9753
|
}
|
|
9521
9754
|
function validateAnimation(spec, errors) {
|
|
9522
9755
|
const animation = spec.animation;
|
|
9756
|
+
if (typeof animation === "string" && !["fluid", "subtle", "legacy"].includes(animation)) {
|
|
9757
|
+
errors.push({
|
|
9758
|
+
code: "INVALID_VALUE",
|
|
9759
|
+
suggestion: "Use a named preset or separate valid spring and tween phase settings.",
|
|
9760
|
+
path: "animation",
|
|
9761
|
+
message: "Unknown animation preset."
|
|
9762
|
+
});
|
|
9763
|
+
}
|
|
9523
9764
|
if (!animation || typeof animation !== "object" || Array.isArray(animation)) return;
|
|
9765
|
+
const config = animation;
|
|
9766
|
+
if (config.preset !== void 0 && !["fluid", "subtle", "legacy"].includes(String(config.preset))) {
|
|
9767
|
+
errors.push({
|
|
9768
|
+
code: "INVALID_VALUE",
|
|
9769
|
+
suggestion: "Use a named preset or separate valid spring and tween phase settings.",
|
|
9770
|
+
path: "animation.preset",
|
|
9771
|
+
message: "Animation preset must be fluid, subtle, or legacy."
|
|
9772
|
+
});
|
|
9773
|
+
}
|
|
9774
|
+
for (const phase of ["enter", "update", "exit"]) {
|
|
9775
|
+
const value = config[phase];
|
|
9776
|
+
if (!value || typeof value !== "object") continue;
|
|
9777
|
+
const settings = value;
|
|
9778
|
+
if (settings.spring !== void 0) {
|
|
9779
|
+
if (settings.duration !== void 0 || settings.ease !== void 0) {
|
|
9780
|
+
errors.push({
|
|
9781
|
+
code: "INVALID_VALUE",
|
|
9782
|
+
suggestion: "Use a named preset or separate valid spring and tween phase settings.",
|
|
9783
|
+
path: `animation.${phase}`,
|
|
9784
|
+
message: "A phase cannot combine spring with duration or ease."
|
|
9785
|
+
});
|
|
9786
|
+
}
|
|
9787
|
+
if (!settings.spring || typeof settings.spring !== "object" || Array.isArray(settings.spring)) {
|
|
9788
|
+
errors.push({
|
|
9789
|
+
code: "INVALID_VALUE",
|
|
9790
|
+
suggestion: "Use a named preset or separate valid spring and tween phase settings.",
|
|
9791
|
+
path: `animation.${phase}.spring`,
|
|
9792
|
+
message: "Spring must be a configuration object."
|
|
9793
|
+
});
|
|
9794
|
+
} else {
|
|
9795
|
+
for (const [key, number] of Object.entries(settings.spring)) {
|
|
9796
|
+
const known = ["stiffness", "damping", "mass"].includes(key);
|
|
9797
|
+
if (known && number === void 0) continue;
|
|
9798
|
+
if (!known || typeof number !== "number" || !Number.isFinite(number) || number <= 0) {
|
|
9799
|
+
errors.push({
|
|
9800
|
+
code: "INVALID_VALUE",
|
|
9801
|
+
suggestion: "Use a named preset or separate valid spring and tween phase settings.",
|
|
9802
|
+
path: `animation.${phase}.spring.${key}`,
|
|
9803
|
+
message: "Spring parameters must be finite positive stiffness, damping, or mass."
|
|
9804
|
+
});
|
|
9805
|
+
}
|
|
9806
|
+
}
|
|
9807
|
+
}
|
|
9808
|
+
}
|
|
9809
|
+
if (settings.duration !== void 0 && (typeof settings.duration !== "number" || !Number.isFinite(settings.duration) || settings.duration < 0)) {
|
|
9810
|
+
errors.push({
|
|
9811
|
+
code: "INVALID_VALUE",
|
|
9812
|
+
suggestion: "Use a named preset or separate valid spring and tween phase settings.",
|
|
9813
|
+
path: `animation.${phase}.duration`,
|
|
9814
|
+
message: "Animation duration must be finite and nonnegative."
|
|
9815
|
+
});
|
|
9816
|
+
}
|
|
9817
|
+
}
|
|
9524
9818
|
const update = animation.update;
|
|
9525
9819
|
if (!update || typeof update !== "object" || Array.isArray(update)) return;
|
|
9526
9820
|
const maxMarks = update.maxMarks;
|
|
@@ -9534,6 +9828,60 @@ function validateAnimation(spec, errors) {
|
|
|
9534
9828
|
});
|
|
9535
9829
|
}
|
|
9536
9830
|
}
|
|
9831
|
+
function validateFieldMetadata(obj, errors) {
|
|
9832
|
+
if (obj.fields === void 0) return;
|
|
9833
|
+
const fail = (path, message) => errors.push({
|
|
9834
|
+
path,
|
|
9835
|
+
message,
|
|
9836
|
+
code: "INVALID_VALUE",
|
|
9837
|
+
suggestion: "Use string labels, units and field references, and documented metadata enum values."
|
|
9838
|
+
});
|
|
9839
|
+
if (!obj.fields || typeof obj.fields !== "object" || Array.isArray(obj.fields)) {
|
|
9840
|
+
fail("fields", "Field metadata must be an object keyed by field name.");
|
|
9841
|
+
return;
|
|
9842
|
+
}
|
|
9843
|
+
for (const [field, value] of Object.entries(obj.fields)) {
|
|
9844
|
+
const path = `fields.${field}`;
|
|
9845
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
9846
|
+
fail(path, "Field metadata must be an object.");
|
|
9847
|
+
continue;
|
|
9848
|
+
}
|
|
9849
|
+
const metadata = value;
|
|
9850
|
+
for (const key of ["label", "unit", "measure", "provenance", "statusField"]) {
|
|
9851
|
+
if (metadata[key] !== void 0 && typeof metadata[key] !== "string")
|
|
9852
|
+
fail(`${path}.${key}`, `${key} must be a string.`);
|
|
9853
|
+
}
|
|
9854
|
+
for (const [key, values] of Object.entries({
|
|
9855
|
+
type: ["text", "number", "date"],
|
|
9856
|
+
role: ["identity"],
|
|
9857
|
+
representation: ["number", "percent", "proportion"]
|
|
9858
|
+
})) {
|
|
9859
|
+
if (metadata[key] !== void 0 && !values.includes(metadata[key]))
|
|
9860
|
+
fail(`${path}.${key}`, `${key} must be one of ${values.join(", ")}.`);
|
|
9861
|
+
}
|
|
9862
|
+
if (metadata.interval !== void 0) {
|
|
9863
|
+
const interval = metadata.interval;
|
|
9864
|
+
if (!interval || typeof interval !== "object" || Array.isArray(interval) || typeof interval.lower !== "string" || !interval.lower || typeof interval.upper !== "string" || !interval.upper) {
|
|
9865
|
+
fail(`${path}.interval`, "Interval lower and upper must name supplied numeric fields.");
|
|
9866
|
+
} else if (Array.isArray(obj.data)) {
|
|
9867
|
+
for (const [index, row] of obj.data.entries()) {
|
|
9868
|
+
if (!row || typeof row !== "object") continue;
|
|
9869
|
+
const lower = row[interval.lower];
|
|
9870
|
+
const upper = row[interval.upper];
|
|
9871
|
+
if ([lower, upper].some(
|
|
9872
|
+
(bound) => bound != null && (typeof bound !== "number" || !Number.isFinite(bound))
|
|
9873
|
+
) || typeof lower === "number" && typeof upper === "number" && lower > upper) {
|
|
9874
|
+
fail(
|
|
9875
|
+
`${path}.interval`,
|
|
9876
|
+
`Row ${index + 1} must supply finite interval bounds with lower <= upper, or missing values.`
|
|
9877
|
+
);
|
|
9878
|
+
break;
|
|
9879
|
+
}
|
|
9880
|
+
}
|
|
9881
|
+
}
|
|
9882
|
+
}
|
|
9883
|
+
}
|
|
9884
|
+
}
|
|
9537
9885
|
function validateSpec(spec) {
|
|
9538
9886
|
const errors = [];
|
|
9539
9887
|
if (!spec || typeof spec !== "object" || Array.isArray(spec)) {
|
|
@@ -9550,6 +9898,31 @@ function validateSpec(spec) {
|
|
|
9550
9898
|
};
|
|
9551
9899
|
}
|
|
9552
9900
|
const obj = spec;
|
|
9901
|
+
validateFieldMetadata(obj, errors);
|
|
9902
|
+
if (obj.chrome !== void 0) {
|
|
9903
|
+
const chrome = obj.chrome;
|
|
9904
|
+
if (!chrome || typeof chrome !== "object" || Array.isArray(chrome)) {
|
|
9905
|
+
errors.push({
|
|
9906
|
+
path: "chrome",
|
|
9907
|
+
code: "INVALID_TYPE",
|
|
9908
|
+
message: "Chrome must be an object of text elements.",
|
|
9909
|
+
suggestion: "Use string text or a text object with a string text property."
|
|
9910
|
+
});
|
|
9911
|
+
} else {
|
|
9912
|
+
for (const key of ["title", "subtitle", "source", "footer", "byline", "eyebrow"]) {
|
|
9913
|
+
const value = chrome[key];
|
|
9914
|
+
if (value === void 0) continue;
|
|
9915
|
+
if (typeof value !== "string" && (!value || typeof value !== "object" || Array.isArray(value) || typeof value.text !== "string")) {
|
|
9916
|
+
errors.push({
|
|
9917
|
+
path: `chrome.${key}`,
|
|
9918
|
+
code: "INVALID_TYPE",
|
|
9919
|
+
message: `${key} must be text or an object with a string text property.`,
|
|
9920
|
+
suggestion: "Preserve authored styling in the object and provide text as a string."
|
|
9921
|
+
});
|
|
9922
|
+
}
|
|
9923
|
+
}
|
|
9924
|
+
}
|
|
9925
|
+
}
|
|
9553
9926
|
const hasLayer = "layer" in obj && Array.isArray(obj.layer);
|
|
9554
9927
|
const hasMark = "mark" in obj;
|
|
9555
9928
|
const isTable = obj.type === "table";
|
|
@@ -9771,10 +10144,7 @@ function compileBarList(spec, options) {
|
|
|
9771
10144
|
const rowContentHeight = Math.max(barHeight, LABEL_FONT_SIZE7 * 1.4);
|
|
9772
10145
|
const rowHeight = rowContentHeight + DEFAULT_ROW_GAP;
|
|
9773
10146
|
const maxFittingRows = Math.max(1, Math.floor(fullArea.height / rowHeight));
|
|
9774
|
-
const validRows = barlistSpec.data.filter((row) =>
|
|
9775
|
-
const val = row[valueField];
|
|
9776
|
-
return val !== null && val !== void 0 && !Number.isNaN(Number(val));
|
|
9777
|
-
}).sort((a, b) => Number(b[valueField]) - Number(a[valueField])).slice(0, Math.min(barlistSpec.maxItems, maxFittingRows));
|
|
10147
|
+
const validRows = barlistSpec.data.filter((row) => finiteNumber(row[valueField]) !== void 0).sort((a, b) => Number(b[valueField]) - Number(a[valueField])).slice(0, Math.min(barlistSpec.maxItems, maxFittingRows));
|
|
9778
10148
|
if (validRows.length === 0) {
|
|
9779
10149
|
return emptyLayout2(chrome, theme, options, watermark);
|
|
9780
10150
|
}
|
|
@@ -10170,7 +10540,9 @@ import {
|
|
|
10170
10540
|
import {
|
|
10171
10541
|
computeFieldFormatContext as computeFieldFormatContext2,
|
|
10172
10542
|
defaultNumberFormatter as defaultNumberFormatter5,
|
|
10173
|
-
|
|
10543
|
+
estimateTextWidth as estimateTextWidth10,
|
|
10544
|
+
resolveNumberFormatter as resolveNumberFormatter2,
|
|
10545
|
+
resolveSchemeName as resolveSchemeName3
|
|
10174
10546
|
} from "@opendata-ai/openchart-core";
|
|
10175
10547
|
import { tickStep } from "d3-array";
|
|
10176
10548
|
import { scaleQuantile } from "d3-scale";
|
|
@@ -10209,10 +10581,8 @@ function sampleRampColors(colors, k) {
|
|
|
10209
10581
|
function numericFieldValues(spec, field) {
|
|
10210
10582
|
const out = [];
|
|
10211
10583
|
for (const row of spec.data) {
|
|
10212
|
-
const
|
|
10213
|
-
if (
|
|
10214
|
-
const num = typeof v === "number" ? v : Number(v);
|
|
10215
|
-
if (Number.isFinite(num)) out.push(num);
|
|
10584
|
+
const value = finiteNumber(row[field]);
|
|
10585
|
+
if (value !== void 0) out.push(value);
|
|
10216
10586
|
}
|
|
10217
10587
|
return out;
|
|
10218
10588
|
}
|
|
@@ -10235,6 +10605,10 @@ function isDivergingRamp(colors, theme) {
|
|
|
10235
10605
|
function resolveRampColors(channel, theme) {
|
|
10236
10606
|
const explicitRange = channel.scale?.range;
|
|
10237
10607
|
if (explicitRange?.length) return explicitRange;
|
|
10608
|
+
if (channel.scale?.scheme) {
|
|
10609
|
+
const scheme = resolveSchemeName3(channel.scale.scheme);
|
|
10610
|
+
if (scheme) return channel.scale.reverse ? [...scheme].reverse() : scheme;
|
|
10611
|
+
}
|
|
10238
10612
|
return Object.values(theme.colors.sequential)[0] ?? theme.colors.categorical;
|
|
10239
10613
|
}
|
|
10240
10614
|
function resolveBarWidth(availableWidth) {
|
|
@@ -10356,6 +10730,22 @@ function computeContinuousLegendContentForChannel(values, channel, theme, availa
|
|
|
10356
10730
|
x: (i + 1) * binWidth,
|
|
10357
10731
|
anchor: "middle"
|
|
10358
10732
|
}));
|
|
10733
|
+
const rowEnds = [];
|
|
10734
|
+
const rowHeight = Math.ceil(theme.fonts.sizes.small * 1.3);
|
|
10735
|
+
for (const tick of ticks2) {
|
|
10736
|
+
const width = estimateTextWidth10(
|
|
10737
|
+
tick.label,
|
|
10738
|
+
theme.fonts.sizes.small,
|
|
10739
|
+
theme.fonts.weights.normal
|
|
10740
|
+
);
|
|
10741
|
+
if (tick.x - width / 2 < 0) tick.anchor = "start";
|
|
10742
|
+
else if (tick.x + width / 2 > barWidth) tick.anchor = "end";
|
|
10743
|
+
const left = tick.x - (tick.anchor === "middle" ? width / 2 : tick.anchor === "end" ? width : 0);
|
|
10744
|
+
let row = rowEnds.findIndex((end) => end + 6 <= left);
|
|
10745
|
+
if (row < 0) row = rowEnds.length;
|
|
10746
|
+
rowEnds[row] = left + width;
|
|
10747
|
+
if (row > 0) tick.offsetY = row * rowHeight;
|
|
10748
|
+
}
|
|
10359
10749
|
return {
|
|
10360
10750
|
mode: "binned",
|
|
10361
10751
|
barWidth,
|
|
@@ -10363,6 +10753,7 @@ function computeContinuousLegendContentForChannel(values, channel, theme, availa
|
|
|
10363
10753
|
colorStops: [],
|
|
10364
10754
|
bins,
|
|
10365
10755
|
ticks: ticks2,
|
|
10756
|
+
labelRows: Math.max(1, rowEnds.length),
|
|
10366
10757
|
classScale: scale
|
|
10367
10758
|
};
|
|
10368
10759
|
}
|
|
@@ -10598,7 +10989,7 @@ import {
|
|
|
10598
10989
|
BREAKPOINT_COMPACT_MAX as BREAKPOINT_COMPACT_MAX2,
|
|
10599
10990
|
computeFieldFormatContext as computeFieldFormatContext5,
|
|
10600
10991
|
defaultNumberFormatter as defaultNumberFormatter7,
|
|
10601
|
-
estimateTextWidth as
|
|
10992
|
+
estimateTextWidth as estimateTextWidth18,
|
|
10602
10993
|
resolveNumberFormatter as resolveNumberFormatter5,
|
|
10603
10994
|
resolveTheme as resolveTheme2,
|
|
10604
10995
|
TICK_LABEL_OFFSET
|
|
@@ -10616,7 +11007,7 @@ import {
|
|
|
10616
11007
|
computeFieldFormatContext as computeFieldFormatContext4,
|
|
10617
11008
|
computeXAxisExtentFromLabels as computeXAxisExtentFromLabels2,
|
|
10618
11009
|
defaultNumberFormatter as defaultNumberFormatter6,
|
|
10619
|
-
estimateTextWidth as
|
|
11010
|
+
estimateTextWidth as estimateTextWidth17,
|
|
10620
11011
|
getHeightClass,
|
|
10621
11012
|
HPAD_COMPACT_FRACTION,
|
|
10622
11013
|
HPAD_COMPACT_MIN,
|
|
@@ -10633,7 +11024,7 @@ import {
|
|
|
10633
11024
|
} from "@opendata-ai/openchart-core";
|
|
10634
11025
|
|
|
10635
11026
|
// src/endpoint-labels/predict.ts
|
|
10636
|
-
import { estimateTextWidth as
|
|
11027
|
+
import { estimateTextWidth as estimateTextWidth11, wrapText as wrapText2 } from "@opendata-ai/openchart-core";
|
|
10637
11028
|
function predictEndpointLabelsWidth(spec, _theme) {
|
|
10638
11029
|
if (spec.endpointLabels === false) return 0;
|
|
10639
11030
|
if (spec.markType !== "line" && spec.markType !== "area") return 0;
|
|
@@ -10678,12 +11069,12 @@ function predictEndpointLabelsWidth(spec, _theme) {
|
|
|
10678
11069
|
const text = content ? composeEndpointText(content, name, sample) : name;
|
|
10679
11070
|
const lines = wrapText2(text, ENDPOINT_LABEL_FONT_SIZE, ENDPOINT_LABEL_FONT_WEIGHT, wrapWidth);
|
|
10680
11071
|
for (const line4 of lines) {
|
|
10681
|
-
const w =
|
|
11072
|
+
const w = estimateTextWidth11(line4, ENDPOINT_LABEL_FONT_SIZE, ENDPOINT_LABEL_FONT_WEIGHT);
|
|
10682
11073
|
if (w > maxWidth) maxWidth = w;
|
|
10683
11074
|
}
|
|
10684
11075
|
}
|
|
10685
11076
|
if (!content && sample) {
|
|
10686
|
-
const w =
|
|
11077
|
+
const w = estimateTextWidth11(sample, ENDPOINT_VALUE_FONT_SIZE, ENDPOINT_VALUE_FONT_WEIGHT);
|
|
10687
11078
|
if (w > maxWidth) maxWidth = w;
|
|
10688
11079
|
}
|
|
10689
11080
|
return maxWidth;
|
|
@@ -10700,7 +11091,7 @@ function isEndsBoth(spec) {
|
|
|
10700
11091
|
}
|
|
10701
11092
|
|
|
10702
11093
|
// src/legend/compute.ts
|
|
10703
|
-
import { BRAND_RESERVE_WIDTH, COMPACT_WIDTH as COMPACT_WIDTH2, estimateTextWidth as
|
|
11094
|
+
import { BRAND_RESERVE_WIDTH, COMPACT_WIDTH as COMPACT_WIDTH2, estimateTextWidth as estimateTextWidth13 } from "@opendata-ai/openchart-core";
|
|
10704
11095
|
|
|
10705
11096
|
// src/layout/scales.ts
|
|
10706
11097
|
import { computeFieldFormatContext as computeFieldFormatContext3 } from "@opendata-ai/openchart-core";
|
|
@@ -10722,11 +11113,9 @@ import {
|
|
|
10722
11113
|
} from "d3-scale";
|
|
10723
11114
|
function numericFieldValues2(data, field) {
|
|
10724
11115
|
const result = [];
|
|
10725
|
-
for (const
|
|
10726
|
-
const
|
|
10727
|
-
if (
|
|
10728
|
-
const n = typeof v === "number" ? v : Number(v);
|
|
10729
|
-
if (Number.isFinite(n)) result.push(n);
|
|
11116
|
+
for (const row of data) {
|
|
11117
|
+
const value = finiteNumber(row[field]);
|
|
11118
|
+
if (value !== void 0) result.push(value);
|
|
10730
11119
|
}
|
|
10731
11120
|
return result;
|
|
10732
11121
|
}
|
|
@@ -11126,8 +11515,8 @@ function computeScales(spec, chartArea, data) {
|
|
|
11126
11515
|
const sums = /* @__PURE__ */ new Map();
|
|
11127
11516
|
for (const row of data) {
|
|
11128
11517
|
const cat = String(row[yField] ?? "");
|
|
11129
|
-
const val =
|
|
11130
|
-
if (
|
|
11518
|
+
const val = finiteNumber(row[xField]);
|
|
11519
|
+
if (val !== void 0 && val > 0) {
|
|
11131
11520
|
sums.set(cat, (sums.get(cat) ?? 0) + val);
|
|
11132
11521
|
}
|
|
11133
11522
|
}
|
|
@@ -11148,8 +11537,8 @@ function computeScales(spec, chartArea, data) {
|
|
|
11148
11537
|
const sums = /* @__PURE__ */ new Map();
|
|
11149
11538
|
for (const row of data) {
|
|
11150
11539
|
const cat = String(row[yField] ?? "");
|
|
11151
|
-
const val =
|
|
11152
|
-
if (
|
|
11540
|
+
const val = finiteNumber(row[xField]);
|
|
11541
|
+
if (val !== void 0 && val > 0) {
|
|
11153
11542
|
sums.set(cat, (sums.get(cat) ?? 0) + val);
|
|
11154
11543
|
}
|
|
11155
11544
|
}
|
|
@@ -11213,8 +11602,8 @@ function computeScales(spec, chartArea, data) {
|
|
|
11213
11602
|
const sums = /* @__PURE__ */ new Map();
|
|
11214
11603
|
for (const row of data) {
|
|
11215
11604
|
const cat = String(row[xField] ?? "");
|
|
11216
|
-
const val =
|
|
11217
|
-
if (
|
|
11605
|
+
const val = finiteNumber(row[yField]);
|
|
11606
|
+
if (val !== void 0 && val > 0) {
|
|
11218
11607
|
sums.set(cat, (sums.get(cat) ?? 0) + val);
|
|
11219
11608
|
}
|
|
11220
11609
|
}
|
|
@@ -11235,8 +11624,8 @@ function computeScales(spec, chartArea, data) {
|
|
|
11235
11624
|
const sums = /* @__PURE__ */ new Map();
|
|
11236
11625
|
for (const row of data) {
|
|
11237
11626
|
const cat = String(row[xField] ?? "");
|
|
11238
|
-
const val =
|
|
11239
|
-
if (
|
|
11627
|
+
const val = finiteNumber(row[yField]);
|
|
11628
|
+
if (val !== void 0 && val > 0) {
|
|
11240
11629
|
sums.set(cat, (sums.get(cat) ?? 0) + val);
|
|
11241
11630
|
}
|
|
11242
11631
|
}
|
|
@@ -11290,7 +11679,7 @@ function computeScales(spec, chartArea, data) {
|
|
|
11290
11679
|
}
|
|
11291
11680
|
|
|
11292
11681
|
// src/legend/wrap.ts
|
|
11293
|
-
import { COMPACT_WIDTH, estimateTextWidth as
|
|
11682
|
+
import { COMPACT_WIDTH, estimateTextWidth as estimateTextWidth12 } from "@opendata-ai/openchart-core";
|
|
11294
11683
|
var SWATCH_SIZE = 12;
|
|
11295
11684
|
var SWATCH_GAP = 6;
|
|
11296
11685
|
var ENTRY_GAP = 16;
|
|
@@ -11304,7 +11693,7 @@ function measureLegendWrap(entries, maxWidth, labelStyle, maxRows, entryGap = EN
|
|
|
11304
11693
|
if (entries.length === 0) {
|
|
11305
11694
|
return { rowCount: 0, fittingCount: 0, rowWidths: [], placements: [] };
|
|
11306
11695
|
}
|
|
11307
|
-
const measureWidth = measure ??
|
|
11696
|
+
const measureWidth = measure ?? estimateTextWidth12;
|
|
11308
11697
|
let rowCount = 1;
|
|
11309
11698
|
let rowWidth = 0;
|
|
11310
11699
|
const rowWidths = [];
|
|
@@ -11450,7 +11839,7 @@ function emptyContent(position, theme) {
|
|
|
11450
11839
|
};
|
|
11451
11840
|
}
|
|
11452
11841
|
function computeLegendContent(spec, strategy, theme, availableWidth, availableHeight, watermark = true, measure, endpointLabelsDemoted) {
|
|
11453
|
-
const measureWidth = measure ??
|
|
11842
|
+
const measureWidth = measure ?? estimateTextWidth13;
|
|
11454
11843
|
const sparklineHidden = spec.display === "sparkline" && !spec.userExplicit.legend;
|
|
11455
11844
|
if (sparklineHidden || spec.legend?.show === false || strategy.legendMaxHeight === 0) {
|
|
11456
11845
|
return emptyContent("top", theme);
|
|
@@ -11471,7 +11860,7 @@ function computeLegendContent(spec, strategy, theme, availableWidth, availableHe
|
|
|
11471
11860
|
fontVariant: "tabular-nums"
|
|
11472
11861
|
};
|
|
11473
11862
|
const labelRowHeight = Math.ceil(labelStyle2.fontSize * labelStyle2.lineHeight);
|
|
11474
|
-
const height = compact ? Math.max(continuous.barHeight, labelRowHeight) : continuous.barHeight + CONTINUOUS_LABEL_GAP + labelRowHeight;
|
|
11863
|
+
const height = compact ? Math.max(continuous.barHeight, labelRowHeight) : continuous.barHeight + CONTINUOUS_LABEL_GAP + labelRowHeight * (continuous.labelRows ?? 1);
|
|
11475
11864
|
const blockWidth = continuous.blockWidth ?? continuous.barWidth;
|
|
11476
11865
|
return {
|
|
11477
11866
|
entries: [],
|
|
@@ -11728,7 +12117,7 @@ function placeLegend(content, chartArea, containerWidth, _theme, xAxisHeight, ax
|
|
|
11728
12117
|
import {
|
|
11729
12118
|
abbreviateNumber,
|
|
11730
12119
|
buildD3Formatter,
|
|
11731
|
-
estimateTextWidth as
|
|
12120
|
+
estimateTextWidth as estimateTextWidth14,
|
|
11732
12121
|
formatNumber as formatNumber3
|
|
11733
12122
|
} from "@opendata-ai/openchart-core";
|
|
11734
12123
|
import { scaleLinear as scaleLinear3, scaleSqrt as scaleSqrt3 } from "d3-scale";
|
|
@@ -11844,11 +12233,11 @@ function computeSizeLegendContent(sizeEncoding, data, theme, options) {
|
|
|
11844
12233
|
};
|
|
11845
12234
|
});
|
|
11846
12235
|
const widestLabel = Math.max(
|
|
11847
|
-
...circles.map((c) =>
|
|
12236
|
+
...circles.map((c) => estimateTextWidth14(c.label, fontSize, theme.fonts.weights.normal))
|
|
11848
12237
|
);
|
|
11849
12238
|
const lastLabelBottom = (circles[circles.length - 1]?.labelY ?? 0) + fontSize * 0.5;
|
|
11850
12239
|
const circleBlockWidth = maxRadius * 2 + LABEL_GAP + widestLabel;
|
|
11851
|
-
const titleWidth = title ?
|
|
12240
|
+
const titleWidth = title ? estimateTextWidth14(title, fontSize, theme.fonts.weights.medium) : 0;
|
|
11852
12241
|
return {
|
|
11853
12242
|
circles,
|
|
11854
12243
|
width: Math.max(circleBlockWidth, titleWidth),
|
|
@@ -11892,7 +12281,7 @@ function placeSizeLegend(content, chartArea, theme, colorLegend) {
|
|
|
11892
12281
|
import {
|
|
11893
12282
|
axisTitleOffset,
|
|
11894
12283
|
computeXAxisExtentFromLabels,
|
|
11895
|
-
estimateTextWidth as
|
|
12284
|
+
estimateTextWidth as estimateTextWidth16,
|
|
11896
12285
|
truncateRotatedLabel,
|
|
11897
12286
|
X_AXIS_TITLE_BAND,
|
|
11898
12287
|
X_AXIS_TITLE_BAND_ROTATED
|
|
@@ -11937,11 +12326,11 @@ function bandLabelStride(maxLabelWidth, angleDeg, fontSize, spacing) {
|
|
|
11937
12326
|
}
|
|
11938
12327
|
|
|
11939
12328
|
// src/layout/axes/thinning.ts
|
|
11940
|
-
import { estimateTextWidth as
|
|
12329
|
+
import { estimateTextWidth as estimateTextWidth15 } from "@opendata-ai/openchart-core";
|
|
11941
12330
|
var MIN_TICK_GAP_FACTOR = 0.5;
|
|
11942
12331
|
var MIN_TICK_COUNT = 2;
|
|
11943
12332
|
function measureLabel(text, fontSize, fontWeight, measureText) {
|
|
11944
|
-
return measureText ? measureText(text, fontSize, fontWeight).width :
|
|
12333
|
+
return measureText ? measureText(text, fontSize, fontWeight).width : estimateTextWidth15(text, fontSize, fontWeight);
|
|
11945
12334
|
}
|
|
11946
12335
|
function ticksOverlap(ticks, fontSize, fontWeight, measureText, orientation = "horizontal") {
|
|
11947
12336
|
if (ticks.length < 2) return false;
|
|
@@ -12418,6 +12807,35 @@ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContex
|
|
|
12418
12807
|
if (isContinuousX && !hasExplicitValues && dataContext?.maxXTicks != null) {
|
|
12419
12808
|
ticks = capTickCount(ticks, dataContext.maxXTicks);
|
|
12420
12809
|
}
|
|
12810
|
+
if (dataContext?.preserveEnd && !hasExplicitValues && (scales.x.type === "time" || scales.x.type === "utc")) {
|
|
12811
|
+
const scale = scales.x.scale;
|
|
12812
|
+
const domain = scale.domain();
|
|
12813
|
+
const value = domain[domain.length - 1];
|
|
12814
|
+
const boundary = {
|
|
12815
|
+
value,
|
|
12816
|
+
position: scale(value),
|
|
12817
|
+
label: formatTickLabel(value, scales.x)
|
|
12818
|
+
};
|
|
12819
|
+
if (Number.isFinite(boundary.position)) {
|
|
12820
|
+
ticks = ticks.filter(
|
|
12821
|
+
(tick) => Number(tick.value) !== Number(value) && tick.label !== boundary.label
|
|
12822
|
+
);
|
|
12823
|
+
ticks.push(boundary);
|
|
12824
|
+
ticks.sort((a, b) => a.position - b.position);
|
|
12825
|
+
ticks = thinTicksUntilFit(ticks, fontSize, fontWeight, measureText);
|
|
12826
|
+
}
|
|
12827
|
+
}
|
|
12828
|
+
if (dataContext?.preserveEnd && !hasExplicitValues && !isContinuousX) {
|
|
12829
|
+
const domain = scales.x.scale.domain();
|
|
12830
|
+
const value = domain[domain.length - 1];
|
|
12831
|
+
const boundary = resolveExplicitTicks([value], scales.x)[0];
|
|
12832
|
+
if (boundary) {
|
|
12833
|
+
ticks = ticks.filter((tick) => tick.label !== boundary.label);
|
|
12834
|
+
ticks.push(boundary);
|
|
12835
|
+
ticks.sort((a, b) => a.position - b.position);
|
|
12836
|
+
ticks = thinTicksUntilFit(ticks, fontSize, fontWeight, measureText);
|
|
12837
|
+
}
|
|
12838
|
+
}
|
|
12421
12839
|
if (tickAngle && Math.abs(tickAngle) > 10) {
|
|
12422
12840
|
const measureWidth = (text, size, weight) => measureLabel(text, size, weight ?? fontWeight, measureText);
|
|
12423
12841
|
ticks = ticks.map((t) => {
|
|
@@ -12567,7 +12985,7 @@ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContex
|
|
|
12567
12985
|
const isRight = result.y.orient === "right";
|
|
12568
12986
|
if (isRight) {
|
|
12569
12987
|
const maxTickLabelWidth = result.y.ticks.reduce((max4, t) => {
|
|
12570
|
-
const w =
|
|
12988
|
+
const w = estimateTextWidth16(
|
|
12571
12989
|
t.label,
|
|
12572
12990
|
result.y.tickLabelStyle.fontSize,
|
|
12573
12991
|
result.y.tickLabelStyle.fontWeight ?? 400
|
|
@@ -12582,7 +13000,7 @@ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContex
|
|
|
12582
13000
|
};
|
|
12583
13001
|
} else {
|
|
12584
13002
|
const maxTickLabelWidth = result.y.ticks.reduce((max4, t) => {
|
|
12585
|
-
const w =
|
|
13003
|
+
const w = estimateTextWidth16(
|
|
12586
13004
|
t.label,
|
|
12587
13005
|
result.y.tickLabelStyle.fontSize,
|
|
12588
13006
|
result.y.tickLabelStyle.fontWeight ?? 400
|
|
@@ -12610,7 +13028,7 @@ function createMeasureFn(measureText) {
|
|
|
12610
13028
|
if (measureText) {
|
|
12611
13029
|
return (text, fontSize, fontWeight) => measureText(text, fontSize, fontWeight).width;
|
|
12612
13030
|
}
|
|
12613
|
-
return
|
|
13031
|
+
return estimateTextWidth17;
|
|
12614
13032
|
}
|
|
12615
13033
|
function sampleLabelFromContext(maxAbsVal, yAxisFormat, ctx) {
|
|
12616
13034
|
if (yAxisFormat) {
|
|
@@ -12975,6 +13393,19 @@ function resolveLayoutPlan(chartSpec, renderSpec, options, theme, strategy, wate
|
|
|
12975
13393
|
}
|
|
12976
13394
|
|
|
12977
13395
|
// src/compile/layer.ts
|
|
13396
|
+
function appendLayerMarks(layout, leafIndex, marks, tooltips, sourceMarks = layout.marks) {
|
|
13397
|
+
const scope = `layer:${leafIndex}:`;
|
|
13398
|
+
for (const [index, mark] of sourceMarks.entries()) {
|
|
13399
|
+
const content = layout.tooltipDescriptors.get(`${mark.type}-${index}`);
|
|
13400
|
+
if (content) tooltips.set(`${mark.type}-${marks.length}`, content);
|
|
13401
|
+
const key = `${scope}${mark.type}:${mark.key !== void 0 ? `key:${mark.key}` : `index:${index}`}`;
|
|
13402
|
+
marks.push({
|
|
13403
|
+
...mark,
|
|
13404
|
+
key,
|
|
13405
|
+
...(mark.type === "line" || mark.type === "area") && mark.pointKeys ? { pointKeys: mark.pointKeys.map((pointKey) => `${scope}${pointKey}`) } : {}
|
|
13406
|
+
});
|
|
13407
|
+
}
|
|
13408
|
+
}
|
|
12978
13409
|
function compileLayer(spec, options, compileChart2) {
|
|
12979
13410
|
const leaves = flattenLayers(spec);
|
|
12980
13411
|
if (leaves.length === 0) {
|
|
@@ -12982,7 +13413,11 @@ function compileLayer(spec, options, compileChart2) {
|
|
|
12982
13413
|
}
|
|
12983
13414
|
if (leaves.length === 1) {
|
|
12984
13415
|
const singleSpec = buildPrimarySpec(leaves, spec);
|
|
12985
|
-
|
|
13416
|
+
const layout = compileChart2(singleSpec, options);
|
|
13417
|
+
const marks = [];
|
|
13418
|
+
const tooltipDescriptors2 = /* @__PURE__ */ new Map();
|
|
13419
|
+
appendLayerMarks(layout, 0, marks, tooltipDescriptors2);
|
|
13420
|
+
return { ...layout, marks, tooltipDescriptors: tooltipDescriptors2 };
|
|
12986
13421
|
}
|
|
12987
13422
|
const layerOptions = { ...options, layered: true };
|
|
12988
13423
|
if (spec.resolve?.scale?.y === "independent") {
|
|
@@ -12991,6 +13426,7 @@ function compileLayer(spec, options, compileChart2) {
|
|
|
12991
13426
|
const primarySpec = buildPrimarySpec(leaves, spec);
|
|
12992
13427
|
const primaryLayout = compileChart2(primarySpec, layerOptions);
|
|
12993
13428
|
const allMarks = [];
|
|
13429
|
+
const tooltipDescriptors = /* @__PURE__ */ new Map();
|
|
12994
13430
|
const seenLabels = /* @__PURE__ */ new Set();
|
|
12995
13431
|
const pLegend = primaryLayout.legend;
|
|
12996
13432
|
const mergedLegendEntries = "entries" in pLegend ? [...pLegend.entries] : [];
|
|
@@ -12999,19 +13435,20 @@ function compileLayer(spec, options, compileChart2) {
|
|
|
12999
13435
|
}
|
|
13000
13436
|
const indexedLeaves = leaves.map((leaf, i) => ({
|
|
13001
13437
|
leaf,
|
|
13438
|
+
leafIndex: i,
|
|
13002
13439
|
zIndex: leaf.zIndex ?? i
|
|
13003
13440
|
}));
|
|
13004
13441
|
indexedLeaves.sort((a, b) => a.zIndex - b.zIndex);
|
|
13005
13442
|
const leafOptions = { ...layerOptions, frozenChartArea: primaryLayout.area };
|
|
13006
13443
|
const sharedDomains = computeSharedDomains(leaves);
|
|
13007
|
-
for (const { leaf } of indexedLeaves) {
|
|
13444
|
+
for (const { leaf, leafIndex } of indexedLeaves) {
|
|
13008
13445
|
const themedLeaf = {
|
|
13009
13446
|
...withSharedDomains(leaf, sharedDomains),
|
|
13010
13447
|
theme: leaf.theme ?? spec.theme,
|
|
13011
13448
|
darkMode: leaf.darkMode ?? spec.darkMode
|
|
13012
13449
|
};
|
|
13013
13450
|
const leafLayout = compileChart2(themedLeaf, leafOptions);
|
|
13014
|
-
|
|
13451
|
+
appendLayerMarks(leafLayout, leafIndex, allMarks, tooltipDescriptors);
|
|
13015
13452
|
const leafLeg = leafLayout.legend;
|
|
13016
13453
|
if ("entries" in leafLeg) {
|
|
13017
13454
|
for (const entry of leafLeg.entries) {
|
|
@@ -13029,6 +13466,7 @@ function compileLayer(spec, options, compileChart2) {
|
|
|
13029
13466
|
return {
|
|
13030
13467
|
...primaryLayout,
|
|
13031
13468
|
marks: allMarks,
|
|
13469
|
+
tooltipDescriptors,
|
|
13032
13470
|
legend: mergedLegend,
|
|
13033
13471
|
// `legends` is what the renderer iterates, so the merged color legend has to
|
|
13034
13472
|
// land in it too. Spreading `primaryLayout` alone would leave legends[0] as
|
|
@@ -13042,7 +13480,7 @@ function estimateYAxisLabelWidth(data, encoding, baseFontSize, measure) {
|
|
|
13042
13480
|
const yEnc = encoding.y;
|
|
13043
13481
|
const yField = yEnc.field;
|
|
13044
13482
|
if (!yField) return 40;
|
|
13045
|
-
const m = measure ??
|
|
13483
|
+
const m = measure ?? estimateTextWidth18;
|
|
13046
13484
|
const yType = yEnc.type;
|
|
13047
13485
|
if (yType === "nominal" || yType === "ordinal") {
|
|
13048
13486
|
let maxWidth = 0;
|
|
@@ -13232,20 +13670,17 @@ function compileLayerIndependent(leaves, layerSpec, options, compileChart2) {
|
|
|
13232
13670
|
mergedLegendEntries.push(entry);
|
|
13233
13671
|
}
|
|
13234
13672
|
}
|
|
13235
|
-
const l0Count = layout0.marks.length;
|
|
13236
|
-
const mergedTooltips = new Map(layout0.tooltipDescriptors);
|
|
13237
|
-
for (const [key, value] of layout1.tooltipDescriptors) {
|
|
13238
|
-
const match = /^(rect|point|arc)-(\d+)$/.exec(key);
|
|
13239
|
-
if (match) {
|
|
13240
|
-
const offsetKey = `${match[1]}-${Number(match[2]) + l0Count}`;
|
|
13241
|
-
mergedTooltips.set(offsetKey, value);
|
|
13242
|
-
} else {
|
|
13243
|
-
mergedTooltips.set(key, value);
|
|
13244
|
-
}
|
|
13245
|
-
}
|
|
13246
13673
|
const z0 = leaf0.zIndex ?? 0;
|
|
13247
13674
|
const z1 = leaf1.zIndex ?? 1;
|
|
13248
|
-
const marks =
|
|
13675
|
+
const marks = [];
|
|
13676
|
+
const mergedTooltips = /* @__PURE__ */ new Map();
|
|
13677
|
+
const layers = [
|
|
13678
|
+
{ layout: layout0, leafIndex: 0, sourceMarks: adjustedMarks0, zIndex: z0 },
|
|
13679
|
+
{ layout: layout1, leafIndex: 1, sourceMarks: taggedMarks1, zIndex: z1 }
|
|
13680
|
+
].sort((a, b) => a.zIndex - b.zIndex);
|
|
13681
|
+
for (const layer of layers) {
|
|
13682
|
+
appendLayerMarks(layer.layout, layer.leafIndex, marks, mergedTooltips, layer.sourceMarks);
|
|
13683
|
+
}
|
|
13249
13684
|
const mergedLegend = {
|
|
13250
13685
|
...layout0.legend,
|
|
13251
13686
|
..."entries" in l0Legend ? { entries: mergedLegendEntries } : {}
|
|
@@ -13312,8 +13747,8 @@ function withUnionXDomain(leaf0, leaf1) {
|
|
|
13312
13747
|
let min4 = Number.POSITIVE_INFINITY;
|
|
13313
13748
|
let max4 = Number.NEGATIVE_INFINITY;
|
|
13314
13749
|
for (const v of allValues) {
|
|
13315
|
-
const n =
|
|
13316
|
-
if (
|
|
13750
|
+
const n = finiteNumber(v);
|
|
13751
|
+
if (n === void 0) continue;
|
|
13317
13752
|
if (n < min4) min4 = n;
|
|
13318
13753
|
if (n > max4) max4 = n;
|
|
13319
13754
|
}
|
|
@@ -13344,8 +13779,8 @@ function alignYDomains(leaf0, leaf1) {
|
|
|
13344
13779
|
if (yEnc0.scale?.domain || yEnc1.scale?.domain) return void 0;
|
|
13345
13780
|
const includeZero0 = yEnc0.scale?.zero !== false;
|
|
13346
13781
|
const includeZero1 = yEnc1.scale?.zero !== false;
|
|
13347
|
-
const vals0 = leaf0.data.map((
|
|
13348
|
-
const vals1 = leaf1.data.map((
|
|
13782
|
+
const vals0 = leaf0.data.map((row) => finiteNumber(row[yEnc0.field])).filter((value) => value !== void 0);
|
|
13783
|
+
const vals1 = leaf1.data.map((row) => finiteNumber(row[yEnc1.field])).filter((value) => value !== void 0);
|
|
13349
13784
|
if (vals0.length === 0 || vals1.length === 0) return void 0;
|
|
13350
13785
|
const niced = (vals, includeZero) => {
|
|
13351
13786
|
let lo = Math.min(...vals);
|
|
@@ -13416,8 +13851,8 @@ function computeSharedDomains(leaves) {
|
|
|
13416
13851
|
break;
|
|
13417
13852
|
}
|
|
13418
13853
|
for (const row of leaf.data ?? []) {
|
|
13419
|
-
const v =
|
|
13420
|
-
if (
|
|
13854
|
+
const v = finiteNumber(row[enc.field]);
|
|
13855
|
+
if (v === void 0) continue;
|
|
13421
13856
|
if (v < lo) lo = v;
|
|
13422
13857
|
if (v > hi) hi = v;
|
|
13423
13858
|
sawAny = true;
|
|
@@ -13574,7 +14009,7 @@ function resolveYouDrawIt(config, marks, scales, chartArea, xField) {
|
|
|
13574
14009
|
}
|
|
13575
14010
|
|
|
13576
14011
|
// src/compiler/a11y-warnings.ts
|
|
13577
|
-
import { contrastRatio, findAccessibleColor, isOpaqueColor } from "@opendata-ai/openchart-core";
|
|
14012
|
+
import { contrastRatio as contrastRatio2, findAccessibleColor, isOpaqueColor as isOpaqueColor2 } from "@opendata-ai/openchart-core";
|
|
13578
14013
|
var GRAPHIC_MIN = 3;
|
|
13579
14014
|
var TEXT_MIN = 4.5;
|
|
13580
14015
|
var ADJACENT_FILL_MARKS = /* @__PURE__ */ new Set(["bar", "area", "arc", "waffle"]);
|
|
@@ -13582,7 +14017,7 @@ var fmt = (ratio) => `${ratio.toFixed(2)}:1`;
|
|
|
13582
14017
|
function collectContrastWarnings(scales, markType, theme) {
|
|
13583
14018
|
const warnings = [];
|
|
13584
14019
|
const bg = theme.colors.background;
|
|
13585
|
-
const bgIsCheckable =
|
|
14020
|
+
const bgIsCheckable = isOpaqueColor2(bg);
|
|
13586
14021
|
const series = [];
|
|
13587
14022
|
if (scales.color && scales.color.type !== "sequential") {
|
|
13588
14023
|
const ordinal = scales.color.scale;
|
|
@@ -13594,7 +14029,7 @@ function collectContrastWarnings(scales, markType, theme) {
|
|
|
13594
14029
|
for (let i = 0; i < series.length - 1; i++) {
|
|
13595
14030
|
const a = series[i];
|
|
13596
14031
|
const b = series[i + 1];
|
|
13597
|
-
const ratio =
|
|
14032
|
+
const ratio = contrastRatio2(a.color, b.color);
|
|
13598
14033
|
if (ratio < GRAPHIC_MIN) {
|
|
13599
14034
|
const suggestion = findAccessibleColor(b.color, a.color, GRAPHIC_MIN);
|
|
13600
14035
|
warnings.push(
|
|
@@ -13605,7 +14040,7 @@ function collectContrastWarnings(scales, markType, theme) {
|
|
|
13605
14040
|
}
|
|
13606
14041
|
if (bgIsCheckable) {
|
|
13607
14042
|
for (const s of series) {
|
|
13608
|
-
const ratio =
|
|
14043
|
+
const ratio = contrastRatio2(s.color, bg);
|
|
13609
14044
|
if (ratio < GRAPHIC_MIN) {
|
|
13610
14045
|
const suggestion = findAccessibleColor(s.color, bg, GRAPHIC_MIN);
|
|
13611
14046
|
warnings.push(
|
|
@@ -13618,7 +14053,7 @@ function collectContrastWarnings(scales, markType, theme) {
|
|
|
13618
14053
|
{ name: "theme.colors.annotationText", color: theme.colors.annotationText }
|
|
13619
14054
|
];
|
|
13620
14055
|
for (const token of textTokens) {
|
|
13621
|
-
const ratio =
|
|
14056
|
+
const ratio = contrastRatio2(token.color, bg);
|
|
13622
14057
|
if (ratio < TEXT_MIN) {
|
|
13623
14058
|
const suggestion = findAccessibleColor(token.color, bg, TEXT_MIN);
|
|
13624
14059
|
warnings.push(
|
|
@@ -13733,16 +14168,14 @@ import {
|
|
|
13733
14168
|
buildD3Formatter as buildD3Formatter2,
|
|
13734
14169
|
CATEGORICAL_PALETTE as CATEGORICAL_PALETTE2,
|
|
13735
14170
|
computeChrome as computeChrome3,
|
|
13736
|
-
estimateTextWidth as
|
|
14171
|
+
estimateTextWidth as estimateTextWidth19,
|
|
13737
14172
|
formatNumber as formatNumber4,
|
|
13738
14173
|
getBreakpoint,
|
|
13739
14174
|
HPAD_COMPACT_FRACTION as HPAD_COMPACT_FRACTION2,
|
|
13740
14175
|
HPAD_COMPACT_MIN as HPAD_COMPACT_MIN2,
|
|
13741
|
-
resolveTheme as resolveTheme3
|
|
13742
|
-
SEQUENTIAL_PALETTES as SEQUENTIAL_PALETTES2
|
|
14176
|
+
resolveTheme as resolveTheme3
|
|
13743
14177
|
} from "@opendata-ai/openchart-core";
|
|
13744
14178
|
import { geoArea, geoPath } from "d3-geo";
|
|
13745
|
-
import { scaleQuantile as scaleQuantile3 } from "d3-scale";
|
|
13746
14179
|
import { feature as topoFeature, mesh as topoMesh } from "topojson-client";
|
|
13747
14180
|
|
|
13748
14181
|
// src/geo/join.ts
|
|
@@ -13776,25 +14209,50 @@ function suggestNormalization(unmatchedKeys, featureIds) {
|
|
|
13776
14209
|
function joinDataToFeatures(features, data, dataKeyField, featureIdField) {
|
|
13777
14210
|
const warnings = [];
|
|
13778
14211
|
const dataLookup = /* @__PURE__ */ new Map();
|
|
14212
|
+
const duplicates = /* @__PURE__ */ new Set();
|
|
14213
|
+
const lowerCounts = /* @__PURE__ */ new Map();
|
|
14214
|
+
let missingKeys = 0;
|
|
13779
14215
|
for (const row of data) {
|
|
13780
|
-
const
|
|
14216
|
+
const value = row[dataKeyField];
|
|
14217
|
+
if (value == null || String(value).trim() === "") {
|
|
14218
|
+
missingKeys++;
|
|
14219
|
+
continue;
|
|
14220
|
+
}
|
|
14221
|
+
const key = String(value);
|
|
14222
|
+
if (dataLookup.has(key)) duplicates.add(key);
|
|
13781
14223
|
dataLookup.set(key, row);
|
|
14224
|
+
lowerCounts.set(key.toLowerCase(), (lowerCounts.get(key.toLowerCase()) ?? 0) + 1);
|
|
13782
14225
|
}
|
|
14226
|
+
if (duplicates.size)
|
|
14227
|
+
warnings.push({
|
|
14228
|
+
code: "DUPLICATE_DATA_KEYS",
|
|
14229
|
+
message: `${duplicates.size} duplicate map key(s) are ambiguous and will use neutral fill. Filter or aggregate to one observation per feature.`,
|
|
14230
|
+
context: { keys: [...duplicates] }
|
|
14231
|
+
});
|
|
14232
|
+
if (missingKeys)
|
|
14233
|
+
warnings.push({
|
|
14234
|
+
code: "MISSING_DATA_KEYS",
|
|
14235
|
+
message: `${missingKeys} data row(s) have no map key.`,
|
|
14236
|
+
context: { count: missingKeys }
|
|
14237
|
+
});
|
|
14238
|
+
const ambiguousFallbacks = /* @__PURE__ */ new Set();
|
|
13783
14239
|
const joined = /* @__PURE__ */ new Map();
|
|
13784
14240
|
const usedDataKeys = /* @__PURE__ */ new Set();
|
|
13785
14241
|
let lowerLookup = null;
|
|
13786
14242
|
let unmatchedFeatureCount = 0;
|
|
13787
14243
|
for (const feature of features) {
|
|
13788
14244
|
const featureId = featureIdField === "id" ? String(feature.id) : String(feature.properties[featureIdField] ?? feature.id);
|
|
13789
|
-
let match = dataLookup.get(featureId);
|
|
13790
|
-
if (!match) {
|
|
14245
|
+
let match = duplicates.has(featureId) ? void 0 : dataLookup.get(featureId);
|
|
14246
|
+
if (!match && !duplicates.has(featureId)) {
|
|
13791
14247
|
if (!lowerLookup) {
|
|
13792
14248
|
lowerLookup = /* @__PURE__ */ new Map();
|
|
13793
14249
|
for (const [key, row] of dataLookup) {
|
|
13794
|
-
lowerLookup.set(key.toLowerCase(), row);
|
|
14250
|
+
if (lowerCounts.get(key.toLowerCase()) === 1) lowerLookup.set(key.toLowerCase(), row);
|
|
13795
14251
|
}
|
|
13796
14252
|
}
|
|
13797
|
-
|
|
14253
|
+
const lower = featureId.toLowerCase();
|
|
14254
|
+
match = lowerLookup.get(lower);
|
|
14255
|
+
if (!match && (lowerCounts.get(lower) ?? 0) > 1) ambiguousFallbacks.add(featureId);
|
|
13798
14256
|
}
|
|
13799
14257
|
if (match) {
|
|
13800
14258
|
joined.set(feature.id, match);
|
|
@@ -13803,6 +14261,12 @@ function joinDataToFeatures(features, data, dataKeyField, featureIdField) {
|
|
|
13803
14261
|
unmatchedFeatureCount++;
|
|
13804
14262
|
}
|
|
13805
14263
|
}
|
|
14264
|
+
if (ambiguousFallbacks.size)
|
|
14265
|
+
warnings.push({
|
|
14266
|
+
code: "AMBIGUOUS_DATA_KEYS",
|
|
14267
|
+
message: `${ambiguousFallbacks.size} feature(s) match multiple case variants and will use neutral fill. Use exact, unique keys.`,
|
|
14268
|
+
context: { keys: [...ambiguousFallbacks] }
|
|
14269
|
+
});
|
|
13806
14270
|
const unmatchedDataKeys = [];
|
|
13807
14271
|
for (const key of dataLookup.keys()) {
|
|
13808
14272
|
if (!usedDataKeys.has(key)) {
|
|
@@ -13948,10 +14412,10 @@ function categoricalLegendLabelStyle(theme) {
|
|
|
13948
14412
|
};
|
|
13949
14413
|
}
|
|
13950
14414
|
function categoricalEntryWidth(label, labelStyle) {
|
|
13951
|
-
return CATEGORICAL_SWATCH_SIZE + CATEGORICAL_SWATCH_GAP +
|
|
14415
|
+
return CATEGORICAL_SWATCH_SIZE + CATEGORICAL_SWATCH_GAP + estimateTextWidth19(label, labelStyle.fontSize, labelStyle.fontWeight) + CATEGORICAL_ENTRY_GAP;
|
|
13952
14416
|
}
|
|
13953
14417
|
function noDataBlockWidth(labelStyle) {
|
|
13954
|
-
return CATEGORICAL_SWATCH_SIZE + NO_DATA_LABEL_GAP +
|
|
14418
|
+
return CATEGORICAL_SWATCH_SIZE + NO_DATA_LABEL_GAP + estimateTextWidth19("No data", labelStyle.fontSize, labelStyle.fontWeight);
|
|
13955
14419
|
}
|
|
13956
14420
|
function wrapCategoricalEntries(widths, legendWidth) {
|
|
13957
14421
|
const placed = [];
|
|
@@ -14079,11 +14543,8 @@ function compileGeoMap(spec, options) {
|
|
|
14079
14543
|
if (colorEncoding && isQuantitative) {
|
|
14080
14544
|
const colorValues = [];
|
|
14081
14545
|
for (const row of mapSpec.data) {
|
|
14082
|
-
const
|
|
14083
|
-
if (
|
|
14084
|
-
const n = Number(v);
|
|
14085
|
-
if (!Number.isNaN(n)) colorValues.push(n);
|
|
14086
|
-
}
|
|
14546
|
+
const value = finiteNumber(row[colorEncoding.field]);
|
|
14547
|
+
if (value !== void 0) colorValues.push(value);
|
|
14087
14548
|
}
|
|
14088
14549
|
const legendChannel = {
|
|
14089
14550
|
...colorEncoding,
|
|
@@ -14105,7 +14566,7 @@ function compileGeoMap(spec, options) {
|
|
|
14105
14566
|
legendTitle = legendTitleFor(colorEncoding);
|
|
14106
14567
|
const labelRowHeight = Math.ceil(theme.fonts.sizes.small * 1.3);
|
|
14107
14568
|
const titleRowHeight = legendTitle ? labelRowHeight : 0;
|
|
14108
|
-
legendBlockHeight = titleRowHeight + continuousContent.barHeight + CONTINUOUS_LABEL_GAP + labelRowHeight;
|
|
14569
|
+
legendBlockHeight = titleRowHeight + continuousContent.barHeight + CONTINUOUS_LABEL_GAP + labelRowHeight * (continuousContent.labelRows ?? 1);
|
|
14109
14570
|
}
|
|
14110
14571
|
}
|
|
14111
14572
|
if (legendBlockHeight === 0 && showChoroplethLegend && colorEncoding && !isQuantitative) {
|
|
@@ -14121,10 +14582,24 @@ function compileGeoMap(spec, options) {
|
|
|
14121
14582
|
const rows = placed.length > 0 ? placed[placed.length - 1].row + 1 : 1;
|
|
14122
14583
|
legendBlockHeight = rows * (Math.max(10, labelHeight) + 6);
|
|
14123
14584
|
}
|
|
14585
|
+
const pointColor = mapSpec.points?.color;
|
|
14586
|
+
const pointColorChannel = pointColor?.type === "quantitative" ? { ...pointColor, scale: { ...pointColor.scale, type: "quantile" } } : void 0;
|
|
14587
|
+
const pointColorValues = pointColorChannel ? mapSpec.points.data.flatMap((row) => {
|
|
14588
|
+
const value = finiteNumber(row[pointColorChannel.field]);
|
|
14589
|
+
return value === void 0 ? [] : [value];
|
|
14590
|
+
}) : [];
|
|
14591
|
+
const pointClassScale = pointColorChannel && pointColorValues.length ? classScaleForChannel(pointColorValues, pointColorChannel, theme) : void 0;
|
|
14592
|
+
const pointContinuousContent = showLegend && pointColorChannel ? computeContinuousLegendContentForChannel(
|
|
14593
|
+
pointColorValues,
|
|
14594
|
+
pointColorChannel,
|
|
14595
|
+
theme,
|
|
14596
|
+
fullArea.width,
|
|
14597
|
+
pointClassScale
|
|
14598
|
+
) : null;
|
|
14124
14599
|
if (showLegend && !pointLegendOverlay && mapSpec.points?.color) {
|
|
14125
14600
|
if (mapSpec.points.color.type === "quantitative") {
|
|
14126
14601
|
const labelRowHeight = Math.ceil(theme.fonts.sizes.small * 1.3);
|
|
14127
|
-
legendBlockHeight += CONTINUOUS_BAR_HEIGHT + CONTINUOUS_LABEL_GAP + labelRowHeight + 8;
|
|
14602
|
+
legendBlockHeight += (pointContinuousContent?.barHeight ?? CONTINUOUS_BAR_HEIGHT) + CONTINUOUS_LABEL_GAP + labelRowHeight * (pointContinuousContent?.labelRows ?? 1) + 8;
|
|
14128
14603
|
} else {
|
|
14129
14604
|
const ptLabelHeight = Math.ceil(theme.fonts.sizes.small * 1.3);
|
|
14130
14605
|
legendBlockHeight += Math.max(10, ptLabelHeight) + 6 + 8;
|
|
@@ -14187,6 +14662,10 @@ function compileGeoMap(spec, options) {
|
|
|
14187
14662
|
}
|
|
14188
14663
|
const droppedFeatures = [];
|
|
14189
14664
|
const formatter = buildD3Formatter2(mapSpec.encoding?.color?.format ?? mapSpec.valueFormat) ?? formatNumber4;
|
|
14665
|
+
const formatQuantitative = (value) => {
|
|
14666
|
+
const number = finiteNumber(value);
|
|
14667
|
+
return number === void 0 ? "No data" : formatter(number);
|
|
14668
|
+
};
|
|
14190
14669
|
const neutralFill = theme.colors.neutral[100];
|
|
14191
14670
|
const noDataPattern = {
|
|
14192
14671
|
type: "diagonal",
|
|
@@ -14249,7 +14728,7 @@ function compileGeoMap(spec, options) {
|
|
|
14249
14728
|
const labelY = bar.y + bar.height + CONTINUOUS_LABEL_GAP + labelStyle.fontSize;
|
|
14250
14729
|
const hasUnmatched = featureMarks.some((m) => m.noData);
|
|
14251
14730
|
const noDataX = bar.x + bar.width + NO_DATA_SWATCH_GAP;
|
|
14252
|
-
const noDataLabelWidth =
|
|
14731
|
+
const noDataLabelWidth = estimateTextWidth19(
|
|
14253
14732
|
"No data",
|
|
14254
14733
|
labelStyle.fontSize,
|
|
14255
14734
|
labelStyle.fontWeight
|
|
@@ -14348,21 +14827,10 @@ function compileGeoMap(spec, options) {
|
|
|
14348
14827
|
const pointCategoryColors = /* @__PURE__ */ new Map();
|
|
14349
14828
|
if (pts.color) {
|
|
14350
14829
|
if (pts.color.type === "quantitative") {
|
|
14351
|
-
|
|
14352
|
-
for (const row of pts.data) {
|
|
14353
|
-
const raw = row[pts.color.field];
|
|
14354
|
-
if (raw != null) {
|
|
14355
|
-
const v = Number(raw);
|
|
14356
|
-
if (Number.isFinite(v)) colorValues.push(v);
|
|
14357
|
-
}
|
|
14358
|
-
}
|
|
14359
|
-
if (colorValues.length > 0) {
|
|
14360
|
-
const palette = pts.color.scale?.scheme ?? "blue";
|
|
14361
|
-
const paletteStops = [...SEQUENTIAL_PALETTES2[palette] ?? SEQUENTIAL_PALETTES2.blue];
|
|
14362
|
-
const qScale = scaleQuantile3().domain(colorValues).range(paletteStops);
|
|
14830
|
+
if (pointClassScale) {
|
|
14363
14831
|
pointColorScale = (val) => {
|
|
14364
|
-
const
|
|
14365
|
-
return
|
|
14832
|
+
const value = finiteNumber(val);
|
|
14833
|
+
return value !== void 0 ? pointClassScale.color(value) : neutralFill;
|
|
14366
14834
|
};
|
|
14367
14835
|
}
|
|
14368
14836
|
} else {
|
|
@@ -14399,16 +14867,19 @@ function compileGeoMap(spec, options) {
|
|
|
14399
14867
|
const droppedPoints = [];
|
|
14400
14868
|
for (let i = 0; i < pts.data.length; i++) {
|
|
14401
14869
|
const row = pts.data[i];
|
|
14402
|
-
const lon =
|
|
14403
|
-
const lat =
|
|
14404
|
-
if (
|
|
14870
|
+
const lon = finiteNumber(row[lonField]);
|
|
14871
|
+
const lat = finiteNumber(row[latField]);
|
|
14872
|
+
if (lon === void 0 || lat === void 0) continue;
|
|
14873
|
+
const sizeValue = pts.size ? finiteNumber(row[pts.size.field]) : void 0;
|
|
14874
|
+
if (pts.size && sizeValue === void 0) continue;
|
|
14405
14875
|
const projected = projection([lon, lat]);
|
|
14406
|
-
if (!projected) {
|
|
14876
|
+
if (!projected || !projected.every(Number.isFinite)) {
|
|
14407
14877
|
droppedPoints.push([lon, lat]);
|
|
14408
14878
|
continue;
|
|
14409
14879
|
}
|
|
14410
|
-
const r = sizeScale ? sizeScale.scale(
|
|
14411
|
-
|
|
14880
|
+
const r = sizeScale && sizeValue !== void 0 ? sizeScale.scale(sizeValue) : 5;
|
|
14881
|
+
if (!Number.isFinite(r) || r < 0) continue;
|
|
14882
|
+
const fill = pointColorScale && pts.color ? pointColorScale(row[pts.color.field]) : pts.color?.type === "quantitative" ? neutralFill : defaultFill;
|
|
14412
14883
|
const key = keyChannel ? String(row[keyChannel.field] ?? i) : String(i);
|
|
14413
14884
|
const stroke = resolveKnockoutColor(theme);
|
|
14414
14885
|
pointMarks.push({
|
|
@@ -14472,24 +14943,7 @@ function compileGeoMap(spec, options) {
|
|
|
14472
14943
|
};
|
|
14473
14944
|
}
|
|
14474
14945
|
if (pts.color && pts.color.type === "quantitative" && showLegend) {
|
|
14475
|
-
const
|
|
14476
|
-
for (const row of pts.data) {
|
|
14477
|
-
const v = row[pts.color.field];
|
|
14478
|
-
if (v != null) {
|
|
14479
|
-
const n = Number(v);
|
|
14480
|
-
if (Number.isFinite(n)) ptColorValues.push(n);
|
|
14481
|
-
}
|
|
14482
|
-
}
|
|
14483
|
-
const ptLegendChannel = {
|
|
14484
|
-
...pts.color,
|
|
14485
|
-
scale: { ...pts.color.scale, type: "quantile" }
|
|
14486
|
-
};
|
|
14487
|
-
const ptContinuousContent = computeContinuousLegendContentForChannel(
|
|
14488
|
-
ptColorValues,
|
|
14489
|
-
ptLegendChannel,
|
|
14490
|
-
theme,
|
|
14491
|
-
fullArea.width
|
|
14492
|
-
);
|
|
14946
|
+
const ptContinuousContent = pointContinuousContent;
|
|
14493
14947
|
if (ptContinuousContent) {
|
|
14494
14948
|
const labelStyle = {
|
|
14495
14949
|
fontFamily: theme.fonts.family,
|
|
@@ -14504,7 +14958,7 @@ function compileGeoMap(spec, options) {
|
|
|
14504
14958
|
const legendX = pointLegendOverlay ? fullArea.x + OVERLAY_LEGEND_INSET : fullArea.x;
|
|
14505
14959
|
const legendY = pointLegendOverlay ? fullArea.y + OVERLAY_LEGEND_INSET : fullArea.y + mapAreaHeight + legendReserveGap + bottomChoroplethH + choroplethLegendGap;
|
|
14506
14960
|
const ptLabelRowHeight = Math.ceil(theme.fonts.sizes.small * 1.3);
|
|
14507
|
-
const ptLegendHeight = ptContinuousContent.barHeight + CONTINUOUS_LABEL_GAP + ptLabelRowHeight;
|
|
14961
|
+
const ptLegendHeight = ptContinuousContent.barHeight + CONTINUOUS_LABEL_GAP + ptLabelRowHeight * (ptContinuousContent.labelRows ?? 1);
|
|
14508
14962
|
const bounds = {
|
|
14509
14963
|
x: legendX,
|
|
14510
14964
|
y: legendY,
|
|
@@ -14578,17 +15032,17 @@ function compileGeoMap(spec, options) {
|
|
|
14578
15032
|
if (tooltipChannels) {
|
|
14579
15033
|
for (const ch of tooltipChannels) {
|
|
14580
15034
|
const val = mark.data[ch.field];
|
|
14581
|
-
if (val != null) {
|
|
15035
|
+
if (val != null || ch.type === "quantitative") {
|
|
14582
15036
|
fields.push({
|
|
14583
15037
|
label: ch.title ?? ch.field,
|
|
14584
|
-
value: ch.type === "quantitative" ?
|
|
15038
|
+
value: ch.type === "quantitative" ? formatQuantitative(val) : String(val)
|
|
14585
15039
|
});
|
|
14586
15040
|
}
|
|
14587
15041
|
}
|
|
14588
|
-
} else if (colorEncoding && isQuantitative
|
|
15042
|
+
} else if (colorEncoding && isQuantitative) {
|
|
14589
15043
|
fields.push({
|
|
14590
15044
|
label: colorEncoding.title ?? colorEncoding.field,
|
|
14591
|
-
value:
|
|
15045
|
+
value: formatQuantitative(mark.data[colorEncoding.field])
|
|
14592
15046
|
});
|
|
14593
15047
|
} else if (colorEncoding && !isQuantitative && mark.data[colorEncoding.field] != null) {
|
|
14594
15048
|
fields.push({
|
|
@@ -14605,10 +15059,10 @@ function compileGeoMap(spec, options) {
|
|
|
14605
15059
|
if (ptTooltipChannels) {
|
|
14606
15060
|
for (const ch of ptTooltipChannels) {
|
|
14607
15061
|
const val = pm.data[ch.field];
|
|
14608
|
-
if (val != null) {
|
|
15062
|
+
if (val != null || ch.type === "quantitative") {
|
|
14609
15063
|
fields.push({
|
|
14610
15064
|
label: ch.title ?? ch.field,
|
|
14611
|
-
value: ch.type === "quantitative" ?
|
|
15065
|
+
value: ch.type === "quantitative" ? formatQuantitative(val) : String(val)
|
|
14612
15066
|
});
|
|
14613
15067
|
}
|
|
14614
15068
|
}
|
|
@@ -14621,16 +15075,15 @@ function compileGeoMap(spec, options) {
|
|
|
14621
15075
|
}
|
|
14622
15076
|
if (pts.color) {
|
|
14623
15077
|
const val = pm.data[pts.color.field];
|
|
14624
|
-
if (val != null)
|
|
15078
|
+
if (val != null || pts.color.type === "quantitative")
|
|
14625
15079
|
fields.push({
|
|
14626
15080
|
label: pts.color.title ?? pts.color.field,
|
|
14627
|
-
value: pts.color.type === "quantitative" ?
|
|
15081
|
+
value: pts.color.type === "quantitative" ? formatQuantitative(val) : String(val)
|
|
14628
15082
|
});
|
|
14629
15083
|
}
|
|
14630
15084
|
if (pts.size) {
|
|
14631
15085
|
const val = pm.data[pts.size.field];
|
|
14632
|
-
|
|
14633
|
-
fields.push({ label: pts.size.title ?? pts.size.field, value: formatter(Number(val)) });
|
|
15086
|
+
fields.push({ label: pts.size.title ?? pts.size.field, value: formatQuantitative(val) });
|
|
14634
15087
|
}
|
|
14635
15088
|
}
|
|
14636
15089
|
if (fields.length > 0) {
|
|
@@ -14688,7 +15141,7 @@ function compileGeoMap(spec, options) {
|
|
|
14688
15141
|
height: contentHeight,
|
|
14689
15142
|
animation: resolvedAnimation,
|
|
14690
15143
|
watermark,
|
|
14691
|
-
measureText: options.measureText ?? ((text, fontSize) => ({ width:
|
|
15144
|
+
measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth19(text, fontSize), height: fontSize })),
|
|
14692
15145
|
warnings: compileWarnings,
|
|
14693
15146
|
mapSize: { width: fullArea.width, height: mapAreaHeight },
|
|
14694
15147
|
focus: resolvedFocus
|
|
@@ -14759,22 +15212,13 @@ function buildQuantitativeMarks(opts) {
|
|
|
14759
15212
|
}
|
|
14760
15213
|
const featureId = feat.id;
|
|
14761
15214
|
const dataRow = joined.get(featureId) ?? null;
|
|
14762
|
-
const hasData = dataRow !== null;
|
|
14763
15215
|
let fill = neutralFill;
|
|
14764
15216
|
let binIndex;
|
|
14765
|
-
|
|
14766
|
-
|
|
14767
|
-
|
|
14768
|
-
|
|
14769
|
-
|
|
14770
|
-
if (!Number.isNaN(v)) {
|
|
14771
|
-
hasValue = true;
|
|
14772
|
-
if (classScale) {
|
|
14773
|
-
fill = classScale.color(v);
|
|
14774
|
-
binIndex = classScale.binIndex(v);
|
|
14775
|
-
}
|
|
14776
|
-
}
|
|
14777
|
-
}
|
|
15217
|
+
const value = finiteNumber(dataRow?.[colorField]);
|
|
15218
|
+
const hasValue = value !== void 0;
|
|
15219
|
+
if (value !== void 0 && classScale) {
|
|
15220
|
+
fill = classScale.color(value);
|
|
15221
|
+
binIndex = classScale.binIndex(value);
|
|
14778
15222
|
}
|
|
14779
15223
|
const props = feat.properties;
|
|
14780
15224
|
const name = props?.name ?? props?.NAME;
|
|
@@ -14798,7 +15242,7 @@ function buildQuantitativeMarks(opts) {
|
|
|
14798
15242
|
data: dataRow,
|
|
14799
15243
|
aria: {
|
|
14800
15244
|
role: "img",
|
|
14801
|
-
label: name ? `${name}: ${
|
|
15245
|
+
label: name ? `${name}: ${value !== void 0 ? formatter(value) : "no data"}` : String(featureId)
|
|
14802
15246
|
},
|
|
14803
15247
|
animationIndex: animIndex++,
|
|
14804
15248
|
bounds,
|
|
@@ -15073,7 +15517,7 @@ function emptyLayout3(chrome, theme, options, watermark) {
|
|
|
15073
15517
|
height: options.height,
|
|
15074
15518
|
animation: void 0,
|
|
15075
15519
|
watermark,
|
|
15076
|
-
measureText: options.measureText ?? ((text, fontSize) => ({ width:
|
|
15520
|
+
measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth19(text, fontSize), height: fontSize })),
|
|
15077
15521
|
warnings: [],
|
|
15078
15522
|
mapSize: { width: 0, height: 0 },
|
|
15079
15523
|
focus: null
|
|
@@ -15824,7 +16268,7 @@ import {
|
|
|
15824
16268
|
computeChrome as computeChrome5,
|
|
15825
16269
|
computeFieldFormatContext as computeFieldFormatContext6,
|
|
15826
16270
|
defaultNumberFormatter as defaultNumberFormatter9,
|
|
15827
|
-
estimateTextWidth as
|
|
16271
|
+
estimateTextWidth as estimateTextWidth21,
|
|
15828
16272
|
getHeightClass as getHeightClass2,
|
|
15829
16273
|
HPAD_COMPACT_FRACTION as HPAD_COMPACT_FRACTION3,
|
|
15830
16274
|
HPAD_COMPACT_MIN as HPAD_COMPACT_MIN3,
|
|
@@ -15841,7 +16285,7 @@ import {
|
|
|
15841
16285
|
} from "@opendata-ai/openchart-core";
|
|
15842
16286
|
|
|
15843
16287
|
// src/layout/metrics.ts
|
|
15844
|
-
import { estimateTextWidth as
|
|
16288
|
+
import { estimateTextWidth as estimateTextWidth20 } from "@opendata-ai/openchart-core";
|
|
15845
16289
|
var DEFAULT_LABEL_FONT_SIZE = 10;
|
|
15846
16290
|
var DEFAULT_VALUE_FONT_SIZE = 22;
|
|
15847
16291
|
var LABEL_LINE_HEIGHT_RATIO = 1.4;
|
|
@@ -15874,7 +16318,7 @@ function computeMetricBar(metrics, metricsTopY, metricsArea, remainingChartHeigh
|
|
|
15874
16318
|
const cellWidth = metricsArea.width / metrics.length;
|
|
15875
16319
|
for (const metric of metrics) {
|
|
15876
16320
|
const text = valueRunText(metric);
|
|
15877
|
-
const measured = measureText ? measureText(text, fonts.value, 510).width :
|
|
16321
|
+
const measured = measureText ? measureText(text, fonts.value, 510).width : estimateTextWidth20(text, fonts.value, 510);
|
|
15878
16322
|
if (measured > cellWidth - CELL_INNER_PAD) return void 0;
|
|
15879
16323
|
}
|
|
15880
16324
|
const labelLine = fonts.label * LABEL_LINE_HEIGHT_RATIO;
|
|
@@ -16009,7 +16453,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
16009
16453
|
if (xField) {
|
|
16010
16454
|
for (const row of spec.data) {
|
|
16011
16455
|
const label = String(row[xField] ?? "");
|
|
16012
|
-
const w =
|
|
16456
|
+
const w = estimateTextWidth21(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
|
|
16013
16457
|
if (w > maxLabelWidth) maxLabelWidth = w;
|
|
16014
16458
|
}
|
|
16015
16459
|
}
|
|
@@ -16063,7 +16507,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
16063
16507
|
const label = String(row[colorField] ?? "");
|
|
16064
16508
|
if (!seen.has(label)) {
|
|
16065
16509
|
seen.add(label);
|
|
16066
|
-
const w =
|
|
16510
|
+
const w = estimateTextWidth21(label, 11, 600);
|
|
16067
16511
|
if (w > maxLabelWidth) maxLabelWidth = w;
|
|
16068
16512
|
}
|
|
16069
16513
|
}
|
|
@@ -16083,7 +16527,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
16083
16527
|
const maxXStr = String(maxX);
|
|
16084
16528
|
for (const ann of spec.annotations) {
|
|
16085
16529
|
if (ann.type === "text" && String(ann.x) === maxXStr) {
|
|
16086
|
-
const textWidth =
|
|
16530
|
+
const textWidth = estimateTextWidth21(ann.text, ann.fontSize ?? 11, ann.fontWeight ?? 600);
|
|
16087
16531
|
const dx = ann.offset?.dx ?? 0;
|
|
16088
16532
|
const anchor = ann.anchor ?? "auto";
|
|
16089
16533
|
const baseRightExtent = anchor === "right" ? textWidth : anchor === "left" ? 0 : textWidth / 2;
|
|
@@ -16105,12 +16549,12 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
16105
16549
|
let maxLabelWidth = 0;
|
|
16106
16550
|
for (const row of spec.data) {
|
|
16107
16551
|
const label = String(row[yField] ?? "");
|
|
16108
|
-
let w =
|
|
16552
|
+
let w = estimateTextWidth21(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
|
|
16109
16553
|
if (yLabelField) {
|
|
16110
16554
|
const subtitle = String(row[yLabelField] ?? "");
|
|
16111
16555
|
if (subtitle) {
|
|
16112
16556
|
const gap = theme.fonts.sizes.axisTick * 0.6;
|
|
16113
|
-
const subtitleWidth =
|
|
16557
|
+
const subtitleWidth = estimateTextWidth21(
|
|
16114
16558
|
subtitle,
|
|
16115
16559
|
theme.fonts.sizes.axisTick,
|
|
16116
16560
|
theme.fonts.weights.normal
|
|
@@ -16147,7 +16591,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
16147
16591
|
}
|
|
16148
16592
|
const negPrefix = spec.data.some((r) => Number(r[yField]) < 0) ? "-" : "";
|
|
16149
16593
|
const labelEst = negPrefix + sampleLabel;
|
|
16150
|
-
const labelWidth =
|
|
16594
|
+
const labelWidth = estimateTextWidth21(
|
|
16151
16595
|
labelEst,
|
|
16152
16596
|
theme.fonts.sizes.axisTick,
|
|
16153
16597
|
theme.fonts.weights.normal
|
|
@@ -16178,7 +16622,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
16178
16622
|
})(maxAbsValForTitle);
|
|
16179
16623
|
}
|
|
16180
16624
|
const negPrefixForTitle = spec.data.some((r) => Number(r[yFieldForTitle]) < 0) ? "-" : "";
|
|
16181
|
-
estTickLabelWidth =
|
|
16625
|
+
estTickLabelWidth = estimateTextWidth21(
|
|
16182
16626
|
negPrefixForTitle + sampleLabelForTitle,
|
|
16183
16627
|
theme.fonts.sizes.axisTick,
|
|
16184
16628
|
theme.fonts.weights.normal
|
|
@@ -16439,7 +16883,7 @@ import {
|
|
|
16439
16883
|
buildD3Formatter as buildD3Formatter3,
|
|
16440
16884
|
computeChrome as computeChrome6,
|
|
16441
16885
|
defaultNumberFormatter as defaultNumberFormatter10,
|
|
16442
|
-
estimateTextWidth as
|
|
16886
|
+
estimateTextWidth as estimateTextWidth22,
|
|
16443
16887
|
resolveTheme as resolveTheme5
|
|
16444
16888
|
} from "@opendata-ai/openchart-core";
|
|
16445
16889
|
|
|
@@ -16452,21 +16896,19 @@ var ALIGN_MAP = {
|
|
|
16452
16896
|
center: sankeyCenter
|
|
16453
16897
|
};
|
|
16454
16898
|
function computeSankeyLayout(data, sourceField, targetField, valueField, area3, nodeWidth, nodePadding, nodeAlign, iterations, nodeSort) {
|
|
16899
|
+
const links = [];
|
|
16455
16900
|
const nodeSet = /* @__PURE__ */ new Set();
|
|
16456
16901
|
for (const row of data) {
|
|
16457
|
-
|
|
16458
|
-
|
|
16459
|
-
|
|
16460
|
-
|
|
16461
|
-
|
|
16462
|
-
|
|
16463
|
-
|
|
16464
|
-
|
|
16465
|
-
|
|
16466
|
-
|
|
16467
|
-
value: Number(row[valueField]) || 0,
|
|
16468
|
-
data: { ...row }
|
|
16469
|
-
}));
|
|
16902
|
+
const value = finiteNumber(row[valueField]);
|
|
16903
|
+
if (value === void 0 || value <= 0) continue;
|
|
16904
|
+
const source = String(row[sourceField]);
|
|
16905
|
+
const target = String(row[targetField]);
|
|
16906
|
+
nodeSet.add(source);
|
|
16907
|
+
nodeSet.add(target);
|
|
16908
|
+
links.push({ source, target, value, data: { ...row } });
|
|
16909
|
+
}
|
|
16910
|
+
if (links.length === 0) return { nodes: [], links: [] };
|
|
16911
|
+
const nodes = [...nodeSet].map((id) => ({ id, label: id }));
|
|
16470
16912
|
const alignFn = ALIGN_MAP[nodeAlign] ?? sankeyJustify;
|
|
16471
16913
|
const generator = sankey().nodeId((d) => d.id).nodeAlign(alignFn).nodeWidth(nodeWidth).nodePadding(nodePadding).extent([
|
|
16472
16914
|
[area3.x, area3.y],
|
|
@@ -16596,10 +17038,11 @@ function rewriteRowsForOther(data, sourceField, targetField, valueField, merged)
|
|
|
16596
17038
|
const newTgt = merged.get(tgt) ?? tgt;
|
|
16597
17039
|
if (newSrc === newTgt) continue;
|
|
16598
17040
|
const key = `${newSrc}\0${newTgt}`;
|
|
16599
|
-
const value =
|
|
17041
|
+
const value = finiteNumber(row[valueField]);
|
|
17042
|
+
if (value === void 0 || value <= 0) continue;
|
|
16600
17043
|
const existing = index.get(key);
|
|
16601
17044
|
if (existing) {
|
|
16602
|
-
existing[valueField] = (
|
|
17045
|
+
existing[valueField] = (finiteNumber(existing[valueField]) ?? 0) + value;
|
|
16603
17046
|
continue;
|
|
16604
17047
|
}
|
|
16605
17048
|
const next = { ...row, [sourceField]: newSrc, [targetField]: newTgt, [valueField]: value };
|
|
@@ -16716,12 +17159,40 @@ function compileSankey(spec, options) {
|
|
|
16716
17159
|
const targetField = sankeySpec.encoding.target.field;
|
|
16717
17160
|
const valueField = sankeySpec.encoding.value.field;
|
|
16718
17161
|
const colorField = sankeySpec.encoding.color?.field;
|
|
17162
|
+
let missingFlowCount = 0;
|
|
17163
|
+
let negativeFlowCount = 0;
|
|
17164
|
+
let zeroFlowCount = 0;
|
|
17165
|
+
const measuredData = sankeySpec.data.filter((row) => {
|
|
17166
|
+
const value = finiteNumber(row[valueField]);
|
|
17167
|
+
if (value === void 0) missingFlowCount++;
|
|
17168
|
+
else if (value < 0) negativeFlowCount++;
|
|
17169
|
+
else if (value === 0) zeroFlowCount++;
|
|
17170
|
+
else return true;
|
|
17171
|
+
return false;
|
|
17172
|
+
});
|
|
17173
|
+
const omittedCount = missingFlowCount + negativeFlowCount + zeroFlowCount;
|
|
17174
|
+
if (omittedCount > 0) {
|
|
17175
|
+
const reasons = [
|
|
17176
|
+
missingFlowCount > 0 ? `${missingFlowCount} missing, nonnumeric, or nonfinite` : "",
|
|
17177
|
+
negativeFlowCount > 0 ? `${negativeFlowCount} negative` : "",
|
|
17178
|
+
zeroFlowCount > 0 ? `${zeroFlowCount} zero-width` : ""
|
|
17179
|
+
].filter(Boolean);
|
|
17180
|
+
emitSpecWarnings(
|
|
17181
|
+
[
|
|
17182
|
+
`Sankey omitted ${omittedCount} flow rows from "${valueField}" (${reasons.join(", ")}). Sankey geometry requires positive finite values. Supply measured positive amounts; zero flows have no drawable width.`
|
|
17183
|
+
],
|
|
17184
|
+
options.onWarn
|
|
17185
|
+
);
|
|
17186
|
+
}
|
|
17187
|
+
if (measuredData.length === 0) {
|
|
17188
|
+
return emptyLayout4(fullArea, chrome, theme, { ...options, height: grownHeight }, watermark);
|
|
17189
|
+
}
|
|
16719
17190
|
const flowFmt = resolveFieldFormatter({
|
|
16720
17191
|
surfaceFormat: resolvedValueFormat,
|
|
16721
|
-
values:
|
|
17192
|
+
values: measuredData.map((r) => r[valueField])
|
|
16722
17193
|
});
|
|
16723
17194
|
const tempNodeIds = /* @__PURE__ */ new Set();
|
|
16724
|
-
for (const row of
|
|
17195
|
+
for (const row of measuredData) {
|
|
16725
17196
|
tempNodeIds.add(String(row[sourceField]));
|
|
16726
17197
|
tempNodeIds.add(String(row[targetField]));
|
|
16727
17198
|
}
|
|
@@ -16729,14 +17200,14 @@ function compileSankey(spec, options) {
|
|
|
16729
17200
|
[...tempNodeIds].map((id) => ({ id })),
|
|
16730
17201
|
theme.colors.categorical,
|
|
16731
17202
|
colorField,
|
|
16732
|
-
|
|
17203
|
+
measuredData,
|
|
16733
17204
|
sourceField,
|
|
16734
17205
|
targetField
|
|
16735
17206
|
);
|
|
16736
17207
|
const legend = buildSankeyLegend(
|
|
16737
17208
|
tempColorMap,
|
|
16738
17209
|
colorField,
|
|
16739
|
-
|
|
17210
|
+
measuredData,
|
|
16740
17211
|
sourceField,
|
|
16741
17212
|
targetField,
|
|
16742
17213
|
theme,
|
|
@@ -16756,7 +17227,7 @@ function compileSankey(spec, options) {
|
|
|
16756
17227
|
const labelFontWeight = theme.fonts.weights.medium;
|
|
16757
17228
|
const valueFontWeight = theme.fonts.weights.normal;
|
|
16758
17229
|
const nodeLabelAlign = sankeySpec.nodeLabelAlign ?? "auto";
|
|
16759
|
-
let workingData =
|
|
17230
|
+
let workingData = measuredData;
|
|
16760
17231
|
const runLayout = (rect) => computeSankeyLayout(
|
|
16761
17232
|
workingData,
|
|
16762
17233
|
sourceField,
|
|
@@ -16798,8 +17269,8 @@ function compileSankey(spec, options) {
|
|
|
16798
17269
|
const nodeFmt = resolveNodeValueFormatter(nodes, resolvedValueFormat, flowFmt);
|
|
16799
17270
|
const labelBlockWidth = (node) => {
|
|
16800
17271
|
const name = node.label ?? node.id;
|
|
16801
|
-
const nameWidth =
|
|
16802
|
-
const valueWidth =
|
|
17272
|
+
const nameWidth = estimateTextWidth22(name, labelFontSize, labelFontWeight);
|
|
17273
|
+
const valueWidth = estimateTextWidth22(
|
|
16803
17274
|
formatFlowValue(node.value ?? 0, nodeFmt),
|
|
16804
17275
|
labelFontSize,
|
|
16805
17276
|
valueFontWeight
|
|
@@ -16873,7 +17344,7 @@ function compileSankey(spec, options) {
|
|
|
16873
17344
|
const nextColumn = columnX0.get(depth + 1);
|
|
16874
17345
|
if (nextColumn === void 0) return { text, style: valueStyle };
|
|
16875
17346
|
const available = nextColumn - LABEL_GAP3 - ((node.x1 ?? 0) + LABEL_GAP3);
|
|
16876
|
-
const needed =
|
|
17347
|
+
const needed = estimateTextWidth22(node.label ?? node.id, labelFontSize, labelFontWeight) + VALUE_GAP + estimateTextWidth22(text, labelFontSize, valueFontWeight);
|
|
16877
17348
|
return needed <= available ? { text, style: valueStyle } : void 0;
|
|
16878
17349
|
};
|
|
16879
17350
|
const nodeMarks = nodes.map((node) => {
|
|
@@ -17131,7 +17602,7 @@ function emptyLayout4(area3, chrome, theme, options, watermark) {
|
|
|
17131
17602
|
}
|
|
17132
17603
|
|
|
17133
17604
|
// src/tables/compile-table.ts
|
|
17134
|
-
import { computeChrome as computeChrome7, estimateTextWidth as
|
|
17605
|
+
import { computeChrome as computeChrome7, estimateTextWidth as estimateTextWidth23 } from "@opendata-ai/openchart-core";
|
|
17135
17606
|
|
|
17136
17607
|
// src/tables/bar-column.ts
|
|
17137
17608
|
var NEGATIVE_BAR_COLOR = "#c44e52";
|
|
@@ -17194,14 +17665,14 @@ import { adaptColorForDarkMode, findAccessibleColor as findAccessibleColor2 } fr
|
|
|
17194
17665
|
import { interpolateRgb as interpolateRgb2 } from "d3-interpolate";
|
|
17195
17666
|
|
|
17196
17667
|
// src/tables/utils.ts
|
|
17197
|
-
import { contrastRatio as
|
|
17668
|
+
import { contrastRatio as contrastRatio3, cssTokenDefault } from "@opendata-ai/openchart-core";
|
|
17198
17669
|
function resolveTableSurface(theme) {
|
|
17199
17670
|
return theme.colors.neutral.surface;
|
|
17200
17671
|
}
|
|
17201
17672
|
function accessibleTextColor(bg, theme) {
|
|
17202
17673
|
const own = theme.colors.text;
|
|
17203
17674
|
const inverse = cssTokenDefault("--oc-text", theme.isDark ? "light" : "dark");
|
|
17204
|
-
return
|
|
17675
|
+
return contrastRatio3(inverse, bg) > contrastRatio3(own, bg) ? inverse : own;
|
|
17205
17676
|
}
|
|
17206
17677
|
|
|
17207
17678
|
// src/tables/category-colors.ts
|
|
@@ -17321,7 +17792,7 @@ function formatValueForSearch(value, column) {
|
|
|
17321
17792
|
}
|
|
17322
17793
|
|
|
17323
17794
|
// src/tables/heatmap.ts
|
|
17324
|
-
import { adaptColorForDarkMode as adaptColorForDarkMode2, contrastRatio as
|
|
17795
|
+
import { adaptColorForDarkMode as adaptColorForDarkMode2, contrastRatio as contrastRatio4 } from "@opendata-ai/openchart-core";
|
|
17325
17796
|
import { interpolateRgb as interpolateRgb3 } from "d3-interpolate";
|
|
17326
17797
|
import { scaleSequential } from "d3-scale";
|
|
17327
17798
|
function interpolatorFromStops(stops) {
|
|
@@ -17378,7 +17849,7 @@ function computeHeatmapColors(data, column, theme, darkMode) {
|
|
|
17378
17849
|
const darkBg = resolveTableSurface(theme);
|
|
17379
17850
|
if (Array.isArray(config.palette)) {
|
|
17380
17851
|
stops = stops.map((c) => {
|
|
17381
|
-
const ratio =
|
|
17852
|
+
const ratio = contrastRatio4(c, lightBg);
|
|
17382
17853
|
if (ratio < 1.15) return darkBg;
|
|
17383
17854
|
return adaptColorForDarkMode2(c, lightBg, darkBg);
|
|
17384
17855
|
});
|
|
@@ -17604,13 +18075,13 @@ function estimateColumnWidth(col, data, fontSize, paddingX) {
|
|
|
17604
18075
|
if (col.image) return (col.image.width ?? 24) + paddingX;
|
|
17605
18076
|
if (col.flag) return 60;
|
|
17606
18077
|
const label = col.label ?? col.key;
|
|
17607
|
-
const headerWidth =
|
|
18078
|
+
const headerWidth = estimateTextWidth23(label, fontSize, 600) + paddingX;
|
|
17608
18079
|
const sampleSize = Math.min(100, data.length);
|
|
17609
18080
|
let maxDataWidth = 0;
|
|
17610
18081
|
for (let i = 0; i < sampleSize; i++) {
|
|
17611
18082
|
const val = data[i][col.key];
|
|
17612
18083
|
const text = val == null ? "" : String(val);
|
|
17613
|
-
const width =
|
|
18084
|
+
const width = estimateTextWidth23(text, fontSize, 400) + paddingX;
|
|
17614
18085
|
if (width > maxDataWidth) maxDataWidth = width;
|
|
17615
18086
|
}
|
|
17616
18087
|
return Math.max(MIN_WIDTH, headerWidth, maxDataWidth);
|
|
@@ -17887,9 +18358,17 @@ function compileTableLayout(spec, options, theme) {
|
|
|
17887
18358
|
sparklineDomains.set(col.key, computeSparklineDomain(data, col.key, col.sparkline));
|
|
17888
18359
|
}
|
|
17889
18360
|
}
|
|
18361
|
+
const identities = spec.rowKey ? resolveObservationIdentities(data, spec.rowKey) : null;
|
|
18362
|
+
if (identities?.some((identity) => !identity.persistent))
|
|
18363
|
+
emitSpecWarnings(
|
|
18364
|
+
[
|
|
18365
|
+
"TABLE_ROW_KEY_AMBIGUOUS: rowKey contains duplicate or missing values; affected rows use revision-local identities."
|
|
18366
|
+
],
|
|
18367
|
+
options.onWarn
|
|
18368
|
+
);
|
|
17890
18369
|
const rows = currentData.map((row, i) => {
|
|
17891
18370
|
const origIdx = originalIndices[i];
|
|
17892
|
-
const rowId =
|
|
18371
|
+
const rowId = identities?.[origIdx].observation ?? String(origIdx);
|
|
17893
18372
|
const cells = spec.columns.map((col, c) => {
|
|
17894
18373
|
const resolved = resolvedColumns[c];
|
|
17895
18374
|
const value = row[col.key];
|
|
@@ -17970,13 +18449,13 @@ import {
|
|
|
17970
18449
|
adaptTheme as adaptTheme5,
|
|
17971
18450
|
CATEGORICAL_PALETTE as CATEGORICAL_PALETTE3,
|
|
17972
18451
|
computeChrome as computeChrome8,
|
|
17973
|
-
estimateTextWidth as
|
|
18452
|
+
estimateTextWidth as estimateTextWidth24,
|
|
17974
18453
|
getBreakpoint as getBreakpoint2,
|
|
17975
18454
|
HPAD_COMPACT_FRACTION as HPAD_COMPACT_FRACTION4,
|
|
17976
18455
|
HPAD_COMPACT_MIN as HPAD_COMPACT_MIN4,
|
|
17977
18456
|
pickLabelColor as pickLabelColor4,
|
|
17978
18457
|
resolveTheme as resolveTheme6,
|
|
17979
|
-
SEQUENTIAL_PALETTES as
|
|
18458
|
+
SEQUENTIAL_PALETTES as SEQUENTIAL_PALETTES2
|
|
17980
18459
|
} from "@opendata-ai/openchart-core";
|
|
17981
18460
|
import { interpolateRgb as interpolateRgb4 } from "d3-interpolate";
|
|
17982
18461
|
import { scaleLinear as scaleLinear6 } from "d3-scale";
|
|
@@ -18083,18 +18562,15 @@ function compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDa
|
|
|
18083
18562
|
const stateValueMap = /* @__PURE__ */ new Map();
|
|
18084
18563
|
for (const row of tilemapSpec.data) {
|
|
18085
18564
|
const stateCode = String(row[stateField]);
|
|
18086
|
-
const
|
|
18087
|
-
if (STATE_CODE_SET.has(stateCode) &&
|
|
18088
|
-
|
|
18089
|
-
if (!Number.isNaN(value)) {
|
|
18090
|
-
stateValueMap.set(stateCode, value);
|
|
18091
|
-
}
|
|
18565
|
+
const value = finiteNumber(row[valueField]);
|
|
18566
|
+
if (STATE_CODE_SET.has(stateCode) && value !== void 0) {
|
|
18567
|
+
stateValueMap.set(stateCode, value);
|
|
18092
18568
|
}
|
|
18093
18569
|
}
|
|
18094
18570
|
const values = Array.from(stateValueMap.values());
|
|
18095
18571
|
const min4 = values.length > 0 ? Math.min(...values) : 0;
|
|
18096
18572
|
const max4 = values.length > 0 ? Math.max(...values) : 100;
|
|
18097
|
-
const paletteStops = [...
|
|
18573
|
+
const paletteStops = [...SEQUENTIAL_PALETTES2[tilemapSpec.palette] ?? SEQUENTIAL_PALETTES2.blue];
|
|
18098
18574
|
const baseColor = isDarkMode ? paletteStops[0] : paletteStops[paletteStops.length - 1];
|
|
18099
18575
|
const opacityRange = isDarkMode ? [0.3, 1] : [0.25, 1];
|
|
18100
18576
|
const opacityScale = scaleLinear6().domain([min4, max4]).range(opacityRange).clamp(true);
|
|
@@ -18155,7 +18631,7 @@ function compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDa
|
|
|
18155
18631
|
});
|
|
18156
18632
|
const hasEmptyTiles = tiles.some((t) => !t.hasData);
|
|
18157
18633
|
const noDataLabel = "No data";
|
|
18158
|
-
const noDataLabelWidth =
|
|
18634
|
+
const noDataLabelWidth = estimateTextWidth24(noDataLabel, 11);
|
|
18159
18635
|
const noDataBlockWidth2 = hasEmptyTiles ? NO_DATA_BAR_GAP + legendBarHeight + NO_DATA_LABEL_GAP2 + noDataLabelWidth : 0;
|
|
18160
18636
|
const barWidth = Math.max(0, legendWidth - noDataBlockWidth2);
|
|
18161
18637
|
const noDataX = legendX + barWidth + NO_DATA_BAR_GAP;
|
|
@@ -18212,7 +18688,7 @@ function compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDa
|
|
|
18212
18688
|
height: contentHeight,
|
|
18213
18689
|
animation: resolvedAnimation,
|
|
18214
18690
|
watermark,
|
|
18215
|
-
measureText: options.measureText ?? ((text, fontSize) => ({ width:
|
|
18691
|
+
measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth24(text, fontSize), height: fontSize }))
|
|
18216
18692
|
};
|
|
18217
18693
|
}
|
|
18218
18694
|
function compileCategorical(tilemapSpec, options, theme, chrome, fullArea, isDarkMode, watermark, chromeLayout) {
|
|
@@ -18353,7 +18829,7 @@ function compileCategorical(tilemapSpec, options, theme, chrome, fullArea, isDar
|
|
|
18353
18829
|
height: contentHeight,
|
|
18354
18830
|
animation: resolvedAnimation,
|
|
18355
18831
|
watermark,
|
|
18356
|
-
measureText: options.measureText ?? ((text, fontSize) => ({ width:
|
|
18832
|
+
measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth24(text, fontSize), height: fontSize }))
|
|
18357
18833
|
};
|
|
18358
18834
|
}
|
|
18359
18835
|
function buildTileMark(opts) {
|
|
@@ -18479,7 +18955,7 @@ function countLegendRows(labels, availableWidth, fontSize) {
|
|
|
18479
18955
|
let rows = 1;
|
|
18480
18956
|
let offsetX = 0;
|
|
18481
18957
|
for (let i = 0; i < labels.length; i++) {
|
|
18482
|
-
const labelWidth =
|
|
18958
|
+
const labelWidth = estimateTextWidth24(labels[i], fontSize);
|
|
18483
18959
|
const entryWidth = LEGEND_SWATCH_SIZE + LEGEND_SWATCH_GAP + labelWidth + LEGEND_ENTRY_GAP;
|
|
18484
18960
|
if (i > 0 && offsetX + entryWidth > availableWidth) {
|
|
18485
18961
|
rows++;
|
|
@@ -18523,7 +18999,7 @@ function emptyLayout5(chrome, theme, options, watermark) {
|
|
|
18523
18999
|
height: options.height,
|
|
18524
19000
|
watermark,
|
|
18525
19001
|
animation: void 0,
|
|
18526
|
-
measureText: options.measureText ?? ((text, fontSize) => ({ width:
|
|
19002
|
+
measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth24(text, fontSize), height: fontSize }))
|
|
18527
19003
|
};
|
|
18528
19004
|
}
|
|
18529
19005
|
|
|
@@ -19169,8 +19645,8 @@ function runBin(data, transform) {
|
|
|
19169
19645
|
let min4 = Infinity;
|
|
19170
19646
|
let max4 = -Infinity;
|
|
19171
19647
|
for (const row of data) {
|
|
19172
|
-
const v =
|
|
19173
|
-
if (
|
|
19648
|
+
const v = finiteNumber(row[field]);
|
|
19649
|
+
if (v !== void 0) {
|
|
19174
19650
|
if (params.scaleType === "log" && v <= 0) continue;
|
|
19175
19651
|
if (v < min4) min4 = v;
|
|
19176
19652
|
if (v > max4) max4 = v;
|
|
@@ -19182,9 +19658,9 @@ function runBin(data, transform) {
|
|
|
19182
19658
|
if (params.scaleType === "log") {
|
|
19183
19659
|
const edges = computeLogBinEdges(extent3, maxbins);
|
|
19184
19660
|
return data.map((row) => {
|
|
19185
|
-
const v =
|
|
19661
|
+
const v = finiteNumber(row[field]);
|
|
19186
19662
|
const newRow = { ...row };
|
|
19187
|
-
if (
|
|
19663
|
+
if (v === void 0 || v <= 0) {
|
|
19188
19664
|
newRow[startAs] = null;
|
|
19189
19665
|
if (endAs) newRow[endAs] = null;
|
|
19190
19666
|
} else {
|
|
@@ -19204,9 +19680,9 @@ function runBin(data, transform) {
|
|
|
19204
19680
|
}
|
|
19205
19681
|
const step = params.step ?? computeStep(extent3, maxbins, nice);
|
|
19206
19682
|
return data.map((row) => {
|
|
19207
|
-
const v =
|
|
19683
|
+
const v = finiteNumber(row[field]);
|
|
19208
19684
|
const newRow = { ...row };
|
|
19209
|
-
if (
|
|
19685
|
+
if (v === void 0) {
|
|
19210
19686
|
newRow[startAs] = null;
|
|
19211
19687
|
if (endAs) newRow[endAs] = null;
|
|
19212
19688
|
} else {
|
|
@@ -19288,7 +19764,7 @@ function groupRows(data, groupby) {
|
|
|
19288
19764
|
if (groupby.length === 0) return /* @__PURE__ */ new Map([["", data]]);
|
|
19289
19765
|
const groups = /* @__PURE__ */ new Map();
|
|
19290
19766
|
for (const row of data) {
|
|
19291
|
-
const key =
|
|
19767
|
+
const key = groupKey(row, groupby);
|
|
19292
19768
|
const bucket = groups.get(key);
|
|
19293
19769
|
if (bucket) bucket.push(row);
|
|
19294
19770
|
else groups.set(key, [row]);
|
|
@@ -19308,10 +19784,8 @@ function runDensity(data, transform) {
|
|
|
19308
19784
|
for (const rows of groups.values()) {
|
|
19309
19785
|
const samples = [];
|
|
19310
19786
|
for (const r of rows) {
|
|
19311
|
-
const
|
|
19312
|
-
if (
|
|
19313
|
-
const v = Number(raw);
|
|
19314
|
-
if (Number.isFinite(v)) samples.push(v);
|
|
19787
|
+
const value = finiteNumber(r[field]);
|
|
19788
|
+
if (value !== void 0) samples.push(value);
|
|
19315
19789
|
}
|
|
19316
19790
|
if (samples.length === 0) continue;
|
|
19317
19791
|
const bandwidth = transform.bandwidth && transform.bandwidth > 0 ? transform.bandwidth : silvermanBandwidth(samples);
|
|
@@ -19483,14 +19957,11 @@ function runFold(data, transform) {
|
|
|
19483
19957
|
}
|
|
19484
19958
|
|
|
19485
19959
|
// src/transforms/joinaggregate.ts
|
|
19486
|
-
function groupKey2(row, groupby) {
|
|
19487
|
-
return groupby.map((f) => String(row[f] ?? "")).join("\0");
|
|
19488
|
-
}
|
|
19489
19960
|
function runJoinAggregate(data, transform) {
|
|
19490
19961
|
const groupby = transform.groupby ?? [];
|
|
19491
19962
|
const groups = /* @__PURE__ */ new Map();
|
|
19492
19963
|
for (const row of data) {
|
|
19493
|
-
const key =
|
|
19964
|
+
const key = groupKey(row, groupby);
|
|
19494
19965
|
const bucket = groups.get(key);
|
|
19495
19966
|
if (bucket) bucket.push(row);
|
|
19496
19967
|
else groups.set(key, [row]);
|
|
@@ -19499,12 +19970,11 @@ function runJoinAggregate(data, transform) {
|
|
|
19499
19970
|
for (const [key, rows] of groups) {
|
|
19500
19971
|
const summary = {};
|
|
19501
19972
|
for (const { op, field, as } of transform.joinaggregate) {
|
|
19502
|
-
|
|
19503
|
-
summary[as] = computeAggregate(op, values);
|
|
19973
|
+
summary[as] = aggregateRows(op, field, rows);
|
|
19504
19974
|
}
|
|
19505
19975
|
summaries.set(key, summary);
|
|
19506
19976
|
}
|
|
19507
|
-
return data.map((row) => ({ ...row, ...summaries.get(
|
|
19977
|
+
return data.map((row) => ({ ...row, ...summaries.get(groupKey(row, groupby)) }));
|
|
19508
19978
|
}
|
|
19509
19979
|
|
|
19510
19980
|
// src/transforms/timeunit.ts
|
|
@@ -19573,9 +20043,6 @@ function runTimeUnit(data, transform) {
|
|
|
19573
20043
|
}
|
|
19574
20044
|
|
|
19575
20045
|
// src/transforms/window.ts
|
|
19576
|
-
function groupKey3(row, groupby) {
|
|
19577
|
-
return groupby.map((f) => String(row[f] ?? "")).join("\0");
|
|
19578
|
-
}
|
|
19579
20046
|
function tryParseDate(val) {
|
|
19580
20047
|
if (val == null) return NaN;
|
|
19581
20048
|
if (typeof val === "number") return new Date(val).getTime();
|
|
@@ -19605,7 +20072,7 @@ function runWindow(data, transform) {
|
|
|
19605
20072
|
const indexed = data.map((row, i) => ({ row, originalIndex: i }));
|
|
19606
20073
|
const groups = /* @__PURE__ */ new Map();
|
|
19607
20074
|
for (const entry of indexed) {
|
|
19608
|
-
const key = groupby.length > 0 ?
|
|
20075
|
+
const key = groupby.length > 0 ? groupKey(entry.row, groupby) : "";
|
|
19609
20076
|
const existing = groups.get(key);
|
|
19610
20077
|
if (existing) {
|
|
19611
20078
|
existing.push(entry);
|
|
@@ -19642,26 +20109,25 @@ function runWindow(data, transform) {
|
|
|
19642
20109
|
case "diff": {
|
|
19643
20110
|
const lagIdx = i - offset;
|
|
19644
20111
|
if (lagIdx >= 0) {
|
|
19645
|
-
const current =
|
|
19646
|
-
const lagged =
|
|
19647
|
-
computed =
|
|
20112
|
+
const current = finiteNumber(entry.row[def.field]);
|
|
20113
|
+
const lagged = finiteNumber(sorted[lagIdx].row[def.field]);
|
|
20114
|
+
computed = current !== void 0 && lagged !== void 0 ? finiteNumber(current - lagged) ?? null : null;
|
|
19648
20115
|
}
|
|
19649
20116
|
break;
|
|
19650
20117
|
}
|
|
19651
20118
|
case "pct_change": {
|
|
19652
20119
|
const lagIdx = i - offset;
|
|
19653
20120
|
if (lagIdx >= 0) {
|
|
19654
|
-
const current =
|
|
19655
|
-
const lagged =
|
|
19656
|
-
if (
|
|
19657
|
-
computed = (current - lagged) / lagged;
|
|
20121
|
+
const current = finiteNumber(entry.row[def.field]);
|
|
20122
|
+
const lagged = finiteNumber(sorted[lagIdx].row[def.field]);
|
|
20123
|
+
if (current !== void 0 && lagged !== void 0 && lagged !== 0) {
|
|
20124
|
+
computed = finiteNumber((current - lagged) / lagged) ?? null;
|
|
19658
20125
|
}
|
|
19659
20126
|
}
|
|
19660
20127
|
break;
|
|
19661
20128
|
}
|
|
19662
20129
|
case "cumsum": {
|
|
19663
|
-
const
|
|
19664
|
-
const addend = Number.isFinite(val) ? val : 0;
|
|
20130
|
+
const addend = finiteNumber(entry.row[def.field]) ?? 0;
|
|
19665
20131
|
if (i === 0) {
|
|
19666
20132
|
computed = addend;
|
|
19667
20133
|
} else {
|
|
@@ -20108,6 +20574,7 @@ function compileChart(spec, optionsInput) {
|
|
|
20108
20574
|
const skipY = axisless && !chartSpec.userExplicit.yAxis;
|
|
20109
20575
|
const axes = isRadial ? { x: void 0, y: void 0 } : computeAxes(scales, chartArea, strategy, theme, options.measureText, {
|
|
20110
20576
|
data: renderSpec.data,
|
|
20577
|
+
preserveEnd: typeof spec.responsive === "object" ? spec.responsive.preserveEnd : false,
|
|
20111
20578
|
encoding: renderSpec.encoding,
|
|
20112
20579
|
skipX,
|
|
20113
20580
|
skipY,
|
|
@@ -20163,7 +20630,7 @@ function compileChart(spec, optionsInput) {
|
|
|
20163
20630
|
obstacles.push(...computeMarkObstacles(marks, scales));
|
|
20164
20631
|
for (const mark of marks) {
|
|
20165
20632
|
if ("label" in mark && mark.label?.visible) {
|
|
20166
|
-
obstacles.push({ ...
|
|
20633
|
+
obstacles.push({ ...computeLabelBounds2(mark.label), kind: "data-label" });
|
|
20167
20634
|
}
|
|
20168
20635
|
}
|
|
20169
20636
|
const watermarkRect = computeWatermarkObstacle(dims, watermark, axes, theme);
|
|
@@ -20222,14 +20689,61 @@ function compileChart(spec, optionsInput) {
|
|
|
20222
20689
|
const reserve = footnoteBandHeight(footnotes.length, theme);
|
|
20223
20690
|
const currentReserve = options.footnoteReserve ?? 0;
|
|
20224
20691
|
if (!options.frozenChartArea && reserve > currentReserve && (options.footnotePass ?? 0) < MAX_FOOTNOTE_PASSES) {
|
|
20692
|
+
const grow = (spec.chromeLayout ?? options.chromeLayout) === "grow";
|
|
20225
20693
|
return compileChart(spec, {
|
|
20226
20694
|
...optionsInput,
|
|
20695
|
+
height: optionsInput.height + (grow ? reserve - currentReserve : 0),
|
|
20227
20696
|
footnoteReserve: reserve,
|
|
20228
20697
|
footnotePass: (options.footnotePass ?? 0) + 1
|
|
20229
20698
|
});
|
|
20230
20699
|
}
|
|
20231
20700
|
}
|
|
20232
20701
|
}
|
|
20702
|
+
const responsive = spec.responsive;
|
|
20703
|
+
const wantsSummary = typeof responsive === "object" && responsive.labelSummary;
|
|
20704
|
+
const seriesSummary = wantsSummary && endpointLabels.entries.length === 0 ? computeEndpointLabels(chartSpec, marks, theme, chartArea, void 0, false).entries.map(
|
|
20705
|
+
(entry) => ({
|
|
20706
|
+
key: entry.seriesKey,
|
|
20707
|
+
text: `${entry.seriesKey} \xB7 ${entry.value}`,
|
|
20708
|
+
lines: wrapText3(
|
|
20709
|
+
`${entry.seriesKey} \xB7 ${entry.value}`,
|
|
20710
|
+
theme.fonts.sizes.small,
|
|
20711
|
+
500,
|
|
20712
|
+
Math.max(1, options.width - 2 * theme.spacing.padding),
|
|
20713
|
+
options.measureText
|
|
20714
|
+
).flatMap((line4) => {
|
|
20715
|
+
const max4 = Math.max(1, options.width - 2 * theme.spacing.padding);
|
|
20716
|
+
const chunks = [];
|
|
20717
|
+
let chunk = "";
|
|
20718
|
+
for (const character of line4) {
|
|
20719
|
+
const candidate = chunk + character;
|
|
20720
|
+
if (chunk && measure(candidate, theme.fonts.sizes.small, 500) > max4) {
|
|
20721
|
+
chunks.push(chunk);
|
|
20722
|
+
chunk = character;
|
|
20723
|
+
} else chunk = candidate;
|
|
20724
|
+
}
|
|
20725
|
+
if (chunk) chunks.push(chunk);
|
|
20726
|
+
return chunks;
|
|
20727
|
+
}),
|
|
20728
|
+
color: entry.color
|
|
20729
|
+
})
|
|
20730
|
+
) : [];
|
|
20731
|
+
if (seriesSummary.length) {
|
|
20732
|
+
const reserve = footnoteBandHeight(
|
|
20733
|
+
(footnotes?.length ?? 0) + seriesSummary.reduce((count, entry) => count + entry.lines.length, 0),
|
|
20734
|
+
theme
|
|
20735
|
+
);
|
|
20736
|
+
const previousReserve = options.footnoteReserve ?? 0;
|
|
20737
|
+
if (!options.frozenChartArea && reserve > previousReserve && (options.footnotePass ?? 0) < MAX_FOOTNOTE_PASSES) {
|
|
20738
|
+
const grow = (spec.chromeLayout ?? options.chromeLayout) === "grow";
|
|
20739
|
+
return compileChart(spec, {
|
|
20740
|
+
...optionsInput,
|
|
20741
|
+
height: optionsInput.height + (grow ? reserve - previousReserve : 0),
|
|
20742
|
+
footnoteReserve: reserve,
|
|
20743
|
+
footnotePass: (options.footnotePass ?? 0) + 1
|
|
20744
|
+
});
|
|
20745
|
+
}
|
|
20746
|
+
}
|
|
20233
20747
|
const tooltipDescriptors = computeTooltipDescriptors(chartSpec, marks);
|
|
20234
20748
|
const altText = chartSpec.a11y?.description ?? generateAltText(
|
|
20235
20749
|
{
|
|
@@ -20249,10 +20763,14 @@ function compileChart(spec, optionsInput) {
|
|
|
20249
20763
|
chartSpec.data
|
|
20250
20764
|
);
|
|
20251
20765
|
assignAnimationIndices(marks, resolvedAnimation);
|
|
20252
|
-
const chrome =
|
|
20766
|
+
const chrome = {
|
|
20767
|
+
...dims.chrome,
|
|
20768
|
+
...footnotes ? { footnotes } : {},
|
|
20769
|
+
...seriesSummary.length ? { seriesSummary } : {}
|
|
20770
|
+
};
|
|
20253
20771
|
const xInvert = resolveScaleInvert(scales.x);
|
|
20254
20772
|
const yInvert = resolveScaleInvert(scales.y);
|
|
20255
|
-
|
|
20773
|
+
const layout = {
|
|
20256
20774
|
area: chartArea,
|
|
20257
20775
|
chrome,
|
|
20258
20776
|
metrics: dims.metrics,
|
|
@@ -20293,6 +20811,18 @@ function compileChart(spec, optionsInput) {
|
|
|
20293
20811
|
...yInvert ? { yInvert } : {},
|
|
20294
20812
|
...markRenderMode === "canvas" ? { markRenderMode } : {}
|
|
20295
20813
|
};
|
|
20814
|
+
if (options.onDiagnostic) {
|
|
20815
|
+
const diagnosticSpec = {
|
|
20816
|
+
...expandedSpec,
|
|
20817
|
+
data: chartSpec.data,
|
|
20818
|
+
encoding: chartSpec.encoding
|
|
20819
|
+
};
|
|
20820
|
+
for (const diagnostic of diagnoseChart(diagnosticSpec, layout)) {
|
|
20821
|
+
if (diagnostic.fix) diagnostic.fix.precondition = JSON.stringify(spec);
|
|
20822
|
+
options.onDiagnostic(diagnostic);
|
|
20823
|
+
}
|
|
20824
|
+
}
|
|
20825
|
+
return layout;
|
|
20296
20826
|
}
|
|
20297
20827
|
function compileFaceted(chartSpec, renderSpec, facetChannel, facetDirection, options, theme, strategy, watermark, resolvedAnimation, crosshair, rawSpec, optionsInput) {
|
|
20298
20828
|
const measure = createMeasureFn(options.measureText);
|
|
@@ -20629,6 +21159,176 @@ function compileGeoMap2(spec, options) {
|
|
|
20629
21159
|
function compileBarList2(spec, options) {
|
|
20630
21160
|
return compileBarList(spec, options);
|
|
20631
21161
|
}
|
|
21162
|
+
|
|
21163
|
+
// src/design/patch.ts
|
|
21164
|
+
var unsafe = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
21165
|
+
function applySpecPatch(spec, patch) {
|
|
21166
|
+
if (JSON.stringify(spec) !== patch.precondition)
|
|
21167
|
+
throw new Error("This suggestion is stale. Review the current document again.");
|
|
21168
|
+
const copy = structuredClone(spec);
|
|
21169
|
+
for (const change of patch.changes) {
|
|
21170
|
+
if (!change.path.startsWith("/")) throw new Error("Patch paths must be JSON pointers.");
|
|
21171
|
+
const keys = change.path.slice(1).split("/").map((key) => key.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
21172
|
+
if (keys.some((key) => unsafe.has(key) || key.length === 0))
|
|
21173
|
+
throw new Error("Unsafe patch path.");
|
|
21174
|
+
let parent = copy;
|
|
21175
|
+
for (const key of keys.slice(0, -1)) {
|
|
21176
|
+
if (parent[key] === void 0) parent[key] = {};
|
|
21177
|
+
if (!parent[key] || typeof parent[key] !== "object")
|
|
21178
|
+
throw new Error("Patch path is no longer an object.");
|
|
21179
|
+
parent = parent[key];
|
|
21180
|
+
}
|
|
21181
|
+
parent[keys[keys.length - 1]] = structuredClone(change.value);
|
|
21182
|
+
}
|
|
21183
|
+
return copy;
|
|
21184
|
+
}
|
|
21185
|
+
|
|
21186
|
+
// src/design/recommend.ts
|
|
21187
|
+
function recommendCompositions(data, question, roles) {
|
|
21188
|
+
if (!data.length)
|
|
21189
|
+
return {
|
|
21190
|
+
recommendations: [],
|
|
21191
|
+
missingRoles: ["data"],
|
|
21192
|
+
explanation: "Add observations before choosing a composition."
|
|
21193
|
+
};
|
|
21194
|
+
const required = {
|
|
21195
|
+
comparison: ["measure", "category"],
|
|
21196
|
+
change: ["measure", "time"],
|
|
21197
|
+
distribution: ["measure"],
|
|
21198
|
+
composition: ["measure", "category"],
|
|
21199
|
+
geography: ["measure", "category"],
|
|
21200
|
+
relationships: ["measure", "secondMeasure"]
|
|
21201
|
+
};
|
|
21202
|
+
const missingRoles = required[question].filter(
|
|
21203
|
+
(role) => !roles[role] || !data.some((row) => Object.hasOwn(row, roles[role]))
|
|
21204
|
+
);
|
|
21205
|
+
if (missingRoles.length) return { recommendations: [], missingRoles };
|
|
21206
|
+
if (question === "geography")
|
|
21207
|
+
return {
|
|
21208
|
+
recommendations: [],
|
|
21209
|
+
missingRoles: ["geographic features and join key"],
|
|
21210
|
+
explanation: "Choose a geography and explicit join before creating a map."
|
|
21211
|
+
};
|
|
21212
|
+
for (const field of [
|
|
21213
|
+
roles.measure,
|
|
21214
|
+
...question === "relationships" ? [roles.secondMeasure] : []
|
|
21215
|
+
]) {
|
|
21216
|
+
const present = data.map((row) => row[field]).filter((value) => value != null);
|
|
21217
|
+
if (!present.length || present.some((value) => typeof value !== "number" || !Number.isFinite(value)))
|
|
21218
|
+
return {
|
|
21219
|
+
recommendations: [],
|
|
21220
|
+
missingRoles: ["numeric measure"],
|
|
21221
|
+
explanation: `Correct the type or values of \u201C${field}\u201D in Data before using it as a measure.`
|
|
21222
|
+
};
|
|
21223
|
+
}
|
|
21224
|
+
if (question === "change") {
|
|
21225
|
+
if (data.some(
|
|
21226
|
+
(row) => row[roles.time] == null || !Number.isFinite(Date.parse(String(row[roles.time])))
|
|
21227
|
+
))
|
|
21228
|
+
return {
|
|
21229
|
+
recommendations: [],
|
|
21230
|
+
missingRoles: ["valid date"],
|
|
21231
|
+
explanation: "Correct missing or invalid dates before connecting observations."
|
|
21232
|
+
};
|
|
21233
|
+
const keys = data.map(
|
|
21234
|
+
(row) => JSON.stringify([roles.series ? row[roles.series] : null, row[roles.time]])
|
|
21235
|
+
);
|
|
21236
|
+
if (new Set(keys).size !== keys.length)
|
|
21237
|
+
return {
|
|
21238
|
+
recommendations: [],
|
|
21239
|
+
missingRoles: ["unique date per series"],
|
|
21240
|
+
explanation: "Repeated dates need a series field or an explicit aggregation decision."
|
|
21241
|
+
};
|
|
21242
|
+
}
|
|
21243
|
+
const measure = { field: roles.measure, type: "quantitative" };
|
|
21244
|
+
const category = { field: roles.category, type: "nominal", sort: null };
|
|
21245
|
+
const recommendations = [];
|
|
21246
|
+
const add = (id, title, reason, tradeoff, spec) => recommendations.push({ id, title, reason, tradeoff, spec });
|
|
21247
|
+
if (question === "comparison" || question === "composition") {
|
|
21248
|
+
const seen = new Set(data.map((row) => row[roles.category]));
|
|
21249
|
+
if (seen.size !== data.length)
|
|
21250
|
+
return {
|
|
21251
|
+
recommendations: [],
|
|
21252
|
+
missingRoles: ["aggregation or a unique category"],
|
|
21253
|
+
explanation: "Repeated categories need an explicit grouping or aggregation decision."
|
|
21254
|
+
};
|
|
21255
|
+
add(
|
|
21256
|
+
"bars",
|
|
21257
|
+
"Compare on a shared baseline",
|
|
21258
|
+
"Position and length make differences easy to judge.",
|
|
21259
|
+
"Long category lists need more vertical space. Input order is preserved.",
|
|
21260
|
+
{ mark: "bar", data, encoding: { x: measure, y: category } }
|
|
21261
|
+
);
|
|
21262
|
+
add(
|
|
21263
|
+
"dots",
|
|
21264
|
+
"A quieter dot comparison",
|
|
21265
|
+
"Points emphasize positions without a wall of filled bars.",
|
|
21266
|
+
"Small differences may need direct value labels.",
|
|
21267
|
+
{ mark: "point", data, encoding: { x: measure, y: category } }
|
|
21268
|
+
);
|
|
21269
|
+
if (question === "composition" && data.every(
|
|
21270
|
+
(row) => typeof row[roles.measure] === "number" && Number(row[roles.measure]) >= 0
|
|
21271
|
+
) && data.some((row) => Number(row[roles.measure]) > 0) && data.length <= 6) {
|
|
21272
|
+
add(
|
|
21273
|
+
"shares",
|
|
21274
|
+
"Show parts of a whole",
|
|
21275
|
+
"A donut makes the whole explicit when categories form a complete partition.",
|
|
21276
|
+
"Only use this if the categories are mutually exclusive parts of the same total; bars are more precise.",
|
|
21277
|
+
{
|
|
21278
|
+
mark: { type: "arc", innerRadius: 0.65 },
|
|
21279
|
+
data,
|
|
21280
|
+
encoding: { theta: measure, color: category }
|
|
21281
|
+
}
|
|
21282
|
+
);
|
|
21283
|
+
}
|
|
21284
|
+
} else if (question === "change") {
|
|
21285
|
+
const encoding = {
|
|
21286
|
+
x: { field: roles.time, type: "temporal" },
|
|
21287
|
+
y: measure,
|
|
21288
|
+
...roles.series ? { color: { field: roles.series, type: "nominal" } } : {}
|
|
21289
|
+
};
|
|
21290
|
+
add(
|
|
21291
|
+
"trend",
|
|
21292
|
+
"Trace change over time",
|
|
21293
|
+
"Connected observations make direction and reversals visible.",
|
|
21294
|
+
"Connections assume comparable measurements across dates; missing observations need explanation.",
|
|
21295
|
+
{ mark: "line", data, encoding }
|
|
21296
|
+
);
|
|
21297
|
+
if (roles.series)
|
|
21298
|
+
add(
|
|
21299
|
+
"small-multiples",
|
|
21300
|
+
"Give every series its own space",
|
|
21301
|
+
"Shared scales reduce crowding and retain comparison.",
|
|
21302
|
+
"Requires more height than an overlaid chart.",
|
|
21303
|
+
{
|
|
21304
|
+
mark: "line",
|
|
21305
|
+
data,
|
|
21306
|
+
encoding: { ...encoding, facet: { field: roles.series, type: "nominal" } }
|
|
21307
|
+
}
|
|
21308
|
+
);
|
|
21309
|
+
} else if (question === "distribution") {
|
|
21310
|
+
add(
|
|
21311
|
+
"distribution",
|
|
21312
|
+
"Show the distribution",
|
|
21313
|
+
"A histogram reveals spread, skew, and concentrations.",
|
|
21314
|
+
"Bin boundaries affect appearance. Review them before drawing conclusions.",
|
|
21315
|
+
{ mark: "histogram", data, encoding: { x: measure } }
|
|
21316
|
+
);
|
|
21317
|
+
} else {
|
|
21318
|
+
add(
|
|
21319
|
+
"relationship",
|
|
21320
|
+
"Inspect the relationship",
|
|
21321
|
+
"A scatterplot exposes clusters and unusual observations.",
|
|
21322
|
+
"Association does not establish causation; dense data may overlap.",
|
|
21323
|
+
{
|
|
21324
|
+
mark: "point",
|
|
21325
|
+
data,
|
|
21326
|
+
encoding: { x: measure, y: { field: roles.secondMeasure, type: "quantitative" } }
|
|
21327
|
+
}
|
|
21328
|
+
);
|
|
21329
|
+
}
|
|
21330
|
+
return { recommendations: recommendations.slice(0, 3), missingRoles: [] };
|
|
21331
|
+
}
|
|
20632
21332
|
export {
|
|
20633
21333
|
ARROWHEAD_LENGTH,
|
|
20634
21334
|
AUTO_CANVAS_THRESHOLD,
|
|
@@ -20639,12 +21339,14 @@ export {
|
|
|
20639
21339
|
EXIT_DEFAULTS,
|
|
20640
21340
|
MAX_3D_NODES,
|
|
20641
21341
|
UPDATE_DEFAULTS,
|
|
21342
|
+
applySpecPatch,
|
|
20642
21343
|
buildArcPath,
|
|
20643
21344
|
buildAreaPath,
|
|
20644
21345
|
buildEdgeTooltip,
|
|
20645
21346
|
buildLinePath,
|
|
20646
21347
|
clampStaggerDelay,
|
|
20647
21348
|
clearRenderers,
|
|
21349
|
+
compareObservations,
|
|
20648
21350
|
compile,
|
|
20649
21351
|
compileBarList2 as compileBarList,
|
|
20650
21352
|
compileChart,
|
|
@@ -20656,17 +21358,23 @@ export {
|
|
|
20656
21358
|
compileTileMap2 as compileTileMap,
|
|
20657
21359
|
computeArrowheadPoints,
|
|
20658
21360
|
dedupeKeys,
|
|
21361
|
+
diagnoseChart,
|
|
20659
21362
|
evaluatePredicate,
|
|
20660
21363
|
facetMinHeight,
|
|
20661
21364
|
getChartRenderer,
|
|
20662
21365
|
heuristicMeasure,
|
|
20663
21366
|
isConditionalValueDef,
|
|
20664
21367
|
legendGap,
|
|
21368
|
+
linkedEntityKeys,
|
|
20665
21369
|
normalizeSpec,
|
|
21370
|
+
observationKey,
|
|
20666
21371
|
parseAnnotationSpans,
|
|
21372
|
+
recommendCompositions,
|
|
21373
|
+
reconcileReaderSelection,
|
|
20667
21374
|
registerChartRenderer,
|
|
20668
21375
|
resolveAnimation,
|
|
20669
21376
|
resolveConditionalValue,
|
|
21377
|
+
resolveObservationIdentities,
|
|
20670
21378
|
runBin,
|
|
20671
21379
|
runCalculate,
|
|
20672
21380
|
runFilter,
|