ggaction 0.0.2 → 0.0.4

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 (242) hide show
  1. package/CHANGELOG.md +55 -2
  2. package/README.md +95 -72
  3. package/package.json +15 -5
  4. package/src/actions/boxPlots/components.js +3 -3
  5. package/src/actions/boxPlots/edit.js +277 -0
  6. package/src/actions/boxPlots/index.js +2 -0
  7. package/src/actions/boxPlots/materialize.js +12 -1
  8. package/src/actions/boxPlots/options.js +25 -14
  9. package/src/actions/canvas/actions.js +21 -17
  10. package/src/actions/composition/actions.js +247 -0
  11. package/src/actions/composition/index.js +1 -0
  12. package/src/actions/coordinates/actions.js +2 -5
  13. package/src/actions/data/density.js +7 -1
  14. package/src/actions/data/filter.js +3 -1
  15. package/src/actions/data/regression.js +1 -1
  16. package/src/actions/encodings/appearance.js +12 -1
  17. package/src/actions/encodings/barWidth.js +5 -3
  18. package/src/actions/encodings/color/continuous.js +137 -0
  19. package/src/actions/encodings/color/index.js +151 -0
  20. package/src/actions/encodings/color/layout.js +91 -0
  21. package/src/actions/encodings/color/policy.js +88 -0
  22. package/src/actions/encodings/index.js +4 -2
  23. package/src/actions/encodings/offset.js +91 -73
  24. package/src/actions/encodings/position/apply.js +66 -0
  25. package/src/actions/encodings/position/index.js +130 -0
  26. package/src/actions/encodings/position/policies/arc.js +26 -0
  27. package/src/actions/encodings/position/policies/bar.js +9 -5
  28. package/src/actions/encodings/position/policies/index.js +5 -1
  29. package/src/actions/encodings/position/policies/line.js +20 -3
  30. package/src/actions/encodings/position/resolve.js +58 -9
  31. package/src/actions/encodings/ranged.js +30 -8
  32. package/src/actions/encodings/shared.js +17 -21
  33. package/src/actions/encodings/strokeDash.js +1 -1
  34. package/src/actions/encodings/text.js +73 -0
  35. package/src/actions/errorBands/create.js +37 -29
  36. package/src/actions/errorBands/edit.js +206 -0
  37. package/src/actions/errorBands/index.js +9 -0
  38. package/src/actions/errorBands/options.js +39 -0
  39. package/src/actions/errorBars/create.js +50 -43
  40. package/src/actions/errorBars/edit.js +130 -0
  41. package/src/actions/errorBars/index.js +3 -0
  42. package/src/actions/errorBars/options.js +52 -0
  43. package/src/actions/facets/actions.js +162 -0
  44. package/src/actions/facets/derive.js +200 -0
  45. package/src/actions/facets/guides.js +16 -0
  46. package/src/actions/facets/index.js +1 -0
  47. package/src/actions/facets/replay.js +69 -0
  48. package/src/actions/guides/applicability.js +113 -0
  49. package/src/actions/guides/axes/axes.js +70 -16
  50. package/src/actions/guides/axes/edit.js +126 -0
  51. package/src/actions/guides/axes/index.js +12 -0
  52. package/src/actions/guides/axes/labels.js +22 -19
  53. package/src/actions/guides/axes/remove.js +60 -0
  54. package/src/actions/guides/axes/tickGroups.js +7 -5
  55. package/src/actions/guides/axes/ticks.js +10 -5
  56. package/src/actions/guides/axes/titles.js +8 -6
  57. package/src/actions/guides/grids/grid.js +144 -15
  58. package/src/actions/guides/grids/resolve.js +6 -14
  59. package/src/actions/guides/guides.js +17 -75
  60. package/src/actions/guides/index.js +6 -0
  61. package/src/actions/guides/legends/categorical/actions.js +48 -9
  62. package/src/actions/guides/legends/categorical/components.js +16 -16
  63. package/src/actions/guides/legends/categorical/index.js +14 -14
  64. package/src/actions/guides/legends/categorical/layout.js +7 -3
  65. package/src/actions/guides/legends/categorical/options.js +2 -5
  66. package/src/actions/guides/legends/categorical/resolve.js +2 -2
  67. package/src/actions/guides/legends/categorical/symbols.js +14 -14
  68. package/src/actions/guides/legends/continuous/common.js +7 -7
  69. package/src/actions/guides/legends/continuous/interval.js +6 -6
  70. package/src/actions/guides/legends/continuous/opacity.js +1 -1
  71. package/src/actions/guides/legends/edit.js +4 -26
  72. package/src/actions/guides/legends/focused.js +63 -0
  73. package/src/actions/guides/legends/index.js +4 -0
  74. package/src/actions/guides/legends/remove.js +46 -0
  75. package/src/actions/guides/legends/size.js +13 -15
  76. package/src/actions/guides/legends/target.js +25 -0
  77. package/src/actions/guides/polar/axes/facade.js +219 -0
  78. package/src/actions/guides/polar/axes/index.js +55 -0
  79. package/src/actions/guides/polar/axes/labels.js +188 -0
  80. package/src/actions/guides/polar/axes/lines.js +113 -0
  81. package/src/actions/guides/polar/axes/shared.js +112 -0
  82. package/src/actions/guides/polar/axes/ticks.js +164 -0
  83. package/src/actions/guides/polar/axes/titles.js +165 -0
  84. package/src/actions/guides/polar/grids.js +252 -0
  85. package/src/actions/guides/polar/index.js +2 -0
  86. package/src/actions/guides/polar/resolve.js +269 -0
  87. package/src/actions/guides/tickValues.js +1 -1
  88. package/src/actions/index.js +4 -0
  89. package/src/actions/intervals/resolve.js +1 -1
  90. package/src/actions/marks/arc/actions.js +239 -0
  91. package/src/actions/marks/arc/index.js +1 -0
  92. package/src/actions/marks/{area.js → area/actions.js} +51 -38
  93. package/src/actions/marks/area/index.js +4 -0
  94. package/src/actions/marks/bar/create.js +26 -5
  95. package/src/actions/marks/bar/edit.js +1 -1
  96. package/src/actions/marks/bar/materialize.js +25 -31
  97. package/src/actions/marks/index.js +12 -4
  98. package/src/actions/marks/lifecycle.js +27 -0
  99. package/src/actions/marks/line/actions.js +373 -0
  100. package/src/actions/marks/line/index.js +1 -0
  101. package/src/actions/marks/{point.js → point/actions.js} +67 -108
  102. package/src/actions/marks/point/index.js +1 -0
  103. package/src/actions/marks/rect/actions.js +165 -0
  104. package/src/actions/marks/rect/index.js +1 -0
  105. package/src/actions/marks/remove.js +214 -0
  106. package/src/actions/marks/{rule.js → rule/actions.js} +34 -28
  107. package/src/actions/marks/rule/index.js +1 -0
  108. package/src/actions/marks/shared.js +156 -25
  109. package/src/actions/marks/text/actions.js +192 -0
  110. package/src/actions/marks/text/index.js +1 -0
  111. package/src/actions/primitives/createGraphics.js +13 -6
  112. package/src/actions/primitives/editGraphics.js +10 -3
  113. package/src/actions/primitives/semantic.js +26 -3
  114. package/src/actions/primitives/semanticValue.js +26 -4
  115. package/src/actions/regression/components.js +2 -2
  116. package/src/actions/regression/create.js +19 -2
  117. package/src/actions/regression/edit.js +247 -0
  118. package/src/actions/regression/index.js +3 -0
  119. package/src/actions/regression/resolve.js +3 -1
  120. package/src/actions/scales/consumers/common.js +91 -0
  121. package/src/actions/scales/consumers/families.js +47 -0
  122. package/src/actions/scales/consumers/index.js +41 -0
  123. package/src/actions/scales/{consumers.js → consumers/seriesLayout.js} +14 -127
  124. package/src/actions/scales/create.js +1 -1
  125. package/src/actions/scales/definitions.js +15 -6
  126. package/src/actions/scales/edit.js +45 -22
  127. package/src/actions/scales/materialize.js +58 -409
  128. package/src/actions/selection/actions.js +38 -24
  129. package/src/actions/selection/index.js +2 -0
  130. package/src/actions/titles/actions.js +63 -2
  131. package/src/actions/titles/index.js +2 -0
  132. package/src/actions/titles/resolve.js +12 -17
  133. package/src/composition.js +10 -0
  134. package/src/core/ChartProgram.js +49 -65
  135. package/src/core/action.js +10 -0
  136. package/src/core/compositionState.js +169 -0
  137. package/src/core/materializationState.js +65 -0
  138. package/src/core/programState.js +28 -0
  139. package/src/core/validation.js +16 -0
  140. package/src/core/vocabulary.js +154 -4
  141. package/src/grammar/arcs.js +154 -0
  142. package/src/grammar/areaSeries.js +1 -1
  143. package/src/grammar/bars/aggregate.js +3 -3
  144. package/src/grammar/bars/geometry.js +3 -3
  145. package/src/grammar/bars/policy.js +7 -1
  146. package/src/grammar/coordinates.js +3 -7
  147. package/src/grammar/density.js +21 -2
  148. package/src/grammar/facets/dependencies.js +185 -0
  149. package/src/grammar/facets/guides.js +257 -0
  150. package/src/grammar/facets/index.js +122 -0
  151. package/src/grammar/facets/scales.js +254 -0
  152. package/src/grammar/interval.js +2 -96
  153. package/src/grammar/lineSeries.js +112 -4
  154. package/src/grammar/palettes.js +5 -7
  155. package/src/grammar/polar.js +102 -0
  156. package/src/grammar/polarGuides.js +243 -0
  157. package/src/grammar/polarLineCommands.js +43 -0
  158. package/src/grammar/polarPaths.js +161 -0
  159. package/src/grammar/positionCompatibility.js +18 -2
  160. package/src/grammar/rects.js +26 -0
  161. package/src/grammar/regression/derive.js +97 -0
  162. package/src/grammar/regression/index.js +22 -0
  163. package/src/grammar/regression/models.js +282 -0
  164. package/src/grammar/regression/parameters.js +106 -0
  165. package/src/grammar/rules.js +1 -1
  166. package/src/grammar/scales/color.js +4 -2
  167. package/src/grammar/scales/continuous.js +6 -1
  168. package/src/grammar/scales/fields.js +7 -0
  169. package/src/grammar/scales/ordinal.js +5 -2
  170. package/src/grammar/scales/policies.js +1 -1
  171. package/src/grammar/schemas/graphic.js +1 -1
  172. package/src/grammar/schemas/graphicBounds.js +64 -9
  173. package/src/grammar/schemas/graphicTree.js +44 -10
  174. package/src/grammar/schemas/semanticPath.js +31 -9
  175. package/src/grammar/seriesLayout.js +13 -2
  176. package/src/grammar/statistics/studentT.js +98 -0
  177. package/src/grammar/text.js +71 -0
  178. package/src/grammar/ticks.js +50 -13
  179. package/src/grammar/transforms.js +1 -1
  180. package/src/index.js +1 -0
  181. package/src/layout/composition.js +310 -0
  182. package/src/layout/facets.js +131 -0
  183. package/src/layout/title.js +6 -0
  184. package/src/materialization/bars/aggregate.js +6 -3
  185. package/src/materialization/bars/grouped.js +75 -37
  186. package/src/materialization/bars/histogram.js +1 -1
  187. package/src/materialization/bars/ranged.js +1 -1
  188. package/src/materialization/composition.js +152 -0
  189. package/src/materialization/compositionSnapshot.js +114 -0
  190. package/src/materialization/density.js +1 -1
  191. package/src/materialization/dependencies.js +47 -73
  192. package/src/materialization/encodings.js +14 -28
  193. package/src/materialization/facetGuides/index.js +2 -0
  194. package/src/materialization/facetGuides/legacyCategorical.js +125 -0
  195. package/src/materialization/facetGuides/placement.js +126 -0
  196. package/src/materialization/facetGuides/preparation.js +188 -0
  197. package/src/materialization/facets.js +214 -0
  198. package/src/materialization/graphicHierarchy.js +15 -5
  199. package/src/materialization/guides/resources.js +46 -0
  200. package/src/materialization/marks/capabilities.js +141 -0
  201. package/src/materialization/marks/index.js +105 -0
  202. package/src/materialization/marks/policies.js +87 -0
  203. package/src/materialization/planner.js +105 -12
  204. package/src/materialization/rect.js +138 -0
  205. package/src/materialization/rectConfig.js +38 -0
  206. package/src/materialization/rowEncoding.js +33 -0
  207. package/src/materialization/scaleGuideDependencies.js +94 -0
  208. package/src/materialization/scales/map.js +40 -0
  209. package/src/materialization/scales/policies/arc.js +37 -0
  210. package/src/materialization/scales/policies/bar.js +132 -0
  211. package/src/materialization/scales/policies/series.js +76 -0
  212. package/src/materialization/scales/resolve.js +326 -0
  213. package/src/materialization/selection/items/arc.js +29 -0
  214. package/src/materialization/selection/items/bar.js +255 -0
  215. package/src/materialization/selection/items/common.js +131 -0
  216. package/src/materialization/selection/items/index.js +6 -0
  217. package/src/materialization/selection/items/path.js +59 -0
  218. package/src/materialization/selection/items/point.js +55 -0
  219. package/src/materialization/selection/items/rect.js +23 -0
  220. package/src/materialization/selection/items/rule.js +25 -0
  221. package/src/materialization/selection/policies/arc.js +10 -0
  222. package/src/materialization/selection/policies/area.js +1 -1
  223. package/src/materialization/selection/policies/bar.js +1 -1
  224. package/src/materialization/selection/policies/index.js +5 -1
  225. package/src/materialization/selection/policies/line.js +1 -1
  226. package/src/materialization/selection/policies/point.js +1 -1
  227. package/src/materialization/selection/policies/rect.js +10 -0
  228. package/src/materialization/selection/policies/rule.js +1 -1
  229. package/src/materialization/selection/styles.js +25 -17
  230. package/src/materialization/text.js +156 -0
  231. package/src/renderers/canvas/index.js +68 -22
  232. package/src/renderers/canvas/text.js +14 -1
  233. package/types/extension.d.ts +5 -1
  234. package/types/index.d.ts +56 -1
  235. package/types/program.d.ts +664 -27
  236. package/src/actions/encodings/color.js +0 -329
  237. package/src/actions/encodings/position.js +0 -176
  238. package/src/actions/marks/line.js +0 -248
  239. package/src/grammar/regression.js +0 -578
  240. package/src/grammar/scales.js +0 -1
  241. package/src/materialization/marks.js +0 -110
  242. package/src/materialization/selection/items.js +0 -503
