ggaction 0.0.6 → 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 (96) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +77 -14
  3. package/package.json +19 -2
  4. package/src/BasicChartProgram.js +10 -0
  5. package/src/actions/basic.js +21 -0
  6. package/src/actions/canvas/actions.js +42 -0
  7. package/src/actions/canvas/index.js +5 -1
  8. package/src/actions/categoryOrder/index.js +111 -0
  9. package/src/actions/charts/basic.js +13 -0
  10. package/src/actions/data/basic.js +11 -0
  11. package/src/actions/data/basicBin2d.js +43 -0
  12. package/src/actions/data/bin2d.js +1 -26
  13. package/src/actions/data/bin2dMaterialize.js +29 -0
  14. package/src/actions/data/index.js +6 -0
  15. package/src/actions/data/timeUnit.js +48 -0
  16. package/src/actions/encodings/angle.js +77 -0
  17. package/src/actions/encodings/appearance.js +6 -2
  18. package/src/actions/encodings/basic.js +21 -0
  19. package/src/actions/encodings/color/index.js +17 -13
  20. package/src/actions/encodings/color/layout.js +2 -0
  21. package/src/actions/encodings/color/policy.js +3 -0
  22. package/src/actions/encodings/index.js +2 -0
  23. package/src/actions/encodings/position/index.js +5 -1
  24. package/src/actions/encodings/position/policies/area.js +40 -2
  25. package/src/actions/encodings/position/policies/bar.js +6 -0
  26. package/src/actions/encodings/position/policies/index.js +2 -0
  27. package/src/actions/encodings/position/policies/tick.js +19 -0
  28. package/src/actions/encodings/ranged.js +6 -2
  29. package/src/actions/encodings/remove.js +1 -1
  30. package/src/actions/guides/axes/index.js +10 -2
  31. package/src/actions/guides/basic.js +19 -0
  32. package/src/actions/guides/grids/grid.js +7 -3
  33. package/src/actions/guides/grids/index.js +1 -1
  34. package/src/actions/guides/legends/categorical/actions.js +23 -6
  35. package/src/actions/guides/legends/categorical/index.js +14 -2
  36. package/src/actions/guides/legends/continuous/common.js +17 -5
  37. package/src/actions/guides/legends/continuous/gradient.js +12 -8
  38. package/src/actions/guides/legends/continuous/index.js +6 -2
  39. package/src/actions/guides/legends/continuous/opacity.js +70 -17
  40. package/src/actions/guides/legends/edit.js +13 -8
  41. package/src/actions/guides/legends/lane.js +468 -0
  42. package/src/actions/guides/legends/remove.js +3 -7
  43. package/src/actions/index.js +2 -0
  44. package/src/actions/marks/area/actions.js +3 -1
  45. package/src/actions/marks/area/materialize.js +12 -3
  46. package/src/actions/marks/bar/index.js +5 -1
  47. package/src/actions/marks/basic.js +11 -0
  48. package/src/actions/marks/index.js +2 -0
  49. package/src/actions/marks/line/actions.js +5 -1
  50. package/src/actions/marks/line/index.js +4 -1
  51. package/src/actions/marks/point/index.js +5 -1
  52. package/src/actions/marks/point/materialize.js +17 -5
  53. package/src/actions/marks/rect/actions.js +5 -1
  54. package/src/actions/marks/rect/index.js +4 -1
  55. package/src/actions/marks/tick/actions.js +225 -0
  56. package/src/actions/marks/tick/index.js +1 -0
  57. package/src/actions/primitives/semanticValidation/layer.js +2 -0
  58. package/src/actions/scales/consumers/index.js +8 -0
  59. package/src/actions/scales/consumers/seriesLayout.js +22 -2
  60. package/src/actions/scales/index.js +5 -1
  61. package/src/actions/scales/materialize.js +2 -0
  62. package/src/actions/selection/actions.js +5 -2
  63. package/src/basic.js +2 -0
  64. package/src/core/vocabulary.js +7 -3
  65. package/src/grammar/areaSeries.js +112 -2
  66. package/src/grammar/categoryOrder.js +138 -0
  67. package/src/grammar/direction.js +46 -0
  68. package/src/grammar/facets/index.js +1 -1
  69. package/src/grammar/pointShapes.js +34 -6
  70. package/src/grammar/positionCompatibility.js +4 -0
  71. package/src/grammar/schemas/semanticPath.js +2 -1
  72. package/src/grammar/seriesLayout.js +10 -2
  73. package/src/grammar/timeUnit.js +108 -0
  74. package/src/grammar/transforms.js +6 -0
  75. package/src/grammar/window.js +73 -7
  76. package/src/layout/legendLane.js +338 -0
  77. package/src/materialization/marks/capabilities.js +9 -0
  78. package/src/materialization/marks/index.js +2 -1
  79. package/src/materialization/marks/policies.js +13 -0
  80. package/src/materialization/scales/policies/series.js +9 -0
  81. package/src/materialization/scales/resolve.js +25 -3
  82. package/src/materialization/selection/items/index.js +1 -0
  83. package/src/materialization/selection/items/path.js +4 -1
  84. package/src/materialization/selection/items/tick.js +42 -0
  85. package/src/materialization/selection/policies/index.js +2 -0
  86. package/src/materialization/selection/policies/tick.js +10 -0
  87. package/src/renderers/canvas/index.js +96 -60
  88. package/src/renderers/canvas/text.js +3 -14
  89. package/src/renderers/pdf.js +112 -0
  90. package/src/renderers/svg.js +559 -0
  91. package/src/renderers/text.js +11 -0
  92. package/types/basic.d.ts +107 -0
  93. package/types/index.d.ts +10 -0
  94. package/types/pdf.d.ts +26 -0
  95. package/types/program.d.ts +96 -3
  96. package/types/svg.d.ts +11 -0
