ggaction 0.0.2 → 0.0.3

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 (199) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +12 -1
  3. package/package.json +6 -1
  4. package/src/actions/boxPlots/components.js +3 -3
  5. package/src/actions/boxPlots/edit.js +277 -0
  6. package/src/actions/boxPlots/index.js +2 -0
  7. package/src/actions/boxPlots/materialize.js +12 -1
  8. package/src/actions/boxPlots/options.js +25 -14
  9. package/src/actions/canvas/actions.js +21 -17
  10. package/src/actions/composition/actions.js +247 -0
  11. package/src/actions/composition/index.js +1 -0
  12. package/src/actions/coordinates/actions.js +2 -5
  13. package/src/actions/data/density.js +7 -1
  14. package/src/actions/data/filter.js +3 -1
  15. package/src/actions/encodings/appearance.js +11 -0
  16. package/src/actions/encodings/barWidth.js +5 -3
  17. package/src/actions/encodings/color.js +120 -25
  18. package/src/actions/encodings/index.js +2 -0
  19. package/src/actions/encodings/offset.js +90 -72
  20. package/src/actions/encodings/position/apply.js +66 -0
  21. package/src/actions/encodings/position/policies/arc.js +26 -0
  22. package/src/actions/encodings/position/policies/bar.js +9 -5
  23. package/src/actions/encodings/position/policies/index.js +5 -1
  24. package/src/actions/encodings/position/policies/line.js +20 -3
  25. package/src/actions/encodings/position/resolve.js +57 -8
  26. package/src/actions/encodings/position.js +44 -90
  27. package/src/actions/encodings/ranged.js +28 -6
  28. package/src/actions/encodings/shared.js +17 -21
  29. package/src/actions/encodings/text.js +73 -0
  30. package/src/actions/errorBands/create.js +37 -29
  31. package/src/actions/errorBands/edit.js +206 -0
  32. package/src/actions/errorBands/index.js +9 -0
  33. package/src/actions/errorBands/options.js +39 -0
  34. package/src/actions/errorBars/create.js +50 -43
  35. package/src/actions/errorBars/edit.js +130 -0
  36. package/src/actions/errorBars/index.js +3 -0
  37. package/src/actions/errorBars/options.js +52 -0
  38. package/src/actions/facets/actions.js +162 -0
  39. package/src/actions/facets/derive.js +200 -0
  40. package/src/actions/facets/guides.js +16 -0
  41. package/src/actions/facets/index.js +1 -0
  42. package/src/actions/facets/replay.js +69 -0
  43. package/src/actions/guides/applicability.js +113 -0
  44. package/src/actions/guides/axes/axes.js +70 -16
  45. package/src/actions/guides/axes/edit.js +126 -0
  46. package/src/actions/guides/axes/index.js +12 -0
  47. package/src/actions/guides/axes/labels.js +21 -18
  48. package/src/actions/guides/axes/remove.js +60 -0
  49. package/src/actions/guides/axes/tickGroups.js +7 -5
  50. package/src/actions/guides/axes/ticks.js +9 -4
  51. package/src/actions/guides/axes/titles.js +7 -5
  52. package/src/actions/guides/grids/grid.js +144 -15
  53. package/src/actions/guides/grids/resolve.js +5 -13
  54. package/src/actions/guides/guides.js +17 -75
  55. package/src/actions/guides/index.js +6 -0
  56. package/src/actions/guides/legends/categorical/actions.js +48 -9
  57. package/src/actions/guides/legends/categorical/components.js +16 -16
  58. package/src/actions/guides/legends/categorical/index.js +14 -14
  59. package/src/actions/guides/legends/categorical/layout.js +3 -1
  60. package/src/actions/guides/legends/categorical/options.js +2 -5
  61. package/src/actions/guides/legends/categorical/resolve.js +2 -2
  62. package/src/actions/guides/legends/categorical/symbols.js +14 -14
  63. package/src/actions/guides/legends/continuous/common.js +7 -7
  64. package/src/actions/guides/legends/continuous/interval.js +5 -5
  65. package/src/actions/guides/legends/edit.js +2 -6
  66. package/src/actions/guides/legends/focused.js +63 -0
  67. package/src/actions/guides/legends/index.js +4 -0
  68. package/src/actions/guides/legends/remove.js +64 -0
  69. package/src/actions/guides/legends/size.js +12 -14
  70. package/src/actions/guides/polar/axes/facade.js +219 -0
  71. package/src/actions/guides/polar/axes/labels.js +188 -0
  72. package/src/actions/guides/polar/axes/lines.js +113 -0
  73. package/src/actions/guides/polar/axes/shared.js +112 -0
  74. package/src/actions/guides/polar/axes/ticks.js +164 -0
  75. package/src/actions/guides/polar/axes/titles.js +165 -0
  76. package/src/actions/guides/polar/axes.js +55 -0
  77. package/src/actions/guides/polar/grids.js +252 -0
  78. package/src/actions/guides/polar/index.js +2 -0
  79. package/src/actions/guides/polar/resolve.js +269 -0
  80. package/src/actions/index.js +4 -0
  81. package/src/actions/marks/arc/actions.js +239 -0
  82. package/src/actions/marks/arc/index.js +1 -0
  83. package/src/actions/marks/{area.js → area/actions.js} +51 -38
  84. package/src/actions/marks/area/index.js +4 -0
  85. package/src/actions/marks/bar/create.js +26 -5
  86. package/src/actions/marks/bar/materialize.js +25 -31
  87. package/src/actions/marks/index.js +12 -4
  88. package/src/actions/marks/lifecycle.js +27 -0
  89. package/src/actions/marks/line/actions.js +373 -0
  90. package/src/actions/marks/line/index.js +1 -0
  91. package/src/actions/marks/{point.js → point/actions.js} +67 -108
  92. package/src/actions/marks/point/index.js +1 -0
  93. package/src/actions/marks/rect/actions.js +165 -0
  94. package/src/actions/marks/rect/index.js +1 -0
  95. package/src/actions/marks/remove.js +214 -0
  96. package/src/actions/marks/{rule.js → rule/actions.js} +34 -28
  97. package/src/actions/marks/rule/index.js +1 -0
  98. package/src/actions/marks/shared.js +156 -25
  99. package/src/actions/marks/text/actions.js +192 -0
  100. package/src/actions/marks/text/index.js +1 -0
  101. package/src/actions/primitives/createGraphics.js +13 -6
  102. package/src/actions/primitives/editGraphics.js +10 -3
  103. package/src/actions/primitives/semantic.js +26 -3
  104. package/src/actions/primitives/semanticValue.js +25 -3
  105. package/src/actions/regression/components.js +1 -1
  106. package/src/actions/regression/create.js +18 -1
  107. package/src/actions/regression/edit.js +247 -0
  108. package/src/actions/regression/index.js +3 -0
  109. package/src/actions/regression/resolve.js +3 -1
  110. package/src/actions/scales/consumers.js +15 -6
  111. package/src/actions/scales/definitions.js +13 -4
  112. package/src/actions/scales/edit.js +42 -19
  113. package/src/actions/scales/materialize.js +57 -408
  114. package/src/actions/selection/actions.js +38 -24
  115. package/src/actions/selection/index.js +2 -0
  116. package/src/actions/titles/actions.js +63 -2
  117. package/src/actions/titles/index.js +2 -0
  118. package/src/actions/titles/resolve.js +12 -17
  119. package/src/composition.js +10 -0
  120. package/src/core/ChartProgram.js +200 -2
  121. package/src/core/action.js +10 -0
  122. package/src/core/validation.js +16 -0
  123. package/src/core/vocabulary.js +154 -4
  124. package/src/grammar/arcs.js +154 -0
  125. package/src/grammar/bars/aggregate.js +2 -2
  126. package/src/grammar/bars/geometry.js +3 -3
  127. package/src/grammar/bars/policy.js +7 -1
  128. package/src/grammar/coordinates.js +3 -7
  129. package/src/grammar/density.js +20 -1
  130. package/src/grammar/facets/dependencies.js +185 -0
  131. package/src/grammar/facets/guides.js +257 -0
  132. package/src/grammar/facets/scales.js +254 -0
  133. package/src/grammar/facets.js +122 -0
  134. package/src/grammar/lineSeries.js +111 -3
  135. package/src/grammar/polar.js +102 -0
  136. package/src/grammar/polarGuides.js +243 -0
  137. package/src/grammar/polarLineCommands.js +43 -0
  138. package/src/grammar/polarPaths.js +161 -0
  139. package/src/grammar/positionCompatibility.js +18 -2
  140. package/src/grammar/rects.js +26 -0
  141. package/src/grammar/scales/continuous.js +6 -1
  142. package/src/grammar/scales/fields.js +7 -0
  143. package/src/grammar/scales/ordinal.js +5 -2
  144. package/src/grammar/scales/policies.js +1 -1
  145. package/src/grammar/schemas/graphic.js +1 -1
  146. package/src/grammar/schemas/graphicBounds.js +64 -9
  147. package/src/grammar/schemas/graphicTree.js +44 -10
  148. package/src/grammar/schemas/semanticPath.js +31 -9
  149. package/src/grammar/seriesLayout.js +13 -2
  150. package/src/grammar/text.js +71 -0
  151. package/src/grammar/ticks.js +50 -13
  152. package/src/index.js +1 -0
  153. package/src/layout/composition.js +310 -0
  154. package/src/layout/facets.js +131 -0
  155. package/src/layout/title.js +6 -0
  156. package/src/materialization/bars/aggregate.js +5 -2
  157. package/src/materialization/bars/grouped.js +75 -37
  158. package/src/materialization/composition.js +152 -0
  159. package/src/materialization/compositionSnapshot.js +114 -0
  160. package/src/materialization/dependencies.js +71 -16
  161. package/src/materialization/encodings.js +14 -28
  162. package/src/materialization/facetGuides.js +426 -0
  163. package/src/materialization/facets.js +214 -0
  164. package/src/materialization/graphicHierarchy.js +15 -5
  165. package/src/materialization/guides/resources.js +46 -0
  166. package/src/materialization/marks.js +220 -14
  167. package/src/materialization/planner.js +105 -12
  168. package/src/materialization/rect.js +138 -0
  169. package/src/materialization/rectConfig.js +38 -0
  170. package/src/materialization/rowEncoding.js +33 -0
  171. package/src/materialization/scales/map.js +40 -0
  172. package/src/materialization/scales/policies/arc.js +37 -0
  173. package/src/materialization/scales/policies/bar.js +132 -0
  174. package/src/materialization/scales/policies/series.js +76 -0
  175. package/src/materialization/scales/resolve.js +326 -0
  176. package/src/materialization/selection/items/arc.js +29 -0
  177. package/src/materialization/selection/items/bar.js +255 -0
  178. package/src/materialization/selection/items/common.js +131 -0
  179. package/src/materialization/selection/items/index.js +6 -0
  180. package/src/materialization/selection/items/path.js +59 -0
  181. package/src/materialization/selection/items/point.js +55 -0
  182. package/src/materialization/selection/items/rect.js +23 -0
  183. package/src/materialization/selection/items/rule.js +25 -0
  184. package/src/materialization/selection/policies/arc.js +10 -0
  185. package/src/materialization/selection/policies/area.js +1 -1
  186. package/src/materialization/selection/policies/bar.js +1 -1
  187. package/src/materialization/selection/policies/index.js +5 -1
  188. package/src/materialization/selection/policies/line.js +1 -1
  189. package/src/materialization/selection/policies/point.js +1 -1
  190. package/src/materialization/selection/policies/rect.js +10 -0
  191. package/src/materialization/selection/policies/rule.js +1 -1
  192. package/src/materialization/selection/styles.js +24 -16
  193. package/src/materialization/text.js +156 -0
  194. package/src/renderers/canvas/index.js +68 -22
  195. package/types/extension.d.ts +1 -0
  196. package/types/index.d.ts +56 -1
  197. package/types/program.d.ts +664 -27
  198. package/src/actions/marks/line.js +0 -248
  199. package/src/materialization/selection/items.js +0 -503
