ggaction 0.0.11 → 0.0.13

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 (289) hide show
  1. package/CHANGELOG.md +80 -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/action-cards.schema.json +1 -1
  6. package/knowledge/extension-authoring.md +2 -1
  7. package/knowledge/intent-taxonomy.json +1 -1
  8. package/knowledge/mcp-resources.json +1 -1
  9. package/knowledge/task-resolver.js +1 -1
  10. package/package.json +4 -2
  11. package/src/actions/basic.js +2 -0
  12. package/src/actions/boxPlots/materialize.js +2 -1
  13. package/src/actions/canvas/actions.js +3 -1
  14. package/src/actions/canvas/fitting.js +211 -0
  15. package/src/actions/canvas/index.js +2 -0
  16. package/src/actions/categoryOrder/index.js +3 -2
  17. package/src/actions/charts/area.js +47 -0
  18. package/src/actions/charts/bar.js +11 -5
  19. package/src/actions/charts/beeswarm.js +70 -0
  20. package/src/actions/charts/density.js +44 -0
  21. package/src/actions/charts/ecdf.js +183 -0
  22. package/src/actions/charts/endpoints.js +451 -0
  23. package/src/actions/charts/heatmap.js +2 -2
  24. package/src/actions/charts/histogram.js +1 -1
  25. package/src/actions/charts/horizon.js +71 -0
  26. package/src/actions/charts/index.js +37 -0
  27. package/src/actions/charts/interval-regression.js +169 -0
  28. package/src/actions/charts/line.js +5 -5
  29. package/src/actions/charts/parallel.js +1 -1
  30. package/src/actions/charts/pie.js +44 -0
  31. package/src/actions/charts/polar.js +123 -0
  32. package/src/actions/charts/radar.js +299 -0
  33. package/src/actions/charts/radial.js +42 -0
  34. package/src/actions/charts/raincloud.js +422 -0
  35. package/src/actions/charts/rug-strip.js +279 -0
  36. package/src/actions/charts/scatter.js +9 -3
  37. package/src/actions/charts/shared.js +69 -10
  38. package/src/actions/composition/actions.js +118 -1
  39. package/src/actions/data/bin.js +29 -0
  40. package/src/actions/data/computed.js +24 -0
  41. package/src/actions/data/derived.js +66 -0
  42. package/src/actions/data/ecdf.js +38 -0
  43. package/src/actions/data/filter.js +212 -28
  44. package/src/actions/data/fold.js +24 -0
  45. package/src/actions/data/index.js +24 -0
  46. package/src/actions/data/interval.js +2 -0
  47. package/src/actions/data/intervalEdit.js +86 -4
  48. package/src/actions/data/regression.js +5 -2
  49. package/src/actions/data/stack.js +26 -0
  50. package/src/actions/data/summary.js +26 -0
  51. package/src/actions/data/timeUnit.js +1 -1
  52. package/src/actions/encodings/appearance.js +32 -13
  53. package/src/actions/encodings/barWidth.js +8 -0
  54. package/src/actions/encodings/basic.js +2 -0
  55. package/src/actions/encodings/color/continuous.js +19 -59
  56. package/src/actions/encodings/color/index.js +28 -22
  57. package/src/actions/encodings/color/layout.js +3 -97
  58. package/src/actions/encodings/color/policy.js +5 -8
  59. package/src/actions/encodings/density/resolve.js +37 -5
  60. package/src/actions/encodings/density.js +120 -33
  61. package/src/actions/encodings/horizon/resolve.js +8 -4
  62. package/src/actions/encodings/horizon.js +18 -8
  63. package/src/actions/encodings/index.js +2 -0
  64. package/src/actions/encodings/offset.js +33 -27
  65. package/src/actions/encodings/parallel.js +2 -8
  66. package/src/actions/encodings/position/apply.js +40 -4
  67. package/src/actions/encodings/position/index.js +43 -5
  68. package/src/actions/encodings/position/policies/arc.js +37 -2
  69. package/src/actions/encodings/position/policies/area.js +4 -42
  70. package/src/actions/encodings/position/policies/bar.js +48 -13
  71. package/src/actions/encodings/position/resolve.js +79 -24
  72. package/src/actions/encodings/ranged.js +123 -97
  73. package/src/actions/encodings/remove.js +37 -32
  74. package/src/actions/encodings/ruleAppearance.js +23 -9
  75. package/src/actions/encodings/seriesLayout.js +104 -0
  76. package/src/actions/encodings/shared.js +25 -23
  77. package/src/actions/encodings/strokeDash.js +9 -6
  78. package/src/actions/encodings/temporal.js +7 -0
  79. package/src/actions/encodings/text.js +30 -15
  80. package/src/actions/errorBands/create.js +18 -5
  81. package/src/actions/errorBands/edit.js +341 -21
  82. package/src/actions/errorBars/create.js +153 -11
  83. package/src/actions/errorBars/edit.js +365 -19
  84. package/src/actions/facets/actions.js +410 -12
  85. package/src/actions/facets/derive.js +57 -8
  86. package/src/actions/gradientPlots/components.js +33 -11
  87. package/src/actions/gradientPlots/materialize.js +5 -7
  88. package/src/actions/guides/applicability.js +20 -17
  89. package/src/actions/guides/axes/axes.js +4 -3
  90. package/src/actions/guides/axes/axis.js +18 -18
  91. package/src/actions/guides/axes/components.js +43 -0
  92. package/src/actions/guides/axes/edit.js +16 -51
  93. package/src/actions/guides/axes/labels.js +129 -19
  94. package/src/actions/guides/axes/lines.js +7 -5
  95. package/src/actions/guides/axes/parallel/lifecycle.js +117 -0
  96. package/src/actions/guides/axes/parallel/policy.js +137 -0
  97. package/src/actions/guides/axes/parallel/resolve.js +44 -23
  98. package/src/actions/guides/axes/parallel.js +93 -217
  99. package/src/actions/guides/axes/policy.js +21 -67
  100. package/src/actions/guides/axes/tickGroups.js +14 -8
  101. package/src/actions/guides/axes/ticks.js +5 -4
  102. package/src/actions/guides/axes/titles.js +21 -8
  103. package/src/actions/guides/basic.js +2 -0
  104. package/src/actions/guides/facade.js +106 -0
  105. package/src/actions/guides/facadeAxes.js +200 -0
  106. package/src/actions/guides/facadeLegend.js +63 -0
  107. package/src/actions/guides/grids/grid.js +37 -33
  108. package/src/actions/guides/grids/resolve.js +10 -2
  109. package/src/actions/guides/guides.js +35 -28
  110. package/src/actions/guides/legends/categorical/actions.js +193 -249
  111. package/src/actions/guides/legends/categorical/components.js +4 -5
  112. package/src/actions/guides/legends/categorical/layout.js +36 -349
  113. package/src/actions/guides/legends/categorical/options.js +30 -14
  114. package/src/actions/guides/legends/categorical/recipes.js +14 -0
  115. package/src/actions/guides/legends/categorical/resolve.js +21 -10
  116. package/src/actions/guides/legends/categorical/symbols.js +2 -3
  117. package/src/actions/guides/legends/continuous/common.js +69 -14
  118. package/src/actions/guides/legends/continuous/gradient.js +111 -83
  119. package/src/actions/guides/legends/continuous/index.js +6 -2
  120. package/src/actions/guides/legends/continuous/interval.js +71 -92
  121. package/src/actions/guides/legends/continuous/opacity.js +105 -84
  122. package/src/actions/guides/legends/creation.js +27 -0
  123. package/src/actions/guides/legends/edit.js +198 -66
  124. package/src/actions/guides/legends/focused.js +2 -2
  125. package/src/actions/guides/legends/lane.js +98 -86
  126. package/src/actions/guides/legends/lifecycle.js +132 -0
  127. package/src/actions/guides/legends/remove.js +31 -76
  128. package/src/actions/guides/legends/size.js +149 -105
  129. package/src/actions/guides/legends/strokeWidth.js +106 -64
  130. package/src/actions/guides/legends/target.js +12 -0
  131. package/src/actions/guides/legends/transition.js +43 -0
  132. package/src/actions/guides/polar/axes/facade.js +77 -62
  133. package/src/actions/guides/polar/axes/labels.js +5 -4
  134. package/src/actions/guides/polar/axes/lines.js +2 -1
  135. package/src/actions/guides/polar/axes/shared.js +6 -0
  136. package/src/actions/guides/polar/axes/ticks.js +5 -4
  137. package/src/actions/guides/polar/axes/titles.js +5 -4
  138. package/src/actions/guides/polar/grids.js +7 -7
  139. package/src/actions/guides/reuse.js +44 -0
  140. package/src/actions/guides/tickValues.js +19 -21
  141. package/src/actions/index.js +2 -0
  142. package/src/actions/intervals/resolve.js +35 -15
  143. package/src/actions/marks/arc/actions.js +1 -1
  144. package/src/actions/marks/area/actions.js +18 -4
  145. package/src/actions/marks/area/materialize.js +5 -2
  146. package/src/actions/marks/bar/materialize.js +11 -7
  147. package/src/actions/marks/index.js +5 -0
  148. package/src/actions/marks/line/actions.js +43 -108
  149. package/src/actions/marks/line/materialize.js +34 -52
  150. package/src/actions/marks/point/edit.js +11 -15
  151. package/src/actions/marks/point/index.js +1 -0
  152. package/src/actions/marks/point/jitter.js +3 -0
  153. package/src/actions/marks/point/materialize.js +138 -14
  154. package/src/actions/marks/point/packing.js +68 -0
  155. package/src/actions/marks/references.js +87 -0
  156. package/src/actions/marks/remove.js +17 -4
  157. package/src/actions/marks/rule/actions.js +60 -4
  158. package/src/actions/marks/rule/appearance.js +26 -0
  159. package/src/actions/marks/shared.js +12 -12
  160. package/src/actions/marks/text/actions.js +224 -6
  161. package/src/actions/marks/text/layout.js +2 -2
  162. package/src/actions/primitives/createGraphics.js +3 -1
  163. package/src/actions/primitives/semantic.js +1 -1
  164. package/src/actions/primitives/semanticAction.js +39 -2
  165. package/src/actions/primitives/semanticValidation/guide.js +10 -0
  166. package/src/actions/primitives/semanticValidation/layer.js +18 -1
  167. package/src/actions/primitives/semanticValidation/scale.js +10 -1
  168. package/src/actions/regression/create.js +1 -1
  169. package/src/actions/regression/edit.js +19 -6
  170. package/src/actions/regression/resolve.js +1 -1
  171. package/src/actions/scales/consumers/common.js +20 -53
  172. package/src/actions/scales/consumers/families.js +27 -14
  173. package/src/actions/scales/consumers/index.js +23 -2
  174. package/src/actions/scales/consumers/seriesLayout.js +33 -2
  175. package/src/actions/scales/create.js +4 -4
  176. package/src/actions/scales/definitions.js +19 -3
  177. package/src/actions/scales/edit.js +46 -34
  178. package/src/actions/scales/editPolicy.js +21 -31
  179. package/src/actions/scales/materialize.js +10 -46
  180. package/src/actions/scales/patch.js +1 -1
  181. package/src/actions/scales/preview.js +39 -0
  182. package/src/actions/scales/quantitativeColor.js +9 -5
  183. package/src/actions/theme/actions.js +43 -0
  184. package/src/actions/theme/index.js +9 -0
  185. package/src/actions/theme/reconcile.js +882 -0
  186. package/src/actions/titles/actions.js +11 -10
  187. package/src/actions/titles/resolve.js +1 -40
  188. package/src/actions/violinPlots/create.js +86 -42
  189. package/src/actions/violinPlots/edit.js +191 -0
  190. package/src/actions/violinPlots/index.js +2 -0
  191. package/src/core/ChartProgram.js +7 -0
  192. package/src/core/action.js +22 -2
  193. package/src/core/compositionState.js +136 -8
  194. package/src/core/vocabulary.js +2 -2
  195. package/src/grammar/aggregate.js +66 -23
  196. package/src/grammar/arcs.js +213 -65
  197. package/src/grammar/areaEndpoints.js +46 -0
  198. package/src/grammar/areaSeries.js +87 -41
  199. package/src/grammar/bars/aggregate.js +33 -12
  200. package/src/grammar/bars/policy.js +9 -7
  201. package/src/grammar/bin.js +181 -0
  202. package/src/grammar/categoryOrder.js +34 -0
  203. package/src/grammar/computed.js +150 -0
  204. package/src/grammar/ecdf.js +198 -0
  205. package/src/grammar/facets/index.js +117 -6
  206. package/src/grammar/fold.js +123 -0
  207. package/src/grammar/horizon.js +6 -4
  208. package/src/grammar/interval.js +37 -20
  209. package/src/grammar/lineSeries.js +20 -63
  210. package/src/grammar/markFilter.js +22 -4
  211. package/src/grammar/markLabels.js +93 -0
  212. package/src/grammar/parallelAxisTitles.js +16 -0
  213. package/src/grammar/pathSeries.js +91 -0
  214. package/src/grammar/pointPacking.js +183 -0
  215. package/src/grammar/positionCompatibility.js +1 -1
  216. package/src/grammar/positionDatum.js +12 -0
  217. package/src/grammar/rects.js +9 -1
  218. package/src/grammar/regression/derive.js +4 -0
  219. package/src/grammar/regression/index.js +7 -2
  220. package/src/grammar/regression/models.js +22 -7
  221. package/src/grammar/regression/parameters.js +40 -13
  222. package/src/grammar/rotation.js +29 -0
  223. package/src/grammar/rules.js +9 -21
  224. package/src/grammar/scales/color.js +23 -1
  225. package/src/grammar/scales/colorConsumers.js +38 -0
  226. package/src/grammar/scales/definition.js +15 -1
  227. package/src/grammar/scales/discretized.js +11 -5
  228. package/src/grammar/scales/fields.js +38 -8
  229. package/src/grammar/scales/mapping.js +2 -0
  230. package/src/grammar/scales/ordinal.js +23 -0
  231. package/src/grammar/scales/radial.js +55 -0
  232. package/src/grammar/scales/types.js +1 -0
  233. package/src/grammar/schemas/semanticPath.js +17 -3
  234. package/src/grammar/stack.js +170 -0
  235. package/src/grammar/statistics/confidenceInterval.js +101 -0
  236. package/src/grammar/summary.js +183 -0
  237. package/src/grammar/text.js +22 -16
  238. package/src/grammar/timeUnit.js +21 -11
  239. package/src/grammar/transformTopology.js +6 -0
  240. package/src/grammar/transforms.js +36 -0
  241. package/src/grammar/valueFormat.js +112 -0
  242. package/src/grammar/window.js +13 -7
  243. package/src/layout/facets.js +86 -23
  244. package/src/layout/guideCollisions.js +12 -0
  245. package/src/layout/legend.js +2 -10
  246. package/src/layout/legendItems.js +102 -0
  247. package/src/layout/legendLane.js +100 -11
  248. package/src/materialization/bars/aggregate.js +38 -152
  249. package/src/materialization/bars/histogram.js +24 -0
  250. package/src/materialization/categorySlotOffset.js +40 -0
  251. package/src/materialization/compositionSnapshot.js +1 -1
  252. package/src/materialization/dependencies.js +57 -16
  253. package/src/materialization/encodings.js +1 -1
  254. package/src/materialization/facetGuides/legacyCategorical.js +7 -1
  255. package/src/materialization/facetGuides/placement.js +16 -1
  256. package/src/materialization/facetGuides/preparation.js +26 -1
  257. package/src/materialization/facets.js +11 -2
  258. package/src/materialization/guides/layout.js +57 -0
  259. package/src/materialization/guides/resources.js +2 -2
  260. package/src/materialization/legends.js +10 -1
  261. package/src/materialization/marks/capabilities.js +30 -7
  262. package/src/materialization/marks/grouping.js +52 -0
  263. package/src/materialization/marks/index.js +63 -25
  264. package/src/materialization/marks/policies.js +2 -0
  265. package/src/materialization/rect.js +33 -33
  266. package/src/materialization/rowEncoding.js +70 -14
  267. package/src/materialization/scaleGuideDependencies.js +8 -5
  268. package/src/materialization/scales/map.js +1 -0
  269. package/src/materialization/scales/policies/arc.js +42 -0
  270. package/src/materialization/scales/policies/bar.js +0 -43
  271. package/src/materialization/scales/policies/binnedPosition.js +7 -4
  272. package/src/materialization/scales/policies/offset.js +54 -0
  273. package/src/materialization/scales/policies/series.js +2 -1
  274. package/src/materialization/scales/resolve.js +48 -18
  275. package/src/materialization/selection/compatibility.js +11 -0
  276. package/src/materialization/selection/items/bar.js +12 -68
  277. package/src/materialization/selection/items/common.js +8 -2
  278. package/src/materialization/selection/items/path.js +3 -13
  279. package/src/materialization/selection/items/rect.js +7 -4
  280. package/src/materialization/text.js +64 -5
  281. package/src/mcp/adapter.js +2 -2
  282. package/src/selectors/datasets.js +13 -0
  283. package/src/selectors/layers.js +3 -2
  284. package/src/selectors/scales.js +6 -0
  285. package/src/theme/defaults.js +39 -0
  286. package/types/basic.d.ts +11 -2
  287. package/types/index.d.ts +126 -0
  288. package/types/program.d.ts +1327 -124
  289. package/src/materialization/bars/grouped.js +0 -144
