ggaction 0.0.17 → 0.0.19

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 (315) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +6 -2
  3. package/knowledge/action-card.schema.json +1 -1
  4. package/knowledge/action-cards.json +1 -1
  5. package/knowledge/intent-taxonomy.json +1 -1
  6. package/knowledge/mcp-resources.json +1 -1
  7. package/knowledge/task-resolver.js +1 -1
  8. package/package.json +3 -3
  9. package/src/BasicChartProgram.js +1 -1
  10. package/src/accessibility.js +3 -2
  11. package/src/actions/boxPlots/create.js +14 -4
  12. package/src/actions/boxPlots/edit.js +39 -33
  13. package/src/actions/boxPlots/materialize.js +7 -3
  14. package/src/actions/boxPlots/options.js +37 -11
  15. package/src/actions/canvas/actions.js +70 -95
  16. package/src/actions/canvas/fitting.js +20 -1
  17. package/src/actions/canvas/index.js +3 -0
  18. package/src/actions/canvas/plotBorder.js +28 -0
  19. package/src/actions/charts/area.js +2 -2
  20. package/src/actions/charts/bar.js +34 -7
  21. package/src/actions/charts/ecdf.js +4 -18
  22. package/src/actions/charts/endpoints.js +4 -18
  23. package/src/actions/charts/heatmap.js +8 -7
  24. package/src/actions/charts/index.js +2 -0
  25. package/src/actions/charts/interval-regression.js +2 -2
  26. package/src/actions/charts/line.js +1 -1
  27. package/src/actions/charts/pie.js +3 -47
  28. package/src/actions/charts/radial.js +34 -12
  29. package/src/actions/charts/scatter.js +16 -2
  30. package/src/actions/charts/shared.js +2 -2
  31. package/src/actions/charts/text.js +35 -0
  32. package/src/actions/composition/actions.js +6 -2
  33. package/src/actions/data/box.js +2 -1
  34. package/src/actions/data/derived.js +11 -1
  35. package/src/actions/data/edit.js +3 -1
  36. package/src/actions/data/index.js +4 -0
  37. package/src/actions/data/regression.js +25 -16
  38. package/src/actions/data/regressionStatistics.js +73 -0
  39. package/src/actions/data/shared.js +8 -4
  40. package/src/actions/data/stack.js +3 -1
  41. package/src/actions/distributions/revision.js +24 -18
  42. package/src/actions/encodings/appearance.js +37 -29
  43. package/src/actions/encodings/barWidth.js +36 -11
  44. package/src/actions/encodings/channels.js +0 -1
  45. package/src/actions/encodings/color/continuous.js +1 -1
  46. package/src/actions/encodings/color/index.js +1 -1
  47. package/src/actions/encodings/color/policy.js +10 -3
  48. package/src/actions/encodings/density/resolve.js +1 -6
  49. package/src/actions/encodings/position/policies/arc.js +12 -10
  50. package/src/actions/encodings/position/policies/area.js +3 -3
  51. package/src/actions/encodings/position/policies/bar.js +15 -6
  52. package/src/actions/encodings/position/policies/line.js +17 -6
  53. package/src/actions/encodings/position/resolve.js +3 -2
  54. package/src/actions/encodings/ranged.js +17 -0
  55. package/src/actions/encodings/remove.js +14 -8
  56. package/src/actions/encodings/seriesLayout.js +1 -0
  57. package/src/actions/encodings/shared.js +26 -24
  58. package/src/actions/encodings/text.js +6 -3
  59. package/src/actions/errorBands/create.js +2 -2
  60. package/src/actions/errorBands/edit.js +2 -1
  61. package/src/actions/facets/actions.js +176 -247
  62. package/src/actions/facets/guides.js +2 -1
  63. package/src/actions/gradientPlots/resolve.js +2 -17
  64. package/src/actions/guides/applicability.js +1 -1
  65. package/src/actions/guides/axes/axes.js +30 -11
  66. package/src/actions/guides/axes/axis.js +72 -10
  67. package/src/actions/guides/axes/components.js +13 -13
  68. package/src/actions/guides/axes/edit.js +36 -24
  69. package/src/actions/guides/axes/labels.js +155 -71
  70. package/src/actions/guides/axes/lines.js +27 -24
  71. package/src/actions/guides/axes/parallel/lifecycle.js +1 -1
  72. package/src/actions/guides/axes/parallel/policy.js +8 -7
  73. package/src/actions/guides/axes/remove.js +13 -8
  74. package/src/actions/guides/axes/tickGroups.js +11 -7
  75. package/src/actions/guides/axes/ticks.js +36 -24
  76. package/src/actions/guides/axes/titles.js +107 -69
  77. package/src/actions/guides/basic.js +2 -0
  78. package/src/actions/guides/facadeAxes.js +33 -24
  79. package/src/actions/guides/facadeLegend.js +12 -4
  80. package/src/actions/guides/grids/grid.js +8 -2
  81. package/src/actions/guides/grids/resolve.js +102 -58
  82. package/src/actions/guides/legends/blocks.js +46 -20
  83. package/src/actions/guides/legends/categorical/actions.js +95 -19
  84. package/src/actions/guides/legends/categorical/components.js +15 -18
  85. package/src/actions/guides/legends/categorical/index.js +8 -0
  86. package/src/actions/guides/legends/categorical/layout.js +45 -20
  87. package/src/actions/guides/legends/categorical/options.js +39 -22
  88. package/src/actions/guides/legends/categorical/recipes.js +7 -3
  89. package/src/actions/guides/legends/categorical/resolve.js +11 -4
  90. package/src/actions/guides/legends/categorical/symbols.js +38 -18
  91. package/src/actions/guides/legends/categorical/validation.js +1 -6
  92. package/src/actions/guides/legends/colorSize.js +110 -0
  93. package/src/actions/guides/legends/combine.js +37 -0
  94. package/src/actions/guides/legends/continuous/colorSymbols.js +1 -0
  95. package/src/actions/guides/legends/continuous/common.js +1 -447
  96. package/src/actions/guides/legends/continuous/gradient.js +73 -46
  97. package/src/actions/guides/legends/continuous/interval.js +4 -4
  98. package/src/actions/guides/legends/continuous/opacity.js +33 -21
  99. package/src/actions/guides/legends/creation.js +2 -0
  100. package/src/actions/guides/legends/edit.js +1 -604
  101. package/src/actions/guides/legends/focused.js +8 -7
  102. package/src/actions/guides/legends/group.js +1 -0
  103. package/src/actions/guides/legends/lane.js +1 -516
  104. package/src/actions/guides/legends/lifecycle.js +2 -1
  105. package/src/actions/guides/legends/remove.js +9 -3
  106. package/src/actions/guides/legends/sampling.js +1 -1
  107. package/src/actions/guides/legends/size.js +65 -47
  108. package/src/actions/guides/legends/strokeWidth.js +13 -29
  109. package/src/actions/guides/legends/target.js +2 -79
  110. package/src/actions/guides/legends/transition.js +44 -11
  111. package/src/actions/guides/polar/axes/labels.js +4 -2
  112. package/src/actions/guides/polar/axes/titles.js +7 -5
  113. package/src/actions/guides/polar/resolve.js +6 -14
  114. package/src/actions/guides/tickValues.js +8 -5
  115. package/src/actions/index.js +16 -16
  116. package/src/actions/marks/arc/actions.js +3 -10
  117. package/src/actions/marks/area/actions.js +8 -5
  118. package/src/actions/marks/area/materialize.js +8 -9
  119. package/src/actions/marks/bar/create.js +10 -4
  120. package/src/actions/marks/bar/edit.js +29 -14
  121. package/src/actions/marks/bar/materialize.js +15 -7
  122. package/src/actions/marks/line/actions.js +11 -4
  123. package/src/actions/marks/line/materialize.js +9 -5
  124. package/src/actions/marks/point/edit.js +1 -1
  125. package/src/actions/marks/point/materialize.js +5 -1
  126. package/src/actions/marks/rect/actions.js +3 -3
  127. package/src/actions/marks/remove.js +12 -10
  128. package/src/actions/marks/rule/actions.js +7 -4
  129. package/src/actions/marks/text/actions.js +17 -5
  130. package/src/actions/marks/text/layout.js +3 -0
  131. package/src/actions/primitives/createGraphics.js +3 -2
  132. package/src/actions/primitives/editGraphics.js +15 -11
  133. package/src/actions/primitives/graphicProperties.js +1 -6
  134. package/src/actions/primitives/semanticAction.js +27 -1
  135. package/src/actions/primitives/semanticValidation/guide.js +11 -1
  136. package/src/actions/primitives/semanticValidation/layer.js +8 -0
  137. package/src/actions/regression/create.js +1 -1
  138. package/src/actions/regression/edit.js +18 -6
  139. package/src/actions/regression/resolve.js +2 -6
  140. package/src/actions/scales/channels.js +9 -4
  141. package/src/actions/scales/consumers/families.js +16 -5
  142. package/src/actions/scales/create.js +10 -5
  143. package/src/actions/scales/definitions.js +1 -425
  144. package/src/actions/scales/edit.js +3 -3
  145. package/src/actions/scales/editPolicy.js +12 -3
  146. package/src/actions/scales/materialize.js +1 -1
  147. package/src/actions/scales/patch.js +1 -1
  148. package/src/actions/scales/preview.js +21 -1
  149. package/src/actions/scales/quantitativeColor.js +8 -3
  150. package/src/actions/selection/actions.js +1 -13
  151. package/src/actions/theme/reconcile.js +126 -45
  152. package/src/actions/theme/state.js +12 -0
  153. package/src/actions/titles/actions.js +5 -18
  154. package/src/actions/titles/resolve.js +41 -19
  155. package/src/actions/violinPlots/edit.js +3 -20
  156. package/src/core/compositionState.js +6 -3
  157. package/src/core/font.js +9 -2
  158. package/src/core/graphicProperties.js +1 -0
  159. package/src/core/optionRequirements.js +1 -0
  160. package/src/core/programState.js +4 -0
  161. package/src/core/resourceReferences.js +5 -2
  162. package/src/core/textMetricProfile.js +3 -2
  163. package/src/core/textMetrics.js +41 -4
  164. package/src/core/validation.js +35 -0
  165. package/src/core/vocabulary.js +7 -0
  166. package/src/grammar/aggregate.js +4 -18
  167. package/src/grammar/arcs.js +1 -386
  168. package/src/grammar/areaPaint.js +10 -0
  169. package/src/grammar/areaSeries.js +18 -10
  170. package/src/grammar/axisTextPlacement.js +1 -0
  171. package/src/grammar/bars/geometry.js +30 -3
  172. package/src/grammar/bars/policy.js +12 -1
  173. package/src/grammar/bin.js +2 -12
  174. package/src/grammar/bin2d.js +1 -6
  175. package/src/grammar/boxPlot.js +56 -9
  176. package/src/grammar/categoryOrder.js +1 -6
  177. package/src/grammar/colorSizeLegend.js +39 -0
  178. package/src/grammar/complete.js +15 -19
  179. package/src/grammar/computed.js +115 -26
  180. package/src/grammar/curveCommands.js +29 -16
  181. package/src/grammar/datasetSchema.js +13 -9
  182. package/src/grammar/displayLabels.js +36 -0
  183. package/src/grammar/ecdf.js +2 -9
  184. package/src/grammar/facets/dependencies.js +6 -2
  185. package/src/grammar/facets/guides.js +58 -30
  186. package/src/grammar/facets/index.js +2 -5
  187. package/src/grammar/facets/legendIntent.js +65 -0
  188. package/src/grammar/fold.js +2 -12
  189. package/src/grammar/gradientProfile.js +1 -6
  190. package/src/grammar/guideDash.js +1 -0
  191. package/src/grammar/impute.js +21 -48
  192. package/src/grammar/interval.js +2 -9
  193. package/src/grammar/legendBlocks.js +1 -0
  194. package/src/grammar/legendBorder.js +9 -0
  195. package/src/grammar/legendClip.js +19 -0
  196. package/src/grammar/legendCombinations.js +45 -0
  197. package/src/grammar/legendGradient.js +27 -0
  198. package/src/grammar/legendGroups.js +1 -0
  199. package/src/grammar/legendOpacity.js +6 -0
  200. package/src/grammar/legendTitleStyle.js +22 -0
  201. package/src/grammar/lineSeries.js +32 -17
  202. package/src/grammar/markLabels.js +5 -2
  203. package/src/grammar/normalize.js +4 -54
  204. package/src/grammar/numeric.js +1 -290
  205. package/src/grammar/parallelCoordinates.js +1 -6
  206. package/src/grammar/parseTime.js +41 -0
  207. package/src/grammar/pathCommands.js +11 -4
  208. package/src/grammar/pointShapes.js +16 -2
  209. package/src/grammar/polar.js +1 -184
  210. package/src/grammar/positionCompatibility.js +3 -3
  211. package/src/grammar/regression/derive.js +16 -2
  212. package/src/grammar/regression/models.js +142 -54
  213. package/src/grammar/regression/parameters.js +23 -4
  214. package/src/grammar/regression/statistics.js +64 -0
  215. package/src/grammar/roundedRect.js +42 -54
  216. package/src/grammar/scales/appearance.js +1 -206
  217. package/src/grammar/scales/color.js +1 -382
  218. package/src/grammar/scales/colorConsumers.js +5 -2
  219. package/src/grammar/scales/definition.js +1 -197
  220. package/src/grammar/scales/discretized.js +1 -195
  221. package/src/grammar/scales/size.js +1 -456
  222. package/src/grammar/scales/temporal.js +2 -13
  223. package/src/grammar/scales/transformed.js +1 -383
  224. package/src/grammar/scales/types.js +1 -1
  225. package/src/grammar/scales/validation.js +1 -66
  226. package/src/grammar/schemas/concreteGraphic.js +14 -0
  227. package/src/grammar/schemas/graphic.js +6 -0
  228. package/src/grammar/schemas/graphicBounds.js +31 -7
  229. package/src/grammar/schemas/semanticPath.js +19 -3
  230. package/src/grammar/stack.js +2 -10
  231. package/src/grammar/statisticalReference.js +1 -6
  232. package/src/grammar/summary.js +4 -27
  233. package/src/grammar/text.js +19 -0
  234. package/src/grammar/timeUnit.js +22 -11
  235. package/src/grammar/transformKeys.js +47 -0
  236. package/src/grammar/transformTopology.js +1 -0
  237. package/src/grammar/transforms.js +15 -2
  238. package/src/grammar/valueFormat.js +17 -5
  239. package/src/grammar/weightedStatistics.js +1 -6
  240. package/src/grammar/window.js +4 -58
  241. package/src/inspectionDescriptors.js +1 -1
  242. package/src/layout/canvas.js +1 -110
  243. package/src/layout/facets.js +1 -244
  244. package/src/layout/guideCollisions.js +9 -1
  245. package/src/layout/labels.js +1 -0
  246. package/src/layout/legend.js +13 -0
  247. package/src/layout/legendGroup.js +1 -0
  248. package/src/layout/legendItems.js +59 -15
  249. package/src/layout/legendLane.js +1 -441
  250. package/src/layout/legendText.js +1 -0
  251. package/src/layout/title.js +35 -13
  252. package/src/materialization/bars/aggregate.js +11 -5
  253. package/src/materialization/bars/histogram.js +4 -1
  254. package/src/materialization/bars/ranged.js +19 -6
  255. package/src/materialization/bars/resolve.js +22 -1
  256. package/src/materialization/compositionSnapshot.js +2 -7
  257. package/src/materialization/coordinateBounds.js +21 -0
  258. package/src/materialization/dependencies.js +4 -44
  259. package/src/materialization/facetGuides/edit.js +30 -0
  260. package/src/materialization/facetGuides/layout.js +1 -0
  261. package/src/materialization/facetGuides/owner.js +125 -0
  262. package/src/materialization/facetGuides/ownerLayout.js +71 -0
  263. package/src/materialization/facetGuides/placement.js +71 -14
  264. package/src/materialization/facetGuides/preparation.js +22 -7
  265. package/src/materialization/facetHeaders.js +1 -347
  266. package/src/materialization/facets.js +1 -185
  267. package/src/materialization/graphicHierarchy.js +5 -1
  268. package/src/materialization/graphicTranslation.js +73 -0
  269. package/src/materialization/guides/layout.js +1 -57
  270. package/src/materialization/guides/resources.js +1 -123
  271. package/src/materialization/layout.js +53 -0
  272. package/src/materialization/legendBorder.js +28 -0
  273. package/src/materialization/legendClip.js +76 -0
  274. package/src/materialization/legendGroups.js +1 -0
  275. package/src/materialization/legendText.js +1 -0
  276. package/src/materialization/legends.js +2 -2
  277. package/src/materialization/marks/capabilities.js +5 -6
  278. package/src/materialization/rect.js +1 -1
  279. package/src/materialization/scaleGuideDependencies.js +14 -15
  280. package/src/materialization/scales/map.js +6 -0
  281. package/src/materialization/scales/policies/arc.js +8 -7
  282. package/src/materialization/scales/policies/bar.js +19 -7
  283. package/src/materialization/scales/policies/series.js +2 -1
  284. package/src/materialization/scales/resolve.js +7 -3
  285. package/src/materialization/selection/items/arc.js +1 -1
  286. package/src/materialization/selection/items/bar.js +7 -6
  287. package/src/materialization/selection/items/path.js +7 -6
  288. package/src/materialization/selection/items/rect.js +2 -2
  289. package/src/materialization/text.js +13 -4
  290. package/src/materialization/textLines.js +15 -0
  291. package/src/materialization/typography.js +10 -2
  292. package/src/persistence/codec.js +37 -6
  293. package/src/persistence/validation.js +220 -0
  294. package/src/persistence.js +19 -6
  295. package/src/renderers/canvas/circle.js +6 -3
  296. package/src/renderers/canvas/fill.js +7 -1
  297. package/src/renderers/canvas/index.js +22 -11
  298. package/src/renderers/canvas/isolated.js +166 -0
  299. package/src/renderers/canvas/limits.js +3 -0
  300. package/src/renderers/canvas/line.js +1 -0
  301. package/src/renderers/canvas/path.js +1 -1
  302. package/src/renderers/canvas/rect.js +11 -8
  303. package/src/renderers/canvas/stroke.js +2 -1
  304. package/src/renderers/canvas/text.js +5 -3
  305. package/src/renderers/pdf.js +2 -2
  306. package/src/renderers/png.js +2 -2
  307. package/src/renderers/svg.js +14 -4
  308. package/src/selectors/axes.js +1 -0
  309. package/src/selectors/graphicCapacity.js +38 -0
  310. package/src/selectors/layers.js +19 -0
  311. package/src/theme/defaults.js +8 -2
  312. package/src/version.js +1 -1
  313. package/types/basic.d.ts +3 -0
  314. package/types/index.d.ts +12 -0
  315. package/types/program.d.ts +536 -129
