ggaction 0.0.10 → 0.0.12

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 (224) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +5 -3
  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/intent-taxonomy.json +1 -183
  7. package/knowledge/intent-taxonomy.schema.json +1 -0
  8. package/knowledge/mcp-resources.json +1 -95
  9. package/knowledge/mcp-resources.schema.json +1 -0
  10. package/knowledge/task-packet.schema.json +1 -159
  11. package/knowledge/task-resolver.js +1 -1230
  12. package/package.json +18 -2
  13. package/src/actions/boxPlots/edit.js +45 -298
  14. package/src/actions/boxPlots/options.js +46 -77
  15. package/src/actions/boxPlots/resolve.js +1 -1
  16. package/src/actions/charts/bar.js +3 -0
  17. package/src/actions/charts/heatmap.js +73 -83
  18. package/src/actions/charts/index.js +1 -1
  19. package/src/actions/charts/parallel.js +35 -1
  20. package/src/actions/charts/shared.js +20 -0
  21. package/src/actions/composition/actions.js +46 -49
  22. package/src/actions/data/bin2d.js +105 -2
  23. package/src/actions/data/box.js +17 -17
  24. package/src/actions/data/density.js +42 -78
  25. package/src/actions/data/gradientProfile.js +22 -52
  26. package/src/actions/data/horizon.js +16 -41
  27. package/src/actions/data/index.js +10 -10
  28. package/src/actions/data/interval.js +16 -38
  29. package/src/actions/data/intervalEdit.js +55 -0
  30. package/src/actions/data/regression.js +7 -17
  31. package/src/actions/data/shared.js +47 -0
  32. package/src/actions/data/timeUnit.js +14 -38
  33. package/src/actions/data/window.js +14 -38
  34. package/src/actions/distributions/revision.js +154 -34
  35. package/src/actions/encodings/angle.js +9 -10
  36. package/src/actions/encodings/appearance.js +17 -51
  37. package/src/actions/encodings/density/resolve.js +3 -1
  38. package/src/actions/encodings/density.js +1 -15
  39. package/src/actions/encodings/horizon/resolve.js +13 -3
  40. package/src/actions/encodings/horizon.js +8 -7
  41. package/src/actions/encodings/index.js +9 -9
  42. package/src/actions/encodings/offset.js +41 -45
  43. package/src/actions/encodings/parallel.js +9 -10
  44. package/src/actions/encodings/pathOrder.js +6 -13
  45. package/src/actions/encodings/position/apply.js +30 -0
  46. package/src/actions/encodings/position/policies/arc.js +23 -2
  47. package/src/actions/encodings/position/policies/index.js +4 -4
  48. package/src/actions/encodings/position/policies/line.js +47 -17
  49. package/src/actions/encodings/position/resolve.js +36 -7
  50. package/src/actions/encodings/ranged.js +18 -23
  51. package/src/actions/encodings/remove.js +15 -25
  52. package/src/actions/encodings/ruleAppearance.js +9 -24
  53. package/src/actions/encodings/shared.js +37 -0
  54. package/src/actions/encodings/strokeDash.js +8 -23
  55. package/src/actions/encodings/text.js +9 -10
  56. package/src/actions/errorBands/create.js +105 -39
  57. package/src/actions/errorBands/edit.js +89 -93
  58. package/src/actions/errorBands/index.js +1 -2
  59. package/src/actions/errorBars/create.js +212 -30
  60. package/src/actions/errorBars/edit.js +91 -48
  61. package/src/actions/errorBars/index.js +1 -2
  62. package/src/actions/facets/actions.js +45 -69
  63. package/src/actions/facets/derive.js +38 -53
  64. package/src/actions/facets/replay.js +0 -1
  65. package/src/actions/gradientPlots/components.js +59 -48
  66. package/src/actions/gradientPlots/edit.js +35 -216
  67. package/src/actions/gradientPlots/index.js +11 -10
  68. package/src/actions/gradientPlots/materialize.js +21 -35
  69. package/src/actions/gradientPlots/options.js +5 -7
  70. package/src/actions/gradientPlots/paint.js +9 -6
  71. package/src/actions/gradientPlots/resolve.js +1 -1
  72. package/src/actions/guides/axes/index.js +3 -3
  73. package/src/actions/guides/axes/labels.js +68 -26
  74. package/src/actions/guides/axes/lines.js +22 -2
  75. package/src/actions/guides/axes/policy.js +28 -8
  76. package/src/actions/guides/axes/ticks.js +27 -9
  77. package/src/actions/guides/axes/titles.js +113 -65
  78. package/src/actions/guides/grids/resolve.js +14 -0
  79. package/src/actions/guides/guides.js +2 -4
  80. package/src/actions/guides/index.js +2 -2
  81. package/src/actions/guides/legends/categorical/actions.js +44 -68
  82. package/src/actions/guides/legends/categorical/components.js +6 -1
  83. package/src/actions/guides/legends/categorical/index.js +2 -2
  84. package/src/actions/guides/legends/categorical/layout.js +148 -175
  85. package/src/actions/guides/legends/categorical/recipes.js +5 -0
  86. package/src/actions/guides/legends/categorical/resolve.js +8 -1
  87. package/src/actions/guides/legends/continuous/common.js +117 -66
  88. package/src/actions/guides/legends/continuous/gradient.js +38 -37
  89. package/src/actions/guides/legends/continuous/interval.js +97 -76
  90. package/src/actions/guides/legends/continuous/opacity.js +28 -38
  91. package/src/actions/guides/legends/edit.js +14 -1
  92. package/src/actions/guides/legends/index.js +1 -1
  93. package/src/actions/guides/legends/lane.js +2 -1
  94. package/src/actions/guides/legends/remove.js +8 -0
  95. package/src/actions/guides/legends/size.js +36 -43
  96. package/src/actions/guides/legends/strokeWidth.js +20 -26
  97. package/src/actions/guides/polar/axes/labels.js +17 -1
  98. package/src/actions/guides/polar/axes/shared.js +7 -1
  99. package/src/actions/guides/polar/axes/titles.js +19 -2
  100. package/src/actions/guides/polar/grids.js +4 -0
  101. package/src/actions/guides/polar/resolve.js +6 -0
  102. package/src/actions/index.js +13 -13
  103. package/src/actions/intervals/resolve.js +87 -117
  104. package/src/actions/marks/arc/actions.js +31 -36
  105. package/src/actions/marks/area/actions.js +23 -31
  106. package/src/actions/marks/index.js +6 -6
  107. package/src/actions/marks/line/actions.js +33 -52
  108. package/src/actions/marks/line/materialize.js +7 -5
  109. package/src/actions/marks/point/materialize.js +38 -16
  110. package/src/actions/marks/remove.js +21 -0
  111. package/src/actions/marks/rule/actions.js +79 -54
  112. package/src/actions/marks/shared.js +8 -4
  113. package/src/actions/marks/text/actions.js +14 -6
  114. package/src/actions/marks/text/layout.js +21 -45
  115. package/src/actions/marks/tick/actions.js +22 -21
  116. package/src/actions/primitives/createGraphics.js +4 -0
  117. package/src/actions/primitives/editGraphics.js +4 -0
  118. package/src/actions/primitives/index.js +3 -3
  119. package/src/actions/primitives/semantic.js +1 -1
  120. package/src/actions/primitives/semanticValidation/index.js +2 -2
  121. package/src/actions/primitives/semanticValidation/layer.js +1 -1
  122. package/src/actions/regression/edit.js +3 -43
  123. package/src/actions/scales/consumers/families.js +24 -4
  124. package/src/actions/scales/consumers/index.js +1 -1
  125. package/src/actions/scales/consumers/seriesLayout.js +17 -0
  126. package/src/actions/scales/definitions.js +100 -103
  127. package/src/actions/scales/materialize.js +5 -0
  128. package/src/actions/selection/actions.js +79 -121
  129. package/src/actions/titles/actions.js +48 -94
  130. package/src/actions/titles/resolve.js +14 -27
  131. package/src/actions/violinPlots/create.js +37 -44
  132. package/src/core/textMetrics.js +74 -37
  133. package/src/core/validation.js +18 -0
  134. package/src/core/vocabulary.js +2 -2
  135. package/src/grammar/aggregate.js +48 -28
  136. package/src/grammar/arcs.js +170 -32
  137. package/src/grammar/areaSeries.js +14 -16
  138. package/src/grammar/bin2d.js +37 -23
  139. package/src/grammar/boxPlot.js +14 -13
  140. package/src/grammar/categoricalDensity.js +14 -14
  141. package/src/grammar/categoryOrder.js +8 -14
  142. package/src/grammar/curveCommands.js +87 -20
  143. package/src/grammar/density.js +162 -141
  144. package/src/grammar/facets/index.js +14 -0
  145. package/src/grammar/facets/scales.js +7 -4
  146. package/src/grammar/gradientProfile.js +12 -15
  147. package/src/grammar/histogram.js +145 -83
  148. package/src/grammar/horizon.js +60 -23
  149. package/src/grammar/interval.js +14 -13
  150. package/src/grammar/lineSeries.js +91 -61
  151. package/src/grammar/numeric.js +290 -0
  152. package/src/grammar/paint.js +3 -4
  153. package/src/grammar/palettes.js +21 -32
  154. package/src/grammar/pathCommands.js +6 -0
  155. package/src/grammar/polarPaths.js +5 -5
  156. package/src/grammar/positionCompatibility.js +1 -1
  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/rules.js +2 -0
  161. package/src/grammar/scales/color.js +35 -26
  162. package/src/grammar/scales/continuous.js +71 -24
  163. package/src/grammar/scales/definition.js +1 -1
  164. package/src/grammar/scales/discretized.js +42 -12
  165. package/src/grammar/scales/fields.js +21 -4
  166. package/src/grammar/scales/ordinal.js +139 -60
  167. package/src/grammar/scales/temporal.js +40 -11
  168. package/src/grammar/scales/transformed.js +199 -52
  169. package/src/grammar/schemas/graphicBounds.js +270 -79
  170. package/src/grammar/seriesLayout.js +50 -18
  171. package/src/grammar/ticks.js +123 -53
  172. package/src/grammar/timeUnit.js +18 -8
  173. package/src/grammar/transforms.js +4 -1
  174. package/src/grammar/window.js +70 -57
  175. package/src/index.js +1 -1
  176. package/src/layout/canvas.js +4 -8
  177. package/src/layout/composition.js +39 -40
  178. package/src/layout/facets.js +55 -30
  179. package/src/layout/labels.js +91 -15
  180. package/src/layout/legend.js +22 -20
  181. package/src/layout/legendLane.js +54 -40
  182. package/src/layout/title.js +7 -7
  183. package/src/materialization/bars/aggregate.js +14 -40
  184. package/src/materialization/bars/grouped.js +7 -18
  185. package/src/materialization/bars/histogram.js +2 -19
  186. package/src/materialization/bars/ranged.js +20 -15
  187. package/src/materialization/bars/resolve.js +21 -0
  188. package/src/materialization/facetGuides/legacyCategorical.js +9 -2
  189. package/src/materialization/facetGuides/placement.js +49 -19
  190. package/src/materialization/facetGuides/preparation.js +35 -6
  191. package/src/materialization/facets.js +42 -28
  192. package/src/materialization/guides/resources.js +2 -1
  193. package/src/materialization/marks/capabilities.js +15 -4
  194. package/src/materialization/marks/index.js +29 -6
  195. package/src/materialization/rowEncoding.js +57 -1
  196. package/src/materialization/scales/policies/bar.js +61 -102
  197. package/src/materialization/scales/policies/binnedPosition.js +34 -0
  198. package/src/materialization/scales/policies/offset.js +54 -0
  199. package/src/materialization/scales/policies/series.js +13 -7
  200. package/src/materialization/scales/resolve.js +6 -9
  201. package/src/materialization/selection/items/arc.js +16 -2
  202. package/src/materialization/selection/items/bar.js +4 -2
  203. package/src/materialization/selection/items/common.js +21 -1
  204. package/src/materialization/selection/items/path.js +19 -2
  205. package/src/materialization/selection/items/point.js +4 -16
  206. package/src/materialization/selection/items/tick.js +13 -21
  207. package/src/materialization/selection/point.js +11 -4
  208. package/src/materialization/selection/styles.js +19 -27
  209. package/src/materialization/text.js +4 -1
  210. package/src/mcp/adapter.js +8 -3
  211. package/src/renderers/canvas/fill.js +2 -2
  212. package/src/renderers/canvas/index.js +35 -2
  213. package/src/renderers/canvas/native.js +160 -0
  214. package/src/renderers/pdf.js +14 -0
  215. package/src/renderers/svg.js +37 -4
  216. package/types/index.d.ts +3 -0
  217. package/types/program.d.ts +523 -131
  218. package/types/svg.d.ts +1 -0
  219. package/src/actions/errorBands/components.js +0 -81
  220. package/src/actions/errorBands/options.js +0 -39
  221. package/src/actions/errorBands/resolve.js +0 -28
  222. package/src/actions/errorBars/components.js +0 -62
  223. package/src/actions/errorBars/options.js +0 -58
  224. package/src/actions/errorBars/resolve.js +0 -25
