ggaction 0.0.11 → 0.0.13

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 (289) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +3 -2
  3. package/knowledge/action-card.schema.json +1 -1
  4. package/knowledge/action-cards.json +1 -1
  5. package/knowledge/action-cards.schema.json +1 -1
  6. package/knowledge/extension-authoring.md +2 -1
  7. package/knowledge/intent-taxonomy.json +1 -1
  8. package/knowledge/mcp-resources.json +1 -1
  9. package/knowledge/task-resolver.js +1 -1
  10. package/package.json +4 -2
  11. package/src/actions/basic.js +2 -0
  12. package/src/actions/boxPlots/materialize.js +2 -1
  13. package/src/actions/canvas/actions.js +3 -1
  14. package/src/actions/canvas/fitting.js +211 -0
  15. package/src/actions/canvas/index.js +2 -0
  16. package/src/actions/categoryOrder/index.js +3 -2
  17. package/src/actions/charts/area.js +47 -0
  18. package/src/actions/charts/bar.js +11 -5
  19. package/src/actions/charts/beeswarm.js +70 -0
  20. package/src/actions/charts/density.js +44 -0
  21. package/src/actions/charts/ecdf.js +183 -0
  22. package/src/actions/charts/endpoints.js +451 -0
  23. package/src/actions/charts/heatmap.js +2 -2
  24. package/src/actions/charts/histogram.js +1 -1
  25. package/src/actions/charts/horizon.js +71 -0
  26. package/src/actions/charts/index.js +37 -0
  27. package/src/actions/charts/interval-regression.js +169 -0
  28. package/src/actions/charts/line.js +5 -5
  29. package/src/actions/charts/parallel.js +1 -1
  30. package/src/actions/charts/pie.js +44 -0
  31. package/src/actions/charts/polar.js +123 -0
  32. package/src/actions/charts/radar.js +299 -0
  33. package/src/actions/charts/radial.js +42 -0
  34. package/src/actions/charts/raincloud.js +422 -0
  35. package/src/actions/charts/rug-strip.js +279 -0
  36. package/src/actions/charts/scatter.js +9 -3
  37. package/src/actions/charts/shared.js +69 -10
  38. package/src/actions/composition/actions.js +118 -1
  39. package/src/actions/data/bin.js +29 -0
  40. package/src/actions/data/computed.js +24 -0
  41. package/src/actions/data/derived.js +66 -0
  42. package/src/actions/data/ecdf.js +38 -0
  43. package/src/actions/data/filter.js +212 -28
  44. package/src/actions/data/fold.js +24 -0
  45. package/src/actions/data/index.js +24 -0
  46. package/src/actions/data/interval.js +2 -0
  47. package/src/actions/data/intervalEdit.js +86 -4
  48. package/src/actions/data/regression.js +5 -2
  49. package/src/actions/data/stack.js +26 -0
  50. package/src/actions/data/summary.js +26 -0
  51. package/src/actions/data/timeUnit.js +1 -1
  52. package/src/actions/encodings/appearance.js +32 -13
  53. package/src/actions/encodings/barWidth.js +8 -0
  54. package/src/actions/encodings/basic.js +2 -0
  55. package/src/actions/encodings/color/continuous.js +19 -59
  56. package/src/actions/encodings/color/index.js +28 -22
  57. package/src/actions/encodings/color/layout.js +3 -97
  58. package/src/actions/encodings/color/policy.js +5 -8
  59. package/src/actions/encodings/density/resolve.js +37 -5
  60. package/src/actions/encodings/density.js +120 -33
  61. package/src/actions/encodings/horizon/resolve.js +8 -4
  62. package/src/actions/encodings/horizon.js +18 -8
  63. package/src/actions/encodings/index.js +2 -0
  64. package/src/actions/encodings/offset.js +33 -27
  65. package/src/actions/encodings/parallel.js +2 -8
  66. package/src/actions/encodings/position/apply.js +40 -4
  67. package/src/actions/encodings/position/index.js +43 -5
  68. package/src/actions/encodings/position/policies/arc.js +37 -2
  69. package/src/actions/encodings/position/policies/area.js +4 -42
  70. package/src/actions/encodings/position/policies/bar.js +48 -13
  71. package/src/actions/encodings/position/resolve.js +79 -24
  72. package/src/actions/encodings/ranged.js +123 -97
  73. package/src/actions/encodings/remove.js +37 -32
  74. package/src/actions/encodings/ruleAppearance.js +23 -9
  75. package/src/actions/encodings/seriesLayout.js +104 -0
  76. package/src/actions/encodings/shared.js +25 -23
  77. package/src/actions/encodings/strokeDash.js +9 -6
  78. package/src/actions/encodings/temporal.js +7 -0
  79. package/src/actions/encodings/text.js +30 -15
  80. package/src/actions/errorBands/create.js +18 -5
  81. package/src/actions/errorBands/edit.js +341 -21
  82. package/src/actions/errorBars/create.js +153 -11
  83. package/src/actions/errorBars/edit.js +365 -19
  84. package/src/actions/facets/actions.js +410 -12
  85. package/src/actions/facets/derive.js +57 -8
  86. package/src/actions/gradientPlots/components.js +33 -11
  87. package/src/actions/gradientPlots/materialize.js +5 -7
  88. package/src/actions/guides/applicability.js +20 -17
  89. package/src/actions/guides/axes/axes.js +4 -3
  90. package/src/actions/guides/axes/axis.js +18 -18
  91. package/src/actions/guides/axes/components.js +43 -0
  92. package/src/actions/guides/axes/edit.js +16 -51
  93. package/src/actions/guides/axes/labels.js +129 -19
  94. package/src/actions/guides/axes/lines.js +7 -5
  95. package/src/actions/guides/axes/parallel/lifecycle.js +117 -0
  96. package/src/actions/guides/axes/parallel/policy.js +137 -0
  97. package/src/actions/guides/axes/parallel/resolve.js +44 -23
  98. package/src/actions/guides/axes/parallel.js +93 -217
  99. package/src/actions/guides/axes/policy.js +21 -67
  100. package/src/actions/guides/axes/tickGroups.js +14 -8
  101. package/src/actions/guides/axes/ticks.js +5 -4
  102. package/src/actions/guides/axes/titles.js +21 -8
  103. package/src/actions/guides/basic.js +2 -0
  104. package/src/actions/guides/facade.js +106 -0
  105. package/src/actions/guides/facadeAxes.js +200 -0
  106. package/src/actions/guides/facadeLegend.js +63 -0
  107. package/src/actions/guides/grids/grid.js +37 -33
  108. package/src/actions/guides/grids/resolve.js +10 -2
  109. package/src/actions/guides/guides.js +35 -28
  110. package/src/actions/guides/legends/categorical/actions.js +193 -249
  111. package/src/actions/guides/legends/categorical/components.js +4 -5
  112. package/src/actions/guides/legends/categorical/layout.js +36 -349
  113. package/src/actions/guides/legends/categorical/options.js +30 -14
  114. package/src/actions/guides/legends/categorical/recipes.js +14 -0
  115. package/src/actions/guides/legends/categorical/resolve.js +21 -10
  116. package/src/actions/guides/legends/categorical/symbols.js +2 -3
  117. package/src/actions/guides/legends/continuous/common.js +69 -14
  118. package/src/actions/guides/legends/continuous/gradient.js +111 -83
  119. package/src/actions/guides/legends/continuous/index.js +6 -2
  120. package/src/actions/guides/legends/continuous/interval.js +71 -92
  121. package/src/actions/guides/legends/continuous/opacity.js +105 -84
  122. package/src/actions/guides/legends/creation.js +27 -0
  123. package/src/actions/guides/legends/edit.js +198 -66
  124. package/src/actions/guides/legends/focused.js +2 -2
  125. package/src/actions/guides/legends/lane.js +98 -86
  126. package/src/actions/guides/legends/lifecycle.js +132 -0
  127. package/src/actions/guides/legends/remove.js +31 -76
  128. package/src/actions/guides/legends/size.js +149 -105
  129. package/src/actions/guides/legends/strokeWidth.js +106 -64
  130. package/src/actions/guides/legends/target.js +12 -0
  131. package/src/actions/guides/legends/transition.js +43 -0
  132. package/src/actions/guides/polar/axes/facade.js +77 -62
  133. package/src/actions/guides/polar/axes/labels.js +5 -4
  134. package/src/actions/guides/polar/axes/lines.js +2 -1
  135. package/src/actions/guides/polar/axes/shared.js +6 -0
  136. package/src/actions/guides/polar/axes/ticks.js +5 -4
  137. package/src/actions/guides/polar/axes/titles.js +5 -4
  138. package/src/actions/guides/polar/grids.js +7 -7
  139. package/src/actions/guides/reuse.js +44 -0
  140. package/src/actions/guides/tickValues.js +19 -21
  141. package/src/actions/index.js +2 -0
  142. package/src/actions/intervals/resolve.js +35 -15
  143. package/src/actions/marks/arc/actions.js +1 -1
  144. package/src/actions/marks/area/actions.js +18 -4
  145. package/src/actions/marks/area/materialize.js +5 -2
  146. package/src/actions/marks/bar/materialize.js +11 -7
  147. package/src/actions/marks/index.js +5 -0
  148. package/src/actions/marks/line/actions.js +43 -108
  149. package/src/actions/marks/line/materialize.js +34 -52
  150. package/src/actions/marks/point/edit.js +11 -15
  151. package/src/actions/marks/point/index.js +1 -0
  152. package/src/actions/marks/point/jitter.js +3 -0
  153. package/src/actions/marks/point/materialize.js +138 -14
  154. package/src/actions/marks/point/packing.js +68 -0
  155. package/src/actions/marks/references.js +87 -0
  156. package/src/actions/marks/remove.js +17 -4
  157. package/src/actions/marks/rule/actions.js +60 -4
  158. package/src/actions/marks/rule/appearance.js +26 -0
  159. package/src/actions/marks/shared.js +12 -12
  160. package/src/actions/marks/text/actions.js +224 -6
  161. package/src/actions/marks/text/layout.js +2 -2
  162. package/src/actions/primitives/createGraphics.js +3 -1
  163. package/src/actions/primitives/semantic.js +1 -1
  164. package/src/actions/primitives/semanticAction.js +39 -2
  165. package/src/actions/primitives/semanticValidation/guide.js +10 -0
  166. package/src/actions/primitives/semanticValidation/layer.js +18 -1
  167. package/src/actions/primitives/semanticValidation/scale.js +10 -1
  168. package/src/actions/regression/create.js +1 -1
  169. package/src/actions/regression/edit.js +19 -6
  170. package/src/actions/regression/resolve.js +1 -1
  171. package/src/actions/scales/consumers/common.js +20 -53
  172. package/src/actions/scales/consumers/families.js +27 -14
  173. package/src/actions/scales/consumers/index.js +23 -2
  174. package/src/actions/scales/consumers/seriesLayout.js +33 -2
  175. package/src/actions/scales/create.js +4 -4
  176. package/src/actions/scales/definitions.js +19 -3
  177. package/src/actions/scales/edit.js +46 -34
  178. package/src/actions/scales/editPolicy.js +21 -31
  179. package/src/actions/scales/materialize.js +10 -46
  180. package/src/actions/scales/patch.js +1 -1
  181. package/src/actions/scales/preview.js +39 -0
  182. package/src/actions/scales/quantitativeColor.js +9 -5
  183. package/src/actions/theme/actions.js +43 -0
  184. package/src/actions/theme/index.js +9 -0
  185. package/src/actions/theme/reconcile.js +882 -0
  186. package/src/actions/titles/actions.js +11 -10
  187. package/src/actions/titles/resolve.js +1 -40
  188. package/src/actions/violinPlots/create.js +86 -42
  189. package/src/actions/violinPlots/edit.js +191 -0
  190. package/src/actions/violinPlots/index.js +2 -0
  191. package/src/core/ChartProgram.js +7 -0
  192. package/src/core/action.js +22 -2
  193. package/src/core/compositionState.js +136 -8
  194. package/src/core/vocabulary.js +2 -2
  195. package/src/grammar/aggregate.js +66 -23
  196. package/src/grammar/arcs.js +213 -65
  197. package/src/grammar/areaEndpoints.js +46 -0
  198. package/src/grammar/areaSeries.js +87 -41
  199. package/src/grammar/bars/aggregate.js +33 -12
  200. package/src/grammar/bars/policy.js +9 -7
  201. package/src/grammar/bin.js +181 -0
  202. package/src/grammar/categoryOrder.js +34 -0
  203. package/src/grammar/computed.js +150 -0
  204. package/src/grammar/ecdf.js +198 -0
  205. package/src/grammar/facets/index.js +117 -6
  206. package/src/grammar/fold.js +123 -0
  207. package/src/grammar/horizon.js +6 -4
  208. package/src/grammar/interval.js +37 -20
  209. package/src/grammar/lineSeries.js +20 -63
  210. package/src/grammar/markFilter.js +22 -4
  211. package/src/grammar/markLabels.js +93 -0
  212. package/src/grammar/parallelAxisTitles.js +16 -0
  213. package/src/grammar/pathSeries.js +91 -0
  214. package/src/grammar/pointPacking.js +183 -0
  215. package/src/grammar/positionCompatibility.js +1 -1
  216. package/src/grammar/positionDatum.js +12 -0
  217. package/src/grammar/rects.js +9 -1
  218. package/src/grammar/regression/derive.js +4 -0
  219. package/src/grammar/regression/index.js +7 -2
  220. package/src/grammar/regression/models.js +22 -7
  221. package/src/grammar/regression/parameters.js +40 -13
  222. package/src/grammar/rotation.js +29 -0
  223. package/src/grammar/rules.js +9 -21
  224. package/src/grammar/scales/color.js +23 -1
  225. package/src/grammar/scales/colorConsumers.js +38 -0
  226. package/src/grammar/scales/definition.js +15 -1
  227. package/src/grammar/scales/discretized.js +11 -5
  228. package/src/grammar/scales/fields.js +38 -8
  229. package/src/grammar/scales/mapping.js +2 -0
  230. package/src/grammar/scales/ordinal.js +23 -0
  231. package/src/grammar/scales/radial.js +55 -0
  232. package/src/grammar/scales/types.js +1 -0
  233. package/src/grammar/schemas/semanticPath.js +17 -3
  234. package/src/grammar/stack.js +170 -0
  235. package/src/grammar/statistics/confidenceInterval.js +101 -0
  236. package/src/grammar/summary.js +183 -0
  237. package/src/grammar/text.js +22 -16
  238. package/src/grammar/timeUnit.js +21 -11
  239. package/src/grammar/transformTopology.js +6 -0
  240. package/src/grammar/transforms.js +36 -0
  241. package/src/grammar/valueFormat.js +112 -0
  242. package/src/grammar/window.js +13 -7
  243. package/src/layout/facets.js +86 -23
  244. package/src/layout/guideCollisions.js +12 -0
  245. package/src/layout/legend.js +2 -10
  246. package/src/layout/legendItems.js +102 -0
  247. package/src/layout/legendLane.js +100 -11
  248. package/src/materialization/bars/aggregate.js +38 -152
  249. package/src/materialization/bars/histogram.js +24 -0
  250. package/src/materialization/categorySlotOffset.js +40 -0
  251. package/src/materialization/compositionSnapshot.js +1 -1
  252. package/src/materialization/dependencies.js +57 -16
  253. package/src/materialization/encodings.js +1 -1
  254. package/src/materialization/facetGuides/legacyCategorical.js +7 -1
  255. package/src/materialization/facetGuides/placement.js +16 -1
  256. package/src/materialization/facetGuides/preparation.js +26 -1
  257. package/src/materialization/facets.js +11 -2
  258. package/src/materialization/guides/layout.js +57 -0
  259. package/src/materialization/guides/resources.js +2 -2
  260. package/src/materialization/legends.js +10 -1
  261. package/src/materialization/marks/capabilities.js +30 -7
  262. package/src/materialization/marks/grouping.js +52 -0
  263. package/src/materialization/marks/index.js +63 -25
  264. package/src/materialization/marks/policies.js +2 -0
  265. package/src/materialization/rect.js +33 -33
  266. package/src/materialization/rowEncoding.js +70 -14
  267. package/src/materialization/scaleGuideDependencies.js +8 -5
  268. package/src/materialization/scales/map.js +1 -0
  269. package/src/materialization/scales/policies/arc.js +42 -0
  270. package/src/materialization/scales/policies/bar.js +0 -43
  271. package/src/materialization/scales/policies/binnedPosition.js +7 -4
  272. package/src/materialization/scales/policies/offset.js +54 -0
  273. package/src/materialization/scales/policies/series.js +2 -1
  274. package/src/materialization/scales/resolve.js +48 -18
  275. package/src/materialization/selection/compatibility.js +11 -0
  276. package/src/materialization/selection/items/bar.js +12 -68
  277. package/src/materialization/selection/items/common.js +8 -2
  278. package/src/materialization/selection/items/path.js +3 -13
  279. package/src/materialization/selection/items/rect.js +7 -4
  280. package/src/materialization/text.js +64 -5
  281. package/src/mcp/adapter.js +2 -2
  282. package/src/selectors/datasets.js +13 -0
  283. package/src/selectors/layers.js +3 -2
  284. package/src/selectors/scales.js +6 -0
  285. package/src/theme/defaults.js +39 -0
  286. package/types/basic.d.ts +11 -2
  287. package/types/index.d.ts +126 -0
  288. package/types/program.d.ts +1327 -124
  289. package/src/materialization/bars/grouped.js +0 -144