@@ -0,0 +1,247 @@
1
+ import { action } from "../../core/action.js";
2
+ import { isPlainObject } from "../../core/immutable.js";
3
+ import { validateUserId } from "../../core/identifiers.js";
4
+ import {
5
+ validateKeys,
6
+ validateNonEmptyString,
7
+ validateNonNegativeFinite,
8
+ validateUnitInterval
9
+ } from "../../core/validation.js";
10
+ import { normalizeRegressionParameters } from "../../grammar/regression.js";
11
+ import { validateCurveInterpolation } from "../../grammar/curveCommands.js";
12
+ import { hasDataset } from "../../selectors/index.js";
13
+ import { findLayer } from "../../selectors/layers.js";
14
+
15
+ const OPTIONS = Object.freeze([
16
+ "target", "method", "degree", "span", "confidence", "interval", "band", "line"
17
+ ]);
18
+ const STATISTICAL_OPTIONS = Object.freeze([
19
+ "method", "degree", "span", "confidence", "interval"
20
+ ]);
21
+ const BAND_OPTIONS = Object.freeze([
22
+ "color", "opacity", "stroke", "strokeWidth", "curve"
23
+ ]);
24
+ const LINE_OPTIONS = Object.freeze(["strokeWidth", "curve"]);
25
+
26
+ function resolveRegressionOwner(program, requested) {
27
+ const eligible = program.semanticSpec.layers.filter(
28
+ layer => program.markConfigs[layer.id]?.regression !== undefined
29
+ );
30
+ if (requested !== undefined) {
31
+ const id = validateUserId(requested, "Regression owner id");
32
+ const layer = findLayer(program, id);
33
+ if (layer === undefined || !eligible.includes(layer)) {
34
+ throw new Error(`Unknown regression owner "${id}".`);
35
+ }
36
+ return layer;
37
+ }
38
+ const current = findLayer(program, program.context.currentMark);
39
+ if (current !== undefined && eligible.includes(current)) return current;
40
+ if (eligible.length === 1) return eligible[0];
41
+ if (eligible.length === 0) throw new Error("No regression owner is available.");
42
+ throw new Error("Regression owner is ambiguous; provide target.");
43
+ }
44
+
45
+ function requirePatch(value, keys, label) {
46
+ if (!isPlainObject(value)) {
47
+ throw new TypeError(`${label} must be a plain object.`);
48
+ }
49
+ validateKeys(value, keys, label);
50
+ return value;
51
+ }
52
+
53
+ function validateBandPatch(value) {
54
+ if (value === false) return false;
55
+ const patch = requirePatch(value, BAND_OPTIONS, "editRegression band");
56
+ if (Object.hasOwn(patch, "color")) {
57
+ validateNonEmptyString(patch.color, "Regression band color");
58
+ }
59
+ if (Object.hasOwn(patch, "opacity")) {
60
+ validateUnitInterval(patch.opacity, "Regression band opacity");
61
+ }
62
+ if (Object.hasOwn(patch, "stroke")) {
63
+ if (patch.stroke !== false) {
64
+ validateNonEmptyString(patch.stroke, "Regression band stroke");
65
+ }
66
+ }
67
+ if (Object.hasOwn(patch, "strokeWidth")) {
68
+ validateNonNegativeFinite(
69
+ patch.strokeWidth,
70
+ "Regression band strokeWidth"
71
+ );
72
+ }
73
+ if (patch.stroke === false && Object.hasOwn(patch, "strokeWidth")) {
74
+ throw new Error(
75
+ "editRegression band cannot set strokeWidth while removing stroke."
76
+ );
77
+ }
78
+ if (Object.hasOwn(patch, "curve")) {
79
+ validateCurveInterpolation(patch.curve);
80
+ }
81
+ return patch;
82
+ }
83
+
84
+ function validateLinePatch(value) {
85
+ const patch = requirePatch(value, LINE_OPTIONS, "editRegression line");
86
+ if (Object.hasOwn(patch, "strokeWidth")) {
87
+ validateNonNegativeFinite(
88
+ patch.strokeWidth,
89
+ "Regression line strokeWidth"
90
+ );
91
+ }
92
+ if (Object.hasOwn(patch, "curve")) {
93
+ validateCurveInterpolation(patch.curve);
94
+ }
95
+ return patch;
96
+ }
97
+
98
+ function resolveParameters(previous, args) {
99
+ const method = args.method ?? previous.method;
100
+ const raw = { method };
101
+ for (const key of ["degree", "span", "confidence", "interval"]) {
102
+ if (Object.hasOwn(args, key)) raw[key] = args[key];
103
+ }
104
+ if (method === previous.method) {
105
+ for (const key of ["degree", "span", "confidence", "interval"]) {
106
+ if (!Object.hasOwn(raw, key) && Object.hasOwn(previous, key)) {
107
+ raw[key] = previous[key];
108
+ }
109
+ }
110
+ } else if (method !== "loess" && previous.method !== "loess") {
111
+ for (const key of ["confidence", "interval"]) {
112
+ if (!Object.hasOwn(raw, key) && Object.hasOwn(previous, key)) {
113
+ raw[key] = previous[key];
114
+ }
115
+ }
116
+ }
117
+ return normalizeRegressionParameters(raw);
118
+ }
119
+
120
+ function nextRevisionId(program, ownerId) {
121
+ let revision = 1;
122
+ while (hasDataset(program, `${ownerId}RegressionDataRevision${revision}`)) {
123
+ revision += 1;
124
+ }
125
+ return `${ownerId}RegressionDataRevision${revision}`;
126
+ }
127
+
128
+ function removeBand(program, id) {
129
+ return program
130
+ .editSemantic({ property: `layer[${id}]`, remove: true })
131
+ .editGraphics({ target: id, remove: true })
132
+ ._withoutMaterializationConfig(["marks", id]);
133
+ }
134
+
135
+ export const editRegression = action(
136
+ {
137
+ op: "editRegression",
138
+ description: "Revise a regression model and its owned visible components."
139
+ },
140
+ function (args = {}) {
141
+ validateKeys(args, OPTIONS, "editRegression");
142
+ if (!OPTIONS.slice(1).some(key => Object.hasOwn(args, key))) {
143
+ throw new Error("editRegression requires a statistical or component option.");
144
+ }
145
+ const owner = resolveRegressionOwner(this, args.target);
146
+ const current = this.markConfigs[owner.id].regression;
147
+ const parameters = resolveParameters(current.parameters, args);
148
+ const changesStatistics = STATISTICAL_OPTIONS.some(
149
+ key => Object.hasOwn(args, key)
150
+ ) && JSON.stringify(parameters) !== JSON.stringify(current.parameters);
151
+ const bandPatch = Object.hasOwn(args, "band")
152
+ ? validateBandPatch(args.band)
153
+ : undefined;
154
+ const linePatch = Object.hasOwn(args, "line")
155
+ ? validateLinePatch(args.line)
156
+ : undefined;
157
+ if (parameters.method === "loess" && bandPatch !== undefined && bandPatch !== false) {
158
+ throw new Error("LOESS regression does not support a band object.");
159
+ }
160
+
161
+ const hadBand = current.bandId !== undefined;
162
+ const wantsBand = parameters.method !== "loess" &&
163
+ (bandPatch === false
164
+ ? false
165
+ : hadBand || bandPatch !== undefined || current.parameters.method === "loess");
166
+ let dataId = current.dataId;
167
+ let next = this;
168
+
169
+ if (changesStatistics) {
170
+ dataId = nextRevisionId(this, owner.id);
171
+ next = next.createRegressionData({
172
+ id: dataId,
173
+ source: current.source,
174
+ x: current.x,
175
+ y: current.y,
176
+ ...(current.groupBy === undefined ? {} : { groupBy: current.groupBy }),
177
+ ...parameters
178
+ });
179
+ }
180
+
181
+ let bandId = current.bandId;
182
+ if (hadBand && !wantsBand) {
183
+ next = removeBand(next, current.bandId);
184
+ bandId = undefined;
185
+ } else if (!hadBand && wantsBand) {
186
+ bandId = `${owner.id}RegressionBands`;
187
+ next = next.createRegressionBand({
188
+ id: bandId,
189
+ data: dataId,
190
+ x: current.x,
191
+ lower: "__regression_ci_lower",
192
+ upper: "__regression_ci_upper",
193
+ ...(current.groupBy === undefined ? {} : { groupBy: current.groupBy }),
194
+ coordinate: current.coordinate,
195
+ xScale: current.xScale,
196
+ yScale: current.yScale,
197
+ ...(bandPatch === undefined || bandPatch === false ? {} : bandPatch)
198
+ });
199
+ } else if (hadBand && changesStatistics) {
200
+ const bandConfig = next.markConfigs[bandId];
201
+ next = next
202
+ .editSemantic({ property: `layer[${bandId}].data`, value: dataId })
203
+ ._withMarkConfig(bandId, {
204
+ ...bandConfig,
205
+ errorBand: { ...bandConfig.errorBand, data: dataId }
206
+ });
207
+ }
208
+
209
+ if (changesStatistics) {
210
+ next = next.editSemantic({
211
+ property: `layer[${current.lineId}].data`,
212
+ value: dataId
213
+ });
214
+ }
215
+
216
+ if (bandId !== undefined) {
217
+ if (
218
+ bandPatch !== undefined &&
219
+ bandPatch !== false &&
220
+ Object.keys(bandPatch).length > 0 &&
221
+ hadBand
222
+ ) {
223
+ next = next.editRegressionBand({ target: bandId, ...bandPatch });
224
+ } else if (changesStatistics) {
225
+ next = next.rematerializeAreaMark({ id: bandId });
226
+ }
227
+ }
228
+ if (linePatch !== undefined && Object.keys(linePatch).length > 0) {
229
+ next = next.editRegressionLine({ target: current.lineId, ...linePatch });
230
+ } else if (changesStatistics) {
231
+ next = next.rematerializeLineMark({ id: current.lineId });
232
+ }
233
+
234
+ if (changesStatistics) {
235
+ next = next.releaseDerivedData({ id: current.dataId });
236
+ }
237
+ return next._withMarkConfig(owner.id, {
238
+ ...next.markConfigs[owner.id],
239
+ regression: {
240
+ ...current,
241
+ dataId,
242
+ bandId,
243
+ parameters
244
+ }
245
+ });
246
+ }
247
+ );
@@ -5,6 +5,7 @@ import {
5
5
  editRegressionLine
6
6
  } from "./components.js";
