ggaction 0.0.4 → 0.0.5

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 (157) hide show
  1. package/CHANGELOG.md +30 -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 +31 -35
  6. package/src/actions/boxPlots/materialize.js +3 -0
  7. package/src/actions/boxPlots/options.js +8 -1
  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/coordinates/parallel.js +50 -0
  18. package/src/actions/data/bin2d.js +137 -0
  19. package/src/actions/data/density.js +54 -13
  20. package/src/actions/data/derived.js +22 -0
  21. package/src/actions/data/filter.js +5 -0
  22. package/src/actions/data/gradientProfile.js +65 -0
  23. package/src/actions/data/horizon.js +71 -0
  24. package/src/actions/data/index.js +29 -2
  25. package/src/actions/data/window.js +50 -0
  26. package/src/actions/encodings/color/index.js +17 -1
  27. package/src/actions/encodings/color/layout.js +8 -0
  28. package/src/actions/encodings/density/resolve.js +109 -0
  29. package/src/actions/encodings/density.js +193 -88
  30. package/src/actions/encodings/horizon/resolve.js +283 -0
  31. package/src/actions/encodings/horizon.js +364 -0
  32. package/src/actions/encodings/index.js +6 -0
  33. package/src/actions/encodings/parallel.js +115 -0
  34. package/src/actions/encodings/pathOrder.js +131 -0
  35. package/src/actions/encodings/position/apply.js +73 -4
  36. package/src/actions/encodings/position/index.js +15 -2
  37. package/src/actions/encodings/position/policies/arc.js +27 -4
  38. package/src/actions/encodings/position/policies/area.js +33 -2
  39. package/src/actions/encodings/position/policies/index.js +7 -2
  40. package/src/actions/encodings/position/policies/line.js +18 -2
  41. package/src/actions/encodings/position/resolve.js +24 -2
  42. package/src/actions/encodings/ranged.js +3 -1
  43. package/src/actions/encodings/ruleAppearance.js +88 -10
  44. package/src/actions/facets/actions.js +1 -2
  45. package/src/actions/facets/derive.js +33 -3
  46. package/src/actions/facets/replay.js +7 -36
  47. package/src/actions/gradientPlots/components.js +177 -0
  48. package/src/actions/gradientPlots/create.js +96 -0
  49. package/src/actions/gradientPlots/edit.js +153 -0
  50. package/src/actions/gradientPlots/index.js +24 -0
  51. package/src/actions/gradientPlots/materialize.js +287 -0
  52. package/src/actions/gradientPlots/options.js +120 -0
  53. package/src/actions/gradientPlots/paint.js +107 -0
  54. package/src/actions/gradientPlots/rebind.js +51 -0
  55. package/src/actions/gradientPlots/resolve.js +81 -0
  56. package/src/actions/guides/applicability.js +44 -17
  57. package/src/actions/guides/axes/axes.js +37 -3
  58. package/src/actions/guides/axes/index.js +2 -0
  59. package/src/actions/guides/axes/labels.js +11 -6
  60. package/src/actions/guides/axes/parallel/resolve.js +86 -0
  61. package/src/actions/guides/axes/parallel.js +228 -0
  62. package/src/actions/guides/axes/titles.js +11 -8
  63. package/src/actions/guides/guides.js +60 -5
  64. package/src/actions/guides/legends/categorical/actions.js +48 -11
  65. package/src/actions/guides/legends/edit.js +5 -0
  66. package/src/actions/guides/legends/index.js +2 -0
  67. package/src/actions/guides/legends/remove.js +4 -11
  68. package/src/actions/guides/legends/strokeWidth.js +170 -0
  69. package/src/actions/index.js +6 -0
  70. package/src/actions/marks/area/actions.js +60 -136
  71. package/src/actions/marks/area/materialize.js +163 -0
  72. package/src/actions/marks/index.js +5 -1
  73. package/src/actions/marks/line/actions.js +99 -80
  74. package/src/actions/marks/line/materialize.js +146 -0
  75. package/src/actions/marks/point/create.js +76 -0
  76. package/src/actions/marks/point/edit.js +85 -0
  77. package/src/actions/marks/point/index.js +11 -1
  78. package/src/actions/marks/point/jitter.js +72 -0
  79. package/src/actions/marks/point/{actions.js → materialize.js} +175 -170
  80. package/src/actions/marks/rect/actions.js +3 -0
  81. package/src/actions/marks/remove.js +13 -2
  82. package/src/actions/marks/rule/actions.js +25 -3
  83. package/src/actions/marks/text/actions.js +19 -4
  84. package/src/actions/marks/text/index.js +7 -1
  85. package/src/actions/marks/text/layout.js +321 -0
  86. package/src/actions/primitives/semanticValidation/dataset.js +22 -0
  87. package/src/actions/primitives/semanticValidation/guide.js +43 -0
  88. package/src/actions/primitives/semanticValidation/index.js +23 -0
  89. package/src/actions/primitives/semanticValidation/layer.js +74 -0
  90. package/src/actions/primitives/semanticValidation/scale.js +74 -0
  91. package/src/actions/primitives/semanticValidation/shared.js +5 -0
  92. package/src/actions/primitives/semanticValue.js +1 -204
  93. package/src/actions/regression/edit.js +19 -21
  94. package/src/actions/scales/consumers/common.js +18 -0
  95. package/src/actions/scales/consumers/families.js +14 -3
  96. package/src/actions/scales/definitions.js +30 -0
  97. package/src/actions/scales/edit.js +6 -204
  98. package/src/actions/scales/editPolicy.js +214 -0
  99. package/src/actions/scales/materialize.js +1 -1
  100. package/src/actions/scales/patch.js +26 -0
  101. package/src/actions/selection/actions.js +28 -3
  102. package/src/actions/violinPlots/create.js +198 -0
  103. package/src/actions/violinPlots/index.js +5 -0
  104. package/src/core/textMetrics.js +47 -0
  105. package/src/core/vocabulary.js +7 -2
  106. package/src/grammar/arcs.js +49 -12
  107. package/src/grammar/areaSeries.js +41 -5
  108. package/src/grammar/bin2d.js +333 -0
  109. package/src/grammar/categoricalDensity.js +192 -0
  110. package/src/grammar/coordinates.js +1 -1
  111. package/src/grammar/curveCommands.js +3 -1
  112. package/src/grammar/density.js +252 -26
  113. package/src/grammar/facets/dependencies.js +3 -11
  114. package/src/grammar/facets/index.js +3 -1
  115. package/src/grammar/gradientProfile.js +213 -0
  116. package/src/grammar/horizon.js +455 -0
  117. package/src/grammar/jitter.js +197 -0
  118. package/src/grammar/lineSeries.js +69 -0
  119. package/src/grammar/paint.js +88 -0
  120. package/src/grammar/parallelCoordinates.js +213 -0
  121. package/src/grammar/pathOrder.js +35 -0
  122. package/src/grammar/pointShapes.js +24 -0
  123. package/src/grammar/rules.js +1 -0
  124. package/src/grammar/scales/appearance.js +21 -0
  125. package/src/grammar/schemas/concreteGraphic.js +11 -1
  126. package/src/grammar/schemas/graphicBounds.js +19 -37
  127. package/src/grammar/schemas/semanticPath.js +17 -2
  128. package/src/grammar/transforms.js +100 -11
  129. package/src/grammar/window.js +339 -0
  130. package/src/layout/labels.js +270 -0
  131. package/src/layout/text.js +4 -1
  132. package/src/layout/title.js +10 -20
  133. package/src/materialization/dataProvenance.js +56 -2
  134. package/src/materialization/dependencies.js +9 -14
  135. package/src/materialization/guides/resources.js +74 -24
  136. package/src/materialization/horizon.js +29 -0
  137. package/src/materialization/layout.js +15 -0
  138. package/src/materialization/marks/capabilities.js +21 -1
  139. package/src/materialization/marks/index.js +3 -0
  140. package/src/materialization/marks/pathOrder.js +71 -0
  141. package/src/materialization/marks/policies.js +6 -2
  142. package/src/materialization/scaleGuideDependencies.js +8 -0
  143. package/src/materialization/scales/resolve.js +2 -0
  144. package/src/materialization/selection/items/path.js +27 -1
  145. package/src/materialization/selection/items/point.js +1 -5
  146. package/src/materialization/selection/items/rect.js +21 -0
  147. package/src/materialization/selection/styles.js +20 -9
  148. package/src/materialization/text.js +12 -4
  149. package/src/renderers/canvas/fill.js +33 -0
  150. package/src/renderers/canvas/index.js +7 -0
  151. package/src/renderers/canvas/path.js +7 -4
  152. package/src/renderers/canvas/rect.js +9 -3
  153. package/src/selectors/datasets.js +4 -0
  154. package/src/theme/defaults.js +4 -0
  155. package/types/extension.d.ts +6 -0
  156. package/types/index.d.ts +76 -0
  157. package/types/program.d.ts +632 -10
