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
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import { isPlainObject } from "../../core/immutable.js";
|
|
3
|
+
import { validateNonEmptyString, validateNonNegativeFinite, validateOptionObject } from
|
|
4
|
+
"../../core/validation.js";
|
|
5
|
+
import {
|
|
6
|
+
applyFacadeGuides,
|
|
7
|
+
normalizeAppearance,
|
|
8
|
+
normalizeEncoding,
|
|
9
|
+
normalizeFieldEncoding,
|
|
10
|
+
normalizeGuides,
|
|
11
|
+
omitUndefinedOptions,
|
|
12
|
+
positionArgs,
|
|
13
|
+
resolveFacadeData,
|
|
14
|
+
resolveFacadeId,
|
|
15
|
+
targetArgs,
|
|
16
|
+
inferFacadeFieldType,
|
|
17
|
+
validateFacadeOptions
|
|
18
|
+
} from "./shared.js";
|
|
19
|
+
|
|
20
|
+
const RUG_OPTIONS = Object.freeze(["id", "data", "coordinate", "x", "y", "edge", "tick", "guides"]);
|
|
21
|
+
const MEASURE_OPTIONS = Object.freeze(["field", "fieldType", "temporalUnit", "scale"]);
|
|
22
|
+
const TICK_OPTIONS = Object.freeze(["length", "stroke", "strokeWidth", "opacity"]);
|
|
23
|
+
const STRIP_OPTIONS = Object.freeze([
|
|
24
|
+
"id", "data", "coordinate", "x", "y", "color", "size", "shape", "point", "jitter", "guides"
|
|
25
|
+
]);
|
|
26
|
+
const POINT_OPTIONS = Object.freeze([
|
|
27
|
+
"radius", "shape", "fill", "opacity", "stroke", "strokeWidth"
|
|
28
|
+
]);
|
|
29
|
+
const JITTER_OPTIONS = Object.freeze(["maxOffset", "seed", "key"]);
|
|
30
|
+
|
|
31
|
+
function normalizeMeasure(value, operation, channel) {
|
|
32
|
+
const result = normalizeFieldEncoding(value, `${operation} ${channel}`);
|
|
33
|
+
validateOptionObject(result, MEASURE_OPTIONS, `${operation} ${channel}`);
|
|
34
|
+
validateNonEmptyString(result.field, `${operation} ${channel} field`);
|
|
35
|
+
const fieldType = result.fieldType ?? "quantitative";
|
|
36
|
+
if (!["quantitative", "temporal"].includes(fieldType)) {
|
|
37
|
+
throw new Error(`${operation} ${channel} must be quantitative or temporal.`);
|
|
38
|
+
}
|
|
39
|
+
return { ...result, fieldType };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizeRugGuides(value, measure, operation) {
|
|
43
|
+
const guides = normalizeGuides(value, operation);
|
|
44
|
+
if (guides === false) return false;
|
|
45
|
+
validateOptionObject(guides, ["axes", "grid", "legend"], `${operation} guides`);
|
|
46
|
+
const anchor = measure === "x" ? "y" : "x";
|
|
47
|
+
if (guides.legend !== undefined && guides.legend !== false) {
|
|
48
|
+
throw new Error(`${operation} does not have a legend encoding.`);
|
|
49
|
+
}
|
|
50
|
+
let axes = guides.axes;
|
|
51
|
+
if (axes !== undefined && axes !== false) {
|
|
52
|
+
if (!isPlainObject(axes)) {
|
|
53
|
+
throw new TypeError(`${operation} guides.axes must be false or a plain object.`);
|
|
54
|
+
}
|
|
55
|
+
if (axes[anchor] !== undefined && axes[anchor] !== false) {
|
|
56
|
+
throw new Error(`${operation} cannot create an axis for its constant anchor.`);
|
|
57
|
+
}
|
|
58
|
+
axes = { ...axes, [anchor]: false };
|
|
59
|
+
} else if (axes === undefined) {
|
|
60
|
+
axes = { [measure]: {}, [anchor]: false };
|
|
61
|
+
}
|
|
62
|
+
let grid = guides.grid;
|
|
63
|
+
const anchorGrid = measure === "x" ? "horizontal" : "vertical";
|
|
64
|
+
if (grid !== undefined && grid !== false) {
|
|
65
|
+
if (!isPlainObject(grid)) {
|
|
66
|
+
throw new TypeError(`${operation} guides.grid must be false or a plain object.`);
|
|
67
|
+
}
|
|
68
|
+
if (grid[anchorGrid] !== undefined && grid[anchorGrid] !== false) {
|
|
69
|
+
throw new Error(`${operation} cannot create a grid for its constant anchor.`);
|
|
70
|
+
}
|
|
71
|
+
grid = { ...grid, [anchorGrid]: false };
|
|
72
|
+
} else if (grid === undefined) {
|
|
73
|
+
grid = false;
|
|
74
|
+
}
|
|
75
|
+
return { axes, grid, legend: false };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const createRugPlot = action(
|
|
79
|
+
{
|
|
80
|
+
op: "createRugPlot",
|
|
81
|
+
description: "Create a one-dimensional Rug plot at an explicit plot edge."
|
|
82
|
+
},
|
|
83
|
+
function (args = {}) {
|
|
84
|
+
const operation = "createRugPlot";
|
|
85
|
+
validateFacadeOptions(args, RUG_OPTIONS, operation);
|
|
86
|
+
const hasX = args.x !== undefined;
|
|
87
|
+
const hasY = args.y !== undefined;
|
|
88
|
+
if (hasX === hasY) {
|
|
89
|
+
throw new Error(`${operation} requires exactly one of x or y.`);
|
|
90
|
+
}
|
|
91
|
+
const measure = hasX ? "x" : "y";
|
|
92
|
+
const edge = args.edge;
|
|
93
|
+
const supportedEdges = measure === "x" ? ["top", "bottom"] : ["left", "right"];
|
|
94
|
+
if (!supportedEdges.includes(edge)) {
|
|
95
|
+
throw new Error(`${operation} ${measure} requires edge ${supportedEdges.join(" or ")}.`);
|
|
96
|
+
}
|
|
97
|
+
const id = resolveFacadeId(this, args.id, {
|
|
98
|
+
defaultId: "rugPlot",
|
|
99
|
+
operation
|
|
100
|
+
});
|
|
101
|
+
const data = resolveFacadeData(this, args.data, operation);
|
|
102
|
+
const encoding = normalizeMeasure(args[measure], operation, measure);
|
|
103
|
+
const tick = normalizeAppearance(args.tick, TICK_OPTIONS, `${operation} tick`);
|
|
104
|
+
const guides = normalizeRugGuides(args.guides, measure, operation);
|
|
105
|
+
const anchor = measure === "x" ? "y" : "x";
|
|
106
|
+
const highEdge = edge === "top" || edge === "right";
|
|
107
|
+
|
|
108
|
+
let next = this.createTickMark({ id, data, ...tick });
|
|
109
|
+
if (args.coordinate !== undefined) {
|
|
110
|
+
next = next.createCoordinate({ id: args.coordinate, type: "cartesian", layers: [id] });
|
|
111
|
+
}
|
|
112
|
+
next = next[measure === "x" ? "encodeX" : "encodeY"](
|
|
113
|
+
positionArgs(encoding, { target: id })
|
|
114
|
+
);
|
|
115
|
+
next = next[anchor === "x" ? "encodeX" : "encodeY"]({
|
|
116
|
+
target: id,
|
|
117
|
+
datum: highEdge ? 1 : 0,
|
|
118
|
+
fieldType: "quantitative",
|
|
119
|
+
scale: { id: `${id}Anchor`, domain: [0, 1], zero: false, nice: false }
|
|
120
|
+
});
|
|
121
|
+
next = next.encodeAngle({ target: id, value: measure === "x" ? 0 : 90 });
|
|
122
|
+
return applyFacadeGuides(next, guides, id, guides);
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
function normalizeStripPosition(program, data, value, operation, channel) {
|
|
127
|
+
const result = normalizeFieldEncoding(value, `${operation} ${channel}`);
|
|
128
|
+
validateOptionObject(result, MEASURE_OPTIONS, `${operation} ${channel}`);
|
|
129
|
+
validateNonEmptyString(result.field, `${operation} ${channel} field`);
|
|
130
|
+
const fieldType = inferFacadeFieldType(
|
|
131
|
+
program,
|
|
132
|
+
data,
|
|
133
|
+
result,
|
|
134
|
+
`${operation} ${channel}`
|
|
135
|
+
);
|
|
136
|
+
if (!["quantitative", "temporal", "nominal", "ordinal"].includes(fieldType)) {
|
|
137
|
+
throw new Error(`${operation} ${channel} has an unsupported field type.`);
|
|
138
|
+
}
|
|
139
|
+
return { ...omitUndefinedOptions(result), fieldType };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function normalizeStripJitter(value, channel, slot, operation) {
|
|
143
|
+
if (value === undefined || value === false) return undefined;
|
|
144
|
+
if (!isPlainObject(value)) {
|
|
145
|
+
throw new TypeError(`${operation} jitter must be false or a plain object.`);
|
|
146
|
+
}
|
|
147
|
+
validateOptionObject(value, JITTER_OPTIONS, `${operation} jitter`);
|
|
148
|
+
if (!isPlainObject(value.maxOffset)) {
|
|
149
|
+
throw new TypeError(`${operation} jitter.maxOffset must be a plain object.`);
|
|
150
|
+
}
|
|
151
|
+
const keys = Object.keys(value.maxOffset);
|
|
152
|
+
const expected = slot === "category" ? "band" : "pixels";
|
|
153
|
+
if (keys.length !== 1 || keys[0] !== expected) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`${operation} ${slot} slot jitter requires maxOffset.${expected}.`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return { ...value, channel };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function normalizeStripGuides(value, actualChannels, hasLegend, operation) {
|
|
162
|
+
const guides = normalizeGuides(value, operation);
|
|
163
|
+
if (guides === false) return false;
|
|
164
|
+
validateOptionObject(guides, ["axes", "grid", "legend"], `${operation} guides`);
|
|
165
|
+
const anchor = actualChannels.length === 1
|
|
166
|
+
? (actualChannels[0] === "x" ? "y" : "x")
|
|
167
|
+
: undefined;
|
|
168
|
+
let axes = guides.axes;
|
|
169
|
+
if (axes !== undefined && axes !== false) {
|
|
170
|
+
if (!isPlainObject(axes)) {
|
|
171
|
+
throw new TypeError(`${operation} guides.axes must be false or a plain object.`);
|
|
172
|
+
}
|
|
173
|
+
if (anchor !== undefined && axes[anchor] !== undefined && axes[anchor] !== false) {
|
|
174
|
+
throw new Error(`${operation} cannot create an axis for its constant slot.`);
|
|
175
|
+
}
|
|
176
|
+
axes = anchor === undefined ? { ...axes } : { ...axes, [anchor]: false };
|
|
177
|
+
} else if (axes === undefined) {
|
|
178
|
+
axes = {
|
|
179
|
+
x: actualChannels.includes("x") ? {} : false,
|
|
180
|
+
y: actualChannels.includes("y") ? {} : false
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
let grid = guides.grid;
|
|
184
|
+
if (grid === undefined) grid = false;
|
|
185
|
+
else if (grid !== false) {
|
|
186
|
+
if (!isPlainObject(grid)) {
|
|
187
|
+
throw new TypeError(`${operation} guides.grid must be false or a plain object.`);
|
|
188
|
+
}
|
|
189
|
+
const anchorGrid = anchor === "x" ? "vertical" : anchor === "y" ? "horizontal" : undefined;
|
|
190
|
+
if (
|
|
191
|
+
anchorGrid !== undefined && grid[anchorGrid] !== undefined &&
|
|
192
|
+
grid[anchorGrid] !== false
|
|
193
|
+
) {
|
|
194
|
+
throw new Error(`${operation} cannot create a grid for its constant slot.`);
|
|
195
|
+
}
|
|
196
|
+
grid = anchorGrid === undefined ? { ...grid } : { ...grid, [anchorGrid]: false };
|
|
197
|
+
}
|
|
198
|
+
if (!hasLegend && guides.legend !== undefined && guides.legend !== false) {
|
|
199
|
+
throw new Error(`${operation} legend requires color, size, or shape.`);
|
|
200
|
+
}
|
|
201
|
+
const legend = guides.legend === undefined ? (hasLegend ? {} : false) : guides.legend;
|
|
202
|
+
return { axes, grid, legend };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export const createStripPlot = action(
|
|
206
|
+
{
|
|
207
|
+
op: "createStripPlot",
|
|
208
|
+
description: "Create a one-dimensional Strip plot with an optional categorical slot and jitter."
|
|
209
|
+
},
|
|
210
|
+
function (args = {}) {
|
|
211
|
+
const operation = "createStripPlot";
|
|
212
|
+
validateFacadeOptions(args, STRIP_OPTIONS, operation);
|
|
213
|
+
if (args.x === undefined) throw new Error(`${operation} requires x.`);
|
|
214
|
+
const id = resolveFacadeId(this, args.id, {
|
|
215
|
+
defaultId: "stripPlot",
|
|
216
|
+
operation
|
|
217
|
+
});
|
|
218
|
+
const data = resolveFacadeData(this, args.data, operation);
|
|
219
|
+
const x = normalizeStripPosition(this, data, args.x, operation, "x");
|
|
220
|
+
const y = args.y === undefined
|
|
221
|
+
? undefined
|
|
222
|
+
: normalizeStripPosition(this, data, args.y, operation, "y");
|
|
223
|
+
const isMeasure = encoding => ["quantitative", "temporal"].includes(encoding.fieldType);
|
|
224
|
+
if (!isMeasure(x) && y === undefined) {
|
|
225
|
+
throw new Error(`${operation} x must be a quantitative or temporal measure when y is omitted.`);
|
|
226
|
+
}
|
|
227
|
+
if (y !== undefined && isMeasure(x) === isMeasure(y)) {
|
|
228
|
+
throw new Error(`${operation} requires exactly one measure and one categorical slot.`);
|
|
229
|
+
}
|
|
230
|
+
const color = normalizeEncoding(args.color, `${operation} color`);
|
|
231
|
+
const size = normalizeEncoding(args.size, `${operation} size`);
|
|
232
|
+
const shape = normalizeEncoding(args.shape, `${operation} shape`);
|
|
233
|
+
const { radius, ...point } = normalizeAppearance(
|
|
234
|
+
args.point,
|
|
235
|
+
POINT_OPTIONS,
|
|
236
|
+
`${operation} point`
|
|
237
|
+
);
|
|
238
|
+
if (radius !== undefined) {
|
|
239
|
+
validateNonNegativeFinite(radius, `${operation} point radius`);
|
|
240
|
+
}
|
|
241
|
+
if (radius !== undefined && size !== undefined) {
|
|
242
|
+
throw new Error(`${operation} point radius conflicts with size.`);
|
|
243
|
+
}
|
|
244
|
+
const categoryChannel = y === undefined ? undefined : (isMeasure(x) ? "y" : "x");
|
|
245
|
+
const jitterChannel = categoryChannel ?? "y";
|
|
246
|
+
const jitter = normalizeStripJitter(
|
|
247
|
+
args.jitter,
|
|
248
|
+
jitterChannel,
|
|
249
|
+
categoryChannel === undefined ? "constant" : "category",
|
|
250
|
+
operation
|
|
251
|
+
);
|
|
252
|
+
const guides = normalizeStripGuides(
|
|
253
|
+
args.guides,
|
|
254
|
+
y === undefined ? ["x"] : ["x", "y"],
|
|
255
|
+
color !== undefined || size !== undefined || shape !== undefined,
|
|
256
|
+
operation
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
let next = this.createPointMark({ id, data, ...point });
|
|
260
|
+
if (args.coordinate !== undefined) {
|
|
261
|
+
next = next.createCoordinate({ id: args.coordinate, type: "cartesian", layers: [id] });
|
|
262
|
+
}
|
|
263
|
+
next = next.encodeX(positionArgs(x, { target: id }));
|
|
264
|
+
next = y === undefined
|
|
265
|
+
? next.encodeY({
|
|
266
|
+
target: id,
|
|
267
|
+
datum: 0.5,
|
|
268
|
+
fieldType: "quantitative",
|
|
269
|
+
scale: { id: `${id}Anchor`, domain: [0, 1], zero: false, nice: false }
|
|
270
|
+
})
|
|
271
|
+
: next.encodeY(positionArgs(y, { target: id }));
|
|
272
|
+
if (radius !== undefined) next = next.encodePointRadius({ target: id, value: radius });
|
|
273
|
+
if (color !== undefined) next = next.encodeColor(targetArgs(color, id));
|
|
274
|
+
if (size !== undefined) next = next.encodeSize(targetArgs(size, id));
|
|
275
|
+
if (shape !== undefined) next = next.encodeShape(targetArgs(shape, id));
|
|
276
|
+
if (jitter !== undefined) next = next.jitterPoints({ target: id, ...jitter });
|
|
277
|
+
return applyFacadeGuides(next, guides, id, guides);
|
|
278
|
+
}
|
|
279
|
+
);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { action } from "../../core/action.js";
|
|
2
|
+
import { validateNonNegativeFinite } from "../../core/validation.js";
|
|
2
3
|
import {
|
|
3
4
|
applyFacadeGuides,
|
|
4
5
|
normalizeAppearance,
|
|
@@ -17,7 +18,7 @@ const OPTIONS = Object.freeze([
|
|
|
17
18
|
"point", "guides"
|
|
18
19
|
]);
|
|
19
20
|
const POINT_OPTIONS = Object.freeze([
|
|
20
|
-
"shape", "fill", "opacity", "stroke", "strokeWidth"
|
|
21
|
+
"shape", "fill", "opacity", "stroke", "strokeWidth", "radius"
|
|
21
22
|
]);
|
|
22
23
|
|
|
23
24
|
export const createScatterPlot = action(
|
|
@@ -32,7 +33,7 @@ export const createScatterPlot = action(
|
|
|
32
33
|
operation: "createScatterPlot"
|
|
33
34
|
});
|
|
34
35
|
const data = resolveFacadeData(this, args.data, "createScatterPlot");
|
|
35
|
-
const point = normalizeAppearance(
|
|
36
|
+
const { radius, ...point } = normalizeAppearance(
|
|
36
37
|
args.point,
|
|
37
38
|
POINT_OPTIONS,
|
|
38
39
|
"createScatterPlot point"
|
|
@@ -43,14 +44,19 @@ export const createScatterPlot = action(
|
|
|
43
44
|
const size = normalizeEncoding(args.size, "createScatterPlot size");
|
|
44
45
|
const shape = normalizeEncoding(args.shape, "createScatterPlot shape");
|
|
45
46
|
const guides = normalizeGuides(args.guides, "createScatterPlot");
|
|
47
|
+
if (Object.hasOwn(args.point ?? {}, "radius")) {
|
|
48
|
+
validateNonNegativeFinite(radius, "createScatterPlot point radius");
|
|
49
|
+
if (size !== undefined) throw new Error("createScatterPlot point radius conflicts with size.");
|
|
50
|
+
}
|
|
46
51
|
|
|
47
52
|
let next = this
|
|
48
53
|
.createPointMark({ id, data, ...point })
|
|
49
54
|
.encodeX(positionArgs(x, { target: id, coordinate: args.coordinate }))
|
|
50
55
|
.encodeY(positionArgs(y, { target: id, coordinate: args.coordinate }));
|
|
56
|
+
if (radius !== undefined) next = next.encodePointRadius({ target: id, value: radius });
|
|
51
57
|
if (color !== undefined) next = next.encodeColor(targetArgs(color, id));
|
|
52
58
|
if (size !== undefined) next = next.encodeSize(targetArgs(size, id));
|
|
53
59
|
if (shape !== undefined) next = next.encodeShape(targetArgs(shape, id));
|
|
54
|
-
return applyFacadeGuides(next, guides);
|
|
60
|
+
return applyFacadeGuides(next, guides, id);
|
|
55
61
|
}
|
|
56
62
|
);
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { resolveOptionalUserId, validateUserId } from "../../core/identifiers.js";
|
|
2
2
|
import { isPlainObject } from "../../core/immutable.js";
|
|
3
|
-
import { validateOptionObject } from "../../core/validation.js";
|
|
4
|
-
import { findDataset } from "../../selectors/datasets.js";
|
|
3
|
+
import { validateNonEmptyString, validateOptionObject } from "../../core/validation.js";
|
|
4
|
+
import { findDataset, requireMaterializedDataset } from "../../selectors/datasets.js";
|
|
5
5
|
import { isNominalValue } from "../../grammar/scales/fields.js";
|
|
6
6
|
import { hasLayer } from "../../selectors/layers.js";
|
|
7
|
+
import { fulfillFacadeGuides } from "../guides/facade.js";
|
|
7
8
|
|
|
8
9
|
export function validateFacadeOptions(args, supported, operation) {
|
|
9
10
|
return validateOptionObject(args, supported, operation);
|
|
@@ -12,18 +13,17 @@ export function validateFacadeOptions(args, supported, operation) {
|
|
|
12
13
|
export function resolveFacadeData(program, requested, operation) {
|
|
13
14
|
if (requested !== undefined) {
|
|
14
15
|
const id = validateUserId(requested, `${operation} dataset id`);
|
|
15
|
-
|
|
16
|
-
throw new Error(`Unknown dataset "${id}".`);
|
|
17
|
-
}
|
|
18
|
-
return id;
|
|
16
|
+
return requireMaterializedDataset(program, id).id;
|
|
19
17
|
}
|
|
20
18
|
|
|
21
19
|
const current = program.context.currentData;
|
|
22
20
|
if (current !== undefined && findDataset(program, current) !== undefined) {
|
|
23
|
-
return current;
|
|
21
|
+
return requireMaterializedDataset(program, current).id;
|
|
24
22
|
}
|
|
25
23
|
const datasets = program.semanticSpec.datasets;
|
|
26
|
-
if (datasets.length === 1)
|
|
24
|
+
if (datasets.length === 1) {
|
|
25
|
+
return requireMaterializedDataset(program, datasets[0].id).id;
|
|
26
|
+
}
|
|
27
27
|
if (datasets.length > 1) {
|
|
28
28
|
throw new Error(
|
|
29
29
|
`${operation} requires data when multiple datasets are available.`
|
|
@@ -104,6 +104,39 @@ export function normalizeAppearance(value, supported, label) {
|
|
|
104
104
|
return { ...value };
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
export function omitUndefinedOptions(value) {
|
|
108
|
+
return Object.fromEntries(Object.entries(value).filter(([, option]) => option !== undefined));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function normalizeCategoricalColor(value, label, supported = ["field", "fieldType", "scale", "palette"]) {
|
|
112
|
+
const color = normalizeFieldEncoding(value, label);
|
|
113
|
+
validateOptionObject(color, supported, label);
|
|
114
|
+
validateNonEmptyString(color.field, `${label} field`);
|
|
115
|
+
if (color.fieldType !== undefined && !["nominal", "ordinal"].includes(color.fieldType)) {
|
|
116
|
+
throw new TypeError(`${label} must be nominal or ordinal.`);
|
|
117
|
+
}
|
|
118
|
+
if (color.scale !== undefined) validateOptionObject(color.scale, undefined, `${label}.scale`);
|
|
119
|
+
return { ...omitUndefinedOptions(color),
|
|
120
|
+
...(color.scale === undefined ? {} : { scale: omitUndefinedOptions(color.scale) }) };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function normalizeCategoricalGuides(value, operation, color) {
|
|
124
|
+
const guides = normalizeGuides(value, operation);
|
|
125
|
+
if (guides === false) return false;
|
|
126
|
+
validateOptionObject(guides, ["axes", "grid", "legend"], `${operation} guides`);
|
|
127
|
+
const legend = guides.legend;
|
|
128
|
+
if (legend !== undefined && legend !== false) {
|
|
129
|
+
validateOptionObject(legend, undefined, `${operation} guides.legend`);
|
|
130
|
+
if (color === undefined) throw new Error(`${operation} legend requires color.`);
|
|
131
|
+
if (Object.hasOwn(legend, "gradient") || Object.hasOwn(legend, "count") ||
|
|
132
|
+
legend.channels !== undefined && (!Array.isArray(legend.channels) ||
|
|
133
|
+
legend.channels.length !== 1 || legend.channels[0] !== "color")) {
|
|
134
|
+
throw new Error(`${operation} only supports a categorical color legend.`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return guides;
|
|
138
|
+
}
|
|
139
|
+
|
|
107
140
|
export function normalizeTargetOptions(value, label) {
|
|
108
141
|
if (value === undefined) return undefined;
|
|
109
142
|
if (!isPlainObject(value)) {
|
|
@@ -136,6 +169,32 @@ export function targetArgs(encoding, target) {
|
|
|
136
169
|
return { ...encoding, target };
|
|
137
170
|
}
|
|
138
171
|
|
|
139
|
-
export function applyFacadeGuides(program, guides) {
|
|
140
|
-
return guides
|
|
172
|
+
export function applyFacadeGuides(program, guides, target, explicitGuides = guides) {
|
|
173
|
+
return fulfillFacadeGuides(program, guides, target, explicitGuides);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function normalizeArcCategory(value, operation) {
|
|
177
|
+
const label = `${operation} category`;
|
|
178
|
+
const category = normalizeFieldEncoding(value, label);
|
|
179
|
+
validateOptionObject(category, ["field", "fieldType", "scale"], label);
|
|
180
|
+
validateNonEmptyString(category.field, `${label} field`);
|
|
181
|
+
const fieldType = category.fieldType === undefined ? "nominal" : category.fieldType;
|
|
182
|
+
if (!["nominal", "ordinal"].includes(fieldType)) throw new TypeError(`${label} must be nominal or ordinal.`);
|
|
183
|
+
if (category.scale !== undefined) {
|
|
184
|
+
validateOptionObject(category.scale, ["id", "type", "domain", "range", "reverse"], `${label}.scale`);
|
|
185
|
+
if (category.scale.type !== undefined && category.scale.type !== "band") {
|
|
186
|
+
throw new TypeError(`${label}.scale.type must be band.`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return { ...omitUndefinedOptions(category), fieldType,
|
|
190
|
+
...(category.scale === undefined ? {} : { scale: omitUndefinedOptions(category.scale) }) };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function normalizeCategoryAggregate(args, operation) {
|
|
194
|
+
const aggregate = args.aggregate === undefined ? "count" : args.aggregate;
|
|
195
|
+
if (!["count", "sum"].includes(aggregate) || (aggregate === "sum") !== (args.value !== undefined)) {
|
|
196
|
+
throw new Error(`${operation} requires count without value or explicit sum with value.`);
|
|
197
|
+
}
|
|
198
|
+
if (args.value !== undefined) validateNonEmptyString(args.value, `${operation} value`);
|
|
199
|
+
return aggregate;
|
|
141
200
|
}
|
|
@@ -20,6 +20,9 @@ const LAYOUT_EDIT_OPTIONS = Object.freeze([
|
|
|
20
20
|
"columns", "gap", "align", "padding"
|
|
21
21
|
]);
|
|
22
22
|
const REPLACEMENT_OPTIONS = Object.freeze(["target", "program"]);
|
|
23
|
+
const INSERTION_OPTIONS = Object.freeze(["id", "program", "before", "after"]);
|
|
24
|
+
const REMOVAL_OPTIONS = Object.freeze(["target"]);
|
|
25
|
+
const REORDER_OPTIONS = Object.freeze(["order"]);
|
|
23
26
|
|
|
24
27
|
function option(args, name, fallback) {
|
|
25
28
|
return Object.hasOwn(args, name) ? args[name] : fallback;
|
|
@@ -180,10 +183,17 @@ const editCompositionLayout = action(
|
|
|
180
183
|
: current.padding;
|
|
181
184
|
let layout;
|
|
182
185
|
if (current.type === "facet") {
|
|
186
|
+
const gridCells = new Map(
|
|
187
|
+
(current.facet.grid?.cells ?? []).map(cell => [cell.id, cell])
|
|
188
|
+
);
|
|
183
189
|
layout = resolveFacetLayout({
|
|
184
190
|
children: current.children.map((id, index) => ({
|
|
185
191
|
...childDescriptor({ id, program: this.children[id] }),
|
|
186
|
-
value: current.facet.values[index]
|
|
192
|
+
value: current.facet.values[index],
|
|
193
|
+
...(gridCells.has(id) ? {
|
|
194
|
+
row: gridCells.get(id).row,
|
|
195
|
+
column: gridCells.get(id).column
|
|
196
|
+
} : {})
|
|
187
197
|
})),
|
|
188
198
|
columns: option(args, "columns", current.columns),
|
|
189
199
|
gap: option(args, "gap", current.gap),
|
|
@@ -224,6 +234,7 @@ const replaceCompositionChild = action(
|
|
|
224
234
|
},
|
|
225
235
|
function (args = {}) {
|
|
226
236
|
validateOptionObject(args, REPLACEMENT_OPTIONS, "replaceCompositionChild");
|
|
237
|
+
this._assertCompositionProgram("replaceCompositionChild");
|
|
227
238
|
if (this.compositionSpec.type === "facet") {
|
|
228
239
|
throw new Error("replaceCompositionChild is not available on a facet composition.");
|
|
229
240
|
}
|
|
@@ -246,9 +257,115 @@ const replaceCompositionChild = action(
|
|
|
246
257
|
}
|
|
247
258
|
);
|
|
248
259
|
|
|
260
|
+
function requireConcat(program, operation) {
|
|
261
|
+
program._assertCompositionProgram(operation);
|
|
262
|
+
if (program.compositionSpec.type === "facet") {
|
|
263
|
+
throw new Error(`${operation} is not available on a facet composition.`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const insertCompositionChild = action(
|
|
268
|
+
{
|
|
269
|
+
op: "insertCompositionChild",
|
|
270
|
+
description: "Insert one named child into a concat composition.",
|
|
271
|
+
scope: "composition"
|
|
272
|
+
},
|
|
273
|
+
function (args = {}) {
|
|
274
|
+
validateOptionObject(args, INSERTION_OPTIONS, "insertCompositionChild");
|
|
275
|
+
requireConcat(this, "insertCompositionChild");
|
|
276
|
+
const id = validateUserId(args.id, "Composition child ID");
|
|
277
|
+
if (Object.hasOwn(this.children, id)) {
|
|
278
|
+
throw new Error(`Composition child "${id}" already exists.`);
|
|
279
|
+
}
|
|
280
|
+
if (!(args.program instanceof CoreChartProgram)) {
|
|
281
|
+
throw new TypeError("insertCompositionChild program must be a ChartProgram.");
|
|
282
|
+
}
|
|
283
|
+
if (args.before !== undefined && args.after !== undefined) {
|
|
284
|
+
throw new Error("insertCompositionChild before and after are mutually exclusive.");
|
|
285
|
+
}
|
|
286
|
+
childDescriptor({ id, program: args.program });
|
|
287
|
+
const order = [...this.compositionSpec.children];
|
|
288
|
+
if (args.before !== undefined || args.after !== undefined) {
|
|
289
|
+
const property = args.before !== undefined ? "before" : "after";
|
|
290
|
+
const anchor = validateUserId(args[property], `Composition child ${property}`);
|
|
291
|
+
const index = order.indexOf(anchor);
|
|
292
|
+
if (index < 0) throw new Error(`Unknown composition child "${anchor}".`);
|
|
293
|
+
order.splice(index + (property === "after" ? 1 : 0), 0, id);
|
|
294
|
+
} else {
|
|
295
|
+
order.push(id);
|
|
296
|
+
}
|
|
297
|
+
return applyCompositionState(this, {
|
|
298
|
+
children: freezeOwned({ ...this.children, [id]: args.program }),
|
|
299
|
+
compositionSpec: { ...this.compositionSpec, children: order }
|
|
300
|
+
}, [id]);
|
|
301
|
+
}
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
const removeCompositionChild = action(
|
|
305
|
+
{
|
|
306
|
+
op: "removeCompositionChild",
|
|
307
|
+
description: "Remove one named child from a concat composition.",
|
|
308
|
+
scope: "composition"
|
|
309
|
+
},
|
|
310
|
+
function (args = {}) {
|
|
311
|
+
validateOptionObject(args, REMOVAL_OPTIONS, "removeCompositionChild");
|
|
312
|
+
requireConcat(this, "removeCompositionChild");
|
|
313
|
+
const target = validateUserId(args.target, "Composition child target");
|
|
314
|
+
if (!Object.hasOwn(this.children, target)) {
|
|
315
|
+
throw new Error(`Unknown composition child "${target}".`);
|
|
316
|
+
}
|
|
317
|
+
if (this.compositionSpec.children.length === 1) {
|
|
318
|
+
throw new Error("removeCompositionChild must leave at least one child.");
|
|
319
|
+
}
|
|
320
|
+
const { [target]: _removed, ...remaining } = this.children;
|
|
321
|
+
void _removed;
|
|
322
|
+
return applyCompositionState(this, {
|
|
323
|
+
children: freezeOwned(remaining),
|
|
324
|
+
compositionSpec: {
|
|
325
|
+
...this.compositionSpec,
|
|
326
|
+
children: this.compositionSpec.children.filter(id => id !== target)
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
const reorderCompositionChildren = action(
|
|
333
|
+
{
|
|
334
|
+
op: "reorderCompositionChildren",
|
|
335
|
+
description: "Reorder every named child in a concat composition.",
|
|
336
|
+
scope: "composition"
|
|
337
|
+
},
|
|
338
|
+
function (args = {}) {
|
|
339
|
+
validateOptionObject(args, REORDER_OPTIONS, "reorderCompositionChildren");
|
|
340
|
+
requireConcat(this, "reorderCompositionChildren");
|
|
341
|
+
if (!Array.isArray(args.order) || args.order.length === 0 ||
|
|
342
|
+
args.order.some(id => typeof id !== "string" || id.length === 0)) {
|
|
343
|
+
throw new TypeError("reorderCompositionChildren order must be a non-empty child ID array.");
|
|
344
|
+
}
|
|
345
|
+
if (new Set(args.order).size !== args.order.length) {
|
|
346
|
+
throw new Error("reorderCompositionChildren order must not contain duplicates.");
|
|
347
|
+
}
|
|
348
|
+
const current = this.compositionSpec.children;
|
|
349
|
+
if (args.order.length !== current.length ||
|
|
350
|
+
args.order.some(id => !Object.hasOwn(this.children, id))) {
|
|
351
|
+
throw new Error("reorderCompositionChildren order must contain every current child exactly once.");
|
|
352
|
+
}
|
|
353
|
+
if (args.order.every((id, index) => id === current[index])) {
|
|
354
|
+
throw new Error("reorderCompositionChildren requires an actual order change.");
|
|
355
|
+
}
|
|
356
|
+
return applyCompositionState(this, {
|
|
357
|
+
children: this.children,
|
|
358
|
+
compositionSpec: { ...this.compositionSpec, children: [...args.order] }
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
);
|
|
362
|
+
|
|
249
363
|
export function registerCompositionActions(ProgramClass) {
|
|
250
364
|
ProgramClass.prototype.useProgram = useProgram;
|
|
251
365
|
ProgramClass.prototype.materializeComposition = materializeComposition;
|
|
252
366
|
ProgramClass.prototype.editCompositionLayout = editCompositionLayout;
|
|
253
367
|
ProgramClass.prototype.replaceCompositionChild = replaceCompositionChild;
|
|
368
|
+
ProgramClass.prototype.insertCompositionChild = insertCompositionChild;
|
|
369
|
+
ProgramClass.prototype.removeCompositionChild = removeCompositionChild;
|
|
370
|
+
ProgramClass.prototype.reorderCompositionChildren = reorderCompositionChildren;
|
|
254
371
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {
|
|
2
|
+
deriveBinRows,
|
|
3
|
+
normalizeBinTransform,
|
|
4
|
+
resolveBinTransform
|
|
5
|
+
} from "../../grammar/bin.js";
|
|
6
|
+
import { derivedCreator, derivedMaterializer } from "./shared.js";
|
|
7
|
+
|
|
8
|
+
const OPTIONS = Object.freeze([
|
|
9
|
+
"id", "source", "field", "maxBins", "step", "boundaries", "extent",
|
|
10
|
+
"nice", "zero", "includeEmpty", "members", "as"
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
export const materializeBinData = derivedMaterializer(
|
|
14
|
+
"materializeBinData",
|
|
15
|
+
"Materialize one immutable one-dimensional bin dataset.",
|
|
16
|
+
"bin",
|
|
17
|
+
deriveBinRows,
|
|
18
|
+
resolveBinTransform
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
export const createBinData = derivedCreator(
|
|
22
|
+
"createBinData",
|
|
23
|
+
"Create reusable one-dimensional bin bounds, counts, and members.",
|
|
24
|
+
OPTIONS,
|
|
25
|
+
"Bin dataset id",
|
|
26
|
+
"Bin source dataset id",
|
|
27
|
+
normalizeBinTransform,
|
|
28
|
+
"materializeBinData"
|
|
29
|
+
);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {
|
|
2
|
+
deriveComputedRows,
|
|
3
|
+
normalizeComputedTransform
|
|
4
|
+
} from "../../grammar/computed.js";
|
|
5
|
+
import { derivedCreator, derivedMaterializer } from "./shared.js";
|
|
6
|
+
|
|
7
|
+
const OPTIONS = Object.freeze(["id", "source", "as", "expression"]);
|
|
8
|
+
|
|
9
|
+
export const materializeComputedData = derivedMaterializer(
|
|
10
|
+
"materializeComputedData",
|
|
11
|
+
"Materialize one immutable row-level arithmetic dataset.",
|
|
12
|
+
"computed",
|
|
13
|
+
deriveComputedRows
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
export const createComputedData = derivedCreator(
|
|
17
|
+
"createComputedData",
|
|
18
|
+
"Create a finite computed field from a closed arithmetic expression.",
|
|
19
|
+
OPTIONS,
|
|
20
|
+
"Computed dataset id",
|
|
21
|
+
"Computed source dataset id",
|
|
22
|
+
normalizeComputedTransform,
|
|
23
|
+
"materializeComputedData"
|
|
24
|
+
);
|