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,23 +1,77 @@
1
1
  import { cloneAndFreeze } from "../core/immutable.js";
2
+ import {
3
+ MAX_GENERATED_ITEMS,
4
+ validateGeneratedItemLimit
5
+ } from "../core/validation.js";
6
+ import {
7
+ alignNumericStep,
8
+ cleanNumericValue,
9
+ niceNumericStep,
10
+ normalizeNumericRange
11
+ } from "./numeric.js";
12
+ import { utcTimestamp, validTimestamp } from "./timeUnit.js";
2
13
 
3
- const LINEAR_FACTORS = Object.freeze([1, 2, 3, 5]);
14
+ function ticksForStep(start, end, step, low, high, scale) {
15
+ const length = Math.floor((end - start) / step + 1e-10) + 1;
16
+ if (!Number.isFinite(length) || length <= 0) return undefined;
17
+ const size = Math.min(length, MAX_GENERATED_ITEMS);
18
+ const values = [];
19
+ for (let index = 0; index < size; index += 1) {
20
+ const tick = length === size || size === 1
21
+ ? index
22
+ : Math.round(index * (length - 1) / (size - 1));
23
+ const raw = start + tick * step;
24
+ const value = scale === undefined
25
+ ? Number(raw.toPrecision(12))
26
+ : cleanNumericValue(raw * scale, step * scale);
27
+ if (
28
+ !Number.isFinite(value) || value < low || value > high ||
29
+ (values.length > 0 && value <= values.at(-1))
30
+ ) {
31
+ if (scale === undefined) return undefined;
32
+ } else {
33
+ values.push(value);
34
+ }
35
+ }
36
+ return values;
37
+ }
4
38
 
5
39
  export function niceTicks(domain, count) {
6
40
  if (!Number.isInteger(count) || count <= 0) throw new RangeError("Tick count must be a positive integer.");
41
+ const effectiveCount = Math.min(count, MAX_GENERATED_ITEMS);
7
42
  const low = Math.min(...domain);
8
43
  const high = Math.max(...domain);
9
44
  if (low === high) return cloneAndFreeze([low]);
10
- const rough = (high - low) / count;
11
- const power = 10 ** Math.floor(Math.log10(rough));
12
- const error = rough / power;
13
- const factor = LINEAR_FACTORS.find(candidate => candidate >= error) ?? 10;
14
- const step = factor * power;
15
- const tolerance = step * 1e-10;
16
- const start = Math.ceil((low - tolerance) / step) * step;
17
- const end = Math.floor((high + tolerance) / step) * step;
18
- const values = [];
19
- for (let value = start; value <= end + step * 1e-10; value += step) values.push(+value.toPrecision(12));
20
- return cloneAndFreeze(values);
45
+ const directSpan = high - low;
46
+ if (Number.isFinite(directSpan) && directSpan > 0) {
47
+ const directStep = niceNumericStep(directSpan, effectiveCount);
48
+ const tolerance = directStep * 1e-10;
49
+ const directStart = Math.ceil((low - tolerance) / directStep) * directStep;
50
+ const directEnd = Math.floor((high + tolerance) / directStep) * directStep;
51
+ const direct = ticksForStep(
52
+ directStart,
53
+ directEnd,
54
+ directStep,
55
+ low,
56
+ high
57
+ );
58
+ if (direct !== undefined && direct.length <= MAX_GENERATED_ITEMS) {
59
+ return cloneAndFreeze(direct);
60
+ }
61
+ }
62
+ const normalized = normalizeNumericRange(low, high);
63
+ const normalizedStep = niceNumericStep(normalized.span, effectiveCount);
64
+ const start = alignNumericStep(normalized.start, normalizedStep, "ceil");
65
+ const end = alignNumericStep(normalized.end, normalizedStep, "floor");
66
+ const values = ticksForStep(
67
+ start,
68
+ end,
69
+ normalizedStep,
70
+ low,
71
+ high,
72
+ normalized.scale
73
+ );
74
+ return cloneAndFreeze(values?.length > 0 ? values : [low, high]);
21
75
  }
22
76
 
23
77
  const SECOND = 1000;
