ggaction 0.0.13 → 0.0.16

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 (359) hide show
  1. package/CHANGELOG.md +146 -0
  2. package/README.md +41 -12
  3. package/knowledge/action-cards.json +1 -1
  4. package/knowledge/intent-taxonomy.json +1 -1
  5. package/knowledge/mcp-resources.json +1 -1
  6. package/knowledge/task-packet.schema.json +1 -1
  7. package/knowledge/task-resolver.js +1 -1
  8. package/package.json +39 -7
  9. package/src/BasicChartProgram.js +3 -1
  10. package/src/ChartProgram.js +2 -0
  11. package/src/accessibility.js +157 -0
  12. package/src/actions/basic.js +2 -2
  13. package/src/actions/boxPlots/components.js +20 -12
  14. package/src/actions/boxPlots/create.js +4 -5
  15. package/src/actions/boxPlots/edit.js +34 -9
  16. package/src/actions/boxPlots/materialize.js +12 -4
  17. package/src/actions/boxPlots/options.js +48 -16
  18. package/src/actions/canvas/actions.js +3 -3
  19. package/src/actions/canvas/fitting.js +1 -1
  20. package/src/actions/categoryOrder/index.js +21 -12
  21. package/src/actions/charts/area.js +6 -2
  22. package/src/actions/charts/bar.js +3 -2
  23. package/src/actions/charts/beeswarm.js +1 -1
  24. package/src/actions/charts/density.js +7 -3
  25. package/src/actions/charts/ecdf.js +6 -3
  26. package/src/actions/charts/endpoints.js +29 -7
  27. package/src/actions/charts/heatmap.js +3 -2
  28. package/src/actions/charts/histogram.js +5 -4
  29. package/src/actions/charts/horizon.js +4 -2
  30. package/src/actions/charts/interval-regression.js +7 -4
  31. package/src/actions/charts/line.js +4 -2
  32. package/src/actions/charts/parallel.js +9 -3
  33. package/src/actions/charts/pie.js +11 -3
  34. package/src/actions/charts/polar.js +17 -6
  35. package/src/actions/charts/radar.js +9 -3
  36. package/src/actions/charts/radial.js +12 -4
  37. package/src/actions/charts/raincloud.js +2 -2
  38. package/src/actions/charts/rug-strip.js +8 -4
  39. package/src/actions/charts/scatter.js +4 -2
  40. package/src/actions/composition/actions.js +29 -24
  41. package/src/actions/coordinates/actions.js +2 -2
  42. package/src/actions/coordinates/edit.js +76 -0
  43. package/src/actions/data/basicBin2d.js +4 -5
  44. package/src/actions/data/bin.js +3 -3
  45. package/src/actions/data/bin2d.js +63 -193
  46. package/src/actions/data/bin2dMaterialize.js +4 -5
  47. package/src/actions/data/box.js +4 -4
  48. package/src/actions/data/complete.js +26 -0
  49. package/src/actions/data/computed.js +4 -4
  50. package/src/actions/data/create.js +7 -14
  51. package/src/actions/data/density.js +15 -23
  52. package/src/actions/data/derived.js +57 -31
  53. package/src/actions/data/ecdf.js +2 -2
  54. package/src/actions/data/edit.js +551 -0
  55. package/src/actions/data/filter.js +28 -22
  56. package/src/actions/data/fold.js +2 -2
  57. package/src/actions/data/gradientProfile.js +2 -2
  58. package/src/actions/data/horizon.js +2 -2
  59. package/src/actions/data/impute.js +26 -0
  60. package/src/actions/data/index.js +62 -47
  61. package/src/actions/data/interval.js +2 -2
  62. package/src/actions/data/intervalEdit.js +26 -18
  63. package/src/actions/data/normalize.js +27 -0
  64. package/src/actions/data/regression.js +21 -27
  65. package/src/actions/data/revise.js +48 -0
  66. package/src/actions/data/revisionBindings.js +125 -0
  67. package/src/actions/data/shared.js +20 -4
  68. package/src/actions/data/stack.js +2 -2
  69. package/src/actions/data/summary.js +3 -3
  70. package/src/actions/data/timeUnit.js +6 -3
  71. package/src/actions/data/window.js +3 -3
  72. package/src/actions/encodings/angle.js +1 -1
  73. package/src/actions/encodings/appearance.js +4 -4
  74. package/src/actions/encodings/barWidth.js +1 -1
  75. package/src/actions/encodings/channels.js +415 -0
  76. package/src/actions/encodings/color/index.js +3 -2
  77. package/src/actions/encodings/density.js +7 -4
  78. package/src/actions/encodings/histogram.js +4 -3
  79. package/src/actions/encodings/horizon.js +6 -8
  80. package/src/actions/encodings/index.js +4 -0
  81. package/src/actions/encodings/offset.js +22 -15
  82. package/src/actions/encodings/parallel.js +4 -5
  83. package/src/actions/encodings/pathOrder.js +6 -11
  84. package/src/actions/encodings/position/apply.js +53 -7
  85. package/src/actions/encodings/position/index.js +14 -6
  86. package/src/actions/encodings/position/policies/bar.js +9 -0
  87. package/src/actions/encodings/position/resolve.js +24 -7
  88. package/src/actions/encodings/ranged.js +5 -5
  89. package/src/actions/encodings/remove.js +13 -9
  90. package/src/actions/encodings/ruleAppearance.js +2 -24
  91. package/src/actions/encodings/seriesLayout.js +2 -1
  92. package/src/actions/encodings/shared.js +82 -14
  93. package/src/actions/encodings/stroke.js +226 -0
  94. package/src/actions/encodings/strokeDash.js +2 -2
  95. package/src/actions/encodings/text.js +1 -1
  96. package/src/actions/errorBands/create.js +34 -20
  97. package/src/actions/errorBands/edit.js +34 -137
  98. package/src/actions/errorBars/create.js +43 -20
  99. package/src/actions/errorBars/edit.js +29 -125
  100. package/src/actions/facets/actions.js +267 -73
  101. package/src/actions/facets/derive.js +99 -28
  102. package/src/actions/facets/guides.js +4 -5
  103. package/src/actions/facets/replay.js +4 -5
  104. package/src/actions/gradientPlots/components.js +15 -8
  105. package/src/actions/gradientPlots/create.js +4 -5
  106. package/src/actions/gradientPlots/edit.js +18 -9
  107. package/src/actions/gradientPlots/materialize.js +5 -3
  108. package/src/actions/gradientPlots/options.js +10 -2
  109. package/src/actions/gradientPlots/rebind.js +4 -5
  110. package/src/actions/guides/applicability.js +8 -2
  111. package/src/actions/guides/axes/axes.js +23 -4
  112. package/src/actions/guides/axes/axis.js +2 -2
  113. package/src/actions/guides/axes/edit.js +2 -2
  114. package/src/actions/guides/axes/labels.js +67 -21
  115. package/src/actions/guides/axes/lines.js +15 -9
  116. package/src/actions/guides/axes/parallel/lifecycle.js +13 -13
  117. package/src/actions/guides/axes/parallel/resolve.js +6 -2
  118. package/src/actions/guides/axes/parallel.js +5 -5
  119. package/src/actions/guides/axes/remove.js +4 -4
  120. package/src/actions/guides/axes/tickGroups.js +6 -5
  121. package/src/actions/guides/axes/ticks.js +19 -6
  122. package/src/actions/guides/axes/titles.js +21 -13
  123. package/src/actions/guides/grids/grid.js +18 -29
  124. package/src/actions/guides/grids/resolve.js +7 -2
  125. package/src/actions/guides/guides.js +20 -4
  126. package/src/actions/guides/legends/blocks.js +426 -0
  127. package/src/actions/guides/legends/categorical/actions.js +138 -52
  128. package/src/actions/guides/legends/categorical/components.js +55 -45
  129. package/src/actions/guides/legends/categorical/index.js +37 -33
  130. package/src/actions/guides/legends/categorical/layout.js +98 -16
  131. package/src/actions/guides/legends/categorical/options.js +2 -2
  132. package/src/actions/guides/legends/categorical/recipes.js +111 -20
  133. package/src/actions/guides/legends/categorical/resolve.js +12 -8
  134. package/src/actions/guides/legends/categorical/symbols.js +104 -52
  135. package/src/actions/guides/legends/continuous/common.js +76 -20
  136. package/src/actions/guides/legends/continuous/gradient.js +95 -158
  137. package/src/actions/guides/legends/continuous/index.js +2 -0
  138. package/src/actions/guides/legends/continuous/interval.js +21 -16
  139. package/src/actions/guides/legends/continuous/opacity.js +37 -83
  140. package/src/actions/guides/legends/continuous/stroke.js +225 -0
  141. package/src/actions/guides/legends/creation.js +14 -1
  142. package/src/actions/guides/legends/edit.js +159 -60
  143. package/src/actions/guides/legends/focused.js +5 -5
  144. package/src/actions/guides/legends/index.js +2 -0
  145. package/src/actions/guides/legends/lane.js +59 -24
  146. package/src/actions/guides/legends/lifecycle.js +19 -8
  147. package/src/actions/guides/legends/remove.js +17 -8
  148. package/src/actions/guides/legends/sampling.js +140 -0
  149. package/src/actions/guides/legends/size.js +100 -108
  150. package/src/actions/guides/legends/strokeWidth.js +38 -92
  151. package/src/actions/guides/legends/target.js +112 -1
  152. package/src/actions/guides/legends/transition.js +165 -16
  153. package/src/actions/guides/polar/axes/facade.js +8 -8
  154. package/src/actions/guides/polar/axes/labels.js +29 -10
  155. package/src/actions/guides/polar/axes/lines.js +19 -19
  156. package/src/actions/guides/polar/axes/shared.js +16 -0
  157. package/src/actions/guides/polar/axes/ticks.js +10 -16
  158. package/src/actions/guides/polar/axes/titles.js +6 -6
  159. package/src/actions/guides/polar/grids.js +12 -14
  160. package/src/actions/guides/polar/resolve.js +12 -8
  161. package/src/actions/index.js +16 -3
  162. package/src/actions/intervals/revision.js +126 -0
  163. package/src/actions/marks/arc/actions.js +80 -22
  164. package/src/actions/marks/area/actions.js +38 -13
  165. package/src/actions/marks/area/materialize.js +17 -1
  166. package/src/actions/marks/bar/create.js +12 -3
  167. package/src/actions/marks/bar/edit.js +21 -4
  168. package/src/actions/marks/bar/materialize.js +34 -40
  169. package/src/actions/marks/index.js +10 -1
  170. package/src/actions/marks/line/actions.js +40 -17
  171. package/src/actions/marks/line/materialize.js +29 -10
  172. package/src/actions/marks/point/create.js +9 -3
  173. package/src/actions/marks/point/edit.js +19 -7
  174. package/src/actions/marks/point/jitter.js +6 -8
  175. package/src/actions/marks/point/materialize.js +48 -22
  176. package/src/actions/marks/point/packing.js +6 -8
  177. package/src/actions/marks/rect/actions.js +36 -15
  178. package/src/actions/marks/references.js +602 -33
  179. package/src/actions/marks/remove.js +67 -41
  180. package/src/actions/marks/rule/actions.js +51 -14
  181. package/src/actions/marks/shared.js +1 -1
  182. package/src/actions/marks/text/actions.js +79 -14
  183. package/src/actions/marks/text/index.js +4 -0
  184. package/src/actions/marks/text/layout.js +157 -17
  185. package/src/actions/marks/text/removal.js +173 -0
  186. package/src/actions/marks/text/selection.js +72 -0
  187. package/src/actions/marks/tick/actions.js +25 -8
  188. package/src/actions/primitives/createGraphics.js +5 -4
  189. package/src/actions/primitives/editGraphics.js +31 -4
  190. package/src/actions/primitives/graphicProperties.js +6 -0
  191. package/src/actions/primitives/semantic.js +11 -3
  192. package/src/actions/primitives/semanticAction.js +90 -14
  193. package/src/actions/primitives/semanticValidation/dataset.js +2 -4
  194. package/src/actions/primitives/semanticValidation/guide.js +1 -1
  195. package/src/actions/primitives/semanticValidation/index.js +9 -1
  196. package/src/actions/primitives/semanticValidation/layer.js +5 -0
  197. package/src/actions/primitives/semanticValidation/scale.js +28 -7
  198. package/src/actions/regression/components.js +36 -25
  199. package/src/actions/regression/create.js +13 -6
  200. package/src/actions/regression/edit.js +24 -11
  201. package/src/actions/resources/index.js +7 -0
  202. package/src/actions/resources/remove.js +173 -0
  203. package/src/actions/scales/channels.js +212 -0
  204. package/src/actions/scales/consumers/common.js +1 -1
  205. package/src/actions/scales/consumers/families.js +3 -2
  206. package/src/actions/scales/consumers/index.js +19 -2
  207. package/src/actions/scales/consumers/seriesLayout.js +28 -14
  208. package/src/actions/scales/create.js +55 -23
  209. package/src/actions/scales/definitions.js +94 -40
  210. package/src/actions/scales/edit.js +67 -12
  211. package/src/actions/scales/editPolicy.js +81 -11
  212. package/src/actions/scales/index.js +6 -0
  213. package/src/actions/scales/materialize.js +5 -4
  214. package/src/actions/scales/offset.js +48 -0
  215. package/src/actions/scales/parallel.js +71 -0
  216. package/src/actions/scales/preview.js +111 -10
  217. package/src/actions/scales/quantitativeColor.js +4 -5
  218. package/src/actions/selection/actions.js +179 -66
  219. package/src/actions/selection/index.js +3 -0
  220. package/src/actions/textMetrics/index.js +56 -0
  221. package/src/actions/theme/actions.js +70 -15
  222. package/src/actions/theme/composition.js +238 -0
  223. package/src/actions/theme/index.js +9 -2
  224. package/src/actions/theme/reconcile.js +313 -121
  225. package/src/actions/theme/state.js +160 -0
  226. package/src/actions/titles/actions.js +8 -8
  227. package/src/actions/titles/resolve.js +3 -3
  228. package/src/actions/violinPlots/create.js +6 -3
  229. package/src/actions/violinPlots/edit.js +2 -1
  230. package/src/core/action.js +117 -35
  231. package/src/core/compositionState.js +6 -1
  232. package/src/core/diagnostics.js +16 -0
  233. package/src/core/extensionRegistry.js +27 -0
  234. package/src/core/font.js +18 -0
  235. package/src/core/identifiers.js +5 -4
  236. package/src/core/immutable.js +10 -3
  237. package/src/core/optionRequirements.js +8 -0
  238. package/src/core/programState.js +21 -1
  239. package/src/core/resourceReferences.js +341 -0
  240. package/src/core/textMetricProfile.js +28 -0
  241. package/src/core/textMetrics.js +24 -3
  242. package/src/core/validation.js +22 -10
  243. package/src/core/vocabulary.js +16 -3
  244. package/src/diagnostics.js +1 -0
  245. package/src/grammar/bars/aggregate.js +4 -1
  246. package/src/grammar/bars/geometry.js +44 -0
  247. package/src/grammar/bin.js +54 -9
  248. package/src/grammar/complete.js +279 -0
  249. package/src/grammar/computed.js +265 -64
  250. package/src/grammar/coordinates.js +37 -0
  251. package/src/grammar/density.js +239 -37
  252. package/src/grammar/displayLabels.js +57 -0
  253. package/src/grammar/facets/dependencies.js +133 -1
  254. package/src/grammar/facets/guides.js +3 -0
  255. package/src/grammar/facets/index.js +7 -47
  256. package/src/grammar/facets/scales.js +15 -18
  257. package/src/grammar/impute.js +341 -0
  258. package/src/grammar/markLabelSelection.js +57 -0
  259. package/src/grammar/markLabels.js +18 -1
  260. package/src/grammar/normalize.js +366 -0
  261. package/src/grammar/pathSeries.js +26 -8
  262. package/src/grammar/pointShapes.js +13 -4
  263. package/src/grammar/polar.js +90 -8
  264. package/src/grammar/regression/index.js +1 -0
  265. package/src/grammar/regression/parameters.js +23 -0
  266. package/src/grammar/roundedRect.js +180 -0
  267. package/src/grammar/rules.js +1 -0
  268. package/src/grammar/scales/colorConsumers.js +20 -1
  269. package/src/grammar/scales/continuous.js +2 -2
  270. package/src/grammar/scales/definition.js +27 -4
  271. package/src/grammar/scales/index.js +1 -0
  272. package/src/grammar/scales/ordinal.js +12 -3
  273. package/src/grammar/scales/policies.js +2 -2
  274. package/src/grammar/scales/size.js +456 -0
  275. package/src/grammar/scales/types.js +8 -0
  276. package/src/grammar/schemas/concreteGraphic.js +14 -2
  277. package/src/grammar/schemas/graphic.js +15 -0
  278. package/src/grammar/schemas/graphicBounds.js +190 -41
  279. package/src/grammar/schemas/semanticPath.js +7 -2
  280. package/src/grammar/statisticalReference.js +103 -0
  281. package/src/grammar/strokeStyle.js +77 -0
  282. package/src/grammar/summary.js +78 -16
  283. package/src/grammar/timeUnit.js +216 -5
  284. package/src/grammar/transformTopology.js +4 -0
  285. package/src/grammar/transforms.js +394 -28
  286. package/src/grammar/weightedStatistics.js +374 -0
  287. package/src/grammar/window.js +221 -16
  288. package/src/layout/aspect.js +80 -0
  289. package/src/layout/facets.js +59 -5
  290. package/src/layout/labels.js +351 -7
  291. package/src/layout/legend.js +4 -4
  292. package/src/layout/legendItems.js +6 -6
  293. package/src/layout/text.js +10 -9
  294. package/src/layout/title.js +9 -9
  295. package/src/materialization/bars/aggregate.js +25 -2
  296. package/src/materialization/bars/histogram.js +84 -24
  297. package/src/materialization/bars/ranged.js +19 -3
  298. package/src/materialization/bars/resolve.js +7 -4
  299. package/src/materialization/composition.js +3 -1
  300. package/src/materialization/coordinateBounds.js +204 -0
  301. package/src/materialization/dependencies.js +36 -0
  302. package/src/materialization/facetGuides/legacyCategorical.js +3 -0
  303. package/src/materialization/facetGuides/placement.js +9 -3
  304. package/src/materialization/facetGuides/preparation.js +13 -8
  305. package/src/materialization/facetHeaders.js +347 -0
  306. package/src/materialization/facets.js +15 -65
  307. package/src/materialization/guides/layout.js +3 -3
  308. package/src/materialization/guides/resources.js +26 -0
  309. package/src/materialization/legends.js +1 -1
  310. package/src/materialization/marks/index.js +11 -5
  311. package/src/materialization/marks/policies.js +2 -1
  312. package/src/materialization/rect.js +25 -12
  313. package/src/materialization/rectConfig.js +5 -1
  314. package/src/materialization/scaleGuideDependencies.js +10 -0
  315. package/src/materialization/scales/map.js +5 -3
  316. package/src/materialization/scales/policies/binnedPosition.js +2 -2
  317. package/src/materialization/scales/policies/offset.js +50 -8
  318. package/src/materialization/scales/policies/series.js +4 -1
  319. package/src/materialization/scales/resolve.js +22 -6
  320. package/src/materialization/selection/items/arc.js +28 -4
  321. package/src/materialization/selection/items/bar.js +30 -3
  322. package/src/materialization/selection/items/common.js +20 -5
  323. package/src/materialization/selection/items/rect.js +28 -4
  324. package/src/materialization/selection/path.js +1 -1
  325. package/src/materialization/selection/point.js +3 -1
  326. package/src/materialization/text.js +265 -14
  327. package/src/materialization/typography.js +31 -0
  328. package/src/mcp/cli.js +5 -3
  329. package/src/persistence/codec.js +65 -0
  330. package/src/persistence/validation.js +185 -0
  331. package/src/persistence.js +74 -0
  332. package/src/renderers/canvas/circle.js +2 -3
  333. package/src/renderers/canvas/index.js +5 -4
  334. package/src/renderers/canvas/line.js +2 -3
  335. package/src/renderers/canvas/native.js +4 -4
  336. package/src/renderers/canvas/path.js +2 -3
  337. package/src/renderers/canvas/rect.js +2 -3
  338. package/src/renderers/canvas/stroke.js +11 -0
  339. package/src/renderers/nativeBackend.js +10 -0
  340. package/src/renderers/options.js +13 -0
  341. package/src/renderers/pdf.js +25 -54
  342. package/src/renderers/png.js +22 -17
  343. package/src/renderers/svg.js +29 -14
  344. package/src/renderers/text.js +1 -11
  345. package/src/selectors/coordinates.js +3 -1
  346. package/src/selectors/datasets.js +38 -5
  347. package/src/selectors/layers.js +10 -4
  348. package/src/selectors/markOwners.js +46 -0
  349. package/src/selectors/scales.js +6 -4
  350. package/src/theme/defaults.js +103 -1
  351. package/src/version.js +2 -0
  352. package/types/accessibility.d.ts +37 -0
  353. package/types/basic.d.ts +10 -2
  354. package/types/diagnostics.d.ts +15 -0
  355. package/types/index.d.ts +90 -0
  356. package/types/pdf.d.ts +9 -0
  357. package/types/persistence.d.ts +7 -0
  358. package/types/png.d.ts +9 -0
  359. package/types/program.d.ts +944 -118
