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 { applyFacadeGuides } from "../charts/shared.js";
|
|
1
2
|
import { action } from "../../core/action.js";
|
|
2
3
|
import { validateUserId } from "../../core/identifiers.js";
|
|
3
4
|
import {
|
|
@@ -190,7 +191,7 @@ export const materializeBoxPlot = action(
|
|
|
190
191
|
}
|
|
191
192
|
});
|
|
192
193
|
if (config.guides !== false) {
|
|
193
|
-
next = next
|
|
194
|
+
next = applyFacadeGuides(next, config.guides, ownerId);
|
|
194
195
|
}
|
|
195
196
|
return next._withContext({ currentMark: ownerId, currentData: source });
|
|
196
197
|
}
|
|
@@ -161,7 +161,9 @@ export const createBasicCanvas = action(
|
|
|
161
161
|
background: Object.hasOwn(args, "background")
|
|
162
162
|
? args.background
|
|
163
163
|
: DEFAULT_CANVAS.background,
|
|
164
|
-
margin:
|
|
164
|
+
margin: Object.hasOwn(args, "margin")
|
|
165
|
+
? normalizeMargin(args.margin, DEFAULT_MARGIN)
|
|
166
|
+
: DEFAULT_MARGIN,
|
|
165
167
|
size: {
|
|
166
168
|
width: Object.hasOwn(args, "width") ? "explicit" : "auto",
|
|
167
169
|
height: Object.hasOwn(args, "height") ? "explicit" : "auto"
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import {
|
|
3
|
+
validateNonNegativeFinite,
|
|
4
|
+
validatePositiveFinite,
|
|
5
|
+
validateOptionObject
|
|
6
|
+
} from "../../core/validation.js";
|
|
7
|
+
import { resolveGraphicBounds } from "../../layout/canvas.js";
|
|
8
|
+
import { findCanvasGraphic } from
|
|
9
|
+
"../../materialization/graphicHierarchy.js";
|
|
10
|
+
|
|
11
|
+
const OPTIONS = Object.freeze([
|
|
12
|
+
"padding",
|
|
13
|
+
"minPlotWidth",
|
|
14
|
+
"minPlotHeight",
|
|
15
|
+
"iterationLimit",
|
|
16
|
+
"overflow"
|
|
17
|
+
]);
|
|
18
|
+
const SIDES = Object.freeze(["top", "right", "bottom", "left"]);
|
|
19
|
+
const DEFAULTS = Object.freeze({
|
|
20
|
+
padding: 0,
|
|
21
|
+
minPlotWidth: 160,
|
|
22
|
+
minPlotHeight: 120,
|
|
23
|
+
iterationLimit: 32,
|
|
24
|
+
overflow: "error"
|
|
25
|
+
});
|
|
26
|
+
const UNITS_PER_PIXEL = 4;
|
|
27
|
+
|
|
28
|
+
function normalizeOptions(args) {
|
|
29
|
+
validateOptionObject(args, OPTIONS, "fitCanvas", { allowEmpty: true });
|
|
30
|
+
const policy = { ...DEFAULTS, ...args };
|
|
31
|
+
validateNonNegativeFinite(policy.padding, "fitCanvas padding");
|
|
32
|
+
validatePositiveFinite(policy.minPlotWidth, "fitCanvas minPlotWidth");
|
|
33
|
+
validatePositiveFinite(policy.minPlotHeight, "fitCanvas minPlotHeight");
|
|
34
|
+
if (!Number.isInteger(policy.iterationLimit) ||
|
|
35
|
+
policy.iterationLimit <= 0 || policy.iterationLimit > 64) {
|
|
36
|
+
throw new RangeError(
|
|
37
|
+
"fitCanvas iterationLimit must be an integer from 1 through 64."
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
if (!["error", "report"].includes(policy.overflow)) {
|
|
41
|
+
throw new Error(`Unsupported fitCanvas overflow policy "${policy.overflow}".`);
|
|
42
|
+
}
|
|
43
|
+
return policy;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function probe(program, margin) {
|
|
47
|
+
try {
|
|
48
|
+
return { ok: true, program: program.editCanvas({ margin }) };
|
|
49
|
+
} catch (error) {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
message: error instanceof Error ? error.message : String(error)
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function marginWith(margin, side, value) {
|
|
58
|
+
return { ...margin, [side]: value };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function minimizeSide(program, margin, side, policy) {
|
|
62
|
+
const current = margin[side];
|
|
63
|
+
const floor = Math.ceil(policy.padding * UNITS_PER_PIXEL) /
|
|
64
|
+
UNITS_PER_PIXEL;
|
|
65
|
+
if (floor > current) {
|
|
66
|
+
const expanded = marginWith(margin, side, floor);
|
|
67
|
+
const result = probe(program, expanded);
|
|
68
|
+
return result.ok
|
|
69
|
+
? { program: result.program, margin: expanded, iterations: 1, issues: [] }
|
|
70
|
+
: {
|
|
71
|
+
program,
|
|
72
|
+
margin,
|
|
73
|
+
iterations: 1,
|
|
74
|
+
issues: [`${side} padding: ${result.message}`]
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let low = Math.ceil(policy.padding * UNITS_PER_PIXEL);
|
|
79
|
+
let high = Math.floor(current * UNITS_PER_PIXEL);
|
|
80
|
+
if (low > high) {
|
|
81
|
+
return { program, margin, iterations: 0, issues: [] };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const cache = new Map();
|
|
85
|
+
const test = units => {
|
|
86
|
+
if (cache.has(units)) return cache.get(units);
|
|
87
|
+
const result = probe(
|
|
88
|
+
program,
|
|
89
|
+
marginWith(margin, side, units / UNITS_PER_PIXEL)
|
|
90
|
+
);
|
|
91
|
+
cache.set(units, result);
|
|
92
|
+
return result;
|
|
93
|
+
};
|
|
94
|
+
let iterations = 0;
|
|
95
|
+
const upper = test(high);
|
|
96
|
+
iterations += 1;
|
|
97
|
+
if (!upper.ok) {
|
|
98
|
+
return { program, margin, iterations, issues: [] };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
while (low < high && iterations < policy.iterationLimit) {
|
|
102
|
+
const middle = Math.floor((low + high) / 2);
|
|
103
|
+
const result = test(middle);
|
|
104
|
+
iterations += 1;
|
|
105
|
+
if (result.ok) high = middle;
|
|
106
|
+
else low = middle + 1;
|
|
107
|
+
}
|
|
108
|
+
const best = test(high);
|
|
109
|
+
const fittedMargin = marginWith(
|
|
110
|
+
margin,
|
|
111
|
+
side,
|
|
112
|
+
high / UNITS_PER_PIXEL
|
|
113
|
+
);
|
|
114
|
+
return {
|
|
115
|
+
program: best.program,
|
|
116
|
+
margin: fittedMargin,
|
|
117
|
+
iterations,
|
|
118
|
+
issues: low === high
|
|
119
|
+
? []
|
|
120
|
+
: [`${side} fitting reached iterationLimit ${policy.iterationLimit}`]
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function fitMargins(program, policy) {
|
|
125
|
+
let probeProgram = program;
|
|
126
|
+
let margin = { ...program.materializationConfigs.canvas.margin };
|
|
127
|
+
let iterations = 0;
|
|
128
|
+
const issues = [];
|
|
129
|
+
for (const side of SIDES) {
|
|
130
|
+
const result = minimizeSide(probeProgram, margin, side, policy);
|
|
131
|
+
probeProgram = result.program;
|
|
132
|
+
margin = result.margin;
|
|
133
|
+
iterations += result.iterations;
|
|
134
|
+
issues.push(...result.issues);
|
|
135
|
+
}
|
|
136
|
+
return { margin, iterations, issues };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function sameMargin(first, second) {
|
|
140
|
+
return SIDES.every(side => first[side] === second[side]);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function samePolicy(first, second) {
|
|
144
|
+
return OPTIONS.every(option => first?.[option] === second[option]);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function layoutSignature(program) {
|
|
148
|
+
const source = JSON.stringify({
|
|
149
|
+
graphicSpec: program.graphicSpec,
|
|
150
|
+
canvas: program.materializationConfigs.canvas,
|
|
151
|
+
guides: program.materializationConfigs.guides,
|
|
152
|
+
title: program.materializationConfigs.title
|
|
153
|
+
});
|
|
154
|
+
let hash = 0xcbf29ce484222325n;
|
|
155
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
156
|
+
hash ^= BigInt(source.charCodeAt(index));
|
|
157
|
+
hash = BigInt.asUintN(64, hash * 0x100000001b3n);
|
|
158
|
+
}
|
|
159
|
+
return hash.toString(16).padStart(16, "0");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export const fitCanvas = action(
|
|
163
|
+
{
|
|
164
|
+
op: "fitCanvas",
|
|
165
|
+
description: "Fit Canvas margins around existing layout resources."
|
|
166
|
+
},
|
|
167
|
+
function (args = {}) {
|
|
168
|
+
const policy = normalizeOptions(args);
|
|
169
|
+
const canvas = findCanvasGraphic(this);
|
|
170
|
+
if (canvas?.type !== "canvas" ||
|
|
171
|
+
this.materializationConfigs.canvas?.margin === undefined) {
|
|
172
|
+
throw new Error("fitCanvas requires an existing Canvas.");
|
|
173
|
+
}
|
|
174
|
+
const previous = this.materializationConfigs.fitting;
|
|
175
|
+
if (samePolicy(previous?.policy, policy) &&
|
|
176
|
+
previous?.result?.signature === layoutSignature(this)) {
|
|
177
|
+
return this;
|
|
178
|
+
}
|
|
179
|
+
const sourceMargin = this.materializationConfigs.canvas.margin;
|
|
180
|
+
const fitted = fitMargins(this, policy);
|
|
181
|
+
let next = sameMargin(sourceMargin, fitted.margin)
|
|
182
|
+
? this
|
|
183
|
+
: this.editCanvas({ margin: fitted.margin });
|
|
184
|
+
const plot = resolveGraphicBounds(next);
|
|
185
|
+
if (plot.width < policy.minPlotWidth) {
|
|
186
|
+
fitted.issues.push(
|
|
187
|
+
`plot width ${plot.width} is smaller than minPlotWidth ${policy.minPlotWidth}`
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
if (plot.height < policy.minPlotHeight) {
|
|
191
|
+
fitted.issues.push(
|
|
192
|
+
`plot height ${plot.height} is smaller than minPlotHeight ${policy.minPlotHeight}`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
if (fitted.issues.length > 0 && policy.overflow === "error") {
|
|
196
|
+
throw new Error(`fitCanvas overflow: ${fitted.issues.join("; ")}.`);
|
|
197
|
+
}
|
|
198
|
+
next = next._withMaterializationConfig(["fitting"], {
|
|
199
|
+
policy,
|
|
200
|
+
result: {
|
|
201
|
+
status: fitted.issues.length === 0 ? "fit" : "overflow",
|
|
202
|
+
margin: fitted.margin,
|
|
203
|
+
plot: { width: plot.width, height: plot.height },
|
|
204
|
+
iterations: fitted.iterations,
|
|
205
|
+
issues: fitted.issues,
|
|
206
|
+
signature: layoutSignature(next)
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
return next;
|
|
210
|
+
}
|
|
211
|
+
);
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { createBasicCanvas, createCanvas, editCanvas } from "./actions.js";
|
|
2
|
+
import { fitCanvas } from "./fitting.js";
|
|
2
3
|
|
|
3
4
|
export function registerCanvasActions(ProgramClass) {
|
|
4
5
|
ProgramClass.prototype.editCanvas = editCanvas;
|
|
5
6
|
ProgramClass.prototype.createCanvas = createCanvas;
|
|
7
|
+
ProgramClass.prototype.fitCanvas = fitCanvas;
|
|
6
8
|
}
|
|
7
9
|
|
|
8
10
|
export function registerBasicCanvasActions(ProgramClass) {
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { getMarkMaterializationStep } from "../../materialization/marks/index.js";
|
|
12
12
|
import { buildMaterializationPlan } from "../../materialization/planner.js";
|
|
13
13
|
import {
|
|
14
|
+
CATEGORY_ORDER_CHANNELS,
|
|
14
15
|
normalizeCategoryOrder,
|
|
15
16
|
resolveCategoryOrder
|
|
16
17
|
} from "../../grammar/categoryOrder.js";
|
|
@@ -19,8 +20,8 @@ const ORDER_OPTIONS = Object.freeze(["target", "channel", "values", "by", "direc
|
|
|
19
20
|
const REMOVE_OPTIONS = Object.freeze(["target", "channel"]);
|
|
20
21
|
|
|
21
22
|
function resolveAssignment(program, args, operation, { activeOnly = false } = {}) {
|
|
22
|
-
if (!
|
|
23
|
-
throw new Error(`${operation} channel must be x or
|
|
23
|
+
if (!CATEGORY_ORDER_CHANNELS.includes(args.channel)) {
|
|
24
|
+
throw new Error(`${operation} channel must be x, y, or theta.`);
|
|
24
25
|
}
|
|
25
26
|
const requested = args.target === undefined
|
|
26
27
|
? undefined
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import { normalizeGroupFields } from "../../grammar/pathSeries.js";
|
|
3
|
+
import { normalizeAreaBound, validateAreaEndpointPair } from "../../grammar/areaEndpoints.js";
|
|
4
|
+
import {
|
|
5
|
+
applyFacadeGuides, normalizeAppearance, normalizeCategoricalColor, normalizeCategoricalGuides,
|
|
6
|
+
normalizeFieldEncoding, omitUndefinedOptions, positionArgs, resolveFacadeData, resolveFacadeId,
|
|
7
|
+
validateFacadeOptions
|
|
8
|
+
} from "./shared.js";
|
|
9
|
+
|
|
10
|
+
const OPERATION = "createAreaPlot";
|
|
11
|
+
const OPTIONS = ["id", "data", "coordinate", "x", "y", "valueChannel", "baseline", "groupBy", "layout", "missing", "color", "area", "guides"];
|
|
12
|
+
|
|
13
|
+
export const createAreaPlot = action({ op: OPERATION, description: "Create a simple, ranged, or stacked Cartesian area plot." }, function (args = {}) {
|
|
14
|
+
validateFacadeOptions(args, OPTIONS, OPERATION);
|
|
15
|
+
for (const [key, value] of Object.entries(args)) if (value === null) throw new TypeError(`${OPERATION} ${key} cannot be null.`);
|
|
16
|
+
const id = resolveFacadeId(this, args.id, { defaultId: "areaPlot", operation: OPERATION });
|
|
17
|
+
const data = resolveFacadeData(this, args.data, OPERATION);
|
|
18
|
+
const valueChannel = args.valueChannel ?? "y";
|
|
19
|
+
if (!["x", "y"].includes(valueChannel)) throw new Error("createAreaPlot valueChannel must be x or y.");
|
|
20
|
+
const independentChannel = valueChannel === "y" ? "x" : "y";
|
|
21
|
+
const independent = omitUndefinedOptions(normalizeFieldEncoding(args[independentChannel], `${OPERATION} ${independentChannel}`));
|
|
22
|
+
validateFacadeOptions(independent, ["field", "fieldType", "temporalUnit", "scale"], `${OPERATION} independent position`);
|
|
23
|
+
if (independent.fieldType !== undefined && !["quantitative", "temporal"].includes(independent.fieldType)) {
|
|
24
|
+
throw new Error("createAreaPlot independent position must be quantitative or temporal.");
|
|
25
|
+
}
|
|
26
|
+
const measurement = omitUndefinedOptions(normalizeFieldEncoding(args[valueChannel], `${OPERATION} ${valueChannel}`));
|
|
27
|
+
const ranged = Object.hasOwn(measurement, "lower") || Object.hasOwn(measurement, "upper");
|
|
28
|
+
validateFacadeOptions(measurement, ranged ? ["lower", "upper", "scale"] : ["field", "scale"], `${OPERATION} measurement`);
|
|
29
|
+
if (ranged && args.baseline !== undefined) throw new Error("createAreaPlot baseline cannot be combined with a range.");
|
|
30
|
+
const lower = ranged ? measurement.lower : measurement.field;
|
|
31
|
+
const upper = ranged ? measurement.upper : { datum: args.baseline ?? 0 };
|
|
32
|
+
validateAreaEndpointPair(normalizeAreaBound(lower), normalizeAreaBound(upper));
|
|
33
|
+
const groupBy = args.groupBy === undefined ? undefined : normalizeGroupFields(args.groupBy);
|
|
34
|
+
const area = omitUndefinedOptions(normalizeAppearance(args.area, ["fill", "opacity", "stroke", "strokeWidth", "curve"], `${OPERATION} area`));
|
|
35
|
+
const color = args.color === undefined ? undefined : normalizeCategoricalColor(args.color, `${OPERATION} color`);
|
|
36
|
+
if (color !== undefined && area.fill !== undefined) throw new Error("createAreaPlot area.fill cannot be combined with color.");
|
|
37
|
+
const guides = normalizeCategoricalGuides(args.guides, OPERATION, color);
|
|
38
|
+
let next = this.createAreaMark({ id, data, ...area, missing: args.missing ?? "error" });
|
|
39
|
+
if (groupBy !== undefined) next = next.encodeGroup({ target: id, fields: groupBy });
|
|
40
|
+
next = next[independentChannel === "x" ? "encodeX" : "encodeY"](positionArgs(independent, { target: id, coordinate: args.coordinate }))
|
|
41
|
+
[valueChannel === "y" ? "encodeYRange" : "encodeXRange"]({ target: id, lower, upper,
|
|
42
|
+
...(measurement.scale === undefined ? {} : { scale: measurement.scale }),
|
|
43
|
+
...(args.coordinate === undefined ? {} : { coordinate: args.coordinate }) })
|
|
44
|
+
.layoutSeries({ target: id, mode: args.layout ?? "overlay" });
|
|
45
|
+
if (color !== undefined) next = next.encodeColor({ target: id, ...color });
|
|
46
|
+
return applyFacadeGuides(next, guides, id, args.guides ?? {});
|
|
47
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { action } from "../../core/action.js";
|
|
2
|
+
import { isBarCategoryEncoding } from "../../grammar/bars/policy.js";
|
|
2
3
|
import {
|
|
3
4
|
applyFacadeGuides,
|
|
4
5
|
inferFacadeFieldType,
|
|
@@ -42,12 +43,17 @@ export const createBarPlot = action(
|
|
|
42
43
|
const width = normalizeTargetOptions(args.width, "createBarPlot width");
|
|
43
44
|
const guides = normalizeGuides(args.guides, "createBarPlot");
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
const positions = isBarCategoryEncoding(y)
|
|
47
|
+
? [["encodeY", y], ["encodeX", x]]
|
|
48
|
+
: [["encodeX", x], ["encodeY", y]];
|
|
49
|
+
let next = this.createBarMark({ id, data, ...bar });
|
|
50
|
+
for (const [operation, encoding] of positions) {
|
|
51
|
+
next = next[operation](positionArgs(encoding, {
|
|
52
|
+
target: id, coordinate: args.coordinate
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
49
55
|
if (color !== undefined) next = next.encodeColor(targetArgs(color, id));
|
|
50
56
|
if (width !== undefined) next = next.encodeBarWidth(targetArgs(width, id));
|
|
51
|
-
return applyFacadeGuides(next, guides);
|
|
57
|
+
return applyFacadeGuides(next, guides, id);
|
|
52
58
|
}
|
|
53
59
|
);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import { isPlainObject } from "../../core/immutable.js";
|
|
3
|
+
import { validateOptionObject } from "../../core/validation.js";
|
|
4
|
+
import { findLayer } from "../../selectors/layers.js";
|
|
5
|
+
import { resolveFacadeId, validateFacadeOptions } from "./shared.js";
|
|
6
|
+
|
|
7
|
+
const OPTIONS = Object.freeze([
|
|
8
|
+
"id", "data", "coordinate", "x", "y", "color", "size", "shape",
|
|
9
|
+
"point", "packing", "guides"
|
|
10
|
+
]);
|
|
11
|
+
const PACKING_OPTIONS = Object.freeze(["maxOffset", "padding", "key", "overflow"]);
|
|
12
|
+
|
|
13
|
+
function ownerScale(value, id, label) {
|
|
14
|
+
if (typeof value === "string") return { field: value, scale: { id } };
|
|
15
|
+
if (!isPlainObject(value)) return value;
|
|
16
|
+
if (value.scale !== undefined && !isPlainObject(value.scale)) {
|
|
17
|
+
throw new TypeError(`${label}.scale must be a plain object.`);
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
...value,
|
|
21
|
+
scale: { ...(value.scale ?? {}), id: value.scale?.id ?? id }
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizePacking(value, operation) {
|
|
26
|
+
if (value === false) return false;
|
|
27
|
+
const packing = value ?? {};
|
|
28
|
+
validateOptionObject(packing, PACKING_OPTIONS, `${operation} packing`);
|
|
29
|
+
return { ...packing };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const createBeeswarmPlot = action(
|
|
33
|
+
{
|
|
34
|
+
op: "createBeeswarmPlot",
|
|
35
|
+
description: "Create a Point plot packed within categorical slots."
|
|
36
|
+
},
|
|
37
|
+
function (args = {}) {
|
|
38
|
+
const operation = "createBeeswarmPlot";
|
|
39
|
+
validateFacadeOptions(args, OPTIONS, operation);
|
|
40
|
+
if (args.x === undefined || args.y === undefined) {
|
|
41
|
+
throw new Error(`${operation} requires x and y.`);
|
|
42
|
+
}
|
|
43
|
+
const id = resolveFacadeId(this, args.id, {
|
|
44
|
+
defaultId: "beeswarmPlot",
|
|
45
|
+
operation
|
|
46
|
+
});
|
|
47
|
+
const packing = normalizePacking(args.packing, operation);
|
|
48
|
+
let next = this.createStripPlot({
|
|
49
|
+
id,
|
|
50
|
+
...(args.data === undefined ? {} : { data: args.data }),
|
|
51
|
+
...(args.coordinate === undefined ? {} : { coordinate: args.coordinate }),
|
|
52
|
+
x: ownerScale(args.x, `${id}X`, `${operation} x`),
|
|
53
|
+
y: ownerScale(args.y, `${id}Y`, `${operation} y`),
|
|
54
|
+
...(args.color === undefined ? {} : { color: args.color }),
|
|
55
|
+
...(args.size === undefined ? {} : { size: args.size }),
|
|
56
|
+
...(args.shape === undefined ? {} : { shape: args.shape }),
|
|
57
|
+
...(args.point === undefined ? {} : { point: args.point }),
|
|
58
|
+
...(args.guides === undefined ? {} : { guides: args.guides })
|
|
59
|
+
});
|
|
60
|
+
if (packing === false) return next;
|
|
61
|
+
const layer = findLayer(next, id);
|
|
62
|
+
const categoryChannel = ["x", "y"].find(channel =>
|
|
63
|
+
["nominal", "ordinal"].includes(layer?.encoding?.[channel]?.fieldType)
|
|
64
|
+
);
|
|
65
|
+
if (categoryChannel === undefined) {
|
|
66
|
+
throw new Error(`${operation} requires exactly one categorical position.`);
|
|
67
|
+
}
|
|
68
|
+
return next.packPoints({ target: id, channel: categoryChannel, ...packing });
|
|
69
|
+
}
|
|
70
|
+
);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import { validateNonEmptyString, validateOptionObject } from "../../core/validation.js";
|
|
3
|
+
import {
|
|
4
|
+
applyFacadeGuides, normalizeAppearance, normalizeCategoricalColor, normalizeCategoricalGuides,
|
|
5
|
+
omitUndefinedOptions, resolveFacadeData, resolveFacadeId, validateFacadeOptions
|
|
6
|
+
} from "./shared.js";
|
|
7
|
+
|
|
8
|
+
const OPERATION = "createDensityPlot";
|
|
9
|
+
const OPTIONS = ["id", "data", "coordinate", "field", "groupBy", "bandwidth", "extent", "steps",
|
|
10
|
+
"kernel", "normalization", "as", "densityChannel", "valueScale", "densityScale", "color", "area", "guides"];
|
|
11
|
+
const AREA_OPTIONS = ["fill", "opacity", "stroke", "strokeWidth", "curve"];
|
|
12
|
+
|
|
13
|
+
export const createDensityPlot = action({
|
|
14
|
+
op: OPERATION, description: "Create a baseline kernel-density area plot with optional explicit groups."
|
|
15
|
+
}, function (args = {}) {
|
|
16
|
+
validateFacadeOptions(args, OPTIONS, OPERATION);
|
|
17
|
+
const id = resolveFacadeId(this, args.id, { defaultId: "densityPlot", operation: OPERATION });
|
|
18
|
+
const data = resolveFacadeData(this, args.data, OPERATION);
|
|
19
|
+
validateNonEmptyString(args.field, `${OPERATION} field`);
|
|
20
|
+
const { id: requestedId, data: requestedData, color: requestedColor, area: requestedArea,
|
|
21
|
+
guides: requestedGuides, ...density } = omitUndefinedOptions(args);
|
|
22
|
+
const area = omitUndefinedOptions(normalizeAppearance(requestedArea, AREA_OPTIONS, `${OPERATION} area`));
|
|
23
|
+
for (const [key, value] of Object.entries({ ...density, ...area })) {
|
|
24
|
+
if (value === null) throw new TypeError(`${OPERATION} ${key} cannot be null.`);
|
|
25
|
+
}
|
|
26
|
+
for (const key of ["valueScale", "densityScale"]) {
|
|
27
|
+
if (density[key] === undefined) continue;
|
|
28
|
+
validateOptionObject(density[key], undefined, `${OPERATION} ${key}`);
|
|
29
|
+
density[key] = omitUndefinedOptions(density[key]);
|
|
30
|
+
}
|
|
31
|
+
const color = requestedColor === undefined ? undefined
|
|
32
|
+
: normalizeCategoricalColor(requestedColor, `${OPERATION} color`, ["field", "fieldType", "scale", "palette", "layout"]);
|
|
33
|
+
if (color !== undefined && (color.field !== density.groupBy ||
|
|
34
|
+
color.layout !== undefined && color.layout !== "overlay")) {
|
|
35
|
+
throw new Error(`${OPERATION} color requires the same explicit groupBy field and overlay layout.`);
|
|
36
|
+
}
|
|
37
|
+
if (color !== undefined && area.fill !== undefined) {
|
|
38
|
+
throw new Error(`${OPERATION} area.fill cannot be combined with color.`);
|
|
39
|
+
}
|
|
40
|
+
const guides = normalizeCategoricalGuides(requestedGuides, OPERATION, color);
|
|
41
|
+
let next = this.createAreaMark({ id, data, ...area }).encodeDensity({ ...density, target: id });
|
|
42
|
+
if (color !== undefined) next = next.encodeColor({ ...color, target: id });
|
|
43
|
+
return applyFacadeGuides(next, guides, id, requestedGuides ?? {});
|
|
44
|
+
});
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import { validateUserId } from "../../core/identifiers.js";
|
|
3
|
+
import { validateNonEmptyString, validateOptionObject } from "../../core/validation.js";
|
|
4
|
+
import { normalizeGroupFields } from "../../grammar/pathSeries.js";
|
|
5
|
+
import {
|
|
6
|
+
applyFacadeGuides,
|
|
7
|
+
normalizeAppearance,
|
|
8
|
+
normalizeCategoricalColor,
|
|
9
|
+
normalizeGuides,
|
|
10
|
+
resolveFacadeData,
|
|
11
|
+
resolveFacadeId,
|
|
12
|
+
targetArgs,
|
|
13
|
+
validateFacadeOptions
|
|
14
|
+
} from "./shared.js";
|
|
15
|
+
|
|
16
|
+
const OPTIONS = Object.freeze([
|
|
17
|
+
"id", "data", "coordinate", "field", "groupBy", "weight", "missing",
|
|
18
|
+
"as", "color", "line", "labels", "guides"
|
|
19
|
+
]);
|
|
20
|
+
const EDIT_OPTIONS = Object.freeze([
|
|
21
|
+
"target", "data", "coordinate", "field", "groupBy", "weight", "missing", "as", "color"
|
|
22
|
+
]);
|
|
23
|
+
const LINE_OPTIONS = Object.freeze(["strokeWidth", "stroke", "opacity"]);
|
|
24
|
+
const LABEL_OPTIONS = Object.freeze([
|
|
25
|
+
"field", "value", "content", "normalizeBy", "format", "layout", "fill",
|
|
26
|
+
"opacity", "fontSize", "fontFamily", "fontWeight", "align", "baseline",
|
|
27
|
+
"rotation", "dx", "dy"
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
function normalizeGrouping(value) {
|
|
31
|
+
return value === undefined ? [] : normalizeGroupFields(value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeLabels(value, operation) {
|
|
35
|
+
if (value === undefined || value === false) return false;
|
|
36
|
+
validateOptionObject(value, LABEL_OPTIONS, `${operation} labels`);
|
|
37
|
+
return { ...value };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizeOutputs(value, id, operation) {
|
|
41
|
+
if (value === undefined) {
|
|
42
|
+
return {
|
|
43
|
+
value: `__${id}ECDFData_value`,
|
|
44
|
+
cumulative: `__${id}ECDFData_cumulative`,
|
|
45
|
+
probability: `__${id}ECDFData_probability`
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
validateOptionObject(value, ["value", "cumulative", "probability"], `${operation} as`);
|
|
49
|
+
for (const field of ["value", "cumulative", "probability"]) {
|
|
50
|
+
validateNonEmptyString(value[field], `${operation} as.${field}`);
|
|
51
|
+
}
|
|
52
|
+
return { ...value };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function resolveOwner(program, requested) {
|
|
56
|
+
const eligible = program.semanticSpec.layers.filter(
|
|
57
|
+
layer => program.markConfigs[layer.id]?.ecdfPlot !== undefined
|
|
58
|
+
);
|
|
59
|
+
if (requested !== undefined) {
|
|
60
|
+
const id = validateUserId(requested, "ECDF-plot owner id");
|
|
61
|
+
const owner = eligible.find(layer => layer.id === id);
|
|
62
|
+
if (owner === undefined) throw new Error(`Unknown ECDF-plot owner "${id}".`);
|
|
63
|
+
return owner;
|
|
64
|
+
}
|
|
65
|
+
const current = eligible.find(layer => layer.id === program.context.currentMark);
|
|
66
|
+
if (current !== undefined) return current;
|
|
67
|
+
if (eligible.length === 1) return eligible[0];
|
|
68
|
+
if (eligible.length === 0) throw new Error("editECDFPlot requires an ECDF plot.");
|
|
69
|
+
throw new Error("editECDFPlot target is ambiguous; provide target.");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const createECDFPlot = action(
|
|
73
|
+
{
|
|
74
|
+
op: "createECDFPlot",
|
|
75
|
+
description: "Create a right-continuous empirical cumulative distribution plot."
|
|
76
|
+
},
|
|
77
|
+
function (args = {}) {
|
|
78
|
+
const operation = "createECDFPlot";
|
|
79
|
+
validateFacadeOptions(args, OPTIONS, operation);
|
|
80
|
+
const id = resolveFacadeId(this, args.id, { defaultId: "ecdfPlot", operation });
|
|
81
|
+
const source = resolveFacadeData(this, args.data, operation);
|
|
82
|
+
validateNonEmptyString(args.field, `${operation} field`);
|
|
83
|
+
if (args.weight !== undefined) validateNonEmptyString(args.weight, `${operation} weight`);
|
|
84
|
+
const groupBy = normalizeGrouping(args.groupBy);
|
|
85
|
+
const as = normalizeOutputs(args.as, id, operation);
|
|
86
|
+
const color = args.color === undefined
|
|
87
|
+
? undefined
|
|
88
|
+
: normalizeCategoricalColor(args.color, `${operation} color`);
|
|
89
|
+
if (color !== undefined && !groupBy.includes(color.field)) {
|
|
90
|
+
throw new Error(`${operation} color field must be included in groupBy.`);
|
|
91
|
+
}
|
|
92
|
+
const line = normalizeAppearance(args.line, LINE_OPTIONS, `${operation} line`);
|
|
93
|
+
const labels = normalizeLabels(args.labels, operation);
|
|
94
|
+
const guides = normalizeGuides(args.guides, operation);
|
|
95
|
+
const dataId = `${id}ECDFData`;
|
|
96
|
+
|
|
97
|
+
let next = this
|
|
98
|
+
.createECDFData({
|
|
99
|
+
id: dataId,
|
|
100
|
+
source,
|
|
101
|
+
field: args.field,
|
|
102
|
+
...(groupBy.length === 0 ? {} : { groupBy }),
|
|
103
|
+
...(args.weight === undefined ? {} : { weight: args.weight }),
|
|
104
|
+
...(args.missing === undefined ? {} : { missing: args.missing }),
|
|
105
|
+
as
|
|
106
|
+
})
|
|
107
|
+
.createLineMark({ id, data: dataId, curve: "step-after", ...line })
|
|
108
|
+
.encodeX({
|
|
109
|
+
target: id,
|
|
110
|
+
field: as.value,
|
|
111
|
+
fieldType: "quantitative",
|
|
112
|
+
scale: { id: `${id}Value`, zero: false, nice: false },
|
|
113
|
+
...(args.coordinate === undefined ? {} : { coordinate: args.coordinate })
|
|
114
|
+
})
|
|
115
|
+
.encodeY({
|
|
116
|
+
target: id,
|
|
117
|
+
field: as.probability,
|
|
118
|
+
fieldType: "quantitative",
|
|
119
|
+
scale: { id: `${id}Probability`, domain: [0, 1], zero: true, nice: false },
|
|
120
|
+
...(args.coordinate === undefined ? {} : { coordinate: args.coordinate })
|
|
121
|
+
});
|
|
122
|
+
if (groupBy.length > 0) next = next.encodeGroup({ target: id, fields: groupBy });
|
|
123
|
+
if (color !== undefined) next = next.encodeColor(targetArgs(color, id));
|
|
124
|
+
if (labels !== false) {
|
|
125
|
+
const hasContent = ["field", "value", "content"].some(key => Object.hasOwn(labels, key));
|
|
126
|
+
next = next.createMarkLabels({
|
|
127
|
+
id: `${id}Label`,
|
|
128
|
+
source: id,
|
|
129
|
+
...labels,
|
|
130
|
+
...(hasContent ? {} : { field: as.probability })
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
next = applyFacadeGuides(next, guides, id, guides);
|
|
134
|
+
return next._withMarkConfig(id, {
|
|
135
|
+
...next.markConfigs[id],
|
|
136
|
+
ecdfPlot: {
|
|
137
|
+
source,
|
|
138
|
+
data: dataId,
|
|
139
|
+
options: {
|
|
140
|
+
id,
|
|
141
|
+
data: source,
|
|
142
|
+
coordinate: args.coordinate,
|
|
143
|
+
field: args.field,
|
|
144
|
+
...(groupBy.length === 0 ? {} : { groupBy }),
|
|
145
|
+
...(args.weight === undefined ? {} : { weight: args.weight }),
|
|
146
|
+
missing: args.missing ?? "drop",
|
|
147
|
+
as,
|
|
148
|
+
...(color === undefined ? {} : { color }),
|
|
149
|
+
line,
|
|
150
|
+
labels,
|
|
151
|
+
guides
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
export const editECDFPlot = action(
|
|
159
|
+
{
|
|
160
|
+
op: "editECDFPlot",
|
|
161
|
+
description: "Atomically revise an ECDF plot's source or statistical roles."
|
|
162
|
+
},
|
|
163
|
+
function (args = {}) {
|
|
164
|
+
validateFacadeOptions(args, EDIT_OPTIONS, "editECDFPlot");
|
|
165
|
+
const changes = Object.keys(args).filter(key => key !== "target");
|
|
166
|
+
if (changes.length === 0) {
|
|
167
|
+
throw new Error("editECDFPlot requires at least one ECDF option.");
|
|
168
|
+
}
|
|
169
|
+
const owner = resolveOwner(this, args.target);
|
|
170
|
+
const current = this.markConfigs[owner.id].ecdfPlot;
|
|
171
|
+
const revised = { ...current.options };
|
|
172
|
+
for (const key of changes) {
|
|
173
|
+
if (key === "groupBy" && args[key] === false) {
|
|
174
|
+
delete revised.groupBy;
|
|
175
|
+
if (!Object.hasOwn(args, "color")) delete revised.color;
|
|
176
|
+
}
|
|
177
|
+
else if (key === "weight" && args[key] === false) delete revised.weight;
|
|
178
|
+
else if (key === "color" && args[key] === false) delete revised.color;
|
|
179
|
+
else revised[key] = args[key];
|
|
180
|
+
}
|
|
181
|
+
return this.removeMark({ target: owner.id }).createECDFPlot(revised);
|
|
182
|
+
}
|
|
183
|
+
);
|