ggaction 0.0.11 → 0.0.12
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 +31 -0
- package/README.md +2 -1
- package/knowledge/action-cards.json +1 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/knowledge/task-resolver.js +1 -1
- package/package.json +1 -1
- package/src/actions/encodings/offset.js +33 -21
- package/src/actions/encodings/position/apply.js +30 -0
- package/src/actions/encodings/position/policies/arc.js +23 -2
- package/src/actions/encodings/position/resolve.js +12 -2
- package/src/actions/errorBars/create.js +138 -7
- package/src/actions/errorBars/edit.js +10 -0
- package/src/actions/marks/point/materialize.js +33 -13
- package/src/actions/marks/rule/actions.js +26 -1
- package/src/core/vocabulary.js +2 -2
- package/src/grammar/arcs.js +156 -65
- package/src/grammar/positionCompatibility.js +1 -1
- package/src/grammar/rules.js +2 -0
- package/src/grammar/scales/ordinal.js +23 -0
- package/src/layout/facets.js +45 -16
- package/src/materialization/facetGuides/legacyCategorical.js +7 -1
- package/src/materialization/facetGuides/placement.js +16 -1
- package/src/materialization/facetGuides/preparation.js +26 -1
- package/src/materialization/facets.js +3 -1
- package/src/materialization/marks/capabilities.js +10 -2
- package/src/materialization/marks/index.js +5 -2
- package/src/materialization/rowEncoding.js +57 -1
- package/src/materialization/scales/policies/bar.js +0 -43
- package/src/materialization/scales/policies/offset.js +54 -0
- package/src/materialization/scales/resolve.js +2 -4
- package/types/program.d.ts +18 -0
|
@@ -80,6 +80,28 @@ function applyArcThetaWeight(program, target, channel, previous, aggregate, weig
|
|
|
80
80
|
});
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
function removeStaleArcThetaAggregate(
|
|
84
|
+
program,
|
|
85
|
+
target,
|
|
86
|
+
channel,
|
|
87
|
+
previous,
|
|
88
|
+
fieldType,
|
|
89
|
+
aggregate
|
|
90
|
+
) {
|
|
91
|
+
if (
|
|
92
|
+
channel !== "theta" ||
|
|
93
|
+
fieldType !== "quantitative" ||
|
|
94
|
+
aggregate !== undefined ||
|
|
95
|
+
previous?.aggregate === undefined
|
|
96
|
+
) {
|
|
97
|
+
return program;
|
|
98
|
+
}
|
|
99
|
+
return program.editSemantic({
|
|
100
|
+
property: `layer[${target}].encoding.theta.aggregate`,
|
|
101
|
+
remove: true
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
83
105
|
export function applyPositionSemantics(program, {
|
|
84
106
|
target,
|
|
85
107
|
channel,
|
|
@@ -124,6 +146,14 @@ export function applyPositionSemantics(program, {
|
|
|
124
146
|
value
|
|
125
147
|
});
|
|
126
148
|
}
|
|
149
|
+
next = removeStaleArcThetaAggregate(
|
|
150
|
+
next,
|
|
151
|
+
target,
|
|
152
|
+
channel,
|
|
153
|
+
previous,
|
|
154
|
+
fieldType,
|
|
155
|
+
aggregate
|
|
156
|
+
);
|
|
127
157
|
return applyArcThetaWeight(
|
|
128
158
|
next,
|
|
129
159
|
target,
|
|
@@ -2,7 +2,7 @@ import { validateAggregate } from "../../../../grammar/aggregate.js";
|
|
|
2
2
|
import { validateNonEmptyString } from "../../../../core/validation.js";
|
|
3
3
|
import { emptyPositionPolicy } from "./common.js";
|
|
4
4
|
|
|
5
|
-
export function resolveArcPositionPolicy({ channel, args, fieldType }) {
|
|
5
|
+
export function resolveArcPositionPolicy({ channel, args, fieldType, layer }) {
|
|
6
6
|
if (args.bin !== undefined) {
|
|
7
7
|
throw new Error(`Arc ${channel} encoding does not support bin.`);
|
|
8
8
|
}
|
|
@@ -10,6 +10,11 @@ export function resolveArcPositionPolicy({ channel, args, fieldType }) {
|
|
|
10
10
|
throw new Error(`Arc ${channel} encoding does not support stack.`);
|
|
11
11
|
}
|
|
12
12
|
if (channel === "radius") {
|
|
13
|
+
if (layer.encoding?.theta?.fieldType === "quantitative") {
|
|
14
|
+
throw new Error(
|
|
15
|
+
"Arc radius encoding cannot be combined with quantitative theta."
|
|
16
|
+
);
|
|
17
|
+
}
|
|
13
18
|
if (args.aggregate !== undefined) {
|
|
14
19
|
throw new Error("Arc radius encoding does not support aggregate.");
|
|
15
20
|
}
|
|
@@ -18,8 +23,24 @@ export function resolveArcPositionPolicy({ channel, args, fieldType }) {
|
|
|
18
23
|
}
|
|
19
24
|
return emptyPositionPolicy();
|
|
20
25
|
}
|
|
26
|
+
if (fieldType === "quantitative") {
|
|
27
|
+
if (layer.encoding?.radius !== undefined) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
"Quantitative arc theta cannot be combined with radius encoding."
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
if (args.aggregate !== undefined) {
|
|
33
|
+
throw new Error("Quantitative arc theta does not support aggregate.");
|
|
34
|
+
}
|
|
35
|
+
if (args.weight !== undefined) {
|
|
36
|
+
throw new Error("Quantitative arc theta does not support weight.");
|
|
37
|
+
}
|
|
38
|
+
return emptyPositionPolicy();
|
|
39
|
+
}
|
|
21
40
|
if (!["ordinal", "nominal"].includes(fieldType)) {
|
|
22
|
-
throw new Error(
|
|
41
|
+
throw new Error(
|
|
42
|
+
"Arc theta encoding requires a quantitative, ordinal, or nominal field."
|
|
43
|
+
);
|
|
23
44
|
}
|
|
24
45
|
if (args.aggregate === undefined) {
|
|
25
46
|
if (args.weight !== undefined) {
|
|
@@ -22,7 +22,10 @@ import {
|
|
|
22
22
|
validateAggregateFieldValues,
|
|
23
23
|
} from "../../../grammar/aggregate.js";
|
|
24
24
|
import { normalizeRuleDatum } from "../../../grammar/rules.js";
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
readArcThetaValues,
|
|
27
|
+
readArcThetaWeights
|
|
28
|
+
} from "../../../grammar/arcs.js";
|
|
26
29
|
import { resolveMarkPositionPolicy } from "./policies/index.js";
|
|
27
30
|
import {
|
|
28
31
|
getPositionChannelDefinition,
|
|
@@ -128,9 +131,14 @@ export function resolvePositionEncoding(program, channel, args, operation) {
|
|
|
128
131
|
: args.field;
|
|
129
132
|
const datum = args.datum;
|
|
130
133
|
const effectiveArgs = { ...args };
|
|
134
|
+
const directQuantitativeArcTheta =
|
|
135
|
+
layer.mark.type === "arc" &&
|
|
136
|
+
channel === "theta" &&
|
|
137
|
+
fieldType === "quantitative";
|
|
131
138
|
for (const property of ["aggregate", "bin", "stack"]) {
|
|
132
139
|
if (!Object.hasOwn(effectiveArgs, property) && previous !== undefined &&
|
|
133
|
-
Object.hasOwn(previous, property)
|
|
140
|
+
Object.hasOwn(previous, property) &&
|
|
141
|
+
!(directQuantitativeArcTheta && property === "aggregate")) {
|
|
134
142
|
effectiveArgs[property] = previous[property];
|
|
135
143
|
}
|
|
136
144
|
}
|
|
@@ -220,6 +228,8 @@ export function resolvePositionEncoding(program, channel, args, operation) {
|
|
|
220
228
|
policy.aggregate === "sum"
|
|
221
229
|
) {
|
|
222
230
|
readArcThetaWeights(dataset.values, policy.weight, layer.id);
|
|
231
|
+
} else if (directQuantitativeArcTheta) {
|
|
232
|
+
readArcThetaValues(dataset.values, field, layer.id);
|
|
223
233
|
} else if (program.markConfigs[target]?.boxPlot !== undefined) {
|
|
224
234
|
for (const [index, row] of dataset.values.entries()) {
|
|
225
235
|
const value = row[field];
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { action } from "../../core/action.js";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
validateKeys,
|
|
4
|
+
validateOptionObject
|
|
5
|
+
} from "../../core/validation.js";
|
|
3
6
|
import { findLayer } from "../../selectors/layers.js";
|
|
7
|
+
import { findDataset } from "../../selectors/datasets.js";
|
|
4
8
|
import { DEFAULT_COLORS } from "../../theme/defaults.js";
|
|
9
|
+
import {
|
|
10
|
+
readNominalField,
|
|
11
|
+
validateCategoricalFieldType
|
|
12
|
+
} from "../../grammar/scales/index.js";
|
|
13
|
+
import { normalizeOffsetPadding } from "../../grammar/bars/geometry.js";
|
|
5
14
|
import { createResolvedIntervalData, ownOptions } from
|
|
6
15
|
"../data/intervalEdit.js";
|
|
7
16
|
import { resolveIntervalComposite } from "../intervals/resolve.js";
|
|
@@ -13,6 +22,8 @@ const OPTIONS = Object.freeze([
|
|
|
13
22
|
"data",
|
|
14
23
|
"x",
|
|
15
24
|
"y",
|
|
25
|
+
"xOffset",
|
|
26
|
+
"yOffset",
|
|
16
27
|
"groupBy",
|
|
17
28
|
"coordinate",
|
|
18
29
|
"caps",
|
|
@@ -23,30 +34,88 @@ const OPTIONS = Object.freeze([
|
|
|
23
34
|
"opacity"
|
|
24
35
|
]);
|
|
25
36
|
|
|
37
|
+
const OFFSET_OPTIONS = Object.freeze([
|
|
38
|
+
"field", "fieldType", "scale", "paddingInner", "paddingOuter"
|
|
39
|
+
]);
|
|
40
|
+
|
|
26
41
|
const ERROR_BAR_POLICY = Object.freeze({
|
|
27
42
|
operation: "createErrorBar",
|
|
28
43
|
resourceLabel: "error-bar",
|
|
29
44
|
defaultId: "errorBar",
|
|
30
45
|
ownerLabel: "Error-bar id",
|
|
31
|
-
positionTypes: Object.freeze([
|
|
46
|
+
positionTypes: Object.freeze([
|
|
47
|
+
"quantitative", "nominal", "ordinal", "temporal"
|
|
48
|
+
]),
|
|
32
49
|
defaultPositionType: "nominal",
|
|
33
50
|
defaultIntervalChannel: "y",
|
|
34
51
|
scaleDefaults: () => ({}),
|
|
35
52
|
intervalScaleDefaults: Object.freeze({ nice: true, zero: false }),
|
|
36
53
|
allowExplicitGrouping: false,
|
|
37
54
|
ambiguousMessage:
|
|
38
|
-
"createErrorBar requires one quantitative interval axis and one
|
|
55
|
+
"createErrorBar requires one quantitative interval axis and one compatible position axis; use interval options to disambiguate two quantitative channels."
|
|
39
56
|
});
|
|
40
57
|
|
|
41
58
|
function resolveErrorBar(program, args) {
|
|
42
59
|
const resolved = resolveIntervalComposite(program, args, ERROR_BAR_POLICY);
|
|
60
|
+
const offset = resolveErrorBarOffset(program, args, resolved);
|
|
61
|
+
const groupBy = resolved.interval.mode === "statistical" && offset !== undefined
|
|
62
|
+
? [...new Set([...resolved.groupBy, offset.field])]
|
|
63
|
+
: resolved.groupBy;
|
|
43
64
|
return {
|
|
44
65
|
...resolved,
|
|
66
|
+
groupBy,
|
|
67
|
+
offset,
|
|
45
68
|
lowerCapId: `${resolved.id}LowerCap`,
|
|
46
69
|
upperCapId: `${resolved.id}UpperCap`
|
|
47
70
|
};
|
|
48
71
|
}
|
|
49
72
|
|
|
73
|
+
function resolveErrorBarOffset(program, args, resolved) {
|
|
74
|
+
const channel = `${resolved.position.channel}Offset`;
|
|
75
|
+
const incompatible = channel === "xOffset" ? "yOffset" : "xOffset";
|
|
76
|
+
if (args[incompatible] !== undefined) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`createErrorBar ${incompatible} does not match the ${resolved.orientation} interval orientation.`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
const explicit = args[channel] === undefined
|
|
82
|
+
? undefined
|
|
83
|
+
: validateOptionObject(args[channel], OFFSET_OPTIONS, `createErrorBar ${channel}`);
|
|
84
|
+
const inferred = resolved.sourceLayer?.encoding?.[channel];
|
|
85
|
+
if (explicit === undefined && inferred === undefined) return undefined;
|
|
86
|
+
if (!["nominal", "ordinal"].includes(resolved.position.fieldType)) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`createErrorBar ${channel} requires a categorical ${resolved.position.channel} position.`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
const field = explicit?.field ?? inferred?.field;
|
|
92
|
+
if (typeof field !== "string" || field.length === 0) {
|
|
93
|
+
throw new TypeError(`createErrorBar ${channel} field must be a non-empty string.`);
|
|
94
|
+
}
|
|
95
|
+
const fieldType = validateCategoricalFieldType(
|
|
96
|
+
explicit?.fieldType ?? inferred?.fieldType ?? "nominal"
|
|
97
|
+
);
|
|
98
|
+
const dataset = findDataset(program, resolved.source);
|
|
99
|
+
readNominalField(dataset.values, field);
|
|
100
|
+
const padding = normalizeOffsetPadding(
|
|
101
|
+
explicit ?? {},
|
|
102
|
+
resolved.sourceLayer === undefined
|
|
103
|
+
? undefined
|
|
104
|
+
: program.markConfigs[resolved.sourceLayer.id]?.[channel],
|
|
105
|
+
channel
|
|
106
|
+
);
|
|
107
|
+
return {
|
|
108
|
+
channel,
|
|
109
|
+
field,
|
|
110
|
+
fieldType,
|
|
111
|
+
scale: {
|
|
112
|
+
...(inferred?.scale === undefined ? {} : { id: inferred.scale }),
|
|
113
|
+
...(explicit?.scale ?? {})
|
|
114
|
+
},
|
|
115
|
+
...padding
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
50
119
|
function resolveAppearance(args) {
|
|
51
120
|
return resolveErrorBarAppearance(ownOptions(args, [
|
|
52
121
|
"caps", "capSize", "stroke", "strokeWidth", "strokeDash", "opacity"
|
|
@@ -72,15 +141,26 @@ export const createErrorBarCap = action(
|
|
|
72
141
|
validateKeys(args, [
|
|
73
142
|
"id", "data", "orientation", "positionField", "positionFieldType",
|
|
74
143
|
"intervalField", "coordinate", "positionScale", "intervalScale",
|
|
144
|
+
"offsetChannel", "offsetField", "offsetFieldType", "offsetScale",
|
|
145
|
+
"offsetPaddingInner", "offsetPaddingOuter",
|
|
75
146
|
"capSize", "stroke", "strokeWidth", "strokeDash", "opacity"
|
|
76
147
|
], "createErrorBarCap");
|
|
77
148
|
if (!["vertical", "horizontal"].includes(args.orientation)) {
|
|
78
149
|
throw new Error(`Unsupported error-bar orientation "${args.orientation}".`);
|
|
79
150
|
}
|
|
80
151
|
const vertical = args.orientation === "vertical";
|
|
152
|
+
const expectedOffset = vertical ? "xOffset" : "yOffset";
|
|
153
|
+
if (
|
|
154
|
+
args.offsetChannel !== undefined &&
|
|
155
|
+
args.offsetChannel !== expectedOffset
|
|
156
|
+
) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
`createErrorBarCap requires ${expectedOffset} for ${args.orientation} intervals.`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
81
161
|
const positionAction = vertical ? "encodeX" : "encodeY";
|
|
82
162
|
const intervalAction = vertical ? "encodeY" : "encodeX";
|
|
83
|
-
|
|
163
|
+
let next = this
|
|
84
164
|
.createRuleMark({ id: args.id, data: args.data })
|
|
85
165
|
[positionAction]({
|
|
86
166
|
target: args.id,
|
|
@@ -95,7 +175,21 @@ export const createErrorBarCap = action(
|
|
|
95
175
|
fieldType: "quantitative",
|
|
96
176
|
coordinate: args.coordinate,
|
|
97
177
|
scale: { id: args.intervalScale }
|
|
98
|
-
})
|
|
178
|
+
});
|
|
179
|
+
if (args.offsetChannel !== undefined) {
|
|
180
|
+
const offsetAction = args.offsetChannel === "xOffset"
|
|
181
|
+
? "encodeXOffset"
|
|
182
|
+
: "encodeYOffset";
|
|
183
|
+
next = next[offsetAction]({
|
|
184
|
+
target: args.id,
|
|
185
|
+
field: args.offsetField,
|
|
186
|
+
fieldType: args.offsetFieldType,
|
|
187
|
+
scale: { id: args.offsetScale },
|
|
188
|
+
paddingInner: args.offsetPaddingInner,
|
|
189
|
+
paddingOuter: args.offsetPaddingOuter
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
return next
|
|
99
193
|
.encodeStroke({ target: args.id, value: args.stroke })
|
|
100
194
|
.encodeStrokeWidth({ target: args.id, value: args.strokeWidth })
|
|
101
195
|
.encodeStrokeDash({ target: args.id, value: args.strokeDash })
|
|
@@ -118,6 +212,19 @@ function positionArgs(resolved) {
|
|
|
118
212
|
};
|
|
119
213
|
}
|
|
120
214
|
|
|
215
|
+
function offsetArgs(resolved) {
|
|
216
|
+
return resolved.offset === undefined
|
|
217
|
+
? undefined
|
|
218
|
+
: {
|
|
219
|
+
target: resolved.id,
|
|
220
|
+
field: resolved.offset.field,
|
|
221
|
+
fieldType: resolved.offset.fieldType,
|
|
222
|
+
scale: resolved.offset.scale,
|
|
223
|
+
paddingInner: resolved.offset.paddingInner,
|
|
224
|
+
paddingOuter: resolved.offset.paddingOuter
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
121
228
|
function intervalArgs(resolved, field) {
|
|
122
229
|
return {
|
|
123
230
|
target: resolved.id,
|
|
@@ -154,8 +261,14 @@ export const createErrorBar = action(
|
|
|
154
261
|
target: resolved.id,
|
|
155
262
|
field: resolved.fields.upper,
|
|
156
263
|
fieldType: "quantitative"
|
|
157
|
-
})
|
|
158
|
-
|
|
264
|
+
});
|
|
265
|
+
if (resolved.offset !== undefined) {
|
|
266
|
+
const offsetAction = resolved.offset.channel === "xOffset"
|
|
267
|
+
? "encodeXOffset"
|
|
268
|
+
: "encodeYOffset";
|
|
269
|
+
next = next[offsetAction](offsetArgs(resolved));
|
|
270
|
+
}
|
|
271
|
+
next = next.encodeStroke({ target: resolved.id, value: appearance.stroke })
|
|
159
272
|
.encodeStrokeWidth({ target: resolved.id, value: appearance.strokeWidth })
|
|
160
273
|
.encodeStrokeDash({ target: resolved.id, value: appearance.strokeDash })
|
|
161
274
|
.encodeOpacity({ target: resolved.id, value: appearance.opacity });
|
|
@@ -173,6 +286,16 @@ export const createErrorBar = action(
|
|
|
173
286
|
coordinate: resolved.coordinate,
|
|
174
287
|
positionScale: intervalLayer.encoding[resolved.position.channel].scale,
|
|
175
288
|
intervalScale: intervalLayer.encoding[resolved.interval.channel].scale,
|
|
289
|
+
...(resolved.offset === undefined
|
|
290
|
+
? {}
|
|
291
|
+
: {
|
|
292
|
+
offsetChannel: resolved.offset.channel,
|
|
293
|
+
offsetField: resolved.offset.field,
|
|
294
|
+
offsetFieldType: resolved.offset.fieldType,
|
|
295
|
+
offsetScale: intervalLayer.encoding[resolved.offset.channel].scale,
|
|
296
|
+
offsetPaddingInner: resolved.offset.paddingInner,
|
|
297
|
+
offsetPaddingOuter: resolved.offset.paddingOuter
|
|
298
|
+
}),
|
|
176
299
|
capSize: appearance.capSize,
|
|
177
300
|
stroke: appearance.stroke,
|
|
178
301
|
strokeWidth: appearance.strokeWidth,
|
|
@@ -193,6 +316,14 @@ export const createErrorBar = action(
|
|
|
193
316
|
coordinate: resolved.coordinate,
|
|
194
317
|
positionScale: intervalLayer.encoding[resolved.position.channel].scale,
|
|
195
318
|
intervalScale: intervalLayer.encoding[resolved.interval.channel].scale,
|
|
319
|
+
...(resolved.offset === undefined
|
|
320
|
+
? {}
|
|
321
|
+
: {
|
|
322
|
+
offset: {
|
|
323
|
+
...resolved.offset,
|
|
324
|
+
scale: intervalLayer.encoding[resolved.offset.channel].scale
|
|
325
|
+
}
|
|
326
|
+
}),
|
|
196
327
|
lowerCapId: resolved.lowerCapId,
|
|
197
328
|
upperCapId: resolved.upperCapId,
|
|
198
329
|
...appearance
|
|
@@ -94,6 +94,16 @@ function capArgs(config, id, intervalField) {
|
|
|
94
94
|
coordinate: config.coordinate,
|
|
95
95
|
positionScale: config.positionScale,
|
|
96
96
|
intervalScale: config.intervalScale,
|
|
97
|
+
...(config.offset === undefined
|
|
98
|
+
? {}
|
|
99
|
+
: {
|
|
100
|
+
offsetChannel: config.offset.channel,
|
|
101
|
+
offsetField: config.offset.field,
|
|
102
|
+
offsetFieldType: config.offset.fieldType,
|
|
103
|
+
offsetScale: config.offset.scale,
|
|
104
|
+
offsetPaddingInner: config.offset.paddingInner,
|
|
105
|
+
offsetPaddingOuter: config.offset.paddingOuter
|
|
106
|
+
}),
|
|
97
107
|
capSize: config.capSize,
|
|
98
108
|
stroke: config.stroke,
|
|
99
109
|
strokeWidth: config.strokeWidth,
|
|
@@ -22,7 +22,10 @@ import {
|
|
|
22
22
|
import { findDataset } from "../../../selectors/datasets.js";
|
|
23
23
|
import { findLayer } from "../../../selectors/layers.js";
|
|
24
24
|
import { rematerializeHighlightBaseline } from "../lifecycle.js";
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
resolveRowEncodingValues,
|
|
27
|
+
resolveRowPositionValues
|
|
28
|
+
} from
|
|
26
29
|
"../../../materialization/rowEncoding.js";
|
|
27
30
|
|
|
28
31
|
const REMATERIALIZE_OPTIONS = Object.freeze(["id"]);
|
|
@@ -33,8 +36,8 @@ function resolvePointPositions(program, layer, dataset) {
|
|
|
33
36
|
layer.encoding?.radius !== undefined;
|
|
34
37
|
if (!hasPolar) {
|
|
35
38
|
return {
|
|
36
|
-
x:
|
|
37
|
-
y:
|
|
39
|
+
x: resolveRowPositionValues(program, layer, dataset, "x"),
|
|
40
|
+
y: resolveRowPositionValues(program, layer, dataset, "y")
|
|
38
41
|
};
|
|
39
42
|
}
|
|
40
43
|
const theta = resolveRowEncodingValues(program, layer, dataset, "theta");
|
|
@@ -222,8 +225,20 @@ export const rematerializePointMark = action(
|
|
|
222
225
|
resetValue: []
|
|
223
226
|
});
|
|
224
227
|
if (highlighted !== undefined) return highlighted;
|
|
225
|
-
|
|
226
|
-
const
|
|
228
|
+
let resolved = this;
|
|
229
|
+
const initialLayer = findLayer(resolved, id);
|
|
230
|
+
for (const channel of ["xOffset", "yOffset"]) {
|
|
231
|
+
const scaleId = initialLayer?.encoding?.[channel]?.scale;
|
|
232
|
+
if (scaleId !== undefined) {
|
|
233
|
+
resolved = resolved.rematerializeScale({
|
|
234
|
+
id: scaleId,
|
|
235
|
+
guides: false,
|
|
236
|
+
marks: false
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const layer = findLayer(resolved, id);
|
|
241
|
+
const graphic = resolved.graphicSpec.objects[id];
|
|
227
242
|
|
|
228
243
|
if (layer?.mark?.type !== "point") {
|
|
229
244
|
throw new Error(`Unknown point mark "${id}".`);
|
|
@@ -231,18 +246,23 @@ export const rematerializePointMark = action(
|
|
|
231
246
|
if (!["circle", "rect", "path", "collection"].includes(graphic?.type)) {
|
|
232
247
|
throw new Error(`Point mark "${id}" requires point graphics.`);
|
|
233
248
|
}
|
|
234
|
-
const dataset = findDataset(
|
|
249
|
+
const dataset = findDataset(resolved, layer.data);
|
|
235
250
|
if (dataset === undefined) {
|
|
236
251
|
throw new Error(`Point mark "${id}" requires an existing dataset.`);
|
|
237
252
|
}
|
|
238
253
|
|
|
239
|
-
const positions = resolvePointPositions(
|
|
240
|
-
const mappedFill = resolveRowEncodingValues(
|
|
241
|
-
const area = resolveRowEncodingValues(
|
|
242
|
-
const encodedShape = resolveRowEncodingValues(
|
|
243
|
-
const encodedOpacity = resolveRowEncodingValues(
|
|
254
|
+
const positions = resolvePointPositions(resolved, layer, dataset);
|
|
255
|
+
const mappedFill = resolveRowEncodingValues(resolved, layer, dataset, "color");
|
|
256
|
+
const area = resolveRowEncodingValues(resolved, layer, dataset, "size");
|
|
257
|
+
const encodedShape = resolveRowEncodingValues(resolved, layer, dataset, "shape");
|
|
258
|
+
const encodedOpacity = resolveRowEncodingValues(
|
|
259
|
+
resolved,
|
|
260
|
+
layer,
|
|
261
|
+
dataset,
|
|
262
|
+
"opacity"
|
|
263
|
+
);
|
|
244
264
|
const angles = resolveDirectionValues(dataset.values, layer.encoding?.angle);
|
|
245
|
-
const config =
|
|
265
|
+
const config = resolved.markConfigs[id] ?? {};
|
|
246
266
|
const fill = mappedFill ?? config.fill ?? DEFAULT_POINT_FILL;
|
|
247
267
|
const shapes = encodedShape ?? dataset.values.map(() => config.shape ?? "circle");
|
|
248
268
|
const existingChildren = graphic.items ?? [];
|
|
@@ -252,7 +272,7 @@ export const rematerializePointMark = action(
|
|
|
252
272
|
angles !== undefined ||
|
|
253
273
|
graphic.type === "collection" ||
|
|
254
274
|
getPointGraphicType(constantShape) !== graphic.type;
|
|
255
|
-
const jittered = applyPointJitter(
|
|
275
|
+
const jittered = applyPointJitter(resolved, {
|
|
256
276
|
id,
|
|
257
277
|
layer,
|
|
258
278
|
dataset,
|
|
@@ -3,6 +3,7 @@ import { validateUserId } from "../../../core/identifiers.js";
|
|
|
3
3
|
import { validatePositiveFinite } from "../../../core/validation.js";
|
|
4
4
|
import {
|
|
5
5
|
mapContinuousScaleValues,
|
|
6
|
+
mapOrdinalOffsetValues,
|
|
6
7
|
mapOrdinalPositionValues,
|
|
7
8
|
mapOrdinalValues,
|
|
8
9
|
normalizeStrokeDashPattern
|
|
@@ -25,6 +26,8 @@ import {
|
|
|
25
26
|
import { resolveMarkGraphicPlacement } from
|
|
26
27
|
"../../../materialization/graphicHierarchy.js";
|
|
27
28
|
import { rematerializeHighlightBaseline } from "../lifecycle.js";
|
|
29
|
+
import { applyOffsetPositionValues } from
|
|
30
|
+
"../../../materialization/rowEncoding.js";
|
|
28
31
|
|
|
29
32
|
const CREATE_OPTIONS = Object.freeze(["id", "data"]);
|
|
30
33
|
const REMATERIALIZE_OPTIONS = Object.freeze(["id"]);
|
|
@@ -154,7 +157,10 @@ const rematerializeRuleMark = action(
|
|
|
154
157
|
}
|
|
155
158
|
|
|
156
159
|
const scaleIds = [...new Set(
|
|
157
|
-
[
|
|
160
|
+
[
|
|
161
|
+
"x", "y", "x2", "y2", "xOffset", "yOffset",
|
|
162
|
+
"strokeDash", "strokeWidth", "opacity"
|
|
163
|
+
]
|
|
158
164
|
.map(channel => layer.encoding?.[channel]?.scale)
|
|
159
165
|
.filter(scale => scale !== undefined)
|
|
160
166
|
)];
|
|
@@ -178,6 +184,25 @@ const rematerializeRuleMark = action(
|
|
|
178
184
|
resolved.resolvedScales[encoding.scale]
|
|
179
185
|
);
|
|
180
186
|
}
|
|
187
|
+
for (const [channel, offsetChannel] of [
|
|
188
|
+
["x", "xOffset"],
|
|
189
|
+
["y", "yOffset"]
|
|
190
|
+
]) {
|
|
191
|
+
const encoding = layer.encoding?.[offsetChannel];
|
|
192
|
+
if (encoding === undefined || mapped[channel] === undefined) continue;
|
|
193
|
+
const offsetScale = resolved.resolvedScales[encoding.scale];
|
|
194
|
+
const offset = mapOrdinalOffsetValues(
|
|
195
|
+
derived.values[offsetChannel],
|
|
196
|
+
offsetScale
|
|
197
|
+
);
|
|
198
|
+
mapped[channel] = applyOffsetPositionValues({
|
|
199
|
+
base: mapped[channel],
|
|
200
|
+
offset,
|
|
201
|
+
parentScale: resolved.resolvedScales[layer.encoding[channel].scale],
|
|
202
|
+
offsetScale,
|
|
203
|
+
channel
|
|
204
|
+
});
|
|
205
|
+
}
|
|
181
206
|
const bounds = resolveGraphicBounds(resolved);
|
|
182
207
|
const x1 = [];
|
|
183
208
|
const y1 = [];
|
package/src/core/vocabulary.js
CHANGED
|
@@ -86,14 +86,14 @@ export const POSITION_CHANNEL_DEFINITIONS = Object.freeze({
|
|
|
86
86
|
role: "offset",
|
|
87
87
|
scaleChannel: "xOffset",
|
|
88
88
|
coordinate: Object.freeze({ id: "main", type: "cartesian" }),
|
|
89
|
-
markTypes: Object.freeze(["bar"])
|
|
89
|
+
markTypes: Object.freeze(["bar", "point", "rule"])
|
|
90
90
|
}),
|
|
91
91
|
yOffset: Object.freeze({
|
|
92
92
|
family: "cartesian",
|
|
93
93
|
role: "offset",
|
|
94
94
|
scaleChannel: "yOffset",
|
|
95
95
|
coordinate: Object.freeze({ id: "main", type: "cartesian" }),
|
|
96
|
-
markTypes: Object.freeze(["bar"])
|
|
96
|
+
markTypes: Object.freeze(["bar", "point", "rule"])
|
|
97
97
|
}),
|
|
98
98
|
theta: Object.freeze({
|
|
99
99
|
family: "polar",
|