ggaction 0.0.12 → 0.0.13

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 (281) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +2 -2
  3. package/knowledge/action-card.schema.json +1 -1
  4. package/knowledge/action-cards.json +1 -1
  5. package/knowledge/action-cards.schema.json +1 -1
  6. package/knowledge/extension-authoring.md +2 -1
  7. package/knowledge/intent-taxonomy.json +1 -1
  8. package/knowledge/mcp-resources.json +1 -1
  9. package/knowledge/task-resolver.js +1 -1
  10. package/package.json +4 -2
  11. package/src/actions/basic.js +2 -0
  12. package/src/actions/boxPlots/materialize.js +2 -1
  13. package/src/actions/canvas/actions.js +3 -1
  14. package/src/actions/canvas/fitting.js +211 -0
  15. package/src/actions/canvas/index.js +2 -0
  16. package/src/actions/categoryOrder/index.js +3 -2
  17. package/src/actions/charts/area.js +47 -0
  18. package/src/actions/charts/bar.js +11 -5
  19. package/src/actions/charts/beeswarm.js +70 -0
  20. package/src/actions/charts/density.js +44 -0
  21. package/src/actions/charts/ecdf.js +183 -0
  22. package/src/actions/charts/endpoints.js +451 -0
  23. package/src/actions/charts/heatmap.js +2 -2
  24. package/src/actions/charts/histogram.js +1 -1
  25. package/src/actions/charts/horizon.js +71 -0
  26. package/src/actions/charts/index.js +37 -0
  27. package/src/actions/charts/interval-regression.js +169 -0
  28. package/src/actions/charts/line.js +5 -5
  29. package/src/actions/charts/parallel.js +1 -1
  30. package/src/actions/charts/pie.js +44 -0
  31. package/src/actions/charts/polar.js +123 -0
  32. package/src/actions/charts/radar.js +299 -0
  33. package/src/actions/charts/radial.js +42 -0
  34. package/src/actions/charts/raincloud.js +422 -0
  35. package/src/actions/charts/rug-strip.js +279 -0
  36. package/src/actions/charts/scatter.js +9 -3
  37. package/src/actions/charts/shared.js +69 -10
  38. package/src/actions/composition/actions.js +118 -1
  39. package/src/actions/data/bin.js +29 -0
  40. package/src/actions/data/computed.js +24 -0
  41. package/src/actions/data/derived.js +66 -0
  42. package/src/actions/data/ecdf.js +38 -0
  43. package/src/actions/data/filter.js +212 -28
  44. package/src/actions/data/fold.js +24 -0
  45. package/src/actions/data/index.js +24 -0
  46. package/src/actions/data/interval.js +2 -0
  47. package/src/actions/data/intervalEdit.js +86 -4
  48. package/src/actions/data/regression.js +5 -2
  49. package/src/actions/data/stack.js +26 -0
  50. package/src/actions/data/summary.js +26 -0
  51. package/src/actions/data/timeUnit.js +1 -1
  52. package/src/actions/encodings/appearance.js +32 -13
  53. package/src/actions/encodings/barWidth.js +8 -0
  54. package/src/actions/encodings/basic.js +2 -0
  55. package/src/actions/encodings/color/continuous.js +19 -59
  56. package/src/actions/encodings/color/index.js +28 -22
  57. package/src/actions/encodings/color/layout.js +3 -97
  58. package/src/actions/encodings/color/policy.js +5 -8
  59. package/src/actions/encodings/density/resolve.js +37 -5
  60. package/src/actions/encodings/density.js +120 -33
  61. package/src/actions/encodings/horizon/resolve.js +8 -4
  62. package/src/actions/encodings/horizon.js +18 -8
  63. package/src/actions/encodings/index.js +2 -0
  64. package/src/actions/encodings/offset.js +10 -16
  65. package/src/actions/encodings/parallel.js +2 -8
  66. package/src/actions/encodings/position/apply.js +10 -4
  67. package/src/actions/encodings/position/index.js +43 -5
  68. package/src/actions/encodings/position/policies/arc.js +14 -0
  69. package/src/actions/encodings/position/policies/area.js +4 -42
  70. package/src/actions/encodings/position/policies/bar.js +48 -13
  71. package/src/actions/encodings/position/resolve.js +67 -22
  72. package/src/actions/encodings/ranged.js +123 -97
  73. package/src/actions/encodings/remove.js +37 -32
  74. package/src/actions/encodings/ruleAppearance.js +23 -9
  75. package/src/actions/encodings/seriesLayout.js +104 -0
  76. package/src/actions/encodings/shared.js +25 -23
  77. package/src/actions/encodings/strokeDash.js +9 -6
  78. package/src/actions/encodings/temporal.js +7 -0
  79. package/src/actions/encodings/text.js +30 -15
  80. package/src/actions/errorBands/create.js +18 -5
  81. package/src/actions/errorBands/edit.js +341 -21
  82. package/src/actions/errorBars/create.js +21 -10
  83. package/src/actions/errorBars/edit.js +355 -19
  84. package/src/actions/facets/actions.js +410 -12
  85. package/src/actions/facets/derive.js +57 -8
  86. package/src/actions/gradientPlots/components.js +33 -11
  87. package/src/actions/gradientPlots/materialize.js +5 -7
  88. package/src/actions/guides/applicability.js +20 -17
  89. package/src/actions/guides/axes/axes.js +4 -3
  90. package/src/actions/guides/axes/axis.js +18 -18
  91. package/src/actions/guides/axes/components.js +43 -0
  92. package/src/actions/guides/axes/edit.js +16 -51
  93. package/src/actions/guides/axes/labels.js +129 -19
  94. package/src/actions/guides/axes/lines.js +7 -5
  95. package/src/actions/guides/axes/parallel/lifecycle.js +117 -0
  96. package/src/actions/guides/axes/parallel/policy.js +137 -0
  97. package/src/actions/guides/axes/parallel/resolve.js +44 -23
  98. package/src/actions/guides/axes/parallel.js +93 -217
  99. package/src/actions/guides/axes/policy.js +21 -67
  100. package/src/actions/guides/axes/tickGroups.js +14 -8
  101. package/src/actions/guides/axes/ticks.js +5 -4
  102. package/src/actions/guides/axes/titles.js +21 -8
  103. package/src/actions/guides/basic.js +2 -0
  104. package/src/actions/guides/facade.js +106 -0
  105. package/src/actions/guides/facadeAxes.js +200 -0
  106. package/src/actions/guides/facadeLegend.js +63 -0
  107. package/src/actions/guides/grids/grid.js +37 -33
  108. package/src/actions/guides/grids/resolve.js +10 -2
  109. package/src/actions/guides/guides.js +35 -28
  110. package/src/actions/guides/legends/categorical/actions.js +193 -249
  111. package/src/actions/guides/legends/categorical/components.js +4 -5
  112. package/src/actions/guides/legends/categorical/layout.js +36 -349
  113. package/src/actions/guides/legends/categorical/options.js +30 -14
  114. package/src/actions/guides/legends/categorical/recipes.js +14 -0
  115. package/src/actions/guides/legends/categorical/resolve.js +21 -10
  116. package/src/actions/guides/legends/categorical/symbols.js +2 -3
  117. package/src/actions/guides/legends/continuous/common.js +69 -14
  118. package/src/actions/guides/legends/continuous/gradient.js +111 -83
  119. package/src/actions/guides/legends/continuous/index.js +6 -2
  120. package/src/actions/guides/legends/continuous/interval.js +71 -92
  121. package/src/actions/guides/legends/continuous/opacity.js +105 -84
  122. package/src/actions/guides/legends/creation.js +27 -0
  123. package/src/actions/guides/legends/edit.js +198 -66
  124. package/src/actions/guides/legends/focused.js +2 -2
  125. package/src/actions/guides/legends/lane.js +98 -86
  126. package/src/actions/guides/legends/lifecycle.js +132 -0
  127. package/src/actions/guides/legends/remove.js +31 -76
  128. package/src/actions/guides/legends/size.js +149 -105
  129. package/src/actions/guides/legends/strokeWidth.js +106 -64
  130. package/src/actions/guides/legends/target.js +12 -0
  131. package/src/actions/guides/legends/transition.js +43 -0
  132. package/src/actions/guides/polar/axes/facade.js +77 -62
  133. package/src/actions/guides/polar/axes/labels.js +5 -4
  134. package/src/actions/guides/polar/axes/lines.js +2 -1
  135. package/src/actions/guides/polar/axes/shared.js +6 -0
  136. package/src/actions/guides/polar/axes/ticks.js +5 -4
  137. package/src/actions/guides/polar/axes/titles.js +5 -4
  138. package/src/actions/guides/polar/grids.js +7 -7
  139. package/src/actions/guides/reuse.js +44 -0
  140. package/src/actions/guides/tickValues.js +19 -21
  141. package/src/actions/index.js +2 -0
  142. package/src/actions/intervals/resolve.js +35 -15
  143. package/src/actions/marks/arc/actions.js +1 -1
  144. package/src/actions/marks/area/actions.js +18 -4
  145. package/src/actions/marks/area/materialize.js +5 -2
  146. package/src/actions/marks/bar/materialize.js +11 -7
  147. package/src/actions/marks/index.js +5 -0
  148. package/src/actions/marks/line/actions.js +43 -108
  149. package/src/actions/marks/line/materialize.js +34 -52
  150. package/src/actions/marks/point/edit.js +11 -15
  151. package/src/actions/marks/point/index.js +1 -0
  152. package/src/actions/marks/point/jitter.js +3 -0
  153. package/src/actions/marks/point/materialize.js +106 -2
  154. package/src/actions/marks/point/packing.js +68 -0
  155. package/src/actions/marks/references.js +87 -0
  156. package/src/actions/marks/remove.js +17 -4
  157. package/src/actions/marks/rule/actions.js +34 -3
  158. package/src/actions/marks/rule/appearance.js +26 -0
  159. package/src/actions/marks/shared.js +12 -12
  160. package/src/actions/marks/text/actions.js +224 -6
  161. package/src/actions/marks/text/layout.js +2 -2
  162. package/src/actions/primitives/createGraphics.js +3 -1
  163. package/src/actions/primitives/semantic.js +1 -1
  164. package/src/actions/primitives/semanticAction.js +39 -2
  165. package/src/actions/primitives/semanticValidation/guide.js +10 -0
  166. package/src/actions/primitives/semanticValidation/layer.js +18 -1
  167. package/src/actions/primitives/semanticValidation/scale.js +10 -1
  168. package/src/actions/regression/create.js +1 -1
  169. package/src/actions/regression/edit.js +19 -6
  170. package/src/actions/regression/resolve.js +1 -1
  171. package/src/actions/scales/consumers/common.js +20 -53
  172. package/src/actions/scales/consumers/families.js +27 -14
  173. package/src/actions/scales/consumers/index.js +23 -2
  174. package/src/actions/scales/consumers/seriesLayout.js +33 -2
  175. package/src/actions/scales/create.js +4 -4
  176. package/src/actions/scales/definitions.js +19 -3
  177. package/src/actions/scales/edit.js +46 -34
  178. package/src/actions/scales/editPolicy.js +21 -31
  179. package/src/actions/scales/materialize.js +10 -46
  180. package/src/actions/scales/patch.js +1 -1
  181. package/src/actions/scales/preview.js +39 -0
  182. package/src/actions/scales/quantitativeColor.js +9 -5
  183. package/src/actions/theme/actions.js +43 -0
  184. package/src/actions/theme/index.js +9 -0
  185. package/src/actions/theme/reconcile.js +882 -0
  186. package/src/actions/titles/actions.js +11 -10
  187. package/src/actions/titles/resolve.js +1 -40
  188. package/src/actions/violinPlots/create.js +86 -42
  189. package/src/actions/violinPlots/edit.js +191 -0
  190. package/src/actions/violinPlots/index.js +2 -0
  191. package/src/core/ChartProgram.js +7 -0
  192. package/src/core/action.js +22 -2
  193. package/src/core/compositionState.js +136 -8
  194. package/src/grammar/aggregate.js +66 -23
  195. package/src/grammar/arcs.js +58 -1
  196. package/src/grammar/areaEndpoints.js +46 -0
  197. package/src/grammar/areaSeries.js +87 -41
  198. package/src/grammar/bars/aggregate.js +33 -12
  199. package/src/grammar/bars/policy.js +9 -7
  200. package/src/grammar/bin.js +181 -0
  201. package/src/grammar/categoryOrder.js +34 -0
  202. package/src/grammar/computed.js +150 -0
  203. package/src/grammar/ecdf.js +198 -0
  204. package/src/grammar/facets/index.js +117 -6
  205. package/src/grammar/fold.js +123 -0
  206. package/src/grammar/horizon.js +6 -4
  207. package/src/grammar/interval.js +37 -20
  208. package/src/grammar/lineSeries.js +20 -63
  209. package/src/grammar/markFilter.js +22 -4
  210. package/src/grammar/markLabels.js +93 -0
  211. package/src/grammar/parallelAxisTitles.js +16 -0
  212. package/src/grammar/pathSeries.js +91 -0
  213. package/src/grammar/pointPacking.js +183 -0
  214. package/src/grammar/positionDatum.js +12 -0
  215. package/src/grammar/rects.js +9 -1
  216. package/src/grammar/regression/derive.js +4 -0
  217. package/src/grammar/regression/index.js +7 -2
  218. package/src/grammar/regression/models.js +22 -7
  219. package/src/grammar/regression/parameters.js +40 -13
  220. package/src/grammar/rotation.js +29 -0
  221. package/src/grammar/rules.js +7 -21
  222. package/src/grammar/scales/color.js +23 -1
  223. package/src/grammar/scales/colorConsumers.js +38 -0
  224. package/src/grammar/scales/definition.js +15 -1
  225. package/src/grammar/scales/discretized.js +11 -5
  226. package/src/grammar/scales/fields.js +38 -8
  227. package/src/grammar/scales/mapping.js +2 -0
  228. package/src/grammar/scales/radial.js +55 -0
  229. package/src/grammar/scales/types.js +1 -0
  230. package/src/grammar/schemas/semanticPath.js +17 -3
  231. package/src/grammar/stack.js +170 -0
  232. package/src/grammar/statistics/confidenceInterval.js +101 -0
  233. package/src/grammar/summary.js +183 -0
  234. package/src/grammar/text.js +22 -16
  235. package/src/grammar/timeUnit.js +21 -11
  236. package/src/grammar/transformTopology.js +6 -0
  237. package/src/grammar/transforms.js +36 -0
  238. package/src/grammar/valueFormat.js +112 -0
  239. package/src/grammar/window.js +13 -7
  240. package/src/layout/facets.js +41 -7
  241. package/src/layout/guideCollisions.js +12 -0
  242. package/src/layout/legend.js +2 -10
  243. package/src/layout/legendItems.js +102 -0
  244. package/src/layout/legendLane.js +100 -11
  245. package/src/materialization/bars/aggregate.js +38 -152
  246. package/src/materialization/bars/histogram.js +24 -0
  247. package/src/materialization/categorySlotOffset.js +40 -0
  248. package/src/materialization/compositionSnapshot.js +1 -1
  249. package/src/materialization/dependencies.js +57 -16
  250. package/src/materialization/encodings.js +1 -1
  251. package/src/materialization/facets.js +8 -1
  252. package/src/materialization/guides/layout.js +57 -0
  253. package/src/materialization/guides/resources.js +2 -2
  254. package/src/materialization/legends.js +10 -1
  255. package/src/materialization/marks/capabilities.js +20 -5
  256. package/src/materialization/marks/grouping.js +52 -0
  257. package/src/materialization/marks/index.js +58 -23
  258. package/src/materialization/marks/policies.js +2 -0
  259. package/src/materialization/rect.js +33 -33
  260. package/src/materialization/rowEncoding.js +13 -13
  261. package/src/materialization/scaleGuideDependencies.js +8 -5
  262. package/src/materialization/scales/map.js +1 -0
  263. package/src/materialization/scales/policies/arc.js +42 -0
  264. package/src/materialization/scales/policies/binnedPosition.js +7 -4
  265. package/src/materialization/scales/policies/series.js +2 -1
  266. package/src/materialization/scales/resolve.js +46 -14
  267. package/src/materialization/selection/compatibility.js +11 -0
  268. package/src/materialization/selection/items/bar.js +12 -68
  269. package/src/materialization/selection/items/common.js +8 -2
  270. package/src/materialization/selection/items/path.js +3 -13
  271. package/src/materialization/selection/items/rect.js +7 -4
  272. package/src/materialization/text.js +64 -5
  273. package/src/mcp/adapter.js +2 -2
  274. package/src/selectors/datasets.js +13 -0
  275. package/src/selectors/layers.js +3 -2
  276. package/src/selectors/scales.js +6 -0
  277. package/src/theme/defaults.js +39 -0
  278. package/types/basic.d.ts +11 -2
  279. package/types/index.d.ts +126 -0
  280. package/types/program.d.ts +1320 -135
  281. package/src/materialization/bars/grouped.js +0 -144
