ggaction 0.0.17 → 0.0.18

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 (263) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +2 -2
  3. package/knowledge/action-card.schema.json +1 -1
  4. package/knowledge/action-cards.json +1 -1
  5. package/knowledge/intent-taxonomy.json +1 -1
  6. package/knowledge/mcp-resources.json +1 -1
  7. package/knowledge/task-resolver.js +1 -1
  8. package/package.json +3 -3
  9. package/src/BasicChartProgram.js +1 -1
  10. package/src/accessibility.js +3 -2
  11. package/src/actions/boxPlots/create.js +14 -4
  12. package/src/actions/boxPlots/edit.js +39 -33
  13. package/src/actions/boxPlots/materialize.js +7 -3
  14. package/src/actions/boxPlots/options.js +37 -11
  15. package/src/actions/canvas/actions.js +70 -95
  16. package/src/actions/canvas/fitting.js +16 -1
  17. package/src/actions/canvas/index.js +3 -0
  18. package/src/actions/canvas/plotBorder.js +28 -0
  19. package/src/actions/charts/area.js +2 -2
  20. package/src/actions/charts/bar.js +34 -7
  21. package/src/actions/charts/ecdf.js +4 -18
  22. package/src/actions/charts/endpoints.js +4 -18
  23. package/src/actions/charts/heatmap.js +4 -4
  24. package/src/actions/charts/index.js +2 -0
  25. package/src/actions/charts/interval-regression.js +2 -2
  26. package/src/actions/charts/line.js +1 -1
  27. package/src/actions/charts/pie.js +3 -47
  28. package/src/actions/charts/radial.js +34 -12
  29. package/src/actions/charts/scatter.js +16 -2
  30. package/src/actions/charts/shared.js +2 -2
  31. package/src/actions/charts/text.js +35 -0
  32. package/src/actions/composition/actions.js +6 -2
  33. package/src/actions/data/box.js +2 -1
  34. package/src/actions/data/edit.js +1 -1
  35. package/src/actions/data/regression.js +2 -1
  36. package/src/actions/data/shared.js +3 -2
  37. package/src/actions/data/stack.js +3 -1
  38. package/src/actions/encodings/appearance.js +37 -29
  39. package/src/actions/encodings/barWidth.js +36 -11
  40. package/src/actions/encodings/channels.js +0 -1
  41. package/src/actions/encodings/color/continuous.js +1 -1
  42. package/src/actions/encodings/color/index.js +1 -1
  43. package/src/actions/encodings/color/policy.js +10 -3
  44. package/src/actions/encodings/density/resolve.js +1 -6
  45. package/src/actions/encodings/position/policies/arc.js +12 -10
  46. package/src/actions/encodings/position/policies/area.js +3 -3
  47. package/src/actions/encodings/position/policies/bar.js +15 -6
  48. package/src/actions/encodings/position/policies/line.js +17 -6
  49. package/src/actions/encodings/position/resolve.js +3 -2
  50. package/src/actions/encodings/ranged.js +17 -0
  51. package/src/actions/encodings/remove.js +3 -2
  52. package/src/actions/encodings/seriesLayout.js +1 -0
  53. package/src/actions/encodings/text.js +6 -3
  54. package/src/actions/errorBands/create.js +2 -2
  55. package/src/actions/errorBands/edit.js +2 -1
  56. package/src/actions/facets/actions.js +158 -240
  57. package/src/actions/gradientPlots/resolve.js +2 -17
  58. package/src/actions/guides/applicability.js +1 -1
  59. package/src/actions/guides/axes/axis.js +16 -4
  60. package/src/actions/guides/axes/edit.js +4 -4
  61. package/src/actions/guides/axes/labels.js +120 -46
  62. package/src/actions/guides/axes/lines.js +6 -5
  63. package/src/actions/guides/axes/parallel/lifecycle.js +1 -1
  64. package/src/actions/guides/axes/parallel/policy.js +8 -7
  65. package/src/actions/guides/axes/remove.js +3 -2
  66. package/src/actions/guides/axes/tickGroups.js +5 -2
  67. package/src/actions/guides/axes/ticks.js +6 -3
  68. package/src/actions/guides/axes/titles.js +71 -26
  69. package/src/actions/guides/basic.js +2 -0
  70. package/src/actions/guides/facadeLegend.js +10 -2
  71. package/src/actions/guides/grids/grid.js +7 -1
  72. package/src/actions/guides/grids/resolve.js +59 -12
  73. package/src/actions/guides/legends/blocks.js +3 -1
  74. package/src/actions/guides/legends/categorical/actions.js +70 -17
  75. package/src/actions/guides/legends/categorical/components.js +12 -8
  76. package/src/actions/guides/legends/categorical/index.js +6 -0
  77. package/src/actions/guides/legends/categorical/layout.js +37 -8
  78. package/src/actions/guides/legends/categorical/options.js +31 -20
  79. package/src/actions/guides/legends/categorical/resolve.js +2 -2
  80. package/src/actions/guides/legends/categorical/symbols.js +18 -12
  81. package/src/actions/guides/legends/categorical/validation.js +1 -6
  82. package/src/actions/guides/legends/continuous/colorSymbols.js +1 -0
  83. package/src/actions/guides/legends/continuous/common.js +1 -447
  84. package/src/actions/guides/legends/continuous/gradient.js +47 -34
  85. package/src/actions/guides/legends/continuous/interval.js +4 -4
  86. package/src/actions/guides/legends/continuous/opacity.js +19 -12
  87. package/src/actions/guides/legends/edit.js +1 -604
  88. package/src/actions/guides/legends/focused.js +2 -2
  89. package/src/actions/guides/legends/group.js +1 -0
  90. package/src/actions/guides/legends/lane.js +1 -516
  91. package/src/actions/guides/legends/remove.js +3 -0
  92. package/src/actions/guides/legends/sampling.js +1 -1
  93. package/src/actions/guides/legends/size.js +51 -43
  94. package/src/actions/guides/legends/strokeWidth.js +11 -29
  95. package/src/actions/guides/legends/target.js +3 -3
  96. package/src/actions/guides/legends/transition.js +21 -7
  97. package/src/actions/guides/polar/axes/labels.js +4 -2
  98. package/src/actions/guides/polar/axes/titles.js +7 -5
  99. package/src/actions/guides/polar/resolve.js +6 -14
  100. package/src/actions/index.js +16 -16
  101. package/src/actions/marks/arc/actions.js +3 -10
  102. package/src/actions/marks/area/actions.js +8 -5
  103. package/src/actions/marks/area/materialize.js +8 -9
  104. package/src/actions/marks/bar/create.js +10 -4
  105. package/src/actions/marks/bar/edit.js +8 -4
  106. package/src/actions/marks/bar/materialize.js +9 -6
  107. package/src/actions/marks/line/actions.js +11 -4
  108. package/src/actions/marks/line/materialize.js +9 -5
  109. package/src/actions/marks/point/edit.js +1 -1
  110. package/src/actions/marks/rect/actions.js +3 -3
  111. package/src/actions/marks/remove.js +2 -0
  112. package/src/actions/marks/rule/actions.js +7 -4
  113. package/src/actions/marks/text/actions.js +17 -5
  114. package/src/actions/marks/text/layout.js +3 -0
  115. package/src/actions/primitives/semanticValidation/layer.js +4 -0
  116. package/src/actions/regression/create.js +1 -1
  117. package/src/actions/regression/edit.js +3 -3
  118. package/src/actions/regression/resolve.js +2 -6
  119. package/src/actions/scales/channels.js +9 -4
  120. package/src/actions/scales/consumers/families.js +16 -5
  121. package/src/actions/scales/create.js +8 -3
  122. package/src/actions/scales/definitions.js +1 -425
  123. package/src/actions/scales/edit.js +2 -2
  124. package/src/actions/scales/editPolicy.js +9 -3
  125. package/src/actions/scales/materialize.js +1 -1
  126. package/src/actions/scales/preview.js +21 -1
  127. package/src/actions/scales/quantitativeColor.js +3 -3
  128. package/src/actions/selection/actions.js +1 -13
  129. package/src/actions/theme/reconcile.js +81 -27
  130. package/src/actions/theme/state.js +12 -0
  131. package/src/actions/titles/actions.js +5 -18
  132. package/src/actions/titles/resolve.js +41 -19
  133. package/src/actions/violinPlots/edit.js +3 -20
  134. package/src/core/compositionState.js +4 -1
  135. package/src/core/font.js +9 -2
  136. package/src/core/optionRequirements.js +1 -0
  137. package/src/core/resourceReferences.js +3 -0
  138. package/src/core/textMetricProfile.js +3 -2
  139. package/src/core/textMetrics.js +41 -4
  140. package/src/core/validation.js +26 -0
  141. package/src/core/vocabulary.js +4 -0
  142. package/src/grammar/aggregate.js +4 -18
  143. package/src/grammar/arcs.js +1 -386
  144. package/src/grammar/areaPaint.js +10 -0
  145. package/src/grammar/areaSeries.js +18 -10
  146. package/src/grammar/bars/geometry.js +30 -3
  147. package/src/grammar/bars/policy.js +12 -1
  148. package/src/grammar/bin.js +2 -12
  149. package/src/grammar/bin2d.js +1 -6
  150. package/src/grammar/boxPlot.js +56 -9
  151. package/src/grammar/categoryOrder.js +1 -6
  152. package/src/grammar/complete.js +15 -19
  153. package/src/grammar/computed.js +92 -25
  154. package/src/grammar/curveCommands.js +29 -16
  155. package/src/grammar/datasetSchema.js +3 -7
  156. package/src/grammar/displayLabels.js +28 -0
  157. package/src/grammar/ecdf.js +2 -9
  158. package/src/grammar/facets/dependencies.js +6 -2
  159. package/src/grammar/facets/guides.js +5 -0
  160. package/src/grammar/facets/index.js +2 -5
  161. package/src/grammar/fold.js +2 -12
  162. package/src/grammar/gradientProfile.js +1 -6
  163. package/src/grammar/impute.js +21 -48
  164. package/src/grammar/interval.js +2 -9
  165. package/src/grammar/legendGradient.js +18 -0
  166. package/src/grammar/legendGroups.js +1 -0
  167. package/src/grammar/legendTitleStyle.js +22 -0
  168. package/src/grammar/lineSeries.js +32 -17
  169. package/src/grammar/markLabels.js +5 -2
  170. package/src/grammar/normalize.js +4 -54
  171. package/src/grammar/numeric.js +1 -290
  172. package/src/grammar/parallelCoordinates.js +1 -6
  173. package/src/grammar/parseTime.js +41 -0
  174. package/src/grammar/pathCommands.js +11 -4
  175. package/src/grammar/polar.js +1 -184
  176. package/src/grammar/positionCompatibility.js +3 -3
  177. package/src/grammar/regression/derive.js +7 -1
  178. package/src/grammar/regression/models.js +83 -55
  179. package/src/grammar/regression/parameters.js +14 -3
  180. package/src/grammar/roundedRect.js +42 -54
  181. package/src/grammar/scales/appearance.js +1 -206
  182. package/src/grammar/scales/color.js +1 -382
  183. package/src/grammar/scales/colorConsumers.js +5 -2
  184. package/src/grammar/scales/definition.js +1 -197
  185. package/src/grammar/scales/discretized.js +1 -195
  186. package/src/grammar/scales/size.js +1 -456
  187. package/src/grammar/scales/temporal.js +2 -13
  188. package/src/grammar/scales/transformed.js +1 -383
  189. package/src/grammar/scales/types.js +1 -1
  190. package/src/grammar/scales/validation.js +1 -66
  191. package/src/grammar/schemas/concreteGraphic.js +11 -0
  192. package/src/grammar/schemas/graphic.js +3 -0
  193. package/src/grammar/schemas/semanticPath.js +2 -1
  194. package/src/grammar/stack.js +2 -10
  195. package/src/grammar/statisticalReference.js +1 -6
  196. package/src/grammar/summary.js +3 -26
  197. package/src/grammar/text.js +19 -0
  198. package/src/grammar/timeUnit.js +16 -7
  199. package/src/grammar/transformKeys.js +47 -0
  200. package/src/grammar/transforms.js +4 -1
  201. package/src/grammar/valueFormat.js +17 -5
  202. package/src/grammar/weightedStatistics.js +1 -6
  203. package/src/grammar/window.js +4 -58
  204. package/src/inspectionDescriptors.js +1 -1
  205. package/src/layout/canvas.js +1 -110
  206. package/src/layout/facets.js +1 -244
  207. package/src/layout/labels.js +1 -0
  208. package/src/layout/legend.js +13 -0
  209. package/src/layout/legendGroup.js +1 -0
  210. package/src/layout/legendItems.js +59 -15
  211. package/src/layout/legendLane.js +1 -441
  212. package/src/layout/legendText.js +1 -0
  213. package/src/layout/title.js +35 -13
  214. package/src/materialization/bars/aggregate.js +11 -5
  215. package/src/materialization/bars/histogram.js +4 -1
  216. package/src/materialization/bars/ranged.js +19 -6
  217. package/src/materialization/bars/resolve.js +22 -1
  218. package/src/materialization/coordinateBounds.js +8 -0
  219. package/src/materialization/dependencies.js +4 -44
  220. package/src/materialization/facetGuides/layout.js +1 -0
  221. package/src/materialization/facetGuides/placement.js +19 -8
  222. package/src/materialization/facetGuides/preparation.js +15 -7
  223. package/src/materialization/facetHeaders.js +1 -347
  224. package/src/materialization/facets.js +1 -185
  225. package/src/materialization/graphicHierarchy.js +1 -1
  226. package/src/materialization/guides/layout.js +1 -57
  227. package/src/materialization/guides/resources.js +1 -123
  228. package/src/materialization/layout.js +53 -0
  229. package/src/materialization/legendGroups.js +1 -0
  230. package/src/materialization/legendText.js +1 -0
  231. package/src/materialization/legends.js +1 -1
  232. package/src/materialization/marks/capabilities.js +5 -6
  233. package/src/materialization/rect.js +1 -1
  234. package/src/materialization/scaleGuideDependencies.js +1 -1
  235. package/src/materialization/scales/map.js +3 -0
  236. package/src/materialization/scales/policies/arc.js +8 -7
  237. package/src/materialization/scales/policies/bar.js +19 -7
  238. package/src/materialization/scales/policies/series.js +2 -1
  239. package/src/materialization/scales/resolve.js +5 -3
  240. package/src/materialization/selection/items/arc.js +1 -1
  241. package/src/materialization/selection/items/bar.js +7 -6
  242. package/src/materialization/selection/items/path.js +7 -6
  243. package/src/materialization/selection/items/rect.js +2 -2
  244. package/src/materialization/text.js +13 -4
  245. package/src/materialization/textLines.js +15 -0
  246. package/src/persistence/codec.js +37 -6
  247. package/src/persistence/validation.js +47 -0
  248. package/src/persistence.js +18 -5
  249. package/src/renderers/canvas/circle.js +6 -3
  250. package/src/renderers/canvas/fill.js +7 -1
  251. package/src/renderers/canvas/index.js +2 -1
  252. package/src/renderers/canvas/line.js +1 -0
  253. package/src/renderers/canvas/path.js +1 -1
  254. package/src/renderers/canvas/rect.js +11 -8
  255. package/src/renderers/canvas/stroke.js +2 -1
  256. package/src/renderers/canvas/text.js +5 -3
  257. package/src/renderers/svg.js +4 -1
  258. package/src/selectors/layers.js +19 -0
  259. package/src/theme/defaults.js +8 -2
  260. package/src/version.js +1 -1
  261. package/types/basic.d.ts +1 -0
  262. package/types/index.d.ts +7 -0
  263. package/types/program.d.ts +347 -85
