ggaction 0.0.9 → 0.0.11

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 (222) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/README.md +5 -4
  3. package/knowledge/action-card.schema.json +1 -0
  4. package/knowledge/action-cards.json +1 -10941
  5. package/knowledge/action-cards.schema.json +1 -0
  6. package/knowledge/extension-authoring.md +248 -0
  7. package/knowledge/intent-taxonomy.json +1 -183
  8. package/knowledge/intent-taxonomy.schema.json +1 -0
  9. package/knowledge/mcp-resources.json +1 -95
  10. package/knowledge/mcp-resources.schema.json +1 -0
  11. package/knowledge/task-packet.schema.json +1 -159
  12. package/knowledge/task-resolver.js +1 -1230
  13. package/package.json +19 -2
  14. package/src/actions/boxPlots/edit.js +45 -298
  15. package/src/actions/boxPlots/options.js +46 -77
  16. package/src/actions/boxPlots/resolve.js +1 -1
  17. package/src/actions/charts/bar.js +3 -0
  18. package/src/actions/charts/heatmap.js +73 -83
  19. package/src/actions/charts/index.js +1 -1
  20. package/src/actions/charts/parallel.js +35 -1
  21. package/src/actions/charts/shared.js +20 -0
  22. package/src/actions/composition/actions.js +46 -49
  23. package/src/actions/data/bin2d.js +105 -2
  24. package/src/actions/data/box.js +17 -17
  25. package/src/actions/data/density.js +42 -78
  26. package/src/actions/data/gradientProfile.js +22 -52
  27. package/src/actions/data/horizon.js +16 -41
  28. package/src/actions/data/index.js +10 -10
  29. package/src/actions/data/interval.js +16 -38
  30. package/src/actions/data/intervalEdit.js +55 -0
  31. package/src/actions/data/regression.js +7 -17
  32. package/src/actions/data/shared.js +47 -0
  33. package/src/actions/data/timeUnit.js +14 -38
  34. package/src/actions/data/window.js +14 -38
  35. package/src/actions/distributions/revision.js +154 -34
  36. package/src/actions/encodings/angle.js +9 -10
  37. package/src/actions/encodings/appearance.js +17 -51
  38. package/src/actions/encodings/density/resolve.js +3 -1
  39. package/src/actions/encodings/density.js +1 -15
  40. package/src/actions/encodings/horizon/resolve.js +13 -3
  41. package/src/actions/encodings/horizon.js +8 -7
  42. package/src/actions/encodings/index.js +9 -9
  43. package/src/actions/encodings/offset.js +8 -24
  44. package/src/actions/encodings/parallel.js +9 -10
  45. package/src/actions/encodings/pathOrder.js +6 -13
  46. package/src/actions/encodings/position/policies/index.js +4 -4
  47. package/src/actions/encodings/position/policies/line.js +47 -17
  48. package/src/actions/encodings/position/resolve.js +24 -5
  49. package/src/actions/encodings/ranged.js +18 -23
  50. package/src/actions/encodings/remove.js +15 -25
  51. package/src/actions/encodings/ruleAppearance.js +9 -24
  52. package/src/actions/encodings/shared.js +37 -0
  53. package/src/actions/encodings/strokeDash.js +8 -23
  54. package/src/actions/encodings/text.js +9 -10
  55. package/src/actions/errorBands/create.js +105 -39
  56. package/src/actions/errorBands/edit.js +89 -93
  57. package/src/actions/errorBands/index.js +1 -2
  58. package/src/actions/errorBars/create.js +78 -27
  59. package/src/actions/errorBars/edit.js +81 -48
  60. package/src/actions/errorBars/index.js +1 -2
  61. package/src/actions/facets/actions.js +45 -69
  62. package/src/actions/facets/derive.js +38 -53
  63. package/src/actions/facets/replay.js +0 -1
  64. package/src/actions/gradientPlots/components.js +59 -48
  65. package/src/actions/gradientPlots/edit.js +35 -216
  66. package/src/actions/gradientPlots/index.js +11 -10
  67. package/src/actions/gradientPlots/materialize.js +21 -35
  68. package/src/actions/gradientPlots/options.js +5 -7
  69. package/src/actions/gradientPlots/paint.js +9 -6
  70. package/src/actions/gradientPlots/resolve.js +1 -1
  71. package/src/actions/guides/axes/index.js +3 -3
  72. package/src/actions/guides/axes/labels.js +68 -26
  73. package/src/actions/guides/axes/lines.js +22 -2
  74. package/src/actions/guides/axes/policy.js +28 -8
  75. package/src/actions/guides/axes/ticks.js +27 -9
  76. package/src/actions/guides/axes/titles.js +113 -65
  77. package/src/actions/guides/grids/resolve.js +14 -0
  78. package/src/actions/guides/guides.js +2 -4
  79. package/src/actions/guides/index.js +2 -2
  80. package/src/actions/guides/legends/categorical/actions.js +44 -68
  81. package/src/actions/guides/legends/categorical/components.js +6 -1
  82. package/src/actions/guides/legends/categorical/index.js +2 -2
  83. package/src/actions/guides/legends/categorical/layout.js +148 -175
  84. package/src/actions/guides/legends/categorical/recipes.js +5 -0
  85. package/src/actions/guides/legends/categorical/resolve.js +8 -1
  86. package/src/actions/guides/legends/continuous/common.js +117 -66
  87. package/src/actions/guides/legends/continuous/gradient.js +38 -37
  88. package/src/actions/guides/legends/continuous/interval.js +97 -76
  89. package/src/actions/guides/legends/continuous/opacity.js +28 -38
  90. package/src/actions/guides/legends/edit.js +14 -1
  91. package/src/actions/guides/legends/index.js +1 -1
  92. package/src/actions/guides/legends/lane.js +2 -1
  93. package/src/actions/guides/legends/remove.js +8 -0
  94. package/src/actions/guides/legends/size.js +36 -43
  95. package/src/actions/guides/legends/strokeWidth.js +20 -26
  96. package/src/actions/guides/polar/axes/labels.js +17 -1
  97. package/src/actions/guides/polar/axes/shared.js +7 -1
  98. package/src/actions/guides/polar/axes/titles.js +19 -2
  99. package/src/actions/guides/polar/grids.js +4 -0
  100. package/src/actions/guides/polar/resolve.js +6 -0
  101. package/src/actions/index.js +13 -13
  102. package/src/actions/intervals/resolve.js +87 -117
  103. package/src/actions/marks/arc/actions.js +31 -36
  104. package/src/actions/marks/area/actions.js +23 -31
  105. package/src/actions/marks/index.js +6 -6
  106. package/src/actions/marks/line/actions.js +33 -52
  107. package/src/actions/marks/line/materialize.js +7 -5
  108. package/src/actions/marks/point/materialize.js +5 -3
  109. package/src/actions/marks/remove.js +21 -0
  110. package/src/actions/marks/rule/actions.js +53 -53
  111. package/src/actions/marks/shared.js +8 -4
  112. package/src/actions/marks/text/actions.js +14 -6
  113. package/src/actions/marks/text/layout.js +21 -45
  114. package/src/actions/marks/tick/actions.js +22 -21
  115. package/src/actions/primitives/createGraphics.js +4 -0
  116. package/src/actions/primitives/editGraphics.js +4 -0
  117. package/src/actions/primitives/index.js +3 -3
  118. package/src/actions/primitives/semantic.js +1 -1
  119. package/src/actions/primitives/semanticValidation/index.js +2 -2
  120. package/src/actions/primitives/semanticValidation/layer.js +1 -1
  121. package/src/actions/regression/edit.js +3 -43
  122. package/src/actions/scales/consumers/families.js +24 -4
  123. package/src/actions/scales/consumers/index.js +1 -1
  124. package/src/actions/scales/consumers/seriesLayout.js +17 -0
  125. package/src/actions/scales/definitions.js +100 -103
  126. package/src/actions/scales/materialize.js +5 -0
  127. package/src/actions/selection/actions.js +79 -121
  128. package/src/actions/titles/actions.js +48 -94
  129. package/src/actions/titles/resolve.js +14 -27
  130. package/src/actions/violinPlots/create.js +37 -44
  131. package/src/core/action.js +20 -5
  132. package/src/core/extensionRegistry.js +104 -0
  133. package/src/core/textMetrics.js +74 -37
  134. package/src/core/validation.js +18 -0
  135. package/src/extension.js +8 -1
  136. package/src/grammar/aggregate.js +48 -28
  137. package/src/grammar/arcs.js +58 -11
  138. package/src/grammar/areaSeries.js +14 -16
  139. package/src/grammar/bin2d.js +37 -23
  140. package/src/grammar/boxPlot.js +14 -13
  141. package/src/grammar/categoricalDensity.js +14 -14
  142. package/src/grammar/categoryOrder.js +8 -14
  143. package/src/grammar/curveCommands.js +87 -20
  144. package/src/grammar/density.js +162 -141
  145. package/src/grammar/facets/index.js +14 -0
  146. package/src/grammar/facets/scales.js +7 -4
  147. package/src/grammar/gradientProfile.js +12 -15
  148. package/src/grammar/histogram.js +145 -83
  149. package/src/grammar/horizon.js +60 -23
  150. package/src/grammar/interval.js +14 -13
  151. package/src/grammar/lineSeries.js +91 -61
  152. package/src/grammar/numeric.js +290 -0
  153. package/src/grammar/paint.js +3 -4
  154. package/src/grammar/palettes.js +21 -32
  155. package/src/grammar/pathCommands.js +6 -0
  156. package/src/grammar/polarPaths.js +5 -5
  157. package/src/grammar/regression/derive.js +48 -9
  158. package/src/grammar/regression/models.js +248 -69
  159. package/src/grammar/regression/parameters.js +8 -0
  160. package/src/grammar/scales/color.js +35 -26
  161. package/src/grammar/scales/continuous.js +71 -24
  162. package/src/grammar/scales/definition.js +1 -1
  163. package/src/grammar/scales/discretized.js +42 -12
  164. package/src/grammar/scales/fields.js +21 -4
  165. package/src/grammar/scales/ordinal.js +117 -61
  166. package/src/grammar/scales/temporal.js +40 -11
  167. package/src/grammar/scales/transformed.js +199 -52
  168. package/src/grammar/schemas/graphicBounds.js +270 -79
  169. package/src/grammar/seriesLayout.js +50 -18
  170. package/src/grammar/ticks.js +123 -53
  171. package/src/grammar/timeUnit.js +18 -8
  172. package/src/grammar/transforms.js +4 -1
  173. package/src/grammar/window.js +70 -57
  174. package/src/index.js +1 -1
  175. package/src/layout/canvas.js +4 -8
  176. package/src/layout/composition.js +39 -40
  177. package/src/layout/facets.js +10 -14
  178. package/src/layout/labels.js +91 -15
  179. package/src/layout/legend.js +22 -20
  180. package/src/layout/legendLane.js +54 -40
  181. package/src/layout/title.js +7 -7
  182. package/src/materialization/bars/aggregate.js +14 -40
  183. package/src/materialization/bars/grouped.js +7 -18
  184. package/src/materialization/bars/histogram.js +2 -19
  185. package/src/materialization/bars/ranged.js +20 -15
  186. package/src/materialization/bars/resolve.js +21 -0
  187. package/src/materialization/facetGuides/legacyCategorical.js +2 -1
  188. package/src/materialization/facetGuides/placement.js +34 -19
  189. package/src/materialization/facetGuides/preparation.js +9 -5
  190. package/src/materialization/facets.js +39 -27
  191. package/src/materialization/guides/resources.js +2 -1
  192. package/src/materialization/marks/capabilities.js +5 -2
  193. package/src/materialization/marks/index.js +24 -4
  194. package/src/materialization/scales/policies/bar.js +61 -59
  195. package/src/materialization/scales/policies/binnedPosition.js +34 -0
  196. package/src/materialization/scales/policies/series.js +13 -7
  197. package/src/materialization/scales/resolve.js +4 -5
  198. package/src/materialization/selection/items/arc.js +16 -2
  199. package/src/materialization/selection/items/bar.js +4 -2
  200. package/src/materialization/selection/items/common.js +21 -1
  201. package/src/materialization/selection/items/path.js +19 -2
  202. package/src/materialization/selection/items/point.js +4 -16
  203. package/src/materialization/selection/items/tick.js +13 -21
  204. package/src/materialization/selection/point.js +11 -4
  205. package/src/materialization/selection/styles.js +19 -27
  206. package/src/materialization/text.js +4 -1
  207. package/src/mcp/adapter.js +8 -3
  208. package/src/renderers/canvas/fill.js +2 -2
  209. package/src/renderers/canvas/index.js +35 -2
  210. package/src/renderers/canvas/native.js +160 -0
  211. package/src/renderers/pdf.js +14 -0
  212. package/src/renderers/svg.js +37 -4
  213. package/types/extension.d.ts +19 -0
  214. package/types/index.d.ts +3 -0
  215. package/types/program.d.ts +509 -131
  216. package/types/svg.d.ts +1 -0
  217. package/src/actions/errorBands/components.js +0 -81
  218. package/src/actions/errorBands/options.js +0 -39
  219. package/src/actions/errorBands/resolve.js +0 -28
  220. package/src/actions/errorBars/components.js +0 -62
  221. package/src/actions/errorBars/options.js +0 -58
  222. package/src/actions/errorBars/resolve.js +0 -25
