ggaction 0.0.2 → 0.0.4

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 (242) hide show
  1. package/CHANGELOG.md +55 -2
  2. package/README.md +95 -72
  3. package/package.json +15 -5
  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/data/regression.js +1 -1
  16. package/src/actions/encodings/appearance.js +12 -1
  17. package/src/actions/encodings/barWidth.js +5 -3
  18. package/src/actions/encodings/color/continuous.js +137 -0
  19. package/src/actions/encodings/color/index.js +151 -0
  20. package/src/actions/encodings/color/layout.js +91 -0
  21. package/src/actions/encodings/color/policy.js +88 -0
  22. package/src/actions/encodings/index.js +4 -2
  23. package/src/actions/encodings/offset.js +91 -73
  24. package/src/actions/encodings/position/apply.js +66 -0
  25. package/src/actions/encodings/position/index.js +130 -0
  26. package/src/actions/encodings/position/policies/arc.js +26 -0
  27. package/src/actions/encodings/position/policies/bar.js +9 -5
  28. package/src/actions/encodings/position/policies/index.js +5 -1
  29. package/src/actions/encodings/position/policies/line.js +20 -3
  30. package/src/actions/encodings/position/resolve.js +58 -9
  31. package/src/actions/encodings/ranged.js +30 -8
  32. package/src/actions/encodings/shared.js +17 -21
  33. package/src/actions/encodings/strokeDash.js +1 -1
  34. package/src/actions/encodings/text.js +73 -0
  35. package/src/actions/errorBands/create.js +37 -29
  36. package/src/actions/errorBands/edit.js +206 -0
  37. package/src/actions/errorBands/index.js +9 -0
  38. package/src/actions/errorBands/options.js +39 -0
  39. package/src/actions/errorBars/create.js +50 -43
  40. package/src/actions/errorBars/edit.js +130 -0
  41. package/src/actions/errorBars/index.js +3 -0
  42. package/src/actions/errorBars/options.js +52 -0
  43. package/src/actions/facets/actions.js +162 -0
  44. package/src/actions/facets/derive.js +200 -0
  45. package/src/actions/facets/guides.js +16 -0
  46. package/src/actions/facets/index.js +1 -0
  47. package/src/actions/facets/replay.js +69 -0
  48. package/src/actions/guides/applicability.js +113 -0
  49. package/src/actions/guides/axes/axes.js +70 -16
  50. package/src/actions/guides/axes/edit.js +126 -0
  51. package/src/actions/guides/axes/index.js +12 -0
  52. package/src/actions/guides/axes/labels.js +22 -19
  53. package/src/actions/guides/axes/remove.js +60 -0
  54. package/src/actions/guides/axes/tickGroups.js +7 -5
  55. package/src/actions/guides/axes/ticks.js +10 -5
  56. package/src/actions/guides/axes/titles.js +8 -6
  57. package/src/actions/guides/grids/grid.js +144 -15
  58. package/src/actions/guides/grids/resolve.js +6 -14
  59. package/src/actions/guides/guides.js +17 -75
  60. package/src/actions/guides/index.js +6 -0
  61. package/src/actions/guides/legends/categorical/actions.js +48 -9
  62. package/src/actions/guides/legends/categorical/components.js +16 -16
  63. package/src/actions/guides/legends/categorical/index.js +14 -14
  64. package/src/actions/guides/legends/categorical/layout.js +7 -3
  65. package/src/actions/guides/legends/categorical/options.js +2 -5
  66. package/src/actions/guides/legends/categorical/resolve.js +2 -2
  67. package/src/actions/guides/legends/categorical/symbols.js +14 -14
  68. package/src/actions/guides/legends/continuous/common.js +7 -7
  69. package/src/actions/guides/legends/continuous/interval.js +6 -6
  70. package/src/actions/guides/legends/continuous/opacity.js +1 -1
  71. package/src/actions/guides/legends/edit.js +4 -26
  72. package/src/actions/guides/legends/focused.js +63 -0
  73. package/src/actions/guides/legends/index.js +4 -0
  74. package/src/actions/guides/legends/remove.js +46 -0
  75. package/src/actions/guides/legends/size.js +13 -15
  76. package/src/actions/guides/legends/target.js +25 -0
  77. package/src/actions/guides/polar/axes/facade.js +219 -0
  78. package/src/actions/guides/polar/axes/index.js +55 -0
  79. package/src/actions/guides/polar/axes/labels.js +188 -0
  80. package/src/actions/guides/polar/axes/lines.js +113 -0
  81. package/src/actions/guides/polar/axes/shared.js +112 -0
  82. package/src/actions/guides/polar/axes/ticks.js +164 -0
  83. package/src/actions/guides/polar/axes/titles.js +165 -0
  84. package/src/actions/guides/polar/grids.js +252 -0
  85. package/src/actions/guides/polar/index.js +2 -0
  86. package/src/actions/guides/polar/resolve.js +269 -0
  87. package/src/actions/guides/tickValues.js +1 -1
  88. package/src/actions/index.js +4 -0
  89. package/src/actions/intervals/resolve.js +1 -1
  90. package/src/actions/marks/arc/actions.js +239 -0
  91. package/src/actions/marks/arc/index.js +1 -0
  92. package/src/actions/marks/{area.js → area/actions.js} +51 -38
  93. package/src/actions/marks/area/index.js +4 -0
  94. package/src/actions/marks/bar/create.js +26 -5
  95. package/src/actions/marks/bar/edit.js +1 -1
  96. package/src/actions/marks/bar/materialize.js +25 -31
  97. package/src/actions/marks/index.js +12 -4
  98. package/src/actions/marks/lifecycle.js +27 -0
  99. package/src/actions/marks/line/actions.js +373 -0
  100. package/src/actions/marks/line/index.js +1 -0
  101. package/src/actions/marks/{point.js → point/actions.js} +67 -108
  102. package/src/actions/marks/point/index.js +1 -0
  103. package/src/actions/marks/rect/actions.js +165 -0
  104. package/src/actions/marks/rect/index.js +1 -0
  105. package/src/actions/marks/remove.js +214 -0
  106. package/src/actions/marks/{rule.js → rule/actions.js} +34 -28
  107. package/src/actions/marks/rule/index.js +1 -0
  108. package/src/actions/marks/shared.js +156 -25
  109. package/src/actions/marks/text/actions.js +192 -0
  110. package/src/actions/marks/text/index.js +1 -0
  111. package/src/actions/primitives/createGraphics.js +13 -6
  112. package/src/actions/primitives/editGraphics.js +10 -3
  113. package/src/actions/primitives/semantic.js +26 -3
  114. package/src/actions/primitives/semanticValue.js +26 -4
  115. package/src/actions/regression/components.js +2 -2
  116. package/src/actions/regression/create.js +19 -2
  117. package/src/actions/regression/edit.js +247 -0
  118. package/src/actions/regression/index.js +3 -0
  119. package/src/actions/regression/resolve.js +3 -1
  120. package/src/actions/scales/consumers/common.js +91 -0
  121. package/src/actions/scales/consumers/families.js +47 -0
  122. package/src/actions/scales/consumers/index.js +41 -0
  123. package/src/actions/scales/{consumers.js → consumers/seriesLayout.js} +14 -127
  124. package/src/actions/scales/create.js +1 -1
  125. package/src/actions/scales/definitions.js +15 -6
  126. package/src/actions/scales/edit.js +45 -22
  127. package/src/actions/scales/materialize.js +58 -409
  128. package/src/actions/selection/actions.js +38 -24
  129. package/src/actions/selection/index.js +2 -0
  130. package/src/actions/titles/actions.js +63 -2
  131. package/src/actions/titles/index.js +2 -0
  132. package/src/actions/titles/resolve.js +12 -17
  133. package/src/composition.js +10 -0
  134. package/src/core/ChartProgram.js +49 -65
  135. package/src/core/action.js +10 -0
  136. package/src/core/compositionState.js +169 -0
  137. package/src/core/materializationState.js +65 -0
  138. package/src/core/programState.js +28 -0
  139. package/src/core/validation.js +16 -0
  140. package/src/core/vocabulary.js +154 -4
  141. package/src/grammar/arcs.js +154 -0
  142. package/src/grammar/areaSeries.js +1 -1
  143. package/src/grammar/bars/aggregate.js +3 -3
  144. package/src/grammar/bars/geometry.js +3 -3
  145. package/src/grammar/bars/policy.js +7 -1
  146. package/src/grammar/coordinates.js +3 -7
  147. package/src/grammar/density.js +21 -2
  148. package/src/grammar/facets/dependencies.js +185 -0
  149. package/src/grammar/facets/guides.js +257 -0
  150. package/src/grammar/facets/index.js +122 -0
  151. package/src/grammar/facets/scales.js +254 -0
  152. package/src/grammar/interval.js +2 -96
  153. package/src/grammar/lineSeries.js +112 -4
  154. package/src/grammar/palettes.js +5 -7
  155. package/src/grammar/polar.js +102 -0
  156. package/src/grammar/polarGuides.js +243 -0
  157. package/src/grammar/polarLineCommands.js +43 -0
  158. package/src/grammar/polarPaths.js +161 -0
  159. package/src/grammar/positionCompatibility.js +18 -2
  160. package/src/grammar/rects.js +26 -0
  161. package/src/grammar/regression/derive.js +97 -0
  162. package/src/grammar/regression/index.js +22 -0
  163. package/src/grammar/regression/models.js +282 -0
  164. package/src/grammar/regression/parameters.js +106 -0
  165. package/src/grammar/rules.js +1 -1
  166. package/src/grammar/scales/color.js +4 -2
  167. package/src/grammar/scales/continuous.js +6 -1
  168. package/src/grammar/scales/fields.js +7 -0
  169. package/src/grammar/scales/ordinal.js +5 -2
  170. package/src/grammar/scales/policies.js +1 -1
  171. package/src/grammar/schemas/graphic.js +1 -1
  172. package/src/grammar/schemas/graphicBounds.js +64 -9
  173. package/src/grammar/schemas/graphicTree.js +44 -10
  174. package/src/grammar/schemas/semanticPath.js +31 -9
  175. package/src/grammar/seriesLayout.js +13 -2
  176. package/src/grammar/statistics/studentT.js +98 -0
  177. package/src/grammar/text.js +71 -0
  178. package/src/grammar/ticks.js +50 -13
  179. package/src/grammar/transforms.js +1 -1
  180. package/src/index.js +1 -0
  181. package/src/layout/composition.js +310 -0
  182. package/src/layout/facets.js +131 -0
  183. package/src/layout/title.js +6 -0
  184. package/src/materialization/bars/aggregate.js +6 -3
  185. package/src/materialization/bars/grouped.js +75 -37
  186. package/src/materialization/bars/histogram.js +1 -1
  187. package/src/materialization/bars/ranged.js +1 -1
  188. package/src/materialization/composition.js +152 -0
  189. package/src/materialization/compositionSnapshot.js +114 -0
  190. package/src/materialization/density.js +1 -1
  191. package/src/materialization/dependencies.js +47 -73
  192. package/src/materialization/encodings.js +14 -28
  193. package/src/materialization/facetGuides/index.js +2 -0
  194. package/src/materialization/facetGuides/legacyCategorical.js +125 -0
  195. package/src/materialization/facetGuides/placement.js +126 -0
  196. package/src/materialization/facetGuides/preparation.js +188 -0
  197. package/src/materialization/facets.js +214 -0
  198. package/src/materialization/graphicHierarchy.js +15 -5
  199. package/src/materialization/guides/resources.js +46 -0
  200. package/src/materialization/marks/capabilities.js +141 -0
  201. package/src/materialization/marks/index.js +105 -0
  202. package/src/materialization/marks/policies.js +87 -0
  203. package/src/materialization/planner.js +105 -12
  204. package/src/materialization/rect.js +138 -0
  205. package/src/materialization/rectConfig.js +38 -0
  206. package/src/materialization/rowEncoding.js +33 -0
  207. package/src/materialization/scaleGuideDependencies.js +94 -0
  208. package/src/materialization/scales/map.js +40 -0
  209. package/src/materialization/scales/policies/arc.js +37 -0
  210. package/src/materialization/scales/policies/bar.js +132 -0
  211. package/src/materialization/scales/policies/series.js +76 -0
  212. package/src/materialization/scales/resolve.js +326 -0
  213. package/src/materialization/selection/items/arc.js +29 -0
  214. package/src/materialization/selection/items/bar.js +255 -0
  215. package/src/materialization/selection/items/common.js +131 -0
  216. package/src/materialization/selection/items/index.js +6 -0
  217. package/src/materialization/selection/items/path.js +59 -0
  218. package/src/materialization/selection/items/point.js +55 -0
  219. package/src/materialization/selection/items/rect.js +23 -0
  220. package/src/materialization/selection/items/rule.js +25 -0
  221. package/src/materialization/selection/policies/arc.js +10 -0
  222. package/src/materialization/selection/policies/area.js +1 -1
  223. package/src/materialization/selection/policies/bar.js +1 -1
  224. package/src/materialization/selection/policies/index.js +5 -1
  225. package/src/materialization/selection/policies/line.js +1 -1
  226. package/src/materialization/selection/policies/point.js +1 -1
  227. package/src/materialization/selection/policies/rect.js +10 -0
  228. package/src/materialization/selection/policies/rule.js +1 -1
  229. package/src/materialization/selection/styles.js +25 -17
  230. package/src/materialization/text.js +156 -0
  231. package/src/renderers/canvas/index.js +68 -22
  232. package/src/renderers/canvas/text.js +14 -1
  233. package/types/extension.d.ts +5 -1
  234. package/types/index.d.ts +56 -1
  235. package/types/program.d.ts +664 -27
  236. package/src/actions/encodings/color.js +0 -329
  237. package/src/actions/encodings/position.js +0 -176
  238. package/src/actions/marks/line.js +0 -248
  239. package/src/grammar/regression.js +0 -578
  240. package/src/grammar/scales.js +0 -1
  241. package/src/materialization/marks.js +0 -110
  242. 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
