ggaction 0.0.23 → 0.0.26

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 (228) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +2 -2
  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/knowledge/task-resolver.js +1 -1
  7. package/package.json +1 -1
  8. package/src/accessibility.js +13 -0
  9. package/src/actions/canvas/actions.js +2 -1
  10. package/src/actions/canvas/fitting.js +68 -13
  11. package/src/actions/charts/beeswarm.js +2 -1
  12. package/src/actions/charts/ecdf.js +2 -1
  13. package/src/actions/charts/endpoints.js +25 -16
  14. package/src/actions/charts/heatmap.js +6 -4
  15. package/src/actions/charts/missing.js +17 -0
  16. package/src/actions/charts/radar.js +70 -33
  17. package/src/actions/charts/raincloud.js +18 -4
  18. package/src/actions/charts/rug-strip.js +2 -2
  19. package/src/actions/charts/shared.js +17 -0
  20. package/src/actions/composition/actions.js +24 -7
  21. package/src/actions/composition/adjacent.js +90 -0
  22. package/src/actions/composition/index.js +11 -1
  23. package/src/actions/composition/layer.js +42 -0
  24. package/src/actions/composition/layerPlan.js +176 -0
  25. package/src/actions/composition/targets.js +105 -0
  26. package/src/actions/data/basicBin2d.js +1 -1
  27. package/src/actions/data/bin2d.js +2 -2
  28. package/src/actions/data/derived.js +3 -0
  29. package/src/actions/data/filter.js +1 -1
  30. package/src/actions/data/raincloudFilter.js +3 -1
  31. package/src/actions/encodings/appearance.js +26 -7
  32. package/src/actions/encodings/channels.js +1 -0
  33. package/src/actions/encodings/color/continuous.js +37 -34
  34. package/src/actions/encodings/color/index.js +57 -43
  35. package/src/actions/encodings/color/policy.js +15 -0
  36. package/src/actions/encodings/density/resolve.js +3 -2
  37. package/src/actions/encodings/density.js +2 -2
  38. package/src/actions/encodings/offset.js +2 -0
  39. package/src/actions/encodings/pathOrder.js +2 -0
  40. package/src/actions/encodings/position/areaAlignment.js +42 -0
  41. package/src/actions/encodings/position/index.js +37 -3
  42. package/src/actions/encodings/position/policies/line.js +1 -1
  43. package/src/actions/encodings/ranged.js +4 -2
  44. package/src/actions/encodings/remove.js +13 -10
  45. package/src/actions/encodings/ruleAppearance.js +14 -2
  46. package/src/actions/facets/actions.js +1 -1
  47. package/src/actions/guides/axes/axes.js +2 -1
  48. package/src/actions/guides/axes/components.js +5 -0
  49. package/src/actions/guides/axes/edit.js +1 -188
  50. package/src/actions/guides/axes/labels.js +2 -537
  51. package/src/actions/guides/axes/parallel/lifecycle.js +11 -4
  52. package/src/actions/guides/axes/parallel/policy.js +5 -2
  53. package/src/actions/guides/axes/parallel/resolve.js +3 -1
  54. package/src/actions/guides/axes/parallel.js +9 -4
  55. package/src/actions/guides/axes/tickGroups.js +16 -1
  56. package/src/actions/guides/axes/ticks.js +33 -24
  57. package/src/actions/guides/axes/titles.js +1 -389
  58. package/src/actions/guides/facadeLegend.js +3 -2
  59. package/src/actions/guides/grids/grid.js +3 -2
  60. package/src/actions/guides/grids/resolve.js +2 -1
  61. package/src/actions/guides/guides.js +3 -2
  62. package/src/actions/guides/legends/automatic.js +28 -0
  63. package/src/actions/guides/legends/blocks.js +15 -21
  64. package/src/actions/guides/legends/categorical/actions.js +1 -1
  65. package/src/actions/guides/legends/categorical/components.js +4 -3
  66. package/src/actions/guides/legends/categorical/layout.js +5 -2
  67. package/src/actions/guides/legends/categorical/options.js +9 -14
  68. package/src/actions/guides/legends/categorical/recipes.js +3 -0
  69. package/src/actions/guides/legends/categorical/resolve.js +7 -5
  70. package/src/actions/guides/legends/colorSize.js +51 -14
  71. package/src/actions/guides/legends/continuous/common.js +1 -1
  72. package/src/actions/guides/legends/continuous/gradient.js +1 -1
  73. package/src/actions/guides/legends/continuous/opacity.js +2 -0
  74. package/src/actions/guides/legends/edit.js +1 -1
  75. package/src/actions/guides/legends/focused.js +2 -2
  76. package/src/actions/guides/legends/group.js +1 -1
  77. package/src/actions/guides/legends/lane.js +1 -1
  78. package/src/actions/guides/legends/lifecycle.js +2 -2
  79. package/src/actions/guides/legends/remove.js +18 -4
  80. package/src/actions/guides/legends/size.js +1 -1
  81. package/src/actions/guides/legends/transition.js +15 -6
  82. package/src/actions/guides/polar/axes/facade.js +4 -2
  83. package/src/actions/guides/polar/axes/labels.js +6 -1
  84. package/src/actions/guides/polar/axes/shared.js +4 -4
  85. package/src/actions/guides/polar/axes/titles.js +13 -2
  86. package/src/actions/guides/polar/grids.js +6 -3
  87. package/src/actions/guides/tickValues.js +7 -1
  88. package/src/actions/index.js +2 -0
  89. package/src/actions/marks/area/actions.js +9 -2
  90. package/src/actions/marks/area/materialize.js +1 -1
  91. package/src/actions/marks/bar/materialize.js +4 -4
  92. package/src/actions/marks/lifecycle.js +14 -0
  93. package/src/actions/marks/line/actions.js +3 -0
  94. package/src/actions/marks/line/materialize.js +4 -2
  95. package/src/actions/marks/point/edit.js +2 -0
  96. package/src/actions/marks/point/jitter.js +2 -0
  97. package/src/actions/marks/point/materialize.js +5 -4
  98. package/src/actions/marks/point/packing.js +2 -0
  99. package/src/actions/marks/references.js +5 -3
  100. package/src/actions/marks/remove.js +16 -0
  101. package/src/actions/marks/shared.js +2 -1
  102. package/src/actions/marks/text/actions.js +5 -2
  103. package/src/actions/overlays/index.js +168 -0
  104. package/src/actions/overlays/snapshot.js +45 -0
  105. package/src/actions/primitives/editGraphics.js +21 -27
  106. package/src/actions/primitives/semanticAction.js +4 -5
  107. package/src/actions/primitives/semanticValidation/guide.js +1 -0
  108. package/src/actions/primitives/semanticValidation/layer.js +7 -0
  109. package/src/actions/scales/consumers/common.js +10 -0
  110. package/src/actions/scales/consumers/families.js +1 -1
  111. package/src/actions/scales/preview.js +1 -1
  112. package/src/actions/selection/actions.js +63 -11
  113. package/src/actions/titles/actions.js +3 -14
  114. package/src/actions/titles/resolve.js +23 -10
  115. package/src/core/action.js +41 -25
  116. package/src/core/compositionScales.js +12 -0
  117. package/src/core/compositionState.js +20 -3
  118. package/src/core/facetGap.js +44 -0
  119. package/src/core/immutable.js +11 -6
  120. package/src/core/programState.js +16 -0
  121. package/src/core/resourceReferences.js +19 -3
  122. package/src/core/validation.js +7 -0
  123. package/src/core/vocabulary.js +8 -2
  124. package/src/grammar/actionTargets.js +24 -0
  125. package/src/grammar/axisTextPlacement.js +1 -1
  126. package/src/grammar/bin2d.js +41 -4
  127. package/src/grammar/colorSizeLegend.js +56 -5
  128. package/src/grammar/displayLabels.js +10 -0
  129. package/src/grammar/facetHeaders.js +1 -1
  130. package/src/grammar/facets/dependencies.js +10 -3
  131. package/src/grammar/facets/guides.js +2 -1
  132. package/src/grammar/facets/legendIntent.js +7 -5
  133. package/src/grammar/legendBlocks.js +1 -1
  134. package/src/grammar/legendCombinations.js +1 -1
  135. package/src/grammar/legendGroups.js +1 -1
  136. package/src/grammar/legendItemsOffset.js +12 -0
  137. package/src/grammar/legendOverflow.js +25 -0
  138. package/src/grammar/lineSeries.js +1 -1
  139. package/src/grammar/markSelection.js +23 -0
  140. package/src/grammar/overlay.js +51 -0
  141. package/src/grammar/parallelCoordinates.js +1 -208
  142. package/src/grammar/pathCommands.js +8 -2
  143. package/src/grammar/pathSeries.js +1 -1
  144. package/src/grammar/plotComposition.js +152 -0
  145. package/src/grammar/pointShapes.js +18 -0
  146. package/src/grammar/scales/transformed.js +1 -1
  147. package/src/grammar/schemas/concreteGraphic.js +8 -1
  148. package/src/grammar/schemas/graphicBounds.js +8 -1
  149. package/src/grammar/schemas/graphicTree.js +18 -2
  150. package/src/grammar/schemas/semanticPath.js +3 -0
  151. package/src/grammar/strokeStyle.js +14 -8
  152. package/src/grammar/text.js +72 -0
  153. package/src/inspection.js +1 -1
  154. package/src/inspectionDescriptors.js +1 -1
  155. package/src/layout/composition.js +32 -10
  156. package/src/layout/compositionLegend.js +91 -0
  157. package/src/layout/facetContentFlow.js +15 -7
  158. package/src/layout/facetHeaderTitles.js +1 -1
  159. package/src/layout/facets.js +1 -1
  160. package/src/layout/labels.js +55 -27
  161. package/src/layout/legend.js +3 -2
  162. package/src/layout/legendGroup.js +1 -1
  163. package/src/layout/legendItems.js +7 -6
  164. package/src/layout/legendLane.js +1 -1
  165. package/src/layout/legendText.js +1 -1
  166. package/src/layout/title.js +1 -1
  167. package/src/materialization/adjacentSource.js +27 -0
  168. package/src/materialization/composition.js +165 -52
  169. package/src/materialization/compositionLegends.js +120 -0
  170. package/src/materialization/compositionScales.js +125 -0
  171. package/src/materialization/compositionTitle.js +96 -0
  172. package/src/materialization/facetGuides/edit.js +22 -2
  173. package/src/materialization/facetGuides/layout.js +1 -1
  174. package/src/materialization/facetGuides/owner.js +4 -3
  175. package/src/materialization/facetGuides/ownerLayout.js +12 -6
  176. package/src/materialization/facetGuides/placement.js +19 -22
  177. package/src/materialization/facetGuides/preparation.js +17 -5
  178. package/src/materialization/facetHeaders.js +5 -1
  179. package/src/materialization/facets.js +1 -1
  180. package/src/materialization/guides/layout.js +1 -1
  181. package/src/materialization/guides/resources.js +1 -1
  182. package/src/materialization/guides/visibility.js +105 -0
  183. package/src/materialization/legendClip.js +1 -1
  184. package/src/materialization/legendGroups.js +1 -1
  185. package/src/materialization/legendItemsOffset.js +68 -0
  186. package/src/materialization/legendSuppression.js +22 -0
  187. package/src/materialization/legends.js +3 -2
  188. package/src/materialization/marks/capabilities.js +4 -0
  189. package/src/materialization/marks/index.js +5 -1
  190. package/src/materialization/marks/pathOrder.js +1 -1
  191. package/src/materialization/marks/policies.js +2 -1
  192. package/src/materialization/overlayItems.js +154 -0
  193. package/src/materialization/planner.js +19 -4
  194. package/src/materialization/scales/policies/binnedPosition.js +43 -5
  195. package/src/materialization/scales/policies/series.js +12 -4
  196. package/src/materialization/scales/resolve.js +2 -0
  197. package/src/materialization/selection/items/bar.js +21 -3
  198. package/src/materialization/selection/items/common.js +18 -5
  199. package/src/materialization/selection/items/overlay.js +15 -0
  200. package/src/materialization/selection/items/path.js +1 -0
  201. package/src/materialization/selection/items/point.js +2 -3
  202. package/src/materialization/selection/items/tick.js +3 -12
  203. package/src/materialization/selection/path.js +2 -2
  204. package/src/materialization/selection/policies/bar.js +2 -0
  205. package/src/materialization/selection/policies/index.js +35 -1
  206. package/src/materialization/selection/policies/point.js +2 -0
  207. package/src/materialization/selection/policies/text.js +17 -0
  208. package/src/materialization/selection/rows.js +103 -0
  209. package/src/materialization/selection/state.js +19 -3
  210. package/src/materialization/selection/styles.js +8 -0
  211. package/src/materialization/text.js +6 -489
  212. package/src/materialization/textGeometry.js +503 -0
  213. package/src/materialization/textLines.js +20 -0
  214. package/src/persistence/validation.js +116 -25
  215. package/src/renderers/canvas/index.js +12 -7
  216. package/src/renderers/canvas/text.js +5 -5
  217. package/src/renderers/nativeBackend.js +25 -0
  218. package/src/renderers/pdf.js +3 -3
  219. package/src/renderers/png.js +4 -3
  220. package/src/renderers/svgSerialization.js +1 -643
  221. package/src/scene.js +5 -3
  222. package/src/selectors/plotComposition.js +115 -0
  223. package/src/selectors/selectionTargets.js +19 -0
  224. package/src/version.js +1 -1
  225. package/types/basic.d.ts +4 -2
  226. package/types/index.d.ts +14 -0
  227. package/types/inspection.d.ts +34 -0
  228. package/types/program.d.ts +572 -372
