ggaction 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +12 -1
  3. package/package.json +6 -1
  4. package/src/actions/boxPlots/components.js +3 -3
  5. package/src/actions/boxPlots/edit.js +277 -0
  6. package/src/actions/boxPlots/index.js +2 -0
  7. package/src/actions/boxPlots/materialize.js +12 -1
  8. package/src/actions/boxPlots/options.js +25 -14
  9. package/src/actions/canvas/actions.js +21 -17
  10. package/src/actions/composition/actions.js +247 -0
  11. package/src/actions/composition/index.js +1 -0
  12. package/src/actions/coordinates/actions.js +2 -5
  13. package/src/actions/data/density.js +7 -1
  14. package/src/actions/data/filter.js +3 -1
  15. package/src/actions/encodings/appearance.js +11 -0
  16. package/src/actions/encodings/barWidth.js +5 -3
  17. package/src/actions/encodings/color.js +120 -25
  18. package/src/actions/encodings/index.js +2 -0
  19. package/src/actions/encodings/offset.js +90 -72
  20. package/src/actions/encodings/position/apply.js +66 -0
  21. package/src/actions/encodings/position/policies/arc.js +26 -0
  22. package/src/actions/encodings/position/policies/bar.js +9 -5
  23. package/src/actions/encodings/position/policies/index.js +5 -1
  24. package/src/actions/encodings/position/policies/line.js +20 -3
  25. package/src/actions/encodings/position/resolve.js +57 -8
  26. package/src/actions/encodings/position.js +44 -90
  27. package/src/actions/encodings/ranged.js +28 -6
  28. package/src/actions/encodings/shared.js +17 -21
  29. package/src/actions/encodings/text.js +73 -0
  30. package/src/actions/errorBands/create.js +37 -29
  31. package/src/actions/errorBands/edit.js +206 -0
  32. package/src/actions/errorBands/index.js +9 -0
  33. package/src/actions/errorBands/options.js +39 -0
  34. package/src/actions/errorBars/create.js +50 -43
  35. package/src/actions/errorBars/edit.js +130 -0
  36. package/src/actions/errorBars/index.js +3 -0
  37. package/src/actions/errorBars/options.js +52 -0
  38. package/src/actions/facets/actions.js +162 -0
  39. package/src/actions/facets/derive.js +200 -0
  40. package/src/actions/facets/guides.js +16 -0
  41. package/src/actions/facets/index.js +1 -0
  42. package/src/actions/facets/replay.js +69 -0
  43. package/src/actions/guides/applicability.js +113 -0
  44. package/src/actions/guides/axes/axes.js +70 -16
  45. package/src/actions/guides/axes/edit.js +126 -0
  46. package/src/actions/guides/axes/index.js +12 -0
  47. package/src/actions/guides/axes/labels.js +21 -18
  48. package/src/actions/guides/axes/remove.js +60 -0
  49. package/src/actions/guides/axes/tickGroups.js +7 -5
  50. package/src/actions/guides/axes/ticks.js +9 -4
  51. package/src/actions/guides/axes/titles.js +7 -5
  52. package/src/actions/guides/grids/grid.js +144 -15
  53. package/src/actions/guides/grids/resolve.js +5 -13
  54. package/src/actions/guides/guides.js +17 -75
  55. package/src/actions/guides/index.js +6 -0
  56. package/src/actions/guides/legends/categorical/actions.js +48 -9
  57. package/src/actions/guides/legends/categorical/components.js +16 -16
  58. package/src/actions/guides/legends/categorical/index.js +14 -14
  59. package/src/actions/guides/legends/categorical/layout.js +3 -1
  60. package/src/actions/guides/legends/categorical/options.js +2 -5
  61. package/src/actions/guides/legends/categorical/resolve.js +2 -2
  62. package/src/actions/guides/legends/categorical/symbols.js +14 -14
  63. package/src/actions/guides/legends/continuous/common.js +7 -7
  64. package/src/actions/guides/legends/continuous/interval.js +5 -5
  65. package/src/actions/guides/legends/edit.js +2 -6
  66. package/src/actions/guides/legends/focused.js +63 -0
  67. package/src/actions/guides/legends/index.js +4 -0
  68. package/src/actions/guides/legends/remove.js +64 -0
  69. package/src/actions/guides/legends/size.js +12 -14
  70. package/src/actions/guides/polar/axes/facade.js +219 -0
  71. package/src/actions/guides/polar/axes/labels.js +188 -0
  72. package/src/actions/guides/polar/axes/lines.js +113 -0
  73. package/src/actions/guides/polar/axes/shared.js +112 -0
  74. package/src/actions/guides/polar/axes/ticks.js +164 -0
  75. package/src/actions/guides/polar/axes/titles.js +165 -0
  76. package/src/actions/guides/polar/axes.js +55 -0
  77. package/src/actions/guides/polar/grids.js +252 -0
  78. package/src/actions/guides/polar/index.js +2 -0
  79. package/src/actions/guides/polar/resolve.js +269 -0
  80. package/src/actions/index.js +4 -0
  81. package/src/actions/marks/arc/actions.js +239 -0
  82. package/src/actions/marks/arc/index.js +1 -0
  83. package/src/actions/marks/{area.js → area/actions.js} +51 -38
  84. package/src/actions/marks/area/index.js +4 -0
  85. package/src/actions/marks/bar/create.js +26 -5
  86. package/src/actions/marks/bar/materialize.js +25 -31
  87. package/src/actions/marks/index.js +12 -4
  88. package/src/actions/marks/lifecycle.js +27 -0
  89. package/src/actions/marks/line/actions.js +373 -0
  90. package/src/actions/marks/line/index.js +1 -0
  91. package/src/actions/marks/{point.js → point/actions.js} +67 -108
  92. package/src/actions/marks/point/index.js +1 -0
  93. package/src/actions/marks/rect/actions.js +165 -0
  94. package/src/actions/marks/rect/index.js +1 -0
  95. package/src/actions/marks/remove.js +214 -0
  96. package/src/actions/marks/{rule.js → rule/actions.js} +34 -28
  97. package/src/actions/marks/rule/index.js +1 -0
  98. package/src/actions/marks/shared.js +156 -25
  99. package/src/actions/marks/text/actions.js +192 -0
  100. package/src/actions/marks/text/index.js +1 -0
  101. package/src/actions/primitives/createGraphics.js +13 -6
  102. package/src/actions/primitives/editGraphics.js +10 -3
  103. package/src/actions/primitives/semantic.js +26 -3
  104. package/src/actions/primitives/semanticValue.js +25 -3
  105. package/src/actions/regression/components.js +1 -1
  106. package/src/actions/regression/create.js +18 -1
  107. package/src/actions/regression/edit.js +247 -0
  108. package/src/actions/regression/index.js +3 -0
  109. package/src/actions/regression/resolve.js +3 -1
  110. package/src/actions/scales/consumers.js +15 -6
  111. package/src/actions/scales/definitions.js +13 -4
  112. package/src/actions/scales/edit.js +42 -19
  113. package/src/actions/scales/materialize.js +57 -408
  114. package/src/actions/selection/actions.js +38 -24
  115. package/src/actions/selection/index.js +2 -0
  116. package/src/actions/titles/actions.js +63 -2
  117. package/src/actions/titles/index.js +2 -0
  118. package/src/actions/titles/resolve.js +12 -17
  119. package/src/composition.js +10 -0
  120. package/src/core/ChartProgram.js +200 -2
  121. package/src/core/action.js +10 -0
  122. package/src/core/validation.js +16 -0
  123. package/src/core/vocabulary.js +154 -4
  124. package/src/grammar/arcs.js +154 -0
  125. package/src/grammar/bars/aggregate.js +2 -2
  126. package/src/grammar/bars/geometry.js +3 -3
  127. package/src/grammar/bars/policy.js +7 -1
  128. package/src/grammar/coordinates.js +3 -7
  129. package/src/grammar/density.js +20 -1
  130. package/src/grammar/facets/dependencies.js +185 -0
  131. package/src/grammar/facets/guides.js +257 -0
  132. package/src/grammar/facets/scales.js +254 -0
  133. package/src/grammar/facets.js +122 -0
  134. package/src/grammar/lineSeries.js +111 -3
  135. package/src/grammar/polar.js +102 -0
  136. package/src/grammar/polarGuides.js +243 -0
  137. package/src/grammar/polarLineCommands.js +43 -0
  138. package/src/grammar/polarPaths.js +161 -0
  139. package/src/grammar/positionCompatibility.js +18 -2
  140. package/src/grammar/rects.js +26 -0
  141. package/src/grammar/scales/continuous.js +6 -1
  142. package/src/grammar/scales/fields.js +7 -0
  143. package/src/grammar/scales/ordinal.js +5 -2
  144. package/src/grammar/scales/policies.js +1 -1
  145. package/src/grammar/schemas/graphic.js +1 -1
  146. package/src/grammar/schemas/graphicBounds.js +64 -9
  147. package/src/grammar/schemas/graphicTree.js +44 -10
  148. package/src/grammar/schemas/semanticPath.js +31 -9
  149. package/src/grammar/seriesLayout.js +13 -2
  150. package/src/grammar/text.js +71 -0
  151. package/src/grammar/ticks.js +50 -13
  152. package/src/index.js +1 -0
  153. package/src/layout/composition.js +310 -0
  154. package/src/layout/facets.js +131 -0
  155. package/src/layout/title.js +6 -0
  156. package/src/materialization/bars/aggregate.js +5 -2
  157. package/src/materialization/bars/grouped.js +75 -37
  158. package/src/materialization/composition.js +152 -0
  159. package/src/materialization/compositionSnapshot.js +114 -0
  160. package/src/materialization/dependencies.js +71 -16
  161. package/src/materialization/encodings.js +14 -28
  162. package/src/materialization/facetGuides.js +426 -0
  163. package/src/materialization/facets.js +214 -0
  164. package/src/materialization/graphicHierarchy.js +15 -5
  165. package/src/materialization/guides/resources.js +46 -0
  166. package/src/materialization/marks.js +220 -14
  167. package/src/materialization/planner.js +105 -12
  168. package/src/materialization/rect.js +138 -0
  169. package/src/materialization/rectConfig.js +38 -0
  170. package/src/materialization/rowEncoding.js +33 -0
  171. package/src/materialization/scales/map.js +40 -0
  172. package/src/materialization/scales/policies/arc.js +37 -0
  173. package/src/materialization/scales/policies/bar.js +132 -0
  174. package/src/materialization/scales/policies/series.js +76 -0
  175. package/src/materialization/scales/resolve.js +326 -0
  176. package/src/materialization/selection/items/arc.js +29 -0
  177. package/src/materialization/selection/items/bar.js +255 -0
  178. package/src/materialization/selection/items/common.js +131 -0
  179. package/src/materialization/selection/items/index.js +6 -0
  180. package/src/materialization/selection/items/path.js +59 -0
  181. package/src/materialization/selection/items/point.js +55 -0
  182. package/src/materialization/selection/items/rect.js +23 -0
  183. package/src/materialization/selection/items/rule.js +25 -0
  184. package/src/materialization/selection/policies/arc.js +10 -0
  185. package/src/materialization/selection/policies/area.js +1 -1
  186. package/src/materialization/selection/policies/bar.js +1 -1
  187. package/src/materialization/selection/policies/index.js +5 -1
  188. package/src/materialization/selection/policies/line.js +1 -1
  189. package/src/materialization/selection/policies/point.js +1 -1
  190. package/src/materialization/selection/policies/rect.js +10 -0
  191. package/src/materialization/selection/policies/rule.js +1 -1
  192. package/src/materialization/selection/styles.js +24 -16
  193. package/src/materialization/text.js +156 -0
  194. package/src/renderers/canvas/index.js +68 -22
  195. package/types/extension.d.ts +1 -0
  196. package/types/index.d.ts +56 -1
  197. package/types/program.d.ts +664 -27
  198. package/src/actions/marks/line.js +0 -248
  199. package/src/materialization/selection/items.js +0 -503
