ggaction 0.0.21 → 0.0.23

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 (265) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +83 -90
  3. package/knowledge/action-card.schema.json +1 -1
  4. package/knowledge/action-cards.json +1 -1
  5. package/knowledge/intent-taxonomy.json +1 -1
  6. package/knowledge/mcp-resources.json +1 -1
  7. package/package.json +11 -4
  8. package/src/BasicChartProgram.js +1 -1
  9. package/src/accessibility.js +1 -0
  10. package/src/actions/boxPlots/components.js +1 -96
  11. package/src/actions/boxPlots/create.js +6 -1
  12. package/src/actions/boxPlots/edit.js +8 -0
  13. package/src/actions/boxPlots/materialize.js +3 -0
  14. package/src/actions/boxPlots/options.js +1 -1
  15. package/src/actions/canvas/actions.js +2 -1
  16. package/src/actions/canvas/index.js +3 -0
  17. package/src/actions/canvas/plotBorder.js +2 -1
  18. package/src/actions/canvas/plotOrder.js +14 -0
  19. package/src/actions/charts/area.js +1 -1
  20. package/src/actions/charts/bar.js +4 -4
  21. package/src/actions/charts/density.js +1 -1
  22. package/src/actions/charts/ecdf.js +1 -1
  23. package/src/actions/charts/endpoints.js +54 -5
  24. package/src/actions/charts/heatmap.js +1 -1
  25. package/src/actions/charts/histogram.js +1 -1
  26. package/src/actions/charts/horizon.js +1 -1
  27. package/src/actions/charts/index.js +2 -1
  28. package/src/actions/charts/interval-regression.js +3 -3
  29. package/src/actions/charts/line.js +1 -1
  30. package/src/actions/charts/radar.js +63 -3
  31. package/src/actions/charts/raincloud.js +37 -6
  32. package/src/actions/charts/rug-strip.js +2 -2
  33. package/src/actions/charts/scatter.js +3 -3
  34. package/src/actions/charts/text.js +1 -1
  35. package/src/actions/composition/actions.js +10 -3
  36. package/src/actions/coordinates/actions.js +2 -0
  37. package/src/actions/data/basic.js +6 -0
  38. package/src/actions/data/derived.js +6 -2
  39. package/src/actions/data/edit.js +22 -36
  40. package/src/actions/data/filter.js +1 -376
  41. package/src/actions/data/flatten.js +19 -0
  42. package/src/actions/data/index.js +6 -0
  43. package/src/actions/data/lookup.js +50 -0
  44. package/src/actions/data/raincloudFilter.js +56 -0
  45. package/src/actions/data/shared.js +10 -0
  46. package/src/actions/data/timeUnit.js +1 -1
  47. package/src/actions/distributions/revision.js +3 -1
  48. package/src/actions/encodings/angle.js +2 -1
  49. package/src/actions/encodings/appearance.js +38 -13
  50. package/src/actions/encodings/channels.js +14 -3
  51. package/src/actions/encodings/color/continuous.js +26 -10
  52. package/src/actions/encodings/color/index.js +50 -20
  53. package/src/actions/encodings/color/policy.js +57 -4
  54. package/src/actions/encodings/parallel.js +4 -0
  55. package/src/actions/encodings/position/policies/bar.js +10 -6
  56. package/src/actions/encodings/position/policies/line.js +1 -152
  57. package/src/actions/encodings/position/resolve.js +4 -1
  58. package/src/actions/encodings/ranged.js +46 -0
  59. package/src/actions/encodings/remove.js +1 -1
  60. package/src/actions/encodings/ruleAppearance.js +7 -6
  61. package/src/actions/encodings/shared.js +14 -2
  62. package/src/actions/encodings/stroke.js +20 -14
  63. package/src/actions/encodings/strokeDash.js +9 -11
  64. package/src/actions/encodings/text.js +30 -2
  65. package/src/actions/errorBars/edit.js +24 -4
  66. package/src/actions/facets/actions.js +1 -793
  67. package/src/actions/facets/derive.js +34 -5
  68. package/src/actions/facets/replay.js +1 -38
  69. package/src/actions/gradientPlots/components.js +21 -9
  70. package/src/actions/gradientPlots/create.js +4 -2
  71. package/src/actions/gradientPlots/edit.js +18 -1
  72. package/src/actions/gradientPlots/materialize.js +35 -36
  73. package/src/actions/gradientPlots/options.js +1 -1
  74. package/src/actions/gradientPlots/paint.js +13 -7
  75. package/src/actions/guides/applicability.js +2 -0
  76. package/src/actions/guides/axes/axes.js +39 -0
  77. package/src/actions/guides/axes/axis.js +9 -5
  78. package/src/actions/guides/axes/edit.js +11 -8
  79. package/src/actions/guides/axes/labels.js +24 -8
  80. package/src/actions/guides/axes/parallel/policy.js +3 -5
  81. package/src/actions/guides/axes/parallel/resolve.js +4 -3
  82. package/src/actions/guides/axes/policy.js +22 -0
  83. package/src/actions/guides/axes/tickGroups.js +4 -2
  84. package/src/actions/guides/axes/ticks.js +22 -4
  85. package/src/actions/guides/axes/titles.js +19 -6
  86. package/src/actions/guides/facadeAxes.js +3 -0
  87. package/src/actions/guides/grids/grid.js +3 -1
  88. package/src/actions/guides/grids/resolve.js +7 -11
  89. package/src/actions/guides/guides.js +21 -1
  90. package/src/actions/guides/legends/categorical/actions.js +1 -515
  91. package/src/actions/guides/legends/categorical/components.js +1 -4
  92. package/src/actions/guides/legends/categorical/layout.js +5 -2
  93. package/src/actions/guides/legends/categorical/options.js +4 -0
  94. package/src/actions/guides/legends/categorical/resolve.js +8 -3
  95. package/src/actions/guides/legends/categorical/symbols.js +3 -3
  96. package/src/actions/guides/legends/colorSize.js +2 -0
  97. package/src/actions/guides/legends/continuous/common.js +1 -1
  98. package/src/actions/guides/legends/continuous/gradient.js +1 -360
  99. package/src/actions/guides/legends/continuous/interval.js +1 -1
  100. package/src/actions/guides/legends/continuous/opacity.js +15 -1
  101. package/src/actions/guides/legends/edit.js +1 -1
  102. package/src/actions/guides/legends/focused.js +1 -1
  103. package/src/actions/guides/legends/lane.js +1 -1
  104. package/src/actions/guides/legends/sampling.js +1 -140
  105. package/src/actions/guides/legends/strokeWidth.js +3 -2
  106. package/src/actions/guides/legends/transition.js +19 -2
  107. package/src/actions/guides/polar/axes/facade.js +5 -4
  108. package/src/actions/guides/polar/axes/labels.js +5 -4
  109. package/src/actions/guides/polar/axes/titles.js +5 -4
  110. package/src/actions/guides/polar/resolve.js +2 -1
  111. package/src/actions/guides/tickValues.js +7 -7
  112. package/src/actions/marks/arc/actions.js +5 -7
  113. package/src/actions/marks/area/actions.js +9 -3
  114. package/src/actions/marks/bar/create.js +4 -2
  115. package/src/actions/marks/bar/edit.js +9 -6
  116. package/src/actions/marks/bar/materialize.js +5 -3
  117. package/src/actions/marks/dependentReferences.js +87 -0
  118. package/src/actions/marks/index.js +2 -0
  119. package/src/actions/marks/lifecycle.js +2 -1
  120. package/src/actions/marks/line/actions.js +24 -6
  121. package/src/actions/marks/line/materialize.js +7 -3
  122. package/src/actions/marks/point/create.js +5 -4
  123. package/src/actions/marks/point/edit.js +6 -3
  124. package/src/actions/marks/point/jitter.js +6 -6
  125. package/src/actions/marks/point/materialize.js +36 -39
  126. package/src/actions/marks/rect/actions.js +16 -4
  127. package/src/actions/marks/references.js +183 -35
  128. package/src/actions/marks/rule/actions.js +20 -9
  129. package/src/actions/marks/shared.js +2 -1
  130. package/src/actions/marks/text/actions.js +10 -3
  131. package/src/actions/marks/tick/actions.js +11 -2
  132. package/src/actions/primitives/editGraphics.js +28 -3
  133. package/src/actions/primitives/index.js +9 -0
  134. package/src/actions/primitives/semanticAction.js +13 -7
  135. package/src/actions/primitives/semanticValidation/layer.js +20 -0
  136. package/src/actions/primitives/semanticValidation/scale.js +8 -1
  137. package/src/actions/regression/create.js +6 -1
  138. package/src/actions/regression/edit.js +10 -1
  139. package/src/actions/scales/channels.js +1 -1
  140. package/src/actions/scales/consumers/common.js +19 -1
  141. package/src/actions/scales/consumers/families.js +2 -2
  142. package/src/actions/scales/consumers/seriesLayout.js +4 -3
  143. package/src/actions/scales/edit.js +11 -0
  144. package/src/actions/scales/editPolicy.js +5 -3
  145. package/src/actions/scales/preview.js +2 -1
  146. package/src/actions/scales/quantitativeColor.js +2 -4
  147. package/src/actions/selection/actions.js +10 -1
  148. package/src/actions/theme/reconcile.js +37 -9
  149. package/src/core/compositionState.js +26 -7
  150. package/src/core/datasetReferences.js +17 -0
  151. package/src/core/facetAxisPolicy.js +30 -0
  152. package/src/core/immutable.js +1 -0
  153. package/src/core/resourceReferences.js +5 -3
  154. package/src/core/validation.js +4 -0
  155. package/src/core/vocabulary.js +24 -2
  156. package/src/grammar/aggregate.js +1 -0
  157. package/src/grammar/arcs.js +1 -1
  158. package/src/grammar/areaSeries.js +1 -1
  159. package/src/grammar/axisTextPlacement.js +1 -1
  160. package/src/grammar/bars/policy.js +2 -2
  161. package/src/grammar/curveCommands.js +35 -2
  162. package/src/grammar/datasetDependencies.js +97 -0
  163. package/src/grammar/datasetSchema.js +32 -2
  164. package/src/grammar/facetHeaders.js +1 -0
  165. package/src/grammar/facets/dependencies.js +6 -2
  166. package/src/grammar/facets/guides.js +7 -5
  167. package/src/grammar/facets/legendIntent.js +6 -2
  168. package/src/grammar/facets/scales.js +27 -7
  169. package/src/grammar/flatten.js +85 -0
  170. package/src/grammar/histogram.js +13 -1
  171. package/src/grammar/horizon.js +1 -494
  172. package/src/grammar/jitter.js +2 -2
  173. package/src/grammar/legendSpacing.js +45 -0
  174. package/src/grammar/lineMissing.js +90 -0
  175. package/src/grammar/lineSeries.js +1 -376
  176. package/src/grammar/lookup.js +97 -0
  177. package/src/grammar/markFilter.js +4 -2
  178. package/src/grammar/markLabels.js +8 -3
  179. package/src/grammar/numeric.js +1 -1
  180. package/src/grammar/pathCommands.js +1 -1
  181. package/src/grammar/pathSeries.js +1 -109
  182. package/src/grammar/pointPolygon.js +81 -0
  183. package/src/grammar/pointShapes.js +15 -15
  184. package/src/grammar/rects.js +1 -1
  185. package/src/grammar/regression/derive.js +1 -195
  186. package/src/grammar/regression/parameters.js +7 -0
  187. package/src/grammar/scales/appearance.js +1 -1
  188. package/src/grammar/scales/color.js +1 -1
  189. package/src/grammar/scales/colorConsumers.js +1 -63
  190. package/src/grammar/scales/continuous.js +58 -1
  191. package/src/grammar/scales/definition.js +1 -1
  192. package/src/grammar/scales/ordinal.js +5 -2
  193. package/src/grammar/scales/policies.js +3 -3
  194. package/src/grammar/schemas/graphicBounds.js +40 -12
  195. package/src/grammar/schemas/graphicClip.js +23 -0
  196. package/src/grammar/schemas/graphicTree.js +16 -0
  197. package/src/grammar/statisticalReference.js +22 -0
  198. package/src/grammar/text.js +65 -1
  199. package/src/grammar/ticks.js +92 -1
  200. package/src/grammar/timeUnit.js +9 -3
  201. package/src/grammar/transformTopology.js +3 -1
  202. package/src/grammar/transforms.js +24 -0
  203. package/src/inspection.js +1 -495
  204. package/src/inspectionDescriptors.js +1 -1
  205. package/src/layout/canvas.js +1 -1
  206. package/src/layout/facetContentFlow.js +101 -0
  207. package/src/layout/facetHeaderTitles.js +1 -0
  208. package/src/layout/facetTopLane.js +29 -0
  209. package/src/layout/facets.js +1 -1
  210. package/src/layout/guideCollisions.js +9 -3
  211. package/src/layout/legendGroup.js +1 -1
  212. package/src/layout/legendItems.js +26 -10
  213. package/src/layout/legendSpacing.js +66 -0
  214. package/src/layout/plotDrawOrder.js +17 -0
  215. package/src/materialization/bars/histogram.js +8 -2
  216. package/src/materialization/bars/resolve.js +1 -1
  217. package/src/materialization/composition.js +1 -0
  218. package/src/materialization/compositionSnapshot.js +22 -2
  219. package/src/materialization/dataProvenance.js +1 -1
  220. package/src/materialization/facetContentBounds.js +96 -0
  221. package/src/materialization/facetGuides/layout.js +1 -1
  222. package/src/materialization/facetGuides/placement.js +39 -11
  223. package/src/materialization/facetGuides/preparation.js +43 -24
  224. package/src/materialization/facetHeaders.js +1 -1
  225. package/src/materialization/facetTopLane.js +25 -0
  226. package/src/materialization/facets.js +1 -1
  227. package/src/materialization/graphicHierarchy.js +30 -1
  228. package/src/materialization/graphicTranslation.js +2 -0
  229. package/src/materialization/guides/layout.js +1 -1
  230. package/src/materialization/guides/resources.js +1 -1
  231. package/src/materialization/legends.js +1 -1
  232. package/src/materialization/markClip.js +26 -0
  233. package/src/materialization/marks/capabilities.js +3 -2
  234. package/src/materialization/marks/grouping.js +7 -5
  235. package/src/materialization/marks/index.js +4 -2
  236. package/src/materialization/marks/policies.js +4 -0
  237. package/src/materialization/plotDrawOrder.js +44 -0
  238. package/src/materialization/rect.js +11 -3
  239. package/src/materialization/scaleGuideDependencies.js +13 -5
  240. package/src/materialization/selection/items/common.js +1 -1
  241. package/src/materialization/selection/items/path.js +1 -0
  242. package/src/materialization/selection/policies/index.js +2 -1
  243. package/src/materialization/text.js +10 -5
  244. package/src/mcp/cli.js +0 -0
  245. package/src/persistence/validation.js +114 -18
  246. package/src/persistence.js +4 -2
  247. package/src/renderers/canvas/index.js +16 -0
  248. package/src/renderers/canvas/isolated.js +11 -1
  249. package/src/renderers/canvas/native.js +7 -0
  250. package/src/renderers/canvas/scene.js +66 -0
  251. package/src/renderers/canvas/text.js +3 -2
  252. package/src/renderers/svg.js +1 -617
  253. package/src/renderers/svgSerialization.js +643 -0
  254. package/src/renderers/transitionScene.js +67 -0
  255. package/src/scene.js +89 -0
  256. package/src/selectors/axes.js +1 -1
  257. package/src/selectors/datasets.js +1 -0
  258. package/src/selectors/grids.js +9 -0
  259. package/src/selectors/markClip.js +28 -0
  260. package/src/selectors/raincloud.js +7 -0
  261. package/src/version.js +1 -1
  262. package/types/basic.d.ts +7 -0
  263. package/types/index.d.ts +8 -0
  264. package/types/program.d.ts +286 -70
  265. package/types/scene.d.ts +35 -0
