ggaction 0.0.16 → 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 (299) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +3 -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 +10 -4
  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/bin.js +1 -1
  34. package/src/actions/data/box.js +2 -1
  35. package/src/actions/data/create.js +18 -5
  36. package/src/actions/data/density.js +1 -1
  37. package/src/actions/data/derived.js +15 -1
  38. package/src/actions/data/edit.js +20 -7
  39. package/src/actions/data/filter.js +15 -4
  40. package/src/actions/data/index.js +3 -0
  41. package/src/actions/data/interval.js +2 -0
  42. package/src/actions/data/regression.js +6 -2
  43. package/src/actions/data/revise.js +37 -5
  44. package/src/actions/data/shared.js +33 -4
  45. package/src/actions/data/sort.js +21 -0
  46. package/src/actions/data/stack.js +3 -1
  47. package/src/actions/data/summary.js +1 -1
  48. package/src/actions/encodings/angle.js +4 -1
  49. package/src/actions/encodings/appearance.js +43 -31
  50. package/src/actions/encodings/barWidth.js +36 -11
  51. package/src/actions/encodings/channels.js +0 -1
  52. package/src/actions/encodings/color/continuous.js +5 -2
  53. package/src/actions/encodings/color/index.js +5 -2
  54. package/src/actions/encodings/color/policy.js +10 -3
  55. package/src/actions/encodings/density/resolve.js +1 -6
  56. package/src/actions/encodings/offset.js +4 -1
  57. package/src/actions/encodings/position/policies/arc.js +12 -10
  58. package/src/actions/encodings/position/policies/area.js +3 -3
  59. package/src/actions/encodings/position/policies/bar.js +15 -6
  60. package/src/actions/encodings/position/policies/line.js +17 -6
  61. package/src/actions/encodings/position/resolve.js +9 -3
  62. package/src/actions/encodings/ranged.js +23 -1
  63. package/src/actions/encodings/remove.js +3 -2
  64. package/src/actions/encodings/ruleAppearance.js +4 -1
  65. package/src/actions/encodings/seriesLayout.js +1 -0
  66. package/src/actions/encodings/stroke.js +4 -1
  67. package/src/actions/encodings/strokeDash.js +6 -1
  68. package/src/actions/encodings/text.js +12 -4
  69. package/src/actions/errorBands/create.js +2 -2
  70. package/src/actions/errorBands/edit.js +2 -1
  71. package/src/actions/facets/actions.js +158 -240
  72. package/src/actions/gradientPlots/resolve.js +2 -17
  73. package/src/actions/guides/applicability.js +1 -1
  74. package/src/actions/guides/axes/axis.js +16 -4
  75. package/src/actions/guides/axes/edit.js +4 -4
  76. package/src/actions/guides/axes/labels.js +120 -46
  77. package/src/actions/guides/axes/lines.js +6 -5
  78. package/src/actions/guides/axes/parallel/lifecycle.js +1 -1
  79. package/src/actions/guides/axes/parallel/policy.js +8 -7
  80. package/src/actions/guides/axes/remove.js +3 -2
  81. package/src/actions/guides/axes/tickGroups.js +5 -2
  82. package/src/actions/guides/axes/ticks.js +6 -3
  83. package/src/actions/guides/axes/titles.js +71 -26
  84. package/src/actions/guides/basic.js +2 -0
  85. package/src/actions/guides/facadeLegend.js +10 -2
  86. package/src/actions/guides/grids/grid.js +7 -1
  87. package/src/actions/guides/grids/resolve.js +59 -12
  88. package/src/actions/guides/legends/blocks.js +3 -1
  89. package/src/actions/guides/legends/categorical/actions.js +70 -17
  90. package/src/actions/guides/legends/categorical/components.js +12 -8
  91. package/src/actions/guides/legends/categorical/index.js +6 -0
  92. package/src/actions/guides/legends/categorical/layout.js +37 -8
  93. package/src/actions/guides/legends/categorical/options.js +31 -20
  94. package/src/actions/guides/legends/categorical/resolve.js +2 -2
  95. package/src/actions/guides/legends/categorical/symbols.js +18 -12
  96. package/src/actions/guides/legends/categorical/validation.js +1 -6
  97. package/src/actions/guides/legends/continuous/colorSymbols.js +1 -0
  98. package/src/actions/guides/legends/continuous/common.js +1 -447
  99. package/src/actions/guides/legends/continuous/gradient.js +47 -34
  100. package/src/actions/guides/legends/continuous/interval.js +4 -4
  101. package/src/actions/guides/legends/continuous/opacity.js +19 -12
  102. package/src/actions/guides/legends/edit.js +1 -604
  103. package/src/actions/guides/legends/focused.js +2 -2
  104. package/src/actions/guides/legends/group.js +1 -0
  105. package/src/actions/guides/legends/lane.js +1 -516
  106. package/src/actions/guides/legends/remove.js +3 -0
  107. package/src/actions/guides/legends/sampling.js +1 -1
  108. package/src/actions/guides/legends/size.js +51 -43
  109. package/src/actions/guides/legends/strokeWidth.js +11 -29
  110. package/src/actions/guides/legends/target.js +3 -3
  111. package/src/actions/guides/legends/transition.js +21 -7
  112. package/src/actions/guides/polar/axes/labels.js +4 -2
  113. package/src/actions/guides/polar/axes/titles.js +7 -5
  114. package/src/actions/guides/polar/resolve.js +6 -14
  115. package/src/actions/index.js +16 -16
  116. package/src/actions/marks/arc/actions.js +3 -10
  117. package/src/actions/marks/area/actions.js +8 -5
  118. package/src/actions/marks/area/materialize.js +8 -9
  119. package/src/actions/marks/bar/create.js +17 -4
  120. package/src/actions/marks/bar/edit.js +18 -7
  121. package/src/actions/marks/bar/materialize.js +9 -6
  122. package/src/actions/marks/line/actions.js +11 -4
  123. package/src/actions/marks/line/materialize.js +9 -5
  124. package/src/actions/marks/point/create.js +8 -1
  125. package/src/actions/marks/point/edit.js +13 -6
  126. package/src/actions/marks/point/materialize.js +4 -2
  127. package/src/actions/marks/rect/actions.js +26 -11
  128. package/src/actions/marks/remove.js +2 -0
  129. package/src/actions/marks/rule/actions.js +31 -12
  130. package/src/actions/marks/text/actions.js +42 -11
  131. package/src/actions/marks/text/layout.js +3 -0
  132. package/src/actions/marks/tick/actions.js +23 -8
  133. package/src/actions/primitives/semanticAction.js +37 -3
  134. package/src/actions/primitives/semanticValidation/dataset.js +13 -1
  135. package/src/actions/primitives/semanticValidation/layer.js +26 -2
  136. package/src/actions/primitives/semanticValidation/scale.js +6 -0
  137. package/src/actions/regression/create.js +26 -5
  138. package/src/actions/regression/edit.js +50 -15
  139. package/src/actions/regression/follow.js +22 -0
  140. package/src/actions/regression/resolve.js +2 -6
  141. package/src/actions/scales/channels.js +20 -15
  142. package/src/actions/scales/consumers/common.js +2 -1
  143. package/src/actions/scales/consumers/families.js +16 -5
  144. package/src/actions/scales/create.js +10 -4
  145. package/src/actions/scales/definitions.js +1 -425
  146. package/src/actions/scales/edit.js +3 -3
  147. package/src/actions/scales/editPolicy.js +9 -3
  148. package/src/actions/scales/materialize.js +1 -1
  149. package/src/actions/scales/parallel.js +1 -1
  150. package/src/actions/scales/patch.js +1 -1
  151. package/src/actions/scales/preview.js +49 -2
  152. package/src/actions/scales/quantitativeColor.js +3 -3
  153. package/src/actions/selection/actions.js +1 -13
  154. package/src/actions/theme/reconcile.js +83 -27
  155. package/src/actions/theme/state.js +12 -0
  156. package/src/actions/titles/actions.js +5 -18
  157. package/src/actions/titles/resolve.js +41 -19
  158. package/src/actions/violinPlots/edit.js +3 -20
  159. package/src/core/action.js +3 -2
  160. package/src/core/compositionState.js +4 -1
  161. package/src/core/font.js +9 -2
  162. package/src/core/optionRequirements.js +1 -0
  163. package/src/core/programState.js +5 -1
  164. package/src/core/resourceReferences.js +3 -0
  165. package/src/core/textMetricProfile.js +3 -2
  166. package/src/core/textMetrics.js +41 -4
  167. package/src/core/validation.js +26 -0
  168. package/src/core/vocabulary.js +4 -0
  169. package/src/grammar/aggregate.js +4 -18
  170. package/src/grammar/arcs.js +1 -386
  171. package/src/grammar/areaPaint.js +10 -0
  172. package/src/grammar/areaSeries.js +18 -10
  173. package/src/grammar/bars/geometry.js +30 -3
  174. package/src/grammar/bars/policy.js +12 -1
  175. package/src/grammar/bin.js +41 -22
  176. package/src/grammar/bin2d.js +1 -6
  177. package/src/grammar/boxPlot.js +56 -9
  178. package/src/grammar/categoryOrder.js +1 -6
  179. package/src/grammar/complete.js +15 -19
  180. package/src/grammar/computed.js +92 -25
  181. package/src/grammar/curveCommands.js +29 -16
  182. package/src/grammar/datasetSchema.js +382 -0
  183. package/src/grammar/density.js +53 -5
  184. package/src/grammar/displayLabels.js +28 -0
  185. package/src/grammar/ecdf.js +2 -9
  186. package/src/grammar/facets/dependencies.js +6 -2
  187. package/src/grammar/facets/guides.js +5 -0
  188. package/src/grammar/facets/index.js +2 -5
  189. package/src/grammar/filter.js +133 -39
  190. package/src/grammar/fold.js +2 -12
  191. package/src/grammar/gradientProfile.js +1 -6
  192. package/src/grammar/impute.js +21 -48
  193. package/src/grammar/interval.js +34 -10
  194. package/src/grammar/itemMissing.js +59 -0
  195. package/src/grammar/legendGradient.js +18 -0
  196. package/src/grammar/legendGroups.js +1 -0
  197. package/src/grammar/legendTitleStyle.js +22 -0
  198. package/src/grammar/lineSeries.js +32 -17
  199. package/src/grammar/markLabels.js +5 -2
  200. package/src/grammar/normalize.js +4 -54
  201. package/src/grammar/numeric.js +1 -290
  202. package/src/grammar/parallelCoordinates.js +1 -6
  203. package/src/grammar/parseTime.js +41 -0
  204. package/src/grammar/pathCommands.js +11 -4
  205. package/src/grammar/polar.js +1 -184
  206. package/src/grammar/positionCompatibility.js +3 -3
  207. package/src/grammar/regression/derive.js +63 -16
  208. package/src/grammar/regression/index.js +1 -0
  209. package/src/grammar/regression/models.js +116 -59
  210. package/src/grammar/regression/parameters.js +69 -6
  211. package/src/grammar/roundedRect.js +42 -54
  212. package/src/grammar/scales/appearance.js +1 -206
  213. package/src/grammar/scales/color.js +1 -382
  214. package/src/grammar/scales/colorConsumers.js +5 -2
  215. package/src/grammar/scales/definition.js +1 -190
  216. package/src/grammar/scales/discretized.js +1 -195
  217. package/src/grammar/scales/size.js +1 -456
  218. package/src/grammar/scales/temporal.js +2 -13
  219. package/src/grammar/scales/transformed.js +1 -383
  220. package/src/grammar/scales/types.js +1 -1
  221. package/src/grammar/scales/validation.js +1 -66
  222. package/src/grammar/schemas/concreteGraphic.js +11 -0
  223. package/src/grammar/schemas/graphic.js +3 -0
  224. package/src/grammar/schemas/semanticPath.js +5 -3
  225. package/src/grammar/sort.js +88 -0
  226. package/src/grammar/stack.js +2 -10
  227. package/src/grammar/statisticalReference.js +1 -6
  228. package/src/grammar/summary.js +120 -45
  229. package/src/grammar/text.js +19 -0
  230. package/src/grammar/timeUnit.js +16 -7
  231. package/src/grammar/transformKeys.js +47 -0
  232. package/src/grammar/transformTopology.js +1 -0
  233. package/src/grammar/transforms.js +25 -12
  234. package/src/grammar/valueFormat.js +17 -5
  235. package/src/grammar/weightedStatistics.js +1 -6
  236. package/src/grammar/window.js +4 -58
  237. package/src/inspection.js +495 -0
  238. package/src/inspectionDescriptors.js +3 -0
  239. package/src/layout/canvas.js +1 -110
  240. package/src/layout/facets.js +1 -244
  241. package/src/layout/labels.js +1 -0
  242. package/src/layout/legend.js +13 -0
  243. package/src/layout/legendGroup.js +1 -0
  244. package/src/layout/legendItems.js +59 -15
  245. package/src/layout/legendLane.js +1 -441
  246. package/src/layout/legendText.js +1 -0
  247. package/src/layout/title.js +35 -13
  248. package/src/materialization/bars/aggregate.js +11 -5
  249. package/src/materialization/bars/histogram.js +4 -1
  250. package/src/materialization/bars/ranged.js +19 -6
  251. package/src/materialization/bars/resolve.js +26 -3
  252. package/src/materialization/coordinateBounds.js +8 -0
  253. package/src/materialization/dependencies.js +4 -44
  254. package/src/materialization/facetGuides/layout.js +1 -0
  255. package/src/materialization/facetGuides/placement.js +19 -8
  256. package/src/materialization/facetGuides/preparation.js +15 -7
  257. package/src/materialization/facetHeaders.js +1 -347
  258. package/src/materialization/facets.js +1 -185
  259. package/src/materialization/graphicHierarchy.js +1 -1
  260. package/src/materialization/guides/layout.js +1 -57
  261. package/src/materialization/guides/resources.js +1 -123
  262. package/src/materialization/layout.js +53 -0
  263. package/src/materialization/legendGroups.js +1 -0
  264. package/src/materialization/legendText.js +1 -0
  265. package/src/materialization/legends.js +1 -1
  266. package/src/materialization/marks/capabilities.js +5 -6
  267. package/src/materialization/rect.js +1 -1
  268. package/src/materialization/revisionIdentity.js +19 -0
  269. package/src/materialization/scaleGuideDependencies.js +1 -1
  270. package/src/materialization/scales/map.js +3 -0
  271. package/src/materialization/scales/policies/arc.js +8 -7
  272. package/src/materialization/scales/policies/bar.js +19 -7
  273. package/src/materialization/scales/policies/series.js +2 -1
  274. package/src/materialization/scales/resolve.js +72 -7
  275. package/src/materialization/selection/items/arc.js +1 -1
  276. package/src/materialization/selection/items/bar.js +7 -6
  277. package/src/materialization/selection/items/path.js +7 -6
  278. package/src/materialization/selection/items/rect.js +2 -2
  279. package/src/materialization/text.js +17 -6
  280. package/src/materialization/textLines.js +15 -0
  281. package/src/persistence/codec.js +37 -6
  282. package/src/persistence/validation.js +47 -0
  283. package/src/persistence.js +94 -8
  284. package/src/renderers/canvas/circle.js +6 -3
  285. package/src/renderers/canvas/fill.js +7 -1
  286. package/src/renderers/canvas/index.js +2 -1
  287. package/src/renderers/canvas/line.js +1 -0
  288. package/src/renderers/canvas/path.js +1 -1
  289. package/src/renderers/canvas/rect.js +11 -8
  290. package/src/renderers/canvas/stroke.js +2 -1
  291. package/src/renderers/canvas/text.js +5 -3
  292. package/src/renderers/svg.js +4 -1
  293. package/src/selectors/layers.js +19 -0
  294. package/src/theme/defaults.js +8 -2
  295. package/src/version.js +1 -1
  296. package/types/basic.d.ts +1 -0
  297. package/types/index.d.ts +7 -0
  298. package/types/inspection.d.ts +17 -0
  299. package/types/program.d.ts +494 -112
