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
@@ -19,6 +19,349 @@ const MAX_EXHAUSTIVE_OFFSET_STEPS = 28;
19
19
  const MAX_SEARCH_DISPLACEMENT = 1_000_000;
20
20
  const DISTANT_RING_COUNT = 16;
21
21
  const DISTANT_ANGLE_COUNT = 32;
22
+ const PLACEMENT_OPTIONS = Object.freeze([
23
+ "anchor", "gap", "overflow", "leader"
24
+ ]);
25
+ const PLACEMENT_LEADER_OPTIONS = Object.freeze(["stroke", "strokeWidth"]);
26
+ const SEMANTIC_ANCHORS = new Set([
27
+ "center", "insideStart", "insideEnd", "outsideStart", "outsideEnd"
28
+ ]);
29
+ const OVERFLOW_POLICIES = new Set(["hide", "outside", "allow"]);
30
+
31
+ export const DEFAULT_MARK_LABEL_PLACEMENT = cloneAndFreeze({
32
+ gap: 4,
33
+ overflow: "hide",
34
+ leader: false
35
+ });
36
+
37
+ export function markLabelPlacementLeaderId(target) {
38
+ return `${target}-placement-leaders`;
39
+ }
40
+
41
+ function normalizePlacementLeader(value) {
42
+ if (value === undefined || value === false) return false;
43
+ validateOptionObject(value, PLACEMENT_LEADER_OPTIONS, "mark label placement leader");
44
+ const leader = { stroke: "#94a3b8", strokeWidth: 1, ...value };
45
+ if (typeof leader.stroke !== "string" || leader.stroke.length === 0) {
46
+ throw new TypeError("Mark label placement leader stroke must be a non-empty string.");
47
+ }
48
+ validateNonNegativeFinite(
49
+ leader.strokeWidth,
50
+ "Mark label placement leader strokeWidth"
51
+ );
52
+ return leader;
53
+ }
54
+
55
+ export function normalizeMarkLabelPlacement(value) {
56
+ validateOptionObject(value, PLACEMENT_OPTIONS, "mark label placement", {
57
+ allowEmpty: false,
58
+ emptyError: TypeError
59
+ });
60
+ if (!SEMANTIC_ANCHORS.has(value.anchor)) {
61
+ throw new Error(`Unsupported mark label anchor "${value.anchor}".`);
62
+ }
63
+ const gap = value.gap ?? DEFAULT_MARK_LABEL_PLACEMENT.gap;
64
+ const overflowPolicy = value.overflow ?? DEFAULT_MARK_LABEL_PLACEMENT.overflow;
65
+ validateNonNegativeFinite(gap, "Mark label placement gap");
66
+ if (!OVERFLOW_POLICIES.has(overflowPolicy)) {
67
+ throw new Error(`Unsupported mark label overflow "${overflowPolicy}".`);
68
+ }
69
+ return cloneAndFreeze({
70
+ anchor: value.anchor,
71
+ gap,
72
+ overflow: overflowPolicy,
73
+ leader: normalizePlacementLeader(value.leader)
74
+ });
75
+ }
76
+
77
+ function finitePoint(value, label) {
78
+ if (
79
+ !isPlainObject(value) ||
80
+ !Number.isFinite(value.x) ||
81
+ !Number.isFinite(value.y)
82
+ ) {
83
+ throw new TypeError(`${label} requires finite x and y.`);
84
+ }
85
+ return value;
86
+ }
87
+
88
+ function unitVector(value, label) {
89
+ finitePoint(value, label);
90
+ const length = Math.hypot(value.x, value.y);
91
+ if (!Number.isFinite(length) || length <= 0) {
92
+ throw new RangeError(`${label} requires a non-zero direction.`);
93
+ }
94
+ return { x: value.x / length, y: value.y / length };
95
+ }
96
+
97
+ function validateIntervalBounds(bounds) {
98
+ if (
99
+ !isPlainObject(bounds) ||
100
+ ![bounds.left, bounds.right, bounds.top, bounds.bottom].every(Number.isFinite) ||
101
+ bounds.right < bounds.left ||
102
+ bounds.bottom < bounds.top
103
+ ) {
104
+ throw new RangeError("Interval label placement requires ordered finite bounds.");
105
+ }
106
+ }
107
+
108
+ function validatePlacementGeometry(geometry, anchor) {
109
+ if (!isPlainObject(geometry)) {
110
+ throw new TypeError("Mark label placement requires source geometry.");
111
+ }
112
+ if (geometry.kind === "interval") {
113
+ finitePoint(geometry.center, "Interval label center");
114
+ for (const role of ["start", "end"]) {
115
+ finitePoint(geometry[role]?.point, `Interval label ${role}`);
116
+ unitVector(geometry[role]?.outward, `Interval label ${role}`);
117
+ }
118
+ validateIntervalBounds(geometry.bounds);
119
+ return;
120
+ }
121
+ if (geometry.kind === "arc") {
122
+ if (![geometry.centerX, geometry.centerY, geometry.startTheta,
123
+ geometry.endTheta, geometry.innerRadius, geometry.outerRadius]
124
+ .every(Number.isFinite)) {
125
+ throw new TypeError("Arc label placement requires finite sector geometry.");
126
+ }
127
+ if (
128
+ geometry.innerRadius < 0 ||
129
+ geometry.outerRadius <= geometry.innerRadius ||
130
+ geometry.startTheta === geometry.endTheta ||
131
+ Math.abs(geometry.endTheta - geometry.startTheta) > 360
132
+ ) {
133
+ throw new RangeError("Arc label placement requires an ordered annular sector.");
134
+ }
135
+ return;
136
+ }
137
+ if (geometry.kind === "point") {
138
+ finitePoint(geometry.center, "Point label center");
139
+ if (anchor === "outsideEnd") {
140
+ finitePoint(geometry.end?.point, "Polar Point label boundary");
141
+ unitVector(geometry.end?.outward, "Polar Point label direction");
142
+ }
143
+ return;
144
+ }
145
+ throw new Error(`Unsupported mark label placement geometry "${geometry.kind}".`);
146
+ }
147
+
148
+ export function assertMarkLabelPlacementSupport({ markType, coordinateType, intervalAxis }, placement) {
149
+ const anchor = placement.anchor;
150
+ if (markType === "bar" || markType === "arc") return placement;
151
+ if (markType === "rect") {
152
+ if (anchor === "center" || intervalAxis !== undefined) return placement;
153
+ throw new Error("Rect semantic label placement requires one explicit interval axis.");
154
+ }
155
+ if (markType === "point") {
156
+ if (anchor === "center" || (coordinateType === "polar" && anchor === "outsideEnd")) {
157
+ return placement;
158
+ }
159
+ throw new Error(
160
+ coordinateType === "polar"
161
+ ? `Polar Point does not support mark label anchor "${anchor}".`
162
+ : `Point does not support mark label anchor "${anchor}".`
163
+ );
164
+ }
165
+ if (markType === "line") {
166
+ throw new Error("Line labels retain the endpoint API and do not accept semantic placement objects.");
167
+ }
168
+ if (markType === "rule") {
169
+ throw new Error("Rule labels do not support semantic placement objects.");
170
+ }
171
+ throw new Error(`Mark type "${markType}" does not support semantic label placement.`);
172
+ }
173
+
174
+ function localTextBounds(text) {
175
+ return resolveTextBounds({ ...text, x: 0, y: 0 });
176
+ }
177
+
178
+ function projectionRange(bounds, direction) {
179
+ const values = [
180
+ { x: bounds.left, y: bounds.top },
181
+ { x: bounds.right, y: bounds.top },
182
+ { x: bounds.right, y: bounds.bottom },
183
+ { x: bounds.left, y: bounds.bottom }
184
+ ].map(point => point.x * direction.x + point.y * direction.y);
185
+ return { minimum: Math.min(...values), maximum: Math.max(...values) };
186
+ }
187
+
188
+ function boundaryCandidate(boundary, direction, gap, text) {
189
+ const resolvedDirection = unitVector(direction, "Mark label placement direction");
190
+ const support = projectionRange(localTextBounds(text), resolvedDirection);
191
+ const distance = gap - support.minimum;
192
+ return {
193
+ x: boundary.x + resolvedDirection.x * distance + (text.dx ?? 0),
194
+ y: boundary.y + resolvedDirection.y * distance + (text.dy ?? 0),
195
+ sourceX: boundary.x,
196
+ sourceY: boundary.y
197
+ };
198
+ }
199
+
200
+ function centerCandidate(center, text) {
201
+ return {
202
+ x: center.x + (text.dx ?? 0),
203
+ y: center.y + (text.dy ?? 0),
204
+ sourceX: center.x,
205
+ sourceY: center.y
206
+ };
207
+ }
208
+
209
+ function candidateBounds(candidate, text) {
210
+ return resolveTextBounds({ ...text, x: candidate.x, y: candidate.y });
211
+ }
212
+
213
+ function intervalContains(bounds, container) {
214
+ const epsilon = 1e-9;
215
+ return bounds.left >= container.left - epsilon &&
216
+ bounds.right <= container.right + epsilon &&
217
+ bounds.top >= container.top - epsilon &&
218
+ bounds.bottom <= container.bottom + epsilon;
219
+ }
220
+
221
+ function distanceToRectangle(centerX, centerY, bounds) {
222
+ const x = Math.max(bounds.left, Math.min(centerX, bounds.right));
223
+ const y = Math.max(bounds.top, Math.min(centerY, bounds.bottom));
224
+ return Math.hypot(x - centerX, y - centerY);
225
+ }
226
+
227
+ function thetaForPoint(point, geometry) {
228
+ return Math.atan2(point.x - geometry.centerX, geometry.centerY - point.y) *
229
+ 180 / Math.PI;
230
+ }
231
+
232
+ function angleWithin(theta, start, end) {
233
+ const sweep = end - start;
234
+ if (Math.abs(sweep) >= 360 - 1e-9) return true;
235
+ const direction = Math.sign(sweep);
236
+ const progress = ((direction * (theta - start)) % 360 + 360) % 360;
237
+ return progress <= Math.abs(sweep) + 1e-9;
238
+ }
239
+
240
+ function arcContains(bounds, geometry) {
241
+ const points = [
242
+ { x: bounds.left, y: bounds.top },
243
+ { x: bounds.right, y: bounds.top },
244
+ { x: bounds.right, y: bounds.bottom },
245
+ { x: bounds.left, y: bounds.bottom },
246
+ { x: (bounds.left + bounds.right) / 2, y: bounds.top },
247
+ { x: bounds.right, y: (bounds.top + bounds.bottom) / 2 },
248
+ { x: (bounds.left + bounds.right) / 2, y: bounds.bottom },
249
+ { x: bounds.left, y: (bounds.top + bounds.bottom) / 2 }
250
+ ];
251
+ const minimumRadius = distanceToRectangle(
252
+ geometry.centerX,
253
+ geometry.centerY,
254
+ bounds
255
+ );
256
+ const maximumRadius = Math.max(...points.slice(0, 4).map(point =>
257
+ Math.hypot(point.x - geometry.centerX, point.y - geometry.centerY)
258
+ ));
259
+ const epsilon = 1e-9;
260
+ return minimumRadius >= geometry.innerRadius - epsilon &&
261
+ maximumRadius <= geometry.outerRadius + epsilon &&
262
+ points.every(point => angleWithin(
263
+ thetaForPoint(point, geometry),
264
+ geometry.startTheta,
265
+ geometry.endTheta
266
+ ));
267
+ }
268
+
269
+ function arcRole(geometry, role) {
270
+ const theta = geometry.startTheta +
271
+ (geometry.endTheta - geometry.startTheta) / 2;
272
+ const radians = theta * Math.PI / 180;
273
+ const radial = { x: Math.sin(radians), y: -Math.cos(radians) };
274
+ const radius = role === "start" ? geometry.innerRadius : geometry.outerRadius;
275
+ return {
276
+ point: {
277
+ x: geometry.centerX + radial.x * radius,
278
+ y: geometry.centerY + radial.y * radius
279
+ },
280
+ outward: role === "start"
281
+ ? { x: -radial.x, y: -radial.y }
282
+ : radial
283
+ };
284
+ }
285
+
286
+ function geometryRole(geometry, role) {
287
+ return geometry.kind === "arc" ? arcRole(geometry, role) : geometry[role];
288
+ }
289
+
290
+ function geometryCenter(geometry) {
291
+ if (geometry.kind !== "arc") return geometry.center;
292
+ const role = arcRole(geometry, "end");
293
+ const radius = (geometry.innerRadius + geometry.outerRadius) / 2;
294
+ return {
295
+ x: geometry.centerX + role.outward.x * radius,
296
+ y: geometry.centerY + role.outward.y * radius
297
+ };
298
+ }
299
+
300
+ function candidateForAnchor(anchor, geometry, placement, text) {
301
+ if (anchor === "center") return centerCandidate(geometryCenter(geometry), text);
302
+ const roleName = anchor.endsWith("Start") ? "start" : "end";
303
+ const role = geometryRole(geometry, roleName);
304
+ const outside = anchor.startsWith("outside");
305
+ const direction = outside
306
+ ? role.outward
307
+ : { x: -role.outward.x, y: -role.outward.y };
308
+ return boundaryCandidate(role.point, direction, placement.gap, text);
309
+ }
310
+
311
+ function arcOutsideStartCrossesCenter(candidate, geometry, text) {
312
+ if (geometry.kind !== "arc") return false;
313
+ const radial = arcRole(geometry, "end").outward;
314
+ const projections = projectionRange(candidateBounds(candidate, text), radial);
315
+ const centerProjection = geometry.centerX * radial.x + geometry.centerY * radial.y;
316
+ return projections.minimum < centerProjection - 1e-9;
317
+ }
318
+
319
+ function fitsSource(candidate, geometry, text) {
320
+ if (geometry.kind === "point") return true;
321
+ const bounds = candidateBounds(candidate, text);
322
+ return geometry.kind === "arc"
323
+ ? arcContains(bounds, geometry)
324
+ : intervalContains(bounds, geometry.bounds);
325
+ }
326
+
327
+ function outsideFallback(anchor) {
328
+ return anchor === "insideStart" ? "outsideStart" : "outsideEnd";
329
+ }
330
+
331
+ export function resolveMarkLabelPlacement({ placement, geometry, text } = {}) {
332
+ const normalized = normalizeMarkLabelPlacement(placement);
333
+ validatePlacementGeometry(geometry, normalized.anchor);
334
+ let anchor = normalized.anchor;
335
+ let candidate = candidateForAnchor(anchor, geometry, normalized, text);
336
+ const requiresFit = anchor === "center" || anchor.startsWith("inside");
337
+ if (requiresFit && !fitsSource(candidate, geometry, text)) {
338
+ if (normalized.overflow === "hide") {
339
+ return cloneAndFreeze({ visible: false, anchor, fallback: false });
340
+ }
341
+ if (normalized.overflow === "outside") {
342
+ anchor = outsideFallback(anchor);
343
+ candidate = candidateForAnchor(anchor, geometry, normalized, text);
344
+ }
345
+ }
346
+ if (
347
+ anchor === "outsideStart" &&
348
+ normalized.overflow !== "allow" &&
349
+ arcOutsideStartCrossesCenter(candidate, geometry, text)
350
+ ) {
351
+ return cloneAndFreeze({
352
+ visible: false,
353
+ anchor,
354
+ fallback: anchor !== normalized.anchor
355
+ });
356
+ }
357
+ return cloneAndFreeze({
358
+ visible: true,
359
+ anchor,
360
+ fallback: anchor !== normalized.anchor,
361
+ ...candidate,
362
+ bounds: candidateBounds(candidate, text)
363
+ });
364
+ }
22
365
 
