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
@@ -2,7 +2,7 @@ import { cloneAndFreeze } from "./immutable.js";
2
2
  import { validateUserId } from "./identifiers.js";
3
3
  import { validateOptionObject, validatePositiveFinite, validateNonNegativeFinite, validateNonEmptyString } from "./validation.js";
4
4
 
5
- import { textMetricKey } from "./font.js";
5
+ import { textMetricKey, validateFontStyle } from "./font.js";
6
6
 
7
7
  export function normalizeTextMetricProfile(profile) {
8
8
  validateOptionObject(profile, ["schemaVersion", "id", "measurements"], "Text metrics profile");
@@ -11,7 +11,7 @@ export function normalizeTextMetricProfile(profile) {
11
11
  if (!Array.isArray(profile.measurements)) throw new TypeError("Text metrics measurements must be an array.");
12
12
  const seen = new Set();
13
13
  for (const measurement of profile.measurements) {
14
- validateOptionObject(measurement, ["text", "fontFamily", "fontSize", "fontWeight", "width"], "Text measurement");
14
+ validateOptionObject(measurement, ["text", "fontFamily", "fontSize", "fontWeight", "fontStyle", "width"], "Text measurement");
15
15
  if (typeof measurement.text !== "string") throw new TypeError("Text measurement text must be a string.");
16
16
  validateNonEmptyString(measurement.fontFamily, "Text measurement fontFamily");
17
17
  validatePositiveFinite(measurement.fontSize, "Text measurement fontSize");
@@ -20,6 +20,7 @@ export function normalizeTextMetricProfile(profile) {
20
20
  measurement.fontWeight > 900 || measurement.fontWeight % 100 !== 0) {
21
21
  throw new RangeError("Text measurement fontWeight must be 100–900 in steps of 100.");
22
22
  }
23
+ if (measurement.fontStyle !== undefined) validateFontStyle(measurement.fontStyle, "Text measurement");
23
24
  const key = textMetricKey(measurement);
24
25
  if (seen.has(key)) throw new Error("Duplicate text measurement combination.");
25
26
  seen.add(key);
@@ -1,3 +1,4 @@
1
+ import { validateGeneratedItemLimit } from "./validation.js";
1
2
  import { DEFAULT_FONT_FAMILY, textMetricFontWeight, textMetricKey } from "./font.js";
2
3
 
3
4
  import { isOwned } from "./immutable.js";
@@ -13,7 +14,7 @@ function measuredWidth(profile, text, style) {
13
14
  if (isOwned(profile)) measurementIndexes.set(profile, index);
14
15
  }
15
16
  return index.get(textMetricKey({ text, fontFamily: style.fontFamily ?? DEFAULT_FONT_FAMILY,
16
- fontSize: style.fontSize, fontWeight: textMetricFontWeight(style.fontWeight) }));
17
+ fontSize: style.fontSize, fontWeight: textMetricFontWeight(style.fontWeight), fontStyle: style.fontStyle }));
17
18
  }
18
19
 
