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,52 +1,8 @@
1
1
  import { action } from "../../core/action.js";
2
- import { STROKE_STYLE_PROPERTIES } from "../../grammar/strokeStyle.js";
3
- import {
4
- applyFacadeGuides, normalizeAppearance, normalizeCategoricalColor, normalizeCategoricalGuides,
5
- normalizeArcCategory, normalizeCategoryAggregate, omitUndefinedOptions,
6
- resolveFacadeData, resolveFacadeId, validateFacadeOptions
7
- } from "./shared.js";
8
-
9
- const OPERATION = "createPiePlot";
10
- const OPTIONS = ["id", "data", "coordinate", "category", "value", "aggregate", "color", "arc", "guides"];
11
- const ARC_OPTIONS = [
12
- "innerRadius", "padAngle", "fill", "opacity", "stroke", "strokeWidth",
13
- ...STROKE_STYLE_PROPERTIES
14
- ];
15
- function guideOptions(value, color) {
16
- const guides = normalizeCategoricalGuides(value, OPERATION, color);
17
- if (guides === false) return false;
18
- for (const key of ["axes", "grid"]) {
19
- if (guides[key] !== undefined && guides[key] !== false) {
20
- throw new TypeError(`${OPERATION} guides.${key} only accepts false.`);
21
- }
22
- }
23
- return { ...guides, axes: false, grid: false };
24
- }
2
+ import { createArcPlot } from "./radial.js";
25
3
 
26
4
  export const createPiePlot = /* @__PURE__ */ action({
27
- op: OPERATION, description: "Create a categorical count or weighted-sum pie or donut plot."
5
+ op: "createPiePlot", description: "Create a categorical count or weighted-sum pie or donut plot."
28
6
  }, function (args = {}) {
29
- validateFacadeOptions(args, OPTIONS, OPERATION);
30
- const id = resolveFacadeId(this, args.id, { defaultId: "piePlot", operation: OPERATION });
31
- const data = resolveFacadeData(this, args.data, OPERATION);
32
- const category = normalizeArcCategory(args.category, OPERATION);
33
- const aggregate = normalizeCategoryAggregate(args, OPERATION);
34
- const arc = omitUndefinedOptions(normalizeAppearance(args.arc, ARC_OPTIONS, `${OPERATION} arc`));
35
- const color = args.color === false ? undefined
36
- : normalizeCategoricalColor(args.color === undefined ? category.field : args.color, `${OPERATION} color`);
37
- if (color !== undefined && arc.fill !== undefined) {
38
- throw new Error(`${OPERATION} arc.fill cannot be combined with color; use color:false.`);
39
- }
40
- const guides = guideOptions(args.guides, color);
41
- let next = this.createArcMark({ id, data, ...arc }).encodeTheta({
42
- ...category, target: id, aggregate,
43
- ...(args.coordinate === undefined ? {} : { coordinate: args.coordinate }),
44
- ...(aggregate === "sum" ? { weight: args.value } : {})
45
- });
46
- if (color !== undefined) next = next.encodeColor({ ...color, target: id });
47
- next = applyFacadeGuides(next, guides, id, args.guides ?? {});
48
- return next._withMarkConfig(id, {
49
- ...next.markConfigs[id],
50
- compositionRole: "pie"
51
- });
7
+ return createArcPlot(this, args, { operation: "createPiePlot", defaultId: "piePlot" });
52
8
  });
@@ -1,3 +1,4 @@
1
+ import { findSemanticScale } from "../../selectors/scales.js";
1
2
  import { action } from "../../core/action.js";
2
3
  import { STROKE_STYLE_PROPERTIES } from "../../grammar/strokeStyle.js";