@@ -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
+ }
@@ -117,7 +117,26 @@ export const applyPointHighlight = action(
117
117
  }
118
118
  );
119
119
 
120
- function applyRectangularHighlight(program, args, operation, markType) {
120
+ function transformRectangularProperties(properties, style, offset = false) {
121
+ if (!offset || style.offset === undefined) {
122
+ return { ...properties, ...style };
123
+ }
124
+ const { offset: translation, ...appearance } = style;
125
+ return {
126
+ ...properties,
127
+ ...appearance,
128
+ x: properties.x + translation.x,
129
+ y: properties.y + translation.y
130
+ };
131
+ }
132
+
133
+ function applyRectangularHighlight(
134
+ program,
135
+ args,
136
+ operation,
137
+ markType,
138
+ { offset = false } = {}
139
+ ) {
121
140
  validateKeys(args, INTERNAL_SELECTION_OPTIONS, operation);
122
141
  const resolved = resolveStoredSelection(program, args.selection);
123
142
  const keys = selectedKeys(args, resolved);
@@ -136,7 +155,7 @@ function applyRectangularHighlight(program, args, operation, markType) {
136
155
  value: graphic.items.map(child => ({
137
156
  type: child.type ?? graphic.type,
138
157
  properties: selected.has(keyByGraphic.get(child.id))
139
- ? { ...child.properties, ...args.style }
158
+ ? transformRectangularProperties(child.properties, args.style, offset)
140
159
  : child.properties
141
160
  }))
142
161
  });
@@ -152,7 +171,13 @@ export const applyBarHighlight = action(
152
171
  export const applyRectHighlight = action(
153
172
  { op: "applyRectHighlight", description: "Apply selected rect appearance." },
154
173
  function (args = {}) {
155
- return applyRectangularHighlight(this, args, "applyRectHighlight", "rect");
174
+ return applyRectangularHighlight(
175
+ this,
176
+ args,
177
+ "applyRectHighlight",
178
+ "rect",
179
+ { offset: true }
180
+ );
156
181
  }
157
182
  );
158
183