ggaction 0.0.12 → 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 (281) hide show
  1. package/CHANGELOG.md +49 -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/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 +10 -16
  65. package/src/actions/encodings/parallel.js +2 -8
  66. package/src/actions/encodings/position/apply.js +10 -4
  67. package/src/actions/encodings/position/index.js +43 -5
  68. package/src/actions/encodings/position/policies/arc.js +14 -0
  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 +67 -22
  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 +21 -10
  83. package/src/actions/errorBars/edit.js +355 -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 +106 -2
  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 +34 -3
  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/grammar/aggregate.js +66 -23
  195. package/src/grammar/arcs.js +58 -1
  196. package/src/grammar/areaEndpoints.js +46 -0
  197. package/src/grammar/areaSeries.js +87 -41
  198. package/src/grammar/bars/aggregate.js +33 -12
  199. package/src/grammar/bars/policy.js +9 -7
  200. package/src/grammar/bin.js +181 -0
  201. package/src/grammar/categoryOrder.js +34 -0
  202. package/src/grammar/computed.js +150 -0
  203. package/src/grammar/ecdf.js +198 -0
  204. package/src/grammar/facets/index.js +117 -6
  205. package/src/grammar/fold.js +123 -0
  206. package/src/grammar/horizon.js +6 -4
  207. package/src/grammar/interval.js +37 -20
  208. package/src/grammar/lineSeries.js +20 -63
  209. package/src/grammar/markFilter.js +22 -4
  210. package/src/grammar/markLabels.js +93 -0
  211. package/src/grammar/parallelAxisTitles.js +16 -0
  212. package/src/grammar/pathSeries.js +91 -0
  213. package/src/grammar/pointPacking.js +183 -0
  214. package/src/grammar/positionDatum.js +12 -0
  215. package/src/grammar/rects.js +9 -1
  216. package/src/grammar/regression/derive.js +4 -0
  217. package/src/grammar/regression/index.js +7 -2
  218. package/src/grammar/regression/models.js +22 -7
  219. package/src/grammar/regression/parameters.js +40 -13
  220. package/src/grammar/rotation.js +29 -0
  221. package/src/grammar/rules.js +7 -21
  222. package/src/grammar/scales/color.js +23 -1
  223. package/src/grammar/scales/colorConsumers.js +38 -0
  224. package/src/grammar/scales/definition.js +15 -1
  225. package/src/grammar/scales/discretized.js +11 -5
  226. package/src/grammar/scales/fields.js +38 -8
  227. package/src/grammar/scales/mapping.js +2 -0
  228. package/src/grammar/scales/radial.js +55 -0
  229. package/src/grammar/scales/types.js +1 -0
  230. package/src/grammar/schemas/semanticPath.js +17 -3
  231. package/src/grammar/stack.js +170 -0
  232. package/src/grammar/statistics/confidenceInterval.js +101 -0
  233. package/src/grammar/summary.js +183 -0
  234. package/src/grammar/text.js +22 -16
  235. package/src/grammar/timeUnit.js +21 -11
  236. package/src/grammar/transformTopology.js +6 -0
  237. package/src/grammar/transforms.js +36 -0
  238. package/src/grammar/valueFormat.js +112 -0
  239. package/src/grammar/window.js +13 -7
  240. package/src/layout/facets.js +41 -7
  241. package/src/layout/guideCollisions.js +12 -0
  242. package/src/layout/legend.js +2 -10
  243. package/src/layout/legendItems.js +102 -0
  244. package/src/layout/legendLane.js +100 -11
  245. package/src/materialization/bars/aggregate.js +38 -152
  246. package/src/materialization/bars/histogram.js +24 -0
  247. package/src/materialization/categorySlotOffset.js +40 -0
  248. package/src/materialization/compositionSnapshot.js +1 -1
  249. package/src/materialization/dependencies.js +57 -16
  250. package/src/materialization/encodings.js +1 -1
  251. package/src/materialization/facets.js +8 -1
  252. package/src/materialization/guides/layout.js +57 -0
  253. package/src/materialization/guides/resources.js +2 -2
  254. package/src/materialization/legends.js +10 -1
  255. package/src/materialization/marks/capabilities.js +20 -5
  256. package/src/materialization/marks/grouping.js +52 -0
  257. package/src/materialization/marks/index.js +58 -23
  258. package/src/materialization/marks/policies.js +2 -0
  259. package/src/materialization/rect.js +33 -33
  260. package/src/materialization/rowEncoding.js +13 -13
  261. package/src/materialization/scaleGuideDependencies.js +8 -5
  262. package/src/materialization/scales/map.js +1 -0
  263. package/src/materialization/scales/policies/arc.js +42 -0
  264. package/src/materialization/scales/policies/binnedPosition.js +7 -4
  265. package/src/materialization/scales/policies/series.js +2 -1
  266. package/src/materialization/scales/resolve.js +46 -14
  267. package/src/materialization/selection/compatibility.js +11 -0
  268. package/src/materialization/selection/items/bar.js +12 -68
  269. package/src/materialization/selection/items/common.js +8 -2
  270. package/src/materialization/selection/items/path.js +3 -13
  271. package/src/materialization/selection/items/rect.js +7 -4
  272. package/src/materialization/text.js +64 -5
  273. package/src/mcp/adapter.js +2 -2
  274. package/src/selectors/datasets.js +13 -0
  275. package/src/selectors/layers.js +3 -2
  276. package/src/selectors/scales.js +6 -0
  277. package/src/theme/defaults.js +39 -0
  278. package/types/basic.d.ts +11 -2
  279. package/types/index.d.ts +126 -0
  280. package/types/program.d.ts +1320 -135
  281. package/src/materialization/bars/grouped.js +0 -144
