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,4 +1,20 @@
|
|
|
1
1
|
import { validateUserId } from "../../../core/identifiers.js";
|
|
2
|
+
import {
|
|
3
|
+
LEGEND_CHANNELS,
|
|
4
|
+
LEGEND_CONFIG_KINDS
|
|
5
|
+
} from "../../../core/vocabulary.js";
|
|
6
|
+
import { isDiscreteSizeScaleType } from "../../../grammar/scales/index.js";
|
|
7
|
+
|
|
8
|
+
export function validateLegendChannels(channels, operation) {
|
|
9
|
+
if (!Array.isArray(channels)) throw new TypeError(`${operation} channels must be an array.`);
|
|
10
|
+
if (channels.length === 0) throw new Error(`${operation} channels must select at least one channel.`);
|
|
11
|
+
const seen = new Set();
|
|
12
|
+
for (const channel of channels) {
|
|
13
|
+
if (!LEGEND_CHANNELS.includes(channel)) throw new Error(`Unsupported legend channel "${channel}".`);
|
|
14
|
+
if (seen.has(channel)) throw new Error(`${operation} channels contains duplicate "${channel}".`);
|
|
15
|
+
seen.add(channel);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
2
18
|
|
|
3
19
|
export function legendTargets(program) {
|
|
4
20
|
return [...new Set(Object.values(program.guideConfigs.legend ?? {})
|
|
@@ -23,3 +39,110 @@ export function resolveLegendTarget(program, requested, operation) {
|
|
|
23
39
|
}
|
|
24
40
|
return targets[0];
|
|
25
41
|
}
|
|
42
|
+
|
|
43
|
+
const CATEGORICAL_KINDS = Object.freeze(["series", "color", "stroke"]);
|
|
44
|
+
const SINGLE_CHANNELS = Object.freeze({
|
|
45
|
+
size: "size",
|
|
46
|
+
opacity: "opacity",
|
|
47
|
+
strokeWidth: "strokeWidth",
|
|
48
|
+
gradient: "color",
|
|
49
|
+
interval: "color",
|
|
50
|
+
strokeGradient: "stroke",
|
|
51
|
+
strokeInterval: "stroke"
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const asciiCompare = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
|
55
|
+
|
|
56
|
+
export function legendBlockChannels(kind, config) {
|
|
57
|
+
const channels = CATEGORICAL_KINDS.includes(kind)
|
|
58
|
+
? config?.channels
|
|
59
|
+
: SINGLE_CHANNELS[kind] === undefined
|
|
60
|
+
? undefined
|
|
61
|
+
: [SINGLE_CHANNELS[kind]];
|
|
62
|
+
if (!Array.isArray(channels) || channels.length === 0) {
|
|
63
|
+
throw new Error(`Legend block ${kind} requires at least one channel.`);
|
|
64
|
+
}
|
|
65
|
+
validateLegendChannels(channels, `Legend block ${kind}`);
|
|
66
|
+
return Object.freeze([...channels].sort(asciiCompare));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function legendBlockKey(channels) {
|
|
70
|
+
if (!Array.isArray(channels) || channels.length === 0) {
|
|
71
|
+
throw new TypeError("Legend block key requires a non-empty channel array.");
|
|
72
|
+
}
|
|
73
|
+
return JSON.stringify([...channels].sort(asciiCompare));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function describeLegendBlock(program, kind, config) {
|
|
77
|
+
if (!LEGEND_CONFIG_KINDS.includes(kind)) {
|
|
78
|
+
throw new Error(`Unknown legend block kind "${kind}".`);
|
|
79
|
+
}
|
|
80
|
+
const channels = legendBlockChannels(kind, config);
|
|
81
|
+
const scaleIds = CATEGORICAL_KINDS.includes(kind)
|
|
82
|
+
? config?.scales
|
|
83
|
+
: [config?.scale];
|
|
84
|
+
if (!Array.isArray(scaleIds) || scaleIds.length === 0 ||
|
|
85
|
+
scaleIds.some(id => typeof id !== "string" || program.resolvedScales[id] === undefined)) {
|
|
86
|
+
throw new Error(`Legend block ${kind} requires resolved scales.`);
|
|
87
|
+
}
|
|
88
|
+
const family = CATEGORICAL_KINDS.includes(kind)
|
|
89
|
+
? "categorical"
|
|
90
|
+
: kind === "size"
|
|
91
|
+
? isDiscreteSizeScaleType(program.resolvedScales[scaleIds[0]].type)
|
|
92
|
+
? "discrete-size"
|
|
93
|
+
: "sampled"
|
|
94
|
+
: ["opacity", "strokeWidth"].includes(kind)
|
|
95
|
+
? "sampled"
|
|
96
|
+
: ["gradient", "strokeGradient"].includes(kind)
|
|
97
|
+
? "gradient"
|
|
98
|
+
: "interval";
|
|
99
|
+
return Object.freeze({
|
|
100
|
+
target: config.target,
|
|
101
|
+
kind,
|
|
102
|
+
family,
|
|
103
|
+
key: legendBlockKey(channels),
|
|
104
|
+
channels,
|
|
105
|
+
scaleIds: Object.freeze([...scaleIds]),
|
|
106
|
+
config
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function legendBlockDescriptors(program, target) {
|
|
111
|
+
const requested = validateUserId(target, "Legend target id");
|
|
112
|
+
const descriptors = LEGEND_CONFIG_KINDS.flatMap(kind => {
|
|
113
|
+
const config = program.guideConfigs.legend?.[kind];
|
|
114
|
+
return config?.target === requested
|
|
115
|
+
? [describeLegendBlock(program, kind, config)]
|
|
116
|
+
: [];
|
|
117
|
+
});
|
|
118
|
+
const seen = new Set();
|
|
119
|
+
for (const descriptor of descriptors) {
|
|
120
|
+
if (seen.has(descriptor.key)) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`Legend target "${requested}" has ambiguous block key ${descriptor.key}.`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
seen.add(descriptor.key);
|
|
126
|
+
}
|
|
127
|
+
return Object.freeze(descriptors);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function resolveLegendBlock(program, { target, channel }, operation = "editLegendBlock") {
|
|
131
|
+
const requested = resolveLegendTarget(program, target, operation);
|
|
132
|
+
if (!LEGEND_CHANNELS.includes(channel)) {
|
|
133
|
+
throw new Error(`${operation} has unsupported legend channel "${channel}".`);
|
|
134
|
+
}
|
|
135
|
+
const matches = legendBlockDescriptors(program, requested)
|
|
136
|
+
.filter(descriptor => descriptor.channels.includes(channel));
|
|
137
|
+
if (matches.length === 0) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
`${operation} target "${requested}" has no legend block for channel "${channel}".`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
if (matches.length !== 1) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`${operation} target "${requested}" has ambiguous channel "${channel}".`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
return matches[0];
|
|
148
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { isDiscretizedColorScaleType } from "../../../grammar/scales/types.js";
|
|
2
|
+
import { normalizeContinuousLegend } from "./continuous/common.js";
|
|
3
|
+
import { DEFAULT_GRADIENT_SIZE } from "./continuous/gradient.js";
|
|
4
|
+
import { normalizeIntervalLegend } from "./continuous/interval.js";
|
|
5
|
+
import {
|
|
6
|
+
describeLegendBlock,
|
|
7
|
+
legendBlockDescriptors,
|
|
8
|
+
resolveLegendBlock
|
|
9
|
+
} from "./target.js";
|
|
10
|
+
import { validateLegendBlockOverride } from "./blocks.js";
|
|
11
|
+
|
|
12
|
+
const family = (channel, type) => type === "sequential"
|
|
13
|
+
? channel === "stroke" ? "strokeGradient" : "gradient"
|
|
14
|
+
: isDiscretizedColorScaleType(type)
|
|
15
|
+
? channel === "stroke" ? "strokeInterval" : "interval"
|
|
16
|
+
: type === "ordinal"
|
|
17
|
+
? channel === "stroke" ? "stroke" : "color"
|
|
18
|
+
: undefined;
|
|
19
|
+
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
20
|
+
|
|
21
|
+
function categoricalFamilyCustom(program, kind, stored) {
|
|
22
|
+
const side = ["left", "right"].includes(stored.position);
|
|
23
|
+
const direction = side ? "vertical" : "horizontal";
|
|
24
|
+
const itemGap = side ? 28 : stored.position === "top" ? 24 : 20;
|
|
25
|
+
const order = program.semanticSpec.guides.legend?.[kind]?.order;
|
|
26
|
+
return stored.inferredSymbol !== true ||
|
|
27
|
+
stored.direction !== direction ||
|
|
28
|
+
(stored.columns !== undefined && !(side && stored.columns === 1)) ||
|
|
29
|
+
stored.itemGap !== itemGap || stored.layout !== "edge" ||
|
|
30
|
+
order !== undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function planChannelLegendTransition(program, scale, nextType, channel) {
|
|
34
|
+
const from = family(channel, scale.type), to = family(channel, nextType);
|
|
35
|
+
if (from === undefined || to === undefined || from === to) return undefined;
|
|
36
|
+
const stored = program.guideConfigs.legend?.[from];
|
|
37
|
+
if ((stored?.scale ?? stored?.scales?.[0]) !== scale.id) return undefined;
|
|
38
|
+
const oldDescriptor = describeLegendBlock(program, from, stored);
|
|
39
|
+
const overrideKeys = Object.keys(stored.blockOverrides ?? {});
|
|
40
|
+
if (overrideKeys.some(key => key !== oldDescriptor.key)) {
|
|
41
|
+
throw new Error(`${channel} legend transition found stale legend block overrides.`);
|
|
42
|
+
}
|
|
43
|
+
const blockOverride = stored.blockOverrides?.[oldDescriptor.key];
|
|
44
|
+
const side = ["left", "right"].includes(stored.position);
|
|
45
|
+
const categorical = ["color", "stroke"].includes(from);
|
|
46
|
+
const gradientFamily = from.toLowerCase().endsWith("gradient");
|
|
47
|
+
const defaults = gradientFamily ? normalizeContinuousLegend({ position: stored.position }, "gradient")
|
|
48
|
+
: normalizeIntervalLegend({ position: stored.position });
|
|
49
|
+
const comparableSymbol = channel === "stroke" && stored.inferredStrokeWidth === true
|
|
50
|
+
? { ...stored.symbol, strokeWidth: defaults.symbol?.strokeWidth }
|
|
51
|
+
: stored.symbol;
|
|
52
|
+
const custom = categorical
|
|
53
|
+
? categoricalFamilyCustom(program, from, stored)
|
|
54
|
+
: gradientFamily
|
|
55
|
+
? stored.count !== defaults.count || !same(stored.gradient, DEFAULT_GRADIENT_SIZE) || stored.itemGap !== defaults.itemGap
|
|
56
|
+
: !same(comparableSymbol, defaults.symbol) || stored.itemGap !== defaults.itemGap ||
|
|
57
|
+
stored.direction !== defaults.direction || (stored.columns !== undefined && !(side && stored.columns === 1));
|
|
58
|
+
if (custom) {
|
|
59
|
+
throw new Error(`${channel} legend transition cannot discard custom family settings; remove and recreate the legend explicitly.`);
|
|
60
|
+
}
|
|
61
|
+
if (blockOverride?.title !== undefined) {
|
|
62
|
+
throw new Error(`${channel} legend transition cannot distribute a legend block title; edit the destination block explicitly.`);
|
|
63
|
+
}
|
|
64
|
+
if (blockOverride?.labelMap !== undefined) {
|
|
65
|
+
throw new Error(`${channel} legend transition cannot distribute a legend block labelMap; edit the destination block explicitly.`);
|
|
66
|
+
}
|
|
67
|
+
if (blockOverride?.symbol !== undefined && ["gradient", "strokeGradient"].includes(to)) {
|
|
68
|
+
throw new Error(`${channel} legend transition cannot apply symbol overrides to a gradient block.`);
|
|
69
|
+
}
|
|
70
|
+
const args = {
|
|
71
|
+
target: stored.target, channels: [channel], position: stored.position,
|
|
72
|
+
labels: stored.labels, titleStyle: stored.titleStyle, border: stored.border,
|
|
73
|
+
align: stored.align, offset: stored.offset, titlePosition: stored.titlePosition,
|
|
74
|
+
...(stored.inferredTitle ? {} : { title: stored.title })
|
|
75
|
+
};
|
|
76
|
+
try {
|
|
77
|
+
if (["interval", "strokeInterval"].includes(to)) {
|
|
78
|
+
normalizeIntervalLegend({ ...args, channels: undefined });
|
|
79
|
+
}
|
|
80
|
+
else if (["gradient", "strokeGradient"].includes(to)) {
|
|
81
|
+
normalizeContinuousLegend(args, "gradient");
|
|
82
|
+
}
|
|
83
|
+
} catch (error) {
|
|
84
|
+
throw new Error(`${channel} legend transition has incompatible layout or style: ${error.message}`);
|
|
85
|
+
}
|
|
86
|
+
return { args, channel, titleVisible: stored.titleVisible !== false, blockOverride };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function planColorLegendTransitions(program, scale, nextType) {
|
|
90
|
+
return ["color", "stroke"].flatMap(channel => {
|
|
91
|
+
const plan = planChannelLegendTransition(program, scale, nextType, channel);
|
|
92
|
+
return plan === undefined ? [] : [plan];
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function applyColorLegendTransition(program, plan) {
|
|
97
|
+
let next = program.createLegend(plan.args);
|
|
98
|
+
if (!plan.titleVisible) {
|
|
99
|
+
next = next.editLegend({ target: plan.args.target, title: false });
|
|
100
|
+
}
|
|
101
|
+
if (plan.blockOverride === undefined) return next;
|
|
102
|
+
const descriptor = resolveLegendBlock(next, {
|
|
103
|
+
target: plan.args.target,
|
|
104
|
+
channel: plan.channel
|
|
105
|
+
}, `${plan.channel} legend transition`);
|
|
106
|
+
validateLegendBlockOverride(descriptor, plan.blockOverride);
|
|
107
|
+
next = next._withLegendConfig(descriptor.kind, {
|
|
108
|
+
...descriptor.config,
|
|
109
|
+
blockOverrides: { [descriptor.key]: plan.blockOverride }
|
|
110
|
+
});
|
|
111
|
+
return next.rematerializeLegend();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function intersects(left, right) {
|
|
115
|
+
return left.channels.some(channel => right.channels.includes(channel));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function explicitOrder(program, descriptor) {
|
|
119
|
+
if (descriptor.family !== "categorical") return undefined;
|
|
120
|
+
return program.semanticSpec.guides.legend?.[descriptor.kind]?.order;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function sameBlockValue(left, right) {
|
|
124
|
+
if (Object.is(left, right)) return true;
|
|
125
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
126
|
+
return left.length === right.length &&
|
|
127
|
+
left.every((value, index) => sameBlockValue(value, right[index]));
|
|
128
|
+
}
|
|
129
|
+
if (left !== null && right !== null &&
|
|
130
|
+
typeof left === "object" && typeof right === "object" &&
|
|
131
|
+
!Array.isArray(left) && !Array.isArray(right)) {
|
|
132
|
+
const leftKeys = Object.keys(left).sort();
|
|
133
|
+
const rightKeys = Object.keys(right).sort();
|
|
134
|
+
return leftKeys.length === rightKeys.length &&
|
|
135
|
+
leftKeys.every((key, index) => key === rightKeys[index] &&
|
|
136
|
+
sameBlockValue(left[key], right[key]));
|
|
137
|
+
}
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function transitionError(target, oldDescriptors, nextDescriptor, field) {
|
|
142
|
+
const oldKeys = oldDescriptors.map(item => item.key).join(", ");
|
|
143
|
+
throw new Error(
|
|
144
|
+
`Legend block transition for target "${target}" from ${oldKeys} to ${nextDescriptor.key} conflicts on ${field}.`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function withoutBlockOverrides(config) {
|
|
149
|
+
const { blockOverrides: _removed, ...rest } = config;
|
|
150
|
+
return rest;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function planLegendBlockTransitions(program, target, plans) {
|
|
154
|
+
const oldDescriptors = legendBlockDescriptors(program, target);
|
|
155
|
+
const nextDescriptors = plans.map(plan =>
|
|
156
|
+
describeLegendBlock(program, plan.kind, plan.config));
|
|
157
|
+
return plans.map((plan, index) => {
|
|
158
|
+
const descriptor = nextDescriptors[index];
|
|
159
|
+
const incoming = oldDescriptors.filter(old => intersects(old, descriptor));
|
|
160
|
+
let override;
|
|
161
|
+
if (incoming.length === 1 && incoming[0].key === descriptor.key) {
|
|
162
|
+
override = incoming[0].config.blockOverrides?.[incoming[0].key];
|
|
163
|
+
} else if (incoming.length === 1) {
|
|
164
|
+
const old = incoming[0];
|
|
165
|
+
override = old.config.blockOverrides?.[old.key];
|
|
166
|
+
if (override?.title !== undefined) {
|
|
167
|
+
transitionError(target, incoming, descriptor, "title");
|
|
168
|
+
}
|
|
169
|
+
if (override?.labelMap !== undefined) {
|
|
170
|
+
transitionError(target, incoming, descriptor, "labelMap");
|
|
171
|
+
}
|
|
172
|
+
} else if (incoming.length > 1) {
|
|
173
|
+
const overrides = incoming.map(old => old.config.blockOverrides?.[old.key]);
|
|
174
|
+
if (!overrides.every(value => sameBlockValue(value, overrides[0]))) {
|
|
175
|
+
transitionError(target, incoming, descriptor, "block override");
|
|
176
|
+
}
|
|
177
|
+
const orders = incoming.map(old => explicitOrder(program, old));
|
|
178
|
+
if (!orders.every(value => sameBlockValue(value, orders[0]))) {
|
|
179
|
+
transitionError(target, incoming, descriptor, "order");
|
|
180
|
+
}
|
|
181
|
+
override = overrides[0];
|
|
182
|
+
}
|
|
183
|
+
if (override !== undefined) validateLegendBlockOverride(descriptor, override);
|
|
184
|
+
const config = withoutBlockOverrides(plan.config);
|
|
185
|
+
return {
|
|
186
|
+
...plan,
|
|
187
|
+
config: override === undefined
|
|
188
|
+
? config
|
|
189
|
+
: { ...config, blockOverrides: { [descriptor.key]: override } }
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
}
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { action } from "../../../../core/action.js";
|
|
2
|
-
import {
|
|
2
|
+
import { resolvePolarGuideResources } from "../resolve.js";
|
|
3
3
|
import {
|
|
4
|
-
|
|
4
|
+
AXIS_COMPONENTS,
|
|
5
|
+
assertRemovableAxisComponent,
|
|
6
|
+
hasAxisComponent,
|
|
7
|
+
removeAxisComponent,
|
|
8
|
+
validateEnabledAxisComponents
|
|
9
|
+
} from "../../axes/components.js";
|
|
10
|
+
import {
|
|
11
|
+
labelEditOptions,
|
|
5
12
|
LINE_EDIT_OPTIONS,
|
|
6
13
|
prefix,
|
|
7
14
|
resolveAngle,
|
|
@@ -22,12 +29,15 @@ const AXIS_EDIT_OPTIONS = Object.freeze([
|
|
|
22
29
|
"angle", "line", "ticks", "labels", "ticksAndLabels", "title"
|
|
23
30
|
]);
|
|
24
31
|
|
|
25
|
-
function
|
|
26
|
-
validateObject(args,
|
|
27
|
-
|
|
32
|
+
export function validatePolarAxisArgs(kind, args, operation) {
|
|
33
|
+
validateObject(args, kind === "theta"
|
|
34
|
+
? AXIS_OPTIONS.filter(option => option !== "angle")
|
|
35
|
+
: AXIS_OPTIONS, operation);
|
|
36
|
+
validateEnabledAxisComponents(args, operation);
|
|
37
|
+
if (Object.hasOwn(args, "line") && args.line !== false) {
|
|
28
38
|
validateObject(args.line, LINE_EDIT_OPTIONS, `${operation}.line`);
|
|
29
39
|
}
|
|
30
|
-
if (Object.hasOwn(args, "ticksAndLabels")) {
|
|
40
|
+
if (Object.hasOwn(args, "ticksAndLabels") && args.ticksAndLabels !== false) {
|
|
31
41
|
validateObject(
|
|
32
42
|
args.ticksAndLabels,
|
|
33
43
|
TICK_GROUP_OPTIONS,
|
|
@@ -44,7 +54,7 @@ function validateAxisArgs(kind, args, operation) {
|
|
|
44
54
|
if (Object.hasOwn(args.ticksAndLabels, "labels")) {
|
|
45
55
|
validateObject(
|
|
46
56
|
args.ticksAndLabels.labels,
|
|
47
|
-
|
|
57
|
+
labelEditOptions(kind).filter(key => !["count", "values"].includes(key)),
|
|
48
58
|
`${operation}.ticksAndLabels.labels`
|
|
49
59
|
);
|
|
50
60
|
}
|
|
@@ -68,7 +78,7 @@ function makeCreateAxis(kind) {
|
|
|
68
78
|
op: operation,
|
|
69
79
|
description: `Create the complete Polar ${kind} axis.`
|
|
70
80
|
}, function (args = {}) {
|
|
71
|
-
|
|
81
|
+
validatePolarAxisArgs(kind, args, operation);
|
|
72
82
|
const resources = resolvePolarGuideResources(this, kind, args, operation);
|
|
73
83
|
const angle = resolveAngle(this, kind, args);
|
|
74
84
|
const shared = {
|
|
@@ -82,21 +92,16 @@ function makeCreateAxis(kind) {
|
|
|
82
92
|
...(Object.hasOwn(group, "values") ? { values: group.values } : {})
|
|
83
93
|
};
|
|
84
94
|
let next = this;
|
|
85
|
-
if (
|
|
86
|
-
next = next
|
|
95
|
+
if (args.line !== false) {
|
|
96
|
+
next = next[`create${prefix(kind)}AxisLine`]({ ...shared, ...(args.line ?? {}) });
|
|
87
97
|
}
|
|
88
|
-
|
|
89
|
-
[`create${prefix(kind)}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
...mode,
|
|
93
|
-
...(group.ticks ?? {})
|
|
94
|
-
})
|
|
95
|
-
[`create${prefix(kind)}AxisLabels`]({
|
|
96
|
-
...shared,
|
|
97
|
-
...mode,
|
|
98
|
-
...(group.labels ?? {})
|
|
98
|
+
if (args.ticksAndLabels !== false) {
|
|
99
|
+
next = next[`create${prefix(kind)}AxisTicks`]({
|
|
100
|
+
...shared, ...mode, ...(group.ticks ?? {})
|
|
101
|
+
})[`create${prefix(kind)}AxisLabels`]({
|
|
102
|
+
...shared, ...mode, ...(group.labels ?? {})
|
|
99
103
|
});
|
|
104
|
+
}
|
|
100
105
|
return args.title === false
|
|
101
106
|
? next
|
|
102
107
|
: next[`create${prefix(kind)}AxisTitle`]({
|
|
@@ -118,29 +123,28 @@ function validateAxisEditArgs(kind, args, operation) {
|
|
|
118
123
|
throw new Error(`${operation} requires at least one axis change.`);
|
|
119
124
|
}
|
|
120
125
|
if (Object.hasOwn(args, "angle")) validateAngle(args.angle);
|
|
121
|
-
if (Object.hasOwn(args, "line")) {
|
|
126
|
+
if (Object.hasOwn(args, "line") && args.line !== false) {
|
|
122
127
|
validateObject(args.line, LINE_EDIT_OPTIONS, `${operation}.line`);
|
|
123
128
|
}
|
|
124
|
-
if (Object.hasOwn(args, "ticks")) {
|
|
129
|
+
if (Object.hasOwn(args, "ticks") && args.ticks !== false) {
|
|
125
130
|
validateObject(args.ticks, TICK_EDIT_OPTIONS, `${operation}.ticks`);
|
|
126
131
|
validateModeOptions(args.ticks, `${operation}.ticks`);
|
|
127
132
|
}
|
|
128
|
-
if (Object.hasOwn(args, "labels")) {
|
|
129
|
-
validateObject(args.labels,
|
|
133
|
+
if (Object.hasOwn(args, "labels") && args.labels !== false) {
|
|
134
|
+
validateObject(args.labels, labelEditOptions(kind), `${operation}.labels`);
|
|
130
135
|
validateModeOptions(args.labels, `${operation}.labels`);
|
|
131
136
|
}
|
|
132
|
-
if (
|
|
137
|
+
if (args.ticksAndLabels !== undefined &&
|
|
138
|
+
(args.ticks !== undefined || args.labels !== undefined)) {
|
|
139
|
+
throw new Error(`${operation} cannot combine ticksAndLabels with ticks or labels.`);
|
|
140
|
+
}
|
|
141
|
+
if (Object.hasOwn(args, "ticksAndLabels") && args.ticksAndLabels !== false) {
|
|
133
142
|
validateObject(
|
|
134
143
|
args.ticksAndLabels,
|
|
135
144
|
TICK_GROUP_OPTIONS,
|
|
136
145
|
`${operation}.ticksAndLabels`
|
|
137
146
|
);
|
|
138
147
|
validateModeOptions(args.ticksAndLabels, `${operation}.ticksAndLabels`);
|
|
139
|
-
if (args.ticks !== undefined || args.labels !== undefined) {
|
|
140
|
-
throw new Error(
|
|
141
|
-
`${operation} cannot combine ticksAndLabels with ticks or labels.`
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
148
|
if (args.ticksAndLabels.ticks !== undefined) {
|
|
145
149
|
validateObject(
|
|
146
150
|
args.ticksAndLabels.ticks,
|
|
@@ -151,12 +155,12 @@ function validateAxisEditArgs(kind, args, operation) {
|
|
|
151
155
|
if (args.ticksAndLabels.labels !== undefined) {
|
|
152
156
|
validateObject(
|
|
153
157
|
args.ticksAndLabels.labels,
|
|
154
|
-
|
|
158
|
+
labelEditOptions(kind).filter(key => !["count", "values"].includes(key)),
|
|
155
159
|
`${operation}.ticksAndLabels.labels`
|
|
156
160
|
);
|
|
157
161
|
}
|
|
158
162
|
}
|
|
159
|
-
if (Object.hasOwn(args, "title")) {
|
|
163
|
+
if (Object.hasOwn(args, "title") && args.title !== false) {
|
|
160
164
|
validateObject(
|
|
161
165
|
args.title,
|
|
162
166
|
kind === "theta"
|
|
@@ -167,6 +171,41 @@ function validateAxisEditArgs(kind, args, operation) {
|
|
|
167
171
|
}
|
|
168
172
|
}
|
|
169
173
|
|
|
174
|
+
function applyAxisEdit(program, kind, args) {
|
|
175
|
+
const angleChanged = Object.hasOwn(args, "angle");
|
|
176
|
+
let next = angleChanged
|
|
177
|
+
? program._withGuideConfig("radius", "layout", { angle: args.angle })
|
|
178
|
+
: program;
|
|
179
|
+
const edit = (component, options) => {
|
|
180
|
+
if (options === false) {
|
|
181
|
+
next = removeAxisComponent(next, kind, component);
|
|
182
|
+
} else if (options !== undefined || angleChanged && hasAxisComponent(next, kind, component)) {
|
|
183
|
+
next = next[`edit${prefix(kind)}Axis${component[0].toUpperCase() + component.slice(1)}`](options ?? {});
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
edit("line", args.line);
|
|
187
|
+
if (args.ticksAndLabels === false) {
|
|
188
|
+
edit("ticks", false);
|
|
189
|
+
edit("labels", false);
|
|
190
|
+
} else if (args.ticksAndLabels !== undefined) {
|
|
191
|
+
const group = args.ticksAndLabels;
|
|
192
|
+
const mode = {
|
|
193
|
+
...(Object.hasOwn(group, "count") ? { count: group.count } : {}),
|
|
194
|
+
...(Object.hasOwn(group, "values") ? { values: group.values } : {})
|
|
195
|
+
};
|
|
196
|
+
edit("ticks", { ...mode, ...(group.ticks ?? {}) });
|
|
197
|
+
edit("labels", { ...mode, ...(group.labels ?? {}) });
|
|
198
|
+
} else {
|
|
199
|
+
edit("ticks", args.ticks);
|
|
200
|
+
edit("labels", args.labels);
|
|
201
|
+
}
|
|
202
|
+
edit("title", args.title);
|
|
203
|
+
if (!AXIS_COMPONENTS.some(component => hasAxisComponent(next, kind, component))) {
|
|
204
|
+
next = next[`remove${prefix(kind)}Axis`]();
|
|
205
|
+
}
|
|
206
|
+
return next;
|
|
207
|
+
}
|
|
208
|
+
|
|
170
209
|
function makeEditAxis(kind) {
|
|
171
210
|
const operation = `edit${prefix(kind)}Axis`;
|
|
172
211
|
return action({
|
|
@@ -174,42 +213,18 @@ function makeEditAxis(kind) {
|
|
|
174
213
|
description: `Edit selected Polar ${kind}-axis components.`
|
|
175
214
|
}, function (args = {}) {
|
|
176
215
|
validateAxisEditArgs(kind, args, operation);
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
let next = angleChanged
|
|
180
|
-
? this._withGuideConfig("radius", "layout", { angle: args.angle })
|
|
181
|
-
: this;
|
|
182
|
-
const has = component =>
|
|
183
|
-
next.graphicSpec.objects[names[component]] !== undefined;
|
|
184
|
-
if (args.line !== undefined || (angleChanged && has("line"))) {
|
|
185
|
-
next = next[`edit${prefix(kind)}AxisLine`](args.line ?? {});
|
|
216
|
+
if (!AXIS_COMPONENTS.some(component => hasAxisComponent(this, kind, component))) {
|
|
217
|
+
throw new Error(`${operation} requires an existing ${kind}-axis component.`);
|
|
186
218
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
...(Object.hasOwn(group, "values") ? { values: group.values } : {})
|
|
192
|
-
};
|
|
193
|
-
next = next[`edit${prefix(kind)}AxisTicks`]({
|
|
194
|
-
...mode,
|
|
195
|
-
...(group.ticks ?? {})
|
|
196
|
-
});
|
|
197
|
-
next = next[`edit${prefix(kind)}AxisLabels`]({
|
|
198
|
-
...mode,
|
|
199
|
-
...(group.labels ?? {})
|
|
200
|
-
});
|
|
201
|
-
} else {
|
|
202
|
-
if (args.ticks !== undefined || (angleChanged && has("ticks"))) {
|
|
203
|
-
next = next[`edit${prefix(kind)}AxisTicks`](args.ticks ?? {});
|
|
219
|
+
for (const component of AXIS_COMPONENTS) {
|
|
220
|
+
if (args[component] === false ||
|
|
221
|
+
args.ticksAndLabels === false && ["ticks", "labels"].includes(component)) {
|
|
222
|
+
assertRemovableAxisComponent(this, kind, component, operation);
|
|
204
223
|
}
|
|
205
|
-
if (args.labels !== undefined || (angleChanged && has("labels"))) {
|
|
206
|
-
next = next[`edit${prefix(kind)}AxisLabels`](args.labels ?? {});
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
if (args.title !== undefined || (angleChanged && has("title"))) {
|
|
210
|
-
next = next[`edit${prefix(kind)}AxisTitle`](args.title ?? {});
|
|
211
224
|
}
|
|
212
|
-
|
|
225
|
+
// Validate the whole proposal on an immutable branch before returning changes.
|
|
226
|
+
applyAxisEdit(this, kind, args);
|
|
227
|
+
return applyAxisEdit(this, kind, args);
|
|
213
228
|
});
|
|
214
229
|
}
|
|
215
230
|
|
|
@@ -20,18 +20,21 @@ import {
|
|
|
20
20
|
} from "../resolve.js";
|
|
21
21
|
import {
|
|
22
22
|
componentResources,
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
labelCreateOptions,
|
|
24
|
+
labelEditOptions,
|
|
25
25
|
operations,
|
|
26
26
|
prefix,
|
|
27
27
|
resolveAngle,
|
|
28
28
|
validateModeOptions,
|
|
29
|
+
validateComponentCreateArgs,
|
|
29
30
|
validateObject,
|
|
30
31
|
withAxisSemantics
|
|
31
32
|
} from "./shared.js";
|
|
33
|
+
import { normalizeDisplayLabelMap } from
|
|
34
|
+
"../../../../grammar/displayLabels.js";
|
|
32
35
|
|
|
33
|
-
function labelGeometry(program, kind, config) {
|
|
34
|
-
const frame = resolvePolarFrameForProgram(program);
|
|
36
|
+
function labelGeometry(program, kind, config, angle = resolveAngle(program, kind, {})) {
|
|
37
|
+
const frame = resolvePolarFrameForProgram(program, config.coordinate);
|
|
35
38
|
const mapped = mapPolarGuideValues(program, config);
|
|
36
39
|
const text = formatPolarGuideValues(program, config, mapped.values);
|
|
37
40
|
const geometry = {
|
|
@@ -45,7 +48,7 @@ function labelGeometry(program, kind, config) {
|
|
|
45
48
|
})
|
|
46
49
|
: resolveRadialAxisLabels({
|
|
47
50
|
frame,
|
|
48
|
-
angle
|
|
51
|
+
angle,
|
|
49
52
|
radii: mapped.positions,
|
|
50
53
|
offset: config.offset
|
|
51
54
|
}))
|
|
@@ -114,6 +117,23 @@ function resolveLabelConfig(program, kind, args, resources, previous) {
|
|
|
114
117
|
fontWeight: args.fontWeight ?? previous?.fontWeight ??
|
|
115
118
|
POLAR_AXIS_DEFAULTS.labels.fontWeight
|
|
116
119
|
};
|
|
120
|
+
const labelScale = program.resolvedScales[resources.scale];
|
|
121
|
+
if (Object.hasOwn(args, "labelMap")) {
|
|
122
|
+
if (kind !== "theta" || !["ordinal", "band", "point"].includes(labelScale?.type)) {
|
|
123
|
+
throw new Error(`${prefix(kind)} axis labelMap requires a categorical theta scale.`);
|
|
124
|
+
}
|
|
125
|
+
if (args.labelMap === "auto") delete config.labelMap;
|
|
126
|
+
else {
|
|
127
|
+
config.labelMap = normalizeDisplayLabelMap(
|
|
128
|
+
args.labelMap,
|
|
129
|
+
`${prefix(kind)} axis labelMap`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (config.labelMap !== undefined &&
|
|
134
|
+
(kind !== "theta" || !["ordinal", "band", "point"].includes(labelScale?.type))) {
|
|
135
|
+
throw new Error(`${prefix(kind)} axis labelMap requires a categorical theta scale.`);
|
|
136
|
+
}
|
|
117
137
|
validatePolarTickConfig(config, `${kind}-axis labels`);
|
|
118
138
|
validateNonNegativeFinite(config.offset, "Polar axis label offset");
|
|
119
139
|
validatePolarLabelFormat(config.format);
|
|
@@ -127,7 +147,7 @@ function makeEditLabels(kind) {
|
|
|
127
147
|
op: operation.edit,
|
|
128
148
|
description: `Edit the Polar ${kind}-axis labels.`
|
|
129
149
|
}, function (args = {}) {
|
|
130
|
-
validateObject(args,
|
|
150
|
+
validateObject(args, labelEditOptions(kind), operation.edit);
|
|
131
151
|
validateModeOptions(args, operation.edit);
|
|
132
152
|
const names = polarGuideNames(kind);
|
|
133
153
|
const previous = this.guideConfigs.axis?.[kind]?.labels;
|
|
@@ -171,7 +191,7 @@ function makeCreateLabels(kind) {
|
|
|
171
191
|
op: operation.create,
|
|
172
192
|
description: `Create the Polar ${kind}-axis labels.`
|
|
173
193
|
}, function (args = {}) {
|
|
174
|
-
|
|
194
|
+
validateComponentCreateArgs(kind, args, labelCreateOptions(kind), operation.create);
|
|
175
195
|
validateModeOptions(args, operation.create);
|
|
176
196
|
const names = polarGuideNames(kind);
|
|
177
197
|
if (this.graphicSpec.objects[names.labels] !== undefined) {
|
|
@@ -180,7 +200,7 @@ function makeCreateLabels(kind) {
|
|
|
180
200
|
const resources = componentResources(this, kind, args, operation.create);
|
|
181
201
|
const angle = resolveAngle(this, kind, args);
|
|
182
202
|
const config = resolveLabelConfig(this, kind, args, resources);
|
|
183
|
-
labelGeometry(this, kind, config);
|
|
203
|
+
labelGeometry(this, kind, config, angle);
|
|
184
204
|
let next = withAxisSemantics(this, kind, resources);
|
|
185
205
|
if (kind === "radius" &&
|
|
186
206
|
next.guideConfigs.axis?.radius?.layout === undefined) {
|