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.
Files changed (32) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +2 -1
  3. package/knowledge/action-cards.json +1 -1
  4. package/knowledge/intent-taxonomy.json +1 -1
  5. package/knowledge/mcp-resources.json +1 -1
  6. package/knowledge/task-resolver.js +1 -1
  7. package/package.json +1 -1
  8. package/src/actions/encodings/offset.js +33 -21
  9. package/src/actions/encodings/position/apply.js +30 -0
  10. package/src/actions/encodings/position/policies/arc.js +23 -2
  11. package/src/actions/encodings/position/resolve.js +12 -2
  12. package/src/actions/errorBars/create.js +138 -7
  13. package/src/actions/errorBars/edit.js +10 -0
  14. package/src/actions/marks/point/materialize.js +33 -13
  15. package/src/actions/marks/rule/actions.js +26 -1
  16. package/src/core/vocabulary.js +2 -2
  17. package/src/grammar/arcs.js +156 -65
  18. package/src/grammar/positionCompatibility.js +1 -1
  19. package/src/grammar/rules.js +2 -0
  20. package/src/grammar/scales/ordinal.js +23 -0
  21. package/src/layout/facets.js +45 -16
  22. package/src/materialization/facetGuides/legacyCategorical.js +7 -1
  23. package/src/materialization/facetGuides/placement.js +16 -1
  24. package/src/materialization/facetGuides/preparation.js +26 -1
  25. package/src/materialization/facets.js +3 -1
  26. package/src/materialization/marks/capabilities.js +10 -2
  27. package/src/materialization/marks/index.js +5 -2
  28. package/src/materialization/rowEncoding.js +57 -1
  29. package/src/materialization/scales/policies/bar.js +0 -43
  30. package/src/materialization/scales/policies/offset.js +54 -0
  31. package/src/materialization/scales/resolve.js +2 -4
  32. 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("Arc theta encoding requires an ordinal or nominal field.");
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 { readArcThetaWeights } from "../../../grammar/arcs.js";
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 { validateKeys } from "../../core/validation.js";
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(["nominal", "ordinal", "temporal"]),
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 nominal, ordinal, or temporal position axis."
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
- return this
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
- .encodeStroke({ target: resolved.id, value: appearance.stroke })
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 { resolveRowEncodingValues } from
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: resolveRowEncodingValues(program, layer, dataset, "x"),
37
- y: resolveRowEncodingValues(program, layer, dataset, "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
- const layer = findLayer(this, id);
226
- const graphic = this.graphicSpec.objects[id];
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(this, layer.data);
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(this, layer, dataset);
240
- const mappedFill = resolveRowEncodingValues(this, layer, dataset, "color");
241
- const area = resolveRowEncodingValues(this, layer, dataset, "size");
242
- const encodedShape = resolveRowEncodingValues(this, layer, dataset, "shape");
243
- const encodedOpacity = resolveRowEncodingValues(this, layer, dataset, "opacity");
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 = this.markConfigs[id] ?? {};
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(this, {
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
- ["x", "y", "x2", "y2", "strokeDash", "strokeWidth", "opacity"]
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 = [];
@@ -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",