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
@@ -0,0 +1,29 @@
1
+ const UNITS = new Set(["radians", "degrees"]);
2
+
3
+ function toRadians(value, unit) {
4
+ return unit === "degrees" ? value * Math.PI / 180 : value;
5
+ }
6
+
7
+ export function resolveRotation(value, label = "Rotation", {
8
+ legacyUnit = "radians"
9
+ } = {}) {
10
+ if (!UNITS.has(legacyUnit)) {
11
+ throw new TypeError(`${label} legacy unit must be radians or degrees.`);
12
+ }
13
+ if (Number.isFinite(value)) return toRadians(value, legacyUnit);
14
+ if (
15
+ value !== null &&
16
+ typeof value === "object" &&
17
+ Object.getPrototypeOf(value) === Object.prototype &&
18
+ Object.keys(value).length === 2 &&
19
+ Object.hasOwn(value, "value") &&
20
+ Object.hasOwn(value, "unit") &&
21
+ Number.isFinite(value.value) &&
22
+ UNITS.has(value.unit)
23
+ ) {
24
+ return toRadians(value.value, value.unit);
25
+ }
26
+ throw new TypeError(
27
+ `${label} must be a finite legacy number or { value, unit: "radians" | "degrees" }.`
28
+ );
29
+ }
@@ -1,16 +1,16 @@
1
+ import { normalizePositionDatum } from "./positionDatum.js";
1
2
  import { cloneAndFreeze } from "../core/immutable.js";
2
3
  import {
3
- isNominalValue,
4
- normalizeTemporalValue,
5
4
  readNominalField,
6
5
  readQuantitativeField,
7
- readTemporalField,
8
- validateSemanticFieldType
6
+ readTemporalField
9
7
  } from "./scales/index.js";
10
8
 
11
9
  const ENDPOINT_CHANNELS = Object.freeze(["x", "y", "x2", "y2"]);
