ggaction 0.0.11 → 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 (289) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +3 -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 +33 -27
  65. package/src/actions/encodings/parallel.js +2 -8
  66. package/src/actions/encodings/position/apply.js +40 -4
  67. package/src/actions/encodings/position/index.js +43 -5
  68. package/src/actions/encodings/position/policies/arc.js +37 -2
  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 +79 -24
  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 +153 -11
  83. package/src/actions/errorBars/edit.js +365 -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 +138 -14
  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 +60 -4
  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/core/vocabulary.js +2 -2
  195. package/src/grammar/aggregate.js +66 -23
  196. package/src/grammar/arcs.js +213 -65
  197. package/src/grammar/areaEndpoints.js +46 -0
  198. package/src/grammar/areaSeries.js +87 -41
  199. package/src/grammar/bars/aggregate.js +33 -12
  200. package/src/grammar/bars/policy.js +9 -7
  201. package/src/grammar/bin.js +181 -0
  202. package/src/grammar/categoryOrder.js +34 -0
  203. package/src/grammar/computed.js +150 -0
  204. package/src/grammar/ecdf.js +198 -0
  205. package/src/grammar/facets/index.js +117 -6
  206. package/src/grammar/fold.js +123 -0
  207. package/src/grammar/horizon.js +6 -4
  208. package/src/grammar/interval.js +37 -20
  209. package/src/grammar/lineSeries.js +20 -63
  210. package/src/grammar/markFilter.js +22 -4
  211. package/src/grammar/markLabels.js +93 -0
  212. package/src/grammar/parallelAxisTitles.js +16 -0
  213. package/src/grammar/pathSeries.js +91 -0
  214. package/src/grammar/pointPacking.js +183 -0
  215. package/src/grammar/positionCompatibility.js +1 -1
  216. package/src/grammar/positionDatum.js +12 -0
  217. package/src/grammar/rects.js +9 -1
  218. package/src/grammar/regression/derive.js +4 -0
  219. package/src/grammar/regression/index.js +7 -2
  220. package/src/grammar/regression/models.js +22 -7
  221. package/src/grammar/regression/parameters.js +40 -13
  222. package/src/grammar/rotation.js +29 -0
  223. package/src/grammar/rules.js +9 -21
  224. package/src/grammar/scales/color.js +23 -1
  225. package/src/grammar/scales/colorConsumers.js +38 -0
  226. package/src/grammar/scales/definition.js +15 -1
  227. package/src/grammar/scales/discretized.js +11 -5
  228. package/src/grammar/scales/fields.js +38 -8
  229. package/src/grammar/scales/mapping.js +2 -0
  230. package/src/grammar/scales/ordinal.js +23 -0
  231. package/src/grammar/scales/radial.js +55 -0
  232. package/src/grammar/scales/types.js +1 -0
  233. package/src/grammar/schemas/semanticPath.js +17 -3
  234. package/src/grammar/stack.js +170 -0
  235. package/src/grammar/statistics/confidenceInterval.js +101 -0
  236. package/src/grammar/summary.js +183 -0
  237. package/src/grammar/text.js +22 -16
  238. package/src/grammar/timeUnit.js +21 -11
  239. package/src/grammar/transformTopology.js +6 -0
  240. package/src/grammar/transforms.js +36 -0
  241. package/src/grammar/valueFormat.js +112 -0
  242. package/src/grammar/window.js +13 -7
  243. package/src/layout/facets.js +86 -23
  244. package/src/layout/guideCollisions.js +12 -0
  245. package/src/layout/legend.js +2 -10
  246. package/src/layout/legendItems.js +102 -0
  247. package/src/layout/legendLane.js +100 -11
  248. package/src/materialization/bars/aggregate.js +38 -152
  249. package/src/materialization/bars/histogram.js +24 -0
  250. package/src/materialization/categorySlotOffset.js +40 -0
  251. package/src/materialization/compositionSnapshot.js +1 -1
  252. package/src/materialization/dependencies.js +57 -16
  253. package/src/materialization/encodings.js +1 -1
  254. package/src/materialization/facetGuides/legacyCategorical.js +7 -1
  255. package/src/materialization/facetGuides/placement.js +16 -1
  256. package/src/materialization/facetGuides/preparation.js +26 -1
  257. package/src/materialization/facets.js +11 -2
  258. package/src/materialization/guides/layout.js +57 -0
  259. package/src/materialization/guides/resources.js +2 -2
  260. package/src/materialization/legends.js +10 -1
  261. package/src/materialization/marks/capabilities.js +30 -7
  262. package/src/materialization/marks/grouping.js +52 -0
  263. package/src/materialization/marks/index.js +63 -25
  264. package/src/materialization/marks/policies.js +2 -0
  265. package/src/materialization/rect.js +33 -33
  266. package/src/materialization/rowEncoding.js +70 -14
  267. package/src/materialization/scaleGuideDependencies.js +8 -5
  268. package/src/materialization/scales/map.js +1 -0
  269. package/src/materialization/scales/policies/arc.js +42 -0
  270. package/src/materialization/scales/policies/bar.js +0 -43
  271. package/src/materialization/scales/policies/binnedPosition.js +7 -4
  272. package/src/materialization/scales/policies/offset.js +54 -0
  273. package/src/materialization/scales/policies/series.js +2 -1
  274. package/src/materialization/scales/resolve.js +48 -18
  275. package/src/materialization/selection/compatibility.js +11 -0
  276. package/src/materialization/selection/items/bar.js +12 -68
  277. package/src/materialization/selection/items/common.js +8 -2
  278. package/src/materialization/selection/items/path.js +3 -13
  279. package/src/materialization/selection/items/rect.js +7 -4
  280. package/src/materialization/text.js +64 -5
  281. package/src/mcp/adapter.js +2 -2
  282. package/src/selectors/datasets.js +13 -0
  283. package/src/selectors/layers.js +3 -2
  284. package/src/selectors/scales.js +6 -0
  285. package/src/theme/defaults.js +39 -0
  286. package/types/basic.d.ts +11 -2
  287. package/types/index.d.ts +126 -0
  288. package/types/program.d.ts +1327 -124
  289. package/src/materialization/bars/grouped.js +0 -144
