ggaction 0.0.2 → 0.0.3
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 +28 -0
- package/README.md +12 -1
- package/package.json +6 -1
- package/src/actions/boxPlots/components.js +3 -3
- package/src/actions/boxPlots/edit.js +277 -0
- package/src/actions/boxPlots/index.js +2 -0
- package/src/actions/boxPlots/materialize.js +12 -1
- package/src/actions/boxPlots/options.js +25 -14
- package/src/actions/canvas/actions.js +21 -17
- package/src/actions/composition/actions.js +247 -0
- package/src/actions/composition/index.js +1 -0
- package/src/actions/coordinates/actions.js +2 -5
- package/src/actions/data/density.js +7 -1
- package/src/actions/data/filter.js +3 -1
- package/src/actions/encodings/appearance.js +11 -0
- package/src/actions/encodings/barWidth.js +5 -3
- package/src/actions/encodings/color.js +120 -25
- package/src/actions/encodings/index.js +2 -0
- package/src/actions/encodings/offset.js +90 -72
- package/src/actions/encodings/position/apply.js +66 -0
- package/src/actions/encodings/position/policies/arc.js +26 -0
- package/src/actions/encodings/position/policies/bar.js +9 -5
- package/src/actions/encodings/position/policies/index.js +5 -1
- package/src/actions/encodings/position/policies/line.js +20 -3
- package/src/actions/encodings/position/resolve.js +57 -8
- package/src/actions/encodings/position.js +44 -90
- package/src/actions/encodings/ranged.js +28 -6
- package/src/actions/encodings/shared.js +17 -21
- package/src/actions/encodings/text.js +73 -0
- package/src/actions/errorBands/create.js +37 -29
- package/src/actions/errorBands/edit.js +206 -0
- package/src/actions/errorBands/index.js +9 -0
- package/src/actions/errorBands/options.js +39 -0
- package/src/actions/errorBars/create.js +50 -43
- package/src/actions/errorBars/edit.js +130 -0
- package/src/actions/errorBars/index.js +3 -0
- package/src/actions/errorBars/options.js +52 -0
- package/src/actions/facets/actions.js +162 -0
- package/src/actions/facets/derive.js +200 -0
- package/src/actions/facets/guides.js +16 -0
- package/src/actions/facets/index.js +1 -0
- package/src/actions/facets/replay.js +69 -0
- package/src/actions/guides/applicability.js +113 -0
- package/src/actions/guides/axes/axes.js +70 -16
- package/src/actions/guides/axes/edit.js +126 -0
- package/src/actions/guides/axes/index.js +12 -0
- package/src/actions/guides/axes/labels.js +21 -18
- package/src/actions/guides/axes/remove.js +60 -0
- package/src/actions/guides/axes/tickGroups.js +7 -5
- package/src/actions/guides/axes/ticks.js +9 -4
- package/src/actions/guides/axes/titles.js +7 -5
- package/src/actions/guides/grids/grid.js +144 -15
- package/src/actions/guides/grids/resolve.js +5 -13
- package/src/actions/guides/guides.js +17 -75
- package/src/actions/guides/index.js +6 -0
- package/src/actions/guides/legends/categorical/actions.js +48 -9
- package/src/actions/guides/legends/categorical/components.js +16 -16
- package/src/actions/guides/legends/categorical/index.js +14 -14
- package/src/actions/guides/legends/categorical/layout.js +3 -1
- package/src/actions/guides/legends/categorical/options.js +2 -5
- package/src/actions/guides/legends/categorical/resolve.js +2 -2
- package/src/actions/guides/legends/categorical/symbols.js +14 -14
- package/src/actions/guides/legends/continuous/common.js +7 -7
- package/src/actions/guides/legends/continuous/interval.js +5 -5
- package/src/actions/guides/legends/edit.js +2 -6
- package/src/actions/guides/legends/focused.js +63 -0
- package/src/actions/guides/legends/index.js +4 -0
- package/src/actions/guides/legends/remove.js +64 -0
- package/src/actions/guides/legends/size.js +12 -14
- package/src/actions/guides/polar/axes/facade.js +219 -0
- package/src/actions/guides/polar/axes/labels.js +188 -0
- package/src/actions/guides/polar/axes/lines.js +113 -0
- package/src/actions/guides/polar/axes/shared.js +112 -0
- package/src/actions/guides/polar/axes/ticks.js +164 -0
- package/src/actions/guides/polar/axes/titles.js +165 -0
- package/src/actions/guides/polar/axes.js +55 -0
- package/src/actions/guides/polar/grids.js +252 -0
- package/src/actions/guides/polar/index.js +2 -0
- package/src/actions/guides/polar/resolve.js +269 -0
- package/src/actions/index.js +4 -0
- package/src/actions/marks/arc/actions.js +239 -0
- package/src/actions/marks/arc/index.js +1 -0
- package/src/actions/marks/{area.js → area/actions.js} +51 -38
- package/src/actions/marks/area/index.js +4 -0
- package/src/actions/marks/bar/create.js +26 -5
- package/src/actions/marks/bar/materialize.js +25 -31
- package/src/actions/marks/index.js +12 -4
- package/src/actions/marks/lifecycle.js +27 -0
- package/src/actions/marks/line/actions.js +373 -0
- package/src/actions/marks/line/index.js +1 -0
- package/src/actions/marks/{point.js → point/actions.js} +67 -108
- package/src/actions/marks/point/index.js +1 -0
- package/src/actions/marks/rect/actions.js +165 -0
- package/src/actions/marks/rect/index.js +1 -0
- package/src/actions/marks/remove.js +214 -0
- package/src/actions/marks/{rule.js → rule/actions.js} +34 -28
- package/src/actions/marks/rule/index.js +1 -0
- package/src/actions/marks/shared.js +156 -25
- package/src/actions/marks/text/actions.js +192 -0
- package/src/actions/marks/text/index.js +1 -0
- package/src/actions/primitives/createGraphics.js +13 -6
- package/src/actions/primitives/editGraphics.js +10 -3
- package/src/actions/primitives/semantic.js +26 -3
- package/src/actions/primitives/semanticValue.js +25 -3
- package/src/actions/regression/components.js +1 -1
- package/src/actions/regression/create.js +18 -1
- package/src/actions/regression/edit.js +247 -0
- package/src/actions/regression/index.js +3 -0
- package/src/actions/regression/resolve.js +3 -1
- package/src/actions/scales/consumers.js +15 -6
- package/src/actions/scales/definitions.js +13 -4
- package/src/actions/scales/edit.js +42 -19
- package/src/actions/scales/materialize.js +57 -408
- package/src/actions/selection/actions.js +38 -24
- package/src/actions/selection/index.js +2 -0
- package/src/actions/titles/actions.js +63 -2
- package/src/actions/titles/index.js +2 -0
- package/src/actions/titles/resolve.js +12 -17
- package/src/composition.js +10 -0
- package/src/core/ChartProgram.js +200 -2
- package/src/core/action.js +10 -0
- package/src/core/validation.js +16 -0
- package/src/core/vocabulary.js +154 -4
- package/src/grammar/arcs.js +154 -0
- package/src/grammar/bars/aggregate.js +2 -2
- package/src/grammar/bars/geometry.js +3 -3
- package/src/grammar/bars/policy.js +7 -1
- package/src/grammar/coordinates.js +3 -7
- package/src/grammar/density.js +20 -1
- package/src/grammar/facets/dependencies.js +185 -0
- package/src/grammar/facets/guides.js +257 -0
- package/src/grammar/facets/scales.js +254 -0
- package/src/grammar/facets.js +122 -0
- package/src/grammar/lineSeries.js +111 -3
- package/src/grammar/polar.js +102 -0
- package/src/grammar/polarGuides.js +243 -0
- package/src/grammar/polarLineCommands.js +43 -0
- package/src/grammar/polarPaths.js +161 -0
- package/src/grammar/positionCompatibility.js +18 -2
- package/src/grammar/rects.js +26 -0
- package/src/grammar/scales/continuous.js +6 -1
- package/src/grammar/scales/fields.js +7 -0
- package/src/grammar/scales/ordinal.js +5 -2
- package/src/grammar/scales/policies.js +1 -1
- package/src/grammar/schemas/graphic.js +1 -1
- package/src/grammar/schemas/graphicBounds.js +64 -9
- package/src/grammar/schemas/graphicTree.js +44 -10
- package/src/grammar/schemas/semanticPath.js +31 -9
- package/src/grammar/seriesLayout.js +13 -2
- package/src/grammar/text.js +71 -0
- package/src/grammar/ticks.js +50 -13
- package/src/index.js +1 -0
- package/src/layout/composition.js +310 -0
- package/src/layout/facets.js +131 -0
- package/src/layout/title.js +6 -0
- package/src/materialization/bars/aggregate.js +5 -2
- package/src/materialization/bars/grouped.js +75 -37
- package/src/materialization/composition.js +152 -0
- package/src/materialization/compositionSnapshot.js +114 -0
- package/src/materialization/dependencies.js +71 -16
- package/src/materialization/encodings.js +14 -28
- package/src/materialization/facetGuides.js +426 -0
- package/src/materialization/facets.js +214 -0
- package/src/materialization/graphicHierarchy.js +15 -5
- package/src/materialization/guides/resources.js +46 -0
- package/src/materialization/marks.js +220 -14
- package/src/materialization/planner.js +105 -12
- package/src/materialization/rect.js +138 -0
- package/src/materialization/rectConfig.js +38 -0
- package/src/materialization/rowEncoding.js +33 -0
- package/src/materialization/scales/map.js +40 -0
- package/src/materialization/scales/policies/arc.js +37 -0
- package/src/materialization/scales/policies/bar.js +132 -0
- package/src/materialization/scales/policies/series.js +76 -0
- package/src/materialization/scales/resolve.js +326 -0
- package/src/materialization/selection/items/arc.js +29 -0
- package/src/materialization/selection/items/bar.js +255 -0
- package/src/materialization/selection/items/common.js +131 -0
- package/src/materialization/selection/items/index.js +6 -0
- package/src/materialization/selection/items/path.js +59 -0
- package/src/materialization/selection/items/point.js +55 -0
- package/src/materialization/selection/items/rect.js +23 -0
- package/src/materialization/selection/items/rule.js +25 -0
- package/src/materialization/selection/policies/arc.js +10 -0
- package/src/materialization/selection/policies/area.js +1 -1
- package/src/materialization/selection/policies/bar.js +1 -1
- package/src/materialization/selection/policies/index.js +5 -1
- package/src/materialization/selection/policies/line.js +1 -1
- package/src/materialization/selection/policies/point.js +1 -1
- package/src/materialization/selection/policies/rect.js +10 -0
- package/src/materialization/selection/policies/rule.js +1 -1
- package/src/materialization/selection/styles.js +24 -16
- package/src/materialization/text.js +156 -0
- package/src/renderers/canvas/index.js +68 -22
- package/types/extension.d.ts +1 -0
- package/types/index.d.ts +56 -1
- package/types/program.d.ts +664 -27
- package/src/actions/marks/line.js +0 -248
- package/src/materialization/selection/items.js +0 -503
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `ggaction` are recorded in this file.
|
|
4
4
|
|
|
5
|
+
## Unreleased
|
|
6
|
+
|
|
7
|
+
## [0.0.3] - 2026-07-19
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Added complete Polar point, line/radar, arc/donut/rose/radial-bar authoring with theta/radius guides, selection,
|
|
12
|
+
highlighting, and Canvas/PNG rendering.
|
|
13
|
+
- Added immutable horizontal and vertical program composition, nested child snapshots, layout editing, stable child
|
|
14
|
+
replacement, and Cartesian facets with derived-data replay, scale resolution, outer axes, and shared legends.
|
|
15
|
+
- Added text and rect marks, directional offsets, horizontal grouped bars, and compatible shared temporal bar/line
|
|
16
|
+
position inference.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Nested compositions now preserve their intrinsic layout and honor outer cross-axis alignment instead of stretching
|
|
21
|
+
internal cells or leaving unequal snapshots pinned to the start edge.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- Corrected zero-baseline and signed geometry for aggregate bars, and materialized complete ranged or aggregate bars
|
|
26
|
+
with their documented default width.
|
|
27
|
+
- Made horizontal error bands compose with color and explicit boundaries in the same supported cases as vertical bands.
|
|
28
|
+
- Added standalone point-size legends and interval-aware temporal axis labels without duplicate automatic tick text.
|
|
29
|
+
- Published the exact `createDerivedData` transform-array contract and stable `ChartProgram` state-inspection paths in
|
|
30
|
+
TypeScript and user documentation.
|
|
31
|
+
|
|
5
32
|
## [0.0.2] - 2026-07-17
|
|
6
33
|
|
|
7
34
|
### Fixed
|
|
@@ -31,5 +58,6 @@ All notable changes to `ggaction` are recorded in this file.
|
|
|
31
58
|
- Cartesian charts are the complete current path. Polar semantic tokens exist only where explicitly documented and do
|
|
32
59
|
not imply complete polar rendering.
|
|
33
60
|
|
|
61
|
+
[0.0.3]: https://github.com/hj-n/ggaction/releases/tag/v0.0.3
|
|
34
62
|
[0.0.2]: https://github.com/hj-n/ggaction/releases/tag/v0.0.2
|
|
35
63
|
[0.0.1]: https://github.com/hj-n/ggaction/releases/tag/v0.0.1
|
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ render(program, document.querySelector("#chart").getContext("2d"));
|
|
|
37
37
|
The renderer reads only fully materialized, backend-neutral graphics. Semantic
|
|
38
38
|
state is never automatically compiled during rendering.
|
|
39
39
|
|
|
40
|
-
> **Status:** `0.0.
|
|
40
|
+
> **Status:** `0.0.3` is the current experimental public release. APIs may change
|
|
41
41
|
> before `1.0.0`; changes are recorded in the [changelog](./CHANGELOG.md).
|
|
42
42
|
|
|
43
43
|
## Install
|
|
@@ -96,6 +96,17 @@ Tukey/min–max ranges, configurable factor and component appearance, and
|
|
|
96
96
|
optional outlier rendering from categorical and quantitative field pairs.
|
|
97
97
|
The [mark-selection example](https://github.com/hj-n/ggaction/tree/main/examples/mark-selection/) compares grouped point,
|
|
98
98
|
complete stacked-bar, and line-series highlighting through one selector grammar.
|
|
99
|
+
The [program-composition example](https://github.com/hj-n/ggaction/tree/main/examples/program-composition/) combines
|
|
100
|
+
complete child programs, edits their inferred layout, and replaces one stable slot.
|
|
101
|
+
The [facet example](https://github.com/hj-n/ggaction/tree/main/examples/cars-origin-scatterplot-facet/)
|
|
102
|
+
repeats one chart with `.facet({ field: "Origin" })`, channel-level shared or
|
|
103
|
+
independent scales, per-cell or occupied-outer axes, and an optional compatible
|
|
104
|
+
parent-owned legend. Regression, density, interval, and box-derived data are
|
|
105
|
+
recomputed independently inside each cell.
|
|
106
|
+
Complete Cartesian and Polar programs can be nested with `hconcat` and
|
|
107
|
+
`vconcat`; the [cross-feature dashboard](https://github.com/hj-n/ggaction/tree/main/examples/cross-feature-dashboard/)
|
|
108
|
+
shows a Polar child replacement beside a Cartesian facet. Polar sources cannot
|
|
109
|
+
currently be faceted and fail before partial child state is created.
|
|
99
110
|
Completed programs can also be exported through
|
|
100
111
|
[`ggaction/png`](https://hyeonword.com/ggaction/api/rendering/#png-output).
|
|
101
112
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ggaction",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "Build charts through immutable, traceable graphical actions.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Hyeon Jeon",
|
|
@@ -59,6 +59,11 @@
|
|
|
59
59
|
"artifacts:gallery:test": "node scripts/test-roadmap-gallery.js",
|
|
60
60
|
"docs:images": "node scripts/generate-doc-images.js",
|
|
61
61
|
"docs:llms": "node scripts/generate-llm-docs.js",
|
|
62
|
+
"docs:signatures": "node scripts/generate-doc-signatures.js",
|
|
63
|
+
"docs:signatures:check": "node scripts/generate-doc-signatures.js --check",
|
|
64
|
+
"docs:generate": "npm run docs:signatures && npm run docs:images && npm run docs:llms",
|
|
65
|
+
"docs:build": "bundle exec jekyll build --source docs --destination _site --baseurl /ggaction",
|
|
66
|
+
"docs:verify": "npm run docs:generate && npm run test:docs && npm run docs:build && npm run test:docs:built && npm run test:docs:browser",
|
|
62
67
|
"contracts:catalog": "node scripts/generate-action-catalog.js",
|
|
63
68
|
"contracts:catalog:check": "node scripts/generate-action-catalog.js --check",
|
|
64
69
|
"package:check": "node scripts/package-artifact.js --check",
|
|
@@ -64,7 +64,8 @@ export const createBoxOutliers = action(
|
|
|
64
64
|
let next = this.createPointMark({
|
|
65
65
|
id: args.id,
|
|
66
66
|
data: args.data,
|
|
67
|
-
shape: args.shape
|
|
67
|
+
shape: args.shape,
|
|
68
|
+
fill: "#111111"
|
|
68
69
|
});
|
|
69
70
|
next = next[categoryAction]({
|
|
70
71
|
target: args.id,
|
|
@@ -82,7 +83,6 @@ export const createBoxOutliers = action(
|
|
|
82
83
|
});
|
|
83
84
|
return next
|
|
84
85
|
.encodeRadius({ target: args.id, value: args.radius })
|
|
85
|
-
.encodeOpacity({ target: args.id, value: args.opacity })
|
|
86
|
-
.editGraphics({ target: args.id, property: "fill", value: "#111111" });
|
|
86
|
+
.encodeOpacity({ target: args.id, value: args.opacity });
|
|
87
87
|
}
|
|
88
88
|
);
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import { isPlainObject } from "../../core/immutable.js";
|
|
3
|
+
import { validateUserId } from "../../core/identifiers.js";
|
|
4
|
+
import { validateKeys } from "../../core/validation.js";
|
|
5
|
+
import { BOX_FIELDS, deriveBoxData, normalizeBoxTransform } from
|
|
6
|
+
"../../grammar/boxPlot.js";
|
|
7
|
+
import { hasDataset } from "../../selectors/index.js";
|
|
8
|
+
import { findDataset } from "../../selectors/datasets.js";
|
|
9
|
+
import { findLayer } from "../../selectors/layers.js";
|
|
10
|
+
import {
|
|
11
|
+
resolveBoxAppearance,
|
|
12
|
+
resolveBoxMedianAppearance,
|
|
13
|
+
resolveBoxOutlierAppearance,
|
|
14
|
+
resolveBoxWhisker,
|
|
15
|
+
resolveBoxWidth
|
|
16
|
+
} from "./options.js";
|
|
17
|
+
|
|
18
|
+
const OPTIONS = Object.freeze([
|
|
19
|
+
"target", "whisker", "width", "outliers", "box", "median", "outlier"
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
function resolveBoxOwner(program, requested) {
|
|
23
|
+
const eligible = program.semanticSpec.layers.filter(
|
|
24
|
+
layer => program.markConfigs[layer.id]?.boxPlot?.materialized === true
|
|
25
|
+
);
|
|
26
|
+
if (requested !== undefined) {
|
|
27
|
+
const id = validateUserId(requested, "Box-plot owner id");
|
|
28
|
+
const layer = findLayer(program, id);
|
|
29
|
+
if (layer === undefined || !eligible.includes(layer)) {
|
|
30
|
+
throw new Error(`Unknown box-plot owner "${id}".`);
|
|
31
|
+
}
|
|
32
|
+
return layer;
|
|
33
|
+
}
|
|
34
|
+
const current = findLayer(program, program.context.currentMark);
|
|
35
|
+
if (current !== undefined && eligible.includes(current)) return current;
|
|
36
|
+
if (eligible.length === 1) return eligible[0];
|
|
37
|
+
if (eligible.length === 0) throw new Error("No box-plot owner is available.");
|
|
38
|
+
throw new Error("Box-plot owner is ambiguous; provide target.");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function requirePatch(value, label) {
|
|
42
|
+
if (!isPlainObject(value)) {
|
|
43
|
+
throw new TypeError(`editBoxPlot ${label} must be a plain object.`);
|
|
44
|
+
}
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resolveEditedWhisker(current, value) {
|
|
49
|
+
if (value === undefined) return current;
|
|
50
|
+
const patch = requirePatch(value, "whisker");
|
|
51
|
+
validateKeys(patch, ["type", "factor"], "editBoxPlot whisker");
|
|
52
|
+
const type = patch.type ?? current.type;
|
|
53
|
+
const candidate = type === "minmax"
|
|
54
|
+
? { type, ...(Object.hasOwn(patch, "factor") ? { factor: patch.factor } : {}) }
|
|
55
|
+
: {
|
|
56
|
+
type,
|
|
57
|
+
factor: Object.hasOwn(patch, "factor")
|
|
58
|
+
? patch.factor
|
|
59
|
+
: current.type === "tukey" ? current.factor : 1.5
|
|
60
|
+
};
|
|
61
|
+
return resolveBoxWhisker(candidate, "editBoxPlot");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function nextRevisionId(program, ownerId, kind) {
|
|
65
|
+
let revision = 1;
|
|
66
|
+
while (hasDataset(program, `${ownerId}${kind}Revision${revision}`)) {
|
|
67
|
+
revision += 1;
|
|
68
|
+
}
|
|
69
|
+
return `${ownerId}${kind}Revision${revision}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function removeOwnedMark(program, id) {
|
|
73
|
+
return program
|
|
74
|
+
.editSemantic({ property: `layer[${id}]`, remove: true })
|
|
75
|
+
.editGraphics({ target: id, remove: true })
|
|
76
|
+
._withoutMaterializationConfig(["marks", id]);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function rebindRuleData(program, id, data) {
|
|
80
|
+
return program.editSemantic({ property: `layer[${id}].data`, value: data });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const editBoxPlot = action(
|
|
84
|
+
{
|
|
85
|
+
op: "editBoxPlot",
|
|
86
|
+
description: "Revise one box plot and its owned components."
|
|
87
|
+
},
|
|
88
|
+
function (args = {}) {
|
|
89
|
+
validateKeys(args, OPTIONS, "editBoxPlot");
|
|
90
|
+
if (!OPTIONS.slice(1).some(key => Object.hasOwn(args, key))) {
|
|
91
|
+
throw new Error("editBoxPlot requires at least one box-plot option.");
|
|
92
|
+
}
|
|
93
|
+
const owner = resolveBoxOwner(this, args.target);
|
|
94
|
+
const current = this.markConfigs[owner.id].boxPlot;
|
|
95
|
+
const whisker = resolveEditedWhisker(current.whisker, args.whisker);
|
|
96
|
+
const width = Object.hasOwn(args, "width")
|
|
97
|
+
? resolveBoxWidth(args.width, "editBoxPlot")
|
|
98
|
+
: current.width;
|
|
99
|
+
if (Object.hasOwn(args, "outliers") && typeof args.outliers !== "boolean") {
|
|
100
|
+
throw new TypeError("editBoxPlot outliers must be a boolean.");
|
|
101
|
+
}
|
|
102
|
+
const outliers = args.outliers ?? current.outliers;
|
|
103
|
+
const boxPatch = Object.hasOwn(args, "box")
|
|
104
|
+
? requirePatch(args.box, "box")
|
|
105
|
+
: {};
|
|
106
|
+
const medianPatch = Object.hasOwn(args, "median")
|
|
107
|
+
? requirePatch(args.median, "median")
|
|
108
|
+
: {};
|
|
109
|
+
const outlierPatch = Object.hasOwn(args, "outlier")
|
|
110
|
+
? requirePatch(args.outlier, "outlier")
|
|
111
|
+
: {};
|
|
112
|
+
const box = resolveBoxAppearance(
|
|
113
|
+
{ ...current.box, ...boxPatch },
|
|
114
|
+
"editBoxPlot"
|
|
115
|
+
);
|
|
116
|
+
const median = resolveBoxMedianAppearance(
|
|
117
|
+
{ ...current.median, ...medianPatch },
|
|
118
|
+
"editBoxPlot"
|
|
119
|
+
);
|
|
120
|
+
const outlier = resolveBoxOutlierAppearance(
|
|
121
|
+
{ ...current.outlier, ...outlierPatch },
|
|
122
|
+
"editBoxPlot"
|
|
123
|
+
);
|
|
124
|
+
if (Object.hasOwn(boxPatch, "fill") && owner.encoding?.color !== undefined) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
"editBoxPlot box.fill cannot be combined with a color encoding."
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const revisesData = JSON.stringify(whisker) !== JSON.stringify(current.whisker) ||
|
|
131
|
+
outliers !== current.outliers;
|
|
132
|
+
const changesBox = Object.hasOwn(args, "box") ||
|
|
133
|
+
Object.hasOwn(args, "width") || revisesData;
|
|
134
|
+
const changesMedian = Object.hasOwn(args, "median") ||
|
|
135
|
+
Object.hasOwn(args, "width") || revisesData;
|
|
136
|
+
const changesOutlier = Object.hasOwn(args, "outlier") || revisesData;
|
|
137
|
+
const categoryEncoding = current.orientation === "vertical"
|
|
138
|
+
? owner.encoding.x
|
|
139
|
+
: owner.encoding.y;
|
|
140
|
+
const measureEncoding = current.orientation === "vertical"
|
|
141
|
+
? owner.encoding.y
|
|
142
|
+
: owner.encoding.x;
|
|
143
|
+
let summaryId = current.summaryId;
|
|
144
|
+
let outlierDataId = current.outlierDataId;
|
|
145
|
+
let next = this;
|
|
146
|
+
|
|
147
|
+
const sourceRows = findDataset(this, current.source).values;
|
|
148
|
+
const derived = deriveBoxData(sourceRows, normalizeBoxTransform({
|
|
149
|
+
type: "boxSummary",
|
|
150
|
+
category: current.category,
|
|
151
|
+
field: current.measure,
|
|
152
|
+
whisker: whisker.type,
|
|
153
|
+
...(whisker.factor === undefined ? {} : { factor: whisker.factor })
|
|
154
|
+
}));
|
|
155
|
+
const hasOutliers = outliers && whisker.type === "tukey" &&
|
|
156
|
+
derived.outliers.length > 0;
|
|
157
|
+
|
|
158
|
+
if (revisesData) {
|
|
159
|
+
summaryId = nextRevisionId(this, owner.id, "SummaryData");
|
|
160
|
+
next = next.createBoxSummaryData({
|
|
161
|
+
id: summaryId,
|
|
162
|
+
source: current.source,
|
|
163
|
+
category: current.category,
|
|
164
|
+
field: current.measure,
|
|
165
|
+
whisker: whisker.type,
|
|
166
|
+
...(whisker.factor === undefined ? {} : { factor: whisker.factor })
|
|
167
|
+
});
|
|
168
|
+
if (hasOutliers) {
|
|
169
|
+
outlierDataId = nextRevisionId(this, owner.id, "OutlierData");
|
|
170
|
+
next = next.createBoxOutlierData({
|
|
171
|
+
id: outlierDataId,
|
|
172
|
+
source: current.source,
|
|
173
|
+
category: current.category,
|
|
174
|
+
field: current.measure,
|
|
175
|
+
whisker: whisker.type,
|
|
176
|
+
factor: whisker.factor
|
|
177
|
+
});
|
|
178
|
+
} else {
|
|
179
|
+
outlierDataId = undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
next = next.editSemantic({
|
|
183
|
+
property: `layer[${owner.id}].data`,
|
|
184
|
+
value: summaryId
|
|
185
|
+
});
|
|
186
|
+
const whiskerConfig = next.markConfigs[current.whiskerId];
|
|
187
|
+
const capIds = [
|
|
188
|
+
whiskerConfig.errorBar.lowerCapId,
|
|
189
|
+
whiskerConfig.errorBar.upperCapId
|
|
190
|
+
].filter(id => id !== undefined);
|
|
191
|
+
next = rebindRuleData(next, current.whiskerId, summaryId)
|
|
192
|
+
._withMarkConfig(current.whiskerId, {
|
|
193
|
+
...whiskerConfig,
|
|
194
|
+
errorBar: { ...whiskerConfig.errorBar, data: summaryId }
|
|
195
|
+
});
|
|
196
|
+
for (const capId of capIds) {
|
|
197
|
+
next = rebindRuleData(next, capId, summaryId);
|
|
198
|
+
}
|
|
199
|
+
next = rebindRuleData(next, current.medianId, summaryId);
|
|
200
|
+
|
|
201
|
+
const hadOutlierLayer = findLayer(next, current.outlierId) !== undefined;
|
|
202
|
+
if (hadOutlierLayer && !hasOutliers) {
|
|
203
|
+
next = removeOwnedMark(next, current.outlierId);
|
|
204
|
+
} else if (hadOutlierLayer && hasOutliers) {
|
|
205
|
+
next = next.editSemantic({
|
|
206
|
+
property: `layer[${current.outlierId}].data`,
|
|
207
|
+
value: outlierDataId
|
|
208
|
+
});
|
|
209
|
+
} else if (!hadOutlierLayer && hasOutliers) {
|
|
210
|
+
next = next.createBoxOutliers({
|
|
211
|
+
id: current.outlierId,
|
|
212
|
+
data: outlierDataId,
|
|
213
|
+
category: current.category,
|
|
214
|
+
categoryType: categoryEncoding.fieldType,
|
|
215
|
+
measure: current.measure,
|
|
216
|
+
orientation: current.orientation,
|
|
217
|
+
coordinate: owner.coordinate,
|
|
218
|
+
categoryScale: categoryEncoding.scale,
|
|
219
|
+
measureScale: measureEncoding.scale,
|
|
220
|
+
shape: outlier.shape,
|
|
221
|
+
radius: outlier.radius,
|
|
222
|
+
opacity: outlier.opacity
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
next = next._withMarkConfig(owner.id, {
|
|
228
|
+
...next.markConfigs[owner.id],
|
|
229
|
+
boxPlot: {
|
|
230
|
+
...current,
|
|
231
|
+
whisker,
|
|
232
|
+
width,
|
|
233
|
+
outliers,
|
|
234
|
+
box,
|
|
235
|
+
median,
|
|
236
|
+
outlier,
|
|
237
|
+
summaryId,
|
|
238
|
+
outlierDataId
|
|
239
|
+
},
|
|
240
|
+
barWidth: { band: width },
|
|
241
|
+
fill: box.fill,
|
|
242
|
+
opacity: box.opacity,
|
|
243
|
+
stroke: box.stroke,
|
|
244
|
+
strokeWidth: box.strokeWidth
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
if (changesBox) next = next.rematerializeBarMark({ id: owner.id });
|
|
248
|
+
if (revisesData) {
|
|
249
|
+
next = next.rematerializeErrorBar({ id: current.whiskerId });
|
|
250
|
+
}
|
|
251
|
+
if (changesMedian) {
|
|
252
|
+
next = next
|
|
253
|
+
.encodeStroke({ target: current.medianId, value: median.stroke })
|
|
254
|
+
.encodeStrokeWidth({
|
|
255
|
+
target: current.medianId,
|
|
256
|
+
value: median.strokeWidth
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
if (findLayer(next, current.outlierId) !== undefined && changesOutlier) {
|
|
260
|
+
next = next
|
|
261
|
+
.editPointMark({
|
|
262
|
+
target: current.outlierId,
|
|
263
|
+
shape: outlier.shape,
|
|
264
|
+
opacity: outlier.opacity
|
|
265
|
+
})
|
|
266
|
+
.encodeRadius({ target: current.outlierId, value: outlier.radius });
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (revisesData) {
|
|
270
|
+
next = next.releaseDerivedData({ id: current.summaryId });
|
|
271
|
+
if (current.outlierDataId !== undefined) {
|
|
272
|
+
next = next.releaseDerivedData({ id: current.outlierDataId });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return next._withContext({ currentMark: owner.id, currentData: current.source });
|
|
276
|
+
}
|
|
277
|
+
);
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { createBoxMedian, createBoxOutliers } from "./components.js";
|
|
2
2
|
import { createBoxPlot } from "./create.js";
|
|
3
|
+
import { editBoxPlot } from "./edit.js";
|
|
3
4
|
import { materializeBoxPlot } from "./materialize.js";
|
|
4
5
|
|
|
5
6
|
export function registerBoxPlotActions(ProgramClass) {
|
|
6
7
|
ProgramClass.prototype.createBoxPlot = createBoxPlot;
|
|
8
|
+
ProgramClass.prototype.editBoxPlot = editBoxPlot;
|
|
7
9
|
ProgramClass.prototype.materializeBoxPlot = materializeBoxPlot;
|
|
8
10
|
ProgramClass.prototype.createBoxMedian = createBoxMedian;
|
|
9
11
|
ProgramClass.prototype.createBoxOutliers = createBoxOutliers;
|
|
@@ -50,7 +50,11 @@ export const materializeBoxPlot = action(
|
|
|
50
50
|
source,
|
|
51
51
|
orientation,
|
|
52
52
|
category: category.field,
|
|
53
|
-
measure: measure.field
|
|
53
|
+
measure: measure.field,
|
|
54
|
+
summaryId,
|
|
55
|
+
whiskerId,
|
|
56
|
+
medianId,
|
|
57
|
+
outlierId
|
|
54
58
|
},
|
|
55
59
|
barWidth: { band: config.width },
|
|
56
60
|
fill: config.box.fill,
|
|
@@ -178,6 +182,13 @@ export const materializeBoxPlot = action(
|
|
|
178
182
|
opacity: config.outlier.opacity
|
|
179
183
|
});
|
|
180
184
|
}
|
|
185
|
+
next = next._withMarkConfig(ownerId, {
|
|
186
|
+
...next.markConfigs[ownerId],
|
|
187
|
+
boxPlot: {
|
|
188
|
+
...next.markConfigs[ownerId].boxPlot,
|
|
189
|
+
outlierDataId: hasOutliers ? outlierDataId : undefined
|
|
190
|
+
}
|
|
191
|
+
});
|
|
181
192
|
return next._withContext({ currentMark: ownerId, currentData: source });
|
|
182
193
|
}
|
|
183
194
|
);
|
|
@@ -27,12 +27,12 @@ const DEFAULT_OUTLIER = Object.freeze({
|
|
|
27
27
|
opacity: 0.75
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
function plainOptions(value, keys, label) {
|
|
30
|
+
function plainOptions(value, keys, label, operation = "createBoxPlot") {
|
|
31
31
|
if (value === undefined) return {};
|
|
32
32
|
if (!isPlainObject(value)) {
|
|
33
|
-
throw new TypeError(
|
|
33
|
+
throw new TypeError(`${operation} ${label} must be a plain object.`);
|
|
34
34
|
}
|
|
35
|
-
validateKeys(value, keys,
|
|
35
|
+
validateKeys(value, keys, `${operation} ${label}`);
|
|
36
36
|
return value;
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -52,12 +52,12 @@ export function boxEncodingArgs(value) {
|
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
export function resolveBoxWhisker(value) {
|
|
55
|
+
export function resolveBoxWhisker(value, operation = "createBoxPlot") {
|
|
56
56
|
if (value === undefined) return Object.freeze({ type: "tukey", factor: 1.5 });
|
|
57
57
|
if (!isPlainObject(value)) {
|
|
58
|
-
throw new TypeError(
|
|
58
|
+
throw new TypeError(`${operation} whisker must be a plain object.`);
|
|
59
59
|
}
|
|
60
|
-
validateKeys(value, ["type", "factor"],
|
|
60
|
+
validateKeys(value, ["type", "factor"], `${operation} whisker`);
|
|
61
61
|
const type = value.type ?? "tukey";
|
|
62
62
|
if (!["tukey", "minmax"].includes(type)) {
|
|
63
63
|
throw new Error(`Unsupported createBoxPlot whisker type "${type}".`);
|
|
@@ -77,8 +77,8 @@ export function resolveBoxWhisker(value) {
|
|
|
77
77
|
return Object.freeze({ type, factor });
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
export function resolveBoxWidth(value) {
|
|
81
|
-
const options = plainOptions(value, ["band"], "width");
|
|
80
|
+
export function resolveBoxWidth(value, operation = "createBoxPlot") {
|
|
81
|
+
const options = plainOptions(value, ["band"], "width", operation);
|
|
82
82
|
const band = options.band ?? 0.7;
|
|
83
83
|
if (!Number.isFinite(band) || band <= 0 || band >= 1) {
|
|
84
84
|
throw new RangeError(
|
|
@@ -88,11 +88,12 @@ export function resolveBoxWidth(value) {
|
|
|
88
88
|
return band;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
export function resolveBoxAppearance(value) {
|
|
91
|
+
export function resolveBoxAppearance(value, operation = "createBoxPlot") {
|
|
92
92
|
const options = plainOptions(
|
|
93
93
|
value,
|
|
94
94
|
["fill", "opacity", "stroke", "strokeWidth"],
|
|
95
|
-
"box"
|
|
95
|
+
"box",
|
|
96
|
+
operation
|
|
96
97
|
);
|
|
97
98
|
return Object.freeze({
|
|
98
99
|
fill: options.fill === undefined
|
|
@@ -113,8 +114,13 @@ export function resolveBoxAppearance(value) {
|
|
|
113
114
|
});
|
|
114
115
|
}
|
|
115
116
|
|
|
116
|
-
export function resolveBoxMedianAppearance(value) {
|
|
117
|
-
const options = plainOptions(
|
|
117
|
+
export function resolveBoxMedianAppearance(value, operation = "createBoxPlot") {
|
|
118
|
+
const options = plainOptions(
|
|
119
|
+
value,
|
|
120
|
+
["stroke", "strokeWidth"],
|
|
121
|
+
"median",
|
|
122
|
+
operation
|
|
123
|
+
);
|
|
118
124
|
return Object.freeze({
|
|
119
125
|
stroke: options.stroke === undefined
|
|
120
126
|
? DEFAULT_MEDIAN.stroke
|
|
@@ -128,8 +134,13 @@ export function resolveBoxMedianAppearance(value) {
|
|
|
128
134
|
});
|
|
129
135
|
}
|
|
130
136
|
|
|
131
|
-
export function resolveBoxOutlierAppearance(value) {
|
|
132
|
-
const options = plainOptions(
|
|
137
|
+
export function resolveBoxOutlierAppearance(value, operation = "createBoxPlot") {
|
|
138
|
+
const options = plainOptions(
|
|
139
|
+
value,
|
|
140
|
+
["shape", "radius", "opacity"],
|
|
141
|
+
"outlier",
|
|
142
|
+
operation
|
|
143
|
+
);
|
|
133
144
|
return Object.freeze({
|
|
134
145
|
shape: options.shape === undefined
|
|
135
146
|
? DEFAULT_OUTLIER.shape
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
validateCanvasState
|
|
7
7
|
} from "../../layout/canvas.js";
|
|
8
8
|
import { cloneAndFreeze } from "../../core/immutable.js";
|
|
9
|
+
import { validateOptionObject } from "../../core/validation.js";
|
|
9
10
|
import {
|
|
10
11
|
applyMaterializationPlan,
|
|
11
12
|
planCanvasRematerialization
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
import {
|
|
14
15
|
assertCanvasHierarchyAvailable,
|
|
15
16
|
CANVAS_GRAPHIC_ID,
|
|
17
|
+
findCanvasGraphic,
|
|
16
18
|
PLOT_GRAPHIC_ID
|
|
17
19
|
} from "../../materialization/graphicHierarchy.js";
|
|
18
20
|
|
|
@@ -24,21 +26,11 @@ const CANVAS_OPTIONS = Object.freeze([
|
|
|
24
26
|
]);
|
|
25
27
|
|
|
26
28
|
function validateOptions(args, operation, { allowEmpty = false } = {}) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (!allowEmpty && keys.length === 0) {
|
|
30
|
-
throw new TypeError(`${operation} requires at least one option.`);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
for (const key of keys) {
|
|
34
|
-
if (!CANVAS_OPTIONS.includes(key)) {
|
|
35
|
-
throw new Error(`Unknown ${operation} option "${key}".`);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
29
|
+
validateOptionObject(args, CANVAS_OPTIONS, operation, { allowEmpty });
|
|
38
30
|
}
|
|
39
31
|
|
|
40
32
|
function requireCanvas(program) {
|
|
41
|
-
const canvas = program
|
|
33
|
+
const canvas = findCanvasGraphic(program);
|
|
42
34
|
|
|
43
35
|
if (canvas?.type !== "canvas") {
|
|
44
36
|
throw new Error("editCanvas requires an existing canvas.");
|
|
@@ -49,8 +41,9 @@ function requireCanvas(program) {
|
|
|
49
41
|
|
|
50
42
|
function resolveCanvasState(program, args) {
|
|
51
43
|
const canvas = requireCanvas(program);
|
|
52
|
-
const
|
|
53
|
-
|
|
44
|
+
const canvasConfig = program.materializationConfigs.canvas;
|
|
45
|
+
const baseMargin = canvasConfig?.margin ?? DEFAULT_MARGIN;
|
|
46
|
+
const baseSize = canvasConfig?.size ?? { width: "explicit", height: "explicit" };
|
|
54
47
|
const state = {
|
|
55
48
|
width: Object.hasOwn(args, "width")
|
|
56
49
|
? args.width
|
|
@@ -63,7 +56,11 @@ function resolveCanvasState(program, args) {
|
|
|
63
56
|
: canvas.properties.background,
|
|
64
57
|
margin: Object.hasOwn(args, "margin")
|
|
65
58
|
? normalizeMargin(args.margin, baseMargin)
|
|
66
|
-
: baseMargin
|
|
59
|
+
: baseMargin,
|
|
60
|
+
size: {
|
|
61
|
+
width: Object.hasOwn(args, "width") ? "explicit" : baseSize.width,
|
|
62
|
+
height: Object.hasOwn(args, "height") ? "explicit" : baseSize.height
|
|
63
|
+
}
|
|
67
64
|
};
|
|
68
65
|
|
|
69
66
|
validateCanvasState(state);
|
|
@@ -90,7 +87,7 @@ export const editCanvas = action(
|
|
|
90
87
|
}
|
|
91
88
|
}
|
|
92
89
|
|
|
93
|
-
next = next._withCanvasConfig({ margin: state.margin });
|
|
90
|
+
next = next._withCanvasConfig({ margin: state.margin, size: state.size });
|
|
94
91
|
|
|
95
92
|
if (
|
|
96
93
|
Object.hasOwn(args, "width") ||
|
|
@@ -132,7 +129,7 @@ export const createCanvas = action(
|
|
|
132
129
|
: DEFAULT_CANVAS.margin
|
|
133
130
|
};
|
|
134
131
|
|
|
135
|
-
|
|
132
|
+
const next = this
|
|
136
133
|
.createGraphics({ id: CANVAS_GRAPHIC_ID, type: "canvas" })
|
|
137
134
|
.createGraphics({
|
|
138
135
|
id: PLOT_GRAPHIC_ID,
|
|
@@ -140,5 +137,12 @@ export const createCanvas = action(
|
|
|
140
137
|
parent: CANVAS_GRAPHIC_ID
|
|
141
138
|
})
|
|
142
139
|
.editCanvas(options);
|
|
140
|
+
return next._withCanvasConfig({
|
|
141
|
+
...next.materializationConfigs.canvas,
|
|
142
|
+
size: {
|
|
143
|
+
width: Object.hasOwn(args, "width") ? "explicit" : "auto",
|
|
144
|
+
height: Object.hasOwn(args, "height") ? "explicit" : "auto"
|
|
145
|
+
}
|
|
146
|
+
});
|
|
143
147
|
}
|
|
144
148
|
);
|