ggaction 0.0.21 → 0.0.22

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 (253) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +3 -2
  3. package/knowledge/action-cards.json +1 -1
  4. package/knowledge/intent-taxonomy.json +1 -1
  5. package/knowledge/mcp-resources.json +1 -1
  6. package/package.json +6 -2
  7. package/src/BasicChartProgram.js +1 -1
  8. package/src/accessibility.js +1 -0
  9. package/src/actions/boxPlots/components.js +1 -96
  10. package/src/actions/boxPlots/create.js +6 -1
  11. package/src/actions/boxPlots/edit.js +8 -0
  12. package/src/actions/boxPlots/materialize.js +3 -0
  13. package/src/actions/boxPlots/options.js +1 -1
  14. package/src/actions/canvas/actions.js +2 -1
  15. package/src/actions/canvas/index.js +3 -0
  16. package/src/actions/canvas/plotBorder.js +2 -1
  17. package/src/actions/canvas/plotOrder.js +14 -0
  18. package/src/actions/charts/area.js +1 -1
  19. package/src/actions/charts/bar.js +4 -4
  20. package/src/actions/charts/density.js +1 -1
  21. package/src/actions/charts/ecdf.js +1 -1
  22. package/src/actions/charts/endpoints.js +54 -5
  23. package/src/actions/charts/heatmap.js +1 -1
  24. package/src/actions/charts/histogram.js +1 -1
  25. package/src/actions/charts/horizon.js +1 -1
  26. package/src/actions/charts/index.js +2 -1
  27. package/src/actions/charts/interval-regression.js +1 -1
  28. package/src/actions/charts/line.js +1 -1
  29. package/src/actions/charts/radar.js +63 -3
  30. package/src/actions/charts/raincloud.js +37 -6
  31. package/src/actions/charts/rug-strip.js +2 -2
  32. package/src/actions/charts/scatter.js +3 -3
  33. package/src/actions/charts/text.js +1 -1
  34. package/src/actions/composition/actions.js +10 -3
  35. package/src/actions/coordinates/actions.js +2 -0
  36. package/src/actions/data/basic.js +6 -0
  37. package/src/actions/data/derived.js +6 -2
  38. package/src/actions/data/edit.js +21 -35
  39. package/src/actions/data/filter.js +1 -376
  40. package/src/actions/data/flatten.js +19 -0
  41. package/src/actions/data/index.js +6 -0
  42. package/src/actions/data/lookup.js +50 -0
  43. package/src/actions/data/raincloudFilter.js +56 -0
  44. package/src/actions/data/shared.js +10 -0
  45. package/src/actions/distributions/revision.js +3 -1
  46. package/src/actions/encodings/angle.js +2 -1
  47. package/src/actions/encodings/appearance.js +38 -13
  48. package/src/actions/encodings/channels.js +14 -3
  49. package/src/actions/encodings/color/continuous.js +26 -10
  50. package/src/actions/encodings/color/index.js +50 -20
  51. package/src/actions/encodings/color/policy.js +57 -4
  52. package/src/actions/encodings/parallel.js +4 -0
  53. package/src/actions/encodings/position/policies/bar.js +10 -6
  54. package/src/actions/encodings/position/policies/line.js +1 -152
  55. package/src/actions/encodings/position/resolve.js +4 -1
  56. package/src/actions/encodings/ranged.js +46 -0
  57. package/src/actions/encodings/remove.js +1 -1
  58. package/src/actions/encodings/ruleAppearance.js +7 -6
  59. package/src/actions/encodings/shared.js +14 -2
  60. package/src/actions/encodings/stroke.js +20 -14
  61. package/src/actions/encodings/strokeDash.js +9 -11
  62. package/src/actions/encodings/text.js +30 -2
  63. package/src/actions/errorBars/edit.js +24 -4
  64. package/src/actions/facets/actions.js +1 -793
  65. package/src/actions/facets/derive.js +34 -5
  66. package/src/actions/facets/replay.js +1 -38
  67. package/src/actions/gradientPlots/components.js +21 -9
  68. package/src/actions/gradientPlots/create.js +4 -2
  69. package/src/actions/gradientPlots/edit.js +18 -1
  70. package/src/actions/gradientPlots/materialize.js +35 -36
  71. package/src/actions/gradientPlots/options.js +1 -1
  72. package/src/actions/gradientPlots/paint.js +13 -7
  73. package/src/actions/guides/applicability.js +2 -0
  74. package/src/actions/guides/axes/axes.js +39 -0
  75. package/src/actions/guides/axes/axis.js +9 -5
  76. package/src/actions/guides/axes/edit.js +11 -8
  77. package/src/actions/guides/axes/labels.js +24 -8
  78. package/src/actions/guides/axes/parallel/policy.js +3 -5
  79. package/src/actions/guides/axes/parallel/resolve.js +4 -3
  80. package/src/actions/guides/axes/policy.js +22 -0
  81. package/src/actions/guides/axes/tickGroups.js +4 -2
  82. package/src/actions/guides/axes/ticks.js +22 -4
  83. package/src/actions/guides/axes/titles.js +19 -6
  84. package/src/actions/guides/facadeAxes.js +3 -0
  85. package/src/actions/guides/grids/grid.js +3 -1
  86. package/src/actions/guides/grids/resolve.js +7 -11
  87. package/src/actions/guides/guides.js +21 -1
  88. package/src/actions/guides/legends/categorical/actions.js +1 -515
  89. package/src/actions/guides/legends/categorical/layout.js +5 -2
  90. package/src/actions/guides/legends/categorical/options.js +4 -0
  91. package/src/actions/guides/legends/categorical/resolve.js +8 -3
  92. package/src/actions/guides/legends/categorical/symbols.js +3 -3
  93. package/src/actions/guides/legends/colorSize.js +2 -0
  94. package/src/actions/guides/legends/continuous/common.js +1 -1
  95. package/src/actions/guides/legends/continuous/gradient.js +1 -360
  96. package/src/actions/guides/legends/continuous/interval.js +1 -1
  97. package/src/actions/guides/legends/continuous/opacity.js +15 -1
  98. package/src/actions/guides/legends/edit.js +1 -1
  99. package/src/actions/guides/legends/focused.js +1 -1
  100. package/src/actions/guides/legends/lane.js +1 -1
  101. package/src/actions/guides/legends/sampling.js +1 -140
  102. package/src/actions/guides/legends/strokeWidth.js +3 -2
  103. package/src/actions/guides/legends/transition.js +19 -2
  104. package/src/actions/guides/polar/axes/facade.js +5 -4
  105. package/src/actions/guides/polar/axes/labels.js +5 -4
  106. package/src/actions/guides/polar/axes/titles.js +5 -4
  107. package/src/actions/guides/polar/resolve.js +2 -1
  108. package/src/actions/guides/tickValues.js +7 -7
  109. package/src/actions/marks/arc/actions.js +5 -7
  110. package/src/actions/marks/area/actions.js +9 -3
  111. package/src/actions/marks/bar/create.js +4 -2
  112. package/src/actions/marks/bar/edit.js +9 -6
  113. package/src/actions/marks/bar/materialize.js +5 -3
  114. package/src/actions/marks/dependentReferences.js +87 -0
  115. package/src/actions/marks/index.js +2 -0
  116. package/src/actions/marks/lifecycle.js +2 -1
  117. package/src/actions/marks/line/actions.js +24 -6
  118. package/src/actions/marks/line/materialize.js +7 -3
  119. package/src/actions/marks/point/create.js +5 -4
  120. package/src/actions/marks/point/edit.js +6 -3
  121. package/src/actions/marks/point/jitter.js +6 -6
  122. package/src/actions/marks/point/materialize.js +36 -39
  123. package/src/actions/marks/rect/actions.js +16 -4
  124. package/src/actions/marks/references.js +183 -35
  125. package/src/actions/marks/rule/actions.js +20 -9
  126. package/src/actions/marks/shared.js +2 -1
  127. package/src/actions/marks/text/actions.js +10 -3
  128. package/src/actions/marks/tick/actions.js +11 -2
  129. package/src/actions/primitives/editGraphics.js +28 -3
  130. package/src/actions/primitives/index.js +9 -0
  131. package/src/actions/primitives/semanticAction.js +13 -7
  132. package/src/actions/primitives/semanticValidation/layer.js +20 -0
  133. package/src/actions/primitives/semanticValidation/scale.js +8 -1
  134. package/src/actions/scales/channels.js +1 -1
  135. package/src/actions/scales/consumers/common.js +19 -1
  136. package/src/actions/scales/consumers/families.js +2 -2
  137. package/src/actions/scales/consumers/seriesLayout.js +4 -3
  138. package/src/actions/scales/edit.js +11 -0
  139. package/src/actions/scales/editPolicy.js +5 -3
  140. package/src/actions/scales/preview.js +2 -1
  141. package/src/actions/scales/quantitativeColor.js +2 -4
  142. package/src/actions/selection/actions.js +10 -1
  143. package/src/actions/theme/reconcile.js +37 -9
  144. package/src/core/compositionState.js +21 -4
  145. package/src/core/datasetReferences.js +17 -0
  146. package/src/core/immutable.js +1 -0
  147. package/src/core/resourceReferences.js +5 -3
  148. package/src/core/validation.js +4 -0
  149. package/src/core/vocabulary.js +24 -2
  150. package/src/grammar/aggregate.js +1 -0
  151. package/src/grammar/arcs.js +1 -1
  152. package/src/grammar/areaSeries.js +1 -1
  153. package/src/grammar/axisTextPlacement.js +1 -1
  154. package/src/grammar/bars/policy.js +2 -2
  155. package/src/grammar/datasetDependencies.js +97 -0
  156. package/src/grammar/datasetSchema.js +31 -1
  157. package/src/grammar/facetHeaders.js +1 -0
  158. package/src/grammar/facets/dependencies.js +6 -2
  159. package/src/grammar/facets/legendIntent.js +6 -2
  160. package/src/grammar/facets/scales.js +27 -7
  161. package/src/grammar/flatten.js +85 -0
  162. package/src/grammar/histogram.js +13 -1
  163. package/src/grammar/horizon.js +1 -494
  164. package/src/grammar/jitter.js +2 -2
  165. package/src/grammar/legendSpacing.js +45 -0
  166. package/src/grammar/lineMissing.js +90 -0
  167. package/src/grammar/lineSeries.js +1 -376
  168. package/src/grammar/lookup.js +97 -0
  169. package/src/grammar/markFilter.js +4 -2
  170. package/src/grammar/markLabels.js +8 -3
  171. package/src/grammar/numeric.js +1 -1
  172. package/src/grammar/pathCommands.js +1 -1
  173. package/src/grammar/pathSeries.js +1 -109
  174. package/src/grammar/pointPolygon.js +81 -0
  175. package/src/grammar/pointShapes.js +15 -15
  176. package/src/grammar/rects.js +1 -1
  177. package/src/grammar/regression/derive.js +1 -195
  178. package/src/grammar/scales/appearance.js +1 -1
  179. package/src/grammar/scales/color.js +1 -1
  180. package/src/grammar/scales/colorConsumers.js +1 -63
  181. package/src/grammar/scales/continuous.js +58 -1
  182. package/src/grammar/scales/definition.js +1 -1
  183. package/src/grammar/scales/ordinal.js +5 -2
  184. package/src/grammar/scales/policies.js +3 -3
  185. package/src/grammar/schemas/graphicBounds.js +40 -12
  186. package/src/grammar/schemas/graphicClip.js +23 -0
  187. package/src/grammar/schemas/graphicTree.js +16 -0
  188. package/src/grammar/statisticalReference.js +22 -0
  189. package/src/grammar/text.js +65 -1
  190. package/src/grammar/ticks.js +92 -1
  191. package/src/grammar/transformTopology.js +3 -1
  192. package/src/grammar/transforms.js +24 -0
  193. package/src/inspection.js +1 -495
  194. package/src/inspectionDescriptors.js +1 -1
  195. package/src/layout/canvas.js +1 -1
  196. package/src/layout/facetContentFlow.js +101 -0
  197. package/src/layout/facetHeaderTitles.js +1 -0
  198. package/src/layout/facetTopLane.js +29 -0
  199. package/src/layout/facets.js +1 -1
  200. package/src/layout/legendGroup.js +1 -1
  201. package/src/layout/legendItems.js +26 -10
  202. package/src/layout/legendSpacing.js +66 -0
  203. package/src/layout/plotDrawOrder.js +17 -0
  204. package/src/materialization/bars/histogram.js +8 -2
  205. package/src/materialization/bars/resolve.js +1 -1
  206. package/src/materialization/composition.js +1 -0
  207. package/src/materialization/compositionSnapshot.js +22 -2
  208. package/src/materialization/dataProvenance.js +1 -1
  209. package/src/materialization/facetGuides/layout.js +1 -1
  210. package/src/materialization/facetGuides/placement.js +23 -11
  211. package/src/materialization/facetGuides/preparation.js +43 -24
  212. package/src/materialization/facetHeaders.js +1 -1
  213. package/src/materialization/facetTopLane.js +25 -0
  214. package/src/materialization/facets.js +1 -1
  215. package/src/materialization/graphicHierarchy.js +5 -0
  216. package/src/materialization/graphicTranslation.js +2 -0
  217. package/src/materialization/guides/layout.js +1 -1
  218. package/src/materialization/guides/resources.js +1 -1
  219. package/src/materialization/legends.js +1 -1
  220. package/src/materialization/markClip.js +26 -0
  221. package/src/materialization/marks/capabilities.js +3 -2
  222. package/src/materialization/marks/grouping.js +7 -5
  223. package/src/materialization/marks/index.js +4 -2
  224. package/src/materialization/marks/policies.js +4 -0
  225. package/src/materialization/plotDrawOrder.js +44 -0
  226. package/src/materialization/rect.js +11 -3
  227. package/src/materialization/scaleGuideDependencies.js +13 -5
  228. package/src/materialization/selection/items/common.js +1 -1
  229. package/src/materialization/selection/items/path.js +1 -0
  230. package/src/materialization/selection/policies/index.js +2 -1
  231. package/src/materialization/text.js +10 -5
  232. package/src/mcp/cli.js +0 -0
  233. package/src/persistence/validation.js +108 -18
  234. package/src/persistence.js +4 -2
  235. package/src/renderers/canvas/index.js +16 -0
  236. package/src/renderers/canvas/isolated.js +11 -1
  237. package/src/renderers/canvas/native.js +7 -0
  238. package/src/renderers/canvas/scene.js +66 -0
  239. package/src/renderers/canvas/text.js +3 -2
  240. package/src/renderers/svg.js +1 -617
  241. package/src/renderers/svgSerialization.js +643 -0
  242. package/src/renderers/transitionScene.js +67 -0
  243. package/src/scene.js +89 -0
  244. package/src/selectors/axes.js +1 -1
  245. package/src/selectors/datasets.js +1 -0
  246. package/src/selectors/grids.js +9 -0
  247. package/src/selectors/markClip.js +28 -0
  248. package/src/selectors/raincloud.js +7 -0
  249. package/src/version.js +1 -1
  250. package/types/basic.d.ts +7 -0
  251. package/types/index.d.ts +8 -0
  252. package/types/program.d.ts +270 -60
  253. package/types/scene.d.ts +35 -0
