ggaction 0.0.11 → 0.0.13

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 (289) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +3 -2
  3. package/knowledge/action-card.schema.json +1 -1
  4. package/knowledge/action-cards.json +1 -1
  5. package/knowledge/action-cards.schema.json +1 -1
  6. package/knowledge/extension-authoring.md +2 -1
  7. package/knowledge/intent-taxonomy.json +1 -1
  8. package/knowledge/mcp-resources.json +1 -1
  9. package/knowledge/task-resolver.js +1 -1
  10. package/package.json +4 -2
  11. package/src/actions/basic.js +2 -0
  12. package/src/actions/boxPlots/materialize.js +2 -1
  13. package/src/actions/canvas/actions.js +3 -1
  14. package/src/actions/canvas/fitting.js +211 -0
  15. package/src/actions/canvas/index.js +2 -0
  16. package/src/actions/categoryOrder/index.js +3 -2
  17. package/src/actions/charts/area.js +47 -0
  18. package/src/actions/charts/bar.js +11 -5
  19. package/src/actions/charts/beeswarm.js +70 -0
  20. package/src/actions/charts/density.js +44 -0
  21. package/src/actions/charts/ecdf.js +183 -0
  22. package/src/actions/charts/endpoints.js +451 -0
  23. package/src/actions/charts/heatmap.js +2 -2
  24. package/src/actions/charts/histogram.js +1 -1
  25. package/src/actions/charts/horizon.js +71 -0
  26. package/src/actions/charts/index.js +37 -0
  27. package/src/actions/charts/interval-regression.js +169 -0
  28. package/src/actions/charts/line.js +5 -5
  29. package/src/actions/charts/parallel.js +1 -1
  30. package/src/actions/charts/pie.js +44 -0
  31. package/src/actions/charts/polar.js +123 -0
  32. package/src/actions/charts/radar.js +299 -0
  33. package/src/actions/charts/radial.js +42 -0
  34. package/src/actions/charts/raincloud.js +422 -0
  35. package/src/actions/charts/rug-strip.js +279 -0
  36. package/src/actions/charts/scatter.js +9 -3
  37. package/src/actions/charts/shared.js +69 -10
  38. package/src/actions/composition/actions.js +118 -1
  39. package/src/actions/data/bin.js +29 -0
  40. package/src/actions/data/computed.js +24 -0
  41. package/src/actions/data/derived.js +66 -0
  42. package/src/actions/data/ecdf.js +38 -0
  43. package/src/actions/data/filter.js +212 -28
  44. package/src/actions/data/fold.js +24 -0
  45. package/src/actions/data/index.js +24 -0
  46. package/src/actions/data/interval.js +2 -0
  47. package/src/actions/data/intervalEdit.js +86 -4
  48. package/src/actions/data/regression.js +5 -2
  49. package/src/actions/data/stack.js +26 -0
  50. package/src/actions/data/summary.js +26 -0
  51. package/src/actions/data/timeUnit.js +1 -1
  52. package/src/actions/encodings/appearance.js +32 -13
  53. package/src/actions/encodings/barWidth.js +8 -0
  54. package/src/actions/encodings/basic.js +2 -0
  55. package/src/actions/encodings/color/continuous.js +19 -59
  56. package/src/actions/encodings/color/index.js +28 -22
  57. package/src/actions/encodings/color/layout.js +3 -97
  58. package/src/actions/encodings/color/policy.js +5 -8
  59. package/src/actions/encodings/density/resolve.js +37 -5
  60. package/src/actions/encodings/density.js +120 -33
  61. package/src/actions/encodings/horizon/resolve.js +8 -4
  62. package/src/actions/encodings/horizon.js +18 -8
  63. package/src/actions/encodings/index.js +2 -0
  64. package/src/actions/encodings/offset.js +33 -27
  65. package/src/actions/encodings/parallel.js +2 -8
  66. package/src/actions/encodings/position/apply.js +40 -4
  67. package/src/actions/encodings/position/index.js +43 -5
  68. package/src/actions/encodings/position/policies/arc.js +37 -2
  69. package/src/actions/encodings/position/policies/area.js +4 -42
  70. package/src/actions/encodings/position/policies/bar.js +48 -13
  71. package/src/actions/encodings/position/resolve.js +79 -24
  72. package/src/actions/encodings/ranged.js +123 -97
  73. package/src/actions/encodings/remove.js +37 -32
  74. package/src/actions/encodings/ruleAppearance.js +23 -9
  75. package/src/actions/encodings/seriesLayout.js +104 -0
  76. package/src/actions/encodings/shared.js +25 -23
  77. package/src/actions/encodings/strokeDash.js +9 -6
  78. package/src/actions/encodings/temporal.js +7 -0
  79. package/src/actions/encodings/text.js +30 -15
  80. package/src/actions/errorBands/create.js +18 -5
  81. package/src/actions/errorBands/edit.js +341 -21
  82. package/src/actions/errorBars/create.js +153 -11
  83. package/src/actions/errorBars/edit.js +365 -19
  84. package/src/actions/facets/actions.js +410 -12
  85. package/src/actions/facets/derive.js +57 -8
  86. package/src/actions/gradientPlots/components.js +33 -11
  87. package/src/actions/gradientPlots/materialize.js +5 -7
  88. package/src/actions/guides/applicability.js +20 -17
  89. package/src/actions/guides/axes/axes.js +4 -3
  90. package/src/actions/guides/axes/axis.js +18 -18
  91. package/src/actions/guides/axes/components.js +43 -0
  92. package/src/actions/guides/axes/edit.js +16 -51
  93. package/src/actions/guides/axes/labels.js +129 -19
  94. package/src/actions/guides/axes/lines.js +7 -5
  95. package/src/actions/guides/axes/parallel/lifecycle.js +117 -0
  96. package/src/actions/guides/axes/parallel/policy.js +137 -0
  97. package/src/actions/guides/axes/parallel/resolve.js +44 -23
  98. package/src/actions/guides/axes/parallel.js +93 -217
  99. package/src/actions/guides/axes/policy.js +21 -67
  100. package/src/actions/guides/axes/tickGroups.js +14 -8
  101. package/src/actions/guides/axes/ticks.js +5 -4
  102. package/src/actions/guides/axes/titles.js +21 -8
  103. package/src/actions/guides/basic.js +2 -0
  104. package/src/actions/guides/facade.js +106 -0
  105. package/src/actions/guides/facadeAxes.js +200 -0
  106. package/src/actions/guides/facadeLegend.js +63 -0
  107. package/src/actions/guides/grids/grid.js +37 -33
  108. package/src/actions/guides/grids/resolve.js +10 -2
  109. package/src/actions/guides/guides.js +35 -28
  110. package/src/actions/guides/legends/categorical/actions.js +193 -249
  111. package/src/actions/guides/legends/categorical/components.js +4 -5
  112. package/src/actions/guides/legends/categorical/layout.js +36 -349
  113. package/src/actions/guides/legends/categorical/options.js +30 -14
  114. package/src/actions/guides/legends/categorical/recipes.js +14 -0
  115. package/src/actions/guides/legends/categorical/resolve.js +21 -10
  116. package/src/actions/guides/legends/categorical/symbols.js +2 -3
  117. package/src/actions/guides/legends/continuous/common.js +69 -14
  118. package/src/actions/guides/legends/continuous/gradient.js +111 -83
  119. package/src/actions/guides/legends/continuous/index.js +6 -2
  120. package/src/actions/guides/legends/continuous/interval.js +71 -92
  121. package/src/actions/guides/legends/continuous/opacity.js +105 -84
  122. package/src/actions/guides/legends/creation.js +27 -0
  123. package/src/actions/guides/legends/edit.js +198 -66
  124. package/src/actions/guides/legends/focused.js +2 -2
  125. package/src/actions/guides/legends/lane.js +98 -86
  126. package/src/actions/guides/legends/lifecycle.js +132 -0
  127. package/src/actions/guides/legends/remove.js +31 -76
  128. package/src/actions/guides/legends/size.js +149 -105
  129. package/src/actions/guides/legends/strokeWidth.js +106 -64
  130. package/src/actions/guides/legends/target.js +12 -0
  131. package/src/actions/guides/legends/transition.js +43 -0
  132. package/src/actions/guides/polar/axes/facade.js +77 -62
  133. package/src/actions/guides/polar/axes/labels.js +5 -4
  134. package/src/actions/guides/polar/axes/lines.js +2 -1
  135. package/src/actions/guides/polar/axes/shared.js +6 -0
  136. package/src/actions/guides/polar/axes/ticks.js +5 -4
  137. package/src/actions/guides/polar/axes/titles.js +5 -4
  138. package/src/actions/guides/polar/grids.js +7 -7
  139. package/src/actions/guides/reuse.js +44 -0
  140. package/src/actions/guides/tickValues.js +19 -21
  141. package/src/actions/index.js +2 -0
  142. package/src/actions/intervals/resolve.js +35 -15
  143. package/src/actions/marks/arc/actions.js +1 -1
  144. package/src/actions/marks/area/actions.js +18 -4
  145. package/src/actions/marks/area/materialize.js +5 -2
  146. package/src/actions/marks/bar/materialize.js +11 -7
  147. package/src/actions/marks/index.js +5 -0
  148. package/src/actions/marks/line/actions.js +43 -108
  149. package/src/actions/marks/line/materialize.js +34 -52
  150. package/src/actions/marks/point/edit.js +11 -15
  151. package/src/actions/marks/point/index.js +1 -0
  152. package/src/actions/marks/point/jitter.js +3 -0
  153. package/src/actions/marks/point/materialize.js +138 -14
  154. package/src/actions/marks/point/packing.js +68 -0
  155. package/src/actions/marks/references.js +87 -0
  156. package/src/actions/marks/remove.js +17 -4
  157. package/src/actions/marks/rule/actions.js +60 -4
  158. package/src/actions/marks/rule/appearance.js +26 -0
  159. package/src/actions/marks/shared.js +12 -12
  160. package/src/actions/marks/text/actions.js +224 -6
  161. package/src/actions/marks/text/layout.js +2 -2
  162. package/src/actions/primitives/createGraphics.js +3 -1
  163. package/src/actions/primitives/semantic.js +1 -1
  164. package/src/actions/primitives/semanticAction.js +39 -2
  165. package/src/actions/primitives/semanticValidation/guide.js +10 -0
  166. package/src/actions/primitives/semanticValidation/layer.js +18 -1
  167. package/src/actions/primitives/semanticValidation/scale.js +10 -1
  168. package/src/actions/regression/create.js +1 -1
  169. package/src/actions/regression/edit.js +19 -6
  170. package/src/actions/regression/resolve.js +1 -1
  171. package/src/actions/scales/consumers/common.js +20 -53
  172. package/src/actions/scales/consumers/families.js +27 -14
  173. package/src/actions/scales/consumers/index.js +23 -2
  174. package/src/actions/scales/consumers/seriesLayout.js +33 -2
  175. package/src/actions/scales/create.js +4 -4
  176. package/src/actions/scales/definitions.js +19 -3
  177. package/src/actions/scales/edit.js +46 -34
  178. package/src/actions/scales/editPolicy.js +21 -31
  179. package/src/actions/scales/materialize.js +10 -46
  180. package/src/actions/scales/patch.js +1 -1
  181. package/src/actions/scales/preview.js +39 -0
  182. package/src/actions/scales/quantitativeColor.js +9 -5
  183. package/src/actions/theme/actions.js +43 -0
  184. package/src/actions/theme/index.js +9 -0
  185. package/src/actions/theme/reconcile.js +882 -0
  186. package/src/actions/titles/actions.js +11 -10
  187. package/src/actions/titles/resolve.js +1 -40
  188. package/src/actions/violinPlots/create.js +86 -42
  189. package/src/actions/violinPlots/edit.js +191 -0
  190. package/src/actions/violinPlots/index.js +2 -0
  191. package/src/core/ChartProgram.js +7 -0
  192. package/src/core/action.js +22 -2
  193. package/src/core/compositionState.js +136 -8
  194. package/src/core/vocabulary.js +2 -2
  195. package/src/grammar/aggregate.js +66 -23
  196. package/src/grammar/arcs.js +213 -65
  197. package/src/grammar/areaEndpoints.js +46 -0
  198. package/src/grammar/areaSeries.js +87 -41
  199. package/src/grammar/bars/aggregate.js +33 -12
  200. package/src/grammar/bars/policy.js +9 -7
  201. package/src/grammar/bin.js +181 -0
  202. package/src/grammar/categoryOrder.js +34 -0
  203. package/src/grammar/computed.js +150 -0
  204. package/src/grammar/ecdf.js +198 -0
  205. package/src/grammar/facets/index.js +117 -6
  206. package/src/grammar/fold.js +123 -0
  207. package/src/grammar/horizon.js +6 -4
  208. package/src/grammar/interval.js +37 -20
  209. package/src/grammar/lineSeries.js +20 -63
  210. package/src/grammar/markFilter.js +22 -4
  211. package/src/grammar/markLabels.js +93 -0
  212. package/src/grammar/parallelAxisTitles.js +16 -0
  213. package/src/grammar/pathSeries.js +91 -0
  214. package/src/grammar/pointPacking.js +183 -0
  215. package/src/grammar/positionCompatibility.js +1 -1
  216. package/src/grammar/positionDatum.js +12 -0
  217. package/src/grammar/rects.js +9 -1
  218. package/src/grammar/regression/derive.js +4 -0
  219. package/src/grammar/regression/index.js +7 -2
  220. package/src/grammar/regression/models.js +22 -7
  221. package/src/grammar/regression/parameters.js +40 -13
  222. package/src/grammar/rotation.js +29 -0
  223. package/src/grammar/rules.js +9 -21
  224. package/src/grammar/scales/color.js +23 -1
  225. package/src/grammar/scales/colorConsumers.js +38 -0
  226. package/src/grammar/scales/definition.js +15 -1
  227. package/src/grammar/scales/discretized.js +11 -5
  228. package/src/grammar/scales/fields.js +38 -8
  229. package/src/grammar/scales/mapping.js +2 -0
  230. package/src/grammar/scales/ordinal.js +23 -0
  231. package/src/grammar/scales/radial.js +55 -0
  232. package/src/grammar/scales/types.js +1 -0
  233. package/src/grammar/schemas/semanticPath.js +17 -3
  234. package/src/grammar/stack.js +170 -0
  235. package/src/grammar/statistics/confidenceInterval.js +101 -0
  236. package/src/grammar/summary.js +183 -0
  237. package/src/grammar/text.js +22 -16
  238. package/src/grammar/timeUnit.js +21 -11
  239. package/src/grammar/transformTopology.js +6 -0
  240. package/src/grammar/transforms.js +36 -0
  241. package/src/grammar/valueFormat.js +112 -0
  242. package/src/grammar/window.js +13 -7
  243. package/src/layout/facets.js +86 -23
  244. package/src/layout/guideCollisions.js +12 -0
  245. package/src/layout/legend.js +2 -10
  246. package/src/layout/legendItems.js +102 -0
  247. package/src/layout/legendLane.js +100 -11
  248. package/src/materialization/bars/aggregate.js +38 -152
  249. package/src/materialization/bars/histogram.js +24 -0
  250. package/src/materialization/categorySlotOffset.js +40 -0
  251. package/src/materialization/compositionSnapshot.js +1 -1
  252. package/src/materialization/dependencies.js +57 -16
  253. package/src/materialization/encodings.js +1 -1
  254. package/src/materialization/facetGuides/legacyCategorical.js +7 -1
  255. package/src/materialization/facetGuides/placement.js +16 -1
  256. package/src/materialization/facetGuides/preparation.js +26 -1
  257. package/src/materialization/facets.js +11 -2
  258. package/src/materialization/guides/layout.js +57 -0
  259. package/src/materialization/guides/resources.js +2 -2
  260. package/src/materialization/legends.js +10 -1
  261. package/src/materialization/marks/capabilities.js +30 -7
  262. package/src/materialization/marks/grouping.js +52 -0
  263. package/src/materialization/marks/index.js +63 -25
  264. package/src/materialization/marks/policies.js +2 -0
  265. package/src/materialization/rect.js +33 -33
  266. package/src/materialization/rowEncoding.js +70 -14
  267. package/src/materialization/scaleGuideDependencies.js +8 -5
  268. package/src/materialization/scales/map.js +1 -0
  269. package/src/materialization/scales/policies/arc.js +42 -0
  270. package/src/materialization/scales/policies/bar.js +0 -43
  271. package/src/materialization/scales/policies/binnedPosition.js +7 -4
  272. package/src/materialization/scales/policies/offset.js +54 -0
  273. package/src/materialization/scales/policies/series.js +2 -1
  274. package/src/materialization/scales/resolve.js +48 -18
  275. package/src/materialization/selection/compatibility.js +11 -0
  276. package/src/materialization/selection/items/bar.js +12 -68
  277. package/src/materialization/selection/items/common.js +8 -2
  278. package/src/materialization/selection/items/path.js +3 -13
  279. package/src/materialization/selection/items/rect.js +7 -4
  280. package/src/materialization/text.js +64 -5
  281. package/src/mcp/adapter.js +2 -2
  282. package/src/selectors/datasets.js +13 -0
  283. package/src/selectors/layers.js +3 -2
  284. package/src/selectors/scales.js +6 -0
  285. package/src/theme/defaults.js +39 -0
  286. package/types/basic.d.ts +11 -2
  287. package/types/index.d.ts +126 -0
  288. package/types/program.d.ts +1327 -124
  289. package/src/materialization/bars/grouped.js +0 -144
