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
@@ -11,6 +11,15 @@ import {
11
11
  resolveCanvasGraphicPlacement
12
12
  } from "../../materialization/graphicHierarchy.js";
13
13
 
14
+ function assertTitleScope(program, operation) {
15
+ if (
16
+ program.compositionSpec !== undefined &&
17
+ program.compositionSpec.type !== "facet"
18
+ ) {
19
+ throw new Error(`${operation} is not available on this composition ChartProgram.`);
20
+ }
21
+ }
22
+
14
23
  function hasRotation(graphic) {
15
24
  const properties = graphic.items?.map(child => child.properties) ??
16
25
  [graphic.properties];
@@ -179,8 +188,13 @@ export const rematerializeTitle = action(
179
188
  );
180
189
 
181
190
  export const createTitle = action(
182
- { op: "createTitle", description: "Create a chart title and optional subtitle." },
191
+ {
192
+ op: "createTitle",
193
+ description: "Create a chart title and optional subtitle.",
194
+ scope: "any"
195
+ },
183
196
  function (args = {}) {
197
+ assertTitleScope(this, "createTitle");
184
198
  const options = normalizeTitleOptions(args);
185
199
  if (Object.keys(this.semanticSpec.title).length > 0) {
186
200
  throw new Error("createTitle requires missing semantic title state.");
@@ -197,6 +211,9 @@ export const createTitle = action(
197
211
  next = next.editSemantic({ property: "title.subtitle", value: subtitle });
198
212
  }
199
213
  next = next._withTitleConfig(config);
214
+ if (next.compositionSpec?.type === "facet") {
215
+ return next.materializeComposition();
216
+ }
200
217
  resolveTitleLayout(next, config);
201
218
  next = next.createTitleText();
202
219
  if (subtitle !== undefined) next = next.createSubtitleText();
@@ -205,8 +222,13 @@ export const createTitle = action(
205
222
  );
206
223
 
207
224
  export const editTitle = action(
208
- { op: "editTitle", description: "Edit one stable chart title resource." },
225
+ {
226
+ op: "editTitle",
227
+ description: "Edit one stable chart title resource.",
228
+ scope: "any"
229
+ },
209
230
  function (args = {}) {
231
+ assertTitleScope(this, "editTitle");
210
232
  if (this.semanticSpec.title.text === undefined) {
211
233
  throw new Error("editTitle requires an existing chart title.");
212
234
  }
@@ -229,7 +251,46 @@ export const editTitle = action(
229
251
  });
230
252
  }
231
253
  next = next._withTitleConfig(normalized.config);
254
+ if (next.compositionSpec?.type === "facet") {
255
+ return next.materializeComposition();
256
+ }
232
257
  resolveTitleLayout(next, normalized.config);
233
258
  return next.rematerializeTitle();
234
259
  }
235
260
  );
261
+
262
+ export const removeTitle = action(
263
+ {
264
+ op: "removeTitle",
265
+ description: "Remove the complete chart title resource.",
266
+ scope: "any"
267
+ },
268
+ function (args = {}) {
269
+ assertTitleScope(this, "removeTitle");
270
+ noOptions(args, "removeTitle");
271
+ if (
272
+ this.semanticSpec.title.text === undefined &&
273
+ this.titleConfig === undefined &&
274
+ this.graphicSpec.objects.chartTitle === undefined &&
275
+ this.graphicSpec.objects.chartSubtitle === undefined
276
+ ) {
277
+ throw new Error("removeTitle requires an existing chart title.");
278
+ }
279
+ let next = this;
280
+ if (next.semanticSpec.title.subtitle !== undefined) {
281
+ next = next.editSemantic({ property: "title.subtitle", remove: true });
282
+ }
283
+ if (next.semanticSpec.title.text !== undefined) {
284
+ next = next.editSemantic({ property: "title.text", remove: true });
285
+ }
286
+ for (const id of ["chartTitle", "chartSubtitle"]) {
287
+ if (next.graphicSpec.objects[id] !== undefined) {
288
+ next = next.editGraphics({ target: id, remove: true });
289
+ }
290
+ }
291
+ next = next._withoutMaterializationConfig(["title"]);
292
+ return next.compositionSpec?.type === "facet"
293
+ ? next.materializeComposition()
294
+ : next;
295
+ }
296
+ );
@@ -3,6 +3,7 @@ import {
3
3
  createTitle,
4
4
  createTitleText,
5
5
  editTitle,
6
+ removeTitle,
6
7
  editSubtitleText,
7
8
  editTitleText,
8
9
  rematerializeTitle
@@ -11,6 +12,7 @@ import {
11
12
  export function registerTitleActions(ProgramClass) {
12
13
  ProgramClass.prototype.createTitle = createTitle;
13
14
  ProgramClass.prototype.editTitle = editTitle;
15
+ ProgramClass.prototype.removeTitle = removeTitle;
14
16
  ProgramClass.prototype.createTitleText = createTitleText;
15
17
  ProgramClass.prototype.editTitleText = editTitleText;
16
18
  ProgramClass.prototype.createSubtitleText = createSubtitleText;
@@ -1,6 +1,7 @@
1
1
  import { isPlainObject } from "../../core/immutable.js";
2
2
  import {
3
3
  validateKeys,
4
+ validateOptionObject,
4
5
  validateNonEmptyString,
5
6
  validateNonNegativeFinite,
6
7
  validatePositiveFinite
@@ -8,6 +9,7 @@ import {
8
9
  import { resolveGraphicBounds } from "../../layout/canvas.js";
9
10
  import { resolveConcreteGraphicBounds } from "../../grammar/schemas/graphicBounds.js";
10
11
  import {
12
+ alignedTextAnchor,
11
13
  alignedTitleAnchor,
12
14
  buildTitleReadingBlock,
13
15
  layoutBoundsIntersect,
@@ -15,6 +17,8 @@ import {
15
17
  unionTitleBounds
16
18
  } from "../../layout/title.js";
17
19
  import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from "../../theme/defaults.js";
20
+ import { findCanvasGraphic } from
21
+ "../../materialization/graphicHierarchy.js";
18
22
 
19
23
  const OPTIONS = Object.freeze([
20
24
  "text", "subtitle", "position", "align", "offset", "gap", "titleStyle",
@@ -129,10 +133,7 @@ function normalizeConfig(args, defaults, { hasSubtitle }) {
129
133
  }
130
134
 
131
135
  export function normalizeTitleOptions(args) {
132
- if (!isPlainObject(args)) {
133
- throw new TypeError("createTitle options must be a plain object.");
134
- }
135
- validateKeys(args, OPTIONS, "createTitle");
136
+ validateOptionObject(args, OPTIONS, "createTitle");
136
137
  const text = validateTitleString(args.text, "Chart title text");
137
138
  const subtitle = args.subtitle === undefined
138
139
  ? undefined
@@ -145,13 +146,11 @@ export function normalizeTitleOptions(args) {
145
146
  }
146
147
 
147
148
  export function normalizeTitleEditOptions(args, previous, semanticTitle) {
148
- if (!isPlainObject(args)) {
149
- throw new TypeError("editTitle options must be a plain object.");
150
- }
151
- validateKeys(args, OPTIONS, "editTitle");
152
- if (Object.keys(args).length === 0) {
153
- throw new Error("editTitle requires at least one change.");
154
- }
149
+ validateOptionObject(args, OPTIONS, "editTitle", {
150
+ allowEmpty: false,
151
+ emptyMessage: "editTitle requires at least one change.",
152
+ emptyError: Error
153
+ });
155
154
  const text = args.text === undefined
156
155
  ? semanticTitle.text
157
156
  : validateTitleString(args.text, "Chart title text");
@@ -233,7 +232,7 @@ function validateLayout(program, position, titleBounds, plot, canvas) {
233
232
 
234
233
  export function resolveTitleLayout(program, config) {
235
234
  const plot = resolveGraphicBounds(program);
236
- const canvas = program.graphicSpec.objects.canvas;
235
+ const canvas = findCanvasGraphic(program);
237
236
  if (
238
237
  plot === undefined ||
239
238
  ![plot.x, plot.y, plot.width, plot.height].every(Number.isFinite) ||
@@ -256,11 +255,7 @@ export function resolveTitleLayout(program, config) {
256
255
  let title;
257
256
  let subtitle;
258
257
  if (horizontal) {
259
- const x = config.align === "left"
260
- ? plot.x
261
- : config.align === "center"
262
- ? plot.x + plot.width / 2
263
- : plot.x + plot.width;
258
+ const x = alignedTextAnchor(plot.x, plot.width, config.align);
264
259
  const blockTop = config.position === "top"
265
260
  ? 16 + config.offset
266
261
  : plot.y + plot.height + config.offset;
@@ -0,0 +1,10 @@
1
+ import { chart } from "./ChartProgram.js";
2
+ import { hconcatAction, vconcatAction } from "./actions/composition/actions.js";
3
+
4
+ export function hconcat(options) {
5
+ return hconcatAction.call(chart(), options);
6
+ }
7
+
8
+ export function vconcat(options) {
9
+ return vconcatAction.call(chart(), options);
10
+ }
@@ -15,12 +15,186 @@ import {
15
15
  createEmptySemanticSpec,
16
16
  createTraceRoot
17
17
  } from "./specs.js";
18
- import { LEGEND_CONFIG_KINDS } from "./vocabulary.js";
18
+ import {
19
+ FACET_SCALE_CHANNELS,
20
+ FACET_SCALE_RESOLUTIONS,
21
+ LEGEND_CONFIG_KINDS
22
+ } from "./vocabulary.js";
19
23
 
20
24
  function ownState(value) {
21
25
  return isOwned(value) ? value : cloneAndFreeze(value);
22
26
  }
23
27
 
28
+ function ownChildPrograms(children) {
29
+ if (!isPlainObject(children)) {
30
+ throw new TypeError("ChartProgram children must be a plain object.");
31
+ }
32
+ if (isOwned(children)) return children;
33
+ const owned = {};
34
+ for (const [id, program] of Object.entries(children)) {
35
+ if (typeof id !== "string" || id.length === 0) {
36
+ throw new TypeError("ChartProgram child IDs must be non-empty strings.");
37
+ }
38
+ if (!(program instanceof ChartProgram)) {
39
+ throw new TypeError(`ChartProgram child "${id}" must be a ChartProgram.`);
40
+ }
41
+ owned[id] = program;
42
+ }
43
+ return freezeOwned(owned);
44
+ }
45
+
46
+ function ownCompositionSpec(compositionSpec, children) {
47
+ const childIds = Object.keys(children);
48
+ if (compositionSpec === undefined) {
49
+ if (childIds.length > 0) {
50
+ throw new Error("ChartProgram children require a compositionSpec.");
51
+ }
52
+ return undefined;
53
+ }
54
+ if (!isPlainObject(compositionSpec)) {
55
+ throw new TypeError("ChartProgram compositionSpec must be a plain object.");
56
+ }
57
+ const allowed = [
58
+ "id", "type", "direction", "children", "columns", "gap", "align",
59
+ "padding", "facet"
60
+ ];
61
+ const unknown = Object.keys(compositionSpec).find(key => !allowed.includes(key));
62
+ if (unknown !== undefined) {
63
+ throw new Error(`Unknown compositionSpec property "${unknown}".`);
64
+ }
65
+ if (typeof compositionSpec.id !== "string" || compositionSpec.id.length === 0) {
66
+ throw new TypeError("compositionSpec.id must be a non-empty string.");
67
+ }
68
+ const facet = compositionSpec.type === "facet";
69
+ if (compositionSpec.type !== undefined && !facet) {
70
+ throw new Error(`Unknown compositionSpec type "${compositionSpec.type}".`);
71
+ }
72
+ if (facet) {
73
+ if (compositionSpec.direction !== undefined) {
74
+ throw new Error("Facet compositionSpec does not use direction.");
75
+ }
76
+ } else if (!["horizontal", "vertical"].includes(compositionSpec.direction)) {
77
+ throw new Error("compositionSpec.direction must be horizontal or vertical.");
78
+ }
79
+ if (
80
+ !Array.isArray(compositionSpec.children) ||
81
+ compositionSpec.children.length < (facet ? 1 : 2) ||
82
+ !compositionSpec.children.every(id => typeof id === "string" && id.length > 0)
83
+ ) {
84
+ throw new TypeError(
85
+ facet
86
+ ? "Facet compositionSpec.children requires at least one child ID."
87
+ : "compositionSpec.children requires at least two child IDs."
88
+ );
89
+ }
90
+ if (new Set(compositionSpec.children).size !== compositionSpec.children.length) {
91
+ throw new Error("compositionSpec.children must not contain duplicate IDs.");
92
+ }
93
+ if (
94
+ compositionSpec.children.length !== childIds.length ||
95
+ compositionSpec.children.some(id => !Object.hasOwn(children, id))
96
+ ) {
97
+ throw new Error("compositionSpec.children must match ChartProgram children exactly.");
98
+ }
99
+ if (facet) {
100
+ if (
101
+ !Number.isInteger(compositionSpec.columns) ||
102
+ compositionSpec.columns <= 0 ||
103
+ compositionSpec.columns > compositionSpec.children.length
104
+ ) {
105
+ throw new RangeError(
106
+ "Facet compositionSpec.columns must be a positive integer no larger than its children."
107
+ );
108
+ }
109
+ if (!isPlainObject(compositionSpec.facet)) {
110
+ throw new TypeError("Facet compositionSpec.facet must be a plain object.");
111
+ }
112
+ const facetKeys = ["data", "field", "values", "scales", "guides"];
113
+ const unknownFacet = Object.keys(compositionSpec.facet).find(
114
+ key => !facetKeys.includes(key)
115
+ );
116
+ if (unknownFacet !== undefined) {
117
+ throw new Error(`Unknown compositionSpec.facet property "${unknownFacet}".`);
118
+ }
119
+ for (const property of ["data", "field"]) {
120
+ if (
121
+ typeof compositionSpec.facet[property] !== "string" ||
122
+ compositionSpec.facet[property].length === 0
123
+ ) {
124
+ throw new TypeError(`compositionSpec.facet.${property} must be a non-empty string.`);
125
+ }
126
+ }
127
+ const values = compositionSpec.facet.values;
128
+ if (
129
+ !Array.isArray(values) ||
130
+ values.length !== compositionSpec.children.length ||
131
+ values.some(value => !(
132
+ typeof value === "string" ||
133
+ typeof value === "boolean" ||
134
+ (typeof value === "number" && Number.isFinite(value))
135
+ )) ||
136
+ new Set(values).size !== values.length
137
+ ) {
138
+ throw new TypeError(
139
+ "compositionSpec.facet.values must contain one unique scalar per child."
140
+ );
141
+ }
142
+ const scales = compositionSpec.facet.scales;
143
+ if (
144
+ !isPlainObject(scales) ||
145
+ Object.keys(scales).length !== FACET_SCALE_CHANNELS.length ||
146
+ FACET_SCALE_CHANNELS.some(channel =>
147
+ !FACET_SCALE_RESOLUTIONS.includes(scales[channel])
148
+ ) ||
149
+ Object.keys(scales).some(channel => !FACET_SCALE_CHANNELS.includes(channel))
150
+ ) {
151
+ throw new Error(
152
+ "compositionSpec.facet.scales requires one shared or independent policy per supported channel."
153
+ );
154
+ }
155
+ const guides = compositionSpec.facet.guides;
156
+ if (
157
+ !isPlainObject(guides) ||
158
+ Object.keys(guides).some(key => !["axes", "legend"].includes(key)) ||
159
+ !["each", "outer"].includes(guides.axes) ||
160
+ ![false, "shared"].includes(guides.legend)
161
+ ) {
162
+ throw new Error(
163
+ 'compositionSpec.facet.guides requires axes "each" or "outer" and legend false or "shared".'
164
+ );
165
+ }
166
+ } else if (
167
+ compositionSpec.columns !== undefined ||
168
+ compositionSpec.facet !== undefined
169
+ ) {
170
+ throw new Error("Concat compositionSpec does not accept facet properties.");
171
+ }
172
+ if (!Number.isFinite(compositionSpec.gap) || compositionSpec.gap < 0) {
173
+ throw new RangeError("compositionSpec.gap must be a non-negative finite number.");
174
+ }
175
+ if (!["start", "center", "end"].includes(compositionSpec.align)) {
176
+ throw new Error("compositionSpec.align must be start, center, or end.");
177
+ }
178
+ if (!isPlainObject(compositionSpec.padding)) {
179
+ throw new TypeError("compositionSpec.padding must be a plain object.");
180
+ }
181
+ for (const side of ["top", "right", "bottom", "left"]) {
182
+ if (!Number.isFinite(compositionSpec.padding[side]) || compositionSpec.padding[side] < 0) {
183
+ throw new RangeError(
184
+ `compositionSpec.padding.${side} must be a non-negative finite number.`
185
+ );
186
+ }
187
+ }
188
+ const paddingKeys = Object.keys(compositionSpec.padding);
189
+ if (
190
+ paddingKeys.length !== 4 ||
191
+ paddingKeys.some(key => !["top", "right", "bottom", "left"].includes(key))
192
+ ) {
193
+ throw new Error("compositionSpec.padding must contain exactly four sides.");
194
+ }
195
+ return ownState(compositionSpec);
196
+ }
197
+
24
198
  function createMaterializationConfigs(
25
199
  markConfigs,
26
200
  guideConfigs,
@@ -54,6 +228,8 @@ export class ChartProgram {
54
228
  guideConfigs = {},
55
229
  titleConfig,
56
230
  canvasConfig,
231
+ children = {},
232
+ compositionSpec,
57
233
  context = {},
58
234
  trace = createTraceRoot(),
59
235
  actionStack = [],
@@ -70,6 +246,8 @@ export class ChartProgram {
70
246
  canvasConfig
71
247
  )
72
248
  : ownState(materializationConfigs);
249
+ this.children = ownChildPrograms(children);
250
+ this.compositionSpec = ownCompositionSpec(compositionSpec, this.children);
73
251
  this.context = ownState(context);
74
252
  this.trace = ownState(trace);
75
253
  this.actionStack = ownState(actionStack);
@@ -118,6 +296,8 @@ export class ChartProgram {
118
296
  graphicSpec = this.graphicSpec,
119
297
  resolvedScales = this.resolvedScales,
120
298
  materializationConfigs = this.materializationConfigs,
299
+ children = this.children,
300
+ compositionSpec = this.compositionSpec,
121
301
  context = this.context,
122
302
  trace = this.trace,
123
303
  actionStack = this.actionStack,
@@ -128,6 +308,8 @@ export class ChartProgram {
128
308
  graphicSpec,
129
309
  resolvedScales,
130
310
  materializationConfigs,
311
+ children,
312
+ compositionSpec,
131
313
  context,
132
314
  trace,
133
315
  actionStack,
@@ -135,6 +317,22 @@ export class ChartProgram {
135
317
  });
136
318
  }
137
319
 
320
+ _assertUnitProgram(operation) {
321
+ if (this.compositionSpec !== undefined) {
322
+ throw new Error(`${operation} is not available on a composition ChartProgram.`);
323
+ }
324
+ }
325
+
326
+ _assertCompositionProgram(operation) {
327
+ if (this.compositionSpec === undefined) {
328
+ throw new Error(`${operation} requires a composition ChartProgram.`);
329
+ }
330
+ }
331
+
332
+ _withCompositionState({ children, compositionSpec, graphicSpec = this.graphicSpec }) {
333
+ return this._clone({ children, compositionSpec, graphicSpec });
334
+ }
335
+
138
336
  _withContext(patch = {}) {
139
337
  if (!isPlainObject(patch)) {
140
338
  throw new TypeError("Context patch must be a plain object.");
@@ -257,7 +455,7 @@ export class ChartProgram {
257
455
  }
258
456
 
259
457
  _withGridConfig(direction, config) {
260
- if (!["horizontal", "vertical"].includes(direction)) {
458
+ if (!["horizontal", "vertical", "theta", "radial"].includes(direction)) {
261
459
  throw new Error(`Unknown grid direction "${direction}".`);
262
460
  }
263
461
  if (!isPlainObject(config)) {
@@ -12,6 +12,8 @@ function summarizeObject(value, ancestors = new WeakSet()) {
12
12
  summary[`${key}Count`] = item.length;
13
13
  } else if (isPlainObject(item)) {
14
14
  summary[key] = summarizeObject(item, ancestors);
15
+ } else if (item !== null && typeof item === "object") {
16
+ summary[`${key}Type`] = item.constructor?.name ?? "object";
15
17
  } else {
16
18
  summary[key] = item;
17
19
  }
@@ -100,11 +102,19 @@ export function action(metadata, implementation) {
100
102
  throw new TypeError("Action implementation must be a function.");
101
103
  }
102
104
 
105
+ const scope = metadata.scope ?? "unit";
106
+ if (!["unit", "composition", "any"].includes(scope)) {
107
+ throw new Error(`Unknown action scope "${scope}".`);
108
+ }
109
+
103
110
  return function wrappedAction(args = {}) {
104
111
  if (!isPlainObject(args)) {
105
112
  throw new TypeError("Action arguments must be a plain object.");
106
113
  }
107
114
 
115
+ if (scope === "unit") this._assertUnitProgram(metadata.op);
116
+ if (scope === "composition") this._assertCompositionProgram(metadata.op);
117
+
108
118
  const entered = this._enterAction({
109
119
  ...metadata,
110
120
  args: summarizeArgs(args)
@@ -8,6 +8,22 @@ export function validateKeys(value, supported, label) {
8
8
  }
9
9
  }
10
10
 
11
+ export function validateOptionObject(value, supported, label, {
12
+ allowEmpty = true,
13
+ plainObjectMessage = `${label} options must be a plain object.`,
14
+ emptyMessage = `${label} requires at least one option.`,
15
+ emptyError = TypeError
16
+ } = {}) {
17
+ if (!isPlainObject(value)) {
18
+ throw new TypeError(plainObjectMessage);
19
+ }
20
+ if (supported !== undefined) validateKeys(value, supported, label);
21
+ if (!allowEmpty && Object.keys(value).length === 0) {
22
+ throw new emptyError(emptyMessage);
23
+ }
24
+ return value;
25
+ }
26
+
11
27
  export function noOptions(args, operation) {
12
28
  if (!isPlainObject(args) || Object.keys(args).length > 0) {
13
29
  throw new Error(`${operation} does not accept options.`);
@@ -3,15 +3,30 @@ export const MARK_TYPES = Object.freeze([
3
3
  "line",
4
4
  "bar",
5
5
  "area",
6
- "rule"
6
+ "arc",
7
+ "rule",
8
+ "text",
9
+ "rect"
7
10
  ]);
8
11
 
12
+ export const MARK_GRAPHIC_TYPES = Object.freeze({
13
+ point: Object.freeze(["circle", "rect", "path", "collection"]),
14
+ line: Object.freeze(["path"]),
15
+ bar: Object.freeze(["rect"]),
16
+ area: Object.freeze(["path"]),
17
+ arc: Object.freeze(["path"]),
18
+ rule: Object.freeze(["line"]),
19
+ text: Object.freeze(["text"]),
20
+ rect: Object.freeze(["rect"])
21
+ });
22
+
9
23
  export const ENCODING_CHANNELS = Object.freeze([
10
24
  "x",
11
25
  "y",
12
26
  "x2",
13
27
  "y2",
14
28
  "xOffset",
29
+ "yOffset",
15
30
  "theta",
16
31
  "radius",
17
32
  "color",
@@ -19,14 +34,149 @@ export const ENCODING_CHANNELS = Object.freeze([
19
34
  "size",
20
35
  "shape",
21
36
  "group",
22
- "opacity"
37
+ "opacity",
38
+ "text"
39
+ ]);
40
+
41
+ const CARTESIAN_MARK_TYPES = Object.freeze([
42
+ "point", "line", "bar", "area", "rule", "text", "rect"
23
43
  ]);
44
+ const POLAR_MARK_TYPES = Object.freeze(["point", "line", "arc"]);
45
+
46
+ export const POSITION_CHANNEL_DEFINITIONS = Object.freeze({
47
+ x: Object.freeze({
48
+ family: "cartesian",
49
+ role: "primary",
50
+ scaleChannel: "x",
51
+ coordinate: Object.freeze({ id: "main", type: "cartesian" }),
52
+ guideChannel: "x",
53
+ gridDirection: "vertical",
54
+ markTypes: CARTESIAN_MARK_TYPES
55
+ }),
56
+ y: Object.freeze({
57
+ family: "cartesian",
58
+ role: "primary",
59
+ scaleChannel: "y",
60
+ coordinate: Object.freeze({ id: "main", type: "cartesian" }),
61
+ guideChannel: "y",
62
+ gridDirection: "horizontal",
63
+ markTypes: CARTESIAN_MARK_TYPES
64
+ }),
65
+ x2: Object.freeze({
66
+ family: "cartesian",
67
+ role: "secondary",
68
+ scaleChannel: "x",
69
+ coordinate: Object.freeze({ id: "main", type: "cartesian" }),
70
+ markTypes: Object.freeze(["area", "rule", "rect"])
71
+ }),
72
+ y2: Object.freeze({
73
+ family: "cartesian",
74
+ role: "secondary",
75
+ scaleChannel: "y",
76
+ coordinate: Object.freeze({ id: "main", type: "cartesian" }),
77
+ markTypes: Object.freeze(["area", "rule", "rect"])
78
+ }),
79
+ xOffset: Object.freeze({
80
+ family: "cartesian",
81
+ role: "offset",
82
+ scaleChannel: "xOffset",
83
+ coordinate: Object.freeze({ id: "main", type: "cartesian" }),
84
+ markTypes: Object.freeze(["bar"])
85
+ }),
86
+ yOffset: Object.freeze({
87
+ family: "cartesian",
88
+ role: "offset",
89
+ scaleChannel: "yOffset",
90
+ coordinate: Object.freeze({ id: "main", type: "cartesian" }),
91
+ markTypes: Object.freeze(["bar"])
92
+ }),
93
+ theta: Object.freeze({
94
+ family: "polar",
95
+ role: "primary",
96
+ scaleChannel: "theta",
97
+ coordinate: Object.freeze({ id: "polar", type: "polar" }),
98
+ guideChannel: "theta",
99
+ gridDirection: "theta",
100
+ markTypes: POLAR_MARK_TYPES
101
+ }),
102
+ radius: Object.freeze({
103
+ family: "polar",
104
+ role: "primary",
105
+ scaleChannel: "radius",
106
+ coordinate: Object.freeze({ id: "polar", type: "polar" }),
107
+ guideChannel: "radius",
108
+ gridDirection: "radial",
109
+ markTypes: POLAR_MARK_TYPES
110
+ })
111
+ });
24
112
 
25
113
  export const SCALED_ENCODING_CHANNELS = Object.freeze(
26
- ENCODING_CHANNELS.filter(channel => channel !== "group")
114
+ ENCODING_CHANNELS.filter(channel => !["group", "text"].includes(channel))
115
+ );
116
+
117
+ export const POSITION_ENCODING_CHANNELS = Object.freeze(
118
+ Object.keys(POSITION_CHANNEL_DEFINITIONS)
119
+ );
120
+
121
+ export const OFFSET_POSITION_CHANNELS = Object.freeze(
122
+ POSITION_ENCODING_CHANNELS.filter(channel =>
123
+ POSITION_CHANNEL_DEFINITIONS[channel].role === "offset"
124
+ )
125
+ );
126
+
127
+ export const CARTESIAN_POSITION_CHANNELS = Object.freeze(
128
+ POSITION_ENCODING_CHANNELS.filter(channel =>
129
+ POSITION_CHANNEL_DEFINITIONS[channel].family === "cartesian" &&
130
+ POSITION_CHANNEL_DEFINITIONS[channel].role === "primary"
131
+ )
132
+ );
133
+
134
+ export const POLAR_POSITION_CHANNELS = Object.freeze(
135
+ POSITION_ENCODING_CHANNELS.filter(channel =>
136
+ POSITION_CHANNEL_DEFINITIONS[channel].family === "polar" &&
137
+ POSITION_CHANNEL_DEFINITIONS[channel].role === "primary"
138
+ )
27
139
  );
28
140
 
29
- export const POSITION_CHANNELS = Object.freeze(["x", "y"]);
141
+ export const POSITION_CHANNELS = Object.freeze([
142
+ ...CARTESIAN_POSITION_CHANNELS,
143
+ ...POLAR_POSITION_CHANNELS
144
+ ]);
145
+
146
+ export const FACET_SCALE_CHANNELS = Object.freeze([
147
+ "x",
148
+ "y",
149
+ "xOffset",
150
+ "yOffset",
151
+ "color",
152
+ "size",
153
+ "shape",
154
+ "opacity",
155
+ "strokeDash"
156
+ ]);
157
+
158
+ export const FACET_SCALE_RESOLUTIONS = Object.freeze([
159
+ "shared",
160
+ "independent"
161
+ ]);
162
+
163
+ export function getPositionChannelDefinition(channel) {
164
+ return POSITION_CHANNEL_DEFINITIONS[channel];
165
+ }
166
+
167
+ export function normalizePositionScaleChannel(channel) {
168
+ return POSITION_CHANNEL_DEFINITIONS[channel]?.scaleChannel ?? channel;
169
+ }
170
+
171
+ export function positionChannelsForFamily(family) {
172
+ return POSITION_CHANNELS.filter(channel =>
173
+ POSITION_CHANNEL_DEFINITIONS[channel].family === family
174
+ );
175
+ }
176
+
177
+ export function getMarkGraphicTypes(markType) {
178
+ return MARK_GRAPHIC_TYPES[markType];
179
+ }
30
180
 
31
181
  export const COLOR_LAYOUTS = Object.freeze([
32
182
  "stack",