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
@@ -1,63 +1 @@
1
- import { BAR_GRAINS, resolveBarChannels, resolveBarGrain } from "../bars/policy.js";
2
- import { validateAggregate, validateAggregateFieldType } from "../aggregate.js";
3
- import { isDiscretizedColorScaleType } from "./types.js";
4
-
5
- export function validateContinuousColorConsumer(layer, encoding, scale, {
6
- inferAggregate = false,
7
- channel = "color"
8
- } = {}) {
9
- const kind = layer.mark?.type;
10
- if (["log", "pow", "sqrt", "symlog"].includes(scale.type) && encoding.fieldType !== "quantitative") {
11
- throw new Error(`Scale type "${scale.type}" requires quantitative color.`);
12
- }
13
- if (channel === "stroke") {
14
- if (!["point", "line", "area", "bar", "rect", "arc", "rule", "tick"].includes(kind)) {
15
- throw new Error("Continuous stroke requires a supported graphical mark consumer.");
16
- }
17
- if (!['quantitative', 'temporal'].includes(encoding.fieldType) ||
18
- (isDiscretizedColorScaleType(scale.type) && encoding.fieldType !== "quantitative")) {
19
- throw new Error(`Scale type "${scale.type}" has an incompatible stroke field type.`);
20
- }
21
- if (encoding.aggregate !== undefined) {
22
- throw new Error("Continuous stroke does not support aggregate.");
23
- }
24
- if (Object.hasOwn(scale, "unknown") && kind !== "point") {
25
- throw new Error("Continuous stroke scale unknown currently requires a row-owned point mark.");
26
- }
27
- return undefined;
28
- }
29
- if (!["point", "bar", "rect", "text", "area"].includes(kind)) {
30
- throw new Error("Continuous color requires a Point, aggregate Bar, Rect, Text, or quantitative Area consumer.");
31
- }
32
- if (!["quantitative", "temporal"].includes(encoding.fieldType) ||
33
- (isDiscretizedColorScaleType(scale.type) && encoding.fieldType !== "quantitative")) {
34
- throw new Error(`Scale type "${scale.type}" has an incompatible color field type.`);
35
- }
36
- if (Object.hasOwn(scale, "unknown") && kind !== "point") {
37
- throw new Error("Continuous color scale unknown currently requires a row-owned point mark.");
38
- }
39
- if (kind === "area" && encoding.fieldType !== "quantitative") {
40
- throw new Error("Continuous area color requires a quantitative field.");
41
- }
42
- if (kind !== "bar") {
43
- if (encoding.aggregate !== undefined) throw new Error(`${kind} continuous color does not support aggregate.`);
44
- return undefined;
45
- }
46
- if (["nominal", "ordinal"].includes(layer.encoding?.color?.fieldType)) {
47
- throw new Error("Continuous bar color cannot replace an existing nominal color layout.");
48
- }
49
- if (encoding.fieldType !== "quantitative") {
50
- throw new Error("Aggregate bar color currently requires a quantitative field.");
51
- }
52
- if (resolveBarGrain(layer) !== BAR_GRAINS.aggregate) {
53
- throw new Error("Continuous bar color requires a complete categorical aggregate bar.");
54
- }
55
- const measure = layer.encoding?.[resolveBarChannels(layer).measure];
56
- const aggregate = encoding.aggregate ?? (inferAggregate && measure?.field === encoding.field ? measure.aggregate : undefined);
57
- if (aggregate === undefined) {
58
- throw new Error("Continuous bar color requires aggregate when its field differs from the measure field.");
59
- }
60
- validateAggregate(aggregate);
61
- validateAggregateFieldType(aggregate, encoding.fieldType);
62
- return aggregate;
63
- }
1
+ import{isNominalValue as d,readNominalField as s}from"./fields.js";import{BAR_GRAINS as f,resolveBarChannels as p,resolveBarGrain as c}from"../bars/policy.js";import{validateAggregate as w,validateAggregateFieldType as g}from"../aggregate.js";import{isDiscretizedColorScaleType as l}from"./types.js";function m(o,e){if(o.mark?.type!=="tick")return;const{color:r,stroke:t}=o.encoding??{};if(r!==void 0&&t!==void 0||e==="color"&&t!==void 0||e==="stroke"&&r!==void 0)throw new Error("Tick color and stroke cannot coexist; remove the existing encoding before switching channels.");if(r!==void 0&&!["quantitative","temporal"].includes(r.fieldType))throw new Error("Tick color requires a quantitative or temporal field.")}function v(o,e,r,{inferAggregate:t=!1,channel:n="color"}={}){const i=o.mark?.type;if(m(o,n),["log","pow","sqrt","symlog"].includes(r.type)&&e.fieldType!=="quantitative")throw new Error(`Scale type "${r.type}" requires quantitative color.`);if(n==="stroke"){if(!["point","line","area","bar","rect","arc","rule","tick"].includes(i))throw new Error("Continuous stroke requires a supported graphical mark consumer.");if(!["quantitative","temporal"].includes(e.fieldType)||l(r.type)&&e.fieldType!=="quantitative")throw new Error(`Scale type "${r.type}" has an incompatible stroke field type.`);if(e.aggregate!==void 0)throw new Error("Continuous stroke does not support aggregate.");if(Object.hasOwn(r,"unknown")&&i!=="point")throw new Error("Continuous stroke scale unknown currently requires a row-owned point mark.");return}if(!["point","line","bar","rect","text","area","arc","tick"].includes(i))throw new Error("Continuous color requires a Point, Line, aggregate Bar, Rect, Text, Arc, Tick, or quantitative Area consumer.");if(!["quantitative","temporal"].includes(e.fieldType)||l(r.type)&&e.fieldType!=="quantitative")throw new Error(`Scale type "${r.type}" has an incompatible color field type.`);if(Object.hasOwn(r,"unknown")&&i!=="point")throw new Error("Continuous color scale unknown currently requires a row-owned point mark.");if(i==="area"&&e.fieldType!=="quantitative")throw new Error("Continuous area color requires a quantitative field.");if(i!=="bar"){if(e.aggregate!==void 0)throw new Error(`${i} continuous color does not support aggregate.`);return}if(["nominal","ordinal"].includes(o.encoding?.color?.fieldType))throw new Error("Continuous bar color cannot replace an existing nominal color layout.");if(e.fieldType!=="quantitative")throw new Error("Aggregate bar color currently requires a quantitative field.");if(c(o)!==f.aggregate)throw new Error("Continuous bar color requires a complete categorical aggregate bar.");const u=o.encoding?.[p(o).measure],a=e.aggregate??(t&&u?.field===e.field?u.aggregate:void 0);if(a===void 0)throw new Error("Continuous bar color requires aggregate when its field differs from the measure field.");return w(a),g(a,e.fieldType),a}function C(o,e,r,t="color"){if(!Object.hasOwn(r,"unknown")||o.mark?.type==="point")return!1;const n=o.encoding??{};if(t==="strokeDash"){if(!["line","rule"].includes(o.mark?.type)||!["nominal","ordinal"].includes(e.fieldType)||r.type!=="ordinal"||n.parallel!==void 0||n.theta!==void 0||n.radius!==void 0||[n.x,n.y,n.x2,n.y2].some(i=>i?.aggregate!==void 0||i?.bin!==void 0))throw new Error("StrokeDash unknown requires a Cartesian line or rule with categorical ordinal mapping.");return!1}if(o.mark?.type!=="line"||t!=="color"||!["nominal","ordinal"].includes(e.fieldType)||r.type!=="ordinal"||!Array.isArray(r.domain)||r.domain.length===0||n.parallel!==void 0||n.theta!==void 0||n.radius!==void 0||[n.x,n.y].some(i=>i?.aggregate!==void 0||i?.bin!==void 0))throw new Error("Categorical unknown requires a row-owned point or a Cartesian line with explicit ordinal domain.");return!0}function T(o,e){return s([],e),o.map((r,t)=>{const n=r[e];if(n==null||d(n))return n;throw new TypeError(`Field "${e}" must contain a nominal value at row ${t}.`)})}export{T as readCategoricalLineColor,C as validateCategoricalColorConsumer,v as validateContinuousColorConsumer,m as validateTickColorChannels};
@@ -5,6 +5,7 @@ import {
5
5
  cleanNumericValue,
6
6
  interpolateNumber,
7
7
  inverseLerp,
8
+ nearestNumericIncrement,
8
9
  niceNumericStep,
9
10
  normalizeNumericRange
10
11
  } from "../numeric.js";
