@opendata-ai/openchart-engine 8.7.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 +38 -3
- package/dist/index.js +1120 -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
|
|
@@ -9108,6 +9341,35 @@ function validateTileMapSpec(spec, errors) {
|
|
|
9108
9341
|
});
|
|
9109
9342
|
}
|
|
9110
9343
|
}
|
|
9344
|
+
function validateGeoMapZoom(zoom, errors) {
|
|
9345
|
+
if (zoom === void 0 || typeof zoom === "boolean") return;
|
|
9346
|
+
if (typeof zoom !== "object" || zoom === null || Array.isArray(zoom)) {
|
|
9347
|
+
errors.push({
|
|
9348
|
+
message: "Spec error: geo.zoom must be a boolean or an object { maxZoom?, controls? }",
|
|
9349
|
+
path: "geo.zoom",
|
|
9350
|
+
code: "INVALID_TYPE",
|
|
9351
|
+
suggestion: "Use zoom: true, or zoom: { maxZoom: 8, controls: true }"
|
|
9352
|
+
});
|
|
9353
|
+
return;
|
|
9354
|
+
}
|
|
9355
|
+
const cfg = zoom;
|
|
9356
|
+
if (cfg.maxZoom !== void 0 && (typeof cfg.maxZoom !== "number" || !Number.isFinite(cfg.maxZoom) || cfg.maxZoom < 1)) {
|
|
9357
|
+
errors.push({
|
|
9358
|
+
message: "Spec error: geo.zoom.maxZoom must be a number >= 1",
|
|
9359
|
+
path: "geo.zoom.maxZoom",
|
|
9360
|
+
code: "INVALID_VALUE",
|
|
9361
|
+
suggestion: "maxZoom is a multiple of the full map, e.g. maxZoom: 8. The default is 12."
|
|
9362
|
+
});
|
|
9363
|
+
}
|
|
9364
|
+
if (cfg.controls !== void 0 && typeof cfg.controls !== "boolean") {
|
|
9365
|
+
errors.push({
|
|
9366
|
+
message: "Spec error: geo.zoom.controls must be a boolean",
|
|
9367
|
+
path: "geo.zoom.controls",
|
|
9368
|
+
code: "INVALID_TYPE",
|
|
9369
|
+
suggestion: "Use controls: false to hide the zoom buttons"
|
|
9370
|
+
});
|
|
9371
|
+
}
|
|
9372
|
+
}
|
|
9111
9373
|
function validateGeoMapSpec(spec, errors) {
|
|
9112
9374
|
if (!spec.geo || typeof spec.geo !== "object") {
|
|
9113
9375
|
errors.push({
|
|
@@ -9228,6 +9490,7 @@ function validateGeoMapSpec(spec, errors) {
|
|
|
9228
9490
|
});
|
|
9229
9491
|
}
|
|
9230
9492
|
}
|
|
9493
|
+
validateGeoMapZoom(geo.zoom, errors);
|
|
9231
9494
|
const encoding = spec.encoding;
|
|
9232
9495
|
const hasPoints = spec.points && typeof spec.points === "object" && !Array.isArray(spec.points);
|
|
9233
9496
|
const specData = spec.data;
|
|
@@ -9490,7 +9753,68 @@ function validateLayerSpec(spec, errors) {
|
|
|
9490
9753
|
}
|
|
9491
9754
|
function validateAnimation(spec, errors) {
|
|
9492
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
|
+
}
|
|
9493
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
|
+
}
|
|
9494
9818
|
const update = animation.update;
|
|
9495
9819
|
if (!update || typeof update !== "object" || Array.isArray(update)) return;
|
|
9496
9820
|
const maxMarks = update.maxMarks;
|
|
@@ -9504,6 +9828,60 @@ function validateAnimation(spec, errors) {
|
|
|
9504
9828
|
});
|
|
9505
9829
|
}
|
|
9506
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
|
+
}
|
|
9507
9885
|
function validateSpec(spec) {
|
|
9508
9886
|
const errors = [];
|
|
9509
9887
|
if (!spec || typeof spec !== "object" || Array.isArray(spec)) {
|
|
@@ -9520,6 +9898,31 @@ function validateSpec(spec) {
|
|
|
9520
9898
|
};
|
|
9521
9899
|
}
|
|
9522
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
|
+
}
|
|
9523
9926
|
const hasLayer = "layer" in obj && Array.isArray(obj.layer);
|
|
9524
9927
|
const hasMark = "mark" in obj;
|
|
9525
9928
|
const isTable = obj.type === "table";
|
|
@@ -9741,10 +10144,7 @@ function compileBarList(spec, options) {
|
|
|
9741
10144
|
const rowContentHeight = Math.max(barHeight, LABEL_FONT_SIZE7 * 1.4);
|
|
9742
10145
|
const rowHeight = rowContentHeight + DEFAULT_ROW_GAP;
|
|
9743
10146
|
const maxFittingRows = Math.max(1, Math.floor(fullArea.height / rowHeight));
|
|
9744
|
-
const validRows = barlistSpec.data.filter((row) =>
|
|
9745
|
-
const val = row[valueField];
|
|
9746
|
-
return val !== null && val !== void 0 && !Number.isNaN(Number(val));
|
|
9747
|
-
}).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));
|
|
9748
10148
|
if (validRows.length === 0) {
|
|
9749
10149
|
return emptyLayout2(chrome, theme, options, watermark);
|
|
9750
10150
|
}
|
|
@@ -10140,7 +10540,9 @@ import {
|
|
|
10140
10540
|
import {
|
|
10141
10541
|
computeFieldFormatContext as computeFieldFormatContext2,
|
|
10142
10542
|
defaultNumberFormatter as defaultNumberFormatter5,
|
|
10143
|
-
|
|
10543
|
+
estimateTextWidth as estimateTextWidth10,
|
|
10544
|
+
resolveNumberFormatter as resolveNumberFormatter2,
|
|
10545
|
+
resolveSchemeName as resolveSchemeName3
|
|
10144
10546
|
} from "@opendata-ai/openchart-core";
|
|
10145
10547
|
import { tickStep } from "d3-array";
|
|
10146
10548
|
import { scaleQuantile } from "d3-scale";
|
|
@@ -10179,10 +10581,8 @@ function sampleRampColors(colors, k) {
|
|
|
10179
10581
|
function numericFieldValues(spec, field) {
|
|
10180
10582
|
const out = [];
|
|
10181
10583
|
for (const row of spec.data) {
|
|
10182
|
-
const
|
|
10183
|
-
if (
|
|
10184
|
-
const num = typeof v === "number" ? v : Number(v);
|
|
10185
|
-
if (Number.isFinite(num)) out.push(num);
|
|
10584
|
+
const value = finiteNumber(row[field]);
|
|
10585
|
+
if (value !== void 0) out.push(value);
|
|
10186
10586
|
}
|
|
10187
10587
|
return out;
|
|
10188
10588
|
}
|
|
@@ -10205,6 +10605,10 @@ function isDivergingRamp(colors, theme) {
|
|
|
10205
10605
|
function resolveRampColors(channel, theme) {
|
|
10206
10606
|
const explicitRange = channel.scale?.range;
|
|
10207
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
|
+
}
|
|
10208
10612
|
return Object.values(theme.colors.sequential)[0] ?? theme.colors.categorical;
|
|
10209
10613
|
}
|
|
10210
10614
|
function resolveBarWidth(availableWidth) {
|
|
@@ -10326,6 +10730,22 @@ function computeContinuousLegendContentForChannel(values, channel, theme, availa
|
|
|
10326
10730
|
x: (i + 1) * binWidth,
|
|
10327
10731
|
anchor: "middle"
|
|
10328
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
|
+
}
|
|
10329
10749
|
return {
|
|
10330
10750
|
mode: "binned",
|
|
10331
10751
|
barWidth,
|
|
@@ -10333,6 +10753,7 @@ function computeContinuousLegendContentForChannel(values, channel, theme, availa
|
|
|
10333
10753
|
colorStops: [],
|
|
10334
10754
|
bins,
|
|
10335
10755
|
ticks: ticks2,
|
|
10756
|
+
labelRows: Math.max(1, rowEnds.length),
|
|
10336
10757
|
classScale: scale
|
|
10337
10758
|
};
|
|
10338
10759
|
}
|
|
@@ -10568,7 +10989,7 @@ import {
|
|
|
10568
10989
|
BREAKPOINT_COMPACT_MAX as BREAKPOINT_COMPACT_MAX2,
|
|
10569
10990
|
computeFieldFormatContext as computeFieldFormatContext5,
|
|
10570
10991
|
defaultNumberFormatter as defaultNumberFormatter7,
|
|
10571
|
-
estimateTextWidth as
|
|
10992
|
+
estimateTextWidth as estimateTextWidth18,
|
|
10572
10993
|
resolveNumberFormatter as resolveNumberFormatter5,
|
|
10573
10994
|
resolveTheme as resolveTheme2,
|
|
10574
10995
|
TICK_LABEL_OFFSET
|
|
@@ -10586,7 +11007,7 @@ import {
|
|
|
10586
11007
|
computeFieldFormatContext as computeFieldFormatContext4,
|
|
10587
11008
|
computeXAxisExtentFromLabels as computeXAxisExtentFromLabels2,
|
|
10588
11009
|
defaultNumberFormatter as defaultNumberFormatter6,
|
|
10589
|
-
estimateTextWidth as
|
|
11010
|
+
estimateTextWidth as estimateTextWidth17,
|
|
10590
11011
|
getHeightClass,
|
|
10591
11012
|
HPAD_COMPACT_FRACTION,
|
|
10592
11013
|
HPAD_COMPACT_MIN,
|
|
@@ -10603,7 +11024,7 @@ import {
|
|
|
10603
11024
|
} from "@opendata-ai/openchart-core";
|
|
10604
11025
|
|
|
10605
11026
|
// src/endpoint-labels/predict.ts
|
|
10606
|
-
import { estimateTextWidth as
|
|
11027
|
+
import { estimateTextWidth as estimateTextWidth11, wrapText as wrapText2 } from "@opendata-ai/openchart-core";
|
|
10607
11028
|
function predictEndpointLabelsWidth(spec, _theme) {
|
|
10608
11029
|
if (spec.endpointLabels === false) return 0;
|
|
10609
11030
|
if (spec.markType !== "line" && spec.markType !== "area") return 0;
|
|
@@ -10648,12 +11069,12 @@ function predictEndpointLabelsWidth(spec, _theme) {
|
|
|
10648
11069
|
const text = content ? composeEndpointText(content, name, sample) : name;
|
|
10649
11070
|
const lines = wrapText2(text, ENDPOINT_LABEL_FONT_SIZE, ENDPOINT_LABEL_FONT_WEIGHT, wrapWidth);
|
|
10650
11071
|
for (const line4 of lines) {
|
|
10651
|
-
const w =
|
|
11072
|
+
const w = estimateTextWidth11(line4, ENDPOINT_LABEL_FONT_SIZE, ENDPOINT_LABEL_FONT_WEIGHT);
|
|
10652
11073
|
if (w > maxWidth) maxWidth = w;
|
|
10653
11074
|
}
|
|
10654
11075
|
}
|
|
10655
11076
|
if (!content && sample) {
|
|
10656
|
-
const w =
|
|
11077
|
+
const w = estimateTextWidth11(sample, ENDPOINT_VALUE_FONT_SIZE, ENDPOINT_VALUE_FONT_WEIGHT);
|
|
10657
11078
|
if (w > maxWidth) maxWidth = w;
|
|
10658
11079
|
}
|
|
10659
11080
|
return maxWidth;
|
|
@@ -10670,7 +11091,7 @@ function isEndsBoth(spec) {
|
|
|
10670
11091
|
}
|
|
10671
11092
|
|
|
10672
11093
|
// src/legend/compute.ts
|
|
10673
|
-
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";
|
|
10674
11095
|
|
|
10675
11096
|
// src/layout/scales.ts
|
|
10676
11097
|
import { computeFieldFormatContext as computeFieldFormatContext3 } from "@opendata-ai/openchart-core";
|
|
@@ -10692,11 +11113,9 @@ import {
|
|
|
10692
11113
|
} from "d3-scale";
|
|
10693
11114
|
function numericFieldValues2(data, field) {
|
|
10694
11115
|
const result = [];
|
|
10695
|
-
for (const
|
|
10696
|
-
const
|
|
10697
|
-
if (
|
|
10698
|
-
const n = typeof v === "number" ? v : Number(v);
|
|
10699
|
-
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);
|
|
10700
11119
|
}
|
|
10701
11120
|
return result;
|
|
10702
11121
|
}
|
|
@@ -11096,8 +11515,8 @@ function computeScales(spec, chartArea, data) {
|
|
|
11096
11515
|
const sums = /* @__PURE__ */ new Map();
|
|
11097
11516
|
for (const row of data) {
|
|
11098
11517
|
const cat = String(row[yField] ?? "");
|
|
11099
|
-
const val =
|
|
11100
|
-
if (
|
|
11518
|
+
const val = finiteNumber(row[xField]);
|
|
11519
|
+
if (val !== void 0 && val > 0) {
|
|
11101
11520
|
sums.set(cat, (sums.get(cat) ?? 0) + val);
|
|
11102
11521
|
}
|
|
11103
11522
|
}
|
|
@@ -11118,8 +11537,8 @@ function computeScales(spec, chartArea, data) {
|
|
|
11118
11537
|
const sums = /* @__PURE__ */ new Map();
|
|
11119
11538
|
for (const row of data) {
|
|
11120
11539
|
const cat = String(row[yField] ?? "");
|
|
11121
|
-
const val =
|
|
11122
|
-
if (
|
|
11540
|
+
const val = finiteNumber(row[xField]);
|
|
11541
|
+
if (val !== void 0 && val > 0) {
|
|
11123
11542
|
sums.set(cat, (sums.get(cat) ?? 0) + val);
|
|
11124
11543
|
}
|
|
11125
11544
|
}
|
|
@@ -11183,8 +11602,8 @@ function computeScales(spec, chartArea, data) {
|
|
|
11183
11602
|
const sums = /* @__PURE__ */ new Map();
|
|
11184
11603
|
for (const row of data) {
|
|
11185
11604
|
const cat = String(row[xField] ?? "");
|
|
11186
|
-
const val =
|
|
11187
|
-
if (
|
|
11605
|
+
const val = finiteNumber(row[yField]);
|
|
11606
|
+
if (val !== void 0 && val > 0) {
|
|
11188
11607
|
sums.set(cat, (sums.get(cat) ?? 0) + val);
|
|
11189
11608
|
}
|
|
11190
11609
|
}
|
|
@@ -11205,8 +11624,8 @@ function computeScales(spec, chartArea, data) {
|
|
|
11205
11624
|
const sums = /* @__PURE__ */ new Map();
|
|
11206
11625
|
for (const row of data) {
|
|
11207
11626
|
const cat = String(row[xField] ?? "");
|
|
11208
|
-
const val =
|
|
11209
|
-
if (
|
|
11627
|
+
const val = finiteNumber(row[yField]);
|
|
11628
|
+
if (val !== void 0 && val > 0) {
|
|
11210
11629
|
sums.set(cat, (sums.get(cat) ?? 0) + val);
|
|
11211
11630
|
}
|
|
11212
11631
|
}
|
|
@@ -11260,7 +11679,7 @@ function computeScales(spec, chartArea, data) {
|
|
|
11260
11679
|
}
|
|
11261
11680
|
|
|
11262
11681
|
// src/legend/wrap.ts
|
|
11263
|
-
import { COMPACT_WIDTH, estimateTextWidth as
|
|
11682
|
+
import { COMPACT_WIDTH, estimateTextWidth as estimateTextWidth12 } from "@opendata-ai/openchart-core";
|
|
11264
11683
|
var SWATCH_SIZE = 12;
|
|
11265
11684
|
var SWATCH_GAP = 6;
|
|
11266
11685
|
var ENTRY_GAP = 16;
|
|
@@ -11274,7 +11693,7 @@ function measureLegendWrap(entries, maxWidth, labelStyle, maxRows, entryGap = EN
|
|
|
11274
11693
|
if (entries.length === 0) {
|
|
11275
11694
|
return { rowCount: 0, fittingCount: 0, rowWidths: [], placements: [] };
|
|
11276
11695
|
}
|
|
11277
|
-
const measureWidth = measure ??
|
|
11696
|
+
const measureWidth = measure ?? estimateTextWidth12;
|
|
11278
11697
|
let rowCount = 1;
|
|
11279
11698
|
let rowWidth = 0;
|
|
11280
11699
|
const rowWidths = [];
|
|
@@ -11420,7 +11839,7 @@ function emptyContent(position, theme) {
|
|
|
11420
11839
|
};
|
|
11421
11840
|
}
|
|
11422
11841
|
function computeLegendContent(spec, strategy, theme, availableWidth, availableHeight, watermark = true, measure, endpointLabelsDemoted) {
|
|
11423
|
-
const measureWidth = measure ??
|
|
11842
|
+
const measureWidth = measure ?? estimateTextWidth13;
|
|
11424
11843
|
const sparklineHidden = spec.display === "sparkline" && !spec.userExplicit.legend;
|
|
11425
11844
|
if (sparklineHidden || spec.legend?.show === false || strategy.legendMaxHeight === 0) {
|
|
11426
11845
|
return emptyContent("top", theme);
|
|
@@ -11441,7 +11860,7 @@ function computeLegendContent(spec, strategy, theme, availableWidth, availableHe
|
|
|
11441
11860
|
fontVariant: "tabular-nums"
|
|
11442
11861
|
};
|
|
11443
11862
|
const labelRowHeight = Math.ceil(labelStyle2.fontSize * labelStyle2.lineHeight);
|
|
11444
|
-
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);
|
|
11445
11864
|
const blockWidth = continuous.blockWidth ?? continuous.barWidth;
|
|
11446
11865
|
return {
|
|
11447
11866
|
entries: [],
|
|
@@ -11698,7 +12117,7 @@ function placeLegend(content, chartArea, containerWidth, _theme, xAxisHeight, ax
|
|
|
11698
12117
|
import {
|
|
11699
12118
|
abbreviateNumber,
|
|
11700
12119
|
buildD3Formatter,
|
|
11701
|
-
estimateTextWidth as
|
|
12120
|
+
estimateTextWidth as estimateTextWidth14,
|
|
11702
12121
|
formatNumber as formatNumber3
|
|
11703
12122
|
} from "@opendata-ai/openchart-core";
|
|
11704
12123
|
import { scaleLinear as scaleLinear3, scaleSqrt as scaleSqrt3 } from "d3-scale";
|
|
@@ -11814,11 +12233,11 @@ function computeSizeLegendContent(sizeEncoding, data, theme, options) {
|
|
|
11814
12233
|
};
|
|
11815
12234
|
});
|
|
11816
12235
|
const widestLabel = Math.max(
|
|
11817
|
-
...circles.map((c) =>
|
|
12236
|
+
...circles.map((c) => estimateTextWidth14(c.label, fontSize, theme.fonts.weights.normal))
|
|
11818
12237
|
);
|
|
11819
12238
|
const lastLabelBottom = (circles[circles.length - 1]?.labelY ?? 0) + fontSize * 0.5;
|
|
11820
12239
|
const circleBlockWidth = maxRadius * 2 + LABEL_GAP + widestLabel;
|
|
11821
|
-
const titleWidth = title ?
|
|
12240
|
+
const titleWidth = title ? estimateTextWidth14(title, fontSize, theme.fonts.weights.medium) : 0;
|
|
11822
12241
|
return {
|
|
11823
12242
|
circles,
|
|
11824
12243
|
width: Math.max(circleBlockWidth, titleWidth),
|
|
@@ -11862,7 +12281,7 @@ function placeSizeLegend(content, chartArea, theme, colorLegend) {
|
|
|
11862
12281
|
import {
|
|
11863
12282
|
axisTitleOffset,
|
|
11864
12283
|
computeXAxisExtentFromLabels,
|
|
11865
|
-
estimateTextWidth as
|
|
12284
|
+
estimateTextWidth as estimateTextWidth16,
|
|
11866
12285
|
truncateRotatedLabel,
|
|
11867
12286
|
X_AXIS_TITLE_BAND,
|
|
11868
12287
|
X_AXIS_TITLE_BAND_ROTATED
|
|
@@ -11907,11 +12326,11 @@ function bandLabelStride(maxLabelWidth, angleDeg, fontSize, spacing) {
|
|
|
11907
12326
|
}
|
|
11908
12327
|
|
|
11909
12328
|
// src/layout/axes/thinning.ts
|
|
11910
|
-
import { estimateTextWidth as
|
|
12329
|
+
import { estimateTextWidth as estimateTextWidth15 } from "@opendata-ai/openchart-core";
|
|
11911
12330
|
var MIN_TICK_GAP_FACTOR = 0.5;
|
|
11912
12331
|
var MIN_TICK_COUNT = 2;
|
|
11913
12332
|
function measureLabel(text, fontSize, fontWeight, measureText) {
|
|
11914
|
-
return measureText ? measureText(text, fontSize, fontWeight).width :
|
|
12333
|
+
return measureText ? measureText(text, fontSize, fontWeight).width : estimateTextWidth15(text, fontSize, fontWeight);
|
|
11915
12334
|
}
|
|
11916
12335
|
function ticksOverlap(ticks, fontSize, fontWeight, measureText, orientation = "horizontal") {
|
|
11917
12336
|
if (ticks.length < 2) return false;
|
|
@@ -12388,6 +12807,35 @@ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContex
|
|
|
12388
12807
|
if (isContinuousX && !hasExplicitValues && dataContext?.maxXTicks != null) {
|
|
12389
12808
|
ticks = capTickCount(ticks, dataContext.maxXTicks);
|
|
12390
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
|
+
}
|
|
12391
12839
|
if (tickAngle && Math.abs(tickAngle) > 10) {
|
|
12392
12840
|
const measureWidth = (text, size, weight) => measureLabel(text, size, weight ?? fontWeight, measureText);
|
|
12393
12841
|
ticks = ticks.map((t) => {
|
|
@@ -12537,7 +12985,7 @@ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContex
|
|
|
12537
12985
|
const isRight = result.y.orient === "right";
|
|
12538
12986
|
if (isRight) {
|
|
12539
12987
|
const maxTickLabelWidth = result.y.ticks.reduce((max4, t) => {
|
|
12540
|
-
const w =
|
|
12988
|
+
const w = estimateTextWidth16(
|
|
12541
12989
|
t.label,
|
|
12542
12990
|
result.y.tickLabelStyle.fontSize,
|
|
12543
12991
|
result.y.tickLabelStyle.fontWeight ?? 400
|
|
@@ -12552,7 +13000,7 @@ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContex
|
|
|
12552
13000
|
};
|
|
12553
13001
|
} else {
|
|
12554
13002
|
const maxTickLabelWidth = result.y.ticks.reduce((max4, t) => {
|
|
12555
|
-
const w =
|
|
13003
|
+
const w = estimateTextWidth16(
|
|
12556
13004
|
t.label,
|
|
12557
13005
|
result.y.tickLabelStyle.fontSize,
|
|
12558
13006
|
result.y.tickLabelStyle.fontWeight ?? 400
|
|
@@ -12580,7 +13028,7 @@ function createMeasureFn(measureText) {
|
|
|
12580
13028
|
if (measureText) {
|
|
12581
13029
|
return (text, fontSize, fontWeight) => measureText(text, fontSize, fontWeight).width;
|
|
12582
13030
|
}
|
|
12583
|
-
return
|
|
13031
|
+
return estimateTextWidth17;
|
|
12584
13032
|
}
|
|
12585
13033
|
function sampleLabelFromContext(maxAbsVal, yAxisFormat, ctx) {
|
|
12586
13034
|
if (yAxisFormat) {
|
|
@@ -12945,6 +13393,19 @@ function resolveLayoutPlan(chartSpec, renderSpec, options, theme, strategy, wate
|
|
|
12945
13393
|
}
|
|
12946
13394
|
|
|
12947
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
|
+
}
|
|
12948
13409
|
function compileLayer(spec, options, compileChart2) {
|
|
12949
13410
|
const leaves = flattenLayers(spec);
|
|
12950
13411
|
if (leaves.length === 0) {
|
|
@@ -12952,7 +13413,11 @@ function compileLayer(spec, options, compileChart2) {
|
|
|
12952
13413
|
}
|
|
12953
13414
|
if (leaves.length === 1) {
|
|
12954
13415
|
const singleSpec = buildPrimarySpec(leaves, spec);
|
|
12955
|
-
|
|
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 };
|
|
12956
13421
|
}
|
|
12957
13422
|
const layerOptions = { ...options, layered: true };
|
|
12958
13423
|
if (spec.resolve?.scale?.y === "independent") {
|
|
@@ -12961,6 +13426,7 @@ function compileLayer(spec, options, compileChart2) {
|
|
|
12961
13426
|
const primarySpec = buildPrimarySpec(leaves, spec);
|
|
12962
13427
|
const primaryLayout = compileChart2(primarySpec, layerOptions);
|
|
12963
13428
|
const allMarks = [];
|
|
13429
|
+
const tooltipDescriptors = /* @__PURE__ */ new Map();
|
|
12964
13430
|
const seenLabels = /* @__PURE__ */ new Set();
|
|
12965
13431
|
const pLegend = primaryLayout.legend;
|
|
12966
13432
|
const mergedLegendEntries = "entries" in pLegend ? [...pLegend.entries] : [];
|
|
@@ -12969,19 +13435,20 @@ function compileLayer(spec, options, compileChart2) {
|
|
|
12969
13435
|
}
|
|
12970
13436
|
const indexedLeaves = leaves.map((leaf, i) => ({
|
|
12971
13437
|
leaf,
|
|
13438
|
+
leafIndex: i,
|
|
12972
13439
|
zIndex: leaf.zIndex ?? i
|
|
12973
13440
|
}));
|
|
12974
13441
|
indexedLeaves.sort((a, b) => a.zIndex - b.zIndex);
|
|
12975
13442
|
const leafOptions = { ...layerOptions, frozenChartArea: primaryLayout.area };
|
|
12976
13443
|
const sharedDomains = computeSharedDomains(leaves);
|
|
12977
|
-
for (const { leaf } of indexedLeaves) {
|
|
13444
|
+
for (const { leaf, leafIndex } of indexedLeaves) {
|
|
12978
13445
|
const themedLeaf = {
|
|
12979
13446
|
...withSharedDomains(leaf, sharedDomains),
|
|
12980
13447
|
theme: leaf.theme ?? spec.theme,
|
|
12981
13448
|
darkMode: leaf.darkMode ?? spec.darkMode
|
|
12982
13449
|
};
|
|
12983
13450
|
const leafLayout = compileChart2(themedLeaf, leafOptions);
|
|
12984
|
-
|
|
13451
|
+
appendLayerMarks(leafLayout, leafIndex, allMarks, tooltipDescriptors);
|
|
12985
13452
|
const leafLeg = leafLayout.legend;
|
|
12986
13453
|
if ("entries" in leafLeg) {
|
|
12987
13454
|
for (const entry of leafLeg.entries) {
|
|
@@ -12999,6 +13466,7 @@ function compileLayer(spec, options, compileChart2) {
|
|
|
12999
13466
|
return {
|
|
13000
13467
|
...primaryLayout,
|
|
13001
13468
|
marks: allMarks,
|
|
13469
|
+
tooltipDescriptors,
|
|
13002
13470
|
legend: mergedLegend,
|
|
13003
13471
|
// `legends` is what the renderer iterates, so the merged color legend has to
|
|
13004
13472
|
// land in it too. Spreading `primaryLayout` alone would leave legends[0] as
|
|
@@ -13012,7 +13480,7 @@ function estimateYAxisLabelWidth(data, encoding, baseFontSize, measure) {
|
|
|
13012
13480
|
const yEnc = encoding.y;
|
|
13013
13481
|
const yField = yEnc.field;
|
|
13014
13482
|
if (!yField) return 40;
|
|
13015
|
-
const m = measure ??
|
|
13483
|
+
const m = measure ?? estimateTextWidth18;
|
|
13016
13484
|
const yType = yEnc.type;
|
|
13017
13485
|
if (yType === "nominal" || yType === "ordinal") {
|
|
13018
13486
|
let maxWidth = 0;
|
|
@@ -13202,20 +13670,17 @@ function compileLayerIndependent(leaves, layerSpec, options, compileChart2) {
|
|
|
13202
13670
|
mergedLegendEntries.push(entry);
|
|
13203
13671
|
}
|
|
13204
13672
|
}
|
|
13205
|
-
const l0Count = layout0.marks.length;
|
|
13206
|
-
const mergedTooltips = new Map(layout0.tooltipDescriptors);
|
|
13207
|
-
for (const [key, value] of layout1.tooltipDescriptors) {
|
|
13208
|
-
const match = /^(rect|point|arc)-(\d+)$/.exec(key);
|
|
13209
|
-
if (match) {
|
|
13210
|
-
const offsetKey = `${match[1]}-${Number(match[2]) + l0Count}`;
|
|
13211
|
-
mergedTooltips.set(offsetKey, value);
|
|
13212
|
-
} else {
|
|
13213
|
-
mergedTooltips.set(key, value);
|
|
13214
|
-
}
|
|
13215
|
-
}
|
|
13216
13673
|
const z0 = leaf0.zIndex ?? 0;
|
|
13217
13674
|
const z1 = leaf1.zIndex ?? 1;
|
|
13218
|
-
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
|
+
}
|
|
13219
13684
|
const mergedLegend = {
|
|
13220
13685
|
...layout0.legend,
|
|
13221
13686
|
..."entries" in l0Legend ? { entries: mergedLegendEntries } : {}
|
|
@@ -13282,8 +13747,8 @@ function withUnionXDomain(leaf0, leaf1) {
|
|
|
13282
13747
|
let min4 = Number.POSITIVE_INFINITY;
|
|
13283
13748
|
let max4 = Number.NEGATIVE_INFINITY;
|
|
13284
13749
|
for (const v of allValues) {
|
|
13285
|
-
const n =
|
|
13286
|
-
if (
|
|
13750
|
+
const n = finiteNumber(v);
|
|
13751
|
+
if (n === void 0) continue;
|
|
13287
13752
|
if (n < min4) min4 = n;
|
|
13288
13753
|
if (n > max4) max4 = n;
|
|
13289
13754
|
}
|
|
@@ -13314,8 +13779,8 @@ function alignYDomains(leaf0, leaf1) {
|
|
|
13314
13779
|
if (yEnc0.scale?.domain || yEnc1.scale?.domain) return void 0;
|
|
13315
13780
|
const includeZero0 = yEnc0.scale?.zero !== false;
|
|
13316
13781
|
const includeZero1 = yEnc1.scale?.zero !== false;
|
|
13317
|
-
const vals0 = leaf0.data.map((
|
|
13318
|
-
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);
|
|
13319
13784
|
if (vals0.length === 0 || vals1.length === 0) return void 0;
|
|
13320
13785
|
const niced = (vals, includeZero) => {
|
|
13321
13786
|
let lo = Math.min(...vals);
|
|
@@ -13386,8 +13851,8 @@ function computeSharedDomains(leaves) {
|
|
|
13386
13851
|
break;
|
|
13387
13852
|
}
|
|
13388
13853
|
for (const row of leaf.data ?? []) {
|
|
13389
|
-
const v =
|
|
13390
|
-
if (
|
|
13854
|
+
const v = finiteNumber(row[enc.field]);
|
|
13855
|
+
if (v === void 0) continue;
|
|
13391
13856
|
if (v < lo) lo = v;
|
|
13392
13857
|
if (v > hi) hi = v;
|
|
13393
13858
|
sawAny = true;
|
|
@@ -13544,7 +14009,7 @@ function resolveYouDrawIt(config, marks, scales, chartArea, xField) {
|
|
|
13544
14009
|
}
|
|
13545
14010
|
|
|
13546
14011
|
// src/compiler/a11y-warnings.ts
|
|
13547
|
-
import { contrastRatio, findAccessibleColor, isOpaqueColor } from "@opendata-ai/openchart-core";
|
|
14012
|
+
import { contrastRatio as contrastRatio2, findAccessibleColor, isOpaqueColor as isOpaqueColor2 } from "@opendata-ai/openchart-core";
|
|
13548
14013
|
var GRAPHIC_MIN = 3;
|
|
13549
14014
|
var TEXT_MIN = 4.5;
|
|
13550
14015
|
var ADJACENT_FILL_MARKS = /* @__PURE__ */ new Set(["bar", "area", "arc", "waffle"]);
|
|
@@ -13552,7 +14017,7 @@ var fmt = (ratio) => `${ratio.toFixed(2)}:1`;
|
|
|
13552
14017
|
function collectContrastWarnings(scales, markType, theme) {
|
|
13553
14018
|
const warnings = [];
|
|
13554
14019
|
const bg = theme.colors.background;
|
|
13555
|
-
const bgIsCheckable =
|
|
14020
|
+
const bgIsCheckable = isOpaqueColor2(bg);
|
|
13556
14021
|
const series = [];
|
|
13557
14022
|
if (scales.color && scales.color.type !== "sequential") {
|
|
13558
14023
|
const ordinal = scales.color.scale;
|
|
@@ -13564,7 +14029,7 @@ function collectContrastWarnings(scales, markType, theme) {
|
|
|
13564
14029
|
for (let i = 0; i < series.length - 1; i++) {
|
|
13565
14030
|
const a = series[i];
|
|
13566
14031
|
const b = series[i + 1];
|
|
13567
|
-
const ratio =
|
|
14032
|
+
const ratio = contrastRatio2(a.color, b.color);
|
|
13568
14033
|
if (ratio < GRAPHIC_MIN) {
|
|
13569
14034
|
const suggestion = findAccessibleColor(b.color, a.color, GRAPHIC_MIN);
|
|
13570
14035
|
warnings.push(
|
|
@@ -13575,7 +14040,7 @@ function collectContrastWarnings(scales, markType, theme) {
|
|
|
13575
14040
|
}
|
|
13576
14041
|
if (bgIsCheckable) {
|
|
13577
14042
|
for (const s of series) {
|
|
13578
|
-
const ratio =
|
|
14043
|
+
const ratio = contrastRatio2(s.color, bg);
|
|
13579
14044
|
if (ratio < GRAPHIC_MIN) {
|
|
13580
14045
|
const suggestion = findAccessibleColor(s.color, bg, GRAPHIC_MIN);
|
|
13581
14046
|
warnings.push(
|
|
@@ -13588,7 +14053,7 @@ function collectContrastWarnings(scales, markType, theme) {
|
|
|
13588
14053
|
{ name: "theme.colors.annotationText", color: theme.colors.annotationText }
|
|
13589
14054
|
];
|
|
13590
14055
|
for (const token of textTokens) {
|
|
13591
|
-
const ratio =
|
|
14056
|
+
const ratio = contrastRatio2(token.color, bg);
|
|
13592
14057
|
if (ratio < TEXT_MIN) {
|
|
13593
14058
|
const suggestion = findAccessibleColor(token.color, bg, TEXT_MIN);
|
|
13594
14059
|
warnings.push(
|
|
@@ -13703,16 +14168,14 @@ import {
|
|
|
13703
14168
|
buildD3Formatter as buildD3Formatter2,
|
|
13704
14169
|
CATEGORICAL_PALETTE as CATEGORICAL_PALETTE2,
|
|
13705
14170
|
computeChrome as computeChrome3,
|
|
13706
|
-
estimateTextWidth as
|
|
14171
|
+
estimateTextWidth as estimateTextWidth19,
|
|
13707
14172
|
formatNumber as formatNumber4,
|
|
13708
14173
|
getBreakpoint,
|
|
13709
14174
|
HPAD_COMPACT_FRACTION as HPAD_COMPACT_FRACTION2,
|
|
13710
14175
|
HPAD_COMPACT_MIN as HPAD_COMPACT_MIN2,
|
|
13711
|
-
resolveTheme as resolveTheme3
|
|
13712
|
-
SEQUENTIAL_PALETTES as SEQUENTIAL_PALETTES2
|
|
14176
|
+
resolveTheme as resolveTheme3
|
|
13713
14177
|
} from "@opendata-ai/openchart-core";
|
|
13714
14178
|
import { geoArea, geoPath } from "d3-geo";
|
|
13715
|
-
import { scaleQuantile as scaleQuantile3 } from "d3-scale";
|
|
13716
14179
|
import { feature as topoFeature, mesh as topoMesh } from "topojson-client";
|
|
13717
14180
|
|
|
13718
14181
|
// src/geo/join.ts
|
|
@@ -13746,25 +14209,50 @@ function suggestNormalization(unmatchedKeys, featureIds) {
|
|
|
13746
14209
|
function joinDataToFeatures(features, data, dataKeyField, featureIdField) {
|
|
13747
14210
|
const warnings = [];
|
|
13748
14211
|
const dataLookup = /* @__PURE__ */ new Map();
|
|
14212
|
+
const duplicates = /* @__PURE__ */ new Set();
|
|
14213
|
+
const lowerCounts = /* @__PURE__ */ new Map();
|
|
14214
|
+
let missingKeys = 0;
|
|
13749
14215
|
for (const row of data) {
|
|
13750
|
-
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);
|
|
13751
14223
|
dataLookup.set(key, row);
|
|
14224
|
+
lowerCounts.set(key.toLowerCase(), (lowerCounts.get(key.toLowerCase()) ?? 0) + 1);
|
|
13752
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();
|
|
13753
14239
|
const joined = /* @__PURE__ */ new Map();
|
|
13754
14240
|
const usedDataKeys = /* @__PURE__ */ new Set();
|
|
13755
14241
|
let lowerLookup = null;
|
|
13756
14242
|
let unmatchedFeatureCount = 0;
|
|
13757
14243
|
for (const feature of features) {
|
|
13758
14244
|
const featureId = featureIdField === "id" ? String(feature.id) : String(feature.properties[featureIdField] ?? feature.id);
|
|
13759
|
-
let match = dataLookup.get(featureId);
|
|
13760
|
-
if (!match) {
|
|
14245
|
+
let match = duplicates.has(featureId) ? void 0 : dataLookup.get(featureId);
|
|
14246
|
+
if (!match && !duplicates.has(featureId)) {
|
|
13761
14247
|
if (!lowerLookup) {
|
|
13762
14248
|
lowerLookup = /* @__PURE__ */ new Map();
|
|
13763
14249
|
for (const [key, row] of dataLookup) {
|
|
13764
|
-
lowerLookup.set(key.toLowerCase(), row);
|
|
14250
|
+
if (lowerCounts.get(key.toLowerCase()) === 1) lowerLookup.set(key.toLowerCase(), row);
|
|
13765
14251
|
}
|
|
13766
14252
|
}
|
|
13767
|
-
|
|
14253
|
+
const lower = featureId.toLowerCase();
|
|
14254
|
+
match = lowerLookup.get(lower);
|
|
14255
|
+
if (!match && (lowerCounts.get(lower) ?? 0) > 1) ambiguousFallbacks.add(featureId);
|
|
13768
14256
|
}
|
|
13769
14257
|
if (match) {
|
|
13770
14258
|
joined.set(feature.id, match);
|
|
@@ -13773,6 +14261,12 @@ function joinDataToFeatures(features, data, dataKeyField, featureIdField) {
|
|
|
13773
14261
|
unmatchedFeatureCount++;
|
|
13774
14262
|
}
|
|
13775
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
|
+
});
|
|
13776
14270
|
const unmatchedDataKeys = [];
|
|
13777
14271
|
for (const key of dataLookup.keys()) {
|
|
13778
14272
|
if (!usedDataKeys.has(key)) {
|
|
@@ -13918,10 +14412,10 @@ function categoricalLegendLabelStyle(theme) {
|
|
|
13918
14412
|
};
|
|
13919
14413
|
}
|
|
13920
14414
|
function categoricalEntryWidth(label, labelStyle) {
|
|
13921
|
-
return CATEGORICAL_SWATCH_SIZE + CATEGORICAL_SWATCH_GAP +
|
|
14415
|
+
return CATEGORICAL_SWATCH_SIZE + CATEGORICAL_SWATCH_GAP + estimateTextWidth19(label, labelStyle.fontSize, labelStyle.fontWeight) + CATEGORICAL_ENTRY_GAP;
|
|
13922
14416
|
}
|
|
13923
14417
|
function noDataBlockWidth(labelStyle) {
|
|
13924
|
-
return CATEGORICAL_SWATCH_SIZE + NO_DATA_LABEL_GAP +
|
|
14418
|
+
return CATEGORICAL_SWATCH_SIZE + NO_DATA_LABEL_GAP + estimateTextWidth19("No data", labelStyle.fontSize, labelStyle.fontWeight);
|
|
13925
14419
|
}
|
|
13926
14420
|
function wrapCategoricalEntries(widths, legendWidth) {
|
|
13927
14421
|
const placed = [];
|
|
@@ -14049,11 +14543,8 @@ function compileGeoMap(spec, options) {
|
|
|
14049
14543
|
if (colorEncoding && isQuantitative) {
|
|
14050
14544
|
const colorValues = [];
|
|
14051
14545
|
for (const row of mapSpec.data) {
|
|
14052
|
-
const
|
|
14053
|
-
if (
|
|
14054
|
-
const n = Number(v);
|
|
14055
|
-
if (!Number.isNaN(n)) colorValues.push(n);
|
|
14056
|
-
}
|
|
14546
|
+
const value = finiteNumber(row[colorEncoding.field]);
|
|
14547
|
+
if (value !== void 0) colorValues.push(value);
|
|
14057
14548
|
}
|
|
14058
14549
|
const legendChannel = {
|
|
14059
14550
|
...colorEncoding,
|
|
@@ -14075,7 +14566,7 @@ function compileGeoMap(spec, options) {
|
|
|
14075
14566
|
legendTitle = legendTitleFor(colorEncoding);
|
|
14076
14567
|
const labelRowHeight = Math.ceil(theme.fonts.sizes.small * 1.3);
|
|
14077
14568
|
const titleRowHeight = legendTitle ? labelRowHeight : 0;
|
|
14078
|
-
legendBlockHeight = titleRowHeight + continuousContent.barHeight + CONTINUOUS_LABEL_GAP + labelRowHeight;
|
|
14569
|
+
legendBlockHeight = titleRowHeight + continuousContent.barHeight + CONTINUOUS_LABEL_GAP + labelRowHeight * (continuousContent.labelRows ?? 1);
|
|
14079
14570
|
}
|
|
14080
14571
|
}
|
|
14081
14572
|
if (legendBlockHeight === 0 && showChoroplethLegend && colorEncoding && !isQuantitative) {
|
|
@@ -14091,10 +14582,24 @@ function compileGeoMap(spec, options) {
|
|
|
14091
14582
|
const rows = placed.length > 0 ? placed[placed.length - 1].row + 1 : 1;
|
|
14092
14583
|
legendBlockHeight = rows * (Math.max(10, labelHeight) + 6);
|
|
14093
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;
|
|
14094
14599
|
if (showLegend && !pointLegendOverlay && mapSpec.points?.color) {
|
|
14095
14600
|
if (mapSpec.points.color.type === "quantitative") {
|
|
14096
14601
|
const labelRowHeight = Math.ceil(theme.fonts.sizes.small * 1.3);
|
|
14097
|
-
legendBlockHeight += CONTINUOUS_BAR_HEIGHT + CONTINUOUS_LABEL_GAP + labelRowHeight + 8;
|
|
14602
|
+
legendBlockHeight += (pointContinuousContent?.barHeight ?? CONTINUOUS_BAR_HEIGHT) + CONTINUOUS_LABEL_GAP + labelRowHeight * (pointContinuousContent?.labelRows ?? 1) + 8;
|
|
14098
14603
|
} else {
|
|
14099
14604
|
const ptLabelHeight = Math.ceil(theme.fonts.sizes.small * 1.3);
|
|
14100
14605
|
legendBlockHeight += Math.max(10, ptLabelHeight) + 6 + 8;
|
|
@@ -14157,6 +14662,10 @@ function compileGeoMap(spec, options) {
|
|
|
14157
14662
|
}
|
|
14158
14663
|
const droppedFeatures = [];
|
|
14159
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
|
+
};
|
|
14160
14669
|
const neutralFill = theme.colors.neutral[100];
|
|
14161
14670
|
const noDataPattern = {
|
|
14162
14671
|
type: "diagonal",
|
|
@@ -14219,7 +14728,7 @@ function compileGeoMap(spec, options) {
|
|
|
14219
14728
|
const labelY = bar.y + bar.height + CONTINUOUS_LABEL_GAP + labelStyle.fontSize;
|
|
14220
14729
|
const hasUnmatched = featureMarks.some((m) => m.noData);
|
|
14221
14730
|
const noDataX = bar.x + bar.width + NO_DATA_SWATCH_GAP;
|
|
14222
|
-
const noDataLabelWidth =
|
|
14731
|
+
const noDataLabelWidth = estimateTextWidth19(
|
|
14223
14732
|
"No data",
|
|
14224
14733
|
labelStyle.fontSize,
|
|
14225
14734
|
labelStyle.fontWeight
|
|
@@ -14318,21 +14827,10 @@ function compileGeoMap(spec, options) {
|
|
|
14318
14827
|
const pointCategoryColors = /* @__PURE__ */ new Map();
|
|
14319
14828
|
if (pts.color) {
|
|
14320
14829
|
if (pts.color.type === "quantitative") {
|
|
14321
|
-
|
|
14322
|
-
for (const row of pts.data) {
|
|
14323
|
-
const raw = row[pts.color.field];
|
|
14324
|
-
if (raw != null) {
|
|
14325
|
-
const v = Number(raw);
|
|
14326
|
-
if (Number.isFinite(v)) colorValues.push(v);
|
|
14327
|
-
}
|
|
14328
|
-
}
|
|
14329
|
-
if (colorValues.length > 0) {
|
|
14330
|
-
const palette = pts.color.scale?.scheme ?? "blue";
|
|
14331
|
-
const paletteStops = [...SEQUENTIAL_PALETTES2[palette] ?? SEQUENTIAL_PALETTES2.blue];
|
|
14332
|
-
const qScale = scaleQuantile3().domain(colorValues).range(paletteStops);
|
|
14830
|
+
if (pointClassScale) {
|
|
14333
14831
|
pointColorScale = (val) => {
|
|
14334
|
-
const
|
|
14335
|
-
return
|
|
14832
|
+
const value = finiteNumber(val);
|
|
14833
|
+
return value !== void 0 ? pointClassScale.color(value) : neutralFill;
|
|
14336
14834
|
};
|
|
14337
14835
|
}
|
|
14338
14836
|
} else {
|
|
@@ -14369,16 +14867,19 @@ function compileGeoMap(spec, options) {
|
|
|
14369
14867
|
const droppedPoints = [];
|
|
14370
14868
|
for (let i = 0; i < pts.data.length; i++) {
|
|
14371
14869
|
const row = pts.data[i];
|
|
14372
|
-
const lon =
|
|
14373
|
-
const lat =
|
|
14374
|
-
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;
|
|
14375
14875
|
const projected = projection([lon, lat]);
|
|
14376
|
-
if (!projected) {
|
|
14876
|
+
if (!projected || !projected.every(Number.isFinite)) {
|
|
14377
14877
|
droppedPoints.push([lon, lat]);
|
|
14378
14878
|
continue;
|
|
14379
14879
|
}
|
|
14380
|
-
const r = sizeScale ? sizeScale.scale(
|
|
14381
|
-
|
|
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;
|
|
14382
14883
|
const key = keyChannel ? String(row[keyChannel.field] ?? i) : String(i);
|
|
14383
14884
|
const stroke = resolveKnockoutColor(theme);
|
|
14384
14885
|
pointMarks.push({
|
|
@@ -14442,24 +14943,7 @@ function compileGeoMap(spec, options) {
|
|
|
14442
14943
|
};
|
|
14443
14944
|
}
|
|
14444
14945
|
if (pts.color && pts.color.type === "quantitative" && showLegend) {
|
|
14445
|
-
const
|
|
14446
|
-
for (const row of pts.data) {
|
|
14447
|
-
const v = row[pts.color.field];
|
|
14448
|
-
if (v != null) {
|
|
14449
|
-
const n = Number(v);
|
|
14450
|
-
if (Number.isFinite(n)) ptColorValues.push(n);
|
|
14451
|
-
}
|
|
14452
|
-
}
|
|
14453
|
-
const ptLegendChannel = {
|
|
14454
|
-
...pts.color,
|
|
14455
|
-
scale: { ...pts.color.scale, type: "quantile" }
|
|
14456
|
-
};
|
|
14457
|
-
const ptContinuousContent = computeContinuousLegendContentForChannel(
|
|
14458
|
-
ptColorValues,
|
|
14459
|
-
ptLegendChannel,
|
|
14460
|
-
theme,
|
|
14461
|
-
fullArea.width
|
|
14462
|
-
);
|
|
14946
|
+
const ptContinuousContent = pointContinuousContent;
|
|
14463
14947
|
if (ptContinuousContent) {
|
|
14464
14948
|
const labelStyle = {
|
|
14465
14949
|
fontFamily: theme.fonts.family,
|
|
@@ -14474,7 +14958,7 @@ function compileGeoMap(spec, options) {
|
|
|
14474
14958
|
const legendX = pointLegendOverlay ? fullArea.x + OVERLAY_LEGEND_INSET : fullArea.x;
|
|
14475
14959
|
const legendY = pointLegendOverlay ? fullArea.y + OVERLAY_LEGEND_INSET : fullArea.y + mapAreaHeight + legendReserveGap + bottomChoroplethH + choroplethLegendGap;
|
|
14476
14960
|
const ptLabelRowHeight = Math.ceil(theme.fonts.sizes.small * 1.3);
|
|
14477
|
-
const ptLegendHeight = ptContinuousContent.barHeight + CONTINUOUS_LABEL_GAP + ptLabelRowHeight;
|
|
14961
|
+
const ptLegendHeight = ptContinuousContent.barHeight + CONTINUOUS_LABEL_GAP + ptLabelRowHeight * (ptContinuousContent.labelRows ?? 1);
|
|
14478
14962
|
const bounds = {
|
|
14479
14963
|
x: legendX,
|
|
14480
14964
|
y: legendY,
|
|
@@ -14548,17 +15032,17 @@ function compileGeoMap(spec, options) {
|
|
|
14548
15032
|
if (tooltipChannels) {
|
|
14549
15033
|
for (const ch of tooltipChannels) {
|
|
14550
15034
|
const val = mark.data[ch.field];
|
|
14551
|
-
if (val != null) {
|
|
15035
|
+
if (val != null || ch.type === "quantitative") {
|
|
14552
15036
|
fields.push({
|
|
14553
15037
|
label: ch.title ?? ch.field,
|
|
14554
|
-
value: ch.type === "quantitative" ?
|
|
15038
|
+
value: ch.type === "quantitative" ? formatQuantitative(val) : String(val)
|
|
14555
15039
|
});
|
|
14556
15040
|
}
|
|
14557
15041
|
}
|
|
14558
|
-
} else if (colorEncoding && isQuantitative
|
|
15042
|
+
} else if (colorEncoding && isQuantitative) {
|
|
14559
15043
|
fields.push({
|
|
14560
15044
|
label: colorEncoding.title ?? colorEncoding.field,
|
|
14561
|
-
value:
|
|
15045
|
+
value: formatQuantitative(mark.data[colorEncoding.field])
|
|
14562
15046
|
});
|
|
14563
15047
|
} else if (colorEncoding && !isQuantitative && mark.data[colorEncoding.field] != null) {
|
|
14564
15048
|
fields.push({
|
|
@@ -14575,10 +15059,10 @@ function compileGeoMap(spec, options) {
|
|
|
14575
15059
|
if (ptTooltipChannels) {
|
|
14576
15060
|
for (const ch of ptTooltipChannels) {
|
|
14577
15061
|
const val = pm.data[ch.field];
|
|
14578
|
-
if (val != null) {
|
|
15062
|
+
if (val != null || ch.type === "quantitative") {
|
|
14579
15063
|
fields.push({
|
|
14580
15064
|
label: ch.title ?? ch.field,
|
|
14581
|
-
value: ch.type === "quantitative" ?
|
|
15065
|
+
value: ch.type === "quantitative" ? formatQuantitative(val) : String(val)
|
|
14582
15066
|
});
|
|
14583
15067
|
}
|
|
14584
15068
|
}
|
|
@@ -14591,16 +15075,15 @@ function compileGeoMap(spec, options) {
|
|
|
14591
15075
|
}
|
|
14592
15076
|
if (pts.color) {
|
|
14593
15077
|
const val = pm.data[pts.color.field];
|
|
14594
|
-
if (val != null)
|
|
15078
|
+
if (val != null || pts.color.type === "quantitative")
|
|
14595
15079
|
fields.push({
|
|
14596
15080
|
label: pts.color.title ?? pts.color.field,
|
|
14597
|
-
value: pts.color.type === "quantitative" ?
|
|
15081
|
+
value: pts.color.type === "quantitative" ? formatQuantitative(val) : String(val)
|
|
14598
15082
|
});
|
|
14599
15083
|
}
|
|
14600
15084
|
if (pts.size) {
|
|
14601
15085
|
const val = pm.data[pts.size.field];
|
|
14602
|
-
|
|
14603
|
-
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) });
|
|
14604
15087
|
}
|
|
14605
15088
|
}
|
|
14606
15089
|
if (fields.length > 0) {
|
|
@@ -14658,7 +15141,7 @@ function compileGeoMap(spec, options) {
|
|
|
14658
15141
|
height: contentHeight,
|
|
14659
15142
|
animation: resolvedAnimation,
|
|
14660
15143
|
watermark,
|
|
14661
|
-
measureText: options.measureText ?? ((text, fontSize) => ({ width:
|
|
15144
|
+
measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth19(text, fontSize), height: fontSize })),
|
|
14662
15145
|
warnings: compileWarnings,
|
|
14663
15146
|
mapSize: { width: fullArea.width, height: mapAreaHeight },
|
|
14664
15147
|
focus: resolvedFocus
|
|
@@ -14729,22 +15212,13 @@ function buildQuantitativeMarks(opts) {
|
|
|
14729
15212
|
}
|
|
14730
15213
|
const featureId = feat.id;
|
|
14731
15214
|
const dataRow = joined.get(featureId) ?? null;
|
|
14732
|
-
const hasData = dataRow !== null;
|
|
14733
15215
|
let fill = neutralFill;
|
|
14734
15216
|
let binIndex;
|
|
14735
|
-
|
|
14736
|
-
|
|
14737
|
-
|
|
14738
|
-
|
|
14739
|
-
|
|
14740
|
-
if (!Number.isNaN(v)) {
|
|
14741
|
-
hasValue = true;
|
|
14742
|
-
if (classScale) {
|
|
14743
|
-
fill = classScale.color(v);
|
|
14744
|
-
binIndex = classScale.binIndex(v);
|
|
14745
|
-
}
|
|
14746
|
-
}
|
|
14747
|
-
}
|
|
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);
|
|
14748
15222
|
}
|
|
14749
15223
|
const props = feat.properties;
|
|
14750
15224
|
const name = props?.name ?? props?.NAME;
|
|
@@ -14768,7 +15242,7 @@ function buildQuantitativeMarks(opts) {
|
|
|
14768
15242
|
data: dataRow,
|
|
14769
15243
|
aria: {
|
|
14770
15244
|
role: "img",
|
|
14771
|
-
label: name ? `${name}: ${
|
|
15245
|
+
label: name ? `${name}: ${value !== void 0 ? formatter(value) : "no data"}` : String(featureId)
|
|
14772
15246
|
},
|
|
14773
15247
|
animationIndex: animIndex++,
|
|
14774
15248
|
bounds,
|
|
@@ -15043,7 +15517,7 @@ function emptyLayout3(chrome, theme, options, watermark) {
|
|
|
15043
15517
|
height: options.height,
|
|
15044
15518
|
animation: void 0,
|
|
15045
15519
|
watermark,
|
|
15046
|
-
measureText: options.measureText ?? ((text, fontSize) => ({ width:
|
|
15520
|
+
measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth19(text, fontSize), height: fontSize })),
|
|
15047
15521
|
warnings: [],
|
|
15048
15522
|
mapSize: { width: 0, height: 0 },
|
|
15049
15523
|
focus: null
|
|
@@ -15794,7 +16268,7 @@ import {
|
|
|
15794
16268
|
computeChrome as computeChrome5,
|
|
15795
16269
|
computeFieldFormatContext as computeFieldFormatContext6,
|
|
15796
16270
|
defaultNumberFormatter as defaultNumberFormatter9,
|
|
15797
|
-
estimateTextWidth as
|
|
16271
|
+
estimateTextWidth as estimateTextWidth21,
|
|
15798
16272
|
getHeightClass as getHeightClass2,
|
|
15799
16273
|
HPAD_COMPACT_FRACTION as HPAD_COMPACT_FRACTION3,
|
|
15800
16274
|
HPAD_COMPACT_MIN as HPAD_COMPACT_MIN3,
|
|
@@ -15811,7 +16285,7 @@ import {
|
|
|
15811
16285
|
} from "@opendata-ai/openchart-core";
|
|
15812
16286
|
|
|
15813
16287
|
// src/layout/metrics.ts
|
|
15814
|
-
import { estimateTextWidth as
|
|
16288
|
+
import { estimateTextWidth as estimateTextWidth20 } from "@opendata-ai/openchart-core";
|
|
15815
16289
|
var DEFAULT_LABEL_FONT_SIZE = 10;
|
|
15816
16290
|
var DEFAULT_VALUE_FONT_SIZE = 22;
|
|
15817
16291
|
var LABEL_LINE_HEIGHT_RATIO = 1.4;
|
|
@@ -15844,7 +16318,7 @@ function computeMetricBar(metrics, metricsTopY, metricsArea, remainingChartHeigh
|
|
|
15844
16318
|
const cellWidth = metricsArea.width / metrics.length;
|
|
15845
16319
|
for (const metric of metrics) {
|
|
15846
16320
|
const text = valueRunText(metric);
|
|
15847
|
-
const measured = measureText ? measureText(text, fonts.value, 510).width :
|
|
16321
|
+
const measured = measureText ? measureText(text, fonts.value, 510).width : estimateTextWidth20(text, fonts.value, 510);
|
|
15848
16322
|
if (measured > cellWidth - CELL_INNER_PAD) return void 0;
|
|
15849
16323
|
}
|
|
15850
16324
|
const labelLine = fonts.label * LABEL_LINE_HEIGHT_RATIO;
|
|
@@ -15979,7 +16453,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
15979
16453
|
if (xField) {
|
|
15980
16454
|
for (const row of spec.data) {
|
|
15981
16455
|
const label = String(row[xField] ?? "");
|
|
15982
|
-
const w =
|
|
16456
|
+
const w = estimateTextWidth21(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
|
|
15983
16457
|
if (w > maxLabelWidth) maxLabelWidth = w;
|
|
15984
16458
|
}
|
|
15985
16459
|
}
|
|
@@ -16033,7 +16507,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
16033
16507
|
const label = String(row[colorField] ?? "");
|
|
16034
16508
|
if (!seen.has(label)) {
|
|
16035
16509
|
seen.add(label);
|
|
16036
|
-
const w =
|
|
16510
|
+
const w = estimateTextWidth21(label, 11, 600);
|
|
16037
16511
|
if (w > maxLabelWidth) maxLabelWidth = w;
|
|
16038
16512
|
}
|
|
16039
16513
|
}
|
|
@@ -16053,7 +16527,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
16053
16527
|
const maxXStr = String(maxX);
|
|
16054
16528
|
for (const ann of spec.annotations) {
|
|
16055
16529
|
if (ann.type === "text" && String(ann.x) === maxXStr) {
|
|
16056
|
-
const textWidth =
|
|
16530
|
+
const textWidth = estimateTextWidth21(ann.text, ann.fontSize ?? 11, ann.fontWeight ?? 600);
|
|
16057
16531
|
const dx = ann.offset?.dx ?? 0;
|
|
16058
16532
|
const anchor = ann.anchor ?? "auto";
|
|
16059
16533
|
const baseRightExtent = anchor === "right" ? textWidth : anchor === "left" ? 0 : textWidth / 2;
|
|
@@ -16075,12 +16549,12 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
16075
16549
|
let maxLabelWidth = 0;
|
|
16076
16550
|
for (const row of spec.data) {
|
|
16077
16551
|
const label = String(row[yField] ?? "");
|
|
16078
|
-
let w =
|
|
16552
|
+
let w = estimateTextWidth21(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
|
|
16079
16553
|
if (yLabelField) {
|
|
16080
16554
|
const subtitle = String(row[yLabelField] ?? "");
|
|
16081
16555
|
if (subtitle) {
|
|
16082
16556
|
const gap = theme.fonts.sizes.axisTick * 0.6;
|
|
16083
|
-
const subtitleWidth =
|
|
16557
|
+
const subtitleWidth = estimateTextWidth21(
|
|
16084
16558
|
subtitle,
|
|
16085
16559
|
theme.fonts.sizes.axisTick,
|
|
16086
16560
|
theme.fonts.weights.normal
|
|
@@ -16117,7 +16591,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
16117
16591
|
}
|
|
16118
16592
|
const negPrefix = spec.data.some((r) => Number(r[yField]) < 0) ? "-" : "";
|
|
16119
16593
|
const labelEst = negPrefix + sampleLabel;
|
|
16120
|
-
const labelWidth =
|
|
16594
|
+
const labelWidth = estimateTextWidth21(
|
|
16121
16595
|
labelEst,
|
|
16122
16596
|
theme.fonts.sizes.axisTick,
|
|
16123
16597
|
theme.fonts.weights.normal
|
|
@@ -16148,7 +16622,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
16148
16622
|
})(maxAbsValForTitle);
|
|
16149
16623
|
}
|
|
16150
16624
|
const negPrefixForTitle = spec.data.some((r) => Number(r[yFieldForTitle]) < 0) ? "-" : "";
|
|
16151
|
-
estTickLabelWidth =
|
|
16625
|
+
estTickLabelWidth = estimateTextWidth21(
|
|
16152
16626
|
negPrefixForTitle + sampleLabelForTitle,
|
|
16153
16627
|
theme.fonts.sizes.axisTick,
|
|
16154
16628
|
theme.fonts.weights.normal
|
|
@@ -16409,7 +16883,7 @@ import {
|
|
|
16409
16883
|
buildD3Formatter as buildD3Formatter3,
|
|
16410
16884
|
computeChrome as computeChrome6,
|
|
16411
16885
|
defaultNumberFormatter as defaultNumberFormatter10,
|
|
16412
|
-
estimateTextWidth as
|
|
16886
|
+
estimateTextWidth as estimateTextWidth22,
|
|
16413
16887
|
resolveTheme as resolveTheme5
|
|
16414
16888
|
} from "@opendata-ai/openchart-core";
|
|
16415
16889
|
|
|
@@ -16422,21 +16896,19 @@ var ALIGN_MAP = {
|
|
|
16422
16896
|
center: sankeyCenter
|
|
16423
16897
|
};
|
|
16424
16898
|
function computeSankeyLayout(data, sourceField, targetField, valueField, area3, nodeWidth, nodePadding, nodeAlign, iterations, nodeSort) {
|
|
16899
|
+
const links = [];
|
|
16425
16900
|
const nodeSet = /* @__PURE__ */ new Set();
|
|
16426
16901
|
for (const row of data) {
|
|
16427
|
-
|
|
16428
|
-
|
|
16429
|
-
|
|
16430
|
-
|
|
16431
|
-
|
|
16432
|
-
|
|
16433
|
-
|
|
16434
|
-
|
|
16435
|
-
|
|
16436
|
-
|
|
16437
|
-
value: Number(row[valueField]) || 0,
|
|
16438
|
-
data: { ...row }
|
|
16439
|
-
}));
|
|
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 }));
|
|
16440
16912
|
const alignFn = ALIGN_MAP[nodeAlign] ?? sankeyJustify;
|
|
16441
16913
|
const generator = sankey().nodeId((d) => d.id).nodeAlign(alignFn).nodeWidth(nodeWidth).nodePadding(nodePadding).extent([
|
|
16442
16914
|
[area3.x, area3.y],
|
|
@@ -16566,10 +17038,11 @@ function rewriteRowsForOther(data, sourceField, targetField, valueField, merged)
|
|
|
16566
17038
|
const newTgt = merged.get(tgt) ?? tgt;
|
|
16567
17039
|
if (newSrc === newTgt) continue;
|
|
16568
17040
|
const key = `${newSrc}\0${newTgt}`;
|
|
16569
|
-
const value =
|
|
17041
|
+
const value = finiteNumber(row[valueField]);
|
|
17042
|
+
if (value === void 0 || value <= 0) continue;
|
|
16570
17043
|
const existing = index.get(key);
|
|
16571
17044
|
if (existing) {
|
|
16572
|
-
existing[valueField] = (
|
|
17045
|
+
existing[valueField] = (finiteNumber(existing[valueField]) ?? 0) + value;
|
|
16573
17046
|
continue;
|
|
16574
17047
|
}
|
|
16575
17048
|
const next = { ...row, [sourceField]: newSrc, [targetField]: newTgt, [valueField]: value };
|
|
@@ -16686,12 +17159,40 @@ function compileSankey(spec, options) {
|
|
|
16686
17159
|
const targetField = sankeySpec.encoding.target.field;
|
|
16687
17160
|
const valueField = sankeySpec.encoding.value.field;
|
|
16688
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
|
+
}
|
|
16689
17190
|
const flowFmt = resolveFieldFormatter({
|
|
16690
17191
|
surfaceFormat: resolvedValueFormat,
|
|
16691
|
-
values:
|
|
17192
|
+
values: measuredData.map((r) => r[valueField])
|
|
16692
17193
|
});
|
|
16693
17194
|
const tempNodeIds = /* @__PURE__ */ new Set();
|
|
16694
|
-
for (const row of
|
|
17195
|
+
for (const row of measuredData) {
|
|
16695
17196
|
tempNodeIds.add(String(row[sourceField]));
|
|
16696
17197
|
tempNodeIds.add(String(row[targetField]));
|
|
16697
17198
|
}
|
|
@@ -16699,14 +17200,14 @@ function compileSankey(spec, options) {
|
|
|
16699
17200
|
[...tempNodeIds].map((id) => ({ id })),
|
|
16700
17201
|
theme.colors.categorical,
|
|
16701
17202
|
colorField,
|
|
16702
|
-
|
|
17203
|
+
measuredData,
|
|
16703
17204
|
sourceField,
|
|
16704
17205
|
targetField
|
|
16705
17206
|
);
|
|
16706
17207
|
const legend = buildSankeyLegend(
|
|
16707
17208
|
tempColorMap,
|
|
16708
17209
|
colorField,
|
|
16709
|
-
|
|
17210
|
+
measuredData,
|
|
16710
17211
|
sourceField,
|
|
16711
17212
|
targetField,
|
|
16712
17213
|
theme,
|
|
@@ -16726,7 +17227,7 @@ function compileSankey(spec, options) {
|
|
|
16726
17227
|
const labelFontWeight = theme.fonts.weights.medium;
|
|
16727
17228
|
const valueFontWeight = theme.fonts.weights.normal;
|
|
16728
17229
|
const nodeLabelAlign = sankeySpec.nodeLabelAlign ?? "auto";
|
|
16729
|
-
let workingData =
|
|
17230
|
+
let workingData = measuredData;
|
|
16730
17231
|
const runLayout = (rect) => computeSankeyLayout(
|
|
16731
17232
|
workingData,
|
|
16732
17233
|
sourceField,
|
|
@@ -16768,8 +17269,8 @@ function compileSankey(spec, options) {
|
|
|
16768
17269
|
const nodeFmt = resolveNodeValueFormatter(nodes, resolvedValueFormat, flowFmt);
|
|
16769
17270
|
const labelBlockWidth = (node) => {
|
|
16770
17271
|
const name = node.label ?? node.id;
|
|
16771
|
-
const nameWidth =
|
|
16772
|
-
const valueWidth =
|
|
17272
|
+
const nameWidth = estimateTextWidth22(name, labelFontSize, labelFontWeight);
|
|
17273
|
+
const valueWidth = estimateTextWidth22(
|
|
16773
17274
|
formatFlowValue(node.value ?? 0, nodeFmt),
|
|
16774
17275
|
labelFontSize,
|
|
16775
17276
|
valueFontWeight
|
|
@@ -16843,7 +17344,7 @@ function compileSankey(spec, options) {
|
|
|
16843
17344
|
const nextColumn = columnX0.get(depth + 1);
|
|
16844
17345
|
if (nextColumn === void 0) return { text, style: valueStyle };
|
|
16845
17346
|
const available = nextColumn - LABEL_GAP3 - ((node.x1 ?? 0) + LABEL_GAP3);
|
|
16846
|
-
const needed =
|
|
17347
|
+
const needed = estimateTextWidth22(node.label ?? node.id, labelFontSize, labelFontWeight) + VALUE_GAP + estimateTextWidth22(text, labelFontSize, valueFontWeight);
|
|
16847
17348
|
return needed <= available ? { text, style: valueStyle } : void 0;
|
|
16848
17349
|
};
|
|
16849
17350
|
const nodeMarks = nodes.map((node) => {
|
|
@@ -17101,7 +17602,7 @@ function emptyLayout4(area3, chrome, theme, options, watermark) {
|
|
|
17101
17602
|
}
|
|
17102
17603
|
|
|
17103
17604
|
// src/tables/compile-table.ts
|
|
17104
|
-
import { computeChrome as computeChrome7, estimateTextWidth as
|
|
17605
|
+
import { computeChrome as computeChrome7, estimateTextWidth as estimateTextWidth23 } from "@opendata-ai/openchart-core";
|
|
17105
17606
|
|
|
17106
17607
|
// src/tables/bar-column.ts
|
|
17107
17608
|
var NEGATIVE_BAR_COLOR = "#c44e52";
|
|
@@ -17164,14 +17665,14 @@ import { adaptColorForDarkMode, findAccessibleColor as findAccessibleColor2 } fr
|
|
|
17164
17665
|
import { interpolateRgb as interpolateRgb2 } from "d3-interpolate";
|
|
17165
17666
|
|
|
17166
17667
|
// src/tables/utils.ts
|
|
17167
|
-
import { contrastRatio as
|
|
17668
|
+
import { contrastRatio as contrastRatio3, cssTokenDefault } from "@opendata-ai/openchart-core";
|
|
17168
17669
|
function resolveTableSurface(theme) {
|
|
17169
17670
|
return theme.colors.neutral.surface;
|
|
17170
17671
|
}
|
|
17171
17672
|
function accessibleTextColor(bg, theme) {
|
|
17172
17673
|
const own = theme.colors.text;
|
|
17173
17674
|
const inverse = cssTokenDefault("--oc-text", theme.isDark ? "light" : "dark");
|
|
17174
|
-
return
|
|
17675
|
+
return contrastRatio3(inverse, bg) > contrastRatio3(own, bg) ? inverse : own;
|
|
17175
17676
|
}
|
|
17176
17677
|
|
|
17177
17678
|
// src/tables/category-colors.ts
|
|
@@ -17291,7 +17792,7 @@ function formatValueForSearch(value, column) {
|
|
|
17291
17792
|
}
|
|
17292
17793
|
|
|
17293
17794
|
// src/tables/heatmap.ts
|
|
17294
|
-
import { adaptColorForDarkMode as adaptColorForDarkMode2, contrastRatio as
|
|
17795
|
+
import { adaptColorForDarkMode as adaptColorForDarkMode2, contrastRatio as contrastRatio4 } from "@opendata-ai/openchart-core";
|
|
17295
17796
|
import { interpolateRgb as interpolateRgb3 } from "d3-interpolate";
|
|
17296
17797
|
import { scaleSequential } from "d3-scale";
|
|
17297
17798
|
function interpolatorFromStops(stops) {
|
|
@@ -17348,7 +17849,7 @@ function computeHeatmapColors(data, column, theme, darkMode) {
|
|
|
17348
17849
|
const darkBg = resolveTableSurface(theme);
|
|
17349
17850
|
if (Array.isArray(config.palette)) {
|
|
17350
17851
|
stops = stops.map((c) => {
|
|
17351
|
-
const ratio =
|
|
17852
|
+
const ratio = contrastRatio4(c, lightBg);
|
|
17352
17853
|
if (ratio < 1.15) return darkBg;
|
|
17353
17854
|
return adaptColorForDarkMode2(c, lightBg, darkBg);
|
|
17354
17855
|
});
|
|
@@ -17574,13 +18075,13 @@ function estimateColumnWidth(col, data, fontSize, paddingX) {
|
|
|
17574
18075
|
if (col.image) return (col.image.width ?? 24) + paddingX;
|
|
17575
18076
|
if (col.flag) return 60;
|
|
17576
18077
|
const label = col.label ?? col.key;
|
|
17577
|
-
const headerWidth =
|
|
18078
|
+
const headerWidth = estimateTextWidth23(label, fontSize, 600) + paddingX;
|
|
17578
18079
|
const sampleSize = Math.min(100, data.length);
|
|
17579
18080
|
let maxDataWidth = 0;
|
|
17580
18081
|
for (let i = 0; i < sampleSize; i++) {
|
|
17581
18082
|
const val = data[i][col.key];
|
|
17582
18083
|
const text = val == null ? "" : String(val);
|
|
17583
|
-
const width =
|
|
18084
|
+
const width = estimateTextWidth23(text, fontSize, 400) + paddingX;
|
|
17584
18085
|
if (width > maxDataWidth) maxDataWidth = width;
|
|
17585
18086
|
}
|
|
17586
18087
|
return Math.max(MIN_WIDTH, headerWidth, maxDataWidth);
|
|
@@ -17857,9 +18358,17 @@ function compileTableLayout(spec, options, theme) {
|
|
|
17857
18358
|
sparklineDomains.set(col.key, computeSparklineDomain(data, col.key, col.sparkline));
|
|
17858
18359
|
}
|
|
17859
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
|
+
);
|
|
17860
18369
|
const rows = currentData.map((row, i) => {
|
|
17861
18370
|
const origIdx = originalIndices[i];
|
|
17862
|
-
const rowId =
|
|
18371
|
+
const rowId = identities?.[origIdx].observation ?? String(origIdx);
|
|
17863
18372
|
const cells = spec.columns.map((col, c) => {
|
|
17864
18373
|
const resolved = resolvedColumns[c];
|
|
17865
18374
|
const value = row[col.key];
|
|
@@ -17940,13 +18449,13 @@ import {
|
|
|
17940
18449
|
adaptTheme as adaptTheme5,
|
|
17941
18450
|
CATEGORICAL_PALETTE as CATEGORICAL_PALETTE3,
|
|
17942
18451
|
computeChrome as computeChrome8,
|
|
17943
|
-
estimateTextWidth as
|
|
18452
|
+
estimateTextWidth as estimateTextWidth24,
|
|
17944
18453
|
getBreakpoint as getBreakpoint2,
|
|
17945
18454
|
HPAD_COMPACT_FRACTION as HPAD_COMPACT_FRACTION4,
|
|
17946
18455
|
HPAD_COMPACT_MIN as HPAD_COMPACT_MIN4,
|
|
17947
18456
|
pickLabelColor as pickLabelColor4,
|
|
17948
18457
|
resolveTheme as resolveTheme6,
|
|
17949
|
-
SEQUENTIAL_PALETTES as
|
|
18458
|
+
SEQUENTIAL_PALETTES as SEQUENTIAL_PALETTES2
|
|
17950
18459
|
} from "@opendata-ai/openchart-core";
|
|
17951
18460
|
import { interpolateRgb as interpolateRgb4 } from "d3-interpolate";
|
|
17952
18461
|
import { scaleLinear as scaleLinear6 } from "d3-scale";
|
|
@@ -18053,18 +18562,15 @@ function compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDa
|
|
|
18053
18562
|
const stateValueMap = /* @__PURE__ */ new Map();
|
|
18054
18563
|
for (const row of tilemapSpec.data) {
|
|
18055
18564
|
const stateCode = String(row[stateField]);
|
|
18056
|
-
const
|
|
18057
|
-
if (STATE_CODE_SET.has(stateCode) &&
|
|
18058
|
-
|
|
18059
|
-
if (!Number.isNaN(value)) {
|
|
18060
|
-
stateValueMap.set(stateCode, value);
|
|
18061
|
-
}
|
|
18565
|
+
const value = finiteNumber(row[valueField]);
|
|
18566
|
+
if (STATE_CODE_SET.has(stateCode) && value !== void 0) {
|
|
18567
|
+
stateValueMap.set(stateCode, value);
|
|
18062
18568
|
}
|
|
18063
18569
|
}
|
|
18064
18570
|
const values = Array.from(stateValueMap.values());
|
|
18065
18571
|
const min4 = values.length > 0 ? Math.min(...values) : 0;
|
|
18066
18572
|
const max4 = values.length > 0 ? Math.max(...values) : 100;
|
|
18067
|
-
const paletteStops = [...
|
|
18573
|
+
const paletteStops = [...SEQUENTIAL_PALETTES2[tilemapSpec.palette] ?? SEQUENTIAL_PALETTES2.blue];
|
|
18068
18574
|
const baseColor = isDarkMode ? paletteStops[0] : paletteStops[paletteStops.length - 1];
|
|
18069
18575
|
const opacityRange = isDarkMode ? [0.3, 1] : [0.25, 1];
|
|
18070
18576
|
const opacityScale = scaleLinear6().domain([min4, max4]).range(opacityRange).clamp(true);
|
|
@@ -18125,7 +18631,7 @@ function compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDa
|
|
|
18125
18631
|
});
|
|
18126
18632
|
const hasEmptyTiles = tiles.some((t) => !t.hasData);
|
|
18127
18633
|
const noDataLabel = "No data";
|
|
18128
|
-
const noDataLabelWidth =
|
|
18634
|
+
const noDataLabelWidth = estimateTextWidth24(noDataLabel, 11);
|
|
18129
18635
|
const noDataBlockWidth2 = hasEmptyTiles ? NO_DATA_BAR_GAP + legendBarHeight + NO_DATA_LABEL_GAP2 + noDataLabelWidth : 0;
|
|
18130
18636
|
const barWidth = Math.max(0, legendWidth - noDataBlockWidth2);
|
|
18131
18637
|
const noDataX = legendX + barWidth + NO_DATA_BAR_GAP;
|
|
@@ -18182,7 +18688,7 @@ function compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDa
|
|
|
18182
18688
|
height: contentHeight,
|
|
18183
18689
|
animation: resolvedAnimation,
|
|
18184
18690
|
watermark,
|
|
18185
|
-
measureText: options.measureText ?? ((text, fontSize) => ({ width:
|
|
18691
|
+
measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth24(text, fontSize), height: fontSize }))
|
|
18186
18692
|
};
|
|
18187
18693
|
}
|
|
18188
18694
|
function compileCategorical(tilemapSpec, options, theme, chrome, fullArea, isDarkMode, watermark, chromeLayout) {
|
|
@@ -18323,7 +18829,7 @@ function compileCategorical(tilemapSpec, options, theme, chrome, fullArea, isDar
|
|
|
18323
18829
|
height: contentHeight,
|
|
18324
18830
|
animation: resolvedAnimation,
|
|
18325
18831
|
watermark,
|
|
18326
|
-
measureText: options.measureText ?? ((text, fontSize) => ({ width:
|
|
18832
|
+
measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth24(text, fontSize), height: fontSize }))
|
|
18327
18833
|
};
|
|
18328
18834
|
}
|
|
18329
18835
|
function buildTileMark(opts) {
|
|
@@ -18449,7 +18955,7 @@ function countLegendRows(labels, availableWidth, fontSize) {
|
|
|
18449
18955
|
let rows = 1;
|
|
18450
18956
|
let offsetX = 0;
|
|
18451
18957
|
for (let i = 0; i < labels.length; i++) {
|
|
18452
|
-
const labelWidth =
|
|
18958
|
+
const labelWidth = estimateTextWidth24(labels[i], fontSize);
|
|
18453
18959
|
const entryWidth = LEGEND_SWATCH_SIZE + LEGEND_SWATCH_GAP + labelWidth + LEGEND_ENTRY_GAP;
|
|
18454
18960
|
if (i > 0 && offsetX + entryWidth > availableWidth) {
|
|
18455
18961
|
rows++;
|
|
@@ -18493,7 +18999,7 @@ function emptyLayout5(chrome, theme, options, watermark) {
|
|
|
18493
18999
|
height: options.height,
|
|
18494
19000
|
watermark,
|
|
18495
19001
|
animation: void 0,
|
|
18496
|
-
measureText: options.measureText ?? ((text, fontSize) => ({ width:
|
|
19002
|
+
measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth24(text, fontSize), height: fontSize }))
|
|
18497
19003
|
};
|
|
18498
19004
|
}
|
|
18499
19005
|
|
|
@@ -19139,8 +19645,8 @@ function runBin(data, transform) {
|
|
|
19139
19645
|
let min4 = Infinity;
|
|
19140
19646
|
let max4 = -Infinity;
|
|
19141
19647
|
for (const row of data) {
|
|
19142
|
-
const v =
|
|
19143
|
-
if (
|
|
19648
|
+
const v = finiteNumber(row[field]);
|
|
19649
|
+
if (v !== void 0) {
|
|
19144
19650
|
if (params.scaleType === "log" && v <= 0) continue;
|
|
19145
19651
|
if (v < min4) min4 = v;
|
|
19146
19652
|
if (v > max4) max4 = v;
|
|
@@ -19152,9 +19658,9 @@ function runBin(data, transform) {
|
|
|
19152
19658
|
if (params.scaleType === "log") {
|
|
19153
19659
|
const edges = computeLogBinEdges(extent3, maxbins);
|
|
19154
19660
|
return data.map((row) => {
|
|
19155
|
-
const v =
|
|
19661
|
+
const v = finiteNumber(row[field]);
|
|
19156
19662
|
const newRow = { ...row };
|
|
19157
|
-
if (
|
|
19663
|
+
if (v === void 0 || v <= 0) {
|
|
19158
19664
|
newRow[startAs] = null;
|
|
19159
19665
|
if (endAs) newRow[endAs] = null;
|
|
19160
19666
|
} else {
|
|
@@ -19174,9 +19680,9 @@ function runBin(data, transform) {
|
|
|
19174
19680
|
}
|
|
19175
19681
|
const step = params.step ?? computeStep(extent3, maxbins, nice);
|
|
19176
19682
|
return data.map((row) => {
|
|
19177
|
-
const v =
|
|
19683
|
+
const v = finiteNumber(row[field]);
|
|
19178
19684
|
const newRow = { ...row };
|
|
19179
|
-
if (
|
|
19685
|
+
if (v === void 0) {
|
|
19180
19686
|
newRow[startAs] = null;
|
|
19181
19687
|
if (endAs) newRow[endAs] = null;
|
|
19182
19688
|
} else {
|
|
@@ -19258,7 +19764,7 @@ function groupRows(data, groupby) {
|
|
|
19258
19764
|
if (groupby.length === 0) return /* @__PURE__ */ new Map([["", data]]);
|
|
19259
19765
|
const groups = /* @__PURE__ */ new Map();
|
|
19260
19766
|
for (const row of data) {
|
|
19261
|
-
const key =
|
|
19767
|
+
const key = groupKey(row, groupby);
|
|
19262
19768
|
const bucket = groups.get(key);
|
|
19263
19769
|
if (bucket) bucket.push(row);
|
|
19264
19770
|
else groups.set(key, [row]);
|
|
@@ -19278,10 +19784,8 @@ function runDensity(data, transform) {
|
|
|
19278
19784
|
for (const rows of groups.values()) {
|
|
19279
19785
|
const samples = [];
|
|
19280
19786
|
for (const r of rows) {
|
|
19281
|
-
const
|
|
19282
|
-
if (
|
|
19283
|
-
const v = Number(raw);
|
|
19284
|
-
if (Number.isFinite(v)) samples.push(v);
|
|
19787
|
+
const value = finiteNumber(r[field]);
|
|
19788
|
+
if (value !== void 0) samples.push(value);
|
|
19285
19789
|
}
|
|
19286
19790
|
if (samples.length === 0) continue;
|
|
19287
19791
|
const bandwidth = transform.bandwidth && transform.bandwidth > 0 ? transform.bandwidth : silvermanBandwidth(samples);
|
|
@@ -19453,14 +19957,11 @@ function runFold(data, transform) {
|
|
|
19453
19957
|
}
|
|
19454
19958
|
|
|
19455
19959
|
// src/transforms/joinaggregate.ts
|
|
19456
|
-
function groupKey2(row, groupby) {
|
|
19457
|
-
return groupby.map((f) => String(row[f] ?? "")).join("\0");
|
|
19458
|
-
}
|
|
19459
19960
|
function runJoinAggregate(data, transform) {
|
|
19460
19961
|
const groupby = transform.groupby ?? [];
|
|
19461
19962
|
const groups = /* @__PURE__ */ new Map();
|
|
19462
19963
|
for (const row of data) {
|
|
19463
|
-
const key =
|
|
19964
|
+
const key = groupKey(row, groupby);
|
|
19464
19965
|
const bucket = groups.get(key);
|
|
19465
19966
|
if (bucket) bucket.push(row);
|
|
19466
19967
|
else groups.set(key, [row]);
|
|
@@ -19469,12 +19970,11 @@ function runJoinAggregate(data, transform) {
|
|
|
19469
19970
|
for (const [key, rows] of groups) {
|
|
19470
19971
|
const summary = {};
|
|
19471
19972
|
for (const { op, field, as } of transform.joinaggregate) {
|
|
19472
|
-
|
|
19473
|
-
summary[as] = computeAggregate(op, values);
|
|
19973
|
+
summary[as] = aggregateRows(op, field, rows);
|
|
19474
19974
|
}
|
|
19475
19975
|
summaries.set(key, summary);
|
|
19476
19976
|
}
|
|
19477
|
-
return data.map((row) => ({ ...row, ...summaries.get(
|
|
19977
|
+
return data.map((row) => ({ ...row, ...summaries.get(groupKey(row, groupby)) }));
|
|
19478
19978
|
}
|
|
19479
19979
|
|
|
19480
19980
|
// src/transforms/timeunit.ts
|
|
@@ -19543,9 +20043,6 @@ function runTimeUnit(data, transform) {
|
|
|
19543
20043
|
}
|
|
19544
20044
|
|
|
19545
20045
|
// src/transforms/window.ts
|
|
19546
|
-
function groupKey3(row, groupby) {
|
|
19547
|
-
return groupby.map((f) => String(row[f] ?? "")).join("\0");
|
|
19548
|
-
}
|
|
19549
20046
|
function tryParseDate(val) {
|
|
19550
20047
|
if (val == null) return NaN;
|
|
19551
20048
|
if (typeof val === "number") return new Date(val).getTime();
|
|
@@ -19575,7 +20072,7 @@ function runWindow(data, transform) {
|
|
|
19575
20072
|
const indexed = data.map((row, i) => ({ row, originalIndex: i }));
|
|
19576
20073
|
const groups = /* @__PURE__ */ new Map();
|
|
19577
20074
|
for (const entry of indexed) {
|
|
19578
|
-
const key = groupby.length > 0 ?
|
|
20075
|
+
const key = groupby.length > 0 ? groupKey(entry.row, groupby) : "";
|
|
19579
20076
|
const existing = groups.get(key);
|
|
19580
20077
|
if (existing) {
|
|
19581
20078
|
existing.push(entry);
|
|
@@ -19612,26 +20109,25 @@ function runWindow(data, transform) {
|
|
|
19612
20109
|
case "diff": {
|
|
19613
20110
|
const lagIdx = i - offset;
|
|
19614
20111
|
if (lagIdx >= 0) {
|
|
19615
|
-
const current =
|
|
19616
|
-
const lagged =
|
|
19617
|
-
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;
|
|
19618
20115
|
}
|
|
19619
20116
|
break;
|
|
19620
20117
|
}
|
|
19621
20118
|
case "pct_change": {
|
|
19622
20119
|
const lagIdx = i - offset;
|
|
19623
20120
|
if (lagIdx >= 0) {
|
|
19624
|
-
const current =
|
|
19625
|
-
const lagged =
|
|
19626
|
-
if (
|
|
19627
|
-
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;
|
|
19628
20125
|
}
|
|
19629
20126
|
}
|
|
19630
20127
|
break;
|
|
19631
20128
|
}
|
|
19632
20129
|
case "cumsum": {
|
|
19633
|
-
const
|
|
19634
|
-
const addend = Number.isFinite(val) ? val : 0;
|
|
20130
|
+
const addend = finiteNumber(entry.row[def.field]) ?? 0;
|
|
19635
20131
|
if (i === 0) {
|
|
19636
20132
|
computed = addend;
|
|
19637
20133
|
} else {
|
|
@@ -20078,6 +20574,7 @@ function compileChart(spec, optionsInput) {
|
|
|
20078
20574
|
const skipY = axisless && !chartSpec.userExplicit.yAxis;
|
|
20079
20575
|
const axes = isRadial ? { x: void 0, y: void 0 } : computeAxes(scales, chartArea, strategy, theme, options.measureText, {
|
|
20080
20576
|
data: renderSpec.data,
|
|
20577
|
+
preserveEnd: typeof spec.responsive === "object" ? spec.responsive.preserveEnd : false,
|
|
20081
20578
|
encoding: renderSpec.encoding,
|
|
20082
20579
|
skipX,
|
|
20083
20580
|
skipY,
|
|
@@ -20133,7 +20630,7 @@ function compileChart(spec, optionsInput) {
|
|
|
20133
20630
|
obstacles.push(...computeMarkObstacles(marks, scales));
|
|
20134
20631
|
for (const mark of marks) {
|
|
20135
20632
|
if ("label" in mark && mark.label?.visible) {
|
|
20136
|
-
obstacles.push({ ...
|
|
20633
|
+
obstacles.push({ ...computeLabelBounds2(mark.label), kind: "data-label" });
|
|
20137
20634
|
}
|
|
20138
20635
|
}
|
|
20139
20636
|
const watermarkRect = computeWatermarkObstacle(dims, watermark, axes, theme);
|
|
@@ -20192,14 +20689,61 @@ function compileChart(spec, optionsInput) {
|
|
|
20192
20689
|
const reserve = footnoteBandHeight(footnotes.length, theme);
|
|
20193
20690
|
const currentReserve = options.footnoteReserve ?? 0;
|
|
20194
20691
|
if (!options.frozenChartArea && reserve > currentReserve && (options.footnotePass ?? 0) < MAX_FOOTNOTE_PASSES) {
|
|
20692
|
+
const grow = (spec.chromeLayout ?? options.chromeLayout) === "grow";
|
|
20195
20693
|
return compileChart(spec, {
|
|
20196
20694
|
...optionsInput,
|
|
20695
|
+
height: optionsInput.height + (grow ? reserve - currentReserve : 0),
|
|
20197
20696
|
footnoteReserve: reserve,
|
|
20198
20697
|
footnotePass: (options.footnotePass ?? 0) + 1
|
|
20199
20698
|
});
|
|
20200
20699
|
}
|
|
20201
20700
|
}
|
|
20202
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
|
+
}
|
|
20203
20747
|
const tooltipDescriptors = computeTooltipDescriptors(chartSpec, marks);
|
|
20204
20748
|
const altText = chartSpec.a11y?.description ?? generateAltText(
|
|
20205
20749
|
{
|
|
@@ -20219,10 +20763,14 @@ function compileChart(spec, optionsInput) {
|
|
|
20219
20763
|
chartSpec.data
|
|
20220
20764
|
);
|
|
20221
20765
|
assignAnimationIndices(marks, resolvedAnimation);
|
|
20222
|
-
const chrome =
|
|
20766
|
+
const chrome = {
|
|
20767
|
+
...dims.chrome,
|
|
20768
|
+
...footnotes ? { footnotes } : {},
|
|
20769
|
+
...seriesSummary.length ? { seriesSummary } : {}
|
|
20770
|
+
};
|
|
20223
20771
|
const xInvert = resolveScaleInvert(scales.x);
|
|
20224
20772
|
const yInvert = resolveScaleInvert(scales.y);
|
|
20225
|
-
|
|
20773
|
+
const layout = {
|
|
20226
20774
|
area: chartArea,
|
|
20227
20775
|
chrome,
|
|
20228
20776
|
metrics: dims.metrics,
|
|
@@ -20263,6 +20811,18 @@ function compileChart(spec, optionsInput) {
|
|
|
20263
20811
|
...yInvert ? { yInvert } : {},
|
|
20264
20812
|
...markRenderMode === "canvas" ? { markRenderMode } : {}
|
|
20265
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;
|
|
20266
20826
|
}
|
|
20267
20827
|
function compileFaceted(chartSpec, renderSpec, facetChannel, facetDirection, options, theme, strategy, watermark, resolvedAnimation, crosshair, rawSpec, optionsInput) {
|
|
20268
20828
|
const measure = createMeasureFn(options.measureText);
|
|
@@ -20599,6 +21159,176 @@ function compileGeoMap2(spec, options) {
|
|
|
20599
21159
|
function compileBarList2(spec, options) {
|
|
20600
21160
|
return compileBarList(spec, options);
|
|
20601
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
|
+
}
|
|
20602
21332
|
export {
|
|
20603
21333
|
ARROWHEAD_LENGTH,
|
|
20604
21334
|
AUTO_CANVAS_THRESHOLD,
|
|
@@ -20609,12 +21339,14 @@ export {
|
|
|
20609
21339
|
EXIT_DEFAULTS,
|
|
20610
21340
|
MAX_3D_NODES,
|
|
20611
21341
|
UPDATE_DEFAULTS,
|
|
21342
|
+
applySpecPatch,
|
|
20612
21343
|
buildArcPath,
|
|
20613
21344
|
buildAreaPath,
|
|
20614
21345
|
buildEdgeTooltip,
|
|
20615
21346
|
buildLinePath,
|
|
20616
21347
|
clampStaggerDelay,
|
|
20617
21348
|
clearRenderers,
|
|
21349
|
+
compareObservations,
|
|
20618
21350
|
compile,
|
|
20619
21351
|
compileBarList2 as compileBarList,
|
|
20620
21352
|
compileChart,
|
|
@@ -20626,17 +21358,23 @@ export {
|
|
|
20626
21358
|
compileTileMap2 as compileTileMap,
|
|
20627
21359
|
computeArrowheadPoints,
|
|
20628
21360
|
dedupeKeys,
|
|
21361
|
+
diagnoseChart,
|
|
20629
21362
|
evaluatePredicate,
|
|
20630
21363
|
facetMinHeight,
|
|
20631
21364
|
getChartRenderer,
|
|
20632
21365
|
heuristicMeasure,
|
|
20633
21366
|
isConditionalValueDef,
|
|
20634
21367
|
legendGap,
|
|
21368
|
+
linkedEntityKeys,
|
|
20635
21369
|
normalizeSpec,
|
|
21370
|
+
observationKey,
|
|
20636
21371
|
parseAnnotationSpans,
|
|
21372
|
+
recommendCompositions,
|
|
21373
|
+
reconcileReaderSelection,
|
|
20637
21374
|
registerChartRenderer,
|
|
20638
21375
|
resolveAnimation,
|
|
20639
21376
|
resolveConditionalValue,
|
|
21377
|
+
resolveObservationIdentities,
|
|
20640
21378
|
runBin,
|
|
20641
21379
|
runCalculate,
|
|
20642
21380
|
runFilter,
|