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
@@ -29,7 +29,7 @@ function requireLineEncoding(layer) {
29
29
  const y = layer.encoding?.y;
30
30
 
31
31
  if (x === undefined) {
32
- throw new Error(`Line mark "${layer.id}" requires a temporal x encoding.`);
32
+ throw new Error(`Line mark "${layer.id}" requires an x encoding.`);
33
33
  }
34
34
 
35
35
  const binnedAggregate =
@@ -37,10 +37,10 @@ function requireLineEncoding(layer) {
37
37
  x.bin !== undefined &&
38
38
  isAggregate(y?.aggregate);
39
39
  const aggregateMode =
40
- (x?.fieldType === "temporal" && isAggregate(y?.aggregate)) ||
40
+ (["temporal", "nominal", "ordinal"].includes(x?.fieldType) && isAggregate(y?.aggregate)) ||
41
41
  binnedAggregate;
42
- const directQuantitative =
43
- x?.fieldType === "quantitative" &&
42
+ const directNumericY =
43
+ ["quantitative", "nominal", "ordinal"].includes(x?.fieldType) &&
44
44
  x.bin === undefined &&
45
45
  y?.fieldType === "quantitative" &&
46
46
  y.aggregate === undefined;
@@ -48,14 +48,14 @@ function requireLineEncoding(layer) {
48
48
  y?.aggregate === undefined &&
49
49
  ((x?.fieldType === "temporal" && y?.fieldType === "quantitative") ||
50
50
  (x?.fieldType === "quantitative" && y?.fieldType === "temporal"));
51
- if (!aggregateMode && !directQuantitative && !directTemporal) {
51
+ if (!aggregateMode && !directNumericY && !directTemporal) {
52
52
  if (x?.fieldType === "temporal") {
53
53
  throw new Error(
54
54
  `Line mark "${layer.id}" requires a supported aggregate y encoding.`
55
55
  );
56
56
  }
57
57
  throw new Error(
58
- `Line mark "${layer.id}" requires temporal/aggregate or direct quantitative x/y encodings.`
58
+ `Line mark "${layer.id}" requires categorical/temporal aggregate or compatible direct x/y encodings.`
59
59
  );
60
60
  }
61
61
  if (aggregateMode) {
@@ -66,7 +66,7 @@ function requireLineEncoding(layer) {
66
66
  y,
67
67
  isAggregate: aggregateMode,
68
68
  binnedAggregate,
69
- directRows: directTemporal || directQuantitative,
69
+ directRows: directTemporal || directNumericY,
70
70
  directTemporal
71
71
  };
72
72
  }
@@ -90,9 +90,17 @@ function groupedSeries(groups, fields, dimensions) {
90
90
  return series;
91
91
  }
92
92
 
93
- function freezeCartesianSeries(series) {
93
+ function freezeCartesianSeries(series, categoryOrder) {
94
+ const observedX = series.flatMap(item => item.values.map(value => value.x));
95
+ const observedSet = new Set(observedX);
96
+ if (categoryOrder !== undefined) {
97
+ const domain = new Set(categoryOrder);
98
+ if (observedX.some(value => !domain.has(value))) {
99
+ throw new Error("Line categorical x domain must contain every observed vertex.");
100
+ }
101
+ }
94
102
  return cloneAndFreeze({
95
- xValues: series.flatMap(item => item.values.map(value => value.x)),
103
+ xValues: categoryOrder === undefined ? observedX : categoryOrder.filter(value => observedSet.has(value)),
96
104
  yValues: series.flatMap(item => item.values.map(value => value.y)),
97
105
  series
98
106
  });
@@ -122,7 +130,9 @@ function deriveCartesianLineSeries(rows, layer, options = {}) {
122
130
  if (isAggregate) {
123
131
  validateAggregateFieldValues(rows, y.field, y.fieldType);
124
132
  }
125
- const xValues = x.fieldType === "temporal"
133
+ const categorical = ["nominal", "ordinal"].includes(x.fieldType);
134
+ const xValues = categorical ? readNominalField(rows, x.field)
135
+ : x.fieldType === "temporal"
126
136
  ? readTemporalField(rows, x.field, x.temporalUnit)
127
137
  : readQuantitativeField(rows, x.field);
128
138
  const yValues = isAggregate
@@ -130,6 +140,11 @@ function deriveCartesianLineSeries(rows, layer, options = {}) {
130
140
  : y.fieldType === "temporal"
131
141
  ? readTemporalField(rows, y.field, y.temporalUnit)
132
142
  : readQuantitativeField(rows, y.field);
143
+ const categoryOrder = categorical ? options.xDomain ?? [...new Set(xValues)] : undefined;
144
+ const ranks = new Map(categoryOrder?.map((value, index) => [value, index]));
145
+ const compareX = categorical
146
+ ? (left, right) => ranks.get(left.x) - ranks.get(right.x)
147
+ : (left, right) => left.x - right.x;
133
148
  const seriesFields = validatePathSeriesAppearance(rows, layer);
134
149
  const binBoundaries = binnedAggregate
135
150
  ? options.xBinBoundaries ?? resolveLineBins(rows, layer).boundaries
@@ -159,14 +174,14 @@ function deriveCartesianLineSeries(rows, layer, options = {}) {
159
174
  item.orderValues,
160
175
  pathOrder.order
161
176
  );
162
- return values.length < 2 ? [] : [{ key: item.key, values }];
177
+ return [{ key: item.key, values }];
163
178
  });
164
179
  if (series.length === 0) {
165
180
  throw new Error(
166
181
  `Line series on mark "${layer.id}" requires at least two ordered points.`
167
182
  );
168
183
  }
169
- return freezeCartesianSeries(series);
184
+ return freezeCartesianSeries(series, categoryOrder);
170
185
  }
171
186
 
172
187
  if (directRows) {
@@ -181,16 +196,16 @@ function deriveCartesianLineSeries(rows, layer, options = {}) {
181
196
  const orderBy = directTemporal && y.fieldType === "temporal" ? "y" : "x";
182
197
  const series = [...groups.values()].flatMap(item => {
183
198
  const values = item.values.sort(
184
- (left, right) => left[orderBy] - right[orderBy]
199
+ orderBy === "x" ? compareX : (left, right) => left.y - right.y
185
200
  );
186
- return values.length < 2 ? [] : [{ key: item.key, values }];
201
+ return [{ key: item.key, values }];
187
202
  });
188
203
  if (series.length === 0) {
189
204
  throw new Error(
190
205
  `Line series on mark "${layer.id}" requires at least two direct points.`
191
206
  );
192
207
  }
193
- return freezeCartesianSeries(series);
208
+ return freezeCartesianSeries(series, categoryOrder);
194
209
  }
195
210
  const aggregateGroups = new Map();
196
211
 
@@ -241,7 +256,7 @@ function deriveCartesianLineSeries(rows, layer, options = {}) {
241
256
  }
242
257
 
243
258
  const series = [...seriesGroups.values()].flatMap(item => {
244
- const values = item.values.sort((left, right) => left.x - right.x);
259
+ const values = item.values.sort(compareX);
245
260
 
246
261
  if (values.length < 2) {
247
262
  return [];
@@ -256,7 +271,7 @@ function deriveCartesianLineSeries(rows, layer, options = {}) {
256
271
  );
257
272
  }
258
273
 
259
- return freezeCartesianSeries(series);
274
+ return freezeCartesianSeries(series, categoryOrder);
260
275
  }
261
276
 
262
277
  function requirePolarLineEncoding(layer) {
@@ -35,7 +35,8 @@ export function normalizeMarkLabelContent(source, { content, normalizeBy }) {
35
35
  }
36
36
  if (source.mark.type === "bar") {
37
37
  const grain = resolveBarGrain(source);
38
- if (grain === BAR_GRAINS.ranged) {
38
+ if (grain === BAR_GRAINS.ranged || grain === BAR_GRAINS.centered &&
39
+ source.encoding[`${resolveBarChannels(source).measure}2`] !== undefined) {
39
40
  throw new Error("Ranged Bar text requires an explicit field or value.");
40
41
  }
41
42
  if (content === "category" && grain === BAR_GRAINS.histogram) {
@@ -63,10 +64,12 @@ function itemValue(source, item) {
63
64
  );
64
65
  }
65
66
  const measure = source.encoding[resolveBarChannels(source).measure];
67
+ if (resolveBarGrain(source) === BAR_GRAINS.centered) return item.channels[`${resolveBarChannels(source).measure}2`];
66
68
  return measure.aggregate === "count" ? item.members.length
67
69
  : aggregateRows(item.members, measure.field, measure.aggregate);
68
70
  }
69
- if (source.encoding?.radius !== undefined) return item.channels.radius;
71
+ if (source.encoding?.radius !== undefined && source.encoding.theta.fieldType !== "quantitative" &&
72
+ source.encoding.theta.aggregate === undefined) return item.channels.radius;
70
73
  const theta = source.encoding.theta;
71
74
  if (theta.fieldType === "quantitative") return item.channels.theta;
72
75
  return theta.aggregate === "count" ? item.members.length
@@ -1,3 +1,6 @@
1
+ import { scalarKey, normalizeSortBy, validateSortBy } from "./transformKeys.js";
2
+ import { rejectUnknownProperties as rejectUnknownKeys } from "../core/validation.js";
3
+ import { requireStringValue as requireField } from "../core/validation.js";
1
4
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
5
  import {
3
6
  inverseLerp,
@@ -15,23 +18,9 @@ const METHODS = Object.freeze([
15
18
  "share", "zscore", "minmax", "index", "change", "percentChange"
16
19
  ]);
17
20
  const ZERO_POLICIES = Object.freeze(["error", "null", "zero"]);
18
- const SORT_KEYS = Object.freeze(["field", "order"]);
19
- const ORDERS = Object.freeze(["ascending", "descending"]);
20
21
  const BASELINE_KEYS = Object.freeze(["position", "value"]);
21
22
  const BASELINE_METHODS = new Set(["index", "change", "percentChange"]);
22
23
 
23
- function rejectUnknownKeys(value, supported, label) {
24
- const unknown = Object.keys(value).find(key => !supported.includes(key));
25
- if (unknown !== undefined) throw new Error(`Unknown ${label} property "${unknown}".`);
26
- }
27
-
28
- function requireField(value, label) {
29
- if (typeof value !== "string" || value.length === 0) {
30
- throw new TypeError(`${label} must be a non-empty string.`);
31
- }
32
- return value;
33
- }
34
-
35
24
  function normalizeGroupBy(value) {
36
25
  if (value === undefined) return [];
37
26
  return Array.isArray(value) ? [...value] : [value];
@@ -48,33 +37,6 @@ function validateGroupBy(groupBy) {
48
37
  }
49
38
  }
50
39
 
51
- function normalizeSortBy(value) {
52
- if (value === undefined) return [];
53
- if (!Array.isArray(value)) return value;
54
- return value.map(sort => isPlainObject(sort)
55
- ? { ...sort, order: sort.order ?? "ascending" }
56
- : sort
57
- );
58
- }
59
-
60
- function validateSortBy(sortBy) {
61
- if (!Array.isArray(sortBy)) throw new TypeError("Normalize sortBy must be an array.");
62
- const fields = [];
63
- sortBy.forEach((sort, index) => {
64
- if (!isPlainObject(sort)) {
65
- throw new TypeError(`Normalize sortBy[${index}] must be a plain object.`);
66
- }
67
- rejectUnknownKeys(sort, SORT_KEYS, `normalize sortBy[${index}]`);
68
- fields.push(requireField(sort.field, `Normalize sortBy[${index}].field`));
69
- if (!ORDERS.includes(sort.order)) {
70
- throw new Error(`Unsupported normalize sort order "${sort.order}".`);
71
- }
72
- });
73
- if (new Set(fields).size !== fields.length) {
74
- throw new Error("Normalize sortBy fields must be unique.");
75
- }
76
- }
77
-
78
40
  function normalizeBaseline(value) {
79
41
  if (!isPlainObject(value)) return value;
80
42
  return { ...value };
@@ -136,7 +98,7 @@ export function validateNormalizeTransform(transform) {
136
98
  throw new Error("Normalize zeroDenominator is not available for change.");
137
99
  }
138
100
  if (BASELINE_METHODS.has(transform.method)) {
139
- validateSortBy(transform.sortBy);
101
+ validateSortBy(transform.sortBy, "Normalize");
140
102
  validateBaseline(transform.baseline, transform.sortBy);
141
103
  } else {
142
104
  if (hasBaseline) {
@@ -184,18 +146,6 @@ export function normalizeNormalizeTransform(args = {}) {
184
146
  return cloneAndFreeze(transform);
185
147
  }
186
148
 
187
- function scalarKey(value, label) {
188
- if (value === null) return "null";
189
- if (typeof value === "string") return `string:${value.length}:${value}`;
190
- if (typeof value === "boolean") return `boolean:${value}`;
191
- if (typeof value === "number" && Number.isFinite(value)) {
192
- return `number:${Object.is(value, -0) ? 0 : value}`;
193
- }
194
- throw new TypeError(
195
- `${label} must contain null, strings, booleans, or finite numbers.`
196
- );
197
- }
198
-
199
149
  function compareScalar(left, right, label) {
200
150
  const leftMissing = left === null || left === undefined;
201
151
  const rightMissing = right === null || right === undefined;
@@ -1,290 +1 @@
1
- export const NICE_FACTORS = Object.freeze([1, 2, 3, 5]);
2
-
3
- function finiteArray(values, label) {
4
- if (!Array.isArray(values) || !values.every(Number.isFinite)) {
5
- throw new TypeError(`${label} must contain finite numbers.`);
6
- }
7
- }
8
-
9
- function rangeError(label) {
10
- return new RangeError(`${label} is outside the finite numeric range.`);
11
- }
12
-
13
- function compensatedSum(values, scale = 1) {
14
- let total = 0;
15
- let correction = 0;
16
- for (const source of values) {
17
- const value = source / scale;
18
- const next = total + value;
19
- correction += Math.abs(total) >= Math.abs(value)
20
- ? total - next + value
21
- : value - next + total;
22
- total = next;
23
- }
24
- return total + correction;
25
- }
26
-
27
- export function maximumMagnitude(values) {
28
- let maximum = 0;
29
- for (const value of values) maximum = Math.max(maximum, Math.abs(value));
30
- return maximum;
31
- }
32
-
33
- export function normalizedFiniteSum(values, label = "Numeric values") {
34
- finiteArray(values, label);
35
- const scale = maximumMagnitude(values) || 1;
36
- return { scale, total: compensatedSum(values, scale) };
37
- }
38
-
39
- export function requireFiniteResult(value, label = "Numeric result") {
40
- if (!Number.isFinite(value)) throw rangeError(label);
41
- return value === 0 ? 0 : value;
42
- }
43
-
44
- export function restoreFiniteScale(value, scale, label = "Numeric result") {
45
- if (!Number.isFinite(value) || !Number.isFinite(scale) || scale < 0) {
46
- throw rangeError(label);
47
- }
48
- return requireFiniteResult(value * scale, label);
49
- }
50
-
51
- function stableReducedSum(values, label, divisor = 1) {
52
- finiteArray(values, label);
53
- const ordinary = values.reduce((sum, value) => sum + value, 0);
54
- if (Number.isFinite(ordinary)) {
55
- return ordinary === 0 ? 0 : ordinary / divisor;
56
- }
57
- const scale = maximumMagnitude(values) || 1;
58
- return restoreFiniteScale(
59
- compensatedSum(values, scale) / divisor, scale, label
60
- );
61
- }
62
-
63
- export function stableFiniteSum(values, label = "Numeric sum") {
64
- return stableReducedSum(values, label);
65
- }
66
-
67
- export function stableFinitePrefixSums(values, label = "Numeric sum") {
68
- const normalized = normalizedFiniteSum(values, label);
69
- let ordinary = 0;
70
- let sum = 0;
71
- let correction = 0;
72
- return values.map(value => {
73
- ordinary += value;
74
- const scaled = value / normalized.scale;
75
- const next = sum + scaled;
76
- correction += Math.abs(sum) >= Math.abs(scaled)
77
- ? sum - next + scaled
78
- : scaled - next + sum;
79
- sum = next;
80
- return Number.isFinite(ordinary)
81
- ? ordinary === 0 ? 0 : ordinary
82
- : restoreFiniteScale(sum + correction, normalized.scale, label);
83
- });
84
- }
85
-
86
- export function stableFiniteMean(values, label = "Numeric mean") {
87
- finiteArray(values, label);
88
- if (values.length === 0) {
89
- throw new RangeError(`${label} requires at least one value.`);
90
- }
91
- return stableReducedSum(values, label, values.length);
92
- }
93
-
94
- export function stableFiniteSquareSum(values, label = "Numeric square sum") {
95
- finiteArray(values, label);
96
- const ordinary = values.reduce((sum, value) => sum + value ** 2, 0);
97
- if (Number.isFinite(ordinary)) return ordinary;
98
- const scale = maximumMagnitude(values);
99
- if (scale === 0) return 0;
100
- const normalized = values.reduce(
101
- (sum, value) => sum + (value / scale) ** 2,
102
- 0
103
- );
104
- const magnitude = restoreFiniteScale(Math.sqrt(normalized), scale, label);
105
- return requireFiniteResult(magnitude ** 2, label);
106
- }
107
-
108
- export function stableFiniteDeviation(values, {
109
- sample = false,
110
- divisor = 1,
111
- label = "Numeric deviation"
112
- } = {}) {
113
- if (!Number.isFinite(divisor) || divisor <= 0) {
114
- throw new RangeError(`${label} divisor must be positive and finite.`);
115
- }
116
- const denominator = values.length - (sample ? 1 : 0);
117
- if (denominator <= 0) {
118
- throw new RangeError(`${label} requires more values.`);
119
- }
120
- const mean = stableFiniteMean(values, `${label} mean`);
121
- const differences = values.map(value => value - mean);
122
- const squared = differences.reduce(
123
- (sum, difference) => sum + difference ** 2,
124
- 0
125
- );
126
- if (Number.isFinite(squared)) {
127
- return {
128
- mean,
129
- deviation: Math.sqrt(squared / denominator) / divisor,
130
- squared
131
- };
132
- }
133
- const finiteDifferences = differences.every(Number.isFinite);
134
- const scale = finiteDifferences
135
- ? maximumMagnitude(differences)
136
- : Math.max(Math.abs(mean), maximumMagnitude(values));
137
- if (scale === 0) return { mean, deviation: 0 };
138
- const normalizedSquares = values.reduce((sum, value, index) => {
139
- const difference = finiteDifferences
140
- ? differences[index] / scale
141
- : value / scale - mean / scale;
142
- return sum + difference ** 2;
143
- }, 0);
144
- const deviation = restoreFiniteScale(
145
- Math.sqrt(normalizedSquares / denominator) / divisor,
146
- scale,
147
- label
148
- );
149
- return { mean, deviation };
150
- }
151
-
152
- export function numericExtent(values) {
153
- let minimum = Infinity;
154
- let maximum = -Infinity;
155
- for (const value of values) {
156
- minimum = Math.min(minimum, value);
157
- maximum = Math.max(maximum, value);
158
- }
159
- return [minimum, maximum];
160
- }
161
-
162
- export function normalizeNumericRange(start, end) {
163
- const magnitude = Math.max(Math.abs(start), Math.abs(end));
164
- if (magnitude === 0) {
165
- return { scale: 1, start: 0, end: 0, span: 0 };
166
- }
167
-
168
- const decimalScale = 10 ** Math.floor(Math.log10(magnitude));
169
- const scale = Number.isFinite(decimalScale) && decimalScale > 0
170
- ? decimalScale
171
- : magnitude;
172
- start /= scale;
173
- end /= scale;
174
- return {
175
- scale,
176
- start,
177
- end,
178
- span: end - start
179
- };
180
- }
181
-
182
- export function niceNumericStep(
183
- span,
184
- count = 5,
185
- factors = NICE_FACTORS
186
- ) {
187
- if (!Number.isFinite(span) || span <= 0) {
188
- throw new RangeError("Nice numeric span must be positive and finite.");
189
- }
190
- const rough = span / Math.max(1, count);
191
- const power = 10 ** Math.floor(Math.log10(rough));
192
- const fraction = rough / power;
193
- const factor = factors.find(candidate => candidate >= fraction) ?? 10;
194
- return factor * power;
195
- }
196
-
197
- export function alignNumericStep(value, step, direction) {
198
- let quotient = value / step;
199
- const nearest = Math.round(quotient);
200
- if (Math.abs(quotient - nearest) <= 1e-10) quotient = nearest;
201
- return (direction === "floor" ? Math.floor(quotient) : Math.ceil(quotient)) * step;
202
- }
203
-
204
- export function cleanNumericValue(value, resolution) {
205
- if (!Number.isFinite(value) || value === 0) return value === 0 ? 0 : value;
206
- const rounded = Number(value.toPrecision(15));
207
- return Number.isFinite(resolution) && resolution > 0 &&
208
- Math.abs(rounded - value) <= resolution * 1e-10
209
- ? rounded
210
- : value;
211
- }
212
-
213
- export function inverseLerp(value, start, end) {
214
- if (value === start) return 0;
215
- if (value === end) return 1;
216
-
217
- const span = end - start;
218
- const offset = value - start;
219
- if (Number.isFinite(span) && span !== 0 && Number.isFinite(offset)) {
220
- return offset / span;
221
- }
222
-
223
- const scale = Math.max(Math.abs(value), Math.abs(start), Math.abs(end));
224
- return (value / scale - start / scale) / (end / scale - start / scale);
225
- }
226
-
227
- export function interpolateNumber(start, end, proportion) {
228
- let result = proportion === 0 ? start : proportion === 1 ? end : undefined;
229
- if (result === undefined) {
230
- const span = end - start;
231
- if (Number.isFinite(span)) {
232
- result = start + proportion * span;
233
- } else {
234
- const scale = Math.max(Math.abs(start), Math.abs(end));
235
- result = scale * (
236
- start / scale * (1 - proportion) + end / scale * proportion
237
- );
238
- }
239
- }
240
- return requireFiniteResult(result, "Numeric interpolation");
241
- }
242
-
243
- export function finiteMidpoint(start, end) {
244
- const sum = start + end;
245
- const result = Number.isFinite(sum) ? sum / 2 : start / 2 + end / 2;
246
- return requireFiniteResult(result, "Numeric midpoint");
247
- }
248
-
249
- export function sampleNumericRange(start, end, count, label = "Numeric range") {
250
- const resolution = Math.abs((end - start) / (count - 1));
251
- const values = [];
252
- const direction = Math.sign(end - start);
253
- for (let index = 0; index < count; index += 1) {
254
- const value = cleanNumericValue(
255
- interpolateNumber(start, end, index / (count - 1)),
256
- resolution
257
- );
258
- if (
259
- !Number.isFinite(value) ||
260
- (direction !== 0 && index > 0 &&
261
- direction * (value - values[index - 1]) <= 0)
262
- ) {
263
- throw new RangeError(`${label} cannot represent ${count} distinct samples.`);
264
- }
265
- values.push(value);
266
- }
267
- return values;
268
- }
269
-
270
- export function formatDistinctNumericSamples(values, initialFormat) {
271
- let labels = values.map(initialFormat ?? (value =>
272
- String(+value.toPrecision(3))
273
- ));
274
- const distinct = () => labels.every((label, index) =>
275
- index === 0 || values[index] === values[index - 1] ||
276
- label !== labels[index - 1]
277
- );
278
- for (let precision = 4; !distinct() && precision <= 17; precision += 1) {
279
- labels = values.map(value => String(+value.toPrecision(precision)));
280
- }
281
- return distinct() ? labels : values.map(String);
282
- }
283
-
284
- export function stableDecimal(value, places = 12) {
285
- if (value === 0) return 0;
286
- const threshold = 10 ** -places;
287
- return Math.abs(value) < threshold
288
- ? Number(value.toPrecision(places))
289
- : Number(value.toFixed(places));
290
- }
1
+ const q=Object.freeze([1,2,3,5]);function h(t,e){if(!Array.isArray(t)||!t.every(Number.isFinite))throw new TypeError(`${e} must contain finite numbers.`)}function p(t){return new RangeError(`${t} is outside the finite numeric range.`)}function x(t,e=1){let n=0,r=0;for(const i of t){const o=i/e,c=n+o;r+=Math.abs(n)>=Math.abs(o)?n-c+o:o-c+n,n=c}return n+r}function f(t){let e=0;for(const n of t)e=Math.max(e,Math.abs(n));return e}function y(t,e="Numeric values"){h(t,e);const n=f(t)||1;return{scale:n,total:x(t,n)}}function b(t,e="Numeric result"){if(!Number.isFinite(t))throw p(e);return t===0?0:t}function N(t,e,n="Numeric result"){if(!Number.isFinite(t)||!Number.isFinite(e)||e<0)throw p(n);return b(t*e,n)}function d(t,e,n=1){h(t,e);const r=t.reduce((o,c)=>o+c,0);if(Number.isFinite(r))return r===0?0:r/n;const i=f(t)||1;return N(x(t,i)/n,i,e)}function $(t,e="Numeric sum"){return d(t,e)}function z(t,e="Numeric sum"){const n=y(t,e);let r=0,i=0,o=0;return t.map(c=>{r+=c;const u=c/n.scale,s=i+u;return o+=Math.abs(i)>=Math.abs(u)?i-s+u:u-s+i,i=s,Number.isFinite(r)?r===0?0:r:N(i+o,n.scale,e)})}function R(t,e="Numeric mean"){if(h(t,e),t.length===0)throw new RangeError(`${e} requires at least one value.`);return d(t,e,t.length)}function P(t,e="Numeric square sum"){h(t,e);const n=t.reduce((c,u)=>c+u**2,0);if(Number.isFinite(n))return n;const r=f(t);if(r===0)return 0;const i=t.reduce((c,u)=>c+(u/r)**2,0),o=N(Math.sqrt(i),r,e);return b(o**2,e)}function A(t,{sample:e=!1,divisor:n=1,label:r="Numeric deviation"}={}){if(!Number.isFinite(n)||n<=0)throw new RangeError(`${r} divisor must be positive and finite.`);const i=t.length-(e?1:0);if(i<=0)throw new RangeError(`${r} requires more values.`);const o=R(t,`${r} mean`),c=t.map(a=>a-o),u=c.reduce((a,M)=>a+M**2,0);if(Number.isFinite(u))return{mean:o,deviation:Math.sqrt(u/i)/n,squared:u};const s=c.every(Number.isFinite),m=s?f(c):Math.max(Math.abs(o),f(t));if(m===0)return{mean:o,deviation:0};const g=t.reduce((a,M,S)=>{const w=s?c[S]/m:M/m-o/m;return a+w**2},0),l=N(Math.sqrt(g/i)/n,m,r);return{mean:o,deviation:l}}function D(t,e){const n=(t.length-1)*e,r=Math.floor(n),i=Math.ceil(n);return F(t[r],t[i],n-r)}function I(t){let e=1/0,n=-1/0;for(const r of t)e=Math.min(e,r),n=Math.max(n,r);return[e,n]}function C(t,e){const n=Math.max(Math.abs(t),Math.abs(e));if(n===0)return{scale:1,start:0,end:0,span:0};const r=10**Math.floor(Math.log10(n)),i=Number.isFinite(r)&&r>0?r:n;return t/=i,e/=i,{scale:i,start:t,end:e,span:e-t}}function O(t,e=5,n=q){if(!Number.isFinite(t)||t<=0)throw new RangeError("Nice numeric span must be positive and finite.");const r=t/Math.max(1,e),i=10**Math.floor(Math.log10(r)),o=r/i;return(n.find(u=>u>=o)??10)*i}function T(t,e,n){let r=t/e;const i=Math.round(r);return Math.abs(r-i)<=1e-10&&(r=i),(n==="floor"?Math.floor(r):Math.ceil(r))*e}function E(t,e){if(!Number.isFinite(t)||t===0)return t===0?0:t;const n=Number(t.toPrecision(15));return Number.isFinite(e)&&e>0&&Math.abs(n-t)<=e*1e-10?n:t}function j(t,e,n){if(t===e)return 0;if(t===n)return 1;const r=n-e,i=t-e;if(Number.isFinite(r)&&r!==0&&Number.isFinite(i))return i/r;const o=Math.max(Math.abs(t),Math.abs(e),Math.abs(n));return(t/o-e/o)/(n/o-e/o)}function F(t,e,n){let r=n===0?t:n===1?e:void 0;if(r===void 0){const i=e-t;if(Number.isFinite(i))r=t+n*i;else{const o=Math.max(Math.abs(t),Math.abs(e));r=o*(t/o*(1-n)+e/o*n)}}return b(r,"Numeric interpolation")}function L(t,e){const n=t+e,r=Number.isFinite(n)?n/2:t/2+e/2;return b(r,"Numeric midpoint")}function V(t,e,n,r="Numeric range"){const i=Math.abs((e-t)/(n-1)),o=[],c=Math.sign(e-t);for(let u=0;u<n;u+=1){const s=E(F(t,e,u/(n-1)),i);if(!Number.isFinite(s)||c!==0&&u>0&&c*(s-o[u-1])<=0)throw new RangeError(`${r} cannot represent ${n} distinct samples.`);o.push(s)}return o}function _(t,e){let n=t.map(e??(i=>String(+i.toPrecision(3))));const r=()=>n.every((i,o)=>o===0||t[o]===t[o-1]||i!==n[o-1]);for(let i=4;!r()&&i<=17;i+=1)n=t.map(o=>String(+o.toPrecision(i)));return r()?n:t.map(String)}function k(t,e=12){if(t===0)return 0;const n=10**-e;return Math.abs(t)<n?Number(t.toPrecision(e)):Number(t.toFixed(e))}export{q as NICE_FACTORS,T as alignNumericStep,E as cleanNumericValue,L as finiteMidpoint,_ as formatDistinctNumericSamples,F as interpolateNumber,j as inverseLerp,f as maximumMagnitude,O as niceNumericStep,C as normalizeNumericRange,y as normalizedFiniteSum,I as numericExtent,D as quantileSorted,b as requireFiniteResult,N as restoreFiniteScale,V as sampleNumericRange,k as stableDecimal,A as stableFiniteDeviation,R as stableFiniteMean,z as stableFinitePrefixSums,P as stableFiniteSquareSum,$ as stableFiniteSum};
@@ -1,3 +1,4 @@
1
+ import { requireStringValue as nonEmptyString } from "../core/validation.js";
1
2
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
3
  import { validateUserId } from "../core/identifiers.js";
3
4
  import { validateOptionObject } from "../core/validation.js";
@@ -18,12 +19,6 @@ const DIMENSION_SCALE_OPTIONS = Object.freeze([
18
19
  "padding", "align", "unknown"
19
20
  ]);
20
21
 
21
- function nonEmptyString(value, label) {
22
- if (typeof value !== "string" || value.length === 0) {
23
- throw new TypeError(`${label} must be a non-empty string.`);
24
- }
25
- return value;
26
- }
27
22
 
28
23
  export function validateParallelKeyField(value) {
29
24
  return nonEmptyString(value, "Parallel key field");
@@ -0,0 +1,41 @@
1
+ import { normalizeTemporalValue } from "./scales/fields.js";
2
+
3
+ export const PARSE_TIME_FORMATS = Object.freeze([
4
+ "auto", "%Y-%m-%d", "%d/%m/%Y", "%m/%d/%Y"
5
+ ]);
6
+
7
+ export function validateParseTimeFormat(format = "auto") {
8
+ if (!PARSE_TIME_FORMATS.includes(format)) {
9
+ throw new Error(`Unsupported parseTime format "${format}".`);
10
+ }
11
+ return format;
12
+ }
13
+
14
+ // Explicit calendar formats are UTC and reject rollover, rather than guessing
15
+ // the day/month order. Invalid or nullish values remain missing data as null.
16
+ export function parseTimeValue(value, format = "auto") {
17
+ validateParseTimeFormat(format);
18
+ if (value === null || value === undefined) return null;
19
+ if (format === "auto") {
20
+ try { return normalizeTemporalValue(value); }
21
+ catch (error) {
22
+ if (error instanceof TypeError || error instanceof RangeError) return null;
23
+ throw error;
24
+ }
25
+ }
26
+ if (typeof value !== "string") return null;
27
+ const match = (format === "%Y-%m-%d"
28
+ ? /^(\d{4})-(\d{1,2})-(\d{1,2})$/
29
+ : /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/).exec(value.trim());
30
+ if (!match) return null;
31
+ const numbers = match.slice(1).map(Number);
32
+ const [year, month, day] = format === "%Y-%m-%d" ? numbers
33
+ : format === "%d/%m/%Y" ? [numbers[2], numbers[1], numbers[0]]
34
+ : [numbers[2], numbers[0], numbers[1]];
35
+ const date = new Date(0);
36
+ date.setUTCFullYear(year, month - 1, day);
37
+ date.setUTCHours(0, 0, 0, 0);
38
+ return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1
39
+ && date.getUTCDate() === day && Number.isFinite(date.getTime())
40
+ ? date.getTime() : null;
41
+ }
@@ -1,6 +1,12 @@
1
1
  import { freezeOwned, isPlainObject } from "../core/immutable.js";
2
2
  import { validateGeneratedItemLimit } from "../core/validation.js";
3
3
 
4
+ export const MAX_PATH_COMMANDS = 20_000;
5
+
6
+ export function validatePathCommandCount(count, label) {
7
+ return validateGeneratedItemLimit(count, label, MAX_PATH_COMMANDS);
8
+ }
9
+
4
10
  const COMMAND_KEYS = Object.freeze({
5
11
  M: Object.freeze(["op", "x", "y"]),
6
12
  L: Object.freeze(["op", "x", "y"]),
@@ -35,14 +41,15 @@ function validateCommand(command, index) {
35
41
  }
36
42
 
37
43
  export function validatePathCommands(commands) {
38
- if (!Array.isArray(commands) || commands.length < 2) {
39
- throw new TypeError("path.commands must contain at least two commands.");
44
+ if (!Array.isArray(commands) || commands.length < 1) {
45
+ throw new TypeError("path.commands must contain at least one command.");
40
46
  }
41
- validateGeneratedItemLimit(commands.length, "path.commands length");
47
+ validatePathCommandCount(commands.length, "path.commands length");
42
48
  commands.forEach(validateCommand);
43
49
  if (commands[0].op !== "M") {
44
50
  throw new Error("path.commands must start with M.");
45
51
  }
52
+ if (commands.length === 1) return commands;
46
53
  if (commands.slice(1).some(command => command.op === "M")) {
47
54
  throw new Error("path.commands supports one subpath and only one initial M.");
48
55
  }
@@ -79,7 +86,7 @@ export function buildLinearPathCommands(points, { close = false } = {}) {
79
86
  if (typeof close !== "boolean") {
80
87
  throw new TypeError("Linear path close must be a boolean.");
81
88
  }
82
- validateGeneratedItemLimit(
89
+ validatePathCommandCount(
83
90
  points.length + Number(close),
84
91
  "Linear path command count"
85
92
  );