@@ -1,3 +1,5 @@
1
+ import { validateFillPaint } from "../../../grammar/paint.js";
2
+ import { validateAreaPaintConfig } from "../../../grammar/areaPaint.js";
1
3
  import { validateAreaMissing } from "../../../grammar/areaEndpoints.js";
2
4
  import { action } from "../../../core/action.js";
3
5
  import { validateUserId } from "../../../core/identifiers.js";
@@ -81,7 +83,7 @@ const createAreaMark = /* @__PURE__ */ action(
81
83
  if (missing === "break" && ["density", "horizon"].some(type => findUpstreamTransform(this, findDataset(this, data), type))) {
82
84
  throw new Error("Area missing:break does not reinterpret density or horizon data.");
83
85
  }
84
- const fill = validateNonEmptyString(args.fill ?? DEFAULT_COLORS.mark, "Area fill");
86
+ const fill = validateFillPaint(args.fill ?? DEFAULT_COLORS.mark, "Area fill");
85
87
  const opacity = validateUnitInterval(args.opacity ?? 0.2, "Area opacity");
86
88
  const curve = validateCurveInterpolation(args.curve ?? "linear");
87
89
  const { stroke, strokeWidth } = validateAreaCreateOutline(args);
@@ -127,14 +129,14 @@ const configureAreaStrokeFromFill = /* @__PURE__ */ action(
127
129
  }
128
130
  const { stroke: _stroke, ...config } = this.markConfigs[id] ?? {};
129
131
  void _stroke;
130
- return this._withMarkConfig(id, {
132
+ return this._withMarkConfig(id, validateAreaPaintConfig({
131
133
  ...config,
132
134
  strokeFromFill: true,
133
135
  strokeWidth: validateNonNegativeFinite(
134
136
  args.strokeWidth ?? 1,
135
137
  "Area strokeWidth"
136
138
  )
137
- });
139
+ }));
138
140
  }
