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,5 +1,6 @@
1
1
  import { USER_ID_SOURCE } from "../../core/identifiers.js";
2
2
  import {
3
+ AXIS_RESOURCE_CHANNELS,
3
4
  CARTESIAN_POSITION_CHANNELS,
4
5
  ENCODING_CHANNELS,
5
6
  SCALED_ENCODING_CHANNELS
@@ -23,7 +24,7 @@ const ENCODING_PATHS = Object.freeze([
23
24
  "encoding.color.aggregate",
24
25
  "encoding.group.fields",
25
26
  "encoding.group.inferredFrom",
26
- ...["x", "y", "x2", "y2", "theta", "color"].map(channel =>
27
+ ...["x", "y", "x2", "y2", "theta", "color", "stroke"].map(channel =>
27
28
  `encoding.${channel}.temporalUnit`
28
29
  ),
29
30
  "encoding.pathOrder.field",
@@ -64,6 +65,8 @@ const ENTITY_PATHS = Object.freeze({
64
65
  "transform",
65
66
  "mark.type",
66
67
  "mark.missing",
68
+ "mark.orientation",
69
+ "mark.blend",
67
70
  "derivedBindings.regression",
68
71
  "layout.mode",
69
72
  ...ENCODING_PATHS
@@ -73,7 +76,7 @@ const ENTITY_PATHS = Object.freeze({
73
76
  collection: "scales",
74
77
  properties: new Set([
75
78
  "type", "domain", "range", "emptyDomain", "nice", "zero", "clamp", "reverse",
76
- "unknown", "base", "exponent", "constant", "interpolate", "radialMapping", "midpoint",
79
+ "unknown", "base", "exponent", "constant", "interpolate", "whitePoint", "gamma", "radialMapping", "midpoint",
77
80
  "paddingInner", "paddingOuter", "padding", "align"
78
81
  ])
79
82
  },
@@ -100,6 +103,9 @@ const GUIDE_PATHS = new Set([
100
103
  "axis.parallel.coordinate",
101
104
  "axis.parallel.scales",
102
105
  "axis.parallel.titles",
106
+ "legend.colorSize.colorScale",
107
+ "legend.colorSize.sizeScale",
108
+ "legend.colorSize.title",
103
109
  "legend.color.scale",
104
110
  "legend.color.title",
105
111
  "legend.color.order",
@@ -136,6 +142,7 @@ const GUIDE_REMOVABLE_CONTAINERS = new Set([
136
142
  "grid.vertical",
137
143
  "grid.theta",
138
144
  "grid.radial",
145
+ "legend.colorSize",
139
146
  "legend.color",
140
147
  "legend.stroke",
141
148
  "legend.size",
@@ -146,6 +153,13 @@ const GUIDE_REMOVABLE_CONTAINERS = new Set([
146
153
 
147
154
  const TITLE_PATHS = new Set(["text", "subtitle"]);
148
155
 
156
+ function isNamedAxisPath(path, container = false) {
157
+ const match = path.match(new RegExp(container
158
+ ? `^axis\\.(${USER_ID_SOURCE})$`
159
+ : `^axis\\.(${USER_ID_SOURCE})\\.(scale|coordinate|title|channel|position)$`));
160
+ return match !== null && !AXIS_RESOURCE_CHANNELS.includes(match[1]);
161
+ }
162
+
149
163
  function splitPropertyPath(property) {
150
164
  return property.split(".");
151
165
  }
@@ -200,7 +214,9 @@ export function parseSemanticPath(property, { allowContainer = false } = {}) {
200
214
 
201
215
  if (
202
216
  !GUIDE_PATHS.has(propertyPath) &&
203
- !(allowContainer && GUIDE_REMOVABLE_CONTAINERS.has(propertyPath))
217
+ !isNamedAxisPath(propertyPath) &&
218
+ !(allowContainer && (GUIDE_REMOVABLE_CONTAINERS.has(propertyPath) ||
219
+ isNamedAxisPath(propertyPath, true)))
204
220
  ) {
205
221
  throw new Error(`Unknown semantic property "${property}".`);
206
222
  }
@@ -1,3 +1,5 @@
1
+ import { rejectUnknownProperties as rejectUnknownKeys } from "../core/validation.js";
2
+ import { requireStringValue as requireField } from "../core/validation.js";
1
3
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
4
  import { layoutSeriesPartition } from "./seriesLayout.js";
3
5
 
@@ -8,17 +10,7 @@ const AS_KEYS = Object.freeze(["start", "end", "value", "share"]);
8
10
  const MODES = new Set(["stack", "fill", "center", "diverging"]);
9
11
  const MAX_OUTPUT_ROWS = 10_000;
10
12
 
11
- function rejectUnknownKeys(value, supported, label) {
12
- const unknown = Object.keys(value).find(key => !supported.includes(key));
13
- if (unknown !== undefined) throw new Error(`Unknown ${label} property "${unknown}".`);
14
- }
15
13
 
16
- function requireField(value, label) {
17
- if (typeof value !== "string" || value.length === 0) {
18
- throw new TypeError(`${label} must be a non-empty string.`);
19
- }
20
- return value;
21
- }
22
14
 
23
15
  function normalizeAs(value, field) {
24
16
  const requested = value ?? {};
@@ -1,3 +1,4 @@
1
+ import { rejectUnknownProperties as rejectUnknownKeys } from "../core/validation.js";
1
2
  import { aggregateRows } from "./aggregate.js";
2
3
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
3
4
 
@@ -6,12 +7,6 @@ const SIMPLE_STATISTICS = new Set(["mean", "median", "min", "max"]);
6
7
  const POPULATIONS = new Set(["boundData", "visibleItems"]);
7
8
  const TRANSFORM_KEYS = Object.freeze(["type", "target"]);
8
9
 
9
- function rejectUnknownKeys(value, supported, label) {
10
- const unknown = Object.keys(value).find(key => !supported.includes(key));
11
- if (unknown !== undefined) {
12
- throw new Error(`Unknown ${label} property "${unknown}".`);
13
- }
14
- }
15
10
 
16
11
  export function normalizeReferenceStatistic(value, label = "Reference statistic") {
17
12
  if (!isPlainObject(value)) {
@@ -1,3 +1,6 @@
1
+ import { scalarKey } from "./transformKeys.js";
2
+ import { rejectUnknownProperties as rejectUnknownKeys } from "../core/validation.js";
3
+ import { requireStringValue as requireField } from "../core/validation.js";
1
4
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
5
  import {
3
6
  aggregateRows,
@@ -20,21 +23,7 @@ const TRANSFORM_KEYS = Object.freeze([
20
23
  ]);
21
24
  const AGGREGATE_KEYS = Object.freeze(["op", "field", "as"]);
22
25
  const NOMINAL_OPERATIONS = new Set(["distinct", "valid", "missing"]);
23
- const MAX_OUTPUT_ROWS = 10_000;
24
-
25
- function rejectUnknownKeys(value, supported, label) {
26
- const unknown = Object.keys(value).find(key => !supported.includes(key));
27
- if (unknown !== undefined) {
28
- throw new Error(`Unknown ${label} property "${unknown}".`);
29
- }
30
- }
31
-
32
- function requireField(value, label) {
33
- if (typeof value !== "string" || value.length === 0) {
34
- throw new TypeError(`${label} must be a non-empty string.`);
35
- }
36
- return value;
37
- }
26
+ const MAX_OUTPUT_ROWS = 30_000;
38
27
 
39
28
  function normalizeGroupBy(value) {
40
29
  if (value === undefined) return [];
@@ -131,18 +120,6 @@ export function validateSummaryTransform(transform) {
131
120
  return transform;
132
121
  }
133
122
 
134
- function scalarKey(value, label) {
135
- if (value === null) return "null";
136
- if (typeof value === "string") return `string:${value.length}:${value}`;
137
- if (typeof value === "boolean") return `boolean:${value}`;
138
- if (typeof value === "number" && Number.isFinite(value)) {
139
- return `number:${Object.is(value, -0) ? 0 : value}`;
140
- }
141
- throw new TypeError(
142
- `${label} must contain null, strings, booleans, or finite numbers.`
143
- );
144
- }
145
-
146
123
  function requireSourceFields(rows, transform) {
147
124
  if (rows.length === 0) return;
148
125
  const fields = new Set(rows.flatMap(row => Object.keys(row)));
@@ -11,6 +11,11 @@ export function isSourceOwnedText(layer) {
11
11
  return layer?.mark?.type === "text" && layer.source !== undefined;
12
12
  }
13
13
 
14
+ export const TEXT_MARK_STYLE_OPTIONS = Object.freeze([
15
+ "fill", "opacity", "fontSize", "fontFamily", "fontWeight",
16
+ "align", "baseline", "rotation", "dx", "dy", "inheritColor", "lineHeight", "blockAlign"
17
+ ]);
18
+
14
19
  export const DEFAULT_TEXT_MARK = cloneAndFreeze({
15
20
  fill: "#334155",
16
21
  opacity: 1,
@@ -49,6 +54,18 @@ export function formatTextValue(value, format = "auto") {
49
54
 
50
55
  export function normalizeTextMarkConfig(options, base = DEFAULT_TEXT_MARK) {
51
56
  const config = { ...base };
57
+ if (Object.hasOwn(options, "lineHeight")) {
58
+ if (!Number.isFinite(options.lineHeight) || options.lineHeight <= 0) throw new RangeError("Text lineHeight must be positive and finite.");
59
+ config.lineHeight = options.lineHeight;
60
+ }
61
+ if (Object.hasOwn(options, "blockAlign")) {
62
+ if (!["first", "middle"].includes(options.blockAlign)) throw new Error("Text blockAlign must be first or middle.");
63
+ config.blockAlign = options.blockAlign;
64
+ }
65
+ if (Object.hasOwn(options, "inheritColor")) {
66
+ if (![false, "fill", "stroke"].includes(options.inheritColor)) throw new Error("Text inheritColor must be fill, stroke, or false.");
67
+ config.inheritColor = options.inheritColor;
68
+ }
52
69
  const mapping = {
53
70
  fill: "fill",
54
71
  opacity: "opacity",
@@ -75,3 +92,5 @@ export function normalizeTextMarkConfig(options, base = DEFAULT_TEXT_MARK) {
75
92
  }
76
93
  return cloneAndFreeze(config);
77
94
  }
95
+
96
+ export { resolveTextLines } from "../core/textMetrics.js";
@@ -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 { normalizeTemporalValue, validateTemporalUnit } from "./scales/fields.js";
3
4
 
@@ -51,6 +52,7 @@ function formatter(timeZone) {
51
52
  try {
52
53
  value = new Intl.DateTimeFormat("en-US-u-ca-gregory-nu-latn", {
53
54
  timeZone,
55
+ era: "short",
54
56
  year: "numeric",
55
57
  month: "2-digit",
56
58
  day: "2-digit",
@@ -67,7 +69,7 @@ function formatter(timeZone) {
67
69
  return CALENDAR_FORMATTERS.get(timeZone);
68
70
  }
69
71
 
70
- function validateTimeZone(timeZone) {
72
+ export function validateTimeZone(timeZone) {
71
73
  if (typeof timeZone !== "string" || timeZone.length === 0) {
72
74
  throw new TypeError("Time-unit timeZone must be a non-empty IANA time-zone name.");
73
75
  }
@@ -75,12 +77,6 @@ function validateTimeZone(timeZone) {
75
77
  return timeZone;
76
78
  }
77
79
 
78
- function requireField(value, label) {
79
- if (typeof value !== "string" || value.length === 0) {
80
- throw new TypeError(`${label} must be a non-empty string.`);
81
- }
82
- return value;
83
- }
84
80
 
85
81
  function rejectUnknownKeys(value) {
86
82
  const unknown = Object.keys(value).find(key => !TRANSFORM_KEYS.includes(key));
@@ -127,7 +123,8 @@ export function validateTimeUnitTransform(transform) {
127
123
  }
128
124
 
129
125
  export function normalizeTimeUnitTransform(args = {}) {
130
- const { field, unit, as, temporalUnit, timeZone } = args;
126
+ const { field, unit, temporalUnit, timeZone } = args;
127
+ const as = args.as === undefined ? `${field}_${unit}` : args.as;
131
128
  const transform = { type: "timeUnit", field, unit, as,
132
129
  ...(temporalUnit === undefined ? {} : { temporalUnit }),
133
130
  ...(timeZone === undefined ? {} : { timeZone }) };
@@ -182,17 +179,18 @@ export function calendarParts(timestamp, timeZone) {
182
179
  validateTimeZone(timeZone);
183
180
  const parts = Object.fromEntries(formatter(timeZone).formatToParts(timestamp)
184
181
  .filter(part => part.type !== "literal")
185
- .map(part => [part.type, Number(part.value)]));
182
+ .map(part => [part.type, part.type === "era" ? part.value : Number(part.value)]));
183
+ const year = parts.era === "BC" ? 1 - parts.year : parts.year;
186
184
  const millisecond = ((timestamp % 1000) + 1000) % 1000;
187
185
  return {
188
- year: parts.year,
186
+ year,
189
187
  month: parts.month,
190
188
  day: parts.day,
191
189
  hour: parts.hour,
192
190
  minute: parts.minute,
193
191
  second: parts.second,
194
192
  millisecond,
195
- weekday: weekdayFromDate(parts.year, parts.month, parts.day)
193
+ weekday: weekdayFromDate(year, parts.month, parts.day)
196
194
  };
197
195
  }
198
196
 
@@ -337,3 +335,16 @@ export function deriveTimeUnitRows(rows, transform) {
337
335
  });
338
336
  return cloneAndFreeze(values);
339
337
  }
338
+
339
+ export function offsetUtcTimeUnit(timestamp, unit) {
340
+ const date = new Date(timestamp);
341
+ if (unit === "year") return utcTimestamp(date.getUTCFullYear() + 1);
342
+ if (unit === "quarter" || unit === "month") {
343
+ return utcTimestamp(date.getUTCFullYear(), date.getUTCMonth() + (unit === "quarter" ? 3 : 1));
344
+ }
345
+ if (unit === "day") return timestamp + 24 * 60 * 60 * 1000;
346
+ if (unit === "hour") return timestamp + 60 * 60 * 1000;
347
+ if (unit === "minute") return timestamp + 60 * 1000;
348
+ return timestamp + 1000;
349
+ }
350
+
@@ -0,0 +1,47 @@
1
+ import { isPlainObject } from "../core/immutable.js";
2
+ import { requireStringValue, rejectUnknownProperties } from "../core/validation.js";
3
+
4
+ export function scalarKey(value, label) {
5
+ if (value === null) return "null";
6
+ if (typeof value === "string") return `string:${value.length}:${value}`;
7
+ if (typeof value === "boolean") return `boolean:${value}`;
8
+ if (typeof value === "number" && Number.isFinite(value)) {
9
+ return `number:${Object.is(value, -0) ? 0 : value}`;
10
+ }
11
+ throw new TypeError(
12
+ `${label} must contain null, strings, booleans, or finite numbers.`
13
+ );
14
+ }
15
+
16
+ export function normalizeSortBy(value) {
17
+ if (value === undefined) return [];
18
+ if (!Array.isArray(value)) return value;
19
+ return value.map(sort => isPlainObject(sort)
20
+ ? { ...sort, order: sort.order ?? "ascending" }
21
+ : sort
22
+ );
23
+ }
24
+
25
+ export function validateSortBy(sortBy, label) {
26
+ const kind = label.toLowerCase();
27
+ if (!Array.isArray(sortBy)) throw new TypeError(`${label} sortBy must be an array.`);
28
+ const fields = [];
29
+ sortBy.forEach((sort, index) => {
30
+ if (!isPlainObject(sort)) {
31
+ throw new TypeError(`${label} sortBy[${index}] must be a plain object.`);
32
+ }
33
+ rejectUnknownProperties(sort, ["field", "order"], `${kind} sortBy[${index}]`);
34
+ fields.push(requireStringValue(sort.field, `${label} sortBy[${index}].field`));
35
+ if (!["ascending", "descending"].includes(sort.order)) {
36
+ throw new Error(`Unsupported ${kind} sort order "${sort.order}".`);
37
+ }
38
+ });
39
+ if (new Set(fields).size !== fields.length) {
40
+ throw new Error(`${label} sortBy fields must be unique.`);
41
+ }
42
+ }
43
+
44
+ export function isGroupValue(value) {
45
+ return typeof value === "string" || typeof value === "boolean" ||
46
+ (typeof value === "number" && Number.isFinite(value));
47
+ }
@@ -16,6 +16,7 @@ const TRANSFORM_TOPOLOGY = Object.freeze({
16
16
  markFilter: Object.freeze({ provenanceTransparent: true }),
17
17
  normalize: Object.freeze({ facetTopology: "statistical" }),
18
18
  regression: Object.freeze({ facetTopology: "statistical" }),
19
+ regressionStatistics: Object.freeze({ facetTopology: "statistical" }),
19
20
  sort: Object.freeze({ facetTopology: "statistical" }),
20
21
  summary: Object.freeze({ facetTopology: "statistical" }),
21
22
  stack: Object.freeze({ facetTopology: "statistical" }),
@@ -1,4 +1,5 @@
1
1
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
+ import { validateRegressionStatisticsTransform } from "./regression/statistics.js";
2
3
  import { validateBoxTransform } from "./boxPlot.js";
3
4
  import { normalizeBinTransform, validateBinTransform } from "./bin.js";
4
5
  import {
@@ -199,8 +200,11 @@ function normalizeRegressionEdit(transform, patch) {
199
200
  const method = patch.method ?? base.method;
200
201
  if (method !== base.method) {
201
202
  if (method !== "polynomial") delete base.degree;
202
- if (method !== "loess") delete base.span;
203
- if (method === "loess" || base.method === "loess") {
203
+ if (method !== "loess") {
204
+ delete base.span;
205
+ delete base.robustIterations;
206
+ }
207
+ if (["loess", "exp"].includes(method) || ["loess", "exp"].includes(base.method)) {
204
208
  for (const key of [
205
209
  "confidenceMethod", "level", "confidence", "interval"
206
210
  ]) delete base[key];
@@ -443,6 +447,15 @@ const TRANSFORM_POLICIES = Object.freeze({
443
447
  normalizeEdit: normalizeRegressionEdit,
444
448
  outputRoles: () => ({})
445
449
  }),
450
+ regressionStatistics: Object.freeze({
451
+ ...findTransformTopology("regressionStatistics"),
452
+ validate: validateRegressionStatisticsTransform,
453
+ materializeOp: "materializeRegressionStatisticsData",
454
+ replayTransform: requestedTransform,
455
+ editable: true,
456
+ normalizeEdit: (transform, patch) => validateRegressionStatisticsTransform({ ...transform, ...patch }),
457
+ outputRoles: () => ({ count: "count", intercept: "intercept", slope: "slope", rSquared: "rSquared" })
458
+ }),
446
459
  sort: Object.freeze({
447
460
  ...findTransformTopology("sort"),
448
461
  validate: validateSortTransform,
@@ -1,5 +1,7 @@
1
1
  const NUMERIC_FORMAT = /^\.(0?\d|1[0-2])(f|%|e)$/;
2
- const TIME_DIRECTIVES = new Set(["Y", "m", "d", "b", "%"]);
2
+ const GROUPED_NUMERIC_FORMAT = /^,(?:\.(0?\d|1[0-2])f)?$/;
3
+ const WEEKDAY_NAMES = Object.freeze(["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]);
4
+ const TIME_DIRECTIVES = new Set(["Y", "m", "d", "b", "a", "H", "M", "S", "L", "%"]);
3
5
  const MONTH_NAMES = Object.freeze([
4
6
  "Jan", "Feb", "Mar", "Apr", "May", "Jun",
5
7
  "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
@@ -33,12 +35,12 @@ export function validateValueFormat(format, label = "Label format", {
33
35
  if (allowDecimalsObject) return format;
34
36
  }
35
37
  if (typeof format === "string" && (
36
- NUMERIC_FORMAT.test(format) || isUtcValueFormat(format)
38
+ NUMERIC_FORMAT.test(format) || GROUPED_NUMERIC_FORMAT.test(format) || isUtcValueFormat(format)
37
39
  )) {
38
40
  return format;
39
41
  }
40
42
  const numericCandidate = typeof format === "string"
41
- ? format.match(/^\.(\d+)(f|%|e)$/)
43
+ ? format.match(/^,?\.(\d+)(f|%|e)$/)
42
44
  : undefined;
43
45
  if (numericCandidate && Number(numericCandidate[1]) > 12) {
44
46
  throw new RangeError(`${label} numeric format supports at most 12 decimals.`);
@@ -61,9 +63,14 @@ function formatUtcValue(value, format, label) {
61
63
  Y: pad(date.getUTCFullYear(), 4),
62
64
  m: pad(date.getUTCMonth() + 1),
63
65
  d: pad(date.getUTCDate()),
64
- b: MONTH_NAMES[date.getUTCMonth()]
66
+ b: MONTH_NAMES[date.getUTCMonth()],
67
+ a: WEEKDAY_NAMES[date.getUTCDay()],
68
+ H: pad(date.getUTCHours()),
69
+ M: pad(date.getUTCMinutes()),
70
+ S: pad(date.getUTCSeconds()),
71
+ L: pad(date.getUTCMilliseconds(), 3)
65
72
  };
66
- return format.replace(/%([Ymdb%])/g, (_, directive) =>
73
+ return format.replace(/%([YmdbaHMSL%])/g, (_, directive) =>
67
74
  directive === "%" ? "%" : values[directive]
68
75
  );
69
76
  }
@@ -98,6 +105,11 @@ export function formatValue(value, {
98
105
  throw new TypeError(`${label} "${typeof resolved === "string" ? resolved : "decimals"}" requires a finite number.`);
99
106
  }
100
107
  if (typeof resolved === "object") return value.toFixed(resolved.decimals);
108
+ const grouped = resolved.match(GROUPED_NUMERIC_FORMAT);
109
+ if (grouped) {
110
+ const precision = grouped[1] === undefined ? 0 : Number(grouped[1]);
111
+ return new Intl.NumberFormat("en-US", { useGrouping: true, minimumFractionDigits: precision, maximumFractionDigits: precision }).format(value);
112
+ }
101
113
  const match = resolved.match(NUMERIC_FORMAT);
102
114
  const precision = Number(match[1]);
103
115
  if (match[2] === "f") return value.toFixed(precision);
@@ -1,3 +1,4 @@
1
+ import { requireStringValue as requireField } from "../core/validation.js";
1
2
  import { isPlainObject } from "../core/immutable.js";
2
3
  import {
3
4
  interpolateNumber,
@@ -15,12 +16,6 @@ export const WEIGHTED_AGGREGATE_OPERATIONS = Object.freeze([
15
16
  "stderr", "median", "q1", "q3", "quantile"
16
17
  ]);
17
18
 
18
- function requireField(value, label) {
19
- if (typeof value !== "string" || value.length === 0) {
20
- throw new TypeError(`${label} must be a non-empty string.`);
21
- }
22
- return value;
23
- }
24
19
 
25
20
  export function normalizeStatisticalWeight(value, label = "Statistical weight") {
26
21
  if (!isPlainObject(value)) {
@@ -1,3 +1,6 @@
1
+ import { scalarKey, 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 { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
5
  import {
3
6
  maximumMagnitude,
@@ -11,8 +14,6 @@ import { normalizeTemporalValue, validateTemporalUnit } from "./scales/fields.js
11
14
  const TRANSFORM_KEYS = [
12
15
  "type", "partitionBy", "sortBy", "operations", "temporalUnit"
13
16
  ];
14
- const SORT_KEYS = ["field", "order"];
15
- const ORDER_VALUES = ["ascending", "descending"];
16
17
  const OPERATION_VALUES = [
17
18
  "rowNumber", "rank", "denseRank", "cumulativeSum", "lag", "lead",
18
19
  "movingMean", "movingSum"
@@ -30,20 +31,6 @@ const DURATION_UNITS = Object.freeze({
30
31
  day: 86_400_000
31
32
  });
32
33
 
33
- function requireField(value, label) {
34
- if (typeof value !== "string" || value.length === 0) {
35
- throw new TypeError(`${label} must be a non-empty string.`);
36
- }
37
- return value;
38
- }
39
-
40
- function rejectUnknownKeys(value, supported, label) {
41
- const unknown = Object.keys(value).find(key => !supported.includes(key));
42
- if (unknown !== undefined) {
43
- throw new Error(`Unknown ${label} property "${unknown}".`);
44
- }
45
- }
46
-
47
34
  function validateFieldList(value, label) {
48
35
  if (
49
36
  !Array.isArray(value) ||
@@ -56,26 +43,6 @@ function validateFieldList(value, label) {
56
43
  }
57
44
  }
58
45
 
59
- function validateSortBy(sortBy) {
60
- if (!Array.isArray(sortBy)) {
61
- throw new TypeError("Window sortBy must be an array.");
62
- }
63
- const fields = [];
64
- sortBy.forEach((sort, index) => {
65
- if (!isPlainObject(sort)) {
66
- throw new TypeError(`Window sortBy[${index}] must be a plain object.`);
67
- }
68
- rejectUnknownKeys(sort, SORT_KEYS, `window sortBy[${index}]`);
69
- fields.push(requireField(sort.field, `Window sortBy[${index}].field`));
70
- if (!ORDER_VALUES.includes(sort.order)) {
71
- throw new Error(`Unsupported window sort order "${sort.order}".`);
72
- }
73
- });
74
- if (new Set(fields).size !== fields.length) {
75
- throw new Error("Window sortBy fields must be unique.");
76
- }
77
- }
78
-
79
46
  function operationKeys(operation) {
80
47
  if (POSITION_OPERATIONS.has(operation.op)) return ["op", "as"];
81
48
  if (operation.op === "cumulativeSum") return ["op", "field", "as"];
@@ -177,7 +144,7 @@ export function validateWindowTransform(transform) {
177
144
  throw new Error(`Unsupported window transform "${transform.type}".`);
178
145
  }
179
146
  validateFieldList(transform.partitionBy, "Window partitionBy");
180
- validateSortBy(transform.sortBy);
147
+ validateSortBy(transform.sortBy, "Window");
181
148
  validateOperations(transform.operations, transform.sortBy);
182
149
  const durationOperations = transform.operations.filter(operation =>
183
150
  MOVING_OPERATIONS.has(operation.op) && Object.hasOwn(operation.frame, "duration")
@@ -199,15 +166,6 @@ function normalizePartitionBy(value) {
199
166
  return Array.isArray(value) ? [...value] : [value];
200
167
  }
201
168
 
202
- function normalizeSortBy(value) {
203
- if (value === undefined) return [];
204
- if (!Array.isArray(value)) return value;
205
- return value.map(sort => isPlainObject(sort)
206
- ? { ...sort, order: sort.order ?? "ascending" }
207
- : sort
208
- );
209
- }
210
-
211
169
  function normalizeOperations(value) {
212
170
  if (!Array.isArray(value)) return value;
213
171
  return value.map(operation => {
@@ -259,18 +217,6 @@ export function normalizeWindowTransform({
259
217
  return cloneAndFreeze(transform);
260
218
  }
261
219
 
262
- function scalarKey(value, label) {
263
- if (value === null) return "null";
264
- if (typeof value === "string") return `string:${value.length}:${value}`;
265
- if (typeof value === "boolean") return `boolean:${value}`;
266
- if (typeof value === "number" && Number.isFinite(value)) {
267
- return `number:${Object.is(value, -0) ? 0 : value}`;
268
- }
269
- throw new TypeError(
270
- `${label} must contain null, strings, booleans, or finite numbers.`
271
- );
272
- }
273
-
274
220
  function compareScalar(left, right, label) {
275
221
  const leftMissing = left === null || left === undefined;
276
222
  const rightMissing = right === null || right === undefined;