@@ -46,20 +46,52 @@ export function resolveDensityTransitionScaleDefinitions(
46
46
  layer,
47
47
  transform,
48
48
  placement,
49
- rawPlacement
49
+ rawPlacement,
50
+ rawValueScale
50
51
  ) {
51
52
  const valueScale = valueScaleForTransition(program, layer, transform);
52
53
  if (valueScale === undefined) {
53
54
  throw new Error(`Density area "${layer.id}" requires its value scale.`);
54
55
  }
55
- const { id: _valueId, range: _valueRange, ...valueDefinition } = valueScale;
56
+ if (rawValueScale?.id !== undefined && rawValueScale.id !== valueScale.id) {
57
+ throw new Error("editDensity value scale cannot change its id.");
58
+ }
59
+ const { id: _valueId, range: _valueRange, ...storedValueDefinition } = valueScale;
56
60
  void _valueId;
57
61
  void _valueRange;
62
+ const valueDefinition = resolveDensityScaleOptions(
63
+ rawValueScale,
64
+ storedValueDefinition,
65
+ "Density valueScale"
66
+ );
67
+ delete valueDefinition.id;
68
+ delete valueDefinition.range;
58
69
  const valueChannel = placement?.channel === "x" ? "y" : "x";
59
70
  const companionChannel = valueChannel === "x" ? "y" : "x";
60
- const companionDefinition = placement === undefined
61
- ? { type: "linear", domain: "auto", range: "auto", nice: true, zero: true }
62
- : resolveDensityCategoryScaleOptions(rawPlacement.scale);
71
+ let companionDefinition;
72
+ if (placement === undefined) {
73
+ companionDefinition = {
74
+ type: "linear", domain: "auto", range: "auto", nice: true, zero: true
75
+ };
76
+ } else {
77
+ const currentCategoryChannel = transform.placement?.channel;
78
+ const currentCategoryScale = currentCategoryChannel === undefined
79
+ ? undefined
80
+ : findSemanticScale(program, layer.encoding[currentCategoryChannel].scale);
81
+ const { id: _categoryId, range: _categoryRange, ...storedCategoryDefinition } =
82
+ currentCategoryScale ?? {};
83
+ void _categoryId;
84
+ void _categoryRange;
85
+ companionDefinition = resolveDensityCategoryScaleOptions({
86
+ domain: "auto",
87
+ range: "auto",
88
+ paddingInner: 0,
89
+ paddingOuter: 0,
90
+ align: 0.5,
91
+ ...storedCategoryDefinition,
92
+ ...(rawPlacement?.scale ?? {})
93
+ });
94
+ }
63
95
  return {
64
96
  [valueChannel]: { ...valueDefinition, range: "auto" },
65
97
  [companionChannel]: companionDefinition
@@ -16,6 +16,8 @@ import { validateOptions } from "./shared.js";
16
16
  import { normalizeDensityPlacement } from "../../grammar/density.js";
17
17
  import { scaleEditPatch } from "../scales/patch.js";
18
18
  import { removeOwnedColorLegends } from "../guides/legends/remove.js";
19
+ import { rebindDistributionGuides } from "../distributions/revision.js";
20
+ import { findSemanticScale } from "../../selectors/scales.js";
19
21
  import {
20
22
  requireDensityField,
21
23
  resolveDensityCategoryScaleOptions,
@@ -31,9 +33,50 @@ const OPTIONS = Object.freeze([
31
33
  ]);
32
34
  const EDIT_OPTIONS = Object.freeze([
33
35
  "target", "source", "field", "groupBy", "bandwidth", "extent", "steps",
34
- "kernel", "normalization", "placement"
36
+ "kernel", "normalization", "densityChannel", "valueScale", "placement"
35
37
  ]);
36
38
  const EDITABLE = Object.freeze(EDIT_OPTIONS.filter(option => option !== "target"));
39
+ const SCALE_PROPERTIES = Object.freeze([
40
+ "type", "domain", "range", "nice", "zero", "clamp", "reverse",
41
+ "base", "exponent", "constant", "paddingInner", "paddingOuter",
42
+ "padding", "align", "interpolate", "midpoint", "unknown"
43
+ ]);
44
+
45
+ function replaceScaleDefinition(program, id, definition) {
46
+ const current = findSemanticScale(program, id);
47
+ let next = program;
48
+ for (const property of SCALE_PROPERTIES) {
49
+ if (Object.hasOwn(current, property) && !Object.hasOwn(definition, property)) {
50
+ next = next.editSemantic({ property: `scale[${id}].${property}`, remove: true });
51
+ }
52
+ }
53
+ for (const property of SCALE_PROPERTIES) {
54
+ if (!Object.hasOwn(definition, property)) continue;
55
+ next = next.editSemantic({
56
+ property: `scale[${id}].${property}`,
57
+ value: definition[property]
58
+ });
59
+ }
60
+ return next;
61
+ }
62
+
63
+ function replaceDensityPositions(program, layerId, definition) {
64
+ let next = program;
65
+ for (const channel of ["x", "y"]) {
66
+ const value = definition[channel];
67
+ for (const [property, propertyValue] of Object.entries({
68
+ field: value.field,
69
+ fieldType: value.fieldType,
70
+ scale: typeof value.scale === "string" ? value.scale : value.scale.id
71
+ })) {
72
+ next = next.editSemantic({
73
+ property: `layer[${layerId}].encoding.${channel}.${property}`,
74
+ value: propertyValue
75
+ });
76
+ }
77
+ }
78
+ return next;
79
+ }
37
80
 
38
81
  function findArea(program, requested) {
39
82
  const areas = program.semanticSpec.layers.filter(layer => layer.mark?.type === "area");
@@ -80,7 +123,7 @@ const encodeDensity = action(
80
123
  validateOptions(args, OPTIONS, "encodeDensity");
81
124
  const layer = findArea(this, args.target);
82
125
  const field = requireDensityField(args.field, "Density field");
83
- const groupBy = args.groupBy === undefined
126
+ const groupBy = args.groupBy === undefined || args.groupBy === false
84
127
  ? undefined
85
128
  : requireDensityField(args.groupBy, "Density groupBy");
86
129
  const source = validateUserId(
@@ -241,17 +284,34 @@ const editDensity = action(
241
284
  : transform.groupBy;
242
285
  const changesGroup = groupBy !== transform.groupBy;
243
286
  assertGroupingSelectionCompatibility(this, layer, changesGroup);
287
+ const currentDensityChannel = transform.placement?.channel ?? (
288
+ layer.encoding.x.field === transform.as[1] ? "x" : "y"
289
+ );
290
+ const changesPlacementMode = Object.hasOwn(args, "placement") &&
291
+ (transform.placement === undefined) !==
292
+ (args.placement?.type === "baseline");
293
+ const densityChannel = Object.hasOwn(args, "densityChannel")
294
+ ? args.densityChannel
295
+ : changesPlacementMode
296
+ ? currentDensityChannel === "x" ? "y" : "x"
297
+ : currentDensityChannel;
298
+ if (!["x", "y"].includes(densityChannel)) {
299
+ throw new Error(`Unsupported densityChannel "${densityChannel}".`);
300
+ }
301
+ const changesDensityChannel = densityChannel !== currentDensityChannel;
244
302
  const categoryField = densityCategoryField(layer, transform, groupBy);
245
303
  const requestedPlacement = Object.hasOwn(args, "placement")
246
304
  ? normalizeDensityPlacement(args.placement, {
247
- densityChannel: transform.placement?.channel ?? "x",
305
+ densityChannel,
248
306
  groupBy,
249
307
  categoryField: categoryField ?? groupBy ??
250
308
  `${layer.id}DensityCategory`
251
309
  })
252
- : transform.placement === undefined || !changesGroup
310
+ : transform.placement === undefined
253
311
  ? transform.placement
254
- : normalizeDensityPlacement({
312
+ : !changesGroup && !changesDensityChannel
313
+ ? transform.placement
314
+ : normalizeDensityPlacement({
255
315
  type: "category",
256
316
  ...(transform.placement.side === undefined
257
317
  ? {}
@@ -261,7 +321,7 @@ const editDensity = action(
261
321
  ? {}
262
322
  : { split: transform.placement.split })
263
323
  }, {
264
- densityChannel: transform.placement.channel,
324
+ densityChannel,
265
325
  groupBy,
266
326
  categoryField
267
327
  });
@@ -308,8 +368,7 @@ const editDensity = action(
308
368
  ? {}
309
369
  : { placement: requestedPlacement })
310
370
  };
311
- const changesPlacementMode = Object.hasOwn(args, "placement") &&
312
- (transform.placement === undefined) !== (requestedPlacement === undefined);
371
+ const rebuildsPositions = changesPlacementMode || changesDensityChannel;
313
372
  const applyEdit = program => {
314
373
  let next = requestedPlacement === undefined
315
374
  ? program.createDensityData(dataArgs)
@@ -360,7 +419,7 @@ const editDensity = action(
360
419
  }
361
420
  }
362
421
 
363
- if (changesPlacementMode) {
422
+ if (rebuildsPositions) {
364
423
  const xScaleId = layer.encoding?.x?.scale;
365
424
  const yScaleId = layer.encoding?.y?.scale;
366
425
  const scaleDefinitions = resolveDensityTransitionScaleDefinitions(
@@ -368,24 +427,18 @@ const editDensity = action(
368
427
  layer,
369
428
  transform,
370
429
  requestedPlacement,
371
- args.placement
372
- );
373
- next = next
374
- .editSemantic({
375
- property: `layer[${layer.id}].encoding.x`,
376
- remove: true
377
- })
378
- .editSemantic({
379
- property: `layer[${layer.id}].encoding.y`,
380
- remove: true
381
- });
382
- next = next
383
- .editScale(scaleEditPatch(xScaleId, scaleDefinitions.x))
384
- .editScale(scaleEditPatch(yScaleId, scaleDefinitions.y));
385
- const densityChannel = requestedPlacement?.channel ?? (
386
- layer.encoding.x.field === transform.as[1] ? "x" : "y"
430
+ args.placement,
431
+ args.valueScale
387
432
  );
388
- const valueChannel = requestedPlacement?.channel === "x" ? "y" : "x";
433
+ if (
434
+ args.placement?.scale?.id !== undefined &&
435
+ args.placement.scale.id !== layer.encoding[requestedPlacement.channel].scale
436
+ ) {
437
+ throw new Error("editDensity placement scale cannot change its id.");
438
+ }
439
+ next = replaceScaleDefinition(next, xScaleId, scaleDefinitions.x);
440
+ next = replaceScaleDefinition(next, yScaleId, scaleDefinitions.y);
441
+ const valueChannel = densityChannel === "x" ? "y" : "x";
389
442
  const definition = resolveDensityPositionDefinition({
390
443
  layer: { densityChannel },
391
444
  output: transform.as,
@@ -393,23 +446,46 @@ const editDensity = action(
393
446
  placement: requestedPlacement,
394
447
  coordinate: layer.coordinate,
395
448
  valueScale: {
396
- id: valueChannel === "y" ? yScaleId : xScaleId,
397
- ...scaleDefinitions[valueChannel]
449
+ ...scaleDefinitions[valueChannel],
450
+ id: valueChannel === "y" ? yScaleId : xScaleId
398
451
  },
399
452
  densityScale: requestedPlacement === undefined
400
453
  ? {
401
- id: densityChannel === "x" ? xScaleId : yScaleId,
402
- ...scaleDefinitions[densityChannel]
454
+ ...scaleDefinitions[densityChannel],
455
+ id: densityChannel === "x" ? xScaleId : yScaleId
403
456
  }
404
457
  : undefined,
405
458
  placementScale: requestedPlacement === undefined
406
459
  ? undefined
407
460
  : {
408
- id: requestedPlacement.channel === "x" ? xScaleId : yScaleId,
409
- ...scaleDefinitions[requestedPlacement.channel]
461
+ ...scaleDefinitions[requestedPlacement.channel],
462
+ id: requestedPlacement.channel === "x" ? xScaleId : yScaleId
410
463
  }
411
464
  });
412
- next = applyDensityPosition(next, layer.id, definition, groupBy);
465
+ next = replaceDensityPositions(next, layer.id, definition);
466
+ next = next.rematerializeScale({ id: xScaleId, marks: false, guides: false })
467
+ .rematerializeScale({ id: yScaleId, marks: false, guides: false });
468
+ const oldValueChannel = currentDensityChannel === "x" ? "y" : "x";
469
+ next = rebindDistributionGuides(next, {
470
+ oldXScale: xScaleId,
471
+ oldYScale: yScaleId,
472
+ newXScale: xScaleId,
473
+ newYScale: yScaleId,
474
+ oldXTitle: oldValueChannel === "x"
475
+ ? transform.field
476
+ : transform.placement?.categoryField ?? transform.as[1],
477
+ oldYTitle: oldValueChannel === "y"
478
+ ? transform.field
479
+ : transform.placement?.categoryField ?? transform.as[1],
480
+ newXTitle: valueChannel === "x"
481
+ ? field
482
+ : requestedPlacement?.categoryField ?? transform.as[1],
483
+ newYTitle: valueChannel === "y"
484
+ ? field
485
+ : requestedPlacement?.categoryField ?? transform.as[1],
486
+ oldMeasureChannel: oldValueChannel,
487
+ newMeasureChannel: valueChannel
488
+ });
413
489
  } else if (
414
490
  requestedPlacement !== undefined &&
415
491
  Object.hasOwn(args.placement ?? {}, "scale")
@@ -425,6 +501,17 @@ const editDensity = action(
425
501
  }
426
502
  next = next.editScale(scaleEditPatch(scaleId, requestedScale));
427
503
  }
504
+ if (!rebuildsPositions && Object.hasOwn(args, "valueScale")) {
505
+ const valueChannel = densityChannel === "x" ? "y" : "x";
506
+ const scaleId = layer.encoding[valueChannel].scale;
507
+ if (args.valueScale?.id !== undefined && args.valueScale.id !== scaleId) {
508
+ throw new Error("editDensity value scale cannot change its id.");
509
+ }
510
+ next = next.editScale(scaleEditPatch(scaleId, {
511
+ ...args.valueScale,
512
+ id: scaleId
513
+ }));
514
+ }
428
515
 
429
516
  next = applyMaterializationPlan(
430
517
  next,
@@ -4,6 +4,7 @@ import { validateOptionObject } from "../../../core/validation.js";
4
4
  import { resolvePalette } from "../../../grammar/palettes.js";
5
5
  import {
6
6
  normalizeTemporalValue,
7
+ resolveTemporalUnit,
7
8
  readNominalField
8
9
  } from "../../../grammar/scales/index.js";
9
10
  import {
@@ -42,7 +43,7 @@ function fieldOption(value, role) {
42
43
  }
43
44
  validateOptionObject(
44
45
  value,
45
- ["field", "fieldType", "scale"],
46
+ ["field", "fieldType", "scale", "temporalUnit"],
46
47
  `Horizon ${role}`
47
48
  );
48
49
  return value;
@@ -80,6 +81,7 @@ function uniqueStoredEncoding(program, dataset, role) {
80
81
  JSON.stringify([
81
82
  encoding.field,
82
83
  encoding.fieldType,
84
+ encoding.temporalUnit,
83
85
  encoding.scale,
84
86
  encoding.title
85
87
  ]),
@@ -103,6 +105,7 @@ export function resolveHorizonField(program, layer, dataset, requested, role) {
103
105
  return {
104
106
  field: inferred.field,
105
107
  fieldType: inferred.fieldType,
108
+ ...(inferred.temporalUnit === undefined ? {} : { temporalUnit: inferred.temporalUnit }),
106
109
  ...(role === "x" && inferred.scale !== undefined
107
110
  ? { scale: { id: inferred.scale } }
108
111
  : {}),
@@ -127,7 +130,9 @@ export function resolveHorizonField(program, layer, dataset, requested, role) {
127
130
  `Horizon ${role} fieldType must be ${role === "x" ? "quantitative or temporal" : "quantitative"}.`
128
131
  );
129
132
  }
133
+ const temporalUnit = resolveTemporalUnit(option, fieldType, matching);
130
134
  return {
135
+ ...(temporalUnit === undefined ? {} : { temporalUnit }),
131
136
  field: option.field,
132
137
  fieldType,
133
138
  ...(option.scale !== undefined
@@ -140,9 +145,7 @@ export function resolveHorizonField(program, layer, dataset, requested, role) {
140
145
  }
141
146
 
142
147
  export function resolveHorizonGroupBy(layer, dataset, requested) {
143
- if (requested === false) {
144
- throw new Error("encodeHorizon groupBy must be a field string when provided.");
145
- }
148
+ if (requested === false) return undefined;
146
149
  const value = requested ?? layer.encoding?.group?.field;
147
150
  if (value === undefined) return undefined;
148
151
  if (typeof value !== "string" || value.length === 0) {
@@ -262,6 +265,7 @@ export function resolveEditedHorizonField(
262
265
  [role]: {
263
266
  field: prior.field,
264
267
  fieldType: prior.fieldType,
268
+ ...(prior.temporalUnit === undefined ? {} : { temporalUnit: prior.temporalUnit }),
265
269
  scale: layer.encoding[role].scale,
266
270
  ...(role === "x" && layer.encoding.x.title !== undefined
267
271
  ? { title: layer.encoding.x.title }
@@ -42,6 +42,8 @@ import {
42
42
  validateUnusedHorizonEncodings
43
43
  } from "./horizon/resolve.js";
44
44
 
45
+ import { applyTemporalUnit } from "./temporal.js";
46
+
45
47
  const OPTIONS = Object.freeze([
46
48
  "target", "source", "x", "y", "groupBy", "bands", "baseline", "extent",
47
49
  "resolve", "missing", "overflow", "palette"
@@ -69,16 +71,17 @@ function applyHorizonEncoding(program, {
69
71
  colorScale
70
72
  }) {
71
73
  let next = clearOwnedEncodings(program, layer)
74
+ .editSemantic({
75
+ property: `layer[${layer.id}].encoding.x.title`,
76
+ value: layer.encoding?.x?.title ?? inferHorizonTitle(transform.x.field)
77
+ })
72
78
  .encodeX({
73
79
  target: layer.id,
74
80
  field: transform.as.x,
75
81
  fieldType: transform.x.fieldType,
82
+ ...(transform.x.fieldType === "temporal" ? { temporalUnit: "timestamp" } : {}),
76
83
  scale: xScale
77
84
  })
78
- .editSemantic({
79
- property: `layer[${layer.id}].encoding.x.title`,
80
- value: layer.encoding?.x?.title ?? inferHorizonTitle(transform.x.field)
81
- })
82
85
  .encodeY({
83
86
  target: layer.id,
84
87
  field: transform.as.lower,
@@ -113,6 +116,9 @@ const editHorizon = action(
113
116
  },
114
117
  function (args = {}) {
115
118
  validateOptionObject(args, EDIT_OPTIONS, "editHorizon");
119
+ if (Object.hasOwn(args, "groupBy") && args.groupBy === undefined) {
120
+ throw new Error("editHorizon groupBy requires a field string or false.");
121
+ }
116
122
  if (!EDITABLE.some(option => Object.hasOwn(args, option))) {
117
123
  throw new Error("editHorizon requires at least one Horizon option.");
118
124
  }
@@ -145,7 +151,8 @@ const editHorizon = action(
145
151
  );
146
152
  const candidate = validateHorizonTransform({
147
153
  type: "horizon",
148
- x: { field: x.field, fieldType: x.fieldType },
154
+ x: { field: x.field, fieldType: x.fieldType,
155
+ ...(x.temporalUnit === undefined ? {} : { temporalUnit: x.temporalUnit }) },
149
156
  y: { field: y.field, fieldType: y.fieldType },
150
157
  ...(groupBy === undefined ? {} : { groupBy }),
151
158
  bands: args.bands ?? prior.bands,
@@ -189,6 +196,8 @@ const editHorizon = action(
189
196
  value: x.fieldType
190
197
  });
191
198
  }
199
+ next = applyTemporalUnit(next, layer.id, "x",
200
+ x.fieldType === "temporal" ? "timestamp" : undefined, layer.encoding.x);
192
201
  if (Object.hasOwn(args, "x")) {
193
202
  next = next.editSemantic({
194
203
  property: `layer[${layer.id}].encoding.x.title`,
@@ -210,7 +219,7 @@ const editHorizon = action(
210
219
  requestedXScale.domain === undefined &&
211
220
  currentX.domain === "auto"
212
221
  ? source.values.map((row, index) => x.fieldType === "temporal"
213
- ? normalizeTemporalValue(row[x.field], x.field, index)
222
+ ? normalizeTemporalValue(row[x.field], x.field, index, x.temporalUnit)
214
223
  : row[x.field])
215
224
  : undefined;
216
225
  const nextX = resolvePositionScaleDefinition(next, "x", x.fieldType, {
@@ -285,7 +294,8 @@ const encodeHorizon = action(
285
294
  const as = horizonOutputFields(layer.id);
286
295
  const transform = validateHorizonTransform({
287
296
  type: "horizon",
288
- x: { field: x.field, fieldType: x.fieldType },
297
+ x: { field: x.field, fieldType: x.fieldType,
298
+ ...(x.temporalUnit === undefined ? {} : { temporalUnit: x.temporalUnit }) },
289
299
  y: { field: y.field, fieldType: y.fieldType },
290
300
  ...(groupBy === undefined ? {} : { groupBy }),
291
301
  bands: args.bands ?? 3,
@@ -308,7 +318,7 @@ const encodeHorizon = action(
308
318
  ? {
309
319
  domain: source.values.map((row, index) =>
310
320
  x.fieldType === "temporal"
311
- ? normalizeTemporalValue(row[x.field], x.field, index)
321
+ ? normalizeTemporalValue(row[x.field], x.field, index, x.temporalUnit)
312
322
  : row[x.field]
313
323
  )
314
324
  }
@@ -1,3 +1,4 @@
1
+ import { registerSeriesLayoutAction } from "./seriesLayout.js";
1
2
  import { registerDensityEncodingAction } from "./density.js";
2
3
  import { registerPathOrderEncodingActions } from "./pathOrder.js";
3
4
  import { registerHorizonEncodingAction } from "./horizon.js";
@@ -16,6 +17,7 @@ import { registerBarWidthEncodingAction } from "./barWidth.js";
16
17
  import { registerEncodingRemovalAction } from "./remove.js";
17
18
 
18
19
  export function registerEncodingActions(ProgramClass) {
20
+ registerSeriesLayoutAction(ProgramClass);
19
21
  registerPositionEncodingActions(ProgramClass);
20
22
  registerRangedEncodingActions(ProgramClass);
21
23
  registerOffsetEncodingAction(ProgramClass);
@@ -1,3 +1,5 @@
1
+ import { inferSeriesGroup } from "./shared.js";
2
+ import { findSemanticScale } from "../../selectors/scales.js";
1
3
  import { action } from "../../core/action.js";
2
4
  import {
3
5
  readNominalField,
@@ -13,7 +15,6 @@ import {
13
15
  validateOptions
14
16
  } from "./shared.js";
15
17
  import {
16
- resolveBarColorLayout,
17
18
  resolveBarChannels,
18
19
  resolveBarGrain
19
20
  } from "../../grammar/bars/policy.js";
@@ -50,15 +51,7 @@ function createOffsetEncoding(channel) {
50
51
  `${operation} requires a complete bar with a ${parentChannel} category encoding.`
51
52
  );
52
53
  }
53
- if (
54
- layer.encoding?.color !== undefined &&
55
- (resolveBarColorLayout(layer) !== "group" ||
56
- layer.encoding.color.field !== args.field)
57
- ) {
58
- throw new Error(
59
- `${operation} field must match a grouped bar color field.`
60
- );
61
- }
54
+
62
55
  } else {
63
56
  const parent = layer.encoding?.[parentChannel];
64
57
  if (
@@ -88,7 +81,8 @@ function createOffsetEncoding(channel) {
88
81
  channel
89
82
  );
90
83
 
91
- let next = setEncodingProperties(this, target, channel, {
84
+ const grouped = layer.mark.type === "bar" ? inferSeriesGroup(this, layer, args.field, "offset") : this;
85
+ let next = setEncodingProperties(grouped, target, channel, {
92
86
  field: args.field,
93
87
  fieldType,
94
88
  scale: scale.id
@@ -99,12 +93,12 @@ function createOffsetEncoding(channel) {
99
93
  next = applyEncodingScale(next, scale, requestedScale, {
100
94
  reassignment: layer.encoding?.[channel]?.scale === scale.id
101
95
  });
102
- if (layer.mark.type === "bar" && layer.encoding?.color === undefined) {
103
- return next
104
- .rematerializeScale({ id: scale.id })
105
- .editGraphics({ target, property: "length", value: 0 });
96
+ if (layer.mark.type === "bar") {
97
+ if (findSemanticScale(this, scale.id) === undefined && args.scale?.id === undefined) next = next._withMarkConfig(target, { ...next.markConfigs[target], seriesOffsetScale: scale.id });
98
+ if (layer.layout?.mode !== "group" || layer.encoding?.group?.field !== args.field) return next.layoutSeries({ target, mode: "group" });
99
+ return rematerializeEncoding(next, target, channel, scale.id, layer);
106
100
  }
107
- return rematerializeEncoding(next, target, channel, scale.id);
101
+ return rematerializeEncoding(next, target, channel, scale.id, layer);
108
102
  }
109
103
  );
110
104
  }
@@ -13,6 +13,7 @@ import {
13
13
  setEncodingProperties
14
14
  } from "./shared.js";
15
15
  import { resolveParallelCoordinate } from "../coordinates/parallel.js";
16
+ import { applyLayerDataRematerialization } from "../../materialization/dependencies.js";
16
17
 
17
18
  const OPTIONS = Object.freeze([
18
19
  "target", "coordinate", "dimensions", "key", "missing"
@@ -98,14 +99,7 @@ export const encodeParallelCoordinates = action(
98
99
  for (let index = 0; index < definitions.length; index += 1) {
99
100
  next = applyEncodingScale(next, definitions[index], dimensions[index].scaleOptions);
100
101
  }
101
- for (const definition of definitions) {
102
- next = next.rematerializeScale({
103
- id: definition.id,
104
- guides: false,
105
- marks: false
106
- });
107
- }
108
- return next.rematerializeLineMark({ id: target, scales: false });
102
+ return applyLayerDataRematerialization(next, target);
109
103
  }
110
104
  );
111
105
 
@@ -1,3 +1,5 @@
1
+ import { applyTemporalUnit } from "../temporal.js";
2
+
1
3
  function replaceAlternateBinding(program, target, channel, previous, usesField) {
2
4
  if (previous === undefined) return program;
3
5
  const alternate = usesField ? "datum" : "field";
@@ -111,6 +113,7 @@ export function applyPositionSemantics(program, {
111
113
  datum,
112
114
  hasField,
113
115
  fieldType,
116
+ temporalUnit,
114
117
  bin,
115
118
  aggregate,
116
119
  stack,
@@ -128,16 +131,19 @@ export function applyPositionSemantics(program, {
128
131
  channel,
129
132
  previous,
130
133
  hasField
131
- )
132
- .editSemantic({
134
+ );
135
+ if (hasField || datum !== undefined) {
136
+ next = next.editSemantic({
133
137
  property: `layer[${target}].encoding.${channel}.${hasField ? "field" : "datum"}`,
134
138
  value: hasField ? field : datum
135
- })
136
- .editSemantic({
139
+ });
140
+ }
141
+ next = next.editSemantic({
137
142
  property: `layer[${target}].encoding.${channel}.fieldType`,
138
143
  value: fieldType
139
144
  });
140
145
 
146
+ next = applyTemporalUnit(next, target, channel, temporalUnit, previous);
141
147
  next = applyBin(next, target, channel, previous, bin);
142
148
  for (const [property, value] of Object.entries({ aggregate, stack })) {
143
149
  if (value === undefined) continue;