ggaction 0.0.16 → 0.0.18

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 (299) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +3 -2
  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/knowledge/task-resolver.js +1 -1
  8. package/package.json +10 -4
  9. package/src/BasicChartProgram.js +1 -1
  10. package/src/accessibility.js +3 -2
  11. package/src/actions/boxPlots/create.js +14 -4
  12. package/src/actions/boxPlots/edit.js +39 -33
  13. package/src/actions/boxPlots/materialize.js +7 -3
  14. package/src/actions/boxPlots/options.js +37 -11
  15. package/src/actions/canvas/actions.js +70 -95
  16. package/src/actions/canvas/fitting.js +16 -1
  17. package/src/actions/canvas/index.js +3 -0
  18. package/src/actions/canvas/plotBorder.js +28 -0
  19. package/src/actions/charts/area.js +2 -2
  20. package/src/actions/charts/bar.js +34 -7
  21. package/src/actions/charts/ecdf.js +4 -18
  22. package/src/actions/charts/endpoints.js +4 -18
  23. package/src/actions/charts/heatmap.js +4 -4
  24. package/src/actions/charts/index.js +2 -0
  25. package/src/actions/charts/interval-regression.js +2 -2
  26. package/src/actions/charts/line.js +1 -1
  27. package/src/actions/charts/pie.js +3 -47
  28. package/src/actions/charts/radial.js +34 -12
  29. package/src/actions/charts/scatter.js +16 -2
  30. package/src/actions/charts/shared.js +2 -2
  31. package/src/actions/charts/text.js +35 -0
  32. package/src/actions/composition/actions.js +6 -2
  33. package/src/actions/data/bin.js +1 -1
  34. package/src/actions/data/box.js +2 -1
  35. package/src/actions/data/create.js +18 -5
  36. package/src/actions/data/density.js +1 -1
  37. package/src/actions/data/derived.js +15 -1
  38. package/src/actions/data/edit.js +20 -7
  39. package/src/actions/data/filter.js +15 -4
  40. package/src/actions/data/index.js +3 -0
  41. package/src/actions/data/interval.js +2 -0
  42. package/src/actions/data/regression.js +6 -2
  43. package/src/actions/data/revise.js +37 -5
  44. package/src/actions/data/shared.js +33 -4
  45. package/src/actions/data/sort.js +21 -0
  46. package/src/actions/data/stack.js +3 -1
  47. package/src/actions/data/summary.js +1 -1
  48. package/src/actions/encodings/angle.js +4 -1
  49. package/src/actions/encodings/appearance.js +43 -31
  50. package/src/actions/encodings/barWidth.js +36 -11
  51. package/src/actions/encodings/channels.js +0 -1
  52. package/src/actions/encodings/color/continuous.js +5 -2
  53. package/src/actions/encodings/color/index.js +5 -2
  54. package/src/actions/encodings/color/policy.js +10 -3
  55. package/src/actions/encodings/density/resolve.js +1 -6
  56. package/src/actions/encodings/offset.js +4 -1
  57. package/src/actions/encodings/position/policies/arc.js +12 -10
  58. package/src/actions/encodings/position/policies/area.js +3 -3
  59. package/src/actions/encodings/position/policies/bar.js +15 -6
  60. package/src/actions/encodings/position/policies/line.js +17 -6
  61. package/src/actions/encodings/position/resolve.js +9 -3
  62. package/src/actions/encodings/ranged.js +23 -1
  63. package/src/actions/encodings/remove.js +3 -2
  64. package/src/actions/encodings/ruleAppearance.js +4 -1
  65. package/src/actions/encodings/seriesLayout.js +1 -0
  66. package/src/actions/encodings/stroke.js +4 -1
  67. package/src/actions/encodings/strokeDash.js +6 -1
  68. package/src/actions/encodings/text.js +12 -4
  69. package/src/actions/errorBands/create.js +2 -2
  70. package/src/actions/errorBands/edit.js +2 -1
  71. package/src/actions/facets/actions.js +158 -240
  72. package/src/actions/gradientPlots/resolve.js +2 -17
  73. package/src/actions/guides/applicability.js +1 -1
  74. package/src/actions/guides/axes/axis.js +16 -4
  75. package/src/actions/guides/axes/edit.js +4 -4
  76. package/src/actions/guides/axes/labels.js +120 -46
  77. package/src/actions/guides/axes/lines.js +6 -5
  78. package/src/actions/guides/axes/parallel/lifecycle.js +1 -1
  79. package/src/actions/guides/axes/parallel/policy.js +8 -7
  80. package/src/actions/guides/axes/remove.js +3 -2
  81. package/src/actions/guides/axes/tickGroups.js +5 -2
  82. package/src/actions/guides/axes/ticks.js +6 -3
  83. package/src/actions/guides/axes/titles.js +71 -26
  84. package/src/actions/guides/basic.js +2 -0
  85. package/src/actions/guides/facadeLegend.js +10 -2
  86. package/src/actions/guides/grids/grid.js +7 -1
  87. package/src/actions/guides/grids/resolve.js +59 -12
  88. package/src/actions/guides/legends/blocks.js +3 -1
  89. package/src/actions/guides/legends/categorical/actions.js +70 -17
  90. package/src/actions/guides/legends/categorical/components.js +12 -8
  91. package/src/actions/guides/legends/categorical/index.js +6 -0
  92. package/src/actions/guides/legends/categorical/layout.js +37 -8
  93. package/src/actions/guides/legends/categorical/options.js +31 -20
  94. package/src/actions/guides/legends/categorical/resolve.js +2 -2
  95. package/src/actions/guides/legends/categorical/symbols.js +18 -12
  96. package/src/actions/guides/legends/categorical/validation.js +1 -6
  97. package/src/actions/guides/legends/continuous/colorSymbols.js +1 -0
  98. package/src/actions/guides/legends/continuous/common.js +1 -447
  99. package/src/actions/guides/legends/continuous/gradient.js +47 -34
  100. package/src/actions/guides/legends/continuous/interval.js +4 -4
  101. package/src/actions/guides/legends/continuous/opacity.js +19 -12
  102. package/src/actions/guides/legends/edit.js +1 -604
  103. package/src/actions/guides/legends/focused.js +2 -2
  104. package/src/actions/guides/legends/group.js +1 -0
  105. package/src/actions/guides/legends/lane.js +1 -516
  106. package/src/actions/guides/legends/remove.js +3 -0
  107. package/src/actions/guides/legends/sampling.js +1 -1
  108. package/src/actions/guides/legends/size.js +51 -43
  109. package/src/actions/guides/legends/strokeWidth.js +11 -29
  110. package/src/actions/guides/legends/target.js +3 -3
  111. package/src/actions/guides/legends/transition.js +21 -7
  112. package/src/actions/guides/polar/axes/labels.js +4 -2
  113. package/src/actions/guides/polar/axes/titles.js +7 -5
  114. package/src/actions/guides/polar/resolve.js +6 -14
  115. package/src/actions/index.js +16 -16
  116. package/src/actions/marks/arc/actions.js +3 -10
  117. package/src/actions/marks/area/actions.js +8 -5
  118. package/src/actions/marks/area/materialize.js +8 -9
  119. package/src/actions/marks/bar/create.js +17 -4
  120. package/src/actions/marks/bar/edit.js +18 -7
  121. package/src/actions/marks/bar/materialize.js +9 -6
  122. package/src/actions/marks/line/actions.js +11 -4
  123. package/src/actions/marks/line/materialize.js +9 -5
  124. package/src/actions/marks/point/create.js +8 -1
  125. package/src/actions/marks/point/edit.js +13 -6
  126. package/src/actions/marks/point/materialize.js +4 -2
  127. package/src/actions/marks/rect/actions.js +26 -11
  128. package/src/actions/marks/remove.js +2 -0
  129. package/src/actions/marks/rule/actions.js +31 -12
  130. package/src/actions/marks/text/actions.js +42 -11
  131. package/src/actions/marks/text/layout.js +3 -0
  132. package/src/actions/marks/tick/actions.js +23 -8
  133. package/src/actions/primitives/semanticAction.js +37 -3
  134. package/src/actions/primitives/semanticValidation/dataset.js +13 -1
  135. package/src/actions/primitives/semanticValidation/layer.js +26 -2
  136. package/src/actions/primitives/semanticValidation/scale.js +6 -0
  137. package/src/actions/regression/create.js +26 -5
  138. package/src/actions/regression/edit.js +50 -15
  139. package/src/actions/regression/follow.js +22 -0
  140. package/src/actions/regression/resolve.js +2 -6
  141. package/src/actions/scales/channels.js +20 -15
  142. package/src/actions/scales/consumers/common.js +2 -1
  143. package/src/actions/scales/consumers/families.js +16 -5
  144. package/src/actions/scales/create.js +10 -4
  145. package/src/actions/scales/definitions.js +1 -425
  146. package/src/actions/scales/edit.js +3 -3
  147. package/src/actions/scales/editPolicy.js +9 -3
  148. package/src/actions/scales/materialize.js +1 -1
  149. package/src/actions/scales/parallel.js +1 -1
  150. package/src/actions/scales/patch.js +1 -1
  151. package/src/actions/scales/preview.js +49 -2
  152. package/src/actions/scales/quantitativeColor.js +3 -3
  153. package/src/actions/selection/actions.js +1 -13
  154. package/src/actions/theme/reconcile.js +83 -27
  155. package/src/actions/theme/state.js +12 -0
  156. package/src/actions/titles/actions.js +5 -18
  157. package/src/actions/titles/resolve.js +41 -19
  158. package/src/actions/violinPlots/edit.js +3 -20
  159. package/src/core/action.js +3 -2
  160. package/src/core/compositionState.js +4 -1
  161. package/src/core/font.js +9 -2
  162. package/src/core/optionRequirements.js +1 -0
  163. package/src/core/programState.js +5 -1
  164. package/src/core/resourceReferences.js +3 -0
  165. package/src/core/textMetricProfile.js +3 -2
  166. package/src/core/textMetrics.js +41 -4
  167. package/src/core/validation.js +26 -0
  168. package/src/core/vocabulary.js +4 -0
  169. package/src/grammar/aggregate.js +4 -18
  170. package/src/grammar/arcs.js +1 -386
  171. package/src/grammar/areaPaint.js +10 -0
  172. package/src/grammar/areaSeries.js +18 -10
  173. package/src/grammar/bars/geometry.js +30 -3
  174. package/src/grammar/bars/policy.js +12 -1
  175. package/src/grammar/bin.js +41 -22
  176. package/src/grammar/bin2d.js +1 -6
  177. package/src/grammar/boxPlot.js +56 -9
  178. package/src/grammar/categoryOrder.js +1 -6
  179. package/src/grammar/complete.js +15 -19
  180. package/src/grammar/computed.js +92 -25
  181. package/src/grammar/curveCommands.js +29 -16
  182. package/src/grammar/datasetSchema.js +382 -0
  183. package/src/grammar/density.js +53 -5
  184. package/src/grammar/displayLabels.js +28 -0
  185. package/src/grammar/ecdf.js +2 -9
  186. package/src/grammar/facets/dependencies.js +6 -2
  187. package/src/grammar/facets/guides.js +5 -0
  188. package/src/grammar/facets/index.js +2 -5
  189. package/src/grammar/filter.js +133 -39
  190. package/src/grammar/fold.js +2 -12
  191. package/src/grammar/gradientProfile.js +1 -6
  192. package/src/grammar/impute.js +21 -48
  193. package/src/grammar/interval.js +34 -10
  194. package/src/grammar/itemMissing.js +59 -0
  195. package/src/grammar/legendGradient.js +18 -0
  196. package/src/grammar/legendGroups.js +1 -0
  197. package/src/grammar/legendTitleStyle.js +22 -0
  198. package/src/grammar/lineSeries.js +32 -17
  199. package/src/grammar/markLabels.js +5 -2
  200. package/src/grammar/normalize.js +4 -54
  201. package/src/grammar/numeric.js +1 -290
  202. package/src/grammar/parallelCoordinates.js +1 -6
  203. package/src/grammar/parseTime.js +41 -0
  204. package/src/grammar/pathCommands.js +11 -4
  205. package/src/grammar/polar.js +1 -184
  206. package/src/grammar/positionCompatibility.js +3 -3
  207. package/src/grammar/regression/derive.js +63 -16
  208. package/src/grammar/regression/index.js +1 -0
  209. package/src/grammar/regression/models.js +116 -59
  210. package/src/grammar/regression/parameters.js +69 -6
  211. package/src/grammar/roundedRect.js +42 -54
  212. package/src/grammar/scales/appearance.js +1 -206
  213. package/src/grammar/scales/color.js +1 -382
  214. package/src/grammar/scales/colorConsumers.js +5 -2
  215. package/src/grammar/scales/definition.js +1 -190
  216. package/src/grammar/scales/discretized.js +1 -195
  217. package/src/grammar/scales/size.js +1 -456
  218. package/src/grammar/scales/temporal.js +2 -13
  219. package/src/grammar/scales/transformed.js +1 -383
  220. package/src/grammar/scales/types.js +1 -1
  221. package/src/grammar/scales/validation.js +1 -66
  222. package/src/grammar/schemas/concreteGraphic.js +11 -0
  223. package/src/grammar/schemas/graphic.js +3 -0
  224. package/src/grammar/schemas/semanticPath.js +5 -3
  225. package/src/grammar/sort.js +88 -0
  226. package/src/grammar/stack.js +2 -10
  227. package/src/grammar/statisticalReference.js +1 -6
  228. package/src/grammar/summary.js +120 -45
  229. package/src/grammar/text.js +19 -0
  230. package/src/grammar/timeUnit.js +16 -7
  231. package/src/grammar/transformKeys.js +47 -0
  232. package/src/grammar/transformTopology.js +1 -0
  233. package/src/grammar/transforms.js +25 -12
  234. package/src/grammar/valueFormat.js +17 -5
  235. package/src/grammar/weightedStatistics.js +1 -6
  236. package/src/grammar/window.js +4 -58
  237. package/src/inspection.js +495 -0
  238. package/src/inspectionDescriptors.js +3 -0
  239. package/src/layout/canvas.js +1 -110
  240. package/src/layout/facets.js +1 -244
  241. package/src/layout/labels.js +1 -0
  242. package/src/layout/legend.js +13 -0
  243. package/src/layout/legendGroup.js +1 -0
  244. package/src/layout/legendItems.js +59 -15
  245. package/src/layout/legendLane.js +1 -441
  246. package/src/layout/legendText.js +1 -0
  247. package/src/layout/title.js +35 -13
  248. package/src/materialization/bars/aggregate.js +11 -5
  249. package/src/materialization/bars/histogram.js +4 -1
  250. package/src/materialization/bars/ranged.js +19 -6
  251. package/src/materialization/bars/resolve.js +26 -3
  252. package/src/materialization/coordinateBounds.js +8 -0
  253. package/src/materialization/dependencies.js +4 -44
  254. package/src/materialization/facetGuides/layout.js +1 -0
  255. package/src/materialization/facetGuides/placement.js +19 -8
  256. package/src/materialization/facetGuides/preparation.js +15 -7
  257. package/src/materialization/facetHeaders.js +1 -347
  258. package/src/materialization/facets.js +1 -185
  259. package/src/materialization/graphicHierarchy.js +1 -1
  260. package/src/materialization/guides/layout.js +1 -57
  261. package/src/materialization/guides/resources.js +1 -123
  262. package/src/materialization/layout.js +53 -0
  263. package/src/materialization/legendGroups.js +1 -0
  264. package/src/materialization/legendText.js +1 -0
  265. package/src/materialization/legends.js +1 -1
  266. package/src/materialization/marks/capabilities.js +5 -6
  267. package/src/materialization/rect.js +1 -1
  268. package/src/materialization/revisionIdentity.js +19 -0
  269. package/src/materialization/scaleGuideDependencies.js +1 -1
  270. package/src/materialization/scales/map.js +3 -0
  271. package/src/materialization/scales/policies/arc.js +8 -7
  272. package/src/materialization/scales/policies/bar.js +19 -7
  273. package/src/materialization/scales/policies/series.js +2 -1
  274. package/src/materialization/scales/resolve.js +72 -7
  275. package/src/materialization/selection/items/arc.js +1 -1
  276. package/src/materialization/selection/items/bar.js +7 -6
  277. package/src/materialization/selection/items/path.js +7 -6
  278. package/src/materialization/selection/items/rect.js +2 -2
  279. package/src/materialization/text.js +17 -6
  280. package/src/materialization/textLines.js +15 -0
  281. package/src/persistence/codec.js +37 -6
  282. package/src/persistence/validation.js +47 -0
  283. package/src/persistence.js +94 -8
  284. package/src/renderers/canvas/circle.js +6 -3
  285. package/src/renderers/canvas/fill.js +7 -1
  286. package/src/renderers/canvas/index.js +2 -1
  287. package/src/renderers/canvas/line.js +1 -0
  288. package/src/renderers/canvas/path.js +1 -1
  289. package/src/renderers/canvas/rect.js +11 -8
  290. package/src/renderers/canvas/stroke.js +2 -1
  291. package/src/renderers/canvas/text.js +5 -3
  292. package/src/renderers/svg.js +4 -1
  293. package/src/selectors/layers.js +19 -0
  294. package/src/theme/defaults.js +8 -2
  295. package/src/version.js +1 -1
  296. package/types/basic.d.ts +1 -0
  297. package/types/index.d.ts +7 -0
  298. package/types/inspection.d.ts +17 -0
  299. package/types/program.d.ts +494 -112
