ggaction 0.0.15 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +23 -2
  3. package/knowledge/action-cards.json +1 -1
  4. package/knowledge/intent-taxonomy.json +1 -1
  5. package/knowledge/mcp-resources.json +1 -1
  6. package/knowledge/task-packet.schema.json +1 -1
  7. package/knowledge/task-resolver.js +1 -1
  8. package/package.json +40 -7
  9. package/src/BasicChartProgram.js +3 -1
  10. package/src/ChartProgram.js +2 -0
  11. package/src/accessibility.js +157 -0
  12. package/src/actions/boxPlots/components.js +6 -8
  13. package/src/actions/boxPlots/create.js +4 -5
  14. package/src/actions/boxPlots/edit.js +3 -4
  15. package/src/actions/boxPlots/materialize.js +1 -1
  16. package/src/actions/canvas/actions.js +3 -3
  17. package/src/actions/canvas/fitting.js +1 -1
  18. package/src/actions/categoryOrder/index.js +6 -8
  19. package/src/actions/charts/area.js +1 -1
  20. package/src/actions/charts/bar.js +1 -1
  21. package/src/actions/charts/beeswarm.js +1 -1
  22. package/src/actions/charts/density.js +1 -1
  23. package/src/actions/charts/ecdf.js +2 -2
  24. package/src/actions/charts/endpoints.js +4 -4
  25. package/src/actions/charts/heatmap.js +1 -1
  26. package/src/actions/charts/histogram.js +1 -1
  27. package/src/actions/charts/horizon.js +1 -1
  28. package/src/actions/charts/interval-regression.js +2 -2
  29. package/src/actions/charts/line.js +1 -1
  30. package/src/actions/charts/parallel.js +1 -1
  31. package/src/actions/charts/pie.js +1 -1
  32. package/src/actions/charts/polar.js +2 -2
  33. package/src/actions/charts/radar.js +1 -1
  34. package/src/actions/charts/radial.js +2 -2
  35. package/src/actions/charts/raincloud.js +2 -2
  36. package/src/actions/charts/rug-strip.js +2 -2
  37. package/src/actions/charts/scatter.js +1 -1
  38. package/src/actions/composition/actions.js +20 -24
  39. package/src/actions/coordinates/actions.js +1 -1
  40. package/src/actions/coordinates/edit.js +1 -1
  41. package/src/actions/data/basicBin2d.js +4 -5
  42. package/src/actions/data/bin.js +3 -3
  43. package/src/actions/data/bin2d.js +6 -8
  44. package/src/actions/data/bin2dMaterialize.js +4 -5
  45. package/src/actions/data/box.js +4 -4
  46. package/src/actions/data/complete.js +2 -2
  47. package/src/actions/data/computed.js +2 -2
  48. package/src/actions/data/create.js +23 -17
  49. package/src/actions/data/density.js +4 -4
  50. package/src/actions/data/derived.js +25 -15
  51. package/src/actions/data/ecdf.js +2 -2
  52. package/src/actions/data/edit.js +54 -29
  53. package/src/actions/data/filter.js +29 -24
  54. package/src/actions/data/fold.js +2 -2
  55. package/src/actions/data/gradientProfile.js +2 -2
  56. package/src/actions/data/horizon.js +2 -2
  57. package/src/actions/data/impute.js +2 -2
  58. package/src/actions/data/index.js +58 -54
  59. package/src/actions/data/interval.js +4 -2
  60. package/src/actions/data/intervalEdit.js +26 -18
  61. package/src/actions/data/normalize.js +2 -2
  62. package/src/actions/data/regression.js +9 -7
  63. package/src/actions/data/revise.js +80 -0
  64. package/src/actions/data/revisionBindings.js +13 -33
  65. package/src/actions/data/shared.js +30 -2
  66. package/src/actions/data/sort.js +21 -0
  67. package/src/actions/data/stack.js +2 -2
  68. package/src/actions/data/summary.js +3 -3
  69. package/src/actions/data/timeUnit.js +2 -2
  70. package/src/actions/data/window.js +2 -2
  71. package/src/actions/encodings/angle.js +5 -2
  72. package/src/actions/encodings/appearance.js +10 -6
  73. package/src/actions/encodings/barWidth.js +1 -1
  74. package/src/actions/encodings/channels.js +2 -2
  75. package/src/actions/encodings/color/continuous.js +4 -1
  76. package/src/actions/encodings/color/index.js +5 -2
  77. package/src/actions/encodings/density.js +2 -2
  78. package/src/actions/encodings/histogram.js +1 -1
  79. package/src/actions/encodings/horizon.js +6 -8
  80. package/src/actions/encodings/offset.js +6 -3
  81. package/src/actions/encodings/parallel.js +4 -5
  82. package/src/actions/encodings/pathOrder.js +6 -11
  83. package/src/actions/encodings/position/index.js +4 -4
  84. package/src/actions/encodings/position/resolve.js +6 -1
  85. package/src/actions/encodings/ranged.js +11 -6
  86. package/src/actions/encodings/remove.js +4 -5
  87. package/src/actions/encodings/ruleAppearance.js +5 -2
  88. package/src/actions/encodings/seriesLayout.js +1 -1
  89. package/src/actions/encodings/stroke.js +5 -2
  90. package/src/actions/encodings/strokeDash.js +8 -3
  91. package/src/actions/encodings/text.js +7 -2
  92. package/src/actions/errorBands/create.js +11 -17
  93. package/src/actions/errorBands/edit.js +13 -131
  94. package/src/actions/errorBars/create.js +8 -13
  95. package/src/actions/errorBars/edit.js +10 -121
  96. package/src/actions/facets/actions.js +30 -16
  97. package/src/actions/facets/derive.js +8 -2
  98. package/src/actions/facets/guides.js +4 -5
  99. package/src/actions/facets/replay.js +4 -5
  100. package/src/actions/gradientPlots/components.js +5 -6
  101. package/src/actions/gradientPlots/create.js +4 -5
  102. package/src/actions/gradientPlots/edit.js +4 -5
  103. package/src/actions/gradientPlots/materialize.js +2 -2
  104. package/src/actions/gradientPlots/rebind.js +4 -5
  105. package/src/actions/guides/axes/axes.js +23 -4
  106. package/src/actions/guides/axes/axis.js +2 -2
  107. package/src/actions/guides/axes/edit.js +2 -2
  108. package/src/actions/guides/axes/labels.js +9 -9
  109. package/src/actions/guides/axes/lines.js +9 -7
  110. package/src/actions/guides/axes/parallel/lifecycle.js +13 -13
  111. package/src/actions/guides/axes/parallel.js +5 -5
  112. package/src/actions/guides/axes/remove.js +4 -4
  113. package/src/actions/guides/axes/tickGroups.js +4 -4
  114. package/src/actions/guides/axes/ticks.js +13 -4
  115. package/src/actions/guides/axes/titles.js +10 -10
  116. package/src/actions/guides/grids/grid.js +18 -29
  117. package/src/actions/guides/guides.js +1 -1
  118. package/src/actions/guides/legends/blocks.js +1 -1
  119. package/src/actions/guides/legends/categorical/actions.js +4 -4
  120. package/src/actions/guides/legends/categorical/components.js +38 -33
  121. package/src/actions/guides/legends/categorical/layout.js +1 -1
  122. package/src/actions/guides/legends/categorical/symbols.js +27 -30
  123. package/src/actions/guides/legends/continuous/common.js +46 -8
  124. package/src/actions/guides/legends/continuous/gradient.js +89 -155
  125. package/src/actions/guides/legends/continuous/interval.js +13 -13
  126. package/src/actions/guides/legends/continuous/opacity.js +30 -79
  127. package/src/actions/guides/legends/continuous/stroke.js +22 -322
  128. package/src/actions/guides/legends/edit.js +1 -1
  129. package/src/actions/guides/legends/focused.js +5 -5
  130. package/src/actions/guides/legends/lane.js +6 -6
  131. package/src/actions/guides/legends/remove.js +4 -5
  132. package/src/actions/guides/legends/size.js +21 -82
  133. package/src/actions/guides/legends/strokeWidth.js +17 -77
  134. package/src/actions/guides/polar/axes/facade.js +4 -4
  135. package/src/actions/guides/polar/axes/labels.js +5 -5
  136. package/src/actions/guides/polar/axes/lines.js +9 -15
  137. package/src/actions/guides/polar/axes/ticks.js +9 -15
  138. package/src/actions/guides/polar/axes/titles.js +5 -5
  139. package/src/actions/guides/polar/grids.js +11 -13
  140. package/src/actions/index.js +2 -0
  141. package/src/actions/intervals/revision.js +126 -0
  142. package/src/actions/marks/arc/actions.js +3 -3
  143. package/src/actions/marks/area/actions.js +4 -4
  144. package/src/actions/marks/bar/create.js +9 -2
  145. package/src/actions/marks/bar/edit.js +13 -6
  146. package/src/actions/marks/bar/materialize.js +12 -38
  147. package/src/actions/marks/line/actions.js +3 -3
  148. package/src/actions/marks/point/create.js +9 -2
  149. package/src/actions/marks/point/edit.js +13 -6
  150. package/src/actions/marks/point/jitter.js +6 -8
  151. package/src/actions/marks/point/materialize.js +20 -26
  152. package/src/actions/marks/point/packing.js +6 -8
  153. package/src/actions/marks/rect/actions.js +26 -11
  154. package/src/actions/marks/references.js +8 -10
  155. package/src/actions/marks/remove.js +5 -49
  156. package/src/actions/marks/rule/actions.js +28 -12
  157. package/src/actions/marks/text/actions.js +30 -11
  158. package/src/actions/marks/text/layout.js +12 -20
  159. package/src/actions/marks/text/removal.js +1 -1
  160. package/src/actions/marks/text/selection.js +4 -5
  161. package/src/actions/marks/tick/actions.js +26 -11
  162. package/src/actions/primitives/createGraphics.js +5 -4
  163. package/src/actions/primitives/editGraphics.js +5 -4
  164. package/src/actions/primitives/graphicProperties.js +6 -0
  165. package/src/actions/primitives/semantic.js +10 -3
  166. package/src/actions/primitives/semanticAction.js +44 -7
  167. package/src/actions/primitives/semanticValidation/dataset.js +14 -4
  168. package/src/actions/primitives/semanticValidation/layer.js +22 -2
  169. package/src/actions/primitives/semanticValidation/scale.js +6 -0
  170. package/src/actions/regression/components.js +10 -14
  171. package/src/actions/regression/create.js +28 -8
  172. package/src/actions/regression/edit.js +50 -16
  173. package/src/actions/regression/follow.js +22 -0
  174. package/src/actions/resources/remove.js +12 -11
  175. package/src/actions/scales/channels.js +11 -11
  176. package/src/actions/scales/consumers/common.js +2 -1
  177. package/src/actions/scales/create.js +3 -2
  178. package/src/actions/scales/definitions.js +1 -1
  179. package/src/actions/scales/edit.js +2 -2
  180. package/src/actions/scales/materialize.js +1 -1
  181. package/src/actions/scales/offset.js +2 -2
  182. package/src/actions/scales/parallel.js +2 -2
  183. package/src/actions/scales/patch.js +1 -1
  184. package/src/actions/scales/preview.js +28 -1
  185. package/src/actions/scales/quantitativeColor.js +4 -5
  186. package/src/actions/selection/actions.js +25 -36
  187. package/src/actions/textMetrics/index.js +56 -0
  188. package/src/actions/theme/actions.js +2 -2
  189. package/src/actions/theme/reconcile.js +67 -121
  190. package/src/actions/titles/actions.js +8 -8
  191. package/src/actions/titles/resolve.js +3 -3
  192. package/src/actions/violinPlots/create.js +1 -1
  193. package/src/actions/violinPlots/edit.js +1 -1
  194. package/src/core/action.js +109 -36
  195. package/src/core/diagnostics.js +16 -0
  196. package/src/core/extensionRegistry.js +27 -0
  197. package/src/core/font.js +18 -0
  198. package/src/core/identifiers.js +5 -4
  199. package/src/core/immutable.js +10 -3
  200. package/src/core/optionRequirements.js +8 -0
  201. package/src/core/programState.js +6 -2
  202. package/src/core/resourceReferences.js +15 -6
  203. package/src/core/textMetricProfile.js +28 -0
  204. package/src/core/textMetrics.js +24 -3
  205. package/src/core/validation.js +22 -10
  206. package/src/diagnostics.js +1 -0
  207. package/src/grammar/bin.js +39 -10
  208. package/src/grammar/datasetSchema.js +386 -0
  209. package/src/grammar/density.js +53 -5
  210. package/src/grammar/filter.js +133 -39
  211. package/src/grammar/impute.js +3 -2
  212. package/src/grammar/interval.js +32 -1
  213. package/src/grammar/itemMissing.js +59 -0
  214. package/src/grammar/regression/derive.js +57 -16
  215. package/src/grammar/regression/index.js +1 -0
  216. package/src/grammar/regression/models.js +51 -22
  217. package/src/grammar/regression/parameters.js +56 -4
  218. package/src/grammar/scales/definition.js +7 -0
  219. package/src/grammar/scales/types.js +8 -0
  220. package/src/grammar/schemas/graphicBounds.js +11 -11
  221. package/src/grammar/schemas/semanticPath.js +3 -2
  222. package/src/grammar/sort.js +88 -0
  223. package/src/grammar/summary.js +117 -19
  224. package/src/grammar/transformTopology.js +1 -0
  225. package/src/grammar/transforms.js +21 -11
  226. package/src/inspection.js +495 -0
  227. package/src/inspectionDescriptors.js +3 -0
  228. package/src/layout/labels.js +11 -10
  229. package/src/layout/legend.js +4 -4
  230. package/src/layout/legendItems.js +6 -6
  231. package/src/layout/text.js +10 -9
  232. package/src/layout/title.js +9 -9
  233. package/src/materialization/bars/resolve.js +4 -2
  234. package/src/materialization/facetGuides/placement.js +1 -1
  235. package/src/materialization/facetGuides/preparation.js +1 -1
  236. package/src/materialization/facetHeaders.js +11 -10
  237. package/src/materialization/guides/layout.js +3 -3
  238. package/src/materialization/revisionIdentity.js +19 -0
  239. package/src/materialization/scaleGuideDependencies.js +10 -0
  240. package/src/materialization/scales/resolve.js +67 -4
  241. package/src/materialization/selection/items/common.js +1 -1
  242. package/src/materialization/text.js +6 -4
  243. package/src/materialization/typography.js +31 -0
  244. package/src/mcp/cli.js +5 -3
  245. package/src/persistence/codec.js +65 -0
  246. package/src/persistence/validation.js +185 -0
  247. package/src/persistence.js +147 -0
  248. package/src/renderers/canvas/index.js +5 -4
  249. package/src/renderers/nativeBackend.js +10 -0
  250. package/src/renderers/options.js +13 -0
  251. package/src/renderers/pdf.js +25 -54
  252. package/src/renderers/png.js +22 -17
  253. package/src/renderers/svg.js +10 -14
  254. package/src/renderers/text.js +1 -11
  255. package/src/selectors/coordinates.js +3 -1
  256. package/src/selectors/datasets.js +8 -6
  257. package/src/selectors/layers.js +6 -4
  258. package/src/selectors/markOwners.js +46 -0
  259. package/src/selectors/scales.js +6 -4
  260. package/src/theme/defaults.js +2 -1
  261. package/src/version.js +2 -0
  262. package/types/accessibility.d.ts +37 -0
  263. package/types/basic.d.ts +4 -2
  264. package/types/diagnostics.d.ts +15 -0
  265. package/types/index.d.ts +5 -0
  266. package/types/inspection.d.ts +17 -0
  267. package/types/pdf.d.ts +9 -0
  268. package/types/persistence.d.ts +7 -0
  269. package/types/png.d.ts +9 -0
  270. package/types/program.d.ts +181 -28
