ggaction 0.0.2 → 0.0.3

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 (199) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +12 -1
  3. package/package.json +6 -1
  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/encodings/appearance.js +11 -0
  16. package/src/actions/encodings/barWidth.js +5 -3
  17. package/src/actions/encodings/color.js +120 -25
  18. package/src/actions/encodings/index.js +2 -0
  19. package/src/actions/encodings/offset.js +90 -72
  20. package/src/actions/encodings/position/apply.js +66 -0
  21. package/src/actions/encodings/position/policies/arc.js +26 -0
  22. package/src/actions/encodings/position/policies/bar.js +9 -5
  23. package/src/actions/encodings/position/policies/index.js +5 -1
  24. package/src/actions/encodings/position/policies/line.js +20 -3
  25. package/src/actions/encodings/position/resolve.js +57 -8
  26. package/src/actions/encodings/position.js +44 -90
  27. package/src/actions/encodings/ranged.js +28 -6
  28. package/src/actions/encodings/shared.js +17 -21
  29. package/src/actions/encodings/text.js +73 -0
  30. package/src/actions/errorBands/create.js +37 -29
  31. package/src/actions/errorBands/edit.js +206 -0
  32. package/src/actions/errorBands/index.js +9 -0
  33. package/src/actions/errorBands/options.js +39 -0
  34. package/src/actions/errorBars/create.js +50 -43
  35. package/src/actions/errorBars/edit.js +130 -0
  36. package/src/actions/errorBars/index.js +3 -0
  37. package/src/actions/errorBars/options.js +52 -0
  38. package/src/actions/facets/actions.js +162 -0
  39. package/src/actions/facets/derive.js +200 -0
  40. package/src/actions/facets/guides.js +16 -0
  41. package/src/actions/facets/index.js +1 -0
  42. package/src/actions/facets/replay.js +69 -0
  43. package/src/actions/guides/applicability.js +113 -0
  44. package/src/actions/guides/axes/axes.js +70 -16
  45. package/src/actions/guides/axes/edit.js +126 -0
  46. package/src/actions/guides/axes/index.js +12 -0
  47. package/src/actions/guides/axes/labels.js +21 -18
  48. package/src/actions/guides/axes/remove.js +60 -0
  49. package/src/actions/guides/axes/tickGroups.js +7 -5
  50. package/src/actions/guides/axes/ticks.js +9 -4
  51. package/src/actions/guides/axes/titles.js +7 -5
  52. package/src/actions/guides/grids/grid.js +144 -15
  53. package/src/actions/guides/grids/resolve.js +5 -13
  54. package/src/actions/guides/guides.js +17 -75
  55. package/src/actions/guides/index.js +6 -0
  56. package/src/actions/guides/legends/categorical/actions.js +48 -9
  57. package/src/actions/guides/legends/categorical/components.js +16 -16
  58. package/src/actions/guides/legends/categorical/index.js +14 -14
  59. package/src/actions/guides/legends/categorical/layout.js +3 -1
  60. package/src/actions/guides/legends/categorical/options.js +2 -5
  61. package/src/actions/guides/legends/categorical/resolve.js +2 -2
  62. package/src/actions/guides/legends/categorical/symbols.js +14 -14
  63. package/src/actions/guides/legends/continuous/common.js +7 -7
  64. package/src/actions/guides/legends/continuous/interval.js +5 -5
  65. package/src/actions/guides/legends/edit.js +2 -6
  66. package/src/actions/guides/legends/focused.js +63 -0
  67. package/src/actions/guides/legends/index.js +4 -0
  68. package/src/actions/guides/legends/remove.js +64 -0
  69. package/src/actions/guides/legends/size.js +12 -14
  70. package/src/actions/guides/polar/axes/facade.js +219 -0
  71. package/src/actions/guides/polar/axes/labels.js +188 -0
  72. package/src/actions/guides/polar/axes/lines.js +113 -0
  73. package/src/actions/guides/polar/axes/shared.js +112 -0
  74. package/src/actions/guides/polar/axes/ticks.js +164 -0
  75. package/src/actions/guides/polar/axes/titles.js +165 -0
  76. package/src/actions/guides/polar/axes.js +55 -0
  77. package/src/actions/guides/polar/grids.js +252 -0
  78. package/src/actions/guides/polar/index.js +2 -0
  79. package/src/actions/guides/polar/resolve.js +269 -0
  80. package/src/actions/index.js +4 -0
  81. package/src/actions/marks/arc/actions.js +239 -0
  82. package/src/actions/marks/arc/index.js +1 -0
  83. package/src/actions/marks/{area.js → area/actions.js} +51 -38
  84. package/src/actions/marks/area/index.js +4 -0
  85. package/src/actions/marks/bar/create.js +26 -5
  86. package/src/actions/marks/bar/materialize.js +25 -31
  87. package/src/actions/marks/index.js +12 -4
  88. package/src/actions/marks/lifecycle.js +27 -0
  89. package/src/actions/marks/line/actions.js +373 -0
  90. package/src/actions/marks/line/index.js +1 -0
  91. package/src/actions/marks/{point.js → point/actions.js} +67 -108
  92. package/src/actions/marks/point/index.js +1 -0
  93. package/src/actions/marks/rect/actions.js +165 -0
  94. package/src/actions/marks/rect/index.js +1 -0
  95. package/src/actions/marks/remove.js +214 -0
  96. package/src/actions/marks/{rule.js → rule/actions.js} +34 -28
  97. package/src/actions/marks/rule/index.js +1 -0
  98. package/src/actions/marks/shared.js +156 -25
  99. package/src/actions/marks/text/actions.js +192 -0
  100. package/src/actions/marks/text/index.js +1 -0
  101. package/src/actions/primitives/createGraphics.js +13 -6
  102. package/src/actions/primitives/editGraphics.js +10 -3
  103. package/src/actions/primitives/semantic.js +26 -3
  104. package/src/actions/primitives/semanticValue.js +25 -3
  105. package/src/actions/regression/components.js +1 -1
  106. package/src/actions/regression/create.js +18 -1
  107. package/src/actions/regression/edit.js +247 -0
  108. package/src/actions/regression/index.js +3 -0
  109. package/src/actions/regression/resolve.js +3 -1
  110. package/src/actions/scales/consumers.js +15 -6
  111. package/src/actions/scales/definitions.js +13 -4
  112. package/src/actions/scales/edit.js +42 -19
  113. package/src/actions/scales/materialize.js +57 -408
  114. package/src/actions/selection/actions.js +38 -24
  115. package/src/actions/selection/index.js +2 -0
  116. package/src/actions/titles/actions.js +63 -2
  117. package/src/actions/titles/index.js +2 -0
  118. package/src/actions/titles/resolve.js +12 -17
  119. package/src/composition.js +10 -0
  120. package/src/core/ChartProgram.js +200 -2
  121. package/src/core/action.js +10 -0
  122. package/src/core/validation.js +16 -0
  123. package/src/core/vocabulary.js +154 -4
  124. package/src/grammar/arcs.js +154 -0
  125. package/src/grammar/bars/aggregate.js +2 -2
  126. package/src/grammar/bars/geometry.js +3 -3
  127. package/src/grammar/bars/policy.js +7 -1
  128. package/src/grammar/coordinates.js +3 -7
  129. package/src/grammar/density.js +20 -1
  130. package/src/grammar/facets/dependencies.js +185 -0
  131. package/src/grammar/facets/guides.js +257 -0
  132. package/src/grammar/facets/scales.js +254 -0
  133. package/src/grammar/facets.js +122 -0
  134. package/src/grammar/lineSeries.js +111 -3
  135. package/src/grammar/polar.js +102 -0
  136. package/src/grammar/polarGuides.js +243 -0
  137. package/src/grammar/polarLineCommands.js +43 -0
  138. package/src/grammar/polarPaths.js +161 -0
  139. package/src/grammar/positionCompatibility.js +18 -2
  140. package/src/grammar/rects.js +26 -0
  141. package/src/grammar/scales/continuous.js +6 -1
  142. package/src/grammar/scales/fields.js +7 -0
  143. package/src/grammar/scales/ordinal.js +5 -2
  144. package/src/grammar/scales/policies.js +1 -1
  145. package/src/grammar/schemas/graphic.js +1 -1
  146. package/src/grammar/schemas/graphicBounds.js +64 -9
  147. package/src/grammar/schemas/graphicTree.js +44 -10
  148. package/src/grammar/schemas/semanticPath.js +31 -9
  149. package/src/grammar/seriesLayout.js +13 -2
  150. package/src/grammar/text.js +71 -0
  151. package/src/grammar/ticks.js +50 -13
  152. package/src/index.js +1 -0
  153. package/src/layout/composition.js +310 -0
  154. package/src/layout/facets.js +131 -0
  155. package/src/layout/title.js +6 -0
  156. package/src/materialization/bars/aggregate.js +5 -2
  157. package/src/materialization/bars/grouped.js +75 -37
  158. package/src/materialization/composition.js +152 -0
  159. package/src/materialization/compositionSnapshot.js +114 -0
  160. package/src/materialization/dependencies.js +71 -16
  161. package/src/materialization/encodings.js +14 -28
  162. package/src/materialization/facetGuides.js +426 -0
  163. package/src/materialization/facets.js +214 -0
  164. package/src/materialization/graphicHierarchy.js +15 -5
  165. package/src/materialization/guides/resources.js +46 -0
  166. package/src/materialization/marks.js +220 -14
  167. package/src/materialization/planner.js +105 -12
  168. package/src/materialization/rect.js +138 -0
  169. package/src/materialization/rectConfig.js +38 -0
  170. package/src/materialization/rowEncoding.js +33 -0
  171. package/src/materialization/scales/map.js +40 -0
  172. package/src/materialization/scales/policies/arc.js +37 -0
  173. package/src/materialization/scales/policies/bar.js +132 -0
  174. package/src/materialization/scales/policies/series.js +76 -0
  175. package/src/materialization/scales/resolve.js +326 -0
  176. package/src/materialization/selection/items/arc.js +29 -0
  177. package/src/materialization/selection/items/bar.js +255 -0
  178. package/src/materialization/selection/items/common.js +131 -0
  179. package/src/materialization/selection/items/index.js +6 -0
  180. package/src/materialization/selection/items/path.js +59 -0
  181. package/src/materialization/selection/items/point.js +55 -0
  182. package/src/materialization/selection/items/rect.js +23 -0
  183. package/src/materialization/selection/items/rule.js +25 -0
  184. package/src/materialization/selection/policies/arc.js +10 -0
  185. package/src/materialization/selection/policies/area.js +1 -1
  186. package/src/materialization/selection/policies/bar.js +1 -1
  187. package/src/materialization/selection/policies/index.js +5 -1
  188. package/src/materialization/selection/policies/line.js +1 -1
  189. package/src/materialization/selection/policies/point.js +1 -1
  190. package/src/materialization/selection/policies/rect.js +10 -0
  191. package/src/materialization/selection/policies/rule.js +1 -1
  192. package/src/materialization/selection/styles.js +24 -16
  193. package/src/materialization/text.js +156 -0
  194. package/src/renderers/canvas/index.js +68 -22
  195. package/types/extension.d.ts +1 -0
  196. package/types/index.d.ts +56 -1
  197. package/types/program.d.ts +664 -27
  198. package/src/actions/marks/line.js +0 -248
  199. package/src/materialization/selection/items.js +0 -503
