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
@@ -18,9 +18,9 @@ const SUPPORTED_BAR_GRAINS = new Set([
18
18
  ]);
19
19
  const MAX_FACET_CHILDREN = 100;
20
20
 
21
- function requireFacetField(field) {
21
+ function requireFacetField(field, label = "facet") {
22
22
  if (typeof field !== "string" || field.length === 0) {
23
- throw new TypeError("facet requires a non-empty field.");
23
+ throw new TypeError(`${label} requires a non-empty field.`);
24
24
  }
25
25
  return field;
26
26
  }
@@ -74,25 +74,52 @@ function uniqueInOrder(values) {
74
74
  return [...new Set(values)];
75
75
  }
76
76
 
77
- function resolveValues(observed, requested) {
77
+ function resolveValues(observed, requested, label = "facet values") {
78
78
  if (requested === undefined) return observed;
79
79
  if (!Array.isArray(requested) || requested.length === 0) {
80
- throw new TypeError("facet values must be a non-empty array when provided.");
80
+ throw new TypeError(`${label} must be a non-empty array when provided.`);
81
81
  }
82
82
  const normalized = readNominalField(
83
83
  requested.map(value => ({ value })),
84
84
  "value"
85
85
  );
86
86
  if (new Set(normalized).size !== normalized.length) {
87
- throw new Error("facet values must be unique.");
87
+ throw new Error(`${label} must be unique.`);
88
88
  }
89
89
  const missing = normalized.find(value => !observed.includes(value));
90
90
  if (missing !== undefined) {
91
- throw new Error(`facet value ${JSON.stringify(missing)} is not present in the source field.`);
91
+ throw new Error(
92
+ `${label} value ${JSON.stringify(missing)} is not present in the source field.`
93
+ );
92
94
  }
93
95
  return normalized;
94
96
  }
95
97
 
98
+ function requireGridRole(value, label) {
99
+ if (!isPlainObject(value)) {
100
+ throw new TypeError(`facetGrid ${label} must be a plain object.`);
101
+ }
102
+ const unknown = Object.keys(value).find(key => !["field", "values"].includes(key));
103
+ if (unknown !== undefined) {
104
+ throw new Error(`Unknown facetGrid ${label} option "${unknown}".`);
105
+ }
106
+ return {
107
+ field: requireFacetField(value.field, `facetGrid ${label}`),
108
+ ...(value.values === undefined ? {} : { values: value.values })
109
+ };
110
+ }
111
+
112
+ function pairKey(row, column) {
113
+ return JSON.stringify([
114
+ [typeof row, row],
115
+ [typeof column, column]
116
+ ]);
117
+ }
118
+
119
+ function displayValue(value) {
120
+ return value === "" ? "(empty)" : String(value);
121
+ }
122
+
96
123
  export function resolveFacetDefinition(semanticSpec, options = {}) {
97
124
  if (!isPlainObject(semanticSpec)) {
98
125
  throw new TypeError("resolveFacetDefinition requires a semantic spec.");
@@ -136,3 +163,87 @@ export function resolveFacetDefinition(semanticSpec, options = {}) {
136
163
  }))
137
164
  });
138
165
  }
