ggaction 0.0.4 → 0.0.6
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 +55 -0
- package/README.md +35 -13
- package/package.json +18 -7
- package/src/actions/boxPlots/create.js +13 -7
- package/src/actions/boxPlots/edit.js +484 -36
- package/src/actions/boxPlots/materialize.js +3 -0
- package/src/actions/boxPlots/options.js +11 -4
- package/src/actions/boxPlots/resolve.js +10 -2
- package/src/actions/charts/bar.js +50 -0
- package/src/actions/charts/heatmap.js +205 -0
- package/src/actions/charts/histogram.js +58 -0
- package/src/actions/charts/index.js +15 -0
- package/src/actions/charts/line.js +69 -0
- package/src/actions/charts/parallel.js +76 -0
- package/src/actions/charts/scatter.js +56 -0
- package/src/actions/charts/shared.js +121 -0
- package/src/actions/composition/actions.js +13 -3
- package/src/actions/coordinates/parallel.js +50 -0
- package/src/actions/data/bin2d.js +260 -0
- package/src/actions/data/density.js +54 -13
- package/src/actions/data/derived.js +22 -0
- package/src/actions/data/filter.js +5 -0
- package/src/actions/data/gradientProfile.js +65 -0
- package/src/actions/data/horizon.js +71 -0
- package/src/actions/data/index.js +34 -2
- package/src/actions/data/intervalEdit.js +76 -0
- package/src/actions/data/window.js +50 -0
- package/src/actions/distributions/revision.js +326 -0
- package/src/actions/encodings/appearance.js +35 -1
- package/src/actions/encodings/color/index.js +17 -1
- package/src/actions/encodings/color/layout.js +8 -0
- package/src/actions/encodings/density/resolve.js +109 -0
- package/src/actions/encodings/density.js +330 -90
- package/src/actions/encodings/horizon/resolve.js +283 -0
- package/src/actions/encodings/horizon.js +364 -0
- package/src/actions/encodings/index.js +8 -0
- package/src/actions/encodings/parallel.js +115 -0
- package/src/actions/encodings/pathOrder.js +131 -0
- package/src/actions/encodings/position/apply.js +73 -4
- package/src/actions/encodings/position/index.js +15 -2
- package/src/actions/encodings/position/policies/arc.js +27 -4
- package/src/actions/encodings/position/policies/area.js +33 -2
- package/src/actions/encodings/position/policies/index.js +7 -2
- package/src/actions/encodings/position/policies/line.js +18 -2
- package/src/actions/encodings/position/resolve.js +24 -2
- package/src/actions/encodings/ranged.js +3 -1
- package/src/actions/encodings/remove.js +268 -0
- package/src/actions/encodings/ruleAppearance.js +88 -10
- package/src/actions/errorBands/edit.js +121 -13
- package/src/actions/errorBars/edit.js +45 -8
- package/src/actions/errorBars/options.js +7 -1
- package/src/actions/facets/actions.js +137 -3
- package/src/actions/facets/derive.js +33 -3
- package/src/actions/facets/replay.js +7 -36
- package/src/actions/gradientPlots/components.js +177 -0
- package/src/actions/gradientPlots/create.js +96 -0
- package/src/actions/gradientPlots/edit.js +501 -0
- package/src/actions/gradientPlots/index.js +24 -0
- package/src/actions/gradientPlots/materialize.js +287 -0
- package/src/actions/gradientPlots/options.js +120 -0
- package/src/actions/gradientPlots/paint.js +107 -0
- package/src/actions/gradientPlots/rebind.js +51 -0
- package/src/actions/gradientPlots/resolve.js +81 -0
- package/src/actions/guides/applicability.js +44 -17
- package/src/actions/guides/axes/axes.js +37 -3
- package/src/actions/guides/axes/edit.js +129 -47
- package/src/actions/guides/axes/index.js +2 -0
- package/src/actions/guides/axes/labels.js +11 -6
- package/src/actions/guides/axes/parallel/resolve.js +86 -0
- package/src/actions/guides/axes/parallel.js +228 -0
- package/src/actions/guides/axes/titles.js +11 -8
- package/src/actions/guides/guides.js +60 -5
- package/src/actions/guides/legends/categorical/actions.js +67 -19
- package/src/actions/guides/legends/continuous/common.js +3 -3
- package/src/actions/guides/legends/edit.js +63 -0
- package/src/actions/guides/legends/index.js +2 -0
- package/src/actions/guides/legends/remove.js +124 -30
- package/src/actions/guides/legends/strokeWidth.js +198 -0
- package/src/actions/index.js +6 -0
- package/src/actions/marks/arc/actions.js +46 -14
- package/src/actions/marks/area/actions.js +60 -136
- package/src/actions/marks/area/materialize.js +163 -0
- package/src/actions/marks/index.js +5 -1
- package/src/actions/marks/line/actions.js +99 -80
- package/src/actions/marks/line/materialize.js +146 -0
- package/src/actions/marks/point/create.js +76 -0
- package/src/actions/marks/point/edit.js +97 -0
- package/src/actions/marks/point/index.js +11 -1
- package/src/actions/marks/point/jitter.js +72 -0
- package/src/actions/marks/point/{actions.js → materialize.js} +183 -172
- package/src/actions/marks/rect/actions.js +3 -0
- package/src/actions/marks/remove.js +13 -2
- package/src/actions/marks/rule/actions.js +29 -4
- package/src/actions/marks/text/actions.js +19 -4
- package/src/actions/marks/text/index.js +7 -1
- package/src/actions/marks/text/layout.js +321 -0
- package/src/actions/primitives/semanticValidation/dataset.js +22 -0
- package/src/actions/primitives/semanticValidation/guide.js +43 -0
- package/src/actions/primitives/semanticValidation/index.js +23 -0
- package/src/actions/primitives/semanticValidation/layer.js +74 -0
- package/src/actions/primitives/semanticValidation/scale.js +74 -0
- package/src/actions/primitives/semanticValidation/shared.js +5 -0
- package/src/actions/primitives/semanticValue.js +1 -204
- package/src/actions/regression/edit.js +247 -88
- package/src/actions/scales/consumers/common.js +18 -0
- package/src/actions/scales/consumers/families.js +14 -3
- package/src/actions/scales/definitions.js +30 -0
- package/src/actions/scales/edit.js +6 -204
- package/src/actions/scales/editPolicy.js +214 -0
- package/src/actions/scales/materialize.js +1 -1
- package/src/actions/scales/patch.js +26 -0
- package/src/actions/selection/actions.js +150 -5
- package/src/actions/selection/index.js +6 -0
- package/src/actions/violinPlots/create.js +198 -0
- package/src/actions/violinPlots/index.js +5 -0
- package/src/core/textMetrics.js +47 -0
- package/src/core/vocabulary.js +14 -2
- package/src/grammar/arcs.js +49 -12
- package/src/grammar/areaSeries.js +41 -5
- package/src/grammar/bin2d.js +333 -0
- package/src/grammar/categoricalDensity.js +192 -0
- package/src/grammar/coordinates.js +1 -1
- package/src/grammar/curveCommands.js +3 -1
- package/src/grammar/density.js +252 -26
- package/src/grammar/facets/dependencies.js +3 -11
- package/src/grammar/facets/index.js +3 -1
- package/src/grammar/gradientProfile.js +213 -0
- package/src/grammar/horizon.js +455 -0
- package/src/grammar/jitter.js +197 -0
- package/src/grammar/lineSeries.js +69 -0
- package/src/grammar/paint.js +88 -0
- package/src/grammar/parallelCoordinates.js +213 -0
- package/src/grammar/pathOrder.js +35 -0
- package/src/grammar/pointShapes.js +24 -0
- package/src/grammar/rules.js +1 -0
- package/src/grammar/scales/appearance.js +21 -0
- package/src/grammar/schemas/concreteGraphic.js +11 -1
- package/src/grammar/schemas/graphicBounds.js +19 -37
- package/src/grammar/schemas/semanticPath.js +17 -2
- package/src/grammar/transforms.js +100 -11
- package/src/grammar/window.js +339 -0
- package/src/layout/labels.js +270 -0
- package/src/layout/text.js +4 -1
- package/src/layout/title.js +10 -20
- package/src/materialization/dataProvenance.js +56 -2
- package/src/materialization/dependencies.js +9 -14
- package/src/materialization/guides/resources.js +74 -24
- package/src/materialization/horizon.js +29 -0
- package/src/materialization/layout.js +15 -0
- package/src/materialization/marks/capabilities.js +21 -1
- package/src/materialization/marks/index.js +3 -0
- package/src/materialization/marks/pathOrder.js +71 -0
- package/src/materialization/marks/policies.js +6 -2
- package/src/materialization/scaleGuideDependencies.js +8 -0
- package/src/materialization/scales/resolve.js +2 -0
- package/src/materialization/selection/items/path.js +27 -1
- package/src/materialization/selection/items/point.js +1 -5
- package/src/materialization/selection/items/rect.js +21 -0
- package/src/materialization/selection/styles.js +20 -9
- package/src/materialization/text.js +12 -4
- package/src/renderers/canvas/fill.js +33 -0
- package/src/renderers/canvas/index.js +7 -0
- package/src/renderers/canvas/path.js +7 -4
- package/src/renderers/canvas/rect.js +9 -3
- package/src/selectors/datasets.js +4 -0
- package/src/theme/defaults.js +4 -0
- package/types/extension.d.ts +6 -0
- package/types/index.d.ts +77 -0
- package/types/program.d.ts +703 -17
|
@@ -7,9 +7,21 @@ import {
|
|
|
7
7
|
POLAR_POSITION_CHANNELS
|
|
8
8
|
} from "../../core/vocabulary.js";
|
|
9
9
|
import { findSemanticScale } from "../../selectors/scales.js";
|
|
10
|
+
import { findDataset } from "../../selectors/datasets.js";
|
|
11
|
+
import { findUpstreamTransform } from
|
|
12
|
+
"../../materialization/dataProvenance.js";
|
|
13
|
+
|
|
14
|
+
function isHorizonLayer(program, layer) {
|
|
15
|
+
return findUpstreamTransform(
|
|
16
|
+
program,
|
|
17
|
+
findDataset(program, layer.data),
|
|
18
|
+
"horizon"
|
|
19
|
+
) !== undefined;
|
|
20
|
+
}
|
|
10
21
|
|
|
11
22
|
function hasEncoding(program, channel, { scaled = false, grid = false } = {}) {
|
|
12
23
|
return program.semanticSpec.layers.some(layer => {
|
|
24
|
+
if (isHorizonLayer(program, layer) && channel !== "x") return false;
|
|
13
25
|
const encoding = layer.encoding?.[channel];
|
|
14
26
|
if (!scaled && !grid) return encoding !== undefined;
|
|
15
27
|
if (encoding?.scale === undefined) return false;
|
|
@@ -33,22 +45,24 @@ function positionalApplicability(program, channels) {
|
|
|
33
45
|
|
|
34
46
|
export function hasInferableLegend(program) {
|
|
35
47
|
return program.semanticSpec.layers.some(layer =>
|
|
36
|
-
(layer
|
|
37
|
-
layer.
|
|
38
|
-
|
|
39
|
-
layer.
|
|
40
|
-
|
|
41
|
-
layer.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
layer.
|
|
48
|
+
!isHorizonLayer(program, layer) && (
|
|
49
|
+
(layer.mark?.type === "point" &&
|
|
50
|
+
layer.encoding?.opacity?.scale !== undefined) ||
|
|
51
|
+
(layer.mark?.type === "point" &&
|
|
52
|
+
layer.encoding?.size?.scale !== undefined) ||
|
|
53
|
+
(layer.mark?.type === "point" &&
|
|
54
|
+
layer.encoding?.color?.scale !== undefined &&
|
|
55
|
+
(layer.encoding?.shape?.scale !== undefined ||
|
|
56
|
+
["sequential", "quantize", "quantile", "threshold"].includes(
|
|
57
|
+
findSemanticScale(program, layer.encoding.color.scale)?.type
|
|
58
|
+
))) ||
|
|
59
|
+
(layer.mark?.type === "line" &&
|
|
60
|
+
["color", "strokeDash"].some(
|
|
61
|
+
channel => layer.encoding?.[channel]?.scale !== undefined
|
|
62
|
+
)) ||
|
|
63
|
+
(["bar", "area", "arc", "rect"].includes(layer.mark?.type) &&
|
|
64
|
+
layer.encoding?.color?.scale !== undefined)
|
|
65
|
+
)
|
|
52
66
|
);
|
|
53
67
|
}
|
|
54
68
|
|
|
@@ -58,12 +72,22 @@ export function resolveGuideApplicability(program) {
|
|
|
58
72
|
CARTESIAN_POSITION_CHANNELS
|
|
59
73
|
);
|
|
60
74
|
const polar = positionalApplicability(program, POLAR_POSITION_CHANNELS);
|
|
75
|
+
const parallel = program.semanticSpec.layers.some(layer =>
|
|
76
|
+
layer.encoding?.parallel?.dimensions?.length >= 2
|
|
77
|
+
);
|
|
61
78
|
return Object.freeze({
|
|
62
79
|
axes: Object.freeze({
|
|
63
80
|
cartesian: CARTESIAN_POSITION_CHANNELS.some(
|
|
64
81
|
channel => cartesian[channel].axis
|
|
65
82
|
),
|
|
66
|
-
polar: POLAR_POSITION_CHANNELS.some(channel => polar[channel].axis)
|
|
83
|
+
polar: POLAR_POSITION_CHANNELS.some(channel => polar[channel].axis),
|
|
84
|
+
parallel,
|
|
85
|
+
directions: Object.freeze({
|
|
86
|
+
x: cartesian.x.axis,
|
|
87
|
+
y: cartesian.y.axis,
|
|
88
|
+
theta: polar.theta.axis,
|
|
89
|
+
radius: polar.radius.axis
|
|
90
|
+
})
|
|
67
91
|
}),
|
|
68
92
|
grid: Object.freeze({
|
|
69
93
|
cartesian: CARTESIAN_POSITION_CHANNELS.some(
|
|
@@ -90,6 +114,9 @@ export function resolveAutomaticGridOptions(program) {
|
|
|
90
114
|
...(directions.radial ? { radial: {} } : {})
|
|
91
115
|
});
|
|
92
116
|
}
|
|
117
|
+
if (directions.vertical && !directions.horizontal) {
|
|
118
|
+
return Object.freeze({ horizontal: false, vertical: {} });
|
|
119
|
+
}
|
|
93
120
|
|
|
94
121
|
const barOrientations = program.semanticSpec.layers
|
|
95
122
|
.map(resolveBarOrientation)
|
|
@@ -9,7 +9,9 @@ const TOP_OPTIONS = Object.freeze([
|
|
|
9
9
|
"coordinate", "x", "y", "theta", "radius"
|
|
10
10
|
]);
|
|
11
11
|
const COORDINATE_OPTIONS = Object.freeze(["id", "type"]);
|
|
12
|
-
const COORDINATE_API_TYPES = new Set([
|
|
12
|
+
const COORDINATE_API_TYPES = new Set([
|
|
13
|
+
"auto", "cartesian", "polar", "parallel"
|
|
14
|
+
]);
|
|
13
15
|
|
|
14
16
|
function validateAxisOption(value, channel) {
|
|
15
17
|
if (value !== undefined && value !== false && !isPlainObject(value)) {
|
|
@@ -47,6 +49,9 @@ function inspectChannels(layers) {
|
|
|
47
49
|
layer =>
|
|
48
50
|
layer.encoding?.theta !== undefined || layer.encoding?.radius !== undefined
|
|
49
51
|
);
|
|
52
|
+
const parallelLayers = layers.filter(
|
|
53
|
+
layer => layer.encoding?.parallel !== undefined
|
|
54
|
+
);
|
|
50
55
|
const hasMixedLayer = layers.some(layer => {
|
|
51
56
|
const hasCartesian =
|
|
52
57
|
layer.encoding?.x !== undefined || layer.encoding?.y !== undefined;
|
|
@@ -61,7 +66,7 @@ function inspectChannels(layers) {
|
|
|
61
66
|
);
|
|
62
67
|
}
|
|
63
68
|
|
|
64
|
-
return { cartesianLayers, polarLayers };
|
|
69
|
+
return { cartesianLayers, polarLayers, parallelLayers };
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
function resolveCoordinate(program, descriptor, cartesianLayers) {
|
|
@@ -185,7 +190,7 @@ const createAxes = action(
|
|
|
185
190
|
},
|
|
186
191
|
function (args = {}) {
|
|
187
192
|
const coordinateDescriptor = validateArgs(args);
|
|
188
|
-
const { cartesianLayers, polarLayers } = inspectChannels(
|
|
193
|
+
const { cartesianLayers, polarLayers, parallelLayers } = inspectChannels(
|
|
189
194
|
this.semanticSpec.layers
|
|
190
195
|
);
|
|
191
196
|
const requestedCoordinateType = coordinateDescriptor.id === undefined
|
|
@@ -194,6 +199,35 @@ const createAxes = action(
|
|
|
194
199
|
const explicitlyPolar = coordinateDescriptor.type === "polar" ||
|
|
195
200
|
requestedCoordinateType === "polar" ||
|
|
196
201
|
args.theta !== undefined || args.radius !== undefined;
|
|
202
|
+
const explicitlyParallel = coordinateDescriptor.type === "parallel" ||
|
|
203
|
+
requestedCoordinateType === "parallel";
|
|
204
|
+
if (parallelLayers.length > 0 && (
|
|
205
|
+
(cartesianLayers.length === 0 && polarLayers.length === 0) ||
|
|
206
|
+
explicitlyParallel
|
|
207
|
+
)) {
|
|
208
|
+
if (
|
|
209
|
+
args.x !== undefined || args.y !== undefined ||
|
|
210
|
+
args.theta !== undefined || args.radius !== undefined
|
|
211
|
+
) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
"createAxes channel options are not supported for Parallel axes."
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
const candidates = coordinateDescriptor.id === undefined
|
|
217
|
+
? parallelLayers
|
|
218
|
+
: parallelLayers.filter(layer =>
|
|
219
|
+
layer.coordinate === coordinateDescriptor.id
|
|
220
|
+
);
|
|
221
|
+
if (candidates.length !== 1) {
|
|
222
|
+
throw new Error(
|
|
223
|
+
"createAxes requires coordinate.id when one Parallel layer cannot be inferred."
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
return this.createParallelAxes({
|
|
227
|
+
target: candidates[0].id,
|
|
228
|
+
coordinate: candidates[0].coordinate
|
|
229
|
+
});
|
|
230
|
+
}
|
|
197
231
|
if (polarLayers.length > 0 &&
|
|
198
232
|
(cartesianLayers.length === 0 || explicitlyPolar)) {
|
|
199
233
|
if (coordinateDescriptor.type === "cartesian") {
|
|
@@ -18,11 +18,19 @@ const TITLE_OPTIONS = Object.freeze([
|
|
|
18
18
|
"text", "at", "offset", "rotation", "color", "fontSize",
|
|
19
19
|
"fontFamily", "fontWeight"
|
|
20
20
|
]);
|
|
21
|
+
const COMPONENTS = Object.freeze({
|
|
22
|
+
line: Object.freeze({ suffix: "Line", config: "line" }),
|
|
23
|
+
ticks: Object.freeze({ suffix: "Ticks", config: "ticks" }),
|
|
24
|
+
labels: Object.freeze({ suffix: "Labels", config: "labels" }),
|
|
25
|
+
title: Object.freeze({ suffix: "Title", config: "title" })
|
|
26
|
+
});
|
|
21
27
|
|
|
22
|
-
function validateNested(args, key, options, operation) {
|
|
28
|
+
function validateNested(args, key, options, operation, { allowFalse = false } = {}) {
|
|
23
29
|
if (!Object.hasOwn(args, key)) return;
|
|
30
|
+
if (allowFalse && args[key] === false) return;
|
|
24
31
|
if (!isPlainObject(args[key])) {
|
|
25
|
-
|
|
32
|
+
const accepted = allowFalse ? "false or a plain object" : "a plain object";
|
|
33
|
+
throw new TypeError(`${operation}.${key} must be ${accepted}.`);
|
|
26
34
|
}
|
|
27
35
|
validateKeys(args[key], options, `${operation}.${key}`);
|
|
28
36
|
}
|
|
@@ -33,11 +41,12 @@ function validateArgs(args, operation) {
|
|
|
33
41
|
emptyMessage: `${operation} requires at least one axis change.`,
|
|
34
42
|
emptyError: Error
|
|
35
43
|
});
|
|
36
|
-
|
|
37
|
-
validateNested(args, "
|
|
38
|
-
validateNested(args, "
|
|
39
|
-
validateNested(args, "
|
|
40
|
-
validateNested(args, "
|
|
44
|
+
const removable = { allowFalse: true };
|
|
45
|
+
validateNested(args, "line", LINE_OPTIONS, operation, removable);
|
|
46
|
+
validateNested(args, "ticks", TICK_OPTIONS, operation, removable);
|
|
47
|
+
validateNested(args, "labels", LABEL_OPTIONS, operation, removable);
|
|
48
|
+
validateNested(args, "ticksAndLabels", GROUP_OPTIONS, operation, removable);
|
|
49
|
+
validateNested(args, "title", TITLE_OPTIONS, operation, removable);
|
|
41
50
|
if (args.ticksAndLabels !== undefined && (
|
|
42
51
|
args.ticks !== undefined || args.labels !== undefined
|
|
43
52
|
)) {
|
|
@@ -45,10 +54,10 @@ function validateArgs(args, operation) {
|
|
|
45
54
|
`${operation} cannot combine ticksAndLabels with ticks or labels.`
|
|
46
55
|
);
|
|
47
56
|
}
|
|
48
|
-
if (args.ticksAndLabels?.ticks !== undefined) {
|
|
57
|
+
if (args.ticksAndLabels !== false && args.ticksAndLabels?.ticks !== undefined) {
|
|
49
58
|
validateNested(args.ticksAndLabels, "ticks", ["length", "color", "lineWidth"], `${operation}.ticksAndLabels`);
|
|
50
59
|
}
|
|
51
|
-
if (args.ticksAndLabels?.labels !== undefined) {
|
|
60
|
+
if (args.ticksAndLabels !== false && args.ticksAndLabels?.labels !== undefined) {
|
|
52
61
|
validateNested(args.ticksAndLabels, "labels", [
|
|
53
62
|
"offset", "format", "color", "fontSize", "fontFamily", "fontWeight"
|
|
54
63
|
], `${operation}.ticksAndLabels`);
|
|
@@ -63,10 +72,114 @@ function names(channel) {
|
|
|
63
72
|
ticks: `edit${prefix}AxisTicks`,
|
|
64
73
|
labels: `edit${prefix}AxisLabels`,
|
|
65
74
|
group: `edit${prefix}AxisTicksAndLabels`,
|
|
66
|
-
title: `edit${prefix}AxisTitle
|
|
75
|
+
title: `edit${prefix}AxisTitle`,
|
|
76
|
+
remove: `remove${prefix}Axis`
|
|
67
77
|
};
|
|
68
78
|
}
|
|
69
79
|
|
|
80
|
+
function graphicId(channel, component) {
|
|
81
|
+
return `${channel}Axis${COMPONENTS[component].suffix}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function hasComponent(program, channel, component) {
|
|
85
|
+
return program.graphicSpec.objects[graphicId(channel, component)] !== undefined ||
|
|
86
|
+
program.guideConfigs.axis?.[channel]?.[COMPONENTS[component].config] !== undefined ||
|
|
87
|
+
(component === "title" &&
|
|
88
|
+
program.semanticSpec.guides.axis?.[channel]?.title !== undefined);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function assertRemovable(program, channel, component, operation) {
|
|
92
|
+
if (!hasComponent(program, channel, component)) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`${operation}.${component} requires an existing ${channel}-axis ${component}.`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function removeComponent(program, channel, component) {
|
|
100
|
+
const id = graphicId(channel, component);
|
|
101
|
+
let next = program;
|
|
102
|
+
|
|
103
|
+
if (
|
|
104
|
+
component === "title" &&
|
|
105
|
+
next.semanticSpec.guides.axis?.[channel]?.title !== undefined
|
|
106
|
+
) {
|
|
107
|
+
next = next.editSemantic({
|
|
108
|
+
property: `guide.axis.${channel}.title`,
|
|
109
|
+
remove: true
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
if (next.graphicSpec.objects[id] !== undefined) {
|
|
113
|
+
next = next.editGraphics({ target: id, remove: true });
|
|
114
|
+
}
|
|
115
|
+
return next._withoutMaterializationConfig([
|
|
116
|
+
"guides", "axis", channel, COMPONENTS[component].config
|
|
117
|
+
]);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function buildPlan(program, channel, args, operation) {
|
|
121
|
+
const shared = Object.hasOwn(args, "position")
|
|
122
|
+
? { position: args.position }
|
|
123
|
+
: {};
|
|
124
|
+
const plan = [];
|
|
125
|
+
const addLeaf = (component, edit) => {
|
|
126
|
+
const value = args[component];
|
|
127
|
+
if (value === false) {
|
|
128
|
+
assertRemovable(program, channel, component, operation.operation);
|
|
129
|
+
plan.push({ kind: "remove", component });
|
|
130
|
+
} else if (value !== undefined) {
|
|
131
|
+
plan.push({ kind: "edit", operation: edit, args: { ...shared, ...value } });
|
|
132
|
+
} else if (args.position !== undefined && hasComponent(program, channel, component)) {
|
|
133
|
+
plan.push({ kind: "edit", operation: edit, args: shared });
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
addLeaf("line", operation.line);
|
|
138
|
+
if (args.ticksAndLabels === false) {
|
|
139
|
+
assertRemovable(program, channel, "ticks", operation.operation);
|
|
140
|
+
assertRemovable(program, channel, "labels", operation.operation);
|
|
141
|
+
plan.push(
|
|
142
|
+
{ kind: "remove", component: "ticks" },
|
|
143
|
+
{ kind: "remove", component: "labels" }
|
|
144
|
+
);
|
|
145
|
+
} else if (args.ticksAndLabels !== undefined) {
|
|
146
|
+
plan.push({
|
|
147
|
+
kind: "edit",
|
|
148
|
+
operation: operation.group,
|
|
149
|
+
args: { ...shared, ...args.ticksAndLabels }
|
|
150
|
+
});
|
|
151
|
+
} else {
|
|
152
|
+
addLeaf("ticks", operation.ticks);
|
|
153
|
+
addLeaf("labels", operation.labels);
|
|
154
|
+
}
|
|
155
|
+
addLeaf("title", operation.title);
|
|
156
|
+
|
|
157
|
+
if (plan.length === 0) {
|
|
158
|
+
throw new Error(`${operation.operation} requires an existing ${channel}-axis component.`);
|
|
159
|
+
}
|
|
160
|
+
return plan;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function applyPlan(program, channel, plan, operation) {
|
|
164
|
+
let next = program;
|
|
165
|
+
for (const step of plan) {
|
|
166
|
+
next = step.kind === "remove"
|
|
167
|
+
? removeComponent(next, channel, step.component)
|
|
168
|
+
: next[step.operation](step.args);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const hasRetainedGraphic = Object.keys(COMPONENTS).some(
|
|
172
|
+
component => next.graphicSpec.objects[graphicId(channel, component)] !== undefined
|
|
173
|
+
);
|
|
174
|
+
if (!hasRetainedGraphic && (
|
|
175
|
+
next.semanticSpec.guides.axis?.[channel] !== undefined ||
|
|
176
|
+
next.guideConfigs.axis?.[channel] !== undefined
|
|
177
|
+
)) {
|
|
178
|
+
next = next[operation.remove]();
|
|
179
|
+
}
|
|
180
|
+
return next;
|
|
181
|
+
}
|
|
182
|
+
|
|
70
183
|
function makeEditAxis(channel) {
|
|
71
184
|
const operation = names(channel);
|
|
72
185
|
return action(
|
|
@@ -76,43 +189,12 @@ function makeEditAxis(channel) {
|
|
|
76
189
|
},
|
|
77
190
|
function (args = {}) {
|
|
78
191
|
validateArgs(args, operation.operation);
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (args.line !== undefined || (args.position !== undefined && has(`${channel}AxisLine`))) {
|
|
86
|
-
next = next[operation.line]({ ...shared, ...(args.line ?? {}) });
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (args.ticksAndLabels !== undefined) {
|
|
90
|
-
next = next[operation.group]({
|
|
91
|
-
...shared,
|
|
92
|
-
...args.ticksAndLabels
|
|
93
|
-
});
|
|
94
|
-
} else if (args.ticks !== undefined || args.labels !== undefined) {
|
|
95
|
-
if (args.ticks !== undefined) {
|
|
96
|
-
next = next[operation.ticks]({ ...shared, ...args.ticks });
|
|
97
|
-
}
|
|
98
|
-
if (args.labels !== undefined) {
|
|
99
|
-
next = next[operation.labels]({ ...shared, ...args.labels });
|
|
100
|
-
}
|
|
101
|
-
} else if (args.position !== undefined) {
|
|
102
|
-
const hasTicks = has(`${channel}AxisTicks`);
|
|
103
|
-
const hasLabels = has(`${channel}AxisLabels`);
|
|
104
|
-
if (hasTicks && hasLabels) {
|
|
105
|
-
next = next[operation.group](shared);
|
|
106
|
-
} else {
|
|
107
|
-
if (hasTicks) next = next[operation.ticks](shared);
|
|
108
|
-
if (hasLabels) next = next[operation.labels](shared);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (args.title !== undefined || (args.position !== undefined && has(`${channel}AxisTitle`))) {
|
|
113
|
-
next = next[operation.title]({ ...shared, ...(args.title ?? {}) });
|
|
114
|
-
}
|
|
115
|
-
return next;
|
|
192
|
+
const plan = buildPlan(this, channel, args, operation);
|
|
193
|
+
|
|
194
|
+
// Run the complete proposal against an immutable speculative branch so a
|
|
195
|
+
// later leaf failure cannot begin the returned action trace or state.
|
|
196
|
+
applyPlan(this, channel, plan, operation);
|
|
197
|
+
return applyPlan(this, channel, plan, operation);
|
|
116
198
|
}
|
|
117
199
|
);
|
|
118
200
|
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
removeXAxis,
|
|
13
13
|
removeYAxis
|
|
14
14
|
} from "./remove.js";
|
|
15
|
+
import { registerParallelAxisActions } from "./parallel.js";
|
|
15
16
|
|
|
16
17
|
export function registerGuideAxisActions(ProgramClass) {
|
|
17
18
|
registerAxisLineActions(ProgramClass);
|
|
@@ -22,6 +23,7 @@ export function registerGuideAxisActions(ProgramClass) {
|
|
|
22
23
|
registerAxisActions(ProgramClass);
|
|
23
24
|
registerAxisCollectionActions(ProgramClass);
|
|
24
25
|
registerCompleteAxisEditActions(ProgramClass);
|
|
26
|
+
registerParallelAxisActions(ProgramClass);
|
|
25
27
|
ProgramClass.prototype.removeXAxis = removeXAxis;
|
|
26
28
|
ProgramClass.prototype.removeYAxis = removeYAxis;
|
|
27
29
|
ProgramClass.prototype.removeThetaAxis = removeThetaAxis;
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
} from "./policy.js";
|
|
28
28
|
import { findCanvasGraphic, resolvePlotGraphicPlacement } from
|
|
29
29
|
"../../../materialization/graphicHierarchy.js";
|
|
30
|
+
import { resolveTextBounds } from "../../../core/textMetrics.js";
|
|
30
31
|
|
|
31
32
|
const OPTIONS = Object.freeze([
|
|
32
33
|
"scale", "position", "count", "values", "offset", "format", "color",
|
|
@@ -122,14 +123,18 @@ function resolve(program, channel, config) {
|
|
|
122
123
|
})
|
|
123
124
|
};
|
|
124
125
|
const canvas = findCanvasGraphic(program)?.properties;
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
126
|
+
const labelBounds = text.map((value, index) => resolveTextBounds({
|
|
127
|
+
x: Array.isArray(resolved.x) ? resolved.x[index] : resolved.x,
|
|
128
|
+
y: Array.isArray(resolved.y) ? resolved.y[index] : resolved.y,
|
|
129
|
+
text: value,
|
|
130
|
+
fontSize: config.fontSize,
|
|
131
|
+
textAlign: resolved.textAlign,
|
|
132
|
+
textBaseline: resolved.textBaseline
|
|
133
|
+
}));
|
|
129
134
|
const fits = config.position === "top"
|
|
130
|
-
?
|
|
135
|
+
? labelBounds.every(item => item.top >= 0)
|
|
131
136
|
: config.position === "right"
|
|
132
|
-
?
|
|
137
|
+
? labelBounds.every(item => item.right <= canvas?.width)
|
|
133
138
|
: true;
|
|
134
139
|
if (!canvas || !fits) {
|
|
135
140
|
throw new Error(`The ${channel}-axis labels do not fit the Canvas margin.`);
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { validateUserId } from "../../../../core/identifiers.js";
|
|
2
|
+
import {
|
|
3
|
+
isTransformedScaleType,
|
|
4
|
+
mapContinuousScaleValues,
|
|
5
|
+
mapOrdinalPositionValues,
|
|
6
|
+
transformedTicks
|
|
7
|
+
} from "../../../../grammar/scales/index.js";
|
|
8
|
+
import { niceTicks } from "../../../../grammar/ticks.js";
|
|
9
|
+
import { resolveGraphicBounds } from "../../../../layout/canvas.js";
|
|
10
|
+
import { findCoordinate } from "../../../../selectors/coordinates.js";
|
|
11
|
+
import { findLayer } from "../../../../selectors/layers.js";
|
|
12
|
+
|
|
13
|
+
export function requireParallelAxisLayer(program, target) {
|
|
14
|
+
const layer = findLayer(program, target);
|
|
15
|
+
const coordinate = layer === undefined
|
|
16
|
+
? undefined
|
|
17
|
+
: findCoordinate(program, layer.coordinate);
|
|
18
|
+
if (
|
|
19
|
+
layer?.mark?.type !== "line" ||
|
|
20
|
+
coordinate?.type !== "parallel" ||
|
|
21
|
+
layer.encoding?.parallel?.dimensions?.length < 2
|
|
22
|
+
) {
|
|
23
|
+
throw new Error(`Parallel axes require an encoded Parallel line "${target}".`);
|
|
24
|
+
}
|
|
25
|
+
return { coordinate, dimensions: layer.encoding.parallel.dimensions, layer };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function resolveParallelAxisTarget(program, requested) {
|
|
29
|
+
if (requested !== undefined) {
|
|
30
|
+
const target = validateUserId(requested, "Parallel axes target");
|
|
31
|
+
requireParallelAxisLayer(program, target);
|
|
32
|
+
return target;
|
|
33
|
+
}
|
|
34
|
+
const candidates = program.semanticSpec.layers.filter(layer => {
|
|
35
|
+
const coordinate = findCoordinate(program, layer.coordinate);
|
|
36
|
+
return layer.encoding?.parallel !== undefined && coordinate?.type === "parallel";
|
|
37
|
+
});
|
|
38
|
+
if (candidates.length !== 1) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
"Parallel axes require target when one Parallel layer cannot be inferred."
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return candidates[0].id;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function ticksForScale(scale) {
|
|
47
|
+
if (["ordinal", "band", "point"].includes(scale.type)) return scale.domain;
|
|
48
|
+
if (isTransformedScaleType(scale.type)) {
|
|
49
|
+
return transformedTicks(scale.type, scale.domain, 5, {
|
|
50
|
+
...(scale.base === undefined ? {} : { base: scale.base }),
|
|
51
|
+
...(scale.exponent === undefined ? {} : { exponent: scale.exponent }),
|
|
52
|
+
...(scale.constant === undefined ? {} : { constant: scale.constant })
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return niceTicks(scale.domain, 5);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function formatValue(value) {
|
|
59
|
+
if (Number.isFinite(value) && Math.abs(value) >= 1000 && value % 1000 === 0) {
|
|
60
|
+
return `${value / 1000}k`;
|
|
61
|
+
}
|
|
62
|
+
return String(value);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function resolveParallelAxisValues(program, dimensions) {
|
|
66
|
+
const bounds = resolveGraphicBounds(program);
|
|
67
|
+
const step = bounds.width / (dimensions.length - 1);
|
|
68
|
+
const axes = dimensions.map((dimension, index) => {
|
|
69
|
+
const scale = program.resolvedScales[dimension.scale];
|
|
70
|
+
if (scale === undefined) {
|
|
71
|
+
throw new Error(`Parallel axis requires resolved scale "${dimension.scale}".`);
|
|
72
|
+
}
|
|
73
|
+
const values = ticksForScale(scale);
|
|
74
|
+
const y = ["ordinal", "band", "point"].includes(scale.type)
|
|
75
|
+
? mapOrdinalPositionValues(values, scale)
|
|
76
|
+
: mapContinuousScaleValues(values, scale);
|
|
77
|
+
return {
|
|
78
|
+
...dimension,
|
|
79
|
+
x: bounds.x + step * index,
|
|
80
|
+
values,
|
|
81
|
+
y,
|
|
82
|
+
labels: values.map(formatValue)
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
return { axes, bounds };
|
|
86
|
+
}
|