ggaction 0.0.1

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 (222) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/LICENSE +21 -0
  3. package/README.md +108 -0
  4. package/package.json +84 -0
  5. package/src/AGENTS.md +119 -0
  6. package/src/ChartProgram.js +10 -0
  7. package/src/actions/boxPlots/components.js +88 -0
  8. package/src/actions/boxPlots/create.js +84 -0
  9. package/src/actions/boxPlots/index.js +10 -0
  10. package/src/actions/boxPlots/materialize.js +183 -0
  11. package/src/actions/boxPlots/options.js +147 -0
  12. package/src/actions/boxPlots/resolve.js +38 -0
  13. package/src/actions/canvas/actions.js +144 -0
  14. package/src/actions/canvas/index.js +6 -0
  15. package/src/actions/coordinates/actions.js +85 -0
  16. package/src/actions/coordinates/index.js +5 -0
  17. package/src/actions/data/box.js +40 -0
  18. package/src/actions/data/create.js +33 -0
  19. package/src/actions/data/density.js +62 -0
  20. package/src/actions/data/derived.js +52 -0
  21. package/src/actions/data/filter.js +168 -0
  22. package/src/actions/data/index.js +32 -0
  23. package/src/actions/data/interval.js +69 -0
  24. package/src/actions/data/regression.js +71 -0
  25. package/src/actions/data/shared.js +25 -0
  26. package/src/actions/encodings/appearance.js +243 -0
  27. package/src/actions/encodings/barWidth.js +60 -0
  28. package/src/actions/encodings/color.js +329 -0
  29. package/src/actions/encodings/density.js +225 -0
  30. package/src/actions/encodings/histogram.js +50 -0
  31. package/src/actions/encodings/index.js +23 -0
  32. package/src/actions/encodings/offset.js +104 -0
  33. package/src/actions/encodings/position/policies/area.js +24 -0
  34. package/src/actions/encodings/position/policies/bar.js +110 -0
  35. package/src/actions/encodings/position/policies/common.js +17 -0
  36. package/src/actions/encodings/position/policies/index.js +25 -0
  37. package/src/actions/encodings/position/policies/line.js +74 -0
  38. package/src/actions/encodings/position/policies/point.js +19 -0
  39. package/src/actions/encodings/position/policies/rule.js +17 -0
  40. package/src/actions/encodings/position/resolve.js +170 -0
  41. package/src/actions/encodings/position.js +176 -0
  42. package/src/actions/encodings/ranged.js +276 -0
  43. package/src/actions/encodings/ruleAppearance.js +47 -0
  44. package/src/actions/encodings/shared.js +164 -0
  45. package/src/actions/encodings/strokeDash.js +145 -0
  46. package/src/actions/errorBands/components.js +81 -0
  47. package/src/actions/errorBands/create.js +159 -0
  48. package/src/actions/errorBands/index.js +7 -0
  49. package/src/actions/errorBands/resolve.js +28 -0
  50. package/src/actions/errorBars/components.js +62 -0
  51. package/src/actions/errorBars/create.js +144 -0
  52. package/src/actions/errorBars/index.js +7 -0
  53. package/src/actions/errorBars/resolve.js +25 -0
  54. package/src/actions/guides/axes/axes.js +204 -0
  55. package/src/actions/guides/axes/axis.js +131 -0
  56. package/src/actions/guides/axes/index.js +17 -0
  57. package/src/actions/guides/axes/labels.js +231 -0
  58. package/src/actions/guides/axes/lines.js +149 -0
  59. package/src/actions/guides/axes/policy.js +155 -0
  60. package/src/actions/guides/axes/tickGroups.js +149 -0
  61. package/src/actions/guides/axes/ticks.js +179 -0
  62. package/src/actions/guides/axes/titles.js +264 -0
  63. package/src/actions/guides/grids/grid.js +228 -0
  64. package/src/actions/guides/grids/index.js +1 -0
  65. package/src/actions/guides/grids/resolve.js +258 -0
  66. package/src/actions/guides/guides.js +138 -0
  67. package/src/actions/guides/index.js +11 -0
  68. package/src/actions/guides/legends/categorical/actions.js +329 -0
  69. package/src/actions/guides/legends/categorical/components.js +140 -0
  70. package/src/actions/guides/legends/categorical/index.js +49 -0
  71. package/src/actions/guides/legends/categorical/layout.js +445 -0
  72. package/src/actions/guides/legends/categorical/options.js +188 -0
  73. package/src/actions/guides/legends/categorical/recipes.js +120 -0
  74. package/src/actions/guides/legends/categorical/resolve.js +131 -0
  75. package/src/actions/guides/legends/categorical/symbols.js +280 -0
  76. package/src/actions/guides/legends/categorical/validation.js +27 -0
  77. package/src/actions/guides/legends/continuous/common.js +273 -0
  78. package/src/actions/guides/legends/continuous/gradient.js +363 -0
  79. package/src/actions/guides/legends/continuous/index.js +25 -0
  80. package/src/actions/guides/legends/continuous/interval.js +234 -0
  81. package/src/actions/guides/legends/continuous/opacity.js +382 -0
  82. package/src/actions/guides/legends/edit.js +314 -0
  83. package/src/actions/guides/legends/index.js +9 -0
  84. package/src/actions/guides/legends/size.js +220 -0
  85. package/src/actions/guides/tickValues.js +84 -0
  86. package/src/actions/index.js +31 -0
  87. package/src/actions/intervals/resolve.js +352 -0
  88. package/src/actions/marks/area.js +369 -0
  89. package/src/actions/marks/bar/create.js +46 -0
  90. package/src/actions/marks/bar/edit.js +90 -0
  91. package/src/actions/marks/bar/index.js +9 -0
  92. package/src/actions/marks/bar/materialize.js +130 -0
  93. package/src/actions/marks/index.js +13 -0
  94. package/src/actions/marks/line.js +248 -0
  95. package/src/actions/marks/point.js +420 -0
  96. package/src/actions/marks/rule.js +287 -0
  97. package/src/actions/marks/shared.js +112 -0
  98. package/src/actions/primitives/createGraphics.js +163 -0
  99. package/src/actions/primitives/editGraphics.js +208 -0
  100. package/src/actions/primitives/index.js +9 -0
  101. package/src/actions/primitives/semantic.js +168 -0
  102. package/src/actions/primitives/semanticValue.js +182 -0
  103. package/src/actions/regression/components.js +216 -0
  104. package/src/actions/regression/create.js +102 -0
  105. package/src/actions/regression/index.js +23 -0
  106. package/src/actions/regression/resolve.js +57 -0
  107. package/src/actions/scales/consumers.js +283 -0
  108. package/src/actions/scales/create.js +148 -0
  109. package/src/actions/scales/definitions.js +319 -0
  110. package/src/actions/scales/edit.js +290 -0
  111. package/src/actions/scales/index.js +11 -0
  112. package/src/actions/scales/materialize.js +466 -0
  113. package/src/actions/scales/quantitativeColor.js +45 -0
  114. package/src/actions/selection/actions.js +384 -0
  115. package/src/actions/selection/index.js +23 -0
  116. package/src/actions/titles/actions.js +235 -0
  117. package/src/actions/titles/index.js +19 -0
  118. package/src/actions/titles/resolve.js +321 -0
  119. package/src/core/ChartProgram.js +316 -0
  120. package/src/core/action.js +120 -0
  121. package/src/core/identifiers.js +28 -0
  122. package/src/core/immutable.js +94 -0
  123. package/src/core/specs.js +29 -0
  124. package/src/core/textMetrics.js +23 -0
  125. package/src/core/validation.js +54 -0
  126. package/src/core/vocabulary.js +58 -0
  127. package/src/extension.js +2 -0
  128. package/src/grammar/aggregate.js +259 -0
  129. package/src/grammar/areaSeries.js +217 -0
  130. package/src/grammar/bars/aggregate.js +98 -0
  131. package/src/grammar/bars/geometry.js +54 -0
  132. package/src/grammar/bars/policy.js +90 -0
  133. package/src/grammar/boxPlot.js +140 -0
  134. package/src/grammar/coordinates.js +25 -0
  135. package/src/grammar/curveCommands.js +356 -0
  136. package/src/grammar/density.js +296 -0
  137. package/src/grammar/filter.js +169 -0
  138. package/src/grammar/histogram.js +332 -0
  139. package/src/grammar/interval.js +343 -0
  140. package/src/grammar/lineSeries.js +197 -0
  141. package/src/grammar/markFilter.js +46 -0
  142. package/src/grammar/markSelection.js +326 -0
  143. package/src/grammar/palettes.js +208 -0
  144. package/src/grammar/pathCommands.js +86 -0
  145. package/src/grammar/pointShapes.js +164 -0
  146. package/src/grammar/positionCompatibility.js +32 -0
  147. package/src/grammar/regression.js +578 -0
  148. package/src/grammar/ruleAppearance.js +12 -0
  149. package/src/grammar/rules.js +90 -0
  150. package/src/grammar/scales/appearance.js +185 -0
  151. package/src/grammar/scales/color.js +349 -0
  152. package/src/grammar/scales/continuous.js +111 -0
  153. package/src/grammar/scales/definition.js +153 -0
  154. package/src/grammar/scales/discretized.js +159 -0
  155. package/src/grammar/scales/fields.js +133 -0
  156. package/src/grammar/scales/index.js +12 -0
  157. package/src/grammar/scales/mapping.js +27 -0
  158. package/src/grammar/scales/ordinal.js +221 -0
  159. package/src/grammar/scales/policies.js +50 -0
  160. package/src/grammar/scales/temporal.js +53 -0
  161. package/src/grammar/scales/transformed.js +236 -0
  162. package/src/grammar/scales/types.js +102 -0
  163. package/src/grammar/scales/validation.js +66 -0
  164. package/src/grammar/scales.js +1 -0
  165. package/src/grammar/schemas/concreteGraphic.js +73 -0
  166. package/src/grammar/schemas/graphic.js +131 -0
  167. package/src/grammar/schemas/graphicBounds.js +146 -0
  168. package/src/grammar/schemas/graphicTree.js +133 -0
  169. package/src/grammar/schemas/mark.js +4 -0
  170. package/src/grammar/schemas/semanticPath.js +174 -0
  171. package/src/grammar/seriesLayout.js +76 -0
  172. package/src/grammar/ticks.js +149 -0
  173. package/src/grammar/transforms.js +37 -0
  174. package/src/index.js +2 -0
  175. package/src/layout/canvas.js +114 -0
  176. package/src/layout/grid.js +17 -0
  177. package/src/layout/legend.js +51 -0
  178. package/src/layout/text.js +62 -0
  179. package/src/layout/title.js +107 -0
  180. package/src/materialization/bars/aggregate.js +184 -0
  181. package/src/materialization/bars/grouped.js +117 -0
  182. package/src/materialization/bars/histogram.js +163 -0
  183. package/src/materialization/bars/ranged.js +53 -0
  184. package/src/materialization/bars/resolve.js +56 -0
  185. package/src/materialization/dataProvenance.js +15 -0
  186. package/src/materialization/density.js +29 -0
  187. package/src/materialization/dependencies.js +134 -0
  188. package/src/materialization/encodings.js +50 -0
  189. package/src/materialization/graphicHierarchy.js +104 -0
  190. package/src/materialization/legends.js +20 -0
  191. package/src/materialization/marks.js +110 -0
  192. package/src/materialization/planner.js +38 -0
  193. package/src/materialization/selection/filter.js +40 -0
  194. package/src/materialization/selection/items.js +503 -0
  195. package/src/materialization/selection/path.js +37 -0
  196. package/src/materialization/selection/point.js +118 -0
  197. package/src/materialization/selection/policies/area.js +10 -0
  198. package/src/materialization/selection/policies/bar.js +10 -0
  199. package/src/materialization/selection/policies/index.js +40 -0
  200. package/src/materialization/selection/policies/line.js +10 -0
  201. package/src/materialization/selection/policies/point.js +10 -0
  202. package/src/materialization/selection/policies/rule.js +10 -0
  203. package/src/materialization/selection/state.js +67 -0
  204. package/src/materialization/selection/styles.js +183 -0
  205. package/src/renderers/canvas/circle.js +62 -0
  206. package/src/renderers/canvas/index.js +132 -0
  207. package/src/renderers/canvas/line.js +54 -0
  208. package/src/renderers/canvas/path.js +98 -0
  209. package/src/renderers/canvas/rect.js +57 -0
  210. package/src/renderers/canvas/text.js +96 -0
  211. package/src/renderers/canvas/validation.js +23 -0
  212. package/src/renderers/png.js +29 -0
  213. package/src/selectors/coordinates.js +13 -0
  214. package/src/selectors/datasets.js +13 -0
  215. package/src/selectors/index.js +4 -0
  216. package/src/selectors/layers.js +32 -0
  217. package/src/selectors/scales.js +21 -0
  218. package/src/theme/defaults.js +13 -0
  219. package/types/extension.d.ts +14 -0
  220. package/types/index.d.ts +81 -0
  221. package/types/png.d.ts +14 -0
  222. package/types/program.d.ts +1096 -0