19
20
  function codePointWidth(codePoint) {
@@ -43,7 +44,7 @@ export function textBoundsFitCanvas(bounds, canvas) {
43
44
 
44
45
  export function measureTextWidth(
45
46
  text,
46
- { fontSize, fontFamily, fontWeight } = {},
47
+ { fontSize, fontFamily, fontWeight, fontStyle } = {},
47
48
  profile
48
49
  ) {
49
50
  if (typeof text !== "string") {
@@ -52,7 +53,7 @@ export function measureTextWidth(
52
53
  if (!Number.isFinite(fontSize) || fontSize <= 0) {
53
54
  throw new RangeError("Text measurement requires a positive fontSize.");
54
55
  }
55
- const measured = measuredWidth(profile, text, { fontSize, fontFamily, fontWeight });
56
+ const measured = measuredWidth(profile, text, { fontSize, fontFamily, fontWeight, fontStyle });
56
57
  if (measured !== undefined) return measured;
57
58
  let joined = false;
58
59
  let width = 0;
@@ -91,9 +92,11 @@ export function resolveTextBounds({
91
92
  x,
92
93
  y,
93
94
  text,
95
+ lines,
94
96
  fontSize,
95
97
  fontFamily,
96
98
  fontWeight,
99
+ fontStyle,
97
100
  textAlign = "left",
98
101
  textBaseline = "alphabetic",
99
102
  rotation = 0
@@ -101,7 +104,19 @@ export function resolveTextBounds({
101
104
  if (![x, y, rotation].every(Number.isFinite)) {
102
105
  throw new TypeError("Text bounds require finite x, y, and rotation values.");
103
106
  }
104
- const width = measureTextWidth(text, { fontSize, fontFamily, fontWeight }, profile);
107
+ if (lines !== undefined) {
108
+ const cosine = Math.cos(rotation), sine = Math.sin(rotation);
109
+ const bounds = lines.map(line => resolveTextBounds({
110
+ x: x + line.x * cosine - line.y * sine, y: y + line.x * sine + line.y * cosine,
111
+ text: line.text, fontSize, fontFamily, fontWeight, fontStyle, textAlign, textBaseline, rotation
112
+ }, profile));
113
+ const union = bounds.reduce((result, bound) => ({
114
+ left: Math.min(result.left, bound.left), right: Math.max(result.right, bound.right),
115
+ top: Math.min(result.top, bound.top), bottom: Math.max(result.bottom, bound.bottom)
116
+ }), { left: Infinity, right: -Infinity, top: Infinity, bottom: -Infinity });
117
+ return Object.freeze(requireFiniteBounds(union, "Text"));
118
+ }
119
+ const width = measureTextWidth(text, { fontSize, fontFamily, fontWeight, fontStyle }, profile);
105
120
  const [left, right] = textAlign === "center"
106
121
  ? [-width / 2, width / 2]
107
122
  : ["right", "end"].includes(textAlign) ? [-width, 0] : [0, width];
@@ -126,3 +141,25 @@ export function resolveTextBounds({
126
141
  };
127
142
  return Object.freeze(requireFiniteBounds(bounds, "Text"));
128
143
  }
144
+
145
+ export function unionBounds(bounds) {
146
+ return bounds.reduce((union, item) => ({
147
+ left: Math.min(union.left, item.left),
148
+ right: Math.max(union.right, item.right),
149
+ top: Math.min(union.top, item.top),
150
+ bottom: Math.max(union.bottom, item.bottom)
151
+ }), { left: Infinity, right: -Infinity, top: Infinity, bottom: -Infinity });
152
+ }
153
+
154
+ // Resolve explicit hard breaks once for text marks and guide layout.
155
+ export function resolveTextLines(text, config) {
156
+ const parts = text.split(/\r\n|\r|\n/u);
157
+ if (parts.length === 1 && config.blockAlign !== "middle") return undefined;
158
+ validateGeneratedItemLimit(parts.length, "Text line count");
159
+ const lineHeight = config.lineHeight ?? config.fontSize * 1.2;
160
+ const bounds = resolveTextBounds({ x: 0, y: 0, text: "", fontSize: config.fontSize,
161
+ textBaseline: config.baseline, textAlign: config.align });
162
+ const offset = config.blockAlign === "middle"
163
+ ? -(bounds.top + bounds.bottom + (parts.length - 1) * lineHeight) / 2 : 0;
164
+ return parts.map((text, index) => ({ text, x: 0, y: offset + index * lineHeight }));
165
+ }
@@ -98,3 +98,29 @@ export function validateNonNegativeFinite(value, label) {
98
98
  }
99
99
  return value;
100
100
  }
101
+
102
+ export function requireStringValue(value, label) {
103
+ if (typeof value !== "string" || value.length === 0) {
104
+ throw new TypeError(`${label} must be a non-empty string.`);
105
+ }
106
+ return value;
107
+ }
108
+
109
+ export function rejectUnknownProperties(value, supported, label) {
110
+ const unknown = Object.keys(value).find(key => !supported.includes(key));
111
+ if (unknown !== undefined) throw new Error(`Unknown ${label} property "${unknown}".`);
112
+ }
113
+
114
+ export function validateFontWeight(value, label, numberType = "number") {
115
+ if (typeof value !== "string" && !Number.isFinite(value)) {
116
+ throw new TypeError(`${label} must be a string or ${numberType}.`);
117
+ }
118
+ return value;
119
+ }
120
+
121
+ export function validateTextStyle(config, label) {
122
+ validateNonEmptyString(config.color, `${label} color`);
123
+ validatePositiveFinite(config.fontSize, `${label} fontSize`);
124
+ validateNonEmptyString(config.fontFamily, `${label} fontFamily`);
125
+ validateFontWeight(config.fontWeight, `${label} fontWeight`);
126
+ }
@@ -235,3 +235,7 @@ export const LEGEND_CONFIG_KINDS = Object.freeze([
235
235
  export function includesVocabulary(vocabulary, value) {
236
236
  return vocabulary.includes(value);
237
237
  }
238
+
239
+ export const PLOT_LEGEND_POSITIONS = Object.freeze(["top-left", "top-right", "bottom-left", "bottom-right"]);
240
+ export const LEGEND_POSITIONS = Object.freeze(["left", "right", "top", "bottom", ...PLOT_LEGEND_POSITIONS]);
241
+ export const LEGEND_BLOCK_LAYOUTS = Object.freeze(["row", "column", "wrap"]);
@@ -1,6 +1,8 @@
1
+ import { requireStringValue as nonEmptyString } from "../core/validation.js";
2
+ import { isGroupValue as isNominal } from "./transformKeys.js";
1
3
  import { isPlainObject } from "../core/immutable.js";
2
4
  import {
3
- interpolateNumber,
5
+ quantileSorted,
4
6
  numericExtent,
5
7
  requireFiniteResult,
6
8
  stableFiniteDeviation,
@@ -25,11 +27,6 @@ const PARAMETERIZED_OPERATIONS = [
25
27
  "quantile", "first", "last", "ciLower", "ciUpper"
26
28
  ];
27
29
 
28
- function nonEmptyString(value, label) {
29
- if (typeof value !== "string" || value.length === 0) {
30
- throw new TypeError(`${label} must be a non-empty string.`);
31
- }
32
- }
33
30
 
34
31
  export function isScalarAggregate(value) {
35
32
  return typeof value === "string" && SCALAR_OPERATIONS.has(value);
@@ -137,10 +134,6 @@ function isMissing(value) {
137
134
  (typeof value === "number" && Number.isNaN(value));
138
135
  }
139
136
 
140
- function isNominal(value) {
141
- return typeof value === "string" || typeof value === "boolean" ||
142
- (typeof value === "number" && Number.isFinite(value));
143
- }
144
137
 
145
138
  export function validateAggregateFieldValues(rows, field, fieldType) {
146
139
  if (!Array.isArray(rows)) {
@@ -165,14 +158,7 @@ function finiteValues(values) {
165
158
 
166
159
  function quantile(values, probability) {
167
160
  const ordered = [...values].sort((left, right) => left - right);
168
- const position = (ordered.length - 1) * probability;
169
- const lower = Math.floor(position);
170
- const upper = Math.ceil(position);
171
- return interpolateNumber(
172
- ordered[lower],
173
- ordered[upper],
174
- position - lower
175
- );
161
+ return quantileSorted(ordered, probability);
176
162
  }
177
163
 
178
164
  function comparableOrderKey(value) {
@@ -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
+ }