ggaction 0.0.13 → 0.0.15

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 (270) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +20 -11
  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 +8 -3
  7. package/src/actions/basic.js +2 -2
  8. package/src/actions/boxPlots/components.js +14 -4
  9. package/src/actions/boxPlots/edit.js +31 -5
  10. package/src/actions/boxPlots/materialize.js +11 -3
  11. package/src/actions/boxPlots/options.js +48 -16
  12. package/src/actions/categoryOrder/index.js +15 -4
  13. package/src/actions/charts/area.js +5 -1
  14. package/src/actions/charts/bar.js +2 -1
  15. package/src/actions/charts/density.js +6 -2
  16. package/src/actions/charts/ecdf.js +4 -1
  17. package/src/actions/charts/endpoints.js +25 -3
  18. package/src/actions/charts/heatmap.js +2 -1
  19. package/src/actions/charts/histogram.js +4 -3
  20. package/src/actions/charts/horizon.js +3 -1
  21. package/src/actions/charts/interval-regression.js +5 -2
  22. package/src/actions/charts/line.js +3 -1
  23. package/src/actions/charts/parallel.js +8 -2
  24. package/src/actions/charts/pie.js +10 -2
  25. package/src/actions/charts/polar.js +15 -4
  26. package/src/actions/charts/radar.js +8 -2
  27. package/src/actions/charts/radial.js +10 -2
  28. package/src/actions/charts/rug-strip.js +6 -2
  29. package/src/actions/charts/scatter.js +3 -1
  30. package/src/actions/composition/actions.js +13 -4
  31. package/src/actions/coordinates/actions.js +1 -1
  32. package/src/actions/coordinates/edit.js +76 -0
  33. package/src/actions/data/bin.js +1 -1
  34. package/src/actions/data/bin2d.js +57 -185
  35. package/src/actions/data/complete.js +26 -0
  36. package/src/actions/data/computed.js +2 -2
  37. package/src/actions/data/create.js +2 -2
  38. package/src/actions/data/density.js +12 -20
  39. package/src/actions/data/derived.js +47 -17
  40. package/src/actions/data/edit.js +539 -0
  41. package/src/actions/data/filter.js +14 -2
  42. package/src/actions/data/impute.js +26 -0
  43. package/src/actions/data/index.js +14 -0
  44. package/src/actions/data/normalize.js +27 -0
  45. package/src/actions/data/regression.js +16 -21
  46. package/src/actions/data/revisionBindings.js +145 -0
  47. package/src/actions/data/shared.js +20 -4
  48. package/src/actions/data/summary.js +1 -1
  49. package/src/actions/data/timeUnit.js +4 -1
  50. package/src/actions/data/window.js +1 -1
  51. package/src/actions/encodings/channels.js +415 -0
  52. package/src/actions/encodings/color/index.js +2 -1
  53. package/src/actions/encodings/density.js +5 -2
  54. package/src/actions/encodings/histogram.js +3 -2
  55. package/src/actions/encodings/index.js +4 -0
  56. package/src/actions/encodings/offset.js +20 -13
  57. package/src/actions/encodings/position/apply.js +53 -7
  58. package/src/actions/encodings/position/index.js +10 -2
  59. package/src/actions/encodings/position/policies/bar.js +9 -0
  60. package/src/actions/encodings/position/resolve.js +24 -7
  61. package/src/actions/encodings/remove.js +9 -4
  62. package/src/actions/encodings/ruleAppearance.js +1 -23
  63. package/src/actions/encodings/seriesLayout.js +1 -0
  64. package/src/actions/encodings/shared.js +82 -14
  65. package/src/actions/encodings/stroke.js +226 -0
  66. package/src/actions/errorBands/create.js +23 -3
  67. package/src/actions/errorBands/edit.js +21 -6
  68. package/src/actions/errorBars/create.js +35 -7
  69. package/src/actions/errorBars/edit.js +19 -4
  70. package/src/actions/facets/actions.js +237 -57
  71. package/src/actions/facets/derive.js +91 -26
  72. package/src/actions/gradientPlots/components.js +10 -2
  73. package/src/actions/gradientPlots/edit.js +14 -4
  74. package/src/actions/gradientPlots/materialize.js +3 -1
  75. package/src/actions/gradientPlots/options.js +10 -2
  76. package/src/actions/guides/applicability.js +8 -2
  77. package/src/actions/guides/axes/labels.js +58 -12
  78. package/src/actions/guides/axes/lines.js +6 -2
  79. package/src/actions/guides/axes/parallel/resolve.js +6 -2
  80. package/src/actions/guides/axes/tickGroups.js +2 -1
  81. package/src/actions/guides/axes/ticks.js +6 -2
  82. package/src/actions/guides/axes/titles.js +11 -3
  83. package/src/actions/guides/grids/resolve.js +7 -2
  84. package/src/actions/guides/guides.js +19 -3
  85. package/src/actions/guides/legends/blocks.js +426 -0
  86. package/src/actions/guides/legends/categorical/actions.js +134 -48
  87. package/src/actions/guides/legends/categorical/components.js +17 -12
  88. package/src/actions/guides/legends/categorical/index.js +37 -33
  89. package/src/actions/guides/legends/categorical/layout.js +97 -15
  90. package/src/actions/guides/legends/categorical/options.js +2 -2
  91. package/src/actions/guides/legends/categorical/recipes.js +111 -20
  92. package/src/actions/guides/legends/categorical/resolve.js +12 -8
  93. package/src/actions/guides/legends/categorical/symbols.js +85 -30
  94. package/src/actions/guides/legends/continuous/common.js +30 -12
  95. package/src/actions/guides/legends/continuous/gradient.js +10 -7
  96. package/src/actions/guides/legends/continuous/index.js +2 -0
  97. package/src/actions/guides/legends/continuous/interval.js +8 -3
  98. package/src/actions/guides/legends/continuous/opacity.js +8 -5
  99. package/src/actions/guides/legends/continuous/stroke.js +525 -0
  100. package/src/actions/guides/legends/creation.js +14 -1
  101. package/src/actions/guides/legends/edit.js +158 -59
  102. package/src/actions/guides/legends/index.js +2 -0
  103. package/src/actions/guides/legends/lane.js +54 -19
  104. package/src/actions/guides/legends/lifecycle.js +19 -8
  105. package/src/actions/guides/legends/remove.js +13 -3
  106. package/src/actions/guides/legends/sampling.js +140 -0
  107. package/src/actions/guides/legends/size.js +93 -40
  108. package/src/actions/guides/legends/strokeWidth.js +34 -28
  109. package/src/actions/guides/legends/target.js +112 -1
  110. package/src/actions/guides/legends/transition.js +165 -16
  111. package/src/actions/guides/polar/axes/facade.js +4 -4
  112. package/src/actions/guides/polar/axes/labels.js +24 -5
  113. package/src/actions/guides/polar/axes/lines.js +10 -4
  114. package/src/actions/guides/polar/axes/shared.js +16 -0
  115. package/src/actions/guides/polar/axes/ticks.js +1 -1
  116. package/src/actions/guides/polar/axes/titles.js +1 -1
  117. package/src/actions/guides/polar/grids.js +1 -1
  118. package/src/actions/guides/polar/resolve.js +12 -8
  119. package/src/actions/index.js +14 -3
  120. package/src/actions/marks/arc/actions.js +77 -19
  121. package/src/actions/marks/area/actions.js +34 -9
  122. package/src/actions/marks/area/materialize.js +17 -1
  123. package/src/actions/marks/bar/create.js +11 -2
  124. package/src/actions/marks/bar/edit.js +20 -3
  125. package/src/actions/marks/bar/materialize.js +22 -2
  126. package/src/actions/marks/index.js +10 -1
  127. package/src/actions/marks/line/actions.js +37 -14
  128. package/src/actions/marks/line/materialize.js +29 -10
  129. package/src/actions/marks/point/create.js +8 -2
  130. package/src/actions/marks/point/edit.js +18 -6
  131. package/src/actions/marks/point/materialize.js +49 -15
  132. package/src/actions/marks/rect/actions.js +33 -12
  133. package/src/actions/marks/references.js +601 -30
  134. package/src/actions/marks/remove.js +71 -1
  135. package/src/actions/marks/rule/actions.js +47 -10
  136. package/src/actions/marks/shared.js +1 -1
  137. package/src/actions/marks/text/actions.js +74 -9
  138. package/src/actions/marks/text/index.js +4 -0
  139. package/src/actions/marks/text/layout.js +154 -6
  140. package/src/actions/marks/text/removal.js +173 -0
  141. package/src/actions/marks/text/selection.js +73 -0
  142. package/src/actions/marks/tick/actions.js +22 -5
  143. package/src/actions/primitives/editGraphics.js +26 -0
  144. package/src/actions/primitives/semantic.js +2 -1
  145. package/src/actions/primitives/semanticAction.js +83 -10
  146. package/src/actions/primitives/semanticValidation/guide.js +1 -1
  147. package/src/actions/primitives/semanticValidation/index.js +9 -1
  148. package/src/actions/primitives/semanticValidation/layer.js +5 -0
  149. package/src/actions/primitives/semanticValidation/scale.js +28 -7
  150. package/src/actions/regression/components.js +26 -11
  151. package/src/actions/regression/create.js +10 -2
  152. package/src/actions/regression/edit.js +21 -7
  153. package/src/actions/resources/index.js +7 -0
  154. package/src/actions/resources/remove.js +172 -0
  155. package/src/actions/scales/channels.js +212 -0
  156. package/src/actions/scales/consumers/common.js +1 -1
  157. package/src/actions/scales/consumers/families.js +3 -2
  158. package/src/actions/scales/consumers/index.js +19 -2
  159. package/src/actions/scales/consumers/seriesLayout.js +28 -14
  160. package/src/actions/scales/create.js +54 -22
  161. package/src/actions/scales/definitions.js +94 -40
  162. package/src/actions/scales/edit.js +66 -11
  163. package/src/actions/scales/editPolicy.js +81 -11
  164. package/src/actions/scales/index.js +6 -0
  165. package/src/actions/scales/materialize.js +4 -3
  166. package/src/actions/scales/offset.js +48 -0
  167. package/src/actions/scales/parallel.js +71 -0
  168. package/src/actions/scales/preview.js +111 -10
  169. package/src/actions/selection/actions.js +154 -30
  170. package/src/actions/selection/index.js +3 -0
  171. package/src/actions/theme/actions.js +68 -13
  172. package/src/actions/theme/composition.js +238 -0
  173. package/src/actions/theme/index.js +9 -2
  174. package/src/actions/theme/reconcile.js +315 -67
  175. package/src/actions/theme/state.js +160 -0
  176. package/src/actions/violinPlots/create.js +5 -2
  177. package/src/actions/violinPlots/edit.js +1 -0
  178. package/src/core/action.js +10 -0
  179. package/src/core/compositionState.js +6 -1
  180. package/src/core/programState.js +20 -0
  181. package/src/core/resourceReferences.js +332 -0
  182. package/src/core/vocabulary.js +16 -3
  183. package/src/grammar/bars/aggregate.js +4 -1
  184. package/src/grammar/bars/geometry.js +44 -0
  185. package/src/grammar/bin.js +54 -9
  186. package/src/grammar/complete.js +279 -0
  187. package/src/grammar/computed.js +265 -64
  188. package/src/grammar/coordinates.js +37 -0
  189. package/src/grammar/density.js +239 -37
  190. package/src/grammar/displayLabels.js +57 -0
  191. package/src/grammar/facets/dependencies.js +133 -1
  192. package/src/grammar/facets/guides.js +3 -0
  193. package/src/grammar/facets/index.js +7 -47
  194. package/src/grammar/facets/scales.js +15 -18
  195. package/src/grammar/impute.js +340 -0
  196. package/src/grammar/markLabelSelection.js +57 -0
  197. package/src/grammar/markLabels.js +18 -1
  198. package/src/grammar/normalize.js +366 -0
  199. package/src/grammar/pathSeries.js +26 -8
  200. package/src/grammar/pointShapes.js +13 -4
  201. package/src/grammar/polar.js +90 -8
  202. package/src/grammar/regression/index.js +1 -0
  203. package/src/grammar/regression/parameters.js +23 -0
  204. package/src/grammar/roundedRect.js +180 -0
  205. package/src/grammar/rules.js +1 -0
  206. package/src/grammar/scales/colorConsumers.js +20 -1
  207. package/src/grammar/scales/continuous.js +2 -2
  208. package/src/grammar/scales/definition.js +27 -4
  209. package/src/grammar/scales/index.js +1 -0
  210. package/src/grammar/scales/ordinal.js +12 -3
  211. package/src/grammar/scales/policies.js +2 -2
  212. package/src/grammar/scales/size.js +456 -0
  213. package/src/grammar/schemas/concreteGraphic.js +14 -2
  214. package/src/grammar/schemas/graphic.js +15 -0
  215. package/src/grammar/schemas/graphicBounds.js +179 -30
  216. package/src/grammar/schemas/semanticPath.js +7 -2
  217. package/src/grammar/statisticalReference.js +103 -0
  218. package/src/grammar/strokeStyle.js +77 -0
  219. package/src/grammar/summary.js +78 -16
  220. package/src/grammar/timeUnit.js +216 -5
  221. package/src/grammar/transformTopology.js +4 -0
  222. package/src/grammar/transforms.js +394 -28
  223. package/src/grammar/weightedStatistics.js +374 -0
  224. package/src/grammar/window.js +221 -16
  225. package/src/layout/aspect.js +80 -0
  226. package/src/layout/facets.js +59 -5
  227. package/src/layout/labels.js +343 -0
  228. package/src/materialization/bars/aggregate.js +25 -2
  229. package/src/materialization/bars/histogram.js +84 -24
  230. package/src/materialization/bars/ranged.js +19 -3
  231. package/src/materialization/bars/resolve.js +7 -4
  232. package/src/materialization/composition.js +3 -1
  233. package/src/materialization/coordinateBounds.js +204 -0
  234. package/src/materialization/dependencies.js +36 -0
  235. package/src/materialization/facetGuides/legacyCategorical.js +3 -0
  236. package/src/materialization/facetGuides/placement.js +8 -2
  237. package/src/materialization/facetGuides/preparation.js +12 -7
  238. package/src/materialization/facetHeaders.js +346 -0
  239. package/src/materialization/facets.js +15 -65
  240. package/src/materialization/guides/resources.js +26 -0
  241. package/src/materialization/legends.js +1 -1
  242. package/src/materialization/marks/index.js +11 -5
  243. package/src/materialization/marks/policies.js +2 -1
  244. package/src/materialization/rect.js +25 -12
  245. package/src/materialization/rectConfig.js +5 -1
  246. package/src/materialization/scales/map.js +5 -3
  247. package/src/materialization/scales/policies/binnedPosition.js +2 -2
  248. package/src/materialization/scales/policies/offset.js +50 -8
  249. package/src/materialization/scales/policies/series.js +4 -1
  250. package/src/materialization/scales/resolve.js +22 -6
  251. package/src/materialization/selection/items/arc.js +28 -4
  252. package/src/materialization/selection/items/bar.js +30 -3
  253. package/src/materialization/selection/items/common.js +19 -4
  254. package/src/materialization/selection/items/rect.js +28 -4
  255. package/src/materialization/selection/path.js +1 -1
  256. package/src/materialization/selection/point.js +3 -1
  257. package/src/materialization/text.js +265 -14
  258. package/src/renderers/canvas/circle.js +2 -3
  259. package/src/renderers/canvas/line.js +2 -3
  260. package/src/renderers/canvas/native.js +4 -4
  261. package/src/renderers/canvas/path.js +2 -3
  262. package/src/renderers/canvas/rect.js +2 -3
  263. package/src/renderers/canvas/stroke.js +11 -0
  264. package/src/renderers/svg.js +19 -0
  265. package/src/selectors/datasets.js +32 -1
  266. package/src/selectors/layers.js +4 -0
  267. package/src/theme/defaults.js +101 -0
  268. package/types/basic.d.ts +6 -0
  269. package/types/index.d.ts +85 -0
  270. package/types/program.d.ts +910 -117
