ggaction 0.0.13 → 0.0.15

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 (270) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +20 -11
  3. package/knowledge/action-cards.json +1 -1
  4. package/knowledge/intent-taxonomy.json +1 -1
  5. package/knowledge/mcp-resources.json +1 -1
  6. package/package.json +8 -3
  7. package/src/actions/basic.js +2 -2
  8. package/src/actions/boxPlots/components.js +14 -4
  9. package/src/actions/boxPlots/edit.js +31 -5
  10. package/src/actions/boxPlots/materialize.js +11 -3
  11. package/src/actions/boxPlots/options.js +48 -16
  12. package/src/actions/categoryOrder/index.js +15 -4
  13. package/src/actions/charts/area.js +5 -1
  14. package/src/actions/charts/bar.js +2 -1
  15. package/src/actions/charts/density.js +6 -2
  16. package/src/actions/charts/ecdf.js +4 -1
  17. package/src/actions/charts/endpoints.js +25 -3
  18. package/src/actions/charts/heatmap.js +2 -1
  19. package/src/actions/charts/histogram.js +4 -3
  20. package/src/actions/charts/horizon.js +3 -1
  21. package/src/actions/charts/interval-regression.js +5 -2
  22. package/src/actions/charts/line.js +3 -1
  23. package/src/actions/charts/parallel.js +8 -2
  24. package/src/actions/charts/pie.js +10 -2
  25. package/src/actions/charts/polar.js +15 -4
  26. package/src/actions/charts/radar.js +8 -2
  27. package/src/actions/charts/radial.js +10 -2
  28. package/src/actions/charts/rug-strip.js +6 -2
  29. package/src/actions/charts/scatter.js +3 -1
  30. package/src/actions/composition/actions.js +13 -4
  31. package/src/actions/coordinates/actions.js +1 -1
  32. package/src/actions/coordinates/edit.js +76 -0
  33. package/src/actions/data/bin.js +1 -1
  34. package/src/actions/data/bin2d.js +57 -185
  35. package/src/actions/data/complete.js +26 -0
  36. package/src/actions/data/computed.js +2 -2
  37. package/src/actions/data/create.js +2 -2
  38. package/src/actions/data/density.js +12 -20
  39. package/src/actions/data/derived.js +47 -17
  40. package/src/actions/data/edit.js +539 -0
  41. package/src/actions/data/filter.js +14 -2
  42. package/src/actions/data/impute.js +26 -0
  43. package/src/actions/data/index.js +14 -0
  44. package/src/actions/data/normalize.js +27 -0
  45. package/src/actions/data/regression.js +16 -21
  46. package/src/actions/data/revisionBindings.js +145 -0
  47. package/src/actions/data/shared.js +20 -4
  48. package/src/actions/data/summary.js +1 -1
  49. package/src/actions/data/timeUnit.js +4 -1
  50. package/src/actions/data/window.js +1 -1
  51. package/src/actions/encodings/channels.js +415 -0
  52. package/src/actions/encodings/color/index.js +2 -1
  53. package/src/actions/encodings/density.js +5 -2
  54. package/src/actions/encodings/histogram.js +3 -2
  55. package/src/actions/encodings/index.js +4 -0
  56. package/src/actions/encodings/offset.js +20 -13
  57. package/src/actions/encodings/position/apply.js +53 -7
  58. package/src/actions/encodings/position/index.js +10 -2
  59. package/src/actions/encodings/position/policies/bar.js +9 -0
  60. package/src/actions/encodings/position/resolve.js +24 -7
  61. package/src/actions/encodings/remove.js +9 -4
  62. package/src/actions/encodings/ruleAppearance.js +1 -23
  63. package/src/actions/encodings/seriesLayout.js +1 -0
  64. package/src/actions/encodings/shared.js +82 -14
  65. package/src/actions/encodings/stroke.js +226 -0
  66. package/src/actions/errorBands/create.js +23 -3
  67. package/src/actions/errorBands/edit.js +21 -6
  68. package/src/actions/errorBars/create.js +35 -7
  69. package/src/actions/errorBars/edit.js +19 -4
  70. package/src/actions/facets/actions.js +237 -57
  71. package/src/actions/facets/derive.js +91 -26
  72. package/src/actions/gradientPlots/components.js +10 -2
  73. package/src/actions/gradientPlots/edit.js +14 -4
  74. package/src/actions/gradientPlots/materialize.js +3 -1
  75. package/src/actions/gradientPlots/options.js +10 -2
  76. package/src/actions/guides/applicability.js +8 -2
  77. package/src/actions/guides/axes/labels.js +58 -12
  78. package/src/actions/guides/axes/lines.js +6 -2
  79. package/src/actions/guides/axes/parallel/resolve.js +6 -2
  80. package/src/actions/guides/axes/tickGroups.js +2 -1
  81. package/src/actions/guides/axes/ticks.js +6 -2
  82. package/src/actions/guides/axes/titles.js +11 -3
  83. package/src/actions/guides/grids/resolve.js +7 -2
  84. package/src/actions/guides/guides.js +19 -3
  85. package/src/actions/guides/legends/blocks.js +426 -0
  86. package/src/actions/guides/legends/categorical/actions.js +134 -48
  87. package/src/actions/guides/legends/categorical/components.js +17 -12
  88. package/src/actions/guides/legends/categorical/index.js +37 -33
  89. package/src/actions/guides/legends/categorical/layout.js +97 -15
  90. package/src/actions/guides/legends/categorical/options.js +2 -2
  91. package/src/actions/guides/legends/categorical/recipes.js +111 -20
  92. package/src/actions/guides/legends/categorical/resolve.js +12 -8
  93. package/src/actions/guides/legends/categorical/symbols.js +85 -30
  94. package/src/actions/guides/legends/continuous/common.js +30 -12
  95. package/src/actions/guides/legends/continuous/gradient.js +10 -7
  96. package/src/actions/guides/legends/continuous/index.js +2 -0
  97. package/src/actions/guides/legends/continuous/interval.js +8 -3
  98. package/src/actions/guides/legends/continuous/opacity.js +8 -5
  99. package/src/actions/guides/legends/continuous/stroke.js +525 -0
  100. package/src/actions/guides/legends/creation.js +14 -1
  101. package/src/actions/guides/legends/edit.js +158 -59
  102. package/src/actions/guides/legends/index.js +2 -0
  103. package/src/actions/guides/legends/lane.js +54 -19
  104. package/src/actions/guides/legends/lifecycle.js +19 -8
  105. package/src/actions/guides/legends/remove.js +13 -3
  106. package/src/actions/guides/legends/sampling.js +140 -0
  107. package/src/actions/guides/legends/size.js +93 -40
  108. package/src/actions/guides/legends/strokeWidth.js +34 -28
  109. package/src/actions/guides/legends/target.js +112 -1
  110. package/src/actions/guides/legends/transition.js +165 -16
  111. package/src/actions/guides/polar/axes/facade.js +4 -4
  112. package/src/actions/guides/polar/axes/labels.js +24 -5
  113. package/src/actions/guides/polar/axes/lines.js +10 -4
  114. package/src/actions/guides/polar/axes/shared.js +16 -0
  115. package/src/actions/guides/polar/axes/ticks.js +1 -1
  116. package/src/actions/guides/polar/axes/titles.js +1 -1
  117. package/src/actions/guides/polar/grids.js +1 -1
  118. package/src/actions/guides/polar/resolve.js +12 -8
  119. package/src/actions/index.js +14 -3
  120. package/src/actions/marks/arc/actions.js +77 -19
  121. package/src/actions/marks/area/actions.js +34 -9
  122. package/src/actions/marks/area/materialize.js +17 -1
  123. package/src/actions/marks/bar/create.js +11 -2
  124. package/src/actions/marks/bar/edit.js +20 -3
  125. package/src/actions/marks/bar/materialize.js +22 -2
  126. package/src/actions/marks/index.js +10 -1
  127. package/src/actions/marks/line/actions.js +37 -14
  128. package/src/actions/marks/line/materialize.js +29 -10
  129. package/src/actions/marks/point/create.js +8 -2
  130. package/src/actions/marks/point/edit.js +18 -6
  131. package/src/actions/marks/point/materialize.js +49 -15
  132. package/src/actions/marks/rect/actions.js +33 -12
  133. package/src/actions/marks/references.js +601 -30
  134. package/src/actions/marks/remove.js +71 -1
  135. package/src/actions/marks/rule/actions.js +47 -10
  136. package/src/actions/marks/shared.js +1 -1
  137. package/src/actions/marks/text/actions.js +74 -9
  138. package/src/actions/marks/text/index.js +4 -0
  139. package/src/actions/marks/text/layout.js +154 -6
  140. package/src/actions/marks/text/removal.js +173 -0
  141. package/src/actions/marks/text/selection.js +73 -0
  142. package/src/actions/marks/tick/actions.js +22 -5
  143. package/src/actions/primitives/editGraphics.js +26 -0
  144. package/src/actions/primitives/semantic.js +2 -1
  145. package/src/actions/primitives/semanticAction.js +83 -10
  146. package/src/actions/primitives/semanticValidation/guide.js +1 -1
  147. package/src/actions/primitives/semanticValidation/index.js +9 -1
  148. package/src/actions/primitives/semanticValidation/layer.js +5 -0
  149. package/src/actions/primitives/semanticValidation/scale.js +28 -7
  150. package/src/actions/regression/components.js +26 -11
  151. package/src/actions/regression/create.js +10 -2
  152. package/src/actions/regression/edit.js +21 -7
  153. package/src/actions/resources/index.js +7 -0
  154. package/src/actions/resources/remove.js +172 -0
  155. package/src/actions/scales/channels.js +212 -0
  156. package/src/actions/scales/consumers/common.js +1 -1
  157. package/src/actions/scales/consumers/families.js +3 -2
  158. package/src/actions/scales/consumers/index.js +19 -2
  159. package/src/actions/scales/consumers/seriesLayout.js +28 -14
  160. package/src/actions/scales/create.js +54 -22
  161. package/src/actions/scales/definitions.js +94 -40
  162. package/src/actions/scales/edit.js +66 -11
  163. package/src/actions/scales/editPolicy.js +81 -11
  164. package/src/actions/scales/index.js +6 -0
  165. package/src/actions/scales/materialize.js +4 -3
  166. package/src/actions/scales/offset.js +48 -0
  167. package/src/actions/scales/parallel.js +71 -0
  168. package/src/actions/scales/preview.js +111 -10
  169. package/src/actions/selection/actions.js +154 -30
  170. package/src/actions/selection/index.js +3 -0
  171. package/src/actions/theme/actions.js +68 -13
  172. package/src/actions/theme/composition.js +238 -0
  173. package/src/actions/theme/index.js +9 -2
  174. package/src/actions/theme/reconcile.js +315 -67
  175. package/src/actions/theme/state.js +160 -0
  176. package/src/actions/violinPlots/create.js +5 -2
  177. package/src/actions/violinPlots/edit.js +1 -0
  178. package/src/core/action.js +10 -0
  179. package/src/core/compositionState.js +6 -1
  180. package/src/core/programState.js +20 -0
  181. package/src/core/resourceReferences.js +332 -0
  182. package/src/core/vocabulary.js +16 -3
  183. package/src/grammar/bars/aggregate.js +4 -1
  184. package/src/grammar/bars/geometry.js +44 -0
  185. package/src/grammar/bin.js +54 -9
  186. package/src/grammar/complete.js +279 -0
  187. package/src/grammar/computed.js +265 -64
  188. package/src/grammar/coordinates.js +37 -0
  189. package/src/grammar/density.js +239 -37
  190. package/src/grammar/displayLabels.js +57 -0
  191. package/src/grammar/facets/dependencies.js +133 -1
  192. package/src/grammar/facets/guides.js +3 -0
  193. package/src/grammar/facets/index.js +7 -47
  194. package/src/grammar/facets/scales.js +15 -18
  195. package/src/grammar/impute.js +340 -0
  196. package/src/grammar/markLabelSelection.js +57 -0
  197. package/src/grammar/markLabels.js +18 -1
  198. package/src/grammar/normalize.js +366 -0
  199. package/src/grammar/pathSeries.js +26 -8
  200. package/src/grammar/pointShapes.js +13 -4
  201. package/src/grammar/polar.js +90 -8
  202. package/src/grammar/regression/index.js +1 -0
  203. package/src/grammar/regression/parameters.js +23 -0
  204. package/src/grammar/roundedRect.js +180 -0
  205. package/src/grammar/rules.js +1 -0
  206. package/src/grammar/scales/colorConsumers.js +20 -1
  207. package/src/grammar/scales/continuous.js +2 -2
  208. package/src/grammar/scales/definition.js +27 -4
  209. package/src/grammar/scales/index.js +1 -0
  210. package/src/grammar/scales/ordinal.js +12 -3
  211. package/src/grammar/scales/policies.js +2 -2
  212. package/src/grammar/scales/size.js +456 -0
  213. package/src/grammar/schemas/concreteGraphic.js +14 -2
  214. package/src/grammar/schemas/graphic.js +15 -0
  215. package/src/grammar/schemas/graphicBounds.js +179 -30
  216. package/src/grammar/schemas/semanticPath.js +7 -2
  217. package/src/grammar/statisticalReference.js +103 -0
  218. package/src/grammar/strokeStyle.js +77 -0
  219. package/src/grammar/summary.js +78 -16
  220. package/src/grammar/timeUnit.js +216 -5
  221. package/src/grammar/transformTopology.js +4 -0
  222. package/src/grammar/transforms.js +394 -28
  223. package/src/grammar/weightedStatistics.js +374 -0
  224. package/src/grammar/window.js +221 -16
  225. package/src/layout/aspect.js +80 -0
  226. package/src/layout/facets.js +59 -5
  227. package/src/layout/labels.js +343 -0
  228. package/src/materialization/bars/aggregate.js +25 -2
  229. package/src/materialization/bars/histogram.js +84 -24
  230. package/src/materialization/bars/ranged.js +19 -3
  231. package/src/materialization/bars/resolve.js +7 -4
  232. package/src/materialization/composition.js +3 -1
  233. package/src/materialization/coordinateBounds.js +204 -0
  234. package/src/materialization/dependencies.js +36 -0
  235. package/src/materialization/facetGuides/legacyCategorical.js +3 -0
  236. package/src/materialization/facetGuides/placement.js +8 -2
  237. package/src/materialization/facetGuides/preparation.js +12 -7
  238. package/src/materialization/facetHeaders.js +346 -0
  239. package/src/materialization/facets.js +15 -65
  240. package/src/materialization/guides/resources.js +26 -0
  241. package/src/materialization/legends.js +1 -1
  242. package/src/materialization/marks/index.js +11 -5
  243. package/src/materialization/marks/policies.js +2 -1
  244. package/src/materialization/rect.js +25 -12
  245. package/src/materialization/rectConfig.js +5 -1
  246. package/src/materialization/scales/map.js +5 -3
  247. package/src/materialization/scales/policies/binnedPosition.js +2 -2
  248. package/src/materialization/scales/policies/offset.js +50 -8
  249. package/src/materialization/scales/policies/series.js +4 -1
  250. package/src/materialization/scales/resolve.js +22 -6
  251. package/src/materialization/selection/items/arc.js +28 -4
  252. package/src/materialization/selection/items/bar.js +30 -3
  253. package/src/materialization/selection/items/common.js +19 -4
  254. package/src/materialization/selection/items/rect.js +28 -4
  255. package/src/materialization/selection/path.js +1 -1
  256. package/src/materialization/selection/point.js +3 -1
  257. package/src/materialization/text.js +265 -14
  258. package/src/renderers/canvas/circle.js +2 -3
  259. package/src/renderers/canvas/line.js +2 -3
  260. package/src/renderers/canvas/native.js +4 -4
  261. package/src/renderers/canvas/path.js +2 -3
  262. package/src/renderers/canvas/rect.js +2 -3
  263. package/src/renderers/canvas/stroke.js +11 -0
  264. package/src/renderers/svg.js +19 -0
  265. package/src/selectors/datasets.js +32 -1
  266. package/src/selectors/layers.js +4 -0
  267. package/src/theme/defaults.js +101 -0
  268. package/types/basic.d.ts +6 -0
  269. package/types/index.d.ts +85 -0
  270. package/types/program.d.ts +910 -117
