ggaction 0.0.9 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/README.md +5 -4
  3. package/knowledge/action-card.schema.json +1 -0
  4. package/knowledge/action-cards.json +1 -10941
  5. package/knowledge/action-cards.schema.json +1 -0
  6. package/knowledge/extension-authoring.md +248 -0
  7. package/knowledge/intent-taxonomy.json +1 -183
  8. package/knowledge/intent-taxonomy.schema.json +1 -0
  9. package/knowledge/mcp-resources.json +1 -95
  10. package/knowledge/mcp-resources.schema.json +1 -0
  11. package/knowledge/task-packet.schema.json +1 -159
  12. package/knowledge/task-resolver.js +1 -1230
  13. package/package.json +19 -2
  14. package/src/actions/boxPlots/edit.js +45 -298
  15. package/src/actions/boxPlots/options.js +46 -77
  16. package/src/actions/boxPlots/resolve.js +1 -1
  17. package/src/actions/charts/bar.js +3 -0
  18. package/src/actions/charts/heatmap.js +73 -83
  19. package/src/actions/charts/index.js +1 -1
  20. package/src/actions/charts/parallel.js +35 -1
  21. package/src/actions/charts/shared.js +20 -0
  22. package/src/actions/composition/actions.js +46 -49
  23. package/src/actions/data/bin2d.js +105 -2
  24. package/src/actions/data/box.js +17 -17
  25. package/src/actions/data/density.js +42 -78
  26. package/src/actions/data/gradientProfile.js +22 -52
  27. package/src/actions/data/horizon.js +16 -41
  28. package/src/actions/data/index.js +10 -10
  29. package/src/actions/data/interval.js +16 -38
  30. package/src/actions/data/intervalEdit.js +55 -0
  31. package/src/actions/data/regression.js +7 -17
  32. package/src/actions/data/shared.js +47 -0
  33. package/src/actions/data/timeUnit.js +14 -38
  34. package/src/actions/data/window.js +14 -38
  35. package/src/actions/distributions/revision.js +154 -34
  36. package/src/actions/encodings/angle.js +9 -10
  37. package/src/actions/encodings/appearance.js +17 -51
  38. package/src/actions/encodings/density/resolve.js +3 -1
  39. package/src/actions/encodings/density.js +1 -15
  40. package/src/actions/encodings/horizon/resolve.js +13 -3
  41. package/src/actions/encodings/horizon.js +8 -7
  42. package/src/actions/encodings/index.js +9 -9
  43. package/src/actions/encodings/offset.js +8 -24
  44. package/src/actions/encodings/parallel.js +9 -10
  45. package/src/actions/encodings/pathOrder.js +6 -13
  46. package/src/actions/encodings/position/policies/index.js +4 -4
  47. package/src/actions/encodings/position/policies/line.js +47 -17
  48. package/src/actions/encodings/position/resolve.js +24 -5
  49. package/src/actions/encodings/ranged.js +18 -23
  50. package/src/actions/encodings/remove.js +15 -25
  51. package/src/actions/encodings/ruleAppearance.js +9 -24
  52. package/src/actions/encodings/shared.js +37 -0
  53. package/src/actions/encodings/strokeDash.js +8 -23
  54. package/src/actions/encodings/text.js +9 -10
  55. package/src/actions/errorBands/create.js +105 -39
  56. package/src/actions/errorBands/edit.js +89 -93
  57. package/src/actions/errorBands/index.js +1 -2
  58. package/src/actions/errorBars/create.js +78 -27
  59. package/src/actions/errorBars/edit.js +81 -48
  60. package/src/actions/errorBars/index.js +1 -2
  61. package/src/actions/facets/actions.js +45 -69
  62. package/src/actions/facets/derive.js +38 -53
  63. package/src/actions/facets/replay.js +0 -1
  64. package/src/actions/gradientPlots/components.js +59 -48
  65. package/src/actions/gradientPlots/edit.js +35 -216
  66. package/src/actions/gradientPlots/index.js +11 -10
  67. package/src/actions/gradientPlots/materialize.js +21 -35
  68. package/src/actions/gradientPlots/options.js +5 -7
  69. package/src/actions/gradientPlots/paint.js +9 -6
  70. package/src/actions/gradientPlots/resolve.js +1 -1
  71. package/src/actions/guides/axes/index.js +3 -3
  72. package/src/actions/guides/axes/labels.js +68 -26
  73. package/src/actions/guides/axes/lines.js +22 -2
  74. package/src/actions/guides/axes/policy.js +28 -8
  75. package/src/actions/guides/axes/ticks.js +27 -9
  76. package/src/actions/guides/axes/titles.js +113 -65
  77. package/src/actions/guides/grids/resolve.js +14 -0
  78. package/src/actions/guides/guides.js +2 -4
  79. package/src/actions/guides/index.js +2 -2
  80. package/src/actions/guides/legends/categorical/actions.js +44 -68
  81. package/src/actions/guides/legends/categorical/components.js +6 -1
  82. package/src/actions/guides/legends/categorical/index.js +2 -2
  83. package/src/actions/guides/legends/categorical/layout.js +148 -175
  84. package/src/actions/guides/legends/categorical/recipes.js +5 -0
  85. package/src/actions/guides/legends/categorical/resolve.js +8 -1
  86. package/src/actions/guides/legends/continuous/common.js +117 -66
  87. package/src/actions/guides/legends/continuous/gradient.js +38 -37
  88. package/src/actions/guides/legends/continuous/interval.js +97 -76
  89. package/src/actions/guides/legends/continuous/opacity.js +28 -38
  90. package/src/actions/guides/legends/edit.js +14 -1
  91. package/src/actions/guides/legends/index.js +1 -1
  92. package/src/actions/guides/legends/lane.js +2 -1
  93. package/src/actions/guides/legends/remove.js +8 -0
  94. package/src/actions/guides/legends/size.js +36 -43
  95. package/src/actions/guides/legends/strokeWidth.js +20 -26
  96. package/src/actions/guides/polar/axes/labels.js +17 -1
  97. package/src/actions/guides/polar/axes/shared.js +7 -1
  98. package/src/actions/guides/polar/axes/titles.js +19 -2
  99. package/src/actions/guides/polar/grids.js +4 -0
  100. package/src/actions/guides/polar/resolve.js +6 -0
  101. package/src/actions/index.js +13 -13
  102. package/src/actions/intervals/resolve.js +87 -117
  103. package/src/actions/marks/arc/actions.js +31 -36
  104. package/src/actions/marks/area/actions.js +23 -31
  105. package/src/actions/marks/index.js +6 -6
  106. package/src/actions/marks/line/actions.js +33 -52
  107. package/src/actions/marks/line/materialize.js +7 -5
  108. package/src/actions/marks/point/materialize.js +5 -3
  109. package/src/actions/marks/remove.js +21 -0
  110. package/src/actions/marks/rule/actions.js +53 -53
  111. package/src/actions/marks/shared.js +8 -4
  112. package/src/actions/marks/text/actions.js +14 -6
  113. package/src/actions/marks/text/layout.js +21 -45
  114. package/src/actions/marks/tick/actions.js +22 -21
  115. package/src/actions/primitives/createGraphics.js +4 -0
  116. package/src/actions/primitives/editGraphics.js +4 -0
  117. package/src/actions/primitives/index.js +3 -3
  118. package/src/actions/primitives/semantic.js +1 -1
  119. package/src/actions/primitives/semanticValidation/index.js +2 -2
  120. package/src/actions/primitives/semanticValidation/layer.js +1 -1
  121. package/src/actions/regression/edit.js +3 -43
  122. package/src/actions/scales/consumers/families.js +24 -4
  123. package/src/actions/scales/consumers/index.js +1 -1
  124. package/src/actions/scales/consumers/seriesLayout.js +17 -0
  125. package/src/actions/scales/definitions.js +100 -103
  126. package/src/actions/scales/materialize.js +5 -0
  127. package/src/actions/selection/actions.js +79 -121
  128. package/src/actions/titles/actions.js +48 -94
  129. package/src/actions/titles/resolve.js +14 -27
  130. package/src/actions/violinPlots/create.js +37 -44
  131. package/src/core/action.js +20 -5
  132. package/src/core/extensionRegistry.js +104 -0
  133. package/src/core/textMetrics.js +74 -37
  134. package/src/core/validation.js +18 -0
  135. package/src/extension.js +8 -1
  136. package/src/grammar/aggregate.js +48 -28
  137. package/src/grammar/arcs.js +58 -11
  138. package/src/grammar/areaSeries.js +14 -16
  139. package/src/grammar/bin2d.js +37 -23
  140. package/src/grammar/boxPlot.js +14 -13
  141. package/src/grammar/categoricalDensity.js +14 -14
  142. package/src/grammar/categoryOrder.js +8 -14
  143. package/src/grammar/curveCommands.js +87 -20
  144. package/src/grammar/density.js +162 -141
  145. package/src/grammar/facets/index.js +14 -0
  146. package/src/grammar/facets/scales.js +7 -4
  147. package/src/grammar/gradientProfile.js +12 -15
  148. package/src/grammar/histogram.js +145 -83
  149. package/src/grammar/horizon.js +60 -23
  150. package/src/grammar/interval.js +14 -13
  151. package/src/grammar/lineSeries.js +91 -61
  152. package/src/grammar/numeric.js +290 -0
  153. package/src/grammar/paint.js +3 -4
  154. package/src/grammar/palettes.js +21 -32
  155. package/src/grammar/pathCommands.js +6 -0
  156. package/src/grammar/polarPaths.js +5 -5
  157. package/src/grammar/regression/derive.js +48 -9
  158. package/src/grammar/regression/models.js +248 -69
  159. package/src/grammar/regression/parameters.js +8 -0
  160. package/src/grammar/scales/color.js +35 -26
  161. package/src/grammar/scales/continuous.js +71 -24
  162. package/src/grammar/scales/definition.js +1 -1
  163. package/src/grammar/scales/discretized.js +42 -12
  164. package/src/grammar/scales/fields.js +21 -4
  165. package/src/grammar/scales/ordinal.js +117 -61
  166. package/src/grammar/scales/temporal.js +40 -11
  167. package/src/grammar/scales/transformed.js +199 -52
  168. package/src/grammar/schemas/graphicBounds.js +270 -79
  169. package/src/grammar/seriesLayout.js +50 -18
  170. package/src/grammar/ticks.js +123 -53
  171. package/src/grammar/timeUnit.js +18 -8
  172. package/src/grammar/transforms.js +4 -1
  173. package/src/grammar/window.js +70 -57
  174. package/src/index.js +1 -1
  175. package/src/layout/canvas.js +4 -8
  176. package/src/layout/composition.js +39 -40
  177. package/src/layout/facets.js +10 -14
  178. package/src/layout/labels.js +91 -15
  179. package/src/layout/legend.js +22 -20
  180. package/src/layout/legendLane.js +54 -40
  181. package/src/layout/title.js +7 -7
  182. package/src/materialization/bars/aggregate.js +14 -40
  183. package/src/materialization/bars/grouped.js +7 -18
  184. package/src/materialization/bars/histogram.js +2 -19
  185. package/src/materialization/bars/ranged.js +20 -15
  186. package/src/materialization/bars/resolve.js +21 -0
  187. package/src/materialization/facetGuides/legacyCategorical.js +2 -1
  188. package/src/materialization/facetGuides/placement.js +34 -19
  189. package/src/materialization/facetGuides/preparation.js +9 -5
  190. package/src/materialization/facets.js +39 -27
  191. package/src/materialization/guides/resources.js +2 -1
  192. package/src/materialization/marks/capabilities.js +5 -2
  193. package/src/materialization/marks/index.js +24 -4
  194. package/src/materialization/scales/policies/bar.js +61 -59
  195. package/src/materialization/scales/policies/binnedPosition.js +34 -0
  196. package/src/materialization/scales/policies/series.js +13 -7
  197. package/src/materialization/scales/resolve.js +4 -5
  198. package/src/materialization/selection/items/arc.js +16 -2
  199. package/src/materialization/selection/items/bar.js +4 -2
  200. package/src/materialization/selection/items/common.js +21 -1
  201. package/src/materialization/selection/items/path.js +19 -2
  202. package/src/materialization/selection/items/point.js +4 -16
  203. package/src/materialization/selection/items/tick.js +13 -21
  204. package/src/materialization/selection/point.js +11 -4
  205. package/src/materialization/selection/styles.js +19 -27
  206. package/src/materialization/text.js +4 -1
  207. package/src/mcp/adapter.js +8 -3
  208. package/src/renderers/canvas/fill.js +2 -2
  209. package/src/renderers/canvas/index.js +35 -2
  210. package/src/renderers/canvas/native.js +160 -0
  211. package/src/renderers/pdf.js +14 -0
  212. package/src/renderers/svg.js +37 -4
  213. package/types/extension.d.ts +19 -0
  214. package/types/index.d.ts +3 -0
  215. package/types/program.d.ts +509 -131
  216. package/types/svg.d.ts +1 -0
  217. package/src/actions/errorBands/components.js +0 -81
  218. package/src/actions/errorBands/options.js +0 -39
  219. package/src/actions/errorBands/resolve.js +0 -28
  220. package/src/actions/errorBars/components.js +0 -62
  221. package/src/actions/errorBars/options.js +0 -58
  222. package/src/actions/errorBars/resolve.js +0 -25