@@ -3,17 +3,44 @@ import { isPlainObject } from "../../core/immutable.js";
3
3
  import { validateUserId } from "../../core/identifiers.js";
4
4
  import {
5
5
  validateOptionObject,
6
+ validateKeys,
6
7
  validateNonEmptyString,
8
+ validateNonNegativeFinite,
7
9
  validateUnitInterval
8
10
  } from "../../core/validation.js";
9
11
  import { validateCurveInterpolation } from "../../grammar/curveCommands.js";
10
- import { findLayer, resolveEligibleLayer } from "../../selectors/layers.js";
12
+ import { normalizeStrokeDashPattern } from "../../grammar/scales/index.js";
13
+ import { findLayer } from "../../selectors/layers.js";
14
+ import { removeOwnedMark } from "../marks/remove.js";
11
15
  import { DEFAULT_COLORS } from "../../theme/defaults.js";
12
- import { planIntervalEdit } from "../data/intervalEdit.js";
13
16
  import {
14
- ERROR_BAND_BOUNDARY_OPTIONS,
15
- resolveBoundaryAppearance
16
- } from "./options.js";
17
+ applyIntervalRevision,
18
+ ownOptions,
19
+ planIntervalEdit,
20
+ releaseIntervalRevision,
21
+ resolveIntervalOwner
22
+ } from "../data/intervalEdit.js";
23
+
24
+ export const ERROR_BAND_BOUNDARY_OPTIONS = Object.freeze([
25
+ "stroke", "strokeWidth", "strokeDash", "opacity", "curve"
26
+ ]);
27
+
28
+ export function resolveBoundaryAppearance(value, { defaults, operation }) {
29
+ if (!isPlainObject(value)) {
30
+ throw new TypeError(`${operation} must be a plain object.`);
31
+ }
32
+ validateKeys(value, ERROR_BAND_BOUNDARY_OPTIONS, operation);
33
+ const stroke = value.stroke ?? defaults.stroke;
34
+ const strokeWidth = value.strokeWidth ?? defaults.strokeWidth;
35
+ const strokeDash = value.strokeDash ?? defaults.strokeDash;
36
+ const opacity = value.opacity ?? defaults.opacity;
37
+ const curve = validateCurveInterpolation(value.curve ?? defaults.curve);
38
+ validateNonEmptyString(stroke, `${operation} stroke`);
39
+ validateNonNegativeFinite(strokeWidth, `${operation} strokeWidth`);
40
+ const resolvedStrokeDash = normalizeStrokeDashPattern(strokeDash);
41
+ validateUnitInterval(opacity, `${operation} opacity`);
42
+ return { stroke, strokeWidth, strokeDash: resolvedStrokeDash, opacity, curve };
43
+ }
17
44
 