@@ -1,18 +1,32 @@
1
1
  import { deriveBarAggregates } from "../../grammar/bars/aggregate.js";
2
- import { mapContinuousScaleValues } from "../../grammar/scales.js";
2
+ import {
3
+ mapContinuousScaleValues,
4
+ mapOrdinalPositionValues
5
+ } from "../../grammar/scales/index.js";
3
6
  import { sameOrderedValues } from "../../core/validation.js";
4
7
  import {
5
8
  DEFAULT_BAR_STROKE,
6
9
  DEFAULT_BAR_STROKE_WIDTH
7
10
  } from "./resolve.js";
8
11
  import { resolveBarWidth } from "../../grammar/bars/geometry.js";
12
+ import { DEFAULT_SERIES_BASELINE } from "../../grammar/seriesLayout.js";
13
+ import {
14
+ BAR_ORIENTATIONS,
15
+ resolveBarChannels,
16
+ resolveBarOffsetChannel
17
+ } from "../../grammar/bars/policy.js";
9
18
 
10
19
  export function deriveGroupedRectangles(required, resolved, widthConfig) {
11
20
  const { dataset, layer } = required;
12
21
  const xScale = resolved.resolvedScales[required.xEncoding.scale];
13
22
  const yScale = resolved.resolvedScales[required.yEncoding.scale];
23
+ const channels = resolveBarChannels(layer);
24
+ const vertical = channels.orientation === BAR_ORIENTATIONS.vertical;
25
+ const categoryScale = vertical ? xScale : yScale;
26
+ const measureScale = vertical ? yScale : xScale;
27
+ const offsetChannel = resolveBarOffsetChannel(layer);
14
28
  const colorEncoding = layer.encoding?.color;
15
- const offsetEncoding = layer.encoding?.xOffset;
29
+ const offsetEncoding = layer.encoding?.[offsetChannel];
16
30
  const colorScale = resolved.resolvedScales[colorEncoding?.scale];
17
31
  const offsetScale = resolved.resolvedScales[offsetEncoding?.scale];
18
32
 
@@ -23,16 +37,18 @@ export function deriveGroupedRectangles(required, resolved, widthConfig) {
23
37
  offsetScale === undefined
24
38
  ) {
25
39
  throw new Error(
26
- `Grouped bar mark "${layer.id}" requires matching color and xOffset scales.`
40
+ `Grouped bar mark "${layer.id}" requires matching color and ${offsetChannel} scales.`
27
41
  );
28
42
  }
29
43
  if (!sameOrderedValues(colorScale.domain, offsetScale.domain)) {
30
44
  throw new Error(
31
- `Grouped bar mark "${layer.id}" requires matching color and xOffset domains.`
45
+ `Grouped bar mark "${layer.id}" requires matching color and ${offsetChannel} domains.`
32
46
  );
33
47
  }
34
48
 
35
- const xIndex = new Map(xScale.domain.map((value, index) => [value, index]));
49
+ const categoryIndex = new Map(
50
+ categoryScale.domain.map((value, index) => [value, index])
51
+ );
36
52
  const offsetIndex = new Map(
37
53
  offsetScale.domain.map((value, index) => [value, index])
38
54
  );
@@ -43,18 +59,21 @@ export function deriveGroupedRectangles(required, resolved, widthConfig) {
43
59
  ])
