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
@@ -6,6 +6,7 @@ import {
6
6
  } from "../../../../grammar/polarGuides.js";
7
7
  import { resolvePlotGraphicPlacement } from
8
8
  "../../../../materialization/graphicHierarchy.js";
9
+ import { assertPolarTextLayout } from "../../../../layout/labels.js";
9
10
  import {
10
11
  formatPolarGuideValues,
11
12
  mapPolarGuideValues,
@@ -33,7 +34,7 @@ function labelGeometry(program, kind, config) {
33
34
  const frame = resolvePolarFrameForProgram(program);
34
35
  const mapped = mapPolarGuideValues(program, config);
35
36
  const text = formatPolarGuideValues(program, config, mapped.values);
36
- return {
37
+ const geometry = {
37
38
  values: mapped.values,
38
39
  text,
39
40
  ...(kind === "theta"
@@ -49,6 +50,21 @@ function labelGeometry(program, kind, config) {
49
50
  offset: config.offset
50
51
  }))
51
52
  };
53
+ assertPolarTextLayout({
54
+ canvas: program.graphicSpec.objects.canvas,
55
+ label: `${prefix(kind)} axis labels`,
56
+ items: text.map((value, index) => ({
57
+ x: geometry.x[index],
58
+ y: geometry.y[index],
59
+ text: value,
60
+ fontSize: config.fontSize,
61
+ fontFamily: config.fontFamily,
62
+ fontWeight: config.fontWeight,
63
+ textAlign: geometry.textAlign[index],
64
+ textBaseline: geometry.textBaseline[index]
65
+ }))
66
+ });
67
+ return geometry;
52
68
  }
53
69
 
54
70
  function resolveLabelConfig(program, kind, args, resources, previous) {
@@ -1,4 +1,7 @@
1
- import { validateOptionObject } from "../../../../core/validation.js";
1
+ import {
2
+ validateGeneratedItemLimit,
3
+ validateOptionObject
4
+ } from "../../../../core/validation.js";
2
5
  import {
3
6
  POLAR_AXIS_DEFAULTS,
4
7
  resolvePolarGuideResources
@@ -49,6 +52,9 @@ export function validateModeOptions(args, operation) {
49
52
  if (Object.hasOwn(args, "count") && Object.hasOwn(args, "values")) {
50
53
  throw new Error(`${operation} cannot use count and values together.`);
51
54
  }
55
+ if (Array.isArray(args.values)) {
56
+ validateGeneratedItemLimit(args.values.length, "Polar guide value count");
57
+ }
52
58
  }
53
59
 
54
60
  export function validateAngle(value) {
@@ -9,6 +9,7 @@ import {
9
9
  } from "../../../../grammar/polarGuides.js";
10
10
  import { resolvePlotGraphicPlacement } from
11
11
  "../../../../materialization/graphicHierarchy.js";
12
+ import { assertPolarTextLayout } from "../../../../layout/labels.js";
12
13
  import { inferAxisTitleText } from "../../axes/titles.js";
13
14
  import {
14
15
  POLAR_AXIS_DEFAULTS,
@@ -39,6 +40,20 @@ function titleGeometry(program, kind, config) {
39
40
  });
40
41
  }
41
42
 
43
+ function validateTitleGeometry(program, kind, config, geometry, text) {
44
+ assertPolarTextLayout({
45
+ canvas: program.graphicSpec.objects.canvas,
46
+ label: `${prefix(kind)} axis title`,
47
+ items: [{
48
+ ...geometry,
49
+ text,
50
+ fontSize: config.fontSize,
51
+ fontFamily: config.fontFamily,
52
+ fontWeight: config.fontWeight
53
+ }]
54
+ });
55
+ }
56
+
42
57
  function resolveTitleConfig(kind, args, resources, previous) {
43
58
  const config = {
44
59
  ...(previous ?? {}),
@@ -96,6 +111,8 @@ function makeEditTitle(kind) {
96
111
  : config.inferredText
97
112
  ? inferAxisTitleText(this, polarGuideNames(kind).channel, config.scale)
98
113
  : this.semanticSpec.guides.axis?.[kind]?.title;
114
+ const geometry = titleGeometry(this, kind, config);
115
+ validateTitleGeometry(this, kind, config, geometry, text);
99
116
  let next = this;
100
117
  if (text !== this.semanticSpec.guides.axis?.[kind]?.title) {
101
118
  next = next.editSemantic({
@@ -103,7 +120,6 @@ function makeEditTitle(kind) {
103
120
  value: text
104
121
  });
105
122
  }
106
- const geometry = titleGeometry(next, kind, config);
107
123
  next = next._withGuideConfig(kind, "title", config);
108
124
  const properties = {
109
125
  ...geometry,
@@ -138,7 +154,8 @@ function makeCreateTitle(kind) {
138
154
  args.text ?? inferAxisTitleText(this, names.channel, resources.scale),
139
155
  "Polar axis title text"
140
156
  );
141
- titleGeometry(this, kind, config);
157
+ const geometry = titleGeometry(this, kind, config);
158
+ validateTitleGeometry(this, kind, config, geometry, text);
142
159
  let next = withAxisSemantics(this, kind, resources);
143
160
  if (kind === "radius" &&
144
161
  next.guideConfigs.axis?.radius?.layout === undefined) {
@@ -1,6 +1,7 @@
1
1
  import { action } from "../../../core/action.js";
2
2
  import {
3
3
  noOptions,
4
+ validateGeneratedItemLimit,
4
5
  validateOptionObject,
5
6
  validateNonEmptyString,
6
7
  validateNonNegativeFinite
@@ -55,6 +56,9 @@ function validateOptions(args, operation, create) {
55
56
  if (Object.hasOwn(args, "count") && Object.hasOwn(args, "values")) {
56
57
  throw new Error(`${operation} cannot use count and values together.`);
57
58
  }
59
+ if (Array.isArray(args.values)) {
60
+ validateGeneratedItemLimit(args.values.length, "Polar grid value count");
61
+ }
58
62
  if (Object.hasOwn(args, "color")) {
59
63
  validateNonEmptyString(args.color, "Polar grid color");
60
64
  }
@@ -1,5 +1,6 @@
1
1
  import { validateUserId } from "../../../core/identifiers.js";
2
2
  import {
3
+ validateGeneratedItemLimit,
3
4
  validateNonEmptyString,
4
5
  validateNonNegativeFinite,
5
6
  validatePositiveFinite
@@ -174,6 +175,7 @@ export function resolvePolarTickValues(program, config) {
174
175
  if (config.mode !== "values") {
175
176
  throw new Error("Discrete theta guides require explicit or inferred values.");
176
177
  }
178
+ validateGeneratedItemLimit(config.values.length, "Polar guide value count");
177
179
  const domain = new Set(scale.domain);
178
180
  if (!config.values.every(value => domain.has(value))) {
179
181
  throw new RangeError("Polar guide values must be inside the scale domain.");
@@ -181,6 +183,7 @@ export function resolvePolarTickValues(program, config) {
181
183
  return config.values;
182
184
  }
183
185
  const values = valuesFromTickConfig(program, config);
186
+ validateGeneratedItemLimit(values.length, "Polar guide value count");
184
187
  const low = Math.min(...scale.domain);
185
188
  const high = Math.max(...scale.domain);
186
189
  if (!values.every(value => value >= low && value <= high)) {
@@ -238,6 +241,9 @@ export function validatePolarTickConfig(config, label) {
238
241
  (!Number.isInteger(config.count) || config.count <= 0)) {
239
242
  throw new RangeError(`${label} count must be a positive integer.`);
240
243
  }
244
+ if (config.mode === "count") {
245
+ validateGeneratedItemLimit(config.count, `${label} count`);
246
+ }
241
247
  if (config.mode === "values" && (
242
248
  !Array.isArray(config.values) || config.values.length === 0 ||
243
249
  !config.values.every(value =>
@@ -1,23 +1,23 @@
1
- import { registerCanvasActions } from "./canvas/index.js";
2
- import { registerChartActions } from "./charts/index.js";
3
- import { registerCategoryOrderActions } from "./categoryOrder/index.js";
4
- import { registerCoordinateActions } from "./coordinates/actions.js";
5
- import { registerCompositionActions } from "./composition/index.js";
1
+ import { registerPrimitiveActions } from "./primitives/index.js";
2
+ import { registerSelectionActions } from "./selection/index.js";
6
3
  import { registerDataActions } from "./data/index.js";
4
+ import { registerCategoryOrderActions } from "./categoryOrder/index.js";
7
5
  import { registerEncodingActions } from "./encodings/index.js";
8
- import { registerErrorBandActions } from "./errorBands/index.js";
9
- import { registerErrorBarActions } from "./errorBars/index.js";
6
+ import { registerScaleActions } from "./scales/index.js";
7
+ import { registerMarkActions } from "./marks/index.js";
8
+ import { registerCoordinateActions } from "./coordinates/actions.js";
10
9
  import { registerFacetActions } from "./facets/index.js";
11
- import { registerBoxPlotActions } from "./boxPlots/index.js";
10
+ import { registerCompositionActions } from "./composition/index.js";
11
+ import { registerTitleActions } from "./titles/index.js";
12
+ import { registerCanvasActions } from "./canvas/index.js";
12
13
  import { registerGuideActions } from "./guides/index.js";
13
14
  import { registerGradientPlotActions } from "./gradientPlots/index.js";
14
- import { registerMarkActions } from "./marks/index.js";
15
- import { registerPrimitiveActions } from "./primitives/index.js";
15
+ import { registerBoxPlotActions } from "./boxPlots/index.js";
16
+ import { registerErrorBarActions } from "./errorBars/index.js";
16
17
  import { registerRegressionActions } from "./regression/index.js";
17
- import { registerScaleActions } from "./scales/index.js";
18
- import { registerTitleActions } from "./titles/index.js";
19
- import { registerSelectionActions } from "./selection/index.js";
20
18
  import { registerViolinPlotActions } from "./violinPlots/index.js";
19
+ import { registerErrorBandActions } from "./errorBands/index.js";
20
+ import { registerChartActions } from "./charts/index.js";
21
21
 
22
22
  export function registerActions(ProgramClass) {
23
23
  registerPrimitiveActions(ProgramClass);
@@ -3,37 +3,28 @@ import {
3
3
  resolveOptionalUserId,
4
4
  validateUserId
5
5
  } from "../../core/identifiers.js";
6
+ import {
7
+ validateNonEmptyString as requireField,
8
+ validateOptionObject
9
+ } from "../../core/validation.js";
6
10
  import { readQuantitativeField } from "../../grammar/scales/index.js";
7
11
  import { findDataset } from "../../selectors/datasets.js";
8
12
  import { hasLayer, resolveEligibleLayer } from "../../selectors/layers.js";
9
13
  import { findSemanticScale } from "../../selectors/scales.js";
10
14
 
11
- const CHANNEL_OPTIONS = Object.freeze([
15
+ const CHANNEL_OPTIONS = [
12
16
  "field", "fieldType", "scale", "center", "extent", "level", "lower", "upper"
13
- ]);
14
- const INTERVAL_PARAMETER_KEYS = Object.freeze([
15
- "center", "extent", "level", "lower", "upper"
16
- ]);
17
- const FIELD_TYPES = Object.freeze([
17
+ ];
18
+ const INTERVAL_PARAMETER_KEYS = CHANNEL_OPTIONS.slice(3);
19
+ const POSITION_CHANNELS = ["x", "y"];
20
+ const FIELD_TYPES = [
18
21
  "quantitative", "temporal", "ordinal", "nominal"
19
- ]);
22
+ ];
20
23
 
21
24
  function requireObject(value, label) {
22
- if (!isPlainObject(value)) {
23
- throw new TypeError(`${label} must be a plain object.`);
24
- }
25
- const unknown = Object.keys(value).find(key => !CHANNEL_OPTIONS.includes(key));
26
- if (unknown !== undefined) {
27
- throw new Error(`Unknown ${label} option "${unknown}".`);
28
- }
29
- return value;
30
- }
31
-
32
- function requireField(value, label) {
33
- if (typeof value !== "string" || value.length === 0) {
34
- throw new TypeError(`${label} must be a non-empty string.`);
35
- }
36
- return value;
25
+ return validateOptionObject(value, CHANNEL_OPTIONS, label, {
26
+ plainObjectMessage: `${label} must be a plain object.`
27
+ });
37
28
  }
38
29
 
39
30
  function hasCompleteFieldPositions(layer) {
@@ -42,65 +33,30 @@ function hasCompleteFieldPositions(layer) {
42
33
  return (
43
34
  typeof layer.data === "string" &&
44
35
  typeof layer.coordinate === "string" &&
45
- typeof x?.field === "string" &&
46
- typeof y?.field === "string" &&
47
- typeof x.scale === "string" &&
48
- typeof y.scale === "string" &&
49
- FIELD_TYPES.includes(x.fieldType) &&
50
- FIELD_TYPES.includes(y.fieldType)
36
+ [x, y].every(encoding =>
37
+ typeof encoding?.field === "string" &&
38
+ typeof encoding.scale === "string" &&
39
+ FIELD_TYPES.includes(encoding.fieldType)
40
+ )
51
41
  );
52
42
  }
53
43
 
54
- function resolveSourceLayer(program, args, operation) {
55
- if (args.target === undefined && args.x !== undefined && args.y !== undefined) {
56
- return undefined;
57
- }
58
- const target = args.target === undefined
59
- ? undefined
60
- : validateUserId(args.target, `${operation} source layer id`);
61
- return resolveEligibleLayer(program, {
62
- target,
63
- predicate: hasCompleteFieldPositions,
64
- label: operation
65
- });
66
- }
67
-
68
- function resolveDataset(program, args, sourceLayer, operation, resourceLabel) {
69
- const requested = args.data ?? sourceLayer?.data ?? program.context.currentData;
70
- let dataset;
71
- if (requested !== undefined) {
72
- const id = validateUserId(requested, `${resourceLabel} dataset id`);
73
- dataset = findDataset(program, id);
74
- if (dataset === undefined) {
75
- throw new Error(`Unknown ${resourceLabel} dataset "${id}".`);
76
- }
77
- } else if (program.semanticSpec.datasets.length === 1) {
78
- dataset = program.semanticSpec.datasets[0];
79
- }
80
- if (dataset === undefined) {
81
- throw new Error(`${operation} requires data or one uniquely inferable dataset.`);
82
- }
83
- if (sourceLayer !== undefined && dataset.id !== sourceLayer.data) {
84
- throw new Error(
85
- `${operation} data must match source layer "${sourceLayer.id}" data "${sourceLayer.data}".`
86
- );
87
- }
88
- return dataset;
89
- }
90
-
91
44
  function scaleOptions(program, value, inferredId, defaults = {}) {
92
45
  if (value !== undefined && !isPlainObject(value)) {
93
46
  throw new TypeError("Interval scale must be a plain object.");
94
47
  }
95
48
  const id = value?.id ?? inferredId;
96
- const usesExisting = id !== undefined && findSemanticScale(program, id) !== undefined;
97
- return {
98
- ...(usesExisting ? {} : defaults),
49
+ const scale = {
50
+ ...(id !== undefined && findSemanticScale(program, id) !== undefined
51
+ ? {}
52
+ : defaults),
99
53
  ...(value ?? {}),
100
54
  ...(value?.id === undefined && inferredId !== undefined
101
55
  ? { id: inferredId }
102
56
  : {})
103
57
  };
58
+ if (value?.type === "log" && value.zero === undefined) delete scale.zero;
59
+ return scale;
104
60
  }
105
61
 
106
62
  function hasAny(value, keys) {
@@ -113,30 +69,28 @@ function resolveIntervalChannel(channels, sourceLayer, {
113
69
  defaultIntervalChannel,
114
70
  ambiguousMessage
115
71
  }) {
116
- const explicitBounds = ["x", "y"].filter(channel =>
117
- hasAny(channels[channel], ["lower", "upper"])
118
- );
119
- if (explicitBounds.length > 1) {
120
- throw new Error(`${operation} requires exactly one interval channel.`);
72
+ for (const hints of [
73
+ ["lower", "upper"],
74
+ ["center", "extent", "level"]
75
+ ]) {
76
+ const hinted = POSITION_CHANNELS.filter(channel =>
77
+ hasAny(channels[channel], hints)
78
+ );
79
+ if (hinted.length > 1) {
80
+ throw new Error(`${operation} requires exactly one interval channel.`);
81
+ }
82
+ if (hinted.length === 1) return hinted[0];
121
83
  }
122
- if (explicitBounds.length === 1) return explicitBounds[0];
123
84
 
124
- const statisticalHints = ["x", "y"].filter(channel =>
125
- hasAny(channels[channel], ["center", "extent", "level"])
126
- );
127
- if (statisticalHints.length > 1) {
128
- throw new Error(`${operation} requires exactly one interval channel.`);
85
+ const effectiveTypes = {};
86
+ for (const channel of POSITION_CHANNELS) {
87
+ effectiveTypes[channel] = channels[channel]?.fieldType ??
88
+ sourceLayer?.encoding?.[channel]?.fieldType;
129
89
  }
130
- if (statisticalHints.length === 1) return statisticalHints[0];
131
-
132
- const effectiveTypes = Object.fromEntries(["x", "y"].map(channel => [
133
- channel,
134
- channels[channel]?.fieldType ?? sourceLayer?.encoding?.[channel]?.fieldType
135
- ]));
136
- const quantitative = ["x", "y"].filter(channel =>
90
+ const quantitative = POSITION_CHANNELS.filter(channel =>
137
91
  effectiveTypes[channel] === "quantitative"
138
92
  );
139
- const positional = ["x", "y"].filter(channel =>
93
+ const positional = POSITION_CHANNELS.filter(channel =>
140
94
  positionTypes.includes(effectiveTypes[channel])
141
95
  );
142
96
  if (quantitative.length === 1) {
@@ -224,10 +178,11 @@ function resolveInterval(program, channel, explicit, inferred, dataset, {
224
178
  lower: requireField(explicit.lower, `${operation} ${channel} lower`),
225
179
  upper: requireField(explicit.upper, `${operation} ${channel} upper`)
226
180
  };
227
- if (new Set(Object.values(fields)).size !== 3) {
181
+ const fieldValues = Object.values(fields);
182
+ if (new Set(fieldValues).size !== 3) {
228
183
  throw new Error("Explicit interval center, lower, and upper fields must be distinct.");
229
184
  }
230
- for (const field of Object.values(fields)) {
185
+ for (const field of fieldValues) {
231
186
  readQuantitativeField(dataset.values, field);
232
187
  }
233
188
  return { channel, mode: "explicit", fields, scale, title: fields.center };
@@ -268,39 +223,46 @@ function resolveGrouping(args, sourceLayer, independentField, mode, {
268
223
  };
269
224
  }
270
225
 
271
- function resolveOwner(program, requested, {
272
- defaultId,
273
- ownerLabel,
274
- operation
275
- }) {
276
- const defaultOccupied = hasLayer(program, defaultId) ||
277
- program.graphicSpec.objects[defaultId] !== undefined ||
278
- findDataset(program, `${defaultId}IntervalData`) !== undefined;
279
- return resolveOptionalUserId(requested, {
280
- defaultId,
281
- label: ownerLabel,
282
- operation,
283
- ambiguous: defaultOccupied
284
- });
285
- }
286
-
287
226
  export function resolveIntervalComposite(program, args, policy) {
227
+ const { operation, resourceLabel } = policy;
288
228
  const channels = {
289
229
  x: args.x === undefined
290
230
  ? undefined
291
- : requireObject(args.x, `${policy.operation} x`),
231
+ : requireObject(args.x, `${operation} x`),
292
232
  y: args.y === undefined
293
233
  ? undefined
294
- : requireObject(args.y, `${policy.operation} y`)
234
+ : requireObject(args.y, `${operation} y`)
295
235
  };
296
- const sourceLayer = resolveSourceLayer(program, args, policy.operation);
297
- const dataset = resolveDataset(
298
- program,
299
- args,
300
- sourceLayer,
301
- policy.operation,
302
- policy.resourceLabel
303
- );
236
+ let sourceLayer;
237
+ if (!(args.target === undefined && args.x !== undefined && args.y !== undefined)) {
238
+ const target = args.target === undefined
239
+ ? undefined
240
+ : validateUserId(args.target, `${operation} source layer id`);
241
+ sourceLayer = resolveEligibleLayer(program, {
242
+ target,
243
+ predicate: hasCompleteFieldPositions,
244
+ label: operation
245
+ });
246
+ }
247
+ const requested = args.data ?? sourceLayer?.data ?? program.context.currentData;
248
+ let dataset;
249
+ if (requested !== undefined) {
250
+ const dataId = validateUserId(requested, `${resourceLabel} dataset id`);
251
+ dataset = findDataset(program, dataId);
252
+ if (dataset === undefined) {
253
+ throw new Error(`Unknown ${resourceLabel} dataset "${dataId}".`);
254
+ }
255
+ } else if (program.semanticSpec.datasets.length === 1) {
256
+ dataset = program.semanticSpec.datasets[0];
257
+ }
258
+ if (dataset === undefined) {
259
+ throw new Error(`${operation} requires data or one uniquely inferable dataset.`);
260
+ }
261
+ if (sourceLayer !== undefined && dataset.id !== sourceLayer.data) {
262
+ throw new Error(
263
+ `${operation} data must match source layer "${sourceLayer.id}" data "${sourceLayer.data}".`
264
+ );
265
+ }
304
266
  const intervalChannel = resolveIntervalChannel(channels, sourceLayer, policy);
305
267
  const positionChannel = intervalChannel === "x" ? "y" : "x";
306
268
  const position = resolvePosition(
@@ -318,7 +280,15 @@ export function resolveIntervalComposite(program, args, policy) {
318
280
  dataset,
319
281
  policy
320
282
  );
321
- const id = resolveOwner(program, args.id, policy);
283
+ const defaultId = policy.defaultId;
284
+ const id = resolveOptionalUserId(args.id, {
285
+ defaultId,
286
+ label: policy.ownerLabel,
287
+ operation,
288
+ ambiguous: hasLayer(program, defaultId) ||
289
+ program.graphicSpec.objects[defaultId] !== undefined ||
290
+ findDataset(program, `${defaultId}IntervalData`) !== undefined
291
+ });
322
292
  const generatedFields = {
323
293
  center: `__${id}_center`,
324
294
  lower: `__${id}_lower`,
@@ -340,7 +310,7 @@ export function resolveIntervalComposite(program, args, policy) {
340
310
  dataId: interval.mode === "statistical" ? `${id}IntervalData` : dataset.id,
341
311
  coordinate: validateUserId(
342
312
  args.coordinate ?? sourceLayer?.coordinate ?? "main",
343
- `${policy.resourceLabel} coordinate id`
313
+ `${resourceLabel} coordinate id`
344
314
  ),
345
315
  orientation: intervalChannel === "y" ? "vertical" : "horizontal",
346
316
  position,
@@ -10,7 +10,10 @@ import { buildAnnularSectorCommands } from "../../../grammar/polarPaths.js";
10
10
  import { resolvePolarFrame } from "../../../grammar/polar.js";
11
11
  import { mapOrdinalValues } from "../../../grammar/scales/index.js";
12
12
  import { resolveGraphicBounds } from "../../../layout/canvas.js";
13
- import { canMaterializeArc } from "../../../materialization/marks/index.js";
13
+ import {
14
+ canMaterializeArc,
15
+ getSourceDependentMarkSteps
16
+ } from "../../../materialization/marks/index.js";
14
17
  import { findDataset } from "../../../selectors/datasets.js";
15
18
  import { findLayer, resolveEligibleLayer } from "../../../selectors/layers.js";
16
19
  import { DEFAULT_COLORS } from "../../../theme/defaults.js";
@@ -20,6 +23,7 @@ import { rematerializeHighlightBaseline } from "../lifecycle.js";
20
23
  import {
21
24
  assertMarkAvailable,
22
25
  applyLayeredMarkInheritance,
26
+ editMarkGraphic,
23
27
  materializeInheritedMark,
24
28
  resolveLayeredMarkInheritance,
25
29
  resolveMarkData,
@@ -27,14 +31,11 @@ import {
27
31
  validateMarkOptions
28
32
  } from "../shared.js";
29
33
 
30
- const CREATE_OPTIONS = Object.freeze([
31
- "id", "data", "innerRadius", "padAngle", "fill", "opacity", "stroke",
32
- "strokeWidth"
33
- ]);
34
- const EDIT_OPTIONS = Object.freeze([
35
- "target", "innerRadius", "padAngle", "fill", "opacity", "stroke",
36
- "strokeWidth"
34
+ const ARC_OPTIONS = Object.freeze([
35
+ "innerRadius", "padAngle", "fill", "opacity", "stroke", "strokeWidth"
37
36
  ]);
37
+ const CREATE_OPTIONS = Object.freeze(["id", "data", ...ARC_OPTIONS]);
38
+ const EDIT_OPTIONS = Object.freeze(["target", ...ARC_OPTIONS]);
38
39
  const REMATERIALIZE_OPTIONS = Object.freeze(["id"]);
39
40
 
40
41
  function validateInnerRadius(value) {
@@ -170,15 +171,15 @@ const rematerializeArcMark = action(
170
171
  resolved = resolved.rematerializeScale({ id: colorScaleId });
171
172
  }
172
173
  const config = resolved.markConfigs[id] ?? {};
174
+ const frame = resolvePolarFrame(resolveGraphicBounds(resolved));
173
175
  const derived = deriveArcSectors(dataset.values, layer, {
174
176
  thetaScale: resolved.resolvedScales[thetaScaleId],
175
177
  ...(radiusScaleId === undefined
176
178
  ? {}
177
179
  : { radiusScale: resolved.resolvedScales[radiusScaleId] }),
178
- frame: resolvePolarFrame(resolveGraphicBounds(resolved)),
180
+ frame,
179
181
  innerRadiusRatio: config.innerRadius ?? 0
180
182
  });
181
- const frame = resolvePolarFrame(resolveGraphicBounds(resolved));
182
183
  const commands = derived.sectors.map(sector => buildAnnularSectorCommands({
183
184
  frame,
184
185
  startTheta: sector.startTheta,
@@ -194,26 +195,15 @@ const rematerializeArcMark = action(
194
195
  resolved.resolvedScales[colorScaleId].domain,
195
196
  resolved.resolvedScales[colorScaleId].range
196
197
  );
197
- return resolved
198
- .editGraphics({ target: id, property: "length", value: commands.length })
199
- .editGraphics({ target: id, property: "commands", value: commands })
200
- .editGraphics({ target: id, property: "fill", value: fills })
201
- .editGraphics({ target: id, property: "opacity", value: config.opacity ?? 1 })
202
- .editGraphics({
203
- target: id,
204
- property: "stroke",
205
- value: config.stroke === false ? "transparent" : config.stroke ?? "#ffffff"
206
- })
207
- .editGraphics({
208
- target: id,
209
- property: "strokeWidth",
210
- value: config.stroke === false ? 0 : config.strokeWidth ?? 1
211
- })
212
- .editGraphics({
213
- target: id,
214
- property: "strokeDash",
215
- value: commands.map(() => [])
216
- });
198
+ return editMarkGraphic(resolved, id, {
199
+ length: commands.length,
200
+ commands,
201
+ fill: fills,
202
+ opacity: config.opacity ?? 1,
203
+ stroke: config.stroke === false ? "transparent" : config.stroke ?? "#ffffff",
204
+ strokeWidth: config.stroke === false ? 0 : config.strokeWidth ?? 1,
205
+ strokeDash: commands.map(() => [])
206
+ });
217
207
  }
218
208
  );
219
209
 
@@ -258,14 +248,19 @@ const editArcMark = action(
258
248
  allowStrokeRemoval: true
259
249
  })
260
250
  );
261
- return canMaterializeArc(next, layer)
262
- ? next.rematerializeArcMark({ id: layer.id })
263
- : next;
251
+ if (!canMaterializeArc(next, layer)) return next;
252
+ let rematerialized = next.rematerializeArcMark({ id: layer.id });
253
+ for (const step of getSourceDependentMarkSteps(rematerialized, layer.id)) {
254
+ rematerialized = rematerialized[step.op](step.args);
255
+ }
256
+ return rematerialized;
264
257
  }
265
258
  );
266
259
 
267
260
  export function registerArcMarkActions(ProgramClass) {
268
- ProgramClass.prototype.createArcMark = createArcMark;
269
- ProgramClass.prototype.editArcMark = editArcMark;
270
- ProgramClass.prototype.rematerializeArcMark = rematerializeArcMark;
261
+ Object.assign(ProgramClass.prototype, {
262
+ createArcMark,
263
+ editArcMark,
264
+ rematerializeArcMark
265
+ });
271
266
  }