166
+
167
+ export function resolveFacetGridDefinition(semanticSpec, options = {}) {
168
+ if (!isPlainObject(semanticSpec)) {
169
+ throw new TypeError("resolveFacetGridDefinition requires a semantic spec.");
170
+ }
171
+ if (!isPlainObject(options)) {
172
+ throw new TypeError("facetGrid options must be a plain object.");
173
+ }
174
+ const rows = requireGridRole(options.rows, "rows");
175
+ const columns = requireGridRole(options.columns, "columns");
176
+ if (rows.field === columns.field) {
177
+ throw new Error("facetGrid rows.field and columns.field must be different.");
178
+ }
179
+ const combinations = options.combinations ?? "observed";
180
+ if (!["observed", "full"].includes(combinations)) {
181
+ throw new Error('facetGrid combinations must be "observed" or "full".');
182
+ }
183
+ const id = validateUserId(options.id ?? "facetGrid", "Facet grid id");
184
+ requireSupportedLayers(semanticSpec);
185
+ const dependencies = planFacetDependencies(semanticSpec, {
186
+ field: rows.field,
187
+ ...(options.data === undefined ? {} : { data: options.data })
188
+ });
189
+ const data = dependencies.anchor;
190
+ const dataset = requirePartitionDataset(semanticSpec, data);
191
+ const observedRows = readNominalField(dataset.values, rows.field);
192
+ const observedColumns = readNominalField(dataset.values, columns.field);
193
+ const rowValues = resolveValues(
194
+ uniqueInOrder(observedRows), rows.values, "facetGrid rows.values"
195
+ );
196
+ const columnValues = resolveValues(
197
+ uniqueInOrder(observedColumns), columns.values, "facetGrid columns.values"
198
+ );
199
+ const observedPairs = new Set(dataset.values.map((_, index) =>
200
+ pairKey(observedRows[index], observedColumns[index])
201
+ ));
202
+ const candidates = rowValues.flatMap((rowValue, row) =>
203
+ columnValues.map((columnValue, column) => ({
204
+ row,
205
+ column,
206
+ rowValue,
207
+ columnValue,
208
+ empty: !observedPairs.has(pairKey(rowValue, columnValue))
209
+ }))
210
+ );
211
+ const selected = combinations === "full"
212
+ ? candidates
213
+ : candidates.filter(cell => !cell.empty);
214
+ if (selected.length === 0) {
215
+ throw new Error("facetGrid has no observed row and column combinations.");
216
+ }
217
+ validateGeneratedItemLimit(
218
+ selected.length,
219
+ "Facet grid child count",
220
+ MAX_FACET_CHILDREN
221
+ );
222
+ validateWorkLimit(
223
+ dataset.values.length * selected.length,
224
+ "Facet grid partition work"
225
+ );
226
+ return cloneAndFreeze({
227
+ id,
228
+ data,
229
+ dependencies,
230
+ grid: {
231
+ rows: { field: rows.field, values: rowValues },
232
+ columns: { field: columns.field, values: columnValues },
233
+ combinations
234
+ },
235
+ cells: selected.map(cell => {
236
+ const cellId = `${id}-row-${cell.row + 1}-column-${cell.column + 1}`;
237
+ return {
238
+ ...cell,
239
+ id: cellId,
240
+ data: `${cellId}-data`,
241
+ value: `${displayValue(cell.rowValue)} · ${displayValue(cell.columnValue)}`,
242
+ filters: [
243
+ { field: rows.field, value: cell.rowValue },
244
+ { field: columns.field, value: cell.columnValue }
245
+ ]
246
+ };
247
+ })
248
+ });
249
+ }
@@ -0,0 +1,123 @@
1
+ import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
+
3
+ const TRANSFORM_KEYS = Object.freeze(["type", "fields", "as"]);
4
+ const AS_KEYS = Object.freeze(["key", "value"]);
5
+ const MAX_FIELDS = 64;
6
+ const MAX_OUTPUT_ROWS = 10_000;
7
+
8
+ function rejectUnknownKeys(value, supported, label) {
9
+ const unknown = Object.keys(value).find(key => !supported.includes(key));
10
+ if (unknown !== undefined) {
11
+ throw new Error(`Unknown ${label} property "${unknown}".`);
12
+ }
13
+ }
14
+
15
+ function requireField(value, label) {
16
+ if (typeof value !== "string" || value.length === 0) {
17
+ throw new TypeError(`${label} must be a non-empty string.`);
18
+ }
19
+ return value;
20
+ }
21
+
22
+ function normalizeAs(value) {
23
+ const requested = value ?? {};
24
+ if (!isPlainObject(requested)) return requested;
25
+ return {
26
+ ...requested,
27
+ key: requested.key ?? "key",
28
+ value: requested.value ?? "value"
29
+ };
30
+ }
31
+
32
+ export function normalizeFoldTransform({ fields, as } = {}) {
33
+ const transform = {
34
+ type: "fold",
35
+ fields: Array.isArray(fields) ? [...fields] : fields,
36
+ as: normalizeAs(as)
37
+ };
38
+ validateFoldTransform(transform);
39
+ return cloneAndFreeze(transform);
40
+ }
41
+
42
+ export function validateFoldTransform(transform) {
43
+ if (!isPlainObject(transform)) {
44
+ throw new TypeError("Fold transform must be a plain object.");
45
+ }
46
+ rejectUnknownKeys(transform, TRANSFORM_KEYS, "fold transform");
47
+ if (transform.type !== "fold") {
48
+ throw new Error(`Unsupported fold transform "${transform.type}".`);
49
+ }
50
+ if (!Array.isArray(transform.fields) || transform.fields.length === 0) {
51
+ throw new TypeError("Fold fields must be a non-empty array.");
52
+ }
53
+ if (transform.fields.length > MAX_FIELDS) {
54
+ throw new RangeError(`Fold fields cannot contain more than ${MAX_FIELDS} entries.`);
55
+ }
56
+ transform.fields.forEach((field, index) => {
57
+ requireField(field, `Fold fields[${index}]`);
58
+ });
59
+ if (new Set(transform.fields).size !== transform.fields.length) {
60
+ throw new Error("Fold fields must be unique.");
61
+ }
62
+ if (!isPlainObject(transform.as)) {
63
+ throw new TypeError("Fold as must be a plain object.");
64
+ }
65
+ rejectUnknownKeys(transform.as, AS_KEYS, "fold as");
66
+ const key = requireField(transform.as.key, "Fold as.key");
67
+ const value = requireField(transform.as.value, "Fold as.value");
68
+ if (key === value) {
69
+ throw new Error("Fold output fields must be unique.");
70
+ }
71
+ return transform;
72
+ }
73
+
74
+ function valueType(value, field, row) {
75
+ if (value === null || value === undefined) {
76
+ throw new TypeError(`Fold field "${field}" is missing at row ${row}.`);
77
+ }
78
+ if (typeof value === "number") {
79
+ if (!Number.isFinite(value)) {
80
+ throw new TypeError(`Fold field "${field}" must contain finite numbers.`);
81
+ }
82
+ return "number";
83
+ }
84
+ if (typeof value === "string" || typeof value === "boolean") {
85
+ return typeof value;
86
+ }
87
+ throw new TypeError(
88
+ `Fold field "${field}" must contain finite numbers, strings, or booleans.`
89
+ );
90
+ }
91
+
92
+ export function deriveFoldRows(rows, transform) {
93
+ validateFoldTransform(transform);
94
+ if (rows.length * transform.fields.length > MAX_OUTPUT_ROWS) {
95
+ throw new RangeError(`Fold output cannot exceed ${MAX_OUTPUT_ROWS} rows.`);
96
+ }
97
+ const sourceFields = new Set(rows.flatMap(row => Object.keys(row)));
98
+ for (const output of [transform.as.key, transform.as.value]) {
99
+ if (sourceFields.has(output)) {
100
+ throw new Error(`Fold output field "${output}" already exists.`);
101
+ }
102
+ }
103
+ let expectedType;
104
+ const output = [];
105
+ rows.forEach((row, rowIndex) => {
106
+ for (const field of transform.fields) {
107
+ if (!Object.hasOwn(row, field)) {
108
+ throw new Error(`Fold source does not contain field "${field}" at row ${rowIndex}.`);
109
+ }
110
+ const type = valueType(row[field], field, rowIndex);
111
+ if (expectedType === undefined) expectedType = type;
112
+ else if (type !== expectedType) {
113
+ throw new TypeError("Fold fields must contain one common primitive type.");
114
+ }
115
+ output.push({
116
+ ...row,
117
+ [transform.as.key]: field,
118
+ [transform.as.value]: row[field]
119
+ });
120
+ }
121
+ });
122
+ return output;
123
+ }
@@ -8,14 +8,15 @@ import { interpolateNumber, inverseLerp } from "./numeric.js";
8
8
  import { requireFiniteResult } from "./numeric.js";
