ggaction 0.0.17 → 0.0.18

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 (263) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +2 -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 +16 -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 +4 -4
  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/edit.js +1 -1
  35. package/src/actions/data/regression.js +2 -1
  36. package/src/actions/data/shared.js +3 -2
  37. package/src/actions/data/stack.js +3 -1
  38. package/src/actions/encodings/appearance.js +37 -29
  39. package/src/actions/encodings/barWidth.js +36 -11
  40. package/src/actions/encodings/channels.js +0 -1
  41. package/src/actions/encodings/color/continuous.js +1 -1
  42. package/src/actions/encodings/color/index.js +1 -1
  43. package/src/actions/encodings/color/policy.js +10 -3
  44. package/src/actions/encodings/density/resolve.js +1 -6
  45. package/src/actions/encodings/position/policies/arc.js +12 -10
  46. package/src/actions/encodings/position/policies/area.js +3 -3
  47. package/src/actions/encodings/position/policies/bar.js +15 -6
  48. package/src/actions/encodings/position/policies/line.js +17 -6
  49. package/src/actions/encodings/position/resolve.js +3 -2
  50. package/src/actions/encodings/ranged.js +17 -0
  51. package/src/actions/encodings/remove.js +3 -2
  52. package/src/actions/encodings/seriesLayout.js +1 -0
  53. package/src/actions/encodings/text.js +6 -3
  54. package/src/actions/errorBands/create.js +2 -2
  55. package/src/actions/errorBands/edit.js +2 -1
  56. package/src/actions/facets/actions.js +158 -240
  57. package/src/actions/gradientPlots/resolve.js +2 -17
  58. package/src/actions/guides/applicability.js +1 -1
  59. package/src/actions/guides/axes/axis.js +16 -4
  60. package/src/actions/guides/axes/edit.js +4 -4
  61. package/src/actions/guides/axes/labels.js +120 -46
  62. package/src/actions/guides/axes/lines.js +6 -5
  63. package/src/actions/guides/axes/parallel/lifecycle.js +1 -1
  64. package/src/actions/guides/axes/parallel/policy.js +8 -7
  65. package/src/actions/guides/axes/remove.js +3 -2
  66. package/src/actions/guides/axes/tickGroups.js +5 -2
  67. package/src/actions/guides/axes/ticks.js +6 -3
  68. package/src/actions/guides/axes/titles.js +71 -26
  69. package/src/actions/guides/basic.js +2 -0
  70. package/src/actions/guides/facadeLegend.js +10 -2
  71. package/src/actions/guides/grids/grid.js +7 -1
  72. package/src/actions/guides/grids/resolve.js +59 -12
  73. package/src/actions/guides/legends/blocks.js +3 -1
  74. package/src/actions/guides/legends/categorical/actions.js +70 -17
  75. package/src/actions/guides/legends/categorical/components.js +12 -8
  76. package/src/actions/guides/legends/categorical/index.js +6 -0
  77. package/src/actions/guides/legends/categorical/layout.js +37 -8
  78. package/src/actions/guides/legends/categorical/options.js +31 -20
  79. package/src/actions/guides/legends/categorical/resolve.js +2 -2
  80. package/src/actions/guides/legends/categorical/symbols.js +18 -12
  81. package/src/actions/guides/legends/categorical/validation.js +1 -6
  82. package/src/actions/guides/legends/continuous/colorSymbols.js +1 -0
  83. package/src/actions/guides/legends/continuous/common.js +1 -447
  84. package/src/actions/guides/legends/continuous/gradient.js +47 -34
  85. package/src/actions/guides/legends/continuous/interval.js +4 -4
  86. package/src/actions/guides/legends/continuous/opacity.js +19 -12
  87. package/src/actions/guides/legends/edit.js +1 -604
  88. package/src/actions/guides/legends/focused.js +2 -2
  89. package/src/actions/guides/legends/group.js +1 -0
  90. package/src/actions/guides/legends/lane.js +1 -516
  91. package/src/actions/guides/legends/remove.js +3 -0
  92. package/src/actions/guides/legends/sampling.js +1 -1
  93. package/src/actions/guides/legends/size.js +51 -43
  94. package/src/actions/guides/legends/strokeWidth.js +11 -29
  95. package/src/actions/guides/legends/target.js +3 -3
  96. package/src/actions/guides/legends/transition.js +21 -7
  97. package/src/actions/guides/polar/axes/labels.js +4 -2
  98. package/src/actions/guides/polar/axes/titles.js +7 -5
  99. package/src/actions/guides/polar/resolve.js +6 -14
  100. package/src/actions/index.js +16 -16
  101. package/src/actions/marks/arc/actions.js +3 -10
  102. package/src/actions/marks/area/actions.js +8 -5
  103. package/src/actions/marks/area/materialize.js +8 -9
  104. package/src/actions/marks/bar/create.js +10 -4
  105. package/src/actions/marks/bar/edit.js +8 -4
  106. package/src/actions/marks/bar/materialize.js +9 -6
  107. package/src/actions/marks/line/actions.js +11 -4
  108. package/src/actions/marks/line/materialize.js +9 -5
  109. package/src/actions/marks/point/edit.js +1 -1
  110. package/src/actions/marks/rect/actions.js +3 -3
  111. package/src/actions/marks/remove.js +2 -0
  112. package/src/actions/marks/rule/actions.js +7 -4
  113. package/src/actions/marks/text/actions.js +17 -5
  114. package/src/actions/marks/text/layout.js +3 -0
  115. package/src/actions/primitives/semanticValidation/layer.js +4 -0
  116. package/src/actions/regression/create.js +1 -1
  117. package/src/actions/regression/edit.js +3 -3
  118. package/src/actions/regression/resolve.js +2 -6
  119. package/src/actions/scales/channels.js +9 -4
  120. package/src/actions/scales/consumers/families.js +16 -5
  121. package/src/actions/scales/create.js +8 -3
  122. package/src/actions/scales/definitions.js +1 -425
  123. package/src/actions/scales/edit.js +2 -2
  124. package/src/actions/scales/editPolicy.js +9 -3
  125. package/src/actions/scales/materialize.js +1 -1
  126. package/src/actions/scales/preview.js +21 -1
  127. package/src/actions/scales/quantitativeColor.js +3 -3
  128. package/src/actions/selection/actions.js +1 -13
  129. package/src/actions/theme/reconcile.js +81 -27
  130. package/src/actions/theme/state.js +12 -0
  131. package/src/actions/titles/actions.js +5 -18
  132. package/src/actions/titles/resolve.js +41 -19
  133. package/src/actions/violinPlots/edit.js +3 -20
  134. package/src/core/compositionState.js +4 -1
  135. package/src/core/font.js +9 -2
  136. package/src/core/optionRequirements.js +1 -0
  137. package/src/core/resourceReferences.js +3 -0
  138. package/src/core/textMetricProfile.js +3 -2
  139. package/src/core/textMetrics.js +41 -4
  140. package/src/core/validation.js +26 -0
  141. package/src/core/vocabulary.js +4 -0
  142. package/src/grammar/aggregate.js +4 -18
  143. package/src/grammar/arcs.js +1 -386
  144. package/src/grammar/areaPaint.js +10 -0
  145. package/src/grammar/areaSeries.js +18 -10
  146. package/src/grammar/bars/geometry.js +30 -3
  147. package/src/grammar/bars/policy.js +12 -1
  148. package/src/grammar/bin.js +2 -12
  149. package/src/grammar/bin2d.js +1 -6
  150. package/src/grammar/boxPlot.js +56 -9
  151. package/src/grammar/categoryOrder.js +1 -6
  152. package/src/grammar/complete.js +15 -19
  153. package/src/grammar/computed.js +92 -25
  154. package/src/grammar/curveCommands.js +29 -16
  155. package/src/grammar/datasetSchema.js +3 -7
  156. package/src/grammar/displayLabels.js +28 -0
  157. package/src/grammar/ecdf.js +2 -9
  158. package/src/grammar/facets/dependencies.js +6 -2
  159. package/src/grammar/facets/guides.js +5 -0
  160. package/src/grammar/facets/index.js +2 -5
  161. package/src/grammar/fold.js +2 -12
  162. package/src/grammar/gradientProfile.js +1 -6
  163. package/src/grammar/impute.js +21 -48
  164. package/src/grammar/interval.js +2 -9
  165. package/src/grammar/legendGradient.js +18 -0
  166. package/src/grammar/legendGroups.js +1 -0
  167. package/src/grammar/legendTitleStyle.js +22 -0
  168. package/src/grammar/lineSeries.js +32 -17
  169. package/src/grammar/markLabels.js +5 -2
  170. package/src/grammar/normalize.js +4 -54
  171. package/src/grammar/numeric.js +1 -290
  172. package/src/grammar/parallelCoordinates.js +1 -6
  173. package/src/grammar/parseTime.js +41 -0
  174. package/src/grammar/pathCommands.js +11 -4
  175. package/src/grammar/polar.js +1 -184
  176. package/src/grammar/positionCompatibility.js +3 -3
  177. package/src/grammar/regression/derive.js +7 -1
  178. package/src/grammar/regression/models.js +83 -55
  179. package/src/grammar/regression/parameters.js +14 -3
  180. package/src/grammar/roundedRect.js +42 -54
  181. package/src/grammar/scales/appearance.js +1 -206
  182. package/src/grammar/scales/color.js +1 -382
  183. package/src/grammar/scales/colorConsumers.js +5 -2
  184. package/src/grammar/scales/definition.js +1 -197
  185. package/src/grammar/scales/discretized.js +1 -195
  186. package/src/grammar/scales/size.js +1 -456
  187. package/src/grammar/scales/temporal.js +2 -13
  188. package/src/grammar/scales/transformed.js +1 -383
  189. package/src/grammar/scales/types.js +1 -1
  190. package/src/grammar/scales/validation.js +1 -66
  191. package/src/grammar/schemas/concreteGraphic.js +11 -0
  192. package/src/grammar/schemas/graphic.js +3 -0
  193. package/src/grammar/schemas/semanticPath.js +2 -1
  194. package/src/grammar/stack.js +2 -10
  195. package/src/grammar/statisticalReference.js +1 -6
  196. package/src/grammar/summary.js +3 -26
  197. package/src/grammar/text.js +19 -0
  198. package/src/grammar/timeUnit.js +16 -7
  199. package/src/grammar/transformKeys.js +47 -0
  200. package/src/grammar/transforms.js +4 -1
  201. package/src/grammar/valueFormat.js +17 -5
  202. package/src/grammar/weightedStatistics.js +1 -6
  203. package/src/grammar/window.js +4 -58
  204. package/src/inspectionDescriptors.js +1 -1
  205. package/src/layout/canvas.js +1 -110
  206. package/src/layout/facets.js +1 -244
  207. package/src/layout/labels.js +1 -0
  208. package/src/layout/legend.js +13 -0
  209. package/src/layout/legendGroup.js +1 -0
  210. package/src/layout/legendItems.js +59 -15
  211. package/src/layout/legendLane.js +1 -441
  212. package/src/layout/legendText.js +1 -0
  213. package/src/layout/title.js +35 -13
  214. package/src/materialization/bars/aggregate.js +11 -5
  215. package/src/materialization/bars/histogram.js +4 -1
  216. package/src/materialization/bars/ranged.js +19 -6
  217. package/src/materialization/bars/resolve.js +22 -1
  218. package/src/materialization/coordinateBounds.js +8 -0
  219. package/src/materialization/dependencies.js +4 -44
  220. package/src/materialization/facetGuides/layout.js +1 -0
  221. package/src/materialization/facetGuides/placement.js +19 -8
  222. package/src/materialization/facetGuides/preparation.js +15 -7
  223. package/src/materialization/facetHeaders.js +1 -347
  224. package/src/materialization/facets.js +1 -185
  225. package/src/materialization/graphicHierarchy.js +1 -1
  226. package/src/materialization/guides/layout.js +1 -57
  227. package/src/materialization/guides/resources.js +1 -123
  228. package/src/materialization/layout.js +53 -0
  229. package/src/materialization/legendGroups.js +1 -0
  230. package/src/materialization/legendText.js +1 -0
  231. package/src/materialization/legends.js +1 -1
  232. package/src/materialization/marks/capabilities.js +5 -6
  233. package/src/materialization/rect.js +1 -1
  234. package/src/materialization/scaleGuideDependencies.js +1 -1
  235. package/src/materialization/scales/map.js +3 -0
  236. package/src/materialization/scales/policies/arc.js +8 -7
  237. package/src/materialization/scales/policies/bar.js +19 -7
  238. package/src/materialization/scales/policies/series.js +2 -1
  239. package/src/materialization/scales/resolve.js +5 -3
  240. package/src/materialization/selection/items/arc.js +1 -1
  241. package/src/materialization/selection/items/bar.js +7 -6
  242. package/src/materialization/selection/items/path.js +7 -6
  243. package/src/materialization/selection/items/rect.js +2 -2
  244. package/src/materialization/text.js +13 -4
  245. package/src/materialization/textLines.js +15 -0
  246. package/src/persistence/codec.js +37 -6
  247. package/src/persistence/validation.js +47 -0
  248. package/src/persistence.js +18 -5
  249. package/src/renderers/canvas/circle.js +6 -3
  250. package/src/renderers/canvas/fill.js +7 -1
  251. package/src/renderers/canvas/index.js +2 -1
  252. package/src/renderers/canvas/line.js +1 -0
  253. package/src/renderers/canvas/path.js +1 -1
  254. package/src/renderers/canvas/rect.js +11 -8
  255. package/src/renderers/canvas/stroke.js +2 -1
  256. package/src/renderers/canvas/text.js +5 -3
  257. package/src/renderers/svg.js +4 -1
  258. package/src/selectors/layers.js +19 -0
  259. package/src/theme/defaults.js +8 -2
  260. package/src/version.js +1 -1
  261. package/types/basic.d.ts +1 -0
  262. package/types/index.d.ts +7 -0
  263. package/types/program.d.ts +347 -85
