ggaction 0.0.18 → 0.0.19
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/CHANGELOG.md +50 -0
- package/README.md +6 -2
- package/knowledge/action-cards.json +1 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/package.json +1 -1
- package/src/actions/canvas/fitting.js +6 -2
- package/src/actions/charts/heatmap.js +4 -3
- package/src/actions/data/derived.js +11 -1
- package/src/actions/data/edit.js +2 -0
- package/src/actions/data/index.js +4 -0
- package/src/actions/data/regression.js +24 -16
- package/src/actions/data/regressionStatistics.js +73 -0
- package/src/actions/data/shared.js +5 -2
- package/src/actions/distributions/revision.js +24 -18
- package/src/actions/encodings/remove.js +11 -6
- package/src/actions/encodings/shared.js +26 -24
- package/src/actions/facets/actions.js +18 -7
- package/src/actions/facets/guides.js +2 -1
- package/src/actions/guides/axes/axes.js +30 -11
- package/src/actions/guides/axes/axis.js +58 -8
- package/src/actions/guides/axes/components.js +13 -13
- package/src/actions/guides/axes/edit.js +36 -24
- package/src/actions/guides/axes/labels.js +37 -27
- package/src/actions/guides/axes/lines.js +22 -20
- package/src/actions/guides/axes/remove.js +10 -6
- package/src/actions/guides/axes/tickGroups.js +8 -7
- package/src/actions/guides/axes/ticks.js +31 -22
- package/src/actions/guides/axes/titles.js +53 -60
- package/src/actions/guides/facadeAxes.js +33 -24
- package/src/actions/guides/facadeLegend.js +3 -3
- package/src/actions/guides/grids/grid.js +1 -1
- package/src/actions/guides/grids/resolve.js +48 -51
- package/src/actions/guides/legends/blocks.js +43 -19
- package/src/actions/guides/legends/categorical/actions.js +30 -7
- package/src/actions/guides/legends/categorical/components.js +3 -10
- package/src/actions/guides/legends/categorical/index.js +2 -0
- package/src/actions/guides/legends/categorical/layout.js +9 -13
- package/src/actions/guides/legends/categorical/options.js +8 -2
- package/src/actions/guides/legends/categorical/recipes.js +7 -3
- package/src/actions/guides/legends/categorical/resolve.js +9 -2
- package/src/actions/guides/legends/categorical/symbols.js +22 -8
- package/src/actions/guides/legends/colorSize.js +110 -0
- package/src/actions/guides/legends/combine.js +37 -0
- package/src/actions/guides/legends/continuous/colorSymbols.js +1 -1
- package/src/actions/guides/legends/continuous/common.js +1 -1
- package/src/actions/guides/legends/continuous/gradient.js +27 -13
- package/src/actions/guides/legends/continuous/opacity.js +16 -11
- package/src/actions/guides/legends/creation.js +2 -0
- package/src/actions/guides/legends/edit.js +1 -1
- package/src/actions/guides/legends/focused.js +6 -5
- package/src/actions/guides/legends/lane.js +1 -1
- package/src/actions/guides/legends/lifecycle.js +2 -1
- package/src/actions/guides/legends/remove.js +6 -3
- package/src/actions/guides/legends/size.js +16 -6
- package/src/actions/guides/legends/strokeWidth.js +2 -0
- package/src/actions/guides/legends/target.js +2 -79
- package/src/actions/guides/legends/transition.js +24 -5
- package/src/actions/guides/tickValues.js +8 -5
- package/src/actions/marks/bar/edit.js +23 -12
- package/src/actions/marks/bar/materialize.js +6 -1
- package/src/actions/marks/point/materialize.js +5 -1
- package/src/actions/marks/remove.js +10 -10
- package/src/actions/primitives/createGraphics.js +3 -2
- package/src/actions/primitives/editGraphics.js +15 -11
- package/src/actions/primitives/graphicProperties.js +1 -6
- package/src/actions/primitives/semanticAction.js +27 -1
- package/src/actions/primitives/semanticValidation/guide.js +11 -1
- package/src/actions/primitives/semanticValidation/layer.js +4 -0
- package/src/actions/regression/edit.js +15 -3
- package/src/actions/scales/channels.js +2 -2
- package/src/actions/scales/create.js +3 -3
- package/src/actions/scales/definitions.js +1 -1
- package/src/actions/scales/edit.js +1 -1
- package/src/actions/scales/editPolicy.js +3 -0
- package/src/actions/scales/patch.js +1 -1
- package/src/actions/scales/quantitativeColor.js +8 -3
- package/src/actions/theme/reconcile.js +50 -23
- package/src/core/compositionState.js +2 -2
- package/src/core/graphicProperties.js +1 -0
- package/src/core/programState.js +4 -0
- package/src/core/resourceReferences.js +2 -2
- package/src/core/validation.js +9 -0
- package/src/core/vocabulary.js +3 -0
- package/src/grammar/axisTextPlacement.js +1 -0
- package/src/grammar/colorSizeLegend.js +39 -0
- package/src/grammar/computed.js +23 -1
- package/src/grammar/datasetSchema.js +10 -2
- package/src/grammar/displayLabels.js +8 -0
- package/src/grammar/facets/guides.js +54 -31
- package/src/grammar/facets/legendIntent.js +65 -0
- package/src/grammar/guideDash.js +1 -0
- package/src/grammar/legendBlocks.js +1 -0
- package/src/grammar/legendBorder.js +9 -0
- package/src/grammar/legendClip.js +19 -0
- package/src/grammar/legendCombinations.js +45 -0
- package/src/grammar/legendGradient.js +9 -0
- package/src/grammar/legendGroups.js +1 -1
- package/src/grammar/legendOpacity.js +6 -0
- package/src/grammar/pointShapes.js +16 -2
- package/src/grammar/regression/derive.js +9 -1
- package/src/grammar/regression/models.js +60 -0
- package/src/grammar/regression/parameters.js +9 -1
- package/src/grammar/regression/statistics.js +64 -0
- package/src/grammar/scales/color.js +1 -1
- package/src/grammar/scales/colorConsumers.js +1 -1
- package/src/grammar/scales/definition.js +1 -1
- package/src/grammar/scales/types.js +1 -1
- package/src/grammar/schemas/concreteGraphic.js +3 -0
- package/src/grammar/schemas/graphic.js +3 -0
- package/src/grammar/schemas/graphicBounds.js +31 -7
- package/src/grammar/schemas/semanticPath.js +17 -2
- package/src/grammar/summary.js +1 -1
- package/src/grammar/timeUnit.js +6 -4
- package/src/grammar/transformTopology.js +1 -0
- package/src/grammar/transforms.js +11 -1
- package/src/inspectionDescriptors.js +1 -1
- package/src/layout/guideCollisions.js +9 -1
- package/src/layout/legendGroup.js +1 -1
- package/src/materialization/compositionSnapshot.js +2 -7
- package/src/materialization/coordinateBounds.js +17 -4
- package/src/materialization/facetGuides/edit.js +30 -0
- package/src/materialization/facetGuides/layout.js +1 -1
- package/src/materialization/facetGuides/owner.js +125 -0
- package/src/materialization/facetGuides/ownerLayout.js +71 -0
- package/src/materialization/facetGuides/placement.js +52 -6
- package/src/materialization/facetGuides/preparation.js +7 -0
- package/src/materialization/facets.js +1 -1
- package/src/materialization/graphicHierarchy.js +5 -1
- package/src/materialization/graphicTranslation.js +73 -0
- package/src/materialization/guides/layout.js +1 -1
- package/src/materialization/guides/resources.js +1 -1
- package/src/materialization/legendBorder.js +28 -0
- package/src/materialization/legendClip.js +76 -0
- package/src/materialization/legendGroups.js +1 -1
- package/src/materialization/legends.js +1 -1
- package/src/materialization/scaleGuideDependencies.js +13 -14
- package/src/materialization/scales/map.js +3 -0
- package/src/materialization/scales/resolve.js +2 -0
- package/src/materialization/typography.js +10 -2
- package/src/persistence/validation.js +174 -1
- package/src/persistence.js +1 -1
- package/src/renderers/canvas/index.js +20 -10
- package/src/renderers/canvas/isolated.js +166 -0
- package/src/renderers/canvas/limits.js +3 -0
- package/src/renderers/canvas/rect.js +1 -1
- package/src/renderers/pdf.js +2 -2
- package/src/renderers/png.js +2 -2
- package/src/renderers/svg.js +10 -3
- package/src/selectors/axes.js +1 -0
- package/src/selectors/graphicCapacity.js +38 -0
- package/src/version.js +1 -1
- package/types/basic.d.ts +2 -0
- package/types/index.d.ts +5 -0
- package/types/program.d.ts +206 -61
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { synchronizeFacetLegendAppearance } from "../../materialization/facetGuides/owner.js";
|
|
1
2
|
import { rematerializeTypography } from "../../materialization/typography.js";
|
|
2
3
|
import { AXIS_LABEL_FONT_SIZES, AXIS_TITLE_FONT_SIZE, themeTokens } from "../../theme/defaults.js";
|
|
3
4
|
import {
|
|
@@ -182,7 +183,7 @@ function componentRole(program, id, property) {
|
|
|
182
183
|
}
|
|
183
184
|
|
|
184
185
|
function graphicRole(program, id, property) {
|
|
185
|
-
const axisPart = /Axis(Labels|Titles?)$/u.exec(id);
|
|
186
|
+
const axisPart = /(?:Axis|^axis_.+__)(Labels|Titles?)$/u.exec(id);
|
|
186
187
|
const scoped = axisPart === null ? undefined : axisTextRole(property, axisPart[1] === "Labels" ? "labels" : "title");
|
|
187
188
|
if (scoped !== undefined) return scoped;
|
|
188
189
|
if (property === "fontFamily") return "fontFamily";
|
|
@@ -200,11 +201,11 @@ function graphicRole(program, id, property) {
|
|
|
200
201
|
}
|
|
201
202
|
return layer.mark?.type === "text" ? "text" : "mark";
|
|
202
203
|
}
|
|
203
|
-
if (/
|
|
204
|
-
if (/
|
|
204
|
+
if (/(?:Axis|^axis_.+__)Lines?$/u.test(id) && property === "stroke") return "axis";
|
|
205
|
+
if (/(?:Axis|^axis_.+__)Ticks$/u.test(id) && property === "stroke") return "mutedText";
|
|
205
206
|
if (/^parallelAxisLabels$/u.test(id) && property === "fill") return "axis";
|
|
206
|
-
if (/
|
|
207
|
-
if (/
|
|
207
|
+
if (/(?:Axis|^axis_.+__)Labels$/u.test(id) && property === "fill") return "text";
|
|
208
|
+
if (/(?:Axis|^axis_.+__)Titles?$/u.test(id) && property === "fill") return "axisTitle";
|
|
208
209
|
if (/Grid(?:Lines|Circles)?$/u.test(id) && property === "stroke") return "grid";
|
|
209
210
|
if (id === "chartTitle" && property === "fill") return "strongText";
|
|
210
211
|
if (id === "chartSubtitle" && property === "fill") return "mutedText";
|
|
@@ -676,12 +677,13 @@ function clearRemovedResources(overrides, node) {
|
|
|
676
677
|
clearPrefix(overrides, `g:${mark[1]}.`);
|
|
677
678
|
return;
|
|
678
679
|
}
|
|
679
|
-
const axis = /^guide\.axis\.(
|
|
680
|
+
const axis = /^guide\.axis\.([A-Za-z0-9_-]+)$/u
|
|
680
681
|
.exec(current.args.property);
|
|
681
682
|
if (axis !== null) {
|
|
682
683
|
const prefix = axis[1] === "radius" ? "radial" : axis[1];
|
|
684
|
+
const graphicPrefix = ["x", "y", "theta", "radius"].includes(axis[1]) ? `${prefix}Axis` : `axis_${axis[1]}__`;
|
|
683
685
|
clearPrefix(overrides, `c:guides.axis.${axis[1]}.`);
|
|
684
|
-
clearPrefix(overrides, `g:${
|
|
686
|
+
clearPrefix(overrides, `g:${graphicPrefix}`);
|
|
685
687
|
return;
|
|
686
688
|
}
|
|
687
689
|
const grid = /^guide\.grid\.([^\.]+)$/u.exec(current.args.property);
|
|
@@ -737,7 +739,23 @@ function clearRecreatedOverrides(overrides, op, args) {
|
|
|
737
739
|
}
|
|
738
740
|
}
|
|
739
741
|
|
|
740
|
-
function
|
|
742
|
+
function axisOverrideId(node, channel, args) {
|
|
743
|
+
if (args.target !== undefined || args.id !== undefined) return args.target ?? args.id;
|
|
744
|
+
const ids = new Set();
|
|
745
|
+
const visit = entry => {
|
|
746
|
+
if (["createGraphics", "editGraphics"].includes(entry.op)) {
|
|
747
|
+
const id = entry.args.target ?? entry.args.id;
|
|
748
|
+
const named = /^axis_(.+)__(Line|Ticks|Labels|Title)$/u.exec(id ?? "");
|
|
749
|
+
if (named !== null) ids.add(named[1]);
|
|
750
|
+
else if (new RegExp(`^${channel}Axis(?:Line|Ticks|Labels|Title)$`).test(id ?? "")) ids.add(channel);
|
|
751
|
+
}
|
|
752
|
+
entry.children?.forEach(visit);
|
|
753
|
+
};
|
|
754
|
+
if (node !== undefined) visit(node);
|
|
755
|
+
return ids.size === 1 ? [...ids][0] : channel;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function addAxisOverrides(overrides, op, args, node) {
|
|
741
759
|
const match = /^(?:create|edit)(X|Y|Theta|Radial)Axis(Line|Ticks|Labels|Title)$/u
|
|
742
760
|
.exec(op);
|
|
743
761
|
if (match === null) return;
|
|
@@ -748,23 +766,25 @@ function addAxisOverrides(overrides, op, args) {
|
|
|
748
766
|
Radial: "radius"
|
|
749
767
|
}[match[1]];
|
|
750
768
|
const component = match[2].toLowerCase();
|
|
769
|
+
const axisId = axisOverrideId(node, channel, args);
|
|
751
770
|
const idPrefix = match[1] === "Radial" ? "radial" : channel;
|
|
752
771
|
if (Object.hasOwn(args, "color")) {
|
|
753
|
-
overrides.add(`c:guides.axis.${
|
|
772
|
+
overrides.add(`c:guides.axis.${axisId}.${component}.color`);
|
|
754
773
|
overrides.add(
|
|
755
|
-
`g:${idPrefix}Axis${match[2]}.${component === "line" || component === "ticks" ? "stroke" : "fill"}`
|
|
774
|
+
`g:${axisId === channel ? `${idPrefix}Axis` : `axis_${axisId}__`}${match[2]}.${component === "line" || component === "ticks" ? "stroke" : "fill"}`
|
|
756
775
|
);
|
|
757
776
|
}
|
|
758
777
|
for (const property of ["fontFamily", "fontSize"]) {
|
|
759
778
|
if (["labels", "title"].includes(component) && Object.hasOwn(args, property)) {
|
|
760
|
-
overrides.add(`c:guides.axis.${
|
|
761
|
-
overrides.add(`g:${idPrefix}Axis${match[2]}.${property}`);
|
|
779
|
+
overrides.add(`c:guides.axis.${axisId}.${component}.${property}`);
|
|
780
|
+
overrides.add(`g:${axisId === channel ? `${idPrefix}Axis` : `axis_${axisId}__`}${match[2]}.${property}`);
|
|
762
781
|
}
|
|
763
782
|
}
|
|
764
783
|
}
|
|
765
784
|
|
|
766
|
-
function addCompleteAxisOverride(overrides, name, args) {
|
|
785
|
+
function addCompleteAxisOverride(overrides, name, args, node) {
|
|
767
786
|
const channel = { X: "x", Y: "y", Theta: "theta", Radial: "radius" }[name];
|
|
787
|
+
const axisId = axisOverrideId(node, channel, args);
|
|
768
788
|
const idPrefix = name === "Radial" ? "radial" : channel;
|
|
769
789
|
const components = {
|
|
770
790
|
line: args.line,
|
|
@@ -779,21 +799,21 @@ function addCompleteAxisOverride(overrides, name, args) {
|
|
|
779
799
|
for (const [component, options] of Object.entries(components)) {
|
|
780
800
|
const suffix = `${component[0].toUpperCase()}${component.slice(1)}`;
|
|
781
801
|
if (Object.hasOwn(options ?? {}, "color")) {
|
|
782
|
-
overrides.add(`c:guides.axis.${
|
|
802
|
+
overrides.add(`c:guides.axis.${axisId}.${component}.color`);
|
|
783
803
|
overrides.add(
|
|
784
|
-
`g:${idPrefix}Axis${suffix}.${component === "line" || component === "ticks" ? "stroke" : "fill"}`
|
|
804
|
+
`g:${axisId === channel ? `${idPrefix}Axis` : `axis_${axisId}__`}${suffix}.${component === "line" || component === "ticks" ? "stroke" : "fill"}`
|
|
785
805
|
);
|
|
786
806
|
}
|
|
787
807
|
for (const property of ["fontFamily", "fontSize"]) {
|
|
788
808
|
if (["labels", "title"].includes(component) && Object.hasOwn(options ?? {}, property)) {
|
|
789
|
-
overrides.add(`c:guides.axis.${
|
|
790
|
-
overrides.add(`g:${idPrefix}Axis${suffix}.${property}`);
|
|
809
|
+
overrides.add(`c:guides.axis.${axisId}.${component}.${property}`);
|
|
810
|
+
overrides.add(`g:${axisId === channel ? `${idPrefix}Axis` : `axis_${axisId}__`}${suffix}.${property}`);
|
|
791
811
|
}
|
|
792
812
|
}
|
|
793
813
|
}
|
|
794
814
|
}
|
|
795
815
|
|
|
796
|
-
function addCompleteAxisOverrides(overrides, op, args) {
|
|
816
|
+
function addCompleteAxisOverrides(overrides, op, args, node) {
|
|
797
817
|
if (args.guides?.axes !== null && typeof args.guides?.axes === "object") {
|
|
798
818
|
addCompleteAxisOverrides(overrides, "createAxes", args.guides.axes);
|
|
799
819
|
}
|
|
@@ -801,10 +821,10 @@ function addCompleteAxisOverrides(overrides, op, args) {
|
|
|
801
821
|
addCompleteAxisOverrides(overrides, "createAxes", args.axes);
|
|
802
822
|
}
|
|
803
823
|
const match = /^(?:create|edit)(X|Y|Theta|Radial)Axis$/u.exec(op);
|
|
804
|
-
if (match !== null) addCompleteAxisOverride(overrides, match[1], args);
|
|
824
|
+
if (match !== null) addCompleteAxisOverride(overrides, match[1], args, node);
|
|
805
825
|
const group = /^(?:create|edit)(X|Y)AxisTicksAndLabels$/u.exec(op);
|
|
806
826
|
if (group !== null) {
|
|
807
|
-
addCompleteAxisOverride(overrides, group[1], { ticksAndLabels: args });
|
|
827
|
+
addCompleteAxisOverride(overrides, group[1], { target: args.target, ticksAndLabels: args }, node);
|
|
808
828
|
}
|
|
809
829
|
if (op !== "createAxes") return;
|
|
810
830
|
for (const [option, name] of [
|
|
@@ -814,7 +834,7 @@ function addCompleteAxisOverrides(overrides, op, args) {
|
|
|
814
834
|
["radius", "Radial"]
|
|
815
835
|
]) {
|
|
816
836
|
if (args[option] && typeof args[option] === "object") {
|
|
817
|
-
addCompleteAxisOverride(overrides, name,
|
|
837
|
+
for (const entry of Array.isArray(args[option]) ? args[option] : [args[option]]) addCompleteAxisOverride(overrides, name, entry);
|
|
818
838
|
}
|
|
819
839
|
}
|
|
820
840
|
}
|
|
@@ -977,8 +997,8 @@ function explicitOverrides(program, node) {
|
|
|
977
997
|
addErrorBarOverrides(overrides, program, node);
|
|
978
998
|
addErrorBandOverrides(overrides, program, node);
|
|
979
999
|
addRegressionOverrides(overrides, node);
|
|
980
|
-
addAxisOverrides(overrides, op, args);
|
|
981
|
-
addCompleteAxisOverrides(overrides, op, args);
|
|
1000
|
+
addAxisOverrides(overrides, op, args, node);
|
|
1001
|
+
addCompleteAxisOverrides(overrides, op, args, node);
|
|
982
1002
|
addGridOverrides(overrides, op, args);
|
|
983
1003
|
addParallelOverrides(overrides, program, op, args);
|
|
984
1004
|
if (["createTitle", "editTitle"].includes(op)) {
|
|
@@ -999,6 +1019,11 @@ function explicitOverrides(program, node) {
|
|
|
999
1019
|
if (op === "editLegendBlock") {
|
|
1000
1020
|
addLegendBlockOverrides(overrides, program, args);
|
|
1001
1021
|
}
|
|
1022
|
+
const parentLegendOptions = op === "editFacetGuides" ? args.legend
|
|
1023
|
+
: ["facet", "facetGrid", "repeatCharts"].includes(op) ? args.guides?.legend : undefined;
|
|
1024
|
+
if (parentLegendOptions !== null && typeof parentLegendOptions === "object") {
|
|
1025
|
+
addLegendOverrides(overrides, program, parentLegendOptions);
|
|
1026
|
+
}
|
|
1002
1027
|
if (op === "editFacetHeaders") {
|
|
1003
1028
|
addFacetOverrides(overrides, program, args);
|
|
1004
1029
|
}
|
|
@@ -1084,6 +1109,8 @@ export function reconcileProgramTheme(program, { source, metadata }) {
|
|
|
1084
1109
|
next = next._withMaterializationConfig(["facets"], configs.value.facets);
|
|
1085
1110
|
}
|
|
1086
1111
|
|
|
1112
|
+
if (configs.changed) next = synchronizeFacetLegendAppearance(next);
|
|
1113
|
+
|
|
1087
1114
|
const facetsChanged = configs.value.facets !==
|
|
1088
1115
|
themed.materializationConfigs.facets;
|
|
1089
1116
|
const typographyChanged = sourceTokens.fontFamily !== targetTokens.fontFamily ||
|
|
@@ -202,10 +202,10 @@ function validateFacetSpec(compositionSpec) {
|
|
|
202
202
|
!isPlainObject(guides) ||
|
|
203
203
|
Object.keys(guides).some(key => !["axes", "legend"].includes(key)) ||
|
|
204
204
|
!["each", "outer"].includes(guides.axes) ||
|
|
205
|
-
![false, "shared"].includes(guides.legend)
|
|
205
|
+
(![false, "shared"].includes(guides.legend) && !isPlainObject(guides.legend))
|
|
206
206
|
) {
|
|
207
207
|
throw new Error(
|
|
208
|
-
'compositionSpec.facet.guides requires axes "each" or "outer" and legend false
|
|
208
|
+
'compositionSpec.facet.guides requires axes "each" or "outer" and legend false, "shared", or parent legend options.'
|
|
209
209
|
);
|
|
210
210
|
}
|
|
211
211
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function n(e,t,i){for(const[r,c]of Object.entries(i))e=e.editGraphics({target:t,property:r,value:c});return e}export{n as editGraphicProperties};
|
package/src/core/programState.js
CHANGED
|
@@ -44,6 +44,10 @@ export function releaseNamedResourceOwnership(program, { kind, id, dataOwner })
|
|
|
44
44
|
materializationConfigs,
|
|
45
45
|
["calculations", "datasets", dataOwner?.current ?? id]
|
|
46
46
|
).value;
|
|
47
|
+
materializationConfigs = removeMaterializationConfig(
|
|
48
|
+
materializationConfigs,
|
|
49
|
+
["regressionFits", dataOwner?.current ?? id]
|
|
50
|
+
).value;
|
|
47
51
|
const context = program.context.currentData === id
|
|
48
52
|
? freezeOwned({ ...program.context, currentData: undefined })
|
|
49
53
|
: program.context;
|
|
@@ -97,7 +97,7 @@ export function collectSemanticScaleReferences(program) {
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
collectKnownKeys(program.semanticSpec.guides, [], (key, value, path) => {
|
|
100
|
-
if (
|
|
100
|
+
if (["scale", "colorScale", "sizeScale"].includes(key)) {
|
|
101
101
|
append(references, reference(
|
|
102
102
|
"scale", value, "semanticGuide", "guides", path
|
|
103
103
|
));
|
|
@@ -208,7 +208,7 @@ export function collectGuideConfigReferences(program) {
|
|
|
208
208
|
const relativePath = parentPath.length === 0
|
|
209
209
|
? path
|
|
210
210
|
: path.slice(parentPath.length);
|
|
211
|
-
if (
|
|
211
|
+
if (["scale", "colorScale", "sizeScale"].includes(key)) {
|
|
212
212
|
append(references, reference(
|
|
213
213
|
"scale", value, family, ownerId, relativePath
|
|
214
214
|
));
|
package/src/core/validation.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { annotateError } from "./diagnostics.js";
|
|
2
2
|
import { isPlainObject } from "./immutable.js";
|
|
3
|
+
import { getMarkGraphicTypes } from "./vocabulary.js";
|
|
3
4
|
|
|
4
5
|
export const MAX_GENERATED_ITEMS = 10_000;
|
|
5
6
|
export const MAX_WORK_ITEMS = 10_000_000;
|
|
@@ -30,6 +31,14 @@ export function validateWorkLimit(value, label) {
|
|
|
30
31
|
return validateGeneratedItemLimit(value, label, MAX_WORK_ITEMS);
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
export function validateGraphicItemLimit(value, capacity) {
|
|
35
|
+
const point = capacity.pointOwned && getMarkGraphicTypes("point").includes(capacity.graphicType);
|
|
36
|
+
const label = point ? "Point mark item count" : capacity.label;
|
|
37
|
+
const maximum = point ? 50_000 : MAX_GENERATED_ITEMS;
|
|
38
|
+
if (point) validateGeneratedItemLimit(capacity.rawCount, label, maximum);
|
|
39
|
+
return validateGeneratedItemLimit(value, label, maximum);
|
|
40
|
+
}
|
|
41
|
+
|
|
33
42
|
export function validateKeys(value, supported, label) {
|
|
34
43
|
for (const key of Object.keys(value)) {
|
|
35
44
|
if (!supported.includes(key)) {
|
package/src/core/vocabulary.js
CHANGED
|
@@ -220,6 +220,7 @@ export const LEGEND_CHANNELS = Object.freeze([
|
|
|
220
220
|
]);
|
|
221
221
|
|
|
222
222
|
export const LEGEND_CONFIG_KINDS = Object.freeze([
|
|
223
|
+
"colorSize",
|
|
223
224
|
"series",
|
|
224
225
|
"color",
|
|
225
226
|
"stroke",
|
|
@@ -239,3 +240,5 @@ export function includesVocabulary(vocabulary, value) {
|
|
|
239
240
|
export const PLOT_LEGEND_POSITIONS = Object.freeze(["top-left", "top-right", "bottom-left", "bottom-right"]);
|
|
240
241
|
export const LEGEND_POSITIONS = Object.freeze(["left", "right", "top", "bottom", ...PLOT_LEGEND_POSITIONS]);
|
|
241
242
|
export const LEGEND_BLOCK_LAYOUTS = Object.freeze(["row", "column", "wrap"]);
|
|
243
|
+
|
|
244
|
+
export const AXIS_RESOURCE_CHANNELS = Object.freeze(["x", "y", "theta", "radius", "parallel"]);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{resolveTextLines as o}from"../core/textMetrics.js";function a(e,{title:i=!1}={}){if(Object.hasOwn(e,"align")&&!["left","center","right"].includes(e.align))throw new TypeError("Axis text align must be left, center, or right.");if(i){if(Object.hasOwn(e,"baseline")&&!["top","hanging","middle","alphabetic","ideographic","bottom"].includes(e.baseline))throw new TypeError("Axis title baseline must be a supported text baseline.");for(const t of["offset","alongOffset"])if(Object.hasOwn(e,t)&&!Number.isFinite(e[t]))throw new TypeError(`Axis title ${t} must be a finite number.`)}}function f(e,i){if(!/[\r\n]/u.test(e))return;const t=i.baseline??"middle",n=o(e,{fontSize:i.fontSize,align:i.align??"center",baseline:t,...t==="middle"?{blockAlign:"middle"}:{}});if(["bottom","ideographic"].includes(t)){const s=(n.length-1)*i.fontSize*1.2;return n.map(r=>({...r,y:r.y-s}))}return n.length>1?n:void 0}export{f as axisTitleTextLines,a as validateAxisTextPlacement};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {sameOrderedValues,validateOptionObject,validateUnitInterval,validateNonNegativeFinite,validateNonEmptyString} from '../core/validation.js';
|
|
2
|
+
import {isContinuousColorScaleType} from './scales/types.js';
|
|
3
|
+
import {isContinuousSizeScaleType} from './scales/size.js';
|
|
4
|
+
|
|
5
|
+
export function isColorSizeChannels(channels) {
|
|
6
|
+
return Array.isArray(channels)&&channels.length===2&&channels.includes('color')&&channels.includes('size');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function resolveColorSizeMapping({layer,scales,pointConfig,coordinate}) {
|
|
10
|
+
const color=layer?.encoding?.color,size=layer?.encoding?.size;
|
|
11
|
+
const colorScale=scales[color?.scale],sizeScale=scales[size?.scale];
|
|
12
|
+
if(layer?.mark?.type!=='point'||coordinate?.type!=='cartesian'||
|
|
13
|
+
pointConfig?.shape!=='circle'||layer.encoding.shape!==undefined||
|
|
14
|
+
color?.fieldType!=='quantitative'||size?.fieldType!=='quantitative'||
|
|
15
|
+
typeof color.field!=='string'||color.field!==size.field||
|
|
16
|
+
color.aggregate!==size.aggregate||color.data!==size.data||
|
|
17
|
+
!isContinuousColorScaleType(colorScale?.type)||!isContinuousSizeScaleType(sizeScale?.type)) {
|
|
18
|
+
throw new Error('Color-size combination requires one Cartesian circle point owner with the same quantitative field and grain.');
|
|
19
|
+
}
|
|
20
|
+
const ends=scale=>[scale.domain?.[0],scale.domain?.at(-1)];
|
|
21
|
+
const domain=ends(colorScale),sizeDomain=ends(sizeScale);
|
|
22
|
+
if(!domain.every(Number.isFinite)||!sizeDomain.every(Number.isFinite)||
|
|
23
|
+
!sameOrderedValues(domain,sizeDomain)||domain[0]===domain[1]) {
|
|
24
|
+
throw new Error('Color-size combination requires identical finite numeric domain endpoints.');
|
|
25
|
+
}
|
|
26
|
+
return {target:layer.id,field:color.field,fieldType:'quantitative',colorScale:color.scale,sizeScale:size.scale,domain};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function normalizeColorSizeSymbol(value,previous) {
|
|
30
|
+
if(value===undefined)return {...(previous??{type:'point',strokeWidth:0,opacity:1})};
|
|
31
|
+
if(value==='auto')return {type:'point',strokeWidth:0,opacity:1};
|
|
32
|
+
validateOptionObject(value,['type','stroke','strokeWidth','opacity'],'Color-size symbol');
|
|
33
|
+
if(value.type!==undefined&&value.type!=='point')throw new Error('Color-size symbols require type point.');
|
|
34
|
+
const symbol={type:'point',strokeWidth:0,opacity:1,...previous,...value};
|
|
35
|
+
if(symbol.stroke!==undefined)validateNonEmptyString(symbol.stroke,'Color-size symbol stroke');
|
|
36
|
+
validateNonNegativeFinite(symbol.strokeWidth,'Color-size symbol strokeWidth');
|
|
37
|
+
validateUnitInterval(symbol.opacity,'Color-size symbol opacity');
|
|
38
|
+
return symbol;
|
|
39
|
+
}
|
package/src/grammar/computed.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {parseTimeValue,validateParseTimeFormat} from "./parseTime.js";
|
|
2
|
+
import { calendarParts, validateTimeZone } from "./timeUnit.js";
|
|
2
3
|
import { rejectUnknownProperties as rejectUnknownKeys } from "../core/validation.js";
|
|
3
4
|
import { requireStringValue as requireField } from "../core/validation.js";
|
|
4
5
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
@@ -6,7 +7,7 @@ import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
|
6
7
|
const TRANSFORM_KEYS = Object.freeze(["type", "as", "expression"]);
|
|
7
8
|
const ARITHMETIC_BINARY = new Set(["add", "subtract", "multiply", "divide"]);
|
|
8
9
|
const COMPARISONS = new Set(["eq", "neq", "lt", "lte", "gt", "gte"]);
|
|
9
|
-
const NUMERIC_UNARY = new Set(["negate", "absolute", "log", "sqrt"]);
|
|
10
|
+
const NUMERIC_UNARY = new Set(["negate", "absolute", "log", "sqrt", "floor"]);
|
|
10
11
|
const BOOLEAN_UNARY = new Set(["not", "isNull"]);
|
|
11
12
|
const LOGICAL = new Set(["and", "or"]);
|
|
12
13
|
const VARIADIC = new Set(["coalesce", "concat"]);
|
|
@@ -107,6 +108,14 @@ function inspectExpression(expression, depth, state, optionalPathRoot = false) {
|
|
|
107
108
|
inspectExpression(expression.operand, depth + 1, state);
|
|
108
109
|
return;
|
|
109
110
|
}
|
|
111
|
+
if (expression.op === "datePart") {
|
|
112
|
+
rejectUnknownKeys(expression, ["op", "part", "operand", "timeZone"], "computed calendar expression");
|
|
113
|
+
requireNode(expression, "operand", expression.op);
|
|
114
|
+
if (expression.part !== "year") throw new Error("Computed datePart supports part year only.");
|
|
115
|
+
if (Object.hasOwn(expression, "timeZone")) validateTimeZone(expression.timeZone);
|
|
116
|
+
inspectExpression(expression.operand, depth + 1, state);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
110
119
|
if (expression.op === "parseTime") {
|
|
111
120
|
rejectUnknownKeys(expression, ["op", "operand", "format"], "computed time expression");
|
|
112
121
|
requireNode(expression, "operand", expression.op);
|
|
@@ -339,6 +348,18 @@ function evaluate(expression, row, rowIndex) {
|
|
|
339
348
|
? value === null
|
|
340
349
|
: !boolean(value, operation, rowIndex);
|
|
341
350
|
}
|
|
351
|
+
if (operation === "datePart") {
|
|
352
|
+
const value = evaluate(expression.operand, row, rowIndex);
|
|
353
|
+
if (value === null) return null;
|
|
354
|
+
number(value, operation, rowIndex);
|
|
355
|
+
if (Math.abs(value) > 8_640_000_000_000_000) {
|
|
356
|
+
throw new RangeError(`Computed datePart timestamp is outside the supported Date range at row ${rowIndex}.`);
|
|
357
|
+
}
|
|
358
|
+
const timestamp = new Date(value).getTime();
|
|
359
|
+
return (expression.timeZone ?? "UTC") === "UTC"
|
|
360
|
+
? new Date(timestamp).getUTCFullYear()
|
|
361
|
+
: calendarParts(timestamp, expression.timeZone).year;
|
|
362
|
+
}
|
|
342
363
|
if (operation === "parseTime") {
|
|
343
364
|
return parseTimeValue(evaluate(expression.operand, row, rowIndex), expression.format);
|
|
344
365
|
}
|
|
@@ -360,6 +381,7 @@ function evaluate(expression, row, rowIndex) {
|
|
|
360
381
|
}
|
|
361
382
|
const result = operation === "negate" ? -value
|
|
362
383
|
: operation === "absolute" ? Math.abs(value)
|
|
384
|
+
: operation === "floor" ? Math.floor(value)
|
|
363
385
|
: operation === "log" ? Math.log(value)
|
|
364
386
|
: Math.sqrt(value);
|
|
365
387
|
if (!Number.isFinite(result)) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { requireStringValue as requireName } from "../core/validation.js";
|
|
2
2
|
import { annotateError } from "../core/diagnostics.js";
|
|
3
3
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
4
|
+
import { requireRegressionStatisticsSource, REGRESSION_STATISTIC_FIELDS } from "./regression/statistics.js";
|
|
4
5
|
|
|
5
6
|
const STORAGE_TYPES = new Set([
|
|
6
7
|
"number", "string", "boolean", "array", "object", "unknown", "mixed"
|
|
@@ -267,7 +268,7 @@ function generatedField(name, storageType, transform, role, inputs = []) {
|
|
|
267
268
|
};
|
|
268
269
|
}
|
|
269
270
|
|
|
270
|
-
export function deriveTransformSchema(inputSchema, transform, values, { sourceId, ownerId } = {}) {
|
|
271
|
+
export function deriveTransformSchema(inputSchema, transform, values, { sourceId, ownerId, sourceTransform } = {}) {
|
|
271
272
|
const input = validateDatasetSchema(inputSchema);
|
|
272
273
|
const definition = { ...transform, __sourceId: sourceId, __ownerId: ownerId };
|
|
273
274
|
const retain = ["filter", "markFilter", "sort", "computed", "timeUnit", "window", "normalize", "stack", "impute", "complete"];
|
|
@@ -300,6 +301,12 @@ export function deriveTransformSchema(inputSchema, transform, values, { sourceId
|
|
|
300
301
|
generatedField(transform.as.count, "number", definition, "count", [transform.x, transform.y])
|
|
301
302
|
];
|
|
302
303
|
if (transform.as.members !== undefined) push(generatedField(transform.as.members, "array", definition, "members"));
|
|
304
|
+
} else if (transform.type === "regressionStatistics") {
|
|
305
|
+
const fit = requireRegressionStatisticsSource({ transform: sourceTransform === undefined ? [] : [sourceTransform] });
|
|
306
|
+
fields = [
|
|
307
|
+
...(fit.groupBy === undefined ? [] : [inheritedField(input, fit.groupBy)]),
|
|
308
|
+
...REGRESSION_STATISTIC_FIELDS.map(name => generatedField(name, "number", definition, name))
|
|
309
|
+
].filter(Boolean);
|
|
303
310
|
} else if (transform.type === "regression") {
|
|
304
311
|
fields = [inheritedField(input, transform.x), inheritedField(input, transform.y)].filter(Boolean);
|
|
305
312
|
if (transform.groupBy !== undefined) fields.unshift(inheritedField(input, transform.groupBy));
|
|
@@ -367,7 +374,8 @@ export function deriveTransformSchema(inputSchema, transform, values, { sourceId
|
|
|
367
374
|
if (planned.lineage === undefined && !["complete", "impute"].includes(transform.type)) {
|
|
368
375
|
return planned;
|
|
369
376
|
}
|
|
370
|
-
return { ...planned, storageType:
|
|
377
|
+
return { ...planned, storageType: transform.type === "regressionStatistics" && planned.storageType === "number"
|
|
378
|
+
? "number" : field.storageType, nullable: field.nullable, optional: field.optional };
|
|
371
379
|
});
|
|
372
380
|
for (const field of inferred.fields) {
|
|
373
381
|
if (!fields.some(planned => planned.name === field.name)) fields.push(field);
|
|
@@ -44,6 +44,14 @@ export function normalizeDisplayLabelMap(value, label = "labelMap") {
|
|
|
44
44
|
return Object.freeze(normalized);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
export function normalizeNumericDisplayLabelMap(value, label = "labelMap") {
|
|
48
|
+
const map = normalizeDisplayLabelMap(value, label);
|
|
49
|
+
if (map.some(entry => typeof entry.value !== "number" || !Number.isFinite(entry.value))) {
|
|
50
|
+
throw new TypeError(`${label} values must be finite numbers.`);
|
|
51
|
+
}
|
|
52
|
+
return map;
|
|
53
|
+
}
|
|
54
|
+
|
|
47
55
|
export function resolveDisplayLabel(value, map, fallback) {
|
|
48
56
|
if (map !== undefined) {
|
|
49
57
|
for (const entry of map) {
|
|
@@ -3,13 +3,15 @@ import { cloneAndFreeze, isPlainObject } from "../../core/immutable.js";
|
|
|
3
3
|
const LEGEND_FAMILIES = Object.freeze({
|
|
4
4
|
series: "categorical",
|
|
5
5
|
color: "categorical",
|
|
6
|
+
colorSize: "color-size",
|
|
6
7
|
stroke: "categorical",
|
|
7
8
|
gradient: "gradient",
|
|
8
9
|
interval: "discretized",
|
|
9
10
|
strokeGradient: "gradient",
|
|
10
11
|
strokeInterval: "discretized",
|
|
11
12
|
size: "size",
|
|
12
|
-
opacity: "opacity"
|
|
13
|
+
opacity: "opacity",
|
|
14
|
+
strokeWidth: "stroke-width"
|
|
13
15
|
});
|
|
14
16
|
|
|
15
17
|
function requirePlacements(placements) {
|
|
@@ -72,25 +74,34 @@ function translatedBounds(placement, bounds, channel) {
|
|
|
72
74
|
};
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
export function resolveFacetOuterSides({ placements } = {}) {
|
|
78
|
+
const cells = requirePlacements(placements);
|
|
79
|
+
const edges = { top: new Map(), bottom: new Map(), left: new Map(), right: new Map() };
|
|
80
|
+
for (const cell of cells) {
|
|
81
|
+
for (const [side, key, coordinate, better] of [
|
|
82
|
+
["top", cell.column, "row", (a, b) => a < b],
|
|
83
|
+
["bottom", cell.column, "row", (a, b) => a > b],
|
|
84
|
+
["left", cell.row, "column", (a, b) => a < b],
|
|
85
|
+
["right", cell.row, "column", (a, b) => a > b]
|
|
86
|
+
]) {
|
|
87
|
+
const previous = edges[side].get(key);
|
|
88
|
+
if (previous === undefined || better(cell[coordinate], previous[coordinate])) edges[side].set(key, cell);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return cloneAndFreeze(Object.fromEntries(Object.entries(edges).map(([side, owners]) => {
|
|
92
|
+
const ids = new Set([...owners.values()].map(cell => cell.id));
|
|
93
|
+
return [side, cells.filter(cell => ids.has(cell.id)).map(cell => cell.id)];
|
|
94
|
+
})));
|
|
95
|
+
}
|
|
96
|
+
|
|
75
97
|
export function resolveFacetOuterAxes({ placements, axisBounds = {} } = {}) {
|
|
76
98
|
const cells = requirePlacements(placements);
|
|
77
99
|
if (!isPlainObject(axisBounds)) {
|
|
78
100
|
throw new TypeError("Facet axisBounds must be a plain object.");
|
|
79
101
|
}
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
const bottom = bottomByColumn.get(cell.column);
|
|
84
|
-
if (bottom === undefined || cell.row > bottom.row) {
|
|
85
|
-
bottomByColumn.set(cell.column, cell);
|
|
86
|
-
}
|
|
87
|
-
const left = leftByRow.get(cell.row);
|
|
88
|
-
if (left === undefined || cell.column < left.column) {
|
|
89
|
-
leftByRow.set(cell.row, cell);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
const xOwners = new Set([...bottomByColumn.values()].map(cell => cell.id));
|
|
93
|
-
const yOwners = new Set([...leftByRow.values()].map(cell => cell.id));
|
|
102
|
+
const sides = resolveFacetOuterSides({ placements: cells });
|
|
103
|
+
const xOwners = new Set(sides.bottom);
|
|
104
|
+
const yOwners = new Set(sides.left);
|
|
94
105
|
return cloneAndFreeze({
|
|
95
106
|
x: cells.filter(cell => xOwners.has(cell.id)).map(cell => cell.id),
|
|
96
107
|
y: cells.filter(cell => yOwners.has(cell.id)).map(cell => cell.id),
|
|
@@ -129,7 +140,7 @@ function comparableConfig(config) {
|
|
|
129
140
|
return stableValue(rest);
|
|
130
141
|
}
|
|
131
142
|
|
|
132
|
-
function
|
|
143
|
+
export function sameFacetGuideValue(left, right) {
|
|
133
144
|
return JSON.stringify(stableValue(left)) === JSON.stringify(stableValue(right));
|
|
134
145
|
}
|
|
135
146
|
|
|
@@ -152,12 +163,13 @@ function requireLegendChildren(children) {
|
|
|
152
163
|
});
|
|
153
164
|
}
|
|
154
165
|
|
|
155
|
-
function scaleIds(config) {
|
|
156
|
-
const ids =
|
|
166
|
+
function scaleIds(config, kind) {
|
|
167
|
+
const ids = kind === "colorSize" ? [config.colorScale, config.sizeScale]
|
|
168
|
+
: config.scales ?? (config.scale === undefined ? [] : [config.scale]);
|
|
157
169
|
if (!Array.isArray(ids) || ids.length === 0 || !ids.every(id =>
|
|
158
170
|
typeof id === "string" && id.length > 0
|
|
159
171
|
)) {
|
|
160
|
-
throw new Error(`Shared ${
|
|
172
|
+
throw new Error(`Shared ${kind} legend requires resolved scale ids.`);
|
|
161
173
|
}
|
|
162
174
|
return ids;
|
|
163
175
|
}
|
|
@@ -166,7 +178,7 @@ export function resolveSharedFacetLegends(children) {
|
|
|
166
178
|
const normalized = requireLegendChildren(children);
|
|
167
179
|
const first = normalized[0];
|
|
168
180
|
for (const child of normalized.slice(1)) {
|
|
169
|
-
if (!
|
|
181
|
+
if (!sameFacetGuideValue(first.guideConfigs.legendGroups, child.guideConfigs.legendGroups)) {
|
|
170
182
|
throw new Error(`Facet child "${child.id}" has incompatible legend group placement.`);
|
|
171
183
|
}
|
|
172
184
|
}
|
|
@@ -183,7 +195,7 @@ export function resolveSharedFacetLegends(children) {
|
|
|
183
195
|
) {
|
|
184
196
|
throw new Error(`Facet child "${first.id}" has invalid ${kind} legend config.`);
|
|
185
197
|
}
|
|
186
|
-
const scales = scaleIds(config);
|
|
198
|
+
const scales = scaleIds(config, kind);
|
|
187
199
|
const resolvedScales = Object.fromEntries(scales.map(id => {
|
|
188
200
|
const scale = first.resolvedScales[id];
|
|
189
201
|
if (!isPlainObject(scale)) {
|
|
@@ -195,14 +207,14 @@ export function resolveSharedFacetLegends(children) {
|
|
|
195
207
|
const candidate = child.legends[kind];
|
|
196
208
|
if (
|
|
197
209
|
!isPlainObject(candidate) ||
|
|
198
|
-
!
|
|
210
|
+
!sameFacetGuideValue(comparableConfig(config), comparableConfig(candidate))
|
|
199
211
|
) {
|
|
200
212
|
throw new Error(
|
|
201
213
|
`Facet child "${child.id}" has an incompatible ${kind} legend config.`
|
|
202
214
|
);
|
|
203
215
|
}
|
|
204
216
|
for (const id of scales) {
|
|
205
|
-
if (!
|
|
217
|
+
if (!sameFacetGuideValue(resolvedScales[id], child.resolvedScales[id])) {
|
|
206
218
|
throw new Error(
|
|
207
219
|
`Facet child "${child.id}" has an incompatible resolved legend scale "${id}".`
|
|
208
220
|
);
|
|
@@ -213,7 +225,7 @@ export function resolveSharedFacetLegends(children) {
|
|
|
213
225
|
});
|
|
214
226
|
for (const child of normalized.slice(1)) {
|
|
215
227
|
const childKinds = Object.keys(child.legends);
|
|
216
|
-
if (!
|
|
228
|
+
if (!sameFacetGuideValue([...kinds].sort(), [...childKinds].sort())) {
|
|
217
229
|
throw new Error(`Facet child "${child.id}" has incompatible legend kinds.`);
|
|
218
230
|
}
|
|
219
231
|
}
|
|
@@ -234,7 +246,7 @@ export function planFacetGuideOwnership({
|
|
|
234
246
|
if (!["each", "outer"].includes(axes)) {
|
|
235
247
|
throw new Error("Facet axes must be each or outer.");
|
|
236
248
|
}
|
|
237
|
-
if (![false, "shared"].includes(legend)) {
|
|
249
|
+
if (![false, "shared"].includes(legend) && !isPlainObject(legend)) {
|
|
238
250
|
throw new Error("Facet legend must be false or shared.");
|
|
239
251
|
}
|
|
240
252
|
const byId = new Map(children.map(child => [child.id, child]));
|
|
@@ -242,23 +254,34 @@ export function planFacetGuideOwnership({
|
|
|
242
254
|
throw new Error("Facet guide child ids must match occupied placements exactly.");
|
|
243
255
|
}
|
|
244
256
|
const outer = resolveFacetOuterAxes({ placements: cells });
|
|
245
|
-
|
|
257
|
+
const sides = resolveFacetOuterSides({ placements: cells });
|
|
258
|
+
if (legend !== false && !isPlainObject(sharedLegends)) {
|
|
246
259
|
throw new Error("Shared facet legend ownership requires a compatibility plan.");
|
|
247
260
|
}
|
|
248
261
|
return cloneAndFreeze({
|
|
249
262
|
children: Object.fromEntries(cells.map(cell => {
|
|
250
263
|
const descriptor = byId.get(cell.id);
|
|
251
264
|
const presentAxes = descriptor.axes ?? [];
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
265
|
+
const records = descriptor.axisOwners;
|
|
266
|
+
const removed = {};
|
|
267
|
+
const keepAxes = axes === "each" ? presentAxes : records === undefined
|
|
268
|
+
? presentAxes.filter(channel => outer.children[cell.id][channel])
|
|
269
|
+
: presentAxes.filter(id => {
|
|
270
|
+
const owner = records.find(owner => owner.id === id);
|
|
271
|
+
if (owner === undefined) throw new Error("Facet axis descriptors must identify every retained owner.");
|
|
272
|
+
const components = Object.entries(owner.components);
|
|
273
|
+
const remove = components.filter(([, side]) => !sides[side]?.includes(cell.id)).map(([component]) => component);
|
|
274
|
+
if (remove.length > 0 && remove.length < components.length) removed[id] = remove;
|
|
275
|
+
return remove.length < components.length;
|
|
276
|
+
});
|
|
255
277
|
return [cell.id, {
|
|
256
278
|
keepAxes,
|
|
257
279
|
removeAxes: presentAxes.filter(channel => !keepAxes.includes(channel)),
|
|
258
|
-
|
|
280
|
+
...(records === undefined ? {} : { removeAxisComponents: removed }),
|
|
281
|
+
removeLegends: legend !== false ? descriptor.legendKinds ?? [] : []
|
|
259
282
|
}];
|
|
260
283
|
})),
|
|
261
|
-
parent: legend
|
|
284
|
+
parent: legend !== false
|
|
262
285
|
? { legends: sharedLegends.entries, promoteFrom: sharedLegends.source }
|
|
263
286
|
: { legends: [] }
|
|
264
287
|
});
|