@@ -11,6 +11,10 @@ import {
11
11
  normalizePointJitterPolicy,
12
12
  resolvePointJitter
13
13
  } from "../../../grammar/jitter.js";
14
+ import {
15
+ normalizePointPackingPolicy,
16
+ resolvePointPacking
17
+ } from "../../../grammar/pointPacking.js";
14
18
  import { polarToCartesian, resolvePolarFrame } from "../../../grammar/polar.js";
15
19
  import { resolveDirectionValues } from "../../../grammar/direction.js";
16
20
  import { resolveGraphicBounds } from "../../../layout/canvas.js";
@@ -21,9 +25,15 @@ import {
21
25
  } from "../../../theme/defaults.js";
22
26
  import { findDataset } from "../../../selectors/datasets.js";
23
27
  import { findLayer } from "../../../selectors/layers.js";
28
+ import { canMaterializePoint } from "../../../materialization/marks/capabilities.js";
24
29
  import { rematerializeHighlightBaseline } from "../lifecycle.js";
25
- import { resolveRowEncodingValues } from
30
+ import {
31
+ resolveRowEncodingValues,
32
+ resolveRowPositionValues
33
+ } from
26
34
  "../../../materialization/rowEncoding.js";
35
+ import { offsetCategoryPositions } from
36
+ "../../../materialization/categorySlotOffset.js";
27
37
 