@@ -76,6 +77,59 @@ export function niceLinearDomain(domain) {
76
77
  return cloneAndFreeze([lower, upper]);
77
78
  }
78
79
 
80
+ export function normalizeScaleNice(type, value) {
81
+ if (typeof value === "boolean") return value;
82
+ if (!isPlainObject(value)) {
83
+ throw new TypeError("Scale nice must be a boolean or a linear count object.");
84
+ }
85
+ if (type !== "linear") {
86
+ throw new Error("Count-based nice requires a linear position scale.");
87
+ }
88
+ if (Reflect.ownKeys(value).length !== 1 || !Object.hasOwn(value, "count")) {
89
+ throw new TypeError("Count-based nice requires exactly one count property.");
90
+ }
91
+ const count = value.count;
92
+ if (!Number.isSafeInteger(count) || count <= 0) {
93
+ throw new RangeError("Nice count must be a positive safe integer.");
94
+ }
95
+ return cloneAndFreeze({ count });
96
+ }
97
+
98
+ export function niceLinearCountDomain(domain, count) {
99
+ const input = validatePair(domain, "Nice linear domain");
100
+ normalizeScaleNice("linear", { count });
101
+ const reversed = input[0] > input[1];
102
+ let lower = Math.min(...input), upper = Math.max(...input);
103
+ let stable = lower === upper;
104
+ for (let iteration = 0; iteration < 10 && lower !== upper; iteration++) {
105
+ let start = lower, end = upper, scale = 1;
106
+ let increment = nearestNumericIncrement(end - start, count);
107
+ if (increment === null) {
108
+ const normalized = normalizeNumericRange(lower, upper);
109
+ ({ start, end, scale } = normalized);
110
+ increment = nearestNumericIncrement(normalized.span, count);
111
+ if (increment === null) break;
112
+ }
113
+ let nextLower = (increment < 0
114
+ ? Math.floor(start * -increment) / -increment
115
+ : Math.floor(start / increment) * increment) * scale;
116
+ let nextUpper = (increment < 0
117
+ ? Math.ceil(end * -increment) / -increment
118
+ : Math.ceil(end / increment) * increment) * scale;
119
+ // At numeric limits an outward step may not be representable. Retain the
120
+ // observed endpoint rather than overflow, move inward, or invent a value.
121
+ if (!Number.isFinite(nextLower) || nextLower > lower) nextLower = lower;
122
+ if (!Number.isFinite(nextUpper) || nextUpper < upper) nextUpper = upper;
123
+ if (nextLower === lower && nextUpper === upper) { stable = true; break; }
124
+ lower = nextLower; upper = nextUpper;
125
+ }
126
+ // A density such as one interval across zero may have no fixed outward
127
+ // grid. Do not return an arbitrarily enlarged result after the loop cap.
128
+ if (!stable) return input;
129
+ const result = [lower === 0 ? 0 : lower, upper === 0 ? 0 : upper];
130
+ return cloneAndFreeze(reversed ? result.reverse() : result);
131
+ }
132
+
79
133
  export function resolveContinuousDomain({ domain, values, type, nice, zero }) {
80
134
  const explicit = domain !== "auto";
81
135
  let resolved = resolveScaleDomain(domain, values);
@@ -86,7 +140,10 @@ export function resolveContinuousDomain({ domain, values, type, nice, zero }) {
86
140
  Math.max(0, resolved[1])
87
141
  ]);
88
142
  }