44
60
  );
45
61
  const offsetMidpoint = (offsetScale.range[0] + offsetScale.range[1]) / 2;
46
- const direction = Math.sign(xScale.step) || 1;
62
+ const categoryDirection = Math.sign(
63
+ categoryScale.step ?? categoryScale.range[1] - categoryScale.range[0]
64
+ ) || 1;
47
65
  const offsetDirection = Math.sign(offsetScale.step) || 1;
48
66
  const width = resolveBarWidth(widthConfig, offsetScale.bandwidth);
49
67
  const baseline = mapContinuousScaleValues(
50
- [yScale.domain[0]],
51
- yScale
68
+ [DEFAULT_SERIES_BASELINE],
69
+ measureScale
52
70
  )[0];
53
71
  const cells = [...deriveBarAggregates(dataset.values, layer).values].sort(
54
72
  (left, right) =>
55
- (xScale.type === "time"
56
- ? left.x - right.x
57
- : xIndex.get(left.x) - xIndex.get(right.x)) ||
73
+ (categoryScale.type === "time"
74
+ ? left[channels.category] - right[channels.category]
75
+ : categoryIndex.get(left[channels.category]) -
76
+ categoryIndex.get(right[channels.category])) ||
58
77
  offsetIndex.get(left.color) - offsetIndex.get(right.color)
59
78
  );
