ggaction 0.0.12 → 0.0.13
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 +49 -0
- package/README.md +2 -2
- package/knowledge/action-card.schema.json +1 -1
- package/knowledge/action-cards.json +1 -1
- package/knowledge/action-cards.schema.json +1 -1
- package/knowledge/extension-authoring.md +2 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/knowledge/task-resolver.js +1 -1
- package/package.json +4 -2
- package/src/actions/basic.js +2 -0
- package/src/actions/boxPlots/materialize.js +2 -1
- package/src/actions/canvas/actions.js +3 -1
- package/src/actions/canvas/fitting.js +211 -0
- package/src/actions/canvas/index.js +2 -0
- package/src/actions/categoryOrder/index.js +3 -2
- package/src/actions/charts/area.js +47 -0
- package/src/actions/charts/bar.js +11 -5
- package/src/actions/charts/beeswarm.js +70 -0
- package/src/actions/charts/density.js +44 -0
- package/src/actions/charts/ecdf.js +183 -0
- package/src/actions/charts/endpoints.js +451 -0
- package/src/actions/charts/heatmap.js +2 -2
- package/src/actions/charts/histogram.js +1 -1
- package/src/actions/charts/horizon.js +71 -0
- package/src/actions/charts/index.js +37 -0
- package/src/actions/charts/interval-regression.js +169 -0
- package/src/actions/charts/line.js +5 -5
- package/src/actions/charts/parallel.js +1 -1
- package/src/actions/charts/pie.js +44 -0
- package/src/actions/charts/polar.js +123 -0
- package/src/actions/charts/radar.js +299 -0
- package/src/actions/charts/radial.js +42 -0
- package/src/actions/charts/raincloud.js +422 -0
- package/src/actions/charts/rug-strip.js +279 -0
- package/src/actions/charts/scatter.js +9 -3
- package/src/actions/charts/shared.js +69 -10
- package/src/actions/composition/actions.js +118 -1
- package/src/actions/data/bin.js +29 -0
- package/src/actions/data/computed.js +24 -0
- package/src/actions/data/derived.js +66 -0
- package/src/actions/data/ecdf.js +38 -0
- package/src/actions/data/filter.js +212 -28
- package/src/actions/data/fold.js +24 -0
- package/src/actions/data/index.js +24 -0
- package/src/actions/data/interval.js +2 -0
- package/src/actions/data/intervalEdit.js +86 -4
- package/src/actions/data/regression.js +5 -2
- package/src/actions/data/stack.js +26 -0
- package/src/actions/data/summary.js +26 -0
- package/src/actions/data/timeUnit.js +1 -1
- package/src/actions/encodings/appearance.js +32 -13
- package/src/actions/encodings/barWidth.js +8 -0
- package/src/actions/encodings/basic.js +2 -0
- package/src/actions/encodings/color/continuous.js +19 -59
- package/src/actions/encodings/color/index.js +28 -22
- package/src/actions/encodings/color/layout.js +3 -97
- package/src/actions/encodings/color/policy.js +5 -8
- package/src/actions/encodings/density/resolve.js +37 -5
- package/src/actions/encodings/density.js +120 -33
- package/src/actions/encodings/horizon/resolve.js +8 -4
- package/src/actions/encodings/horizon.js +18 -8
- package/src/actions/encodings/index.js +2 -0
- package/src/actions/encodings/offset.js +10 -16
- package/src/actions/encodings/parallel.js +2 -8
- package/src/actions/encodings/position/apply.js +10 -4
- package/src/actions/encodings/position/index.js +43 -5
- package/src/actions/encodings/position/policies/arc.js +14 -0
- package/src/actions/encodings/position/policies/area.js +4 -42
- package/src/actions/encodings/position/policies/bar.js +48 -13
- package/src/actions/encodings/position/resolve.js +67 -22
- package/src/actions/encodings/ranged.js +123 -97
- package/src/actions/encodings/remove.js +37 -32
- package/src/actions/encodings/ruleAppearance.js +23 -9
- package/src/actions/encodings/seriesLayout.js +104 -0
- package/src/actions/encodings/shared.js +25 -23
- package/src/actions/encodings/strokeDash.js +9 -6
- package/src/actions/encodings/temporal.js +7 -0
- package/src/actions/encodings/text.js +30 -15
- package/src/actions/errorBands/create.js +18 -5
- package/src/actions/errorBands/edit.js +341 -21
- package/src/actions/errorBars/create.js +21 -10
- package/src/actions/errorBars/edit.js +355 -19
- package/src/actions/facets/actions.js +410 -12
- package/src/actions/facets/derive.js +57 -8
- package/src/actions/gradientPlots/components.js +33 -11
- package/src/actions/gradientPlots/materialize.js +5 -7
- package/src/actions/guides/applicability.js +20 -17
- package/src/actions/guides/axes/axes.js +4 -3
- package/src/actions/guides/axes/axis.js +18 -18
- package/src/actions/guides/axes/components.js +43 -0
- package/src/actions/guides/axes/edit.js +16 -51
- package/src/actions/guides/axes/labels.js +129 -19
- package/src/actions/guides/axes/lines.js +7 -5
- package/src/actions/guides/axes/parallel/lifecycle.js +117 -0
- package/src/actions/guides/axes/parallel/policy.js +137 -0
- package/src/actions/guides/axes/parallel/resolve.js +44 -23
- package/src/actions/guides/axes/parallel.js +93 -217
- package/src/actions/guides/axes/policy.js +21 -67
- package/src/actions/guides/axes/tickGroups.js +14 -8
- package/src/actions/guides/axes/ticks.js +5 -4
- package/src/actions/guides/axes/titles.js +21 -8
- package/src/actions/guides/basic.js +2 -0
- package/src/actions/guides/facade.js +106 -0
- package/src/actions/guides/facadeAxes.js +200 -0
- package/src/actions/guides/facadeLegend.js +63 -0
- package/src/actions/guides/grids/grid.js +37 -33
- package/src/actions/guides/grids/resolve.js +10 -2
- package/src/actions/guides/guides.js +35 -28
- package/src/actions/guides/legends/categorical/actions.js +193 -249
- package/src/actions/guides/legends/categorical/components.js +4 -5
- package/src/actions/guides/legends/categorical/layout.js +36 -349
- package/src/actions/guides/legends/categorical/options.js +30 -14
- package/src/actions/guides/legends/categorical/recipes.js +14 -0
- package/src/actions/guides/legends/categorical/resolve.js +21 -10
- package/src/actions/guides/legends/categorical/symbols.js +2 -3
- package/src/actions/guides/legends/continuous/common.js +69 -14
- package/src/actions/guides/legends/continuous/gradient.js +111 -83
- package/src/actions/guides/legends/continuous/index.js +6 -2
- package/src/actions/guides/legends/continuous/interval.js +71 -92
- package/src/actions/guides/legends/continuous/opacity.js +105 -84
- package/src/actions/guides/legends/creation.js +27 -0
- package/src/actions/guides/legends/edit.js +198 -66
- package/src/actions/guides/legends/focused.js +2 -2
- package/src/actions/guides/legends/lane.js +98 -86
- package/src/actions/guides/legends/lifecycle.js +132 -0
- package/src/actions/guides/legends/remove.js +31 -76
- package/src/actions/guides/legends/size.js +149 -105
- package/src/actions/guides/legends/strokeWidth.js +106 -64
- package/src/actions/guides/legends/target.js +12 -0
- package/src/actions/guides/legends/transition.js +43 -0
- package/src/actions/guides/polar/axes/facade.js +77 -62
- package/src/actions/guides/polar/axes/labels.js +5 -4
- package/src/actions/guides/polar/axes/lines.js +2 -1
- package/src/actions/guides/polar/axes/shared.js +6 -0
- package/src/actions/guides/polar/axes/ticks.js +5 -4
- package/src/actions/guides/polar/axes/titles.js +5 -4
- package/src/actions/guides/polar/grids.js +7 -7
- package/src/actions/guides/reuse.js +44 -0
- package/src/actions/guides/tickValues.js +19 -21
- package/src/actions/index.js +2 -0
- package/src/actions/intervals/resolve.js +35 -15
- package/src/actions/marks/arc/actions.js +1 -1
- package/src/actions/marks/area/actions.js +18 -4
- package/src/actions/marks/area/materialize.js +5 -2
- package/src/actions/marks/bar/materialize.js +11 -7
- package/src/actions/marks/index.js +5 -0
- package/src/actions/marks/line/actions.js +43 -108
- package/src/actions/marks/line/materialize.js +34 -52
- package/src/actions/marks/point/edit.js +11 -15
- package/src/actions/marks/point/index.js +1 -0
- package/src/actions/marks/point/jitter.js +3 -0
- package/src/actions/marks/point/materialize.js +106 -2
- package/src/actions/marks/point/packing.js +68 -0
- package/src/actions/marks/references.js +87 -0
- package/src/actions/marks/remove.js +17 -4
- package/src/actions/marks/rule/actions.js +34 -3
- package/src/actions/marks/rule/appearance.js +26 -0
- package/src/actions/marks/shared.js +12 -12
- package/src/actions/marks/text/actions.js +224 -6
- package/src/actions/marks/text/layout.js +2 -2
- package/src/actions/primitives/createGraphics.js +3 -1
- package/src/actions/primitives/semantic.js +1 -1
- package/src/actions/primitives/semanticAction.js +39 -2
- package/src/actions/primitives/semanticValidation/guide.js +10 -0
- package/src/actions/primitives/semanticValidation/layer.js +18 -1
- package/src/actions/primitives/semanticValidation/scale.js +10 -1
- package/src/actions/regression/create.js +1 -1
- package/src/actions/regression/edit.js +19 -6
- package/src/actions/regression/resolve.js +1 -1
- package/src/actions/scales/consumers/common.js +20 -53
- package/src/actions/scales/consumers/families.js +27 -14
- package/src/actions/scales/consumers/index.js +23 -2
- package/src/actions/scales/consumers/seriesLayout.js +33 -2
- package/src/actions/scales/create.js +4 -4
- package/src/actions/scales/definitions.js +19 -3
- package/src/actions/scales/edit.js +46 -34
- package/src/actions/scales/editPolicy.js +21 -31
- package/src/actions/scales/materialize.js +10 -46
- package/src/actions/scales/patch.js +1 -1
- package/src/actions/scales/preview.js +39 -0
- package/src/actions/scales/quantitativeColor.js +9 -5
- package/src/actions/theme/actions.js +43 -0
- package/src/actions/theme/index.js +9 -0
- package/src/actions/theme/reconcile.js +882 -0
- package/src/actions/titles/actions.js +11 -10
- package/src/actions/titles/resolve.js +1 -40
- package/src/actions/violinPlots/create.js +86 -42
- package/src/actions/violinPlots/edit.js +191 -0
- package/src/actions/violinPlots/index.js +2 -0
- package/src/core/ChartProgram.js +7 -0
- package/src/core/action.js +22 -2
- package/src/core/compositionState.js +136 -8
- package/src/grammar/aggregate.js +66 -23
- package/src/grammar/arcs.js +58 -1
- package/src/grammar/areaEndpoints.js +46 -0
- package/src/grammar/areaSeries.js +87 -41
- package/src/grammar/bars/aggregate.js +33 -12
- package/src/grammar/bars/policy.js +9 -7
- package/src/grammar/bin.js +181 -0
- package/src/grammar/categoryOrder.js +34 -0
- package/src/grammar/computed.js +150 -0
- package/src/grammar/ecdf.js +198 -0
- package/src/grammar/facets/index.js +117 -6
- package/src/grammar/fold.js +123 -0
- package/src/grammar/horizon.js +6 -4
- package/src/grammar/interval.js +37 -20
- package/src/grammar/lineSeries.js +20 -63
- package/src/grammar/markFilter.js +22 -4
- package/src/grammar/markLabels.js +93 -0
- package/src/grammar/parallelAxisTitles.js +16 -0
- package/src/grammar/pathSeries.js +91 -0
- package/src/grammar/pointPacking.js +183 -0
- package/src/grammar/positionDatum.js +12 -0
- package/src/grammar/rects.js +9 -1
- package/src/grammar/regression/derive.js +4 -0
- package/src/grammar/regression/index.js +7 -2
- package/src/grammar/regression/models.js +22 -7
- package/src/grammar/regression/parameters.js +40 -13
- package/src/grammar/rotation.js +29 -0
- package/src/grammar/rules.js +7 -21
- package/src/grammar/scales/color.js +23 -1
- package/src/grammar/scales/colorConsumers.js +38 -0
- package/src/grammar/scales/definition.js +15 -1
- package/src/grammar/scales/discretized.js +11 -5
- package/src/grammar/scales/fields.js +38 -8
- package/src/grammar/scales/mapping.js +2 -0
- package/src/grammar/scales/radial.js +55 -0
- package/src/grammar/scales/types.js +1 -0
- package/src/grammar/schemas/semanticPath.js +17 -3
- package/src/grammar/stack.js +170 -0
- package/src/grammar/statistics/confidenceInterval.js +101 -0
- package/src/grammar/summary.js +183 -0
- package/src/grammar/text.js +22 -16
- package/src/grammar/timeUnit.js +21 -11
- package/src/grammar/transformTopology.js +6 -0
- package/src/grammar/transforms.js +36 -0
- package/src/grammar/valueFormat.js +112 -0
- package/src/grammar/window.js +13 -7
- package/src/layout/facets.js +41 -7
- package/src/layout/guideCollisions.js +12 -0
- package/src/layout/legend.js +2 -10
- package/src/layout/legendItems.js +102 -0
- package/src/layout/legendLane.js +100 -11
- package/src/materialization/bars/aggregate.js +38 -152
- package/src/materialization/bars/histogram.js +24 -0
- package/src/materialization/categorySlotOffset.js +40 -0
- package/src/materialization/compositionSnapshot.js +1 -1
- package/src/materialization/dependencies.js +57 -16
- package/src/materialization/encodings.js +1 -1
- package/src/materialization/facets.js +8 -1
- package/src/materialization/guides/layout.js +57 -0
- package/src/materialization/guides/resources.js +2 -2
- package/src/materialization/legends.js +10 -1
- package/src/materialization/marks/capabilities.js +20 -5
- package/src/materialization/marks/grouping.js +52 -0
- package/src/materialization/marks/index.js +58 -23
- package/src/materialization/marks/policies.js +2 -0
- package/src/materialization/rect.js +33 -33
- package/src/materialization/rowEncoding.js +13 -13
- package/src/materialization/scaleGuideDependencies.js +8 -5
- package/src/materialization/scales/map.js +1 -0
- package/src/materialization/scales/policies/arc.js +42 -0
- package/src/materialization/scales/policies/binnedPosition.js +7 -4
- package/src/materialization/scales/policies/series.js +2 -1
- package/src/materialization/scales/resolve.js +46 -14
- package/src/materialization/selection/compatibility.js +11 -0
- package/src/materialization/selection/items/bar.js +12 -68
- package/src/materialization/selection/items/common.js +8 -2
- package/src/materialization/selection/items/path.js +3 -13
- package/src/materialization/selection/items/rect.js +7 -4
- package/src/materialization/text.js +64 -5
- package/src/mcp/adapter.js +2 -2
- package/src/selectors/datasets.js +13 -0
- package/src/selectors/layers.js +3 -2
- package/src/selectors/scales.js +6 -0
- package/src/theme/defaults.js +39 -0
- package/types/basic.d.ts +11 -2
- package/types/index.d.ts +126 -0
- package/types/program.d.ts +1320 -135
- package/src/materialization/bars/grouped.js +0 -144
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { findCoordinate } from "../../selectors/coordinates.js";
|
|
2
|
+
import { polarGuideNames } from "./polar/resolve.js";
|
|
3
|
+
import { polarGridOperations, validatePolarGridOptions } from "./polar/grids.js";
|
|
4
|
+
import { findLayer } from "../../selectors/layers.js";
|
|
5
|
+
import { resolveGuideOptions } from "./guides.js";
|
|
6
|
+
import { resolveGridOptions } from "./grids/grid.js";
|
|
7
|
+
import { gridNames, validateGridCreateArgs } from "./grids/resolve.js";
|
|
8
|
+
import { scopeFacadeAxes, planFacadeAxes } from "./facadeAxes.js";
|
|
9
|
+
import { assertGuideOptions, guideConflict, resolveStoredGuideCoordinate } from "./reuse.js";
|
|
10
|
+
import { planFacadeLegend } from "./facadeLegend.js";
|
|
11
|
+
import { applyLegendCreationPlan } from "./legends/categorical/actions.js";
|
|
12
|
+
|
|
13
|
+
function facadeGridNames(direction) {
|
|
14
|
+
return ["theta", "radial"].includes(direction)
|
|
15
|
+
? { ...polarGridOperations(direction), channel: polarGuideNames(direction).channel }
|
|
16
|
+
: gridNames(direction);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function scopeGrid(program, layer, args) {
|
|
20
|
+
const directions = resolveGridOptions(program, args, [layer]);
|
|
21
|
+
const scoped = {};
|
|
22
|
+
const polar = findCoordinate(program, layer.coordinate)?.type === "polar";
|
|
23
|
+
for (const [direction, option] of Object.entries(directions)) {
|
|
24
|
+
if (option === undefined) continue;
|
|
25
|
+
if (polar !== ["theta", "radial"].includes(direction)) {
|
|
26
|
+
guideConflict("grid coordinate family does not belong to this facade");
|
|
27
|
+
}
|
|
28
|
+
const { channel, create } = facadeGridNames(direction);
|
|
29
|
+
const scale = layer.encoding?.[channel]?.scale;
|
|
30
|
+
if (scale === undefined || option.scale !== undefined && option.scale !== scale ||
|
|
31
|
+
option.coordinate !== undefined && option.coordinate !== layer.coordinate) {
|
|
32
|
+
guideConflict(`${direction} grid does not belong to this facade`);
|
|
33
|
+
}
|
|
34
|
+
scoped[direction] = { ...option, scale, coordinate: layer.coordinate };
|
|
35
|
+
if (polar) validatePolarGridOptions(scoped[direction], create, true);
|
|
36
|
+
else validateGridCreateArgs(scoped[direction], create);
|
|
37
|
+
}
|
|
38
|
+
// An explicit vertical-only request must not re-enable horizontal grid defaults.
|
|
39
|
+
if (polar) {
|
|
40
|
+
for (const direction of ["theta", "radial"]) if (scoped[direction] === undefined) scoped[direction] = false;
|
|
41
|
+
} else if (scoped.horizontal === undefined) scoped.horizontal = false;
|
|
42
|
+
return scoped;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function planGrid(program, args) {
|
|
46
|
+
return Object.entries(args).flatMap(([direction, option]) => {
|
|
47
|
+
if (option === false) return [];
|
|
48
|
+
const { channel, create } = facadeGridNames(direction);
|
|
49
|
+
const guide = program.semanticSpec.guides.grid?.[direction];
|
|
50
|
+
const config = program.guideConfigs.grid?.[direction];
|
|
51
|
+
if (guide === undefined && config === undefined) return [{ op: create, args: option }];
|
|
52
|
+
if (guide?.scale !== option.scale || config === undefined || config.scale !== option.scale ||
|
|
53
|
+
config.coordinate !== undefined && config.coordinate !== option.coordinate ||
|
|
54
|
+
resolveStoredGuideCoordinate(program, guide, channel) !== option.coordinate) {
|
|
55
|
+
guideConflict(`${direction} grid uses a different coordinate or scale`);
|
|
56
|
+
}
|
|
57
|
+
const { scale, coordinate, ...appearance } = option;
|
|
58
|
+
assertGuideOptions(appearance, config, `${direction} grid`);
|
|
59
|
+
return [];
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function fulfillFacadeGuides(program, guides, target, explicitGuides = guides) {
|
|
64
|
+
if (guides === false) return program;
|
|
65
|
+
const layer = findLayer(program, target);
|
|
66
|
+
if (layer === undefined) throw new Error(`Unknown facade guide target "${target}".`);
|
|
67
|
+
const resolved = resolveGuideOptions(program, guides, [layer]);
|
|
68
|
+
const axisDirections = Object.entries(resolved.axes ?? {}).filter(([key]) => key !== "coordinate");
|
|
69
|
+
const scopedAxes = resolved.axes === undefined || axisDirections.length > 0 && axisDirections.every(
|
|
70
|
+
([, value]) => value === false
|
|
71
|
+
)
|
|
72
|
+
? undefined
|
|
73
|
+
: scopeFacadeAxes(program, layer, resolved.axes);
|
|
74
|
+
const axes = scopedAxes;
|
|
75
|
+
const scopedGrid = resolved.grid === undefined || Object.values(resolved.grid).every(
|
|
76
|
+
value => value === false
|
|
77
|
+
)
|
|
78
|
+
? undefined
|
|
79
|
+
: scopeGrid(program, layer, resolved.grid);
|
|
80
|
+
const grid = scopedGrid;
|
|
81
|
+
const legend = resolved.legend === undefined ? undefined : { ...resolved.legend, target };
|
|
82
|
+
if (resolved.legend?.target !== undefined && resolved.legend.target !== target) {
|
|
83
|
+
guideConflict("legend target does not belong to this facade");
|
|
84
|
+
}
|
|
85
|
+
if (axes === undefined && grid === undefined && legend === undefined) return program;
|
|
86
|
+
const legendPlan = legend === undefined ? undefined
|
|
87
|
+
: planFacadeLegend(program, layer, legend, explicitGuides.legend ?? {});
|
|
88
|
+
const steps = [
|
|
89
|
+
...(axes === undefined ? [] : planFacadeAxes(program, layer, axes, explicitGuides.axes)),
|
|
90
|
+
...(grid === undefined ? [] : planGrid(program, grid))
|
|
91
|
+
];
|
|
92
|
+
const hasExisting = [program.guideConfigs.axis, program.guideConfigs.grid, program.guideConfigs.legend,
|
|
93
|
+
program.semanticSpec.guides.axis, program.semanticSpec.guides.grid, program.semanticSpec.guides.legend]
|
|
94
|
+
.some(config => Object.keys(config ?? {}).length > 0);
|
|
95
|
+
const unambiguousAxes = axes?.coordinate.type !== "parallel" ||
|
|
96
|
+
program.semanticSpec.layers.filter(candidate =>
|
|
97
|
+
candidate.coordinate === layer.coordinate && candidate.encoding?.parallel !== undefined
|
|
98
|
+
).length === 1;
|
|
99
|
+
if (!hasExisting && unambiguousAxes) {
|
|
100
|
+
return program.createGuides({ axes: axes ?? false, grid: grid ?? false, legend: legendPlan?.args ?? false });
|
|
101
|
+
}
|
|
102
|
+
let next = program;
|
|
103
|
+
for (const step of steps) next = next[step.op](step.args);
|
|
104
|
+
if (legendPlan !== undefined) next = applyLegendCreationPlan(next, legendPlan);
|
|
105
|
+
return next;
|
|
106
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { validatePolarAxisArgs } from "./polar/axes/facade.js";
|
|
2
|
+
import { prefix as polarPrefix, resolveAngle } from "./polar/axes/shared.js";
|
|
3
|
+
import { validateAxesArgs } from "./axes/axes.js";
|
|
4
|
+
import { validateAxisArgs } from "./axes/axis.js";
|
|
5
|
+
import { axisComponentDisabled, hasAxisComponent } from "./axes/components.js";
|
|
6
|
+
import { validateAxisTickGroupArgs } from "./axes/tickGroups.js";
|
|
7
|
+
import { findCoordinate } from "../../selectors/coordinates.js";
|
|
8
|
+
import {
|
|
9
|
+
assertGuideOptions, guideConflict, resolveStoredGuideCoordinate, sameGuideValue
|
|
10
|
+
} from "./reuse.js";
|
|
11
|
+
|
|
12
|
+
export function scopeFacadeAxes(program, layer, args) {
|
|
13
|
+
const descriptor = validateAxesArgs(args);
|
|
14
|
+
const coordinate = findCoordinate(program, layer.coordinate);
|
|
15
|
+
if (coordinate === undefined) guideConflict(`layer "${layer.id}" has no coordinate`);
|
|
16
|
+
if (descriptor.id !== undefined && descriptor.id !== coordinate.id) {
|
|
17
|
+
guideConflict("axes.coordinate.id does not belong to this facade");
|
|
18
|
+
}
|
|
19
|
+
if (descriptor.type !== undefined && descriptor.type !== "auto" && descriptor.type !== coordinate.type) {
|
|
20
|
+
guideConflict("axes.coordinate.type does not match this facade");
|
|
21
|
+
}
|
|
22
|
+
if (coordinate.type === "polar") {
|
|
23
|
+
if (args.x !== undefined || args.y !== undefined) guideConflict("Polar facades do not own Cartesian axes");
|
|
24
|
+
const scoped = { coordinate: { id: coordinate.id, type: "polar" } };
|
|
25
|
+
for (const channel of ["theta", "radius"]) {
|
|
26
|
+
const option = args[channel];
|
|
27
|
+
const scale = layer.encoding?.[channel]?.scale;
|
|
28
|
+
if (option === false || option === undefined && scale === undefined) { scoped[channel] = false; continue; }
|
|
29
|
+
if (scale === undefined || option?.scale !== undefined && option.scale !== scale ||
|
|
30
|
+
option?.coordinate !== undefined && option.coordinate !== coordinate.id) {
|
|
31
|
+
guideConflict(`axes.${channel} does not belong to this facade`);
|
|
32
|
+
}
|
|
33
|
+
scoped[channel] = { ...option, scale, coordinate: coordinate.id };
|
|
34
|
+
validatePolarAxisArgs(channel, scoped[channel], `create${polarPrefix(channel)}Axis`);
|
|
35
|
+
resolveAngle(program, channel, scoped[channel]);
|
|
36
|
+
}
|
|
37
|
+
return scoped;
|
|
38
|
+
}
|
|
39
|
+
if (args.theta !== undefined || args.radius !== undefined) {
|
|
40
|
+
guideConflict("Cartesian and Parallel facades do not own Polar axes");
|
|
41
|
+
}
|
|
42
|
+
if (coordinate.type === "parallel") {
|
|
43
|
+
if (args.x !== undefined || args.y !== undefined) {
|
|
44
|
+
throw new Error("createAxes channel options are not supported for Parallel axes.");
|
|
45
|
+
}
|
|
46
|
+
return { coordinate: { id: coordinate.id, type: "parallel" } };
|
|
47
|
+
}
|
|
48
|
+
const scoped = { coordinate: { id: coordinate.id, type: "cartesian" } };
|
|
49
|
+
for (const channel of ["x", "y"]) {
|
|
50
|
+
const option = args[channel];
|
|
51
|
+
if (option === false || (option === undefined && layer.encoding?.[channel] === undefined)) {
|
|
52
|
+
scoped[channel] = false;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const scale = layer.encoding?.[channel]?.scale;
|
|
56
|
+
if (scale === undefined) guideConflict(`this facade has no ${channel} scale`);
|
|
57
|
+
if (option?.scale !== undefined && option.scale !== scale) {
|
|
58
|
+
guideConflict(`axes.${channel}.scale does not belong to this facade`);
|
|
59
|
+
}
|
|
60
|
+
if (option?.coordinate !== undefined && option.coordinate !== coordinate.id) {
|
|
61
|
+
guideConflict(`axes.${channel}.coordinate does not belong to this facade`);
|
|
62
|
+
}
|
|
63
|
+
scoped[channel] = { ...option, scale, coordinate: coordinate.id };
|
|
64
|
+
validateAxisArgs(scoped[channel], `create${channel.toUpperCase()}Axis`);
|
|
65
|
+
if (scoped[channel].ticksAndLabels !== false) {
|
|
66
|
+
validateAxisTickGroupArgs(scoped[channel].ticksAndLabels ?? {}, "createAxisTicksAndLabels", true);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return scoped;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function tickMode(config) {
|
|
73
|
+
return config?.mode === "values" ? { values: config.values }
|
|
74
|
+
: config?.mode === "count" ? { count: config.count } : {};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function planAxis(program, channel, args, explicit) {
|
|
78
|
+
const prefix = channel.toUpperCase();
|
|
79
|
+
const guide = program.semanticSpec.guides.axis?.[channel];
|
|
80
|
+
const configs = program.guideConfigs.axis?.[channel] ?? {};
|
|
81
|
+
if (guide === undefined && Object.keys(configs).length === 0) {
|
|
82
|
+
return [{ op: `create${prefix}Axis`, args }];
|
|
83
|
+
}
|
|
84
|
+
if (guide?.scale !== args.scale || resolveStoredGuideCoordinate(program, guide, channel) !== args.coordinate) {
|
|
85
|
+
guideConflict(`${channel} axis uses a different coordinate or scale`);
|
|
86
|
+
}
|
|
87
|
+
const positions = [...new Set(Object.values(configs).map(config => config.position).filter(Boolean))];
|
|
88
|
+
if (positions.length > 1) guideConflict(`${channel} axis components have incompatible positions`);
|
|
89
|
+
const position = explicit.position ?? positions[0];
|
|
90
|
+
const shared = { scale: args.scale, ...(position === undefined ? {} : { position }) };
|
|
91
|
+
const explicitShared = explicit.position === undefined ? {} : { position: explicit.position };
|
|
92
|
+
const steps = guide.coordinate === undefined ? [{
|
|
93
|
+
op: "editSemantic", args: { property: `guide.axis.${channel}.coordinate`, value: args.coordinate }
|
|
94
|
+
}] : [];
|
|
95
|
+
const group = args.ticksAndLabels ?? {};
|
|
96
|
+
const explicitGroup = explicit.ticksAndLabels ?? {};
|
|
97
|
+
const explicitMode = Object.fromEntries(["count", "values"].filter(key =>
|
|
98
|
+
Object.hasOwn(explicitGroup, key)
|
|
99
|
+
).map(key => [key, explicitGroup[key]]));
|
|
100
|
+
const requested = {
|
|
101
|
+
line: { ...explicitShared, ...(explicit.line ?? {}) },
|
|
102
|
+
ticks: { ...explicitShared, ...explicitMode, ...(explicitGroup.ticks ?? {}) },
|
|
103
|
+
labels: { ...explicitShared, ...explicitMode, ...(explicitGroup.labels ?? {}) },
|
|
104
|
+
title: { ...explicitShared, ...(explicit.title ?? {}) }
|
|
105
|
+
};
|
|
106
|
+
for (const component of ["line", "ticks", "labels", "title"]) {
|
|
107
|
+
const config = configs[component];
|
|
108
|
+
if (axisComponentDisabled(args, component)) {
|
|
109
|
+
if (hasAxisComponent(program, channel, component)) guideConflict(`${channel} axis already has ${component}`);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (config === undefined) continue;
|
|
113
|
+
if (config.scale !== undefined && config.scale !== args.scale) {
|
|
114
|
+
guideConflict(`${channel} ${component} uses a different scale`);
|
|
115
|
+
}
|
|
116
|
+
assertGuideOptions(requested[component], component === "title"
|
|
117
|
+
? { ...config, text: guide.title } : config, `${channel} axis ${component}`);
|
|
118
|
+
}
|
|
119
|
+
if (args.line !== false && configs.line === undefined) {
|
|
120
|
+
steps.push({ op: `create${prefix}AxisLine`, args: { ...shared, ...args.line } });
|
|
121
|
+
}
|
|
122
|
+
if (args.ticksAndLabels !== false) {
|
|
123
|
+
if (configs.ticks === undefined && configs.labels === undefined) {
|
|
124
|
+
steps.push({ op: `create${prefix}AxisTicksAndLabels`, args: { ...shared, ...group } });
|
|
125
|
+
} else {
|
|
126
|
+
if (configs.ticks === undefined) {
|
|
127
|
+
steps.push({ op: `create${prefix}AxisTicks`, args: { ...shared, ...tickMode(configs.labels), ...explicitMode, ...group.ticks } });
|
|
128
|
+
}
|
|
129
|
+
if (configs.labels === undefined) {
|
|
130
|
+
steps.push({ op: `create${prefix}AxisLabels`, args: { ...shared, ...group.labels } });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (args.title !== false && configs.title === undefined) {
|
|
135
|
+
steps.push({ op: `create${prefix}AxisTitle`, args: { ...shared, ...args.title } });
|
|
136
|
+
}
|
|
137
|
+
return steps;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function planFacadeAxes(program, layer, scoped, explicit = {}) {
|
|
141
|
+
if (scoped.coordinate.type === "parallel") {
|
|
142
|
+
const existing = program.semanticSpec.guides.axis?.parallel;
|
|
143
|
+
if (existing === undefined) return [{
|
|
144
|
+
op: "createParallelAxes", args: { target: layer.id, coordinate: layer.coordinate }
|
|
145
|
+
}];
|
|
146
|
+
const scales = layer.encoding.parallel.dimensions.map(dimension => dimension.scale);
|
|
147
|
+
if (existing.coordinate !== layer.coordinate || !sameGuideValue(existing.scales, scales)) {
|
|
148
|
+
guideConflict("Parallel axes use a different coordinate or dimension scales");
|
|
149
|
+
}
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
if (scoped.coordinate.type === "polar") {
|
|
153
|
+
return ["theta", "radius"].flatMap(channel => scoped[channel] === false ? []
|
|
154
|
+
: planPolarAxis(program, channel, scoped[channel], explicit[channel] ?? {}));
|
|
155
|
+
}
|
|
156
|
+
return ["x", "y"].flatMap(channel => scoped[channel] === false ? []
|
|
157
|
+
: planAxis(program, channel, scoped[channel], explicit[channel] ?? {}));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function planPolarAxis(program, channel, args, explicit) {
|
|
161
|
+
const prefix = polarPrefix(channel);
|
|
162
|
+
const guide = program.semanticSpec.guides.axis?.[channel];
|
|
163
|
+
const configs = program.guideConfigs.axis?.[channel] ?? {};
|
|
164
|
+
if (guide === undefined && Object.keys(configs).length === 0) return [{ op: `create${prefix}Axis`, args }];
|
|
165
|
+
if (guide?.scale !== args.scale || resolveStoredGuideCoordinate(program, guide, channel) !== args.coordinate) {
|
|
166
|
+
guideConflict(`${channel} axis uses a different coordinate or scale`);
|
|
167
|
+
}
|
|
168
|
+
const shared = { scale: args.scale, coordinate: args.coordinate,
|
|
169
|
+
...(channel === "radius" ? { angle: resolveAngle(program, channel, args) } : {}) };
|
|
170
|
+
const group = args.ticksAndLabels ?? {};
|
|
171
|
+
const explicitGroup = explicit.ticksAndLabels ?? {};
|
|
172
|
+
const mode = Object.fromEntries(["count", "values"].filter(key => Object.hasOwn(explicitGroup, key))
|
|
173
|
+
.map(key => [key, explicitGroup[key]]));
|
|
174
|
+
const requested = {
|
|
175
|
+
line: explicit.line ?? {},
|
|
176
|
+
ticks: { ...mode, ...explicitGroup.ticks },
|
|
177
|
+
labels: { ...mode, ...explicitGroup.labels },
|
|
178
|
+
title: explicit.title || {}
|
|
179
|
+
};
|
|
180
|
+
const steps = [];
|
|
181
|
+
for (const component of ["line", "ticks", "labels", "title"]) {
|
|
182
|
+
const config = configs[component];
|
|
183
|
+
if (axisComponentDisabled(args, component)) {
|
|
184
|
+
if (hasAxisComponent(program, channel, component)) guideConflict(`${channel} axis already has ${component}`);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (config !== undefined) {
|
|
188
|
+
if (config.scale !== args.scale || config.coordinate !== args.coordinate) guideConflict(`${channel} ${component} uses different resources`);
|
|
189
|
+
assertGuideOptions(requested[component], component === "title" ? { ...config, text: guide.title } : config,
|
|
190
|
+
`${channel} axis ${component}`);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const options = component === "line" ? args.line ?? {} : component === "title" ? args.title ?? {}
|
|
194
|
+
: { ...tickMode(configs[component === "ticks" ? "labels" : "ticks"]),
|
|
195
|
+
...Object.fromEntries(["count", "values"].filter(key => Object.hasOwn(group, key)).map(key => [key, group[key]])),
|
|
196
|
+
...group[component] };
|
|
197
|
+
steps.push({ op: `create${prefix}Axis${component[0].toUpperCase() + component.slice(1)}`, args: { ...shared, ...options } });
|
|
198
|
+
}
|
|
199
|
+
return steps;
|
|
200
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { resolveLegendCreationPlan } from "./legends/categorical/actions.js";
|
|
2
|
+
import { resolveLegendStepConfig } from "./legends/creation.js";
|
|
3
|
+
import { legendResourcePolicy } from "../../materialization/guides/resources.js";
|
|
4
|
+
import { assertGuideOptions, guideConflict, sameGuideValue } from "./reuse.js";
|
|
5
|
+
|
|
6
|
+
function compatibleLegend(program, kind, requested, existing, explicit) {
|
|
7
|
+
const expectedScales = requested.scales ?? [requested.scale];
|
|
8
|
+
const actualScales = existing.scales ?? [existing.scale];
|
|
9
|
+
if (!sameGuideValue(expectedScales, actualScales) ||
|
|
10
|
+
!sameGuideValue(requested.channels, existing.channels)) {
|
|
11
|
+
guideConflict(`${kind} legend uses different channels or scales`);
|
|
12
|
+
}
|
|
13
|
+
const domain = config => config.domain ?? program.resolvedScales[config.scale]?.domain;
|
|
14
|
+
if (!sameGuideValue(domain(requested), domain(existing))) {
|
|
15
|
+
guideConflict(`${kind} legend uses a different domain or order`);
|
|
16
|
+
}
|
|
17
|
+
if (requested.symbol?.layers !== undefined && !sameGuideValue(
|
|
18
|
+
requested.symbol.layers.map(layer => layer.type), existing.symbol?.layers?.map(layer => layer.type)
|
|
19
|
+
)) guideConflict(`${kind} legend uses a different symbol recipe`);
|
|
20
|
+
const appearance = {};
|
|
21
|
+
for (const [key, value] of Object.entries(explicit)) {
|
|
22
|
+
if (["target", "channels"].includes(key) || requested[key] === undefined) continue;
|
|
23
|
+
if (key === "symbol" && requested.symbol?.layers !== undefined) {
|
|
24
|
+
appearance.symbol = value === "auto" ? requested.symbol
|
|
25
|
+
: value.layers !== undefined ? value
|
|
26
|
+
: { layers: [{ type: requested.symbol.layers[0].type, ...value }] };
|
|
27
|
+
} else if (key === "border" && value === true) {
|
|
28
|
+
if (existing.border === false) guideConflict(`${kind} legend border is disabled`);
|
|
29
|
+
appearance.border = {};
|
|
30
|
+
} else appearance[key] = value;
|
|
31
|
+
}
|
|
32
|
+
assertGuideOptions(appearance, existing, `${kind} legend`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function planFacadeLegend(program, layer, args, explicit = args) {
|
|
36
|
+
const plan = resolveLegendCreationPlan(program, args, [layer]);
|
|
37
|
+
const steps = [];
|
|
38
|
+
for (const step of plan.steps) {
|
|
39
|
+
const { kind, config } = resolveLegendStepConfig(program, step);
|
|
40
|
+
const stored = program.guideConfigs.legend?.[kind];
|
|
41
|
+
const policy = legendResourcePolicy(kind);
|
|
42
|
+
const occupied = program.semanticSpec.guides.legend?.[policy.semanticKind];
|
|
43
|
+
if (stored === undefined) {
|
|
44
|
+
if (occupied !== undefined ||
|
|
45
|
+
policy.family === "categorical" && (
|
|
46
|
+
program.semanticSpec.guides.legend?.series !== undefined ||
|
|
47
|
+
program.semanticSpec.guides.legend?.color !== undefined
|
|
48
|
+
)) {
|
|
49
|
+
guideConflict(`${kind} legend slot belongs to an incompatible legend family`);
|
|
50
|
+
}
|
|
51
|
+
steps.push(step);
|
|
52
|
+
} else {
|
|
53
|
+
if (occupied === undefined) guideConflict(`${kind} legend has no semantic owner`);
|
|
54
|
+
compatibleLegend(program, kind, config, stored, explicit);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const first = plan.steps[0];
|
|
58
|
+
const scopedArgs = first?.op === "createCategoricalLegend" && first.args.symbol !== undefined
|
|
59
|
+
? { ...args, symbol: first.args.symbol } : args;
|
|
60
|
+
// Reused blocks retain their original target. The direct createLegend combined
|
|
61
|
+
// owner check is inapplicable to independently verified shared blocks.
|
|
62
|
+
return { ...plan, combined: false, steps, args: scopedArgs };
|
|
63
|
+
}
|
|
@@ -217,45 +217,49 @@ const editGrid = action(
|
|
|
217
217
|
}
|
|
218
218
|
);
|
|
219
219
|
|
|
220
|
+
export function resolveGridOptions(program, args = {}, layers = program.semanticSpec.layers) {
|
|
221
|
+
validateOptionObject(args, AGGREGATE_OPTIONS, "createGrid");
|
|
222
|
+
const automatic = Object.keys(args).length === 0
|
|
223
|
+
? resolveAutomaticGridOptions(program, layers)
|
|
224
|
+
: undefined;
|
|
225
|
+
const hasExplicitPolar = Object.hasOwn(args, "theta") ||
|
|
226
|
+
Object.hasOwn(args, "radial");
|
|
227
|
+
const polarDefault = automatic !== undefined &&
|
|
228
|
+
(Object.hasOwn(automatic, "theta") || Object.hasOwn(automatic, "radial"));
|
|
229
|
+
const horizontal = automatic !== undefined
|
|
230
|
+
? automatic.horizontal === false ? undefined : automatic.horizontal
|
|
231
|
+
: polarDefault ||
|
|
232
|
+
(hasExplicitPolar && args.horizontal === undefined)
|
|
233
|
+
? undefined
|
|
234
|
+
: normalizeDirection(args.horizontal, "horizontal");
|
|
235
|
+
const vertical = automatic !== undefined
|
|
236
|
+
? automatic.vertical === false ? undefined : automatic.vertical
|
|
237
|
+
: args.vertical === undefined
|
|
238
|
+
? undefined
|
|
239
|
+
: normalizeDirection(args.vertical, "vertical");
|
|
240
|
+
const theta = automatic !== undefined
|
|
241
|
+
? automatic.theta
|
|
242
|
+
: args.theta === undefined
|
|
243
|
+
? undefined
|
|
244
|
+
: normalizeDirection(args.theta, "theta");
|
|
245
|
+
const radial = automatic !== undefined
|
|
246
|
+
? automatic.radial
|
|
247
|
+
: args.radial === undefined
|
|
248
|
+
? undefined
|
|
249
|
+
: normalizeDirection(args.radial, "radial");
|
|
250
|
+
if ([horizontal, vertical, theta, radial].every(value => value === undefined)) {
|
|
251
|
+
throw new Error("createGrid requires at least one selected direction.");
|
|
252
|
+
}
|
|
253
|
+
return { horizontal, vertical, theta, radial };
|
|
254
|
+
}
|
|
255
|
+
|
|
220
256
|
const createGrid = action(
|
|
221
257
|
{
|
|
222
258
|
op: "createGrid",
|
|
223
259
|
description: "Create selected Cartesian grid directions."
|
|
224
260
|
},
|
|
225
261
|
function (args = {}) {
|
|
226
|
-
|
|
227
|
-
const automatic = Object.keys(args).length === 0
|
|
228
|
-
? resolveAutomaticGridOptions(this)
|
|
229
|
-
: undefined;
|
|
230
|
-
const hasExplicitPolar = Object.hasOwn(args, "theta") ||
|
|
231
|
-
Object.hasOwn(args, "radial");
|
|
232
|
-
const polarDefault = automatic !== undefined &&
|
|
233
|
-
(Object.hasOwn(automatic, "theta") || Object.hasOwn(automatic, "radial"));
|
|
234
|
-
const horizontal = automatic !== undefined
|
|
235
|
-
? automatic.horizontal === false ? undefined : automatic.horizontal
|
|
236
|
-
: polarDefault ||
|
|
237
|
-
(hasExplicitPolar && args.horizontal === undefined)
|
|
238
|
-
? undefined
|
|
239
|
-
: normalizeDirection(args.horizontal, "horizontal");
|
|
240
|
-
const vertical = automatic !== undefined
|
|
241
|
-
? automatic.vertical === false ? undefined : automatic.vertical
|
|
242
|
-
: args.vertical === undefined
|
|
243
|
-
? undefined
|
|
244
|
-
: normalizeDirection(args.vertical, "vertical");
|
|
245
|
-
const theta = automatic !== undefined
|
|
246
|
-
? automatic.theta
|
|
247
|
-
: args.theta === undefined
|
|
248
|
-
? undefined
|
|
249
|
-
: normalizeDirection(args.theta, "theta");
|
|
250
|
-
const radial = automatic !== undefined
|
|
251
|
-
? automatic.radial
|
|
252
|
-
: args.radial === undefined
|
|
253
|
-
? undefined
|
|
254
|
-
: normalizeDirection(args.radial, "radial");
|
|
255
|
-
if ([horizontal, vertical, theta, radial].every(value => value === undefined)) {
|
|
256
|
-
throw new Error("createGrid requires at least one selected direction.");
|
|
257
|
-
}
|
|
258
|
-
|
|
262
|
+
const { horizontal, vertical, theta, radial } = resolveGridOptions(this, args);
|
|
259
263
|
let next = this;
|
|
260
264
|
if (horizontal !== undefined) {
|
|
261
265
|
next = next.createHorizontalGrid(horizontal);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isSourceOwnedText } from "../../../grammar/text.js";
|
|
1
2
|
import { validateUserId } from "../../../core/identifiers.js";
|
|
2
3
|
import {
|
|
3
4
|
validateGeneratedItemLimit,
|
|
@@ -126,7 +127,7 @@ export function validateGridEditArgs(args, operation) {
|
|
|
126
127
|
export function resolveGridResources(program, direction, args) {
|
|
127
128
|
const { channel } = gridNames(direction);
|
|
128
129
|
let layers = program.semanticSpec.layers.filter(
|
|
129
|
-
layer => layer.encoding?.[channel]?.scale !== undefined
|
|
130
|
+
layer => !isSourceOwnedText(layer) && layer.encoding?.[channel]?.scale !== undefined
|
|
130
131
|
);
|
|
131
132
|
if (layers.length === 0) throw new Error(`${direction} grid requires a ${channel} encoding.`);
|
|
132
133
|
const requestedCoordinate = args.coordinate === undefined
|
|
@@ -167,7 +168,14 @@ export function resolveGridResources(program, direction, args) {
|
|
|
167
168
|
) {
|
|
168
169
|
throw new Error(`${direction} grid requires resolved continuous scale "${scale}".`);
|
|
169
170
|
}
|
|
170
|
-
const
|
|
171
|
+
const sourceIds = new Set(layers.map(layer => layer.id));
|
|
172
|
+
const relatedLayers = [...layers, ...program.semanticSpec.layers.filter(layer =>
|
|
173
|
+
isSourceOwnedText(layer) && sourceIds.has(layer.source)
|
|
174
|
+
)];
|
|
175
|
+
const related = new Set(relatedLayers.flatMap(layer => {
|
|
176
|
+
const leader = program.materializationConfigs.labelLayouts?.[layer.id]?.leaderId;
|
|
177
|
+
return leader === undefined ? [layer.id] : [leader, layer.id];
|
|
178
|
+
}));
|
|
171
179
|
let before;
|
|
172
180
|
walkGraphicDrawOrder(program.graphicSpec, ({ id }) => {
|
|
173
181
|
if (before === undefined && related.has(id)) before = id;
|
|
@@ -25,8 +25,9 @@ function validateOptions(args) {
|
|
|
25
25
|
validateGuideOption(args.legend, "createGuides legend");
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
function inferAxesOptions(program, applicability) {
|
|
29
|
-
const horizontalInterval =
|
|
28
|
+
function inferAxesOptions(program, applicability, layers) {
|
|
29
|
+
const horizontalInterval = layers.some(layer =>
|
|
30
|
+
program.markConfigs[layer.id]?.boxPlot?.orientation === "horizontal" ||
|
|
30
31
|
layer.mark?.type === "rule" &&
|
|
31
32
|
layer.encoding?.x?.fieldType === "quantitative" &&
|
|
32
33
|
layer.encoding?.x2?.fieldType === "quantitative" &&
|
|
@@ -36,11 +37,11 @@ function inferAxesOptions(program, applicability) {
|
|
|
36
37
|
? { x: { ticksAndLabels: { count: 7 } } }
|
|
37
38
|
: {};
|
|
38
39
|
const directions = applicability.axes.directions;
|
|
39
|
-
const hasStoredY =
|
|
40
|
+
const hasStoredY = layers.some(
|
|
40
41
|
layer => layer.encoding?.y !== undefined
|
|
41
42
|
);
|
|
42
43
|
if (!directions.y && hasStoredY) inferred.y = false;
|
|
43
|
-
const horizonLayers =
|
|
44
|
+
const horizonLayers = layers.filter(layer =>
|
|
44
45
|
findUpstreamTransform(
|
|
45
46
|
program,
|
|
46
47
|
findDataset(program, layer.data),
|
|
@@ -83,39 +84,45 @@ function mergeInferredOptions(inferred, explicit) {
|
|
|
83
84
|
? mergeInferredOptions(inferred[key], value)
|
|
84
85
|
: value;
|
|
85
86
|
}
|
|
87
|
+
if (Object.hasOwn(explicit, "count")) delete merged.values;
|
|
88
|
+
if (Object.hasOwn(explicit, "values")) delete merged.count;
|
|
86
89
|
return merged;
|
|
87
90
|
}
|
|
88
91
|
|
|
92
|
+
export function resolveGuideOptions(program, args = {}, layers = program.semanticSpec.layers) {
|
|
93
|
+
validateOptions(args);
|
|
94
|
+
const applicability = resolveGuideApplicability(program, layers);
|
|
95
|
+
const hasAxes = applicability.axes.cartesian || applicability.axes.polar ||
|
|
96
|
+
applicability.axes.parallel;
|
|
97
|
+
const inferredAxes = applicability.axes.cartesian
|
|
98
|
+
? inferAxesOptions(program, applicability, layers)
|
|
99
|
+
: {};
|
|
100
|
+
const axes = args.axes === undefined && applicability.axes.cartesian
|
|
101
|
+
? inferredAxes
|
|
102
|
+
: args.axes !== undefined && args.axes !== false && hasAxes
|
|
103
|
+
? mergeInferredOptions(inferredAxes, args.axes)
|
|
104
|
+
: selectOption(args.axes, hasAxes);
|
|
105
|
+
const grid = args.grid === undefined &&
|
|
106
|
+
(applicability.grid.cartesian || applicability.grid.polar)
|
|
107
|
+
? resolveAutomaticGridOptions(program, layers)
|
|
108
|
+
: selectOption(
|
|
109
|
+
args.grid,
|
|
110
|
+
applicability.grid.cartesian || applicability.grid.polar
|
|
111
|
+
);
|
|
112
|
+
const legend = selectOption(
|
|
113
|
+
args.legend,
|
|
114
|
+
applicability.legend
|
|
115
|
+
);
|
|
116
|
+
return { axes, grid, legend };
|
|
117
|
+
}
|
|
118
|
+
|
|
89
119
|
const createGuides = action(
|
|
90
120
|
{
|
|
91
121
|
op: "createGuides",
|
|
92
122
|
description: "Create applicable axes, grid, and legend."
|
|
93
123
|
},
|
|
94
124
|
function (args = {}) {
|
|
95
|
-
|
|
96
|
-
const applicability = resolveGuideApplicability(this);
|
|
97
|
-
const hasAxes = applicability.axes.cartesian || applicability.axes.polar ||
|
|
98
|
-
applicability.axes.parallel;
|
|
99
|
-
const inferredAxes = applicability.axes.cartesian
|
|
100
|
-
? inferAxesOptions(this, applicability)
|
|
101
|
-
: {};
|
|
102
|
-
const axes = args.axes === undefined && applicability.axes.cartesian
|
|
103
|
-
? inferredAxes
|
|
104
|
-
: args.axes !== undefined && args.axes !== false && hasAxes
|
|
105
|
-
? mergeInferredOptions(inferredAxes, args.axes)
|
|
106
|
-
: selectOption(args.axes, hasAxes);
|
|
107
|
-
const grid = args.grid === undefined &&
|
|
108
|
-
(applicability.grid.cartesian || applicability.grid.polar)
|
|
109
|
-
? resolveAutomaticGridOptions(this)
|
|
110
|
-
: selectOption(
|
|
111
|
-
args.grid,
|
|
112
|
-
applicability.grid.cartesian || applicability.grid.polar
|
|
113
|
-
);
|
|
114
|
-
const legend = selectOption(
|
|
115
|
-
args.legend,
|
|
116
|
-
applicability.legend
|
|
117
|
-
);
|
|
118
|
-
|
|
125
|
+
const { axes, grid, legend } = resolveGuideOptions(this, args);
|
|
119
126
|
if (axes === undefined && grid === undefined && legend === undefined) {
|
|
120
127
|
throw new Error("createGuides requires at least one selected guide.");
|
|
121
128
|
}
|