@@ -22,7 +22,7 @@ import {
22
22
  resolveAxisTitleGeometry,
23
23
  validateAxisPosition
24
24
  } from "./policy.js";
25
- import { resolvePlotGraphicPlacement } from
25
+ import { findCanvasGraphic, resolvePlotGraphicPlacement } from
26
26
  "../../../materialization/graphicHierarchy.js";
27
27
 
28
28
  const CREATE_OPTIONS = Object.freeze([
@@ -52,7 +52,7 @@ function validateConfig(channel, config) {
52
52
  if (typeof config.fontWeight !== "string" && !Number.isFinite(config.fontWeight)) throw new TypeError("Axis title fontWeight must be a string or number.");
53
53
  }
54
54
 
55
- function inferText(program, channel, scaleId) {
55
+ export function inferAxisTitleText(program, channel, scaleId) {
56
56
  const titles = new Set();
57
57
  const primaryTitles = new Set();
58
58
  for (const layer of program.semanticSpec.layers) {
@@ -138,7 +138,7 @@ function resolveGeometry(program, channel, config) {
138
138
  const sine = Math.sin(config.rotation);
139
139
  const extentX = Math.abs(cosine) * width / 2 + Math.abs(sine) * height / 2;
140
140
  const extentY = Math.abs(sine) * width / 2 + Math.abs(cosine) * height / 2;
141
- const canvas = program.graphicSpec.objects.canvas?.properties;
141
+ const canvas = findCanvasGraphic(program)?.properties;
142
142
  const newEdgeFits = config.position === "top"
143
143
  ? geometry.y - extentY >= 0
144
144
  : config.position === "right"
@@ -184,7 +184,7 @@ function makeEdit(channel) {
184
184
  const resolvedTitle = explicitText
185
185
  ? validateText(text)
186
186
  : config.inferredText === true
187
- ? inferText(this, channel, config.scale)
187
+ ? inferAxisTitleText(this, channel, config.scale)
188
188
  : this.semanticSpec.guides.axis?.[channel]?.title;
189
189
  if (resolvedTitle !== this.semanticSpec.guides.axis?.[channel]?.title) {
190
190
  next = next.editSemantic({
@@ -218,7 +218,9 @@ function makeCreate(channel) {
218
218
  if (guideScale && guideScale !== scale) throw new Error(`${operation.create} conflicts with the existing axis scale.`);
219
219
  if (this.graphicSpec.objects[operation.graphic]) throw new Error(`${operation.create} requires a missing axis title.`);
220
220
  const inferredText = !Object.hasOwn(args, "text");
221
- const text = validateText(args.text ?? inferText(this, channel, scale));
221
+ const text = validateText(
222
+ args.text ?? inferAxisTitleText(this, channel, scale)
223
+ );
222
224
  const position = args.position ?? defaultAxisPosition(channel);
223
225
  const inferredRotation = !Object.hasOwn(args, "rotation");
224
226
  const config = {
@@ -1,6 +1,10 @@
1
1
  import { action } from "../../../core/action.js";
2
2
  import { isPlainObject } from "../../../core/immutable.js";
3
- import { validateKeys } from "../../../core/validation.js";
3
+ import {
4
+ noOptions,
5
+ validateKeys,
6
+ validateOptionObject
7
+ } from "../../../core/validation.js";
4
8
  import {
5
9
  gridNames,
6
10
  editGridConfig,
@@ -11,8 +15,29 @@ import {
11
15
  validateGridCreateArgs,
12
16
  validateGridEditArgs
13
17
  } from "./resolve.js";
18
+ import { polarGuideNames } from "../polar/resolve.js";
19
+ import { resolveAutomaticGridOptions } from "../applicability.js";
14
20
 
15
- const AGGREGATE_OPTIONS = Object.freeze(["horizontal", "vertical"]);
21
+ const CARTESIAN_OPTIONS = Object.freeze(["horizontal", "vertical"]);
22
+ const POLAR_OPTIONS = Object.freeze(["theta", "radial"]);
23
+ const AGGREGATE_OPTIONS = Object.freeze([
24
+ ...CARTESIAN_OPTIONS,
25
+ ...POLAR_OPTIONS
26
+ ]);
27
+
28
+ function removeDirection(program, direction) {
29
+ const graphic = POLAR_OPTIONS.includes(direction)
30
+ ? polarGuideNames(direction).grid
31
+ : gridNames(direction).graphic;
32
+ let next = program.editSemantic({
33
+ property: `guide.grid.${direction}`,
34
+ remove: true
35
+ });
36
+ if (next.graphicSpec.objects[graphic] !== undefined) {
37
+ next = next.editGraphics({ target: graphic, remove: true });
38
+ }
39
+ return next._withoutMaterializationConfig(["guides", "grid", direction]);
40
+ }
16
41
 
17
42
  function makeRematerialize(direction) {
18
43
  const operation = gridNames(direction);
@@ -22,9 +47,7 @@ function makeRematerialize(direction) {
22
47
  description: `Recompute concrete ${direction} grid lines.`
23
48
  },
24
49
  function (args = {}) {
25
- if (!isPlainObject(args) || Object.keys(args).length !== 0) {
26
- throw new TypeError(`${operation.rematerialize} does not accept options.`);
27
- }
50
+ noOptions(args, operation.rematerialize);
28
51
  const storedConfig = this.guideConfigs.grid?.[direction];
29
52
  const semantic = this.semanticSpec.guides.grid?.[direction];
30
53
  if (
@@ -160,21 +183,76 @@ const createVerticalGrid = makeCreate("vertical");
160
183
  const editHorizontalGrid = makeEdit("horizontal");
161
184
  const editVerticalGrid = makeEdit("vertical");
162
185
 
186
+ const editGrid = action(
187
+ {
188
+ op: "editGrid",
189
+ description: "Edit selected existing Cartesian grid directions."
190
+ },
191
+ function (args = {}) {
192
+ validateOptionObject(args, AGGREGATE_OPTIONS, "editGrid", {
193
+ allowEmpty: false,
194
+ emptyMessage: "editGrid requires horizontal or vertical changes.",
195
+ emptyError: Error
196
+ });
197
+ for (const direction of AGGREGATE_OPTIONS) {
198
+ if (!Object.hasOwn(args, direction)) continue;
199
+ if (!isPlainObject(args[direction])) {
200
+ throw new TypeError(`editGrid ${direction} must be a plain object.`);
201
+ }
202
+ validateGridEditArgs(
203
+ args[direction],
204
+ `edit${direction === "horizontal" ? "Horizontal" : "Vertical"}Grid`
205
+ );
206
+ }
207
+ let next = this;
208
+ if (args.horizontal !== undefined) {
209
+ next = next.editHorizontalGrid(args.horizontal);
210
+ }
211
+ if (args.vertical !== undefined) {
212
+ next = next.editVerticalGrid(args.vertical);
213
+ }
214
+ if (args.theta !== undefined) next = next.editThetaGrid(args.theta);
215
+ if (args.radial !== undefined) next = next.editRadialGrid(args.radial);
216
+ return next;
217
+ }
218
+ );
219
+
163
220
  const createGrid = action(
164
221
  {
165
222
  op: "createGrid",
166
223
  description: "Create selected Cartesian grid directions."
167
224
  },
168
225
  function (args = {}) {
169
- if (!isPlainObject(args)) {
170
- throw new TypeError("createGrid options must be a plain object.");
171
- }
172
- validateKeys(args, AGGREGATE_OPTIONS, "createGrid");
173
- const horizontal = normalizeDirection(args.horizontal, "horizontal");
174
- const vertical = args.vertical === undefined
226
+ validateOptionObject(args, AGGREGATE_OPTIONS, "createGrid");
227
+ const automatic = Object.keys(args).length === 0
228
+ ? resolveAutomaticGridOptions(this)
229
+ : undefined;
230
+ const hasExplicitPolar = Object.hasOwn(args, "theta") ||
231
+ Object.hasOwn(args, "radial");
232
+ const polarDefault = automatic !== undefined &&
233
+ (Object.hasOwn(automatic, "theta") || Object.hasOwn(automatic, "radial"));
234
+ const horizontal = automatic !== undefined
235
+ ? automatic.horizontal === false ? undefined : automatic.horizontal
236
+ : polarDefault ||
237
+ (hasExplicitPolar && args.horizontal === undefined)
238
+ ? undefined
239
+ : normalizeDirection(args.horizontal, "horizontal");
240
+ const vertical = automatic !== undefined
241
+ ? automatic.vertical === false ? undefined : automatic.vertical
242
+ : args.vertical === undefined
175
243
  ? undefined
176
244
  : normalizeDirection(args.vertical, "vertical");
177
- if (horizontal === undefined && vertical === undefined) {
245
+ const theta = automatic !== undefined
246
+ ? automatic.theta
247
+ : args.theta === undefined
248
+ ? undefined
249
+ : normalizeDirection(args.theta, "theta");
250
+ const radial = automatic !== undefined
251
+ ? automatic.radial
252
+ : args.radial === undefined
253
+ ? undefined
254
+ : normalizeDirection(args.radial, "radial");
255
+ if ([horizontal, vertical, theta, radial].every(value => value === undefined)) {
178
256
  throw new Error("createGrid requires at least one selected direction.");
179
257
  }
180
258
 
@@ -185,6 +263,8 @@ const createGrid = action(
185
263
  if (vertical !== undefined) {
186
264
  next = next.createVerticalGrid(vertical);
187
265
  }
266
+ if (radial !== undefined) next = next.createRadialGrid(radial);
267
+ if (theta !== undefined) next = next.createThetaGrid(theta);
188
268
  return next;
189
269
  }
190
270
  );
@@ -195,9 +275,7 @@ const rematerializeGrid = action(
195
275
  description: "Recompute every existing grid direction."
196
276
  },
197
277
  function (args = {}) {
198
- if (!isPlainObject(args) || Object.keys(args).length !== 0) {
199
- throw new TypeError("rematerializeGrid does not accept options.");
200
- }
278
+ noOptions(args, "rematerializeGrid");
201
279
  let next = this;
202
280
  let count = 0;
203
281
  if (this.guideConfigs.grid?.horizontal !== undefined) {
@@ -208,6 +286,14 @@ const rematerializeGrid = action(
208
286
  next = next.rematerializeVerticalGrid();
209
287
  count += 1;
210
288
  }
289
+ if (this.guideConfigs.grid?.theta !== undefined) {
290
+ next = next.rematerializeThetaGrid();
291
+ count += 1;
292
+ }
293
+ if (this.guideConfigs.grid?.radial !== undefined) {
294
+ next = next.rematerializeRadialGrid();
295
+ count += 1;
296
+ }
211
297
  if (count === 0) {
212
298
  throw new Error("rematerializeGrid requires an existing grid.");
213
299
  }
@@ -215,12 +301,55 @@ const rematerializeGrid = action(
215
301
  }
216
302
  );
217
303
 
304
+ const removeGrid = action(
305
+ {
306
+ op: "removeGrid",
307
+ description: "Remove selected Cartesian grid directions."
308
+ },
309
+ function (args = {}) {
310
+ validateOptionObject(args, AGGREGATE_OPTIONS, "removeGrid");
311
+ for (const direction of AGGREGATE_OPTIONS) {
312
+ if (
313
+ Object.hasOwn(args, direction) &&
314
+ typeof args[direction] !== "boolean"
315
+ ) {
316
+ throw new TypeError(`removeGrid ${direction} must be a boolean.`);
317
+ }
318
+ }
319
+ const existing = AGGREGATE_OPTIONS.filter(direction =>
320
+ this.semanticSpec.guides.grid?.[direction] !== undefined ||
321
+ this.guideConfigs.grid?.[direction] !== undefined ||
322
+ this.graphicSpec.objects[
323
+ POLAR_OPTIONS.includes(direction)
324
+ ? polarGuideNames(direction).grid
325
+ : gridNames(direction).graphic
326
+ ] !== undefined
327
+ );
328
+ const selected = Object.keys(args).length === 0
329
+ ? existing
330
+ : AGGREGATE_OPTIONS.filter(direction => args[direction] === true);
331
+ if (selected.length === 0) {
332
+ throw new Error("removeGrid requires at least one selected direction.");
333
+ }
334
+ for (const direction of selected) {
335
+ if (!existing.includes(direction)) {
336
+ throw new Error(`removeGrid requires an existing ${direction} grid.`);
337
+ }
338
+ }
339
+ let next = this;
340
+ for (const direction of selected) next = removeDirection(next, direction);
341
+ return next;
342
+ }
343
+ );
344
+
218
345
  export function registerGridActions(ProgramClass) {
219
346
  ProgramClass.prototype.createGrid = createGrid;
220
347
  ProgramClass.prototype.createHorizontalGrid = createHorizontalGrid;
221
348
  ProgramClass.prototype.createVerticalGrid = createVerticalGrid;
222
349
  ProgramClass.prototype.editHorizontalGrid = editHorizontalGrid;
223
350
  ProgramClass.prototype.editVerticalGrid = editVerticalGrid;
351
+ ProgramClass.prototype.editGrid = editGrid;
352
+ ProgramClass.prototype.removeGrid = removeGrid;
224
353
  ProgramClass.prototype.rematerializeGrid = rematerializeGrid;
225
354
  ProgramClass.prototype.rematerializeHorizontalGrid =
226
355
  rematerializeHorizontalGrid;
@@ -1,7 +1,6 @@
1
1
  import { validateUserId } from "../../../core/identifiers.js";
2
- import { isPlainObject } from "../../../core/immutable.js";
3
2
  import {
4
- validateKeys,
3
+ validateOptionObject,
5
4
  validateNonEmptyString,
6
5
  validateNonNegativeFinite
7
6
  } from "../../../core/validation.js";
@@ -58,10 +57,7 @@ function validateStrokeDash(value) {
58
57
  }
59
58
 
60
59
  export function validateGridCreateArgs(args, operation) {
61
- if (!isPlainObject(args)) {
62
- throw new TypeError(`${operation} options must be a plain object.`);
63
- }
64
- validateKeys(args, GRID_OPTIONS, operation);
60
+ validateOptionObject(args, GRID_OPTIONS, operation);
65
61
  if (Object.hasOwn(args, "count") && Object.hasOwn(args, "values")) {
66
62
  throw new Error(`${operation} cannot use count and values together.`);
67
63
  }
@@ -85,13 +81,9 @@ export function validateGridCreateArgs(args, operation) {
85
81
  }
86
82
 
87
83
  export function validateGridEditArgs(args, operation) {
88
- if (!isPlainObject(args)) {
89
- throw new TypeError(`${operation} options must be a plain object.`);
90
- }
91
- validateKeys(args, GRID_EDIT_OPTIONS, operation);
92
- if (Object.keys(args).length === 0) {
93
- throw new TypeError(`${operation} requires at least one option.`);
94
- }
84
+ validateOptionObject(args, GRID_EDIT_OPTIONS, operation, {
85
+ allowEmpty: false
86
+ });
95
87
  if (Object.hasOwn(args, "count") && Object.hasOwn(args, "values")) {
96
88
  throw new Error(`${operation} cannot use count and values together.`);
97
89
  }
@@ -1,10 +1,10 @@
1
1
  import { action } from "../../core/action.js";
2
2
  import { isPlainObject } from "../../core/immutable.js";
3
+ import { validateOptionObject } from "../../core/validation.js";
3
4
  import {
4
- BAR_ORIENTATIONS,
5
- resolveBarOrientation
6
- } from "../../grammar/bars/policy.js";
7
- import { findSemanticScale } from "../../selectors/scales.js";
5
+ resolveAutomaticGridOptions,
6
+ resolveGuideApplicability
7
+ } from "./applicability.js";
8
8
 
9
9
  const OPTIONS = Object.freeze(["axes", "grid", "legend"]);
10
10
 
@@ -15,54 +15,12 @@ function validateGuideOption(value, label) {
15
15
  }
16
16
 
17
17
  function validateOptions(args) {
18
- if (!isPlainObject(args)) {
19
- throw new TypeError("createGuides options must be a plain object.");
20
- }
21
- for (const key of Object.keys(args)) {
22
- if (!OPTIONS.includes(key)) {
23
- throw new Error(`Unknown createGuides option "${key}".`);
24
- }
25
- }
18
+ validateOptionObject(args, OPTIONS, "createGuides");
26
19
  validateGuideOption(args.axes, "createGuides axes");
27
20
  validateGuideOption(args.grid, "createGuides grid");
28
21
  validateGuideOption(args.legend, "createGuides legend");
29
22
  }
30
23
 
31
- function hasCartesianEncoding(program) {
32
- return program.semanticSpec.layers.some(
33
- layer => layer.encoding?.x !== undefined || layer.encoding?.y !== undefined
34
- );
35
- }
36
-
37
- function hasGridEncoding(program) {
38
- return program.semanticSpec.layers.some(
39
- layer => layer.encoding?.x?.scale !== undefined ||
40
- layer.encoding?.y?.scale !== undefined
41
- );
42
- }
43
-
44
- function inferGridOptions(program) {
45
- const barOrientations = program.semanticSpec.layers
46
- .map(resolveBarOrientation)
47
- .filter(Boolean);
48
- const positionedLayers = program.semanticSpec.layers.filter(
49
- layer => layer.encoding?.x !== undefined && layer.encoding?.y !== undefined
50
- );
51
- const horizontalQuantitative = positionedLayers.length > 0 &&
52
- positionedLayers.every(layer =>
53
- layer.encoding.x.fieldType === "quantitative" &&
54
- ["nominal", "ordinal", "temporal"].includes(layer.encoding.y.fieldType)
55
- );
56
- if (
57
- horizontalQuantitative ||
58
- (barOrientations.length > 0 &&
59
- barOrientations.every(value => value === BAR_ORIENTATIONS.horizontal))
60
- ) {
61
- return { horizontal: false, vertical: true };
62
- }
63
- return { horizontal: true, vertical: false };
64
- }
65
-
66
24
  function inferAxesOptions(program) {
67
25
  const horizontalInterval = program.semanticSpec.layers.some(layer =>
68
26
  layer.mark?.type === "rule" &&
@@ -75,28 +33,6 @@ function inferAxesOptions(program) {
75
33
  : {};
76
34
  }
77
35
 
78
- function hasLegendEncoding(program) {
79
- return program.semanticSpec.layers.some(
80
- layer =>
81
- (layer.mark?.type === "point" &&
82
- layer.encoding?.opacity?.scale !== undefined) ||
83
- (layer.mark?.type === "point" &&
84
- layer.encoding?.color?.scale !== undefined &&
85
- (layer.encoding?.shape?.scale !== undefined ||
86
- ["sequential", "quantize", "quantile", "threshold"].includes(
87
- findSemanticScale(program, layer.encoding.color.scale)?.type
88
- ))) ||
89
- (layer.mark?.type === "line" &&
90
- ["color", "strokeDash"].some(
91
- channel => layer.encoding?.[channel]?.scale !== undefined
92
- )) ||
93
- (layer.mark?.type === "bar" &&
94
- layer.encoding?.color?.scale !== undefined) ||
95
- (layer.mark?.type === "area" &&
96
- layer.encoding?.color?.scale !== undefined)
97
- );
98
- }
99
-
100
36
  function selectOption(explicit, applicable) {
101
37
  if (explicit === false) return undefined;
102
38
  if (explicit !== undefined) return explicit;
@@ -110,15 +46,21 @@ const createGuides = action(
110
46
  },
111
47
  function (args = {}) {
112
48
  validateOptions(args);
113
- const axes = args.axes === undefined && hasCartesianEncoding(this)
49
+ const applicability = resolveGuideApplicability(this);
50
+ const hasAxes = applicability.axes.cartesian || applicability.axes.polar;
51
+ const axes = args.axes === undefined && applicability.axes.cartesian
114
52
  ? inferAxesOptions(this)
115
- : selectOption(args.axes, hasCartesianEncoding(this));
116
- const grid = args.grid === undefined && hasGridEncoding(this)
117
- ? inferGridOptions(this)
118
- : selectOption(args.grid, hasGridEncoding(this));
53
+ : selectOption(args.axes, hasAxes);
54
+ const grid = args.grid === undefined &&
55
+ (applicability.grid.cartesian || applicability.grid.polar)
56
+ ? resolveAutomaticGridOptions(this)
57
+ : selectOption(
58
+ args.grid,
59
+ applicability.grid.cartesian || applicability.grid.polar
60
+ );
119
61
  const legend = selectOption(
120
62
  args.legend,
121
- hasLegendEncoding(this)
63
+ applicability.legend
122
64
  );
123
65
 
124
66
  if (axes === undefined && grid === undefined && legend === undefined) {
@@ -2,10 +2,16 @@ import { registerGuideAxisActions } from "./axes/index.js";
2
2
  import { registerGuideCollectionActions } from "./guides.js";
3
3
  import { registerGridActions } from "./grids/index.js";
4
4
  import { registerLegendActions } from "./legends/index.js";
5
+ import {
6
+ registerPolarAxisActions,
7
+ registerPolarGridActions
8
+ } from "./polar/index.js";
5
9
 
6
10
  export function registerGuideActions(ProgramClass) {
7
11
  registerGuideAxisActions(ProgramClass);
8
12
  registerLegendActions(ProgramClass);
9
13
  registerGridActions(ProgramClass);
14
+ registerPolarGridActions(ProgramClass);
15
+ registerPolarAxisActions(ProgramClass);
10
16
  registerGuideCollectionActions(ProgramClass);
11
17
  }
@@ -1,9 +1,10 @@
1
1
  import { action } from "../../../../core/action.js";
2
- import { isPlainObject } from "../../../../core/immutable.js";
2
+ import { validateOptionObject } from "../../../../core/validation.js";
3
3
  import { noOptions, resolveLayout, activeConfig } from "./layout.js";
4
4
  import { normalizeOptions } from "./options.js";
5
5
  import { findLayer } from "../../../../selectors/layers.js";
6
6
  import { findSemanticScale } from "../../../../selectors/scales.js";
7
+ import { isSizeLegendPoint } from "../size.js";
7
8
  import {
8
9
  resolveCurrentDefinition,
9
10
  resolveDefinition,
@@ -69,11 +70,11 @@ export const rematerializeLegend = action(
69
70
  });
70
71
  }
71
72
  }
72
- if (config.border !== false) next = next.editLegendBackground();
73
+ if (config.border !== false) next = next.rematerializeLegendBackground();
73
74
  next = next
74
- .editLegendSymbols()
75
- .editLegendLabels();
76
- if (config.titleVisible !== false) next = next.editLegendTitle();
75
+ .rematerializeLegendSymbols()
76
+ .rematerializeLegendLabels();
77
+ if (config.titleVisible !== false) next = next.rematerializeLegendTitle();
77
78
  const hasHighlight = Object.values(
78
79
  next.materializationConfigs.highlights ?? {}
79
80
  ).some(highlight => highlight.target === config.target);
@@ -170,13 +171,51 @@ export const createCategoricalLegend = action(
170
171
  export const createLegend = action(
171
172
  { op: "createLegend", description: "Create an inferred legend for selected channels." },
172
173
  function (args = {}) {
173
- if (!isPlainObject(args)) {
174
- throw new TypeError("createLegend options must be a plain object.");
175
- }
174
+ validateOptionObject(args, undefined, "createLegend");
176
175
  const channels = args.channels;
177
176
  if (channels !== undefined && !Array.isArray(channels)) {
178
177
  throw new TypeError("createLegend channels must be an array.");
179
178
  }
179
+ const explicitSize = channels?.length === 1 && channels[0] === "size";
180
+ const requestedSizeLayer = args.target === undefined
181
+ ? undefined
182
+ : findLayer(this, args.target);
183
+ const sizeCandidates = this.semanticSpec.layers.filter(isSizeLegendPoint);
184
+ const hasNonSizeLegendEncoding = layer =>
185
+ ["color", "shape", "strokeDash", "opacity"].some(channel =>
186
+ layer.encoding?.[channel]?.scale !== undefined
187
+ );
188
+ const hasNonSizeLegendCandidate = this.semanticSpec.layers.some(
189
+ hasNonSizeLegendEncoding
190
+ );
191
+ const inferredSize = channels === undefined && (
192
+ (args.target !== undefined && isSizeLegendPoint(requestedSizeLayer) &&
193
+ !hasNonSizeLegendEncoding(requestedSizeLayer)) ||
194
+ (args.target === undefined && sizeCandidates.length === 1 &&
195
+ !hasNonSizeLegendCandidate)
196
+ );
197
+ if (explicitSize || inferredSize) {
198
+ const {
199
+ target,
200
+ count,
201
+ position,
202
+ channels: _channels,
203
+ ...unsupported
204
+ } = args;
205
+ const unsupportedKeys = Object.keys(unsupported);
206
+ if (unsupportedKeys.length > 0) {
207
+ throw new Error(
208
+ `Standalone size legend does not support option "${unsupportedKeys[0]}".`
209
+ );
210
+ }
211
+ if (position !== undefined && position !== "right") {
212
+ throw new Error('Standalone size legends currently require position "right".');
213
+ }
214
+ return this.createSizeLegend({
215
+ ...(target === undefined ? {} : { target }),
216
+ ...(count === undefined ? {} : { count })
217
+ });
218
+ }
180
219
  const opacityCandidates = this.semanticSpec.layers.filter(layer =>
181
220
  layer.mark?.type === "point" && layer.encoding?.opacity?.scale !== undefined
182
221
  );
@@ -193,7 +232,7 @@ export const createLegend = action(
193
232
  return this.createOpacityLegend(args);
194
233
  }
195
234
  const continuousColorCandidates = this.semanticSpec.layers.filter(layer => {
196
- const encoding = ["point", "bar"].includes(layer.mark?.type)
235
+ const encoding = ["point", "bar", "rect"].includes(layer.mark?.type)
197
236
  ? layer.encoding?.color
198
237
  : undefined;
199
238
  const scale = findSemanticScale(this, encoding?.scale);
@@ -3,14 +3,14 @@ import { activeConfig, graphic, noOptions, resolveLayout } from "./layout.js";
3
3
  import { resolveLegendGraphicPlacement } from
4
4
  "../../../../materialization/graphicHierarchy.js";
5
5
 
6
- export const editLegendLabels = action(
7
- { op: "editLegendLabels", description: "Rematerialize categorical legend labels." },
6
+ export const rematerializeLegendLabels = action(
7
+ { op: "rematerializeLegendLabels", description: "Rematerialize categorical legend labels." },
8
8
  function (args = {}) {
9
- noOptions(args, "editLegendLabels");
9
+ noOptions(args, "rematerializeLegendLabels");
10
10
  const { config } = activeConfig(this);
11
11
  const id = graphic(config, "Labels");
12
12
  if (this.graphicSpec.objects[id]?.type !== "text") {
13
- throw new Error("editLegendLabels requires existing legend labels.");
13
+ throw new Error("rematerializeLegendLabels requires existing legend labels.");
14
14
  }
15
15
  const layout = resolveLayout(this, config);
16
16
  return this
@@ -43,18 +43,18 @@ export const createLegendLabels = action(
43
43
  length: config.domain.length,
44
44
  ...resolveLegendGraphicPlacement(this)
45
45
  })
46
- .editLegendLabels();
46
+ .rematerializeLegendLabels();
47
47
  }
48
48
  );
49
49
 
50
- export const editLegendTitle = action(
51
- { op: "editLegendTitle", description: "Rematerialize the categorical legend title." },
50
+ export const rematerializeLegendTitle = action(
51
+ { op: "rematerializeLegendTitle", description: "Rematerialize the categorical legend title." },
52
52
  function (args = {}) {
53
- noOptions(args, "editLegendTitle");
53
+ noOptions(args, "rematerializeLegendTitle");
54
54
  const { config } = activeConfig(this);
55
55
  const id = graphic(config, "Title");
56
56
  if (this.graphicSpec.objects[id]?.type !== "text") {
57
- throw new Error("editLegendTitle requires an existing legend title.");
57
+ throw new Error("rematerializeLegendTitle requires an existing legend title.");
58
58
  }
59
59
  const layout = resolveLayout(this, config);
60
60
  return this
@@ -92,21 +92,21 @@ export const createLegendTitle = action(
92
92
  type: "text",
93
93
  ...resolveLegendGraphicPlacement(this)
94
94
  })
95
- .editLegendTitle();
95
+ .rematerializeLegendTitle();
96
96
  }
97
97
  );
98
98
 
99
- export const editLegendBackground = action(
100
- { op: "editLegendBackground", description: "Rematerialize the legend background." },
99
+ export const rematerializeLegendBackground = action(
100
+ { op: "rematerializeLegendBackground", description: "Rematerialize the legend background." },
101
101
  function (args = {}) {
102
- noOptions(args, "editLegendBackground");
102
+ noOptions(args, "rematerializeLegendBackground");
103
103
  const { config } = activeConfig(this);
104
104
  if (config.border === false) {
105
- throw new Error("editLegendBackground requires border configuration.");
105
+ throw new Error("rematerializeLegendBackground requires border configuration.");
106
106
  }
107
107
  const id = graphic(config, "Background");
108
108
  if (this.graphicSpec.objects[id]?.type !== "rect") {
109
- throw new Error("editLegendBackground requires an existing background.");
109
+ throw new Error("rematerializeLegendBackground requires an existing background.");
110
110
  }
111
111
  const layout = resolveLayout(this, config).background;
112
112
  return this
@@ -135,6 +135,6 @@ export const createLegendBackground = action(
135
135
  type: "rect",
136
136
  ...resolveLegendGraphicPlacement(this)
137
137
  })
138
- .editLegendBackground();
138
+ .rematerializeLegendBackground();
139
139
  }
140
140
  );