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,225 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateUserId } from "../../core/identifiers.js";
3
+ import { isPlainObject } from "../../core/immutable.js";
4
+ import {
5
+ findDataset,
6
+ findLayer,
7
+ hasDataset,
8
+ resolveEligibleLayer
9
+ } from "../../selectors/index.js";
10
+ import { applyMaterializationPlan } from
11
+ "../../materialization/dependencies.js";
12
+ import { planDensityRematerialization } from
13
+ "../../materialization/density.js";
14
+ import { validateOptions } from "./shared.js";
15
+
16
+ const OPTIONS = Object.freeze([
17
+ "field", "target", "source", "groupBy", "bandwidth", "extent", "steps",
18
+ "kernel", "normalization", "as", "densityChannel", "coordinate",
19
+ "valueScale", "densityScale"
20
+ ]);
21
+ const EDIT_OPTIONS = Object.freeze([
22
+ "target", "bandwidth", "extent", "steps", "kernel", "normalization"
23
+ ]);
24
+ const EDITABLE = Object.freeze(EDIT_OPTIONS.filter(option => option !== "target"));
25
+
26
+ function findArea(program, requested) {
27
+ const areas = program.semanticSpec.layers.filter(layer => layer.mark?.type === "area");
28
+ if (requested !== undefined) {
29
+ const id = validateUserId(requested, "Density target id");
30
+ const selected = findLayer(program, id);
31
+ if (selected === undefined || !areas.includes(selected)) {
32
+ throw new Error(`Unknown density area target "${id}".`);
33
+ }
34
+ return selected;
35
+ }
36
+ const current = findLayer(program, program.context.currentMark);
37
+ if (current !== undefined && areas.includes(current)) return current;
38
+ if (areas.length === 1) return areas[0];
39
+ if (areas.length === 0) {
40
+ throw new Error("encodeDensity requires an eligible area mark.");
41
+ }
42
+ throw new Error("encodeDensity target is ambiguous; provide target.");
43
+ }
44
+
45
+ function requireField(value, label) {
46
+ if (typeof value !== "string" || value.length === 0) {
47
+ throw new TypeError(`${label} must be a non-empty string.`);
48
+ }
49
+ return value;
50
+ }
51
+
52
+ function scaleOptions(value, defaults, label) {
53
+ if (value !== undefined && !isPlainObject(value)) {
54
+ throw new TypeError(`${label} must be a plain object.`);
55
+ }
56
+ return { ...defaults, ...(value ?? {}) };
57
+ }
58
+
59
+ const encodeDensity = action(
60
+ {
61
+ op: "encodeDensity",
62
+ description: "Derive and encode a baseline-oriented kernel density area."
63
+ },
64
+ function (args = {}) {
65
+ validateOptions(args, OPTIONS, "encodeDensity");
66
+ const layer = findArea(this, args.target);
67
+ const field = requireField(args.field, "Density field");
68
+ const groupBy = args.groupBy === undefined
69
+ ? undefined
70
+ : requireField(args.groupBy, "Density groupBy");
71
+ const source = validateUserId(
72
+ args.source ?? layer.data,
73
+ "Density source dataset id"
74
+ );
75
+ if (!hasDataset(this, source)) {
76
+ throw new Error(`Unknown density source dataset "${source}".`);
77
+ }
78
+ if (["x", "y", "y2", "group"].some(channel =>
79
+ layer.encoding?.[channel] !== undefined
80
+ )) {
81
+ throw new Error(
82
+ `Density area target "${layer.id}" already has positional or group encodings.`
83
+ );
84
+ }
85
+ const densityChannel = args.densityChannel ?? "y";
86
+ if (!["x", "y"].includes(densityChannel)) {
87
+ throw new Error(`Unsupported densityChannel "${densityChannel}".`);
88
+ }
89
+ const output = args.as ?? [`${field}_value`, `${field}_density`];
90
+ if (!Array.isArray(output) || output.length !== 2) {
91
+ throw new TypeError("Density as must contain value and density field names.");
92
+ }
93
+ const derivedId = `${layer.id}DensityData`;
94
+ if (hasDataset(this, derivedId)) {
95
+ throw new Error(`Dataset "${derivedId}" already exists.`);
96
+ }
97
+ const valueScale = scaleOptions(
98
+ args.valueScale,
99
+ { nice: false, zero: false },
100
+ "Density valueScale"
101
+ );
102
+ const densityScale = scaleOptions(
103
+ args.densityScale,
104
+ { nice: true, zero: true },
105
+ "Density densityScale"
106
+ );
107
+ const xIsDensity = densityChannel === "x";
108
+ const xField = output[xIsDensity ? 1 : 0];
109
+ const yField = output[xIsDensity ? 0 : 1];
110
+ const xScale = xIsDensity ? densityScale : valueScale;
111
+ const yScale = xIsDensity ? valueScale : densityScale;
112
+
113
+ let next = this
114
+ .createDensityData({
115
+ id: derivedId,
116
+ source,
117
+ field,
118
+ ...(groupBy === undefined ? {} : { groupBy }),
119
+ ...(args.bandwidth === undefined ? {} : { bandwidth: args.bandwidth }),
120
+ ...(args.extent === undefined ? {} : { extent: args.extent }),
121
+ ...(args.steps === undefined ? {} : { steps: args.steps }),
122
+ ...(args.kernel === undefined ? {} : { kernel: args.kernel }),
123
+ ...(args.normalization === undefined
124
+ ? {}
125
+ : { normalization: args.normalization }),
126
+ ...(args.as === undefined ? {} : { as: args.as })
127
+ })
128
+ .editSemantic({
129
+ property: `layer[${layer.id}].data`,
130
+ value: derivedId
131
+ })
132
+ .encodeX({
133
+ target: layer.id,
134
+ field: xField,
135
+ ...(args.coordinate === undefined ? {} : { coordinate: args.coordinate }),
136
+ scale: xScale
137
+ })
138
+ .encodeY({
139
+ target: layer.id,
140
+ field: yField,
141
+ ...(args.coordinate === undefined ? {} : { coordinate: args.coordinate }),
142
+ scale: yScale
143
+ });
144
+ if (groupBy !== undefined) {
145
+ next = next.encodeGroup({ target: layer.id, field: groupBy });
146
+ }
147
+ return next.rematerializeAreaMark({ id: layer.id });
148
+ }
149
+ );
150
+
151
+ function findDensityArea(program, requested) {
152
+ const target = requested === undefined
153
+ ? undefined
154
+ : validateUserId(requested, "Density target id");
155
+ return resolveEligibleLayer(program, {
156
+ target,
157
+ label: "density area",
158
+ predicate(layer) {
159
+ const dataset = findDataset(program, layer.data);
160
+ return layer.mark?.type === "area" &&
161
+ dataset?.transform?.length === 1 &&
162
+ dataset.transform[0].type === "density";
163
+ }
164
+ });
165
+ }
166
+
167
+ function nextDensityRevisionId(program, target) {
168
+ let revision = 1;
169
+ while (hasDataset(program, `${target}DensityDataRevision${revision}`)) {
170
+ revision += 1;
171
+ }
172
+ return `${target}DensityDataRevision${revision}`;
173
+ }
174
+
175
+ const editDensity = action(
176
+ {
177
+ op: "editDensity",
178
+ description: "Revise one density transform and rematerialize its consumers."
179
+ },
180
+ function (args = {}) {
181
+ validateOptions(args, EDIT_OPTIONS, "editDensity");
182
+ if (!EDITABLE.some(option => Object.hasOwn(args, option))) {
183
+ throw new Error("editDensity requires at least one density option.");
184
+ }
185
+ const layer = findDensityArea(this, args.target);
186
+ const previous = findDataset(this, layer.data);
187
+ const transform = previous.transform[0];
188
+ const revisionId = nextDensityRevisionId(this, layer.id);
189
+ const option = property => Object.hasOwn(args, property)
190
+ ? args[property]
191
+ : transform[property];
192
+
193
+ let next = this
194
+ .createDensityData({
195
+ id: revisionId,
196
+ source: previous.source,
197
+ field: transform.field,
198
+ ...(transform.groupBy === undefined
199
+ ? {}
200
+ : { groupBy: transform.groupBy }),
201
+ bandwidth: option("bandwidth"),
202
+ extent: option("extent"),
203
+ steps: option("steps"),
204
+ kernel: option("kernel") ?? "gaussian",
205
+ normalization: option("normalization") ?? "unit",
206
+ as: transform.as
207
+ })
208
+ .editSemantic({
209
+ property: `layer[${layer.id}].data`,
210
+ value: revisionId
211
+ })
212
+ .releaseDerivedData({ id: previous.id });
213
+
214
+ next = applyMaterializationPlan(
215
+ next,
216
+ planDensityRematerialization(next, layer.id)
217
+ );
218
+ return next;
219
+ }
220
+ );
221
+
222
+ export function registerDensityEncodingAction(ProgramClass) {
223
+ ProgramClass.prototype.encodeDensity = encodeDensity;
224
+ ProgramClass.prototype.editDensity = editDensity;
225
+ }
@@ -0,0 +1,50 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateOptions } from "./shared.js";
3
+
4
+ const HISTOGRAM_OPTIONS = Object.freeze([
5
+ "field", "target", "coordinate", "maxBins", "binStep", "binBoundaries",
6
+ "stack", "xScale", "yScale"
7
+ ]);
8
+
9
+ const encodeHistogram = action(
10
+ {
11
+ op: "encodeHistogram",
12
+ description: "Encode a binned count histogram with a configurable layout stack."
13
+ },
14
+ function (args = {}) {
15
+ validateOptions(args, HISTOGRAM_OPTIONS, "encodeHistogram");
16
+ const binOptions = ["maxBins", "binStep", "binBoundaries"].filter(
17
+ key => Object.hasOwn(args, key)
18
+ );
19
+ if (binOptions.length > 1) {
20
+ throw new Error(
21
+ "encodeHistogram accepts only one of maxBins, binStep, or binBoundaries."
22
+ );
23
+ }
24
+ const bin = Object.hasOwn(args, "binStep")
25
+ ? { step: args.binStep }
26
+ : Object.hasOwn(args, "binBoundaries")
27
+ ? { boundaries: args.binBoundaries }
28
+ : { maxBins: args.maxBins ?? 10 };
29
+ const x = {
30
+ field: args.field,
31
+ bin
32
+ };
33
+ const y = {
34
+ stack: Object.hasOwn(args, "stack") ? args.stack : "zero"
35
+ };
36
+
37
+ for (const key of ["target", "coordinate"]) {
38
+ if (Object.hasOwn(args, key)) x[key] = args[key];
39
+ }
40
+ if (Object.hasOwn(args, "target")) y.target = args.target;
41
+ if (Object.hasOwn(args, "xScale")) x.scale = args.xScale;
42
+ if (Object.hasOwn(args, "yScale")) y.scale = args.yScale;
43
+
44
+ return this.encodeX(x).encodeY(y);
45
+ }
46
+ );
47
+
48
+ export function registerHistogramEncodingAction(ProgramClass) {
49
+ ProgramClass.prototype.encodeHistogram = encodeHistogram;
50
+ }
@@ -0,0 +1,23 @@
1
+ import { registerAppearanceEncodingAction } from "./appearance.js";
2
+ import { registerBarWidthEncodingAction } from "./barWidth.js";
3
+ import { registerColorEncodingAction } from "./color.js";
4
+ import { registerDensityEncodingAction } from "./density.js";
5
+ import { registerHistogramEncodingAction } from "./histogram.js";
6
+ import { registerOffsetEncodingAction } from "./offset.js";
7
+ import { registerPositionEncodingActions } from "./position.js";
8
+ import { registerRangedEncodingActions } from "./ranged.js";
9
+ import { registerStrokeDashEncodingActions } from "./strokeDash.js";
10
+ import { registerRuleAppearanceEncodingActions } from "./ruleAppearance.js";
11
+
12
+ export function registerEncodingActions(ProgramClass) {
13
+ registerPositionEncodingActions(ProgramClass);
14
+ registerRangedEncodingActions(ProgramClass);
15
+ registerOffsetEncodingAction(ProgramClass);
16
+ registerBarWidthEncodingAction(ProgramClass);
17
+ registerHistogramEncodingAction(ProgramClass);
18
+ registerDensityEncodingAction(ProgramClass);
19
+ registerColorEncodingAction(ProgramClass);
20
+ registerStrokeDashEncodingActions(ProgramClass);
21
+ registerAppearanceEncodingAction(ProgramClass);
22
+ registerRuleAppearanceEncodingActions(ProgramClass);
23
+ }
@@ -0,0 +1,104 @@
1
+ import { action } from "../../core/action.js";
2
+ import {
3
+ readNominalField,
4
+ validateNominalFieldType
5
+ } from "../../grammar/scales.js";
6
+ import { resolveOffsetScaleDefinition } from "../scales/definitions.js";
7
+ import {
8
+ applyEncodingScale,
9
+ resolveReassignmentScaleOptions,
10
+ resolveTarget,
11
+ validateOptions
12
+ } from "./shared.js";
13
+ import {
14
+ resolveBarColorLayout,
15
+ resolveBarGrain
16
+ } from "../../grammar/bars/policy.js";
17
+ import { normalizeOffsetPadding } from "../../grammar/bars/geometry.js";
18
+ import { applyMaterializationPlan } from "../../materialization/dependencies.js";
19
+ import { planEncodingRematerialization } from "../../materialization/encodings.js";
20
+
21
+ const ENCODING_OPTIONS = Object.freeze([
22
+ "field", "target", "fieldType", "scale", "paddingInner", "paddingOuter"
23
+ ]);
24
+ const encodeXOffset = action(
25
+ {
26
+ op: "encodeXOffset",
27
+ description: "Encode a nominal field within each ordinal x band."
28
+ },
29
+ function (args = {}) {
30
+ validateOptions(args, ENCODING_OPTIONS, "encodeXOffset");
31
+ const fieldType = validateNominalFieldType(args.fieldType ?? "nominal");
32
+ const { id: target, dataset, layer } = resolveTarget(
33
+ this,
34
+ args.target,
35
+ ["bar"],
36
+ "bar mark"
37
+ );
38
+
39
+ if (
40
+ resolveBarGrain(layer) === undefined ||
41
+ layer.encoding.x.scale === undefined
42
+ ) {
43
+ throw new Error(
44
+ "encodeXOffset requires a complete bar x/y encoding."
45
+ );
46
+ }
47
+ if (
48
+ layer.encoding?.color !== undefined &&
49
+ (resolveBarColorLayout(layer) !== "group" ||
50
+ layer.encoding.color.field !== args.field)
51
+ ) {
52
+ throw new Error(
53
+ "encodeXOffset field must match a grouped bar color field."
54
+ );
55
+ }
56
+
57
+ readNominalField(dataset.values, args.field);
58
+ const requestedScale = resolveReassignmentScaleOptions(
59
+ layer.encoding?.xOffset,
60
+ args.scale ?? {}
61
+ );
62
+ const scale = resolveOffsetScaleDefinition(this, requestedScale);
63
+ if (Object.hasOwn(scale, "unknown")) {
64
+ throw new Error("xOffset scale unknown is not supported for grouped bars.");
65
+ }
66
+ const padding = normalizeOffsetPadding(
67
+ args,
68
+ this.markConfigs[target]?.xOffset
69
+ );
70
+
71
+ let next = this
72
+ .editSemantic({
73
+ property: `layer[${target}].encoding.xOffset.field`,
74
+ value: args.field
75
+ })
76
+ .editSemantic({
77
+ property: `layer[${target}].encoding.xOffset.fieldType`,
78
+ value: fieldType
79
+ })
80
+ .editSemantic({
81
+ property: `layer[${target}].encoding.xOffset.scale`,
82
+ value: scale.id
83
+ })
84
+ ._withMarkConfig(target, {
85
+ ...this.markConfigs[target],
86
+ xOffset: padding
87
+ });
88
+ next = applyEncodingScale(next, scale, requestedScale, {
89
+ reassignment: layer.encoding?.xOffset?.scale === scale.id
90
+ });
91
+ return applyMaterializationPlan(
92
+ next,
93
+ planEncodingRematerialization(next, {
94
+ target,
95
+ channel: "xOffset",
96
+ scale: scale.id
97
+ })
98
+ );
99
+ }
100
+ );
101
+
102
+ export function registerOffsetEncodingAction(ProgramClass) {
103
+ ProgramClass.prototype.encodeXOffset = encodeXOffset;
104
+ }
@@ -0,0 +1,24 @@
1
+ import { validateStack, emptyPositionPolicy } from "./common.js";
2
+
3
+ export function resolveAreaPositionPolicy({ dataset, channel, args, fieldType }) {
4
+ if (!["quantitative", "temporal"].includes(fieldType)) {
5
+ throw new Error(
6
+ "Area position encoding requires quantitative or temporal fields."
7
+ );
8
+ }
9
+ if (args.aggregate !== undefined || args.bin !== undefined) {
10
+ throw new Error("Area position encoding does not support aggregate or bin.");
11
+ }
12
+ if (args.stack === undefined) return emptyPositionPolicy();
13
+ if (
14
+ channel !== "y" ||
15
+ !dataset.transform?.some(transform => transform.type === "density")
16
+ ) {
17
+ throw new Error("Area stack currently requires a density y encoding.");
18
+ }
19
+ return {
20
+ bin: undefined,
21
+ aggregate: undefined,
22
+ stack: validateStack(args.stack, "Area y encoding")
23
+ };
24
+ }
@@ -0,0 +1,110 @@
1
+ import {
2
+ validateAggregate,
3
+ validateAggregateFieldType
4
+ } from "../../../../grammar/aggregate.js";
5
+ import {
6
+ BAR_ORIENTATIONS,
7
+ resolveBarOrientation
8
+ } from "../../../../grammar/bars/policy.js";
9
+ import { resolveBin, validateStack } from "./common.js";
10
+
11
+ export function resolveBarPositionPolicy({
12
+ program,
13
+ layer,
14
+ channel,
15
+ args,
16
+ fieldType,
17
+ field
18
+ }) {
19
+ let bin;
20
+ let aggregate;
21
+ let stack;
22
+ const xEncoding = layer.encoding?.x;
23
+ const opposite = layer.encoding?.[channel === "x" ? "y" : "x"];
24
+ const pendingBoxRange = program.markConfigs[layer.id]?.boxPlot !== undefined;
25
+
26
+ if (["nominal", "ordinal", "temporal"].includes(fieldType)) {
27
+ if (args.aggregate !== undefined || args.bin !== undefined || args.stack !== undefined) {
28
+ throw new Error(
29
+ "Categorical bar position does not support bin or aggregate; a binned bar requires a quantitative field."
30
+ );
31
+ }
32
+ } else if (fieldType === "quantitative" && channel === "x" && args.bin !== undefined) {
33
+ if (args.aggregate !== undefined || args.stack !== undefined) {
34
+ throw new Error("Binned bar x encoding does not support aggregate or stack.");
35
+ }
36
+ bin = resolveBin(args.bin);
37
+ } else if (
38
+ fieldType === "quantitative" &&
39
+ channel === "y" &&
40
+ xEncoding?.bin !== undefined
41
+ ) {
42
+ if (args.bin !== undefined) {
43
+ throw new Error("Histogram bar y encoding does not support bin.");
44
+ }
45
+ if (field !== xEncoding.field) {
46
+ throw new Error("Bar y field must match the binned x field.");
47
+ }
48
+ aggregate = args.aggregate ?? "count";
49
+ stack = Object.hasOwn(args, "stack") ? args.stack : "zero";
50
+ if (aggregate !== "count") {
51
+ throw new Error('Histogram bar y aggregate must be "count".');
52
+ }
53
+ stack = validateStack(stack, "Histogram bar y encoding");
54
+ } else if (fieldType === "quantitative") {
55
+ if (args.bin !== undefined) {
56
+ throw new Error(
57
+ channel === "y"
58
+ ? "Bar y does not support bin; histogram y requires a binned x encoding."
59
+ : "Quantitative bar measure encoding does not support bin."
60
+ );
61
+ }
62
+ aggregate = args.aggregate ?? (
63
+ ["nominal", "ordinal", "temporal"].includes(opposite?.fieldType) &&
64
+ !pendingBoxRange
65
+ ? "mean"
66
+ : undefined
67
+ );
68
+ if (pendingBoxRange && args.aggregate === undefined) {
69
+ return { bin, aggregate, stack };
70
+ }
71
+ if (aggregate === undefined) {
72
+ throw new Error(
73
+ channel === "x"
74
+ ? "Quantitative bar x encoding requires bin or aggregate."
75
+ : "Bar y encoding requires a binned quantitative or ordinal x category, temporal x category, or aggregate."
76
+ );
77
+ }
78
+ stack = Object.hasOwn(args, "stack") ? args.stack : null;
79
+ aggregate = validateAggregate(aggregate);
80
+ validateAggregateFieldType(aggregate, fieldType);
81
+ stack = validateStack(stack, `Bar ${channel} encoding`);
82
+ } else {
83
+ throw new Error(
84
+ "Bar position requires quantitative, temporal, ordinal, or nominal fields."
85
+ );
86
+ }
87
+
88
+ const candidate = {
89
+ ...layer,
90
+ encoding: {
91
+ ...layer.encoding,
92
+ [channel]: { field, fieldType, bin, aggregate, stack }
93
+ }
94
+ };
95
+ const orientation = resolveBarOrientation(candidate);
96
+ if (opposite !== undefined && orientation === undefined && !pendingBoxRange) {
97
+ throw new Error(
98
+ `Bar ${channel} encoding requires a quantitative field opposite a categorical position.`
99
+ );
100
+ }
101
+ if (
102
+ orientation === BAR_ORIENTATIONS.horizontal &&
103
+ candidate.encoding?.xOffset !== undefined
104
+ ) {
105
+ throw new Error(
106
+ "Horizontal grouped bars require yOffset support, which is not available yet."
107
+ );
108
+ }
109
+ return { bin, aggregate, stack };
110
+ }
@@ -0,0 +1,17 @@
1
+ import { STACK_MODES } from "../../../../core/vocabulary.js";
2
+ import { normalizeHistogramBin } from "../../../../grammar/histogram.js";
3
+
4
+ export function resolveBin(bin) {
5
+ return normalizeHistogramBin(bin);
6
+ }
7
+
8
+ export function validateStack(stack, label) {
9
+ if (stack !== null && !STACK_MODES.includes(stack)) {
10
+ throw new Error(`${label} has unsupported stack "${stack}".`);
11
+ }
12
+ return stack;
13
+ }
14
+
15
+ export function emptyPositionPolicy() {
16
+ return { bin: undefined, aggregate: undefined, stack: undefined };
17
+ }
@@ -0,0 +1,25 @@
1
+ import { validatePositionFieldCompatibility } from
2
+ "../../../../grammar/positionCompatibility.js";
3
+ import { resolveAreaPositionPolicy } from "./area.js";
4
+ import { resolveBarPositionPolicy } from "./bar.js";
5
+ import { resolveLinePositionPolicy } from "./line.js";
6
+ import { resolvePointPositionPolicy } from "./point.js";
7
+ import { resolveRulePositionPolicy } from "./rule.js";
8
+
9
+ const POSITION_POLICIES = Object.freeze({
10
+ area: resolveAreaPositionPolicy,
11
+ bar: resolveBarPositionPolicy,
12
+ line: resolveLinePositionPolicy,
13
+ point: resolvePointPositionPolicy,
14
+ rule: resolveRulePositionPolicy
15
+ });
16
+
17
+ export function resolveMarkPositionPolicy(context) {
18
+ const { layer, channel, fieldType } = context;
19
+ validatePositionFieldCompatibility(layer.mark.type, channel, fieldType);
20
+ const policy = POSITION_POLICIES[layer.mark.type];
21
+ if (policy === undefined) {
22
+ throw new Error(`Unsupported position policy for mark "${layer.mark.type}".`);
23
+ }
24
+ return policy(context);
25
+ }
@@ -0,0 +1,74 @@
1
+ import {
2
+ validateAggregate,
3
+ validateAggregateFieldType
4
+ } from "../../../../grammar/aggregate.js";
5
+ import { emptyPositionPolicy } from "./common.js";
6
+
7
+ function rejectSharedOptions(args, channel) {
8
+ if (args.bin !== undefined) {
9
+ throw new Error(`Line ${channel} encoding does not support bin.`);
10
+ }
11
+ if (args.stack !== undefined) {
12
+ throw new Error(`Line ${channel} encoding does not support stack.`);
13
+ }
14
+ }
15
+
16
+ export function resolveLinePositionPolicy({
17
+ layer,
18
+ dataset,
19
+ channel,
20
+ args,
21
+ fieldType
22
+ }) {
23
+ const regression = dataset.transform?.some(item => item.type === "regression");
24
+ const interval = dataset.transform?.some(item => item.type === "interval");
25
+
26
+ if (channel === "x") {
27
+ const horizontalDirect =
28
+ layer.encoding?.y?.fieldType === "temporal" &&
29
+ fieldType === "quantitative";
30
+ if (
31
+ fieldType !== "temporal" &&
32
+ !((regression || interval || horizontalDirect) && fieldType === "quantitative")
33
+ ) {
34
+ throw new Error(
35
+ "Line x encoding requires a temporal field or a compatible derived quantitative field."
36
+ );
37
+ }
38
+ if (args.aggregate !== undefined) {
39
+ throw new Error("Line x encoding does not support aggregate.");
40
+ }
41
+ rejectSharedOptions(args, "x");
42
+ return emptyPositionPolicy();
43
+ }
44
+
45
+ rejectSharedOptions(args, "y");
46
+ const prospectiveDirect =
47
+ args.aggregate === undefined &&
48
+ (fieldType === "temporal" ||
49
+ (fieldType === "quantitative" && layer.encoding?.x === undefined));
50
+ if (interval || prospectiveDirect) {
51
+ if (!["quantitative", "temporal"].includes(fieldType)) {
52
+ throw new Error(
53
+ "Direct line y encoding requires a quantitative or temporal field."
54
+ );
55
+ }
56
+ if (args.aggregate !== undefined) {
57
+ throw new Error("Direct line y encoding does not support aggregate.");
58
+ }
59
+ return emptyPositionPolicy();
60
+ }
61
+ if (regression) {
62
+ if (fieldType !== "quantitative") {
63
+ throw new Error("Regression line y encoding requires a quantitative field.");
64
+ }
65
+ if (args.aggregate !== undefined) {
66
+ throw new Error("Regression line y encoding does not support aggregate.");
67
+ }
68
+ return emptyPositionPolicy();
69
+ }
70
+
71
+ const aggregate = validateAggregate(args.aggregate);
72
+ validateAggregateFieldType(aggregate, fieldType);
73
+ return { bin: undefined, aggregate, stack: undefined };
74
+ }
@@ -0,0 +1,19 @@
1
+ import { emptyPositionPolicy } from "./common.js";
2
+
3
+ export function resolvePointPositionPolicy({ args, fieldType }) {
4
+ if (!["quantitative", "temporal", "ordinal", "nominal"].includes(fieldType)) {
5
+ throw new Error(
6
+ "Point position encoding requires quantitative, temporal, ordinal, or nominal fields."
7
+ );
8
+ }
9
+ if (args.aggregate !== undefined) {
10
+ throw new Error("Point position encoding does not support aggregate.");
11
+ }
12
+ if (args.bin !== undefined) {
13
+ throw new Error("Point position encoding does not support bin.");
14
+ }
15
+ if (args.stack !== undefined) {
16
+ throw new Error("Point position encoding does not support stack.");
17
+ }
18
+ return emptyPositionPolicy();
19
+ }