@@ -19,6 +19,7 @@ import { deriveHistogramSegments } from "../../bars/histogram.js";
19
19
  import {
20
20
  channelMapFromRow,
21
21
  finalizeItems,
22
+ reorderHighlightedItems,
22
23
  itemKey,
23
24
  ownFields,
24
25
  uniqueFields
@@ -203,7 +204,7 @@ function barStackDefinitions(layer, barGrain, definitions) {
203
204
  });
204
205
  }
205
206
 
206
- export function resolveBarItems(program, layer, dataset, selectionGrain) {
207
+ export function resolveBarItemDefinitions(program, layer, dataset) {
207
208
  const grain = resolveBarGrain(layer);
208
209
  const definitions = grain === BAR_GRAINS.histogram
209
210
  ? histogramDefinitions(program, layer, dataset)
@@ -215,15 +216,32 @@ export function resolveBarItems(program, layer, dataset, selectionGrain) {
215
216
  if (definitions === undefined) {
216
217
  throw new Error(`Bar mark "${layer.id}" is incomplete for selection.`);
217
218
  }
219
+ return definitions;
220
+ }
221
+
222
+ export function resolveBarItems(program, layer, dataset, selectionGrain) {
223
+ const grain = resolveBarGrain(layer);
224
+ const definitions = resolveBarItemDefinitions(program, layer, dataset).map((definition, index) => ({
225
+ ...definition, key: definition.key ?? itemKey(layer, grain, index)
226
+ }));
227
+ const needsStacks = selectionGrain === 'stack' || Object.values(program.materializationConfigs.highlights ?? {}).some(config =>
228
+ config.target === layer.id && program.materializationConfigs.selections?.[config.selection]?.selector.grain === 'stack');
229
+ const stacks = needsStacks ? barStackDefinitions(layer, grain, definitions) : undefined;
230
+ const ordered = reorderHighlightedItems(program, layer, definitions, { stack: stacks });
218
231
  const graphicTypes = hasRectRadius(program.markConfigs[layer.id]?.barAppearance) ? ["rect", "path"] : "rect";
219
232
  if (selectionGrain === "stack") {
220
233
  return finalizeItems(
221
234
  program,
222
235
  layer,
223
236
  "stack",
224
- barStackDefinitions(layer, grain, definitions),
237
+ stacks.map(stack => ({ ...stack, graphicIndices: stack.graphicIndices.map(index => ordered.indexOf(definitions[index])) })),
225
238
  graphicTypes
226
239
  );
227
240
  }
228
- return finalizeItems(program, layer, grain, definitions, graphicTypes);
241
+ return finalizeItems(program, layer, grain, ordered, graphicTypes, { reorder: false });
242
+ }
243
+
244
+ export function resolveBarSourceItems(program, layer, dataset, selectionGrain) {
245
+ const definitions = resolveBarItemDefinitions(program, layer, dataset);
246
+ return selectionGrain === 'stack' ? barStackDefinitions(layer, resolveBarGrain(layer), definitions) : definitions;
229
247
  }
@@ -1,3 +1,4 @@
1
+ import { memberRowReferences } from "../rows.js";
1
2
  import { normalizeTemporalValue } from "../../../grammar/scales/fields.js";
2
3
  import { cloneAndFreeze, isPlainObject } from "../../../core/immutable.js";
3
4
  import {
@@ -108,7 +109,7 @@ function collectionBounds(program, items) {
108
109
  return { x: left, y: top, width: right - left, height: bottom - top };
109
110
  }
110
111
 
111
- export function finalizeItems(program, layer, grain, definitions, graphicType) {
112
+ export function finalizeItems(program, layer, grain, definitions, graphicType, { reorder = true } = {}) {
112
113
  const graphic = requireResolvedGraphic(program, layer, graphicType);
113
114
  const referenced = definitions.flatMap((definition, index) =>
114
115
  definition.graphicIndices ?? [index]
@@ -122,10 +123,14 @@ export function finalizeItems(program, layer, grain, definitions, graphicType) {
122
123
  `Mark "${layer.id}" item count does not match its materialized graphics.`
123
124
  );
124
125
  }
126
+ definitions = definitions.map((definition, index) => ({ ...definition, key: definition.key ?? itemKey(layer, grain, index) }));
127
+ if (reorder && definitions.every(definition => definition.graphicIndices === undefined)) {
128
+ definitions = reorderHighlightedItems(program, layer, definitions);
129
+ }
125
130
  return Object.freeze(definitions.map((definition, index) => {
126
131
  const indices = definition.graphicIndices ?? [index];
127
132
  const items = indices.map(childIndex => graphic.items[childIndex]);
128
- const properties = definition.properties ?? (
133
+ const properties = definition.properties === undefined ? (
129
134
  items.length === 1
130
135
  ? selectableGraphicProperties({
131
136
  type: items[0].type ?? graphic.type,
@@ -135,7 +140,7 @@ export function finalizeItems(program, layer, grain, definitions, graphicType) {
135
140
  ...sharedConcreteProperties(items),
136
141
  ...collectionBounds(program, items)
137
142
  }
138
- );
143
+ ) : { ...definition.properties, ...concreteProperties(items[0]?.properties) };
139
144
  return Object.freeze({
140
145
  key: definition.key ?? itemKey(layer, grain, index),
141
146
  layer: layer.id,
@@ -147,6 +152,7 @@ export function finalizeItems(program, layer, grain, definitions, graphicType) {
147
152
  ? {}
148
153
  : { geometry: cloneAndFreeze(definition.geometry) }),
149
154
  members: Object.freeze([...definition.members]),
155
+ rows: Object.freeze(memberRowReferences(program, definition.members)),
150
156
  graphicIds: Object.freeze(items.map((child, childOffset) =>
151
157
  child.id ?? graphicId(layer, indices[childOffset])
152
158
  ))
@@ -154,7 +160,7 @@ export function finalizeItems(program, layer, grain, definitions, graphicType) {
154
160
  }));
155
161
  }
156
162
 
157
- export function reorderHighlightedItems(program, layer, definitions) {
163
+ export function reorderHighlightedItems(program, layer, definitions, groups = {}) {
158
164
  let ordered = definitions;
159
165
  for (const config of Object.values(
160
166
  program.materializationConfigs.highlights ?? {}
@@ -162,7 +168,14 @@ export function reorderHighlightedItems(program, layer, definitions) {
162
168
  if (config.target !== layer.id || config.bringToFront !== true) continue;
163
169
  const selection = program.materializationConfigs.selections?.[config.selection];
164
170
  if (selection?.target !== layer.id) continue;
165
- const selected = new Set(selectMarkItemKeys(ordered, selection.selector));
171
+ const predicate = selection.selector;
172
+ const grouped = groups[predicate.grain];
173
+ const candidates = (grouped ?? ordered).map(item => ({ ...item, properties: item.properties ?? {}, ...(predicate.rows ? { rows: memberRowReferences(program, item.members) } : {}) }));
174
+ let selected = new Set(selectMarkItemKeys(candidates, predicate));
175
+ if (grouped) {
176
+ const members = new Set(grouped.filter(item => selected.has(item.key)).flatMap(item => item.members));
177
+ selected = new Set(ordered.filter(item => item.members.some(row => members.has(row))).map(item => item.key));
178
+ }
166
179
  ordered = [
167
180
  ...ordered.filter(definition => !selected.has(definition.key)),
168
181
  ...ordered.filter(definition => selected.has(definition.key))
@@ -0,0 +1,15 @@
1
+ import { resolveOverlaySeries, overlaySegments } from "../../overlayItems.js";
2
+ import { finalizeItems, uniqueFields, channelMapFromRow } from "./common.js";
3
+
4
+ export function resolveOverlayItems(program, layer) {
5
+ const binding = layer.derivedBindings.overlay;
6
+ const series = resolveOverlaySeries(program, binding);
7
+ const definitions = layer.mark.type === "point" ? series.flatMap(group => group.points)
8
+ : series.flatMap(group => overlaySegments(group, binding).map(points => {
9
+ const members = [...new Set(points.flatMap(point => point.members))];
10
+ const fields = uniqueFields(points.map(point => point.fields));
11
+ return { fields, channels: channelMapFromRow(fields, layer), members };
12
+ }));
13
+ return finalizeItems(program, layer, layer.mark.type === "point" ? "point" : "series", definitions,
14
+ layer.mark.type === "line" ? "path" : ["circle", "rect", "path"]);
15
+ }
@@ -62,6 +62,7 @@ export function resolveLineItems(program, layer, dataset) {
62
62
  {
63
63
  scales: program.semanticSpec.scales,
64
64
  xDomain: program.resolvedScales[x?.scale]?.domain,
65
+ yDomain: program.resolvedScales[layer.encoding.y?.scale]?.domain,
65
66
  ...(x?.bin === undefined ? {} : {
66
67
  xBinBoundaries: resolveLineBins(
67
68
  dataset.values,
@@ -3,8 +3,7 @@ import {
3
3
  concreteProperties,
4
4
  finalizeItems,
5
5
  itemKey,
6
- ownFields,
7
- reorderHighlightedItems
6
+ ownFields
8
7
  } from "./common.js";
9
8
 
10
9
  export function resolvePointItems(program, layer, dataset) {
@@ -33,7 +32,7 @@ export function resolvePointItems(program, layer, dataset) {
33
32
  program,
34
33
  layer,
35
34
  "point",
36
- reorderHighlightedItems(program, layer, definitions),
35
+ definitions,
37
36
  program.graphicSpec.objects[layer.id]?.type
38
37
  );
39
38
  }
@@ -3,8 +3,7 @@ import {
3
3
  concreteProperties,
4
4
  finalizeItems,
5
5
  itemKey,
6
- ownFields,
7
- reorderHighlightedItems
6
+ ownFields
8
7
  } from "./common.js";
9
8
 
10
9
  export function resolveTickItems(program, layer, dataset) {
@@ -15,20 +14,12 @@ export function resolveTickItems(program, layer, dataset) {
15
14
  if (!Array.isArray(graphic?.items) || !completePosition) {
16
15
  throw new Error(`Tick mark "${layer.id}" is incomplete for selection.`);
17
16
  }
18
- let definitions = reorderHighlightedItems(
19
- program,
20
- layer,
21
- dataset.values.map((row, index) => ({
17
+ const definitions = dataset.values.map((row, index) => ({
22
18
  key: itemKey(layer, "tick", index),
23
19
  fields: ownFields(row),
24
20
  channels: channelMapFromRow(row, layer),
25
21
  properties: concreteProperties(graphic.items[index]?.properties),
26
22
  members: [row]
27
- }))
28
- );
29
- definitions = definitions.map((definition, index) => ({
30
- ...definition,
31
- properties: concreteProperties(graphic.items[index]?.properties)
32
- }));
23
+ }));
33
24
  return finalizeItems(program, layer, "tick", definitions, "line");
34
25
  }
@@ -17,9 +17,9 @@ export function transformPathHighlightProperties(properties, style) {
17
17
  ...(offset.x === 0 && offset.y === 0
18
18
  ? {}
19
19
  : {
20
- commands: properties.commands.map(command =>
20
+ ...(properties.commands ? { commands: properties.commands.map(command =>
21
21
  translateCommand(command, offset)
22
- )
22
+ ) } : { x: properties.x + offset.x, y: properties.y + offset.y })
23
23
  })
24
24
  };
25
25
  }
@@ -1,9 +1,11 @@
1
+ import { resolveBarSourceItems } from "../items/bar.js";
1
2
  import { resolveBarItems } from "../items/index.js";
2
3
  import { normalizeBarHighlightStyle } from "../styles.js";
3
4
 
4
5
  export const barSelectionPolicy = Object.freeze({
5
6
  supportedGrains: Object.freeze(["item", "stack"]),
6
7
  resolveItems: resolveBarItems,
8
+ resolveSourceItems: resolveBarSourceItems,
7
9
  normalizeHighlightStyle: normalizeBarHighlightStyle,
8
10
  applyHighlightOp: "applyBarHighlight",
9
11
  rematerializeOp: "rematerializeBarMark"
@@ -1,3 +1,6 @@
1
+ import { rowsForSelectedItems, memberRowReferences } from "../rows.js";
2
+ import { selectMarkItemKeys } from "../../../grammar/markSelection.js";
3
+ import { textSelectionPolicy } from "./text.js";
1
4
  import { findDataset } from "../../../selectors/datasets.js";
2
5
  import { requireLayer } from "../../../selectors/layers.js";
3
6
  import { areaSelectionPolicy } from "./area.js";
@@ -9,8 +12,10 @@ import { ruleSelectionPolicy } from "./rule.js";
9
12
  import { tickSelectionPolicy } from "./tick.js";
10
13
  import { rectSelectionPolicy } from "./rect.js";
11
14
  import { applyItemMissingPolicy } from "../../../grammar/itemMissing.js";
15
+ import { resolveOverlayItems } from "../items/overlay.js";
12
16
 
13
17
  const POLICIES = Object.freeze({
18
+ text: textSelectionPolicy,
14
19
  arc: arcSelectionPolicy,
15
20
  area: areaSelectionPolicy,
16
21
  bar: barSelectionPolicy,
@@ -34,7 +39,24 @@ export function requireSelectionPolicy(markType) {
34
39
  }
35
40
 
36
41
  export function resolveMarkItems(program, target, grain = "item") {
42
+ for (const config of Object.values(program.materializationConfigs.highlights ?? {})) {
43
+ const definition = program.materializationConfigs.selections?.[config.selection];
44
+ if (config.target !== target || !definition?.sourceSelection) continue;
45
+ const parent = program.materializationConfigs.selections[definition.sourceSelection];
46
+ if (!parent || parent.sourceSelection) throw new Error('Invalid composite selection owner.');
47
+ if (!definition.selector.rows) {
48
+ program = program._withSelectionConfig(config.selection, { ...definition, selector: parent.selector });
49
+ continue;
50
+ }
51
+ const parentItems = parent.selector.property ? resolveMarkItems(program, parent.target, parent.selector.grain) : resolveMarkSourceItems(program, parent.target, parent.selector.grain);
52
+ const rows = rowsForSelectedItems(program, parentItems, selectMarkItemKeys(parentItems, parent.selector), definition.selector.rows.data);
53
+ program = program._withSelectionConfig(config.selection, { ...definition, selector: { ...definition.selector, rows } });
54
+ }
37
55
  const layer = requireLayer(program, target, `Unknown mark target "${target}"`);
56
+ if (layer.derivedBindings?.overlay) {
57
+ if (grain !== "item") throw new Error(`Overlay "${target}" supports item selection grain.`);
58
+ return resolveOverlayItems(program, layer);
59
+ }
38
60
  const dataset = findDataset(program, layer.data);
39
61
  if (dataset === undefined) {
40
62
  throw new Error(`Mark "${target}" requires an existing dataset for selection.`);
@@ -43,5 +65,17 @@ export function resolveMarkItems(program, target, grain = "item") {
43
65
  if (!policy.supportedGrains.includes(grain)) {
44
66
  throw new Error(`Mark "${target}" does not support ${grain} selection grain.`);
45
67
  }
46
- return policy.resolveItems(program, layer, applyItemMissingPolicy(layer, dataset), grain);
68
+ return policy.resolveItems(program, layer, applyItemMissingPolicy(layer, dataset), grain, resolveMarkItems);
69
+ }
70
+
71
+ // Composite source membership does not require the parent's graphics to have
72
+ // finished rebuilding. Each owning policy supplies its same final semantic grain.
73
+ export function resolveMarkSourceItems(program, target, grain = 'item') {
74
+ const layer = requireLayer(program, target), policy = requireSelectionPolicy(layer.mark.type);
75
+ if (!policy.resolveSourceItems) return resolveMarkItems(program, target, grain);
76
+ const dataset = applyItemMissingPolicy(layer, findDataset(program, layer.data));
77
+ return policy.resolveSourceItems(program, layer, dataset, grain).map((item,index) => ({
78
+ ...item, key: item.key ?? `${layer.id}/source/${index}`, properties: item.properties ?? {},
79
+ rows: memberRowReferences(program, item.members)
80
+ }));
47
81
  }
@@ -1,9 +1,11 @@
1
+ import { ownFields, channelMapFromRow } from "../items/common.js";
1
2
  import { resolvePointItems } from "../items/index.js";
2
3
  import { normalizePointHighlightStyle } from "../styles.js";
3
4
 
4
5
  export const pointSelectionPolicy = Object.freeze({
5
6
  supportedGrains: Object.freeze(["item"]),
6
7
  resolveItems: resolvePointItems,
8
+ resolveSourceItems: (program, layer, dataset) => dataset.values.map(row => ({fields:ownFields(row),channels:channelMapFromRow(row,layer),members:[row]})),
7
9
  normalizeHighlightStyle: normalizePointHighlightStyle,
8
10
  applyHighlightOp: "applyPointHighlight",
9
11
  rematerializeOp: "rematerializePointMark"
@@ -0,0 +1,17 @@
1
+ import { DEFAULT_TEXT_MARK } from '../../../grammar/text.js';
2
+ import { resolveTextGraphicEntries } from '../../textGeometry.js';
3
+ import { finalizeItems, itemKey } from '../items/common.js';
4
+ import { normalizeTextHighlightStyle } from '../styles.js';
5
+
6
+ export const textSelectionPolicy = Object.freeze({
7
+ supportedGrains: Object.freeze(['item']),
8
+ resolveItems(program, layer, dataset, grain, resolveMarkItems) {
9
+ const entries = resolveTextGraphicEntries(program, layer, program.markConfigs[layer.id] ?? DEFAULT_TEXT_MARK, { members: true, resolveMarkItems });
10
+ const definitions = entries.map((entry, i) => ({ key: itemKey(layer, 'text', i), fields: entry.fields,
11
+ channels: entry.channels, properties: {}, members: entry.members }));
12
+ return finalizeItems(program, layer, 'text', definitions, 'text');
13
+ },
14
+ normalizeHighlightStyle: normalizeTextHighlightStyle,
15
+ applyHighlightOp: 'applyPathHighlight',
16
+ rematerializeOp: 'rematerializeTextMark'
17
+ });
@@ -0,0 +1,103 @@
1
+ import { validateRowSelectionDataset } from "../../grammar/markSelection.js";
2
+ import { findDataset } from "../../selectors/datasets.js";
3
+ import { deriveBin2DRows } from "../../grammar/bin2d.js";
4
+ import { deriveBoxData } from "../../grammar/boxPlot.js";
5
+ import { deriveFilteredRows } from '../../grammar/filter.js';
6
+ import { deriveSortedRows } from '../../grammar/sort.js';
7
+
8
+ const cache = new WeakMap();
9
+
10
+ // Provenance is an inspection cache, never an injected data column. Identical
11
+ // observations retain distinct indices. Only explicit transform semantics are
12
+ // used to traverse a derived dataset; unknown lineage is not guessed by value.
13
+ function indexRows(program) {
14
+ const provenance = program.materializationConfigs.rowProvenance;
15
+ const cached = cache.get(program.semanticSpec.datasets);
16
+ if (cached && cached.provenance === provenance) return cached.byRow;
17
+ const datasets = new Map(program.semanticSpec.datasets.filter(d => Array.isArray(d.values)).map(d => [d.id, d]));
18
+ const references = new Map(), building = new Set();
19
+ function visit(id) {
20
+ if (references.has(id)) return references.get(id);
21
+ if (building.has(id)) throw new Error('Cyclic dataset lineage.');
22
+ building.add(id);
23
+ const d = datasets.get(id);
24
+ if (!d) throw new Error(`Unknown row dataset "${id}".`);
25
+ const result = d.values.map((_, index) => [{ data: id, index }]);
26
+ references.set(id, result);
27
+ if (d.source && datasets.has(d.source)) {
28
+ const source = datasets.get(d.source), parents = visit(d.source), t = d.transform?.length === 1 ? d.transform[0] : null;
29
+ let mapping;
30
+ if (t?.type === 'markFilter' && program.materializationConfigs.rowProvenance?.[id]) {
31
+ mapping = program.materializationConfigs.rowProvenance[id].indices.map(index => [index]);
32
+ } else if (t && ['filter', 'sort'].includes(t.type)) {
33
+ const derived = (t.type === 'filter' ? deriveFilteredRows : deriveSortedRows)(source.values, t);
34
+ const positions = new Map(source.values.map((row, i) => [row, i]));
35
+ mapping = derived.map(row => [positions.get(row)]);
36
+ } else if (t && ['computed', 'timeUnit', 'lookup', 'window', 'normalize'].includes(t.type) && d.values.length === source.values.length) {
37
+ mapping = d.values.map((_, i) => [i]);
38
+ } else if (t?.type === 'fold') {
39
+ mapping = d.values.map((_, i) => [Math.floor(i / t.fields.length)]);
40
+ } else if (t?.type === 'bin2d') {
41
+ const memberField = '__selection_members';
42
+ mapping = deriveBin2DRows(source.values, { ...t, members: true, as: { ...t.as, members: memberField } }).values.map(row => row[memberField]);
43
+ } else if (t?.type === 'boxOutlier') {
44
+ const summaries = deriveBoxData(source.values, { ...t, type: 'boxSummary' }).summaries;
45
+ const groups = new Map(summaries.map(row => [row[t.category], row]));
46
+ mapping = source.values.flatMap((row,i) => { const group = groups.get(row[t.category]); return group && (row[t.field] < group[t.as.lowerWhisker] || row[t.field] > group[t.as.upperWhisker]) ? [[i]] : []; });
47
+ } else if (t && ['ecdf', 'horizon', 'boxSummary', 'summary', 'interval', 'density', 'gradientProfile', 'regression', 'regressionStatistics'].includes(t.type)) {
48
+ const fields = [...new Set([...(Array.isArray(t.groupBy) ? t.groupBy : t.groupBy ? [t.groupBy] : []), ...(t.category ? [t.category] : [])])];
49
+ const groups = new Map();
50
+ source.values.forEach((row, i) => { const key = JSON.stringify(fields.map(f => row[f])); if (!groups.has(key)) groups.set(key, []); groups.get(key).push(i); });
51
+ mapping = d.values.map(row => groups.get(JSON.stringify(fields.map(f => row[f]))) ?? []);
52
+ }
53
+ if (mapping?.length === result.length) mapping.forEach((indices, i) => {
54
+ for (const index of indices) if (Number.isInteger(index)) result[i].push(...parents[index]);
55
+ });
56
+ }
57
+ building.delete(id);
58
+ return result;
59
+ }
60
+ const byRow = new Map();
61
+ for (const d of datasets.values()) {
62
+ const refs = visit(d.id);
63
+ d.values.forEach((row, i) => byRow.set(row, [...(byRow.get(row) ?? []), ...refs[i]]));
64
+ }
65
+ cache.set(program.semanticSpec.datasets, { provenance, byRow });
66
+ return byRow;
67
+ }
68
+
69
+ // Record identity while the retained rows still share their source objects.
70
+ // Persisting these positions avoids guessing between equal-valued rows on restore.
71
+ export function markFilterSourceIndices(sourceRows, rows) {
72
+ const positions = new Map(sourceRows.map((row, index) => [row, index]));
73
+ return rows.map(row => {
74
+ const index = positions.get(row);
75
+ if (index === undefined) throw new Error('Filtered row has no source identity.');
76
+ return index;
77
+ });
78
+ }
79
+
80
+ export function memberRowReferences(program, members) {
81
+ const index = indexRows(program), refs = new Map();
82
+ for (const row of members) for (const ref of index.get(row) ?? []) refs.set(`${ref.data}:${ref.index}`, ref);
83
+ const order = new Map(program.semanticSpec.datasets.map((dataset, index) => [dataset.id, index]));
84
+ return [...refs.values()].sort((a, b) => order.get(a.data) - order.get(b.data) || a.index - b.index);
85
+ }
86
+
87
+ export function validateRowSelection(program, rows, items) {
88
+ const dataset = findDataset(program, rows.data);
89
+ validateRowSelectionDataset(rows, dataset);
90
+ if (items.length && !items.some(item => item.rows.some(ref => ref.data === rows.data))) {
91
+ throw new Error(`Row lineage from "${rows.data}" is unavailable for this mark; select its materialized dataset instead.`);
92
+ }
93
+ }
94
+
95
+ export function rowsForSelectedItems(program, items, keys, preferredData) {
96
+ const available = new Set(items.flatMap(item => item.rows.map(ref => ref.data)));
97
+ const data = available.has(preferredData) ? preferredData
98
+ : program.semanticSpec.datasets.filter(dataset => available.has(dataset.id))[0]?.id ?? preferredData;
99
+ const selected = new Set(keys);
100
+ const indices = [...new Set(items.filter(item => selected.has(item.key)).flatMap(item => item.rows)
101
+ .filter(ref => ref.data === data).map(ref => ref.index))];
102
+ return { data, indices };
103
+ }
@@ -1,11 +1,13 @@
1
+ import { validateRowSelection, rowsForSelectedItems } from "./rows.js";
1
2
  import { validateUserId } from "../../core/identifiers.js";
2
3
  import { normalizeMarkSelector, selectMarkItemKeys } from "../../grammar/markSelection.js";
3
- import { resolveMarkItems } from "./policies/index.js";
4
+ import { resolveMarkItems, resolveMarkSourceItems } from "./policies/index.js";
4
5
 
5
6
  export function resolveMarkSelection(program, target, selector) {
6
7
  const normalized = normalizeMarkSelector(selector);
7
8
  const items = resolveMarkItems(program, target, normalized.grain);
8
- if (items.length > 0) {
9
+ if (normalized.rows) validateRowSelection(program, normalized.rows, items);
10
+ if (items.length > 0 && !normalized.rows) {
9
11
  const source = normalized.field !== undefined
10
12
  ? "fields"
11
13
  : normalized.channel !== undefined
@@ -41,6 +43,20 @@ export function resolveSelectionCreationId(program, id, target) {
41
43
  return resolved;
42
44
  }
43
45
 
46
+
47
+ // Dependent components follow their owner's current selection after data or
48
+ // encoding revisions, rather than freezing a list of previously selected rows.
49
+ export function selectionPredicate(program, definition) {
50
+ if (!definition.sourceSelection) return definition.selector;
51
+ const parent = program.materializationConfigs.selections?.[definition.sourceSelection];
52
+ if (!parent || parent.sourceSelection) throw new Error('Invalid composite selection owner.');
53
+ if (!definition.selector.rows) return parent.selector;
54
+ const items = parent.selector.property ? resolveMarkItems(program, parent.target, parent.selector.grain)
55
+ : resolveMarkSourceItems(program, parent.target, parent.selector.grain);
56
+ const resolved = { items, keys: selectMarkItemKeys(items, parent.selector) };
57
+ return { ...definition.selector, rows: rowsForSelectedItems(program, resolved.items, resolved.keys, definition.selector.rows.data) };
58
+ }
59
+
44
60
  export function resolveStoredSelection(program, id) {
45
61
  const selections = program.materializationConfigs.selections ?? {};
46
62
  const requested = id ?? program.context.currentSelection;
@@ -61,7 +77,7 @@ export function resolveStoredSelection(program, id) {
61
77
  const resolved = resolveMarkSelection(
62
78
  program,
63
79
  definition.target,
64
- definition.selector
80
+ selectionPredicate(program, definition)
65
81
  );
66
82
  return Object.freeze({ id: resolvedId, definition, ...resolved });
67
83
  }
@@ -192,3 +192,11 @@ export function normalizeAreaHighlightStyle(args, mark = "Area") {
192
192
  offset: normalizeOffset(args.offset)
193
193
  };
194
194
  }
195
+
196
+ export function normalizeTextHighlightStyle(args) {
197
+ for (const key of ['shape', 'size', 'stroke', 'strokeWidth', 'strokeDash']) if (args[key] !== undefined) throw new Error(`Text highlight does not support ${key}.`);
198
+ if (args.color !== undefined && args.fill !== undefined) throw new Error('Text highlight accepts color or fill, not both.');
199
+ return { fill: validateNonEmptyString(args.color ?? args.fill ?? DEFAULT_COLORS.highlight, 'Text highlight color'),
200
+ ...(args.opacity === undefined ? {} : { opacity: validateUnitInterval(args.opacity, 'Text highlight opacity') }),
201
+ ...(args.fontSize === undefined ? {} : { fontSize: validatePositiveFinite(args.fontSize, 'Text highlight fontSize') }), offset: normalizeOffset(args.offset) };
202
+ }