ggaction 0.0.2 → 0.0.4

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 (242) hide show
  1. package/CHANGELOG.md +55 -2
  2. package/README.md +95 -72
  3. package/package.json +15 -5
  4. package/src/actions/boxPlots/components.js +3 -3
  5. package/src/actions/boxPlots/edit.js +277 -0
  6. package/src/actions/boxPlots/index.js +2 -0
  7. package/src/actions/boxPlots/materialize.js +12 -1
  8. package/src/actions/boxPlots/options.js +25 -14
  9. package/src/actions/canvas/actions.js +21 -17
  10. package/src/actions/composition/actions.js +247 -0
  11. package/src/actions/composition/index.js +1 -0
  12. package/src/actions/coordinates/actions.js +2 -5
  13. package/src/actions/data/density.js +7 -1
  14. package/src/actions/data/filter.js +3 -1
  15. package/src/actions/data/regression.js +1 -1
  16. package/src/actions/encodings/appearance.js +12 -1
  17. package/src/actions/encodings/barWidth.js +5 -3
  18. package/src/actions/encodings/color/continuous.js +137 -0
  19. package/src/actions/encodings/color/index.js +151 -0
  20. package/src/actions/encodings/color/layout.js +91 -0
  21. package/src/actions/encodings/color/policy.js +88 -0
  22. package/src/actions/encodings/index.js +4 -2
  23. package/src/actions/encodings/offset.js +91 -73
  24. package/src/actions/encodings/position/apply.js +66 -0
  25. package/src/actions/encodings/position/index.js +130 -0
  26. package/src/actions/encodings/position/policies/arc.js +26 -0
  27. package/src/actions/encodings/position/policies/bar.js +9 -5
  28. package/src/actions/encodings/position/policies/index.js +5 -1
  29. package/src/actions/encodings/position/policies/line.js +20 -3
  30. package/src/actions/encodings/position/resolve.js +58 -9
  31. package/src/actions/encodings/ranged.js +30 -8
  32. package/src/actions/encodings/shared.js +17 -21
  33. package/src/actions/encodings/strokeDash.js +1 -1
  34. package/src/actions/encodings/text.js +73 -0
  35. package/src/actions/errorBands/create.js +37 -29
  36. package/src/actions/errorBands/edit.js +206 -0
  37. package/src/actions/errorBands/index.js +9 -0
  38. package/src/actions/errorBands/options.js +39 -0
  39. package/src/actions/errorBars/create.js +50 -43
  40. package/src/actions/errorBars/edit.js +130 -0
  41. package/src/actions/errorBars/index.js +3 -0
  42. package/src/actions/errorBars/options.js +52 -0
  43. package/src/actions/facets/actions.js +162 -0
  44. package/src/actions/facets/derive.js +200 -0
  45. package/src/actions/facets/guides.js +16 -0
  46. package/src/actions/facets/index.js +1 -0
  47. package/src/actions/facets/replay.js +69 -0
  48. package/src/actions/guides/applicability.js +113 -0
  49. package/src/actions/guides/axes/axes.js +70 -16
  50. package/src/actions/guides/axes/edit.js +126 -0
  51. package/src/actions/guides/axes/index.js +12 -0
  52. package/src/actions/guides/axes/labels.js +22 -19
  53. package/src/actions/guides/axes/remove.js +60 -0
  54. package/src/actions/guides/axes/tickGroups.js +7 -5
  55. package/src/actions/guides/axes/ticks.js +10 -5
  56. package/src/actions/guides/axes/titles.js +8 -6
  57. package/src/actions/guides/grids/grid.js +144 -15
  58. package/src/actions/guides/grids/resolve.js +6 -14
  59. package/src/actions/guides/guides.js +17 -75
  60. package/src/actions/guides/index.js +6 -0
  61. package/src/actions/guides/legends/categorical/actions.js +48 -9
  62. package/src/actions/guides/legends/categorical/components.js +16 -16
  63. package/src/actions/guides/legends/categorical/index.js +14 -14
  64. package/src/actions/guides/legends/categorical/layout.js +7 -3
  65. package/src/actions/guides/legends/categorical/options.js +2 -5
  66. package/src/actions/guides/legends/categorical/resolve.js +2 -2
  67. package/src/actions/guides/legends/categorical/symbols.js +14 -14
  68. package/src/actions/guides/legends/continuous/common.js +7 -7
  69. package/src/actions/guides/legends/continuous/interval.js +6 -6
  70. package/src/actions/guides/legends/continuous/opacity.js +1 -1
  71. package/src/actions/guides/legends/edit.js +4 -26
  72. package/src/actions/guides/legends/focused.js +63 -0
  73. package/src/actions/guides/legends/index.js +4 -0
  74. package/src/actions/guides/legends/remove.js +46 -0
  75. package/src/actions/guides/legends/size.js +13 -15
  76. package/src/actions/guides/legends/target.js +25 -0
  77. package/src/actions/guides/polar/axes/facade.js +219 -0
  78. package/src/actions/guides/polar/axes/index.js +55 -0
  79. package/src/actions/guides/polar/axes/labels.js +188 -0
  80. package/src/actions/guides/polar/axes/lines.js +113 -0
  81. package/src/actions/guides/polar/axes/shared.js +112 -0
  82. package/src/actions/guides/polar/axes/ticks.js +164 -0
  83. package/src/actions/guides/polar/axes/titles.js +165 -0
  84. package/src/actions/guides/polar/grids.js +252 -0
  85. package/src/actions/guides/polar/index.js +2 -0
  86. package/src/actions/guides/polar/resolve.js +269 -0
  87. package/src/actions/guides/tickValues.js +1 -1
  88. package/src/actions/index.js +4 -0
  89. package/src/actions/intervals/resolve.js +1 -1
  90. package/src/actions/marks/arc/actions.js +239 -0
  91. package/src/actions/marks/arc/index.js +1 -0
  92. package/src/actions/marks/{area.js → area/actions.js} +51 -38
  93. package/src/actions/marks/area/index.js +4 -0
  94. package/src/actions/marks/bar/create.js +26 -5
  95. package/src/actions/marks/bar/edit.js +1 -1
  96. package/src/actions/marks/bar/materialize.js +25 -31
  97. package/src/actions/marks/index.js +12 -4
  98. package/src/actions/marks/lifecycle.js +27 -0
  99. package/src/actions/marks/line/actions.js +373 -0
  100. package/src/actions/marks/line/index.js +1 -0
  101. package/src/actions/marks/{point.js → point/actions.js} +67 -108
  102. package/src/actions/marks/point/index.js +1 -0
  103. package/src/actions/marks/rect/actions.js +165 -0
  104. package/src/actions/marks/rect/index.js +1 -0
  105. package/src/actions/marks/remove.js +214 -0
  106. package/src/actions/marks/{rule.js → rule/actions.js} +34 -28
  107. package/src/actions/marks/rule/index.js +1 -0
  108. package/src/actions/marks/shared.js +156 -25
  109. package/src/actions/marks/text/actions.js +192 -0
  110. package/src/actions/marks/text/index.js +1 -0
  111. package/src/actions/primitives/createGraphics.js +13 -6
  112. package/src/actions/primitives/editGraphics.js +10 -3
  113. package/src/actions/primitives/semantic.js +26 -3
  114. package/src/actions/primitives/semanticValue.js +26 -4
  115. package/src/actions/regression/components.js +2 -2
  116. package/src/actions/regression/create.js +19 -2
  117. package/src/actions/regression/edit.js +247 -0
  118. package/src/actions/regression/index.js +3 -0
  119. package/src/actions/regression/resolve.js +3 -1
  120. package/src/actions/scales/consumers/common.js +91 -0
  121. package/src/actions/scales/consumers/families.js +47 -0
  122. package/src/actions/scales/consumers/index.js +41 -0
  123. package/src/actions/scales/{consumers.js → consumers/seriesLayout.js} +14 -127
  124. package/src/actions/scales/create.js +1 -1
  125. package/src/actions/scales/definitions.js +15 -6
  126. package/src/actions/scales/edit.js +45 -22
  127. package/src/actions/scales/materialize.js +58 -409
  128. package/src/actions/selection/actions.js +38 -24
  129. package/src/actions/selection/index.js +2 -0
  130. package/src/actions/titles/actions.js +63 -2
  131. package/src/actions/titles/index.js +2 -0
  132. package/src/actions/titles/resolve.js +12 -17
  133. package/src/composition.js +10 -0
  134. package/src/core/ChartProgram.js +49 -65
  135. package/src/core/action.js +10 -0
  136. package/src/core/compositionState.js +169 -0
  137. package/src/core/materializationState.js +65 -0
  138. package/src/core/programState.js +28 -0
  139. package/src/core/validation.js +16 -0
  140. package/src/core/vocabulary.js +154 -4
  141. package/src/grammar/arcs.js +154 -0
  142. package/src/grammar/areaSeries.js +1 -1
  143. package/src/grammar/bars/aggregate.js +3 -3
  144. package/src/grammar/bars/geometry.js +3 -3
  145. package/src/grammar/bars/policy.js +7 -1
  146. package/src/grammar/coordinates.js +3 -7
  147. package/src/grammar/density.js +21 -2
  148. package/src/grammar/facets/dependencies.js +185 -0
  149. package/src/grammar/facets/guides.js +257 -0
  150. package/src/grammar/facets/index.js +122 -0
  151. package/src/grammar/facets/scales.js +254 -0
  152. package/src/grammar/interval.js +2 -96
  153. package/src/grammar/lineSeries.js +112 -4
  154. package/src/grammar/palettes.js +5 -7
  155. package/src/grammar/polar.js +102 -0
  156. package/src/grammar/polarGuides.js +243 -0
  157. package/src/grammar/polarLineCommands.js +43 -0
  158. package/src/grammar/polarPaths.js +161 -0
  159. package/src/grammar/positionCompatibility.js +18 -2
  160. package/src/grammar/rects.js +26 -0
  161. package/src/grammar/regression/derive.js +97 -0
  162. package/src/grammar/regression/index.js +22 -0
  163. package/src/grammar/regression/models.js +282 -0
  164. package/src/grammar/regression/parameters.js +106 -0
  165. package/src/grammar/rules.js +1 -1
  166. package/src/grammar/scales/color.js +4 -2
  167. package/src/grammar/scales/continuous.js +6 -1
  168. package/src/grammar/scales/fields.js +7 -0
  169. package/src/grammar/scales/ordinal.js +5 -2
  170. package/src/grammar/scales/policies.js +1 -1
  171. package/src/grammar/schemas/graphic.js +1 -1
  172. package/src/grammar/schemas/graphicBounds.js +64 -9
  173. package/src/grammar/schemas/graphicTree.js +44 -10
  174. package/src/grammar/schemas/semanticPath.js +31 -9
  175. package/src/grammar/seriesLayout.js +13 -2
  176. package/src/grammar/statistics/studentT.js +98 -0
  177. package/src/grammar/text.js +71 -0
  178. package/src/grammar/ticks.js +50 -13
  179. package/src/grammar/transforms.js +1 -1
  180. package/src/index.js +1 -0
  181. package/src/layout/composition.js +310 -0
  182. package/src/layout/facets.js +131 -0
  183. package/src/layout/title.js +6 -0
  184. package/src/materialization/bars/aggregate.js +6 -3
  185. package/src/materialization/bars/grouped.js +75 -37
  186. package/src/materialization/bars/histogram.js +1 -1
  187. package/src/materialization/bars/ranged.js +1 -1
  188. package/src/materialization/composition.js +152 -0
  189. package/src/materialization/compositionSnapshot.js +114 -0
  190. package/src/materialization/density.js +1 -1
  191. package/src/materialization/dependencies.js +47 -73
  192. package/src/materialization/encodings.js +14 -28
  193. package/src/materialization/facetGuides/index.js +2 -0
  194. package/src/materialization/facetGuides/legacyCategorical.js +125 -0
  195. package/src/materialization/facetGuides/placement.js +126 -0
  196. package/src/materialization/facetGuides/preparation.js +188 -0
  197. package/src/materialization/facets.js +214 -0
  198. package/src/materialization/graphicHierarchy.js +15 -5
  199. package/src/materialization/guides/resources.js +46 -0
  200. package/src/materialization/marks/capabilities.js +141 -0
  201. package/src/materialization/marks/index.js +105 -0
  202. package/src/materialization/marks/policies.js +87 -0
  203. package/src/materialization/planner.js +105 -12
  204. package/src/materialization/rect.js +138 -0
  205. package/src/materialization/rectConfig.js +38 -0
  206. package/src/materialization/rowEncoding.js +33 -0
  207. package/src/materialization/scaleGuideDependencies.js +94 -0
  208. package/src/materialization/scales/map.js +40 -0
  209. package/src/materialization/scales/policies/arc.js +37 -0
  210. package/src/materialization/scales/policies/bar.js +132 -0
  211. package/src/materialization/scales/policies/series.js +76 -0
  212. package/src/materialization/scales/resolve.js +326 -0
  213. package/src/materialization/selection/items/arc.js +29 -0
  214. package/src/materialization/selection/items/bar.js +255 -0
  215. package/src/materialization/selection/items/common.js +131 -0
  216. package/src/materialization/selection/items/index.js +6 -0
  217. package/src/materialization/selection/items/path.js +59 -0
  218. package/src/materialization/selection/items/point.js +55 -0
  219. package/src/materialization/selection/items/rect.js +23 -0
  220. package/src/materialization/selection/items/rule.js +25 -0
  221. package/src/materialization/selection/policies/arc.js +10 -0
  222. package/src/materialization/selection/policies/area.js +1 -1
  223. package/src/materialization/selection/policies/bar.js +1 -1
  224. package/src/materialization/selection/policies/index.js +5 -1
  225. package/src/materialization/selection/policies/line.js +1 -1
  226. package/src/materialization/selection/policies/point.js +1 -1
  227. package/src/materialization/selection/policies/rect.js +10 -0
  228. package/src/materialization/selection/policies/rule.js +1 -1
  229. package/src/materialization/selection/styles.js +25 -17
  230. package/src/materialization/text.js +156 -0
  231. package/src/renderers/canvas/index.js +68 -22
  232. package/src/renderers/canvas/text.js +14 -1
  233. package/types/extension.d.ts +5 -1
  234. package/types/index.d.ts +56 -1
  235. package/types/program.d.ts +664 -27
  236. package/src/actions/encodings/color.js +0 -329
  237. package/src/actions/encodings/position.js +0 -176
  238. package/src/actions/marks/line.js +0 -248
  239. package/src/grammar/regression.js +0 -578
  240. package/src/grammar/scales.js +0 -1
  241. package/src/materialization/marks.js +0 -110
  242. package/src/materialization/selection/items.js +0 -503