+ }
@@ -6,43 +6,23 @@ import {
6
6
  import {
7
7
  cloneAndFreeze,
8
8
  freezeOwned,
9
- isOwned,
10
- isPlainObject,
11
- removeOwnedPath
9
+ isPlainObject
12
10
  } from "./immutable.js";
13
11
  import {
14
12
  createEmptyGraphicSpec,
15
13
  createEmptySemanticSpec,
16
14
  createTraceRoot
17
15
  } from "./specs.js";
18
- import { LEGEND_CONFIG_KINDS } from "./vocabulary.js";
19
-
20
- function ownState(value) {
21
- return isOwned(value) ? value : cloneAndFreeze(value);
22
- }
23
-
24
- function createMaterializationConfigs(
25
- markConfigs,
26
- guideConfigs,
27
- titleConfig,
28
- canvasConfig
29
- ) {
30
- return cloneAndFreeze({
31
- marks: markConfigs,
32
- guides: guideConfigs,
33
- ...(canvasConfig === undefined ? {} : { canvas: canvasConfig }),
34
- ...(titleConfig === undefined ? {} : { title: titleConfig })
35
- });
36
- }
37
-
38
- function updateConfigPath(value, path, config) {
39
- if (path.length === 0) return cloneAndFreeze(config);
40
- const [key, ...rest] = path;
41
- return freezeOwned({
42
- ...value,
43
- [key]: updateConfigPath(value?.[key] ?? {}, rest, config)
44
- });
45
- }
16
+ import {
17
+ LEGEND_CONFIG_KINDS
18
+ } from "./vocabulary.js";
19
+ import { ownCompositionSpec } from "./compositionState.js";
20
+ import {
21
+ createMaterializationConfigs,
22
+ removeMaterializationConfig,
23
+ setMaterializationConfig
24
+ } from "./materializationState.js";
25
+ import { ownChildPrograms, ownProgramState } from "./programState.js";
46
26
 
47
27
  export class ChartProgram {
48
28
  constructor({
@@ -54,14 +34,16 @@ export class ChartProgram {
54
34
  guideConfigs = {},
55
35
  titleConfig,
56
36
  canvasConfig,
37
+ children = {},
38
+ compositionSpec,
57
39
  context = {},
58
40
  trace = createTraceRoot(),
59
41
  actionStack = [],
60
42
  actionSequence
61
43
  } = {}) {
62
- this.semanticSpec = ownState(semanticSpec);
63
- this.graphicSpec = ownState(graphicSpec);
64
- this.resolvedScales = ownState(resolvedScales);
44
+ this.semanticSpec = ownProgramState(semanticSpec);
45
+ this.graphicSpec = ownProgramState(graphicSpec);
46
+ this.resolvedScales = ownProgramState(resolvedScales);
65
47
  this.materializationConfigs = materializationConfigs === undefined
66
48
  ? createMaterializationConfigs(
67
49
  markConfigs,
@@ -69,10 +51,12 @@ export class ChartProgram {
69
51
  titleConfig,
70
52
  canvasConfig
71
53
  )
72
- : ownState(materializationConfigs);
73
- this.context = ownState(context);
74
- this.trace = ownState(trace);
75
- this.actionStack = ownState(actionStack);
54
+ : ownProgramState(materializationConfigs);
55
+ this.children = ownChildPrograms(children, ChartProgram);
56
+ this.compositionSpec = ownCompositionSpec(compositionSpec, this.children);
57
+ this.context = ownProgramState(context);
58
+ this.trace = ownProgramState(trace);
59
+ this.actionStack = ownProgramState(actionStack);
76
60
  Object.defineProperty(this, "_actionSequence", {
77
61
  value: actionSequence ?? countActionNodes(this.trace),
78
62
  enumerable: false
@@ -118,6 +102,8 @@ export class ChartProgram {
118
102
  graphicSpec = this.graphicSpec,
119
103
  resolvedScales = this.resolvedScales,
120
104
  materializationConfigs = this.materializationConfigs,
105
+ children = this.children,
106
+ compositionSpec = this.compositionSpec,
121
107
  context = this.context,
122
108
  trace = this.trace,
123
109
  actionStack = this.actionStack,
@@ -128,6 +114,8 @@ export class ChartProgram {
128
114
  graphicSpec,
129
115
  resolvedScales,
130
116
  materializationConfigs,
117
+ children,
118
+ compositionSpec,
131
119
  context,
132
120
  trace,
133
121
  actionStack,
@@ -135,6 +123,22 @@ export class ChartProgram {
135
123
  });
136
124
  }
137
125
 
126
+ _assertUnitProgram(operation) {
127
+ if (this.compositionSpec !== undefined) {
128
+ throw new Error(`${operation} is not available on a composition ChartProgram.`);
129
+ }
130
+ }
131
+
132
+ _assertCompositionProgram(operation) {
133
+ if (this.compositionSpec === undefined) {
134
+ throw new Error(`${operation} requires a composition ChartProgram.`);
135
+ }
136
+ }
137
+
138
+ _withCompositionState({ children, compositionSpec, graphicSpec = this.graphicSpec }) {
139
+ return this._clone({ children, compositionSpec, graphicSpec });
140
+ }
141
+
138
142
  _withContext(patch = {}) {
139
143
  if (!isPlainObject(patch)) {
140
144
  throw new TypeError("Context patch must be a plain object.");
@@ -188,18 +192,8 @@ export class ChartProgram {
188
192
  }
189
193
 
190
194
  _withMaterializationConfig(path, config) {
191
- if (
192
- !Array.isArray(path) ||
193
- path.length === 0 ||
194
- !path.every(key => typeof key === "string" && key.length > 0)
195
- ) {
196
- throw new TypeError("Materialization config path must contain names.");
197
- }
198
- if (!isPlainObject(config)) {
199
- throw new TypeError("Materialization config must be a plain object.");
200
- }
201
195
  return this._clone({
202
- materializationConfigs: updateConfigPath(
196
+ materializationConfigs: setMaterializationConfig(
203
197
  this.materializationConfigs,
204
198
  path,
205
199
  config
@@ -208,22 +202,12 @@ export class ChartProgram {
208
202
  }
209
203
 
210
204
  _withoutMaterializationConfig(path) {
211
- if (
212
- !Array.isArray(path) ||
213
- path.length === 0 ||
214
- !path.every(key => typeof key === "string" && key.length > 0)
215
- ) {
216
- throw new TypeError("Materialization config path must contain names.");
217
- }
218
- const removed = removeOwnedPath(this.materializationConfigs, path);
205
+ const removed = removeMaterializationConfig(
206
+ this.materializationConfigs,
207
+ path
208
+ );
219
209
  if (!removed.removed) return this;
220
- return this._clone({
221
- materializationConfigs: freezeOwned({
222
- ...removed.value,
223
- marks: removed.value.marks ?? freezeOwned({}),
224
- guides: removed.value.guides ?? freezeOwned({})
225
- })
226
- });
210
+ return this._clone({ materializationConfigs: removed.value });
227
211
  }
228
212
 
229
213
  _withGuideConfig(channel, component, config) {
@@ -257,7 +241,7 @@ export class ChartProgram {
257
241
  }
258
242
 
259
243
  _withGridConfig(direction, config) {
260
- if (!["horizontal", "vertical"].includes(direction)) {
244
+ if (!["horizontal", "vertical", "theta", "radial"].includes(direction)) {
261
245
  throw new Error(`Unknown grid direction "${direction}".`);
262
246
  }
263
247
  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)
@@ -0,0 +1,169 @@
1
+ import { isPlainObject } from "./immutable.js";
2
+ import { ownProgramState } from "./programState.js";
3
+ import {
4
+ FACET_SCALE_CHANNELS,
5
+ FACET_SCALE_RESOLUTIONS
6
+ } from "./vocabulary.js";
7
+
8
+ function validateFacetSpec(compositionSpec) {
9
+ if (compositionSpec.direction !== undefined) {
10
+ throw new Error("Facet compositionSpec does not use direction.");
11
+ }
12
+ if (
13
+ !Number.isInteger(compositionSpec.columns) ||
14
+ compositionSpec.columns <= 0 ||
15
+ compositionSpec.columns > compositionSpec.children.length
16
+ ) {
17
+ throw new RangeError(
18
+ "Facet compositionSpec.columns must be a positive integer no larger than its children."
19
+ );
20
+ }
21
+ if (!isPlainObject(compositionSpec.facet)) {
22
+ throw new TypeError("Facet compositionSpec.facet must be a plain object.");
23
+ }
24
+ const facetKeys = ["data", "field", "values", "scales", "guides"];
25
+ const unknownFacet = Object.keys(compositionSpec.facet).find(
26
+ key => !facetKeys.includes(key)
27
+ );
28
+ if (unknownFacet !== undefined) {
29
+ throw new Error(`Unknown compositionSpec.facet property "${unknownFacet}".`);
30
+ }
31
+ for (const property of ["data", "field"]) {
32
+ if (
33
+ typeof compositionSpec.facet[property] !== "string" ||
34
+ compositionSpec.facet[property].length === 0
35
+ ) {
36
+ throw new TypeError(`compositionSpec.facet.${property} must be a non-empty string.`);
37
+ }
38
+ }
39
+ const values = compositionSpec.facet.values;
40
+ if (
41
+ !Array.isArray(values) ||
42
+ values.length !== compositionSpec.children.length ||
43
+ values.some(value => !(
44
+ typeof value === "string" ||
45
+ typeof value === "boolean" ||
46
+ (typeof value === "number" && Number.isFinite(value))
47
+ )) ||
48
+ new Set(values).size !== values.length
49
+ ) {
50
+ throw new TypeError(
51
+ "compositionSpec.facet.values must contain one unique scalar per child."
52
+ );
53
+ }
54
+ const scales = compositionSpec.facet.scales;
55
+ if (
56
+ !isPlainObject(scales) ||
57
+ Object.keys(scales).length !== FACET_SCALE_CHANNELS.length ||
58
+ FACET_SCALE_CHANNELS.some(channel =>
59
+ !FACET_SCALE_RESOLUTIONS.includes(scales[channel])
60
+ ) ||
61
+ Object.keys(scales).some(channel => !FACET_SCALE_CHANNELS.includes(channel))
62
+ ) {
63
+ throw new Error(
64
+ "compositionSpec.facet.scales requires one shared or independent policy per supported channel."
65
+ );
66
+ }
67
+ const guides = compositionSpec.facet.guides;
68
+ if (
69
+ !isPlainObject(guides) ||
70
+ Object.keys(guides).some(key => !["axes", "legend"].includes(key)) ||
71
+ !["each", "outer"].includes(guides.axes) ||
72
+ ![false, "shared"].includes(guides.legend)
73
+ ) {
74
+ throw new Error(
75
+ 'compositionSpec.facet.guides requires axes "each" or "outer" and legend false or "shared".'
76
+ );
77
+ }
78
+ }
79
+
80
+ function validateCompositionChildren(compositionSpec, childIds, facet) {
81
+ if (
82
+ !Array.isArray(compositionSpec.children) ||
83
+ compositionSpec.children.length < (facet ? 1 : 2) ||
84
+ !compositionSpec.children.every(id => typeof id === "string" && id.length > 0)
85
+ ) {
86
+ throw new TypeError(
87
+ facet
88
+ ? "Facet compositionSpec.children requires at least one child ID."
89
+ : "compositionSpec.children requires at least two child IDs."
90
+ );
91
+ }
92
+ if (new Set(compositionSpec.children).size !== compositionSpec.children.length) {
93
+ throw new Error("compositionSpec.children must not contain duplicate IDs.");
94
+ }
95
+ if (
96
+ compositionSpec.children.length !== childIds.length ||
97
+ compositionSpec.children.some(id => !childIds.includes(id))
98
+ ) {
99
+ throw new Error("compositionSpec.children must match ChartProgram children exactly.");
100
+ }
101
+ }
102
+
103
+ function validateLayout(compositionSpec) {
104
+ if (!Number.isFinite(compositionSpec.gap) || compositionSpec.gap < 0) {
105
+ throw new RangeError("compositionSpec.gap must be a non-negative finite number.");
106
+ }
107
+ if (!["start", "center", "end"].includes(compositionSpec.align)) {
108
+ throw new Error("compositionSpec.align must be start, center, or end.");
109
+ }
110
+ if (!isPlainObject(compositionSpec.padding)) {
111
+ throw new TypeError("compositionSpec.padding must be a plain object.");
112
+ }
113
+ for (const side of ["top", "right", "bottom", "left"]) {
114
+ if (!Number.isFinite(compositionSpec.padding[side]) || compositionSpec.padding[side] < 0) {
115
+ throw new RangeError(
116
+ `compositionSpec.padding.${side} must be a non-negative finite number.`
117
+ );
118
+ }
119
+ }
120
+ const paddingKeys = Object.keys(compositionSpec.padding);
121
+ if (
122
+ paddingKeys.length !== 4 ||
123
+ paddingKeys.some(key => !["top", "right", "bottom", "left"].includes(key))
124
+ ) {
125
+ throw new Error("compositionSpec.padding must contain exactly four sides.");
126
+ }
127
+ }
128
+
129
+ export function ownCompositionSpec(compositionSpec, children) {
130
+ const childIds = Object.keys(children);
131
+ if (compositionSpec === undefined) {
132
+ if (childIds.length > 0) {
133
+ throw new Error("ChartProgram children require a compositionSpec.");
134
+ }
135
+ return undefined;
136
+ }
137
+ if (!isPlainObject(compositionSpec)) {
138
+ throw new TypeError("ChartProgram compositionSpec must be a plain object.");
139
+ }
140
+ const allowed = [
141
+ "id", "type", "direction", "children", "columns", "gap", "align",
142
+ "padding", "facet"
143
+ ];
144
+ const unknown = Object.keys(compositionSpec).find(key => !allowed.includes(key));
145
+ if (unknown !== undefined) {
146
+ throw new Error(`Unknown compositionSpec property "${unknown}".`);
147
+ }
148
+ if (typeof compositionSpec.id !== "string" || compositionSpec.id.length === 0) {
149
+ throw new TypeError("compositionSpec.id must be a non-empty string.");
150
+ }
151
+ const facet = compositionSpec.type === "facet";
152
+ if (compositionSpec.type !== undefined && !facet) {
153
+ throw new Error(`Unknown compositionSpec type "${compositionSpec.type}".`);
154
+ }
155
+ if (!facet && !["horizontal", "vertical"].includes(compositionSpec.direction)) {
156
+ throw new Error("compositionSpec.direction must be horizontal or vertical.");
157
+ }
158
+ validateCompositionChildren(compositionSpec, childIds, facet);
159
+ if (facet) {
160
+ validateFacetSpec(compositionSpec);
161
+ } else if (
162
+ compositionSpec.columns !== undefined ||
163
+ compositionSpec.facet !== undefined
164
+ ) {
165
+ throw new Error("Concat compositionSpec does not accept facet properties.");
166
+ }
167
+ validateLayout(compositionSpec);
168
+ return ownProgramState(compositionSpec);
169
+ }
@@ -0,0 +1,65 @@
1
+ import {
2
+ cloneAndFreeze,
3
+ freezeOwned,
4
+ isPlainObject,
5
+ removeOwnedPath
6
+ } from "./immutable.js";
7
+
8
+ function validatePath(path) {
9
+ if (
10
+ !Array.isArray(path) ||
11
+ path.length === 0 ||
12
+ !path.every(key => typeof key === "string" && key.length > 0)
13
+ ) {
14
+ throw new TypeError("Materialization config path must contain names.");
15
+ }
16
+ }
17
+
18
+ function validateConfig(config) {
19
+ if (!isPlainObject(config)) {
20
+ throw new TypeError("Materialization config must be a plain object.");
21
+ }
22
+ }
23
+
24
+ function updateConfigPath(value, path, config) {
25
+ if (path.length === 0) return cloneAndFreeze(config);
26
+ const [key, ...rest] = path;
27
+ return freezeOwned({
28
+ ...value,
29
+ [key]: updateConfigPath(value?.[key] ?? {}, rest, config)
30
+ });
31
+ }
32
+
33
+ export function createMaterializationConfigs(
34
+ markConfigs,
35
+ guideConfigs,
36
+ titleConfig,
37
+ canvasConfig
38
+ ) {
39
+ return cloneAndFreeze({
40
+ marks: markConfigs,
41
+ guides: guideConfigs,
42
+ ...(canvasConfig === undefined ? {} : { canvas: canvasConfig }),
43
+ ...(titleConfig === undefined ? {} : { title: titleConfig })
44
+ });
45
+ }
46
+
47
+ export function setMaterializationConfig(configs, path, config) {
48
+ validatePath(path);
49
+ validateConfig(config);
50
+ return updateConfigPath(configs, path, config);
51
+ }
52
+
53
+ export function removeMaterializationConfig(configs, path) {
54
+ validatePath(path);
55
+ const removed = removeOwnedPath(configs, path);
56
+ if (!removed.removed) return { removed: false, value: configs };
57
+ return {
58
+ removed: true,
59
+ value: freezeOwned({
60
+ ...removed.value,
61
+ marks: removed.value.marks ?? freezeOwned({}),
62
+ guides: removed.value.guides ?? freezeOwned({})
63
+ })
64
+ };
65
+ }
@@ -0,0 +1,28 @@
1
+ import {
2
+ cloneAndFreeze,
3
+ freezeOwned,
4
+ isOwned,
5
+ isPlainObject
6
+ } from "./immutable.js";
7
+
8
+ export function ownProgramState(value) {
9
+ return isOwned(value) ? value : cloneAndFreeze(value);
10
+ }
11
+
12
+ export function ownChildPrograms(children, ProgramClass) {
13
+ if (!isPlainObject(children)) {
14
+ throw new TypeError("ChartProgram children must be a plain object.");
15
+ }
16
+ if (isOwned(children)) return children;
17
+ const owned = {};
18
+ for (const [id, program] of Object.entries(children)) {
19
+ if (typeof id !== "string" || id.length === 0) {
20
+ throw new TypeError("ChartProgram child IDs must be non-empty strings.");
21
+ }
22
+ if (!(program instanceof ProgramClass)) {
23
+ throw new TypeError(`ChartProgram child "${id}" must be a ChartProgram.`);
24
+ }
25
+ owned[id] = program;
26
+ }
27
+ return freezeOwned(owned);
28
+ }