18
45
  const EDIT_OPTIONS = Object.freeze([
19
46
  "target", "fill", "opacity", "curve", "statistics", "boundaries"
@@ -25,16 +52,27 @@ const REMATERIALIZE_OPTIONS = Object.freeze([
25
52
  "id", ...ERROR_BAND_BOUNDARY_OPTIONS
26
53
  ]);
27
54
 
55
+ export function errorBandBoundaries(config) {
56
+ return [
57
+ [
58
+ config.lowerBoundaryId,
59
+ config.fields?.lower ?? config.lowerField,
60
+ "lower"
61
+ ],
62
+ [
63
+ config.upperBoundaryId,
64
+ config.fields?.upper ?? config.upperField,
65
+ "upper"
66
+ ]
67
+ ];
68
+ }
69
+
28
70
  function resolveOwner(program, requested) {
29
- const target = requested === undefined
30
- ? undefined
31
- : validateUserId(requested, "Error-band id");
32
- return resolveEligibleLayer(program, {
33
- target,
71
+ return resolveIntervalOwner(program, requested, {
72
+ idLabel: "Error-band id",
34
73
  label: "error band",
35
- predicate: layer =>
36
- layer.mark?.type === "area" &&
37
- program.markConfigs[layer.id]?.errorBand !== undefined
74
+ mark: "area",
75
+ config: "errorBand"
38
76
  });
39
77
  }
40
78
 
@@ -90,31 +128,7 @@ function createBoundary(program, owner, id, bound, appearance) {
90
128
  }
91
129
 
92
130
  function removeBoundary(program, id) {
93
- if (findLayer(program, id) === undefined) return program;
94
- const selectionIds = Object.entries(
95
- program.materializationConfigs.selections ?? {}
96
- ).filter(([, config]) => config.target === id).map(([selection]) => selection);
97
- let next = program;
98
- for (const [highlight, config] of Object.entries(
99
- program.materializationConfigs.highlights ?? {}
100
- )) {
101
- if (config.target === id || selectionIds.includes(config.selection)) {
102
- next = next._withoutMaterializationConfig(["highlights", highlight]);
103
- }
104
- }
105
- for (const selection of selectionIds) {
106
- next = next._withoutMaterializationConfig(["selections", selection]);
107
- }
108
- return next
109
- .editSemantic({ property: `layer[${id}]`, remove: true })
110
- .editGraphics({ target: id, remove: true })
111
- ._withoutMaterializationConfig(["marks", id])
112
- ._withContext({
113
- ...(program.context.currentMark === id ? { currentMark: undefined } : {}),
114
- ...(selectionIds.includes(program.context.currentSelection)
115
- ? { currentSelection: undefined }
116
- : {})
117
- });
131
+ return removeOwnedMark(program, id);
118
132
  }
119
133
 
120
134
  export const rematerializeErrorBandBoundary = action(
@@ -178,7 +192,8 @@ export const editErrorBand = action(
178
192
  data: errorBand.data,
179
193
  consumers: [
180
194
  owner.id,
181
- ...[errorBand.lowerBoundaryId, errorBand.upperBoundaryId]
195
+ ...errorBandBoundaries(errorBand)
196
+ .map(([id]) => id)
182
197
  .filter(id => findLayer(this, id) !== undefined)
183
198
  ],
184
199
  statistics: args.statistics,
@@ -195,58 +210,43 @@ export const editErrorBand = action(
195
210
  );
196
211
  }
197
212
 
198
- const applyEdit = program => {
199
- let next = program;
200
- if (interval.changed) {
201
- next = next.createIntervalData(interval.dataArgs);
202
- for (const rebind of interval.revision.rebinds) {
203
- next = next.rebindLayerData(rebind);
213
+ let next = applyIntervalRevision(this, interval);
214
+ if (interval.changed) errorBand.data = interval.revision.id;
215
+ next = next
216
+ ._withMarkConfig(owner.id, { ...config, errorBand })
217
+ .rematerializeAreaMark({ id: owner.id });
218
+ if (Object.hasOwn(args, "boundaries")) {
219
+ if (args.boundaries === false) {
220
+ next = removeBoundary(next, errorBand.lowerBoundaryId);
221
+ next = removeBoundary(next, errorBand.upperBoundaryId);
222
+ } else if (Object.keys(args.boundaries).length === 0) {
223
+ for (const [id, bound] of errorBandBoundaries(errorBand)) {
224
+ next = findLayer(next, id) === undefined
225
+ ? createBoundary(
226
+ next,
227
+ owner.id,
228
+ id,
229
+ bound,
230
+ defaultBoundaryAppearance(next, owner.id)
231
+ )
232
+ : interval.changed
233
+ ? next.rematerializeLineMark({ id })
234
+ : next;
204
235
  }
205
- errorBand.data = interval.revision.id;
236
+ } else {
237
+ next = next.editErrorBandBoundary({
238
+ target: owner.id,
239
+ ...args.boundaries
240
+ });
206
241
  }
207
- next = next
208
- ._withMarkConfig(owner.id, { ...config, errorBand })
209
- .rematerializeAreaMark({ id: owner.id });
210
- if (Object.hasOwn(args, "boundaries")) {
211
- if (args.boundaries === false) {
212
- next = removeBoundary(next, errorBand.lowerBoundaryId);
213
- next = removeBoundary(next, errorBand.upperBoundaryId);
214
- } else if (Object.keys(args.boundaries).length === 0) {
215
- for (const [id, bound] of [
216
- [errorBand.lowerBoundaryId, errorBand.lowerField],
217
- [errorBand.upperBoundaryId, errorBand.upperField]
218
- ]) {
219
- next = findLayer(next, id) === undefined
220
- ? createBoundary(
221
- next,
222
- owner.id,
223
- id,
224
- bound,
225
- defaultBoundaryAppearance(next, owner.id)
226
- )
227
- : interval.changed
228
- ? next.rematerializeLineMark({ id })
229
- : next;
230
- }
231
- } else {
232
- next = next.editErrorBandBoundary({
233
- target: owner.id,
234
- ...args.boundaries
235
- });
236
- }
237
- } else if (interval.changed) {
238
- for (const id of [errorBand.lowerBoundaryId, errorBand.upperBoundaryId]) {
239
- if (findLayer(next, id) !== undefined) {
240
- next = next.rematerializeLineMark({ id });
241
- }
242
+ } else if (interval.changed) {
243
+ for (const [id] of errorBandBoundaries(errorBand)) {
244
+ if (findLayer(next, id) !== undefined) {
245
+ next = next.rematerializeLineMark({ id });
242
246
  }
243
247
  }
244
- return interval.changed
245
- ? next.releaseDerivedData(interval.revision.release)
246
- : next;
247
- };
248
- if (interval.changed || Object.hasOwn(args, "boundaries")) applyEdit(this);
249
- return applyEdit(this);
248
+ }
249
+ return releaseIntervalRevision(next, interval);
250
250
  }
251
251
  );
252
252
 
@@ -270,13 +270,9 @@ export const editErrorBandBoundary = action(
270
270
  }
271
271
  const owner = resolveOwner(this, args.target);
272
272
  const config = this.markConfigs[owner.id].errorBand;
273
- const patch = Object.fromEntries(
274
- ERROR_BAND_BOUNDARY_OPTIONS
275
- .filter(key => Object.hasOwn(args, key))
276
- .map(key => [key, args[key]])
277
- );
273
+ const patch = ownOptions(args, ERROR_BAND_BOUNDARY_OPTIONS);
278
274
  const ids = boundary === "both"
279
- ? [config.lowerBoundaryId, config.upperBoundaryId]
275
+ ? errorBandBoundaries(config).map(([id]) => id)
280
276
  : [boundary === "lower" ? config.lowerBoundaryId : config.upperBoundaryId];
281
277
  const plans = ids.map(id => {
282
278
  const existing = findLayer(this, id) !== undefined;
@@ -1,5 +1,4 @@
1
- import { createErrorBand } from "./create.js";
2
- import { createErrorBandBoundary } from "./components.js";
1
+ import { createErrorBand, createErrorBandBoundary } from "./create.js";
3
2
  import {
4
3
  editErrorBand,
5
4
  editErrorBandBoundary,
@@ -1,9 +1,20 @@
1
1
  import { action } from "../../core/action.js";
2
- import { validateKeys } from "../../core/validation.js";
2
+ import {
3
+ validateKeys,
4
+ validateOptionObject
5
+ } from "../../core/validation.js";
3
6
  import { findLayer } from "../../selectors/layers.js";
7
+ import { findDataset } from "../../selectors/datasets.js";
4
8
  import { DEFAULT_COLORS } from "../../theme/defaults.js";
5
- import { resolveErrorBarAppearance } from "./options.js";
6
- import { resolveErrorBar } from "./resolve.js";
9
+ import {
10
+ readNominalField,
11
+ validateCategoricalFieldType
12
+ } from "../../grammar/scales/index.js";
13
+ import { normalizeOffsetPadding } from "../../grammar/bars/geometry.js";
14
+ import { createResolvedIntervalData, ownOptions } from
15
+ "../data/intervalEdit.js";
16
+ import { resolveIntervalComposite } from "../intervals/resolve.js";
17
+ import { errorBarCaps, resolveErrorBarAppearance } from "./edit.js";
7
18
 
8
19
  const OPTIONS = Object.freeze([
9
20
  "id",
@@ -11,6 +22,8 @@ const OPTIONS = Object.freeze([
11
22
  "data",
12
23
  "x",
13
24
  "y",
25
+ "xOffset",
26
+ "yOffset",
14
27
  "groupBy",
15
28
  "coordinate",
16
29
  "caps",
@@ -21,13 +34,92 @@ const OPTIONS = Object.freeze([
21
34
  "opacity"
22
35
  ]);
23
36
 
24
- function resolveAppearance(args) {
25
- const appearanceArgs = Object.fromEntries(
26
- ["caps", "capSize", "stroke", "strokeWidth", "strokeDash", "opacity"]
27
- .filter(key => Object.hasOwn(args, key))
28
- .map(key => [key, args[key]])
37
+ const OFFSET_OPTIONS = Object.freeze([
38
+ "field", "fieldType", "scale", "paddingInner", "paddingOuter"
39
+ ]);
40
+
41
+ const ERROR_BAR_POLICY = Object.freeze({
42
+ operation: "createErrorBar",
43
+ resourceLabel: "error-bar",
44
+ defaultId: "errorBar",
45
+ ownerLabel: "Error-bar id",
46
+ positionTypes: Object.freeze([
47
+ "quantitative", "nominal", "ordinal", "temporal"
48
+ ]),
49
+ defaultPositionType: "nominal",
50
+ defaultIntervalChannel: "y",
51
+ scaleDefaults: () => ({}),
52
+ intervalScaleDefaults: Object.freeze({ nice: true, zero: false }),
53
+ allowExplicitGrouping: false,
54
+ ambiguousMessage:
55
+ "createErrorBar requires one quantitative interval axis and one compatible position axis; use interval options to disambiguate two quantitative channels."
56
+ });
57
+
58
+ function resolveErrorBar(program, args) {
59
+ const resolved = resolveIntervalComposite(program, args, ERROR_BAR_POLICY);
60
+ const offset = resolveErrorBarOffset(program, args, resolved);
61
+ const groupBy = resolved.interval.mode === "statistical" && offset !== undefined
62
+ ? [...new Set([...resolved.groupBy, offset.field])]
63
+ : resolved.groupBy;
64
+ return {
65
+ ...resolved,
66
+ groupBy,
67
+ offset,
68
+ lowerCapId: `${resolved.id}LowerCap`,
69
+ upperCapId: `${resolved.id}UpperCap`
70
+ };
71
+ }
72
+
73
+ function resolveErrorBarOffset(program, args, resolved) {
74
+ const channel = `${resolved.position.channel}Offset`;
75
+ const incompatible = channel === "xOffset" ? "yOffset" : "xOffset";
76
+ if (args[incompatible] !== undefined) {
77
+ throw new Error(
78
+ `createErrorBar ${incompatible} does not match the ${resolved.orientation} interval orientation.`
79
+ );
80
+ }
81
+ const explicit = args[channel] === undefined
82
+ ? undefined
83
+ : validateOptionObject(args[channel], OFFSET_OPTIONS, `createErrorBar ${channel}`);
84
+ const inferred = resolved.sourceLayer?.encoding?.[channel];
85
+ if (explicit === undefined && inferred === undefined) return undefined;
86
+ if (!["nominal", "ordinal"].includes(resolved.position.fieldType)) {
87
+ throw new Error(
88
+ `createErrorBar ${channel} requires a categorical ${resolved.position.channel} position.`
89
+ );
90
+ }
91
+ const field = explicit?.field ?? inferred?.field;
92
+ if (typeof field !== "string" || field.length === 0) {
93
+ throw new TypeError(`createErrorBar ${channel} field must be a non-empty string.`);
94
+ }
95
+ const fieldType = validateCategoricalFieldType(
96
+ explicit?.fieldType ?? inferred?.fieldType ?? "nominal"
97
+ );
98
+ const dataset = findDataset(program, resolved.source);
99
+ readNominalField(dataset.values, field);
100
+ const padding = normalizeOffsetPadding(
101
+ explicit ?? {},
102
+ resolved.sourceLayer === undefined
103
+ ? undefined
104
+ : program.markConfigs[resolved.sourceLayer.id]?.[channel],
105
+ channel
29
106
  );
30
- return resolveErrorBarAppearance(appearanceArgs, {
107
+ return {
108
+ channel,
109
+ field,
110
+ fieldType,
111
+ scale: {
112
+ ...(inferred?.scale === undefined ? {} : { id: inferred.scale }),
113
+ ...(explicit?.scale ?? {})
114
+ },
115
+ ...padding
116
+ };
117
+ }
118
+
119
+ function resolveAppearance(args) {
120
+ return resolveErrorBarAppearance(ownOptions(args, [
121
+ "caps", "capSize", "stroke", "strokeWidth", "strokeDash", "opacity"
122
+ ]), {
31
123
  defaults: {
32
124
  caps: true,
33
125
  capSize: 8,
@@ -40,6 +132,76 @@ function resolveAppearance(args) {
40
132
  });
41
133
  }
42
134
 
135
+ export const createErrorBarCap = action(
136
+ {
137
+ op: "createErrorBarCap",
138
+ description: "Create one fixed-pixel error-bar cap."
139
+ },
140
+ function (args = {}) {
141
+ validateKeys(args, [
142
+ "id", "data", "orientation", "positionField", "positionFieldType",
143
+ "intervalField", "coordinate", "positionScale", "intervalScale",
144
+ "offsetChannel", "offsetField", "offsetFieldType", "offsetScale",
145
+ "offsetPaddingInner", "offsetPaddingOuter",
146
+ "capSize", "stroke", "strokeWidth", "strokeDash", "opacity"
147
+ ], "createErrorBarCap");
148
+ if (!["vertical", "horizontal"].includes(args.orientation)) {
149
+ throw new Error(`Unsupported error-bar orientation "${args.orientation}".`);
150
+ }
151
+ const vertical = args.orientation === "vertical";
152
+ const expectedOffset = vertical ? "xOffset" : "yOffset";
153
+ if (
154
+ args.offsetChannel !== undefined &&
155
+ args.offsetChannel !== expectedOffset
156
+ ) {
157
+ throw new Error(
158
+ `createErrorBarCap requires ${expectedOffset} for ${args.orientation} intervals.`
159
+ );
160
+ }
161
+ const positionAction = vertical ? "encodeX" : "encodeY";
162
+ const intervalAction = vertical ? "encodeY" : "encodeX";
163
+ let next = this
164
+ .createRuleMark({ id: args.id, data: args.data })
165
+ [positionAction]({
166
+ target: args.id,
167
+ field: args.positionField,
168
+ fieldType: args.positionFieldType,
169
+ coordinate: args.coordinate,
170
+ scale: { id: args.positionScale }
171
+ })
172
+ [intervalAction]({
173
+ target: args.id,
174
+ field: args.intervalField,
175
+ fieldType: "quantitative",
176
+ coordinate: args.coordinate,
177
+ scale: { id: args.intervalScale }
178
+ });
179
+ if (args.offsetChannel !== undefined) {
180
+ const offsetAction = args.offsetChannel === "xOffset"
181
+ ? "encodeXOffset"
182
+ : "encodeYOffset";
183
+ next = next[offsetAction]({
184
+ target: args.id,
185
+ field: args.offsetField,
186
+ fieldType: args.offsetFieldType,
187
+ scale: { id: args.offsetScale },
188
+ paddingInner: args.offsetPaddingInner,
189
+ paddingOuter: args.offsetPaddingOuter
190
+ });
191
+ }
192
+ return next
193
+ .encodeStroke({ target: args.id, value: args.stroke })
194
+ .encodeStrokeWidth({ target: args.id, value: args.strokeWidth })
195
+ .encodeStrokeDash({ target: args.id, value: args.strokeDash })
196
+ .encodeOpacity({ target: args.id, value: args.opacity })
197
+ .materializeRuleSpan({
198
+ id: args.id,
199
+ orientation: vertical ? "horizontal" : "vertical",
200
+ size: args.capSize
201
+ });
202
+ }
203
+ );
204
+
43
205
  function positionArgs(resolved) {
44
206
  return {
45
207
  target: resolved.id,
@@ -50,6 +212,19 @@ function positionArgs(resolved) {
50
212
  };
51
213
  }
52
214
 
215
+ function offsetArgs(resolved) {
216
+ return resolved.offset === undefined
217
+ ? undefined
218
+ : {
219
+ target: resolved.id,
220
+ field: resolved.offset.field,
221
+ fieldType: resolved.offset.fieldType,
222
+ scale: resolved.offset.scale,
223
+ paddingInner: resolved.offset.paddingInner,
224
+ paddingOuter: resolved.offset.paddingOuter
225
+ };
226
+ }
227
+
53
228
  function intervalArgs(resolved, field) {
54
229
  return {
55
230
  target: resolved.id,
@@ -69,21 +244,7 @@ export const createErrorBar = action(
69
244
  validateKeys(args, OPTIONS, "createErrorBar");
70
245
  const resolved = resolveErrorBar(this, args);
71
246
  const appearance = resolveAppearance(args);
72
- let next = this;
73
-
74
- if (resolved.interval.mode === "statistical") {
75
- next = next.createIntervalData({
76
- id: resolved.dataId,
77
- source: resolved.source,
78
- field: resolved.interval.field,
79
- groupBy: resolved.groupBy,
80
- center: resolved.interval.center,
81
- extent: resolved.interval.extent,
82
- level: resolved.interval.level,
83
- as: resolved.fields
84
- });
85
- }
86
-
247
+ let next = createResolvedIntervalData(this, resolved);
87
248
  next = next.createRuleMark({ id: resolved.id, data: resolved.dataId });
88
249
  const positionAction = resolved.position.channel === "x" ? "encodeX" : "encodeY";
89
250
  const intervalAction = resolved.interval.channel === "x" ? "encodeX" : "encodeY";
@@ -100,18 +261,21 @@ export const createErrorBar = action(
100
261
  target: resolved.id,
101
262
  field: resolved.fields.upper,
102
263
  fieldType: "quantitative"
103
- })
104
- .encodeStroke({ target: resolved.id, value: appearance.stroke })
264
+ });
265
+ if (resolved.offset !== undefined) {
266
+ const offsetAction = resolved.offset.channel === "xOffset"
267
+ ? "encodeXOffset"
268
+ : "encodeYOffset";
269
+ next = next[offsetAction](offsetArgs(resolved));
270
+ }
271
+ next = next.encodeStroke({ target: resolved.id, value: appearance.stroke })
105
272
  .encodeStrokeWidth({ target: resolved.id, value: appearance.strokeWidth })
106
273
  .encodeStrokeDash({ target: resolved.id, value: appearance.strokeDash })
107
274
  .encodeOpacity({ target: resolved.id, value: appearance.opacity });
108
275
 
109
276
  const intervalLayer = findLayer(next, resolved.id);
110
277
  if (appearance.caps) {
111
- for (const [id, field] of [
112
- [resolved.lowerCapId, resolved.fields.lower],
113
- [resolved.upperCapId, resolved.fields.upper]
114
- ]) {
278
+ for (const [id, field] of errorBarCaps(resolved)) {
115
279
  next = next.createErrorBarCap({
116
280
  id,
117
281
  data: resolved.dataId,
@@ -122,6 +286,16 @@ export const createErrorBar = action(
122
286
  coordinate: resolved.coordinate,
123
287
  positionScale: intervalLayer.encoding[resolved.position.channel].scale,
124
288
  intervalScale: intervalLayer.encoding[resolved.interval.channel].scale,
289
+ ...(resolved.offset === undefined
290
+ ? {}
291
+ : {
292
+ offsetChannel: resolved.offset.channel,
293
+ offsetField: resolved.offset.field,
294
+ offsetFieldType: resolved.offset.fieldType,
295
+ offsetScale: intervalLayer.encoding[resolved.offset.channel].scale,
296
+ offsetPaddingInner: resolved.offset.paddingInner,
297
+ offsetPaddingOuter: resolved.offset.paddingOuter
298
+ }),
125
299
  capSize: appearance.capSize,
126
300
  stroke: appearance.stroke,
127
301
  strokeWidth: appearance.strokeWidth,
@@ -142,6 +316,14 @@ export const createErrorBar = action(
142
316
  coordinate: resolved.coordinate,
143
317
  positionScale: intervalLayer.encoding[resolved.position.channel].scale,
144
318
  intervalScale: intervalLayer.encoding[resolved.interval.channel].scale,
319
+ ...(resolved.offset === undefined
320
+ ? {}
321
+ : {
322
+ offset: {
323
+ ...resolved.offset,
324
+ scale: intervalLayer.encoding[resolved.offset.channel].scale
325
+ }
326
+ }),
145
327
  lowerCapId: resolved.lowerCapId,
146
328
  upperCapId: resolved.upperCapId,
147
329
  ...appearance