@@ -1,3 +1,4 @@
1
+ import { datasetInputReferences, rebindTransformDatasetInputs } from "../../core/datasetReferences.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";
@@ -12,6 +13,15 @@ import {
12
13
  validateRowsAgainstSchema
13
14
  } from "../../grammar/datasetSchema.js";
14
15
 
16
+ export function resolveDatasetTransformInputs(program, transform) {
17
+ const replacements = new Map();
18
+ for (const reference of datasetInputReferences({ transform: [transform] })) {
19
+ const input = resolveDatasetReference(program, reference.id, "Lookup dataset");
20
+ if (input.id !== reference.id) replacements.set(reference.id, input.id);
21
+ }
22
+ return rebindTransformDatasetInputs(transform, replacements);
23
+ }
24
+
15
25
  export const MATERIALIZE_OPTIONS = Object.freeze(["id"]);
16
26
 
17
27
  export function requireDerivedDataset(program, id, type) {
@@ -6,7 +6,7 @@ import { derivedCreator, derivedMaterializer } from "./shared.js";
6
6
 
7
7
  const OPTIONS = Object.freeze([
8
8
  "id", "source", "field", "unit", "as", "temporalUnit", "timeZone",
9
- "weekStartsOn", "weekRule"
9
+ "weekStartsOn", "weekRule", "missing"
10
10
  ]);
11
11
 
12
12
  export const materializeTimeUnitData = /* @__PURE__ */ derivedMaterializer(
@@ -321,8 +321,10 @@ function rebindAxisOwner(program, channel, axisId, {
321
321
  });
322
322
  }
323
323
  const scale = next.resolvedScales[nextScale];
324
+ const previousTicks = next.guideConfigs.axis?.[axisId]?.ticks;
324
325
  const ticks = normalizeTickConfig(
325
- next.guideConfigs.axis?.[axisId]?.ticks,
326
+ previousTitle !== nextTitle && previousTicks?.mode === "values"
327
+ ? { ...previousTicks, inferredValues: true } : previousTicks,
326
328
  scale
327
329
  );
328
330
  if (ticks !== undefined) {
@@ -1,3 +1,4 @@
1
+ import { APPEARANCE_MARK_TYPES } from "../../core/vocabulary.js";
1
2
  import { action } from "../../core/action.js";
2
3
  import { readQuantitativeField } from "../../grammar/scales/index.js";
3
4
  import {
@@ -34,7 +35,7 @@ export const encodeAngle = /* @__PURE__ */ action(
34
35
  const { id: target, dataset: sourceDataset, layer } = resolveTarget(
35
36
  this,
36
37
  args.target,
37
- ["point", "tick"],
38
+ APPEARANCE_MARK_TYPES.encodeAngle,
38
39
  "point or Tick mark"
39
40
  );
40
41
  if (hasValue && !Number.isFinite(args.value)) {
@@ -1,3 +1,5 @@
1
+ import { APPEARANCE_MARK_TYPES } from "../../core/vocabulary.js";
2
+ import { removeEncoding } from "./remove.js";
1
3
  import { canMaterializeBar } from "../../materialization/marks/index.js";
2
4
  import { action } from "../../core/action.js";
3
5
  import { validatePathSeriesAppearance } from "../../grammar/pathSeries.js";
@@ -16,6 +18,7 @@ import {
16
18
  } from "../../selectors/layers.js";
17
19
  import {
18
20
  applyEncodingScale,
21
+ releaseDetachedEncodingScale,
19
22
  applyDetachedScaleRematerialization,
20
23
  clearMarkGraphic,
21
24
  rematerializeEncoding,
@@ -38,7 +41,7 @@ function encodeAppearanceField(program, channel, args, operation) {
38
41
  const { id: target, dataset: sourceDataset, layer } = resolveTarget(
39
42
  program,
40
43
  args.target,
41
- ["point"],
44
+ APPEARANCE_MARK_TYPES[operation],
42
45
  "point mark"
43
46
  );
44
47
  const fieldTypes = channel === "shape" ? ["nominal"] : ["quantitative", "nominal", "ordinal"];
@@ -47,9 +50,7 @@ function encodeAppearanceField(program, channel, args, operation) {
47
50
  if (!fieldTypes.includes(fieldType)) {
48
51
  throw new Error(`${operation} requires a ${fieldTypes.join(" or ")} field.`);
49
52
  }
50
- if (channel === "size" && program.markConfigs[target]?.radius !== undefined) {
51
- throw new Error("encodeSize cannot be combined with a constant radius.");
52
- }
53
+
53
54
  const previous = layer.encoding?.[channel];
54
55
  const requestedScale = resolveReassignmentScaleOptions(
55
56
  previous,
@@ -65,7 +66,12 @@ function encodeAppearanceField(program, channel, args, operation) {
65
66
  allowUnknown: Object.hasOwn(scale, "unknown")
66
67
  });
67
68
 
68
- let next = setEncodingProperties(program, target, channel, {
69
+ const baseline = channel === "size"
70
+ ? program._withoutMaterializationConfig(["marks", target, "radius"])
71
+ : program;
72
+ const prepared = channel === "size" && program.markConfigs[target]?.radius !== undefined
73
+ ? releaseDetachedEncodingScale(baseline, scale.id) : baseline;
74
+ let next = setEncodingProperties(prepared, target, channel, {
69
75
  field: args.field,
70
76
  fieldType,
71
77
  scale: scale.id
@@ -146,9 +152,26 @@ const removePointRadius = /* @__PURE__ */ action(
146
152
  const encodeSize = /* @__PURE__ */ action(
147
153
  {
148
154
  op: "encodeSize",
149
- description: "Encode a field as equal-area point size."
155
+ description: "Assign constant or field-driven equal-area point size."
150
156
  },
151
157
  function (args = {}) {
158
+ validateOptions(args, [...FIELD_OPTIONS, "value"], "encodeSize");
159
+ const hasValue = Object.hasOwn(args, "value");
160
+ if (hasValue === Object.hasOwn(args, "field")) {
161
+ throw new Error("encodeSize requires exactly one of value or field.");
162
+ }
163
+ if (hasValue) {
164
+ if (args.fieldType !== undefined || args.scale !== undefined) {
165
+ throw new Error("Constant size does not accept fieldType or scale.");
166
+ }
167
+ if (!Number.isFinite(args.value) || args.value < 0) {
168
+ throw new RangeError("encodeSize requires a non-negative finite area.");
169
+ }
170
+ const { id: target, layer } = resolveTarget(this, args.target, APPEARANCE_MARK_TYPES.encodeSize, "point mark");
171
+ const next = layer.encoding?.size === undefined ? this
172
+ : removeEncoding.call(this, { target, channel: "size" });
173
+ return next.encodeRadius({ target, value: Math.sqrt(args.value / Math.PI) });
174
+ }
152
175
  return encodeAppearanceField(this, "size", args, "encodeSize");
153
176
  }
154
177
  );
@@ -207,8 +230,8 @@ const encodeOpacity = /* @__PURE__ */ action(
207
230
  const { id: target, dataset: sourceDataset, layer } = resolveTarget(
208
231
  this,
209
232
  args.target,
210
- ["point", "rule", "line", "bar"],
211
- "point, rule, line or bar mark"
233
+ APPEARANCE_MARK_TYPES.encodeOpacity,
234
+ "point, rule, line, bar or rect mark"
212
235
  );
213
236
  if (hasValue) {
214
237
  validateOpacityValue(args.value, "encodeOpacity");
@@ -221,7 +244,7 @@ const encodeOpacity = /* @__PURE__ */ action(
221
244
  .clearOpacityEncoding({ target })
222
245
  ._withoutMaterializationConfig(["marks", target, "opacity"])
223
246
  ._withMarkConfig(target, { ...config, opacity: args.value });
224
- if (layer.mark.type === "line") {
247
+ if (["line", "rect"].includes(layer.mark.type)) {
225
248
  return rematerializeEncoding(next, target, "opacity", undefined, layer);
226
249
  }
227
250
  if (layer.mark.type === "bar") {
@@ -245,13 +268,13 @@ const encodeOpacity = /* @__PURE__ */ action(
245
268
  args.scale ?? {}
246
269
  );
247
270
  const scale = resolveOpacityScaleDefinition(this, requestedScale);
248
- if (layer.mark.type === "line") {
271
+ if (["line", "rect"].includes(layer.mark.type)) {
249
272
  if (Object.hasOwn(scale, "unknown")) {
250
- throw new Error("Line opacity does not support unknown fallback.");
273
+ throw new Error("Line and Rect opacity do not support unknown fallback.");
251
274
  }
252
275
  validatePathSeriesAppearance(dataset.values, {
253
276
  ...layer, encoding: { ...layer.encoding, opacity: { field: args.field, fieldType } }
254
- });
277
+ }, this.semanticSpec.scales);
255
278
  }
256
279
  readScaleField(dataset.values, args.field, fieldType, {
257
280
  allowUnknown: Object.hasOwn(scale, "unknown")
@@ -259,7 +282,9 @@ const encodeOpacity = /* @__PURE__ */ action(
259
282
  const { opacity, ...config } = this.markConfigs[target] ?? {};
260
283
  void opacity;
261
284
  let next = setEncodingProperties(
262
- withoutConstantOpacity(this, target),
285
+ this.markConfigs[target]?.opacity !== undefined || this.markConfigs[target]?.barAppearance?.opacity !== undefined
286
+ ? releaseDetachedEncodingScale(withoutConstantOpacity(this, target), scale.id)
287
+ : withoutConstantOpacity(this, target),
263
288
  target,
264
289
  "opacity",
265
290
  { field: args.field, fieldType, scale: scale.id }
@@ -24,13 +24,14 @@ import {
24
24
  import { buildMaterializationPlan } from "../../materialization/planner.js";
25
25
  import { hasMaterializedLegend } from "../../materialization/legends.js";
26
26
  import { rebindPositionGuides } from "./shared.js";
27
- import { withoutPreviewLayerEncodings } from
27
+ import { withoutPreviewLayerEncodings, withPreviewLayerEncodings } from
28
28
  "../primitives/semanticAction.js";
29
+ import { isIndependentText, normalizeTextOffsetAssignment } from "../../grammar/text.js";
29
30
 
30
31
  export const ENCODING_CHANNEL_ORDER = Object.freeze([
31
32
  "x", "y", "x2", "y2", "theta", "r", "xOffset", "yOffset",
32
33
  "group", "pathOrder", "color", "stroke", "size", "shape",
33
- "opacity", "strokeWidth", "strokeDash", "angle", "text"
34
+ "opacity", "strokeWidth", "strokeDash", "angle", "text", "dx", "dy"
34
35
  ]);
35
36
 
36
37
  const CHANNEL_METHODS = Object.freeze({
@@ -52,7 +53,9 @@ const CHANNEL_METHODS = Object.freeze({
52
53
  strokeWidth: "encodeStrokeWidth",
53
54
  strokeDash: "encodeStrokeDash",
54
55
  angle: "encodeAngle",
55
- text: "encodeText"
56
+ text: "encodeText",
57
+ dx: "encodeDx",
58
+ dy: "encodeDy"
56
59
  });
57
60
 
58
61
  const FORBIDDEN_PAYLOAD_KEYS = Object.freeze(["target", "coordinate", "id"]);
@@ -340,6 +343,14 @@ function applyEncodingDraft(program, target, requests) {
340
343
  target,
341
344
  requests
342
345
  );
346
+ if (isIndependentText(findLayer(draft, target))) {
347
+ const offsets = requests.filter(request => ["dx", "dy"].includes(request.channel));
348
+ if (offsets.length) {
349
+ draft = withPreviewLayerEncodings(draft, { id: target, encodings: Object.fromEntries(
350
+ offsets.map(({ channel, payload }) => [channel, normalizeTextOffsetAssignment(payload, channel)])
351
+ ) });
352
+ }
353
+ }
343
354
  for (const { channel, payload } of requests) {
344
355
  const focusedAction = draft[CHANNEL_METHODS[channel]];
345
356
  draft = invokeWrappedActionImplementation(
@@ -5,13 +5,14 @@ import {
5
5
  resolveTemporalUnit
6
6
  } from "../../../grammar/scales/index.js";
7
7
  import { validateContinuousColorConsumer } from "../../../grammar/scales/colorConsumers.js";
8
- import { validatePathSeriesAppearance } from "../../../grammar/pathSeries.js";
8
+ import { validatePathGroupAppearance } from "../../../materialization/marks/grouping.js";
9
+ import { readArcColorValues } from "../../../grammar/arcs.js";
9
10
  import { validateAggregateFieldValues } from "../../../grammar/aggregate.js";
10
11
  import {
11
12
  resolveQuantitativeColorScaleDefinition
12
13
  } from "../../scales/definitions.js";
13
14
  import {
14
- resolveReassignmentScaleOptions,
15
+ releaseDetachedEncodingScale,
15
16
  resolveTarget
16
17
  } from "../shared.js";
17
18
  import {
@@ -23,7 +24,9 @@ import {
23
24
  } from "../../../materialization/encodings.js";
24
25
  import {
25
26
  assertNoConstantColor,
26
- resolveColorScaleOptions
27
+ withoutConstantColor,
28
+ resolveColorScaleOptions,
29
+ resolveColorReassignmentScaleOptions
27
30
  } from "./policy.js";
28
31
 
29
32
  import { applyTemporalUnit } from "../temporal.js";
@@ -40,14 +43,15 @@ export function encodeContinuousColor(program, args) {
40
43
  const { id: target, dataset: sourceDataset, layer } = resolveTarget(
41
44
  program,
42
45
  args.target,
43
- ["point", "bar", "rect", "text", "area"],
46
+ ["point", "line", "bar", "rect", "text", "area", "arc", "tick"],
44
47
  "continuous color mark"
45
48
  );
46
49
  const dataset = applyRequestedItemMissingPolicy(layer, sourceDataset, args.field);
47
- assertNoConstantColor(program, layer);
50
+ const baseline = withoutConstantColor(program, layer);
51
+ assertNoConstantColor(baseline, layer);
48
52
  const temporalUnit = resolveTemporalUnit(args, args.fieldType, layer.encoding?.color);
49
- const requestedScale = resolveReassignmentScaleOptions(
50
- layer.encoding?.color,
53
+ const requestedScale = resolveColorReassignmentScaleOptions(
54
+ program, layer, "color", args.fieldType,
51
55
  resolveColorScaleOptions(args)
52
56
  );
53
57
  const scale = resolveQuantitativeColorScaleDefinition(
@@ -56,9 +60,15 @@ export function encodeContinuousColor(program, args) {
56
60
  requestedScale
57
61
  );
58
62
  const aggregate = validateContinuousColorConsumer(layer, args, scale, { inferAggregate: true });
59
- if (layer.mark.type === "area") {
60
- validatePathSeriesAppearance(dataset.values, {
63
+ if (["line", "area"].includes(layer.mark.type)) {
64
+ validatePathGroupAppearance(program, {
61
65
  ...layer, encoding: { ...layer.encoding, color: { field: args.field, fieldType: args.fieldType } }
66
+ }, dataset);
67
+ }
68
+ if (layer.mark.type === "arc") {
69
+ readArcColorValues(dataset.values, {
70
+ ...layer, encoding: { ...layer.encoding,
71
+ color: { field: args.field, fieldType: args.fieldType, temporalUnit } }
62
72
  });
63
73
  }
64
74
  if (aggregate !== undefined) validateAggregateFieldValues(dataset.values, args.field, args.fieldType);
@@ -75,7 +85,8 @@ export function encodeContinuousColor(program, args) {
75
85
  } else {
76
86
  readQuantitativeField(dataset.values, args.field);
77
87
  }
78
- const next = applyTemporalUnit(program, target, "color", temporalUnit, layer.encoding?.color)
88
+ const prepared = baseline === program ? baseline : releaseDetachedEncodingScale(baseline, scale.id);
89
+ let next = applyTemporalUnit(prepared, target, "color", temporalUnit, layer.encoding?.color)
79
90
  .editSemantic({
80
91
  property: `layer[${target}].encoding.color.field`,
81
92
  value: args.field
@@ -88,6 +99,11 @@ export function encodeContinuousColor(program, args) {
88
99
  property: `layer[${target}].encoding.color.scale`,
89
100
  value: scale.id
90
101
  });
102
+ if (layer.mark.type === "tick") {
103
+ const config = { ...next.markConfigs[target] };
104
+ delete config.stroke;
105
+ next = next._withMarkConfig(target, config);
106
+ }
91
107
  const encoded = aggregate === undefined
92
108
  ? next
93
109
  : next.editSemantic({
@@ -1,3 +1,7 @@
1
+ import { findRaincloudParent } from "../../../selectors/raincloud.js";
2
+ import { APPEARANCE_MARK_TYPES } from "../../../core/vocabulary.js";
3
+ import { removeEncoding } from "../remove.js";
4
+ import { validateNonEmptyString } from "../../../core/validation.js";
1
5
  import { inferSeriesGroup } from "../shared.js";
2
6
  import { action } from "../../../core/action.js";
3
7
  import {
@@ -12,7 +16,7 @@ import {
12
16
  import { resolveColorScaleDefinition } from "../../scales/definitions.js";
13
17
  import {
14
18
  applyEncodingScale,
15
- resolveReassignmentScaleOptions,
19
+ releaseDetachedEncodingScale,
16
20
  resolveTarget,
17
21
  validateOptions
18
22
  } from "../shared.js";
@@ -26,15 +30,18 @@ import {
26
30
  import { findUpstreamTransform } from
27
31
  "../../../materialization/dataProvenance.js";
28
32
  import { validatePathSeriesAppearance } from "../../../grammar/pathSeries.js";
33
+ import { readCategoricalLineColor, validateCategoricalColorConsumer } from "../../../grammar/scales/colorConsumers.js";
29
34
  import { encodeContinuousColor } from "./continuous.js";
30
35
  import {
31
36
  applyColorLayoutCompanion
32
37
  } from "./layout.js";
33
38
  import {
34
39
  assertNoConstantColor,
40
+ withoutConstantColor,
35
41
  COLOR_ENCODING_OPTIONS,
36
42
  resolveColorLayout,
37
- resolveColorScaleOptions
43
+ resolveColorScaleOptions,
44
+ resolveColorReassignmentScaleOptions
38
45
  } from "./policy.js";
39
46
 
40
47
  import { applyTemporalUnit } from "../temporal.js";
@@ -44,10 +51,32 @@ import { applyRequestedItemMissingPolicy } from
44
51
  const encodeColor = /* @__PURE__ */ action(
45
52
  {
46
53
  op: "encodeColor",
47
- description: "Encode a field as graphical color."
54
+ description: "Assign constant or field-driven graphical color."
48
55
  },
49
56
  function (args = {}) {
50
57
  validateOptions(args, COLOR_ENCODING_OPTIONS, "encodeColor");
58
+ const hasValue = Object.hasOwn(args, "value");
59
+ if (hasValue === Object.hasOwn(args, "field")) {
60
+ throw new Error("encodeColor requires exactly one of value or field.");
61
+ }
62
+ const raincloud = findRaincloudParent(this, args.target);
63
+ if (raincloud !== undefined) {
64
+ const { target, ...color } = args;
65
+ return this.editRaincloudPlot({ target, color });
66
+ }
67
+ if (hasValue) {
68
+ if (COLOR_ENCODING_OPTIONS.some(key => !["target", "value"].includes(key) && args[key] !== undefined)) {
69
+ throw new Error("Constant color accepts only value and target.");
70
+ }
71
+ const value = validateNonEmptyString(args.value, "Color value");
72
+ const { id: target, layer } = resolveTarget(this, args.target,
73
+ APPEARANCE_MARK_TYPES.encodeColor, "color mark");
74
+ const next = layer.encoding?.color === undefined ? this
75
+ : removeEncoding.call(this, { target, channel: "color" });
76
+ const method = { point: "editPointMark", line: "editLineMark", bar: "editBarMark",
77
+ area: "editAreaMark", arc: "editArcMark", rect: "editRectMark", text: "editTextMark" }[layer.mark.type];
78
+ return next[method]({ target, [layer.mark.type === "line" ? "stroke" : "fill"]: value });
79
+ }
51
80
  const requestedFieldType = args.fieldType ?? "nominal";
52
81
  if (!["nominal", "ordinal"].includes(requestedFieldType)) {
53
82
  return encodeContinuousColor(this, {
@@ -63,11 +92,12 @@ const encodeColor = /* @__PURE__ */ action(
63
92
  const { id: target, dataset: sourceDataset, layer } = resolveTarget(
64
93
  this,
65
94
  args.target,
66
- ["point", "line", "bar", "area", "arc", "rect", "text"],
95
+ APPEARANCE_MARK_TYPES.encodeColor,
67
96
  "color mark"
68
97
  );
69
98
  const dataset = applyRequestedItemMissingPolicy(layer, sourceDataset, args.field);
70
- assertNoConstantColor(this, layer);
99
+ const baseline = withoutConstantColor(this, layer);
100
+ assertNoConstantColor(baseline, layer);
71
101
  const densityTransform = dataset.transform?.length === 1 &&
72
102
  dataset.transform[0].type === "density"
73
103
  ? dataset.transform[0]
@@ -86,7 +116,7 @@ const encodeColor = /* @__PURE__ */ action(
86
116
  "Bar color encoding requires a complete histogram encoding or a complete ordinal aggregate encoding."
87
117
  );
88
118
  }
89
- const layout = resolveColorLayout(layer, args.layout, barGrain);
119
+ const layout = resolveColorLayout(layer, args.layout, barGrain, args.field);
90
120
  const createsCenterGroup = layer.mark.type === "area" &&
91
121
  layout === "center" && layer.encoding?.group === undefined;
92
122
  const ordinaryGroup = layer.mark.type === "area" && !createsCenterGroup &&
@@ -104,22 +134,21 @@ const encodeColor = /* @__PURE__ */ action(
104
134
  "Area color encoding must match an existing group encoding."
105
135
  );
106
136
  }
107
- if (layer.mark.type === "line" || ordinaryGroup) {
108
- validatePathSeriesAppearance(dataset.values, {
109
- ...layer, encoding: { ...layer.encoding, color: { field: args.field, fieldType } }
110
- });
111
- }
112
- const requestedScale = resolveReassignmentScaleOptions(
113
- layer.encoding?.color,
137
+ const requestedScale = resolveColorReassignmentScaleOptions(
138
+ this, layer, "color", fieldType,
114
139
  resolveColorScaleOptions(args)
115
140
  );
116
141
  const scale = resolveColorScaleDefinition(this, requestedScale);
117
- if (Object.hasOwn(scale, "unknown") && layer.mark.type !== "point") {
118
- throw new Error(
119
- "Categorical color scale unknown currently requires a row-owned point mark."
120
- );
142
+ const color = { field: args.field, fieldType, scale: scale.id };
143
+ const lineFallback = validateCategoricalColorConsumer(layer, color, scale);
144
+ if (layer.mark.type === "line" || ordinaryGroup) {
145
+ validatePathSeriesAppearance(dataset.values, {
146
+ ...layer, encoding: { ...layer.encoding, color }
147
+ }, [...this.semanticSpec.scales.filter(item => item.id !== scale.id), scale]);
121
148
  }
122
- if (layer.mark.type === "rect") {
149
+ if (lineFallback) {
150
+ readCategoricalLineColor(dataset.values, args.field);
151
+ } else if (layer.mark.type === "rect") {
123
152
  readScaleField(dataset.values, args.field, fieldType, {
124
153
  allowUnknown: true
125
154
  });
@@ -131,9 +160,10 @@ const encodeColor = /* @__PURE__ */ action(
131
160
  readNominalField(dataset.values, args.field);
132
161
  }
133
162
 
163
+ const prepared = baseline === this ? baseline : releaseDetachedEncodingScale(baseline, scale.id);
134
164
  let next = layer.mark.type === "bar" || createsCenterGroup
135
- ? inferSeriesGroup(this, layer, args.field, "color")
136
- : this;
165
+ ? inferSeriesGroup(prepared, layer, args.field, "color")
166
+ : prepared;
137
167
  next = applyTemporalUnit(next, target, "color", undefined, layer.encoding?.color).editSemantic({
138
168
  property: `layer[${target}].encoding.color.field`,
139
169
  value: args.field
@@ -1,11 +1,14 @@
1
1
  import {
2
2
  BAR_GRAINS,
3
- inferBarColorLayout
3
+ inferBarColorLayout,
4
+ resolveBarChannels
4
5
  } from "../../../grammar/bars/policy.js";
5
6
  import { validateColorLayout } from "../../../grammar/seriesLayout.js";
7
+ import { findScaleConsumers } from "../../scales/consumers/common.js";
8
+ import { resolveReassignmentScaleOptions } from "../shared.js";
6
9
 
7
10
  export const COLOR_ENCODING_OPTIONS = Object.freeze([
8
- "field", "target", "fieldType", "scale", "palette", "layout", "aggregate", "temporalUnit"
11
+ "value", "field", "target", "fieldType", "scale", "palette", "layout", "aggregate", "temporalUnit"
9
12
  ]);
10
13
 
11
14
  export function resolveColorScaleOptions(args) {
@@ -19,6 +22,50 @@ export function resolveColorScaleOptions(args) {
19
22
  return { ...options, palette: args.palette };
20
23
  }
21
24
 
25
+ export function resolveColorReassignmentScaleOptions(
26
+ program, layer, channel, fieldType, options = {}
27
+ ) {
28
+ const previous = layer.encoding?.[channel];
29
+ const requested = resolveReassignmentScaleOptions(previous, options);
30
+ if (options.id !== undefined || previous?.scale === undefined) return requested;
31
+ const categorical = type => ["nominal", "ordinal"].includes(type);
32
+ if (categorical(previous.fieldType) === categorical(fieldType)) return requested;
33
+ const otherConsumer = consumer => consumer.layer.id !== layer.id ||
34
+ consumer.channel !== channel;
35
+ if (!findScaleConsumers(program, previous.scale).some(otherConsumer)) return requested;
36
+ // Re-encoding one mark must not change the scale family of its siblings.
37
+ // Explicit scale IDs still opt into normal shared-resource validation.
38
+ const id = `${layer.id}-${channel}-${fieldType}`;
39
+ if (findScaleConsumers(program, id).some(otherConsumer)) {
40
+ throw new Error(`Color reassignment requires scale.id because "${id}" has other consumers.`);
41
+ }
42
+ return { ...options, id };
43
+ }
44
+
45
+ export function withoutConstantColor(program, layer) {
46
+ const previous = program.markConfigs[layer.id];
47
+ const explicit = { point: previous?.fill !== undefined, line: previous?.stroke !== undefined,
48
+ bar: previous?.barAppearance?.fill !== undefined, area: previous?.errorBand?.fill !== undefined,
49
+ arc: previous?.fill !== undefined, rect: previous?.fillExplicit === true,
50
+ text: previous?.fillExplicit === true }[layer.mark.type];
51
+ if (!explicit) return program;
52
+ const config = { ...previous };
53
+ if (layer.mark.type === "bar") {
54
+ config.barAppearance = { ...config.barAppearance };
55
+ delete config.barAppearance.fill;
56
+ } else if (layer.mark.type === "line") {
57
+ delete config.stroke;
58
+ } else {
59
+ if (["point", "arc"].includes(layer.mark.type)) delete config.fill;
60
+ if (["rect", "text"].includes(layer.mark.type)) config.fillExplicit = false;
61
+ if (config.errorBand !== undefined) {
62
+ config.errorBand = { ...config.errorBand };
63
+ delete config.errorBand.fill;
64
+ }
65
+ }
66
+ return program._withMarkConfig(layer.id, config);
67
+ }
68
+
22
69
  export function assertNoConstantColor(program, layer) {
23
70
  if (layer.mark.type === "text" && layer.source !== undefined) {
24
71
  throw new Error("Source-owned text uses inheritColor or constant fill; field color requires row-backed text.");
@@ -47,7 +94,13 @@ export function isRangedArea(layer) {
47
94
  );
48
95
  }
49
96
 
50
- export function resolveColorLayout(layer, requested, barGrain) {
97
+ export function resolveColorLayout(layer, requested, barGrain, field) {
98
+ const category = layer.encoding?.[resolveBarChannels(layer)?.category];
99
+ // Coloring the category does not introduce another partition. An explicit
100
+ // group or a stored/requested layout still owns series placement.
101
+ const categoryColorOnly = barGrain === BAR_GRAINS.aggregate &&
102
+ ["nominal", "ordinal"].includes(category?.fieldType) &&
103
+ category.field === field && layer.encoding?.group === undefined;
51
104
  const existing = layer.encoding?.color === undefined
52
105
  ? undefined
53
106
  : layer.encoding.color.layout ?? (
@@ -62,7 +115,7 @@ export function resolveColorLayout(layer, requested, barGrain) {
62
115
  layer.mark.type === "bar"
63
116
  ? barGrain === BAR_GRAINS.histogram
64
117
  ? "stack"
65
- : [BAR_GRAINS.ranged, BAR_GRAINS.centered].includes(barGrain) ? "overlay" : "group"
118
+ : [BAR_GRAINS.ranged, BAR_GRAINS.centered].includes(barGrain) || categoryColorOnly ? "overlay" : "group"
66
119
  : layer.mark.type === "area"
67
120
  ? "overlay"
68
121
  : undefined
@@ -1,4 +1,6 @@
1
1
  import { closedAction } from "../../core/action.js";
2
+ import { validateMarkClipCoordinate } from "../../selectors/markClip.js";
3
+ import { validateLineMissingScope } from "../../grammar/lineMissing.js";
2
4
 
3
5
  import {
4
6
  normalizeParallelDimensions,
@@ -31,6 +33,7 @@ export const encodeParallelCoordinates = /* @__PURE__ */ closedAction(
31
33
  ["line"],
32
34
  "line mark"
33
35
  );
36
+ validateLineMissingScope({ ...layer, encoding: { ...layer.encoding, parallel: {} } });
34
37
  if (
35
38
  ["x", "y", "theta", "radius"].some(
36
39
  channel => layer.encoding?.[channel] !== undefined
@@ -45,6 +48,7 @@ export const encodeParallelCoordinates = /* @__PURE__ */ closedAction(
45
48
  layer,
46
49
  operation: "encodeParallelCoordinates"
47
50
  });
51
+ validateMarkClipCoordinate(this, layer, this.markConfigs[target]?.clip, "parallel");
48
52
  const dimensions = normalizeParallelDimensions(
49
53
  dataset.values,
50
54
  args.dimensions,
@@ -50,6 +50,9 @@ function resolveBarChannelPolicy({
50
50
  const weight = args.weight === undefined
51
51
  ? undefined
52
52
  : normalizeStatisticalWeight(args.weight, "Histogram weight");
53
+ if (weight !== undefined && opposite?.aggregate !== undefined && opposite.aggregate !== "count") {
54
+ throw new Error("Binned bar weights require count aggregation.");
55
+ }
53
56
  return { bin, aggregate, stack, weight };
54
57
  } else if (
55
58
  fieldType === "quantitative" &&
@@ -59,14 +62,15 @@ function resolveBarChannelPolicy({
59
62
  if (args.bin !== undefined) {
60
63
  throw new Error("Histogram bar y encoding does not support bin.");
61
64
  }
62
- if (field !== xEncoding.field) {
63
- throw new Error("Bar y field must match the binned x field.");
65
+ aggregate = validateAggregate(args.aggregate ?? "count");
66
+ validateAggregateFieldType(aggregate, fieldType);
67
+ if (aggregate === "count" && field !== xEncoding.field) {
68
+ throw new Error("Count bar y field must match the binned x field.");
64
69
  }
65
- aggregate = args.aggregate ?? "count";
66
- stack = Object.hasOwn(args, "stack") ? args.stack : "zero";
67
- if (aggregate !== "count") {
68
- throw new Error('Histogram bar y aggregate must be "count".');
70
+ if (aggregate !== "count" && xEncoding.weight !== undefined) {
71
+ throw new Error("Binned bar weights require count aggregation.");
69
72
  }
73
+ stack = Object.hasOwn(args, "stack") ? args.stack : aggregate === "count" ? "zero" : null;
70
74
  stack = validateStack(stack, "Histogram bar y encoding");
71
75
  if (stack === "center") {
72
76
  throw new Error("Centered bars are not supported.");