ggaction 0.0.11 → 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 +80 -0
- package/README.md +3 -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 +33 -27
- package/src/actions/encodings/parallel.js +2 -8
- package/src/actions/encodings/position/apply.js +40 -4
- package/src/actions/encodings/position/index.js +43 -5
- package/src/actions/encodings/position/policies/arc.js +37 -2
- 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 +79 -24
- 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 +153 -11
- package/src/actions/errorBars/edit.js +365 -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 +138 -14
- 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 +60 -4
- 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/core/vocabulary.js +2 -2
- package/src/grammar/aggregate.js +66 -23
- package/src/grammar/arcs.js +213 -65
- 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/positionCompatibility.js +1 -1
- 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 +9 -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/ordinal.js +23 -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 +86 -23
- 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/facetGuides/legacyCategorical.js +7 -1
- package/src/materialization/facetGuides/placement.js +16 -1
- package/src/materialization/facetGuides/preparation.js +26 -1
- package/src/materialization/facets.js +11 -2
- 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 +30 -7
- package/src/materialization/marks/grouping.js +52 -0
- package/src/materialization/marks/index.js +63 -25
- package/src/materialization/marks/policies.js +2 -0
- package/src/materialization/rect.js +33 -33
- package/src/materialization/rowEncoding.js +70 -14
- 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/bar.js +0 -43
- package/src/materialization/scales/policies/binnedPosition.js +7 -4
- package/src/materialization/scales/policies/offset.js +54 -0
- package/src/materialization/scales/policies/series.js +2 -1
- package/src/materialization/scales/resolve.js +48 -18
- 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 +1327 -124
- package/src/materialization/bars/grouped.js +0 -144
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withGuideLayoutValidation } from "../../materialization/guides/layout.js";
|
|
1
2
|
import { action } from "../../core/action.js";
|
|
2
3
|
import { noOptions } from "../../core/validation.js";
|
|
3
4
|
import {
|
|
@@ -92,7 +93,7 @@ function titleTextAction(kind, create) {
|
|
|
92
93
|
return action({
|
|
93
94
|
op,
|
|
94
95
|
description: `${create ? "Create" : "Rematerialize"} chart ${kind} text.`
|
|
95
|
-
}, function (args = {}) {
|
|
96
|
+
}, withGuideLayoutValidation(function (args = {}) {
|
|
96
97
|
noOptions(args, op);
|
|
97
98
|
const config = requireTitleConfig(this);
|
|
98
99
|
if (create && this.graphicSpec.objects[id] !== undefined) {
|
|
@@ -111,7 +112,7 @@ function titleTextAction(kind, create) {
|
|
|
111
112
|
...(component.lines.length > 1 ? { length: component.lines.length } : {}),
|
|
112
113
|
...resolveCanvasGraphicPlacement(this)
|
|
113
114
|
})[`edit${suffix}`]();
|
|
114
|
-
});
|
|
115
|
+
}));
|
|
115
116
|
}
|
|
116
117
|
|
|
117
118
|
export const editTitleText = titleTextAction("title", false);
|
|
@@ -121,7 +122,7 @@ export const createSubtitleText = titleTextAction("subtitle", true);
|
|
|
121
122
|
|
|
122
123
|
export const rematerializeTitle = action(
|
|
123
124
|
{ op: "rematerializeTitle", description: "Rematerialize chart title graphics." },
|
|
124
|
-
function (args = {}) {
|
|
125
|
+
withGuideLayoutValidation(function (args = {}) {
|
|
125
126
|
noOptions(args, "rematerializeTitle");
|
|
126
127
|
requireTitleConfig(this);
|
|
127
128
|
if (this.graphicSpec.objects.chartTitle?.type !== "text") {
|
|
@@ -138,7 +139,7 @@ export const rematerializeTitle = action(
|
|
|
138
139
|
next = next.editGraphics({ target: "chartSubtitle", remove: true });
|
|
139
140
|
}
|
|
140
141
|
return next;
|
|
141
|
-
}
|
|
142
|
+
})
|
|
142
143
|
);
|
|
143
144
|
|
|
144
145
|
export const createTitle = action(
|
|
@@ -147,7 +148,7 @@ export const createTitle = action(
|
|
|
147
148
|
description: "Create a chart title and optional subtitle.",
|
|
148
149
|
scope: "any"
|
|
149
150
|
},
|
|
150
|
-
function (args = {}) {
|
|
151
|
+
withGuideLayoutValidation(function (args = {}) {
|
|
151
152
|
assertTitleScope(this, "createTitle");
|
|
152
153
|
const options = normalizeTitleOptions(args);
|
|
153
154
|
if (Object.keys(this.semanticSpec.title).length > 0) {
|
|
@@ -172,7 +173,7 @@ export const createTitle = action(
|
|
|
172
173
|
next = next.createTitleText();
|
|
173
174
|
if (subtitle !== undefined) next = next.createSubtitleText();
|
|
174
175
|
return next;
|
|
175
|
-
}
|
|
176
|
+
})
|
|
176
177
|
);
|
|
177
178
|
|
|
178
179
|
export const editTitle = action(
|
|
@@ -181,7 +182,7 @@ export const editTitle = action(
|
|
|
181
182
|
description: "Edit one stable chart title resource.",
|
|
182
183
|
scope: "any"
|
|
183
184
|
},
|
|
184
|
-
function (args = {}) {
|
|
185
|
+
withGuideLayoutValidation(function (args = {}) {
|
|
185
186
|
assertTitleScope(this, "editTitle");
|
|
186
187
|
if (this.semanticSpec.title.text === undefined) {
|
|
187
188
|
throw new Error("editTitle requires an existing chart title.");
|
|
@@ -210,7 +211,7 @@ export const editTitle = action(
|
|
|
210
211
|
}
|
|
211
212
|
resolveTitleLayout(next, normalized.config);
|
|
212
213
|
return next.rematerializeTitle();
|
|
213
|
-
}
|
|
214
|
+
})
|
|
214
215
|
);
|
|
215
216
|
|
|
216
217
|
export const removeTitle = action(
|
|
@@ -219,7 +220,7 @@ export const removeTitle = action(
|
|
|
219
220
|
description: "Remove the complete chart title resource.",
|
|
220
221
|
scope: "any"
|
|
221
222
|
},
|
|
222
|
-
function (args = {}) {
|
|
223
|
+
withGuideLayoutValidation(function (args = {}) {
|
|
223
224
|
assertTitleScope(this, "removeTitle");
|
|
224
225
|
noOptions(args, "removeTitle");
|
|
225
226
|
if (
|
|
@@ -246,5 +247,5 @@ export const removeTitle = action(
|
|
|
246
247
|
return next.compositionSpec?.type === "facet"
|
|
247
248
|
? next.materializeComposition()
|
|
248
249
|
: next;
|
|
249
|
-
}
|
|
250
|
+
})
|
|
250
251
|
);
|
|
@@ -7,12 +7,10 @@ import {
|
|
|
7
7
|
validatePositiveFinite
|
|
8
8
|
} from "../../core/validation.js";
|
|
9
9
|
import { resolveGraphicBounds } from "../../layout/canvas.js";
|
|
10
|
-
import { resolveConcreteGraphicBounds } from "../../grammar/schemas/graphicBounds.js";
|
|
11
10
|
import {
|
|
12
11
|
alignedTextAnchor,
|
|
13
12
|
alignedTitleAnchor,
|
|
14
13
|
buildTitleReadingBlock,
|
|
15
|
-
layoutBoundsIntersect,
|
|
16
14
|
resolveTitleComponentBounds,
|
|
17
15
|
unionTitleBounds
|
|
18
16
|
} from "../../layout/title.js";
|
|
@@ -176,35 +174,7 @@ export function requireTitleConfig(program) {
|
|
|
176
174
|
return program.titleConfig;
|
|
177
175
|
}
|
|
178
176
|
|
|
179
|
-
function
|
|
180
|
-
const ids = [];
|
|
181
|
-
const axisChannel = ["top", "bottom"].includes(position) ? "x" : "y";
|
|
182
|
-
const axis = program.guideConfigs.axis?.[axisChannel];
|
|
183
|
-
if (axis && Object.values(axis).some(config => config?.position === position)) {
|
|
184
|
-
const prefix = axisChannel === "x" ? "xAxis" : "yAxis";
|
|
185
|
-
ids.push(`${prefix}Line`, `${prefix}Ticks`, `${prefix}Labels`, `${prefix}Title`);
|
|
186
|
-
}
|
|
187
|
-
const prefixes = {
|
|
188
|
-
series: "seriesLegend",
|
|
189
|
-
color: "colorLegend",
|
|
190
|
-
gradient: "colorGradient",
|
|
191
|
-
opacity: "opacityLegend"
|
|
192
|
-
};
|
|
193
|
-
for (const [kind, config] of Object.entries(program.guideConfigs.legend ?? {})) {
|
|
194
|
-
if (config?.position !== position || prefixes[kind] === undefined) continue;
|
|
195
|
-
ids.push(...Object.keys(program.graphicSpec.objects).filter(
|
|
196
|
-
id => id.startsWith(prefixes[kind])
|
|
197
|
-
));
|
|
198
|
-
if (kind === "series" && program.guideConfigs.legend?.size !== undefined) {
|
|
199
|
-
ids.push(...Object.keys(program.graphicSpec.objects).filter(
|
|
200
|
-
id => id.startsWith("sizeLegend")
|
|
201
|
-
));
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
return [...new Set(ids)];
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function validateLayout(program, position, titleBounds, plot, canvas) {
|
|
177
|
+
function validateLayout(position, titleBounds, plot, canvas) {
|
|
208
178
|
if (
|
|
209
179
|
titleBounds.left < 0 || titleBounds.top < 0 ||
|
|
210
180
|
titleBounds.right > canvas.width || titleBounds.bottom > canvas.height
|
|
@@ -220,14 +190,6 @@ function validateLayout(program, position, titleBounds, plot, canvas) {
|
|
|
220
190
|
if (!outsidePlot) {
|
|
221
191
|
throw new Error(`Chart title requires more ${position}-margin space.`);
|
|
222
192
|
}
|
|
223
|
-
for (const id of reservedGraphicIds(program, position)) {
|
|
224
|
-
const bounds = program.graphicSpec.objects[id] === undefined
|
|
225
|
-
? undefined
|
|
226
|
-
: resolveConcreteGraphicBounds(program.graphicSpec, id);
|
|
227
|
-
if (bounds !== undefined && layoutBoundsIntersect(titleBounds, bounds)) {
|
|
228
|
-
throw new Error(`Chart title and ${position} guides require more margin space.`);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
193
|
}
|
|
232
194
|
|
|
233
195
|
export function resolveTitleLayout(program, config) {
|
|
@@ -293,7 +255,6 @@ export function resolveTitleLayout(program, config) {
|
|
|
293
255
|
: [resolveTitleComponentBounds(subtitle, config.subtitleStyle)])
|
|
294
256
|
]);
|
|
295
257
|
validateLayout(
|
|
296
|
-
program,
|
|
297
258
|
config.position,
|
|
298
259
|
titleBounds,
|
|
299
260
|
plot,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { action } from "../../core/action.js";
|
|
2
2
|
import { validateOptionObject } from "../../core/validation.js";
|
|
3
3
|
import { findDataset } from "../../selectors/datasets.js";
|
|
4
|
+
import { findLayer } from "../../selectors/layers.js";
|
|
4
5
|
import {
|
|
5
6
|
applyFacadeGuides,
|
|
6
7
|
normalizeAppearance,
|
|
@@ -19,7 +20,7 @@ const OPTIONS = Object.freeze([
|
|
|
19
20
|
]);
|
|
20
21
|
const POSITION_OPTIONS = Object.freeze(["field", "fieldType", "scale"]);
|
|
21
22
|
const DENSITY_OPTIONS = Object.freeze([
|
|
22
|
-
"bandwidth", "extent", "steps", "kernel", "normalization", "width"
|
|
23
|
+
"bandwidth", "extent", "steps", "kernel", "normalization", "width", "side"
|
|
23
24
|
]);
|
|
24
25
|
const WIDTH_OPTIONS = Object.freeze(["band", "resolve"]);
|
|
25
26
|
const SPLIT_OPTIONS = Object.freeze(["field", "domain"]);
|
|
@@ -28,7 +29,6 @@ const AREA_OPTIONS = Object.freeze([
|
|
|
28
29
|
]);
|
|
29
30
|
const CATEGORICAL_TYPES = Object.freeze(["nominal", "ordinal"]);
|
|
30
31
|
const OPERATION = "createViolinPlot";
|
|
31
|
-
const ROLE_ERROR = `${OPERATION} requires one categorical axis and one quantitative axis.`;
|
|
32
32
|
|
|
33
33
|
function inferFieldType(dataset, encoding, label) {
|
|
34
34
|
if (encoding.fieldType !== undefined) return encoding.fieldType;
|
|
@@ -44,7 +44,7 @@ function inferFieldType(dataset, encoding, label) {
|
|
|
44
44
|
return values.every(Number.isFinite) ? "quantitative" : "nominal";
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
function
|
|
47
|
+
export function normalizeViolinPosition(dataset, value, label) {
|
|
48
48
|
const encoding = normalizeFieldEncoding(value, label);
|
|
49
49
|
validateOptionObject(encoding, POSITION_OPTIONS, label);
|
|
50
50
|
return {
|
|
@@ -53,6 +53,54 @@ function normalizePosition(dataset, value, label) {
|
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
export function resolveViolinRoles(dataset, xValue, yValue, operation) {
|
|
57
|
+
const x = normalizeViolinPosition(dataset, xValue, `${operation} x`);
|
|
58
|
+
const y = normalizeViolinPosition(dataset, yValue, `${operation} y`);
|
|
59
|
+
const xCategorical = CATEGORICAL_TYPES.includes(x.fieldType);
|
|
60
|
+
const yCategorical = CATEGORICAL_TYPES.includes(y.fieldType);
|
|
61
|
+
const roleError = `${operation} requires one categorical axis and one quantitative axis.`;
|
|
62
|
+
if (xCategorical === yCategorical || (
|
|
63
|
+
x.fieldType !== "quantitative" && y.fieldType !== "quantitative"
|
|
64
|
+
)) {
|
|
65
|
+
throw new Error(roleError);
|
|
66
|
+
}
|
|
67
|
+
const category = xCategorical ? x : y;
|
|
68
|
+
const value = xCategorical ? y : x;
|
|
69
|
+
if (value.fieldType !== "quantitative") throw new Error(roleError);
|
|
70
|
+
return {
|
|
71
|
+
x,
|
|
72
|
+
y,
|
|
73
|
+
category,
|
|
74
|
+
value,
|
|
75
|
+
orientation: xCategorical ? "vertical" : "horizontal",
|
|
76
|
+
densityChannel: xCategorical ? "x" : "y"
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function resolveViolinSplit(value, category, operation, { removable = false } = {}) {
|
|
81
|
+
if (removable && value === false) return undefined;
|
|
82
|
+
if (value === undefined) return undefined;
|
|
83
|
+
validateOptionObject(value, SPLIT_OPTIONS, `${operation} split`);
|
|
84
|
+
if (typeof value.field !== "string" || value.field.length === 0) {
|
|
85
|
+
throw new TypeError(`${operation} split.field must be a non-empty string.`);
|
|
86
|
+
}
|
|
87
|
+
const split = { ...value };
|
|
88
|
+
if (split.field === category.field) {
|
|
89
|
+
throw new Error(`${operation} split field must differ from its category field.`);
|
|
90
|
+
}
|
|
91
|
+
return split;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function resolveViolinDensity(value, current = {}, operation = OPERATION) {
|
|
95
|
+
if (value !== undefined) {
|
|
96
|
+
validateOptionObject(value, DENSITY_OPTIONS, `${operation} density`);
|
|
97
|
+
if (value.width !== undefined) {
|
|
98
|
+
validateOptionObject(value.width, WIDTH_OPTIONS, `${operation} density.width`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return { ...current, ...(value ?? {}) };
|
|
102
|
+
}
|
|
103
|
+
|
|
56
104
|
function axisGuideOptions(value, field) {
|
|
57
105
|
if (value === false) return false;
|
|
58
106
|
return {
|
|
@@ -95,31 +143,9 @@ export const createViolinPlot = action(
|
|
|
95
143
|
});
|
|
96
144
|
const data = resolveFacadeData(this, args.data, OPERATION);
|
|
97
145
|
const dataset = findDataset(this, data);
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
const yCategorical = CATEGORICAL_TYPES.includes(y.fieldType);
|
|
102
|
-
if (xCategorical === yCategorical || (
|
|
103
|
-
x.fieldType !== "quantitative" && y.fieldType !== "quantitative"
|
|
104
|
-
)) {
|
|
105
|
-
throw new Error(ROLE_ERROR);
|
|
106
|
-
}
|
|
107
|
-
const category = xCategorical ? x : y;
|
|
108
|
-
const value = xCategorical ? y : x;
|
|
109
|
-
if (value.fieldType !== "quantitative") {
|
|
110
|
-
throw new Error(ROLE_ERROR);
|
|
111
|
-
}
|
|
112
|
-
let split = args.split;
|
|
113
|
-
if (split !== undefined) {
|
|
114
|
-
validateOptionObject(split, SPLIT_OPTIONS, `${OPERATION} split`);
|
|
115
|
-
if (typeof split.field !== "string" || split.field.length === 0) {
|
|
116
|
-
throw new TypeError(`${OPERATION} split.field must be a non-empty string.`);
|
|
117
|
-
}
|
|
118
|
-
split = { ...split };
|
|
119
|
-
}
|
|
120
|
-
if (split?.field === category.field) {
|
|
121
|
-
throw new Error(`${OPERATION} split field must differ from its category field.`);
|
|
122
|
-
}
|
|
146
|
+
const roles = resolveViolinRoles(dataset, args.x, args.y, OPERATION);
|
|
147
|
+
const { x, y, category, value } = roles;
|
|
148
|
+
const split = resolveViolinSplit(args.split, category, OPERATION);
|
|
123
149
|
const color = normalizeEncoding(args.color, `${OPERATION} color`);
|
|
124
150
|
if (
|
|
125
151
|
color !== undefined &&
|
|
@@ -129,18 +155,8 @@ export const createViolinPlot = action(
|
|
|
129
155
|
`${OPERATION} color must encode its category or split field.`
|
|
130
156
|
);
|
|
131
157
|
}
|
|
132
|
-
const density = args.density
|
|
133
|
-
|
|
134
|
-
validateOptionObject(density, DENSITY_OPTIONS, `${OPERATION} density`);
|
|
135
|
-
if (density.width !== undefined) {
|
|
136
|
-
validateOptionObject(
|
|
137
|
-
density.width,
|
|
138
|
-
WIDTH_OPTIONS,
|
|
139
|
-
`${OPERATION} density.width`
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
const { width: densityWidth, ...densityOptions } = density;
|
|
158
|
+
const density = resolveViolinDensity(args.density);
|
|
159
|
+
const { width: densityWidth, side: densitySide, ...densityOptions } = density;
|
|
144
160
|
const area = normalizeAppearance(
|
|
145
161
|
args.area,
|
|
146
162
|
AREA_OPTIONS,
|
|
@@ -174,18 +190,46 @@ export const createViolinPlot = action(
|
|
|
174
190
|
source: data,
|
|
175
191
|
field: value.field,
|
|
176
192
|
groupBy: category.field,
|
|
177
|
-
densityChannel:
|
|
193
|
+
densityChannel: roles.densityChannel,
|
|
178
194
|
...(args.coordinate === undefined ? {} : { coordinate: args.coordinate }),
|
|
179
195
|
...(value.scale === undefined ? {} : { valueScale: value.scale }),
|
|
180
196
|
...densityOptions,
|
|
181
197
|
placement: {
|
|
182
198
|
type: "category",
|
|
199
|
+
...(densitySide === undefined ? {} : { side: densitySide }),
|
|
183
200
|
...(densityWidth === undefined ? {} : { width: densityWidth }),
|
|
184
201
|
...(split === undefined ? {} : { split }),
|
|
185
202
|
...(category.scale === undefined ? {} : { scale: category.scale })
|
|
186
203
|
}
|
|
187
204
|
});
|
|
188
205
|
if (color !== undefined) next = next.encodeColor(targetArgs(color, id));
|
|
189
|
-
|
|
206
|
+
next = applyFacadeGuides(next, guides, id, args.guides ?? {});
|
|
207
|
+
const transform = findDataset(next, findLayer(next, id).data).transform[0];
|
|
208
|
+
return next._withMarkConfig(id, {
|
|
209
|
+
...next.markConfigs[id],
|
|
210
|
+
violinPlot: {
|
|
211
|
+
materialized: true,
|
|
212
|
+
source: data,
|
|
213
|
+
orientation: roles.orientation,
|
|
214
|
+
category: category.field,
|
|
215
|
+
categoryType: category.fieldType,
|
|
216
|
+
value: value.field,
|
|
217
|
+
split: transform.placement.split,
|
|
218
|
+
density: {
|
|
219
|
+
bandwidth: transform.bandwidth,
|
|
220
|
+
extent: transform.extent,
|
|
221
|
+
steps: transform.steps,
|
|
222
|
+
kernel: transform.kernel,
|
|
223
|
+
normalization: transform.normalization,
|
|
224
|
+
width: transform.placement.width,
|
|
225
|
+
side: transform.placement.side
|
|
226
|
+
},
|
|
227
|
+
colorRole: color === undefined
|
|
228
|
+
? undefined
|
|
229
|
+
: color.field === category.field
|
|
230
|
+
? "category"
|
|
231
|
+
: color.field === split?.field ? "split" : undefined
|
|
232
|
+
}
|
|
233
|
+
});
|
|
190
234
|
}
|
|
191
235
|
);
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import { validateUserId } from "../../core/identifiers.js";
|
|
3
|
+
import { validateKeys } from "../../core/validation.js";
|
|
4
|
+
import { findDataset } from "../../selectors/datasets.js";
|
|
5
|
+
import { findLayer } from "../../selectors/layers.js";
|
|
6
|
+
import { rebindDistributionGuides } from "../distributions/revision.js";
|
|
7
|
+
import {
|
|
8
|
+
resolveViolinDensity,
|
|
9
|
+
resolveViolinRoles,
|
|
10
|
+
resolveViolinSplit
|
|
11
|
+
} from "./create.js";
|
|
12
|
+
|
|
13
|
+
const OPERATION = "editViolinPlot";
|
|
14
|
+
const OPTIONS = Object.freeze([
|
|
15
|
+
"target", "data", "x", "y", "split", "density"
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
function resolveOwner(program, requested) {
|
|
19
|
+
const eligible = program.semanticSpec.layers.filter(
|
|
20
|
+
layer => program.markConfigs[layer.id]?.violinPlot?.materialized === true
|
|
21
|
+
);
|
|
22
|
+
if (requested !== undefined) {
|
|
23
|
+
const id = validateUserId(requested, "Violin-plot owner id");
|
|
24
|
+
const layer = findLayer(program, id);
|
|
25
|
+
if (layer === undefined || !eligible.includes(layer)) {
|
|
26
|
+
throw new Error(`Unknown violin-plot owner "${id}".`);
|
|
27
|
+
}
|
|
28
|
+
return layer;
|
|
29
|
+
}
|
|
30
|
+
const current = findLayer(program, program.context.currentMark);
|
|
31
|
+
if (current !== undefined && eligible.includes(current)) return current;
|
|
32
|
+
if (eligible.length === 1) return eligible[0];
|
|
33
|
+
if (eligible.length === 0) throw new Error(`${OPERATION} requires a violin plot.`);
|
|
34
|
+
throw new Error(`${OPERATION} target is ambiguous; provide target.`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function currentPosition(owner, current, channel) {
|
|
38
|
+
const categorical = current.orientation === "vertical"
|
|
39
|
+
? channel === "x"
|
|
40
|
+
: channel === "y";
|
|
41
|
+
return {
|
|
42
|
+
field: categorical ? current.category : current.value,
|
|
43
|
+
fieldType: categorical ? current.categoryType : "quantitative",
|
|
44
|
+
scale: { id: owner.encoding[channel].scale }
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function requestedScale(args, channel, expectedId) {
|
|
49
|
+
if (!Object.hasOwn(args, channel) || typeof args[channel] !== "object") {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
const scale = args[channel].scale;
|
|
53
|
+
if (scale === undefined) return undefined;
|
|
54
|
+
if (scale.id !== undefined && scale.id !== expectedId) {
|
|
55
|
+
throw new Error(`${OPERATION} ${channel} scale cannot change its id.`);
|
|
56
|
+
}
|
|
57
|
+
const { id: _id, ...definition } = scale;
|
|
58
|
+
void _id;
|
|
59
|
+
return definition;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function assertSplitSelectionCompatibility(program, owner, changesSplit) {
|
|
63
|
+
if (!changesSplit) return;
|
|
64
|
+
const referenced = Object.entries(
|
|
65
|
+
program.materializationConfigs.selections ?? {}
|
|
66
|
+
).find(([, selection]) =>
|
|
67
|
+
selection.target === owner.id && selection.selector?.channel === "color"
|
|
68
|
+
);
|
|
69
|
+
if (referenced !== undefined) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`${OPERATION} cannot change split while selection "${referenced[0]}" ` +
|
|
72
|
+
"references color."
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const editViolinPlot = action(
|
|
78
|
+
{
|
|
79
|
+
op: OPERATION,
|
|
80
|
+
description: "Revise one violin plot's data and statistical roles."
|
|
81
|
+
},
|
|
82
|
+
function (args = {}) {
|
|
83
|
+
validateKeys(args, OPTIONS, OPERATION);
|
|
84
|
+
if (!OPTIONS.slice(1).some(key => Object.hasOwn(args, key))) {
|
|
85
|
+
throw new Error(`${OPERATION} requires at least one violin-plot option.`);
|
|
86
|
+
}
|
|
87
|
+
const owner = resolveOwner(this, args.target);
|
|
88
|
+
const current = this.markConfigs[owner.id].violinPlot;
|
|
89
|
+
const source = Object.hasOwn(args, "data")
|
|
90
|
+
? validateUserId(args.data, "Violin-plot data id")
|
|
91
|
+
: current.source;
|
|
92
|
+
const dataset = findDataset(this, source);
|
|
93
|
+
if (dataset === undefined) {
|
|
94
|
+
throw new Error(`Unknown violin-plot data "${source}".`);
|
|
95
|
+
}
|
|
96
|
+
const roles = resolveViolinRoles(
|
|
97
|
+
dataset,
|
|
98
|
+
Object.hasOwn(args, "x") ? args.x : currentPosition(owner, current, "x"),
|
|
99
|
+
Object.hasOwn(args, "y") ? args.y : currentPosition(owner, current, "y"),
|
|
100
|
+
OPERATION
|
|
101
|
+
);
|
|
102
|
+
const split = Object.hasOwn(args, "split")
|
|
103
|
+
? resolveViolinSplit(args.split, roles.category, OPERATION, {
|
|
104
|
+
removable: true
|
|
105
|
+
})
|
|
106
|
+
: current.split;
|
|
107
|
+
if (split?.field === roles.category.field) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`${OPERATION} split field must differ from its category field.`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
const density = resolveViolinDensity(args.density, current.density, OPERATION);
|
|
113
|
+
const changesSplit = split?.field !== current.split?.field;
|
|
114
|
+
assertSplitSelectionCompatibility(this, owner, changesSplit);
|
|
115
|
+
if (current.colorRole === "split" && split === undefined) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`${OPERATION} cannot remove split while color encodes the split field.`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const categoryChannel = roles.orientation === "vertical" ? "x" : "y";
|
|
122
|
+
const valueChannel = roles.orientation === "vertical" ? "y" : "x";
|
|
123
|
+
const categoryScale = requestedScale(
|
|
124
|
+
args, categoryChannel, owner.encoding[categoryChannel].scale
|
|
125
|
+
);
|
|
126
|
+
const valueScale = requestedScale(
|
|
127
|
+
args, valueChannel, owner.encoding[valueChannel].scale
|
|
128
|
+
);
|
|
129
|
+
const { width, side, ...densityOptions } = density;
|
|
130
|
+
let next = this;
|
|
131
|
+
if (current.colorRole === "split" && changesSplit) {
|
|
132
|
+
next = next.editSemantic({
|
|
133
|
+
property: `layer[${owner.id}].encoding.color.field`,
|
|
134
|
+
value: split.field
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
next = next.editDensity({
|
|
138
|
+
target: owner.id,
|
|
139
|
+
source,
|
|
140
|
+
field: roles.value.field,
|
|
141
|
+
groupBy: roles.category.field,
|
|
142
|
+
densityChannel: roles.densityChannel,
|
|
143
|
+
...densityOptions,
|
|
144
|
+
...(valueScale === undefined ? {} : { valueScale }),
|
|
145
|
+
placement: {
|
|
146
|
+
type: "category",
|
|
147
|
+
...(side === undefined ? {} : { side }),
|
|
148
|
+
width,
|
|
149
|
+
...(split === undefined ? {} : { split }),
|
|
150
|
+
...(categoryScale === undefined ? {} : { scale: categoryScale })
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
next = rebindDistributionGuides(next, {
|
|
154
|
+
oldXScale: owner.encoding.x.scale,
|
|
155
|
+
oldYScale: owner.encoding.y.scale,
|
|
156
|
+
newXScale: owner.encoding.x.scale,
|
|
157
|
+
newYScale: owner.encoding.y.scale,
|
|
158
|
+
oldXTitle: current.orientation === "vertical" ? current.category : current.value,
|
|
159
|
+
oldYTitle: current.orientation === "vertical" ? current.value : current.category,
|
|
160
|
+
newXTitle: roles.x.field,
|
|
161
|
+
newYTitle: roles.y.field,
|
|
162
|
+
oldMeasureChannel: current.orientation === "vertical" ? "y" : "x",
|
|
163
|
+
newMeasureChannel: valueChannel
|
|
164
|
+
});
|
|
165
|
+
const revisedLayer = findLayer(next, owner.id);
|
|
166
|
+
const transform = findDataset(next, revisedLayer.data).transform[0];
|
|
167
|
+
next = next._withMarkConfig(owner.id, {
|
|
168
|
+
...next.markConfigs[owner.id],
|
|
169
|
+
violinPlot: {
|
|
170
|
+
...current,
|
|
171
|
+
materialized: true,
|
|
172
|
+
source,
|
|
173
|
+
orientation: roles.orientation,
|
|
174
|
+
category: roles.category.field,
|
|
175
|
+
categoryType: roles.category.fieldType,
|
|
176
|
+
value: roles.value.field,
|
|
177
|
+
split: transform.placement.split,
|
|
178
|
+
density: {
|
|
179
|
+
bandwidth: transform.bandwidth,
|
|
180
|
+
extent: transform.extent,
|
|
181
|
+
steps: transform.steps,
|
|
182
|
+
kernel: transform.kernel,
|
|
183
|
+
normalization: transform.normalization,
|
|
184
|
+
width: transform.placement.width,
|
|
185
|
+
side: transform.placement.side
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
return next._withContext({ currentMark: owner.id, currentData: source });
|
|
190
|
+
}
|
|
191
|
+
);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createViolinPlot } from "./create.js";
|
|
2
|
+
import { editViolinPlot } from "./edit.js";
|
|
2
3
|
|
|
3
4
|
export function registerViolinPlotActions(ProgramClass) {
|
|
4
5
|
ProgramClass.prototype.createViolinPlot = createViolinPlot;
|
|
6
|
+
ProgramClass.prototype.editViolinPlot = editViolinPlot;
|
|
5
7
|
}
|
package/src/core/ChartProgram.js
CHANGED
|
@@ -173,6 +173,13 @@ export class ChartProgram {
|
|
|
173
173
|
});
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
_withoutResolvedScale(id) {
|
|
177
|
+
if (!Object.hasOwn(this.resolvedScales, id)) return this;
|
|
178
|
+
const { [id]: unused, ...remaining } = this.resolvedScales;
|
|
179
|
+
void unused;
|
|
180
|
+
return this._clone({ resolvedScales: freezeOwned(remaining) });
|
|
181
|
+
}
|
|
182
|
+
|
|
176
183
|
_withMarkConfig(id, config) {
|
|
177
184
|
if (typeof id !== "string" || id.length === 0) {
|
|
178
185
|
throw new TypeError("Mark config id must be a non-empty string.");
|
package/src/core/action.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { cloneAndFreeze, freezeOwned, isPlainObject } from "./immutable.js";
|
|
2
2
|
|
|
3
3
|
const metadataByWrappedAction = new WeakMap();
|
|
4
|
+
let actionCompletionHook;
|
|
5
|
+
|
|
6
|
+
export function setActionCompletionHook(hook) {
|
|
7
|
+
if (hook !== undefined && typeof hook !== "function") {
|
|
8
|
+
throw new TypeError("Action completion hook must be a function.");
|
|
9
|
+
}
|
|
10
|
+
actionCompletionHook = hook;
|
|
11
|
+
}
|
|
4
12
|
|
|
5
13
|
export function getWrappedActionMetadata(value) {
|
|
6
14
|
return metadataByWrappedAction.get(value);
|
|
@@ -127,16 +135,28 @@ export function action(metadata, implementation) {
|
|
|
127
135
|
if (scope === "unit") this._assertUnitProgram(ownedMetadata.op);
|
|
128
136
|
if (scope === "composition") this._assertCompositionProgram(ownedMetadata.op);
|
|
129
137
|
|
|
138
|
+
const summarizedArgs = summarizeArgs(args);
|
|
130
139
|
const entered = this._enterAction({
|
|
131
140
|
...ownedMetadata,
|
|
132
|
-
args:
|
|
141
|
+
args: summarizedArgs
|
|
133
142
|
});
|
|
134
|
-
|
|
143
|
+
let result = implementation.call(entered, args);
|
|
135
144
|
|
|
136
145
|
if (!(result instanceof this.constructor)) {
|
|
137
146
|
throw new TypeError(`${ownedMetadata.op} must return a ChartProgram.`);
|
|
138
147
|
}
|
|
139
148
|
|
|
149
|
+
if (this.actionStack.length === 0 && actionCompletionHook !== undefined) {
|
|
150
|
+
result = actionCompletionHook(result, {
|
|
151
|
+
source: this,
|
|
152
|
+
metadata: ownedMetadata,
|
|
153
|
+
args: summarizedArgs
|
|
154
|
+
});
|
|
155
|
+
if (!(result instanceof this.constructor)) {
|
|
156
|
+
throw new TypeError("Action completion hook must return a ChartProgram.");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
140
160
|
return result._exitAction();
|
|
141
161
|
};
|
|
142
162
|
|