@opendata-ai/openchart-engine 8.0.0-rc.4 → 8.0.0-rc.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +60 -9
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/compiler/validate.ts +32 -5
- package/src/geo/__tests__/compile-map.test.ts +172 -0
- package/src/geo/compile-map.ts +57 -3
package/dist/index.js
CHANGED
|
@@ -11758,12 +11758,31 @@ function validateMapSpec(spec, errors) {
|
|
|
11758
11758
|
}
|
|
11759
11759
|
} else if (typeof focus === "object") {
|
|
11760
11760
|
const focusObj = focus;
|
|
11761
|
-
if (
|
|
11761
|
+
if ("points" in focusObj) {
|
|
11762
|
+
const pts = focusObj.points;
|
|
11763
|
+
const isSubset = typeof pts === "object" && pts !== null && typeof pts.field === "string" && "value" in pts;
|
|
11764
|
+
if (pts !== true && !isSubset) {
|
|
11765
|
+
errors.push({
|
|
11766
|
+
message: "Spec error: geo.focus.points must be true or { field: string, value: string | number }",
|
|
11767
|
+
path: "geo.focus.points",
|
|
11768
|
+
code: "INVALID_TYPE",
|
|
11769
|
+
suggestion: 'Use { points: true } to fit all points, or { points: { field: "rating", value: "F" } } to fit a subset.'
|
|
11770
|
+
});
|
|
11771
|
+
}
|
|
11772
|
+
if (focusObj.padding !== void 0 && typeof focusObj.padding !== "number") {
|
|
11773
|
+
errors.push({
|
|
11774
|
+
message: "Spec error: geo.focus.padding must be a number",
|
|
11775
|
+
path: "geo.focus.padding",
|
|
11776
|
+
code: "INVALID_TYPE",
|
|
11777
|
+
suggestion: "Provide padding as a number in map-local units, e.g. padding: 16"
|
|
11778
|
+
});
|
|
11779
|
+
}
|
|
11780
|
+
} else if (!("features" in focusObj) || focusObj.features == null) {
|
|
11762
11781
|
errors.push({
|
|
11763
|
-
message: 'Spec error: geo.focus object must have a "features" property',
|
|
11782
|
+
message: 'Spec error: geo.focus object must have a "features" or "points" property',
|
|
11764
11783
|
path: "geo.focus.features",
|
|
11765
11784
|
code: "MISSING_FIELD",
|
|
11766
|
-
suggestion: 'Provide feature id(s) to focus on, e.g.
|
|
11785
|
+
suggestion: 'Provide feature id(s) to focus on, e.g. { features: "36", padding: 32 }, or { points: true } to fit the point cluster.'
|
|
11767
11786
|
});
|
|
11768
11787
|
} else {
|
|
11769
11788
|
const feats = focusObj.features;
|
|
@@ -11797,10 +11816,10 @@ function validateMapSpec(spec, errors) {
|
|
|
11797
11816
|
}
|
|
11798
11817
|
} else {
|
|
11799
11818
|
errors.push({
|
|
11800
|
-
message: "Spec error: geo.focus must be a string, number, array of ids, or object with { features, padding? }",
|
|
11819
|
+
message: "Spec error: geo.focus must be a string, number, array of ids, or object with { features, padding? } or { points: true, padding? }",
|
|
11801
11820
|
path: "geo.focus",
|
|
11802
11821
|
code: "INVALID_TYPE",
|
|
11803
|
-
suggestion: 'Use a feature id, array of ids,
|
|
11822
|
+
suggestion: 'Use a feature id, array of ids, { features: "36", padding: 16 }, or { points: true }. Pass null to clear focus.'
|
|
11804
11823
|
});
|
|
11805
11824
|
}
|
|
11806
11825
|
}
|
|
@@ -17992,6 +18011,7 @@ function compileMap(spec, options) {
|
|
|
17992
18011
|
fullArea,
|
|
17993
18012
|
mapAreaHeight,
|
|
17994
18013
|
legendReserveGap,
|
|
18014
|
+
legendPosition,
|
|
17995
18015
|
showLegend,
|
|
17996
18016
|
droppedFeatures
|
|
17997
18017
|
});
|
|
@@ -18301,7 +18321,7 @@ function compileMap(spec, options) {
|
|
|
18301
18321
|
role: "img",
|
|
18302
18322
|
keyboardNavigable: featureMarks.length > 0 || pointMarks.length > 0
|
|
18303
18323
|
};
|
|
18304
|
-
const resolvedFocus = resolveFocus(mapSpec.geo.focus, featureMarks, compileWarnings);
|
|
18324
|
+
const resolvedFocus = resolveFocus(mapSpec.geo.focus, featureMarks, pointMarks, compileWarnings);
|
|
18305
18325
|
const resolvedAnimation = resolveAnimation(mapSpec.animation);
|
|
18306
18326
|
const mapAreaY = legendPosition === "top" && legendBlockHeight > 0 ? fullArea.y + legendBlockHeight + legendReserveGap : fullArea.y;
|
|
18307
18327
|
const mapArea = {
|
|
@@ -18472,6 +18492,7 @@ function buildCategoricalMarks(opts) {
|
|
|
18472
18492
|
fullArea,
|
|
18473
18493
|
mapAreaHeight,
|
|
18474
18494
|
legendReserveGap,
|
|
18495
|
+
legendPosition,
|
|
18475
18496
|
showLegend,
|
|
18476
18497
|
droppedFeatures
|
|
18477
18498
|
} = opts;
|
|
@@ -18565,14 +18586,14 @@ function buildCategoricalMarks(opts) {
|
|
|
18565
18586
|
shape: "square"
|
|
18566
18587
|
}));
|
|
18567
18588
|
const legendX = fullArea.x;
|
|
18568
|
-
const legendY = fullArea.y + mapAreaHeight + legendReserveGap;
|
|
18589
|
+
const legendY = legendPosition === "bottom" ? fullArea.y + mapAreaHeight + legendReserveGap : fullArea.y + legendReserveGap;
|
|
18569
18590
|
const legendWidth = fullArea.width;
|
|
18570
18591
|
const swatchSize = 10;
|
|
18571
18592
|
const swatchGap = 6;
|
|
18572
18593
|
const entryGap = 16;
|
|
18573
18594
|
legend = {
|
|
18574
18595
|
type: "categorical",
|
|
18575
|
-
position:
|
|
18596
|
+
position: legendPosition,
|
|
18576
18597
|
bounds: { x: legendX, y: legendY, width: legendWidth, height: swatchSize + 6 },
|
|
18577
18598
|
labelStyle,
|
|
18578
18599
|
entries,
|
|
@@ -18584,8 +18605,38 @@ function buildCategoricalMarks(opts) {
|
|
|
18584
18605
|
}
|
|
18585
18606
|
return { marks, legend };
|
|
18586
18607
|
}
|
|
18587
|
-
function resolveFocus(focus, features, warnings) {
|
|
18608
|
+
function resolveFocus(focus, features, pointMarks, warnings) {
|
|
18588
18609
|
if (focus === null || focus === void 0) return null;
|
|
18610
|
+
if (typeof focus === "object" && !Array.isArray(focus) && "points" in focus) {
|
|
18611
|
+
const filter = focus.points;
|
|
18612
|
+
const matched = filter === true ? pointMarks : pointMarks.filter((p) => p.data[filter.field] === filter.value);
|
|
18613
|
+
if (matched.length === 0) {
|
|
18614
|
+
const detail = filter === true ? "the map has no points to fit" : `no points match { field: "${filter.field}", value: ${JSON.stringify(filter.value)} }`;
|
|
18615
|
+
warnings.push({
|
|
18616
|
+
code: "FOCUS_UNMATCHED",
|
|
18617
|
+
message: `geo.focus.points is set but ${detail}`,
|
|
18618
|
+
context: {}
|
|
18619
|
+
});
|
|
18620
|
+
return null;
|
|
18621
|
+
}
|
|
18622
|
+
const padding2 = focus.padding ?? 16;
|
|
18623
|
+
let minX2 = Infinity;
|
|
18624
|
+
let minY2 = Infinity;
|
|
18625
|
+
let maxX2 = -Infinity;
|
|
18626
|
+
let maxY2 = -Infinity;
|
|
18627
|
+
for (const p of matched) {
|
|
18628
|
+
minX2 = Math.min(minX2, p.cx - p.r);
|
|
18629
|
+
minY2 = Math.min(minY2, p.cy - p.r);
|
|
18630
|
+
maxX2 = Math.max(maxX2, p.cx + p.r);
|
|
18631
|
+
maxY2 = Math.max(maxY2, p.cy + p.r);
|
|
18632
|
+
}
|
|
18633
|
+
return {
|
|
18634
|
+
target: { x: minX2, y: minY2, width: maxX2 - minX2, height: maxY2 - minY2, padding: padding2 },
|
|
18635
|
+
// No feature ids: focus-dim is a feature concept, so a points focus
|
|
18636
|
+
// dims no features (there are no "other" features to mute).
|
|
18637
|
+
ids: []
|
|
18638
|
+
};
|
|
18639
|
+
}
|
|
18589
18640
|
let ids;
|
|
18590
18641
|
let padding = 16;
|
|
18591
18642
|
if (typeof focus === "string" || typeof focus === "number") {
|