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,17 @@
1
- import { POSITION_CHANNELS } from "../core/vocabulary.js";
2
1
  import {
2
+ OFFSET_POSITION_CHANNELS,
3
+ POSITION_CHANNELS
4
+ } from "../core/vocabulary.js";
5
+ import {
6
+ mapOrdinalOffsetValues,
3
7
  mapOrdinalPositionValues,
4
- readNominalField,
5
- readQuantitativeField,
6
- readScaleField,
7
- readTemporalField
8
+ readScaleField
8
9
  } from "../grammar/scales/index.js";
10
+ import {
11
+ finiteMidpoint,
12
+ requireFiniteResult
13
+ } from "../grammar/numeric.js";
14
+ import { normalizePositionDatum } from "../grammar/positionDatum.js";
9
15
  import { mapScaleConsumerValues } from "./scales/map.js";
10
16
 
11
17
  export function resolveRowEncodingValues(program, layer, dataset, channel) {
@@ -18,16 +24,66 @@ export function resolveRowEncodingValues(program, layer, dataset, channel) {
18
24
  );
19
25
  }
20
26
  const categorical = ["nominal", "ordinal"].includes(encoding.fieldType);
21
- const values = Object.hasOwn(scale, "unknown")
22
- ? readScaleField(dataset.values, encoding.field, encoding.fieldType, {
23
- allowUnknown: true
24
- })
25
- : categorical
26
- ? readNominalField(dataset.values, encoding.field)
27
- : encoding.fieldType === "temporal"
28
- ? readTemporalField(dataset.values, encoding.field)
29
- : readQuantitativeField(dataset.values, encoding.field);
27
+ const values = Object.hasOwn(encoding, "datum")
28
+ ? Array(dataset.values.length).fill(normalizePositionDatum(
29
+ encoding.datum,
30
+ encoding.fieldType,
31
+ channel,
32
+ encoding.temporalUnit,
33
+ layer.mark.type[0].toUpperCase() + layer.mark.type.slice(1)
34
+ ))
35
+ : readScaleField(dataset.values, encoding.field, encoding.fieldType, {
36
+ allowUnknown: Object.hasOwn(scale, "unknown"), temporalUnit: encoding.temporalUnit
37
+ });
38
+ if (categorical && OFFSET_POSITION_CHANNELS.includes(channel)) {
39
+ return mapOrdinalOffsetValues(values, scale);
40
+ }
30
41
  return categorical && POSITION_CHANNELS.includes(channel)
31
42
  ? mapOrdinalPositionValues(values, scale)
32
43
  : mapScaleConsumerValues(values, scale, channel);
33
44
  }
45
+
46
+ export function applyOffsetPositionValues({
47
+ base,
48
+ offset,
49
+ parentScale,
50
+ offsetScale,
51
+ channel
52
+ }) {
53
+ if (base.length !== offset.length) {
54
+ throw new Error(`${channel} offset values must match the base position grain.`);
55
+ }
56
+ const midpoint = finiteMidpoint(offsetScale.range[0], offsetScale.range[1]);
57
+ const direction = Math.sign(parentScale.step) ||
58
+ Math.sign(parentScale.range[1] - parentScale.range[0]) || 1;
59
+ return Object.freeze(base.map((value, index) => {
60
+ if (!Number.isFinite(value) || !Number.isFinite(offset[index])) {
61
+ return undefined;
62
+ }
63
+ return requireFiniteResult(
64
+ value + direction * (offset[index] - midpoint),
65
+ `${channel} offset position`
66
+ );
67
+ }));
68
+ }
69
+
70
+ export function resolveRowPositionValues(program, layer, dataset, channel) {
71
+ const base = resolveRowEncodingValues(program, layer, dataset, channel);
72
+ if (base === undefined) return undefined;
73
+ const offsetChannel = channel === "x" ? "xOffset" : "yOffset";
74
+ const offsetEncoding = layer.encoding?.[offsetChannel];
75
+ if (offsetEncoding === undefined) return base;
76
+ const offset = resolveRowEncodingValues(
77
+ program,
78
+ layer,
79
+ dataset,
80
+ offsetChannel
81
+ );
82
+ return applyOffsetPositionValues({
83
+ base,
84
+ offset,
85
+ parentScale: program.resolvedScales[layer.encoding[channel].scale],
86
+ offsetScale: program.resolvedScales[offsetEncoding.scale],
87
+ channel
88
+ });
89
+ }
@@ -1,3 +1,4 @@
1
+ import { isSourceOwnedText } from "../grammar/text.js";
1
2
  import { POSITION_ENCODING_CHANNELS } from "../core/vocabulary.js";
