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
@@ -1,3 +1,4 @@
1
+ import { validateTextAlignment } from "../../core/validation.js";
1
2
  import { textBoundsFitCanvas } from "../../core/textMetrics.js";
2
3
  import { resolveGuideCollisionBlocks } from "../../materialization/guides/resources.js";
3
4
  import { canvasOverflowError } from "../../layout/canvas.js";
@@ -24,10 +25,10 @@ import { findCanvasGraphic } from
24
25
 
25
26
  const OPTIONS = Object.freeze([
26
27
  "text", "subtitle", "position", "align", "offset", "alongOffset", "gap", "titleStyle",
27
- "subtitleStyle", "maxWidth", "wrap", "lineHeight"
28
+ "subtitleStyle", "maxWidth", "wrap", "lineHeight", "subtitleLineHeight"
28
29
  ]);
29
30
  const STYLE_OPTIONS = Object.freeze([
30
- "color", "fontSize", "fontFamily", "fontWeight", "fontStyle"
31
+ "color", "fontSize", "fontFamily", "fontWeight", "fontStyle", "align"
31
32
  ]);
32
33
  const POSITIONS = Object.freeze(["top", "bottom", "left", "right"]);
33
34
  const ALIGNS = Object.freeze(["left", "center", "right"]);
@@ -69,6 +70,7 @@ function normalizeStyle(value, defaults, label) {
69
70
  }
70
71
  const style = { ...defaults, ...(value ?? {}) };
71
72
  validateKeys(style, STYLE_OPTIONS, label);
73
+ validateTextAlignment(style, label);
72
74
  validateTitleString(style.color, `${label} color`);
73
75
  validateTitleString(style.fontFamily, `${label} fontFamily`);
74
76
  validatePositiveFinite(style.fontSize, `${label} fontSize`);
@@ -101,6 +103,9 @@ function normalizeConfig(args, defaults, { hasSubtitle }) {
101
103
  const maxWidth = args.maxWidth ?? defaults.maxWidth;
102
104
  const wrap = args.wrap ?? defaults.wrap ?? (maxWidth === undefined ? undefined : "word");
103
105
  const lineHeight = args.lineHeight ?? defaults.lineHeight;
106
+ const subtitleLineHeight = args.subtitleLineHeight === undefined
107
+ ? defaults.subtitleLineHeight
108
+ : args.subtitleLineHeight;
104
109
  if (!POSITIONS.includes(position)) {
105
110
  throw new Error(`Unsupported title position "${position}".`);
106
111
  }
@@ -128,6 +133,12 @@ function normalizeConfig(args, defaults, { hasSubtitle }) {
128
133
  )) {
129
134
  throw new RangeError("Chart title lineHeight must cover every visible fontSize.");
130
135
  }
