ggaction 0.0.12 → 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 (281) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +2 -2
  3. package/knowledge/action-card.schema.json +1 -1
  4. package/knowledge/action-cards.json +1 -1
  5. package/knowledge/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 +10 -16
  65. package/src/actions/encodings/parallel.js +2 -8
  66. package/src/actions/encodings/position/apply.js +10 -4
  67. package/src/actions/encodings/position/index.js +43 -5
  68. package/src/actions/encodings/position/policies/arc.js +14 -0
  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 +67 -22
  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 +21 -10
  83. package/src/actions/errorBars/edit.js +355 -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 +106 -2
  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 +34 -3
  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/grammar/aggregate.js +66 -23
  195. package/src/grammar/arcs.js +58 -1
  196. package/src/grammar/areaEndpoints.js +46 -0
  197. package/src/grammar/areaSeries.js +87 -41
  198. package/src/grammar/bars/aggregate.js +33 -12
  199. package/src/grammar/bars/policy.js +9 -7
  200. package/src/grammar/bin.js +181 -0
  201. package/src/grammar/categoryOrder.js +34 -0
  202. package/src/grammar/computed.js +150 -0
  203. package/src/grammar/ecdf.js +198 -0
  204. package/src/grammar/facets/index.js +117 -6
  205. package/src/grammar/fold.js +123 -0
  206. package/src/grammar/horizon.js +6 -4
  207. package/src/grammar/interval.js +37 -20
  208. package/src/grammar/lineSeries.js +20 -63
  209. package/src/grammar/markFilter.js +22 -4
  210. package/src/grammar/markLabels.js +93 -0
  211. package/src/grammar/parallelAxisTitles.js +16 -0
  212. package/src/grammar/pathSeries.js +91 -0
  213. package/src/grammar/pointPacking.js +183 -0
  214. package/src/grammar/positionDatum.js +12 -0
  215. package/src/grammar/rects.js +9 -1
  216. package/src/grammar/regression/derive.js +4 -0
  217. package/src/grammar/regression/index.js +7 -2
  218. package/src/grammar/regression/models.js +22 -7
  219. package/src/grammar/regression/parameters.js +40 -13
  220. package/src/grammar/rotation.js +29 -0
  221. package/src/grammar/rules.js +7 -21
  222. package/src/grammar/scales/color.js +23 -1
  223. package/src/grammar/scales/colorConsumers.js +38 -0
  224. package/src/grammar/scales/definition.js +15 -1
  225. package/src/grammar/scales/discretized.js +11 -5
  226. package/src/grammar/scales/fields.js +38 -8
  227. package/src/grammar/scales/mapping.js +2 -0
  228. package/src/grammar/scales/radial.js +55 -0
  229. package/src/grammar/scales/types.js +1 -0
  230. package/src/grammar/schemas/semanticPath.js +17 -3
  231. package/src/grammar/stack.js +170 -0
  232. package/src/grammar/statistics/confidenceInterval.js +101 -0
  233. package/src/grammar/summary.js +183 -0
  234. package/src/grammar/text.js +22 -16
  235. package/src/grammar/timeUnit.js +21 -11
  236. package/src/grammar/transformTopology.js +6 -0
  237. package/src/grammar/transforms.js +36 -0
  238. package/src/grammar/valueFormat.js +112 -0
  239. package/src/grammar/window.js +13 -7
  240. package/src/layout/facets.js +41 -7
  241. package/src/layout/guideCollisions.js +12 -0
  242. package/src/layout/legend.js +2 -10
  243. package/src/layout/legendItems.js +102 -0
  244. package/src/layout/legendLane.js +100 -11
  245. package/src/materialization/bars/aggregate.js +38 -152
  246. package/src/materialization/bars/histogram.js +24 -0
  247. package/src/materialization/categorySlotOffset.js +40 -0
  248. package/src/materialization/compositionSnapshot.js +1 -1
  249. package/src/materialization/dependencies.js +57 -16
  250. package/src/materialization/encodings.js +1 -1
  251. package/src/materialization/facets.js +8 -1
  252. package/src/materialization/guides/layout.js +57 -0
  253. package/src/materialization/guides/resources.js +2 -2
  254. package/src/materialization/legends.js +10 -1
  255. package/src/materialization/marks/capabilities.js +20 -5
  256. package/src/materialization/marks/grouping.js +52 -0
  257. package/src/materialization/marks/index.js +58 -23
  258. package/src/materialization/marks/policies.js +2 -0
  259. package/src/materialization/rect.js +33 -33
  260. package/src/materialization/rowEncoding.js +13 -13
  261. package/src/materialization/scaleGuideDependencies.js +8 -5
  262. package/src/materialization/scales/map.js +1 -0
  263. package/src/materialization/scales/policies/arc.js +42 -0
  264. package/src/materialization/scales/policies/binnedPosition.js +7 -4
  265. package/src/materialization/scales/policies/series.js +2 -1
  266. package/src/materialization/scales/resolve.js +46 -14
  267. package/src/materialization/selection/compatibility.js +11 -0
  268. package/src/materialization/selection/items/bar.js +12 -68
  269. package/src/materialization/selection/items/common.js +8 -2
  270. package/src/materialization/selection/items/path.js +3 -13
  271. package/src/materialization/selection/items/rect.js +7 -4
  272. package/src/materialization/text.js +64 -5
  273. package/src/mcp/adapter.js +2 -2
  274. package/src/selectors/datasets.js +13 -0
  275. package/src/selectors/layers.js +3 -2
  276. package/src/selectors/scales.js +6 -0
  277. package/src/theme/defaults.js +39 -0
  278. package/types/basic.d.ts +11 -2
  279. package/types/index.d.ts +126 -0
  280. package/types/program.d.ts +1320 -135
  281. package/src/materialization/bars/grouped.js +0 -144