@@ -0,0 +1,226 @@
1
+ import { action } from "../../core/action.js";
2
+ import {
3
+ readNominalField,
4
+ readQuantitativeField,
5
+ readScaleField,
6
+ readTemporalField,
7
+ resolveTemporalUnit,
8
+ validateCategoricalFieldType
9
+ } from "../../grammar/scales/index.js";
10
+ import { validateContinuousColorConsumer } from
11
+ "../../grammar/scales/colorConsumers.js";
12
+ import { validatePathSeriesAppearance } from "../../grammar/pathSeries.js";
13
+ import { validateRuleStroke } from "../../grammar/ruleAppearance.js";
14
+ import { assertEncodingSelectionCompatibility } from
15
+ "../../materialization/selection/compatibility.js";
16
+ import {
17
+ resolveColorScaleDefinition,
18
+ resolveQuantitativeColorScaleDefinition
19
+ } from "../scales/definitions.js";
20
+ import { applyTemporalUnit } from "./temporal.js";
21
+ import {
22
+ applyEncodingScale,
23
+ rematerializeEncoding,
24
+ resolveReassignmentScaleOptions,
25
+ resolveTarget,
26
+ setEncodingProperties,
27
+ validateOptions
28
+ } from "./shared.js";
29
+
30
+ const OPTIONS = Object.freeze([
31
+ "target", "value", "field", "fieldType", "temporalUnit", "scale"
32
+ ]);
33
+ const MARK_TYPES = Object.freeze([
34
+ "point", "line", "area", "bar", "rect", "arc", "rule", "tick"
35
+ ]);
36
+
37
+ function withoutConstantStroke(program, layer) {
38
+ const config = { ...program.markConfigs[layer.id] };
39
+ if (layer.mark.type === "bar") {
40
+ const appearance = { ...config.barAppearance };
41
+ delete appearance.stroke;
42
+ config.barAppearance = appearance;
43
+ } else {
44
+ delete config.stroke;
45
+ if (layer.mark.type === "area") delete config.strokeFromFill;
46
+ }
47
+ return program._withMarkConfig(layer.id, config);
48
+ }
49
+
50
+ function withConstantStroke(program, layer, value) {
51
+ const config = { ...program.markConfigs[layer.id] };
52
+ if (layer.mark.type === "bar") {
53
+ const appearance = { ...config.barAppearance, stroke: value };
54
+ if (config.barAppearance?.stroke === false && appearance.strokeWidth === undefined) {
55
+ appearance.strokeWidth = 0.5;
56
+ }
57
+ config.barAppearance = appearance;
58
+ } else {
59
+ const restores = config.stroke === false;
60
+ config.stroke = value;
61
+ if (layer.mark.type === "area") delete config.strokeFromFill;
62
+ if (
63
+ (restores || ["point", "area"].includes(layer.mark.type)) &&
64
+ config.strokeWidth === undefined
65
+ ) {
66
+ config.strokeWidth = 1;
67
+ }
68
+ }
69
+ return program._withMarkConfig(layer.id, config);
70
+ }
71
+
72
+ function hasStrokeLegend(program, target) {
73
+ return Object.values(program.guideConfigs.legend ?? {}).some(config =>
74
+ config?.target === target && (
75
+ config.channel === "stroke" || config.channels?.includes("stroke")
76
+ )
77
+ );
78
+ }
79
+
80
+ function validateStrokeField(dataset, layer, args, scale, temporalUnit) {
81
+ const { field, fieldType } = args;
82
+ validateContinuousColorConsumer(layer, args, scale, { channel: "stroke" });
83
+ if (["line", "area"].includes(layer.mark.type)) {
84
+ validatePathSeriesAppearance(dataset.values, {
85
+ ...layer,
86
+ encoding: {
87
+ ...layer.encoding,
88
+ stroke: { field, fieldType, temporalUnit, scale: scale.id }
89
+ }
90
+ });
91
+ }
92
+ if (Object.hasOwn(scale, "unknown")) {
93
+ readScaleField(dataset.values, field, fieldType, {
94
+ allowUnknown: true,
95
+ temporalUnit
96
+ });
97
+ } else if (fieldType === "temporal") {
98
+ readTemporalField(dataset.values, field, temporalUnit);
99
+ } else {
100
+ readQuantitativeField(dataset.values, field);
101
+ }
102
+ }
103
+
104
+ export const encodeStroke = action(
105
+ {
106
+ op: "encodeStroke",
107
+ description: "Assign constant or field-driven graphical stroke color."
108
+ },
109
+ function (args = {}) {
110
+ validateOptions(args, OPTIONS, "encodeStroke");
111
+ const hasValue = Object.hasOwn(args, "value");
112
+ const hasField = Object.hasOwn(args, "field");
113
+ if (hasValue === hasField) {
114
+ throw new Error("encodeStroke requires exactly one of value or field.");
115
+ }
116
+ if (hasValue && (
117
+ args.fieldType !== undefined ||
118
+ args.temporalUnit !== undefined ||
119
+ args.scale !== undefined
120
+ )) {
121
+ throw new Error(
122
+ "Constant stroke does not accept fieldType, temporalUnit, or scale."
123
+ );
124
+ }
125
+ const { id: target, dataset, layer } = resolveTarget(
126
+ this,
127
+ args.target,
128
+ MARK_TYPES,
129
+ "stroke-capable mark"
130
+ );
131
+
132
+ if (hasValue) {
133
+ const value = validateRuleStroke(args.value);
134
+ if (
135
+ layer.mark.type === "rule" &&
136
+ layer.encoding?.stroke === undefined &&
137
+ !hasStrokeLegend(this, target)
138
+ ) {
139
+ return this
140
+ ._withMarkConfig(target, { ...this.markConfigs[target], stroke: value })
141
+ .rematerializeRuleMark({ id: target });
142
+ }
143
+ assertEncodingSelectionCompatibility(this, target, ["stroke"]);
144
+ let next = hasStrokeLegend(this, target)
145
+ ? this.removeLegend({ target, channels: ["stroke"] })
146
+ : this;
147
+ if (layer.encoding?.stroke !== undefined) {
148
+ next = next.editSemantic({
149
+ property: `layer[${target}].encoding.stroke`,
150
+ remove: true
151
+ });
152
+ }
153
+ next = withConstantStroke(next, layer, value);
154
+ return rematerializeEncoding(next, target, "stroke", undefined, layer);
155
+ }
156
+
157
+ const fieldType = args.fieldType ?? "nominal";
158
+ const previous = layer.encoding?.stroke;
159
+ const temporalUnit = resolveTemporalUnit(args, fieldType, previous);
160
+ const requestedScale = resolveReassignmentScaleOptions(
161
+ previous,
162
+ args.scale ?? {}
163
+ );
164
+ let scale;
165
+ if (["nominal", "ordinal"].includes(fieldType)) {
166
+ validateCategoricalFieldType(fieldType);
167
+ if (temporalUnit !== undefined) {
168
+ throw new Error("Categorical stroke does not support temporalUnit.");
169
+ }
170
+ scale = resolveColorScaleDefinition(this, requestedScale, "stroke");
171
+ if (Object.hasOwn(scale, "unknown")) {
172
+ readScaleField(dataset.values, args.field, fieldType, {
173
+ allowUnknown: true
174
+ });
175
+ } else {
176
+ readNominalField(dataset.values, args.field);
177
+ }
178
+ if (["line", "area"].includes(layer.mark.type)) {
179
+ validatePathSeriesAppearance(dataset.values, {
180
+ ...layer,
181
+ encoding: {
182
+ ...layer.encoding,
183
+ stroke: {
184
+ field: args.field,
185
+ fieldType,
186
+ scale: scale.id
187
+ }
188
+ }
189
+ });
190
+ }
191
+ } else if (["quantitative", "temporal"].includes(fieldType)) {
192
+ scale = resolveQuantitativeColorScaleDefinition(
193
+ this,
194
+ fieldType,
195
+ requestedScale,
196
+ "stroke"
197
+ );
198
+ validateStrokeField(
199
+ dataset,
200
+ layer,
201
+ { ...args, fieldType },
202
+ scale,
203
+ temporalUnit
204
+ );
205
+ } else {
206
+ throw new Error(`Unsupported stroke field type "${fieldType}".`);
207
+ }
208
+
209
+ let next = withoutConstantStroke(this, layer);
210
+ next = applyTemporalUnit(next, target, "stroke", temporalUnit, previous);
211
+ next = setEncodingProperties(next, target, "stroke", {
212
+ field: args.field,
213
+ fieldType,
214
+ scale: scale.id
215
+ });
216
+ next = applyEncodingScale(next, scale, requestedScale, {
217
+ reassignment: previous?.scale === scale.id,
218
+ allowTypeChange: true
219
+ });
220
+ return rematerializeEncoding(next, target, "stroke", scale.id, layer);
221
+ }
222
+ );
223
+
224
+ export function registerStrokeEncodingAction(ProgramClass) {
225
+ ProgramClass.prototype.encodeStroke = encodeStroke;
226
+ }
@@ -6,6 +6,10 @@ import {
6
6
  } from "../../core/validation.js";