@@ -58,14 +58,14 @@ function linearPrediction(model, xValue) {
58
58
  }
59
59
 
60
60
  function fitLinearGroup(rows, {
61
- x, y, group, confidenceMethod, level
61
+ x, y, group, confidenceMethod, level, interval
62
62
  }) {
63
63
  const count = rows.length;
64
64
  const groupLabel = group === undefined ? "all" : String(group);
65
65
  const label = `Regression group "${groupLabel}"`;
66
- if (count < 3) {
66
+ if (count < (interval === false ? 2 : 3)) {
67
67
  throw new Error(
68
- `${label} requires at least three rows.`
68
+ `${label} requires at least ${interval === false ? "two" : "three"} rows.`
69
69
  );
70
70
  }
71
71
  const xValues = rows.map(row => row[x]);
@@ -148,6 +148,7 @@ function fitLinearGroup(rows, {
148
148
  normalizedSxx
149
149
  });
150
150
  if (!normalizedXByDifference) model.normalizedXByDifference = false;
151
+ if (interval === false) return model;
151
152
  const residualSumSquares = stableFiniteSquareSum(
152
153
  rows.map(row => row[y] - linearPrediction(model, row[x])),
153
154
  `${label} residual sum of squares`
@@ -238,19 +239,19 @@ function polynomialResponse(design, values, size, scale = 1) {
238
239
  }
239
240
 
240
241
  function fitPolynomialGroup(rows, {
241
- x, y, group, confidenceMethod, level, degree
242
+ x, y, group, confidenceMethod, level, degree, interval
242
243
  }) {
243
244
  const count = rows.length;
244
245
  const parameterCount = degree + 1;
245
246
  const groupLabel = group === undefined ? "all" : String(group);
246
247
  const label = `Polynomial regression group "${groupLabel}"`;
247
248
  if (
248
- count < degree + 2 ||
249
+ count < (interval === false ? parameterCount : degree + 2) ||
249
250
  new Set(rows.map(row => row[x])).size < parameterCount
250
251
  ) {
251
252
  throw new Error(
252
253
  `${label} requires at least ` +
253
- `${degree + 2} rows and ${parameterCount} distinct x values.`
254
+ `${interval === false ? parameterCount : degree + 2} rows and ${parameterCount} distinct x values.`
254
255
  );
255
256
  }
256
257
  const xValues = rows.map(row => row[x]);
@@ -289,7 +290,7 @@ function fitPolynomialGroup(rows, {
289
290
  `${label} coefficient ${index}`
290
291
  ))
291
292
  : scaledCoefficients;
292
- const inverse = invertSymmetricMatrix(normal);
293
+ const inverse = interval === false ? undefined : invertSymmetricMatrix(normal);
293
294
  const fitted = design.map(basis => stableResponse
294
295
  ? restoreFiniteScale(
295
296
  dot(basis, scaledCoefficients),
@@ -298,12 +299,12 @@ function fitPolynomialGroup(rows, {
298
299
  )
299
300
  : dot(basis, normalizedCoefficients)
300
301
  );
301
- const residualSumSquares = stableFiniteSquareSum(
302
+ const residualSumSquares = interval === false ? undefined : stableFiniteSquareSum(
302
303
  fitted.map((value, index) => rows[index][y] - value),
303
304
  `${label} residual sum of squares`
304
305
  );
305
306
  const degreesOfFreedom = count - parameterCount;
306
- const residualVariance = residualSumSquares / degreesOfFreedom;
307
+ const residualVariance = interval === false ? undefined : residualSumSquares / degreesOfFreedom;
307
308
  const model = {
308
309
  count,
309
310
  degreesOfFreedom,
@@ -316,13 +317,15 @@ function fitPolynomialGroup(rows, {
316
317
  normalizedCoefficients,
317
318
  center,
318
319
  scale,
319
- inverse,
320
- residualSumSquares,
321
- residualStandardError: Math.sqrt(residualVariance),
322
- critical: confidenceCriticalValue({
323
- method: confidenceMethod,
324
- level,
325
- degreesOfFreedom
320
+ ...(inverse === undefined ? {} : { inverse }),
321
+ ...(interval === false ? {} : {
322
+ residualSumSquares,
323
+ residualStandardError: Math.sqrt(residualVariance),
324
+ critical: confidenceCriticalValue({
325
+ method: confidenceMethod,
326
+ level,
327
+ degreesOfFreedom
328
+ })
326
329
  })
327
330
  };
328
331
  if (!normalizedXByDifference) model.normalizedXByDifference = false;
@@ -345,7 +348,9 @@ function evaluatePolynomial(model, xValue) {
345
348
  model.responseScale,
346
349
  "Polynomial regression prediction"
347
350
  ),
348
- leverage: dot(basis, model.inverse.map(row => dot(row, basis)))
351
+ ...(model.inverse === undefined ? {} : {
352
+ leverage: dot(basis, model.inverse.map(row => dot(row, basis)))
353
+ })
349
354
  };
350
355
  }
351
356
 
@@ -420,7 +425,29 @@ function fitLoessGroup(rows, { x, y, group, span }) {
420
425
  neighborIndices: weighted.map(item => item.index)
421
426
  };
422
427
  });
423
- return { count: rows.length, span, neighborCount, fits };
428
+ return { count: rows.length, span, neighborCount, fits, rows, x, y, xScale, yScale };
429
+ }
430
+
431
+ function predictLoessAt(model, xValue) {
432
+ const found = model.fits.find(fit => fit.x === xValue);
433
+ if (found !== undefined) return found.prediction;
434
+ const neighbors = model.rows.map((row, index) => ({
435
+ row,
436
+ index,
437
+ distance: Math.abs(row[model.x] / model.xScale - xValue / model.xScale)
438
+ })).sort((left, right) => left.distance - right.distance || left.index - right.index)
439
+ .slice(0, model.neighborCount);
440
+ const radius = neighbors.at(-1).distance;
441
+ const weighted = neighbors.map(neighbor => ({
442
+ ...neighbor,
443
+ weight: radius === 0 ? 1 : (1 - (neighbor.distance / radius) ** 3) ** 3
444
+ }));
445
+ const ordinary = weightedPrediction(weighted, model.x, model.y, xValue);
446
+ return Number.isFinite(ordinary) ? ordinary : restoreFiniteScale(
447
+ weightedPrediction(weighted, model.x, model.y, xValue, model.xScale, model.yScale),
448
+ model.yScale,
449
+ "LOESS regression prediction"
450
+ );
424
451
  }
425
452
 
426
453
  export function fitRegressionGroup(rows, { x, y, group, parameters }) {
@@ -430,7 +457,8 @@ export function fitRegressionGroup(rows, { x, y, group, parameters }) {
430
457
  y,
431
458
  group,
432
459
  confidenceMethod: parameters.confidenceMethod,
433
- level: parameters.level
460
+ level: parameters.level,
461
+ interval: parameters.interval
434
462
  });
435
463
  }
436
464
  if (parameters.method === "polynomial") {
@@ -440,7 +468,8 @@ export function fitRegressionGroup(rows, { x, y, group, parameters }) {
440
468
  group,
441
469
  confidenceMethod: parameters.confidenceMethod,
442
470
  level: parameters.level,
443
- degree: parameters.degree
471
+ degree: parameters.degree,
472
+ interval: parameters.interval
444
473
  });
445
474
  }
446
475
  return fitLoessGroup(rows, { x, y, group, span: parameters.span });
@@ -454,8 +483,8 @@ export function predictRegressionAt(model, xValue, parameters) {
454
483
  ? linearPrediction(model, xValue)
455
484
  : parameters.method === "polynomial"
456
485
  ? polynomial.prediction
457
- : model.fits.find(fit => fit.x === xValue).prediction;
458
- if (parameters.method === "loess") return { prediction };
486
+ : predictLoessAt(model, xValue);
487
+ if (parameters.method === "loess" || parameters.interval === false) return { prediction };
459
488
  const leverage = parameters.method === "linear"
460
489
  ? model.normalizedSxx !== undefined
461
490
  ? 1 / model.count + (
@@ -1,10 +1,39 @@
1
- import { cloneAndFreeze } from "../../core/immutable.js";
1
+ import { cloneAndFreeze, isPlainObject } from "../../core/immutable.js";
2
2
  import { validateGeneratedItemLimit } from "../../core/validation.js";
3
3
  import { normalizeConfidenceInterval } from
4
4
  "../statistics/confidenceInterval.js";
5
5
 
6
6
  const MAX_POLYNOMIAL_DEGREE = 32;
7
7
 
8
+ export function normalizeRegressionPredict(predict) {
9
+ if (predict === undefined) return undefined;
10
+ if (predict === false) throw new TypeError("Regression predict false is only valid in edits.");
11
+ if (!isPlainObject(predict)) {
12
+ throw new TypeError("Regression predict must be a plain object.");
13
+ }
14
+ const unknown = Object.keys(predict).find(key => !["values", "domain", "steps"].includes(key));
15
+ if (unknown !== undefined) throw new Error(`Unknown regression predict property "${unknown}".`);
16
+ const usesValues = Object.hasOwn(predict, "values");
17
+ const usesDomain = Object.hasOwn(predict, "domain") || Object.hasOwn(predict, "steps");
18
+ if (usesValues === usesDomain) throw new Error("Regression predict requires values or domain with steps.");
19
+ if (usesValues) {
20
+ const values = predict.values;
21
+ if (!Array.isArray(values) || values.length === 0 || values.some((value, index) =>
22
+ !Number.isFinite(value) || (index > 0 && !(value > values[index - 1]))
23
+ )) throw new TypeError("Regression predict values must be finite, unique, and strictly ascending.");
24
+ return cloneAndFreeze({ values: [...values] });
25
+ }
26
+ if (!Array.isArray(predict.domain) || predict.domain.length !== 2 ||
27
+ !predict.domain.every(Number.isFinite) || !(predict.domain[0] < predict.domain[1])) {
28
+ throw new TypeError("Regression predict domain must be two increasing finite numbers.");
29
+ }
30
+ if (!Number.isInteger(predict.steps) || predict.steps < 2) {
31
+ throw new RangeError("Regression predict steps must be an integer of at least two.");
32
+ }
33
+ validateGeneratedItemLimit(predict.steps, "Regression predict steps");
34
+ return cloneAndFreeze({ domain: [...predict.domain], steps: predict.steps });
35
+ }
36
+
8
37
  export function requireRegressionField(field, label) {
9
38
  if (typeof field !== "string" || field.length === 0) {
10
39
  throw new TypeError(`${label} must be a non-empty string.`);
@@ -45,6 +74,21 @@ export function normalizeRegressionParameters({
45
74
  if (span !== undefined) {
46
75
  throw new Error("Regression span requires the loess method.");
47
76
  }
77
+ if (interval === false) {
78
+ if (confidenceMethod !== undefined || level !== undefined || confidence !== undefined) {
79
+ throw new Error("Regression interval false cannot include confidence options.");
80
+ }
81
+ if (method === "polynomial") {
82
+ const resolvedDegree = degree ?? 2;
83
+ if (!Number.isInteger(resolvedDegree) || resolvedDegree < 1) {
84
+ throw new RangeError("Regression polynomial degree must be a positive integer.");
85
+ }
86
+ validateGeneratedItemLimit(resolvedDegree, "Regression polynomial degree", MAX_POLYNOMIAL_DEGREE);
87
+ return cloneAndFreeze({ method, degree: resolvedDegree, interval: false });
88
+ }
89
+ if (degree !== undefined) throw new Error("Regression degree requires the polynomial method.");
90
+ return cloneAndFreeze({ method, interval: false });
91
+ }
48
92
  if (level !== undefined && confidence !== undefined && level !== confidence) {
49
93
  throw new Error(
50
94
  "Regression level and confidence alias must match when both are provided."
@@ -96,7 +140,7 @@ export function validateRegressionTransform(transform) {
96
140
  const supported = [
97
141
  "type", "method", "x", "y", "groupBy", "confidenceMethod", "level",
98
142
  "confidence", "interval",
99
- "degree", "span"
143
+ "degree", "span", "predict", "missing"
100
144
  ];
101
145
  const unknown = Object.keys(transform).find(key => !supported.includes(key));
102
146
  if (unknown !== undefined) {
@@ -127,9 +171,14 @@ export function validateRegressionTransform(transform) {
127
171
  confidence: transform.confidence,
128
172
  interval: transform.interval
129
173
  });
174
+ normalizeRegressionPredict(transform.predict);
175
+ if (transform.missing !== undefined && !["error", "drop"].includes(transform.missing)) {
176
+ throw new Error('Regression missing must be "error" or "drop".');
177
+ }
130
178
  if (normalized.method === "loess") return transform;
131
179
  if (
132
180
  transform.confidence === undefined &&
181
+ transform.interval !== false &&
133
182
  (transform.confidenceMethod === undefined || transform.level === undefined)
134
183
  ) {
135
184
  throw new Error("Regression confidence provenance requires method and level.");
@@ -142,6 +191,7 @@ export function validateRegressionTransform(transform) {
142
191
 
143
192
  export function normalizeRegressionTransform(args = {}) {
144
193
  const parameters = normalizeRegressionParameters(args);
194
+ const predict = normalizeRegressionPredict(args.predict);
145
195
  const transform = {
146
196
  type: "regression",
147
197
  method: parameters.method,
@@ -153,11 +203,13 @@ export function normalizeRegressionTransform(args = {}) {
153
203
  : {}),
154
204
  ...(parameters.method === "loess"
155
205
  ? { span: parameters.span }
156
- : {
206
+ : parameters.interval === false ? { interval: false } : {
157
207
  confidenceMethod: parameters.confidenceMethod,
158
208
  level: parameters.level,
159
209
  interval: parameters.interval
160
- })
210
+ }),
211
+ ...(predict === undefined ? {} : { predict }),
212
+ ...(args.missing === undefined ? {} : { missing: args.missing })
161
213
  };
162
214
  validateRegressionTransform(transform);
163
215
  return cloneAndFreeze(transform);
@@ -108,6 +108,13 @@ export function normalizeScaleDefinition({
108
108
  domain: validateDomain(type, rawDomain),
109
109
  range: validateRange(type, rawRange)
110
110
  };
111
+ const emptyDomain = retainedValue(previous, patch, defaults, "emptyDomain", typeChanged);
112
+ if (emptyDomain !== undefined) {
113
+ if (!["preserve", "require-explicit"].includes(emptyDomain)) {
114
+ throw new Error('Scale emptyDomain must be "preserve" or "require-explicit".');
115
+ }
116
+ definition.emptyDomain = emptyDomain;
117
+ }
111
118
 
112
119
  for (const property of BOOLEAN_PROPERTIES) {
113
120
  let value = retainedValue(previous, patch, defaults, property, typeChanged);
@@ -26,6 +26,14 @@ export const COMPLETE_SCALE_TYPES = Object.freeze([
26
26
  ...new Set(Object.values(SCALE_TYPES_BY_ROLE).flat())
27
27
  ]);
28
28
 
29
+ // Dimensionless scale spacing is shared by generic and focused editors.
30
+ export const SCALE_OPTION_UNITS = Object.freeze({
31
+ padding: "band-fraction",
32
+ paddingInner: "band-fraction",
33
+ paddingOuter: "band-fraction",
34
+ align: "ratio"
35
+ });
36
+
29
37
  const SCALE_PROPERTY_TYPES = Object.freeze({
30
38
  midpoint: Object.freeze(["sequential"]),
31
39
  nice: Object.freeze(["linear", "log", "pow", "sqrt", "symlog", "time"]),
@@ -376,7 +376,7 @@ function hasActiveStroke(properties) {
376
376
  (properties.strokeWidth ?? 0) > 0;
377
377
  }
378
378
 
379
- function primitiveBounds(type, properties = {}) {
379
+ function primitiveBounds(type, properties = {}, profile) {
380
380
  const strokeExtent = hasActiveStroke(properties)
381
381
  ? properties.strokeWidth / 2
382
382
  : 0;
@@ -420,7 +420,7 @@ function primitiveBounds(type, properties = {}) {
420
420
  !Number.isFinite(properties.fontSize) ||
421
421
  typeof properties.text !== "string"
422
422
  ) return undefined;
423
- return resolveTextBounds(properties);
423
+ return resolveTextBounds(properties, profile);
424
424
  }
425
425
  if (type === "path") {
426
426
  const commands = properties.commands;
@@ -469,15 +469,15 @@ function ancestorOffset(graphicSpec, target) {
469
469
  }
470
470
  }
471
471
 
472
- function objectBounds(graphicSpec, id, object, ancestors, x, y) {
472
+ function objectBounds(graphicSpec, id, object, ancestors, x, y, profile) {
473
473
  if (ancestors.has(id)) {
474
474
  throw new Error(`Graphic attachment cycle includes "${id}".`);
475
475
  }
476
476
  ancestors.add(id);
477
477
  let result = object.items === undefined
478
- ? translateBounds(primitiveBounds(object.type, object.properties), x, y)
478
+ ? translateBounds(primitiveBounds(object.type, object.properties, profile), x, y)
479
479
  : unionBounds(object.items.map(item => translateBounds(
480
- primitiveBounds(item.type ?? object.type, item.properties),
480
+ primitiveBounds(item.type ?? object.type, item.properties, profile),
481
481
  x,
482
482
  y
483
483
  )));
@@ -489,7 +489,7 @@ function objectBounds(graphicSpec, id, object, ancestors, x, y) {
489
489
  throw new Error(`Unknown attached graphic "${childId}".`);
490
490
  }
491
491
  const bounds = objectBounds(
492
- graphicSpec, childId, child.object, ancestors, childX, childY
492
+ graphicSpec, childId, child.object, ancestors, childX, childY, profile
493
493
  );
494
494
  if (bounds !== undefined) {
495
495
  result = includeExtent(
@@ -501,23 +501,23 @@ function objectBounds(graphicSpec, id, object, ancestors, x, y) {
501
501
  return result;
502
502
  }
503
503
 
504
- export function resolveConcreteGraphicBounds(graphicSpec, target) {
504
+ export function resolveConcreteGraphicBounds(graphicSpec, target, profile) {
505
505
  const found = requireGraphic(graphicSpec, target);
506
506
  const [x, y] = ancestorOffset(graphicSpec, target);
507
507
  return requireFiniteBounds(found.kind === "item"
508
508
  ? translateBounds(
509
- primitiveBounds(found.object.type ?? found.owner.type, found.object.properties),
509
+ primitiveBounds(found.object.type ?? found.owner.type, found.object.properties, profile),
510
510
  x,
511
511
  y
512
512
  )
513
- : objectBounds(graphicSpec, found.id, found.object, new Set(), x, y));
513
+ : objectBounds(graphicSpec, found.id, found.object, new Set(), x, y, profile));
514
514
  }
515
515
 
516
- export function unionConcreteGraphicBounds(graphicSpec, targets) {
516
+ export function unionConcreteGraphicBounds(graphicSpec, targets, profile) {
517
517
  if (!Array.isArray(targets)) {
518
518
  throw new TypeError("Graphic bounds targets must be an array.");
519
519
  }
520
520
  return unionBounds(targets.map(target =>
521
- resolveConcreteGraphicBounds(graphicSpec, target)
521
+ resolveConcreteGraphicBounds(graphicSpec, target, profile)
522
522
  ));
523
523
  }
@@ -49,7 +49,7 @@ const ENCODING_PATHS = Object.freeze([
49
49
  const ENTITY_PATHS = Object.freeze({
50
50
  dataset: {
51
51
  collection: "datasets",
52
- properties: new Set(["source", "transform", "values"])
52
+ properties: new Set(["source", "transform", "schema", "values"])
53
53
  },
54
54
  layer: {
55
55
  collection: "layers",
@@ -64,6 +64,7 @@ const ENTITY_PATHS = Object.freeze({
64
64
  "transform",
65
65
  "mark.type",
66
66
  "mark.missing",
67
+ "derivedBindings.regression",
67
68
  "layout.mode",
68
69
  ...ENCODING_PATHS
69
70
  ])
@@ -71,7 +72,7 @@ const ENTITY_PATHS = Object.freeze({
71
72
  scale: {
72
73
  collection: "scales",
73
74
  properties: new Set([
74
- "type", "domain", "range", "nice", "zero", "clamp", "reverse",
75
+ "type", "domain", "range", "emptyDomain", "nice", "zero", "clamp", "reverse",
75
76
  "unknown", "base", "exponent", "constant", "interpolate", "radialMapping", "midpoint",
76
77
  "paddingInner", "paddingOuter", "padding", "align"
77
78
  ])
@@ -0,0 +1,88 @@
1
+ import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
+ import { normalizeTemporalValue } from "./scales/fields.js";
3
+
4
+ const ORDERS = new Set(["ascending", "descending"]);
5
+ const NULLS = new Set(["first", "last"]);
6
+ const TEMPORAL_UNITS = new Set(["year", "timestamp"]);
7
+
8
+ export function normalizeSortTransform({ sortBy }) {
9
+ if (!Array.isArray(sortBy) || sortBy.length === 0 ||
10
+ Array.from({ length: sortBy.length }, (_, index) => index).some(index => !Object.hasOwn(sortBy, index))) {
11
+ throw new TypeError("Sorted data sortBy must be a non-empty dense array.");
12
+ }
13
+ const keys = sortBy.map((key, index) => {
14
+ if (!isPlainObject(key)) throw new TypeError(`Sort key ${index} must be a plain object.`);
15
+ const unknown = Object.keys(key).find(name => !["field", "order", "nulls", "temporalUnit"].includes(name));
16
+ if (unknown !== undefined) throw new Error(`Unknown sort key property "${unknown}".`);
17
+ if (typeof key.field !== "string" || key.field.length === 0) {
18
+ throw new TypeError(`Sort key ${index} field must be a non-empty string.`);
19
+ }
20
+ const order = key.order ?? "ascending";
21
+ const nulls = key.nulls ?? "last";
22
+ if (!ORDERS.has(order)) throw new Error(`Unsupported sort order "${order}".`);
23
+ if (!NULLS.has(nulls)) throw new Error(`Unsupported sort null placement "${nulls}".`);
24
+ if (key.temporalUnit !== undefined && !TEMPORAL_UNITS.has(key.temporalUnit)) {
25
+ throw new Error(`Unsupported sort temporalUnit "${key.temporalUnit}".`);
26
+ }
27
+ return { field: key.field, order, nulls, ...(key.temporalUnit === undefined ? {} : { temporalUnit: key.temporalUnit }) };
28
+ });
29
+ if (new Set(keys.map(key => key.field)).size !== keys.length) {
30
+ throw new Error("Sorted data sortBy fields must be unique.");
31
+ }
32
+ return cloneAndFreeze({ type: "sort", sortBy: keys });
33
+ }
34
+
35
+ export function validateSortTransform(transform) {
36
+ if (!isPlainObject(transform)) throw new TypeError("Sort transform must be a plain object.");
37
+ const unknown = Object.keys(transform).find(key => !["type", "sortBy"].includes(key));
38
+ if (unknown !== undefined) throw new Error(`Unknown sort transform property "${unknown}".`);
39
+ if (transform.type !== "sort") throw new Error('Sort transform type must be "sort".');
40
+ normalizeSortTransform(transform);
41
+ }
42
+
43
+ function compareValues(left, right) {
44
+ if (left === right) return 0;
45
+ return left < right ? -1 : 1;
46
+ }
47
+
48
+ function normalizedKey(value, key, rowIndex) {
49
+ if (value === null || value === undefined) return { missing: true };
50
+ if (key.temporalUnit !== undefined) {
51
+ return { missing: false, value: normalizeTemporalValue(value, key.field, rowIndex, key.temporalUnit) };
52
+ }
53
+ if (!["number", "string", "boolean"].includes(typeof value) ||
54
+ (typeof value === "number" && !Number.isFinite(value))) {
55
+ throw new TypeError(`Sort field "${key.field}" requires finite number, string, boolean, or missing values at row ${rowIndex}.`);
56
+ }
57
+ return { missing: false, value, type: typeof value };
58
+ }
59
+
60
+ export function deriveSortedRows(values, transform) {
61
+ if (!Array.isArray(values)) throw new TypeError("Sort source values must be an array.");
62
+ validateSortTransform(transform);
63
+ const decorated = values.map((row, index) => ({
64
+ row,
65
+ index,
66
+ keys: transform.sortBy.map(key => normalizedKey(row[key.field], key, index))
67
+ }));
68
+ for (let keyIndex = 0; keyIndex < transform.sortBy.length; keyIndex += 1) {
69
+ const key = transform.sortBy[keyIndex];
70
+ const types = new Set(decorated.map(entry => entry.keys[keyIndex]).filter(value => !value.missing).map(value => value.type ?? "number"));
71
+ if (types.size > 1) throw new TypeError(`Sort field "${key.field}" contains mixed non-missing types.`);
72
+ }
73
+ decorated.sort((left, right) => {
74
+ for (let index = 0; index < transform.sortBy.length; index += 1) {
75
+ const key = transform.sortBy[index];
76
+ const a = left.keys[index];
77
+ const b = right.keys[index];
78
+ if (a.missing || b.missing) {
79
+ if (a.missing !== b.missing) return a.missing === (key.nulls === "first") ? -1 : 1;
80
+ continue;
81
+ }
82
+ const compared = compareValues(a.value, b.value);
83
+ if (compared !== 0) return key.order === "ascending" ? compared : -compared;
84
+ }
85
+ return left.index - right.index;
86
+ });
87
+ return decorated.map(entry => entry.row);
88
+ }