ggaction 0.0.9 → 0.0.11

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 (222) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/README.md +5 -4
  3. package/knowledge/action-card.schema.json +1 -0
  4. package/knowledge/action-cards.json +1 -10941
  5. package/knowledge/action-cards.schema.json +1 -0
  6. package/knowledge/extension-authoring.md +248 -0
  7. package/knowledge/intent-taxonomy.json +1 -183
  8. package/knowledge/intent-taxonomy.schema.json +1 -0
  9. package/knowledge/mcp-resources.json +1 -95
  10. package/knowledge/mcp-resources.schema.json +1 -0
  11. package/knowledge/task-packet.schema.json +1 -159
  12. package/knowledge/task-resolver.js +1 -1230
  13. package/package.json +19 -2
  14. package/src/actions/boxPlots/edit.js +45 -298
  15. package/src/actions/boxPlots/options.js +46 -77
  16. package/src/actions/boxPlots/resolve.js +1 -1
  17. package/src/actions/charts/bar.js +3 -0
  18. package/src/actions/charts/heatmap.js +73 -83
  19. package/src/actions/charts/index.js +1 -1
  20. package/src/actions/charts/parallel.js +35 -1
  21. package/src/actions/charts/shared.js +20 -0
  22. package/src/actions/composition/actions.js +46 -49
  23. package/src/actions/data/bin2d.js +105 -2
  24. package/src/actions/data/box.js +17 -17
  25. package/src/actions/data/density.js +42 -78
  26. package/src/actions/data/gradientProfile.js +22 -52
  27. package/src/actions/data/horizon.js +16 -41
  28. package/src/actions/data/index.js +10 -10
  29. package/src/actions/data/interval.js +16 -38
  30. package/src/actions/data/intervalEdit.js +55 -0
  31. package/src/actions/data/regression.js +7 -17
  32. package/src/actions/data/shared.js +47 -0
  33. package/src/actions/data/timeUnit.js +14 -38
  34. package/src/actions/data/window.js +14 -38
  35. package/src/actions/distributions/revision.js +154 -34
  36. package/src/actions/encodings/angle.js +9 -10
  37. package/src/actions/encodings/appearance.js +17 -51
  38. package/src/actions/encodings/density/resolve.js +3 -1
  39. package/src/actions/encodings/density.js +1 -15
  40. package/src/actions/encodings/horizon/resolve.js +13 -3
  41. package/src/actions/encodings/horizon.js +8 -7
  42. package/src/actions/encodings/index.js +9 -9
  43. package/src/actions/encodings/offset.js +8 -24
  44. package/src/actions/encodings/parallel.js +9 -10
  45. package/src/actions/encodings/pathOrder.js +6 -13
  46. package/src/actions/encodings/position/policies/index.js +4 -4
  47. package/src/actions/encodings/position/policies/line.js +47 -17
  48. package/src/actions/encodings/position/resolve.js +24 -5
  49. package/src/actions/encodings/ranged.js +18 -23
  50. package/src/actions/encodings/remove.js +15 -25
  51. package/src/actions/encodings/ruleAppearance.js +9 -24
  52. package/src/actions/encodings/shared.js +37 -0
  53. package/src/actions/encodings/strokeDash.js +8 -23
  54. package/src/actions/encodings/text.js +9 -10
  55. package/src/actions/errorBands/create.js +105 -39
  56. package/src/actions/errorBands/edit.js +89 -93
  57. package/src/actions/errorBands/index.js +1 -2
  58. package/src/actions/errorBars/create.js +78 -27
  59. package/src/actions/errorBars/edit.js +81 -48
  60. package/src/actions/errorBars/index.js +1 -2
  61. package/src/actions/facets/actions.js +45 -69
  62. package/src/actions/facets/derive.js +38 -53
  63. package/src/actions/facets/replay.js +0 -1
  64. package/src/actions/gradientPlots/components.js +59 -48
  65. package/src/actions/gradientPlots/edit.js +35 -216
  66. package/src/actions/gradientPlots/index.js +11 -10
  67. package/src/actions/gradientPlots/materialize.js +21 -35
  68. package/src/actions/gradientPlots/options.js +5 -7
  69. package/src/actions/gradientPlots/paint.js +9 -6
  70. package/src/actions/gradientPlots/resolve.js +1 -1
  71. package/src/actions/guides/axes/index.js +3 -3
  72. package/src/actions/guides/axes/labels.js +68 -26
  73. package/src/actions/guides/axes/lines.js +22 -2
  74. package/src/actions/guides/axes/policy.js +28 -8
  75. package/src/actions/guides/axes/ticks.js +27 -9
  76. package/src/actions/guides/axes/titles.js +113 -65
  77. package/src/actions/guides/grids/resolve.js +14 -0
  78. package/src/actions/guides/guides.js +2 -4
  79. package/src/actions/guides/index.js +2 -2
  80. package/src/actions/guides/legends/categorical/actions.js +44 -68
  81. package/src/actions/guides/legends/categorical/components.js +6 -1
  82. package/src/actions/guides/legends/categorical/index.js +2 -2
  83. package/src/actions/guides/legends/categorical/layout.js +148 -175
  84. package/src/actions/guides/legends/categorical/recipes.js +5 -0
  85. package/src/actions/guides/legends/categorical/resolve.js +8 -1
  86. package/src/actions/guides/legends/continuous/common.js +117 -66
  87. package/src/actions/guides/legends/continuous/gradient.js +38 -37
  88. package/src/actions/guides/legends/continuous/interval.js +97 -76
  89. package/src/actions/guides/legends/continuous/opacity.js +28 -38
  90. package/src/actions/guides/legends/edit.js +14 -1
  91. package/src/actions/guides/legends/index.js +1 -1
  92. package/src/actions/guides/legends/lane.js +2 -1
  93. package/src/actions/guides/legends/remove.js +8 -0
  94. package/src/actions/guides/legends/size.js +36 -43
  95. package/src/actions/guides/legends/strokeWidth.js +20 -26
  96. package/src/actions/guides/polar/axes/labels.js +17 -1
  97. package/src/actions/guides/polar/axes/shared.js +7 -1
  98. package/src/actions/guides/polar/axes/titles.js +19 -2
  99. package/src/actions/guides/polar/grids.js +4 -0
  100. package/src/actions/guides/polar/resolve.js +6 -0
  101. package/src/actions/index.js +13 -13
  102. package/src/actions/intervals/resolve.js +87 -117
  103. package/src/actions/marks/arc/actions.js +31 -36
  104. package/src/actions/marks/area/actions.js +23 -31
  105. package/src/actions/marks/index.js +6 -6
  106. package/src/actions/marks/line/actions.js +33 -52
  107. package/src/actions/marks/line/materialize.js +7 -5
  108. package/src/actions/marks/point/materialize.js +5 -3
  109. package/src/actions/marks/remove.js +21 -0
  110. package/src/actions/marks/rule/actions.js +53 -53
  111. package/src/actions/marks/shared.js +8 -4
  112. package/src/actions/marks/text/actions.js +14 -6
  113. package/src/actions/marks/text/layout.js +21 -45
  114. package/src/actions/marks/tick/actions.js +22 -21
  115. package/src/actions/primitives/createGraphics.js +4 -0
  116. package/src/actions/primitives/editGraphics.js +4 -0
  117. package/src/actions/primitives/index.js +3 -3
  118. package/src/actions/primitives/semantic.js +1 -1
  119. package/src/actions/primitives/semanticValidation/index.js +2 -2
  120. package/src/actions/primitives/semanticValidation/layer.js +1 -1
  121. package/src/actions/regression/edit.js +3 -43
  122. package/src/actions/scales/consumers/families.js +24 -4
  123. package/src/actions/scales/consumers/index.js +1 -1
  124. package/src/actions/scales/consumers/seriesLayout.js +17 -0
  125. package/src/actions/scales/definitions.js +100 -103
  126. package/src/actions/scales/materialize.js +5 -0
  127. package/src/actions/selection/actions.js +79 -121
  128. package/src/actions/titles/actions.js +48 -94
  129. package/src/actions/titles/resolve.js +14 -27
  130. package/src/actions/violinPlots/create.js +37 -44
  131. package/src/core/action.js +20 -5
  132. package/src/core/extensionRegistry.js +104 -0
  133. package/src/core/textMetrics.js +74 -37
  134. package/src/core/validation.js +18 -0
  135. package/src/extension.js +8 -1
  136. package/src/grammar/aggregate.js +48 -28
  137. package/src/grammar/arcs.js +58 -11
  138. package/src/grammar/areaSeries.js +14 -16
  139. package/src/grammar/bin2d.js +37 -23
  140. package/src/grammar/boxPlot.js +14 -13
  141. package/src/grammar/categoricalDensity.js +14 -14
  142. package/src/grammar/categoryOrder.js +8 -14
  143. package/src/grammar/curveCommands.js +87 -20
  144. package/src/grammar/density.js +162 -141
  145. package/src/grammar/facets/index.js +14 -0
  146. package/src/grammar/facets/scales.js +7 -4
  147. package/src/grammar/gradientProfile.js +12 -15
  148. package/src/grammar/histogram.js +145 -83
  149. package/src/grammar/horizon.js +60 -23
  150. package/src/grammar/interval.js +14 -13
  151. package/src/grammar/lineSeries.js +91 -61
  152. package/src/grammar/numeric.js +290 -0
  153. package/src/grammar/paint.js +3 -4
  154. package/src/grammar/palettes.js +21 -32
  155. package/src/grammar/pathCommands.js +6 -0
  156. package/src/grammar/polarPaths.js +5 -5
  157. package/src/grammar/regression/derive.js +48 -9
  158. package/src/grammar/regression/models.js +248 -69
  159. package/src/grammar/regression/parameters.js +8 -0
  160. package/src/grammar/scales/color.js +35 -26
  161. package/src/grammar/scales/continuous.js +71 -24
  162. package/src/grammar/scales/definition.js +1 -1
  163. package/src/grammar/scales/discretized.js +42 -12
  164. package/src/grammar/scales/fields.js +21 -4
  165. package/src/grammar/scales/ordinal.js +117 -61
  166. package/src/grammar/scales/temporal.js +40 -11
  167. package/src/grammar/scales/transformed.js +199 -52
  168. package/src/grammar/schemas/graphicBounds.js +270 -79
  169. package/src/grammar/seriesLayout.js +50 -18
  170. package/src/grammar/ticks.js +123 -53
  171. package/src/grammar/timeUnit.js +18 -8
  172. package/src/grammar/transforms.js +4 -1
  173. package/src/grammar/window.js +70 -57
  174. package/src/index.js +1 -1
  175. package/src/layout/canvas.js +4 -8
  176. package/src/layout/composition.js +39 -40
  177. package/src/layout/facets.js +10 -14
  178. package/src/layout/labels.js +91 -15
  179. package/src/layout/legend.js +22 -20
  180. package/src/layout/legendLane.js +54 -40
  181. package/src/layout/title.js +7 -7
  182. package/src/materialization/bars/aggregate.js +14 -40
  183. package/src/materialization/bars/grouped.js +7 -18
  184. package/src/materialization/bars/histogram.js +2 -19
  185. package/src/materialization/bars/ranged.js +20 -15
  186. package/src/materialization/bars/resolve.js +21 -0
  187. package/src/materialization/facetGuides/legacyCategorical.js +2 -1
  188. package/src/materialization/facetGuides/placement.js +34 -19
  189. package/src/materialization/facetGuides/preparation.js +9 -5
  190. package/src/materialization/facets.js +39 -27
  191. package/src/materialization/guides/resources.js +2 -1
  192. package/src/materialization/marks/capabilities.js +5 -2
  193. package/src/materialization/marks/index.js +24 -4
  194. package/src/materialization/scales/policies/bar.js +61 -59
  195. package/src/materialization/scales/policies/binnedPosition.js +34 -0
  196. package/src/materialization/scales/policies/series.js +13 -7
  197. package/src/materialization/scales/resolve.js +4 -5
  198. package/src/materialization/selection/items/arc.js +16 -2
  199. package/src/materialization/selection/items/bar.js +4 -2
  200. package/src/materialization/selection/items/common.js +21 -1
  201. package/src/materialization/selection/items/path.js +19 -2
  202. package/src/materialization/selection/items/point.js +4 -16
  203. package/src/materialization/selection/items/tick.js +13 -21
  204. package/src/materialization/selection/point.js +11 -4
  205. package/src/materialization/selection/styles.js +19 -27
  206. package/src/materialization/text.js +4 -1
  207. package/src/mcp/adapter.js +8 -3
  208. package/src/renderers/canvas/fill.js +2 -2
  209. package/src/renderers/canvas/index.js +35 -2
  210. package/src/renderers/canvas/native.js +160 -0
  211. package/src/renderers/pdf.js +14 -0
  212. package/src/renderers/svg.js +37 -4
  213. package/types/extension.d.ts +19 -0
  214. package/types/index.d.ts +3 -0
  215. package/types/program.d.ts +509 -131
  216. package/types/svg.d.ts +1 -0
  217. package/src/actions/errorBands/components.js +0 -81
  218. package/src/actions/errorBands/options.js +0 -39
  219. package/src/actions/errorBands/resolve.js +0 -28
  220. package/src/actions/errorBars/components.js +0 -62
  221. package/src/actions/errorBars/options.js +0 -58
  222. package/src/actions/errorBars/resolve.js +0 -25