139
141
  );
140
142
 
@@ -199,7 +201,7 @@ const rematerializeAreaMark = /* @__PURE__ */ action(
199
201
  if (args.scales !== false && strokeEncoding?.scale !== undefined) {
200
202
  resolved = resolved.rematerializeScale({ id: strokeEncoding.scale });
201
203
  }
202
- const config = this.markConfigs[id];
204
+ const config = validateAreaPaintConfig(this.markConfigs[id]);
203
205
  const { paths, fills, strokes } = resolveAreaMaterialization({
204
206
  rows: dataset.values,
205
207
  layer,
@@ -273,7 +275,7 @@ const editAreaMark = /* @__PURE__ */ action(
273
275
  let config = { ...this.markConfigs[layer.id] };
274
276
  Object.assign(config, strokeDetails);
275
277
  if (Object.hasOwn(args, "fill")) {
276
- config.fill = validateNonEmptyString(args.fill, "Area fill");
278
+ config.fill = validateFillPaint(args.fill, "Area fill");
277
279
  if (config.errorBand !== undefined) config.errorBand = { ...config.errorBand, fill: config.fill };
278
280
  }
279
281
  if (Object.hasOwn(args, "opacity")) {
@@ -314,6 +316,7 @@ const editAreaMark = /* @__PURE__ */ action(
314
316
  );
315
317
  }
316
318
 
319
+ validateAreaPaintConfig(config);
317
320
  let next = this._withMarkConfig(layer.id, config);
318
321
  if (Object.hasOwn(args, "missing")) {
319
322
  const missing = validateAreaMissing(args.missing);
@@ -6,6 +6,7 @@ import {
6
6
  } from "../../../grammar/areaSeries.js";
7
7
  import {
8
8
  mapContinuousScaleValues,
9
+ mapOrdinalPositionValues,
9
10
  mapOrdinalValues
10
11
  } from "../../../grammar/scales/index.js";
11
12
  import { mapScaleConsumerValues } from
@@ -37,9 +38,8 @@ function resolveAreaPaths({
37
38
  }
38
39
  return derived.series.map(series => {
39
40
  if (densityTransform === undefined && derived.orientation === "horizontal") {
40
- const y = mapContinuousScaleValues(
41
- series.values.map(value => value.y),
42
- yScale
41
+ const y = (["point", "band"].includes(yScale.type) ? mapOrdinalPositionValues : mapContinuousScaleValues)(
42
+ series.values.map(value => value.y), yScale
43
43
  );
44
44
  const lower = mapContinuousScaleValues(
45
45
  series.values.map(value => value.x),
@@ -57,9 +57,8 @@ function resolveAreaPaths({
57
57
  );
58
58
  }
59
59
  if (densityTransform !== undefined && derived.mode === "x-density") {
60
- const y = mapContinuousScaleValues(
61
- series.values.map(value => value.y),
62
- yScale
60
+ const y = (["point", "band"].includes(yScale.type) ? mapOrdinalPositionValues : mapContinuousScaleValues)(
61
+ series.values.map(value => value.y), yScale
63
62
  );
64
63
  const upper = mapContinuousScaleValues(
65
64
  series.values.map(value => value.x),
@@ -80,7 +79,7 @@ function resolveAreaPaths({
80
79
  { independentAxis: "y" }
81
80
  );
82
81
  }
83
- const x = mapContinuousScaleValues(
82
+ const x = (["point", "band"].includes(xScale.type) ? mapOrdinalPositionValues : mapContinuousScaleValues)(
84
83
  series.values.map(value => value.x),
85
84
  xScale
86
85
  );
@@ -134,8 +133,8 @@ export function resolveAreaMaterialization({
134
133
  layer.encoding?.y?.stack === "center";
135
134
  const rawDerived = densityTransform === undefined
136
135
  ? centered
137
- ? deriveCenteredAreaSeries(rows, layer)
138
- : deriveAreaSeries(rows, layer)
136
+ ? deriveCenteredAreaSeries(rows, layer, { xDomain: xScale.domain })
137
+ : deriveAreaSeries(rows, layer, { xDomain: xScale.domain, yDomain: yScale.domain })
139
138
  : deriveDensityAreaSeries(rows, layer, densityTransform);
140
139
  const colorEncoding = layer.encoding?.color;
141
140
  const layout = layer.layout?.mode ?? colorEncoding?.layout ?? "overlay";
@@ -10,15 +10,15 @@ import {
10
10
  } from "../shared.js";
11
11
  import { resolveMarkGraphicPlacement } from
12
12
  "../../../materialization/graphicHierarchy.js";
13
- import { requestedRectStyleDetails } from
13
+ import { requestedRectStyleDetails, RECT_RADIUS_PROPERTIES } from
14
14
  "../../../grammar/roundedRect.js";
15
15
  import { STROKE_STYLE_PROPERTIES } from
16
16
  "../../../grammar/strokeStyle.js";
17
17
  import { validateItemMissing } from "../../../grammar/itemMissing.js";
18
18
 
19
19
  const CREATE_OPTIONS = Object.freeze([
20
- "id", "data", "missing", "fill", "opacity", "stroke", "strokeWidth",
21
- "cornerRadius", ...STROKE_STYLE_PROPERTIES
20
+ "id", "data", "orientation", "missing", "fill", "opacity", "stroke", "strokeWidth",
21
+ ...RECT_RADIUS_PROPERTIES, ...STROKE_STYLE_PROPERTIES
22
22
  ]);
23
23
 
24
24
  export const createBarMark = /* @__PURE__ */ action(
@@ -29,6 +29,9 @@ export const createBarMark = /* @__PURE__ */ action(
29
29
  function (args = {}) {
30
30
  validateMarkOptions(args, CREATE_OPTIONS, "createBarMark");
31
31
  requestedRectStyleDetails(args, "createBarMark");
32
+ if (args.orientation !== undefined && !["vertical", "horizontal"].includes(args.orientation)) {
33
+ throw new Error("Bar orientation must be vertical or horizontal.");
34
+ }
32
35
  const id = resolveMarkId(this, args.id, {
33
36
  defaultId: "bar",
34
37
  label: "Bar mark id",
@@ -59,6 +62,9 @@ export const createBarMark = /* @__PURE__ */ action(
59
62
  value: validateItemMissing(args.missing, "Bar missing")
60
63
  });
61
64
  }
65
+ if (args.orientation !== undefined) {
66
+ created = created.editSemantic({ property: `layer[${id}].mark.orientation`, value: args.orientation });
67
+ }
62
68
  created = applyLayeredMarkInheritance(created, id, inherited);
63
69
  created = created
64
70
  .createGraphics({
@@ -70,7 +76,7 @@ export const createBarMark = /* @__PURE__ */ action(
70
76
  created = materializeInheritedMark(created, id);
71
77
  const appearance = Object.fromEntries(
72
78
  [
73
- "fill", "opacity", "stroke", "strokeWidth", "cornerRadius",
79
+ "fill", "opacity", "stroke", "strokeWidth", ...RECT_RADIUS_PROPERTIES,
74
80
  ...STROKE_STYLE_PROPERTIES
75
81
  ]
76
82
  .filter(property => Object.hasOwn(args, property))
@@ -9,15 +9,16 @@ import { canMaterializeBar } from "../../../materialization/marks/index.js";
9
9
  import { DEFAULT_BAR_STROKE_WIDTH } from "../../../materialization/bars/resolve.js";
10
10
  import { resolveEligibleLayer } from "../../../selectors/layers.js";
11
11
  import { validateMarkOptions } from "../shared.js";
12
- import { requestedRectStyleDetails } from
12
+ import { requestedRectStyleDetails, RECT_RADIUS_PROPERTIES } from
13
13
  "../../../grammar/roundedRect.js";
14
14
  import { STROKE_STYLE_PROPERTIES } from
15
15
  "../../../grammar/strokeStyle.js";
16
16
  import { rematerializeExistingLegend } from "../../encodings/shared.js";
17
+ import { validateFillPaint } from "../../../grammar/paint.js";
17
18
  import { validateItemMissing } from "../../../grammar/itemMissing.js";
18
19
 
19
20
  const EDIT_OPTIONS = Object.freeze([
20
- "target", "missing", "fill", "opacity", "stroke", "strokeWidth", "cornerRadius",
21
+ "target", "missing", "fill", "opacity", "stroke", "strokeWidth", ...RECT_RADIUS_PROPERTIES,
21
22
  ...STROKE_STYLE_PROPERTIES
22
23
  ]);
23
24
 
@@ -30,7 +31,7 @@ export const editBarMark = /* @__PURE__ */ action(
30
31
  validateMarkOptions(args, EDIT_OPTIONS, "editBarMark");
31
32
  const styleDetails = requestedRectStyleDetails(args, "editBarMark");
32
33
  const changes = [
33
- "missing", "fill", "opacity", "stroke", "strokeWidth", "cornerRadius",
34
+ "missing", "fill", "opacity", "stroke", "strokeWidth", ...RECT_RADIUS_PROPERTIES,
34
35
  ...STROKE_STYLE_PROPERTIES
35
36
  ];
36
37
  if (!changes.some(key => Object.hasOwn(args, key))) {
@@ -56,6 +57,9 @@ export const editBarMark = /* @__PURE__ */ action(
56
57
  "editBarMark stroke conflicts with a field encoding; use encodeStroke with value to replace it."
57
58
  );
58
59
  }
60
+ if (Object.hasOwn(args, "opacity") && layer.encoding?.opacity !== undefined) {
61
+ throw new Error("editBarMark opacity conflicts with a field encoding; use encodeOpacity with value to replace it.");
62
+ }
59
63
  if (args.stroke === false && Object.hasOwn(args, "strokeWidth")) {
60
64
  throw new Error(
61
65
  "editBarMark cannot set strokeWidth while removing stroke."
@@ -72,7 +76,7 @@ export const editBarMark = /* @__PURE__ */ action(
72
76
  const appearance = { ...config.barAppearance };
73
77
  Object.assign(appearance, styleDetails);
74
78
  if (Object.hasOwn(args, "fill")) {
75
- appearance.fill = validateNonEmptyString(args.fill, "Bar fill");
79
+ appearance.fill = validateFillPaint(args.fill, "Bar fill");
76
80
  }
77
81
  if (Object.hasOwn(args, "opacity")) {
78
82
  appearance.opacity = validateUnitInterval(args.opacity, "Bar opacity");
@@ -13,19 +13,17 @@ import {
13
13
  import { rematerializeHighlightBaseline } from "../lifecycle.js";
14
14
  import { offsetCategoryRectangles } from
15
15
  "../../../materialization/categorySlotOffset.js";
16
- import { materializeRectItem } from "../../../grammar/roundedRect.js";
17
- import { STROKE_STYLE_PROPERTIES } from
18
- "../../../grammar/strokeStyle.js";
16
+ import { materializeRectItem, RECT_STYLE_PROPERTIES } from "../../../grammar/roundedRect.js";
19
17
  import { replaceGraphicItems } from "../../primitives/editGraphics.js";
20
18
 
21
19
  const REMATERIALIZE_OPTIONS = Object.freeze(["id", "scales"]);
22
20
 
23
21
  function editRectangles(program, id, rectangles) {
24
22
  const appearance = program.markConfigs[id]?.barAppearance ?? {};
25
- const hasShapeStyle = ["cornerRadius", ...STROKE_STYLE_PROPERTIES]
23
+ const hasShapeStyle = RECT_STYLE_PROPERTIES
26
24
  .some(property => Object.hasOwn(appearance, property));
27
25
  if (hasShapeStyle) {
28
- const radius = appearance.cornerRadius ?? 0;
26
+ const radius = appearance;
29
27
  return replaceGraphicItems(
30
28
  program,
31
29
  id,
@@ -92,6 +90,11 @@ export const rematerializeBarMark = /* @__PURE__ */ action(
92
90
  resolved = resolved.rematerializeScale({ id: strokeScaleId });
93
91
  }
94
92
 
93
+ const opacityScaleId = required.layer.encoding?.opacity?.scale;
94
+ if (opacityScaleId !== undefined && args.scales !== false) {
95
+ resolved = resolved.rematerializeScale({ id: opacityScaleId });
96
+ }
97
+
95
98
  if (required.materialization === "aggregate") {
96
99
  const offsetChannel = resolveBarOffsetChannel(required.layer);
97
100
  const offsetScaleId = required.layer.encoding?.[offsetChannel]?.scale;
@@ -104,7 +107,7 @@ export const rematerializeBarMark = /* @__PURE__ */ action(
104
107
  ));
105
108
  }
106
109
 
107
- if (required.materialization === BAR_GRAINS.ranged) {
110
+ if ([BAR_GRAINS.ranged, BAR_GRAINS.centered].includes(required.materialization)) {
108
111
  const width = resolved.markConfigs[id]?.barWidth;
109
112
  return editRectangles(resolved, id, offsetCategoryRectangles(
110
113
  resolved, required.layer, deriveRangedRectangles(required, resolved, width)
@@ -18,7 +18,7 @@ import {
18
18
  import { DEFAULT_COLORS } from "../../../theme/defaults.js";
19
19
  import { findDataset } from "../../../selectors/datasets.js";
20
20
  import { findLayer, resolveEligibleLayer } from "../../../selectors/layers.js";
21
- import { validateCurveInterpolation } from "../../../grammar/curveCommands.js";
21
+ import { validateCurveInterpolation, validateCardinalTension } from "../../../grammar/curveCommands.js";
22
22
  import {
23
23
  resolveCoordinateBounds,
24
24
  resolveCoordinatePolarFrame
@@ -43,11 +43,11 @@ import {
43
43
  const DEFAULT_LINE_STROKE = DEFAULT_COLORS.mark;
44
44
  const DEFAULT_LINE_WIDTH = 2;
45
45
  const CREATE_OPTIONS = Object.freeze([
46
- "id", "data", "stroke", "strokeWidth", "opacity", "curve", "closed",
46
+ "id", "data", "stroke", "strokeWidth", "opacity", "curve", "tension", "closed",
47
47
  ...STROKE_STYLE_PROPERTIES
48
48
  ]);
49
49
  const EDIT_OPTIONS = Object.freeze([
50
- "target", "stroke", "strokeWidth", "opacity", "curve", "closed",
50
+ "target", "stroke", "strokeWidth", "opacity", "curve", "tension", "closed",
51
51
  ...STROKE_STYLE_PROPERTIES
52
52
  ]);
53
53
  const REMATERIALIZE_OPTIONS = Object.freeze(["id", "scales"]);
@@ -118,6 +118,7 @@ const createLineMark = /* @__PURE__ */ action(
118
118
  "Line strokeWidth"
119
119
  );
120
120
  const curve = validateCurveInterpolation(args.curve ?? "linear");
121
+ validateCardinalTension(args.tension, curve);
121
122
  const closed = Object.hasOwn(args, "closed")
122
123
  ? validateClosed(args.closed)
123
124
  : false;
@@ -151,6 +152,7 @@ const createLineMark = /* @__PURE__ */ action(
151
152
  {
152
153
  ...(Object.hasOwn(args, "strokeWidth") ? { strokeWidth } : {}),
153
154
  ...(Object.hasOwn(args, "curve") ? { curve } : {}),
155
+ ...(args.tension === undefined ? {} : { tension: args.tension }),
154
156
  ...(Object.hasOwn(args, "closed") ? { closed } : {}),
155
157
  ...strokeDetails
156
158
  }
@@ -277,7 +279,7 @@ const editLineMark = /* @__PURE__ */ action(
277
279
  validateMarkOptions(args, EDIT_OPTIONS, "editLineMark");
278
280
  const strokeDetails = requestedStrokeDetails(args, "editLineMark");
279
281
  if (!EDIT_OPTIONS.slice(1).some(key => Object.hasOwn(args, key))) {
280
- throw new Error("editLineMark requires stroke, strokeWidth, opacity, curve, or closed.");
282
+ throw new Error("editLineMark requires stroke, strokeWidth, opacity, curve, tension, or closed.");
281
283
  }
282
284
  const target = Object.hasOwn(args, "target")
283
285
  ? validateUserId(args.target, "Line mark id")
@@ -315,6 +317,11 @@ const editLineMark = /* @__PURE__ */ action(
315
317
  })) {
316
318
  if (Object.hasOwn(args, key)) config[key] = validate(args[key], `Line ${key}`);
317
319
  }
320
+ if (args.curve !== undefined && args.curve !== "cardinal" && args.tension === undefined) {
321
+ delete config.tension;
322
+ }
323
+ if (Object.hasOwn(args, "tension")) config.tension = args.tension;
324
+ validateCardinalTension(config.tension, config.curve ?? "linear");
318
325
  validatePolarLineConfig(layer, config);
319
326
  const next = this._withMarkConfig(layer.id, config);
320
327
  const materialized = canMaterializeLine(next, layer)
@@ -3,6 +3,7 @@ import {
3
3
  } from "../../../grammar/lineSeries.js";
4
4
  import {
5
5
  mapContinuousScaleValues,
6
+ mapOrdinalPositionValues,
6
7
  normalizeStrokeDashPattern
7
8
  } from "../../../grammar/scales/index.js";
8
9
  import { buildCurvePathCommands } from
@@ -97,9 +98,7 @@ export function resolvePositionedLineMaterialization({
97
98
  const radiusScaleId = layer.encoding?.radius?.scale;
98
99
  const lineOptions = polar
99
100
  ? { thetaDomain: resolvedScales[thetaScaleId].domain }
100
- : layer.encoding?.x?.bin === undefined
101
- ? undefined
102
- : { xBinBoundaries };
101
+ : { xBinBoundaries, xDomain: resolvedScales[xScaleId].domain };
103
102
  const derived = deriveLineSeries(rows, layer, lineOptions);
104
103
  const commands = polar
105
104
  ? derived.series.map(series => buildPolarLinePathCommands({
@@ -111,7 +110,9 @@ export function resolvePositionedLineMaterialization({
111
110
  closed: config.closed ?? false
112
111
  }))
113
112
  : derived.series.map(series => {
114
- const x = mapContinuousScaleValues(
113
+ const mapX = ["nominal", "ordinal"].includes(layer.encoding.x.fieldType)
114
+ ? mapOrdinalPositionValues : mapContinuousScaleValues;
115
+ const x = mapX(
115
116
  series.values.map(value => value.x),
116
117
  resolvedScales[xScaleId]
117
118
  );
@@ -119,9 +120,12 @@ export function resolvePositionedLineMaterialization({
119
120
  series.values.map(value => value.y),
120
121
  resolvedScales[yScaleId]
121
122
  );
123
+ // A raw singleton retains its data position and series identity, but
124
+ // contributes no segment. Do not fabricate another observation.
125
+ if (series.values.length === 1) return [{ op: "M", x: x[0], y: y[0] }];
122
126
  return buildCurvePathCommands(
123
127
  series.values.map((_, index) => ({ x: x[index], y: y[index] })),
124
- config.curve ?? "linear"
128
+ config.curve ?? "linear", config.tension
125
129
  );
126
130
  });
127
131
  const appearance = appearanceMapper(layer, resolvedScales,
@@ -96,7 +96,7 @@ export const editPointMark = /* @__PURE__ */ action(
96
96
  }
97
97
  }
98
98
  if (Object.hasOwn(args, "strokeWidth")) {
99
- if (typeof config.stroke !== "string") {
99
+ if (typeof config.stroke !== "string" && layer.encoding?.stroke?.field === undefined) {
100
100
  throw new Error("Point strokeWidth requires an active stroke.");
101
101
  }
102
102
  config.strokeWidth = validateNonNegativeFinite(
@@ -25,7 +25,7 @@ import { resolveMarkGraphicPlacement } from
25
25
  import { rematerializeExistingLegend } from "../../encodings/shared.js";
26
26
  import { rematerializeHighlightBaseline } from "../lifecycle.js";
27
27
  import {
28
- requestedRectStyleDetails
28
+ requestedRectStyleDetails, hasRectRadius, RECT_RADIUS_PROPERTIES
29
29
  } from "../../../grammar/roundedRect.js";
30
30
  import { STROKE_STYLE_PROPERTIES } from
31
31
  "../../../grammar/strokeStyle.js";
@@ -33,7 +33,7 @@ import { applyItemMissingPolicy, validateItemMissing } from
33
33
  "../../../grammar/itemMissing.js";
34
34
 
35
35
  const STYLE_OPTIONS = Object.freeze([
36
- "fill", "opacity", "stroke", "strokeWidth", "cornerRadius",
36
+ "fill", "opacity", "stroke", "strokeWidth", ...RECT_RADIUS_PROPERTIES,
37
37
  ...STROKE_STYLE_PROPERTIES
38
38
  ]);
39
39
  const CREATE_OPTIONS = Object.freeze(["id", "data", "missing", ...STYLE_OPTIONS]);
@@ -117,7 +117,7 @@ const rematerializeRectMark = /* @__PURE__ */ action(
117
117
  const graphic = this.graphicSpec.objects[id];
118
118
  if (layer?.mark?.type !== "rect") throw new Error(`Unknown rect mark "${id}".`);
119
119
  const roundedCollection = graphic?.type === "collection" &&
120
- Object.hasOwn(this.markConfigs[id] ?? {}, "cornerRadius") &&
120
+ hasRectRadius(this.markConfigs[id]) &&
121
121
  graphic.items?.every(item => ["rect", "path"].includes(item.type));
122
122
  if (
123
123
  (graphic?.type !== "rect" && !roundedCollection) ||
@@ -163,6 +163,7 @@ export function removeOwnedMark(program, id, partial = false) {
163
163
  next = next.editGraphics({ target: id, remove: true });
164
164
  }
165
165
  return next
166
+ ._withoutMaterializationConfig(["guides", "legendGroups", id])
166
167
  ._withoutMaterializationConfig(["marks", id])
167
168
  ._withContext({
168
169
  ...(program.context.currentMark === id ? { currentMark: undefined } : {}),
@@ -262,6 +263,7 @@ export const removeMark = /* @__PURE__ */ closedAction(
262
263
  if (next.graphicSpec.objects[id] !== undefined) {
263
264
  next = next.editGraphics({ target: id, remove: true });
264
265
  }
266
+ next = next._withoutMaterializationConfig(["guides", "legendGroups", id]);
265
267
  next = next._withoutMaterializationConfig(["marks", id]);
266
268
  next = next._withoutMaterializationConfig(["jitters", id]);
267
269
  next = next._withoutMaterializationConfig(["pointPacking", id]);
@@ -298,13 +298,16 @@ const rematerializeRuleMark = /* @__PURE__ */ action(
298
298
  properties: item?.properties
299
299
  });
300
300
  if (box === undefined) throw new Error(`Rule mark "${id}" requires box span owner "${boxSpan}".`);
301
+ const pixels = resolved.markConfigs[boxSpan]?.boxPlot?.median?.width?.pixels;
301
302
  if (layer.encoding?.x?.fieldType === "quantitative") {
302
303
  startX = endX = mapped.x[index];
303
- startY = box.y;
304
- endY = box.y + box.height;
304
+ const height = pixels ?? box.height;
305
+ startY = box.y + (box.height - height) / 2;
306
+ endY = startY + height;
305
307
  } else {
306
- startX = box.x;
307
- endX = box.x + box.width;
308
+ const width = pixels ?? box.width;
309
+ startX = box.x + (box.width - width) / 2;
310
+ endX = startX + width;
308
311
  startY = endY = mapped.y[index];
309
312
  }
310
313
  } else if (mode === "fixed-span") {
@@ -3,6 +3,7 @@ import { action } from "../../../core/action.js";
3
3
  import { validateUserId } from "../../../core/identifiers.js";
4
4
  import {
5
5
  DEFAULT_TEXT_MARK,
6
+ TEXT_MARK_STYLE_OPTIONS as STYLE_OPTIONS,
6
7
  isSourceOwnedText,
7
8
  normalizeTextMarkConfig
8
9
  } from "../../../grammar/text.js";
@@ -41,10 +42,6 @@ import { resolveMarkGraphicPlacement } from
41
42
  "../../../materialization/graphicHierarchy.js";
42
43
  import { validateItemMissing } from "../../../grammar/itemMissing.js";
43
44
 
44
- const STYLE_OPTIONS = Object.freeze([
45
- "fill", "opacity", "fontSize", "fontFamily", "fontWeight",
46
- "align", "baseline", "rotation", "dx", "dy"
47
- ]);
48
45
  const CREATE_OPTIONS = Object.freeze(["id", "data", "source", "text", "missing", ...STYLE_OPTIONS]);
49
46
  const EDIT_OPTIONS = Object.freeze(["target", "missing", ...STYLE_OPTIONS]);
50
47
  const REMATERIALIZE_OPTIONS = Object.freeze(["id", "replayLayout"]);
@@ -195,7 +192,7 @@ const LABEL_OPTIONS = Object.freeze([
195
192
  ]);
196
193
 
197
194
  const ANNOTATION_OPTIONS = Object.freeze([
198
- "id", "text", "format", "x", "y", "space", "source", "data", "coordinate", "layout",
195
+ "id", "text", "format", "anchor", "x", "y", "space", "source", "data", "coordinate", "layout",
199
196
  ...STYLE_OPTIONS
200
197
  ]);
201
198
 
@@ -271,6 +268,15 @@ const createAnnotation = /* @__PURE__ */ action(
271
268
  },
272
269
  function (args = {}) {
273
270
  validateMarkOptions(args, ANNOTATION_OPTIONS, "createAnnotation");
271
+ if (Object.hasOwn(args, "anchor")) {
272
+ if (args.anchor !== "plot-center") {
273
+ throw new Error('createAnnotation anchor must be "plot-center".');
274
+ }
275
+ if (["x", "y", "space", "source", "align", "baseline"].some(key => Object.hasOwn(args, key))) {
276
+ throw new Error("createAnnotation plot-center owns x, y, space, align, and baseline and does not accept source.");
277
+ }
278
+ args = { ...args, space: "plot", x: 0.5, y: 0.5, align: "center", baseline: "middle" };
279
+ }
274
280
  if (!Object.hasOwn(args, "text")) {
275
281
  throw new Error("createAnnotation requires text.");
276
282
  }
@@ -482,6 +488,12 @@ const editTextMark = /* @__PURE__ */ action(
482
488
  if (Object.hasOwn(args, "missing") && layer.source !== undefined) {
483
489
  throw new Error("Text missing policy requires a row-backed text mark, not source-owned labels.");
484
490
  }
491
+ if (args.inheritColor && layer.source === undefined) {
492
+ throw new Error("Text inheritColor requires a source-owned label.");
493
+ }
494
+ if (Object.hasOwn(args, "fill") && layer.encoding?.color !== undefined) {
495
+ throw new Error("Text fill conflicts with its color encoding.");
496
+ }
485
497
  const semantic = Object.hasOwn(args, "missing")
486
498
  ? this.editSemantic({
487
499
  property: `layer[${layer.id}].mark.missing`,
@@ -144,6 +144,9 @@ function layoutInputs(program, layer) {
144
144
  sourceX: entry.anchor.x,
145
145
  sourceY: entry.anchor.y,
146
146
  text: entry.graphic.properties.text,
147
+ lines: entry.graphic.properties.lines,
148
+ fontFamily: entry.graphic.properties.fontFamily,
149
+ fontWeight: entry.graphic.properties.fontWeight,
147
150
  fontSize: entry.graphic.properties.fontSize,
148
151
  textAlign: entry.graphic.properties.textAlign,
149
152
  textBaseline: entry.graphic.properties.textBaseline,
@@ -66,6 +66,10 @@ export function validateLayerSemanticValue(
66
66
  throw new Error(`Mark type "${markType}" does not support a missing policy.`);
67
67
  }
68
68
  }
69
+ if (property === "mark.orientation" && (findLayer(program, parsed.id)?.mark?.type !== "bar" ||
70
+ !["vertical", "horizontal"].includes(value))) {
71
+ throw new Error("Bar orientation must be vertical or horizontal.");
72
+ }
69
73
  if (property === "derivedBindings.regression") {
70
74
  if (!value || typeof value !== "object" || Array.isArray(value) ||
71
75
  Object.keys(value).length !== 2 || value.mode !== "follow" ||
@@ -10,7 +10,7 @@ import {
10
10
  } from "./resolve.js";
11
11
 
12
12
  const REGRESSION_OPTIONS = Object.freeze([
13
- "target", "x", "y", "groupBy", "method", "degree", "span",
13
+ "target", "x", "y", "groupBy", "method", "degree", "span", "robustIterations",
14
14
  "confidenceMethod", "level", "confidence", "interval", "band", "line"
15
15
  , "predict", "sourceBinding", "missing"
16
16
  ]);
@@ -21,7 +21,7 @@ import { removeOwnedColorLegends } from "../guides/legends/remove.js";
21
21
  import { requireRegressionField } from "./resolve.js";
22
22
 
23
23
  const OPTIONS = Object.freeze([
24
- "target", "data", "x", "y", "groupBy", "method", "degree", "span",
24
+ "target", "data", "x", "y", "groupBy", "method", "degree", "span", "robustIterations",
25
25
  "confidenceMethod", "level", "confidence", "interval", "band", "line"
26
26
  , "predict", "sourceBinding", "missing"
27
27
  ]);
@@ -111,14 +111,14 @@ function resolveParameters(previous, args) {
111
111
  const method = args.method ?? previous.method;
112
112
  const raw = { method };
113
113
  for (const key of [
114
- "degree", "span", "confidenceMethod", "level", "confidence", "interval"
114
+ "degree", "span", "robustIterations", "confidenceMethod", "level", "confidence", "interval"
115
115
  ]) {
116
116
  if (Object.hasOwn(args, key)) raw[key] = args[key];
117
117
  }
118
118
  const inheritsLevel = !Object.hasOwn(raw, "confidence");
119
119
  if (method === previous.method && args.interval !== false) {
120
120
  for (const key of [
121
- "degree", "span", "confidenceMethod", "level", "interval"
121
+ "degree", "span", "robustIterations", "confidenceMethod", "level", "interval"
122
122
  ]) {
123
123
  if (
124
124
  !Object.hasOwn(raw, key) &&
@@ -1,3 +1,5 @@
1
+ import { requireStringValue as requireRegressionField } from "../../core/validation.js";
2
+ export { requireStringValue as requireRegressionField } from "../../core/validation.js";
1
3
  import { isPlainObject } from "../../core/immutable.js";
2
4
  import { findLayer } from "../../selectors/layers.js";
3
5
 
@@ -8,12 +10,6 @@ export function requireRegressionObject(value, label) {
8
10
  return value;
9
11
  }
10
12
 
11
- export function requireRegressionField(value, label) {
12
- if (typeof value !== "string" || value.length === 0) {
13
- throw new TypeError(`${label} must be a non-empty string.`);
14
- }
15
- return value;
16
- }
17
13
 
18
14
  export function findRegressionPoint(program, requested) {
19
15
  const eligible = program.semanticSpec.layers.filter(layer =>
@@ -5,6 +5,7 @@ import { normalizePositionScaleChannel } from "../../core/vocabulary.js";
5
5
  import { findLayer } from "../../selectors/layers.js";
6
6
  import { requireSemanticScale } from "../../selectors/scales.js";
7
7
  import { findScaleConsumers } from "./consumers/index.js";
8
+ import { axisScaleBindings } from "../../materialization/coordinateBounds.js";
8
9
 
9
10
  const DEFINITIONS = Object.freeze({
10
11
  editXScale: Object.freeze({
@@ -41,7 +42,7 @@ const DEFINITIONS = Object.freeze({
41
42
  channel: "color",
42
43
  options: Object.freeze([
43
44
  "type", "domain", "range", "emptyDomain", "clamp", "reverse", "palette",
44
- "interpolate", "midpoint", "unknown"
45
+ "interpolate", "midpoint", "base", "constant", "unknown"
45
46
  ])
46
47
  }),
47
48
  editStrokeScale: Object.freeze({
@@ -49,7 +50,7 @@ const DEFINITIONS = Object.freeze({
49
50
  targetOnly: true,
50
51
  options: Object.freeze([
51
52
  "type", "domain", "range", "emptyDomain", "clamp", "reverse", "palette",
52
- "interpolate", "midpoint", "unknown"
53
+ "interpolate", "midpoint", "base", "constant", "unknown"
53
54
  ])
54
55
  }),
55
56
  editSizeScale: Object.freeze({
@@ -99,9 +100,11 @@ function allConsumers(program) {
99
100
  }
100
101
 
101
102
  function channelScaleIds(program, channel) {
102
- return [...new Set(allConsumers(program)
103
+ return [...new Set([...allConsumers(program)
103
104
  .filter(consumer => normalizePositionScaleChannel(consumer.channel) === channel)
104
- .map(consumer => consumer.encoding.scale))];
105
+ .map(consumer => consumer.encoding.scale),
106
+ ...program.semanticSpec.scales.filter(scale => axisScaleBindings(program, scale.id)
107
+ .some(binding => binding.channel === channel)).map(scale => scale.id)])];
105
108
  }
106
109
 
107
110
  function requireSingleScale(ids, operation, scope) {
@@ -115,6 +118,8 @@ function requireSingleScale(ids, operation, scope) {
115
118
  function validateScaleChannel(program, id, channel, operation) {
116
119
  requireSemanticScale(program, id);
117
120
  const consumers = findScaleConsumers(program, id);
121
+ const axes = axisScaleBindings(program, id);
122
+ if (consumers.length === 0 && axes.length === 1 && axes[0].channel === channel) return id;
118
123
  if (consumers.length === 0) {
119
124
  throw new Error(`${operation} requires a scale bound to the ${channel} channel.`);
120
125
  }