ggaction 0.0.1

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 (222) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/LICENSE +21 -0
  3. package/README.md +108 -0
  4. package/package.json +84 -0
  5. package/src/AGENTS.md +119 -0
  6. package/src/ChartProgram.js +10 -0
  7. package/src/actions/boxPlots/components.js +88 -0
  8. package/src/actions/boxPlots/create.js +84 -0
  9. package/src/actions/boxPlots/index.js +10 -0
  10. package/src/actions/boxPlots/materialize.js +183 -0
  11. package/src/actions/boxPlots/options.js +147 -0
  12. package/src/actions/boxPlots/resolve.js +38 -0
  13. package/src/actions/canvas/actions.js +144 -0
  14. package/src/actions/canvas/index.js +6 -0
  15. package/src/actions/coordinates/actions.js +85 -0
  16. package/src/actions/coordinates/index.js +5 -0
  17. package/src/actions/data/box.js +40 -0
  18. package/src/actions/data/create.js +33 -0
  19. package/src/actions/data/density.js +62 -0
  20. package/src/actions/data/derived.js +52 -0
  21. package/src/actions/data/filter.js +168 -0
  22. package/src/actions/data/index.js +32 -0
  23. package/src/actions/data/interval.js +69 -0
  24. package/src/actions/data/regression.js +71 -0
  25. package/src/actions/data/shared.js +25 -0
  26. package/src/actions/encodings/appearance.js +243 -0
  27. package/src/actions/encodings/barWidth.js +60 -0
  28. package/src/actions/encodings/color.js +329 -0
  29. package/src/actions/encodings/density.js +225 -0
  30. package/src/actions/encodings/histogram.js +50 -0
  31. package/src/actions/encodings/index.js +23 -0
  32. package/src/actions/encodings/offset.js +104 -0
  33. package/src/actions/encodings/position/policies/area.js +24 -0
  34. package/src/actions/encodings/position/policies/bar.js +110 -0
  35. package/src/actions/encodings/position/policies/common.js +17 -0
  36. package/src/actions/encodings/position/policies/index.js +25 -0
  37. package/src/actions/encodings/position/policies/line.js +74 -0
  38. package/src/actions/encodings/position/policies/point.js +19 -0
  39. package/src/actions/encodings/position/policies/rule.js +17 -0
  40. package/src/actions/encodings/position/resolve.js +170 -0
  41. package/src/actions/encodings/position.js +176 -0
  42. package/src/actions/encodings/ranged.js +276 -0
  43. package/src/actions/encodings/ruleAppearance.js +47 -0
  44. package/src/actions/encodings/shared.js +164 -0
  45. package/src/actions/encodings/strokeDash.js +145 -0
  46. package/src/actions/errorBands/components.js +81 -0
  47. package/src/actions/errorBands/create.js +159 -0
  48. package/src/actions/errorBands/index.js +7 -0
  49. package/src/actions/errorBands/resolve.js +28 -0
  50. package/src/actions/errorBars/components.js +62 -0
  51. package/src/actions/errorBars/create.js +144 -0
  52. package/src/actions/errorBars/index.js +7 -0
  53. package/src/actions/errorBars/resolve.js +25 -0
  54. package/src/actions/guides/axes/axes.js +204 -0
  55. package/src/actions/guides/axes/axis.js +131 -0
  56. package/src/actions/guides/axes/index.js +17 -0
  57. package/src/actions/guides/axes/labels.js +231 -0
  58. package/src/actions/guides/axes/lines.js +149 -0
  59. package/src/actions/guides/axes/policy.js +155 -0
  60. package/src/actions/guides/axes/tickGroups.js +149 -0
  61. package/src/actions/guides/axes/ticks.js +179 -0
  62. package/src/actions/guides/axes/titles.js +264 -0
  63. package/src/actions/guides/grids/grid.js +228 -0
  64. package/src/actions/guides/grids/index.js +1 -0
  65. package/src/actions/guides/grids/resolve.js +258 -0
  66. package/src/actions/guides/guides.js +138 -0
  67. package/src/actions/guides/index.js +11 -0
  68. package/src/actions/guides/legends/categorical/actions.js +329 -0
  69. package/src/actions/guides/legends/categorical/components.js +140 -0
  70. package/src/actions/guides/legends/categorical/index.js +49 -0
  71. package/src/actions/guides/legends/categorical/layout.js +445 -0
  72. package/src/actions/guides/legends/categorical/options.js +188 -0
  73. package/src/actions/guides/legends/categorical/recipes.js +120 -0
  74. package/src/actions/guides/legends/categorical/resolve.js +131 -0
  75. package/src/actions/guides/legends/categorical/symbols.js +280 -0
  76. package/src/actions/guides/legends/categorical/validation.js +27 -0
  77. package/src/actions/guides/legends/continuous/common.js +273 -0
  78. package/src/actions/guides/legends/continuous/gradient.js +363 -0
  79. package/src/actions/guides/legends/continuous/index.js +25 -0
  80. package/src/actions/guides/legends/continuous/interval.js +234 -0
  81. package/src/actions/guides/legends/continuous/opacity.js +382 -0
  82. package/src/actions/guides/legends/edit.js +314 -0
  83. package/src/actions/guides/legends/index.js +9 -0
  84. package/src/actions/guides/legends/size.js +220 -0
  85. package/src/actions/guides/tickValues.js +84 -0
  86. package/src/actions/index.js +31 -0
  87. package/src/actions/intervals/resolve.js +352 -0
  88. package/src/actions/marks/area.js +369 -0
  89. package/src/actions/marks/bar/create.js +46 -0
  90. package/src/actions/marks/bar/edit.js +90 -0
  91. package/src/actions/marks/bar/index.js +9 -0
  92. package/src/actions/marks/bar/materialize.js +130 -0
  93. package/src/actions/marks/index.js +13 -0
  94. package/src/actions/marks/line.js +248 -0
  95. package/src/actions/marks/point.js +420 -0
  96. package/src/actions/marks/rule.js +287 -0
  97. package/src/actions/marks/shared.js +112 -0
  98. package/src/actions/primitives/createGraphics.js +163 -0
  99. package/src/actions/primitives/editGraphics.js +208 -0
  100. package/src/actions/primitives/index.js +9 -0
  101. package/src/actions/primitives/semantic.js +168 -0
  102. package/src/actions/primitives/semanticValue.js +182 -0
  103. package/src/actions/regression/components.js +216 -0
  104. package/src/actions/regression/create.js +102 -0
  105. package/src/actions/regression/index.js +23 -0
  106. package/src/actions/regression/resolve.js +57 -0
  107. package/src/actions/scales/consumers.js +283 -0
  108. package/src/actions/scales/create.js +148 -0
  109. package/src/actions/scales/definitions.js +319 -0
  110. package/src/actions/scales/edit.js +290 -0
  111. package/src/actions/scales/index.js +11 -0
  112. package/src/actions/scales/materialize.js +466 -0
  113. package/src/actions/scales/quantitativeColor.js +45 -0
  114. package/src/actions/selection/actions.js +384 -0
  115. package/src/actions/selection/index.js +23 -0
  116. package/src/actions/titles/actions.js +235 -0
  117. package/src/actions/titles/index.js +19 -0
  118. package/src/actions/titles/resolve.js +321 -0
  119. package/src/core/ChartProgram.js +316 -0
  120. package/src/core/action.js +120 -0
  121. package/src/core/identifiers.js +28 -0
  122. package/src/core/immutable.js +94 -0
  123. package/src/core/specs.js +29 -0
  124. package/src/core/textMetrics.js +23 -0
  125. package/src/core/validation.js +54 -0
  126. package/src/core/vocabulary.js +58 -0
  127. package/src/extension.js +2 -0
  128. package/src/grammar/aggregate.js +259 -0
  129. package/src/grammar/areaSeries.js +217 -0
  130. package/src/grammar/bars/aggregate.js +98 -0
  131. package/src/grammar/bars/geometry.js +54 -0
  132. package/src/grammar/bars/policy.js +90 -0
  133. package/src/grammar/boxPlot.js +140 -0
  134. package/src/grammar/coordinates.js +25 -0
  135. package/src/grammar/curveCommands.js +356 -0
  136. package/src/grammar/density.js +296 -0
  137. package/src/grammar/filter.js +169 -0
  138. package/src/grammar/histogram.js +332 -0
  139. package/src/grammar/interval.js +343 -0
  140. package/src/grammar/lineSeries.js +197 -0
  141. package/src/grammar/markFilter.js +46 -0
  142. package/src/grammar/markSelection.js +326 -0
  143. package/src/grammar/palettes.js +208 -0
  144. package/src/grammar/pathCommands.js +86 -0
  145. package/src/grammar/pointShapes.js +164 -0
  146. package/src/grammar/positionCompatibility.js +32 -0
  147. package/src/grammar/regression.js +578 -0
  148. package/src/grammar/ruleAppearance.js +12 -0
  149. package/src/grammar/rules.js +90 -0
  150. package/src/grammar/scales/appearance.js +185 -0
  151. package/src/grammar/scales/color.js +349 -0
  152. package/src/grammar/scales/continuous.js +111 -0
  153. package/src/grammar/scales/definition.js +153 -0
  154. package/src/grammar/scales/discretized.js +159 -0
  155. package/src/grammar/scales/fields.js +133 -0
  156. package/src/grammar/scales/index.js +12 -0
  157. package/src/grammar/scales/mapping.js +27 -0
  158. package/src/grammar/scales/ordinal.js +221 -0
  159. package/src/grammar/scales/policies.js +50 -0
  160. package/src/grammar/scales/temporal.js +53 -0
  161. package/src/grammar/scales/transformed.js +236 -0
  162. package/src/grammar/scales/types.js +102 -0
  163. package/src/grammar/scales/validation.js +66 -0
  164. package/src/grammar/scales.js +1 -0
  165. package/src/grammar/schemas/concreteGraphic.js +73 -0
  166. package/src/grammar/schemas/graphic.js +131 -0
  167. package/src/grammar/schemas/graphicBounds.js +146 -0
  168. package/src/grammar/schemas/graphicTree.js +133 -0
  169. package/src/grammar/schemas/mark.js +4 -0
  170. package/src/grammar/schemas/semanticPath.js +174 -0
  171. package/src/grammar/seriesLayout.js +76 -0
  172. package/src/grammar/ticks.js +149 -0
  173. package/src/grammar/transforms.js +37 -0
  174. package/src/index.js +2 -0
  175. package/src/layout/canvas.js +114 -0
  176. package/src/layout/grid.js +17 -0
  177. package/src/layout/legend.js +51 -0
  178. package/src/layout/text.js +62 -0
  179. package/src/layout/title.js +107 -0
  180. package/src/materialization/bars/aggregate.js +184 -0
  181. package/src/materialization/bars/grouped.js +117 -0
  182. package/src/materialization/bars/histogram.js +163 -0
  183. package/src/materialization/bars/ranged.js +53 -0
  184. package/src/materialization/bars/resolve.js +56 -0
  185. package/src/materialization/dataProvenance.js +15 -0
  186. package/src/materialization/density.js +29 -0
  187. package/src/materialization/dependencies.js +134 -0
  188. package/src/materialization/encodings.js +50 -0
  189. package/src/materialization/graphicHierarchy.js +104 -0
  190. package/src/materialization/legends.js +20 -0
  191. package/src/materialization/marks.js +110 -0
  192. package/src/materialization/planner.js +38 -0
  193. package/src/materialization/selection/filter.js +40 -0
  194. package/src/materialization/selection/items.js +503 -0
  195. package/src/materialization/selection/path.js +37 -0
  196. package/src/materialization/selection/point.js +118 -0
  197. package/src/materialization/selection/policies/area.js +10 -0
  198. package/src/materialization/selection/policies/bar.js +10 -0
  199. package/src/materialization/selection/policies/index.js +40 -0
  200. package/src/materialization/selection/policies/line.js +10 -0
  201. package/src/materialization/selection/policies/point.js +10 -0
  202. package/src/materialization/selection/policies/rule.js +10 -0
  203. package/src/materialization/selection/state.js +67 -0
  204. package/src/materialization/selection/styles.js +183 -0
  205. package/src/renderers/canvas/circle.js +62 -0
  206. package/src/renderers/canvas/index.js +132 -0
  207. package/src/renderers/canvas/line.js +54 -0
  208. package/src/renderers/canvas/path.js +98 -0
  209. package/src/renderers/canvas/rect.js +57 -0
  210. package/src/renderers/canvas/text.js +96 -0
  211. package/src/renderers/canvas/validation.js +23 -0
  212. package/src/renderers/png.js +29 -0
  213. package/src/selectors/coordinates.js +13 -0
  214. package/src/selectors/datasets.js +13 -0
  215. package/src/selectors/index.js +4 -0
  216. package/src/selectors/layers.js +32 -0
  217. package/src/selectors/scales.js +21 -0
  218. package/src/theme/defaults.js +13 -0
  219. package/types/extension.d.ts +14 -0
  220. package/types/index.d.ts +81 -0
  221. package/types/png.d.ts +14 -0
  222. package/types/program.d.ts +1096 -0
