ggaction 0.0.21 → 0.0.22

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 (253) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +3 -2
  3. package/knowledge/action-cards.json +1 -1
  4. package/knowledge/intent-taxonomy.json +1 -1
  5. package/knowledge/mcp-resources.json +1 -1
  6. package/package.json +6 -2
  7. package/src/BasicChartProgram.js +1 -1
  8. package/src/accessibility.js +1 -0
  9. package/src/actions/boxPlots/components.js +1 -96
  10. package/src/actions/boxPlots/create.js +6 -1
  11. package/src/actions/boxPlots/edit.js +8 -0
  12. package/src/actions/boxPlots/materialize.js +3 -0
  13. package/src/actions/boxPlots/options.js +1 -1
  14. package/src/actions/canvas/actions.js +2 -1
  15. package/src/actions/canvas/index.js +3 -0
  16. package/src/actions/canvas/plotBorder.js +2 -1
  17. package/src/actions/canvas/plotOrder.js +14 -0
  18. package/src/actions/charts/area.js +1 -1
  19. package/src/actions/charts/bar.js +4 -4
  20. package/src/actions/charts/density.js +1 -1
  21. package/src/actions/charts/ecdf.js +1 -1
  22. package/src/actions/charts/endpoints.js +54 -5
  23. package/src/actions/charts/heatmap.js +1 -1
  24. package/src/actions/charts/histogram.js +1 -1
  25. package/src/actions/charts/horizon.js +1 -1
  26. package/src/actions/charts/index.js +2 -1
  27. package/src/actions/charts/interval-regression.js +1 -1
  28. package/src/actions/charts/line.js +1 -1
  29. package/src/actions/charts/radar.js +63 -3
  30. package/src/actions/charts/raincloud.js +37 -6
  31. package/src/actions/charts/rug-strip.js +2 -2
  32. package/src/actions/charts/scatter.js +3 -3
  33. package/src/actions/charts/text.js +1 -1
  34. package/src/actions/composition/actions.js +10 -3
  35. package/src/actions/coordinates/actions.js +2 -0
  36. package/src/actions/data/basic.js +6 -0
  37. package/src/actions/data/derived.js +6 -2
  38. package/src/actions/data/edit.js +21 -35
  39. package/src/actions/data/filter.js +1 -376
  40. package/src/actions/data/flatten.js +19 -0
  41. package/src/actions/data/index.js +6 -0
  42. package/src/actions/data/lookup.js +50 -0
  43. package/src/actions/data/raincloudFilter.js +56 -0
  44. package/src/actions/data/shared.js +10 -0
  45. package/src/actions/distributions/revision.js +3 -1
  46. package/src/actions/encodings/angle.js +2 -1
  47. package/src/actions/encodings/appearance.js +38 -13
  48. package/src/actions/encodings/channels.js +14 -3
  49. package/src/actions/encodings/color/continuous.js +26 -10
  50. package/src/actions/encodings/color/index.js +50 -20
  51. package/src/actions/encodings/color/policy.js +57 -4
  52. package/src/actions/encodings/parallel.js +4 -0
  53. package/src/actions/encodings/position/policies/bar.js +10 -6
  54. package/src/actions/encodings/position/policies/line.js +1 -152
  55. package/src/actions/encodings/position/resolve.js +4 -1
  56. package/src/actions/encodings/ranged.js +46 -0
  57. package/src/actions/encodings/remove.js +1 -1
  58. package/src/actions/encodings/ruleAppearance.js +7 -6
  59. package/src/actions/encodings/shared.js +14 -2
  60. package/src/actions/encodings/stroke.js +20 -14
  61. package/src/actions/encodings/strokeDash.js +9 -11
  62. package/src/actions/encodings/text.js +30 -2
  63. package/src/actions/errorBars/edit.js +24 -4
  64. package/src/actions/facets/actions.js +1 -793
  65. package/src/actions/facets/derive.js +34 -5
  66. package/src/actions/facets/replay.js +1 -38
  67. package/src/actions/gradientPlots/components.js +21 -9
  68. package/src/actions/gradientPlots/create.js +4 -2
  69. package/src/actions/gradientPlots/edit.js +18 -1
  70. package/src/actions/gradientPlots/materialize.js +35 -36
  71. package/src/actions/gradientPlots/options.js +1 -1
  72. package/src/actions/gradientPlots/paint.js +13 -7
  73. package/src/actions/guides/applicability.js +2 -0
  74. package/src/actions/guides/axes/axes.js +39 -0
  75. package/src/actions/guides/axes/axis.js +9 -5
  76. package/src/actions/guides/axes/edit.js +11 -8
  77. package/src/actions/guides/axes/labels.js +24 -8
  78. package/src/actions/guides/axes/parallel/policy.js +3 -5
  79. package/src/actions/guides/axes/parallel/resolve.js +4 -3
  80. package/src/actions/guides/axes/policy.js +22 -0
  81. package/src/actions/guides/axes/tickGroups.js +4 -2
  82. package/src/actions/guides/axes/ticks.js +22 -4
  83. package/src/actions/guides/axes/titles.js +19 -6
  84. package/src/actions/guides/facadeAxes.js +3 -0
  85. package/src/actions/guides/grids/grid.js +3 -1
  86. package/src/actions/guides/grids/resolve.js +7 -11
  87. package/src/actions/guides/guides.js +21 -1
  88. package/src/actions/guides/legends/categorical/actions.js +1 -515
  89. package/src/actions/guides/legends/categorical/layout.js +5 -2
  90. package/src/actions/guides/legends/categorical/options.js +4 -0
  91. package/src/actions/guides/legends/categorical/resolve.js +8 -3
  92. package/src/actions/guides/legends/categorical/symbols.js +3 -3
  93. package/src/actions/guides/legends/colorSize.js +2 -0
  94. package/src/actions/guides/legends/continuous/common.js +1 -1
  95. package/src/actions/guides/legends/continuous/gradient.js +1 -360
  96. package/src/actions/guides/legends/continuous/interval.js +1 -1
  97. package/src/actions/guides/legends/continuous/opacity.js +15 -1
  98. package/src/actions/guides/legends/edit.js +1 -1
  99. package/src/actions/guides/legends/focused.js +1 -1
  100. package/src/actions/guides/legends/lane.js +1 -1
  101. package/src/actions/guides/legends/sampling.js +1 -140
  102. package/src/actions/guides/legends/strokeWidth.js +3 -2
  103. package/src/actions/guides/legends/transition.js +19 -2
  104. package/src/actions/guides/polar/axes/facade.js +5 -4
  105. package/src/actions/guides/polar/axes/labels.js +5 -4
  106. package/src/actions/guides/polar/axes/titles.js +5 -4
  107. package/src/actions/guides/polar/resolve.js +2 -1
  108. package/src/actions/guides/tickValues.js +7 -7
  109. package/src/actions/marks/arc/actions.js +5 -7
  110. package/src/actions/marks/area/actions.js +9 -3
  111. package/src/actions/marks/bar/create.js +4 -2
  112. package/src/actions/marks/bar/edit.js +9 -6
  113. package/src/actions/marks/bar/materialize.js +5 -3
  114. package/src/actions/marks/dependentReferences.js +87 -0
  115. package/src/actions/marks/index.js +2 -0
  116. package/src/actions/marks/lifecycle.js +2 -1
  117. package/src/actions/marks/line/actions.js +24 -6
  118. package/src/actions/marks/line/materialize.js +7 -3
  119. package/src/actions/marks/point/create.js +5 -4
  120. package/src/actions/marks/point/edit.js +6 -3
  121. package/src/actions/marks/point/jitter.js +6 -6
  122. package/src/actions/marks/point/materialize.js +36 -39
  123. package/src/actions/marks/rect/actions.js +16 -4
  124. package/src/actions/marks/references.js +183 -35
  125. package/src/actions/marks/rule/actions.js +20 -9
  126. package/src/actions/marks/shared.js +2 -1
  127. package/src/actions/marks/text/actions.js +10 -3
  128. package/src/actions/marks/tick/actions.js +11 -2
  129. package/src/actions/primitives/editGraphics.js +28 -3
  130. package/src/actions/primitives/index.js +9 -0
  131. package/src/actions/primitives/semanticAction.js +13 -7
  132. package/src/actions/primitives/semanticValidation/layer.js +20 -0
  133. package/src/actions/primitives/semanticValidation/scale.js +8 -1
  134. package/src/actions/scales/channels.js +1 -1
  135. package/src/actions/scales/consumers/common.js +19 -1
  136. package/src/actions/scales/consumers/families.js +2 -2
  137. package/src/actions/scales/consumers/seriesLayout.js +4 -3
  138. package/src/actions/scales/edit.js +11 -0
  139. package/src/actions/scales/editPolicy.js +5 -3
  140. package/src/actions/scales/preview.js +2 -1
  141. package/src/actions/scales/quantitativeColor.js +2 -4
  142. package/src/actions/selection/actions.js +10 -1
  143. package/src/actions/theme/reconcile.js +37 -9
  144. package/src/core/compositionState.js +21 -4
  145. package/src/core/datasetReferences.js +17 -0
  146. package/src/core/immutable.js +1 -0
  147. package/src/core/resourceReferences.js +5 -3
  148. package/src/core/validation.js +4 -0
  149. package/src/core/vocabulary.js +24 -2
  150. package/src/grammar/aggregate.js +1 -0
  151. package/src/grammar/arcs.js +1 -1
  152. package/src/grammar/areaSeries.js +1 -1
  153. package/src/grammar/axisTextPlacement.js +1 -1
  154. package/src/grammar/bars/policy.js +2 -2
  155. package/src/grammar/datasetDependencies.js +97 -0
  156. package/src/grammar/datasetSchema.js +31 -1
  157. package/src/grammar/facetHeaders.js +1 -0
  158. package/src/grammar/facets/dependencies.js +6 -2
  159. package/src/grammar/facets/legendIntent.js +6 -2
  160. package/src/grammar/facets/scales.js +27 -7
  161. package/src/grammar/flatten.js +85 -0
  162. package/src/grammar/histogram.js +13 -1
  163. package/src/grammar/horizon.js +1 -494
  164. package/src/grammar/jitter.js +2 -2
  165. package/src/grammar/legendSpacing.js +45 -0
  166. package/src/grammar/lineMissing.js +90 -0
  167. package/src/grammar/lineSeries.js +1 -376
  168. package/src/grammar/lookup.js +97 -0
  169. package/src/grammar/markFilter.js +4 -2
  170. package/src/grammar/markLabels.js +8 -3
  171. package/src/grammar/numeric.js +1 -1
  172. package/src/grammar/pathCommands.js +1 -1
  173. package/src/grammar/pathSeries.js +1 -109
  174. package/src/grammar/pointPolygon.js +81 -0
  175. package/src/grammar/pointShapes.js +15 -15
  176. package/src/grammar/rects.js +1 -1
  177. package/src/grammar/regression/derive.js +1 -195
  178. package/src/grammar/scales/appearance.js +1 -1
  179. package/src/grammar/scales/color.js +1 -1
  180. package/src/grammar/scales/colorConsumers.js +1 -63
  181. package/src/grammar/scales/continuous.js +58 -1
  182. package/src/grammar/scales/definition.js +1 -1
  183. package/src/grammar/scales/ordinal.js +5 -2
  184. package/src/grammar/scales/policies.js +3 -3
  185. package/src/grammar/schemas/graphicBounds.js +40 -12
  186. package/src/grammar/schemas/graphicClip.js +23 -0
  187. package/src/grammar/schemas/graphicTree.js +16 -0
  188. package/src/grammar/statisticalReference.js +22 -0
  189. package/src/grammar/text.js +65 -1
  190. package/src/grammar/ticks.js +92 -1
  191. package/src/grammar/transformTopology.js +3 -1
  192. package/src/grammar/transforms.js +24 -0
  193. package/src/inspection.js +1 -495
  194. package/src/inspectionDescriptors.js +1 -1
  195. package/src/layout/canvas.js +1 -1
  196. package/src/layout/facetContentFlow.js +101 -0
  197. package/src/layout/facetHeaderTitles.js +1 -0
  198. package/src/layout/facetTopLane.js +29 -0
  199. package/src/layout/facets.js +1 -1
  200. package/src/layout/legendGroup.js +1 -1
  201. package/src/layout/legendItems.js +26 -10
  202. package/src/layout/legendSpacing.js +66 -0
  203. package/src/layout/plotDrawOrder.js +17 -0
  204. package/src/materialization/bars/histogram.js +8 -2
  205. package/src/materialization/bars/resolve.js +1 -1
  206. package/src/materialization/composition.js +1 -0
  207. package/src/materialization/compositionSnapshot.js +22 -2
  208. package/src/materialization/dataProvenance.js +1 -1
  209. package/src/materialization/facetGuides/layout.js +1 -1
  210. package/src/materialization/facetGuides/placement.js +23 -11
  211. package/src/materialization/facetGuides/preparation.js +43 -24
  212. package/src/materialization/facetHeaders.js +1 -1
  213. package/src/materialization/facetTopLane.js +25 -0
  214. package/src/materialization/facets.js +1 -1
  215. package/src/materialization/graphicHierarchy.js +5 -0
  216. package/src/materialization/graphicTranslation.js +2 -0
  217. package/src/materialization/guides/layout.js +1 -1
  218. package/src/materialization/guides/resources.js +1 -1
  219. package/src/materialization/legends.js +1 -1
  220. package/src/materialization/markClip.js +26 -0
  221. package/src/materialization/marks/capabilities.js +3 -2
  222. package/src/materialization/marks/grouping.js +7 -5
  223. package/src/materialization/marks/index.js +4 -2
  224. package/src/materialization/marks/policies.js +4 -0
  225. package/src/materialization/plotDrawOrder.js +44 -0
  226. package/src/materialization/rect.js +11 -3
  227. package/src/materialization/scaleGuideDependencies.js +13 -5
  228. package/src/materialization/selection/items/common.js +1 -1
  229. package/src/materialization/selection/items/path.js +1 -0
  230. package/src/materialization/selection/policies/index.js +2 -1
  231. package/src/materialization/text.js +10 -5
  232. package/src/mcp/cli.js +0 -0
  233. package/src/persistence/validation.js +108 -18
  234. package/src/persistence.js +4 -2
  235. package/src/renderers/canvas/index.js +16 -0
  236. package/src/renderers/canvas/isolated.js +11 -1
  237. package/src/renderers/canvas/native.js +7 -0
  238. package/src/renderers/canvas/scene.js +66 -0
  239. package/src/renderers/canvas/text.js +3 -2
  240. package/src/renderers/svg.js +1 -617
  241. package/src/renderers/svgSerialization.js +643 -0
  242. package/src/renderers/transitionScene.js +67 -0
  243. package/src/scene.js +89 -0
  244. package/src/selectors/axes.js +1 -1
  245. package/src/selectors/datasets.js +1 -0
  246. package/src/selectors/grids.js +9 -0
  247. package/src/selectors/markClip.js +28 -0
  248. package/src/selectors/raincloud.js +7 -0
  249. package/src/version.js +1 -1
  250. package/types/basic.d.ts +7 -0
  251. package/types/index.d.ts +8 -0
  252. package/types/program.d.ts +270 -60
  253. package/types/scene.d.ts +35 -0