@@ -13,30 +13,31 @@ function indexSegments(segments) {
13
13
  return indexed;
14
14
  }
15
15
 
16
- export function deriveAreaSeries(rows, layer) {
16
+ export function deriveAreaSeries(rows, layer, options = {}) {
17
17
  if (layer?.mark?.type !== "area") {
18
18
  throw new Error("Area series derivation requires a semantic area mark.");
19
19
  }
20
20
  const { x, y, x2, y2 } = layer.encoding ?? {};
21
21
  const vertical =
22
- ["quantitative", "temporal"].includes(x?.fieldType) &&
22
+ ["quantitative", "temporal", "nominal", "ordinal"].includes(x?.fieldType) &&
23
23
  y?.fieldType === "quantitative" &&
24
24
  y2?.fieldType === "quantitative" &&
25
25
  x2 === undefined;
26
26
  const horizontal =
27
- ["quantitative", "temporal"].includes(y?.fieldType) &&
27
+ ["quantitative", "temporal", "nominal", "ordinal"].includes(y?.fieldType) &&
28
28
  x?.fieldType === "quantitative" &&
29
29
  x2?.fieldType === "quantitative" &&
30
30
  y2 === undefined;
31
31
  if (vertical === horizontal) {
32
32
  throw new Error(
33
- `Area mark "${layer.id}" requires exactly one quantitative x/x2 or y/y2 range and one quantitative or temporal independent position.`
33
+ `Area mark "${layer.id}" requires exactly one quantitative x/x2 or y/y2 range and one quantitative, temporal, or categorical independent position.`
34
34
  );
35
35
  }
36
36
  const grouping = validatePathSeriesAppearance(rows, layer);
37
37
  const orientation = vertical ? "vertical" : "horizontal";
38
38
  const independent = vertical ? x : y;
39
- const independentValues = independent.fieldType === "temporal"
39
+ const categorical = ["nominal", "ordinal"].includes(independent.fieldType);
40
+ const independentValues = categorical ? readNominalField(rows, independent.field) : independent.fieldType === "temporal"
40
41
  ? readTemporalField(rows, independent.field, independent.temporalUnit)
41
42
  : readQuantitativeField(rows, independent.field);
42
43
  if (Object.hasOwn(independent, "datum")) throw new Error("Area independent position requires a field.");
@@ -46,6 +47,9 @@ export function deriveAreaSeries(rows, layer) {
46
47
  const missing = layer.mark.missing ?? "error";
47
48
  const lower = readAreaEndpoint(rows, primary, missing);
48
49
  const upper = readAreaEndpoint(rows, secondary, missing);
50
+ const categoryOrder = categorical ? options[vertical ? "xDomain" : "yDomain"] ?? [...new Set(independentValues)] : undefined;
51
+ const ranks = new Map(categoryOrder?.map((value, index) => [value, index]));
52
+ if (categorical && independentValues.some(value => !ranks.has(value))) throw new Error("Area categorical domain must contain every observed position.");
49
53
  const pathOrder = layer.encoding?.pathOrder;
50
54
  const orderValues = pathOrder === undefined
51
55
  ? undefined
@@ -83,7 +87,7 @@ export function deriveAreaSeries(rows, layer) {
83
87
  const series = [...groups.values()].flatMap(item => {
84
88
  const key = vertical ? "x" : "y";
85
89
  const values = pathOrder === undefined
86
- ? item.values.sort((left, right) => left[key] - right[key])
90
+ ? item.values.sort((left, right) => categorical ? ranks.get(left[key]) - ranks.get(right[key]) : left[key] - right[key])
87
91
  : stableOrderPathValues(
88
92
  item.values.map(({ pathOrder: _pathOrder, ...value }) => value),
89
93
  item.values.map(value => value.pathOrder),
@@ -125,13 +129,13 @@ export function deriveAreaSeries(rows, layer) {
125
129
  });
126
130
  }
127
131
 
128
- export function deriveCenteredAreaSeries(rows, layer) {
132
+ export function deriveCenteredAreaSeries(rows, layer, options = {}) {
129
133
  if (layer?.mark?.type !== "area") {
130
134
  throw new Error("Centered area series derivation requires an area mark.");
131
135
  }
132
136
  const { x, y, x2, y2, group, color } = layer.encoding ?? {};
133
137
  if (
134
- !["quantitative", "temporal"].includes(x?.fieldType) ||
138
+ !["quantitative", "temporal", "nominal", "ordinal"].includes(x?.fieldType) ||
135
139
  y?.fieldType !== "quantitative" ||
136
140
  y?.stack !== "center" ||
137
141
  x2 !== undefined ||
@@ -149,7 +153,8 @@ export function deriveCenteredAreaSeries(rows, layer) {
149
153
  `Centered area color on mark "${layer.id}" must match its group field.`
150
154
  );
151
155
  }
152
- const xValues = x.fieldType === "temporal"
156
+ const categorical = ["nominal", "ordinal"].includes(x.fieldType);
157
+ const xValues = categorical ? readNominalField(rows, x.field) : x.fieldType === "temporal"
153
158
  ? readTemporalField(rows, x.field, x.temporalUnit)
154
159
  : readQuantitativeField(rows, x.field);
155
160
  const yValues = readQuantitativeField(rows, y.field);
@@ -172,7 +177,10 @@ export function deriveCenteredAreaSeries(rows, layer) {
172
177
  values.set(xValues[index], yValues[index]);
173
178
  positions.add(xValues[index]);
174
179
  }
175
- const orderedPositions = [...positions].sort((left, right) => left - right);
180
+ const orderedPositions = categorical
181
+ ? (options.xDomain ?? [...positions]).filter(value => positions.has(value))
182
+ : [...positions].sort((left, right) => left - right);
183
+ if (orderedPositions.length !== positions.size) throw new Error("Area categorical domain must contain every observed position.");
176
184
  if (groupOrder.length === 0 || orderedPositions.length < 2) {
177
185
  throw new Error(
178
186
  `Centered area mark "${layer.id}" requires at least two aligned positions.`
@@ -1,3 +1,6 @@
1
+ import { validateOptionObject } from "../../core/validation.js";
2
+ import { floorUtcTimeUnit, offsetUtcTimeUnit, TIME_UNITS, validTimestamp } from "../timeUnit.js";
3
+ import { mapContinuousScaleValues } from "../scales/mapping.js";
1
4
  import { cloneAndFreeze } from "../../core/immutable.js";
2
5
 
3
6
  export const DEFAULT_BAR_WIDTH = Object.freeze({ band: 0.72 });
@@ -9,8 +12,16 @@ export const DEFAULT_OFFSET_PADDING = Object.freeze({
9
12
  export function normalizeBarWidth(options = {}, existing) {
10
13
  const hasBand = options.band !== undefined;
11
14
  const hasPixels = options.pixels !== undefined;
12
- if (hasBand && hasPixels) {
13
- throw new Error("encodeBarWidth band and pixels are mutually exclusive.");
15
+ const hasInterval = options.interval !== undefined;
16
+ if (Number(hasBand) + Number(hasPixels) + Number(hasInterval) > 1) {
17
+ throw new Error("encodeBarWidth band, pixels, and interval are mutually exclusive.");
18
+ }
19
+ if (hasInterval) {
20
+ validateOptionObject(options.interval, ["unit", "insetStart", "insetEnd"], "Bar width interval");
21
+ const { unit, insetStart = 0, insetEnd = 0 } = options.interval;
22
+ if (!TIME_UNITS.includes(unit) || ["week", "weekday"].includes(unit)) throw new Error("Bar width interval requires year, quarter, month, day, hour, minute, or second.");
23
+ if (![insetStart, insetEnd].every(v => Number.isFinite(v) && v >= 0)) throw new RangeError("Bar interval insets must be non-negative finite pixels.");
24
+ return cloneAndFreeze({ interval: { unit, insetStart, insetEnd } });
14
25
  }
15
26
  if (!hasBand && !hasPixels) {
16
27
  return cloneAndFreeze(existing ?? DEFAULT_BAR_WIDTH);
@@ -90,9 +101,25 @@ export function normalizeOffsetScalePolicy(
90
101
  }
91
102
 
92
103
  export function resolveBarWidth(config, slotBandwidth) {
104
+ const normalized = normalizeBarWidth(config);
105
+ if (normalized.pixels !== undefined) return normalized.pixels;
93
106
  if (!Number.isFinite(slotBandwidth) || slotBandwidth <= 0) {
94
107
  throw new Error("Bar width requires a positive resolved slot bandwidth.");
95
108
  }
96
- const normalized = normalizeBarWidth(config);
97
109
  return normalized.pixels ?? slotBandwidth * normalized.band;
98
110
  }
111
+
112
+
113
+ // Width intent never buckets or aggregates data: category values are interval starts.
114
+ export function resolveBarInterval(config, value, scale) {
115
+ if (scale.type !== "time") throw new Error("Bar interval width requires a temporal category scale.");
116
+ const { unit, insetStart, insetEnd } = normalizeBarWidth(config).interval;
117
+ const start = floorUtcTimeUnit(value, unit);
118
+ if (start !== value) throw new Error("Bar interval category values must be UTC interval starts.");
119
+ const end = offsetUtcTimeUnit(start, unit);
120
+ if (!validTimestamp(end)) throw new RangeError("Bar interval end exceeds the supported Date range.");
121
+ if (start < Math.min(...scale.domain) || end > Math.max(...scale.domain)) throw new RangeError("Bar interval endpoints must be inside the temporal scale domain; set an explicit covering domain.");
122
+ const [a, b] = mapContinuousScaleValues([start, end], scale), width = Math.abs(b - a) - insetStart - insetEnd;
123
+ if (width <= 0) throw new RangeError("Bar interval insets must leave a positive width.");
124
+ return { position: Math.min(a, b) + (b >= a ? insetStart : insetEnd), width };
125
+ }
@@ -3,6 +3,7 @@ import { isAggregate } from "../aggregate.js";
3
3
  export const BAR_GRAINS = Object.freeze({
4
4
  histogram: "histogram",
5
5
  aggregate: "aggregate",
6
+ centered: "centered",
6
7
  ranged: "ranged"
7
8
  });
8
9
 
@@ -20,10 +21,17 @@ function isMeasure(encoding) {
20
21
  isAggregate(encoding.aggregate);
21
22
  }
22
23
 
24
+ function hasNumericCenters(layer) {
25
+ return layer.mark.orientation !== undefined && [layer.encoding?.x, layer.encoding?.y].every(
26
+ encoding => encoding?.fieldType === "quantitative" && encoding.bin === undefined && encoding.aggregate === undefined
27
+ );
28
+ }
29
+
23
30
  export function resolveBarOrientation(layer) {
24
31
  if (layer?.mark?.type !== "bar") return undefined;
25
32
  const x = layer.encoding?.x;
26
33
  const y = layer.encoding?.y;
34
+ if (hasNumericCenters(layer)) return layer.mark.orientation;
27
35
  if (x?.bin !== undefined && y?.aggregate === "count") {
28
36
  return BAR_ORIENTATIONS.vertical;
29
37
  }
@@ -56,6 +64,8 @@ export function resolveBarGrain(layer) {
56
64
  const x = layer.encoding?.x;
57
65
  const y = layer.encoding?.y;
58
66
 
67
+ if (hasNumericCenters(layer)) return BAR_GRAINS.centered;
68
+
59
69
  if (
60
70
  x?.bin !== undefined &&
61
71
  y?.aggregate === "count"
@@ -80,7 +90,7 @@ export function inferBarColorLayout(layer) {
80
90
  const grain = resolveBarGrain(layer);
81
91
  if (grain === BAR_GRAINS.histogram) return "stack";
82
92
  if (grain === BAR_GRAINS.aggregate) return "group";
83
- if (grain === BAR_GRAINS.ranged) return "overlay";
93
+ if (grain === BAR_GRAINS.ranged || grain === BAR_GRAINS.centered) return "overlay";
84
94
  return undefined;
85
95
  }
86
96
 
@@ -89,6 +99,7 @@ export function resolveBarColorLayout(layer) {
89
99
  if (layer?.encoding?.color?.layout !== undefined) {
90
100
  return layer.encoding.color.layout;
91
101
  }
102
+ if (resolveBarGrain(layer) === BAR_GRAINS.centered) return "overlay";
92
103
  const channels = resolveBarChannels(layer);
93
104
  if (layer?.encoding?.[channels?.measure]?.stack === "normalize") return "fill";
94
105
  const offsetChannel = resolveBarOffsetChannel(layer);
@@ -1,3 +1,5 @@
1
+ import { rejectUnknownProperties as rejectUnknownKeys } from "../core/validation.js";
2
+ import { requireStringValue as requireField } from "../core/validation.js";
1
3
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
4
  import {
3
5
  findHistogramBinIndex,
@@ -15,24 +17,12 @@ import {
15
17
 
16
18
  const TRANSFORM_KEYS = Object.freeze([
17
19
  "type", "field", "bin", "extent", "nice", "zero", "includeEmpty",
18
- "members", "as", "weight", "resolved"
20
+ "members", "as", "weight", "missing", "resolved"
19
21
  ]);
20
22
  const AS_KEYS = Object.freeze(["lower", "upper", "count", "members"]);
21
23
  const RESOLVED_KEYS = Object.freeze(["domain", "step", "boundaries"]);
22
24
 
23
- function rejectUnknownKeys(value, supported, label) {
24
- const unknown = Object.keys(value).find(key => !supported.includes(key));
25
- if (unknown !== undefined) {
26
- throw new Error(`Unknown ${label} property "${unknown}".`);
27
- }
28
- }
29
25
 
30
- function requireField(value, label) {
31
- if (typeof value !== "string" || value.length === 0) {
32
- throw new TypeError(`${label} must be a non-empty string.`);
33
- }
34
- return value;
35
- }
36
26
 
37
27
  function normalizeAs(field, value, members) {
38
28
  const requested = value ?? {};
@@ -59,7 +49,8 @@ export function normalizeBinTransform({
59
49
  includeEmpty,
60
50
  members,
61
51
  as,
62
- weight
52
+ weight,
53
+ missing
63
54
  } = {}) {
64
55
  const selected = [maxBins, step, boundaries].filter(value => value !== undefined);
65
56
  if (selected.length > 1) {
@@ -83,7 +74,8 @@ export function normalizeBinTransform({
83
74
  as: normalizeAs(field, as, resolvedMembers),
84
75
  ...(weight === undefined
85
76
  ? {}
86
- : { weight: normalizeStatisticalWeight(weight, "Bin weight") })
77
+ : { weight: normalizeStatisticalWeight(weight, "Bin weight") }),
78
+ ...(missing === undefined ? {} : { missing })
87
79
  };
88
80
  validateBinTransform(transform);
89
81
  return cloneAndFreeze(transform);
@@ -101,6 +93,9 @@ export function validateBinTransform(transform) {
101
93
  if (transform.weight !== undefined) {
102
94
  normalizeStatisticalWeight(transform.weight, "Bin weight");
103
95
  }
96
+ if (transform.missing !== undefined && !["error", "drop"].includes(transform.missing)) {
97
+ throw new Error('Bin missing must be "error" or "drop".');
98
+ }
104
99
  normalizeHistogramBin(transform.bin);
105
100
  if (
106
101
  transform.extent !== "auto" &&
@@ -142,22 +137,31 @@ export function validateBinTransform(transform) {
142
137
  return transform;
143
138
  }
144
139
 
145
- function readValues(rows, field) {
146
- return rows.map((row, index) => {
140
+ function readValues(rows, field, missing) {
141
+ const eligible = [];
142
+ let excluded = 0;
143
+ rows.forEach((row, index) => {
147
144
  const value = row[field];
145
+ if (value === null || value === undefined) {
146
+ if (missing === "drop") { excluded += 1; return; }
147
+ throw new TypeError(`Bin field "${field}" must contain a finite number at row ${index}.`);
148
+ }
148
149
  if (!Number.isFinite(value)) {
149
150
  throw new TypeError(`Bin field "${field}" must contain a finite number at row ${index}.`);
150
151
  }
151
- return value;
152
+ eligible.push({ row, index });
152
153
  });
154
+ return { eligible, excluded };
153
155
  }
154
156
 
155
157
  export function deriveBinRows(rows, transform) {
156
158
  validateBinTransform(transform);
157
- const values = readValues(rows, transform.field);
159
+ const selected = readValues(rows, transform.field, transform.missing);
160
+ const eligibleRows = selected.eligible.map(entry => entry.row);
161
+ const values = eligibleRows.map(row => row[transform.field]);
158
162
  const weights = transform.weight === undefined
159
163
  ? undefined
160
- : readStatisticalWeights(rows, transform.weight, "Bin");
164
+ : readStatisticalWeights(eligibleRows, transform.weight, "Bin");
161
165
  if (weights !== undefined) {
162
166
  validateWeightedNumericFields(weights.entries, [transform.field], "Bin");
163
167
  }
@@ -188,7 +192,7 @@ export function deriveBinRows(rows, transform) {
188
192
  });
189
193
  const members = resolved.boundaries.slice(0, -1).map(() => []);
190
194
  const entries = weightSummary === undefined
191
- ? rows.map((row, index) => ({ row, index }))
195
+ ? selected.eligible
192
196
  : weightSummary.positive;
193
197
  entries.forEach(entry => {
194
198
  const value = entry.row[transform.field];
@@ -217,7 +221,22 @@ export function deriveBinRows(rows, transform) {
217
221
  domain: resolved.domain,
218
222
  ...(resolved.step === undefined ? {} : { step: resolved.step }),
219
223
  boundaries: resolved.boundaries
220
- }
224
+ },
225
+ ...(transform.missing === undefined ? {} : {
226
+ report: {
227
+ version: 1,
228
+ owner: { kind: "data", id: "pending" },
229
+ inputs: [],
230
+ units: [{
231
+ role: transform.as.count,
232
+ group: {},
233
+ inputRows: rows.length,
234
+ usedRows: eligibleRows.length,
235
+ excludedRows: selected.excluded,
236
+ excludedByReason: selected.excluded === 0 ? {} : { "missing-value": selected.excluded }
237
+ }]
238
+ }
239
+ })
221
240
  };
222
241
  }
223
242
 
@@ -1,3 +1,4 @@
1
+ import { rejectUnknownProperties as rejectUnknownKeys } from "../core/validation.js";
1
2
  import {
2
3
  cloneAndFreeze,
3
4
  isPlainObject
@@ -18,12 +19,6 @@ const RESOLVED_KEYS = Object.freeze([
18
19
  "extent", "edges", "eligibleCount", "occupiedCount"
19
20
  ]);
20
21
 
21
- function rejectUnknownKeys(value, supported, label) {
22
- const unknown = Object.keys(value).find(key => !supported.includes(key));
23
- if (unknown !== undefined) {
24
- throw new Error(`Unknown ${label} property "${unknown}".`);
25
- }
26
- }
27
22
 
28
23
  function requirePositiveInteger(value, label) {
29
24
  if (!Number.isInteger(value) || value <= 0) {
@@ -1,5 +1,5 @@
1
1
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
- import { interpolateNumber, stableDecimal } from "./numeric.js";
2
+ import { quantileSorted, stableDecimal } from "./numeric.js";
3
3
 
4
4
  export const BOX_FIELDS = Object.freeze({
5
5
  q1: "__boxPlot_q1", median: "__boxPlot_median", q3: "__boxPlot_q3",
@@ -9,7 +9,7 @@ export const BOX_FIELDS = Object.freeze({
9
9
  });
10
10
 
11
11
  const TRANSFORM_KEYS = [
12
- "type", "category", "field", "method", "whisker", "factor", "as"
12
+ "type", "category", "field", "method", "whisker", "factor", "as", "summary"
13
13
  ];
14
14
  const OUTPUT_KEYS = Object.keys(BOX_FIELDS);
15
15
 
@@ -18,6 +18,36 @@ function field(value, label) {
18
18
  return value;
19
19
  }
20
20
 
21
+ const SUMMARY_KEYS = Object.freeze(["min", "q1", "median", "q3", "max"]);
22
+
23
+ export function normalizeBoxSummary(value) {
24
+ if (value === undefined) return undefined;
25
+ if (!isPlainObject(value) || Object.keys(value).length !== SUMMARY_KEYS.length ||
26
+ !SUMMARY_KEYS.every(key => Object.hasOwn(value, key))) {
27
+ throw new TypeError("Box summary requires min, q1, median, q3 and max field names.");
28
+ }
29
+ return cloneAndFreeze(Object.fromEntries(SUMMARY_KEYS.map(key => [key, field(value[key], `Box summary.${key}`)])));
30
+ }
31
+
32
+ function derivePrecomputedBoxData(rows, transform) {
33
+ const { category, summary, as } = transform;
34
+ const summaries = [];
35
+ for (const row of rows) {
36
+ const key = row[category];
37
+ if (key === undefined || key === null || key === "") continue;
38
+ const values = SUMMARY_KEYS.map(role => row[summary[role]]);
39
+ if (!values.every(Number.isFinite)) throw new TypeError("Precomputed box summaries require finite values for all five fields.");
40
+ if (values.some((value, index) => index > 0 && value < values[index - 1])) {
41
+ throw new RangeError("Box summary must satisfy min <= q1 <= median <= q3 <= max.");
42
+ }
43
+ const [min, q1, median, q3, max] = values;
44
+ summaries.push({ [category]: key, [as.q1]: q1, [as.median]: median, [as.q3]: q3,
45
+ [as.lowerWhisker]: min, [as.upperWhisker]: max });
46
+ }
47
+ if (summaries.length === 0) throw new Error("Box transform requires at least one valid row.");
48
+ return cloneAndFreeze({ summaries, outliers: [] });
49
+ }
50
+
21
51
  function validateOutputs(as, occupied) {
22
52
  if (!isPlainObject(as)) throw new TypeError("Box as must be a plain object.");
23
53
  const keys = Object.keys(as);
@@ -40,6 +70,18 @@ export function validateBoxTransform(value) {
40
70
  }
41
71
  field(value.category, "Box category field");
42
72
  field(value.field, "Box measure field");
73
+ if (value.method === "precomputed") {
74
+ const summary = normalizeBoxSummary(value.summary);
75
+ if (summary === undefined || value.type !== "boxSummary" || value.field !== summary.median) {
76
+ throw new Error("Precomputed box summaries require a summary mapping and its median measure field.");
77
+ }
78
+ if (value.whisker !== undefined || value.factor !== undefined) {
79
+ throw new Error("Precomputed box summaries do not compute whiskers or factors.");
80
+ }
81
+ validateOutputs(value.as, new Set([value.category, ...Object.values(summary)]));
82
+ return value;
83
+ }
84
+ if (value.summary !== undefined) throw new Error("Box summary fields require the precomputed method.");
43
85
  if (value.method !== "linear") throw new Error(`Unsupported box quantile method "${value.method}".`);
44
86
  const whisker = value.whisker ?? "tukey";
45
87
  if (!["tukey", "minmax"].includes(whisker)) {
@@ -56,6 +98,16 @@ export function validateBoxTransform(value) {
56
98
  }
57
99
 
58
100
  export function normalizeBoxTransform(options = {}) {
101
+ if (options.summary !== undefined) {
102
+ const summary = normalizeBoxSummary(options.summary);
103
+ const normalized = { type: options.type ?? "boxSummary", category: options.category,
104
+ field: options.field ?? summary.median, method: "precomputed", summary, as: options.as ?? BOX_FIELDS };
105
+ if (options.factor !== undefined || (options.whisker !== undefined && options.whisker !== "minmax")) {
106
+ throw new Error("Precomputed box summaries require minmax whiskers without a factor.");
107
+ }
108
+ validateBoxTransform(normalized);
109
+ return cloneAndFreeze(normalized);
110
+ }
59
111
  const {
60
112
  type = "boxSummary",
61
113
  category,
@@ -79,16 +131,11 @@ export function normalizeBoxTransform(options = {}) {
79
131
  return cloneAndFreeze(normalized);
80
132
  }
81
133
 
82
- function quantile(sorted, p) {
83
- const position = (sorted.length - 1) * p;
84
- const lower = Math.floor(position);
85
- const upper = Math.ceil(position);
86
- return interpolateNumber(sorted[lower], sorted[upper], position - lower);
87
- }
88
134
 
89
135
  export function deriveBoxData(rows, transform) {
90
136
  if (!Array.isArray(rows)) throw new TypeError("Box rows must be an array.");
91
137
  validateBoxTransform(transform);
138
+ if (transform.method === "precomputed") return derivePrecomputedBoxData(rows, transform);
92
139
  const { category, field: measure, factor, as } = transform;
93
140
  const whisker = transform.whisker ?? "tukey";
94
141
  const groups = [];
@@ -111,7 +158,7 @@ export function deriveBoxData(rows, transform) {
111
158
  for (const group of groups) {
112
159
  const sorted = group.rows.map(row => row[measure]).sort((a, b) => a - b);
113
160
  const [q1, median, q3] = [0.25, 0.5, 0.75].map(probability =>
114
- stableDecimal(quantile(sorted, probability))
161
+ stableDecimal(quantileSorted(sorted, probability))
115
162
  );
116
163
  const spread = q3 - q1;
117
164
  const lowerFence = whisker === "minmax"
@@ -1,3 +1,4 @@
1
+ import { requireStringValue as nonEmptyField } from "../core/validation.js";
1
2
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
3
  import { isNominalValue, readNominalField } from "./scales/fields.js";
3
4
  import { aggregateScalarValues } from "./aggregate.js";
@@ -7,12 +8,6 @@ export const CATEGORY_ORDER_CHANNELS = Object.freeze(["x", "y", "theta"]);
7
8
  const DIRECTIONS = ["ascending", "descending"];
8
9
  const AGGREGATES = ["sum", "mean", "min", "max"];
9
10
 
10
- function nonEmptyField(value, label) {
11
- if (typeof value !== "string" || value.length === 0) {
12
- throw new TypeError(`${label} must be a non-empty string.`);
13
- }
14
- return value;
15
- }
16
11
 
17
12
  const sameValue = Object.is;
18
13
 
@@ -1,3 +1,5 @@
1
+ import { rejectUnknownProperties as rejectUnknownKeys } from "../core/validation.js";
2
+ import { requireStringValue as requireField } from "../core/validation.js";
1
3
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
4
 
3
5
  const TRANSFORM_KEYS = Object.freeze([
@@ -6,17 +8,7 @@ const TRANSFORM_KEYS = Object.freeze([
6
8
  const SEQUENCE_KEYS = Object.freeze(["start", "end", "step"]);
7
9
  const MAX_OUTPUT_ROWS = 10_000;
8
10
 
9
- function rejectUnknownKeys(value, supported, label) {
10
- const unknown = Object.keys(value).find(key => !supported.includes(key));
11
- if (unknown !== undefined) throw new Error(`Unknown ${label} property "${unknown}".`);
12
- }
13
11
 
14
- function requireField(value, label) {
15
- if (typeof value !== "string" || value.length === 0) {
16
- throw new TypeError(`${label} must be a non-empty string.`);
17
- }
18
- return value;
19
- }
20
12
 
21
13
  function scalarType(value, label) {
22
14
  if (value === null) return "null";
@@ -258,20 +250,24 @@ export function deriveCompleteRows(rows, transform) {
258
250
  if (!allowed.has(identity)) {
259
251
  throw new Error(`Complete observed key is outside the requested domain at row ${entry.index}.`);
260
252
  }
261
- if (lookup.has(identity)) {
262
- throw new Error("Complete source contains a duplicate group and key combination.");
263
- }
264
- lookup.set(identity, entry);
253
+ if (!lookup.has(identity)) lookup.set(identity, []);
254
+ lookup.get(identity).push(entry);
255
+ }
256
+ const groupSize = group.values.length + domain.length - lookup.size;
257
+ if (output.length + groupSize > MAX_OUTPUT_ROWS) {
258
+ throw new RangeError(`Complete output cannot exceed ${MAX_OUTPUT_ROWS} rows.`);
265
259
  }
266
260
  for (const keyValue of domain) {
267
261
  const identity = scalarKey(keyValue, "Complete domain value");
268
- const entry = lookup.get(identity);
269
- if (entry === undefined) {
262
+ const entries = lookup.get(identity);
263
+ if (entries === undefined) {
270
264
  output.push(syntheticRow(fields, group, keyValue, transform));
271
265
  } else {
272
- const row = { ...entry.row };
273
- if (transform.members !== undefined) define(row, transform.members, [entry.index]);
274
- output.push(row);
266
+ for (const entry of entries) {
267
+ const row = { ...entry.row };
268
+ if (transform.members !== undefined) define(row, transform.members, [entry.index]);
269
+ output.push(row);
270
+ }
275
271
  }
276
272
  }
277
273
  }