9
9
  import {
10
10
  isNominalValue,
11
- normalizeTemporalValue
11
+ normalizeTemporalValue,
12
+ resolveTemporalUnit
12
13
  } from "./scales/index.js";
13
14
 
14
15
  export const HORIZON_RESOLUTIONS = Object.freeze(["shared", "independent"]);
15
16
  export const HORIZON_MISSING_POLICIES = Object.freeze(["break", "error"]);
16
17
  export const HORIZON_OVERFLOW_POLICIES = Object.freeze(["clip", "error"]);
17
18
 
18
- const ENCODING_PROPERTIES = Object.freeze(["field", "fieldType"]);
19
+ const ENCODING_PROPERTIES = Object.freeze(["field", "fieldType", "temporalUnit"]);
19
20
  const OUTPUT_PROPERTIES = Object.freeze([
20
21
  "x", "lower", "upper", "group", "color", "sign", "band", "segment"
21
22
  ]);
@@ -41,7 +42,8 @@ function validateEncoding(value, role) {
41
42
  `Horizon ${role} fieldType must be quantitative${supportsTemporal ? " or temporal" : ""}.`
42
43
  );
43
44
  }
44
- return { field, fieldType };
45
+ const temporalUnit = resolveTemporalUnit(value, fieldType);
46
+ return { field, fieldType, ...(temporalUnit === undefined ? {} : { temporalUnit }) };
45
47
  }