@@ -1,5 +1,5 @@
1
1
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
- import { interpolateNumber, stableDecimal } from "./numeric.js";
2
+ import { quantileSorted, stableDecimal } from "./numeric.js";
3
3
 
4
4
  export const BOX_FIELDS = Object.freeze({
5
5
  q1: "__boxPlot_q1", median: "__boxPlot_median", q3: "__boxPlot_q3",
@@ -9,7 +9,7 @@ export const BOX_FIELDS = Object.freeze({
9
9
  });
10
10
 
11
11
  const TRANSFORM_KEYS = [
12
- "type", "category", "field", "method", "whisker", "factor", "as"
12
+ "type", "category", "field", "method", "whisker", "factor", "as", "summary"
13
13
  ];
14
14
  const OUTPUT_KEYS = Object.keys(BOX_FIELDS);
15
15
 
@@ -18,6 +18,36 @@ function field(value, label) {
18
18
  return value;
19
19
  }
20
20
 
21
+ const SUMMARY_KEYS = Object.freeze(["min", "q1", "median", "q3", "max"]);
22
+
23
+ export function normalizeBoxSummary(value) {
24
+ if (value === undefined) return undefined;
25
+ if (!isPlainObject(value) || Object.keys(value).length !== SUMMARY_KEYS.length ||
26
+ !SUMMARY_KEYS.every(key => Object.hasOwn(value, key))) {
27
+ throw new TypeError("Box summary requires min, q1, median, q3 and max field names.");
28
+ }
29
+ return cloneAndFreeze(Object.fromEntries(SUMMARY_KEYS.map(key => [key, field(value[key], `Box summary.${key}`)])));
30
+ }
31
+
32
+ function derivePrecomputedBoxData(rows, transform) {
33
+ const { category, summary, as } = transform;
34
+ const summaries = [];
35
+ for (const row of rows) {
36
+ const key = row[category];
37
+ if (key === undefined || key === null || key === "") continue;
38
+ const values = SUMMARY_KEYS.map(role => row[summary[role]]);
39
+ if (!values.every(Number.isFinite)) throw new TypeError("Precomputed box summaries require finite values for all five fields.");
40
+ if (values.some((value, index) => index > 0 && value < values[index - 1])) {
41
+ throw new RangeError("Box summary must satisfy min <= q1 <= median <= q3 <= max.");
42
+ }
43
+ const [min, q1, median, q3, max] = values;
44
+ summaries.push({ [category]: key, [as.q1]: q1, [as.median]: median, [as.q3]: q3,
45
+ [as.lowerWhisker]: min, [as.upperWhisker]: max });
46
+ }
47
+ if (summaries.length === 0) throw new Error("Box transform requires at least one valid row.");
48
+ return cloneAndFreeze({ summaries, outliers: [] });
49
+ }
50
+
21
51
  function validateOutputs(as, occupied) {
22
52
  if (!isPlainObject(as)) throw new TypeError("Box as must be a plain object.");
23
53
  const keys = Object.keys(as);
@@ -40,6 +70,18 @@ export function validateBoxTransform(value) {
40
70
  }
41
71
  field(value.category, "Box category field");
42
72
  field(value.field, "Box measure field");
73
+ if (value.method === "precomputed") {
74
+ const summary = normalizeBoxSummary(value.summary);
75
+ if (summary === undefined || value.type !== "boxSummary" || value.field !== summary.median) {
76
+ throw new Error("Precomputed box summaries require a summary mapping and its median measure field.");
77
+ }
78
+ if (value.whisker !== undefined || value.factor !== undefined) {
79
+ throw new Error("Precomputed box summaries do not compute whiskers or factors.");
80
+ }
81
+ validateOutputs(value.as, new Set([value.category, ...Object.values(summary)]));
82
+ return value;
83
+ }
84
+ if (value.summary !== undefined) throw new Error("Box summary fields require the precomputed method.");
43
85
  if (value.method !== "linear") throw new Error(`Unsupported box quantile method "${value.method}".`);
44
86
  const whisker = value.whisker ?? "tukey";
45
87
  if (!["tukey", "minmax"].includes(whisker)) {
@@ -56,6 +98,16 @@ export function validateBoxTransform(value) {
56
98
  }
57
99
 
58
100
  export function normalizeBoxTransform(options = {}) {
101
+ if (options.summary !== undefined) {
102
+ const summary = normalizeBoxSummary(options.summary);
103
+ const normalized = { type: options.type ?? "boxSummary", category: options.category,
104
+ field: options.field ?? summary.median, method: "precomputed", summary, as: options.as ?? BOX_FIELDS };
105
+ if (options.factor !== undefined || (options.whisker !== undefined && options.whisker !== "minmax")) {
106
+ throw new Error("Precomputed box summaries require minmax whiskers without a factor.");
107
+ }
108
+ validateBoxTransform(normalized);
109
+ return cloneAndFreeze(normalized);
110
+ }
59
111
  const {
60
112
  type = "boxSummary",
61
113
  category,
@@ -79,16 +131,11 @@ export function normalizeBoxTransform(options = {}) {
79
131
  return cloneAndFreeze(normalized);
80
132
  }
81
133
 
82
- function quantile(sorted, p) {
83
- const position = (sorted.length - 1) * p;
84
- const lower = Math.floor(position);
85
- const upper = Math.ceil(position);
86
- return interpolateNumber(sorted[lower], sorted[upper], position - lower);
87
- }
88
134
 
89
135
  export function deriveBoxData(rows, transform) {
90
136
  if (!Array.isArray(rows)) throw new TypeError("Box rows must be an array.");
91
137
  validateBoxTransform(transform);
138
+ if (transform.method === "precomputed") return derivePrecomputedBoxData(rows, transform);
92
139
  const { category, field: measure, factor, as } = transform;
93
140
  const whisker = transform.whisker ?? "tukey";
94
141
  const groups = [];
@@ -111,7 +158,7 @@ export function deriveBoxData(rows, transform) {
111
158
  for (const group of groups) {
112
159
  const sorted = group.rows.map(row => row[measure]).sort((a, b) => a - b);
113
160
  const [q1, median, q3] = [0.25, 0.5, 0.75].map(probability =>
114
- stableDecimal(quantile(sorted, probability))
161
+ stableDecimal(quantileSorted(sorted, probability))
115
162
  );
116
163
  const spread = q3 - q1;
117
164
  const lowerFence = whisker === "minmax"
@@ -1,3 +1,4 @@
1
+ import { requireStringValue as nonEmptyField } from "../core/validation.js";
1
2
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
3
  import { isNominalValue, readNominalField } from "./scales/fields.js";
3
4
  import { aggregateScalarValues } from "./aggregate.js";
@@ -7,12 +8,6 @@ export const CATEGORY_ORDER_CHANNELS = Object.freeze(["x", "y", "theta"]);
7
8
  const DIRECTIONS = ["ascending", "descending"];
8
9
  const AGGREGATES = ["sum", "mean", "min", "max"];
9
10
 
10
- function nonEmptyField(value, label) {
11
- if (typeof value !== "string" || value.length === 0) {
12
- throw new TypeError(`${label} must be a non-empty string.`);
13
- }
14
- return value;
15
- }
16
11
 
17
12
  const sameValue = Object.is;
18
13
 
@@ -0,0 +1,39 @@
1
+ import {sameOrderedValues,validateOptionObject,validateUnitInterval,validateNonNegativeFinite,validateNonEmptyString} from '../core/validation.js';
2
+ import {isContinuousColorScaleType} from './scales/types.js';
3
+ import {isContinuousSizeScaleType} from './scales/size.js';
4
+
5
+ export function isColorSizeChannels(channels) {
6
+ return Array.isArray(channels)&&channels.length===2&&channels.includes('color')&&channels.includes('size');
7
+ }
8
+
9
+ export function resolveColorSizeMapping({layer,scales,pointConfig,coordinate}) {
10
+ const color=layer?.encoding?.color,size=layer?.encoding?.size;
11
+ const colorScale=scales[color?.scale],sizeScale=scales[size?.scale];
12
+ if(layer?.mark?.type!=='point'||coordinate?.type!=='cartesian'||
13
+ pointConfig?.shape!=='circle'||layer.encoding.shape!==undefined||
14
+ color?.fieldType!=='quantitative'||size?.fieldType!=='quantitative'||
15
+ typeof color.field!=='string'||color.field!==size.field||
16
+ color.aggregate!==size.aggregate||color.data!==size.data||
17
+ !isContinuousColorScaleType(colorScale?.type)||!isContinuousSizeScaleType(sizeScale?.type)) {
18
+ throw new Error('Color-size combination requires one Cartesian circle point owner with the same quantitative field and grain.');
19
+ }
20
+ const ends=scale=>[scale.domain?.[0],scale.domain?.at(-1)];
21
+ const domain=ends(colorScale),sizeDomain=ends(sizeScale);
22
+ if(!domain.every(Number.isFinite)||!sizeDomain.every(Number.isFinite)||
23
+ !sameOrderedValues(domain,sizeDomain)||domain[0]===domain[1]) {
24
+ throw new Error('Color-size combination requires identical finite numeric domain endpoints.');
25
+ }
26
+ return {target:layer.id,field:color.field,fieldType:'quantitative',colorScale:color.scale,sizeScale:size.scale,domain};
27
+ }
28
+
29
+ export function normalizeColorSizeSymbol(value,previous) {
30
+ if(value===undefined)return {...(previous??{type:'point',strokeWidth:0,opacity:1})};
31
+ if(value==='auto')return {type:'point',strokeWidth:0,opacity:1};
32
+ validateOptionObject(value,['type','stroke','strokeWidth','opacity'],'Color-size symbol');
33
+ if(value.type!==undefined&&value.type!=='point')throw new Error('Color-size symbols require type point.');
34
+ const symbol={type:'point',strokeWidth:0,opacity:1,...previous,...value};
35
+ if(symbol.stroke!==undefined)validateNonEmptyString(symbol.stroke,'Color-size symbol stroke');
36
+ validateNonNegativeFinite(symbol.strokeWidth,'Color-size symbol strokeWidth');
37
+ validateUnitInterval(symbol.opacity,'Color-size symbol opacity');
38
+ return symbol;
39
+ }
@@ -1,3 +1,5 @@
1
+ import { rejectUnknownProperties as rejectUnknownKeys } from "../core/validation.js";
2
+ import { requireStringValue as requireField } from "../core/validation.js";
1
3
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
4
 
3
5
  const TRANSFORM_KEYS = Object.freeze([
@@ -6,17 +8,7 @@ const TRANSFORM_KEYS = Object.freeze([
6
8
  const SEQUENCE_KEYS = Object.freeze(["start", "end", "step"]);
7
9
  const MAX_OUTPUT_ROWS = 10_000;
8
10
 
9
- function rejectUnknownKeys(value, supported, label) {
10
- const unknown = Object.keys(value).find(key => !supported.includes(key));
11
- if (unknown !== undefined) throw new Error(`Unknown ${label} property "${unknown}".`);
12
- }
13
11
 
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
12
 
21
13
  function scalarType(value, label) {
22
14
  if (value === null) return "null";
@@ -258,20 +250,24 @@ export function deriveCompleteRows(rows, transform) {
258
250
  if (!allowed.has(identity)) {
259
251
  throw new Error(`Complete observed key is outside the requested domain at row ${entry.index}.`);
260
252
  }
261
- if (lookup.has(identity)) {
262
- throw new Error("Complete source contains a duplicate group and key combination.");
263
- }
264
- lookup.set(identity, entry);
253
+ if (!lookup.has(identity)) lookup.set(identity, []);
254
+ lookup.get(identity).push(entry);
255
+ }
256
+ const groupSize = group.values.length + domain.length - lookup.size;
257
+ if (output.length + groupSize > MAX_OUTPUT_ROWS) {
258
+ throw new RangeError(`Complete output cannot exceed ${MAX_OUTPUT_ROWS} rows.`);
265
259
  }
266
260
  for (const keyValue of domain) {
267
261
  const identity = scalarKey(keyValue, "Complete domain value");
268
- const entry = lookup.get(identity);
269
- if (entry === undefined) {
262
+ const entries = lookup.get(identity);
263
+ if (entries === undefined) {
270
264
  output.push(syntheticRow(fields, group, keyValue, transform));
271
265
  } else {
272
- const row = { ...entry.row };
273
- if (transform.members !== undefined) define(row, transform.members, [entry.index]);
274
- output.push(row);
266
+ for (const entry of entries) {
267
+ const row = { ...entry.row };
268
+ if (transform.members !== undefined) define(row, transform.members, [entry.index]);
269
+ output.push(row);
270
+ }
275
271
  }
276
272
  }
277
273
  }
@@ -1,29 +1,22 @@
1
+ import {parseTimeValue,validateParseTimeFormat} from "./parseTime.js";
2
+ import { calendarParts, validateTimeZone } from "./timeUnit.js";
3
+ import { rejectUnknownProperties as rejectUnknownKeys } from "../core/validation.js";
4
+ import { requireStringValue as requireField } from "../core/validation.js";
1
5
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
6
 
3
7
  const TRANSFORM_KEYS = Object.freeze(["type", "as", "expression"]);
4
8
  const ARITHMETIC_BINARY = new Set(["add", "subtract", "multiply", "divide"]);
5
9
  const COMPARISONS = new Set(["eq", "neq", "lt", "lte", "gt", "gte"]);
6
- const NUMERIC_UNARY = new Set(["negate", "absolute", "log", "sqrt"]);
10
+ const NUMERIC_UNARY = new Set(["negate", "absolute", "log", "sqrt", "floor"]);
7
11
  const BOOLEAN_UNARY = new Set(["not", "isNull"]);
8
12
  const LOGICAL = new Set(["and", "or"]);
9
13
  const VARIADIC = new Set(["coalesce", "concat"]);
14
+ const RESERVED_PATH_KEYS = new Set(["__proto__", "prototype", "constructor"]);
10
15
  const MAX_DEPTH = 16;
11
16
  const MAX_NODES = 128;
12
17
  const MAX_WORK = 10_000_000;
13
18
 
14
- function rejectUnknownKeys(value, supported, label) {
15
- const unknown = Object.keys(value).find(key => !supported.includes(key));
16
- if (unknown !== undefined) {
17
- throw new Error(`Unknown ${label} property "${unknown}".`);
18
- }
19
- }
20
19
 
21
- function requireField(value, label) {
22
- if (typeof value !== "string" || value.length === 0) {
23
- throw new TypeError(`${label} must be a non-empty string.`);
24
- }
25
- return value;
26
- }
27
20
 
28
21
  function validateConstant(value) {
29
22
  if (value === null || typeof value === "string" || typeof value === "boolean") return;
@@ -50,7 +43,7 @@ function validateOperands(expression, minimum) {
50
43
  }
51
44
  }
52
45
 
53
- function inspectExpression(expression, depth, state) {
46
+ function inspectExpression(expression, depth, state, optionalPathRoot = false) {
54
47
  if (!isPlainObject(expression)) {
55
48
  throw new TypeError("Computed expression nodes must be plain objects.");
56
49
  }
@@ -72,7 +65,9 @@ function inspectExpression(expression, depth, state) {
72
65
  }
73
66
  if (kinds[0] === "field") {
74
67
  rejectUnknownKeys(expression, ["field"], "computed field expression");
75
- state.fields.add(requireField(expression.field, "Computed expression field"));
68
+ const field = requireField(expression.field, "Computed expression field");
69
+ state.fields.add(field);
70
+ if (!optionalPathRoot) state.requiredFields.add(field);
76
71
  return;
77
72
  }
78
73
  if (kinds[0] === "constant") {
@@ -88,7 +83,47 @@ function inspectExpression(expression, depth, state) {
88
83
  inspectExpression(expression.right, depth + 1, state);
89
84
  return;
90
85
  }
91
- if (NUMERIC_UNARY.has(expression.op) || BOOLEAN_UNARY.has(expression.op)) {
86
+ if (expression.op === "getPath") {
87
+ rejectUnknownKeys(expression, ["op", "operand", "path"], "computed path expression");
88
+ if (!isPlainObject(expression.operand) ||
89
+ Object.keys(expression.operand).length !== 1 ||
90
+ !Object.hasOwn(expression.operand, "field")) {
91
+ throw new TypeError("Computed getPath operand must be a literal field reference.");
92
+ }
93
+ if (!Array.isArray(expression.path) || expression.path.length < 1 ||
94
+ expression.path.length > 8 || !Array.from(expression.path).every(key =>
95
+ typeof key === "string" && key.length > 0 && !RESERVED_PATH_KEYS.has(key))) {
96
+ throw new TypeError("Computed getPath path must contain 1 to 8 nonempty, nonreserved string keys.");
97
+ }
98
+ inspectExpression(expression.operand, depth + 1, state, true);
99
+ return;
100
+ }
101
+ if (expression.op === "replaceLiteral") {
102
+ rejectUnknownKeys(expression, ["op", "operand", "search", "replacement"], "computed literal replacement");
103
+ requireNode(expression, "operand", expression.op);
104
+ if (typeof expression.search !== "string" || expression.search.length === 0 ||
105
+ typeof expression.replacement !== "string") {
106
+ throw new TypeError("Computed replaceLiteral requires a nonempty string search and string replacement.");
107
+ }
108
+ inspectExpression(expression.operand, depth + 1, state);
109
+ return;
110
+ }
111
+ if (expression.op === "datePart") {
112
+ rejectUnknownKeys(expression, ["op", "part", "operand", "timeZone"], "computed calendar expression");
113
+ requireNode(expression, "operand", expression.op);
114
+ if (expression.part !== "year") throw new Error("Computed datePart supports part year only.");
115
+ if (Object.hasOwn(expression, "timeZone")) validateTimeZone(expression.timeZone);
116
+ inspectExpression(expression.operand, depth + 1, state);
117
+ return;
118
+ }
119
+ if (expression.op === "parseTime") {
120
+ rejectUnknownKeys(expression, ["op", "operand", "format"], "computed time expression");
121
+ requireNode(expression, "operand", expression.op);
122
+ validateParseTimeFormat(expression.format);
123
+ inspectExpression(expression.operand, depth + 1, state);
124
+ return;
125
+ }
126
+ if (NUMERIC_UNARY.has(expression.op) || BOOLEAN_UNARY.has(expression.op) || expression.op === "toNumber") {
92
127
  rejectUnknownKeys(expression, ["op", "operand"], "computed unary expression");
93
128
  requireNode(expression, "operand", expression.op);
94
129
  inspectExpression(expression.operand, depth + 1, state);
@@ -131,7 +166,7 @@ function inspectComputedTransform(transform) {
131
166
  throw new Error(`Unsupported computed transform "${transform.type}".`);
132
167
  }
133
168
  requireField(transform.as, "Computed output field");
134
- const state = { nodes: 0, fields: new Set(), ancestors: new WeakSet() };
169
+ const state = { nodes: 0, fields: new Set(), requiredFields: new Set(), ancestors: new WeakSet() };
135
170
  inspectExpression(transform.expression, 1, state);
136
171
  return state;
137
172
  }
@@ -233,6 +268,27 @@ function compare(left, right, operation, rowIndex) {
233
268
  : order >= 0;
234
269
  }
235
270
 
271
+ function ownDataProperty(record, key, rowIndex) {
272
+ const descriptor = Object.getOwnPropertyDescriptor(record, key);
273
+ if (descriptor === undefined) return undefined;
274
+ if (!Object.hasOwn(descriptor, "value")) {
275
+ throw new TypeError(`Computed getPath cannot read an accessor at row ${rowIndex}.`);
276
+ }
277
+ return descriptor.value;
278
+ }
279
+
280
+ function nestedScalar(expression, row, rowIndex) {
281
+ let value = ownDataProperty(row, expression.operand.field, rowIndex);
282
+ for (const key of expression.path) {
283
+ if (value === undefined || value === null) return null;
284
+ if (!isPlainObject(value)) {
285
+ throw new TypeError(`Computed getPath requires plain records at row ${rowIndex}.`);
286
+ }
287
+ value = ownDataProperty(value, key, rowIndex);
288
+ }
289
+ return scalar(value, "Computed getPath result", rowIndex);
290
+ }
291
+
236
292
  function evaluate(expression, row, rowIndex) {
237
293
  if (Object.hasOwn(expression, "field")) {
238
294
  return scalar(
@@ -243,6 +299,16 @@ function evaluate(expression, row, rowIndex) {
243
299
  }
244
300
  if (Object.hasOwn(expression, "constant")) return expression.constant;
245
301
  const operation = expression.op;
302
+ if (operation === "getPath") return nestedScalar(expression, row, rowIndex);
303
+ if (operation === "replaceLiteral") {
304
+ const value = evaluate(expression.operand, row, rowIndex);
305
+ if (typeof value !== "string") {
306
+ throw new TypeError(`Computed replaceLiteral requires a string at row ${rowIndex}.`);
307
+ }
308
+ const index = value.indexOf(expression.search);
309
+ return index < 0 ? value
310
+ : value.slice(0, index) + expression.replacement + value.slice(index + expression.search.length);
311
+ }
246
312
  if (operation === "if") {
247
313
  return boolean(evaluate(expression.condition, row, rowIndex), operation, rowIndex)
248
314
  ? evaluate(expression.then, row, rowIndex)
@@ -282,6 +348,29 @@ function evaluate(expression, row, rowIndex) {
282
348
  ? value === null
283
349
  : !boolean(value, operation, rowIndex);
284
350
  }
351
+ if (operation === "datePart") {
352
+ const value = evaluate(expression.operand, row, rowIndex);
353
+ if (value === null) return null;
354
+ number(value, operation, rowIndex);
355
+ if (Math.abs(value) > 8_640_000_000_000_000) {
356
+ throw new RangeError(`Computed datePart timestamp is outside the supported Date range at row ${rowIndex}.`);
357
+ }
358
+ const timestamp = new Date(value).getTime();
359
+ return (expression.timeZone ?? "UTC") === "UTC"
360
+ ? new Date(timestamp).getUTCFullYear()
361
+ : calendarParts(timestamp, expression.timeZone).year;
362
+ }
363
+ if (operation === "parseTime") {
364
+ return parseTimeValue(evaluate(expression.operand, row, rowIndex), expression.format);
365
+ }
366
+ if (operation === "toNumber") {
367
+ const value = evaluate(expression.operand, row, rowIndex);
368
+ if (typeof value === "number") return number(value, operation, rowIndex);
369
+ if (typeof value !== "string" || !/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(value.trim())) {
370
+ throw new TypeError(`Computed toNumber requires a finite number or decimal string at row ${rowIndex}.`);
371
+ }
372
+ return number(Number(value.trim()), operation, rowIndex);
373
+ }
285
374
  if (NUMERIC_UNARY.has(operation)) {
286
375
  const value = number(evaluate(expression.operand, row, rowIndex), operation, rowIndex);
287
376
  if (operation === "log" && value <= 0) {
@@ -292,6 +381,7 @@ function evaluate(expression, row, rowIndex) {
292
381
  }
293
382
  const result = operation === "negate" ? -value
294
383
  : operation === "absolute" ? Math.abs(value)
384
+ : operation === "floor" ? Math.floor(value)
295
385
  : operation === "log" ? Math.log(value)
296
386
  : Math.sqrt(value);
297
387
  if (!Number.isFinite(result)) {
@@ -318,7 +408,7 @@ function evaluate(expression, row, rowIndex) {
318
408
  }
319
409
 
320
410
  export function deriveComputedRows(rows, transform) {
321
- const { nodes, fields } = inspectComputedTransform(transform);
411
+ const { nodes, fields, requiredFields } = inspectComputedTransform(transform);
322
412
  if (rows.length * nodes > MAX_WORK) {
323
413
  throw new RangeError(`Computed work cannot exceed ${MAX_WORK} row-nodes.`);
324
414
  }
@@ -326,15 +416,14 @@ export function deriveComputedRows(rows, transform) {
326
416
  throw new Error(`Computed output field "${transform.as}" already exists.`);
327
417
  }
328
418
  if (rows.length > 0) {
329
- rows.forEach((row, index) => {
330
- for (const field of fields) {
331
- if (!Object.hasOwn(row, field)) {
332
- throw new Error(
333
- `Computed source does not contain field "${field}" at row ${index}.`
334
- );
419
+ for (const field of fields) {
420
+ const knownOptional = !requiredFields.has(field) && rows.some(row => Object.hasOwn(row, field));
421
+ rows.forEach((row, index) => {
422
+ if (!knownOptional && !Object.hasOwn(row, field)) {
423
+ throw new Error(`Computed source does not contain field "${field}" at row ${index}.`);
335
424
  }
336
- }
337
- });
425
+ });
426
+ }
338
427
  }
339
428
  let outputType;
340
429
  return rows.map((row, index) => {
@@ -1,6 +1,5 @@
1
1
  import { freezeOwned, isPlainObject } from "../core/immutable.js";
2
- import { validateGeneratedItemLimit } from "../core/validation.js";
3
- import { buildLinearPathCommands } from "./pathCommands.js";
2
+ import { buildLinearPathCommands, validatePathCommandCount } from "./pathCommands.js";
4
3
 
5
4
  export const CURVE_INTERPOLATIONS = Object.freeze([
6
5
  "linear",
@@ -42,6 +41,15 @@ function validatePoints(points) {
42
41
  return points;
43
42
  }
44
43
 
44
+ export function validateCardinalTension(tension, curve) {
45
+ if (tension === undefined) return 0;
46
+ if (curve !== "cardinal") throw new Error("Line tension requires cardinal interpolation.");
47
+ if (!Number.isFinite(tension) || tension < 0 || tension > 1) {
48
+ throw new RangeError("Cardinal tension must be between 0 and 1.");
49
+ }
50
+ return tension;
51
+ }
52
+
45
53
  export function validateCurveInterpolation(curve) {
46
54
  if (!CURVE_INTERPOLATIONS.includes(curve)) {
47
55
  throw new Error(`Unsupported curve interpolation "${curve}".`);
@@ -134,7 +142,7 @@ function buildBasisCommands(points) {
134
142
  return ownCommands(commands);
135
143
  }
136
144
 
137
- function buildCardinalCommands(points) {
145
+ function buildCardinalCommands(points, tension = 0) {
138
146
  const commands = [{ op: "M", x: points[0].x, y: points[0].y }];
139
147
  for (let index = 0; index < points.length - 1; index += 1) {
140
148
  const before = points[index - 1] ?? points[index];
@@ -143,10 +151,10 @@ function buildCardinalCommands(points) {
143
151
  const after = points[index + 2] ?? next;
144
152
  commands.push({
145
153
  op: "C",
146
- x1: current.x + (next.x - before.x) / 6,
147
- y1: current.y + (next.y - before.y) / 6,
148
- x2: next.x - (after.x - current.x) / 6,
149
- y2: next.y - (after.y - current.y) / 6,
154
+ x1: current.x + (next.x - before.x) / 6 * (1 - tension),
155
+ y1: current.y + (next.y - before.y) / 6 * (1 - tension),
156
+ x2: next.x - (after.x - current.x) / 6 * (1 - tension),
157
+ y2: next.y - (after.y - current.y) / 6 * (1 - tension),
150
158
  x: next.x,
151
159
  y: next.y
152
160
  });
@@ -265,7 +273,7 @@ function buildNaturalCommands(points) {
265
273
  return ownCommands(commands);
266
274
  }
267
275
 
268
- function buildValidatedCurvePathCommands(points, curve) {
276
+ function buildValidatedCurvePathCommands(points, curve, tension) {
269
277
  if (curve === "linear" ||
270
278
  (SMOOTH_CURVES.has(curve) &&
271
279
  curve !== "monotone" &&
@@ -277,7 +285,7 @@ function buildValidatedCurvePathCommands(points, curve) {
277
285
  }
278
286
  if (curve === "basis") return buildBasisCommands(points);
279
287
  if (curve === "cardinal") {
280
- return buildCardinalCommands(points);
288
+ return buildCardinalCommands(points, tension);
281
289
  }
282
290
  if (curve === "monotone") {
283
291
  return buildMonotoneCommands(points);
@@ -301,17 +309,19 @@ function curveCommandCount(pointCount, curve) {
301
309
  : pointCount;
302
310
  }
303
311
 
304
- export function buildCurvePathCommands(points, curve = "linear") {
312
+ export function buildCurvePathCommands(points, curve = "linear", tension) {
313
+ validateCardinalTension(tension, curve);
305
314
  const validatedPoints = validatePoints(points);
306
315
  const validatedCurve = validateCurveInterpolation(curve);
307
- validateGeneratedItemLimit(
316
+ validatePathCommandCount(
308
317
  curveCommandCount(validatedPoints.length, validatedCurve),
309
318
  "Curve path command count"
310
319
  );
311
320
  const direct = buildValidatedCurvePathCommands(
312
321
  validatedPoints,
313
- validatedCurve
322
+ validatedCurve, tension
314
323
  );
324
+ validatePathCommandCount(direct.length, "Curve path command count");
315
325
  if (finiteCommands(direct)) return direct;
316
326
 
317
327
  let xScale = 0;
@@ -327,7 +337,7 @@ export function buildCurvePathCommands(points, curve = "linear") {
327
337
  x: point.x / xScale,
328
338
  y: point.y / yScale
329
339
  })),
330
- validatedCurve
340
+ validatedCurve, tension
331
341
  );
332
342
  const restored = normalized.map(command => {
333
343
  const copy = {};
@@ -341,6 +351,7 @@ export function buildCurvePathCommands(points, curve = "linear") {
341
351
  if (!finiteCommands(restored)) {
342
352
  throw new RangeError("Curve interpolation exceeds the finite numeric range.");
343
353
  }
354
+ validatePathCommandCount(restored.length, "Curve path command count");
344
355
  return ownCommands(restored);
345
356
  }
346
357
 
@@ -403,7 +414,7 @@ export function buildAreaCurvePathCommands(
403
414
  const validatedCurve = validateCurveInterpolation(curve);
404
415
  const validatedLower = validatePoints(lowerPoints);
405
416
  const validatedUpper = validatePoints(upperPoints);
406
- validateGeneratedItemLimit(
417
+ validatePathCommandCount(
407
418
  curveCommandCount(validatedLower.length, validatedCurve) +
408
419
  curveCommandCount(validatedUpper.length, validatedCurve) + 1,
409
420
  "Area path command count"
@@ -416,10 +427,12 @@ export function buildAreaCurvePathCommands(
416
427
  const upper = reverseOpenPathCommands(
417
428
  buildOrientedCurvePathCommands(validatedUpper, validatedCurve, independentAxis)
418
429
  );
419
- return ownCommands([
430
+ const commands = [
420
431
  ...lower,
421
432
  { op: "L", x: upper[0].x, y: upper[0].y },
422
433
  ...upper.slice(1),
423
434
  { op: "Z" }
424
- ]);
435
+ ];
436
+ validatePathCommandCount(commands.length, "Area path command count");
437
+ return ownCommands(commands);
425
438
  }
@@ -1,5 +1,7 @@
1
+ import { requireStringValue as requireName } from "../core/validation.js";
1
2
  import { annotateError } from "../core/diagnostics.js";
2
3
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
4
+ import { requireRegressionStatisticsSource, REGRESSION_STATISTIC_FIELDS } from "./regression/statistics.js";
3
5
 
4
6
  const STORAGE_TYPES = new Set([
5
7
  "number", "string", "boolean", "array", "object", "unknown", "mixed"
@@ -19,12 +21,6 @@ function rejectUnknownKeys(value, keys, label) {
19
21
  if (unknown !== undefined) throw new Error(`Unknown ${label} property "${unknown}".`);
20
22
  }
21
23
 
22
- function requireName(value, label) {
23
- if (typeof value !== "string" || value.length === 0) {
24
- throw new TypeError(`${label} must be a non-empty string.`);
25
- }
26
- return value;
27
- }
28
24
 
29
25
  function valueStorageType(value) {
30
26
  if (typeof value === "number") return "number";
@@ -61,7 +57,8 @@ function normalizeField(value, index, { declared = false } = {}) {
61
57
  declared ? SOURCE_FIELD_KEYS : FIELD_KEYS,
62
58
  `dataset schema field ${index}`
63
59
  );
64
- const name = requireName(value.name, `Dataset schema field ${index} name`);
60
+ // Stored row keys are lossless; action field references still require non-empty names.
61
+ const name = value.name === "" ? "" : requireName(value.name, `Dataset schema field ${index} name`);
65
62
  const allowed = declared ? DECLARED_STORAGE_TYPES : STORAGE_TYPES;
66
63
  if (!allowed.has(value.storageType)) {
67
64
  throw new Error(`Unsupported dataset schema storageType "${value.storageType}".`);
@@ -271,7 +268,7 @@ function generatedField(name, storageType, transform, role, inputs = []) {
271
268
  };
272
269
  }
273
270
 
274
- export function deriveTransformSchema(inputSchema, transform, values, { sourceId, ownerId } = {}) {
271
+ export function deriveTransformSchema(inputSchema, transform, values, { sourceId, ownerId, sourceTransform } = {}) {
275
272
  const input = validateDatasetSchema(inputSchema);
276
273
  const definition = { ...transform, __sourceId: sourceId, __ownerId: ownerId };
277
274
  const retain = ["filter", "markFilter", "sort", "computed", "timeUnit", "window", "normalize", "stack", "impute", "complete"];
@@ -304,6 +301,12 @@ export function deriveTransformSchema(inputSchema, transform, values, { sourceId
304
301
  generatedField(transform.as.count, "number", definition, "count", [transform.x, transform.y])
305
302
  ];
306
303
  if (transform.as.members !== undefined) push(generatedField(transform.as.members, "array", definition, "members"));
304
+ } else if (transform.type === "regressionStatistics") {
305
+ const fit = requireRegressionStatisticsSource({ transform: sourceTransform === undefined ? [] : [sourceTransform] });
306
+ fields = [
307
+ ...(fit.groupBy === undefined ? [] : [inheritedField(input, fit.groupBy)]),
308
+ ...REGRESSION_STATISTIC_FIELDS.map(name => generatedField(name, "number", definition, name))
309
+ ].filter(Boolean);
307
310
  } else if (transform.type === "regression") {
308
311
  fields = [inheritedField(input, transform.x), inheritedField(input, transform.y)].filter(Boolean);
309
312
  if (transform.groupBy !== undefined) fields.unshift(inheritedField(input, transform.groupBy));
@@ -371,7 +374,8 @@ export function deriveTransformSchema(inputSchema, transform, values, { sourceId
371
374
  if (planned.lineage === undefined && !["complete", "impute"].includes(transform.type)) {
372
375
  return planned;
373
376
  }
374
- return { ...planned, storageType: field.storageType, nullable: field.nullable, optional: field.optional };
377
+ return { ...planned, storageType: transform.type === "regressionStatistics" && planned.storageType === "number"
378
+ ? "number" : field.storageType, nullable: field.nullable, optional: field.optional };
375
379
  });
376
380
  for (const field of inferred.fields) {
377
381
  if (!fields.some(planned => planned.name === field.name)) fields.push(field);