@@ -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"]),
@@ -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,
@@ -102,7 +104,11 @@ export function deriveRegression(values, {
102
104
  const size = parameters.degree + 1;
103
105
  work += groupRows.length * size ** 2 + size ** 3;
104
106
  } else if (parameters.method === "loess") {
105
- work += groupRows.length * xValues.length *
107
+ const observedSet = new Set(observedX);
108
+ const newPredictions = predict === undefined ? 0 :
109
+ xValues.filter(value => !observedSet.has(value)).length;
110
+ work += groupRows.length *
111
+ (observedX.length * (1 + (parameters.robustIterations ?? 0)) + newPredictions) *
106
112
  Math.ceil(Math.log2(groupRows.length + 1));
107
113
  }
108
114
  validateWorkLimit(work, "Regression computation");
@@ -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,64 +439,42 @@ 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"
450
- );
475
+ return loessPrediction(model.rows, {
476
+ ...model, xValue, scaled: true
477
+ }, "LOESS regression prediction").prediction;
451
478
  }
452
479
 
453
480
  export function fitRegressionGroup(rows, { x, y, group, parameters }) {
@@ -472,7 +499,8 @@ export function fitRegressionGroup(rows, { x, y, group, parameters }) {
472
499
  interval: parameters.interval
473
500
  });