@@ -0,0 +1,173 @@
1
+ import { action } from "../../../core/action.js";
2
+ import { validateUserId } from "../../../core/identifiers.js";
3
+ import { validateOptionObject } from "../../../core/validation.js";
4
+ import { isSourceOwnedText } from "../../../grammar/text.js";
5
+ import { isTextSource } from "../../../materialization/marks/index.js";
6
+ import { findLayer } from "../../../selectors/layers.js";
7
+ import { markLabelPlacementLeaderId } from "../../../layout/labels.js";
8
+
9
+ const REMOVE_MARK_LABELS_OPTIONS = Object.freeze(["target", "source"]);
10
+
11
+ function compareIds(left, right) {
12
+ return left < right ? -1 : left > right ? 1 : 0;
13
+ }
14
+
15
+ function attachedLabels(program, source) {
16
+ return program.semanticSpec.layers
17
+ .filter(layer => isSourceOwnedText(layer) && layer.source === source)
18
+ .sort((left, right) => compareIds(left.id, right.id));
19
+ }
20
+
21
+ function resolveAttachedLabelTargets(program, args) {
22
+ const hasTarget = Object.hasOwn(args, "target");
23
+ const hasSource = Object.hasOwn(args, "source");
24
+ if (hasTarget === hasSource) {
25
+ throw new Error("removeMarkLabels requires exactly one of target or source.");
26
+ }
27
+ if (hasTarget) {
28
+ const target = validateUserId(args.target, "Attached label id");
29
+ const layer = findLayer(program, target);
30
+ if (!isSourceOwnedText(layer) || !isTextSource(findLayer(program, layer.source))) {
31
+ throw new Error(`Unknown attached label target "${target}".`);
32
+ }
33
+ return [layer];
34
+ }
35
+ const source = validateUserId(args.source, "Label source id");
36
+ const layer = findLayer(program, source);
37
+ if (!isTextSource(layer)) {
38
+ throw new Error(`Unknown label source "${source}".`);
39
+ }
40
+ return attachedLabels(program, source);
41
+ }
42
+
43
+ function collectAttachedLabelRemoval(program, labels) {
44
+ const labelIds = labels.map(layer => layer.id);
45
+ const targets = new Set(labelIds);
46
+ const sourceByLabel = new Map(labels.map(layer => [layer.id, layer.source]));
47
+ const leaderIds = labelIds.flatMap(id => {
48
+ const ids = [];
49
+ const layoutLeader = program.materializationConfigs.labelLayouts?.[id]?.leaderId;
50
+ if (layoutLeader !== undefined) ids.push(layoutLeader);
51
+ const placement = program.markConfigs[id]?.labelAuthoring?.placement;
52
+ if (placement?.leader !== undefined && placement.leader !== false) {
53
+ ids.push(markLabelPlacementLeaderId(id));
54
+ }
55
+ return ids;
56
+ });
57
+ const selectionIds = Object.entries(
58
+ program.materializationConfigs.selections ?? {}
59
+ ).filter(([, config]) => targets.has(config.target)).map(([id]) => id).sort();
60
+ const selected = new Set(selectionIds);
61
+ const highlightIds = Object.entries(
62
+ program.materializationConfigs.highlights ?? {}
63
+ ).filter(([, config]) =>
64
+ targets.has(config.target) || selected.has(config.selection)
65
+ ).map(([id]) => id).sort();
66
+ return Object.freeze({
67
+ sourceByLabel,
68
+ labelIds: Object.freeze(labelIds),
69
+ leaderIds: Object.freeze([...new Set(leaderIds)].sort()),
70
+ selectionIds: Object.freeze(selectionIds),
71
+ highlightIds: Object.freeze(highlightIds)
72
+ });
73
+ }
74
+
75
+ function externalAttachedLabelReferences(program, plan) {
76
+ const labels = new Set(plan.labelIds);
77
+ const leaders = new Set(plan.leaderIds);
78
+ const selections = new Set(plan.selectionIds);
79
+ const references = [];
80
+ for (const layer of program.semanticSpec.layers) {
81
+ if (!labels.has(layer.id) && labels.has(layer.source)) {
82
+ references.push(`layer[${layer.id}].source`);
83
+ }
84
+ }
85
+ for (const [kind, config] of Object.entries(program.guideConfigs.legend ?? {})) {
86
+ if (labels.has(config?.target)) references.push(`guide.legend.${kind}.target`);
87
+ }
88
+ for (const [id, config] of Object.entries(
89
+ program.materializationConfigs.labelLayouts ?? {}
90
+ )) {
91
+ if (!labels.has(id) && leaders.has(config?.leaderId)) {
92
+ references.push(`materialization.labelLayouts[${id}].leaderId`);
93
+ }
94
+ }
95
+ for (const [id, config] of Object.entries(program.markConfigs)) {
96
+ if (labels.has(id)) continue;
97
+ const selection = config?.labelAuthoring?.selection;
98
+ if (selection?.kind === "named" && selections.has(selection.id)) {
99
+ references.push(`markConfig[${id}].labelAuthoring.selection`);
100
+ }
101
+ }
102
+ return references.sort();
103
+ }
104
+
105
+ function removeLabelState(program, plan) {
106
+ let next = program;
107
+ for (const id of plan.highlightIds) {
108
+ next = next._withoutMaterializationConfig(["highlights", id]);
109
+ }
110
+ for (const id of plan.selectionIds) {
111
+ next = next._withoutMaterializationConfig(["selections", id]);
112
+ }
113
+ for (const id of plan.labelIds) {
114
+ next = next
115
+ ._withoutMaterializationConfig(["labelLayouts", id])
116
+ ._withoutMaterializationConfig(["marks", id])
117
+ ._withoutMaterializationConfig(["jitters", id])
118
+ ._withoutMaterializationConfig(["pointPacking", id]);
119
+ }
120
+ for (const id of plan.leaderIds) {
121
+ if (next.graphicSpec.objects[id] !== undefined) {
122
+ next = next.editGraphics({ target: id, remove: true });
123
+ }
124
+ }
125
+ for (const id of plan.labelIds) {
126
+ if (next.graphicSpec.objects[id] !== undefined) {
127
+ next = next.editGraphics({ target: id, remove: true });
128
+ }
129
+ if (findLayer(next, id) !== undefined) {
130
+ next = next.editSemantic({ property: `layer[${id}]`, remove: true });
131
+ }
132
+ }
133
+ const currentLabel = plan.sourceByLabel.get(program.context.currentMark);
134
+ const context = {
135
+ ...(currentLabel === undefined ? {} : { currentMark: currentLabel }),
136
+ ...(plan.selectionIds.includes(program.context.currentSelection)
137
+ ? { currentSelection: undefined }
138
+ : {})
139
+ };
140
+ return Object.keys(context).length === 0 ? next : next._withContext(context);
141
+ }
142
+
143
+ export const removeMarkLabels = action(
144
+ {
145
+ op: "removeMarkLabels",
146
+ description: "Remove attached label layers while preserving their source mark."
147
+ },
148
+ function (args = {}) {
149
+ validateOptionObject(
150
+ args,
151
+ REMOVE_MARK_LABELS_OPTIONS,
152
+ "removeMarkLabels",
153
+ {
154
+ allowEmpty: false,
155
+ emptyError: Error,
156
+ emptyMessage: "removeMarkLabels requires exactly one of target or source."
157
+ }
158
+ );
159
+ const labels = resolveAttachedLabelTargets(this, args);
160
+ const plan = collectAttachedLabelRemoval(this, labels);
161
+ const external = externalAttachedLabelReferences(this, plan);
162
+ if (external.length > 0) {
163
+ throw new Error(
164
+ `removeMarkLabels cannot remove externally referenced labels: ${external.join(", ")}.`
165
+ );
166
+ }
167
+ return removeLabelState(this, plan);
168
+ }
169
+ );
170
+
171
+ export function registerTextLabelRemovalActions(ProgramClass) {
172
+ ProgramClass.prototype.removeMarkLabels = removeMarkLabels;
173
+ }
@@ -0,0 +1,73 @@
1
+ import { action } from "../../../core/action.js";
2
+ import { validateUserId } from "../../../core/identifiers.js";
3
+ import { validateOptionObject } from "../../../core/validation.js";
4
+ import { normalizeLabelSelectionEdit } from
5
+ "../../../grammar/markLabelSelection.js";
6
+ import { isSourceOwnedText } from "../../../grammar/text.js";
7
+ import { isTextSource } from "../../../materialization/marks/index.js";
8
+ import { resolveMarkSelection } from
9
+ "../../../materialization/selection/state.js";
10
+ import { findLayer } from "../../../selectors/layers.js";
11
+ import { rematerializeLabelsBeforeHighlights } from
12
+ "../../selection/actions.js";
13
+
14
+ const OPTIONS = Object.freeze(["target", "select", "selection", "all"]);
15
+
16
+ function requireAttachedLabel(program, target) {
17
+ const id = validateUserId(target, "Attached label id");
18
+ const layer = findLayer(program, id);
19
+ if (!isSourceOwnedText(layer) || !isTextSource(findLayer(program, layer.source))) {
20
+ throw new Error(`Unknown attached label target "${id}".`);
21
+ }
22
+ return layer;
23
+ }
24
+
25
+ function validateSelectionSource(program, source, requested) {
26
+ if (requested.kind === "all") return;
27
+ if (requested.kind === "inline") {
28
+ resolveMarkSelection(program, source, requested.selector);
29
+ return;
30
+ }
31
+ const definition = program.materializationConfigs.selections?.[requested.id];
32
+ if (definition === undefined) {
33
+ throw new Error(`Unknown label selection "${requested.id}".`);
34
+ }
35
+ if (definition.target !== source) {
36
+ throw new Error(
37
+ `Label selection "${requested.id}" must target source mark "${source}".`
38
+ );
39
+ }
40
+ resolveMarkSelection(program, source, definition.selector);
41
+ }
42
+
43
+ const editMarkLabelSelection = action(
44
+ {
45
+ op: "editMarkLabelSelection",
46
+ description: "Replace the final-item membership requested by an attached label."
47
+ },
48
+ function (args = {}) {
49
+ validateOptionObject(args, OPTIONS, "editMarkLabelSelection");
50
+ if (!Object.hasOwn(args, "target")) {
51
+ throw new Error("editMarkLabelSelection requires target.");
52
+ }
53
+ const layer = requireAttachedLabel(this, args.target);
54
+ const selection = normalizeLabelSelectionEdit(args);
55
+ validateSelectionSource(this, layer.source, selection);
56
+ const next = this._withMarkConfig(layer.id, {
57
+ ...this.markConfigs[layer.id],
58
+ labelAuthoring: {
59
+ ...this.markConfigs[layer.id]?.labelAuthoring,
60
+ selection
61
+ }
62
+ });
63
+ return rematerializeLabelsBeforeHighlights(
64
+ next,
65
+ layer.source,
66
+ [layer.id]
67
+ );
68
+ }
69
+ );
70
+
71
+ export function registerTextLabelSelectionActions(ProgramClass) {
72
+ ProgramClass.prototype.editMarkLabelSelection = editMarkLabelSelection;
73
+ }
@@ -17,7 +17,12 @@ import { resolveRowEncodingValues } from
17
17
  import { findDataset } from "../../../selectors/datasets.js";
