@opendata-ai/openchart-engine 8.0.0-rc.1 → 8.0.0-rc.10
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 +2 -8
- package/dist/index.js +793 -91
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/compile-snapshot.test.ts.snap +62 -0
- package/src/__tests__/annotation-behavior.test.ts +131 -0
- package/src/__tests__/facet.test.ts +144 -0
- package/src/annotations/geometry.ts +7 -1
- package/src/annotations/resolve-text.ts +6 -0
- package/src/compile/layer.ts +4 -0
- package/src/compile/size-scale.ts +2 -0
- package/src/compile/spec-sugar.ts +10 -1
- package/src/compile.ts +106 -8
- package/src/compiler/__tests__/keys.test.ts +3 -9
- package/src/compiler/__tests__/validate-did-you-mean.test.ts +84 -0
- package/src/compiler/__tests__/validate-near-miss.test.ts +162 -0
- package/src/compiler/__tests__/validate.test.ts +102 -0
- package/src/compiler/keys.ts +5 -4
- package/src/compiler/normalize.ts +1 -0
- package/src/compiler/validate.ts +326 -43
- package/src/geo/__tests__/compile-map.test.ts +507 -0
- package/src/geo/compile-map.ts +555 -35
- package/src/geo/projections.ts +21 -1
- package/src/geo/types.ts +2 -0
- package/src/layout/axes.ts +5 -4
- package/src/legend/compute.ts +4 -3
package/dist/index.js
CHANGED
|
@@ -244,7 +244,10 @@ function refreshConnector(connector, bounds, markerRadius) {
|
|
|
244
244
|
}
|
|
245
245
|
const exit = connectorExit(bounds, endpoint.x, endpoint.y);
|
|
246
246
|
if (!exit) return void 0;
|
|
247
|
-
const from = {
|
|
247
|
+
const from = {
|
|
248
|
+
x: exit.x + (connector.fromOffset?.dx ?? 0),
|
|
249
|
+
y: exit.y + (connector.fromOffset?.dy ?? 0)
|
|
250
|
+
};
|
|
248
251
|
const gap = connectorPullbackGap(markerRadius, connector.arrow);
|
|
249
252
|
const cdx = endpoint.x - from.x;
|
|
250
253
|
const cdy = endpoint.y - from.y;
|
|
@@ -506,6 +509,10 @@ function resolveTextAnnotation(annotation, scales, chartArea, isDark, measure =
|
|
|
506
509
|
from,
|
|
507
510
|
to,
|
|
508
511
|
endpoint,
|
|
512
|
+
fromOffset: annotation.connectorOffset?.from ? {
|
|
513
|
+
dx: annotation.connectorOffset.from.dx ?? 0,
|
|
514
|
+
dy: annotation.connectorOffset.from.dy ?? 0
|
|
515
|
+
} : void 0,
|
|
509
516
|
stroke: connectorStroke,
|
|
510
517
|
style: connectorStyle,
|
|
511
518
|
arrow: connectorArrow,
|
|
@@ -2160,6 +2167,7 @@ function serializeKeyValue(v) {
|
|
|
2160
2167
|
if (v instanceof Date) return String(v.getTime());
|
|
2161
2168
|
return String(v);
|
|
2162
2169
|
}
|
|
2170
|
+
var KEY_DEDUP_SEP = "\u2E31";
|
|
2163
2171
|
function dedupeKeys(keys) {
|
|
2164
2172
|
const counts = /* @__PURE__ */ new Map();
|
|
2165
2173
|
for (const k of keys) {
|
|
@@ -2170,7 +2178,7 @@ function dedupeKeys(keys) {
|
|
|
2170
2178
|
for (const k of keys) {
|
|
2171
2179
|
if (counts.get(k) > 1) {
|
|
2172
2180
|
const idx = seen.get(k) ?? 0;
|
|
2173
|
-
result.push(`${k}
|
|
2181
|
+
result.push(`${k}${KEY_DEDUP_SEP}${idx}`);
|
|
2174
2182
|
seen.set(k, idx + 1);
|
|
2175
2183
|
} else {
|
|
2176
2184
|
result.push(k);
|
|
@@ -5544,7 +5552,9 @@ var SIZE_SCALE_DEFAULTS = {
|
|
|
5544
5552
|
* Text glyphs. Linear, not sqrt -- a word reads by its height, which is
|
|
5545
5553
|
* linear in font size, where a disc reads by its area.
|
|
5546
5554
|
*/
|
|
5547
|
-
text: { curve: "linear", range: [11, 32] }
|
|
5555
|
+
text: { curve: "linear", range: [11, 32] },
|
|
5556
|
+
/** Map point dots. sqrt for area-proportional, tighter range than scatter. */
|
|
5557
|
+
mapPoint: { curve: "sqrt", range: [3, 20] }
|
|
5548
5558
|
};
|
|
5549
5559
|
function sizeScaleDefaultsFor(markType) {
|
|
5550
5560
|
const found = SIZE_SCALE_DEFAULTS[markType];
|
|
@@ -9836,7 +9846,16 @@ function expandChannelSugar(spec, warnings) {
|
|
|
9836
9846
|
}
|
|
9837
9847
|
return result;
|
|
9838
9848
|
}
|
|
9839
|
-
var COUNT_GROUP_CHANNELS = [
|
|
9849
|
+
var COUNT_GROUP_CHANNELS = [
|
|
9850
|
+
"x",
|
|
9851
|
+
"y",
|
|
9852
|
+
"color",
|
|
9853
|
+
"detail",
|
|
9854
|
+
"strokeDash",
|
|
9855
|
+
"facet",
|
|
9856
|
+
"row",
|
|
9857
|
+
"column"
|
|
9858
|
+
];
|
|
9840
9859
|
function expandCountAggregate(spec) {
|
|
9841
9860
|
const encoding = spec.encoding;
|
|
9842
9861
|
if (!encoding || typeof encoding !== "object") return spec;
|
|
@@ -10630,7 +10649,8 @@ function normalizeMapSpec(spec, _warnings) {
|
|
|
10630
10649
|
darkMode: spec.darkMode ?? "off",
|
|
10631
10650
|
watermark: spec.watermark ?? true,
|
|
10632
10651
|
animation: spec.animation,
|
|
10633
|
-
valueFormat: spec.valueFormat
|
|
10652
|
+
valueFormat: spec.valueFormat,
|
|
10653
|
+
points: spec.points
|
|
10634
10654
|
};
|
|
10635
10655
|
}
|
|
10636
10656
|
function normalizeBarListSpec(spec, _warnings) {
|
|
@@ -10725,6 +10745,7 @@ import {
|
|
|
10725
10745
|
MARK_ENCODING_RULES,
|
|
10726
10746
|
MARK_TYPES,
|
|
10727
10747
|
resolveSchemeName as resolveSchemeName2,
|
|
10748
|
+
SEQUENTIAL_PALETTES,
|
|
10728
10749
|
SUPPORTED_SCHEME_NAMES
|
|
10729
10750
|
} from "@opendata-ai/openchart-core";
|
|
10730
10751
|
var VALID_FIELD_TYPES = /* @__PURE__ */ new Set(["quantitative", "temporal", "nominal", "ordinal"]);
|
|
@@ -10783,6 +10804,53 @@ function didYouMean(field, columns) {
|
|
|
10783
10804
|
const match = nearestColumn(field, columns);
|
|
10784
10805
|
return match ? ` Did you mean "${match}"?` : "";
|
|
10785
10806
|
}
|
|
10807
|
+
function checkSchemeName(channelObj, channelPath, errors) {
|
|
10808
|
+
const scale = channelObj.scale;
|
|
10809
|
+
if (scale && typeof scale.scheme === "string" && !resolveSchemeName2(scale.scheme)) {
|
|
10810
|
+
errors.push({
|
|
10811
|
+
message: `Spec error: ${channelPath}.scale.scheme "${scale.scheme}" is not a supported scheme name`,
|
|
10812
|
+
path: `${channelPath}.scale.scheme`,
|
|
10813
|
+
code: "INVALID_VALUE",
|
|
10814
|
+
suggestion: `Use one of the supported scheme names: ${[...SUPPORTED_SCHEME_NAMES].join(", ")}. Or provide explicit colors via scale.range.`
|
|
10815
|
+
});
|
|
10816
|
+
}
|
|
10817
|
+
}
|
|
10818
|
+
function checkSchemeUnused(encoding, errors, familyNote, suggestion) {
|
|
10819
|
+
for (const [channel, ch] of Object.entries(encoding)) {
|
|
10820
|
+
if (!ch || typeof ch !== "object" || Array.isArray(ch)) continue;
|
|
10821
|
+
const scale = ch.scale;
|
|
10822
|
+
if (scale && typeof scale.scheme === "string") {
|
|
10823
|
+
errors.push({
|
|
10824
|
+
message: `Spec error: encoding.${channel}.scale.scheme has no effect \u2014 ${familyNote}`,
|
|
10825
|
+
path: `encoding.${channel}.scale.scheme`,
|
|
10826
|
+
code: "INVALID_VALUE",
|
|
10827
|
+
suggestion
|
|
10828
|
+
});
|
|
10829
|
+
}
|
|
10830
|
+
}
|
|
10831
|
+
}
|
|
10832
|
+
var MAP_PALETTE_NAMES = Object.keys(SEQUENTIAL_PALETTES).join(", ");
|
|
10833
|
+
function checkMapSchemeName(channelObj, channelPath, errors) {
|
|
10834
|
+
const scale = channelObj.scale;
|
|
10835
|
+
if (!scale || typeof scale.scheme !== "string") return;
|
|
10836
|
+
if (channelObj.type === "nominal" || channelObj.type === "ordinal") {
|
|
10837
|
+
errors.push({
|
|
10838
|
+
message: `Spec error: ${channelPath}.scale.scheme has no effect \u2014 categorical map colors come from scale.range and scale.domain`,
|
|
10839
|
+
path: `${channelPath}.scale.scheme`,
|
|
10840
|
+
code: "INVALID_VALUE",
|
|
10841
|
+
suggestion: "Remove scale.scheme. To customize categorical map colors, provide explicit colors via scale.range (with scale.domain for category order)."
|
|
10842
|
+
});
|
|
10843
|
+
return;
|
|
10844
|
+
}
|
|
10845
|
+
if (!(scale.scheme in SEQUENTIAL_PALETTES)) {
|
|
10846
|
+
errors.push({
|
|
10847
|
+
message: `Spec error: ${channelPath}.scale.scheme "${scale.scheme}" is not a supported map palette`,
|
|
10848
|
+
path: `${channelPath}.scale.scheme`,
|
|
10849
|
+
code: "INVALID_VALUE",
|
|
10850
|
+
suggestion: `Maps support the sequential palettes: ${MAP_PALETTE_NAMES}. The default is "blue".`
|
|
10851
|
+
});
|
|
10852
|
+
}
|
|
10853
|
+
}
|
|
10786
10854
|
var VALID_RANGE_STYLES = /* @__PURE__ */ new Set(["dumbbell", "arrow", "bar"]);
|
|
10787
10855
|
function validateRangeSpec(spec, errors) {
|
|
10788
10856
|
const encoding = spec.encoding;
|
|
@@ -11144,15 +11212,7 @@ function validateChartSpec(spec, errors) {
|
|
|
11144
11212
|
suggestion: `Use one of: ${[...VALID_FIELD_TYPES].join(", ")}`
|
|
11145
11213
|
});
|
|
11146
11214
|
}
|
|
11147
|
-
|
|
11148
|
-
if (channelScale && typeof channelScale.scheme === "string" && !resolveSchemeName2(channelScale.scheme)) {
|
|
11149
|
-
errors.push({
|
|
11150
|
-
message: `Spec error: encoding.${channel}.scale.scheme "${channelScale.scheme}" is not a supported scheme name`,
|
|
11151
|
-
path: `encoding.${channel}.scale.scheme`,
|
|
11152
|
-
code: "INVALID_VALUE",
|
|
11153
|
-
suggestion: `Use one of the supported scheme names: ${[...SUPPORTED_SCHEME_NAMES].join(", ")}. Or provide explicit colors via scale.range.`
|
|
11154
|
-
});
|
|
11155
|
-
}
|
|
11215
|
+
checkSchemeName(channelObj, `encoding.${channel}`, errors);
|
|
11156
11216
|
if (channelRule && channelObj.type && channelRule.allowedTypes.length > 0) {
|
|
11157
11217
|
if (!channelRule.allowedTypes.includes(channelObj.type)) {
|
|
11158
11218
|
errors.push({
|
|
@@ -11266,6 +11326,52 @@ function validateChartSpec(spec, errors) {
|
|
|
11266
11326
|
}
|
|
11267
11327
|
}
|
|
11268
11328
|
}
|
|
11329
|
+
for (const channel of ["row", "column"]) {
|
|
11330
|
+
const ch = encoding[channel];
|
|
11331
|
+
if (ch && typeof ch === "object") {
|
|
11332
|
+
const facetCh = ch;
|
|
11333
|
+
if (!facetCh.field || typeof facetCh.field !== "string") {
|
|
11334
|
+
errors.push({
|
|
11335
|
+
message: `Spec error: encoding.${channel}.field is required and must be a non-empty string`,
|
|
11336
|
+
path: `encoding.${channel}.field`,
|
|
11337
|
+
code: "MISSING_FIELD",
|
|
11338
|
+
suggestion: `Add a field property to encoding.${channel} (e.g. { field: "category", type: "nominal" })`
|
|
11339
|
+
});
|
|
11340
|
+
} else if (!dataColumns.has(facetCh.field) && !transformFields.has(facetCh.field)) {
|
|
11341
|
+
errors.push({
|
|
11342
|
+
message: `Spec error: encoding.${channel}.field "${facetCh.field}" does not exist in data. Available columns: ${availableColumns}`,
|
|
11343
|
+
path: `encoding.${channel}.field`,
|
|
11344
|
+
code: "DATA_FIELD_MISSING",
|
|
11345
|
+
suggestion: `Use one of the available data columns: ${availableColumns}.${didYouMean(facetCh.field, columnList)}`
|
|
11346
|
+
});
|
|
11347
|
+
}
|
|
11348
|
+
if (!facetCh.type || facetCh.type !== "nominal" && facetCh.type !== "ordinal") {
|
|
11349
|
+
errors.push({
|
|
11350
|
+
message: `Spec error: encoding.${channel}.type must be "nominal" or "ordinal"${facetCh.type ? `, got "${facetCh.type}"` : ""}`,
|
|
11351
|
+
path: `encoding.${channel}.type`,
|
|
11352
|
+
code: facetCh.type ? "INVALID_VALUE" : "MISSING_FIELD",
|
|
11353
|
+
suggestion: `Set encoding.${channel}.type to "nominal" or "ordinal"`
|
|
11354
|
+
});
|
|
11355
|
+
}
|
|
11356
|
+
if (encoding.facet && typeof encoding.facet === "object") {
|
|
11357
|
+
errors.push({
|
|
11358
|
+
message: `Spec error: encoding.${channel} and encoding.facet cannot be used together`,
|
|
11359
|
+
path: `encoding.${channel}`,
|
|
11360
|
+
code: "INVALID_VALUE",
|
|
11361
|
+
suggestion: `Use either encoding.${channel} or encoding.facet, not both`
|
|
11362
|
+
});
|
|
11363
|
+
}
|
|
11364
|
+
const otherChannel = channel === "row" ? "column" : "row";
|
|
11365
|
+
if (encoding[otherChannel]) {
|
|
11366
|
+
errors.push({
|
|
11367
|
+
message: "Spec error: encoding.row and encoding.column cannot be used together (cross-product faceting is not yet supported)",
|
|
11368
|
+
path: `encoding.${channel}`,
|
|
11369
|
+
code: "INVALID_VALUE",
|
|
11370
|
+
suggestion: "Use encoding.facet with columns for grid layout, or use only row or column"
|
|
11371
|
+
});
|
|
11372
|
+
}
|
|
11373
|
+
}
|
|
11374
|
+
}
|
|
11269
11375
|
if (spec.darkMode !== void 0 && !VALID_DARK_MODES.has(spec.darkMode)) {
|
|
11270
11376
|
errors.push({
|
|
11271
11377
|
message: `Spec error: darkMode must be "auto", "force", or "off"`,
|
|
@@ -11478,7 +11584,7 @@ function validateGraphSpec(spec, errors) {
|
|
|
11478
11584
|
message: `Spec error: edges[${i}].source "${edge.source}" does not reference an existing node id`,
|
|
11479
11585
|
path: `edges[${i}].source`,
|
|
11480
11586
|
code: "DATA_FIELD_MISSING",
|
|
11481
|
-
suggestion: `Use one of the existing node ids: ${[...nodeIds].slice(0, 5).join(", ")}${nodeIds.size > 5 ? "..." : ""}`
|
|
11587
|
+
suggestion: `Use one of the existing node ids: ${[...nodeIds].slice(0, 5).join(", ")}${nodeIds.size > 5 ? "..." : ""}.${didYouMean(edge.source, [...nodeIds])}`
|
|
11482
11588
|
});
|
|
11483
11589
|
}
|
|
11484
11590
|
if (typeof edge.target !== "string" || edge.target === "") {
|
|
@@ -11493,7 +11599,7 @@ function validateGraphSpec(spec, errors) {
|
|
|
11493
11599
|
message: `Spec error: edges[${i}].target "${edge.target}" does not reference an existing node id`,
|
|
11494
11600
|
path: `edges[${i}].target`,
|
|
11495
11601
|
code: "DATA_FIELD_MISSING",
|
|
11496
|
-
suggestion: `Use one of the existing node ids: ${[...nodeIds].slice(0, 5).join(", ")}${nodeIds.size > 5 ? "..." : ""}`
|
|
11602
|
+
suggestion: `Use one of the existing node ids: ${[...nodeIds].slice(0, 5).join(", ")}${nodeIds.size > 5 ? "..." : ""}.${didYouMean(edge.target, [...nodeIds])}`
|
|
11497
11603
|
});
|
|
11498
11604
|
}
|
|
11499
11605
|
}
|
|
@@ -11511,7 +11617,7 @@ function validateGraphSpec(spec, errors) {
|
|
|
11511
11617
|
message: `Spec error: encoding.${channel}.field "${ch.field}" does not exist on nodes. Available fields: ${[...nodeFields].join(", ")}`,
|
|
11512
11618
|
path: `encoding.${channel}.field`,
|
|
11513
11619
|
code: "DATA_FIELD_MISSING",
|
|
11514
|
-
suggestion: `Use one of the node fields: ${[...nodeFields].join(", ")}`
|
|
11620
|
+
suggestion: `Use one of the node fields: ${[...nodeFields].join(", ")}.${didYouMean(ch.field, [...nodeFields])}`
|
|
11515
11621
|
});
|
|
11516
11622
|
}
|
|
11517
11623
|
}
|
|
@@ -11523,10 +11629,16 @@ function validateGraphSpec(spec, errors) {
|
|
|
11523
11629
|
message: `Spec error: encoding.${channel}.field "${ch.field}" does not exist on edges. Available fields: ${[...edgeFields].join(", ")}`,
|
|
11524
11630
|
path: `encoding.${channel}.field`,
|
|
11525
11631
|
code: "DATA_FIELD_MISSING",
|
|
11526
|
-
suggestion: `Use one of the edge fields: ${[...edgeFields].join(", ")}`
|
|
11632
|
+
suggestion: `Use one of the edge fields: ${[...edgeFields].join(", ")}.${didYouMean(ch.field, [...edgeFields])}`
|
|
11527
11633
|
});
|
|
11528
11634
|
}
|
|
11529
11635
|
}
|
|
11636
|
+
checkSchemeUnused(
|
|
11637
|
+
encoding,
|
|
11638
|
+
errors,
|
|
11639
|
+
"graph colors come from scale.range and scale.domain",
|
|
11640
|
+
"Remove scale.scheme. To customize graph colors, provide explicit colors via scale.range."
|
|
11641
|
+
);
|
|
11530
11642
|
}
|
|
11531
11643
|
if (spec.layout && typeof spec.layout === "object") {
|
|
11532
11644
|
const layout = spec.layout;
|
|
@@ -11580,7 +11692,8 @@ function validateSankeySpec(spec, errors) {
|
|
|
11580
11692
|
}
|
|
11581
11693
|
const encoding = spec.encoding;
|
|
11582
11694
|
const dataColumns = new Set(Object.keys(firstRow));
|
|
11583
|
-
const
|
|
11695
|
+
const columnList = [...dataColumns];
|
|
11696
|
+
const availableColumns = columnList.join(", ");
|
|
11584
11697
|
for (const channel of ["source", "target", "value"]) {
|
|
11585
11698
|
const ch = encoding[channel];
|
|
11586
11699
|
if (!ch || typeof ch !== "object") {
|
|
@@ -11588,7 +11701,7 @@ function validateSankeySpec(spec, errors) {
|
|
|
11588
11701
|
message: `Spec error: sankey encoding requires "${channel}" channel`,
|
|
11589
11702
|
path: `encoding.${channel}`,
|
|
11590
11703
|
code: "MISSING_FIELD",
|
|
11591
|
-
suggestion: `Add encoding.${channel} with a field from your data (${availableColumns}). Example: ${channel}: { field: "${[
|
|
11704
|
+
suggestion: `Add encoding.${channel} with a field from your data (${availableColumns}). Example: ${channel}: { field: "${columnList[0] ?? "myField"}", type: "${channel === "value" ? "quantitative" : "nominal"}" }`
|
|
11592
11705
|
});
|
|
11593
11706
|
continue;
|
|
11594
11707
|
}
|
|
@@ -11606,10 +11719,16 @@ function validateSankeySpec(spec, errors) {
|
|
|
11606
11719
|
message: `Spec error: encoding.${channel}.field "${ch.field}" does not exist in data. Available columns: ${availableColumns}`,
|
|
11607
11720
|
path: `encoding.${channel}.field`,
|
|
11608
11721
|
code: "DATA_FIELD_MISSING",
|
|
11609
|
-
suggestion: `Use one of the available data columns: ${availableColumns}`
|
|
11722
|
+
suggestion: `Use one of the available data columns: ${availableColumns}.${didYouMean(ch.field, columnList)}`
|
|
11610
11723
|
});
|
|
11611
11724
|
}
|
|
11612
11725
|
}
|
|
11726
|
+
checkSchemeUnused(
|
|
11727
|
+
encoding,
|
|
11728
|
+
errors,
|
|
11729
|
+
"sankey node colors cycle the theme categorical palette",
|
|
11730
|
+
"Remove scale.scheme. To customize sankey colors, set theme.colors.categorical."
|
|
11731
|
+
);
|
|
11613
11732
|
if (spec.darkMode !== void 0 && !VALID_DARK_MODES.has(spec.darkMode)) {
|
|
11614
11733
|
errors.push({
|
|
11615
11734
|
message: 'Spec error: darkMode must be "auto", "force", or "off"',
|
|
@@ -11669,7 +11788,8 @@ function validateTileMapSpec(spec, errors) {
|
|
|
11669
11788
|
}
|
|
11670
11789
|
const encoding = spec.encoding;
|
|
11671
11790
|
const dataColumns = new Set(Object.keys(firstRow));
|
|
11672
|
-
const
|
|
11791
|
+
const columnList = [...dataColumns];
|
|
11792
|
+
const availableColumns = columnList.join(", ");
|
|
11673
11793
|
for (const channel of ["state", "value"]) {
|
|
11674
11794
|
const ch = encoding[channel];
|
|
11675
11795
|
if (!ch || typeof ch !== "object") {
|
|
@@ -11677,7 +11797,7 @@ function validateTileMapSpec(spec, errors) {
|
|
|
11677
11797
|
message: `Spec error: tilemap encoding requires "${channel}" channel`,
|
|
11678
11798
|
path: `encoding.${channel}`,
|
|
11679
11799
|
code: "MISSING_FIELD",
|
|
11680
|
-
suggestion: `Add encoding.${channel} with a field from your data (${availableColumns}). Example: ${channel}: { field: "${[
|
|
11800
|
+
suggestion: `Add encoding.${channel} with a field from your data (${availableColumns}). Example: ${channel}: { field: "${columnList[0] ?? "myField"}", type: "${channel === "value" ? "quantitative" : "nominal"}" }`
|
|
11681
11801
|
});
|
|
11682
11802
|
continue;
|
|
11683
11803
|
}
|
|
@@ -11695,11 +11815,19 @@ function validateTileMapSpec(spec, errors) {
|
|
|
11695
11815
|
message: `Spec error: encoding.${channel}.field "${ch.field}" does not exist in data. Available columns: ${availableColumns}`,
|
|
11696
11816
|
path: `encoding.${channel}.field`,
|
|
11697
11817
|
code: "DATA_FIELD_MISSING",
|
|
11698
|
-
suggestion: `Use one of the available data columns: ${availableColumns}`
|
|
11818
|
+
suggestion: `Use one of the available data columns: ${availableColumns}.${didYouMean(ch.field, columnList)}`
|
|
11699
11819
|
});
|
|
11700
11820
|
}
|
|
11701
11821
|
}
|
|
11702
11822
|
}
|
|
11823
|
+
if (spec.encoding && typeof spec.encoding === "object") {
|
|
11824
|
+
checkSchemeUnused(
|
|
11825
|
+
spec.encoding,
|
|
11826
|
+
errors,
|
|
11827
|
+
"tilemap fills come from the top-level palette property",
|
|
11828
|
+
`Remove scale.scheme and set the top-level palette property instead, e.g. palette: "green". Supported: ${MAP_PALETTE_NAMES}.`
|
|
11829
|
+
);
|
|
11830
|
+
}
|
|
11703
11831
|
if (spec.darkMode !== void 0 && !VALID_DARK_MODES.has(spec.darkMode)) {
|
|
11704
11832
|
errors.push({
|
|
11705
11833
|
message: 'Spec error: darkMode must be "auto", "force", or "off"',
|
|
@@ -11738,6 +11866,16 @@ function validateMapSpec(spec, errors) {
|
|
|
11738
11866
|
});
|
|
11739
11867
|
return;
|
|
11740
11868
|
}
|
|
11869
|
+
const mapEncoding = spec.encoding;
|
|
11870
|
+
if (mapEncoding.color && typeof mapEncoding.color === "object") {
|
|
11871
|
+
checkMapSchemeName(mapEncoding.color, "encoding.color", errors);
|
|
11872
|
+
}
|
|
11873
|
+
if (spec.points && typeof spec.points === "object") {
|
|
11874
|
+
const pointsLayer = spec.points;
|
|
11875
|
+
if (pointsLayer.color && typeof pointsLayer.color === "object") {
|
|
11876
|
+
checkMapSchemeName(pointsLayer.color, "points.color", errors);
|
|
11877
|
+
}
|
|
11878
|
+
}
|
|
11741
11879
|
if ("focus" in geo && geo.focus !== void 0 && geo.focus !== null) {
|
|
11742
11880
|
const focus = geo.focus;
|
|
11743
11881
|
if (typeof focus === "string" || typeof focus === "number") {
|
|
@@ -11754,12 +11892,31 @@ function validateMapSpec(spec, errors) {
|
|
|
11754
11892
|
}
|
|
11755
11893
|
} else if (typeof focus === "object") {
|
|
11756
11894
|
const focusObj = focus;
|
|
11757
|
-
if (
|
|
11895
|
+
if ("points" in focusObj) {
|
|
11896
|
+
const pts = focusObj.points;
|
|
11897
|
+
const isSubset = typeof pts === "object" && pts !== null && typeof pts.field === "string" && "value" in pts;
|
|
11898
|
+
if (pts !== true && !isSubset) {
|
|
11899
|
+
errors.push({
|
|
11900
|
+
message: "Spec error: geo.focus.points must be true or { field: string, value: string | number }",
|
|
11901
|
+
path: "geo.focus.points",
|
|
11902
|
+
code: "INVALID_TYPE",
|
|
11903
|
+
suggestion: 'Use { points: true } to fit all points, or { points: { field: "rating", value: "F" } } to fit a subset.'
|
|
11904
|
+
});
|
|
11905
|
+
}
|
|
11906
|
+
if (focusObj.padding !== void 0 && typeof focusObj.padding !== "number") {
|
|
11907
|
+
errors.push({
|
|
11908
|
+
message: "Spec error: geo.focus.padding must be a number",
|
|
11909
|
+
path: "geo.focus.padding",
|
|
11910
|
+
code: "INVALID_TYPE",
|
|
11911
|
+
suggestion: "Provide padding as a number in map-local units, e.g. padding: 16"
|
|
11912
|
+
});
|
|
11913
|
+
}
|
|
11914
|
+
} else if (!("features" in focusObj) || focusObj.features == null) {
|
|
11758
11915
|
errors.push({
|
|
11759
|
-
message: 'Spec error: geo.focus object must have a "features" property',
|
|
11916
|
+
message: 'Spec error: geo.focus object must have a "features" or "points" property',
|
|
11760
11917
|
path: "geo.focus.features",
|
|
11761
11918
|
code: "MISSING_FIELD",
|
|
11762
|
-
suggestion: 'Provide feature id(s) to focus on, e.g.
|
|
11919
|
+
suggestion: 'Provide feature id(s) to focus on, e.g. { features: "36", padding: 32 }, or { points: true } to fit the point cluster.'
|
|
11763
11920
|
});
|
|
11764
11921
|
} else {
|
|
11765
11922
|
const feats = focusObj.features;
|
|
@@ -11793,33 +11950,99 @@ function validateMapSpec(spec, errors) {
|
|
|
11793
11950
|
}
|
|
11794
11951
|
} else {
|
|
11795
11952
|
errors.push({
|
|
11796
|
-
message: "Spec error: geo.focus must be a string, number, array of ids, or object with { features, padding? }",
|
|
11953
|
+
message: "Spec error: geo.focus must be a string, number, array of ids, or object with { features, padding? } or { points: true, padding? }",
|
|
11797
11954
|
path: "geo.focus",
|
|
11798
11955
|
code: "INVALID_TYPE",
|
|
11799
|
-
suggestion: 'Use a feature id, array of ids,
|
|
11956
|
+
suggestion: 'Use a feature id, array of ids, { features: "36", padding: 16 }, or { points: true }. Pass null to clear focus.'
|
|
11800
11957
|
});
|
|
11801
11958
|
}
|
|
11802
11959
|
}
|
|
11803
11960
|
const encoding = spec.encoding;
|
|
11804
|
-
|
|
11805
|
-
|
|
11806
|
-
|
|
11961
|
+
const hasPoints = spec.points && typeof spec.points === "object" && !Array.isArray(spec.points);
|
|
11962
|
+
const specData = spec.data;
|
|
11963
|
+
const isBasemapOnly = hasPoints && (!specData || specData.length === 0);
|
|
11964
|
+
const keyCh = encoding.key;
|
|
11965
|
+
if (!isBasemapOnly) {
|
|
11966
|
+
if (!keyCh || typeof keyCh !== "object") {
|
|
11807
11967
|
errors.push({
|
|
11808
|
-
message:
|
|
11809
|
-
path:
|
|
11968
|
+
message: 'Spec error: map encoding requires "key" channel',
|
|
11969
|
+
path: "encoding.key",
|
|
11810
11970
|
code: "MISSING_FIELD",
|
|
11811
|
-
suggestion:
|
|
11971
|
+
suggestion: 'Add encoding.key, e.g. key: { field: "id", type: "nominal" }'
|
|
11972
|
+
});
|
|
11973
|
+
} else if (!keyCh.field || typeof keyCh.field !== "string") {
|
|
11974
|
+
errors.push({
|
|
11975
|
+
message: 'Spec error: encoding.key must have a "field" string',
|
|
11976
|
+
path: "encoding.key.field",
|
|
11977
|
+
code: "MISSING_FIELD",
|
|
11978
|
+
suggestion: 'Add a field name, e.g. key: { field: "id" }'
|
|
11812
11979
|
});
|
|
11813
|
-
continue;
|
|
11814
11980
|
}
|
|
11815
|
-
|
|
11981
|
+
}
|
|
11982
|
+
const colorCh = encoding.color;
|
|
11983
|
+
if (!hasPoints) {
|
|
11984
|
+
if (!colorCh || typeof colorCh !== "object") {
|
|
11816
11985
|
errors.push({
|
|
11817
|
-
message:
|
|
11818
|
-
path:
|
|
11986
|
+
message: 'Spec error: map encoding requires "color" channel (or add a "points" layer for basemap-only mode)',
|
|
11987
|
+
path: "encoding.color",
|
|
11819
11988
|
code: "MISSING_FIELD",
|
|
11820
|
-
suggestion:
|
|
11989
|
+
suggestion: 'Add encoding.color, e.g. color: { field: "value", type: "quantitative" }'
|
|
11990
|
+
});
|
|
11991
|
+
} else if (!colorCh.field || typeof colorCh.field !== "string") {
|
|
11992
|
+
errors.push({
|
|
11993
|
+
message: 'Spec error: encoding.color must have a "field" string',
|
|
11994
|
+
path: "encoding.color.field",
|
|
11995
|
+
code: "MISSING_FIELD",
|
|
11996
|
+
suggestion: 'Add a field name, e.g. color: { field: "value" }'
|
|
11821
11997
|
});
|
|
11822
11998
|
}
|
|
11999
|
+
} else if (colorCh && typeof colorCh === "object" && (!colorCh.field || typeof colorCh.field !== "string")) {
|
|
12000
|
+
errors.push({
|
|
12001
|
+
message: 'Spec error: encoding.color must have a "field" string when provided',
|
|
12002
|
+
path: "encoding.color.field",
|
|
12003
|
+
code: "MISSING_FIELD",
|
|
12004
|
+
suggestion: 'Add a field name, e.g. color: { field: "value" }'
|
|
12005
|
+
});
|
|
12006
|
+
}
|
|
12007
|
+
if (hasPoints) {
|
|
12008
|
+
const points = spec.points;
|
|
12009
|
+
if (!Array.isArray(points.data)) {
|
|
12010
|
+
errors.push({
|
|
12011
|
+
message: "Spec error: points.data must be an array of data rows",
|
|
12012
|
+
path: "points.data",
|
|
12013
|
+
code: "INVALID_TYPE",
|
|
12014
|
+
suggestion: "Provide data as an array of objects with lat/lng fields"
|
|
12015
|
+
});
|
|
12016
|
+
}
|
|
12017
|
+
for (const ch of ["longitude", "latitude"]) {
|
|
12018
|
+
const enc = points[ch];
|
|
12019
|
+
if (!enc || typeof enc !== "object") {
|
|
12020
|
+
errors.push({
|
|
12021
|
+
message: `Spec error: points requires "${ch}" encoding channel`,
|
|
12022
|
+
path: `points.${ch}`,
|
|
12023
|
+
code: "MISSING_FIELD",
|
|
12024
|
+
suggestion: `Add points.${ch}, e.g. ${ch}: { field: "${ch === "longitude" ? "lon" : "lat"}", type: "quantitative" }`
|
|
12025
|
+
});
|
|
12026
|
+
} else if (!enc.field || typeof enc.field !== "string") {
|
|
12027
|
+
errors.push({
|
|
12028
|
+
message: `Spec error: points.${ch} must have a "field" string`,
|
|
12029
|
+
path: `points.${ch}.field`,
|
|
12030
|
+
code: "MISSING_FIELD",
|
|
12031
|
+
suggestion: `Add a field name, e.g. ${ch}: { field: "${ch === "longitude" ? "lon" : "lat"}" }`
|
|
12032
|
+
});
|
|
12033
|
+
}
|
|
12034
|
+
}
|
|
12035
|
+
for (const ch of ["size", "color", "key"]) {
|
|
12036
|
+
const enc = points[ch];
|
|
12037
|
+
if (enc && typeof enc === "object" && (!enc.field || typeof enc.field !== "string")) {
|
|
12038
|
+
errors.push({
|
|
12039
|
+
message: `Spec error: points.${ch} must have a "field" string when provided`,
|
|
12040
|
+
path: `points.${ch}.field`,
|
|
12041
|
+
code: "MISSING_FIELD",
|
|
12042
|
+
suggestion: `Add a field name, e.g. ${ch}: { field: "myField" }`
|
|
12043
|
+
});
|
|
12044
|
+
}
|
|
12045
|
+
}
|
|
11823
12046
|
}
|
|
11824
12047
|
}
|
|
11825
12048
|
function validateBarListSpec(spec, errors) {
|
|
@@ -11862,7 +12085,8 @@ function validateBarListSpec(spec, errors) {
|
|
|
11862
12085
|
}
|
|
11863
12086
|
const encoding = spec.encoding;
|
|
11864
12087
|
const dataColumns = new Set(Object.keys(firstRow));
|
|
11865
|
-
const
|
|
12088
|
+
const columnList = [...dataColumns];
|
|
12089
|
+
const availableColumns = columnList.join(", ");
|
|
11866
12090
|
for (const channel of ["label", "value"]) {
|
|
11867
12091
|
const ch = encoding[channel];
|
|
11868
12092
|
if (!ch || typeof ch !== "object") {
|
|
@@ -11870,7 +12094,7 @@ function validateBarListSpec(spec, errors) {
|
|
|
11870
12094
|
message: `Spec error: barlist encoding requires "${channel}" channel`,
|
|
11871
12095
|
path: `encoding.${channel}`,
|
|
11872
12096
|
code: "MISSING_FIELD",
|
|
11873
|
-
suggestion: `Add encoding.${channel} with a field from your data (${availableColumns}). Example: ${channel}: { field: "${[
|
|
12097
|
+
suggestion: `Add encoding.${channel} with a field from your data (${availableColumns}). Example: ${channel}: { field: "${columnList[0] ?? "myField"}", type: "${channel === "value" ? "quantitative" : "nominal"}" }`
|
|
11874
12098
|
});
|
|
11875
12099
|
continue;
|
|
11876
12100
|
}
|
|
@@ -11888,7 +12112,7 @@ function validateBarListSpec(spec, errors) {
|
|
|
11888
12112
|
message: `Spec error: encoding.${channel}.field "${ch.field}" does not exist in data. Available columns: ${availableColumns}`,
|
|
11889
12113
|
path: `encoding.${channel}.field`,
|
|
11890
12114
|
code: "DATA_FIELD_MISSING",
|
|
11891
|
-
suggestion: `Use one of the available data columns: ${availableColumns}`
|
|
12115
|
+
suggestion: `Use one of the available data columns: ${availableColumns}.${didYouMean(ch.field, columnList)}`
|
|
11892
12116
|
});
|
|
11893
12117
|
}
|
|
11894
12118
|
}
|
|
@@ -11901,10 +12125,16 @@ function validateBarListSpec(spec, errors) {
|
|
|
11901
12125
|
message: `Spec error: encoding.${channel}.field "${field}" does not exist in data. Available columns: ${availableColumns}`,
|
|
11902
12126
|
path: `encoding.${channel}.field`,
|
|
11903
12127
|
code: "DATA_FIELD_MISSING",
|
|
11904
|
-
suggestion: `Use one of the available data columns: ${availableColumns}`
|
|
12128
|
+
suggestion: `Use one of the available data columns: ${availableColumns}.${didYouMean(field, columnList)}`
|
|
11905
12129
|
});
|
|
11906
12130
|
}
|
|
11907
12131
|
}
|
|
12132
|
+
checkSchemeUnused(
|
|
12133
|
+
encoding,
|
|
12134
|
+
errors,
|
|
12135
|
+
"bar list colors cycle the built-in barlist palette",
|
|
12136
|
+
"Remove scale.scheme. Bar list colors are not configurable via encoding; they cycle the built-in palette."
|
|
12137
|
+
);
|
|
11908
12138
|
if (spec.darkMode !== void 0 && !VALID_DARK_MODES.has(spec.darkMode)) {
|
|
11909
12139
|
errors.push({
|
|
11910
12140
|
message: 'Spec error: darkMode must be "auto", "force", or "off"',
|
|
@@ -13649,7 +13879,8 @@ function computeLegendContent(spec, strategy, theme, availableWidth, availableHe
|
|
|
13649
13879
|
fill: theme.colors.text,
|
|
13650
13880
|
lineHeight: 1.3
|
|
13651
13881
|
};
|
|
13652
|
-
const
|
|
13882
|
+
const specPosition = spec.legend?.position === "top-left" ? "top" : spec.legend?.position;
|
|
13883
|
+
const resolvedPosition = specPosition ?? (strategy.legendPosition === "right" ? "right" : "top");
|
|
13653
13884
|
if (entries.length === 0) {
|
|
13654
13885
|
return emptyContent(resolvedPosition, theme);
|
|
13655
13886
|
}
|
|
@@ -13994,6 +14225,7 @@ import {
|
|
|
13994
14225
|
estimateTextWidth as estimateTextWidth15,
|
|
13995
14226
|
getAxisTitleOffset,
|
|
13996
14227
|
truncateRotatedLabel,
|
|
14228
|
+
X_AXIS_TITLE_BAND,
|
|
13997
14229
|
X_AXIS_TITLE_BAND_ROTATED
|
|
13998
14230
|
} from "@opendata-ai/openchart-core";
|
|
13999
14231
|
|
|
@@ -14606,10 +14838,12 @@ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContex
|
|
|
14606
14838
|
measure: (t, fs, fw) => measureLabel(t, fs, fw, measureText)
|
|
14607
14839
|
});
|
|
14608
14840
|
if (result.x.label) {
|
|
14609
|
-
const
|
|
14841
|
+
const titleBand = isRotated ? X_AXIS_TITLE_BAND_ROTATED : X_AXIS_TITLE_BAND;
|
|
14842
|
+
const tickBand = result.x.extent - titleBand;
|
|
14843
|
+
const titleY = isRotated ? tickBand + 14 : tickBand + 9;
|
|
14610
14844
|
result.x.titlePosition = {
|
|
14611
14845
|
x: chartArea.x + chartArea.width / 2,
|
|
14612
|
-
y: chartArea.y + chartArea.height +
|
|
14846
|
+
y: chartArea.y + chartArea.height + titleY
|
|
14613
14847
|
};
|
|
14614
14848
|
}
|
|
14615
14849
|
}
|
|
@@ -15453,6 +15687,10 @@ function buildPrimarySpec(leaves, layerSpec) {
|
|
|
15453
15687
|
...leaves[0],
|
|
15454
15688
|
data: allData,
|
|
15455
15689
|
chrome: layerSpec.chrome ?? leaves[0].chrome,
|
|
15690
|
+
// Annotations come from the parent LayerSpec when present, otherwise
|
|
15691
|
+
// the first flattened leaf's. Later leaves' annotations are silently
|
|
15692
|
+
// dropped. The dual-axis path (compileLayerIndependent) explicitly
|
|
15693
|
+
// clears the secondary axis spec's annotations.
|
|
15456
15694
|
annotations: layerSpec.annotations ?? leaves[0].annotations,
|
|
15457
15695
|
labels: layerSpec.labels ?? leaves[0].labels,
|
|
15458
15696
|
legend: layerSpec.legend ?? leaves[0].legend,
|
|
@@ -15687,7 +15925,7 @@ import {
|
|
|
15687
15925
|
HPAD_COMPACT_FRACTION as HPAD_COMPACT_FRACTION2,
|
|
15688
15926
|
HPAD_COMPACT_MIN as HPAD_COMPACT_MIN2,
|
|
15689
15927
|
resolveTheme as resolveTheme3,
|
|
15690
|
-
SEQUENTIAL_PALETTES
|
|
15928
|
+
SEQUENTIAL_PALETTES as SEQUENTIAL_PALETTES2
|
|
15691
15929
|
} from "@opendata-ai/openchart-core";
|
|
15692
15930
|
|
|
15693
15931
|
// ../../node_modules/.bun/d3-geo@3.1.1/node_modules/d3-geo/src/math.js
|
|
@@ -17665,13 +17903,32 @@ function joinDataToFeatures(features, data, dataKeyField, featureIdField) {
|
|
|
17665
17903
|
|
|
17666
17904
|
// src/geo/projections.ts
|
|
17667
17905
|
var MERCATOR_FIT_MIN_LAT = -60;
|
|
17668
|
-
function createProjection(type, width, height, geojson) {
|
|
17906
|
+
function createProjection(type, width, height, geojson, inset = 0) {
|
|
17669
17907
|
if (type === "identity") {
|
|
17908
|
+
if (inset > 0) {
|
|
17909
|
+
return identity_default4().fitExtent(
|
|
17910
|
+
[
|
|
17911
|
+
[inset, inset],
|
|
17912
|
+
[width - inset, height - inset]
|
|
17913
|
+
],
|
|
17914
|
+
geojson
|
|
17915
|
+
);
|
|
17916
|
+
}
|
|
17670
17917
|
return identity_default4().fitSize([width, height], geojson);
|
|
17671
17918
|
}
|
|
17672
17919
|
const projection2 = type === "albersUsa" ? albersUsa_default() : type === "equalEarth" ? equalEarth_default() : mercator_default();
|
|
17673
17920
|
const fitGeometry = type === "mercator" ? excludePolarFeatures(geojson) : geojson;
|
|
17674
|
-
|
|
17921
|
+
if (inset > 0) {
|
|
17922
|
+
projection2.fitExtent(
|
|
17923
|
+
[
|
|
17924
|
+
[inset, inset],
|
|
17925
|
+
[width - inset, height - inset]
|
|
17926
|
+
],
|
|
17927
|
+
fitGeometry
|
|
17928
|
+
);
|
|
17929
|
+
} else {
|
|
17930
|
+
projection2.fitSize([width, height], fitGeometry);
|
|
17931
|
+
}
|
|
17675
17932
|
return projection2;
|
|
17676
17933
|
}
|
|
17677
17934
|
function excludePolarFeatures(geojson) {
|
|
@@ -17691,6 +17948,7 @@ function excludePolarFeatures(geojson) {
|
|
|
17691
17948
|
}
|
|
17692
17949
|
|
|
17693
17950
|
// src/geo/compile-map.ts
|
|
17951
|
+
var OVERLAY_LEGEND_INSET = 18;
|
|
17694
17952
|
function validateGeoFeatures(geo) {
|
|
17695
17953
|
if (!geo.features) {
|
|
17696
17954
|
throw new Error(
|
|
@@ -17763,11 +18021,13 @@ function compileMap(spec, options) {
|
|
|
17763
18021
|
const compileWarnings = [];
|
|
17764
18022
|
const showLegend = mapSpec.legend?.show !== false;
|
|
17765
18023
|
const colorEncoding = mapSpec.encoding.color;
|
|
17766
|
-
const
|
|
18024
|
+
const showChoroplethLegend = showLegend && colorEncoding?.legend !== null;
|
|
18025
|
+
const isQuantitative = colorEncoding?.type === "quantitative";
|
|
17767
18026
|
const legendPosition = mapSpec.legend?.position === "bottom" ? "bottom" : "top";
|
|
18027
|
+
const pointLegendOverlay = mapSpec.legend?.position === "top-left";
|
|
17768
18028
|
let legendBlockHeight = 0;
|
|
17769
18029
|
let continuousContent = null;
|
|
17770
|
-
if (
|
|
18030
|
+
if (showChoroplethLegend && colorEncoding && isQuantitative) {
|
|
17771
18031
|
const colorValues = [];
|
|
17772
18032
|
for (const row of mapSpec.data) {
|
|
17773
18033
|
const v = row[colorEncoding.field];
|
|
@@ -17790,17 +18050,37 @@ function compileMap(spec, options) {
|
|
|
17790
18050
|
const labelRowHeight = Math.ceil(theme.fonts.sizes.small * 1.3);
|
|
17791
18051
|
legendBlockHeight = continuousContent.barHeight + CONTINUOUS_LABEL_GAP + labelRowHeight;
|
|
17792
18052
|
}
|
|
17793
|
-
} else if (
|
|
18053
|
+
} else if (showChoroplethLegend && colorEncoding) {
|
|
17794
18054
|
const labelHeight = Math.ceil(theme.fonts.sizes.small * 1.3);
|
|
17795
18055
|
legendBlockHeight = Math.max(10, labelHeight) + 6;
|
|
17796
18056
|
}
|
|
18057
|
+
if (showLegend && !pointLegendOverlay && mapSpec.points?.color) {
|
|
18058
|
+
if (mapSpec.points.color.type === "quantitative") {
|
|
18059
|
+
const labelRowHeight = Math.ceil(theme.fonts.sizes.small * 1.3);
|
|
18060
|
+
legendBlockHeight += CONTINUOUS_BAR_HEIGHT + CONTINUOUS_LABEL_GAP + labelRowHeight + 8;
|
|
18061
|
+
} else {
|
|
18062
|
+
const ptLabelHeight = Math.ceil(theme.fonts.sizes.small * 1.3);
|
|
18063
|
+
legendBlockHeight += Math.max(10, ptLabelHeight) + 6 + 8;
|
|
18064
|
+
}
|
|
18065
|
+
}
|
|
17797
18066
|
const legendReserveGap = legendBlockHeight > 0 ? 8 : 0;
|
|
17798
18067
|
const mapAreaHeight = fullArea.height - legendBlockHeight - legendReserveGap;
|
|
17799
18068
|
if (mapAreaHeight <= 0) {
|
|
17800
18069
|
return emptyLayout3(chrome, theme, options, watermark);
|
|
17801
18070
|
}
|
|
17802
18071
|
const projectionType = mapSpec.geo.projection;
|
|
17803
|
-
|
|
18072
|
+
let pointInset = 0;
|
|
18073
|
+
if (mapSpec.points) {
|
|
18074
|
+
const sizeRange = mapSpec.points.size?.scale?.range;
|
|
18075
|
+
pointInset = sizeRange ? sizeRange[1] : SIZE_SCALE_DEFAULTS.mapPoint.range[1];
|
|
18076
|
+
}
|
|
18077
|
+
const projection2 = createProjection(
|
|
18078
|
+
projectionType,
|
|
18079
|
+
fullArea.width,
|
|
18080
|
+
mapAreaHeight,
|
|
18081
|
+
geoCollection,
|
|
18082
|
+
pointInset
|
|
18083
|
+
);
|
|
17804
18084
|
const pathGen = path_default(projection2);
|
|
17805
18085
|
if (projectionType !== "identity") {
|
|
17806
18086
|
for (const feat of geoFeatures) {
|
|
@@ -17815,17 +18095,22 @@ function compileMap(spec, options) {
|
|
|
17815
18095
|
}
|
|
17816
18096
|
}
|
|
17817
18097
|
}
|
|
17818
|
-
|
|
17819
|
-
|
|
17820
|
-
|
|
17821
|
-
|
|
17822
|
-
|
|
17823
|
-
|
|
17824
|
-
|
|
17825
|
-
|
|
17826
|
-
|
|
17827
|
-
|
|
17828
|
-
|
|
18098
|
+
let joined = /* @__PURE__ */ new Map();
|
|
18099
|
+
if (mapSpec.encoding.key) {
|
|
18100
|
+
const featureObjs = geoFeatures.map((f) => ({
|
|
18101
|
+
id: f.id,
|
|
18102
|
+
properties: f.properties ?? {}
|
|
18103
|
+
}));
|
|
18104
|
+
const joinResult = joinDataToFeatures(
|
|
18105
|
+
featureObjs,
|
|
18106
|
+
mapSpec.data,
|
|
18107
|
+
mapSpec.encoding.key.field,
|
|
18108
|
+
mapSpec.geo.idField
|
|
18109
|
+
);
|
|
18110
|
+
joined = joinResult.joined;
|
|
18111
|
+
const filteredJoinWarnings = mapSpec.data.length === 0 && mapSpec.points ? joinResult.warnings.filter((w) => w.code !== "UNMATCHED_FEATURES") : joinResult.warnings;
|
|
18112
|
+
compileWarnings.push(...filteredJoinWarnings);
|
|
18113
|
+
}
|
|
17829
18114
|
const droppedFeatures = [];
|
|
17830
18115
|
const formatter = buildD3Formatter2(mapSpec.valueFormat) ?? formatNumber3;
|
|
17831
18116
|
const neutralFill = isDarkMode ? "#2a2a2a" : "#e8e8e8";
|
|
@@ -17833,13 +18118,21 @@ function compileMap(spec, options) {
|
|
|
17833
18118
|
let featureMarks;
|
|
17834
18119
|
let continuousLegend = null;
|
|
17835
18120
|
let categoricalLegend = null;
|
|
17836
|
-
if (
|
|
18121
|
+
if (!colorEncoding) {
|
|
18122
|
+
featureMarks = buildBasemapMarks({
|
|
18123
|
+
geoFeatures,
|
|
18124
|
+
pathGen,
|
|
18125
|
+
neutralFill,
|
|
18126
|
+
neutralStroke,
|
|
18127
|
+
droppedFeatures
|
|
18128
|
+
});
|
|
18129
|
+
} else if (isQuantitative) {
|
|
17837
18130
|
const result = buildQuantitativeMarks({
|
|
17838
18131
|
geoFeatures,
|
|
17839
18132
|
pathGen,
|
|
17840
18133
|
joined,
|
|
17841
18134
|
colorField: colorEncoding.field,
|
|
17842
|
-
palette:
|
|
18135
|
+
palette: colorEncoding.scale?.scheme ?? "blue",
|
|
17843
18136
|
isDarkMode: !!isDarkMode,
|
|
17844
18137
|
neutralFill,
|
|
17845
18138
|
neutralStroke,
|
|
@@ -17847,7 +18140,7 @@ function compileMap(spec, options) {
|
|
|
17847
18140
|
droppedFeatures
|
|
17848
18141
|
});
|
|
17849
18142
|
featureMarks = result.marks;
|
|
17850
|
-
if (
|
|
18143
|
+
if (showChoroplethLegend && continuousContent) {
|
|
17851
18144
|
const labelStyle = {
|
|
17852
18145
|
fontFamily: theme.fonts.family,
|
|
17853
18146
|
fontSize: theme.fonts.sizes.small,
|
|
@@ -17889,6 +18182,7 @@ function compileMap(spec, options) {
|
|
|
17889
18182
|
pathGen,
|
|
17890
18183
|
joined,
|
|
17891
18184
|
colorField: colorEncoding.field,
|
|
18185
|
+
colorScale: colorEncoding.scale,
|
|
17892
18186
|
isDarkMode: !!isDarkMode,
|
|
17893
18187
|
neutralFill,
|
|
17894
18188
|
neutralStroke,
|
|
@@ -17896,7 +18190,8 @@ function compileMap(spec, options) {
|
|
|
17896
18190
|
fullArea,
|
|
17897
18191
|
mapAreaHeight,
|
|
17898
18192
|
legendReserveGap,
|
|
17899
|
-
|
|
18193
|
+
legendPosition,
|
|
18194
|
+
showLegend: showChoroplethLegend,
|
|
17900
18195
|
droppedFeatures
|
|
17901
18196
|
});
|
|
17902
18197
|
featureMarks = result.marks;
|
|
@@ -17924,6 +18219,201 @@ function compileMap(spec, options) {
|
|
|
17924
18219
|
const interiorStroke = isDarkMode ? "rgba(255,255,255,0.3)" : "rgba(0,0,0,0.15)";
|
|
17925
18220
|
const outlineStroke = isDarkMode ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.3)";
|
|
17926
18221
|
const borders = { interiorPath, outlinePath, interiorStroke, outlineStroke };
|
|
18222
|
+
const pointMarks = [];
|
|
18223
|
+
let pointCategoricalLegend = null;
|
|
18224
|
+
let pointContinuousLegend = null;
|
|
18225
|
+
if (mapSpec.points) {
|
|
18226
|
+
const pts = mapSpec.points;
|
|
18227
|
+
const lonField = pts.longitude.field;
|
|
18228
|
+
const latField = pts.latitude.field;
|
|
18229
|
+
const keyChannel = pts.key;
|
|
18230
|
+
const sizeScale = buildSizeScale(pts.size, pts.data, SIZE_SCALE_DEFAULTS.mapPoint);
|
|
18231
|
+
let pointColorScale = null;
|
|
18232
|
+
const pointCategories = [];
|
|
18233
|
+
const pointCategoryColors = /* @__PURE__ */ new Map();
|
|
18234
|
+
if (pts.color) {
|
|
18235
|
+
if (pts.color.type === "quantitative") {
|
|
18236
|
+
const colorValues = [];
|
|
18237
|
+
for (const row of pts.data) {
|
|
18238
|
+
const raw = row[pts.color.field];
|
|
18239
|
+
if (raw != null) {
|
|
18240
|
+
const v = Number(raw);
|
|
18241
|
+
if (Number.isFinite(v)) colorValues.push(v);
|
|
18242
|
+
}
|
|
18243
|
+
}
|
|
18244
|
+
if (colorValues.length > 0) {
|
|
18245
|
+
const palette = pts.color.scale?.scheme ?? "blue";
|
|
18246
|
+
const paletteStops = [...SEQUENTIAL_PALETTES2[palette] ?? SEQUENTIAL_PALETTES2.blue];
|
|
18247
|
+
const qScale = quantile2().domain(colorValues).range(paletteStops);
|
|
18248
|
+
pointColorScale = (val) => {
|
|
18249
|
+
const v = Number(val);
|
|
18250
|
+
return Number.isFinite(v) ? qScale(v) : CATEGORICAL_PALETTE[0];
|
|
18251
|
+
};
|
|
18252
|
+
}
|
|
18253
|
+
} else {
|
|
18254
|
+
const explicitDomain = pts.color.scale?.domain;
|
|
18255
|
+
const explicitRange = pts.color.scale?.range;
|
|
18256
|
+
if (explicitDomain && Array.isArray(explicitDomain)) {
|
|
18257
|
+
for (const cat of explicitDomain) {
|
|
18258
|
+
pointCategories.push(String(cat));
|
|
18259
|
+
}
|
|
18260
|
+
}
|
|
18261
|
+
const seen = new Set(pointCategories);
|
|
18262
|
+
for (const row of pts.data) {
|
|
18263
|
+
const raw = row[pts.color.field];
|
|
18264
|
+
if (raw != null) {
|
|
18265
|
+
const cat = String(raw);
|
|
18266
|
+
if (!seen.has(cat)) {
|
|
18267
|
+
seen.add(cat);
|
|
18268
|
+
pointCategories.push(cat);
|
|
18269
|
+
}
|
|
18270
|
+
}
|
|
18271
|
+
}
|
|
18272
|
+
const colorSource = explicitRange && Array.isArray(explicitRange) ? explicitRange : CATEGORICAL_PALETTE;
|
|
18273
|
+
for (let i = 0; i < pointCategories.length; i++) {
|
|
18274
|
+
pointCategoryColors.set(pointCategories[i], colorSource[i % colorSource.length]);
|
|
18275
|
+
}
|
|
18276
|
+
pointColorScale = (val) => {
|
|
18277
|
+
if (val == null) return colorSource[0];
|
|
18278
|
+
return pointCategoryColors.get(String(val)) ?? colorSource[0];
|
|
18279
|
+
};
|
|
18280
|
+
}
|
|
18281
|
+
}
|
|
18282
|
+
const defaultFill = CATEGORICAL_PALETTE[0];
|
|
18283
|
+
const pointOpacity = pts.opacity ?? 0.65;
|
|
18284
|
+
let animIndex = 0;
|
|
18285
|
+
const droppedPoints = [];
|
|
18286
|
+
for (let i = 0; i < pts.data.length; i++) {
|
|
18287
|
+
const row = pts.data[i];
|
|
18288
|
+
const lon = Number(row[lonField]);
|
|
18289
|
+
const lat = Number(row[latField]);
|
|
18290
|
+
if (!Number.isFinite(lon) || !Number.isFinite(lat)) continue;
|
|
18291
|
+
const projected = projection2([lon, lat]);
|
|
18292
|
+
if (!projected) {
|
|
18293
|
+
droppedPoints.push([lon, lat]);
|
|
18294
|
+
continue;
|
|
18295
|
+
}
|
|
18296
|
+
const r = sizeScale ? sizeScale.scale(Number(row[sizeScale.field])) : 5;
|
|
18297
|
+
const fill = pointColorScale && pts.color ? pointColorScale(row[pts.color.field]) : defaultFill;
|
|
18298
|
+
const key = keyChannel ? String(row[keyChannel.field] ?? i) : String(i);
|
|
18299
|
+
const stroke = isDarkMode ? "rgba(0,0,0,0.5)" : "rgba(255,255,255,0.8)";
|
|
18300
|
+
pointMarks.push({
|
|
18301
|
+
type: "map-point",
|
|
18302
|
+
cx: projected[0],
|
|
18303
|
+
cy: projected[1],
|
|
18304
|
+
r,
|
|
18305
|
+
fill,
|
|
18306
|
+
stroke,
|
|
18307
|
+
strokeWidth: 1,
|
|
18308
|
+
fillOpacity: pointOpacity,
|
|
18309
|
+
key,
|
|
18310
|
+
data: row,
|
|
18311
|
+
aria: { role: "img", label: key },
|
|
18312
|
+
animationIndex: animIndex++
|
|
18313
|
+
});
|
|
18314
|
+
}
|
|
18315
|
+
if (droppedPoints.length > 0) {
|
|
18316
|
+
const sample = droppedPoints.slice(0, 3).map(([lo, la]) => `[${lo}, ${la}]`).join(", ");
|
|
18317
|
+
compileWarnings.push({
|
|
18318
|
+
code: "POINT_NULL_PROJECTION",
|
|
18319
|
+
message: `${droppedPoints.length} point(s) project to null and were dropped (e.g. ${sample}). This is common with albersUsa which excludes territories. Consider using projection: "mercator" or "equalEarth".`,
|
|
18320
|
+
context: { count: droppedPoints.length, samples: droppedPoints.slice(0, 5) }
|
|
18321
|
+
});
|
|
18322
|
+
}
|
|
18323
|
+
if (pts.color && pts.color.type !== "quantitative" && pointCategories.length > 0 && showLegend) {
|
|
18324
|
+
const labelStyle = {
|
|
18325
|
+
fontFamily: theme.fonts.family,
|
|
18326
|
+
fontSize: theme.fonts.sizes.small,
|
|
18327
|
+
fontWeight: theme.fonts.weights.normal,
|
|
18328
|
+
fill: theme.colors.text,
|
|
18329
|
+
lineHeight: 1.2
|
|
18330
|
+
};
|
|
18331
|
+
const entries = pointCategories.map((cat) => ({
|
|
18332
|
+
label: cat,
|
|
18333
|
+
color: pointCategoryColors.get(cat),
|
|
18334
|
+
shape: "circle"
|
|
18335
|
+
}));
|
|
18336
|
+
const bottomChoroplethHeight = legendPosition === "bottom" ? continuousLegend?.bounds.height ?? categoricalLegend?.bounds.height ?? 0 : 0;
|
|
18337
|
+
const choroplethLegendGap = bottomChoroplethHeight > 0 ? 8 : 0;
|
|
18338
|
+
const legendX = pointLegendOverlay ? fullArea.x + OVERLAY_LEGEND_INSET : fullArea.x;
|
|
18339
|
+
const legendY = pointLegendOverlay ? fullArea.y + OVERLAY_LEGEND_INSET : fullArea.y + mapAreaHeight + legendReserveGap + bottomChoroplethHeight + choroplethLegendGap;
|
|
18340
|
+
const legendWidth = pointLegendOverlay ? fullArea.width - OVERLAY_LEGEND_INSET * 2 : fullArea.width;
|
|
18341
|
+
const swatchSize = 10;
|
|
18342
|
+
const swatchGap = 6;
|
|
18343
|
+
const entryGap = 16;
|
|
18344
|
+
pointCategoricalLegend = {
|
|
18345
|
+
type: "categorical",
|
|
18346
|
+
position: pointLegendOverlay ? "top-left" : "bottom",
|
|
18347
|
+
bounds: { x: legendX, y: legendY, width: legendWidth, height: swatchSize + 6 },
|
|
18348
|
+
labelStyle,
|
|
18349
|
+
entries,
|
|
18350
|
+
swatchSize,
|
|
18351
|
+
swatchGap,
|
|
18352
|
+
entryGap,
|
|
18353
|
+
swatchChipFill: isDarkMode ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.04)"
|
|
18354
|
+
};
|
|
18355
|
+
}
|
|
18356
|
+
if (pts.color && pts.color.type === "quantitative" && showLegend) {
|
|
18357
|
+
const ptColorValues = [];
|
|
18358
|
+
for (const row of pts.data) {
|
|
18359
|
+
const v = row[pts.color.field];
|
|
18360
|
+
if (v != null) {
|
|
18361
|
+
const n = Number(v);
|
|
18362
|
+
if (Number.isFinite(n)) ptColorValues.push(n);
|
|
18363
|
+
}
|
|
18364
|
+
}
|
|
18365
|
+
const ptLegendChannel = {
|
|
18366
|
+
...pts.color,
|
|
18367
|
+
scale: { ...pts.color.scale, type: "quantile" }
|
|
18368
|
+
};
|
|
18369
|
+
const ptContinuousContent = computeContinuousLegendContentForChannel(
|
|
18370
|
+
ptColorValues,
|
|
18371
|
+
ptLegendChannel,
|
|
18372
|
+
theme,
|
|
18373
|
+
fullArea.width
|
|
18374
|
+
);
|
|
18375
|
+
if (ptContinuousContent) {
|
|
18376
|
+
const labelStyle = {
|
|
18377
|
+
fontFamily: theme.fonts.family,
|
|
18378
|
+
fontSize: theme.fonts.sizes.small,
|
|
18379
|
+
fontWeight: theme.fonts.weights.normal,
|
|
18380
|
+
fill: theme.colors.text,
|
|
18381
|
+
lineHeight: 1.3,
|
|
18382
|
+
fontVariant: "tabular-nums"
|
|
18383
|
+
};
|
|
18384
|
+
const bottomChoroplethH = legendPosition === "bottom" ? continuousLegend?.bounds.height ?? categoricalLegend?.bounds.height ?? 0 : 0;
|
|
18385
|
+
const choroplethLegendGap = bottomChoroplethH > 0 ? 8 : 0;
|
|
18386
|
+
const legendX = pointLegendOverlay ? fullArea.x + OVERLAY_LEGEND_INSET : fullArea.x;
|
|
18387
|
+
const legendY = pointLegendOverlay ? fullArea.y + OVERLAY_LEGEND_INSET : fullArea.y + mapAreaHeight + legendReserveGap + bottomChoroplethH + choroplethLegendGap;
|
|
18388
|
+
const ptLabelRowHeight = Math.ceil(theme.fonts.sizes.small * 1.3);
|
|
18389
|
+
const ptLegendHeight = ptContinuousContent.barHeight + CONTINUOUS_LABEL_GAP + ptLabelRowHeight;
|
|
18390
|
+
const bounds = {
|
|
18391
|
+
x: legendX,
|
|
18392
|
+
y: legendY,
|
|
18393
|
+
width: ptContinuousContent.barWidth,
|
|
18394
|
+
height: ptLegendHeight
|
|
18395
|
+
};
|
|
18396
|
+
const bar = {
|
|
18397
|
+
x: bounds.x,
|
|
18398
|
+
y: bounds.y,
|
|
18399
|
+
width: ptContinuousContent.barWidth,
|
|
18400
|
+
height: ptContinuousContent.barHeight
|
|
18401
|
+
};
|
|
18402
|
+
pointContinuousLegend = {
|
|
18403
|
+
type: "continuous",
|
|
18404
|
+
mode: ptContinuousContent.mode,
|
|
18405
|
+
position: pointLegendOverlay ? "top-left" : "bottom",
|
|
18406
|
+
bounds,
|
|
18407
|
+
labelStyle,
|
|
18408
|
+
bar,
|
|
18409
|
+
colorStops: ptContinuousContent.colorStops,
|
|
18410
|
+
bins: ptContinuousContent.bins.map((b) => ({ ...b, x: b.x + bar.x })),
|
|
18411
|
+
ticks: ptContinuousContent.ticks.map((t) => ({ ...t, x: t.x + bar.x })),
|
|
18412
|
+
labelY: bar.y + bar.height + CONTINUOUS_LABEL_GAP + labelStyle.fontSize
|
|
18413
|
+
};
|
|
18414
|
+
}
|
|
18415
|
+
}
|
|
18416
|
+
}
|
|
17927
18417
|
const tooltipChannels = mapSpec.encoding.tooltip ? Array.isArray(mapSpec.encoding.tooltip) ? mapSpec.encoding.tooltip : [mapSpec.encoding.tooltip] : null;
|
|
17928
18418
|
const tooltipDescriptors = /* @__PURE__ */ new Map();
|
|
17929
18419
|
for (const mark of featureMarks) {
|
|
@@ -17941,12 +18431,12 @@ function compileMap(spec, options) {
|
|
|
17941
18431
|
});
|
|
17942
18432
|
}
|
|
17943
18433
|
}
|
|
17944
|
-
} else if (isQuantitative && mark.data[colorEncoding.field] != null) {
|
|
18434
|
+
} else if (colorEncoding && isQuantitative && mark.data[colorEncoding.field] != null) {
|
|
17945
18435
|
fields.push({
|
|
17946
18436
|
label: colorEncoding.title ?? colorEncoding.field,
|
|
17947
18437
|
value: formatter(Number(mark.data[colorEncoding.field]))
|
|
17948
18438
|
});
|
|
17949
|
-
} else if (!isQuantitative && mark.data[colorEncoding.field] != null) {
|
|
18439
|
+
} else if (colorEncoding && !isQuantitative && mark.data[colorEncoding.field] != null) {
|
|
17950
18440
|
fields.push({
|
|
17951
18441
|
label: colorEncoding.title ?? colorEncoding.field,
|
|
17952
18442
|
value: String(mark.data[colorEncoding.field])
|
|
@@ -17954,13 +18444,63 @@ function compileMap(spec, options) {
|
|
|
17954
18444
|
}
|
|
17955
18445
|
tooltipDescriptors.set(String(mark.id), { fields });
|
|
17956
18446
|
}
|
|
18447
|
+
if (mapSpec.points && pointMarks.length > 0) {
|
|
18448
|
+
const ptTooltipChannels = mapSpec.points.tooltip ? Array.isArray(mapSpec.points.tooltip) ? mapSpec.points.tooltip : [mapSpec.points.tooltip] : null;
|
|
18449
|
+
for (const pm of pointMarks) {
|
|
18450
|
+
const fields = [];
|
|
18451
|
+
if (ptTooltipChannels) {
|
|
18452
|
+
for (const ch of ptTooltipChannels) {
|
|
18453
|
+
const val = pm.data[ch.field];
|
|
18454
|
+
if (val != null) {
|
|
18455
|
+
fields.push({
|
|
18456
|
+
label: ch.title ?? ch.field,
|
|
18457
|
+
value: ch.type === "quantitative" ? formatter(Number(val)) : String(val)
|
|
18458
|
+
});
|
|
18459
|
+
}
|
|
18460
|
+
}
|
|
18461
|
+
} else {
|
|
18462
|
+
const pts = mapSpec.points;
|
|
18463
|
+
if (pts.key) {
|
|
18464
|
+
const val = pm.data[pts.key.field];
|
|
18465
|
+
if (val != null)
|
|
18466
|
+
fields.push({ label: pts.key.title ?? pts.key.field, value: String(val) });
|
|
18467
|
+
}
|
|
18468
|
+
if (pts.color) {
|
|
18469
|
+
const val = pm.data[pts.color.field];
|
|
18470
|
+
if (val != null)
|
|
18471
|
+
fields.push({
|
|
18472
|
+
label: pts.color.title ?? pts.color.field,
|
|
18473
|
+
value: pts.color.type === "quantitative" ? formatter(Number(val)) : String(val)
|
|
18474
|
+
});
|
|
18475
|
+
}
|
|
18476
|
+
if (pts.size) {
|
|
18477
|
+
const val = pm.data[pts.size.field];
|
|
18478
|
+
if (val != null)
|
|
18479
|
+
fields.push({ label: pts.size.title ?? pts.size.field, value: formatter(Number(val)) });
|
|
18480
|
+
}
|
|
18481
|
+
}
|
|
18482
|
+
if (fields.length > 0) {
|
|
18483
|
+
tooltipDescriptors.set(`point:${pm.key}`, { fields });
|
|
18484
|
+
}
|
|
18485
|
+
}
|
|
18486
|
+
}
|
|
18487
|
+
const a11yDataField = colorEncoding?.field;
|
|
17957
18488
|
const a11y = {
|
|
17958
|
-
altText: `Map showing ${geoFeatures.length} regions with ${isQuantitative ? "quantitative" : "categorical"} data`,
|
|
17959
|
-
dataTableFallback:
|
|
18489
|
+
altText: pointMarks.length > 0 ? colorEncoding ? `Map showing ${geoFeatures.length} regions with ${isQuantitative ? "quantitative" : "categorical"} data and ${pointMarks.length} point markers` : `Map showing ${geoFeatures.length} regions with ${pointMarks.length} point markers` : colorEncoding ? `Map showing ${geoFeatures.length} regions with ${isQuantitative ? "quantitative" : "categorical"} data` : `Map showing ${geoFeatures.length} regions`,
|
|
18490
|
+
dataTableFallback: [
|
|
18491
|
+
...featureMarks.filter((m) => m.data).map((m) => [
|
|
18492
|
+
m.name ?? String(m.id),
|
|
18493
|
+
a11yDataField ? String(m.data?.[a11yDataField] ?? "") : ""
|
|
18494
|
+
]),
|
|
18495
|
+
...pointMarks.map((pm) => [
|
|
18496
|
+
pm.key,
|
|
18497
|
+
mapSpec.points?.color ? String(pm.data[mapSpec.points.color.field] ?? "") : ""
|
|
18498
|
+
])
|
|
18499
|
+
],
|
|
17960
18500
|
role: "img",
|
|
17961
|
-
keyboardNavigable: featureMarks.length > 0
|
|
18501
|
+
keyboardNavigable: featureMarks.length > 0 || pointMarks.length > 0
|
|
17962
18502
|
};
|
|
17963
|
-
const resolvedFocus = resolveFocus(mapSpec.geo.focus, featureMarks, compileWarnings);
|
|
18503
|
+
const resolvedFocus = resolveFocus(mapSpec.geo.focus, featureMarks, pointMarks, compileWarnings);
|
|
17964
18504
|
const resolvedAnimation = resolveAnimation(mapSpec.animation);
|
|
17965
18505
|
const mapAreaY = legendPosition === "top" && legendBlockHeight > 0 ? fullArea.y + legendBlockHeight + legendReserveGap : fullArea.y;
|
|
17966
18506
|
const mapArea = {
|
|
@@ -17983,6 +18523,9 @@ function compileMap(spec, options) {
|
|
|
17983
18523
|
borders,
|
|
17984
18524
|
continuousLegend,
|
|
17985
18525
|
categoricalLegend,
|
|
18526
|
+
pointMarks,
|
|
18527
|
+
pointCategoricalLegend,
|
|
18528
|
+
pointContinuousLegend,
|
|
17986
18529
|
tooltipDescriptors,
|
|
17987
18530
|
a11y,
|
|
17988
18531
|
theme,
|
|
@@ -17996,6 +18539,46 @@ function compileMap(spec, options) {
|
|
|
17996
18539
|
focus: resolvedFocus
|
|
17997
18540
|
};
|
|
17998
18541
|
}
|
|
18542
|
+
function buildBasemapMarks(opts) {
|
|
18543
|
+
const { geoFeatures, pathGen, neutralFill, neutralStroke, droppedFeatures } = opts;
|
|
18544
|
+
const marks = [];
|
|
18545
|
+
let animIndex = 0;
|
|
18546
|
+
for (const feat of geoFeatures) {
|
|
18547
|
+
const pathD = pathGen(feat);
|
|
18548
|
+
if (!pathD) {
|
|
18549
|
+
const name2 = feat.properties?.name ?? String(feat.id);
|
|
18550
|
+
droppedFeatures.push(name2);
|
|
18551
|
+
continue;
|
|
18552
|
+
}
|
|
18553
|
+
const featureId = feat.id;
|
|
18554
|
+
const props = feat.properties;
|
|
18555
|
+
const name = props?.name ?? props?.NAME;
|
|
18556
|
+
const b = pathGen.bounds(feat);
|
|
18557
|
+
const bx = Number.isFinite(b[0][0]) ? b[0][0] : 0;
|
|
18558
|
+
const by = Number.isFinite(b[0][1]) ? b[0][1] : 0;
|
|
18559
|
+
const bw = Number.isFinite(b[1][0]) ? b[1][0] - bx : 0;
|
|
18560
|
+
const bh = Number.isFinite(b[1][1]) ? b[1][1] - by : 0;
|
|
18561
|
+
const bounds = { x: bx, y: by, width: bw, height: bh };
|
|
18562
|
+
const rawCentroid = pathGen.centroid(feat);
|
|
18563
|
+
const cx = Number.isFinite(rawCentroid[0]) ? rawCentroid[0] : bx + bw / 2;
|
|
18564
|
+
const cy = Number.isFinite(rawCentroid[1]) ? rawCentroid[1] : by + bh / 2;
|
|
18565
|
+
marks.push({
|
|
18566
|
+
type: "map-feature",
|
|
18567
|
+
path: pathD,
|
|
18568
|
+
fill: neutralFill,
|
|
18569
|
+
stroke: neutralStroke,
|
|
18570
|
+
strokeWidth: 0.5,
|
|
18571
|
+
id: featureId,
|
|
18572
|
+
name,
|
|
18573
|
+
data: null,
|
|
18574
|
+
aria: { role: "img", label: name ?? String(featureId) },
|
|
18575
|
+
animationIndex: animIndex++,
|
|
18576
|
+
bounds,
|
|
18577
|
+
centroid: [cx, cy]
|
|
18578
|
+
});
|
|
18579
|
+
}
|
|
18580
|
+
return marks;
|
|
18581
|
+
}
|
|
17999
18582
|
function buildQuantitativeMarks(opts) {
|
|
18000
18583
|
const {
|
|
18001
18584
|
geoFeatures,
|
|
@@ -18017,7 +18600,7 @@ function buildQuantitativeMarks(opts) {
|
|
|
18017
18600
|
if (!Number.isNaN(v)) values.push(v);
|
|
18018
18601
|
}
|
|
18019
18602
|
}
|
|
18020
|
-
const paletteStops = [...
|
|
18603
|
+
const paletteStops = [...SEQUENTIAL_PALETTES2[palette] ?? SEQUENTIAL_PALETTES2.blue];
|
|
18021
18604
|
const colorScale = quantile2().domain(values).range(paletteStops);
|
|
18022
18605
|
const marks = [];
|
|
18023
18606
|
let animIndex = 0;
|
|
@@ -18080,6 +18663,7 @@ function buildCategoricalMarks(opts) {
|
|
|
18080
18663
|
pathGen,
|
|
18081
18664
|
joined,
|
|
18082
18665
|
colorField,
|
|
18666
|
+
colorScale: scaleConfig,
|
|
18083
18667
|
isDarkMode,
|
|
18084
18668
|
neutralFill,
|
|
18085
18669
|
neutralStroke,
|
|
@@ -18087,11 +18671,18 @@ function buildCategoricalMarks(opts) {
|
|
|
18087
18671
|
fullArea,
|
|
18088
18672
|
mapAreaHeight,
|
|
18089
18673
|
legendReserveGap,
|
|
18674
|
+
legendPosition,
|
|
18090
18675
|
showLegend,
|
|
18091
18676
|
droppedFeatures
|
|
18092
18677
|
} = opts;
|
|
18093
18678
|
const categories = [];
|
|
18094
|
-
const
|
|
18679
|
+
const explicitDomain = scaleConfig?.domain;
|
|
18680
|
+
if (explicitDomain && Array.isArray(explicitDomain)) {
|
|
18681
|
+
for (const cat of explicitDomain) {
|
|
18682
|
+
categories.push(String(cat));
|
|
18683
|
+
}
|
|
18684
|
+
}
|
|
18685
|
+
const seen = new Set(categories);
|
|
18095
18686
|
for (const row of joined.values()) {
|
|
18096
18687
|
const raw = row[colorField];
|
|
18097
18688
|
if (raw != null) {
|
|
@@ -18102,9 +18693,11 @@ function buildCategoricalMarks(opts) {
|
|
|
18102
18693
|
}
|
|
18103
18694
|
}
|
|
18104
18695
|
}
|
|
18696
|
+
const explicitRange = scaleConfig?.range;
|
|
18697
|
+
const colorSource = explicitRange && Array.isArray(explicitRange) ? explicitRange : CATEGORICAL_PALETTE;
|
|
18105
18698
|
const categoryColors = /* @__PURE__ */ new Map();
|
|
18106
18699
|
for (let i = 0; i < categories.length; i++) {
|
|
18107
|
-
categoryColors.set(categories[i],
|
|
18700
|
+
categoryColors.set(categories[i], colorSource[i % colorSource.length]);
|
|
18108
18701
|
}
|
|
18109
18702
|
const marks = [];
|
|
18110
18703
|
let animIndex = 0;
|
|
@@ -18172,14 +18765,14 @@ function buildCategoricalMarks(opts) {
|
|
|
18172
18765
|
shape: "square"
|
|
18173
18766
|
}));
|
|
18174
18767
|
const legendX = fullArea.x;
|
|
18175
|
-
const legendY = fullArea.y + mapAreaHeight + legendReserveGap;
|
|
18768
|
+
const legendY = legendPosition === "bottom" ? fullArea.y + mapAreaHeight + legendReserveGap : fullArea.y + legendReserveGap;
|
|
18176
18769
|
const legendWidth = fullArea.width;
|
|
18177
18770
|
const swatchSize = 10;
|
|
18178
18771
|
const swatchGap = 6;
|
|
18179
18772
|
const entryGap = 16;
|
|
18180
18773
|
legend = {
|
|
18181
18774
|
type: "categorical",
|
|
18182
|
-
position:
|
|
18775
|
+
position: legendPosition,
|
|
18183
18776
|
bounds: { x: legendX, y: legendY, width: legendWidth, height: swatchSize + 6 },
|
|
18184
18777
|
labelStyle,
|
|
18185
18778
|
entries,
|
|
@@ -18191,8 +18784,40 @@ function buildCategoricalMarks(opts) {
|
|
|
18191
18784
|
}
|
|
18192
18785
|
return { marks, legend };
|
|
18193
18786
|
}
|
|
18194
|
-
function resolveFocus(focus, features, warnings) {
|
|
18787
|
+
function resolveFocus(focus, features, pointMarks, warnings) {
|
|
18195
18788
|
if (focus === null || focus === void 0) return null;
|
|
18789
|
+
if (typeof focus === "object" && !Array.isArray(focus) && "points" in focus) {
|
|
18790
|
+
const filter = focus.points;
|
|
18791
|
+
const matched = filter === true ? pointMarks : pointMarks.filter((p) => p.data[filter.field] === filter.value);
|
|
18792
|
+
if (matched.length === 0) {
|
|
18793
|
+
const detail = filter === true ? "the map has no points to fit" : `no points match { field: "${filter.field}", value: ${JSON.stringify(filter.value)} }`;
|
|
18794
|
+
warnings.push({
|
|
18795
|
+
code: "FOCUS_UNMATCHED",
|
|
18796
|
+
message: `geo.focus.points is set but ${detail}`,
|
|
18797
|
+
context: {}
|
|
18798
|
+
});
|
|
18799
|
+
return null;
|
|
18800
|
+
}
|
|
18801
|
+
const padding2 = focus.padding ?? 16;
|
|
18802
|
+
let minX2 = Infinity;
|
|
18803
|
+
let minY2 = Infinity;
|
|
18804
|
+
let maxX2 = -Infinity;
|
|
18805
|
+
let maxY2 = -Infinity;
|
|
18806
|
+
for (const p of matched) {
|
|
18807
|
+
minX2 = Math.min(minX2, p.cx);
|
|
18808
|
+
minY2 = Math.min(minY2, p.cy);
|
|
18809
|
+
maxX2 = Math.max(maxX2, p.cx);
|
|
18810
|
+
maxY2 = Math.max(maxY2, p.cy);
|
|
18811
|
+
}
|
|
18812
|
+
const w = Math.max(maxX2 - minX2, 1);
|
|
18813
|
+
const h = Math.max(maxY2 - minY2, 1);
|
|
18814
|
+
return {
|
|
18815
|
+
target: { x: minX2, y: minY2, width: w, height: h, padding: padding2 },
|
|
18816
|
+
// No feature ids: focus-dim is a feature concept, so a points focus
|
|
18817
|
+
// dims no features (there are no "other" features to mute).
|
|
18818
|
+
ids: []
|
|
18819
|
+
};
|
|
18820
|
+
}
|
|
18196
18821
|
let ids;
|
|
18197
18822
|
let padding = 16;
|
|
18198
18823
|
if (typeof focus === "string" || typeof focus === "number") {
|
|
@@ -18252,6 +18877,9 @@ function emptyLayout3(chrome, theme, options, watermark) {
|
|
|
18252
18877
|
},
|
|
18253
18878
|
continuousLegend: null,
|
|
18254
18879
|
categoricalLegend: null,
|
|
18880
|
+
pointMarks: [],
|
|
18881
|
+
pointCategoricalLegend: null,
|
|
18882
|
+
pointContinuousLegend: null,
|
|
18255
18883
|
tooltipDescriptors: /* @__PURE__ */ new Map(),
|
|
18256
18884
|
a11y: {
|
|
18257
18885
|
altText: "Empty map",
|
|
@@ -20902,7 +21530,7 @@ import {
|
|
|
20902
21530
|
HPAD_COMPACT_MIN as HPAD_COMPACT_MIN4,
|
|
20903
21531
|
pickLabelColor as pickLabelColor4,
|
|
20904
21532
|
resolveTheme as resolveTheme6,
|
|
20905
|
-
SEQUENTIAL_PALETTES as
|
|
21533
|
+
SEQUENTIAL_PALETTES as SEQUENTIAL_PALETTES3
|
|
20906
21534
|
} from "@opendata-ai/openchart-core";
|
|
20907
21535
|
var TILE_CORNER_RADIUS = 6;
|
|
20908
21536
|
var TILE_STROKE_WIDTH = 1;
|
|
@@ -20980,7 +21608,7 @@ function compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDa
|
|
|
20980
21608
|
const values = Array.from(stateValueMap.values());
|
|
20981
21609
|
const min4 = values.length > 0 ? Math.min(...values) : 0;
|
|
20982
21610
|
const max4 = values.length > 0 ? Math.max(...values) : 100;
|
|
20983
|
-
const paletteStops = [...
|
|
21611
|
+
const paletteStops = [...SEQUENTIAL_PALETTES3[tilemapSpec.palette] ?? SEQUENTIAL_PALETTES3.blue];
|
|
20984
21612
|
const baseColor = isDarkMode ? paletteStops[0] : paletteStops[paletteStops.length - 1];
|
|
20985
21613
|
const opacityRange = isDarkMode ? [0.15, 1] : [0.2, 1];
|
|
20986
21614
|
const opacityScale = linear2().domain([min4, max4]).range(opacityRange).clamp(true);
|
|
@@ -22260,6 +22888,38 @@ function applySparklineDefaults(spec, theme) {
|
|
|
22260
22888
|
}
|
|
22261
22889
|
return { ...spec, markDef: newMarkDef };
|
|
22262
22890
|
}
|
|
22891
|
+
function resolveScaleInvert(resolved) {
|
|
22892
|
+
if (!resolved) return void 0;
|
|
22893
|
+
const { scale, type } = resolved;
|
|
22894
|
+
if (type === "band" || type === "point") {
|
|
22895
|
+
const domain = scale.domain();
|
|
22896
|
+
const positions = domain.map((v) => {
|
|
22897
|
+
const pos = scale(v) ?? 0;
|
|
22898
|
+
return type === "band" ? pos + scale.bandwidth() / 2 : pos;
|
|
22899
|
+
});
|
|
22900
|
+
const range4 = scale.range();
|
|
22901
|
+
return {
|
|
22902
|
+
topPixel: range4[0],
|
|
22903
|
+
bottomPixel: range4[range4.length - 1],
|
|
22904
|
+
topData: 0,
|
|
22905
|
+
bottomData: 0,
|
|
22906
|
+
domain,
|
|
22907
|
+
positions
|
|
22908
|
+
};
|
|
22909
|
+
}
|
|
22910
|
+
const invert = scale.invert;
|
|
22911
|
+
if (typeof invert !== "function") return void 0;
|
|
22912
|
+
const range3 = scale.range();
|
|
22913
|
+
const topPx = range3[0];
|
|
22914
|
+
const bottomPx = range3[range3.length - 1];
|
|
22915
|
+
const topVal = invert.call(scale, topPx);
|
|
22916
|
+
const bottomVal = invert.call(scale, bottomPx);
|
|
22917
|
+
const topData = topVal instanceof Date ? +topVal : Number(topVal);
|
|
22918
|
+
const bottomData = bottomVal instanceof Date ? +bottomVal : Number(bottomVal);
|
|
22919
|
+
if (!Number.isFinite(topData) || !Number.isFinite(bottomData)) return void 0;
|
|
22920
|
+
const scaleType = type;
|
|
22921
|
+
return { topPixel: topPx, bottomPixel: bottomPx, topData, bottomData, scaleType };
|
|
22922
|
+
}
|
|
22263
22923
|
function compileChart(spec, optionsInput) {
|
|
22264
22924
|
const sugarWarnings = [];
|
|
22265
22925
|
const expandedSpec = spec && typeof spec === "object" && !Array.isArray(spec) ? expandSpecSugar(spec, sugarWarnings) : spec;
|
|
@@ -22460,12 +23120,35 @@ function compileChart(spec, optionsInput) {
|
|
|
22460
23120
|
}
|
|
22461
23121
|
};
|
|
22462
23122
|
}
|
|
23123
|
+
let facetDirection = "wrap";
|
|
23124
|
+
const rowChannel = renderSpec.encoding.row;
|
|
23125
|
+
const colChannel = renderSpec.encoding.column;
|
|
23126
|
+
if (rowChannel?.field || colChannel?.field) {
|
|
23127
|
+
const channel = rowChannel?.field ? rowChannel : colChannel;
|
|
23128
|
+
facetDirection = rowChannel?.field ? "row" : "column";
|
|
23129
|
+
const desugared = { ...renderSpec.encoding };
|
|
23130
|
+
delete desugared.row;
|
|
23131
|
+
delete desugared.column;
|
|
23132
|
+
desugared.facet = {
|
|
23133
|
+
field: channel.field,
|
|
23134
|
+
type: channel.type,
|
|
23135
|
+
sort: channel.sort,
|
|
23136
|
+
...facetDirection === "row" ? { columns: 1 } : {}
|
|
23137
|
+
};
|
|
23138
|
+
renderSpec = { ...renderSpec, encoding: desugared };
|
|
23139
|
+
const chartDesugared = { ...chartSpec.encoding };
|
|
23140
|
+
delete chartDesugared.row;
|
|
23141
|
+
delete chartDesugared.column;
|
|
23142
|
+
chartDesugared.facet = desugared.facet;
|
|
23143
|
+
chartSpec = { ...chartSpec, encoding: chartDesugared };
|
|
23144
|
+
}
|
|
22463
23145
|
const facetChannel = renderSpec.encoding.facet;
|
|
22464
23146
|
if (facetChannel?.field) {
|
|
22465
23147
|
return compileFaceted(
|
|
22466
23148
|
chartSpec,
|
|
22467
23149
|
renderSpec,
|
|
22468
23150
|
facetChannel,
|
|
23151
|
+
facetDirection,
|
|
22469
23152
|
options,
|
|
22470
23153
|
theme,
|
|
22471
23154
|
strategy,
|
|
@@ -22625,6 +23308,8 @@ function compileChart(spec, optionsInput) {
|
|
|
22625
23308
|
);
|
|
22626
23309
|
assignAnimationIndices(marks, resolvedAnimation);
|
|
22627
23310
|
const chrome = footnotes ? { ...dims.chrome, footnotes } : dims.chrome;
|
|
23311
|
+
const xInvert = resolveScaleInvert(scales.x);
|
|
23312
|
+
const yInvert = resolveScaleInvert(scales.y);
|
|
22628
23313
|
return {
|
|
22629
23314
|
area: chartArea,
|
|
22630
23315
|
chrome,
|
|
@@ -22657,10 +23342,12 @@ function compileChart(spec, optionsInput) {
|
|
|
22657
23342
|
watermark,
|
|
22658
23343
|
display: chartSpec.display,
|
|
22659
23344
|
crosshair,
|
|
22660
|
-
measureText: options.measureText
|
|
23345
|
+
measureText: options.measureText,
|
|
23346
|
+
...xInvert ? { xInvert } : {},
|
|
23347
|
+
...yInvert ? { yInvert } : {}
|
|
22661
23348
|
};
|
|
22662
23349
|
}
|
|
22663
|
-
function compileFaceted(chartSpec, renderSpec, facetChannel, options, theme, strategy, watermark, resolvedAnimation, crosshair, rawSpec, optionsInput) {
|
|
23350
|
+
function compileFaceted(chartSpec, renderSpec, facetChannel, facetDirection, options, theme, strategy, watermark, resolvedAnimation, crosshair, rawSpec, optionsInput) {
|
|
22664
23351
|
const measure = createMeasureFn(options.measureText);
|
|
22665
23352
|
const panelEncoding = { ...renderSpec.encoding };
|
|
22666
23353
|
delete panelEncoding.facet;
|
|
@@ -22708,7 +23395,8 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, options, theme, str
|
|
|
22708
23395
|
);
|
|
22709
23396
|
const leftReservation = plan.leftGutter;
|
|
22710
23397
|
const bottomReservation = plan.xAxisExtent;
|
|
22711
|
-
const
|
|
23398
|
+
const effectiveColumns = facetDirection === "column" ? facetValues.length : facetChannel.columns;
|
|
23399
|
+
const grid = computeFacetGrid(facetValues, effectiveColumns, chartArea, {
|
|
22712
23400
|
left: leftReservation,
|
|
22713
23401
|
bottom: bottomReservation
|
|
22714
23402
|
});
|
|
@@ -22724,8 +23412,10 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, options, theme, str
|
|
|
22724
23412
|
}
|
|
22725
23413
|
}
|
|
22726
23414
|
const resolveConfig = chartSpec.resolve;
|
|
22727
|
-
const
|
|
22728
|
-
const
|
|
23415
|
+
const xDefault = facetDirection === "column" ? "independent" : "shared";
|
|
23416
|
+
const yDefault = facetDirection === "row" ? "independent" : "shared";
|
|
23417
|
+
const yResolve = resolveConfig?.scale?.y ?? yDefault;
|
|
23418
|
+
const xResolve = resolveConfig?.scale?.x ?? xDefault;
|
|
22729
23419
|
const fullScales = computeScales(panelSpec, chartArea, renderSpec.data);
|
|
22730
23420
|
const sharedXDomain = xResolve === "shared" && fullScales.x ? fullScales.x.scale.domain() : void 0;
|
|
22731
23421
|
const sharedYDomain = yResolve === "shared" && fullScales.y ? fullScales.y.scale.domain() : void 0;
|
|
@@ -22790,6 +23480,16 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, options, theme, str
|
|
|
22790
23480
|
tickMarks: false
|
|
22791
23481
|
};
|
|
22792
23482
|
}
|
|
23483
|
+
const isBottomRow = gridPanel.row === grid.rows - 1;
|
|
23484
|
+
if (xResolve === "shared" && facetDirection === "row" && !isBottomRow && panelAxes.x) {
|
|
23485
|
+
panelAxes.x = {
|
|
23486
|
+
...panelAxes.x,
|
|
23487
|
+
ticks: [],
|
|
23488
|
+
label: void 0,
|
|
23489
|
+
domainLine: false,
|
|
23490
|
+
tickMarks: false
|
|
23491
|
+
};
|
|
23492
|
+
}
|
|
22793
23493
|
const panelMarks = renderer ? renderer(
|
|
22794
23494
|
panelSpecWithDomains,
|
|
22795
23495
|
panelScales,
|
|
@@ -22843,10 +23543,11 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, options, theme, str
|
|
|
22843
23543
|
annotations: panelAnnotations,
|
|
22844
23544
|
header: {
|
|
22845
23545
|
text: gridPanel.key,
|
|
22846
|
-
x: gridPanel.headerPos.x,
|
|
23546
|
+
x: facetDirection === "row" ? gridPanel.area.x : gridPanel.headerPos.x,
|
|
22847
23547
|
y: gridPanel.headerPos.y,
|
|
22848
23548
|
fontSize: theme.fonts.sizes.body,
|
|
22849
|
-
fontWeight: theme.fonts.weights.medium
|
|
23549
|
+
fontWeight: theme.fonts.weights.medium,
|
|
23550
|
+
...facetDirection === "row" ? { textAnchor: "start" } : {}
|
|
22850
23551
|
}
|
|
22851
23552
|
});
|
|
22852
23553
|
}
|
|
@@ -22914,7 +23615,8 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, options, theme, str
|
|
|
22914
23615
|
panels: panelLayouts,
|
|
22915
23616
|
facetField: facetChannel.field,
|
|
22916
23617
|
columns: grid.columns,
|
|
22917
|
-
sharedScales: yResolve === "shared" && xResolve === "shared"
|
|
23618
|
+
sharedScales: yResolve === "shared" && xResolve === "shared",
|
|
23619
|
+
direction: facetDirection
|
|
22918
23620
|
}
|
|
22919
23621
|
};
|
|
22920
23622
|
}
|