89
- if (nice === true) {
143
+ if (nice !== undefined && typeof nice === "object") {
144
+ const policy = normalizeScaleNice(type, nice);
145
+ resolved = niceLinearCountDomain(resolved, policy.count);
146
+ } else if (nice === true) {
90
147
  resolved = type === "time"
91
148
  ? niceTimeDomain(resolved)
92
149
  : niceLinearDomain(resolved);
@@ -1 +1 @@
1
- import{validateMeasuredRadiusScale as D}from"./radial.js";import{normalizeColorInterpolationOptions as F,validateContinuousColorInterpolation as N,validateSequentialMidpoint as T}from"./color.js";import{isTransformedScaleType as k}from"./mapping.js";import{normalizeTransformParameters as q,validateTransformedDomain as A}from"./transformed.js";import{isContinuousColorScaleType as R,validateScalePropertyForType as b}from"./types.js";const p=Object.freeze(["nice","zero","clamp","reverse"]),B=Object.freeze(["base","exponent","constant"]);function f(i,l,n,e,o){return Object.hasOwn(l,e)?l[e]:!o&&Object.hasOwn(i,e)?i[e]:n[e]}function _(i,l,n,e,o){const m=Object.hasOwn(n,"padding");if(m&&(Object.hasOwn(n,"paddingInner")||Object.hasOwn(n,"paddingOuter")))throw new Error("Scale padding cannot be combined with paddingInner or paddingOuter.");if(m&&(!Number.isFinite(n.padding)||n.padding<0||n.padding>=1))throw new RangeError("Scale padding must be from 0 (inclusive) to 1 (exclusive).");const d=m?{...n,paddingInner:n.padding,paddingOuter:n.padding}:n,g=f(l,d,e,"paddingInner",o)??0,w=f(l,d,e,"paddingOuter",o)??0,c=f(l,d,e,"align",o)??.5;if(!Number.isFinite(g)||g<0||g>=1)throw new RangeError("Scale paddingInner must be from 0 (inclusive) to 1 (exclusive).");if(!Number.isFinite(w)||w<0)throw new RangeError("Scale paddingOuter must be non-negative and finite.");if(!Number.isFinite(c)||c<0||c>1)throw new RangeError("Scale align must be between 0 and 1.");Object.assign(i,{paddingInner:g,paddingOuter:w,align:c})}function $(i,l,n,e,o){const m=f(l,n,e,"padding",o)??.5,d=f(l,n,e,"align",o)??.5;if(!Number.isFinite(m)||m<0)throw new RangeError("Scale padding must be non-negative and finite.");if(!Number.isFinite(d)||d<0||d>1)throw new RangeError("Scale align must be between 0 and 1.");Object.assign(i,{padding:m,align:d})}function J({type:i,color:l=!1,previous:n={},patch:e={},defaults:o={},retainCoreOnTypeChange:m=!1,retainCompatibleOnTypeChange:d=!1,allowColorReset:g=!1,allowOrdinalBandParameters:w=!1,validateDomain:c,validateRange:I}){const s=n.type!==void 0&&n.type!==i,M=Object.hasOwn(e,"domain")?e.domain:(!s||m)&&Object.hasOwn(n,"domain")?n.domain:o.domain??"auto",x=Object.hasOwn(e,"range")?e.range:(!s||m)&&Object.hasOwn(n,"range")?n.range:o.range??"auto",r={type:i,domain:c(i,M),range:I(i,x)},u=f(n,e,o,"emptyDomain",s);if(u!==void 0){if(!["preserve","require-explicit"].includes(u))throw new Error('Scale emptyDomain must be "preserve" or "require-explicit".');r.emptyDomain=u}for(const a of p){let t=f(n,e,o,a,s);if(t===void 0&&s&&d&&Object.hasOwn(n,a))try{b(i,a),t=n[a]}catch{t=void 0}if(t!==void 0){if(typeof t!="boolean")throw new TypeError(`Scale ${a} must be a boolean.`);b(i,a),r[a]=t}}const j=Object.fromEntries(B.flatMap(a=>{const t=f(n,e,o,a,s);return t===void 0?[]:[[a,t]]}));if(k(i)){const a=q(i,j);i==="log"&&(r.base=a.base),i==="pow"&&(r.exponent=a.exponent),i==="symlog"&&(r.constant=a.constant),r.domain!=="auto"&&A(i,r.domain,j)}else for(const a of Object.keys(j))b(i,a);let O=f(n,e,o,"interpolate",s);if(i==="sequential"||l&&R(i))O===void 0&&d&&(O=n.interpolate),r.interpolate=N(O??"rgb");else if(O!==void 0)throw new Error(`Scale type "${i}" does not support interpolate.`);const z=Object.hasOwn(e,"interpolate")&&e.interpolate!==n.interpolate,S={};for(const a of["whitePoint","gamma"]){const t=Object.hasOwn(e,a)?e[a]:z?void 0:f(n,e,o,a,s&&!(d&&R(i)));t!==void 0&&!(g&&t==="auto")&&(S[a]=t)}Object.assign(r,F(r.interpolate,S));const E=T(f(n,e,o,"midpoint",s),i,r.domain);if(E!==void 0&&(r.midpoint=E),i==="band"||i==="ordinal"&&w)_(r,n,e,o,s);else if(i==="point")$(r,n,e,o,s);else for(const a of["paddingInner","paddingOuter","padding","align"])Object.hasOwn(e,a)&&b(i,a);const P=Object.hasOwn(e,"radialMapping")?e.radialMapping:n.radialMapping;return P!==void 0&&(r.radialMapping=P,D({...r,...Object.hasOwn(e,"unknown")||Object.hasOwn(n,"unknown")?{unknown:e.unknown??n.unknown}:{}},{allowAuto:!0})),r}export{J as normalizeScaleDefinition};
1
+ import{validateMeasuredRadiusScale as D}from"./radial.js";import{normalizeScaleNice as R}from"./continuous.js";import{normalizeColorInterpolationOptions as F,validateContinuousColorInterpolation as T,validateSequentialMidpoint as k}from"./color.js";import{isTransformedScaleType as q}from"./mapping.js";import{normalizeTransformParameters as A,validateTransformedDomain as _}from"./transformed.js";import{isContinuousColorScaleType as M,validateScalePropertyForType as b}from"./types.js";const $=Object.freeze(["nice","zero","clamp","reverse"]),p=Object.freeze(["base","exponent","constant"]);function f(i,l,n,e,o){return Object.hasOwn(l,e)?l[e]:!o&&Object.hasOwn(i,e)?i[e]:n[e]}function B(i,l,n,e,o){const m=Object.hasOwn(n,"padding");if(m&&(Object.hasOwn(n,"paddingInner")||Object.hasOwn(n,"paddingOuter")))throw new Error("Scale padding cannot be combined with paddingInner or paddingOuter.");if(m&&(!Number.isFinite(n.padding)||n.padding<0||n.padding>=1))throw new RangeError("Scale padding must be from 0 (inclusive) to 1 (exclusive).");const d=m?{...n,paddingInner:n.padding,paddingOuter:n.padding}:n,c=f(l,d,e,"paddingInner",o)??0,w=f(l,d,e,"paddingOuter",o)??0,g=f(l,d,e,"align",o)??.5;if(!Number.isFinite(c)||c<0||c>=1)throw new RangeError("Scale paddingInner must be from 0 (inclusive) to 1 (exclusive).");if(!Number.isFinite(w)||w<0)throw new RangeError("Scale paddingOuter must be non-negative and finite.");if(!Number.isFinite(g)||g<0||g>1)throw new RangeError("Scale align must be between 0 and 1.");Object.assign(i,{paddingInner:c,paddingOuter:w,align:g})}function L(i,l,n,e,o){const m=f(l,n,e,"padding",o)??.5,d=f(l,n,e,"align",o)??.5;if(!Number.isFinite(m)||m<0)throw new RangeError("Scale padding must be non-negative and finite.");if(!Number.isFinite(d)||d<0||d>1)throw new RangeError("Scale align must be between 0 and 1.");Object.assign(i,{padding:m,align:d})}function K({type:i,color:l=!1,previous:n={},patch:e={},defaults:o={},retainCoreOnTypeChange:m=!1,retainCompatibleOnTypeChange:d=!1,allowColorReset:c=!1,allowOrdinalBandParameters:w=!1,validateDomain:g,validateRange:I}){const s=n.type!==void 0&&n.type!==i,x=Object.hasOwn(e,"domain")?e.domain:(!s||m)&&Object.hasOwn(n,"domain")?n.domain:o.domain??"auto",z=Object.hasOwn(e,"range")?e.range:(!s||m)&&Object.hasOwn(n,"range")?n.range:o.range??"auto",r={type:i,domain:g(i,x),range:I(i,z)},u=f(n,e,o,"emptyDomain",s);if(u!==void 0){if(!["preserve","require-explicit"].includes(u))throw new Error('Scale emptyDomain must be "preserve" or "require-explicit".');r.emptyDomain=u}for(const a of $){let t=f(n,e,o,a,s);if(t===void 0&&s&&d&&Object.hasOwn(n,a))try{b(i,a),a==="nice"&&R(i,n[a]),t=n[a]}catch{t=void 0}if(t!==void 0){if(a==="nice")t=R(i,t);else if(typeof t!="boolean")throw new TypeError(`Scale ${a} must be a boolean.`);b(i,a),r[a]=t}}const j=Object.fromEntries(p.flatMap(a=>{const t=f(n,e,o,a,s);return t===void 0?[]:[[a,t]]}));if(q(i)){const a=A(i,j);i==="log"&&(r.base=a.base),i==="pow"&&(r.exponent=a.exponent),i==="symlog"&&(r.constant=a.constant),r.domain!=="auto"&&_(i,r.domain,j)}else for(const a of Object.keys(j))b(i,a);let O=f(n,e,o,"interpolate",s);if(i==="sequential"||l&&M(i))O===void 0&&d&&(O=n.interpolate),r.interpolate=T(O??"rgb");else if(O!==void 0)throw new Error(`Scale type "${i}" does not support interpolate.`);const N=Object.hasOwn(e,"interpolate")&&e.interpolate!==n.interpolate,S={};for(const a of["whitePoint","gamma"]){const t=Object.hasOwn(e,a)?e[a]:N?void 0:f(n,e,o,a,s&&!(d&&M(i)));t!==void 0&&!(c&&t==="auto")&&(S[a]=t)}Object.assign(r,F(r.interpolate,S));const E=k(f(n,e,o,"midpoint",s),i,r.domain);if(E!==void 0&&(r.midpoint=E),i==="band"||i==="ordinal"&&w)B(r,n,e,o,s);else if(i==="point")L(r,n,e,o,s);else for(const a of["paddingInner","paddingOuter","padding","align"])Object.hasOwn(e,a)&&b(i,a);const P=Object.hasOwn(e,"radialMapping")?e.radialMapping:n.radialMapping;return P!==void 0&&(r.radialMapping=P,D({...r,...Object.hasOwn(e,"unknown")||Object.hasOwn(n,"unknown")?{unknown:e.unknown??n.unknown}:{}},{allowAuto:!0})),r}export{K as normalizeScaleDefinition};
@@ -1,6 +1,6 @@
1
- import { cloneAndFreeze } from "../../core/immutable.js";
1
+ import { cloneAndFreeze, isPlainObject } from "../../core/immutable.js";
2
2
  import { isNominalValue } from "./fields.js";
3
- import { validateColorRange, validateStrokeDashRange } from "./appearance.js";
3
+ import { validateColorRange, validateShapeRange, validateStrokeDashRange } from "./appearance.js";
4
4
  import { validatePair, validateScaleRange } from "./validation.js";
5
5
  import { resolveScaleRange } from "./continuous.js";
6
6
  import { interpolateNumber } from "../numeric.js";
@@ -70,6 +70,9 @@ export function validateOrdinalDomain(domain) {
70
70
  export function validateOrdinalRange(range) {
71
71
  if (range === "auto") return range;
72
72
  if (Array.isArray(range)) {
73
+ if (range.some(value => isPlainObject(value) && Object.hasOwn(value, "polygon"))) {
74
+ return validateShapeRange(range);
75
+ }
73
76
  if (range.length === 2 && range.every(Number.isFinite)) {
74
77
  return cloneAndFreeze(range);
75
78
  }
@@ -1,5 +1,5 @@
1
1
  import { cloneAndFreeze } from "../../core/immutable.js";
2
- import { POINT_SHAPES } from "../pointShapes.js";
2
+ import { POINT_SHAPES, validatePointShape } from "../pointShapes.js";
3
3
  import { normalizeStrokeDashPattern } from "./appearance.js";
4
4
 
5
5
  export function validateScaleUnknown(channel, value) {
@@ -30,10 +30,10 @@ export function validateScaleUnknown(channel, value) {
30
30
  return value;
31
31
  }
32
32
  if (channel === "shape") {
33
- if (!POINT_SHAPES.includes(value)) {
33
+ if (typeof value !== "object" && !POINT_SHAPES.includes(value)) {
34
34
  throw new Error(`Unsupported scale unknown shape "${value}".`);
35
35
  }
36
- return value;
36
+ return validatePointShape(value);
37
37
  }
38
38
  if (channel === "strokeDash") {
39
39
  return cloneAndFreeze(normalizeStrokeDashPattern(value));
@@ -3,6 +3,7 @@ import {
3
3
  resolveTextBounds
4
4
  } from "../../core/textMetrics.js";
5
5
  import { interpolateNumber } from "../numeric.js";
6
+ import { graphicClipBounds, intersectClipBounds } from "./graphicClip.js";
6
7
  import { resolveStrokeDetails } from "../strokeStyle.js";
7
8
  import {
8
9
  findGraphic,
@@ -452,7 +453,7 @@ function canvasOffset(object, property) {
452
453
  return Number.isFinite(value) ? value : 0;
453
454
  }
454
455
 
455
- function ancestorOffset(graphicSpec, target) {
456
+ export function resolveGraphicParentOffset(graphicSpec, target) {
456
457
  const offset = [0, 0];
457
458
  const visited = new Set();
458
459
  let current = target;
@@ -476,11 +477,15 @@ function intersectVisibleBounds(bounds,clip) {
476
477
  return result.right<=result.left||result.bottom<=result.top?undefined:result;
477
478
  }
478
479
 
479
- function clipAncestorBounds(graphicSpec,id,bounds) {
480
+ function clipAncestorBounds(graphicSpec,id,bounds,includeOwnerClips) {
480
481
  let parent=findGraphicParent(graphicSpec,id);
481
482
  while(parent!==undefined){
483
+ if (includeOwnerClips && parent.object.clip !== undefined) {
484
+ bounds = intersectVisibleBounds(bounds, graphicClipBounds(parent.object.clip,
485
+ ...resolveGraphicParentOffset(graphicSpec, parent.id)));
486
+ }
482
487
  if(parent.object.type==="canvas"&&findGraphicParent(graphicSpec,parent.id)!==undefined){
483
- const [x,y]=ancestorOffset(graphicSpec,parent.id),p=parent.object.properties;
488
+ const [x,y]=resolveGraphicParentOffset(graphicSpec,parent.id),p=parent.object.properties;
484
489
  bounds=intersectVisibleBounds(bounds,{left:x+(p.x??0),right:x+(p.x??0)+p.width,top:y+(p.y??0),bottom:y+(p.y??0)+p.height});
485
490
  }
486
491
  parent=findGraphicParent(graphicSpec,parent.id);
@@ -488,7 +493,27 @@ function clipAncestorBounds(graphicSpec,id,bounds) {
488
493
  return bounds;
489
494
  }
490
495
 
491
- function objectBounds(graphicSpec, id, object, ancestors, x, y, profile) {
496
+ // Effective rectangular visibility, including the named owner itself. Inspection
497
+ // can opt into the root viewport without changing geometry/selection properties.
498
+ export function resolveGraphicClipBounds(graphicSpec, target, {rootViewport = false} = {}) {
499
+ const found = requireGraphic(graphicSpec, target);
500
+ let current = found.kind === "item" ? found.ownerId : found.id;
501
+ let clip;
502
+ while (current !== undefined) {
503
+ const object = graphicSpec.objects[current];
504
+ const offset = resolveGraphicParentOffset(graphicSpec, current);
505
+ const parent = findGraphicParent(graphicSpec, current);
506
+ const local = object.clip !== undefined ? graphicClipBounds(object.clip, ...offset)
507
+ : object.type === "canvas" && (parent !== undefined || rootViewport)
508
+ ? graphicClipBounds({...object.properties, x: parent === undefined ? 0 : object.properties.x,
509
+ y: parent === undefined ? 0 : object.properties.y}, ...offset) : undefined;
510
+ if (local !== undefined) clip = clip === undefined ? local : intersectClipBounds(clip, local);
511
+ current = parent?.id;
512
+ }
513
+ return clip;
514
+ }
515
+
516
+ function objectBounds(graphicSpec, id, object, ancestors, x, y, profile, includeOwnerClips) {
492
517
  if (ancestors.has(id)) {
493
518
  throw new Error(`Graphic attachment cycle includes "${id}".`);
494
519
  }
@@ -508,7 +533,7 @@ function objectBounds(graphicSpec, id, object, ancestors, x, y, profile) {
508
533
  throw new Error(`Unknown attached graphic "${childId}".`);
509
534
  }
510
535
  const bounds = objectBounds(
511
- graphicSpec, childId, child.object, ancestors, childX, childY, profile
536
+ graphicSpec, childId, child.object, ancestors, childX, childY, profile, includeOwnerClips
512
537
  );
513
538
  if (bounds !== undefined) {
514
539
  result = includeExtent(
@@ -520,28 +545,31 @@ function objectBounds(graphicSpec, id, object, ancestors, x, y, profile) {
520
545
  result=intersectVisibleBounds(result,{left:childX,right:childX+object.properties.width,
521
546
  top:childY,bottom:childY+object.properties.height});
522
547
  }
548
+ if (includeOwnerClips && object.clip !== undefined) result = intersectVisibleBounds(result, graphicClipBounds(object.clip, x, y));
523
549
  ancestors.delete(id);
524
550
  return result;
525
551
  }
526
552
 
527
- export function resolveConcreteGraphicBounds(graphicSpec, target, profile) {
553
+ export function resolveConcreteGraphicBounds(graphicSpec, target, profile, {includeOwnerClips = true} = {}) {
528
554
  const found = requireGraphic(graphicSpec, target);
529
555
  const anchor=found.kind==="item"?found.ownerId:found.id;
530
- const [x, y] = ancestorOffset(graphicSpec, anchor);
556
+ const [x, y] = resolveGraphicParentOffset(graphicSpec, anchor);
531
557
  return requireFiniteBounds(clipAncestorBounds(graphicSpec,anchor,found.kind === "item"
532
- ? translateBounds(
558
+ ? (!includeOwnerClips || found.owner.clip === undefined ? translateBounds(
533
559
  resolvePrimitiveGraphicBounds(found.object.type ?? found.owner.type, found.object.properties, profile),
534
560
  x,
535
561
  y
536
- )
537
- : objectBounds(graphicSpec, found.id, found.object, new Set(), x, y, profile)));
562
+ ) : intersectVisibleBounds(translateBounds(
563
+ resolvePrimitiveGraphicBounds(found.object.type ?? found.owner.type, found.object.properties, profile), x, y
564
+ ), graphicClipBounds(found.owner.clip, x, y)))
565
+ : objectBounds(graphicSpec, found.id, found.object, new Set(), x, y, profile, includeOwnerClips), includeOwnerClips));
538
566
  }
539
567
 
540
- export function unionConcreteGraphicBounds(graphicSpec, targets, profile) {
568
+ export function unionConcreteGraphicBounds(graphicSpec, targets, profile, options) {
541
569
  if (!Array.isArray(targets)) {
542
570
  throw new TypeError("Graphic bounds targets must be an array.");
543
571
  }
544
572
  return unionBounds(targets.map(target =>
545
- resolveConcreteGraphicBounds(graphicSpec, target, profile)
573
+ resolveConcreteGraphicBounds(graphicSpec, target, profile, options)
546
574
  ));
547
575
  }
@@ -0,0 +1,23 @@
1
+ import { isPlainObject } from "../../core/immutable.js";
2
+
3
+ // A named owner's rectangle uses its existing parent frame; it never rebases items.
4
+ export function validateGraphicClip(value, type) {
5
+ if (type === "canvas") throw new Error("Canvas uses its existing viewport, not an owner clip.");
6
+ if (!isPlainObject(value) || Object.keys(value).length !== 4 ||
7
+ !["x", "y", "width", "height"].every(key => Object.hasOwn(value, key) && Number.isFinite(value[key])) ||
8
+ value.width < 0 || value.height < 0 ||
9
+ !Number.isFinite(value.x + value.width) || !Number.isFinite(value.y + value.height)) {
10
+ throw new TypeError("Graphic clip requires finite x, y and non-negative width, height.");
11
+ }
12
+ return value;
13
+ }
14
+
15
+ export function graphicClipBounds(rect, x = 0, y = 0) {
16
+ return {left: x + rect.x, right: x + rect.x + rect.width,
17
+ top: y + rect.y, bottom: y + rect.y + rect.height};
18
+ }
19
+
20
+ export function intersectClipBounds(a, b) {
21
+ return {left: Math.max(a.left, b.left), right: Math.min(a.right, b.right),
22
+ top: Math.max(a.top, b.top), bottom: Math.min(a.bottom, b.bottom)};
23
+ }
@@ -1,4 +1,20 @@
1
1
  import { isPlainObject } from "../../core/immutable.js";
2
+ import { isGraphicContainerType } from "./graphic.js";
3
+
4
+ export function resolveGraphicChildOrder(graphicSpec, target, value) {
5
+ const found = requireGraphic(graphicSpec, target);
6
+ if (found.kind !== "object" || !isGraphicContainerType(found.object.type)) {
7
+ throw new Error("Graphic children order requires a named Canvas or collection.");
8
+ }
9
+ const children = found.object.children ?? [];
10
+ const existing = new Set(children);
11
+ if (!Array.isArray(value) || value.length !== children.length ||
12
+ new Set(value).size !== children.length ||
13
+ value.some(id => typeof id !== "string" || !existing.has(id))) {
14
+ throw new Error("Graphic children order must be an exact permutation of existing direct children.");
15
+ }
16
+ return [...value];
17
+ }
2
18
 
3
19
  function requireGraphicSpec(graphicSpec) {
4
20
  if (
@@ -1,12 +1,34 @@
1
1
  import { rejectUnknownProperties as rejectUnknownKeys } from "../core/validation.js";
2
2
  import { aggregateRows } from "./aggregate.js";
3
3
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
4
+ import { scalarKey } from "./transformKeys.js";
4
5
 
5
6
  const STATISTIC_KEYS = Object.freeze(["op", "p"]);
6
7
  const SIMPLE_STATISTICS = new Set(["mean", "median", "min", "max"]);
7
8
  const POPULATIONS = new Set(["boundData", "visibleItems"]);
8
9
  const TRANSFORM_KEYS = Object.freeze(["type", "target"]);
9
10
 
11
+ export function normalizeReferenceGroupBy(value) {
12
+ if (value === undefined) return undefined;
13
+ if (typeof value !== "string" || value.length === 0) {
14
+ throw new TypeError("Statistical reference groupBy must be a non-empty field name.");
15
+ }
16
+ return value;
17
+ }
18
+
19
+ export function partitionReferenceRows(rows, groupBy) {
20
+ if (groupBy === undefined) return [{ rows }];
21
+ const groups = new Map();
22
+ for (const [index, row] of rows.entries()) {
23
+ if (!isPlainObject(row) || !Object.hasOwn(row, groupBy)) {
24
+ throw new Error(`Statistical reference groupBy field "${groupBy}" is missing at item ${index}.`);
25
+ }
26
+ const group = row[groupBy], key = scalarKey(group, "Statistical reference groupBy");
27
+ if (!groups.has(key)) groups.set(key, { key, group, rows: [] });
28
+ groups.get(key).rows.push(row);
29
+ }
30
+ return [...groups.values()];
31
+ }
10
32
 
11
33
  export function normalizeReferenceStatistic(value, label = "Reference statistic") {
12
34
  if (!isPlainObject(value)) {
@@ -1,4 +1,8 @@
1
- import { cloneAndFreeze } from "../core/immutable.js";
1
+ import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
+ import { schemaField } from "./datasetSchema.js";
3
+ import { readQuantitativeField } from "./scales/fields.js";
4
+ import { applyItemMissingPolicy } from "./itemMissing.js";
5
+ import { validateOptionObject } from "../core/validation.js";
2
6
  import { validateConcreteGraphicValue } from "./schemas/concreteGraphic.js";
3
7
  import {
4
8
  formatValue,
@@ -11,6 +15,66 @@ export function isSourceOwnedText(layer) {
11
15
  return layer?.mark?.type === "text" && layer.source !== undefined;
12
16
  }
13
17
 
18
+ export function isIndependentText(layer) {
19
+ return layer?.mark?.type === "text" && layer.source === undefined;
20
+ }
21
+
22
+ export function validateTextOffsetEncoding(encoding, channel) {
23
+ if (!isPlainObject(encoding)) throw new TypeError(`Text ${channel} encoding must be an object.`);
24
+ const field = Object.hasOwn(encoding, "field");
25
+ const datum = Object.hasOwn(encoding, "datum");
26
+ if (field === datum) throw new Error(`Text ${channel} requires exactly one field or datum.`);
27
+ const allowed = field ? ["field", "fieldType"] : ["datum"];
28
+ if (Object.keys(encoding).some(key => !allowed.includes(key))) {
29
+ throw new Error(`Unsupported Text ${channel} encoding option.`);
30
+ }
31
+ if (field) {
32
+ if (typeof encoding.field !== "string" || encoding.field.length === 0 ||
33
+ encoding.fieldType !== "quantitative") {
34
+ throw new TypeError(`Text ${channel} requires a quantitative field.`);
35
+ }
36
+ } else if (!Number.isFinite(encoding.datum)) {
37
+ throw new TypeError(`Text ${channel} datum must be finite logical pixels.`);
38
+ }
39
+ }
40
+
41
+ export function normalizeTextOffsetAssignment(args, channel) {
42
+ const operation = channel === "dx" ? "encodeDx" : "encodeDy";
43
+ validateOptionObject(args, ["target", "field", "value"], operation);
44
+ const field = Object.hasOwn(args, "field");
45
+ if (field === Object.hasOwn(args, "value")) {
46
+ throw new Error(`${operation} requires exactly one of field or value.`);
47
+ }
48
+ const encoding = field ? { field: args.field, fieldType: "quantitative" } : { datum: args.value };
49
+ validateTextOffsetEncoding(encoding, channel);
50
+ return cloneAndFreeze(encoding);
51
+ }
52
+
53
+ // Resolve missing rows once for positions, content, color and both pixel offsets.
54
+ export function resolveTextOffsetDataset(layer, sourceDataset) {
55
+ const channels = ["dx", "dy"].filter(channel => layer.encoding?.[channel] !== undefined);
56
+ if (channels.length && !isIndependentText(layer)) {
57
+ throw new Error("Numeric text offsets require independent Text marks.");
58
+ }
59
+ const dataset = applyItemMissingPolicy(layer, sourceDataset);
60
+ for (const channel of channels) {
61
+ const encoding = layer.encoding[channel];
62
+ validateTextOffsetEncoding(encoding, channel);
63
+ if (!Object.hasOwn(encoding, "field")) continue;
64
+ if (sourceDataset.values.length === 0 &&
65
+ schemaField(sourceDataset.schema, encoding.field)?.storageType !== "number") {
66
+ throw new Error(`Empty Text ${channel} data requires a known numeric field "${encoding.field}".`);
67
+ }
68
+ readQuantitativeField(dataset.values, encoding.field);
69
+ }
70
+ return dataset;
71
+ }
72
+
73
+ export function textOffsetValue(encoding, row, fallback) {
74
+ if (encoding === undefined) return fallback;
75
+ return Object.hasOwn(encoding, "field") ? row[encoding.field] : encoding.datum;
76
+ }
77
+
14
78
  export const TEXT_MARK_STYLE_OPTIONS = Object.freeze([
15
79
  "fill", "opacity", "fontSize", "fontFamily", "fontWeight",
16
80
  "align", "baseline", "rotation", "dx", "dy", "inheritColor", "lineHeight", "blockAlign"
@@ -6,6 +6,7 @@ import {
6
6
  import {
7
7
  alignNumericStep,
8
8
  cleanNumericValue,
9
+ nearestNumericIncrement,
9
10
  niceNumericStep,
10
11
  normalizeNumericRange
11
12
  } from "./numeric.js";
@@ -36,12 +37,64 @@ function ticksForStep(start, end, step, low, high, scale) {
36
37
  return values;
37
38
  }
38
39
 
39
- export function niceTicks(domain, count) {
40
+ export function validateTickRounding(rounding, optional = true) {
41
+ if ((!optional || rounding !== undefined) && !["ceiling", "nearest"].includes(rounding)) {
42
+ throw new TypeError('Tick rounding must be "ceiling" or "nearest".');
43
+ }
44
+ }
45
+
46
+ export function validateTickSelectionRounding(config, scaleType) {
47
+ validateTickRounding(config.rounding, !Object.hasOwn(config, "rounding"));
48
+ if (config.rounding !== undefined && (config.mode !== "count" || scaleType !== "linear")) {
49
+ throw new Error("Tick rounding requires an automatic linear Cartesian axis.");
50
+ }
51
+ if (config.rounding !== undefined) {
52
+ if (!Number.isInteger(config.count) || config.count <= 0) {
53
+ throw new RangeError("Tick rounding count must be a positive integer.");
54
+ }
55
+ validateGeneratedItemLimit(config.count, "Tick rounding count");
56
+ }
57
+ }
58
+
59
+ export function axisTickSelection(config) {
60
+ return config.mode === "values" ? { mode: "values", values: config.values } : {
61
+ mode: "count", count: config.count,
62
+ ...(config.rounding === "nearest" ? { rounding: "nearest" } : {})
63
+ };
64
+ }
65
+
66
+ function nearestTicks(low, high, count) {
67
+ let start = low, end = high, scale = 1;
68
+ let increment = nearestNumericIncrement(end - start, count);
69
+ if (increment === null) {
70
+ const normalized = normalizeNumericRange(low, high);
71
+ ({ start, end, scale } = normalized);
72
+ increment = nearestNumericIncrement(normalized.span, count);
73
+ }
74
+ if (increment === null) return cloneAndFreeze([low, high]);
75
+ const first = Math.ceil(increment < 0 ? start * -increment : start / increment);
76
+ const last = Math.floor(increment < 0 ? end * -increment : end / increment);
77
+ const length = last - first + 1;
78
+ if (!Number.isFinite(length) || length <= 0) return cloneAndFreeze([low, high]);
79
+ const size = Math.min(length, MAX_GENERATED_ITEMS), values = [];
80
+ for (let index = 0; index < size; index++) {
81
+ const offset = length === size ? index : Math.round(index * (length - 1) / (size - 1));
82
+ const at = first + offset;
83
+ const value = (increment < 0 ? at / -increment : at * increment) * scale;
84
+ if (Number.isFinite(value) && value >= low && value <= high &&
85
+ (values.length === 0 || value > values.at(-1))) values.push(value === 0 ? 0 : value);
86
+ }
87
+ return cloneAndFreeze(values.length > 0 ? values : [low, high]);
88
+ }
89
+
90
+ export function niceTicks(domain, count, rounding = "ceiling") {
91
+ validateTickRounding(rounding);
40
92
  if (!Number.isInteger(count) || count <= 0) throw new RangeError("Tick count must be a positive integer.");
41
93
  const effectiveCount = Math.min(count, MAX_GENERATED_ITEMS);
42
94
  const low = Math.min(...domain);
43
95
  const high = Math.max(...domain);
44
96
  if (low === high) return cloneAndFreeze([low]);
97
+ if (rounding === "nearest") return nearestTicks(low, high, effectiveCount);
45
98
  const directSpan = high - low;
46
99
  if (Number.isFinite(directSpan) && directSpan > 0) {
47
100
  const directStep = niceNumericStep(directSpan, effectiveCount);
@@ -254,3 +307,41 @@ export function formatTimeTicks(values, domain) {
254
307
  }
255
308
  return cloneAndFreeze(values.map(value => String(value)));
256
309
  }
310
+
311
+
312
+ // Fixed numeric display precision belongs to one settled axis, not its dataset.
313
+ // Sorting a copied distinct list preserves the caller's tick and painter order.
314
+ export function formatNumericTickValues(values) {
315
+ if (!Array.isArray(values) || !values.every(Number.isFinite)) {
316
+ throw new TypeError("Numeric tick formatting requires finite tick values.");
317
+ }
318
+ const ordered = [...new Set(values)].sort((a, b) => a - b);
319
+ if (ordered.length === 0) return [];
320
+ let step = Infinity;
321
+ for (let index = 1; index < ordered.length; index += 1) {
322
+ step = Math.min(step, ordered[index] - ordered[index - 1]);
323
+ }
324
+ let precision;
325
+ if (ordered.length === 1) {
326
+ const normalized = Number(ordered[0].toPrecision(12));
327
+ const [coefficient, exponent = "0"] = String(normalized).split("e");
328
+ precision = Math.max(0, (coefficient.split(".")[1]?.length ?? 0) - Number(exponent));
329
+ } else {
330
+ const normalizedStep = Number(step.toPrecision(12));
331
+ precision = Math.max(0, -Math.floor(Math.log10(normalizedStep)));
332
+ }
333
+ if (precision > 12) {
334
+ throw new RangeError("Numeric tick format supports at most 12 decimals.");
335
+ }
336
+ const fixed = (value, digits) => {
337
+ const label = value.toFixed(digits);
338
+ return Number(label) === 0 ? (0).toFixed(digits) : label.replace(/^-/, "−");
339
+ };
340
+ for (; precision <= 12; precision += 1) {
341
+ const labels = ordered.map(value => fixed(value, precision));
342
+ if (new Set(labels).size === labels.length) {
343
+ return values.map(value => fixed(value, precision));
344
+ }
345
+ }
346
+ throw new RangeError("Numeric tick format cannot distinguish values within 12 decimals.");
347
+ }
@@ -41,7 +41,7 @@ export const TIME_UNITS = cloneAndFreeze([
41
41
 
42
42
  const TRANSFORM_KEYS = Object.freeze([
43
43
  "type", "field", "unit", "as", "temporalUnit", "timeZone",
44
- "weekStartsOn", "weekRule"
44
+ "weekStartsOn", "weekRule", "missing"
45
45
  ]);
46
46
 
47
47
  const CALENDAR_FORMATTERS = new Map();
@@ -93,6 +93,9 @@ export function validateTimeUnitTransform(transform) {
93
93
  if (transform.type !== "timeUnit") {
94
94
  throw new Error(`Unsupported time-unit transform "${transform.type}".`);
95
95
  }
96
+ if (Object.hasOwn(transform, "missing") && !["error", "null"].includes(transform.missing)) {
97
+ throw new Error('Time-unit missing must be "error" or "null".');
98
+ }
96
99
  validateTemporalUnit(transform.temporalUnit);
97
100
  const field = requireField(transform.field, "Time-unit field");
98
101
  const output = requireField(transform.as, "Time-unit output field");
@@ -127,7 +130,8 @@ export function normalizeTimeUnitTransform(args = {}) {
127
130
  const as = args.as === undefined ? `${field}_${unit}` : args.as;
128
131
  const transform = { type: "timeUnit", field, unit, as,
129
132
  ...(temporalUnit === undefined ? {} : { temporalUnit }),
130
- ...(timeZone === undefined ? {} : { timeZone }) };
133
+ ...(timeZone === undefined ? {} : { timeZone }),
134
+ ...(Object.hasOwn(args, "missing") ? { missing: args.missing } : {}) };
131
135
  if (unit === "week") {
132
136
  transform.weekStartsOn = args.weekStartsOn ?? 1;
133
137
  transform.weekRule = args.weekRule ?? "calendar";
@@ -319,7 +323,8 @@ export function deriveTimeUnitRows(rows, transform) {
319
323
  throw new TypeError("Time-unit source must contain plain row objects.");
320
324
  }
321
325
  const values = rows.map((row, index) => {
322
- if (!Object.hasOwn(row, transform.field)) {
326
+ const missing = !Object.hasOwn(row, transform.field) || row[transform.field] === null;
327
+ if (!Object.hasOwn(row, transform.field) && transform.missing !== "null") {
323
328
  throw new Error(
324
329
  `Time-unit source does not contain field "${transform.field}" at row ${index}.`
325
330
  );
@@ -327,6 +332,7 @@ export function deriveTimeUnitRows(rows, transform) {
327
332
  if (Object.hasOwn(row, transform.as)) {
328
333
  throw new Error(`Time-unit output field "${transform.as}" already exists.`);
329
334
  }
335
+ if (missing && transform.missing === "null") return { ...row, [transform.as]: null };
330
336
  const timestamp = normalizeTemporalValue(row[transform.field], transform.field, index, transform.temporalUnit);
331
337
  return {
332
338
  ...row,