474
501
  }
475
- return fitLoessGroup(rows, { x, y, group, span: parameters.span });
502
+ return fitLoessGroup(rows, { x, y, group, span: parameters.span,
503
+ robustIterations: parameters.robustIterations });
476
504
  }
477
505
 
478
506
  export function predictRegressionAt(model, xValue, parameters) {
@@ -45,6 +45,7 @@ export function normalizeRegressionParameters({
45
45
  method = "linear",
46
46
  degree,
47
47
  span,
48
+ robustIterations,
48
49
  confidenceMethod,
49
50
  level,
50
51
  confidence,
@@ -69,7 +70,15 @@ export function normalizeRegressionParameters({
69
70
  "Regression LOESS span must be greater than zero and at most one."
70
71
  );
71
72
  }
72
- return cloneAndFreeze({ method, span: resolvedSpan });
73
+ const iterations = robustIterations === undefined ? 0 : robustIterations;
74
+ if (!Number.isInteger(iterations) || iterations < 0 || iterations > 32) {
75
+ throw new RangeError("LOESS robustIterations must be an integer in [0, 32].");
76
+ }
77
+ return cloneAndFreeze({ method, span: resolvedSpan,
78
+ ...(iterations === 0 ? {} : { robustIterations: iterations }) });
79
+ }
80
+ if (robustIterations !== undefined) {
81
+ throw new Error("Regression robustIterations requires the loess method.");
73
82
  }
74
83
  if (span !== undefined) {
75
84
  throw new Error("Regression span requires the loess method.");
@@ -140,7 +149,7 @@ export function validateRegressionTransform(transform) {
140
149
  const supported = [
141
150
  "type", "method", "x", "y", "groupBy", "confidenceMethod", "level",
142
151
  "confidence", "interval",
143
- "degree", "span", "predict", "missing"
152
+ "degree", "span", "robustIterations", "predict", "missing"
144
153
  ];
145
154
  const unknown = Object.keys(transform).find(key => !supported.includes(key));
146
155
  if (unknown !== undefined) {
@@ -166,6 +175,7 @@ export function validateRegressionTransform(transform) {
166
175
  method: transform.method,
167
176
  degree: transform.degree,
168
177
  span: transform.span,
178
+ robustIterations: transform.robustIterations,
169
179
  confidenceMethod: transform.confidenceMethod,
170
180
  level: transform.level,
171
181
  confidence: transform.confidence,
@@ -202,7 +212,8 @@ export function normalizeRegressionTransform(args = {}) {
202
212
  ? { degree: parameters.degree }
203
213
  : {}),
204
214
  ...(parameters.method === "loess"
205
- ? { span: parameters.span }
215
+ ? { span: parameters.span, ...(parameters.robustIterations === undefined
216
+ ? {} : { robustIterations: parameters.robustIterations }) }
206
217
  : parameters.interval === false ? { interval: false } : {
207
218
  confidenceMethod: parameters.confidenceMethod,
208
219
  level: parameters.level,
@@ -5,8 +5,12 @@ import {
5
5
  } from "./strokeStyle.js";
6
6
 
7
7
  export const ROUNDED_RECT_K = 4 * (Math.sqrt(2) - 1) / 3;
8
+ export const RECT_RADIUS_PROPERTIES = Object.freeze([
9
+ "cornerRadius", "cornerRadiusTopLeft", "cornerRadiusTopRight",
10
+ "cornerRadiusBottomRight", "cornerRadiusBottomLeft"
11
+ ]);
8
12
  export const RECT_STYLE_PROPERTIES = Object.freeze([
9
- "cornerRadius",
13
+ ...RECT_RADIUS_PROPERTIES,
10
14
  ...STROKE_STYLE_PROPERTIES
11
15
  ]);
12
16
 
@@ -17,12 +21,12 @@ function requireFinite(value, property) {
17
21
  return value;
18
22
  }
19
23
 
20
- export function validateCornerRadius(value, label = "Rect style") {
24
+ export function validateCornerRadius(value, label = "Rect style", property = "cornerRadius") {
21
25
  if (!Number.isFinite(value)) {
22
- throw new TypeError(`${label} cornerRadius must be a finite number.`);
26
+ throw new TypeError(`${label} ${property} must be a finite number.`);
23
27
  }
24
28
  if (value < 0) {
25
- throw new RangeError(`${label} cornerRadius must not be negative.`);
29
+ throw new RangeError(`${label} ${property} must not be negative.`);
26
30
  }
27
31
  return value;
28
32
  }
@@ -33,9 +37,9 @@ export function requestedRectStyleDetails(request = {}, label = "Rect style") {
33
37
  }
34
38
  return freezeOwned({
35
39
  ...requestedStrokeDetails(request, label),
36
- ...(Object.hasOwn(request, "cornerRadius")
37
- ? { cornerRadius: validateCornerRadius(request.cornerRadius, label) }
38
- : {})
40
+ ...Object.fromEntries(RECT_RADIUS_PROPERTIES
41
+ .filter(property => Object.hasOwn(request, property))
42
+ .map(property => [property, validateCornerRadius(request[property], label, property)]))
39
43
  });
40
44
  }
41
45
 
@@ -65,60 +69,48 @@ export function resolveRoundedRectRadius(width, height, requestedRadius) {
65
69
  return Math.min(requestedRadius, width / 2, height / 2);
66
70
  }
67
71
 
72
+ export function hasRectRadius(style = {}) {
73
+ return RECT_RADIUS_PROPERTIES.some(property => Object.hasOwn(style, property));
74
+ }
75
+
76
+ function resolveCornerRadii(width, height, requested) {
77
+ const style = typeof requested === "number" ? { cornerRadius: requested } : requested;
78
+ const details = requestedRectStyleDetails(style, "Rounded rect");
79
+ return RECT_RADIUS_PROPERTIES.slice(1).map(property => resolveRoundedRectRadius(
80
+ width, height, details[property] ?? details.cornerRadius ?? 0
81
+ ));
82
+ }
83
+
68
84
  export function roundedRectCommands({ x, y, width, height, radius }) {
69
85
  const geometry = normalizeRectGeometry({ x, y, width, height });
70
- const r = resolveRoundedRectRadius(
71
- geometry.width,
72
- geometry.height,
73
- radius
74
- );
75
- if (!(r > 0)) {
86
+ const [tl, tr, br, bl] = resolveCornerRadii(geometry.width, geometry.height, radius);
87
+ if (![tl, tr, br, bl].some(value => value > 0)) {
76
88
  throw new RangeError("Rounded rect commands require a positive resolved radius.");
77
89
  }
78
90
  const right = geometry.x + geometry.width;
79
91
  const bottom = geometry.y + geometry.height;
80
- const c = ROUNDED_RECT_K * r;
92
+ const k = ROUNDED_RECT_K;
81
93
  return freezeOwned([
82
- { op: "M", x: geometry.x + r, y: geometry.y },
83
- { op: "L", x: right - r, y: geometry.y },
94
+ { op: "M", x: geometry.x + tl, y: geometry.y },
95
+ { op: "L", x: right - tr, y: geometry.y },
84
96
  {
85
- op: "C",
86
- x1: right - r + c,
87
- y1: geometry.y,
88
- x2: right,
89
- y2: geometry.y + r - c,
90
- x: right,
91
- y: geometry.y + r
97
+ op: "C", x1: right - tr + k * tr, y1: geometry.y,
98
+ x2: right, y2: geometry.y + tr - k * tr, x: right, y: geometry.y + tr
92
99
  },
93
- { op: "L", x: right, y: bottom - r },
100
+ { op: "L", x: right, y: bottom - br },
94
101
  {
95
- op: "C",
96
- x1: right,
97
- y1: bottom - r + c,
98
- x2: right - r + c,
99
- y2: bottom,
100
- x: right - r,
101
- y: bottom
102
+ op: "C", x1: right, y1: bottom - br + k * br,
103
+ x2: right - br + k * br, y2: bottom, x: right - br, y: bottom
102
104
  },
103
- { op: "L", x: geometry.x + r, y: bottom },
105
+ { op: "L", x: geometry.x + bl, y: bottom },
104
106
  {
105
- op: "C",
106
- x1: geometry.x + r - c,
107
- y1: bottom,
108
- x2: geometry.x,
109
- y2: bottom - r + c,
110
- x: geometry.x,
111
- y: bottom - r
107
+ op: "C", x1: geometry.x + bl - k * bl, y1: bottom,
108
+ x2: geometry.x, y2: bottom - bl + k * bl, x: geometry.x, y: bottom - bl
112
109
  },
113
- { op: "L", x: geometry.x, y: geometry.y + r },
110
+ { op: "L", x: geometry.x, y: geometry.y + tl },
114
111
  {
115
- op: "C",
116
- x1: geometry.x,
117
- y1: geometry.y + r - c,
118
- x2: geometry.x + r - c,
119
- y2: geometry.y,
120
- x: geometry.x + r,
121
- y: geometry.y
112
+ op: "C", x1: geometry.x, y1: geometry.y + tl - k * tl,
113
+ x2: geometry.x + tl - k * tl, y2: geometry.y, x: geometry.x + tl, y: geometry.y
122
114
  },
123
115
  { op: "Z" }
124
116
  ].map(command => freezeOwned(command)));
@@ -129,15 +121,11 @@ export function materializeRectItem(properties, requestedRadius = 0) {
129
121
  throw new TypeError("Rounded rect item properties must be a plain object.");
130
122
  }
131
123
  const geometry = normalizeRectGeometry(properties);
132
- const radius = resolveRoundedRectRadius(
133
- geometry.width,
134
- geometry.height,
135
- requestedRadius
136
- );
124
+ const radii = resolveCornerRadii(geometry.width, geometry.height, requestedRadius);
137
125
  const appearance = Object.fromEntries(Object.entries(properties).filter(
138
126
  ([key]) => !["x", "y", "width", "height"].includes(key)
139
127
  ));
140
- return radius === 0
128
+ return radii.every(radius => radius === 0)
141
129
  ? freezeOwned({
142
130
  type: "rect",
143
131
  properties: freezeOwned({ ...geometry, ...appearance })
@@ -145,7 +133,7 @@ export function materializeRectItem(properties, requestedRadius = 0) {
145
133
  : freezeOwned({
146
134
  type: "path",
147
135
  properties: freezeOwned({
148
- commands: roundedRectCommands({ ...geometry, radius }),
136
+ commands: roundedRectCommands({ ...geometry, radius: requestedRadius }),
149
137
  ...appearance
150
138
  })
151
139
  });