60
79
  const existing = resolved.graphicSpec.objects[layer.id].items;
@@ -62,41 +81,60 @@ export function deriveGroupedRectangles(required, resolved, widthConfig) {
62
81
  const appearance = config.barAppearance ?? {};
63
82
 
64
83
  return cells.map((cell, index) => {
65
- const category = xIndex.get(cell.x);
84
+ const categoryValue = cell[channels.category];
85
+ const category = categoryIndex.get(categoryValue);
66
86
  const offset = offsetIndex.get(cell.color);
67
- if ((xScale.type !== "time" && category === undefined) || offset === undefined) {
87
+ if ((categoryScale.type !== "time" && category === undefined) || offset === undefined) {
68
88
  throw new Error("Grouped bar value is outside a resolved ordinal domain.");
69
89
  }
70
90
 
71
- const temporal = xScale.type === "time";
72
- const categoryStart = temporal
73
- ? mapContinuousScaleValues(
74
- [cell.x],
75
- xScale
76
- )[0] - xScale.bandwidth / 2
77
- : (xScale.start ?? xScale.range[0]) + category * xScale.step;
78
91
  const offsetCenter = offsetScale.start + offset * offsetScale.step +
79
92
  offsetDirection * offsetScale.bandwidth / 2;
80
- const center = temporal
81
- ? categoryStart + offsetCenter
82
- : xScale.step > 0 && offsetScale.step > 0
83
- ? categoryStart + offsetCenter
84
- : categoryStart + xScale.step / 2 +
85
- direction * (offsetCenter - offsetMidpoint);
86
- const x = temporal || (xScale.step > 0 && offsetScale.step > 0)
87
- ? categoryStart + offsetScale.start + offset * offsetScale.step +
88
- (offsetScale.bandwidth - width) / 2
89
- : center - width / 2;
90
- const valueY = mapContinuousScaleValues(
91
- [cell.y],
92
- yScale
93
+ const valuePosition = mapContinuousScaleValues(
94
+ [cell[channels.measure]],
95
+ measureScale
93
96
  )[0];
97
+ let geometry;
98
+ if (vertical) {
99
+ // Preserve the established vertical arithmetic order exactly. Primitive
100
+ // baselines intentionally compare concrete floating-point coordinates.
101
+ const temporal = xScale.type === "time";
102
+ const categoryStart = temporal
103
+ ? mapContinuousScaleValues([cell.x], xScale)[0] - xScale.bandwidth / 2
104
+ : (xScale.start ?? xScale.range[0]) + category * xScale.step;
105
+ const center = temporal
106
+ ? categoryStart + offsetCenter
107
+ : xScale.step > 0 && offsetScale.step > 0
108
+ ? categoryStart + offsetCenter
109
+ : categoryStart + xScale.step / 2 +
110
+ categoryDirection * (offsetCenter - offsetMidpoint);
111
+ const x = temporal || (xScale.step > 0 && offsetScale.step > 0)
112
+ ? categoryStart + offsetScale.start + offset * offsetScale.step +
113
+ (offsetScale.bandwidth - width) / 2
114
+ : center - width / 2;
115
+ geometry = {
116
+ x,
117
+ y: Math.min(valuePosition, baseline),
118
+ width,
119
+ height: Math.abs(baseline - valuePosition)
120
+ };
121
+ } else {
122
+ const categoryCenter = mapOrdinalPositionValues(
123
+ [categoryValue],
124
+ categoryScale
125
+ )[0];
126
+ const center = categoryCenter + categoryDirection *
127
+ (offsetCenter - offsetMidpoint);
128
+ geometry = {
129
+ x: Math.min(valuePosition, baseline),
130
+ y: center - width / 2,
131
+ width: Math.abs(baseline - valuePosition),
132
+ height: width
133
+ };
134
+ }
94
135
 
95
136
  return {
96
- x,
97
- y: Math.min(valueY, baseline),
98
- width,
99
- height: Math.abs(baseline - valueY),
137
+ ...geometry,
100
138
  fill: colors.get(cell.color),
101
139
  stroke: appearance.stroke === false
102
140
  ? "transparent"
@@ -8,7 +8,7 @@ import {
8
8
  mapOrdinalValues,
9
9
  readNominalField,
10
10
  readQuantitativeField
11
- } from "../../grammar/scales.js";
11
+ } from "../../grammar/scales/index.js";
12
12
  import { resolveBarColorLayout } from "../../grammar/bars/policy.js";
13
13
  import { layoutSeriesPartition } from "../../grammar/seriesLayout.js";
14
14
  import {
@@ -1,4 +1,4 @@
1
- import { mapContinuousScaleValues, mapOrdinalPositionValues, mapOrdinalValues } from "../../grammar/scales.js";
1
+ import { mapContinuousScaleValues, mapOrdinalPositionValues, mapOrdinalValues } from "../../grammar/scales/index.js";
2
2
  import { resolveBarWidth } from "../../grammar/bars/geometry.js";
3
3
  import { DEFAULT_BAR_FILL, DEFAULT_BAR_STROKE, DEFAULT_BAR_STROKE_WIDTH } from "./resolve.js";
4
4
 
@@ -0,0 +1,152 @@
1
+ import { freezeOwned } from "../core/immutable.js";
2
+ import {
3
+ requireSingleOrderedGraphicByType
4
+ } from "../grammar/schemas/graphicTree.js";
5
+ import {
6
+ resolveCompositionLayout,
7
+ resolveCompositionSnapshotPlacement
8
+ } from "../layout/composition.js";
9
+ import { namespaceGraphicSnapshot } from "./compositionSnapshot.js";
10
+
11
+ const ZERO_MARGIN = freezeOwned({ top: 0, right: 0, bottom: 0, left: 0 });
12
+
13
+ export function requireChildCanvas(program, id) {
14
+ const { object } = requireSingleOrderedGraphicByType(program.graphicSpec, "canvas");
15
+ const { width, height } = object.properties ?? {};
16
+ if (!Number.isFinite(width) || width <= 0 || !Number.isFinite(height) || height <= 0) {
17
+ throw new Error(`Composition child "${id}" requires a complete positive Canvas size.`);
18
+ }
19
+ return object;
20
+ }
21
+
22
+ export function compositionChildDescriptor(id, program) {
23
+ const canvas = requireChildCanvas(program, id);
24
+ const size = program.materializationConfigs.canvas?.size ?? {
25
+ width: "explicit",
26
+ height: "explicit"
27
+ };
28
+ return {
29
+ id,
30
+ width: canvas.properties.width,
31
+ height: canvas.properties.height,
32
+ widthMode: size.width,
33
+ heightMode: size.height
34
+ };
35
+ }
36
+
37
+ export function resolveCompositionProgramLayout(program) {
38
+ program._assertCompositionProgram("resolveCompositionProgramLayout");
39
+ const spec = program.compositionSpec;
40
+ return resolveCompositionLayout({
41
+ direction: spec.direction,
42
+ children: spec.children.map(id => compositionChildDescriptor(id, program.children[id])),
43
+ gap: spec.gap,
44
+ align: spec.align,
45
+ padding: spec.padding
46
+ });
47
+ }
48
+
49
+ function resizeChildForSnapshot(program, placement) {
50
+ const canvas = requireSingleOrderedGraphicByType(program.graphicSpec, "canvas");
51
+ const widthChanged = canvas.object.properties.width !== placement.width;
52
+ const heightChanged = canvas.object.properties.height !== placement.height;
53
+ if (!widthChanged && !heightChanged) return program;
54
+
55
+ if (program.compositionSpec !== undefined) {
56
+ return program;
57
+ }
58
+ if (typeof program.editCanvas !== "function") {
59
+ throw new Error("Automatic composition sizing requires child editCanvas support.");
60
+ }
61
+ return program.editCanvas({
62
+ ...(widthChanged ? { width: placement.width } : {}),
63
+ ...(heightChanged ? { height: placement.height } : {})
64
+ });
65
+ }
66
+
67
+ function resolveSnapshotPlacement(program, placement, layout) {
68
+ if (program.compositionSpec === undefined) return placement;
69
+ const canvas = requireSingleOrderedGraphicByType(program.graphicSpec, "canvas");
70
+ return resolveCompositionSnapshotPlacement({
71
+ direction: layout.direction,
72
+ align: layout.align,
73
+ placement,
74
+ width: canvas.object.properties.width,
75
+ height: canvas.object.properties.height
76
+ });
77
+ }
78
+
79
+ function itemDefinitions(object) {
80
+ return object.items.map(item => ({
81
+ type: item.type ?? object.type,
82
+ properties: item.properties
83
+ }));
84
+ }
85
+
86
+ export function attachSnapshotObject(program, snapshot, id, parent) {
87
+ const object = snapshot.objects[id];
88
+ let next = program.createGraphics({
89
+ id,
90
+ type: object.type,
91
+ parent,
92
+ ...(object.items !== undefined && object.type !== "collection"
93
+ ? { length: object.items.length }
94
+ : {})
95
+ });
96
+ for (const [property, value] of Object.entries(object.properties ?? {})) {
97
+ next = next.editGraphics({ target: id, property, value });
98
+ }
99
+ if (object.items !== undefined) {
100
+ next = next.editGraphics({
101
+ target: id,
102
+ property: "items",
103
+ value: itemDefinitions(object)
104
+ });
105
+ }
106
+ for (const childId of object.children ?? []) {
107
+ next = attachSnapshotObject(next, snapshot, childId, id);
108
+ }
109
+ return next;
110
+ }
111
+
112
+ export function clearCompositionChildren(program) {
113
+ const canvas = program.graphicSpec.objects.canvas;
114
+ let next = program;
115
+ for (const id of canvas?.children ?? []) {
116
+ next = next.editGraphics({ target: id, remove: true });
117
+ }
118
+ return next;
119
+ }
120
+
121
+ export function materializeCompositionGraphics(program) {
122
+ program._assertCompositionProgram("materializeCompositionGraphics");
123
+ const layout = resolveCompositionProgramLayout(program);
124
+ let next = clearCompositionChildren(program);
125
+ if (next.graphicSpec.objects.canvas === undefined) {
126
+ next = next.createGraphics({ id: "canvas", type: "canvas" });
127
+ }
128
+ for (const [property, value] of Object.entries({
129
+ width: layout.width,
130
+ height: layout.height,
131
+ background: "white"
132
+ })) {
133
+ next = next.editGraphics({ target: "canvas", property, value });
134
+ }
135
+
136
+ for (const placement of layout.children) {
137
+ const source = program.children[placement.id];
138
+ const child = resizeChildForSnapshot(source, placement);
139
+ const snapshotPlacement = resolveSnapshotPlacement(child, placement, layout);
140
+ const snapshot = namespaceGraphicSnapshot(child.graphicSpec, {
141
+ namespace: `${program.compositionSpec.id}-${placement.id}`,
142
+ x: snapshotPlacement.x,
143
+ y: snapshotPlacement.y
144
+ });
145
+ next = attachSnapshotObject(next, snapshot, snapshot.order[0], "canvas");
146
+ }
147
+
148
+ return next._withCanvasConfig({
149
+ margin: ZERO_MARGIN,
150
+ size: { width: "auto", height: "auto" }
151
+ });
152
+ }
@@ -0,0 +1,114 @@
1
+ import { cloneAndFreeze, freezeOwned, isPlainObject } from "../core/immutable.js";
2
+ import {
3
+ requireSingleOrderedGraphicByType,
4
+ walkGraphicTreeEvents
5
+ } from "../grammar/schemas/graphicTree.js";
6
+
7
+ function validateNamespace(namespace) {
8
+ if (typeof namespace !== "string" || namespace.length === 0) {
9
+ throw new TypeError("Graphic snapshot namespace must be a non-empty string.");
10
+ }
11
+ return namespace;
12
+ }
13
+
14
+ function validatePlacement(value, property) {
15
+ if (!Number.isFinite(value)) {
16
+ throw new TypeError(`Graphic snapshot ${property} must be a finite number.`);
17
+ }
18
+ return value;
19
+ }
20
+
21
+ function validateCanvas(canvas, id) {
22
+ const { width, height } = canvas.properties ?? {};
23
+ if (!Number.isFinite(width) || width <= 0) {
24
+ throw new RangeError(`Child Canvas "${id}" width must be a positive finite number.`);
25
+ }
26
+ if (!Number.isFinite(height) || height <= 0) {
27
+ throw new RangeError(`Child Canvas "${id}" height must be a positive finite number.`);
28
+ }
29
+ }
30
+
31
+ export function namespaceGraphicId(namespace, id) {
32
+ const encode = value => Array.from(value, character =>
33
+ character.codePointAt(0).toString(16).padStart(6, "0")
34
+ ).join("");
35
+ return `g${encode(namespace)}_${encode(id)}`;
36
+ }
37
+
38
+ function rewriteItem(item, namespace) {
39
+ if (!isPlainObject(item) || typeof item.id !== "string" || item.id.length === 0) {
40
+ throw new TypeError("Graphic snapshot items require non-empty string ids.");
41
+ }
42
+ return freezeOwned({
43
+ ...cloneAndFreeze(item),
44
+ id: namespaceGraphicId(namespace, item.id)
45
+ });
46
+ }
47
+
48
+ function rewriteObject(object, namespace, rootCanvasId, placement, extraRoots) {
49
+ const children = object.children ?? [];
50
+ const rewritten = {
51
+ ...cloneAndFreeze(object),
52
+ ...(object.items === undefined
53
+ ? {}
54
+ : { items: freezeOwned(object.items.map(item => rewriteItem(item, namespace))) }),
55
+ ...(children.length === 0 && rootCanvasId === undefined
56
+ ? {}
57
+ : {
58
+ children: freezeOwned([
59
+ ...children.map(id => namespaceGraphicId(namespace, id)),
60
+ ...(rootCanvasId === undefined
61
+ ? []
62
+ : extraRoots.map(id => namespaceGraphicId(namespace, id)))
63
+ ])
64
+ })
65
+ };
66
+ if (rootCanvasId !== undefined) {
67
+ rewritten.properties = freezeOwned({
68
+ ...rewritten.properties,
69
+ x: placement.x,
70
+ y: placement.y
71
+ });
72
+ }
73
+ return freezeOwned(rewritten);
74
+ }
75
+
76
+ export function namespaceGraphicSnapshot(
77
+ graphicSpec,
78
+ { namespace, x = 0, y = 0 } = {}
79
+ ) {
80
+ const resolvedNamespace = validateNamespace(namespace);
81
+ const placement = {
82
+ x: validatePlacement(x, "x"),
83
+ y: validatePlacement(y, "y")
84
+ };
85
+ if (
86
+ !isPlainObject(graphicSpec) ||
87
+ !isPlainObject(graphicSpec.objects) ||
88
+ !Array.isArray(graphicSpec.order)
89
+ ) {
90
+ throw new TypeError("Graphic snapshot requires objects and order.");
91
+ }
92
+ walkGraphicTreeEvents(graphicSpec, {});
93
+ const { id: canvasId, object: canvas } =
94
+ requireSingleOrderedGraphicByType(graphicSpec, "canvas");
95
+ validateCanvas(canvas, canvasId);
96
+ const extraRoots = graphicSpec.order.filter(id => id !== canvasId);
97
+ const objects = Object.fromEntries(Object.entries(graphicSpec.objects).map(
98
+ ([id, object]) => [
99
+ namespaceGraphicId(resolvedNamespace, id),
100
+ rewriteObject(
101
+ object,
102
+ resolvedNamespace,
103
+ id === canvasId ? canvasId : undefined,
104
+ placement,
105
+ id === canvasId ? extraRoots : []
106
+ )
107
+ ]
108
+ ));
109
+ const rootId = namespaceGraphicId(resolvedNamespace, canvasId);
110
+ return freezeOwned({
111
+ objects: freezeOwned(objects),
112
+ order: freezeOwned([rootId])
113
+ });
114
+ }
@@ -1,4 +1,4 @@
1
- import { getMarkMaterializationStep } from "./marks.js";
1
+ import { getMarkMaterializationStep } from "./marks/index.js";
2
2
  import { requireLayer } from "../selectors/layers.js";
3
3
  import { buildMaterializationPlan } from "./planner.js";
4
4
 
@@ -1,33 +1,19 @@
1
- import { getMarkMaterializationStep } from "./marks.js";
1
+ import {
2
+ canDeferScaleConsumerApplication,
3
+ getExistingMarkRematerializationStep,
4
+ getMarkMaterializationStep,
5
+ getSourceDependentMarkSteps
6
+ } from "./marks/index.js";
2
7
  import { requireLayer } from "../selectors/layers.js";
3
8
  import {
4
9
  applyMaterializationPlan,
5
10
  buildMaterializationPlan
6
11
  } from "./planner.js";
12
+ import { hasMaterializedLegend } from "./legends.js";
7
13
  import {
8
- hasMaterializedLegend,
9
- materializedLegendUsesScale
10
- } from "./legends.js";
11
-
12
- function usesPositionalScale(program, id) {
13
- return program.semanticSpec.layers.some(layer =>
14
- ["x", "y", "x2", "y2", "xOffset"].some(
15
- channel => layer.encoding?.[channel]?.scale === id
16
- )
17
- );
18
- }
19
-
20
- function needsCanvasScaleRematerialization(program, scale) {
21
- return (
22
- (scale.range === "auto" ||
23
- program.semanticSpec.guides.axis?.x?.scale === scale.id ||
24
- program.semanticSpec.guides.axis?.y?.scale === scale.id ||
25
- program.semanticSpec.guides.grid?.horizontal?.scale === scale.id ||
26
- program.semanticSpec.guides.grid?.vertical?.scale === scale.id) &&
27
- program.resolvedScales[scale.id] !== undefined &&
28
- usesPositionalScale(program, scale.id)
29
- );
30
- }
14
+ needsCanvasScaleRematerialization,
15
+ planScaleGuideRematerialization
16
+ } from "./scaleGuideDependencies.js";
31
17
 
32
18
  function hasTitle(program) {
33
19
  return (
@@ -37,20 +23,36 @@ function hasTitle(program) {
37
23
  }
38
24
 
39
25
  export function planCanvasRematerialization(program) {
26
+ const marks = [];
27
+ for (const layer of program.semanticSpec.layers) {
28
+ const step = getMarkMaterializationStep(program, layer);
29
+ if (step !== undefined) marks.push(step);
30
+ }
31
+ const deferredMarkIds = new Set(
32
+ marks
33
+ .map(step => step.args.id)
34
+ );
40
35
  const scales = [];
41
36
  for (const scale of program.semanticSpec.scales) {
42
37
  if (needsCanvasScaleRematerialization(program, scale)) {
38
+ const deferredConsumers = program.semanticSpec.layers.filter(layer =>
39
+ canDeferScaleConsumerApplication(layer) &&
40
+ Object.values(layer.encoding ?? {}).some(
41
+ encoding => encoding?.scale === scale.id
42
+ )
43
+ );
44
+ const canDeferMarks = deferredConsumers.length === 0 ||
45
+ deferredConsumers.every(layer => deferredMarkIds.has(layer.id));
43
46
  scales.push({
44
47
  op: "rematerializeScale",
45
- args: { id: scale.id, guides: false }
48
+ args: {
49
+ id: scale.id,
50
+ guides: false,
51
+ ...(canDeferMarks ? { marks: false } : {})
52
+ }
46
53
  });
47
54
  }
48
55
  }
49
- const marks = [];
50
- for (const layer of program.semanticSpec.layers) {
51
- const step = getMarkMaterializationStep(program, layer);
52
- if (step !== undefined) marks.push(step);
53
- }
54
56
  const guides = program.semanticSpec.scales.flatMap(scale =>
55
57
  needsCanvasScaleRematerialization(program, scale)
56
58
  ? planScaleGuideRematerialization(program, scale.id)
@@ -63,42 +65,6 @@ export function planCanvasRematerialization(program) {
63
65
  return buildMaterializationPlan({ scales, marks, guides, layout });
64
66
  }
65
67
 
66
- export function planScaleGuideRematerialization(program, id) {
67
- const guides = [];
68
- const objects = program.graphicSpec?.objects ?? {};
69
- const guideConfigs = program.guideConfigs ?? {};
70
- if (
71
- objects.xAxisLine &&
72
- program.semanticSpec.guides.axis?.x?.scale === id
73
- ) {
74
- guides.push({ op: "editXAxisLine" });
75
- }
76
- if (
77
- objects.yAxisLine &&
78
- program.semanticSpec.guides.axis?.y?.scale === id
79
- ) {
80
- guides.push({ op: "editYAxisLine" });
81
- }
82
- for (const channel of ["x", "y"]) {
83
- for (const component of ["ticks", "labels", "title"]) {
84
- if (guideConfigs.axis?.[channel]?.[component]?.scale === id) {
85
- const suffix = component[0].toUpperCase() + component.slice(1);
86
- guides.push({ op: `edit${channel.toUpperCase()}Axis${suffix}` });
87
- }
88
- }
89
- }
90
- if (guideConfigs.grid?.horizontal?.scale === id) {
91
- guides.push({ op: "rematerializeHorizontalGrid" });
92
- }
93
- if (guideConfigs.grid?.vertical?.scale === id) {
94
- guides.push({ op: "rematerializeVerticalGrid" });
95
- }
96
- if (materializedLegendUsesScale(program, id)) {
97
- guides.push({ op: "rematerializeLegend" });
98
- }
99
- return buildMaterializationPlan({ guides });
100
- }
101
-
102
68
  export function planLayerDataRematerialization(program, id) {
103
69
  const layer = requireLayer(program, id);
104
70
  const scaleIds = [...new Set(
@@ -106,22 +72,29 @@ export function planLayerDataRematerialization(program, id) {
106
72
  .map(encoding => encoding?.scale)
107
73
  .filter(scale => scale !== undefined)
108
74
  )];
75
+ const markStep = getMarkMaterializationStep(program, layer);
109
76
  const scales = scaleIds.map(scale => ({
110
77
  op: "rematerializeScale",
111
- args: { id: scale, guides: false }
78
+ args: {
79
+ id: scale,
80
+ guides: false,
81
+ ...(markStep === undefined ? {} : { marks: false })
82
+ }
112
83
  }));
113
- const markStep = getMarkMaterializationStep(program, layer);
114
- const marks = markStep === undefined ? [] : [markStep];
84
+ const marks = [
85
+ ...(markStep === undefined ? [] : [markStep]),
86
+ ...getSourceDependentMarkSteps(program, id)
87
+ ];
115
88
  if (scales.length > 0 || marks.length > 0) {
116
89
  const guides = scaleIds.flatMap(scale =>
117
90
  planScaleGuideRematerialization(program, scale)
118
91
  );
119
92
  return buildMaterializationPlan({ scales, marks, guides });
120
93
  }
121
- return buildMaterializationPlan({ marks: layer.mark?.type === "point" &&
122
- program.graphicSpec.objects[layer.id] !== undefined
123
- ? [{ op: "rematerializePointMark", args: { id: layer.id } }]
124
- : [] });
94
+ const existingStep = getExistingMarkRematerializationStep(program, layer);
95
+ return buildMaterializationPlan({
96
+ marks: existingStep === undefined ? [] : [existingStep]
97
+ });
125
98
  }
126
99
 
127
100
  export function applyLayerDataRematerialization(program, id) {
@@ -132,3 +105,4 @@ export function applyLayerDataRematerialization(program, id) {
132
105
  }
133
106
 
134
107
  export { applyMaterializationPlan } from "./planner.js";
108
+ export { planScaleGuideRematerialization };
@@ -1,7 +1,10 @@
1
1
  import { hasMaterializedLegend } from "./legends.js";
2
2
  import { requireLayer } from "../selectors/layers.js";
3
- import { getMarkRematerializationStep } from "./marks.js";
3
+ import {
4
+ getEncodingMaterializationStages
5
+ } from "./marks/index.js";
4
6
  import { buildMaterializationPlan } from "./planner.js";
7
+ import { getSourceDependentMarkSteps } from "./marks/index.js";
5
8
 
6
9
  export function planEncodingRematerialization(program, {
7
10
  target,
@@ -14,33 +17,16 @@ export function planEncodingRematerialization(program, {
14
17
  `Unknown encoding materialization target "${target}"`
15
18
  );
16
19
 
17
- const scales = [];
18
- if (layer.mark?.type === "point" && scale !== undefined) {
19
- scales.push({
20
- op: "rematerializeScale",
21
- args: { id: scale, guides: false }
22
- });
23
- }
24
-
25
- const marks = [];
26
- if (
27
- layer.mark?.type === "area" &&
28
- channel === "color" &&
29
- scale !== undefined
30
- ) {
31
- for (const candidate of program.semanticSpec.layers) {
32
- if (
33
- candidate.mark?.type === "area" &&
34
- candidate.encoding?.color?.scale === scale
35
- ) {
36
- const step = getMarkRematerializationStep(candidate);
37
- if (step !== undefined) marks.push(step);
38
- }
39
- }
40
- } else {
41
- const step = getMarkRematerializationStep(layer);
42
- if (step !== undefined) marks.push(step);
43
- }
20
+ const { scales, marks: directMarks } = getEncodingMaterializationStages(
21
+ program,
22
+ layer,
23
+ channel,
24
+ scale
25
+ );
26
+ const marks = [
27
+ ...directMarks,
28
+ ...getSourceDependentMarkSteps(program, target)
29
+ ];
44
30
 
45
31
  const guides = [];
46
32
  if (hasMaterializedLegend(program)) {