@@ -39,19 +93,31 @@ function validateTimeDomain(domain) {
39
93
  if (
40
94
  !Array.isArray(domain) ||
41
95
  domain.length !== 2 ||
42
- !domain.every(Number.isFinite)
96
+ !domain.every(validTimestamp)
43
97
  ) {
44
- throw new TypeError("Time tick domain must contain two finite timestamps.");
98
+ throw new TypeError(
99
+ "Time tick domain must contain two finite timestamps representing valid dates."
100
+ );
45
101
  }
46
102
 
47
103
  return [Math.min(...domain), Math.max(...domain)];
48
104
  }
49
105
 
50
106
  function selectTimeInterval(span, count) {
51
- return TIME_INTERVALS.reduce((best, candidate) => {
107
+ let interval = TIME_INTERVALS[0];
108
+ let bestError = Infinity;
109
+ for (const candidate of TIME_INTERVALS) {
52
110
  const error = Math.abs(span / candidate.duration - count);
53
- return error < best.error ? { ...candidate, error } : best;
54
- }, { ...TIME_INTERVALS[0], error: Infinity });
111
+ if (error < bestError) {
112
+ interval = candidate;
113
+ bestError = error;
114
+ }
115
+ }
116
+ if (span / interval.duration + 1 > Math.max(count * 10, 100)) {
117
+ const step = niceNumericStep(span / YEAR, count);
118
+ return { unit: "year", step, duration: step * YEAR };
119
+ }
120
+ return interval;
55
121
  }
56
122
 