@@ -0,0 +1,48 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateUserId } from "../../core/identifiers.js";
3
+ import { validateKeys } from "../../core/validation.js";
4
+ import {
5
+ deriveTimeUnitRows,
6
+ normalizeTimeUnitTransform
7
+ } from "../../grammar/timeUnit.js";
8
+ import { MATERIALIZE_OPTIONS, requireDerivedDataset } from "./shared.js";
9
+
10
+ const OPTIONS = Object.freeze(["id", "source", "field", "unit", "as"]);
11
+
12
+ export const materializeTimeUnitData = action(
13
+ {
14
+ op: "materializeTimeUnitData",
15
+ description: "Materialize one immutable UTC time-unit dataset."
16
+ },
17
+ function (args = {}) {
18
+ validateKeys(args, MATERIALIZE_OPTIONS, "materializeTimeUnitData");
19
+ const { id, source, transform } = requireDerivedDataset(
20
+ this,
21
+ args.id,
22
+ "timeUnit"
23
+ );
24
+ return this.editSemantic({
25
+ property: `dataset[${id}].values`,
26
+ value: deriveTimeUnitRows(source.values, transform)
27
+ });
28
+ }
29
+ );
30
+
31
+ export const createTimeUnitData = action(
32
+ {
33
+ op: "createTimeUnitData",
34
+ description: "Create immutable UTC calendar bucket values."
35
+ },
36
+ function (args = {}) {
37
+ validateKeys(args, OPTIONS, "createTimeUnitData");
38
+ const id = validateUserId(args.id, "Time-unit dataset id");
39
+ const source = validateUserId(
40
+ args.source ?? this.context.currentData,
41
+ "Source dataset id"
42
+ );
43
+ const transform = normalizeTimeUnitTransform(args);
44
+ return this
45
+ .createDerivedData({ id, source, transform: [transform] })
46
+ .materializeTimeUnitData({ id });
47
+ }
48
+ );
@@ -0,0 +1,77 @@
1
+ import { action } from "../../core/action.js";
2
+ import { readQuantitativeField } from "../../grammar/scales/index.js";
3
+ import { resolveTarget, validateOptions } from "./shared.js";
4
+
5
+ const OPTIONS = Object.freeze(["target", "value", "field", "fieldType"]);
6
+
7
+ function rematerialize(program, layer, target) {
8
+ return layer.mark.type === "tick"
9
+ ? program.rematerializeTickMark({ id: target })
10
+ : program.rematerializePointMark({ id: target });
11
+ }
12
+
13
+ export const encodeAngle = action(
14
+ {
15
+ op: "encodeAngle",
16
+ description: "Assign direct clockwise degree rotation to point or Tick glyphs."
17
+ },
18
+ function (args = {}) {
19
+ validateOptions(args, OPTIONS, "encodeAngle");
20
+ const hasValue = Object.hasOwn(args, "value");
21
+ const hasField = Object.hasOwn(args, "field");
22
+ if (hasValue === hasField) {
23
+ throw new Error("encodeAngle requires exactly one of value or field.");
24
+ }
25
+ if (hasValue && args.fieldType !== undefined) {
26
+ throw new Error("Constant angle does not accept fieldType.");
27
+ }
28
+ const { id: target, dataset, layer } = resolveTarget(
29
+ this,
30
+ args.target,
31
+ ["point", "tick"],
32
+ "point or Tick mark"
33
+ );
34
+ if (hasValue && !Number.isFinite(args.value)) {
35
+ throw new TypeError("encodeAngle value must be finite degrees.");
36
+ }
37
+ if (hasField) {
38
+ const fieldType = args.fieldType ?? "quantitative";
39
+ if (fieldType !== "quantitative") {
40
+ throw new Error("encodeAngle requires a quantitative field.");
41
+ }
42
+ readQuantitativeField(dataset.values, args.field);
43
+ }
44
+
45
+ const previous = layer.encoding?.angle;
46
+ let next = this;
47
+ for (const property of ["datum", "field", "fieldType"]) {
48
+ if (Object.hasOwn(previous ?? {}, property)) {
49
+ next = next.editSemantic({
50
+ property: `layer[${target}].encoding.angle.${property}`,
51
+ remove: true
52
+ });
53
+ }
54
+ }
55
+ if (hasValue) {
56
+ next = next.editSemantic({
57
+ property: `layer[${target}].encoding.angle.datum`,
58
+ value: args.value
59
+ });
60
+ } else {
61
+ next = next
62
+ .editSemantic({
63
+ property: `layer[${target}].encoding.angle.field`,
64
+ value: args.field
65
+ })
66
+ .editSemantic({
67
+ property: `layer[${target}].encoding.angle.fieldType`,
68
+ value: "quantitative"
69
+ });
70
+ }
71
+ return rematerialize(next, layer, target);
72
+ }
73
+ );
74
+
75
+ export function registerAngleEncodingAction(ProgramClass) {
76
+ ProgramClass.prototype.encodeAngle = encodeAngle;
77
+ }
@@ -277,12 +277,16 @@ const encodeOpacity = action(
277
277
  }
