ggaction 0.0.3 → 0.0.5

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 (223) hide show
  1. package/CHANGELOG.md +58 -3
  2. package/README.md +117 -83
  3. package/package.json +27 -11
  4. package/src/actions/boxPlots/create.js +13 -7
  5. package/src/actions/boxPlots/edit.js +31 -35
  6. package/src/actions/boxPlots/materialize.js +3 -0
  7. package/src/actions/boxPlots/options.js +8 -1
  8. package/src/actions/boxPlots/resolve.js +10 -2
  9. package/src/actions/charts/bar.js +50 -0
  10. package/src/actions/charts/heatmap.js +205 -0
  11. package/src/actions/charts/histogram.js +58 -0
  12. package/src/actions/charts/index.js +15 -0
  13. package/src/actions/charts/line.js +69 -0
  14. package/src/actions/charts/parallel.js +76 -0
  15. package/src/actions/charts/scatter.js +56 -0
  16. package/src/actions/charts/shared.js +121 -0
  17. package/src/actions/coordinates/parallel.js +50 -0
  18. package/src/actions/data/bin2d.js +137 -0
  19. package/src/actions/data/density.js +54 -13
  20. package/src/actions/data/derived.js +22 -0
  21. package/src/actions/data/filter.js +5 -0
  22. package/src/actions/data/gradientProfile.js +65 -0
  23. package/src/actions/data/horizon.js +71 -0
  24. package/src/actions/data/index.js +29 -2
  25. package/src/actions/data/regression.js +1 -1
  26. package/src/actions/data/window.js +50 -0
  27. package/src/actions/encodings/appearance.js +1 -1
  28. package/src/actions/encodings/color/continuous.js +137 -0
  29. package/src/actions/encodings/color/index.js +167 -0
  30. package/src/actions/encodings/color/layout.js +99 -0
  31. package/src/actions/encodings/color/policy.js +88 -0
  32. package/src/actions/encodings/density/resolve.js +109 -0
  33. package/src/actions/encodings/density.js +193 -88
  34. package/src/actions/encodings/horizon/resolve.js +283 -0
  35. package/src/actions/encodings/horizon.js +364 -0
  36. package/src/actions/encodings/index.js +8 -2
  37. package/src/actions/encodings/offset.js +1 -1
  38. package/src/actions/encodings/parallel.js +115 -0
  39. package/src/actions/encodings/pathOrder.js +131 -0
  40. package/src/actions/encodings/position/apply.js +73 -4
  41. package/src/actions/encodings/{position.js → position/index.js} +21 -8
  42. package/src/actions/encodings/position/policies/arc.js +27 -4
  43. package/src/actions/encodings/position/policies/area.js +33 -2
  44. package/src/actions/encodings/position/policies/index.js +7 -2
  45. package/src/actions/encodings/position/policies/line.js +18 -2
  46. package/src/actions/encodings/position/resolve.js +25 -3
  47. package/src/actions/encodings/ranged.js +5 -3
  48. package/src/actions/encodings/ruleAppearance.js +88 -10
  49. package/src/actions/encodings/strokeDash.js +1 -1
  50. package/src/actions/encodings/text.js +1 -1
  51. package/src/actions/errorBands/options.js +1 -1
  52. package/src/actions/errorBars/options.js +1 -1
  53. package/src/actions/facets/actions.js +2 -3
  54. package/src/actions/facets/derive.js +35 -5
  55. package/src/actions/facets/guides.js +1 -1
  56. package/src/actions/facets/replay.js +7 -36
  57. package/src/actions/gradientPlots/components.js +177 -0
  58. package/src/actions/gradientPlots/create.js +96 -0
  59. package/src/actions/gradientPlots/edit.js +153 -0
  60. package/src/actions/gradientPlots/index.js +24 -0
  61. package/src/actions/gradientPlots/materialize.js +287 -0
  62. package/src/actions/gradientPlots/options.js +120 -0
  63. package/src/actions/gradientPlots/paint.js +107 -0
  64. package/src/actions/gradientPlots/rebind.js +51 -0
  65. package/src/actions/gradientPlots/resolve.js +81 -0
  66. package/src/actions/guides/applicability.js +44 -17
  67. package/src/actions/guides/axes/axes.js +37 -3
  68. package/src/actions/guides/axes/index.js +2 -0
  69. package/src/actions/guides/axes/labels.js +12 -7
  70. package/src/actions/guides/axes/parallel/resolve.js +86 -0
  71. package/src/actions/guides/axes/parallel.js +228 -0
  72. package/src/actions/guides/axes/ticks.js +1 -1
  73. package/src/actions/guides/axes/titles.js +12 -9
  74. package/src/actions/guides/grids/resolve.js +1 -1
  75. package/src/actions/guides/guides.js +60 -5
  76. package/src/actions/guides/legends/categorical/actions.js +48 -11
  77. package/src/actions/guides/legends/categorical/layout.js +4 -2
  78. package/src/actions/guides/legends/continuous/interval.js +1 -1
  79. package/src/actions/guides/legends/continuous/opacity.js +1 -1
  80. package/src/actions/guides/legends/edit.js +7 -20
  81. package/src/actions/guides/legends/index.js +2 -0
  82. package/src/actions/guides/legends/remove.js +6 -31
  83. package/src/actions/guides/legends/size.js +1 -1
  84. package/src/actions/guides/legends/strokeWidth.js +170 -0
  85. package/src/actions/guides/legends/target.js +25 -0
  86. package/src/actions/guides/polar/{axes.js → axes/index.js} +5 -5
  87. package/src/actions/guides/polar/index.js +1 -1
  88. package/src/actions/guides/polar/resolve.js +1 -1
  89. package/src/actions/guides/tickValues.js +1 -1
  90. package/src/actions/index.js +6 -0
  91. package/src/actions/intervals/resolve.js +1 -1
  92. package/src/actions/marks/arc/actions.js +2 -2
  93. package/src/actions/marks/area/actions.js +61 -137
  94. package/src/actions/marks/area/materialize.js +163 -0
  95. package/src/actions/marks/bar/edit.js +1 -1
  96. package/src/actions/marks/index.js +5 -1
  97. package/src/actions/marks/line/actions.js +100 -81
  98. package/src/actions/marks/line/materialize.js +146 -0
  99. package/src/actions/marks/point/create.js +76 -0
  100. package/src/actions/marks/point/edit.js +85 -0
  101. package/src/actions/marks/point/index.js +11 -1
  102. package/src/actions/marks/point/jitter.js +72 -0
  103. package/src/actions/marks/point/{actions.js → materialize.js} +175 -170
  104. package/src/actions/marks/rect/actions.js +4 -1
  105. package/src/actions/marks/remove.js +13 -2
  106. package/src/actions/marks/rule/actions.js +26 -4
  107. package/src/actions/marks/shared.js +1 -1
  108. package/src/actions/marks/text/actions.js +20 -5
  109. package/src/actions/marks/text/index.js +7 -1
  110. package/src/actions/marks/text/layout.js +321 -0
  111. package/src/actions/primitives/semanticValidation/dataset.js +22 -0
  112. package/src/actions/primitives/semanticValidation/guide.js +43 -0
  113. package/src/actions/primitives/semanticValidation/index.js +23 -0
  114. package/src/actions/primitives/semanticValidation/layer.js +74 -0
  115. package/src/actions/primitives/semanticValidation/scale.js +74 -0
  116. package/src/actions/primitives/semanticValidation/shared.js +5 -0
  117. package/src/actions/primitives/semanticValue.js +1 -204
  118. package/src/actions/regression/components.js +1 -1
  119. package/src/actions/regression/create.js +1 -1
  120. package/src/actions/regression/edit.js +20 -22
  121. package/src/actions/scales/consumers/common.js +109 -0
  122. package/src/actions/scales/consumers/families.js +58 -0
  123. package/src/actions/scales/consumers/index.js +41 -0
  124. package/src/actions/scales/{consumers.js → consumers/seriesLayout.js} +10 -132
  125. package/src/actions/scales/create.js +1 -1
  126. package/src/actions/scales/definitions.js +32 -2
  127. package/src/actions/scales/edit.js +8 -206
  128. package/src/actions/scales/editPolicy.js +214 -0
  129. package/src/actions/scales/materialize.js +3 -3
  130. package/src/actions/scales/patch.js +26 -0
  131. package/src/actions/selection/actions.js +28 -3
  132. package/src/actions/violinPlots/create.js +198 -0
  133. package/src/actions/violinPlots/index.js +5 -0
  134. package/src/core/ChartProgram.js +22 -236
  135. package/src/core/compositionState.js +169 -0
  136. package/src/core/materializationState.js +65 -0
  137. package/src/core/programState.js +28 -0
  138. package/src/core/textMetrics.js +47 -0
  139. package/src/core/vocabulary.js +7 -2
  140. package/src/grammar/arcs.js +50 -13
  141. package/src/grammar/areaSeries.js +42 -6
  142. package/src/grammar/bars/aggregate.js +1 -1
  143. package/src/grammar/bin2d.js +333 -0
  144. package/src/grammar/categoricalDensity.js +192 -0
  145. package/src/grammar/coordinates.js +1 -1
  146. package/src/grammar/curveCommands.js +3 -1
  147. package/src/grammar/density.js +253 -27
  148. package/src/grammar/facets/dependencies.js +3 -11
  149. package/src/grammar/{facets.js → facets/index.js} +8 -6
  150. package/src/grammar/gradientProfile.js +213 -0
  151. package/src/grammar/horizon.js +455 -0
  152. package/src/grammar/interval.js +2 -96
  153. package/src/grammar/jitter.js +197 -0
  154. package/src/grammar/lineSeries.js +70 -1
  155. package/src/grammar/paint.js +88 -0
  156. package/src/grammar/palettes.js +5 -7
  157. package/src/grammar/parallelCoordinates.js +213 -0
  158. package/src/grammar/pathOrder.js +35 -0
  159. package/src/grammar/pointShapes.js +24 -0
  160. package/src/grammar/polarLineCommands.js +1 -1
  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 +2 -1
  166. package/src/grammar/scales/appearance.js +21 -0
  167. package/src/grammar/scales/color.js +4 -2
  168. package/src/grammar/schemas/concreteGraphic.js +11 -1
  169. package/src/grammar/schemas/graphicBounds.js +19 -37
  170. package/src/grammar/schemas/semanticPath.js +17 -2
  171. package/src/grammar/statistics/studentT.js +98 -0
  172. package/src/grammar/transforms.js +101 -12
  173. package/src/grammar/window.js +339 -0
  174. package/src/layout/labels.js +270 -0
  175. package/src/layout/text.js +4 -1
  176. package/src/layout/title.js +10 -20
  177. package/src/materialization/bars/aggregate.js +1 -1
  178. package/src/materialization/bars/grouped.js +1 -1
  179. package/src/materialization/bars/histogram.js +1 -1
  180. package/src/materialization/bars/ranged.js +1 -1
  181. package/src/materialization/dataProvenance.js +56 -2
  182. package/src/materialization/density.js +1 -1
  183. package/src/materialization/dependencies.js +15 -101
  184. package/src/materialization/encodings.js +2 -2
  185. package/src/materialization/facetGuides/index.js +2 -0
  186. package/src/materialization/facetGuides/legacyCategorical.js +125 -0
  187. package/src/materialization/facetGuides/placement.js +126 -0
  188. package/src/materialization/facetGuides/preparation.js +188 -0
  189. package/src/materialization/facets.js +1 -1
  190. package/src/materialization/guides/resources.js +74 -24
  191. package/src/materialization/horizon.js +29 -0
  192. package/src/materialization/layout.js +15 -0
  193. package/src/materialization/marks/capabilities.js +161 -0
  194. package/src/materialization/marks/index.js +108 -0
  195. package/src/materialization/marks/pathOrder.js +71 -0
  196. package/src/materialization/marks/policies.js +91 -0
  197. package/src/materialization/rect.js +1 -1
  198. package/src/materialization/rowEncoding.js +1 -1
  199. package/src/materialization/scaleGuideDependencies.js +102 -0
  200. package/src/materialization/scales/map.js +1 -1
  201. package/src/materialization/scales/policies/series.js +1 -1
  202. package/src/materialization/scales/resolve.js +3 -1
  203. package/src/materialization/selection/items/bar.js +1 -1
  204. package/src/materialization/selection/items/path.js +27 -1
  205. package/src/materialization/selection/items/point.js +1 -5
  206. package/src/materialization/selection/items/rect.js +21 -0
  207. package/src/materialization/selection/styles.js +21 -10
  208. package/src/materialization/text.js +12 -4
  209. package/src/renderers/canvas/fill.js +33 -0
  210. package/src/renderers/canvas/index.js +7 -0
  211. package/src/renderers/canvas/path.js +7 -4
  212. package/src/renderers/canvas/rect.js +9 -3
  213. package/src/renderers/canvas/text.js +14 -1
  214. package/src/selectors/datasets.js +4 -0
  215. package/src/theme/defaults.js +4 -0
  216. package/types/extension.d.ts +10 -1
  217. package/types/index.d.ts +76 -0
  218. package/types/program.d.ts +632 -10
  219. package/src/actions/encodings/color.js +0 -424
  220. package/src/grammar/regression.js +0 -578
  221. package/src/grammar/scales.js +0 -1
  222. package/src/materialization/facetGuides.js +0 -426
  223. package/src/materialization/marks.js +0 -316