@@ -1,11 +1,15 @@
1
1
  import { isPlainObject } from "../core/immutable.js";
2
2
  import { validateBoxTransform } from "./boxPlot.js";
3
+ import { validateBinTransform } from "./bin.js";
3
4
  import {
4
5
  requestedBin2DTransform,
5
6
  validateBin2DTransform
6
7
  } from "./bin2d.js";
7
8
  import { validateDensityTransform } from "./density.js";
9
+ import { validateECDFTransform } from "./ecdf.js";
10
+ import { validateComputedTransform } from "./computed.js";
8
11
  import { validateFilterTransform } from "./filter.js";
12
+ import { validateFoldTransform } from "./fold.js";
9
13
  import {
10
14
  requestedGradientProfileTransform,
11
15
  validateGradientProfileTransform
@@ -19,6 +23,8 @@ import { validateMarkFilterTransform } from "./markFilter.js";
19
23
  import { validateRegressionTransform } from "./regression/index.js";
20
24
  import { validateWindowTransform } from "./window.js";
21
25
  import { validateTimeUnitTransform } from "./timeUnit.js";
26
+ import { validateSummaryTransform } from "./summary.js";
27
+ import { validateStackTransform } from "./stack.js";
22
28
  import { findTransformTopology } from "./transformTopology.js";
23
29
 
24
30
  function requestedDensityTransform(transform) {
@@ -44,12 +50,22 @@ function facetHorizonTransform(transform, { scales = {} } = {}) {
44
50
  }
45
51
 
46
52
  const TRANSFORM_POLICIES = Object.freeze({
53
+ bin: Object.freeze({
54
+ ...findTransformTopology("bin"),
55
+ validate: validateBinTransform,
56
+ materializeOp: "materializeBinData"
57
+ }),
47
58
  bin2d: Object.freeze({
48
59
  ...findTransformTopology("bin2d"),
49
60
  validate: validateBin2DTransform,
50
61
  materializeOp: "materializeBin2DData",
51
62
  replayTransform: requestedBin2DTransform
52
63
  }),
64
+ computed: Object.freeze({
65
+ ...findTransformTopology("computed"),
66
+ validate: validateComputedTransform,
67
+ materializeOp: "materializeComputedData"
68
+ }),
53
69
  boxOutlier: Object.freeze({
54
70
  ...findTransformTopology("boxOutlier"),
55
71
  validate: validateBoxTransform,
@@ -66,11 +82,21 @@ const TRANSFORM_POLICIES = Object.freeze({
66
82
  materializeOp: "materializeDensityData",
67
83
  replayTransform: requestedDensityTransform
68
84
  }),
85
+ ecdf: Object.freeze({
86
+ ...findTransformTopology("ecdf"),
87
+ validate: validateECDFTransform,
88
+ materializeOp: "materializeECDFData"
89
+ }),
69
90
  filter: Object.freeze({
70
91
  ...findTransformTopology("filter"),
71
92
  validate: validateFilterTransform,
72
93
  materializeOp: "materializeFilteredData"
73
94
  }),
95
+ fold: Object.freeze({
96
+ ...findTransformTopology("fold"),
97
+ validate: validateFoldTransform,
98
+ materializeOp: "materializeFoldData"
99
+ }),
74
100
  gradientProfile: Object.freeze({
75
101
  ...findTransformTopology("gradientProfile"),
76
102
  validate: validateGradientProfileTransform,
@@ -99,6 +125,16 @@ const TRANSFORM_POLICIES = Object.freeze({
99
125
  validate: validateRegressionTransform,
100
126
  materializeOp: "materializeRegressionData"
101
127
  }),
128
+ summary: Object.freeze({
129
+ ...findTransformTopology("summary"),
130
+ validate: validateSummaryTransform,
131
+ materializeOp: "materializeSummaryData"
132
+ }),
133
+ stack: Object.freeze({
134
+ ...findTransformTopology("stack"),
135
+ validate: validateStackTransform,
136
+ materializeOp: "materializeStackData"
137
+ }),
102
138
  timeUnit: Object.freeze({
103
139
  ...findTransformTopology("timeUnit"),
104
140
  validate: validateTimeUnitTransform,
@@ -0,0 +1,112 @@
1
+ const NUMERIC_FORMAT = /^\.(0?\d|1[0-2])(f|%|e)$/;
2
+ const TIME_DIRECTIVES = new Set(["Y", "m", "d", "b", "%"]);
3
+ const MONTH_NAMES = Object.freeze([
4
+ "Jan", "Feb", "Mar", "Apr", "May", "Jun",
5
+ "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
6
+ ]);
7
+
8
+ export function isUtcValueFormat(format) {
9
+ if (typeof format !== "string") return false;
10
+ let directives = 0;
11
+ for (let index = 0; index < format.length; index += 1) {
12
+ if (format[index] !== "%") continue;
13
+ const directive = format[index + 1];
14
+ if (!TIME_DIRECTIVES.has(directive)) return false;
15
+ if (directive !== "%") directives += 1;
16
+ index += 1;
17
+ }
18
+ return directives > 0;
19
+ }
20
+
21
+ export function validateValueFormat(format, label = "Label format", {
22
+ allowDecimalsObject = false
23
+ } = {}) {
24
+ if (format === undefined || format === "auto") return format ?? "auto";
25
+ if (
26
+ format !== null &&
27
+ typeof format === "object" &&
28
+ Number.isInteger(format.decimals) &&
29
+ format.decimals >= 0 &&
30
+ format.decimals <= 100 &&
31
+ Object.keys(format).length === 1
32
+ ) {
33
+ if (allowDecimalsObject) return format;
34
+ }
35
+ if (typeof format === "string" && (
36
+ NUMERIC_FORMAT.test(format) || isUtcValueFormat(format)
37
+ )) {
38
+ return format;
39
+ }
40
+ const numericCandidate = typeof format === "string"
41
+ ? format.match(/^\.(\d+)(f|%|e)$/)
42
+ : undefined;
43
+ if (numericCandidate && Number(numericCandidate[1]) > 12) {
44
+ throw new RangeError(`${label} numeric format supports at most 12 decimals.`);
45
+ }
46
+ throw new TypeError(
47
+ `${label} must be auto${allowDecimalsObject ? ", { decimals }," : ","} or a supported format string (numeric or UTC).`
48
+ );
49
+ }
50
+
51
+ function pad(value, length = 2) {
52
+ return String(value).padStart(length, "0");
53
+ }
54
+
55
+ function formatUtcValue(value, format, label) {
56
+ const date = new Date(value);
57
+ if (!Number.isFinite(date.getTime())) {
58
+ throw new TypeError(`${label} requires a valid date or timestamp.`);
59
+ }
60
+ const values = {
61
+ Y: pad(date.getUTCFullYear(), 4),
62
+ m: pad(date.getUTCMonth() + 1),
63
+ d: pad(date.getUTCDate()),
64
+ b: MONTH_NAMES[date.getUTCMonth()]
65
+ };
66
+ return format.replace(/%([Ymdb%])/g, (_, directive) =>
67
+ directive === "%" ? "%" : values[directive]
68
+ );
69
+ }
70
+
71
+ export function formatValue(value, {
72
+ format = "auto",
73
+ valueType,
74
+ autoFormatter = current => String(current),
75
+ label = "Label format",
76
+ allowDecimalsObject = false
77
+ } = {}) {
78
+ const resolved = validateValueFormat(format, label, {
79
+ allowDecimalsObject
80
+ });
81
+ if (value === undefined || value === null) return undefined;
82
+ if (resolved === "auto") return autoFormatter(value);
83
+ if (["nominal", "ordinal", "discrete"].includes(valueType)) {
84
+ throw new Error('Discrete labels require format "auto".');
85
+ }
86
+ const utc = isUtcValueFormat(resolved);
87
+ if (valueType === "temporal") {
88
+ if (!utc) throw new Error("Temporal labels require a supported UTC format string.");
89
+ return formatUtcValue(value, resolved, label);
90
+ }
91
+ if (utc) {
92
+ if (valueType === "quantitative") {
93
+ throw new Error("Quantitative labels cannot use a UTC format string.");
94
+ }
95
+ return formatUtcValue(value, resolved, label);
96
+ }
97
+ if (!Number.isFinite(value)) {
98
+ throw new TypeError(`${label} "${typeof resolved === "string" ? resolved : "decimals"}" requires a finite number.`);
99
+ }
100
+ if (typeof resolved === "object") return value.toFixed(resolved.decimals);
101
+ const match = resolved.match(NUMERIC_FORMAT);
102
+ const precision = Number(match[1]);
103
+ if (match[2] === "f") return value.toFixed(precision);
104
+ if (match[2] === "%") {
105
+ const percentage = value * 100;
106
+ if (!Number.isFinite(percentage)) {
107
+ throw new RangeError(`${label} percent format overflow.`);
108
+ }
109
+ return `${percentage.toFixed(precision)}%`;
110
+ }
111
+ return value.toExponential(precision);
112
+ }
@@ -336,10 +336,16 @@ function stableWindowOutput(operation, calculate) {
336
336
  }
337
337
  }
338
338
 
339
+ function setWindowOutput(entry, field, value) {
340
+ Object.defineProperty(entry.row, field, {
341
+ value, enumerable: true, writable: true, configurable: true
342
+ });
343
+ }
344
+
339
345
  function applyOperation(partition, operation, sortBy) {
340
346
  if (operation.op === "rowNumber") {
341
347
  partition.forEach((entry, index) => {
342
- entry.row[operation.as] = index + 1;
348
+ setWindowOutput(entry, operation.as, index + 1);
343
349
  });
344
350
  return;
345
351
  }
@@ -354,7 +360,7 @@ function applyOperation(partition, operation, sortBy) {
354
360
  rank = index + 1;
355
361
  denseRank += 1;
356
362
  }
357
- entry.row[operation.as] = operation.op === "rank" ? rank : denseRank;
363
+ setWindowOutput(entry, operation.as, operation.op === "rank" ? rank : denseRank);
358
364
  });
359
365
  return;
360
366
  }
@@ -364,7 +370,7 @@ function applyOperation(partition, operation, sortBy) {
364
370
  stableFinitePrefixSums(values, label)
365
371
  );
366
372
  partition.forEach((entry, index) => {
367
- entry.row[operation.as] = totals[index];
373
+ setWindowOutput(entry, operation.as, totals[index]);
368
374
  });
369
375
  return;
370
376
  }
@@ -379,18 +385,18 @@ function applyOperation(partition, operation, sortBy) {
379
385
  index + operation.frame.following
380
386
  );
381
387
  const values = operationValues(partition, operation, first, last);
382
- entry.row[operation.as] = stableWindowOutput(operation, label =>
388
+ setWindowOutput(entry, operation.as, stableWindowOutput(operation, label =>
383
389
  calculate(values, label)
384
- );
390
+ ));
385
391
  });
386
392
  return;
387
393
  }
388
394
  const direction = operation.op === "lag" ? -1 : 1;
389
395
  partition.forEach((entry, index) => {
390
396
  const peer = partition[index + direction * operation.offset];
391
- entry.row[operation.as] = peer === undefined
397
+ setWindowOutput(entry, operation.as, peer === undefined
392
398
  ? operation.default
393
- : peer.row[operation.field];
399
+ : peer.row[operation.field]);
394
400
  });
395
401
  }
396
402
 
@@ -9,6 +9,11 @@ import {
9
9
 
10
10
  export const DEFAULT_FACET_LEGEND_GAP = 18;
11
11
  export const DEFAULT_FACET_LEGEND_WIDTH = 132;
12
+ export const DEFAULT_FACET_LEGEND_HEIGHT = 0;
13
+
14
+ const FACET_LEGEND_POSITIONS = Object.freeze([
15
+ "right", "left", "top", "bottom"
16
+ ]);
12
17
 
13
18
  function resolveColumns(columns, count) {
14
19
  const value = columns ?? count;
@@ -18,6 +23,29 @@ function resolveColumns(columns, count) {
18
23
  return Math.min(value, count);
19
24
  }
20
25
 
26
+ function resolveGridCoordinates(children, columns) {
27
+ const requested = children.some(child => child.row !== undefined || child.column !== undefined);
28
+ if (!requested) return undefined;
29
+ if (children.some(child => !(
30
+ Number.isInteger(child.row) && child.row >= 0 &&
31
+ Number.isInteger(child.column) && child.column >= 0
32
+ ))) {
33
+ throw new RangeError("Facet grid children require non-negative integer row and column coordinates.");
34
+ }
35
+ const keys = children.map(child => `${child.row}:${child.column}`);
36
+ if (new Set(keys).size !== keys.length) {
37
+ throw new Error("Facet grid children must occupy unique row and column coordinates.");
38
+ }
39
+ const requiredColumns = Math.max(...children.map(child => child.column)) + 1;
40
+ if (columns !== undefined && columns !== requiredColumns) {
41
+ throw new RangeError("Facet grid columns must match the declared column domain.");
42
+ }
43
+ return {
44
+ columns: requiredColumns,
45
+ rows: Math.max(...children.map(child => child.row)) + 1
46
+ };
47
+ }
48
+
21
49
  function alignedOffset(remaining, align) {
22
50
  if (align === "start") return 0;
23
51
  if (align === "end") return remaining;
@@ -33,13 +61,17 @@ export function resolveFacetLayout({
33
61
  titleHeight = 0,
34
62
  sharedLegend = false,
35
63
  sharedLegendGap = DEFAULT_FACET_LEGEND_GAP,
36
- sharedLegendWidth = DEFAULT_FACET_LEGEND_WIDTH
64
+ sharedLegendWidth = DEFAULT_FACET_LEGEND_WIDTH,
65
+ sharedLegendHeight = DEFAULT_FACET_LEGEND_HEIGHT,
66
+ sharedLegendPosition = "right"
37
67
  } = {}) {
38
68
  const values = children?.map(child => child?.value);
69
+ const coordinates = resolveGridCoordinates(children ?? [], columns);
39
70
  const resolvedChildren = normalizeCompositionChildren(children?.map(
40
- ({ value: _value, ...child }) => child
71
+ ({ value: _value, row: _row, column: _column, ...child }) => child
41
72
  ));
42
- const resolvedColumns = resolveColumns(columns, resolvedChildren.length);
73
+ const resolvedColumns = coordinates?.columns ??
74
+ resolveColumns(columns, resolvedChildren.length);
43
75
  const resolvedGap = validateCompositionSpacing(gap, "Facet gap");
44
76
  const resolvedAlign = normalizeCompositionAlign(align);
45
77
  const resolvedPadding = normalizeCompositionPadding(padding);
@@ -58,12 +90,31 @@ export function resolveFacetLayout({
58
90
  sharedLegendWidth,
59
91
  "Facet shared legend width"
60
92
  );
61
- const rowCount = Math.ceil(resolvedChildren.length / resolvedColumns);
93
+ const legendHeight = validateCompositionSpacing(
94
+ sharedLegendHeight,
95
+ "Facet shared legend height"
96
+ );
97
+ if (!FACET_LEGEND_POSITIONS.includes(sharedLegendPosition)) {
98
+ throw new Error(
99
+ `Unknown facet shared legend position "${sharedLegendPosition}".`
100
+ );
101
+ }
102
+ const sideLegend = ["right", "left"].includes(sharedLegendPosition);
103
+ const beforeGrid = ["left", "top"].includes(sharedLegendPosition);
104
+ const legendLane = sharedLegend
105
+ ? legendGap + (sideLegend ? legendWidth : legendHeight)
106
+ : 0;
107
+ const rowCount = coordinates?.rows ??
108
+ Math.ceil(resolvedChildren.length / resolvedColumns);
62
109
  const columnWidths = Array(resolvedColumns).fill(-Infinity);
63
110
  const rowHeights = Array(rowCount).fill(-Infinity);
64
111
  resolvedChildren.forEach((child, index) => {
65
- const column = index % resolvedColumns;
66
- const row = Math.floor(index / resolvedColumns);
112
+ const column = coordinates === undefined
113
+ ? index % resolvedColumns
114
+ : children[index].column;
115
+ const row = coordinates === undefined
116
+ ? Math.floor(index / resolvedColumns)
117
+ : children[index].row;
67
118
  columnWidths[column] = Math.max(columnWidths[column], child.width);
68
119
  rowHeights[row] = Math.max(rowHeights[row], child.height);
69
120
  });
@@ -76,21 +127,27 @@ export function resolveFacetLayout({
76
127
  .reduce((sum, height) => sum + height, 0) + resolvedGap * row
77
128
  );
78
129
  const placements = resolvedChildren.map((child, index) => {
79
- const column = index % resolvedColumns;
80
- const row = Math.floor(index / resolvedColumns);
130
+ const column = coordinates === undefined
131
+ ? index % resolvedColumns
132
+ : children[index].column;
133
+ const row = coordinates === undefined
134
+ ? Math.floor(index / resolvedColumns)
135
+ : children[index].row;
81
136
  return {
82
137
  id: child.id,
83
138
  value: values[index],
84
139
  column,
85
140
  row,
86
- x: columnStarts[column] + alignedOffset(
87
- columnWidths[column] - child.width,
88
- resolvedAlign
89
- ),
90
- y: rowStarts[row] + alignedOffset(
91
- rowHeights[row] - child.height,
92
- resolvedAlign
93
- ),
141
+ x: columnStarts[column] + (beforeGrid && sideLegend ? legendLane : 0) +
142
+ alignedOffset(
143
+ columnWidths[column] - child.width,
144
+ resolvedAlign
145
+ ),
146
+ y: rowStarts[row] + (beforeGrid && !sideLegend ? legendLane : 0) +
147
+ alignedOffset(
148
+ rowHeights[row] - child.height,
149
+ resolvedAlign
150
+ ),
94
151
  width: child.width,
95
152
  height: child.height
96
153
  };
@@ -111,16 +168,22 @@ export function resolveFacetLayout({
111
168
  padding: resolvedPadding,
112
169
  titleHeight: resolvedTitleHeight,
113
170
  gridWidth,
114
- width: gridWidth + (sharedLegend
115
- ? legendGap + legendWidth
116
- : 0),
117
- height: gridHeight,
171
+ width: gridWidth + (sideLegend ? legendLane : 0),
172
+ height: gridHeight + (sideLegend ? 0 : legendLane),
118
173
  children: placements,
119
174
  ...(sharedLegend ? {
120
175
  legend: {
121
- x: gridWidth + legendGap,
122
- y: resolvedTitleHeight + resolvedPadding.top + 30,
123
- width: legendWidth
176
+ position: sharedLegendPosition,
177
+ x: sharedLegendPosition === "right"
178
+ ? gridWidth + legendGap
179
+ : 0,
180
+ y: sharedLegendPosition === "bottom"
181
+ ? gridHeight + legendGap
182
+ : sharedLegendPosition === "top"
183
+ ? resolvedTitleHeight
184
+ : resolvedTitleHeight + resolvedPadding.top + 30,
185
+ width: legendWidth,
186
+ height: legendHeight
124
187
  }
125
188
  } : {})
126
189
  });
@@ -0,0 +1,12 @@
1
+ export function assertGuideCollisionBlocks(blocks) {
2
+ for (let index = 0; index < blocks.length; index += 1) {
3
+ const a = blocks[index];
4
+ for (const b of blocks.slice(index + 1)) {
5
+ if (a.position !== b.position || a.kind === "axis" && b.kind === "axis") continue;
6
+ if (a.bounds.left < b.bounds.right && a.bounds.right > b.bounds.left &&
7
+ a.bounds.top < b.bounds.bottom && a.bounds.bottom > b.bounds.top) {
8
+ throw new Error(`The ${a.position} ${a.id} and ${b.id} overlap; guides require more margin space or different offsets.`);
9
+ }
10
+ }
11
+ }
12
+ }
@@ -3,19 +3,11 @@ import {
3
3
  measureTextWidth
4
4
  } from "../core/textMetrics.js";
5
5
 
6
- export function measureLegendSymbolHeight(config) {
7
- return Math.max(...config.symbol.layers.map(layer => {
8
- if (layer.type === "swatch") return layer.height;
9
- if (layer.type === "point") return layer.size * 2;
10
- return layer.lineWidth;
11
- }));
12
- }
13
-
14
6
  export function measureLegendTextWidth(value, style = { fontSize: 12 }) {
15
7
  return measureTextWidth(formatVisibleText(value), style);
16
8
  }
17
9
 
18
- export function resolveLegendGrid(config, width, count) {
10
+ export function resolveLegendGrid(config, width, count, symbolHeight) {
19
11
  const labels = config.domain.map(formatVisibleText);
20
12
  const itemWidths = labels.map(
21
13
  label => width + config.labels.offset +
@@ -39,7 +31,7 @@ export function resolveLegendGrid(config, width, count) {
39
31
  config.itemGap * Math.max(0, actualColumns - 1);
40
32
  const rowHeight = Math.max(
41
33
  config.labels.fontSize,
42
- measureLegendSymbolHeight(config)
34
+ symbolHeight
43
35
  );
44
36
  const gridHeight = rowHeight * rowCount +
45
37
  config.itemGap * Math.max(0, rowCount - 1);
@@ -0,0 +1,102 @@
1
+ import { measureTextWidth, resolveTextBounds } from "../core/textMetrics.js";
2
+ import { alignLegendStart, resolveLegendGrid } from "./legend.js";
3
+
4
+ // Measure item content before choosing its edge. Family owners supply the
5
+ // sample dimensions and formatted labels; this module does not inspect scales.
6
+ export function resolveLegendItemLayout(plot, config, labels, symbol, canvas) {
7
+ const strokes = labels.map((_, index) => Array.isArray(symbol.strokeWidth)
8
+ ? symbol.strokeWidth[index] : symbol.strokeWidth ?? 0);
9
+ const sampleBounds = labels.map((_, index) => symbol.itemBounds?.[index] ?? {
10
+ left: -strokes[index] / 2, right: symbol.width + strokes[index] / 2,
11
+ top: -symbol.height / 2 - strokes[index] / 2,
12
+ bottom: symbol.height / 2 + strokes[index] / 2
13
+ });
14
+ // Retain a family's minimum slot while reserving its complete painted extent.
15
+ const sampleLeft = Math.min(0, ...sampleBounds.map(bounds => bounds.left));
16
+ const sampleRight = Math.max(symbol.width, ...sampleBounds.map(bounds => bounds.right));
17
+ const sampleWidth = sampleRight - sampleLeft;
18
+ const sampleHeight = Math.max(symbol.height,
19
+ ...sampleBounds.map(bounds => 2 * Math.max(-bounds.top, bounds.bottom)));
20
+ const side = ["left", "right"].includes(config.position);
21
+ const titleVisible = config.titleVisible !== false;
22
+ const titleWidth = titleVisible ? measureTextWidth(config.title, config.titleStyle) : 0;
23
+ const titleHeight = titleVisible ? config.titleStyle.fontSize : 0;
24
+ const itemWidth = sampleWidth + config.labels.offset + Math.max(...labels.map(
25
+ text => measureTextWidth(text, config.labels)
26
+ ));
27
+ let symbolX;
28
+ let labelX;
29
+ let itemY;
30
+ let title;
31
+ if (config.layout === "legacy-bottom") {
32
+ const widths = labels.map(text => sampleWidth + config.labels.offset + measureTextWidth(text, config.labels));
33
+ const width = widths.reduce((sum, value) => sum + value, 0) + config.itemGap * (labels.length - 1);
34
+ const x = config.align === "center" ? (canvas.width - width) / 2 : alignLegendStart(plot, width, config.align);
35
+ let cursor = x;
36
+ symbolX = widths.map(width => {
37
+ const start = cursor - sampleLeft;
38
+ cursor += width + config.itemGap;
39
+ return start;
40
+ });
41
+ labelX = symbolX.map(value => value + sampleRight + config.labels.offset);
42
+ itemY = labels.map(() => canvas.height - 28);
43
+ title = { x: x + width / 2, y: canvas.height - 52, align: "center" };
44
+ } else if (side) {
45
+ const width = Math.max(itemWidth, titleWidth);
46
+ const x = config.position === "right" ? plot.x + plot.width + config.offset
47
+ : plot.x - config.offset - width;
48
+ const itemHeight = Math.max(sampleHeight, config.labels.fontSize);
49
+ const pitch = Math.max(config.itemGap, itemHeight);
50
+ symbolX = labels.map(() => x - sampleLeft);
51
+ labelX = labels.map(() => x + sampleWidth + config.labels.offset);
52
+ const firstY = Math.max(plot.y + 52, titleVisible
53
+ ? plot.y + 20 + titleHeight / 2 + 12 + itemHeight / 2 : plot.y + 52);
54
+ itemY = labels.map((_, index) => firstY + index * pitch);
55
+ title = { x, y: plot.y + 20, align: "left" };
56
+ } else {
57
+ const grid = resolveLegendGrid({ ...config, domain: labels }, sampleWidth, labels.length, sampleHeight);
58
+ const inline = config.titlePosition === "left" && titleVisible;
59
+ const prefix = inline ? titleWidth + 20 : 0;
60
+ const width = inline ? prefix + grid.gridWidth : Math.max(titleWidth, grid.gridWidth);
61
+ const top = !inline && titleVisible ? titleHeight + 12 : 0;
62
+ const height = inline ? Math.max(titleHeight, grid.gridHeight) : top + grid.gridHeight;
63
+ const x = alignLegendStart(plot, width, config.align);
64
+ const y = config.position === "top" ? plot.y - config.offset - height
65
+ : plot.y + plot.height + config.offset;
66
+ const gridX = inline ? x + prefix : x + (width - grid.gridWidth) / 2;
67
+ const gridY = inline ? y + (height - grid.gridHeight) / 2 : y + top;
68
+ let cursor = 0;
69
+ const columns = grid.columnWidths.map(width => {
70
+ const start = cursor;
71
+ cursor += width + config.itemGap;
72
+ return start;
73
+ });
74
+ symbolX = grid.cells.map(cell => gridX + columns[cell.column] - sampleLeft);
75
+ labelX = symbolX.map(value => value + sampleRight + config.labels.offset);
76
+ itemY = grid.cells.map(cell => gridY + grid.rowHeight / 2 + cell.row * (grid.rowHeight + config.itemGap));
77
+ title = inline ? { x, y: y + height / 2, align: "left" }
78
+ : { x: x + width / 2, y: y + titleHeight / 2, align: "center" };
79
+ }
80
+ const textBounds = (x, y, text, style, align = "left") => resolveTextBounds({
81
+ x, y, text, ...style, textAlign: align, textBaseline: "middle"
82
+ });
83
+ const bounds = [
84
+ ...(titleVisible ? [textBounds(title.x, title.y, config.title, config.titleStyle, title.align)] : []),
85
+ ...labels.map((label, index) => textBounds(labelX[index], itemY[index], label, config.labels)),
86
+ ...symbolX.map((x, index) => {
87
+ const local = sampleBounds[index];
88
+ return { left: x + local.left, right: x + local.right,
89
+ top: itemY[index] + local.top, bottom: itemY[index] + local.bottom };
90
+ })
91
+ ];
92
+ if (config.layout === "legacy-bottom") {
93
+ const itemBounds = titleVisible ? bounds.slice(1) : bounds;
94
+ if (titleVisible && itemBounds.some(bound => bound.top < bounds[0].bottom)) {
95
+ throw new Error("Legacy bottom legend requires more space between its fixed title and item rows.");
96
+ }
97
+ if (bounds.some(bound => bound.top <= plot.y + plot.height)) {
98
+ throw new Error("Legacy bottom legend requires more bottom-margin space.");
99
+ }
100
+ }
101
+ return { symbolX, labelX, itemY, title, bounds };
102
+ }