3
4
  import {
@@ -6,21 +7,22 @@ import {
6
7
  resolveFacadeData, resolveFacadeId, validateFacadeOptions
7
8
  } from "./shared.js";
8
9
 
9
- const OPTIONS = ["id", "data", "coordinate", "category", "value", "aggregate", "radiusScale", "color", "arc", "guides"];
10
+ const OPTIONS = ["id", "data", "coordinate", "category", "value", "aggregate", "color", "arc", "guides"];
10
11
  const ARC_OPTIONS = [
11
12
  "innerRadius", "padAngle", "fill", "opacity", "stroke", "strokeWidth",
12
13
  ...STROKE_STYLE_PROPERTIES
13
14
  ];
14
15
  const RADIUS_OPTIONS = ["id", "type", "domain", "range", "zero", "nice", "reverse", "clamp"];
15
16
 
16
- function createMeasuredPlot(program, args, { operation, defaultId, mapping }) {
17
- validateFacadeOptions(args, OPTIONS, operation);
17
+ export function createArcPlot(program, args, { operation, defaultId, mapping }) {
18
+ const measured = mapping !== undefined;
19
+ validateFacadeOptions(args, [...OPTIONS, measured ? "radiusScale" : "radius"], operation);
18
20
  const id = resolveFacadeId(program, args.id, { defaultId, operation });
19
21
  const data = resolveFacadeData(program, args.data, operation);
20
22
  const category = normalizeArcCategory(args.category, operation);
21
23
  const aggregate = normalizeCategoryAggregate(args, operation);
22
24
  const arc = omitUndefinedOptions(normalizeAppearance(args.arc, ARC_OPTIONS, `${operation} arc`));
23
- if (arc.padAngle !== undefined && arc.padAngle !== 0) throw new Error(`${operation} requires padAngle 0.`);
25
+ if (measured && arc.padAngle !== undefined && arc.padAngle !== 0) throw new Error(`${operation} requires padAngle 0.`);
24
26
  const radiusScale = args.radiusScale === undefined ? undefined
25
27
  : omitUndefinedOptions(normalizeAppearance(args.radiusScale, RADIUS_OPTIONS, `${operation} radiusScale`));
26
28
  const color = args.color === false ? undefined
@@ -28,23 +30,43 @@ function createMeasuredPlot(program, args, { operation, defaultId, mapping }) {
28
30
  if (color !== undefined && arc.fill !== undefined) {
29
31
  throw new Error(`${operation} arc.fill cannot be combined with color; use color:false.`);
30
32
  }
31
- const guides = normalizeCategoricalGuides(args.guides, operation, color);
33
+ let guides = normalizeCategoricalGuides(args.guides, operation, color);
34
+ if (!measured && guides !== false) {
35
+ for (const key of ["axes", "grid"]) {
36
+ if (guides[key] !== undefined && guides[key] !== false) {
37
+ throw new TypeError(`${operation} guides.${key} only accepts false.`);
38
+ }
39
+ }
40
+ guides = { ...guides, axes: false, grid: false };
41
+ }
32
42
  let next = program.createArcMark({ id, data, ...arc }).encodeTheta({
33
- ...category, target: id, ...(args.coordinate === undefined ? {} : { coordinate: args.coordinate })
34
- }).encodeR({ target: id, aggregate, mapping,
35
- ...(aggregate === "sum" ? { field: args.value } : {}),
36
- ...(radiusScale === undefined ? {} : { scale: radiusScale }) });
43
+ ...category, target: id, ...(args.coordinate === undefined ? {} : { coordinate: args.coordinate }),
44
+ ...(!measured ? { aggregate, ...(aggregate === "sum" ? { weight: args.value } : {}) } : {})
45
+ });
46
+ if (measured) {
47
+ next = next.encodeR({ target: id, aggregate, mapping,
48
+ ...(aggregate === "sum" ? { field: args.value } : {}),
49
+ ...(radiusScale === undefined ? {} : { scale: radiusScale }) });
50
+ } else if (args.radius !== undefined) {
51
+ const radius = typeof args.radius === "string" ? { field: args.radius }
52
+ : normalizeAppearance(args.radius, ["field", "aggregate", "scale"], `${operation} radius`);
53
+ const scale = radius.scale ?? {};
54
+ next = next.encodeR({ target: id, ...omitUndefinedOptions(radius), scale: {
55
+ ...(scale.type !== "log" && findSemanticScale(next, scale.id ?? "radius") === undefined ? { zero: true } : {}),
56
+ ...scale
57
+ } });
58
+ }
37
59
  if (color !== undefined) next = next.encodeColor({ ...color, target: id });
38
60
  next = applyFacadeGuides(next, guides, id, args.guides ?? {});
39
61
  return next._withMarkConfig(id, {
40
62
  ...next.markConfigs[id],
41
- compositionRole: "rose"
63
+ compositionRole: measured ? "rose" : "pie"
42
64
  });
43
65
  }
44
66
 
45
67
  export const createRosePlot = /* @__PURE__ */ action({ op: "createRosePlot", description: "Create equal-angle sectors with area proportional to category count or sum." }, function (args = {}) {
46
- return createMeasuredPlot(this, args, { operation: "createRosePlot", defaultId: "rosePlot", mapping: "area" });
68
+ return createArcPlot(this, args, { operation: "createRosePlot", defaultId: "rosePlot", mapping: "area" });
47
69
  });
48
70
  export const createRadialBarPlot = /* @__PURE__ */ action({ op: "createRadialBarPlot", description: "Create equal-angle sectors with radial length proportional to category count or sum." }, function (args = {}) {
49
- return createMeasuredPlot(this, args, { operation: "createRadialBarPlot", defaultId: "radialBarPlot", mapping: "radius-length" });
71
+ return createArcPlot(this, args, { operation: "createRadialBarPlot", defaultId: "radialBarPlot", mapping: "radius-length" });
50
72
  });
@@ -1,3 +1,4 @@
1
+ import { encodeStroke } from "../encodings/stroke.js";
1
2
  import { action } from "../../core/action.js";
2
3
  import { validateNonNegativeFinite } from "../../core/validation.js";
3
4
  import { STROKE_STYLE_PROPERTIES } from "../../grammar/strokeStyle.js";
@@ -15,7 +16,7 @@ import {
15
16
  } from "./shared.js";
16
17
 
17
18
  const OPTIONS = Object.freeze([
18
- "id", "data", "coordinate", "x", "y", "color", "size", "shape",
19
+ "id", "data", "coordinate", "x", "y", "color", "stroke", "size", "shape",
19
20
  "point", "guides"
20
21
  ]);
21
22
  const POINT_OPTIONS = Object.freeze([
@@ -43,6 +44,13 @@ export const createScatterPlot = /* @__PURE__ */ action(
43
44
  const x = normalizeFieldEncoding(args.x, "createScatterPlot x");
44
45
  const y = normalizeFieldEncoding(args.y, "createScatterPlot y");
45
46
  const color = normalizeEncoding(args.color, "createScatterPlot color");
47
+ const stroke = normalizeEncoding(args.stroke, "createScatterPlot stroke");
48
+ if (stroke !== undefined) {
49
+ if (typeof stroke.field !== "string" || stroke.field.length === 0 || Object.hasOwn(stroke, "value")) {
50
+ throw new TypeError("createScatterPlot stroke requires a field; constant stroke belongs in point.");
51
+ }
52
+ if (Object.hasOwn(point, "stroke")) throw new Error("createScatterPlot stroke conflicts with point.stroke.");
53
+ }
46
54
  const size = normalizeEncoding(args.size, "createScatterPlot size");
47
55
  const shape = normalizeEncoding(args.shape, "createScatterPlot shape");
48
56
  const guides = normalizeGuides(args.guides, "createScatterPlot");
@@ -51,12 +59,18 @@ export const createScatterPlot = /* @__PURE__ */ action(
51
59
  if (size !== undefined) throw new Error("createScatterPlot point radius conflicts with size.");
52
60
  }
53
61
 
62
+ const initialPoint = { ...point };
63
+ if (stroke !== undefined) delete initialPoint.strokeWidth;
54
64
  let next = this
55
- .createPointMark({ id, data, ...point })
65
+ .createPointMark({ id, data, ...initialPoint })
56
66
  .encodeX(positionArgs(x, { target: id, coordinate: args.coordinate }))
57
67
  .encodeY(positionArgs(y, { target: id, coordinate: args.coordinate }));
58
68
  if (radius !== undefined) next = next.encodePointRadius({ target: id, value: radius });
59
69
  if (color !== undefined) next = next.encodeColor(targetArgs(color, id));
70
+ if (stroke !== undefined) {
71
+ next = encodeStroke.call(next, targetArgs(stroke, id));
72
+ if (Object.hasOwn(point, "strokeWidth")) next = next.editPointMark({ target: id, strokeWidth: point.strokeWidth });
73
+ }
60
74
  if (size !== undefined) next = next.encodeSize(targetArgs(size, id));
61
75
  if (shape !== undefined) next = next.encodeShape(targetArgs(shape, id));
62
76
  return applyFacadeGuides(next, guides, id);
@@ -191,9 +191,9 @@ export function normalizeArcCategory(value, operation) {
191
191
  }
192
192
 
193
193
  export function normalizeCategoryAggregate(args, operation) {
194
- const aggregate = args.aggregate === undefined ? "count" : args.aggregate;
194
+ const aggregate = args.aggregate === undefined ? args.value === undefined ? "count" : "sum" : args.aggregate;
195
195
  if (!["count", "sum"].includes(aggregate) || (aggregate === "sum") !== (args.value !== undefined)) {
196
- throw new Error(`${operation} requires count without value or explicit sum with value.`);
196
+ throw new Error(`${operation} requires count without value or sum with value.`);
197
197
  }
198
198
  if (args.value !== undefined) validateNonEmptyString(args.value, `${operation} value`);
199
199
  return aggregate;
@@ -0,0 +1,35 @@
1
+ import { action } from "../../core/action.js";
2
+ import { TEXT_MARK_STYLE_OPTIONS } from "../../grammar/text.js";
3
+ import {
4
+ applyFacadeGuides, normalizeAppearance, normalizeEncoding, normalizeFieldEncoding,
5
+ normalizeGuides, positionArgs, resolveFacadeData, resolveFacadeId, targetArgs,
6
+ validateFacadeOptions
7
+ } from "./shared.js";
8
+
9
+ const OPTIONS = Object.freeze(["id", "data", "coordinate", "x", "y", "text", "color", "style", "guides"]);
10
+ const STYLE_OPTIONS = Object.freeze(["missing", ...TEXT_MARK_STYLE_OPTIONS.filter(key => key !== "inheritColor")]);
11
+
12
+ export const createTextPlot = /* @__PURE__ */ action({
13
+ op: "createTextPlot", description: "Create a Cartesian text plot from existing chart data."
14
+ }, function (args = {}) {
15
+ validateFacadeOptions(args, OPTIONS, "createTextPlot");
16
+ const id = resolveFacadeId(this, args.id, { defaultId: "textPlot", operation: "createTextPlot" });
17
+ const data = resolveFacadeData(this, args.data, "createTextPlot");
18
+ const x = normalizeFieldEncoding(args.x, "createTextPlot x");
19
+ const y = normalizeFieldEncoding(args.y, "createTextPlot y");
20
+ const text = normalizeEncoding(args.text, "createTextPlot text");
21
+ if (text === undefined) throw new Error("createTextPlot requires text.");
22
+ const color = normalizeEncoding(args.color, "createTextPlot color");
23
+ const style = normalizeAppearance(args.style, STYLE_OPTIONS, "createTextPlot style");
24
+ const guides = normalizeGuides(args.guides, "createTextPlot");
25
+ const apply = program => {
26
+ let next = program.createTextMark({ id, data, ...style })
27
+ .encodeX(positionArgs(x, { target: id, coordinate: args.coordinate }))
28
+ .encodeY(positionArgs(y, { target: id, coordinate: args.coordinate }))
29
+ .encodeText(targetArgs(text, id));
30
+ if (color !== undefined) next = next.encodeColor(targetArgs(color, id));
31
+ return applyFacadeGuides(next, guides, id);
32
+ };
33
+ apply(this);
34
+ return apply(this);
35
+ });
@@ -1,3 +1,4 @@
1
+ import { resolveGraphicBounds } from "../../layout/canvas.js";
1
2
  import { inheritTextMetrics } from "../textMetrics/index.js";
2
3
  import { action, closedAction } from "../../core/action.js";
3
4
  import { ChartProgram as CoreChartProgram } from "../../core/ChartProgram.js";
@@ -19,7 +20,7 @@ const CONCAT_OPTIONS = Object.freeze([
19
20
  "id", "programs", "gap", "align", "padding"
20
21
  ]);
21
22
  const LAYOUT_EDIT_OPTIONS = Object.freeze([
22
- "columns", "gap", "align", "padding"
23
+ "columns", "gap", "spacing", "align", "padding"
23
24
  ]);
24
25
  const REPLACEMENT_OPTIONS = Object.freeze(["target", "program"]);
25
26
  const INSERTION_OPTIONS = Object.freeze(["id", "program", "before", "after"]);
@@ -179,6 +180,7 @@ const editCompositionLayout = /* @__PURE__ */ closedAction(
179
180
  "editCompositionLayout columns is available only on a facet composition."
180
181
  );
181
182
  }
183
+ if (current.type !== "facet" && Object.hasOwn(args, "spacing")) throw new Error("Plot spacing requires a facet composition.");
182
184
  const padding = Object.hasOwn(args, "padding")
183
185
  ? normalizeCompositionPadding(args.padding, current.padding)
184
186
  : current.padding;
@@ -188,6 +190,8 @@ const editCompositionLayout = /* @__PURE__ */ closedAction(
188
190
  (current.facet.grid?.cells ?? []).map(cell => [cell.id, cell])
189
191
  );
190
192
  layout = resolveFacetLayout({
193
+ spacing: option(args, "spacing", current.spacing),
194
+ plots: current.children.map(id => ({ id, ...resolveGraphicBounds(this.children[id]) })),
191
195
  children: current.children.map((id, index) => ({
192
196
  ...childDescriptor({ id, program: this.children[id] }),
193
197
  value: current.facet.values[index],
@@ -218,7 +222,7 @@ const editCompositionLayout = /* @__PURE__ */ closedAction(
218
222
  children: this.children,
219
223
  compositionSpec: {
220
224
  ...current,
221
- ...(current.type === "facet" ? { columns: layout.columns } : {}),
225
+ ...(current.type === "facet" ? { columns: layout.columns, ...(layout.spacing !== undefined ? { spacing: layout.spacing } : current.spacing !== undefined ? { spacing: "canvas" } : {}) } : {}),
222
226
  gap: layout.gap,
223
227
  align: layout.align,
224
228
  padding: layout.padding
@@ -2,7 +2,7 @@ import { BOX_FIELDS, deriveBoxData, normalizeBoxTransform } from "../../grammar/
2
2
  import { derivedCreator, derivedMaterializer } from "./shared.js";
3
3
 
4
4
  const OPTIONS = Object.freeze([
5
- "id", "source", "category", "field", "whisker", "factor", "as"
5
+ "id", "source", "category", "field", "whisker", "factor", "as", "summary"
6
6
  ]);
7
7
 
8
8
  function materializer(type, op, select) {
@@ -28,6 +28,7 @@ function creator(type, op, materialize) {
28
28
  type,
29
29
  category: args.category,
30
30
  field: args.field,
31
+ ...(args.summary === undefined ? {} : { summary: args.summary }),
31
32
  ...(args.whisker === undefined ? {} : { whisker: args.whisker }),
32
33
  ...(args.factor === undefined ? {} : { factor: args.factor }),
33
34
  as: args.as ?? BOX_FIELDS
@@ -509,7 +509,7 @@ export const editStackData = /* @__PURE__ */ focusedEditor(
509
509
  );
510
510
  export const editRegressionData = /* @__PURE__ */ focusedEditor(
511
511
  "editRegressionData", "regression", [
512
- "x", "y", "groupBy", "method", "degree", "span", "confidenceMethod",
512
+ "x", "y", "groupBy", "method", "degree", "span", "robustIterations", "confidenceMethod",
513
513
  "level", "confidence", "interval", "predict"
514
514
  ]
515
515
  );
@@ -9,7 +9,7 @@ import { resolveDatasetReference } from "../../selectors/datasets.js";
9
9
  import { derivedMaterializer } from "./shared.js";
10
10
 
11
11
  const OPTIONS = Object.freeze([
12
- "id", "source", "x", "y", "groupBy", "method", "degree", "span",
12
+ "id", "source", "x", "y", "groupBy", "method", "degree", "span", "robustIterations",
13
13
  "confidenceMethod", "level", "confidence", "interval"
14
14
  , "predict", "missing"
15
15
  ]);
@@ -25,6 +25,7 @@ export const materializeRegressionData = /* @__PURE__ */ derivedMaterializer(
25
25
  method: transform.method,
26
26
  degree: transform.degree,
27
27
  span: transform.span,
28
+ robustIterations: transform.robustIterations,
28
29
  confidenceMethod: transform.confidenceMethod,
29
30
  level: transform.level,
30
31
  confidence: transform.confidence,
@@ -87,11 +87,12 @@ export function derivedCreator(
87
87
  sourceLabel,
88
88
  transform,
89
89
  materialize,
90
- requireSource = false
90
+ requireSource = false,
91
+ defaultId
91
92
  ) {
92
93
  return action({ op, description }, function (args = {}) {
93
94
  validateKeys(args, options, op);
94
- const id = validateUserId(args.id, idLabel);
95
+ const id = validateUserId(args.id === undefined ? defaultId : args.id, idLabel);
95
96
  const requestedSource = validateUserId(
96
97
  requireSource ? args.source : args.source ?? this.context.currentData,
97
98
  sourceLabel
@@ -22,5 +22,7 @@ export const createStackData = /* @__PURE__ */ derivedCreator(
22
22
  "Stack dataset id",
23
23
  "Stack source dataset id",
24
24
  normalizeStackTransform,
25
- "materializeStackData"
25
+ "materializeStackData",
26
+ false,
27
+ "stacked"
26
28
  );
@@ -1,9 +1,8 @@
1
+ import { canMaterializeBar } from "../../materialization/marks/index.js";
1
2
  import { action } from "../../core/action.js";
2
3
  import { validatePathSeriesAppearance } from "../../grammar/pathSeries.js";
3
4
  import { assertEncodingSelectionCompatibility } from "../../materialization/selection/compatibility.js";
4
5
  import {
5
- readNominalField,
6
- readQuantitativeField,
7
6
  readScaleField,
8
7
  validateOpacityValue
9
8
  } from "../../grammar/scales/index.js";
@@ -42,11 +41,11 @@ function encodeAppearanceField(program, channel, args, operation) {
42
41
  ["point"],
43
42
  "point mark"
44
43
  );
45
- const expectedFieldType = channel === "shape" ? "nominal" : "quantitative";
44
+ const fieldTypes = channel === "shape" ? ["nominal"] : ["quantitative", "nominal", "ordinal"];
46
45
  const dataset = applyRequestedItemMissingPolicy(layer, sourceDataset, args.field);
47
- const fieldType = args.fieldType ?? expectedFieldType;
48
- if (fieldType !== expectedFieldType) {
49
- throw new Error(`${operation} requires a ${expectedFieldType} field.`);
46
+ const fieldType = args.fieldType ?? fieldTypes[0];
47
+ if (!fieldTypes.includes(fieldType)) {
48
+ throw new Error(`${operation} requires a ${fieldTypes.join(" or ")} field.`);
50
49
  }
51
50
  if (channel === "size" && program.markConfigs[target]?.radius !== undefined) {
52
51
  throw new Error("encodeSize cannot be combined with a constant radius.");
@@ -59,17 +58,12 @@ function encodeAppearanceField(program, channel, args, operation) {
59
58
  const scale = resolveAppearanceScaleDefinition(
60
59
  program,
61
60
  channel,
62
- requestedScale
61
+ requestedScale,
62
+ fieldType
63
63
  );
64
- if (Object.hasOwn(scale, "unknown")) {
65
- readScaleField(dataset.values, args.field, fieldType, {
66
- allowUnknown: true
67
- });
68
- } else if (channel === "shape") {
69
- readNominalField(dataset.values, args.field);
70
- } else {
71
- readQuantitativeField(dataset.values, args.field);
72
- }
64
+ readScaleField(dataset.values, args.field, fieldType, {
65
+ allowUnknown: Object.hasOwn(scale, "unknown")
66
+ });
73
67
 
74
68
  let next = setEncodingProperties(program, target, channel, {
75
69
  field: args.field,
@@ -152,7 +146,7 @@ const removePointRadius = /* @__PURE__ */ action(
152
146
  const encodeSize = /* @__PURE__ */ action(
153
147
  {
154
148
  op: "encodeSize",
155
- description: "Encode a quantitative field as equal-area point size."
149
+ description: "Encode a field as equal-area point size."
156
150
  },
157
151
  function (args = {}) {
158
152
  return encodeAppearanceField(this, "size", args, "encodeSize");
@@ -184,6 +178,17 @@ const clearOpacityEncoding = /* @__PURE__ */ action(
184
178
  }
185
179
  );
186
180
 
181
+ function withoutConstantOpacity(program, target) {
182
+ const { opacity, ...config } = program.markConfigs[target] ?? {};
183
+ void opacity;
184
+ if (config.barAppearance?.opacity !== undefined) {
185
+ const { opacity: barOpacity, ...appearance } = config.barAppearance;
186
+ void barOpacity;
187
+ config.barAppearance = appearance;
188
+ }
189
+ return program._withMarkConfig(target, config);
190
+ }
191
+
187
192
  const encodeOpacity = /* @__PURE__ */ action(
188
193
  {
189
194
  op: "encodeOpacity",
@@ -202,14 +207,13 @@ const encodeOpacity = /* @__PURE__ */ action(
202
207
  const { id: target, dataset: sourceDataset, layer } = resolveTarget(
203
208
  this,
204
209
  args.target,
205
- ["point", "rule", "line"],
206
- "point, rule or line mark"
210
+ ["point", "rule", "line", "bar"],
211
+ "point, rule, line or bar mark"
207
212
  );
208
213
  if (hasValue) {
209
214
  validateOpacityValue(args.value, "encodeOpacity");
210
215
  assertEncodingSelectionCompatibility(this, target, ["opacity"]);
211
- const { opacity, ...config } = this.markConfigs[target] ?? {};
212
- void opacity;
216
+ const config = withoutConstantOpacity(this, target).markConfigs[target];
213
217
  const withoutLegend = this.guideConfigs.legend?.opacity?.target === target
214
218
  ? this.removeOpacityLegend()
215
219
  : this;
@@ -220,6 +224,11 @@ const encodeOpacity = /* @__PURE__ */ action(
220
224
  if (layer.mark.type === "line") {
221
225
  return rematerializeEncoding(next, target, "opacity", undefined, layer);
222
226
  }
227
+ if (layer.mark.type === "bar") {
228
+ return canMaterializeBar(next, findLayer(next, target))
229
+ ? applyDetachedScaleRematerialization(next.rematerializeBarMark({ id: target }), [layer])
230
+ : next;
231
+ }
223
232
  const materialized = layer.mark.type === "rule"
224
233
  ? next.rematerializeRuleMark({ id: target })
225
234
  : next.rematerializePointMark({ id: target });
@@ -244,17 +253,13 @@ const encodeOpacity = /* @__PURE__ */ action(
244
253
  ...layer, encoding: { ...layer.encoding, opacity: { field: args.field, fieldType } }
245
254
  });
246
255
  }
247
- if (Object.hasOwn(scale, "unknown")) {
248
- readScaleField(dataset.values, args.field, fieldType, {
249
- allowUnknown: true
250
- });
251
- } else {
252
- readQuantitativeField(dataset.values, args.field);
253
- }
256
+ readScaleField(dataset.values, args.field, fieldType, {
257
+ allowUnknown: Object.hasOwn(scale, "unknown")
258
+ });
254
259
  const { opacity, ...config } = this.markConfigs[target] ?? {};
255
260
  void opacity;
256
261
  let next = setEncodingProperties(
257
- this._withoutMaterializationConfig(["marks", target, "opacity"]),
262
+ withoutConstantOpacity(this, target),
258
263
  target,
259
264
  "opacity",
260
265
  { field: args.field, fieldType, scale: scale.id }
@@ -262,6 +267,9 @@ const encodeOpacity = /* @__PURE__ */ action(
262
267
  next = applyEncodingScale(next, scale, requestedScale, {
263
268
  reassignment: previous?.scale === scale.id
264
269
  });
270
+ if (layer.mark.type === "bar" && !canMaterializeBar(next, findLayer(next, target))) {
271
+ return next.rematerializeScale({ id: scale.id });
272
+ }
265
273
  return rematerializeEncoding(next, target, "opacity", scale.id, layer);
266
274
  }
267
275
  );
@@ -4,11 +4,33 @@ import {
4
4
  BAR_GRAINS,
5
5
  resolveBarColorLayout,
6
6
  resolveBarOffsetChannel,
7
+ resolveBarChannels,
7
8
  resolveBarGrain
8
9
  } from "../../grammar/bars/policy.js";
9
10
  import { normalizeBarWidth } from "../../grammar/bars/geometry.js";
11
+ import { applyMaterializationPlan } from "../../materialization/dependencies.js";
12
+ import {
13
+ getScaleConsumerMarkSteps,
14
+ getSourceDependentMarkSteps
15
+ } from "../../materialization/marks/index.js";
16
+
17
+ function rematerializeWidth(program, target, layer, width) {
18
+ const next = program._withMarkConfig(target, {
19
+ ...program.markConfigs[target], barWidth: width
20
+ }).rematerializeBarMark({ id: target });
21
+ const category = resolveBarChannels(layer)?.category;
22
+ const encoding = layer.encoding?.[category];
23
+ if (encoding?.fieldType !== "temporal") return next;
24
+ const marks = getScaleConsumerMarkSteps(next, [encoding.scale])
25
+ .filter(step => step.args.id !== target);
26
+ return applyMaterializationPlan(next, [
27
+ ...marks,
28
+ ...[target, ...marks.map(step => step.args.id)]
29
+ .flatMap(id => getSourceDependentMarkSteps(next, id))
30
+ ]);
31
+ }
10
32
 
11
- const OPTIONS = Object.freeze(["band", "pixels", "target"]);
33
+ const OPTIONS = Object.freeze(["band", "pixels", "interval", "target"]);
12
34
 
13
35
  const encodeBarWidth = /* @__PURE__ */ action(
14
36
  {
@@ -26,22 +48,30 @@ const encodeBarWidth = /* @__PURE__ */ action(
26
48
  const layout = resolveBarColorLayout(layer);
27
49
  const width = normalizeBarWidth(
28
50
  args,
29
- this.markConfigs[target]?.barWidth
51
+ this.markConfigs[target]?.barWidth ?? (layer.mark.orientation === undefined ? undefined : { pixels: 5 })
30
52
  );
31
53
  const grouped = layout === "group";
32
54
  const offsetChannel = resolveBarOffsetChannel(layer);
33
55
  const grain = resolveBarGrain(layer);
56
+ if (layer.mark.orientation !== undefined && width.band !== undefined) {
57
+ throw new Error("Numeric-center bars require pixel width.");
58
+ }
34
59
  if (grain === undefined && this.markConfigs[target]?.boxPlot === undefined && (
35
60
  layer.encoding?.x === undefined || layer.encoding?.y === undefined
36
61
  )) {
37
62
  return this._withMarkConfig(target, { ...this.markConfigs[target], barWidth: width });
38
63
  }
64
+ if (width.interval !== undefined && grain !== undefined) {
65
+ const category = resolveBarChannels(layer)?.category;
66
+ if (grain !== BAR_GRAINS.aggregate || grouped || layer.encoding?.[category]?.fieldType !== "temporal") {
67
+ throw new Error("Bar interval width requires ungrouped aggregate temporal categories.");
68
+ }
69
+ }
39
70
  if (grain === BAR_GRAINS.histogram) {
40
71
  throw new Error("encodeBarWidth requires an aggregate or ranged category slot, not histogram bins.");
41
72
  }
42
- if (grain === BAR_GRAINS.ranged) {
43
- return this._withMarkConfig(target, { ...this.markConfigs[target], barWidth: width })
44
- .rematerializeBarMark({ id: target });
73
+ if ([BAR_GRAINS.ranged, BAR_GRAINS.centered].includes(grain)) {
74
+ return rematerializeWidth(this, target, layer, width);
45
75
  }
46
76
  if (
47
77
  grain !== BAR_GRAINS.aggregate ||
@@ -56,12 +86,7 @@ const encodeBarWidth = /* @__PURE__ */ action(
56
86
  );
57
87
  }
58
88
 
59
- return this
60
- ._withMarkConfig(target, {
61
- ...this.markConfigs[target],
62
- barWidth: width
63
- })
64
- .rematerializeBarMark({ id: target });
89
+ return rematerializeWidth(this, target, layer, width);
65
90
  }
66
91
  );
67
92
 
@@ -155,7 +155,6 @@ function resolveScaleOnly(program, id) {
155
155
  }
156
156
  const consumers = findScaleConsumers(program, id);
157
157
  if (
158
- semanticScale.radialMapping !== undefined &&
159
158
  consumers.length > 0 &&
160
159
  consumers.every(isPendingMeasuredRadiusConsumer)
161
160
  ) {
@@ -39,7 +39,7 @@ export function encodeContinuousColor(program, args) {
39
39
  const { id: target, dataset: sourceDataset, layer } = resolveTarget(
40
40
  program,
41
41
  args.target,
42
- ["point", "bar", "rect"],
42
+ ["point", "bar", "rect", "text"],
43
43
  "continuous color mark"
44
44
  );
45
45
  const dataset = applyRequestedItemMissingPolicy(layer, sourceDataset, args.field);
@@ -63,7 +63,7 @@ const encodeColor = /* @__PURE__ */ action(
63
63
  const { id: target, dataset: sourceDataset, layer } = resolveTarget(
64
64
  this,
65
65
  args.target,
66
- ["point", "line", "bar", "area", "arc", "rect"],
66
+ ["point", "line", "bar", "area", "arc", "rect", "text"],
67
67
  "color mark"
68
68
  );
69
69
  const dataset = applyRequestedItemMissingPolicy(layer, sourceDataset, args.field);
@@ -20,6 +20,9 @@ export function resolveColorScaleOptions(args) {
20
20
  }
21
21
 
22
22
  export function assertNoConstantColor(program, layer) {
23
+ if (layer.mark.type === "text" && layer.source !== undefined) {
24
+ throw new Error("Source-owned text uses inheritColor or constant fill; field color requires row-backed text.");
25
+ }
23
26
  const config = program.markConfigs[layer.id];
24
27
  const hasConstant = {
25
28
  point: config?.fill !== undefined,
@@ -27,7 +30,8 @@ export function assertNoConstantColor(program, layer) {
27
30
  area: config?.errorBand?.fill !== undefined,
28
31
  bar: config?.barAppearance?.fill !== undefined,
29
32
  arc: config?.fill !== undefined,
30
- rect: config?.fillExplicit === true
33
+ rect: config?.fillExplicit === true,
34
+ text: config?.fillExplicit === true
31
35
  }[layer.mark.type] ?? false;
32
36
  if (hasConstant) {
33
37
  throw new Error(
@@ -58,15 +62,18 @@ export function resolveColorLayout(layer, requested, barGrain) {
58
62
  layer.mark.type === "bar"
59
63
  ? barGrain === BAR_GRAINS.histogram
60
64
  ? "stack"
61
- : barGrain === BAR_GRAINS.ranged ? "overlay" : "group"
65
+ : [BAR_GRAINS.ranged, BAR_GRAINS.centered].includes(barGrain) ? "overlay" : "group"
62
66
  : layer.mark.type === "area"
63
67
  ? "overlay"
64
68
  : undefined
65
69
  );
66
70
 
67
- if (["point", "line", "rect"].includes(layer.mark.type) && layout !== undefined) {
71
+ if (["point", "line", "rect", "text"].includes(layer.mark.type) && layout !== undefined) {
68
72
  throw new Error(`Color layout is not supported for ${layer.mark.type} marks.`);
69
73
  }
74
+ if (barGrain === BAR_GRAINS.centered && layout !== "overlay") {
75
+ throw new Error("Numeric-center bars support only overlay layout.");
76
+ }
70
77
  if (layer.mark.type === "area" && layout === "group") {
71
78
  throw new Error('Area color layout does not support "group".');
72
79
  }