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
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import {
|
|
3
|
+
applyFacadeGuides, normalizeAppearance, normalizeArcCategory, normalizeCategoryAggregate,
|
|
4
|
+
normalizeCategoricalColor, normalizeCategoricalGuides, omitUndefinedOptions,
|
|
5
|
+
resolveFacadeData, resolveFacadeId, validateFacadeOptions
|
|
6
|
+
} from "./shared.js";
|
|
7
|
+
|
|
8
|
+
const OPTIONS = ["id", "data", "coordinate", "category", "value", "aggregate", "radiusScale", "color", "arc", "guides"];
|
|
9
|
+
const ARC_OPTIONS = ["innerRadius", "padAngle", "fill", "opacity", "stroke", "strokeWidth"];
|
|
10
|
+
const RADIUS_OPTIONS = ["id", "type", "domain", "range", "zero", "nice", "reverse", "clamp"];
|
|
11
|
+
|
|
12
|
+
function createMeasuredPlot(program, args, { operation, defaultId, mapping }) {
|
|
13
|
+
validateFacadeOptions(args, OPTIONS, operation);
|
|
14
|
+
const id = resolveFacadeId(program, args.id, { defaultId, operation });
|
|
15
|
+
const data = resolveFacadeData(program, args.data, operation);
|
|
16
|
+
const category = normalizeArcCategory(args.category, operation);
|
|
17
|
+
const aggregate = normalizeCategoryAggregate(args, operation);
|
|
18
|
+
const arc = omitUndefinedOptions(normalizeAppearance(args.arc, ARC_OPTIONS, `${operation} arc`));
|
|
19
|
+
if (arc.padAngle !== undefined && arc.padAngle !== 0) throw new Error(`${operation} requires padAngle 0.`);
|
|
20
|
+
const radiusScale = args.radiusScale === undefined ? undefined
|
|
21
|
+
: omitUndefinedOptions(normalizeAppearance(args.radiusScale, RADIUS_OPTIONS, `${operation} radiusScale`));
|
|
22
|
+
const color = args.color === false ? undefined
|
|
23
|
+
: normalizeCategoricalColor(args.color === undefined ? category.field : args.color, `${operation} color`);
|
|
24
|
+
if (color !== undefined && arc.fill !== undefined) {
|
|
25
|
+
throw new Error(`${operation} arc.fill cannot be combined with color; use color:false.`);
|
|
26
|
+
}
|
|
27
|
+
const guides = normalizeCategoricalGuides(args.guides, operation, color);
|
|
28
|
+
let next = program.createArcMark({ id, data, ...arc }).encodeTheta({
|
|
29
|
+
...category, target: id, ...(args.coordinate === undefined ? {} : { coordinate: args.coordinate })
|
|
30
|
+
}).encodeR({ target: id, aggregate, mapping,
|
|
31
|
+
...(aggregate === "sum" ? { field: args.value } : {}),
|
|
32
|
+
...(radiusScale === undefined ? {} : { scale: radiusScale }) });
|
|
33
|
+
if (color !== undefined) next = next.encodeColor({ ...color, target: id });
|
|
34
|
+
return applyFacadeGuides(next, guides, id, args.guides ?? {});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const createRosePlot = action({ op: "createRosePlot", description: "Create equal-angle sectors with area proportional to category count or sum." }, function (args = {}) {
|
|
38
|
+
return createMeasuredPlot(this, args, { operation: "createRosePlot", defaultId: "rosePlot", mapping: "area" });
|
|
39
|
+
});
|
|
40
|
+
export const createRadialBarPlot = action({ op: "createRadialBarPlot", description: "Create equal-angle sectors with radial length proportional to category count or sum." }, function (args = {}) {
|
|
41
|
+
return createMeasuredPlot(this, args, { operation: "createRadialBarPlot", defaultId: "radialBarPlot", mapping: "radius-length" });
|
|
42
|
+
});
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import { isPlainObject } from "../../core/immutable.js";
|
|
3
|
+
import { validateUserId } from "../../core/identifiers.js";
|
|
4
|
+
import { validateOptionObject } from "../../core/validation.js";
|
|
5
|
+
import { findDataset } from "../../selectors/datasets.js";
|
|
6
|
+
import { findLayer } from "../../selectors/layers.js";
|
|
7
|
+
import { findSemanticScale } from "../../selectors/scales.js";
|
|
8
|
+
import { resolveViolinRoles } from "../violinPlots/create.js";
|
|
9
|
+
import {
|
|
10
|
+
applyFacadeGuides,
|
|
11
|
+
normalizeEncoding,
|
|
12
|
+
normalizeGuides,
|
|
13
|
+
resolveFacadeData,
|
|
14
|
+
resolveFacadeId,
|
|
15
|
+
validateFacadeOptions
|
|
16
|
+
} from "./shared.js";
|
|
17
|
+
|
|
18
|
+
const CREATE_OPTIONS = Object.freeze([
|
|
19
|
+
"id", "data", "coordinate", "category", "value", "orientation", "side",
|
|
20
|
+
"density", "summary", "points", "color", "guides"
|
|
21
|
+
]);
|
|
22
|
+
const EDIT_OPTIONS = Object.freeze([
|
|
23
|
+
"target", "data", "category", "value", "orientation", "side", "density",
|
|
24
|
+
"summary", "points", "color"
|
|
25
|
+
]);
|
|
26
|
+
const DENSITY_OPTIONS = Object.freeze([
|
|
27
|
+
"bandwidth", "extent", "steps", "kernel", "normalization", "width", "area"
|
|
28
|
+
]);
|
|
29
|
+
const BOX_OPTIONS = Object.freeze([
|
|
30
|
+
"type", "whisker", "width", "outliers", "box", "median", "outlier"
|
|
31
|
+
]);
|
|
32
|
+
const INTERVAL_OPTIONS = Object.freeze([
|
|
33
|
+
"type", "center", "extent", "method", "level", "point", "errorBar"
|
|
34
|
+
]);
|
|
35
|
+
const POINTS_OPTIONS = Object.freeze([
|
|
36
|
+
"type", "size", "shape", "point", "jitter", "packing"
|
|
37
|
+
]);
|
|
38
|
+
const SLOT_OFFSET_BAND = 0.22;
|
|
39
|
+
const POINT_SPREAD_BAND = 0.12;
|
|
40
|
+
|
|
41
|
+
function normalizeOrientation(value) {
|
|
42
|
+
const orientation = value ?? "vertical";
|
|
43
|
+
if (!["vertical", "horizontal"].includes(orientation)) {
|
|
44
|
+
throw new Error(`Unsupported Raincloud orientation "${orientation}".`);
|
|
45
|
+
}
|
|
46
|
+
return orientation;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizeSide(value) {
|
|
50
|
+
const side = value ?? "before";
|
|
51
|
+
if (!["before", "after"].includes(side)) {
|
|
52
|
+
throw new Error(`Unsupported Raincloud side "${side}".`);
|
|
53
|
+
}
|
|
54
|
+
return side;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function normalizeDensity(value, current) {
|
|
58
|
+
if (value === false) return false;
|
|
59
|
+
const patch = value ?? {};
|
|
60
|
+
validateOptionObject(patch, DENSITY_OPTIONS, "Raincloud density");
|
|
61
|
+
return { ...(current === false ? {} : current ?? {}), ...patch };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function normalizeSummary(value, current) {
|
|
65
|
+
if (value === false) return false;
|
|
66
|
+
const patch = value ?? {};
|
|
67
|
+
if (!isPlainObject(patch)) {
|
|
68
|
+
throw new TypeError("Raincloud summary must be false or a plain object.");
|
|
69
|
+
}
|
|
70
|
+
const type = patch.type ?? (current === false ? undefined : current?.type) ?? "box";
|
|
71
|
+
const keys = type === "box" ? BOX_OPTIONS : type === "interval" ? INTERVAL_OPTIONS : undefined;
|
|
72
|
+
if (keys === undefined) throw new Error(`Unsupported Raincloud summary type "${type}".`);
|
|
73
|
+
validateOptionObject(patch, keys, "Raincloud summary");
|
|
74
|
+
const base = current !== false && current?.type === type ? current : {};
|
|
75
|
+
return { ...base, ...patch, type };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizePoints(value, current) {
|
|
79
|
+
if (value === false) return false;
|
|
80
|
+
const patch = value ?? {};
|
|
81
|
+
if (!isPlainObject(patch)) {
|
|
82
|
+
throw new TypeError("Raincloud points must be false or a plain object.");
|
|
83
|
+
}
|
|
84
|
+
const type = patch.type ?? (current === false ? undefined : current?.type) ?? "beeswarm";
|
|
85
|
+
if (!["strip", "beeswarm"].includes(type)) {
|
|
86
|
+
throw new Error(`Unsupported Raincloud points type "${type}".`);
|
|
87
|
+
}
|
|
88
|
+
validateOptionObject(patch, POINTS_OPTIONS, "Raincloud points");
|
|
89
|
+
if (type === "strip" && Object.hasOwn(patch, "packing")) {
|
|
90
|
+
throw new Error("Raincloud strip points do not accept packing.");
|
|
91
|
+
}
|
|
92
|
+
if (type === "beeswarm" && Object.hasOwn(patch, "jitter")) {
|
|
93
|
+
throw new Error("Raincloud beeswarm points do not accept jitter.");
|
|
94
|
+
}
|
|
95
|
+
const base = current !== false && current?.type === type ? current : {};
|
|
96
|
+
return { ...base, ...patch, type };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function roleScale(encoding, fallbackId, label, defaults = {}) {
|
|
100
|
+
if (encoding.scale !== undefined && !isPlainObject(encoding.scale)) {
|
|
101
|
+
throw new TypeError(`${label}.scale must be a plain object.`);
|
|
102
|
+
}
|
|
103
|
+
if (defaults.type === "band" && encoding.scale?.type !== undefined && encoding.scale.type !== "band") {
|
|
104
|
+
throw new TypeError(`${label}.scale.type must be band.`);
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
...encoding,
|
|
108
|
+
scale: { ...defaults, ...(encoding.scale ?? {}), id: encoding.scale?.id ?? fallbackId }
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function normalizeColor(value, category, id) {
|
|
113
|
+
if (value === undefined || value === false) return undefined;
|
|
114
|
+
const color = normalizeEncoding(value, "Raincloud color");
|
|
115
|
+
if (color.field !== category.field) {
|
|
116
|
+
throw new Error("Raincloud color must encode its category field.");
|
|
117
|
+
}
|
|
118
|
+
if (color.scale !== undefined && !isPlainObject(color.scale)) {
|
|
119
|
+
throw new TypeError("Raincloud color.scale must be a plain object.");
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
...color,
|
|
123
|
+
fieldType: color.fieldType ?? "nominal",
|
|
124
|
+
scale: { ...(color.scale ?? {}), id: color.scale?.id ?? `${id}Color` }
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function rolePositions(category, value, orientation) {
|
|
129
|
+
return orientation === "vertical"
|
|
130
|
+
? { x: category, y: value, categoryChannel: "x" }
|
|
131
|
+
: { x: value, y: category, categoryChannel: "y" };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function densitySide(orientation, side) {
|
|
135
|
+
if (orientation === "vertical") return side === "before" ? "left" : "right";
|
|
136
|
+
return side === "before" ? "top" : "bottom";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function markRematerializer(layer) {
|
|
140
|
+
return {
|
|
141
|
+
point: "rematerializePointMark",
|
|
142
|
+
bar: "rematerializeBarMark",
|
|
143
|
+
rule: "rematerializeRuleMark"
|
|
144
|
+
}[layer?.mark?.type];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function applySlotOffset(program, ids, categoryChannel, side, follow = []) {
|
|
148
|
+
const band = (side === "before" ? 1 : -1) * SLOT_OFFSET_BAND;
|
|
149
|
+
let next = program;
|
|
150
|
+
for (const id of ids) {
|
|
151
|
+
const layer = findLayer(next, id);
|
|
152
|
+
if (layer === undefined) continue;
|
|
153
|
+
next = next._withMarkConfig(id, {
|
|
154
|
+
...next.markConfigs[id],
|
|
155
|
+
categorySlotOffset: { channel: categoryChannel, band }
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
for (const id of [...ids, ...follow]) {
|
|
159
|
+
const layer = findLayer(next, id);
|
|
160
|
+
const operation = markRematerializer(layer);
|
|
161
|
+
if (operation !== undefined) next = next[operation]({ id });
|
|
162
|
+
}
|
|
163
|
+
return next;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function boxOffsetTargets(program, id) {
|
|
167
|
+
const box = program.markConfigs[id]?.boxPlot;
|
|
168
|
+
const errorBar = program.markConfigs[box?.whiskerId]?.errorBar;
|
|
169
|
+
return {
|
|
170
|
+
offset: [id, box?.whiskerId, errorBar?.lowerCapId, errorBar?.upperCapId, box?.outlierId]
|
|
171
|
+
.filter(candidate => candidate !== undefined && findLayer(program, candidate) !== undefined),
|
|
172
|
+
follow: [box?.medianId].filter(candidate => findLayer(program, candidate) !== undefined)
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function intervalOffsetTargets(program, id) {
|
|
177
|
+
const intervalId = `${id}Interval`;
|
|
178
|
+
const errorBar = program.markConfigs[intervalId]?.errorBar;
|
|
179
|
+
return [id, intervalId, errorBar?.lowerCapId, errorBar?.upperCapId]
|
|
180
|
+
.filter(candidate => candidate !== undefined && findLayer(program, candidate) !== undefined);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function addSummary(program, config, positions) {
|
|
184
|
+
if (config.summary === false) return program;
|
|
185
|
+
const id = config.childIds.summary;
|
|
186
|
+
const { type, ...options } = config.summary;
|
|
187
|
+
let next;
|
|
188
|
+
if (type === "box") {
|
|
189
|
+
next = program.createBoxPlot({
|
|
190
|
+
id,
|
|
191
|
+
data: config.source,
|
|
192
|
+
coordinate: config.coordinate,
|
|
193
|
+
x: positions.x,
|
|
194
|
+
y: positions.y,
|
|
195
|
+
width: options.width ?? { band: 0.24 },
|
|
196
|
+
...options,
|
|
197
|
+
guides: false
|
|
198
|
+
});
|
|
199
|
+
if (config.color !== undefined) next = next.encodeColor({ target: id, ...config.color });
|
|
200
|
+
const targets = boxOffsetTargets(next, id);
|
|
201
|
+
return applySlotOffset(next, targets.offset, positions.categoryChannel, config.side, targets.follow);
|
|
202
|
+
}
|
|
203
|
+
const { center, extent, method, level, point, errorBar } = options;
|
|
204
|
+
const interval = {
|
|
205
|
+
field: config.value.field,
|
|
206
|
+
scale: config.value.scale,
|
|
207
|
+
...(center === undefined ? {} : { center }),
|
|
208
|
+
...(extent === undefined ? {} : { extent }),
|
|
209
|
+
...(method === undefined ? {} : { method }),
|
|
210
|
+
...(level === undefined ? {} : { level })
|
|
211
|
+
};
|
|
212
|
+
next = program.createIntervalPlot({
|
|
213
|
+
id,
|
|
214
|
+
data: config.source,
|
|
215
|
+
coordinate: config.coordinate,
|
|
216
|
+
x: config.orientation === "vertical" ? config.category : interval,
|
|
217
|
+
y: config.orientation === "vertical" ? interval : config.category,
|
|
218
|
+
...(point === undefined ? {} : { point }),
|
|
219
|
+
...(errorBar === undefined ? {} : { errorBar }),
|
|
220
|
+
guides: false
|
|
221
|
+
});
|
|
222
|
+
if (config.color !== undefined) next = next.encodeColor({ target: id, ...config.color });
|
|
223
|
+
return applySlotOffset(
|
|
224
|
+
next,
|
|
225
|
+
intervalOffsetTargets(next, id),
|
|
226
|
+
positions.categoryChannel,
|
|
227
|
+
config.side
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function addPoints(program, config, positions) {
|
|
232
|
+
if (config.points === false) return program;
|
|
233
|
+
const id = config.childIds.points;
|
|
234
|
+
const { type, size, shape, point } = config.points;
|
|
235
|
+
const common = {
|
|
236
|
+
id,
|
|
237
|
+
data: config.source,
|
|
238
|
+
coordinate: config.coordinate,
|
|
239
|
+
x: positions.x,
|
|
240
|
+
y: positions.y,
|
|
241
|
+
...(config.color === undefined ? {} : { color: config.color }),
|
|
242
|
+
...(size === undefined ? {} : { size }),
|
|
243
|
+
...(shape === undefined ? {} : { shape }),
|
|
244
|
+
...(point === undefined ? {} : { point }),
|
|
245
|
+
guides: false
|
|
246
|
+
};
|
|
247
|
+
let next;
|
|
248
|
+
if (type === "strip") {
|
|
249
|
+
const jitter = Object.hasOwn(config.points, "jitter")
|
|
250
|
+
? config.points.jitter
|
|
251
|
+
: { maxOffset: { band: POINT_SPREAD_BAND } };
|
|
252
|
+
next = program.createStripPlot({ ...common, jitter });
|
|
253
|
+
} else {
|
|
254
|
+
const requested = config.points.packing;
|
|
255
|
+
const packing = requested === false
|
|
256
|
+
? false
|
|
257
|
+
: {
|
|
258
|
+
maxOffset: { band: POINT_SPREAD_BAND },
|
|
259
|
+
...(requested ?? {})
|
|
260
|
+
};
|
|
261
|
+
next = program.createBeeswarmPlot({ ...common, packing });
|
|
262
|
+
}
|
|
263
|
+
return applySlotOffset(next, [id], positions.categoryChannel, config.side);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function buildRaincloud(program, config) {
|
|
267
|
+
const positions = rolePositions(config.category, config.value, config.orientation);
|
|
268
|
+
let next = program;
|
|
269
|
+
if (config.density !== false) {
|
|
270
|
+
const { area, ...density } = config.density;
|
|
271
|
+
next = next.createViolinPlot({
|
|
272
|
+
id: config.childIds.cloud,
|
|
273
|
+
data: config.source,
|
|
274
|
+
coordinate: config.coordinate,
|
|
275
|
+
x: positions.x,
|
|
276
|
+
y: positions.y,
|
|
277
|
+
...(config.color === undefined ? {} : { color: config.color }),
|
|
278
|
+
density: { ...density, side: densitySide(config.orientation, config.side) },
|
|
279
|
+
...(area === undefined ? {} : { area }),
|
|
280
|
+
guides: false
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
next = addSummary(next, config, positions);
|
|
284
|
+
next = addPoints(next, config, positions);
|
|
285
|
+
const ownerId = [config.childIds.cloud, config.childIds.summary, config.childIds.points]
|
|
286
|
+
.find(id => findLayer(next, id) !== undefined);
|
|
287
|
+
if (ownerId === undefined) throw new Error("Raincloud requires at least one enabled component.");
|
|
288
|
+
const guideTarget = [config.childIds.points, config.childIds.cloud, config.childIds.summary]
|
|
289
|
+
.find(id => findLayer(next, id) !== undefined);
|
|
290
|
+
next = applyFacadeGuides(next, config.guides, guideTarget, config.guides);
|
|
291
|
+
const ownedChildIds = [config.childIds.cloud, config.childIds.summary, config.childIds.points]
|
|
292
|
+
.filter(id => findLayer(next, id) !== undefined);
|
|
293
|
+
return next._withMarkConfig(ownerId, {
|
|
294
|
+
...next.markConfigs[ownerId],
|
|
295
|
+
raincloudPlot: { ...config, ownerId, ownedChildIds, materialized: true }
|
|
296
|
+
})._withContext({ currentMark: ownerId, currentData: config.source });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function alignExistingRoleScales(program, config) {
|
|
300
|
+
let next = program;
|
|
301
|
+
for (const encoding of [config.category, config.value, config.color]) {
|
|
302
|
+
const scale = encoding?.scale;
|
|
303
|
+
if (!isPlainObject(scale) || findSemanticScale(next, scale.id) === undefined) continue;
|
|
304
|
+
const patch = Object.fromEntries(Object.entries(scale).filter(
|
|
305
|
+
([property, value]) => property !== "id" && value !== undefined
|
|
306
|
+
));
|
|
307
|
+
if (Object.keys(patch).length > 0) next = next.editScale({ id: scale.id, ...patch });
|
|
308
|
+
}
|
|
309
|
+
return next;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function normalizeCreate(program, args, operation) {
|
|
313
|
+
const id = resolveFacadeId(program, args.id, { defaultId: "raincloudPlot", operation });
|
|
314
|
+
if (Object.values(program.markConfigs).some(value => value.raincloudPlot?.id === id)) {
|
|
315
|
+
throw new Error(`Raincloud plot "${id}" already exists.`);
|
|
316
|
+
}
|
|
317
|
+
const source = resolveFacadeData(program, args.data, operation);
|
|
318
|
+
const dataset = findDataset(program, source);
|
|
319
|
+
const orientation = normalizeOrientation(args.orientation);
|
|
320
|
+
if (args.category === undefined || args.value === undefined) {
|
|
321
|
+
throw new Error(`${operation} requires category and value.`);
|
|
322
|
+
}
|
|
323
|
+
const raw = orientation === "vertical"
|
|
324
|
+
? resolveViolinRoles(dataset, args.category, args.value, operation)
|
|
325
|
+
: resolveViolinRoles(dataset, args.value, args.category, operation);
|
|
326
|
+
if (raw.orientation !== orientation) {
|
|
327
|
+
throw new Error(`${operation} category and value do not match orientation ${orientation}.`);
|
|
328
|
+
}
|
|
329
|
+
const category = roleScale(
|
|
330
|
+
raw.category,
|
|
331
|
+
`${id}Category`,
|
|
332
|
+
`${operation} category`,
|
|
333
|
+
{ type: "band" }
|
|
334
|
+
);
|
|
335
|
+
const value = roleScale(raw.value, `${id}Value`, `${operation} value`);
|
|
336
|
+
const density = normalizeDensity(args.density);
|
|
337
|
+
const summary = normalizeSummary(args.summary);
|
|
338
|
+
const points = normalizePoints(args.points);
|
|
339
|
+
if ([density, summary, points].every(component => component === false)) {
|
|
340
|
+
throw new Error("Raincloud requires at least one enabled component.");
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
id,
|
|
344
|
+
source,
|
|
345
|
+
...(args.coordinate === undefined ? {} : { coordinate: args.coordinate }),
|
|
346
|
+
category,
|
|
347
|
+
value,
|
|
348
|
+
orientation,
|
|
349
|
+
side: normalizeSide(args.side),
|
|
350
|
+
density,
|
|
351
|
+
summary,
|
|
352
|
+
points,
|
|
353
|
+
color: normalizeColor(args.color, category, id),
|
|
354
|
+
guides: normalizeGuides(args.guides, operation),
|
|
355
|
+
childIds: {
|
|
356
|
+
cloud: `${id}Cloud`,
|
|
357
|
+
summary: `${id}Summary`,
|
|
358
|
+
points: `${id}Points`
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function raincloudOwner(program, requested) {
|
|
364
|
+
const owners = Object.entries(program.markConfigs)
|
|
365
|
+
.filter(([, value]) => value.raincloudPlot?.materialized === true)
|
|
366
|
+
.map(([id]) => id);
|
|
367
|
+
if (requested !== undefined) {
|
|
368
|
+
const target = validateUserId(requested, "Raincloud target");
|
|
369
|
+
const owner = owners.find(id => id === target || program.markConfigs[id].raincloudPlot.id === target);
|
|
370
|
+
if (owner === undefined) throw new Error(`Unknown Raincloud plot "${target}".`);
|
|
371
|
+
return owner;
|
|
372
|
+
}
|
|
373
|
+
if (owners.includes(program.context.currentMark)) return program.context.currentMark;
|
|
374
|
+
if (owners.length === 1) return owners[0];
|
|
375
|
+
if (owners.length === 0) throw new Error("editRaincloudPlot requires a Raincloud plot.");
|
|
376
|
+
throw new Error("editRaincloudPlot target is ambiguous; provide target.");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export const createRaincloudPlot = action(
|
|
380
|
+
{ op: "createRaincloudPlot", description: "Create a half-density, summary, and raw-point distribution plot." },
|
|
381
|
+
function (args = {}) {
|
|
382
|
+
const operation = "createRaincloudPlot";
|
|
383
|
+
validateFacadeOptions(args, CREATE_OPTIONS, operation);
|
|
384
|
+
return buildRaincloud(this, normalizeCreate(this, args, operation));
|
|
385
|
+
}
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
export const editRaincloudPlot = action(
|
|
389
|
+
{ op: "editRaincloudPlot", description: "Revise one Raincloud plot's shared source and statistical roles." },
|
|
390
|
+
function (args = {}) {
|
|
391
|
+
const operation = "editRaincloudPlot";
|
|
392
|
+
validateFacadeOptions(args, EDIT_OPTIONS, operation);
|
|
393
|
+
if (!EDIT_OPTIONS.slice(1).some(key => Object.hasOwn(args, key))) {
|
|
394
|
+
throw new Error(`${operation} requires at least one Raincloud option.`);
|
|
395
|
+
}
|
|
396
|
+
const ownerId = raincloudOwner(this, args.target);
|
|
397
|
+
const current = this.markConfigs[ownerId].raincloudPlot;
|
|
398
|
+
const candidateArgs = {
|
|
399
|
+
id: current.id,
|
|
400
|
+
data: Object.hasOwn(args, "data") ? args.data : current.source,
|
|
401
|
+
...(current.coordinate === undefined ? {} : { coordinate: current.coordinate }),
|
|
402
|
+
category: Object.hasOwn(args, "category") ? args.category : current.category,
|
|
403
|
+
value: Object.hasOwn(args, "value") ? args.value : current.value,
|
|
404
|
+
orientation: args.orientation ?? current.orientation,
|
|
405
|
+
side: args.side ?? current.side,
|
|
406
|
+
density: Object.hasOwn(args, "density")
|
|
407
|
+
? normalizeDensity(args.density, current.density)
|
|
408
|
+
: current.density,
|
|
409
|
+
summary: Object.hasOwn(args, "summary")
|
|
410
|
+
? normalizeSummary(args.summary, current.summary)
|
|
411
|
+
: current.summary,
|
|
412
|
+
points: Object.hasOwn(args, "points")
|
|
413
|
+
? normalizePoints(args.points, current.points)
|
|
414
|
+
: current.points,
|
|
415
|
+
color: Object.hasOwn(args, "color") ? args.color : current.color,
|
|
416
|
+
guides: current.guides
|
|
417
|
+
};
|
|
418
|
+
const withoutCurrent = this.removeMark({ target: ownerId });
|
|
419
|
+
const revised = normalizeCreate(withoutCurrent, candidateArgs, operation);
|
|
420
|
+
return buildRaincloud(alignExistingRoleScales(withoutCurrent, revised), revised);
|
|
421
|
+
}
|
|
422
|
+
);
|