ggaction 0.0.7 → 0.0.8

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 (61) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/actions/categoryOrder/index.js +111 -0
  5. package/src/actions/data/index.js +6 -0
  6. package/src/actions/data/timeUnit.js +48 -0
  7. package/src/actions/encodings/angle.js +77 -0
  8. package/src/actions/encodings/color/index.js +17 -13
  9. package/src/actions/encodings/color/layout.js +2 -0
  10. package/src/actions/encodings/color/policy.js +3 -0
  11. package/src/actions/encodings/index.js +2 -0
  12. package/src/actions/encodings/position/policies/area.js +40 -2
  13. package/src/actions/encodings/position/policies/bar.js +6 -0
  14. package/src/actions/encodings/position/policies/index.js +2 -0
  15. package/src/actions/encodings/position/policies/tick.js +19 -0
  16. package/src/actions/encodings/remove.js +1 -1
  17. package/src/actions/guides/legends/categorical/actions.js +23 -6
  18. package/src/actions/guides/legends/categorical/index.js +8 -0
  19. package/src/actions/guides/legends/continuous/common.js +17 -5
  20. package/src/actions/guides/legends/continuous/gradient.js +12 -8
  21. package/src/actions/guides/legends/continuous/opacity.js +70 -17
  22. package/src/actions/guides/legends/edit.js +13 -8
  23. package/src/actions/guides/legends/lane.js +468 -0
  24. package/src/actions/guides/legends/remove.js +3 -7
  25. package/src/actions/index.js +2 -0
  26. package/src/actions/marks/area/actions.js +3 -1
  27. package/src/actions/marks/area/materialize.js +12 -3
  28. package/src/actions/marks/index.js +2 -0
  29. package/src/actions/marks/point/materialize.js +17 -5
  30. package/src/actions/marks/tick/actions.js +225 -0
  31. package/src/actions/marks/tick/index.js +1 -0
  32. package/src/actions/primitives/semanticValidation/layer.js +2 -0
  33. package/src/actions/scales/consumers/index.js +8 -0
  34. package/src/actions/scales/consumers/seriesLayout.js +22 -2
  35. package/src/actions/scales/materialize.js +2 -0
  36. package/src/actions/selection/actions.js +5 -2
  37. package/src/core/vocabulary.js +7 -3
  38. package/src/grammar/areaSeries.js +112 -2
  39. package/src/grammar/categoryOrder.js +138 -0
  40. package/src/grammar/direction.js +46 -0
  41. package/src/grammar/facets/index.js +1 -1
  42. package/src/grammar/pointShapes.js +34 -6
  43. package/src/grammar/positionCompatibility.js +4 -0
  44. package/src/grammar/schemas/semanticPath.js +2 -1
  45. package/src/grammar/seriesLayout.js +10 -2
  46. package/src/grammar/timeUnit.js +108 -0
  47. package/src/grammar/transforms.js +6 -0
  48. package/src/grammar/window.js +73 -7
  49. package/src/layout/legendLane.js +338 -0
  50. package/src/materialization/marks/capabilities.js +9 -0
  51. package/src/materialization/marks/index.js +2 -1
  52. package/src/materialization/marks/policies.js +13 -0
  53. package/src/materialization/scales/policies/series.js +9 -0
  54. package/src/materialization/scales/resolve.js +25 -3
  55. package/src/materialization/selection/items/index.js +1 -0
  56. package/src/materialization/selection/items/path.js +4 -1
  57. package/src/materialization/selection/items/tick.js +42 -0
  58. package/src/materialization/selection/policies/index.js +2 -0
  59. package/src/materialization/selection/policies/tick.js +10 -0
  60. package/types/index.d.ts +10 -0
  61. package/types/program.d.ts +96 -3