18
18
  import { findLayer } from "../../../selectors/layers.js";
19
19
  import { DEFAULT_COLORS } from "../../../theme/defaults.js";
20
+ import {
21
+ requestedStrokeDetails,
22
+ STROKE_STYLE_PROPERTIES
23
+ } from "../../../grammar/strokeStyle.js";
20
24
  import { rematerializeHighlightBaseline } from "../lifecycle.js";
25
+ import { rematerializeExistingLegend } from "../../encodings/shared.js";
21
26
  import {
22
27
  applyLayeredMarkInheritance,
23
28
  assertMarkAvailable,
@@ -30,7 +35,7 @@ import {
30
35
  } from "../shared.js";
31
36
 
32
37
  const APPEARANCE_OPTIONS = Object.freeze([
33
- "length", "stroke", "strokeWidth", "opacity"
38
+ "length", "stroke", "strokeWidth", "opacity", ...STROKE_STYLE_PROPERTIES
34
39
  ]);
35
40
  const CREATE_OPTIONS = Object.freeze(["id", "data", ...APPEARANCE_OPTIONS]);
36
41
  const EDIT_OPTIONS = Object.freeze(["target", ...APPEARANCE_OPTIONS]);
@@ -66,6 +71,7 @@ function resolveTick(program, requested, operation) {
66
71
 
67
72
  function validateTickConfig(args, current = DEFAULT_TICK_CONFIG) {
68
73
  return {
74
+ ...current,
69
75
  length: Object.hasOwn(args, "length")
70
76
  ? validatePositiveFinite(args.length, "Tick length")
71
77
  : current.length,
@@ -77,7 +83,8 @@ function validateTickConfig(args, current = DEFAULT_TICK_CONFIG) {
77
83
  : current.strokeWidth,
78
84
  opacity: Object.hasOwn(args, "opacity")
79
85
  ? validateUnitInterval(args.opacity, "Tick opacity")
80
- : current.opacity
86
+ : current.opacity,
87
+ ...requestedStrokeDetails(args, "Tick")
81
88
  };
82
89
  }
83
90
 
@@ -88,6 +95,7 @@ export const createTickMark = action(
88
95
  },
89
96
  function (args = {}) {
90
97
  validateMarkOptions(args, CREATE_OPTIONS, "createTickMark");
98
+ requestedStrokeDetails(args, "createTickMark");
91
99
  const id = resolveMarkId(this, args.id, {
92
100
  defaultId: "tick",
93
101
  label: "Tick mark id",
@@ -132,19 +140,26 @@ export const editTickMark = action(
132
140
  },
133
141
  function (args = {}) {
134
142
  validateMarkOptions(args, EDIT_OPTIONS, "editTickMark");
143
+ requestedStrokeDetails(args, "editTickMark");
135
144
  if (!APPEARANCE_OPTIONS.some(property => Object.hasOwn(args, property))) {
136
145
  throw new Error(
137
146
  "editTickMark requires length, stroke, strokeWidth, or opacity."
138
147
  );
139
148
  }
140
149
  const layer = resolveTick(this, args.target, "editTickMark");
150
+ if (Object.hasOwn(args, "stroke") && layer.encoding?.stroke !== undefined) {
151
+ throw new Error(
152
+ "editTickMark stroke conflicts with a field encoding; use encodeStroke with value to replace it."
153
+ );
154
+ }
141
155
  const config = validateTickConfig(args, {
142
156
  ...DEFAULT_TICK_CONFIG,
143
157
  ...this.markConfigs[layer.id]
144
158
  });
145
- return this
159
+ const materialized = this
146
160
  ._withMarkConfig(layer.id, config)
147
161
  .rematerializeTickMark({ id: layer.id });
162
+ return rematerializeExistingLegend(materialized);
148
163
  }
149
164
  );
150
165
 
@@ -190,6 +205,7 @@ export const rematerializeTickMark = action(
190
205
 
191
206
  const x = resolveRowEncodingValues(this, layer, dataset, "x");
192
207
  const y = resolveRowEncodingValues(this, layer, dataset, "y");
208
+ const stroke = resolveRowEncodingValues(this, layer, dataset, "stroke");
193
209
  const angles = resolveDirectionValues(dataset.values, layer.encoding?.angle);
194
210
  const config = validateTickConfig({}, {
195
211
  ...DEFAULT_TICK_CONFIG,
@@ -210,9 +226,10 @@ export const rematerializeTickMark = action(
210
226
  y1: segments.map(item => item.y1),
211
227
  x2: segments.map(item => item.x2),
212
228
  y2: segments.map(item => item.y2),
213
- stroke: config.stroke,
229
+ stroke: stroke ?? config.stroke,
214
230
  strokeWidth: config.strokeWidth,
215
- opacity: config.opacity
231
+ opacity: config.opacity,
232
+ ...requestedStrokeDetails(config, "Tick mark")
216
233
  });
217
234
  }
218
235
  );
@@ -63,6 +63,32 @@ function replaceDrawableItems(value, id, type) {
63
63
  });
64
64
  }
65
65
 
66
+ export function replaceGraphicItems(program, target, baseType, items) {
67
+ let next = program.editGraphics({ target, property: "items", value: items });
68
+ const graphic = next.graphicSpec.objects[target];
69
+ if (
70
+ graphic.type !== "collection" ||
71
+ !graphic.items.every(item => item.type === baseType)
72
+ ) return next;
73
+ const normalized = freezeOwned({
74
+ ...graphic,
75
+ type: baseType,
76
+ items: freezeOwned(graphic.items.map(item => freezeOwned({
77
+ id: item.id,
78
+ properties: item.properties
79
+ })))
80
+ });
81
+ return next._clone({
82
+ graphicSpec: freezeOwned({
83
+ ...next.graphicSpec,
84
+ objects: freezeOwned({
85
+ ...next.graphicSpec.objects,
86
+ [target]: normalized
87
+ })
88
+ })
89
+ });
90
+ }
91
+
66
92
  function editGeneratedItem(owner, itemIndex, property, value) {
67
93
  const items = [...owner.items];
68
94
  const item = items[itemIndex];
@@ -1,4 +1,5 @@
1
1
  import { createSemanticPrimitiveAction } from "./semanticAction.js";
2
+ import { planResourceRemoval } from "../resources/remove.js";
2
3
  import { validateDatasetTransforms } from "../../grammar/transforms.js";
3
4
  import {
4
5
  validateParallelDimensions,
@@ -24,5 +25,5 @@ export function registerSemanticPrimitiveAction(ProgramClass) {
24
25
  sourceMarkTypes: ["point", "bar", "line", "rule", "rect", "arc"]
25
26
  });
26
27
  ProgramClass.prototype.editSemantic =
27
- createSemanticPrimitiveAction(validateSemanticValue);
28
+ createSemanticPrimitiveAction(validateSemanticValue, planResourceRemoval);
28
29
  }
@@ -79,7 +79,7 @@ function removeEntity(spec, parsed) {
79
79
  if (parsed.kind === "scale" && parsed.path.length === 0) {
80
80
  if (hasSemanticScaleReferences(spec, parsed.id)) throw new Error(`Scale "${parsed.id}" is still referenced.`);
81
81
  }
82
- if (["layer", "scale"].includes(parsed.kind) && parsed.path.length === 0) {
82
+ if (["layer", "scale", "coordinate"].includes(parsed.kind) && parsed.path.length === 0) {
83
83
  const nextCollection = collection.filter((_, itemIndex) => itemIndex !== index);
84
84
  return freezeOwned({
85
85
  ...spec,
@@ -87,10 +87,6 @@ function removeEntity(spec, parsed) {
87
87
  });
88
88
  }
89
89
  if (parsed.kind === "dataset" && parsed.path.length === 0) {
90
- const dataset = collection[index];
91
- if (dataset.source === undefined) {
92
- throw new Error(`Source dataset "${parsed.id}" is immutable after creation.`);
93
- }
94
90
  const referenced = spec.layers.some(layer => layer.data === parsed.id) ||
95
91
  spec.datasets.some(candidate => candidate.source === parsed.id);
96
92
  if (referenced) {
@@ -151,7 +147,50 @@ export function withPreviewDatasetValues(program, {
151
147
  return program._clone({ semanticSpec });
152
148
  }
153
149
 
154
- export function createSemanticPrimitiveAction(validateSemanticValue) {
150
+ // Owned materializers may refresh a generated derived dataset at a stable ID.
151
+ // Keep that privileged immutable transition inside the semantic primitive
152
+ // boundary; public editSemantic still rejects every post-creation dataset edit.
153
+ export function withRematerializedDerivedDataset(program, {
154
+ id,
155
+ source,
156
+ transform,
157
+ values
158
+ }) {
159
+ const semanticSpec = {
160
+ ...program.semanticSpec,
161
+ datasets: program.semanticSpec.datasets.map(dataset =>
162
+ dataset.id === id
163
+ ? { ...dataset, source, transform: [transform], values }
164
+ : dataset
165
+ )
166
+ };
167
+ return program._clone({ semanticSpec });
168
+ }
169
+
170
+ // Final-state planners may need to remove mutually constrained roles before
171
+ // replaying their canonical primitive owners. Keep that speculative clone in
172
+ // the primitive boundary and preserve the caller's layer for later dependency
173
+ // and guide reconciliation.
174
+ export function withoutPreviewLayerEncodings(program, { id, channels }) {
175
+ const removed = new Set(channels);
176
+ const semanticSpec = {
177
+ ...program.semanticSpec,
178
+ layers: program.semanticSpec.layers.map(layer => {
179
+ if (layer.id !== id) return layer;
180
+ return {
181
+ ...layer,
182
+ encoding: Object.fromEntries(
183
+ Object.entries(layer.encoding ?? {}).filter(
184
+ ([channel]) => !removed.has(channel)
185
+ )
186
+ )
187
+ };
188
+ })
189
+ };
190
+ return program._clone({ semanticSpec });
191
+ }
192
+
193
+ export function createSemanticPrimitiveAction(validateSemanticValue, planNamedRemoval) {
155
194
  return action(
156
195
  {
157
196
  op: "editSemantic",
@@ -170,14 +209,37 @@ export function createSemanticPrimitiveAction(validateSemanticValue) {
170
209
  }
171
210
 
172
211
  const parsed = parseSemanticPath(property, { allowContainer: remove });
212
+ const removesNamedResource = remove && parsed.path.length === 0 &&
213
+ ["dataset", "scale", "coordinate"].includes(parsed.kind);
173
214
  if (this.compositionSpec !== undefined && !(
174
- this.compositionSpec.type === "facet" && parsed.kind === "title"
215
+ this.compositionSpec.type === "facet" &&
216
+ (parsed.kind === "title" || removesNamedResource)
175
217
  )) {
176
218
  throw new Error(
177
- "editSemantic on a composition parent currently supports only facet title state."
219
+ "editSemantic on a composition parent supports only facet title state and unreferenced named resource removal."
178
220
  );
179
221
  }
180
222
  if (remove) {
223
+ // Source/coordinate removal and facet-parent removal use the same full
224
+ // dependency policy as the domain lifecycle. Existing unit derived-data
225
+ // teardown retains its semantic-only primitive contract; its owning
226
+ // action coordinates configuration and graphics cleanup separately.
227
+ const resource = removesNamedResource
228
+ ? this.semanticSpec[parsed.collection].find(item => item.id === parsed.id)
229
+ : undefined;
230
+ if (resource !== undefined && (
231
+ parsed.kind === "coordinate" || this.compositionSpec?.type === "facet" ||
232
+ (parsed.kind === "dataset" && resource.source === undefined)
233
+ )) {
234
+ if (planNamedRemoval === undefined) {
235
+ throw new Error("Named resource removal requires the full ChartProgram.");
236
+ }
237
+ planNamedRemoval(this, {
238
+ kind: parsed.kind === "dataset" ? "data" : parsed.kind,
239
+ id: parsed.id,
240
+ operation: "editSemantic"
241
+ });
242
+ }
181
243
  const semanticSpec = parsed.kind === "guide"
182
244
  ? removeRootProperty(this.semanticSpec, "guides", parsed.path)
183
245
  : parsed.kind === "title"
@@ -192,17 +254,28 @@ export function createSemanticPrimitiveAction(validateSemanticValue) {
192
254
  parsed.kind === "layer" &&
193
255
  parsed.path.length === 0 &&
194
256
  this.context.currentMark === parsed.id;
257
+ const clearsCurrentCoordinate =
258
+ parsed.kind === "coordinate" &&
259
+ parsed.path.length === 0 &&
260
+ this.context.currentCoordinate === parsed.id;
261
+ const clearsCurrentGuide =
262
+ parsed.kind === "guide" &&
263
+ parsed.path.length === 2 &&
264
+ this.context.currentGuide === parsed.id;
195
265
  const removesScale = parsed.kind === "scale" && parsed.path.length === 0;
196
266
  const resolvedScales = removesScale ? Object.fromEntries(Object.entries(this.resolvedScales).filter(([id]) => id !== parsed.id)) : this.resolvedScales;
197
267
  return this._clone({
198
268
  semanticSpec,
199
269
  ...(removesScale ? { resolvedScales: freezeOwned(resolvedScales) } : {}),
200
- ...(clearsCurrentData || clearsCurrentMark || (removesScale && this.context.currentScale === parsed.id)
270
+ ...(clearsCurrentData || clearsCurrentMark || clearsCurrentGuide || clearsCurrentCoordinate ||
271
+ (removesScale && this.context.currentScale === parsed.id)
201
272
  ? { context: freezeOwned({
202
273
  ...this.context,
203
274
  ...(removesScale && this.context.currentScale === parsed.id ? { currentScale: undefined } : {}),
204
275
  ...(clearsCurrentData ? { currentData: undefined } : {}),
205
- ...(clearsCurrentMark ? { currentMark: undefined } : {})
276
+ ...(clearsCurrentMark ? { currentMark: undefined } : {}),
277
+ ...(clearsCurrentCoordinate ? { currentCoordinate: undefined } : {}),
278
+ ...(clearsCurrentGuide ? { currentGuide: undefined } : {})
206
279
  }) }
207
280
  : {})
208
281
  });
@@ -30,7 +30,7 @@ export function validateGuideSemanticValue(_program, parsed, value) {
30
30
  validateParallelAxisTitles(value);
31
31
  return;
32
32
  }
33
- if (["legend.series", "legend.color"].includes(parsed.id) && property === "order") {
33
+ if (["legend.series", "legend.color", "legend.stroke"].includes(parsed.id) && property === "order") {
34
34
  normalizeLegendOrder(value);
35
35
  return;
36
36
  }
@@ -1,4 +1,8 @@
1
- import { validateCoordinateType } from "../../../grammar/coordinates.js";
1
+ import {
2
+ normalizeCoordinateAspect,
3
+ validateCoordinateType
4
+ } from "../../../grammar/coordinates.js";
5
+ import { normalizePolarFrameOptions } from "../../../grammar/polar.js";
2
6
  import { validateScaleSemanticValue } from "./scale.js";
3
7
  import { validateDatasetSemanticValue } from "./dataset.js";
4
8
  import { validateLayerSemanticValue } from "./layer.js";
@@ -27,6 +31,10 @@ export function createSemanticValueValidator({
27
31
  validateScaleSemanticValue(program, parsed, value);
28
32
  } else if (parsed.kind === "coordinate" && parsed.path[0] === "type") {
29
33
  validateCoordinateType(value);
34
+ } else if (parsed.kind === "coordinate" && parsed.path[0] === "aspect") {
35
+ normalizeCoordinateAspect(value);
36
+ } else if (parsed.kind === "coordinate" && parsed.path[0] === "polarFrame") {
37
+ normalizePolarFrameOptions(value);
30
38
  } else if (parsed.kind === "guide") {
31
39
  validateGuideSemanticValue(program, parsed, value);
32
40
  } else if (parsed.kind === "title") {
@@ -17,6 +17,8 @@ import { normalizeCategoryOrder } from "../../../grammar/categoryOrder.js";
17
17
  import { validateSemanticFieldType } from "../../../grammar/scales/index.js";
18
18
  import { findLayer } from "../../../selectors/layers.js";
19
19
  import { validateNonEmptySemanticString } from "./shared.js";
20
+ import { normalizeStatisticalWeight } from
21
+ "../../../grammar/weightedStatistics.js";
20
22
 
21
23
  function validateLayerSource(program, parsed, value, sourceMarkTypes) {
22
24
  validateUserId(value, "Layer source id");
@@ -81,6 +83,9 @@ export function validateLayerSemanticValue(
81
83
  validateParallel?.(property, value);
82
84
  }
83
85
  if (property.endsWith(".aggregate")) validateAggregate(value);
86
+ if (property === "encoding.x.weight") {
87
+ normalizeStatisticalWeight(value, "Histogram weight");
88
+ }
84
89
  if (property.endsWith(".bin.maxBins")) normalizeHistogramBin({ maxBins: value });
85
90
  if (property.endsWith(".bin.step")) validateHistogramBinStep(value);
86
91
  if (property.endsWith(".bin.boundaries")) validateHistogramBinBoundaries(value);
@@ -8,9 +8,22 @@ import {
8
8
  validateSemanticScaleType
9
9
  } from "../../../grammar/scales/index.js";
10
10
  import { findSemanticScale } from "../../../selectors/scales.js";
11
+ import { findScaleConsumers } from "../../scales/consumers/index.js";
11
12
 
12
- function validateOwnedProperty(existing, property) {
13
+ function isOffsetScale(program, id, existing) {
14
+ if (existing?.type !== "ordinal") return false;
15
+ const consumers = findScaleConsumers(program, id);
16
+ return consumers.length > 0 && consumers.every(
17
+ consumer => ["xOffset", "yOffset"].includes(consumer.channel)
18
+ );
19
+ }
20
+
21
+ function validateOwnedProperty(program, id, existing, property) {
13
22
  if (existing?.type !== undefined) {
23
+ if (
24
+ isOffsetScale(program, id, existing) &&
25
+ ["paddingInner", "paddingOuter", "align"].includes(property)
26
+ ) return;
14
27
  validateScalePropertyForType(existing.type, property);
15
28
  }
16
29
  }
@@ -31,7 +44,13 @@ export function validateScaleSemanticValue(program, parsed, value) {
31
44
  "nice", "zero", "clamp", "base", "exponent", "constant", "midpoint",
32
45
  "paddingInner", "paddingOuter", "padding", "align"
33
46
  ]) {
34
- if (existing?.[owned] !== undefined) validateScalePropertyForType(value, owned);
47
+ if (existing?.[owned] === undefined) continue;
48
+ if (
49
+ value === "ordinal" &&
50
+ isOffsetScale(program, parsed.id, existing) &&
51
+ ["paddingInner", "paddingOuter", "align"].includes(owned)
52
+ ) continue;
53
+ validateScalePropertyForType(value, owned);
35
54
  }
36
55
  return;
37
56
  }
@@ -41,7 +60,9 @@ export function validateScaleSemanticValue(program, parsed, value) {
41
60
  if (typeof value !== "boolean") {
42
61
  throw new TypeError(`Scale ${property} must be a boolean.`);
43
62
  }
44
- if (property !== "reverse") validateOwnedProperty(existing, property);
63
+ if (property !== "reverse") {
64
+ validateOwnedProperty(program, parsed.id, existing, property);
65
+ }
45
66
  return;
46
67
  }
47
68
  if (["base", "exponent", "constant"].includes(property)) {
@@ -51,7 +72,7 @@ export function validateScaleSemanticValue(program, parsed, value) {
51
72
  if (property === "base" && value === 1) {
52
73
  throw new RangeError("Scale base must not equal 1.");
53
74
  }
54
- validateOwnedProperty(existing, property);
75
+ validateOwnedProperty(program, parsed.id, existing, property);
55
76
  return;
56
77
  }
57
78
  if (property === "interpolate") {
@@ -64,20 +85,20 @@ export function validateScaleSemanticValue(program, parsed, value) {
64
85
  "Scale paddingInner must be from 0 (inclusive) to 1 (exclusive)."
65
86
  );
66
87
  }
67
- validateOwnedProperty(existing, property);
88
+ validateOwnedProperty(program, parsed.id, existing, property);
68
89
  return;
69
90
  }
70
91
  if (property === "paddingOuter" || property === "padding") {
71
92
  if (!Number.isFinite(value) || value < 0) {
72
93
  throw new RangeError(`Scale ${property} must be a non-negative finite number.`);
73
94
  }
74
- validateOwnedProperty(existing, property);
95
+ validateOwnedProperty(program, parsed.id, existing, property);
75
96
  return;
76
97
  }
77
98
  if (property === "align") {
78
99
  if (!Number.isFinite(value) || value < 0 || value > 1) {
79
100
  throw new RangeError("Scale align must be between 0 and 1.");
80
101
  }
81
- validateOwnedProperty(existing, property);
102
+ validateOwnedProperty(program, parsed.id, existing, property);
82
103
  }
83
104
  }