@@ -10,7 +10,8 @@ import {
10
10
  } from "../../materialization/dependencies.js";
11
11
  import {
12
12
  getLayerScaleIds,
13
- getMarkMaterializationStep
13
+ getMarkMaterializationStep,
14
+ reusePlannedMarkScales
14
15
  } from "../../materialization/marks/index.js";
15
16
  import { buildMaterializationPlan } from "../../materialization/planner.js";
16
17
  import { requireDataset } from "../../selectors/datasets.js";
@@ -163,6 +164,32 @@ function buildFacetCellCandidate(
163
164
  return { program, populated: true };
164
165
  }
165
166
 
167
+ function partitionGuideValues(program) {
168
+ let next = program;
169
+ const restrict = config => {
170
+ if (config?.mode !== "values" || config.inferredValues === true) return config;
171
+ const scale = next.resolvedScales[config.scale];
172
+ if (!Array.isArray(scale?.domain)) return config;
173
+ const discrete = ["ordinal", "band", "point"].includes(scale.type);
174
+ const domain = new Set(scale.domain);
175
+ const low = Math.min(...scale.domain), high = Math.max(...scale.domain);
176
+ return { ...config, values: config.values.filter(value => discrete
177
+ ? domain.has(value) : value >= low && value <= high) };
178
+ };
179
+ for (const [id, axis] of Object.entries(program.guideConfigs.axis ?? {})) {
180
+ for (const component of ["ticks", "labels"]) {
181
+ const config = axis[component];
182
+ const restricted = restrict(config);
183
+ if (restricted !== config) next = next._withGuideConfig(id, component, restricted);
184
+ }
185
+ }
186
+ for (const [id, config] of Object.entries(program.guideConfigs.grid ?? {})) {
187
+ const restricted = restrict(config);
188
+ if (restricted !== config) next = next._withGridConfig(id, restricted);
189
+ }
190
+ return next;
191
+ }
192
+
166
193
  function applyResolvedDomains(
167
194
  program,
168
195
  childId,
@@ -188,19 +215,21 @@ function applyResolvedDomains(
188
215
  domain
189
216
  });
