ggaction 0.0.4 → 0.0.6

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 (169) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +35 -13
  3. package/package.json +18 -7
  4. package/src/actions/boxPlots/create.js +13 -7
  5. package/src/actions/boxPlots/edit.js +484 -36
  6. package/src/actions/boxPlots/materialize.js +3 -0
  7. package/src/actions/boxPlots/options.js +11 -4
  8. package/src/actions/boxPlots/resolve.js +10 -2
  9. package/src/actions/charts/bar.js +50 -0
  10. package/src/actions/charts/heatmap.js +205 -0
  11. package/src/actions/charts/histogram.js +58 -0
  12. package/src/actions/charts/index.js +15 -0
  13. package/src/actions/charts/line.js +69 -0
  14. package/src/actions/charts/parallel.js +76 -0
  15. package/src/actions/charts/scatter.js +56 -0
  16. package/src/actions/charts/shared.js +121 -0
  17. package/src/actions/composition/actions.js +13 -3
  18. package/src/actions/coordinates/parallel.js +50 -0
  19. package/src/actions/data/bin2d.js +260 -0
  20. package/src/actions/data/density.js +54 -13
  21. package/src/actions/data/derived.js +22 -0
  22. package/src/actions/data/filter.js +5 -0
  23. package/src/actions/data/gradientProfile.js +65 -0
  24. package/src/actions/data/horizon.js +71 -0
  25. package/src/actions/data/index.js +34 -2
  26. package/src/actions/data/intervalEdit.js +76 -0
  27. package/src/actions/data/window.js +50 -0
  28. package/src/actions/distributions/revision.js +326 -0
  29. package/src/actions/encodings/appearance.js +35 -1
  30. package/src/actions/encodings/color/index.js +17 -1
  31. package/src/actions/encodings/color/layout.js +8 -0
  32. package/src/actions/encodings/density/resolve.js +109 -0
  33. package/src/actions/encodings/density.js +330 -90
  34. package/src/actions/encodings/horizon/resolve.js +283 -0
  35. package/src/actions/encodings/horizon.js +364 -0
  36. package/src/actions/encodings/index.js +8 -0
  37. package/src/actions/encodings/parallel.js +115 -0
  38. package/src/actions/encodings/pathOrder.js +131 -0
  39. package/src/actions/encodings/position/apply.js +73 -4
  40. package/src/actions/encodings/position/index.js +15 -2
  41. package/src/actions/encodings/position/policies/arc.js +27 -4
  42. package/src/actions/encodings/position/policies/area.js +33 -2
  43. package/src/actions/encodings/position/policies/index.js +7 -2
  44. package/src/actions/encodings/position/policies/line.js +18 -2
  45. package/src/actions/encodings/position/resolve.js +24 -2
  46. package/src/actions/encodings/ranged.js +3 -1
  47. package/src/actions/encodings/remove.js +268 -0
  48. package/src/actions/encodings/ruleAppearance.js +88 -10
  49. package/src/actions/errorBands/edit.js +121 -13
  50. package/src/actions/errorBars/edit.js +45 -8
  51. package/src/actions/errorBars/options.js +7 -1
  52. package/src/actions/facets/actions.js +137 -3
  53. package/src/actions/facets/derive.js +33 -3
  54. package/src/actions/facets/replay.js +7 -36
  55. package/src/actions/gradientPlots/components.js +177 -0
  56. package/src/actions/gradientPlots/create.js +96 -0
  57. package/src/actions/gradientPlots/edit.js +501 -0
  58. package/src/actions/gradientPlots/index.js +24 -0
  59. package/src/actions/gradientPlots/materialize.js +287 -0
  60. package/src/actions/gradientPlots/options.js +120 -0
  61. package/src/actions/gradientPlots/paint.js +107 -0
  62. package/src/actions/gradientPlots/rebind.js +51 -0
  63. package/src/actions/gradientPlots/resolve.js +81 -0
  64. package/src/actions/guides/applicability.js +44 -17
  65. package/src/actions/guides/axes/axes.js +37 -3
  66. package/src/actions/guides/axes/edit.js +129 -47
  67. package/src/actions/guides/axes/index.js +2 -0
  68. package/src/actions/guides/axes/labels.js +11 -6
  69. package/src/actions/guides/axes/parallel/resolve.js +86 -0
  70. package/src/actions/guides/axes/parallel.js +228 -0
  71. package/src/actions/guides/axes/titles.js +11 -8
  72. package/src/actions/guides/guides.js +60 -5
  73. package/src/actions/guides/legends/categorical/actions.js +67 -19
  74. package/src/actions/guides/legends/continuous/common.js +3 -3
  75. package/src/actions/guides/legends/edit.js +63 -0
  76. package/src/actions/guides/legends/index.js +2 -0
  77. package/src/actions/guides/legends/remove.js +124 -30
  78. package/src/actions/guides/legends/strokeWidth.js +198 -0
  79. package/src/actions/index.js +6 -0
  80. package/src/actions/marks/arc/actions.js +46 -14
  81. package/src/actions/marks/area/actions.js +60 -136
  82. package/src/actions/marks/area/materialize.js +163 -0
  83. package/src/actions/marks/index.js +5 -1
  84. package/src/actions/marks/line/actions.js +99 -80
  85. package/src/actions/marks/line/materialize.js +146 -0
  86. package/src/actions/marks/point/create.js +76 -0
  87. package/src/actions/marks/point/edit.js +97 -0
  88. package/src/actions/marks/point/index.js +11 -1
  89. package/src/actions/marks/point/jitter.js +72 -0
  90. package/src/actions/marks/point/{actions.js → materialize.js} +183 -172
  91. package/src/actions/marks/rect/actions.js +3 -0
  92. package/src/actions/marks/remove.js +13 -2
  93. package/src/actions/marks/rule/actions.js +29 -4
  94. package/src/actions/marks/text/actions.js +19 -4
  95. package/src/actions/marks/text/index.js +7 -1
  96. package/src/actions/marks/text/layout.js +321 -0
  97. package/src/actions/primitives/semanticValidation/dataset.js +22 -0
  98. package/src/actions/primitives/semanticValidation/guide.js +43 -0
  99. package/src/actions/primitives/semanticValidation/index.js +23 -0
  100. package/src/actions/primitives/semanticValidation/layer.js +74 -0
  101. package/src/actions/primitives/semanticValidation/scale.js +74 -0
  102. package/src/actions/primitives/semanticValidation/shared.js +5 -0
  103. package/src/actions/primitives/semanticValue.js +1 -204
  104. package/src/actions/regression/edit.js +247 -88
  105. package/src/actions/scales/consumers/common.js +18 -0
  106. package/src/actions/scales/consumers/families.js +14 -3
  107. package/src/actions/scales/definitions.js +30 -0
  108. package/src/actions/scales/edit.js +6 -204
  109. package/src/actions/scales/editPolicy.js +214 -0
  110. package/src/actions/scales/materialize.js +1 -1
  111. package/src/actions/scales/patch.js +26 -0
  112. package/src/actions/selection/actions.js +150 -5
  113. package/src/actions/selection/index.js +6 -0
  114. package/src/actions/violinPlots/create.js +198 -0
  115. package/src/actions/violinPlots/index.js +5 -0
  116. package/src/core/textMetrics.js +47 -0
  117. package/src/core/vocabulary.js +14 -2
  118. package/src/grammar/arcs.js +49 -12
  119. package/src/grammar/areaSeries.js +41 -5
  120. package/src/grammar/bin2d.js +333 -0
  121. package/src/grammar/categoricalDensity.js +192 -0
  122. package/src/grammar/coordinates.js +1 -1
  123. package/src/grammar/curveCommands.js +3 -1
  124. package/src/grammar/density.js +252 -26
  125. package/src/grammar/facets/dependencies.js +3 -11
  126. package/src/grammar/facets/index.js +3 -1
  127. package/src/grammar/gradientProfile.js +213 -0
  128. package/src/grammar/horizon.js +455 -0
  129. package/src/grammar/jitter.js +197 -0
  130. package/src/grammar/lineSeries.js +69 -0
  131. package/src/grammar/paint.js +88 -0
  132. package/src/grammar/parallelCoordinates.js +213 -0
  133. package/src/grammar/pathOrder.js +35 -0
  134. package/src/grammar/pointShapes.js +24 -0
  135. package/src/grammar/rules.js +1 -0
  136. package/src/grammar/scales/appearance.js +21 -0
  137. package/src/grammar/schemas/concreteGraphic.js +11 -1
  138. package/src/grammar/schemas/graphicBounds.js +19 -37
  139. package/src/grammar/schemas/semanticPath.js +17 -2
  140. package/src/grammar/transforms.js +100 -11
  141. package/src/grammar/window.js +339 -0
  142. package/src/layout/labels.js +270 -0
  143. package/src/layout/text.js +4 -1
  144. package/src/layout/title.js +10 -20
  145. package/src/materialization/dataProvenance.js +56 -2
  146. package/src/materialization/dependencies.js +9 -14
  147. package/src/materialization/guides/resources.js +74 -24
  148. package/src/materialization/horizon.js +29 -0
  149. package/src/materialization/layout.js +15 -0
  150. package/src/materialization/marks/capabilities.js +21 -1
  151. package/src/materialization/marks/index.js +3 -0
  152. package/src/materialization/marks/pathOrder.js +71 -0
  153. package/src/materialization/marks/policies.js +6 -2
  154. package/src/materialization/scaleGuideDependencies.js +8 -0
  155. package/src/materialization/scales/resolve.js +2 -0
  156. package/src/materialization/selection/items/path.js +27 -1
  157. package/src/materialization/selection/items/point.js +1 -5
  158. package/src/materialization/selection/items/rect.js +21 -0
  159. package/src/materialization/selection/styles.js +20 -9
  160. package/src/materialization/text.js +12 -4
  161. package/src/renderers/canvas/fill.js +33 -0
  162. package/src/renderers/canvas/index.js +7 -0
  163. package/src/renderers/canvas/path.js +7 -4
  164. package/src/renderers/canvas/rect.js +9 -3
  165. package/src/selectors/datasets.js +4 -0
  166. package/src/theme/defaults.js +4 -0
  167. package/types/extension.d.ts +6 -0
  168. package/types/index.d.ts +77 -0
  169. package/types/program.d.ts +703 -17