28
38
  const REMATERIALIZE_OPTIONS = Object.freeze(["id"]);
29
39
  const DEFAULT_POINT_FILL = DEFAULT_COLORS.mark;
@@ -33,8 +43,8 @@ function resolvePointPositions(program, layer, dataset) {
33
43
  layer.encoding?.radius !== undefined;
34
44
  if (!hasPolar) {
35
45
  return {
36
- x: resolveRowEncodingValues(program, layer, dataset, "x"),
37
- y: resolveRowEncodingValues(program, layer, dataset, "y")
46
+ x: resolveRowPositionValues(program, layer, dataset, "x"),
47
+ y: resolveRowPositionValues(program, layer, dataset, "y")
38
48
  };
39
49
  }
40
50
  const theta = resolveRowEncodingValues(program, layer, dataset, "theta");
@@ -207,6 +217,80 @@ function applyPointJitter(program, {
207
217
  };
208
218
  }
209
219
 
220
+ function applyPointPacking(program, {
221
+ id,
222
+ layer,
223
+ dataset,
224
+ x,
225
+ y,
226
+ area,
227
+ shapes,
228
+ angles,
229
+ config,
230
+ graphic,
231
+ existingChildren
232
+ }) {
233
+ const stored = program.materializationConfigs.pointPacking?.[id];
234
+ if (stored === undefined) return { program, x, y };
235
+ const policy = normalizePointPackingPolicy(stored);
236
+ const encoding = layer.encoding?.[policy.channel];
237
+ const scale = program.resolvedScales[encoding?.scale];
238
+ if (scale === undefined) {
239
+ throw new Error(`Point packing on "${id}" requires a resolved ${policy.channel} scale.`);
240
+ }
241
+ const seen = new Set();
242
+ const entries = dataset.values.flatMap((row, index) => {
243
+ if (!Number.isFinite(x?.[index]) || !Number.isFinite(y?.[index])) return [];
244
+ const rawIdentity = policy.key === undefined ? index : row[policy.key];
245
+ const identity = canonicalJitterScalar(
246
+ rawIdentity,
247
+ policy.key === undefined
248
+ ? "Point packing source index"
249
+ : `Point packing key field "${policy.key}"`
250
+ );
251
+ if (seen.has(identity)) {
252
+ throw new Error(`Point packing key field "${policy.key}" must be unique for materialized items.`);
253
+ }
254
+ seen.add(identity);
255
+ const resolvedArea = resolvedPointArea(
256
+ area, index, config, existingChildren, graphic.type
257
+ );
258
+ const extent = resolvePointShapeExtent({
259
+ shape: shapes[index],
260
+ area: resolvedArea,
261
+ strokeWidth: typeof config.stroke === "string" ? config.strokeWidth ?? 1 : 0,
262
+ angle: angles?.[index]
263
+ });
264
+ return [{
265
+ index,
266
+ identity,
267
+ base: policy.channel === "x" ? x[index] : y[index],
268
+ fixed: policy.channel === "x" ? y[index] : x[index],
269
+ halfExtent: extent[policy.channel],
270
+ fixedHalfExtent: extent[policy.channel === "x" ? "y" : "x"]
271
+ }];
272
+ });
273
+ const bounds = resolveGraphicBounds(program);
274
+ const resolution = resolvePointPacking({
275
+ target: id,
276
+ policy,
277
+ scale,
278
+ entries,
279
+ plotMinimum: policy.channel === "x" ? bounds.x : bounds.y,
280
+ plotMaximum: policy.channel === "x" ? bounds.x + bounds.width : bounds.y + bounds.height
281
+ });
282
+ const values = [...(policy.channel === "x" ? x : y)];
283
+ for (const item of resolution.items) values[item.index] = item.final;
284
+ return {
285
+ program: program._withMaterializationConfig(["pointPacking", id], {
286
+ ...policy,
287
+ resolved: resolution
288
+ }),
289
+ x: policy.channel === "x" ? values : x,
290
+ y: policy.channel === "y" ? values : y
291
+ };
292
+ }
293
+
210
294
  export const rematerializePointMark = action(
211
295
  {
212
296
  op: "rematerializePointMark",
@@ -222,8 +306,20 @@ export const rematerializePointMark = action(
222
306
  resetValue: []
223
307
  });
224
308
  if (highlighted !== undefined) return highlighted;
225
- const layer = findLayer(this, id);
226
- const graphic = this.graphicSpec.objects[id];
309
+ let resolved = this;
310
+ const initialLayer = findLayer(resolved, id);
311
+ for (const channel of ["xOffset", "yOffset"]) {
312
+ const scaleId = initialLayer?.encoding?.[channel]?.scale;
313
+ if (scaleId !== undefined) {
314
+ resolved = resolved.rematerializeScale({
315
+ id: scaleId,
316
+ guides: false,
317
+ marks: false
318
+ });
319
+ }
320
+ }
321
+ const layer = findLayer(resolved, id);
322
+ const graphic = resolved.graphicSpec.objects[id];
227
323
 
228
324
  if (layer?.mark?.type !== "point") {
229
325
  throw new Error(`Unknown point mark "${id}".`);
@@ -231,18 +327,27 @@ export const rematerializePointMark = action(
231
327
  if (!["circle", "rect", "path", "collection"].includes(graphic?.type)) {
232
328
  throw new Error(`Point mark "${id}" requires point graphics.`);
233
329
  }
234
- const dataset = findDataset(this, layer.data);
330
+ const dataset = findDataset(resolved, layer.data);
235
331
  if (dataset === undefined) {
236
332
  throw new Error(`Point mark "${id}" requires an existing dataset.`);
237
333
  }
238
334
 
239
- const positions = resolvePointPositions(this, layer, dataset);
240
- const mappedFill = resolveRowEncodingValues(this, layer, dataset, "color");
241
- const area = resolveRowEncodingValues(this, layer, dataset, "size");
242
- const encodedShape = resolveRowEncodingValues(this, layer, dataset, "shape");
243
- const encodedOpacity = resolveRowEncodingValues(this, layer, dataset, "opacity");
335
+ if (graphic.items.length === 0 && !canMaterializePoint(resolved, layer)) {
336
+ return resolved;
337
+ }
338
+
339
+ const positions = resolvePointPositions(resolved, layer, dataset);
340
+ const mappedFill = resolveRowEncodingValues(resolved, layer, dataset, "color");
341
+ const area = resolveRowEncodingValues(resolved, layer, dataset, "size");
342
+ const encodedShape = resolveRowEncodingValues(resolved, layer, dataset, "shape");
343
+ const encodedOpacity = resolveRowEncodingValues(
344
+ resolved,
345
+ layer,
346
+ dataset,
347
+ "opacity"
348
+ );
244
349
  const angles = resolveDirectionValues(dataset.values, layer.encoding?.angle);
245
- const config = this.markConfigs[id] ?? {};
350
+ const config = resolved.markConfigs[id] ?? {};
246
351
  const fill = mappedFill ?? config.fill ?? DEFAULT_POINT_FILL;
247
352
  const shapes = encodedShape ?? dataset.values.map(() => config.shape ?? "circle");
248
353
  const existingChildren = graphic.items ?? [];
@@ -252,7 +357,13 @@ export const rematerializePointMark = action(
252
357
  angles !== undefined ||
253
358
  graphic.type === "collection" ||
254
359
  getPointGraphicType(constantShape) !== graphic.type;
255
- const jittered = applyPointJitter(this, {
360
+ if (
361
+ resolved.materializationConfigs.jitters?.[id] !== undefined &&
362
+ resolved.materializationConfigs.pointPacking?.[id] !== undefined
363
+ ) {
364
+ throw new Error(`Point mark "${id}" cannot combine jitter and packing.`);
365
+ }
366
+ const packed = applyPointPacking(resolved, {
256
367
  id,
257
368
  layer,
258
369
  dataset,
@@ -265,7 +376,20 @@ export const rematerializePointMark = action(
265
376
  graphic,
266
377
  existingChildren
267
378
  });
268
- const { x, y } = jittered;
379
+ const jittered = applyPointJitter(packed.program, {
380
+ id,
381
+ layer,
382
+ dataset,
383
+ x: packed.x,
384
+ y: packed.y,
385
+ area,
386
+ shapes,
387
+ angles,
388
+ config,
389
+ graphic,
390
+ existingChildren
391
+ });
392
+ const { x, y } = offsetCategoryPositions(jittered.program, layer, jittered);
269
393
 
270
394
  if (requiresMixedCollection) {
271
395
  const items = dataset.values.map((_, index) => {
@@ -0,0 +1,68 @@
1
+ import { action } from "../../../core/action.js";
2
+ import { validateUserId } from "../../../core/identifiers.js";
3
+ import { validateKeys } from "../../../core/validation.js";
4
+ import { normalizePointPackingPolicy } from "../../../grammar/pointPacking.js";
5
+ import { findCoordinate } from "../../../selectors/coordinates.js";
6
+ import { resolveEligibleLayer } from "../../../selectors/layers.js";
7
+
8
+ const OPTIONS = Object.freeze([
9
+ "target", "channel", "maxOffset", "padding", "key", "overflow"
10
+ ]);
11
+ const REMOVE_OPTIONS = Object.freeze(["target"]);
12
+
13
+ function isPackablePoint(program, layer, channel) {
14
+ const packed = layer.encoding?.[channel];
15
+ const fixed = layer.encoding?.[channel === "x" ? "y" : "x"];
16
+ return layer.mark?.type === "point" &&
17
+ ["nominal", "ordinal"].includes(packed?.fieldType) &&
18
+ ["quantitative", "temporal"].includes(fixed?.fieldType) &&
19
+ packed.scale !== undefined && fixed.scale !== undefined &&
20
+ findCoordinate(program, layer.coordinate)?.type === "cartesian";
21
+ }
22
+
23
+ function target(program, requested, channel) {
24
+ return resolveEligibleLayer(program, {
25
+ target: requested === undefined ? undefined : validateUserId(requested, "Point packing target"),
26
+ predicate: layer => isPackablePoint(program, layer, channel),
27
+ label: "point packing"
28
+ });
29
+ }
30
+
31
+ export const packPoints = action(
32
+ { op: "packPoints", description: "Pack point glyphs deterministically within categorical slots." },
33
+ function (args = {}) {
34
+ validateKeys(args, OPTIONS, "packPoints");
35
+ const policy = normalizePointPackingPolicy(args);
36
+ const layer = target(this, args.target, policy.channel);
37
+ if (this.materializationConfigs.jitters?.[layer.id] !== undefined) {
38
+ throw new Error(`Point packing on "${layer.id}" conflicts with point jitter.`);
39
+ }
40
+ return this
41
+ ._withMaterializationConfig(["pointPacking", layer.id], policy)
42
+ .rematerializePointMark({ id: layer.id });
43
+ }
44
+ );
45
+
46
+ export const removePointPacking = action(
47
+ { op: "removePointPacking", description: "Remove point packing and restore semantic positions." },
48
+ function (args = {}) {
49
+ validateKeys(args, REMOVE_OPTIONS, "removePointPacking");
50
+ const requested = args.target === undefined
51
+ ? undefined
52
+ : validateUserId(args.target, "Point packing target");
53
+ const layer = resolveEligibleLayer(this, {
54
+ target: requested,
55
+ predicate: candidate => candidate.mark?.type === "point" &&
56
+ this.materializationConfigs.pointPacking?.[candidate.id] !== undefined,
57
+ label: "point packing"
58
+ });
59
+ return this
60
+ ._withoutMaterializationConfig(["pointPacking", layer.id])
61
+ .rematerializePointMark({ id: layer.id });
62
+ }
63
+ );
64
+
65
+ export function registerPointPackingActions(ProgramClass) {
66
+ ProgramClass.prototype.packPoints = packPoints;
67
+ ProgramClass.prototype.removePointPacking = removePointPacking;
68
+ }
@@ -0,0 +1,87 @@
1
+ import { isSourceOwnedText } from "../../grammar/text.js";
2
+ import { action } from "../../core/action.js";
3
+ import { validateUserId } from "../../core/identifiers.js";
4
+ import { findCoordinate } from "../../selectors/coordinates.js";
5
+ import { resolveEligibleLayer } from "../../selectors/layers.js";
6
+ import { resolveFacadeData, resolveFacadeId, validateFacadeOptions } from "../charts/shared.js";
7
+
8
+ const COMMON_OPTIONS = ["id", "x", "y", "space", "source", "data", "coordinate", "temporalUnit"];
9
+ const LINE_STYLE = ["stroke", "strokeWidth", "strokeDash", "opacity"];
10
+ const BAND_STYLE = ["fill", "opacity", "stroke", "strokeWidth"];
11
+
12
+ function resolveBinding(program, args, axis, operation, band) {
13
+ const space = args.space === undefined ? "data" : args.space;
14
+ if (space !== "data" && space !== "plot") {
15
+ throw new Error(`${operation} space must be data or plot.`);
16
+ }
17
+ if (space === "plot") {
18
+ if (Object.hasOwn(args, "source") || Object.hasOwn(args, "temporalUnit")) {
19
+ throw new Error(`${operation} plot space does not accept source or temporalUnit.`);
20
+ }
21
+ return { data: resolveFacadeData(program, args.data, operation),
22
+ fieldType: "quantitative", coordinate: args.coordinate, plot: true };
23
+ }
24
+ if (Object.hasOwn(args, "data") || Object.hasOwn(args, "coordinate")) {
25
+ throw new Error(`${operation} data space uses source data and coordinate.`);
26
+ }
27
+ const source = resolveEligibleLayer(program, {
28
+ target: args.source === undefined ? undefined : validateUserId(args.source, `${operation} source`),
29
+ predicate: layer => !isSourceOwnedText(layer) && layer.data !== undefined && layer.encoding?.[axis]?.scale !== undefined &&
30
+ findCoordinate(program, layer.coordinate)?.type === "cartesian" &&
31
+ (!band || ["quantitative", "temporal"].includes(layer.encoding[axis].fieldType)),
32
+ label: `${operation} Cartesian layer`,
33
+ targetOption: "source"
34
+ });
35
+ const encoding = source.encoding[axis];
36
+ return { data: source.data, coordinate: source.coordinate, scale: encoding.scale,
37
+ fieldType: encoding.fieldType, temporalUnit: args.temporalUnit === undefined ? encoding.temporalUnit : args.temporalUnit };
38
+ }
39
+
40
+ function createReference(program, args, band) {
41
+ const operation = band ? "createReferenceBand" : "createReferenceLine";
42
+ const styleOptions = band ? BAND_STYLE : LINE_STYLE;
43
+ validateFacadeOptions(args, [...COMMON_OPTIONS, ...styleOptions], operation);
44
+ const axes = ["x", "y"].filter(axis => Object.hasOwn(args, axis));
45
+ if (axes.length !== 1) throw new Error(`${operation} requires exactly one of x or y.`);
46
+ const axis = axes[0];
47
+ const values = band ? args[axis] : [args[axis]];
48
+ if (!Array.isArray(values) || values.length !== (band ? 2 : 1)) {
49
+ throw new Error(`${operation} ${axis} requires a two-value interval.`);
50
+ }
51
+ const id = resolveFacadeId(program, args.id, {
52
+ defaultId: band ? "referenceBand" : "referenceLine", operation
53
+ });
54
+ const binding = resolveBinding(program, args, axis, operation, band);
55
+ if (binding.plot && !values.every(value => Number.isFinite(value) && value >= 0 && value <= 1)) {
56
+ throw new Error(`${operation} plot coordinates must be finite numbers in [0, 1].`);
57
+ }
58
+ const scale = binding.plot ? `${id}-${axis}` : binding.scale;
59
+ const style = Object.fromEntries(styleOptions.filter(key => args[key] !== undefined).map(key => [key, args[key]]));
60
+ const encode = axis === "x" ? "encodeX" : "encodeY";
61
+ const options = { target: id, fieldType: binding.fieldType, scale: { id: scale },
62
+ ...(binding.coordinate === undefined ? {} : { coordinate: binding.coordinate }),
63
+ ...(binding.temporalUnit === undefined ? {} : { temporalUnit: binding.temporalUnit }) };
64
+ const apply = initial => {
65
+ let next = binding.plot
66
+ ? initial.createScale({ id: scale, type: "linear", domain: [0, 1] })
67
+ : initial;
68
+ next = band
69
+ ? next.createRectMark({ id, data: binding.data, fill: "#94a3b8", opacity: 0.15, stroke: false, ...style })
70
+ : next.createRuleMark({ id, data: binding.data, stroke: "#64748b", strokeWidth: 1, strokeDash: "dashed", ...style });
71
+ next = next[encode]({ ...options, datum: values[0] });
72
+ return band ? next[`${encode}2`]({ ...options, datum: values[1] }) : next;
73
+ };
74
+ // Validate the complete lower-action chain on a discarded immutable branch.
75
+ apply(program);
76
+ return apply(program);
77
+ }
78
+
79
+ export const createReferenceLine = action(
80
+ { op: "createReferenceLine", description: "Create a constant reference line in data or plot coordinates." },
81
+ function (args = {}) { return createReference(this, args, false); }
82
+ );
83
+
84
+ export const createReferenceBand = action(
85
+ { op: "createReferenceBand", description: "Create a constant reference interval in data or plot coordinates." },
86
+ function (args = {}) { return createReference(this, args, true); }
87
+ );
@@ -1,8 +1,10 @@
1
+ import { isSourceOwnedText } from "../../grammar/text.js";
1
2
  import { action } from "../../core/action.js";
2
3
  import { validateUserId } from "../../core/identifiers.js";
3
4
  import { validateKeys } from "../../core/validation.js";
4
5
  import { findDataset } from "../../selectors/datasets.js";
5
6
  import { findLayer } from "../../selectors/layers.js";
7
+ import { applyDetachedScaleRematerialization } from "../../materialization/dependencies.js";
6
8
  import {
7
9
  getPositionChannelDefinition,
8
10
  POSITION_CHANNELS
@@ -28,12 +30,17 @@ function ownedChildren(program, id) {
28
30
  config.boxPlot?.whiskerId,
29
31
  config.boxPlot?.medianId,
30
32
  config.boxPlot?.outlierId,
31
- config.gradientPlot?.centerId
33
+ config.gradientPlot?.centerId,
34
+ config.intervalPlot?.intervalId,
35
+ config.endpointPlot?.roles?.stemId,
36
+ config.endpointPlot?.roles?.startId,
37
+ config.endpointPlot?.roles?.connectorId,
38
+ ...(config.raincloudPlot?.ownedChildIds ?? [])
32
39
  ].concat(
33
40
  program.semanticSpec.layers
34
41
  .filter(layer => layer.source === id)
35
42
  .map(layer => layer.id)
36
- ).filter(child => child !== undefined && findLayer(program, child) !== undefined);
43
+ ).filter(child => child !== undefined && child !== id && findLayer(program, child) !== undefined);
37
44
  }
38
45
 
39
46
  function ownership(program) {
@@ -90,7 +97,10 @@ function ownedDerivedData(program, ids) {
90
97
  config.regression?.dataId,
91
98
  config.boxPlot?.summaryId,
92
99
  config.boxPlot?.outlierDataId,
93
- config.gradientPlot?.profileId
100
+ config.gradientPlot?.profileId,
101
+ config.violinPlot?.materialized === true ? layer?.data : undefined,
102
+ config.ecdfPlot?.data,
103
+ config.endpointPlot?.data
94
104
  ]) {
95
105
  if (candidate !== undefined) candidates.add(candidate);
96
106
  }
@@ -161,7 +171,7 @@ export function removeOwnedMark(program, id, partial = false) {
161
171
 
162
172
  function hasScaleConsumer(program, channel, scale) {
163
173
  return program.semanticSpec.layers.some(
164
- layer => layer.encoding?.[channel]?.scale === scale
174
+ layer => !isSourceOwnedText(layer) && layer.encoding?.[channel]?.scale === scale
165
175
  );
166
176
  }
167
177
 
@@ -198,6 +208,7 @@ export const removeMark = action(
198
208
  validateKeys(args, OPTIONS, "removeMark");
199
209
  const owner = resolveOwner(this, args.target);
200
210
  const ids = collectClosure(this, owner.id);
211
+ const previousLayers = ids.map(id => findLayer(this, id));
201
212
  const derived = ownedDerivedData(this, ids);
202
213
  const positionScales = usedPositionScales(this, ids);
203
214
  let next = this;
@@ -227,6 +238,7 @@ export const removeMark = action(
227
238
  }
228
239
  next = next._withoutMaterializationConfig(["marks", id]);
229
240
  next = next._withoutMaterializationConfig(["jitters", id]);
241
+ next = next._withoutMaterializationConfig(["pointPacking", id]);
230
242
  next = next._withoutMaterializationConfig(["labelLayouts", id]);
231
243
  }
232
244
 
@@ -236,6 +248,7 @@ export const removeMark = action(
236
248
  next = next.releaseDerivedData({ id });
237
249
  }
238
250
  }
251
+ next = applyDetachedScaleRematerialization(next, previousLayers);
239
252
  return next._withContext({
240
253
  ...(ids.includes(this.context.currentMark) ? { currentMark: undefined } : {}),
241
254
  ...(selectionCleanup.selectionIds.includes(this.context.currentSelection)
@@ -3,6 +3,7 @@ import { validateUserId } from "../../../core/identifiers.js";
3
3
  import { validatePositiveFinite } from "../../../core/validation.js";
4
4
  import {
5
5
  mapContinuousScaleValues,
6
+ mapOrdinalOffsetValues,
6
7
  mapOrdinalPositionValues,
7
8
  mapOrdinalValues,
8
9
  normalizeStrokeDashPattern
@@ -10,7 +11,8 @@ import {
10
11
  import { deriveRuleValues, resolveRuleMode } from "../../../grammar/rules.js";
11
12
  import { resolveGraphicBounds } from "../../../layout/canvas.js";
12
13
  import { findDataset } from "../../../selectors/datasets.js";
13
- import { findLayer } from "../../../selectors/layers.js";
14
+ import { findLayer, resolveEligibleLayer } from "../../../selectors/layers.js";
15
+ import { applyRuleAppearance, planRuleAppearance } from "./appearance.js";
14
16
  import { DEFAULT_COLORS } from "../../../theme/defaults.js";
15
17
  import {
16
18
  assertMarkAvailable,
@@ -25,8 +27,13 @@ import {
25
27
  import { resolveMarkGraphicPlacement } from
26
28
  "../../../materialization/graphicHierarchy.js";
27
29
  import { rematerializeHighlightBaseline } from "../lifecycle.js";
30
+ import { applyOffsetPositionValues } from
31
+ "../../../materialization/rowEncoding.js";
32
+ import { resolveCategorySlotOffset } from
33
+ "../../../materialization/categorySlotOffset.js";
28
34
 
29
- const CREATE_OPTIONS = Object.freeze(["id", "data"]);
35
+ const CREATE_OPTIONS = Object.freeze(["id", "data", "stroke", "strokeWidth", "strokeDash", "opacity"]);
36
+ const EDIT_OPTIONS = Object.freeze(["target", "stroke", "strokeWidth", "strokeDash", "opacity"]);
30
37
  const REMATERIALIZE_OPTIONS = Object.freeze(["id"]);
31
38
  const SPAN_OPTIONS = Object.freeze(["id", "orientation", "size"]);
32
39
  const DEFAULT_RULE_CONFIG = Object.freeze({
@@ -93,6 +100,7 @@ const createRuleMark = action(
93
100
  inherited?.data !== undefined ? { data: inherited.data } : {})
94
101
  });
95
102
  assertMarkAvailable(this, id);
103
+ const appearance = planRuleAppearance(args, inherited);
96
104
 
97
105
  let created = this
98
106
  .editSemantic({ property: `layer[${id}].mark.type`, value: "rule" })
@@ -116,7 +124,23 @@ const createRuleMark = action(
116
124
  }
117
125
  })
118
126
  });
119
- return materializeInheritedMark(created, id);
127
+ return applyRuleAppearance(materializeInheritedMark(created, id), id, appearance);
128
+ }
129
+ );
130
+
131
+ const editRuleMark = action(
132
+ { op: "editRuleMark", description: "Edit constant rule appearance through its encoding owners." },
133
+ function (args = {}) {
134
+ validateMarkOptions(args, EDIT_OPTIONS, "editRuleMark");
135
+ if (args.target !== undefined) validateUserId(args.target, "Rule mark id");
136
+ const layer = resolveEligibleLayer(this, {
137
+ target: args.target,
138
+ predicate: candidate => candidate.mark?.type === "rule",
139
+ label: "rule mark"
140
+ });
141
+ const appearance = planRuleAppearance(args, layer);
142
+ if (appearance.length === 0) throw new Error("editRuleMark requires an appearance change.");
143
+ return applyRuleAppearance(this, layer.id, appearance);
120
144
  }
121
145
  );
122
146
 
@@ -154,7 +178,10 @@ const rematerializeRuleMark = action(
154
178
  }
155
179
 
156
180
  const scaleIds = [...new Set(
157
- ["x", "y", "x2", "y2", "strokeDash", "strokeWidth", "opacity"]
181
+ [
182
+ "x", "y", "x2", "y2", "xOffset", "yOffset",
183
+ "strokeDash", "strokeWidth", "opacity"
184
+ ]
158
185
  .map(channel => layer.encoding?.[channel]?.scale)
159
186
  .filter(scale => scale !== undefined)
160
187
  )];
@@ -178,6 +205,25 @@ const rematerializeRuleMark = action(
178
205
  resolved.resolvedScales[encoding.scale]
179
206
  );
180
207
  }
208
+ for (const [channel, offsetChannel] of [
209
+ ["x", "xOffset"],
210
+ ["y", "yOffset"]
211
+ ]) {
212
+ const encoding = layer.encoding?.[offsetChannel];
213
+ if (encoding === undefined || mapped[channel] === undefined) continue;
214
+ const offsetScale = resolved.resolvedScales[encoding.scale];
215
+ const offset = mapOrdinalOffsetValues(
216
+ derived.values[offsetChannel],
217
+ offsetScale
218
+ );
219
+ mapped[channel] = applyOffsetPositionValues({
220
+ base: mapped[channel],
221
+ offset,
222
+ parentScale: resolved.resolvedScales[layer.encoding[channel].scale],
223
+ offsetScale,
224
+ channel
225
+ });
226
+ }
181
227
  const bounds = resolveGraphicBounds(resolved);
182
228
  const x1 = [];
183
229
  const y1 = [];
@@ -238,6 +284,15 @@ const rematerializeRuleMark = action(
238
284
  x2.push(endX);
239
285
  y2.push(endY);
240
286
  }
287
+ const categoryOffset = resolveCategorySlotOffset(resolved, layer);
288
+ if (categoryOffset?.pixels) {
289
+ const targets = categoryOffset.channel === "x" ? [x1, x2] : [y1, y2];
290
+ for (const values of targets) {
291
+ for (let index = 0; index < values.length; index += 1) {
292
+ if (Number.isFinite(values[index])) values[index] += categoryOffset.pixels;
293
+ }
294
+ }
295
+ }
241
296
 
242
297
  const resolvedConfig = {
243
298
  ...DEFAULT_RULE_CONFIG,
@@ -312,6 +367,7 @@ export const materializeRuleSpan = action(
312
367
  export function registerRuleMarkActions(ProgramClass) {
313
368
  Object.assign(ProgramClass.prototype, {
314
369
  createRuleMark,
370
+ editRuleMark,
315
371
  rematerializeRuleMark,
316
372
  materializeRuleSpan
317
373
  });
@@ -0,0 +1,26 @@
1
+ import { validateRuleStroke, validateRuleStrokeWidth } from "../../../grammar/ruleAppearance.js";
2
+ import { normalizeStrokeDashPattern, validateOpacityValue } from "../../../grammar/scales/index.js";
3
+
4
+ const STYLE_ACTIONS = Object.freeze({
5
+ stroke: ["encodeStroke", validateRuleStroke],
6
+ strokeWidth: ["encodeStrokeWidth", validateRuleStrokeWidth],
7
+ strokeDash: ["encodeStrokeDash", normalizeStrokeDashPattern],
8
+ opacity: ["encodeOpacity", validateOpacityValue]
9
+ });
10
+
11
+ export function planRuleAppearance(args, layer) {
12
+ return Object.entries(STYLE_ACTIONS).flatMap(([property, [op, validate]]) => {
13
+ if (!Object.hasOwn(args, property)) return [];
14
+ if (layer?.encoding?.[property]?.field !== undefined) {
15
+ throw new Error(`Rule ${property} conflicts with a field encoding; use ${op} with value to replace it.`);
16
+ }
17
+ validate(args[property], `Rule ${property}`);
18
+ return [{ op, value: args[property] }];
19
+ });
20
+ }
21
+
22
+ export function applyRuleAppearance(program, target, plan) {
23
+ let next = program;
24
+ for (const { op, value } of plan) next = next[op]({ target, value });
25
+ return next;
26
+ }
@@ -1,8 +1,9 @@
1
+ import { isSourceOwnedText } from "../../grammar/text.js";
1
2
  import {
2
3
  resolveOptionalUserId,
3
4
  validateUserId
4
5
  } from "../../core/identifiers.js";
5
- import { findDataset } from "../../selectors/datasets.js";
6
+ import { findDataset, requireMaterializedDataset } from "../../selectors/datasets.js";
6
7
  import { findLayer, hasLayer } from "../../selectors/layers.js";
7
8
  import { findSemanticScale } from "../../selectors/scales.js";
8
9
  import { resolveMarkPositionPolicy } from "../encodings/position/policies/index.js";
@@ -27,12 +28,7 @@ export function resolveMarkData(program, requested) {
27
28
  throw new Error("Mark creation requires data or a current dataset.");
28
29
  }
29
30
 
30
- const dataset = findDataset(program, data);
31
-
32
- if (dataset === undefined) {
33
- throw new Error(`Unknown dataset "${data}".`);
34
- }
35
-
31
+ const dataset = requireMaterializedDataset(program, data);
36
32
  return { data, dataset };
37
33
  }
38
34
 
@@ -60,7 +56,7 @@ export function assertMarkAvailable(program, id) {
60
56
  throw new Error(`Mark "${id}" already exists.`);
61
57
  }
62
58
 
63
- if (program.graphicSpec.objects[id] !== undefined) {
59
+ if (Object.hasOwn(program.graphicSpec.objects, id)) {
64
60
  throw new Error(`Graphic "${id}" already exists.`);
65
61
  }
66
62
  }
@@ -78,9 +74,9 @@ const POSITION_POLICY_SUBSETS = Object.freeze([
78
74
  ]);
79
75
 
80
76
  function inheritedPositionEncodings(encoding) {
81
- if (encoding?.field === undefined) return undefined;
77
+ if (encoding?.field === undefined && encoding?.aggregate !== "count") return undefined;
82
78
  const base = Object.fromEntries(
83
- ["field", "fieldType", "scale", "title"]
79
+ ["field", "fieldType", "scale", "title", "temporalUnit"]
84
80
  .filter(property => Object.hasOwn(encoding, property))
85
81
  .map(property => [property, encoding[property]])
86
82
  );
@@ -104,6 +100,9 @@ function inheritedPositionEncodings(encoding) {
104
100
  function scaleSupportsEncoding(program, markType, channel, encoding) {
105
101
  const scale = findSemanticScale(program, encoding.scale);
106
102
  if (scale === undefined) return false;
103
+ if (scale.radialMapping !== undefined) {
104
+ return markType === "arc" && channel === "radius" && ["count", "sum"].includes(encoding.aggregate);
105
+ }
107
106
  const categorical = ["nominal", "ordinal"].includes(encoding.fieldType);
108
107
  if (categorical) {
109
108
  if (["bar", "rect"].includes(markType)) return scale.type === "band";
@@ -143,7 +142,8 @@ export function resolveCompatibleEncodings(program, source, markType) {
143
142
  layer: candidate,
144
143
  dataset,
145
144
  channel,
146
- args: encoding,
145
+ args: { ...encoding, ...(channel === "radius" && findSemanticScale(program, encoding.scale)?.radialMapping !== undefined
146
+ ? { mapping: findSemanticScale(program, encoding.scale).radialMapping } : {}) },
147
147
  field: encoding.field,
148
148
  fieldType: encoding.fieldType
149
149
  });
@@ -163,7 +163,7 @@ export function resolveCompatibleEncodings(program, source, markType) {
163
163
  }
164
164
 
165
165
  function eligibleLayeredSource(program, layer, requestedData, markType) {
166
- if (layer?.data === undefined) return false;
166
+ if (layer?.data === undefined || isSourceOwnedText(layer)) return false;
167
167
  if (requestedData !== undefined && layer.data !== requestedData) return false;
168
168
  return Object.keys(resolveCompatibleEncodings(program, layer, markType)).length > 0;
169
169
  }