@@ -0,0 +1,310 @@
1
+ import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
+
3
+ const DIRECTIONS = Object.freeze(["horizontal", "vertical"]);
4
+ const ALIGNMENTS = Object.freeze(["start", "center", "end"]);
5
+ const SIZE_MODES = Object.freeze(["auto", "explicit"]);
6
+ const PADDING_KEYS = Object.freeze(["top", "right", "bottom", "left"]);
7
+
8
+ export const DEFAULT_COMPOSITION_LAYOUT = cloneAndFreeze({
9
+ gap: 16,
10
+ align: "center",
11
+ padding: { top: 0, right: 0, bottom: 0, left: 0 }
12
+ });
13
+
14
+ export function validateCompositionSpacing(value, label) {
15
+ if (!Number.isFinite(value) || value < 0) {
16
+ throw new RangeError(`${label} must be a non-negative finite number.`);
17
+ }
18
+ return value;
19
+ }
20
+
21
+ function validatePaddingBase(base) {
22
+ if (!isPlainObject(base)) {
23
+ throw new TypeError("Composition padding base must be a plain object.");
24
+ }
25
+ for (const key of PADDING_KEYS) {
26
+ validateCompositionSpacing(base[key], `Composition padding.${key}`);
27
+ }
28
+ return base;
29
+ }
30
+
31
+ export function normalizeCompositionPadding(
32
+ padding,
33
+ base = DEFAULT_COMPOSITION_LAYOUT.padding
34
+ ) {
35
+ validatePaddingBase(base);
36
+ if (Number.isFinite(padding)) {
37
+ validateCompositionSpacing(padding, "Composition padding");
38
+ return cloneAndFreeze({
39
+ top: padding,
40
+ right: padding,
41
+ bottom: padding,
42
+ left: padding
43
+ });
44
+ }
45
+ if (!isPlainObject(padding)) {
46
+ throw new TypeError("Composition padding must be a number or a plain object.");
47
+ }
48
+ const keys = Object.keys(padding);
49
+ if (keys.length === 0) {
50
+ throw new TypeError("Composition padding object must contain at least one side.");
51
+ }
52
+ for (const key of keys) {
53
+ if (!PADDING_KEYS.includes(key)) {
54
+ throw new Error(`Unknown composition padding option "${key}".`);
55
+ }
56
+ validateCompositionSpacing(padding[key], `Composition padding.${key}`);
57
+ }
58
+ return cloneAndFreeze({ ...base, ...padding });
59
+ }
60
+
61
+ function normalizeDirection(direction) {
62
+ if (!DIRECTIONS.includes(direction)) {
63
+ throw new Error(
64
+ `Unknown composition direction "${direction}"; expected horizontal or vertical.`
65
+ );
66
+ }
67
+ return direction;
68
+ }
69
+
70
+ export function normalizeCompositionAlign(align) {
71
+ if (!ALIGNMENTS.includes(align)) {
72
+ throw new Error(
73
+ `Unknown composition align "${align}"; expected start, center, or end.`
74
+ );
75
+ }
76
+ return align;
77
+ }
78
+
79
+ export function normalizeCompositionChildren(children) {
80
+ if (!Array.isArray(children) || children.length === 0) {
81
+ throw new TypeError("Composition layout requires a non-empty children array.");
82
+ }
83
+ const ids = new Set();
84
+ return children.map((child, index) => {
85
+ if (!isPlainObject(child)) {
86
+ throw new TypeError(`Composition child ${index} must be a plain object.`);
87
+ }
88
+ const unknown = Object.keys(child).find(
89
+ key => !["id", "width", "height", "widthMode", "heightMode"].includes(key)
90
+ );
91
+ if (unknown !== undefined) {
92
+ throw new Error(`Unknown composition child property "${unknown}".`);
93
+ }
94
+ if (typeof child.id !== "string" || child.id.length === 0) {
95
+ throw new TypeError(`Composition child ${index} requires a non-empty id.`);
96
+ }
97
+ if (ids.has(child.id)) {
98
+ throw new Error(`Duplicate composition child id "${child.id}".`);
99
+ }
100
+ ids.add(child.id);
101
+ for (const dimension of ["width", "height"]) {
102
+ if (!Number.isFinite(child[dimension]) || child[dimension] <= 0) {
103
+ throw new RangeError(
104
+ `Composition child "${child.id}" ${dimension} must be a positive finite number.`
105
+ );
106
+ }
107
+ const mode = child[`${dimension}Mode`] ?? "explicit";
108
+ if (!SIZE_MODES.includes(mode)) {
109
+ throw new Error(
110
+ `Unknown composition child ${dimension} mode "${mode}"; expected auto or explicit.`
111
+ );
112
+ }
113
+ }
114
+ return cloneAndFreeze({
115
+ id: child.id,
116
+ width: child.width,
117
+ height: child.height,
118
+ widthMode: child.widthMode ?? "explicit",
119
+ heightMode: child.heightMode ?? "explicit"
120
+ });
121
+ });
122
+ }
123
+
124
+ function normalizeAutoCrossSize(children, horizontal) {
125
+ const dimension = horizontal ? "height" : "width";
126
+ const mode = `${dimension}Mode`;
127
+ const sharedSize = Math.max(...children.map(child => child[dimension]));
128
+ return children.map(child => cloneAndFreeze({
129
+ ...child,
130
+ [dimension]: child[mode] === "auto" ? sharedSize : child[dimension]
131
+ }));
132
+ }
133
+
134
+ function placementSize(child) {
135
+ return {
136
+ id: child.id,
137
+ width: child.width,
138
+ height: child.height
139
+ };
140
+ }
141
+
142
+ function crossOffset(remaining, align) {
143
+ if (align === "start") return 0;
144
+ if (align === "end") return remaining;
145
+ return remaining / 2;
146
+ }
147
+
148
+ export function resolveCompositionLayout({
149
+ direction,
150
+ children,
151
+ gap = DEFAULT_COMPOSITION_LAYOUT.gap,
152
+ align = DEFAULT_COMPOSITION_LAYOUT.align,
153
+ padding = DEFAULT_COMPOSITION_LAYOUT.padding
154
+ } = {}) {
155
+ const resolvedDirection = normalizeDirection(direction);
156
+ const resolvedChildren = normalizeCompositionChildren(children);
157
+ const resolvedGap = validateCompositionSpacing(gap, "Composition gap");
158
+ const resolvedAlign = normalizeCompositionAlign(align);
159
+ const resolvedPadding = normalizeCompositionPadding(padding);
160
+ const horizontal = resolvedDirection === "horizontal";
161
+ const sizedChildren = normalizeAutoCrossSize(resolvedChildren, horizontal);
162
+ const contentWidth = horizontal
163
+ ? sizedChildren.reduce((sum, child) => sum + child.width, 0) +
164
+ resolvedGap * (sizedChildren.length - 1)
165
+ : Math.max(...sizedChildren.map(child => child.width));
166
+ const contentHeight = horizontal
167
+ ? Math.max(...sizedChildren.map(child => child.height))
168
+ : sizedChildren.reduce((sum, child) => sum + child.height, 0) +
169
+ resolvedGap * (sizedChildren.length - 1);
170
+ let cursor = horizontal ? resolvedPadding.left : resolvedPadding.top;
171
+ const placements = sizedChildren.map(child => {
172
+ const placement = horizontal
173
+ ? {
174
+ ...placementSize(child),
175
+ x: cursor,
176
+ y: resolvedPadding.top + crossOffset(
177
+ contentHeight - child.height,
178
+ resolvedAlign
179
+ )
180
+ }
181
+ : {
182
+ ...placementSize(child),
183
+ x: resolvedPadding.left + crossOffset(
184
+ contentWidth - child.width,
185
+ resolvedAlign
186
+ ),
187
+ y: cursor
188
+ };
189
+ cursor += (horizontal ? child.width : child.height) + resolvedGap;
190
+ return placement;
191
+ });
192
+ return cloneAndFreeze({
193
+ direction: resolvedDirection,
194
+ gap: resolvedGap,
195
+ align: resolvedAlign,
196
+ padding: resolvedPadding,
197
+ width: resolvedPadding.left + contentWidth + resolvedPadding.right,
198
+ height: resolvedPadding.top + contentHeight + resolvedPadding.bottom,
199
+ children: placements
200
+ });
201
+ }
202
+
203
+ export function resolveCompositionSnapshotPlacement({
204
+ direction,
205
+ align,
206
+ placement,
207
+ width,
208
+ height
209
+ } = {}) {
210
+ const resolvedDirection = normalizeDirection(direction);
211
+ const resolvedAlign = normalizeCompositionAlign(align);
212
+ validatePlacedBounds(placement, "Composition snapshot placement");
213
+ if (!Number.isFinite(width) || width <= 0) {
214
+ throw new RangeError("Composition snapshot width must be a positive finite number.");
215
+ }
216
+ if (!Number.isFinite(height) || height <= 0) {
217
+ throw new RangeError("Composition snapshot height must be a positive finite number.");
218
+ }
219
+ if (width > placement.width || height > placement.height) {
220
+ throw new RangeError("Composition snapshot must fit its resolved placement.");
221
+ }
222
+ const horizontal = resolvedDirection === "horizontal";
223
+ return cloneAndFreeze({
224
+ x: placement.x + (horizontal
225
+ ? 0
226
+ : crossOffset(placement.width - width, resolvedAlign)),
227
+ y: placement.y + (horizontal
228
+ ? crossOffset(placement.height - height, resolvedAlign)
229
+ : 0),
230
+ width,
231
+ height
232
+ });
233
+ }
234
+
235
+ function validatePlacedBounds(value, label) {
236
+ if (!isPlainObject(value)) {
237
+ throw new TypeError(`${label} must be a plain object.`);
238
+ }
239
+ for (const property of ["x", "y", "width", "height"]) {
240
+ if (!Number.isFinite(value[property])) {
241
+ throw new TypeError(`${label}.${property} must be finite.`);
242
+ }
243
+ }
244
+ if (value.width <= 0 || value.height <= 0) {
245
+ throw new RangeError(`${label} width and height must be positive.`);
246
+ }
247
+ return value;
248
+ }
249
+
250
+ export function resolvePlacedPlotBounds({ placements, plots } = {}) {
251
+ if (!Array.isArray(placements) || placements.length === 0) {
252
+ throw new TypeError("Placed plot bounds require non-empty placements.");
253
+ }
254
+ if (!Array.isArray(plots) || plots.length !== placements.length) {
255
+ throw new TypeError("Placed plot bounds require one local plot per placement.");
256
+ }
257
+ const plotById = new Map();
258
+ for (const [index, plot] of plots.entries()) {
259
+ if (!isPlainObject(plot) || typeof plot.id !== "string" || plot.id.length === 0) {
260
+ throw new TypeError(`Placed plot ${index} requires a non-empty id.`);
261
+ }
262
+ if (plotById.has(plot.id)) {
263
+ throw new Error(`Duplicate placed plot id "${plot.id}".`);
264
+ }
265
+ plotById.set(plot.id, validatePlacedBounds(plot, `Placed plot "${plot.id}"`));
266
+ }
267
+ const translated = placements.map((placement, index) => {
268
+ if (
269
+ !isPlainObject(placement) ||
270
+ typeof placement.id !== "string" ||
271
+ placement.id.length === 0
272
+ ) {
273
+ throw new TypeError(`Plot placement ${index} requires a non-empty id.`);
274
+ }
275
+ validatePlacedBounds(placement, `Plot placement "${placement.id}"`);
276
+ const plot = plotById.get(placement.id);
277
+ if (plot === undefined) {
278
+ throw new Error(`Missing local plot bounds for "${placement.id}".`);
279
+ }
280
+ if (
281
+ plot.x < 0 || plot.y < 0 ||
282
+ plot.x + plot.width > placement.width ||
283
+ plot.y + plot.height > placement.height
284
+ ) {
285
+ throw new RangeError(
286
+ `Local plot bounds for "${placement.id}" must fit its placement.`
287
+ );
288
+ }
289
+ plotById.delete(placement.id);
290
+ return {
291
+ left: placement.x + plot.x,
292
+ top: placement.y + plot.y,
293
+ right: placement.x + plot.x + plot.width,
294
+ bottom: placement.y + plot.y + plot.height
295
+ };
296
+ });
297
+ if (plotById.size > 0) {
298
+ throw new Error(`Unknown placed plot id "${plotById.keys().next().value}".`);
299
+ }
300
+ const left = Math.min(...translated.map(bounds => bounds.left));
301
+ const top = Math.min(...translated.map(bounds => bounds.top));
302
+ const right = Math.max(...translated.map(bounds => bounds.right));
303
+ const bottom = Math.max(...translated.map(bounds => bounds.bottom));
304
+ return cloneAndFreeze({
305
+ x: left,
306
+ y: top,
307
+ width: right - left,
308
+ height: bottom - top
309
+ });
310
+ }
@@ -0,0 +1,131 @@
1
+ import { cloneAndFreeze } from "../core/immutable.js";
2
+ import {
3
+ DEFAULT_COMPOSITION_LAYOUT,
4
+ normalizeCompositionAlign,
5
+ normalizeCompositionChildren,
6
+ normalizeCompositionPadding,
7
+ validateCompositionSpacing
8
+ } from "./composition.js";
9
+
10
+ export const DEFAULT_FACET_LEGEND_GAP = 18;
11
+ export const DEFAULT_FACET_LEGEND_WIDTH = 132;
12
+
13
+ function resolveColumns(columns, count) {
14
+ const value = columns ?? count;
15
+ if (!Number.isInteger(value) || value <= 0) {
16
+ throw new RangeError("Facet columns must be a positive integer.");
17
+ }
18
+ return Math.min(value, count);
19
+ }
20
+
21
+ function alignedOffset(remaining, align) {
22
+ if (align === "start") return 0;
23
+ if (align === "end") return remaining;
24
+ return remaining / 2;
25
+ }
26
+
27
+ export function resolveFacetLayout({
28
+ children,
29
+ columns,
30
+ gap = DEFAULT_COMPOSITION_LAYOUT.gap,
31
+ align = DEFAULT_COMPOSITION_LAYOUT.align,
32
+ padding = DEFAULT_COMPOSITION_LAYOUT.padding,
33
+ titleHeight = 0,
34
+ sharedLegend = false,
35
+ sharedLegendGap = DEFAULT_FACET_LEGEND_GAP,
36
+ sharedLegendWidth = DEFAULT_FACET_LEGEND_WIDTH
37
+ } = {}) {
38
+ const values = children?.map(child => child?.value);
39
+ const resolvedChildren = normalizeCompositionChildren(children?.map(
40
+ ({ value: _value, ...child }) => child
41
+ ));
42
+ const resolvedColumns = resolveColumns(columns, resolvedChildren.length);
43
+ const resolvedGap = validateCompositionSpacing(gap, "Facet gap");
44
+ const resolvedAlign = normalizeCompositionAlign(align);
45
+ const resolvedPadding = normalizeCompositionPadding(padding);
46
+ const resolvedTitleHeight = validateCompositionSpacing(
47
+ titleHeight,
48
+ "Facet title height"
49
+ );
50
+ if (typeof sharedLegend !== "boolean") {
51
+ throw new TypeError("Facet sharedLegend must be a boolean.");
52
+ }
53
+ const legendGap = validateCompositionSpacing(
54
+ sharedLegendGap,
55
+ "Facet shared legend gap"
56
+ );
57
+ const legendWidth = validateCompositionSpacing(
58
+ sharedLegendWidth,
59
+ "Facet shared legend width"
60
+ );
61
+ const rowCount = Math.ceil(resolvedChildren.length / resolvedColumns);
62
+ const columnWidths = Array.from({ length: resolvedColumns }, (_, column) =>
63
+ Math.max(...resolvedChildren
64
+ .filter((_, index) => index % resolvedColumns === column)
65
+ .map(child => child.width))
66
+ );
67
+ const rowHeights = Array.from({ length: rowCount }, (_, row) =>
68
+ Math.max(...resolvedChildren
69
+ .slice(row * resolvedColumns, (row + 1) * resolvedColumns)
70
+ .map(child => child.height))
71
+ );
72
+ const columnStarts = columnWidths.map((_, column) =>
73
+ resolvedPadding.left + columnWidths
74
+ .slice(0, column)
75
+ .reduce((sum, width) => sum + width, 0) + resolvedGap * column
76
+ );
77
+ const rowStarts = rowHeights.map((_, row) =>
78
+ resolvedTitleHeight + resolvedPadding.top + rowHeights
79
+ .slice(0, row)
80
+ .reduce((sum, height) => sum + height, 0) + resolvedGap * row
81
+ );
82
+ const placements = resolvedChildren.map((child, index) => {
83
+ const column = index % resolvedColumns;
84
+ const row = Math.floor(index / resolvedColumns);
85
+ return {
86
+ id: child.id,
87
+ value: values[index],
88
+ column,
89
+ row,
90
+ x: columnStarts[column] + alignedOffset(
91
+ columnWidths[column] - child.width,
92
+ resolvedAlign
93
+ ),
94
+ y: rowStarts[row] + alignedOffset(
95
+ rowHeights[row] - child.height,
96
+ resolvedAlign
97
+ ),
98
+ width: child.width,
99
+ height: child.height
100
+ };
101
+ });
102
+ const gridWidth = resolvedPadding.left +
103
+ columnWidths.reduce((sum, width) => sum + width, 0) +
104
+ resolvedGap * Math.max(0, resolvedColumns - 1) +
105
+ resolvedPadding.right;
106
+ const gridHeight = resolvedTitleHeight + resolvedPadding.top +
107
+ rowHeights.reduce((sum, height) => sum + height, 0) +
108
+ resolvedGap * Math.max(0, rowCount - 1) +
109
+ resolvedPadding.bottom;
110
+ return cloneAndFreeze({
111
+ columns: resolvedColumns,
112
+ rows: rowCount,
113
+ gap: resolvedGap,
114
+ align: resolvedAlign,
115
+ padding: resolvedPadding,
116
+ titleHeight: resolvedTitleHeight,
117
+ gridWidth,
118
+ width: gridWidth + (sharedLegend
119
+ ? legendGap + legendWidth
120
+ : 0),
121
+ height: gridHeight,
122
+ children: placements,
123
+ ...(sharedLegend ? {
124
+ legend: {
125
+ x: gridWidth + legendGap,
126
+ y: resolvedTitleHeight + resolvedPadding.top + 30,
127
+ width: legendWidth
128
+ }
129
+ } : {})
130
+ });
131
+ }
@@ -55,6 +55,12 @@ export function alignedTitleAnchor(start, length, blockLength, align) {
55
55
  return start + length - blockLength / 2;
56
56
  }