7
7
  import { createRegression } from "./create.js";
8
+ import { editRegression } from "./edit.js";
8
9
 
9
10
  export {
10
11
  createRegressionBand,
@@ -13,6 +14,7 @@ export {
13
14
  editRegressionLine
14
15
  } from "./components.js";
15
16
  export { createRegression } from "./create.js";
17
+ export { editRegression } from "./edit.js";
16
18
 
17
19
  export function registerRegressionActions(ProgramClass) {
18
20
  ProgramClass.prototype.createRegressionBand = createRegressionBand;
@@ -20,4 +22,5 @@ export function registerRegressionActions(ProgramClass) {
20
22
  ProgramClass.prototype.editRegressionBand = editRegressionBand;
21
23
  ProgramClass.prototype.editRegressionLine = editRegressionLine;
22
24
  ProgramClass.prototype.createRegression = createRegression;
25
+ ProgramClass.prototype.editRegression = editRegression;
23
26
  }
@@ -47,7 +47,9 @@ export function inferRegressionGroup(layer, args) {
47
47
  }
48
48
  const candidates = [...new Set(
49
49
  [layer.encoding?.color, layer.encoding?.shape]
50
- .filter(encoding => encoding?.fieldType === "nominal")
50
+ .filter(encoding =>
51
+ ["nominal", "ordinal"].includes(encoding?.fieldType)
52
+ )
51
53
  .map(encoding => encoding.field)
52
54
  )];
53
55
  if (candidates.length > 1) {
@@ -24,6 +24,8 @@ import { findDataset } from "../../selectors/datasets.js";
24
24
  import { requireSemanticScale } from "../../selectors/scales.js";
25
25
  import { isAggregate } from "../../grammar/aggregate.js";
26
26
  import { normalizeRuleDatum } from "../../grammar/rules.js";
27
+ import { SCALED_ENCODING_CHANNELS } from "../../core/vocabulary.js";
28
+ import { resolveRectConsumerValues } from "../../materialization/rect.js";
27
29
 
28
30
  export function findScale(program, id) {
29
31
  return requireSemanticScale(program, id);
@@ -32,10 +34,7 @@ export function findScale(program, id) {
32
34
  export function findScaleConsumers(program, id) {
33
35
  const consumers = [];
34
36
  for (const layer of program.semanticSpec.layers) {
35
- for (const channel of [
36
- "x", "y", "x2", "y2", "xOffset", "color", "strokeDash", "size", "shape",
37
- "opacity"
38
- ]) {
37
+ for (const channel of SCALED_ENCODING_CHANNELS) {
39
38
  const encoding = layer.encoding?.[channel];
40
39
  if (encoding?.scale === id) consumers.push({ layer, channel, encoding });
41
40
  }
@@ -67,9 +66,16 @@ export function resolveConsumerValues(program, consumer) {
67
66
  }
68
67
  const scale = findScale(program, consumer.encoding.scale);
69
68
  const allowUnknown = Object.hasOwn(scale, "unknown");
69
+ if (consumer.layer.mark?.type === "rect") {
70
+ return resolveRectConsumerValues(
71
+ consumer.layer,
72
+ dataset,
73
+ consumer.channel
74
+ );
75
+ }
70
76
 
71
77
  if (
72
- ["color", "strokeDash", "xOffset", "shape"].includes(consumer.channel) &&
78
+ ["color", "strokeDash", "xOffset", "yOffset", "shape"].includes(consumer.channel) &&
73
79
  consumer.encoding.fieldType === "nominal"
74
80
  ) {
75
81
  return allowUnknown
@@ -208,7 +214,10 @@ function resolveAggregatePartitions(program, consumer) {
208
214
  : derived.yValues;
209
215
  const categoryDomain = resolveOrdinalDomain(categoryScale.domain, categoryValues);
210
216
  const colorEncoding = layer.encoding?.color;
211
- if (colorEncoding?.scale === undefined) {
217
+ if (
218
+ colorEncoding?.scale === undefined ||
219
+ colorEncoding.fieldType === "quantitative"
220
+ ) {
212
221
  const byCategory = new Map(derived.values.map(value => [
213
222
  value[channels.category],
214
223
  value[channels.measure]
@@ -23,6 +23,10 @@ import {
23
23
  validateScaleTypeForRole,
24
24
  isDiscretePositionScaleType
25
25
  } from "../../grammar/scales.js";
26
+ import {
27
+ validateRadialRange,
28
+ validateThetaRange
29
+ } from "../../grammar/polar.js";
26
30
  import { withScaleUnknown } from "../../grammar/scales.js";
27
31
  import { findSemanticScale } from "../../selectors/scales.js";
28
32
 
@@ -87,6 +91,7 @@ export function resolvePositionScaleDefinition(
87
91
  );
88
92
  }
89
93
  }
94
+ else if (channel === "theta") validateLinearScaleType(type);
90
95
  else validateScaleTypeForRole(type, SCALE_ROLES.quantitativePosition);
91
96
  const scale = {
92
97
  id,
@@ -108,7 +113,11 @@ export function resolvePositionScaleDefinition(
108
113
  ["ordinal", "nominal"].includes(fieldType)
109
114
  ? validateOrdinalDomain(value)
110
115
  : validateScaleDomain(value),
111
- validateRange: (_scaleType, value) => validateScaleRange(value)
116
+ validateRange: (_scaleType, value) => channel === "theta"
117
+ ? validateThetaRange(value)
118
+ : channel === "radius"
119
+ ? validateRadialRange(value)
120
+ : validateScaleRange(value)
112
121
  })
113
122
  };
114
123
  return withScaleUnknown(scale, { ...existing, ...options }, channel);
@@ -305,15 +314,15 @@ export function resolveOpacityScaleDefinition(program, options) {
305
314
  return withScaleUnknown(scale, { ...existing, ...options }, "opacity");
306
315
  }
307
316
 
308
- export function resolveOffsetScaleDefinition(program, options) {
317
+ export function resolveOffsetScaleDefinition(program, options, channel = "xOffset") {
309
318
  optionsObject(options);
310
319
  validateKeys(options, [...BASE_OPTIONS, "unknown"], "scale");
311
- const id = validateUserId(options.id ?? "xOffset", "Scale id");
320
+ const id = validateUserId(options.id ?? channel, "Scale id");
312
321
  const existing = findSemanticScale(program, id);
313
322
  return withScaleUnknown({
314
323
  id,
315
324
  type: validateOrdinalScaleType(options.type ?? existing?.type ?? "ordinal"),
316
325
  domain: validateOrdinalDomain(options.domain ?? existing?.domain ?? "auto"),
317
326
  range: validateScaleRange(options.range ?? existing?.range ?? "auto")
318
- }, { ...existing, ...options }, "xOffset");
327
+ }, { ...existing, ...options }, channel);
319
328
  }
@@ -1,7 +1,6 @@
1
1
  import { action } from "../../core/action.js";
2
2
  import { validateUserId } from "../../core/identifiers.js";
3
- import { isPlainObject } from "../../core/immutable.js";
4
- import { validateKeys } from "../../core/validation.js";
3
+ import { validateOptionObject } from "../../core/validation.js";
5
4
  import {
6
5
  isTransformedScaleType,
7
6
  SCALE_ROLES,
@@ -23,6 +22,10 @@ import {
23
22
  hasOrdinalDomain,
24
23
  normalizeScaleDefinition
25
24
  } from "../../grammar/scales.js";
25
+ import {
26
+ validateRadialRange,
27
+ validateThetaRange
28
+ } from "../../grammar/polar.js";
26
29
  import { getMarkMaterializationStep } from "../../materialization/marks.js";
27
30
  import {
28
31
  applyMaterializationPlan
@@ -32,13 +35,17 @@ import {
32
35
  requireSemanticScale
33
36
  } from "../../selectors/scales.js";
34
37
  import { findScaleConsumers } from "./consumers.js";
38
+ import { normalizePositionScaleChannel } from "../../core/vocabulary.js";
35
39
 
36
40
  const OPTIONS = Object.freeze([
37
41
  "id", "type", "domain", "range", "nice", "zero", "clamp", "reverse",
38
42
  "base", "exponent", "constant", "paddingInner", "paddingOuter", "padding",
39
- "align", "interpolate", "unknown"
43
+ "align", "interpolate", "unknown", "palette"
40
44
  ]);
41
- const EDITABLE = Object.freeze(OPTIONS.filter(option => option !== "id"));
45
+ const EDITABLE = Object.freeze(
46
+ OPTIONS.filter(option => !["id", "palette"].includes(option))
47
+ );
48
+ const REQUESTED_CHANGES = Object.freeze(OPTIONS.filter(option => option !== "id"));
42
49
 
43
50
  function resolveScaleId(program, requested) {
44
51
  if (requested !== undefined) {
@@ -60,13 +67,9 @@ function resolveScaleId(program, requested) {
60
67
  );
61
68
  }
62
69
 
63
- function normalizeChannel(channel) {
64
- return channel === "x2" ? "x" : channel === "y2" ? "y" : channel;
65
- }
66
-
67
70
  function resolveChannel(consumers, id) {
68
71
  const channels = new Set(
69
- consumers.map(consumer => normalizeChannel(consumer.channel))
72
+ consumers.map(consumer => normalizePositionScaleChannel(consumer.channel))
70
73
  );
71
74
  if (channels.size > 1) {
72
75
  throw new Error(`Scale "${id}" cannot be shared across channels.`);
@@ -76,6 +79,8 @@ function resolveChannel(consumers, id) {
76
79
 
77
80
  function validateRangeForChannel(scale, channel, value) {
78
81
  if (value === "auto") return value;
82
+ if (channel === "theta") return validateThetaRange(value);
83
+ if (channel === "radius") return validateRadialRange(value);
79
84
  if (scale.type === "sequential") {
80
85
  return validateSequentialColorRange(value);
81
86
  }
@@ -97,7 +102,7 @@ function validateTypeTransition(scale, nextType, channel, consumers) {
97
102
  if (nextType === "sequential" || isDiscretizedColorScaleType(nextType)) {
98
103
  const discretized = isDiscretizedColorScaleType(nextType);
99
104
  if (channel !== "color" || consumers.some(consumer =>
100
- consumer.encoding.fieldType === "nominal" ||
105
+ ["nominal", "ordinal"].includes(consumer.encoding.fieldType) ||
101
106
  (discretized && (
102
107
  consumer.encoding.fieldType !== "quantitative" ||
103
108
  consumer.layer.mark?.type !== "point"
@@ -113,7 +118,7 @@ function validateTypeTransition(scale, nextType, channel, consumers) {
113
118
  if (nextType === "time") {
114
119
  if (
115
120
  consumers.length > 0 &&
116
- (!["x", "y"].includes(channel) ||
121
+ (!["x", "y", "theta"].includes(channel) ||
117
122
  consumers.some(consumer => consumer.encoding.fieldType !== "temporal"))
118
123
  ) {
119
124
  throw new Error(
@@ -126,7 +131,7 @@ function validateTypeTransition(scale, nextType, channel, consumers) {
126
131
  if (discrete) {
127
132
  if (
128
133
  consumers.length > 0 &&
129
- (!["x", "y"].includes(channel) || consumers.some(consumer =>
134
+ (!["x", "y", "theta"].includes(channel) || consumers.some(consumer =>
130
135
  !["nominal", "ordinal"].includes(consumer.encoding.fieldType)
131
136
  ))
132
137
  ) {
@@ -145,13 +150,16 @@ function validateTypeTransition(scale, nextType, channel, consumers) {
145
150
  const quantitative = nextType === "linear" || isTransformedScaleType(nextType);
146
151
  if (
147
152
  !quantitative ||
148
- (consumers.length > 0 && !["x", "y"].includes(channel))
153
+ (consumers.length > 0 && !["x", "y", "theta", "radius"].includes(channel))
149
154
  ) {
150
155
  throw new Error(
151
156
  "editScale type transition currently requires a quantitative position scale."
152
157
  );
153
158
  }
154
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
+ }
155
163
  if (consumers.some(consumer => consumer.encoding.fieldType !== "quantitative")) {
156
164
  throw new Error(
157
165
  `Scale "${scale.id}" has a consumer incompatible with type "${nextType}".`
@@ -242,20 +250,35 @@ export const editScale = action(
242
250
  description: "Edit an existing scale and rematerialize its consumers."
243
251
  },
244
252
  function (args = {}) {
245
- if (!isPlainObject(args)) {
246
- throw new TypeError("editScale options must be a plain object.");
247
- }
248
- validateKeys(args, OPTIONS, "editScale");
249
- if (!EDITABLE.some(property => Object.hasOwn(args, property))) {
253
+ validateOptionObject(args, OPTIONS, "editScale");
254
+ if (!REQUESTED_CHANGES.some(property => Object.hasOwn(args, property))) {
250
255
  throw new Error("editScale requires at least one editable property.");
251
256
  }
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
+ }
252
261
 
253
262
  const id = resolveScaleId(this, args.id);
254
263
  const scale = requireSemanticScale(this, id);
255
264
  const consumers = findScaleConsumers(this, id);
256
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
+ ]);
257
280
  validateLegendTypeTransition(this, scale, args.type ?? scale.type);
258
- const definition = normalizeDefinition(scale, channel, consumers, args);
281
+ const definition = normalizeDefinition(scale, channel, consumers, patch);
259
282
 
260
283
  let next = this;
261
284
  for (const property of EDITABLE) {