@@ -0,0 +1,468 @@
1
+ import { action } from "../../../core/action.js";
2
+ import { noOptions } from "../../../core/validation.js";
3
+ import {
4
+ resolveConcreteGraphicBounds,
5
+ unionConcreteGraphicBounds
6
+ } from "../../../grammar/schemas/graphicBounds.js";
7
+ import { resolveGraphicBounds } from "../../../layout/canvas.js";
8
+ import {
9
+ resolveHorizontalLegendLane,
10
+ resolveSideLegendLane
11
+ } from "../../../layout/legendLane.js";
12
+ import { findCanvasGraphic } from
13
+ "../../../materialization/graphicHierarchy.js";
14
+ import { legendResourcePolicy } from
15
+ "../../../materialization/guides/resources.js";
16
+
17
+ const FAMILY_ORDER = Object.freeze({
18
+ series: 0,
19
+ color: 0,
20
+ gradient: 0,
21
+ interval: 0,
22
+ size: 1,
23
+ opacity: 2,
24
+ strokeWidth: 3
25
+ });
26
+
27
+ function categoricalFor(program, target) {
28
+ return ["series", "color"]
29
+ .map(kind => program.guideConfigs.legend?.[kind])
30
+ .find(config => config?.target === target);
31
+ }
32
+
33
+ function legendPosition(program, kind, config) {
34
+ if (kind === "size") {
35
+ return categoricalFor(program, config.target)?.position ?? "right";
36
+ }
37
+ if (kind === "strokeWidth" || kind === "interval") return "right";
38
+ return config.position;
39
+ }
40
+
41
+ function requestedOffset(kind, config) {
42
+ return ["size", "strokeWidth"].includes(kind)
43
+ ? 30
44
+ : config.offset;
45
+ }
46
+
47
+ function existingIds(program, kind) {
48
+ return legendResourcePolicy(kind).graphicIds.filter(
49
+ id => program.graphicSpec.objects[id] !== undefined
50
+ );
51
+ }
52
+
53
+ function componentIds(program, kind) {
54
+ const ids = existingIds(program, kind);
55
+ const titleId = ids.find(id => id.endsWith("Title"));
56
+ const labelId = ids.find(id => id.endsWith("Labels"));
57
+ const backgroundId = ids.find(id => id.endsWith("Background"));
58
+ const symbolIds = ids.filter(id =>
59
+ id !== titleId && id !== labelId && id !== backgroundId
60
+ );
61
+ return { titleId, labelId, backgroundId, symbolIds };
62
+ }
63
+
64
+ function textAnchor(program, id) {
65
+ const graphic = program.graphicSpec.objects[id];
66
+ const properties = graphic.items?.[0]?.properties ?? graphic.properties;
67
+ return properties.x;
68
+ }
69
+
70
+ function blockDescriptor(program, kind, config) {
71
+ const components = componentIds(program, kind);
72
+ if (components.labelId === undefined || components.symbolIds.length === 0) {
73
+ throw new Error(`Legend lane requires complete ${kind} graphics.`);
74
+ }
75
+ const foregroundIds = [
76
+ ...components.symbolIds,
77
+ components.labelId,
78
+ ...(components.titleId === undefined ? [] : [components.titleId])
79
+ ];
80
+ const bounds = unionConcreteGraphicBounds(program.graphicSpec, foregroundIds);
81
+ const symbolAnchorIds = kind === "gradient"
82
+ ? ["colorGradientStrips"]
83
+ : components.symbolIds;
84
+ const symbolAnchor = unionConcreteGraphicBounds(
85
+ program.graphicSpec,
86
+ symbolAnchorIds
87
+ );
88
+ const symbolBounds = unionConcreteGraphicBounds(
89
+ program.graphicSpec,
90
+ components.symbolIds
91
+ );
92
+ const labels = resolveConcreteGraphicBounds(
93
+ program.graphicSpec,
94
+ components.labelId
95
+ );
96
+ const title = components.titleId === undefined
97
+ ? undefined
98
+ : (() => {
99
+ const graphic = program.graphicSpec.objects[components.titleId];
100
+ const titleBounds = resolveConcreteGraphicBounds(
101
+ program.graphicSpec,
102
+ components.titleId
103
+ );
104
+ return {
105
+ x: graphic.properties.x,
106
+ y: graphic.properties.y,
107
+ fontSize: graphic.properties.fontSize,
108
+ width: titleBounds.right - titleBounds.left,
109
+ bounds: titleBounds
110
+ };
111
+ })();
112
+ if ([bounds, symbolAnchor, symbolBounds, labels].includes(undefined)) {
113
+ throw new Error(`Legend lane could not measure ${kind} graphics.`);
114
+ }
115
+ return {
116
+ id: kind,
117
+ kind,
118
+ target: config.target,
119
+ offset: requestedOffset(kind, config),
120
+ bounds,
121
+ title,
122
+ symbol: {
123
+ centerX: (symbolAnchor.left + symbolAnchor.right) / 2,
124
+ left: symbolBounds.left,
125
+ right: symbolBounds.right,
126
+ bounds: symbolAnchor
127
+ },
128
+ labels: {
129
+ x: textAnchor(program, components.labelId),
130
+ width: labels.right - labels.left
131
+ },
132
+ occupiedBounds: components.backgroundId === undefined
133
+ ? bounds
134
+ : resolveConcreteGraphicBounds(
135
+ program.graphicSpec,
136
+ components.backgroundId
137
+ ),
138
+ ...components,
139
+ foregroundIds
140
+ };
141
+ }
142
+
143
+ function borderFor(kind, config) {
144
+ return ["series", "color", "gradient", "opacity"].includes(kind)
145
+ ? config.border
146
+ : false;
147
+ }
148
+
149
+ function groupBlocks(blocks, configs) {
150
+ const groups = [];
151
+ for (let index = 0; index < blocks.length; index += 1) {
152
+ const block = blocks[index];
153
+ const config = configs[block.kind];
154
+ const next = blocks[index + 1];
155
+ if (
156
+ ["series", "color"].includes(block.kind) &&
157
+ next?.kind === "size" && next.target === block.target
158
+ ) {
159
+ groups.push({
160
+ id: `${block.kind}+size`,
161
+ blocks: [block, next],
162
+ border: borderFor(block.kind, config),
163
+ backgroundId: block.backgroundId
164
+ });
165
+ index += 1;
166
+ continue;
167
+ }
168
+ groups.push({
169
+ id: block.kind,
170
+ blocks: [block],
171
+ border: borderFor(block.kind, config),
172
+ backgroundId: block.backgroundId
173
+ });
174
+ }
175
+ return groups;
176
+ }
177
+
178
+ function edgeKinds(program, edge) {
179
+ const configs = program.guideConfigs.legend ?? {};
180
+ const layerOrder = new Map(program.semanticSpec.layers.map(
181
+ (layer, index) => [layer.id, index]
182
+ ));
183
+ return Object.entries(configs)
184
+ .filter(([kind, config]) => legendPosition(program, kind, config) === edge)
185
+ .sort(([kindA, configA], [kindB, configB]) =>
186
+ (layerOrder.get(configA.target) - layerOrder.get(configB.target)) ||
187
+ (FAMILY_ORDER[kindA] - FAMILY_ORDER[kindB])
188
+ );
189
+ }
190
+
191
+ export function hasMultiSideLegendLane(program) {
192
+ return ["right", "left"].some(side => edgeKinds(program, side).length > 1);
193
+ }
194
+
195
+ export function hasMultiHorizontalLegendLane(program) {
196
+ return ["top", "bottom"].some(edge => edgeKinds(program, edge).length > 1);
197
+ }
198
+
199
+ export function hasMultiLegendLane(program) {
200
+ return hasMultiSideLegendLane(program) ||
201
+ hasMultiHorizontalLegendLane(program);
202
+ }
203
+
204
+ function translateCommands(commands, dx, dy) {
205
+ return commands.map(command => {
206
+ if (command.op === "Z") return { ...command };
207
+ const next = {
208
+ ...command,
209
+ x: command.x + dx,
210
+ y: command.y + dy
211
+ };
212
+ if (command.op === "C") {
213
+ next.x1 = command.x1 + dx;
214
+ next.y1 = command.y1 + dy;
215
+ next.x2 = command.x2 + dx;
216
+ next.y2 = command.y2 + dy;
217
+ }
218
+ return next;
219
+ });
220
+ }
221
+
222
+ function translatedProperties(type, properties, dx, dy) {
223
+ const next = { ...properties };
224
+ if (["circle", "rect", "text"].includes(type)) {
225
+ next.x += dx;
226
+ next.y += dy;
227
+ } else if (type === "line") {
228
+ next.x1 += dx;
229
+ next.x2 += dx;
230
+ next.y1 += dy;
231
+ next.y2 += dy;
232
+ } else if (type === "path") {
233
+ next.commands = translateCommands(next.commands, dx, dy);
234
+ }
235
+ return next;
236
+ }
237
+
238
+ function translateGraphic(program, id, dx, dy) {
239
+ const graphic = program.graphicSpec.objects[id];
240
+ if (dx === 0 && dy === 0) return program;
241
+ if (graphic.items !== undefined) {
242
+ return program.editGraphics({
243
+ target: id,
244
+ property: "items",
245
+ value: graphic.items.map(item => ({
246
+ type: item.type ?? graphic.type,
247
+ properties: translatedProperties(
248
+ item.type ?? graphic.type,
249
+ item.properties,
250
+ dx,
251
+ dy
252
+ )
253
+ }))
254
+ });
255
+ }
256
+ let next = program;
257
+ const properties = translatedProperties(
258
+ graphic.type,
259
+ graphic.properties,
260
+ dx,
261
+ dy
262
+ );
263
+ for (const [property, value] of Object.entries(properties)) {
264
+ if (value !== graphic.properties[property]) {
265
+ next = next.editGraphics({ target: id, property, value });
266
+ }
267
+ }
268
+ return next;
269
+ }
270
+
271
+ function yAxisBounds(program) {
272
+ const ids = ["yAxisLine", "yAxisTicks", "yAxisLabels", "yAxisTitle"]
273
+ .filter(id => program.graphicSpec.objects[id] !== undefined);
274
+ return ids.length === 0
275
+ ? undefined
276
+ : unionConcreteGraphicBounds(program.graphicSpec, ids);
277
+ }
278
+
279
+ function horizontalCollisionBounds(program, edge) {
280
+ const ids = edge === "top"
281
+ ? ["chartTitle", "chartSubtitle"]
282
+ : ["xAxisLine", "xAxisTicks", "xAxisLabels", "xAxisTitle"];
283
+ return ids
284
+ .filter(id => program.graphicSpec.objects[id] !== undefined)
285
+ .map(id => resolveConcreteGraphicBounds(program.graphicSpec, id))
286
+ .filter(bounds => bounds !== undefined);
287
+ }
288
+
289
+ function horizontalGroups(program, groups) {
290
+ return groups.map(group => {
291
+ const representative = group.blocks[0];
292
+ const config = program.guideConfigs.legend?.[representative.kind];
293
+ const titleId = representative.titleId;
294
+ const contentIds = group.blocks.flatMap(block => block.foregroundIds)
295
+ .filter(id => id !== titleId);
296
+ const content = unionConcreteGraphicBounds(program.graphicSpec, contentIds);
297
+ const foreground = unionConcreteGraphicBounds(
298
+ program.graphicSpec,
299
+ group.blocks.flatMap(block => block.foregroundIds)
300
+ );
301
+ if (content === undefined || foreground === undefined) {
302
+ throw new Error(`Legend lane could not measure ${group.id} content.`);
303
+ }
304
+ const border = group.border;
305
+ const inset = border === false
306
+ ? 0
307
+ : border.padding + border.lineWidth / 2;
308
+ return {
309
+ id: group.id,
310
+ titleId,
311
+ contentIds,
312
+ title: representative.title,
313
+ inline: config?.titlePosition === "left",
314
+ element: representative.symbol.bounds,
315
+ content,
316
+ horizontal: { left: foreground.left, right: foreground.right,
317
+ top: foreground.top, bottom: foreground.bottom },
318
+ inset,
319
+ padding: border === false ? 0 : border.padding,
320
+ backgroundId: group.backgroundId
321
+ };
322
+ });
323
+ }
324
+
325
+ export const rematerializeSideLegendLane = action(
326
+ {
327
+ op: "rematerializeSideLegendLane",
328
+ description: "Align and stack every right or left legend block."
329
+ },
330
+ function (args = {}) {
331
+ noOptions(args, "rematerializeSideLegendLane");
332
+ const plot = resolveGraphicBounds(this);
333
+ const canvas = findCanvasGraphic(this)?.properties;
334
+ if (plot === undefined || canvas === undefined) {
335
+ throw new Error("Legend lane requires resolved Canvas and plot bounds.");
336
+ }
337
+ const configs = this.guideConfigs.legend ?? {};
338
+ const plans = ["right", "left"].flatMap(side => {
339
+ const entries = edgeKinds(this, side);
340
+ if (entries.length < 2) return [];
341
+ const blocks = entries.map(([kind, config]) =>
342
+ blockDescriptor(this, kind, config)
343
+ );
344
+ const plan = resolveSideLegendLane({
345
+ side,
346
+ plot,
347
+ canvas,
348
+ groups: groupBlocks(blocks, configs),
349
+ axisBounds: yAxisBounds(this)
350
+ });
351
+ return [{ plan, blocks }];
352
+ });
353
+ let next = this;
354
+ for (const { plan, blocks } of plans) {
355
+ const byId = new Map(blocks.map(block => [block.id, block]));
356
+ for (const placement of plan.placements) {
357
+ const block = byId.get(placement.id);
358
+ if (block.titleId !== undefined) {
359
+ next = translateGraphic(
360
+ next,
361
+ block.titleId,
362
+ placement.titleDx,
363
+ placement.dy
364
+ ).editGraphics({
365
+ target: block.titleId,
366
+ property: "textAlign",
367
+ value: "left"
368
+ });
369
+ }
370
+ for (const id of block.symbolIds) {
371
+ next = translateGraphic(
372
+ next,
373
+ id,
374
+ placement.symbolDx,
375
+ placement.dy
376
+ );
377
+ }
378
+ next = translateGraphic(
379
+ next,
380
+ block.labelId,
381
+ placement.labelDx,
382
+ placement.dy
383
+ ).editGraphics({
384
+ target: block.labelId,
385
+ property: "textAlign",
386
+ value: "left"
387
+ });
388
+ }
389
+ for (const background of plan.backgrounds) {
390
+ for (const property of ["x", "y", "width", "height"]) {
391
+ next = next.editGraphics({
392
+ target: background.id,
393
+ property,
394
+ value: background[property]
395
+ });
396
+ }
397
+ }
398
+ }
399
+ return next;
400
+ }
401
+ );
402
+
403
+ export const rematerializeHorizontalLegendLane = action(
404
+ {
405
+ op: "rematerializeHorizontalLegendLane",
406
+ description: "Pack and align every top or bottom legend block."
407
+ },
408
+ function (args = {}) {
409
+ noOptions(args, "rematerializeHorizontalLegendLane");
410
+ const plot = resolveGraphicBounds(this);
411
+ const canvas = findCanvasGraphic(this)?.properties;
412
+ if (plot === undefined || canvas === undefined) {
413
+ throw new Error("Legend lane requires resolved Canvas and plot bounds.");
414
+ }
415
+ const configs = this.guideConfigs.legend ?? {};
416
+ const plans = ["top", "bottom"].flatMap(edge => {
417
+ const entries = edgeKinds(this, edge);
418
+ if (entries.length < 2) return [];
419
+ const blocks = entries.map(([kind, config]) =>
420
+ blockDescriptor(this, kind, config)
421
+ );
422
+ const groups = groupBlocks(blocks, configs);
423
+ if (groups.length < 2) return [];
424
+ const horizontal = horizontalGroups(this, groups);
425
+ const plan = resolveHorizontalLegendLane({
426
+ edge,
427
+ plot,
428
+ canvas,
429
+ groups: horizontal,
430
+ collisionBounds: horizontalCollisionBounds(this, edge)
431
+ });
432
+ return [{ plan, groups: horizontal }];
433
+ });
434
+ let next = this;
435
+ for (const { plan, groups } of plans) {
436
+ const byId = new Map(groups.map(group => [group.id, group]));
437
+ for (const placement of plan.placements) {
438
+ const group = byId.get(placement.id);
439
+ if (group.titleId !== undefined) {
440
+ next = translateGraphic(
441
+ next,
442
+ group.titleId,
443
+ placement.dx,
444
+ placement.titleDy
445
+ );
446
+ }
447
+ for (const id of group.contentIds) {
448
+ next = translateGraphic(
449
+ next,
450
+ id,
451
+ placement.dx,
452
+ placement.contentDy
453
+ );
454
+ }
455
+ if (placement.background !== undefined) {
456
+ for (const property of ["x", "y", "width", "height"]) {
457
+ next = next.editGraphics({
458
+ target: placement.background.id,
459
+ property,
460
+ value: placement.background[property]
461
+ });
462
+ }
463
+ }
464
+ }
465
+ }
466
+ return next;
467
+ }
468
+ );
@@ -113,13 +113,9 @@ export const removeLegend = action(
113
113
  let next = removeLegendKinds(this, kinds);
114
114
  const retainedKinds = Object.keys(next.guideConfigs.legend ?? {});
115
115
  if (args.channels === undefined) {
116
- const removedSemanticKinds = new Set(
117
- kinds.map(kind => legendResourcePolicy(kind).semanticKind)
118
- );
119
- const hasSharedSemanticKind = retainedKinds.some(kind =>
120
- removedSemanticKinds.has(legendResourcePolicy(kind).semanticKind)
121
- );
122
- return hasSharedSemanticKind ? next.rematerializeLegend() : next;
116
+ return retainedKinds.length === 0
117
+ ? next
118
+ : next.rematerializeLegend();
123
119
  }
124
120
  const remainingTargetKinds = targetKinds.filter(kind => !kinds.includes(kind));
125
121
  const removedCategorical = kinds.some(kind => ["series", "color"].includes(kind));
@@ -1,5 +1,6 @@
1
1
  import { registerCanvasActions } from "./canvas/index.js";
2
2
  import { registerChartActions } from "./charts/index.js";
3
+ import { registerCategoryOrderActions } from "./categoryOrder/index.js";
3
4
  import { registerCoordinateActions } from "./coordinates/index.js";
4
5
  import { registerCompositionActions } from "./composition/index.js";
5
6
  import { registerDataActions } from "./data/index.js";
@@ -37,5 +38,6 @@ export function registerActions(ProgramClass) {
37
38
  registerGradientPlotActions(ProgramClass);
38
39
  registerViolinPlotActions(ProgramClass);
39
40
  registerChartActions(ProgramClass);
41
+ registerCategoryOrderActions(ProgramClass);
40
42
  registerSelectionActions(ProgramClass);
41
43
  }
@@ -159,10 +159,12 @@ const rematerializeAreaMark = action(
159
159
  const yScaleId = layer.encoding?.y?.scale;
160
160
  const verticalRange = layer.encoding?.y2?.scale === yScaleId;
161
161
  const horizontalRange = layer.encoding?.x2?.scale === xScaleId;
162
+ const centered = densityTransform === undefined &&
163
+ layer.encoding?.y?.stack === "center";
162
164
  if (
163
165
  xScaleId === undefined ||
164
166
  yScaleId === undefined ||
165
- (densityTransform === undefined && verticalRange === horizontalRange)
167
+ (densityTransform === undefined && !centered && verticalRange === horizontalRange)
166
168
  ) {
167
169
  throw new Error(
168
170
  densityTransform === undefined
@@ -1,4 +1,5 @@
1
1
  import {
2
+ deriveCenteredAreaSeries,
2
3
  deriveAreaSeries,
3
4
  deriveDensityAreaSeries,
4
5
  layoutDensityAreaSeries
@@ -81,13 +82,17 @@ function resolveAreaPaths({
81
82
  xScale
82
83
  );
83
84
  const lowerValues = densityTransform === undefined
84
- ? series.values.map(value => value.y)
85
+ ? derived.mode === "y-center"
86
+ ? series.values.map(value => value.lower)
87
+ : series.values.map(value => value.y)
85
88
  : derived.mode === "y-density"
86
89
  ? series.values.map(value => value.lower)
87
90
  : series.values.map(value => value.y);
88
91
  const lower = mapContinuousScaleValues(lowerValues, yScale);
89
92
  const upperValues = densityTransform === undefined
90
- ? series.values.map(value => value.y2)
93
+ ? derived.mode === "y-center"
94
+ ? series.values.map(value => value.upper)
95
+ : series.values.map(value => value.y2)
91
96
  : series.values.map(value => value.upper);
92
97
  const upper = mapContinuousScaleValues(upperValues, yScale);
93
98
  if (densityTransform !== undefined && layout === "overlay") {
@@ -122,8 +127,12 @@ export function resolveAreaMaterialization({
122
127
  }) {
123
128
  const xScale = resolvedScales[layer.encoding.x.scale];
124
129
  const yScale = resolvedScales[layer.encoding.y.scale];
130
+ const centered = densityTransform === undefined &&
131
+ layer.encoding?.y?.stack === "center";
125
132
  const rawDerived = densityTransform === undefined
126
- ? deriveAreaSeries(rows, layer)
133
+ ? centered
134
+ ? deriveCenteredAreaSeries(rows, layer)
135
+ : deriveAreaSeries(rows, layer)
127
136
  : deriveDensityAreaSeries(rows, layer, densityTransform);
128
137
  const colorEncoding = layer.encoding?.color;
129
138
  const layout = colorEncoding?.layout ?? "overlay";
@@ -9,6 +9,7 @@ import {
9
9
  import { registerRectMarkActions } from "./rect/index.js";
10
10
  import { registerRuleMarkActions } from "./rule/index.js";
11
11
  import { registerTextMarkActions } from "./text/index.js";
12
+ import { registerTickMarkActions } from "./tick/index.js";
12
13
  import { removeMark } from "./remove.js";
13
14
 
14
15
  export function registerMarkActions(ProgramClass) {
@@ -21,5 +22,6 @@ export function registerMarkActions(ProgramClass) {
21
22
  registerBarMarkActions(ProgramClass);
22
23
  registerRuleMarkActions(ProgramClass);
23
24
  registerTextMarkActions(ProgramClass);
25
+ registerTickMarkActions(ProgramClass);
24
26
  ProgramClass.prototype.removeMark = removeMark;
25
27
  }
@@ -12,6 +12,7 @@ import {
12
12
  resolvePointJitter
13
13
  } from "../../../grammar/jitter.js";
14
14
  import { polarToCartesian, resolvePolarFrame } from "../../../grammar/polar.js";
15
+ import { resolveDirectionValues } from "../../../grammar/direction.js";
15
16
  import { resolveGraphicBounds } from "../../../layout/canvas.js";
16
17
  import { validateMarkOptions } from "../shared.js";
17
18
  import {
@@ -60,8 +61,10 @@ function compactProperties(properties) {
60
61
  );
61
62
  }
62
63
 
63
- function incompleteShapeGraphic(shape, properties) {
64
- const type = getPointGraphicType(shape);
64
+ function incompleteShapeGraphic(shape, properties, angle) {
65
+ const type = angle !== undefined && shape === "square"
66
+ ? "path"
67
+ : getPointGraphicType(shape);
65
68
  const incomplete = type === "path"
66
69
  ? { fill: properties.fill, opacity: properties.opacity }
67
70
  : properties;
@@ -99,6 +102,7 @@ function resolveJitterEntries({
99
102
  y,
100
103
  area,
101
104
  shapes,
105
+ angles,
102
106
  config,
103
107
  graphic,
104
108
  existingChildren
@@ -131,7 +135,8 @@ function resolveJitterEntries({
131
135
  area: resolvedArea,
132
136
  strokeWidth: typeof config.stroke === "string"
133
137
  ? config.strokeWidth ?? 1
134
- : 0
138
+ : 0,
139
+ angle: angles?.[index]
135
140
  });
136
141
  return [{
137
142
  index,
@@ -150,6 +155,7 @@ function applyPointJitter(program, {
150
155
  y,
151
156
  area,
152
157
  shapes,
158
+ angles,
153
159
  config,
154
160
  graphic,
155
161
  existingChildren
@@ -172,6 +178,7 @@ function applyPointJitter(program, {
172
178
  y,
173
179
  area,
174
180
  shapes,
181
+ angles,
175
182
  config,
176
183
  graphic,
177
184
  existingChildren
@@ -234,6 +241,7 @@ export const rematerializePointMark = action(
234
241
  const area = resolveRowEncodingValues(this, layer, dataset, "size");
235
242
  const encodedShape = resolveRowEncodingValues(this, layer, dataset, "shape");
236
243
  const encodedOpacity = resolveRowEncodingValues(this, layer, dataset, "opacity");
244
+ const angles = resolveDirectionValues(dataset.values, layer.encoding?.angle);
237
245
  const config = this.markConfigs[id] ?? {};
238
246
  const fill = mappedFill ?? config.fill ?? DEFAULT_POINT_FILL;
239
247
  const shapes = encodedShape ?? dataset.values.map(() => config.shape ?? "circle");
@@ -241,6 +249,7 @@ export const rematerializePointMark = action(
241
249
  const constantShape = validatePointShape(config.shape ?? "circle");
242
250
  const requiresMixedCollection =
243
251
  encodedShape !== undefined ||
252
+ angles !== undefined ||
244
253
  graphic.type === "collection" ||
245
254
  getPointGraphicType(constantShape) !== graphic.type;
246
255
  const jittered = applyPointJitter(this, {
@@ -251,6 +260,7 @@ export const rematerializePointMark = action(
251
260
  y: positions.y,
252
261
  area,
253
262
  shapes,
263
+ angles,
254
264
  config,
255
265
  graphic,
256
266
  existingChildren
@@ -273,6 +283,7 @@ export const rematerializePointMark = action(
273
283
  );
274
284
  const centerX = x?.[index] ?? existing.x;
275
285
  const centerY = y?.[index] ?? existing.y;
286
+ const angle = angles?.[index];
276
287
  if (
277
288
  resolvedArea === undefined ||
278
289
  !Number.isFinite(centerX) ||
@@ -283,7 +294,7 @@ export const rematerializePointMark = action(
283
294
  y: centerY,
284
295
  fill: color,
285
296
  opacity
286
- });
297
+ }, angle);
287
298
  }
288
299
  return createPointShapeGraphic({
289
300
  shape,
@@ -295,7 +306,8 @@ export const rematerializePointMark = action(
295
306
  ...(config.strokeWidth === undefined
296
307
  ? {}
297
308
  : { strokeWidth: config.strokeWidth }),
298
- opacity
309
+ opacity,
310
+ angle
299
311
  });
300
312
  });
301
313
  return jittered.program.editGraphics({