57
57
 
58
+ export function alignedTextAnchor(start, length, align) {
59
+ if (align === "left") return start;
60
+ if (align === "center") return start + length / 2;
61
+ return start + length;
62
+ }
63
+
58
64
  function axisAlignedTextBounds({ x, y, text, style, align, rotation }) {
59
65
  const width = measureTextWidth(text, style);
60
66
  const left = align === "left" ? 0 : align === "center" ? -width / 2 : -width;
@@ -4,7 +4,10 @@ import {
4
4
  resolveBarChannels,
5
5
  resolveBarColorLayout
6
6
  } from "../../grammar/bars/policy.js";
7
- import { layoutSeriesPartition } from "../../grammar/seriesLayout.js";
7
+ import {
8
+ DEFAULT_SERIES_BASELINE,
9
+ layoutSeriesPartition
10
+ } from "../../grammar/seriesLayout.js";
8
11
  import {
9
12
  isDiscretePositionScaleType,
10
13
  mapContinuousScaleValues,
@@ -66,7 +69,7 @@ export function deriveAggregateRectangles(required, resolved, widthConfig) {
66
69
  widthConfig,
67
70
  Math.abs(categoryScale.bandwidth ?? categoryScale.step)
68
71
  );
69
- const baseline = layout === "overlay" ? measureScale.domain[0] : 0;
72
+ const baseline = DEFAULT_SERIES_BASELINE;
70
73
  const continuousColor = colorEncoding?.fieldType === "quantitative";
71
74
  if (continuousColor) {
72
75
  const mappedColors = mapSequentialColors(
@@ -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.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"