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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { action } from "../../core/action.js";
|
|
2
2
|
import {
|
|
3
3
|
readNominalField,
|
|
4
|
-
|
|
4
|
+
validateCategoricalFieldType
|
|
5
5
|
} from "../../grammar/scales.js";
|
|
6
6
|
import { resolveOffsetScaleDefinition } from "../scales/definitions.js";
|
|
7
7
|
import {
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from "./shared.js";
|
|
13
13
|
import {
|
|
14
14
|
resolveBarColorLayout,
|
|
15
|
+
resolveBarChannels,
|
|
15
16
|
resolveBarGrain
|
|
16
17
|
} from "../../grammar/bars/policy.js";
|
|
17
18
|
import { normalizeOffsetPadding } from "../../grammar/bars/geometry.js";
|
|
@@ -21,84 +22,101 @@ import { planEncodingRematerialization } from "../../materialization/encodings.j
|
|
|
21
22
|
const ENCODING_OPTIONS = Object.freeze([
|
|
22
23
|
"field", "target", "fieldType", "scale", "paddingInner", "paddingOuter"
|
|
23
24
|
]);
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
args.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
25
|
+
function createOffsetEncoding(channel) {
|
|
26
|
+
const operation = channel === "xOffset" ? "encodeXOffset" : "encodeYOffset";
|
|
27
|
+
const parentChannel = channel === "xOffset" ? "x" : "y";
|
|
28
|
+
return action(
|
|
29
|
+
{
|
|
30
|
+
op: operation,
|
|
31
|
+
description: `Encode a categorical field within each ${parentChannel} category band.`
|
|
32
|
+
},
|
|
33
|
+
function (args = {}) {
|
|
34
|
+
validateOptions(args, ENCODING_OPTIONS, operation);
|
|
35
|
+
const fieldType = validateCategoricalFieldType(args.fieldType ?? "nominal");
|
|
36
|
+
const { id: target, dataset, layer } = resolveTarget(
|
|
37
|
+
this,
|
|
38
|
+
args.target,
|
|
39
|
+
["bar"],
|
|
40
|
+
"bar mark"
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const channels = resolveBarChannels(layer);
|
|
44
|
+
if (
|
|
45
|
+
resolveBarGrain(layer) === undefined ||
|
|
46
|
+
channels?.category !== parentChannel
|
|
47
|
+
) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`${operation} requires a complete bar with a ${parentChannel} category encoding.`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
if (
|
|
53
|
+
layer.encoding?.color !== undefined &&
|
|
54
|
+
(resolveBarColorLayout(layer) !== "group" ||
|
|
55
|
+
layer.encoding.color.field !== args.field)
|
|
56
|
+
) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`${operation} field must match a grouped bar color field.`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
38
61
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
throw new Error(
|
|
44
|
-
"encodeXOffset requires a complete bar x/y encoding."
|
|
62
|
+
readNominalField(dataset.values, args.field);
|
|
63
|
+
const requestedScale = resolveReassignmentScaleOptions(
|
|
64
|
+
layer.encoding?.[channel],
|
|
65
|
+
args.scale ?? {}
|
|
45
66
|
);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
67
|
+
const scale = resolveOffsetScaleDefinition(this, requestedScale, channel);
|
|
68
|
+
if (Object.hasOwn(scale, "unknown")) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`${channel} scale unknown is not supported for grouped bars.`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
const padding = normalizeOffsetPadding(
|
|
74
|
+
args,
|
|
75
|
+
this.markConfigs[target]?.[channel],
|
|
76
|
+
channel
|
|
54
77
|
);
|
|
55
|
-
}
|
|
56
78
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
79
|
+
let next = this
|
|
80
|
+
.editSemantic({
|
|
81
|
+
property: `layer[${target}].encoding.${channel}.field`,
|
|
82
|
+
value: args.field
|
|
83
|
+
})
|
|
84
|
+
.editSemantic({
|
|
85
|
+
property: `layer[${target}].encoding.${channel}.fieldType`,
|
|
86
|
+
value: fieldType
|
|
87
|
+
})
|
|
88
|
+
.editSemantic({
|
|
89
|
+
property: `layer[${target}].encoding.${channel}.scale`,
|
|
90
|
+
value: scale.id
|
|
91
|
+
})
|
|
92
|
+
._withMarkConfig(target, {
|
|
93
|
+
...this.markConfigs[target],
|
|
94
|
+
[channel]: padding
|
|
95
|
+
});
|
|
96
|
+
next = applyEncodingScale(next, scale, requestedScale, {
|
|
97
|
+
reassignment: layer.encoding?.[channel]?.scale === scale.id
|
|
98
|
+
});
|
|
99
|
+
if (layer.encoding?.color === undefined) {
|
|
100
|
+
return next
|
|
101
|
+
.rematerializeScale({ id: scale.id })
|
|
102
|
+
.editGraphics({ target, property: "length", value: 0 });
|
|
103
|
+
}
|
|
104
|
+
return applyMaterializationPlan(
|
|
105
|
+
next,
|
|
106
|
+
planEncodingRematerialization(next, {
|
|
107
|
+
target,
|
|
108
|
+
channel,
|
|
109
|
+
scale: scale.id
|
|
110
|
+
})
|
|
111
|
+
);
|
|
65
112
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
this.markConfigs[target]?.xOffset
|
|
69
|
-
);
|
|
113
|
+
);
|
|
114
|
+
}
|
|
70
115
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
property: `layer[${target}].encoding.xOffset.field`,
|
|
74
|
-
value: args.field
|
|
75
|
-
})
|
|
76
|
-
.editSemantic({
|
|
77
|
-
property: `layer[${target}].encoding.xOffset.fieldType`,
|
|
78
|
-
value: fieldType
|
|
79
|
-
})
|
|
80
|
-
.editSemantic({
|
|
81
|
-
property: `layer[${target}].encoding.xOffset.scale`,
|
|
82
|
-
value: scale.id
|
|
83
|
-
})
|
|
84
|
-
._withMarkConfig(target, {
|
|
85
|
-
...this.markConfigs[target],
|
|
86
|
-
xOffset: padding
|
|
87
|
-
});
|
|
88
|
-
next = applyEncodingScale(next, scale, requestedScale, {
|
|
89
|
-
reassignment: layer.encoding?.xOffset?.scale === scale.id
|
|
90
|
-
});
|
|
91
|
-
return applyMaterializationPlan(
|
|
92
|
-
next,
|
|
93
|
-
planEncodingRematerialization(next, {
|
|
94
|
-
target,
|
|
95
|
-
channel: "xOffset",
|
|
96
|
-
scale: scale.id
|
|
97
|
-
})
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
);
|
|
116
|
+
const encodeXOffset = createOffsetEncoding("xOffset");
|
|
117
|
+
const encodeYOffset = createOffsetEncoding("yOffset");
|
|
101
118
|
|
|
102
119
|
export function registerOffsetEncodingAction(ProgramClass) {
|
|
103
120
|
ProgramClass.prototype.encodeXOffset = encodeXOffset;
|
|
121
|
+
ProgramClass.prototype.encodeYOffset = encodeYOffset;
|
|
104
122
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
function replaceAlternateBinding(program, target, channel, previous, usesField) {
|
|
2
|
+
if (previous === undefined) return program;
|
|
3
|
+
const alternate = usesField ? "datum" : "field";
|
|
4
|
+
return Object.hasOwn(previous, alternate)
|
|
5
|
+
? program.editSemantic({
|
|
6
|
+
property: `layer[${target}].encoding.${channel}.${alternate}`,
|
|
7
|
+
remove: true
|
|
8
|
+
})
|
|
9
|
+
: program;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function applyBin(program, target, channel, previous, bin) {
|
|
13
|
+
if (bin === undefined) return program;
|
|
14
|
+
const [mode] = Object.keys(bin);
|
|
15
|
+
let next = program;
|
|
16
|
+
for (const previousMode of Object.keys(previous?.bin ?? {})) {
|
|
17
|
+
if (previousMode === mode) continue;
|
|
18
|
+
next = next.editSemantic({
|
|
19
|
+
property: `layer[${target}].encoding.${channel}.bin.${previousMode}`,
|
|
20
|
+
remove: true
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
return next.editSemantic({
|
|
24
|
+
property: `layer[${target}].encoding.${channel}.bin.${mode}`,
|
|
25
|
+
value: bin[mode]
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function applyPositionSemantics(program, {
|
|
30
|
+
target,
|
|
31
|
+
channel,
|
|
32
|
+
previous,
|
|
33
|
+
field,
|
|
34
|
+
datum,
|
|
35
|
+
hasField,
|
|
36
|
+
fieldType,
|
|
37
|
+
bin,
|
|
38
|
+
aggregate,
|
|
39
|
+
stack
|
|
40
|
+
}) {
|
|
41
|
+
let next = replaceAlternateBinding(
|
|
42
|
+
program,
|
|
43
|
+
target,
|
|
44
|
+
channel,
|
|
45
|
+
previous,
|
|
46
|
+
hasField
|
|
47
|
+
)
|
|
48
|
+
.editSemantic({
|
|
49
|
+
property: `layer[${target}].encoding.${channel}.${hasField ? "field" : "datum"}`,
|
|
50
|
+
value: hasField ? field : datum
|
|
51
|
+
})
|
|
52
|
+
.editSemantic({
|
|
53
|
+
property: `layer[${target}].encoding.${channel}.fieldType`,
|
|
54
|
+
value: fieldType
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
next = applyBin(next, target, channel, previous, bin);
|
|
58
|
+
for (const [property, value] of Object.entries({ aggregate, stack })) {
|
|
59
|
+
if (value === undefined) continue;
|
|
60
|
+
next = next.editSemantic({
|
|
61
|
+
property: `layer[${target}].encoding.${channel}.${property}`,
|
|
62
|
+
value
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return next;
|
|
66
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { validateAggregate } from "../../../../grammar/aggregate.js";
|
|
2
|
+
import { emptyPositionPolicy } from "./common.js";
|
|
3
|
+
|
|
4
|
+
export function resolveArcPositionPolicy({ channel, args, fieldType }) {
|
|
5
|
+
if (args.bin !== undefined) {
|
|
6
|
+
throw new Error(`Arc ${channel} encoding does not support bin.`);
|
|
7
|
+
}
|
|
8
|
+
if (args.stack !== undefined) {
|
|
9
|
+
throw new Error(`Arc ${channel} encoding does not support stack.`);
|
|
10
|
+
}
|
|
11
|
+
if (channel === "radius") {
|
|
12
|
+
if (args.aggregate !== undefined) {
|
|
13
|
+
throw new Error("Arc radius encoding does not support aggregate.");
|
|
14
|
+
}
|
|
15
|
+
return emptyPositionPolicy();
|
|
16
|
+
}
|
|
17
|
+
if (!["ordinal", "nominal"].includes(fieldType)) {
|
|
18
|
+
throw new Error("Arc theta encoding requires an ordinal or nominal field.");
|
|
19
|
+
}
|
|
20
|
+
if (args.aggregate === undefined) return emptyPositionPolicy();
|
|
21
|
+
const aggregate = validateAggregate(args.aggregate);
|
|
22
|
+
if (aggregate !== "count") {
|
|
23
|
+
throw new Error('Arc theta aggregate currently supports only "count".');
|
|
24
|
+
}
|
|
25
|
+
return { bin: undefined, aggregate, stack: undefined };
|
|
26
|
+
}
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
} from "../../../../grammar/aggregate.js";
|
|
5
5
|
import {
|
|
6
6
|
BAR_ORIENTATIONS,
|
|
7
|
+
resolveBarOffsetChannel,
|
|
7
8
|
resolveBarOrientation
|
|
8
9
|
} from "../../../../grammar/bars/policy.js";
|
|
9
10
|
import { resolveBin, validateStack } from "./common.js";
|
|
@@ -98,12 +99,15 @@ export function resolveBarPositionPolicy({
|
|
|
98
99
|
`Bar ${channel} encoding requires a quantitative field opposite a categorical position.`
|
|
99
100
|
);
|
|
100
101
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
const expectedOffset = resolveBarOffsetChannel(candidate);
|
|
103
|
+
const incompatibleOffset = orientation === BAR_ORIENTATIONS.horizontal
|
|
104
|
+
? "xOffset"
|
|
105
|
+
: orientation === BAR_ORIENTATIONS.vertical
|
|
106
|
+
? "yOffset"
|
|
107
|
+
: undefined;
|
|
108
|
+
if (incompatibleOffset !== undefined && candidate.encoding?.[incompatibleOffset] !== undefined) {
|
|
105
109
|
throw new Error(
|
|
106
|
-
|
|
110
|
+
`${orientation} bars require ${expectedOffset}; remove the incompatible ${incompatibleOffset} encoding.`
|
|
107
111
|
);
|
|
108
112
|
}
|
|
109
113
|
return { bin, aggregate, stack };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { validatePositionFieldCompatibility } from
|
|
2
2
|
"../../../../grammar/positionCompatibility.js";
|
|
3
3
|
import { resolveAreaPositionPolicy } from "./area.js";
|
|
4
|
+
import { resolveArcPositionPolicy } from "./arc.js";
|
|
4
5
|
import { resolveBarPositionPolicy } from "./bar.js";
|
|
5
6
|
import { resolveLinePositionPolicy } from "./line.js";
|
|
6
7
|
import { resolvePointPositionPolicy } from "./point.js";
|
|
@@ -8,10 +9,13 @@ import { resolveRulePositionPolicy } from "./rule.js";
|
|
|
8
9
|
|
|
9
10
|
const POSITION_POLICIES = Object.freeze({
|
|
10
11
|
area: resolveAreaPositionPolicy,
|
|
12
|
+
arc: resolveArcPositionPolicy,
|
|
11
13
|
bar: resolveBarPositionPolicy,
|
|
12
14
|
line: resolveLinePositionPolicy,
|
|
13
15
|
point: resolvePointPositionPolicy,
|
|
14
|
-
rule: resolveRulePositionPolicy
|
|
16
|
+
rule: resolveRulePositionPolicy,
|
|
17
|
+
text: resolvePointPositionPolicy,
|
|
18
|
+
rect: resolvePointPositionPolicy
|
|
15
19
|
});
|
|
16
20
|
|
|
17
21
|
export function resolveMarkPositionPolicy(context) {
|
|
@@ -14,22 +14,39 @@ function rejectSharedOptions(args, channel) {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export function resolveLinePositionPolicy({
|
|
17
|
+
program,
|
|
17
18
|
layer,
|
|
18
19
|
dataset,
|
|
19
20
|
channel,
|
|
20
21
|
args,
|
|
21
22
|
fieldType
|
|
22
23
|
}) {
|
|
24
|
+
const polar = ["theta", "radius"].includes(channel);
|
|
25
|
+
const config = program.markConfigs[layer.id] ?? {};
|
|
26
|
+
if (polar) {
|
|
27
|
+
if (config.curve !== undefined && config.curve !== "linear") {
|
|
28
|
+
throw new Error("Polar line position currently requires curve \"linear\".");
|
|
29
|
+
}
|
|
30
|
+
rejectSharedOptions(args, channel);
|
|
31
|
+
if (args.aggregate !== undefined) {
|
|
32
|
+
throw new Error(`Line ${channel} encoding does not support aggregate.`);
|
|
33
|
+
}
|
|
34
|
+
return emptyPositionPolicy();
|
|
35
|
+
}
|
|
36
|
+
if (config.closed === true) {
|
|
37
|
+
throw new Error("Line closed requires theta/radius Polar position encodings.");
|
|
38
|
+
}
|
|
23
39
|
const regression = dataset.transform?.some(item => item.type === "regression");
|
|
24
40
|
const interval = dataset.transform?.some(item => item.type === "interval");
|
|
25
41
|
|
|
26
42
|
if (channel === "x") {
|
|
27
|
-
const
|
|
28
|
-
layer.encoding?.y?.
|
|
43
|
+
const directPair =
|
|
44
|
+
layer.encoding?.y?.aggregate === undefined &&
|
|
45
|
+
["quantitative", "temporal"].includes(layer.encoding?.y?.fieldType) &&
|
|
29
46
|
fieldType === "quantitative";
|
|
30
47
|
if (
|
|
31
48
|
fieldType !== "temporal" &&
|
|
32
|
-
!((regression || interval ||
|
|
49
|
+
!((regression || interval || directPair) && fieldType === "quantitative")
|
|
33
50
|
) {
|
|
34
51
|
throw new Error(
|
|
35
52
|
"Line x encoding requires a temporal field or a compatible derived quantitative field."
|
|
@@ -22,12 +22,29 @@ import {
|
|
|
22
22
|
} from "../../../grammar/aggregate.js";
|
|
23
23
|
import { normalizeRuleDatum } from "../../../grammar/rules.js";
|
|
24
24
|
import { resolveMarkPositionPolicy } from "./policies/index.js";
|
|
25
|
+
import {
|
|
26
|
+
getPositionChannelDefinition,
|
|
27
|
+
positionChannelsForFamily
|
|
28
|
+
} from "../../../core/vocabulary.js";
|
|
25
29
|
|
|
26
30
|
const POSITION_ENCODING_OPTIONS = Object.freeze([
|
|
27
31
|
"field", "datum", "target", "fieldType", "scale", "coordinate",
|
|
28
32
|
"aggregate", "bin", "stack"
|
|
29
33
|
]);
|
|
30
34
|
|
|
35
|
+
function validateCoordinateFamily(layer, channel, operation) {
|
|
36
|
+
const family = getPositionChannelDefinition(channel).family;
|
|
37
|
+
const incompatible = positionChannelsForFamily(
|
|
38
|
+
family === "polar" ? "cartesian" : "polar"
|
|
39
|
+
);
|
|
40
|
+
const existing = incompatible.filter(name => layer.encoding?.[name] !== undefined);
|
|
41
|
+
if (existing.length > 0) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`${operation} cannot mix ${channel} with ${existing.join("/")} position encodings on layer "${layer.id}".`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
31
48
|
function resolveCoordinate(program, channel, layer, requestedId) {
|
|
32
49
|
const defaults = getPositionCoordinateDefaults(channel);
|
|
33
50
|
const existingId = layer.coordinate;
|
|
@@ -35,7 +52,19 @@ function resolveCoordinate(program, channel, layer, requestedId) {
|
|
|
35
52
|
if (existingId !== undefined && requestedId !== undefined && existingId !== requestedId) {
|
|
36
53
|
throw new Error(`Layer "${layer.id}" already uses coordinate "${existingId}".`);
|
|
37
54
|
}
|
|
38
|
-
const
|
|
55
|
+
const compatible = program.semanticSpec.coordinates.filter(
|
|
56
|
+
coordinate => coordinate.type === defaults.type
|
|
57
|
+
);
|
|
58
|
+
if (
|
|
59
|
+
existingId === undefined &&
|
|
60
|
+
requestedId === undefined &&
|
|
61
|
+
compatible.length > 1
|
|
62
|
+
) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`${channel} encoding requires coordinate when multiple ${defaults.type} coordinates are available.`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
const id = existingId ?? requestedId ?? compatible[0]?.id ?? defaults.id;
|
|
39
68
|
const coordinate = findCoordinate(program, id);
|
|
40
69
|
if (coordinate !== undefined && coordinate.type !== defaults.type) {
|
|
41
70
|
throw new Error(
|
|
@@ -51,8 +80,9 @@ export function resolvePositionEncoding(program, channel, args, operation) {
|
|
|
51
80
|
const { id: target, dataset, layer } = resolveTarget(
|
|
52
81
|
program,
|
|
53
82
|
args.target,
|
|
54
|
-
|
|
83
|
+
getPositionChannelDefinition(channel).markTypes
|
|
55
84
|
);
|
|
85
|
+
validateCoordinateFamily(layer, channel, operation);
|
|
56
86
|
const hasField = Object.hasOwn(args, "field");
|
|
57
87
|
const hasDatum = Object.hasOwn(args, "datum");
|
|
58
88
|
if (layer.mark.type === "rule") {
|
|
@@ -66,8 +96,11 @@ export function resolvePositionEncoding(program, channel, args, operation) {
|
|
|
66
96
|
throw new Error(`${operation} does not support datum for a ${layer.mark.type} mark.`);
|
|
67
97
|
}
|
|
68
98
|
const previous = layer.encoding?.[channel];
|
|
69
|
-
const requestedFieldType =
|
|
70
|
-
|
|
99
|
+
const requestedFieldType = args.fieldType ?? previous?.fieldType ?? (
|
|
100
|
+
layer.mark.type === "arc" && channel === "theta" && args.aggregate === "count"
|
|
101
|
+
? "nominal"
|
|
102
|
+
: "quantitative"
|
|
103
|
+
);
|
|
71
104
|
const fieldType = requestedFieldType === "nominal"
|
|
72
105
|
? "nominal"
|
|
73
106
|
: validateFieldType(requestedFieldType);
|
|
@@ -98,7 +131,9 @@ export function resolvePositionEncoding(program, channel, args, operation) {
|
|
|
98
131
|
throw new TypeError(`${operation} field must be a non-empty string.`);
|
|
99
132
|
}
|
|
100
133
|
|
|
101
|
-
const aggregateOutput = isAggregate(policy.aggregate)
|
|
134
|
+
const aggregateOutput = isAggregate(policy.aggregate) && !(
|
|
135
|
+
layer.mark.type === "arc" && channel === "theta"
|
|
136
|
+
);
|
|
102
137
|
const requestedScale = resolveReassignmentScaleOptions(
|
|
103
138
|
previous,
|
|
104
139
|
Object.hasOwn(args, "scale") ? args.scale : {}
|
|
@@ -119,9 +154,21 @@ export function resolvePositionEncoding(program, channel, args, operation) {
|
|
|
119
154
|
? { nice: true }
|
|
120
155
|
: { discreteType: "band" }
|
|
121
156
|
: ["ordinal", "nominal"].includes(fieldType)
|
|
122
|
-
? {
|
|
157
|
+
? {
|
|
158
|
+
discreteType: ["arc", "rect"].includes(layer.mark.type)
|
|
159
|
+
? "band"
|
|
160
|
+
: "point"
|
|
161
|
+
}
|
|
123
162
|
: {}
|
|
124
163
|
);
|
|
164
|
+
if (
|
|
165
|
+
layer.mark.type === "arc" &&
|
|
166
|
+
channel === "theta" &&
|
|
167
|
+
["ordinal", "nominal"].includes(fieldType) &&
|
|
168
|
+
scale.type !== "band"
|
|
169
|
+
) {
|
|
170
|
+
throw new Error("Categorical arc theta position requires a band scale.");
|
|
171
|
+
}
|
|
125
172
|
if (Object.hasOwn(scale, "unknown") && layer.mark.type !== "point") {
|
|
126
173
|
throw new Error(
|
|
127
174
|
"Position scale unknown currently requires a row-owned point mark."
|
|
@@ -140,6 +187,8 @@ export function resolvePositionEncoding(program, channel, args, operation) {
|
|
|
140
187
|
throw new TypeError(`Field "${field}" must contain a finite number at row ${index}.`);
|
|
141
188
|
}
|
|
142
189
|
}
|
|
190
|
+
} else if (layer.mark.type === "rect") {
|
|
191
|
+
readScaleField(dataset.values, field, fieldType, { allowUnknown: true });
|
|
143
192
|
} else if (Object.hasOwn(scale, "unknown")) {
|
|
144
193
|
readScaleField(dataset.values, field, fieldType, { allowUnknown: true });
|
|
145
194
|
} else if (fieldType === "temporal") readTemporalField(dataset.values, field);
|
|
@@ -148,11 +197,11 @@ export function resolvePositionEncoding(program, channel, args, operation) {
|
|
|
148
197
|
} else readQuantitativeField(dataset.values, field);
|
|
149
198
|
|
|
150
199
|
if (
|
|
151
|
-
layer.mark.type
|
|
200
|
+
["bar", "rect"].includes(layer.mark.type) &&
|
|
152
201
|
["ordinal", "nominal"].includes(fieldType) &&
|
|
153
202
|
scale.type === "point"
|
|
154
203
|
) {
|
|
155
|
-
throw new Error(
|
|
204
|
+
throw new Error(`Categorical ${layer.mark.type} positions require a band scale.`);
|
|
156
205
|
}
|
|
157
206
|
return {
|
|
158
207
|
target,
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { action } from "../../core/action.js";
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
canMaterializeLine,
|
|
3
|
+
getPositionEncodingMaterializationSteps
|
|
5
4
|
} from "../../materialization/marks.js";
|
|
6
|
-
import { resolveBarGrain } from "../../grammar/bars/policy.js";
|
|
7
5
|
import { resolvePositionEncoding } from "./position/resolve.js";
|
|
8
6
|
import { findLayer } from "../../selectors/layers.js";
|
|
7
|
+
import { applyPositionSemantics } from "./position/apply.js";
|
|
9
8
|
import {
|
|
10
9
|
applyEncodingScale,
|
|
11
10
|
rebindPositionGuides
|
|
@@ -34,69 +33,18 @@ function encodePosition(program, channel, args, operation) {
|
|
|
34
33
|
type: coordinate.type,
|
|
35
34
|
layers: [target]
|
|
36
35
|
});
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
value: hasField ? field : datum
|
|
50
|
-
})
|
|
51
|
-
.editSemantic({
|
|
52
|
-
property: `layer[${target}].encoding.${channel}.fieldType`,
|
|
53
|
-
value: fieldType
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
if (layer.mark.type === "line" && channel === "y" && args.aggregate !== undefined) {
|
|
57
|
-
next = next.editSemantic({
|
|
58
|
-
property: `layer[${target}].encoding.y.aggregate`,
|
|
59
|
-
value: aggregate
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (layer.mark.type === "bar" && channel === "x" && bin !== undefined) {
|
|
64
|
-
const [mode] = Object.keys(bin);
|
|
65
|
-
const previousModes = Object.keys(layer.encoding?.x?.bin ?? {});
|
|
66
|
-
for (const previousMode of previousModes) {
|
|
67
|
-
if (previousMode === mode) continue;
|
|
68
|
-
next = next.editSemantic({
|
|
69
|
-
property: `layer[${target}].encoding.x.bin.${previousMode}`,
|
|
70
|
-
remove: true
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
next = next.editSemantic({
|
|
74
|
-
property: `layer[${target}].encoding.x.bin.${mode}`,
|
|
75
|
-
value: bin[mode]
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (layer.mark.type === "bar") {
|
|
80
|
-
if (aggregate !== undefined) {
|
|
81
|
-
next = next.editSemantic({
|
|
82
|
-
property: `layer[${target}].encoding.${channel}.aggregate`,
|
|
83
|
-
value: aggregate
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
if (stack !== undefined) {
|
|
87
|
-
next = next.editSemantic({
|
|
88
|
-
property: `layer[${target}].encoding.${channel}.stack`,
|
|
89
|
-
value: stack
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (layer.mark.type === "area" && channel === "y" && stack !== undefined) {
|
|
95
|
-
next = next.editSemantic({
|
|
96
|
-
property: `layer[${target}].encoding.y.stack`,
|
|
97
|
-
value: stack
|
|
98
|
-
});
|
|
99
|
-
}
|
|
36
|
+
next = applyPositionSemantics(next, {
|
|
37
|
+
target,
|
|
38
|
+
channel,
|
|
39
|
+
previous,
|
|
40
|
+
field,
|
|
41
|
+
datum,
|
|
42
|
+
hasField,
|
|
43
|
+
fieldType,
|
|
44
|
+
bin,
|
|
45
|
+
aggregate,
|
|
46
|
+
stack
|
|
47
|
+
});
|
|
100
48
|
|
|
101
49
|
next = next.editSemantic({
|
|
102
50
|
property: `layer[${target}].encoding.${channel}.scale`,
|
|
@@ -113,17 +61,6 @@ function encodePosition(program, channel, args, operation) {
|
|
|
113
61
|
target
|
|
114
62
|
);
|
|
115
63
|
|
|
116
|
-
if (layer.mark.type === "line") {
|
|
117
|
-
const updated = findLayer(next, target);
|
|
118
|
-
return canMaterializeLine(next, updated)
|
|
119
|
-
? next.rematerializeLineMark({ id: target })
|
|
120
|
-
: next.rematerializeScale({ id: scale.id });
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (layer.mark.type === "rule") {
|
|
124
|
-
return next.rematerializeRuleMark({ id: target });
|
|
125
|
-
}
|
|
126
|
-
|
|
127
64
|
if (layer.mark.type === "bar") {
|
|
128
65
|
const updated = findLayer(next, target);
|
|
129
66
|
const pendingBox = next.markConfigs[target]?.boxPlot;
|
|
@@ -132,22 +69,17 @@ function encodePosition(program, channel, args, operation) {
|
|
|
132
69
|
? next.materializeBoxPlot({ id: target })
|
|
133
70
|
: next;
|
|
134
71
|
}
|
|
135
|
-
const materialized = resolveBarGrain(updated) !== undefined
|
|
136
|
-
? next.rematerializeBarMark({ id: target })
|
|
137
|
-
: next.rematerializeScale({ id: scale.id });
|
|
138
|
-
return materialized;
|
|
139
72
|
}
|
|
140
73
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
74
|
+
const updated = findLayer(next, target);
|
|
75
|
+
for (const step of getPositionEncodingMaterializationSteps(
|
|
76
|
+
next,
|
|
77
|
+
updated,
|
|
78
|
+
scale.id
|
|
79
|
+
)) {
|
|
80
|
+
next = next[step.op](step.args);
|
|
147
81
|
}
|
|
148
|
-
return
|
|
149
|
-
? next.rematerializePointMark({ id: target })
|
|
150
|
-
: next;
|
|
82
|
+
return next;
|
|
151
83
|
}
|
|
152
84
|
|
|
153
85
|
const encodeX = action(
|
|
@@ -170,7 +102,29 @@ const encodeY = action(
|
|
|
170
102
|
}
|
|
171
103
|
);
|
|
172
104
|
|
|
105
|
+
const encodeTheta = action(
|
|
106
|
+
{
|
|
107
|
+
op: "encodeTheta",
|
|
108
|
+
description: "Encode a field as Polar angle in clockwise degrees."
|
|
109
|
+
},
|
|
110
|
+
function (args = {}) {
|
|
111
|
+
return encodePosition(this, "theta", args, "encodeTheta");
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const encodeR = action(
|
|
116
|
+
{
|
|
117
|
+
op: "encodeR",
|
|
118
|
+
description: "Encode a quantitative field as Polar radius."
|
|
119
|
+
},
|
|
120
|
+
function (args = {}) {
|
|
121
|
+
return encodePosition(this, "radius", args, "encodeR");
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
|
|
173
125
|
export function registerPositionEncodingActions(ProgramClass) {
|
|
174
126
|
ProgramClass.prototype.encodeX = encodeX;
|
|
175
127
|
ProgramClass.prototype.encodeY = encodeY;
|
|
128
|
+
ProgramClass.prototype.encodeTheta = encodeTheta;
|
|
129
|
+
ProgramClass.prototype.encodeR = encodeR;
|
|
176
130
|
}
|