@@ -0,0 +1,88 @@
1
+ import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
+ import { normalizeTemporalValue } from "./scales/fields.js";
3
+
4
+ const ORDERS = new Set(["ascending", "descending"]);
5
+ const NULLS = new Set(["first", "last"]);
6
+ const TEMPORAL_UNITS = new Set(["year", "timestamp"]);
7
+
8
+ export function normalizeSortTransform({ sortBy }) {
9
+ if (!Array.isArray(sortBy) || sortBy.length === 0 ||
10
+ Array.from({ length: sortBy.length }, (_, index) => index).some(index => !Object.hasOwn(sortBy, index))) {
11
+ throw new TypeError("Sorted data sortBy must be a non-empty dense array.");
12
+ }
13
+ const keys = sortBy.map((key, index) => {
14
+ if (!isPlainObject(key)) throw new TypeError(`Sort key ${index} must be a plain object.`);
15
+ const unknown = Object.keys(key).find(name => !["field", "order", "nulls", "temporalUnit"].includes(name));
16
+ if (unknown !== undefined) throw new Error(`Unknown sort key property "${unknown}".`);
17
+ if (typeof key.field !== "string" || key.field.length === 0) {
18
+ throw new TypeError(`Sort key ${index} field must be a non-empty string.`);
19
+ }
20
+ const order = key.order ?? "ascending";
21
+ const nulls = key.nulls ?? "last";
22
+ if (!ORDERS.has(order)) throw new Error(`Unsupported sort order "${order}".`);
23
+ if (!NULLS.has(nulls)) throw new Error(`Unsupported sort null placement "${nulls}".`);
24
+ if (key.temporalUnit !== undefined && !TEMPORAL_UNITS.has(key.temporalUnit)) {
25
+ throw new Error(`Unsupported sort temporalUnit "${key.temporalUnit}".`);
26
+ }
27
+ return { field: key.field, order, nulls, ...(key.temporalUnit === undefined ? {} : { temporalUnit: key.temporalUnit }) };
28
+ });
29
+ if (new Set(keys.map(key => key.field)).size !== keys.length) {
30
+ throw new Error("Sorted data sortBy fields must be unique.");
31
+ }
32
+ return cloneAndFreeze({ type: "sort", sortBy: keys });
33
+ }
34
+
35
+ export function validateSortTransform(transform) {
36
+ if (!isPlainObject(transform)) throw new TypeError("Sort transform must be a plain object.");
37
+ const unknown = Object.keys(transform).find(key => !["type", "sortBy"].includes(key));
38
+ if (unknown !== undefined) throw new Error(`Unknown sort transform property "${unknown}".`);
39
+ if (transform.type !== "sort") throw new Error('Sort transform type must be "sort".');
40
+ normalizeSortTransform(transform);
41
+ }
42
+
43
+ function compareValues(left, right) {
44
+ if (left === right) return 0;
45
+ return left < right ? -1 : 1;
46
+ }
47
+
48
+ function normalizedKey(value, key, rowIndex) {
49
+ if (value === null || value === undefined) return { missing: true };
50
+ if (key.temporalUnit !== undefined) {
51
+ return { missing: false, value: normalizeTemporalValue(value, key.field, rowIndex, key.temporalUnit) };
52
+ }
53
+ if (!["number", "string", "boolean"].includes(typeof value) ||
54
+ (typeof value === "number" && !Number.isFinite(value))) {
55
+ throw new TypeError(`Sort field "${key.field}" requires finite number, string, boolean, or missing values at row ${rowIndex}.`);
56
+ }
57
+ return { missing: false, value, type: typeof value };
58
+ }
59
+
60
+ export function deriveSortedRows(values, transform) {
61
+ if (!Array.isArray(values)) throw new TypeError("Sort source values must be an array.");
62
+ validateSortTransform(transform);
63
+ const decorated = values.map((row, index) => ({
64
+ row,
65
+ index,
66
+ keys: transform.sortBy.map(key => normalizedKey(row[key.field], key, index))
67
+ }));
68
+ for (let keyIndex = 0; keyIndex < transform.sortBy.length; keyIndex += 1) {
69
+ const key = transform.sortBy[keyIndex];
70
+ const types = new Set(decorated.map(entry => entry.keys[keyIndex]).filter(value => !value.missing).map(value => value.type ?? "number"));
71
+ if (types.size > 1) throw new TypeError(`Sort field "${key.field}" contains mixed non-missing types.`);
72
+ }
73
+ decorated.sort((left, right) => {
74
+ for (let index = 0; index < transform.sortBy.length; index += 1) {
75
+ const key = transform.sortBy[index];
76
+ const a = left.keys[index];
77
+ const b = right.keys[index];
78
+ if (a.missing || b.missing) {
79
+ if (a.missing !== b.missing) return a.missing === (key.nulls === "first") ? -1 : 1;
80
+ continue;
81
+ }
82
+ const compared = compareValues(a.value, b.value);
83
+ if (compared !== 0) return key.order === "ascending" ? compared : -compared;
84
+ }
85
+ return left.index - right.index;
86
+ });
87
+ return decorated.map(entry => entry.row);
88
+ }
@@ -1,3 +1,5 @@
1
+ import { rejectUnknownProperties as rejectUnknownKeys } from "../core/validation.js";
2
+ import { requireStringValue as requireField } from "../core/validation.js";
1
3
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
4
  import { layoutSeriesPartition } from "./seriesLayout.js";