12
10
  const VALUE_CHANNELS = Object.freeze([
13
11
  ...ENDPOINT_CHANNELS,
12
+ "xOffset",
13
+ "yOffset",
14
14
  "strokeDash",
15
15
  "strokeWidth",
16
16
  "opacity"
@@ -37,26 +37,13 @@ function readField(rows, encoding) {
37
37
  return readNominalField(rows, encoding.field);
38
38
  }
39
39
  if (encoding.fieldType === "temporal") {
40
- return readTemporalField(rows, encoding.field);
40
+ return readTemporalField(rows, encoding.field, encoding.temporalUnit);
41
41
  }
42
42
  return readQuantitativeField(rows, encoding.field);
43
43
  }
44
44
 
45
- export function normalizeRuleDatum(value, fieldType, channel) {
46
- validateSemanticFieldType(fieldType);
47
- if (fieldType === "quantitative") {
48
- if (!Number.isFinite(value)) {
49
- throw new TypeError(`Rule ${channel} datum must be a finite number.`);
50
- }
51
- return value;
52
- }
53
- if (fieldType === "temporal") {
54
- return normalizeTemporalValue(value, `${channel} datum`, 0);
55
- }
56
- if (!isNominalValue(value)) {
57
- throw new TypeError(`Rule ${channel} datum must be a nominal value.`);
58
- }
59
- return value;
45
+ export function normalizeRuleDatum(value, fieldType, channel, temporalUnit) {
46
+ return normalizePositionDatum(value, fieldType, channel, temporalUnit, "Rule");
60
47
  }
61
48
 
62
49
  export function deriveRuleValues(rows, layer) {
@@ -81,7 +68,8 @@ export function deriveRuleValues(rows, layer) {
81
68
  const datum = normalizeRuleDatum(
82
69
  encoding.datum,
83
70
  encoding.fieldType,
84
- channel
71
+ channel,
72
+ encoding.temporalUnit
85
73
  );
86
74
  values[channel] = Array.from({ length }, () => datum);
87
75
  }
@@ -325,6 +325,23 @@ export function interpolateColorStops(stops, position, interpolation = "rgb") {
325
325
  return interpolateColors(colors, position, method, true);
326
326
  }
327
327
 
328
+ export function validateSequentialMidpoint(value, type, domain = "auto") {
329
+ if (value === undefined || value === "auto") return undefined;
330
+ if (type !== "sequential") {
331
+ throw new Error(`Scale type "${type}" does not support midpoint.`);
332
+ }
333
+ if (!Number.isFinite(value)) {
334
+ throw new TypeError("Scale midpoint must be finite or auto.");
335
+ }
336
+ if (domain !== "auto" && (
337
+ !Array.isArray(domain) || domain.length !== 2 || !domain.every(Number.isFinite) ||
338
+ !(value > Math.min(...domain) && value < Math.max(...domain))
339
+ )) {
340
+ throw new RangeError("Scale midpoint must lie strictly inside its resolved domain.");
341
+ }
342
+ return value;
343
+ }
344
+
328
345
  export function mapSequentialColors(
329
346
  values,
330
347
  domain,
@@ -342,6 +359,7 @@ export function mapSequentialColors(
342
359
  if (typeof shouldClamp !== "boolean") {
343
360
  throw new TypeError("Sequential color clamp must be a boolean.");
344
361
  }
362
+ const midpoint = validateSequentialMidpoint(options.midpoint, "sequential", domain);
345
363
  const [start, end] = domain;
346
364
  const constant = start === end;
347
365
  const method = validateContinuousColorInterpolation(interpolation);
@@ -354,7 +372,11 @@ export function mapSequentialColors(
354
372
  if (hasUnknown) return options.unknown;
355
373
  throw new TypeError("Sequential color values must be finite numbers.");
356
374
  }
357
- const raw = constant ? 0.5 : inverseLerp(value, start, end);
375
+ const raw = midpoint === undefined
376
+ ? constant ? 0.5 : inverseLerp(value, start, end)
377
+ : (end > start ? value <= midpoint : value >= midpoint)
378
+ ? inverseLerp(value, start, midpoint) / 2
379
+ : 0.5 + inverseLerp(value, midpoint, end) / 2;
358
380
  return interpolateColors(colors, raw, method, shouldClamp);
359
381
  }));
360
382
  }
@@ -0,0 +1,38 @@
1
+ import { BAR_GRAINS, resolveBarChannels, resolveBarGrain } from "../bars/policy.js";
2
+ import { validateAggregate, validateAggregateFieldType } from "../aggregate.js";
3
+ import { isDiscretizedColorScaleType } from "./types.js";
4
+
5
+ export function validateContinuousColorConsumer(layer, encoding, scale, { inferAggregate = false } = {}) {
6
+ const kind = layer.mark?.type;
7
+ if (!["point", "bar", "rect"].includes(kind)) {
8
+ throw new Error("Continuous color requires a Point, aggregate Bar, or Rect consumer.");
9
+ }
10
+ if (!["quantitative", "temporal"].includes(encoding.fieldType) ||
11
+ (isDiscretizedColorScaleType(scale.type) && encoding.fieldType !== "quantitative")) {
12
+ throw new Error(`Scale type "${scale.type}" has an incompatible color field type.`);
13
+ }
14
+ if (Object.hasOwn(scale, "unknown") && kind !== "point") {
15
+ throw new Error("Continuous color scale unknown currently requires a row-owned point mark.");
16
+ }
17
+ if (kind !== "bar") {
18
+ if (encoding.aggregate !== undefined) throw new Error(`${kind} continuous color does not support aggregate.`);
19
+ return undefined;
20
+ }
21
+ if (["nominal", "ordinal"].includes(layer.encoding?.color?.fieldType)) {
22
+ throw new Error("Continuous bar color cannot replace an existing nominal color layout.");
23
+ }
24
+ if (encoding.fieldType !== "quantitative") {
25
+ throw new Error("Aggregate bar color currently requires a quantitative field.");
26
+ }
27
+ if (resolveBarGrain(layer) !== BAR_GRAINS.aggregate) {
28
+ throw new Error("Continuous bar color requires a complete categorical aggregate bar.");
29
+ }
30
+ const measure = layer.encoding?.[resolveBarChannels(layer).measure];
31
+ const aggregate = encoding.aggregate ?? (inferAggregate && measure?.field === encoding.field ? measure.aggregate : undefined);
32
+ if (aggregate === undefined) {
33
+ throw new Error("Continuous bar color requires aggregate when its field differs from the measure field.");
34
+ }
35
+ validateAggregate(aggregate);
36
+ validateAggregateFieldType(aggregate, encoding.fieldType);
37
+ return aggregate;
38
+ }
@@ -1,4 +1,5 @@
1
- import { validateContinuousColorInterpolation } from "./color.js";
1
+ import { validateMeasuredRadiusScale } from "./radial.js";
2
+ import { validateContinuousColorInterpolation, validateSequentialMidpoint } from "./color.js";
2
3
  import { isTransformedScaleType } from "./mapping.js";
3
4
  import {
4
5
  normalizeTransformParameters,
@@ -137,6 +138,11 @@ export function normalizeScaleDefinition({
137
138
  throw new Error(`Scale type "${type}" does not support interpolate.`);
138
139
  }
139
140
 
141
+ const midpoint = validateSequentialMidpoint(
142
+ retainedValue(previous, patch, defaults, "midpoint", typeChanged), type, definition.domain
143
+ );
144
+ if (midpoint !== undefined) definition.midpoint = midpoint;
145
+
140
146
  if (type === "band") {
141
147
  validateBandParameters(definition, previous, patch, defaults, typeChanged);
142
148
  } else if (type === "point") {
@@ -149,5 +155,13 @@ export function normalizeScaleDefinition({
149
155
  }
150
156
  }
151
157
 
158
+ const radialMapping = Object.hasOwn(patch, "radialMapping") ? patch.radialMapping : previous.radialMapping;
159
+ if (radialMapping !== undefined) {
160
+ definition.radialMapping = radialMapping;
161
+ validateMeasuredRadiusScale({ ...definition,
162
+ ...(Object.hasOwn(patch, "unknown") || Object.hasOwn(previous, "unknown")
163
+ ? { unknown: patch.unknown ?? previous.unknown } : {})
164
+ }, { allowAuto: true });
165
+ }
152
166
  return definition;
153
167
  }
@@ -1,6 +1,7 @@
1
1
  import { cloneAndFreeze } from "../../core/immutable.js";
2
2
  import { validateGeneratedItemLimit } from "../../core/validation.js";
3
3
  import { interpolateNumber } from "../numeric.js";
4
+ import { formatValue, validateValueFormat } from "../valueFormat.js";
4
5
  import { resolveColorRange, validateColorRange } from "./appearance.js";
5
6
  import { SCALE_ROLES, validateScaleTypeForRole } from "./types.js";
6
7
 
@@ -162,17 +163,22 @@ export function mapDiscretizedColors(values, scale) {
162
163
  }));
163
164
  }
