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,3 +1,4 @@
1
+ import { requireStringValue as nonEmptyString } from "../core/validation.js";
1
2
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
3
  import { validateUserId } from "../core/identifiers.js";
3
4
  import { validateOptionObject } from "../core/validation.js";
@@ -18,12 +19,6 @@ const DIMENSION_SCALE_OPTIONS = Object.freeze([
18
19
  "padding", "align", "unknown"
19
20
  ]);
20
21
 
21
- function nonEmptyString(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
22
 
28
23
  export function validateParallelKeyField(value) {
29
24
  return nonEmptyString(value, "Parallel key field");
@@ -0,0 +1,41 @@
1
+ import { normalizeTemporalValue } from "./scales/fields.js";
2
+
3
+ export const PARSE_TIME_FORMATS = Object.freeze([
4
+ "auto", "%Y-%m-%d", "%d/%m/%Y", "%m/%d/%Y"
5
+ ]);
6
+
7
+ export function validateParseTimeFormat(format = "auto") {
8
+ if (!PARSE_TIME_FORMATS.includes(format)) {
9
+ throw new Error(`Unsupported parseTime format "${format}".`);
10
+ }
11
+ return format;
12
+ }
13
+
14
+ // Explicit calendar formats are UTC and reject rollover, rather than guessing
15
+ // the day/month order. Invalid or nullish values remain missing data as null.
16
+ export function parseTimeValue(value, format = "auto") {
17
+ validateParseTimeFormat(format);
18
+ if (value === null || value === undefined) return null;
19
+ if (format === "auto") {
20
+ try { return normalizeTemporalValue(value); }
21
+ catch (error) {
22
+ if (error instanceof TypeError || error instanceof RangeError) return null;
23
+ throw error;
24
+ }
25
+ }
26
+ if (typeof value !== "string") return null;
27
+ const match = (format === "%Y-%m-%d"
28
+ ? /^(\d{4})-(\d{1,2})-(\d{1,2})$/
29
+ : /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/).exec(value.trim());
30
+ if (!match) return null;
31
+ const numbers = match.slice(1).map(Number);
32
+ const [year, month, day] = format === "%Y-%m-%d" ? numbers
33
+ : format === "%d/%m/%Y" ? [numbers[2], numbers[1], numbers[0]]
34
+ : [numbers[2], numbers[0], numbers[1]];
35
+ const date = new Date(0);
36
+ date.setUTCFullYear(year, month - 1, day);
37
+ date.setUTCHours(0, 0, 0, 0);
38
+ return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1
39
+ && date.getUTCDate() === day && Number.isFinite(date.getTime())
40
+ ? date.getTime() : null;
41
+ }
@@ -1,6 +1,12 @@
1
1
  import { freezeOwned, isPlainObject } from "../core/immutable.js";
2
2
  import { validateGeneratedItemLimit } from "../core/validation.js";
3
3
 
4
+ export const MAX_PATH_COMMANDS = 20_000;
5
+
6
+ export function validatePathCommandCount(count, label) {
7
+ return validateGeneratedItemLimit(count, label, MAX_PATH_COMMANDS);
8
+ }
9
+
4
10
  const COMMAND_KEYS = Object.freeze({
5
11
  M: Object.freeze(["op", "x", "y"]),
6
12
  L: Object.freeze(["op", "x", "y"]),
@@ -35,14 +41,15 @@ function validateCommand(command, index) {
35
41
  }
36
42
 
37
43
  export function validatePathCommands(commands) {
38
- if (!Array.isArray(commands) || commands.length < 2) {
39
- throw new TypeError("path.commands must contain at least two commands.");
44
+ if (!Array.isArray(commands) || commands.length < 1) {
45
+ throw new TypeError("path.commands must contain at least one command.");
40
46
  }
41
- validateGeneratedItemLimit(commands.length, "path.commands length");
47
+ validatePathCommandCount(commands.length, "path.commands length");
42
48
  commands.forEach(validateCommand);
43
49
  if (commands[0].op !== "M") {
44
50
  throw new Error("path.commands must start with M.");
45
51
  }
52
+ if (commands.length === 1) return commands;
46
53
  if (commands.slice(1).some(command => command.op === "M")) {
47
54
  throw new Error("path.commands supports one subpath and only one initial M.");
48
55
  }
@@ -79,7 +86,7 @@ export function buildLinearPathCommands(points, { close = false } = {}) {
79
86
  if (typeof close !== "boolean") {
80
87
  throw new TypeError("Linear path close must be a boolean.");
81
88
  }
82
- validateGeneratedItemLimit(
89
+ validatePathCommandCount(
83
90
  points.length + Number(close),
84
91
  "Linear path command count"
85
92
  );
@@ -14,7 +14,9 @@ export const POINT_SHAPES = cloneAndFreeze([
14
14
  "cross",
15
15
  "star",
16
16
  "hexagon",
17
- "wye"
17
+ "wye",
18
+ "wedge",
19
+ "arrow"
18
20
  ]);
19
21
 
20
22
  const SHAPE_SET = new Set(POINT_SHAPES);
@@ -98,7 +100,19 @@ function shapePolygon(shape) {
98
100
  cross: rotate(plus, Math.PI / 4),
99
101
  star: alternatingPolygon(5, 0.45),
100
102
  hexagon: regularPolygon(6),
101
- wye: alternatingPolygon(3, 0.28)
103
+ wye: alternatingPolygon(3, 0.28),
104
+ wedge: [
105
+ { x: 0, y: -2 / Math.sqrt(3) },
106
+ { x: -1 / 4, y: 1 / Math.sqrt(3) },
107
+ { x: 1 / 4, y: 1 / Math.sqrt(3) }
108
+ ],
109
+ // The directional arrow is anchored at its bounding-box center, not its centroid.
110
+ arrow: [
111
+ { x: -1 / 7, y: 1 }, { x: 1 / 7, y: 1 },
112
+ { x: 1 / 7, y: -1 / 8 }, { x: 2 / 5, y: -1 / 8 },
113
+ { x: 0, y: -1 }, { x: -2 / 5, y: -1 / 8 },
114
+ { x: -1 / 7, y: -1 / 8 }
115
+ ]
102
116
  }[shape];
103
117
  }
104
118
 
@@ -1,184 +1 @@
1
- import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
- import { validateOptionObject } from "../core/validation.js";
3
- import { validatePair } from "./scales/validation.js";
4
-
5
- const POLAR_FRAME_OPTIONS = Object.freeze(["center", "radius"]);
6
- const POLAR_CENTER_OPTIONS = Object.freeze(["x", "y"]);
7
- const POLAR_RADIUS_OPTIONS = Object.freeze(["unit", "value"]);
8
- const POLAR_RADIUS_UNITS = new Set(["fraction", "px"]);
9
-
10
- function validateBounds(bounds) {
11
- if (
12
- !isPlainObject(bounds) ||
13
- ![bounds.x, bounds.y, bounds.width, bounds.height].every(Number.isFinite) ||
14
- bounds.width < 0 ||
15
- bounds.height < 0
16
- ) {
17
- throw new TypeError(
18
- "Polar geometry requires finite, non-negative graphical bounds."
19
- );
20
- }
21
- return bounds;
22
- }
23
-
24
- function finiteUnitInterval(value, label) {
25
- if (!Number.isFinite(value) || value < 0 || value > 1) {
26
- throw new RangeError(`${label} must be a finite number from 0 through 1.`);
27
- }
28
- return value;
29
- }
30
-
31
- export function normalizePolarFrameOptions(value) {
32
- if (value === "auto") return value;
33
- if (!isPlainObject(value)) {
34
- throw new TypeError('Polar frame must be "auto" or a plain object.');
35
- }
36
- validateOptionObject(value, POLAR_FRAME_OPTIONS, "polar frame");
37
- const center = value.center ?? {};
38
- if (!isPlainObject(center)) {
39
- throw new TypeError("Polar frame center must be a plain object.");
40
- }
41
- validateOptionObject(center, POLAR_CENTER_OPTIONS, "polar frame center");
42
- const radius = value.radius ?? { unit: "fraction", value: 1 };
43
- if (!isPlainObject(radius)) {
44
- throw new TypeError("Polar frame radius must be a plain object.");
45
- }
46
- validateOptionObject(radius, POLAR_RADIUS_OPTIONS, "polar frame radius", {
47
- allowEmpty: false,
48
- emptyError: TypeError
49
- });
50
- if (!POLAR_RADIUS_UNITS.has(radius.unit)) {
51
- throw new Error(`Unknown Polar frame radius unit "${radius.unit}".`);
52
- }
53
- if (!Number.isFinite(radius.value) || radius.value <= 0) {
54
- throw new RangeError("Polar frame radius must be a positive finite number.");
55
- }
56
- if (radius.unit === "fraction" && radius.value > 1) {
57
- throw new RangeError("Polar frame radius fraction must not exceed 1.");
58
- }
59
- return cloneAndFreeze({
60
- center: {
61
- x: finiteUnitInterval(center.x ?? 0.5, "Polar frame center x"),
62
- y: finiteUnitInterval(center.y ?? 0.5, "Polar frame center y")
63
- },
64
- radius: {
65
- unit: radius.unit,
66
- value: radius.value
67
- }
68
- });
69
- }
70
-
71
- export function resolvePolarFrame(bounds, requestedFrame = "auto") {
72
- validateBounds(bounds);
73
- if (requestedFrame === undefined || requestedFrame === "auto") {
74
- return cloneAndFreeze({
75
- centerX: bounds.x + bounds.width / 2,
76
- centerY: bounds.y + bounds.height / 2,
77
- availableRadius: Math.min(bounds.width, bounds.height) / 2
78
- });
79
- }
80
- const requested = normalizePolarFrameOptions(requestedFrame);
81
- const centerX = bounds.x + bounds.width * requested.center.x;
82
- const centerY = bounds.y + bounds.height * requested.center.y;
83
- const maximumRadius = Math.min(
84
- centerX - bounds.x,
85
- bounds.x + bounds.width - centerX,
86
- centerY - bounds.y,
87
- bounds.y + bounds.height - centerY
88
- );
89
- if (!Number.isFinite(maximumRadius) || maximumRadius <= 0) {
90
- throw new RangeError(
91
- "Polar frame center must leave a positive radius inside its bounds."
92
- );
93
- }
94
- const availableRadius = requested.radius.unit === "fraction"
95
- ? maximumRadius * requested.radius.value
96
- : requested.radius.value;
97
- if (availableRadius > maximumRadius) {
98
- throw new RangeError(
99
- `Polar frame radius ${availableRadius} exceeds the maximum radius ${maximumRadius}.`
100
- );
101
- }
102
- return cloneAndFreeze({
103
- centerX,
104
- centerY,
105
- availableRadius
106
- });
107
- }
108
-
109
- export function validateThetaRange(range) {
110
- if (range === "auto") return range;
111
- const validated = validatePair(range, "Theta scale range");
112
- if (Math.abs(validated[1] - validated[0]) > 360) {
113
- throw new RangeError("Theta scale range span must not exceed 360 degrees.");
114
- }
115
- return validated;
116
- }
117
-
118
- export function validateRadialRange(range, availableRadius) {
119
- if (range === "auto") return range;
120
- const validated = validatePair(range, "Radius scale range");
121
- if (validated.some(value => value < 0)) {
122
- throw new RangeError("Radius scale range values must be non-negative.");
123
- }
124
- if (
125
- availableRadius !== undefined &&
126
- validated.some(value => value > availableRadius)
127
- ) {
128
- throw new RangeError(
129
- `Radius scale range must fit within the available radius ${availableRadius}.`
130
- );
131
- }
132
- return validated;
133
- }
134
-
135
- export function resolvePolarScaleRange(range, channel, bounds, frame) {
136
- if (channel === "theta") {
137
- return range === "auto"
138
- ? cloneAndFreeze([0, 360])
139
- : validateThetaRange(range);
140
- }
141
- if (channel !== "radius") {
142
- throw new Error(`Unknown Polar position channel "${channel}".`);
143
- }
144
- const resolvedFrame = frame ?? resolvePolarFrame(bounds);
145
- return range === "auto"
146
- ? cloneAndFreeze([0, resolvedFrame.availableRadius])
147
- : validateRadialRange(range, resolvedFrame.availableRadius);
148
- }
149
-
150
- export function polarDirection(theta) {
151
- if (!Number.isFinite(theta)) {
152
- throw new TypeError("Polar theta must be a finite number of degrees.");
153
- }
154
- const angle = theta * Math.PI / 180;
155
- return cloneAndFreeze({
156
- x: Math.sin(angle),
157
- y: -Math.cos(angle)
158
- });
159
- }
160
-
161
- export function polarToCartesian({ theta, radius, frame }) {
162
- const direction = polarDirection(theta);
163
- if (!Number.isFinite(radius) || radius < 0) {
164
- throw new TypeError("Polar radius must be a non-negative finite number.");
165
- }
166
- const resolvedFrame = frame?.availableRadius === undefined
167
- ? resolvePolarFrame(frame)
168
- : frame;
169
- if (
170
- ![resolvedFrame.centerX, resolvedFrame.centerY,
171
- resolvedFrame.availableRadius].every(Number.isFinite)
172
- ) {
173
- throw new TypeError("Polar frame must contain finite geometry.");
174
- }
175
- if (radius > resolvedFrame.availableRadius) {
176
- throw new RangeError(
177
- `Polar radius ${radius} exceeds the available radius ${resolvedFrame.availableRadius}.`
178
- );
179
- }
180
- return cloneAndFreeze({
181
- x: resolvedFrame.centerX + radius * direction.x,
182
- y: resolvedFrame.centerY + radius * direction.y
183
- });
184
- }
1
+ import{cloneAndFreeze as n,isPlainObject as l}from"../core/immutable.js";import{validateOptionObject as u}from"../core/validation.js";import{validatePair as c}from"./scales/validation.js";const w=Object.freeze(["center","radius","overflow"]),h=Object.freeze(["x","y"]),d=Object.freeze(["unit","value"]),p=new Set(["fraction","px"]);function v(e){if(!l(e)||![e.x,e.y,e.width,e.height].every(Number.isFinite)||e.width<0||e.height<0)throw new TypeError("Polar geometry requires finite, non-negative graphical bounds.");return e}function s(e,r){if(!Number.isFinite(e)||e<0||e>1)throw new RangeError(`${r} must be a finite number from 0 through 1.`);return e}function R(e){if(e==="auto")return e;if(!l(e))throw new TypeError('Polar frame must be "auto" or a plain object.');if(u(e,w,"polar frame"),e.overflow!==void 0&&!["error","allow"].includes(e.overflow))throw new Error("Polar frame overflow must be error or allow.");const r=e.center??{};if(!l(r))throw new TypeError("Polar frame center must be a plain object.");u(r,h,"polar frame center");const t=e.radius??{unit:"fraction",value:1};if(!l(t))throw new TypeError("Polar frame radius must be a plain object.");if(u(t,d,"polar frame radius",{allowEmpty:!1,emptyError:TypeError}),!p.has(t.unit))throw new Error(`Unknown Polar frame radius unit "${t.unit}".`);if(!Number.isFinite(t.value)||t.value<=0)throw new RangeError("Polar frame radius must be a positive finite number.");if(t.unit==="fraction"&&t.value>1)throw new RangeError("Polar frame radius fraction must not exceed 1.");return n({center:{x:s(r.x??.5,"Polar frame center x"),y:s(r.y??.5,"Polar frame center y")},radius:{unit:t.unit,value:t.value},...e.overflow==="allow"?{overflow:"allow"}:{}})}function m(e,r="auto"){if(v(e),r===void 0||r==="auto")return n({centerX:e.x+e.width/2,centerY:e.y+e.height/2,availableRadius:Math.min(e.width,e.height)/2});const t=R(r),a=e.x+e.width*t.center.x,i=e.y+e.height*t.center.y,o=Math.min(a-e.x,e.x+e.width-a,i-e.y,e.y+e.height-i);if(!Number.isFinite(o)||o<=0)throw new RangeError("Polar frame center must leave a positive radius inside its bounds.");const f=t.radius.unit==="fraction"?o*t.radius.value:t.radius.value;if(f>o&&t.overflow!=="allow")throw new RangeError(`Polar frame radius ${f} exceeds the maximum radius ${o}.`);return n({centerX:a,centerY:i,availableRadius:f})}function P(e){if(e==="auto")return e;const r=c(e,"Theta scale range");if(Math.abs(r[1]-r[0])>360)throw new RangeError("Theta scale range span must not exceed 360 degrees.");return r}function x(e,r){if(e==="auto")return e;const t=c(e,"Radius scale range");if(t.some(a=>a<0))throw new RangeError("Radius scale range values must be non-negative.");if(r!==void 0&&t.some(a=>a>r))throw new RangeError(`Radius scale range must fit within the available radius ${r}.`);return t}function T(e,r,t,a){if(r==="theta")return e==="auto"?n([0,360]):P(e);if(r!=="radius")throw new Error(`Unknown Polar position channel "${r}".`);const i=a??m(t);return e==="auto"?n([0,i.availableRadius]):x(e,i.availableRadius)}function y(e){if(!Number.isFinite(e))throw new TypeError("Polar theta must be a finite number of degrees.");const r=e*Math.PI/180;return n({x:Math.sin(r),y:-Math.cos(r)})}function b({theta:e,radius:r,frame:t}){const a=y(e);if(!Number.isFinite(r)||r<0)throw new TypeError("Polar radius must be a non-negative finite number.");const i=t?.availableRadius===void 0?m(t):t;if(![i.centerX,i.centerY,i.availableRadius].every(Number.isFinite))throw new TypeError("Polar frame must contain finite geometry.");if(r>i.availableRadius)throw new RangeError(`Polar radius ${r} exceeds the available radius ${i.availableRadius}.`);return n({x:i.centerX+r*a.x,y:i.centerY+r*a.y})}export{R as normalizePolarFrameOptions,y as polarDirection,b as polarToCartesian,m as resolvePolarFrame,T as resolvePolarScaleRange,x as validateRadialRange,P as validateThetaRange};
@@ -6,14 +6,14 @@ export const POSITION_FIELD_COMPATIBILITY = Object.freeze({
6
6
  radius: Object.freeze(["quantitative"])
7
7
  }),
8
8
  line: Object.freeze({
9
- x: Object.freeze(["quantitative", "temporal"]),
9
+ x: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"]),
10
10
  y: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"]),
11
11
  theta: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"]),
12
12
  radius: Object.freeze(["quantitative"])
13
13
  }),
14
14
  area: Object.freeze({
15
- x: Object.freeze(["quantitative", "temporal"]),
16
- y: Object.freeze(["quantitative", "temporal"])
15
+ x: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"]),
16
+ y: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"])
17
17
  }),
18
18
  arc: Object.freeze({
19
19
  theta: Object.freeze(["quantitative", "ordinal", "nominal"]),
@@ -3,7 +3,7 @@ import {
3
3
  readNominalField,
4
4
  readQuantitativeField
5
5
  } from "../scales/index.js";
6
- import { fitRegressionGroup, predictRegressionAt } from "./models.js";
6
+ import { fitRegressionGroup, predictRegressionAt, linearRegressionStatistics } from "./models.js";
7
7
  import { requireFiniteResult } from "../numeric.js";
8
8
  import {
9
9
  validateGeneratedItemLimit,
@@ -24,6 +24,7 @@ export function deriveRegression(values, {
24
24
  method,
25
25
  degree,
26
26
  span,
27
+ robustIterations,
27
28
  confidenceMethod,
28
29
  level,
29
30
  confidence,
@@ -43,6 +44,7 @@ export function deriveRegression(values, {
43
44
  method,
44
45
  degree,
45
46
  span,
47
+ robustIterations,
46
48
  confidenceMethod,
47
49
  level,
48
50
  confidence,
@@ -79,6 +81,7 @@ export function deriveRegression(values, {
79
81
  }
80
82
  const groups = [...groupedRows.keys()];
81
83
  const models = [];
84
+ const statistics = [];
82
85
  const rows = [];
83
86
  let work = 0;
84
87
 
@@ -102,7 +105,11 @@ export function deriveRegression(values, {
102
105
  const size = parameters.degree + 1;
103
106
  work += groupRows.length * size ** 2 + size ** 3;
104
107
  } else if (parameters.method === "loess") {
105
- work += groupRows.length * xValues.length *
108
+ const observedSet = new Set(observedX);
109
+ const newPredictions = predict === undefined ? 0 :
110
+ xValues.filter(value => !observedSet.has(value)).length;
111
+ work += groupRows.length *
112
+ (observedX.length * (1 + (parameters.robustIterations ?? 0)) + newPredictions) *
106
113
  Math.ceil(Math.log2(groupRows.length + 1));
107
114
  }
108
115
  validateWorkLimit(work, "Regression computation");
@@ -113,6 +120,10 @@ export function deriveRegression(values, {
113
120
  parameters
114
121
  });
115
122
  models.push({ ...(groupBy === undefined ? {} : { group }), ...model, xValues });
123
+ if (parameters.method === "linear") statistics.push({
124
+ ...(groupBy === undefined ? {} : { group }),
125
+ ...linearRegressionStatistics(groupRows, model, { x, y })
126
+ });
116
127
 
117
128
  for (const xValue of xValues) {
118
129
  const prediction = predictRegressionAt(model, xValue, parameters);
@@ -157,6 +168,9 @@ export function deriveRegression(values, {
157
168
  parameters,
158
169
  groups,
159
170
  models,
171
+ ...(parameters.method === "linear" ? {
172
+ fitStatistics: { version: 1, method: "linear", groups: statistics }
173
+ } : {}),
160
174
  values: rows,
161
175
  ...(missing === undefined ? {} : {
162
176
  report: {
@@ -376,7 +376,56 @@ function weightedPrediction(items, x, y, xValue, xScale = 1, yScale = 1) {
376
376
  return meanY - (variance === 0 ? 0 : covariance / variance) * meanDifference;
377
377
  }
378
378
 
379
- function fitLoessGroup(rows, { x, y, group, span }) {
379
+ function applyResidualWeights(neighbors, residualWeights) {
380
+ if (!neighbors.some(item => item.weight > 0)) {
381
+ neighbors = neighbors.map(item => ({ ...item, weight: 1 }));
382
+ }
383
+ if (residualWeights === undefined) return neighbors;
384
+ const weighted = neighbors.map(item => ({
385
+ ...item, weight: item.weight * residualWeights[item.index]
386
+ }));
387
+ return weighted.some(item => item.weight > 0) ? weighted : neighbors;
388
+ }
389
+
390
+ function loessPrediction(rows, {
391
+ x, y, xValue, xScale, yScale, neighborCount, residualWeights, scaled = false
392
+ }, label) {
393
+ const rawDistances = rows.map(row => Math.abs(row[x] - xValue));
394
+ const scaledDistances = scaled || rawDistances.some(value => !Number.isFinite(value));
395
+ const neighbors = rows
396
+ .map((row, index) => ({
397
+ row,
398
+ index,
399
+ distance: scaledDistances
400
+ ? Math.abs(row[x] / xScale - xValue / xScale)
401
+ : rawDistances[index]
402
+ }))
403
+ .sort((left, right) =>
404
+ left.distance - right.distance || left.index - right.index
405
+ )
406
+ .slice(0, neighborCount);
407
+ const radius = neighbors.at(-1).distance;
408
+ let weighted = neighbors.map(neighbor => ({
409
+ ...neighbor,
410
+ weight: radius === 0
411
+ ? 1
412
+ : (1 - (neighbor.distance / radius) ** 3) ** 3
413
+ }));
414
+ weighted = applyResidualWeights(weighted, residualWeights);
415
+ const ordinary = weightedPrediction(weighted, x, y, xValue);
416
+ return {
417
+ prediction: Number.isFinite(ordinary)
418
+ ? ordinary
419
+ : restoreFiniteScale(
420
+ weightedPrediction(weighted, x, y, xValue, xScale, yScale),
421
+ yScale,
422
+ label
423
+ ),
424
+ neighborIndices: weighted.map(item => item.index)
425
+ };
426
+ }
427
+
428
+ function fitLoessGroup(rows, { x, y, group, span, robustIterations = 0 }) {
380
429
  const groupLabel = group === undefined ? "all" : String(group);
381
430
  const label = `LOESS regression group "${groupLabel}"`;
382
431
  if (rows.length < 2 || new Set(rows.map(row => row[x])).size < 2) {
@@ -390,67 +439,100 @@ function fitLoessGroup(rows, { x, y, group, span }) {
390
439
  .sort((left, right) => left - right);
391
440
  const xScale = maximumMagnitude(rows.map(row => row[x])) || 1;
392
441
  const yScale = maximumMagnitude(rows.map(row => row[y])) || 1;
393
- const fits = xValues.map(xValue => {
394
- const rawDistances = rows.map(row => Math.abs(row[x] - xValue));
395
- const scaledDistances = rawDistances.some(value => !Number.isFinite(value));
396
- const neighbors = rows
397
- .map((row, index) => ({
398
- row,
399
- index,
400
- distance: scaledDistances
401
- ? Math.abs(row[x] / xScale - xValue / xScale)
402
- : rawDistances[index]
403
- }))
404
- .sort((left, right) =>
405
- left.distance - right.distance || left.index - right.index
406
- )
407
- .slice(0, neighborCount);
408
- const radius = neighbors.at(-1).distance;
409
- const weighted = neighbors.map(neighbor => ({
410
- ...neighbor,
411
- weight: radius === 0
412
- ? 1
413
- : (1 - (neighbor.distance / radius) ** 3) ** 3
414
- }));
415
- const ordinary = weightedPrediction(weighted, x, y, xValue);
416
- return {
417
- x: xValue,
418
- prediction: Number.isFinite(ordinary)
419
- ? ordinary
420
- : restoreFiniteScale(
421
- weightedPrediction(weighted, x, y, xValue, xScale, yScale),
422
- yScale,
423
- `${label} prediction`
424
- ),
425
- neighborIndices: weighted.map(item => item.index)
426
- };
427
- });
428
- return { count: rows.length, span, neighborCount, fits, rows, x, y, xScale, yScale };
442
+ const fitAtObserved = residualWeights => xValues.map(xValue => ({
443
+ x: xValue,
444
+ ...loessPrediction(rows, {
445
+ x, y, xValue, xScale, yScale, neighborCount, residualWeights
446
+ }, `${label} prediction`)
447
+ }));
448
+ let fits = fitAtObserved();
449
+ let residualWeights;
450
+ for (let iteration = 0; iteration < robustIterations; iteration += 1) {
451
+ const predictions = new Map(fits.map(fit => [fit.x, fit.prediction]));
452
+ const residuals = rows.map(row => Math.abs(row[y] / yScale - predictions.get(row[x]) / yScale));
453
+ const sorted = [...residuals].sort((a, b) => a - b);
454
+ const middle = Math.floor(sorted.length / 2);
455
+ const median = sorted.length % 2 === 0
456
+ ? (sorted[middle - 1] + sorted[middle]) / 2 : sorted[middle];
457
+ const tolerance = 64 * Number.EPSILON;
458
+ const cutoff = 6 * median;
459
+ const nextWeights = residuals.map(residual => {
460
+ if (cutoff <= tolerance) return residual <= tolerance ? 1 : 0;
461
+ const ratio = residual / cutoff;
462
+ return ratio >= 1 ? 0 : (1 - ratio * ratio) ** 2;
463
+ });
464
+ if (nextWeights.every((weight, index) => weight === (residualWeights?.[index] ?? 1))) break;
465
+ residualWeights = nextWeights;
466
+ fits = fitAtObserved(residualWeights);
467
+ }
468
+ return { count: rows.length, span, neighborCount, fits, rows, x, y, xScale, yScale,
469
+ ...(robustIterations === 0 ? {} : { robustIterations, residualWeights: residualWeights ?? rows.map(() => 1) }) };
429
470
  }
430
471
 
431
472
  function predictLoessAt(model, xValue) {
432
473
  const found = model.fits.find(fit => fit.x === xValue);
433
474
  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"
475
+ return loessPrediction(model.rows, {
476
+ ...model, xValue, scaled: true
477
+ }, "LOESS regression prediction").prediction;
478
+ }
479
+
480
+ function fitExponentialGroup(rows, { x, y, group }) {
481
+ const label = `Regression group "${group === undefined ? "all" : String(group)}"`;
482
+ if (rows.length < 2) throw new Error(`${label} requires at least two rows.`);
483
+ if (rows.some(row => !(row[y] > 0))) {
484
+ throw new RangeError("Exponential regression y values must be strictly positive.");
485
+ }
486
+ const xValues = rows.map(row => row[x]);
487
+ if (new Set(xValues).size < 2) throw new Error(`${label} requires varying x values.`);
488
+ const responseScale = maximumMagnitude(rows.map(row => row[y]));
489
+ const xMagnitude = maximumMagnitude(xValues) || 1;
490
+ const weights = rows.map(row => row[y] / responseScale);
491
+ const weightSum = weights.reduce((sum, value) => sum + value, 0);
492
+ const logs = rows.map(row => Math.log(row[y]));
493
+ const meanX = restoreFiniteScale(rows.reduce((sum, row, index) =>
494
+ sum + weights[index] * (row[x] / xMagnitude), 0) / weightSum,
495
+ xMagnitude, `${label} weighted x mean`);
496
+ const meanLogY = logs.reduce((sum, value, index) => sum + weights[index] * value, 0) / weightSum;
497
+ const [centeredX, xScale] = normalizedCoordinates(xValues, meanX);
498
+ const variance = centeredX.reduce((sum, value, index) => sum + weights[index] * value ** 2, 0);
499
+ if (variance === 0) throw new Error(`${label} requires varying x values.`);
500
+ const covariance = centeredX.reduce((sum, value, index) =>
501
+ sum + weights[index] * value * (logs[index] - meanLogY), 0);
502
+ const rate = restoreFiniteRatio(covariance / variance, 1, xScale, `${label} exponential rate`);
503
+ const logAmplitude = requireFiniteResult(meanLogY - rate * meanX, `${label} exponential log amplitude`);
504
+ const amplitude = requireFiniteResult(Math.exp(logAmplitude), `${label} exponential amplitude`);
505
+ if (amplitude === 0) throw new RangeError("Exponential regression amplitude must be positive and representable.");
506
+ return { count: rows.length, amplitude, rate, logAmplitude };
507
+ }
508
+
509
+ export function linearRegressionStatistics(rows, model, { x, y }) {
510
+ const responseScale = maximumMagnitude(rows.map(row => row[y])) || 1;
511
+ const [coordinates, xScale] = normalizedCoordinates(rows.map(row => row[x]), model.meanX);
512
+ const normalizedSlope = model.normalizedResponseSlope ?? (
513
+ Number.isFinite(model.slope * xScale) && (model.slope * xScale !== 0 || model.slope === 0)
514
+ ? model.slope * xScale / responseScale
515
+ : restoreFiniteRatio(model.slope, xScale, responseScale, "Linear regression normalized slope")
450
516
  );
517
+ let residualSquares = 0;
518
+ let totalSquares = 0;
519
+ rows.forEach((row, index) => {
520
+ const difference = row[y] - model.meanY;
521
+ const centered = Number.isFinite(difference)
522
+ ? difference / responseScale
523
+ : row[y] / responseScale - model.meanY / responseScale;
524
+ const residual = centered - normalizedSlope * coordinates[index];
525
+ totalSquares += centered ** 2;
526
+ residualSquares += residual ** 2;
527
+ });
528
+ return {
529
+ count: rows.length, intercept: model.intercept, slope: model.slope,
530
+ rSquared: totalSquares === 0 ? null : 1 - residualSquares / totalSquares
531
+ };
451
532
  }
452
533
 
453
534
  export function fitRegressionGroup(rows, { x, y, group, parameters }) {
535
+ if (parameters.method === "exp") return fitExponentialGroup(rows, { x, y, group });
454
536
  if (parameters.method === "linear") {
455
537
  return fitLinearGroup(rows, {
456
538
  x,
@@ -472,7 +554,8 @@ export function fitRegressionGroup(rows, { x, y, group, parameters }) {
472
554
  interval: parameters.interval
473
555
  });
474
556
  }
475
- return fitLoessGroup(rows, { x, y, group, span: parameters.span });
557
+ return fitLoessGroup(rows, { x, y, group, span: parameters.span,
558
+ robustIterations: parameters.robustIterations });
476
559
  }
477
560
 
478
561
  export function predictRegressionAt(model, xValue, parameters) {
@@ -481,9 +564,14 @@ export function predictRegressionAt(model, xValue, parameters) {
481
564
  : undefined;
482
565
  const prediction = parameters.method === "linear"
483
566
  ? linearPrediction(model, xValue)
567
+ : parameters.method === "exp"
568
+ ? requireFiniteResult(Math.exp(model.logAmplitude + model.rate * xValue), "Exponential regression prediction")
484
569
  : parameters.method === "polynomial"
485
570
  ? polynomial.prediction
486
571
  : predictLoessAt(model, xValue);
572
+ if (parameters.method === "exp" && prediction === 0) {
573
+ throw new RangeError("Exponential regression prediction must be positive and representable.");
574
+ }
487
575
  if (parameters.method === "loess" || parameters.interval === false) return { prediction };
488
576
  const leverage = parameters.method === "linear"
489
577
  ? model.normalizedSxx !== undefined