46
48
 
47
49
  function validateOutputFields(value) {
@@ -183,7 +185,7 @@ export function validateHorizonTransform(transform) {
183
185
 
184
186
  function orderedX(value, encoding, rowIndex) {
185
187
  if (encoding.fieldType === "temporal") {
186
- return normalizeTemporalValue(value, encoding.field, rowIndex);
188
+ return normalizeTemporalValue(value, encoding.field, rowIndex, encoding.temporalUnit);
187
189
  }
188
190
  if (!Number.isFinite(value)) {
189
191
  throw new TypeError(
@@ -1,7 +1,10 @@
1
1
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
2
  import { aggregateScalarValues } from "./aggregate.js";
3
3
  import { requireFiniteResult, stableDecimal } from "./numeric.js";
4
- import { studentTCriticalValue } from "./statistics/studentT.js";
4
+ import {
5
+ confidenceCriticalValue,
6
+ normalizeConfidenceInterval
7
+ } from "./statistics/confidenceInterval.js";
5
8
 
6
9
  const CENTER_VALUES = ["mean", "median"];
7
10
  const EXTENT_VALUES = ["stderr", "stdev", "ci", "iqr"];
@@ -11,6 +14,7 @@ const TRANSFORM_KEYS = [
11
14
  "groupBy",
12
15
  "center",
13
16
  "extent",
17
+ "method",
14
18
  "level",
15
19
  "as"
16
20
  ];
@@ -84,13 +88,12 @@ export function validateIntervalTransform(transform) {
84
88
  throw new Error("Median intervals require iqr, and iqr requires median.");
85
89
  }
86
90
  if (transform.extent === "ci") {
87
- if (
88
- !Number.isFinite(transform.level) ||
89
- transform.level <= 0 ||
90
- transform.level >= 1
91
- ) {
92
- throw new RangeError("Interval CI level must be between 0 and 1.");
93
- }
91
+ normalizeConfidenceInterval(transform, {
92
+ defaultMethod: "student-t",
93
+ label: "Interval CI"
94
+ });
95
+ } else if (transform.method !== undefined) {
96
+ throw new Error("Interval method is supported only for ci extent.");
94
97
  } else if (transform.level !== undefined) {
95
98
  throw new Error("Interval level is supported only for ci extent.");
96
99
  }
@@ -111,16 +114,27 @@ function normalizeGrouping(groupBy) {
111
114
  export function normalizeIntervalParameters({
112
115
  center = "mean",
113
116
  extent = "ci",
117
+ method,
114
118
  level
115
119
  } = {}) {
116
- const resolvedLevel = extent === "ci" ? level ?? 0.95 : level;
120
+ const confidence = extent === "ci"
121
+ ? normalizeConfidenceInterval({ method, level }, {
122
+ defaultMethod: "student-t",
123
+ label: "Interval CI"
124
+ })
125
+ : undefined;
117
126
  const candidate = {
118
127
  type: "interval",
119
128
  field: "__interval_input",
120
129
  groupBy: [],
121
130
  center,
122
131
  extent,
123
- ...(resolvedLevel === undefined ? {} : { level: resolvedLevel }),
132
+ ...(confidence === undefined
133
+ ? {
134
+ ...(method === undefined ? {} : { method }),
135
+ ...(level === undefined ? {} : { level })
136
+ }
137
+ : confidence),
124
138
  as: {
125
139
  center: "__interval_center",
126
140
  lower: "__interval_lower",
@@ -131,18 +145,16 @@ export function normalizeIntervalParameters({
131
145
  return cloneAndFreeze({
132
146
  center,
133
147
  extent,
134
- ...(resolvedLevel === undefined ? {} : { level: resolvedLevel })
148
+ ...(confidence === undefined ? {} : confidence)
135
149
  });
136
150
  }
137
151
 
138
152
  export function studentTCritical(degreesOfFreedom, level) {
139
- if (!Number.isInteger(degreesOfFreedom) || degreesOfFreedom < 1) {
140
- throw new RangeError("Student-t degreesOfFreedom must be a positive integer.");
141
- }
142
- if (!Number.isFinite(level) || level <= 0 || level >= 1) {
143
- throw new RangeError("Student-t level must be between 0 and 1.");
144
- }
145
- return studentTCriticalValue(level, degreesOfFreedom);
153
+ return confidenceCriticalValue({
154
+ method: "student-t",
155
+ level,
156
+ degreesOfFreedom
157
+ });
146
158
  }
147
159
 
148
160
  function isMissing(value) {
@@ -170,7 +182,11 @@ function deriveGroup(values, transform) {
170
182
  ? aggregateScalarValues(values, "stdev")
171
183
  : transform.extent === "stderr"
172
184
  ? aggregateScalarValues(values, "stderr")
173
- : studentTCritical(values.length - 1, transform.level) *
185
+ : confidenceCriticalValue({
186
+ method: transform.method,
187
+ level: transform.level,
188
+ degreesOfFreedom: values.length - 1
189
+ }) *
174
190
  aggregateScalarValues(values, "stderr");
175
191
  return {
176
192
  center,
@@ -232,12 +248,13 @@ export function normalizeIntervalTransform({
232
248
  groupBy,
233
249
  center,
234
250
  extent,
251
+ method,
235
252
  level,
236
253
  as
237
254
  }) {
238
255
  nonEmptyString(field, "Interval field");
239
256
  const grouping = normalizeGrouping(groupBy);
240
- const parameters = normalizeIntervalParameters({ center, extent, level });
257
+ const parameters = normalizeIntervalParameters({ center, extent, method, level });
241
258
  const transform = {
242
259
  type: "interval",
243
260
  field,
@@ -16,8 +16,9 @@ import {
16
16
  findHistogramBinIndex,
17
17
  resolveHistogramBins
18
18
  } from "./histogram.js";
19
-
20
- const SERIES_CHANNELS = ["group", "color", "strokeDash"];
19
+ import {
20
+ validatePathSeriesAppearance
21
+ } from "./pathSeries.js";
21
22
 
22
23
  function requireLineEncoding(layer) {
23
24
  if (layer?.mark?.type !== "line") {
@@ -70,32 +71,6 @@ function requireLineEncoding(layer) {
70
71
  };
71
72
  }
72
73
 
73
- function readSeriesFields(rows, layer) {
74
- const fields = [];
75
- const values = new Map();
76
-
77
- for (const channel of SERIES_CHANNELS) {
78
- const encoding = layer.encoding?.[channel];
79
-
80
- if (encoding?.field === undefined) continue;
81
- const categorical = channel === "color"
82
- ? ["nominal", "ordinal"].includes(encoding.fieldType)
83
- : encoding.fieldType === "nominal";
84
- if (!categorical) {
85
- throw new Error(
86
- `Line ${channel} encoding on mark "${layer.id}" must be categorical.`
87
- );
88
- }
89
-
90
- if (!values.has(encoding.field)) {
91
- fields.push(encoding.field);
92
- values.set(encoding.field, readNominalField(rows, encoding.field));
93
- }
94
- }
95
-
96
- return { fields, values };
97
- }
98
-
99
74
  function groupKey(values) {
100
75
  return JSON.stringify(values);
101
76
  }
@@ -148,14 +123,14 @@ function deriveCartesianLineSeries(rows, layer, options = {}) {
148
123
  validateAggregateFieldValues(rows, y.field, y.fieldType);
149
124
  }
150
125
  const xValues = x.fieldType === "temporal"
151
- ? readTemporalField(rows, x.field)
126
+ ? readTemporalField(rows, x.field, x.temporalUnit)
152
127
  : readQuantitativeField(rows, x.field);
153
128
  const yValues = isAggregate
154
129
  ? undefined
155
130
  : y.fieldType === "temporal"
156
- ? readTemporalField(rows, y.field)
131
+ ? readTemporalField(rows, y.field, y.temporalUnit)
157
132
  : readQuantitativeField(rows, y.field);
158
- const seriesFields = readSeriesFields(rows, layer);
133
+ const seriesFields = validatePathSeriesAppearance(rows, layer);
159
134
  const binBoundaries = binnedAggregate
160
135
  ? options.xBinBoundaries ?? resolveLineBins(rows, layer).boundaries
161
136
  : undefined;
@@ -170,10 +145,10 @@ function deriveCartesianLineSeries(rows, layer, options = {}) {
170
145
  const orderValues = readQuantitativeField(rows, pathOrder.field);
171
146
  const groups = new Map();
172
147
  for (let index = 0; index < rows.length; index += 1) {
173
- const dimensions = seriesFields.fields.map(
174
- field => seriesFields.values.get(field)[index]
148
+ const dimensions = seriesFields.map(
149
+ field => rows[index][field]
175
150
  );
176
- const series = groupedSeries(groups, seriesFields.fields, dimensions);
151
+ const series = groupedSeries(groups, seriesFields, dimensions);
177
152
  series.values.push({ x: xValues[index], y: yValues[index] });
178
153
  series.orderValues ??= [];
179
154
  series.orderValues.push(orderValues[index]);
@@ -197,10 +172,10 @@ function deriveCartesianLineSeries(rows, layer, options = {}) {
197
172
  if (directRows) {
198
173
  const groups = new Map();
199
174
  for (let index = 0; index < rows.length; index += 1) {
200
- const dimensions = seriesFields.fields.map(
201
- field => seriesFields.values.get(field)[index]
175
+ const dimensions = seriesFields.map(
176
+ field => rows[index][field]
202
177
  );
203
- const series = groupedSeries(groups, seriesFields.fields, dimensions);
178
+ const series = groupedSeries(groups, seriesFields, dimensions);
204
179
  series.values.push({ x: xValues[index], y: yValues[index] });
205
180
  }
206
181
  const orderBy = directTemporal && y.fieldType === "temporal" ? "y" : "x";
@@ -220,8 +195,8 @@ function deriveCartesianLineSeries(rows, layer, options = {}) {
220
195
  const aggregateGroups = new Map();
221
196
 
222
197
  for (let index = 0; index < rows.length; index += 1) {
223
- const dimensions = seriesFields.fields.map(
224
- field => seriesFields.values.get(field)[index]
198
+ const dimensions = seriesFields.map(
199
+ field => rows[index][field]
225
200
  );
226
201
  const binIndex = binnedAggregate
227
202
  ? findHistogramBinIndex(xValues[index], binBoundaries)
@@ -255,7 +230,7 @@ function deriveCartesianLineSeries(rows, layer, options = {}) {
255
230
  if (value === undefined) continue;
256
231
  const series = groupedSeries(
257
232
  seriesGroups,
258
- seriesFields.fields,
233
+ seriesFields,
259
234
  group.dimensions
260
235
  );
261
236
 
@@ -315,7 +290,7 @@ function readThetaValues(rows, encoding) {
315
290
  return readNominalField(rows, encoding.field);
316
291
  }
317
292
  return encoding.fieldType === "temporal"
318
- ? readTemporalField(rows, encoding.field)
293
+ ? readTemporalField(rows, encoding.field, encoding.temporalUnit)
319
294
  : readQuantitativeField(rows, encoding.field);
320
295
  }
321
296
 
@@ -341,14 +316,14 @@ export function derivePolarLineSeries(rows, layer, { thetaDomain } = {}) {
341
316
  const thetaValues = readThetaValues(rows, theta);
342
317
  const radiusValues = readQuantitativeField(rows, radius.field);
343
318
  const sortValues = thetaOrder(thetaValues, theta.fieldType, thetaDomain);
344
- const seriesFields = readSeriesFields(rows, layer);
319
+ const seriesFields = validatePathSeriesAppearance(rows, layer);
345
320
  const groups = new Map();
346
321
 
347
322
  for (let index = 0; index < rows.length; index += 1) {
348
- const dimensions = seriesFields.fields.map(
349
- field => seriesFields.values.get(field)[index]
323
+ const dimensions = seriesFields.map(
324
+ field => rows[index][field]
350
325
  );
351
- const series = groupedSeries(groups, seriesFields.fields, dimensions);
326
+ const series = groupedSeries(groups, seriesFields, dimensions);
352
327
  series.values.push({
353
328
  theta: thetaValues[index],
354
329
  radius: radiusValues[index],
@@ -384,21 +359,3 @@ export function deriveLineSeries(rows, layer, options) {
384
359
  ? derivePolarLineSeries(rows, layer, options)
385
360
  : deriveCartesianLineSeries(rows, layer, options);
386
361
  }
387
-
388
- export function deriveLineSeriesFieldValues(rows, layer, derived, field) {
389
- const values = readQuantitativeField(rows, field);
390
- if (values.some(value => value < 0)) {
391
- throw new RangeError(`Line strokeWidth field "${field}" cannot contain negative values.`);
392
- }
393
- return cloneAndFreeze(derived.series.map(series => {
394
- const matching = values.filter((_, index) => Object.entries(series.key)
395
- .every(([key, value]) => rows[index]?.[key] === value));
396
- const unique = [...new Set(matching)];
397
- if (unique.length !== 1) {
398
- throw new Error(
399
- `Line strokeWidth field "${field}" must have one value within each series.`
400
- );
401
- }
402
- return unique[0];
403
- }));
404
- }
@@ -1,7 +1,24 @@
1
1
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
2
  import { normalizeMarkSelector } from "./markSelection.js";
3
3
 
4
- const TRANSFORM_KEYS = Object.freeze(["type", "target", "selector"]);
4
+ const TRANSFORM_KEYS = Object.freeze([
5
+ "type", "target", "selector", "selectors"
6
+ ]);
7
+
8
+ export function markFilterSelectors(transform) {
9
+ const hasSelector = Object.hasOwn(transform, "selector");
10
+ const hasSelectors = Object.hasOwn(transform, "selectors");
11
+ if (hasSelector === hasSelectors) {
12
+ throw new Error(
13
+ "Mark filter transform requires exactly one of selector or selectors."
14
+ );
15
+ }
16
+ const selectors = hasSelector ? [transform.selector] : transform.selectors;
17
+ if (!Array.isArray(selectors) || selectors.length === 0) {
18
+ throw new TypeError("Mark filter selectors must be a non-empty array.");
19
+ }
20
+ return cloneAndFreeze(selectors.map(normalizeMarkSelector));
21
+ }
5
22
 
6
23
  export function validateMarkFilterTransform(transform) {
7
24
  if (!isPlainObject(transform)) {
@@ -19,14 +36,15 @@ export function validateMarkFilterTransform(transform) {
19
36
  if (typeof transform.target !== "string" || transform.target.length === 0) {
20
37
  throw new TypeError("Mark filter target must be a non-empty string.");
21
38
  }
22
- normalizeMarkSelector(transform.selector);
39
+ markFilterSelectors(transform);
23
40
  }
24
41
 
25
- export function normalizeMarkFilterTransform(target, selector) {
42
+ export function normalizeMarkFilterTransform(target, selectors) {
26
43
  const transform = {
27
44
  type: "markFilter",
28
45
  target,
29
- selector: normalizeMarkSelector(selector)
46
+ selectors: (Array.isArray(selectors) ? selectors : [selectors])
47
+ .map(normalizeMarkSelector)
30
48
  };
31
49
  validateMarkFilterTransform(transform);
32
50
  return cloneAndFreeze(transform);