ggaction 0.0.13 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +20 -11
  3. package/knowledge/action-cards.json +1 -1
  4. package/knowledge/intent-taxonomy.json +1 -1
  5. package/knowledge/mcp-resources.json +1 -1
  6. package/package.json +8 -3
  7. package/src/actions/basic.js +2 -2
  8. package/src/actions/boxPlots/components.js +14 -4
  9. package/src/actions/boxPlots/edit.js +31 -5
  10. package/src/actions/boxPlots/materialize.js +11 -3
  11. package/src/actions/boxPlots/options.js +48 -16
  12. package/src/actions/categoryOrder/index.js +15 -4
  13. package/src/actions/charts/area.js +5 -1
  14. package/src/actions/charts/bar.js +2 -1
  15. package/src/actions/charts/density.js +6 -2
  16. package/src/actions/charts/ecdf.js +4 -1
  17. package/src/actions/charts/endpoints.js +25 -3
  18. package/src/actions/charts/heatmap.js +2 -1
  19. package/src/actions/charts/histogram.js +4 -3
  20. package/src/actions/charts/horizon.js +3 -1
  21. package/src/actions/charts/interval-regression.js +5 -2
  22. package/src/actions/charts/line.js +3 -1
  23. package/src/actions/charts/parallel.js +8 -2
  24. package/src/actions/charts/pie.js +10 -2
  25. package/src/actions/charts/polar.js +15 -4
  26. package/src/actions/charts/radar.js +8 -2
  27. package/src/actions/charts/radial.js +10 -2
  28. package/src/actions/charts/rug-strip.js +6 -2
  29. package/src/actions/charts/scatter.js +3 -1
  30. package/src/actions/composition/actions.js +13 -4
  31. package/src/actions/coordinates/actions.js +1 -1
  32. package/src/actions/coordinates/edit.js +76 -0
  33. package/src/actions/data/bin.js +1 -1
  34. package/src/actions/data/bin2d.js +57 -185
  35. package/src/actions/data/complete.js +26 -0
  36. package/src/actions/data/computed.js +2 -2
  37. package/src/actions/data/create.js +2 -2
  38. package/src/actions/data/density.js +12 -20
  39. package/src/actions/data/derived.js +47 -17
  40. package/src/actions/data/edit.js +539 -0
  41. package/src/actions/data/filter.js +14 -2
  42. package/src/actions/data/impute.js +26 -0
  43. package/src/actions/data/index.js +14 -0
  44. package/src/actions/data/normalize.js +27 -0
  45. package/src/actions/data/regression.js +16 -21
  46. package/src/actions/data/revisionBindings.js +145 -0
  47. package/src/actions/data/shared.js +20 -4
  48. package/src/actions/data/summary.js +1 -1
  49. package/src/actions/data/timeUnit.js +4 -1
  50. package/src/actions/data/window.js +1 -1
  51. package/src/actions/encodings/channels.js +415 -0
  52. package/src/actions/encodings/color/index.js +2 -1
  53. package/src/actions/encodings/density.js +5 -2
  54. package/src/actions/encodings/histogram.js +3 -2
  55. package/src/actions/encodings/index.js +4 -0
  56. package/src/actions/encodings/offset.js +20 -13
  57. package/src/actions/encodings/position/apply.js +53 -7
  58. package/src/actions/encodings/position/index.js +10 -2
  59. package/src/actions/encodings/position/policies/bar.js +9 -0
  60. package/src/actions/encodings/position/resolve.js +24 -7
  61. package/src/actions/encodings/remove.js +9 -4
  62. package/src/actions/encodings/ruleAppearance.js +1 -23
  63. package/src/actions/encodings/seriesLayout.js +1 -0
  64. package/src/actions/encodings/shared.js +82 -14
  65. package/src/actions/encodings/stroke.js +226 -0
  66. package/src/actions/errorBands/create.js +23 -3
  67. package/src/actions/errorBands/edit.js +21 -6
  68. package/src/actions/errorBars/create.js +35 -7
  69. package/src/actions/errorBars/edit.js +19 -4
  70. package/src/actions/facets/actions.js +237 -57
  71. package/src/actions/facets/derive.js +91 -26
  72. package/src/actions/gradientPlots/components.js +10 -2
  73. package/src/actions/gradientPlots/edit.js +14 -4
  74. package/src/actions/gradientPlots/materialize.js +3 -1
  75. package/src/actions/gradientPlots/options.js +10 -2
  76. package/src/actions/guides/applicability.js +8 -2
  77. package/src/actions/guides/axes/labels.js +58 -12
  78. package/src/actions/guides/axes/lines.js +6 -2
  79. package/src/actions/guides/axes/parallel/resolve.js +6 -2
  80. package/src/actions/guides/axes/tickGroups.js +2 -1
  81. package/src/actions/guides/axes/ticks.js +6 -2
  82. package/src/actions/guides/axes/titles.js +11 -3
  83. package/src/actions/guides/grids/resolve.js +7 -2
  84. package/src/actions/guides/guides.js +19 -3
  85. package/src/actions/guides/legends/blocks.js +426 -0
  86. package/src/actions/guides/legends/categorical/actions.js +134 -48
  87. package/src/actions/guides/legends/categorical/components.js +17 -12
  88. package/src/actions/guides/legends/categorical/index.js +37 -33
  89. package/src/actions/guides/legends/categorical/layout.js +97 -15
  90. package/src/actions/guides/legends/categorical/options.js +2 -2
  91. package/src/actions/guides/legends/categorical/recipes.js +111 -20
  92. package/src/actions/guides/legends/categorical/resolve.js +12 -8
  93. package/src/actions/guides/legends/categorical/symbols.js +85 -30
  94. package/src/actions/guides/legends/continuous/common.js +30 -12
  95. package/src/actions/guides/legends/continuous/gradient.js +10 -7
  96. package/src/actions/guides/legends/continuous/index.js +2 -0
  97. package/src/actions/guides/legends/continuous/interval.js +8 -3
  98. package/src/actions/guides/legends/continuous/opacity.js +8 -5
  99. package/src/actions/guides/legends/continuous/stroke.js +525 -0
  100. package/src/actions/guides/legends/creation.js +14 -1
  101. package/src/actions/guides/legends/edit.js +158 -59
  102. package/src/actions/guides/legends/index.js +2 -0
  103. package/src/actions/guides/legends/lane.js +54 -19
  104. package/src/actions/guides/legends/lifecycle.js +19 -8
  105. package/src/actions/guides/legends/remove.js +13 -3
  106. package/src/actions/guides/legends/sampling.js +140 -0
  107. package/src/actions/guides/legends/size.js +93 -40
  108. package/src/actions/guides/legends/strokeWidth.js +34 -28
  109. package/src/actions/guides/legends/target.js +112 -1
  110. package/src/actions/guides/legends/transition.js +165 -16
  111. package/src/actions/guides/polar/axes/facade.js +4 -4
  112. package/src/actions/guides/polar/axes/labels.js +24 -5
  113. package/src/actions/guides/polar/axes/lines.js +10 -4
  114. package/src/actions/guides/polar/axes/shared.js +16 -0
  115. package/src/actions/guides/polar/axes/ticks.js +1 -1
  116. package/src/actions/guides/polar/axes/titles.js +1 -1
  117. package/src/actions/guides/polar/grids.js +1 -1
  118. package/src/actions/guides/polar/resolve.js +12 -8
  119. package/src/actions/index.js +14 -3
  120. package/src/actions/marks/arc/actions.js +77 -19
  121. package/src/actions/marks/area/actions.js +34 -9
  122. package/src/actions/marks/area/materialize.js +17 -1
  123. package/src/actions/marks/bar/create.js +11 -2
  124. package/src/actions/marks/bar/edit.js +20 -3
  125. package/src/actions/marks/bar/materialize.js +22 -2
  126. package/src/actions/marks/index.js +10 -1
  127. package/src/actions/marks/line/actions.js +37 -14
  128. package/src/actions/marks/line/materialize.js +29 -10
  129. package/src/actions/marks/point/create.js +8 -2
  130. package/src/actions/marks/point/edit.js +18 -6
  131. package/src/actions/marks/point/materialize.js +49 -15
  132. package/src/actions/marks/rect/actions.js +33 -12
  133. package/src/actions/marks/references.js +601 -30
  134. package/src/actions/marks/remove.js +71 -1
  135. package/src/actions/marks/rule/actions.js +47 -10
  136. package/src/actions/marks/shared.js +1 -1
  137. package/src/actions/marks/text/actions.js +74 -9
  138. package/src/actions/marks/text/index.js +4 -0
  139. package/src/actions/marks/text/layout.js +154 -6
  140. package/src/actions/marks/text/removal.js +173 -0
  141. package/src/actions/marks/text/selection.js +73 -0
  142. package/src/actions/marks/tick/actions.js +22 -5
  143. package/src/actions/primitives/editGraphics.js +26 -0
  144. package/src/actions/primitives/semantic.js +2 -1
  145. package/src/actions/primitives/semanticAction.js +83 -10
  146. package/src/actions/primitives/semanticValidation/guide.js +1 -1
  147. package/src/actions/primitives/semanticValidation/index.js +9 -1
  148. package/src/actions/primitives/semanticValidation/layer.js +5 -0
  149. package/src/actions/primitives/semanticValidation/scale.js +28 -7
  150. package/src/actions/regression/components.js +26 -11
  151. package/src/actions/regression/create.js +10 -2
  152. package/src/actions/regression/edit.js +21 -7
  153. package/src/actions/resources/index.js +7 -0
  154. package/src/actions/resources/remove.js +172 -0
  155. package/src/actions/scales/channels.js +212 -0
  156. package/src/actions/scales/consumers/common.js +1 -1
  157. package/src/actions/scales/consumers/families.js +3 -2
  158. package/src/actions/scales/consumers/index.js +19 -2
  159. package/src/actions/scales/consumers/seriesLayout.js +28 -14
  160. package/src/actions/scales/create.js +54 -22
  161. package/src/actions/scales/definitions.js +94 -40
  162. package/src/actions/scales/edit.js +66 -11
  163. package/src/actions/scales/editPolicy.js +81 -11
  164. package/src/actions/scales/index.js +6 -0
  165. package/src/actions/scales/materialize.js +4 -3
  166. package/src/actions/scales/offset.js +48 -0
  167. package/src/actions/scales/parallel.js +71 -0
  168. package/src/actions/scales/preview.js +111 -10
  169. package/src/actions/selection/actions.js +154 -30
  170. package/src/actions/selection/index.js +3 -0
  171. package/src/actions/theme/actions.js +68 -13
  172. package/src/actions/theme/composition.js +238 -0
  173. package/src/actions/theme/index.js +9 -2
  174. package/src/actions/theme/reconcile.js +315 -67
  175. package/src/actions/theme/state.js +160 -0
  176. package/src/actions/violinPlots/create.js +5 -2
  177. package/src/actions/violinPlots/edit.js +1 -0
  178. package/src/core/action.js +10 -0
  179. package/src/core/compositionState.js +6 -1
  180. package/src/core/programState.js +20 -0
  181. package/src/core/resourceReferences.js +332 -0
  182. package/src/core/vocabulary.js +16 -3
  183. package/src/grammar/bars/aggregate.js +4 -1
  184. package/src/grammar/bars/geometry.js +44 -0
  185. package/src/grammar/bin.js +54 -9
  186. package/src/grammar/complete.js +279 -0
  187. package/src/grammar/computed.js +265 -64
  188. package/src/grammar/coordinates.js +37 -0
  189. package/src/grammar/density.js +239 -37
  190. package/src/grammar/displayLabels.js +57 -0
  191. package/src/grammar/facets/dependencies.js +133 -1
  192. package/src/grammar/facets/guides.js +3 -0
  193. package/src/grammar/facets/index.js +7 -47
  194. package/src/grammar/facets/scales.js +15 -18
  195. package/src/grammar/impute.js +340 -0
  196. package/src/grammar/markLabelSelection.js +57 -0
  197. package/src/grammar/markLabels.js +18 -1
  198. package/src/grammar/normalize.js +366 -0
  199. package/src/grammar/pathSeries.js +26 -8
  200. package/src/grammar/pointShapes.js +13 -4
  201. package/src/grammar/polar.js +90 -8
  202. package/src/grammar/regression/index.js +1 -0
  203. package/src/grammar/regression/parameters.js +23 -0
  204. package/src/grammar/roundedRect.js +180 -0
  205. package/src/grammar/rules.js +1 -0
  206. package/src/grammar/scales/colorConsumers.js +20 -1
  207. package/src/grammar/scales/continuous.js +2 -2
  208. package/src/grammar/scales/definition.js +27 -4
  209. package/src/grammar/scales/index.js +1 -0
  210. package/src/grammar/scales/ordinal.js +12 -3
  211. package/src/grammar/scales/policies.js +2 -2
  212. package/src/grammar/scales/size.js +456 -0
  213. package/src/grammar/schemas/concreteGraphic.js +14 -2
  214. package/src/grammar/schemas/graphic.js +15 -0
  215. package/src/grammar/schemas/graphicBounds.js +179 -30
  216. package/src/grammar/schemas/semanticPath.js +7 -2
  217. package/src/grammar/statisticalReference.js +103 -0
  218. package/src/grammar/strokeStyle.js +77 -0
  219. package/src/grammar/summary.js +78 -16
  220. package/src/grammar/timeUnit.js +216 -5
  221. package/src/grammar/transformTopology.js +4 -0
  222. package/src/grammar/transforms.js +394 -28
  223. package/src/grammar/weightedStatistics.js +374 -0
  224. package/src/grammar/window.js +221 -16
  225. package/src/layout/aspect.js +80 -0
  226. package/src/layout/facets.js +59 -5
  227. package/src/layout/labels.js +343 -0
  228. package/src/materialization/bars/aggregate.js +25 -2
  229. package/src/materialization/bars/histogram.js +84 -24
  230. package/src/materialization/bars/ranged.js +19 -3
  231. package/src/materialization/bars/resolve.js +7 -4
  232. package/src/materialization/composition.js +3 -1
  233. package/src/materialization/coordinateBounds.js +204 -0
  234. package/src/materialization/dependencies.js +36 -0
  235. package/src/materialization/facetGuides/legacyCategorical.js +3 -0
  236. package/src/materialization/facetGuides/placement.js +8 -2
  237. package/src/materialization/facetGuides/preparation.js +12 -7
  238. package/src/materialization/facetHeaders.js +346 -0
  239. package/src/materialization/facets.js +15 -65
  240. package/src/materialization/guides/resources.js +26 -0
  241. package/src/materialization/legends.js +1 -1
  242. package/src/materialization/marks/index.js +11 -5
  243. package/src/materialization/marks/policies.js +2 -1
  244. package/src/materialization/rect.js +25 -12
  245. package/src/materialization/rectConfig.js +5 -1
  246. package/src/materialization/scales/map.js +5 -3
  247. package/src/materialization/scales/policies/binnedPosition.js +2 -2
  248. package/src/materialization/scales/policies/offset.js +50 -8
  249. package/src/materialization/scales/policies/series.js +4 -1
  250. package/src/materialization/scales/resolve.js +22 -6
  251. package/src/materialization/selection/items/arc.js +28 -4
  252. package/src/materialization/selection/items/bar.js +30 -3
  253. package/src/materialization/selection/items/common.js +19 -4
  254. package/src/materialization/selection/items/rect.js +28 -4
  255. package/src/materialization/selection/path.js +1 -1
  256. package/src/materialization/selection/point.js +3 -1
  257. package/src/materialization/text.js +265 -14
  258. package/src/renderers/canvas/circle.js +2 -3
  259. package/src/renderers/canvas/line.js +2 -3
  260. package/src/renderers/canvas/native.js +4 -4
  261. package/src/renderers/canvas/path.js +2 -3
  262. package/src/renderers/canvas/rect.js +2 -3
  263. package/src/renderers/canvas/stroke.js +11 -0
  264. package/src/renderers/svg.js +19 -0
  265. package/src/selectors/datasets.js +32 -1
  266. package/src/selectors/layers.js +4 -0
  267. package/src/theme/defaults.js +101 -0
  268. package/types/basic.d.ts +6 -0
  269. package/types/index.d.ts +85 -0
  270. package/types/program.d.ts +910 -117