@@ -0,0 +1,339 @@
1
+ import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
+
3
+ const TRANSFORM_KEYS = Object.freeze([
4
+ "type", "partitionBy", "sortBy", "operations"
5
+ ]);
6
+ const SORT_KEYS = Object.freeze(["field", "order"]);
7
+ const ORDER_VALUES = Object.freeze(["ascending", "descending"]);
8
+ const OPERATION_VALUES = Object.freeze([
9
+ "rowNumber", "rank", "denseRank", "cumulativeSum", "lag", "lead"
10
+ ]);
11
+ const POSITION_OPERATIONS = new Set(["rowNumber", "rank", "denseRank"]);
12
+ const OFFSET_OPERATIONS = new Set(["lag", "lead"]);
13
+
14
+ function requireField(value, label) {
15
+ if (typeof value !== "string" || value.length === 0) {
16
+ throw new TypeError(`${label} must be a non-empty string.`);
17
+ }
18
+ return value;
19
+ }
20
+
21
+ function rejectUnknownKeys(value, supported, label) {
22
+ const unknown = Object.keys(value).find(key => !supported.includes(key));
23
+ if (unknown !== undefined) {
24
+ throw new Error(`Unknown ${label} property "${unknown}".`);
25
+ }
26
+ }
27
+
28
+ function validateFieldList(value, label) {
29
+ if (
30
+ !Array.isArray(value) ||
31
+ value.some(field => typeof field !== "string" || field.length === 0)
32
+ ) {
33
+ throw new TypeError(`${label} must contain field names.`);
34
+ }
35
+ if (new Set(value).size !== value.length) {
36
+ throw new Error(`${label} fields must be unique.`);
37
+ }
38
+ }
39
+
40
+ function validateSortBy(sortBy) {
41
+ if (!Array.isArray(sortBy)) {
42
+ throw new TypeError("Window sortBy must be an array.");
43
+ }
44
+ const fields = [];
45
+ sortBy.forEach((sort, index) => {
46
+ if (!isPlainObject(sort)) {
47
+ throw new TypeError(`Window sortBy[${index}] must be a plain object.`);
48
+ }
49
+ rejectUnknownKeys(sort, SORT_KEYS, `window sortBy[${index}]`);
50
+ fields.push(requireField(sort.field, `Window sortBy[${index}].field`));
51
+ if (!ORDER_VALUES.includes(sort.order)) {
52
+ throw new Error(`Unsupported window sort order "${sort.order}".`);
53
+ }
54
+ });
55
+ if (new Set(fields).size !== fields.length) {
56
+ throw new Error("Window sortBy fields must be unique.");
57
+ }
58
+ }
59
+
60
+ function operationKeys(operation) {
61
+ if (POSITION_OPERATIONS.has(operation.op)) return ["op", "as"];
62
+ if (operation.op === "cumulativeSum") return ["op", "field", "as"];
63
+ if (OFFSET_OPERATIONS.has(operation.op)) {
64
+ return ["op", "field", "as", "offset", "default"];
65
+ }
66
+ return ["op"];
67
+ }
68
+
69
+ function validateOperations(operations, sortBy) {
70
+ if (!Array.isArray(operations) || operations.length === 0) {
71
+ throw new TypeError("Window operations must be a non-empty array.");
72
+ }
73
+ const outputs = new Set();
74
+ operations.forEach((operation, index) => {
75
+ if (!isPlainObject(operation)) {
76
+ throw new TypeError(`Window operation ${index} must be a plain object.`);
77
+ }
78
+ if (!OPERATION_VALUES.includes(operation.op)) {
79
+ throw new Error(`Unsupported window operation "${operation.op}".`);
80
+ }
81
+ rejectUnknownKeys(operation, operationKeys(operation), `window operation ${index}`);
82
+ const output = requireField(operation.as, `Window operation ${index} as`);
83
+ if (outputs.has(output)) {
84
+ throw new Error(`Window output field "${output}" must be unique.`);
85
+ }
86
+ outputs.add(output);
87
+ if (["rank", "denseRank"].includes(operation.op) && sortBy.length === 0) {
88
+ throw new Error(`${operation.op} requires a non-empty window sortBy.`);
89
+ }
90
+ if (!POSITION_OPERATIONS.has(operation.op)) {
91
+ requireField(operation.field, `Window ${operation.op} field`);
92
+ }
93
+ if (OFFSET_OPERATIONS.has(operation.op)) {
94
+ if (!Number.isInteger(operation.offset) || operation.offset <= 0) {
95
+ throw new RangeError(`Window ${operation.op} offset must be a positive integer.`);
96
+ }
97
+ if (!Object.hasOwn(operation, "default")) {
98
+ throw new TypeError(`Window ${operation.op} requires a default value.`);
99
+ }
100
+ }
101
+ });
102
+ }
103
+
104
+ export function validateWindowTransform(transform) {
105
+ if (!isPlainObject(transform)) {
106
+ throw new TypeError("Window transform must be a plain object.");
107
+ }
108
+ rejectUnknownKeys(transform, TRANSFORM_KEYS, "window transform");
109
+ if (transform.type !== "window") {
110
+ throw new Error(`Unsupported window transform "${transform.type}".`);
111
+ }
112
+ validateFieldList(transform.partitionBy, "Window partitionBy");
113
+ validateSortBy(transform.sortBy);
114
+ validateOperations(transform.operations, transform.sortBy);
115
+ return transform;
116
+ }
117
+
118
+ function normalizePartitionBy(value) {
119
+ if (value === undefined) return [];
120
+ return Array.isArray(value) ? [...value] : [value];
121
+ }
122
+
123
+ function normalizeSortBy(value) {
124
+ if (value === undefined) return [];
125
+ if (!Array.isArray(value)) return value;
126
+ return value.map(sort => isPlainObject(sort)
127
+ ? { field: sort.field, order: sort.order ?? "ascending" }
128
+ : sort
129
+ );
130
+ }
131
+
132
+ function normalizeOperations(value) {
133
+ if (!Array.isArray(value)) return value;
134
+ return value.map(operation => {
135
+ if (!isPlainObject(operation) || !OFFSET_OPERATIONS.has(operation.op)) {
136
+ return operation;
137
+ }
138
+ return {
139
+ ...operation,
140
+ offset: operation.offset ?? 1,
141
+ default: Object.hasOwn(operation, "default") ? operation.default : null
142
+ };
143
+ });
144
+ }
145
+
146
+ export function normalizeWindowTransform({
147
+ partitionBy,
148
+ sortBy,
149
+ operations
150
+ } = {}) {
151
+ const transform = {
152
+ type: "window",
153
+ partitionBy: normalizePartitionBy(partitionBy),
154
+ sortBy: normalizeSortBy(sortBy),
155
+ operations: normalizeOperations(operations)
156
+ };
157
+ validateWindowTransform(transform);
158
+ return cloneAndFreeze(transform);
159
+ }
160
+
161
+ function scalarKey(value, label) {
162
+ if (value === null) return "null";
163
+ if (typeof value === "string") return `string:${value.length}:${value}`;
164
+ if (typeof value === "boolean") return `boolean:${value}`;
165
+ if (typeof value === "number" && Number.isFinite(value)) {
166
+ return `number:${Object.is(value, -0) ? 0 : value}`;
167
+ }
168
+ throw new TypeError(
169
+ `${label} must contain null, strings, booleans, or finite numbers.`
170
+ );
171
+ }
172
+
173
+ function compareScalar(left, right, label) {
174
+ const leftMissing = left === null || left === undefined;
175
+ const rightMissing = right === null || right === undefined;
176
+ if (leftMissing || rightMissing) {
177
+ if (leftMissing && rightMissing) return 0;
178
+ return leftMissing ? 1 : -1;
179
+ }
180
+ if (typeof left !== typeof right || !["number", "string", "boolean"].includes(
181
+ typeof left
182
+ )) {
183
+ throw new TypeError(`${label} must contain one comparable primitive type.`);
184
+ }
185
+ if (typeof left === "number" && (!Number.isFinite(left) || !Number.isFinite(right))) {
186
+ throw new TypeError(`${label} must contain finite numbers.`);
187
+ }
188
+ if (left === right) return 0;
189
+ return left < right ? -1 : 1;
190
+ }
191
+
192
+ function compareEntries(left, right, sortBy, stable = true) {
193
+ for (const sort of sortBy) {
194
+ const comparison = compareScalar(
195
+ left.row[sort.field],
196
+ right.row[sort.field],
197
+ `Window sort field "${sort.field}"`
198
+ );
199
+ if (comparison !== 0) {
200
+ return sort.order === "ascending" ? comparison : -comparison;
201
+ }
202
+ }
203
+ return stable ? left.index - right.index : 0;
204
+ }
205
+
206
+ function validateSourceFields(rows, transform) {
207
+ const sourceFields = new Set(rows.flatMap(row => Object.keys(row)));
208
+ for (const field of [...transform.partitionBy, ...transform.sortBy.map(sort => sort.field)]) {
209
+ if (!sourceFields.has(field)) {
210
+ throw new Error(`Window source does not contain field "${field}".`);
211
+ }
212
+ }
213
+ const available = new Set(sourceFields);
214
+ for (const operation of transform.operations) {
215
+ if (
216
+ !POSITION_OPERATIONS.has(operation.op) &&
217
+ !available.has(operation.field)
218
+ ) {
219
+ throw new Error(`Window source does not contain field "${operation.field}".`);
220
+ }
221
+ if (available.has(operation.as)) {
222
+ throw new Error(`Window output field "${operation.as}" already exists.`);
223
+ }
224
+ available.add(operation.as);
225
+ }
226
+ }
227
+
228
+ function validatePartitionSortValues(partition, transform) {
229
+ for (const field of transform.partitionBy) {
230
+ partition.forEach(entry => scalarKey(
231
+ entry.row[field],
232
+ `Window partition field "${field}"`
233
+ ));
234
+ }
235
+ for (const sort of transform.sortBy) {
236
+ const types = new Set(partition.flatMap(({ row }) => {
237
+ const value = row[sort.field];
238
+ if (value === null || value === undefined) return [];
239
+ if (
240
+ !["number", "string", "boolean"].includes(typeof value) ||
241
+ (typeof value === "number" && !Number.isFinite(value))
242
+ ) {
243
+ throw new TypeError(
244
+ `Window sort field "${sort.field}" must contain comparable primitive values.`
245
+ );
246
+ }
247
+ return [typeof value];
248
+ }));
249
+ if (types.size > 1) {
250
+ throw new TypeError(
251
+ `Window sort field "${sort.field}" must contain one comparable primitive type.`
252
+ );
253
+ }
254
+ }
255
+ }
256
+
257
+ function partitionRows(entries, partitionBy) {
258
+ const partitions = new Map();
259
+ for (const entry of entries) {
260
+ const key = partitionBy.length === 0
261
+ ? "all"
262
+ : partitionBy.map(field => scalarKey(
263
+ entry.row[field],
264
+ `Window partition field "${field}"`
265
+ )).join("\0");
266
+ if (!partitions.has(key)) partitions.set(key, []);
267
+ partitions.get(key).push(entry);
268
+ }
269
+ return [...partitions.values()];
270
+ }
271
+
272
+ function applyOperation(partition, operation, sortBy) {
273
+ if (operation.op === "rowNumber") {
274
+ partition.forEach((entry, index) => {
275
+ entry.row[operation.as] = index + 1;
276
+ });
277
+ return;
278
+ }
279
+ if (["rank", "denseRank"].includes(operation.op)) {
280
+ let rank = 1;
281
+ let denseRank = 1;
282
+ partition.forEach((entry, index) => {
283
+ if (
284
+ index > 0 &&
285
+ compareEntries(entry, partition[index - 1], sortBy, false) !== 0
286
+ ) {
287
+ rank = index + 1;
288
+ denseRank += 1;
289
+ }
290
+ entry.row[operation.as] = operation.op === "rank" ? rank : denseRank;
291
+ });
292
+ return;
293
+ }
294
+ if (operation.op === "cumulativeSum") {
295
+ let total = 0;
296
+ for (const entry of partition) {
297
+ const value = entry.row[operation.field];
298
+ if (!Number.isFinite(value)) {
299
+ throw new TypeError(
300
+ `Window cumulativeSum field "${operation.field}" must contain finite numbers.`
301
+ );
302
+ }
303
+ total += value;
304
+ entry.row[operation.as] = total;
305
+ }
306
+ return;
307
+ }
308
+ const direction = operation.op === "lag" ? -1 : 1;
309
+ partition.forEach((entry, index) => {
310
+ const peer = partition[index + direction * operation.offset];
311
+ entry.row[operation.as] = peer === undefined
312
+ ? operation.default
313
+ : peer.row[operation.field];
314
+ });
315
+ }
316
+
317
+ export function deriveWindowRows(rows, transform) {
318
+ if (!Array.isArray(rows)) {
319
+ throw new TypeError("Window source rows must be an array.");
320
+ }
321
+ if (!rows.every(isPlainObject)) {
322
+ throw new TypeError("Window source rows must be plain objects.");
323
+ }
324
+ validateWindowTransform(transform);
325
+ validateSourceFields(rows, transform);
326
+ const entries = rows.map((row, index) => ({ index, row: { ...row } }));
327
+ const partitions = partitionRows(entries, transform.partitionBy);
328
+ for (const partition of partitions) {
329
+ validatePartitionSortValues(partition, transform);
330
+ partition.sort((left, right) => compareEntries(left, right, transform.sortBy));
331
+ }
332
+ for (const operation of transform.operations) {
333
+ for (const partition of partitions) {
334
+ applyOperation(partition, operation, transform.sortBy);
335
+ }
336
+ }
337
+ entries.sort((left, right) => left.index - right.index);
338
+ return cloneAndFreeze(entries.map(entry => entry.row));
339
+ }
@@ -0,0 +1,270 @@
1
+ import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
+ import { resolveTextBounds } from "../core/textMetrics.js";
3
+ import {
4
+ validateNonNegativeFinite,
5
+ validateOptionObject
6
+ } from "../core/validation.js";
7
+
8
+ const AXES = new Set(["x", "y", "both"]);
9
+ const BOUNDS = new Set(["plot", "canvas"]);
10
+ const POLICY_OPTIONS = Object.freeze([
11
+ "axis", "padding", "maxDisplacement", "bounds"
12
+ ]);
13
+
14
+ export const DEFAULT_LABEL_LAYOUT_GEOMETRY = cloneAndFreeze({
15
+ axis: "both",
16
+ padding: 3,
17
+ maxDisplacement: 48,
18
+ bounds: "plot"
19
+ });
20
+
21
+ export function normalizeLabelLayoutGeometry(options = {}) {
22
+ validateOptionObject(options, POLICY_OPTIONS, "label layout");
23
+ const normalized = {
24
+ ...DEFAULT_LABEL_LAYOUT_GEOMETRY,
25
+ ...options
26
+ };
27
+ if (!AXES.has(normalized.axis)) {
28
+ throw new Error(`Unsupported label layout axis "${normalized.axis}".`);
29
+ }
30
+ if (!BOUNDS.has(normalized.bounds)) {
31
+ throw new Error(`Unsupported label layout bounds "${normalized.bounds}".`);
32
+ }
33
+ validateNonNegativeFinite(normalized.padding, "Label layout padding");
34
+ validateNonNegativeFinite(
35
+ normalized.maxDisplacement,
36
+ "Label layout maxDisplacement"
37
+ );
38
+ return cloneAndFreeze(normalized);
39
+ }
40
+
41
+ function validateBounds(bounds) {
42
+ if (
43
+ !isPlainObject(bounds) ||
44
+ ![bounds.left, bounds.right, bounds.top, bounds.bottom].every(Number.isFinite) ||
45
+ bounds.right <= bounds.left ||
46
+ bounds.bottom <= bounds.top
47
+ ) {
48
+ throw new RangeError("Label layout requires ordered finite bounds.");
49
+ }
50
+ }
51
+
52
+ function validateItem(item, ids) {
53
+ if (!isPlainObject(item)) {
54
+ throw new TypeError("Label layout items must be plain objects.");
55
+ }
56
+ if (typeof item.id !== "string" || item.id.length === 0 || ids.has(item.id)) {
57
+ throw new TypeError("Label layout items require unique non-empty ids.");
58
+ }
59
+ if (
60
+ ![item.x, item.y, item.sourceX, item.sourceY, item.fontSize]
61
+ .every(Number.isFinite) ||
62
+ item.fontSize <= 0 ||
63
+ typeof item.text !== "string" ||
64
+ item.text.length === 0
65
+ ) {
66
+ throw new TypeError(
67
+ "Label layout items require text, positive fontSize, and finite geometry."
68
+ );
69
+ }
70
+ ids.add(item.id);
71
+ }
72
+
73
+ function expanded(bounds, padding) {
74
+ const half = padding / 2;
75
+ return {
76
+ left: bounds.left - half,
77
+ right: bounds.right + half,
78
+ top: bounds.top - half,
79
+ bottom: bounds.bottom + half
80
+ };
81
+ }
82
+
83
+ function intersectionArea(first, second) {
84
+ const width = Math.min(first.right, second.right) -
85
+ Math.max(first.left, second.left);
86
+ const height = Math.min(first.bottom, second.bottom) -
87
+ Math.max(first.top, second.top);
88
+ return width > 0 && height > 0 ? width * height : 0;
89
+ }
90
+
91
+ function overflow(bounds, boundary) {
92
+ return Math.max(0, boundary.left - bounds.left) +
93
+ Math.max(0, bounds.right - boundary.right) +
94
+ Math.max(0, boundary.top - bounds.top) +
95
+ Math.max(0, bounds.bottom - boundary.bottom);
96
+ }
97
+
98
+ export function enumerateLabelOffsets({ axis, padding, maxDisplacement }) {
99
+ const policy = normalizeLabelLayoutGeometry({ axis, padding, maxDisplacement });
100
+ const step = Math.max(2, policy.padding);
101
+ const limit = Math.ceil(policy.maxDisplacement / step);
102
+ const candidates = [];
103
+ for (let x = -limit; x <= limit; x += 1) {
104
+ for (let y = -limit; y <= limit; y += 1) {
105
+ if (policy.axis === "x" && y !== 0) continue;
106
+ if (policy.axis === "y" && x !== 0) continue;
107
+ const offset = { x: x * step, y: y * step };
108
+ const distanceSquared = offset.x ** 2 + offset.y ** 2;
109
+ if (Math.sqrt(distanceSquared) > policy.maxDisplacement + 1e-9) continue;
110
+ candidates.push({ ...offset, distanceSquared });
111
+ }
112
+ }
113
+ candidates.sort((first, second) =>
114
+ first.distanceSquared - second.distanceSquared ||
115
+ Math.abs(first.x) - Math.abs(second.x) ||
116
+ first.y - second.y ||
117
+ second.x - first.x
118
+ );
119
+ return cloneAndFreeze(candidates);
120
+ }
121
+
122
+ function textBounds(item, offset = { x: 0, y: 0 }) {
123
+ return resolveTextBounds({
124
+ x: item.x + offset.x,
125
+ y: item.y + offset.y,
126
+ text: item.text,
127
+ fontSize: item.fontSize,
128
+ textAlign: item.textAlign,
129
+ textBaseline: item.textBaseline,
130
+ rotation: item.rotation
131
+ });
132
+ }
133
+
134
+ function candidateScore(bounds, boundary, placed, candidate, order) {
135
+ return {
136
+ outside: overflow(bounds, boundary),
137
+ overlap: placed.reduce(
138
+ (area, prior) => area + intersectionArea(bounds, prior),
139
+ 0
140
+ ),
141
+ distanceSquared: candidate.distanceSquared,
142
+ order
143
+ };
144
+ }
145
+
146
+ function compareScore(first, second) {
147
+ return Number(first.outside > 0) - Number(second.outside > 0) ||
148
+ first.outside - second.outside ||
149
+ first.overlap - second.overlap ||
150
+ first.distanceSquared - second.distanceSquared ||
151
+ first.order - second.order;
152
+ }
153
+
154
+ function overlapPairs(items) {
155
+ const pairs = [];
156
+ for (let first = 0; first < items.length; first += 1) {
157
+ for (let second = first + 1; second < items.length; second += 1) {
158
+ if (
159
+ intersectionArea(
160
+ items[first].collisionBounds,
161
+ items[second].collisionBounds
162
+ ) > 0
163
+ ) {
164
+ pairs.push([items[first].id, items[second].id]);
165
+ }
166
+ }
167
+ }
168
+ return pairs;
169
+ }
170
+
171
+ export function resolveLabelLayout({ items, bounds, ...options } = {}) {
172
+ if (!Array.isArray(items)) {
173
+ throw new TypeError("Label layout requires an item array.");
174
+ }
175
+ validateBounds(bounds);
176
+ const policy = normalizeLabelLayoutGeometry(options);
177
+ const ids = new Set();
178
+ for (const item of items) validateItem(item, ids);
179
+ const candidates = enumerateLabelOffsets(policy);
180
+ const base = items.map(item => ({
181
+ id: item.id,
182
+ collisionBounds: expanded(textBounds(item), policy.padding)
183
+ }));
184
+ const placed = [];
185
+ const resolved = [];
186
+ for (const item of items) {
187
+ let best;
188
+ for (const [order, candidate] of candidates.entries()) {
189
+ const boundsAtCandidate = textBounds(item, candidate);
190
+ const collisionBounds = expanded(boundsAtCandidate, policy.padding);
191
+ const score = candidateScore(
192
+ collisionBounds,
193
+ bounds,
194
+ placed,
195
+ candidate,
196
+ order
197
+ );
198
+ if (best === undefined || compareScore(score, best.score) < 0) {
199
+ best = { candidate, bounds: boundsAtCandidate, collisionBounds, score };
200
+ }
201
+ if (score.outside === 0 && score.overlap === 0) break;
202
+ }
203
+ const resolvedItem = {
204
+ ...item,
205
+ baseX: item.x,
206
+ baseY: item.y,
207
+ x: item.x + best.candidate.x,
208
+ y: item.y + best.candidate.y,
209
+ dx: best.candidate.x,
210
+ dy: best.candidate.y,
211
+ distance: Math.sqrt(best.candidate.distanceSquared),
212
+ bounds: best.bounds,
213
+ collisionBounds: best.collisionBounds
214
+ };
215
+ placed.push(resolvedItem.collisionBounds);
216
+ resolved.push(resolvedItem);
217
+ }
218
+ const beforePairs = overlapPairs(base);
219
+ const afterPairs = overlapPairs(resolved);
220
+ const outside = resolved
221
+ .filter(item => overflow(item.collisionBounds, bounds) > 0)
222
+ .map(item => item.id);
223
+ const warnings = [];
224
+ if (afterPairs.length > 0) warnings.push({ code: "overlap", pairs: afterPairs });
225
+ if (outside.length > 0) warnings.push({ code: "bounds", items: outside });
226
+ return cloneAndFreeze({
227
+ items: resolved,
228
+ overlapBefore: beforePairs.length,
229
+ overlapAfter: afterPairs.length,
230
+ warnings
231
+ });
232
+ }
233
+
234
+ function contains(bounds, point) {
235
+ return point.x >= bounds.left && point.x <= bounds.right &&
236
+ point.y >= bounds.top && point.y <= bounds.bottom;
237
+ }
238
+
239
+ export function resolveLabelLeader(item) {
240
+ if (!isPlainObject(item) || ![item.dx, item.dy].every(Number.isFinite)) {
241
+ throw new TypeError("Label leader requires a resolved label item.");
242
+ }
243
+ if (item.dx === 0 && item.dy === 0) return undefined;
244
+ const source = { x: item.sourceX, y: item.sourceY };
245
+ if (contains(item.bounds, source)) return undefined;
246
+ const center = {
247
+ x: (item.bounds.left + item.bounds.right) / 2,
248
+ y: (item.bounds.top + item.bounds.bottom) / 2
249
+ };
250
+ const dx = center.x - source.x;
251
+ const dy = center.y - source.y;
252
+ const enterX = dx > 0
253
+ ? (item.bounds.left - source.x) / dx
254
+ : dx < 0
255
+ ? (item.bounds.right - source.x) / dx
256
+ : 0;
257
+ const enterY = dy > 0
258
+ ? (item.bounds.top - source.y) / dy
259
+ : dy < 0
260
+ ? (item.bounds.bottom - source.y) / dy
261
+ : 0;
262
+ const ratio = Math.max(0, Math.min(1, Math.max(enterX, enterY)));
263
+ return cloneAndFreeze({
264
+ id: item.id,
265
+ x1: source.x,
266
+ y1: source.y,
267
+ x2: source.x + dx * ratio,
268
+ y2: source.y + dy * ratio
269
+ });
270
+ }
@@ -1,6 +1,9 @@
1
1
  import { measureTextWidth } from "../core/textMetrics.js";