@@ -1,34 +1,99 @@
1
- import { resolveTextBounds } from "../../core/textMetrics.js";
2
- import { findGraphic, requireGraphic } from "./graphicTree.js";
1
+ import {
2
+ requireFiniteBounds,
3
+ resolveTextBounds
4
+ } from "../../core/textMetrics.js";
5
+ import { interpolateNumber } from "../numeric.js";
6
+ import {
7
+ findGraphic,
8
+ findGraphicParent,
9
+ requireGraphic
10
+ } from "./graphicTree.js";
3
11
 
4
- function unionBounds(bounds) {
5
- if (bounds.length === 0) return undefined;
12
+ const EPSILON = Number.EPSILON * 64;
13
+
14
+ function box(left, right, top, bottom, extent = 0) {
6
15
  return {
7
- left: Math.min(...bounds.map(item => item.left)),
8
- right: Math.max(...bounds.map(item => item.right)),
9
- top: Math.min(...bounds.map(item => item.top)),
10
- bottom: Math.max(...bounds.map(item => item.bottom))
16
+ left: left - extent,
17
+ right: right + extent,
18
+ top: top - extent,
19
+ bottom: bottom + extent
11
20
  };
12
21
  }
13
22
 
23
+ function includeExtent(result, left, right, top, bottom) {
24
+ if (result === undefined) return { left, right, top, bottom };
25
+ result.left = Math.min(result.left, left);
26
+ result.right = Math.max(result.right, right);
27
+ result.top = Math.min(result.top, top);
28
+ result.bottom = Math.max(result.bottom, bottom);
29
+ return result;
30
+ }
31
+
32
+ function unionBounds(bounds) {
33
+ let result;
34
+ for (const item of bounds) {
35
+ if (item !== undefined) {
36
+ result = includeExtent(
37
+ result, item.left, item.right, item.top, item.bottom
38
+ );
39
+ }
40
+ }
41
+ return result;
42
+ }
43
+
14
44
  function cubicValue(start, control1, control2, end, t) {
15
45
  const remaining = 1 - t;
16
- return remaining ** 3 * start +
17
- 3 * remaining ** 2 * t * control1 +
18
- 3 * remaining * t ** 2 * control2 +
19
- t ** 3 * end;
46
+ if (Math.max(
47
+ Math.abs(start),
48
+ Math.abs(control1),
49
+ Math.abs(control2),
50
+ Math.abs(end)
51
+ ) <= Number.MAX_VALUE / 4) {
52
+ return remaining ** 3 * start +
53
+ 3 * remaining ** 2 * t * control1 +
54
+ 3 * remaining * t ** 2 * control2 +
55
+ t ** 3 * end;
56
+ }
57
+ const left = interpolateNumber(start, control1, t);
58
+ const middle = interpolateNumber(control1, control2, t);
59
+ const right = interpolateNumber(control2, end, t);
60
+ return interpolateNumber(
61
+ interpolateNumber(left, middle, t),
62
+ interpolateNumber(middle, right, t),
63
+ t
64
+ );
20
65
  }
21
66
 
22
67
  function cubicExtrema(start, control1, control2, end) {
23
- const quadratic = 3 * (-start + 3 * control1 - 3 * control2 + end);
24
- const linear = 2 * (3 * start - 6 * control1 + 3 * control2);
25
- const constant = 3 * (control1 - start);
26
- const epsilon = Number.EPSILON * 64 * Math.max(
27
- 1,
68
+ let scale = Math.max(
69
+ Math.abs(start),
70
+ Math.abs(control1),
71
+ Math.abs(control2),
72
+ Math.abs(end)
73
+ );
74
+ const normalizeCoordinates = scale > Number.MAX_VALUE / 24 ||
75
+ (scale > 0 && scale < EPSILON);
76
+ if (normalizeCoordinates) {
77
+ start /= scale;
78
+ control1 /= scale;
79
+ control2 /= scale;
80
+ end /= scale;
81
+ }
82
+ let quadratic = 3 * (-start + 3 * control1 - 3 * control2 + end);
83
+ let linear = 2 * (3 * start - 6 * control1 + 3 * control2);
84
+ let constant = 3 * (control1 - start);
85
+ scale = Math.max(
28
86
  Math.abs(quadratic),
29
87
  Math.abs(linear),
30
88
  Math.abs(constant)
31
89
  );
90
+ let epsilon = EPSILON * Math.max(normalizeCoordinates ? 0 : 1, scale);
91
+ if (scale > Math.sqrt(Number.MAX_VALUE) / 2) {
92
+ quadratic /= scale;
93
+ linear /= scale;
94
+ constant /= scale;
95
+ epsilon = EPSILON;
96
+ }
32
97
  if (Math.abs(quadratic) <= epsilon) {
33
98
  if (Math.abs(linear) <= epsilon) return [];
34
99
  const root = -constant / linear;
@@ -43,14 +108,16 @@ function cubicExtrema(start, control1, control2, end) {
43
108
  ].filter(value => value > 0 && value < 1);
44
109
  }
45
110
 
46
- function pathPoints(commands) {
47
- const points = [];
111
+ export function resolvePathCommandBounds(commands) {
112
+ let bounds;
48
113
  let current;
49
114
  for (const command of commands) {
50
115
  if (command.op === "M" || command.op === "L") {
51
116
  if ([command.x, command.y].every(Number.isFinite)) {
52
117
  current = { x: command.x, y: command.y };
53
- points.push(current);
118
+ bounds = includeExtent(
119
+ bounds, current.x, current.x, current.y, current.y
120
+ );
54
121
  }
55
122
  continue;
56
123
  }
@@ -64,25 +131,106 @@ function pathPoints(commands) {
64
131
  ...cubicExtrema(current.y, command.y1, command.y2, command.y)
65
132
  ]);
66
133
  for (const t of roots) {
67
- points.push({
68
- x: cubicValue(current.x, command.x1, command.x2, command.x, t),
69
- y: cubicValue(current.y, command.y1, command.y2, command.y, t)
70
- });
134
+ const x = cubicValue(current.x, command.x1, command.x2, command.x, t);
135
+ const y = cubicValue(current.y, command.y1, command.y2, command.y, t);
136
+ bounds = includeExtent(
137
+ bounds,
138
+ x,
139
+ x,
140
+ y,
141
+ y
142
+ );
71
143
  }
72
144
  current = { x: command.x, y: command.y };
73
145
  }
74
- return points;
146
+ return bounds;
75
147
  }
76
148
 
77
- export function resolvePathCommandBounds(commands) {
78
- const points = pathPoints(commands);
79
- if (points.length === 0) return undefined;
80
- return {
81
- left: Math.min(...points.map(point => point.x)),
82
- right: Math.max(...points.map(point => point.x)),
83
- top: Math.min(...points.map(point => point.y)),
84
- bottom: Math.max(...points.map(point => point.y))
85
- };
149
+ function endpointDirection(start, command, end, reverse = false) {
150
+ const endpoint = reverse ? end : start;
151
+ const candidates = command.op === "C"
152
+ ? reverse
153
+ ? [{ x: command.x2, y: command.y2 },
154
+ { x: command.x1, y: command.y1 }, start]
155
+ : [{ x: command.x1, y: command.y1 },
156
+ { x: command.x2, y: command.y2 }, end]
157
+ : [reverse ? start : end];
158
+ const sign = reverse ? -1 : 1;
159
+ for (const point of candidates) {
160
+ let x = (point.x - endpoint.x) * sign;
161
+ let y = (point.y - endpoint.y) * sign;
162
+ let length = Math.hypot(x, y);
163
+ if (length === Infinity) {
164
+ const scale = Math.max(
165
+ Math.abs(point.x), Math.abs(point.y),
166
+ Math.abs(endpoint.x), Math.abs(endpoint.y)
167
+ );
168
+ x = (point.x / scale - endpoint.x / scale) * sign;
169
+ y = (point.y / scale - endpoint.y / scale) * sign;
170
+ length = Math.hypot(x, y);
171
+ }
172
+ if (length !== 0) return { x: x / length, y: y / length };
173
+ }
174
+ return undefined;
175
+ }
176
+
177
+ function includeMiter(bounds, point, incoming, outgoing, strokeExtent) {
178
+ if (incoming === undefined || outgoing === undefined) return;
179
+ const cross = incoming.x * outgoing.y - incoming.y * outgoing.x;
180
+ if (Math.abs(cross) <= EPSILON) return;
181
+ const normalX = -incoming.y - outgoing.y;
182
+ const normalY = incoming.x + outgoing.x;
183
+ const normalLength = Math.hypot(normalX, normalY);
184
+ const ratio = 2 / normalLength;
185
+ if (ratio > 10 * (1 + EPSILON)) return;
186
+ const distance = strokeExtent * ratio * (cross > 0 ? -1 : 1) / normalLength;
187
+ includeExtent(
188
+ bounds,
189
+ point.x + normalX * distance,
190
+ point.x + normalX * distance,
191
+ point.y + normalY * distance,
192
+ point.y + normalY * distance
193
+ );
194
+ }
195
+
196
+ function pathStrokeBounds(commands, bounds, strokeExtent) {
197
+ const expanded = box(
198
+ bounds.left, bounds.right, bounds.top, bounds.bottom, strokeExtent
199
+ );
200
+ if (!(strokeExtent > 0)) return expanded;
201
+ const start = { x: commands[0].x, y: commands[0].y };
202
+ let current = start;
203
+ let firstDirection;
204
+ let previousDirection;
205
+ let segmentCount = 0;
206
+ for (const command of commands.slice(1)) {
207
+ const end = command.op === "Z"
208
+ ? start
209
+ : { x: command.x, y: command.y };
210
+ const outgoing = endpointDirection(current, command, end);
211
+ const incoming = endpointDirection(current, command, end, true);
212
+ if (outgoing !== undefined || incoming !== undefined) {
213
+ if (segmentCount > 0) {
214
+ includeMiter(expanded, current, previousDirection, outgoing, strokeExtent);
215
+ } else {
216
+ firstDirection = outgoing;
217
+ }
218
+ previousDirection = incoming;
219
+ segmentCount += 1;
220
+ }
221
+ current = end;
222
+ if (command.op === "Z") break;
223
+ }
224
+ if (commands.at(-1)?.op === "Z" && segmentCount > 1) {
225
+ includeMiter(
226
+ expanded,
227
+ start,
228
+ previousDirection,
229
+ firstDirection,
230
+ strokeExtent
231
+ );
232
+ }
233
+ return expanded;
86
234
  }
87
235
 
88
236
  function primitiveBounds(type, properties = {}) {
@@ -92,32 +240,34 @@ function primitiveBounds(type, properties = {}) {
92
240
  return undefined;
93
241
  }
94
242
  const radius = properties.radius + strokeExtent;
95
- return {
96
- left: properties.x - radius,
97
- right: properties.x + radius,
98
- top: properties.y - radius,
99
- bottom: properties.y + radius
100
- };
243
+ return box(
244
+ properties.x - radius,
245
+ properties.x + radius,
246
+ properties.y - radius,
247
+ properties.y + radius
248
+ );
101
249
  }
102
250
  if (type === "rect") {
103
251
  if (![properties.x, properties.y, properties.width, properties.height]
104
252
  .every(Number.isFinite)) return undefined;
105
- return {
106
- left: properties.x - strokeExtent,
107
- right: properties.x + properties.width + strokeExtent,
108
- top: properties.y - strokeExtent,
109
- bottom: properties.y + properties.height + strokeExtent
110
- };
253
+ return box(
254
+ properties.x,
255
+ properties.x + properties.width,
256
+ properties.y,
257
+ properties.y + properties.height,
258
+ strokeExtent
259
+ );
111
260
  }
112
261
  if (type === "line") {
113
262
  if (![properties.x1, properties.y1, properties.x2, properties.y2]
114
263
  .every(Number.isFinite)) return undefined;
115
- return {
116
- left: Math.min(properties.x1, properties.x2) - strokeExtent,
117
- right: Math.max(properties.x1, properties.x2) + strokeExtent,
118
- top: Math.min(properties.y1, properties.y2) - strokeExtent,
119
- bottom: Math.max(properties.y1, properties.y2) + strokeExtent
120
- };
264
+ return box(
265
+ Math.min(properties.x1, properties.x2),
266
+ Math.max(properties.x1, properties.x2),
267
+ Math.min(properties.y1, properties.y2),
268
+ Math.max(properties.y1, properties.y2),
269
+ strokeExtent
270
+ );
121
271
  }
122
272
  if (type === "text") {
123
273
  if (
@@ -133,51 +283,92 @@ function primitiveBounds(type, properties = {}) {
133
283
  if (!Array.isArray(commands)) return undefined;
134
284
  const bounds = resolvePathCommandBounds(commands);
135
285
  if (bounds === undefined) return undefined;
136
- return {
137
- left: bounds.left - strokeExtent,
138
- right: bounds.right + strokeExtent,
139
- top: bounds.top - strokeExtent,
140
- bottom: bounds.bottom + strokeExtent
141
- };
286
+ return pathStrokeBounds(commands, bounds, strokeExtent);
142
287
  }
143
288
  return undefined;
144
289
  }
145
290
 
146
- function objectBounds(graphicSpec, id, object, ancestors) {
147
- if (ancestors.includes(id)) {
291
+ function translateBounds(bounds, x, y) {
292
+ if (bounds === undefined) return undefined;
293
+ return box(
294
+ bounds.left + x,
295
+ bounds.right + x,
296
+ bounds.top + y,
297
+ bounds.bottom + y
298
+ );
299
+ }
300
+
301
+ function canvasOffset(object, property) {
302
+ const value = object.type === "canvas" ? object.properties?.[property] : 0;
303
+ return Number.isFinite(value) ? value : 0;
304
+ }
305
+
306
+ function ancestorOffset(graphicSpec, target) {
307
+ const offset = [0, 0];
308
+ const visited = new Set();
309
+ let current = target;
310
+ while (true) {
311
+ if (visited.has(current)) {
312
+ throw new Error(`Graphic attachment cycle includes "${current}".`);
313
+ }
314
+ visited.add(current);
315
+ const parent = findGraphicParent(graphicSpec, current);
316
+ if (parent === undefined) return offset;
317
+ offset[0] += canvasOffset(parent.object, "x");
318
+ offset[1] += canvasOffset(parent.object, "y");
319
+ current = parent.id;
320
+ }
321
+ }
322
+
323
+ function objectBounds(graphicSpec, id, object, ancestors, x, y) {
324
+ if (ancestors.has(id)) {
148
325
  throw new Error(`Graphic attachment cycle includes "${id}".`);
149
326
  }
150
- const own = object.items === undefined
151
- ? primitiveBounds(object.type, object.properties)
152
- : unionBounds(object.items.flatMap(item => {
153
- const bounds = primitiveBounds(item.type ?? object.type, item.properties);
154
- return bounds === undefined ? [] : [bounds];
155
- }));
156
- const descendants = (object.children ?? []).flatMap(childId => {
327
+ ancestors.add(id);
328
+ let result = object.items === undefined
329
+ ? translateBounds(primitiveBounds(object.type, object.properties), x, y)
330
+ : unionBounds(object.items.map(item => translateBounds(
331
+ primitiveBounds(item.type ?? object.type, item.properties),
332
+ x,
333
+ y
334
+ )));
335
+ const childX = x + canvasOffset(object, "x");
336
+ const childY = y + canvasOffset(object, "y");
337
+ for (const childId of object.children ?? []) {
157
338
  const child = findGraphic(graphicSpec, childId);
158
339
  if (child?.kind !== "object") {
159
340
  throw new Error(`Unknown attached graphic "${childId}".`);
160
341
  }
161
- const bounds = objectBounds(graphicSpec, childId, child.object, [...ancestors, id]);
162
- return bounds === undefined ? [] : [bounds];
163
- });
164
- return unionBounds([...(own === undefined ? [] : [own]), ...descendants]);
342
+ const bounds = objectBounds(
343
+ graphicSpec, childId, child.object, ancestors, childX, childY
344
+ );
345
+ if (bounds !== undefined) {
346
+ result = includeExtent(
347
+ result, bounds.left, bounds.right, bounds.top, bounds.bottom
348
+ );
349
+ }
350
+ }
351
+ ancestors.delete(id);
352
+ return result;
165
353
  }
166
354
 
167
355
  export function resolveConcreteGraphicBounds(graphicSpec, target) {
168
356
  const found = requireGraphic(graphicSpec, target);
169
- if (found.kind === "item") {
170
- return primitiveBounds(found.object.type ?? found.owner.type, found.object.properties);
171
- }
172
- return objectBounds(graphicSpec, found.id, found.object, []);
357
+ const [x, y] = ancestorOffset(graphicSpec, target);
358
+ return requireFiniteBounds(found.kind === "item"
359
+ ? translateBounds(
360
+ primitiveBounds(found.object.type ?? found.owner.type, found.object.properties),
361
+ x,
362
+ y
363
+ )
364
+ : objectBounds(graphicSpec, found.id, found.object, new Set(), x, y));
173
365
  }
174
366
 
175
367
  export function unionConcreteGraphicBounds(graphicSpec, targets) {
176
368
  if (!Array.isArray(targets)) {
177
369
  throw new TypeError("Graphic bounds targets must be an array.");
178
370
  }
179
- return unionBounds(targets.flatMap(target => {
180
- const bounds = resolveConcreteGraphicBounds(graphicSpec, target);
181
- return bounds === undefined ? [] : [bounds];
182
- }));
371
+ return unionBounds(targets.map(target =>
372
+ resolveConcreteGraphicBounds(graphicSpec, target)
373
+ ));
183
374
  }
@@ -1,5 +1,10 @@
1
1
  import { cloneAndFreeze } from "../core/immutable.js";
2
2
  import { COLOR_LAYOUTS } from "../core/vocabulary.js";
3
+ import {
4
+ normalizedFiniteSum,
5
+ restoreFiniteScale,
6
+ requireFiniteResult
7
+ } from "./numeric.js";
3
8
 
4
9
  export const DEFAULT_SERIES_BASELINE = 0;
5
10
 
@@ -16,6 +21,15 @@ function validateValues(values) {
16
21
  }
17
22
  }
18
23
 
24
+ function requireResolvedSegment(start, end, value, layout) {
25
+ requireFiniteResult(end, `Series ${layout} end`);
26
+ if (value > 0 ? end <= start : end >= start) {
27
+ throw new RangeError(
28
+ `Series ${layout} value cannot resolve a distinct finite segment.`
29
+ );
30
+ }
31
+ }
32
+
19
33
  export function layoutSeriesPartition(
20
34
  values,
21
35
  layout,
@@ -35,18 +49,43 @@ export function layoutSeriesPartition(
35
49
  ));
36
50
  }
37
51
 
38
- if (["stack", "fill", "center"].includes(layout)) {
52
+ if (layout === "stack" || layout === "fill" || layout === "center") {
39
53
  if (values.some(value => value < 0)) {
40
54
  throw new RangeError(`${layout} layout requires non-negative values.`);
41
55
  }
42
56
  const total = values.reduce((sum, value) => sum + value, 0);
43
57
  if (layout === "fill" && total === 0) return cloneAndFreeze([]);
44
- let offset = layout === "center" ? -total / 2 : 0;
58
+ const normalized = Number.isFinite(total)
59
+ ? undefined
60
+ : normalizedFiniteSum(values, `Series ${layout} values`);
61
+ if (layout === "stack" && normalized) {
62
+ throw new RangeError("Series stack total is outside the finite numeric range.");
63
+ }
64
+ let offset = 0;
65
+ if (layout === "center") {
66
+ offset = -(normalized
67
+ ? restoreFiniteScale(
68
+ normalized.total / 2,
69
+ normalized.scale,
70
+ "Series center half-total"
71
+ )
72
+ : total / 2);
73
+ }
74
+ let positiveIndex = 0;
75
+ const positiveCount = values.filter(value => value > 0).length;
45
76
  return cloneAndFreeze(values.flatMap((value, index) => {
46
77
  if (value === 0) return [];
47
- const resolvedValue = layout === "fill" ? value / total : value;
78
+ positiveIndex += 1;
79
+ const resolvedValue = layout === "fill"
80
+ ? normalized
81
+ ? (value / normalized.scale) / normalized.total
82
+ : value / total
83
+ : value;
48
84
  const start = offset;
49
- const end = start + resolvedValue;
85
+ const end = layout === "fill" && positiveIndex === positiveCount
86
+ ? 1
87
+ : start + resolvedValue;
88
+ requireResolvedSegment(start, end, value, layout);
50
89
  offset = end;
51
90
  return [{ index, value, start, end }];
52
91
  }));
@@ -58,6 +97,7 @@ export function layoutSeriesPartition(
58
97
  if (value === 0) return [];
59
98
  const start = value > 0 ? positive : negative;
60
99
  const end = start + value;
100
+ requireResolvedSegment(start, end, value, layout);
61
101
  if (value > 0) positive = end;
62
102
  else negative = end;
63
103
  return [{ index, value, start, end }];
@@ -78,18 +118,10 @@ export function resolveSeriesLayoutDomainValues(partitions, layout) {
78
118
  ...partition
79
119
  ]);
80
120
  }
81
- if (layout === "center") {
82
- return partitions.flatMap(partition => {
83
- const segments = layoutSeriesPartition(partition, layout);
84
- return segments.length === 0
85
- ? [DEFAULT_SERIES_BASELINE]
86
- : segments.flatMap(segment => [segment.start, segment.end]);
87
- });
88
- }
89
- return partitions.flatMap(partition =>
90
- layoutSeriesPartition(partition, layout).flatMap(segment => [
91
- segment.start,
92
- segment.end
93
- ])
94
- );
121
+ return partitions.flatMap(partition => {
122
+ const segments = layoutSeriesPartition(partition, layout);
123
+ return layout === "center" && segments.length === 0
124
+ ? [DEFAULT_SERIES_BASELINE]
125
+ : segments.flatMap(segment => [segment.start, segment.end]);
126
+ });
95
127
  }