@@ -9,7 +9,6 @@ import {
9
9
  resolveBarGrain
10
10
  } from "../../../grammar/bars/policy.js";
11
11
  import {
12
- countHistogramBins,
13
12
  findHistogramBinIndex,
14
13
  resolveHistogramBins
15
14
  } from "../../../grammar/histogram.js";
@@ -22,11 +21,12 @@ import {
22
21
  } from "../../../grammar/seriesLayout.js";
23
22
  import {
24
23
  readNominalField,
25
- readQuantitativeField,
26
24
  resolveOrdinalDomain
27
25
  } from "../../../grammar/scales/index.js";
28
26
  import { findDataset } from "../../../selectors/datasets.js";
29
27
  import { findScale } from "./common.js";
28
+ import { resolveHistogramInput } from
29
+ "../../../materialization/bars/histogram.js";
30
30
 
31
31
  export function resolveHistogramCountValues(program, consumer) {
32
32
  const xEncoding = consumer.layer.encoding?.x;
@@ -44,7 +44,8 @@ export function resolveHistogramCountValues(program, consumer) {
44
44
  `Histogram mark "${consumer.layer.id}" requires an existing dataset.`
45
45
  );
46
46
  }
47
- const xValues = readQuantitativeField(dataset.values, xEncoding.field);
47
+ const input = resolveHistogramInput(dataset, xEncoding);
48
+ const xValues = input.values;
48
49
  const bins = resolveHistogramBins({
49
50
  values: xValues,
50
51
  bin: xEncoding.bin,
@@ -52,7 +53,12 @@ export function resolveHistogramCountValues(program, consumer) {
52
53
  nice: xScale.nice ?? true,
53
54
  zero: xScale.zero ?? false
54
55
  });
55
- return countHistogramBins(xValues, bins.boundaries);
56
+ const cells = bins.boundaries.slice(0, -1).map(() => []);
57
+ xValues.forEach((value, index) => {
58
+ const bin = findHistogramBinIndex(value, bins.boundaries);
59
+ if (bin !== -1) cells[bin].push(input.entries[index]);
60
+ });
61
+ return cells.map(input.mass);
56
62
  }