3
5
 
@@ -8,17 +10,7 @@ const AS_KEYS = Object.freeze(["start", "end", "value", "share"]);
8
10
  const MODES = new Set(["stack", "fill", "center", "diverging"]);
9
11
  const MAX_OUTPUT_ROWS = 10_000;
10
12
 
11
- function rejectUnknownKeys(value, supported, label) {
12
- const unknown = Object.keys(value).find(key => !supported.includes(key));
13
- if (unknown !== undefined) throw new Error(`Unknown ${label} property "${unknown}".`);
14
- }
15
13
 
16
- function requireField(value, label) {
17
- if (typeof value !== "string" || value.length === 0) {
18
- throw new TypeError(`${label} must be a non-empty string.`);
19
- }
20
- return value;
21
- }
22
14
 
23
15
  function normalizeAs(value, field) {
24
16
  const requested = value ?? {};
@@ -1,3 +1,4 @@
1
+ import { rejectUnknownProperties as rejectUnknownKeys } from "../core/validation.js";
1
2
  import { aggregateRows } from "./aggregate.js";
2
3
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
3
4
 
@@ -6,12 +7,6 @@ const SIMPLE_STATISTICS = new Set(["mean", "median", "min", "max"]);
6
7
  const POPULATIONS = new Set(["boundData", "visibleItems"]);
7
8
  const TRANSFORM_KEYS = Object.freeze(["type", "target"]);
8
9
 
9
- function rejectUnknownKeys(value, supported, label) {
10
- const unknown = Object.keys(value).find(key => !supported.includes(key));
11
- if (unknown !== undefined) {
12
- throw new Error(`Unknown ${label} property "${unknown}".`);
13
- }
14
- }
15
10
 
16
11
  export function normalizeReferenceStatistic(value, label = "Reference statistic") {
17
12
  if (!isPlainObject(value)) {
@@ -1,3 +1,6 @@
1
+ import { scalarKey } 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
  aggregateRows,
@@ -16,26 +19,12 @@ import {
16
19
  } from "./weightedStatistics.js";
17
20
 
18
21
  const TRANSFORM_KEYS = Object.freeze([
19
- "type", "groupBy", "aggregates", "members", "weight"
22
+ "type", "groupBy", "aggregates", "members", "weight", "missing", "empty"
20
23
  ]);
21
24
  const AGGREGATE_KEYS = Object.freeze(["op", "field", "as"]);
22
25
  const NOMINAL_OPERATIONS = new Set(["distinct", "valid", "missing"]);
23
26
  const MAX_OUTPUT_ROWS = 10_000;
24
27
 
25
- function rejectUnknownKeys(value, supported, label) {
26
- const unknown = Object.keys(value).find(key => !supported.includes(key));
27
- if (unknown !== undefined) {
28
- throw new Error(`Unknown ${label} property "${unknown}".`);
29
- }
30
- }
31
-
32
- function requireField(value, label) {
33
- if (typeof value !== "string" || value.length === 0) {
34
- throw new TypeError(`${label} must be a non-empty string.`);
35
- }
36
- return value;
37
- }
38
-
39
28
  function normalizeGroupBy(value) {
40
29
  if (value === undefined) return [];
41
30
  return Array.isArray(value) ? [...value] : [value];
@@ -51,7 +40,7 @@ function normalizeAggregate(value) {
51
40
  };
52
41
  }
53
42
 
54
- export function normalizeSummaryTransform({ groupBy, aggregates, members, weight } = {}) {
43
+ export function normalizeSummaryTransform({ groupBy, aggregates, members, weight, missing, empty } = {}) {
55
44
  const transform = {
56
45
  type: "summary",
57
46
  groupBy: normalizeGroupBy(groupBy),
@@ -61,7 +50,9 @@ export function normalizeSummaryTransform({ groupBy, aggregates, members, weight
61
50
  ...(members === undefined ? {} : { members }),
62
51
  ...(weight === undefined
63
52
  ? {}
64
- : { weight: normalizeStatisticalWeight(weight, "Summary weight") })
53
+ : { weight: normalizeStatisticalWeight(weight, "Summary weight") }),
54
+ ...(missing === undefined ? {} : { missing }),
55
+ ...(empty === undefined ? {} : { empty })
65
56
  };
66
57
  validateSummaryTransform(transform);
67
58
  return cloneAndFreeze(transform);
@@ -120,21 +111,15 @@ export function validateSummaryTransform(transform) {
120
111
  if (transform.weight !== undefined) {
121
112
  normalizeStatisticalWeight(transform.weight, "Summary weight");
122
113
  }
114
+ if (transform.missing !== undefined && !["error", "drop"].includes(transform.missing)) {
115
+ throw new Error('Summary missing must be "error" or "drop".');
116
+ }
117
+ if (transform.empty !== undefined && !["null", "identity"].includes(transform.empty)) {
118
+ throw new Error('Summary empty must be "null" or "identity".');
119
+ }
123
120
  return transform;
124
121
  }
125
122
 
126
- function scalarKey(value, label) {
127
- if (value === null) return "null";
128
- if (typeof value === "string") return `string:${value.length}:${value}`;
129
- if (typeof value === "boolean") return `boolean:${value}`;
130
- if (typeof value === "number" && Number.isFinite(value)) {
131
- return `number:${Object.is(value, -0) ? 0 : value}`;
132
- }
133
- throw new TypeError(
134
- `${label} must contain null, strings, booleans, or finite numbers.`
135
- );
136
- }
137
-
138
123
  function requireSourceFields(rows, transform) {
139
124
  if (rows.length === 0) return;
140
125
  const fields = new Set(rows.flatMap(row => Object.keys(row)));
@@ -169,12 +154,13 @@ function validateAggregateValues(rows, aggregate) {
169
154
  export function deriveSummaryRows(rows, transform) {
170
155
  validateSummaryTransform(transform);
171
156
  requireSourceFields(rows, transform);
157
+ const explicitWeighted = transform.weight !== undefined && transform.missing !== undefined;
172
158
  let weightEntries;
173
159
  if (transform.weight === undefined) {
174
160
  for (const aggregate of transform.aggregates) {
175
161
  validateAggregateValues(rows, aggregate);
176
162
  }
177
- } else {
163
+ } else if (!explicitWeighted) {
178
164
  weightEntries = readStatisticalWeights(rows, transform.weight, "Summary").entries;
179
165
  validateWeightedNumericFields(
180
166
  weightEntries,
@@ -183,6 +169,37 @@ export function deriveSummaryRows(rows, transform) {
183
169
  ))],
184
170
  "Summary"
185
171
  );
172
+ } else {
173
+ // Explicit missing policy is evaluated per measure. Validate every
174
+ // nonmissing value now, while leaving nullish value/weight pairs for the
175
+ // reason-priority accounting below.
176
+ readStatisticalWeights(
177
+ rows.filter(row => row[transform.weight.field] !== null && row[transform.weight.field] !== undefined),
178
+ transform.weight,
179
+ "Summary"
180
+ );
181
+ for (const aggregate of transform.aggregates) {
182
+ if (aggregate.field === undefined) continue;
183
+ validateAggregateValues(
184
+ rows.filter(row => row[aggregate.field] !== null && row[aggregate.field] !== undefined),
185
+ aggregate
186
+ );
187
+ }
188
+ }
189
+ if (transform.missing === "error") {
190
+ if (transform.weight !== undefined) {
191
+ const weightIndex = rows.findIndex(row =>
192
+ row[transform.weight.field] === null || row[transform.weight.field] === undefined
193
+ );
194
+ if (weightIndex !== -1) {
195
+ throw new TypeError(`Summary weight "${transform.weight.field}" is missing at row ${weightIndex}.`);
196
+ }
197
+ }
198
+ for (const aggregate of transform.aggregates) {
199
+ if (aggregate.field === undefined) continue;
200
+ const index = rows.findIndex(row => row[aggregate.field] === null || row[aggregate.field] === undefined);
201
+ if (index !== -1) throw new TypeError(`Summary field "${aggregate.field}" is missing at row ${index}.`);
202
+ }
186
203
  }
187
204
 
188
205
  const groups = new Map();
@@ -212,34 +229,92 @@ export function deriveSummaryRows(rows, transform) {
212
229
  throw new RangeError(`Summary output cannot exceed ${MAX_OUTPUT_ROWS} groups.`);
213
230
  }
214
231
 
215
- return [...groups.values()].map(group => {
216
- const weightSummary = transform.weight === undefined
232
+ const units = [];
233
+ const values = [...groups.values()].map(group => {
234
+ const sharedWeightSummary = transform.weight === undefined || explicitWeighted
217
235
  ? undefined
218
236
  : summarizeStatisticalWeights(
219
237
  group.entries,
220
238
  transform.weight.kind,
221
239
  "Summary group"
222
240
  );
241
+ const aggregated = Object.fromEntries(transform.aggregates.map(aggregate => {
242
+ let missingValue = 0;
243
+ let missingWeight = 0;
244
+ let zeroWeightRows = 0;
245
+ let measureWeightSummary = sharedWeightSummary;
246
+ if (explicitWeighted) {
247
+ const eligible = group.rows.filter(row => {
248
+ if (aggregate.field !== undefined &&
249
+ (row[aggregate.field] === null || row[aggregate.field] === undefined)) {
250
+ missingValue += 1;
251
+ return false;
252
+ }
253
+ if (row[transform.weight.field] === null || row[transform.weight.field] === undefined) {
254
+ missingWeight += 1;
255
+ return false;
256
+ }
257
+ return true;
258
+ });
259
+ const entries = readStatisticalWeights(eligible, transform.weight, "Summary").entries;
260
+ zeroWeightRows = entries.filter(entry => entry.weight === 0).length;
261
+ measureWeightSummary = summarizeStatisticalWeights(
262
+ entries,
263
+ transform.weight.kind,
264
+ "Summary group"
265
+ );
266
+ }
267
+ let value = measureWeightSummary === undefined
268
+ ? aggregateRows(group.rows, aggregate.field ?? "__row", aggregate.op)
269
+ : calculateWeightedAggregate(
270
+ measureWeightSummary,
271
+ aggregate.field,
272
+ aggregate.op,
273
+ `Summary aggregate "${aggregate.as}"`
274
+ );
275
+ if (value === undefined && transform.empty !== undefined) {
276
+ const op = typeof aggregate.op === "string" ? aggregate.op : aggregate.op.op;
277
+ value = transform.empty === "identity" && op === "sum" ? 0 : null;
278
+ }
279
+ if (transform.missing !== undefined || transform.empty !== undefined) {
280
+ const missing = explicitWeighted
281
+ ? missingValue + missingWeight
282
+ : aggregate.field === undefined ? 0 : group.rows.filter(row => row[aggregate.field] === null || row[aggregate.field] === undefined).length;
283
+ units.push({
284
+ role: aggregate.as,
285
+ group: group.values,
286
+ inputRows: group.rows.length,
287
+ usedRows: group.rows.length - missing,
288
+ excludedRows: missing,
289
+ excludedByReason: missing === 0 ? {} : {
290
+ ...(missingValue === 0 && explicitWeighted ? {} : { "missing-value": explicitWeighted ? missingValue : missing }),
291
+ ...(missingWeight === 0 ? {} : { "missing-weight": missingWeight })
292
+ },
293
+ ...(explicitWeighted ? { zeroWeightRows } : {})
294
+ });
295
+ }
296
+ return [aggregate.as, value];
297
+ }));
223
298
  return {
224
299
  ...group.values,
225
- ...Object.fromEntries(transform.aggregates.map(aggregate => [
226
- aggregate.as,
227
- weightSummary === undefined
228
- ? aggregateRows(group.rows, aggregate.field ?? "__row", aggregate.op)
229
- : calculateWeightedAggregate(
230
- weightSummary,
231
- aggregate.field,
232
- aggregate.op,
233
- `Summary aggregate "${aggregate.as}"`
234
- )
235
- ])),
300
+ ...aggregated,
236
301
  ...(transform.members === undefined
237
302
  ? {}
238
303
  : {
239
- [transform.members]: weightSummary === undefined
304
+ [transform.members]: sharedWeightSummary === undefined
240
305
  ? group.rows
241
- : weightedRows(weightSummary)
306
+ : weightedRows(sharedWeightSummary)
242
307
  })
243
308
  };
244
309
  });
310
+ if (transform.missing === undefined && transform.empty === undefined) return values;
311
+ return {
312
+ values,
313
+ report: cloneAndFreeze({
314
+ version: 1,
315
+ owner: { kind: "data", id: "pending" },
316
+ inputs: [],
317
+ units
318
+ })
319
+ };
245
320
  }
@@ -11,6 +11,11 @@ export function isSourceOwnedText(layer) {
11
11
  return layer?.mark?.type === "text" && layer.source !== undefined;
12
12
  }
13
13
 
14
+ export const TEXT_MARK_STYLE_OPTIONS = Object.freeze([
15
+ "fill", "opacity", "fontSize", "fontFamily", "fontWeight",
16
+ "align", "baseline", "rotation", "dx", "dy", "inheritColor", "lineHeight", "blockAlign"
17
+ ]);
18
+
14
19
  export const DEFAULT_TEXT_MARK = cloneAndFreeze({
15
20
  fill: "#334155",
16
21
  opacity: 1,
@@ -49,6 +54,18 @@ export function formatTextValue(value, format = "auto") {
49
54
 
50
55
  export function normalizeTextMarkConfig(options, base = DEFAULT_TEXT_MARK) {
51
56
  const config = { ...base };
57
+ if (Object.hasOwn(options, "lineHeight")) {
58
+ if (!Number.isFinite(options.lineHeight) || options.lineHeight <= 0) throw new RangeError("Text lineHeight must be positive and finite.");
59
+ config.lineHeight = options.lineHeight;
60
+ }
61
+ if (Object.hasOwn(options, "blockAlign")) {
62
+ if (!["first", "middle"].includes(options.blockAlign)) throw new Error("Text blockAlign must be first or middle.");
63
+ config.blockAlign = options.blockAlign;
64
+ }
65
+ if (Object.hasOwn(options, "inheritColor")) {
66
+ if (![false, "fill", "stroke"].includes(options.inheritColor)) throw new Error("Text inheritColor must be fill, stroke, or false.");
67
+ config.inheritColor = options.inheritColor;
68
+ }
52
69
  const mapping = {
53
70
  fill: "fill",
54
71
  opacity: "opacity",
@@ -75,3 +92,5 @@ export function normalizeTextMarkConfig(options, base = DEFAULT_TEXT_MARK) {
75
92
  }
76
93
  return cloneAndFreeze(config);
77
94
  }
95
+
96
+ export { resolveTextLines } from "../core/textMetrics.js";
@@ -1,3 +1,4 @@
1
+ import { requireStringValue as requireField } from "../core/validation.js";
1
2
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
3
  import { normalizeTemporalValue, validateTemporalUnit } from "./scales/fields.js";
3
4
 
@@ -75,12 +76,6 @@ function validateTimeZone(timeZone) {
75
76
  return timeZone;
76
77
  }
77
78
 
78
- function requireField(value, label) {
79
- if (typeof value !== "string" || value.length === 0) {
80
- throw new TypeError(`${label} must be a non-empty string.`);
81
- }
82
- return value;
83
- }
84
79
 
85
80
  function rejectUnknownKeys(value) {
86
81
  const unknown = Object.keys(value).find(key => !TRANSFORM_KEYS.includes(key));
@@ -127,7 +122,8 @@ export function validateTimeUnitTransform(transform) {
127
122
  }
128
123
 
129
124
  export function normalizeTimeUnitTransform(args = {}) {
130
- const { field, unit, as, temporalUnit, timeZone } = args;
125
+ const { field, unit, temporalUnit, timeZone } = args;
126
+ const as = args.as === undefined ? `${field}_${unit}` : args.as;
131
127
  const transform = { type: "timeUnit", field, unit, as,
132
128
  ...(temporalUnit === undefined ? {} : { temporalUnit }),
133
129
  ...(timeZone === undefined ? {} : { timeZone }) };
@@ -337,3 +333,16 @@ export function deriveTimeUnitRows(rows, transform) {
337
333
  });
338
334
  return cloneAndFreeze(values);
339
335
  }
336
+
337
+ export function offsetUtcTimeUnit(timestamp, unit) {
338
+ const date = new Date(timestamp);
339
+ if (unit === "year") return utcTimestamp(date.getUTCFullYear() + 1);
340
+ if (unit === "quarter" || unit === "month") {
341
+ return utcTimestamp(date.getUTCFullYear(), date.getUTCMonth() + (unit === "quarter" ? 3 : 1));
342
+ }
343
+ if (unit === "day") return timestamp + 24 * 60 * 60 * 1000;
344
+ if (unit === "hour") return timestamp + 60 * 60 * 1000;
345
+ if (unit === "minute") return timestamp + 60 * 1000;
346
+ return timestamp + 1000;
347
+ }
348
+
@@ -0,0 +1,47 @@
1
+ import { isPlainObject } from "../core/immutable.js";
2
+ import { requireStringValue, rejectUnknownProperties } from "../core/validation.js";
3
+
4
+ export function scalarKey(value, label) {
5
+ if (value === null) return "null";
6
+ if (typeof value === "string") return `string:${value.length}:${value}`;
7
+ if (typeof value === "boolean") return `boolean:${value}`;
8
+ if (typeof value === "number" && Number.isFinite(value)) {
9
+ return `number:${Object.is(value, -0) ? 0 : value}`;
10
+ }
11
+ throw new TypeError(
12
+ `${label} must contain null, strings, booleans, or finite numbers.`
13
+ );
14
+ }
15
+
16
+ export function normalizeSortBy(value) {
17
+ if (value === undefined) return [];
18
+ if (!Array.isArray(value)) return value;
19
+ return value.map(sort => isPlainObject(sort)
20
+ ? { ...sort, order: sort.order ?? "ascending" }
21
+ : sort
22
+ );
23
+ }
24
+
25
+ export function validateSortBy(sortBy, label) {
26
+ const kind = label.toLowerCase();
27
+ if (!Array.isArray(sortBy)) throw new TypeError(`${label} sortBy must be an array.`);
28
+ const fields = [];
29
+ sortBy.forEach((sort, index) => {
30
+ if (!isPlainObject(sort)) {
31
+ throw new TypeError(`${label} sortBy[${index}] must be a plain object.`);
32
+ }
33
+ rejectUnknownProperties(sort, ["field", "order"], `${kind} sortBy[${index}]`);
34
+ fields.push(requireStringValue(sort.field, `${label} sortBy[${index}].field`));
35
+ if (!["ascending", "descending"].includes(sort.order)) {
36
+ throw new Error(`Unsupported ${kind} sort order "${sort.order}".`);
37
+ }
38
+ });
39
+ if (new Set(fields).size !== fields.length) {
40
+ throw new Error(`${label} sortBy fields must be unique.`);
41
+ }
42
+ }
43
+
44
+ export function isGroupValue(value) {
45
+ return typeof value === "string" || typeof value === "boolean" ||
46
+ (typeof value === "number" && Number.isFinite(value));
47
+ }
@@ -16,6 +16,7 @@ const TRANSFORM_TOPOLOGY = Object.freeze({
16
16
  markFilter: Object.freeze({ provenanceTransparent: true }),
17
17
  normalize: Object.freeze({ facetTopology: "statistical" }),
18
18
  regression: Object.freeze({ facetTopology: "statistical" }),
19
+ sort: Object.freeze({ facetTopology: "statistical" }),
19
20
  summary: Object.freeze({ facetTopology: "statistical" }),
20
21
  stack: Object.freeze({ facetTopology: "statistical" }),
21
22
  statisticalReference: Object.freeze({ facetTopology: "statistical" }),
@@ -23,6 +23,7 @@ import {
23
23
  validateCompleteTransform
24
24
  } from "./complete.js";
25
25
  import {
26
+ normalizeFilterTransformEdit,
26
27
  normalizeFilterTransform,
27
28
  validateFilterTransform
28
29
  } from "./filter.js";
@@ -62,6 +63,7 @@ import {
62
63
  validateSummaryTransform
63
64
  } from "./summary.js";
64
65
  import { normalizeStackTransform, validateStackTransform } from "./stack.js";
66
+ import { normalizeSortTransform, validateSortTransform } from "./sort.js";
65
67
  import { validateStatisticalReferenceTransform } from
66
68
  "./statisticalReference.js";
67
69
  import { findTransformTopology } from "./transformTopology.js";
@@ -102,15 +104,6 @@ function normalizeSimple(normalize) {
102
104
  return (transform, patch) => normalize(mergedOptions(transform, patch));
103
105
  }
104
106
 
105
- function normalizeFilterEdit(transform, patch) {
106
- const base = replaceExclusive(
107
- requestedOptions(transform),
108
- patch,
109
- ["oneOf", "predicate", "range"]
110
- );
111
- return normalizeFilterTransform({ ...base, ...patch });
112
- }
113
-
114
107
  function normalizeBinEdit(transform, patch) {
115
108
  const prior = requestedOptions(transform);
116
109
  const base = replaceExclusive(
@@ -202,17 +195,28 @@ function normalizeNormalizeEdit(transform, patch) {
202
195
 
203
196
  function normalizeRegressionEdit(transform, patch) {
204
197
  const base = { ...requestedOptions(transform) };
198
+ if (patch.predict === false) delete base.predict;
205
199
  const method = patch.method ?? base.method;
206
200
  if (method !== base.method) {
207
201
  if (method !== "polynomial") delete base.degree;
208
- if (method !== "loess") delete base.span;
202
+ if (method !== "loess") {
203
+ delete base.span;
204
+ delete base.robustIterations;
205
+ }
209
206
  if (method === "loess" || base.method === "loess") {
210
207
  for (const key of [
211
208
  "confidenceMethod", "level", "confidence", "interval"
212
209
  ]) delete base[key];
213
210
  }
214
211
  }
215
- return normalizeRegressionTransform({ ...base, ...patch, method });
212
+ if (patch.interval === false) {
213
+ for (const key of ["confidenceMethod", "level", "confidence"]) delete base[key];
214
+ }
215
+ return normalizeRegressionTransform({
216
+ ...base,
217
+ ...Object.fromEntries(Object.entries(patch).filter(([key, value]) => !(key === "predict" && value === false))),
218
+ method
219
+ });
216
220
  }
217
221
 
218
222
  function normalizeTimeUnitEdit(transform, patch) {
@@ -373,7 +377,7 @@ const TRANSFORM_POLICIES = Object.freeze({
373
377
  materializeOp: "materializeFilteredData",
374
378
  replayTransform: requestedTransform,
375
379
  editable: true,
376
- normalizeEdit: normalizeFilterEdit,
380
+ normalizeEdit: normalizeFilterTransformEdit,
377
381
  outputRoles: () => ({})
378
382
  }),
379
383
  fold: Object.freeze({
@@ -442,6 +446,15 @@ const TRANSFORM_POLICIES = Object.freeze({
442
446
  normalizeEdit: normalizeRegressionEdit,
443
447
  outputRoles: () => ({})
444
448
  }),
449
+ sort: Object.freeze({
450
+ ...findTransformTopology("sort"),
451
+ validate: validateSortTransform,
452
+ materializeOp: "materializeSortedData",
453
+ replayTransform: requestedTransform,
454
+ editable: true,
455
+ normalizeEdit: normalizeSimple(normalizeSortTransform),
456
+ outputRoles: () => ({})
457
+ }),
445
458
  summary: Object.freeze({
446
459
  ...findTransformTopology("summary"),
447
460
  validate: validateSummaryTransform,
@@ -1,5 +1,7 @@
1
1
  const NUMERIC_FORMAT = /^\.(0?\d|1[0-2])(f|%|e)$/;
2
- const TIME_DIRECTIVES = new Set(["Y", "m", "d", "b", "%"]);
2
+ const GROUPED_NUMERIC_FORMAT = /^,(?:\.(0?\d|1[0-2])f)?$/;
3
+ const WEEKDAY_NAMES = Object.freeze(["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]);
4
+ const TIME_DIRECTIVES = new Set(["Y", "m", "d", "b", "a", "H", "M", "S", "L", "%"]);
3
5
  const MONTH_NAMES = Object.freeze([
4
6
  "Jan", "Feb", "Mar", "Apr", "May", "Jun",
5
7
  "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
@@ -33,12 +35,12 @@ export function validateValueFormat(format, label = "Label format", {
33
35
  if (allowDecimalsObject) return format;
34
36
  }
35
37
  if (typeof format === "string" && (
36
- NUMERIC_FORMAT.test(format) || isUtcValueFormat(format)
38
+ NUMERIC_FORMAT.test(format) || GROUPED_NUMERIC_FORMAT.test(format) || isUtcValueFormat(format)
37
39
  )) {
38
40
  return format;
39
41
  }
40
42
  const numericCandidate = typeof format === "string"
41
- ? format.match(/^\.(\d+)(f|%|e)$/)
43
+ ? format.match(/^,?\.(\d+)(f|%|e)$/)
42
44
  : undefined;
43
45
  if (numericCandidate && Number(numericCandidate[1]) > 12) {
44
46
  throw new RangeError(`${label} numeric format supports at most 12 decimals.`);
@@ -61,9 +63,14 @@ function formatUtcValue(value, format, label) {
61
63
  Y: pad(date.getUTCFullYear(), 4),
62
64
  m: pad(date.getUTCMonth() + 1),
63
65
  d: pad(date.getUTCDate()),
64
- b: MONTH_NAMES[date.getUTCMonth()]
66
+ b: MONTH_NAMES[date.getUTCMonth()],
67
+ a: WEEKDAY_NAMES[date.getUTCDay()],
68
+ H: pad(date.getUTCHours()),
69
+ M: pad(date.getUTCMinutes()),
70
+ S: pad(date.getUTCSeconds()),
71
+ L: pad(date.getUTCMilliseconds(), 3)
65
72
  };
66
- return format.replace(/%([Ymdb%])/g, (_, directive) =>
73
+ return format.replace(/%([YmdbaHMSL%])/g, (_, directive) =>
67
74
  directive === "%" ? "%" : values[directive]
68
75
  );
69
76
  }
@@ -98,6 +105,11 @@ export function formatValue(value, {
98
105
  throw new TypeError(`${label} "${typeof resolved === "string" ? resolved : "decimals"}" requires a finite number.`);
99
106
  }
100
107
  if (typeof resolved === "object") return value.toFixed(resolved.decimals);
108
+ const grouped = resolved.match(GROUPED_NUMERIC_FORMAT);
109
+ if (grouped) {
110
+ const precision = grouped[1] === undefined ? 0 : Number(grouped[1]);
111
+ return new Intl.NumberFormat("en-US", { useGrouping: true, minimumFractionDigits: precision, maximumFractionDigits: precision }).format(value);
112
+ }
101
113
  const match = resolved.match(NUMERIC_FORMAT);
102
114
  const precision = Number(match[1]);
103
115
  if (match[2] === "f") return value.toFixed(precision);
@@ -1,3 +1,4 @@
1
+ import { requireStringValue as requireField } from "../core/validation.js";
1
2
  import { isPlainObject } from "../core/immutable.js";
2
3
  import {
3
4
  interpolateNumber,
@@ -15,12 +16,6 @@ export const WEIGHTED_AGGREGATE_OPERATIONS = Object.freeze([
15
16
  "stderr", "median", "q1", "q3", "quantile"
16
17
  ]);
17
18
 
18
- function requireField(value, label) {
19
- if (typeof value !== "string" || value.length === 0) {
20
- throw new TypeError(`${label} must be a non-empty string.`);
21
- }
22
- return value;
23
- }
24
19
 
25
20
  export function normalizeStatisticalWeight(value, label = "Statistical weight") {
26
21
  if (!isPlainObject(value)) {