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,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
+ }
@@ -16,7 +16,7 @@ export const POSITION_FIELD_COMPATIBILITY = Object.freeze({
16
16
  y: Object.freeze(["quantitative", "temporal"])
17
17
  }),
18
18
  arc: Object.freeze({
19
- theta: Object.freeze(["ordinal", "nominal"]),
19
+ theta: Object.freeze(["quantitative", "ordinal", "nominal"]),
20
20
  radius: Object.freeze(["quantitative"])
21
21
  }),
22
22
  rule: Object.freeze({
@@ -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
  }
@@ -1,4 +1,5 @@
1
- import { studentTCriticalValue } from "../statistics/studentT.js";
1
+ import { confidenceCriticalValue } from
2
+ "../statistics/confidenceInterval.js";
2
3
  import {
3
4
  maximumMagnitude,
4
5
  requireFiniteResult,
@@ -56,7 +57,9 @@ function linearPrediction(model, xValue) {
56
57
  );
57
58
  }
58
59
 
59
- function fitLinearGroup(rows, { x, y, group, confidence }) {
60
+ function fitLinearGroup(rows, {
61
+ x, y, group, confidenceMethod, level
62
+ }) {
60
63
  const count = rows.length;
61
64
  const groupLabel = group === undefined ? "all" : String(group);
62
65
  const label = `Regression group "${groupLabel}"`;
@@ -152,7 +155,11 @@ function fitLinearGroup(rows, { x, y, group, confidence }) {
152
155
  return Object.assign(model, {
153
156
  residualSumSquares,
154
157
  residualStandardError: Math.sqrt(residualSumSquares / degreesOfFreedom),
155
- critical: studentTCriticalValue(confidence, degreesOfFreedom)
158
+ critical: confidenceCriticalValue({
159
+ method: confidenceMethod,
160
+ level,
161
+ degreesOfFreedom
162
+ })
156
163
  });
157
164
  }
158
165
 
@@ -230,7 +237,9 @@ function polynomialResponse(design, values, size, scale = 1) {
230
237
  ));
231
238
  }
232
239
 
233
- function fitPolynomialGroup(rows, { x, y, group, confidence, degree }) {
240
+ function fitPolynomialGroup(rows, {
241
+ x, y, group, confidenceMethod, level, degree
242
+ }) {
234
243
  const count = rows.length;
235
244
  const parameterCount = degree + 1;
236
245
  const groupLabel = group === undefined ? "all" : String(group);
@@ -310,7 +319,11 @@ function fitPolynomialGroup(rows, { x, y, group, confidence, degree }) {
310
319
  inverse,
311
320
  residualSumSquares,
312
321
  residualStandardError: Math.sqrt(residualVariance),
313
- critical: studentTCriticalValue(confidence, degreesOfFreedom)
322
+ critical: confidenceCriticalValue({
323
+ method: confidenceMethod,
324
+ level,
325
+ degreesOfFreedom
326
+ })
314
327
  };
315
328
  if (!normalizedXByDifference) model.normalizedXByDifference = false;
316
329
  if (stableResponse) Object.assign(model, { responseScale, scaledCoefficients });
@@ -416,7 +429,8 @@ export function fitRegressionGroup(rows, { x, y, group, parameters }) {
416
429
  x,
417
430
  y,
418
431
  group,
419
- confidence: parameters.confidence
432
+ confidenceMethod: parameters.confidenceMethod,
433
+ level: parameters.level
420
434
  });
421
435
  }
422
436
  if (parameters.method === "polynomial") {
@@ -424,7 +438,8 @@ export function fitRegressionGroup(rows, { x, y, group, parameters }) {
424
438
  x,
425
439
  y,
426
440
  group,
427
- confidence: parameters.confidence,
441
+ confidenceMethod: parameters.confidenceMethod,
442
+ level: parameters.level,
428
443
  degree: parameters.degree
429
444
  });
430
445
  }
@@ -1,5 +1,7 @@
1
1
  import { cloneAndFreeze } from "../../core/immutable.js";
2
2
  import { validateGeneratedItemLimit } from "../../core/validation.js";
3
+ import { normalizeConfidenceInterval } from
4
+ "../statistics/confidenceInterval.js";
3
5
 
4
6
  const MAX_POLYNOMIAL_DEGREE = 32;
5
7
 