57
63
 
58
64
  function resolveHistogramPartitions(program, consumer) {
@@ -60,7 +66,8 @@ function resolveHistogramPartitions(program, consumer) {
60
66
  const dataset = findDataset(program, layer.data);
61
67
  const xEncoding = layer.encoding.x;
62
68
  const xScale = findScale(program, xEncoding.scale);
63
- const xValues = readQuantitativeField(dataset.values, xEncoding.field);
69
+ const input = resolveHistogramInput(dataset, xEncoding);
70
+ const xValues = input.values;
64
71
  const bins = resolveHistogramBins({
65
72
  values: xValues,
66
73
  bin: xEncoding.bin,
@@ -70,19 +77,24 @@ function resolveHistogramPartitions(program, consumer) {
70
77
  });
71
78
  const colorEncoding = layer.encoding?.color;
72
79
  if (layer.encoding?.group !== undefined) {
73
- const identity = readSeriesIdentity(dataset.values, layer);
80
+ const identity = readSeriesIdentity(input.rows, layer);
74
81
  validateGeneratedItemLimit((bins.boundaries.length - 1) * identity.domain.length, "Histogram layout cell count");
75
- const counts = bins.boundaries.slice(0, -1).map(() => identity.domain.map(() => 0));
82
+ const cells = bins.boundaries.slice(0, -1).map(() => identity.domain.map(() => []));
76
83
  const groups = new Map(identity.domain.map((value, index) => [value, index]));
77
84
  xValues.forEach((value, index) => { const bin = findHistogramBinIndex(value, bins.boundaries);
78
- if (bin !== -1) counts[bin][groups.get(identity.values[index])] += 1; });
79
- return counts;
85
+ if (bin !== -1) cells[bin][groups.get(identity.values[index])].push(input.entries[index]); });
86
+ return cells.map(partition => partition.map(input.mass));
80
87
  }
81
88
  if (colorEncoding?.scale === undefined) {
82
- return countHistogramBins(xValues, bins.boundaries).map(value => [value]);
89
+ const cells = bins.boundaries.slice(0, -1).map(() => []);
90
+ xValues.forEach((value, index) => {
91
+ const bin = findHistogramBinIndex(value, bins.boundaries);
92
+ if (bin !== -1) cells[bin].push(input.entries[index]);
93
+ });
94
+ return cells.map(entries => [input.mass(entries)]);
83
95
  }
84
96
  const colorScale = findScale(program, colorEncoding.scale);
85
- const colorValues = readNominalField(dataset.values, colorEncoding.field);
97
+ const colorValues = readNominalField(input.rows, colorEncoding.field);
86
98
  const colorDomain = resolveOrdinalDomain(colorScale.domain, colorValues);
87
99
  validateGeneratedItemLimit(
88
100
  (bins.boundaries.length - 1) * colorDomain.length,
@@ -90,14 +102,16 @@ function resolveHistogramPartitions(program, consumer) {
90
102
  );
91
103
  const colorIndex = new Map(colorDomain.map((value, index) => [value, index]));
92
104
  const partitions = bins.boundaries.slice(0, -1).map(() =>
93
- colorDomain.map(() => 0)
105
+ colorDomain.map(() => [])
94
106
  );
95
107
  for (let index = 0; index < xValues.length; index += 1) {
96
108
  const bin = findHistogramBinIndex(xValues[index], bins.boundaries);
97
109
  const color = colorIndex.get(colorValues[index]);
98
- if (bin !== -1 && color !== undefined) partitions[bin][color] += 1;
110
+ if (bin !== -1 && color !== undefined) {
111
+ partitions[bin][color].push(input.entries[index]);
112
+ }
99
113
  }
100
- return partitions;
114
+ return partitions.map(partition => partition.map(input.mass));
101
115
  }
102
116
 
103
117
  function resolveAggregatePartitions(program, consumer) {
@@ -14,9 +14,11 @@ import {
14
14
  isColorScaleType,
15
15
  isContinuousColorScaleType,
16
16
  isDiscretizedColorScaleType,
17
+ normalizeSizeScaleDefinition,
17
18
  normalizeScaleDefinition
18
19
  } from "../../grammar/scales/index.js";
19
20
  import { findSemanticScale } from "../../selectors/scales.js";
21
+ import { findScaleConsumers } from "./consumers/index.js";
20
22
 
21
23
  const CREATE_SCALE_OPTIONS = Object.freeze([
22
24
  "id",
@@ -97,28 +99,58 @@ export const createScale = action(
97
99
  const requestedRange = args.palette === undefined
98
100
  ? args.range
99
101
  : { palette: args.palette };
100
- const definition = normalizeScaleDefinition({
101
- type,
102
- patch: {
103
- ...args,
104
- ...(requestedRange === undefined ? {} : { range: requestedRange })
105
- },
106
- validateDomain: (scaleType, value) =>
107
- isDiscretizedColorScaleType(scaleType)
108
- ? validateDiscretizedColorDomain(scaleType, value)
109
- : hasOrdinalDomain(scaleType)
110
- ? validateOrdinalDomain(value)
111
- : validateScaleDomain(value),
112
- validateRange: (scaleType, value) =>
113
- isDiscretizedColorScaleType(scaleType)
114
- ? validateDiscretizedColorRange(value)
115
- : isContinuousColorScaleType(scaleType)
116
- ? validateSequentialColorRange(value)
117
- : scaleType === "ordinal"
118
- ? validateOrdinalRange(value)
119
- : validateScaleRange(value)
120
- });
121
- if (Object.hasOwn(args, "unknown")) definition.unknown = args.unknown;
102
+ const consumers = findScaleConsumers(this, id);
103
+ const offsetScale = consumers.length > 0 && consumers.every(
104
+ consumer => ["xOffset", "yOffset"].includes(consumer.channel)
105
+ );
106
+ const sizeScale = consumers.length > 0 && consumers.every(
107
+ consumer => consumer.channel === "size"
108
+ );
109
+ if (offsetScale && type !== "ordinal") {
110
+ throw new Error(`Scale type "${type}" is not valid for offset positions.`);
111
+ }
112
+ if (
113
+ offsetScale &&
114
+ Object.hasOwn(args, "range") &&
115
+ args.range !== "auto"
116
+ ) {
117
+ throw new Error(
118
+ "Offset scale range is derived from its parent categorical slot."
119
+ );
120
+ }
121
+ const definition = sizeScale
122
+ ? normalizeSizeScaleDefinition({
123
+ patch: {
124
+ ...args,
125
+ type,
126
+ ...(requestedRange === undefined ? {} : { range: requestedRange })
127
+ }
128
+ })
129
+ : normalizeScaleDefinition({
130
+ type,
131
+ patch: {
132
+ ...args,
133
+ ...(requestedRange === undefined ? {} : { range: requestedRange })
134
+ },
135
+ allowOrdinalBandParameters: offsetScale,
136
+ validateDomain: (scaleType, value) =>
137
+ isDiscretizedColorScaleType(scaleType)
138
+ ? validateDiscretizedColorDomain(scaleType, value)
139
+ : hasOrdinalDomain(scaleType)
140
+ ? validateOrdinalDomain(value)
141
+ : validateScaleDomain(value),
142
+ validateRange: (scaleType, value) =>
143
+ isDiscretizedColorScaleType(scaleType)
144
+ ? validateDiscretizedColorRange(value)
145
+ : isContinuousColorScaleType(scaleType)
146
+ ? validateSequentialColorRange(value)
147
+ : scaleType === "ordinal"
148
+ ? validateOrdinalRange(value)
149
+ : validateScaleRange(value)
150
+ });
151
+ if (Object.hasOwn(args, "unknown") && !sizeScale) {
152
+ definition.unknown = args.unknown;
153
+ }
122
154
  const existing = findSemanticScale(this, id);
123
155
 
124
156
  if (existing !== undefined) {
@@ -13,7 +13,6 @@ import {
13
13
  validateScaleDomain,
14
14
  validateScaleRange,
15
15
  validateShapeRange,
16
- validateSizeRange,
17
16
  validateStrokeDashRange,
18
17
  validateStrokeWidthRange,
19
18
  validateSequentialColorRange,
@@ -25,6 +24,7 @@ import {
25
24
  validateScalePropertyForType,
26
25
  validateScaleTypeForRole,
27
26
  isDiscretePositionScaleType,
27
+ normalizeSizeScaleDefinition,
28
28
  withScaleUnknown
29
29
  } from "../../grammar/scales/index.js";
30
30
  import {
@@ -32,6 +32,10 @@ import {
32
32
  validateThetaRange
33
33
  } from "../../grammar/polar.js";
34
34
  import { findSemanticScale } from "../../selectors/scales.js";
35
+ import { normalizeOffsetScalePolicy } from "../../grammar/bars/geometry.js";
36
+ import { resolveRequestedOffsetPolicy } from
37
+ "../../materialization/scales/policies/offset.js";
38
+ import { findScaleConsumers } from "./consumers/index.js";
35
39
 
36
40
  const BASE_OPTIONS = ["id", "type", "domain", "range"];
37
41
  const UNKNOWN_OPTIONS = [...BASE_OPTIONS, "unknown"];
@@ -58,6 +62,9 @@ const OPACITY_OPTIONS = [...BASE_OPTIONS, ...BOOLEAN_OPTIONS, "unknown"];
58
62
  const STROKE_WIDTH_OPTIONS = [
59
63
  ...BASE_OPTIONS, ...BOOLEAN_OPTIONS, ...TRANSFORM_OPTIONS
60
64
  ];
65
+ const SIZE_OPTIONS = [
66
+ ...UNKNOWN_OPTIONS, "clamp", "reverse", "base", "exponent"
67
+ ];
61
68
 
62
69
  function optionsObject(options) {
63
70
  if (!isPlainObject(options)) {
@@ -151,21 +158,22 @@ export function resolvePositionScaleDefinition(
151
158
  return withScaleUnknown(scale, { ...existing, ...options }, channel);
152
159
  }
153
160
 
154
- export function resolveColorScaleDefinition(program, options) {
161
+ export function resolveColorScaleDefinition(program, options, channel = "color") {
155
162
  optionsObject(options);
156
163
  validateKeys(options, COLOR_OPTIONS, "scale");
157
164
  validatePaletteRange(options);
158
- const id = validateUserId(options.id ?? "color", "Scale id");
165
+ const id = validateUserId(options.id ?? channel, "Scale id");
159
166
  const existing = findSemanticScale(program, id);
167
+ const previous = existing?.type === "ordinal" ? existing : undefined;
160
168
  const range = options.palette === undefined
161
169
  ? options.range
162
170
  : { palette: options.palette };
163
171
  return withScaleUnknown({
164
172
  id,
165
- type: validateOrdinalScaleType(options.type ?? existing?.type ?? "ordinal"),
166
- domain: validateOrdinalDomain(options.domain ?? existing?.domain ?? "auto"),
167
- range: validateColorRange(range ?? existing?.range ?? "auto")
168
- }, { ...existing, ...options }, "color");
173
+ type: validateOrdinalScaleType(options.type ?? previous?.type ?? "ordinal"),
174
+ domain: validateOrdinalDomain(options.domain ?? previous?.domain ?? "auto"),
175
+ range: validateColorRange(range ?? previous?.range ?? "auto")
176
+ }, { ...previous, ...options }, channel);
169
177
  }
170
178
 
171
179
  function continuousDomain(value, fieldType) {
@@ -186,14 +194,16 @@ function continuousDomain(value, fieldType) {
186
194
  export function resolveSequentialColorScaleDefinition(
187
195
  program,
188
196
  fieldType,
189
- options
197
+ options,
198
+ channel = "color"
190
199
  ) {
191
200
  optionsObject(options);
192
201
  validateKeys(options, SEQUENTIAL_COLOR_OPTIONS, "scale");
193
202
  validatePaletteRange(options);
194
- const id = validateUserId(options.id ?? "color", "Scale id");
203
+ const id = validateUserId(options.id ?? channel, "Scale id");
195
204
  const existing = findSemanticScale(program, id);
196
- const type = options.type ?? existing?.type ?? "sequential";
205
+ const previous = existing?.type === "sequential" ? existing : undefined;
206
+ const type = options.type ?? previous?.type ?? "sequential";
197
207
  if (type !== "sequential") {
198
208
  throw new Error(`Unsupported continuous color scale type "${type}".`);
199
209
  }
@@ -204,16 +214,16 @@ export function resolveSequentialColorScaleDefinition(
204
214
  const scale = {
205
215
  id,
206
216
  type,
207
- domain: continuousDomain(options.domain ?? existing?.domain ?? "auto", fieldType),
217
+ domain: continuousDomain(options.domain ?? previous?.domain ?? "auto", fieldType),
208
218
  range: validateSequentialColorRange(
209
- requestedRange ?? existing?.range ?? { palette: "viridis" }
219
+ requestedRange ?? previous?.range ?? { palette: "viridis" }
210
220
  ),
211
221
  interpolate: validateContinuousColorInterpolation(
212
- options.interpolate ?? existing?.interpolate ?? "rgb"
222
+ options.interpolate ?? previous?.interpolate ?? "rgb"
213
223
  )
214
224
  };
215
225
  const midpoint = validateSequentialMidpoint(
216
- Object.hasOwn(options, "midpoint") ? options.midpoint : existing?.midpoint, type, scale.domain
226
+ Object.hasOwn(options, "midpoint") ? options.midpoint : previous?.midpoint, type, scale.domain
217
227
  );
218
228
  if (midpoint !== undefined) {
219
229
  if (fieldType !== "quantitative") {
@@ -222,30 +232,34 @@ export function resolveSequentialColorScaleDefinition(
222
232
  scale.midpoint = midpoint;
223
233
  }
224
234
  return withScaleUnknown(
225
- assignOptions(scale, options, existing, CLAMP_REVERSE),
226
- { ...existing, ...options },
227
- "color"
235
+ assignOptions(scale, options, previous, CLAMP_REVERSE),
236
+ { ...previous, ...options },
237
+ channel
228
238
  );
229
239
  }
230
240
 
231
241
  export function resolveQuantitativeColorScaleDefinition(
232
242
  program,
233
243
  fieldType,
234
- options
244
+ options,
245
+ channel = "color"
235
246
  ) {
236
247
  optionsObject(options);
237
- const type = options.type ?? findSemanticScale(
238
- program,
239
- options.id ?? "color"
240
- )?.type ?? "sequential";
241
- const existing = findSemanticScale(program, options.id ?? "color");
242
- if (existing !== undefined && existing.type !== type) {
248
+ const existing = findSemanticScale(program, options.id ?? channel);
249
+ const existingContinuous = existing !== undefined && (
250
+ existing.type === "sequential" ||
251
+ ["quantize", "quantile", "threshold"].includes(existing.type)
252
+ );
253
+ const type = options.type ?? (
254
+ existingContinuous ? existing.type : "sequential"
255
+ );
256
+ if (existingContinuous && existing.type !== type) {
243
257
  validateKeys(options, type === "sequential" ? SEQUENTIAL_COLOR_OPTIONS : [...COLOR_OPTIONS, ...CLAMP_REVERSE], "scale");
244
258
  if (fieldType !== "quantitative") throw new Error("Color scale type transition requires quantitative color.");
245
- return { id: existing.id, ...prepareScaleEdit(program, existing, "color", [], options) };
259
+ return { id: existing.id, ...prepareScaleEdit(program, existing, channel, [], options) };
246
260
  }
247
261
  if (type === "sequential") {
248
- return resolveSequentialColorScaleDefinition(program, fieldType, options);
262
+ return resolveSequentialColorScaleDefinition(program, fieldType, options, channel);
249
263
  }
250
264
  validateKeys(options, [...COLOR_OPTIONS, ...CLAMP_REVERSE], "scale");
251
265
  if (fieldType !== "quantitative") {
@@ -254,7 +268,7 @@ export function resolveQuantitativeColorScaleDefinition(
254
268
  validateScaleTypeForRole(type, SCALE_ROLES.discretizedColor);
255
269
  validatePaletteRange(options);
256
270
  validateBooleanOptions(options, CLAMP_REVERSE, type);
257
- const id = validateUserId(options.id ?? "color", "Scale id");
271
+ const id = validateUserId(options.id ?? channel, "Scale id");
258
272
  const previous = existing?.type === type ? existing : undefined;
259
273
  const requestedRange = options.palette === undefined
260
274
  ? options.range
@@ -273,7 +287,7 @@ export function resolveQuantitativeColorScaleDefinition(
273
287
  return withScaleUnknown(
274
288
  assignOptions(scale, options, previous, CLAMP_REVERSE),
275
289
  { ...existing, ...options },
276
- "color"
290
+ channel
277
291
  );
278
292
  }
279
293
 
@@ -298,21 +312,23 @@ export function resolveStrokeDashScaleDefinition(program, options) {
298
312
 
299
313
  export function resolveAppearanceScaleDefinition(program, channel, options) {
300
314
  optionsObject(options);
315
+ if (channel === "size") {
316
+ validateKeys(options, SIZE_OPTIONS, "scale");
317
+ const id = validateUserId(options.id ?? channel, "Scale id");
318
+ const existing = findSemanticScale(program, id);
319
+ return {
320
+ id,
321
+ ...normalizeSizeScaleDefinition({ previous: existing, patch: options })
322
+ };
323
+ }
301
324
  validateKeys(options, UNKNOWN_OPTIONS, "scale");
302
325
  const id = validateUserId(options.id ?? channel, "Scale id");
303
326
  const existing = findSemanticScale(program, id);
304
- const shape = channel === "shape";
305
327
  return withScaleUnknown({
306
328
  id,
307
- type: shape
308
- ? validateOrdinalScaleType(options.type ?? existing?.type ?? "ordinal")
309
- : validateLinearScaleType(options.type ?? existing?.type ?? "linear"),
310
- domain: shape
311
- ? validateOrdinalDomain(options.domain ?? existing?.domain ?? "auto")
312
- : validateScaleDomain(options.domain ?? existing?.domain ?? "auto"),
313
- range: shape
314
- ? validateShapeRange(options.range ?? existing?.range ?? "auto")
315
- : validateSizeRange(options.range ?? existing?.range ?? "auto")
329
+ type: validateOrdinalScaleType(options.type ?? existing?.type ?? "ordinal"),
330
+ domain: validateOrdinalDomain(options.domain ?? existing?.domain ?? "auto"),
331
+ range: validateShapeRange(options.range ?? existing?.range ?? "auto")
316
332
  }, { ...existing, ...options }, channel);
317
333
  }
318
334
 
@@ -365,7 +381,45 @@ export function resolveStrokeWidthScaleDefinition(program, options) {
365
381
  }
366
382
 
367
383
  export function resolveOffsetScaleDefinition(program, options, channel = "xOffset") {
368
- return resolveOrdinalScaleDefinition(
369
- program, options, channel, validateScaleRange
384
+ optionsObject(options);
385
+ validateKeys(options, [
386
+ "id", "type", "domain", "range", "reverse",
387
+ "padding", "paddingInner", "paddingOuter", "align"
388
+ ], "scale");
389
+ const id = validateUserId(options.id ?? channel, "Scale id");
390
+ const existing = findSemanticScale(program, id);
391
+ const type = options.type ?? existing?.type ?? "ordinal";
392
+ if (type !== "ordinal") {
393
+ throw new Error(`Scale type "${type}" is not valid for ${channel}.`);
394
+ }
395
+ if (Object.hasOwn(options, "range") && options.range !== "auto") {
396
+ throw new Error(
397
+ `${channel} scale range is derived from its parent categorical slot.`
398
+ );
399
+ }
400
+ const consumers = findScaleConsumers(program, id).filter(
401
+ consumer => consumer.channel === channel
370
402
  );
403
+ const currentPolicy = resolveRequestedOffsetPolicy({
404
+ scale: existing,
405
+ consumers,
406
+ markConfigs: program.markConfigs,
407
+ id,
408
+ channel
409
+ });
410
+ const policy = normalizeOffsetScalePolicy(options, currentPolicy, channel);
411
+ const patch = Object.fromEntries(Object.entries(options).filter(
412
+ ([property]) => !["id", "padding"].includes(property)
413
+ ));
414
+ return {
415
+ id,
416
+ ...normalizeScaleDefinition({
417
+ type,
418
+ previous: existing,
419
+ patch: { ...patch, ...policy },
420
+ allowOrdinalBandParameters: true,
421
+ validateDomain: (_scaleType, value) => validateOrdinalDomain(value),
422
+ validateRange: (_scaleType, value) => validateScaleRange(value)
423
+ })
424
+ };
371
425
  }
@@ -1,12 +1,18 @@
1
1
  import { withGuideLayoutValidation } from "../../materialization/guides/layout.js";
2
- import { planColorLegendTransition, applyColorLegendTransition } from "../guides/legends/transition.js";
2
+ import {
3
+ applyColorLegendTransition,
4
+ planColorLegendTransitions
5
+ } from "../guides/legends/transition.js";
3
6
  import { action } from "../../core/action.js";
4
7
  import { validateUserId } from "../../core/identifiers.js";
5
8
  import { validateOptionObject } from "../../core/validation.js";
6
9
  import { getMarkMaterializationStep, getSourceDependentMarkSteps } from "../../materialization/marks/index.js";
7
10
  import {
8
- applyMaterializationPlan
11
+ applyMaterializationPlan,
12
+ planCoordinateRematerialization
9
13
  } from "../../materialization/dependencies.js";
14
+ import { findDataAspectCoordinatesForScale } from
15
+ "../../materialization/coordinateBounds.js";
10
16
  import {
11
17
  findSemanticScale,
12
18
  requireSemanticScale
@@ -16,6 +22,8 @@ import {
16
22
  prepareScaleEdit,
17
23
  resolveScaleConsumerChannel
18
24
  } from "./editPolicy.js";
25
+ import { hasExactLegendSamplingForScale } from
26
+ "../guides/legends/sampling.js";
19
27
 
20
28
  const OPTIONS = Object.freeze([
21
29
  "id", "type", "domain", "range", "nice", "zero", "clamp", "reverse",
@@ -71,9 +79,21 @@ function planMarkRematerialization(program, consumers) {
71
79
  ];
72
80
  }
73
81
 
74
- function applyScaleEdit(program, { id, scale, consumers, definition, legendTransition }) {
75
- let next = legendTransition === undefined ? program
76
- : program.removeLegend({ target: legendTransition.args.target, channels: ["color"] });
82
+ function applyScaleEdit(program, {
83
+ id,
84
+ scale,
85
+ channel,
86
+ consumers,
87
+ definition,
88
+ legendTransitions
89
+ }) {
90
+ let next = program;
91
+ for (const transition of legendTransitions) {
92
+ next = next.removeLegend({
93
+ target: transition.args.target,
94
+ channels: [transition.channel]
95
+ });
96
+ }
77
97
  for (const property of EDITABLE) {
78
98
  if (
79
99
  Object.hasOwn(scale, property) &&
@@ -97,9 +117,32 @@ function applyScaleEdit(program, { id, scale, consumers, definition, legendTrans
97
117
  }
98
118
  if (consumers.length === 0) return next;
99
119
 
100
- next = next.rematerializeScale({ id });
101
- next = applyMaterializationPlan(next, planMarkRematerialization(next, consumers));
102
- return legendTransition === undefined ? next : applyColorLegendTransition(next, legendTransition);
120
+ const aspectCoordinates = findDataAspectCoordinatesForScale(next, id);
121
+ if (aspectCoordinates.length > 0) {
122
+ for (const coordinate of aspectCoordinates) {
123
+ next = applyMaterializationPlan(
124
+ next,
125
+ planCoordinateRematerialization(next, coordinate.id)
126
+ );
127
+ }
128
+ } else {
129
+ next = next.rematerializeScale({ id });
130
+ next = applyMaterializationPlan(
131
+ next,
132
+ planMarkRematerialization(next, consumers)
133
+ );
134
+ }
135
+ if (["xOffset", "yOffset"].includes(channel)) {
136
+ for (const consumer of consumers) {
137
+ next = next._withoutMaterializationConfig([
138
+ "marks", consumer.layer.id, channel
139
+ ]);
140
+ }
141
+ }
142
+ for (const transition of legendTransitions) {
143
+ next = applyColorLegendTransition(next, transition);
144
+ }
145
+ return next;
103
146
  }
104
147
 
105
148
  export const editScale = action(
@@ -118,10 +161,22 @@ export const editScale = action(
118
161
  const channel = resolveScaleConsumerChannel(consumers, id);
119
162
  const definition = prepareScaleEdit(this, scale, channel, consumers, args);
120
163
 
121
- const legendTransition = planColorLegendTransition(this, scale, definition.type);
122
- const proposal = { id, scale, consumers, definition, legendTransition };
164
+ const legendTransitions = planColorLegendTransitions(this, scale, definition.type);
165
+ const proposal = {
166
+ id,
167
+ scale,
168
+ channel,
169
+ consumers,
170
+ definition,
171
+ legendTransitions
172
+ };
123
173
  // Preflight every dependent mark and guide on a discarded immutable branch.
124
- if (scale.type !== definition.type) applyScaleEdit(this, proposal);
174
+ if (
175
+ scale.type !== definition.type ||
176
+ hasExactLegendSamplingForScale(this, id)
177
+ ) {
178
+ applyScaleEdit(this, proposal);
179
+ }
125
180
  return applyScaleEdit(this, proposal);
126
181
  })
127
182
  );