ggaction 0.0.1

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 (222) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/LICENSE +21 -0
  3. package/README.md +108 -0
  4. package/package.json +84 -0
  5. package/src/AGENTS.md +119 -0
  6. package/src/ChartProgram.js +10 -0
  7. package/src/actions/boxPlots/components.js +88 -0
  8. package/src/actions/boxPlots/create.js +84 -0
  9. package/src/actions/boxPlots/index.js +10 -0
  10. package/src/actions/boxPlots/materialize.js +183 -0
  11. package/src/actions/boxPlots/options.js +147 -0
  12. package/src/actions/boxPlots/resolve.js +38 -0
  13. package/src/actions/canvas/actions.js +144 -0
  14. package/src/actions/canvas/index.js +6 -0
  15. package/src/actions/coordinates/actions.js +85 -0
  16. package/src/actions/coordinates/index.js +5 -0
  17. package/src/actions/data/box.js +40 -0
  18. package/src/actions/data/create.js +33 -0
  19. package/src/actions/data/density.js +62 -0
  20. package/src/actions/data/derived.js +52 -0
  21. package/src/actions/data/filter.js +168 -0
  22. package/src/actions/data/index.js +32 -0
  23. package/src/actions/data/interval.js +69 -0
  24. package/src/actions/data/regression.js +71 -0
  25. package/src/actions/data/shared.js +25 -0
  26. package/src/actions/encodings/appearance.js +243 -0
  27. package/src/actions/encodings/barWidth.js +60 -0
  28. package/src/actions/encodings/color.js +329 -0
  29. package/src/actions/encodings/density.js +225 -0
  30. package/src/actions/encodings/histogram.js +50 -0
  31. package/src/actions/encodings/index.js +23 -0
  32. package/src/actions/encodings/offset.js +104 -0
  33. package/src/actions/encodings/position/policies/area.js +24 -0
  34. package/src/actions/encodings/position/policies/bar.js +110 -0
  35. package/src/actions/encodings/position/policies/common.js +17 -0
  36. package/src/actions/encodings/position/policies/index.js +25 -0
  37. package/src/actions/encodings/position/policies/line.js +74 -0
  38. package/src/actions/encodings/position/policies/point.js +19 -0
  39. package/src/actions/encodings/position/policies/rule.js +17 -0
  40. package/src/actions/encodings/position/resolve.js +170 -0
  41. package/src/actions/encodings/position.js +176 -0
  42. package/src/actions/encodings/ranged.js +276 -0
  43. package/src/actions/encodings/ruleAppearance.js +47 -0
  44. package/src/actions/encodings/shared.js +164 -0
  45. package/src/actions/encodings/strokeDash.js +145 -0
  46. package/src/actions/errorBands/components.js +81 -0
  47. package/src/actions/errorBands/create.js +159 -0
  48. package/src/actions/errorBands/index.js +7 -0
  49. package/src/actions/errorBands/resolve.js +28 -0
  50. package/src/actions/errorBars/components.js +62 -0
  51. package/src/actions/errorBars/create.js +144 -0
  52. package/src/actions/errorBars/index.js +7 -0
  53. package/src/actions/errorBars/resolve.js +25 -0
  54. package/src/actions/guides/axes/axes.js +204 -0
  55. package/src/actions/guides/axes/axis.js +131 -0
  56. package/src/actions/guides/axes/index.js +17 -0
  57. package/src/actions/guides/axes/labels.js +231 -0
  58. package/src/actions/guides/axes/lines.js +149 -0
  59. package/src/actions/guides/axes/policy.js +155 -0
  60. package/src/actions/guides/axes/tickGroups.js +149 -0
  61. package/src/actions/guides/axes/ticks.js +179 -0
  62. package/src/actions/guides/axes/titles.js +264 -0
  63. package/src/actions/guides/grids/grid.js +228 -0
  64. package/src/actions/guides/grids/index.js +1 -0
  65. package/src/actions/guides/grids/resolve.js +258 -0
  66. package/src/actions/guides/guides.js +138 -0
  67. package/src/actions/guides/index.js +11 -0
  68. package/src/actions/guides/legends/categorical/actions.js +329 -0
  69. package/src/actions/guides/legends/categorical/components.js +140 -0
  70. package/src/actions/guides/legends/categorical/index.js +49 -0
  71. package/src/actions/guides/legends/categorical/layout.js +445 -0
  72. package/src/actions/guides/legends/categorical/options.js +188 -0
  73. package/src/actions/guides/legends/categorical/recipes.js +120 -0
  74. package/src/actions/guides/legends/categorical/resolve.js +131 -0
  75. package/src/actions/guides/legends/categorical/symbols.js +280 -0
  76. package/src/actions/guides/legends/categorical/validation.js +27 -0
  77. package/src/actions/guides/legends/continuous/common.js +273 -0
  78. package/src/actions/guides/legends/continuous/gradient.js +363 -0
  79. package/src/actions/guides/legends/continuous/index.js +25 -0
  80. package/src/actions/guides/legends/continuous/interval.js +234 -0
  81. package/src/actions/guides/legends/continuous/opacity.js +382 -0
  82. package/src/actions/guides/legends/edit.js +314 -0
  83. package/src/actions/guides/legends/index.js +9 -0
  84. package/src/actions/guides/legends/size.js +220 -0
  85. package/src/actions/guides/tickValues.js +84 -0
  86. package/src/actions/index.js +31 -0
  87. package/src/actions/intervals/resolve.js +352 -0
  88. package/src/actions/marks/area.js +369 -0
  89. package/src/actions/marks/bar/create.js +46 -0
  90. package/src/actions/marks/bar/edit.js +90 -0
  91. package/src/actions/marks/bar/index.js +9 -0
  92. package/src/actions/marks/bar/materialize.js +130 -0
  93. package/src/actions/marks/index.js +13 -0
  94. package/src/actions/marks/line.js +248 -0
  95. package/src/actions/marks/point.js +420 -0
  96. package/src/actions/marks/rule.js +287 -0
  97. package/src/actions/marks/shared.js +112 -0
  98. package/src/actions/primitives/createGraphics.js +163 -0
  99. package/src/actions/primitives/editGraphics.js +208 -0
  100. package/src/actions/primitives/index.js +9 -0
  101. package/src/actions/primitives/semantic.js +168 -0
  102. package/src/actions/primitives/semanticValue.js +182 -0
  103. package/src/actions/regression/components.js +216 -0
  104. package/src/actions/regression/create.js +102 -0
  105. package/src/actions/regression/index.js +23 -0
  106. package/src/actions/regression/resolve.js +57 -0
  107. package/src/actions/scales/consumers.js +283 -0
  108. package/src/actions/scales/create.js +148 -0
  109. package/src/actions/scales/definitions.js +319 -0
  110. package/src/actions/scales/edit.js +290 -0
  111. package/src/actions/scales/index.js +11 -0
  112. package/src/actions/scales/materialize.js +466 -0
  113. package/src/actions/scales/quantitativeColor.js +45 -0
  114. package/src/actions/selection/actions.js +384 -0
  115. package/src/actions/selection/index.js +23 -0
  116. package/src/actions/titles/actions.js +235 -0
  117. package/src/actions/titles/index.js +19 -0
  118. package/src/actions/titles/resolve.js +321 -0
  119. package/src/core/ChartProgram.js +316 -0
  120. package/src/core/action.js +120 -0
  121. package/src/core/identifiers.js +28 -0
  122. package/src/core/immutable.js +94 -0
  123. package/src/core/specs.js +29 -0
  124. package/src/core/textMetrics.js +23 -0
  125. package/src/core/validation.js +54 -0
  126. package/src/core/vocabulary.js +58 -0
  127. package/src/extension.js +2 -0
  128. package/src/grammar/aggregate.js +259 -0
  129. package/src/grammar/areaSeries.js +217 -0
  130. package/src/grammar/bars/aggregate.js +98 -0
  131. package/src/grammar/bars/geometry.js +54 -0
  132. package/src/grammar/bars/policy.js +90 -0
  133. package/src/grammar/boxPlot.js +140 -0
  134. package/src/grammar/coordinates.js +25 -0
  135. package/src/grammar/curveCommands.js +356 -0
  136. package/src/grammar/density.js +296 -0
  137. package/src/grammar/filter.js +169 -0
  138. package/src/grammar/histogram.js +332 -0
  139. package/src/grammar/interval.js +343 -0
  140. package/src/grammar/lineSeries.js +197 -0
  141. package/src/grammar/markFilter.js +46 -0
  142. package/src/grammar/markSelection.js +326 -0
  143. package/src/grammar/palettes.js +208 -0
  144. package/src/grammar/pathCommands.js +86 -0
  145. package/src/grammar/pointShapes.js +164 -0
  146. package/src/grammar/positionCompatibility.js +32 -0
  147. package/src/grammar/regression.js +578 -0
  148. package/src/grammar/ruleAppearance.js +12 -0
  149. package/src/grammar/rules.js +90 -0
  150. package/src/grammar/scales/appearance.js +185 -0
  151. package/src/grammar/scales/color.js +349 -0
  152. package/src/grammar/scales/continuous.js +111 -0
  153. package/src/grammar/scales/definition.js +153 -0
  154. package/src/grammar/scales/discretized.js +159 -0
  155. package/src/grammar/scales/fields.js +133 -0
  156. package/src/grammar/scales/index.js +12 -0
  157. package/src/grammar/scales/mapping.js +27 -0
  158. package/src/grammar/scales/ordinal.js +221 -0
  159. package/src/grammar/scales/policies.js +50 -0
  160. package/src/grammar/scales/temporal.js +53 -0
  161. package/src/grammar/scales/transformed.js +236 -0
  162. package/src/grammar/scales/types.js +102 -0
  163. package/src/grammar/scales/validation.js +66 -0
  164. package/src/grammar/scales.js +1 -0
  165. package/src/grammar/schemas/concreteGraphic.js +73 -0
  166. package/src/grammar/schemas/graphic.js +131 -0
  167. package/src/grammar/schemas/graphicBounds.js +146 -0
  168. package/src/grammar/schemas/graphicTree.js +133 -0
  169. package/src/grammar/schemas/mark.js +4 -0
  170. package/src/grammar/schemas/semanticPath.js +174 -0
  171. package/src/grammar/seriesLayout.js +76 -0
  172. package/src/grammar/ticks.js +149 -0
  173. package/src/grammar/transforms.js +37 -0
  174. package/src/index.js +2 -0
  175. package/src/layout/canvas.js +114 -0
  176. package/src/layout/grid.js +17 -0
  177. package/src/layout/legend.js +51 -0
  178. package/src/layout/text.js +62 -0
  179. package/src/layout/title.js +107 -0
  180. package/src/materialization/bars/aggregate.js +184 -0
  181. package/src/materialization/bars/grouped.js +117 -0
  182. package/src/materialization/bars/histogram.js +163 -0
  183. package/src/materialization/bars/ranged.js +53 -0
  184. package/src/materialization/bars/resolve.js +56 -0
  185. package/src/materialization/dataProvenance.js +15 -0
  186. package/src/materialization/density.js +29 -0
  187. package/src/materialization/dependencies.js +134 -0
  188. package/src/materialization/encodings.js +50 -0
  189. package/src/materialization/graphicHierarchy.js +104 -0
  190. package/src/materialization/legends.js +20 -0
  191. package/src/materialization/marks.js +110 -0
  192. package/src/materialization/planner.js +38 -0
  193. package/src/materialization/selection/filter.js +40 -0
  194. package/src/materialization/selection/items.js +503 -0
  195. package/src/materialization/selection/path.js +37 -0
  196. package/src/materialization/selection/point.js +118 -0
  197. package/src/materialization/selection/policies/area.js +10 -0
  198. package/src/materialization/selection/policies/bar.js +10 -0
  199. package/src/materialization/selection/policies/index.js +40 -0
  200. package/src/materialization/selection/policies/line.js +10 -0
  201. package/src/materialization/selection/policies/point.js +10 -0
  202. package/src/materialization/selection/policies/rule.js +10 -0
  203. package/src/materialization/selection/state.js +67 -0
  204. package/src/materialization/selection/styles.js +183 -0
  205. package/src/renderers/canvas/circle.js +62 -0
  206. package/src/renderers/canvas/index.js +132 -0
  207. package/src/renderers/canvas/line.js +54 -0
  208. package/src/renderers/canvas/path.js +98 -0
  209. package/src/renderers/canvas/rect.js +57 -0
  210. package/src/renderers/canvas/text.js +96 -0
  211. package/src/renderers/canvas/validation.js +23 -0
  212. package/src/renderers/png.js +29 -0
  213. package/src/selectors/coordinates.js +13 -0
  214. package/src/selectors/datasets.js +13 -0
  215. package/src/selectors/index.js +4 -0
  216. package/src/selectors/layers.js +32 -0
  217. package/src/selectors/scales.js +21 -0
  218. package/src/theme/defaults.js +13 -0
  219. package/types/extension.d.ts +14 -0
  220. package/types/index.d.ts +81 -0
  221. package/types/png.d.ts +14 -0
  222. package/types/program.d.ts +1096 -0
