ggaction 0.0.12 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/README.md +2 -2
- package/knowledge/action-card.schema.json +1 -1
- package/knowledge/action-cards.json +1 -1
- package/knowledge/action-cards.schema.json +1 -1
- package/knowledge/extension-authoring.md +2 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/knowledge/task-resolver.js +1 -1
- package/package.json +4 -2
- package/src/actions/basic.js +2 -0
- package/src/actions/boxPlots/materialize.js +2 -1
- package/src/actions/canvas/actions.js +3 -1
- package/src/actions/canvas/fitting.js +211 -0
- package/src/actions/canvas/index.js +2 -0
- package/src/actions/categoryOrder/index.js +3 -2
- package/src/actions/charts/area.js +47 -0
- package/src/actions/charts/bar.js +11 -5
- package/src/actions/charts/beeswarm.js +70 -0
- package/src/actions/charts/density.js +44 -0
- package/src/actions/charts/ecdf.js +183 -0
- package/src/actions/charts/endpoints.js +451 -0
- package/src/actions/charts/heatmap.js +2 -2
- package/src/actions/charts/histogram.js +1 -1
- package/src/actions/charts/horizon.js +71 -0
- package/src/actions/charts/index.js +37 -0
- package/src/actions/charts/interval-regression.js +169 -0
- package/src/actions/charts/line.js +5 -5
- package/src/actions/charts/parallel.js +1 -1
- package/src/actions/charts/pie.js +44 -0
- package/src/actions/charts/polar.js +123 -0
- package/src/actions/charts/radar.js +299 -0
- package/src/actions/charts/radial.js +42 -0
- package/src/actions/charts/raincloud.js +422 -0
- package/src/actions/charts/rug-strip.js +279 -0
- package/src/actions/charts/scatter.js +9 -3
- package/src/actions/charts/shared.js +69 -10
- package/src/actions/composition/actions.js +118 -1
- package/src/actions/data/bin.js +29 -0
- package/src/actions/data/computed.js +24 -0
- package/src/actions/data/derived.js +66 -0
- package/src/actions/data/ecdf.js +38 -0
- package/src/actions/data/filter.js +212 -28
- package/src/actions/data/fold.js +24 -0
- package/src/actions/data/index.js +24 -0
- package/src/actions/data/interval.js +2 -0
- package/src/actions/data/intervalEdit.js +86 -4
- package/src/actions/data/regression.js +5 -2
- package/src/actions/data/stack.js +26 -0
- package/src/actions/data/summary.js +26 -0
- package/src/actions/data/timeUnit.js +1 -1
- package/src/actions/encodings/appearance.js +32 -13
- package/src/actions/encodings/barWidth.js +8 -0
- package/src/actions/encodings/basic.js +2 -0
- package/src/actions/encodings/color/continuous.js +19 -59
- package/src/actions/encodings/color/index.js +28 -22
- package/src/actions/encodings/color/layout.js +3 -97
- package/src/actions/encodings/color/policy.js +5 -8
- package/src/actions/encodings/density/resolve.js +37 -5
- package/src/actions/encodings/density.js +120 -33
- package/src/actions/encodings/horizon/resolve.js +8 -4
- package/src/actions/encodings/horizon.js +18 -8
- package/src/actions/encodings/index.js +2 -0
- package/src/actions/encodings/offset.js +10 -16
- package/src/actions/encodings/parallel.js +2 -8
- package/src/actions/encodings/position/apply.js +10 -4
- package/src/actions/encodings/position/index.js +43 -5
- package/src/actions/encodings/position/policies/arc.js +14 -0
- package/src/actions/encodings/position/policies/area.js +4 -42
- package/src/actions/encodings/position/policies/bar.js +48 -13
- package/src/actions/encodings/position/resolve.js +67 -22
- package/src/actions/encodings/ranged.js +123 -97
- package/src/actions/encodings/remove.js +37 -32
- package/src/actions/encodings/ruleAppearance.js +23 -9
- package/src/actions/encodings/seriesLayout.js +104 -0
- package/src/actions/encodings/shared.js +25 -23
- package/src/actions/encodings/strokeDash.js +9 -6
- package/src/actions/encodings/temporal.js +7 -0
- package/src/actions/encodings/text.js +30 -15
- package/src/actions/errorBands/create.js +18 -5
- package/src/actions/errorBands/edit.js +341 -21
- package/src/actions/errorBars/create.js +21 -10
- package/src/actions/errorBars/edit.js +355 -19
- package/src/actions/facets/actions.js +410 -12
- package/src/actions/facets/derive.js +57 -8
- package/src/actions/gradientPlots/components.js +33 -11
- package/src/actions/gradientPlots/materialize.js +5 -7
- package/src/actions/guides/applicability.js +20 -17
- package/src/actions/guides/axes/axes.js +4 -3
- package/src/actions/guides/axes/axis.js +18 -18
- package/src/actions/guides/axes/components.js +43 -0
- package/src/actions/guides/axes/edit.js +16 -51
- package/src/actions/guides/axes/labels.js +129 -19
- package/src/actions/guides/axes/lines.js +7 -5
- package/src/actions/guides/axes/parallel/lifecycle.js +117 -0
- package/src/actions/guides/axes/parallel/policy.js +137 -0
- package/src/actions/guides/axes/parallel/resolve.js +44 -23
- package/src/actions/guides/axes/parallel.js +93 -217
- package/src/actions/guides/axes/policy.js +21 -67
- package/src/actions/guides/axes/tickGroups.js +14 -8
- package/src/actions/guides/axes/ticks.js +5 -4
- package/src/actions/guides/axes/titles.js +21 -8
- package/src/actions/guides/basic.js +2 -0
- package/src/actions/guides/facade.js +106 -0
- package/src/actions/guides/facadeAxes.js +200 -0
- package/src/actions/guides/facadeLegend.js +63 -0
- package/src/actions/guides/grids/grid.js +37 -33
- package/src/actions/guides/grids/resolve.js +10 -2
- package/src/actions/guides/guides.js +35 -28
- package/src/actions/guides/legends/categorical/actions.js +193 -249
- package/src/actions/guides/legends/categorical/components.js +4 -5
- package/src/actions/guides/legends/categorical/layout.js +36 -349
- package/src/actions/guides/legends/categorical/options.js +30 -14
- package/src/actions/guides/legends/categorical/recipes.js +14 -0
- package/src/actions/guides/legends/categorical/resolve.js +21 -10
- package/src/actions/guides/legends/categorical/symbols.js +2 -3
- package/src/actions/guides/legends/continuous/common.js +69 -14
- package/src/actions/guides/legends/continuous/gradient.js +111 -83
- package/src/actions/guides/legends/continuous/index.js +6 -2
- package/src/actions/guides/legends/continuous/interval.js +71 -92
- package/src/actions/guides/legends/continuous/opacity.js +105 -84
- package/src/actions/guides/legends/creation.js +27 -0
- package/src/actions/guides/legends/edit.js +198 -66
- package/src/actions/guides/legends/focused.js +2 -2
- package/src/actions/guides/legends/lane.js +98 -86
- package/src/actions/guides/legends/lifecycle.js +132 -0
- package/src/actions/guides/legends/remove.js +31 -76
- package/src/actions/guides/legends/size.js +149 -105
- package/src/actions/guides/legends/strokeWidth.js +106 -64
- package/src/actions/guides/legends/target.js +12 -0
- package/src/actions/guides/legends/transition.js +43 -0
- package/src/actions/guides/polar/axes/facade.js +77 -62
- package/src/actions/guides/polar/axes/labels.js +5 -4
- package/src/actions/guides/polar/axes/lines.js +2 -1
- package/src/actions/guides/polar/axes/shared.js +6 -0
- package/src/actions/guides/polar/axes/ticks.js +5 -4
- package/src/actions/guides/polar/axes/titles.js +5 -4
- package/src/actions/guides/polar/grids.js +7 -7
- package/src/actions/guides/reuse.js +44 -0
- package/src/actions/guides/tickValues.js +19 -21
- package/src/actions/index.js +2 -0
- package/src/actions/intervals/resolve.js +35 -15
- package/src/actions/marks/arc/actions.js +1 -1
- package/src/actions/marks/area/actions.js +18 -4
- package/src/actions/marks/area/materialize.js +5 -2
- package/src/actions/marks/bar/materialize.js +11 -7
- package/src/actions/marks/index.js +5 -0
- package/src/actions/marks/line/actions.js +43 -108
- package/src/actions/marks/line/materialize.js +34 -52
- package/src/actions/marks/point/edit.js +11 -15
- package/src/actions/marks/point/index.js +1 -0
- package/src/actions/marks/point/jitter.js +3 -0
- package/src/actions/marks/point/materialize.js +106 -2
- package/src/actions/marks/point/packing.js +68 -0
- package/src/actions/marks/references.js +87 -0
- package/src/actions/marks/remove.js +17 -4
- package/src/actions/marks/rule/actions.js +34 -3
- package/src/actions/marks/rule/appearance.js +26 -0
- package/src/actions/marks/shared.js +12 -12
- package/src/actions/marks/text/actions.js +224 -6
- package/src/actions/marks/text/layout.js +2 -2
- package/src/actions/primitives/createGraphics.js +3 -1
- package/src/actions/primitives/semantic.js +1 -1
- package/src/actions/primitives/semanticAction.js +39 -2
- package/src/actions/primitives/semanticValidation/guide.js +10 -0
- package/src/actions/primitives/semanticValidation/layer.js +18 -1
- package/src/actions/primitives/semanticValidation/scale.js +10 -1
- package/src/actions/regression/create.js +1 -1
- package/src/actions/regression/edit.js +19 -6
- package/src/actions/regression/resolve.js +1 -1
- package/src/actions/scales/consumers/common.js +20 -53
- package/src/actions/scales/consumers/families.js +27 -14
- package/src/actions/scales/consumers/index.js +23 -2
- package/src/actions/scales/consumers/seriesLayout.js +33 -2
- package/src/actions/scales/create.js +4 -4
- package/src/actions/scales/definitions.js +19 -3
- package/src/actions/scales/edit.js +46 -34
- package/src/actions/scales/editPolicy.js +21 -31
- package/src/actions/scales/materialize.js +10 -46
- package/src/actions/scales/patch.js +1 -1
- package/src/actions/scales/preview.js +39 -0
- package/src/actions/scales/quantitativeColor.js +9 -5
- package/src/actions/theme/actions.js +43 -0
- package/src/actions/theme/index.js +9 -0
- package/src/actions/theme/reconcile.js +882 -0
- package/src/actions/titles/actions.js +11 -10
- package/src/actions/titles/resolve.js +1 -40
- package/src/actions/violinPlots/create.js +86 -42
- package/src/actions/violinPlots/edit.js +191 -0
- package/src/actions/violinPlots/index.js +2 -0
- package/src/core/ChartProgram.js +7 -0
- package/src/core/action.js +22 -2
- package/src/core/compositionState.js +136 -8
- package/src/grammar/aggregate.js +66 -23
- package/src/grammar/arcs.js +58 -1
- package/src/grammar/areaEndpoints.js +46 -0
- package/src/grammar/areaSeries.js +87 -41
- package/src/grammar/bars/aggregate.js +33 -12
- package/src/grammar/bars/policy.js +9 -7
- package/src/grammar/bin.js +181 -0
- package/src/grammar/categoryOrder.js +34 -0
- package/src/grammar/computed.js +150 -0
- package/src/grammar/ecdf.js +198 -0
- package/src/grammar/facets/index.js +117 -6
- package/src/grammar/fold.js +123 -0
- package/src/grammar/horizon.js +6 -4
- package/src/grammar/interval.js +37 -20
- package/src/grammar/lineSeries.js +20 -63
- package/src/grammar/markFilter.js +22 -4
- package/src/grammar/markLabels.js +93 -0
- package/src/grammar/parallelAxisTitles.js +16 -0
- package/src/grammar/pathSeries.js +91 -0
- package/src/grammar/pointPacking.js +183 -0
- package/src/grammar/positionDatum.js +12 -0
- package/src/grammar/rects.js +9 -1
- package/src/grammar/regression/derive.js +4 -0
- package/src/grammar/regression/index.js +7 -2
- package/src/grammar/regression/models.js +22 -7
- package/src/grammar/regression/parameters.js +40 -13
- package/src/grammar/rotation.js +29 -0
- package/src/grammar/rules.js +7 -21
- package/src/grammar/scales/color.js +23 -1
- package/src/grammar/scales/colorConsumers.js +38 -0
- package/src/grammar/scales/definition.js +15 -1
- package/src/grammar/scales/discretized.js +11 -5
- package/src/grammar/scales/fields.js +38 -8
- package/src/grammar/scales/mapping.js +2 -0
- package/src/grammar/scales/radial.js +55 -0
- package/src/grammar/scales/types.js +1 -0
- package/src/grammar/schemas/semanticPath.js +17 -3
- package/src/grammar/stack.js +170 -0
- package/src/grammar/statistics/confidenceInterval.js +101 -0
- package/src/grammar/summary.js +183 -0
- package/src/grammar/text.js +22 -16
- package/src/grammar/timeUnit.js +21 -11
- package/src/grammar/transformTopology.js +6 -0
- package/src/grammar/transforms.js +36 -0
- package/src/grammar/valueFormat.js +112 -0
- package/src/grammar/window.js +13 -7
- package/src/layout/facets.js +41 -7
- package/src/layout/guideCollisions.js +12 -0
- package/src/layout/legend.js +2 -10
- package/src/layout/legendItems.js +102 -0
- package/src/layout/legendLane.js +100 -11
- package/src/materialization/bars/aggregate.js +38 -152
- package/src/materialization/bars/histogram.js +24 -0
- package/src/materialization/categorySlotOffset.js +40 -0
- package/src/materialization/compositionSnapshot.js +1 -1
- package/src/materialization/dependencies.js +57 -16
- package/src/materialization/encodings.js +1 -1
- package/src/materialization/facets.js +8 -1
- package/src/materialization/guides/layout.js +57 -0
- package/src/materialization/guides/resources.js +2 -2
- package/src/materialization/legends.js +10 -1
- package/src/materialization/marks/capabilities.js +20 -5
- package/src/materialization/marks/grouping.js +52 -0
- package/src/materialization/marks/index.js +58 -23
- package/src/materialization/marks/policies.js +2 -0
- package/src/materialization/rect.js +33 -33
- package/src/materialization/rowEncoding.js +13 -13
- package/src/materialization/scaleGuideDependencies.js +8 -5
- package/src/materialization/scales/map.js +1 -0
- package/src/materialization/scales/policies/arc.js +42 -0
- package/src/materialization/scales/policies/binnedPosition.js +7 -4
- package/src/materialization/scales/policies/series.js +2 -1
- package/src/materialization/scales/resolve.js +46 -14
- package/src/materialization/selection/compatibility.js +11 -0
- package/src/materialization/selection/items/bar.js +12 -68
- package/src/materialization/selection/items/common.js +8 -2
- package/src/materialization/selection/items/path.js +3 -13
- package/src/materialization/selection/items/rect.js +7 -4
- package/src/materialization/text.js +64 -5
- package/src/mcp/adapter.js +2 -2
- package/src/selectors/datasets.js +13 -0
- package/src/selectors/layers.js +3 -2
- package/src/selectors/scales.js +6 -0
- package/src/theme/defaults.js +39 -0
- package/types/basic.d.ts +11 -2
- package/types/index.d.ts +126 -0
- package/types/program.d.ts +1320 -135
- package/src/materialization/bars/grouped.js +0 -144
|
@@ -1,34 +1,27 @@
|
|
|
1
|
+
import { readAreaEndpoint, validateAreaEndpointPair, normalizeAreaBound } from "../../grammar/areaEndpoints.js";
|
|
1
2
|
import { action } from "../../core/action.js";
|
|
2
3
|
import { validateUserId } from "../../core/identifiers.js";
|
|
3
4
|
import { isPlainObject } from "../../core/immutable.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
validateSemanticFieldType
|
|
10
|
-
} from "../../grammar/scales/index.js";
|
|
11
|
-
import { normalizeRuleDatum } from "../../grammar/rules.js";
|
|
12
|
-
import {
|
|
13
|
-
canMaterializeArea,
|
|
14
|
-
canMaterializeLine,
|
|
15
|
-
getPositionEncodingMaterializationSteps
|
|
16
|
-
} from "../../materialization/marks/index.js";
|
|
5
|
+
import { readNominalField, readQuantitativeField, readScaleField, readTemporalField, resolveTemporalUnit, validateSemanticFieldType } from "../../grammar/scales/index.js";
|
|
6
|
+
import { normalizePositionDatum } from "../../grammar/positionDatum.js";
|
|
7
|
+
import { normalizeGroupFields } from "../../grammar/pathSeries.js";
|
|
8
|
+
import { assertPathGroupCompatible, validatePathGroupAppearance } from "../../materialization/marks/grouping.js";
|
|
9
|
+
import { canMaterializeArea, canMaterializeLine, getPositionEncodingMaterializationSteps } from "../../materialization/marks/index.js";
|
|
17
10
|
import { findLayer } from "../../selectors/layers.js";
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
} from "
|
|
11
|
+
import { resolveTarget, setEncodingProperties, validateOptions } from "./shared.js";
|
|
12
|
+
import { applyTemporalUnit } from "./temporal.js";
|
|
13
|
+
import { deriveAreaSeries } from "../../grammar/areaSeries.js";
|
|
14
|
+
import { resolvePositionEncoding } from "./position/resolve.js";
|
|
15
|
+
import { resolveScalePreview } from "../scales/preview.js";
|
|
16
|
+
import { findUpstreamTransform } from "../../materialization/dataProvenance.js";
|
|
24
17
|
|
|
25
18
|
const SECONDARY_OPTIONS = Object.freeze([
|
|
26
|
-
"field", "datum", "target", "fieldType", "scale", "coordinate"
|
|
19
|
+
"field", "datum", "target", "fieldType", "scale", "coordinate", "temporalUnit"
|
|
27
20
|
]);
|
|
28
21
|
const RANGE_OPTIONS = Object.freeze([
|
|
29
|
-
"lower", "upper", "target", "fieldType", "coordinate", "scale"
|
|
22
|
+
"lower", "upper", "target", "fieldType", "coordinate", "scale", "temporalUnit"
|
|
30
23
|
]);
|
|
31
|
-
const GROUP_OPTIONS = Object.freeze(["field", "target", "fieldType"]);
|
|
24
|
+
const GROUP_OPTIONS = Object.freeze(["field", "fields", "target", "fieldType"]);
|
|
32
25
|
|
|
33
26
|
function validateSecondaryScale(args, primaryScale, axisLabel) {
|
|
34
27
|
if (args.scale === undefined) return;
|
|
@@ -41,11 +34,11 @@ function validateSecondaryScale(args, primaryScale, axisLabel) {
|
|
|
41
34
|
}
|
|
42
35
|
}
|
|
43
36
|
|
|
44
|
-
function validateSecondaryField(dataset, field, fieldType) {
|
|
37
|
+
function validateSecondaryField(dataset, field, fieldType, temporalUnit) {
|
|
45
38
|
if (["nominal", "ordinal"].includes(fieldType)) {
|
|
46
39
|
readNominalField(dataset.values, field);
|
|
47
40
|
} else if (fieldType === "temporal") {
|
|
48
|
-
readTemporalField(dataset.values, field);
|
|
41
|
+
readTemporalField(dataset.values, field, temporalUnit);
|
|
49
42
|
} else {
|
|
50
43
|
readQuantitativeField(dataset.values, field);
|
|
51
44
|
}
|
|
@@ -75,12 +68,14 @@ function encodeSecondaryPosition(program, channel, args, operation, types) {
|
|
|
75
68
|
validateSecondaryScale(args, primary.scale, primaryChannel);
|
|
76
69
|
|
|
77
70
|
const rule = layer.mark.type === "rule";
|
|
71
|
+
const area = layer.mark.type === "area";
|
|
72
|
+
const rect = layer.mark.type === "rect";
|
|
78
73
|
const hasField = Object.hasOwn(args, "field");
|
|
79
74
|
const hasDatum = Object.hasOwn(args, "datum");
|
|
80
|
-
if (rule && hasField === hasDatum) {
|
|
81
|
-
throw new Error(`${operation} requires exactly one of field or datum for a
|
|
75
|
+
if ((rule || area || rect) && hasField === hasDatum) {
|
|
76
|
+
throw new Error(`${operation} requires exactly one of field or datum for a ${layer.mark.type} mark.`);
|
|
82
77
|
}
|
|
83
|
-
if (!rule && (!hasField || hasDatum)) {
|
|
78
|
+
if (!rule && !area && !rect && (!hasField || hasDatum)) {
|
|
84
79
|
throw new Error(`${operation} requires a field for a ranged mark.`);
|
|
85
80
|
}
|
|
86
81
|
if (rule && args.fieldType === undefined) {
|
|
@@ -114,14 +109,20 @@ function encodeSecondaryPosition(program, channel, args, operation, types) {
|
|
|
114
109
|
`${operation} fieldType must match the primary ${primaryChannel} fieldType.`
|
|
115
110
|
);
|
|
116
111
|
}
|
|
117
|
-
if (hasField && layer.mark.type === "rect") {
|
|
118
|
-
readScaleField(dataset.values, args.field, fieldType, { allowUnknown: true });
|
|
119
|
-
} else if (hasField) validateSecondaryField(dataset, args.field, fieldType);
|
|
120
|
-
else normalizeRuleDatum(args.datum, fieldType, channel);
|
|
121
|
-
|
|
122
112
|
const previous = layer.encoding?.[channel];
|
|
113
|
+
const temporalUnit = resolveTemporalUnit(args, fieldType, previous);
|
|
114
|
+
if (area) {
|
|
115
|
+
const endpoint = { ...(hasField ? { field: args.field } : { datum: args.datum }), fieldType };
|
|
116
|
+
validateAreaEndpointPair(primary, endpoint);
|
|
117
|
+
readAreaEndpoint(dataset.values, endpoint, layer.mark.missing);
|
|
118
|
+
} else if (hasField && layer.mark.type === "rect") {
|
|
119
|
+
readScaleField(dataset.values, args.field, fieldType, { allowUnknown: true, temporalUnit });
|
|
120
|
+
} else if (hasField) validateSecondaryField(dataset, args.field, fieldType, temporalUnit);
|
|
121
|
+
else normalizePositionDatum(args.datum, fieldType, channel, temporalUnit, rule ? "Rule" : "Rect");
|
|
122
|
+
|
|
123
123
|
let next = program;
|
|
124
124
|
if (layer.mark.type === "bar") {
|
|
125
|
+
if (layer.layout?.mode !== undefined && layer.layout.mode !== "overlay") throw new Error("Ranged bars support only overlay layout; clear series layout before assigning endpoints.");
|
|
125
126
|
for (const property of ["aggregate", "stack", "bin"]) {
|
|
126
127
|
if (layer.encoding?.[primaryChannel]?.[property] !== undefined) {
|
|
127
128
|
next = next.editSemantic({
|
|
@@ -146,6 +147,7 @@ function encodeSecondaryPosition(program, channel, args, operation, types) {
|
|
|
146
147
|
scale: primary.scale
|
|
147
148
|
});
|
|
148
149
|
|
|
150
|
+
next = applyTemporalUnit(next, target, channel, temporalUnit, previous);
|
|
149
151
|
const updated = findLayer(next, target);
|
|
150
152
|
for (const step of getPositionEncodingMaterializationSteps(
|
|
151
153
|
next,
|
|
@@ -189,91 +191,91 @@ const encodeY2 = action(
|
|
|
189
191
|
}
|
|
190
192
|
);
|
|
191
193
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
},
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
field: args.lower,
|
|
202
|
-
...(target === undefined ? {} : { target }),
|
|
203
|
-
fieldType: args.fieldType ?? "quantitative",
|
|
204
|
-
...(args.coordinate === undefined ? {} : { coordinate: args.coordinate }),
|
|
205
|
-
...(args.scale === undefined ? {} : { scale: args.scale })
|
|
206
|
-
});
|
|
207
|
-
return lower.encodeY2({
|
|
208
|
-
field: args.upper,
|
|
209
|
-
...(target === undefined ? {} : { target }),
|
|
194
|
+
function rangeAction(channel) {
|
|
195
|
+
const primary = channel === "x" ? "encodeX" : "encodeY";
|
|
196
|
+
const secondary = channel === "x" ? "encodeX2" : "encodeY2";
|
|
197
|
+
const op = channel === "x" ? "encodeXRange" : "encodeYRange";
|
|
198
|
+
return action({ op, description: "Atomically encode lower and upper bounds." }, function (args = {}) {
|
|
199
|
+
validateOptions(args, RANGE_OPTIONS, op);
|
|
200
|
+
const common = {
|
|
201
|
+
...(args.target === undefined ? {} : { target: args.target }),
|
|
202
|
+
...(Object.hasOwn(args, "temporalUnit") ? { temporalUnit: args.temporalUnit } : {}),
|
|
210
203
|
fieldType: args.fieldType ?? "quantitative"
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
...
|
|
226
|
-
fieldType: args.fieldType ?? "quantitative",
|
|
204
|
+
};
|
|
205
|
+
const { layer, dataset } = resolveTarget(this, args.target, ["area", "bar", "rect", "rule"], "ranged mark");
|
|
206
|
+
let next = this;
|
|
207
|
+
let lower = { field: args.lower }, upper = { field: args.upper };
|
|
208
|
+
if (layer.mark.type === "area") {
|
|
209
|
+
common.target = layer.id;
|
|
210
|
+
const prepared = prepareAreaRange(this, layer, dataset, channel, args, common);
|
|
211
|
+
lower = prepared.lower; upper = prepared.upper;
|
|
212
|
+
const secondaryChannel = `${channel}2`;
|
|
213
|
+
if (layer.encoding?.[secondaryChannel] !== undefined) next = next.editSemantic({ property: `layer[${layer.id}].encoding.${secondaryChannel}`, remove: true });
|
|
214
|
+
next = setEncodingProperties(next, layer.id, secondaryChannel, prepared.secondary);
|
|
215
|
+
common.target = layer.id;
|
|
216
|
+
}
|
|
217
|
+
return next[primary]({
|
|
218
|
+
...common, ...lower,
|
|
227
219
|
...(args.coordinate === undefined ? {} : { coordinate: args.coordinate }),
|
|
228
220
|
...(args.scale === undefined ? {} : { scale: args.scale })
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
236
|
-
);
|
|
221
|
+
})[secondary]({ ...common, ...upper });
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const encodeYRange = rangeAction("y");
|
|
226
|
+
const encodeXRange = rangeAction("x");
|
|
237
227
|
|
|
238
228
|
const encodeGroup = action(
|
|
239
229
|
{
|
|
240
230
|
op: "encodeGroup",
|
|
241
|
-
description: "Split path geometry by
|
|
231
|
+
description: "Split path geometry by nominal identity fields without a scale."
|
|
242
232
|
},
|
|
243
233
|
function (args = {}) {
|
|
244
234
|
validateOptions(args, GROUP_OPTIONS, "encodeGroup");
|
|
245
235
|
const { id: target, dataset, layer } = resolveTarget(
|
|
246
236
|
this,
|
|
247
237
|
args.target,
|
|
248
|
-
["line", "area"],
|
|
249
|
-
"
|
|
238
|
+
["line", "area", "bar"],
|
|
239
|
+
"series mark"
|
|
250
240
|
);
|
|
251
241
|
if ((args.fieldType ?? "nominal") !== "nominal") {
|
|
252
242
|
throw new Error("encodeGroup requires a nominal field.");
|
|
253
243
|
}
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
: undefined;
|
|
258
|
-
if (densityTransform !== undefined && densityTransform.groupBy !== args.field) {
|
|
259
|
-
throw new Error(
|
|
260
|
-
densityTransform.groupBy === undefined
|
|
261
|
-
? `Ungrouped density area mark "${target}" cannot encode group.`
|
|
262
|
-
: `Density area mark "${target}" must group by "${densityTransform.groupBy}".`
|
|
263
|
-
);
|
|
244
|
+
const hasFields = Object.hasOwn(args, "fields");
|
|
245
|
+
if (hasFields === Object.hasOwn(args, "field")) {
|
|
246
|
+
throw new Error("encodeGroup requires exactly one of field or fields.");
|
|
264
247
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
248
|
+
if (hasFields ? !Array.isArray(args.fields) : typeof args.field !== "string") {
|
|
249
|
+
throw new TypeError("encodeGroup field must be a string and fields must be an array.");
|
|
250
|
+
}
|
|
251
|
+
const fields = normalizeGroupFields(hasFields ? args.fields : args.field);
|
|
252
|
+
const group = {
|
|
253
|
+
...(fields.length === 1 ? { field: fields[0] } : { fields }),
|
|
254
|
+
fieldType: "nominal"
|
|
255
|
+
};
|
|
256
|
+
assertPathGroupCompatible(this, layer, dataset, group);
|
|
257
|
+
for (const field of fields) readNominalField(dataset.values, field);
|
|
258
|
+
if (layer.mark.type !== "bar") validatePathGroupAppearance(this, {
|
|
259
|
+
...layer, encoding: { ...layer.encoding, group }
|
|
260
|
+
}, dataset);
|
|
261
|
+
let next = this;
|
|
262
|
+
const alternate = fields.length === 1 ? "fields" : "field";
|
|
263
|
+
if (layer.encoding?.group?.[alternate] !== undefined) {
|
|
264
|
+
next = next.editSemantic({
|
|
265
|
+
property: `layer[${target}].encoding.group.${alternate}`, remove: true
|
|
275
266
|
});
|
|
267
|
+
}
|
|
268
|
+
if (layer.encoding?.group?.inferredFrom !== undefined) next = next.editSemantic({ property: `layer[${target}].encoding.group.inferredFrom`, remove: true });
|
|
269
|
+
next = setEncodingProperties(next, target, "group", group);
|
|
270
|
+
if (layer.mark.type === "bar") {
|
|
271
|
+
const mode = layer.layout?.mode;
|
|
272
|
+
if (mode !== undefined) return next.layoutSeries({ target, mode });
|
|
273
|
+
if (layer.encoding?.x?.scale === undefined || layer.encoding?.y?.scale === undefined) return next;
|
|
274
|
+
for (const step of getPositionEncodingMaterializationSteps(next, findLayer(next, target), layer.encoding?.y?.scale)) next = next[step.op](step.args);
|
|
275
|
+
return next;
|
|
276
|
+
}
|
|
276
277
|
if (layer.mark.type === "area") {
|
|
278
|
+
if (layer.layout?.mode !== undefined) return next.layoutSeries({ target, mode: layer.layout.mode });
|
|
277
279
|
const updated = findLayer(next, target);
|
|
278
280
|
return canMaterializeArea(next, updated)
|
|
279
281
|
? next.rematerializeAreaMark({ id: target })
|
|
@@ -297,3 +299,27 @@ export function registerRangedEncodingActions(ProgramClass) {
|
|
|
297
299
|
ProgramClass.prototype.encodeXRange = encodeXRange;
|
|
298
300
|
ProgramClass.prototype.encodeYRange = encodeYRange;
|
|
299
301
|
}
|
|
302
|
+
|
|
303
|
+
export function prepareAreaRange(program, layer, dataset, channel, args, common) {
|
|
304
|
+
const lower = normalizeAreaBound(args.lower), upper = normalizeAreaBound(args.upper);
|
|
305
|
+
validateAreaEndpointPair(lower, upper);
|
|
306
|
+
if (common.fieldType !== "quantitative") throw new Error("Area range requires quantitative endpoints.");
|
|
307
|
+
readAreaEndpoint(dataset.values, { ...upper, fieldType: "quantitative" }, layer.mark.missing);
|
|
308
|
+
const resolved = resolvePositionEncoding(program, channel, { ...common, ...lower,
|
|
309
|
+
...(args.coordinate === undefined ? {} : { coordinate: args.coordinate }),
|
|
310
|
+
...(args.scale === undefined ? {} : { scale: args.scale }) }, `encode${channel.toUpperCase()}Range`);
|
|
311
|
+
const secondary = { ...upper, fieldType: "quantitative", scale: resolved.scale.id };
|
|
312
|
+
const primary = { ...lower, fieldType: "quantitative", scale: resolved.scale.id };
|
|
313
|
+
const updated = { ...layer, coordinate: resolved.coordinate.id,
|
|
314
|
+
encoding: { ...layer.encoding, [channel]: primary, [`${channel}2`]: secondary } };
|
|
315
|
+
const scales = program.semanticSpec.scales.filter(scale => scale.id !== resolved.scale.id);
|
|
316
|
+
const preview = { ...program, markConfigs: program.markConfigs, canvasConfig: program.canvasConfig,
|
|
317
|
+
semanticSpec: { ...program.semanticSpec,
|
|
318
|
+
layers: program.semanticSpec.layers.map(item => item.id === layer.id ? updated : item),
|
|
319
|
+
scales: [...scales, resolved.scale] } };
|
|
320
|
+
resolveScalePreview(preview, resolved.scale.id);
|
|
321
|
+
if (canMaterializeArea(preview, updated) && !findUpstreamTransform(program, dataset, "density")) {
|
|
322
|
+
deriveAreaSeries(dataset.values, updated);
|
|
323
|
+
}
|
|
324
|
+
return { lower, upper, secondary };
|
|
325
|
+
}
|
|
@@ -15,8 +15,16 @@ import {
|
|
|
15
15
|
getSourceDependentMarkSteps
|
|
16
16
|
} from "../../materialization/marks/index.js";
|
|
17
17
|
import { findLayer } from "../../selectors/layers.js";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
removeLegendKinds,
|
|
20
|
+
resolveCategoricalLegendRevision,
|
|
21
|
+
createCategoricalLegendFromConfig
|
|
22
|
+
} from "../guides/legends/lifecycle.js";
|
|
19
23
|
import { clearMarkGraphic } from "./shared.js";
|
|
24
|
+
import { applyDetachedScaleRematerialization } from "../../materialization/dependencies.js";
|
|
25
|
+
import { findDataset } from "../../selectors/datasets.js";
|
|
26
|
+
import { assertPathGroupCompatible, validatePathGroupAppearance } from "../../materialization/marks/grouping.js";
|
|
27
|
+
import { assertEncodingSelectionCompatibility } from "../../materialization/selection/compatibility.js";
|
|
20
28
|
|
|
21
29
|
const OPTIONS = Object.freeze(["target", "channel"]);
|
|
22
30
|
const REMOVABLE_CHANNELS = Object.freeze([
|
|
@@ -36,10 +44,6 @@ const SPECIALIZED_LEGEND_KIND = Object.freeze({
|
|
|
36
44
|
opacity: Object.freeze(["opacity"]),
|
|
37
45
|
strokeWidth: Object.freeze(["strokeWidth"])
|
|
38
46
|
});
|
|
39
|
-
const LEGEND_OPTION_KEYS = Object.freeze([
|
|
40
|
-
"position", "align", "direction", "columns", "offset", "titlePosition",
|
|
41
|
-
"title", "symbol", "labels", "titleStyle", "itemGap", "border"
|
|
42
|
-
]);
|
|
43
47
|
|
|
44
48
|
function resolveTarget(program, requested, channel) {
|
|
45
49
|
const candidates = program.semanticSpec.layers.filter(
|
|
@@ -77,7 +81,7 @@ function activeCascade(layer, channel) {
|
|
|
77
81
|
channels.add("y2");
|
|
78
82
|
channels.add("yOffset");
|
|
79
83
|
}
|
|
80
|
-
if (channel === "color" && layer.mark?.type === "bar") {
|
|
84
|
+
if (channel === "color" && layer.mark?.type === "bar" && layer.layout === undefined) {
|
|
81
85
|
if (resolveBarColorLayout(layer) === "group") {
|
|
82
86
|
channels.add(resolveBarOffsetChannel(layer));
|
|
83
87
|
}
|
|
@@ -85,6 +89,7 @@ function activeCascade(layer, channel) {
|
|
|
85
89
|
if (
|
|
86
90
|
channel === "group" &&
|
|
87
91
|
layer.mark?.type === "area" &&
|
|
92
|
+
layer.layout === undefined &&
|
|
88
93
|
layer.encoding?.color?.field !== undefined &&
|
|
89
94
|
layer.encoding.color.field === layer.encoding.group?.field
|
|
90
95
|
) {
|
|
@@ -93,21 +98,6 @@ function activeCascade(layer, channel) {
|
|
|
93
98
|
return [...channels].filter(active => layer.encoding?.[active] !== undefined);
|
|
94
99
|
}
|
|
95
100
|
|
|
96
|
-
function assertSelectionCompatibility(program, target, channels) {
|
|
97
|
-
for (const [id, selection] of Object.entries(
|
|
98
|
-
program.materializationConfigs.selections ?? {}
|
|
99
|
-
)) {
|
|
100
|
-
if (
|
|
101
|
-
selection.target === target &&
|
|
102
|
-
channels.includes(selection.selector?.channel)
|
|
103
|
-
) {
|
|
104
|
-
throw new Error(
|
|
105
|
-
`Cannot remove ${selection.selector.channel} encoding while selection "${id}" references that channel.`
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
101
|
function removeChannelConfigs(program, target, channels) {
|
|
112
102
|
let next = program;
|
|
113
103
|
for (const channel of channels) {
|
|
@@ -182,16 +172,9 @@ function reconcileCategoricalLegend(program, target, channels) {
|
|
|
182
172
|
if (remaining.length === 0) {
|
|
183
173
|
return removeLegendKinds(program, [kind]);
|
|
184
174
|
}
|
|
175
|
+
const revision = resolveCategoricalLegendRevision(program, kind, config, remaining);
|
|
185
176
|
const next = removeLegendKinds(program, [kind]);
|
|
186
|
-
|
|
187
|
-
for (const property of LEGEND_OPTION_KEYS) {
|
|
188
|
-
if (
|
|
189
|
-
(property === "columns" && config[property] === undefined) ||
|
|
190
|
-
(property === "title" && config.inferredTitle)
|
|
191
|
-
) continue;
|
|
192
|
-
options[property] = config[property];
|
|
193
|
-
}
|
|
194
|
-
return next.createLegend(options);
|
|
177
|
+
return createCategoricalLegendFromConfig(next, revision.config, revision.order);
|
|
195
178
|
}
|
|
196
179
|
|
|
197
180
|
function cleanupLegends(program, target, channels) {
|
|
@@ -228,8 +211,30 @@ export const removeEncoding = action(
|
|
|
228
211
|
);
|
|
229
212
|
}
|
|
230
213
|
const layer = resolveTarget(this, args.target, args.channel);
|
|
214
|
+
if (args.channel === "group" && layer.layout?.mode !== undefined && layer.layout.mode !== "overlay") throw new Error("Remove active series layout before removing its group.");
|
|
215
|
+
if (args.channel === "theta" && layer.mark.type === "arc" &&
|
|
216
|
+
["count", "sum"].includes(layer.encoding?.radius?.aggregate)) {
|
|
217
|
+
throw new Error("Remove measured radius before removing its category theta encoding.");
|
|
218
|
+
}
|
|
231
219
|
const channels = activeCascade(layer, args.channel);
|
|
232
|
-
|
|
220
|
+
assertEncodingSelectionCompatibility(this, layer.id, channels);
|
|
221
|
+
for (const kind of ["series", "color"]) {
|
|
222
|
+
const config = this.guideConfigs.legend?.[kind];
|
|
223
|
+
const order = this.semanticSpec.guides.legend?.[kind]?.order;
|
|
224
|
+
if (config?.target === layer.id && channels.includes(order?.channel) &&
|
|
225
|
+
config.channels.some(channel => !channels.includes(channel))) {
|
|
226
|
+
throw new Error('Reset linked legend order to "scale" before removing its position encoding.');
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (["line", "area"].includes(layer.mark?.type)) {
|
|
230
|
+
const dataset = findDataset(this, layer.data);
|
|
231
|
+
if (args.channel === "group") {
|
|
232
|
+
assertPathGroupCompatible(this, layer, dataset, undefined);
|
|
233
|
+
}
|
|
234
|
+
const encoding = { ...layer.encoding };
|
|
235
|
+
for (const channel of channels) delete encoding[channel];
|
|
236
|
+
validatePathGroupAppearance(this, { ...layer, encoding }, dataset);
|
|
237
|
+
}
|
|
233
238
|
const removedPositions = channels
|
|
234
239
|
.filter(channel => POSITION_ENCODING_CHANNELS.includes(channel))
|
|
235
240
|
.map(channel => ({
|
|
@@ -249,7 +254,7 @@ export const removeEncoding = action(
|
|
|
249
254
|
next = restoreBarBaseline(next, layer, channels);
|
|
250
255
|
next = cleanupLegends(next, layer.id, channels);
|
|
251
256
|
next = cleanupPositionGuides(next, removedPositions);
|
|
252
|
-
return rematerializeTarget(next, layer.id);
|
|
257
|
+
return applyDetachedScaleRematerialization(rematerializeTarget(next, layer.id), [layer]);
|
|
253
258
|
}
|
|
254
259
|
);
|
|
255
260
|
|
|
@@ -7,6 +7,7 @@ import { readQuantitativeField } from "../../grammar/scales/index.js";
|
|
|
7
7
|
import { resolveStrokeWidthScaleDefinition } from "../scales/definitions.js";
|
|
8
8
|
import {
|
|
9
9
|
applyEncodingScale,
|
|
10
|
+
applyDetachedScaleRematerialization,
|
|
10
11
|
rematerializeEncoding,
|
|
11
12
|
resolveReassignmentScaleOptions,
|
|
12
13
|
resolveTarget,
|
|
@@ -14,6 +15,8 @@ import {
|
|
|
14
15
|
validateOptions
|
|
15
16
|
} from "./shared.js";
|
|
16
17
|
import { findLayer } from "../../selectors/layers.js";
|
|
18
|
+
import { validatePathSeriesAppearance } from "../../grammar/pathSeries.js";
|
|
19
|
+
import { assertEncodingSelectionCompatibility } from "../../materialization/selection/compatibility.js";
|
|
17
20
|
|
|
18
21
|
const STROKE_OPTIONS = Object.freeze(["target", "value"]);
|
|
19
22
|
const WIDTH_OPTIONS = Object.freeze([
|
|
@@ -40,7 +43,7 @@ const encodeStroke = action(
|
|
|
40
43
|
const encodeStrokeWidth = action(
|
|
41
44
|
{
|
|
42
45
|
op: "encodeStrokeWidth",
|
|
43
|
-
description: "
|
|
46
|
+
description: "Assign constant or field-driven line or rule stroke width."
|
|
44
47
|
},
|
|
45
48
|
function (args = {}) {
|
|
46
49
|
validateOptions(args, WIDTH_OPTIONS, "encodeStrokeWidth");
|
|
@@ -50,9 +53,14 @@ const encodeStrokeWidth = action(
|
|
|
50
53
|
throw new Error("encodeStrokeWidth requires exactly one of value or field.");
|
|
51
54
|
}
|
|
52
55
|
if (hasValue) {
|
|
53
|
-
|
|
56
|
+
if (args.fieldType !== undefined || args.scale !== undefined) {
|
|
57
|
+
throw new Error("Constant stroke width does not accept fieldType or scale.");
|
|
58
|
+
}
|
|
59
|
+
validateRuleStrokeWidth(args.value);
|
|
60
|
+
const { id, layer } = resolveTarget(this, args.target, ["rule", "line"], "rule or line mark");
|
|
61
|
+
assertEncodingSelectionCompatibility(this, id, ["strokeWidth"]);
|
|
54
62
|
let next = this.guideConfigs.legend?.strokeWidth?.target === id
|
|
55
|
-
? this.removeLegend({ target: id })
|
|
63
|
+
? this.removeLegend({ target: id, channels: ["strokeWidth"] })
|
|
56
64
|
: this;
|
|
57
65
|
if (findLayer(next, id)?.encoding?.strokeWidth !== undefined) {
|
|
58
66
|
next = next.editSemantic({
|
|
@@ -60,12 +68,15 @@ const encodeStrokeWidth = action(
|
|
|
60
68
|
remove: true
|
|
61
69
|
});
|
|
62
70
|
}
|
|
63
|
-
|
|
64
|
-
._withMarkConfig(id, {
|
|
71
|
+
next = next._withMarkConfig(id, {
|
|
65
72
|
...next.markConfigs[id],
|
|
66
|
-
strokeWidth:
|
|
67
|
-
})
|
|
68
|
-
|
|
73
|
+
strokeWidth: args.value
|
|
74
|
+
});
|
|
75
|
+
if (layer.mark.type === "line") {
|
|
76
|
+
return rematerializeEncoding(next, id, "strokeWidth", undefined, layer);
|
|
77
|
+
}
|
|
78
|
+
next = next.rematerializeRuleMark({ id });
|
|
79
|
+
return applyDetachedScaleRematerialization(next, [layer]);
|
|
69
80
|
}
|
|
70
81
|
const { id, dataset, layer } = resolveTarget(
|
|
71
82
|
this,
|
|
@@ -83,6 +94,9 @@ const encodeStrokeWidth = action(
|
|
|
83
94
|
`encodeStrokeWidth field "${args.field}" cannot contain negative values.`
|
|
84
95
|
);
|
|
85
96
|
}
|
|
97
|
+
validatePathSeriesAppearance(dataset.values, {
|
|
98
|
+
...layer, encoding: { ...layer.encoding, strokeWidth: { field: args.field, fieldType } }
|
|
99
|
+
});
|
|
86
100
|
const previous = layer.encoding?.strokeWidth;
|
|
87
101
|
const requestedScale = resolveReassignmentScaleOptions(
|
|
88
102
|
previous,
|
|
@@ -100,7 +114,7 @@ const encodeStrokeWidth = action(
|
|
|
100
114
|
next = applyEncodingScale(next, scale, requestedScale, {
|
|
101
115
|
reassignment: previous?.scale === scale.id
|
|
102
116
|
});
|
|
103
|
-
return rematerializeEncoding(next, id, "strokeWidth", scale.id);
|
|
117
|
+
return rematerializeEncoding(next, id, "strokeWidth", scale.id, layer);
|
|
104
118
|
}
|
|
105
119
|
);
|
|
106
120
|
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import { validateColorLayout } from "../../grammar/seriesLayout.js";
|
|
3
|
+
import { deriveAreaSeries, deriveDensityAreaSeries, layoutDensityAreaSeries } from "../../grammar/areaSeries.js";
|
|
4
|
+
import { readSeriesIdentity } from "../../grammar/pathSeries.js";
|
|
5
|
+
import { BAR_GRAINS, resolveBarChannels, resolveBarGrain } from "../../grammar/bars/policy.js";
|
|
6
|
+
import { deriveBarAggregates } from "../../grammar/bars/aggregate.js";
|
|
7
|
+
import { findSemanticScale, hasSemanticScaleReferences } from "../../selectors/scales.js";
|
|
8
|
+
import { findUpstreamTransform } from "../../materialization/dataProvenance.js";
|
|
9
|
+
import { resolveOffsetScaleDefinition } from "../scales/definitions.js";
|
|
10
|
+
import { resolveScalePreview } from "../scales/preview.js";
|
|
11
|
+
import { getPositionEncodingMaterializationSteps } from "../../materialization/marks/index.js";
|
|
12
|
+
import { resolveTarget, setEncodingProperties, validateOptions } from "./shared.js";
|
|
13
|
+
|
|
14
|
+
export function stackLayoutMode(stack) {
|
|
15
|
+
return stack === "normalize" ? "fill" : stack === "center" ? "center" : stack === null ? "overlay" : "stack";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const layoutSeries = action({ op: "layoutSeries", description: "Assign series placement independently of color." }, function (args = {}) {
|
|
19
|
+
validateOptions(args, ["target", "mode"], "layoutSeries");
|
|
20
|
+
const mode = validateColorLayout(args.mode);
|
|
21
|
+
const { id: target, layer, dataset } = resolveTarget(this, args.target, ["bar", "area"], "series mark");
|
|
22
|
+
const encoding = Object.fromEntries(Object.entries(layer.encoding ?? {}).map(([key, value]) => [key, { ...value }]));
|
|
23
|
+
const group = encoding.group;
|
|
24
|
+
const color = encoding.color;
|
|
25
|
+
if (group === undefined && color?.field !== undefined && ["nominal", "ordinal"].includes(color.fieldType)) {
|
|
26
|
+
encoding.group = { field: color.field, fieldType: "nominal", inferredFrom: "color" };
|
|
27
|
+
}
|
|
28
|
+
for (const channel of ["x", "y"]) if (encoding[channel]) delete encoding[channel].stack;
|
|
29
|
+
if (color) delete color.layout;
|
|
30
|
+
const updated = { ...layer, layout: { mode }, encoding };
|
|
31
|
+
const density = findUpstreamTransform(this, dataset, "density");
|
|
32
|
+
const horizon = findUpstreamTransform(this, dataset, "horizon");
|
|
33
|
+
let endpoint, offset, offsetScale, offsetChannel;
|
|
34
|
+
if (layer.mark.type === "area") {
|
|
35
|
+
if (horizon !== undefined) throw new Error("Horizon owns its layout; layoutSeries is not supported.");
|
|
36
|
+
if (mode === "group") throw new Error("Area does not support group layout.");
|
|
37
|
+
if (density !== undefined) layoutDensityAreaSeries(deriveDensityAreaSeries(dataset.values, updated, density), mode);
|
|
38
|
+
else {
|
|
39
|
+
if (mode !== "overlay" && encoding.x2 === undefined && encoding.y2 === undefined && encoding.y?.fieldType === "quantitative") {
|
|
40
|
+
endpoint = { channel: "y2", datum: 0, fieldType: "quantitative", scale: encoding.y.scale };
|
|
41
|
+
encoding.y2 = { datum: 0, fieldType: "quantitative", scale: encoding.y.scale };
|
|
42
|
+
}
|
|
43
|
+
if (encoding.x?.scale === undefined || encoding.y?.scale === undefined) throw new Error("Area layout requires complete positions.");
|
|
44
|
+
deriveAreaSeries(dataset.values, updated);
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
const channels = resolveBarChannels(updated), grain = resolveBarGrain(updated);
|
|
48
|
+
if (grain === undefined || encoding.x?.scale === undefined || encoding.y?.scale === undefined) throw new Error("Series layout requires a complete bar.");
|
|
49
|
+
if (mode === "center") throw new Error("Centered bars are not supported.");
|
|
50
|
+
if (grain === BAR_GRAINS.ranged && mode !== "overlay") throw new Error("Ranged bars support only overlay layout.");
|
|
51
|
+
if (grain === BAR_GRAINS.aggregate) deriveBarAggregates(dataset.values, updated);
|
|
52
|
+
if (mode === "group" && grain === BAR_GRAINS.aggregate) {
|
|
53
|
+
if (encoding.group === undefined) throw new Error("Grouped bars require a group encoding.");
|
|
54
|
+
readSeriesIdentity(dataset.values, updated);
|
|
55
|
+
offsetChannel = `${channels.category}Offset`;
|
|
56
|
+
offsetScale = resolveOffsetScaleDefinition(this, { id: encoding[offsetChannel]?.scale ?? offsetChannel }, offsetChannel);
|
|
57
|
+
offset = { ...(encoding.group.field === undefined ? {} : { field: encoding.group.field }), fieldType: encoding[offsetChannel]?.fieldType ?? "nominal", scale: offsetScale.id };
|
|
58
|
+
encoding[offsetChannel] = offset;
|
|
59
|
+
} else for (const channel of ["xOffset", "yOffset"]) delete encoding[channel];
|
|
60
|
+
}
|
|
61
|
+
const scales = offsetScale === undefined ? this.semanticSpec.scales
|
|
62
|
+
: [...this.semanticSpec.scales.filter(scale => scale.id !== offsetScale.id), offsetScale];
|
|
63
|
+
const preview = { ...this, markConfigs: this.markConfigs, canvasConfig: this.canvasConfig,
|
|
64
|
+
resolvedScales: { ...this.resolvedScales },
|
|
65
|
+
semanticSpec: { ...this.semanticSpec, scales,
|
|
66
|
+
layers: this.semanticSpec.layers.map(item => item.id === target ? updated : item) } };
|
|
67
|
+
for (const id of new Set([encoding.x?.scale, encoding.y?.scale, offset?.scale].filter(Boolean))) {
|
|
68
|
+
preview.resolvedScales[id] = resolveScalePreview(preview, id).resolvedScale;
|
|
69
|
+
}
|
|
70
|
+
let next = this.editSemantic({ property: `layer[${target}].layout.mode`, value: mode });
|
|
71
|
+
if (group === undefined && encoding.group !== undefined) next = setEncodingProperties(next, target, "group", encoding.group);
|
|
72
|
+
for (const channel of ["x", "y"]) if (Object.hasOwn(layer.encoding?.[channel] ?? {}, "stack")) {
|
|
73
|
+
next = next.editSemantic({ property: `layer[${target}].encoding.${channel}.stack`, remove: true });
|
|
74
|
+
}
|
|
75
|
+
if (color && Object.hasOwn(layer.encoding.color, "layout")) next = next.editSemantic({ property: `layer[${target}].encoding.color.layout`, remove: true });
|
|
76
|
+
if (endpoint !== undefined) {
|
|
77
|
+
next = next[endpoint.channel === "y2" ? "encodeY2" : "encodeX2"]({ target, datum: 0 });
|
|
78
|
+
}
|
|
79
|
+
if (layer.mark.type === "bar") {
|
|
80
|
+
for (const channel of ["xOffset", "yOffset"]) {
|
|
81
|
+
if (layer.encoding?.[channel] !== undefined) next = next.editSemantic({ property: `layer[${target}].encoding.${channel}`, remove: true });
|
|
82
|
+
if (channel !== offsetChannel) next = next._withoutMaterializationConfig(["marks", target, channel]);
|
|
83
|
+
}
|
|
84
|
+
if (offset !== undefined) {
|
|
85
|
+
next = offset.field === undefined
|
|
86
|
+
? setEncodingProperties(next, target, offsetChannel, offset).createScale(offsetScale)
|
|
87
|
+
: next[offsetChannel === "xOffset" ? "encodeXOffset" : "encodeYOffset"]({ target, field: offset.field, fieldType: offset.fieldType, scale: offsetScale });
|
|
88
|
+
if (findSemanticScale(this, offsetScale.id) === undefined) next = next._withMarkConfig(target, { ...next.markConfigs[target], seriesOffsetScale: offsetScale.id });
|
|
89
|
+
} else {
|
|
90
|
+
const owned = next.markConfigs[target]?.seriesOffsetScale;
|
|
91
|
+
if (owned !== undefined) {
|
|
92
|
+
const referenced = hasSemanticScaleReferences(next.semanticSpec, owned);
|
|
93
|
+
if (!referenced) next = next.editSemantic({ property: `scale[${owned}]`, remove: true });
|
|
94
|
+
next = next._withoutMaterializationConfig(["marks", target, "seriesOffsetScale"]);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
for (const step of getPositionEncodingMaterializationSteps(next, updated, encoding.y.scale)) next = next[step.op](step.args);
|
|
99
|
+
return next;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
export function registerSeriesLayoutAction(ProgramClass) {
|
|
103
|
+
ProgramClass.prototype.layoutSeries = layoutSeries;
|
|
104
|
+
}
|