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,287 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateUserId } from "../../core/identifiers.js";
3
+ import { validatePositiveFinite } from "../../core/validation.js";
4
+ import {
5
+ mapContinuousScaleValues,
6
+ mapOrdinalPositionValues,
7
+ mapOrdinalValues,
8
+ normalizeStrokeDashPattern
9
+ } from "../../grammar/scales.js";
10
+ import { deriveRuleValues, resolveRuleMode } from "../../grammar/rules.js";
11
+ import { resolveGraphicBounds } from "../../layout/canvas.js";
12
+ import { findDataset } from "../../selectors/datasets.js";
13
+ import { findLayer } from "../../selectors/layers.js";
14
+ import { DEFAULT_COLORS } from "../../theme/defaults.js";
15
+ import {
16
+ assertMarkAvailable,
17
+ resolveMarkData,
18
+ resolveMarkId,
19
+ validateMarkOptions
20
+ } from "./shared.js";
21
+ import { resolveMarkGraphicPlacement } from
22
+ "../../materialization/graphicHierarchy.js";
23
+
24
+ const CREATE_OPTIONS = Object.freeze(["id", "data"]);
25
+ const REMATERIALIZE_OPTIONS = Object.freeze(["id"]);
26
+ const SPAN_OPTIONS = Object.freeze(["id", "orientation", "size"]);
27
+ const DEFAULT_RULE_CONFIG = Object.freeze({
28
+ stroke: DEFAULT_COLORS.mark,
29
+ strokeWidth: 2,
30
+ strokeDash: Object.freeze([]),
31
+ opacity: 1
32
+ });
33
+
34
+ function mapPosition(values, scale) {
35
+ return ["ordinal", "band", "point"].includes(scale.type)
36
+ ? mapOrdinalPositionValues(values, scale)
37
+ : mapContinuousScaleValues(values, scale);
38
+ }
39
+
40
+ function requireRule(program, id) {
41
+ const layer = findLayer(program, id);
42
+ if (layer?.mark?.type !== "rule") {
43
+ throw new Error(`Unknown rule mark "${id}".`);
44
+ }
45
+ const dataset = findDataset(program, layer.data);
46
+ if (dataset === undefined) {
47
+ throw new Error(`Rule mark "${id}" requires an existing dataset.`);
48
+ }
49
+ const graphic = program.graphicSpec.objects[id];
50
+ if (graphic?.type !== "line" || graphic.items === undefined) {
51
+ throw new Error(`Rule mark "${id}" requires line collection graphics.`);
52
+ }
53
+ return { dataset, graphic, layer };
54
+ }
55
+
56
+ function validateEndpointBindings(layer) {
57
+ if (
58
+ layer.encoding?.x2 !== undefined &&
59
+ layer.encoding.x2.scale !== layer.encoding?.x?.scale
60
+ ) {
61
+ throw new Error(`Rule mark "${layer.id}" requires x and x2 to share one scale.`);
62
+ }
63
+ if (
64
+ layer.encoding?.y2 !== undefined &&
65
+ layer.encoding.y2.scale !== layer.encoding?.y?.scale
66
+ ) {
67
+ throw new Error(`Rule mark "${layer.id}" requires y and y2 to share one scale.`);
68
+ }
69
+ }
70
+
71
+ const createRuleMark = action(
72
+ {
73
+ op: "createRuleMark",
74
+ description: "Create a semantic rule mark and empty line collection."
75
+ },
76
+ function (args = {}) {
77
+ validateMarkOptions(args, CREATE_OPTIONS, "createRuleMark");
78
+ const id = resolveMarkId(this, args.id, {
79
+ defaultId: "rule",
80
+ label: "Rule mark id",
81
+ markType: "rule",
82
+ operation: "createRuleMark"
83
+ });
84
+ const { data } = resolveMarkData(this, args);
85
+ assertMarkAvailable(this, id);
86
+
87
+ return this
88
+ .editSemantic({ property: `layer[${id}].mark.type`, value: "rule" })
89
+ .editSemantic({ property: `layer[${id}].data`, value: data })
90
+ .createGraphics({
91
+ id,
92
+ type: "line",
93
+ length: 0,
94
+ ...resolveMarkGraphicPlacement(this, { data, markType: "rule" })
95
+ })
96
+ ._withMarkConfig(id, DEFAULT_RULE_CONFIG);
97
+ }
98
+ );
99
+
100
+ const rematerializeRuleMark = action(
101
+ {
102
+ op: "rematerializeRuleMark",
103
+ description: "Recompute concrete rule endpoints and appearance."
104
+ },
105
+ function (args = {}) {
106
+ validateMarkOptions(args, REMATERIALIZE_OPTIONS, "rematerializeRuleMark");
107
+ const id = validateUserId(args.id, "Rule mark id");
108
+ const highlights = Object.entries(
109
+ this.materializationConfigs.highlights ?? {}
110
+ ).filter(([, config]) => config.target === id);
111
+ if (highlights.length > 0) {
112
+ let baseline = this;
113
+ for (const [highlightId] of highlights) {
114
+ baseline = baseline._withoutMaterializationConfig(["highlights", highlightId]);
115
+ }
116
+ return baseline
117
+ .editGraphics({ target: id, property: "length", value: 0 })
118
+ .rematerializeRuleMark({ id })
119
+ .rematerializeMarkHighlights({ target: id, highlights });
120
+ }
121
+ const { dataset, graphic, layer } = requireRule(this, id);
122
+ validateEndpointBindings(layer);
123
+ const config = this.markConfigs[id] ?? DEFAULT_RULE_CONFIG;
124
+ const fixedSpan = config.fixedSpan;
125
+ const boxSpan = config.boxSpanOwner;
126
+ const mode = boxSpan !== undefined
127
+ ? "box-span"
128
+ : fixedSpan !== undefined &&
129
+ layer.encoding?.x?.scale !== undefined &&
130
+ layer.encoding?.y?.scale !== undefined
131
+ ? "fixed-span"
132
+ : resolveRuleMode(layer);
133
+ if (mode === undefined) {
134
+ return graphic.items.length === 0
135
+ ? this
136
+ : this.editGraphics({ target: id, property: "length", value: 0 });
137
+ }
138
+
139
+ const scaleIds = [...new Set(
140
+ ["x", "y", "x2", "y2", "strokeDash", "opacity"]
141
+ .map(channel => layer.encoding?.[channel]?.scale)
142
+ .filter(scale => scale !== undefined)
143
+ )];
144
+ let resolved = this;
145
+ for (const scaleId of scaleIds) {
146
+ resolved = resolved.rematerializeScale({ id: scaleId });
147
+ }
148
+
149
+ const derived = deriveRuleValues(dataset.values, layer);
150
+ const mapped = {};
151
+ for (const channel of ["x", "y", "x2", "y2"]) {
152
+ const encoding = layer.encoding?.[channel];
153
+ if (encoding === undefined) continue;
154
+ mapped[channel] = mapPosition(
155
+ derived.values[channel],
156
+ resolved.resolvedScales[encoding.scale]
157
+ );
158
+ }
159
+ const bounds = resolveGraphicBounds(resolved);
160
+ const x1 = [];
161
+ const y1 = [];
162
+ const x2 = [];
163
+ const y2 = [];
164
+ for (let index = 0; index < derived.length; index += 1) {
165
+ if (mode === "vertical-span") {
166
+ x1.push(mapped.x[index]);
167
+ y1.push(bounds.y);
168
+ x2.push(mapped.x[index]);
169
+ y2.push(bounds.y + bounds.height);
170
+ } else if (mode === "horizontal-span") {
171
+ x1.push(bounds.x);
172
+ y1.push(mapped.y[index]);
173
+ x2.push(bounds.x + bounds.width);
174
+ y2.push(mapped.y[index]);
175
+ } else if (mode === "vertical-interval") {
176
+ x1.push(mapped.x[index]);
177
+ y1.push(mapped.y[index]);
178
+ x2.push(mapped.x[index]);
179
+ y2.push(mapped.y2[index]);
180
+ } else if (mode === "horizontal-interval") {
181
+ x1.push(mapped.x[index]);
182
+ y1.push(mapped.y[index]);
183
+ x2.push(mapped.x2[index]);
184
+ y2.push(mapped.y[index]);
185
+ } else if (mode === "box-span") {
186
+ const owner = resolved.graphicSpec.objects[boxSpan];
187
+ const box = owner?.items?.[index]?.properties;
188
+ if (box === undefined) throw new Error(`Rule mark "${id}" requires box span owner "${boxSpan}".`);
189
+ if (layer.encoding?.x?.fieldType === "quantitative") {
190
+ x1.push(mapped.x[index]);
191
+ y1.push(box.y);
192
+ x2.push(mapped.x[index]);
193
+ y2.push(box.y + box.height);
194
+ } else {
195
+ x1.push(box.x);
196
+ y1.push(mapped.y[index]);
197
+ x2.push(box.x + box.width);
198
+ y2.push(mapped.y[index]);
199
+ }
200
+ } else if (mode === "fixed-span") {
201
+ if (fixedSpan.orientation === "horizontal") {
202
+ x1.push(mapped.x[index] - fixedSpan.size / 2);
203
+ y1.push(mapped.y[index]);
204
+ x2.push(mapped.x[index] + fixedSpan.size / 2);
205
+ y2.push(mapped.y[index]);
206
+ } else {
207
+ x1.push(mapped.x[index]);
208
+ y1.push(mapped.y[index] - fixedSpan.size / 2);
209
+ x2.push(mapped.x[index]);
210
+ y2.push(mapped.y[index] + fixedSpan.size / 2);
211
+ }
212
+ } else {
213
+ x1.push(mapped.x[index]);
214
+ y1.push(mapped.y[index]);
215
+ x2.push(mapped.x2[index]);
216
+ y2.push(mapped.y2[index]);
217
+ }
218
+ }
219
+
220
+ const resolvedConfig = resolved.markConfigs[id] ?? DEFAULT_RULE_CONFIG;
221
+ const dashEncoding = layer.encoding?.strokeDash;
222
+ const opacityEncoding = layer.encoding?.opacity;
223
+ const strokeDash = dashEncoding?.scale === undefined
224
+ ? Array.from(
225
+ { length: derived.length },
226
+ () => normalizeStrokeDashPattern(
227
+ dashEncoding?.datum ?? resolvedConfig.strokeDash
228
+ )
229
+ )
230
+ : mapOrdinalValues(
231
+ derived.values.strokeDash,
232
+ resolved.resolvedScales[dashEncoding.scale].domain,
233
+ resolved.resolvedScales[dashEncoding.scale].range
234
+ );
235
+ const opacity = opacityEncoding?.scale === undefined
236
+ ? resolvedConfig.opacity
237
+ : mapContinuousScaleValues(
238
+ derived.values.opacity,
239
+ resolved.resolvedScales[opacityEncoding.scale]
240
+ );
241
+
242
+ return resolved
243
+ .editGraphics({ target: id, property: "length", value: derived.length })
244
+ .editGraphics({ target: id, property: "x1", value: x1 })
245
+ .editGraphics({ target: id, property: "y1", value: y1 })
246
+ .editGraphics({ target: id, property: "x2", value: x2 })
247
+ .editGraphics({ target: id, property: "y2", value: y2 })
248
+ .editGraphics({ target: id, property: "stroke", value: resolvedConfig.stroke })
249
+ .editGraphics({
250
+ target: id,
251
+ property: "strokeWidth",
252
+ value: resolvedConfig.strokeWidth
253
+ })
254
+ .editGraphics({ target: id, property: "strokeDash", value: strokeDash })
255
+ .editGraphics({ target: id, property: "opacity", value: opacity });
256
+ }
257
+ );
258
+
259
+ export const materializeRuleSpan = action(
260
+ {
261
+ op: "materializeRuleSpan",
262
+ description: "Materialize a fixed-pixel span around a rule anchor."
263
+ },
264
+ function (args = {}) {
265
+ validateMarkOptions(args, SPAN_OPTIONS, "materializeRuleSpan");
266
+ const id = validateUserId(args.id, "Rule mark id");
267
+ requireRule(this, id);
268
+ if (!["horizontal", "vertical"].includes(args.orientation)) {
269
+ throw new Error(
270
+ `Unsupported rule span orientation "${args.orientation}".`
271
+ );
272
+ }
273
+ validatePositiveFinite(args.size, "Rule span size");
274
+ return this
275
+ ._withMarkConfig(id, {
276
+ ...this.markConfigs[id],
277
+ fixedSpan: { orientation: args.orientation, size: args.size }
278
+ })
279
+ .rematerializeRuleMark({ id });
280
+ }
281
+ );
282
+
283
+ export function registerRuleMarkActions(ProgramClass) {
284
+ ProgramClass.prototype.createRuleMark = createRuleMark;
285
+ ProgramClass.prototype.rematerializeRuleMark = rematerializeRuleMark;
286
+ ProgramClass.prototype.materializeRuleSpan = materializeRuleSpan;
287
+ }
@@ -0,0 +1,112 @@
1
+ import {
2
+ resolveOptionalUserId,
3
+ validateUserId
4
+ } from "../../core/identifiers.js";
5
+ import { findDataset } from "../../selectors/datasets.js";
6
+ import { findLayer, hasLayer } from "../../selectors/layers.js";
7
+
8
+ export function validateMarkOptions(args, supported, operation) {
9
+ for (const key of Object.keys(args)) {
10
+ if (!supported.includes(key)) {
11
+ throw new Error(`Unknown ${operation} option "${key}".`);
12
+ }
13
+ }
14
+ }
15
+
16
+ export function resolveMarkData(program, requested) {
17
+ const data = Object.hasOwn(requested, "data")
18
+ ? validateUserId(requested.data, "Dataset id")
19
+ : program.context.currentData;
20
+
21
+ if (data === undefined) {
22
+ throw new Error("Mark creation requires data or a current dataset.");
23
+ }
24
+
25
+ const dataset = findDataset(program, data);
26
+
27
+ if (dataset === undefined) {
28
+ throw new Error(`Unknown dataset "${data}".`);
29
+ }
30
+
31
+ return { data, dataset };
32
+ }
33
+
34
+ export function resolveMarkId(program, requested, {
35
+ defaultId,
36
+ label,
37
+ markType,
38
+ operation
39
+ }) {
40
+ const sameRoleExists = program.semanticSpec.layers.some(
41
+ layer => layer.mark?.type === markType
42
+ );
43
+ const defaultUnavailable = hasLayer(program, defaultId) ||
44
+ program.graphicSpec.objects[defaultId] !== undefined;
45
+ return resolveOptionalUserId(requested, {
46
+ defaultId,
47
+ label,
48
+ operation,
49
+ ambiguous: sameRoleExists || defaultUnavailable
50
+ });
51
+ }
52
+
53
+ export function assertMarkAvailable(program, id) {
54
+ if (hasLayer(program, id)) {
55
+ throw new Error(`Mark "${id}" already exists.`);
56
+ }
57
+
58
+ if (program.graphicSpec.objects[id] !== undefined) {
59
+ throw new Error(`Graphic "${id}" already exists.`);
60
+ }
61
+ }
62
+
63
+ function inheritedPositionEncoding(encoding) {
64
+ if (encoding === undefined) return undefined;
65
+ return Object.fromEntries(
66
+ ["field", "datum", "fieldType", "scale", "title"]
67
+ .filter(property => Object.hasOwn(encoding, property))
68
+ .map(property => [property, encoding[property]])
69
+ );
70
+ }
71
+
72
+ function eligibleLayeredSource(layer, requestedData) {
73
+ if (layer?.data === undefined) return false;
74
+ if (requestedData !== undefined && layer.data !== requestedData) return false;
75
+ return layer.encoding?.x !== undefined || layer.encoding?.y !== undefined;
76
+ }
77
+
78
+ export function resolveLayeredMarkInheritance(program, requested = {}) {
79
+ const requestedData = Object.hasOwn(requested, "data")
80
+ ? validateUserId(requested.data, "Dataset id")
81
+ : undefined;
82
+ const eligible = program.semanticSpec.layers.filter(layer =>
83
+ eligibleLayeredSource(layer, requestedData)
84
+ );
85
+ const current = findLayer(program, program.context.currentMark);
86
+ const source = eligibleLayeredSource(current, requestedData)
87
+ ? current
88
+ : eligible.length === 1
89
+ ? eligible[0]
90
+ : undefined;
91
+
92
+ if (source === undefined && eligible.length > 1) {
93
+ throw new Error(
94
+ "Layered mark inference is ambiguous; specify data and encode its position explicitly."
95
+ );
96
+ }
97
+ if (source === undefined) return undefined;
98
+
99
+ return {
100
+ source: source.id,
101
+ data: source.data,
102
+ coordinate: source.coordinate,
103
+ encoding: Object.fromEntries(
104
+ ["x", "y"]
105
+ .map(channel => [
106
+ channel,
107
+ inheritedPositionEncoding(source.encoding?.[channel])
108
+ ])
109
+ .filter(([, encoding]) => encoding !== undefined)
110
+ )
111
+ };
112
+ }
@@ -0,0 +1,163 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateUserId } from "../../core/identifiers.js";
3
+ import { freezeOwned } from "../../core/immutable.js";
4
+ import {
5
+ isDrawableGraphicType,
6
+ isGraphicContainerType,
7
+ validateGraphicType
8
+ } from "../../grammar/schemas/graphic.js";
9
+ import {
10
+ findGraphicParent,
11
+ graphicSiblings
12
+ } from "../../grammar/schemas/graphicTree.js";
13
+
14
+ const GRAPHIC_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
15
+
16
+ function createGraphicDefinition({ id, type, length }) {
17
+ if (type === "collection") {
18
+ return freezeOwned({ type, items: freezeOwned([]) });
19
+ }
20
+ if (length === undefined) {
21
+ return freezeOwned({ type, properties: freezeOwned({}) });
22
+ }
23
+ return freezeOwned({
24
+ type,
25
+ items: freezeOwned(Array.from({ length }, (_, index) => freezeOwned({
26
+ id: `${id}:${index}`,
27
+ properties: freezeOwned({})
28
+ }))),
29
+ });
30
+ }
31
+
32
+ function sameDefinition(existing, { type, length }) {
33
+ if (existing.type !== type) return false;
34
+ if (type === "collection") return length === undefined && existing.items?.length === 0;
35
+ if (length === undefined) return existing.items === undefined;
36
+ return existing.items?.length === length;
37
+ }
38
+
39
+ function samePlacement(graphicSpec, id, { parent, before, after }) {
40
+ const actualParent = findGraphicParent(graphicSpec, id);
41
+ const actualParentId = actualParent?.kind === "parent" ? actualParent.id : undefined;
42
+ if (actualParentId !== parent) return false;
43
+ const siblings = graphicSiblings(graphicSpec, id);
44
+ const index = siblings.indexOf(id);
45
+ if (before !== undefined) return index < siblings.indexOf(before);
46
+ if (after !== undefined) return index > siblings.indexOf(after);
47
+ return true;
48
+ }
49
+
50
+ function resolvePlacement(graphicSpec, { id, type, parent, before, after }) {
51
+ if (before !== undefined && after !== undefined) {
52
+ throw new Error("createGraphics cannot use before and after together.");
53
+ }
54
+ if (parent !== undefined) {
55
+ validateUserId(parent, "Graphic parent");
56
+ if (parent === id) throw new Error("createGraphics cannot attach a graphic to itself.");
57
+ const parentGraphic = graphicSpec.objects[parent];
58
+ if (parentGraphic === undefined) {
59
+ throw new Error(`Unknown graphic parent "${parent}".`);
60
+ }
61
+ if (!isGraphicContainerType(parentGraphic.type)) {
62
+ throw new Error(`Graphic parent "${parent}" must be a canvas or collection.`);
63
+ }
64
+ if (type === "canvas") {
65
+ throw new Error("Canvas graphics cannot have a parent.");
66
+ }
67
+ }
68
+ const anchor = before ?? after;
69
+ if (anchor === undefined) return;
70
+ validateUserId(anchor, "Graphic placement target");
71
+ if (anchor === id) {
72
+ throw new Error("createGraphics cannot place a graphic relative to itself.");
73
+ }
74
+ const anchorGraphic = graphicSpec.objects[anchor];
75
+ if (anchorGraphic === undefined) {
76
+ throw new Error(`Unknown graphic placement target "${anchor}".`);
77
+ }
78
+ const anchorParent = findGraphicParent(graphicSpec, anchor);
79
+ const anchorParentId = anchorParent?.kind === "parent" ? anchorParent.id : undefined;
80
+ if (anchorParentId !== parent) {
81
+ throw new Error("Graphic placement target must be a direct sibling.");
82
+ }
83
+ if (parent === undefined && before === "canvas") {
84
+ throw new Error("createGraphics cannot place a graphic before the canvas.");
85
+ }
86
+ if (type === "canvas") {
87
+ throw new Error("Canvas graphics do not accept before or after placement.");
88
+ }
89
+ }
90
+
91
+ function insertSibling(siblings, id, { before, after }) {
92
+ const next = [...siblings];
93
+ if (before !== undefined) next.splice(next.indexOf(before), 0, id);
94
+ else if (after !== undefined) next.splice(next.indexOf(after) + 1, 0, id);
95
+ else next.push(id);
96
+ return freezeOwned(next);
97
+ }
98
+
99
+ const createGraphics = action(
100
+ {
101
+ op: "createGraphics",
102
+ description: "Create and optionally attach a concrete graphic."
103
+ },
104
+ function ({ id, type, length, parent, before, after } = {}) {
105
+ if (typeof id !== "string" || !GRAPHIC_ID_PATTERN.test(id)) {
106
+ throw new TypeError(
107
+ "createGraphics requires an id containing only letters, numbers, _ or -."
108
+ );
109
+ }
110
+ const validatedType = validateGraphicType(type);
111
+ if (length !== undefined && (!Number.isInteger(length) || length < 0)) {
112
+ throw new TypeError("createGraphics length must be a non-negative integer.");
113
+ }
114
+ if (validatedType === "collection" && length !== undefined) {
115
+ throw new Error("Heterogeneous collections use editGraphics items instead of length.");
116
+ }
117
+ if (length !== undefined && !isDrawableGraphicType(validatedType)) {
118
+ throw new Error(`Graphic type "${validatedType}" does not accept length.`);
119
+ }
120
+ resolvePlacement(this.graphicSpec, {
121
+ id, type: validatedType, parent, before, after
122
+ });
123
+
124
+ const existing = this.graphicSpec.objects[id];
125
+ if (existing !== undefined) {
126
+ if (
127
+ sameDefinition(existing, { type: validatedType, length }) &&
128
+ samePlacement(this.graphicSpec, id, { parent, before, after })
129
+ ) return this;
130
+ throw new Error(`Graphic "${id}" already exists with a different definition or placement.`);
131
+ }
132
+ if (
133
+ validatedType === "canvas" &&
134
+ Object.values(this.graphicSpec.objects).some(graphic => graphic.type === "canvas")
135
+ ) {
136
+ throw new Error("graphicSpec supports exactly one canvas.");
137
+ }
138
+
139
+ const objects = {
140
+ ...this.graphicSpec.objects,
141
+ [id]: createGraphicDefinition({ id, type: validatedType, length })
142
+ };
143
+ let order = this.graphicSpec.order;
144
+ if (parent === undefined) {
145
+ order = insertSibling(order, id, { before, after });
146
+ } else {
147
+ objects[parent] = freezeOwned({
148
+ ...objects[parent],
149
+ children: insertSibling(objects[parent].children ?? [], id, { before, after })
150
+ });
151
+ }
152
+ return this._clone({
153
+ graphicSpec: freezeOwned({
154
+ objects: freezeOwned(objects),
155
+ order
156
+ })
157
+ });
158
+ }
159
+ );
160
+
161
+ export function registerCreateGraphicsAction(ProgramClass) {
162
+ ProgramClass.prototype.createGraphics = createGraphics;
163
+ }