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
|
@@ -4,6 +4,7 @@ import { isPlainObject } from "../../core/immutable.js";
|
|
|
4
4
|
import {
|
|
5
5
|
readNominalField,
|
|
6
6
|
readQuantitativeField,
|
|
7
|
+
readScaleField,
|
|
7
8
|
readTemporalField,
|
|
8
9
|
validateSemanticFieldType
|
|
9
10
|
} from "../../grammar/scales.js";
|
|
@@ -85,16 +86,35 @@ function encodeSecondaryPosition(program, channel, args, operation, types) {
|
|
|
85
86
|
}
|
|
86
87
|
const fieldType = rule
|
|
87
88
|
? validateSemanticFieldType(args.fieldType)
|
|
88
|
-
:
|
|
89
|
-
|
|
89
|
+
: layer.mark.type === "rect"
|
|
90
|
+
? args.fieldType ?? primary.fieldType
|
|
91
|
+
: args.fieldType ?? "quantitative";
|
|
92
|
+
if (
|
|
93
|
+
!rule &&
|
|
94
|
+
layer.mark.type !== "rect" &&
|
|
95
|
+
fieldType !== "quantitative"
|
|
96
|
+
) {
|
|
90
97
|
throw new Error(`${operation} requires a quantitative field.`);
|
|
91
98
|
}
|
|
99
|
+
if (
|
|
100
|
+
layer.mark.type === "rect" &&
|
|
101
|
+
!["quantitative", "temporal"].includes(fieldType)
|
|
102
|
+
) {
|
|
103
|
+
throw new Error(`${operation} requires a quantitative or temporal rect field.`);
|
|
104
|
+
}
|
|
105
|
+
if (layer.mark.type === "rect" && fieldType !== primary.fieldType) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`${operation} fieldType must match the primary ${primaryChannel} fieldType.`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
92
110
|
if (rule && fieldType !== primary.fieldType) {
|
|
93
111
|
throw new Error(
|
|
94
112
|
`${operation} fieldType must match the primary ${primaryChannel} fieldType.`
|
|
95
113
|
);
|
|
96
114
|
}
|
|
97
|
-
if (hasField
|
|
115
|
+
if (hasField && layer.mark.type === "rect") {
|
|
116
|
+
readScaleField(dataset.values, args.field, fieldType, { allowUnknown: true });
|
|
117
|
+
} else if (hasField) validateSecondaryField(dataset, args.field, fieldType);
|
|
98
118
|
else normalizeRuleDatum(args.datum, fieldType, channel);
|
|
99
119
|
|
|
100
120
|
const previous = layer.encoding?.[channel];
|
|
@@ -135,7 +155,9 @@ function encodeSecondaryPosition(program, channel, args, operation, types) {
|
|
|
135
155
|
if (rule) return next.rematerializeRuleMark({ id: target });
|
|
136
156
|
return layer.mark.type === "bar"
|
|
137
157
|
? next.rematerializeBarMark({ id: target })
|
|
138
|
-
:
|
|
158
|
+
: layer.mark.type === "rect"
|
|
159
|
+
? next.rematerializeRectMark({ id: target })
|
|
160
|
+
: next.rematerializeAreaMark({ id: target });
|
|
139
161
|
}
|
|
140
162
|
|
|
141
163
|
const encodeX2 = action(
|
|
@@ -149,7 +171,7 @@ const encodeX2 = action(
|
|
|
149
171
|
"x2",
|
|
150
172
|
args,
|
|
151
173
|
"encodeX2",
|
|
152
|
-
["area", "rule", "bar"]
|
|
174
|
+
["area", "rule", "bar", "rect"]
|
|
153
175
|
);
|
|
154
176
|
}
|
|
155
177
|
);
|
|
@@ -165,7 +187,7 @@ const encodeY2 = action(
|
|
|
165
187
|
"y2",
|
|
166
188
|
args,
|
|
167
189
|
"encodeY2",
|
|
168
|
-
["area", "rule", "bar"]
|
|
190
|
+
["area", "rule", "bar", "rect"]
|
|
169
191
|
);
|
|
170
192
|
}
|
|
171
193
|
);
|
|
@@ -3,10 +3,14 @@ import { findDataset } from "../../selectors/datasets.js";
|
|
|
3
3
|
import { resolveEligibleLayer } from "../../selectors/layers.js";
|
|
4
4
|
import { hasMaterializedLegend } from "../../materialization/legends.js";
|
|
5
5
|
import { findSemanticScale } from "../../selectors/scales.js";
|
|
6
|
-
import {
|
|
6
|
+
import { validateOptionObject } from "../../core/validation.js";
|
|
7
|
+
import {
|
|
8
|
+
getMarkGraphicTypes,
|
|
9
|
+
getPositionChannelDefinition
|
|
10
|
+
} from "../../core/vocabulary.js";
|
|
7
11
|
|
|
8
12
|
export function validateOptions(args, supported, operation) {
|
|
9
|
-
|
|
13
|
+
validateOptionObject(args, supported, operation);
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
export function validateLineSeriesCompatibility(layer, channel, field) {
|
|
@@ -65,10 +69,12 @@ export function rebindPositionGuides(
|
|
|
65
69
|
nextScale,
|
|
66
70
|
target
|
|
67
71
|
) {
|
|
72
|
+
const definition = getPositionChannelDefinition(channel);
|
|
73
|
+
if (definition?.guideChannel === undefined) return program;
|
|
68
74
|
if (previousScale === undefined || previousScale === nextScale) return program;
|
|
69
75
|
|
|
70
|
-
const axis = program.semanticSpec.guides.axis?.[
|
|
71
|
-
const direction =
|
|
76
|
+
const axis = program.semanticSpec.guides.axis?.[definition.guideChannel];
|
|
77
|
+
const direction = definition.gridDirection;
|
|
72
78
|
const grid = program.semanticSpec.guides.grid?.[direction];
|
|
73
79
|
const ownsAxis = axis?.scale === previousScale;
|
|
74
80
|
const ownsGrid = grid?.scale === previousScale;
|
|
@@ -86,13 +92,13 @@ export function rebindPositionGuides(
|
|
|
86
92
|
let next = program;
|
|
87
93
|
if (ownsAxis) {
|
|
88
94
|
next = next.editSemantic({
|
|
89
|
-
property: `guide.axis.${
|
|
95
|
+
property: `guide.axis.${definition.guideChannel}.scale`,
|
|
90
96
|
value: nextScale
|
|
91
97
|
});
|
|
92
|
-
for (const component of ["ticks", "labels", "title"]) {
|
|
93
|
-
const config = next.guideConfigs.axis?.[
|
|
98
|
+
for (const component of ["line", "ticks", "labels", "title"]) {
|
|
99
|
+
const config = next.guideConfigs.axis?.[definition.guideChannel]?.[component];
|
|
94
100
|
if (config?.scale === previousScale) {
|
|
95
|
-
next = next._withGuideConfig(
|
|
101
|
+
next = next._withGuideConfig(definition.guideChannel, component, {
|
|
96
102
|
...config,
|
|
97
103
|
scale: nextScale
|
|
98
104
|
});
|
|
@@ -141,22 +147,12 @@ export function resolveTarget(
|
|
|
141
147
|
throw new Error(`Mark "${id}" requires an existing dataset.`);
|
|
142
148
|
}
|
|
143
149
|
|
|
144
|
-
const expectedGraphic =
|
|
145
|
-
point: ["circle", "rect", "collection"],
|
|
146
|
-
line: "path",
|
|
147
|
-
bar: "rect",
|
|
148
|
-
area: "path",
|
|
149
|
-
rule: "line"
|
|
150
|
-
}[layer.mark.type];
|
|
150
|
+
const expectedGraphic = getMarkGraphicTypes(layer.mark.type);
|
|
151
151
|
|
|
152
152
|
const graphicType = program.graphicSpec.objects[id]?.type;
|
|
153
|
-
const matches =
|
|
154
|
-
? expectedGraphic.includes(graphicType)
|
|
155
|
-
: graphicType === expectedGraphic;
|
|
153
|
+
const matches = expectedGraphic?.includes(graphicType) === true;
|
|
156
154
|
if (!matches) {
|
|
157
|
-
const label =
|
|
158
|
-
? expectedGraphic.join(" or ")
|
|
159
|
-
: expectedGraphic;
|
|
155
|
+
const label = expectedGraphic?.join(" or ") ?? "supported";
|
|
160
156
|
throw new Error(`Mark "${id}" requires ${label} graphics.`);
|
|
161
157
|
}
|
|
162
158
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import { formatTextValue, validateTextFormat } from "../../grammar/text.js";
|
|
3
|
+
import { canMaterializeText } from "../../materialization/marks.js";
|
|
4
|
+
import { findLayer } from "../../selectors/layers.js";
|
|
5
|
+
import { resolveTarget, validateOptions } from "./shared.js";
|
|
6
|
+
|
|
7
|
+
const OPTIONS = Object.freeze(["target", "field", "value", "format"]);
|
|
8
|
+
|
|
9
|
+
function validateTextField(rows, field, format) {
|
|
10
|
+
if (typeof field !== "string" || field.length === 0) {
|
|
11
|
+
throw new TypeError("encodeText field must be a non-empty string.");
|
|
12
|
+
}
|
|
13
|
+
let found = false;
|
|
14
|
+
for (const row of rows) {
|
|
15
|
+
if (!Object.hasOwn(row, field)) continue;
|
|
16
|
+
found = true;
|
|
17
|
+
formatTextValue(row[field], format);
|
|
18
|
+
}
|
|
19
|
+
if (!found) throw new Error(`Unknown text field "${field}".`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const encodeText = action(
|
|
23
|
+
{
|
|
24
|
+
op: "encodeText",
|
|
25
|
+
description: "Assign field-driven or constant text content."
|
|
26
|
+
},
|
|
27
|
+
function (args = {}) {
|
|
28
|
+
validateOptions(args, OPTIONS, "encodeText");
|
|
29
|
+
const hasField = Object.hasOwn(args, "field");
|
|
30
|
+
const hasValue = Object.hasOwn(args, "value");
|
|
31
|
+
if (hasField === hasValue) {
|
|
32
|
+
throw new Error("encodeText requires exactly one of field or value.");
|
|
33
|
+
}
|
|
34
|
+
const { id: target, dataset, layer } = resolveTarget(
|
|
35
|
+
this,
|
|
36
|
+
args.target,
|
|
37
|
+
["text"],
|
|
38
|
+
"text mark"
|
|
39
|
+
);
|
|
40
|
+
const previous = layer.encoding?.text;
|
|
41
|
+
const format = validateTextFormat(args.format ?? previous?.format ?? "auto");
|
|
42
|
+
if (hasField) validateTextField(dataset.values, args.field, format);
|
|
43
|
+
else if (formatTextValue(args.value, format) === undefined) {
|
|
44
|
+
throw new Error("encodeText value must produce non-empty text.");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let next = this;
|
|
48
|
+
const alternate = hasField ? "datum" : "field";
|
|
49
|
+
if (Object.hasOwn(previous ?? {}, alternate)) {
|
|
50
|
+
next = next.editSemantic({
|
|
51
|
+
property: `layer[${target}].encoding.text.${alternate}`,
|
|
52
|
+
remove: true
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
next = next
|
|
56
|
+
.editSemantic({
|
|
57
|
+
property: `layer[${target}].encoding.text.${hasField ? "field" : "datum"}`,
|
|
58
|
+
value: hasField ? args.field : args.value
|
|
59
|
+
})
|
|
60
|
+
.editSemantic({
|
|
61
|
+
property: `layer[${target}].encoding.text.format`,
|
|
62
|
+
value: format
|
|
63
|
+
});
|
|
64
|
+
const updated = findLayer(next, target);
|
|
65
|
+
return canMaterializeText(next, updated)
|
|
66
|
+
? next.rematerializeTextMark({ id: target })
|
|
67
|
+
: next;
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
export function registerTextEncodingAction(ProgramClass) {
|
|
72
|
+
ProgramClass.prototype.encodeText = encodeText;
|
|
73
|
+
}
|
|
@@ -2,13 +2,10 @@ import { action } from "../../core/action.js";
|
|
|
2
2
|
import { isPlainObject } from "../../core/immutable.js";
|
|
3
3
|
import {
|
|
4
4
|
validateKeys,
|
|
5
|
-
validateNonEmptyString,
|
|
6
|
-
validateNonNegativeFinite,
|
|
7
|
-
validateUnitInterval
|
|
8
5
|
} from "../../core/validation.js";
|
|
9
6
|
import { DEFAULT_COLORS } from "../../theme/defaults.js";
|
|
10
7
|
import { validateCurveInterpolation } from "../../grammar/curveCommands.js";
|
|
11
|
-
import {
|
|
8
|
+
import { resolveBoundaryAppearance } from "./options.js";
|
|
12
9
|
import { resolveErrorBand } from "./resolve.js";
|
|
13
10
|
|
|
14
11
|
const OPTIONS = Object.freeze([
|
|
@@ -24,35 +21,21 @@ const OPTIONS = Object.freeze([
|
|
|
24
21
|
"curve",
|
|
25
22
|
"boundaries"
|
|
26
23
|
]);
|
|
27
|
-
const BOUNDARY_OPTIONS = Object.freeze([
|
|
28
|
-
"stroke", "strokeWidth", "strokeDash", "opacity", "curve"
|
|
29
|
-
]);
|
|
30
|
-
|
|
31
24
|
function resolveBoundaries(value, areaCurve) {
|
|
32
25
|
if (value === undefined || value === false) return undefined;
|
|
33
26
|
if (!isPlainObject(value)) {
|
|
34
27
|
throw new TypeError("createErrorBand boundaries must be false or a plain object.");
|
|
35
28
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
);
|
|
47
|
-
const resolvedStrokeDash = normalizeStrokeDashPattern(strokeDash);
|
|
48
|
-
validateUnitInterval(opacity, "Error-band boundary opacity");
|
|
49
|
-
return {
|
|
50
|
-
stroke,
|
|
51
|
-
strokeWidth,
|
|
52
|
-
strokeDash: resolvedStrokeDash,
|
|
53
|
-
opacity,
|
|
54
|
-
curve
|
|
55
|
-
};
|
|
29
|
+
return resolveBoundaryAppearance(value, {
|
|
30
|
+
defaults: {
|
|
31
|
+
stroke: DEFAULT_COLORS.mark,
|
|
32
|
+
strokeWidth: 1,
|
|
33
|
+
strokeDash: "solid",
|
|
34
|
+
opacity: 1,
|
|
35
|
+
curve: areaCurve
|
|
36
|
+
},
|
|
37
|
+
operation: "createErrorBand boundaries"
|
|
38
|
+
});
|
|
56
39
|
}
|
|
57
40
|
|
|
58
41
|
function positionArgs(resolved) {
|
|
@@ -153,7 +136,32 @@ export const createErrorBand = action(
|
|
|
153
136
|
id: `${resolved.id}UpperBoundary`,
|
|
154
137
|
bound: resolved.fields.upper
|
|
155
138
|
});
|
|
139
|
+
for (const [id, bound] of [
|
|
140
|
+
[`${resolved.id}LowerBoundary`, "lower"],
|
|
141
|
+
[`${resolved.id}UpperBoundary`, "upper"]
|
|
142
|
+
]) {
|
|
143
|
+
next = next._withMarkConfig(id, {
|
|
144
|
+
...next.markConfigs[id],
|
|
145
|
+
...boundaries,
|
|
146
|
+
errorBandBoundary: { owner: resolved.id, bound }
|
|
147
|
+
});
|
|
148
|
+
}
|
|
156
149
|
}
|
|
157
|
-
return next
|
|
150
|
+
return next._withMarkConfig(resolved.id, {
|
|
151
|
+
...next.markConfigs[resolved.id],
|
|
152
|
+
errorBand: {
|
|
153
|
+
data: resolved.dataId,
|
|
154
|
+
orientation: resolved.orientation,
|
|
155
|
+
position: resolved.position,
|
|
156
|
+
coordinate: resolved.coordinate,
|
|
157
|
+
intervalScale: resolved.interval.scale.id ?? resolved.interval.channel,
|
|
158
|
+
positionScale: resolved.position.scale.id ?? resolved.position.channel,
|
|
159
|
+
groupBy: resolved.groupField,
|
|
160
|
+
lowerField: resolved.fields.lower,
|
|
161
|
+
upperField: resolved.fields.upper,
|
|
162
|
+
lowerBoundaryId: `${resolved.id}LowerBoundary`,
|
|
163
|
+
upperBoundaryId: `${resolved.id}UpperBoundary`
|
|
164
|
+
}
|
|
165
|
+
});
|
|
158
166
|
}
|
|
159
167
|
);
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import { validateUserId } from "../../core/identifiers.js";
|
|
3
|
+
import {
|
|
4
|
+
validateOptionObject,
|
|
5
|
+
validateNonEmptyString,
|
|
6
|
+
validateUnitInterval
|
|
7
|
+
} from "../../core/validation.js";
|
|
8
|
+
import { validateCurveInterpolation } from "../../grammar/curveCommands.js";
|
|
9
|
+
import { findLayer, resolveEligibleLayer } from "../../selectors/layers.js";
|
|
10
|
+
import { DEFAULT_COLORS } from "../../theme/defaults.js";
|
|
11
|
+
import {
|
|
12
|
+
ERROR_BAND_BOUNDARY_OPTIONS,
|
|
13
|
+
resolveBoundaryAppearance
|
|
14
|
+
} from "./options.js";
|
|
15
|
+
|
|
16
|
+
const EDIT_OPTIONS = Object.freeze(["target", "fill", "opacity", "curve"]);
|
|
17
|
+
const BOUNDARY_EDIT_OPTIONS = Object.freeze([
|
|
18
|
+
"target", "boundary", ...ERROR_BAND_BOUNDARY_OPTIONS
|
|
19
|
+
]);
|
|
20
|
+
const REMATERIALIZE_OPTIONS = Object.freeze([
|
|
21
|
+
"id", ...ERROR_BAND_BOUNDARY_OPTIONS
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
function resolveOwner(program, requested) {
|
|
25
|
+
const target = requested === undefined
|
|
26
|
+
? undefined
|
|
27
|
+
: validateUserId(requested, "Error-band id");
|
|
28
|
+
return resolveEligibleLayer(program, {
|
|
29
|
+
target,
|
|
30
|
+
label: "error band",
|
|
31
|
+
predicate: layer =>
|
|
32
|
+
layer.mark?.type === "area" &&
|
|
33
|
+
program.markConfigs[layer.id]?.errorBand !== undefined
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function currentBoundaryAppearance(program, id) {
|
|
38
|
+
const config = program.markConfigs[id];
|
|
39
|
+
if (findLayer(program, id)?.mark?.type !== "line" || config === undefined) {
|
|
40
|
+
throw new Error(`Unknown error-band boundary "${id}".`);
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
stroke: config.stroke,
|
|
44
|
+
strokeWidth: config.strokeWidth,
|
|
45
|
+
strokeDash: config.strokeDash,
|
|
46
|
+
opacity: config.opacity,
|
|
47
|
+
curve: config.curve ?? "linear"
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function defaultBoundaryAppearance(program, owner) {
|
|
52
|
+
return {
|
|
53
|
+
stroke: DEFAULT_COLORS.mark,
|
|
54
|
+
strokeWidth: 1,
|
|
55
|
+
strokeDash: "solid",
|
|
56
|
+
opacity: 1,
|
|
57
|
+
curve: program.markConfigs[owner]?.curve ?? "linear"
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function createBoundary(program, owner, id, bound, appearance) {
|
|
62
|
+
const config = program.markConfigs[owner].errorBand;
|
|
63
|
+
const next = program.createErrorBandBoundary({
|
|
64
|
+
id,
|
|
65
|
+
data: config.data,
|
|
66
|
+
orientation: config.orientation,
|
|
67
|
+
bound,
|
|
68
|
+
position: config.position,
|
|
69
|
+
coordinate: config.coordinate,
|
|
70
|
+
intervalScale: config.intervalScale,
|
|
71
|
+
positionScale: config.positionScale,
|
|
72
|
+
groupBy: config.groupBy,
|
|
73
|
+
...appearance
|
|
74
|
+
});
|
|
75
|
+
return next._withMarkConfig(id, {
|
|
76
|
+
...next.markConfigs[id],
|
|
77
|
+
...appearance,
|
|
78
|
+
errorBandBoundary: {
|
|
79
|
+
owner,
|
|
80
|
+
bound: id === config.lowerBoundaryId ? "lower" : "upper"
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const rematerializeErrorBandBoundary = action(
|
|
86
|
+
{
|
|
87
|
+
op: "rematerializeErrorBandBoundary",
|
|
88
|
+
description: "Rematerialize one owned error-band boundary."
|
|
89
|
+
},
|
|
90
|
+
function (args = {}) {
|
|
91
|
+
validateOptionObject(
|
|
92
|
+
args,
|
|
93
|
+
REMATERIALIZE_OPTIONS,
|
|
94
|
+
"rematerializeErrorBandBoundary"
|
|
95
|
+
);
|
|
96
|
+
const id = validateUserId(args.id, "Error-band boundary id");
|
|
97
|
+
currentBoundaryAppearance(this, id);
|
|
98
|
+
const graphic = this.graphicSpec.objects[id];
|
|
99
|
+
const next = this
|
|
100
|
+
._withMarkConfig(id, {
|
|
101
|
+
...this.markConfigs[id],
|
|
102
|
+
stroke: args.stroke,
|
|
103
|
+
strokeWidth: args.strokeWidth,
|
|
104
|
+
strokeDash: args.strokeDash,
|
|
105
|
+
opacity: args.opacity,
|
|
106
|
+
curve: args.curve
|
|
107
|
+
})
|
|
108
|
+
.editGraphics({
|
|
109
|
+
target: id,
|
|
110
|
+
property: "strokeDash",
|
|
111
|
+
value: graphic.items.map(() => args.strokeDash)
|
|
112
|
+
});
|
|
113
|
+
return next.rematerializeLineMark({ id });
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
export const editErrorBand = action(
|
|
118
|
+
{
|
|
119
|
+
op: "editErrorBand",
|
|
120
|
+
description: "Edit one error-band body appearance."
|
|
121
|
+
},
|
|
122
|
+
function (args = {}) {
|
|
123
|
+
validateOptionObject(args, EDIT_OPTIONS, "editErrorBand");
|
|
124
|
+
if (!["fill", "opacity", "curve"].some(key => Object.hasOwn(args, key))) {
|
|
125
|
+
throw new Error("editErrorBand requires fill, opacity, or curve.");
|
|
126
|
+
}
|
|
127
|
+
const owner = resolveOwner(this, args.target);
|
|
128
|
+
const config = { ...this.markConfigs[owner.id] };
|
|
129
|
+
const errorBand = { ...config.errorBand };
|
|
130
|
+
if (Object.hasOwn(args, "fill")) {
|
|
131
|
+
errorBand.fill = validateNonEmptyString(args.fill, "Error-band fill");
|
|
132
|
+
}
|
|
133
|
+
if (Object.hasOwn(args, "opacity")) {
|
|
134
|
+
config.opacity = validateUnitInterval(args.opacity, "Error-band opacity");
|
|
135
|
+
}
|
|
136
|
+
if (Object.hasOwn(args, "curve")) {
|
|
137
|
+
config.curve = validateCurveInterpolation(args.curve);
|
|
138
|
+
}
|
|
139
|
+
return this
|
|
140
|
+
._withMarkConfig(owner.id, { ...config, errorBand })
|
|
141
|
+
.rematerializeAreaMark({ id: owner.id });
|
|
142
|
+
}
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
export const editErrorBandBoundary = action(
|
|
146
|
+
{
|
|
147
|
+
op: "editErrorBandBoundary",
|
|
148
|
+
description: "Edit one or both owned error-band boundaries."
|
|
149
|
+
},
|
|
150
|
+
function (args = {}) {
|
|
151
|
+
validateOptionObject(
|
|
152
|
+
args,
|
|
153
|
+
BOUNDARY_EDIT_OPTIONS,
|
|
154
|
+
"editErrorBandBoundary"
|
|
155
|
+
);
|
|
156
|
+
if (!ERROR_BAND_BOUNDARY_OPTIONS.some(key => Object.hasOwn(args, key))) {
|
|
157
|
+
throw new Error("editErrorBandBoundary requires an appearance change.");
|
|
158
|
+
}
|
|
159
|
+
const boundary = args.boundary ?? "both";
|
|
160
|
+
if (!["both", "lower", "upper"].includes(boundary)) {
|
|
161
|
+
throw new Error(`Unsupported error-band boundary "${boundary}".`);
|
|
162
|
+
}
|
|
163
|
+
const owner = resolveOwner(this, args.target);
|
|
164
|
+
const config = this.markConfigs[owner.id].errorBand;
|
|
165
|
+
const patch = Object.fromEntries(
|
|
166
|
+
ERROR_BAND_BOUNDARY_OPTIONS
|
|
167
|
+
.filter(key => Object.hasOwn(args, key))
|
|
168
|
+
.map(key => [key, args[key]])
|
|
169
|
+
);
|
|
170
|
+
const ids = boundary === "both"
|
|
171
|
+
? [config.lowerBoundaryId, config.upperBoundaryId]
|
|
172
|
+
: [boundary === "lower" ? config.lowerBoundaryId : config.upperBoundaryId];
|
|
173
|
+
const plans = ids.map(id => {
|
|
174
|
+
const existing = findLayer(this, id) !== undefined;
|
|
175
|
+
return {
|
|
176
|
+
id,
|
|
177
|
+
existing,
|
|
178
|
+
bound: id === config.lowerBoundaryId
|
|
179
|
+
? config.lowerField
|
|
180
|
+
: config.upperField,
|
|
181
|
+
appearance: resolveBoundaryAppearance(patch, {
|
|
182
|
+
defaults: existing
|
|
183
|
+
? currentBoundaryAppearance(this, id)
|
|
184
|
+
: defaultBoundaryAppearance(this, owner.id),
|
|
185
|
+
operation: "editErrorBandBoundary"
|
|
186
|
+
})
|
|
187
|
+
};
|
|
188
|
+
});
|
|
189
|
+
let next = this;
|
|
190
|
+
for (const plan of plans) {
|
|
191
|
+
next = plan.existing
|
|
192
|
+
? next.rematerializeErrorBandBoundary({
|
|
193
|
+
id: plan.id,
|
|
194
|
+
...plan.appearance
|
|
195
|
+
})
|
|
196
|
+
: createBoundary(
|
|
197
|
+
next,
|
|
198
|
+
owner.id,
|
|
199
|
+
plan.id,
|
|
200
|
+
plan.bound,
|
|
201
|
+
plan.appearance
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
return next;
|
|
205
|
+
}
|
|
206
|
+
);
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { createErrorBand } from "./create.js";
|
|
2
2
|
import { createErrorBandBoundary } from "./components.js";
|
|
3
|
+
import {
|
|
4
|
+
editErrorBand,
|
|
5
|
+
editErrorBandBoundary,
|
|
6
|
+
rematerializeErrorBandBoundary
|
|
7
|
+
} from "./edit.js";
|
|
3
8
|
|
|
4
9
|
export function registerErrorBandActions(ProgramClass) {
|
|
5
10
|
ProgramClass.prototype.createErrorBand = createErrorBand;
|
|
6
11
|
ProgramClass.prototype.createErrorBandBoundary = createErrorBandBoundary;
|
|
12
|
+
ProgramClass.prototype.editErrorBand = editErrorBand;
|
|
13
|
+
ProgramClass.prototype.editErrorBandBoundary = editErrorBandBoundary;
|
|
14
|
+
ProgramClass.prototype.rematerializeErrorBandBoundary =
|
|
15
|
+
rematerializeErrorBandBoundary;
|
|
7
16
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { isPlainObject } from "../../core/immutable.js";
|
|
2
|
+
import {
|
|
3
|
+
validateKeys,
|
|
4
|
+
validateNonEmptyString,
|
|
5
|
+
validateNonNegativeFinite,
|
|
6
|
+
validateUnitInterval
|
|
7
|
+
} from "../../core/validation.js";
|
|
8
|
+
import { validateCurveInterpolation } from "../../grammar/curveCommands.js";
|
|
9
|
+
import { normalizeStrokeDashPattern } from "../../grammar/scales.js";
|
|
10
|
+
|
|
11
|
+
export const ERROR_BAND_BOUNDARY_OPTIONS = Object.freeze([
|
|
12
|
+
"stroke", "strokeWidth", "strokeDash", "opacity", "curve"
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
export function resolveBoundaryAppearance(value, {
|
|
16
|
+
defaults,
|
|
17
|
+
operation
|
|
18
|
+
}) {
|
|
19
|
+
if (!isPlainObject(value)) {
|
|
20
|
+
throw new TypeError(`${operation} must be a plain object.`);
|
|
21
|
+
}
|
|
22
|
+
validateKeys(value, ERROR_BAND_BOUNDARY_OPTIONS, operation);
|
|
23
|
+
const stroke = value.stroke ?? defaults.stroke;
|
|
24
|
+
const strokeWidth = value.strokeWidth ?? defaults.strokeWidth;
|
|
25
|
+
const strokeDash = value.strokeDash ?? defaults.strokeDash;
|
|
26
|
+
const opacity = value.opacity ?? defaults.opacity;
|
|
27
|
+
const curve = validateCurveInterpolation(value.curve ?? defaults.curve);
|
|
28
|
+
validateNonEmptyString(stroke, `${operation} stroke`);
|
|
29
|
+
validateNonNegativeFinite(strokeWidth, `${operation} strokeWidth`);
|
|
30
|
+
const resolvedStrokeDash = normalizeStrokeDashPattern(strokeDash);
|
|
31
|
+
validateUnitInterval(opacity, `${operation} opacity`);
|
|
32
|
+
return {
|
|
33
|
+
stroke,
|
|
34
|
+
strokeWidth,
|
|
35
|
+
strokeDash: resolvedStrokeDash,
|
|
36
|
+
opacity,
|
|
37
|
+
curve
|
|
38
|
+
};
|
|
39
|
+
}
|