@@ -10,6 +10,15 @@ import {
10
10
  requireFiniteResult,
11
11
  stableFiniteDeviation
12
12
  } from "./numeric.js";
13
+ import {
14
+ normalizeStatisticalWeight,
15
+ readStatisticalWeights,
16
+ summarizeStatisticalWeights,
17
+ validateWeightedNumericFields,
18
+ weightedBandwidth,
19
+ weightedEntriesExtent,
20
+ weightedKernelEstimate
21
+ } from "./weightedStatistics.js";
13
22
 
14
23
  const SQRT_TWO_PI = Math.sqrt(2 * Math.PI);
15
24
 
@@ -37,6 +46,25 @@ export const DENSITY_WIDTH_RESOLUTIONS = Object.freeze([
37
46
  "shared", "independent"
38
47
  ]);
39
48
 
49
+ export function normalizeDensityTransform(args = {}) {
50
+ const transform = {
51
+ type: "density",
52
+ field: args.field,
53
+ ...(args.groupBy === undefined ? {} : { groupBy: args.groupBy }),
54
+ bandwidth: args.bandwidth ?? "auto",
55
+ extent: args.extent ?? "auto",
56
+ steps: args.steps ?? 100,
57
+ kernel: args.kernel ?? "gaussian",
58
+ normalization: args.normalization ?? "unit",
59
+ as: args.as ?? [`${args.field}_value`, `${args.field}_density`],
60
+ resolve: args.resolve ?? "shared",
61
+ ...(args.weight === undefined ? {} : { weight: args.weight }),
62
+ ...(args.placement === undefined ? {} : { placement: args.placement })
63
+ };
64
+ validateDensityTransform(transform);
65
+ return cloneAndFreeze(transform);
66
+ }
67
+
40
68
  export function validateDensitySteps(value, label = "Density steps") {
41
69
  if (!Number.isInteger(value) || value < 2) {
42
70
  throw new RangeError(`${label} must be an integer of at least 2.`);
@@ -261,10 +289,88 @@ function validateStoredDensityPlacement(value, groupBy) {
261
289
  return value;
262
290
  }
263
291
 
292
+ function validateResolvedBandwidths(value, transform) {
293
+ if (!Array.isArray(value) || value.length === 0) {
294
+ throw new TypeError(
295
+ "Density resolved bandwidths must be a non-empty array."
296
+ );
297
+ }
298
+ const normalized = value.map((item, index) => {
299
+ if (!isPlainObject(item)) {
300
+ throw new TypeError(
301
+ `Density resolved bandwidth ${index} must be a plain object.`
302
+ );
303
+ }
304
+ const unknown = Object.keys(item).find(
305
+ key => !["group", "split", "bandwidth"].includes(key)
306
+ );
307
+ if (unknown !== undefined) {
308
+ throw new Error(
309
+ `Unknown density resolved bandwidth property "${unknown}".`
310
+ );
311
+ }
312
+ if (!Number.isFinite(item.bandwidth) || item.bandwidth <= 0) {
313
+ throw new RangeError(
314
+ `Density resolved bandwidth ${index} must be positive and finite.`
315
+ );
316
+ }
317
+ if (transform.groupBy === undefined) {
318
+ if (Object.hasOwn(item, "group")) {
319
+ throw new Error(
320
+ "Density resolved bandwidth cannot store a group without groupBy."
321
+ );
322
+ }
323
+ } else if (!Object.hasOwn(item, "group") || !isNominalValue(item.group)) {
324
+ throw new TypeError(
325
+ `Density resolved bandwidth ${index} requires a nominal group.`
326
+ );
327
+ }
328
+ if (transform.placement?.split === undefined) {
329
+ if (Object.hasOwn(item, "split")) {
330
+ throw new Error(
331
+ "Density resolved bandwidth cannot store a split without split placement."
332
+ );
333
+ }
334
+ } else if (!Object.hasOwn(item, "split") || !isNominalValue(item.split)) {
335
+ throw new TypeError(
336
+ `Density resolved bandwidth ${index} requires a nominal split.`
337
+ );
338
+ }
339
+ return item;
340
+ });
341
+ for (let index = 0; index < normalized.length; index += 1) {
342
+ const duplicate = normalized.slice(0, index).some(item =>
343
+ Object.is(item.group, normalized[index].group) &&
344
+ Object.is(item.split, normalized[index].split)
345
+ );
346
+ if (duplicate) {
347
+ throw new Error(
348
+ `Density resolved bandwidth ${index} duplicates an earlier profile.`
349
+ );
350
+ }
351
+ }
352
+ return value;
353
+ }
354
+
355
+ function hasValidResolvedBandwidthState(resolved, transform) {
356
+ const hasBandwidth = Object.hasOwn(resolved, "bandwidth");
357
+ const hasBandwidths = Object.hasOwn(resolved, "bandwidths");
358
+ if (hasBandwidth === hasBandwidths) return false;
359
+ if (hasBandwidth) {
360
+ return Number.isFinite(resolved.bandwidth) && resolved.bandwidth > 0;
361
+ }
362
+ try {
363
+ validateResolvedBandwidths(resolved.bandwidths, transform);
364
+ return true;
365
+ } catch {
366
+ return false;
367
+ }
368
+ }
369
+
264
370
  export function validateDensityTransform(transform) {
265
371
  const supported = [
266
372
  "type", "field", "groupBy", "bandwidth", "extent", "steps", "as",
267
- "resolve", "kernel", "normalization", "placement", "resolved"
373
+ "resolve", "kernel", "normalization", "placement", "weight", "resolved"
268
374
  ];
269
375
  const unknown = Object.keys(transform).find(key => !supported.includes(key));
270
376
  if (unknown !== undefined) {
@@ -274,6 +380,9 @@ export function validateDensityTransform(transform) {
274
380
  throw new Error(`Unsupported density transform "${transform.type}".`);
275
381
  }
276
382
  requireField(transform.field, "Density field");
383
+ if (transform.weight !== undefined) {
384
+ normalizeStatisticalWeight(transform.weight, "Density weight");
385
+ }
277
386
  if (transform.groupBy !== undefined) {
278
387
  requireField(transform.groupBy, "Density groupBy");
279
388
  }
@@ -301,10 +410,9 @@ export function validateDensityTransform(transform) {
301
410
  typeof resolved !== "object" ||
302
411
  Array.isArray(resolved) ||
303
412
  Object.keys(resolved).some(
304
- key => !["bandwidth", "extent", "splitDomain"].includes(key)
413
+ key => !["bandwidth", "bandwidths", "extent", "splitDomain"].includes(key)
305
414
  ) ||
306
- !Number.isFinite(resolved.bandwidth) ||
307
- resolved.bandwidth <= 0 ||
415
+ !hasValidResolvedBandwidthState(resolved, transform) ||
308
416
  !Array.isArray(resolved.extent) ||
309
417
  resolved.extent.length !== 2 ||
310
418
  !resolved.extent.every(Number.isFinite) ||
@@ -319,7 +427,7 @@ export function validateDensityTransform(transform) {
319
427
  })())
320
428
  ) {
321
429
  throw new TypeError(
322
- "Density resolved provenance requires a positive bandwidth, ascending finite extent, and optional two-value split domain."
430
+ "Density resolved provenance requires one positive bandwidth or profile bandwidth list, an ascending finite extent, and an optional two-value split domain."
323
431
  );
324
432
  }
325
433
  }
@@ -363,22 +471,26 @@ export function estimateDensityBandwidth(values) {
363
471
  return bandwidth;
364
472
  }
365
473
 
366
- function resolveBandwidth(value, sourceValues) {
474
+ function resolveBandwidth(value, sourceValues, weightSummary, field) {
367
475
  if (value === undefined || value === "auto") {
368
- return estimateDensityBandwidth(sourceValues);
476
+ return weightSummary === undefined
477
+ ? estimateDensityBandwidth(sourceValues)
478
+ : weightedBandwidth(weightSummary, field);
369
479
  }
370
480
  return validateDensityBandwidth(value);
371
481
  }
372
482
 
373
- function resolveExtent(value, sourceValues) {
483
+ function resolveExtent(value, sourceValues, weightSummary, field) {
374
484
  if (value === undefined || value === "auto") {
375
- let lower = Infinity;
376
- let upper = -Infinity;
377
- for (const sourceValue of sourceValues) {
378
- lower = Math.min(lower, sourceValue);
379
- upper = Math.max(upper, sourceValue);
380
- }
381
- const extent = [lower, upper];
485
+ const extent = weightSummary === undefined
486
+ ? sourceValues.reduce(
487
+ ([lower, upper], sourceValue) => [
488
+ Math.min(lower, sourceValue),
489
+ Math.max(upper, sourceValue)
490
+ ],
491
+ [Infinity, -Infinity]
492
+ )
493
+ : weightedEntriesExtent(weightSummary, field);
382
494
  if (extent[0] === extent[1]) {
383
495
  throw new Error("Density observed extent requires varying finite values.");
384
496
  }
@@ -407,6 +519,17 @@ function estimateAt(sample, values, bandwidth, kernel, normalization) {
407
519
  return requireFiniteResult(stable, "Density estimate");
408
520
  }
409
521
 
522
+ function estimateWeightedAt(sample, summary, field, bandwidth, kernel, normalization) {
523
+ return weightedKernelEstimate(
524
+ summary,
525
+ field,
526
+ sample,
527
+ bandwidth,
528
+ KERNEL_FUNCTIONS[kernel],
529
+ normalization
530
+ );
531
+ }
532
+
410
533
  export function deriveKernelDensity(values, {
411
534
  field,
412
535
  groupBy,
@@ -416,7 +539,8 @@ export function deriveKernelDensity(values, {
416
539
  kernel = "gaussian",
417
540
  normalization = "unit",
418
541
  as,
419
- placement
542
+ placement,
543
+ weight
420
544
  } = {}) {
421
545
  if (!Array.isArray(values)) {
422
546
  throw new TypeError("Density values must be an array.");
@@ -437,29 +561,66 @@ export function deriveKernelDensity(values, {
437
561
  )];
438
562
  const resolvedKernel = validateDensityKernel(kernel);
439
563
  const resolvedNormalization = validateDensityNormalization(normalization);
440
- const validRows = values.filter(row =>
441
- row !== null &&
442
- typeof row === "object" &&
443
- Number.isFinite(row[sourceField]) &&
444
- (groupField === undefined || isNominalValue(row[groupField])) &&
445
- (placement?.split === undefined || isNominalValue(row[placement.split.field]))
446
- );
564
+ const statisticalWeights = weight === undefined
565
+ ? undefined
566
+ : readStatisticalWeights(values, weight, "Density");
567
+ if (statisticalWeights !== undefined) {
568
+ validateWeightedNumericFields(
569
+ statisticalWeights.entries,
570
+ [sourceField],
571
+ "Density"
572
+ );
573
+ }
574
+ const validEntries = statisticalWeights === undefined
575
+ ? undefined
576
+ : statisticalWeights.entries.filter(entry =>
577
+ (groupField === undefined || isNominalValue(entry.row[groupField])) &&
578
+ (placement?.split === undefined ||
579
+ isNominalValue(entry.row[placement.split.field]))
580
+ );
581
+ const validRows = statisticalWeights === undefined
582
+ ? values.filter(row =>
583
+ row !== null &&
584
+ typeof row === "object" &&
585
+ Number.isFinite(row[sourceField]) &&
586
+ (groupField === undefined || isNominalValue(row[groupField])) &&
587
+ (placement?.split === undefined || isNominalValue(row[placement.split.field]))
588
+ )
589
+ : validEntries.map(entry => entry.row);
447
590
  if (validRows.length === 0) {
448
591
  throw new Error("Density requires at least one valid field/group row.");
449
592
  }
450
- validateWorkLimit(validRows.length * steps, "Density computation");
451
- const sourceValues = validRows.map(row => row[sourceField]);
452
- const resolvedBandwidth = resolveBandwidth(bandwidth, sourceValues);
453
- const resolvedExtent = resolveExtent(extent, sourceValues);
593
+ const globalWeightSummary = statisticalWeights === undefined
594
+ ? undefined
595
+ : summarizeStatisticalWeights(
596
+ validEntries,
597
+ statisticalWeights.definition.kind,
598
+ "Density data"
599
+ );
600
+ validateWorkLimit(
601
+ (globalWeightSummary?.positive.length ?? validRows.length) * steps,
602
+ "Density computation"
603
+ );
604
+ const sourceValues = globalWeightSummary === undefined
605
+ ? validRows.map(row => row[sourceField])
606
+ : globalWeightSummary.positive.map(entry => entry.row[sourceField]);
607
+ const resolvedExtent = resolveExtent(
608
+ extent,
609
+ sourceValues,
610
+ globalWeightSummary,
611
+ sourceField
612
+ );
454
613
  const groupedValues = new Map();
455
- for (const row of validRows) {
614
+ const groupedSource = statisticalWeights === undefined ? validRows : validEntries;
615
+ for (const source of groupedSource) {
616
+ const row = statisticalWeights === undefined ? source : source.row;
456
617
  const group = groupField === undefined ? undefined : row[groupField];
457
618
  const split = placement?.split === undefined
458
619
  ? undefined
459
620
  : row[placement.split.field];
460
621
  const bySplit = groupedValues.get(group) ?? new Map();
461
622
  const groupValues = bySplit.get(split) ?? [];
462
- groupValues.push(row[sourceField]);
623
+ groupValues.push(statisticalWeights === undefined ? row[sourceField] : source);
463
624
  bySplit.set(split, groupValues);
464
625
  groupedValues.set(group, bySplit);
465
626
  }
@@ -492,6 +653,16 @@ export function deriveKernelDensity(values, {
492
653
  outputGroups * steps,
493
654
  "Density generated row count"
494
655
  );
656
+ const profileBandwidths = statisticalWeights !== undefined &&
657
+ bandwidth === "auto" && outputGroups > 1;
658
+ const resolvedBandwidth = profileBandwidths
659
+ ? undefined
660
+ : resolveBandwidth(
661
+ bandwidth,
662
+ sourceValues,
663
+ globalWeightSummary,
664
+ sourceField
665
+ );
495
666
  const extentSpan = resolvedExtent[1] - resolvedExtent[0];
496
667
  const sampleStep = extentSpan / (steps - 1);
497
668
  const samples = Array.from(
@@ -516,10 +687,30 @@ export function deriveKernelDensity(values, {
516
687
  );
517
688
  }
518
689
  const rows = [];
690
+ const resolvedBandwidths = [];
519
691
  for (const group of groups) {
520
692
  for (const split of splits) {
521
693
  const groupValues = groupedValues.get(group).get(split) ?? [];
522
694
  if (groupValues.length === 0) continue;
695
+ const groupWeightSummary = statisticalWeights === undefined
696
+ ? undefined
697
+ : summarizeStatisticalWeights(
698
+ groupValues,
699
+ statisticalWeights.definition.kind,
700
+ "Density group"
701
+ );
702
+ const groupBandwidth = resolvedBandwidth ?? weightedBandwidth(
703
+ groupWeightSummary,
704
+ sourceField,
705
+ "Density group auto bandwidth"
706
+ );
707
+ if (profileBandwidths) {
708
+ resolvedBandwidths.push({
709
+ ...(groupField === undefined ? {} : { group }),
710
+ ...(placement?.split === undefined ? {} : { split }),
711
+ bandwidth: groupBandwidth
712
+ });
713
+ }
523
714
  for (const sample of samples) {
524
715
  rows.push({
525
716
  ...(groupField === undefined
@@ -531,13 +722,22 @@ export function deriveKernelDensity(values, {
531
722
  ? {}
532
723
  : { [placement.split.field]: split }),
533
724
  [outputFields[0]]: sample,
534
- [outputFields[1]]: estimateAt(
535
- sample,
536
- groupValues,
537
- resolvedBandwidth,
538
- resolvedKernel,
539
- resolvedNormalization
540
- )
725
+ [outputFields[1]]: groupWeightSummary === undefined
726
+ ? estimateAt(
727
+ sample,
728
+ groupValues,
729
+ groupBandwidth,
730
+ resolvedKernel,
731
+ resolvedNormalization
732
+ )
733
+ : estimateWeightedAt(
734
+ sample,
735
+ groupWeightSummary,
736
+ sourceField,
737
+ groupBandwidth,
738
+ resolvedKernel,
739
+ resolvedNormalization
740
+ )
541
741
  });
542
742
  }
543
743
  }
@@ -551,7 +751,9 @@ export function deriveKernelDensity(values, {
551
751
  },
552
752
  groups,
553
753
  ...(splitDomain === undefined ? {} : { splitDomain }),
554
- bandwidth: resolvedBandwidth,
754
+ ...(resolvedBandwidth === undefined
755
+ ? { bandwidths: resolvedBandwidths }
756
+ : { bandwidth: resolvedBandwidth }),
555
757
  kernel: resolvedKernel,
556
758
  normalization: resolvedNormalization,
557
759
  extent: resolvedExtent,
@@ -0,0 +1,57 @@
1
+ import { isPlainObject } from "../core/immutable.js";
2
+
3
+ const ENTRY_KEYS = Object.freeze(["value", "label"]);
4
+
5
+ export function sameDisplayLabelValue(left, right) {
6
+ return left === right;
7
+ }
8
+
9
+ function isDisplayLabelValue(value) {
10
+ return value === null || typeof value === "string" ||
11
+ typeof value === "boolean" ||
12
+ (typeof value === "number" && Number.isFinite(value));
13
+ }
14
+
15
+ export function normalizeDisplayLabelMap(value, label = "labelMap") {
16
+ if (!Array.isArray(value)) {
17
+ throw new TypeError(`${label} must be an array.`);
18
+ }
19
+ const normalized = [];
20
+ for (const [index, entry] of value.entries()) {
21
+ if (!isPlainObject(entry)) {
22
+ throw new TypeError(`${label}[${index}] must be a plain object.`);
23
+ }
24
+ const keys = Reflect.ownKeys(entry);
25
+ if (keys.length !== ENTRY_KEYS.length ||
26
+ ENTRY_KEYS.some(key => !Object.hasOwn(entry, key))) {
27
+ throw new Error(`${label}[${index}] must contain exactly value and label.`);
28
+ }
29
+ if (!isDisplayLabelValue(entry.value)) {
30
+ throw new TypeError(
31
+ `${label}[${index}].value must be a string, boolean, null, or finite number.`
32
+ );
33
+ }
34
+ if (typeof entry.label !== "string") {
35
+ throw new TypeError(`${label}[${index}].label must be a string.`);
36
+ }
37
+ if (normalized.some(previous =>
38
+ sameDisplayLabelValue(previous.value, entry.value))) {
39
+ throw new Error(`${label} contains a duplicate typed value.`);
40
+ }
41
+ normalized.push(Object.freeze({ value: entry.value, label: entry.label }));
42
+ }
43
+ return Object.freeze(normalized);
44
+ }
45
+
46
+ export function resolveDisplayLabel(value, map, fallback) {
47
+ if (map !== undefined) {
48
+ for (const entry of map) {
49
+ if (sameDisplayLabelValue(entry.value, value)) return entry.label;
50
+ }
51
+ }
52
+ const resolved = fallback(value);
53
+ if (typeof resolved !== "string") {
54
+ throw new TypeError("Display label fallback must return a string.");
55
+ }
56
+ return resolved;
57
+ }
@@ -1,6 +1,137 @@
1
1
  import { cloneAndFreeze, isPlainObject } from "../../core/immutable.js";
2
2
  import { validateUserId } from "../../core/identifiers.js";
3
3
  import { findTransformPolicy } from "../transforms.js";
4
+ import { BAR_GRAINS, resolveBarGrain } from "../bars/policy.js";
5
+ import { resolveRectMode } from "../rects.js";
6
+ import { resolveRuleMode } from "../rules.js";
7
+
8
+ const CARTESIAN_MARKS = new Set([
9
+ "point", "line", "area", "bar", "rule", "tick", "rect"
10
+ ]);
11
+ const FACET_BAR_GRAINS = new Set([
12
+ BAR_GRAINS.histogram,
13
+ BAR_GRAINS.aggregate,
14
+ BAR_GRAINS.ranged
15
+ ]);
16
+ const ORDINARY_SCALE_CHANNELS = Object.freeze([
17
+ ["x", "x"], ["y", "y"], ["xOffset", "xOffset"], ["yOffset", "yOffset"],
18
+ ["theta", "theta"], ["r", "radius"], ["color", "color"],
19
+ ["stroke", "stroke"], ["size", "size"], ["shape", "shape"],
20
+ ["opacity", "opacity"], ["strokeDash", "strokeDash"]
21
+ ]);
22
+
23
+ function coordinateType(semanticSpec, layer) {
24
+ if (layer.coordinate === undefined) return undefined;
25
+ return semanticSpec.coordinates?.find(value => value.id === layer.coordinate)?.type;
26
+ }
27
+
28
+ function classifyPrimaryLayer(semanticSpec, layer) {
29
+ const type = layer.mark?.type;
30
+ const parallel = layer.encoding?.parallel;
31
+ if (parallel !== undefined || coordinateType(semanticSpec, layer) === "parallel") {
32
+ const complete = type === "line" && Array.isArray(parallel?.dimensions) &&
33
+ parallel.dimensions.length >= 2 &&
34
+ parallel.dimensions.every(dimension =>
35
+ typeof dimension?.scale === "string" && dimension.scale.length > 0
36
+ );
37
+ if (!complete) {
38
+ throw new Error(
39
+ `Facet layer "${layer.id}" must be a complete materializable Parallel mark.`
40
+ );
41
+ }
42
+ return "parallel";
43
+ }
44
+ const polar = layer.encoding?.theta !== undefined ||
45
+ layer.encoding?.radius !== undefined || coordinateType(semanticSpec, layer) === "polar";
46
+ if (polar) {
47
+ const theta = layer.encoding?.theta?.scale !== undefined;
48
+ const radius = layer.encoding?.radius?.scale !== undefined;
49
+ const complete = (type === "point" || type === "line")
50
+ ? theta && radius
51
+ : type === "arc" && theta;
52
+ if (!complete) {
53
+ throw new Error(
54
+ `Facet layer "${layer.id}" must be a complete materializable Polar mark.`
55
+ );
56
+ }
57
+ return "polar";
58
+ }
59
+ if (!CARTESIAN_MARKS.has(type)) {
60
+ throw new Error(
61
+ `facet does not support mark "${layer.id}" of type ${type ?? "incomplete"}.`
62
+ );
63
+ }
64
+ if (type === "bar" && !FACET_BAR_GRAINS.has(resolveBarGrain(layer))) {
65
+ throw new Error(
66
+ `facet requires bar mark "${layer.id}" to be a complete histogram, aggregate, or ranged bar.`
67
+ );
68
+ }
69
+ const complete = (
70
+ layer.encoding?.x?.scale !== undefined &&
71
+ layer.encoding?.y?.scale !== undefined
72
+ ) || (type === "rule" && resolveRuleMode(layer) !== undefined) ||
73
+ (type === "rect" && resolveRectMode(layer) !== undefined);
74
+ if (!complete) {
75
+ throw new Error(
76
+ `Facet layer "${layer.id}" must be a complete materializable Cartesian mark.`
77
+ );
78
+ }
79
+ return "cartesian";
80
+ }
81
+
82
+ export function collectFacetScaleBindings(semanticSpec) {
83
+ if (!isPlainObject(semanticSpec) || !Array.isArray(semanticSpec.layers)) {
84
+ throw new TypeError("Facet scale bindings require a semantic spec with layers.");
85
+ }
86
+ const bindings = [];
87
+ for (const layer of semanticSpec.layers) {
88
+ for (const [policyKey, semanticChannel] of ORDINARY_SCALE_CHANNELS) {
89
+ const scaleId = layer.encoding?.[semanticChannel]?.scale;
90
+ if (scaleId === undefined) continue;
91
+ bindings.push({ layerId: layer.id, scaleId, policyKey, semanticChannel });
92
+ }
93
+ for (const [dimensionIndex, dimension] of
94
+ (layer.encoding?.parallel?.dimensions ?? []).entries()) {
95
+ if (dimension?.scale === undefined) continue;
96
+ bindings.push({
97
+ layerId: layer.id,
98
+ scaleId: dimension.scale,
99
+ policyKey: "parallelDimensions",
100
+ semanticChannel: "parallel",
101
+ dimensionField: dimension.field,
102
+ dimensionIndex
103
+ });
104
+ }
105
+ }
106
+ return cloneAndFreeze(bindings);
107
+ }
108
+
109
+ export function resolveFacetFamily(semanticSpec) {
110
+ if (!isPlainObject(semanticSpec) || !Array.isArray(semanticSpec.layers) ||
111
+ semanticSpec.layers.length === 0) {
112
+ throw new Error("facet requires at least one materializable layer.");
113
+ }
114
+ const primary = semanticSpec.layers.filter(layer =>
115
+ !(layer.mark?.type === "text" && layer.source !== undefined)
116
+ );
117
+ if (primary.length === 0) {
118
+ throw new Error("facet requires at least one primary materializable layer.");
119
+ }
120
+ const families = primary.map(layer => classifyPrimaryLayer(semanticSpec, layer));
121
+ if (new Set(families).size !== 1) {
122
+ throw new Error("facet requires every primary layer to use one coordinate family.");
123
+ }
124
+ const family = families[0];
125
+ return cloneAndFreeze({
126
+ family,
127
+ primaryLayers: primary.map(layer => layer.id),
128
+ dependentLayers: semanticSpec.layers
129
+ .filter(layer => !primary.includes(layer))
130
+ .map(layer => layer.id),
131
+ scaleBindings: collectFacetScaleBindings(semanticSpec),
132
+ coordinates: [...new Set(primary.map(layer => layer.coordinate).filter(Boolean))]
133
+ });
134
+ }
4
135
 
5
136
  function requireCollection(value, label) {
6
137
  if (!Array.isArray(value) || value.length === 0) {
@@ -172,6 +303,7 @@ export function planFacetDependencies(semanticSpec, options = {}) {
172
303
  field,
173
304
  anchor,
174
305
  replay,
175
- layers: layerPaths.map(({ layerId, data }) => ({ id: layerId, data }))
306
+ layers: layerPaths.map(({ layerId, data }) => ({ id: layerId, data })),
307
+ ...(options.family === undefined ? {} : options.family)
176
308
  });
177
309
  }
@@ -3,8 +3,11 @@ import { cloneAndFreeze, isPlainObject } from "../../core/immutable.js";
3
3
  const LEGEND_FAMILIES = Object.freeze({
4
4
  series: "categorical",
5
5
  color: "categorical",
6
+ stroke: "categorical",
6
7
  gradient: "gradient",
7
8
  interval: "discretized",
9
+ strokeGradient: "gradient",
10
+ strokeInterval: "discretized",
8
11
  size: "size",
9
12
  opacity: "opacity"
10
13
  });