@@ -1,6 +1,10 @@
1
1
  import { mapContinuousScaleValues, mapOrdinalPositionValues, mapOrdinalValues } from "../../grammar/scales/index.js";
2
2
  import { resolveBarWidth } from "../../grammar/bars/geometry.js";
3
- import { DEFAULT_BAR_FILL, DEFAULT_BAR_STROKE, DEFAULT_BAR_STROKE_WIDTH } from "./resolve.js";
3
+ import {
4
+ DEFAULT_BAR_FILL,
5
+ DEFAULT_BAR_STROKE,
6
+ resolveBarAppearance
7
+ } from "./resolve.js";
4
8
 
5
9
  export function deriveRangedRectangles(required, program, width) {
6
10
  const { layer, dataset } = required;
@@ -29,25 +33,26 @@ export function deriveRangedRectangles(required, program, width) {
29
33
  const fills = color === undefined
30
34
  ? dataset.values.map(() => appearance.fill ?? config.fill ?? DEFAULT_BAR_FILL)
31
35
  : mapOrdinalValues(dataset.values.map(row => row[color.field]), program.resolvedScales[color.scale].domain, program.resolvedScales[color.scale].range);
36
+ const appearanceConfig = color === undefined
37
+ ? config
38
+ : { ...config, stroke: undefined };
32
39
  return dataset.values.map((_, index) => vertical ? {
33
40
  x: centers[index] - band / 2, y: Math.min(first[index], second[index]), width: band,
34
41
  height: Math.abs(second[index] - first[index]), fill: fills[index],
35
- stroke: appearance.stroke === false
36
- ? "transparent"
37
- : appearance.stroke ?? (color === undefined ? config.stroke ?? DEFAULT_BAR_STROKE : fills[index]),
38
- strokeWidth: appearance.stroke === false
39
- ? 0
40
- : appearance.strokeWidth ?? config.strokeWidth ?? DEFAULT_BAR_STROKE_WIDTH,
41
- opacity: appearance.opacity ?? config.opacity ?? 1
42
+ ...resolveBarAppearance(
43
+ appearanceConfig,
44
+ undefined,
45
+ color === undefined ? DEFAULT_BAR_STROKE : fills[index],
46
+ 1
47
+ )
42
48
  } : {
43
49
  x: Math.min(first[index], second[index]), y: centers[index] - band / 2,
44
50
  width: Math.abs(second[index] - first[index]), height: band, fill: fills[index],
45
- stroke: appearance.stroke === false
46
- ? "transparent"
47
- : appearance.stroke ?? (color === undefined ? config.stroke ?? DEFAULT_BAR_STROKE : fills[index]),
48
- strokeWidth: appearance.stroke === false
49
- ? 0
50
- : appearance.strokeWidth ?? config.strokeWidth ?? DEFAULT_BAR_STROKE_WIDTH,
51
- opacity: appearance.opacity ?? config.opacity ?? 1
51
+ ...resolveBarAppearance(
52
+ appearanceConfig,
53
+ undefined,
54
+ color === undefined ? DEFAULT_BAR_STROKE : fills[index],
55
+ 1
56
+ )
52
57
  });
53
58
  }
@@ -8,6 +8,27 @@ export const DEFAULT_BAR_FILL = DEFAULT_COLORS.mark;
8
8
  export const DEFAULT_BAR_STROKE = "white";
9
9
  export const DEFAULT_BAR_STROKE_WIDTH = 0.5;
10
10
 
11
+ export function resolveBarAppearance(
12
+ config,
13
+ existing,
14
+ defaultStroke = DEFAULT_BAR_STROKE,
15
+ defaultOpacity
16
+ ) {
17
+ const appearance = config.barAppearance ?? {};
18
+ const opacity = appearance.opacity ?? config.opacity ??
19
+ existing?.opacity ?? defaultOpacity;
20
+ return {
21
+ stroke: appearance.stroke === false
22
+ ? "transparent"
23
+ : appearance.stroke ?? config.stroke ?? existing?.stroke ?? defaultStroke,
24
+ strokeWidth: appearance.stroke === false
25
+ ? 0
26
+ : appearance.strokeWidth ?? config.strokeWidth ??
27
+ existing?.strokeWidth ?? DEFAULT_BAR_STROKE_WIDTH,
28
+ ...(opacity === undefined ? {} : { opacity })
29
+ };
30
+ }
31
+
11
32
  export function requireCompleteBar(program, id) {
12
33
  const layer = findLayer(program, id);
13
34
 
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  resolveSharedFacetLegends
3
3
  } from "../../grammar/facets/guides.js";
4
+ import { formatVisibleText } from "../../core/textMetrics.js";
4
5
  import { mapOrdinalValues } from "../../grammar/scales/index.js";
5
6
  import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from "../../theme/defaults.js";
6
7
 
@@ -10,7 +11,7 @@ function textItem(text, x, y, options = {}) {
10
11
  properties: {
11
12
  x,
12
13
  y,
13
- text: String(text),
14
+ text: formatVisibleText(text),
14
15
  fill: options.color ?? DEFAULT_COLORS.strongText,
15
16
  fontSize: options.fontSize ?? 12,
16
17
  fontFamily: options.fontFamily ?? DEFAULT_FONT_FAMILY,
@@ -1,4 +1,6 @@
1
1
  import { isPlainObject } from "../../core/immutable.js";
2
+ import { resolveConcreteGraphicBounds } from
3
+ "../../grammar/schemas/graphicBounds.js";
2
4
  import { planFacetGuideOwnership } from "../../grammar/facets/guides.js";
3
5
  import {
4
6
  axisGraphicIds,
@@ -30,25 +32,14 @@ function removeNamespacedGraphics(program, namespace, ids) {
30
32
  return next;
31
33
  }
32
34
 
33
- function gradientAnchor(prepared, plot, layout) {
34
- const strips = prepared.source.graphicSpec.objects.colorGradientStrips;
35
- if (strips?.items?.[0] === undefined) return undefined;
36
- const properties = strips.items[0].properties;
37
- const length = prepared.source.guideConfigs.legend.gradient.gradient.length;
38
- return {
39
- sourceX: properties.x,
40
- sourceY: properties.y,
41
- targetX: layout.legend.x,
42
- targetY: plot.y + (plot.height - length) / 2
43
- };
44
- }
45
-
46
35
  function legendTranslation(prepared, plot, layout) {
47
- const gradient = gradientAnchor(prepared, plot, layout);
48
- if (gradient !== undefined) {
36
+ const strips = prepared.source.graphicSpec.objects.colorGradientStrips;
37
+ if (strips?.items?.[0] !== undefined) {
38
+ const properties = strips.items[0].properties;
39
+ const length = prepared.source.guideConfigs.legend.gradient.gradient.length;
49
40
  return {
50
- x: gradient.targetX - gradient.sourceX,
51
- y: gradient.targetY - gradient.sourceY
41
+ x: layout.legend.x - properties.x,
42
+ y: plot.y + (plot.height - length) / 2 - properties.y
52
43
  };
53
44
  }
54
45
  const height = prepared.bounds.bottom - prepared.bounds.top;
@@ -79,6 +70,25 @@ function attachParentLegend(program, prepared, plot, layout) {
79
70
  return next;
80
71
  }
81
72
 
73
+ function assertSharedLegendFits(program, id, layout, plot) {
74
+ const bounds = resolveConcreteGraphicBounds(program.graphicSpec, id);
75
+ if (bounds === undefined) {
76
+ throw new Error("Facet shared legend requires measurable concrete bounds.");
77
+ }
78
+ const outside = bounds.left < 0 || bounds.right > layout.width ||
79
+ bounds.top < 0 || bounds.bottom > layout.height;
80
+ const overlapsPlot = Math.min(bounds.right, plot.x + plot.width) >
81
+ Math.max(bounds.left, plot.x) &&
82
+ Math.min(bounds.bottom, plot.y + plot.height) >
83
+ Math.max(bounds.top, plot.y);
84
+ if (outside || overlapsPlot) {
85
+ throw new Error(
86
+ "Facet shared legend requires more space than the composed Canvas provides."
87
+ );
88
+ }
89
+ return program;
90
+ }
91
+
82
92
  export function applyFacetGuideComposition(program, { layout, plot } = {}) {
83
93
  if (!isPlainObject(layout) || !Array.isArray(layout.children)) {
84
94
  throw new TypeError("Facet guide composition requires a resolved layout.");
@@ -119,8 +129,13 @@ export function applyFacetGuideComposition(program, { layout, plot } = {}) {
119
129
  }
120
130
  }
121
131
  if (prepared === undefined) return next;
132
+ let id;
122
133
  if (prepared.mode === "legacyCategorical") {
123
- return materializeLegacyCategoricalLegend(next, prepared, layout);
134
+ id = `${program.compositionSpec.id}-legend`;
135
+ next = materializeLegacyCategoricalLegend(next, prepared, layout);
136
+ } else {
137
+ id = `${program.compositionSpec.id}-shared-legend`;
138
+ next = attachParentLegend(next, prepared, plot, layout);
124
139
  }
125
- return attachParentLegend(next, prepared, plot, layout);
140
+ return assertSharedLegendFits(next, id, layout, plot);
126
141
  }
@@ -9,6 +9,10 @@ import { findSemanticScale } from "../../selectors/scales.js";
9
9
  import { allLegendGraphicIds } from "../guides/resources.js";
10
10
  import { DEFAULT_COLORS } from "../../theme/defaults.js";
11
11
  import { resolveLegacyCategoricalLegend } from "./legacyCategorical.js";
12
+ import {
13
+ formatDistinctNumericSamples,
14
+ sampleNumericRange
15
+ } from "../../grammar/numeric.js";
12
16
 
13
17
  const LEGEND_SOURCE_CANVAS = Object.freeze({
14
18
  width: 640,
@@ -71,10 +75,7 @@ function compactNumber(value) {
71
75
  }
72
76
 
73
77
  function continuousValues(domain, count) {
74
- return Array.from(
75
- { length: count },
76
- (_, index) => domain[0] + index / (count - 1) * (domain[1] - domain[0])
77
- );
78
+ return sampleNumericRange(domain[0], domain[1], count, "Facet legend domain");
78
79
  }
79
80
 
80
81
  function prepareAutoLegendSource(child) {
@@ -123,7 +124,10 @@ function prepareAutoLegendSource(child) {
123
124
  .editGraphics({
124
125
  target: "colorGradientLabels",
125
126
  property: "text",
126
- value: continuousValues(resolved.domain, 5).map(compactNumber)
127
+ value: formatDistinctNumericSamples(
128
+ continuousValues(resolved.domain, 5),
129
+ compactNumber
130
+ )
127
131
  })
128
132
  .editGraphics({
129
133
  target: "colorGradientTitle",
@@ -1,4 +1,9 @@
1
1
  import { resolveGraphicBounds } from "../layout/canvas.js";
2
+ import {
3
+ formatVisibleText,
4
+ resolveTextBounds,
5
+ textBoundsIntersect
6
+ } from "../core/textMetrics.js";
2
7
  import { resolvePlacedPlotBounds } from "../layout/composition.js";
3
8
  import {
4
9
  alignedTextAnchor,
@@ -12,9 +17,8 @@ import {
12
17
  clearCompositionChildren,
13
18
  compositionChildDescriptor
14
19
  } from "./composition.js";
15
- import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from "../theme/defaults.js";
16
20
 
17
- const ZERO_MARGIN = Object.freeze({ top: 0, right: 0, bottom: 0, left: 0 });
21
+ const ZERO_MARGIN = { top: 0, right: 0, bottom: 0, left: 0 };
18
22
 
19
23
  function facetConfig(program) {
20
24
  const config = program.materializationConfigs.facets?.[program.compositionSpec.id];
@@ -46,29 +50,35 @@ function titleLayout(program) {
46
50
  return { config, block, top, height: Math.ceil(top + block.height) };
47
51
  }
48
52
 
49
- function textItem(text, x, y, options = {}) {
50
- return {
51
- type: "text",
52
- properties: {
53
- x,
54
- y,
55
- text: String(text),
56
- fill: options.color ?? DEFAULT_COLORS.strongText,
57
- fontSize: options.fontSize ?? 12,
58
- fontFamily: options.fontFamily ?? DEFAULT_FONT_FAMILY,
59
- fontWeight: options.fontWeight ?? "normal",
60
- textAlign: options.textAlign ?? "left",
61
- textBaseline: options.textBaseline ?? "middle"
62
- }
63
- };
64
- }
65
-
66
53
  function collection(program, id, items) {
67
54
  return program
68
55
  .createGraphics({ id, type: "collection", parent: "canvas" })
69
56
  .editGraphics({ target: id, property: "items", value: items });
70
57
  }
71
58
 
59
+ function assertFacetHeadersFit(items, layout, plotById) {
60
+ const previous = [];
61
+ for (let index = 0; index < items.length; index += 1) {
62
+ const bounds = resolveTextBounds(items[index].properties);
63
+ const cell = layout.children[index];
64
+ const plot = plotById.get(cell.id);
65
+ if (bounds.left < 0 || bounds.right > layout.width ||
66
+ bounds.top < 0 || bounds.bottom > layout.height ||
67
+ previous.some(item => textBoundsIntersect(item, bounds)) ||
68
+ textBoundsIntersect(bounds, {
69
+ left: cell.x + plot.x,
70
+ right: cell.x + plot.x + plot.width,
71
+ top: cell.y + plot.y,
72
+ bottom: cell.y + plot.y + plot.height
73
+ })) {
74
+ throw new Error(
75
+ "Facet headers require sufficient non-overlapping space above every child plot."
76
+ );
77
+ }
78
+ previous.push(bounds);
79
+ }
80
+ }
81
+
72
82
  function materializeHeaders(program, layout, plots, config) {
73
83
  const plotById = new Map(plots.map(plot => [plot.id, plot]));
74
84
  const items = layout.children.map(cell => {
@@ -76,20 +86,22 @@ function materializeHeaders(program, layout, plots, config) {
76
86
  if (plot === undefined) {
77
87
  throw new Error(`Facet header requires plot bounds for "${cell.id}".`);
78
88
  }
79
- return textItem(
80
- cell.value,
81
- cell.x + plot.x + plot.width / 2,
82
- cell.y + config.offset,
83
- {
84
- color: config.color,
89
+ return {
90
+ type: "text",
91
+ properties: {
92
+ x: cell.x + plot.x + plot.width / 2,
93
+ y: cell.y + config.offset,
94
+ text: formatVisibleText(cell.value),
95
+ fill: config.color,
85
96
  fontSize: config.fontSize,
86
97
  fontFamily: config.fontFamily,
87
98
  fontWeight: config.fontWeight,
88
99
  textAlign: "center",
89
- textBaseline: "top"
100
+ textBaseline: "middle"
90
101
  }
91
- );
102
+ };
92
103
  });
104
+ assertFacetHeadersFit(items, layout, plotById);
93
105
  return collection(program, `${program.compositionSpec.id}-headers`, items);
94
106
  }
95
107
 
@@ -44,7 +44,8 @@ const LEGEND_RESOURCE_POLICIES = Object.freeze({
44
44
  family: "interval",
45
45
  rematerializeOp: "rematerializeIntervalLegend",
46
46
  graphicIds: Object.freeze([
47
- "colorLegendSymbols", "colorLegendLabels", "colorLegendTitle"
47
+ "colorLegendBackground", "colorLegendSymbols", "colorLegendLabels",
48
+ "colorLegendTitle"
48
49
  ])
49
50
  }),
50
51
  opacity: Object.freeze({
@@ -59,7 +59,10 @@ export function canMaterializeLine(program, layer) {
59
59
  return (
60
60
  layer.mark?.type === "line" &&
61
61
  hasCartesianPositionScales(layer) &&
62
- (isAggregate(layer.encoding.y.aggregate) ||
62
+ ((isAggregate(layer.encoding.y.aggregate) &&
63
+ (layer.encoding.x.fieldType === "temporal" ||
64
+ (layer.encoding.x.fieldType === "quantitative" &&
65
+ layer.encoding.x.bin !== undefined))) ||
63
66
  (layer.encoding.x.fieldType === "quantitative" &&
64
67
  layer.encoding.y.fieldType === "quantitative") ||
65
68
  ((interval || layer.encoding.y.aggregate === undefined) &&
@@ -159,7 +162,7 @@ export function canMaterializeText(program, layer) {
159
162
  if (layer.source !== undefined) {
160
163
  const source = findLayer(program, layer.source);
161
164
  return source !== undefined &&
162
- ["point", "bar", "rule", "rect"].includes(source.mark?.type) &&
165
+ ["point", "bar", "rule", "rect", "arc"].includes(source.mark?.type) &&
163
166
  Array.isArray(program.graphicSpec.objects[source.id]?.items);
164
167
  }
165
168
  return hasCartesianPositionScales(layer);
@@ -42,14 +42,34 @@ export function getSourceDependentMarkSteps(program, sourceId) {
42
42
  export function getPositionEncodingMaterializationSteps(program, layer, scaleId) {
43
43
  const policy = getMarkMaterializationPolicy(layer);
44
44
  if (policy === undefined) return [];
45
- const scale = { op: "rematerializeScale", args: { id: scaleId } };
46
45
  const complete = policy.canMaterialize(program, layer);
47
46
  const mark = getMarkRematerializationStep(layer);
47
+ const sharedConsumerMarks = (program.semanticSpec.layers ?? [])
48
+ .filter(candidate =>
49
+ candidate.id !== layer.id &&
50
+ POSITION_CHANNELS.some(channel =>
51
+ candidate.encoding?.[channel]?.scale === scaleId
52
+ )
53
+ )
54
+ .map(candidate => getMarkMaterializationStep(program, candidate))
55
+ .filter(step => step !== undefined);
56
+ const scale = {
57
+ op: "rematerializeScale",
58
+ args: sharedConsumerMarks.length === 0
59
+ ? { id: scaleId }
60
+ : { id: scaleId, marks: false }
61
+ };
48
62
  if (!complete) {
49
- if (policy.positionEncoding.incomplete === "scale") return [scale];
50
- return policy.positionEncoding.scaleFirst ? [scale, mark] : [mark];
63
+ if (policy.positionEncoding.incomplete === "scale") {
64
+ return [scale, ...sharedConsumerMarks];
65
+ }
66
+ return policy.positionEncoding.scaleFirst
67
+ ? [scale, mark, ...sharedConsumerMarks]
68
+ : [mark, ...sharedConsumerMarks];
51
69
  }
52
- return policy.positionEncoding.scaleFirst ? [scale, mark] : [mark];
70
+ return policy.positionEncoding.scaleFirst
71
+ ? [scale, mark, ...sharedConsumerMarks]
72
+ : [mark, ...sharedConsumerMarks];
53
73
  }
54
74
 
55
75
  export function getScaleConsumerMaterializationMode(layer, channel) {
@@ -4,41 +4,10 @@ import {
4
4
  resolveBarChannels,
5
5
  resolveBarGrain
6
6
  } from "../../../grammar/bars/policy.js";
7
- import { resolveHistogramBins } from "../../../grammar/histogram.js";
8
-
9
- export function resolveBinnedBarDomain({
10
- valuesByConsumer,
11
- channel,
12
- scale,
13
- id,
14
- allValues
15
- }) {
16
- const binnedBars = valuesByConsumer.filter(
17
- ({ consumer }) =>
18
- consumer.layer.mark?.type === "bar" &&
19
- consumer.encoding.bin !== undefined
20
- );
21
- if (binnedBars.length === 0) return undefined;
22
- if (channel !== "x" || binnedBars.length !== valuesByConsumer.length) {
23
- throw new Error(
24
- `Binned scale "${id}" cannot be shared with an unbinned consumer.`
25
- );
26
- }
27
- const binDefinitions = new Set(
28
- binnedBars.map(({ consumer }) => JSON.stringify(consumer.encoding.bin))
29
- );
30
- if (binDefinitions.size !== 1) {
31
- throw new Error(`Binned scale "${id}" requires one shared bin definition.`);
32
- }
33
- return resolveHistogramBins({
34
- values: allValues,
35
- bin: binnedBars[0].consumer.encoding.bin,
36
- domain: scale.domain,
37
- nice: scale.nice ?? true,
38
- zero: scale.zero ?? false
39
- }).domain;
40
- }
41
-
7
+ import {
8
+ interpolateNumber,
9
+ inverseLerp
10
+ } from "../../../grammar/numeric.js";
42
11
  export function resolveOffsetScalePolicy({
43
12
  consumers,
44
13
  resolvedScales,
@@ -81,8 +50,8 @@ export function resolveOffsetScalePolicy({
81
50
  };
82
51
  }
83
52
 
84
- export function resolveTemporalBarBand(consumers, domain, range, values) {
85
- const temporalBars = consumers.filter(consumer => {
53
+ export function resolveTemporalBarBand(valuesByConsumer, domain, range) {
54
+ const temporalBars = valuesByConsumer.filter(({ consumer }) => {
86
55
  const channels = resolveBarChannels(consumer.layer);
87
56
  return resolveBarGrain(consumer.layer) === BAR_GRAINS.aggregate &&
88
57
  channels?.category === consumer.channel &&
@@ -90,43 +59,76 @@ export function resolveTemporalBarBand(consumers, domain, range, values) {
90
59
  });
91
60
  if (temporalBars.length === 0) return undefined;
92
61
  const temporalFields = new Set(
93
- temporalBars.map(consumer => consumer.encoding.field)
62
+ temporalBars.map(({ consumer }) => consumer.encoding.field)
94
63
  );
95
- const compatibleLines = consumers.filter(consumer =>
96
- consumer.layer.mark?.type === "line" &&
97
- consumer.encoding.fieldType === "temporal" &&
98
- temporalFields.has(consumer.encoding.field)
64
+ const compatibleConsumers = valuesByConsumer.filter(({ consumer }) =>
65
+ ["line", "rule", "text"].includes(consumer.layer.mark?.type) &&
66
+ consumer.encoding.fieldType === "temporal" &&
67
+ temporalFields.has(consumer.encoding.field)
99
68
  );
100
- if (temporalBars.length + compatibleLines.length !== consumers.length) {
69
+ if (
70
+ temporalBars.length + compatibleConsumers.length !==
71
+ valuesByConsumer.length
72
+ ) {
101
73
  throw new Error(
102
- "A temporal bar position scale requires compatible bar or line consumers of one field."
74
+ "A temporal bar position scale requires compatible bar, line, rule, or text consumers of one field."
103
75
  );
104
76
  }
77
+ const values = temporalBars.flatMap(item => item.values);
105
78
  const ordered = [...new Set(values)].sort((left, right) => left - right);
106
79
  if (ordered.length < 2) {
107
80
  throw new Error("Temporal bar position requires at least two distinct values.");
108
81
  }
109
- const minimumGap = Math.min(
110
- ...ordered.slice(1).map((value, index) => value - ordered[index])
111
- );
82
+ let minimumGap = Infinity;
83
+ for (let index = 1; index < ordered.length; index += 1) {
84
+ minimumGap = Math.min(minimumGap, ordered[index] - ordered[index - 1]);
85
+ }
112
86
  const domainSpan = Math.abs(domain[1] - domain[0]);
113
87
  if (!(minimumGap > 0) || !(domainSpan > 0)) {
114
88
  throw new Error("Temporal bar position requires an increasing time domain.");
115
89
  }
116
90
  const direction = Math.sign(range[1] - range[0]) || 1;
117
- const estimatedBandwidth = Math.abs(range[1] - range[0]) * minimumGap /
91
+ const bandwidthFraction = minimumGap / (domainSpan + minimumGap);
92
+ const directBandwidth = Math.abs(range[1] - range[0]) * minimumGap /
118
93
  (domainSpan + minimumGap);
119
- const resolvedRange = [
120
- range[0] + direction * estimatedBandwidth / 2,
121
- range[1] - direction * estimatedBandwidth / 2
94
+ const directRange = [
95
+ range[0] + direction * directBandwidth / 2,
96
+ range[1] - direction * directBandwidth / 2
122
97
  ];
123
- const positions = ordered.map(value =>
124
- resolvedRange[0] +
125
- (value - domain[0]) / (domain[1] - domain[0]) *
126
- (resolvedRange[1] - resolvedRange[0])
127
- );
128
- const bandwidth = Math.min(
129
- ...positions.slice(1).map((value, index) => Math.abs(value - positions[index]))
130
- );
98
+ const stable = !Number.isFinite(directBandwidth) ||
99
+ !directRange.every(Number.isFinite);
100
+ const estimatedBandwidth = stable
101
+ ? Math.abs(
102
+ interpolateNumber(range[0], range[1], bandwidthFraction) - range[0]
103
+ )
104
+ : directBandwidth;
105
+ const resolvedRange = stable
106
+ ? [
107
+ interpolateNumber(range[0], range[1], bandwidthFraction / 2),
108
+ interpolateNumber(range[0], range[1], 1 - bandwidthFraction / 2)
109
+ ]
110
+ : directRange;
111
+ const resolvedSpan = resolvedRange[1] - resolvedRange[0];
112
+ const positions = ordered.map(value => {
113
+ const direct = resolvedRange[0] +
114
+ (value - domain[0]) / (domain[1] - domain[0]) * resolvedSpan;
115
+ return Number.isFinite(direct)
116
+ ? direct
117
+ : interpolateNumber(
118
+ resolvedRange[0],
119
+ resolvedRange[1],
120
+ inverseLerp(value, domain[0], domain[1])
121
+ );
122
+ });
123
+ let bandwidth = Infinity;
124
+ for (let index = 1; index < positions.length; index += 1) {
125
+ bandwidth = Math.min(
126
+ bandwidth,
127
+ Math.abs(positions[index] - positions[index - 1])
128
+ );
129
+ }
130
+ if (![estimatedBandwidth, bandwidth, ...resolvedRange].every(Number.isFinite)) {
131
+ throw new RangeError("Temporal bar position exceeds the finite numeric range.");
132
+ }
131
133
  return { bandwidth, range: resolvedRange };
132
134
  }
@@ -0,0 +1,34 @@
1
+ import { resolveHistogramBins } from "../../../grammar/histogram.js";
2
+
3
+ export function resolveBinnedPositionDomain({
4
+ valuesByConsumer,
5
+ channel,
6
+ scale,
7
+ id,
8
+ allValues
9
+ }) {
10
+ const binnedPositions = valuesByConsumer.filter(
11
+ ({ consumer }) =>
12
+ ["bar", "line"].includes(consumer.layer.mark?.type) &&
13
+ consumer.encoding.bin !== undefined
14
+ );
15
+ if (binnedPositions.length === 0) return undefined;
16
+ if (channel !== "x" || binnedPositions.length !== valuesByConsumer.length) {
17
+ throw new Error(
18
+ `Binned scale "${id}" cannot be shared with an unbinned consumer.`
19
+ );
20
+ }
21
+ const binDefinitions = new Set(
22
+ binnedPositions.map(({ consumer }) => JSON.stringify(consumer.encoding.bin))
23
+ );
24
+ if (binDefinitions.size !== 1) {
25
+ throw new Error(`Binned scale "${id}" requires one shared bin definition.`);
26
+ }
27
+ return resolveHistogramBins({
28
+ values: allValues,
29
+ bin: binnedPositions[0].consumer.encoding.bin,
30
+ domain: scale.domain,
31
+ nice: scale.nice ?? true,
32
+ zero: scale.zero ?? false
33
+ }).domain;
34
+ }
@@ -3,6 +3,7 @@ import {
3
3
  resolveContinuousDomain,
4
4
  resolveTransformedDomain
5
5
  } from "../../../grammar/scales/index.js";
6
+ import { numericExtent } from "../../../grammar/numeric.js";
6
7
 
7
8
  export function resolveSeriesLayoutDomain({
8
9
  id,
@@ -25,9 +26,14 @@ export function resolveSeriesLayoutDomain({
25
26
  const compatibleDirect = directConsumers.every(({ consumer }) =>
26
27
  layoutConsumers.every(({ consumer: layoutConsumer }) => {
27
28
  const aggregate = layoutConsumer.encoding.aggregate;
28
- return aggregate !== "count" &&
29
- consumer.encoding.fieldType === "quantitative" &&
30
- consumer.encoding.field === layoutConsumer.encoding.field;
29
+ const directEncoding = consumer.encoding;
30
+ return directEncoding.fieldType === "quantitative" && (
31
+ Object.hasOwn(directEncoding, "datum") ||
32
+ (
33
+ aggregate !== "count" &&
34
+ directEncoding.field === layoutConsumer.encoding.field
35
+ )
36
+ );
31
37
  })
32
38
  );
33
39
  if (!compatibleDirect) {
@@ -41,9 +47,9 @@ export function resolveSeriesLayoutDomain({
41
47
  ...directConsumers.flatMap(item => item.values)
42
48
  ];
43
49
  if (layout === "center" && scale.domain !== "auto") {
44
- const minimum = Math.min(...values);
45
- const maximum = Math.max(...values);
46
- if (Math.min(...scale.domain) > minimum || Math.max(...scale.domain) < maximum) {
50
+ const [minimum, maximum] = numericExtent(values);
51
+ const [domainMinimum, domainMaximum] = numericExtent(scale.domain);
52
+ if (domainMinimum > minimum || domainMaximum < maximum) {
47
53
  throw new Error(
48
54
  `Center layout scale "${id}" explicit domain must contain every centered bound.`
49
55
  );
@@ -52,7 +58,7 @@ export function resolveSeriesLayoutDomain({
52
58
  if (
53
59
  ["group", "overlay"].includes(layout) &&
54
60
  scale.domain !== "auto" &&
55
- (Math.min(...scale.domain) > 0 || Math.max(...scale.domain) < 0)
61
+ (numericExtent(scale.domain)[0] > 0 || numericExtent(scale.domain)[1] < 0)
56
62
  ) {
57
63
  throw new Error(
58
64
  `Series layout scale "${id}" requires an explicit domain containing zero.`
@@ -28,10 +28,10 @@ import {
28
28
  } from "../../grammar/scales/index.js";
29
29
  import { resolveArcAutoPositionRange } from "./policies/arc.js";
30
30
  import {
31
- resolveBinnedBarDomain,
32
31
  resolveOffsetScalePolicy,
33
32
  resolveTemporalBarBand
34
33
  } from "./policies/bar.js";
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";
37
37
 
@@ -223,7 +223,7 @@ export function resolveScaleMaterialization({
223
223
  }
224
224
  const binnedDomain = isDiscretizedColor
225
225
  ? undefined
226
- : resolveBinnedBarDomain({
226
+ : resolveBinnedPositionDomain({
227
227
  valuesByConsumer,
228
228
  channel,
229
229
  scale,
@@ -331,10 +331,9 @@ export function resolveScaleMaterialization({
331
331
  }
332
332
  if (scale.type === "time" && ["x", "y"].includes(channel)) {
333
333
  const temporalBarBand = resolveTemporalBarBand(
334
- consumers,
334
+ valuesByConsumer,
335
335
  domain,
336
- range,
337
- allValues
336
+ range
338
337
  );
339
338
  if (temporalBarBand !== undefined) {
340
339
  resolvedScale = {