@@ -14,6 +16,8 @@ export function normalizeRegressionParameters({
14
16
  method = "linear",
15
17
  degree,
16
18
  span,
19
+ confidenceMethod,
20
+ level,
17
21
  confidence,
18
22
  interval
19
23
  } = {}) {
@@ -24,7 +28,10 @@ export function normalizeRegressionParameters({
24
28
  if (degree !== undefined) {
25
29
  throw new Error("Regression degree requires the polynomial method.");
26
30
  }
27
- if (confidence !== undefined || interval !== undefined) {
31
+ if (
32
+ confidenceMethod !== undefined || level !== undefined ||
33
+ confidence !== undefined || interval !== undefined
34
+ ) {
28
35
  throw new Error("LOESS regression does not support confidence intervals.");
29
36
  }
30
37
  const resolvedSpan = span ?? 0.75;
@@ -38,14 +45,18 @@ export function normalizeRegressionParameters({
38
45
  if (span !== undefined) {
39
46
  throw new Error("Regression span requires the loess method.");
40
47
  }
41
- const resolvedConfidence = confidence ?? 0.95;
42
- if (
43
- !Number.isFinite(resolvedConfidence) ||
44
- resolvedConfidence <= 0 ||
45
- resolvedConfidence >= 1
46
- ) {
47
- throw new RangeError("Regression confidence must be between 0 and 1.");
48
+ if (level !== undefined && confidence !== undefined && level !== confidence) {
49
+ throw new Error(
50
+ "Regression level and confidence alias must match when both are provided."
51
+ );
48
52
  }
53
+ const confidenceInterval = normalizeConfidenceInterval({
54
+ method: confidenceMethod,
55
+ level: level ?? confidence
56
+ }, {
57
+ defaultMethod: "student-t",
58
+ label: "Regression confidence interval"
59
+ });
49
60
  const resolvedInterval = interval ?? "mean";
50
61
  if (!["mean", "prediction"].includes(resolvedInterval)) {
51
62
  throw new Error(`Unsupported regression interval "${resolvedInterval}".`);
@@ -65,7 +76,8 @@ export function normalizeRegressionParameters({
65
76
  return cloneAndFreeze({
66
77
  method,
67
78
  degree: resolvedDegree,
68
- confidence: resolvedConfidence,
79
+ confidenceMethod: confidenceInterval.method,
80
+ level: confidenceInterval.level,
69
81
  interval: resolvedInterval
70
82
  });
71
83
  }
@@ -74,14 +86,16 @@ export function normalizeRegressionParameters({
74
86
  }
75
87
  return cloneAndFreeze({
76
88
  method,
77
- confidence: resolvedConfidence,
89
+ confidenceMethod: confidenceInterval.method,
90
+ level: confidenceInterval.level,
78
91
  interval: resolvedInterval
79
92
  });
80
93
  }
81
94
 
82
95
  export function validateRegressionTransform(transform) {
83
96
  const supported = [
84
- "type", "method", "x", "y", "groupBy", "confidence", "interval",
97
+ "type", "method", "x", "y", "groupBy", "confidenceMethod", "level",
98
+ "confidence", "interval",
85
99
  "degree", "span"
86
100
  ];
87
101
  const unknown = Object.keys(transform).find(key => !supported.includes(key));
@@ -96,16 +110,29 @@ export function validateRegressionTransform(transform) {
96
110
  if (transform.groupBy !== undefined) {
97
111
  requireRegressionField(transform.groupBy, "Regression groupBy field");
98
112
  }
113
+ if (
114
+ transform.confidence !== undefined &&
115
+ (transform.confidenceMethod !== undefined || transform.level !== undefined)
116
+ ) {
117
+ throw new Error(
118
+ "Regression confidence provenance must use either the legacy confidence field or method and level."
119
+ );
120
+ }
99
121
  const normalized = normalizeRegressionParameters({
100
122
  method: transform.method,
101
123
  degree: transform.degree,
102
124
  span: transform.span,
125
+ confidenceMethod: transform.confidenceMethod,
126
+ level: transform.level,
103
127
  confidence: transform.confidence,
104
128
  interval: transform.interval
105
129
  });
106
130
  if (normalized.method === "loess") return transform;
107
- if (!Number.isFinite(transform.confidence)) {
108
- throw new RangeError("Regression confidence must be between 0 and 1.");
131
+ if (
132
+ transform.confidence === undefined &&
133
+ (transform.confidenceMethod === undefined || transform.level === undefined)
134
+ ) {
135
+ throw new Error("Regression confidence provenance requires method and level.");
109
136
  }
110
137
  if (transform.interval === undefined) {
111
138
  throw new Error(`Unsupported regression interval "${transform.interval}".`);