57
123
  function floorCalendar(timestamp, { unit, step }) {
@@ -59,13 +125,13 @@ function floorCalendar(timestamp, { unit, step }) {
59
125
 
60
126
  if (unit === "year") {
61
127
  const year = Math.floor(date.getUTCFullYear() / step) * step;
62
- return Date.UTC(year, 0, 1);
128
+ return utcTimestamp(year);
63
129
  }
64
130
 
65
131
  if (unit === "month") {
66
132
  const month = date.getUTCFullYear() * 12 + date.getUTCMonth();
67
133
  const aligned = Math.floor(month / step) * step;
68
- return Date.UTC(Math.floor(aligned / 12), aligned % 12, 1);
134
+ return utcTimestamp(Math.floor(aligned / 12), aligned % 12);
69
135
  }
70
136
 
71
137
  const duration = unit === "day"
@@ -79,43 +145,44 @@ function floorCalendar(timestamp, { unit, step }) {
79
145
  return Math.floor(timestamp / duration) * duration;
80
146
  }
81
147
 
82
- function addCalendar(timestamp, { unit, step }) {
83
- const date = new Date(timestamp);
84
-
85
- if (unit === "year") {
86
- return Date.UTC(date.getUTCFullYear() + step, 0, 1);
148
+ function addCalendar(timestamp, interval) {
149
+ if (interval.unit === "year" || interval.unit === "month") {
150
+ const date = new Date(timestamp);
151
+ return utcTimestamp(
152
+ date.getUTCFullYear() + (interval.unit === "year" ? interval.step : 0),
153
+ date.getUTCMonth() + (interval.unit === "month" ? interval.step : 0)
154
+ );
87
155
  }
88
-
89
- if (unit === "month") {
90
- return Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + step, 1);
91
- }
92
-
93
- const duration = unit === "day"
94
- ? step * DAY
95
- : unit === "hour"
96
- ? step * HOUR
97
- : unit === "minute"
98
- ? step * MINUTE
99
- : step * SECOND;
100
-
101
- return timestamp + duration;
156
+ return timestamp + interval.duration;
102
157
  }
103
158
 
104
159
  export function timeTicks(domain, count) {
105
160
  if (!Number.isInteger(count) || count <= 0) {
106
161
  throw new RangeError("Time tick count must be a positive integer.");
107
162
  }
163
+ validateGeneratedItemLimit(count, "Time tick count");
108
164
 
109
165
  const [low, high] = validateTimeDomain(domain);
110
166
  if (low === high) return cloneAndFreeze([low]);
111
167
  const interval = selectTimeInterval(high - low, count);
112
168
  let value = floorCalendar(low, interval);
169
+ if (!validTimestamp(value) && interval.unit === "year") {
170
+ const year = new Date(low).getUTCFullYear();
171
+ value = utcTimestamp(Math.ceil(year / interval.step) * interval.step);
172
+ }
113
173
  if (value < low) value = addCalendar(value, interval);
114
174
  const values = [];
115
175
 
116
176
  while (value <= high) {
177
+ if (values.length === MAX_GENERATED_ITEMS) {
178
+ throw new RangeError(
179
+ `Time tick range must generate at most ${MAX_GENERATED_ITEMS} ticks.`
180
+ );
181
+ }
117
182
  values.push(value);
118
- value = addCalendar(value, interval);
183
+ const next = addCalendar(value, interval);
184
+ if (!validTimestamp(next) || next <= value) break;
185
+ value = next;
119
186
  }
120
187
 
121
188
  return cloneAndFreeze(values.length === 0 ? [low, high] : values);
@@ -125,9 +192,18 @@ function pad(value) {
125
192
  return String(value).padStart(2, "0");
126
193
  }
127
194
 
195
+ function formatYear(value) {
196
+ if (value >= 0 && value <= 9999) return String(value).padStart(4, "0");
197
+ const sign = value < 0 ? "-" : "+";
198
+ return `${sign}${String(Math.abs(value)).padStart(6, "0")}`;
199
+ }
200
+
128
201
  function formatTimePrecision(value, precision) {
129
202
  const date = new Date(value);
130
- const year = String(date.getUTCFullYear()).padStart(4, "0");
203
+ if (!Number.isFinite(date.getTime())) {
204
+ throw new TypeError("Time tick value must represent a valid date.");
205
+ }
206
+ const year = formatYear(date.getUTCFullYear());
131
207
  const month = pad(date.getUTCMonth() + 1);
132
208
  const day = pad(date.getUTCDate());
133
209
  const hour = pad(date.getUTCHours());
@@ -157,22 +233,14 @@ function timePrecision(domain) {
157
233
  }
158
234
 
159
235
  export function formatTimeTick(value, domain) {
160
- if (!Number.isFinite(value)) {
161
- throw new TypeError("Time tick value must be a finite timestamp.");
236
+ if (!validTimestamp(value)) {
237
+ throw new TypeError(
238
+ "Time tick value must be a finite timestamp representing a valid date."
239
+ );
162
240
  }
163
241
  return formatTimePrecision(value, timePrecision(domain));
164
242
  }
165
243
 
166
- function distinguishesTicks(values, labels) {
167
- const timestamps = new Map();
168
- for (let index = 0; index < values.length; index += 1) {
169
- const previous = timestamps.get(labels[index]);
170
- if (previous !== undefined && previous !== values[index]) return false;
171
- timestamps.set(labels[index], values[index]);
172
- }
173
- return true;
174
- }
175
-
176
244
  export function formatTimeTicks(values, domain) {
177
245
  if (!Array.isArray(values) || !values.every(Number.isFinite)) {
178
246
  throw new TypeError("Time tick values must be finite timestamps.");
@@ -180,7 +248,9 @@ export function formatTimeTicks(values, domain) {
180
248
  const initial = timePrecision(domain);
181
249
  for (let precision = initial; precision <= 6; precision += 1) {
182
250
  const labels = values.map(value => formatTimePrecision(value, precision));
183
- if (distinguishesTicks(values, labels)) return cloneAndFreeze(labels);
251
+ if (new Set(labels).size === new Set(values).size) {
252
+ return cloneAndFreeze(labels);
253
+ }
184
254
  }
185
255
  return cloneAndFreeze(values.map(value => String(value)));
186
256
  }
@@ -1,6 +1,23 @@
1
1
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
2
  import { normalizeTemporalValue } from "./scales/fields.js";
3
3
 
4
+ export function utcTimestamp(
5
+ year,
6
+ month = 0,
7
+ day = 1,
8
+ hour = 0,
9
+ minute = 0
10
+ ) {
11
+ const date = new Date(0);
12
+ date.setUTCFullYear(year, month, day);
13
+ date.setUTCHours(hour, minute, 0, 0);
14
+ return date.getTime();
15
+ }
16
+
17
+ export function validTimestamp(value) {
18
+ return Number.isFinite(value) && Number.isFinite(new Date(value).getTime());
19
+ }
20
+
4
21
  export const TIME_UNITS = cloneAndFreeze([
5
22
  "year",
6
23
  "quarter",
@@ -52,13 +69,6 @@ export function normalizeTimeUnitTransform({ field, unit, as } = {}) {
52
69
  return cloneAndFreeze(transform);
53
70
  }
54
71
 
55
- function utcTimestamp(year, month = 0, day = 1, hour = 0, minute = 0) {
56
- const date = new Date(0);
57
- date.setUTCFullYear(year, month, day);
58
- date.setUTCHours(hour, minute, 0, 0);
59
- return date.getTime();
60
- }
61
-
62
72
  export function floorUtcTimeUnit(timestamp, unit) {
63
73
  if (!Number.isFinite(timestamp)) {
64
74
  throw new TypeError("Time-unit timestamp must be finite.");
@@ -67,7 +77,7 @@ export function floorUtcTimeUnit(timestamp, unit) {
67
77
  throw new Error(`Unsupported time unit "${unit}".`);
68
78
  }
69
79
  const date = new Date(timestamp);
70
- if (!Number.isFinite(date.getTime())) {
80
+ if (!validTimestamp(timestamp)) {
71
81
  throw new TypeError("Time-unit timestamp must represent a valid date.");
72
82
  }
73
83
  const year = date.getUTCFullYear();
@@ -32,7 +32,10 @@ function facetHorizonTransform(transform, { scales = {} } = {}) {
32
32
  (scales.y ?? "shared") !== "shared" ||
33
33
  !Array.isArray(transform.resolved?.extents)
34
34
  ) return transform;
35
- const extent = Math.max(...transform.resolved.extents.map(item => item.extent));
35
+ let extent = -Infinity;
36
+ for (const item of transform.resolved.extents) {
37
+ extent = Math.max(extent, item.extent);
38
+ }
36
39
  if (!(extent > 0)) return transform;
37
40
  return {
38
41
  ...requestedHorizonTransform(transform),
@@ -1,18 +1,23 @@
1
1
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
+ import {
3
+ stableFiniteMean,
4
+ stableFinitePrefixSums,
5
+ stableFiniteSum
6
+ } from "./numeric.js";
2
7
 
3
- const TRANSFORM_KEYS = Object.freeze([
8
+ const TRANSFORM_KEYS = [
4
9
  "type", "partitionBy", "sortBy", "operations"
5
- ]);
6
- const SORT_KEYS = Object.freeze(["field", "order"]);
7
- const ORDER_VALUES = Object.freeze(["ascending", "descending"]);
8
- const OPERATION_VALUES = Object.freeze([
10
+ ];
11
+ const SORT_KEYS = ["field", "order"];
12
+ const ORDER_VALUES = ["ascending", "descending"];
13
+ const OPERATION_VALUES = [
9
14
  "rowNumber", "rank", "denseRank", "cumulativeSum", "lag", "lead",
10
15
  "movingMean", "movingSum"
11
- ]);
12
- const POSITION_OPERATIONS = new Set(["rowNumber", "rank", "denseRank"]);
13
- const OFFSET_OPERATIONS = new Set(["lag", "lead"]);
14
- const MOVING_OPERATIONS = new Set(["movingMean", "movingSum"]);
15
- const FRAME_KEYS = Object.freeze(["preceding", "following"]);
16
+ ];
17
+ const POSITION_OPERATIONS = new Set(OPERATION_VALUES.slice(0, 3));
18
+ const OFFSET_OPERATIONS = new Set(OPERATION_VALUES.slice(4, 6));
19
+ const MOVING_OPERATIONS = new Set(OPERATION_VALUES.slice(-2));
20
+ const FRAME_KEYS = ["preceding", "following"];
16
21
 
17
22
  function requireField(value, label) {
18
23
  if (typeof value !== "string" || value.length === 0) {
@@ -77,15 +82,12 @@ function validateFrame(frame, operation) {
77
82
  throw new TypeError(`Window ${operation} frame must be a plain object.`);
78
83
  }
79
84
  rejectUnknownKeys(frame, FRAME_KEYS, `window ${operation} frame`);
80
- if (!Number.isInteger(frame.preceding) || frame.preceding < 0) {
81
- throw new RangeError(
82
- `Window ${operation} frame preceding must be a non-negative integer.`
83
- );
84
- }
85
- if (!Number.isInteger(frame.following) || frame.following < 0) {
86
- throw new RangeError(
87
- `Window ${operation} frame following must be a non-negative integer.`
88
- );
85
+ for (const field of FRAME_KEYS) {
86
+ if (!Number.isInteger(frame[field]) || frame[field] < 0) {
87
+ throw new RangeError(
88
+ `Window ${operation} frame ${field} must be a non-negative integer.`
89
+ );
90
+ }
89
91
  }
90
92
  }
91
93
 
@@ -264,12 +266,6 @@ function validateSourceFields(rows, transform) {
264
266
  }
265
267
 
266
268
  function validatePartitionSortValues(partition, transform) {
267
- for (const field of transform.partitionBy) {
268
- partition.forEach(entry => scalarKey(
269
- entry.row[field],
270
- `Window partition field "${field}"`
271
- ));
272
- }
273
269
  for (const sort of transform.sortBy) {
274
270
  const types = new Set(partition.flatMap(({ row }) => {
275
271
  const value = row[sort.field];
@@ -301,12 +297,45 @@ function partitionRows(entries, partitionBy) {
301
297
  entry.row[field],
302
298
  `Window partition field "${field}"`
303
299
  )).join("\0");
304
- if (!partitions.has(key)) partitions.set(key, []);
305
- partitions.get(key).push(entry);
300
+ let partition = partitions.get(key);
301
+ if (partition === undefined) {
302
+ partition = [];
303
+ partitions.set(key, partition);
304
+ }
305
+ partition.push(entry);
306
306
  }
307
307
  return [...partitions.values()];
308
308
  }
309
309
 
310
+ function operationValues(
311
+ partition,
312
+ operation,
313
+ first = 0,
314
+ last = partition.length - 1
315
+ ) {
316
+ const values = [];
317
+ for (let index = first; index <= last; index += 1) {
318
+ const value = partition[index].row[operation.field];
319
+ if (!Number.isFinite(value)) {
320
+ throw new TypeError(
321
+ `Window ${operation.op} field "${operation.field}" must contain finite numbers.`
322
+ );
323
+ }
324
+ values.push(value);
325
+ }
326
+ return values;
327
+ }
328
+
329
+ function stableWindowOutput(operation, calculate) {
330
+ const label = `Window ${operation.op} output "${operation.as}"`;
331
+ try {
332
+ return calculate(label);
333
+ } catch (error) {
334
+ if (!(error instanceof RangeError)) throw error;
335
+ throw new RangeError(`${label} must be finite.`);
336
+ }
337
+ }
338
+
310
339
  function applyOperation(partition, operation, sortBy) {
311
340
  if (operation.op === "rowNumber") {
312
341
  partition.forEach((entry, index) => {
@@ -330,44 +359,29 @@ function applyOperation(partition, operation, sortBy) {
330
359
  return;
331
360
  }
332
361
  if (operation.op === "cumulativeSum") {
333
- let total = 0;
334
- for (const entry of partition) {
335
- const value = entry.row[operation.field];
336
- if (!Number.isFinite(value)) {
337
- throw new TypeError(
338
- `Window cumulativeSum field "${operation.field}" must contain finite numbers.`
339
- );
340
- }
341
- total += value;
342
- entry.row[operation.as] = total;
343
- }
362
+ const values = operationValues(partition, operation);
363
+ const totals = stableWindowOutput(operation, label =>
364
+ stableFinitePrefixSums(values, label)
365
+ );
366
+ partition.forEach((entry, index) => {
367
+ entry.row[operation.as] = totals[index];
368
+ });
344
369
  return;
345
370
  }
346
371
  if (MOVING_OPERATIONS.has(operation.op)) {
372
+ const calculate = operation.op === "movingMean"
373
+ ? stableFiniteMean
374
+ : stableFiniteSum;
347
375
  partition.forEach((entry, index) => {
348
376
  const first = Math.max(0, index - operation.frame.preceding);
349
377
  const last = Math.min(
350
378
  partition.length - 1,
351
379
  index + operation.frame.following
352
380
  );
353
- let total = 0;
354
- for (let peerIndex = first; peerIndex <= last; peerIndex += 1) {
355
- const value = partition[peerIndex].row[operation.field];
356
- if (!Number.isFinite(value)) {
357
- throw new TypeError(
358
- `Window ${operation.op} field "${operation.field}" must contain finite numbers.`
359
- );
360
- }
361
- total += value;
362
- if (!Number.isFinite(total)) {
363
- throw new RangeError(
364
- `Window ${operation.op} output "${operation.as}" must be finite.`
365
- );
366
- }
367
- }
368
- entry.row[operation.as] = operation.op === "movingMean"
369
- ? total / (last - first + 1)
370
- : total;
381
+ const values = operationValues(partition, operation, first, last);
382
+ entry.row[operation.as] = stableWindowOutput(operation, label =>
383
+ calculate(values, label)
384
+ );
371
385
  });
372
386
  return;
373
387
  }
@@ -400,6 +414,5 @@ export function deriveWindowRows(rows, transform) {
400
414
  applyOperation(partition, operation, transform.sortBy);
401
415
  }
402
416
  }
403
- entries.sort((left, right) => left.index - right.index);
404
417
  return cloneAndFreeze(entries.map(entry => entry.row));
405
418
  }
package/src/index.js CHANGED
@@ -1,3 +1,3 @@
1
+ export { render } from "./renderers/canvas/index.js";
1
2
  export { chart } from "./ChartProgram.js";
2
3
  export { hconcat, vconcat } from "./composition.js";
3
- export { render } from "./renderers/canvas/index.js";
@@ -1,7 +1,8 @@
1
1
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
2
  import {
3
3
  validateNonEmptyString,
4
- validateNonNegativeFinite
4
+ validateNonNegativeFinite,
5
+ validatePositiveFinite
5
6
  } from "../core/validation.js";
6
7
 
7
8
  const MARGIN_KEYS = Object.freeze(["top", "right", "bottom", "left"]);
@@ -63,13 +64,8 @@ export function normalizeMargin(margin, base = DEFAULT_MARGIN) {
63
64
  }
64
65
 
65
66
  export function validateCanvasState({ width, height, background, margin }) {
66
- if (!Number.isInteger(width) || width <= 0) {
67
- throw new RangeError("Canvas width must be a positive integer.");
68
- }
69
-
70
- if (!Number.isInteger(height) || height <= 0) {
71
- throw new RangeError("Canvas height must be a positive integer.");
72
- }
67
+ validatePositiveFinite(width, "Canvas width");
68
+ validatePositiveFinite(height, "Canvas height");
73
69
 
74
70
  validateNonEmptyString(background, "Canvas background");
75
71
 
@@ -1,9 +1,9 @@
1
1
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
2
 
3
- const DIRECTIONS = Object.freeze(["horizontal", "vertical"]);
4
- const ALIGNMENTS = Object.freeze(["start", "center", "end"]);
5
- const SIZE_MODES = Object.freeze(["auto", "explicit"]);
6
- const PADDING_KEYS = Object.freeze(["top", "right", "bottom", "left"]);
3
+ const DIRECTIONS = ["horizontal", "vertical"];
4
+ const ALIGNMENTS = ["start", "center", "end"];
5
+ const SIZE_MODES = ["auto", "explicit"];
6
+ const PADDING_KEYS = ["top", "right", "bottom", "left"];
7
7
 
8
8
  export const DEFAULT_COMPOSITION_LAYOUT = cloneAndFreeze({
9
9
  gap: 16,
@@ -124,21 +124,16 @@ export function normalizeCompositionChildren(children) {
124
124
  function normalizeAutoCrossSize(children, horizontal) {
125
125
  const dimension = horizontal ? "height" : "width";
126
126
  const mode = `${dimension}Mode`;
127
- const sharedSize = Math.max(...children.map(child => child[dimension]));
127
+ const sharedSize = children.reduce(
128
+ (maximum, child) => Math.max(maximum, child[dimension]),
129
+ -Infinity
130
+ );
128
131
  return children.map(child => cloneAndFreeze({
129
132
  ...child,
130
133
  [dimension]: child[mode] === "auto" ? sharedSize : child[dimension]
131
134
  }));
132
135
  }
133
136
 
134
- function placementSize(child) {
135
- return {
136
- id: child.id,
137
- width: child.width,
138
- height: child.height
139
- };
140
- }
141
-
142
137
  function crossOffset(remaining, align) {
143
138
  if (align === "start") return 0;
144
139
  if (align === "end") return remaining;
@@ -162,30 +157,36 @@ export function resolveCompositionLayout({
162
157
  const contentWidth = horizontal
163
158
  ? sizedChildren.reduce((sum, child) => sum + child.width, 0) +
164
159
  resolvedGap * (sizedChildren.length - 1)
165
- : Math.max(...sizedChildren.map(child => child.width));
160
+ : sizedChildren.reduce(
161
+ (maximum, child) => Math.max(maximum, child.width),
162
+ -Infinity
163
+ );
166
164
  const contentHeight = horizontal
167
- ? Math.max(...sizedChildren.map(child => child.height))
165
+ ? sizedChildren.reduce(
166
+ (maximum, child) => Math.max(maximum, child.height),
167
+ -Infinity
168
+ )
168
169
  : sizedChildren.reduce((sum, child) => sum + child.height, 0) +
169
170
  resolvedGap * (sizedChildren.length - 1);
170
171
  let cursor = horizontal ? resolvedPadding.left : resolvedPadding.top;
171
172
  const placements = sizedChildren.map(child => {
172
- const placement = horizontal
173
- ? {
174
- ...placementSize(child),
175
- x: cursor,
176
- y: resolvedPadding.top + crossOffset(
177
- contentHeight - child.height,
178
- resolvedAlign
179
- )
180
- }
181
- : {
182
- ...placementSize(child),
183
- x: resolvedPadding.left + crossOffset(
173
+ const placement = {
174
+ id: child.id,
175
+ width: child.width,
176
+ height: child.height,
177
+ x: horizontal
178
+ ? cursor
179
+ : resolvedPadding.left + crossOffset(
184
180
  contentWidth - child.width,
185
181
  resolvedAlign
186
182
  ),
187
- y: cursor
188
- };
183
+ y: horizontal
184
+ ? resolvedPadding.top + crossOffset(
185
+ contentHeight - child.height,
186
+ resolvedAlign
187
+ )
188
+ : cursor
189
+ };
189
190
  cursor += (horizontal ? child.width : child.height) + resolvedGap;
190
191
  return placement;
191
192
  });
@@ -264,7 +265,11 @@ export function resolvePlacedPlotBounds({ placements, plots } = {}) {
264
265
  }
265
266
  plotById.set(plot.id, validatePlacedBounds(plot, `Placed plot "${plot.id}"`));
266
267
  }
267
- const translated = placements.map((placement, index) => {
268
+ let left = Infinity;
269
+ let top = Infinity;
270
+ let right = -Infinity;
271
+ let bottom = -Infinity;
272
+ placements.forEach((placement, index) => {
268
273
  if (
269
274
  !isPlainObject(placement) ||
270
275
  typeof placement.id !== "string" ||
@@ -287,20 +292,14 @@ export function resolvePlacedPlotBounds({ placements, plots } = {}) {
287
292
  );
288
293
  }
289
294
  plotById.delete(placement.id);
290
- return {
291
- left: placement.x + plot.x,
292
- top: placement.y + plot.y,
293
- right: placement.x + plot.x + plot.width,
294
- bottom: placement.y + plot.y + plot.height
295
- };
295
+ left = Math.min(left, placement.x + plot.x);
296
+ top = Math.min(top, placement.y + plot.y);
297
+ right = Math.max(right, placement.x + plot.x + plot.width);
298
+ bottom = Math.max(bottom, placement.y + plot.y + plot.height);
296
299
  });
297
300
  if (plotById.size > 0) {
298
301
  throw new Error(`Unknown placed plot id "${plotById.keys().next().value}".`);
299
302
  }
300
- const left = Math.min(...translated.map(bounds => bounds.left));
301
- const top = Math.min(...translated.map(bounds => bounds.top));
302
- const right = Math.max(...translated.map(bounds => bounds.right));
303
- const bottom = Math.max(...translated.map(bounds => bounds.bottom));
304
303
  return cloneAndFreeze({
305
304
  x: left,
306
305
  y: top,