190
217
  }
218
+ next = partitionGuideValues(next);
191
219
  const marks = materializeMarks ? next.semanticSpec.layers.flatMap(layer => {
192
220
  const step = getMarkMaterializationStep(next, layer);
193
221
  if (step === undefined) return [];
194
- return ["bar", "line", "area", "arc"].includes(layer.mark?.type)
195
- ? [{ ...step, args: { ...step.args, scales: false } }]
196
- : [step];
222
+ return [step];
197
223
  }) : [];
198
224
  const guides = Object.keys(resolution.scales).flatMap(id =>
199
225
  planScaleGuideRematerialization(next, id)
200
226
  );
201
227
  return applyMaterializationPlan(
202
228
  next,
203
- buildMaterializationPlan({ marks, guides })
229
+ buildMaterializationPlan({
230
+ marks: reusePlannedMarkScales(next, marks, Object.keys(resolution.scales)),
231
+ guides
232
+ })
204
233
  );
205
234
  }
206
235
 
@@ -1,38 +1 @@
1
- import { closedAction } from "../../core/action.js";
2
- import { isPlainObject } from "../../core/immutable.js";
3
- import { validateUserId } from "../../core/identifiers.js";
4
-
5
- import { findTransformPolicy } from "../../grammar/transforms.js";
6
-
7
- const REPLAY_OPTIONS = Object.freeze(["id", "source", "transform"]);
8
-
9
- function requestedTransform(transform) {
10
- if (!isPlainObject(transform)) {
11
- throw new TypeError("replayDerivedData transform must be a plain object.");
12
- }
13
- const policy = findTransformPolicy(transform.type);
14
- if (policy?.facetTopology === undefined) {
15
- throw new Error(
16
- `replayDerivedData does not support transform "${transform.type ?? "unknown"}".`
17
- );
18
- }
19
- return {
20
- transform: policy.replayTransform?.(transform) ?? transform,
21
- materialize: policy.materializeOp
22
- };
23
- }
24
-
25
- export const replayDerivedData = /* @__PURE__ */ closedAction(
26
- {
27
- op: "replayDerivedData",
28
- description: "Replay one stored derived-data transform for a facet cell."
29
- }, REPLAY_OPTIONS,
30
- function (args = {}) {
31
- const id = validateUserId(args.id, "Facet replay dataset id");
32
- const source = validateUserId(args.source, "Facet replay source id");
33
- const resolved = requestedTransform(args.transform);
34
- return this
35
- .createDerivedData({ id, source, transform: [resolved.transform] })
36
- [resolved.materialize]({ id });
37
- }
38
- );
1
+ import{findRaincloudParent as d}from"../../selectors/raincloud.js";import{closedAction as m}from"../../core/action.js";import{isPlainObject as p}from"../../core/immutable.js";import{validateUserId as s}from"../../core/identifiers.js";import{findTransformPolicy as c}from"../../grammar/transforms.js";import{applyLayerDataRematerialization as l}from"../../materialization/dependencies.js";const y=Object.freeze(["id","source","transform"]);function u(r){if(!p(r))throw new TypeError("replayDerivedData transform must be a plain object.");const e=c(r.type);if(e?.facetTopology===void 0)throw new Error(`replayDerivedData does not support transform "${r.type??"unknown"}".`);return{transform:e.replayTransform?.(r)??r,materialize:e.materializeOp}}const b=m({op:"replayDerivedData",description:"Replay one stored derived-data transform for a facet cell."},y,function(r={}){const e=s(r.id,"Facet replay dataset id"),i=s(r.source,"Facet replay source id"),a=u(r.transform);let t=this;if(a.transform.type==="markFilter"&&d(t,a.transform.target)===void 0){const o=a.transform.target,{markFilter:f,...n}=t.markConfigs[o]??{};t=t.rebindLayerData({id:o,data:i})._withMarkConfig(o,n),t=l(t,o)._withMarkConfig(o,{...n,markFilter:{...f,source:i,dataset:e,empty:!1}})}return t.createDerivedData({id:e,source:i,transform:[a.transform]})[a.materialize]({id:e})});export{b as replayDerivedData};
@@ -9,6 +9,8 @@ import {
9
9
  import { resolveGraphicBounds } from "../../layout/canvas.js";
10
10
  import { findLayer } from "../../selectors/layers.js";
11
11
  import { createDensityLegendPaint } from "./paint.js";
12
+ import { legendResourcePolicy } from "../../materialization/guides/resources.js";
13
+ import { resolveConcreteGraphicBounds } from "../../grammar/schemas/graphicBounds.js";
12
14
 
13
15
  const CENTER_OPTIONS = Object.freeze([
14
16
  "id", "owner", "data", "category", "categoryType", "coordinate",
@@ -94,11 +96,18 @@ export const rematerializeGradientPlotLegend = /* @__PURE__ */ action(
94
96
  if (position !== "right") {
95
97
  throw new Error('Gradient plot density legend currently requires position "right".');
96
98
  }
97
- const x = bounds.x + bounds.width + 50;
99
+ const right = bounds.x + bounds.width;
100
+ const legendBounds = Object.keys(this.guideConfigs.legend ?? {}).flatMap(kind =>
101
+ legendResourcePolicy(kind).graphicIds.filter(id => this.graphicSpec.objects[id])
102
+ .map(id => resolveConcreteGraphicBounds(this.graphicSpec, id, this.materializationConfigs.textMetrics)))
103
+ .filter(box => box && box.left >= right);
104
+ const x = Math.max(right + 50, ...legendBounds.map(box => box.right + 24));
98
105
  const y = bounds.y + 50;
99
106
  const width = 22;
100
107
  const height = Math.min(170, Math.max(80, bounds.height - 110));
101
- let next = editProperties(this, ids.strip, {
108
+ const baseline = this.semanticSpec.guides.legend?.color?.scale === `${owner}Density`
109
+ ? this.editSemantic({ property: "guide.legend.color", remove: true }) : this;
110
+ let next = editProperties(baseline, ids.strip, {
102
111
  x, y, width, height,
103
112
  fill: createDensityLegendPaint(config.gradient.opacity),
104
113
  stroke: "#cbd5e1",
@@ -144,14 +153,14 @@ function resolveDensityLegend(program, args) {
144
153
  }
145
154
 
146
155
  export function fulfillGradientPlotLegend(program, args) {
147
- const { ids, densityScale } = resolveDensityLegend(program, args);
148
- const occupied = program.semanticSpec.guides.legend?.color;
149
- if (occupied !== undefined && occupied.scale !== densityScale) {
156
+ const { ids } = resolveDensityLegend(program, args);
157
+ if (Object.keys(program.markConfigs).some(owner => owner !== args.owner &&
158
+ program.graphicSpec.objects[`${owner}DensityLegend`])) {
150
159
  guideConflict("Gradient plot density legend uses a different scale or family");
151
160
  }
152
161
  const existing = Object.values(ids).filter(id => program.graphicSpec.objects[id] !== undefined);
153
162
  if (existing.length === 0) return program.createGradientPlotLegend(args);
154
- if (existing.length !== Object.keys(ids).length || occupied?.scale !== densityScale) {
163
+ if (existing.length !== Object.keys(ids).length) {
155
164
  guideConflict("Gradient plot density legend has incomplete ownership");
156
165
  }
157
166
  const { owner, ...explicit } = args;
@@ -170,12 +179,15 @@ export const createGradientPlotLegend = /* @__PURE__ */ action(
170
179
  for (const [property, value] of [
171
180
  [`scale[${densityScale}].type`, "sequential"],
172
181
  [`scale[${densityScale}].domain`, this.markConfigs[args.owner].gradientPlot.intensityDomain],
173
- [`scale[${densityScale}].range`, { palette: { name: "greys" } }],
174
- ["guide.legend.color.scale", densityScale],
175
- ["guide.legend.color.title", title]
182
+ [`scale[${densityScale}].range`, { palette: { name: "greys" } }]
176
183
  ]) {
177
184
  next = next.editSemantic({ property, value });
178
185
  }
186
+ // Older snapshots used the color-channel slot for this independent guide.
187
+ // Its existing owner config is sufficient; free that slot for actual hue.
188
+ if (next.semanticSpec.guides.legend?.color?.scale === densityScale) {
189
+ next = next.editSemantic({ property: "guide.legend.color", remove: true });
190
+ }
179
191
  for (const [id, type, length] of [
180
192
  [ids.strip, "rect"],
181
193
  [ids.labels, "text", 2],
@@ -2,6 +2,7 @@ import { closedAction } from "../../core/action.js";
2
2
 
3
3
  import { findDataset } from "../../selectors/datasets.js";
4
4
  import { findSemanticScale } from "../../selectors/scales.js";
5
+ import { normalizeEncoding } from "../charts/shared.js";
5
6
  import {
6
7
  GRADIENT_PLOT_OPTIONS,
7
8
  gradientEncodingArgs,
@@ -49,6 +50,7 @@ export const createGradientPlot = /* @__PURE__ */ closedAction(
49
50
  const width = resolveGradientWidth(args.width);
50
51
  const gradient = resolveGradientAppearance(args.gradient);
51
52
  const center = resolveGradientCenter(args.center);
53
+ const color = normalizeEncoding(args.color, "createGradientPlot color");
52
54
  const guides = resolveGradientGuides(args.guides);
53
55
  const category = positions.orientation === "vertical"
54
56
  ? positions.x
@@ -70,11 +72,11 @@ export const createGradientPlot = /* @__PURE__ */ closedAction(
70
72
  width,
71
73
  gradient,
72
74
  center,
75
+ ...(color === undefined ? {} : { color }),
73
76
  guides
74
77
  },
75
78
  itemFilterable: false,
76
- stroke: false,
77
- strokeWidth: 0
79
+ stroke: false
78
80
  });
79
81
  if (positions.x !== undefined) {
80
82
  next = next.encodeX({
@@ -9,6 +9,7 @@ import { removeOwnedMark } from "../marks/remove.js";
9
9
  import { GRADIENT_PROFILE_FIELDS } from "../../grammar/gradientProfile.js";
10
10
  import { requestedStrokeDetails } from "../../grammar/strokeStyle.js";
11
11
  import { removeGradientPlotLegend } from "./components.js";
12
+ import { rematerializeExistingLegend } from "../encodings/shared.js";
12
13
  import {
13
14
  resolveDistributionRoles,
14
15
  setCartesianPosition,
@@ -191,6 +192,22 @@ export const editGradientPlot = /* @__PURE__ */ closedAction(
191
192
  intensityDomain: profile.transform[0].resolved.intensityDomain
192
193
  }
193
194
  });
195
+ // Hue assigned to the old category follows the category role. Internal
196
+ // profile fields keep their stable names across statistical revisions.
197
+ if (candidate.category !== current.category) {
198
+ for (const channel of ["color", "stroke"]) {
199
+ const binding = owner.encoding?.[channel];
200
+ if (binding?.field !== current.category ||
201
+ !["nominal", "ordinal"].includes(binding.fieldType)) continue;
202
+ next = next.editSemantic({
203
+ property: `layer[${owner.id}].encoding.${channel}.field`,
204
+ value: candidate.category
205
+ }).editSemantic({
206
+ property: `layer[${owner.id}].encoding.${channel}.fieldType`,
207
+ value: candidate.categoryType
208
+ });
209
+ }
210
+ }
194
211
  if (center === false && hadCenter) {
195
212
  next = removeCenter(next, current.centerId);
196
213
  }
@@ -252,7 +269,7 @@ export const editGradientPlot = /* @__PURE__ */ closedAction(
252
269
  });
253
270
  }
254
271
  next = next.releaseDerivedData(revision.release);
255
- return next._withContext({
272
+ return rematerializeExistingLegend(next)._withContext({
256
273
  currentMark: owner.id,
257
274
  currentData: candidate.source
258
275
  });
@@ -5,6 +5,8 @@ import { validateUserId } from "../../core/identifiers.js";
5
5
  import { isPlainObject } from "../../core/immutable.js";
6
6
  import { GRADIENT_PROFILE_FIELDS } from "../../grammar/gradientProfile.js";
7
7
  import { requestedStrokeDetails } from "../../grammar/strokeStyle.js";
8
+ import { materializeRectItem } from "../../grammar/roundedRect.js";
9
+ import { replaceGraphicItems } from "../primitives/editGraphics.js";
8
10
  import {
9
11
  mapContinuousScaleValues,
10
12
  mapOrdinalPositionValues
@@ -16,7 +18,7 @@ import { findDataset } from "../../selectors/datasets.js";
16
18
  import { findLayer } from "../../selectors/layers.js";
17
19
  import {
18
20
  createGradientPlotPaint,
19
- resolveGradientPlotCategoryColors
21
+ resolveGradientPlotAppearanceValues
20
22
  } from "./paint.js";
21
23
  import { resolveGradientOrientation } from "./resolve.js";
22
24
 
@@ -73,11 +75,9 @@ function bodyItems(program, layer, dataset, config) {
73
75
  rows.map(row => row[GRADIENT_PROFILE_FIELDS.upper]),
74
76
  measureScale
75
77
  );
76
- const categoryColors = resolveGradientPlotCategoryColors(
77
- program,
78
- layer,
79
- categories
80
- );
78
+ const appearance = program.markConfigs[layer.id];
79
+ const mapped = Object.fromEntries(["color", "opacity", "stroke", "strokeWidth"]
80
+ .map(channel => [channel, resolveGradientPlotAppearanceValues(program, layer, rows, channel)]));
81
81
  const thickness = categoryScale.bandwidth * config.width.band;
82
82
  if (!Number.isFinite(thickness) || thickness <= 0) {
83
83
  throw new Error(`Gradient plot "${layer.id}" requires a positive band width.`);
@@ -98,28 +98,27 @@ function bodyItems(program, layer, dataset, config) {
98
98
  width: Math.abs(a - b),
99
99
  height: thickness
100
100
  };
101
- return {
102
- type: "rect",
103
- properties: {
104
- ...geometry,
105
- fill: createGradientPlotPaint({
106
- values: row[GRADIENT_PROFILE_FIELDS.values],
107
- intensities: row[GRADIENT_PROFILE_FIELDS.intensities]
108
- }, {
109
- orientation: config.orientation,
110
- valueScale: measureScale,
111
- intensityDomain: config.intensityDomain,
112
- palette: config.gradient.palette,
113
- opacity: config.gradient.opacity,
114
- ...(categoryColors === undefined
115
- ? {}
116
- : { baseColor: categoryColors[index] })
117
- }),
118
- opacity: 1,
119
- stroke: "transparent",
120
- strokeWidth: 0
121
- }
122
- };
101
+ return materializeRectItem({
102
+ ...geometry,
103
+ fill: createGradientPlotPaint({
104
+ values: row[GRADIENT_PROFILE_FIELDS.values],
105
+ intensities: row[GRADIENT_PROFILE_FIELDS.intensities]
106
+ }, {
107
+ orientation: config.orientation,
108
+ valueScale: measureScale,
109
+ intensityDomain: config.intensityDomain,
110
+ palette: config.gradient.palette,
111
+ opacity: config.gradient.opacity,
112
+ ...(mapped.color === undefined
113
+ ? appearance.fillExplicit ? { baseColor: appearance.fill } : {}
114
+ : { baseColor: mapped.color[index] })
115
+ }),
116
+ opacity: mapped.opacity?.[index] ?? appearance.opacity ?? 1,
117
+ stroke: mapped.stroke?.[index] ?? (appearance.stroke === false ? "transparent" : appearance.stroke ?? "transparent"),
118
+ strokeWidth: mapped.strokeWidth?.[index] ??
119
+ (mapped.stroke === undefined && appearance.stroke === false ? 0 : appearance.strokeWidth ?? 1),
120
+ ...requestedStrokeDetails(appearance, "Gradient plot strip")
121
+ }, appearance);
123
122
  });
124
123
  }
125
124
 
@@ -128,7 +127,7 @@ export const materializeGradientPlotFill = /* @__PURE__ */ action(
128
127
  op: "materializeGradientPlotFill",
129
128
  description: "Materialize categorical density profiles as gradient strips."
130
129
  },
131
- function ({ id } = {}) {
130
+ function ({ id, scales = true } = {}) {
132
131
  const owner = validateUserId(id, "Gradient-plot id");
133
132
  const layer = findLayer(this, owner);
134
133
  const config = this.markConfigs[owner]?.gradientPlot;
@@ -142,17 +141,14 @@ export const materializeGradientPlotFill = /* @__PURE__ */ action(
142
141
  const scaleIds = [...new Set([
143
142
  layer.encoding?.x?.scale,
144
143
  layer.encoding?.y?.scale,
145
- layer.encoding?.color?.scale
144
+ ...["color", "opacity", "stroke", "strokeWidth"].map(channel => layer.encoding?.[channel]?.scale)
146
145
  ].filter(Boolean))];
147
146
  let next = this;
148
- for (const scale of scaleIds) {
147
+ for (const scale of scales ? scaleIds : []) {
149
148
  next = next.rematerializeScale({ id: scale, marks: false, guides: false });
150
149
  }
151
- next = next.editGraphics({
152
- target: owner,
153
- property: "items",
154
- value: bodyItems(next, findLayer(next, owner), dataset, config)
155
- });
150
+ next = replaceGraphicItems(next, owner, "rect",
151
+ bodyItems(next, findLayer(next, owner), dataset, config));
156
152
  next = applyMaterializationPlan(
157
153
  next,
158
154
  buildMaterializationPlan({
@@ -253,6 +249,9 @@ export const materializeGradientPlot = /* @__PURE__ */ action(
253
249
  ...requestedStrokeDetails(config.center, "Gradient plot center")
254
250
  });
255
251
  }
252
+ if (config.color !== undefined) {
253
+ next = next.encodeColor({ ...config.color, target: owner });
254
+ }
256
255
  const guides = config.guides;
257
256
  if (guides !== false) {
258
257
  const wantsStandard = guides.axes !== false || guides.grid !== false;
@@ -12,7 +12,7 @@ import {
12
12
 
13
13
  export const GRADIENT_PLOT_OPTIONS = Object.freeze([
14
14
  "id", "target", "data", "x", "y", "coordinate", "density", "width",
15
- "gradient", "center", "guides"
15
+ "gradient", "center", "color", "guides"
16
16
  ]);
17
17
 
18
18
  function plain(value, keys, label, operation) {
@@ -1,6 +1,8 @@
1
1
  import { resolveContinuousPalette } from "../../grammar/palettes.js";
2
2
  import { inverseLerp } from "../../grammar/numeric.js";
3
3
  import { mapScaleConsumerValues } from "../../materialization/scales/map.js";
4
+ import { readScaleField } from "../../grammar/scales/index.js";
5
+ import { resolveSequentialColorStops } from "../../grammar/scales/color.js";
4
6
 
5
7
  function parseHex(value) {
6
8
  const match = value.match(/^#([0-9a-f]{6})$/i);
@@ -31,21 +33,25 @@ function paletteColor(colors, amount) {
31
33
  }
32
34
 
33
35
  function categoricalRamp(base) {
34
- const rgb = parseHex(base);
36
+ const normalized = resolveSequentialColorStops([base, base])[0];
37
+ const rgb = parseHex(normalized);
35
38
  const pale = interpolate([255, 255, 255], rgb, 0.2);
36
39
  return [
37
40
  `#${pale.map(value => value.toString(16).padStart(2, "0")).join("")}`,
38
- base
41
+ normalized
39
42
  ];
40
43
  }
41
44
 
42
- export function resolveGradientPlotCategoryColors(program, layer, categories) {
43
- if (layer.encoding?.color === undefined) return undefined;
44
- const scale = program.resolvedScales[layer.encoding.color.scale];
45
+ export function resolveGradientPlotAppearanceValues(program, layer, rows, channel) {
46
+ const encoding = layer.encoding?.[channel];
47
+ if (encoding === undefined) return undefined;
48
+ const scale = program.resolvedScales[encoding.scale];
45
49
  if (scale === undefined) {
46
- throw new Error(`Gradient plot "${layer.id}" requires a resolved color scale.`);
50
+ throw new Error(`Gradient plot "${layer.id}" requires a resolved ${channel} scale.`);
47
51
  }
48
- return mapScaleConsumerValues(categories, scale, "color");
52
+ return mapScaleConsumerValues(readScaleField(rows, encoding.field, encoding.fieldType, {
53
+ temporalUnit: encoding.temporalUnit
54
+ }), scale, channel);
49
55
  }
50
56
 
51
57
  export function createGradientPlotPaint(profile, {
@@ -25,6 +25,8 @@ function hasEncoding(program, layers, channel, { scaled = false, grid = false }
25
25
  return layers.some(layer => {
26
26
  if (isHorizonLayer(program, layer) && channel !== "x") return false;
27
27
  const encoding = layer.encoding?.[channel];
28
+ if (encoding?.datum !== undefined && encoding.field === undefined &&
29
+ layer.encoding?.[`${channel}2`]?.field === undefined) return false;
28
30
  if (!scaled && !grid) return encoding !== undefined;
29
31
  if (encoding?.scale === undefined) return false;
30
32
  if (!grid || POLAR_POSITION_CHANNELS.includes(channel)) return true;
@@ -5,6 +5,7 @@ import { isPlainObject } from "../../../core/immutable.js";
5
5
  import { validateUserId } from "../../../core/identifiers.js";
6
6
  import { validateKeys, validateOptionObject } from "../../../core/validation.js";
7
7
  import { findCoordinate } from "../../../selectors/coordinates.js";
8
+ import { resolveParallelAxisTarget } from "./parallel/resolve.js";
8
9
  import { resolvePolarGuideResources } from "../polar/resolve.js";
9
10
 
10
11
  const TOP_OPTIONS = Object.freeze([
@@ -326,6 +327,44 @@ const createAxes = /* @__PURE__ */ action(
326
327
  }
327
328
  );
328
329
 
330
+ // Aggregate existing component editors; omission/false preserves that axis.
331
+ const editAxes = /* @__PURE__ */ action(
332
+ { op: "editAxes", description: "Edit selected existing axes in one atomic layout transaction." },
333
+ function (args = {}) {
334
+ const methods = { x: "editXAxis", y: "editYAxis", theta: "editThetaAxis",
335
+ radius: "editRadialAxis", parallel: "editParallelAxis" };
336
+ validateOptionObject(args, Object.keys(methods), "editAxes", { allowEmpty: false });
337
+ const plan = [];
338
+ for (const [channel, method] of Object.entries(methods)) {
339
+ const option = args[channel];
340
+ if (option === undefined || option === false) continue;
341
+ const entries = Array.isArray(option) ? option : [option];
342
+ if (entries.length === 0 || entries.some(entry => !isPlainObject(entry))) {
343
+ throw new TypeError(`editAxes ${channel} must be a plain object or a nonempty array of edit options.`);
344
+ }
345
+ if (Array.isArray(option) && ["theta", "radius"].includes(channel)) {
346
+ throw new TypeError(`editAxes ${channel} does not accept an array.`);
347
+ }
348
+ const targets = new Set();
349
+ for (const entry of entries) {
350
+ if (Array.isArray(option) && ["x", "y"].includes(channel) && entry.target === undefined) {
351
+ throw new Error(`editAxes ${channel} arrays require an explicit axis target for every entry.`);
352
+ }
353
+ const key = channel === "parallel" ? `${resolveParallelAxisTarget(this, entry.target ?? this.guideConfigs.axis?.parallel?.axes?.target)}:${entry.field}` : entry.target ?? channel;
354
+ if (targets.has(key)) throw new Error(`editAxes ${channel} contains a duplicate axis target.`);
355
+ targets.add(key);
356
+ plan.push({ method, args: entry });
357
+ }
358
+ }
359
+ if (plan.length === 0) throw new Error("editAxes requires at least one axis change.");
360
+ const apply = source => plan.reduce((program, step) => program[step.method](step.args), source);
361
+ // Preflight the full immutable proposal before beginning the returned branch.
362
+ withGuideLayoutTransaction(this, apply);
363
+ return withGuideLayoutTransaction(this, apply);
364
+ }
365
+ );
366
+
329
367
  export function registerAxisCollectionActions(ProgramClass) {
330
368
  ProgramClass.prototype.createAxes = createAxes;
369
+ ProgramClass.prototype.editAxes = editAxes;
331
370
  }
@@ -1,7 +1,7 @@
1
1
  import { AXIS_RESOURCE_CHANNELS } from "../../../core/vocabulary.js";
2
2
  import { cartesianAxisOwners, axisTargetArgs, assertCartesianAxisSideAvailable } from "../../../selectors/axes.js";
3
3
  import { axisGraphicIds } from "../../../materialization/guides/resources.js";
4
- import { defaultAxisPosition, validateAxisPosition } from "./policy.js";
4
+ import { defaultAxisPosition, validateAxisPosition, resolveAxisFrameConfig } from "./policy.js";
5
5
  import { isSourceOwnedText } from "../../../grammar/text.js";
6
6
  import { withGuideLayoutValidation } from "../../../materialization/guides/layout.js";
7
7
  import { action } from "../../../core/action.js";
@@ -21,12 +21,13 @@ const TOP_OPTIONS = Object.freeze([
21
21
  "line",
22
22
  "ticksAndLabels",
23
23
  "title",
24
- "offset"
24
+ "offset", "underlay"
25
25
  ]);
26
26
  const LINE_OPTIONS = Object.freeze(["color", "lineWidth"]);
27
27
  const TICK_GROUP_OPTIONS = Object.freeze([
28
28
  "count",
29
29
  "values",
30
+ "rounding",
30
31
  "ticks",
31
32
  "labels"
32
33
  ]);
@@ -39,7 +40,7 @@ const TITLE_OPTIONS = Object.freeze([
39
40
  "fontSize",
40
41
  "fontFamily",
41
42
  "fontWeight",
42
- "alongOffset", "align", "baseline"
43
+ "alongOffset", "align", "baseline", "overlap"
43
44
  ]);
44
45
 
45
46
  function validateNested(value, supported, label) {
@@ -53,6 +54,7 @@ function validateNested(value, supported, label) {
53
54
  export function validateAxisArgs(args, operation) {
54
55
  validateKeys(args, TOP_OPTIONS, operation);
55
56
  if (Object.hasOwn(args, "offset")) validateNonNegativeFinite(args.offset, "Axis frame offset");
57
+ resolveAxisFrameConfig(undefined, args);
56
58
 
57
59
  validateEnabledAxisComponents(args, operation);
58
60
  if (Object.hasOwn(args, "line") && args.line !== false) {
@@ -136,8 +138,10 @@ function makeCreateAxis(channel) {
136
138
  const shared = axisTargetArgs(axisId, channel);
137
139
  if (Object.hasOwn(args, "scale")) shared.scale = args.scale;
138
140
  if (Object.hasOwn(args, "position")) shared.position = args.position;
139
- let next = Object.hasOwn(args, "offset")
140
- ? source._withGuideConfig(axisId, "frame", { offset: args.offset }) : source;
141
+ const frame = resolveAxisFrameConfig(source.guideConfigs.axis?.[axisId]?.frame, args);
142
+ let next = frame === undefined
143
+ ? source._withoutMaterializationConfig(["guides", "axis", axisId, "frame"])
144
+ : source._withGuideConfig(axisId, "frame", frame);
141
145
 
142
146
  if (Object.hasOwn(args, "coordinate")) {
143
147
  const coordinate = validateUserId(args.coordinate, "Coordinate id");
@@ -1,5 +1,5 @@
1
1
  import { selectCartesianAxis, axisTargetArgs, assertCartesianAxisSideAvailable } from "../../../selectors/axes.js";
2
- import { validateAxisPosition } from "./policy.js";
2
+ import { validateAxisPosition, resolveAxisFrameConfig } from "./policy.js";
3
3
  import { withGuideLayoutValidation } from "../../../materialization/guides/layout.js";
4
4
  import { action } from "../../../core/action.js";
5
5
  import { isPlainObject } from "../../../core/immutable.js";
@@ -14,21 +14,21 @@ import {
14
14
  } from "./components.js";
15
15
 
16
16
  const OPTIONS = Object.freeze([
17
- "target", "position", "line", "ticks", "labels", "ticksAndLabels", "title", "offset"
17
+ "target", "position", "line", "ticks", "labels", "ticksAndLabels", "title", "offset", "underlay"
18
18
  ]);
19
19
  const LINE_OPTIONS = Object.freeze(["color", "lineWidth"]);
20
20
  const TICK_OPTIONS = Object.freeze([
21
- "count", "values", "length", "color", "lineWidth", "alongOffset", "strokeDash", "strokeDashMap"
21
+ "count", "values", "rounding", "length", "color", "lineWidth", "alongOffset", "strokeDash", "strokeDashMap"
22
22
  ]);
23
23
  const LABEL_OPTIONS = Object.freeze([
24
- "count", "values", "offset", "format", "color", "fontSize",
24
+ "count", "values", "rounding", "offset", "format", "color", "fontSize",
25
25
  "fontFamily", "fontWeight", "rotation", "maxWidth", "wrap",
26
26
  "lineHeight", "overlap", "alongOffset", "labelMap", "flush", "colorMap", "align"
27
27
  ]);
28
- const GROUP_OPTIONS = Object.freeze(["count", "values", "ticks", "labels"]);
28
+ const GROUP_OPTIONS = Object.freeze(["count", "values", "rounding", "ticks", "labels"]);
29
29
  const TITLE_OPTIONS = Object.freeze([
30
30
  "text", "at", "offset", "rotation", "color", "fontSize",
31
- "fontFamily", "fontWeight", "alongOffset", "align", "baseline"
31
+ "fontFamily", "fontWeight", "alongOffset", "align", "baseline", "overlap"
32
32
  ]);
33
33
 
34
34
  function validateNested(args, key, options, operation, { allowFalse = false } = {}) {
@@ -43,6 +43,7 @@ function validateNested(args, key, options, operation, { allowFalse = false } =
43
43
 
44
44
  function validateArgs(args, operation) {
45
45
  if (Object.hasOwn(args, "offset")) validateNonNegativeFinite(args.offset, "Axis frame offset");
46
+ resolveAxisFrameConfig(undefined, args);
46
47
  validateOptionObject(args, OPTIONS, operation, {
47
48
  allowEmpty: false,
48
49
  emptyMessage: `${operation} requires at least one axis change.`,
@@ -96,7 +97,7 @@ function buildPlan(program, channel, args, operation, axisId = channel) {
96
97
  plan.push({ kind: "remove", component });
97
98
  } else if (value !== undefined) {
98
99
  plan.push({ kind: "edit", operation: edit, args: { ...shared, ...value } });
99
- } else if ((args.position !== undefined || Object.hasOwn(args, "offset")) && hasComponent(program, channel, component, axisId)) {
100
+ } else if ((args.position !== undefined || Object.hasOwn(args, "offset") || Object.hasOwn(args, "underlay")) && hasComponent(program, channel, component, axisId)) {
100
101
  plan.push({ kind: "edit", operation: edit, args: shared });
101
102
  }
102
103
  };
@@ -165,7 +166,9 @@ function makeEditAxis(channel) {
165
166
  coordinate: selected.owner.coordinate, position });
166
167
  source = source.editSemantic({ property: `guide.axis.${axisId}.position`, value: position });
167
168
  }
168
- if (Object.hasOwn(args, "offset")) source = source._withGuideConfig(axisId, "frame", { offset: args.offset });
169
+ const frame = resolveAxisFrameConfig(source.guideConfigs.axis?.[axisId]?.frame, args);
170
+ source = frame === undefined ? source._withoutMaterializationConfig(["guides", "axis", axisId, "frame"])
171
+ : source._withGuideConfig(axisId, "frame", frame);
169
172
  const plan = buildPlan(source, channel, args, operation, axisId);
170
173
 
171
174
  // Run the complete proposal against an immutable speculative branch so a