ggaction 0.0.17 → 0.0.19

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 (315) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +6 -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 +3 -3
  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 +20 -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 +8 -7
  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/box.js +2 -1
  34. package/src/actions/data/derived.js +11 -1
  35. package/src/actions/data/edit.js +3 -1
  36. package/src/actions/data/index.js +4 -0
  37. package/src/actions/data/regression.js +25 -16
  38. package/src/actions/data/regressionStatistics.js +73 -0
  39. package/src/actions/data/shared.js +8 -4
  40. package/src/actions/data/stack.js +3 -1
  41. package/src/actions/distributions/revision.js +24 -18
  42. package/src/actions/encodings/appearance.js +37 -29
  43. package/src/actions/encodings/barWidth.js +36 -11
  44. package/src/actions/encodings/channels.js +0 -1
  45. package/src/actions/encodings/color/continuous.js +1 -1
  46. package/src/actions/encodings/color/index.js +1 -1
  47. package/src/actions/encodings/color/policy.js +10 -3
  48. package/src/actions/encodings/density/resolve.js +1 -6
  49. package/src/actions/encodings/position/policies/arc.js +12 -10
  50. package/src/actions/encodings/position/policies/area.js +3 -3
  51. package/src/actions/encodings/position/policies/bar.js +15 -6
  52. package/src/actions/encodings/position/policies/line.js +17 -6
  53. package/src/actions/encodings/position/resolve.js +3 -2
  54. package/src/actions/encodings/ranged.js +17 -0
  55. package/src/actions/encodings/remove.js +14 -8
  56. package/src/actions/encodings/seriesLayout.js +1 -0
  57. package/src/actions/encodings/shared.js +26 -24
  58. package/src/actions/encodings/text.js +6 -3
  59. package/src/actions/errorBands/create.js +2 -2
  60. package/src/actions/errorBands/edit.js +2 -1
  61. package/src/actions/facets/actions.js +176 -247
  62. package/src/actions/facets/guides.js +2 -1
  63. package/src/actions/gradientPlots/resolve.js +2 -17
  64. package/src/actions/guides/applicability.js +1 -1
  65. package/src/actions/guides/axes/axes.js +30 -11
  66. package/src/actions/guides/axes/axis.js +72 -10
  67. package/src/actions/guides/axes/components.js +13 -13
  68. package/src/actions/guides/axes/edit.js +36 -24
  69. package/src/actions/guides/axes/labels.js +155 -71
  70. package/src/actions/guides/axes/lines.js +27 -24
  71. package/src/actions/guides/axes/parallel/lifecycle.js +1 -1
  72. package/src/actions/guides/axes/parallel/policy.js +8 -7
  73. package/src/actions/guides/axes/remove.js +13 -8
  74. package/src/actions/guides/axes/tickGroups.js +11 -7
  75. package/src/actions/guides/axes/ticks.js +36 -24
  76. package/src/actions/guides/axes/titles.js +107 -69
  77. package/src/actions/guides/basic.js +2 -0
  78. package/src/actions/guides/facadeAxes.js +33 -24
  79. package/src/actions/guides/facadeLegend.js +12 -4
  80. package/src/actions/guides/grids/grid.js +8 -2
  81. package/src/actions/guides/grids/resolve.js +102 -58
  82. package/src/actions/guides/legends/blocks.js +46 -20
  83. package/src/actions/guides/legends/categorical/actions.js +95 -19
  84. package/src/actions/guides/legends/categorical/components.js +15 -18
  85. package/src/actions/guides/legends/categorical/index.js +8 -0
  86. package/src/actions/guides/legends/categorical/layout.js +45 -20
  87. package/src/actions/guides/legends/categorical/options.js +39 -22
  88. package/src/actions/guides/legends/categorical/recipes.js +7 -3
  89. package/src/actions/guides/legends/categorical/resolve.js +11 -4
  90. package/src/actions/guides/legends/categorical/symbols.js +38 -18
  91. package/src/actions/guides/legends/categorical/validation.js +1 -6
  92. package/src/actions/guides/legends/colorSize.js +110 -0
  93. package/src/actions/guides/legends/combine.js +37 -0
  94. package/src/actions/guides/legends/continuous/colorSymbols.js +1 -0
  95. package/src/actions/guides/legends/continuous/common.js +1 -447
  96. package/src/actions/guides/legends/continuous/gradient.js +73 -46
  97. package/src/actions/guides/legends/continuous/interval.js +4 -4
  98. package/src/actions/guides/legends/continuous/opacity.js +33 -21
  99. package/src/actions/guides/legends/creation.js +2 -0
  100. package/src/actions/guides/legends/edit.js +1 -604
  101. package/src/actions/guides/legends/focused.js +8 -7
  102. package/src/actions/guides/legends/group.js +1 -0
  103. package/src/actions/guides/legends/lane.js +1 -516
  104. package/src/actions/guides/legends/lifecycle.js +2 -1
  105. package/src/actions/guides/legends/remove.js +9 -3
  106. package/src/actions/guides/legends/sampling.js +1 -1
  107. package/src/actions/guides/legends/size.js +65 -47
  108. package/src/actions/guides/legends/strokeWidth.js +13 -29
  109. package/src/actions/guides/legends/target.js +2 -79
  110. package/src/actions/guides/legends/transition.js +44 -11
  111. package/src/actions/guides/polar/axes/labels.js +4 -2
  112. package/src/actions/guides/polar/axes/titles.js +7 -5
  113. package/src/actions/guides/polar/resolve.js +6 -14
  114. package/src/actions/guides/tickValues.js +8 -5
  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 +10 -4
  120. package/src/actions/marks/bar/edit.js +29 -14
  121. package/src/actions/marks/bar/materialize.js +15 -7
  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/edit.js +1 -1
  125. package/src/actions/marks/point/materialize.js +5 -1
  126. package/src/actions/marks/rect/actions.js +3 -3
  127. package/src/actions/marks/remove.js +12 -10
  128. package/src/actions/marks/rule/actions.js +7 -4
  129. package/src/actions/marks/text/actions.js +17 -5
  130. package/src/actions/marks/text/layout.js +3 -0
  131. package/src/actions/primitives/createGraphics.js +3 -2
  132. package/src/actions/primitives/editGraphics.js +15 -11
  133. package/src/actions/primitives/graphicProperties.js +1 -6
  134. package/src/actions/primitives/semanticAction.js +27 -1
  135. package/src/actions/primitives/semanticValidation/guide.js +11 -1
  136. package/src/actions/primitives/semanticValidation/layer.js +8 -0
  137. package/src/actions/regression/create.js +1 -1
  138. package/src/actions/regression/edit.js +18 -6
  139. package/src/actions/regression/resolve.js +2 -6
  140. package/src/actions/scales/channels.js +9 -4
  141. package/src/actions/scales/consumers/families.js +16 -5
  142. package/src/actions/scales/create.js +10 -5
  143. package/src/actions/scales/definitions.js +1 -425
  144. package/src/actions/scales/edit.js +3 -3
  145. package/src/actions/scales/editPolicy.js +12 -3
  146. package/src/actions/scales/materialize.js +1 -1
  147. package/src/actions/scales/patch.js +1 -1
  148. package/src/actions/scales/preview.js +21 -1
  149. package/src/actions/scales/quantitativeColor.js +8 -3
  150. package/src/actions/selection/actions.js +1 -13
  151. package/src/actions/theme/reconcile.js +126 -45
  152. package/src/actions/theme/state.js +12 -0
  153. package/src/actions/titles/actions.js +5 -18
  154. package/src/actions/titles/resolve.js +41 -19
  155. package/src/actions/violinPlots/edit.js +3 -20
  156. package/src/core/compositionState.js +6 -3
  157. package/src/core/font.js +9 -2
  158. package/src/core/graphicProperties.js +1 -0
  159. package/src/core/optionRequirements.js +1 -0
  160. package/src/core/programState.js +4 -0
  161. package/src/core/resourceReferences.js +5 -2
  162. package/src/core/textMetricProfile.js +3 -2
  163. package/src/core/textMetrics.js +41 -4
  164. package/src/core/validation.js +35 -0
  165. package/src/core/vocabulary.js +7 -0
  166. package/src/grammar/aggregate.js +4 -18
  167. package/src/grammar/arcs.js +1 -386
  168. package/src/grammar/areaPaint.js +10 -0
  169. package/src/grammar/areaSeries.js +18 -10
  170. package/src/grammar/axisTextPlacement.js +1 -0
  171. package/src/grammar/bars/geometry.js +30 -3
  172. package/src/grammar/bars/policy.js +12 -1
  173. package/src/grammar/bin.js +2 -12
  174. package/src/grammar/bin2d.js +1 -6
  175. package/src/grammar/boxPlot.js +56 -9
  176. package/src/grammar/categoryOrder.js +1 -6
  177. package/src/grammar/colorSizeLegend.js +39 -0
  178. package/src/grammar/complete.js +15 -19
  179. package/src/grammar/computed.js +115 -26
  180. package/src/grammar/curveCommands.js +29 -16
  181. package/src/grammar/datasetSchema.js +13 -9
  182. package/src/grammar/displayLabels.js +36 -0
  183. package/src/grammar/ecdf.js +2 -9
  184. package/src/grammar/facets/dependencies.js +6 -2
  185. package/src/grammar/facets/guides.js +58 -30
  186. package/src/grammar/facets/index.js +2 -5
  187. package/src/grammar/facets/legendIntent.js +65 -0
  188. package/src/grammar/fold.js +2 -12
  189. package/src/grammar/gradientProfile.js +1 -6
  190. package/src/grammar/guideDash.js +1 -0
  191. package/src/grammar/impute.js +21 -48
  192. package/src/grammar/interval.js +2 -9
  193. package/src/grammar/legendBlocks.js +1 -0
  194. package/src/grammar/legendBorder.js +9 -0
  195. package/src/grammar/legendClip.js +19 -0
  196. package/src/grammar/legendCombinations.js +45 -0
  197. package/src/grammar/legendGradient.js +27 -0
  198. package/src/grammar/legendGroups.js +1 -0
  199. package/src/grammar/legendOpacity.js +6 -0
  200. package/src/grammar/legendTitleStyle.js +22 -0
  201. package/src/grammar/lineSeries.js +32 -17
  202. package/src/grammar/markLabels.js +5 -2
  203. package/src/grammar/normalize.js +4 -54
  204. package/src/grammar/numeric.js +1 -290
  205. package/src/grammar/parallelCoordinates.js +1 -6
  206. package/src/grammar/parseTime.js +41 -0
  207. package/src/grammar/pathCommands.js +11 -4
  208. package/src/grammar/pointShapes.js +16 -2
  209. package/src/grammar/polar.js +1 -184
  210. package/src/grammar/positionCompatibility.js +3 -3
  211. package/src/grammar/regression/derive.js +16 -2
  212. package/src/grammar/regression/models.js +142 -54
  213. package/src/grammar/regression/parameters.js +23 -4
  214. package/src/grammar/regression/statistics.js +64 -0
  215. package/src/grammar/roundedRect.js +42 -54
  216. package/src/grammar/scales/appearance.js +1 -206
  217. package/src/grammar/scales/color.js +1 -382
  218. package/src/grammar/scales/colorConsumers.js +5 -2
  219. package/src/grammar/scales/definition.js +1 -197
  220. package/src/grammar/scales/discretized.js +1 -195
  221. package/src/grammar/scales/size.js +1 -456
  222. package/src/grammar/scales/temporal.js +2 -13
  223. package/src/grammar/scales/transformed.js +1 -383
  224. package/src/grammar/scales/types.js +1 -1
  225. package/src/grammar/scales/validation.js +1 -66
  226. package/src/grammar/schemas/concreteGraphic.js +14 -0
  227. package/src/grammar/schemas/graphic.js +6 -0
  228. package/src/grammar/schemas/graphicBounds.js +31 -7
  229. package/src/grammar/schemas/semanticPath.js +19 -3
  230. package/src/grammar/stack.js +2 -10
  231. package/src/grammar/statisticalReference.js +1 -6
  232. package/src/grammar/summary.js +4 -27
  233. package/src/grammar/text.js +19 -0
  234. package/src/grammar/timeUnit.js +22 -11
  235. package/src/grammar/transformKeys.js +47 -0
  236. package/src/grammar/transformTopology.js +1 -0
  237. package/src/grammar/transforms.js +15 -2
  238. package/src/grammar/valueFormat.js +17 -5
  239. package/src/grammar/weightedStatistics.js +1 -6
  240. package/src/grammar/window.js +4 -58
  241. package/src/inspectionDescriptors.js +1 -1
  242. package/src/layout/canvas.js +1 -110
  243. package/src/layout/facets.js +1 -244
  244. package/src/layout/guideCollisions.js +9 -1
  245. package/src/layout/labels.js +1 -0
  246. package/src/layout/legend.js +13 -0
  247. package/src/layout/legendGroup.js +1 -0
  248. package/src/layout/legendItems.js +59 -15
  249. package/src/layout/legendLane.js +1 -441
  250. package/src/layout/legendText.js +1 -0
  251. package/src/layout/title.js +35 -13
  252. package/src/materialization/bars/aggregate.js +11 -5
  253. package/src/materialization/bars/histogram.js +4 -1
  254. package/src/materialization/bars/ranged.js +19 -6
  255. package/src/materialization/bars/resolve.js +22 -1
  256. package/src/materialization/compositionSnapshot.js +2 -7
  257. package/src/materialization/coordinateBounds.js +21 -0
  258. package/src/materialization/dependencies.js +4 -44
  259. package/src/materialization/facetGuides/edit.js +30 -0
  260. package/src/materialization/facetGuides/layout.js +1 -0
  261. package/src/materialization/facetGuides/owner.js +125 -0
  262. package/src/materialization/facetGuides/ownerLayout.js +71 -0
  263. package/src/materialization/facetGuides/placement.js +71 -14
  264. package/src/materialization/facetGuides/preparation.js +22 -7
  265. package/src/materialization/facetHeaders.js +1 -347
  266. package/src/materialization/facets.js +1 -185
  267. package/src/materialization/graphicHierarchy.js +5 -1
  268. package/src/materialization/graphicTranslation.js +73 -0
  269. package/src/materialization/guides/layout.js +1 -57
  270. package/src/materialization/guides/resources.js +1 -123
  271. package/src/materialization/layout.js +53 -0
  272. package/src/materialization/legendBorder.js +28 -0
  273. package/src/materialization/legendClip.js +76 -0
  274. package/src/materialization/legendGroups.js +1 -0
  275. package/src/materialization/legendText.js +1 -0
  276. package/src/materialization/legends.js +2 -2
  277. package/src/materialization/marks/capabilities.js +5 -6
  278. package/src/materialization/rect.js +1 -1
  279. package/src/materialization/scaleGuideDependencies.js +14 -15
  280. package/src/materialization/scales/map.js +6 -0
  281. package/src/materialization/scales/policies/arc.js +8 -7
  282. package/src/materialization/scales/policies/bar.js +19 -7
  283. package/src/materialization/scales/policies/series.js +2 -1
  284. package/src/materialization/scales/resolve.js +7 -3
  285. package/src/materialization/selection/items/arc.js +1 -1
  286. package/src/materialization/selection/items/bar.js +7 -6
  287. package/src/materialization/selection/items/path.js +7 -6
  288. package/src/materialization/selection/items/rect.js +2 -2
  289. package/src/materialization/text.js +13 -4
  290. package/src/materialization/textLines.js +15 -0
  291. package/src/materialization/typography.js +10 -2
  292. package/src/persistence/codec.js +37 -6
  293. package/src/persistence/validation.js +220 -0
  294. package/src/persistence.js +19 -6
  295. package/src/renderers/canvas/circle.js +6 -3
  296. package/src/renderers/canvas/fill.js +7 -1
  297. package/src/renderers/canvas/index.js +22 -11
  298. package/src/renderers/canvas/isolated.js +166 -0
  299. package/src/renderers/canvas/limits.js +3 -0
  300. package/src/renderers/canvas/line.js +1 -0
  301. package/src/renderers/canvas/path.js +1 -1
  302. package/src/renderers/canvas/rect.js +11 -8
  303. package/src/renderers/canvas/stroke.js +2 -1
  304. package/src/renderers/canvas/text.js +5 -3
  305. package/src/renderers/pdf.js +2 -2
  306. package/src/renderers/png.js +2 -2
  307. package/src/renderers/svg.js +14 -4
  308. package/src/selectors/axes.js +1 -0
  309. package/src/selectors/graphicCapacity.js +38 -0
  310. package/src/selectors/layers.js +19 -0
  311. package/src/theme/defaults.js +8 -2
  312. package/src/version.js +1 -1
  313. package/types/basic.d.ts +3 -0
  314. package/types/index.d.ts +12 -0
  315. package/types/program.d.ts +536 -129