136
+ if (subtitleLineHeight !== undefined && (
137
+ !Number.isFinite(subtitleLineHeight) ||
138
+ subtitleLineHeight < subtitleStyle.fontSize
139
+ )) {
140
+ throw new RangeError("Chart subtitleLineHeight must cover subtitleStyle.fontSize.");
141
+ }
131
142
  return {
132
143
  position,
133
144
  align,
@@ -136,6 +147,7 @@ function normalizeConfig(args, defaults, { hasSubtitle }) {
136
147
  gap,
137
148
  titleStyle,
138
149
  subtitleStyle,
150
+ ...(subtitleLineHeight === undefined ? {} : { subtitleLineHeight }),
139
151
  ...(maxWidth === undefined ? {} : { maxWidth, wrap, ...(lineHeight === undefined
140
152
  ? {}
141
153
  : { lineHeight }) })
@@ -241,11 +253,12 @@ export function resolveTitleLayout(program, config) {
241
253
  : config.position === "top"
242
254
  ? 16 + config.offset
243
255
  : plot.y + plot.height + config.offset;
244
- component = (lines, centers) => ({
256
+ const left = x - (config.align === "right" ? block.width : config.align === "center" ? block.width / 2 : 0);
257
+ component = (lines, centers, style) => ({
245
258
  lines,
246
- x,
259
+ x: style.align === undefined ? x : alignedTextAnchor(left, block.width, style.align),
247
260
  y: centers.map(value => blockTop + value),
248
- textAlign: config.align,
261
+ textAlign: style.align ?? config.align,
249
262
  rotation: 0,
250
263
  explicitRotation: config.position !== "top" || lines.length > 1
251
264
  });
@@ -257,19 +270,19 @@ export function resolveTitleLayout(program, config) {
257
270
  ? 16 + config.offset
258
271
  : canvas.properties.width - 16 + config.offset;
259
272
  const mapX = value => config.position === "left" ? edge + value : edge - value;
260
- component = (lines, centers) => ({
273
+ component = (lines, centers, style) => ({
261
274
  lines,
262
275
  x: centers.map(mapX),
263
- y,
264
- textAlign: "center",
276
+ y: y + Math.sin(rotation) * (style.align === "left" ? -block.width / 2 : style.align === "right" ? block.width / 2 : 0),
277
+ textAlign: style.align ?? "center",
265
278
  rotation,
266
279
  explicitRotation: true
267
280
  });
268
281
  }
269
- const title = component(block.titleLines, block.titleCenters);
282
+ const title = component(block.titleLines, block.titleCenters, config.titleStyle);
270
283
  const subtitle = block.subtitleLines.length === 0
271
284
  ? undefined
272
- : component(block.subtitleLines, block.subtitleCenters);
285
+ : component(block.subtitleLines, block.subtitleCenters, config.subtitleStyle);
273
286
  const titleBounds = unionTitleBounds([
274
287
  resolveTitleComponentBounds(title, config.titleStyle, program.materializationConfigs.textMetrics),
275
288
  ...(subtitle === undefined
@@ -5,6 +5,21 @@ import { cloneAndFreeze, freezeOwned, isOwned, isPlainObject } from "./immutable
5
5
  const metadataByWrappedAction = new WeakMap();
6
6
  const implementationByWrappedAction = new WeakMap();
7
7
  let actionCompletionHook;
8
+ const targetDispatchers = new WeakMap();
9
+ const UNSCOPED_PRIMITIVES = new Set(["createGraphics", "editGraphics", "editSemantic", "createData"]);
10
+ export function registerActionTargetDispatcher(ProgramClass, dispatcher) {
11
+ targetDispatchers.set(ProgramClass, dispatcher);
12
+ }
13
+ function targetDispatcher(ProgramClass) {
14
+ for (let owner = ProgramClass; owner; owner = Object.getPrototypeOf(owner)) {
15
+ if (targetDispatchers.has(owner)) return targetDispatchers.get(owner);
16
+ }
17
+ }
18
+
19
+
20
+ export function supportsScopedActionTargets(ProgramClass, operation) {
21
+ return !UNSCOPED_PRIMITIVES.has(operation) && targetDispatcher(ProgramClass) !== undefined;
22
+ }
8
23
 
9
24
  export function setActionCompletionHook(hook) {
10
25
  if (hook !== undefined && typeof hook !== "function") {
@@ -89,29 +104,28 @@ function childList(node) {
89
104
  return list;
90
105
  }
91
106
 
92
- function withChildList(node, list) {
93
- let children;
94
- const next = {};
95
- for (const key of Object.keys(node)) {
96
- if (key !== "children") Object.defineProperty(next, key, {
97
- value: node[key], enumerable: true
98
- });
99
- }
100
- Object.defineProperty(next, "children", {
101
- enumerable: true,
102
- get() {
103
- if (children === undefined) {
104
- const values = new Array(list.length);
105
- let item = list.tail;
106
- for (let index = list.length - 1; index >= 0; index -= 1) {
107
- values[index] = item.value;
108
- item = item.previous;
109
- }
110
- children = freezeOwned(values);
111
- }
112
- return children;
107
+ function readChildren() {
108
+ const list = childLists.get(this);
109
+ if (list.children === undefined) {
110
+ const values = new Array(list.length);
111
+ let item = list.tail;
112
+ for (let index = list.length - 1; index >= 0; index -= 1) {
113
+ values[index] = item.value;
114
+ item = item.previous;
113
115
  }
114
- });
116
+ list.children = freezeOwned(values);
117
+ }
118
+ return list.children;
119
+ }
120
+
121
+ function withChildList(node, list) {
122
+ // Keep one accessor identity across revisions. A new getter closure and
123
+ // nonconfigurable descriptors per append force dictionary-shaped objects;
124
+ // freezing those dominates long nested materialization traces in V8.
125
+ // Copy data properties without evaluating the lazy children array.
126
+ const next = Object.fromEntries(Object.keys(node)
127
+ .filter(key => key !== "children").map(key => [key, node[key]]));
128
+ Object.defineProperty(next, "children", { enumerable: true, get: readChildren });
115
129
  childLists.set(next, list);
116
130
  return freezeOwned(next);
117
131
  }
@@ -202,15 +216,17 @@ export function action(metadata, implementation) {
202
216
  throw new TypeError("Action arguments must be a plain object.");
203
217
  }
204
218
 
205
- if (scope === "unit") this._assertUnitProgram(ownedMetadata.op);
206
- if (scope === "composition") this._assertCompositionProgram(ownedMetadata.op);
219
+ const scoped = isPlainObject(args.target) || Array.isArray(args.target);
220
+ const dispatch = scoped && !UNSCOPED_PRIMITIVES.has(ownedMetadata.op) ? targetDispatcher(this.constructor) : undefined;
221
+ if (!dispatch && scope === "unit") this._assertUnitProgram(ownedMetadata.op);
222
+ if (!dispatch && scope === "composition") this._assertCompositionProgram(ownedMetadata.op);
207
223
 
208
224
  const summarizedArgs = summarizeArgs(args);
209
225
  const entered = this._enterAction({
210
226
  ...ownedMetadata,
211
227
  args: summarizedArgs
212
228
  });
213
- let result = implementation.call(entered, args);
229
+ let result = dispatch ? dispatch(entered, wrappedAction, args) : implementation.call(entered, args);
214
230
 
215
231
  if (!(result instanceof this.constructor)) {
216
232
  throw new TypeError(`${ownedMetadata.op} must return a ChartProgram.`);
@@ -0,0 +1,12 @@
1
+ import { isPlainObject, cloneAndFreeze } from "./immutable.js";
2
+
3
+ export const COMPOSITION_SCALE_CHANNELS = Object.freeze(["x", "y", "color", "stroke", "size", "shape", "opacity", "strokeWidth", "strokeDash"]);
4
+
5
+ export function normalizeCompositionShares(value = {}) {
6
+ if (!isPlainObject(value)) throw new TypeError("Composition share requires a channel object.");
7
+ for (const [channel, shared] of Object.entries(value)) {
8
+ if (!COMPOSITION_SCALE_CHANNELS.includes(channel)) throw new Error(`Unknown composition share channel "${channel}".`);
9
+ if (typeof shared !== "boolean") throw new TypeError(`Composition share.${channel} must be boolean.`);
10
+ }
11
+ return cloneAndFreeze(value);
12
+ }
@@ -1,3 +1,5 @@
1
+ import { normalizeCompositionShares } from "./compositionScales.js";
2
+ import { normalizeFacetGap } from "./facetGap.js";
1
3
  import { normalizeFacetAxes } from "./facetAxisPolicy.js";
2
4
  import { isOwned, isPlainObject } from "./immutable.js";
3
5
  import { ownProgramState } from "./programState.js";
@@ -8,6 +10,11 @@ import {
8
10
  DEFAULT_FACET_TOP_LANE
9
11
  } from "./vocabulary.js";
10
12
 
13
+ export function normalizeCompositionLegends(value) {
14
+ if (!["local", "collect"].includes(value)) throw new TypeError('Composition legends must be "local" or "collect".');
15
+ return value;
16
+ }
17
+
11
18
  // Primitive graphics/trace edits retain both immutable owners. Validate their
12
19
  // relationship once, rather than scanning every facet cell on every primitive.
13
20
  const validatedCompositions = new WeakMap();
@@ -242,10 +249,12 @@ function validateCompositionChildren(compositionSpec, childIds, facet) {
242
249
  }
243
250
 
244
251
  function validateLayout(compositionSpec) {
245
- if (!Number.isFinite(compositionSpec.gap) || compositionSpec.gap < 0) {
252
+ if (compositionSpec.type === "facet") {
253
+ normalizeFacetGap(compositionSpec.gap, "compositionSpec.gap");
254
+ } else if (!Number.isFinite(compositionSpec.gap) || compositionSpec.gap < 0) {
246
255
  throw new RangeError("compositionSpec.gap must be a non-negative finite number.");
247
256
  }
248
- if (!["start", "center", "end"].includes(compositionSpec.align)) {
257
+ if (!(compositionSpec.type === "facet" ? ["start", "center", "end"] : ["start", "center", "end", "plot"]).includes(compositionSpec.align)) {
249
258
  throw new Error("compositionSpec.align must be start, center, or end.");
250
259
  }
251
260
  if (!isPlainObject(compositionSpec.padding)) {
@@ -281,7 +290,7 @@ export function ownCompositionSpec(compositionSpec, children) {
281
290
  }
282
291
  const allowed = [
283
292
  "id", "type", "direction", "children", "columns", "gap", "align",
284
- "padding", "facet", "spacing", "topLane"
293
+ "padding", "facet", "spacing", "topLane", "share", "legends"
285
294
  ];
286
295
  const unknown = Object.keys(compositionSpec).find(key => !allowed.includes(key));
287
296
  if (unknown !== undefined) {
@@ -314,6 +323,14 @@ export function ownCompositionSpec(compositionSpec, children) {
314
323
  ) {
315
324
  throw new Error("Concat compositionSpec does not accept facet properties.");
316
325
  }
326
+ if (compositionSpec.legends !== undefined) {
327
+ if (facet) throw new Error("Facet uses guides.legend, not composition legends.");
328
+ normalizeCompositionLegends(compositionSpec.legends);
329
+ }
330
+ if (compositionSpec.share !== undefined) {
331
+ if (facet) throw new Error("Facet uses scales, not composition share.");
332
+ normalizeCompositionShares(compositionSpec.share);
333
+ }
317
334
  validateLayout(compositionSpec);
318
335
  const owned = ownProgramState(compositionSpec);
319
336
  if (isOwned(children)) {
@@ -0,0 +1,44 @@
1
+ import { cloneAndFreeze, isPlainObject } from "./immutable.js";
2
+ import { annotateError } from "./diagnostics.js";
3
+ export function validateCompositionSpacing(value, label) {
4
+ if (!Number.isFinite(value) || value < 0) {
5
+ throw new RangeError(`${label} must be a non-negative finite number.`);
6
+ }
7
+ return value;
8
+ }
9
+
10
+ /** Keep scalar intent intact; a facet pair always declares both axes. */
11
+ export function normalizeFacetGap(value, label = "Facet gap") {
12
+ if (!isPlainObject(value)) return validateCompositionSpacing(value, label);
13
+ const keys = Reflect.ownKeys(value);
14
+ if (keys.length !== 2 || !keys.includes("x") || !keys.includes("y")) {
15
+ throw annotateError(new TypeError(`${label} must contain exactly x and y.`), {
16
+ code: "invalid-value", optionPath: "gap", actual: value
17
+ });
18
+ }
19
+ const result = {};
20
+ for (const axis of ["x", "y"]) {
21
+ try {
22
+ result[axis] = validateCompositionSpacing(value[axis], `${label}.${axis}`);
23
+ } catch (error) {
24
+ throw annotateError(error, {
25
+ code: "invalid-value", optionPath: `gap.${axis}`, actual: value[axis], limit: 0
26
+ });
27
+ }
28
+ }
29
+ return cloneAndFreeze(result);
30
+ }
31
+
32
+ /** Accept only a previously normalized scalar or pair. */
33
+ export function facetGapAxis(gap, axis) {
34
+ return typeof gap === "number" ? gap : gap[axis];
35
+ }
36
+
37
+ /** Overflow remains axis-specific for a pair; scalar callers keep their old error. */
38
+ export function assertFacetGapFinite(gap, axis, values, message) {
39
+ if (values.every(Number.isFinite)) return;
40
+ const error = new RangeError(message);
41
+ throw typeof gap === "number" ? error : annotateError(error, {
42
+ code: "invalid-value", optionPath: `gap.${axis}`, actual: gap[axis]
43
+ });
44
+ }
@@ -46,12 +46,17 @@ export function cloneAndFreeze(value, ancestors = new WeakSet(), path = "state")
46
46
  if (Array.isArray(value)) {
47
47
  clone = value.map((item, index) => cloneAndFreeze(item, ancestors, `${path}[${index}]`));
48
48
  } else if (isPlainObject(value)) {
49
- clone = Object.fromEntries(
50
- Object.entries(value).map(([key, item]) => [
51
- key,
52
- cloneAndFreeze(item, ancestors, `${path}.${key}`)
53
- ])
54
- );
49
+ clone = {};
50
+ // Snapshot entries before descending, as with Object.fromEntries, but avoid
51
+ // allocating a second entries array and a pair for every stored property.
52
+ for (const [key, item] of Object.entries(value)) {
53
+ const child = cloneAndFreeze(item, ancestors, `${path}.${key}`);
54
+ if (key === "__proto__") {
55
+ Object.defineProperty(clone, key, {
56
+ value: child, enumerable: true, writable: true, configurable: true
57
+ });
58
+ } else clone[key] = child;
59
+ }
55
60
  } else {
56
61
  throw new TypeError("ChartProgram state only supports plain objects and arrays.");
57
62
  }
@@ -48,9 +48,25 @@ export function releaseNamedResourceOwnership(program, { kind, id, dataOwner })
48
48
  materializationConfigs,
49
49
  ["regressionFits", dataOwner?.current ?? id]
50
50
  ).value;
51
+ materializationConfigs = removeMaterializationConfig(
52
+ materializationConfigs,
53
+ ["rowProvenance", dataOwner?.current ?? id]
54
+ ).value;
51
55
  const context = program.context.currentData === id
52
56
  ? freezeOwned({ ...program.context, currentData: undefined })
53
57
  : program.context;
54
58
  if (context === program.context && materializationConfigs === program.materializationConfigs) return program;
55
59
  return program._clone({ context, materializationConfigs });
56
60
  }
61
+
62
+ // Scoped planners temporarily annotate context. Restore only those keys after
63
+ // the action so genuine target/data context changes survive, and absent keys
64
+ // remain absent in persistence rather than becoming serialized undefined tags.
65
+ export function restoreProgramContextKeys(program, reference, keys) {
66
+ const context = { ...program.context };
67
+ for (const key of keys) {
68
+ if (Object.hasOwn(reference.context, key)) context[key] = reference.context[key];
69
+ else delete context[key];
70
+ }
71
+ return program._clone({ context: freezeOwned(context) });
72
+ }
@@ -13,7 +13,7 @@ const DATA_VALUE_KEYS = new Set([
13
13
  const DATA_SOURCE_CONFIGS = new Set([
14
14
  "boxPlot", "density", "ecdfPlot", "endpointPlot", "errorBand", "errorBar",
15
15
  "gradientPlot", "horizon", "intervalPlot", "markFilter", "raincloudPlot",
16
- "regression", "violinPlot"
16
+ "regression", "violinPlot", "radarPlot"
17
17
  ]);
18
18
  const MARK_ID_KEYS = new Set([
19
19
  "bandId", "bodyId", "centerId", "connectorId", "intervalId", "lineId",
@@ -98,7 +98,7 @@ export function collectSemanticScaleReferences(program) {
98
98
  }
99
99
  }
100
100
  collectKnownKeys(program.semanticSpec.guides, [], (key, value, path) => {
101
- if (["scale", "colorScale", "sizeScale"].includes(key)) {
101
+ if (["scale", "colorScale", "sizeScale", "shapeScale"].includes(key)) {
102
102
  append(references, reference(
103
103
  "scale", value, "semanticGuide", "guides", path
104
104
  ));
@@ -134,6 +134,10 @@ export function collectMarkConfigReferences(program) {
134
134
  append(references, reference(
135
135
  "mark", layer.source, "layer", layer.id, ["source"]
136
136
  ));
137
+ append(references, reference(
138
+ "mark", layer.derivedBindings?.overlay?.source, "layer", layer.id,
139
+ ["derivedBindings", "overlay", "source"]
140
+ ));
137
141
  }
138
142
  for (const [ownerId, config] of Object.entries(program.markConfigs)) {
139
143
  collectKnownKeys(config, [], (key, value, path, parentPath) => {
@@ -204,13 +208,16 @@ export function collectGuideConfigReferences(program) {
204
208
  for (const target of Object.keys(program.guideConfigs?.legendGroups ?? {})) {
205
209
  append(references, reference("mark", target, "legend", target, ["legendGroups", target]));
206
210
  }
211
+ for (const target of Object.keys(program.guideConfigs?.suppressedLegends ?? {})) {
212
+ append(references, reference("mark", target, "legend", target, ["suppressedLegends", target]));
213
+ }
207
214
  for (const [family, configs] of Object.entries(program.guideConfigs ?? {})) {
208
215
  collectKnownKeys(configs, [], (key, value, path, parentPath) => {
209
216
  const ownerId = parentPath.length === 0 ? family : parentPath.join(".");
210
217
  const relativePath = parentPath.length === 0
211
218
  ? path
212
219
  : path.slice(parentPath.length);
213
- if (["scale", "colorScale", "sizeScale"].includes(key)) {
220
+ if (["scale", "colorScale", "sizeScale", "shapeScale"].includes(key)) {
214
221
  append(references, reference(
215
222
  "scale", value, family, ownerId, relativePath
216
223
  ));
@@ -240,6 +247,12 @@ export function collectSelectionReferences(program) {
240
247
  append(references, reference(
241
248
  "mark", config.target, "selection", id, ["target"]
242
249
  ));
250
+ append(references, reference(
251
+ "selection", config.sourceSelection, "selection", id, ["sourceSelection"]
252
+ ));
253
+ append(references, reference(
254
+ "data", config.selector?.rows?.data, "selection", id, ["selector", "rows", "data"]
255
+ ));
243
256
  }
244
257
  for (const [id, config] of Object.entries(
245
258
  program.materializationConfigs.highlights ?? {}
@@ -250,6 +263,9 @@ export function collectSelectionReferences(program) {
250
263
  append(references, reference(
251
264
  "selection", config.selection, "highlight", id, ["selection"]
252
265
  ));
266
+ append(references, reference(
267
+ "selection", config.groupSelection, "highlight", id, ["groupSelection"]
268
+ ));
253
269
  }
254
270
  return freezeOwned(references);
255
271
  }
@@ -127,7 +127,14 @@ export function validateFontWeight(value, label, numberType = "number") {
127
127
  return value;
128
128
  }
129
129
 
130
+ export function validateTextAlignment(style, label) {
131
+ if (Object.hasOwn(style, "align") && !["left", "center", "right"].includes(style.align)) {
132
+ throw new TypeError(`${label} align requires left, center or right.`);
133
+ }
134
+ }
135
+
130
136
  export function validateTextStyle(config, label) {
137
+ validateTextAlignment(config, label);
131
138
  validateNonEmptyString(config.color, `${label} color`);
132
139
  validatePositiveFinite(config.fontSize, `${label} fontSize`);
133
140
  validateNonEmptyString(config.fontFamily, `${label} fontFamily`);
@@ -253,7 +253,7 @@ const marks = (...types) => Object.freeze(types);
253
253
  export const APPEARANCE_MARK_TYPES = Object.freeze({
254
254
  encodeColor: marks('point', 'line', 'bar', 'area', 'arc', 'rect', 'text'),
255
255
  encodeStroke: marks('point', 'line', 'area', 'bar', 'rect', 'arc', 'rule', 'tick'),
256
- encodeOpacity: marks('point', 'rule', 'line', 'bar', 'rect'),
256
+ encodeOpacity: marks('point', 'rule', 'line', 'area', 'bar', 'rect'),
257
257
  encodeStrokeWidth: marks('rule', 'line', 'rect'),
258
258
  encodeStrokeDash: marks('line', 'rule'),
259
259
  encodeSize: marks('point'),
@@ -261,6 +261,12 @@ export const APPEARANCE_MARK_TYPES = Object.freeze({
261
261
  encodeAngle: marks('point', 'tick')
262
262
  });
263
263
 
264
+ // Constant appearance can support more marks than field mapping. Inspection
265
+ // and execution share this distinction instead of advertising unusable fields.
266
+ export const FIXED_APPEARANCE_MARK_TYPES = Object.freeze({
267
+ encodeStrokeWidth: marks('rule', 'line', 'rect', 'point', 'bar', 'area', 'arc', 'tick')
268
+ });
269
+
264
270
  // Generated default action methods: start
265
- export const DEFAULT_ACTION_METHODS = /* @__PURE__ */ Object.freeze(["createCanvas","editCanvas","fitCanvas","applyTextMetrics","removeTextMetrics","applyTheme","removeTheme","createData","reviseData","removeData","removeScale","removeCoordinate","bindMarkData","filterData","createSummaryData","createBinData","createLookupData","createFlattenData","createFoldData","createComputedData","createNormalizedData","createSortedData","createCompleteData","createImputedData","createStackData","createDensityData","createRegressionData","createRegressionStatisticsData","createWindowData","createTimeUnitData","createBin2DData","editBin2DData","editDerivedData","editComputedData","editFilteredData","editLookupData","editFlattenData","editFoldData","editSummaryData","editBinData","editTimeUnitData","editWindowData","editDensityData","editStackData","editRegressionData","editRegressionStatisticsData","editIntervalData","editECDFData","editNormalizedData","editSortedData","editCompleteData","editImputedData","createIntervalData","createECDFData","createPointMark","createTickMark","createStatisticalReference","createReferenceLine","createReferenceBand","createMarkLabels","editMarkLabelSelection","editMarkLabelPlacement","removeMarkLabels","createAnnotation","createTextMark","editTextMark","layoutLabels","removeLabelLayout","editPointMark","editTickMark","jitterPoints","removeJitter","packPoints","removePointPacking","removeMark","createLineMark","editLineMark","createBarMark","editBarMark","createAreaMark","createRuleMark","editRuleMark","editAreaMark","encodeX","encodeY","encodeChannels","encodeX2","encodeColor","encodeStrokeDash","encodeStroke","encodeStrokeWidth","encodeSize","encodeShape","encodeAngle","encodeOpacity","encodeRadius","encodeTheta","encodeR","encodePointRadius","removePointRadius","encodeXOffset","encodeYOffset","encodeY2","encodeYRange","encodeXRange","encodeGroup","encodePathOrder","encodeParallelCoordinates","removePathOrder","orderCategories","removeCategoryOrder","removeEncoding","encodeText","encodeDx","encodeDy","encodeHistogram","encodeDensity","editDensity","encodeHorizon","editHorizon","encodeBarWidth","createRegression","editRegression","createErrorBar","editErrorBar","createErrorBand","editErrorBand","editErrorBandBoundary","createBoxPlot","editBoxPlot","createGradientPlot","editGradientPlot","createViolinPlot","editViolinPlot","createParallelAxes","createParallelAxis","editParallelAxis","removeParallelAxis","removeParallelAxes","createAxes","createXAxis","createYAxis","createXAxisLine","createYAxisLine","editXAxisLine","editYAxisLine","createXAxisTicks","createYAxisTicks","editXAxisTicks","editYAxisTicks","createXAxisLabels","createYAxisLabels","editXAxisLabels","editYAxisLabels","createXAxisTicksAndLabels","createYAxisTicksAndLabels","editXAxisTicksAndLabels","editYAxisTicksAndLabels","createXAxisTitle","createYAxisTitle","editXAxisTitle","editYAxisTitle","editAxes","editXAxis","editYAxis","removeXAxis","removeYAxis","createGrid","createHorizontalGrid","createVerticalGrid","editHorizontalGrid","editVerticalGrid","editGrid","removeGrid","createLegend","editLegend","editLegendBlock","editLegendLayout","editLegendLabels","editLegendTitle","editLegendSymbols","editLegendBorder","createGuides","removeLegend","createTitle","editTitle","removeTitle","createCoordinate","editCoordinate","createScale","editScale","editXScale","editYScale","editXOffsetScale","editYOffsetScale","editParallelScale","editThetaScale","editRScale","editColorScale","editStrokeScale","editSizeScale","editOpacityScale","editShapeScale","editStrokeWidthScale","editStrokeDashScale","createDerivedData","createRegressionBand","editRegressionBand","createRegressionLine","editRegressionLine","filterMarks","removeMarkFilter","selectMarks","editMarkSelection","removeMarkHighlight","removeMarkSelection","highlightMarks","editSemantic","createGraphics","editGraphics","createArcMark","editArcMark","createThetaAxis","createRadialAxis","editThetaAxis","editRadialAxis","removeThetaAxis","removeRadialAxis","createThetaAxisLine","editThetaAxisLine","createRadialAxisLine","editRadialAxisLine","createThetaAxisTicks","editThetaAxisTicks","createRadialAxisTicks","editRadialAxisTicks","createThetaAxisLabels","editThetaAxisLabels","createRadialAxisLabels","editRadialAxisLabels","createThetaAxisTitle","editThetaAxisTitle","createRadialAxisTitle","editRadialAxisTitle","createThetaGrid","createRadialGrid","editThetaGrid","editRadialGrid","editCompositionLayout","replaceCompositionChild","insertCompositionChild","removeCompositionChild","reorderCompositionChildren","facet","facetGrid","repeatCharts","editFacetSource","editFacetHeaders","editFacetScales","editFacetGuides","createScatterPlot","createTextPlot","createDotPlot","createLollipopPlot","createDumbbellPlot","editEndpointPlot","createECDFPlot","editECDFPlot","createIntervalPlot","createRegressionPlot","createLinePlot","createPolarScatterPlot","createPolarLinePlot","createRadarPlot","editRadarPlot","createRugPlot","createStripPlot","createBeeswarmPlot","createRaincloudPlot","editRaincloudPlot","createBarPlot","createHistogram","createHeatmap","createParallelCoordinates","createRectMark","editRectMark","createPiePlot","createRosePlot","createRadialBarPlot","createDensityPlot","createHorizonPlot","createAreaPlot","layoutSeries"]);
271
+ export const DEFAULT_ACTION_METHODS = /* @__PURE__ */ Object.freeze(["createCanvas","editCanvas","fitCanvas","applyTextMetrics","removeTextMetrics","applyTheme","removeTheme","createData","reviseData","removeData","removeScale","removeCoordinate","bindMarkData","filterData","createSummaryData","createBinData","createLookupData","createFlattenData","createFoldData","createComputedData","createNormalizedData","createSortedData","createCompleteData","createImputedData","createStackData","createDensityData","createRegressionData","createRegressionStatisticsData","createWindowData","createTimeUnitData","createBin2DData","editBin2DData","editDerivedData","editComputedData","editFilteredData","editLookupData","editFlattenData","editFoldData","editSummaryData","editBinData","editTimeUnitData","editWindowData","editDensityData","editStackData","editRegressionData","editRegressionStatisticsData","editIntervalData","editECDFData","editNormalizedData","editSortedData","editCompleteData","editImputedData","createIntervalData","createECDFData","createPointMark","createTickMark","createStatisticalReference","createReferenceLine","createReferenceBand","createMarkLabels","editMarkLabelSelection","editMarkLabelPlacement","removeMarkLabels","createAnnotation","createTextMark","editTextMark","layoutLabels","removeLabelLayout","editPointMark","editTickMark","jitterPoints","removeJitter","packPoints","removePointPacking","removeMark","createLineMark","editLineMark","createBarMark","editBarMark","createAreaMark","createRuleMark","editRuleMark","editAreaMark","encodeX","encodeY","encodeChannels","encodeX2","encodeColor","encodeStrokeDash","encodeStroke","encodeStrokeWidth","encodeSize","encodeShape","encodeAngle","encodeOpacity","encodeRadius","encodeTheta","encodeR","encodePointRadius","removePointRadius","encodeXOffset","encodeYOffset","encodeY2","encodeYRange","encodeXRange","encodeGroup","encodePathOrder","encodeParallelCoordinates","removePathOrder","orderCategories","removeCategoryOrder","removeEncoding","encodeText","encodeDx","encodeDy","encodeHistogram","encodeDensity","editDensity","encodeHorizon","editHorizon","encodeBarWidth","createRegression","editRegression","createErrorBar","editErrorBar","createErrorBand","editErrorBand","editErrorBandBoundary","createBoxPlot","editBoxPlot","createGradientPlot","editGradientPlot","createViolinPlot","editViolinPlot","createParallelAxes","createParallelAxis","editParallelAxis","removeParallelAxis","removeParallelAxes","createAxes","createXAxis","createYAxis","createXAxisLine","createYAxisLine","editXAxisLine","editYAxisLine","createXAxisTicks","createYAxisTicks","editXAxisTicks","editYAxisTicks","createXAxisLabels","createYAxisLabels","editXAxisLabels","editYAxisLabels","createXAxisTicksAndLabels","createYAxisTicksAndLabels","editXAxisTicksAndLabels","editYAxisTicksAndLabels","createXAxisTitle","createYAxisTitle","editXAxisTitle","editYAxisTitle","editAxes","editXAxis","editYAxis","removeXAxis","removeYAxis","createGrid","createHorizontalGrid","createVerticalGrid","editHorizontalGrid","editVerticalGrid","editGrid","removeGrid","createLegend","editLegend","editLegendBlock","editLegendLayout","editLegendLabels","editLegendTitle","editLegendSymbols","editLegendBorder","createGuides","removeLegend","createTitle","editTitle","removeTitle","createCoordinate","editCoordinate","createScale","editScale","editXScale","editYScale","editXOffsetScale","editYOffsetScale","editParallelScale","editThetaScale","editRScale","editColorScale","editStrokeScale","editSizeScale","editOpacityScale","editShapeScale","editStrokeWidthScale","editStrokeDashScale","createDerivedData","createRegressionBand","editRegressionBand","createRegressionLine","editRegressionLine","filterMarks","removeMarkFilter","selectMarks","editMarkSelection","removeMarkHighlight","removeMarkSelection","highlightMarks","editSemantic","createGraphics","editGraphics","createArcMark","editArcMark","createThetaAxis","createRadialAxis","editThetaAxis","editRadialAxis","removeThetaAxis","removeRadialAxis","createThetaAxisLine","editThetaAxisLine","createRadialAxisLine","editRadialAxisLine","createThetaAxisTicks","editThetaAxisTicks","createRadialAxisTicks","editRadialAxisTicks","createThetaAxisLabels","editThetaAxisLabels","createRadialAxisLabels","editRadialAxisLabels","createThetaAxisTitle","editThetaAxisTitle","createRadialAxisTitle","editRadialAxisTitle","createThetaGrid","createRadialGrid","editThetaGrid","editRadialGrid","editCompositionLayout","replaceCompositionChild","insertCompositionChild","removeCompositionChild","reorderCompositionChildren","facet","facetGrid","repeatCharts","editFacetSource","editFacetHeaders","editFacetScales","editFacetGuides","createScatterPlot","createTextPlot","createDotPlot","createLollipopPlot","createDumbbellPlot","editEndpointPlot","createECDFPlot","editECDFPlot","createIntervalPlot","createRegressionPlot","createLinePlot","createPolarScatterPlot","createPolarLinePlot","createRadarPlot","editRadarPlot","createRugPlot","createStripPlot","createBeeswarmPlot","createRaincloudPlot","editRaincloudPlot","createBarPlot","createHistogram","createHeatmap","createParallelCoordinates","createRectMark","editRectMark","createPiePlot","createRosePlot","createRadialBarPlot","createDensityPlot","createHorizonPlot","createAreaPlot","layoutSeries","createOverlay","editOverlay","createLayerPlot","createAdjacentPlot","editCompositionScales"]);
266
272
  // Generated default action methods: end
@@ -0,0 +1,24 @@
1
+ import { validateOptionObject } from "../core/validation.js";
2
+ import { validateUserId } from "../core/identifiers.js";
3
+
4
+ function normalizeTarget(target) {
5
+ if (typeof target === "string") return { childPath: [], id: validateUserId(target, "Action target") };
6
+ validateOptionObject(target, ["childPath", "id", "facetSource"], "Scoped action target");
7
+ if (!Array.isArray(target.childPath) || target.childPath.some(id => typeof id !== "string")) {
8
+ throw new TypeError("Scoped action target requires a childPath array.");
9
+ }
10
+ if (target.facetSource !== undefined && target.facetSource !== true) throw new TypeError("facetSource must be true when supplied.");
11
+ target.childPath.forEach(id => validateUserId(id, "Target child id"));
12
+ return { childPath: [...target.childPath], ...(target.facetSource ? { facetSource: true } : {}), ...(target.id === undefined ? {} : { id: validateUserId(target.id, "Action target id") }) };
13
+ }
14
+
15
+
16
+ export function normalizeActionTargets(value) {
17
+ const requested = Array.isArray(value) ? value : [value];
18
+ if (!requested.length) throw new Error("Action target list must not be empty.");
19
+ const targets = requested.map(normalizeTarget);
20
+ if (new Set(targets.map(target => JSON.stringify(target))).size !== targets.length) {
21
+ throw new Error("Action targets must be unique.");
22
+ }
23
+ return targets;
24
+ }
@@ -1 +1 @@
1
- import{resolveTextLines as l}from"../core/textMetrics.js";function a(e,{title:i=!1}={}){if(Object.hasOwn(e,"align")&&!["left","center","right"].includes(e.align))throw new TypeError("Axis text align must be left, center, or right.");if(i){if(Object.hasOwn(e,"overlap")&&!["error","allow"].includes(e.overlap))throw new TypeError("Axis title overlap must be error or allow.");if(Object.hasOwn(e,"baseline")&&!["top","hanging","middle","alphabetic","ideographic","bottom"].includes(e.baseline))throw new TypeError("Axis title baseline must be a supported text baseline.");for(const t of["offset","alongOffset"])if(Object.hasOwn(e,t)&&!Number.isFinite(e[t]))throw new TypeError(`Axis title ${t} must be a finite number.`)}}function f(e,i){if(!/[\r\n]/u.test(e))return;const t=i.baseline??"middle",n=l(e,{fontSize:i.fontSize,align:i.align??"center",baseline:t,...t==="middle"?{blockAlign:"middle"}:{}});if(["bottom","ideographic"].includes(t)){const o=(n.length-1)*i.fontSize*1.2;return n.map(r=>({...r,y:r.y-o}))}return n.length>1?n:void 0}function b(e){return e?.overlap==="allow"}export{b as axisTitleAllowsLabelOverlap,f as axisTitleTextLines,a as validateAxisTextPlacement};
1
+ import{validateKeys as a,validateNonNegativeFinite as o}from"../core/validation.js";import{isPlainObject as s}from"../core/immutable.js";import{resolveTextLines as b}from"../core/textMetrics.js";function c(e,{title:t=!1}={}){if(Object.hasOwn(e,"align")&&!["left","center","right"].includes(e.align))throw new TypeError("Axis text align must be left, center, or right.");if(t){if(Object.hasOwn(e,"overlap")&&!["error","allow"].includes(e.overlap))throw new TypeError("Axis title overlap must be error or allow.");if(Object.hasOwn(e,"baseline")&&!["top","hanging","middle","alphabetic","ideographic","bottom"].includes(e.baseline))throw new TypeError("Axis title baseline must be a supported text baseline.");if(Object.hasOwn(e,"offset")&&s(e.offset)){if(a(e.offset,["from","gap"],"Axis title offset"),e.offset.from!=="labels"||!Object.hasOwn(e.offset,"gap"))throw new TypeError("Axis title offset requires from: labels and gap.");o(e.offset.gap,"Axis title label gap")}for(const i of["offset","alongOffset"])if(!(i==="offset"&&f(e.offset))&&Object.hasOwn(e,i)&&!Number.isFinite(e[i]))throw new TypeError(`Axis title ${i} must be a finite number.`)}}function f(e){return s(e)&&e.from==="labels"}function h(e){return e?.inferredOffset===!0||f(e?.offset)}function m(e){return(e?.lines??[{text:e?.text}]).some(t=>typeof t.text=="string"&&t.text.length>0)}function w(e,t){if(!/[\r\n]/u.test(e))return;const i=t.baseline??"middle",r=b(e,{fontSize:t.fontSize,align:t.align??"center",baseline:i,...i==="middle"?{blockAlign:"middle"}:{}});if(["bottom","ideographic"].includes(i)){const l=(r.length-1)*t.fontSize*1.2;return r.map(n=>({...n,y:n.y-l}))}return r.length>1?r:void 0}function u(e){return e?.overlap==="allow"}function g(e,{allowReset:t=!1}={}){e===void 0||t&&e==="auto"||o(e,"Label flushThreshold")}export{m as axisLabelHasText,u as axisTitleAllowsLabelOverlap,h as axisTitleFollowsLabels,w as axisTitleTextLines,f as isLabelRelativeTitleOffset,g as validateAxisLabelFlushThreshold,c as validateAxisTextPlacement};
@@ -11,7 +11,7 @@ const MAX_CELLS = 1_000_000;
11
11
 
12
12
  const TRANSFORM_KEYS = Object.freeze([
13
13
  "type", "x", "y", "bins", "extent", "includeEmpty", "members", "as",
14
- "resolved"
14
+ "resolved", "assignment"
15
15
  ]);
16
16
  const AXIS_KEYS = Object.freeze(["x", "y"]);
17
17
  const FIELD_KEYS = Object.freeze(["x0", "x1", "y0", "y1", "count", "members"]);
@@ -180,6 +180,11 @@ export function validateBin2DTransform(transform) {
180
180
  }
181
181
  requireField(transform.x, "2D bin x field");
182
182
  requireField(transform.y, "2D bin y field");
183
+ if (transform.assignment !== undefined && transform.assignment !== "step") {
184
+ throw new TypeError(
185
+ 'Stored 2D bin assignment must be "step" or omitted.'
186
+ );
187
+ }
183
188
  validateBins(transform.bins);
184
189
  validateRequestedExtent(transform.extent);
185
190
  if (typeof transform.includeEmpty !== "boolean") {
@@ -201,10 +206,16 @@ export function normalizeBin2DTransform({
201
206
  y,
202
207
  bins,
203
208
  extent,
209
+ assignment = "edges",
204
210
  includeEmpty = false,
205
211
  members = false,
206
212
  as
207
213
  } = {}) {
214
+ if (assignment !== "edges" && assignment !== "step") {
215
+ throw new TypeError(
216
+ '2D bin assignment must be "edges" or "step".'
217
+ );
218
+ }
208
219
  const normalizedBins = normalizeBins(bins);
209
220
  const transform = {
210
221
  type: "bin2d",
@@ -214,7 +225,8 @@ export function normalizeBin2DTransform({
214
225
  extent: normalizeRequestedExtent(extent),
215
226
  includeEmpty,
216
227
  members,
217
- as: normalizeOutputFields(id, as, members)
228
+ as: normalizeOutputFields(id, as, members),
229
+ ...(assignment === "step" ? { assignment } : {})
218
230
  };
219
231
  validateBin2DTransform(transform);
220
232
  return cloneAndFreeze(transform);
@@ -266,6 +278,20 @@ function findBinIndex(value, edges) {
266
278
  return low;
267
279
  }
268
280
 
281
+ function findStepBinIndex(value, extent, count) {
282
+ if (count === 1 || value === extent[1]) return count - 1;
283
+ const [minimum, maximum] = extent;
284
+ const span = maximum - minimum;
285
+ const step = Number.isFinite(span)
286
+ ? span / count
287
+ : maximum / count - minimum / count;
288
+ const distance = value - minimum;
289
+ const position = Number.isFinite(distance)
290
+ ? distance / step
291
+ : value / step - minimum / step;
292
+ return Math.max(0, Math.min(count - 1, Math.floor(1e-14 + position)));
293
+ }
294
+
269
295
  export function deriveBin2DRows(rows, transform) {
270
296
  if (!Array.isArray(rows)) {
271
297
  throw new TypeError("2D bin source rows must be an array.");
@@ -304,8 +330,12 @@ export function deriveBin2DRows(rows, transform) {
304
330
  () => []
305
331
  );
306
332
  for (const item of eligible) {
307
- const xIndex = findBinIndex(item.row[transform.x], edges.x);
308
- const yIndex = findBinIndex(item.row[transform.y], edges.y);
333
+ const xIndex = transform.assignment === "step"
334
+ ? findStepBinIndex(item.row[transform.x], extent.x, transform.bins.x)
335
+ : findBinIndex(item.row[transform.x], edges.x);
336
+ const yIndex = transform.assignment === "step"
337
+ ? findStepBinIndex(item.row[transform.y], extent.y, transform.bins.y)
338
+ : findBinIndex(item.row[transform.y], edges.y);
309
339
  buckets[yIndex * transform.bins.x + xIndex].push(item.index);
310
340
  }
311
341
  const values = [];
@@ -340,3 +370,10 @@ export function requestedBin2DTransform(transform) {
340
370
  const { resolved: _resolved, ...requested } = transform;
341
371
  return cloneAndFreeze(requested);
342
372
  }
373
+
374
+ // Preserve caller-authored aliases; only the canonical generated count field
375
+ // receives a human-readable title when guide creation is deferred.
376
+ export function inferBin2DFieldTitle(transform, id, field) {
377
+ return transform?.type === 'bin2d' && transform.as.count === field &&
378
+ normalizeOutputFields(id, undefined, false).count === field ? 'Count' : field;
379
+ }