@@ -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);
@@ -0,0 +1,93 @@
1
+ import { aggregateRows } from "./aggregate.js";
2
+ import { BAR_GRAINS, resolveBarChannels, resolveBarGrain } from "./bars/policy.js";
3
+ import { stableFiniteSum } from "./numeric.js";
4
+
5
+ const CONTENTS = new Set(["category", "value", "share"]);
6
+ const NORMALIZATIONS = new Set(["source", "category"]);
7
+
8
+ export function validateMarkLabelContent(content) {
9
+ if (!CONTENTS.has(content)) throw new Error(`Unsupported text content "${content}".`);
10
+ return content;
11
+ }
12
+
13
+ export function validateMarkLabelNormalization(normalizeBy) {
14
+ if (!NORMALIZATIONS.has(normalizeBy)) throw new Error(`Unsupported text normalizeBy "${normalizeBy}".`);
15
+ return normalizeBy;
16
+ }
17
+
18
+ export function normalizeMarkLabelContent(source, { content, normalizeBy }) {
19
+ validateMarkLabelContent(content);
20
+ if (!["bar", "arc"].includes(source?.mark?.type)) {
21
+ throw new Error("Semantic text content requires a Bar or Arc source; use field or value for other marks.");
22
+ }
23
+ if (content !== "share" && normalizeBy !== undefined) {
24
+ throw new Error("Text normalizeBy is only supported with share content.");
25
+ }
26
+ const normalization = content === "share" ? normalizeBy ?? "source" : undefined;
27
+ if (normalization !== undefined) validateMarkLabelNormalization(normalization);
28
+ if (normalization === "category" && source.mark.type !== "bar") {
29
+ throw new Error("Category share normalization requires a Bar source.");
30
+ }
31
+ if (source.mark.type === "bar") {
32
+ const grain = resolveBarGrain(source);
33
+ if (grain === BAR_GRAINS.ranged) {
34
+ throw new Error("Ranged Bar text requires an explicit field or value.");
35
+ }
36
+ if (content === "category" && grain === BAR_GRAINS.histogram) {
37
+ throw new Error("Histogram categories are intervals; use an explicit text field or value.");
38
+ }
39
+ } else if (content === "category" && source.encoding?.theta?.fieldType === "quantitative") {
40
+ throw new Error("Category text requires categorical Arc theta.");
41
+ }
42
+ return { content, ...(normalization === undefined ? {} : { normalizeBy: normalization }) };
43
+ }
44
+
45
+ function itemValue(source, item) {
46
+ if (source.mark.type === "bar") {
47
+ if (resolveBarGrain(source) === BAR_GRAINS.histogram) return item.members.length;
48
+ const measure = source.encoding[resolveBarChannels(source).measure];
49
+ return measure.aggregate === "count" ? item.members.length
50
+ : aggregateRows(item.members, measure.field, measure.aggregate);
51
+ }
52
+ if (source.encoding?.radius !== undefined) return item.channels.radius;
53
+ const theta = source.encoding.theta;
54
+ if (theta.fieldType === "quantitative") return item.channels.theta;
55
+ return theta.aggregate === "count" ? item.members.length
56
+ : aggregateRows(item.members, theta.weight, "sum");
57
+ }
58
+
59
+ function categoryValue(source, item) {
60
+ return item.channels[source.mark.type === "arc" ? "theta" : resolveBarChannels(source).category];
61
+ }
62
+
63
+ function categoryKey(source, item) {
64
+ return JSON.stringify(resolveBarGrain(source) === BAR_GRAINS.histogram
65
+ ? [item.channels.x, item.channels.x2]
66
+ : [categoryValue(source, item)]);
67
+ }
68
+
69
+ export function resolveMarkLabelValues(source, items, options) {
70
+ const config = normalizeMarkLabelContent(source, options);
71
+ if (config.content === "category") return items.map(item => categoryValue(source, item));
72
+ const values = items.map(item => itemValue(source, item));
73
+ if (config.content === "value" || items.length === 0) return values;
74
+ if (values.some(value => !Number.isFinite(value) || value < 0)) {
75
+ throw new Error("Share labels require finite non-negative source values.");
76
+ }
77
+ const groups = new Map();
78
+ items.forEach((item, index) => {
79
+ const key = config.normalizeBy === "source" ? "source" : categoryKey(source, item);
80
+ const group = groups.get(key) ?? [];
81
+ group.push(index);
82
+ groups.set(key, group);
83
+ });
84
+ const shares = [];
85
+ for (const indices of groups.values()) {
86
+ const maximum = indices.reduce((max, index) => Math.max(max, values[index]), 0);
87
+ if (maximum === 0) throw new Error("Share label denominator must be positive.");
88
+ // Ratios remain defined even when the unscaled sum would overflow.
89
+ const total = stableFiniteSum(indices.map(index => values[index] / maximum), "Share label denominator");
90
+ for (const index of indices) shares[index] = (values[index] / maximum) / total;
91
+ }
92
+ return shares;
93
+ }
@@ -0,0 +1,16 @@
1
+ import { validateNonEmptyString, validateOptionObject } from "../core/validation.js";
2
+
3
+ export function validateParallelAxisTitles(value) {
4
+ if (!Array.isArray(value) || value.length === 0) {
5
+ throw new TypeError("Parallel axis titles must be a non-empty field/text array.");
6
+ }
7
+ const fields = new Set();
8
+ for (const title of value) {
9
+ validateOptionObject(title, ["field", "text"], "Parallel axis title");
10
+ validateNonEmptyString(title.field, "Parallel axis title field");
11
+ validateNonEmptyString(title.text, "Parallel axis title text");
12
+ if (fields.has(title.field)) throw new Error("Parallel axis titles require unique fields.");
13
+ fields.add(title.field);
14
+ }
15
+ return value;
16
+ }
@@ -0,0 +1,91 @@
1
+ import { validateNonEmptyString } from "../core/validation.js";
2
+ import { readNominalField, readQuantitativeField } from "./scales/fields.js";
3
+
4
+ export function normalizeGroupFields(value) {
5
+ const fields = Array.isArray(value) ? [...value] : [value];
6
+ for (const field of fields) validateNonEmptyString(field, "Path group field");
7
+ if (fields.length === 0 || new Set(fields).size !== fields.length) {
8
+ throw new Error("Path group fields must be non-empty and unique.");
9
+ }
10
+ return fields;
11
+ }
12
+
13
+ export function pathGroupFields(layer) {
14
+ const group = layer.encoding?.group;
15
+ if (group !== undefined) {
16
+ if (group.fieldType !== "nominal") {
17
+ throw new Error(`Path group on mark "${layer.id}" must be nominal.`);
18
+ }
19
+ if (Object.hasOwn(group, "field") && Object.hasOwn(group, "fields")) {
20
+ throw new Error("Path group requires exactly one of field or fields.");
21
+ }
22
+ return normalizeGroupFields(group.fields ?? group.field);
23
+ }
24
+ if (layer.mark?.type !== "line") return [];
25
+ const color = layer.encoding?.color?.field;
26
+ const dash = layer.encoding?.strokeDash?.field;
27
+ if (color !== undefined && dash !== undefined && color !== dash) {
28
+ throw new Error("Line color field must match strokeDash field unless encodeGroup defines the series.");
29
+ }
30
+ return color === undefined && dash === undefined ? [] : [color ?? dash];
31
+ }
32
+
33
+ export function readPathSeriesFields(rows, layer) {
34
+ const fields = pathGroupFields(layer);
35
+ for (const field of fields) readNominalField(rows, field);
36
+ return fields;
37
+ }
38
+
39
+ function seriesValueIndex(rows, fields, field, channel) {
40
+ const quantitative = channel === "strokeWidth" || channel === "opacity";
41
+ const values = quantitative
42
+ ? readQuantitativeField(rows, field)
43
+ : readNominalField(rows, field);
44
+ const result = new Map();
45
+ rows.forEach((row, index) => {
46
+ const key = JSON.stringify(fields.map(field => row[field]));
47
+ const value = values[index];
48
+ if (channel === "strokeWidth" && value < 0) {
49
+ throw new RangeError(`Line strokeWidth field "${field}" cannot contain negative values.`);
50
+ }
51
+ if (result.has(key) && result.get(key) !== value) {
52
+ throw new Error(`Path ${channel} field "${field}" must have one value within each series.`);
53
+ }
54
+ result.set(key, value);
55
+ });
56
+ return result;
57
+ }
58
+
59
+ export function derivePathSeriesFieldValues(rows, series, field, channel) {
60
+ const fields = Object.keys(series[0]?.key ?? {});
61
+ const values = seriesValueIndex(rows, fields, field, channel);
62
+ return series.map(item => values.get(JSON.stringify(Object.values(item.key))));
63
+ }
64
+
65
+ export function validatePathSeriesAppearance(rows, layer) {
66
+ if (!["line", "area"].includes(layer.mark?.type) ||
67
+ layer.encoding?.parallel !== undefined) return;
68
+ const grouping = readPathSeriesFields(rows, layer);
69
+ const channels = layer.mark.type === "line"
70
+ ? ["color", "strokeDash", "strokeWidth", "opacity"] : ["color"];
71
+ for (const channel of channels) {
72
+ const encoding = layer.encoding?.[channel];
73
+ if (encoding?.field === undefined) continue;
74
+ const allowed = channel === "color" ? ["nominal", "ordinal"]
75
+ : channel === "strokeDash" ? ["nominal"] : ["quantitative"];
76
+ if (!allowed.includes(encoding.fieldType)) {
77
+ throw new Error(`Path ${channel} encoding has an unsupported field type.`);
78
+ }
79
+ seriesValueIndex(rows, grouping, encoding.field, channel);
80
+ }
81
+ return grouping;
82
+ }
83
+
84
+ export function readSeriesIdentity(rows, layer) {
85
+ const group = layer.encoding?.group;
86
+ const fields = group === undefined ? [] : normalizeGroupFields(group.fields ?? group.field);
87
+ for (const field of fields) readNominalField(rows, field);
88
+ const values = rows.map(row => fields.length === 0 ? undefined : fields.length === 1
89
+ ? row[fields[0]] : JSON.stringify(fields.map(field => row[field])));
90
+ return { fields, values, domain: [...new Set(values)] };
91
+ }
@@ -0,0 +1,183 @@
1
+ import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
+ import { validateNonEmptyString } from "../core/validation.js";
3
+ import { resolveJitterSlotWidth } from "./jitter.js";
4
+
5
+ export const POINT_PACKING_ALGORITHM = "point-pack-greedy-v1";
6
+
7
+ export function normalizePointPackingPolicy(options = {}) {
8
+ if (!isPlainObject(options)) {
9
+ throw new TypeError("Point packing options must be a plain object.");
10
+ }
11
+ if (!["x", "y"].includes(options.channel)) {
12
+ throw new Error('Point packing channel must be "x" or "y".');
13
+ }
14
+ let maxOffset;
15
+ if (options.maxOffset !== undefined) {
16
+ if (!isPlainObject(options.maxOffset)) {
17
+ throw new TypeError("Point packing maxOffset must be a plain object.");
18
+ }
19
+ const keys = Object.keys(options.maxOffset);
20
+ if (keys.length !== 1 || !["pixels", "band"].includes(keys[0])) {
21
+ throw new Error("Point packing maxOffset requires exactly one of pixels or band.");
22
+ }
23
+ const value = options.maxOffset[keys[0]];
24
+ if (!Number.isFinite(value) || value <= 0) {
25
+ throw new RangeError(`Point packing ${keys[0]} must be positive and finite.`);
26
+ }
27
+ if (keys[0] === "band" && value > 0.5) {
28
+ throw new RangeError("Point packing band must be at most 0.5.");
29
+ }
30
+ maxOffset = { [keys[0]]: value };
31
+ }
32
+ const padding = options.padding ?? 1;
33
+ if (!Number.isFinite(padding) || padding < 0) {
34
+ throw new RangeError("Point packing padding must be non-negative and finite.");
35
+ }
36
+ const key = options.key === undefined
37
+ ? undefined
38
+ : validateNonEmptyString(options.key, "Point packing key");
39
+ const overflow = options.overflow ?? "error";
40
+ if (!["error", "overlap"].includes(overflow)) {
41
+ throw new Error('Point packing overflow must be "error" or "overlap".');
42
+ }
43
+ return cloneAndFreeze({
44
+ algorithm: POINT_PACKING_ALGORITHM,
45
+ channel: options.channel,
46
+ ...(maxOffset === undefined ? {} : { maxOffset }),
47
+ padding,
48
+ ...(key === undefined ? {} : { key }),
49
+ overflow
50
+ });
51
+ }
52
+
53
+ function offsetLimit(policy, slotWidth) {
54
+ if (policy.maxOffset?.pixels !== undefined) return policy.maxOffset.pixels;
55
+ if (policy.maxOffset?.band !== undefined) return slotWidth * policy.maxOffset.band;
56
+ return slotWidth / 2;
57
+ }
58
+
59
+ function candidateOffsets(entry, placed, minimum, maximum, padding) {
60
+ const candidates = [0, minimum - entry.base, maximum - entry.base];
61
+ for (const other of placed) {
62
+ if (Math.abs(entry.fixed - other.fixed) >=
63
+ entry.fixedHalfExtent + other.fixedHalfExtent + padding) continue;
64
+ const distance = entry.halfExtent + other.halfExtent + padding;
65
+ candidates.push(other.final - entry.base - distance, other.final - entry.base + distance);
66
+ }
67
+ return [...new Set(candidates
68
+ .map(value => Math.max(minimum - entry.base, Math.min(maximum - entry.base, value)))
69
+ .map(value => Object.is(value, -0) ? 0 : value))]
70
+ .sort((left, right) => Math.abs(left) - Math.abs(right) || left - right);
71
+ }
72
+
73
+ function collisions(entry, final, placed, padding) {
74
+ return placed.filter(other =>
75
+ Math.abs(entry.fixed - other.fixed) <
76
+ entry.fixedHalfExtent + other.fixedHalfExtent + padding &&
77
+ Math.abs(final - other.final) < entry.halfExtent + other.halfExtent + padding
78
+ );
79
+ }
80
+
81
+ export function resolvePointPacking({
82
+ target,
83
+ policy,
84
+ scale,
85
+ entries,
86
+ plotMinimum,
87
+ plotMaximum
88
+ }) {
89
+ validateNonEmptyString(target, "Point packing target");
90
+ if (!Array.isArray(entries)) throw new TypeError("Point packing entries must be an array.");
91
+ if (!Number.isFinite(plotMinimum) || !Number.isFinite(plotMaximum) || plotMaximum < plotMinimum) {
92
+ throw new RangeError("Point packing plot bounds are invalid.");
93
+ }
94
+ const slotWidth = resolveJitterSlotWidth(scale);
95
+ if (slotWidth === undefined) {
96
+ throw new Error("Point packing requires a categorical position scale with a positive slot.");
97
+ }
98
+ const maximumOffset = offsetLimit(policy, slotWidth);
99
+ const seenIndices = new Set();
100
+ const seenIdentities = new Set();
101
+ const ordered = entries.map((entry, offset) => {
102
+ for (const field of ["index", "base", "fixed", "halfExtent", "fixedHalfExtent"]) {
103
+ if (!Number.isFinite(entry[field])) {
104
+ throw new TypeError(`Point packing entry ${offset} ${field} must be finite.`);
105
+ }
106
+ }
107
+ if (!Number.isInteger(entry.index) || entry.index < 0 ||
108
+ entry.halfExtent < 0 || entry.fixedHalfExtent < 0) {
109
+ throw new RangeError(`Point packing entry ${offset} geometry is invalid.`);
110
+ }
111
+ validateNonEmptyString(entry.identity, `Point packing entry ${offset} identity`);
112
+ if (seenIndices.has(entry.index)) {
113
+ throw new Error(`Point packing entry index ${entry.index} must be unique.`);
114
+ }
115
+ if (seenIdentities.has(entry.identity)) {
116
+ throw new Error(`Point packing entry identity "${entry.identity}" must be unique.`);
117
+ }
118
+ seenIndices.add(entry.index);
119
+ seenIdentities.add(entry.identity);
120
+ return { ...entry };
121
+ }).sort((left, right) =>
122
+ left.fixed - right.fixed ||
123
+ (left.identity < right.identity ? -1 : left.identity > right.identity ? 1 : 0)
124
+ );
125
+ const placed = [];
126
+ for (const entry of ordered) {
127
+ const minimum = Math.max(
128
+ plotMinimum + entry.halfExtent,
129
+ entry.base - slotWidth / 2 + entry.halfExtent,
130
+ entry.base - maximumOffset
131
+ );
132
+ const maximum = Math.min(
133
+ plotMaximum - entry.halfExtent,
134
+ entry.base + slotWidth / 2 - entry.halfExtent,
135
+ entry.base + maximumOffset
136
+ );
137
+ if (minimum > maximum) {
138
+ if (policy.overflow === "error") {
139
+ throw new RangeError(`Point packing for "${target}" has no available category-slot position.`);
140
+ }
141
+ placed.push({ ...entry, final: entry.base, offset: 0, unresolved: true, collisionCount: 0 });
142
+ continue;
143
+ }
144
+ const candidates = candidateOffsets(entry, placed, minimum, maximum, policy.padding)
145
+ .map(offset => ({
146
+ offset,
147
+ final: entry.base + offset,
148
+ collisions: collisions(entry, entry.base + offset, placed, policy.padding)
149
+ }));
150
+ const resolved = candidates.find(candidate => candidate.collisions.length === 0);
151
+ if (resolved !== undefined) {
152
+ placed.push({ ...entry, final: resolved.final, offset: resolved.offset,
153
+ unresolved: false, collisionCount: 0 });
154
+ continue;
155
+ }
156
+ if (policy.overflow === "error") {
157
+ throw new RangeError(`Point packing for "${target}" cannot avoid glyph overlap.`);
158
+ }
159
+ const best = candidates.sort((left, right) =>
160
+ left.collisions.length - right.collisions.length ||
161
+ Math.abs(left.offset) - Math.abs(right.offset) || left.offset - right.offset
162
+ )[0];
163
+ placed.push({ ...entry, final: best.final, offset: best.offset,
164
+ unresolved: true, collisionCount: best.collisions.length });
165
+ }
166
+ const items = placed.sort((left, right) => left.index - right.index).map(item => ({
167
+ index: item.index,
168
+ identity: item.identity,
169
+ base: item.base,
170
+ final: item.final,
171
+ finalOffset: item.offset,
172
+ unresolved: item.unresolved,
173
+ collisionCount: item.collisionCount
174
+ }));
175
+ return cloneAndFreeze({
176
+ algorithm: POINT_PACKING_ALGORITHM,
177
+ slotWidth,
178
+ maximumOffset,
179
+ itemCount: items.length,
180
+ unresolvedItemCount: items.filter(item => item.unresolved).length,
181
+ items
182
+ });
183
+ }
@@ -0,0 +1,12 @@
1
+ import { isNominalValue, normalizeTemporalValue, validateSemanticFieldType } from "./scales/index.js";
2
+
3
+ export function normalizePositionDatum(value, fieldType, channel, temporalUnit, mark = "Position") {
4
+ validateSemanticFieldType(fieldType);
5
+ if (fieldType === "quantitative") {
6
+ if (!Number.isFinite(value)) throw new TypeError(`${mark} ${channel} datum must be a finite number.`);
7
+ return value;
8
+ }
9
+ if (fieldType === "temporal") return normalizeTemporalValue(value, `${channel} datum`, 0, temporalUnit);
10
+ if (!isNominalValue(value)) throw new TypeError(`${mark} ${channel} datum must be a nominal value.`);
11
+ return value;
12
+ }
@@ -1,6 +1,8 @@
1
1
  export const RECT_MODES = Object.freeze({
2
2
  discrete: "discrete",
3
- ranged: "ranged"
3
+ ranged: "ranged",
4
+ xSpan: "x-span",
5
+ ySpan: "y-span"
4
6
  });