@@ -1,8 +1,11 @@
1
+ import { isSourceOwnedText } from "../../grammar/text.js";
2
+ import { findLayer } from "../../selectors/layers.js";
1
3
  import {
2
4
  POSITION_CHANNELS,
3
5
  POSITION_ENCODING_CHANNELS
4
6
  } from "../../core/vocabulary.js";
5
7
  import { getMarkMaterializationPolicy } from "./policies.js";
8
+ import { buildMaterializationPlan } from "../planner.js";
6
9
 
7
10
  export {
8
11
  canMaterializeArc,
@@ -13,10 +16,29 @@ export {
13
16
  canMaterializeRect,
14
17
  canMaterializeRule,
15
18
  canMaterializeText,
16
- canMaterializeTick
19
+ canMaterializeTick,
20
+ isTextSource
17
21
  } from "./capabilities.js";
18
22
 
19
- export function getMarkRematerializationStep(layer) {
23
+ export function getLayerScaleIds(layer) {
24
+ if (isSourceOwnedText(layer)) return [];
25
+ return [
26
+ ...Object.values(layer.encoding ?? {}).map(encoding => encoding?.scale),
27
+ ...(layer.encoding?.parallel?.dimensions ?? []).map(dimension => dimension.scale)
28
+ ].filter(scale => scale !== undefined);
29
+ }
30
+
31
+ export function getScaleConsumerMarkSteps(program, scaleIds) {
32
+ return program.semanticSpec.layers
33
+ .filter(layer => getLayerScaleIds(layer).some(id => scaleIds.includes(id)))
34
+ .map(layer => getMarkMaterializationStep(program, layer))
35
+ .filter(step => step !== undefined);
36
+ }
37
+
38
+ export function getMarkRematerializationStep(program, layer) {
39
+ if (program.markConfigs?.[layer.id]?.markFilter?.empty === true) {
40
+ return { op: "materializeEmptyMark", args: { id: layer.id } };
41
+ }
20
42
  const policy = getMarkMaterializationPolicy(layer);
21
43
  return policy === undefined
22
44
  ? undefined
@@ -28,14 +50,15 @@ export function getMarkMaterializationStep(program, layer) {
28
50
  if (policy === undefined || !policy.canMaterialize(program, layer)) {
29
51
  return undefined;
30
52
  }
31
- return getMarkRematerializationStep(layer);
53
+ return getMarkRematerializationStep(program, layer);
32
54
  }
33
55
 
34
56
  export function getSourceDependentMarkSteps(program, sourceId) {
35
- return program.semanticSpec.layers.flatMap(layer =>
57
+ return (program.semanticSpec.layers ?? []).flatMap(layer =>
36
58
  layer.source === sourceId &&
37
59
  getMarkMaterializationPolicy(layer)?.sourceDependent === true
38
- ? [getMarkMaterializationStep(program, layer)].filter(
60
+ ? [getMarkMaterializationStep(program, layer) ??
61
+ getExistingMarkRematerializationStep(program, layer)].filter(
39
62
  step => step !== undefined
40
63
  )
41
64
  : []
@@ -46,39 +69,39 @@ export function getPositionEncodingMaterializationSteps(program, layer, scaleId)
46
69
  const policy = getMarkMaterializationPolicy(layer);
47
70
  if (policy === undefined) return [];
48
71
  const complete = policy.canMaterialize(program, layer);
49
- const mark = getMarkRematerializationStep(layer);
72
+ const mark = getMarkRematerializationStep(program, layer);
50
73
  const sharedConsumerMarks = (program.semanticSpec.layers ?? [])
51
74
  .filter(candidate =>
52
75
  candidate.id !== layer.id &&
76
+ !isSourceOwnedText(candidate) &&
53
77
  POSITION_CHANNELS.some(channel =>
54
78
  candidate.encoding?.[channel]?.scale === scaleId
55
79
  )
56
80
  )
57
81
  .map(candidate => getMarkMaterializationStep(program, candidate))
58
82
  .filter(step => step !== undefined);
83
+ const dependentMarks = [mark, ...sharedConsumerMarks]
84
+ .flatMap(step => getSourceDependentMarkSteps(program, step.args.id));
59
85
  const scale = {
60
86
  op: "rematerializeScale",
61
87
  args: sharedConsumerMarks.length === 0
62
88
  ? { id: scaleId }
63
89
  : { id: scaleId, marks: false }
64
90
  };
65
- if (!complete) {
66
- if (policy.positionEncoding.incomplete === "scale") {
67
- return [scale, ...sharedConsumerMarks];
68
- }
69
- return policy.positionEncoding.scaleFirst
91
+ const direct = !complete && policy.positionEncoding.incomplete === "scale"
92
+ ? [scale, ...sharedConsumerMarks]
93
+ : policy.positionEncoding.scaleFirst
70
94
  ? [scale, mark, ...sharedConsumerMarks]
71
95
  : [mark, ...sharedConsumerMarks];
72
- }
73
- return policy.positionEncoding.scaleFirst
74
- ? [scale, mark, ...sharedConsumerMarks]
75
- : [mark, ...sharedConsumerMarks];
96
+ return buildMaterializationPlan({ marks: [...direct, ...dependentMarks] });
76
97
  }
77
98
 
78
99
  export function getScaleConsumerMaterializationMode(layer, channel) {
79
100
  const policy = getMarkMaterializationPolicy(layer);
80
101
  if (policy === undefined) return "direct";
81
102
  if (POSITION_ENCODING_CHANNELS.includes(channel)) {
103
+ // Attached labels require their source's completed geometry, after scales.
104
+ if (layer.source !== undefined && policy.sourceDependent === true) return "defer";
82
105
  return policy.scaleApplication.position ?? policy.scaleApplication.default;
83
106
  }
84
107
  if (policy.scaleApplication.deferredChannels?.includes(channel)) {
@@ -99,7 +122,7 @@ export function getExistingMarkRematerializationStep(program, layer) {
99
122
  ) {
100
123
  return undefined;
101
124
  }
102
- return getMarkRematerializationStep(layer);
125
+ return getMarkRematerializationStep(program, layer);
103
126
  }
104
127
 
105
128
  export function getEncodingMaterializationStages(program, layer, channel, scale) {
@@ -111,22 +134,34 @@ export function getEncodingMaterializationStages(program, layer, channel, scale)
111
134
  args: { id: scale, guides: false, marks: false }
112
135
  }]
113
136
  : [];
114
- const shared = policy.encoding?.sharedChannels?.includes(channel) === true &&
115
- scale !== undefined;
137
+ const shared = scale !== undefined && (
138
+ scales.length > 0 || policy.encoding?.sharedChannels?.includes(channel) === true
139
+ );
116
140
  const candidates = shared
117
141
  ? program.semanticSpec.layers.filter(candidate =>
118
- candidate.mark?.type === layer.mark?.type &&
119
- candidate.encoding?.[channel]?.scale === scale
142
+ candidate.id === layer.id || candidate.encoding?.[channel]?.scale === scale
120
143
  )
121
144
  : [layer];
122
145
  const marks = candidates.flatMap(candidate => {
123
- if (policy.encoding?.skipRematerialization?.(program, candidate) === true) {
146
+ const candidatePolicy = getMarkMaterializationPolicy(candidate);
147
+ if (candidatePolicy?.encoding?.skipRematerialization?.(program, candidate) === true) {
124
148
  return [];
125
149
  }
126
- const step = policy.encoding?.completeOnly === true
150
+ const step = candidate.id !== layer.id || candidatePolicy?.encoding?.completeOnly === true
127
151
  ? getMarkMaterializationStep(program, candidate)
128
- : getMarkRematerializationStep(candidate);
152
+ : getMarkRematerializationStep(program, candidate);
129
153
  return step === undefined ? [] : [step];
130
154
  });
131
155
  return { scales, marks };
132
156
  }
157
+
158
+ // Reuse a plan's scale stage only when it covers every scale consumed by the mark.
159
+ export function reusePlannedMarkScales(program, steps, scaleIds) {
160
+ const resolved = new Set(scaleIds);
161
+ return steps.map(step => {
162
+ const layer = findLayer(program, step.args?.id);
163
+ if (layer === undefined || getMarkMaterializationPolicy(layer)?.acceptsResolvedScales !== true ||
164
+ !getLayerScaleIds(layer).every(id => resolved.has(id))) return step;
165
+ return { ...step, args: { ...step.args, scales: false } };
166
+ });
167
+ }
@@ -40,7 +40,9 @@ const MARK_MATERIALIZATION_POLICIES = Object.freeze({
40
40
  line: Object.freeze({
41
41
  canMaterialize: canMaterializeLine,
42
42
  op: "rematerializeLineMark",
43
+ acceptsResolvedScales: true,
43
44
  positionEncoding: Object.freeze({ incomplete: "scale", scaleFirst: false }),
45
+ encoding: Object.freeze({ scaleFirst: true, completeOnly: true }),
44
46
  scaleApplication: Object.freeze({ default: "defer" })
45
47
  }),
46
48
  area: Object.freeze({
@@ -2,28 +2,36 @@ import {
2
2
  mapOrdinalPositionValues,
3
3
  readScaleField
4
4
  } from "../grammar/scales/index.js";
5
- import { RECT_MODES, resolveRectMode } from "../grammar/rects.js";
5
+ import { normalizePositionDatum } from "../grammar/positionDatum.js";
6
+ import { resolveGraphicBounds } from "../layout/canvas.js";
7
+ import { RECT_MODES, resolveRectMode, rectUsesFields } from "../grammar/rects.js";
6
8
  import { DEFAULT_RECT_MARK } from "./rectConfig.js";
7
9
  import { mapScaleConsumerValues } from "./scales/map.js";
8
10
 
9
- function optionalValues(rows, encoding) {
11
+ function optionalValues(rows, encoding, length, channel) {
12
+ if (Object.hasOwn(encoding, "datum")) {
13
+ const value = normalizePositionDatum(encoding.datum, encoding.fieldType, channel, encoding.temporalUnit, "Rect");
14
+ return Array.from({ length }, () => value);
15
+ }
10
16
  return readScaleField(rows, encoding.field, encoding.fieldType, {
11
- allowUnknown: true
17
+ allowUnknown: true, temporalUnit: encoding.temporalUnit
12
18
  });
13
19
  }
14
20
 
15
21
  function requiredChannels(layer, mode) {
16
- return mode === RECT_MODES.ranged
17
- ? ["x", "x2", "y", "y2", ...(layer.encoding?.color ? ["color"] : [])]
18
- : ["x", "y", ...(layer.encoding?.color ? ["color"] : [])];
22
+ const position = mode === RECT_MODES.xSpan ? ["x", "x2"]
23
+ : mode === RECT_MODES.ySpan ? ["y", "y2"]
24
+ : mode === RECT_MODES.ranged ? ["x", "x2", "y", "y2"] : ["x", "y"];
25
+ return [...position, ...(layer.encoding?.color ? ["color"] : [])];
19
26
  }
20
27
 
21
28
  export function resolveRectConsumerValues(layer, dataset, channel) {
22
- const requested = optionalValues(dataset.values, layer.encoding[channel]);
29
+ const length = rectUsesFields(layer) ? dataset.values.length : 1;
30
+ const requested = optionalValues(dataset.values, layer.encoding[channel], length, channel);
23
31
  const mode = resolveRectMode(layer);
24
32
  if (mode === undefined) return requested;
25
33
  const complete = requiredChannels(layer, mode).map(candidate =>
26
- optionalValues(dataset.values, layer.encoding[candidate])
34
+ optionalValues(dataset.values, layer.encoding[candidate], length, candidate)
27
35
  );
28
36
  return requested.map((value, index) =>
29
37
  complete.every(values => values[index] !== undefined) ? value : undefined
@@ -68,24 +76,22 @@ function appearance(config, fill) {
68
76
  export function resolveRectRows(program, layer, dataset) {
69
77
  const mode = resolveRectMode(layer);
70
78
  if (mode === undefined) return [];
71
- const x = mappedEncoding(program, layer, dataset, "x");
72
- const y = mappedEncoding(program, layer, dataset, "y");
73
- const x2 = mode === RECT_MODES.ranged
74
- ? mappedEncoding(program, layer, dataset, "x2")
75
- : undefined;
76
- const y2 = mode === RECT_MODES.ranged
77
- ? mappedEncoding(program, layer, dataset, "y2")
78
- : undefined;
79
+ const bounds = resolveGraphicBounds(program);
80
+ const x = layer.encoding.x === undefined ? undefined : mappedEncoding(program, layer, dataset, "x");
81
+ const y = layer.encoding.y === undefined ? undefined : mappedEncoding(program, layer, dataset, "y");
82
+ const x2 = layer.encoding.x2 === undefined ? undefined : mappedEncoding(program, layer, dataset, "x2");
83
+ const y2 = layer.encoding.y2 === undefined ? undefined : mappedEncoding(program, layer, dataset, "y2");
79
84
  const color = layer.encoding?.color === undefined
80
85
  ? undefined
81
86
  : mappedEncoding(program, layer, dataset, "color").values;
82
87
  const config = program.markConfigs[layer.id] ?? DEFAULT_RECT_MARK;
83
88
 
84
- return dataset.values.flatMap((row, index) => {
89
+ const rows = rectUsesFields(layer) ? dataset.values : [{}];
90
+ return rows.flatMap((row, index) => {
85
91
  const fill = color === undefined ? config.fill : color[index];
86
92
  if (
87
- !Number.isFinite(x.values[index]) ||
88
- !Number.isFinite(y.values[index]) ||
93
+ (x !== undefined && !Number.isFinite(x.values[index])) ||
94
+ (y !== undefined && !Number.isFinite(y.values[index])) ||
89
95
  typeof fill !== "string"
90
96
  ) return [];
91
97
 
@@ -103,28 +109,22 @@ export function resolveRectRows(program, layer, dataset) {
103
109
  height
104
110
  };
105
111
  } else {
106
- const endX = x2.values[index];
107
- const endY = y2.values[index];
112
+ const startX = x?.values[index] ?? bounds.x;
113
+ const startY = y?.values[index] ?? bounds.y;
114
+ const endX = x2?.values[index] ?? bounds.x + bounds.width;
115
+ const endY = y2?.values[index] ?? bounds.y + bounds.height;
108
116
  if (!Number.isFinite(endX) || !Number.isFinite(endY)) return [];
109
117
  geometry = {
110
- x: Math.min(x.values[index], endX),
111
- y: Math.min(y.values[index], endY),
112
- width: Math.abs(endX - x.values[index]),
113
- height: Math.abs(endY - y.values[index])
118
+ x: Math.min(startX, endX),
119
+ y: Math.min(startY, endY),
120
+ width: Math.abs(endX - startX),
121
+ height: Math.abs(endY - startY)
114
122
  };
115
123
  if (geometry.width <= 0 || geometry.height <= 0) return [];
116
124
  }
117
125
  return [{
118
126
  row,
119
127
  sourceIndex: index,
120
- channels: Object.fromEntries(
121
- ["x", "y", "x2", "y2", "color"].flatMap(channel => {
122
- const encoding = layer.encoding?.[channel];
123
- return encoding?.field === undefined
124
- ? []
125
- : [[channel, row[encoding.field]]];
126
- })
127
- ),
128
128
  properties: { ...geometry, ...appearance(config, fill) }
129
129
  }];
130
130
  });
@@ -5,15 +5,13 @@ import {
5
5
  import {
6
6
  mapOrdinalOffsetValues,
7
7
  mapOrdinalPositionValues,
8
- readNominalField,
9
- readQuantitativeField,
10
- readScaleField,
11
- readTemporalField
8
+ readScaleField
12
9
  } from "../grammar/scales/index.js";
13
10
  import {
14
11
  finiteMidpoint,
15
12
  requireFiniteResult
16
13
  } from "../grammar/numeric.js";
14
+ import { normalizePositionDatum } from "../grammar/positionDatum.js";
17
15
  import { mapScaleConsumerValues } from "./scales/map.js";
18
16
 
19
17
  export function resolveRowEncodingValues(program, layer, dataset, channel) {
@@ -26,15 +24,17 @@ export function resolveRowEncodingValues(program, layer, dataset, channel) {
26
24
  );
27
25
  }
28
26
  const categorical = ["nominal", "ordinal"].includes(encoding.fieldType);
29
- const values = Object.hasOwn(scale, "unknown")
30
- ? readScaleField(dataset.values, encoding.field, encoding.fieldType, {
31
- allowUnknown: true
32
- })
33
- : categorical
34
- ? readNominalField(dataset.values, encoding.field)
35
- : encoding.fieldType === "temporal"
36
- ? readTemporalField(dataset.values, encoding.field)
37
- : readQuantitativeField(dataset.values, encoding.field);
27
+ const values = Object.hasOwn(encoding, "datum")
28
+ ? Array(dataset.values.length).fill(normalizePositionDatum(
29
+ encoding.datum,
30
+ encoding.fieldType,
31
+ channel,
32
+ encoding.temporalUnit,
33
+ layer.mark.type[0].toUpperCase() + layer.mark.type.slice(1)
34
+ ))
35
+ : readScaleField(dataset.values, encoding.field, encoding.fieldType, {
36
+ allowUnknown: Object.hasOwn(scale, "unknown"), temporalUnit: encoding.temporalUnit
37
+ });
38
38
  if (categorical && OFFSET_POSITION_CHANNELS.includes(channel)) {
39
39
  return mapOrdinalOffsetValues(values, scale);
40
40
  }
@@ -1,3 +1,4 @@
1
+ import { isSourceOwnedText } from "../grammar/text.js";
1
2
  import { POSITION_ENCODING_CHANNELS } from "../core/vocabulary.js";
2
3
  import { materializedLegendUsesScale } from "./legends.js";
3
4
  import { buildMaterializationPlan } from "./planner.js";
@@ -19,10 +20,12 @@ const GRIDS = Object.freeze([
19
20
 
20
21
  function usesPositionalScale(program, id) {
21
22
  return program.semanticSpec.layers.some(layer =>
22
- POSITION_ENCODING_CHANNELS.some(
23
- channel => layer.encoding?.[channel]?.scale === id
24
- ) || (layer.encoding?.parallel?.dimensions ?? []).some(
25
- dimension => dimension.scale === id
23
+ !isSourceOwnedText(layer) && (
24
+ POSITION_ENCODING_CHANNELS.some(
25
+ channel => layer.encoding?.[channel]?.scale === id
26
+ ) || (layer.encoding?.parallel?.dimensions ?? []).some(
27
+ dimension => dimension.scale === id
28
+ )
26
29
  )
27
30
  );
28
31
  }
@@ -50,7 +53,7 @@ export function needsCanvasScaleRematerialization(program, scale) {
50
53
  (scale.range === "auto" ||
51
54
  usesRadialScale(program, scale.id) ||
52
55
  semanticGuideUsesScale(program, scale.id)) &&
53
- program.resolvedScales[scale.id] !== undefined &&
56
+ (program.resolvedScales[scale.id] !== undefined || scale.radialMapping !== undefined) &&
54
57
  usesPositionalScale(program, scale.id)
55
58
  );
56
59
  }
@@ -19,6 +19,7 @@ export function mapScaleConsumerValues(values, resolvedScale, channel) {
19
19
  resolvedScale.range,
20
20
  {
21
21
  interpolation: resolvedScale.interpolate,
22
+ midpoint: resolvedScale.midpoint,
22
23
  clamp: resolvedScale.clamp ?? false,
23
24
  ...(Object.hasOwn(resolvedScale, "unknown")
24
25
  ? { unknown: resolvedScale.unknown }
@@ -1,3 +1,4 @@
1
+ import { validateMeasuredRadiusScale } from "../../../grammar/scales/radial.js";
1
2
  export function resolveArcAutoPositionRange({
2
3
  consumers,
3
4
  scale,
@@ -35,3 +36,44 @@ export function resolveArcAutoPositionRange({
35
36
  }
36
37
  return range;
37
38
  }
39
+
40
+
41
+ export function resolveMeasuredRadiusDomain({ scale, channel, allValues }) {
42
+ if (scale.radialMapping === undefined) return undefined;
43
+ if (channel !== "radius") throw new Error("Measured radius scales support only radius consumers.");
44
+ const maximum = allValues.reduce((maximum, value) => Math.max(maximum, value), -Infinity);
45
+ if (!Number.isFinite(maximum) || maximum <= 0 || allValues.some(value => value < 0)) {
46
+ throw new Error("Measured radius requires finite non-negative aggregates and a positive maximum.");
47
+ }
48
+ const domain = scale.domain === "auto" ? [0, maximum] : scale.domain;
49
+ if (Array.isArray(domain) && domain[1] < maximum) {
50
+ throw new RangeError("Measured radius domain maximum must cover every category aggregate.");
51
+ }
52
+ return domain;
53
+ }
54
+
55
+ export function validateMeasuredRadiusConsumers({ scale, domain, range, consumers, markConfigs, thetaScales }) {
56
+ if (scale.radialMapping === undefined) return;
57
+ validateMeasuredRadiusScale({ ...scale, domain, range }, { allowAuto: true });
58
+ for (const consumer of consumers) {
59
+ const { layer, encoding } = consumer;
60
+ const theta = layer.encoding?.theta;
61
+ const thetaScale = thetaScales[layer.id];
62
+ if (!isPendingMeasuredRadiusConsumer(consumer) && (layer.mark?.type !== "arc" || !["count", "sum"].includes(encoding.aggregate) ||
63
+ !["nominal", "ordinal"].includes(theta?.fieldType) || theta.aggregate !== undefined ||
64
+ thetaScale?.type !== "band" || (thetaScale.paddingInner ?? 0) !== 0 || (thetaScale.paddingOuter ?? 0) !== 0)) {
65
+ throw new Error("Measured radius requires only equal-angle categorical Arc consumers.");
66
+ }
67
+ const config = markConfigs[layer.id] ?? {};
68
+ if ((config.padAngle ?? 0) !== 0) throw new Error("Measured Arc requires padAngle 0.");
69
+ if (scale.range !== "auto" && config.innerRadiusExplicit === true &&
70
+ Math.abs(config.innerRadius - range[0] / range[1]) > Number.EPSILON * 8) {
71
+ throw new Error("Measured Arc innerRadius must agree with its explicit radius range.");
72
+ }
73
+ }
74
+ }
75
+
76
+ export function isPendingMeasuredRadiusConsumer({ layer, channel, encoding }) {
77
+ return layer.mark?.type === "arc" && channel === "radius" &&
78
+ ["count", "sum"].includes(encoding.aggregate) && layer.encoding?.theta === undefined;
79
+ }
@@ -1,11 +1,11 @@
1
+ import { isSourceOwnedText } from "../../../grammar/text.js";
1
2
  import { resolveHistogramBins } from "../../../grammar/histogram.js";
2
3
 
3
4
  export function resolveBinnedPositionDomain({
4
5
  valuesByConsumer,
5
6
  channel,
6
7
  scale,
7
- id,
8
- allValues
8
+ id
9
9
  }) {
10
10
  const binnedPositions = valuesByConsumer.filter(
11
11
  ({ consumer }) =>
@@ -13,7 +13,10 @@ export function resolveBinnedPositionDomain({
13
13
  consumer.encoding.bin !== undefined
14
14
  );
15
15
  if (binnedPositions.length === 0) return undefined;
16
- if (channel !== "x" || binnedPositions.length !== valuesByConsumer.length) {
16
+ const independentConsumers = valuesByConsumer.filter(({ consumer }) =>
17
+ !isSourceOwnedText(consumer.layer)
18
+ );
19
+ if (channel !== "x" || binnedPositions.length !== independentConsumers.length) {
17
20
  throw new Error(
18
21
  `Binned scale "${id}" cannot be shared with an unbinned consumer.`
19
22
  );
@@ -25,7 +28,7 @@ export function resolveBinnedPositionDomain({
25
28
  throw new Error(`Binned scale "${id}" requires one shared bin definition.`);
26
29
  }
27
30
  return resolveHistogramBins({
28
- values: allValues,
31
+ values: binnedPositions.flatMap(item => item.values).filter(value => value !== undefined),
29
32
  bin: binnedPositions[0].consumer.encoding.bin,
30
33
  domain: scale.domain,
31
34
  nice: scale.nice ?? true,
@@ -1,3 +1,4 @@
1
+ import { isSourceOwnedText } from "../../../grammar/text.js";
1
2
  import {
2
3
  isTransformedScaleType,
3
4
  resolveContinuousDomain,
@@ -21,7 +22,7 @@ export function resolveSeriesLayoutDomain({
21
22
  (_, index) => seriesLayouts[index] !== undefined
22
23
  );
23
24
  const directConsumers = valuesByConsumer.filter(
24
- (_, index) => seriesLayouts[index] === undefined
25
+ ({ consumer }, index) => seriesLayouts[index] === undefined && !isSourceOwnedText(consumer.layer)
25
26
  );
26
27
  const compatibleDirect = directConsumers.every(({ consumer }) =>
27
28
  layoutConsumers.every(({ consumer: layoutConsumer }) => {
@@ -1,3 +1,4 @@
1
+ import { validateContinuousColorConsumer } from "../../grammar/scales/colorConsumers.js";
1
2
  import {
2
3
  isContinuousColorScaleType,
3
4
  isDiscretePositionScaleType,
@@ -21,12 +22,13 @@ import {
21
22
  resolveStrokeWidthRange,
22
23
  resolveTransformedDomain,
23
24
  SCALE_ROLES,
25
+ validateSequentialMidpoint,
24
26
  validateLinearScaleType,
25
27
  validateOrdinalScaleType,
26
28
  validateScaleTypeForRole,
27
29
  validateTimeScaleType
28
30
  } from "../../grammar/scales/index.js";
29
- import { resolveArcAutoPositionRange } from "./policies/arc.js";
31
+ import { resolveArcAutoPositionRange, resolveMeasuredRadiusDomain, validateMeasuredRadiusConsumers } from "./policies/arc.js";
30
32
  import { resolveTemporalBarBand } from "./policies/bar.js";
31
33
  import { resolveOffsetScalePolicy } from "./policies/offset.js";
32
34
  import { resolveBinnedPositionDomain } from "./policies/binnedPosition.js";
@@ -139,7 +141,9 @@ function resolveContinuousScale({
139
141
  : validateLinearScaleType(scale.type),
140
142
  domain,
141
143
  range,
144
+ ...(scale.midpoint === undefined ? {} : { midpoint: scale.midpoint }),
142
145
  ...(scale.clamp === undefined ? {} : { clamp: scale.clamp }),
146
+ ...(scale.radialMapping === undefined ? {} : { radialMapping: scale.radialMapping }),
143
147
  ...(scale.type === "log"
144
148
  ? {
145
149
  base: normalizeTransformParameters("log", {
@@ -182,19 +186,38 @@ function reverseResolvedScale(scale) {
182
186
  };
183
187
  }
184
188
 
185
- export function resolveScaleMaterialization({
186
- id,
187
- scale,
188
- channel,
189
- consumers,
190
- valuesByConsumer,
191
- bounds,
192
- resolvedScales,
193
- markConfigs
194
- }) {
189
+ export function resolveScaleMaterialization(options) {
190
+ const {
191
+ id,
192
+ scale,
193
+ channel,
194
+ consumers,
195
+ valuesByConsumer,
196
+ bounds,
197
+ resolvedScales,
198
+ markConfigs,
199
+ thetaScales
200
+ } = options;
195
201
  const allValues = valuesByConsumer
196
202
  .flatMap(item => item.values)
197
203
  .filter(value => value !== undefined);
204
+ const preserved = resolvedScales[id]?.domain;
205
+ if (
206
+ scale.domain === "auto" &&
207
+ Array.isArray(preserved) &&
208
+ consumers.some(({ layer }) =>
209
+ markConfigs?.[layer.id]?.markFilter?.empty === true
210
+ )
211
+ ) {
212
+ return resolveScaleMaterialization({
213
+ ...options,
214
+ scale: { ...scale, domain: preserved },
215
+ valuesByConsumer: valuesByConsumer.map(item => ({
216
+ ...item,
217
+ categoryOrder: undefined
218
+ }))
219
+ });
220
+ }
198
221
  const isSequentialColor = channel === "color" &&
199
222
  isContinuousColorScaleType(scale.type);
200
223
  const isDiscretizedColor = channel === "color" &&
@@ -210,6 +233,9 @@ export function resolveScaleMaterialization({
210
233
  !isOrdinalOffset &&
211
234
  isDiscretePositionScaleType(scale.type);
212
235
 
236
+ if (isSequentialColor || isDiscretizedColor) {
237
+ for (const consumer of consumers) validateContinuousColorConsumer(consumer.layer, consumer.encoding, scale);
238
+ }
213
239
  let discretizedScale;
214
240
  if (isDiscretizedColor) {
215
241
  discretizedScale = resolveDiscretizedColorScale({
@@ -225,8 +251,7 @@ export function resolveScaleMaterialization({
225
251
  valuesByConsumer,
226
252
  channel,
227
253
  scale,
228
- id,
229
- allValues
254
+ id
230
255
  });
231
256
  const seriesLayouts = valuesByConsumer.map(item => item.seriesLayout);
232
257
  const seriesDomain = isDiscretizedColor || binnedDomain !== undefined
@@ -247,7 +272,7 @@ export function resolveScaleMaterialization({
247
272
  }
248
273
  const domain = isDiscretizedColor
249
274
  ? discretizedScale.domain
250
- : categoryOrderDomain ?? binnedDomain ?? seriesDomain ?? resolveDefaultDomain({
275
+ : resolveMeasuredRadiusDomain({ scale, channel, allValues }) ?? categoryOrderDomain ?? binnedDomain ?? seriesDomain ?? resolveDefaultDomain({
251
276
  scale,
252
277
  allValues,
253
278
  isOrdinalAppearance,
@@ -267,6 +292,13 @@ export function resolveScaleMaterialization({
267
292
  isOrdinalOffset,
268
293
  discretizedScale
269
294
  });
295
+ validateSequentialMidpoint(scale.midpoint, scale.type, domain);
296
+ if (scale.midpoint !== undefined && (channel !== "color" || consumers.some(
297
+ consumer => consumer.encoding.fieldType !== "quantitative"
298
+ ))) {
299
+ throw new Error("Scale midpoint requires quantitative color consumers.");
300
+ }
301
+ validateMeasuredRadiusConsumers({ scale, domain, range, consumers, markConfigs, thetaScales });
270
302
  if (channel === "shape" && domain.length > range.length) {
271
303
  throw new Error(
272
304
  `Shape scale "${id}" requires at least one distinct shape per domain value.`
@@ -0,0 +1,11 @@
1
+ export function assertEncodingSelectionCompatibility(program, target, channels) {
2
+ for (const [id, selection] of Object.entries(
3
+ program.materializationConfigs.selections ?? {}
4
+ )) {
5
+ if (selection.target === target && channels.includes(selection.selector?.channel)) {
6
+ throw new Error(
7
+ `Cannot remove ${selection.selector.channel} encoding while selection "${id}" references that channel.`
8
+ );
9
+ }
10
+ }
11
+ }