ggaction 0.0.23 → 0.0.26

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 (228) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +2 -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/knowledge/task-resolver.js +1 -1
  7. package/package.json +1 -1
  8. package/src/accessibility.js +13 -0
  9. package/src/actions/canvas/actions.js +2 -1
  10. package/src/actions/canvas/fitting.js +68 -13
  11. package/src/actions/charts/beeswarm.js +2 -1
  12. package/src/actions/charts/ecdf.js +2 -1
  13. package/src/actions/charts/endpoints.js +25 -16
  14. package/src/actions/charts/heatmap.js +6 -4
  15. package/src/actions/charts/missing.js +17 -0
  16. package/src/actions/charts/radar.js +70 -33
  17. package/src/actions/charts/raincloud.js +18 -4
  18. package/src/actions/charts/rug-strip.js +2 -2
  19. package/src/actions/charts/shared.js +17 -0
  20. package/src/actions/composition/actions.js +24 -7
  21. package/src/actions/composition/adjacent.js +90 -0
  22. package/src/actions/composition/index.js +11 -1
  23. package/src/actions/composition/layer.js +42 -0
  24. package/src/actions/composition/layerPlan.js +176 -0
  25. package/src/actions/composition/targets.js +105 -0
  26. package/src/actions/data/basicBin2d.js +1 -1
  27. package/src/actions/data/bin2d.js +2 -2
  28. package/src/actions/data/derived.js +3 -0
  29. package/src/actions/data/filter.js +1 -1
  30. package/src/actions/data/raincloudFilter.js +3 -1
  31. package/src/actions/encodings/appearance.js +26 -7
  32. package/src/actions/encodings/channels.js +1 -0
  33. package/src/actions/encodings/color/continuous.js +37 -34
  34. package/src/actions/encodings/color/index.js +57 -43
  35. package/src/actions/encodings/color/policy.js +15 -0
  36. package/src/actions/encodings/density/resolve.js +3 -2
  37. package/src/actions/encodings/density.js +2 -2
  38. package/src/actions/encodings/offset.js +2 -0
  39. package/src/actions/encodings/pathOrder.js +2 -0
  40. package/src/actions/encodings/position/areaAlignment.js +42 -0
  41. package/src/actions/encodings/position/index.js +37 -3
  42. package/src/actions/encodings/position/policies/line.js +1 -1
  43. package/src/actions/encodings/ranged.js +4 -2
  44. package/src/actions/encodings/remove.js +13 -10
  45. package/src/actions/encodings/ruleAppearance.js +14 -2
  46. package/src/actions/facets/actions.js +1 -1
  47. package/src/actions/guides/axes/axes.js +2 -1
  48. package/src/actions/guides/axes/components.js +5 -0
  49. package/src/actions/guides/axes/edit.js +1 -188
  50. package/src/actions/guides/axes/labels.js +2 -537
  51. package/src/actions/guides/axes/parallel/lifecycle.js +11 -4
  52. package/src/actions/guides/axes/parallel/policy.js +5 -2
  53. package/src/actions/guides/axes/parallel/resolve.js +3 -1
  54. package/src/actions/guides/axes/parallel.js +9 -4
  55. package/src/actions/guides/axes/tickGroups.js +16 -1
  56. package/src/actions/guides/axes/ticks.js +33 -24
  57. package/src/actions/guides/axes/titles.js +1 -389
  58. package/src/actions/guides/facadeLegend.js +3 -2
  59. package/src/actions/guides/grids/grid.js +3 -2
  60. package/src/actions/guides/grids/resolve.js +2 -1
  61. package/src/actions/guides/guides.js +3 -2
  62. package/src/actions/guides/legends/automatic.js +28 -0
  63. package/src/actions/guides/legends/blocks.js +15 -21
  64. package/src/actions/guides/legends/categorical/actions.js +1 -1
  65. package/src/actions/guides/legends/categorical/components.js +4 -3
  66. package/src/actions/guides/legends/categorical/layout.js +5 -2
  67. package/src/actions/guides/legends/categorical/options.js +9 -14
  68. package/src/actions/guides/legends/categorical/recipes.js +3 -0
  69. package/src/actions/guides/legends/categorical/resolve.js +7 -5
  70. package/src/actions/guides/legends/colorSize.js +51 -14
  71. package/src/actions/guides/legends/continuous/common.js +1 -1
  72. package/src/actions/guides/legends/continuous/gradient.js +1 -1
  73. package/src/actions/guides/legends/continuous/opacity.js +2 -0
  74. package/src/actions/guides/legends/edit.js +1 -1
  75. package/src/actions/guides/legends/focused.js +2 -2
  76. package/src/actions/guides/legends/group.js +1 -1
  77. package/src/actions/guides/legends/lane.js +1 -1
  78. package/src/actions/guides/legends/lifecycle.js +2 -2
  79. package/src/actions/guides/legends/remove.js +18 -4
  80. package/src/actions/guides/legends/size.js +1 -1
  81. package/src/actions/guides/legends/transition.js +15 -6
  82. package/src/actions/guides/polar/axes/facade.js +4 -2
  83. package/src/actions/guides/polar/axes/labels.js +6 -1
  84. package/src/actions/guides/polar/axes/shared.js +4 -4
  85. package/src/actions/guides/polar/axes/titles.js +13 -2
  86. package/src/actions/guides/polar/grids.js +6 -3
  87. package/src/actions/guides/tickValues.js +7 -1
  88. package/src/actions/index.js +2 -0
  89. package/src/actions/marks/area/actions.js +9 -2
  90. package/src/actions/marks/area/materialize.js +1 -1
  91. package/src/actions/marks/bar/materialize.js +4 -4
  92. package/src/actions/marks/lifecycle.js +14 -0
  93. package/src/actions/marks/line/actions.js +3 -0
  94. package/src/actions/marks/line/materialize.js +4 -2
  95. package/src/actions/marks/point/edit.js +2 -0
  96. package/src/actions/marks/point/jitter.js +2 -0
  97. package/src/actions/marks/point/materialize.js +5 -4
  98. package/src/actions/marks/point/packing.js +2 -0
  99. package/src/actions/marks/references.js +5 -3
  100. package/src/actions/marks/remove.js +16 -0
  101. package/src/actions/marks/shared.js +2 -1
  102. package/src/actions/marks/text/actions.js +5 -2
  103. package/src/actions/overlays/index.js +168 -0
  104. package/src/actions/overlays/snapshot.js +45 -0
  105. package/src/actions/primitives/editGraphics.js +21 -27
  106. package/src/actions/primitives/semanticAction.js +4 -5
  107. package/src/actions/primitives/semanticValidation/guide.js +1 -0
  108. package/src/actions/primitives/semanticValidation/layer.js +7 -0
  109. package/src/actions/scales/consumers/common.js +10 -0
  110. package/src/actions/scales/consumers/families.js +1 -1
  111. package/src/actions/scales/preview.js +1 -1
  112. package/src/actions/selection/actions.js +63 -11
  113. package/src/actions/titles/actions.js +3 -14
  114. package/src/actions/titles/resolve.js +23 -10
  115. package/src/core/action.js +41 -25
  116. package/src/core/compositionScales.js +12 -0
  117. package/src/core/compositionState.js +20 -3
  118. package/src/core/facetGap.js +44 -0
  119. package/src/core/immutable.js +11 -6
  120. package/src/core/programState.js +16 -0
  121. package/src/core/resourceReferences.js +19 -3
  122. package/src/core/validation.js +7 -0
  123. package/src/core/vocabulary.js +8 -2
  124. package/src/grammar/actionTargets.js +24 -0
  125. package/src/grammar/axisTextPlacement.js +1 -1
  126. package/src/grammar/bin2d.js +41 -4
  127. package/src/grammar/colorSizeLegend.js +56 -5
  128. package/src/grammar/displayLabels.js +10 -0
  129. package/src/grammar/facetHeaders.js +1 -1
  130. package/src/grammar/facets/dependencies.js +10 -3
  131. package/src/grammar/facets/guides.js +2 -1
  132. package/src/grammar/facets/legendIntent.js +7 -5
  133. package/src/grammar/legendBlocks.js +1 -1
  134. package/src/grammar/legendCombinations.js +1 -1
  135. package/src/grammar/legendGroups.js +1 -1
  136. package/src/grammar/legendItemsOffset.js +12 -0
  137. package/src/grammar/legendOverflow.js +25 -0
  138. package/src/grammar/lineSeries.js +1 -1
  139. package/src/grammar/markSelection.js +23 -0
  140. package/src/grammar/overlay.js +51 -0
  141. package/src/grammar/parallelCoordinates.js +1 -208
  142. package/src/grammar/pathCommands.js +8 -2
  143. package/src/grammar/pathSeries.js +1 -1
  144. package/src/grammar/plotComposition.js +152 -0
  145. package/src/grammar/pointShapes.js +18 -0
  146. package/src/grammar/scales/transformed.js +1 -1
  147. package/src/grammar/schemas/concreteGraphic.js +8 -1
  148. package/src/grammar/schemas/graphicBounds.js +8 -1
  149. package/src/grammar/schemas/graphicTree.js +18 -2
  150. package/src/grammar/schemas/semanticPath.js +3 -0
  151. package/src/grammar/strokeStyle.js +14 -8
  152. package/src/grammar/text.js +72 -0
  153. package/src/inspection.js +1 -1
  154. package/src/inspectionDescriptors.js +1 -1
  155. package/src/layout/composition.js +32 -10
  156. package/src/layout/compositionLegend.js +91 -0
  157. package/src/layout/facetContentFlow.js +15 -7
  158. package/src/layout/facetHeaderTitles.js +1 -1
  159. package/src/layout/facets.js +1 -1
  160. package/src/layout/labels.js +55 -27
  161. package/src/layout/legend.js +3 -2
  162. package/src/layout/legendGroup.js +1 -1
  163. package/src/layout/legendItems.js +7 -6
  164. package/src/layout/legendLane.js +1 -1
  165. package/src/layout/legendText.js +1 -1
  166. package/src/layout/title.js +1 -1
  167. package/src/materialization/adjacentSource.js +27 -0
  168. package/src/materialization/composition.js +165 -52
  169. package/src/materialization/compositionLegends.js +120 -0
  170. package/src/materialization/compositionScales.js +125 -0
  171. package/src/materialization/compositionTitle.js +96 -0
  172. package/src/materialization/facetGuides/edit.js +22 -2
  173. package/src/materialization/facetGuides/layout.js +1 -1
  174. package/src/materialization/facetGuides/owner.js +4 -3
  175. package/src/materialization/facetGuides/ownerLayout.js +12 -6
  176. package/src/materialization/facetGuides/placement.js +19 -22
  177. package/src/materialization/facetGuides/preparation.js +17 -5
  178. package/src/materialization/facetHeaders.js +5 -1
  179. package/src/materialization/facets.js +1 -1
  180. package/src/materialization/guides/layout.js +1 -1
  181. package/src/materialization/guides/resources.js +1 -1
  182. package/src/materialization/guides/visibility.js +105 -0
  183. package/src/materialization/legendClip.js +1 -1
  184. package/src/materialization/legendGroups.js +1 -1
  185. package/src/materialization/legendItemsOffset.js +68 -0
  186. package/src/materialization/legendSuppression.js +22 -0
  187. package/src/materialization/legends.js +3 -2
  188. package/src/materialization/marks/capabilities.js +4 -0
  189. package/src/materialization/marks/index.js +5 -1
  190. package/src/materialization/marks/pathOrder.js +1 -1
  191. package/src/materialization/marks/policies.js +2 -1
  192. package/src/materialization/overlayItems.js +154 -0
  193. package/src/materialization/planner.js +19 -4
  194. package/src/materialization/scales/policies/binnedPosition.js +43 -5
  195. package/src/materialization/scales/policies/series.js +12 -4
  196. package/src/materialization/scales/resolve.js +2 -0
  197. package/src/materialization/selection/items/bar.js +21 -3
  198. package/src/materialization/selection/items/common.js +18 -5
  199. package/src/materialization/selection/items/overlay.js +15 -0
  200. package/src/materialization/selection/items/path.js +1 -0
  201. package/src/materialization/selection/items/point.js +2 -3
  202. package/src/materialization/selection/items/tick.js +3 -12
  203. package/src/materialization/selection/path.js +2 -2
  204. package/src/materialization/selection/policies/bar.js +2 -0
  205. package/src/materialization/selection/policies/index.js +35 -1
  206. package/src/materialization/selection/policies/point.js +2 -0
  207. package/src/materialization/selection/policies/text.js +17 -0
  208. package/src/materialization/selection/rows.js +103 -0
  209. package/src/materialization/selection/state.js +19 -3
  210. package/src/materialization/selection/styles.js +8 -0
  211. package/src/materialization/text.js +6 -489
  212. package/src/materialization/textGeometry.js +503 -0
  213. package/src/materialization/textLines.js +20 -0
  214. package/src/persistence/validation.js +116 -25
  215. package/src/renderers/canvas/index.js +12 -7
  216. package/src/renderers/canvas/text.js +5 -5
  217. package/src/renderers/nativeBackend.js +25 -0
  218. package/src/renderers/pdf.js +3 -3
  219. package/src/renderers/png.js +4 -3
  220. package/src/renderers/svgSerialization.js +1 -643
  221. package/src/scene.js +5 -3
  222. package/src/selectors/plotComposition.js +115 -0
  223. package/src/selectors/selectionTargets.js +19 -0
  224. package/src/version.js +1 -1
  225. package/types/basic.d.ts +4 -2
  226. package/types/index.d.ts +14 -0
  227. package/types/inspection.d.ts +34 -0
  228. package/types/program.d.ts +572 -372