278
278
  );
279
279
 
280
+ export function registerBasicAppearanceEncodingActions(ProgramClass) {
281
+ ProgramClass.prototype.encodeSize = encodeSize;
282
+ ProgramClass.prototype.encodeShape = encodeShape;
283
+ }
284
+
280
285
  export function registerAppearanceEncodingAction(ProgramClass) {
286
+ registerBasicAppearanceEncodingActions(ProgramClass);
281
287
  ProgramClass.prototype.encodeRadius = encodeRadius;
282
288
  ProgramClass.prototype.encodePointRadius = encodePointRadius;
283
289
  ProgramClass.prototype.removePointRadius = removePointRadius;
284
- ProgramClass.prototype.encodeSize = encodeSize;
285
- ProgramClass.prototype.encodeShape = encodeShape;
286
290
  ProgramClass.prototype.encodeOpacity = encodeOpacity;
287
291
  ProgramClass.prototype.clearOpacityEncoding = clearOpacityEncoding;
288
292
  }
@@ -0,0 +1,21 @@
1
+ import { registerBasicAppearanceEncodingActions } from "./appearance.js";
2
+ import { registerBarWidthEncodingAction } from "./barWidth.js";
3
+ import { registerColorEncodingAction } from "./color/index.js";
4
+ import { registerHistogramEncodingAction } from "./histogram.js";
5
+ import { registerOffsetEncodingAction } from "./offset.js";
6
+ import {
7
+ registerCartesianPositionEncodingActions
8
+ } from "./position/index.js";
9
+ import { registerBasicRangedEncodingActions } from "./ranged.js";
10
+ import { registerStrokeDashEncodingActions } from "./strokeDash.js";
11
+
12
+ export function registerBasicEncodingActions(ProgramClass) {
13
+ registerCartesianPositionEncodingActions(ProgramClass);
14
+ registerBasicRangedEncodingActions(ProgramClass);
15
+ registerColorEncodingAction(ProgramClass);
16
+ registerBasicAppearanceEncodingActions(ProgramClass);
17
+ registerStrokeDashEncodingActions(ProgramClass);
18
+ registerOffsetEncodingAction(ProgramClass);
19
+ registerHistogramEncodingAction(ProgramClass);
20
+ registerBarWidthEncodingAction(ProgramClass);
21
+ }
@@ -70,8 +70,18 @@ const encodeColor = action(
70
70
  const horizonColor = horizonTransform !== undefined &&
71
71
  horizonTransform.as?.color === args.field &&
72
72
  layer.encoding?.group?.field === horizonTransform.as?.group;
73
+ const barGrain = resolveBarGrain(layer);
74
+ if (layer.mark.type === "bar" && barGrain === undefined) {
75
+ throw new Error(
76
+ "Bar color encoding requires a complete histogram encoding or a complete ordinal aggregate encoding."
77
+ );
78
+ }
79
+ const layout = resolveColorLayout(layer, args.layout, barGrain);
80
+ const createsCenterGroup = layer.mark.type === "area" &&
81
+ layout === "center" && layer.encoding?.group?.field === undefined;
73
82
  if (
74
83
  layer.mark.type === "area" &&
84
+ !createsCenterGroup &&
75
85
  (layer.encoding?.group?.field === undefined ||
76
86
  layer.encoding.group.field !== args.field) &&
77
87
  !densitySeriesFields.includes(args.field) &&
@@ -82,14 +92,6 @@ const encodeColor = action(
82
92
  );
83
93
  }
84
94
  validateLineSeriesCompatibility(layer, "color", args.field);
85
-
86
- const barGrain = resolveBarGrain(layer);
87
- if (layer.mark.type === "bar" && barGrain === undefined) {
88
- throw new Error(
89
- "Bar color encoding requires a complete histogram encoding or a complete ordinal aggregate encoding."
90
- );
91
- }
92
- const layout = resolveColorLayout(layer, args.layout, barGrain);
93
95
  const requestedScale = resolveReassignmentScaleOptions(
94
96
  layer.encoding?.color,
95
97
  resolveColorScaleOptions(args)
@@ -112,11 +114,13 @@ const encodeColor = action(
112
114
  readNominalField(dataset.values, args.field);
113
115
  }
114
116
 
115
- let next = this
116
- .editSemantic({
117
- property: `layer[${target}].encoding.color.field`,
118
- value: args.field
119
- })
117
+ let next = createsCenterGroup
118
+ ? this.encodeGroup({ target, field: args.field })
119
+ : this;
120
+ next = next.editSemantic({
121
+ property: `layer[${target}].encoding.color.field`,
122
+ value: args.field
123
+ })
120
124
  .editSemantic({
121
125
  property: `layer[${target}].encoding.color.fieldType`,
122
126
  value: fieldType
@@ -37,6 +37,8 @@ export function applyColorLayoutCompanion(
37
37
  }
38
38
  const stack = layout === "fill"
39
39
  ? "normalize"
40
+ : layout === "center"
41
+ ? "center"
40
42
  : layout === "overlay" || layout === "group"
41
43
  ? null
42
44
  : "zero";
@@ -74,6 +74,9 @@ export function resolveColorLayout(layer, requested, barGrain) {
74
74
  if (layer.mark.type === "area" && layout === "group") {
75
75
  throw new Error('Area color layout does not support "group".');
76
76
  }
77
+ if (layer.mark.type === "bar" && layout === "center") {
78
+ throw new Error("Centered bars are not supported.");
79
+ }
77
80
  if (
78
81
  layer.mark.type === "arc" &&
79
82
  layout !== undefined &&
@@ -1,4 +1,5 @@
1
1
  import { registerAppearanceEncodingAction } from "./appearance.js";
2
+ import { registerAngleEncodingAction } from "./angle.js";
2
3
  import { registerBarWidthEncodingAction } from "./barWidth.js";
3
4
  import { registerColorEncodingAction } from "./color/index.js";
4
5
  import { registerDensityEncodingAction } from "./density.js";
@@ -26,6 +27,7 @@ export function registerEncodingActions(ProgramClass) {
26
27
  registerColorEncodingAction(ProgramClass);
27
28
  registerStrokeDashEncodingActions(ProgramClass);
28
29
  registerAppearanceEncodingAction(ProgramClass);
30
+ registerAngleEncodingAction(ProgramClass);
29
31
  registerRuleAppearanceEncodingActions(ProgramClass);
30
32
  registerTextEncodingAction(ProgramClass);
31
33
  registerParallelEncodingAction(ProgramClass);
@@ -135,9 +135,13 @@ const encodeR = action(
135
135
  }
136
136
  );
137
137
 
138
- export function registerPositionEncodingActions(ProgramClass) {
138
+ export function registerCartesianPositionEncodingActions(ProgramClass) {
139
139
  ProgramClass.prototype.encodeX = encodeX;
140
140
  ProgramClass.prototype.encodeY = encodeY;
141
+ }
142
+
143
+ export function registerPositionEncodingActions(ProgramClass) {
144
+ registerCartesianPositionEncodingActions(ProgramClass);
141
145
  ProgramClass.prototype.encodeTheta = encodeTheta;
142
146
  ProgramClass.prototype.encodeR = encodeR;
143
147
  }
@@ -1,4 +1,6 @@
1
1
  import { validateStack, emptyPositionPolicy } from "./common.js";
2
+ import { deriveCenteredAreaSeries } from "../../../../grammar/areaSeries.js";
3
+ import { readQuantitativeField } from "../../../../grammar/scales/index.js";
2
4
 
3
5
  export function isCategoricalDensityPosition({
4
6
  layer,
@@ -15,7 +17,14 @@ export function isCategoricalDensityPosition({
15
17
  density.placement.categoryField === field;
16
18
  }
17
19
 
18
- export function resolveAreaPositionPolicy({ dataset, channel, args, fieldType, field }) {
20
+ export function resolveAreaPositionPolicy({
21
+ dataset,
22
+ layer,
23
+ channel,
24
+ args,
25
+ fieldType,
26
+ field
27
+ }) {
19
28
  const density = dataset.transform?.find(transform => transform.type === "density");
20
29
  if (["nominal", "ordinal"].includes(fieldType)) {
21
30
  if (
@@ -41,6 +50,35 @@ export function resolveAreaPositionPolicy({ dataset, channel, args, fieldType, f
41
50
  throw new Error("Area position encoding does not support aggregate or bin.");
42
51
  }
43
52
  if (args.stack === undefined) return emptyPositionPolicy();
53
+ const stack = validateStack(args.stack, "Area y encoding");
54
+ if (stack === "center") {
55
+ if (channel !== "y") {
56
+ throw new Error("Area center stack requires a y encoding.");
57
+ }
58
+ if (layer.encoding?.group?.fieldType !== "nominal") {
59
+ throw new Error("Area center stack requires a nominal group encoding.");
60
+ }
61
+ if (
62
+ layer.encoding?.color?.layout !== undefined &&
63
+ layer.encoding.color.layout !== "center"
64
+ ) {
65
+ throw new Error("Area center stack requires a matching center color layout.");
66
+ }
67
+ const values = readQuantitativeField(dataset.values, field);
68
+ if (values.some(value => value < 0)) {
69
+ throw new RangeError("Area center stack requires non-negative values.");
70
+ }
71
+ if (layer.encoding?.x !== undefined) {
72
+ deriveCenteredAreaSeries(dataset.values, {
73
+ ...layer,
74
+ encoding: {
75
+ ...layer.encoding,
76
+ y: { field, fieldType, stack: "center" }
77
+ }
78
+ });
79
+ }
80
+ return { bin: undefined, aggregate: undefined, stack };
81
+ }
44
82
  if (
45
83
  channel !== "y" ||
46
84
  density === undefined
@@ -50,6 +88,6 @@ export function resolveAreaPositionPolicy({ dataset, channel, args, fieldType, f
50
88
  return {
51
89
  bin: undefined,
52
90
  aggregate: undefined,
53
- stack: validateStack(args.stack, "Area y encoding")
91
+ stack
54
92
  };
55
93
  }
@@ -52,6 +52,9 @@ export function resolveBarPositionPolicy({
52
52
  throw new Error('Histogram bar y aggregate must be "count".');
53
53
  }
54
54
  stack = validateStack(stack, "Histogram bar y encoding");
55
+ if (stack === "center") {
56
+ throw new Error("Centered bars are not supported.");
57
+ }
55
58
  } else if (fieldType === "quantitative") {
56
59
  if (args.bin !== undefined) {
57
60
  throw new Error(
@@ -80,6 +83,9 @@ export function resolveBarPositionPolicy({
80
83
  aggregate = validateAggregate(aggregate);
81
84
  validateAggregateFieldType(aggregate, fieldType);
82
85
  stack = validateStack(stack, `Bar ${channel} encoding`);
86
+ if (stack === "center") {
87
+ throw new Error("Centered bars are not supported.");
88
+ }
83
89
  } else {
84
90
  throw new Error(
85
91
  "Bar position requires quantitative, temporal, ordinal, or nominal fields."
@@ -9,6 +9,7 @@ import { resolveBarPositionPolicy } from "./bar.js";
9
9
  import { resolveLinePositionPolicy } from "./line.js";
10
10
  import { resolvePointPositionPolicy } from "./point.js";
11
11
  import { resolveRulePositionPolicy } from "./rule.js";
12
+ import { resolveTickPositionPolicy } from "./tick.js";
12
13
 
13
14
  const POSITION_POLICIES = Object.freeze({
14
15
  area: resolveAreaPositionPolicy,
@@ -17,6 +18,7 @@ const POSITION_POLICIES = Object.freeze({
17
18
  line: resolveLinePositionPolicy,
18
19
  point: resolvePointPositionPolicy,
19
20
  rule: resolveRulePositionPolicy,
21
+ tick: resolveTickPositionPolicy,
20
22
  text: resolvePointPositionPolicy,
21
23
  rect: resolvePointPositionPolicy
22
24
  });
@@ -0,0 +1,19 @@
1
+ import { emptyPositionPolicy } from "./common.js";
2
+
3
+ export function resolveTickPositionPolicy({ args, fieldType }) {
4
+ if (!["quantitative", "temporal", "ordinal", "nominal"].includes(fieldType)) {
5
+ throw new Error(
6
+ "Tick position encoding requires quantitative, temporal, ordinal, or nominal fields."
7
+ );
8
+ }
9
+ if (args.aggregate !== undefined) {
10
+ throw new Error("Tick position encoding does not support aggregate.");
11
+ }
12
+ if (args.bin !== undefined) {
13
+ throw new Error("Tick position encoding does not support bin.");
14
+ }
15
+ if (args.stack !== undefined) {
16
+ throw new Error("Tick position encoding does not support stack.");
17
+ }
18
+ return emptyPositionPolicy();
19
+ }
@@ -291,10 +291,14 @@ const encodeGroup = action(
291
291
  }
292
292
  );
293
293
 
294
- export function registerRangedEncodingActions(ProgramClass) {
294
+ export function registerBasicRangedEncodingActions(ProgramClass) {
295
295
  ProgramClass.prototype.encodeX2 = encodeX2;
296
296
  ProgramClass.prototype.encodeY2 = encodeY2;
297
+ ProgramClass.prototype.encodeGroup = encodeGroup;
298
+ }
299
+
300
+ export function registerRangedEncodingActions(ProgramClass) {
301
+ registerBasicRangedEncodingActions(ProgramClass);
297
302
  ProgramClass.prototype.encodeXRange = encodeXRange;
298
303
  ProgramClass.prototype.encodeYRange = encodeYRange;
299
- ProgramClass.prototype.encodeGroup = encodeGroup;
300
304
  }
@@ -21,7 +21,7 @@ const OPTIONS = Object.freeze(["target", "channel"]);
21
21
  const REMOVABLE_CHANNELS = Object.freeze([
22
22
  "x", "y", "x2", "y2", "xOffset", "yOffset", "theta", "radius",
23
23
  "color", "strokeDash", "strokeWidth", "size", "shape", "group",
24
- "opacity", "text"
24
+ "angle", "opacity", "text"
25
25
  ]);
26
26
  const REMOVE_AXIS = Object.freeze({
27
27
  x: "removeXAxis",
@@ -14,7 +14,7 @@ import {
14
14
  } from "./remove.js";
15
15
  import { registerParallelAxisActions } from "./parallel.js";
16
16
 
17
- export function registerGuideAxisActions(ProgramClass) {
17
+ export function registerBasicCartesianAxisActions(ProgramClass) {
18
18
  registerAxisLineActions(ProgramClass);
19
19
  registerAxisTickActions(ProgramClass);
20
20
  registerAxisLabelActions(ProgramClass);
@@ -22,10 +22,18 @@ export function registerGuideAxisActions(ProgramClass) {
22
22
  registerAxisTitleActions(ProgramClass);
23
23
  registerAxisActions(ProgramClass);
24
24
  registerAxisCollectionActions(ProgramClass);
25
+ }
26
+
27
+ export function registerCartesianAxisActions(ProgramClass) {
28
+ registerBasicCartesianAxisActions(ProgramClass);
25
29
  registerCompleteAxisEditActions(ProgramClass);
26
- registerParallelAxisActions(ProgramClass);
27
30
  ProgramClass.prototype.removeXAxis = removeXAxis;
28
31
  ProgramClass.prototype.removeYAxis = removeYAxis;
32
+ }
33
+
34
+ export function registerGuideAxisActions(ProgramClass) {
35
+ registerCartesianAxisActions(ProgramClass);
36
+ registerParallelAxisActions(ProgramClass);
29
37
  ProgramClass.prototype.removeThetaAxis = removeThetaAxis;
30
38
  ProgramClass.prototype.removeRadialAxis = removeRadialAxis;
31
39
  }
@@ -0,0 +1,19 @@
1
+ import { registerBasicCartesianAxisActions } from "./axes/index.js";
2
+ import { registerGuideCollectionActions } from "./guides.js";
3
+ import { registerBasicGridActions } from "./grids/index.js";
4
+ import {
5
+ registerBasicCategoricalLegendActions
6
+ } from "./legends/categorical/index.js";
7
+ import {
8
+ registerGradientLegendActions
9
+ } from "./legends/continuous/index.js";
10
+ import { registerSizeLegendActions } from "./legends/size.js";
11
+
12
+ export function registerBasicGuideActions(ProgramClass) {
13
+ registerBasicCartesianAxisActions(ProgramClass);
14
+ registerBasicCategoricalLegendActions(ProgramClass);
15
+ registerGradientLegendActions(ProgramClass);
16
+ registerSizeLegendActions(ProgramClass);
17
+ registerBasicGridActions(ProgramClass);
18
+ registerGuideCollectionActions(ProgramClass);
19
+ }
@@ -343,13 +343,17 @@ const removeGrid = action(
343
343
  );
344
344
 
345
345
  export function registerGridActions(ProgramClass) {
346
- ProgramClass.prototype.createGrid = createGrid;
347
- ProgramClass.prototype.createHorizontalGrid = createHorizontalGrid;
348
- ProgramClass.prototype.createVerticalGrid = createVerticalGrid;
346
+ registerBasicGridActions(ProgramClass);
349
347
  ProgramClass.prototype.editHorizontalGrid = editHorizontalGrid;
350
348
  ProgramClass.prototype.editVerticalGrid = editVerticalGrid;
351
349
  ProgramClass.prototype.editGrid = editGrid;
352
350
  ProgramClass.prototype.removeGrid = removeGrid;
351
+ }
352
+
353
+ export function registerBasicGridActions(ProgramClass) {
354
+ ProgramClass.prototype.createGrid = createGrid;
355
+ ProgramClass.prototype.createHorizontalGrid = createHorizontalGrid;
356
+ ProgramClass.prototype.createVerticalGrid = createVerticalGrid;
353
357
  ProgramClass.prototype.rematerializeGrid = rematerializeGrid;
354
358
  ProgramClass.prototype.rematerializeHorizontalGrid =
355
359
  rematerializeHorizontalGrid;
@@ -1 +1 @@
1
- export { registerGridActions } from "./grid.js";
1
+ export { registerBasicGridActions, registerGridActions } from "./grid.js";
@@ -8,6 +8,11 @@ import { isSizeLegendPoint } from "../size.js";
8
8
  import { isStrokeWidthLegendLayer } from "../strokeWidth.js";
9
9
  import { legendResourcePolicies } from
10
10
  "../../../../materialization/guides/resources.js";
11
+ import {
12
+ hasMultiHorizontalLegendLane,
13
+ hasMultiLegendLane,
14
+ hasMultiSideLegendLane
15
+ } from "../lane.js";
11
16
  import {
12
17
  resolveCurrentDefinition,
13
18
  resolveDefinition,
@@ -91,6 +96,12 @@ export const rematerializeLegend = action(
91
96
  next = next[policy.rematerializeOp]();
92
97
  }
93
98
  }
99
+ if (hasMultiSideLegendLane(next)) {
100
+ next = next.rematerializeSideLegendLane();
101
+ }
102
+ if (hasMultiHorizontalLegendLane(next)) {
103
+ next = next.rematerializeHorizontalLegendLane();
104
+ }
94
105
  return next;
95
106
  }
96
107
  );
@@ -212,10 +223,11 @@ export const createLegend = action(
212
223
  if (position !== undefined && position !== "right") {
213
224
  throw new Error('Standalone size legends currently require position "right".');
214
225
  }
215
- return this.createSizeLegend({
226
+ const next = this.createSizeLegend({
216
227
  ...(target === undefined ? {} : { target }),
217
228
  ...(count === undefined ? {} : { count })
218
229
  });
230
+ return hasMultiLegendLane(next) ? next.rematerializeLegend() : next;
219
231
  }
220
232
  const strokeWidthCandidates = this.semanticSpec.layers.filter(
221
233
  isStrokeWidthLegendLayer
@@ -248,10 +260,11 @@ export const createLegend = action(
248
260
  'Standalone stroke-width legends currently require position "right".'
249
261
  );
250
262
  }
251
- return this.createStrokeWidthLegend({
263
+ const next = this.createStrokeWidthLegend({
252
264
  ...(target === undefined ? {} : { target }),
253
265
  ...(count === undefined ? {} : { count })
254
266
  });
267
+ return hasMultiLegendLane(next) ? next.rematerializeLegend() : next;
255
268
  }
256
269
  const opacityCandidates = this.semanticSpec.layers.filter(layer =>
257
270
  layer.mark?.type === "point" && layer.encoding?.opacity?.scale !== undefined
@@ -266,7 +279,8 @@ export const createLegend = action(
266
279
  (channels === undefined && opacityCandidates.length === 1 &&
267
280
  !hasOtherLegendCandidate)
268
281
  ) {
269
- return this.createOpacityLegend(args);
282
+ const next = this.createOpacityLegend(args);
283
+ return hasMultiLegendLane(next) ? next.rematerializeLegend() : next;
270
284
  }
271
285
  const continuousColorCandidates = this.semanticSpec.layers.filter(layer => {
272
286
  const encoding = ["point", "bar", "rect"].includes(layer.mark?.type)
@@ -287,7 +301,8 @@ export const createLegend = action(
287
301
  (channels?.length === 1 && channels[0] === "color" && continuousColor) ||
288
302
  (channels === undefined && continuousColor)
289
303
  ) {
290
- return this.createGradientLegend(args);
304
+ const next = this.createGradientLegend(args);
305
+ return hasMultiLegendLane(next) ? next.rematerializeLegend() : next;
291
306
  }
292
307
  const intervalColorCandidates = this.semanticSpec.layers.filter(layer => {
293
308
  const encoding = layer.mark?.type === "point" ? layer.encoding?.color : undefined;
@@ -307,7 +322,8 @@ export const createLegend = action(
307
322
  intervalColorCandidates.length > 0) ||
308
323
  (channels === undefined && intervalColorCandidates.length > 0)
309
324
  ) {
310
- return this.createIntervalLegend(args);
325
+ const next = this.createIntervalLegend(args);
326
+ return hasMultiLegendLane(next) ? next.rematerializeLegend() : next;
311
327
  }
312
328
  const wantsShape = channels?.includes("shape") === true;
313
329
  const pointCandidates = this.semanticSpec.layers.filter(layer =>
@@ -383,7 +399,8 @@ export const createLegend = action(
383
399
  }
384
400
  return next;
385
401
  }
386
- return this.createCategoricalLegend(args);
402
+ const next = this.createCategoricalLegend(args);
403
+ return hasMultiLegendLane(next) ? next.rematerializeLegend() : next;
387
404
  }
388
405
  );
389
406
 
@@ -24,10 +24,13 @@ import {
24
24
  rematerializeLegendHighlights
25
25
  } from "./symbols.js";
26
26
  import { editLegend } from "../edit.js";
27
+ import {
28
+ rematerializeHorizontalLegendLane,
29
+ rematerializeSideLegendLane
30
+ } from "../lane.js";
27
31
 
28
- export function registerCategoricalLegendActions(ProgramClass) {
32
+ export function registerBasicCategoricalLegendActions(ProgramClass) {
29
33
  ProgramClass.prototype.createLegend = createLegend;
30
- ProgramClass.prototype.editLegend = editLegend;
31
34
  ProgramClass.prototype.createCategoricalLegend = createCategoricalLegend;
32
35
  ProgramClass.prototype.removeCategoricalLegend = removeCategoricalLegend;
33
36
  ProgramClass.prototype.createLegendBackground = createLegendBackground;
@@ -46,4 +49,13 @@ export function registerCategoricalLegendActions(ProgramClass) {
46
49
  ProgramClass.prototype.createLegendTitle = createLegendTitle;
47
50
  ProgramClass.prototype.rematerializeLegendTitle = rematerializeLegendTitle;
48
51
  ProgramClass.prototype.rematerializeLegend = rematerializeLegend;
52
+ ProgramClass.prototype.rematerializeSideLegendLane =
53
+ rematerializeSideLegendLane;
54
+ ProgramClass.prototype.rematerializeHorizontalLegendLane =
55
+ rematerializeHorizontalLegendLane;
56
+ }
57
+
58
+ export function registerCategoricalLegendActions(ProgramClass) {
59
+ registerBasicCategoricalLegendActions(ProgramClass);
60
+ ProgramClass.prototype.editLegend = editLegend;
49
61
  }
@@ -105,12 +105,21 @@ export function normalizeContinuousLegend(args, kind) {
105
105
  }
106
106
  const offset = args.offset ?? 30;
107
107
  validateNonNegative(offset, "Legend offset");
108
- const itemGap = args.itemGap ?? 28;
108
+ const titlePosition = args.titlePosition ?? "top";
109
+ if (!["top", "left"].includes(titlePosition)) {
110
+ throw new Error(`Unsupported legend titlePosition "${titlePosition}".`);
111
+ }
112
+ if (
113
+ titlePosition === "left" &&
114
+ (!["top", "bottom"].includes(position) || kind !== "opacity")
115
+ ) {
116
+ throw new Error(
117
+ "Only horizontal opacity legends support left titlePosition."
118
+ );
119
+ }
120
+ const itemGap = args.itemGap ?? (titlePosition === "left" ? 20 : 28);
109
121
  validatePositive(itemGap, "Legend itemGap");
110
122
  if (args.title !== undefined) validateNonEmptyString(args.title, "Legend title");
111
- if (args.titlePosition !== undefined && args.titlePosition !== "top") {
112
- throw new Error("Continuous legends currently require top titlePosition.");
113
- }
114
123
  if (kind === "gradient") {
115
124
  for (const key of ["symbol", "columns", "direction", "itemGap"]) {
116
125
  if (Object.hasOwn(args, key)) {
@@ -135,7 +144,9 @@ export function normalizeContinuousLegend(args, kind) {
135
144
  labels: normalizeLegendTextOptions(
136
145
  args.labels,
137
146
  "createLegend.labels",
138
- DEFAULT_LABELS
147
+ titlePosition === "left"
148
+ ? { ...DEFAULT_LABELS, offset: 8 }
149
+ : DEFAULT_LABELS
139
150
  ),
140
151
  titleStyle: normalizeLegendTextOptions(
141
152
  args.titleStyle,
@@ -143,6 +154,7 @@ export function normalizeContinuousLegend(args, kind) {
143
154
  DEFAULT_TITLE
144
155
  ),
145
156
  itemGap,
157
+ titlePosition,
146
158
  border: normalizeBorder(args.border)
147
159
  };
148
160
  }