2
3
  import { materializedLegendUsesScale } from "./legends.js";
3
4
  import { buildMaterializationPlan } from "./planner.js";
@@ -19,10 +20,12 @@ const GRIDS = Object.freeze([
19
20
 
20
21
  function usesPositionalScale(program, id) {
21
22
  return program.semanticSpec.layers.some(layer =>
22
- POSITION_ENCODING_CHANNELS.some(
23
- channel => layer.encoding?.[channel]?.scale === id
24
- ) || (layer.encoding?.parallel?.dimensions ?? []).some(
25
- dimension => dimension.scale === id
23
+ !isSourceOwnedText(layer) && (
24
+ POSITION_ENCODING_CHANNELS.some(
25
+ channel => layer.encoding?.[channel]?.scale === id
26
+ ) || (layer.encoding?.parallel?.dimensions ?? []).some(
27
+ dimension => dimension.scale === id
28
+ )
26
29
  )
27
30
  );
28
31
  }
@@ -50,7 +53,7 @@ export function needsCanvasScaleRematerialization(program, scale) {
50
53
  (scale.range === "auto" ||
51
54
  usesRadialScale(program, scale.id) ||
52
55
  semanticGuideUsesScale(program, scale.id)) &&
53
- program.resolvedScales[scale.id] !== undefined &&
56
+ (program.resolvedScales[scale.id] !== undefined || scale.radialMapping !== undefined) &&
54
57
  usesPositionalScale(program, scale.id)
55
58
  );
56
59
  }
@@ -19,6 +19,7 @@ export function mapScaleConsumerValues(values, resolvedScale, channel) {
19
19
  resolvedScale.range,
20
20
  {
21
21
  interpolation: resolvedScale.interpolate,
22
+ midpoint: resolvedScale.midpoint,
22
23
  clamp: resolvedScale.clamp ?? false,
23
24
  ...(Object.hasOwn(resolvedScale, "unknown")
24
25
  ? { unknown: resolvedScale.unknown }
@@ -1,3 +1,4 @@
1
+ import { validateMeasuredRadiusScale } from "../../../grammar/scales/radial.js";
1
2
  export function resolveArcAutoPositionRange({
2
3
  consumers,
3
4
  scale,
@@ -35,3 +36,44 @@ export function resolveArcAutoPositionRange({
35
36
  }
36
37
  return range;
37
38
  }
39
+
40
+
41
+ export function resolveMeasuredRadiusDomain({ scale, channel, allValues }) {
42
+ if (scale.radialMapping === undefined) return undefined;
43
+ if (channel !== "radius") throw new Error("Measured radius scales support only radius consumers.");
44
+ const maximum = allValues.reduce((maximum, value) => Math.max(maximum, value), -Infinity);
45
+ if (!Number.isFinite(maximum) || maximum <= 0 || allValues.some(value => value < 0)) {
46
+ throw new Error("Measured radius requires finite non-negative aggregates and a positive maximum.");
47
+ }
48
+ const domain = scale.domain === "auto" ? [0, maximum] : scale.domain;
49
+ if (Array.isArray(domain) && domain[1] < maximum) {
50
+ throw new RangeError("Measured radius domain maximum must cover every category aggregate.");
51
+ }
52
+ return domain;
53
+ }
54
+
55
+ export function validateMeasuredRadiusConsumers({ scale, domain, range, consumers, markConfigs, thetaScales }) {
56
+ if (scale.radialMapping === undefined) return;
57
+ validateMeasuredRadiusScale({ ...scale, domain, range }, { allowAuto: true });
58
+ for (const consumer of consumers) {
59
+ const { layer, encoding } = consumer;
60
+ const theta = layer.encoding?.theta;
61
+ const thetaScale = thetaScales[layer.id];
62
+ if (!isPendingMeasuredRadiusConsumer(consumer) && (layer.mark?.type !== "arc" || !["count", "sum"].includes(encoding.aggregate) ||
63
+ !["nominal", "ordinal"].includes(theta?.fieldType) || theta.aggregate !== undefined ||
64
+ thetaScale?.type !== "band" || (thetaScale.paddingInner ?? 0) !== 0 || (thetaScale.paddingOuter ?? 0) !== 0)) {
65
+ throw new Error("Measured radius requires only equal-angle categorical Arc consumers.");
66
+ }
67
+ const config = markConfigs[layer.id] ?? {};
68
+ if ((config.padAngle ?? 0) !== 0) throw new Error("Measured Arc requires padAngle 0.");
69
+ if (scale.range !== "auto" && config.innerRadiusExplicit === true &&
70
+ Math.abs(config.innerRadius - range[0] / range[1]) > Number.EPSILON * 8) {
71
+ throw new Error("Measured Arc innerRadius must agree with its explicit radius range.");
72
+ }
73
+ }
74
+ }
75
+
76
+ export function isPendingMeasuredRadiusConsumer({ layer, channel, encoding }) {
77
+ return layer.mark?.type === "arc" && channel === "radius" &&
78
+ ["count", "sum"].includes(encoding.aggregate) && layer.encoding?.theta === undefined;
79
+ }
@@ -1,4 +1,3 @@
1
- import { normalizeOffsetPadding } from "../../../grammar/bars/geometry.js";
2
1
  import {
3
2
  BAR_GRAINS,
4
3
  resolveBarChannels,
@@ -8,48 +7,6 @@ import {
8
7
  interpolateNumber,
9
8
  inverseLerp
10
9
  } from "../../../grammar/numeric.js";
11
- export function resolveOffsetScalePolicy({
12
- consumers,
13
- resolvedScales,
14
- markConfigs,
15
- id,
16
- channel
17
- }) {
18
- const parentChannel = channel === "xOffset" ? "x" : "y";
19
- const bandwidths = consumers.map(consumer => {
20
- if (parentChannel === "x" && consumer.layer.encoding?.x?.bin !== undefined) {
21
- return 1;
22
- }
23
- const parentScaleId = consumer.layer.encoding?.[parentChannel]?.scale;
24
- return resolvedScales[parentScaleId]?.bandwidth;
25
- });
26
- if (
27
- bandwidths.some(value => !Number.isFinite(value)) ||
28
- new Set(bandwidths).size !== 1
29
- ) {
30
- throw new Error(
31
- `${channel} scale "${id}" requires one shared resolved ${parentChannel} bandwidth.`
32
- );
33
- }
34
- const paddings = consumers.map(consumer => normalizeOffsetPadding(
35
- markConfigs[consumer.layer.id]?.[channel],
36
- undefined,
37
- channel
38
- ));
39
- const signatures = new Set(
40
- paddings.map(padding => JSON.stringify(padding))
41
- );
42
- if (signatures.size !== 1) {
43
- throw new Error(
44
- `${channel} scale "${id}" requires one shared padding policy.`
45
- );
46
- }
47
- return {
48
- parentBandwidth: bandwidths[0],
49
- ...paddings[0]
50
- };
51
- }
52
-
53
10
  export function resolveTemporalBarBand(valuesByConsumer, domain, range) {
54
11
  const temporalBars = valuesByConsumer.filter(({ consumer }) => {
55
12
  const channels = resolveBarChannels(consumer.layer);
@@ -1,11 +1,11 @@
1
+ import { isSourceOwnedText } from "../../../grammar/text.js";
1
2
  import { resolveHistogramBins } from "../../../grammar/histogram.js";
2
3
 
3
4
  export function resolveBinnedPositionDomain({
4
5
  valuesByConsumer,
5
6
  channel,
6
7
  scale,
7
- id,
8
- allValues
8
+ id
9
9
  }) {
10
10
  const binnedPositions = valuesByConsumer.filter(
11
11
  ({ consumer }) =>
@@ -13,7 +13,10 @@ export function resolveBinnedPositionDomain({
13
13
  consumer.encoding.bin !== undefined
14
14
  );
15
15
  if (binnedPositions.length === 0) return undefined;
16
- if (channel !== "x" || binnedPositions.length !== valuesByConsumer.length) {
16
+ const independentConsumers = valuesByConsumer.filter(({ consumer }) =>
17
+ !isSourceOwnedText(consumer.layer)
18
+ );
19
+ if (channel !== "x" || binnedPositions.length !== independentConsumers.length) {
17
20
  throw new Error(
18
21
  `Binned scale "${id}" cannot be shared with an unbinned consumer.`
19
22
  );
@@ -25,7 +28,7 @@ export function resolveBinnedPositionDomain({
25
28
  throw new Error(`Binned scale "${id}" requires one shared bin definition.`);
26
29
  }
27
30
  return resolveHistogramBins({
28
- values: allValues,
31
+ values: binnedPositions.flatMap(item => item.values).filter(value => value !== undefined),
29
32
  bin: binnedPositions[0].consumer.encoding.bin,
30
33
  domain: scale.domain,
31
34
  nice: scale.nice ?? true,
@@ -0,0 +1,54 @@
1
+ import { normalizeOffsetPadding } from "../../../grammar/bars/geometry.js";
2
+
3
+ function resolveParentSlot(consumer, resolvedScales, parentChannel) {
4
+ if (parentChannel === "x" && consumer.layer.encoding?.x?.bin !== undefined) {
5
+ return 1;
6
+ }
7
+ const parentScaleId = consumer.layer.encoding?.[parentChannel]?.scale;
8
+ const parentScale = resolvedScales[parentScaleId];
9
+ if (Number.isFinite(parentScale?.bandwidth) && parentScale.bandwidth > 0) {
10
+ return parentScale.bandwidth;
11
+ }
12
+ if (parentScale?.type === "point" && Number.isFinite(parentScale.step)) {
13
+ return Math.abs(parentScale.step);
14
+ }
15
+ return undefined;
16
+ }
17
+
18
+ export function resolveOffsetScalePolicy({
19
+ consumers,
20
+ resolvedScales,
21
+ markConfigs,
22
+ id,
23
+ channel
24
+ }) {
25
+ const parentChannel = channel === "xOffset" ? "x" : "y";
26
+ const slots = consumers.map(consumer =>
27
+ resolveParentSlot(consumer, resolvedScales, parentChannel)
28
+ );
29
+ if (
30
+ slots.some(value => !Number.isFinite(value) || value <= 0) ||
31
+ new Set(slots).size !== 1
32
+ ) {
33
+ throw new Error(
34
+ `${channel} scale "${id}" requires one shared resolved ${parentChannel} categorical slot.`
35
+ );
36
+ }
37
+ const paddings = consumers.map(consumer => normalizeOffsetPadding(
38
+ markConfigs[consumer.layer.id]?.[channel],
39
+ undefined,
40
+ channel
41
+ ));
42
+ const signatures = new Set(
43
+ paddings.map(padding => JSON.stringify(padding))
44
+ );
45
+ if (signatures.size !== 1) {
46
+ throw new Error(
47
+ `${channel} scale "${id}" requires one shared padding policy.`
48
+ );
49
+ }
50
+ return {
51
+ parentBandwidth: slots[0],
52
+ ...paddings[0]
53
+ };
54
+ }
@@ -1,3 +1,4 @@
1
+ import { isSourceOwnedText } from "../../../grammar/text.js";
1
2
  import {
2
3
  isTransformedScaleType,
3
4
  resolveContinuousDomain,
@@ -21,7 +22,7 @@ export function resolveSeriesLayoutDomain({
21
22
  (_, index) => seriesLayouts[index] !== undefined
22
23
  );
23
24
  const directConsumers = valuesByConsumer.filter(
24
- (_, index) => seriesLayouts[index] === undefined
25
+ ({ consumer }, index) => seriesLayouts[index] === undefined && !isSourceOwnedText(consumer.layer)
25
26
  );
26
27
  const compatibleDirect = directConsumers.every(({ consumer }) =>
27
28
  layoutConsumers.every(({ consumer: layoutConsumer }) => {
@@ -1,3 +1,4 @@
1
+ import { validateContinuousColorConsumer } from "../../grammar/scales/colorConsumers.js";
1
2
  import {
2
3
  isContinuousColorScaleType,
3
4
  isDiscretePositionScaleType,
@@ -21,16 +22,15 @@ import {
21
22
  resolveStrokeWidthRange,
22
23
  resolveTransformedDomain,
23
24
  SCALE_ROLES,
25
+ validateSequentialMidpoint,
24
26
  validateLinearScaleType,
25
27
  validateOrdinalScaleType,
26
28
  validateScaleTypeForRole,
27
29
  validateTimeScaleType
28
30
  } from "../../grammar/scales/index.js";
29
- import { resolveArcAutoPositionRange } from "./policies/arc.js";
30
- import {
31
- resolveOffsetScalePolicy,
32
- resolveTemporalBarBand
33
- } from "./policies/bar.js";
31
+ import { resolveArcAutoPositionRange, resolveMeasuredRadiusDomain, validateMeasuredRadiusConsumers } from "./policies/arc.js";
32
+ import { resolveTemporalBarBand } from "./policies/bar.js";
33
+ import { resolveOffsetScalePolicy } from "./policies/offset.js";
34
34
  import { resolveBinnedPositionDomain } from "./policies/binnedPosition.js";
35
35
  import { resolveSeriesLayoutDomain } from "./policies/series.js";
36
36
  import { OFFSET_POSITION_CHANNELS } from "../../core/vocabulary.js";
@@ -141,7 +141,9 @@ function resolveContinuousScale({
141
141
  : validateLinearScaleType(scale.type),
142
142
  domain,
143
143
  range,
144
+ ...(scale.midpoint === undefined ? {} : { midpoint: scale.midpoint }),
144
145
  ...(scale.clamp === undefined ? {} : { clamp: scale.clamp }),
146
+ ...(scale.radialMapping === undefined ? {} : { radialMapping: scale.radialMapping }),
145
147
  ...(scale.type === "log"
146
148
  ? {
147
149
  base: normalizeTransformParameters("log", {
@@ -184,19 +186,38 @@ function reverseResolvedScale(scale) {
184
186
  };
185
187
  }
186
188
 
187
- export function resolveScaleMaterialization({
188
- id,
189
- scale,
190
- channel,
191
- consumers,
192
- valuesByConsumer,
193
- bounds,
194
- resolvedScales,
195
- markConfigs
196
- }) {
189
+ export function resolveScaleMaterialization(options) {
190
+ const {
191
+ id,
192
+ scale,
193
+ channel,
194
+ consumers,
195
+ valuesByConsumer,
196
+ bounds,
197
+ resolvedScales,
198
+ markConfigs,
199
+ thetaScales
200
+ } = options;
197
201
  const allValues = valuesByConsumer
198
202
  .flatMap(item => item.values)
199
203
  .filter(value => value !== undefined);
204
+ const preserved = resolvedScales[id]?.domain;
205
+ if (
206
+ scale.domain === "auto" &&
207
+ Array.isArray(preserved) &&
208
+ consumers.some(({ layer }) =>
209
+ markConfigs?.[layer.id]?.markFilter?.empty === true
210
+ )
211
+ ) {
212
+ return resolveScaleMaterialization({
213
+ ...options,
214
+ scale: { ...scale, domain: preserved },
215
+ valuesByConsumer: valuesByConsumer.map(item => ({
216
+ ...item,
217
+ categoryOrder: undefined
218
+ }))
219
+ });
220
+ }
200
221
  const isSequentialColor = channel === "color" &&
201
222
  isContinuousColorScaleType(scale.type);
202
223
  const isDiscretizedColor = channel === "color" &&
@@ -212,6 +233,9 @@ export function resolveScaleMaterialization({
212
233
  !isOrdinalOffset &&
213
234
  isDiscretePositionScaleType(scale.type);
214
235
 
236
+ if (isSequentialColor || isDiscretizedColor) {
237
+ for (const consumer of consumers) validateContinuousColorConsumer(consumer.layer, consumer.encoding, scale);
238
+ }
215
239
  let discretizedScale;
216
240
  if (isDiscretizedColor) {
217
241
  discretizedScale = resolveDiscretizedColorScale({
@@ -227,8 +251,7 @@ export function resolveScaleMaterialization({
227
251
  valuesByConsumer,
228
252
  channel,
229
253
  scale,
230
- id,
231
- allValues
254
+ id
232
255
  });
233
256
  const seriesLayouts = valuesByConsumer.map(item => item.seriesLayout);
234
257
  const seriesDomain = isDiscretizedColor || binnedDomain !== undefined
@@ -249,7 +272,7 @@ export function resolveScaleMaterialization({
249
272
  }
250
273
  const domain = isDiscretizedColor
251
274
  ? discretizedScale.domain
252
- : categoryOrderDomain ?? binnedDomain ?? seriesDomain ?? resolveDefaultDomain({
275
+ : resolveMeasuredRadiusDomain({ scale, channel, allValues }) ?? categoryOrderDomain ?? binnedDomain ?? seriesDomain ?? resolveDefaultDomain({
253
276
  scale,
254
277
  allValues,
255
278
  isOrdinalAppearance,
@@ -269,6 +292,13 @@ export function resolveScaleMaterialization({
269
292
  isOrdinalOffset,
270
293
  discretizedScale
271
294
  });
295
+ validateSequentialMidpoint(scale.midpoint, scale.type, domain);
296
+ if (scale.midpoint !== undefined && (channel !== "color" || consumers.some(
297
+ consumer => consumer.encoding.fieldType !== "quantitative"
298
+ ))) {
299
+ throw new Error("Scale midpoint requires quantitative color consumers.");
300
+ }
301
+ validateMeasuredRadiusConsumers({ scale, domain, range, consumers, markConfigs, thetaScales });
272
302
  if (channel === "shape" && domain.length > range.length) {
273
303
  throw new Error(
274
304
  `Shape scale "${id}" requires at least one distinct shape per domain value.`
@@ -0,0 +1,11 @@
1
+ export function assertEncodingSelectionCompatibility(program, target, channels) {
2
+ for (const [id, selection] of Object.entries(
3
+ program.materializationConfigs.selections ?? {}
4
+ )) {
5
+ if (selection.target === target && channels.includes(selection.selector?.channel)) {
6
+ throw new Error(
7
+ `Cannot remove ${selection.selector.channel} encoding while selection "${id}" references that channel.`
8
+ );
9
+ }
10
+ }
11
+ }
@@ -1,4 +1,5 @@
1
- import { deriveBarAggregates } from "../../../grammar/bars/aggregate.js";
1
+ import { deriveSeriesBarCells } from "../../../grammar/bars/aggregate.js";
2
+ import { readSeriesIdentity } from "../../../grammar/pathSeries.js";
2
3
  import {
3
4
  BAR_GRAINS,
4
5
  resolveBarChannels,
@@ -7,7 +8,6 @@ import {
7
8
  resolveBarGrain
8
9
  } from "../../../grammar/bars/policy.js";
9
10
  import { numericExtent } from "../../../grammar/numeric.js";
10
- import { layoutSeriesPartition } from "../../../grammar/seriesLayout.js";
11
11
  import {
12
12
  readNominalField,
13
13
  readTemporalField
@@ -24,7 +24,7 @@ import {
24
24
 
25
25
  function categoryValues(rows, encoding) {
26
26
  return encoding.fieldType === "temporal"
27
- ? readTemporalField(rows, encoding.field)
27
+ ? readTemporalField(rows, encoding.field, encoding.temporalUnit)
28
28
  : readNominalField(rows, encoding.field);
29
29
  }
30
30
 
@@ -36,20 +36,17 @@ function aggregateMembers(rows, layer, cell, channels) {
36
36
  const colors = !["nominal", "ordinal"].includes(color?.fieldType)
37
37
  ? undefined
38
38
  : readNominalField(rows, color.field);
39
+ const identity = layer.encoding?.group === undefined ? undefined : readSeriesIdentity(rows, layer);
39
40
  return rows.filter((_, index) =>
40
41
  categories[index] === categoryValue &&
41
- (colors === undefined || colors[index] === cell.color)
42
+ (identity === undefined ? colors === undefined || colors[index] === cell.color : identity.values[index] === cell.series)
42
43
  );
43
44
  }
44
45
 
45
46
  function aggregateCellDefinitions(program, layer, dataset) {
46
47
  const channels = resolveBarChannels(layer);
47
- const derived = deriveBarAggregates(dataset.values, layer).values;
48
48
  const categoryEncoding = layer.encoding[channels.category];
49
49
  const categoryScale = program.resolvedScales[categoryEncoding.scale];
50
- const measureScale = program.resolvedScales[
51
- layer.encoding[channels.measure].scale
52
- ];
53
50
  const colorEncoding = layer.encoding?.color;
54
51
  const colorScale = program.resolvedScales[colorEncoding?.scale];
55
52
  const offsetChannel = resolveBarOffsetChannel(layer);
@@ -67,70 +64,17 @@ function aggregateCellDefinitions(program, layer, dataset) {
67
64
  ...(cell.color === undefined ? {} : { color: cell.color }),
68
65
  ...(layer.encoding?.[offsetChannel] === undefined
69
66
  ? {}
70
- : { [offsetChannel]: cell.color })
67
+ : { [offsetChannel]: cell.series ?? cell.color })
71
68
  },
72
69
  members
73
70
  };
74
71
  }
75
72
 
76
- if (colorEncoding?.fieldType === "quantitative") {
77
- const cells = new Map(derived.map(cell => [
78
- cell[channels.category],
79
- cell
80
- ]));
81
- const categories = ["ordinal", "band", "point"].includes(categoryScale.type)
82
- ? categoryScale.domain
83
- : [...new Set(derived.map(cell => cell[channels.category]))]
84
- .sort((left, right) => left - right);
85
- return categories.flatMap(category => {
86
- const cell = cells.get(category);
87
- return cell === undefined
88
- ? []
89
- : [definition(cell, measureScale.domain[0], cell[channels.measure])];
90
- });
91
- }
92
-
93
- if (layout === "group" && offsetScale !== undefined) {
94
- const categoryIndex = new Map(
95
- categoryScale.domain.map((value, index) => [value, index])
96
- );
97
- const offsetIndex = new Map(
98
- offsetScale.domain.map((value, index) => [value, index])
99
- );
100
- const cells = [...derived].sort((left, right) =>
101
- categoryIndex.get(left[channels.category]) -
102
- categoryIndex.get(right[channels.category]) ||
103
- offsetIndex.get(left.color) - offsetIndex.get(right.color)
104
- );
105
- return cells.map(cell => definition(
106
- cell,
107
- measureScale.domain[0],
108
- cell[channels.measure]
109
- ));
110
- }
111
-
112
- const categories = ["ordinal", "band", "point"].includes(categoryScale.type)
113
- ? categoryScale.domain
114
- : [...new Set(derived.map(cell => cell[channels.category]))]
115
- .sort((left, right) => left - right);
116
- const series = colorScale?.domain ?? [undefined];
117
- const lookup = new Map(derived.map(cell => [
118
- JSON.stringify([cell[channels.category], cell.color]),
119
- cell
120
- ]));
121
- const baseline = layout === "overlay" ? measureScale.domain[0] : 0;
122
- return categories.flatMap(category => {
123
- const cells = series.map(color =>
124
- lookup.get(JSON.stringify([category, color]))
125
- );
126
- const values = cells.map(cell => cell?.[channels.measure] ?? 0);
127
- return layoutSeriesPartition(values, layout, { baseline }).flatMap(segment => {
128
- const cell = cells[segment.index];
129
- return cell === undefined
130
- ? []
131
- : [definition(cell, segment.start, segment.end)];
132
- });
133
- });
73
+ const categories = categoryScale.type === "time" ? undefined : categoryScale.domain;
74
+ const series = layout === "group" ? offsetScale?.domain
75
+ : layer.encoding.group === undefined && colorEncoding?.fieldType !== "quantitative" ? colorScale?.domain : undefined;
76
+ return deriveSeriesBarCells(dataset.values, layer, categories, series)
77
+ .map(({ cell, start, end }) => definition(cell, start, end));
134
78
  }
135
79
 
136
80
  function histogramDefinitions(program, layer, dataset) {
@@ -145,7 +89,7 @@ function histogramDefinitions(program, layer, dataset) {
145
89
  });
146
90
  const colorScale = program.resolvedScales[colorEncoding?.scale];
147
91
  return segments.map(segment => {
148
- const colorValue = colorScale?.domain[segment.category];
92
+ const colorValue = segment.colorValue ?? colorScale?.domain[segment.category];
149
93
  return {
150
94
  fields: uniqueFields(segment.members),
151
95
  channels: {
@@ -1,3 +1,4 @@
1
+ import { normalizeTemporalValue } from "../../../grammar/scales/fields.js";
1
2
  import { cloneAndFreeze, isPlainObject } from "../../../core/immutable.js";
2
3
  import {
3
4
  MARK_GRAPHIC_PROPERTIES,
@@ -16,8 +17,13 @@ function graphicId(layer, index) {
16
17
 
17
18
  function encodingValue(row, encoding) {
18
19
  if (encoding === undefined) return undefined;
19
- if (Object.hasOwn(encoding, "field")) return row?.[encoding.field];
20
- return encoding.datum;
20
+ const value = Object.hasOwn(encoding, "field") ? row?.[encoding.field] : encoding.datum;
21
+ if (encoding.fieldType !== "temporal") return value;
22
+ try {
23
+ return normalizeTemporalValue(value, encoding.field, 0, encoding.temporalUnit);
24
+ } catch {
25
+ return undefined;
26
+ }
21
27
  }
22
28
 
23
29
  export function channelMapFromRow(row, layer) {
@@ -25,20 +25,10 @@ function rowsForSeries(rows, key) {
25
25
 
26
26
  function seriesDefinitions(layer, rows, series) {
27
27
  return series.map(item => {
28
- const members = rowsForSeries(rows, item.key);
28
+ const members = item.sourceIndices === undefined ? rowsForSeries(rows, item.key)
29
+ : item.sourceIndices.map(index => rows[index]);
29
30
  const fields = { ...uniqueFields(members), ...item.key };
30
- const channels = Object.fromEntries(
31
- Object.entries(layer.encoding ?? {}).flatMap(([channel, encoding]) => {
32
- if (encoding.field === undefined) {
33
- return Object.hasOwn(encoding, "datum")
34
- ? [[channel, encoding.datum]]
35
- : [];
36
- }
37
- return Object.hasOwn(fields, encoding.field)
38
- ? [[channel, fields[encoding.field]]]
39
- : [];
40
- })
41
- );
31
+ const channels = channelMapFromRow(fields, layer);
42
32
  return { fields, channels, members };
43
33
  });
44
34
  }