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
@@ -5,10 +5,15 @@ import { validateKeys } from "../../core/validation.js";
5
5
  import { findDataset } from "../../selectors/datasets.js";
6
6
  import { findLayer } from "../../selectors/layers.js";
7
7
  import { applyDetachedScaleRematerialization } from "../../materialization/dependencies.js";
8
+ import { markLabelPlacementLeaderId } from "../../layout/labels.js";
8
9
  import {
9
10
  getPositionChannelDefinition,
10
11
  POSITION_CHANNELS
11
12
  } from "../../core/vocabulary.js";
13
+ import {
14
+ canonicalResourcePath,
15
+ collectResourceReferences
16
+ } from "../../core/resourceReferences.js";
12
17
 
13
18
  const OPTIONS = Object.freeze(["target"]);
14
19
  const REMOVE_AXIS = Object.freeze({
@@ -40,6 +45,12 @@ function ownedChildren(program, id) {
40
45
  program.semanticSpec.layers
41
46
  .filter(layer => layer.source === id)
42
47
  .map(layer => layer.id)
48
+ ).concat(
49
+ program.semanticSpec.layers
50
+ .filter(layer =>
51
+ program.markConfigs[layer.id]?.statisticalReference?.source === id
52
+ )
53
+ .map(layer => layer.id)
43
54
  ).filter(child => child !== undefined && child !== id && findLayer(program, child) !== undefined);
44
55
  }
45
56
 
@@ -47,6 +58,9 @@ function ownership(program) {
47
58
  const ownerByChild = new Map();
48
59
  for (const layer of program.semanticSpec.layers) {
49
60
  for (const child of ownedChildren(program, layer.id)) {
61
+ if (program.markConfigs[child]?.statisticalReference !== undefined) {
62
+ continue;
63
+ }
50
64
  ownerByChild.set(child, layer.id);
51
65
  }
52
66
  }
@@ -86,6 +100,37 @@ function collectClosure(program, root) {
86
100
  return result;
87
101
  }
88
102
 
103
+ function externalMarkReferences(program, ids) {
104
+ const closure = new Set(ids);
105
+ const references = ids.flatMap(id => collectResourceReferences(program, {
106
+ kind: "mark",
107
+ id
108
+ })).filter(reference => {
109
+ if (reference.strength === "context") return false;
110
+ if (["layer", "markConfig"].includes(reference.ownerKind)) {
111
+ return !closure.has(reference.ownerId);
112
+ }
113
+ if (["selection", "highlight", "legend"].includes(reference.ownerKind)) {
114
+ return false;
115
+ }
116
+ if (
117
+ reference.ownerKind === "axis" &&
118
+ reference.ownerId === "parallel.axes"
119
+ ) {
120
+ return false;
121
+ }
122
+ return true;
123
+ });
124
+ const unique = new Map(references.map(reference => [[
125
+ reference.ownerKind,
126
+ reference.ownerId,
127
+ canonicalResourcePath(reference.path)
128
+ ].join("\u0000"), reference]));
129
+ return [...unique.entries()]
130
+ .sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
131
+ .map(([, reference]) => reference);
132
+ }
133
+
89
134
  function ownedDerivedData(program, ids) {
90
135
  const candidates = new Set();
91
136
  for (const id of ids) {
@@ -100,7 +145,8 @@ function ownedDerivedData(program, ids) {
100
145
  config.gradientPlot?.profileId,
101
146
  config.violinPlot?.materialized === true ? layer?.data : undefined,
102
147
  config.ecdfPlot?.data,
103
- config.endpointPlot?.data
148
+ config.endpointPlot?.data,
149
+ config.statisticalReference?.dataId
104
150
  ]) {
105
151
  if (candidate !== undefined) candidates.add(candidate);
106
152
  }
@@ -208,11 +254,26 @@ export const removeMark = action(
208
254
  validateKeys(args, OPTIONS, "removeMark");
209
255
  const owner = resolveOwner(this, args.target);
210
256
  const ids = collectClosure(this, owner.id);
257
+ const external = externalMarkReferences(this, ids);
258
+ if (external.length > 0) {
259
+ throw new Error(
260
+ `removeMark cannot remove externally referenced marks: ${external.map(
261
+ reference => `${reference.ownerKind} "${reference.ownerId}" at ` +
262
+ canonicalResourcePath(reference.path)
263
+ ).join("; ")}.`
264
+ );
265
+ }
211
266
  const previousLayers = ids.map(id => findLayer(this, id));
212
267
  const derived = ownedDerivedData(this, ids);
213
268
  const positionScales = usedPositionScales(this, ids);
214
269
  let next = this;
215
270
 
271
+ if (ids.includes(next.guideConfigs.axis?.parallel?.axes?.target)) {
272
+ next = next.removeParallelAxes({
273
+ target: next.guideConfigs.axis.parallel.axes.target
274
+ });
275
+ }
276
+
216
277
  const legendTargets = [...new Set(Object.values(next.guideConfigs.legend ?? {})
217
278
  .filter(config => ids.includes(config?.target))
218
279
  .map(config => config.target))];
@@ -230,6 +291,15 @@ export const removeMark = action(
230
291
  ) {
231
292
  next = next.editGraphics({ target: labelLayout.leaderId, remove: true });
232
293
  }
294
+ const placement = next.markConfigs[id]?.labelAuthoring?.placement;
295
+ const placementLeaderId = markLabelPlacementLeaderId(id);
296
+ if (
297
+ placement?.leader !== undefined &&
298
+ placement.leader !== false &&
299
+ next.graphicSpec.objects[placementLeaderId] !== undefined
300
+ ) {
301
+ next = next.editGraphics({ target: placementLeaderId, remove: true });
302
+ }
233
303
  if (findLayer(next, id) !== undefined) {
234
304
  next = next.editSemantic({ property: `layer[${id}]`, remove: true });
235
305
  }
@@ -9,11 +9,13 @@ import {
9
9
  normalizeStrokeDashPattern
10
10
  } from "../../../grammar/scales/index.js";
11
11
  import { deriveRuleValues, resolveRuleMode } from "../../../grammar/rules.js";
12
- import { resolveGraphicBounds } from "../../../layout/canvas.js";
12
+ import { resolveCoordinateBounds } from "../../../materialization/coordinateBounds.js";
13
+ import { readRectItemGeometry } from "../../../grammar/roundedRect.js";
13
14
  import { findDataset } from "../../../selectors/datasets.js";
14
15
  import { findLayer, resolveEligibleLayer } from "../../../selectors/layers.js";
15
16
  import { applyRuleAppearance, planRuleAppearance } from "./appearance.js";
16
17
  import { DEFAULT_COLORS } from "../../../theme/defaults.js";
18
+ import { rematerializeExistingLegend } from "../../encodings/shared.js";
17
19
  import {
18
20
  assertMarkAvailable,
19
21
  applyLayeredMarkInheritance,
@@ -31,9 +33,19 @@ import { applyOffsetPositionValues } from
31
33
  "../../../materialization/rowEncoding.js";
32
34
  import { resolveCategorySlotOffset } from
33
35
  "../../../materialization/categorySlotOffset.js";
36
+ import { mapScaleConsumerValues } from
37
+ "../../../materialization/scales/map.js";
38
+ import {
39
+ requestedStrokeDetails,
40
+ STROKE_STYLE_PROPERTIES
41
+ } from "../../../grammar/strokeStyle.js";
34
42
 
35
- const CREATE_OPTIONS = Object.freeze(["id", "data", "stroke", "strokeWidth", "strokeDash", "opacity"]);
36
- const EDIT_OPTIONS = Object.freeze(["target", "stroke", "strokeWidth", "strokeDash", "opacity"]);
43
+ const APPEARANCE_OPTIONS = Object.freeze([
44
+ "stroke", "strokeWidth", "strokeDash", "opacity",
45
+ ...STROKE_STYLE_PROPERTIES
46
+ ]);
47
+ const CREATE_OPTIONS = Object.freeze(["id", "data", ...APPEARANCE_OPTIONS]);
48
+ const EDIT_OPTIONS = Object.freeze(["target", ...APPEARANCE_OPTIONS]);
37
49
  const REMATERIALIZE_OPTIONS = Object.freeze(["id"]);
38
50
  const SPAN_OPTIONS = Object.freeze(["id", "orientation", "size"]);
39
51
  const DEFAULT_RULE_CONFIG = Object.freeze({
@@ -87,6 +99,7 @@ const createRuleMark = action(
87
99
  },
88
100
  function (args = {}) {
89
101
  validateMarkOptions(args, CREATE_OPTIONS, "createRuleMark");
102
+ const strokeDetails = requestedStrokeDetails(args, "createRuleMark");
90
103
  const id = resolveMarkId(this, args.id, {
91
104
  defaultId: "rule",
92
105
  label: "Rule mark id",
@@ -115,6 +128,7 @@ const createRuleMark = action(
115
128
  })
116
129
  ._withMarkConfig(id, {
117
130
  ...DEFAULT_RULE_CONFIG,
131
+ ...strokeDetails,
118
132
  ...(inherited === undefined
119
133
  ? {}
120
134
  : {
@@ -132,6 +146,7 @@ const editRuleMark = action(
132
146
  { op: "editRuleMark", description: "Edit constant rule appearance through its encoding owners." },
133
147
  function (args = {}) {
134
148
  validateMarkOptions(args, EDIT_OPTIONS, "editRuleMark");
149
+ const strokeDetails = requestedStrokeDetails(args, "editRuleMark");
135
150
  if (args.target !== undefined) validateUserId(args.target, "Rule mark id");
136
151
  const layer = resolveEligibleLayer(this, {
137
152
  target: args.target,
@@ -139,8 +154,17 @@ const editRuleMark = action(
139
154
  label: "rule mark"
140
155
  });
141
156
  const appearance = planRuleAppearance(args, layer);
142
- if (appearance.length === 0) throw new Error("editRuleMark requires an appearance change.");
143
- return applyRuleAppearance(this, layer.id, appearance);
157
+ if (appearance.length === 0 && Object.keys(strokeDetails).length === 0) {
158
+ throw new Error("editRuleMark requires an appearance change.");
159
+ }
160
+ const configured = this._withMarkConfig(layer.id, {
161
+ ...this.markConfigs[layer.id],
162
+ ...strokeDetails
163
+ });
164
+ const materialized = appearance.length === 0
165
+ ? configured.rematerializeRuleMark({ id: layer.id })
166
+ : applyRuleAppearance(configured, layer.id, appearance);
167
+ return rematerializeExistingLegend(materialized);
144
168
  }
145
169
  );
146
170
 
@@ -180,7 +204,7 @@ const rematerializeRuleMark = action(
180
204
  const scaleIds = [...new Set(
181
205
  [
182
206
  "x", "y", "x2", "y2", "xOffset", "yOffset",
183
- "strokeDash", "strokeWidth", "opacity"
207
+ "stroke", "strokeDash", "strokeWidth", "opacity"
184
208
  ]
185
209
  .map(channel => layer.encoding?.[channel]?.scale)
186
210
  .filter(scale => scale !== undefined)
@@ -224,7 +248,7 @@ const rematerializeRuleMark = action(
224
248
  channel
225
249
  });
226
250
  }
227
- const bounds = resolveGraphicBounds(resolved);
251
+ const bounds = resolveCoordinateBounds(resolved, layer.coordinate);
228
252
  const x1 = [];
229
253
  const y1 = [];
230
254
  const x2 = [];
@@ -252,7 +276,11 @@ const rematerializeRuleMark = action(
252
276
  startY = endY = mapped.y[index];
253
277
  } else if (mode === "box-span") {
254
278
  const owner = resolved.graphicSpec.objects[boxSpan];
255
- const box = owner?.items?.[index]?.properties;
279
+ const item = owner?.items?.[index];
280
+ const box = readRectItemGeometry({
281
+ type: item?.type ?? owner?.type,
282
+ properties: item?.properties
283
+ });
256
284
  if (box === undefined) throw new Error(`Rule mark "${id}" requires box span owner "${boxSpan}".`);
257
285
  if (layer.encoding?.x?.fieldType === "quantitative") {
258
286
  startX = endX = mapped.x[index];
@@ -299,6 +327,7 @@ const rematerializeRuleMark = action(
299
327
  ...resolved.markConfigs[id]
300
328
  };
301
329
  const dashEncoding = layer.encoding?.strokeDash;
330
+ const strokeEncoding = layer.encoding?.stroke;
302
331
  const opacityEncoding = layer.encoding?.opacity;
303
332
  const strokeWidthEncoding = layer.encoding?.strokeWidth;
304
333
  const strokeDash = dashEncoding?.scale === undefined
@@ -325,6 +354,13 @@ const rematerializeRuleMark = action(
325
354
  derived.values.strokeWidth,
326
355
  resolved.resolvedScales[strokeWidthEncoding.scale]
327
356
  );
357
+ const stroke = strokeEncoding?.scale === undefined
358
+ ? resolvedConfig.stroke
359
+ : mapScaleConsumerValues(
360
+ derived.values.stroke,
361
+ resolved.resolvedScales[strokeEncoding.scale],
362
+ "stroke"
363
+ );
328
364
 
329
365
  return editMarkGraphic(resolved, id, {
330
366
  length: derived.length,
@@ -332,10 +368,11 @@ const rematerializeRuleMark = action(
332
368
  y1,
333
369
  x2,
334
370
  y2,
335
- stroke: resolvedConfig.stroke,
371
+ stroke,
336
372
  strokeWidth,
337
373
  strokeDash,
338
- opacity
374
+ opacity,
375
+ ...requestedStrokeDetails(resolvedConfig, "Rule mark")
339
376
  });
340
377
  }
341
378
  );
@@ -29,7 +29,7 @@ export function resolveMarkData(program, requested) {
29
29
  }
30
30
 
31
31
  const dataset = requireMaterializedDataset(program, data);
32
- return { data, dataset };
32
+ return { data: dataset.id, dataset };
33
33
  }
34
34
 
35
35
  export function resolveMarkId(program, requested, {
@@ -13,7 +13,17 @@ import {
13
13
  canMaterializeText,
14
14
  isTextSource
15
15
  } from "../../../materialization/marks/index.js";
16
- import { resolveTextGraphicItems } from "../../../materialization/text.js";
16
+ import {
17
+ resolveTextGraphicItems,
18
+ validateSourceMarkLabelPlacement
19
+ } from "../../../materialization/text.js";
20
+ import { markLabelPlacementLeaderId } from "../../../layout/labels.js";
21
+ import { resolveMarkSelection } from
22
+ "../../../materialization/selection/state.js";
23
+ import { rematerializeLabelsBeforeHighlights } from
24
+ "../../selection/actions.js";
25
+ import { normalizeLabelSelectionCreate } from
26
+ "../../../grammar/markLabelSelection.js";
17
27
  import { findLayer, resolveEligibleLayer } from "../../../selectors/layers.js";
18
28
  import {
19
29
  applyLayeredMarkInheritance,
@@ -49,6 +59,13 @@ function eligibleSource(program, layer, requestedData) {
49
59
  !sourceMatchesData(program, layer, requestedData)
50
60
  ) return false;
51
61
  if (layer.mark.type === "arc") return canMaterializeArc(program, layer);
62
+ if (
63
+ layer.mark.type === "point" &&
64
+ findCoordinate(program, layer.coordinate)?.type === "polar"
65
+ ) {
66
+ return layer.encoding?.theta?.scale !== undefined &&
67
+ layer.encoding?.radius?.scale !== undefined;
68
+ }
52
69
  if (layer.mark.type === "rect" && (canMaterializeRect(program, layer) ||
53
70
  program.markConfigs[layer.id]?.gradientPlot?.materialized === true)) return true;
54
71
  const encodings = resolveCompatibleEncodings(program, layer, "text");
@@ -91,7 +108,9 @@ function textInheritance(program, source) {
91
108
  source: source.id,
92
109
  data: source.data,
93
110
  coordinate: source.coordinate,
94
- encoding: source.mark.type === "arc"
111
+ encoding: source.mark.type === "arc" ||
112
+ (source.mark.type === "point" &&
113
+ findCoordinate(program, source.coordinate)?.type === "polar")
95
114
  ? {}
96
115
  : resolveCompatibleEncodings(program, source, "text")
97
116
  };
@@ -161,6 +180,7 @@ const createTextMark = action(
161
180
 
162
181
  const LABEL_OPTIONS = Object.freeze([
163
182
  "id", "source", "field", "value", "content", "normalizeBy", "format", "layout",
183
+ "select", "selection", "placement",
164
184
  ...STYLE_OPTIONS
165
185
  ]);
166
186
 
@@ -320,6 +340,32 @@ const createMarkLabels = action(
320
340
  }
321
341
  const id = validateUserId(args.id === undefined ? `${inherited.source}-labels` : args.id, "Text mark id");
322
342
  assertMarkAvailable(this, id);
343
+ const selection = normalizeLabelSelectionCreate(args);
344
+ const placement = args.placement === undefined
345
+ ? undefined
346
+ : validateSourceMarkLabelPlacement(this, inherited.source, args.placement);
347
+ if (
348
+ placement?.leader !== undefined && placement.leader !== false &&
349
+ this.graphicSpec.objects[markLabelPlacementLeaderId(id)] !== undefined
350
+ ) {
351
+ throw new Error(
352
+ `Mark label placement leader graphic "${markLabelPlacementLeaderId(id)}" already exists.`
353
+ );
354
+ }
355
+ if (selection.kind === "inline") {
356
+ resolveMarkSelection(this, inherited.source, selection.selector);
357
+ } else if (selection.kind === "named") {
358
+ const definition = this.materializationConfigs.selections?.[selection.id];
359
+ if (definition === undefined) {
360
+ throw new Error(`Unknown label selection "${selection.id}".`);
361
+ }
362
+ if (definition.target !== inherited.source) {
363
+ throw new Error(
364
+ `Label selection "${selection.id}" must target source mark "${inherited.source}".`
365
+ );
366
+ }
367
+ resolveMarkSelection(this, inherited.source, definition.selector);
368
+ }
323
369
  const style = Object.fromEntries(STYLE_OPTIONS
324
370
  .filter(option => Object.hasOwn(args, option))
325
371
  .map(option => [option, args[option]]));
@@ -337,9 +383,23 @@ const createMarkLabels = action(
337
383
  throw new Error("createMarkLabels layout target is owned by the created label layer.");
338
384
  }
339
385
  const apply = program => {
340
- const next = program.createTextMark({
386
+ let next = program.createTextMark({
341
387
  id, source: inherited.source, align: "center", baseline: "middle", ...style
342
- }).encodeText({ target: id, ...encoding });
388
+ });
389
+ next = next._withMarkConfig(id, {
390
+ ...next.markConfigs[id],
391
+ labelAuthoring: {
392
+ selection,
393
+ ...(placement === undefined ? {} : { placement })
394
+ }
395
+ });
396
+ next = next.encodeText({ target: id, ...encoding });
397
+ next = rematerializeLabelsBeforeHighlights(
398
+ next,
399
+ inherited.source,
400
+ [id],
401
+ { skipWithoutHighlights: true }
402
+ );
343
403
  return layout === undefined ? next : next.layoutLabels({ ...layout, target: id });
344
404
  };
345
405
  if (layout !== undefined) {
@@ -376,9 +436,11 @@ const rematerializeTextMark = action(
376
436
  : this.editGraphics({ target: id, property: "length", value: 0 });
377
437
  return replayLayout && next.materializationConfigs.labelLayouts?.[id] !== undefined
378
438
  ? next.materializeLabelLayout({ id, rematerializeBase: false })
379
- : next;
439
+ : layer.source === undefined
440
+ ? next
441
+ : next.materializeMarkLabelPlacement({ id });
380
442
  }
381
- const next = this.editGraphics({
443
+ let next = this.editGraphics({
382
444
  target: id,
383
445
  property: "items",
384
446
  value: resolveTextGraphicItems(
@@ -387,9 +449,12 @@ const rematerializeTextMark = action(
387
449
  this.markConfigs[id] ?? DEFAULT_TEXT_MARK
388
450
  )
389
451
  });
390
- return replayLayout && next.materializationConfigs.labelLayouts?.[id] !== undefined
391
- ? next.materializeLabelLayout({ id, rematerializeBase: false })
392
- : next;
452
+ if (replayLayout && next.materializationConfigs.labelLayouts?.[id] !== undefined) {
453
+ return next.materializeLabelLayout({ id, rematerializeBase: false });
454
+ }
455
+ return layer.source === undefined
456
+ ? next
457
+ : next.materializeMarkLabelPlacement({ id });
393
458
  }
394
459
  );
395
460
 
@@ -1,7 +1,11 @@
1
1
  import { registerTextMarkActions as registerTextCoreActions } from "./actions.js";
2
2
  import { registerTextLabelLayoutActions } from "./layout.js";
3
+ import { registerTextLabelRemovalActions } from "./removal.js";
4
+ import { registerTextLabelSelectionActions } from "./selection.js";
3
5
 
4
6
  export function registerTextMarkActions(ProgramClass) {
5
7
  registerTextCoreActions(ProgramClass);
6
8
  registerTextLabelLayoutActions(ProgramClass);
9
+ registerTextLabelRemovalActions(ProgramClass);
10
+ registerTextLabelSelectionActions(ProgramClass);
7
11
  }
@@ -2,17 +2,23 @@ import { action } from "../../../core/action.js";
2
2
  import { validateUserId } from "../../../core/identifiers.js";
3
3
  import { validateOptionObject } from "../../../core/validation.js";
4
4
  import { DEFAULT_TEXT_MARK } from "../../../grammar/text.js";
5
+ import { isSourceOwnedText } from "../../../grammar/text.js";
5
6
  import { findGraphicParent } from "../../../grammar/schemas/graphicTree.js";
6
7
  import { validateConcreteGraphicValue } from
7
8
  "../../../grammar/schemas/concreteGraphic.js";
8
9
  import {
10
+ markLabelPlacementLeaderId,
9
11
  normalizeLabelLayoutGeometry,
10
12
  resolveLabelLayout,
11
13
  resolveLabelLeader
12
14
  } from "../../../layout/labels.js";
15
+ import { resolveTextBounds } from "../../../core/textMetrics.js";
13
16
  import { resolveGraphicBounds } from "../../../layout/canvas.js";
14
17
  import { canMaterializeText } from "../../../materialization/marks/index.js";
15
- import { resolveTextGraphicEntries } from "../../../materialization/text.js";
18
+ import {
19
+ resolveTextGraphicEntries,
20
+ validateSourceMarkLabelPlacement
21
+ } from "../../../materialization/text.js";
16
22
  import { findLayer, resolveEligibleLayer } from "../../../selectors/layers.js";
17
23
  import { editMarkGraphic } from "../shared.js";
18
24
 
@@ -21,6 +27,8 @@ export const TEXT_LABEL_LAYOUT_OPTIONS = Object.freeze([
21
27
  ]);
22
28
  const REMOVE_OPTIONS = Object.freeze(["target"]);
23
29
  const MATERIALIZE_OPTIONS = Object.freeze(["id", "rematerializeBase"]);
30
+ const MATERIALIZE_PLACEMENT_OPTIONS = Object.freeze(["id"]);
31
+ const EDIT_PLACEMENT_OPTIONS = Object.freeze(["target", "placement"]);
24
32
  const LEADER_OPTIONS = Object.freeze([
25
33
  "stroke", "strokeWidth", "strokeDash", "opacity"
26
34
  ]);
@@ -166,10 +174,10 @@ function removeLeaderGraphic(program, id) {
166
174
  : program.editGraphics({ target: id, remove: true });
167
175
  }
168
176
 
169
- function materializeLeaders(program, layer, config, resolved) {
177
+ function materializeLeaders(program, layer, config, items) {
170
178
  let next = removeLeaderGraphic(program, config.leaderId);
171
179
  if (config.leader === false) return { program: next, count: 0 };
172
- const leaders = resolved.items.map(resolveLabelLeader).filter(Boolean);
180
+ const leaders = items.map(resolveLabelLeader).filter(Boolean);
173
181
  if (leaders.length === 0) return { program: next, count: 0 };
174
182
  next = next.createGraphics({
175
183
  id: config.leaderId,
@@ -190,6 +198,75 @@ function materializeLeaders(program, layer, config, resolved) {
190
198
  return { program: next, count: leaders.length };
191
199
  }
192
200
 
201
+ function placementLeaderItems(program, layer) {
202
+ const entries = resolveTextGraphicEntries(
203
+ program,
204
+ layer,
205
+ program.markConfigs[layer.id] ?? DEFAULT_TEXT_MARK
206
+ );
207
+ const graphic = program.graphicSpec.objects[layer.id];
208
+ if (graphic?.type !== "text" || !Array.isArray(graphic.items)) {
209
+ throw new Error(`Text mark "${layer.id}" requires text collection graphics.`);
210
+ }
211
+ if (entries.length !== graphic.items.length) {
212
+ throw new Error(`Text mark "${layer.id}" placement materialization is inconsistent.`);
213
+ }
214
+ return entries.map((entry, index) => {
215
+ const properties = graphic.items[index].properties;
216
+ return {
217
+ id: graphic.items[index].id,
218
+ x: properties.x,
219
+ y: properties.y,
220
+ sourceX: entry.anchor.x,
221
+ sourceY: entry.anchor.y,
222
+ dx: properties.x - entry.anchor.x,
223
+ dy: properties.y - entry.anchor.y,
224
+ bounds: resolveTextBounds(properties)
225
+ };
226
+ });
227
+ }
228
+
229
+ const materializeMarkLabelPlacement = action(
230
+ {
231
+ op: "materializeMarkLabelPlacement",
232
+ description: "Reconcile semantic attached-label placement leaders."
233
+ },
234
+ function (args = {}) {
235
+ validateOptionObject(
236
+ args,
237
+ MATERIALIZE_PLACEMENT_OPTIONS,
238
+ "materializeMarkLabelPlacement"
239
+ );
240
+ const id = validateUserId(args.id, "Attached label id");
241
+ const layer = findLayer(this, id);
242
+ if (!isSourceOwnedText(layer)) {
243
+ throw new Error(`Unknown attached label target "${id}".`);
244
+ }
245
+ const leaderId = markLabelPlacementLeaderId(id);
246
+ const placement = this.markConfigs[id]?.labelAuthoring?.placement;
247
+ if (placement?.leader === undefined || placement.leader === false) {
248
+ return this;
249
+ }
250
+ const layout = this.materializationConfigs.labelLayouts?.[id];
251
+ if (layout !== undefined && layout.leader !== false) {
252
+ throw new Error(
253
+ "Semantic label placement leader conflicts with the active collision-layout leader."
254
+ );
255
+ }
256
+ if (!canMaterializeText(this, layer)) {
257
+ return removeLeaderGraphic(this, leaderId);
258
+ }
259
+ return materializeLeaders(this, layer, {
260
+ leaderId,
261
+ leader: {
262
+ ...placement.leader,
263
+ strokeDash: [],
264
+ opacity: 1
265
+ }
266
+ }, placementLeaderItems(this, layer)).program;
267
+ }
268
+ );
269
+
193
270
  const materializeLabelLayout = action(
194
271
  {
195
272
  op: "materializeLabelLayout",
@@ -222,9 +299,9 @@ const materializeLabelLayout = action(
222
299
  x: resolved.items.map(item => item.x),
223
300
  y: resolved.items.map(item => item.y)
224
301
  });
225
- const leaders = materializeLeaders(next, layer, config, resolved);
302
+ const leaders = materializeLeaders(next, layer, config, resolved.items);
226
303
  next = leaders.program;
227
- return next._withMaterializationConfig(["labelLayouts", id], {
304
+ next = next._withMaterializationConfig(["labelLayouts", id], {
228
305
  axis: config.axis,
229
306
  padding: config.padding,
230
307
  maxDisplacement: config.maxDisplacement,
@@ -243,6 +320,9 @@ const materializeLabelLayout = action(
243
320
  warnings: resolved.warnings
244
321
  }
245
322
  });
323
+ return layer.source === undefined
324
+ ? next
325
+ : next.materializeMarkLabelPlacement({ id });
246
326
  }
247
327
  );
248
328
 
@@ -255,6 +335,12 @@ const layoutLabels = action(
255
335
  validateOptionObject(args, TEXT_LABEL_LAYOUT_OPTIONS, "layoutLabels");
256
336
  const layer = requireCompleteText(this, args.target, "layoutLabels");
257
337
  const policy = normalizePolicy(args);
338
+ const placement = this.markConfigs[layer.id]?.labelAuthoring?.placement;
339
+ if (policy.leader !== false && placement?.leader !== false && placement?.leader !== undefined) {
340
+ throw new Error(
341
+ "Collision-layout leader conflicts with the active semantic label placement leader."
342
+ );
343
+ }
258
344
  const generatedId = leaderId(layer.id);
259
345
  const existingConfig = this.materializationConfigs.labelLayouts?.[layer.id];
260
346
  if (
@@ -272,6 +358,66 @@ const layoutLabels = action(
272
358
  }
273
359
  );
274
360
 
361
+ const editMarkLabelPlacement = action(
362
+ {
363
+ op: "editMarkLabelPlacement",
364
+ description: "Replace or reset semantic placement for one attached label layer."
365
+ },
366
+ function (args = {}) {
367
+ validateOptionObject(args, EDIT_PLACEMENT_OPTIONS, "editMarkLabelPlacement", {
368
+ allowEmpty: false,
369
+ emptyError: Error
370
+ });
371
+ if (!Object.hasOwn(args, "target") || !Object.hasOwn(args, "placement")) {
372
+ throw new Error("editMarkLabelPlacement requires target and placement.");
373
+ }
374
+ const id = validateUserId(args.target, "Attached label id");
375
+ const layer = findLayer(this, id);
376
+ if (!isSourceOwnedText(layer)) {
377
+ throw new Error(`Unknown attached label target "${id}".`);
378
+ }
379
+ let placement;
380
+ if (args.placement !== "auto") {
381
+ placement = validateSourceMarkLabelPlacement(this, layer.source, args.placement);
382
+ if (
383
+ placement.leader !== false &&
384
+ this.materializationConfigs.labelLayouts?.[id]?.leader !== false &&
385
+ this.materializationConfigs.labelLayouts?.[id]?.leader !== undefined
386
+ ) {
387
+ throw new Error(
388
+ "Semantic label placement leader conflicts with the active collision-layout leader."
389
+ );
390
+ }
391
+ const generatedId = markLabelPlacementLeaderId(id);
392
+ const previous = this.markConfigs[id]?.labelAuthoring?.placement;
393
+ if (
394
+ placement.leader !== false &&
395
+ this.graphicSpec.objects[generatedId] !== undefined &&
396
+ (previous?.leader === undefined || previous.leader === false)
397
+ ) {
398
+ throw new Error(`Mark label placement leader graphic "${generatedId}" already exists.`);
399
+ }
400
+ }
401
+ const config = this.markConfigs[id] ?? DEFAULT_TEXT_MARK;
402
+ const previousPlacement = config.labelAuthoring?.placement;
403
+ let base = this;
404
+ if (
405
+ previousPlacement?.leader !== undefined &&
406
+ previousPlacement.leader !== false &&
407
+ (placement === undefined || placement.leader === false)
408
+ ) {
409
+ base = removeLeaderGraphic(base, markLabelPlacementLeaderId(id));
410
+ }
411
+ const labelAuthoring = { ...config.labelAuthoring };
412
+ if (placement === undefined) delete labelAuthoring.placement;
413
+ else labelAuthoring.placement = placement;
414
+ const next = base._withMarkConfig(id, { ...config, labelAuthoring });
415
+ return canMaterializeText(next, layer)
416
+ ? next.rematerializeTextMark({ id })
417
+ : next.materializeMarkLabelPlacement({ id });
418
+ }
419
+ );
420
+
275
421
  const removeLabelLayout = action(
276
422
  {
277
423
  op: "removeLabelLayout",
@@ -292,6 +438,8 @@ export function registerTextLabelLayoutActions(ProgramClass) {
292
438
  Object.assign(ProgramClass.prototype, {
293
439
  layoutLabels,
294
440
  removeLabelLayout,
295
- materializeLabelLayout
441
+ materializeLabelLayout,
442
+ editMarkLabelPlacement,
443
+ materializeMarkLabelPlacement
296
444
  });
297
445
  }