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
@@ -1,9 +1,4 @@
1
1
  import { resolveGraphicBounds } from "../layout/canvas.js";
2
- import {
3
- formatVisibleText,
4
- resolveTextBounds,
5
- textBoundsIntersect
6
- } from "../core/textMetrics.js";
7
2
  import { resolvePlacedPlotBounds } from "../layout/composition.js";
8
3
  import {
9
4
  alignedTextAnchor,
@@ -17,6 +12,10 @@ import {
17
12
  clearCompositionChildren,
18
13
  compositionChildDescriptor
19
14
  } from "./composition.js";
15
+ import {
16
+ materializeFacetHeaders,
17
+ prepareFacetHeaders
18
+ } from "./facetHeaders.js";
20
19
 
21
20
  const ZERO_MARGIN = { top: 0, right: 0, bottom: 0, left: 0 };
22
21
 
@@ -50,61 +49,6 @@ function titleLayout(program) {
50
49
  return { config, block, top, height: Math.ceil(top + block.height) };
51
50
  }
52
51
 
53
- function collection(program, id, items) {
54
- return program
55
- .createGraphics({ id, type: "collection", parent: "canvas" })
56
- .editGraphics({ target: id, property: "items", value: items });
57
- }
58
-
59
- function assertFacetHeadersFit(items, layout, plotById) {
60
- const previous = [];
61
- for (let index = 0; index < items.length; index += 1) {
62
- const bounds = resolveTextBounds(items[index].properties);
63
- const cell = layout.children[index];
64
- const plot = plotById.get(cell.id);
65
- if (bounds.left < 0 || bounds.right > layout.width ||
66
- bounds.top < 0 || bounds.bottom > layout.height ||
67
- previous.some(item => textBoundsIntersect(item, bounds)) ||
68
- textBoundsIntersect(bounds, {
69
- left: cell.x + plot.x,
70
- right: cell.x + plot.x + plot.width,
71
- top: cell.y + plot.y,
72
- bottom: cell.y + plot.y + plot.height
73
- })) {
74
- throw new Error(
75
- "Facet headers require sufficient non-overlapping space above every child plot."
76
- );
77
- }
78
- previous.push(bounds);
79
- }
80
- }
81
-
82
- function materializeHeaders(program, layout, plots, config) {
83
- const plotById = new Map(plots.map(plot => [plot.id, plot]));
84
- const items = layout.children.map(cell => {
85
- const plot = plotById.get(cell.id);
86
- if (plot === undefined) {
87
- throw new Error(`Facet header requires plot bounds for "${cell.id}".`);
88
- }
89
- return {
90
- type: "text",
91
- properties: {
92
- x: cell.x + plot.x + plot.width / 2,
93
- y: cell.y + config.offset,
94
- text: formatVisibleText(cell.value),
95
- fill: config.color,
96
- fontSize: config.fontSize,
97
- fontFamily: config.fontFamily,
98
- fontWeight: config.fontWeight,
99
- textAlign: "center",
100
- textBaseline: "middle"
101
- }
102
- };
103
- });
104
- assertFacetHeadersFit(items, layout, plotById);
105
- return collection(program, `${program.compositionSpec.id}-headers`, items);
106
- }
107
-
108
52
  function materializeTitleComponent(program, id, lines, centers, style, plot, top) {
109
53
  if (lines.length === 0) return program;
110
54
  const x = alignedTextAnchor(plot.x, plot.width, program.titleConfig.align);
@@ -162,6 +106,10 @@ export function resolveFacetProgramLayout(program, preparedLegend) {
162
106
  }
163
107
  const title = titleLayout(program);
164
108
  const spec = program.compositionSpec;
109
+ const preparedHeaders = prepareFacetHeaders(
110
+ program,
111
+ facetConfig(program).headers
112
+ );
165
113
  const gridCells = new Map(
166
114
  (spec.facet.grid?.cells ?? []).map(cell => [cell.id, cell])
167
115
  );
@@ -179,6 +127,7 @@ export function resolveFacetProgramLayout(program, preparedLegend) {
179
127
  align: spec.align,
180
128
  padding: spec.padding,
181
129
  titleHeight: title.height,
130
+ headerLayout: preparedHeaders.headerLayout,
182
131
  sharedLegend: spec.facet.guides.legend === "shared",
183
132
  ...(preparedLegend === undefined ? {} : {
184
133
  sharedLegendGap: preparedLegend.reservation.gap,
@@ -195,16 +144,17 @@ export function resolveFacetProgramLayout(program, preparedLegend) {
195
144
  placements: layout.children,
196
145
  plots
197
146
  });
198
- return { layout, title, plot, plots };
147
+ return { layout, title, plot, plots, preparedHeaders };
199
148
  }
200
149
 
201
150
  export function materializeFacetGraphics(program) {
202
151
  const preparedLegend = prepareSharedFacetLegend(program);
203
- const { layout, title, plot, plots } = resolveFacetProgramLayout(
152
+ const { layout, title, plot, plots, preparedHeaders } = resolveFacetProgramLayout(
204
153
  program,
205
154
  preparedLegend
206
155
  );
207
- const config = facetConfig(program);
156
+ const background = program.graphicSpec.objects.canvas?.properties
157
+ ?.background ?? "white";
208
158
  let next = clearCompositionChildren(program);
209
159
  if (next.graphicSpec.objects.canvas === undefined) {
210
160
  next = next.createGraphics({ id: "canvas", type: "canvas" });
@@ -212,7 +162,7 @@ export function materializeFacetGraphics(program) {
212
162
  for (const [property, value] of Object.entries({
213
163
  width: layout.width,
214
164
  height: layout.height,
215
- background: "white"
165
+ background
216
166
  })) {
217
167
  next = next.editGraphics({ target: "canvas", property, value });
218
168
  }
@@ -225,7 +175,7 @@ export function materializeFacetGraphics(program) {
225
175
  });
226
176
  next = attachSnapshotObject(next, snapshot, snapshot.order[0], "canvas");
227
177
  }
228
- next = materializeHeaders(next, layout, plots, config.headers);
178
+ next = materializeFacetHeaders(next, layout, plots, preparedHeaders);
229
179
  next = next.composeFacetGuides({ layout, plot });
230
180
  if (title.height > 0) next = materializeTitle(next, plot, title);
231
181
  return next._withCanvasConfig({
@@ -22,6 +22,14 @@ const LEGEND_RESOURCE_POLICIES = Object.freeze({
22
22
  component => `colorLegend${component}`
23
23
  ))
24
24
  }),
25
+ stroke: Object.freeze({
26
+ semanticKind: "stroke",
27
+ family: "categorical",
28
+ rematerializeOp: undefined,
29
+ graphicIds: Object.freeze(CATEGORICAL_COMPONENTS.map(
30
+ component => `strokeLegend${component}`
31
+ ))
32
+ }),
25
33
  size: Object.freeze({
26
34
  semanticKind: "size",
27
35
  family: "size",
@@ -48,6 +56,24 @@ const LEGEND_RESOURCE_POLICIES = Object.freeze({
48
56
  "colorLegendTitle"
49
57
  ])
50
58
  }),
59
+ strokeGradient: Object.freeze({
60
+ semanticKind: "stroke",
61
+ family: "continuous",
62
+ rematerializeOp: "rematerializeStrokeGradientLegend",
63
+ graphicIds: Object.freeze([
64
+ "strokeGradientBackground", "strokeGradientStrips", "strokeGradientTicks",
65
+ "strokeGradientLabels", "strokeGradientTitle"
66
+ ])
67
+ }),
68
+ strokeInterval: Object.freeze({
69
+ semanticKind: "stroke",
70
+ family: "interval",
71
+ rematerializeOp: "rematerializeStrokeIntervalLegend",
72
+ graphicIds: Object.freeze([
73
+ "strokeIntervalBackground", "strokeIntervalSymbols",
74
+ "strokeIntervalLabels", "strokeIntervalTitle"
75
+ ])
76
+ }),
51
77
  opacity: Object.freeze({
52
78
  semanticKind: "opacity",
53
79
  family: "continuous",
@@ -10,7 +10,7 @@ export function hasMaterializedLegend(program) {
10
10
  return LEGEND_CONFIG_KINDS.some(kind => {
11
11
  const config = program.guideConfigs?.legend?.[kind];
12
12
  if (config === undefined) return false;
13
- if (kind === "series" || kind === "color") {
13
+ if (["series", "color", "stroke"].includes(kind)) {
14
14
  return program.semanticSpec.guides.legend?.[kind] !== undefined;
15
15
  }
16
16
  return true;
@@ -36,6 +36,9 @@ export function getScaleConsumerMarkSteps(program, scaleIds) {
36
36
  }
37
37
 
38
38
  export function getMarkRematerializationStep(program, layer) {
39
+ if (program.markConfigs?.[layer.id]?.statisticalReference !== undefined) {
40
+ return { op: "rematerializeStatisticalReference", args: { id: layer.id } };
41
+ }
39
42
  if (program.markConfigs?.[layer.id]?.markFilter?.empty === true) {
40
43
  return { op: "materializeEmptyMark", args: { id: layer.id } };
41
44
  }
@@ -54,15 +57,18 @@ export function getMarkMaterializationStep(program, layer) {
54
57
  }
55
58
 
56
59
  export function getSourceDependentMarkSteps(program, sourceId) {
57
- return (program.semanticSpec.layers ?? []).flatMap(layer =>
58
- layer.source === sourceId &&
59
- getMarkMaterializationPolicy(layer)?.sourceDependent === true
60
+ return (program.semanticSpec.layers ?? []).flatMap(layer => {
61
+ const sourceOwned = layer.source === sourceId &&
62
+ getMarkMaterializationPolicy(layer)?.sourceDependent === true;
63
+ const statistical = program.markConfigs?.[layer.id]
64
+ ?.statisticalReference?.source === sourceId;
65
+ return sourceOwned || statistical
60
66
  ? [getMarkMaterializationStep(program, layer) ??
61
67
  getExistingMarkRematerializationStep(program, layer)].filter(
62
68
  step => step !== undefined
63
69
  )
64
- : []
65
- );
70
+ : [];
71
+ });
66
72
  }
67
73
 
68
74
  export function getPositionEncodingMaterializationSteps(program, layer, scaleId) {
@@ -50,7 +50,7 @@ const MARK_MATERIALIZATION_POLICIES = Object.freeze({
50
50
  op: "rematerializeAreaMark",
51
51
  positionEncoding: Object.freeze({ incomplete: "scale", scaleFirst: true }),
52
52
  encoding: Object.freeze({
53
- sharedChannels: Object.freeze(["color"]),
53
+ sharedChannels: Object.freeze(["color", "stroke"]),
54
54
  skipRematerialization: isIntentionallyEmptyArea
55
55
  }),
56
56
  scaleApplication: Object.freeze({ default: "defer" })
@@ -58,6 +58,7 @@ const MARK_MATERIALIZATION_POLICIES = Object.freeze({
58
58
  arc: Object.freeze({
59
59
  canMaterialize: canMaterializeArc,
60
60
  op: "rematerializeArcMark",
61
+ acceptsResolvedScales: true,
61
62
  positionEncoding: Object.freeze({ incomplete: "scale", scaleFirst: false }),
62
63
  encoding: Object.freeze({ completeOnly: true }),
63
64
  scaleApplication: Object.freeze({ default: "defer" })
@@ -3,10 +3,12 @@ import {
3
3
  readScaleField
4
4
  } from "../grammar/scales/index.js";
5
5
  import { normalizePositionDatum } from "../grammar/positionDatum.js";
6
- import { resolveGraphicBounds } from "../layout/canvas.js";
6
+ import { resolveCoordinateBounds } from "./coordinateBounds.js";
7
7
  import { RECT_MODES, resolveRectMode, rectUsesFields } from "../grammar/rects.js";
8
8
  import { DEFAULT_RECT_MARK } from "./rectConfig.js";
9
9
  import { mapScaleConsumerValues } from "./scales/map.js";
10
+ import { materializeRectItem } from "../grammar/roundedRect.js";
11
+ import { requestedStrokeDetails } from "../grammar/strokeStyle.js";
10
12
 
11
13
  function optionalValues(rows, encoding, length, channel) {
12
14
  if (Object.hasOwn(encoding, "datum")) {
@@ -22,7 +24,11 @@ function requiredChannels(layer, mode) {
22
24
  const position = mode === RECT_MODES.xSpan ? ["x", "x2"]
23
25
  : mode === RECT_MODES.ySpan ? ["y", "y2"]
24
26
  : mode === RECT_MODES.ranged ? ["x", "x2", "y", "y2"] : ["x", "y"];
25
- return [...position, ...(layer.encoding?.color ? ["color"] : [])];
27
+ return [
28
+ ...position,
29
+ ...(layer.encoding?.color ? ["color"] : []),
30
+ ...(layer.encoding?.stroke ? ["stroke"] : [])
31
+ ];
26
32
  }
27
33
 
28
34
  export function resolveRectConsumerValues(layer, dataset, channel) {
@@ -64,19 +70,22 @@ function mappedEncoding(program, layer, dataset, channel) {
64
70
  };
65
71
  }
66
72
 
67
- function appearance(config, fill) {
73
+ function appearance(config, fill, stroke) {
68
74
  return {
69
75
  fill,
70
76
  opacity: config.opacity,
71
- stroke: config.stroke === false ? "transparent" : config.stroke,
72
- strokeWidth: config.stroke === false ? 0 : config.strokeWidth
77
+ stroke: stroke ?? (config.stroke === false ? "transparent" : config.stroke),
78
+ strokeWidth: stroke === undefined && config.stroke === false
79
+ ? 0
80
+ : config.strokeWidth ?? 1,
81
+ ...requestedStrokeDetails(config, "Rect mark")
73
82
  };
74
83
  }
75
84
 
76
85
  export function resolveRectRows(program, layer, dataset) {
77
86
  const mode = resolveRectMode(layer);
78
87
  if (mode === undefined) return [];
79
- const bounds = resolveGraphicBounds(program);
88
+ const bounds = resolveCoordinateBounds(program, layer.coordinate);
80
89
  const x = layer.encoding.x === undefined ? undefined : mappedEncoding(program, layer, dataset, "x");
81
90
  const y = layer.encoding.y === undefined ? undefined : mappedEncoding(program, layer, dataset, "y");
82
91
  const x2 = layer.encoding.x2 === undefined ? undefined : mappedEncoding(program, layer, dataset, "x2");
@@ -84,6 +93,9 @@ export function resolveRectRows(program, layer, dataset) {
84
93
  const color = layer.encoding?.color === undefined
85
94
  ? undefined
86
95
  : mappedEncoding(program, layer, dataset, "color").values;
96
+ const stroke = layer.encoding?.stroke === undefined
97
+ ? undefined
98
+ : mappedEncoding(program, layer, dataset, "stroke").values;
87
99
  const config = program.markConfigs[layer.id] ?? DEFAULT_RECT_MARK;
88
100
 
89
101
  const rows = rectUsesFields(layer) ? dataset.values : [{}];
@@ -92,7 +104,8 @@ export function resolveRectRows(program, layer, dataset) {
92
104
  if (
93
105
  (x !== undefined && !Number.isFinite(x.values[index])) ||
94
106
  (y !== undefined && !Number.isFinite(y.values[index])) ||
95
- typeof fill !== "string"
107
+ typeof fill !== "string" ||
108
+ (stroke !== undefined && typeof stroke[index] !== "string")
96
109
  ) return [];
97
110
 
98
111
  let geometry;
@@ -125,14 +138,14 @@ export function resolveRectRows(program, layer, dataset) {
125
138
  return [{
126
139
  row,
127
140
  sourceIndex: index,
128
- properties: { ...geometry, ...appearance(config, fill) }
141
+ properties: { ...geometry, ...appearance(config, fill, stroke?.[index]) }
129
142
  }];
130
143
  });
131
144
  }
132
145
 
133
146
  export function resolveRectGraphicItems(program, layer, dataset) {
134
- return resolveRectRows(program, layer, dataset).map(item => ({
135
- type: "rect",
136
- properties: item.properties
137
- }));
147
+ const radius = program.markConfigs[layer.id]?.cornerRadius ?? 0;
148
+ return resolveRectRows(program, layer, dataset).map(item =>
149
+ materializeRectItem(item.properties, radius)
150
+ );
138
151
  }
@@ -4,6 +4,7 @@ import {
4
4
  validateUnitInterval
5
5
  } from "../core/validation.js";
6
6
  import { DEFAULT_COLORS } from "../theme/defaults.js";
7
+ import { requestedRectStyleDetails } from "../grammar/roundedRect.js";
7
8
 
8
9
  export const DEFAULT_RECT_MARK = Object.freeze({
9
10
  fill: DEFAULT_COLORS.mark,
@@ -13,7 +14,10 @@ export const DEFAULT_RECT_MARK = Object.freeze({
13
14
  });
14
15
 
15
16
  export function normalizeRectMarkConfig(patch, previous = DEFAULT_RECT_MARK) {
16
- const result = { ...previous };
17
+ const result = {
18
+ ...previous,
19
+ ...requestedRectStyleDetails(patch, "Rect")
20
+ };
17
21
  if (Object.hasOwn(patch, "fill")) {
18
22
  result.fill = validateNonEmptyString(patch.fill, "Rect fill");
19
23
  }
@@ -4,15 +4,17 @@ import {
4
4
  isOrdinalScaleType,
5
5
  mapContinuousScaleValues,
6
6
  mapDiscretizedColors,
7
+ mapSizeValues,
7
8
  mapOrdinalValues,
8
9
  mapSequentialColors
9
10
  } from "../../grammar/scales/index.js";
10
11
 
11
12
  export function mapScaleConsumerValues(values, resolvedScale, channel) {
12
- if (channel === "color" && isDiscretizedColorScaleType(resolvedScale.type)) {
13
+ if (channel === "size") return mapSizeValues(values, resolvedScale);
14
+ if (["color", "stroke"].includes(channel) && isDiscretizedColorScaleType(resolvedScale.type)) {
13
15
  return mapDiscretizedColors(values, resolvedScale);
14
16
  }
15
- if (channel === "color" && isContinuousColorScaleType(resolvedScale.type)) {
17
+ if (["color", "stroke"].includes(channel) && isContinuousColorScaleType(resolvedScale.type)) {
16
18
  return mapSequentialColors(
17
19
  values,
18
20
  resolvedScale.domain,
@@ -28,7 +30,7 @@ export function mapScaleConsumerValues(values, resolvedScale, channel) {
28
30
  );
29
31
  }
30
32
  if (
31
- ["color", "strokeDash", "shape"].includes(channel) &&
33
+ ["color", "stroke", "strokeDash", "shape"].includes(channel) &&
32
34
  isOrdinalScaleType(resolvedScale.type)
33
35
  ) {
34
36
  return mapOrdinalValues(values, resolvedScale.domain, resolvedScale.range, {
@@ -13,8 +13,8 @@ export function resolveBinnedPositionDomain({
13
13
  consumer.encoding.bin !== undefined
14
14
  );
15
15
  if (binnedPositions.length === 0) return undefined;
16
- const independentConsumers = valuesByConsumer.filter(({ consumer }) =>
17
- !isSourceOwnedText(consumer.layer)
16
+ const independentConsumers = valuesByConsumer.filter(({ consumer, scalePolicy }) =>
17
+ scalePolicy !== false && !isSourceOwnedText(consumer.layer)
18
18
  );
19
19
  if (channel !== "x" || binnedPositions.length !== independentConsumers.length) {
20
20
  throw new Error(
@@ -1,4 +1,4 @@
1
- import { normalizeOffsetPadding } from "../../../grammar/bars/geometry.js";
1
+ import { normalizeOffsetScalePolicy } from "../../../grammar/bars/geometry.js";
2
2
 
3
3
  function resolveParentSlot(consumer, resolvedScales, parentChannel) {
4
4
  if (parentChannel === "x" && consumer.layer.encoding?.x?.bin !== undefined) {
@@ -16,6 +16,7 @@ function resolveParentSlot(consumer, resolvedScales, parentChannel) {
16
16
  }
17
17
 
18
18
  export function resolveOffsetScalePolicy({
19
+ scale,
19
20
  consumers,
20
21
  resolvedScales,
21
22
  markConfigs,
@@ -34,21 +35,62 @@ export function resolveOffsetScalePolicy({
34
35
  `${channel} scale "${id}" requires one shared resolved ${parentChannel} categorical slot.`
35
36
  );
36
37
  }
37
- const paddings = consumers.map(consumer => normalizeOffsetPadding(
38
+ return {
39
+ parentBandwidth: slots[0],
40
+ ...resolveRequestedOffsetPolicy({
41
+ scale,
42
+ consumers,
43
+ markConfigs,
44
+ id,
45
+ channel
46
+ })
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Resolve the requested offset policy independently from its parent slot.
52
+ * Semantic scale fields are canonical. Consistent legacy mark-owned padding is
53
+ * retained as a fallback until the next successful scale/encoding edit migrates
54
+ * it into the semantic scale.
55
+ */
56
+ export function resolveRequestedOffsetPolicy({
57
+ scale,
58
+ consumers = [],
59
+ markConfigs = {},
60
+ id = scale?.id ?? channel,
61
+ channel = "xOffset"
62
+ }) {
63
+ const semanticPatch = scale === undefined
64
+ ? {}
65
+ : Object.fromEntries(
66
+ ["padding", "paddingInner", "paddingOuter", "align"]
67
+ .filter(property => Object.hasOwn(scale, property))
68
+ .map(property => [property, scale[property]])
69
+ );
70
+ if (
71
+ Object.hasOwn(semanticPatch, "padding") ||
72
+ ["paddingInner", "paddingOuter", "align"].every(
73
+ property => Object.hasOwn(semanticPatch, property)
74
+ )
75
+ ) {
76
+ return normalizeOffsetScalePolicy(semanticPatch, undefined, channel);
77
+ }
78
+ const legacyPolicies = consumers.map(consumer => normalizeOffsetScalePolicy(
38
79
  markConfigs[consumer.layer.id]?.[channel],
39
80
  undefined,
40
81
  channel
41
82
  ));
42
83
  const signatures = new Set(
43
- paddings.map(padding => JSON.stringify(padding))
84
+ legacyPolicies.map(policy => JSON.stringify(policy))
44
85
  );
45
- if (signatures.size !== 1) {
86
+ if (signatures.size > 1) {
46
87
  throw new Error(
47
88
  `${channel} scale "${id}" requires one shared padding policy.`
48
89
  );
49
90
  }
50
- return {
51
- parentBandwidth: slots[0],
52
- ...paddings[0]
53
- };
91
+ const legacy = legacyPolicies[0] ?? normalizeOffsetScalePolicy(
92
+ {}, undefined, channel
93
+ );
94
+ if (scale === undefined) return legacy;
95
+ return normalizeOffsetScalePolicy(semanticPatch, legacy, channel);
54
96
  }
@@ -22,7 +22,10 @@ export function resolveSeriesLayoutDomain({
22
22
  (_, index) => seriesLayouts[index] !== undefined
23
23
  );
24
24
  const directConsumers = valuesByConsumer.filter(
25
- ({ consumer }, index) => seriesLayouts[index] === undefined && !isSourceOwnedText(consumer.layer)
25
+ ({ consumer, scalePolicy }, index) =>
26
+ scalePolicy !== false &&
27
+ seriesLayouts[index] === undefined &&
28
+ !isSourceOwnedText(consumer.layer)
26
29
  );
27
30
  const compatibleDirect = directConsumers.every(({ consumer }) =>
28
31
  layoutConsumers.every(({ consumer: layoutConsumer }) => {
@@ -17,6 +17,7 @@ import {
17
17
  resolveScaleRange,
18
18
  resolveSequentialColorStops,
19
19
  resolveShapeRange,
20
+ resolveSizeScale,
20
21
  resolveSizeRange,
21
22
  resolveStrokeDashRange,
22
23
  resolveStrokeWidthRange,
@@ -96,7 +97,8 @@ function resolveRange({
96
97
  isSequentialColor,
97
98
  isDiscretizedColor,
98
99
  isOrdinalOffset,
99
- discretizedScale
100
+ discretizedScale,
101
+ polarFrame
100
102
  }) {
101
103
  if (channel === "color") {
102
104
  if (isDiscretizedColor) return discretizedScale.range;
@@ -114,7 +116,7 @@ function resolveRange({
114
116
  scale,
115
117
  channel,
116
118
  domain,
117
- range: resolveScaleRange(scale.range, channel, bounds),
119
+ range: resolveScaleRange(scale.range, channel, bounds, polarFrame),
118
120
  markConfigs
119
121
  });
120
122
  }
@@ -190,14 +192,16 @@ export function resolveScaleMaterialization(options) {
190
192
  const {
191
193
  id,
192
194
  scale,
193
- channel,
195
+ channel: requestedChannel,
194
196
  consumers,
195
197
  valuesByConsumer,
196
198
  bounds,
197
199
  resolvedScales,
198
200
  markConfigs,
199
- thetaScales
201
+ thetaScales,
202
+ polarFrame
200
203
  } = options;
204
+ const channel = requestedChannel === "stroke" ? "color" : requestedChannel;
201
205
  const allValues = valuesByConsumer
202
206
  .flatMap(item => item.values)
203
207
  .filter(value => value !== undefined);
@@ -218,6 +222,9 @@ export function resolveScaleMaterialization(options) {
218
222
  }))
219
223
  });
220
224
  }
225
+ if (channel === "size") {
226
+ return resolveSizeScale({ ...scale, values: allValues });
227
+ }
221
228
  const isSequentialColor = channel === "color" &&
222
229
  isContinuousColorScaleType(scale.type);
223
230
  const isDiscretizedColor = channel === "color" &&
@@ -234,7 +241,14 @@ export function resolveScaleMaterialization(options) {
234
241
  isDiscretePositionScaleType(scale.type);
235
242
 
236
243
  if (isSequentialColor || isDiscretizedColor) {
237
- for (const consumer of consumers) validateContinuousColorConsumer(consumer.layer, consumer.encoding, scale);
244
+ for (const consumer of consumers) {
245
+ validateContinuousColorConsumer(
246
+ consumer.layer,
247
+ consumer.encoding,
248
+ scale,
249
+ { channel: consumer.channel }
250
+ );
251
+ }
238
252
  }
239
253
  let discretizedScale;
240
254
  if (isDiscretizedColor) {
@@ -290,7 +304,8 @@ export function resolveScaleMaterialization(options) {
290
304
  isSequentialColor,
291
305
  isDiscretizedColor,
292
306
  isOrdinalOffset,
293
- discretizedScale
307
+ discretizedScale,
308
+ polarFrame
294
309
  });
295
310
  validateSequentialMidpoint(scale.midpoint, scale.type, domain);
296
311
  if (scale.midpoint !== undefined && (channel !== "color" || consumers.some(
@@ -313,6 +328,7 @@ export function resolveScaleMaterialization(options) {
313
328
  values: allValues,
314
329
  range: scale.range,
315
330
  ...resolveOffsetScalePolicy({
331
+ scale,
316
332
  consumers,
317
333
  resolvedScales,
318
334
  markConfigs,
@@ -1,7 +1,7 @@
1
1
  import { deriveArcSectors } from "../../../grammar/arcs.js";
2
2
  import { resolvePolarPoint } from "../../../grammar/polarPaths.js";
3
- import { resolvePolarFrame } from "../../../grammar/polar.js";
4
- import { resolveGraphicBounds } from "../../../layout/canvas.js";
3
+ import { readScaleField } from "../../../grammar/scales/index.js";
4
+ import { resolveCoordinatePolarFrame } from "../../coordinateBounds.js";
5
5
  import {
6
6
  concreteProperties,
7
7
  finalizeItems,
@@ -11,7 +11,7 @@ import {
11
11
  export function resolveArcItems(program, layer, dataset) {
12
12
  const thetaScale = program.resolvedScales[layer.encoding?.theta?.scale];
13
13
  const radiusScale = program.resolvedScales[layer.encoding?.radius?.scale];
14
- const frame = resolvePolarFrame(resolveGraphicBounds(program));
14
+ const frame = resolveCoordinatePolarFrame(program, layer.coordinate);
15
15
  const derived = deriveArcSectors(dataset.values, layer, {
16
16
  thetaScale,
17
17
  ...(radiusScale === undefined ? {} : { radiusScale }),
@@ -19,14 +19,25 @@ export function resolveArcItems(program, layer, dataset) {
19
19
  innerRadiusRatio: program.markConfigs[layer.id]?.innerRadius ?? 0
20
20
  });
21
21
  const graphic = program.graphicSpec.objects[layer.id];
22
+ const stroke = layer.encoding?.stroke;
22
23
  const definitions = derived.sectors.map((sector, index) => {
23
24
  const members = sector.sourceIndices.map(index => dataset.values[index]);
25
+ const strokeValues = stroke === undefined ? undefined : readScaleField(
26
+ members,
27
+ stroke.field,
28
+ stroke.fieldType,
29
+ { temporalUnit: stroke.temporalUnit }
30
+ );
31
+ if (strokeValues !== undefined && new Set(strokeValues).size !== 1) {
32
+ throw new Error("Arc stroke requires one value within each sector.");
33
+ }
24
34
  return {
25
35
  fields: uniqueFields(members),
26
36
  channels: {
27
37
  theta: sector.theta,
28
38
  ...(sector.radius === undefined ? {} : { radius: sector.radius }),
29
- ...(sector.color === undefined ? {} : { color: sector.color })
39
+ ...(sector.color === undefined ? {} : { color: sector.color }),
40
+ ...(strokeValues === undefined ? {} : { stroke: strokeValues[0] })
30
41
  },
31
42
  properties: {
32
43
  ...concreteProperties(graphic.items[index]?.properties),
@@ -36,6 +47,19 @@ export function resolveArcItems(program, layer, dataset) {
36
47
  (sector.innerRadius + sector.outerRadius) / 2
37
48
  )
38
49
  },
50
+ geometry: {
51
+ kind: "arc",
52
+ centerX: frame.centerX,
53
+ centerY: frame.centerY,
54
+ startTheta: sector.startTheta +
55
+ Math.sign(sector.endTheta - sector.startTheta) *
56
+ (program.markConfigs[layer.id]?.padAngle ?? 0) / 2,
57
+ endTheta: sector.endTheta -
58
+ Math.sign(sector.endTheta - sector.startTheta) *
59
+ (program.markConfigs[layer.id]?.padAngle ?? 0) / 2,
60
+ innerRadius: sector.innerRadius,
61
+ outerRadius: sector.outerRadius
62
+ },
39
63
  members
40
64
  };
41
65
  });