7
7
  import { DEFAULT_COLORS } from "../../theme/defaults.js";
8
8
  import { validateCurveInterpolation } from "../../grammar/curveCommands.js";
9
+ import {
10
+ requestedStrokeDetails,
11
+ STROKE_STYLE_PROPERTIES
12
+ } from "../../grammar/strokeStyle.js";
9
13
  import { createResolvedIntervalData } from "../data/intervalEdit.js";
10
14
  import { resolveIntervalComposite } from "../intervals/resolve.js";
11
15
  import { errorBandBoundaries, resolveBoundaryAppearance } from "./edit.js";
@@ -21,6 +25,7 @@ const OPTIONS = Object.freeze([
21
25
  "fill",
22
26
  "opacity",
23
27
  "curve",
28
+ ...STROKE_STYLE_PROPERTIES,
24
29
  "boundaries"
25
30
  ]);
26
31
 
@@ -96,11 +101,24 @@ export const createErrorBandBoundary = action(
96
101
  strokeWidth,
97
102
  strokeDash,
98
103
  opacity,
99
- curve
104
+ curve,
105
+ lineCap,
106
+ lineJoin,
107
+ miterLimit
100
108
  } = {}) {
101
109
  const vertical = orientation === "vertical";
102
110
  let next = this
103
- .createLineMark({ id, data, strokeWidth, curve })
111
+ .createLineMark({
112
+ id,
113
+ data,
114
+ strokeWidth,
115
+ curve,
116
+ ...requestedStrokeDetails(Object.fromEntries([
117
+ ["lineCap", lineCap],
118
+ ["lineJoin", lineJoin],
119
+ ["miterLimit", miterLimit]
120
+ ].filter(([, value]) => value !== undefined)), "Error-band boundary")
121
+ })
104
122
  .encodeY(positionOptions({
105
123
  target: id,
106
124
  field: vertical ? bound : position.field,
@@ -160,6 +178,7 @@ export const createErrorBand = action(
160
178
  },
161
179
  function (args = {}) {
162
180
  validateKeys(args, OPTIONS, "createErrorBand");
181
+ const strokeDetails = requestedStrokeDetails(args, "createErrorBand");
163
182
  const resolved = resolveErrorBand(this, args);
164
183
  const curve = validateCurveInterpolation(args.curve ?? "linear");
165
184
  const boundaries = resolveBoundaries(args.boundaries, curve);
@@ -170,7 +189,8 @@ export const createErrorBand = action(
170
189
  data: resolved.dataId,
171
190
  ...(args.fill === undefined ? {} : { fill: args.fill }),
172
191
  ...(args.opacity === undefined ? {} : { opacity: args.opacity }),
173
- ...(Object.hasOwn(args, "curve") ? { curve } : {})
192
+ ...(Object.hasOwn(args, "curve") ? { curve } : {}),
193
+ ...strokeDetails
174
194
  });
175
195
  const vertical = resolved.orientation === "vertical";
176
196
  next = next[vertical ? "encodeX" : "encodeY"](positionArgs(resolved));
@@ -10,6 +10,10 @@ import {
10
10
  } from "../../core/validation.js";
11
11
  import { validateCurveInterpolation } from "../../grammar/curveCommands.js";
12
12
  import { normalizeStrokeDashPattern } from "../../grammar/scales/index.js";
13
+ import {
14
+ requestedStrokeDetails,
15
+ STROKE_STYLE_PROPERTIES
16
+ } from "../../grammar/strokeStyle.js";
13
17
  import { findLayer } from "../../selectors/layers.js";
14
18
  import { findSemanticScale } from "../../selectors/scales.js";
15
19
  import { removeOwnedMark } from "../marks/remove.js";
@@ -32,7 +36,8 @@ import {
32
36
  } from "../data/intervalEdit.js";
33
37
 
34
38
  export const ERROR_BAND_BOUNDARY_OPTIONS = Object.freeze([
35
- "stroke", "strokeWidth", "strokeDash", "opacity", "curve"
39
+ "stroke", "strokeWidth", "strokeDash", "opacity", "curve",
40
+ ...STROKE_STYLE_PROPERTIES
36
41
  ]);
37
42
 
38
43
  export function resolveBoundaryAppearance(value, { defaults, operation }) {
@@ -49,12 +54,19 @@ export function resolveBoundaryAppearance(value, { defaults, operation }) {
49
54
  validateNonNegativeFinite(strokeWidth, `${operation} strokeWidth`);
50
55
  const resolvedStrokeDash = normalizeStrokeDashPattern(strokeDash);
51
56
  validateUnitInterval(opacity, `${operation} opacity`);
52
- return { stroke, strokeWidth, strokeDash: resolvedStrokeDash, opacity, curve };
57
+ return {
58
+ stroke,
59
+ strokeWidth,
60
+ strokeDash: resolvedStrokeDash,
61
+ opacity,
62
+ curve,
63
+ ...requestedStrokeDetails({ ...defaults, ...value }, operation)
64
+ };
53
65
  }
54
66
 
55
67
  const EDIT_OPTIONS = Object.freeze([
56
68
  "target", "data", "x", "y", "groupBy", "fill", "opacity", "curve",
57
- "statistics", "boundaries"
69
+ "statistics", "boundaries", ...STROKE_STYLE_PROPERTIES
58
70
  ]);
59
71
  const STATISTICS_OPTIONS = Object.freeze(["center", "extent", "method", "level"]);
60
72
  const EDIT_POLICY = Object.freeze({
@@ -331,7 +343,8 @@ function currentBoundaryAppearance(program, id) {
331
343
  strokeWidth: config.strokeWidth,
332
344
  strokeDash: config.strokeDash,
333
345
  opacity: config.opacity,
334
- curve: config.curve ?? "linear"
346
+ curve: config.curve ?? "linear",
347
+ ...requestedStrokeDetails(config, "Error-band boundary")
335
348
  };
336
349
  }
337
350
 
@@ -397,7 +410,8 @@ export const rematerializeErrorBandBoundary = action(
397
410
  strokeWidth: args.strokeWidth,
398
411
  strokeDash: args.strokeDash,
399
412
  opacity: args.opacity,
400
- curve: args.curve
413
+ curve: args.curve,
414
+ ...requestedStrokeDetails(args, "Error-band boundary")
401
415
  })
402
416
  .editGraphics({
403
417
  target: id,
@@ -416,12 +430,13 @@ export const editErrorBand = action(
416
430
  function (args = {}) {
417
431
  validateOptionObject(args, EDIT_OPTIONS, "editErrorBand");
418
432
  if (!["data", "x", "y", "groupBy", "fill", "opacity", "curve",
419
- "statistics", "boundaries"]
433
+ "statistics", "boundaries", ...STROKE_STYLE_PROPERTIES]
420
434
  .some(key => Object.hasOwn(args, key))) {
421
435
  throw new Error("editErrorBand requires at least one change.");
422
436
  }
423
437
  const owner = resolveOwner(this, args.target);
424
438
  const config = { ...this.markConfigs[owner.id] };
439
+ Object.assign(config, requestedStrokeDetails(args, "editErrorBand"));
425
440
  const errorBand = { ...config.errorBand };
426
441
  if (Object.hasOwn(args, "fill")) {
427
442
  if (args.fill === false) {
@@ -5,12 +5,14 @@ import {
5
5
  } from "../../core/validation.js";
6
6
  import { findLayer } from "../../selectors/layers.js";
7
7
  import { findDataset } from "../../selectors/datasets.js";
8
+ import { findSemanticScale } from "../../selectors/scales.js";
8
9
  import { DEFAULT_COLORS } from "../../theme/defaults.js";
9
10
  import {
10
11
  readNominalField,
11
12
  validateCategoricalFieldType
12
13
  } from "../../grammar/scales/index.js";
13
14
  import { normalizeOffsetPadding } from "../../grammar/bars/geometry.js";
15
+ import { STROKE_STYLE_PROPERTIES } from "../../grammar/strokeStyle.js";
14
16
  import { createResolvedIntervalData, ownOptions } from
15
17
  "../data/intervalEdit.js";
16
18
  import { resolveIntervalComposite } from "../intervals/resolve.js";
@@ -31,7 +33,8 @@ const OPTIONS = Object.freeze([
31
33
  "stroke",
32
34
  "strokeWidth",
33
35
  "strokeDash",
34
- "opacity"
36
+ "opacity",
37
+ ...STROKE_STYLE_PROPERTIES
35
38
  ]);
36
39
 
37
40
  const OFFSET_OPTIONS = Object.freeze([
@@ -101,7 +104,15 @@ function resolveErrorBarOffset(program, args, resolved, operation) {
101
104
  explicit ?? {},
102
105
  resolved.sourceLayer === undefined
103
106
  ? undefined
104
- : program.markConfigs[resolved.sourceLayer.id]?.[channel],
107
+ : (() => {
108
+ const semantic = inferred?.scale === undefined
109
+ ? undefined
110
+ : findSemanticScale(program, inferred.scale);
111
+ return semantic !== undefined && ["paddingInner", "paddingOuter"]
112
+ .some(property => Object.hasOwn(semantic, property))
113
+ ? semantic
114
+ : program.markConfigs[resolved.sourceLayer.id]?.[channel];
115
+ })(),
105
116
  channel
106
117
  );
107
118
  return {
@@ -118,7 +129,8 @@ function resolveErrorBarOffset(program, args, resolved, operation) {
118
129
 
119
130
  function resolveAppearance(args) {
120
131
  return resolveErrorBarAppearance(ownOptions(args, [
121
- "caps", "capSize", "stroke", "strokeWidth", "strokeDash", "opacity"
132
+ "caps", "capSize", "stroke", "strokeWidth", "strokeDash", "opacity",
133
+ ...STROKE_STYLE_PROPERTIES
122
134
  ]), {
123
135
  defaults: {
124
136
  caps: true,
@@ -143,7 +155,8 @@ export const createErrorBarCap = action(
143
155
  "intervalField", "coordinate", "positionScale", "intervalScale", "positionTemporalUnit",
144
156
  "offsetChannel", "offsetField", "offsetFieldType", "offsetScale",
145
157
  "offsetPaddingInner", "offsetPaddingOuter",
146
- "capSize", "stroke", "strokeWidth", "strokeDash", "opacity"
158
+ "capSize", "stroke", "strokeWidth", "strokeDash", "opacity",
159
+ ...STROKE_STYLE_PROPERTIES
147
160
  ], "createErrorBarCap");
148
161
  if (!["vertical", "horizontal"].includes(args.orientation)) {
149
162
  throw new Error(`Unsupported error-bar orientation "${args.orientation}".`);
@@ -161,7 +174,13 @@ export const createErrorBarCap = action(
161
174
  const positionAction = vertical ? "encodeX" : "encodeY";
162
175
  const intervalAction = vertical ? "encodeY" : "encodeX";
163
176
  let next = this
164
- .createRuleMark({ id: args.id, data: args.data })
177
+ .createRuleMark({
178
+ id: args.id,
179
+ data: args.data,
180
+ ...Object.fromEntries(STROKE_STYLE_PROPERTIES.flatMap(property =>
181
+ Object.hasOwn(args, property) ? [[property, args[property]]] : []
182
+ ))
183
+ })
165
184
  [positionAction]({
166
185
  target: args.id,
167
186
  field: args.positionField,
@@ -247,7 +266,13 @@ export const createErrorBar = action(
247
266
  const resolved = resolveErrorBar(this, args);
248
267
  const appearance = resolveAppearance(args);
249
268
  let next = createResolvedIntervalData(this, resolved);
250
- next = next.createRuleMark({ id: resolved.id, data: resolved.dataId });
269
+ next = next.createRuleMark({
270
+ id: resolved.id,
271
+ data: resolved.dataId,
272
+ ...Object.fromEntries(STROKE_STYLE_PROPERTIES.flatMap(property =>
273
+ Object.hasOwn(appearance, property) ? [[property, appearance[property]]] : []
274
+ ))
275
+ });
251
276
  const positionAction = resolved.position.channel === "x" ? "encodeX" : "encodeY";
252
277
  const intervalAction = resolved.interval.channel === "x" ? "encodeX" : "encodeY";
253
278
  const secondaryAction = resolved.interval.channel === "x" ? "encodeX2" : "encodeY2";
@@ -303,7 +328,10 @@ export const createErrorBar = action(
303
328
  stroke: appearance.stroke,
304
329
  strokeWidth: appearance.strokeWidth,
305
330
  strokeDash: appearance.strokeDash,
306
- opacity: appearance.opacity
331
+ opacity: appearance.opacity,
332
+ ...Object.fromEntries(STROKE_STYLE_PROPERTIES.flatMap(property =>
333
+ Object.hasOwn(appearance, property) ? [[property, appearance[property]]] : []
334
+ ))
307
335
  });
308
336
  }
309
337
  }
@@ -31,15 +31,20 @@ import {
31
31
  releaseIntervalRevision,
32
32
  resolveIntervalOwner
33
33
  } from "../data/intervalEdit.js";
34
+ import {
35
+ requestedStrokeDetails,
36
+ STROKE_STYLE_PROPERTIES
37
+ } from "../../grammar/strokeStyle.js";
34
38
 
35
39
  export const ERROR_BAR_EDIT_OPTIONS = Object.freeze([
36
40
  "target", "data", "x", "y", "xOffset", "yOffset", "groupBy",
37
41
  "caps", "capSize", "stroke", "strokeWidth", "strokeDash", "opacity",
38
- "statistics"
42
+ "statistics", ...STROKE_STYLE_PROPERTIES
39
43
  ]);
40
44
 
41
45
  export const ERROR_BAR_APPEARANCE_OPTIONS = Object.freeze([
42
- "caps", "capSize", "stroke", "strokeWidth", "strokeDash", "opacity"
46
+ "caps", "capSize", "stroke", "strokeWidth", "strokeDash", "opacity",
47
+ ...STROKE_STYLE_PROPERTIES
43
48
  ]);
44
49
  const STATISTICS_OPTIONS = Object.freeze(["center", "extent", "method", "level"]);
45
50
  const EDIT_POLICY = Object.freeze({
@@ -81,7 +86,15 @@ export function resolveErrorBarAppearance(args, { defaults, operation }) {
81
86
  args.opacity ?? defaults.opacity,
82
87
  `${operation} opacity`
83
88
  );
84
- return { caps, capSize, stroke, strokeWidth, strokeDash, opacity };
89
+ return {
90
+ caps,
91
+ capSize,
92
+ stroke,
93
+ strokeWidth,
94
+ strokeDash,
95
+ opacity,
96
+ ...requestedStrokeDetails({ ...defaults, ...args }, operation)
97
+ };
85
98
  }
86
99
 
87
100
  const REMATERIALIZE_OPTIONS = Object.freeze(["id"]);
@@ -307,7 +320,8 @@ function capArgs(config, id, intervalField) {
307
320
  stroke: config.stroke,
308
321
  strokeWidth: config.strokeWidth,
309
322
  strokeDash: config.strokeDash,
310
- opacity: config.opacity
323
+ opacity: config.opacity,
324
+ ...requestedStrokeDetails(config, "Error bar")
311
325
  };
312
326
  }
313
327
 
@@ -323,6 +337,7 @@ function rematerializeRuleAppearance(program, id, config, fixedSpan) {
323
337
  strokeWidth: config.strokeWidth,
324
338
  strokeDash: config.strokeDash,
325
339
  opacity: config.opacity,
340
+ ...requestedStrokeDetails(config, "Error bar"),
326
341
  ...(fixedSpan === undefined ? {} : { fixedSpan })
327
342
  })
328
343
  .rematerializeRuleMark({ id });