ggaction 0.0.21 → 0.0.23

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 (265) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +83 -90
  3. package/knowledge/action-card.schema.json +1 -1
  4. package/knowledge/action-cards.json +1 -1
  5. package/knowledge/intent-taxonomy.json +1 -1
  6. package/knowledge/mcp-resources.json +1 -1
  7. package/package.json +11 -4
  8. package/src/BasicChartProgram.js +1 -1
  9. package/src/accessibility.js +1 -0
  10. package/src/actions/boxPlots/components.js +1 -96
  11. package/src/actions/boxPlots/create.js +6 -1
  12. package/src/actions/boxPlots/edit.js +8 -0
  13. package/src/actions/boxPlots/materialize.js +3 -0
  14. package/src/actions/boxPlots/options.js +1 -1
  15. package/src/actions/canvas/actions.js +2 -1
  16. package/src/actions/canvas/index.js +3 -0
  17. package/src/actions/canvas/plotBorder.js +2 -1
  18. package/src/actions/canvas/plotOrder.js +14 -0
  19. package/src/actions/charts/area.js +1 -1
  20. package/src/actions/charts/bar.js +4 -4
  21. package/src/actions/charts/density.js +1 -1
  22. package/src/actions/charts/ecdf.js +1 -1
  23. package/src/actions/charts/endpoints.js +54 -5
  24. package/src/actions/charts/heatmap.js +1 -1
  25. package/src/actions/charts/histogram.js +1 -1
  26. package/src/actions/charts/horizon.js +1 -1
  27. package/src/actions/charts/index.js +2 -1
  28. package/src/actions/charts/interval-regression.js +3 -3
  29. package/src/actions/charts/line.js +1 -1
  30. package/src/actions/charts/radar.js +63 -3
  31. package/src/actions/charts/raincloud.js +37 -6
  32. package/src/actions/charts/rug-strip.js +2 -2
  33. package/src/actions/charts/scatter.js +3 -3
  34. package/src/actions/charts/text.js +1 -1
  35. package/src/actions/composition/actions.js +10 -3
  36. package/src/actions/coordinates/actions.js +2 -0
  37. package/src/actions/data/basic.js +6 -0
  38. package/src/actions/data/derived.js +6 -2
  39. package/src/actions/data/edit.js +22 -36
  40. package/src/actions/data/filter.js +1 -376
  41. package/src/actions/data/flatten.js +19 -0
  42. package/src/actions/data/index.js +6 -0
  43. package/src/actions/data/lookup.js +50 -0
  44. package/src/actions/data/raincloudFilter.js +56 -0
  45. package/src/actions/data/shared.js +10 -0
  46. package/src/actions/data/timeUnit.js +1 -1
  47. package/src/actions/distributions/revision.js +3 -1
  48. package/src/actions/encodings/angle.js +2 -1
  49. package/src/actions/encodings/appearance.js +38 -13
  50. package/src/actions/encodings/channels.js +14 -3
  51. package/src/actions/encodings/color/continuous.js +26 -10
  52. package/src/actions/encodings/color/index.js +50 -20
  53. package/src/actions/encodings/color/policy.js +57 -4
  54. package/src/actions/encodings/parallel.js +4 -0
  55. package/src/actions/encodings/position/policies/bar.js +10 -6
  56. package/src/actions/encodings/position/policies/line.js +1 -152
  57. package/src/actions/encodings/position/resolve.js +4 -1
  58. package/src/actions/encodings/ranged.js +46 -0
  59. package/src/actions/encodings/remove.js +1 -1
  60. package/src/actions/encodings/ruleAppearance.js +7 -6
  61. package/src/actions/encodings/shared.js +14 -2
  62. package/src/actions/encodings/stroke.js +20 -14
  63. package/src/actions/encodings/strokeDash.js +9 -11
  64. package/src/actions/encodings/text.js +30 -2
  65. package/src/actions/errorBars/edit.js +24 -4
  66. package/src/actions/facets/actions.js +1 -793
  67. package/src/actions/facets/derive.js +34 -5
  68. package/src/actions/facets/replay.js +1 -38
  69. package/src/actions/gradientPlots/components.js +21 -9
  70. package/src/actions/gradientPlots/create.js +4 -2
  71. package/src/actions/gradientPlots/edit.js +18 -1
  72. package/src/actions/gradientPlots/materialize.js +35 -36
  73. package/src/actions/gradientPlots/options.js +1 -1
  74. package/src/actions/gradientPlots/paint.js +13 -7
  75. package/src/actions/guides/applicability.js +2 -0
  76. package/src/actions/guides/axes/axes.js +39 -0
  77. package/src/actions/guides/axes/axis.js +9 -5
  78. package/src/actions/guides/axes/edit.js +11 -8
  79. package/src/actions/guides/axes/labels.js +24 -8
  80. package/src/actions/guides/axes/parallel/policy.js +3 -5
  81. package/src/actions/guides/axes/parallel/resolve.js +4 -3
  82. package/src/actions/guides/axes/policy.js +22 -0
  83. package/src/actions/guides/axes/tickGroups.js +4 -2
  84. package/src/actions/guides/axes/ticks.js +22 -4
  85. package/src/actions/guides/axes/titles.js +19 -6
  86. package/src/actions/guides/facadeAxes.js +3 -0
  87. package/src/actions/guides/grids/grid.js +3 -1
  88. package/src/actions/guides/grids/resolve.js +7 -11
  89. package/src/actions/guides/guides.js +21 -1
  90. package/src/actions/guides/legends/categorical/actions.js +1 -515
  91. package/src/actions/guides/legends/categorical/components.js +1 -4
  92. package/src/actions/guides/legends/categorical/layout.js +5 -2
  93. package/src/actions/guides/legends/categorical/options.js +4 -0
  94. package/src/actions/guides/legends/categorical/resolve.js +8 -3
  95. package/src/actions/guides/legends/categorical/symbols.js +3 -3
  96. package/src/actions/guides/legends/colorSize.js +2 -0
  97. package/src/actions/guides/legends/continuous/common.js +1 -1
  98. package/src/actions/guides/legends/continuous/gradient.js +1 -360
  99. package/src/actions/guides/legends/continuous/interval.js +1 -1
  100. package/src/actions/guides/legends/continuous/opacity.js +15 -1
  101. package/src/actions/guides/legends/edit.js +1 -1
  102. package/src/actions/guides/legends/focused.js +1 -1
  103. package/src/actions/guides/legends/lane.js +1 -1
  104. package/src/actions/guides/legends/sampling.js +1 -140
  105. package/src/actions/guides/legends/strokeWidth.js +3 -2
  106. package/src/actions/guides/legends/transition.js +19 -2
  107. package/src/actions/guides/polar/axes/facade.js +5 -4
  108. package/src/actions/guides/polar/axes/labels.js +5 -4
  109. package/src/actions/guides/polar/axes/titles.js +5 -4
  110. package/src/actions/guides/polar/resolve.js +2 -1
  111. package/src/actions/guides/tickValues.js +7 -7
  112. package/src/actions/marks/arc/actions.js +5 -7
  113. package/src/actions/marks/area/actions.js +9 -3
  114. package/src/actions/marks/bar/create.js +4 -2
  115. package/src/actions/marks/bar/edit.js +9 -6
  116. package/src/actions/marks/bar/materialize.js +5 -3
  117. package/src/actions/marks/dependentReferences.js +87 -0
  118. package/src/actions/marks/index.js +2 -0
  119. package/src/actions/marks/lifecycle.js +2 -1
  120. package/src/actions/marks/line/actions.js +24 -6
  121. package/src/actions/marks/line/materialize.js +7 -3
  122. package/src/actions/marks/point/create.js +5 -4
  123. package/src/actions/marks/point/edit.js +6 -3
  124. package/src/actions/marks/point/jitter.js +6 -6
  125. package/src/actions/marks/point/materialize.js +36 -39
  126. package/src/actions/marks/rect/actions.js +16 -4
  127. package/src/actions/marks/references.js +183 -35
  128. package/src/actions/marks/rule/actions.js +20 -9
  129. package/src/actions/marks/shared.js +2 -1
  130. package/src/actions/marks/text/actions.js +10 -3
  131. package/src/actions/marks/tick/actions.js +11 -2
  132. package/src/actions/primitives/editGraphics.js +28 -3
  133. package/src/actions/primitives/index.js +9 -0
  134. package/src/actions/primitives/semanticAction.js +13 -7
  135. package/src/actions/primitives/semanticValidation/layer.js +20 -0
  136. package/src/actions/primitives/semanticValidation/scale.js +8 -1
  137. package/src/actions/regression/create.js +6 -1
  138. package/src/actions/regression/edit.js +10 -1
  139. package/src/actions/scales/channels.js +1 -1
  140. package/src/actions/scales/consumers/common.js +19 -1
  141. package/src/actions/scales/consumers/families.js +2 -2
  142. package/src/actions/scales/consumers/seriesLayout.js +4 -3
  143. package/src/actions/scales/edit.js +11 -0
  144. package/src/actions/scales/editPolicy.js +5 -3
  145. package/src/actions/scales/preview.js +2 -1
  146. package/src/actions/scales/quantitativeColor.js +2 -4
  147. package/src/actions/selection/actions.js +10 -1
  148. package/src/actions/theme/reconcile.js +37 -9
  149. package/src/core/compositionState.js +26 -7
  150. package/src/core/datasetReferences.js +17 -0
  151. package/src/core/facetAxisPolicy.js +30 -0
  152. package/src/core/immutable.js +1 -0
  153. package/src/core/resourceReferences.js +5 -3
  154. package/src/core/validation.js +4 -0
  155. package/src/core/vocabulary.js +24 -2
  156. package/src/grammar/aggregate.js +1 -0
  157. package/src/grammar/arcs.js +1 -1
  158. package/src/grammar/areaSeries.js +1 -1
  159. package/src/grammar/axisTextPlacement.js +1 -1
  160. package/src/grammar/bars/policy.js +2 -2
  161. package/src/grammar/curveCommands.js +35 -2
  162. package/src/grammar/datasetDependencies.js +97 -0
  163. package/src/grammar/datasetSchema.js +32 -2
  164. package/src/grammar/facetHeaders.js +1 -0
  165. package/src/grammar/facets/dependencies.js +6 -2
  166. package/src/grammar/facets/guides.js +7 -5
  167. package/src/grammar/facets/legendIntent.js +6 -2
  168. package/src/grammar/facets/scales.js +27 -7
  169. package/src/grammar/flatten.js +85 -0
  170. package/src/grammar/histogram.js +13 -1
  171. package/src/grammar/horizon.js +1 -494
  172. package/src/grammar/jitter.js +2 -2
  173. package/src/grammar/legendSpacing.js +45 -0
  174. package/src/grammar/lineMissing.js +90 -0
  175. package/src/grammar/lineSeries.js +1 -376
  176. package/src/grammar/lookup.js +97 -0
  177. package/src/grammar/markFilter.js +4 -2
  178. package/src/grammar/markLabels.js +8 -3
  179. package/src/grammar/numeric.js +1 -1
  180. package/src/grammar/pathCommands.js +1 -1
  181. package/src/grammar/pathSeries.js +1 -109
  182. package/src/grammar/pointPolygon.js +81 -0
  183. package/src/grammar/pointShapes.js +15 -15
  184. package/src/grammar/rects.js +1 -1
  185. package/src/grammar/regression/derive.js +1 -195
  186. package/src/grammar/regression/parameters.js +7 -0
  187. package/src/grammar/scales/appearance.js +1 -1
  188. package/src/grammar/scales/color.js +1 -1
  189. package/src/grammar/scales/colorConsumers.js +1 -63
  190. package/src/grammar/scales/continuous.js +58 -1
  191. package/src/grammar/scales/definition.js +1 -1
  192. package/src/grammar/scales/ordinal.js +5 -2
  193. package/src/grammar/scales/policies.js +3 -3
  194. package/src/grammar/schemas/graphicBounds.js +40 -12
  195. package/src/grammar/schemas/graphicClip.js +23 -0
  196. package/src/grammar/schemas/graphicTree.js +16 -0
  197. package/src/grammar/statisticalReference.js +22 -0
  198. package/src/grammar/text.js +65 -1
  199. package/src/grammar/ticks.js +92 -1
  200. package/src/grammar/timeUnit.js +9 -3
  201. package/src/grammar/transformTopology.js +3 -1
  202. package/src/grammar/transforms.js +24 -0
  203. package/src/inspection.js +1 -495
  204. package/src/inspectionDescriptors.js +1 -1
  205. package/src/layout/canvas.js +1 -1
  206. package/src/layout/facetContentFlow.js +101 -0
  207. package/src/layout/facetHeaderTitles.js +1 -0
  208. package/src/layout/facetTopLane.js +29 -0
  209. package/src/layout/facets.js +1 -1
  210. package/src/layout/guideCollisions.js +9 -3
  211. package/src/layout/legendGroup.js +1 -1
  212. package/src/layout/legendItems.js +26 -10
  213. package/src/layout/legendSpacing.js +66 -0
  214. package/src/layout/plotDrawOrder.js +17 -0
  215. package/src/materialization/bars/histogram.js +8 -2
  216. package/src/materialization/bars/resolve.js +1 -1
  217. package/src/materialization/composition.js +1 -0
  218. package/src/materialization/compositionSnapshot.js +22 -2
  219. package/src/materialization/dataProvenance.js +1 -1
  220. package/src/materialization/facetContentBounds.js +96 -0
  221. package/src/materialization/facetGuides/layout.js +1 -1
  222. package/src/materialization/facetGuides/placement.js +39 -11
  223. package/src/materialization/facetGuides/preparation.js +43 -24
  224. package/src/materialization/facetHeaders.js +1 -1
  225. package/src/materialization/facetTopLane.js +25 -0
  226. package/src/materialization/facets.js +1 -1
  227. package/src/materialization/graphicHierarchy.js +30 -1
  228. package/src/materialization/graphicTranslation.js +2 -0
  229. package/src/materialization/guides/layout.js +1 -1
  230. package/src/materialization/guides/resources.js +1 -1
  231. package/src/materialization/legends.js +1 -1
  232. package/src/materialization/markClip.js +26 -0
  233. package/src/materialization/marks/capabilities.js +3 -2
  234. package/src/materialization/marks/grouping.js +7 -5
  235. package/src/materialization/marks/index.js +4 -2
  236. package/src/materialization/marks/policies.js +4 -0
  237. package/src/materialization/plotDrawOrder.js +44 -0
  238. package/src/materialization/rect.js +11 -3
  239. package/src/materialization/scaleGuideDependencies.js +13 -5
  240. package/src/materialization/selection/items/common.js +1 -1
  241. package/src/materialization/selection/items/path.js +1 -0
  242. package/src/materialization/selection/policies/index.js +2 -1
  243. package/src/materialization/text.js +10 -5
  244. package/src/mcp/cli.js +0 -0
  245. package/src/persistence/validation.js +114 -18
  246. package/src/persistence.js +4 -2
  247. package/src/renderers/canvas/index.js +16 -0
  248. package/src/renderers/canvas/isolated.js +11 -1
  249. package/src/renderers/canvas/native.js +7 -0
  250. package/src/renderers/canvas/scene.js +66 -0
  251. package/src/renderers/canvas/text.js +3 -2
  252. package/src/renderers/svg.js +1 -617
  253. package/src/renderers/svgSerialization.js +643 -0
  254. package/src/renderers/transitionScene.js +67 -0
  255. package/src/scene.js +89 -0
  256. package/src/selectors/axes.js +1 -1
  257. package/src/selectors/datasets.js +1 -0
  258. package/src/selectors/grids.js +9 -0
  259. package/src/selectors/markClip.js +28 -0
  260. package/src/selectors/raincloud.js +7 -0
  261. package/src/version.js +1 -1
  262. package/types/basic.d.ts +7 -0
  263. package/types/index.d.ts +8 -0
  264. package/types/program.d.ts +286 -70
  265. package/types/scene.d.ts +35 -0