5
7
 
6
8
  function categorical(encoding) {
@@ -22,5 +24,11 @@ export function resolveRectMode(layer) {
22
24
  return RECT_MODES.discrete;
23
25
  }
24
26
  if (ranged(x, x2) && ranged(y, y2)) return RECT_MODES.ranged;
27
+ if (ranged(x, x2) && y === undefined && y2 === undefined) return RECT_MODES.xSpan;
28
+ if (ranged(y, y2) && x === undefined && x2 === undefined) return RECT_MODES.ySpan;
25
29
  return undefined;
26
30
  }
31
+
32
+ export function rectUsesFields(layer) {
33
+ return ["x", "y", "x2", "y2", "color"].some(channel => Object.hasOwn(layer.encoding?.[channel] ?? {}, "field"));
34
+ }
@@ -24,6 +24,8 @@ export function deriveRegression(values, {
24
24
  method,
25
25
  degree,
26
26
  span,
27
+ confidenceMethod,
28
+ level,
27
29
  confidence,
28
30
  interval
29
31
  } = {}) {
@@ -39,6 +41,8 @@ export function deriveRegression(values, {
39
41
  method,
40
42
  degree,
41
43
  span,
44
+ confidenceMethod,
45
+ level,
42
46
  confidence,
43
47
  interval
44
48
  });
@@ -1,4 +1,5 @@
1
- import { studentTCriticalValue } from "../statistics/studentT.js";
1
+ import { confidenceCriticalValue } from
2
+ "../statistics/confidenceInterval.js";
2
3
 
3
4
  export {
4
5
  REGRESSION_LOWER_FIELD,
@@ -18,5 +19,9 @@ export function studentTCritical(confidence, degreesOfFreedom) {
18
19
  if (!Number.isInteger(degreesOfFreedom) || degreesOfFreedom <= 0) {
19
20
  throw new RangeError("Student-t degrees of freedom must be positive.");
20
21
  }
21
- return studentTCriticalValue(confidence, degreesOfFreedom);
22
+ return confidenceCriticalValue({
23
+ method: "student-t",
24
+ level: confidence,
25
+ degreesOfFreedom
26
+ });
22
27
  }