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
@@ -4,6 +4,7 @@ import {
4
4
  isDiscretizedColorScaleType,
5
5
  isDiscretePositionScaleType,
6
6
  isTransformedScaleType,
7
+ normalizeSizeScaleDefinition,
7
8
  normalizeScaleDefinition,
8
9
  SCALE_ROLES,
9
10
  validateColorRange,
@@ -26,11 +27,20 @@ import {
26
27
  validateRadialRange,
27
28
  validateThetaRange
28
29
  } from "../../grammar/polar.js";
29
- import { normalizePositionScaleChannel } from "../../core/vocabulary.js";
30
+ import {
31
+ normalizePositionScaleChannel,
32
+ OFFSET_POSITION_CHANNELS
33
+ } from "../../core/vocabulary.js";
34
+ import { normalizeOffsetScalePolicy } from "../../grammar/bars/geometry.js";
35
+ import { resolveRequestedOffsetPolicy } from
36
+ "../../materialization/scales/policies/offset.js";
30
37
 
31
38
  export function resolveScaleConsumerChannel(consumers, id) {
32
39
  const channels = new Set(
33
- consumers.map(consumer => normalizePositionScaleChannel(consumer.channel))
40
+ consumers.map(consumer => {
41
+ const channel = normalizePositionScaleChannel(consumer.channel);
42
+ return channel === "stroke" ? "color" : channel;
43
+ })
34
44
  );
35
45
  if (channels.size > 1) {
36
46
  throw new Error(`Scale "${id}" cannot be shared across channels.`);
@@ -40,6 +50,7 @@ export function resolveScaleConsumerChannel(consumers, id) {
40
50
 
41
51
  function validateRangeForChannel(scale, channel, value) {
42
52
  if (value === "auto") return value;
53
+ if (channel === "size") return validateSizeRange(value);
43
54
  if (channel === "theta") return validateThetaRange(value);
44
55
  if (channel === "radius") return validateRadialRange(value);
45
56
  if (scale.type === "sequential") {
@@ -49,7 +60,7 @@ function validateRangeForChannel(scale, channel, value) {
49
60
  return validateDiscretizedColorRange(value);
50
61
  }
51
62
  if (scale.type !== "ordinal") return validateScaleRange(value);
52
- if (channel === "color") return validateColorRange(value);
63
+ if (["color", "stroke"].includes(channel)) return validateColorRange(value);
53
64
  if (channel === "shape") return validateShapeRange(value);
54
65
  if (channel === "strokeDash") return validateStrokeDashRange(value);
55
66
  if (channel === "size") return validateSizeRange(value);
@@ -66,7 +77,23 @@ function validateTypeTransition(scale, nextType, channel, consumers) {
66
77
  throw new Error(`Scale "${scale.id}" has a consumer incompatible with type "${nextType}".`);
67
78
  }
68
79
  for (const consumer of consumers) {
69
- validateContinuousColorConsumer(consumer.layer, consumer.encoding, { type: nextType });
80
+ validateContinuousColorConsumer(
81
+ consumer.layer,
82
+ consumer.encoding,
83
+ { type: nextType },
84
+ { channel: consumer.channel }
85
+ );
86
+ }
87
+ return;
88
+ }
89
+ if (nextType === "ordinal" && channel === "color") {
90
+ if (consumers.some(consumer =>
91
+ !["color", "stroke"].includes(consumer.channel) ||
92
+ !["nominal", "ordinal"].includes(consumer.encoding.fieldType)
93
+ )) {
94
+ throw new Error(
95
+ `Scale "${scale.id}" has a consumer incompatible with type "ordinal".`
96
+ );
70
97
  }
71
98
  return;
72
99
  }
@@ -127,23 +154,65 @@ function validateTypeTransition(scale, nextType, channel, consumers) {
127
154
  }
128
155
  }
129
156
 
130
- function normalizeDefinition(scale, channel, consumers, patch) {
157
+ function normalizeDefinition(program, scale, channel, consumers, patch) {
131
158
  if (scale.radialMapping !== undefined && Object.hasOwn(patch, "radialMapping") &&
132
159
  patch.radialMapping === undefined && consumers.some(consumer => consumer.encoding.aggregate !== undefined)) {
133
160
  throw new Error("Remove measured radius encodings before clearing their scale mapping.");
134
161
  }
162
+ if (channel === "size") {
163
+ if (consumers.some(consumer =>
164
+ consumer.layer.mark?.type !== "point" ||
165
+ consumer.encoding.fieldType !== "quantitative"
166
+ )) {
167
+ throw new Error(
168
+ `Scale "${scale.id}" has a consumer incompatible with size mapping.`
169
+ );
170
+ }
171
+ return normalizeSizeScaleDefinition({ previous: scale, patch });
172
+ }
135
173
  const type = patch.type ?? scale.type;
136
174
  validateTypeTransition(scale, type, channel, consumers);
175
+ const offset = OFFSET_POSITION_CHANNELS.includes(channel);
176
+ if (offset && Object.hasOwn(patch, "range")) {
177
+ throw new Error(
178
+ `${channel} scale range is derived from its parent categorical slot.`
179
+ );
180
+ }
181
+ if (offset && Object.hasOwn(patch, "unknown")) {
182
+ throw new Error(`${channel} scale unknown is not supported for offset positions.`);
183
+ }
137
184
  if (type !== scale.type && scale.domain !== "auto" && !Object.hasOwn(patch, "domain") &&
138
185
  (["quantile", "threshold"].includes(type) || ["quantile", "threshold"].includes(scale.type))) {
139
186
  throw new Error("Color scale type transition requires an explicit domain when its meaning changes.");
140
187
  }
188
+ const offsetPolicy = offset
189
+ ? normalizeOffsetScalePolicy(
190
+ patch,
191
+ resolveRequestedOffsetPolicy({
192
+ scale,
193
+ consumers,
194
+ markConfigs: program.markConfigs,
195
+ id: scale.id,
196
+ channel
197
+ }),
198
+ channel
199
+ )
200
+ : undefined;
201
+ const normalizedPatch = offset
202
+ ? {
203
+ ...Object.fromEntries(Object.entries(patch).filter(
204
+ ([property]) => property !== "padding"
205
+ )),
206
+ ...offsetPolicy
207
+ }
208
+ : patch;
141
209
  const definition = normalizeScaleDefinition({
142
210
  type,
143
211
  previous: scale,
144
- patch,
212
+ patch: normalizedPatch,
145
213
  retainCoreOnTypeChange: true,
146
214
  retainCompatibleOnTypeChange: true,
215
+ allowOrdinalBandParameters: offset,
147
216
  validateDomain: (scaleType, value) =>
148
217
  isDiscretizedColorScaleType(scaleType)
149
218
  ? validateDiscretizedColorDomain(scaleType, value)
@@ -162,13 +231,14 @@ function normalizeDefinition(scale, channel, consumers, patch) {
162
231
  }
163
232
  validateSequentialMidpoint(definition.midpoint, type, definition.domain);
164
233
  if (definition.midpoint !== undefined && consumers.some(
165
- consumer => consumer.channel !== "color" || consumer.encoding.fieldType !== "quantitative"
234
+ consumer => !["color", "stroke"].includes(consumer.channel) ||
235
+ consumer.encoding.fieldType !== "quantitative"
166
236
  )) {
167
237
  throw new Error("Scale midpoint requires quantitative color consumers.");
168
238
  }
169
239
  const typeChanged = type !== scale.type;
170
- const unknown = Object.hasOwn(patch, "unknown")
171
- ? patch.unknown
240
+ const unknown = Object.hasOwn(normalizedPatch, "unknown")
241
+ ? normalizedPatch.unknown
172
242
  : typeChanged ? undefined : scale.unknown;
173
243
  if (unknown !== undefined) {
174
244
  if (consumers.some(consumer => consumer.layer.mark?.type !== "point")) {
@@ -188,7 +258,7 @@ export function prepareScaleEdit(program, scale, channel, consumers, args) {
188
258
  }
189
259
  if (
190
260
  hasPalette &&
191
- channel !== "color" &&
261
+ !["color", "stroke"].includes(channel) &&
192
262
  scale.type !== "sequential" &&
193
263
  !isDiscretizedColorScaleType(scale.type)
194
264
  ) {
@@ -200,5 +270,5 @@ export function prepareScaleEdit(program, scale, channel, consumers, args) {
200
270
  ...Object.entries(args).filter(([key]) => key !== "palette"),
201
271
  ["range", { palette: args.palette }]
202
272
  ]);
203
- return normalizeDefinition(scale, channel, consumers, patch);
273
+ return normalizeDefinition(program, scale, channel, consumers, patch);
204
274
  }
@@ -2,9 +2,15 @@ import { createScale } from "./create.js";
2
2
  import { editScale } from "./edit.js";
3
3
  import { rematerializeScale } from "./materialize.js";
4
4
  import { setQuantitativeColorScale } from "./quantitativeColor.js";
5
+ import { registerChannelScaleActions } from "./channels.js";
6
+ import { editParallelScale } from "./parallel.js";
7
+ import { registerOffsetScaleActions } from "./offset.js";
5
8
 
6
9
  export function registerScaleActions(ProgramClass) {
7
10
  ProgramClass.prototype.editScale = editScale;
11
+ ProgramClass.prototype.editParallelScale = editParallelScale;
12
+ registerOffsetScaleActions(ProgramClass);
13
+ registerChannelScaleActions(ProgramClass);
8
14
  registerBasicScaleActions(ProgramClass);
9
15
  }
10
16
 
@@ -52,9 +52,10 @@ export const rematerializeScale = action(
52
52
  ) {
53
53
  continue;
54
54
  }
55
+ const consumerChannel = consumer.channel;
55
56
  const materializationMode = getScaleConsumerMaterializationMode(
56
57
  consumer.layer,
57
- channel
58
+ consumerChannel
58
59
  );
59
60
  if (materializationMode === "rematerialize") {
60
61
  if (args.marks !== false) {
@@ -71,8 +72,8 @@ export const rematerializeScale = action(
71
72
  }
72
73
  next = next.editGraphics({
73
74
  target: consumer.layer.id,
74
- property: channel === "color" ? "fill" : channel,
75
- value: mapScaleConsumerValues(values, resolvedScale, channel)
75
+ property: consumerChannel === "color" ? "fill" : consumerChannel,
76
+ value: mapScaleConsumerValues(values, resolvedScale, consumerChannel)
76
77
  });
77
78
  }
78
79
 
@@ -0,0 +1,48 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateUserId } from "../../core/identifiers.js";
3
+ import { validateOptionObject } from "../../core/validation.js";
4
+ import { requireLayer } from "../../selectors/layers.js";
5
+ import { requireSemanticScale } from "../../selectors/scales.js";
6
+
7
+ const EDITABLE = Object.freeze([
8
+ "domain", "reverse", "padding", "paddingInner", "paddingOuter", "align"
9
+ ]);
10
+
11
+ function createOffsetScaleEditor(channel) {
12
+ const operation = channel === "xOffset"
13
+ ? "editXOffsetScale"
14
+ : "editYOffsetScale";
15
+ return action(
16
+ {
17
+ op: operation,
18
+ description: `Edit the nested ${channel} scale for one mark.`
19
+ },
20
+ function (args = {}) {
21
+ validateOptionObject(args, ["target", ...EDITABLE], operation);
22
+ const target = validateUserId(args.target, `${operation} target`);
23
+ if (!EDITABLE.some(property => Object.hasOwn(args, property))) {
24
+ throw new Error(`${operation} requires at least one editable property.`);
25
+ }
26
+ const layer = requireLayer(this, target, `${operation} target "${target}"`);
27
+ const scaleId = layer.encoding?.[channel]?.scale;
28
+ if (typeof scaleId !== "string") {
29
+ throw new Error(
30
+ `${operation} target "${target}" has no ${channel} scale.`
31
+ );
32
+ }
33
+ requireSemanticScale(this, scaleId);
34
+ const patch = Object.fromEntries(Object.entries(args).filter(
35
+ ([property]) => property !== "target"
36
+ ));
37
+ return this.editScale({ id: scaleId, ...patch });
38
+ }
39
+ );
40
+ }
41
+
42
+ export const editXOffsetScale = createOffsetScaleEditor("xOffset");
43
+ export const editYOffsetScale = createOffsetScaleEditor("yOffset");
44
+
45
+ export function registerOffsetScaleActions(ProgramClass) {
46
+ ProgramClass.prototype.editXOffsetScale = editXOffsetScale;
47
+ ProgramClass.prototype.editYOffsetScale = editYOffsetScale;
48
+ }
@@ -0,0 +1,71 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateUserId } from "../../core/identifiers.js";
3
+ import { validateOptionObject } from "../../core/validation.js";
4
+ import { findCoordinate } from "../../selectors/coordinates.js";
5
+ import { requireLayer } from "../../selectors/layers.js";
6
+ import { requireSemanticScale } from "../../selectors/scales.js";
7
+
8
+ const QUANTITATIVE_OPTIONS = Object.freeze([
9
+ "type", "domain", "range", "nice", "zero", "clamp", "reverse",
10
+ "base", "exponent", "constant", "unknown"
11
+ ]);
12
+ const ORDINAL_OPTIONS = Object.freeze([
13
+ "type", "domain", "range", "reverse", "padding", "align", "unknown"
14
+ ]);
15
+ const ALL_OPTIONS = Object.freeze([
16
+ "target", "dimension",
17
+ ...new Set([...QUANTITATIVE_OPTIONS, ...ORDINAL_OPTIONS])
18
+ ]);
19
+
20
+ function resolveParallelDimension(program, args) {
21
+ validateOptionObject(args, ALL_OPTIONS, "editParallelScale");
22
+ const target = validateUserId(args.target, "Parallel scale target");
23
+ if (typeof args.dimension !== "string" || args.dimension.length === 0) {
24
+ throw new TypeError("Parallel scale dimension must be a non-empty field string.");
25
+ }
26
+ const layer = requireLayer(program, target, `Parallel scale target "${target}"`);
27
+ if (
28
+ findCoordinate(program, layer.coordinate)?.type !== "parallel" ||
29
+ !Array.isArray(layer.encoding?.parallel?.dimensions)
30
+ ) {
31
+ throw new Error(`Parallel scale target "${target}" is not a Parallel layer.`);
32
+ }
33
+ const matches = layer.encoding.parallel.dimensions.filter(
34
+ dimension => dimension.field === args.dimension
35
+ );
36
+ if (matches.length !== 1) {
37
+ throw new Error(
38
+ matches.length === 0
39
+ ? `Parallel scale target "${target}" has no dimension field "${args.dimension}".`
40
+ : `Parallel scale dimension field "${args.dimension}" is ambiguous on target "${target}".`
41
+ );
42
+ }
43
+ const dimension = matches[0];
44
+ requireSemanticScale(program, dimension.scale);
45
+ return dimension;
46
+ }
47
+
48
+ export const editParallelScale = action(
49
+ {
50
+ op: "editParallelScale",
51
+ description: "Edit a Parallel-coordinate dimension scale by field identity."
52
+ },
53
+ function (args = {}) {
54
+ const dimension = resolveParallelDimension(this, args);
55
+ const editable = dimension.fieldType === "quantitative"
56
+ ? QUANTITATIVE_OPTIONS
57
+ : ORDINAL_OPTIONS;
58
+ validateOptionObject(
59
+ args,
60
+ ["target", "dimension", ...editable],
61
+ "editParallelScale"
62
+ );
63
+ if (!editable.some(property => Object.hasOwn(args, property))) {
64
+ throw new Error("editParallelScale requires at least one editable property.");
65
+ }
66
+ const patch = Object.fromEntries(Object.entries(args).filter(
67
+ ([property]) => !["target", "dimension"].includes(property)
68
+ ));
69
+ return this.editScale({ id: dimension.scale, ...patch });
70
+ }
71
+ );
@@ -1,39 +1,140 @@
1
1
  import { resolveScaleRange } from "../../grammar/scales/index.js";
2
+ import { resolvePolarFrame } from "../../grammar/polar.js";
2
3
  import { resolveArcAutoPositionRange, validateMeasuredRadiusConsumers } from "../../materialization/scales/policies/arc.js";
3
4
  import { findSemanticScale } from "../../selectors/scales.js";
4
5
  import { normalizePositionScaleChannel } from "../../core/vocabulary.js";
5
6
  import { resolveGraphicBounds } from "../../layout/canvas.js";
7
+ import {
8
+ resolveDataAspectScalePair,
9
+ resolveScaleConsumerBounds
10
+ } from "../../materialization/coordinateBounds.js";
11
+ import { requireCoordinate } from "../../selectors/coordinates.js";
6
12
  import { resolveScaleMaterialization } from "../../materialization/scales/resolve.js";
7
13
  import { findScale, findScaleConsumers, resolveConsumerCategoryOrder,
8
14
  resolveConsumerValues, resolveSeriesLayoutScaleValues } from "./consumers/index.js";
9
15
 
10
- export function resolveScalePreview(program, id) {
16
+ function resolveScalePreviewAtBounds(program, id, bounds) {
11
17
  const scale = findScale(program, id);
12
18
  const consumers = findScaleConsumers(program, id);
13
19
  if (consumers.length === 0) throw new Error(`Scale "${id}" has no supported consumers.`);
14
- const channels = new Set(consumers.map(consumer => normalizePositionScaleChannel(consumer.channel)));
15
- if (channels.size !== 1) throw new Error(`Scale "${id}" cannot be shared across channels.`);
16
- const channel = channels.values().next().value;
20
+ const consumerChannels = new Set(consumers.map(
21
+ consumer => normalizePositionScaleChannel(consumer.channel)
22
+ ));
23
+ const families = new Set([...consumerChannels].map(
24
+ channel => channel === "stroke" ? "color" : channel
25
+ ));
26
+ if (families.size !== 1) {
27
+ throw new Error(`Scale "${id}" cannot be shared across channels.`);
28
+ }
29
+ const channel = consumerChannels.size === 1
30
+ ? consumerChannels.values().next().value
31
+ : families.values().next().value;
17
32
  const valuesByConsumer = consumers.map(consumer => {
18
- if (program.markConfigs?.[consumer.layer.id]?.markFilter?.empty === true) {
19
- return { consumer, values: [], categoryOrder: undefined, seriesLayout: undefined };
33
+ if (
34
+ program.markConfigs?.[consumer.layer.id]?.markFilter?.empty === true ||
35
+ program.markConfigs?.[consumer.layer.id]?.statisticalReference !== undefined
36
+ ) {
37
+ return {
38
+ consumer,
39
+ values: [],
40
+ categoryOrder: undefined,
41
+ seriesLayout: undefined,
42
+ ...(program.markConfigs?.[consumer.layer.id]?.statisticalReference === undefined
43
+ ? {}
44
+ : { scalePolicy: false })
45
+ };
20
46
  }
21
47
  return { consumer,
22
48
  values: resolveConsumerValues(program, consumer),
23
49
  categoryOrder: resolveConsumerCategoryOrder(program, consumer),
24
50
  seriesLayout: resolveSeriesLayoutScaleValues(program, consumer) };
25
51
  });
52
+ const polarFrame = channel === "radius"
53
+ ? resolveSharedRadiusFrame(program, consumers, bounds)
54
+ : undefined;
26
55
  const resolvedScale = resolveScaleMaterialization({ id, scale, channel, consumers, valuesByConsumer,
27
- bounds: ["color", "strokeDash", "strokeWidth", "shape", "size", "opacity", "xOffset", "yOffset"].includes(channel)
28
- ? undefined : resolveGraphicBounds(program),
29
- resolvedScales: program.resolvedScales, markConfigs: program.markConfigs, thetaScales: scale.radialMapping === undefined ? undefined : Object.fromEntries(consumers.map(({ layer }) =>
56
+ bounds: ["color", "stroke", "strokeDash", "strokeWidth", "shape", "size", "opacity", "xOffset", "yOffset"].includes(channel)
57
+ ? undefined : bounds,
58
+ resolvedScales: program.resolvedScales, markConfigs: program.markConfigs, polarFrame,
59
+ thetaScales: scale.radialMapping === undefined ? undefined : Object.fromEntries(consumers.map(({ layer }) =>
30
60
  [layer.id, findSemanticScale(program, layer.encoding?.theta?.scale)])) });
31
61
  return { channel, consumers, valuesByConsumer, resolvedScale };
32
62
  }
33
63
 
64
+ function resolveSharedRadiusFrame(program, consumers, bounds) {
65
+ const coordinateIds = [...new Set(consumers.map(({ layer }) => layer.coordinate))];
66
+ if (coordinateIds.some(id => id === undefined)) {
67
+ throw new Error("Radius scale consumers require stored Polar coordinates.");
68
+ }
69
+ const frames = coordinateIds.map(id => {
70
+ const coordinate = requireCoordinate(program, id);
71
+ if (coordinate.type !== "polar") {
72
+ throw new Error("Radius scale consumers require Polar coordinates.");
73
+ }
74
+ return resolvePolarFrame(bounds, coordinate.polarFrame);
75
+ });
76
+ if (frames.slice(1).some(
77
+ frame => Math.abs(frame.availableRadius - frames[0].availableRadius) > 1e-9
78
+ )) {
79
+ throw new Error(
80
+ "A radius scale cannot be shared across coordinates with different Polar frames."
81
+ );
82
+ }
83
+ return frames[0];
84
+ }
85
+
86
+ function dataAspectScaleIds(program) {
87
+ const ids = new Set();
88
+ for (const coordinate of program.semanticSpec.coordinates) {
89
+ if (coordinate.aspect?.mode !== "data") continue;
90
+ const pair = resolveDataAspectScalePair(program, coordinate);
91
+ ids.add(pair.x);
92
+ ids.add(pair.y);
93
+ }
94
+ return ids;
95
+ }
96
+
97
+ export function resolveScalePreview(program, id) {
98
+ const requestedConsumers = findScaleConsumers(program, id);
99
+ const positionChannels = new Set([
100
+ "x", "y", "x2", "y2", "theta", "radius"
101
+ ]);
102
+ const positional = requestedConsumers.some(consumer =>
103
+ consumer.role === "parallelDimension" ||
104
+ positionChannels.has(consumer.channel)
105
+ );
106
+ const allocated = positional ? resolveGraphicBounds(program) : undefined;
107
+ const provisional = resolveScalePreviewAtBounds(program, id, allocated);
108
+ if (!positional) return provisional;
109
+ const resolvedScales = {
110
+ ...program.resolvedScales,
111
+ [id]: provisional.resolvedScale
112
+ };
113
+ for (const scaleId of dataAspectScaleIds(program)) {
114
+ if (scaleId === id) continue;
115
+ resolvedScales[scaleId] = resolveScalePreviewAtBounds(
116
+ program,
117
+ scaleId,
118
+ allocated
119
+ ).resolvedScale;
120
+ }
121
+ const bounds = resolveScaleConsumerBounds(
122
+ program,
123
+ provisional.consumers,
124
+ { resolvedScales }
125
+ );
126
+ return resolveScalePreviewAtBounds(program, id, bounds);
127
+ }
128
+
34
129
  export function validatePendingMeasuredScale(program, scale, consumers) {
130
+ const bounds = resolveScaleConsumerBounds(program, consumers);
35
131
  const range = resolveArcAutoPositionRange({ consumers, scale, channel: "radius",
36
- range: resolveScaleRange(scale.range, "radius", resolveGraphicBounds(program)), markConfigs: program.markConfigs });
132
+ range: resolveScaleRange(
133
+ scale.range,
134
+ "radius",
135
+ bounds,
136
+ resolveSharedRadiusFrame(program, consumers, bounds)
137
+ ), markConfigs: program.markConfigs });
37
138
  validateMeasuredRadiusConsumers({ scale, domain: scale.domain, range, consumers,
38
139
  markConfigs: program.markConfigs, thetaScales: {} });
39
140
  }