@@ -21,9 +21,9 @@ function assertTitleScope(program, operation) {
21
21
  }
22
22
 
23
23
  function hasRotation(graphic) {
24
- const properties = graphic.items?.map(child => child.properties) ??
25
- [graphic.properties];
26
- return properties.some(item => Object.hasOwn(item, "rotation"));
24
+ return (graphic.items ?? [graphic]).some(
25
+ item => Object.hasOwn(item.properties, "rotation")
26
+ );
27
27
  }
28
28
 
29
29
  function ensureTextShape(program, id, component) {
@@ -45,14 +45,7 @@ function ensureTextShape(program, id, component) {
45
45
  ...placement
46
46
  });
47
47
  }
48
- if (!collection && needsCollection) {
49
- return program.editGraphics({
50
- target: id,
51
- property: "length",
52
- value: component.lines.length
53
- });
54
- }
55
- if (collection && graphic.items.length !== component.lines.length) {
48
+ if (needsCollection && graphic.items?.length !== component.lines.length) {
56
49
  return program.editGraphics({
57
50
  target: id,
58
51
  property: "length",
@@ -68,20 +61,20 @@ function distributed(value, count) {
68
61
 
69
62
  function editTextGraphic(program, id, component, style) {
70
63
  const count = component.lines.length;
71
- let next = ensureTextShape(program, id, component)
72
- .editGraphics({ target: id, property: "x", value: distributed(component.x, count) })
73
- .editGraphics({ target: id, property: "y", value: distributed(component.y, count) })
74
- .editGraphics({
75
- target: id,
76
- property: "text",
77
- value: distributed(component.lines, count)
78
- })
79
- .editGraphics({ target: id, property: "fill", value: style.color })
80
- .editGraphics({ target: id, property: "fontSize", value: style.fontSize })
81
- .editGraphics({ target: id, property: "fontFamily", value: style.fontFamily })
82
- .editGraphics({ target: id, property: "fontWeight", value: style.fontWeight })
83
- .editGraphics({ target: id, property: "textAlign", value: component.textAlign })
84
- .editGraphics({ target: id, property: "textBaseline", value: "middle" });
64
+ let next = ensureTextShape(program, id, component);
65
+ for (const [property, value] of Object.entries({
66
+ x: distributed(component.x, count),
67
+ y: distributed(component.y, count),
68
+ text: distributed(component.lines, count),
69
+ fill: style.color,
70
+ fontSize: style.fontSize,
71
+ fontFamily: style.fontFamily,
72
+ fontWeight: style.fontWeight,
73
+ textAlign: component.textAlign,
74
+ textBaseline: "middle"
75
+ })) {
76
+ next = next.editGraphics({ target: id, property, value });
77
+ }
85
78
  if (component.explicitRotation) {
86
79
  next = next.editGraphics({
87
80
  target: id,
@@ -92,78 +85,39 @@ function editTextGraphic(program, id, component, style) {
92
85
  return next;
93
86
  }
94
87
 
95
- function createTextGraphic(program, id, component) {
96
- return program.createGraphics({
97
- id,
98
- type: "text",
99
- ...(component.lines.length > 1 ? { length: component.lines.length } : {}),
100
- ...resolveCanvasGraphicPlacement(program)
88
+ function titleTextAction(kind, create) {
89
+ const suffix = `${kind[0].toUpperCase()}${kind.slice(1)}Text`;
90
+ const op = `${create ? "create" : "edit"}${suffix}`;
91
+ const id = `chart${kind[0].toUpperCase()}${kind.slice(1)}`;
92
+ return action({
93
+ op,
94
+ description: `${create ? "Create" : "Rematerialize"} chart ${kind} text.`
95
+ }, function (args = {}) {
96
+ noOptions(args, op);
97
+ const config = requireTitleConfig(this);
98
+ if (create && this.graphicSpec.objects[id] !== undefined) {
99
+ throw new Error(`${op} requires a missing chart ${kind} graphic.`);
100
+ }
101
+ const component = resolveTitleLayout(this, config)[kind];
102
+ if (component === undefined) {
103
+ throw new Error(`${op} requires semantic subtitle text.`);
104
+ }
105
+ if (!create) {
106
+ return editTextGraphic(this, id, component, config[`${kind}Style`]);
107
+ }
108
+ return this.createGraphics({
109
+ id,
110
+ type: "text",
111
+ ...(component.lines.length > 1 ? { length: component.lines.length } : {}),
112
+ ...resolveCanvasGraphicPlacement(this)
113
+ })[`edit${suffix}`]();
101
114
  });
102
115
  }
103
116
 
104
- export const editTitleText = action(
105
- { op: "editTitleText", description: "Rematerialize chart title text." },
106
- function (args = {}) {
107
- noOptions(args, "editTitleText");
108
- const config = requireTitleConfig(this);
109
- const layout = resolveTitleLayout(this, config);
110
- return editTextGraphic(
111
- this,
112
- "chartTitle",
113
- layout.title,
114
- config.titleStyle
115
- );
116
- }
117
- );
118
-
119
- export const createTitleText = action(
120
- { op: "createTitleText", description: "Create chart title text." },
121
- function (args = {}) {
122
- noOptions(args, "createTitleText");
123
- const config = requireTitleConfig(this);
124
- if (this.graphicSpec.objects.chartTitle !== undefined) {
125
- throw new Error("createTitleText requires a missing chart title graphic.");
126
- }
127
- const layout = resolveTitleLayout(this, config);
128
- return createTextGraphic(this, "chartTitle", layout.title)
129
- .editTitleText();
130
- }
131
- );
132
-
133
- export const editSubtitleText = action(
134
- { op: "editSubtitleText", description: "Rematerialize chart subtitle text." },
135
- function (args = {}) {
136
- noOptions(args, "editSubtitleText");
137
- const config = requireTitleConfig(this);
138
- const layout = resolveTitleLayout(this, config);
139
- if (layout.subtitle === undefined) {
140
- throw new Error("editSubtitleText requires semantic subtitle text.");
141
- }
142
- return editTextGraphic(
143
- this,
144
- "chartSubtitle",
145
- layout.subtitle,
146
- config.subtitleStyle
147
- );
148
- }
149
- );
150
-
151
- export const createSubtitleText = action(
152
- { op: "createSubtitleText", description: "Create chart subtitle text." },
153
- function (args = {}) {
154
- noOptions(args, "createSubtitleText");
155
- const config = requireTitleConfig(this);
156
- if (this.graphicSpec.objects.chartSubtitle !== undefined) {
157
- throw new Error("createSubtitleText requires a missing chart subtitle graphic.");
158
- }
159
- const layout = resolveTitleLayout(this, config);
160
- if (layout.subtitle === undefined) {
161
- throw new Error("createSubtitleText requires semantic subtitle text.");
162
- }
163
- return createTextGraphic(this, "chartSubtitle", layout.subtitle)
164
- .editSubtitleText();
165
- }
166
- );
117
+ export const editTitleText = titleTextAction("title", false);
118
+ export const createTitleText = titleTextAction("title", true);
119
+ export const editSubtitleText = titleTextAction("subtitle", false);
120
+ export const createSubtitleText = titleTextAction("subtitle", true);
167
121
 
168
122
  export const rematerializeTitle = action(
169
123
  { op: "rematerializeTitle", description: "Rematerialize chart title graphics." },
@@ -252,29 +252,20 @@ export function resolveTitleLayout(program, config) {
252
252
  )
253
253
  }, config);
254
254
  const horizontal = ["top", "bottom"].includes(config.position);
255
- let title;
256
- let subtitle;
255
+ let component;
257
256
  if (horizontal) {
258
257
  const x = alignedTextAnchor(plot.x, plot.width, config.align);
259
258
  const blockTop = config.position === "top"
260
259
  ? 16 + config.offset
261
260
  : plot.y + plot.height + config.offset;
262
- title = {
263
- lines: block.titleLines,
261
+ component = (lines, centers) => ({
262
+ lines,
264
263
  x,
265
- y: block.titleCenters.map(value => blockTop + value),
264
+ y: centers.map(value => blockTop + value),
266
265
  textAlign: config.align,
267
266
  rotation: 0,
268
- explicitRotation: config.position !== "top" || block.titleLines.length > 1
269
- };
270
- subtitle = block.subtitleLines.length === 0 ? undefined : {
271
- lines: block.subtitleLines,
272
- x,
273
- y: block.subtitleCenters.map(value => blockTop + value),
274
- textAlign: config.align,
275
- rotation: 0,
276
- explicitRotation: config.position !== "top" || block.subtitleLines.length > 1
277
- };
267
+ explicitRotation: config.position !== "top" || lines.length > 1
268
+ });
278
269
  } else {
279
270
  const rotation = config.position === "left" ? -Math.PI / 2 : Math.PI / 2;
280
271
  const y = alignedTitleAnchor(plot.y, plot.height, block.width, config.align);
@@ -282,23 +273,19 @@ export function resolveTitleLayout(program, config) {
282
273
  ? 16 + config.offset
283
274
  : canvas.properties.width - 16 + config.offset;
284
275
  const mapX = value => config.position === "left" ? edge + value : edge - value;
285
- title = {
286
- lines: block.titleLines,
287
- x: block.titleCenters.map(mapX),
288
- y,
289
- textAlign: "center",
290
- rotation,
291
- explicitRotation: true
292
- };
293
- subtitle = block.subtitleLines.length === 0 ? undefined : {
294
- lines: block.subtitleLines,
295
- x: block.subtitleCenters.map(mapX),
276
+ component = (lines, centers) => ({
277
+ lines,
278
+ x: centers.map(mapX),
296
279
  y,
297
280
  textAlign: "center",
298
281
  rotation,
299
282
  explicitRotation: true
300
- };
283
+ });
301
284
  }
285
+ const title = component(block.titleLines, block.titleCenters);
286
+ const subtitle = block.subtitleLines.length === 0
287
+ ? undefined
288
+ : component(block.subtitleLines, block.subtitleCenters);
302
289
  const titleBounds = unionTitleBounds([
303
290
  resolveTitleComponentBounds(title, config.titleStyle),
304
291
  ...(subtitle === undefined
@@ -27,6 +27,8 @@ const AREA_OPTIONS = Object.freeze([
27
27
  "fill", "opacity", "stroke", "strokeWidth", "curve"
28
28
  ]);
29
29
  const CATEGORICAL_TYPES = Object.freeze(["nominal", "ordinal"]);
30
+ const OPERATION = "createViolinPlot";
31
+ const ROLE_ERROR = `${OPERATION} requires one categorical axis and one quantitative axis.`;
30
32
 
31
33
  function inferFieldType(dataset, encoding, label) {
32
34
  if (encoding.fieldType !== undefined) return encoding.fieldType;
@@ -51,29 +53,6 @@ function normalizePosition(dataset, value, label) {
51
53
  };
52
54
  }
53
55
 
54
- function normalizeDensity(value) {
55
- if (value === undefined) return { options: {}, width: undefined };
56
- validateOptionObject(value, DENSITY_OPTIONS, "createViolinPlot density");
57
- if (value.width !== undefined) {
58
- validateOptionObject(
59
- value.width,
60
- WIDTH_OPTIONS,
61
- "createViolinPlot density.width"
62
- );
63
- }
64
- const { width, ...options } = value;
65
- return { options, width };
66
- }
67
-
68
- function normalizeSplit(value) {
69
- if (value === undefined) return undefined;
70
- validateOptionObject(value, SPLIT_OPTIONS, "createViolinPlot split");
71
- if (typeof value.field !== "string" || value.field.length === 0) {
72
- throw new TypeError("createViolinPlot split.field must be a non-empty string.");
73
- }
74
- return { ...value };
75
- }
76
-
77
56
  function axisGuideOptions(value, field) {
78
57
  if (value === false) return false;
79
58
  return {
@@ -109,55 +88,69 @@ export const createViolinPlot = action(
109
88
  description: "Create a categorical kernel-density violin plot."
110
89
  },
111
90
  function (args = {}) {
112
- validateFacadeOptions(args, OPTIONS, "createViolinPlot");
91
+ validateFacadeOptions(args, OPTIONS, OPERATION);
113
92
  const id = resolveFacadeId(this, args.id, {
114
93
  defaultId: "violinPlot",
115
- operation: "createViolinPlot"
94
+ operation: OPERATION
116
95
  });
117
- const data = resolveFacadeData(this, args.data, "createViolinPlot");
96
+ const data = resolveFacadeData(this, args.data, OPERATION);
118
97
  const dataset = findDataset(this, data);
119
- const x = normalizePosition(dataset, args.x, "createViolinPlot x");
120
- const y = normalizePosition(dataset, args.y, "createViolinPlot y");
98
+ const x = normalizePosition(dataset, args.x, `${OPERATION} x`);
99
+ const y = normalizePosition(dataset, args.y, `${OPERATION} y`);
121
100
  const xCategorical = CATEGORICAL_TYPES.includes(x.fieldType);
122
101
  const yCategorical = CATEGORICAL_TYPES.includes(y.fieldType);
123
102
  if (xCategorical === yCategorical || (
124
103
  x.fieldType !== "quantitative" && y.fieldType !== "quantitative"
125
104
  )) {
126
- throw new Error(
127
- "createViolinPlot requires one categorical axis and one quantitative axis."
128
- );
105
+ throw new Error(ROLE_ERROR);
129
106
  }
130
107
  const category = xCategorical ? x : y;
131
108
  const value = xCategorical ? y : x;
132
109
  if (value.fieldType !== "quantitative") {
133
- throw new Error(
134
- "createViolinPlot requires one categorical axis and one quantitative axis."
135
- );
110
+ throw new Error(ROLE_ERROR);
111
+ }
112
+ let split = args.split;
113
+ if (split !== undefined) {
114
+ validateOptionObject(split, SPLIT_OPTIONS, `${OPERATION} split`);
115
+ if (typeof split.field !== "string" || split.field.length === 0) {
116
+ throw new TypeError(`${OPERATION} split.field must be a non-empty string.`);
117
+ }
118
+ split = { ...split };
136
119
  }
137
- const split = normalizeSplit(args.split);
138
120
  if (split?.field === category.field) {
139
- throw new Error("createViolinPlot split field must differ from its category field.");
121
+ throw new Error(`${OPERATION} split field must differ from its category field.`);
140
122
  }
141
- const color = normalizeEncoding(args.color, "createViolinPlot color");
123
+ const color = normalizeEncoding(args.color, `${OPERATION} color`);
142
124
  if (
143
125
  color !== undefined &&
144
126
  ![category.field, split?.field].includes(color.field)
145
127
  ) {
146
128
  throw new Error(
147
- "createViolinPlot color must encode its category or split field."
129
+ `${OPERATION} color must encode its category or split field.`
148
130
  );
149
131
  }
150
- const density = normalizeDensity(args.density);
132
+ const density = args.density === undefined ? {} : args.density;
133
+ if (args.density !== undefined) {
134
+ validateOptionObject(density, DENSITY_OPTIONS, `${OPERATION} density`);
135
+ if (density.width !== undefined) {
136
+ validateOptionObject(
137
+ density.width,
138
+ WIDTH_OPTIONS,
139
+ `${OPERATION} density.width`
140
+ );
141
+ }
142
+ }
143
+ const { width: densityWidth, ...densityOptions } = density;
151
144
  const area = normalizeAppearance(
152
145
  args.area,
153
146
  AREA_OPTIONS,
154
- "createViolinPlot area"
147
+ `${OPERATION} area`
155
148
  );
156
149
  if (color !== undefined && Object.hasOwn(area, "fill")) {
157
- throw new Error("createViolinPlot area.fill cannot be combined with color.");
150
+ throw new Error(`${OPERATION} area.fill cannot be combined with color.`);
158
151
  }
159
152
  const guides = guideOptions(
160
- normalizeGuides(args.guides, "createViolinPlot"),
153
+ normalizeGuides(args.guides, OPERATION),
161
154
  x,
162
155
  y,
163
156
  category,
@@ -184,10 +177,10 @@ export const createViolinPlot = action(
184
177
  densityChannel: xCategorical ? "x" : "y",
185
178
  ...(args.coordinate === undefined ? {} : { coordinate: args.coordinate }),
186
179
  ...(value.scale === undefined ? {} : { valueScale: value.scale }),
187
- ...density.options,
180
+ ...densityOptions,
188
181
  placement: {
189
182
  type: "category",
190
- ...(density.width === undefined ? {} : { width: density.width }),
183
+ ...(densityWidth === undefined ? {} : { width: densityWidth }),
191
184
  ...(split === undefined ? {} : { split }),
192
185
  ...(category.scale === undefined ? {} : { scale: category.scale })
193
186
  }
@@ -1,5 +1,11 @@
1
1
  import { cloneAndFreeze, freezeOwned, isPlainObject } from "./immutable.js";
2
2
 
3
+ const metadataByWrappedAction = new WeakMap();
4
+
5
+ export function getWrappedActionMetadata(value) {
6
+ return metadataByWrappedAction.get(value);
7
+ }
8
+
3
9
  function summarizeObject(value, ancestors = new WeakSet()) {
4
10
  if (ancestors.has(value)) {
5
11
  throw new TypeError("Action arguments must not contain circular references.");
@@ -107,24 +113,33 @@ export function action(metadata, implementation) {
107
113
  throw new Error(`Unknown action scope "${scope}".`);
108
114
  }
109
115
 
110
- return function wrappedAction(args = {}) {
116
+ const ownedMetadata = freezeOwned({
117
+ op: metadata.op,
118
+ description: metadata.description,
119
+ scope
120
+ });
121
+
122
+ const wrappedAction = function wrappedAction(args = {}) {
111
123
  if (!isPlainObject(args)) {
112
124
  throw new TypeError("Action arguments must be a plain object.");
113
125
  }
114
126
 
115
- if (scope === "unit") this._assertUnitProgram(metadata.op);
116
- if (scope === "composition") this._assertCompositionProgram(metadata.op);
127
+ if (scope === "unit") this._assertUnitProgram(ownedMetadata.op);
128
+ if (scope === "composition") this._assertCompositionProgram(ownedMetadata.op);
117
129
 
118
130
  const entered = this._enterAction({
119
- ...metadata,
131
+ ...ownedMetadata,
120
132
  args: summarizeArgs(args)
121
133
  });
122
134
  const result = implementation.call(entered, args);
123
135
 
124
136
  if (!(result instanceof this.constructor)) {
125
- throw new TypeError(`${metadata.op} must return a ChartProgram.`);
137
+ throw new TypeError(`${ownedMetadata.op} must return a ChartProgram.`);
126
138
  }
127
139
 
128
140
  return result._exitAction();
129
141
  };
142
+
143
+ metadataByWrappedAction.set(wrappedAction, ownedMetadata);
144
+ return wrappedAction;
130
145
  }
@@ -0,0 +1,104 @@
1
+ import { getWrappedActionMetadata } from "./action.js";
2
+ import { isPlainObject } from "./immutable.js";
3
+
4
+ const ACTION_NAME_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
5
+ const DEFINITION_KEYS = new Set(["name", "actions"]);
6
+ const registeredExtensionNames = new Set();
7
+
8
+ function readDataProperty(object, key, owner) {
9
+ const descriptor = Object.getOwnPropertyDescriptor(object, key);
10
+ if (descriptor === undefined) {
11
+ throw new TypeError(`${owner} requires a ${key} property.`);
12
+ }
13
+ if (!("value" in descriptor)) {
14
+ throw new TypeError(`${owner} ${key} must be a data property.`);
15
+ }
16
+ return descriptor.value;
17
+ }
18
+
19
+ function validateDefinitionShape(definition) {
20
+ if (!isPlainObject(definition)) {
21
+ throw new TypeError("Extension definition must be a plain object.");
22
+ }
23
+
24
+ for (const key of Reflect.ownKeys(definition)) {
25
+ if (typeof key !== "string" || !DEFINITION_KEYS.has(key)) {
26
+ throw new TypeError(`Unknown extension definition property "${String(key)}".`);
27
+ }
28
+ }
29
+
30
+ const name = readDataProperty(definition, "name", "Extension definition");
31
+ const actions = readDataProperty(
32
+ definition,
33
+ "actions",
34
+ "Extension definition"
35
+ );
36
+
37
+ if (
38
+ typeof name !== "string" ||
39
+ name.length === 0 ||
40
+ name !== name.trim()
41
+ ) {
42
+ throw new TypeError("Extension name must be a non-empty trimmed string.");
43
+ }
44
+ if (!isPlainObject(actions)) {
45
+ throw new TypeError("Extension actions must be a plain object.");
46
+ }
47
+
48
+ return { name, actions };
49
+ }
50
+
51
+ export function registerProgramExtension(ProgramClass, definition) {
52
+ const { name, actions } = validateDefinitionShape(definition);
53
+
54
+ if (registeredExtensionNames.has(name)) {
55
+ throw new Error(`Extension "${name}" is already registered.`);
56
+ }
57
+ if (!Object.isExtensible(ProgramClass.prototype)) {
58
+ throw new Error("ChartProgram prototype does not accept extension actions.");
59
+ }
60
+
61
+ const program = new ProgramClass();
62
+ const descriptors = Object.create(null);
63
+ const actionNames = Reflect.ownKeys(actions);
64
+ if (actionNames.length === 0) {
65
+ throw new TypeError("Extension actions must contain at least one action.");
66
+ }
67
+
68
+ for (const actionName of actionNames) {
69
+ if (
70
+ typeof actionName !== "string" ||
71
+ !ACTION_NAME_PATTERN.test(actionName)
72
+ ) {
73
+ throw new TypeError(
74
+ `Extension action name "${String(actionName)}" must be a JavaScript identifier.`
75
+ );
76
+ }
77
+
78
+ const action = readDataProperty(actions, actionName, "Extension actions");
79
+ const metadata = getWrappedActionMetadata(action);
80
+ if (metadata === undefined) {
81
+ throw new TypeError(
82
+ `Extension action "${actionName}" must be created with action().`
83
+ );
84
+ }
85
+ if (metadata.op !== actionName) {
86
+ throw new Error(
87
+ `Extension action "${actionName}" must use the same action op.`
88
+ );
89
+ }
90
+ if (actionName in program) {
91
+ throw new Error(`ChartProgram action "${actionName}" is already defined.`);
92
+ }
93
+
94
+ descriptors[actionName] = {
95
+ value: action,
96
+ configurable: false,
97
+ enumerable: true,
98
+ writable: false
99
+ };
100
+ }
101
+
102
+ Object.defineProperties(ProgramClass.prototype, descriptors);
103
+ registeredExtensionNames.add(name);
104
+ }