23
366
  export const DEFAULT_LABEL_LAYOUT_GEOMETRY = cloneAndFreeze({
24
367
  axis: "both",
@@ -1,6 +1,10 @@
1
1
  import { deriveSeriesBarCells } from "../../grammar/bars/aggregate.js";
2
2
  import { resolveBarChannels, resolveBarColorLayout, resolveBarOffsetChannel } from "../../grammar/bars/policy.js";
3
- import { mapContinuousScaleValues, mapOrdinalPositionValues } from "../../grammar/scales/index.js";
3
+ import {
4
+ mapContinuousScaleValues,
5
+ mapOrdinalPositionValues,
6
+ readScaleField
7
+ } from "../../grammar/scales/index.js";
4
8
  import { mapScaleConsumerValues } from "../scales/map.js";
5
9
  import { DEFAULT_BAR_FILL, resolveBarAppearance } from "./resolve.js";
6
10
  import { resolveBarWidth } from "../../grammar/bars/geometry.js";
@@ -19,6 +23,24 @@ export function deriveAggregateRectangles(required, resolved, widthConfig) {
19
23
  const segments = deriveSeriesBarCells(dataset.values, layer, temporal ? undefined : categoryScale.domain,
20
24
  grouped ? offsetScale.domain : layer.encoding.group === undefined && color?.fieldType !== "quantitative" ? colorScale?.domain : undefined);
21
25
  const fills = color === undefined ? undefined : mapScaleConsumerValues(segments.map(segment => segment.cell.color), colorScale, "color");
26
+ const stroke = layer.encoding?.stroke;
27
+ const strokeValues = stroke === undefined ? undefined : segments.map(segment => {
28
+ const values = readScaleField(
29
+ segment.cell.sourceIndices.map(index => dataset.values[index]),
30
+ stroke.field,
31
+ stroke.fieldType,
32
+ { temporalUnit: stroke.temporalUnit }
33
+ );
34
+ if (new Set(values).size !== 1) {
35
+ throw new Error("Bar stroke requires one value within each aggregate cell.");
36
+ }
37
+ return values[0];
38
+ });
39
+ const strokes = strokeValues === undefined ? undefined : mapScaleConsumerValues(
40
+ strokeValues,
41
+ resolved.resolvedScales[stroke.scale],
42
+ "stroke"
43
+ );
22
44
  const config = resolved.markConfigs[layer.id] ?? {};
23
45
  const existing = resolved.graphicSpec.objects[layer.id].items;
24
46
  const width = resolveBarWidth(widthConfig, grouped ? offsetScale.bandwidth : Math.abs(categoryScale.bandwidth ?? categoryScale.step));
@@ -42,6 +64,7 @@ export function deriveAggregateRectangles(required, resolved, widthConfig) {
42
64
  return { x: vertical ? position : Math.min(a, b), y: vertical ? Math.min(a, b) : position,
43
65
  width: vertical ? width : Math.abs(a - b), height: vertical ? Math.abs(a - b) : width,
44
66
  fill: fills?.[index] ?? config.barAppearance?.fill ?? config.fill ?? DEFAULT_BAR_FILL,
45
- ...resolveBarAppearance(config, existing[index]?.properties) };
67
+ ...resolveBarAppearance(config, existing[index]?.properties),
68
+ ...(strokes === undefined ? {} : { stroke: strokes[index] }) };
46
69
  });
47
70
  }