164
165
 
165
- export function formatDiscretizedIntervals(thresholds) {
166
+ export function formatDiscretizedIntervals(thresholds, format = "auto") {
166
167
  nondecreasing(thresholds, "Discretized color thresholds");
168
+ const resolvedFormat = validateValueFormat(format, "Legend label format");
167
169
  let resolution = Infinity;
168
170
  for (let index = 1; index < thresholds.length; index += 1) {
169
171
  const gap = thresholds[index] - thresholds[index - 1];
170
172
  if (Number.isFinite(gap) && gap > 0) resolution = Math.min(resolution, gap);
171
173
  }
172
- let labels = thresholds.map(value =>
173
- Number.isInteger(value) ? String(value) : value.toFixed(1)
174
- );
175
- if (thresholds.some((value, index) => {
174
+ let labels = resolvedFormat === "auto"
175
+ ? thresholds.map(value => Number.isInteger(value) ? String(value) : value.toFixed(1))
176
+ : thresholds.map(value => formatValue(value, {
177
+ format: resolvedFormat,
178
+ valueType: "quantitative",
179
+ label: "Legend label format"
180
+ }));
181
+ if (resolvedFormat === "auto" && thresholds.some((value, index) => {
176
182
  const rounded = Number(labels[index]);
177
183
  return value !== 0 && rounded === 0 ||
178
184
  index > 0 && value !== thresholds[index - 1] &&
@@ -81,9 +81,37 @@ function validTimestamp(value) {
81
81
  return Number.isFinite(value) && Number.isFinite(new Date(value).getTime());
82
82
  }
83
83
 
84
- export function normalizeTemporalValue(value, field = "value", index = 0) {
84
+ export function validateTemporalUnit(unit) {
85
+ if (unit !== undefined && !["auto", "year", "timestamp"].includes(unit)) {
86
+ throw new Error(`Unsupported temporalUnit "${unit}"; use auto, year, or timestamp.`);
87
+ }
88
+ return unit;
89
+ }
90
+
91
+ export function resolveTemporalUnit(options, fieldType, previous) {
92
+ if (fieldType !== "temporal") {
93
+ if (Object.hasOwn(options, "temporalUnit")) {
94
+ throw new Error("temporalUnit requires a temporal fieldType.");
95
+ }
96
+ return undefined;
97
+ }
98
+ validateTemporalUnit(options.temporalUnit);
99
+ const sameBinding = ["field", "datum"].some(key =>
100
+ Object.hasOwn(options, key) && Object.hasOwn(previous ?? {}, key) &&
101
+ options[key] === previous[key]);
102
+ return options.temporalUnit ?? (sameBinding && previous?.fieldType === "temporal"
103
+ ? previous.temporalUnit : undefined);
104
+ }
105
+
106
+ export function normalizeTemporalValue(value, field = "value", index = 0, temporalUnit = "auto") {
107
+ validateTemporalUnit(temporalUnit);
85
108
  let timestamp;
86
- if (typeof value === "number") {
109
+ if (temporalUnit === "year") {
110
+ const year = typeof value === "string" && /^\d{4}$/.test(value) ? Number(value) : value;
111
+ if (Number.isInteger(year) && year >= 0 && year <= 9999) timestamp = utcDate(year);
112
+ } else if (temporalUnit === "timestamp") {
113
+ if (typeof value === "number") timestamp = value;
114
+ } else if (typeof value === "number") {
87
115
  timestamp = Number.isInteger(value) && value >= 1000 && value <= 9999
88
116
  ? utcDate(value)
89
117
  : value;
@@ -99,21 +127,22 @@ export function normalizeTemporalValue(value, field = "value", index = 0) {
99
127
  if (!validTimestamp(timestamp)) {
100
128
  throw new TypeError(
101
129
  `Field "${field}" must contain a temporal string or finite timestamp ` +
102
- `(including a valid date or four-digit year) at row ${index}.`
130
+ `(including a valid date or four-digit year) at row ${index}; temporalUnit is ${temporalUnit}.`
103
131
  );
104
132
  }
105
133
  return timestamp;
106
134
  }
107
135
 
108
- export function readTemporalField(rows, field) {
136
+ export function readTemporalField(rows, field, temporalUnit) {
109
137
  validateField(field);
138
+ validateTemporalUnit(temporalUnit);
110
139
  return cloneAndFreeze(rows.map((row, index) => {
111
140
  if (!Object.hasOwn(row, field)) {
112
141
  throw new TypeError(
113
142
  `Field "${field}" must contain a temporal string or finite timestamp at row ${index}.`
114
143
  );
115
144
  }
116
- return normalizeTemporalValue(row[field], field, index);
145
+ return normalizeTemporalValue(row[field], field, index, temporalUnit);
117
146
  }));
118
147
  }
119
148
 
@@ -129,10 +158,11 @@ export function readNominalField(rows, field) {
129
158
  }));
130
159
  }
131
160
 
132
- export function readScaleField(rows, field, fieldType, { allowUnknown = false } = {}) {
161
+ export function readScaleField(rows, field, fieldType, { allowUnknown = false, temporalUnit } = {}) {
133
162
  validateSemanticFieldType(fieldType);
163
+ validateTemporalUnit(temporalUnit);
134
164
  if (!allowUnknown) {
135
- if (fieldType === "temporal") return readTemporalField(rows, field);
165
+ if (fieldType === "temporal") return readTemporalField(rows, field, temporalUnit);
136
166
  if (["nominal", "ordinal"].includes(fieldType)) {
137
167
  return readNominalField(rows, field);
138
168
  }
@@ -149,7 +179,7 @@ export function readScaleField(rows, field, fieldType, { allowUnknown = false }
149
179
  return isNominalValue(value) ? value : undefined;
150
180
  }
151
181
  try {
152
- return normalizeTemporalValue(value, field, index);
182
+ return normalizeTemporalValue(value, field, index, temporalUnit);
153
183
  } catch {
154
184
  return undefined;
155
185
  }
@@ -1,3 +1,4 @@
1
+ import { mapMeasuredRadiusValues } from "./radial.js";
1
2
  import { mapLinearValues } from "./continuous.js";
2
3
  import { mapTransformedValues } from "./transformed.js";
3
4
  import { SCALE_ROLES, validateScaleTypeForRole } from "./types.js";
@@ -9,6 +10,7 @@ export function isTransformedScaleType(type) {
9
10
  }
10
11
 
11
12
  export function mapContinuousScaleValues(values, scale) {
13
+ if (scale?.radialMapping !== undefined) return mapMeasuredRadiusValues(values, scale);
12
14
  if (isTransformedScaleType(scale?.type)) {
13
15
  validateScaleTypeForRole(scale.type, SCALE_ROLES.quantitativePosition);
14
16
  return mapTransformedValues(values, scale.domain, scale.range, {
@@ -278,3 +278,26 @@ export function mapOrdinalPositionValues(values, scale) {
278
278
  );
279
279
  }));
280
280
  }
281
+
282
+ export function mapOrdinalOffsetValues(values, scale) {
283
+ const indices = new Map(scale.domain.map((value, index) => [value, index]));
284
+ const direction = Math.sign(scale.step) || 1;
285
+ const denominator = Math.max(
286
+ 1,
287
+ scale.domain.length - scale.paddingInner + scale.paddingOuter * 2
288
+ );
289
+ return cloneAndFreeze(values.map(value => {
290
+ const index = indices.get(value);
291
+ if (index === undefined) {
292
+ throw new Error(`Value "${value}" is outside the ordinal domain.`);
293
+ }
294
+ const direct = scale.start + index * scale.step +
295
+ direction * scale.bandwidth / 2;
296
+ return finitePosition(
297
+ direct,
298
+ scale,
299
+ (scale.paddingOuter + index + (1 - scale.paddingInner) / 2) /
300
+ denominator
301
+ );
302
+ }));
303
+ }
@@ -0,0 +1,55 @@
1
+ import { cloneAndFreeze } from "../../core/immutable.js";
2
+ import { interpolateNumber } from "../numeric.js";
3
+
4
+ export function validateRadialMapping(mapping) {
5
+ if (!["area", "radius-length"].includes(mapping)) {
6
+ throw new Error('Radial mapping must be "area" or "radius-length".');
7
+ }
8
+ return mapping;
9
+ }
10
+
11
+ export function validateMeasuredRadiusScale(scale, { allowAuto = false } = {}) {
12
+ validateRadialMapping(scale.radialMapping);
13
+ if (scale.type !== "linear" || (scale.nice !== undefined && scale.nice !== false) ||
14
+ (scale.zero !== undefined && scale.zero !== true) ||
15
+ (scale.reverse !== undefined && scale.reverse !== false) || Object.hasOwn(scale, "unknown")) {
16
+ throw new Error("Measured radius requires a linear, zero-based, non-reversed scale without nice or unknown.");
17
+ }
18
+ if (!(allowAuto && scale.domain === "auto") && (!Array.isArray(scale.domain) || scale.domain.length !== 2 ||
19
+ scale.domain[0] !== 0 || !Number.isFinite(scale.domain[1]) || scale.domain[1] <= 0)) {
20
+ throw new RangeError("Measured radius domain must be [0, positive finite maximum].");
21
+ }
22
+ if (!(allowAuto && scale.range === "auto") && (!Array.isArray(scale.range) || scale.range.length !== 2 ||
23
+ !scale.range.every(Number.isFinite) || scale.range[0] < 0 || scale.range[0] >= scale.range[1])) {
24
+ throw new RangeError("Measured radius range must satisfy 0 <= inner < outer with finite radii.");
25
+ }
26
+ if (scale.clamp !== undefined && typeof scale.clamp !== "boolean") {
27
+ throw new TypeError("Measured radius clamp must be a boolean.");
28
+ }
29
+ return scale;
30
+ }
31
+
32
+ export function mapMeasuredRadiusValues(values, scale) {
33
+ validateMeasuredRadiusScale(scale);
34
+ const [inner, outer] = scale.range;
35
+ const ratio = inner / outer;
36
+ // Normalize before squaring so finite large radii do not overflow.
37
+ const annulus = (1 - ratio) * (1 + ratio);
38
+ return cloneAndFreeze(values.map(value => {
39
+ if (!Number.isFinite(value)) throw new TypeError("Measured radius values must be finite.");
40
+ const proportion = value / scale.domain[1];
41
+ const t = scale.clamp ? Math.max(0, Math.min(1, proportion)) : proportion;
42
+ if (!Number.isFinite(t) || t < 0 || t > 1) {
43
+ throw new RangeError("Measured radius value must lie inside its zero-based domain.");
44
+ }
45
+ if (t === 0 && value <= 0) return inner;
46
+ if (t === 1) return outer;
47
+ const radius = scale.radialMapping === "area"
48
+ ? outer * Math.sqrt(Math.min(1, ratio * ratio + t * annulus))
49
+ : interpolateNumber(inner, outer, t);
50
+ if (radius <= inner) {
51
+ throw new RangeError("Positive measured radius cannot be represented distinctly from the inner radius at this numeric precision.");
52
+ }
53
+ return Math.min(outer, radius);
54
+ }));
55
+ }
@@ -27,6 +27,7 @@ export const COMPLETE_SCALE_TYPES = Object.freeze([
27
27
  ]);
28
28
 
29
29
  const SCALE_PROPERTY_TYPES = Object.freeze({
30
+ midpoint: Object.freeze(["sequential"]),
30
31
  nice: Object.freeze(["linear", "log", "pow", "sqrt", "symlog", "time"]),
31
32
  zero: Object.freeze(["linear", "pow", "sqrt", "symlog"]),
32
33
  clamp: Object.freeze([
@@ -20,12 +20,21 @@ const ENCODING_PATHS = Object.freeze([
20
20
  "encoding.x.bin.boundaries",
21
21
  "encoding.color.layout",
22
22
  "encoding.color.aggregate",
23
+ "encoding.group.fields",
24
+ "encoding.group.inferredFrom",
25
+ ...["x", "y", "x2", "y2", "theta", "color"].map(channel =>
26
+ `encoding.${channel}.temporalUnit`
27
+ ),
23
28
  "encoding.pathOrder.field",
24
29
  "encoding.pathOrder.fieldType",
25
30
  "encoding.pathOrder.order",
26
31
  "encoding.theta.aggregate",
27
32
  "encoding.theta.weight",
33
+ "encoding.radius.aggregate",
34
+ "encoding.theta.categoryOrder",
28
35
  "encoding.text.format",
36
+ "encoding.text.content",
37
+ "encoding.text.normalizeBy",
29
38
  "encoding.parallel.dimensions",
30
39
  "encoding.parallel.key",
31
40
  "encoding.parallel.missing",
@@ -45,7 +54,7 @@ const ENTITY_PATHS = Object.freeze({
45
54
  collection: "layers",
46
55
  removableContainers: new Set(
47
56
  [...ENCODING_CHANNELS.map(channel => `encoding.${channel}`),
48
- "encoding.parallel"]
57
+ "encoding.parallel", "layout"]
49
58
  ),
50
59
  properties: new Set([
51
60
  "data",
@@ -53,6 +62,8 @@ const ENTITY_PATHS = Object.freeze({
53
62
  "coordinate",
54
63
  "transform",
55
64
  "mark.type",
65
+ "mark.missing",
66
+ "layout.mode",
56
67
  ...ENCODING_PATHS
57
68
  ])
58
69
  },
@@ -60,7 +71,7 @@ const ENTITY_PATHS = Object.freeze({
60
71
  collection: "scales",
61
72
  properties: new Set([
62
73
  "type", "domain", "range", "nice", "zero", "clamp", "reverse",
63
- "unknown", "base", "exponent", "constant", "interpolate",
74
+ "unknown", "base", "exponent", "constant", "interpolate", "radialMapping", "midpoint",
64
75
  "paddingInner", "paddingOuter", "padding", "align"
65
76
  ])
66
77
  },
@@ -86,8 +97,10 @@ const GUIDE_PATHS = new Set([
86
97
  "axis.parallel.target",
87
98
  "axis.parallel.coordinate",
88
99
  "axis.parallel.scales",
100
+ "axis.parallel.titles",
89
101
  "legend.color.scale",
90
102
  "legend.color.title",
103
+ "legend.color.order",
91
104
  "legend.size.scale",
92
105
  "legend.size.title",
93
106
  "legend.strokeWidth.scale",
@@ -97,6 +110,7 @@ const GUIDE_PATHS = new Set([
97
110
  "legend.series.channels",
98
111
  "legend.series.scales",
99
112
  "legend.series.title",
113
+ "legend.series.order",
100
114
  "grid.horizontal.scale",
101
115
  "grid.horizontal.coordinate",
102
116
  "grid.vertical.scale",
@@ -137,7 +151,7 @@ export function parseSemanticPath(property, { allowContainer = false } = {}) {
137
151
 
138
152
  if (allowContainer) {
139
153
  const entityMatch = property.match(
140
- new RegExp(`^(dataset|layer)\\[(${USER_ID_SOURCE})\\]$`)
154
+ new RegExp(`^(dataset|layer|scale)\\[(${USER_ID_SOURCE})\\]$`)
141
155
  );
142
156
  if (entityMatch) {
143
157
  const [, kind, id] = entityMatch;