@@ -149,6 +149,27 @@ function compatibleLegendChildren(program, source, inferred) {
149
149
  });
150
150
  }
151
151
 
152
+ function promotedLegendPlacement(source, kinds) {
153
+ const positioned = kinds.map(kind => source.guideConfigs.legend[kind])
154
+ .filter(config => config.position !== undefined);
155
+ const positions = new Set(positioned.map(config => config.position));
156
+ if (positions.size > 1) {
157
+ throw new Error(
158
+ "Shared facet legends must use one common edge position."
159
+ );
160
+ }
161
+ const aligns = new Set(positioned.map(config => config.align ?? "center"));
162
+ if (aligns.size > 1) {
163
+ throw new Error(
164
+ "Shared facet legends must use one common alignment."
165
+ );
166
+ }
167
+ return {
168
+ position: positioned[0]?.position ?? "right",
169
+ align: positioned[0]?.align ?? "center"
170
+ };
171
+ }
172
+
152
173
  export function prepareSharedFacetLegend(program) {
153
174
  if (program.compositionSpec.facet.guides.legend !== "shared") {
154
175
  return undefined;
@@ -176,6 +197,8 @@ export function prepareSharedFacetLegend(program) {
176
197
  throw new Error("Shared facet legend has no measurable concrete bounds.");
177
198
  }
178
199
  const gradient = kinds.includes("gradient");
200
+ const placement = promotedLegendPlacement(source, kinds);
201
+ const width = Math.ceil(bounds.right - bounds.left);
179
202
  return {
180
203
  mode: "promoted",
181
204
  source,
@@ -185,8 +208,10 @@ export function prepareSharedFacetLegend(program) {
185
208
  bounds,
186
209
  compatibility,
187
210
  reservation: {
211
+ ...placement,
188
212
  gap: gradient ? 24 : 18,
189
- width: gradient ? 112 : Math.max(132, Math.ceil(bounds.right - bounds.left))
213
+ width: Math.max(gradient ? 112 : 132, width),
214
+ height: Math.ceil(bounds.bottom - bounds.top)
190
215
  }
191
216
  };
192
217
  }
@@ -162,10 +162,17 @@ export function resolveFacetProgramLayout(program, preparedLegend) {
162
162
  }
163
163
  const title = titleLayout(program);
164
164
  const spec = program.compositionSpec;
165
+ const gridCells = new Map(
166
+ (spec.facet.grid?.cells ?? []).map(cell => [cell.id, cell])
167
+ );
165
168
  const layout = resolveFacetLayout({
166
169
  children: spec.children.map((id, index) => ({
167
170
  ...compositionChildDescriptor(id, program.children[id]),
168
- value: spec.facet.values[index]
171
+ value: spec.facet.values[index],
172
+ ...(gridCells.has(id) ? {
173
+ row: gridCells.get(id).row,
174
+ column: gridCells.get(id).column
175
+ } : {})
169
176
  })),
170
177
  columns: spec.columns,
171
178
  gap: spec.gap,
@@ -175,7 +182,9 @@ export function resolveFacetProgramLayout(program, preparedLegend) {
175
182
  sharedLegend: spec.facet.guides.legend === "shared",
176
183
  ...(preparedLegend === undefined ? {} : {
177
184
  sharedLegendGap: preparedLegend.reservation.gap,
178
- sharedLegendWidth: preparedLegend.reservation.width
185
+ sharedLegendWidth: preparedLegend.reservation.width,
186
+ sharedLegendHeight: preparedLegend.reservation.height,
187
+ sharedLegendPosition: preparedLegend.reservation.position
179
188
  })
180
189
  });
181
190
  const plots = spec.children.map(id => ({
@@ -0,0 +1,57 @@
1
+ import { assertGuideCollisionBlocks } from "../../layout/guideCollisions.js";
2
+ import { unionConcreteGraphicBounds } from "../../grammar/schemas/graphicBounds.js";
3
+ import { axisGraphicIds, legendResourcePolicies } from "./resources.js";
4
+
5
+ // Only domain-owned guides participate. Extension primitives may deliberately
6
+ // overlap, and a combined categorical/size legend is one occupied group.
7
+ export function resolveGuideCollisionBlocks(graphicSpec, guideConfigs, titleConfig) {
8
+ const blocks = [];
9
+ const append = (id, kind, position, ids) => {
10
+ if (position === undefined) return;
11
+ const existing = ids.filter(id => graphicSpec.objects[id] !== undefined);
12
+ const bounds = unionConcreteGraphicBounds(graphicSpec, existing);
13
+ if (bounds !== undefined) blocks.push({ id, kind, position, bounds });
14
+ };
15
+ append("chart title", "title", titleConfig?.position, ["chartTitle", "chartSubtitle"]);
16
+ for (const channel of ["x", "y"]) {
17
+ for (const id of axisGraphicIds(channel)) {
18
+ const component = id.slice(`${channel}Axis`.length).toLowerCase();
19
+ append(`${channel}-axis ${component}`, "axis", guideConfigs.axis?.[channel]?.[component]?.position, [id]);
20
+ }
21
+ }
22
+ const policies = legendResourcePolicies();
23
+ const configs = guideConfigs.legend ?? {};
24
+ const categorical = policies.find(policy => policy.family === "categorical" && configs[policy.kind] !== undefined);
25
+ const combined = categorical !== undefined && configs.size !== undefined &&
26
+ configs[categorical.kind].target === configs.size.target;
27
+ for (const policy of policies) {
28
+ const config = configs[policy.kind];
29
+ if (config === undefined || combined && policy.kind === "size") continue;
30
+ const ids = combined && policy === categorical
31
+ ? [...policy.graphicIds, ...policies.find(item => item.kind === "size").graphicIds]
32
+ : policy.graphicIds;
33
+ append(`${policy.kind}${combined && policy === categorical ? "+size" : ""} legend`, "legend", config.position, ids);
34
+ }
35
+ return blocks;
36
+ }
37
+
38
+
39
+ // Defer cross-guide checks while sibling materializers still carry old
40
+ // coordinates. The scope is transient and never survives a returned program.
41
+ export function withGuideLayoutTransaction(program, apply) {
42
+ if (program.context.deferGuideLayoutValidation === true) return apply(program);
43
+ const result = apply(program._withContext({ deferGuideLayoutValidation: true }));
44
+ const { deferGuideLayoutValidation: _deferred, ...context } = result.context;
45
+ if (Object.hasOwn(program.context, "deferGuideLayoutValidation")) {
46
+ context.deferGuideLayoutValidation = program.context.deferGuideLayoutValidation;
47
+ }
48
+ const next = result._clone({ context });
49
+ assertGuideCollisionBlocks(resolveGuideCollisionBlocks(next.graphicSpec, next.guideConfigs, next.titleConfig));
50
+ return next;
51
+ }
52
+
53
+ export function withGuideLayoutValidation(implementation) {
54
+ return function (args) {
55
+ return withGuideLayoutTransaction(this, program => implementation.call(program, args));
56
+ };
57
+ }
@@ -27,7 +27,7 @@ const LEGEND_RESOURCE_POLICIES = Object.freeze({
27
27
  family: "size",
28
28
  rematerializeOp: "rematerializeSizeLegend",
29
29
  graphicIds: Object.freeze([
30
- "sizeLegendSymbols", "sizeLegendLabels", "sizeLegendTitle"
30
+ "sizeLegendBackground", "sizeLegendSymbols", "sizeLegendLabels", "sizeLegendTitle"
31
31
  ])
32
32
  }),
33
33
  gradient: Object.freeze({
@@ -62,7 +62,7 @@ const LEGEND_RESOURCE_POLICIES = Object.freeze({
62
62
  family: "strokeWidth",
63
63
  rematerializeOp: "rematerializeStrokeWidthLegend",
64
64
  graphicIds: Object.freeze([
65
- "strokeWidthLegendSymbols", "strokeWidthLegendLabels",
65
+ "strokeWidthLegendBackground", "strokeWidthLegendSymbols", "strokeWidthLegendLabels",
66
66
  "strokeWidthLegendTitle"
67
67
  ])
68
68
  })
@@ -1,5 +1,11 @@
1
+ import { findLayer } from "../selectors/layers.js";
1
2
  import { LEGEND_CONFIG_KINDS } from "../core/vocabulary.js";
2
3
 
4
+ export function isOpacityLegendLayer(layer) {
5
+ return ["point", "line"].includes(layer.mark?.type) &&
6
+ layer.encoding?.opacity?.scale !== undefined;
7
+ }
8
+
3
9
  export function hasMaterializedLegend(program) {
4
10
  return LEGEND_CONFIG_KINDS.some(kind => {
5
11
  const config = program.guideConfigs?.legend?.[kind];
@@ -15,6 +21,9 @@ export function materializedLegendUsesScale(program, id) {
15
21
  return LEGEND_CONFIG_KINDS.some(kind => {
16
22
  const config = program.guideConfigs?.legend?.[kind];
17
23
  if (config === undefined) return false;
18
- return config.scale === id || config.scales?.includes(id);
24
+ const order = program.semanticSpec.guides.legend?.[kind]?.order;
25
+ const linkedScale = order?.channel === undefined ? undefined
26
+ : findLayer(program, config.target)?.encoding?.[order.channel]?.scale;
27
+ return config.scale === id || config.scales?.includes(id) || linkedScale === id;
19
28
  });
20
29
  }
@@ -108,11 +108,19 @@ export function isIntentionallyEmptyArea(program, layer) {
108
108
  export function canMaterializeArc(_program, layer) {
109
109
  if (
110
110
  layer.mark?.type !== "arc" ||
111
- layer.encoding?.theta?.scale === undefined ||
112
- !["nominal", "ordinal"].includes(layer.encoding.theta.fieldType)
111
+ layer.encoding?.theta?.scale === undefined
113
112
  ) {
114
113
  return false;
115
114
  }
115
+ if (layer.encoding.theta.fieldType === "quantitative") {
116
+ return (
117
+ layer.encoding.theta.aggregate === undefined &&
118
+ layer.encoding?.radius === undefined
119
+ );
120
+ }
121
+ if (!["nominal", "ordinal"].includes(layer.encoding.theta.fieldType)) {
122
+ return false;
123
+ }
116
124
  if (["count", "sum"].includes(layer.encoding.theta.aggregate)) {
117
125
  return layer.encoding?.radius === undefined;
118
126
  }
@@ -137,9 +145,9 @@ export function canMaterializeBar(program, layer) {
137
145
  ) {
138
146
  const offsetChannel = resolveBarOffsetChannel(layer);
139
147
  return (
140
- layer.encoding?.color?.field !== undefined &&
141
- layer.encoding?.[offsetChannel]?.field === layer.encoding.color.field &&
142
- layer.encoding[offsetChannel].scale !== undefined
148
+ (layer.encoding?.group !== undefined || (layer.encoding?.color?.field !== undefined &&
149
+ layer.encoding?.[offsetChannel]?.field === layer.encoding.color.field)) &&
150
+ layer.encoding[offsetChannel]?.scale !== undefined
143
151
  );
144
152
  }
145
153
  return true;
@@ -155,14 +163,29 @@ export function canMaterializeRule(program, layer) {
155
163
  );
156
164
  }
157
165
 
166
+ const TEXT_SOURCE_CAPABILITIES = Object.freeze({
167
+ point: canMaterializePoint,
168
+ line: canMaterializeLine,
169
+ bar: canMaterializeBar,
170
+ rule: canMaterializeRule,
171
+ rect: (program, layer) => canMaterializeRect(program, layer) ||
172
+ program.markConfigs[layer.id]?.gradientPlot?.materialized === true,
173
+ arc: canMaterializeArc
174
+ });
175
+
176
+ export function isTextSource(layer) {
177
+ return layer?.data !== undefined &&
178
+ Object.hasOwn(TEXT_SOURCE_CAPABILITIES, layer.mark?.type);
179
+ }
180
+
158
181
  export function canMaterializeText(program, layer) {
159
182
  if (layer.mark?.type !== "text" || layer.encoding?.text === undefined) {
160
183
  return false;
161
184
  }
162
185
  if (layer.source !== undefined) {
163
186
  const source = findLayer(program, layer.source);
164
- return source !== undefined &&
165
- ["point", "bar", "rule", "rect", "arc"].includes(source.mark?.type) &&
187
+ return isTextSource(source) &&
188
+ TEXT_SOURCE_CAPABILITIES[source.mark.type](program, source) &&
166
189
  Array.isArray(program.graphicSpec.objects[source.id]?.items);
167
190
  }
168
191
  return hasCartesianPositionScales(layer);
@@ -0,0 +1,52 @@
1
+ import { findUpstreamTransform } from "../dataProvenance.js";
2
+ import { readPathSeriesFields, validatePathSeriesAppearance } from "../../grammar/pathSeries.js";
3
+
4
+ export function validatePathGroupAppearance(program, layer, dataset) {
5
+ if (layer.mark.type === "area" && findUpstreamTransform(program, dataset, "density")) {
6
+ // Density placement owns a category/split grain beyond the visible group field.
7
+ readPathSeriesFields(dataset.values, layer);
8
+ return;
9
+ }
10
+ validatePathSeriesAppearance(dataset.values, layer);
11
+ }
12
+
13
+ export function assertPathGroupCompatible(program, layer, dataset, group) {
14
+ if (layer.encoding?.parallel !== undefined) {
15
+ throw new Error("Parallel paths have row identity and do not support encodeGroup.");
16
+ }
17
+ const field = group?.field;
18
+ for (const [type, editor] of [
19
+ ["density", "editDensity"],
20
+ ["horizon", "editHorizon"],
21
+ ["regression", "editRegression"],
22
+ ["interval", "editErrorBand"]
23
+ ]) {
24
+ const transform = findUpstreamTransform(program, dataset, type);
25
+ if (transform === undefined) continue;
26
+ const fields = type === "horizon" ? [transform.as.group]
27
+ : type === "interval"
28
+ ? transform.groupBy.filter(value =>
29
+ value !== layer.encoding?.x?.field && value !== layer.encoding?.y?.field
30
+ )
31
+ : [transform.groupBy];
32
+ if (group?.fields !== undefined || group === undefined ||
33
+ field === undefined || !fields.includes(field)) {
34
+ throw new Error(
35
+ `${type === "density" ? "Density area" : type + " path"} mark "${layer.id}" must group by ${fields.length === 1 && fields[0] !== undefined ? JSON.stringify(fields[0]) : "its owned field"}; use ${editor}.`
36
+ );
37
+ }
38
+ }
39
+ if (layer.layout?.mode !== undefined) {
40
+ if (group === undefined && !["overlay"].includes(layer.layout.mode)) throw new Error("Remove active series layout before removing its group.");
41
+ return;
42
+ }
43
+ if (layer.mark?.type === "area" && (
44
+ layer.encoding?.y?.stack === "center" ||
45
+ ![undefined, "overlay"].includes(layer.encoding?.color?.layout)
46
+ )) {
47
+ if (group?.fields !== undefined ||
48
+ (layer.encoding?.color !== undefined && field !== layer.encoding.color.field)) {
49
+ throw new Error("Area layout requires one group field matching its color field.");
50
+ }
51
+ }
52
+ }
@@ -1,5 +1,11 @@
1
- import { POSITION_CHANNELS } from "../../core/vocabulary.js";
1
+ import { isSourceOwnedText } from "../../grammar/text.js";
2
+ import { findLayer } from "../../selectors/layers.js";
3
+ import {
4
+ POSITION_CHANNELS,
5
+ POSITION_ENCODING_CHANNELS
6
+ } from "../../core/vocabulary.js";
2
7
  import { getMarkMaterializationPolicy } from "./policies.js";
8
+ import { buildMaterializationPlan } from "../planner.js";
3
9
 
4
10
  export {
5
11
  canMaterializeArc,
@@ -10,10 +16,29 @@ export {
10
16
  canMaterializeRect,
11
17
  canMaterializeRule,
12
18
  canMaterializeText,
13
- canMaterializeTick
19
+ canMaterializeTick,
20
+ isTextSource
14
21
  } from "./capabilities.js";
15
22
 
16
- 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
+ }
17
42
  const policy = getMarkMaterializationPolicy(layer);
18
43
  return policy === undefined
19
44
  ? undefined
@@ -25,14 +50,15 @@ export function getMarkMaterializationStep(program, layer) {
25
50
  if (policy === undefined || !policy.canMaterialize(program, layer)) {
26
51
  return undefined;
27
52
  }
28
- return getMarkRematerializationStep(layer);
53
+ return getMarkRematerializationStep(program, layer);
29
54
  }
30
55
 
31
56
  export function getSourceDependentMarkSteps(program, sourceId) {
32
- return program.semanticSpec.layers.flatMap(layer =>
57
+ return (program.semanticSpec.layers ?? []).flatMap(layer =>
33
58
  layer.source === sourceId &&
34
59
  getMarkMaterializationPolicy(layer)?.sourceDependent === true
35
- ? [getMarkMaterializationStep(program, layer)].filter(
60
+ ? [getMarkMaterializationStep(program, layer) ??
61
+ getExistingMarkRematerializationStep(program, layer)].filter(
36
62
  step => step !== undefined
37
63
  )
38
64
  : []
@@ -43,39 +69,39 @@ export function getPositionEncodingMaterializationSteps(program, layer, scaleId)
43
69
  const policy = getMarkMaterializationPolicy(layer);
44
70
  if (policy === undefined) return [];
45
71
  const complete = policy.canMaterialize(program, layer);
46
- const mark = getMarkRematerializationStep(layer);
72
+ const mark = getMarkRematerializationStep(program, layer);
47
73
  const sharedConsumerMarks = (program.semanticSpec.layers ?? [])
48
74
  .filter(candidate =>
49
75
  candidate.id !== layer.id &&
76
+ !isSourceOwnedText(candidate) &&
50
77
  POSITION_CHANNELS.some(channel =>
51
78
  candidate.encoding?.[channel]?.scale === scaleId
52
79
  )
53
80
  )
54
81
  .map(candidate => getMarkMaterializationStep(program, candidate))
55
82
  .filter(step => step !== undefined);
83
+ const dependentMarks = [mark, ...sharedConsumerMarks]
84
+ .flatMap(step => getSourceDependentMarkSteps(program, step.args.id));
56
85
  const scale = {
57
86
  op: "rematerializeScale",
58
87
  args: sharedConsumerMarks.length === 0
59
88
  ? { id: scaleId }
60
89
  : { id: scaleId, marks: false }
61
90
  };
62
- if (!complete) {
63
- if (policy.positionEncoding.incomplete === "scale") {
64
- return [scale, ...sharedConsumerMarks];
65
- }
66
- return policy.positionEncoding.scaleFirst
91
+ const direct = !complete && policy.positionEncoding.incomplete === "scale"
92
+ ? [scale, ...sharedConsumerMarks]
93
+ : policy.positionEncoding.scaleFirst
67
94
  ? [scale, mark, ...sharedConsumerMarks]
68
95
  : [mark, ...sharedConsumerMarks];
69
- }
70
- return policy.positionEncoding.scaleFirst
71
- ? [scale, mark, ...sharedConsumerMarks]
72
- : [mark, ...sharedConsumerMarks];
96
+ return buildMaterializationPlan({ marks: [...direct, ...dependentMarks] });
73
97
  }
74
98
 
75
99
  export function getScaleConsumerMaterializationMode(layer, channel) {
76
100
  const policy = getMarkMaterializationPolicy(layer);
77
101
  if (policy === undefined) return "direct";
78
- if (POSITION_CHANNELS.includes(channel)) {
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";
79
105
  return policy.scaleApplication.position ?? policy.scaleApplication.default;
80
106
  }
81
107
  if (policy.scaleApplication.deferredChannels?.includes(channel)) {
@@ -96,7 +122,7 @@ export function getExistingMarkRematerializationStep(program, layer) {
96
122
  ) {
97
123
  return undefined;
98
124
  }
99
- return getMarkRematerializationStep(layer);
125
+ return getMarkRematerializationStep(program, layer);
100
126
  }
101
127
 
102
128
  export function getEncodingMaterializationStages(program, layer, channel, scale) {
@@ -108,22 +134,34 @@ export function getEncodingMaterializationStages(program, layer, channel, scale)
108
134
  args: { id: scale, guides: false, marks: false }
109
135
  }]
110
136
  : [];
111
- const shared = policy.encoding?.sharedChannels?.includes(channel) === true &&
112
- scale !== undefined;
137
+ const shared = scale !== undefined && (
138
+ scales.length > 0 || policy.encoding?.sharedChannels?.includes(channel) === true
139
+ );
113
140
  const candidates = shared
114
141
  ? program.semanticSpec.layers.filter(candidate =>
115
- candidate.mark?.type === layer.mark?.type &&
116
- candidate.encoding?.[channel]?.scale === scale
142
+ candidate.id === layer.id || candidate.encoding?.[channel]?.scale === scale
117
143
  )
118
144
  : [layer];
119
145
  const marks = candidates.flatMap(candidate => {
120
- if (policy.encoding?.skipRematerialization?.(program, candidate) === true) {
146
+ const candidatePolicy = getMarkMaterializationPolicy(candidate);
147
+ if (candidatePolicy?.encoding?.skipRematerialization?.(program, candidate) === true) {
121
148
  return [];
122
149
  }
123
- const step = policy.encoding?.completeOnly === true
150
+ const step = candidate.id !== layer.id || candidatePolicy?.encoding?.completeOnly === true
124
151
  ? getMarkMaterializationStep(program, candidate)
125
- : getMarkRematerializationStep(candidate);
152
+ : getMarkRematerializationStep(program, candidate);
126
153
  return step === undefined ? [] : [step];
127
154
  });
128
155
  return { scales, marks };
129
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
  });