ggaction 0.0.12 → 0.0.15
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 +127 -0
- package/README.md +20 -11
- 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 +10 -3
- package/src/actions/basic.js +4 -2
- package/src/actions/boxPlots/components.js +14 -4
- package/src/actions/boxPlots/edit.js +31 -5
- package/src/actions/boxPlots/materialize.js +13 -4
- package/src/actions/boxPlots/options.js +48 -16
- 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 +18 -6
- package/src/actions/charts/area.js +51 -0
- package/src/actions/charts/bar.js +13 -6
- package/src/actions/charts/beeswarm.js +70 -0
- package/src/actions/charts/density.js +48 -0
- package/src/actions/charts/ecdf.js +186 -0
- package/src/actions/charts/endpoints.js +473 -0
- package/src/actions/charts/heatmap.js +4 -3
- package/src/actions/charts/histogram.js +5 -4
- package/src/actions/charts/horizon.js +73 -0
- package/src/actions/charts/index.js +37 -0
- package/src/actions/charts/interval-regression.js +172 -0
- package/src/actions/charts/line.js +8 -6
- package/src/actions/charts/parallel.js +8 -2
- package/src/actions/charts/pie.js +52 -0
- package/src/actions/charts/polar.js +134 -0
- package/src/actions/charts/radar.js +305 -0
- package/src/actions/charts/radial.js +50 -0
- package/src/actions/charts/raincloud.js +422 -0
- package/src/actions/charts/rug-strip.js +283 -0
- package/src/actions/charts/scatter.js +11 -3
- package/src/actions/charts/shared.js +69 -10
- package/src/actions/composition/actions.js +129 -3
- package/src/actions/coordinates/actions.js +1 -1
- package/src/actions/coordinates/edit.js +76 -0
- package/src/actions/data/bin.js +29 -0
- package/src/actions/data/bin2d.js +57 -185
- package/src/actions/data/complete.js +26 -0
- package/src/actions/data/computed.js +24 -0
- package/src/actions/data/create.js +2 -2
- package/src/actions/data/density.js +12 -20
- package/src/actions/data/derived.js +111 -15
- package/src/actions/data/ecdf.js +38 -0
- package/src/actions/data/edit.js +539 -0
- package/src/actions/data/filter.js +226 -30
- package/src/actions/data/fold.js +24 -0
- package/src/actions/data/impute.js +26 -0
- package/src/actions/data/index.js +38 -0
- package/src/actions/data/interval.js +2 -0
- package/src/actions/data/intervalEdit.js +86 -4
- package/src/actions/data/normalize.js +27 -0
- package/src/actions/data/regression.js +19 -21
- package/src/actions/data/revisionBindings.js +145 -0
- package/src/actions/data/shared.js +20 -4
- package/src/actions/data/stack.js +26 -0
- package/src/actions/data/summary.js +26 -0
- package/src/actions/data/timeUnit.js +4 -1
- package/src/actions/data/window.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/channels.js +415 -0
- package/src/actions/encodings/color/continuous.js +19 -59
- package/src/actions/encodings/color/index.js +30 -23
- 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 +124 -34
- package/src/actions/encodings/histogram.js +3 -2
- package/src/actions/encodings/horizon/resolve.js +8 -4
- package/src/actions/encodings/horizon.js +18 -8
- package/src/actions/encodings/index.js +6 -0
- package/src/actions/encodings/offset.js +30 -29
- package/src/actions/encodings/parallel.js +2 -8
- package/src/actions/encodings/position/apply.js +63 -11
- package/src/actions/encodings/position/index.js +52 -6
- 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 +57 -13
- package/src/actions/encodings/position/resolve.js +88 -26
- package/src/actions/encodings/ranged.js +123 -97
- package/src/actions/encodings/remove.js +44 -34
- package/src/actions/encodings/ruleAppearance.js +24 -32
- package/src/actions/encodings/seriesLayout.js +105 -0
- package/src/actions/encodings/shared.js +107 -37
- package/src/actions/encodings/stroke.js +226 -0
- 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 +41 -8
- package/src/actions/errorBands/edit.js +360 -25
- package/src/actions/errorBars/create.js +56 -17
- package/src/actions/errorBars/edit.js +372 -21
- package/src/actions/facets/actions.js +619 -41
- package/src/actions/facets/derive.js +139 -25
- package/src/actions/gradientPlots/components.js +43 -13
- package/src/actions/gradientPlots/edit.js +14 -4
- package/src/actions/gradientPlots/materialize.js +8 -8
- package/src/actions/gradientPlots/options.js +10 -2
- package/src/actions/guides/applicability.js +28 -19
- 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 +184 -28
- package/src/actions/guides/axes/lines.js +13 -7
- 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 +50 -25
- 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 +15 -8
- package/src/actions/guides/axes/ticks.js +11 -6
- package/src/actions/guides/axes/titles.js +32 -11
- 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 +17 -4
- package/src/actions/guides/guides.js +54 -31
- package/src/actions/guides/legends/blocks.js +426 -0
- package/src/actions/guides/legends/categorical/actions.js +309 -279
- package/src/actions/guides/legends/categorical/components.js +21 -17
- package/src/actions/guides/legends/categorical/index.js +37 -33
- package/src/actions/guides/legends/categorical/layout.js +123 -354
- package/src/actions/guides/legends/categorical/options.js +32 -16
- package/src/actions/guides/legends/categorical/recipes.js +114 -9
- package/src/actions/guides/legends/categorical/resolve.js +30 -15
- package/src/actions/guides/legends/categorical/symbols.js +87 -33
- package/src/actions/guides/legends/continuous/common.js +99 -26
- package/src/actions/guides/legends/continuous/gradient.js +120 -89
- package/src/actions/guides/legends/continuous/index.js +8 -2
- package/src/actions/guides/legends/continuous/interval.js +79 -95
- package/src/actions/guides/legends/continuous/opacity.js +113 -89
- package/src/actions/guides/legends/continuous/stroke.js +525 -0
- package/src/actions/guides/legends/creation.js +40 -0
- package/src/actions/guides/legends/edit.js +321 -90
- package/src/actions/guides/legends/focused.js +2 -2
- package/src/actions/guides/legends/index.js +2 -0
- package/src/actions/guides/legends/lane.js +149 -102
- package/src/actions/guides/legends/lifecycle.js +143 -0
- package/src/actions/guides/legends/remove.js +43 -78
- package/src/actions/guides/legends/sampling.js +140 -0
- package/src/actions/guides/legends/size.js +213 -116
- package/src/actions/guides/legends/strokeWidth.js +121 -73
- package/src/actions/guides/legends/target.js +123 -0
- package/src/actions/guides/legends/transition.js +192 -0
- package/src/actions/guides/polar/axes/facade.js +81 -66
- package/src/actions/guides/polar/axes/labels.js +28 -8
- package/src/actions/guides/polar/axes/lines.js +12 -5
- package/src/actions/guides/polar/axes/shared.js +22 -0
- package/src/actions/guides/polar/axes/ticks.js +6 -5
- package/src/actions/guides/polar/axes/titles.js +6 -5
- package/src/actions/guides/polar/grids.js +8 -8
- package/src/actions/guides/polar/resolve.js +12 -8
- package/src/actions/guides/reuse.js +44 -0
- package/src/actions/guides/tickValues.js +19 -21
- package/src/actions/index.js +15 -2
- package/src/actions/intervals/resolve.js +35 -15
- package/src/actions/marks/arc/actions.js +78 -20
- package/src/actions/marks/area/actions.js +51 -12
- package/src/actions/marks/area/materialize.js +22 -3
- package/src/actions/marks/bar/create.js +11 -2
- package/src/actions/marks/bar/edit.js +20 -3
- package/src/actions/marks/bar/materialize.js +33 -9
- package/src/actions/marks/index.js +14 -0
- package/src/actions/marks/line/actions.js +71 -113
- package/src/actions/marks/line/materialize.js +57 -56
- package/src/actions/marks/point/create.js +8 -2
- package/src/actions/marks/point/edit.js +29 -21
- 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 +154 -16
- package/src/actions/marks/point/packing.js +68 -0
- package/src/actions/marks/rect/actions.js +33 -12
- package/src/actions/marks/references.js +658 -0
- package/src/actions/marks/remove.js +87 -4
- package/src/actions/marks/rule/actions.js +77 -9
- package/src/actions/marks/rule/appearance.js +26 -0
- package/src/actions/marks/shared.js +13 -13
- package/src/actions/marks/text/actions.js +296 -13
- package/src/actions/marks/text/index.js +4 -0
- package/src/actions/marks/text/layout.js +156 -8
- package/src/actions/marks/text/removal.js +173 -0
- package/src/actions/marks/text/selection.js +73 -0
- package/src/actions/marks/tick/actions.js +22 -5
- package/src/actions/primitives/createGraphics.js +3 -1
- package/src/actions/primitives/editGraphics.js +26 -0
- package/src/actions/primitives/semantic.js +3 -2
- package/src/actions/primitives/semanticAction.js +120 -10
- package/src/actions/primitives/semanticValidation/guide.js +10 -0
- package/src/actions/primitives/semanticValidation/index.js +9 -1
- package/src/actions/primitives/semanticValidation/layer.js +23 -1
- package/src/actions/primitives/semanticValidation/scale.js +38 -8
- package/src/actions/regression/components.js +26 -11
- package/src/actions/regression/create.js +11 -3
- package/src/actions/regression/edit.js +40 -13
- package/src/actions/regression/resolve.js +1 -1
- package/src/actions/resources/index.js +7 -0
- package/src/actions/resources/remove.js +172 -0
- package/src/actions/scales/channels.js +212 -0
- package/src/actions/scales/consumers/common.js +21 -54
- package/src/actions/scales/consumers/families.js +28 -14
- package/src/actions/scales/consumers/index.js +42 -4
- package/src/actions/scales/consumers/seriesLayout.js +57 -12
- package/src/actions/scales/create.js +58 -26
- package/src/actions/scales/definitions.js +109 -39
- package/src/actions/scales/edit.js +101 -34
- package/src/actions/scales/editPolicy.js +95 -35
- package/src/actions/scales/index.js +6 -0
- package/src/actions/scales/materialize.js +14 -49
- package/src/actions/scales/offset.js +48 -0
- package/src/actions/scales/parallel.js +71 -0
- package/src/actions/scales/patch.js +1 -1
- package/src/actions/scales/preview.js +140 -0
- package/src/actions/scales/quantitativeColor.js +9 -5
- package/src/actions/selection/actions.js +154 -30
- package/src/actions/selection/index.js +3 -0
- package/src/actions/theme/actions.js +98 -0
- package/src/actions/theme/composition.js +238 -0
- package/src/actions/theme/index.js +16 -0
- package/src/actions/theme/reconcile.js +1130 -0
- package/src/actions/theme/state.js +160 -0
- package/src/actions/titles/actions.js +11 -10
- package/src/actions/titles/resolve.js +1 -40
- package/src/actions/violinPlots/create.js +90 -43
- package/src/actions/violinPlots/edit.js +192 -0
- package/src/actions/violinPlots/index.js +2 -0
- package/src/core/ChartProgram.js +7 -0
- package/src/core/action.js +32 -2
- package/src/core/compositionState.js +141 -8
- package/src/core/programState.js +20 -0
- package/src/core/resourceReferences.js +332 -0
- package/src/core/vocabulary.js +16 -3
- 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 +37 -13
- package/src/grammar/bars/geometry.js +44 -0
- package/src/grammar/bars/policy.js +9 -7
- package/src/grammar/bin.js +226 -0
- package/src/grammar/categoryOrder.js +34 -0
- package/src/grammar/complete.js +279 -0
- package/src/grammar/computed.js +351 -0
- package/src/grammar/coordinates.js +37 -0
- package/src/grammar/density.js +239 -37
- package/src/grammar/displayLabels.js +57 -0
- package/src/grammar/ecdf.js +198 -0
- package/src/grammar/facets/dependencies.js +133 -1
- package/src/grammar/facets/guides.js +3 -0
- package/src/grammar/facets/index.js +123 -52
- package/src/grammar/facets/scales.js +15 -18
- package/src/grammar/fold.js +123 -0
- package/src/grammar/horizon.js +6 -4
- package/src/grammar/impute.js +340 -0
- package/src/grammar/interval.js +37 -20
- package/src/grammar/lineSeries.js +20 -63
- package/src/grammar/markFilter.js +22 -4
- package/src/grammar/markLabelSelection.js +57 -0
- package/src/grammar/markLabels.js +110 -0
- package/src/grammar/normalize.js +366 -0
- package/src/grammar/parallelAxisTitles.js +16 -0
- package/src/grammar/pathSeries.js +109 -0
- package/src/grammar/pointPacking.js +183 -0
- package/src/grammar/pointShapes.js +13 -4
- package/src/grammar/polar.js +90 -8
- 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 +8 -2
- package/src/grammar/regression/models.js +22 -7
- package/src/grammar/regression/parameters.js +63 -13
- package/src/grammar/rotation.js +29 -0
- package/src/grammar/roundedRect.js +180 -0
- package/src/grammar/rules.js +8 -21
- package/src/grammar/scales/color.js +23 -1
- package/src/grammar/scales/colorConsumers.js +57 -0
- package/src/grammar/scales/continuous.js +2 -2
- package/src/grammar/scales/definition.js +42 -5
- package/src/grammar/scales/discretized.js +11 -5
- package/src/grammar/scales/fields.js +38 -8
- package/src/grammar/scales/index.js +1 -0
- package/src/grammar/scales/mapping.js +2 -0
- package/src/grammar/scales/ordinal.js +12 -3
- package/src/grammar/scales/policies.js +2 -2
- package/src/grammar/scales/radial.js +55 -0
- package/src/grammar/scales/size.js +456 -0
- package/src/grammar/scales/types.js +1 -0
- package/src/grammar/schemas/concreteGraphic.js +14 -2
- package/src/grammar/schemas/graphic.js +15 -0
- package/src/grammar/schemas/graphicBounds.js +179 -30
- package/src/grammar/schemas/semanticPath.js +23 -4
- package/src/grammar/stack.js +170 -0
- package/src/grammar/statisticalReference.js +103 -0
- package/src/grammar/statistics/confidenceInterval.js +101 -0
- package/src/grammar/strokeStyle.js +77 -0
- package/src/grammar/summary.js +245 -0
- package/src/grammar/text.js +22 -16
- package/src/grammar/timeUnit.js +234 -13
- package/src/grammar/transformTopology.js +10 -0
- package/src/grammar/transforms.js +418 -16
- package/src/grammar/valueFormat.js +112 -0
- package/src/grammar/weightedStatistics.js +374 -0
- package/src/grammar/window.js +232 -21
- package/src/layout/aspect.js +80 -0
- package/src/layout/facets.js +100 -12
- package/src/layout/guideCollisions.js +12 -0
- package/src/layout/labels.js +343 -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 +56 -147
- package/src/materialization/bars/histogram.js +104 -20
- package/src/materialization/bars/ranged.js +19 -3
- package/src/materialization/bars/resolve.js +7 -4
- package/src/materialization/categorySlotOffset.js +40 -0
- package/src/materialization/composition.js +3 -1
- package/src/materialization/compositionSnapshot.js +1 -1
- package/src/materialization/coordinateBounds.js +204 -0
- package/src/materialization/dependencies.js +93 -16
- package/src/materialization/encodings.js +1 -1
- package/src/materialization/facetGuides/legacyCategorical.js +3 -0
- package/src/materialization/facetGuides/placement.js +8 -2
- package/src/materialization/facetGuides/preparation.js +12 -7
- package/src/materialization/facetHeaders.js +346 -0
- package/src/materialization/facets.js +23 -66
- package/src/materialization/guides/layout.js +57 -0
- package/src/materialization/guides/resources.js +28 -2
- package/src/materialization/legends.js +11 -2
- package/src/materialization/marks/capabilities.js +20 -5
- package/src/materialization/marks/grouping.js +52 -0
- package/src/materialization/marks/index.js +68 -27
- package/src/materialization/marks/policies.js +4 -1
- package/src/materialization/rect.js +55 -42
- package/src/materialization/rectConfig.js +5 -1
- package/src/materialization/rowEncoding.js +13 -13
- package/src/materialization/scaleGuideDependencies.js +8 -5
- package/src/materialization/scales/map.js +6 -3
- package/src/materialization/scales/policies/arc.js +42 -0
- package/src/materialization/scales/policies/binnedPosition.js +7 -4
- package/src/materialization/scales/policies/offset.js +50 -8
- package/src/materialization/scales/policies/series.js +5 -1
- package/src/materialization/scales/resolve.js +65 -17
- package/src/materialization/selection/compatibility.js +11 -0
- package/src/materialization/selection/items/arc.js +28 -4
- package/src/materialization/selection/items/bar.js +42 -71
- package/src/materialization/selection/items/common.js +27 -6
- package/src/materialization/selection/items/path.js +3 -13
- package/src/materialization/selection/items/rect.js +35 -8
- package/src/materialization/selection/path.js +1 -1
- package/src/materialization/selection/point.js +3 -1
- package/src/materialization/text.js +324 -14
- package/src/mcp/adapter.js +2 -2
- package/src/renderers/canvas/circle.js +2 -3
- package/src/renderers/canvas/line.js +2 -3
- package/src/renderers/canvas/native.js +4 -4
- package/src/renderers/canvas/path.js +2 -3
- package/src/renderers/canvas/rect.js +2 -3
- package/src/renderers/canvas/stroke.js +11 -0
- package/src/renderers/svg.js +19 -0
- package/src/selectors/datasets.js +44 -0
- package/src/selectors/layers.js +7 -2
- package/src/selectors/scales.js +6 -0
- package/src/theme/defaults.js +140 -0
- package/types/basic.d.ts +17 -2
- package/types/index.d.ts +211 -0
- package/types/program.d.ts +2182 -204
- package/src/materialization/bars/grouped.js +0 -144
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isSourceOwnedText } from "../../../grammar/text.js";
|
|
2
|
+
import { withGuideLayoutValidation } from "../../../materialization/guides/layout.js";
|
|
1
3
|
import { action } from "../../../core/action.js";
|
|
2
4
|
import { validateUserId } from "../../../core/identifiers.js";
|
|
3
5
|
import {
|
|
@@ -5,7 +7,7 @@ import {
|
|
|
5
7
|
validateNonEmptyString,
|
|
6
8
|
validateNonNegativeFinite
|
|
7
9
|
} from "../../../core/validation.js";
|
|
8
|
-
import {
|
|
10
|
+
import { resolveGuideCoordinateBounds } from "../../../materialization/coordinateBounds.js";
|
|
9
11
|
import {
|
|
10
12
|
isTransformedScaleType,
|
|
11
13
|
mapContinuousScaleValues,
|
|
@@ -32,6 +34,7 @@ import {
|
|
|
32
34
|
import { resolveConcreteGraphicBounds } from
|
|
33
35
|
"../../../grammar/schemas/graphicBounds.js";
|
|
34
36
|
import { validateAxisTextStyle } from "./labels.js";
|
|
37
|
+
import { resolveRotation } from "../../../grammar/rotation.js";
|
|
35
38
|
|
|
36
39
|
function titleBounds(geometry, config, text) {
|
|
37
40
|
return resolveTextBounds({
|
|
@@ -47,7 +50,11 @@ function titleBounds(geometry, config, text) {
|
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
function inferredTitleOffset(program, channel, config) {
|
|
50
|
-
const plot =
|
|
53
|
+
const plot = resolveGuideCoordinateBounds(program, {
|
|
54
|
+
coordinate: program.semanticSpec.guides.axis?.[channel]?.coordinate,
|
|
55
|
+
scale: config.scale,
|
|
56
|
+
channel
|
|
57
|
+
});
|
|
51
58
|
const canvas = findCanvasGraphic(program)?.properties;
|
|
52
59
|
const labels = program.graphicSpec.objects[`${channel}AxisLabels`]
|
|
53
60
|
? resolveConcreteGraphicBounds(program.graphicSpec, `${channel}AxisLabels`)
|
|
@@ -102,7 +109,15 @@ export function inferAxisTitleText(program, channel, scaleId) {
|
|
|
102
109
|
const titles = new Set();
|
|
103
110
|
const primaryTitles = new Set();
|
|
104
111
|
for (const layer of program.semanticSpec.layers) {
|
|
105
|
-
|
|
112
|
+
if (isSourceOwnedText(layer)) continue;
|
|
113
|
+
const primary = layer.encoding?.[channel];
|
|
114
|
+
const encoding = layer.mark?.type === "area" && Object.hasOwn(primary ?? {}, "datum")
|
|
115
|
+
? layer.encoding?.[`${channel}2`] : primary;
|
|
116
|
+
if (channel === "radius" && layer.mark?.type === "arc" &&
|
|
117
|
+
encoding?.scale === scaleId && encoding.aggregate === "count" && encoding.field === undefined) {
|
|
118
|
+
titles.add(encoding.title ?? "count");
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
106
121
|
if (
|
|
107
122
|
encoding?.scale === scaleId &&
|
|
108
123
|
typeof encoding.field === "string" &&
|
|
@@ -140,7 +155,11 @@ export function inferAxisTitleText(program, channel, scaleId) {
|
|
|
140
155
|
|
|
141
156
|
function resolveGeometry(program, channel, config) {
|
|
142
157
|
const scale = program.resolvedScales[config.scale];
|
|
143
|
-
const bounds =
|
|
158
|
+
const bounds = resolveGuideCoordinateBounds(program, {
|
|
159
|
+
coordinate: program.semanticSpec.guides.axis?.[channel]?.coordinate,
|
|
160
|
+
scale: config.scale,
|
|
161
|
+
channel
|
|
162
|
+
});
|
|
144
163
|
const discrete = ["ordinal", "band", "point"].includes(scale?.type);
|
|
145
164
|
if ((
|
|
146
165
|
!["linear", "time", "ordinal", "band", "point"].includes(scale?.type) &&
|
|
@@ -193,12 +212,12 @@ function names(channel) {
|
|
|
193
212
|
|
|
194
213
|
function makeEdit(channel) {
|
|
195
214
|
const operation = names(channel);
|
|
196
|
-
return action({ op: operation.edit, description: `Edit the ${channel}-axis title.` }, function (args = {}) {
|
|
215
|
+
return action({ op: operation.edit, description: `Edit the ${channel}-axis title.` }, withGuideLayoutValidation(function (args = {}) {
|
|
197
216
|
validateKeys(args, EDIT_OPTIONS, operation.edit);
|
|
198
217
|
if (this.graphicSpec.objects[operation.graphic]?.type !== "text") throw new Error(`${operation.edit} requires an existing axis title.`);
|
|
199
218
|
const previous = this.guideConfigs.axis?.[channel]?.title;
|
|
200
219
|
if (!previous) throw new Error(`${operation.edit} requires title configuration.`);
|
|
201
|
-
const { text, ...appearance } = args;
|
|
220
|
+
const { text, rotation: requestedRotation, ...appearance } = args;
|
|
202
221
|
const explicitText = Object.hasOwn(args, "text");
|
|
203
222
|
const explicitRotation = Object.hasOwn(args, "rotation");
|
|
204
223
|
const position = appearance.position ?? previous.position;
|
|
@@ -211,7 +230,9 @@ function makeEdit(channel) {
|
|
|
211
230
|
position,
|
|
212
231
|
rotation: inferredRotation
|
|
213
232
|
? defaultAxisTitleRotation(channel, position)
|
|
214
|
-
:
|
|
233
|
+
: explicitRotation
|
|
234
|
+
? resolveRotation(requestedRotation, "Axis title rotation")
|
|
235
|
+
: previous.rotation,
|
|
215
236
|
inferredRotation,
|
|
216
237
|
inferredOffset: Object.hasOwn(args, "offset")
|
|
217
238
|
? false
|
|
@@ -245,7 +266,7 @@ function makeEdit(channel) {
|
|
|
245
266
|
};
|
|
246
267
|
for (const [property, value] of Object.entries(properties)) next = next.editGraphics({ target: operation.graphic, property, value });
|
|
247
268
|
return next;
|
|
248
|
-
});
|
|
269
|
+
}));
|
|
249
270
|
}
|
|
250
271
|
|
|
251
272
|
const editXAxisTitle = makeEdit("x");
|
|
@@ -253,7 +274,7 @@ const editYAxisTitle = makeEdit("y");
|
|
|
253
274
|
|
|
254
275
|
function makeCreate(channel) {
|
|
255
276
|
const operation = names(channel);
|
|
256
|
-
return action({ op: operation.create, description: `Create the ${channel}-axis title.` }, function (args = {}) {
|
|
277
|
+
return action({ op: operation.create, description: `Create the ${channel}-axis title.` }, withGuideLayoutValidation(function (args = {}) {
|
|
257
278
|
validateKeys(args, CREATE_OPTIONS, operation.create);
|
|
258
279
|
const {
|
|
259
280
|
text: requestedText,
|
|
@@ -281,7 +302,7 @@ function makeCreate(channel) {
|
|
|
281
302
|
offset: channel === "x" ? 42 : 52,
|
|
282
303
|
rotation: inferredRotation
|
|
283
304
|
? defaultAxisTitleRotation(channel, position)
|
|
284
|
-
: requestedRotation,
|
|
305
|
+
: resolveRotation(requestedRotation, "Axis title rotation"),
|
|
285
306
|
inferredRotation,
|
|
286
307
|
inferredOffset,
|
|
287
308
|
color: DEFAULTS.color,
|
|
@@ -303,7 +324,7 @@ function makeCreate(channel) {
|
|
|
303
324
|
...resolvePlotGraphicPlacement(this)
|
|
304
325
|
})
|
|
305
326
|
._withGuideConfig(channel, "title", config)[operation.edit]();
|
|
306
|
-
});
|
|
327
|
+
}));
|
|
307
328
|
}
|
|
308
329
|
|
|
309
330
|
const createXAxisTitle = makeCreate("x");
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
registerBasicCategoricalLegendActions
|
|
6
6
|
} from "./legends/categorical/index.js";
|
|
7
7
|
import {
|
|
8
|
+
registerIntervalLegendActions,
|
|
8
9
|
registerGradientLegendActions
|
|
9
10
|
} from "./legends/continuous/index.js";
|
|
10
11
|
import { registerSizeLegendActions } from "./legends/size.js";
|
|
@@ -13,6 +14,7 @@ export function registerBasicGuideActions(ProgramClass) {
|
|
|
13
14
|
registerBasicCartesianAxisActions(ProgramClass);
|
|
14
15
|
registerBasicCategoricalLegendActions(ProgramClass);
|
|
15
16
|
registerGradientLegendActions(ProgramClass);
|
|
17
|
+
registerIntervalLegendActions(ProgramClass);
|
|
16
18
|
registerSizeLegendActions(ProgramClass);
|
|
17
19
|
registerBasicGridActions(ProgramClass);
|
|
18
20
|
registerGuideCollectionActions(ProgramClass);
|
|
@@ -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,
|
|
@@ -9,7 +10,7 @@ import {
|
|
|
9
10
|
isTransformedScaleType,
|
|
10
11
|
mapContinuousScaleValues
|
|
11
12
|
} from "../../../grammar/scales/index.js";
|
|
12
|
-
import {
|
|
13
|
+
import { resolveGuideCoordinateBounds } from "../../../materialization/coordinateBounds.js";
|
|
13
14
|
import { resolveGridLineGeometry } from "../../../layout/grid.js";
|
|
14
15
|
import { DEFAULT_COLORS } from "../../../theme/defaults.js";
|
|
15
16
|
import { findCoordinate } from "../../../selectors/coordinates.js";
|
|
@@ -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;
|
|
@@ -240,7 +248,12 @@ export function editGridConfig(previous, args) {
|
|
|
240
248
|
|
|
241
249
|
export function resolveGridGeometry(program, config) {
|
|
242
250
|
const scale = program.resolvedScales[config.scale];
|
|
243
|
-
const
|
|
251
|
+
const channel = config.direction === "horizontal" ? "y" : "x";
|
|
252
|
+
const bounds = resolveGuideCoordinateBounds(program, {
|
|
253
|
+
coordinate: config.coordinate,
|
|
254
|
+
scale: config.scale,
|
|
255
|
+
channel
|
|
256
|
+
});
|
|
244
257
|
if ((
|
|
245
258
|
!["linear", "time"].includes(scale?.type) &&
|
|
246
259
|
!isTransformedScaleType(scale?.type)
|