2
2
 
3
- export { measureTextWidth } from "../core/textMetrics.js";
3
+ export {
4
+ measureTextWidth,
5
+ resolveTextBounds
6
+ } from "../core/textMetrics.js";
4
7
 
5
8
  function wrapCodePoints(text, maxWidth, style) {
6
9
  const lines = [];
@@ -1,4 +1,4 @@
1
- import { measureTextWidth, wrapText } from "./text.js";
1
+ import { measureTextWidth, resolveTextBounds, wrapText } from "./text.js";
2
2
 
3
3
  function lineCenters(lines, style, lineHeight, start) {
4
4
  return lines.map((_, index) => start + style.fontSize / 2 + index * lineHeight);
@@ -62,25 +62,15 @@ export function alignedTextAnchor(start, length, align) {
62
62
  }
63
63
 
64
64
  function axisAlignedTextBounds({ x, y, text, style, align, rotation }) {
65
- const width = measureTextWidth(text, style);
66
- const left = align === "left" ? 0 : align === "center" ? -width / 2 : -width;
67
- const right = left + width;
68
- const top = -style.fontSize / 2;
69
- const bottom = style.fontSize / 2;
70
- const cosine = Math.cos(rotation);
71
- const sine = Math.sin(rotation);
72
- const corners = [
73
- [left, top], [right, top], [right, bottom], [left, bottom]
74
- ].map(([localX, localY]) => ({
75
- x: x + localX * cosine - localY * sine,
76
- y: y + localX * sine + localY * cosine
77
- }));
78
- return {
79
- left: Math.min(...corners.map(point => point.x)),
80
- right: Math.max(...corners.map(point => point.x)),
81
- top: Math.min(...corners.map(point => point.y)),
82
- bottom: Math.max(...corners.map(point => point.y))
83
- };
65
+ return resolveTextBounds({
66
+ x,
67
+ y,
68
+ text,
69
+ ...style,
70
+ textAlign: align,
71
+ textBaseline: "middle",
72
+ rotation
73
+ });
84
74
  }
85
75
 
86
76
  function unionBounds(bounds) {
@@ -13,7 +13,7 @@ import {
13
13
  mapContinuousScaleValues,
14
14
  mapOrdinalPositionValues,
15
15
  mapSequentialColors
16
- } from "../../grammar/scales.js";
16
+ } from "../../grammar/scales/index.js";
17
17
  import {
18
18
  DEFAULT_BAR_FILL,
19
19
  DEFAULT_BAR_STROKE,
@@ -2,7 +2,7 @@ import { deriveBarAggregates } from "../../grammar/bars/aggregate.js";
2
2
  import {
3
3
  mapContinuousScaleValues,
4
4
  mapOrdinalPositionValues
5
- } from "../../grammar/scales.js";
5
+ } from "../../grammar/scales/index.js";
6
6
  import { sameOrderedValues } from "../../core/validation.js";
7
7
  import {
8
8
  DEFAULT_BAR_STROKE,
@@ -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