@@ -13,6 +13,7 @@ import {
13
13
  validateShapeRange,
14
14
  validateSizeRange,
15
15
  validateStrokeDashRange,
16
+ validateStrokeWidthRange,
16
17
  validateSequentialColorRange,
17
18
  validateDiscretizedColorDomain,
18
19
  validateDiscretizedColorRange,
@@ -314,6 +315,35 @@ export function resolveOpacityScaleDefinition(program, options) {
314
315
  return withScaleUnknown(scale, { ...existing, ...options }, "opacity");
315
316
  }
316
317
 
318
+ export function resolveStrokeWidthScaleDefinition(program, options) {
319
+ optionsObject(options);
320
+ validateKeys(options, OPACITY_OPTIONS, "scale");
321
+ const id = validateUserId(options.id ?? "strokeWidth", "Scale id");
322
+ const existing = findSemanticScale(program, id);
323
+ const type = options.type ?? existing?.type ?? "linear";
324
+ validateScaleTypeForRole(type, SCALE_ROLES.quantitativePosition);
325
+ for (const property of ["nice", "zero", "clamp", "reverse"]) {
326
+ if (options[property] !== undefined && typeof options[property] !== "boolean") {
327
+ throw new TypeError(`Scale ${property} must be a boolean.`);
328
+ }
329
+ }
330
+ const domain = validateScaleDomain(options.domain ?? existing?.domain ?? "auto");
331
+ if (domain !== "auto" && domain.some(value => value < 0)) {
332
+ throw new RangeError("StrokeWidth scale domain cannot contain negative values.");
333
+ }
334
+ const scale = {
335
+ id,
336
+ type,
337
+ domain,
338
+ range: validateStrokeWidthRange(options.range ?? existing?.range ?? "auto")
339
+ };
340
+ for (const property of ["nice", "zero", "clamp", "reverse", "base", "exponent", "constant"]) {
341
+ const value = options[property] ?? existing?.[property];
342
+ if (value !== undefined) scale[property] = value;
343
+ }
344
+ return scale;
345
+ }
346
+
317
347
  export function resolveOffsetScaleDefinition(program, options, channel = "xOffset") {
318
348
  optionsObject(options);
319
349
  validateKeys(options, [...BASE_OPTIONS, "unknown"], "scale");
@@ -1,31 +1,6 @@
1
1
  import { action } from "../../core/action.js";
2
2
  import { validateUserId } from "../../core/identifiers.js";
3
3
  import { validateOptionObject } from "../../core/validation.js";
4
- import {
5
- isTransformedScaleType,
6
- SCALE_ROLES,
7
- validateColorRange,
8
- validateDiscretizedColorDomain,
9
- validateDiscretizedColorRange,
10
- validateOrdinalDomain,
11
- validateScaleDomain,
12
- validateScaleRange,
13
- validateScaleType,
14
- validateScaleTypeForRole,
15
- validateShapeRange,
16
- validateSizeRange,
17
- validateStrokeDashRange,
18
- validateSequentialColorRange,
19
- validateScaleUnknown,
20
- isDiscretizedColorScaleType,
21
- isDiscretePositionScaleType,
22
- hasOrdinalDomain,
23
- normalizeScaleDefinition
24
- } from "../../grammar/scales/index.js";
25
- import {
26
- validateRadialRange,
27
- validateThetaRange
28
- } from "../../grammar/polar.js";
29
4
  import { getMarkMaterializationStep } from "../../materialization/marks/index.js";
30
5
  import {
31
6
  applyMaterializationPlan
@@ -35,7 +10,10 @@ import {
35
10
  requireSemanticScale
36
11
  } from "../../selectors/scales.js";
37
12
  import { findScaleConsumers } from "./consumers/index.js";
38
- import { normalizePositionScaleChannel } from "../../core/vocabulary.js";
13
+ import {
14
+ prepareScaleEdit,
15
+ resolveScaleConsumerChannel
16
+ } from "./editPolicy.js";
39
17
 
40
18
  const OPTIONS = Object.freeze([
41
19
  "id", "type", "domain", "range", "nice", "zero", "clamp", "reverse",
@@ -67,162 +45,6 @@ function resolveScaleId(program, requested) {
67
45
  );
68
46
  }
69
47
 
70
- function resolveChannel(consumers, id) {
71
- const channels = new Set(
72
- consumers.map(consumer => normalizePositionScaleChannel(consumer.channel))
73
- );
74
- if (channels.size > 1) {
75
- throw new Error(`Scale "${id}" cannot be shared across channels.`);
76
- }
77
- return channels.values().next().value;
78
- }
79
-
80
- function validateRangeForChannel(scale, channel, value) {
81
- if (value === "auto") return value;
82
- if (channel === "theta") return validateThetaRange(value);
83
- if (channel === "radius") return validateRadialRange(value);
84
- if (scale.type === "sequential") {
85
- return validateSequentialColorRange(value);
86
- }
87
- if (isDiscretizedColorScaleType(scale.type)) {
88
- return validateDiscretizedColorRange(value);
89
- }
90
- if (scale.type !== "ordinal") return validateScaleRange(value);
91
- if (channel === "color") return validateColorRange(value);
92
- if (channel === "shape") return validateShapeRange(value);
93
- if (channel === "strokeDash") return validateStrokeDashRange(value);
94
- if (channel === "size") return validateSizeRange(value);
95
- return validateScaleRange(value);
96
- }
97
-
98
- function validateTypeTransition(scale, nextType, channel, consumers) {
99
- if (nextType === scale.type) return;
100
- validateScaleType(nextType);
101
- if (consumers.length === 0) return;
102
- if (nextType === "sequential" || isDiscretizedColorScaleType(nextType)) {
103
- const discretized = isDiscretizedColorScaleType(nextType);
104
- if (channel !== "color" || consumers.some(consumer =>
105
- ["nominal", "ordinal"].includes(consumer.encoding.fieldType) ||
106
- (discretized && (
107
- consumer.encoding.fieldType !== "quantitative" ||
108
- consumer.layer.mark?.type !== "point"
109
- )) ||
110
- (!discretized && !["point", "bar"].includes(consumer.layer.mark?.type))
111
- )) {
112
- throw new Error(
113
- `Scale "${scale.id}" has a consumer incompatible with type "${nextType}".`
114
- );
115
- }
116
- return;
117
- }
118
- if (nextType === "time") {
119
- if (
120
- consumers.length > 0 &&
121
- (!["x", "y", "theta"].includes(channel) ||
122
- consumers.some(consumer => consumer.encoding.fieldType !== "temporal"))
123
- ) {
124
- throw new Error(
125
- `Scale "${scale.id}" has a consumer incompatible with type "time".`
126
- );
127
- }
128
- return;
129
- }
130
- const discrete = isDiscretePositionScaleType(nextType);
131
- if (discrete) {
132
- if (
133
- consumers.length > 0 &&
134
- (!["x", "y", "theta"].includes(channel) || consumers.some(consumer =>
135
- !["nominal", "ordinal"].includes(consumer.encoding.fieldType)
136
- ))
137
- ) {
138
- throw new Error(
139
- `Scale "${scale.id}" has a consumer incompatible with type "${nextType}".`
140
- );
141
- }
142
- if (
143
- nextType === "point" &&
144
- consumers.some(consumer => consumer.layer.mark?.type === "bar")
145
- ) {
146
- throw new Error("Point scales cannot provide bar bandwidth.");
147
- }
148
- return;
149
- }
150
- const quantitative = nextType === "linear" || isTransformedScaleType(nextType);
151
- if (
152
- !quantitative ||
153
- (consumers.length > 0 && !["x", "y", "theta", "radius"].includes(channel))
154
- ) {
155
- throw new Error(
156
- "editScale type transition currently requires a quantitative position scale."
157
- );
158
- }
159
- validateScaleTypeForRole(nextType, SCALE_ROLES.quantitativePosition);
160
- if (channel === "theta" && nextType !== "linear") {
161
- throw new Error("Theta quantitative position currently requires a linear scale.");
162
- }
163
- if (consumers.some(consumer => consumer.encoding.fieldType !== "quantitative")) {
164
- throw new Error(
165
- `Scale "${scale.id}" has a consumer incompatible with type "${nextType}".`
166
- );
167
- }
168
- }
169
-
170
- function validateLegendTypeTransition(program, scale, nextType) {
171
- if (nextType === scale.type) return;
172
- const legends = program.guideConfigs.legend ?? {};
173
- if (
174
- legends.gradient?.scale === scale.id &&
175
- nextType !== "sequential"
176
- ) {
177
- throw new Error(
178
- `Scale "${scale.id}" cannot change type while its gradient legend is active.`
179
- );
180
- }
181
- if (
182
- legends.interval?.scale === scale.id &&
183
- !isDiscretizedColorScaleType(nextType)
184
- ) {
185
- throw new Error(
186
- `Scale "${scale.id}" cannot change type while its interval legend is active.`
187
- );
188
- }
189
- }
190
-
191
- function normalizeDefinition(scale, channel, consumers, args) {
192
- const type = args.type ?? scale.type;
193
- validateTypeTransition(scale, type, channel, consumers);
194
- const definition = normalizeScaleDefinition({
195
- type,
196
- previous: scale,
197
- patch: args,
198
- retainCoreOnTypeChange: true,
199
- retainCompatibleOnTypeChange: true,
200
- validateDomain: (scaleType, value) =>
201
- isDiscretizedColorScaleType(scaleType)
202
- ? validateDiscretizedColorDomain(scaleType, value)
203
- : hasOrdinalDomain(scaleType)
204
- ? validateOrdinalDomain(value)
205
- : validateScaleDomain(value),
206
- validateRange: (scaleType, value) =>
207
- validateRangeForChannel({ type: scaleType }, channel, value)
208
- });
209
- const typeChanged = type !== scale.type;
210
- const unknown = Object.hasOwn(args, "unknown")
211
- ? args.unknown
212
- : typeChanged ? undefined : scale.unknown;
213
- if (unknown !== undefined) {
214
- if (consumers.some(consumer => consumer.layer.mark?.type !== "point")) {
215
- throw new Error(
216
- "Scale unknown currently requires row-owned point consumers."
217
- );
218
- }
219
- definition.unknown = consumers.length === 0
220
- ? unknown
221
- : validateScaleUnknown(channel, unknown);
222
- }
223
- return definition;
224
- }
225
-
226
48
  function sameValue(left, right) {
227
49
  return JSON.stringify(left) === JSON.stringify(right);
228
50
  }
@@ -254,31 +76,11 @@ export const editScale = action(
254
76
  if (!REQUESTED_CHANGES.some(property => Object.hasOwn(args, property))) {
255
77
  throw new Error("editScale requires at least one editable property.");
256
78
  }
257
- const hasPalette = Object.hasOwn(args, "palette");
258
- if (hasPalette && Object.hasOwn(args, "range")) {
259
- throw new Error("editScale cannot specify both palette and range.");
260
- }
261
-
262
79
  const id = resolveScaleId(this, args.id);
263
80
  const scale = requireSemanticScale(this, id);
264
81
  const consumers = findScaleConsumers(this, id);
265
- const channel = resolveChannel(consumers, id);
266
- if (
267
- hasPalette &&
268
- channel !== "color" &&
269
- scale.type !== "sequential" &&
270
- !isDiscretizedColorScaleType(scale.type)
271
- ) {
272
- throw new Error("editScale palette requires a color scale.");
273
- }
274
- const patch = !hasPalette
275
- ? args
276
- : Object.fromEntries([
277
- ...Object.entries(args).filter(([key]) => key !== "palette"),
278
- ["range", { palette: args.palette }]
279
- ]);
280
- validateLegendTypeTransition(this, scale, args.type ?? scale.type);
281
- const definition = normalizeDefinition(scale, channel, consumers, patch);
82
+ const channel = resolveScaleConsumerChannel(consumers, id);
83
+ const definition = prepareScaleEdit(this, scale, channel, consumers, args);
282
84
 
283
85
  let next = this;
284
86
  for (const property of EDITABLE) {
@@ -0,0 +1,214 @@
1
+ import {
2
+ hasOrdinalDomain,
3
+ isDiscretizedColorScaleType,
4
+ isDiscretePositionScaleType,
5
+ isTransformedScaleType,
6
+ normalizeScaleDefinition,
7
+ SCALE_ROLES,
8
+ validateColorRange,
9
+ validateDiscretizedColorDomain,
10
+ validateDiscretizedColorRange,
11
+ validateOrdinalDomain,
12
+ validateScaleDomain,
13
+ validateScaleRange,
14
+ validateScaleType,
15
+ validateScaleTypeForRole,
16
+ validateScaleUnknown,
17
+ validateSequentialColorRange,
18
+ validateShapeRange,
19
+ validateSizeRange,
20
+ validateStrokeDashRange,
21
+ validateStrokeWidthRange
22
+ } from "../../grammar/scales/index.js";
23
+ import {
24
+ validateRadialRange,
25
+ validateThetaRange
26
+ } from "../../grammar/polar.js";
27
+ import { normalizePositionScaleChannel } from "../../core/vocabulary.js";
28
+
29
+ export function resolveScaleConsumerChannel(consumers, id) {
30
+ const channels = new Set(
31
+ consumers.map(consumer => normalizePositionScaleChannel(consumer.channel))
32
+ );
33
+ if (channels.size > 1) {
34
+ throw new Error(`Scale "${id}" cannot be shared across channels.`);
35
+ }
36
+ return channels.values().next().value;
37
+ }
38
+
39
+ function validateRangeForChannel(scale, channel, value) {
40
+ if (value === "auto") return value;
41
+ if (channel === "theta") return validateThetaRange(value);
42
+ if (channel === "radius") return validateRadialRange(value);
43
+ if (scale.type === "sequential") {
44
+ return validateSequentialColorRange(value);
45
+ }
46
+ if (isDiscretizedColorScaleType(scale.type)) {
47
+ return validateDiscretizedColorRange(value);
48
+ }
49
+ if (scale.type !== "ordinal") return validateScaleRange(value);
50
+ if (channel === "color") return validateColorRange(value);
51
+ if (channel === "shape") return validateShapeRange(value);
52
+ if (channel === "strokeDash") return validateStrokeDashRange(value);
53
+ if (channel === "size") return validateSizeRange(value);
54
+ if (channel === "strokeWidth") return validateStrokeWidthRange(value);
55
+ return validateScaleRange(value);
56
+ }
57
+
58
+ function validateTypeTransition(scale, nextType, channel, consumers) {
59
+ if (nextType === scale.type) return;
60
+ validateScaleType(nextType);
61
+ if (consumers.length === 0) return;
62
+ if (nextType === "sequential" || isDiscretizedColorScaleType(nextType)) {
63
+ const discretized = isDiscretizedColorScaleType(nextType);
64
+ if (channel !== "color" || consumers.some(consumer =>
65
+ ["nominal", "ordinal"].includes(consumer.encoding.fieldType) ||
66
+ (discretized && (
67
+ consumer.encoding.fieldType !== "quantitative" ||
68
+ consumer.layer.mark?.type !== "point"
69
+ )) ||
70
+ (!discretized && !["point", "bar"].includes(consumer.layer.mark?.type))
71
+ )) {
72
+ throw new Error(
73
+ `Scale "${scale.id}" has a consumer incompatible with type "${nextType}".`
74
+ );
75
+ }
76
+ return;
77
+ }
78
+ if (nextType === "time") {
79
+ if (
80
+ !["x", "y", "theta"].includes(channel) ||
81
+ consumers.some(consumer => consumer.encoding.fieldType !== "temporal")
82
+ ) {
83
+ throw new Error(
84
+ `Scale "${scale.id}" has a consumer incompatible with type "time".`
85
+ );
86
+ }
87
+ return;
88
+ }
89
+ if (isDiscretePositionScaleType(nextType)) {
90
+ if (
91
+ !["x", "y", "theta"].includes(channel) || consumers.some(consumer =>
92
+ !["nominal", "ordinal"].includes(consumer.encoding.fieldType)
93
+ )
94
+ ) {
95
+ throw new Error(
96
+ `Scale "${scale.id}" has a consumer incompatible with type "${nextType}".`
97
+ );
98
+ }
99
+ if (
100
+ nextType === "point" &&
101
+ consumers.some(consumer => consumer.layer.mark?.type === "bar")
102
+ ) {
103
+ throw new Error("Point scales cannot provide bar bandwidth.");
104
+ }
105
+ return;
106
+ }
107
+ const quantitative = nextType === "linear" || isTransformedScaleType(nextType);
108
+ if (
109
+ !quantitative ||
110
+ !["x", "y", "theta", "radius", "strokeWidth"].includes(channel)
111
+ ) {
112
+ throw new Error(
113
+ "editScale type transition currently requires a quantitative position scale."
114
+ );
115
+ }
116
+ validateScaleTypeForRole(nextType, SCALE_ROLES.quantitativePosition);
117
+ if (channel === "theta" && nextType !== "linear") {
118
+ throw new Error("Theta quantitative position currently requires a linear scale.");
119
+ }
120
+ if (
121
+ channel === "strokeWidth" &&
122
+ consumers.some(consumer => !["line", "rule"].includes(consumer.layer.mark?.type))
123
+ ) {
124
+ throw new Error(
125
+ `Scale "${scale.id}" has a consumer incompatible with type "${nextType}".`
126
+ );
127
+ }
128
+ if (consumers.some(consumer => consumer.encoding.fieldType !== "quantitative")) {
129
+ throw new Error(
130
+ `Scale "${scale.id}" has a consumer incompatible with type "${nextType}".`
131
+ );
132
+ }
133
+ }
134
+
135
+ function validateLegendTypeTransition(program, scale, nextType) {
136
+ if (nextType === scale.type) return;
137
+ const legends = program.guideConfigs.legend ?? {};
138
+ if (legends.gradient?.scale === scale.id && nextType !== "sequential") {
139
+ throw new Error(
140
+ `Scale "${scale.id}" cannot change type while its gradient legend is active.`
141
+ );
142
+ }
143
+ if (
144
+ legends.interval?.scale === scale.id &&
145
+ !isDiscretizedColorScaleType(nextType)
146
+ ) {
147
+ throw new Error(
148
+ `Scale "${scale.id}" cannot change type while its interval legend is active.`
149
+ );
150
+ }
151
+ }
152
+
153
+ function normalizeDefinition(scale, channel, consumers, patch) {
154
+ const type = patch.type ?? scale.type;
155
+ validateTypeTransition(scale, type, channel, consumers);
156
+ const definition = normalizeScaleDefinition({
157
+ type,
158
+ previous: scale,
159
+ patch,
160
+ retainCoreOnTypeChange: true,
161
+ retainCompatibleOnTypeChange: true,
162
+ validateDomain: (scaleType, value) =>
163
+ isDiscretizedColorScaleType(scaleType)
164
+ ? validateDiscretizedColorDomain(scaleType, value)
165
+ : hasOrdinalDomain(scaleType)
166
+ ? validateOrdinalDomain(value)
167
+ : validateScaleDomain(value),
168
+ validateRange: (scaleType, value) =>
169
+ validateRangeForChannel({ type: scaleType }, channel, value)
170
+ });
171
+ if (
172
+ channel === "strokeWidth" &&
173
+ definition.domain !== "auto" &&
174
+ definition.domain.some(value => value < 0)
175
+ ) {
176
+ throw new RangeError("StrokeWidth scale domain cannot contain negative values.");
177
+ }
178
+ const typeChanged = type !== scale.type;
179
+ const unknown = Object.hasOwn(patch, "unknown")
180
+ ? patch.unknown
181
+ : typeChanged ? undefined : scale.unknown;
182
+ if (unknown !== undefined) {
183
+ if (consumers.some(consumer => consumer.layer.mark?.type !== "point")) {
184
+ throw new Error("Scale unknown currently requires row-owned point consumers.");
185
+ }
186
+ definition.unknown = consumers.length === 0
187
+ ? unknown
188
+ : validateScaleUnknown(channel, unknown);
189
+ }
190
+ return definition;
191
+ }
192
+
193
+ export function prepareScaleEdit(program, scale, channel, consumers, args) {
194
+ const hasPalette = Object.hasOwn(args, "palette");
195
+ if (hasPalette && Object.hasOwn(args, "range")) {
196
+ throw new Error("editScale cannot specify both palette and range.");
197
+ }
198
+ if (
199
+ hasPalette &&
200
+ channel !== "color" &&
201
+ scale.type !== "sequential" &&
202
+ !isDiscretizedColorScaleType(scale.type)
203
+ ) {
204
+ throw new Error("editScale palette requires a color scale.");
205
+ }
206
+ const patch = !hasPalette
207
+ ? args
208
+ : Object.fromEntries([
209
+ ...Object.entries(args).filter(([key]) => key !== "palette"),
210
+ ["range", { palette: args.palette }]
211
+ ]);
212
+ validateLegendTypeTransition(program, scale, args.type ?? scale.type);
213
+ return normalizeDefinition(scale, channel, consumers, patch);
214
+ }
@@ -70,7 +70,7 @@ export const rematerializeScale = action(
70
70
  channel,
71
71
  consumers,
72
72
  valuesByConsumer,
73
- bounds: ["color", "strokeDash", "shape", "size", "opacity", "xOffset", "yOffset"]
73
+ bounds: ["color", "strokeDash", "strokeWidth", "shape", "size", "opacity", "xOffset", "yOffset"]
74
74
  .includes(channel)
75
75
  ? undefined
76
76
  : resolveGraphicBounds(this),
@@ -0,0 +1,26 @@
1
+ const SCALE_EDIT_PROPERTIES = Object.freeze([
2
+ "type", "domain", "range", "nice", "zero", "clamp", "reverse",
3
+ "base", "exponent", "constant", "paddingInner", "paddingOuter",
4
+ "padding", "align", "interpolate", "unknown"
5
+ ]);
6
+
7
+ function sameValue(left, right) {
8
+ return JSON.stringify(left) === JSON.stringify(right);
9
+ }
10
+
11
+ export function scaleEditPatch(id, definition) {
12
+ return {
13
+ id,
14
+ ...Object.fromEntries(SCALE_EDIT_PROPERTIES
15
+ .filter(property => Object.hasOwn(definition, property))
16
+ .map(property => [property, definition[property]]))
17
+ };
18
+ }
19
+
20
+ export function changedScaleEditPatch(current, definition) {
21
+ const patch = scaleEditPatch(current.id, definition);
22
+ return Object.entries(patch).some(
23
+ ([property, value]) => property !== "id" &&
24
+ !sameValue(current[property], value)
25
+ ) ? patch : undefined;
26
+ }