@@ -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
+ }
@@ -8,12 +8,14 @@ const TRANSFORM_TOPOLOGY = Object.freeze({
8
8
  density: Object.freeze({ facetTopology: "statistical" }),
9
9
  ecdf: Object.freeze({ facetTopology: "statistical" }),
10
10
  filter: Object.freeze({ facetTopology: "rowPreserving" }),
11
+ flatten: Object.freeze({ facetTopology: "statistical" }),
12
+ lookup: Object.freeze({ facetTopology: "rowPreserving" }),
11
13
  fold: Object.freeze({ facetTopology: "statistical" }),
12
14
  gradientProfile: Object.freeze({ facetTopology: "statistical" }),
13
15
  horizon: Object.freeze({ facetTopology: "statistical" }),
14
16
  interval: Object.freeze({ facetTopology: "statistical" }),
15
17
  impute: Object.freeze({ facetTopology: "statistical" }),
16
- markFilter: Object.freeze({ provenanceTransparent: true }),
18
+ markFilter: Object.freeze({ provenanceTransparent: true, facetTopology: "statistical" }),
17
19
  normalize: Object.freeze({ facetTopology: "statistical" }),
18
20
  regression: Object.freeze({ facetTopology: "statistical" }),
19
21
  regressionStatistics: Object.freeze({ facetTopology: "statistical" }),
@@ -29,6 +29,8 @@ import {
29
29
  validateFilterTransform
30
30
  } from "./filter.js";
31
31
  import { normalizeFoldTransform, validateFoldTransform } from "./fold.js";
32
+ import { normalizeFlattenTransform, validateFlattenTransform } from "./flatten.js";
33
+ import {normalizeLookupTransform,validateLookupTransform} from "./lookup.js";
32
34
  import {
33
35
  requestedGradientProfileTransform,
34
36
  validateGradientProfileTransform
@@ -381,6 +383,28 @@ const TRANSFORM_POLICIES = Object.freeze({
381
383
  normalizeEdit: normalizeFilterTransformEdit,
382
384
  outputRoles: () => ({})
383
385
  }),
386
+ flatten: Object.freeze({
387
+ ...findTransformTopology("flatten"),
388
+ validate: validateFlattenTransform,
389
+ materializeOp: "materializeFlattenData",
390
+ replayTransform: requestedTransform,
391
+ editable: true,
392
+ normalizeEdit: normalizeSimple(normalizeFlattenTransform),
393
+ outputRoles: transform => Object.fromEntries(
394
+ transform.fields.map((field, index) => [`value:${field}`, transform.as[index]])
395
+ )
396
+ }),
397
+ lookup: Object.freeze({
398
+ ...findTransformTopology("lookup"),
399
+ validate:validateLookupTransform,
400
+ materializeOp:"materializeLookupData",
401
+ replayTransform:requestedTransform,
402
+ editable:true,
403
+ normalizeEdit:normalizeSimple(normalizeLookupTransform),
404
+ outputRoles:transform=>Object.fromEntries(transform.lookup.fields.map((field,index)=>[
405
+ `value:${field}`,transform.lookup.as[index]
406
+ ]))
407
+ }),
384
408
  fold: Object.freeze({
385
409
  ...findTransformTopology("fold"),
386
410
  validate: validateFoldTransform,