@@ -26,7 +26,8 @@ import {
26
26
  assertNoConstantColor,
27
27
  withoutConstantColor,
28
28
  resolveColorScaleOptions,
29
- resolveColorReassignmentScaleOptions
29
+ resolveColorReassignmentScaleOptions,
30
+ withColorLegendReassignment
30
31
  } from "./policy.js";
31
32
 
32
33
  import { applyTemporalUnit } from "../temporal.js";
@@ -85,38 +86,40 @@ export function encodeContinuousColor(program, args) {
85
86
  } else {
86
87
  readQuantitativeField(dataset.values, args.field);
87
88
  }
88
- const prepared = baseline === program ? baseline : releaseDetachedEncodingScale(baseline, scale.id);
89
- let next = applyTemporalUnit(prepared, target, "color", temporalUnit, layer.encoding?.color)
90
- .editSemantic({
91
- property: `layer[${target}].encoding.color.field`,
92
- value: args.field
93
- })
94
- .editSemantic({
95
- property: `layer[${target}].encoding.color.fieldType`,
96
- value: args.fieldType
97
- })
98
- .editSemantic({
99
- property: `layer[${target}].encoding.color.scale`,
100
- value: scale.id
101
- });
102
- if (layer.mark.type === "tick") {
103
- const config = { ...next.markConfigs[target] };
104
- delete config.stroke;
105
- next = next._withMarkConfig(target, config);
106
- }
107
- const encoded = aggregate === undefined
108
- ? next
109
- : next.editSemantic({
110
- property: `layer[${target}].encoding.color.aggregate`,
111
- value: aggregate
89
+ return withColorLegendReassignment(baseline, layer, scale, candidate => {
90
+ const prepared = baseline === program ? candidate : releaseDetachedEncodingScale(candidate, scale.id);
91
+ let next = applyTemporalUnit(prepared, target, "color", temporalUnit, layer.encoding?.color)
92
+ .editSemantic({
93
+ property: `layer[${target}].encoding.color.field`,
94
+ value: args.field
95
+ })
96
+ .editSemantic({
97
+ property: `layer[${target}].encoding.color.fieldType`,
98
+ value: args.fieldType
99
+ })
100
+ .editSemantic({
101
+ property: `layer[${target}].encoding.color.scale`,
102
+ value: scale.id
112
103
  });
113
- const scaled = encoded.setQuantitativeColorScale(scale);
114
- return applyDetachedScaleRematerialization(applyMaterializationPlan(
115
- scaled,
116
- planEncodingRematerialization(scaled, {
117
- target,
118
- channel: "color",
119
- scale: scale.id
120
- })
121
- ), [layer]);
104
+ if (layer.mark.type === "tick") {
105
+ const config = { ...next.markConfigs[target] };
106
+ delete config.stroke;
107
+ next = next._withMarkConfig(target, config);
108
+ }
109
+ const encoded = aggregate === undefined
110
+ ? next
111
+ : next.editSemantic({
112
+ property: `layer[${target}].encoding.color.aggregate`,
113
+ value: aggregate
114
+ });
115
+ const scaled = encoded.setQuantitativeColorScale(scale);
116
+ return applyDetachedScaleRematerialization(applyMaterializationPlan(
117
+ scaled,
118
+ planEncodingRematerialization(scaled, {
119
+ target,
120
+ channel: "color",
121
+ scale: scale.id
122
+ })
123
+ ), [layer]);
124
+ });
122
125
  }
@@ -1,9 +1,11 @@
1
+ import { withGuideLayoutValidation } from "../../../materialization/guides/layout.js";
2
+ import { findLayer } from "../../../selectors/layers.js";
1
3
  import { findRaincloudParent } from "../../../selectors/raincloud.js";
2
4
  import { APPEARANCE_MARK_TYPES } from "../../../core/vocabulary.js";
3
5
  import { removeEncoding } from "../remove.js";
4
6
  import { validateNonEmptyString } from "../../../core/validation.js";
5
7
  import { inferSeriesGroup } from "../shared.js";
6
- import { action } from "../../../core/action.js";
8
+ import { action, invokeWrappedActionImplementation } from "../../../core/action.js";
7
9
  import {
8
10
  readNominalField,
9
11
  readScaleField,
@@ -41,7 +43,8 @@ import {
41
43
  COLOR_ENCODING_OPTIONS,
42
44
  resolveColorLayout,
43
45
  resolveColorScaleOptions,
44
- resolveColorReassignmentScaleOptions
46
+ resolveColorReassignmentScaleOptions,
47
+ withColorLegendReassignment
45
48
  } from "./policy.js";
46
49
 
47
50
  import { applyTemporalUnit } from "../temporal.js";
@@ -53,7 +56,7 @@ const encodeColor = /* @__PURE__ */ action(
53
56
  op: "encodeColor",
54
57
  description: "Assign constant or field-driven graphical color."
55
58
  },
56
- function (args = {}) {
59
+ withGuideLayoutValidation(function (args = {}) {
57
60
  validateOptions(args, COLOR_ENCODING_OPTIONS, "encodeColor");
58
61
  const hasValue = Object.hasOwn(args, "value");
59
62
  if (hasValue === Object.hasOwn(args, "field")) {
@@ -64,6 +67,12 @@ const encodeColor = /* @__PURE__ */ action(
64
67
  const { target, ...color } = args;
65
68
  return this.editRaincloudPlot({ target, color });
66
69
  }
70
+ const overlayTarget = findLayer(this, args.target ?? this.context.currentMark);
71
+ if (overlayTarget?.derivedBindings?.overlay?.inheritColor) {
72
+ const next = this.editSemantic({ property: `layer[${overlayTarget.id}].derivedBindings.overlay`,
73
+ value: { ...overlayTarget.derivedBindings.overlay, inheritColor: false } });
74
+ return invokeWrappedActionImplementation(encodeColor, next, args);
75
+ }
67
76
  if (hasValue) {
68
77
  if (COLOR_ENCODING_OPTIONS.some(key => !["target", "value"].includes(key) && args[key] !== undefined)) {
69
78
  throw new Error("Constant color accepts only value and target.");
@@ -160,50 +169,55 @@ const encodeColor = /* @__PURE__ */ action(
160
169
  readNominalField(dataset.values, args.field);
161
170
  }
162
171
 
163
- const prepared = baseline === this ? baseline : releaseDetachedEncodingScale(baseline, scale.id);
164
- let next = layer.mark.type === "bar" || createsCenterGroup
165
- ? inferSeriesGroup(prepared, layer, args.field, "color")
166
- : prepared;
167
- next = applyTemporalUnit(next, target, "color", undefined, layer.encoding?.color).editSemantic({
168
- property: `layer[${target}].encoding.color.field`,
169
- value: args.field
170
- })
171
- .editSemantic({
172
- property: `layer[${target}].encoding.color.fieldType`,
173
- value: fieldType
172
+ return withColorLegendReassignment(baseline, layer, scale, candidate => {
173
+ const prepared = baseline === this ? candidate : releaseDetachedEncodingScale(candidate, scale.id);
174
+ let next = layer.mark.type === "bar" || createsCenterGroup
175
+ ? inferSeriesGroup(prepared, layer, args.field, "color")
176
+ : prepared;
177
+ next = applyTemporalUnit(next, target, "color", undefined, layer.encoding?.color).editSemantic({
178
+ property: `layer[${target}].encoding.color.field`,
179
+ value: args.field
174
180
  })
175
- .editSemantic({
176
- property: `layer[${target}].encoding.color.scale`,
177
- value: scale.id
181
+ .editSemantic({
182
+ property: `layer[${target}].encoding.color.fieldType`,
183
+ value: fieldType
184
+ })
185
+ .editSemantic({
186
+ property: `layer[${target}].encoding.color.scale`,
187
+ value: scale.id
188
+ });
189
+ if (layout !== undefined && (layer.mark.type === "arc" || horizonTransform !== undefined)) {
190
+ next = next.editSemantic({
191
+ property: `layer[${target}].encoding.color.layout`,
192
+ value: layout
193
+ });
194
+ }
195
+ if (layer.encoding?.color?.aggregate !== undefined) {
196
+ next = next.editSemantic({ property: `layer[${target}].encoding.color.aggregate`, remove: true });
197
+ }
198
+ next = applyEncodingScale(next, scale, requestedScale, {
199
+ reassignment: layer.encoding?.color?.scale === scale.id,
200
+ allowTypeChange: true
178
201
  });
179
- if (layout !== undefined && (layer.mark.type === "arc" || horizonTransform !== undefined)) {
180
- next = next.editSemantic({
181
- property: `layer[${target}].encoding.color.layout`,
182
- value: layout
202
+ next = applyColorLayoutCompanion(next, {
203
+ target,
204
+ layer,
205
+ layout,
206
+ scale,
207
+ field: args.field,
208
+ horizon: horizonTransform !== undefined
183
209
  });
184
- }
185
- next = applyEncodingScale(next, scale, requestedScale, {
186
- reassignment: layer.encoding?.color?.scale === scale.id,
187
- allowTypeChange: true
188
- });
189
- next = applyColorLayoutCompanion(next, {
190
- target,
191
- layer,
192
- layout,
193
- scale,
194
- field: args.field,
195
- horizon: horizonTransform !== undefined
196
- });
197
210
 
198
- return applyDetachedScaleRematerialization(applyMaterializationPlan(
199
- next,
200
- planEncodingRematerialization(next, {
201
- target,
202
- channel: "color",
203
- scale: scale.id
204
- })
205
- ), [layer]);
206
- }
211
+ return applyDetachedScaleRematerialization(applyMaterializationPlan(
212
+ next,
213
+ planEncodingRematerialization(next, {
214
+ target,
215
+ channel: "color",
216
+ scale: scale.id
217
+ })
218
+ ), [layer]);
219
+ });
220
+ })
207
221
  );
208
222
 
209
223
  export function registerColorEncodingAction(ProgramClass) {
@@ -1,3 +1,5 @@
1
+ import { findSemanticScale } from "../../../selectors/scales.js";
2
+ import { planColorLegendTransitions, applyColorLegendTransition } from "../../guides/legends/transition.js";
1
3
  import {
2
4
  BAR_GRAINS,
3
5
  inferBarColorLayout,
@@ -146,3 +148,16 @@ export function resolveColorLayout(layer, requested, barGrain, field) {
146
148
  }
147
149
  return layout;
148
150
  }
151
+
152
+ // A new scale identity can also change the existing target's legend family.
153
+ // Keep the same transition validation/style policy used by in-place scale edits.
154
+ export function withColorLegendReassignment(program, layer, scale, apply) {
155
+ const previous = findSemanticScale(program, layer.encoding?.color?.scale);
156
+ if (previous === undefined || previous.id === scale.id) return apply(program);
157
+ const plans = planColorLegendTransitions(program, previous, scale.type, { channel: "color", target: layer.id });
158
+ let next = program;
159
+ for (const plan of plans) next = next.removeLegend({ target: layer.id, channels: ["color"] });
160
+ next = apply(next);
161
+ for (const plan of plans) next = applyColorLegendTransition(next, plan);
162
+ return next;
163
+ }
@@ -3,11 +3,12 @@ import { isPlainObject } from "../../../core/immutable.js";
3
3
  import { findSemanticScale } from "../../../selectors/scales.js";
4
4
 
5
5
 
6
- export function resolveDensityScaleOptions(value, defaults, label) {
6
+ export function resolveDensityScaleOptions(value, defaults, label, program) {
7
7
  if (value !== undefined && !isPlainObject(value)) {
8
8
  throw new TypeError(`${label} must be a plain object.`);
9
9
  }
10
- const scale = { ...defaults, ...(value ?? {}) };
10
+ const existing = program && value?.id ? findSemanticScale(program, value.id) : undefined;
11
+ const scale = { ...(existing ?? defaults), ...(value ?? {}) };
11
12
  if (value?.type === "log" && value.zero === undefined) delete scale.zero;
12
13
  return scale;
13
14
  }
@@ -156,7 +156,7 @@ const encodeDensity = /* @__PURE__ */ action(
156
156
  const valueScale = resolveDensityScaleOptions(
157
157
  args.valueScale,
158
158
  { nice: false, zero: false },
159
- "Density valueScale"
159
+ "Density valueScale", this
160
160
  );
161
161
  const placement = args.placement === undefined
162
162
  ? undefined
@@ -174,7 +174,7 @@ const encodeDensity = /* @__PURE__ */ action(
174
174
  ? resolveDensityScaleOptions(
175
175
  args.densityScale,
176
176
  { nice: true, zero: true },
177
- "Density densityScale"
177
+ "Density densityScale", this
178
178
  )
179
179
  : undefined;
180
180
  const placementScale = placement === undefined
@@ -1,3 +1,4 @@
1
+ import { assertIndependentOverlayGeometry } from "../../grammar/overlay.js";
1
2
  import { inferSeriesGroup } from "./shared.js";
2
3
  import { findSemanticScale } from "../../selectors/scales.js";
3
4
  import { action } from "../../core/action.js";
@@ -41,6 +42,7 @@ function createOffsetEncoding(channel) {
41
42
  ["bar", "point", "rule"],
42
43
  "offset-compatible mark"
43
44
  );
45
+ assertIndependentOverlayGeometry(layer, operation);
44
46
  const dataset = applyRequestedItemMissingPolicy(layer, sourceDataset, args.field);
45
47
 
46
48
  if (layer.mark.type === "bar") {
@@ -1,3 +1,4 @@
1
+ import { assertIndependentOverlayGeometry } from "../../grammar/overlay.js";
1
2
  import { closedAction } from "../../core/action.js";
2
3
  import { validateUserId } from "../../core/identifiers.js";
3
4
  import { validateNonEmptyString } from "../../core/validation.js";
@@ -86,6 +87,7 @@ const encodePathOrder = /* @__PURE__ */ closedAction(
86
87
  }
87
88
  const order = validatePathOrderDirection(args.order ?? "ascending");
88
89
  const layer = resolvePathOrderLayer(this, args.target);
90
+ assertIndependentOverlayGeometry(layer, "encodePathOrder");
89
91
  const dataset = assertPathOrderCompatible(this, layer, "encodePathOrder");
90
92
  readQuantitativeField(dataset.values, field);
91
93
 
@@ -0,0 +1,42 @@
1
+ import { resolveTarget } from '../shared.js';
2
+ import { findDataset, requireDataset } from '../../../selectors/datasets.js';
3
+
4
+ // Completed-key data is tied to one independent coordinate. Its generated
5
+ // rows are not observations and must never be reused as a different measure.
6
+ export function rebindAlignedAreaPosition(program, channel, args) {
7
+ if (!['x', 'y'].includes(channel) || args.field === undefined ||
8
+ !program.semanticSpec.layers.some(layer => layer.mark.type === 'area')) return program;
9
+ const { layer } = resolveTarget(program, args.target, ['point', 'line', 'bar', 'area', 'rule', 'rect', 'text', 'tick']);
10
+ if (layer.mark.type !== 'area' || layer.encoding?.[`${channel}2`] ||
11
+ !layer.encoding?.[channel === 'x' ? 'y2' : 'x2']) return program;
12
+ const dataset = findDataset(program, layer.data);
13
+ const complete = dataset?.transform?.length === 1 && dataset.transform[0].type === 'complete' ? dataset.transform[0] : undefined;
14
+ if (!complete || complete.key !== layer.encoding[channel]?.field || args.field === complete.key) return program;
15
+ if (!layer.layout?.mode || layer.layout.mode === 'overlay') {
16
+ return program.rebindLayerData({ id: layer.id, data: dataset.source });
17
+ }
18
+ const measureChannel = channel === 'x' ? 'y' : 'x';
19
+ const measures = [layer.encoding[measureChannel]?.field, layer.encoding[`${measureChannel}2`]?.field].filter(Boolean);
20
+ if (measures.includes(args.field)) {
21
+ throw new Error('A stacked Area cannot use its measurement as the completed position key: generated rows need separate key and measurement fields. Choose another position field or Overlay layout.');
22
+ }
23
+ if (complete.groupBy.includes(args.field)) {
24
+ throw new Error('A stacked Area needs at least two positions within each series; its grouping field cannot also be the position key.');
25
+ }
26
+ const source = requireDataset(program, dataset.source);
27
+ const unique = new Set();
28
+ for (const row of source.values) {
29
+ const key = JSON.stringify([...complete.groupBy, args.field].map(field => row[field]));
30
+ if (unique.has(key)) throw new Error('The new Area position has duplicate rows within a series. Reaggregate the original observations at that grain before stacking, or use Overlay layout.');
31
+ unique.add(key);
32
+ }
33
+ const stem = `${layer.id}AlignedPosition`, ids = new Set(program.semanticSpec.datasets.map(data => data.id));
34
+ let id = stem;
35
+ for (let index = 2; ids.has(id); index++) id = `${stem}${index}`;
36
+ const fill = Object.fromEntries(Object.entries(complete.fill).filter(([field]) => field !== args.field));
37
+ let next = program.createCompleteData({ id, source: dataset.source, key: args.field, groupBy: complete.groupBy, fill });
38
+ next = next.rebindLayerData({ id: layer.id, data: id });
39
+ return next._withMarkConfig(layer.id, { ...next.markConfigs[layer.id], alignedPositionData: [
40
+ id, ...(next.markConfigs[layer.id]?.alignedPositionData ?? [])
41
+ ] });
42
+ }
@@ -1,3 +1,5 @@
1
+ import { rebindAlignedAreaPosition } from './areaAlignment.js';
2
+ import { assertSharedPositionMeaning } from '../../../selectors/plotComposition.js';
1
3
  import { stackLayoutMode } from "../seriesLayout.js";
2
4
  import { resolveBarGrain } from "../../../grammar/bars/policy.js";
3
5
  import { action } from "../../../core/action.js";
@@ -9,12 +11,31 @@ import { findLayer } from "../../../selectors/layers.js";
9
11
  import { findSemanticScale } from "../../../selectors/scales.js";
10
12
  import { applyPositionSemantics } from "./apply.js";
11
13
  import {
14
+ resolveTarget,
12
15
  applyEncodingScale,
13
16
  applyDetachedScaleRematerialization,
14
17
  rebindPositionGuides
15
18
  } from "../shared.js";
16
19
 
17
20
  function encodePosition(program, channel, args, operation) {
21
+ program = rebindAlignedAreaPosition(program, channel, args);
22
+ if (channel === 'theta' && typeof program.encodeChannels === 'function') {
23
+ const { layer } = resolveTarget(program, args.target, ['arc', 'line', 'point']);
24
+ const old = layer.encoding?.theta, color = layer.encoding?.color;
25
+ const scale = findSemanticScale(program, layer.encoding?.radius?.scale);
26
+ if (layer.mark.type === 'arc' && scale?.radialMapping && old?.field &&
27
+ args.field !== old.field && color?.field === old.field &&
28
+ ['nominal', 'ordinal'].includes(args.fieldType ?? old.fieldType)) {
29
+ const { target: _target, coordinate, ...theta } = args;
30
+ if (coordinate !== undefined && coordinate !== layer.coordinate) {
31
+ throw new Error('encodeTheta cannot move a measured polar mark to a different coordinate.');
32
+ }
33
+ return program.editSemantic({ property: `layer[${layer.id}].encoding.color.field`, value: args.field })
34
+ .encodeChannels({ target: layer.id, channels: {
35
+ theta, color: { field: args.field, fieldType: args.fieldType ?? old.fieldType }
36
+ } });
37
+ }
38
+ }
18
39
  const {
19
40
  target,
20
41
  layer,
@@ -34,9 +55,22 @@ function encodePosition(program, channel, args, operation) {
34
55
  companion,
35
56
  clearRadialMapping
36
57
  } = resolvePositionEncoding(program, channel, args, operation);
58
+ if (layer.derivedBindings?.overlay) throw new Error("Overlay position follows its source; edit the source or detach the overlay first.");
37
59
 
38
- let next = program
39
- .createCoordinate({
60
+ if (args.scale?.id === undefined) assertSharedPositionMeaning(program, layer, channel, {
61
+ ...(hasField ? { field } : { datum }), fieldType, temporalUnit,
62
+ scale: scale.id, aggregate, bin, stack
63
+ });
64
+ let next = program;
65
+ if (previous?.field !== undefined && previous.field !== field && ['theta', 'radius'].includes(channel)) {
66
+ for (const [id, config] of Object.entries(next.guideConfigs.axis ?? {})) {
67
+ for (const part of ['ticks', 'labels']) {
68
+ if (config[part]?.scale !== previous.scale || config[part].mode !== 'values') continue;
69
+ next = next._withGuideConfig(id, part, { ...config[part], inferredValues: true });
70
+ }
71
+ }
72
+ }
73
+ next = next.createCoordinate({
40
74
  id: coordinate.id,
41
75
  type: coordinate.type,
42
76
  layers: [target]
@@ -70,7 +104,7 @@ function encodePosition(program, channel, args, operation) {
70
104
  }
71
105
  next = applyEncodingScale(next, scale, requestedScale, {
72
106
  reassignment: previous?.scale === scale.id,
73
- allowTypeChange: layer.mark.type === "area"
107
+ allowTypeChange: layer.mark.type === "area" || previous?.field === field && previous?.fieldType === fieldType
74
108
  });
75
109
  if (["bar", "area"].includes(layer.mark.type) && Object.hasOwn(args, "stack") && args.stack !== undefined) {
76
110
  next = next.editSemantic({ property: `layer[${target}].layout.mode`, value: stackLayoutMode(args.stack) });
@@ -1 +1 @@
1
- import{validateAggregate as E,validateAggregateFieldType as b}from"../../../../grammar/aggregate.js";import{emptyPositionPolicy as a,resolveBin as h}from"./common.js";import{findSemanticScale as P}from"../../../../selectors/scales.js";import{validateCategoricalColorConsumer as L}from"../../../../grammar/scales/colorConsumers.js";import{validateLineMissingScope as k}from"../../../../grammar/lineMissing.js";function u(d,e){if(d.stack!==void 0)throw new Error(`Line ${e} encoding does not support stack.`)}function p(d,e){if(d.bin!==void 0)throw new Error(`Line ${e} encoding does not support bin.`)}function $({program:d,layer:e,dataset:g,channel:o,args:n,fieldType:i}){k({...e,encoding:{...e.encoding,[o]:{...n,fieldType:i}}});for(const t of["color","strokeDash"]){const r=e.encoding?.[t],l=r===void 0?void 0:P(d,r.scale);l!==void 0&&L({...e,encoding:{...e.encoding,[o]:{...e.encoding[o],...n,fieldType:i}}},r,l,t)}const m=["theta","radius"].includes(o),c=d.markConfigs[e.id]??{};if(m){if(c.curve!==void 0&&c.curve!=="linear")throw new Error('Polar line position currently requires curve "linear".');if(p(n,o),u(n,o),n.aggregate!==void 0)throw new Error(`Line ${o} encoding does not support aggregate.`);return a()}if(c.closed===!0)throw new Error("Line closed requires theta/radius Polar position encodings.");const f=g.transform?.some(t=>t.type==="regression"),s=g.transform?.some(t=>t.type==="interval"),v=g.transform?.some(t=>t.type==="window"&&t.operations?.some(r=>r.as===n.field));if(o==="x"){if(u(n,"x"),n.aggregate!==void 0)throw new Error("Line x encoding does not support aggregate.");if(n.bin!==void 0){if(i!=="quantitative")throw new Error("Binned line x encoding requires a quantitative field.");if(e.encoding?.y!==void 0&&e.encoding.y.aggregate===void 0)throw new Error("Binned line x encoding requires an aggregate y encoding.");return{bin:h(n.bin),aggregate:void 0,stack:void 0}}if(["nominal","ordinal"].includes(i)){if(e.encoding?.y!==void 0&&e.encoding.y.aggregate===void 0&&e.encoding.y.fieldType!=="quantitative")throw new Error("Categorical line x requires a quantitative value or aggregate y encoding.");return a()}const t=e.encoding?.y?.aggregate===void 0&&["quantitative","temporal"].includes(e.encoding?.y?.fieldType)&&i==="quantitative",r=e.encoding?.y===void 0&&i==="quantitative";if(i!=="temporal"&&!((f||s||t||r)&&i==="quantitative"))throw new Error("Line x encoding requires a categorical or temporal field, or a compatible quantitative field.");return a()}if(p(n,"y"),u(n,"y"),e.encoding?.x?.bin!==void 0&&n.aggregate===void 0)throw new Error("Binned line x encoding requires an aggregate y encoding.");if(["nominal","ordinal"].includes(e.encoding?.x?.fieldType)&&n.aggregate===void 0&&i!=="quantitative")throw new Error("Categorical line x requires a quantitative value or aggregate y encoding.");const q=i==="quantitative"&&["quantitative","temporal","nominal","ordinal"].includes(e.encoding?.x?.fieldType)&&e.encoding.x.bin===void 0,x=n.aggregate===void 0&&(i==="temporal"||i==="quantitative"&&(e.encoding?.x===void 0||q));if(s||v&&n.aggregate===void 0||x){if(!["quantitative","temporal"].includes(i))throw new Error("Direct line y encoding requires a quantitative or temporal field.");if(n.aggregate!==void 0)throw new Error("Direct line y encoding does not support aggregate.");return a()}if(f){if(i!=="quantitative")throw new Error("Regression line y encoding requires a quantitative field.");if(n.aggregate!==void 0)throw new Error("Regression line y encoding does not support aggregate.");return a()}if(e.encoding?.x!==void 0&&!(["temporal","nominal","ordinal"].includes(e.encoding.x.fieldType)||e.encoding.x.fieldType==="quantitative"&&e.encoding.x.bin!==void 0))throw new Error("Aggregate line y encoding requires a temporal, categorical, or binned quantitative x encoding.");const w=E(n.aggregate);return b(w,i),{bin:void 0,aggregate:w,stack:void 0}}export{$ as resolveLinePositionPolicy};
1
+ import{validateAggregate as L,validateAggregateFieldType as P}from"../../../../grammar/aggregate.js";import{emptyPositionPolicy as g,resolveBin as k}from"./common.js";import{findSemanticScale as A}from"../../../../selectors/scales.js";import{validateCategoricalColorConsumer as C}from"../../../../grammar/scales/colorConsumers.js";import{validateLineMissingScope as B}from"../../../../grammar/lineMissing.js";import{isAggregateLinePosition as v,isHorizontalLine as D}from"../../../../grammar/lineSeries.js";function f(r,e){if(r.stack!==void 0)throw new Error(`Line ${e} encoding does not support stack.`)}function q(r,e){if(r.bin!==void 0)throw new Error(`Line ${e} encoding does not support bin.`)}function S({program:r,layer:e,dataset:a,channel:t,args:n,fieldType:i,orientationNormalized:l=!1}){B({...e,encoding:{...e.encoding,[t]:{...n,fieldType:i}}});for(const o of["color","strokeDash"]){const d=e.encoding?.[o],c=d===void 0?void 0:A(r,d.scale);c!==void 0&&C({...e,encoding:{...e.encoding,[t]:{...e.encoding[t],...n,fieldType:i}}},d,c,o)}const x=["theta","radius"].includes(t),u=r.markConfigs[e.id]??{};if(x){if(u.curve!==void 0&&u.curve!=="linear")throw new Error('Polar line position currently requires curve "linear".');if(q(n,t),f(n,t),n.aggregate!==void 0)throw new Error(`Line ${t} encoding does not support aggregate.`);return g()}if(u.closed===!0)throw new Error("Line closed requires theta/radius Polar position encodings.");const s={...e,encoding:{...e.encoding,[t]:{...n,fieldType:i}}};if(s.encoding.x?.aggregate!==void 0&&s.encoding.y?.aggregate!==void 0)throw new Error("A Line requires one independent position; both axes cannot aggregate.");if(!l&&D(s))return S({program:r,dataset:a,layer:{...e,encoding:{...e.encoding,x:e.encoding?.y,y:e.encoding?.x}},channel:t==="x"?"y":"x",args:n,fieldType:i,orientationNormalized:!0});const w=a.transform?.some(o=>o.type==="regression"),m=a.transform?.some(o=>o.type==="interval"),E=a.transform?.some(o=>o.type==="window"&&o.operations?.some(d=>d.as===n.field));if(t==="x"){if(f(n,"x"),n.aggregate!==void 0)throw new Error("Line x encoding does not support aggregate.");if(n.bin!==void 0){if(i!=="quantitative")throw new Error("Binned line x encoding requires a quantitative field.");if(e.encoding?.y!==void 0&&e.encoding.y.aggregate===void 0)throw new Error("Binned line x encoding requires an aggregate y encoding.");return{bin:k(n.bin),aggregate:void 0,stack:void 0}}if(["nominal","ordinal"].includes(i)){if(e.encoding?.y!==void 0&&e.encoding.y.aggregate===void 0&&e.encoding.y.fieldType!=="quantitative")throw new Error("Categorical line x requires a quantitative value or aggregate y encoding.");return g()}const o=e.encoding?.y?.aggregate===void 0&&["quantitative","temporal"].includes(e.encoding?.y?.fieldType)&&i==="quantitative",d=e.encoding?.y===void 0&&i==="quantitative",c=e.encoding?.y?.aggregate!==void 0&&v({fieldType:i});if(i!=="temporal"&&!((w||m||o||d||c)&&i==="quantitative"))throw new Error("Line x encoding requires a categorical or temporal field, or a compatible quantitative field.");return g()}if(q(n,"y"),f(n,"y"),e.encoding?.x?.bin!==void 0&&n.aggregate===void 0)throw new Error("Binned line x encoding requires an aggregate y encoding.");if(["nominal","ordinal"].includes(e.encoding?.x?.fieldType)&&n.aggregate===void 0&&i!=="quantitative")throw new Error("Categorical line x requires a quantitative value or aggregate y encoding.");const h=i==="quantitative"&&["quantitative","temporal","nominal","ordinal"].includes(e.encoding?.x?.fieldType)&&e.encoding.x.bin===void 0,b=n.aggregate===void 0&&(i==="temporal"||i==="quantitative"&&(e.encoding?.x===void 0||h));if(m||E&&n.aggregate===void 0||b){if(!["quantitative","temporal"].includes(i))throw new Error("Direct line y encoding requires a quantitative or temporal field.");if(n.aggregate!==void 0)throw new Error("Direct line y encoding does not support aggregate.");return g()}if(w){if(i!=="quantitative")throw new Error("Regression line y encoding requires a quantitative field.");if(n.aggregate!==void 0)throw new Error("Regression line y encoding does not support aggregate.");return g()}if(e.encoding?.x!==void 0&&!v(e.encoding.x))throw new Error("Aggregate line y encoding requires a quantitative, temporal, or categorical x encoding.");const p=L(n.aggregate);return P(p,i),{bin:void 0,aggregate:p,stack:void 0}}export{S as resolveLinePositionPolicy};
@@ -1,3 +1,4 @@
1
+ import { assertIndependentOverlayGeometry } from "../../grammar/overlay.js";
1
2
  import { readAreaEndpoint, validateAreaEndpointPair, normalizeAreaBound } from "../../grammar/areaEndpoints.js";
2
3
  import { action } from "../../core/action.js";
3
4
  import { validateUserId } from "../../core/identifiers.js";
@@ -83,11 +84,11 @@ function encodeSecondaryPosition(program, channel, args, operation, types) {
83
84
  if (!rule && !area && !rect && (!hasField || hasDatum)) {
84
85
  throw new Error(`${operation} requires a field for a ranged mark.`);
85
86
  }
86
- if (rule && args.fieldType === undefined) {
87
+ if (rule && hasField && args.fieldType === undefined) {
87
88
  throw new Error(`${operation} requires fieldType for a rule mark.`);
88
89
  }
89
90
  const fieldType = rule
90
- ? validateSemanticFieldType(args.fieldType)
91
+ ? validateSemanticFieldType(args.fieldType ?? "quantitative")
91
92
  : layer.mark.type === "rect"
92
93
  ? args.fieldType ?? primary.fieldType
93
94
  : args.fieldType ?? "quantitative";
@@ -306,6 +307,7 @@ const encodeGroup = /* @__PURE__ */ action(
306
307
  ["line", "area", "bar"],
307
308
  "series mark"
308
309
  );
310
+ assertIndependentOverlayGeometry(layer, "encodeGroup");
309
311
  if ((args.fieldType ?? "nominal") !== "nominal") {
310
312
  throw new Error("encodeGroup requires a nominal field.");
311
313
  }
@@ -1,3 +1,4 @@
1
+ import {legendBlockChannels} from "../../grammar/legendBlocks.js";
1
2
  import { cartesianAxisOwners, axisTargetArgs } from "../../selectors/axes.js";
2
3
  import { closedAction } from "../../core/action.js";
3
4
  import { validateUserId } from "../../core/identifiers.js";
@@ -41,13 +42,7 @@ const REMOVE_AXIS = Object.freeze({
41
42
  theta: "removeThetaAxis",
42
43
  radius: "removeRadialAxis"
43
44
  });
44
- const SPECIALIZED_LEGEND_KIND = Object.freeze({
45
- color: Object.freeze(["gradient", "interval", "colorSize"]),
46
- stroke: Object.freeze(["strokeGradient", "strokeInterval"]),
47
- size: Object.freeze(["size", "colorSize"]),
48
- opacity: Object.freeze(["opacity"]),
49
- strokeWidth: Object.freeze(["strokeWidth"])
50
- });
45
+
51
46
 
52
47
  function resolveTarget(program, requested, channel) {
53
48
  const candidates = program.semanticSpec.layers.filter(
@@ -190,9 +185,10 @@ function reconcileCategoricalLegend(program, target, channels) {
190
185
 
191
186
  function cleanupLegends(program, target, channels) {
192
187
  let next = reconcileCategoricalLegend(program, target, channels);
193
- const kinds = [...new Set(channels.flatMap(
194
- channel => SPECIALIZED_LEGEND_KIND[channel] ?? []
195
- ))].filter(kind => next.guideConfigs.legend?.[kind]?.target === target);
188
+ const kinds = Object.entries(next.guideConfigs.legend ?? {})
189
+ .filter(([kind, config]) => !["series", "color", "stroke"].includes(kind) &&
190
+ config.target === target && legendBlockChannels(kind, config).some(channel => channels.includes(channel)))
191
+ .map(([kind]) => kind);
196
192
  return kinds.length === 0 ? next : removeLegendKinds(next, kinds);
197
193
  }
198
194
 
@@ -221,6 +217,9 @@ export const removeEncoding = /* @__PURE__ */ closedAction(
221
217
  );
222
218
  }
223
219
  const layer = resolveTarget(this, args.target, args.channel);
220
+ if (layer.derivedBindings?.overlay && ["x", "y", "x2", "y2", "xOffset", "yOffset", "group", "pathOrder"].includes(args.channel)) {
221
+ throw new Error("Overlay geometry follows its source; edit the source or detach the overlay first.");
222
+ }
224
223
  if (args.channel === "group" && layer.layout?.mode !== undefined && layer.layout.mode !== "overlay") throw new Error("Remove active series layout before removing its group.");
225
224
  if (args.channel === "theta" && layer.mark.type === "arc" &&
226
225
  ["count", "sum"].includes(layer.encoding?.radius?.aggregate)) {
@@ -254,6 +253,10 @@ export const removeEncoding = /* @__PURE__ */ closedAction(
254
253
  .filter(item => item.scale !== undefined);
255
254
 
256
255
  let next = this;
256
+ if (args.channel === "color" && layer.derivedBindings?.overlay?.inheritColor) {
257
+ next = next.editSemantic({ property: `layer[${layer.id}].derivedBindings.overlay`,
258
+ value: { ...layer.derivedBindings.overlay, inheritColor: false } });
259
+ }
257
260
  for (const channel of channels) {
258
261
  next = next.editSemantic({
259
262
  property: `layer[${layer.id}].encoding.${channel}`,
@@ -1,4 +1,4 @@
1
- import { APPEARANCE_MARK_TYPES } from "../../core/vocabulary.js";
1
+ import { APPEARANCE_MARK_TYPES, FIXED_APPEARANCE_MARK_TYPES } from "../../core/vocabulary.js";
2
2
  import { action } from "../../core/action.js";
3
3
  import { validateRuleStrokeWidth } from "../../grammar/ruleAppearance.js";
4
4
  import { readQuantitativeField } from "../../grammar/scales/index.js";
@@ -39,8 +39,20 @@ const encodeStrokeWidth = /* @__PURE__ */ action(
39
39
  throw new Error("Constant stroke width does not accept fieldType or scale.");
40
40
  }
41
41
  validateRuleStrokeWidth(args.value);
42
- const { id, layer } = resolveTarget(this, args.target, APPEARANCE_MARK_TYPES.encodeStrokeWidth, "rule, line or rect mark");
42
+ const { id, layer } = resolveTarget(this, args.target, FIXED_APPEARANCE_MARK_TYPES.encodeStrokeWidth, "stroked mark");
43
43
  assertEncodingSelectionCompatibility(this, id, ["strokeWidth"]);
44
+ if (!APPEARANCE_MARK_TYPES.encodeStrokeWidth.includes(layer.mark.type)) {
45
+ const config = { ...this.markConfigs[id] };
46
+ const appearance = layer.mark.type === "bar" ? { ...config.barAppearance } : config;
47
+ // Width authoring enables an absent outline; existing constant or
48
+ // field-driven strokes keep their color and scale ownership.
49
+ const needsStroke = !layer.encoding?.stroke && (appearance.stroke === false ||
50
+ (["point", "area"].includes(layer.mark.type) && appearance.stroke === undefined));
51
+ if (needsStroke) appearance.stroke = "#000000";
52
+ appearance.strokeWidth = args.value;
53
+ if (layer.mark.type === "bar") config.barAppearance = appearance;
54
+ return rematerializeEncoding(this._withMarkConfig(id, config), id, "strokeWidth", undefined, layer);
55
+ }
44
56
  let next = this.guideConfigs.legend?.strokeWidth?.target === id
45
57
  ? this.removeLegend({ target: id, channels: ["strokeWidth"] })
46
58
  : this;