@@ -0,0 +1,170 @@
1
+ import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
+ import { layoutSeriesPartition } from "./seriesLayout.js";
3
+
4
+ const TRANSFORM_KEYS = Object.freeze([
5
+ "type", "category", "group", "value", "mode", "as"
6
+ ]);
7
+ const AS_KEYS = Object.freeze(["start", "end", "value", "share"]);
8
+ const MODES = new Set(["stack", "fill", "center", "diverging"]);
9
+ const MAX_OUTPUT_ROWS = 10_000;
10
+
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
+
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
+
23
+ function normalizeAs(value, field) {
24
+ const requested = value ?? {};
25
+ if (!isPlainObject(requested)) return requested;
26
+ return {
27
+ ...requested,
28
+ start: requested.start ?? `${field}_start`,
29
+ end: requested.end ?? `${field}_end`,
30
+ value: requested.value ?? `${field}_value`,
31
+ share: requested.share ?? `${field}_share`
32
+ };
33
+ }
34
+
35
+ export function normalizeStackTransform({ category, group, value, mode, as } = {}) {
36
+ const transform = {
37
+ type: "stack",
38
+ category,
39
+ group,
40
+ value,
41
+ mode: mode ?? "stack",
42
+ as: normalizeAs(as, value)
43
+ };
44
+ validateStackTransform(transform);
45
+ return cloneAndFreeze(transform);
46
+ }
47
+
48
+ export function validateStackTransform(transform) {
49
+ if (!isPlainObject(transform)) {
50
+ throw new TypeError("Stack transform must be a plain object.");
51
+ }
52
+ rejectUnknownKeys(transform, TRANSFORM_KEYS, "stack transform");
53
+ if (transform.type !== "stack") {
54
+ throw new Error(`Unsupported stack transform "${transform.type}".`);
55
+ }
56
+ const roles = [
57
+ requireField(transform.category, "Stack category"),
58
+ requireField(transform.group, "Stack group"),
59
+ requireField(transform.value, "Stack value")
60
+ ];
61
+ if (new Set(roles).size !== roles.length) {
62
+ throw new Error("Stack category, group, and value fields must be unique.");
63
+ }
64
+ if (!MODES.has(transform.mode)) {
65
+ throw new Error(`Unsupported stack mode "${transform.mode}".`);
66
+ }
67
+ if (!isPlainObject(transform.as)) {
68
+ throw new TypeError("Stack as must be a plain object.");
69
+ }
70
+ rejectUnknownKeys(transform.as, AS_KEYS, "stack as");
71
+ const outputs = AS_KEYS.map(key => requireField(transform.as[key], `Stack as.${key}`));
72
+ if (new Set(outputs).size !== outputs.length) {
73
+ throw new Error("Stack output fields must be unique.");
74
+ }
75
+ return transform;
76
+ }
77
+
78
+ function scalarKey(value, label) {
79
+ if (value === null) return "null";
80
+ if (typeof value === "string") return `string:${value.length}:${value}`;
81
+ if (typeof value === "boolean") return `boolean:${value}`;
82
+ if (typeof value === "number" && Number.isFinite(value)) {
83
+ return `number:${Object.is(value, -0) ? 0 : value}`;
84
+ }
85
+ throw new TypeError(`${label} must contain null, strings, booleans, or finite numbers.`);
86
+ }
87
+
88
+ function resolvedPartition(entries, mode) {
89
+ const values = entries.map(entry => entry.row.value);
90
+ const segments = layoutSeriesPartition(values, mode);
91
+ const byIndex = new Map(segments.map(segment => [segment.index, segment]));
92
+ const shares = layoutSeriesPartition(values.map(Math.abs), "fill");
93
+ const shareByIndex = new Map(shares.map(segment => [
94
+ segment.index,
95
+ segment.end - segment.start
96
+ ]));
97
+ let boundary = mode === "center" ? segments[0]?.start ?? 0 : 0;
98
+ return entries.map((entry, index) => {
99
+ const segment = byIndex.get(index);
100
+ const start = segment?.start ?? (mode === "diverging" ? 0 : boundary);
101
+ const end = segment?.end ?? start;
102
+ if (segment !== undefined && mode !== "diverging") boundary = segment.end;
103
+ return {
104
+ index: entry.index,
105
+ value: entry.row.value,
106
+ start,
107
+ end,
108
+ share: shareByIndex.get(index) ?? 0
109
+ };
110
+ });
111
+ }
112
+
113
+ export function deriveStackRows(rows, transform) {
114
+ validateStackTransform(transform);
115
+ if (rows.length > MAX_OUTPUT_ROWS) {
116
+ throw new RangeError(`Stack output cannot exceed ${MAX_OUTPUT_ROWS} rows.`);
117
+ }
118
+ const sourceFields = new Set(rows.flatMap(row => Object.keys(row)));
119
+ for (const output of Object.values(transform.as)) {
120
+ if (sourceFields.has(output)) {
121
+ throw new Error(`Stack output field "${output}" already exists.`);
122
+ }
123
+ }
124
+ const partitions = new Map();
125
+ const groups = new Map();
126
+ rows.forEach((source, index) => {
127
+ for (const field of [transform.category, transform.group, transform.value]) {
128
+ if (!Object.hasOwn(source, field)) {
129
+ throw new Error(`Stack source does not contain field "${field}" at row ${index}.`);
130
+ }
131
+ }
132
+ const categoryKey = scalarKey(source[transform.category], "Stack category field");
133
+ const groupKey = scalarKey(source[transform.group], "Stack group field");
134
+ if (!Number.isFinite(source[transform.value])) {
135
+ throw new TypeError(
136
+ `Stack value field "${transform.value}" must contain a finite number at row ${index}.`
137
+ );
138
+ }
139
+ if (!groups.has(groupKey)) groups.set(groupKey, groups.size);
140
+ let partition = partitions.get(categoryKey);
141
+ if (partition === undefined) {
142
+ partition = new Map();
143
+ partitions.set(categoryKey, partition);
144
+ }
145
+ if (partition.has(groupKey)) {
146
+ throw new Error("Stack source requires one row per category/group cell.");
147
+ }
148
+ partition.set(groupKey, {
149
+ index,
150
+ row: { groupKey, value: source[transform.value] }
151
+ });
152
+ });
153
+
154
+ const resolved = new Array(rows.length);
155
+ for (const partition of partitions.values()) {
156
+ const entries = [...partition.values()].sort((left, right) =>
157
+ groups.get(left.row.groupKey) - groups.get(right.row.groupKey)
158
+ );
159
+ for (const item of resolvedPartition(entries, transform.mode)) {
160
+ resolved[item.index] = item;
161
+ }
162
+ }
163
+ return rows.map((row, index) => ({
164
+ ...row,
165
+ [transform.as.start]: resolved[index].start,
166
+ [transform.as.end]: resolved[index].end,
167
+ [transform.as.value]: resolved[index].value,
168
+ [transform.as.share]: resolved[index].share
169
+ }));
170
+ }
@@ -0,0 +1,101 @@
1
+ import { cloneAndFreeze } from "../../core/immutable.js";
2
+ import { studentTCriticalValue } from "./studentT.js";
3
+
4
+ export const CONFIDENCE_INTERVAL_METHODS = Object.freeze([
5
+ "normal",
6
+ "student-t"
7
+ ]);
8
+
9
+ function validateMethod(method, label) {
10
+ if (!CONFIDENCE_INTERVAL_METHODS.includes(method)) {
11
+ throw new Error(`Unsupported ${label} method "${method}".`);
12
+ }
13
+ }
14
+
15
+ function validateLevel(level, label) {
16
+ if (!Number.isFinite(level) || level <= 0 || level >= 1) {
17
+ throw new RangeError(`${label} level must be between 0 and 1.`);
18
+ }
19
+ }
20
+
21
+ // Peter J. Acklam's inverse-normal approximation. The exact 95% compatibility
22
+ // value is handled by normalCriticalValue before this approximation is used.
23
+ function inverseNormal(probability) {
24
+ const a = [
25
+ -3.969683028665376e1, 2.209460984245205e2,
26
+ -2.759285104469687e2, 1.38357751867269e2,
27
+ -3.066479806614716e1, 2.506628277459239
28
+ ];
29
+ const b = [
30
+ -5.447609879822406e1, 1.615858368580409e2,
31
+ -1.556989798598866e2, 6.680131188771972e1,
32
+ -1.328068155288572e1
33
+ ];
34
+ const c = [
35
+ -7.784894002430293e-3, -3.223964580411365e-1,
36
+ -2.400758277161838, -2.549732539343734,
37
+ 4.374664141464968, 2.938163982698783
38
+ ];
39
+ const d = [
40
+ 7.784695709041462e-3, 3.224671290700398e-1,
41
+ 2.445134137142996, 3.754408661907416
42
+ ];
43
+ const low = 0.02425;
44
+ const high = 1 - low;
45
+ if (probability < low) {
46
+ const q = Math.sqrt(-2 * Math.log(probability));
47
+ return (((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5]) /
48
+ ((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1);
49
+ }
50
+ if (probability > high) {
51
+ const q = Math.sqrt(-2 * Math.log(1 - probability));
52
+ return -(((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5]) /
53
+ ((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1);
54
+ }
55
+ const q = probability - 0.5;
56
+ const r = q * q;
57
+ return (((((a[0] * r + a[1]) * r + a[2]) * r + a[3]) * r + a[4]) * r + a[5]) * q /
58
+ (((((b[0] * r + b[1]) * r + b[2]) * r + b[3]) * r + b[4]) * r + 1);
59
+ }
60
+
61
+ export function normalizeConfidenceInterval({
62
+ method,
63
+ level
64
+ } = {}, {
65
+ defaultMethod,
66
+ defaultLevel = 0.95,
67
+ label = "Confidence interval"
68
+ } = {}) {
69
+ validateMethod(defaultMethod, label);
70
+ const resolvedMethod = method ?? defaultMethod;
71
+ const resolvedLevel = level ?? defaultLevel;
72
+ validateMethod(resolvedMethod, label);
73
+ validateLevel(resolvedLevel, label);
74
+ return cloneAndFreeze({ method: resolvedMethod, level: resolvedLevel });
75
+ }
76
+
77
+ export function normalCriticalValue(level) {
78
+ validateLevel(level, "Normal confidence interval");
79
+ if (level === 0.95) return 1.96;
80
+ return inverseNormal((1 + level) / 2);
81
+ }
82
+
83
+ export function confidenceCriticalValue({
84
+ method,
85
+ level,
86
+ degreesOfFreedom
87
+ }) {
88
+ const normalized = normalizeConfidenceInterval(
89
+ { method, level },
90
+ { defaultMethod: method, label: "Confidence interval" }
91
+ );
92
+ if (normalized.method === "normal") {
93
+ return normalCriticalValue(normalized.level);
94
+ }
95
+ if (!Number.isInteger(degreesOfFreedom) || degreesOfFreedom < 1) {
96
+ throw new RangeError(
97
+ "Student-t confidence interval degreesOfFreedom must be a positive integer."
98
+ );
99
+ }
100
+ return studentTCriticalValue(normalized.level, degreesOfFreedom);
101
+ }
@@ -0,0 +1,183 @@
1
+ import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
+ import {
3
+ aggregateRows,
4
+ isScalarAggregate,
5
+ validateAggregate,
6
+ validateAggregateFieldValues
7
+ } from "./aggregate.js";
8
+
9
+ const TRANSFORM_KEYS = Object.freeze([
10
+ "type", "groupBy", "aggregates", "members"
11
+ ]);
12
+ const AGGREGATE_KEYS = Object.freeze(["op", "field", "as"]);
13
+ const NOMINAL_OPERATIONS = new Set(["distinct", "valid", "missing"]);
14
+ const MAX_OUTPUT_ROWS = 10_000;
15
+
16
+ function rejectUnknownKeys(value, supported, label) {
17
+ const unknown = Object.keys(value).find(key => !supported.includes(key));
18
+ if (unknown !== undefined) {
19
+ throw new Error(`Unknown ${label} property "${unknown}".`);
20
+ }
21
+ }
22
+
23
+ function requireField(value, label) {
24
+ if (typeof value !== "string" || value.length === 0) {
25
+ throw new TypeError(`${label} must be a non-empty string.`);
26
+ }
27
+ return value;
28
+ }
29
+
30
+ function normalizeGroupBy(value) {
31
+ if (value === undefined) return [];
32
+ return Array.isArray(value) ? [...value] : [value];
33
+ }
34
+
35
+ function normalizeAggregate(value) {
36
+ if (!isPlainObject(value)) return value;
37
+ return {
38
+ ...value,
39
+ op: validateAggregate(value.op),
40
+ ...(Object.hasOwn(value, "field") ? { field: value.field } : {}),
41
+ as: value.as
42
+ };
43
+ }
44
+
45
+ export function normalizeSummaryTransform({ groupBy, aggregates, members } = {}) {
46
+ const transform = {
47
+ type: "summary",
48
+ groupBy: normalizeGroupBy(groupBy),
49
+ aggregates: Array.isArray(aggregates)
50
+ ? aggregates.map(normalizeAggregate)
51
+ : aggregates,
52
+ ...(members === undefined ? {} : { members })
53
+ };
54
+ validateSummaryTransform(transform);
55
+ return cloneAndFreeze(transform);
56
+ }
57
+
58
+ export function validateSummaryTransform(transform) {
59
+ if (!isPlainObject(transform)) {
60
+ throw new TypeError("Summary transform must be a plain object.");
61
+ }
62
+ rejectUnknownKeys(transform, TRANSFORM_KEYS, "summary transform");
63
+ if (transform.type !== "summary") {
64
+ throw new Error(`Unsupported summary transform "${transform.type}".`);
65
+ }
66
+ if (
67
+ !Array.isArray(transform.groupBy) ||
68
+ transform.groupBy.some(field => typeof field !== "string" || field.length === 0)
69
+ ) {
70
+ throw new TypeError("Summary groupBy must contain field names.");
71
+ }
72
+ if (new Set(transform.groupBy).size !== transform.groupBy.length) {
73
+ throw new Error("Summary groupBy fields must be unique.");
74
+ }
75
+ if (!Array.isArray(transform.aggregates) || transform.aggregates.length === 0) {
76
+ throw new TypeError("Summary aggregates must be a non-empty array.");
77
+ }
78
+ if (transform.aggregates.length > 64) {
79
+ throw new RangeError("Summary aggregates cannot contain more than 64 outputs.");
80
+ }
81
+ const outputs = new Set(transform.groupBy);
82
+ transform.aggregates.forEach((aggregate, index) => {
83
+ if (!isPlainObject(aggregate)) {
84
+ throw new TypeError(`Summary aggregate ${index} must be a plain object.`);
85
+ }
86
+ rejectUnknownKeys(aggregate, AGGREGATE_KEYS, `summary aggregate ${index}`);
87
+ const operation = validateAggregate(aggregate.op);
88
+ if (operation === "count") {
89
+ if (Object.hasOwn(aggregate, "field")) {
90
+ throw new Error("Summary count does not accept a field.");
91
+ }
92
+ } else {
93
+ requireField(aggregate.field, `Summary aggregate ${index} field`);
94
+ }
95
+ const output = requireField(aggregate.as, `Summary aggregate ${index} as`);
96
+ if (outputs.has(output)) {
97
+ throw new Error(`Summary output field "${output}" collides with another output.`);
98
+ }
99
+ outputs.add(output);
100
+ });
101
+ if (transform.members !== undefined) {
102
+ const members = requireField(transform.members, "Summary members");
103
+ if (outputs.has(members)) {
104
+ throw new Error(`Summary members field "${members}" collides with another output.`);
105
+ }
106
+ }
107
+ return transform;
108
+ }
109
+
110
+ function scalarKey(value, label) {
111
+ if (value === null) return "null";
112
+ if (typeof value === "string") return `string:${value.length}:${value}`;
113
+ if (typeof value === "boolean") return `boolean:${value}`;
114
+ if (typeof value === "number" && Number.isFinite(value)) {
115
+ return `number:${Object.is(value, -0) ? 0 : value}`;
116
+ }
117
+ throw new TypeError(
118
+ `${label} must contain null, strings, booleans, or finite numbers.`
119
+ );
120
+ }
121
+
122
+ function requireSourceFields(rows, transform) {
123
+ if (rows.length === 0) return;
124
+ const fields = new Set(rows.flatMap(row => Object.keys(row)));
125
+ for (const field of transform.groupBy) {
126
+ if (!fields.has(field)) {
127
+ throw new Error(`Summary source does not contain group field "${field}".`);
128
+ }
129
+ }
130
+ for (const aggregate of transform.aggregates) {
131
+ if (aggregate.field !== undefined && !fields.has(aggregate.field)) {
132
+ throw new Error(`Summary source does not contain aggregate field "${aggregate.field}".`);
133
+ }
134
+ const orderBy = isPlainObject(aggregate.op) ? aggregate.op.orderBy : undefined;
135
+ if (orderBy !== undefined && !fields.has(orderBy)) {
136
+ throw new Error(`Summary source does not contain order field "${orderBy}".`);
137
+ }
138
+ }
139
+ }
140
+
141
+ function validateAggregateValues(rows, aggregate) {
142
+ if (aggregate.op === "count") return;
143
+ if (isScalarAggregate(aggregate.op) && NOMINAL_OPERATIONS.has(aggregate.op)) {
144
+ validateAggregateFieldValues(rows, aggregate.field, "nominal");
145
+ return;
146
+ }
147
+ validateAggregateFieldValues(rows, aggregate.field, "quantitative");
148
+ }
149
+
150
+ export function deriveSummaryRows(rows, transform) {
151
+ validateSummaryTransform(transform);
152
+ requireSourceFields(rows, transform);
153
+ for (const aggregate of transform.aggregates) {
154
+ validateAggregateValues(rows, aggregate);
155
+ }
156
+
157
+ const groups = new Map();
158
+ if (transform.groupBy.length === 0) {
159
+ groups.set("all", { values: {}, rows });
160
+ } else {
161
+ rows.forEach(row => {
162
+ const values = Object.fromEntries(transform.groupBy.map(field => [field, row[field]]));
163
+ const key = transform.groupBy.map(field =>
164
+ scalarKey(row[field], `Summary group field "${field}"`)
165
+ ).join("|");
166
+ const group = groups.get(key) ?? { values, rows: [] };
167
+ group.rows.push(row);
168
+ groups.set(key, group);
169
+ });
170
+ }
171
+ if (groups.size > MAX_OUTPUT_ROWS) {
172
+ throw new RangeError(`Summary output cannot exceed ${MAX_OUTPUT_ROWS} groups.`);
173
+ }
174
+
175
+ return [...groups.values()].map(group => ({
176
+ ...group.values,
177
+ ...Object.fromEntries(transform.aggregates.map(aggregate => [
178
+ aggregate.as,
179
+ aggregateRows(group.rows, aggregate.field ?? "__row", aggregate.op)
180
+ ])),
181
+ ...(transform.members === undefined ? {} : { [transform.members]: group.rows })
182
+ }));
183
+ }
@@ -1,5 +1,15 @@
1
1
  import { cloneAndFreeze } from "../core/immutable.js";
2
2
  import { validateConcreteGraphicValue } from "./schemas/concreteGraphic.js";
3
+ import {
4
+ formatValue,
5
+ isUtcValueFormat,
6
+ validateValueFormat
7
+ } from "./valueFormat.js";
8
+ import { resolveRotation } from "./rotation.js";
9
+
10
+ export function isSourceOwnedText(layer) {
11
+ return layer?.mark?.type === "text" && layer.source !== undefined;
12
+ }
3
13
 
4
14
  export const DEFAULT_TEXT_MARK = cloneAndFreeze({
5
15
  fill: "#334155",
@@ -14,20 +24,13 @@ export const DEFAULT_TEXT_MARK = cloneAndFreeze({
14
24
  dy: 0
15
25
  });
16
26
 
17
- const FIXED_FORMAT = /^\.(\d{1,2})f$/;
18
-
19
27
  export function validateTextFormat(format) {
20
- if (format === undefined || format === "auto") return format ?? "auto";
21
- if (typeof format !== "string" || !FIXED_FORMAT.test(format)) {
28
+ if (format !== undefined && typeof format !== "string") {
22
29
  throw new Error(
23
- 'Text format must be "auto" or a fixed-decimal token such as ".1f".'
30
+ 'Text format must be "auto" or a supported numeric/UTC format string.'
24
31
  );
25
32
  }
26
- const decimals = Number(format.match(FIXED_FORMAT)[1]);
27
- if (decimals > 12) {
28
- throw new RangeError("Text fixed-decimal format supports at most 12 decimals.");
29
- }
30
- return format;
33
+ return validateValueFormat(format, "Text format");
31
34
  }
32
35
 
33
36
  export function formatTextValue(value, format = "auto") {
@@ -37,10 +40,11 @@ export function formatTextValue(value, format = "auto") {
37
40
  const text = String(value);
38
41
  return text.length === 0 ? undefined : text;
39
42
  }
40
- if (!Number.isFinite(value)) {
41
- throw new TypeError(`Text format "${resolved}" requires a finite number.`);
42
- }
43
- return value.toFixed(Number(resolved.match(FIXED_FORMAT)[1]));
43
+ return formatValue(value, {
44
+ format: resolved,
45
+ valueType: isUtcValueFormat(resolved) ? "temporal" : "quantitative",
46
+ label: "Text format"
47
+ });
44
48
  }
45
49
 
46
50
  export function normalizeTextMarkConfig(options, base = DEFAULT_TEXT_MARK) {
@@ -52,14 +56,16 @@ export function normalizeTextMarkConfig(options, base = DEFAULT_TEXT_MARK) {
52
56
  fontFamily: "fontFamily",
53
57
  fontWeight: "fontWeight",
54
58
  align: "textAlign",
55
- baseline: "textBaseline",
56
- rotation: "rotation"
59
+ baseline: "textBaseline"
57
60
  };
58
61
  for (const [option, property] of Object.entries(mapping)) {
59
62
  if (!Object.hasOwn(options, option)) continue;
60
63
  validateConcreteGraphicValue("text", property, options[option]);
61
64
  config[option] = options[option];
62
65
  }
66
+ if (Object.hasOwn(options, "rotation")) {
67
+ config.rotation = resolveRotation(options.rotation, "Text rotation");
68
+ }
63
69
  for (const property of ["dx", "dy"]) {
64
70
  if (!Object.hasOwn(options, property)) continue;
65
71
  if (!Number.isFinite(options[property])) {
@@ -1,5 +1,5 @@
1
1
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
- import { normalizeTemporalValue } from "./scales/fields.js";
2
+ import { normalizeTemporalValue, validateTemporalUnit } from "./scales/fields.js";
3
3
 
4
4
  export function utcTimestamp(
5
5
  year,
@@ -18,6 +18,14 @@ export function validTimestamp(value) {
18
18
  return Number.isFinite(value) && Number.isFinite(new Date(value).getTime());
19
19
  }
20
20
 
21
+ function utcBucket(...parts) {
22
+ const timestamp = utcTimestamp(...parts);
23
+ if (!validTimestamp(timestamp)) {
24
+ throw new RangeError("Time-unit bucket start is outside the supported Date range.");
25
+ }
26
+ return timestamp;
27
+ }
28
+
21
29
  export const TIME_UNITS = cloneAndFreeze([
22
30
  "year",
23
31
  "quarter",
@@ -28,7 +36,7 @@ export const TIME_UNITS = cloneAndFreeze([
28
36
  "second"
29
37
  ]);
30
38
 
31
- const TRANSFORM_KEYS = Object.freeze(["type", "field", "unit", "as"]);
39
+ const TRANSFORM_KEYS = Object.freeze(["type", "field", "unit", "as", "temporalUnit"]);
32
40
 
33
41
  function requireField(value, label) {
34
42
  if (typeof value !== "string" || value.length === 0) {
@@ -52,6 +60,7 @@ export function validateTimeUnitTransform(transform) {
52
60
  if (transform.type !== "timeUnit") {
53
61
  throw new Error(`Unsupported time-unit transform "${transform.type}".`);
54
62
  }
63
+ validateTemporalUnit(transform.temporalUnit);
55
64
  const field = requireField(transform.field, "Time-unit field");
56
65
  const output = requireField(transform.as, "Time-unit output field");
57
66
  if (field === output) {
@@ -63,8 +72,9 @@ export function validateTimeUnitTransform(transform) {
63
72
  return transform;
64
73
  }
65
74
 
66
- export function normalizeTimeUnitTransform({ field, unit, as } = {}) {
67
- const transform = { type: "timeUnit", field, unit, as };
75
+ export function normalizeTimeUnitTransform({ field, unit, as, temporalUnit } = {}) {
76
+ const transform = { type: "timeUnit", field, unit, as,
77
+ ...(temporalUnit === undefined ? {} : { temporalUnit }) };
68
78
  validateTimeUnitTransform(transform);
69
79
  return cloneAndFreeze(transform);
70
80
  }
@@ -82,15 +92,15 @@ export function floorUtcTimeUnit(timestamp, unit) {
82
92
  }
83
93
  const year = date.getUTCFullYear();
84
94
  const month = date.getUTCMonth();
85
- if (unit === "year") return utcTimestamp(year);
86
- if (unit === "quarter") return utcTimestamp(year, Math.floor(month / 3) * 3);
87
- if (unit === "month") return utcTimestamp(year, month);
95
+ if (unit === "year") return utcBucket(year);
96
+ if (unit === "quarter") return utcBucket(year, Math.floor(month / 3) * 3);
97
+ if (unit === "month") return utcBucket(year, month);
88
98
  const day = date.getUTCDate();
89
- if (unit === "day") return utcTimestamp(year, month, day);
99
+ if (unit === "day") return utcBucket(year, month, day);
90
100
  const hour = date.getUTCHours();
91
- if (unit === "hour") return utcTimestamp(year, month, day, hour);
101
+ if (unit === "hour") return utcBucket(year, month, day, hour);
92
102
  const minute = date.getUTCMinutes();
93
- if (unit === "minute") return utcTimestamp(year, month, day, hour, minute);
103
+ if (unit === "minute") return utcBucket(year, month, day, hour, minute);
94
104
  return Math.floor(timestamp / 1000) * 1000;
95
105
  }
96
106
 
@@ -108,7 +118,7 @@ export function deriveTimeUnitRows(rows, transform) {
108
118
  if (Object.hasOwn(row, transform.as)) {
109
119
  throw new Error(`Time-unit output field "${transform.as}" already exists.`);
110
120
  }
111
- const timestamp = normalizeTemporalValue(row[transform.field], transform.field, index);
121
+ const timestamp = normalizeTemporalValue(row[transform.field], transform.field, index, transform.temporalUnit);
112
122
  return {
113
123
  ...row,
114
124
  [transform.as]: floorUtcTimeUnit(timestamp, transform.unit)
@@ -1,14 +1,20 @@
1
1
  const TRANSFORM_TOPOLOGY = Object.freeze({
2
+ bin: Object.freeze({ facetTopology: "statistical" }),
2
3
  bin2d: Object.freeze({ facetTopology: "statistical" }),
4
+ computed: Object.freeze({ facetTopology: "rowPreserving" }),
3
5
  boxOutlier: Object.freeze({ facetTopology: "statistical" }),
4
6
  boxSummary: Object.freeze({ facetTopology: "statistical" }),
5
7
  density: Object.freeze({ facetTopology: "statistical" }),
8
+ ecdf: Object.freeze({ facetTopology: "statistical" }),
6
9
  filter: Object.freeze({ facetTopology: "rowPreserving" }),
10
+ fold: Object.freeze({ facetTopology: "statistical" }),
7
11
  gradientProfile: Object.freeze({ facetTopology: "statistical" }),
8
12
  horizon: Object.freeze({ facetTopology: "statistical" }),
9
13
  interval: Object.freeze({ facetTopology: "statistical" }),
10
14
  markFilter: Object.freeze({ provenanceTransparent: true }),
11
15
  regression: Object.freeze({ facetTopology: "statistical" }),
16
+ summary: Object.freeze({ facetTopology: "statistical" }),
17
+ stack: Object.freeze({ facetTopology: "statistical" }),
12
18
  timeUnit: Object.freeze({ facetTopology: "rowPreserving" }),
13
19
  window: Object.freeze({ facetTopology: "statistical" })
14
20
  });