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
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import { validateOptionObject } from "../../core/validation.js";
|
|
3
|
+
import {
|
|
4
|
+
normalizeParallelDimensions,
|
|
5
|
+
validateParallelKeyField,
|
|
6
|
+
validateParallelMissingPolicy,
|
|
7
|
+
validateParallelRows
|
|
8
|
+
} from "../../grammar/parallelCoordinates.js";
|
|
9
|
+
import { resolvePositionScaleDefinition } from "../scales/definitions.js";
|
|
10
|
+
import { applyEncodingScale, resolveTarget } from "./shared.js";
|
|
11
|
+
import { resolveParallelCoordinate } from "../coordinates/parallel.js";
|
|
12
|
+
|
|
13
|
+
const OPTIONS = Object.freeze([
|
|
14
|
+
"target", "coordinate", "dimensions", "key", "missing"
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
export const encodeParallelCoordinates = action(
|
|
18
|
+
{
|
|
19
|
+
op: "encodeParallelCoordinates",
|
|
20
|
+
description: "Atomically encode ordered Parallel-coordinate dimensions."
|
|
21
|
+
},
|
|
22
|
+
function (args = {}) {
|
|
23
|
+
validateOptionObject(args, OPTIONS, "encodeParallelCoordinates");
|
|
24
|
+
const { id: target, dataset, layer } = resolveTarget(
|
|
25
|
+
this,
|
|
26
|
+
args.target,
|
|
27
|
+
["line"],
|
|
28
|
+
"line mark"
|
|
29
|
+
);
|
|
30
|
+
if (
|
|
31
|
+
["x", "y", "theta", "radius"].some(
|
|
32
|
+
channel => layer.encoding?.[channel] !== undefined
|
|
33
|
+
)
|
|
34
|
+
) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`Parallel encoding cannot mix with existing position encodings on layer "${target}".`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
const coordinate = resolveParallelCoordinate(this, {
|
|
40
|
+
requested: args.coordinate,
|
|
41
|
+
layer,
|
|
42
|
+
operation: "encodeParallelCoordinates"
|
|
43
|
+
});
|
|
44
|
+
const dimensions = normalizeParallelDimensions(
|
|
45
|
+
dataset.values,
|
|
46
|
+
args.dimensions,
|
|
47
|
+
target
|
|
48
|
+
);
|
|
49
|
+
const key = args.key === undefined
|
|
50
|
+
? undefined
|
|
51
|
+
: validateParallelKeyField(args.key);
|
|
52
|
+
const missing = validateParallelMissingPolicy(args.missing ?? "break");
|
|
53
|
+
validateParallelRows(dataset.values, dimensions, { key, missing });
|
|
54
|
+
|
|
55
|
+
const definitions = dimensions.map(dimension =>
|
|
56
|
+
resolvePositionScaleDefinition(
|
|
57
|
+
this,
|
|
58
|
+
"y",
|
|
59
|
+
dimension.fieldType,
|
|
60
|
+
{ ...dimension.scaleOptions, id: dimension.scale },
|
|
61
|
+
dimension.fieldType === "ordinal" ? { discreteType: "point" } : {}
|
|
62
|
+
)
|
|
63
|
+
);
|
|
64
|
+
const storedDimensions = dimensions.map(({ scaleOptions, ...dimension }) => {
|
|
65
|
+
void scaleOptions;
|
|
66
|
+
return dimension;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
let next = this;
|
|
70
|
+
if (coordinate.create) {
|
|
71
|
+
next = next.createCoordinate({ id: coordinate.id, type: "parallel" });
|
|
72
|
+
}
|
|
73
|
+
if (layer.coordinate === undefined) {
|
|
74
|
+
next = next.editSemantic({
|
|
75
|
+
property: `layer[${target}].coordinate`,
|
|
76
|
+
value: coordinate.id
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
next = next
|
|
80
|
+
.editSemantic({
|
|
81
|
+
property: `layer[${target}].encoding.parallel.dimensions`,
|
|
82
|
+
value: storedDimensions
|
|
83
|
+
})
|
|
84
|
+
.editSemantic({
|
|
85
|
+
property: `layer[${target}].encoding.parallel.missing`,
|
|
86
|
+
value: missing
|
|
87
|
+
});
|
|
88
|
+
if (key === undefined && layer.encoding?.parallel?.key !== undefined) {
|
|
89
|
+
next = next.editSemantic({
|
|
90
|
+
property: `layer[${target}].encoding.parallel.key`,
|
|
91
|
+
remove: true
|
|
92
|
+
});
|
|
93
|
+
} else if (key !== undefined) {
|
|
94
|
+
next = next.editSemantic({
|
|
95
|
+
property: `layer[${target}].encoding.parallel.key`,
|
|
96
|
+
value: key
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
for (let index = 0; index < definitions.length; index += 1) {
|
|
100
|
+
next = applyEncodingScale(next, definitions[index], dimensions[index].scaleOptions);
|
|
101
|
+
}
|
|
102
|
+
for (const definition of definitions) {
|
|
103
|
+
next = next.rematerializeScale({
|
|
104
|
+
id: definition.id,
|
|
105
|
+
guides: false,
|
|
106
|
+
marks: false
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return next.rematerializeLineMark({ id: target, scales: false });
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
export function registerParallelEncodingAction(ProgramClass) {
|
|
114
|
+
ProgramClass.prototype.encodeParallelCoordinates = encodeParallelCoordinates;
|
|
115
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import { validateUserId } from "../../core/identifiers.js";
|
|
3
|
+
import {
|
|
4
|
+
validateNonEmptyString,
|
|
5
|
+
validateOptionObject
|
|
6
|
+
} from "../../core/validation.js";
|
|
7
|
+
import { readQuantitativeField } from "../../grammar/scales/index.js";
|
|
8
|
+
import { validatePathOrderDirection } from "../../grammar/pathOrder.js";
|
|
9
|
+
import {
|
|
10
|
+
assertPathOrderCompatible,
|
|
11
|
+
pathOrderCompatibility
|
|
12
|
+
} from "../../materialization/marks/pathOrder.js";
|
|
13
|
+
import {
|
|
14
|
+
canMaterializeArea,
|
|
15
|
+
canMaterializeLine
|
|
16
|
+
} from "../../materialization/marks/index.js";
|
|
17
|
+
import { findLayer } from "../../selectors/layers.js";
|
|
18
|
+
|
|
19
|
+
const ENCODE_OPTIONS = Object.freeze([
|
|
20
|
+
"target", "field", "fieldType", "order"
|
|
21
|
+
]);
|
|
22
|
+
const REMOVE_OPTIONS = Object.freeze(["target"]);
|
|
23
|
+
|
|
24
|
+
function resolvePathOrderLayer(program, target, { active = false } = {}) {
|
|
25
|
+
const requested = target === undefined
|
|
26
|
+
? undefined
|
|
27
|
+
: validateUserId(target, "Path order target");
|
|
28
|
+
if (requested !== undefined) {
|
|
29
|
+
const layer = findLayer(program, requested);
|
|
30
|
+
if (layer === undefined) {
|
|
31
|
+
throw new Error(`Unknown path order target "${requested}".`);
|
|
32
|
+
}
|
|
33
|
+
if (active && layer.encoding?.pathOrder === undefined) {
|
|
34
|
+
throw new Error(`Mark "${requested}" has no path order encoding.`);
|
|
35
|
+
}
|
|
36
|
+
assertPathOrderCompatible(
|
|
37
|
+
program,
|
|
38
|
+
layer,
|
|
39
|
+
active ? "removePathOrder" : "encodePathOrder"
|
|
40
|
+
);
|
|
41
|
+
return layer;
|
|
42
|
+
}
|
|
43
|
+
const pathLayers = program.semanticSpec.layers.filter(layer =>
|
|
44
|
+
["line", "area"].includes(layer.mark?.type)
|
|
45
|
+
);
|
|
46
|
+
const candidates = pathLayers.filter(layer =>
|
|
47
|
+
pathOrderCompatibility(program, layer).compatible &&
|
|
48
|
+
(!active || layer.encoding?.pathOrder !== undefined)
|
|
49
|
+
);
|
|
50
|
+
const current = candidates.find(
|
|
51
|
+
layer => layer.id === program.context.currentMark
|
|
52
|
+
);
|
|
53
|
+
if (current !== undefined) return current;
|
|
54
|
+
if (candidates.length === 1) return candidates[0];
|
|
55
|
+
if (candidates.length > 1) {
|
|
56
|
+
throw new Error("path order target is ambiguous; provide target.");
|
|
57
|
+
}
|
|
58
|
+
if (!active && pathLayers.length === 1) {
|
|
59
|
+
assertPathOrderCompatible(program, pathLayers[0], "encodePathOrder");
|
|
60
|
+
}
|
|
61
|
+
if (active && pathLayers.length === 1) {
|
|
62
|
+
throw new Error(`Mark "${pathLayers[0].id}" has no path order encoding.`);
|
|
63
|
+
}
|
|
64
|
+
throw new Error("path order requires an eligible layer.");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function rematerializePath(program, layer) {
|
|
68
|
+
if (layer.mark.type === "line") {
|
|
69
|
+
return canMaterializeLine(program, layer)
|
|
70
|
+
? program.rematerializeLineMark({ id: layer.id })
|
|
71
|
+
: program;
|
|
72
|
+
}
|
|
73
|
+
return canMaterializeArea(program, layer)
|
|
74
|
+
? program.rematerializeAreaMark({ id: layer.id })
|
|
75
|
+
: program;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const encodePathOrder = action(
|
|
79
|
+
{
|
|
80
|
+
op: "encodePathOrder",
|
|
81
|
+
description: "Order vertices within each Cartesian path series."
|
|
82
|
+
},
|
|
83
|
+
function (args = {}) {
|
|
84
|
+
validateOptionObject(args, ENCODE_OPTIONS, "encodePathOrder");
|
|
85
|
+
const field = validateNonEmptyString(args.field, "Path order field");
|
|
86
|
+
const fieldType = args.fieldType ?? "quantitative";
|
|
87
|
+
if (fieldType !== "quantitative") {
|
|
88
|
+
throw new Error("encodePathOrder requires a quantitative field.");
|
|
89
|
+
}
|
|
90
|
+
const order = validatePathOrderDirection(args.order ?? "ascending");
|
|
91
|
+
const layer = resolvePathOrderLayer(this, args.target);
|
|
92
|
+
const dataset = assertPathOrderCompatible(this, layer, "encodePathOrder");
|
|
93
|
+
readQuantitativeField(dataset.values, field);
|
|
94
|
+
|
|
95
|
+
const next = this
|
|
96
|
+
.editSemantic({
|
|
97
|
+
property: `layer[${layer.id}].encoding.pathOrder.field`,
|
|
98
|
+
value: field
|
|
99
|
+
})
|
|
100
|
+
.editSemantic({
|
|
101
|
+
property: `layer[${layer.id}].encoding.pathOrder.fieldType`,
|
|
102
|
+
value: fieldType
|
|
103
|
+
})
|
|
104
|
+
.editSemantic({
|
|
105
|
+
property: `layer[${layer.id}].encoding.pathOrder.order`,
|
|
106
|
+
value: order
|
|
107
|
+
});
|
|
108
|
+
return rematerializePath(next, findLayer(next, layer.id));
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const removePathOrder = action(
|
|
113
|
+
{
|
|
114
|
+
op: "removePathOrder",
|
|
115
|
+
description: "Remove explicit path order and restore automatic ordering."
|
|
116
|
+
},
|
|
117
|
+
function (args = {}) {
|
|
118
|
+
validateOptionObject(args, REMOVE_OPTIONS, "removePathOrder");
|
|
119
|
+
const layer = resolvePathOrderLayer(this, args.target, { active: true });
|
|
120
|
+
const next = this.editSemantic({
|
|
121
|
+
property: `layer[${layer.id}].encoding.pathOrder`,
|
|
122
|
+
remove: true
|
|
123
|
+
});
|
|
124
|
+
return rematerializePath(next, findLayer(next, layer.id));
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
export function registerPathOrderEncodingActions(ProgramClass) {
|
|
129
|
+
ProgramClass.prototype.encodePathOrder = encodePathOrder;
|
|
130
|
+
ProgramClass.prototype.removePathOrder = removePathOrder;
|
|
131
|
+
}
|
|
@@ -9,6 +9,44 @@ function replaceAlternateBinding(program, target, channel, previous, usesField)
|
|
|
9
9
|
: program;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
function reconcileInheritedRulePosition(program, {
|
|
13
|
+
target,
|
|
14
|
+
channel,
|
|
15
|
+
layer,
|
|
16
|
+
hasField
|
|
17
|
+
}) {
|
|
18
|
+
const config = program.markConfigs[target] ?? {};
|
|
19
|
+
const inherited = config.inheritedPosition;
|
|
20
|
+
if (layer.mark.type !== "rule" || inherited === undefined) return program;
|
|
21
|
+
|
|
22
|
+
let channels = inherited.channels.filter(candidate => candidate !== channel);
|
|
23
|
+
let next = program;
|
|
24
|
+
if (
|
|
25
|
+
!hasField &&
|
|
26
|
+
["x", "y"].includes(channel) &&
|
|
27
|
+
layer.encoding?.x2 === undefined &&
|
|
28
|
+
layer.encoding?.y2 === undefined
|
|
29
|
+
) {
|
|
30
|
+
const opposite = channel === "x" ? "y" : "x";
|
|
31
|
+
if (channels.includes(opposite) && layer.encoding?.[opposite] !== undefined) {
|
|
32
|
+
next = next.editSemantic({
|
|
33
|
+
property: `layer[${target}].encoding.${opposite}`,
|
|
34
|
+
remove: true
|
|
35
|
+
});
|
|
36
|
+
channels = channels.filter(candidate => candidate !== opposite);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const { inheritedPosition, ...rest } = config;
|
|
41
|
+
void inheritedPosition;
|
|
42
|
+
return next._withMarkConfig(target, channels.length === 0
|
|
43
|
+
? rest
|
|
44
|
+
: {
|
|
45
|
+
...rest,
|
|
46
|
+
inheritedPosition: { ...inherited, channels }
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
12
50
|
function applyBin(program, target, channel, previous, bin) {
|
|
13
51
|
if (bin === undefined) return program;
|
|
14
52
|
const [mode] = Object.keys(bin);
|
|
@@ -26,9 +64,26 @@ function applyBin(program, target, channel, previous, bin) {
|
|
|
26
64
|
});
|
|
27
65
|
}
|
|
28
66
|
|
|
67
|
+
function applyArcThetaWeight(program, target, channel, previous, aggregate, weight) {
|
|
68
|
+
if (channel !== "theta") return program;
|
|
69
|
+
if (aggregate !== "sum") {
|
|
70
|
+
return previous?.weight === undefined
|
|
71
|
+
? program
|
|
72
|
+
: program.editSemantic({
|
|
73
|
+
property: `layer[${target}].encoding.theta.weight`,
|
|
74
|
+
remove: true
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return program.editSemantic({
|
|
78
|
+
property: `layer[${target}].encoding.theta.weight`,
|
|
79
|
+
value: weight
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
29
83
|
export function applyPositionSemantics(program, {
|
|
30
84
|
target,
|
|
31
85
|
channel,
|
|
86
|
+
layer,
|
|
32
87
|
previous,
|
|
33
88
|
field,
|
|
34
89
|
datum,
|
|
@@ -36,10 +91,17 @@ export function applyPositionSemantics(program, {
|
|
|
36
91
|
fieldType,
|
|
37
92
|
bin,
|
|
38
93
|
aggregate,
|
|
39
|
-
stack
|
|
94
|
+
stack,
|
|
95
|
+
weight
|
|
40
96
|
}) {
|
|
41
|
-
let next =
|
|
42
|
-
|
|
97
|
+
let next = reconcileInheritedRulePosition(program, {
|
|
98
|
+
target,
|
|
99
|
+
channel,
|
|
100
|
+
layer,
|
|
101
|
+
hasField
|
|
102
|
+
});
|
|
103
|
+
next = replaceAlternateBinding(
|
|
104
|
+
next,
|
|
43
105
|
target,
|
|
44
106
|
channel,
|
|
45
107
|
previous,
|
|
@@ -62,5 +124,12 @@ export function applyPositionSemantics(program, {
|
|
|
62
124
|
value
|
|
63
125
|
});
|
|
64
126
|
}
|
|
65
|
-
return
|
|
127
|
+
return applyArcThetaWeight(
|
|
128
|
+
next,
|
|
129
|
+
target,
|
|
130
|
+
channel,
|
|
131
|
+
previous,
|
|
132
|
+
aggregate,
|
|
133
|
+
weight
|
|
134
|
+
);
|
|
66
135
|
}
|
|
@@ -24,7 +24,8 @@ function encodePosition(program, channel, args, operation) {
|
|
|
24
24
|
coordinate,
|
|
25
25
|
bin,
|
|
26
26
|
aggregate,
|
|
27
|
-
stack
|
|
27
|
+
stack,
|
|
28
|
+
weight
|
|
28
29
|
} = resolvePositionEncoding(program, channel, args, operation);
|
|
29
30
|
|
|
30
31
|
let next = program
|
|
@@ -36,6 +37,7 @@ function encodePosition(program, channel, args, operation) {
|
|
|
36
37
|
next = applyPositionSemantics(next, {
|
|
37
38
|
target,
|
|
38
39
|
channel,
|
|
40
|
+
layer,
|
|
39
41
|
previous,
|
|
40
42
|
field,
|
|
41
43
|
datum,
|
|
@@ -43,7 +45,8 @@ function encodePosition(program, channel, args, operation) {
|
|
|
43
45
|
fieldType,
|
|
44
46
|
bin,
|
|
45
47
|
aggregate,
|
|
46
|
-
stack
|
|
48
|
+
stack,
|
|
49
|
+
weight
|
|
47
50
|
});
|
|
48
51
|
|
|
49
52
|
next = next.editSemantic({
|
|
@@ -71,6 +74,16 @@ function encodePosition(program, channel, args, operation) {
|
|
|
71
74
|
}
|
|
72
75
|
}
|
|
73
76
|
|
|
77
|
+
if (layer.mark.type === "rect") {
|
|
78
|
+
const updated = findLayer(next, target);
|
|
79
|
+
const pendingGradient = next.markConfigs[target]?.gradientPlot;
|
|
80
|
+
if (pendingGradient !== undefined && !pendingGradient.materialized) {
|
|
81
|
+
return updated.encoding?.x !== undefined && updated.encoding?.y !== undefined
|
|
82
|
+
? next.materializeGradientPlot({ id: target })
|
|
83
|
+
: next;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
74
87
|
const updated = findLayer(next, target);
|
|
75
88
|
for (const step of getPositionEncodingMaterializationSteps(
|
|
76
89
|
next,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { validateAggregate } from "../../../../grammar/aggregate.js";
|
|
2
|
+
import { validateNonEmptyString } from "../../../../core/validation.js";
|
|
2
3
|
import { emptyPositionPolicy } from "./common.js";
|
|
3
4
|
|
|
4
5
|
export function resolveArcPositionPolicy({ channel, args, fieldType }) {
|
|
@@ -12,15 +13,37 @@ export function resolveArcPositionPolicy({ channel, args, fieldType }) {
|
|
|
12
13
|
if (args.aggregate !== undefined) {
|
|
13
14
|
throw new Error("Arc radius encoding does not support aggregate.");
|
|
14
15
|
}
|
|
16
|
+
if (args.weight !== undefined) {
|
|
17
|
+
throw new Error("Arc radius encoding does not support weight.");
|
|
18
|
+
}
|
|
15
19
|
return emptyPositionPolicy();
|
|
16
20
|
}
|
|
17
21
|
if (!["ordinal", "nominal"].includes(fieldType)) {
|
|
18
22
|
throw new Error("Arc theta encoding requires an ordinal or nominal field.");
|
|
19
23
|
}
|
|
20
|
-
if (args.aggregate === undefined)
|
|
24
|
+
if (args.aggregate === undefined) {
|
|
25
|
+
if (args.weight !== undefined) {
|
|
26
|
+
throw new Error('Arc theta weight requires aggregate: "sum".');
|
|
27
|
+
}
|
|
28
|
+
return emptyPositionPolicy();
|
|
29
|
+
}
|
|
21
30
|
const aggregate = validateAggregate(args.aggregate);
|
|
22
|
-
if (
|
|
23
|
-
throw new Error('Arc theta aggregate
|
|
31
|
+
if (!["count", "sum"].includes(aggregate)) {
|
|
32
|
+
throw new Error('Arc theta aggregate supports only "count" or "sum".');
|
|
33
|
+
}
|
|
34
|
+
if (aggregate === "count") {
|
|
35
|
+
if (args.weight !== undefined) {
|
|
36
|
+
throw new Error('Arc theta weight requires aggregate: "sum".');
|
|
37
|
+
}
|
|
38
|
+
return { bin: undefined, aggregate, stack: undefined, weight: undefined };
|
|
39
|
+
}
|
|
40
|
+
if (args.weight === undefined) {
|
|
41
|
+
throw new Error('Arc theta aggregate "sum" requires weight.');
|
|
24
42
|
}
|
|
25
|
-
return {
|
|
43
|
+
return {
|
|
44
|
+
bin: undefined,
|
|
45
|
+
aggregate,
|
|
46
|
+
stack: undefined,
|
|
47
|
+
weight: validateNonEmptyString(args.weight, "Arc theta weight")
|
|
48
|
+
};
|
|
26
49
|
}
|
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
import { validateStack, emptyPositionPolicy } from "./common.js";
|
|
2
2
|
|
|
3
|
-
export function
|
|
3
|
+
export function isCategoricalDensityPosition({
|
|
4
|
+
layer,
|
|
5
|
+
dataset,
|
|
6
|
+
channel,
|
|
7
|
+
fieldType,
|
|
8
|
+
field
|
|
9
|
+
}) {
|
|
10
|
+
const density = dataset.transform?.find(transform => transform.type === "density");
|
|
11
|
+
return layer.mark?.type === "area" &&
|
|
12
|
+
["nominal", "ordinal"].includes(fieldType) &&
|
|
13
|
+
density?.placement?.type === "category" &&
|
|
14
|
+
density.placement.channel === channel &&
|
|
15
|
+
density.placement.categoryField === field;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function resolveAreaPositionPolicy({ dataset, channel, args, fieldType, field }) {
|
|
19
|
+
const density = dataset.transform?.find(transform => transform.type === "density");
|
|
20
|
+
if (["nominal", "ordinal"].includes(fieldType)) {
|
|
21
|
+
if (
|
|
22
|
+
density?.placement?.type !== "category" ||
|
|
23
|
+
density.placement.channel !== channel ||
|
|
24
|
+
density.placement.categoryField !== field
|
|
25
|
+
) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
"Categorical area position requires a matching category density placement."
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
if (args.aggregate !== undefined || args.bin !== undefined || args.stack !== undefined) {
|
|
31
|
+
throw new Error("Categorical density position does not support aggregate, bin, or stack.");
|
|
32
|
+
}
|
|
33
|
+
return emptyPositionPolicy();
|
|
34
|
+
}
|
|
4
35
|
if (!["quantitative", "temporal"].includes(fieldType)) {
|
|
5
36
|
throw new Error(
|
|
6
37
|
"Area position encoding requires quantitative or temporal fields."
|
|
@@ -12,7 +43,7 @@ export function resolveAreaPositionPolicy({ dataset, channel, args, fieldType })
|
|
|
12
43
|
if (args.stack === undefined) return emptyPositionPolicy();
|
|
13
44
|
if (
|
|
14
45
|
channel !== "y" ||
|
|
15
|
-
|
|
46
|
+
density === undefined
|
|
16
47
|
) {
|
|
17
48
|
throw new Error("Area stack currently requires a density y encoding.");
|
|
18
49
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { validatePositionFieldCompatibility } from
|
|
2
2
|
"../../../../grammar/positionCompatibility.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
isCategoricalDensityPosition,
|
|
5
|
+
resolveAreaPositionPolicy
|
|
6
|
+
} from "./area.js";
|
|
4
7
|
import { resolveArcPositionPolicy } from "./arc.js";
|
|
5
8
|
import { resolveBarPositionPolicy } from "./bar.js";
|
|
6
9
|
import { resolveLinePositionPolicy } from "./line.js";
|
|
@@ -20,7 +23,9 @@ const POSITION_POLICIES = Object.freeze({
|
|
|
20
23
|
|
|
21
24
|
export function resolveMarkPositionPolicy(context) {
|
|
22
25
|
const { layer, channel, fieldType } = context;
|
|
23
|
-
|
|
26
|
+
if (!isCategoricalDensityPosition(context)) {
|
|
27
|
+
validatePositionFieldCompatibility(layer.mark.type, channel, fieldType);
|
|
28
|
+
}
|
|
24
29
|
const policy = POSITION_POLICIES[layer.mark.type];
|
|
25
30
|
if (policy === undefined) {
|
|
26
31
|
throw new Error(`Unsupported position policy for mark "${layer.mark.type}".`);
|
|
@@ -44,9 +44,14 @@ export function resolveLinePositionPolicy({
|
|
|
44
44
|
layer.encoding?.y?.aggregate === undefined &&
|
|
45
45
|
["quantitative", "temporal"].includes(layer.encoding?.y?.fieldType) &&
|
|
46
46
|
fieldType === "quantitative";
|
|
47
|
+
const pendingQuantitativePair =
|
|
48
|
+
layer.encoding?.y === undefined && fieldType === "quantitative";
|
|
47
49
|
if (
|
|
48
50
|
fieldType !== "temporal" &&
|
|
49
|
-
!(
|
|
51
|
+
!(
|
|
52
|
+
(regression || interval || directPair || pendingQuantitativePair) &&
|
|
53
|
+
fieldType === "quantitative"
|
|
54
|
+
)
|
|
50
55
|
) {
|
|
51
56
|
throw new Error(
|
|
52
57
|
"Line x encoding requires a temporal field or a compatible derived quantitative field."
|
|
@@ -60,10 +65,14 @@ export function resolveLinePositionPolicy({
|
|
|
60
65
|
}
|
|
61
66
|
|
|
62
67
|
rejectSharedOptions(args, "y");
|
|
68
|
+
const quantitativePair =
|
|
69
|
+
fieldType === "quantitative" &&
|
|
70
|
+
layer.encoding?.x?.fieldType === "quantitative";
|
|
63
71
|
const prospectiveDirect =
|
|
64
72
|
args.aggregate === undefined &&
|
|
65
73
|
(fieldType === "temporal" ||
|
|
66
|
-
(fieldType === "quantitative" &&
|
|
74
|
+
(fieldType === "quantitative" &&
|
|
75
|
+
(layer.encoding?.x === undefined || quantitativePair)));
|
|
67
76
|
if (interval || prospectiveDirect) {
|
|
68
77
|
if (!["quantitative", "temporal"].includes(fieldType)) {
|
|
69
78
|
throw new Error(
|
|
@@ -85,6 +94,13 @@ export function resolveLinePositionPolicy({
|
|
|
85
94
|
return emptyPositionPolicy();
|
|
86
95
|
}
|
|
87
96
|
|
|
97
|
+
if (
|
|
98
|
+
layer.encoding?.x !== undefined &&
|
|
99
|
+
layer.encoding.x.fieldType !== "temporal"
|
|
100
|
+
) {
|
|
101
|
+
throw new Error("Aggregate line y encoding requires a temporal x encoding.");
|
|
102
|
+
}
|
|
103
|
+
|
|
88
104
|
const aggregate = validateAggregate(args.aggregate);
|
|
89
105
|
validateAggregateFieldType(aggregate, fieldType);
|
|
90
106
|
return { bin: undefined, aggregate, stack: undefined };
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
validateAggregateFieldValues,
|
|
22
22
|
} from "../../../grammar/aggregate.js";
|
|
23
23
|
import { normalizeRuleDatum } from "../../../grammar/rules.js";
|
|
24
|
+
import { readArcThetaWeights } from "../../../grammar/arcs.js";
|
|
24
25
|
import { resolveMarkPositionPolicy } from "./policies/index.js";
|
|
25
26
|
import {
|
|
26
27
|
getPositionChannelDefinition,
|
|
@@ -29,7 +30,7 @@ import {
|
|
|
29
30
|
|
|
30
31
|
const POSITION_ENCODING_OPTIONS = Object.freeze([
|
|
31
32
|
"field", "datum", "target", "fieldType", "scale", "coordinate",
|
|
32
|
-
"aggregate", "bin", "stack"
|
|
33
|
+
"aggregate", "bin", "stack", "weight"
|
|
33
34
|
]);
|
|
34
35
|
|
|
35
36
|
function validateCoordinateFamily(layer, channel, operation) {
|
|
@@ -82,6 +83,12 @@ export function resolvePositionEncoding(program, channel, args, operation) {
|
|
|
82
83
|
args.target,
|
|
83
84
|
getPositionChannelDefinition(channel).markTypes
|
|
84
85
|
);
|
|
86
|
+
if (
|
|
87
|
+
Object.hasOwn(args, "weight") &&
|
|
88
|
+
!(layer.mark.type === "arc" && channel === "theta")
|
|
89
|
+
) {
|
|
90
|
+
throw new Error(`${operation} weight is supported only for arc theta encoding.`);
|
|
91
|
+
}
|
|
85
92
|
validateCoordinateFamily(layer, channel, operation);
|
|
86
93
|
const hasField = Object.hasOwn(args, "field");
|
|
87
94
|
const hasDatum = Object.hasOwn(args, "datum");
|
|
@@ -97,7 +104,8 @@ export function resolvePositionEncoding(program, channel, args, operation) {
|
|
|
97
104
|
}
|
|
98
105
|
const previous = layer.encoding?.[channel];
|
|
99
106
|
const requestedFieldType = args.fieldType ?? previous?.fieldType ?? (
|
|
100
|
-
layer.mark.type === "arc" && channel === "theta" &&
|
|
107
|
+
layer.mark.type === "arc" && channel === "theta" &&
|
|
108
|
+
["count", "sum"].includes(args.aggregate)
|
|
101
109
|
? "nominal"
|
|
102
110
|
: "quantitative"
|
|
103
111
|
);
|
|
@@ -117,6 +125,14 @@ export function resolvePositionEncoding(program, channel, args, operation) {
|
|
|
117
125
|
effectiveArgs[property] = previous[property];
|
|
118
126
|
}
|
|
119
127
|
}
|
|
128
|
+
if (
|
|
129
|
+
!Object.hasOwn(effectiveArgs, "weight") &&
|
|
130
|
+
effectiveArgs.aggregate === "sum" &&
|
|
131
|
+
previous !== undefined &&
|
|
132
|
+
Object.hasOwn(previous, "weight")
|
|
133
|
+
) {
|
|
134
|
+
effectiveArgs.weight = previous.weight;
|
|
135
|
+
}
|
|
120
136
|
const policy = resolveMarkPositionPolicy({
|
|
121
137
|
program,
|
|
122
138
|
layer,
|
|
@@ -179,6 +195,12 @@ export function resolvePositionEncoding(program, channel, args, operation) {
|
|
|
179
195
|
} else if (aggregateOutput) {
|
|
180
196
|
validateAggregateFieldType(policy.aggregate, fieldType);
|
|
181
197
|
validateAggregateFieldValues(dataset.values, field, fieldType);
|
|
198
|
+
} else if (
|
|
199
|
+
layer.mark.type === "arc" &&
|
|
200
|
+
channel === "theta" &&
|
|
201
|
+
policy.aggregate === "sum"
|
|
202
|
+
) {
|
|
203
|
+
readArcThetaWeights(dataset.values, policy.weight, layer.id);
|
|
182
204
|
} else if (program.markConfigs[target]?.boxPlot !== undefined) {
|
|
183
205
|
for (const [index, row] of dataset.values.entries()) {
|
|
184
206
|
const value = row[field];
|
|
@@ -157,7 +157,9 @@ function encodeSecondaryPosition(program, channel, args, operation, types) {
|
|
|
157
157
|
? next.rematerializeBarMark({ id: target })
|
|
158
158
|
: layer.mark.type === "rect"
|
|
159
159
|
? next.rematerializeRectMark({ id: target })
|
|
160
|
-
: next
|
|
160
|
+
: canMaterializeArea(next, findLayer(next, target))
|
|
161
|
+
? next.rematerializeAreaMark({ id: target })
|
|
162
|
+
: next;
|
|
161
163
|
}
|
|
162
164
|
|
|
163
165
|
const encodeX2 = action(
|