@@ -1,386 +1 @@
1
- import { cloneAndFreeze } from "../core/immutable.js";
2
- import {
3
- mapContinuousScaleValues,
4
- mapOrdinalPositionValues,
5
- readNominalField,
6
- readQuantitativeField
7
- } from "./scales/index.js";
8
- import {
9
- interpolateNumber,
10
- normalizedFiniteSum,
11
- requireFiniteResult,
12
- stableFiniteSum
13
- } from "./numeric.js";
14
-
15
- function requireArcLayer(layer) {
16
- if (layer?.mark?.type !== "arc") {
17
- throw new Error("Arc derivation requires a semantic arc mark.");
18
- }
19
- const theta = layer.encoding?.theta;
20
- if (
21
- theta === undefined ||
22
- !["quantitative", "nominal", "ordinal"].includes(theta.fieldType)
23
- ) {
24
- throw new Error(
25
- `Arc mark "${layer.id}" requires quantitative or categorical theta.`
26
- );
27
- }
28
- return theta;
29
- }
30
-
31
- function requireContinuousThetaScale(scale, label) {
32
- if (
33
- scale?.type !== "linear" ||
34
- !Array.isArray(scale.range) ||
35
- scale.range.length !== 2 ||
36
- !scale.range.every(Number.isFinite) ||
37
- scale.range[0] === scale.range[1]
38
- ) {
39
- throw new Error(`${label} requires a resolved linear scale.`);
40
- }
41
- return scale;
42
- }
43
-
44
- function requireBandScale(scale, label) {
45
- if (
46
- scale?.type !== "band" ||
47
- !Array.isArray(scale.domain) ||
48
- !Array.isArray(scale.range) ||
49
- scale.range.length !== 2 ||
50
- !scale.range.every(Number.isFinite) ||
51
- scale.range[0] === scale.range[1] ||
52
- !Number.isFinite(scale.bandwidth) ||
53
- scale.bandwidth <= 0
54
- ) {
55
- throw new Error(`${label} requires a resolved band scale.`);
56
- }
57
- return scale;
58
- }
59
-
60
- function colorValues(rows, encoding) {
61
- return encoding === undefined
62
- ? rows.map(() => undefined)
63
- : readNominalField(rows, encoding.field);
64
- }
65
-
66
- function readNonNegativeArcThetaValues(rows, field, layerId, role) {
67
- if (!Array.isArray(rows)) throw new TypeError(`Arc theta ${role}s require rows.`);
68
- if (typeof field !== "string" || field.length === 0) {
69
- throw new TypeError(`Arc theta ${role} must be a non-empty string.`);
70
- }
71
- const values = rows.map((row, index) => {
72
- const value = row?.[field];
73
- if (!Number.isFinite(value) || value < 0) {
74
- throw new TypeError(
75
- `Arc theta ${role} field "${field}" must contain non-negative finite numbers at row ${index}.`
76
- );
77
- }
78
- return value;
79
- });
80
- if (values.every(value => value === 0)) {
81
- throw new Error(`Arc mark "${layerId}" theta ${role}s must have a positive total.`);
82
- }
83
- return cloneAndFreeze(values);
84
- }
85
-
86
- export function readArcThetaWeights(rows, field, layerId = "arc") {
87
- return readNonNegativeArcThetaValues(rows, field, layerId, "weight");
88
- }
89
-
90
- export function readArcThetaValues(rows, field, layerId = "arc") {
91
- return readNonNegativeArcThetaValues(rows, field, layerId, "value");
92
- }
93
-
94
- function resolveProportionalRanges(values, range, labelAt) {
95
- const total = values.reduce((sum, value) => sum + value, 0);
96
- const normalizedTotal = Number.isFinite(total)
97
- ? undefined
98
- : normalizedFiniteSum(values, "Arc theta values");
99
- if (total === 0 || normalizedTotal?.total <= 0) {
100
- throw new Error("Arc theta values must have a positive total.");
101
- }
102
- const span = range[1] - range[0];
103
- let cursor = range[0];
104
- let cumulative = 0;
105
- let cumulativeCorrection = 0;
106
- return values.map((value, index) => {
107
- const normalizedValue = normalizedTotal === undefined
108
- ? value
109
- : value / normalizedTotal.scale;
110
- const next = cumulative + normalizedValue;
111
- if (normalizedTotal !== undefined) {
112
- cumulativeCorrection += Math.abs(cumulative) >= Math.abs(normalizedValue)
113
- ? cumulative - next + normalizedValue
114
- : normalizedValue - next + cumulative;
115
- }
116
- cumulative = next;
117
- const directEnd = cursor + value / total * span;
118
- const endTheta = index === values.length - 1
119
- ? range[1]
120
- : normalizedTotal === undefined && Number.isFinite(directEnd)
121
- ? directEnd
122
- : interpolateNumber(
123
- range[0],
124
- range[1],
125
- (cumulative + cumulativeCorrection) /
126
- (normalizedTotal?.total ?? total)
127
- );
128
- if (!Number.isFinite(endTheta) || endTheta === cursor) {
129
- throw new RangeError(
130
- `Arc theta ${labelAt(index)} cannot resolve a distinct finite sector.`
131
- );
132
- }
133
- const result = { startTheta: cursor, endTheta };
134
- cursor = endTheta;
135
- return result;
136
- });
137
- }
138
-
139
- function resolveProportionalRadii(layer, frame, innerRadiusRatio) {
140
- if (!Number.isFinite(frame?.availableRadius) || frame.availableRadius < 0) {
141
- throw new RangeError(`Arc mark "${layer.id}" requires a finite available radius.`);
142
- }
143
- return {
144
- innerRadius: requireFiniteResult(
145
- frame.availableRadius * innerRadiusRatio,
146
- `Arc mark "${layer.id}" inner radius`
147
- ),
148
- outerRadius: frame.availableRadius
149
- };
150
- }
151
-
152
- function proportionalSectors(rows, layer, thetaScale, frame, innerRadiusRatio) {
153
- const theta = layer.encoding.theta;
154
- if (!["count", "sum"].includes(theta.aggregate)) {
155
- throw new Error(`Arc mark "${layer.id}" requires count, sum, or radial layout.`);
156
- }
157
- const values = readNominalField(rows, theta.field);
158
- const colors = colorValues(rows, layer.encoding?.color);
159
- const weights = theta.aggregate === "count"
160
- ? rows.map(() => 1)
161
- : readArcThetaWeights(rows, theta.weight, layer.id);
162
- const groups = new Map(thetaScale.domain.map(value => [value, []]));
163
- for (let index = 0; index < rows.length; index += 1) {
164
- const group = groups.get(values[index]);
165
- if (group === undefined) {
166
- throw new Error(`Arc theta value "${values[index]}" is outside the scale domain.`);
167
- }
168
- group.push({
169
- color: colors[index],
170
- sourceIndex: index,
171
- weight: weights[index]
172
- });
173
- }
174
- const radii = resolveProportionalRadii(layer, frame, innerRadiusRatio);
175
- const sectors = [];
176
- const positiveValues = thetaScale.domain.filter(value =>
177
- groups.get(value).some(item => item.weight > 0)
178
- );
179
- const aggregateValues = positiveValues.map(value => stableFiniteSum(
180
- groups.get(value).map(item => item.weight),
181
- `Arc theta group "${String(value)}" aggregate`
182
- ));
183
- const ranges = resolveProportionalRanges(
184
- aggregateValues,
185
- thetaScale.range,
186
- index => `group "${String(positiveValues[index])}"`
187
- );
188
- for (const [positiveIndex, value] of positiveValues.entries()) {
189
- const group = groups.get(value);
190
- const aggregateValue = aggregateValues[positiveIndex];
191
- const { startTheta, endTheta } = ranges[positiveIndex];
192
- const distinctColors = [...new Set(group.map(item => item.color))];
193
- if (distinctColors.length > 1) {
194
- throw new Error(
195
- `Aggregated arc theta group "${value}" must resolve to one color value.`
196
- );
197
- }
198
- sectors.push({
199
- key: value,
200
- theta: value,
201
- count: group.length,
202
- aggregateValue,
203
- color: distinctColors[0],
204
- startTheta,
205
- endTheta,
206
- ...radii,
207
- sourceIndices: group.map(item => item.sourceIndex)
208
- });
209
- }
210
- return sectors;
211
- }
212
-
213
- function quantitativeSectors(rows, layer, thetaScale, frame, innerRadiusRatio) {
214
- const theta = layer.encoding.theta;
215
- if (layer.encoding?.radius !== undefined) {
216
- throw new Error(
217
- `Arc mark "${layer.id}" cannot combine quantitative theta with radius.`
218
- );
219
- }
220
- const values = readArcThetaValues(rows, theta.field, layer.id);
221
- const colors = colorValues(rows, layer.encoding?.color);
222
- const positiveIndices = values
223
- .map((value, index) => value > 0 ? index : undefined)
224
- .filter(index => index !== undefined);
225
- const positiveValues = positiveIndices.map(index => values[index]);
226
- const ranges = resolveProportionalRanges(
227
- positiveValues,
228
- thetaScale.range,
229
- index => `row ${positiveIndices[index]}`
230
- );
231
- const radii = resolveProportionalRadii(layer, frame, innerRadiusRatio);
232
- return positiveIndices.map((sourceIndex, index) => ({
233
- key: `${String(values[sourceIndex])}:${sourceIndex}`,
234
- theta: values[sourceIndex],
235
- count: 1,
236
- aggregateValue: values[sourceIndex],
237
- color: colors[sourceIndex],
238
- ...ranges[index],
239
- ...radii,
240
- sourceIndices: [sourceIndex]
241
- }));
242
- }
243
-
244
- export function deriveMeasuredArcValues(rows, layer) {
245
- const theta = requireArcLayer(layer);
246
- const radius = layer.encoding?.radius;
247
- if (!["nominal", "ordinal"].includes(theta.fieldType) || theta.aggregate !== undefined ||
248
- !["count", "sum"].includes(radius?.aggregate) || radius.fieldType !== "quantitative") {
249
- throw new Error("Measured Arc requires equal-angle categorical theta and count or sum radius.");
250
- }
251
- if (radius.aggregate === "count" && Object.hasOwn(radius, "field")) {
252
- throw new Error("Measured count radius does not accept a field.");
253
- }
254
- const categories = readNominalField(rows, theta.field);
255
- const measures = radius.aggregate === "count" ? rows.map(() => 1)
256
- : readQuantitativeField(rows, radius.field);
257
- if (measures.some(value => value < 0)) {
258
- throw new RangeError("Measured Arc radius inputs must be non-negative.");
259
- }
260
- const colors = colorValues(rows, layer.encoding?.color);
261
- const groups = new Map();
262
- for (const [index, category] of categories.entries()) {
263
- const group = groups.get(category) ?? { key: category, sourceIndices: [], values: [], colors: [] };
264
- group.sourceIndices.push(index);
265
- group.values.push(measures[index]);
266
- group.colors.push(colors[index]);
267
- groups.set(category, group);
268
- }
269
- const result = [...groups.values()].map(group => {
270
- const distinctColors = [...new Set(group.colors)];
271
- if (distinctColors.length > 1) {
272
- throw new Error(`Measured Arc category "${group.key}" must resolve to one color value.`);
273
- }
274
- return { key: group.key, theta: group.key, count: group.sourceIndices.length,
275
- radius: stableFiniteSum(group.values, "Measured Arc radius aggregate"),
276
- color: distinctColors[0], sourceIndices: group.sourceIndices };
277
- });
278
- if (!result.some(group => group.radius > 0)) {
279
- throw new Error("Measured Arc radius requires at least one positive category aggregate.");
280
- }
281
- return cloneAndFreeze(result);
282
- }
283
-
284
- function measuredRadialSectors(rows, layer, thetaScale, radiusScale) {
285
- const items = deriveMeasuredArcValues(rows, layer);
286
- const centers = mapOrdinalPositionValues(items.map(item => item.theta), thetaScale);
287
- const radii = mapContinuousScaleValues(items.map(item => item.radius), radiusScale);
288
- const halfBand = (Math.sign(thetaScale.step) || 1) * thetaScale.bandwidth / 2;
289
- const byCategory = new Map(items.map((item, index) => [item.theta, {
290
- ...item, startTheta: centers[index] - halfBand, endTheta: centers[index] + halfBand,
291
- innerRadius: radiusScale.range[0], outerRadius: radii[index]
292
- }]));
293
- return thetaScale.domain.flatMap(category => {
294
- const item = byCategory.get(category);
295
- return item === undefined || item.radius === 0 ? [] : [item];
296
- });
297
- }
298
-
299
- function radialSectors(rows, layer, thetaScale, radiusScale) {
300
- const theta = layer.encoding.theta;
301
- const radius = layer.encoding?.radius;
302
- if (radius?.fieldType !== "quantitative") {
303
- throw new Error(`Arc mark "${layer.id}" requires quantitative radius.`);
304
- }
305
- const thetaValues = readNominalField(rows, theta.field);
306
- const radiusValues = readQuantitativeField(rows, radius.field);
307
- const colors = colorValues(rows, layer.encoding?.color);
308
- const centers = mapOrdinalPositionValues(thetaValues, thetaScale);
309
- const outerRadii = mapContinuousScaleValues(radiusValues, radiusScale);
310
- const innerRadius = Math.min(...radiusScale.range);
311
- const direction = Math.sign(thetaScale.step) || 1;
312
- const halfBand = direction * thetaScale.bandwidth / 2;
313
- const grouped = new Map(thetaScale.domain.map(value => [value, []]));
314
- for (let index = 0; index < rows.length; index += 1) {
315
- if (outerRadii[index] <= innerRadius) continue;
316
- const group = grouped.get(thetaValues[index]);
317
- if (group === undefined) {
318
- throw new Error(
319
- `Arc theta value "${thetaValues[index]}" is outside the scale domain.`
320
- );
321
- }
322
- group.push({
323
- key: `${String(thetaValues[index])}:${index}`,
324
- theta: thetaValues[index],
325
- radius: radiusValues[index],
326
- color: colors[index],
327
- startTheta: centers[index] - halfBand,
328
- endTheta: centers[index] + halfBand,
329
- innerRadius,
330
- outerRadius: outerRadii[index],
331
- sourceIndices: [index]
332
- });
333
- }
334
- const sectors = [];
335
- for (const value of thetaScale.domain) {
336
- const group = grouped.get(value);
337
- group.sort((left, right) =>
338
- right.outerRadius - left.outerRadius ||
339
- left.sourceIndices[0] - right.sourceIndices[0]
340
- );
341
- sectors.push(...group);
342
- }
343
- return sectors;
344
- }
345
-
346
- export function deriveArcSectors(rows, layer, {
347
- thetaScale,
348
- radiusScale,
349
- frame,
350
- innerRadiusRatio = 0
351
- } = {}) {
352
- if (!Array.isArray(rows)) throw new TypeError("Arc derivation requires rows.");
353
- const theta = requireArcLayer(layer);
354
- if (
355
- !Number.isFinite(innerRadiusRatio) ||
356
- innerRadiusRatio < 0 ||
357
- innerRadiusRatio >= 1
358
- ) {
359
- throw new RangeError("Arc innerRadius must be from 0 (inclusive) to 1 (exclusive).");
360
- }
361
- const sectors = radiusScale?.radialMapping !== undefined
362
- ? measuredRadialSectors(rows, layer, requireBandScale(thetaScale, `Arc mark "${layer.id}" theta`), radiusScale)
363
- : theta.fieldType === "quantitative"
364
- ? quantitativeSectors(
365
- rows,
366
- layer,
367
- requireContinuousThetaScale(thetaScale, `Arc mark "${layer.id}" theta`),
368
- frame,
369
- innerRadiusRatio
370
- )
371
- : ["count", "sum"].includes(theta.aggregate)
372
- ? proportionalSectors(
373
- rows,
374
- layer,
375
- requireBandScale(thetaScale, `Arc mark "${layer.id}" theta`),
376
- frame,
377
- innerRadiusRatio
378
- )
379
- : radialSectors(
380
- rows,
381
- layer,
382
- requireBandScale(thetaScale, `Arc mark "${layer.id}" theta`),
383
- radiusScale
384
- );
385
- return cloneAndFreeze({ sectors });
386
- }
1
+ import{validateOptionObject as P,validateNonNegativeFinite as j}from"../core/validation.js";import{cloneAndFreeze as R,isPlainObject as B}from"../core/immutable.js";import{mapContinuousScaleValues as y,mapOrdinalPositionValues as M,readNominalField as b,readQuantitativeField as I}from"./scales/index.js";import{interpolateNumber as S,normalizedFiniteSum as _,stableFiniteSum as q}from"./numeric.js";function k(e){if(e?.mark?.type!=="arc")throw new Error("Arc derivation requires a semantic arc mark.");const t=e.encoding?.theta;if(t===void 0||!["quantitative","nominal","ordinal"].includes(t.fieldType))throw new Error(`Arc mark "${e.id}" requires quantitative or categorical theta.`);return t}function G(e,t){if(e?.type!=="linear"||!Array.isArray(e.range)||e.range.length!==2||!e.range.every(Number.isFinite)||e.range[0]===e.range[1])throw new Error(`${t} requires a resolved linear scale.`);return e}function L(e,t){if(e?.type!=="band"||!Array.isArray(e.domain)||!Array.isArray(e.range)||e.range.length!==2||!e.range.every(Number.isFinite)||e.range[0]===e.range[1]||!Number.isFinite(e.bandwidth)||e.bandwidth<=0)throw new Error(`${t} requires a resolved band scale.`);return e}function $(e,t){return t===void 0?e.map(()=>{}):b(e,t.field)}function F(e,t,r,n){if(!Array.isArray(e))throw new TypeError(`Arc theta ${n}s require rows.`);if(typeof t!="string"||t.length===0)throw new TypeError(`Arc theta ${n} must be a non-empty string.`);const s=e.map((u,a)=>{const i=u?.[t];if(!Number.isFinite(i)||i<0)throw new TypeError(`Arc theta ${n} field "${t}" must contain non-negative finite numbers at row ${a}.`);return i});if(s.every(u=>u===0))throw new Error(`Arc mark "${r}" theta ${n}s must have a positive total.`);return R(s)}function N(e,t,r="arc"){return F(e,t,r,"weight")}function V(e,t,r="arc"){return F(e,t,r,"value")}function Q(e,t,r){const n=e.reduce((c,d)=>c+d,0),s=Number.isFinite(n)?void 0:_(e,"Arc theta values");if(n===0||s?.total<=0)throw new Error("Arc theta values must have a positive total.");const u=t[1]-t[0];let a=t[0],i=0,o=0;return e.map((c,d)=>{const h=s===void 0?c:c/s.scale,v=i+h;s!==void 0&&(o+=Math.abs(i)>=Math.abs(h)?i-v+h:h-v+i),i=v;const p=a+c/n*u,l=d===e.length-1?t[1]:s===void 0&&Number.isFinite(p)?p:S(t[0],t[1],(i+o)/(s?.total??n));if(!Number.isFinite(l)||l===a)throw new RangeError(`Arc theta ${r(d)} cannot resolve a distinct finite sector.`);const m={startTheta:a,endTheta:l};return a=l,m})}function W(e,t,r){if(!Number.isFinite(t?.availableRadius)||t.availableRadius<0)throw new RangeError(`Arc mark "${e.id}" requires a finite available radius.`);return{innerRadius:U(r,t.availableRadius),outerRadius:t.availableRadius}}function D(e,t,r,n,s){const u=t.encoding.theta,a=u.fieldType==="quantitative";if(!a&&!["count","sum"].includes(u.aggregate))throw new Error(`Arc mark "${t.id}" requires count, sum, or radial layout.`);const i=a?V(e,u.field,t.id):u.aggregate==="count"?e.map(()=>1):N(e,u.weight,t.id),o=a?e.map((g,w)=>w):b(e,u.field),c=$(e,t.encoding?.color),d=new Map(z(o,i).map(g=>[g.key,g])),h=a?o:r.domain,v=new Set(h);for(const g of d.keys())if(!v.has(g))throw new Error(`Arc theta value "${g}" is outside the scale domain.`);const p=h.filter(g=>d.get(g)?.values.some(w=>w>0)),l=g=>a?`row ${p[g]}`:`group "${String(p[g])}"`,m=p.map((g,w)=>a?i[g]:q(d.get(g).values,`Arc theta ${l(w)} aggregate`)),f=Q(m,r.range,l),A=W(t,n,s);return p.map((g,w)=>{const E=d.get(g),T=[...new Set(E.sourceIndices.map(O=>c[O]))];if(T.length>1)throw new Error(`Aggregated arc theta group "${g}" must resolve to one color value.`);return{key:a?`${String(i[g])}:${g}`:g,theta:a?i[g]:g,count:E.sourceIndices.length,aggregateValue:m[w],color:T[0],...f[w],...A,sourceIndices:E.sourceIndices}})}function x(e,t,r){const n=t.aggregate==="count"?e.map(()=>1):I(e,t.field);return z(r,n)}function z(e,t){const r=new Map;for(const[n,s]of e.entries()){const u=r.get(s)??{key:s,sourceIndices:[],values:[]};u.sourceIndices.push(n),u.values.push(t[n]),r.set(s,u)}return[...r.values()]}function H(e,t){const r=k(t),n=t.encoding?.radius;if(!["nominal","ordinal"].includes(r.fieldType)||r.aggregate!==void 0||!["count","sum"].includes(n?.aggregate)||n.fieldType!=="quantitative")throw new Error("Measured Arc requires equal-angle categorical theta and count or sum radius.");if(n.aggregate==="count"&&Object.hasOwn(n,"field"))throw new Error("Measured count radius does not accept a field.");const s=b(e,r.field),u=$(e,t.encoding?.color),a=x(e,n,s);if(a.some(o=>o.values.some(c=>c<0)))throw new RangeError("Measured Arc radius inputs must be non-negative.");const i=a.map(o=>{const c=[...new Set(o.sourceIndices.map(d=>u[d]))];if(c.length>1)throw new Error(`Measured Arc category "${o.key}" must resolve to one color value.`);return{key:o.key,theta:o.key,count:o.sourceIndices.length,radius:q(o.values,"Measured Arc radius aggregate"),color:c[0],sourceIndices:o.sourceIndices}});if(!i.some(o=>o.radius>0))throw new Error("Measured Arc radius requires at least one positive category aggregate.");return R(i)}function J(e,t,r,n){const s=H(e,t),u=M(s.map(c=>c.theta),r),a=y(s.map(c=>c.radius),n),i=(Math.sign(r.step)||1)*r.bandwidth/2,o=new Map(s.map((c,d)=>[c.theta,{...c,startTheta:u[d]-i,endTheta:u[d]+i,innerRadius:n.range[0],outerRadius:a[d]}]));return r.domain.flatMap(c=>{const d=o.get(c);return d===void 0||d.radius===0?[]:[d]})}function K(e,t,r,n){const s=t.encoding.theta,u=t.encoding?.radius;if(u?.fieldType!=="quantitative")throw new Error(`Arc mark "${t.id}" requires quantitative radius.`);const a=b(e,s.field),i=I(e,u.field),o=$(e,t.encoding?.color),c=M(a,r),d=y(i,n),h=Math.min(...n.range),p=(Math.sign(r.step)||1)*r.bandwidth/2,l=new Map(r.domain.map(f=>[f,[]]));for(let f=0;f<e.length;f+=1){if(d[f]<=h)continue;const A=l.get(a[f]);if(A===void 0)throw new Error(`Arc theta value "${a[f]}" is outside the scale domain.`);A.push({key:`${String(a[f])}:${f}`,theta:a[f],radius:i[f],color:o[f],startTheta:c[f]-p,endTheta:c[f]+p,innerRadius:h,outerRadius:d[f],sourceIndices:[f]})}const m=[];for(const f of r.domain){const A=l.get(f);A.sort((g,w)=>w.outerRadius-g.outerRadius||g.sourceIndices[0]-w.sourceIndices[0]),m.push(...A)}return m}function re(e,t,{thetaScale:r,radiusScale:n,frame:s,innerRadius:u=0}={}){if(!Array.isArray(e))throw new TypeError("Arc derivation requires rows.");const a=k(t);C(u);const i=n?.radialMapping!==void 0,o=a.fieldType==="quantitative",c=(o&&!i?G:L)(r,`Arc mark "${t.id}" theta`);let d=i?J(e,t,c,n):o||["count","sum"].includes(a.aggregate)?D(e,t,c,s,u):K(e,t,c,n);if(n!==void 0&&!i&&(o||a.aggregate!==void 0)){const h=X(e,t),v=y(h.map(l=>l.radius),n),p=new Map(h.map((l,m)=>[l.sourceIndices[0],{radius:l.radius,outerRadius:v[m]}]));d=d.map(l=>{const m=p.get(l.sourceIndices[0]);if(m===void 0||!Number.isFinite(m.outerRadius)||m.outerRadius<=l.innerRadius)throw new RangeError("Arc outer radius must exceed inner radius.");return{...l,...m}})}return R({sectors:d})}function C(e){if(B(e)){if(P(e,["unit","value"],"Arc innerRadius"),e.unit!=="px")throw new RangeError("Arc innerRadius unit must be px.");return R({unit:"px",value:j(e.value,"Arc innerRadius")})}if(!Number.isFinite(e)||e<0||e>=1)throw new RangeError("Arc innerRadius must be from 0 (inclusive) to 1 (exclusive).");return e}function U(e,t){const r=C(e);if(typeof r=="number")return r*t;if(r.value>=t)throw new RangeError("Arc pixel innerRadius must be smaller than its outer radius.");return r.value}function X(e,t){const r=t.encoding?.theta,n=t.encoding?.radius;if(r===void 0)return[];const s=r.fieldType==="quantitative";if(s?n.aggregate!==void 0:!["count","sum"].includes(r.aggregate))throw new Error("Grouped arc radius requires aggregated categorical theta.");const u=s?V(e,r.field,t.id):r.aggregate==="count"?e.map(()=>1):N(e,r.weight,t.id),a=s?e.map((i,o)=>o):b(e,r.field);return x(e,n,a).filter(i=>i.sourceIndices.some(o=>u[o]>0)).map(({sourceIndices:i,values:o})=>{if(n.aggregate===void 0&&new Set(o).size!==1)throw new Error("Arc radius requires one value per theta group or explicit aggregation.");return{sourceIndices:i,radius:n.aggregate===void 0?o[0]:q(o,"Arc radius aggregate")}})}export{re as deriveArcSectors,H as deriveMeasuredArcValues,X as deriveProportionalArcRadiusValues,C as normalizeArcInnerRadius,V as readArcThetaValues,N as readArcThetaWeights,U as resolveArcInnerRadius};
@@ -0,0 +1,10 @@
1
+ import { isLinearGradientPaint, validateFillPaint } from "./paint.js";
2
+
3
+ // Area fill shares the concrete paint grammar; outlines remain solid colors.
4
+ export function validateAreaPaintConfig(config) {
5
+ if (config.fill !== undefined) validateFillPaint(config.fill, "Area fill");
6
+ if (config.strokeFromFill === true && isLinearGradientPaint(config.fill)) {
7
+ throw new Error("Area stroke-from-fill requires a solid fill; specify a solid stroke for a gradient area.");
8
+ }
9
+ return config;
10
+ }
@@ -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.`
@@ -0,0 +1 @@
1
+ import{resolveTextLines as o}from"../core/textMetrics.js";function a(e,{title:i=!1}={}){if(Object.hasOwn(e,"align")&&!["left","center","right"].includes(e.align))throw new TypeError("Axis text align must be left, center, or right.");if(i){if(Object.hasOwn(e,"baseline")&&!["top","hanging","middle","alphabetic","ideographic","bottom"].includes(e.baseline))throw new TypeError("Axis title baseline must be a supported text baseline.");for(const t of["offset","alongOffset"])if(Object.hasOwn(e,t)&&!Number.isFinite(e[t]))throw new TypeError(`Axis title ${t} must be a finite number.`)}}function f(e,i){if(!/[\r\n]/u.test(e))return;const t=i.baseline??"middle",n=o(e,{fontSize:i.fontSize,align:i.align??"center",baseline:t,...t==="middle"?{blockAlign:"middle"}:{}});if(["bottom","ideographic"].includes(t)){const s=(n.length-1)*i.fontSize*1.2;return n.map(r=>({...r,y:r.y-s}))}return n.length>1?n:void 0}export{f as axisTitleTextLines,a as validateAxisTextPlacement};
@@ -1,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,
@@ -20,19 +22,7 @@ const TRANSFORM_KEYS = Object.freeze([
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 ?? {};
@@ -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) {