@@ -1,6 +1,5 @@
1
1
  import { readSeriesIdentity } from "../../grammar/pathSeries.js";
2
2
  import {
3
- countHistogramBins,
4
3
  findHistogramBinIndex,
5
4
  resolveHistogramBins
6
5
  } from "../../grammar/histogram.js";
@@ -8,14 +7,55 @@ import {
8
7
  mapContinuousScaleValues,
9
8
  mapOrdinalValues,
10
9
  readNominalField,
11
- readQuantitativeField
10
+ readQuantitativeField,
11
+ readScaleField
12
12
  } from "../../grammar/scales/index.js";
13
+ import { mapScaleConsumerValues } from "../scales/map.js";
13
14
  import { resolveBarColorLayout } from "../../grammar/bars/policy.js";
14
15
  import { layoutSeriesPartition } from "../../grammar/seriesLayout.js";
15
16
  import {
16
17
  DEFAULT_BAR_FILL,
17
18
  resolveBarAppearance
18
19
  } from "./resolve.js";
20
+ import {
21
+ readStatisticalWeights,
22
+ sumEntryWeights,
23
+ summarizeStatisticalWeights,
24
+ validateWeightedNumericFields
25
+ } from "../../grammar/weightedStatistics.js";
26
+
27
+ export function resolveHistogramInput(dataset, xEncoding) {
28
+ const allValues = readQuantitativeField(dataset.values, xEncoding.field);
29
+ if (xEncoding.weight === undefined) {
30
+ return {
31
+ rows: dataset.values,
32
+ values: allValues,
33
+ entries: dataset.values.map((row, index) => ({ row, index })),
34
+ mass(entries) {
35
+ return entries.length;
36
+ }
37
+ };
38
+ }
39
+ const weights = readStatisticalWeights(
40
+ dataset.values,
41
+ xEncoding.weight,
42
+ "Histogram"
43
+ );
44
+ validateWeightedNumericFields(weights.entries, [xEncoding.field], "Histogram");
45
+ const summary = summarizeStatisticalWeights(
46
+ weights.entries,
47
+ weights.definition.kind,
48
+ "Histogram data"
49
+ );
50
+ return {
51
+ rows: summary.positive.map(entry => entry.row),
52
+ values: summary.positive.map(entry => entry.row[xEncoding.field]),
53
+ entries: summary.positive,
54
+ mass(entries) {
55
+ return sumEntryWeights(entries, summary, "Histogram bin mass");
56
+ }
57
+ };
58
+ }
19
59
 