@@ -0,0 +1,466 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateUserId } from "../../core/identifiers.js";
3
+ import { validateKeys } from "../../core/validation.js";
4
+ import { resolveHistogramBins } from "../../grammar/histogram.js";
5
+ import { resolveGraphicBounds } from "../../layout/canvas.js";
6
+ import { normalizeOffsetPadding } from "../../grammar/bars/geometry.js";
7
+ import {
8
+ BAR_GRAINS,
9
+ resolveBarChannels,
10
+ resolveBarGrain
11
+ } from "../../grammar/bars/policy.js";
12
+ import {
13
+ mapContinuousScaleValues,
14
+ mapDiscretizedColors,
15
+ mapSequentialColors,
16
+ mapOrdinalValues,
17
+ resolveColorRange,
18
+ resolveDiscretizedColorScale,
19
+ resolveContinuousDomain,
20
+ resolveOrdinalDomain,
21
+ resolveOrdinalOffsetScale,
22
+ resolveOrdinalPositionScale,
23
+ resolveDiscretePositionScale,
24
+ resolveScaleRange,
25
+ resolveOpacityRange,
26
+ resolveSequentialColorStops,
27
+ resolveShapeRange,
28
+ resolveSizeRange,
29
+ resolveStrokeDashRange,
30
+ validateLinearScaleType,
31
+ validateOrdinalScaleType,
32
+ validateTimeScaleType,
33
+ isDiscretePositionScaleType,
34
+ isDiscretizedColorScaleType,
35
+ isContinuousColorScaleType,
36
+ isOrdinalScaleType,
37
+ isTransformedScaleType,
38
+ normalizeTransformParameters,
39
+ resolveTransformedDomain,
40
+ SCALE_ROLES,
41
+ validateScaleTypeForRole
42
+ } from "../../grammar/scales.js";
43
+ import {
44
+ findScale,
45
+ findScaleConsumers,
46
+ resolveConsumerValues,
47
+ resolveSeriesLayoutScaleValues
48
+ } from "./consumers.js";
49
+ import {
50
+ applyMaterializationPlan,
51
+ planScaleGuideRematerialization
52
+ } from "../../materialization/dependencies.js";
53
+
54
+ const OPTIONS = Object.freeze(["id", "guides"]);
55
+
56
+ function resolveTemporalBarBand(consumers, domain, range, values) {
57
+ const temporalBars = consumers.filter(consumer => {
58
+ const channels = resolveBarChannels(consumer.layer);
59
+ return resolveBarGrain(consumer.layer) === BAR_GRAINS.aggregate &&
60
+ channels?.category === consumer.channel &&
61
+ consumer.encoding.fieldType === "temporal";
62
+ });
63
+ if (temporalBars.length === 0) return undefined;
64
+ if (temporalBars.length !== consumers.length) {
65
+ throw new Error("A temporal bar position scale cannot share a non-bar layout policy.");
66
+ }
67
+ const ordered = [...new Set(values)].sort((left, right) => left - right);
68
+ if (ordered.length < 2) {
69
+ throw new Error("Temporal bar position requires at least two distinct values.");
70
+ }
71
+ const minimumGap = Math.min(
72
+ ...ordered.slice(1).map((value, index) => value - ordered[index])
73
+ );
74
+ const domainSpan = Math.abs(domain[1] - domain[0]);
75
+ if (!(minimumGap > 0) || !(domainSpan > 0)) {
76
+ throw new Error("Temporal bar position requires an increasing time domain.");
77
+ }
78
+ const direction = Math.sign(range[1] - range[0]) || 1;
79
+ const estimatedBandwidth = Math.abs(range[1] - range[0]) * minimumGap /
80
+ (domainSpan + minimumGap);
81
+ const resolvedRange = [
82
+ range[0] + direction * estimatedBandwidth / 2,
83
+ range[1] - direction * estimatedBandwidth / 2
84
+ ];
85
+ const positions = ordered.map(value =>
86
+ resolvedRange[0] +
87
+ (value - domain[0]) / (domain[1] - domain[0]) *
88
+ (resolvedRange[1] - resolvedRange[0])
89
+ );
90
+ const bandwidth = Math.min(
91
+ ...positions.slice(1).map((value, index) => Math.abs(value - positions[index]))
92
+ );
93
+ return {
94
+ bandwidth,
95
+ range: resolvedRange
96
+ };
97
+ }
98
+
99
+ function validateOptions(args) {
100
+ validateKeys(args, OPTIONS, "rematerializeScale");
101
+ }
102
+
103
+ export const rematerializeScale = action(
104
+ {
105
+ op: "rematerializeScale",
106
+ description: "Recompute every concrete consumer of a scale."
107
+ },
108
+ function (args = {}) {
109
+ validateOptions(args);
110
+ if (args.guides !== undefined && typeof args.guides !== "boolean") {
111
+ throw new TypeError("rematerializeScale guides must be a boolean.");
112
+ }
113
+ const id = validateUserId(args.id, "Scale id");
114
+ const scale = findScale(this, id);
115
+ const consumers = findScaleConsumers(this, id);
116
+
117
+ if (consumers.length === 0) {
118
+ throw new Error(`Scale "${id}" has no supported consumers.`);
119
+ }
120
+ const channels = new Set(
121
+ consumers.map(consumer =>
122
+ consumer.channel === "x2"
123
+ ? "x"
124
+ : consumer.channel === "y2" ? "y" : consumer.channel
125
+ )
126
+ );
127
+ if (channels.size !== 1) {
128
+ throw new Error(`Scale "${id}" cannot be shared across channels.`);
129
+ }
130
+
131
+ const channel = consumers[0].channel === "x2"
132
+ ? "x"
133
+ : consumers[0].channel === "y2" ? "y" : consumers[0].channel;
134
+ const valuesByConsumer = consumers.map(consumer => ({
135
+ consumer,
136
+ values: resolveConsumerValues(this, consumer)
137
+ }));
138
+ const allValues = valuesByConsumer
139
+ .flatMap(item => item.values)
140
+ .filter(value => value !== undefined);
141
+ const isSequentialColor = channel === "color" &&
142
+ isContinuousColorScaleType(scale.type);
143
+ const isDiscretizedColor = channel === "color" &&
144
+ isDiscretizedColorScaleType(scale.type);
145
+ const isOrdinalAppearance =
146
+ ["color", "strokeDash", "shape"].includes(channel) &&
147
+ isOrdinalScaleType(scale.type);
148
+ const isOrdinalOffset = channel === "xOffset";
149
+ const isOrdinalPosition =
150
+ !isOrdinalAppearance && !isOrdinalOffset && isOrdinalScaleType(scale.type);
151
+ const isDiscretePosition =
152
+ !isOrdinalAppearance && !isOrdinalOffset &&
153
+ isDiscretePositionScaleType(scale.type);
154
+ const binnedBars = valuesByConsumer.filter(
155
+ ({ consumer }) =>
156
+ consumer.layer.mark?.type === "bar" &&
157
+ consumer.encoding.bin !== undefined
158
+ );
159
+ const seriesLayouts = valuesByConsumer.map(({ consumer }) =>
160
+ resolveSeriesLayoutScaleValues(this, consumer)
161
+ );
162
+ let domain;
163
+
164
+ let discretizedScale;
165
+ if (isDiscretizedColor) {
166
+ discretizedScale = resolveDiscretizedColorScale({
167
+ type: scale.type,
168
+ domain: scale.domain,
169
+ range: scale.range,
170
+ values: allValues
171
+ });
172
+ domain = discretizedScale.domain;
173
+ } else if (binnedBars.length > 0) {
174
+ if (channel !== "x" || binnedBars.length !== valuesByConsumer.length) {
175
+ throw new Error(
176
+ `Binned scale "${id}" cannot be shared with an unbinned consumer.`
177
+ );
178
+ }
179
+ const binDefinitions = new Set(
180
+ binnedBars.map(({ consumer }) => JSON.stringify(consumer.encoding.bin))
181
+ );
182
+ if (binDefinitions.size !== 1) {
183
+ throw new Error(
184
+ `Binned scale "${id}" requires one shared bin definition.`
185
+ );
186
+ }
187
+ domain = resolveHistogramBins({
188
+ values: allValues,
189
+ bin: binnedBars[0].consumer.encoding.bin,
190
+ domain: scale.domain,
191
+ nice: scale.nice ?? true,
192
+ zero: scale.zero ?? false
193
+ }).domain;
194
+ } else if (seriesLayouts.some(Boolean)) {
195
+ const activeLayouts = seriesLayouts.filter(Boolean);
196
+ const layouts = new Set(activeLayouts.map(item => item.layout));
197
+ if (layouts.size !== 1) {
198
+ throw new Error(`Series layout scale "${id}" requires one layout policy.`);
199
+ }
200
+ const layoutConsumers = valuesByConsumer.filter(
201
+ (_, index) => seriesLayouts[index] !== undefined
202
+ );
203
+ const directConsumers = valuesByConsumer.filter(
204
+ (_, index) => seriesLayouts[index] === undefined
205
+ );
206
+ const compatibleDirect = directConsumers.every(({ consumer }) =>
207
+ layoutConsumers.every(({ consumer: layoutConsumer }) => {
208
+ const aggregate = layoutConsumer.encoding.aggregate;
209
+ return aggregate !== "count" &&
210
+ consumer.encoding.fieldType === "quantitative" &&
211
+ consumer.encoding.field === layoutConsumer.encoding.field;
212
+ })
213
+ );
214
+ if (!compatibleDirect) {
215
+ throw new Error(
216
+ `Series layout scale "${id}" cannot be shared with another policy.`
217
+ );
218
+ }
219
+ const layout = activeLayouts[0].layout;
220
+ const values = [
221
+ ...activeLayouts.flatMap(item => item.values),
222
+ ...directConsumers.flatMap(item => item.values)
223
+ ];
224
+ domain = resolveContinuousDomain({
225
+ domain: scale.domain,
226
+ values,
227
+ type: scale.type,
228
+ nice: layout === "fill" && scale.domain === "auto"
229
+ ? false
230
+ : scale.nice,
231
+ zero: layout === "fill" && scale.domain === "auto"
232
+ ? false
233
+ : scale.zero
234
+ });
235
+ } else {
236
+ domain = isOrdinalAppearance || isOrdinalOffset || isOrdinalPosition ||
237
+ isDiscretePosition
238
+ ? resolveOrdinalDomain(scale.domain, allValues)
239
+ : isTransformedScaleType(scale.type)
240
+ ? resolveTransformedDomain({
241
+ type: scale.type,
242
+ domain: scale.domain,
243
+ values: allValues,
244
+ nice: scale.nice ?? false,
245
+ zero: scale.zero ?? false,
246
+ ...(scale.base === undefined ? {} : { base: scale.base }),
247
+ ...(scale.exponent === undefined ? {} : { exponent: scale.exponent }),
248
+ ...(scale.constant === undefined ? {} : { constant: scale.constant })
249
+ })
250
+ : resolveContinuousDomain({
251
+ domain: scale.domain,
252
+ values: allValues,
253
+ type: scale.type,
254
+ nice: scale.nice,
255
+ zero: scale.zero
256
+ });
257
+ }
258
+
259
+ let range;
260
+ if (channel === "color") {
261
+ range = isDiscretizedColor
262
+ ? discretizedScale.range
263
+ : isSequentialColor
264
+ ? resolveSequentialColorStops(scale.range)
265
+ : resolveColorRange(scale.range, domain.length);
266
+ }
267
+ else if (channel === "strokeDash") range = resolveStrokeDashRange(scale.range);
268
+ else if (channel === "shape") range = resolveShapeRange(scale.range);
269
+ else if (channel === "size") range = resolveSizeRange(scale.range);
270
+ else if (channel === "opacity") range = resolveOpacityRange(scale.range);
271
+ else if (isOrdinalOffset) range = undefined;
272
+ else {
273
+ range = resolveScaleRange(
274
+ scale.range,
275
+ channel,
276
+ resolveGraphicBounds(this)
277
+ );
278
+ }
279
+ if (channel === "shape" && domain.length > range.length) {
280
+ throw new Error(
281
+ `Shape scale "${id}" requires at least one distinct shape per domain value.`
282
+ );
283
+ }
284
+
285
+ let resolvedScale = isDiscretizedColor
286
+ ? discretizedScale
287
+ : isOrdinalOffset
288
+ ? resolveOrdinalOffsetScale({
289
+ domain: scale.domain,
290
+ values: allValues,
291
+ range: scale.range,
292
+ parentBandwidth: (() => {
293
+ const bandwidths = consumers.map(consumer => {
294
+ if (consumer.layer.encoding?.x?.bin !== undefined) return 1;
295
+ const xScaleId = consumer.layer.encoding?.x?.scale;
296
+ return this.resolvedScales[xScaleId]?.bandwidth;
297
+ });
298
+ if (
299
+ bandwidths.some(value => !Number.isFinite(value)) ||
300
+ new Set(bandwidths).size !== 1
301
+ ) {
302
+ throw new Error(
303
+ `xOffset scale "${id}" requires one shared resolved x bandwidth.`
304
+ );
305
+ }
306
+ return bandwidths[0];
307
+ })(),
308
+ ...(() => {
309
+ const paddings = consumers.map(consumer => normalizeOffsetPadding(
310
+ this.markConfigs[consumer.layer.id]?.xOffset
311
+ ));
312
+ const signatures = new Set(
313
+ paddings.map(padding => JSON.stringify(padding))
314
+ );
315
+ if (signatures.size !== 1) {
316
+ throw new Error(
317
+ `xOffset scale "${id}" requires one shared padding policy.`
318
+ );
319
+ }
320
+ return paddings[0];
321
+ })()
322
+ })
323
+ : isDiscretePosition
324
+ ? resolveDiscretePositionScale({
325
+ type: scale.type,
326
+ domain: scale.domain,
327
+ values: allValues,
328
+ range: scale.range,
329
+ channel,
330
+ bounds: resolveGraphicBounds(this),
331
+ ...(scale.type === "band"
332
+ ? {
333
+ paddingInner: scale.paddingInner,
334
+ paddingOuter: scale.paddingOuter
335
+ }
336
+ : { padding: scale.padding }),
337
+ align: scale.align,
338
+ unknown: scale.unknown
339
+ })
340
+ : isOrdinalPosition
341
+ ? resolveOrdinalPositionScale({
342
+ domain: scale.domain,
343
+ values: allValues,
344
+ range: scale.range,
345
+ channel,
346
+ bounds: resolveGraphicBounds(this),
347
+ unknown: scale.unknown
348
+ })
349
+ : {
350
+ type: isSequentialColor
351
+ ? "sequential"
352
+ : isOrdinalAppearance
353
+ ? validateOrdinalScaleType(scale.type)
354
+ : scale.type === "time"
355
+ ? validateTimeScaleType(scale.type)
356
+ : isTransformedScaleType(scale.type)
357
+ ? validateScaleTypeForRole(
358
+ scale.type,
359
+ SCALE_ROLES.quantitativePosition
360
+ )
361
+ : validateLinearScaleType(scale.type),
362
+ domain,
363
+ range,
364
+ ...(scale.clamp === undefined ? {} : { clamp: scale.clamp }),
365
+ ...(scale.type === "log"
366
+ ? { base: normalizeTransformParameters("log", {
367
+ ...(scale.base === undefined ? {} : { base: scale.base })
368
+ }).base }
369
+ : scale.type === "pow"
370
+ ? { exponent: normalizeTransformParameters("pow", {
371
+ ...(scale.exponent === undefined
372
+ ? {}
373
+ : { exponent: scale.exponent })
374
+ }).exponent }
375
+ : scale.type === "symlog"
376
+ ? { constant: normalizeTransformParameters("symlog", {
377
+ ...(scale.constant === undefined
378
+ ? {}
379
+ : { constant: scale.constant })
380
+ }).constant }
381
+ : {}),
382
+ ...(isSequentialColor ? { interpolate: scale.interpolate ?? "rgb" } : {}),
383
+ ...(Object.hasOwn(scale, "unknown")
384
+ ? { unknown: scale.unknown }
385
+ : {})
386
+ };
387
+ if (Object.hasOwn(scale, "unknown") && !Object.hasOwn(resolvedScale, "unknown")) {
388
+ resolvedScale = { ...resolvedScale, unknown: scale.unknown };
389
+ }
390
+ if (scale.type === "time" && ["x", "y"].includes(channel)) {
391
+ const temporalBarBand = resolveTemporalBarBand(
392
+ consumers,
393
+ domain,
394
+ range,
395
+ allValues
396
+ );
397
+ if (temporalBarBand !== undefined) {
398
+ resolvedScale = {
399
+ ...resolvedScale,
400
+ range: temporalBarBand.range,
401
+ bandwidth: temporalBarBand.bandwidth
402
+ };
403
+ }
404
+ }
405
+ if (scale.reverse === true) {
406
+ const range = [...resolvedScale.range].reverse();
407
+ const direction = Math.sign(range[1] - range[0]) || 1;
408
+ const offset = resolvedScale.start === undefined
409
+ ? undefined
410
+ : Math.abs(resolvedScale.start - resolvedScale.range[0]);
411
+ resolvedScale = {
412
+ ...resolvedScale,
413
+ range,
414
+ ...(resolvedScale.step === undefined
415
+ ? {}
416
+ : { step: -resolvedScale.step }),
417
+ ...(offset === undefined ? {} : { start: range[0] + direction * offset })
418
+ };
419
+ }
420
+ let next = this._withResolvedScale(id, resolvedScale);
421
+
422
+ for (const { consumer, values } of valuesByConsumer) {
423
+ if (
424
+ consumer.layer.mark?.type === "point" &&
425
+ ["x", "y"].includes(channel)
426
+ ) {
427
+ next = next.rematerializePointMark({ id: consumer.layer.id });
428
+ continue;
429
+ }
430
+ if (
431
+ ["line", "bar", "area", "rule"].includes(consumer.layer.mark?.type) ||
432
+ (consumer.layer.mark?.type === "point" &&
433
+ ["x", "y"].includes(channel) && isOrdinalPosition) ||
434
+ (consumer.layer.mark?.type === "point" && ["size", "shape"].includes(channel))
435
+ ) continue;
436
+ next = next.editGraphics({
437
+ target: consumer.layer.id,
438
+ property: channel === "color" ? "fill" : channel,
439
+ value: isDiscretizedColor
440
+ ? mapDiscretizedColors(values, resolvedScale)
441
+ : isSequentialColor
442
+ ? mapSequentialColors(values, resolvedScale.domain, resolvedScale.range, {
443
+ interpolation: resolvedScale.interpolate,
444
+ clamp: resolvedScale.clamp ?? false,
445
+ ...(Object.hasOwn(resolvedScale, "unknown")
446
+ ? { unknown: resolvedScale.unknown }
447
+ : {})
448
+ })
449
+ : isOrdinalAppearance
450
+ ? mapOrdinalValues(values, domain, resolvedScale.range, {
451
+ ...(Object.hasOwn(resolvedScale, "unknown")
452
+ ? { unknown: resolvedScale.unknown }
453
+ : {})
454
+ })
455
+ : mapContinuousScaleValues(values, resolvedScale)
456
+ });
457
+ }
458
+
459
+ return args.guides === false
460
+ ? next
461
+ : applyMaterializationPlan(
462
+ next,
463
+ planScaleGuideRematerialization(next, id)
464
+ );
465
+ }
466
+ );
@@ -0,0 +1,45 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateKeys } from "../../core/validation.js";
3
+ import { findSemanticScale } from "../../selectors/scales.js";
4
+
5
+ const OPTIONS = Object.freeze([
6
+ "id",
7
+ "type",
8
+ "domain",
9
+ "range",
10
+ "interpolate",
11
+ "clamp",
12
+ "reverse",
13
+ "unknown"
14
+ ]);
15
+
16
+ export const setQuantitativeColorScale = action(
17
+ {
18
+ op: "setQuantitativeColorScale",
19
+ description: "Create or update an internal quantitative color scale."
20
+ },
21
+ function (args = {}) {
22
+ validateKeys(args, OPTIONS, "setQuantitativeColorScale");
23
+ if (!["sequential", "quantize", "quantile", "threshold"].includes(args.type)) {
24
+ throw new Error(`Unsupported quantitative color scale type "${args.type}".`);
25
+ }
26
+ const existing = findSemanticScale(this, args.id);
27
+ if (existing !== undefined && existing.type !== args.type) {
28
+ throw new Error(
29
+ `Scale "${args.id}" cannot change type from "${existing.type}" to "${args.type}".`
30
+ );
31
+ }
32
+ let next = this;
33
+ for (const property of [
34
+ "type", "domain", "range", "interpolate", "clamp", "reverse", "unknown"
35
+ ]) {
36
+ if (!Object.hasOwn(args, property)) continue;
37
+ if (existing?.[property] === args[property]) continue;
38
+ next = next.editSemantic({
39
+ property: `scale[${args.id}].${property}`,
40
+ value: args[property]
41
+ });
42
+ }
43
+ return next;
44
+ }
45
+ );