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,4 +1,5 @@
1
1
  import { isPlainObject } from "../core/immutable.js";
2
+ import { validateNonEmptyString } from "../core/validation.js";
2
3
 
3
4
  const ENTRY_KEYS = Object.freeze(["value", "label"]);
4
5
 
@@ -43,6 +44,14 @@ export function normalizeDisplayLabelMap(value, label = "labelMap") {
43
44
  return Object.freeze(normalized);
44
45
  }
45
46
 
47
+ export function normalizeNumericDisplayLabelMap(value, label = "labelMap") {
48
+ const map = normalizeDisplayLabelMap(value, label);
49
+ if (map.some(entry => typeof entry.value !== "number" || !Number.isFinite(entry.value))) {
50
+ throw new TypeError(`${label} values must be finite numbers.`);
51
+ }
52
+ return map;
53
+ }
54
+
46
55
  export function resolveDisplayLabel(value, map, fallback) {
47
56
  if (map !== undefined) {
48
57
  for (const entry of map) {
@@ -55,3 +64,30 @@ export function resolveDisplayLabel(value, map, fallback) {
55
64
  }
56
65
  return resolved;
57
66
  }
67
+
68
+
69
+ export function normalizeDisplayColorMap(value, label = "colorMap") {
70
+ if (!Array.isArray(value)) throw new TypeError(`${label} must be an array.`);
71
+ const normalized = [];
72
+ for (const [index, entry] of value.entries()) {
73
+ if (!isPlainObject(entry)) throw new TypeError(`${label}[${index}] must be a plain object.`);
74
+ if (Reflect.ownKeys(entry).length !== 2 ||
75
+ !Object.hasOwn(entry, "value") || !Object.hasOwn(entry, "color")) {
76
+ throw new Error(`${label}[${index}] must contain exactly value and color.`);
77
+ }
78
+ if (!isDisplayLabelValue(entry.value)) {
79
+ throw new TypeError(`${label}[${index}].value must be a string, boolean, null, or finite number.`);
80
+ }
81
+ validateNonEmptyString(entry.color, `${label}[${index}].color`);
82
+ if (normalized.some(previous => sameDisplayLabelValue(previous.value, entry.value))) {
83
+ throw new Error(`${label} contains a duplicate typed value.`);
84
+ }
85
+ normalized.push(Object.freeze({ value: entry.value, color: entry.color }));
86
+ }
87
+ return Object.freeze(normalized);
88
+ }
89
+
90
+ export function resolveDisplayLabelColor(value, map, fallback) {
91
+ const entry = map?.find(entry => sameDisplayLabelValue(entry.value, value));
92
+ return entry?.color ?? fallback;
93
+ }
@@ -1,3 +1,5 @@
1
+ import { requireStringValue as nonEmptyString } from "../core/validation.js";
2
+ import { isGroupValue as groupValue } from "./transformKeys.js";
1
3
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
4
  import { stableDecimal } from "./numeric.js";
3
5
 
@@ -6,11 +8,6 @@ const TRANSFORM_KEYS = Object.freeze([
6
8
  ]);
7
9
  const OUTPUT_KEYS = Object.freeze(["value", "cumulative", "probability"]);
8
10
 
9
- function nonEmptyString(value, label) {
10
- if (typeof value !== "string" || value.length === 0) {
11
- throw new TypeError(`${label} must be a non-empty string.`);
12
- }
13
- }
14
11
 
15
12
  function normalizeGroupBy(groupBy) {
16
13
  const fields = groupBy === undefined
@@ -101,10 +98,6 @@ export function normalizeECDFTransform({ field, groupBy, weight, missing = "drop
101
98
  return cloneAndFreeze(transform);
102
99
  }
103
100
 
104
- function groupValue(value) {
105
- return typeof value === "string" || typeof value === "boolean" ||
106
- (typeof value === "number" && Number.isFinite(value));
107
- }
108
101
 
109
102
  function rejectOrDrop(transform, index, reason) {
110
103
  if (transform.missing === "error") {
@@ -6,12 +6,13 @@ import { resolveRectMode } from "../rects.js";
6
6
  import { resolveRuleMode } from "../rules.js";
7
7
 
8
8
  const CARTESIAN_MARKS = new Set([
9
- "point", "line", "area", "bar", "rule", "tick", "rect"
9
+ "point", "line", "area", "bar", "rule", "tick", "rect", "text"
10
10
  ]);
11
11
  const FACET_BAR_GRAINS = new Set([
12
12
  BAR_GRAINS.histogram,
13
13
  BAR_GRAINS.aggregate,
14
- BAR_GRAINS.ranged
14
+ BAR_GRAINS.ranged,
15
+ BAR_GRAINS.centered
15
16
  ]);
16
17
  const ORDINARY_SCALE_CHANNELS = Object.freeze([
17
18
  ["x", "x"], ["y", "y"], ["xOffset", "xOffset"], ["yOffset", "yOffset"],
@@ -66,6 +67,9 @@ function classifyPrimaryLayer(semanticSpec, layer) {
66
67
  `facet requires bar mark "${layer.id}" to be a complete histogram, aggregate, or ranged bar.`
67
68
  );
68
69
  }
70
+ if (type === "text" && layer.encoding?.text === undefined) {
71
+ throw new Error(`Facet text layer "${layer.id}" requires a text encoding.`);
72
+ }
69
73
  const complete = (
70
74
  layer.encoding?.x?.scale !== undefined &&
71
75
  layer.encoding?.y?.scale !== undefined
@@ -3,13 +3,15 @@ import { cloneAndFreeze, isPlainObject } from "../../core/immutable.js";
3
3
  const LEGEND_FAMILIES = Object.freeze({
4
4
  series: "categorical",
5
5
  color: "categorical",
6
+ colorSize: "color-size",
6
7
  stroke: "categorical",
7
8
  gradient: "gradient",
8
9
  interval: "discretized",
9
10
  strokeGradient: "gradient",
10
11
  strokeInterval: "discretized",
11
12
  size: "size",
12
- opacity: "opacity"
13
+ opacity: "opacity",
14
+ strokeWidth: "stroke-width"
13
15
  });
14
16
 
15
17
  function requirePlacements(placements) {
@@ -72,25 +74,34 @@ function translatedBounds(placement, bounds, channel) {
72
74
  };
73
75
  }
74
76
 
77
+ export function resolveFacetOuterSides({ placements } = {}) {
78
+ const cells = requirePlacements(placements);
79
+ const edges = { top: new Map(), bottom: new Map(), left: new Map(), right: new Map() };
80
+ for (const cell of cells) {
81
+ for (const [side, key, coordinate, better] of [
82
+ ["top", cell.column, "row", (a, b) => a < b],
83
+ ["bottom", cell.column, "row", (a, b) => a > b],
84
+ ["left", cell.row, "column", (a, b) => a < b],
85
+ ["right", cell.row, "column", (a, b) => a > b]
86
+ ]) {
87
+ const previous = edges[side].get(key);
88
+ if (previous === undefined || better(cell[coordinate], previous[coordinate])) edges[side].set(key, cell);
89
+ }
90
+ }
91
+ return cloneAndFreeze(Object.fromEntries(Object.entries(edges).map(([side, owners]) => {
92
+ const ids = new Set([...owners.values()].map(cell => cell.id));
93
+ return [side, cells.filter(cell => ids.has(cell.id)).map(cell => cell.id)];
94
+ })));
95
+ }
96
+
75
97
  export function resolveFacetOuterAxes({ placements, axisBounds = {} } = {}) {
76
98
  const cells = requirePlacements(placements);
77
99
  if (!isPlainObject(axisBounds)) {
78
100
  throw new TypeError("Facet axisBounds must be a plain object.");
79
101
  }
80
- const bottomByColumn = new Map();
81
- const leftByRow = new Map();
82
- for (const cell of cells) {
83
- const bottom = bottomByColumn.get(cell.column);
84
- if (bottom === undefined || cell.row > bottom.row) {
85
- bottomByColumn.set(cell.column, cell);
86
- }
87
- const left = leftByRow.get(cell.row);
88
- if (left === undefined || cell.column < left.column) {
89
- leftByRow.set(cell.row, cell);
90
- }
91
- }
92
- const xOwners = new Set([...bottomByColumn.values()].map(cell => cell.id));
93
- const yOwners = new Set([...leftByRow.values()].map(cell => cell.id));
102
+ const sides = resolveFacetOuterSides({ placements: cells });
103
+ const xOwners = new Set(sides.bottom);
104
+ const yOwners = new Set(sides.left);
94
105
  return cloneAndFreeze({
95
106
  x: cells.filter(cell => xOwners.has(cell.id)).map(cell => cell.id),
96
107
  y: cells.filter(cell => yOwners.has(cell.id)).map(cell => cell.id),
@@ -129,7 +140,7 @@ function comparableConfig(config) {
129
140
  return stableValue(rest);
130
141
  }
131
142
 
132
- function sameValue(left, right) {
143
+ export function sameFacetGuideValue(left, right) {
133
144
  return JSON.stringify(stableValue(left)) === JSON.stringify(stableValue(right));
134
145
  }
135
146
 
@@ -152,12 +163,13 @@ function requireLegendChildren(children) {
152
163
  });
153
164
  }
154
165
 
155
- function scaleIds(config) {
156
- const ids = config.scales ?? (config.scale === undefined ? [] : [config.scale]);
166
+ function scaleIds(config, kind) {
167
+ const ids = kind === "colorSize" ? [config.colorScale, config.sizeScale]
168
+ : config.scales ?? (config.scale === undefined ? [] : [config.scale]);
157
169
  if (!Array.isArray(ids) || ids.length === 0 || !ids.every(id =>
158
170
  typeof id === "string" && id.length > 0
159
171
  )) {
160
- throw new Error(`Shared ${config.kind} legend requires resolved scale ids.`);
172
+ throw new Error(`Shared ${kind} legend requires resolved scale ids.`);
161
173
  }
162
174
  return ids;
163
175
  }
@@ -165,6 +177,11 @@ function scaleIds(config) {
165
177
  export function resolveSharedFacetLegends(children) {
166
178
  const normalized = requireLegendChildren(children);
167
179
  const first = normalized[0];
180
+ for (const child of normalized.slice(1)) {
181
+ if (!sameFacetGuideValue(first.guideConfigs.legendGroups, child.guideConfigs.legendGroups)) {
182
+ throw new Error(`Facet child "${child.id}" has incompatible legend group placement.`);
183
+ }
184
+ }
168
185
  const kinds = Object.keys(first.legends);
169
186
  const entries = kinds.map(kind => {
170
187
  const family = LEGEND_FAMILIES[kind];
@@ -178,7 +195,7 @@ export function resolveSharedFacetLegends(children) {
178
195
  ) {
179
196
  throw new Error(`Facet child "${first.id}" has invalid ${kind} legend config.`);
180
197
  }
181
- const scales = scaleIds(config);
198
+ const scales = scaleIds(config, kind);
182
199
  const resolvedScales = Object.fromEntries(scales.map(id => {
183
200
  const scale = first.resolvedScales[id];
184
201
  if (!isPlainObject(scale)) {
@@ -190,14 +207,14 @@ export function resolveSharedFacetLegends(children) {
190
207
  const candidate = child.legends[kind];
191
208
  if (
192
209
  !isPlainObject(candidate) ||
193
- !sameValue(comparableConfig(config), comparableConfig(candidate))
210
+ !sameFacetGuideValue(comparableConfig(config), comparableConfig(candidate))
194
211
  ) {
195
212
  throw new Error(
196
213
  `Facet child "${child.id}" has an incompatible ${kind} legend config.`
197
214
  );
198
215
  }
199
216
  for (const id of scales) {
200
- if (!sameValue(resolvedScales[id], child.resolvedScales[id])) {
217
+ if (!sameFacetGuideValue(resolvedScales[id], child.resolvedScales[id])) {
201
218
  throw new Error(
202
219
  `Facet child "${child.id}" has an incompatible resolved legend scale "${id}".`
203
220
  );
@@ -208,7 +225,7 @@ export function resolveSharedFacetLegends(children) {
208
225
  });
209
226
  for (const child of normalized.slice(1)) {
210
227
  const childKinds = Object.keys(child.legends);
211
- if (!sameValue([...kinds].sort(), [...childKinds].sort())) {
228
+ if (!sameFacetGuideValue([...kinds].sort(), [...childKinds].sort())) {
212
229
  throw new Error(`Facet child "${child.id}" has incompatible legend kinds.`);
213
230
  }
214
231
  }
@@ -229,7 +246,7 @@ export function planFacetGuideOwnership({
229
246
  if (!["each", "outer"].includes(axes)) {
230
247
  throw new Error("Facet axes must be each or outer.");
231
248
  }
232
- if (![false, "shared"].includes(legend)) {
249
+ if (![false, "shared"].includes(legend) && !isPlainObject(legend)) {
233
250
  throw new Error("Facet legend must be false or shared.");
234
251
  }
235
252
  const byId = new Map(children.map(child => [child.id, child]));
@@ -237,23 +254,34 @@ export function planFacetGuideOwnership({
237
254
  throw new Error("Facet guide child ids must match occupied placements exactly.");
238
255
  }
239
256
  const outer = resolveFacetOuterAxes({ placements: cells });
240
- if (legend === "shared" && !isPlainObject(sharedLegends)) {
257
+ const sides = resolveFacetOuterSides({ placements: cells });
258
+ if (legend !== false && !isPlainObject(sharedLegends)) {
241
259
  throw new Error("Shared facet legend ownership requires a compatibility plan.");
242
260
  }
243
261
  return cloneAndFreeze({
244
262
  children: Object.fromEntries(cells.map(cell => {
245
263
  const descriptor = byId.get(cell.id);
246
264
  const presentAxes = descriptor.axes ?? [];
247
- const keepAxes = axes === "each"
248
- ? presentAxes
249
- : presentAxes.filter(channel => outer.children[cell.id][channel]);
265
+ const records = descriptor.axisOwners;
266
+ const removed = {};
267
+ const keepAxes = axes === "each" ? presentAxes : records === undefined
268
+ ? presentAxes.filter(channel => outer.children[cell.id][channel])
269
+ : presentAxes.filter(id => {
270
+ const owner = records.find(owner => owner.id === id);
271
+ if (owner === undefined) throw new Error("Facet axis descriptors must identify every retained owner.");
272
+ const components = Object.entries(owner.components);
273
+ const remove = components.filter(([, side]) => !sides[side]?.includes(cell.id)).map(([component]) => component);
274
+ if (remove.length > 0 && remove.length < components.length) removed[id] = remove;
275
+ return remove.length < components.length;
276
+ });
250
277
  return [cell.id, {
251
278
  keepAxes,
252
279
  removeAxes: presentAxes.filter(channel => !keepAxes.includes(channel)),
253
- removeLegends: legend === "shared" ? descriptor.legendKinds ?? [] : []
280
+ ...(records === undefined ? {} : { removeAxisComponents: removed }),
281
+ removeLegends: legend !== false ? descriptor.legendKinds ?? [] : []
254
282
  }];
255
283
  })),
256
- parent: legend === "shared"
284
+ parent: legend !== false
257
285
  ? { legends: sharedLegends.entries, promoteFrom: sharedLegends.source }
258
286
  : { legends: [] }
259
287
  });
@@ -6,7 +6,6 @@ import {
6
6
  } from "../../core/validation.js";
7
7
  import { planFacetDependencies, resolveFacetFamily } from "./dependencies.js";
8
8
  import { readNominalField } from "../scales/index.js";
9
- const MAX_FACET_CHILDREN = 100;
10
9
 
11
10
  function requireFacetField(field, label = "facet") {
12
11
  if (typeof field !== "string" || field.length === 0) {
@@ -100,8 +99,7 @@ export function resolveFacetDefinition(semanticSpec, options = {}) {
100
99
  const values = resolveValues(observed, options.values);
101
100
  validateGeneratedItemLimit(
102
101
  values.length,
103
- "Facet child count",
104
- MAX_FACET_CHILDREN
102
+ "Facet child count"
105
103
  );
106
104
  validateWorkLimit(
107
105
  dataset.values.length * values.length,
@@ -175,8 +173,7 @@ export function resolveFacetGridDefinition(semanticSpec, options = {}) {
175
173
  }
176
174
  validateGeneratedItemLimit(
177
175
  selected.length,
178
- "Facet grid child count",
179
- MAX_FACET_CHILDREN
176
+ "Facet grid child count"
180
177
  );
181
178
  validateWorkLimit(
182
179
  dataset.values.length * selected.length,
@@ -0,0 +1,65 @@
1
+ import { isPlainObject } from "../../core/immutable.js";
2
+ import { validateOptionObject, validateNonEmptyString } from "../../core/validation.js";
3
+ import { validateUserId } from "../../core/identifiers.js";
4
+ import { LEGEND_CONFIG_KINDS, LEGEND_POSITIONS } from "../../core/vocabulary.js";
5
+ import { describeLegendBlock } from "../legendBlocks.js";
6
+ import { validateLegendGroupState } from "../legendGroups.js";
7
+ import { resolveSharedFacetLegends, sameFacetGuideValue as same } from "./guides.js";
8
+
9
+ const CONFIG_KEYS = Object.freeze([
10
+ "kind", "target", "channels", "scales", "scale", "field", "fieldType", "domain", "title", "titleVisible",
11
+ "inferredTitle", "inferredSymbol", "inferredStrokeWidth", "inferredOffset", "position", "align", "offset", "direction", "columns",
12
+ "titlePosition", "symbol", "labels", "titleStyle", "itemGap", "layout", "overflow", "border", "gradient",
13
+ "count", "sampling", "representation", "gradientDirection", "inheritAppearance", "inheritPosition", "blockOverrides",
14
+ "combine", "sizeAreas", "clipHeight"
15
+ ]);
16
+
17
+ export function isAuthoredFacetLegend(value) {
18
+ return isPlainObject(value) && value.type === "authored-legend";
19
+ }
20
+
21
+ export function validateAuthoredFacetLegend(program, intent) {
22
+ validateOptionObject(intent, ["type", "blocks", "groups"], "Stored parent legend");
23
+ if (!isAuthoredFacetLegend(intent) || Object.keys(intent).length !== 3 ||
24
+ !isPlainObject(intent.blocks) || Object.keys(intent.blocks).length === 0 || !isPlainObject(intent.groups)) {
25
+ throw new Error("Stored parent legend requires its closed authored block intent.");
26
+ }
27
+ validateLegendGroupState(intent.groups);
28
+ const configs = {};
29
+ for (const [kind, block] of Object.entries(intent.blocks)) {
30
+ if (!LEGEND_CONFIG_KINDS.includes(kind)) throw new Error("Stored parent legend has an unknown block kind.");
31
+ validateOptionObject(block, ["config", "guide"], "Stored parent legend block");
32
+ validateOptionObject(block.config, kind === "colorSize" ? [...CONFIG_KEYS, "colorScale", "sizeScale"] : CONFIG_KEYS, "Stored parent legend config");
33
+ const config = block.config;
34
+ validateUserId(config.target, "Parent legend target");
35
+ if (!LEGEND_POSITIONS.includes(config.position)) throw new Error("Stored parent legend requires a native position.");
36
+ for (const flag of ["inferredTitle", "inferredSymbol", "inferredStrokeWidth", "inferredOffset", "titleVisible", "inheritAppearance", "inheritPosition"]) {
37
+ if (config[flag] !== undefined && typeof config[flag] !== "boolean") throw new Error(`Stored parent legend ${flag} must be boolean.`);
38
+ }
39
+ const descriptor = describeLegendBlock(program, kind, config);
40
+ if (block.guide !== undefined) {
41
+ validateOptionObject(block.guide, kind === "colorSize" ? ["colorScale", "sizeScale", "title"]
42
+ : ["scale", "scales", "channels", "title", "order"], "Stored parent legend meaning");
43
+ if (kind === "colorSize" && (block.guide.colorScale !== config.colorScale || block.guide.sizeScale !== config.sizeScale)) {
44
+ throw new Error("Stored parent color-size meaning must match both block scale bindings.");
45
+ }
46
+ if (block.guide.title !== undefined) validateNonEmptyString(block.guide.title, "Parent legend title");
47
+ if (block.guide.channels !== undefined && !same([...block.guide.channels].sort(), descriptor.channels)) {
48
+ throw new Error("Stored parent legend meaning must match its block channels.");
49
+ }
50
+ if (block.guide.scale !== undefined && block.guide.scale !== descriptor.scaleIds[0] ||
51
+ block.guide.scales !== undefined && !same(block.guide.scales, descriptor.scaleIds)) {
52
+ throw new Error("Stored parent legend meaning must match its block scale binding.");
53
+ }
54
+ }
55
+ if (!same(config, program.guideConfigs.legend?.[kind])) throw new Error("Parent legend materialization must match its authored owner.");
56
+ configs[kind] = config;
57
+ }
58
+ if (!same(Object.keys(configs).sort(), Object.keys(program.guideConfigs.legend ?? {}).sort()) ||
59
+ !same(intent.groups, program.guideConfigs.legendGroups ?? {})) {
60
+ throw new Error("Parent legend materialization must contain exactly its authored blocks and groups.");
61
+ }
62
+ resolveSharedFacetLegends(program.compositionSpec.children.map(id => ({ id,
63
+ guideConfigs: { legend: configs, legendGroups: intent.groups }, resolvedScales: program.children[id].resolvedScales })));
64
+ return intent;
65
+ }
@@ -1,3 +1,5 @@
1
+ import { rejectUnknownProperties as rejectUnknownKeys } from "../core/validation.js";
2
+ import { requireStringValue as requireField } from "../core/validation.js";
1
3
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
4
 
3
5
  const TRANSFORM_KEYS = Object.freeze(["type", "fields", "as"]);
@@ -5,19 +7,7 @@ const AS_KEYS = Object.freeze(["key", "value"]);
5
7
  const MAX_FIELDS = 64;
6
8
  const MAX_OUTPUT_ROWS = 10_000;
7
9
 
8
- function rejectUnknownKeys(value, supported, label) {
9
- const unknown = Object.keys(value).find(key => !supported.includes(key));
10
- if (unknown !== undefined) {
11
- throw new Error(`Unknown ${label} property "${unknown}".`);
12
- }
13
- }
14
10
 
15
- function requireField(value, label) {
16
- if (typeof value !== "string" || value.length === 0) {
17
- throw new TypeError(`${label} must be a non-empty string.`);
18
- }
19
- return value;
20
- }
21
11
 
22
12
  function normalizeAs(value) {
23
13
  const requested = value ?? {};
@@ -1,3 +1,4 @@
1
+ import { requireStringValue as requireField } from "../core/validation.js";
1
2
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
3
  import {
3
4
  deriveKernelDensity,
@@ -23,12 +24,6 @@ const TRANSFORM_KEYS = [
23
24
  ];
24
25
  const OUTPUT_KEYS = Object.keys(GRADIENT_PROFILE_FIELDS);
25
26
 
26
- function requireField(value, label) {
27
- if (typeof value !== "string" || value.length === 0) {
28
- throw new TypeError(`${label} must be a non-empty string.`);
29
- }
30
- return value;
31
- }
32
27
 
33
28
  function validatePair(value, label) {
34
29
  if (
@@ -0,0 +1 @@
1
+ import{isPlainObject as i}from"../core/immutable.js";import{validateGeneratedItemLimit as o}from"../core/validation.js";function n(e,s="Guide strokeDash"){if(!Array.isArray(e)||e.length%2!==0||!e.every(r=>Number.isFinite(r)&&r>=0))throw new TypeError(`${s} must be an even-length array of non-negative finite numbers.`);return o(e.length,s),e}function u(e,{reset:s=!0}={}){if(e==="auto"&&s)return;if(!Array.isArray(e))throw new TypeError("Guide strokeDashMap must be an array.");o(e.length,"Guide strokeDashMap count");const r=new Set,a=e.map(t=>{if(!i(t)||Reflect.ownKeys(t).length!==2||!Object.hasOwn(t,"value")||!Object.hasOwn(t,"strokeDash"))throw new TypeError("Guide strokeDashMap entries must contain exactly value and strokeDash.");if(!["string","boolean"].includes(typeof t.value)&&!Number.isFinite(t.value))throw new TypeError("Guide strokeDashMap values must be strings, booleans, or finite numbers.");if(r.has(t.value))throw new Error("Guide strokeDashMap contains a duplicate typed value.");return r.add(t.value),n(t.strokeDash),Object.freeze({value:t.value,strokeDash:Object.freeze([...t.strokeDash])})});return a.length===0?void 0:Object.freeze(a)}function d(e,s){const r={...e};if(Object.hasOwn(s,"strokeDash")&&(n(s.strokeDash),r.strokeDash=[...s.strokeDash]),Object.hasOwn(s,"strokeDashMap")){const a=u(s.strokeDashMap);a===void 0?delete r.strokeDashMap:r.strokeDashMap=a}return r}function D(e,s){const r=new Map(s.strokeDashMap?.map(a=>[a.value,a.strokeDash]));return e.map(a=>r.get(a)??s.strokeDash??[])}export{u as normalizeGuideDashMap,d as patchGuideDash,D as resolveGuideDashes,n as validateGuideDash};
@@ -1,26 +1,16 @@
1
+ import { normalizeSortBy, validateSortBy } from "./transformKeys.js";
2
+ import { rejectUnknownProperties as rejectUnknownKeys } from "../core/validation.js";
3
+ import { requireStringValue as requireField } from "../core/validation.js";
1
4
  import { IMPUTE_REQUIRED_OPTIONS } from "../core/optionRequirements.js";
2
5
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
3
- import { interpolateNumber, inverseLerp } from "./numeric.js";
6
+ import { interpolateNumber, inverseLerp, stableFiniteMean } from "./numeric.js";
4
7
  import { normalizeTemporalValue } from "./scales/fields.js";
5
8
 
6
9
  const TRANSFORM_KEYS = Object.freeze([
7
10
  "type", "fields", "groupBy", "sortBy", "method", "value", "edges", "maxGap"
8
11
  ]);
9
- const SORT_KEYS = Object.freeze(["field", "order"]);
10
12
  const METHODS = Object.freeze(Object.keys(IMPUTE_REQUIRED_OPTIONS));
11
13
 
12
- function rejectUnknownKeys(value, supported, label) {
13
- const unknown = Object.keys(value).find(key => !supported.includes(key));
14
- if (unknown !== undefined) throw new Error(`Unknown ${label} property "${unknown}".`);
15
- }
16
-
17
- function requireField(value, label) {
18
- if (typeof value !== "string" || value.length === 0) {
19
- throw new TypeError(`${label} must be a non-empty string.`);
20
- }
21
- return value;
22
- }
23
-
24
14
  function validateFieldList(value, label) {
25
15
  if (!Array.isArray(value) || value.length === 0 || value.some(field =>
26
16
  typeof field !== "string" || field.length === 0
@@ -35,33 +25,6 @@ function normalizeFieldList(value, { allowEmpty = false } = {}) {
35
25
  return Array.isArray(value) ? [...value] : [value];
36
26
  }
37
27
 
38
- function normalizeSortBy(value) {
39
- if (value === undefined) return [];
40
- if (!Array.isArray(value)) return value;
41
- return value.map(sort => isPlainObject(sort)
42
- ? { ...sort, order: sort.order ?? "ascending" }
43
- : sort
44
- );
45
- }
46
-
47
- function validateSortBy(sortBy) {
48
- if (!Array.isArray(sortBy)) throw new TypeError("Impute sortBy must be an array.");
49
- const fields = [];
50
- sortBy.forEach((sort, index) => {
51
- if (!isPlainObject(sort)) {
52
- throw new TypeError(`Impute sortBy[${index}] must be a plain object.`);
53
- }
54
- rejectUnknownKeys(sort, SORT_KEYS, `impute sortBy[${index}]`);
55
- fields.push(requireField(sort.field, `Impute sortBy[${index}].field`));
56
- if (!["ascending", "descending"].includes(sort.order)) {
57
- throw new Error(`Unsupported impute sort order "${sort.order}".`);
58
- }
59
- });
60
- if (new Set(fields).size !== fields.length) {
61
- throw new Error("Impute sortBy fields must be unique.");
62
- }
63
- }
64
-
65
28
  function scalarType(value, label) {
66
29
  if (value === null || value === undefined) return undefined;
67
30
  if (typeof value === "string" || typeof value === "boolean") return typeof value;
@@ -94,7 +57,7 @@ export function validateImputeTransform(transform) {
94
57
  if (new Set(transform.groupBy).size !== transform.groupBy.length) {
95
58
  throw new Error("Impute groupBy fields must be unique.");
96
59
  }
97
- validateSortBy(transform.sortBy);
60
+ validateSortBy(transform.sortBy, "Impute");
98
61
  if (!METHODS.includes(transform.method)) {
99
62
  throw new Error(`Unsupported impute method "${transform.method}".`);
100
63
  }
@@ -107,7 +70,7 @@ export function validateImputeTransform(transform) {
107
70
  scalarType(transform.value, "Impute constant value");
108
71
  } else {
109
72
  if (hasValue) throw new Error(`Impute value is not available for ${transform.method}.`);
110
- if (transform.sortBy.length === 0) {
73
+ if (transform.method !== "mean" && transform.sortBy.length === 0) {
111
74
  throw new Error(`Impute ${transform.method} requires non-empty sortBy.`);
112
75
  }
113
76
  }
@@ -132,7 +95,7 @@ export function normalizeImputeTransform(args = {}) {
132
95
  fields: normalizeFieldList(args.fields),
133
96
  groupBy: normalizeFieldList(args.groupBy, { allowEmpty: true }),
134
97
  sortBy: normalizeSortBy(args.sortBy),
135
- method: args.method,
98
+ method: args.method ?? "mean",
136
99
  edges: args.edges ?? "keep"
137
100
  };
138
101
  if (Object.hasOwn(args, "value")) transform.value = args.value;
@@ -212,9 +175,9 @@ function validateFieldTypes(entries, transform) {
212
175
  if (types.size > 1) {
213
176
  throw new TypeError(`Impute field "${field}" must contain one scalar type.`);
214
177
  }
215
- if (transform.method === "linear" &&
178
+ if (["linear", "mean"].includes(transform.method) &&
216
179
  (types.size > 0 && !types.has("number"))) {
217
- throw new TypeError(`Impute linear field "${field}" must contain finite numbers.`);
180
+ throw new TypeError(`Impute ${transform.method} field "${field}" must contain finite numbers.`);
218
181
  }
219
182
  }
220
183
  }
@@ -278,10 +241,18 @@ function setField(row, field, value) {
278
241
  });
279
242
  }
280
243
 
281
- function fillRun(partition, field, run, transform, positions) {
244
+ function fillRun(partition, field, run, transform, positions, mean) {
282
245
  const [start, end] = run;
283
246
  const length = end - start + 1;
284
247
  if (transform.maxGap !== undefined && length > transform.maxGap) return;
248
+ if (transform.method === "mean") {
249
+ if (mean === undefined) {
250
+ if (transform.edges === "error") throw new RangeError(`Impute mean requires an observed value in field "${field}".`);
251
+ return;
252
+ }
253
+ for (let index = start; index <= end; index += 1) setField(partition[index].row, field, mean);
254
+ return;
255
+ }
285
256
  if (transform.method === "constant") {
286
257
  for (let index = start; index <= end; index += 1) {
287
258
  setField(partition[index].row, field, transform.value);
@@ -332,8 +303,10 @@ export function deriveImputedRows(rows, transform) {
332
303
  partition.sort((left, right) => compareEntries(left, right, transform.sortBy));
333
304
  const positions = linearPositions(partition, transform);
334
305
  for (const field of transform.fields) {
306
+ const observed = transform.method === "mean" ? partition.map(entry => entry.row[field]).filter(value => value !== null && value !== undefined) : [];
307
+ const mean = observed.length ? stableFiniteMean(observed, "Impute group mean") : undefined;
335
308
  for (const run of missingRuns(partition, field)) {
336
- fillRun(partition, field, run, transform, positions);
309
+ fillRun(partition, field, run, transform, positions, mean);
337
310
  }
338
311
  }
339
312
  }
@@ -1,3 +1,5 @@
1
+ import { requireStringValue as nonEmptyString } from "../core/validation.js";
2
+ import { isGroupValue } from "./transformKeys.js";
1
3
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
4
  import { aggregateScalarValues } from "./aggregate.js";
3
5
  import { requireFiniteResult, stableDecimal } from "./numeric.js";
@@ -21,11 +23,6 @@ const TRANSFORM_KEYS = [
21
23
  ];
22
24
  const OUTPUT_KEYS = ["center", "lower", "upper"];
23
25
 
24
- function nonEmptyString(value, label) {
25
- if (typeof value !== "string" || value.length === 0) {
26
- throw new TypeError(`${label} must be a non-empty string.`);
27
- }
28
- }
29
26
 
30
27
  function validateGrouping(groupBy) {
31
28
  if (
@@ -166,10 +163,6 @@ function isMissing(value) {
166
163
  (typeof value === "number" && Number.isNaN(value));
167
164
  }
168
165
 
169
- function isGroupValue(value) {
170
- return typeof value === "string" || typeof value === "boolean" ||
171
- (typeof value === "number" && Number.isFinite(value));
172
- }
173
166
 
174
167
  function deriveGroup(values, transform) {
175
168
  if (transform.center === "median") {
@@ -0,0 +1 @@
1
+ import{LEGEND_CHANNELS as d,LEGEND_CONFIG_KINDS as h}from"../core/vocabulary.js";import{isEnumeratedSizeScaleType as u}from"./scales/size.js";function y(e,r){if(!Array.isArray(e))throw new TypeError(`${r} channels must be an array.`);if(e.length===0)throw new Error(`${r} channels must select at least one channel.`);const s=new Set;for(const t of e){if(!d.includes(t))throw new Error(`Unsupported legend channel "${t}".`);if(s.has(t))throw new Error(`${r} channels contains duplicate "${t}".`);s.add(t)}}const o=Object.freeze(["series","color","stroke"]),a=Object.freeze({size:"size",opacity:"opacity",strokeWidth:"strokeWidth",gradient:"color",interval:"color",strokeGradient:"stroke",strokeInterval:"stroke"}),c=(e,r)=>e<r?-1:e>r?1:0;function w(e,r){const s=e==="colorSize"?["color","size"]:o.includes(e)?r?.channels:a[e]===void 0?void 0:[a[e]];if(!Array.isArray(s)||s.length===0)throw new Error(`Legend block ${e} requires at least one channel.`);return y(s,`Legend block ${e}`),Object.freeze([...s].sort(c))}function p(e){if(!Array.isArray(e)||e.length===0)throw new TypeError("Legend block key requires a non-empty channel array.");return JSON.stringify([...e].sort(c))}function b(e,r,s){if(!h.includes(r))throw new Error(`Unknown legend block kind "${r}".`);const t=w(r,s),n=r==="colorSize"?[s.colorScale,s.sizeScale]:o.includes(r)?s?.scales:[s?.scale];if(!Array.isArray(n)||n.length===0||n.some(l=>typeof l!="string"||e.resolvedScales[l]===void 0))throw new Error(`Legend block ${r} requires resolved scales.`);const i=o.includes(r)?"categorical":r==="colorSize"?"sampled":r==="size"?u(e.resolvedScales[n[0]].type)?"discrete-size":"sampled":["opacity","strokeWidth"].includes(r)||r==="gradient"&&s.representation==="symbols"?"sampled":["gradient","strokeGradient"].includes(r)?"gradient":"interval";return Object.freeze({target:s.target,kind:r,family:i,key:p(t),channels:t,scaleIds:Object.freeze([...n]),config:s})}function g(e,r){return e.symbol.layers.length===1&&(e.kind==="series"&&r==="line"||["color","stroke"].includes(e.kind)&&r==="swatch")?`${e.kind}LegendSymbols`:`${e.kind}LegendSymbol${{line:"Lines",point:"Points",swatch:"Swatches"}[r]}`}export{g as categoricalLegendSymbolId,b as describeLegendBlock,w as legendBlockChannels,p as legendBlockKey,y as validateLegendChannels};
@@ -0,0 +1,9 @@
1
+ // Legend borders use one closed rectangular path, so odd dash duplication is not implicit.
2
+ export function normalizeLegendBorderDash(value, label = "Legend border strokeDash") {
3
+ if (value === undefined) return undefined;
4
+ if (!Array.isArray(value) || value.length % 2 !== 0 ||
5
+ ![...value].every(item => Number.isFinite(item) && item >= 0)) {
6
+ throw new TypeError(`${label} must be an even-length array of non-negative finite numbers.`);
7
+ }
8
+ return Object.freeze([...value]);
9
+ }
@@ -0,0 +1,19 @@
1
+ export function normalizeLegendClipHeight(value,{edit=false}={}){
2
+ if(value===undefined||edit&&value==='auto')return undefined;
3
+ if(!Number.isFinite(value)||value<=0)throw new RangeError('Legend clipHeight must be a positive finite number.');
4
+ return value;
5
+ }
6
+ export function validateLegendClip(kind,config){
7
+ if(config.clipHeight===undefined)return;
8
+ normalizeLegendClipHeight(config.clipHeight);
9
+ const supported=['size','opacity','colorSize'].includes(kind)||kind==='gradient'&&config.representation==='symbols'||
10
+ ['series','color','stroke'].includes(kind)&&config.symbol?.layers?.some(layer=>layer.type==='point');
11
+ if(!supported)throw new Error('Legend clipHeight requires a point-bearing sample block.');
12
+ }
13
+ export function patchLegendClipHeight(config,value){
14
+ const {clipHeight:_old,...rest}=config,clipHeight=normalizeLegendClipHeight(value,{edit:true});
15
+ return clipHeight===undefined?rest:{...rest,clipHeight};
16
+ }
17
+ export function clippedSampleBounds(bounds,height){
18
+ return height===undefined?bounds:{...bounds,top:Math.max(bounds.top,-height/2),bottom:Math.min(bounds.bottom,height/2)};
19
+ }