@@ -0,0 +1,216 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateUserId } from "../../core/identifiers.js";
3
+ import { validateKeys } from "../../core/validation.js";
4
+ import { DEFAULT_COLORS } from "../../theme/defaults.js";
5
+ import { findDataset } from "../../selectors/datasets.js";
6
+ import { resolveEligibleLayer } from "../../selectors/layers.js";
7
+ import {
8
+ REGRESSION_LOWER_FIELD,
9
+ REGRESSION_UPPER_FIELD
10
+ } from "../../grammar/regression.js";
11
+ import { validateAreaCreateOutline } from "../marks/area.js";
12
+
13
+ const BAND_OPTIONS = Object.freeze([
14
+ "id", "data", "x", "lower", "upper", "groupBy", "coordinate", "xScale", "yScale",
15
+ "color", "opacity", "stroke", "strokeWidth", "curve"
16
+ ]);
17
+ const LINE_OPTIONS = Object.freeze([
18
+ "id", "data", "x", "y", "groupBy", "coordinate", "xScale", "yScale", "colorScale",
19
+ "strokeWidth", "curve"
20
+ ]);
21
+ const EDIT_BAND_OPTIONS = Object.freeze([
22
+ "target", "color", "opacity", "stroke", "strokeWidth", "curve"
23
+ ]);
24
+ const EDIT_LINE_OPTIONS = Object.freeze(["target", "strokeWidth", "curve"]);
25
+
26
+ function isRegressionLayer(program, layer, type) {
27
+ const dataset = findDataset(program, layer.data);
28
+ return layer.mark?.type === type &&
29
+ dataset?.transform?.length === 1 &&
30
+ dataset.transform[0].type === "regression";
31
+ }
32
+
33
+ function resolveRegressionComponent(program, args, type, label) {
34
+ const target = Object.hasOwn(args, "target")
35
+ ? validateUserId(args.target, `${label} id`)
36
+ : undefined;
37
+ return resolveEligibleLayer(program, {
38
+ target,
39
+ predicate: layer => isRegressionLayer(program, layer, type),
40
+ label
41
+ });
42
+ }
43
+
44
+ function requireRegressionBandDataset(program, args) {
45
+ const dataset = findDataset(program, args.data);
46
+ const transform = dataset?.transform?.length === 1
47
+ ? dataset.transform[0]
48
+ : undefined;
49
+ if (transform?.type !== "regression" || transform.method === "loess") {
50
+ throw new Error(
51
+ "createRegressionBand requires a regression dataset with interval bounds."
52
+ );
53
+ }
54
+ if (
55
+ args.x !== transform.x ||
56
+ args.lower !== REGRESSION_LOWER_FIELD ||
57
+ args.upper !== REGRESSION_UPPER_FIELD ||
58
+ args.groupBy !== transform.groupBy
59
+ ) {
60
+ throw new Error(
61
+ "createRegressionBand fields and grouping must match regression provenance."
62
+ );
63
+ }
64
+ return { dataset, transform };
65
+ }
66
+
67
+ export const createRegressionBand = action(
68
+ {
69
+ op: "createRegressionBand",
70
+ description: "Create and encode a grouped regression confidence band."
71
+ },
72
+ function (args = {}) {
73
+ validateKeys(args, BAND_OPTIONS, "createRegressionBand");
74
+ const id = validateUserId(args.id, "Regression band id");
75
+ const { transform } = requireRegressionBandDataset(this, args);
76
+ validateAreaCreateOutline(args, "createRegressionBand");
77
+ let next = this
78
+ .createErrorBand({
79
+ id,
80
+ data: args.data,
81
+ x: {
82
+ field: args.x,
83
+ fieldType: "quantitative",
84
+ scale: { id: args.xScale }
85
+ },
86
+ y: {
87
+ center: transform.y,
88
+ lower: args.lower,
89
+ upper: args.upper,
90
+ scale: { id: args.yScale }
91
+ },
92
+ ...(args.groupBy === undefined ? {} : { groupBy: args.groupBy }),
93
+ coordinate: args.coordinate,
94
+ fill: args.color ?? DEFAULT_COLORS.regressionBand,
95
+ opacity: args.opacity ?? 0.18,
96
+ ...(args.curve === undefined ? {} : { curve: args.curve })
97
+ })
98
+ .editSemantic({
99
+ property: `layer[${id}].encoding.y.title`,
100
+ remove: true
101
+ });
102
+ if (args.stroke !== undefined || args.strokeWidth !== undefined) {
103
+ next = next.editAreaMark({
104
+ target: id,
105
+ ...(args.stroke === undefined ? {} : { stroke: args.stroke }),
106
+ ...(args.strokeWidth === undefined
107
+ ? {}
108
+ : { strokeWidth: args.strokeWidth })
109
+ });
110
+ }
111
+ return next;
112
+ }
113
+ );
114
+
115
+ export const editRegressionBand = action(
116
+ {
117
+ op: "editRegressionBand",
118
+ description: "Edit regression-band fill, opacity, and outline."
119
+ },
120
+ function (args = {}) {
121
+ validateKeys(args, EDIT_BAND_OPTIONS, "editRegressionBand");
122
+ const changes = ["color", "opacity", "stroke", "strokeWidth", "curve"];
123
+ if (!changes.some(key => Object.hasOwn(args, key))) {
124
+ throw new Error(
125
+ "editRegressionBand requires color, opacity, stroke, strokeWidth, or curve."
126
+ );
127
+ }
128
+ const layer = resolveRegressionComponent(
129
+ this,
130
+ args,
131
+ "area",
132
+ "regression band"
133
+ );
134
+ return this.editAreaMark({
135
+ target: layer.id,
136
+ ...(Object.hasOwn(args, "color") ? { fill: args.color } : {}),
137
+ ...(Object.hasOwn(args, "opacity") ? { opacity: args.opacity } : {}),
138
+ ...(Object.hasOwn(args, "stroke") ? { stroke: args.stroke } : {}),
139
+ ...(Object.hasOwn(args, "strokeWidth")
140
+ ? { strokeWidth: args.strokeWidth }
141
+ : {}),
142
+ ...(Object.hasOwn(args, "curve") ? { curve: args.curve } : {})
143
+ });
144
+ }
145
+ );
146
+
147
+ export const editRegressionLine = action(
148
+ {
149
+ op: "editRegressionLine",
150
+ description: "Edit regression-line width or curve."
151
+ },
152
+ function (args = {}) {
153
+ validateKeys(args, EDIT_LINE_OPTIONS, "editRegressionLine");
154
+ if (
155
+ !Object.hasOwn(args, "strokeWidth") &&
156
+ !Object.hasOwn(args, "curve")
157
+ ) {
158
+ throw new Error("editRegressionLine requires strokeWidth or curve.");
159
+ }
160
+ const layer = resolveRegressionComponent(
161
+ this,
162
+ args,
163
+ "line",
164
+ "regression line"
165
+ );
166
+ return this.editLineMark({
167
+ target: layer.id,
168
+ ...(Object.hasOwn(args, "strokeWidth")
169
+ ? { strokeWidth: args.strokeWidth }
170
+ : {}),
171
+ ...(Object.hasOwn(args, "curve") ? { curve: args.curve } : {})
172
+ });
173
+ }
174
+ );
175
+
176
+ export const createRegressionLine = action(
177
+ {
178
+ op: "createRegressionLine",
179
+ description: "Create and encode grouped regression line paths."
180
+ },
181
+ function (args = {}) {
182
+ validateKeys(args, LINE_OPTIONS, "createRegressionLine");
183
+ const id = validateUserId(args.id, "Regression line id");
184
+ let next = this
185
+ .createLineMark({
186
+ id,
187
+ data: args.data,
188
+ strokeWidth: args.strokeWidth ?? 3,
189
+ ...(args.curve === undefined ? {} : { curve: args.curve })
190
+ })
191
+ .encodeX({
192
+ target: id,
193
+ field: args.x,
194
+ fieldType: "quantitative",
195
+ coordinate: args.coordinate,
196
+ scale: { id: args.xScale }
197
+ })
198
+ .encodeY({
199
+ target: id,
200
+ field: args.y,
201
+ fieldType: "quantitative",
202
+ coordinate: args.coordinate,
203
+ scale: { id: args.yScale }
204
+ });
205
+ if (args.groupBy !== undefined) {
206
+ next = next
207
+ .encodeColor({
208
+ target: id,
209
+ field: args.groupBy,
210
+ scale: { id: args.colorScale }
211
+ })
212
+ .encodeGroup({ target: id, field: args.groupBy });
213
+ }
214
+ return next.rematerializeLineMark({ id });
215
+ }
216
+ );
@@ -0,0 +1,102 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateKeys } from "../../core/validation.js";
3
+ import { normalizeRegressionParameters } from "../../grammar/regression.js";
4
+ import {
5
+ findRegressionPoint,
6
+ inferRegressionGroup,
7
+ requireRegressionField,
8
+ requireRegressionObject
9
+ } from "./resolve.js";
10
+
11
+ const REGRESSION_OPTIONS = Object.freeze([
12
+ "target", "x", "y", "groupBy", "method", "degree", "span",
13
+ "confidence", "interval", "band", "line"
14
+ ]);
15
+
16
+ export const createRegression = action(
17
+ {
18
+ op: "createRegression",
19
+ description: "Fit and layer regression lines with optional interval bands."
20
+ },
21
+ function (args = {}) {
22
+ validateKeys(args, REGRESSION_OPTIONS, "createRegression");
23
+ const point = findRegressionPoint(this, args.target);
24
+ const x = args.x === undefined
25
+ ? point.encoding.x.field
26
+ : requireRegressionField(args.x, "Regression x");
27
+ const y = args.y === undefined
28
+ ? point.encoding.y.field
29
+ : requireRegressionField(args.y, "Regression y");
30
+ const groupBy = inferRegressionGroup(point, args);
31
+ const coordinate = point.coordinate;
32
+ const xScale = point.encoding.x.scale;
33
+ const yScale = point.encoding.y.scale;
34
+ if (coordinate === undefined || xScale === undefined || yScale === undefined) {
35
+ throw new Error(
36
+ "createRegression target requires stored coordinate and scales."
37
+ );
38
+ }
39
+ const parameters = normalizeRegressionParameters(args);
40
+ let band = false;
41
+ if (parameters.method === "loess") {
42
+ if (args.band !== undefined && args.band !== false) {
43
+ throw new Error("LOESS regression does not support a band object.");
44
+ }
45
+ } else if (args.band !== false) {
46
+ band = requireRegressionObject(args.band ?? {}, "Regression band");
47
+ }
48
+ const line = requireRegressionObject(args.line ?? {}, "Regression line");
49
+ if (band !== false) {
50
+ validateKeys(
51
+ band,
52
+ ["color", "opacity", "stroke", "strokeWidth", "curve"],
53
+ "regression band"
54
+ );
55
+ }
56
+ validateKeys(line, ["strokeWidth", "curve"], "regression line");
57
+ const colorEncoding = point.encoding?.color;
58
+ const namespace = point.id;
59
+ const dataId = `${namespace}RegressionData`;
60
+ const bandId = `${namespace}RegressionBands`;
61
+ const lineId = `${namespace}RegressionLines`;
62
+ const colorScale =
63
+ groupBy !== undefined && colorEncoding?.field === groupBy
64
+ ? colorEncoding.scale
65
+ : `${namespace}RegressionColor`;
66
+
67
+ let next = this
68
+ .createRegressionData({
69
+ id: dataId,
70
+ source: point.data,
71
+ x,
72
+ y,
73
+ ...(groupBy === undefined ? {} : { groupBy }),
74
+ ...parameters
75
+ });
76
+ if (band !== false) {
77
+ next = next.createRegressionBand({
78
+ id: bandId,
79
+ data: dataId,
80
+ x,
81
+ lower: "__regression_ci_lower",
82
+ upper: "__regression_ci_upper",
83
+ ...(groupBy === undefined ? {} : { groupBy }),
84
+ coordinate,
85
+ xScale,
86
+ yScale,
87
+ ...band
88
+ });
89
+ }
90
+ return next.createRegressionLine({
91
+ id: lineId,
92
+ data: dataId,
93
+ x,
94
+ y,
95
+ ...(groupBy === undefined ? {} : { groupBy, colorScale }),
96
+ coordinate,
97
+ xScale,
98
+ yScale,
99
+ ...line
100
+ });
101
+ }
102
+ );
@@ -0,0 +1,23 @@
1
+ import {
2
+ createRegressionBand,
3
+ createRegressionLine,
4
+ editRegressionBand,
5
+ editRegressionLine
6
+ } from "./components.js";
7
+ import { createRegression } from "./create.js";
8
+
9
+ export {
10
+ createRegressionBand,
11
+ createRegressionLine,
12
+ editRegressionBand,
13
+ editRegressionLine
14
+ } from "./components.js";
15
+ export { createRegression } from "./create.js";
16
+
17
+ export function registerRegressionActions(ProgramClass) {
18
+ ProgramClass.prototype.createRegressionBand = createRegressionBand;
19
+ ProgramClass.prototype.createRegressionLine = createRegressionLine;
20
+ ProgramClass.prototype.editRegressionBand = editRegressionBand;
21
+ ProgramClass.prototype.editRegressionLine = editRegressionLine;
22
+ ProgramClass.prototype.createRegression = createRegression;
23
+ }
@@ -0,0 +1,57 @@
1
+ import { isPlainObject } from "../../core/immutable.js";
2
+ import { findLayer } from "../../selectors/layers.js";
3
+
4
+ export function requireRegressionObject(value, label) {
5
+ if (!isPlainObject(value)) {
6
+ throw new TypeError(`${label} must be a plain object.`);
7
+ }
8
+ return value;
9
+ }
10
+
11
+ export function requireRegressionField(value, label) {
12
+ if (typeof value !== "string" || value.length === 0) {
13
+ throw new TypeError(`${label} must be a non-empty string.`);
14
+ }
15
+ return value;
16
+ }
17
+
18
+ export function findRegressionPoint(program, requested) {
19
+ const eligible = program.semanticSpec.layers.filter(layer =>
20
+ layer.mark?.type === "point" &&
21
+ layer.encoding?.x?.fieldType === "quantitative" &&
22
+ layer.encoding?.y?.fieldType === "quantitative"
23
+ );
24
+ if (requested !== undefined) {
25
+ const selected = findLayer(program, requested);
26
+ if (selected === undefined || !eligible.includes(selected)) {
27
+ throw new Error(`Unknown regression point target "${requested}".`);
28
+ }
29
+ return selected;
30
+ }
31
+ const current = findLayer(program, program.context.currentMark);
32
+ if (current !== undefined && eligible.includes(current)) return current;
33
+ if (eligible.length === 1) return eligible[0];
34
+ if (eligible.length === 0) {
35
+ throw new Error(
36
+ "createRegression requires an eligible quantitative point mark."
37
+ );
38
+ }
39
+ throw new Error("createRegression target is ambiguous; provide target.");
40
+ }
41
+
42
+ export function inferRegressionGroup(layer, args) {
43
+ if (Object.hasOwn(args, "groupBy")) {
44
+ return args.groupBy === undefined
45
+ ? undefined
46
+ : requireRegressionField(args.groupBy, "Regression groupBy");
47
+ }
48
+ const candidates = [...new Set(
49
+ [layer.encoding?.color, layer.encoding?.shape]
50
+ .filter(encoding => encoding?.fieldType === "nominal")
51
+ .map(encoding => encoding.field)
52
+ )];
53
+ if (candidates.length > 1) {
54
+ throw new Error("createRegression groupBy is ambiguous; provide groupBy.");
55
+ }
56
+ return candidates[0];
57
+ }
@@ -0,0 +1,283 @@
1
+ import { deriveBarAggregates } from "../../grammar/bars/aggregate.js";
2
+ import {
3
+ BAR_GRAINS,
4
+ resolveBarChannels,
5
+ resolveBarColorLayout,
6
+ resolveBarGrain
7
+ } from "../../grammar/bars/policy.js";
8
+ import {
9
+ countHistogramBins,
10
+ findHistogramBinIndex,
11
+ resolveHistogramBins
12
+ } from "../../grammar/histogram.js";
13
+ import { deriveDensityAreaSeries } from "../../grammar/areaSeries.js";
14
+ import { deriveLineSeries } from "../../grammar/lineSeries.js";
15
+ import { resolveSeriesLayoutDomainValues } from "../../grammar/seriesLayout.js";
16
+ import {
17
+ readNominalField,
18
+ readQuantitativeField,
19
+ readScaleField,
20
+ readTemporalField,
21
+ resolveOrdinalDomain
22
+ } from "../../grammar/scales.js";
23
+ import { findDataset } from "../../selectors/datasets.js";
24
+ import { requireSemanticScale } from "../../selectors/scales.js";
25
+ import { isAggregate } from "../../grammar/aggregate.js";
26
+ import { normalizeRuleDatum } from "../../grammar/rules.js";
27
+
28
+ export function findScale(program, id) {
29
+ return requireSemanticScale(program, id);
30
+ }
31
+
32
+ export function findScaleConsumers(program, id) {
33
+ const consumers = [];
34
+ for (const layer of program.semanticSpec.layers) {
35
+ for (const channel of [
36
+ "x", "y", "x2", "y2", "xOffset", "color", "strokeDash", "size", "shape",
37
+ "opacity"
38
+ ]) {
39
+ const encoding = layer.encoding?.[channel];
40
+ if (encoding?.scale === id) consumers.push({ layer, channel, encoding });
41
+ }
42
+ }
43
+ return consumers;
44
+ }
45
+
46
+ export function resolveConsumerValues(program, consumer) {
47
+ const dataset = findDataset(program, consumer.layer.data);
48
+ if (dataset === undefined) {
49
+ throw new Error(
50
+ `Mark "${consumer.layer.id}" references unknown dataset "${consumer.layer.data}".`
51
+ );
52
+ }
53
+
54
+ if (Object.hasOwn(consumer.encoding, "datum")) {
55
+ return [normalizeRuleDatum(
56
+ consumer.encoding.datum,
57
+ consumer.encoding.fieldType,
58
+ consumer.channel
59
+ )];
60
+ }
61
+
62
+ if (
63
+ consumer.channel === "strokeDash" &&
64
+ !["line", "rule"].includes(consumer.layer.mark?.type)
65
+ ) {
66
+ throw new Error("strokeDash scale materialization requires a line mark or rule mark.");
67
+ }
68
+ const scale = findScale(program, consumer.encoding.scale);
69
+ const allowUnknown = Object.hasOwn(scale, "unknown");
70
+
71
+ if (
72
+ ["color", "strokeDash", "xOffset", "shape"].includes(consumer.channel) &&
73
+ consumer.encoding.fieldType === "nominal"
74
+ ) {
75
+ return allowUnknown
76
+ ? readScaleField(
77
+ dataset.values,
78
+ consumer.encoding.field,
79
+ consumer.encoding.fieldType,
80
+ { allowUnknown: true }
81
+ )
82
+ : readNominalField(dataset.values, consumer.encoding.field);
83
+ }
84
+
85
+ if (
86
+ consumer.layer.mark?.type === "line" &&
87
+ isAggregate(consumer.layer.encoding?.y?.aggregate)
88
+ ) {
89
+ const derived = deriveLineSeries(dataset.values, consumer.layer);
90
+ return consumer.channel === "x" ? derived.xValues : derived.yValues;
91
+ }
92
+
93
+ if (
94
+ resolveBarGrain(consumer.layer) === BAR_GRAINS.aggregate
95
+ ) {
96
+ const derived = deriveBarAggregates(dataset.values, consumer.layer);
97
+ if (consumer.channel === "color") {
98
+ return derived.values.map(value => value.color);
99
+ }
100
+ return consumer.channel === "x" ? derived.xValues : derived.yValues;
101
+ }
102
+
103
+ if (consumer.encoding.fieldType === "temporal") {
104
+ return allowUnknown
105
+ ? readScaleField(dataset.values, consumer.encoding.field, "temporal", {
106
+ allowUnknown: true
107
+ })
108
+ : readTemporalField(dataset.values, consumer.encoding.field);
109
+ }
110
+ if (["nominal", "ordinal"].includes(consumer.encoding.fieldType)) {
111
+ if (!["ordinal", "band", "point"].includes(scale.type)) {
112
+ throw new Error(
113
+ `Scale materialization requires a quantitative encoding on mark "${consumer.layer.id}".`
114
+ );
115
+ }
116
+ return allowUnknown
117
+ ? readScaleField(
118
+ dataset.values,
119
+ consumer.encoding.field,
120
+ consumer.encoding.fieldType,
121
+ { allowUnknown: true }
122
+ )
123
+ : readNominalField(dataset.values, consumer.encoding.field);
124
+ }
125
+ if (consumer.encoding.fieldType !== "quantitative") {
126
+ throw new Error(
127
+ `Scale materialization requires a quantitative encoding on mark "${consumer.layer.id}".`
128
+ );
129
+ }
130
+ return allowUnknown
131
+ ? readScaleField(dataset.values, consumer.encoding.field, "quantitative", {
132
+ allowUnknown: true
133
+ })
134
+ : readQuantitativeField(dataset.values, consumer.encoding.field);
135
+ }
136
+
137
+ export function resolveHistogramCountValues(program, consumer) {
138
+ const xEncoding = consumer.layer.encoding?.x;
139
+ const xScale = xEncoding?.scale === undefined
140
+ ? undefined
141
+ : findScale(program, xEncoding.scale);
142
+ const dataset = findDataset(program, consumer.layer.data);
143
+
144
+ if (xEncoding?.bin === undefined || xScale === undefined) {
145
+ throw new Error(
146
+ `Histogram mark "${consumer.layer.id}" requires a binned x scale.`
147
+ );
148
+ }
149
+ if (dataset === undefined) {
150
+ throw new Error(
151
+ `Histogram mark "${consumer.layer.id}" requires an existing dataset.`
152
+ );
153
+ }
154
+
155
+ const xValues = readQuantitativeField(dataset.values, xEncoding.field);
156
+ const bins = resolveHistogramBins({
157
+ values: xValues,
158
+ bin: xEncoding.bin,
159
+ domain: xScale.domain,
160
+ nice: xScale.nice ?? true,
161
+ zero: xScale.zero ?? false
162
+ });
163
+ return countHistogramBins(xValues, bins.boundaries);
164
+ }
165
+
166
+ function resolveHistogramPartitions(program, consumer) {
167
+ const layer = consumer.layer;
168
+ const dataset = findDataset(program, layer.data);
169
+ const xEncoding = layer.encoding.x;
170
+ const xScale = findScale(program, xEncoding.scale);
171
+ const xValues = readQuantitativeField(dataset.values, xEncoding.field);
172
+ const bins = resolveHistogramBins({
173
+ values: xValues,
174
+ bin: xEncoding.bin,
175
+ domain: xScale.domain,
176
+ nice: xScale.nice ?? true,
177
+ zero: xScale.zero ?? false
178
+ });
179
+ const colorEncoding = layer.encoding?.color;
180
+ if (colorEncoding?.scale === undefined) {
181
+ return countHistogramBins(xValues, bins.boundaries).map(value => [value]);
182
+ }
183
+
184
+ const colorScale = findScale(program, colorEncoding.scale);
185
+ const colorValues = readNominalField(dataset.values, colorEncoding.field);
186
+ const colorDomain = resolveOrdinalDomain(colorScale.domain, colorValues);
187
+ const colorIndex = new Map(colorDomain.map((value, index) => [value, index]));
188
+ const partitions = bins.boundaries.slice(0, -1).map(() =>
189
+ colorDomain.map(() => 0)
190
+ );
191
+ for (let index = 0; index < xValues.length; index += 1) {
192
+ const bin = findHistogramBinIndex(xValues[index], bins.boundaries);
193
+ const color = colorIndex.get(colorValues[index]);
194
+ if (bin !== -1 && color !== undefined) partitions[bin][color] += 1;
195
+ }
196
+ return partitions;
197
+ }
198
+
199
+ function resolveAggregatePartitions(program, consumer) {
200
+ const layer = consumer.layer;
201
+ const dataset = findDataset(program, layer.data);
202
+ const derived = deriveBarAggregates(dataset.values, layer);
203
+ const channels = resolveBarChannels(layer);
204
+ const categoryEncoding = layer.encoding[channels.category];
205
+ const categoryScale = findScale(program, categoryEncoding.scale);
206
+ const categoryValues = channels.category === "x"
207
+ ? derived.xValues
208
+ : derived.yValues;
209
+ const categoryDomain = resolveOrdinalDomain(categoryScale.domain, categoryValues);
210
+ const colorEncoding = layer.encoding?.color;
211
+ if (colorEncoding?.scale === undefined) {
212
+ const byCategory = new Map(derived.values.map(value => [
213
+ value[channels.category],
214
+ value[channels.measure]
215
+ ]));
216
+ return categoryDomain.map(value => [byCategory.get(value) ?? 0]);
217
+ }
218
+
219
+ const colorScale = findScale(program, colorEncoding.scale);
220
+ const colorValues = readNominalField(dataset.values, colorEncoding.field);
221
+ const colorDomain = resolveOrdinalDomain(colorScale.domain, colorValues);
222
+ const cells = new Map(derived.values.map(value => [
223
+ JSON.stringify([value[channels.category], value.color]),
224
+ value[channels.measure]
225
+ ]));
226
+ return categoryDomain.map(category => colorDomain.map(color =>
227
+ cells.get(JSON.stringify([category, color])) ?? 0
228
+ ));
229
+ }
230
+
231
+ function resolveAreaPartitions(program, consumer) {
232
+ const layer = consumer.layer;
233
+ const dataset = findDataset(program, layer.data);
234
+ const transform = dataset?.transform?.length === 1 &&
235
+ dataset.transform[0].type === "density"
236
+ ? dataset.transform[0]
237
+ : undefined;
238
+ if (transform === undefined) return undefined;
239
+ const derived = deriveDensityAreaSeries(dataset.values, layer, transform);
240
+ if (derived.mode !== "y-density") return undefined;
241
+ const sampleCount = derived.series[0].values.length;
242
+ if (derived.series.some(series => series.values.length !== sampleCount)) {
243
+ throw new Error(`Area mark "${layer.id}" requires aligned layout samples.`);
244
+ }
245
+ return Array.from({ length: sampleCount }, (_, index) =>
246
+ derived.series.map(series => series.values[index].y)
247
+ );
248
+ }
249
+
250
+ export function resolveSeriesLayoutScaleValues(program, consumer) {
251
+ if (consumer.layer.mark?.type === "bar") {
252
+ const grain = resolveBarGrain(consumer.layer);
253
+ const channels = resolveBarChannels(consumer.layer);
254
+ if (consumer.channel !== channels?.measure) return undefined;
255
+ const partitions = grain === BAR_GRAINS.histogram
256
+ ? resolveHistogramPartitions(program, consumer)
257
+ : grain === BAR_GRAINS.aggregate
258
+ ? resolveAggregatePartitions(program, consumer)
259
+ : undefined;
260
+ if (partitions === undefined) return undefined;
261
+ const layout = resolveBarColorLayout(consumer.layer);
262
+ return {
263
+ layout,
264
+ values: resolveSeriesLayoutDomainValues(partitions, layout)
265
+ };
266
+ }
267
+ if (consumer.channel !== "y") return undefined;
268
+ if (consumer.layer.mark?.type === "area") {
269
+ const layout = consumer.layer.encoding?.color?.layout;
270
+ if (layout === undefined || layout === "overlay") return undefined;
271
+ const partitions = resolveAreaPartitions(program, consumer);
272
+ if (partitions === undefined) {
273
+ throw new Error(
274
+ `Area layout "${layout}" currently requires vertical density series.`
275
+ );
276
+ }
277
+ return {
278
+ layout,
279
+ values: resolveSeriesLayoutDomainValues(partitions, layout)
280
+ };
281
+ }
282
+ return undefined;
283
+ }