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,384 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateUserId } from "../../core/identifiers.js";
3
+ import { isPlainObject } from "../../core/immutable.js";
4
+ import { validateKeys } from "../../core/validation.js";
5
+ import { resolveEligibleLayer } from "../../selectors/layers.js";
6
+ import { transformPointHighlightChild } from "../../materialization/selection/point.js";
7
+ import {
8
+ transformPathHighlightProperties,
9
+ transformRuleHighlightProperties
10
+ } from "../../materialization/selection/path.js";
11
+ import {
12
+ resolveMarkSelection,
13
+ resolveSelectionCreationId,
14
+ resolveStoredSelection
15
+ } from "../../materialization/selection/state.js";
16
+ import {
17
+ findSelectionPolicy,
18
+ requireSelectionPolicy
19
+ } from "../../materialization/selection/policies/index.js";
20
+ import {
21
+ normalizeDimOthers,
22
+ validateUnitInterval
23
+ } from "../../materialization/selection/styles.js";
24
+
25
+ const SELECTOR_KEYS = Object.freeze([
26
+ "grain", "field", "channel", "property", "op", "value", "values", "min", "max",
27
+ "inclusive", "count", "groupBy", "ties"
28
+ ]);
29
+ const SELECT_OPTIONS = Object.freeze(["id", "target", ...SELECTOR_KEYS]);
30
+ const HIGHLIGHT_OPTIONS = Object.freeze([
31
+ "id", "target", "select", "selection", "color", "opacity", "fill",
32
+ "stroke", "strokeWidth", "strokeDash", "shape", "size", "offset",
33
+ "dimOthers", "bringToFront"
34
+ ]);
35
+ const INTERNAL_SELECTION_OPTIONS = Object.freeze(["selection", "style", "keys"]);
36
+ const INTERNAL_DIM_OPTIONS = Object.freeze(["selection", "opacity", "keys"]);
37
+ const INTERNAL_ORDER_OPTIONS = Object.freeze(["selection", "keys"]);
38
+ const REMATERIALIZE_OPTIONS = Object.freeze(["target", "highlights"]);
39
+
40
+ function selectorFrom(args) {
41
+ return Object.fromEntries(
42
+ SELECTOR_KEYS.flatMap(key => Object.hasOwn(args, key) ? [[key, args[key]]] : [])
43
+ );
44
+ }
45
+
46
+ function resolveTarget(program, target, label = "mark selection") {
47
+ const requested = target === undefined
48
+ ? undefined
49
+ : validateUserId(target, "Mark id");
50
+ return resolveEligibleLayer(program, {
51
+ target: requested,
52
+ label,
53
+ predicate: layer => findSelectionPolicy(layer.mark?.type) !== undefined
54
+ });
55
+ }
56
+
57
+ function hasLegendSelection(program, target, selection) {
58
+ const legend = program.guideConfigs.legend?.series ??
59
+ program.guideConfigs.legend?.color;
60
+ const selector = program.materializationConfigs.selections?.[selection]?.selector;
61
+ return legend?.target === target &&
62
+ selector?.field !== undefined &&
63
+ selector.field === legend.field;
64
+ }
65
+
66
+ function selectedKeys(args, resolved) {
67
+ if (args.keys === undefined) return resolved.keys;
68
+ if (!Array.isArray(args.keys) || !args.keys.every(key =>
69
+ typeof key === "string" && key.length > 0
70
+ )) {
71
+ throw new TypeError("Selected mark item keys must be non-empty strings.");
72
+ }
73
+ return args.keys;
74
+ }
75
+
76
+ export const selectMarks = action(
77
+ { op: "selectMarks", description: "Create one reusable selection over final mark items." },
78
+ function (args = {}) {
79
+ validateKeys(args, SELECT_OPTIONS, "selectMarks");
80
+ const layer = resolveTarget(this, args.target);
81
+ const resolved = resolveMarkSelection(this, layer.id, selectorFrom(args));
82
+ const id = resolveSelectionCreationId(this, args.id, layer.id);
83
+ return this
84
+ ._withSelectionConfig(id, { target: layer.id, selector: resolved.selector })
85
+ ._withContext({ currentSelection: id });
86
+ }
87
+ );
88
+
89
+ export const applyPointHighlight = action(
90
+ { op: "applyPointHighlight", description: "Apply selected point appearance and geometry." },
91
+ function (args = {}) {
92
+ validateKeys(args, INTERNAL_SELECTION_OPTIONS, "applyPointHighlight");
93
+ const resolved = resolveStoredSelection(this, args.selection);
94
+ const keys = selectedKeys(args, resolved);
95
+ if (resolved.items[0]?.markType !== "point" && resolved.items.length > 0) {
96
+ throw new Error("applyPointHighlight requires a point selection.");
97
+ }
98
+ if (keys.length === 0) return this;
99
+ const selected = new Set(keys);
100
+ const graphic = this.graphicSpec.objects[resolved.definition.target];
101
+ const keyByGraphic = new Map(resolved.items.flatMap(item =>
102
+ item.graphicIds.map(id => [id, item.key])
103
+ ));
104
+ const items = graphic.items.map(child =>
105
+ selected.has(keyByGraphic.get(child.id))
106
+ ? transformPointHighlightChild({
107
+ type: child.type ?? graphic.type,
108
+ properties: child.properties
109
+ }, args.style)
110
+ : { type: child.type ?? graphic.type, properties: child.properties }
111
+ );
112
+ return this.editGraphics({
113
+ target: resolved.definition.target,
114
+ property: "items",
115
+ value: items
116
+ });
117
+ }
118
+ );
119
+
120
+ export const applyBarHighlight = action(
121
+ { op: "applyBarHighlight", description: "Apply selected bar appearance." },
122
+ function (args = {}) {
123
+ validateKeys(args, INTERNAL_SELECTION_OPTIONS, "applyBarHighlight");
124
+ const resolved = resolveStoredSelection(this, args.selection);
125
+ const keys = selectedKeys(args, resolved);
126
+ if (resolved.items[0]?.markType !== "bar" && resolved.items.length > 0) {
127
+ throw new Error("applyBarHighlight requires a bar selection.");
128
+ }
129
+ if (keys.length === 0) return this;
130
+ const selected = new Set(keys);
131
+ const graphic = this.graphicSpec.objects[resolved.definition.target];
132
+ const keyByGraphic = new Map(resolved.items.flatMap(item =>
133
+ item.graphicIds.map(id => [id, item.key])
134
+ ));
135
+ return this.editGraphics({
136
+ target: resolved.definition.target,
137
+ property: "items",
138
+ value: graphic.items.map(child => ({
139
+ type: child.type ?? graphic.type,
140
+ properties: selected.has(keyByGraphic.get(child.id))
141
+ ? { ...child.properties, ...args.style }
142
+ : child.properties
143
+ }))
144
+ });
145
+ }
146
+ );
147
+
148
+ export const applyPathHighlight = action(
149
+ { op: "applyPathHighlight", description: "Apply selected line or area path appearance and offset." },
150
+ function (args = {}) {
151
+ validateKeys(args, INTERNAL_SELECTION_OPTIONS, "applyPathHighlight");
152
+ const resolved = resolveStoredSelection(this, args.selection);
153
+ const keys = selectedKeys(args, resolved);
154
+ const markType = resolved.items[0]?.markType;
155
+ if (!["line", "area"].includes(markType) && resolved.items.length > 0) {
156
+ throw new Error("applyPathHighlight requires a line or area selection.");
157
+ }
158
+ if (keys.length === 0) return this;
159
+ const selected = new Set(keys);
160
+ const graphic = this.graphicSpec.objects[resolved.definition.target];
161
+ const keyByGraphic = new Map(resolved.items.flatMap(item =>
162
+ item.graphicIds.map(id => [id, item.key])
163
+ ));
164
+ return this.editGraphics({
165
+ target: resolved.definition.target,
166
+ property: "items",
167
+ value: graphic.items.map(child => ({
168
+ type: child.type ?? graphic.type,
169
+ properties: selected.has(keyByGraphic.get(child.id))
170
+ ? transformPathHighlightProperties(child.properties, args.style)
171
+ : child.properties
172
+ }))
173
+ });
174
+ }
175
+ );
176
+
177
+ export const applyRuleHighlight = action(
178
+ { op: "applyRuleHighlight", description: "Apply selected rule appearance and offset." },
179
+ function (args = {}) {
180
+ validateKeys(args, INTERNAL_SELECTION_OPTIONS, "applyRuleHighlight");
181
+ const resolved = resolveStoredSelection(this, args.selection);
182
+ const keys = selectedKeys(args, resolved);
183
+ if (resolved.items[0]?.markType !== "rule" && resolved.items.length > 0) {
184
+ throw new Error("applyRuleHighlight requires a rule selection.");
185
+ }
186
+ if (keys.length === 0) return this;
187
+ const selected = new Set(keys);
188
+ const graphic = this.graphicSpec.objects[resolved.definition.target];
189
+ const keyByGraphic = new Map(resolved.items.flatMap(item =>
190
+ item.graphicIds.map(id => [id, item.key])
191
+ ));
192
+ return this.editGraphics({
193
+ target: resolved.definition.target,
194
+ property: "items",
195
+ value: graphic.items.map(child => ({
196
+ type: child.type ?? graphic.type,
197
+ properties: selected.has(keyByGraphic.get(child.id))
198
+ ? transformRuleHighlightProperties(child.properties, args.style)
199
+ : child.properties
200
+ }))
201
+ });
202
+ }
203
+ );
204
+
205
+ export const dimUnselectedMarkItems = action(
206
+ { op: "dimUnselectedMarkItems", description: "Dim the complement of one mark selection." },
207
+ function (args = {}) {
208
+ validateKeys(args, INTERNAL_DIM_OPTIONS, "dimUnselectedMarkItems");
209
+ const opacity = validateUnitInterval(args.opacity, "Dim opacity");
210
+ const resolved = resolveStoredSelection(this, args.selection);
211
+ const selected = new Set(selectedKeys(args, resolved));
212
+ const graphic = this.graphicSpec.objects[resolved.definition.target];
213
+ const keyByGraphic = new Map(resolved.items.flatMap(item =>
214
+ item.graphicIds.map(id => [id, item.key])
215
+ ));
216
+ return this.editGraphics({
217
+ target: resolved.definition.target,
218
+ property: "items",
219
+ value: graphic.items.map(child => ({
220
+ type: child.type ?? graphic.type,
221
+ properties: selected.has(keyByGraphic.get(child.id))
222
+ ? child.properties
223
+ : { ...child.properties, opacity }
224
+ }))
225
+ });
226
+ }
227
+ );
228
+
229
+ export const placeSelectedMarkItemsLast = action(
230
+ { op: "placeSelectedMarkItemsLast", description: "Place selected collection items after their complement." },
231
+ function (args = {}) {
232
+ validateKeys(args, INTERNAL_ORDER_OPTIONS, "placeSelectedMarkItemsLast");
233
+ const resolved = resolveStoredSelection(this, args.selection);
234
+ const keys = selectedKeys(args, resolved);
235
+ if (keys.length === 0 || keys.length === resolved.items.length) {
236
+ return this;
237
+ }
238
+ const selected = new Set(keys);
239
+ const graphic = this.graphicSpec.objects[resolved.definition.target];
240
+ const keyByGraphic = new Map(resolved.items.flatMap(item =>
241
+ item.graphicIds.map(id => [id, item.key])
242
+ ));
243
+ const items = graphic.items.map(child => ({
244
+ key: keyByGraphic.get(child.id),
245
+ child: { type: child.type ?? graphic.type, properties: child.properties }
246
+ }));
247
+ return this.editGraphics({
248
+ target: resolved.definition.target,
249
+ property: "items",
250
+ value: [
251
+ ...items.filter(item => !selected.has(item.key)),
252
+ ...items.filter(item => selected.has(item.key))
253
+ ].map(item => item.child)
254
+ });
255
+ }
256
+ );
257
+
258
+ export const rematerializeMarkHighlights = action(
259
+ { op: "rematerializeMarkHighlights", description: "Reapply stored highlight assignments to one rematerialized mark." },
260
+ function (args = {}) {
261
+ validateKeys(args, REMATERIALIZE_OPTIONS, "rematerializeMarkHighlights");
262
+ validateUserId(args.target, "Highlight target id");
263
+ if (!Array.isArray(args.highlights)) {
264
+ throw new TypeError("rematerializeMarkHighlights requires highlight entries.");
265
+ }
266
+ const prepared = args.highlights.map(([id, config]) => ({
267
+ id,
268
+ config,
269
+ keys: resolveStoredSelection(this, config.selection).keys
270
+ }));
271
+ let next = this;
272
+ for (const { id, config, keys } of prepared) {
273
+ const policy = requireSelectionPolicy(config.markType);
274
+ next = next[policy.applyHighlightOp]({
275
+ selection: config.selection,
276
+ style: config.style,
277
+ keys
278
+ });
279
+ if (config.dimOthers !== false) {
280
+ next = next.dimUnselectedMarkItems({
281
+ selection: config.selection,
282
+ opacity: config.dimOthers.opacity,
283
+ keys
284
+ });
285
+ }
286
+ if (config.bringToFront) {
287
+ next = next.placeSelectedMarkItemsLast({
288
+ selection: config.selection,
289
+ keys
290
+ });
291
+ }
292
+ next = next._withHighlightConfig(id, config);
293
+ }
294
+ return prepared.some(({ config }) =>
295
+ hasLegendSelection(next, config.target, config.selection)
296
+ )
297
+ ? next.rematerializeLegendHighlights()
298
+ : next;
299
+ }
300
+ );
301
+
302
+ export const highlightMarks = action(
303
+ { op: "highlightMarks", description: "Select and emphasize final visual mark items." },
304
+ function (args = {}) {
305
+ validateKeys(args, HIGHLIGHT_OPTIONS, "highlightMarks");
306
+ if (args.select !== undefined && args.selection !== undefined) {
307
+ throw new Error("highlightMarks accepts select or selection, not both.");
308
+ }
309
+ if (args.select !== undefined && !isPlainObject(args.select)) {
310
+ throw new TypeError("highlightMarks select must be a plain object.");
311
+ }
312
+ if (args.id !== undefined && args.select === undefined) {
313
+ throw new Error("highlightMarks id is available only with inline select.");
314
+ }
315
+ const dimOthers = normalizeDimOthers(args.dimOthers);
316
+ const bringToFront = args.bringToFront ?? true;
317
+ if (typeof bringToFront !== "boolean") {
318
+ throw new TypeError("highlightMarks bringToFront must be a boolean.");
319
+ }
320
+
321
+ let next = this;
322
+ let resolved;
323
+ let layer;
324
+ if (args.select !== undefined) {
325
+ layer = resolveTarget(this, args.target, "highlight mark");
326
+ resolveMarkSelection(this, layer.id, args.select);
327
+ const style = requireSelectionPolicy(
328
+ layer.mark.type
329
+ ).normalizeHighlightStyle(args);
330
+ next = next.selectMarks({
331
+ ...(args.id === undefined ? {} : { id: args.id }),
332
+ target: layer.id,
333
+ ...args.select
334
+ });
335
+ resolved = resolveStoredSelection(next);
336
+ resolved = { ...resolved, style };
337
+ } else {
338
+ resolved = resolveStoredSelection(next, args.selection);
339
+ if (args.target !== undefined && args.target !== resolved.definition.target) {
340
+ throw new Error("highlightMarks target must match its selection target.");
341
+ }
342
+ layer = resolveTarget(next, resolved.definition.target, "highlight mark");
343
+ resolved = {
344
+ ...resolved,
345
+ style: requireSelectionPolicy(
346
+ layer.mark.type
347
+ ).normalizeHighlightStyle(args)
348
+ };
349
+ }
350
+
351
+ const selection = resolved.id;
352
+ const keys = resolved.keys;
353
+ const style = resolved.style;
354
+ const existing = next.materializationConfigs.highlights?.[selection];
355
+ if (existing !== undefined) {
356
+ next = next._withoutMaterializationConfig(["highlights", selection]);
357
+ const policy = requireSelectionPolicy(layer.mark.type);
358
+ next = next[policy.rematerializeOp]({ id: resolved.definition.target });
359
+ }
360
+ const policy = requireSelectionPolicy(layer.mark.type);
361
+ next = next[policy.applyHighlightOp]({ selection, style, keys });
362
+ if (dimOthers !== false) {
363
+ next = next.dimUnselectedMarkItems({
364
+ selection,
365
+ opacity: dimOthers.opacity,
366
+ keys
367
+ });
368
+ }
369
+ if (bringToFront) {
370
+ next = next.placeSelectedMarkItemsLast({ selection, keys });
371
+ }
372
+ next = next._withHighlightConfig(selection, {
373
+ target: resolved.definition.target,
374
+ selection,
375
+ markType: layer.mark.type,
376
+ style,
377
+ dimOthers,
378
+ bringToFront
379
+ });
380
+ return hasLegendSelection(next, resolved.definition.target, selection)
381
+ ? next.rematerializeLegendHighlights()
382
+ : next;
383
+ }
384
+ );
@@ -0,0 +1,23 @@
1
+ import {
2
+ applyBarHighlight,
3
+ applyPathHighlight,
4
+ applyPointHighlight,
5
+ applyRuleHighlight,
6
+ dimUnselectedMarkItems,
7
+ highlightMarks,
8
+ placeSelectedMarkItemsLast,
9
+ rematerializeMarkHighlights,
10
+ selectMarks
11
+ } from "./actions.js";
12
+
13
+ export function registerSelectionActions(ProgramClass) {
14
+ ProgramClass.prototype.selectMarks = selectMarks;
15
+ ProgramClass.prototype.highlightMarks = highlightMarks;
16
+ ProgramClass.prototype.applyBarHighlight = applyBarHighlight;
17
+ ProgramClass.prototype.applyPathHighlight = applyPathHighlight;
18
+ ProgramClass.prototype.applyPointHighlight = applyPointHighlight;
19
+ ProgramClass.prototype.applyRuleHighlight = applyRuleHighlight;
20
+ ProgramClass.prototype.dimUnselectedMarkItems = dimUnselectedMarkItems;
21
+ ProgramClass.prototype.placeSelectedMarkItemsLast = placeSelectedMarkItemsLast;
22
+ ProgramClass.prototype.rematerializeMarkHighlights = rematerializeMarkHighlights;
23
+ }
@@ -0,0 +1,235 @@
1
+ import { action } from "../../core/action.js";
2
+ import { noOptions } from "../../core/validation.js";
3
+ import {
4
+ normalizeTitleEditOptions,
5
+ normalizeTitleOptions,
6
+ requireTitleConfig,
7
+ resolveTitleLayout
8
+ } from "./resolve.js";
9
+ import {
10
+ preserveGraphicPlacement,
11
+ resolveCanvasGraphicPlacement
12
+ } from "../../materialization/graphicHierarchy.js";
13
+
14
+ function hasRotation(graphic) {
15
+ const properties = graphic.items?.map(child => child.properties) ??
16
+ [graphic.properties];
17
+ return properties.some(item => Object.hasOwn(item, "rotation"));
18
+ }
19
+
20
+ function ensureTextShape(program, id, component) {
21
+ const graphic = program.graphicSpec.objects[id];
22
+ if (graphic?.type !== "text") {
23
+ throw new Error(`${id} requires an existing text graphic.`);
24
+ }
25
+ const collection = graphic.items !== undefined;
26
+ const needsCollection = component.lines.length > 1;
27
+ const rotationMismatch = hasRotation(graphic) !== component.explicitRotation;
28
+ if ((collection && !needsCollection) || rotationMismatch) {
29
+ const placement = preserveGraphicPlacement(program, id);
30
+ return program
31
+ .editGraphics({ target: id, remove: true })
32
+ .createGraphics({
33
+ id,
34
+ type: "text",
35
+ ...(needsCollection ? { length: component.lines.length } : {}),
36
+ ...placement
37
+ });
38
+ }
39
+ if (!collection && needsCollection) {
40
+ return program.editGraphics({
41
+ target: id,
42
+ property: "length",
43
+ value: component.lines.length
44
+ });
45
+ }
46
+ if (collection && graphic.items.length !== component.lines.length) {
47
+ return program.editGraphics({
48
+ target: id,
49
+ property: "length",
50
+ value: component.lines.length
51
+ });
52
+ }
53
+ return program;
54
+ }
55
+
56
+ function distributed(value, count) {
57
+ return count === 1 && Array.isArray(value) ? value[0] : value;
58
+ }
59
+
60
+ function editTextGraphic(program, id, component, style) {
61
+ const count = component.lines.length;
62
+ let next = ensureTextShape(program, id, component)
63
+ .editGraphics({ target: id, property: "x", value: distributed(component.x, count) })
64
+ .editGraphics({ target: id, property: "y", value: distributed(component.y, count) })
65
+ .editGraphics({
66
+ target: id,
67
+ property: "text",
68
+ value: distributed(component.lines, count)
69
+ })
70
+ .editGraphics({ target: id, property: "fill", value: style.color })
71
+ .editGraphics({ target: id, property: "fontSize", value: style.fontSize })
72
+ .editGraphics({ target: id, property: "fontFamily", value: style.fontFamily })
73
+ .editGraphics({ target: id, property: "fontWeight", value: style.fontWeight })
74
+ .editGraphics({ target: id, property: "textAlign", value: component.textAlign })
75
+ .editGraphics({ target: id, property: "textBaseline", value: "middle" });
76
+ if (component.explicitRotation) {
77
+ next = next.editGraphics({
78
+ target: id,
79
+ property: "rotation",
80
+ value: component.rotation
81
+ });
82
+ }
83
+ return next;
84
+ }
85
+
86
+ function createTextGraphic(program, id, component) {
87
+ return program.createGraphics({
88
+ id,
89
+ type: "text",
90
+ ...(component.lines.length > 1 ? { length: component.lines.length } : {}),
91
+ ...resolveCanvasGraphicPlacement(program)
92
+ });
93
+ }
94
+
95
+ export const editTitleText = action(
96
+ { op: "editTitleText", description: "Rematerialize chart title text." },
97
+ function (args = {}) {
98
+ noOptions(args, "editTitleText");
99
+ const config = requireTitleConfig(this);
100
+ const layout = resolveTitleLayout(this, config);
101
+ return editTextGraphic(
102
+ this,
103
+ "chartTitle",
104
+ layout.title,
105
+ config.titleStyle
106
+ );
107
+ }
108
+ );
109
+
110
+ export const createTitleText = action(
111
+ { op: "createTitleText", description: "Create chart title text." },
112
+ function (args = {}) {
113
+ noOptions(args, "createTitleText");
114
+ const config = requireTitleConfig(this);
115
+ if (this.graphicSpec.objects.chartTitle !== undefined) {
116
+ throw new Error("createTitleText requires a missing chart title graphic.");
117
+ }
118
+ const layout = resolveTitleLayout(this, config);
119
+ return createTextGraphic(this, "chartTitle", layout.title)
120
+ .editTitleText();
121
+ }
122
+ );
123
+
124
+ export const editSubtitleText = action(
125
+ { op: "editSubtitleText", description: "Rematerialize chart subtitle text." },
126
+ function (args = {}) {
127
+ noOptions(args, "editSubtitleText");
128
+ const config = requireTitleConfig(this);
129
+ const layout = resolveTitleLayout(this, config);
130
+ if (layout.subtitle === undefined) {
131
+ throw new Error("editSubtitleText requires semantic subtitle text.");
132
+ }
133
+ return editTextGraphic(
134
+ this,
135
+ "chartSubtitle",
136
+ layout.subtitle,
137
+ config.subtitleStyle
138
+ );
139
+ }
140
+ );
141
+
142
+ export const createSubtitleText = action(
143
+ { op: "createSubtitleText", description: "Create chart subtitle text." },
144
+ function (args = {}) {
145
+ noOptions(args, "createSubtitleText");
146
+ const config = requireTitleConfig(this);
147
+ if (this.graphicSpec.objects.chartSubtitle !== undefined) {
148
+ throw new Error("createSubtitleText requires a missing chart subtitle graphic.");
149
+ }
150
+ const layout = resolveTitleLayout(this, config);
151
+ if (layout.subtitle === undefined) {
152
+ throw new Error("createSubtitleText requires semantic subtitle text.");
153
+ }
154
+ return createTextGraphic(this, "chartSubtitle", layout.subtitle)
155
+ .editSubtitleText();
156
+ }
157
+ );
158
+
159
+ export const rematerializeTitle = action(
160
+ { op: "rematerializeTitle", description: "Rematerialize chart title graphics." },
161
+ function (args = {}) {
162
+ noOptions(args, "rematerializeTitle");
163
+ requireTitleConfig(this);
164
+ if (this.graphicSpec.objects.chartTitle?.type !== "text") {
165
+ throw new Error("rematerializeTitle requires an existing chart title graphic.");
166
+ }
167
+ let next = this.editTitleText();
168
+ const hasSubtitle = next.semanticSpec.title.subtitle !== undefined;
169
+ const graphic = next.graphicSpec.objects.chartSubtitle;
170
+ if (hasSubtitle && graphic === undefined) {
171
+ next = next.createSubtitleText();
172
+ } else if (hasSubtitle) {
173
+ next = next.editSubtitleText();
174
+ } else if (graphic !== undefined) {
175
+ next = next.editGraphics({ target: "chartSubtitle", remove: true });
176
+ }
177
+ return next;
178
+ }
179
+ );
180
+
181
+ export const createTitle = action(
182
+ { op: "createTitle", description: "Create a chart title and optional subtitle." },
183
+ function (args = {}) {
184
+ const options = normalizeTitleOptions(args);
185
+ if (Object.keys(this.semanticSpec.title).length > 0) {
186
+ throw new Error("createTitle requires missing semantic title state.");
187
+ }
188
+ if (
189
+ this.graphicSpec.objects.chartTitle !== undefined ||
190
+ this.graphicSpec.objects.chartSubtitle !== undefined
191
+ ) {
192
+ throw new Error("createTitle requires missing chart title graphics.");
193
+ }
194
+ const { text, subtitle, ...config } = options;
195
+ let next = this.editSemantic({ property: "title.text", value: text });
196
+ if (subtitle !== undefined) {
197
+ next = next.editSemantic({ property: "title.subtitle", value: subtitle });
198
+ }
199
+ next = next._withTitleConfig(config);
200
+ resolveTitleLayout(next, config);
201
+ next = next.createTitleText();
202
+ if (subtitle !== undefined) next = next.createSubtitleText();
203
+ return next;
204
+ }
205
+ );
206
+
207
+ export const editTitle = action(
208
+ { op: "editTitle", description: "Edit one stable chart title resource." },
209
+ function (args = {}) {
210
+ if (this.semanticSpec.title.text === undefined) {
211
+ throw new Error("editTitle requires an existing chart title.");
212
+ }
213
+ const previous = requireTitleConfig(this);
214
+ const normalized = normalizeTitleEditOptions(
215
+ args,
216
+ previous,
217
+ this.semanticSpec.title
218
+ );
219
+ let next = this;
220
+ if (args.text !== undefined) {
221
+ next = next.editSemantic({ property: "title.text", value: normalized.text });
222
+ }
223
+ if (args.subtitle === false) {
224
+ next = next.editSemantic({ property: "title.subtitle", remove: true });
225
+ } else if (args.subtitle !== undefined) {
226
+ next = next.editSemantic({
227
+ property: "title.subtitle",
228
+ value: normalized.subtitle
229
+ });
230
+ }
231
+ next = next._withTitleConfig(normalized.config);
232
+ resolveTitleLayout(next, normalized.config);
233
+ return next.rematerializeTitle();
234
+ }
235
+ );
@@ -0,0 +1,19 @@
1
+ import {
2
+ createSubtitleText,
3
+ createTitle,
4
+ createTitleText,
5
+ editTitle,
6
+ editSubtitleText,
7
+ editTitleText,
8
+ rematerializeTitle
9
+ } from "./actions.js";
10
+
11
+ export function registerTitleActions(ProgramClass) {
12
+ ProgramClass.prototype.createTitle = createTitle;
13
+ ProgramClass.prototype.editTitle = editTitle;
14
+ ProgramClass.prototype.createTitleText = createTitleText;
15
+ ProgramClass.prototype.editTitleText = editTitleText;
16
+ ProgramClass.prototype.createSubtitleText = createSubtitleText;
17
+ ProgramClass.prototype.editSubtitleText = editSubtitleText;
18
+ ProgramClass.prototype.rematerializeTitle = rematerializeTitle;
19
+ }