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
@@ -10,6 +10,7 @@ import {
10
10
  import { numericExtent } from "../../../grammar/numeric.js";
11
11
  import {
12
12
  readNominalField,
13
+ readScaleField,
13
14
  readTemporalField
14
15
  } from "../../../grammar/scales/index.js";
15
16
  import { findSemanticScale } from "../../../selectors/scales.js";
@@ -52,9 +53,19 @@ function aggregateCellDefinitions(program, layer, dataset) {
52
53
  const offsetChannel = resolveBarOffsetChannel(layer);
53
54
  const offsetScale = program.resolvedScales[layer.encoding?.[offsetChannel]?.scale];
54
55
  const layout = resolveBarColorLayout(layer);
56
+ const stroke = layer.encoding?.stroke;
55
57
 
56
58
  function definition(cell, start, end) {
57
59
  const members = aggregateMembers(dataset.values, layer, cell, channels);
60
+ const strokeValues = stroke === undefined ? undefined : readScaleField(
61
+ members,
62
+ stroke.field,
63
+ stroke.fieldType,
64
+ { temporalUnit: stroke.temporalUnit }
65
+ );
66
+ if (strokeValues !== undefined && new Set(strokeValues).size !== 1) {
67
+ throw new Error("Bar stroke requires one value within each aggregate cell.");
68
+ }
58
69
  return {
59
70
  fields: uniqueFields(members),
60
71
  channels: {
@@ -62,6 +73,7 @@ function aggregateCellDefinitions(program, layer, dataset) {
62
73
  [channels.measure]: start,
63
74
  [`${channels.measure}2`]: end,
64
75
  ...(cell.color === undefined ? {} : { color: cell.color }),
76
+ ...(strokeValues === undefined ? {} : { stroke: strokeValues[0] }),
65
77
  ...(layer.encoding?.[offsetChannel] === undefined
66
78
  ? {}
67
79
  : { [offsetChannel]: cell.series ?? cell.color })
@@ -88,8 +100,18 @@ function histogramDefinitions(program, layer, dataset) {
88
100
  resolvedScales: program.resolvedScales
89
101
  });
90
102
  const colorScale = program.resolvedScales[colorEncoding?.scale];
103
+ const stroke = layer.encoding?.stroke;
91
104
  return segments.map(segment => {
92
105
  const colorValue = segment.colorValue ?? colorScale?.domain[segment.category];
106
+ const strokeValues = stroke === undefined ? undefined : readScaleField(
107
+ segment.members,
108
+ stroke.field,
109
+ stroke.fieldType,
110
+ { temporalUnit: stroke.temporalUnit }
111
+ );
112
+ if (strokeValues !== undefined && new Set(strokeValues).size !== 1) {
113
+ throw new Error("Histogram stroke requires one value within each bin/series cell.");
114
+ }
93
115
  return {
94
116
  fields: uniqueFields(segment.members),
95
117
  channels: {
@@ -97,7 +119,8 @@ function histogramDefinitions(program, layer, dataset) {
97
119
  x2: segment.end,
98
120
  y: segment.stackStart,
99
121
  y2: segment.stackEnd,
100
- ...(colorValue === undefined ? {} : { color: colorValue })
122
+ ...(colorValue === undefined ? {} : { color: colorValue }),
123
+ ...(strokeValues === undefined ? {} : { stroke: strokeValues[0] })
101
124
  },
102
125
  members: segment.members
103
126
  };
@@ -188,14 +211,18 @@ export function resolveBarItems(program, layer, dataset, selectionGrain) {
188
211
  if (definitions === undefined) {
189
212
  throw new Error(`Bar mark "${layer.id}" is incomplete for selection.`);
190
213
  }
214
+ const graphicTypes = Object.hasOwn(
215
+ program.markConfigs[layer.id]?.barAppearance ?? {},
216
+ "cornerRadius"
217
+ ) ? ["rect", "path"] : "rect";
191
218
  if (selectionGrain === "stack") {
192
219
  return finalizeItems(
193
220
  program,
194
221
  layer,
195
222
  "stack",
196
223
  barStackDefinitions(layer, grain, definitions),
197
- "rect"
224
+ graphicTypes
198
225
  );
199
226
  }
200
- return finalizeItems(program, layer, grain, definitions, "rect");
227
+ return finalizeItems(program, layer, grain, definitions, graphicTypes);
201
228
  }
@@ -6,6 +6,7 @@ import {
6
6
  } from "../../../grammar/markSelection.js";
7
7
  import { unionConcreteGraphicBounds } from
8
8
  "../../../grammar/schemas/graphicBounds.js";
9
+ import { readRectItemGeometry } from "../../../grammar/roundedRect.js";
9
10
 
10
11
  export function itemKey(layer, grain, index) {
11
12
  return `${layer.id}/${grain}/${index}`;
@@ -54,14 +55,15 @@ export function uniqueFields(rows) {
54
55
 
55
56
  function requireResolvedGraphic(program, layer, type) {
56
57
  const graphic = program.graphicSpec.objects[layer.id];
58
+ const types = Array.isArray(type) ? type : [type];
57
59
  const compatibleCollection = graphic?.type === "collection" &&
58
- graphic.items?.every(child => child.type === type);
60
+ graphic.items?.every(child => types.includes(child.type));
59
61
  if (
60
- (graphic?.type !== type && !compatibleCollection) ||
62
+ (!types.includes(graphic?.type) && !compatibleCollection) ||
61
63
  !Array.isArray(graphic.items)
62
64
  ) {
63
65
  throw new Error(
64
- `Mark "${layer.id}" requires a materialized ${type} collection for selection.`
66
+ `Mark "${layer.id}" requires a materialized ${types.join("/")} collection for selection.`
65
67
  );
66
68
  }
67
69
  return graphic;
@@ -78,6 +80,13 @@ export function concreteProperties(properties) {
78
80
  );
79
81
  }
80
82
 
83
+ function selectableGraphicProperties(item) {
84
+ return {
85
+ ...concreteProperties(item?.properties),
86
+ ...(readRectItemGeometry(item) ?? {})
87
+ };
88
+ }
89
+
81
90
  function sharedConcreteProperties(items) {
82
91
  if (items.length === 0) return {};
83
92
  const shared = concreteProperties(items[0].properties);
@@ -118,7 +127,10 @@ export function finalizeItems(program, layer, grain, definitions, graphicType) {
118
127
  const items = indices.map(childIndex => graphic.items[childIndex]);
119
128
  const properties = definition.properties ?? (
120
129
  items.length === 1
121
- ? concreteProperties(items[0].properties)
130
+ ? selectableGraphicProperties({
131
+ type: items[0].type ?? graphic.type,
132
+ properties: items[0].properties
133
+ })
122
134
  : {
123
135
  ...sharedConcreteProperties(items),
124
136
  ...collectionBounds(program, items)
@@ -131,6 +143,9 @@ export function finalizeItems(program, layer, grain, definitions, graphicType) {
131
143
  fields: cloneAndFreeze(definition.fields),
132
144
  channels: cloneAndFreeze(definition.channels),
133
145
  properties: cloneAndFreeze(properties),
146
+ ...(definition.geometry === undefined
147
+ ? {}
148
+ : { geometry: cloneAndFreeze(definition.geometry) }),
134
149
  members: Object.freeze([...definition.members]),
135
150
  graphicIds: Object.freeze(items.map((child, childOffset) =>
136
151
  child.id ?? graphicId(layer, indices[childOffset])
@@ -1,4 +1,5 @@
1
1
  import { rectUsesFields } from "../../../grammar/rects.js";
2
+ import { readRectItemGeometry } from "../../../grammar/roundedRect.js";
2
3
  import { resolveRectRows } from "../../rect.js";
3
4
  import {
4
5
  channelMapFromRow,
@@ -9,6 +10,17 @@ import {
9
10
  uniqueFields
10
11
  } from "./common.js";
11
12
 
13
+ function rectSelectionProperties(graphic, index) {
14
+ const item = graphic.items[index];
15
+ return {
16
+ ...concreteProperties(item?.properties),
17
+ ...(readRectItemGeometry({
18
+ type: item?.type ?? graphic.type,
19
+ properties: item?.properties
20
+ }) ?? {})
21
+ };
22
+ }
23
+
12
24
  function resolveGradientPlotItems(program, layer, dataset) {
13
25
  const graphic = program.graphicSpec.objects[layer.id];
14
26
  return finalizeItems(
@@ -19,7 +31,7 @@ function resolveGradientPlotItems(program, layer, dataset) {
19
31
  key: itemKey(layer, "rect", index),
20
32
  fields: ownFields(row),
21
33
  channels: channelMapFromRow(row, layer),
22
- properties: concreteProperties(graphic.items[index]?.properties),
34
+ properties: rectSelectionProperties(graphic, index),
23
35
  members: [row]
24
36
  })),
25
37
  "rect"
@@ -28,7 +40,13 @@ function resolveGradientPlotItems(program, layer, dataset) {
28
40
 
29
41
  export function resolveRectItems(program, layer, dataset) {
30
42
  const graphic = program.graphicSpec.objects[layer.id];
31
- if (graphic?.type !== "rect" || !Array.isArray(graphic.items)) {
43
+ const rounded = Object.hasOwn(program.markConfigs[layer.id] ?? {}, "cornerRadius");
44
+ const compatibleCollection = rounded && graphic?.type === "collection" &&
45
+ graphic.items?.every(item => ["rect", "path"].includes(item.type));
46
+ if (
47
+ (graphic?.type !== "rect" && !compatibleCollection) ||
48
+ !Array.isArray(graphic.items)
49
+ ) {
32
50
  throw new Error(`Rect mark "${layer.id}" is incomplete for selection.`);
33
51
  }
34
52
  if (program.markConfigs[layer.id]?.gradientPlot?.materialized === true) {
@@ -40,8 +58,14 @@ export function resolveRectItems(program, layer, dataset) {
40
58
  key: itemKey(layer, "rect", item.sourceIndex),
41
59
  fields: hasField ? ownFields(item.row) : uniqueFields(dataset.values),
42
60
  channels: channelMapFromRow(item.row, layer),
43
- properties: concreteProperties(graphic.items[graphicIndex]?.properties),
61
+ properties: rectSelectionProperties(graphic, graphicIndex),
44
62
  members: hasField ? [item.row] : dataset.values
45
63
  }));
46
- return finalizeItems(program, layer, "rect", definitions, "rect");
64
+ return finalizeItems(
65
+ program,
66
+ layer,
67
+ "rect",
68
+ definitions,
69
+ rounded ? ["rect", "path"] : "rect"
70
+ );
47
71
  }
@@ -10,7 +10,7 @@ function translateCommand(command, offset) {
10
10
  }
11
11
 
12
12
  export function transformPathHighlightProperties(properties, style) {
13
- const { offset, ...appearance } = style;
13
+ const { offset = { x: 0, y: 0 }, ...appearance } = style;
14
14
  return {
15
15
  ...properties,
16
16
  ...appearance,
@@ -1,5 +1,6 @@
1
1
  import { createPointShapeGraphic } from "../../grammar/pointShapes.js";
2
2
  import { finiteMidpoint } from "../../grammar/numeric.js";
3
+ import { requestedStrokeDetails } from "../../grammar/strokeStyle.js";
3
4
 
4
5
  function pointCenterAndArea(child) {
5
6
  const properties = child.properties;
@@ -75,7 +76,8 @@ export function transformPointHighlightChild(child, style) {
75
76
  ? (child.properties.opacity === undefined ? {} : { opacity: child.properties.opacity })
76
77
  : { opacity: style.opacity }),
77
78
  ...(style.stroke === undefined ? {} : { stroke: style.stroke }),
78
- ...(style.strokeWidth === undefined ? {} : { strokeWidth: style.strokeWidth })
79
+ ...(style.strokeWidth === undefined ? {} : { strokeWidth: style.strokeWidth }),
80
+ ...requestedStrokeDetails(child.properties, "Point highlight source")
79
81
  };
80
82
  if (style.shape !== undefined) {
81
83
  return createPointShapeGraphic({
@@ -1,11 +1,21 @@
1
1
  import { resolveBarChannels } from "../grammar/bars/policy.js";
2
+ import {
3
+ assertMarkLabelPlacementSupport,
4
+ normalizeMarkLabelPlacement,
5
+ resolveMarkLabelPlacement
6
+ } from "../layout/labels.js";
2
7
  import { resolveMarkLabelValues } from "../grammar/markLabels.js";
3
8
  import { formatTextValue } from "../grammar/text.js";
4
9
  import { normalizePositionDatum } from "../grammar/positionDatum.js";
5
10
  import { mapOrdinalPositionValues } from "../grammar/scales/index.js";
6
11
  import { findDataset } from "../selectors/datasets.js";
12
+ import { findCoordinate } from "../selectors/coordinates.js";
7
13
  import { findLayer } from "../selectors/layers.js";
14
+ import { unionConcreteGraphicBounds } from
15
+ "../grammar/schemas/graphicBounds.js";
16
+ import { polarDirection } from "../grammar/polar.js";
8
17
  import { resolveMarkItems } from "./selection/policies/index.js";
18
+ import { resolveMarkSelection } from "./selection/state.js";
9
19
  import { mapScaleConsumerValues } from "./scales/map.js";
10
20
  import { resolveRowEncodingValues } from "./rowEncoding.js";
11
21
 
@@ -83,6 +93,172 @@ function sourceAnchor(program, source, item) {
83
93
  return { x: item.properties.x, y: item.properties.y };
84
94
  }
85
95
 
96
+ function explicitIntervalAxis(source) {
97
+ const axes = ["x", "y"].filter(axis =>
98
+ source.encoding?.[axis] !== undefined &&
99
+ source.encoding?.[`${axis}2`] !== undefined
100
+ );
101
+ return axes.length === 1 ? axes[0] : undefined;
102
+ }
103
+
104
+ function sourcePlacementContext(program, source) {
105
+ const coordinateType = findCoordinate(program, source.coordinate)?.type;
106
+ return {
107
+ markType: source.mark.type,
108
+ coordinateType,
109
+ intervalAxis: source.mark.type === "bar"
110
+ ? resolveBarChannels(source).measure
111
+ : source.mark.type === "rect"
112
+ ? explicitIntervalAxis(source)
113
+ : undefined
114
+ };
115
+ }
116
+
117
+ export function validateSourceMarkLabelPlacement(program, sourceId, value) {
118
+ const source = findLayer(program, sourceId);
119
+ if (source === undefined) {
120
+ throw new Error(`Unknown mark label placement source "${sourceId}".`);
121
+ }
122
+ const placement = normalizeMarkLabelPlacement(value);
123
+ assertMarkLabelPlacementSupport(sourcePlacementContext(program, source), placement);
124
+ return placement;
125
+ }
126
+
127
+ function rectangleBounds(item) {
128
+ const { x, y, width, height } = item.properties;
129
+ if (![x, y, width, height].every(Number.isFinite)) {
130
+ throw new TypeError("Interval mark label placement requires finite rectangle geometry.");
131
+ }
132
+ return {
133
+ left: Math.min(x, x + width),
134
+ right: Math.max(x, x + width),
135
+ top: Math.min(y, y + height),
136
+ bottom: Math.max(y, y + height)
137
+ };
138
+ }
139
+
140
+ function mappedChannel(program, source, item, channel) {
141
+ const value = item.channels[channel];
142
+ const primary = channel === "x2" ? "x" : channel === "y2" ? "y" : channel;
143
+ const scale = program.resolvedScales[
144
+ source.encoding?.[channel]?.scale ?? source.encoding?.[primary]?.scale
145
+ ];
146
+ return value === undefined || scale === undefined
147
+ ? undefined
148
+ : mapScaleConsumerValues([value], scale, channel)[0];
149
+ }
150
+
151
+ function scaleIncreaseDirection(program, source, axis) {
152
+ const scale = program.resolvedScales[source.encoding?.[axis]?.scale];
153
+ const domain = scale?.domain;
154
+ if (!Array.isArray(domain) || domain.length < 2) {
155
+ return axis === "x" ? { x: 1, y: 0 } : { x: 0, y: -1 };
156
+ }
157
+ const mapped = mapScaleConsumerValues(
158
+ [domain[0], domain.at(-1)],
159
+ scale,
160
+ axis
161
+ );
162
+ const delta = mapped[1] - mapped[0];
163
+ if (!Number.isFinite(delta) || delta === 0) {
164
+ return axis === "x" ? { x: 1, y: 0 } : { x: 0, y: -1 };
165
+ }
166
+ return axis === "x"
167
+ ? { x: Math.sign(delta), y: 0 }
168
+ : { x: 0, y: Math.sign(delta) };
169
+ }
170
+
171
+ function intervalPlacementGeometry(program, source, item, axis) {
172
+ const bounds = rectangleBounds(item);
173
+ const primary = mappedChannel(program, source, item, axis);
174
+ const secondary = mappedChannel(program, source, item, `${axis}2`);
175
+ if (![primary, secondary].every(Number.isFinite)) {
176
+ throw new TypeError(
177
+ `${source.mark.type === "bar" ? "Bar" : "Rect"} semantic label placement requires resolved ${axis}/${axis}2 endpoints.`
178
+ );
179
+ }
180
+ const cross = axis === "x"
181
+ ? (bounds.top + bounds.bottom) / 2
182
+ : (bounds.left + bounds.right) / 2;
183
+ const start = axis === "x" ? { x: primary, y: cross } : { x: cross, y: primary };
184
+ const end = axis === "x" ? { x: secondary, y: cross } : { x: cross, y: secondary };
185
+ const delta = { x: end.x - start.x, y: end.y - start.y };
186
+ const length = Math.hypot(delta.x, delta.y);
187
+ const endOutward = length > 0
188
+ ? { x: delta.x / length, y: delta.y / length }
189
+ : scaleIncreaseDirection(program, source, axis);
190
+ return {
191
+ kind: "interval",
192
+ center: {
193
+ x: (bounds.left + bounds.right) / 2,
194
+ y: (bounds.top + bounds.bottom) / 2
195
+ },
196
+ start: {
197
+ point: start,
198
+ outward: { x: -endOutward.x, y: -endOutward.y }
199
+ },
200
+ end: { point: end, outward: endOutward },
201
+ bounds
202
+ };
203
+ }
204
+
205
+ function pointPlacementGeometry(program, source, item) {
206
+ const center = { x: item.properties.x, y: item.properties.y };
207
+ const coordinateType = findCoordinate(program, source.coordinate)?.type;
208
+ if (coordinateType !== "polar") return { kind: "point", center };
209
+ const thetaScale = program.resolvedScales[source.encoding?.theta?.scale];
210
+ const theta = item.channels.theta === undefined || thetaScale === undefined
211
+ ? undefined
212
+ : mapScaleConsumerValues([item.channels.theta], thetaScale, "theta")[0];
213
+ const outward = Number.isFinite(theta) ? polarDirection(theta) : { x: 0, y: -1 };
214
+ const objectBounds = unionConcreteGraphicBounds(
215
+ program.graphicSpec,
216
+ item.graphicIds
217
+ );
218
+ const corners = objectBounds === undefined ? [] : [
219
+ { x: objectBounds.left, y: objectBounds.top },
220
+ { x: objectBounds.right, y: objectBounds.top },
221
+ { x: objectBounds.right, y: objectBounds.bottom },
222
+ { x: objectBounds.left, y: objectBounds.bottom }
223
+ ];
224
+ const support = Math.max(0, ...corners.map(point =>
225
+ (point.x - center.x) * outward.x + (point.y - center.y) * outward.y
226
+ ));
227
+ return {
228
+ kind: "point",
229
+ center,
230
+ end: {
231
+ point: {
232
+ x: center.x + outward.x * support,
233
+ y: center.y + outward.y * support
234
+ },
235
+ outward
236
+ }
237
+ };
238
+ }
239
+
240
+ function sourcePlacementGeometry(program, source, item) {
241
+ if (source.mark.type === "bar") {
242
+ return intervalPlacementGeometry(
243
+ program,
244
+ source,
245
+ item,
246
+ resolveBarChannels(source).measure
247
+ );
248
+ }
249
+ if (source.mark.type === "rect") {
250
+ const axis = explicitIntervalAxis(source);
251
+ return axis === undefined
252
+ ? { kind: "point", center: sourceAnchor(program, source, item) }
253
+ : intervalPlacementGeometry(program, source, item, axis);
254
+ }
255
+ if (source.mark.type === "arc") return item.geometry;
256
+ if (source.mark.type === "point") {
257
+ return pointPlacementGeometry(program, source, item);
258
+ }
259
+ return { kind: "point", center: sourceAnchor(program, source, item) };
260
+ }
261
+
86
262
  function contentValue(encoding, rowOrItem, source) {
87
263
  return Object.hasOwn(encoding, "field")
88
264
  ? source === undefined
@@ -91,8 +267,7 @@ function contentValue(encoding, rowOrItem, source) {
91
267
  : encoding.datum;
92
268
  }
93
269
 
94
- function concreteItem(config, position, value, format) {
95
- const text = formatTextValue(value, format);
270
+ function concreteTextItem(config, position, text, { offsets = true } = {}) {
96
271
  const x = position.x;
97
272
  const y = position.y;
98
273
  if (text === undefined || !Number.isFinite(x) || !Number.isFinite(y)) {
@@ -101,8 +276,8 @@ function concreteItem(config, position, value, format) {
101
276
  return {
102
277
  type: "text",
103
278
  properties: {
104
- x: x + config.dx,
105
- y: y + config.dy,
279
+ x: x + (offsets ? config.dx : 0),
280
+ y: y + (offsets ? config.dy : 0),
106
281
  text,
107
282
  fill: config.fill,
108
283
  opacity: config.opacity,
@@ -116,6 +291,10 @@ function concreteItem(config, position, value, format) {
116
291
  };
117
292
  }
118
293
 
294
+ function concreteItem(config, position, value, format) {
295
+ return concreteTextItem(config, position, formatTextValue(value, format));
296
+ }
297
+
119
298
  function relativeLuminance(color) {
120
299
  if (typeof color !== "string" || !/^#[0-9a-f]{6}$/i.test(color)) {
121
300
  return undefined;
@@ -147,18 +326,90 @@ function resolveSourceTextItems(program, layer, config) {
147
326
  if (source === undefined) {
148
327
  throw new Error(`Text mark "${layer.id}" requires source layer "${layer.source}".`);
149
328
  }
150
- const items = resolveMarkItems(program, source.id);
151
- const values = layer.encoding.text.content === undefined ? undefined
152
- : resolveMarkLabelValues(source, items, layer.encoding.text);
153
- return items.flatMap((item, index) => {
154
- const anchor = sourceAnchor(program, source, item);
155
- const concrete = concreteItem(
156
- sourceTextConfig(config, source, item),
157
- anchor,
158
- values === undefined ? contentValue(layer.encoding.text, item, source) : values[index],
329
+ const requested = program.markConfigs[layer.id]?.labelAuthoring?.selection ?? {
330
+ kind: "all"
331
+ };
332
+ let allItems;
333
+ let items;
334
+ if (requested.kind === "all") {
335
+ allItems = resolveMarkItems(program, source.id);
336
+ items = allItems;
337
+ } else if (requested.kind === "inline") {
338
+ const resolved = resolveMarkSelection(program, source.id, requested.selector);
339
+ allItems = resolved.items;
340
+ const selected = new Set(resolved.keys);
341
+ items = allItems.filter(item => selected.has(item.key));
342
+ } else if (requested.kind === "named") {
343
+ const definition = program.materializationConfigs.selections?.[requested.id];
344
+ if (definition === undefined) {
345
+ throw new Error(`Unknown label selection "${requested.id}".`);
346
+ }
347
+ if (definition.target !== source.id) {
348
+ throw new Error(
349
+ `Label selection "${requested.id}" must target source mark "${source.id}".`
350
+ );
351
+ }
352
+ const resolved = resolveMarkSelection(program, source.id, definition.selector);
353
+ allItems = resolved.items;
354
+ const selected = new Set(resolved.keys);
355
+ items = allItems.filter(item => selected.has(item.key));
356
+ } else {
357
+ throw new Error(`Unknown label selection kind "${requested.kind}".`);
358
+ }
359
+ const resolvedValues = layer.encoding.text.content === undefined ? undefined
360
+ : resolveMarkLabelValues(source, allItems, layer.encoding.text);
361
+ const values = resolvedValues === undefined ? undefined : new Map(
362
+ allItems.map((item, index) => [item.key, resolvedValues[index]])
363
+ );
364
+ return items.flatMap(item => {
365
+ let resolvedConfig = sourceTextConfig(config, source, item);
366
+ const text = formatTextValue(
367
+ values === undefined
368
+ ? contentValue(layer.encoding.text, item, source)
369
+ : values.get(item.key),
159
370
  layer.encoding.text.format
160
371
  );
161
- return concrete === undefined ? [] : [{ graphic: concrete, anchor }];
372
+ if (text === undefined) return [];
373
+ const placement = program.markConfigs[layer.id]?.labelAuthoring?.placement;
374
+ if (placement === undefined) {
375
+ const anchor = sourceAnchor(program, source, item);
376
+ const concrete = concreteTextItem(resolvedConfig, anchor, text);
377
+ return concrete === undefined ? [] : [{ graphic: concrete, anchor }];
378
+ }
379
+ assertMarkLabelPlacementSupport(sourcePlacementContext(program, source), placement);
380
+ const resolved = resolveMarkLabelPlacement({
381
+ placement,
382
+ geometry: sourcePlacementGeometry(program, source, item),
383
+ text: {
384
+ text,
385
+ fontSize: resolvedConfig.fontSize,
386
+ fontFamily: resolvedConfig.fontFamily,
387
+ fontWeight: resolvedConfig.fontWeight,
388
+ textAlign: resolvedConfig.align,
389
+ textBaseline: resolvedConfig.baseline,
390
+ rotation: resolvedConfig.rotation,
391
+ dx: resolvedConfig.dx,
392
+ dy: resolvedConfig.dy
393
+ }
394
+ });
395
+ if (!resolved.visible) return [];
396
+ if (
397
+ resolved.anchor.startsWith("outside") &&
398
+ config.fillExplicit !== true
399
+ ) {
400
+ resolvedConfig = config;
401
+ }
402
+ const anchor = { x: resolved.sourceX, y: resolved.sourceY };
403
+ return [{
404
+ graphic: concreteTextItem(
405
+ resolvedConfig,
406
+ { x: resolved.x, y: resolved.y },
407
+ text,
408
+ { offsets: false }
409
+ ),
410
+ anchor,
411
+ placement: resolved
412
+ }];
162
413
  });
163
414
  }
164
415
 
@@ -1,6 +1,7 @@
1
1
  import { requireFiniteProperty } from "./validation.js";
2
2
  import { validateConcreteGraphicProperties } from
3
3
  "../../grammar/schemas/concreteGraphic.js";
4
+ import { applyCanvasStroke } from "./stroke.js";
4
5
 
5
6
  function drawCircle(context, child, collectionId) {
6
7
  const properties = child.properties ?? {};
@@ -43,9 +44,7 @@ function drawCircle(context, child, collectionId) {
43
44
  `Graphic "${graphicId}" requires a non-negative strokeWidth.`
44
45
  );
45
46
  }
46
- context.strokeStyle = properties.stroke;
47
- context.lineWidth = strokeWidth;
48
- context.setLineDash([]);
47
+ applyCanvasStroke(context, properties);
49
48
  context.stroke();
50
49
  }
51
50
  }
@@ -1,6 +1,7 @@
1
1
  import { requireFiniteProperty } from "./validation.js";
2
2
  import { validateConcreteGraphicProperties } from
3
3
  "../../grammar/schemas/concreteGraphic.js";
4
+ import { applyCanvasStroke } from "./stroke.js";
4
5
 
5
6
  function drawLine(context, child, collectionId) {
6
7
  const properties = child.properties ?? {};
@@ -32,10 +33,8 @@ function drawLine(context, child, collectionId) {
32
33
  throw new Error(`Graphic "${graphicId}" requires opacity from 0 to 1.`);
33
34
  }
34
35
 
35
- context.strokeStyle = properties.stroke;
36
- context.lineWidth = strokeWidth;
37
36
  context.globalAlpha = opacity;
38
- context.setLineDash(properties.strokeDash ?? []);
37
+ applyCanvasStroke(context, properties, properties.strokeDash ?? []);
39
38
  context.beginPath();
40
39
  context.moveTo(x1, y1);
41
40
  context.lineTo(x2, y2);
@@ -9,10 +9,10 @@ import { validateConcreteGraphicProperties } from
9
9
  const MAX_NATIVE_VALUE = 16_777_216;
10
10
  const NATIVE_PROPERTIES = {
11
11
  canvas: ["x", "y", "width", "height"],
12
- circle: ["x", "y", "radius", "strokeWidth"],
13
- rect: ["x", "y", "width", "height", "strokeWidth"],
14
- line: ["x1", "y1", "x2", "y2", "strokeWidth"],
15
- path: ["strokeWidth"],
12
+ circle: ["x", "y", "radius", "strokeWidth", "miterLimit"],
13
+ rect: ["x", "y", "width", "height", "strokeWidth", "miterLimit"],
14
+ line: ["x1", "y1", "x2", "y2", "strokeWidth", "miterLimit"],
15
+ path: ["strokeWidth", "miterLimit"],
16
16
  text: ["x", "y", "fontSize"]
17
17
  };
18
18
  const COMMAND_PROPERTIES = ["x", "y", "x1", "y1", "x2", "y2"];
@@ -3,6 +3,7 @@ import { validateConcreteGraphicProperties } from
3
3
  "../../grammar/schemas/concreteGraphic.js";
4
4
  import { resolvePathCommandBounds } from "../../grammar/schemas/graphicBounds.js";
5
5
  import { applyCanvasFill } from "./fill.js";
6
+ import { applyCanvasStroke } from "./stroke.js";
6
7
 
7
8
  function drawPath(context, child, collectionId) {
8
9
  const properties = child.properties ?? {};
@@ -82,9 +83,7 @@ function drawPath(context, child, collectionId) {
82
83
  context.fill();
83
84
  }
84
85
  if (hasStroke) {
85
- context.strokeStyle = properties.stroke;
86
- context.lineWidth = strokeWidth;
87
- context.setLineDash(strokeDash);
86
+ applyCanvasStroke(context, properties, strokeDash);
88
87
  context.stroke();
89
88
  }
90
89
  }
@@ -2,6 +2,7 @@ import { requireFiniteProperty } from "./validation.js";
2
2
  import { validateConcreteGraphicProperties } from
3
3
  "../../grammar/schemas/concreteGraphic.js";
4
4
  import { applyCanvasFill } from "./fill.js";
5
+ import { applyCanvasStroke } from "./stroke.js";
5
6
 
6
7
  function drawRect(context, child, collectionId) {
7
8
  const properties = child.properties ?? {};
@@ -42,9 +43,7 @@ function drawRect(context, child, collectionId) {
42
43
  bottom: y + height
43
44
  }, graphicId);
44
45
  context.fillRect(x, y, width, height);
45
- context.strokeStyle = properties.stroke;
46
- context.lineWidth = strokeWidth;
47
- context.setLineDash([]);
46
+ applyCanvasStroke(context, properties);
48
47
  context.beginPath();
49
48
  context.moveTo(x, y);
50
49
  context.lineTo(x + width, y);
@@ -0,0 +1,11 @@
1
+ import { resolveStrokeDetails } from "../../grammar/strokeStyle.js";
2
+
3
+ export function applyCanvasStroke(context, properties, strokeDash = []) {
4
+ const details = resolveStrokeDetails(properties);
5
+ context.strokeStyle = properties.stroke;
6
+ context.lineWidth = properties.strokeWidth;
7
+ context.lineCap = details.lineCap;
8
+ context.lineJoin = details.lineJoin;
9
+ context.miterLimit = details.miterLimit;
10
+ context.setLineDash(strokeDash);
11
+ }