@@ -0,0 +1,343 @@
1
+ import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
+ import { aggregateScalarValues } from "./aggregate.js";
3
+
4
+ const CENTER_VALUES = Object.freeze(["mean", "median"]);
5
+ const EXTENT_VALUES = Object.freeze(["stderr", "stdev", "ci", "iqr"]);
6
+ const TRANSFORM_KEYS = Object.freeze([
7
+ "type",
8
+ "field",
9
+ "groupBy",
10
+ "center",
11
+ "extent",
12
+ "level",
13
+ "as"
14
+ ]);
15
+ const OUTPUT_KEYS = Object.freeze(["center", "lower", "upper"]);
16
+
17
+ function nonEmptyString(value, label) {
18
+ if (typeof value !== "string" || value.length === 0) {
19
+ throw new TypeError(`${label} must be a non-empty string.`);
20
+ }
21
+ }
22
+
23
+ function validateGrouping(groupBy) {
24
+ if (
25
+ !Array.isArray(groupBy) ||
26
+ !groupBy.every(field => typeof field === "string" && field.length > 0) ||
27
+ new Set(groupBy).size !== groupBy.length
28
+ ) {
29
+ throw new TypeError(
30
+ "Interval groupBy must contain unique field names."
31
+ );
32
+ }
33
+ }
34
+
35
+ function validateOutputs(as, occupied) {
36
+ if (!isPlainObject(as)) {
37
+ throw new TypeError("Interval as must be a plain object.");
38
+ }
39
+ const keys = Object.keys(as);
40
+ if (
41
+ keys.length !== OUTPUT_KEYS.length ||
42
+ !OUTPUT_KEYS.every(key => Object.hasOwn(as, key))
43
+ ) {
44
+ throw new Error("Interval as requires exactly center, lower, and upper.");
45
+ }
46
+ const values = OUTPUT_KEYS.map(key => as[key]);
47
+ values.forEach((value, index) => {
48
+ nonEmptyString(value, `Interval as.${OUTPUT_KEYS[index]}`);
49
+ });
50
+ if (new Set(values).size !== values.length) {
51
+ throw new Error("Interval output fields must be distinct.");
52
+ }
53
+ if (values.some(value => occupied.has(value))) {
54
+ throw new Error("Interval output fields must not collide with input fields.");
55
+ }
56
+ }
57
+
58
+ export function validateIntervalTransform(transform) {
59
+ if (!isPlainObject(transform)) {
60
+ throw new TypeError("Interval transform must be a plain object.");
61
+ }
62
+ const unknown = Object.keys(transform).find(
63
+ key => !TRANSFORM_KEYS.includes(key)
64
+ );
65
+ if (unknown !== undefined) {
66
+ throw new Error(`Unknown interval transform property "${unknown}".`);
67
+ }
68
+ if (transform.type !== "interval") {
69
+ throw new Error(`Unsupported interval transform "${transform.type}".`);
70
+ }
71
+ nonEmptyString(transform.field, "Interval field");
72
+ validateGrouping(transform.groupBy);
73
+ if (!CENTER_VALUES.includes(transform.center)) {
74
+ throw new Error(`Unsupported interval center "${transform.center}".`);
75
+ }
76
+ if (!EXTENT_VALUES.includes(transform.extent)) {
77
+ throw new Error(`Unsupported interval extent "${transform.extent}".`);
78
+ }
79
+ if (
80
+ (transform.center === "median") !== (transform.extent === "iqr")
81
+ ) {
82
+ throw new Error("Median intervals require iqr, and iqr requires median.");
83
+ }
84
+ if (transform.extent === "ci") {
85
+ if (
86
+ !Number.isFinite(transform.level) ||
87
+ transform.level <= 0 ||
88
+ transform.level >= 1
89
+ ) {
90
+ throw new RangeError("Interval CI level must be between 0 and 1.");
91
+ }
92
+ } else if (transform.level !== undefined) {
93
+ throw new Error("Interval level is supported only for ci extent.");
94
+ }
95
+ validateOutputs(
96
+ transform.as,
97
+ new Set([transform.field, ...transform.groupBy])
98
+ );
99
+ }
100
+
101
+ function normalizeGrouping(groupBy) {
102
+ const values = groupBy === undefined
103
+ ? []
104
+ : Array.isArray(groupBy) ? [...groupBy] : [groupBy];
105
+ validateGrouping(values);
106
+ return values;
107
+ }
108
+
109
+ export function normalizeIntervalParameters({
110
+ center = "mean",
111
+ extent = "ci",
112
+ level
113
+ } = {}) {
114
+ const resolvedLevel = extent === "ci" ? level ?? 0.95 : level;
115
+ const candidate = {
116
+ type: "interval",
117
+ field: "__interval_input",
118
+ groupBy: [],
119
+ center,
120
+ extent,
121
+ ...(resolvedLevel === undefined ? {} : { level: resolvedLevel }),
122
+ as: {
123
+ center: "__interval_center",
124
+ lower: "__interval_lower",
125
+ upper: "__interval_upper"
126
+ }
127
+ };
128
+ validateIntervalTransform(candidate);
129
+ return cloneAndFreeze({
130
+ center,
131
+ extent,
132
+ ...(resolvedLevel === undefined ? {} : { level: resolvedLevel })
133
+ });
134
+ }
135
+
136
+ const LANCZOS = Object.freeze([
137
+ 676.5203681218851,
138
+ -1259.1392167224028,
139
+ 771.3234287776531,
140
+ -176.6150291621406,
141
+ 12.507343278686905,
142
+ -0.13857109526572012,
143
+ 9.984369578019572e-6,
144
+ 1.5056327351493116e-7
145
+ ]);
146
+
147
+ function logGamma(value) {
148
+ if (value < 0.5) {
149
+ return Math.log(Math.PI) - Math.log(Math.sin(Math.PI * value)) -
150
+ logGamma(1 - value);
151
+ }
152
+ const shifted = value - 1;
153
+ let sum = 0.9999999999998099;
154
+ for (const [index, coefficient] of LANCZOS.entries()) {
155
+ sum += coefficient / (shifted + index + 1);
156
+ }
157
+ const t = shifted + LANCZOS.length - 0.5;
158
+ return 0.5 * Math.log(2 * Math.PI) +
159
+ (shifted + 0.5) * Math.log(t) - t + Math.log(sum);
160
+ }
161
+
162
+ function betaFraction(a, b, x) {
163
+ const maximumIterations = 200;
164
+ const epsilon = 3e-14;
165
+ const floor = 1e-300;
166
+ const sum = a + b;
167
+ let c = 1;
168
+ let d = 1 - sum * x / (a + 1);
169
+ if (Math.abs(d) < floor) d = floor;
170
+ d = 1 / d;
171
+ let result = d;
172
+
173
+ for (let iteration = 1; iteration <= maximumIterations; iteration += 1) {
174
+ const even = 2 * iteration;
175
+ let coefficient = iteration * (b - iteration) * x /
176
+ ((a + even - 1) * (a + even));
177
+ d = 1 + coefficient * d;
178
+ if (Math.abs(d) < floor) d = floor;
179
+ c = 1 + coefficient / c;
180
+ if (Math.abs(c) < floor) c = floor;
181
+ d = 1 / d;
182
+ result *= d * c;
183
+
184
+ coefficient = -(a + iteration) * (sum + iteration) * x /
185
+ ((a + even) * (a + even + 1));
186
+ d = 1 + coefficient * d;
187
+ if (Math.abs(d) < floor) d = floor;
188
+ c = 1 + coefficient / c;
189
+ if (Math.abs(c) < floor) c = floor;
190
+ d = 1 / d;
191
+ const delta = d * c;
192
+ result *= delta;
193
+ if (Math.abs(delta - 1) <= epsilon) return result;
194
+ }
195
+ throw new Error("Student-t calculation did not converge.");
196
+ }
197
+
198
+ function regularizedBeta(x, a, b) {
199
+ if (x <= 0) return 0;
200
+ if (x >= 1) return 1;
201
+ const front = Math.exp(
202
+ logGamma(a + b) - logGamma(a) - logGamma(b) +
203
+ a * Math.log(x) + b * Math.log1p(-x)
204
+ );
205
+ return x < (a + 1) / (a + b + 2)
206
+ ? front * betaFraction(a, b, x) / a
207
+ : 1 - front * betaFraction(b, a, 1 - x) / b;
208
+ }
209
+
210
+ function studentTCdf(value, degreesOfFreedom) {
211
+ if (value === 0) return 0.5;
212
+ const x = degreesOfFreedom /
213
+ (degreesOfFreedom + value * value);
214
+ const tail = 0.5 * regularizedBeta(
215
+ x,
216
+ degreesOfFreedom / 2,
217
+ 0.5
218
+ );
219
+ return value > 0 ? 1 - tail : tail;
220
+ }
221
+
222
+ export function studentTCritical(degreesOfFreedom, level) {
223
+ if (!Number.isInteger(degreesOfFreedom) || degreesOfFreedom < 1) {
224
+ throw new RangeError("Student-t degreesOfFreedom must be a positive integer.");
225
+ }
226
+ if (!Number.isFinite(level) || level <= 0 || level >= 1) {
227
+ throw new RangeError("Student-t level must be between 0 and 1.");
228
+ }
229
+ const target = 0.5 + level / 2;
230
+ let low = 0;
231
+ let high = 1;
232
+ while (studentTCdf(high, degreesOfFreedom) < target) high *= 2;
233
+ for (let iteration = 0; iteration < 100; iteration += 1) {
234
+ const midpoint = (low + high) / 2;
235
+ if (studentTCdf(midpoint, degreesOfFreedom) < target) low = midpoint;
236
+ else high = midpoint;
237
+ }
238
+ return (low + high) / 2;
239
+ }
240
+
241
+ function isMissing(value) {
242
+ return value === undefined || value === null ||
243
+ (typeof value === "number" && Number.isNaN(value));
244
+ }
245
+
246
+ function isGroupValue(value) {
247
+ return typeof value === "string" || typeof value === "boolean" ||
248
+ (typeof value === "number" && Number.isFinite(value));
249
+ }
250
+
251
+ function stableNumber(value) {
252
+ return Number(value.toFixed(12));
253
+ }
254
+
255
+ function deriveGroup(values, transform) {
256
+ if (transform.center === "median") {
257
+ if (values.length === 0) return undefined;
258
+ return {
259
+ center: aggregateScalarValues(values, "median"),
260
+ lower: aggregateScalarValues(values, "q1"),
261
+ upper: aggregateScalarValues(values, "q3")
262
+ };
263
+ }
264
+ if (values.length < 2) return undefined;
265
+ const center = aggregateScalarValues(values, "mean");
266
+ const spread = transform.extent === "stdev"
267
+ ? aggregateScalarValues(values, "stdev")
268
+ : transform.extent === "stderr"
269
+ ? aggregateScalarValues(values, "stderr")
270
+ : studentTCritical(values.length - 1, transform.level) *
271
+ aggregateScalarValues(values, "stderr");
272
+ return { center, lower: center - spread, upper: center + spread };
273
+ }
274
+
275
+ export function deriveInterval(rows, transform) {
276
+ if (!Array.isArray(rows)) {
277
+ throw new TypeError("Interval rows must be an array.");
278
+ }
279
+ validateIntervalTransform(transform);
280
+ const groups = new Map();
281
+ for (const row of rows) {
282
+ if (!isPlainObject(row)) continue;
283
+ const value = row[transform.field];
284
+ if (isMissing(value) || !Number.isFinite(value)) {
285
+ if (value !== undefined && value !== null && typeof value !== "number") {
286
+ throw new TypeError(
287
+ `Interval field "${transform.field}" must contain numeric or missing values.`
288
+ );
289
+ }
290
+ continue;
291
+ }
292
+ const groupValues = transform.groupBy.map(field => row[field]);
293
+ if (groupValues.some(isMissing)) continue;
294
+ if (!groupValues.every(isGroupValue)) {
295
+ throw new TypeError("Interval grouping fields must contain nominal values.");
296
+ }
297
+ const key = JSON.stringify(groupValues);
298
+ if (!groups.has(key)) {
299
+ groups.set(key, {
300
+ fields: Object.fromEntries(
301
+ transform.groupBy.map((field, index) => [field, groupValues[index]])
302
+ ),
303
+ values: []
304
+ });
305
+ }
306
+ groups.get(key).values.push(value);
307
+ }
308
+
309
+ const output = [];
310
+ for (const group of groups.values()) {
311
+ const result = deriveGroup(group.values, transform);
312
+ if (result === undefined) continue;
313
+ output.push({
314
+ ...group.fields,
315
+ [transform.as.center]: stableNumber(result.center),
316
+ [transform.as.lower]: stableNumber(result.lower),
317
+ [transform.as.upper]: stableNumber(result.upper)
318
+ });
319
+ }
320
+ return cloneAndFreeze(output);
321
+ }
322
+
323
+ export function normalizeIntervalTransform({
324
+ field,
325
+ groupBy,
326
+ center,
327
+ extent,
328
+ level,
329
+ as
330
+ }) {
331
+ nonEmptyString(field, "Interval field");
332
+ const grouping = normalizeGrouping(groupBy);
333
+ const parameters = normalizeIntervalParameters({ center, extent, level });
334
+ const transform = {
335
+ type: "interval",
336
+ field,
337
+ groupBy: grouping,
338
+ ...parameters,
339
+ as
340
+ };
341
+ validateIntervalTransform(transform);
342
+ return cloneAndFreeze(transform);
343
+ }
@@ -0,0 +1,197 @@
1
+ import { cloneAndFreeze } from "../core/immutable.js";
2
+ import {
3
+ readNominalField,
4
+ readQuantitativeField,
5
+ readTemporalField
6
+ } from "./scales.js";
7
+ import {
8
+ aggregateRows,
9
+ isAggregate,
10
+ validateAggregateFieldType,
11
+ validateAggregateFieldValues,
12
+ } from "./aggregate.js";
13
+
14
+ const SERIES_CHANNELS = Object.freeze(["group", "color", "strokeDash"]);
15
+
16
+ function requireLineEncoding(layer) {
17
+ if (layer?.mark?.type !== "line") {
18
+ throw new Error("Line series derivation requires a semantic line mark.");
19
+ }
20
+
21
+ const x = layer.encoding?.x;
22
+ const y = layer.encoding?.y;
23
+
24
+ if (x === undefined) {
25
+ throw new Error(`Line mark "${layer.id}" requires a temporal x encoding.`);
26
+ }
27
+
28
+ const aggregateMode =
29
+ x?.fieldType === "temporal" && isAggregate(y?.aggregate);
30
+ const directQuantitative =
31
+ x?.fieldType === "quantitative" &&
32
+ y?.fieldType === "quantitative" &&
33
+ y.aggregate === undefined;
34
+ const directTemporal =
35
+ y?.aggregate === undefined &&
36
+ ((x?.fieldType === "temporal" && y?.fieldType === "quantitative") ||
37
+ (x?.fieldType === "quantitative" && y?.fieldType === "temporal"));
38
+ if (!aggregateMode && !directQuantitative && !directTemporal) {
39
+ if (x?.fieldType === "temporal") {
40
+ throw new Error(
41
+ `Line mark "${layer.id}" requires a supported aggregate y encoding.`
42
+ );
43
+ }
44
+ throw new Error(
45
+ `Line mark "${layer.id}" requires temporal/aggregate or direct quantitative x/y encodings.`
46
+ );
47
+ }
48
+ if (aggregateMode) {
49
+ validateAggregateFieldType(y.aggregate, y.fieldType);
50
+ }
51
+ return {
52
+ x,
53
+ y,
54
+ isAggregate: aggregateMode,
55
+ directTemporal
56
+ };
57
+ }
58
+
59
+ function readSeriesFields(rows, layer) {
60
+ const fields = [];
61
+ const values = new Map();
62
+
63
+ for (const channel of SERIES_CHANNELS) {
64
+ const encoding = layer.encoding?.[channel];
65
+
66
+ if (encoding?.field === undefined) continue;
67
+ if (encoding.fieldType !== "nominal") {
68
+ throw new Error(`Line ${channel} encoding on mark "${layer.id}" must be nominal.`);
69
+ }
70
+
71
+ if (!values.has(encoding.field)) {
72
+ fields.push(encoding.field);
73
+ values.set(encoding.field, readNominalField(rows, encoding.field));
74
+ }
75
+ }
76
+
77
+ return { fields, values };
78
+ }
79
+
80
+ function groupKey(values) {
81
+ return JSON.stringify(values);
82
+ }
83
+
84
+ export function deriveLineSeries(rows, layer) {
85
+ const { x, y, isAggregate, directTemporal } = requireLineEncoding(layer);
86
+ if (isAggregate) {
87
+ validateAggregateFieldValues(rows, y.field, y.fieldType);
88
+ }
89
+ const xValues = x.fieldType === "temporal"
90
+ ? readTemporalField(rows, x.field)
91
+ : readQuantitativeField(rows, x.field);
92
+ const yValues = isAggregate
93
+ ? undefined
94
+ : y.fieldType === "temporal"
95
+ ? readTemporalField(rows, y.field)
96
+ : readQuantitativeField(rows, y.field);
97
+ const seriesFields = readSeriesFields(rows, layer);
98
+
99
+ if (directTemporal) {
100
+ const groups = new Map();
101
+ for (let index = 0; index < rows.length; index += 1) {
102
+ const dimensions = seriesFields.fields.map(
103
+ field => seriesFields.values.get(field)[index]
104
+ );
105
+ const key = groupKey(dimensions);
106
+ const series = groups.get(key) ?? {
107
+ key: Object.fromEntries(
108
+ seriesFields.fields.map((field, item) => [field, dimensions[item]])
109
+ ),
110
+ values: []
111
+ };
112
+ series.values.push({ x: xValues[index], y: yValues[index] });
113
+ groups.set(key, series);
114
+ }
115
+ const orderBy = x.fieldType === "temporal" ? "x" : "y";
116
+ const series = [...groups.values()].flatMap(item => {
117
+ const values = item.values.sort(
118
+ (left, right) => left[orderBy] - right[orderBy]
119
+ );
120
+ return values.length < 2 ? [] : [{ key: item.key, values }];
121
+ });
122
+ if (series.length === 0) {
123
+ throw new Error(
124
+ `Line series on mark "${layer.id}" requires at least two direct points.`
125
+ );
126
+ }
127
+ return cloneAndFreeze({
128
+ xValues: series.flatMap(item => item.values.map(value => value.x)),
129
+ yValues: series.flatMap(item => item.values.map(value => value.y)),
130
+ series
131
+ });
132
+ }
133
+ const aggregateGroups = new Map();
134
+
135
+ for (let index = 0; index < rows.length; index += 1) {
136
+ const dimensions = seriesFields.fields.map(
137
+ field => seriesFields.values.get(field)[index]
138
+ );
139
+ const key = groupKey([xValues[index], ...dimensions]);
140
+ const group = aggregateGroups.get(key) ?? {
141
+ x: xValues[index],
142
+ dimensions,
143
+ rows: []
144
+ };
145
+
146
+ group.rows.push(rows[index]);
147
+ aggregateGroups.set(key, group);
148
+ }
149
+
150
+ if (aggregateGroups.size === 0) {
151
+ throw new Error(`Line mark "${layer.id}" has no values to aggregate.`);
152
+ }
153
+
154
+ const seriesGroups = new Map();
155
+
156
+ for (const group of aggregateGroups.values()) {
157
+ const value = isAggregate
158
+ ? aggregateRows(group.rows, y.field, y.aggregate)
159
+ : group.rows.reduce((sum, row) => sum + row[y.field], 0);
160
+ if (value === undefined) continue;
161
+ const key = groupKey(group.dimensions);
162
+ const series = seriesGroups.get(key) ?? {
163
+ key: Object.fromEntries(
164
+ seriesFields.fields.map((field, index) => [field, group.dimensions[index]])
165
+ ),
166
+ values: []
167
+ };
168
+
169
+ series.values.push({
170
+ x: group.x,
171
+ y: value
172
+ });
173
+ seriesGroups.set(key, series);
174
+ }
175
+
176
+ const series = [...seriesGroups.values()].flatMap(item => {
177
+ const values = item.values.sort((left, right) => left.x - right.x);
178
+
179
+ if (values.length < 2) {
180
+ return [];
181
+ }
182
+
183
+ return [{ key: item.key, values }];
184
+ });
185
+
186
+ if (series.length === 0) {
187
+ throw new Error(
188
+ `Line series on mark "${layer.id}" requires at least two aggregate points.`
189
+ );
190
+ }
191
+
192
+ return cloneAndFreeze({
193
+ xValues: series.flatMap(item => item.values.map(value => value.x)),
194
+ yValues: series.flatMap(item => item.values.map(value => value.y)),
195
+ series
196
+ });
197
+ }
@@ -0,0 +1,46 @@
1
+ import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
+ import { normalizeMarkSelector } from "./markSelection.js";
3
+
4
+ const TRANSFORM_KEYS = Object.freeze(["type", "target", "selector"]);
5
+
6
+ export function validateMarkFilterTransform(transform) {
7
+ if (!isPlainObject(transform)) {
8
+ throw new TypeError("Mark filter transform must be a plain object.");
9
+ }
10
+ const unknown = Object.keys(transform).find(
11
+ key => !TRANSFORM_KEYS.includes(key)
12
+ );
13
+ if (unknown !== undefined) {
14
+ throw new Error(`Unknown mark filter transform property "${unknown}".`);
15
+ }
16
+ if (transform.type !== "markFilter") {
17
+ throw new Error(`Unsupported mark filter transform "${transform.type}".`);
18
+ }
19
+ if (typeof transform.target !== "string" || transform.target.length === 0) {
20
+ throw new TypeError("Mark filter target must be a non-empty string.");
21
+ }
22
+ normalizeMarkSelector(transform.selector);
23
+ }
24
+
25
+ export function normalizeMarkFilterTransform(target, selector) {
26
+ const transform = {
27
+ type: "markFilter",
28
+ target,
29
+ selector: normalizeMarkSelector(selector)
30
+ };
31
+ validateMarkFilterTransform(transform);
32
+ return cloneAndFreeze(transform);
33
+ }
34
+
35
+ export function deriveMarkFilteredRows(sourceRows, items, selectedKeys) {
36
+ if (!Array.isArray(sourceRows)) {
37
+ throw new TypeError("Mark filter source values must be an array.");
38
+ }
39
+ const selected = new Set(selectedKeys);
40
+ const members = new Set(
41
+ items
42
+ .filter(item => selected.has(item.key))
43
+ .flatMap(item => item.members)
44
+ );
45
+ return cloneAndFreeze(sourceRows.filter(row => members.has(row)));
46
+ }