@@ -6,6 +6,7 @@ import {
6
6
  resolveEffectiveThemeTokens,
7
7
  setThemeStateOverrides
8
8
  } from "./state.js";
9
+ import { isOwned } from "../../core/immutable.js";
9
10
  import { findLayer } from "../../selectors/layers.js";
10
11
  import { reconcileFollowingRegressions } from "../regression/follow.js";
11
12
 
@@ -285,6 +286,14 @@ function recolorGraphic(
285
286
  targetTokens,
286
287
  overrides
287
288
  ) {
289
+ if (id === `${program.compositionSpec?.id}-headers`) {
290
+ const headers = program.materializationConfigs.facets?.[program.compositionSpec.id]?.headers;
291
+ if (headers?.column?.title !== undefined || headers?.row?.title !== undefined) {
292
+ // Role titles and category labels have independently owned paint. Their
293
+ // reconciled header configs already materialized the complete collection.
294
+ return program;
295
+ }
296
+ }
288
297
  if (graphic.items === undefined) {
289
298
  const mapped = mapGraphicProperties(
290
299
  program,
@@ -663,18 +672,27 @@ function visitTrace(node, visitor) {
663
672
  for (const child of node.children ?? []) visitTrace(child, visitor);
664
673
  }
665
674
 
666
- function clearRemovedResources(overrides, node) {
675
+ // Completed immutable action branches never acquire new removals. Retain only
676
+ // their distinct reset prefixes so later themed edits do not rescan old facet
677
+ // materialization traces. Prefix deletion commutes; authored overrides are still
678
+ // applied in their original top-level action order.
679
+ const removedResources = new WeakMap();
680
+
681
+ function removedResourcePrefixes(node) {
682
+ const cached = removedResources.get(node);
683
+ if (cached !== undefined) return cached;
684
+ const prefixes = new Set();
667
685
  visitTrace(node, current => {
668
686
  if (current.op === "editGraphics" && current.args.remove === true &&
669
687
  typeof current.args.target === "string") {
670
- clearPrefix(overrides, `g:${current.args.target}.`);
688
+ prefixes.add(`g:${current.args.target}.`);
671
689
  }
672
690
  if (current.op !== "editSemantic" || current.args.remove !== true ||
673
691
  typeof current.args.property !== "string") return;
674
692
  const mark = /^layer\[([^\]]+)\]$/u.exec(current.args.property);
675
693
  if (mark !== null) {
676
- clearPrefix(overrides, `c:marks.${mark[1]}.`);
677
- clearPrefix(overrides, `g:${mark[1]}.`);
694
+ prefixes.add(`c:marks.${mark[1]}.`);
695
+ prefixes.add(`g:${mark[1]}.`);
678
696
  return;
679
697
  }
680
698
  const axis = /^guide\.axis\.([A-Za-z0-9_-]+)$/u
@@ -682,21 +700,28 @@ function clearRemovedResources(overrides, node) {
682
700
  if (axis !== null) {
683
701
  const prefix = axis[1] === "radius" ? "radial" : axis[1];
684
702
  const graphicPrefix = ["x", "y", "theta", "radius"].includes(axis[1]) ? `${prefix}Axis` : `axis_${axis[1]}__`;
685
- clearPrefix(overrides, `c:guides.axis.${axis[1]}.`);
686
- clearPrefix(overrides, `g:${graphicPrefix}`);
703
+ prefixes.add(`c:guides.axis.${axis[1]}.`);
704
+ prefixes.add(`g:${graphicPrefix}`);
687
705
  return;
688
706
  }
689
707
  const grid = /^guide\.grid\.([^\.]+)$/u.exec(current.args.property);
690
708
  if (grid !== null) {
691
- clearPrefix(overrides, `c:guides.grid.${grid[1]}.`);
692
- clearPrefix(overrides, `g:${grid[1]}Grid`);
709
+ prefixes.add(`c:guides.grid.${grid[1]}.`);
710
+ prefixes.add(`g:${grid[1]}Grid`);
693
711
  return;
694
712
  }
695
713
  const legend = /^guide\.legend\.([^\.]+)$/u.exec(current.args.property);
696
714
  if (legend !== null) {
697
- clearPrefix(overrides, `c:guides.legend.${legend[1]}.`);
715
+ prefixes.add(`c:guides.legend.${legend[1]}.`);
698
716
  }
699
717
  });
718
+ const result = [...prefixes];
719
+ if (isOwned(node)) removedResources.set(node, result);
720
+ return result;
721
+ }
722
+
723
+ function clearRemovedResources(overrides, node) {
724
+ for (const prefix of removedResourcePrefixes(node)) clearPrefix(overrides, prefix);
700
725
  }
701
726
 
702
727
  function clearLocalResets(overrides, program, node) {
@@ -970,6 +995,9 @@ function addFacetOverrides(overrides, program, args) {
970
995
  ["color", "fill"],
971
996
  ["fontFamily", "fontFamily"]
972
997
  ]) {
998
+ if (Object.hasOwn(args.title ?? {}, property)) {
999
+ overrides.add(`c:facets.${id}.headers.${owner}.title.${property}`);
1000
+ }
973
1001
  if (!Object.hasOwn(args, property)) continue;
974
1002
  overrides.add(`c:facets.${id}.headers.${owner}.${property}`);
975
1003
  overrides.add(`g:${id}-headers.${graphicProperty}`);
@@ -1,10 +1,17 @@
1
- import { isPlainObject } from "./immutable.js";
1
+ import { normalizeFacetAxes } from "./facetAxisPolicy.js";
2
+ import { isOwned, isPlainObject } from "./immutable.js";
2
3
  import { ownProgramState } from "./programState.js";
3
4
  import {
4
5
  FACET_SCALE_CHANNELS,
5
- FACET_SCALE_RESOLUTIONS
6
+ FACET_SCALE_RESOLUTIONS,
7
+ FACET_TOP_LANE_MODES,
8
+ DEFAULT_FACET_TOP_LANE
6
9
  } from "./vocabulary.js";
7
10
 
11
+ // Primitive graphics/trace edits retain both immutable owners. Validate their
12
+ // relationship once, rather than scanning every facet cell on every primitive.
13
+ const validatedCompositions = new WeakMap();
14
+
8
15
  function validateFacetSpec(compositionSpec) {
9
16
  if (compositionSpec.direction !== undefined) {
10
17
  throw new Error("Facet compositionSpec does not use direction.");
@@ -37,6 +44,7 @@ function validateFacetSpec(compositionSpec) {
37
44
  }
38
45
  const grid = compositionSpec.facet.grid;
39
46
  const repeat = compositionSpec.facet.repeat;
47
+ if (grid !== undefined && compositionSpec.spacing === "content") throw new Error("Content flow requires a wrapped facet.");
40
48
  if (grid !== undefined && repeat !== undefined) {
41
49
  throw new Error("Facet compositionSpec cannot combine grid and repeat recipes.");
42
50
  }
@@ -198,10 +206,10 @@ function validateFacetSpec(compositionSpec) {
198
206
  );
199
207
  }
200
208
  const guides = compositionSpec.facet.guides;
209
+ normalizeFacetAxes(guides?.axes, "each", { stored: true });
201
210
  if (
202
211
  !isPlainObject(guides) ||
203
212
  Object.keys(guides).some(key => !["axes", "legend"].includes(key)) ||
204
- !["each", "outer"].includes(guides.axes) ||
205
213
  (![false, "shared"].includes(guides.legend) && !isPlainObject(guides.legend))
206
214
  ) {
207
215
  throw new Error(
@@ -260,6 +268,7 @@ function validateLayout(compositionSpec) {
260
268
  }
261
269
 
262
270
  export function ownCompositionSpec(compositionSpec, children) {
271
+ if (validatedCompositions.get(compositionSpec)?.has(children)) return compositionSpec;
263
272
  const childIds = Object.keys(children);
264
273
  if (compositionSpec === undefined) {
265
274
  if (childIds.length > 0) {
@@ -272,7 +281,7 @@ export function ownCompositionSpec(compositionSpec, children) {
272
281
  }
273
282
  const allowed = [
274
283
  "id", "type", "direction", "children", "columns", "gap", "align",
275
- "padding", "facet", "spacing"
284
+ "padding", "facet", "spacing", "topLane"
276
285
  ];
277
286
  const unknown = Object.keys(compositionSpec).find(key => !allowed.includes(key));
278
287
  if (unknown !== undefined) {
@@ -282,8 +291,13 @@ export function ownCompositionSpec(compositionSpec, children) {
282
291
  throw new TypeError("compositionSpec.id must be a non-empty string.");
283
292
  }
284
293
  const facet = compositionSpec.type === "facet";
285
- if (compositionSpec.spacing !== undefined && (!facet || !["plot", "canvas"].includes(compositionSpec.spacing))) {
286
- throw new Error("compositionSpec.spacing requires a facet and canvas or plot spacing.");
294
+ if (compositionSpec.spacing !== undefined && (!facet || !["plot", "canvas", "content"].includes(compositionSpec.spacing))) {
295
+ throw new Error("compositionSpec.spacing requires a facet and canvas, plot, or content spacing.");
296
+ }
297
+ if (compositionSpec.topLane !== undefined && (!facet ||
298
+ !FACET_TOP_LANE_MODES.includes(compositionSpec.topLane) ||
299
+ compositionSpec.topLane === DEFAULT_FACET_TOP_LANE || compositionSpec.spacing !== "plot")) {
300
+ throw new Error('compositionSpec.topLane requires a facet with plot spacing and canonical shared intent.');
287
301
  }
288
302
  if (compositionSpec.type !== undefined && !facet) {
289
303
  throw new Error(`Unknown compositionSpec type "${compositionSpec.type}".`);
@@ -301,5 +315,10 @@ export function ownCompositionSpec(compositionSpec, children) {
301
315
  throw new Error("Concat compositionSpec does not accept facet properties.");
302
316
  }
303
317
  validateLayout(compositionSpec);
304
- return ownProgramState(compositionSpec);
318
+ const owned = ownProgramState(compositionSpec);
319
+ if (isOwned(children)) {
320
+ if (!validatedCompositions.has(owned)) validatedCompositions.set(owned, new WeakSet());
321
+ validatedCompositions.get(owned).add(children);
322
+ }
323
+ return owned;
305
324
  }
@@ -0,0 +1,17 @@
1
+ // Stored dataset reference paths have one owner across resource and replay policies.
2
+ export function datasetInputReferences(dataset) {
3
+ const refs = dataset.source === undefined ? [] : [{ id: dataset.source, path: ["source"] }];
4
+ if (dataset.transform?.[0]?.type === "lookup") {
5
+ refs.push({ id: dataset.transform[0].lookup.data, path: ["transform", 0, "lookup", "data"] });
6
+ }
7
+ return refs;
8
+ }
9
+
10
+ export function datasetInputIds(dataset) {
11
+ return [...new Set(datasetInputReferences(dataset).map(ref => ref.id))];
12
+ }
13
+
14
+ export function rebindTransformDatasetInputs(transform, replacements) {
15
+ if (transform.type !== "lookup" || !replacements.has(transform.lookup.data)) return transform;
16
+ return { ...transform, lookup: { ...transform.lookup, data: replacements.get(transform.lookup.data) } };
17
+ }
@@ -0,0 +1,30 @@
1
+ import { cloneAndFreeze, isPlainObject } from "./immutable.js";
2
+
3
+ export function normalizeFacetAxes(value, previous = "each", { stored = false } = {}) {
4
+ if (value === undefined && !stored) value = "each";
5
+ if (["each", "outer"].includes(value)) return value;
6
+ if (!isPlainObject(value) || Object.keys(value).length === 0 ||
7
+ Object.keys(value).some(key => !["x", "y"].includes(key)) ||
8
+ Object.values(value).some(mode => !["each", "outer"].includes(mode)) ||
9
+ stored && (!Object.hasOwn(value, "x") || !Object.hasOwn(value, "y"))) {
10
+ throw new Error('Facet axes must be each, outer, or a non-empty x/y policy object.');
11
+ }
12
+ return cloneAndFreeze({ x: value.x ?? facetAxisMode(previous, "x"), y: value.y ?? facetAxisMode(previous, "y") });
13
+ }
14
+
15
+ export function facetAxisMode(policy, channel) {
16
+ return typeof policy === "string" ? policy : policy[channel];
17
+ }
18
+
19
+ export function hasOuterFacetAxes(policy) {
20
+ return ["x", "y"].some(channel => facetAxisMode(policy, channel) === "outer");
21
+ }
22
+
23
+ export function validateFacetAxisPolicy(policy, family, repeat = false) {
24
+ if (family !== "cartesian" && (hasOuterFacetAxes(policy) || isPlainObject(policy))) {
25
+ throw new Error("Polar and Parallel facets do not support outer axes or per-channel axis policies.");
26
+ }
27
+ if (repeat && hasOuterFacetAxes(policy)) {
28
+ throw new Error("repeatCharts does not promote axes across different repeated fields.");
29
+ }
30
+ }
@@ -11,6 +11,7 @@ export function isPlainObject(value) {
11
11
 
12
12
  export function freezeOwned(value) {
13
13
  if (value !== null && typeof value === "object") {
14
+ if (ownedValues.has(value)) return value;
14
15
  Object.freeze(value);
15
16
  ownedValues.add(value);
16
17
  }
@@ -1,5 +1,6 @@
1
1
  import { freezeOwned, isPlainObject } from "./immutable.js";
2
2
  import { validateUserId } from "./identifiers.js";
3
+ import {datasetInputReferences} from "./datasetReferences.js";
3
4
 
4
5
  export const RESOURCE_KINDS = Object.freeze([
5
6
  "data", "scale", "coordinate", "mark", "selection"
@@ -71,8 +72,8 @@ export function collectSemanticDataReferences(program) {
71
72
  append(references, reference("data", layer.data, "layer", layer.id, ["data"]));
72
73
  }
73
74
  for (const dataset of program.semanticSpec.datasets) {
74
- append(references, reference(
75
- "data", dataset.source, "dataset", dataset.id, ["source"]
75
+ for(const input of datasetInputReferences(dataset))append(references,reference(
76
+ "data",input.id,"dataset",dataset.id,input.path
76
77
  ));
77
78
  }
78
79
  return freezeOwned(references);
@@ -167,7 +168,8 @@ export function collectMarkConfigReferences(program) {
167
168
  (root === "errorBandBoundary" && key === "owner") ||
168
169
  (parentPath.length === 0 && key === "boxSpanOwner") ||
169
170
  (root === "raincloudPlot" && (
170
- key === "ownerId" || parentPath[1] === "childIds" ||
171
+ key === "ownerId" ||
172
+ (parentPath[1] === "childIds" && config.raincloudPlot.ownedChildIds?.includes(value)) ||
171
173
  parentPath[1] === "ownedChildIds"
172
174
  ))
173
175
  ) {
@@ -133,3 +133,7 @@ export function validateTextStyle(config, label) {
133
133
  validateNonEmptyString(config.fontFamily, `${label} fontFamily`);
134
134
  validateFontWeight(config.fontWeight, `${label} fontWeight`);
135
135
  }
136
+ export function validateBoolean(value, label) {
137
+ if (typeof value !== "boolean") throw new TypeError(`${label} must be a boolean.`);
138
+ return value;
139
+ }
@@ -41,7 +41,9 @@ export const ENCODING_CHANNELS = Object.freeze([
41
41
  "group",
42
42
  "pathOrder",
43
43
  "opacity",
44
- "text"
44
+ "text",
45
+ "dx",
46
+ "dy"
45
47
  ]);
46
48
 
47
49
  const CARTESIAN_MARK_TYPES = Object.freeze([
@@ -118,7 +120,7 @@ export const POSITION_CHANNEL_DEFINITIONS = Object.freeze({
118
120
 
119
121
  export const SCALED_ENCODING_CHANNELS = Object.freeze(
120
122
  ENCODING_CHANNELS.filter(channel =>
121
- !["angle", "group", "pathOrder", "text"].includes(channel)
123
+ !["angle", "group", "pathOrder", "text", "dx", "dy"].includes(channel)
122
124
  )
123
125
  );
124
126
 
@@ -176,6 +178,9 @@ export const FACET_SCALE_RESOLUTIONS = Object.freeze([
176
178
  "independent"
177
179
  ]);
178
180
 
181
+ export const FACET_TOP_LANE_MODES = Object.freeze(["stacked", "shared"]);
182
+ export const DEFAULT_FACET_TOP_LANE = "stacked";
183
+
179
184
  export function getPositionChannelDefinition(channel) {
180
185
  return POSITION_CHANNEL_DEFINITIONS[channel];
181
186
  }
@@ -242,3 +247,20 @@ export const LEGEND_POSITIONS = Object.freeze(["left", "right", "top", "bottom",
242
247
  export const LEGEND_BLOCK_LAYOUTS = Object.freeze(["row", "column", "wrap"]);
243
248
 
244
249
  export const AXIS_RESOURCE_CHANNELS = Object.freeze(["x", "y", "theta", "radius", "parallel"]);
250
+
251
+ // Shared target policy for authoring and read-only action inspection.
252
+ const marks = (...types) => Object.freeze(types);
253
+ export const APPEARANCE_MARK_TYPES = Object.freeze({
254
+ encodeColor: marks('point', 'line', 'bar', 'area', 'arc', 'rect', 'text'),
255
+ encodeStroke: marks('point', 'line', 'area', 'bar', 'rect', 'arc', 'rule', 'tick'),
256
+ encodeOpacity: marks('point', 'rule', 'line', 'bar', 'rect'),
257
+ encodeStrokeWidth: marks('rule', 'line', 'rect'),
258
+ encodeStrokeDash: marks('line', 'rule'),
259
+ encodeSize: marks('point'),
260
+ encodeShape: marks('point'),
261
+ encodeAngle: marks('point', 'tick')
262
+ });
263
+
264
+ // 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"]);
266
+ // Generated default action methods: end
@@ -297,6 +297,7 @@ export function aggregateRows(rows, field, operation) {
297
297
 
298
298
  export function formatAggregateTitle(operation, field) {
299
299
  const aggregate = validateAggregate(operation);
300
+ if (aggregate === "count" && field === undefined) return "count";
300
301
  nonEmptyString(field, "Aggregate field");
301
302
  if (isScalarAggregate(aggregate)) return `${aggregate}(${field})`;
302
303
  if (aggregate.op === "quantile") {
@@ -1 +1 @@
1
- import{validateOptionObject as P,validateNonNegativeFinite as j}from"../core/validation.js";import{cloneAndFreeze as R,isPlainObject as B}from"../core/immutable.js";import{mapContinuousScaleValues as y,mapOrdinalPositionValues as M,readNominalField as b,readQuantitativeField as I}from"./scales/index.js";import{interpolateNumber as S,normalizedFiniteSum as _,stableFiniteSum as q}from"./numeric.js";function k(e){if(e?.mark?.type!=="arc")throw new Error("Arc derivation requires a semantic arc mark.");const t=e.encoding?.theta;if(t===void 0||!["quantitative","nominal","ordinal"].includes(t.fieldType))throw new Error(`Arc mark "${e.id}" requires quantitative or categorical theta.`);return t}function G(e,t){if(e?.type!=="linear"||!Array.isArray(e.range)||e.range.length!==2||!e.range.every(Number.isFinite)||e.range[0]===e.range[1])throw new Error(`${t} requires a resolved linear scale.`);return e}function L(e,t){if(e?.type!=="band"||!Array.isArray(e.domain)||!Array.isArray(e.range)||e.range.length!==2||!e.range.every(Number.isFinite)||e.range[0]===e.range[1]||!Number.isFinite(e.bandwidth)||e.bandwidth<=0)throw new Error(`${t} requires a resolved band scale.`);return e}function $(e,t){return t===void 0?e.map(()=>{}):b(e,t.field)}function F(e,t,r,n){if(!Array.isArray(e))throw new TypeError(`Arc theta ${n}s require rows.`);if(typeof t!="string"||t.length===0)throw new TypeError(`Arc theta ${n} must be a non-empty string.`);const s=e.map((u,a)=>{const i=u?.[t];if(!Number.isFinite(i)||i<0)throw new TypeError(`Arc theta ${n} field "${t}" must contain non-negative finite numbers at row ${a}.`);return i});if(s.every(u=>u===0))throw new Error(`Arc mark "${r}" theta ${n}s must have a positive total.`);return R(s)}function N(e,t,r="arc"){return F(e,t,r,"weight")}function V(e,t,r="arc"){return F(e,t,r,"value")}function Q(e,t,r){const n=e.reduce((c,d)=>c+d,0),s=Number.isFinite(n)?void 0:_(e,"Arc theta values");if(n===0||s?.total<=0)throw new Error("Arc theta values must have a positive total.");const u=t[1]-t[0];let a=t[0],i=0,o=0;return e.map((c,d)=>{const h=s===void 0?c:c/s.scale,v=i+h;s!==void 0&&(o+=Math.abs(i)>=Math.abs(h)?i-v+h:h-v+i),i=v;const p=a+c/n*u,l=d===e.length-1?t[1]:s===void 0&&Number.isFinite(p)?p:S(t[0],t[1],(i+o)/(s?.total??n));if(!Number.isFinite(l)||l===a)throw new RangeError(`Arc theta ${r(d)} cannot resolve a distinct finite sector.`);const m={startTheta:a,endTheta:l};return a=l,m})}function W(e,t,r){if(!Number.isFinite(t?.availableRadius)||t.availableRadius<0)throw new RangeError(`Arc mark "${e.id}" requires a finite available radius.`);return{innerRadius:U(r,t.availableRadius),outerRadius:t.availableRadius}}function D(e,t,r,n,s){const u=t.encoding.theta,a=u.fieldType==="quantitative";if(!a&&!["count","sum"].includes(u.aggregate))throw new Error(`Arc mark "${t.id}" requires count, sum, or radial layout.`);const i=a?V(e,u.field,t.id):u.aggregate==="count"?e.map(()=>1):N(e,u.weight,t.id),o=a?e.map((g,w)=>w):b(e,u.field),c=$(e,t.encoding?.color),d=new Map(z(o,i).map(g=>[g.key,g])),h=a?o:r.domain,v=new Set(h);for(const g of d.keys())if(!v.has(g))throw new Error(`Arc theta value "${g}" is outside the scale domain.`);const p=h.filter(g=>d.get(g)?.values.some(w=>w>0)),l=g=>a?`row ${p[g]}`:`group "${String(p[g])}"`,m=p.map((g,w)=>a?i[g]:q(d.get(g).values,`Arc theta ${l(w)} aggregate`)),f=Q(m,r.range,l),A=W(t,n,s);return p.map((g,w)=>{const E=d.get(g),T=[...new Set(E.sourceIndices.map(O=>c[O]))];if(T.length>1)throw new Error(`Aggregated arc theta group "${g}" must resolve to one color value.`);return{key:a?`${String(i[g])}:${g}`:g,theta:a?i[g]:g,count:E.sourceIndices.length,aggregateValue:m[w],color:T[0],...f[w],...A,sourceIndices:E.sourceIndices}})}function x(e,t,r){const n=t.aggregate==="count"?e.map(()=>1):I(e,t.field);return z(r,n)}function z(e,t){const r=new Map;for(const[n,s]of e.entries()){const u=r.get(s)??{key:s,sourceIndices:[],values:[]};u.sourceIndices.push(n),u.values.push(t[n]),r.set(s,u)}return[...r.values()]}function H(e,t){const r=k(t),n=t.encoding?.radius;if(!["nominal","ordinal"].includes(r.fieldType)||r.aggregate!==void 0||!["count","sum"].includes(n?.aggregate)||n.fieldType!=="quantitative")throw new Error("Measured Arc requires equal-angle categorical theta and count or sum radius.");if(n.aggregate==="count"&&Object.hasOwn(n,"field"))throw new Error("Measured count radius does not accept a field.");const s=b(e,r.field),u=$(e,t.encoding?.color),a=x(e,n,s);if(a.some(o=>o.values.some(c=>c<0)))throw new RangeError("Measured Arc radius inputs must be non-negative.");const i=a.map(o=>{const c=[...new Set(o.sourceIndices.map(d=>u[d]))];if(c.length>1)throw new Error(`Measured Arc category "${o.key}" must resolve to one color value.`);return{key:o.key,theta:o.key,count:o.sourceIndices.length,radius:q(o.values,"Measured Arc radius aggregate"),color:c[0],sourceIndices:o.sourceIndices}});if(!i.some(o=>o.radius>0))throw new Error("Measured Arc radius requires at least one positive category aggregate.");return R(i)}function J(e,t,r,n){const s=H(e,t),u=M(s.map(c=>c.theta),r),a=y(s.map(c=>c.radius),n),i=(Math.sign(r.step)||1)*r.bandwidth/2,o=new Map(s.map((c,d)=>[c.theta,{...c,startTheta:u[d]-i,endTheta:u[d]+i,innerRadius:n.range[0],outerRadius:a[d]}]));return r.domain.flatMap(c=>{const d=o.get(c);return d===void 0||d.radius===0?[]:[d]})}function K(e,t,r,n){const s=t.encoding.theta,u=t.encoding?.radius;if(u?.fieldType!=="quantitative")throw new Error(`Arc mark "${t.id}" requires quantitative radius.`);const a=b(e,s.field),i=I(e,u.field),o=$(e,t.encoding?.color),c=M(a,r),d=y(i,n),h=Math.min(...n.range),p=(Math.sign(r.step)||1)*r.bandwidth/2,l=new Map(r.domain.map(f=>[f,[]]));for(let f=0;f<e.length;f+=1){if(d[f]<=h)continue;const A=l.get(a[f]);if(A===void 0)throw new Error(`Arc theta value "${a[f]}" is outside the scale domain.`);A.push({key:`${String(a[f])}:${f}`,theta:a[f],radius:i[f],color:o[f],startTheta:c[f]-p,endTheta:c[f]+p,innerRadius:h,outerRadius:d[f],sourceIndices:[f]})}const m=[];for(const f of r.domain){const A=l.get(f);A.sort((g,w)=>w.outerRadius-g.outerRadius||g.sourceIndices[0]-w.sourceIndices[0]),m.push(...A)}return m}function re(e,t,{thetaScale:r,radiusScale:n,frame:s,innerRadius:u=0}={}){if(!Array.isArray(e))throw new TypeError("Arc derivation requires rows.");const a=k(t);C(u);const i=n?.radialMapping!==void 0,o=a.fieldType==="quantitative",c=(o&&!i?G:L)(r,`Arc mark "${t.id}" theta`);let d=i?J(e,t,c,n):o||["count","sum"].includes(a.aggregate)?D(e,t,c,s,u):K(e,t,c,n);if(n!==void 0&&!i&&(o||a.aggregate!==void 0)){const h=X(e,t),v=y(h.map(l=>l.radius),n),p=new Map(h.map((l,m)=>[l.sourceIndices[0],{radius:l.radius,outerRadius:v[m]}]));d=d.map(l=>{const m=p.get(l.sourceIndices[0]);if(m===void 0||!Number.isFinite(m.outerRadius)||m.outerRadius<=l.innerRadius)throw new RangeError("Arc outer radius must exceed inner radius.");return{...l,...m}})}return R({sectors:d})}function C(e){if(B(e)){if(P(e,["unit","value"],"Arc innerRadius"),e.unit!=="px")throw new RangeError("Arc innerRadius unit must be px.");return R({unit:"px",value:j(e.value,"Arc innerRadius")})}if(!Number.isFinite(e)||e<0||e>=1)throw new RangeError("Arc innerRadius must be from 0 (inclusive) to 1 (exclusive).");return e}function U(e,t){const r=C(e);if(typeof r=="number")return r*t;if(r.value>=t)throw new RangeError("Arc pixel innerRadius must be smaller than its outer radius.");return r.value}function X(e,t){const r=t.encoding?.theta,n=t.encoding?.radius;if(r===void 0)return[];const s=r.fieldType==="quantitative";if(s?n.aggregate!==void 0:!["count","sum"].includes(r.aggregate))throw new Error("Grouped arc radius requires aggregated categorical theta.");const u=s?V(e,r.field,t.id):r.aggregate==="count"?e.map(()=>1):N(e,r.weight,t.id),a=s?e.map((i,o)=>o):b(e,r.field);return x(e,n,a).filter(i=>i.sourceIndices.some(o=>u[o]>0)).map(({sourceIndices:i,values:o})=>{if(n.aggregate===void 0&&new Set(o).size!==1)throw new Error("Arc radius requires one value per theta group or explicit aggregation.");return{sourceIndices:i,radius:n.aggregate===void 0?o[0]:q(o,"Arc radius aggregate")}})}export{re as deriveArcSectors,H as deriveMeasuredArcValues,X as deriveProportionalArcRadiusValues,C as normalizeArcInnerRadius,V as readArcThetaValues,N as readArcThetaWeights,U as resolveArcInnerRadius};
1
+ import{validateOptionObject as P,validateNonNegativeFinite as j}from"../core/validation.js";import{cloneAndFreeze as R,isPlainObject as S}from"../core/immutable.js";import{mapContinuousScaleValues as q,mapOrdinalPositionValues as M,readNominalField as b,readQuantitativeField as I,readScaleField as B}from"./scales/index.js";import{interpolateNumber as U,normalizedFiniteSum as _,stableFiniteSum as y}from"./numeric.js";function k(e){if(e?.mark?.type!=="arc")throw new Error("Arc derivation requires a semantic arc mark.");const t=e.encoding?.theta;if(t===void 0||!["quantitative","nominal","ordinal"].includes(t.fieldType))throw new Error(`Arc mark "${e.id}" requires quantitative or categorical theta.`);return t}function G(e,t){if(e?.type!=="linear"||!Array.isArray(e.range)||e.range.length!==2||!e.range.every(Number.isFinite)||e.range[0]===e.range[1])throw new Error(`${t} requires a resolved linear scale.`);return e}function L(e,t){if(e?.type!=="band"||!Array.isArray(e.domain)||!Array.isArray(e.range)||e.range.length!==2||!e.range.every(Number.isFinite)||e.range[0]===e.range[1]||!Number.isFinite(e.bandwidth)||e.bandwidth<=0)throw new Error(`${t} requires a resolved band scale.`);return e}function E(e,t){return t===void 0?e.map(()=>{}):B(e,t.field,t.fieldType??"nominal",{temporalUnit:t.temporalUnit})}function ne(e,t){const r=E(e,t.encoding?.color),n=t.encoding?.theta;if(!(n?.aggregate!==void 0||t.encoding?.radius?.aggregate!==void 0)||n?.field===void 0)return r;const s=b(e,n.field),o=new Map;return s.forEach((i,a)=>{const u=r[a];if(o.has(i)&&!Object.is(o.get(i),u))throw new Error(`Arc category "${i}" must resolve to one color value.`);o.set(i,u)}),[...o.values()]}function F(e,t,r,n){if(!Array.isArray(e))throw new TypeError(`Arc theta ${n}s require rows.`);if(typeof t!="string"||t.length===0)throw new TypeError(`Arc theta ${n} must be a non-empty string.`);const c=e.map((s,o)=>{const i=s?.[t];if(!Number.isFinite(i)||i<0)throw new TypeError(`Arc theta ${n} field "${t}" must contain non-negative finite numbers at row ${o}.`);return i});if(c.every(s=>s===0))throw new Error(`Arc mark "${r}" theta ${n}s must have a positive total.`);return R(c)}function N(e,t,r="arc"){return F(e,t,r,"weight")}function V(e,t,r="arc"){return F(e,t,r,"value")}function Q(e,t,r){const n=e.reduce((u,d)=>u+d,0),c=Number.isFinite(n)?void 0:_(e,"Arc theta values");if(n===0||c?.total<=0)throw new Error("Arc theta values must have a positive total.");const s=t[1]-t[0];let o=t[0],i=0,a=0;return e.map((u,d)=>{const h=c===void 0?u:u/c.scale,v=i+h;c!==void 0&&(a+=Math.abs(i)>=Math.abs(h)?i-v+h:h-v+i),i=v;const m=o+u/n*s,f=d===e.length-1?t[1]:c===void 0&&Number.isFinite(m)?m:U(t[0],t[1],(i+a)/(c?.total??n));if(!Number.isFinite(f)||f===o)throw new RangeError(`Arc theta ${r(d)} cannot resolve a distinct finite sector.`);const p={startTheta:o,endTheta:f};return o=f,p})}function W(e,t,r){if(!Number.isFinite(t?.availableRadius)||t.availableRadius<0)throw new RangeError(`Arc mark "${e.id}" requires a finite available radius.`);return{innerRadius:X(r,t.availableRadius),outerRadius:t.availableRadius}}function D(e,t,r,n,c){const s=t.encoding.theta,o=s.fieldType==="quantitative";if(!o&&!["count","sum"].includes(s.aggregate))throw new Error(`Arc mark "${t.id}" requires count, sum, or radial layout.`);const i=o?V(e,s.field,t.id):s.aggregate==="count"?e.map(()=>1):N(e,s.weight,t.id),a=o?e.map((g,w)=>w):b(e,s.field),u=E(e,t.encoding?.color),d=new Map(z(a,i).map(g=>[g.key,g])),h=o?a:r.domain,v=new Set(h);for(const g of d.keys())if(!v.has(g))throw new Error(`Arc theta value "${g}" is outside the scale domain.`);const m=h.filter(g=>d.get(g)?.values.some(w=>w>0)),f=g=>o?`row ${m[g]}`:`group "${String(m[g])}"`,p=m.map((g,w)=>o?i[g]:y(d.get(g).values,`Arc theta ${f(w)} aggregate`)),l=Q(p,r.range,f),A=W(t,n,c);return m.map((g,w)=>{const $=d.get(g),T=[...new Set($.sourceIndices.map(O=>u[O]))];if(T.length>1)throw new Error(`Aggregated arc theta group "${g}" must resolve to one color value.`);return{key:o?`${String(i[g])}:${g}`:g,theta:o?i[g]:g,count:$.sourceIndices.length,aggregateValue:p[w],color:T[0],...l[w],...A,sourceIndices:$.sourceIndices}})}function x(e,t,r){const n=t.aggregate==="count"?e.map(()=>1):I(e,t.field);return z(r,n)}function z(e,t){const r=new Map;for(const[n,c]of e.entries()){const s=r.get(c)??{key:c,sourceIndices:[],values:[]};s.sourceIndices.push(n),s.values.push(t[n]),r.set(c,s)}return[...r.values()]}function H(e,t){const r=k(t),n=t.encoding?.radius;if(!["nominal","ordinal"].includes(r.fieldType)||r.aggregate!==void 0||!["count","sum"].includes(n?.aggregate)||n.fieldType!=="quantitative")throw new Error("Measured Arc requires equal-angle categorical theta and count or sum radius.");if(n.aggregate==="count"&&Object.hasOwn(n,"field"))throw new Error("Measured count radius does not accept a field.");const c=b(e,r.field),s=E(e,t.encoding?.color),o=x(e,n,c);if(o.some(a=>a.values.some(u=>u<0)))throw new RangeError("Measured Arc radius inputs must be non-negative.");const i=o.map(a=>{const u=[...new Set(a.sourceIndices.map(d=>s[d]))];if(u.length>1)throw new Error(`Measured Arc category "${a.key}" must resolve to one color value.`);return{key:a.key,theta:a.key,count:a.sourceIndices.length,radius:y(a.values,"Measured Arc radius aggregate"),color:u[0],sourceIndices:a.sourceIndices}});if(!i.some(a=>a.radius>0))throw new Error("Measured Arc radius requires at least one positive category aggregate.");return R(i)}function J(e,t,r,n){const c=H(e,t),s=M(c.map(u=>u.theta),r),o=q(c.map(u=>u.radius),n),i=(Math.sign(r.step)||1)*r.bandwidth/2,a=new Map(c.map((u,d)=>[u.theta,{...u,startTheta:s[d]-i,endTheta:s[d]+i,innerRadius:n.range[0],outerRadius:o[d]}]));return r.domain.flatMap(u=>{const d=a.get(u);return d===void 0||d.radius===0?[]:[d]})}function K(e,t,r,n){const c=t.encoding.theta,s=t.encoding?.radius;if(s?.fieldType!=="quantitative")throw new Error(`Arc mark "${t.id}" requires quantitative radius.`);const o=b(e,c.field),i=I(e,s.field),a=E(e,t.encoding?.color),u=M(o,r),d=q(i,n),h=Math.min(...n.range),m=(Math.sign(r.step)||1)*r.bandwidth/2,f=new Map(r.domain.map(l=>[l,[]]));for(let l=0;l<e.length;l+=1){if(d[l]<=h)continue;const A=f.get(o[l]);if(A===void 0)throw new Error(`Arc theta value "${o[l]}" is outside the scale domain.`);A.push({key:`${String(o[l])}:${l}`,theta:o[l],radius:i[l],color:a[l],startTheta:u[l]-m,endTheta:u[l]+m,innerRadius:h,outerRadius:d[l],sourceIndices:[l]})}const p=[];for(const l of r.domain){const A=f.get(l);A.sort((g,w)=>w.outerRadius-g.outerRadius||g.sourceIndices[0]-w.sourceIndices[0]),p.push(...A)}return p}function ie(e,t,{thetaScale:r,radiusScale:n,frame:c,innerRadius:s=0}={}){if(!Array.isArray(e))throw new TypeError("Arc derivation requires rows.");const o=k(t);C(s);const i=n?.radialMapping!==void 0,a=o.fieldType==="quantitative",u=(a&&!i?G:L)(r,`Arc mark "${t.id}" theta`);let d=i?J(e,t,u,n):a||["count","sum"].includes(o.aggregate)?D(e,t,u,c,s):K(e,t,u,n);if(n!==void 0&&!i&&(a||o.aggregate!==void 0)){const h=Y(e,t),v=q(h.map(f=>f.radius),n),m=new Map(h.map((f,p)=>[f.sourceIndices[0],{radius:f.radius,outerRadius:v[p]}]));d=d.map(f=>{const p=m.get(f.sourceIndices[0]);if(p===void 0||!Number.isFinite(p.outerRadius)||p.outerRadius<=f.innerRadius)throw new RangeError("Arc outer radius must exceed inner radius.");return{...f,...p}})}return R({sectors:d})}function C(e){if(S(e)){if(P(e,["unit","value"],"Arc innerRadius"),e.unit!=="px")throw new RangeError("Arc innerRadius unit must be px.");return R({unit:"px",value:j(e.value,"Arc innerRadius")})}if(!Number.isFinite(e)||e<0||e>=1)throw new RangeError("Arc innerRadius must be from 0 (inclusive) to 1 (exclusive).");return e}function X(e,t){const r=C(e);if(typeof r=="number")return r*t;if(r.value>=t)throw new RangeError("Arc pixel innerRadius must be smaller than its outer radius.");return r.value}function Y(e,t){const r=t.encoding?.theta,n=t.encoding?.radius;if(r===void 0)return[];const c=r.fieldType==="quantitative";if(c?n.aggregate!==void 0:!["count","sum"].includes(r.aggregate))throw new Error("Grouped arc radius requires aggregated categorical theta.");const s=c?V(e,r.field,t.id):r.aggregate==="count"?e.map(()=>1):N(e,r.weight,t.id),o=c?e.map((i,a)=>a):b(e,r.field);return x(e,n,o).filter(i=>i.sourceIndices.some(a=>s[a]>0)).map(({sourceIndices:i,values:a})=>{if(n.aggregate===void 0&&new Set(a).size!==1)throw new Error("Arc radius requires one value per theta group or explicit aggregation.");return{sourceIndices:i,radius:n.aggregate===void 0?a[0]:y(a,"Arc radius aggregate")}})}export{ie as deriveArcSectors,H as deriveMeasuredArcValues,Y as deriveProportionalArcRadiusValues,C as normalizeArcInnerRadius,ne as readArcColorValues,V as readArcThetaValues,N as readArcThetaWeights,X as resolveArcInnerRadius};
@@ -1 +1 @@
1
- import{cloneAndFreeze as C}from"../core/immutable.js";import{readNominalField as z,readQuantitativeField as b,readTemporalField as I}from"./scales/index.js";import{layoutSeriesPartition as B,validateColorLayout as F}from"./seriesLayout.js";import{stableOrderPathValues as L}from"./pathOrder.js";import{deriveCategoricalDensitySeries as N}from"./categoricalDensity.js";import{validatePathSeriesAppearance as P}from"./pathSeries.js";import{readAreaEndpoint as j,validateAreaEndpointPair as J}from"./areaEndpoints.js";import{validateGeneratedItemLimit as K}from"../core/validation.js";function G(n){const e=[];for(const a of n)e[a.index]=a;return e}function ne(n,e,a={}){if(e?.mark?.type!=="area")throw new Error("Area series derivation requires a semantic area mark.");const{x:p,y:d,x2:l,y2:k}=e.encoding??{},u=["quantitative","temporal","nominal","ordinal"].includes(p?.fieldType)&&d?.fieldType==="quantitative"&&k?.fieldType==="quantitative"&&l===void 0,m=["quantitative","temporal","nominal","ordinal"].includes(d?.fieldType)&&p?.fieldType==="quantitative"&&l?.fieldType==="quantitative"&&k===void 0;if(u===m)throw new Error(`Area mark "${e.id}" requires exactly one quantitative x/x2 or y/y2 range and one quantitative, temporal, or categorical independent position.`);const w=P(n,e),c=u?"vertical":"horizontal",f=u?p:d,x=["nominal","ordinal"].includes(f.fieldType),y=x?z(n,f.field):f.fieldType==="temporal"?I(n,f.field,f.temporalUnit):b(n,f.field);if(Object.hasOwn(f,"datum"))throw new Error("Area independent position requires a field.");const v=u?d:p,o=u?k:l;J(v,o);const r=e.mark.missing??"error",g=j(n,v,r),O=j(n,o,r),s=x?a[u?"xDomain":"yDomain"]??[...new Set(y)]:void 0,t=new Map(s?.map((i,h)=>[i,h]));if(x&&y.some(i=>!t.has(i)))throw new Error("Area categorical domain must contain every observed position.");const E=e.encoding?.pathOrder,$=E===void 0?void 0:b(n,E.field),D=new Map;for(let i=0;i<n.length;i+=1){const h=w.map(q=>n[i][q]),M=JSON.stringify(h),T=D.get(M)??{key:Object.fromEntries(w.map((q,V)=>[q,h[V]])),values:[]};T.values.push(u?{x:y[i],y:g[i],y2:O[i],...r==="break"?{sourceIndex:i}:{},...$===void 0?{}:{pathOrder:$[i]}}:{x:g[i],x2:O[i],...r==="break"?{sourceIndex:i}:{},y:y[i],...$===void 0?{}:{pathOrder:$[i]}}),D.set(M,T)}if(D.size===0)throw new Error(`Area mark "${e.id}" has no values.`);Q(D,{layer:e,vertical:u,primary:v,secondary:o});const A=[...D.values()].flatMap(i=>{const h=u?"x":"y",M=E===void 0?i.values.sort((T,q)=>x?t.get(T[h])-t.get(q[h]):T[h]-q[h]):L(i.values.map(({pathOrder:T,...q})=>q),i.values.map(T=>T.pathOrder),E.order);if(r==="break"){const T=[];let q=[];const V=()=>{q.length>=2&&T.push({key:i.key,sourceIndices:q.map(S=>S.sourceIndex),values:q.map(({sourceIndex:S,...U})=>U)}),q=[]};for(const S of M)(u?S.y==null||S.y2==null:S.x==null||S.x2==null)?V():q.push(S);return V(),T}if(M.length<2)throw new Error(`Area series on mark "${e.id}" requires at least two points.`);return{key:i.key,values:M}});if(A.length===0)throw new Error(`Area mark "${e.id}" has no valid segment with at least two points.`);return C({orientation:c,xValues:A.flatMap(i=>i.values.flatMap(h=>u?[h.x]:[h.x,h.x2])),yValues:A.flatMap(i=>i.values.flatMap(h=>u?[h.y,h.y2]:[h.y])),series:A})}function te(n,e,a={}){if(e?.mark?.type!=="area")throw new Error("Centered area series derivation requires an area mark.");const{x:p,y:d,x2:l,y2:k,group:u,color:m}=e.encoding??{};if(!["quantitative","temporal","nominal","ordinal"].includes(p?.fieldType)||d?.fieldType!=="quantitative"||d?.stack!=="center"||l!==void 0||k!==void 0)throw new Error(`Centered area mark "${e.id}" requires one x field and one quantitative y field without ranged endpoints.`);if(u?.fieldType!=="nominal")throw new Error(`Centered area mark "${e.id}" requires a nominal group encoding.`);if(m!==void 0&&m.fieldType!=="quantitative"&&m.field!==u.field)throw new Error(`Centered area color on mark "${e.id}" must match its group field.`);P(n,e);const w=["nominal","ordinal"].includes(p.fieldType),c=w?z(n,p.field):p.fieldType==="temporal"?I(n,p.field,p.temporalUnit):b(n,p.field),f=b(n,d.field),x=z(n,u.field),y=[],v=new Set,o=new Map;for(let s=0;s<n.length;s+=1){const t=x[s];o.has(t)||(y.push(t),o.set(t,new Map));const E=o.get(t);if(E.has(c[s]))throw new Error(`Centered area mark "${e.id}" has duplicate ${u.field}/${p.field} rows.`);E.set(c[s],f[s]),v.add(c[s])}const r=w?(a.xDomain??[...v]).filter(s=>v.has(s)):[...v].sort((s,t)=>s-t);if(r.length!==v.size)throw new Error("Area categorical domain must contain every observed position.");if(y.length===0||r.length<2)throw new Error(`Centered area mark "${e.id}" requires at least two aligned positions.`);for(const[s,t]of o)if(t.size!==r.length||r.some(E=>!t.has(E)))throw new Error(`Centered area series "${String(s)}" requires one aligned value at every x position.`);const g=y.map(()=>[]);for(const s of r){const t=y.map(A=>o.get(A).get(s)),E=B(t,"center"),$=G(E);let D=E[0]?.start??0;for(let A=0;A<y.length;A+=1){const i=$[A];g[A].push({x:s,y:t[A],lower:i?.start??D,upper:i?.end??D}),i!==void 0&&(D=i.end)}}const O=y.map((s,t)=>({key:{[u.field]:s,...m===void 0||m.fieldType==="quantitative"?{}:{[m.field]:s}},values:g[t]}));return C({mode:"y-center",orientation:"vertical",xValues:r,yValues:O.flatMap(s=>s.values.flatMap(t=>[t.lower,t.upper])),series:O})}function re(n,e,a){if(e?.mark?.type!=="area")throw new Error("Density area derivation requires a semantic area mark.");if(e.encoding?.pathOrder!==void 0)throw new Error(`Density area mark "${e.id}" does not support path order.`);if(a?.type!=="density"||!Array.isArray(a.as))throw new Error(`Area mark "${e.id}" requires density provenance.`);if(a.placement?.type==="category")return N(n,e,a);const{x:p,y:d,group:l}=e.encoding??{};if(p?.fieldType!=="quantitative"||d?.fieldType!=="quantitative")throw new Error(`Density area mark "${e.id}" requires quantitative x and y encodings.`);const[k,u]=a.as,m=p.field===k&&d.field===u?"y-density":p.field===u&&d.field===k?"x-density":void 0;if(m===void 0)throw new Error(`Density area mark "${e.id}" must encode its value and density fields.`);if(a.groupBy===void 0){if(l!==void 0)throw new Error(`Ungrouped density area mark "${e.id}" cannot encode group.`)}else if(l?.field!==a.groupBy||l.fieldType!=="nominal")throw new Error(`Density area mark "${e.id}" must group by "${a.groupBy}".`);const w=b(n,p.field),c=b(n,d.field),f=a.groupBy===void 0?n.map(()=>{}):z(n,a.groupBy),x=new Map;for(let o=0;o<n.length;o+=1){const r=f[o],g=x.get(r)??{key:a.groupBy===void 0?{}:{[a.groupBy]:r},values:[]};g.values.push({x:w[o],y:c[o]}),x.set(r,g)}if(x.size===0)throw new Error(`Density area mark "${e.id}" has no values.`);const y=m==="y-density"?"x":"y",v=[...x.values()].map(o=>{const r=o.values.sort((g,O)=>g[y]-O[y]);if(r.length<2)throw new Error(`Density area series on mark "${e.id}" requires at least two points.`);return{key:o.key,values:r}});return C({mode:m,series:v})}function ie(n,e="overlay"){if(F(e),e==="group")throw new Error('Density area series do not support "group" layout.');if(n?.mode!=="y-density"){if(e==="overlay")return n;throw new Error(`Area layout "${e}" currently requires vertical density series.`)}const a=n.series[0]?.values.length??0;if(a===0||n.series.some(d=>d.values.length!==a))throw new Error("Density area layout requires aligned non-empty samples.");const p=n.series.map(()=>[]);for(let d=0;d<a;d+=1){const l=n.series[0].values[d].x,k=n.series.map(c=>{if(c.values[d].x!==l)throw new Error("Density area layout requires one shared sample grid.");return c.values[d].y}),u=B(k,e),m=G(u);let w=e==="center"?u[0]?.start??0:0;for(let c=0;c<n.series.length;c+=1){const f=m[c];p[c].push({x:l,lower:f?.start??w,upper:f?.end??w}),f!==void 0&&["stack","fill","center"].includes(e)&&(w=f.end)}}return C({mode:n.mode,series:n.series.map((d,l)=>({key:d.key,values:p[l]}))})}function Q(n,{layer:e,vertical:a,primary:p,secondary:d}){const l=e.layout?.mode??"overlay";if(F(l),l==="overlay")return;if(l==="group"||l==="center"&&!a)throw new Error(`Area layout "${l}" is incompatible with this orientation.`);const k=Object.hasOwn(p,"datum"),u=Object.hasOwn(d,"datum");if(k===u||(k?p.datum:d.datum)!==0)throw new Error("Stacked area requires one value field and one zero datum endpoint.");const m=a?"x":"y",w=a?"y":"x",c=`${w}2`,f=k?c:w,x=[...n.values()],y=x.map(o=>{const r=new Map;for(const g of o.values){if(r.has(g[m]))throw new Error("Area layout requires unique group/position rows.");r.set(g[m],g)}return r}),v=[...new Set(x.flatMap(o=>o.values.map(r=>r[m])))].sort((o,r)=>o-r);if(K(v.length*x.length,"Area layout cell count"),y.some(o=>o.size!==v.length||v.some(r=>!o.has(r))))throw new Error("Area layout requires an aligned row for every group and position.");for(const o of v){const r=y.map(t=>t.get(o));if(r.some(t=>t[f]==null)){for(const t of r)t[w]=null,t[c]=null;continue}const g=B(r.map(t=>t[f]),l),O=new Map(g.map(t=>[t.index,t]));let s=l==="center"?g[0]?.start??0:0;r.forEach((t,E)=>{const $=O.get(E);t[w]=$?.start??(l==="diverging"?0:s),t[c]=$?.end??(l==="diverging"?0:s),$!==void 0&&l!=="diverging"&&(s=$.end)})}}export{ne as deriveAreaSeries,te as deriveCenteredAreaSeries,re as deriveDensityAreaSeries,ie as layoutDensityAreaSeries,Q as layoutRawAreaGroups};
1
+ import{cloneAndFreeze as z}from"../core/immutable.js";import{readNominalField as B,readQuantitativeField as V,readTemporalField as P}from"./scales/index.js";import{layoutSeriesPartition as F,validateColorLayout as j}from"./seriesLayout.js";import{stableOrderPathValues as J}from"./pathOrder.js";import{deriveCategoricalDensitySeries as K}from"./categoricalDensity.js";import{validatePathSeriesAppearance as G}from"./pathSeries.js";import{readAreaEndpoint as N,validateAreaEndpointPair as Q}from"./areaEndpoints.js";import{validateGeneratedItemLimit as R}from"../core/validation.js";function U(n){const e=[];for(const a of n)e[a.index]=a;return e}function re(n,e,a={}){if(e?.mark?.type!=="area")throw new Error("Area series derivation requires a semantic area mark.");const{x:p,y:u,x2:l,y2:k}=e.encoding??{},o=["quantitative","temporal","nominal","ordinal"].includes(p?.fieldType)&&u?.fieldType==="quantitative"&&k?.fieldType==="quantitative"&&l===void 0,y=["quantitative","temporal","nominal","ordinal"].includes(u?.fieldType)&&p?.fieldType==="quantitative"&&l?.fieldType==="quantitative"&&k===void 0;if(o===y)throw new Error(`Area mark "${e.id}" requires exactly one quantitative x/x2 or y/y2 range and one quantitative, temporal, or categorical independent position.`);const v=G(n,e),c=o?"vertical":"horizontal",f=o?p:u,x=["nominal","ordinal"].includes(f.fieldType),m=x?B(n,f.field):f.fieldType==="temporal"?P(n,f.field,f.temporalUnit):V(n,f.field);if(Object.hasOwn(f,"datum"))throw new Error("Area independent position requires a field.");const w=o?u:p,i=o?k:l;Q(w,i);const r=e.mark.missing??"error",g=N(n,w,r),O=N(n,i,r),s=x?a[o?"xDomain":"yDomain"]??[...new Set(m)]:void 0,t=new Map(s?.map((d,h)=>[d,h]));if(x&&m.some(d=>!t.has(d)))throw new Error("Area categorical domain must contain every observed position.");const E=e.encoding?.pathOrder,$=E===void 0?void 0:V(n,E.field),D=new Map;for(let d=0;d<n.length;d+=1){const h=v.map(q=>n[d][q]),M=JSON.stringify(h),T=D.get(M)??{key:Object.fromEntries(v.map((q,C)=>[q,h[C]])),values:[]};T.values.push(o?{x:m[d],y:g[d],y2:O[d],...r==="break"?{sourceIndex:d}:{},...$===void 0?{}:{pathOrder:$[d]}}:{x:g[d],x2:O[d],...r==="break"?{sourceIndex:d}:{},y:m[d],...$===void 0?{}:{pathOrder:$[d]}}),D.set(M,T)}if(D.size===0)throw new Error(`Area mark "${e.id}" has no values.`);_(D,{layer:e,vertical:o,primary:w,secondary:i});const A=[...D.values()].flatMap(d=>{const h=o?"x":"y",M=E===void 0?d.values.sort((T,q)=>x?t.get(T[h])-t.get(q[h]):T[h]-q[h]):J(d.values.map(({pathOrder:T,...q})=>q),d.values.map(T=>T.pathOrder),E.order);if(r==="break"){const T=[];let q=[];const C=()=>{q.length>=2&&T.push({key:d.key,sourceIndices:q.map(S=>S.sourceIndex),values:q.map(({sourceIndex:S,...L})=>L)}),q=[]};for(const S of M)(o?S.y==null||S.y2==null:S.x==null||S.x2==null)?C():q.push(S);return C(),T}if(M.length<2)throw new Error(`Area series on mark "${e.id}" requires at least two points.`);return{key:d.key,values:M}}),b=A.length===0&&r==="break",I=[...g,...O].filter(Number.isFinite);return z({orientation:c,xValues:b?o?m:I:A.flatMap(d=>d.values.flatMap(h=>o?[h.x]:[h.x,h.x2])),yValues:b?o?I:m:A.flatMap(d=>d.values.flatMap(h=>o?[h.y,h.y2]:[h.y])),series:A})}function ie(n,e,a={}){if(e?.mark?.type!=="area")throw new Error("Centered area series derivation requires an area mark.");const{x:p,y:u,x2:l,y2:k,group:o,color:y}=e.encoding??{};if(!["quantitative","temporal","nominal","ordinal"].includes(p?.fieldType)||u?.fieldType!=="quantitative"||u?.stack!=="center"||l!==void 0||k!==void 0)throw new Error(`Centered area mark "${e.id}" requires one x field and one quantitative y field without ranged endpoints.`);if(o?.fieldType!=="nominal")throw new Error(`Centered area mark "${e.id}" requires a nominal group encoding.`);if(y!==void 0&&y.fieldType!=="quantitative"&&y.field!==o.field)throw new Error(`Centered area color on mark "${e.id}" must match its group field.`);G(n,e);const v=["nominal","ordinal"].includes(p.fieldType),c=v?B(n,p.field):p.fieldType==="temporal"?P(n,p.field,p.temporalUnit):V(n,p.field),f=V(n,u.field),x=B(n,o.field),m=[],w=new Set,i=new Map;for(let s=0;s<n.length;s+=1){const t=x[s];i.has(t)||(m.push(t),i.set(t,new Map));const E=i.get(t);if(E.has(c[s]))throw new Error(`Centered area mark "${e.id}" has duplicate ${o.field}/${p.field} rows.`);E.set(c[s],f[s]),w.add(c[s])}const r=v?(a.xDomain??[...w]).filter(s=>w.has(s)):[...w].sort((s,t)=>s-t);if(r.length!==w.size)throw new Error("Area categorical domain must contain every observed position.");if(m.length===0||r.length<2)throw new Error(`Centered area mark "${e.id}" requires at least two aligned positions.`);for(const[s,t]of i)if(t.size!==r.length||r.some(E=>!t.has(E)))throw new Error(`Centered area series "${String(s)}" requires one aligned value at every x position.`);const g=m.map(()=>[]);for(const s of r){const t=m.map(A=>i.get(A).get(s)),E=F(t,"center"),$=U(E);let D=E[0]?.start??0;for(let A=0;A<m.length;A+=1){const b=$[A];g[A].push({x:s,y:t[A],lower:b?.start??D,upper:b?.end??D}),b!==void 0&&(D=b.end)}}const O=m.map((s,t)=>({key:{[o.field]:s,...y===void 0||y.fieldType==="quantitative"?{}:{[y.field]:s}},values:g[t]}));return z({mode:"y-center",orientation:"vertical",xValues:r,yValues:O.flatMap(s=>s.values.flatMap(t=>[t.lower,t.upper])),series:O})}function oe(n,e,a){if(e?.mark?.type!=="area")throw new Error("Density area derivation requires a semantic area mark.");if(e.encoding?.pathOrder!==void 0)throw new Error(`Density area mark "${e.id}" does not support path order.`);if(a?.type!=="density"||!Array.isArray(a.as))throw new Error(`Area mark "${e.id}" requires density provenance.`);if(a.placement?.type==="category")return K(n,e,a);const{x:p,y:u,group:l}=e.encoding??{};if(p?.fieldType!=="quantitative"||u?.fieldType!=="quantitative")throw new Error(`Density area mark "${e.id}" requires quantitative x and y encodings.`);const[k,o]=a.as,y=p.field===k&&u.field===o?"y-density":p.field===o&&u.field===k?"x-density":void 0;if(y===void 0)throw new Error(`Density area mark "${e.id}" must encode its value and density fields.`);if(a.groupBy===void 0){if(l!==void 0)throw new Error(`Ungrouped density area mark "${e.id}" cannot encode group.`)}else if(l?.field!==a.groupBy||l.fieldType!=="nominal")throw new Error(`Density area mark "${e.id}" must group by "${a.groupBy}".`);const v=V(n,p.field),c=V(n,u.field),f=a.groupBy===void 0?n.map(()=>{}):B(n,a.groupBy),x=new Map;for(let i=0;i<n.length;i+=1){const r=f[i],g=x.get(r)??{key:a.groupBy===void 0?{}:{[a.groupBy]:r},values:[]};g.values.push({x:v[i],y:c[i]}),x.set(r,g)}if(x.size===0)throw new Error(`Density area mark "${e.id}" has no values.`);const m=y==="y-density"?"x":"y",w=[...x.values()].map(i=>{const r=i.values.sort((g,O)=>g[m]-O[m]);if(r.length<2)throw new Error(`Density area series on mark "${e.id}" requires at least two points.`);return{key:i.key,values:r}});return z({mode:y,series:w})}function ae(n,e="overlay"){if(j(e),e==="group")throw new Error('Density area series do not support "group" layout.');if(n?.mode!=="y-density"){if(e==="overlay")return n;throw new Error(`Area layout "${e}" currently requires vertical density series.`)}const a=n.series[0]?.values.length??0;if(a===0||n.series.some(u=>u.values.length!==a))throw new Error("Density area layout requires aligned non-empty samples.");const p=n.series.map(()=>[]);for(let u=0;u<a;u+=1){const l=n.series[0].values[u].x,k=n.series.map(c=>{if(c.values[u].x!==l)throw new Error("Density area layout requires one shared sample grid.");return c.values[u].y}),o=F(k,e),y=U(o);let v=e==="center"?o[0]?.start??0:0;for(let c=0;c<n.series.length;c+=1){const f=y[c];p[c].push({x:l,lower:f?.start??v,upper:f?.end??v}),f!==void 0&&["stack","fill","center"].includes(e)&&(v=f.end)}}return z({mode:n.mode,series:n.series.map((u,l)=>({key:u.key,values:p[l]}))})}function _(n,{layer:e,vertical:a,primary:p,secondary:u}){const l=e.layout?.mode??"overlay";if(j(l),l==="overlay")return;if(l==="group"||l==="center"&&!a)throw new Error(`Area layout "${l}" is incompatible with this orientation.`);const k=Object.hasOwn(p,"datum"),o=Object.hasOwn(u,"datum");if(k===o||(k?p.datum:u.datum)!==0)throw new Error("Stacked area requires one value field and one zero datum endpoint.");const y=a?"x":"y",v=a?"y":"x",c=`${v}2`,f=k?c:v,x=[...n.values()],m=x.map(i=>{const r=new Map;for(const g of i.values){if(r.has(g[y]))throw new Error("Area layout requires unique group/position rows.");r.set(g[y],g)}return r}),w=[...new Set(x.flatMap(i=>i.values.map(r=>r[y])))].sort((i,r)=>i-r);if(R(w.length*x.length,"Area layout cell count"),m.some(i=>i.size!==w.length||w.some(r=>!i.has(r))))throw new Error("Area layout requires an aligned row for every group and position.");for(const i of w){const r=m.map(t=>t.get(i));if(r.some(t=>t[f]==null)){for(const t of r)t[v]=null,t[c]=null;continue}const g=F(r.map(t=>t[f]),l),O=new Map(g.map(t=>[t.index,t]));let s=l==="center"?g[0]?.start??0:0;r.forEach((t,E)=>{const $=O.get(E);t[v]=$?.start??(l==="diverging"?0:s),t[c]=$?.end??(l==="diverging"?0:s),$!==void 0&&l!=="diverging"&&(s=$.end)})}}export{re as deriveAreaSeries,ie as deriveCenteredAreaSeries,oe as deriveDensityAreaSeries,ae as layoutDensityAreaSeries,_ as layoutRawAreaGroups};
@@ -1 +1 @@
1
- import{resolveTextLines as o}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,"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=o(e,{fontSize:i.fontSize,align:i.align??"center",baseline:t,...t==="middle"?{blockAlign:"middle"}:{}});if(["bottom","ideographic"].includes(t)){const s=(n.length-1)*i.fontSize*1.2;return n.map(r=>({...r,y:r.y-s}))}return n.length>1?n:void 0}export{f as axisTitleTextLines,a as validateAxisTextPlacement};
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};
@@ -32,7 +32,7 @@ export function resolveBarOrientation(layer) {
32
32
  const x = layer.encoding?.x;
33
33
  const y = layer.encoding?.y;
34
34
  if (hasNumericCenters(layer)) return layer.mark.orientation;
35
- if (x?.bin !== undefined && y?.aggregate === "count") {
35
+ if (x?.bin !== undefined && isAggregate(y?.aggregate)) {
36
36
  return BAR_ORIENTATIONS.vertical;
37
37
  }
38
38
  if (isBarCategoryEncoding(x) && y?.fieldType === "quantitative" && layer.encoding?.y2?.fieldType === "quantitative") {
@@ -68,7 +68,7 @@ export function resolveBarGrain(layer) {
68
68
 
69
69
  if (
70
70
  x?.bin !== undefined &&
71
- y?.aggregate === "count"
71
+ isAggregate(y?.aggregate)
72
72
  ) {
73
73
  return BAR_GRAINS.histogram;
74
74
  }
@@ -9,6 +9,7 @@ export const CURVE_INTERPOLATIONS = Object.freeze([
9
9
  "basis",
10
10
  "cardinal",
11
11
  "monotone",
12
+ "monotone-steffen",
12
13
  "natural"
13
14
  ]);
14
15
 
@@ -16,6 +17,7 @@ const SMOOTH_CURVES = new Set([
16
17
  "basis",
17
18
  "cardinal",
18
19
  "monotone",
20
+ "monotone-steffen",
19
21
  "natural"
20
22
  ]);
21
23
 
@@ -196,8 +198,38 @@ function monotoneTangents(points) {
196
198
  return tangents;
197
199
  }
198
200
 
199
- function buildMonotoneCommands(points) {
200
- const tangents = monotoneTangents(points);
201
+ function steffenTangents(points) {
202
+ const widths = points.slice(1).map((point, index) => point.x - points[index].x);
203
+ const direction = Math.sign(widths.find(width => width !== 0) ?? 0);
204
+ if (widths.some(width => width !== 0 && Math.sign(width) !== direction)) {
205
+ throw new Error("Steffen curve requires monotonic x values.");
206
+ }
207
+ const slopes = widths.map((width, i) =>
208
+ (points[i + 1].y - points[i].y) / (width || (direction < 0 ? -0 : 0)));
209
+ const tangents = Array(points.length);
210
+ for (let i = 1; i < points.length - 1; i += 1) {
211
+ const a = slopes[i - 1], b = slopes[i];
212
+ const mean = (a * widths[i] + b * widths[i - 1]) / (widths[i - 1] + widths[i]);
213
+ // A zero-width turn has no finite secant estimate; its Hermite limit is flat.
214
+ const sign = value => value < 0 ? -1 : 1;
215
+ const slope = (sign(a) + sign(b)) * Math.min(Math.abs(a), Math.abs(b), Math.abs(mean) / 2);
216
+ tangents[i] = Number.isNaN(slope) && (widths[i - 1] === 0 || widths[i] === 0) ? 0 : slope;
217
+ }
218
+ tangents[0] = widths[0] === 0 ? tangents[1] : (3 * slopes[0] - tangents[1]) / 2;
219
+ tangents[points.length - 1] = widths.at(-1) === 0 ? tangents.at(-2) :
220
+ (3 * slopes.at(-1) - tangents.at(-2)) / 2;
221
+ return tangents;
222
+ }
223
+
224
+ function buildSteffenCommands(points) {
225
+ const distinct = points.filter((point, i) => i === 0 ||
226
+ point.x !== points[i - 1].x || point.y !== points[i - 1].y);
227
+ if (distinct.length === 1) return ownCommands([{ op: "M", ...distinct[0] }]);
228
+ if (distinct.length === 2) return buildLinearPathCommands(distinct);
229
+ return buildMonotoneCommands(distinct, steffenTangents(distinct));
230
+ }
231
+
232
+ function buildMonotoneCommands(points, tangents = monotoneTangents(points)) {
201
233
  const commands = [{ op: "M", x: points[0].x, y: points[0].y }];
202
234
  for (let index = 1; index < points.length; index += 1) {
203
235
  const previous = points[index - 1];
@@ -274,6 +306,7 @@ function buildNaturalCommands(points) {
274
306
  }
275
307
 
276
308
  function buildValidatedCurvePathCommands(points, curve, tension) {
309
+ if (curve === "monotone-steffen") return buildSteffenCommands(points);
277
310
  if (curve === "linear" ||
278
311
  (SMOOTH_CURVES.has(curve) &&
279
312
  curve !== "monotone" &&