20
60
  export function deriveHistogramSegments({
21
61
  dataset,
@@ -24,7 +64,8 @@ export function deriveHistogramSegments({
24
64
  xScale,
25
65
  resolvedScales
26
66
  }) {
27
- const xValues = readQuantitativeField(dataset.values, xEncoding.field);
67
+ const input = resolveHistogramInput(dataset, xEncoding);
68
+ const { rows, values: xValues, entries } = input;
28
69
  const bins = resolveHistogramBins({
29
70
  values: xValues,
30
71
  bin: xEncoding.bin,
@@ -36,13 +77,13 @@ export function deriveHistogramSegments({
36
77
  const layout = resolveBarColorLayout(layer);
37
78
 
38
79
  if (layer.encoding?.group !== undefined) {
39
- const identity = readSeriesIdentity(dataset.values, layer);
80
+ const identity = readSeriesIdentity(rows, layer);
40
81
  const cellRows = bins.boundaries.slice(0, -1).map(() => identity.domain.map(() => []));
41
82
  const index = new Map(identity.domain.map((value, i) => [value, i]));
42
83
  xValues.forEach((value, i) => { const bin = findHistogramBinIndex(value, bins.boundaries);
43
- if (bin !== -1) cellRows[bin][index.get(identity.values[i])].push(dataset.values[i]); });
44
- return cellRows.flatMap((cells, bin) => layoutSeriesPartition(cells.map(rows => rows.length), layout).map(segment => {
45
- const members = cells[segment.index];
84
+ if (bin !== -1) cellRows[bin][index.get(identity.values[i])].push(entries[i]); });
85
+ return cellRows.flatMap((cells, bin) => layoutSeriesPartition(cells.map(input.mass), layout).map(segment => {
86
+ const members = cells[segment.index].map(entry => entry.row);
46
87
  let color, colorValue;
47
88
  if (colorEncoding !== undefined) {
48
89
  const values = readNominalField(members, colorEncoding.field);
@@ -59,8 +100,13 @@ export function deriveHistogramSegments({
59
100
  }
60
101
 
61
102
  if (colorEncoding?.scale === undefined) {
62
- return countHistogramBins(xValues, bins.boundaries).flatMap((count, bin) =>
63
- layoutSeriesPartition([count], layout).map(segment => ({
103
+ const cellRows = bins.boundaries.slice(0, -1).map(() => []);
104
+ xValues.forEach((value, index) => {
105
+ const bin = findHistogramBinIndex(value, bins.boundaries);
106
+ if (bin !== -1) cellRows[bin].push(entries[index]);
107
+ });
108
+ return cellRows.flatMap((cell, bin) =>
109
+ layoutSeriesPartition([input.mass(cell)], layout).map(segment => ({
64
110
  bin,
65
111
  start: bins.boundaries[bin],
66
112
  end: bins.boundaries[bin + 1],
@@ -68,9 +114,7 @@ export function deriveHistogramSegments({
68
114
  categoryCount: 1,
69
115
  stackStart: segment.start,
70
116
  stackEnd: segment.end,
71
- members: dataset.values.filter((_, index) =>
72
- findHistogramBinIndex(xValues[index], bins.boundaries) === bin
73
- )
117
+ members: cell.map(entry => entry.row)
74
118
  }))
75
119
  );
76
120
  }
@@ -81,24 +125,24 @@ export function deriveHistogramSegments({
81
125
  `Bar mark "${layer.id}" requires a resolved color scale.`
82
126
  );
83
127
  }
84
- const colorValues = readNominalField(dataset.values, colorEncoding.field);
128
+ const colorValues = readNominalField(rows, colorEncoding.field);
85
129
  mapOrdinalValues(colorValues, colorScale.domain, colorScale.range);
86
130
  const categoryIndex = new Map(
87
131
  colorScale.domain.map((value, index) => [value, index])
88
132
  );
89
- const counts = bins.boundaries.slice(0, -1).map(() =>
90
- colorScale.domain.map(() => 0)
133
+ const cells = bins.boundaries.slice(0, -1).map(() =>
134
+ colorScale.domain.map(() => [])
91
135
  );
92
136
 
93
137
  for (let index = 0; index < xValues.length; index += 1) {
94
138
  const bin = findHistogramBinIndex(xValues[index], bins.boundaries);
95
139
  const category = categoryIndex.get(colorValues[index]);
96
- if (bin !== -1 && category !== undefined) counts[bin][category] += 1;
140
+ if (bin !== -1 && category !== undefined) cells[bin][category].push(entries[index]);
97
141
  }
98
142
 
99
143
  const segments = [];
100
- for (let bin = 0; bin < counts.length; bin += 1) {
101
- const partition = layoutSeriesPartition(counts[bin], layout);
144
+ for (let bin = 0; bin < cells.length; bin += 1) {
145
+ const partition = layoutSeriesPartition(cells[bin].map(input.mass), layout);
102
146
  for (const segment of partition) {
103
147
  const category = segment.index;
104
148
  segments.push({
@@ -106,13 +150,10 @@ export function deriveHistogramSegments({
106
150
  start: bins.boundaries[bin],
107
151
  end: bins.boundaries[bin + 1],
108
152
  category,
109
- categoryCount: counts[bin].length,
153
+ categoryCount: cells[bin].length,
110
154
  stackStart: segment.start,
111
155
  stackEnd: segment.end,
112
- members: dataset.values.filter((_, index) =>
113
- findHistogramBinIndex(xValues[index], bins.boundaries) === bin &&
114
- categoryIndex.get(colorValues[index]) === category
115
- ),
156
+ members: cells[bin][category].map(entry => entry.row),
116
157
  color: mapOrdinalValues(
117
158
  [colorScale.domain[category]],
118
159
  colorScale.domain,
@@ -135,6 +176,24 @@ export function deriveHistogramRectangles(required, resolved) {
135
176
  const layout = resolveBarColorLayout(required.layer);
136
177
  const config = resolved.markConfigs[required.layer.id] ?? {};
137
178
  const appearance = config.barAppearance ?? {};
179
+ const stroke = required.layer.encoding?.stroke;
180
+ const strokeValues = stroke === undefined ? undefined : segments.map(segment => {
181
+ const values = readScaleField(
182
+ segment.members,
183
+ stroke.field,
184
+ stroke.fieldType,
185
+ { temporalUnit: stroke.temporalUnit }
186
+ );
187
+ if (new Set(values).size !== 1) {
188
+ throw new Error("Histogram stroke requires one value within each bin/series cell.");
189
+ }
190
+ return values[0];
191
+ });
192
+ const strokes = strokeValues === undefined ? undefined : mapScaleConsumerValues(
193
+ strokeValues,
194
+ resolved.resolvedScales[stroke.scale],
195
+ "stroke"
196
+ );
138
197
 
139
198
  return segments.map((segment, index) => {
140
199
  const [x1, x2] = mapContinuousScaleValues(
@@ -164,7 +223,8 @@ export function deriveHistogramRectangles(required, resolved) {
164
223
  config.fill ??
165
224
  existing[index]?.properties.fill ??
166
225
  DEFAULT_BAR_FILL,
167
- ...resolveBarAppearance(config, existing[index]?.properties)
226
+ ...resolveBarAppearance(config, existing[index]?.properties),
227
+ ...(strokes === undefined ? {} : { stroke: strokes[index] })
168
228
  };
169
229
  });
170
230
  }
@@ -1,4 +1,10 @@
1
- import { mapContinuousScaleValues, mapOrdinalPositionValues, mapOrdinalValues } from "../../grammar/scales/index.js";
1
+ import {
2
+ mapContinuousScaleValues,
3
+ mapOrdinalPositionValues,
4
+ mapOrdinalValues,
5
+ readScaleField
6
+ } from "../../grammar/scales/index.js";
7
+ import { mapScaleConsumerValues } from "../scales/map.js";
2
8
  import { resolveBarWidth } from "../../grammar/bars/geometry.js";
3
9
  import {
4
10
  DEFAULT_BAR_FILL,
@@ -36,6 +42,14 @@ export function deriveRangedRectangles(required, program, width) {
36
42
  const appearanceConfig = color === undefined
37
43
  ? config
38
44
  : { ...config, stroke: undefined };
45
+ const stroke = layer.encoding?.stroke;
46
+ const strokes = stroke === undefined ? undefined : mapScaleConsumerValues(
47
+ readScaleField(dataset.values, stroke.field, stroke.fieldType, {
48
+ temporalUnit: stroke.temporalUnit
49
+ }),
50
+ program.resolvedScales[stroke.scale],
51
+ "stroke"
52
+ );
39
53
  return dataset.values.map((_, index) => vertical ? {
40
54
  x: centers[index] - band / 2, y: Math.min(first[index], second[index]), width: band,
41
55
  height: Math.abs(second[index] - first[index]), fill: fills[index],
@@ -44,7 +58,8 @@ export function deriveRangedRectangles(required, program, width) {
44
58
  undefined,
45
59
  color === undefined ? DEFAULT_BAR_STROKE : fills[index],
46
60
  1
47
- )
61
+ ),
62
+ ...(strokes === undefined ? {} : { stroke: strokes[index] })
48
63
  } : {
49
64
  x: Math.min(first[index], second[index]), y: centers[index] - band / 2,
50
65
  width: Math.abs(second[index] - first[index]), height: band, fill: fills[index],
@@ -53,6 +68,7 @@ export function deriveRangedRectangles(required, program, width) {
53
68
  undefined,
54
69
  color === undefined ? DEFAULT_BAR_STROKE : fills[index],
55
70
  1
56
- )
71
+ ),
72
+ ...(strokes === undefined ? {} : { stroke: strokes[index] })
57
73
  });
58
74
  }
@@ -3,6 +3,7 @@ import { findDataset } from "../../selectors/datasets.js";
3
3
  import { findLayer } from "../../selectors/layers.js";
4
4
  import { findSemanticScale } from "../../selectors/scales.js";
5
5
  import { BAR_GRAINS, resolveBarGrain } from "../../grammar/bars/policy.js";
6
+ import { requestedStrokeDetails } from "../../grammar/strokeStyle.js";
6
7
 
7
8
  export const DEFAULT_BAR_FILL = DEFAULT_COLORS.mark;
8
9
  export const DEFAULT_BAR_STROKE = "white";
@@ -25,7 +26,8 @@ export function resolveBarAppearance(
25
26
  ? 0
26
27
  : appearance.strokeWidth ?? config.strokeWidth ??
27
28
  existing?.strokeWidth ?? DEFAULT_BAR_STROKE_WIDTH,
28
- ...(opacity === undefined ? {} : { opacity })
29
+ ...(opacity === undefined ? {} : { opacity }),
30
+ ...requestedStrokeDetails(appearance, "Bar mark")
29
31
  };
30
32
  }
31
33
 
@@ -40,9 +42,10 @@ export function requireCompleteBar(program, id) {
40
42
  throw new Error(`Bar mark "${id}" requires an existing dataset.`);
41
43
  }
42
44
  const graphic = program.graphicSpec.objects[id];
43
- const rectCollection = graphic?.type === "collection" &&
44
- graphic.items?.every(child => child.type === "rect");
45
- if (graphic?.type !== "rect" && !rectCollection) {
45
+ const roundedCollection = graphic?.type === "collection" &&
46
+ Object.hasOwn(program.markConfigs[id]?.barAppearance ?? {}, "cornerRadius") &&
47
+ graphic.items?.every(child => ["rect", "path"].includes(child.type));
48
+ if (graphic?.type !== "rect" && !roundedCollection) {
46
49
  throw new Error(`Bar mark "${id}" requires rect graphics.`);
47
50
  }
48
51
 
@@ -121,6 +121,8 @@ export function clearCompositionChildren(program) {
121
121
  export function materializeCompositionGraphics(program) {
122
122
  program._assertCompositionProgram("materializeCompositionGraphics");
123
123
  const layout = resolveCompositionProgramLayout(program);
124
+ const background = program.graphicSpec.objects.canvas?.properties
125
+ ?.background ?? "white";
124
126
  let next = clearCompositionChildren(program);
125
127
  if (next.graphicSpec.objects.canvas === undefined) {
126
128
  next = next.createGraphics({ id: "canvas", type: "canvas" });
@@ -128,7 +130,7 @@ export function materializeCompositionGraphics(program) {
128
130
  for (const [property, value] of Object.entries({
129
131
  width: layout.width,
130
132
  height: layout.height,
131
- background: "white"
133
+ background
132
134
  })) {
133
135
  next = next.editGraphics({ target: "canvas", property, value });
134
136
  }