@@ -4,13 +4,18 @@ import {
4
4
  } from "../../core/identifiers.js";
5
5
  import { findDataset } from "../../selectors/datasets.js";
6
6
  import { findLayer, hasLayer } from "../../selectors/layers.js";
7
+ import { findSemanticScale } from "../../selectors/scales.js";
8
+ import { resolveMarkPositionPolicy } from "../encodings/position/policies/index.js";
9
+ import { getMarkMaterializationStep } from "../../materialization/marks/index.js";
10
+ import {
11
+ POLAR_POSITION_CHANNELS,
12
+ CARTESIAN_POSITION_CHANNELS,
13
+ POSITION_CHANNELS
14
+ } from "../../core/vocabulary.js";
15
+ import { validateOptionObject } from "../../core/validation.js";
7
16
 
8
17
  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
- }
18
+ validateOptionObject(args, supported, operation);
14
19
  }
15
20
 
16
21
  export function resolveMarkData(program, requested) {
@@ -60,30 +65,120 @@ export function assertMarkAvailable(program, id) {
60
65
  }
61
66
  }
62
67
 
63
- function inheritedPositionEncoding(encoding) {
64
- if (encoding === undefined) return undefined;
65
- return Object.fromEntries(
66
- ["field", "datum", "fieldType", "scale", "title"]
68
+ const POSITION_POLICY_PROPERTIES = Object.freeze(["aggregate", "bin", "stack"]);
69
+ const POSITION_POLICY_SUBSETS = Object.freeze([
70
+ Object.freeze(["aggregate", "bin", "stack"]),
71
+ Object.freeze(["aggregate", "bin"]),
72
+ Object.freeze(["aggregate", "stack"]),
73
+ Object.freeze(["bin", "stack"]),
74
+ Object.freeze(["aggregate"]),
75
+ Object.freeze(["bin"]),
76
+ Object.freeze(["stack"]),
77
+ Object.freeze([])
78
+ ]);
79
+
80
+ function inheritedPositionEncodings(encoding) {
81
+ if (encoding?.field === undefined) return undefined;
82
+ const base = Object.fromEntries(
83
+ ["field", "fieldType", "scale", "title"]
67
84
  .filter(property => Object.hasOwn(encoding, property))
68
85
  .map(property => [property, encoding[property]])
69
86
  );
87
+ const available = POSITION_POLICY_PROPERTIES.filter(property =>
88
+ Object.hasOwn(encoding, property)
89
+ );
90
+ const seen = new Set();
91
+ return POSITION_POLICY_SUBSETS.map(properties => ({
92
+ ...base,
93
+ ...Object.fromEntries(properties
94
+ .filter(property => available.includes(property))
95
+ .map(property => [property, encoding[property]]))
96
+ })).filter(candidate => {
97
+ const key = JSON.stringify(candidate);
98
+ if (seen.has(key)) return false;
99
+ seen.add(key);
100
+ return true;
101
+ });
102
+ }
103
+
104
+ function scaleSupportsEncoding(program, markType, channel, encoding) {
105
+ const scale = findSemanticScale(program, encoding.scale);
106
+ if (scale === undefined) return false;
107
+ const categorical = ["nominal", "ordinal"].includes(encoding.fieldType);
108
+ if (categorical) {
109
+ if (["bar", "rect"].includes(markType)) return scale.type === "band";
110
+ if (markType === "arc") return scale.type === "band";
111
+ return ["ordinal", "band", "point"].includes(scale.type);
112
+ }
113
+ if (encoding.fieldType === "temporal") return scale.type === "time";
114
+ return ["linear", "log", "pow", "sqrt", "symlog"].includes(scale.type);
70
115
  }
71
116
 
72
- function eligibleLayeredSource(layer, requestedData) {
117
+ export function resolveCompatibleEncodings(program, source, markType) {
118
+ const dataset = findDataset(program, source.data);
119
+ if (dataset === undefined) return {};
120
+ const channels = POLAR_POSITION_CHANNELS.some(
121
+ channel => source.encoding?.[channel] !== undefined
122
+ )
123
+ ? POLAR_POSITION_CHANNELS
124
+ : CARTESIAN_POSITION_CHANNELS;
125
+ const pending = new Map(channels.map(channel => [
126
+ channel,
127
+ inheritedPositionEncodings(source.encoding?.[channel])
128
+ ]).filter(([, encodings]) => encodings !== undefined));
129
+ const candidate = {
130
+ id: "layered-inference",
131
+ data: source.data,
132
+ mark: { type: markType },
133
+ encoding: {}
134
+ };
135
+
136
+ for (let pass = 0; pass < 2; pass += 1) {
137
+ for (const [channel, encodings] of pending) {
138
+ for (const encoding of encodings) {
139
+ if (!scaleSupportsEncoding(program, markType, channel, encoding)) continue;
140
+ try {
141
+ const policy = resolveMarkPositionPolicy({
142
+ program,
143
+ layer: candidate,
144
+ dataset,
145
+ channel,
146
+ args: encoding,
147
+ field: encoding.field,
148
+ fieldType: encoding.fieldType
149
+ });
150
+ candidate.encoding[channel] = {
151
+ ...encoding,
152
+ ...Object.fromEntries(
153
+ Object.entries(policy).filter(([, value]) => value !== undefined)
154
+ )
155
+ };
156
+ pending.delete(channel);
157
+ break;
158
+ } catch {
159
+ // Try the next target-compatible policy subset. Another channel may
160
+ // also make a richer subset resolvable on the next pass.
161
+ }
162
+ }
163
+ }
164
+ }
165
+ return candidate.encoding;
166
+ }
167
+
168
+ function eligibleLayeredSource(program, layer, requestedData, markType) {
73
169
  if (layer?.data === undefined) return false;
74
170
  if (requestedData !== undefined && layer.data !== requestedData) return false;
75
- return layer.encoding?.x !== undefined || layer.encoding?.y !== undefined;
171
+ return Object.keys(resolveCompatibleEncodings(program, layer, markType)).length > 0;
76
172
  }
77
173
 
78
- export function resolveLayeredMarkInheritance(program, requested = {}) {
79
- const requestedData = Object.hasOwn(requested, "data")
80
- ? validateUserId(requested.data, "Dataset id")
81
- : undefined;
174
+ export function resolveLayeredMarkInheritance(program, requested = {}, markType) {
175
+ if (Object.hasOwn(requested, "data")) return undefined;
176
+ const requestedData = program.context.currentData;
82
177
  const eligible = program.semanticSpec.layers.filter(layer =>
83
- eligibleLayeredSource(layer, requestedData)
178
+ eligibleLayeredSource(program, layer, requestedData, markType)
84
179
  );
85
180
  const current = findLayer(program, program.context.currentMark);
86
- const source = eligibleLayeredSource(current, requestedData)
181
+ const source = eligibleLayeredSource(program, current, requestedData, markType)
87
182
  ? current
88
183
  : eligible.length === 1
89
184
  ? eligible[0]
@@ -95,18 +190,54 @@ export function resolveLayeredMarkInheritance(program, requested = {}) {
95
190
  );
96
191
  }
97
192
  if (source === undefined) return undefined;
193
+ if (
194
+ markType === "bar" &&
195
+ (
196
+ source.encoding?.x?.bin !== undefined ||
197
+ source.encoding?.y?.bin !== undefined ||
198
+ (source.encoding?.x?.stack !== undefined && source.encoding.x.stack !== null) ||
199
+ (source.encoding?.y?.stack !== undefined && source.encoding.y.stack !== null) ||
200
+ source.encoding?.xOffset !== undefined ||
201
+ source.encoding?.yOffset !== undefined ||
202
+ source.encoding?.color?.layout !== undefined
203
+ )
204
+ ) {
205
+ return undefined;
206
+ }
98
207
 
99
208
  return {
100
209
  source: source.id,
101
210
  data: source.data,
102
211
  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
- )
212
+ encoding: resolveCompatibleEncodings(program, source, markType)
111
213
  };
112
214
  }
215
+
216
+ export function applyLayeredMarkInheritance(program, id, inherited) {
217
+ let next = program;
218
+ if (inherited?.coordinate !== undefined) {
219
+ next = next.editSemantic({
220
+ property: `layer[${id}].coordinate`,
221
+ value: inherited.coordinate
222
+ });
223
+ }
224
+ for (const channel of POSITION_CHANNELS) {
225
+ for (const [property, value] of Object.entries(
226
+ inherited?.encoding[channel] ?? {}
227
+ )) {
228
+ next = next.editSemantic({
229
+ property: `layer[${id}].encoding.${channel}.${property}`,
230
+ value
231
+ });
232
+ }
233
+ }
234
+ return next;
235
+ }
236
+
237
+ export function materializeInheritedMark(program, id) {
238
+ const layer = findLayer(program, id);
239
+ const step = layer === undefined
240
+ ? undefined
241
+ : getMarkMaterializationStep(program, layer);
242
+ return step === undefined ? program : program[step.op](step.args);
243
+ }
@@ -0,0 +1,192 @@
1
+ import { action } from "../../../core/action.js";
2
+ import { validateUserId } from "../../../core/identifiers.js";
3
+ import {
4
+ DEFAULT_TEXT_MARK,
5
+ normalizeTextMarkConfig
6
+ } from "../../../grammar/text.js";
7
+ import { canMaterializeText } from "../../../materialization/marks/index.js";
8
+ import { resolveTextGraphicItems } from "../../../materialization/text.js";
9
+ import { findLayer, resolveEligibleLayer } from "../../../selectors/layers.js";
10
+ import {
11
+ applyLayeredMarkInheritance,
12
+ assertMarkAvailable,
13
+ resolveCompatibleEncodings,
14
+ resolveMarkData,
15
+ resolveMarkId,
16
+ validateMarkOptions
17
+ } from "../shared.js";
18
+ import { resolveMarkGraphicPlacement } from
19
+ "../../../materialization/graphicHierarchy.js";
20
+
21
+ const STYLE_OPTIONS = Object.freeze([
22
+ "fill", "opacity", "fontSize", "fontFamily", "fontWeight",
23
+ "align", "baseline", "rotation", "dx", "dy"
24
+ ]);
25
+ const CREATE_OPTIONS = Object.freeze(["id", "data", "text", ...STYLE_OPTIONS]);
26
+ const EDIT_OPTIONS = Object.freeze(["target", ...STYLE_OPTIONS]);
27
+ const REMATERIALIZE_OPTIONS = Object.freeze(["id"]);
28
+ const SOURCE_TYPES = new Set(["point", "bar", "rule", "rect"]);
29
+
30
+ function eligibleSource(program, layer, requestedData) {
31
+ if (
32
+ !SOURCE_TYPES.has(layer?.mark?.type) ||
33
+ layer.data === undefined ||
34
+ (requestedData !== undefined && layer.data !== requestedData)
35
+ ) return false;
36
+ const encodings = resolveCompatibleEncodings(program, layer, "text");
37
+ return encodings.x !== undefined && encodings.y !== undefined;
38
+ }
39
+
40
+ function resolveTextInheritance(program, args) {
41
+ if (Object.hasOwn(args, "data")) return undefined;
42
+ const requestedData = program.context.currentData;
43
+ const candidates = program.semanticSpec.layers.filter(layer =>
44
+ eligibleSource(program, layer, requestedData)
45
+ );
46
+ const current = findLayer(program, program.context.currentMark);
47
+ const source = eligibleSource(program, current, requestedData)
48
+ ? current
49
+ : candidates.length === 1
50
+ ? candidates[0]
51
+ : undefined;
52
+ if (source === undefined && candidates.length > 1) {
53
+ throw new Error(
54
+ "Text source inference is ambiguous; provide data and encode its position explicitly."
55
+ );
56
+ }
57
+ if (source === undefined) return undefined;
58
+ return {
59
+ source: source.id,
60
+ data: source.data,
61
+ coordinate: source.coordinate,
62
+ encoding: resolveCompatibleEncodings(program, source, "text")
63
+ };
64
+ }
65
+
66
+ function requireTextLayer(program, requested, operation) {
67
+ const id = requested === undefined ? undefined : validateUserId(requested, "Text mark id");
68
+ return resolveEligibleLayer(program, {
69
+ target: id,
70
+ predicate: layer => layer.mark?.type === "text",
71
+ label: `${operation} text mark`
72
+ });
73
+ }
74
+
75
+ const createTextMark = action(
76
+ {
77
+ op: "createTextMark",
78
+ description: "Create a semantic text annotation layer."
79
+ },
80
+ function (args = {}) {
81
+ validateMarkOptions(args, CREATE_OPTIONS, "createTextMark");
82
+ const id = resolveMarkId(this, args.id, {
83
+ defaultId: "text",
84
+ label: "Text mark id",
85
+ markType: "text",
86
+ operation: "createTextMark"
87
+ });
88
+ const inherited = resolveTextInheritance(this, args);
89
+ const { data } = resolveMarkData(this, {
90
+ ...args,
91
+ ...(args.data === undefined && inherited?.data !== undefined
92
+ ? { data: inherited.data }
93
+ : {})
94
+ });
95
+ assertMarkAvailable(this, id);
96
+
97
+ let next = this
98
+ .editSemantic({ property: `layer[${id}].mark.type`, value: "text" })
99
+ .editSemantic({ property: `layer[${id}].data`, value: data });
100
+ if (inherited?.source !== undefined) {
101
+ next = next.editSemantic({
102
+ property: `layer[${id}].source`,
103
+ value: inherited.source
104
+ });
105
+ }
106
+ next = applyLayeredMarkInheritance(next, id, inherited)
107
+ .createGraphics({
108
+ id,
109
+ type: "text",
110
+ length: 0,
111
+ ...resolveMarkGraphicPlacement(next, { data, markType: "text" })
112
+ })
113
+ ._withMarkConfig(id, { ...DEFAULT_TEXT_MARK, fillExplicit: false });
114
+
115
+ const style = Object.fromEntries(
116
+ STYLE_OPTIONS.filter(option => Object.hasOwn(args, option))
117
+ .map(option => [option, args[option]])
118
+ );
119
+ if (Object.keys(style).length > 0) {
120
+ next = next.editTextMark({ target: id, ...style });
121
+ }
122
+ return Object.hasOwn(args, "text")
123
+ ? next.encodeText({ target: id, value: args.text })
124
+ : next;
125
+ }
126
+ );
127
+
128
+ const rematerializeTextMark = action(
129
+ {
130
+ op: "rematerializeTextMark",
131
+ description: "Recompute concrete text content, anchors, and typography."
132
+ },
133
+ function (args = {}) {
134
+ validateMarkOptions(args, REMATERIALIZE_OPTIONS, "rematerializeTextMark");
135
+ const id = validateUserId(args.id, "Text mark id");
136
+ const layer = findLayer(this, id);
137
+ const graphic = this.graphicSpec.objects[id];
138
+ if (layer?.mark?.type !== "text") throw new Error(`Unknown text mark "${id}".`);
139
+ if (graphic?.type !== "text" || !Array.isArray(graphic.items)) {
140
+ throw new Error(`Text mark "${id}" requires text collection graphics.`);
141
+ }
142
+ if (!canMaterializeText(this, layer)) {
143
+ return graphic.items.length === 0
144
+ ? this
145
+ : this.editGraphics({ target: id, property: "length", value: 0 });
146
+ }
147
+ return this.editGraphics({
148
+ target: id,
149
+ property: "items",
150
+ value: resolveTextGraphicItems(
151
+ this,
152
+ layer,
153
+ this.markConfigs[id] ?? DEFAULT_TEXT_MARK
154
+ )
155
+ });
156
+ }
157
+ );
158
+
159
+ const editTextMark = action(
160
+ {
161
+ op: "editTextMark",
162
+ description: "Edit text typography, alignment, rotation, and offsets."
163
+ },
164
+ function (args = {}) {
165
+ validateMarkOptions(args, EDIT_OPTIONS, "editTextMark");
166
+ if (!STYLE_OPTIONS.some(option => Object.hasOwn(args, option))) {
167
+ throw new Error("editTextMark requires at least one editable property.");
168
+ }
169
+ const layer = requireTextLayer(this, args.target, "editTextMark");
170
+ const next = this._withMarkConfig(
171
+ layer.id,
172
+ {
173
+ ...normalizeTextMarkConfig(
174
+ args,
175
+ this.markConfigs[layer.id] ?? DEFAULT_TEXT_MARK
176
+ ),
177
+ fillExplicit: Object.hasOwn(args, "fill")
178
+ ? true
179
+ : this.markConfigs[layer.id]?.fillExplicit ?? false
180
+ }
181
+ );
182
+ return canMaterializeText(next, layer)
183
+ ? next.rematerializeTextMark({ id: layer.id })
184
+ : next;
185
+ }
186
+ );
187
+
188
+ export function registerTextMarkActions(ProgramClass) {
189
+ ProgramClass.prototype.createTextMark = createTextMark;
190
+ ProgramClass.prototype.editTextMark = editTextMark;
191
+ ProgramClass.prototype.rematerializeTextMark = rematerializeTextMark;
192
+ }
@@ -0,0 +1 @@
1
+ export { registerTextMarkActions } from "./actions.js";
@@ -61,8 +61,11 @@ function resolvePlacement(graphicSpec, { id, type, parent, before, after }) {
61
61
  if (!isGraphicContainerType(parentGraphic.type)) {
62
62
  throw new Error(`Graphic parent "${parent}" must be a canvas or collection.`);
63
63
  }
64
- if (type === "canvas") {
65
- throw new Error("Canvas graphics cannot have a parent.");
64
+ if (
65
+ type === "canvas" &&
66
+ !graphicSpec.order.some(id => graphicSpec.objects[id]?.type === "canvas")
67
+ ) {
68
+ throw new Error("A nested Canvas requires an ordered root Canvas.");
66
69
  }
67
70
  }
68
71
  const anchor = before ?? after;
@@ -83,7 +86,7 @@ function resolvePlacement(graphicSpec, { id, type, parent, before, after }) {
83
86
  if (parent === undefined && before === "canvas") {
84
87
  throw new Error("createGraphics cannot place a graphic before the canvas.");
85
88
  }
86
- if (type === "canvas") {
89
+ if (type === "canvas" && parent === undefined) {
87
90
  throw new Error("Canvas graphics do not accept before or after placement.");
88
91
  }
89
92
  }
@@ -99,7 +102,8 @@ function insertSibling(siblings, id, { before, after }) {
99
102
  const createGraphics = action(
100
103
  {
101
104
  op: "createGraphics",
102
- description: "Create and optionally attach a concrete graphic."
105
+ description: "Create and optionally attach a concrete graphic.",
106
+ scope: "any"
103
107
  },
104
108
  function ({ id, type, length, parent, before, after } = {}) {
105
109
  if (typeof id !== "string" || !GRAPHIC_ID_PATTERN.test(id)) {
@@ -131,9 +135,12 @@ const createGraphics = action(
131
135
  }
132
136
  if (
133
137
  validatedType === "canvas" &&
134
- Object.values(this.graphicSpec.objects).some(graphic => graphic.type === "canvas")
138
+ parent === undefined &&
139
+ this.graphicSpec.order.some(
140
+ objectId => this.graphicSpec.objects[objectId]?.type === "canvas"
141
+ )
135
142
  ) {
136
- throw new Error("graphicSpec supports exactly one canvas.");
143
+ throw new Error("graphicSpec supports exactly one ordered canvas.");
137
144
  }
138
145
 
139
146
  const objects = {
@@ -110,14 +110,17 @@ function editItems(owner, property, value, id) {
110
110
  }
111
111
 
112
112
  function removeGraphicTree(graphicSpec, id) {
113
- if (graphicSpec.objects[id].type === "canvas") {
113
+ const parent = findGraphicParent(graphicSpec, id);
114
+ if (
115
+ graphicSpec.objects[id].type === "canvas" &&
116
+ parent?.kind !== "parent"
117
+ ) {
114
118
  throw new Error("The canvas root cannot be removed.");
115
119
  }
116
120
  const removedIds = new Set(collectGraphicSubtreeIds(graphicSpec, id));
117
121
  const objects = Object.fromEntries(
118
122
  Object.entries(graphicSpec.objects).filter(([objectId]) => !removedIds.has(objectId))
119
123
  );
120
- const parent = findGraphicParent(graphicSpec, id);
121
124
  let order = graphicSpec.order;
122
125
  if (parent?.kind === "parent") {
123
126
  objects[parent.id] = freezeOwned({
@@ -131,7 +134,11 @@ function removeGraphicTree(graphicSpec, id) {
131
134
  }
132
135
 
133
136
  const editGraphics = action(
134
- { op: "editGraphics", description: "Replace or remove concrete graphic state." },
137
+ {
138
+ op: "editGraphics",
139
+ description: "Replace or remove concrete graphic state.",
140
+ scope: "any"
141
+ },
135
142
  function ({ target, property, value, remove = false } = {}) {
136
143
  if (typeof target !== "string" || target.length === 0) {
137
144
  throw new TypeError("editGraphics requires a non-empty target string.");
@@ -76,6 +76,13 @@ function removeEntity(spec, parsed) {
76
76
  const collection = spec[parsed.collection];
77
77
  const index = collection.findIndex(item => item.id === parsed.id);
78
78
  if (index === -1) return spec;
79
+ if (parsed.kind === "layer" && parsed.path.length === 0) {
80
+ const nextCollection = collection.filter((_, itemIndex) => itemIndex !== index);
81
+ return freezeOwned({
82
+ ...spec,
83
+ [parsed.collection]: freezeOwned(nextCollection)
84
+ });
85
+ }
79
86
  if (parsed.kind === "dataset" && parsed.path.length === 0) {
80
87
  const dataset = collection[index];
81
88
  if (dataset.source === undefined) {
@@ -115,7 +122,8 @@ function removeRootProperty(spec, root, path) {
115
122
  const editSemantic = action(
116
123
  {
117
124
  op: "editSemantic",
118
- description: "Create, replace, or remove one semantic property."
125
+ description: "Create, replace, or remove one semantic property.",
126
+ scope: "any"
119
127
  },
120
128
  function ({ property, value, remove = false } = {}) {
121
129
  if (typeof remove !== "boolean") {
@@ -129,6 +137,13 @@ const editSemantic = action(
129
137
  }
130
138
 
131
139
  const parsed = parseSemanticPath(property, { allowContainer: remove });
140
+ if (this.compositionSpec !== undefined && !(
141
+ this.compositionSpec.type === "facet" && parsed.kind === "title"
142
+ )) {
143
+ throw new Error(
144
+ "editSemantic on a composition parent currently supports only facet title state."
145
+ );
146
+ }
132
147
  if (remove) {
133
148
  const semanticSpec = parsed.kind === "guide"
134
149
  ? removeRootProperty(this.semanticSpec, "guides", parsed.path)
@@ -140,10 +155,18 @@ const editSemantic = action(
140
155
  parsed.kind === "dataset" &&
141
156
  parsed.path.length === 0 &&
142
157
  this.context.currentData === parsed.id;
158
+ const clearsCurrentMark =
159
+ parsed.kind === "layer" &&
160
+ parsed.path.length === 0 &&
161
+ this.context.currentMark === parsed.id;
143
162
  return this._clone({
144
163
  semanticSpec,
145
- ...(clearsCurrentData
146
- ? { context: freezeOwned({ ...this.context, currentData: undefined }) }
164
+ ...(clearsCurrentData || clearsCurrentMark
165
+ ? { context: freezeOwned({
166
+ ...this.context,
167
+ ...(clearsCurrentData ? { currentData: undefined } : {}),
168
+ ...(clearsCurrentMark ? { currentMark: undefined } : {})
169
+ }) }
147
170
  : {})
148
171
  });
149
172
  }
@@ -7,7 +7,7 @@ import {
7
7
  MARK_TYPES
8
8
  } from "../../core/vocabulary.js";
9
9
  import { validateAggregate } from "../../grammar/aggregate.js";
10
- import { findSemanticScale, hasDataset } from "../../selectors/index.js";
10
+ import { findLayer, findSemanticScale, hasDataset } from "../../selectors/index.js";
11
11
  import { validateCoordinateType } from "../../grammar/coordinates.js";
12
12
  import {
13
13
  normalizeHistogramBin,
@@ -22,7 +22,7 @@ import {
22
22
  validateSemanticScaleDomain,
23
23
  validateSemanticScaleRange,
24
24
  validateSemanticScaleType
25
- } from "../../grammar/scales.js";
25
+ } from "../../grammar/scales/index.js";
26
26
 
27
27
  function nonEmptyString(value, label) {
28
28
  if (typeof value !== "string" || value.length === 0) {
@@ -74,6 +74,21 @@ export function validateSemanticValue(program, parsed, value) {
74
74
  }
75
75
  if (parsed.kind === "layer") {
76
76
  const property = parsed.path.join(".");
77
+ if (property === "source") {
78
+ validateUserId(value, "Layer source id");
79
+ if (value === parsed.id) {
80
+ throw new Error("A layer cannot use itself as its source.");
81
+ }
82
+ const source = findLayer(program, value);
83
+ if (source === undefined) {
84
+ throw new Error(`Unknown source layer "${value}".`);
85
+ }
86
+ if (!["point", "bar", "rule", "rect"].includes(source.mark?.type)) {
87
+ throw new Error(
88
+ `Layer source "${value}" must be a point, bar, rule, or rect mark.`
89
+ );
90
+ }
91
+ }
77
92
  if (property.endsWith(".title")) {
78
93
  nonEmptyString(value, "Encoding title");
79
94
  }
@@ -173,8 +188,15 @@ export function validateSemanticValue(program, parsed, value) {
173
188
  if (parsed.kind === "guide" && parsed.id === "legend.series") {
174
189
  validateLegend(parsed.path.at(-1), value);
175
190
  }
176
- if (parsed.kind === "guide" && parsed.id.startsWith("grid.")) {
177
- validateUserId(value, `Grid ${parsed.path.at(-1)} id`);
191
+ if (parsed.kind === "guide" && (
192
+ parsed.id.startsWith("grid.") || parsed.id.startsWith("axis.")
193
+ )) {
194
+ const property = parsed.path.at(-1);
195
+ if (property === "title") nonEmptyString(value, "Axis title");
196
+ else {
197
+ const guideKind = parsed.id.startsWith("grid.") ? "Grid" : "Axis";
198
+ validateUserId(value, `${guideKind} ${property} id`);
199
+ }
178
200
  }
179
201
  if (parsed.kind === "title") {
180
202
  nonEmptyString(value, `Chart title ${parsed.path[0]}`);
@@ -7,8 +7,8 @@ import { resolveEligibleLayer } from "../../selectors/layers.js";
7
7
  import {
8
8
  REGRESSION_LOWER_FIELD,
9
9
  REGRESSION_UPPER_FIELD
10
- } from "../../grammar/regression.js";
11
- import { validateAreaCreateOutline } from "../marks/area.js";
10
+ } from "../../grammar/regression/index.js";
11
+ import { validateAreaCreateOutline } from "../marks/area/index.js";
12
12
 
13
13
  const BAND_OPTIONS = Object.freeze([
14
14
  "id", "data", "x", "lower", "upper", "groupBy", "coordinate", "xScale", "yScale",
@@ -1,6 +1,6 @@
1
1
  import { action } from "../../core/action.js";
2
2
  import { validateKeys } from "../../core/validation.js";
3
- import { normalizeRegressionParameters } from "../../grammar/regression.js";
3
+ import { normalizeRegressionParameters } from "../../grammar/regression/index.js";
4
4
  import {
5
5
  findRegressionPoint,
6
6
  inferRegressionGroup,
@@ -87,7 +87,7 @@ export const createRegression = action(
87
87
  ...band
88
88
  });
89
89
  }
90
- return next.createRegressionLine({
90
+ next = next.createRegressionLine({
91
91
  id: lineId,
92
92
  data: dataId,
93
93
  x,
@@ -98,5 +98,22 @@ export const createRegression = action(
98
98
  yScale,
99
99
  ...line
100
100
  });
101
+ return next._withMarkConfig(point.id, {
102
+ ...next.markConfigs[point.id],
103
+ regression: {
104
+ source: point.data,
105
+ x,
106
+ y,
107
+ groupBy,
108
+ coordinate,
109
+ xScale,
110
+ yScale,
111
+ colorScale,
112
+ dataId,
113
+ bandId: band === false ? undefined : bandId,
114
+ lineId,
115
+ parameters
116
+ }
117
+ });
101
118
  }
102
119
  );