ggaction 0.0.10 → 0.0.12

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 (224) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +5 -3
  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/intent-taxonomy.json +1 -183
  7. package/knowledge/intent-taxonomy.schema.json +1 -0
  8. package/knowledge/mcp-resources.json +1 -95
  9. package/knowledge/mcp-resources.schema.json +1 -0
  10. package/knowledge/task-packet.schema.json +1 -159
  11. package/knowledge/task-resolver.js +1 -1230
  12. package/package.json +18 -2
  13. package/src/actions/boxPlots/edit.js +45 -298
  14. package/src/actions/boxPlots/options.js +46 -77
  15. package/src/actions/boxPlots/resolve.js +1 -1
  16. package/src/actions/charts/bar.js +3 -0
  17. package/src/actions/charts/heatmap.js +73 -83
  18. package/src/actions/charts/index.js +1 -1
  19. package/src/actions/charts/parallel.js +35 -1
  20. package/src/actions/charts/shared.js +20 -0
  21. package/src/actions/composition/actions.js +46 -49
  22. package/src/actions/data/bin2d.js +105 -2
  23. package/src/actions/data/box.js +17 -17
  24. package/src/actions/data/density.js +42 -78
  25. package/src/actions/data/gradientProfile.js +22 -52
  26. package/src/actions/data/horizon.js +16 -41
  27. package/src/actions/data/index.js +10 -10
  28. package/src/actions/data/interval.js +16 -38
  29. package/src/actions/data/intervalEdit.js +55 -0
  30. package/src/actions/data/regression.js +7 -17
  31. package/src/actions/data/shared.js +47 -0
  32. package/src/actions/data/timeUnit.js +14 -38
  33. package/src/actions/data/window.js +14 -38
  34. package/src/actions/distributions/revision.js +154 -34
  35. package/src/actions/encodings/angle.js +9 -10
  36. package/src/actions/encodings/appearance.js +17 -51
  37. package/src/actions/encodings/density/resolve.js +3 -1
  38. package/src/actions/encodings/density.js +1 -15
  39. package/src/actions/encodings/horizon/resolve.js +13 -3
  40. package/src/actions/encodings/horizon.js +8 -7
  41. package/src/actions/encodings/index.js +9 -9
  42. package/src/actions/encodings/offset.js +41 -45
  43. package/src/actions/encodings/parallel.js +9 -10
  44. package/src/actions/encodings/pathOrder.js +6 -13
  45. package/src/actions/encodings/position/apply.js +30 -0
  46. package/src/actions/encodings/position/policies/arc.js +23 -2
  47. package/src/actions/encodings/position/policies/index.js +4 -4
  48. package/src/actions/encodings/position/policies/line.js +47 -17
  49. package/src/actions/encodings/position/resolve.js +36 -7
  50. package/src/actions/encodings/ranged.js +18 -23
  51. package/src/actions/encodings/remove.js +15 -25
  52. package/src/actions/encodings/ruleAppearance.js +9 -24
  53. package/src/actions/encodings/shared.js +37 -0
  54. package/src/actions/encodings/strokeDash.js +8 -23
  55. package/src/actions/encodings/text.js +9 -10
  56. package/src/actions/errorBands/create.js +105 -39
  57. package/src/actions/errorBands/edit.js +89 -93
  58. package/src/actions/errorBands/index.js +1 -2
  59. package/src/actions/errorBars/create.js +212 -30
  60. package/src/actions/errorBars/edit.js +91 -48
  61. package/src/actions/errorBars/index.js +1 -2
  62. package/src/actions/facets/actions.js +45 -69
  63. package/src/actions/facets/derive.js +38 -53
  64. package/src/actions/facets/replay.js +0 -1
  65. package/src/actions/gradientPlots/components.js +59 -48
  66. package/src/actions/gradientPlots/edit.js +35 -216
  67. package/src/actions/gradientPlots/index.js +11 -10
  68. package/src/actions/gradientPlots/materialize.js +21 -35
  69. package/src/actions/gradientPlots/options.js +5 -7
  70. package/src/actions/gradientPlots/paint.js +9 -6
  71. package/src/actions/gradientPlots/resolve.js +1 -1
  72. package/src/actions/guides/axes/index.js +3 -3
  73. package/src/actions/guides/axes/labels.js +68 -26
  74. package/src/actions/guides/axes/lines.js +22 -2
  75. package/src/actions/guides/axes/policy.js +28 -8
  76. package/src/actions/guides/axes/ticks.js +27 -9
  77. package/src/actions/guides/axes/titles.js +113 -65
  78. package/src/actions/guides/grids/resolve.js +14 -0
  79. package/src/actions/guides/guides.js +2 -4
  80. package/src/actions/guides/index.js +2 -2
  81. package/src/actions/guides/legends/categorical/actions.js +44 -68
  82. package/src/actions/guides/legends/categorical/components.js +6 -1
  83. package/src/actions/guides/legends/categorical/index.js +2 -2
  84. package/src/actions/guides/legends/categorical/layout.js +148 -175
  85. package/src/actions/guides/legends/categorical/recipes.js +5 -0
  86. package/src/actions/guides/legends/categorical/resolve.js +8 -1
  87. package/src/actions/guides/legends/continuous/common.js +117 -66
  88. package/src/actions/guides/legends/continuous/gradient.js +38 -37
  89. package/src/actions/guides/legends/continuous/interval.js +97 -76
  90. package/src/actions/guides/legends/continuous/opacity.js +28 -38
  91. package/src/actions/guides/legends/edit.js +14 -1
  92. package/src/actions/guides/legends/index.js +1 -1
  93. package/src/actions/guides/legends/lane.js +2 -1
  94. package/src/actions/guides/legends/remove.js +8 -0
  95. package/src/actions/guides/legends/size.js +36 -43
  96. package/src/actions/guides/legends/strokeWidth.js +20 -26
  97. package/src/actions/guides/polar/axes/labels.js +17 -1
  98. package/src/actions/guides/polar/axes/shared.js +7 -1
  99. package/src/actions/guides/polar/axes/titles.js +19 -2
  100. package/src/actions/guides/polar/grids.js +4 -0
  101. package/src/actions/guides/polar/resolve.js +6 -0
  102. package/src/actions/index.js +13 -13
  103. package/src/actions/intervals/resolve.js +87 -117
  104. package/src/actions/marks/arc/actions.js +31 -36
  105. package/src/actions/marks/area/actions.js +23 -31
  106. package/src/actions/marks/index.js +6 -6
  107. package/src/actions/marks/line/actions.js +33 -52
  108. package/src/actions/marks/line/materialize.js +7 -5
  109. package/src/actions/marks/point/materialize.js +38 -16
  110. package/src/actions/marks/remove.js +21 -0
  111. package/src/actions/marks/rule/actions.js +79 -54
  112. package/src/actions/marks/shared.js +8 -4
  113. package/src/actions/marks/text/actions.js +14 -6
  114. package/src/actions/marks/text/layout.js +21 -45
  115. package/src/actions/marks/tick/actions.js +22 -21
  116. package/src/actions/primitives/createGraphics.js +4 -0
  117. package/src/actions/primitives/editGraphics.js +4 -0
  118. package/src/actions/primitives/index.js +3 -3
  119. package/src/actions/primitives/semantic.js +1 -1
  120. package/src/actions/primitives/semanticValidation/index.js +2 -2
  121. package/src/actions/primitives/semanticValidation/layer.js +1 -1
  122. package/src/actions/regression/edit.js +3 -43
  123. package/src/actions/scales/consumers/families.js +24 -4
  124. package/src/actions/scales/consumers/index.js +1 -1
  125. package/src/actions/scales/consumers/seriesLayout.js +17 -0
  126. package/src/actions/scales/definitions.js +100 -103
  127. package/src/actions/scales/materialize.js +5 -0
  128. package/src/actions/selection/actions.js +79 -121
  129. package/src/actions/titles/actions.js +48 -94
  130. package/src/actions/titles/resolve.js +14 -27
  131. package/src/actions/violinPlots/create.js +37 -44
  132. package/src/core/textMetrics.js +74 -37
  133. package/src/core/validation.js +18 -0
  134. package/src/core/vocabulary.js +2 -2
  135. package/src/grammar/aggregate.js +48 -28
  136. package/src/grammar/arcs.js +170 -32
  137. package/src/grammar/areaSeries.js +14 -16
  138. package/src/grammar/bin2d.js +37 -23
  139. package/src/grammar/boxPlot.js +14 -13
  140. package/src/grammar/categoricalDensity.js +14 -14
  141. package/src/grammar/categoryOrder.js +8 -14
  142. package/src/grammar/curveCommands.js +87 -20
  143. package/src/grammar/density.js +162 -141
  144. package/src/grammar/facets/index.js +14 -0
  145. package/src/grammar/facets/scales.js +7 -4
  146. package/src/grammar/gradientProfile.js +12 -15
  147. package/src/grammar/histogram.js +145 -83
  148. package/src/grammar/horizon.js +60 -23
  149. package/src/grammar/interval.js +14 -13
  150. package/src/grammar/lineSeries.js +91 -61
  151. package/src/grammar/numeric.js +290 -0
  152. package/src/grammar/paint.js +3 -4
  153. package/src/grammar/palettes.js +21 -32
  154. package/src/grammar/pathCommands.js +6 -0
  155. package/src/grammar/polarPaths.js +5 -5
  156. package/src/grammar/positionCompatibility.js +1 -1
  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/rules.js +2 -0
  161. package/src/grammar/scales/color.js +35 -26
  162. package/src/grammar/scales/continuous.js +71 -24
  163. package/src/grammar/scales/definition.js +1 -1
  164. package/src/grammar/scales/discretized.js +42 -12
  165. package/src/grammar/scales/fields.js +21 -4
  166. package/src/grammar/scales/ordinal.js +139 -60
  167. package/src/grammar/scales/temporal.js +40 -11
  168. package/src/grammar/scales/transformed.js +199 -52
  169. package/src/grammar/schemas/graphicBounds.js +270 -79
  170. package/src/grammar/seriesLayout.js +50 -18
  171. package/src/grammar/ticks.js +123 -53
  172. package/src/grammar/timeUnit.js +18 -8
  173. package/src/grammar/transforms.js +4 -1
  174. package/src/grammar/window.js +70 -57
  175. package/src/index.js +1 -1
  176. package/src/layout/canvas.js +4 -8
  177. package/src/layout/composition.js +39 -40
  178. package/src/layout/facets.js +55 -30
  179. package/src/layout/labels.js +91 -15
  180. package/src/layout/legend.js +22 -20
  181. package/src/layout/legendLane.js +54 -40
  182. package/src/layout/title.js +7 -7
  183. package/src/materialization/bars/aggregate.js +14 -40
  184. package/src/materialization/bars/grouped.js +7 -18
  185. package/src/materialization/bars/histogram.js +2 -19
  186. package/src/materialization/bars/ranged.js +20 -15
  187. package/src/materialization/bars/resolve.js +21 -0
  188. package/src/materialization/facetGuides/legacyCategorical.js +9 -2
  189. package/src/materialization/facetGuides/placement.js +49 -19
  190. package/src/materialization/facetGuides/preparation.js +35 -6
  191. package/src/materialization/facets.js +42 -28
  192. package/src/materialization/guides/resources.js +2 -1
  193. package/src/materialization/marks/capabilities.js +15 -4
  194. package/src/materialization/marks/index.js +29 -6
  195. package/src/materialization/rowEncoding.js +57 -1
  196. package/src/materialization/scales/policies/bar.js +61 -102
  197. package/src/materialization/scales/policies/binnedPosition.js +34 -0
  198. package/src/materialization/scales/policies/offset.js +54 -0
  199. package/src/materialization/scales/policies/series.js +13 -7
  200. package/src/materialization/scales/resolve.js +6 -9
  201. package/src/materialization/selection/items/arc.js +16 -2
  202. package/src/materialization/selection/items/bar.js +4 -2
  203. package/src/materialization/selection/items/common.js +21 -1
  204. package/src/materialization/selection/items/path.js +19 -2
  205. package/src/materialization/selection/items/point.js +4 -16
  206. package/src/materialization/selection/items/tick.js +13 -21
  207. package/src/materialization/selection/point.js +11 -4
  208. package/src/materialization/selection/styles.js +19 -27
  209. package/src/materialization/text.js +4 -1
  210. package/src/mcp/adapter.js +8 -3
  211. package/src/renderers/canvas/fill.js +2 -2
  212. package/src/renderers/canvas/index.js +35 -2
  213. package/src/renderers/canvas/native.js +160 -0
  214. package/src/renderers/pdf.js +14 -0
  215. package/src/renderers/svg.js +37 -4
  216. package/types/index.d.ts +3 -0
  217. package/types/program.d.ts +523 -131
  218. package/types/svg.d.ts +1 -0
  219. package/src/actions/errorBands/components.js +0 -81
  220. package/src/actions/errorBands/options.js +0 -39
  221. package/src/actions/errorBands/resolve.js +0 -28
  222. package/src/actions/errorBars/components.js +0 -62
  223. package/src/actions/errorBars/options.js +0 -58
  224. package/src/actions/errorBars/resolve.js +0 -25
@@ -5,11 +5,11 @@ import {
5
5
  } from "../../grammar/scales/index.js";
6
6
  import { sameOrderedValues } from "../../core/validation.js";
7
7
  import {
8
- DEFAULT_BAR_STROKE,
9
- DEFAULT_BAR_STROKE_WIDTH
8
+ resolveBarAppearance
10
9
  } from "./resolve.js";
11
10
  import { resolveBarWidth } from "../../grammar/bars/geometry.js";
12
11
  import { DEFAULT_SERIES_BASELINE } from "../../grammar/seriesLayout.js";
12
+ import { finiteMidpoint } from "../../grammar/numeric.js";
13
13
  import {
14
14
  BAR_ORIENTATIONS,
15
15
  resolveBarChannels,
@@ -58,7 +58,10 @@ export function deriveGroupedRectangles(required, resolved, widthConfig) {
58
58
  colorScale.range[index % colorScale.range.length]
59
59
  ])
60
60
  );
61
- const offsetMidpoint = (offsetScale.range[0] + offsetScale.range[1]) / 2;
61
+ const offsetMidpoint = finiteMidpoint(
62
+ offsetScale.range[0],
63
+ offsetScale.range[1]
64
+ );
62
65
  const categoryDirection = Math.sign(
63
66
  categoryScale.step ?? categoryScale.range[1] - categoryScale.range[0]
64
67
  ) || 1;
@@ -78,7 +81,6 @@ export function deriveGroupedRectangles(required, resolved, widthConfig) {
78
81
  );
79
82
  const existing = resolved.graphicSpec.objects[layer.id].items;
80
83
  const config = resolved.markConfigs[layer.id] ?? {};
81
- const appearance = config.barAppearance ?? {};
82
84
 
83
85
  return cells.map((cell, index) => {
84
86
  const categoryValue = cell[channels.category];
@@ -136,20 +138,7 @@ export function deriveGroupedRectangles(required, resolved, widthConfig) {
136
138
  return {
137
139
  ...geometry,
138
140
  fill: colors.get(cell.color),
139
- stroke: appearance.stroke === false
140
- ? "transparent"
141
- : appearance.stroke ?? config.stroke ?? existing[index]?.properties.stroke ?? DEFAULT_BAR_STROKE,
142
- strokeWidth:
143
- appearance.stroke === false
144
- ? 0
145
- : appearance.strokeWidth ?? config.strokeWidth ??
146
- existing[index]?.properties.strokeWidth ??
147
- DEFAULT_BAR_STROKE_WIDTH,
148
- ...((appearance.opacity ?? config.opacity) === undefined
149
- ? (existing[index]?.properties.opacity === undefined
150
- ? {}
151
- : { opacity: existing[index].properties.opacity })
152
- : { opacity: appearance.opacity ?? config.opacity })
141
+ ...resolveBarAppearance(config, existing[index]?.properties)
153
142
  };
154
143
  });
155
144
  }
@@ -13,8 +13,7 @@ import { resolveBarColorLayout } from "../../grammar/bars/policy.js";
13
13
  import { layoutSeriesPartition } from "../../grammar/seriesLayout.js";
14
14
  import {
15
15
  DEFAULT_BAR_FILL,
16
- DEFAULT_BAR_STROKE,
17
- DEFAULT_BAR_STROKE_WIDTH
16
+ resolveBarAppearance
18
17
  } from "./resolve.js";
19
18
 
20
19
  export function deriveHistogramSegments({
@@ -141,23 +140,7 @@ export function deriveHistogramRectangles(required, resolved) {
141
140
  config.fill ??
142
141
  existing[index]?.properties.fill ??
143
142
  DEFAULT_BAR_FILL,
144
- stroke:
145
- appearance.stroke === false
146
- ? "transparent"
147
- : appearance.stroke ?? config.stroke ??
148
- existing[index]?.properties.stroke ??
149
- DEFAULT_BAR_STROKE,
150
- strokeWidth:
151
- appearance.stroke === false
152
- ? 0
153
- : appearance.strokeWidth ?? config.strokeWidth ??
154
- existing[index]?.properties.strokeWidth ??
155
- DEFAULT_BAR_STROKE_WIDTH,
156
- ...((appearance.opacity ?? config.opacity) === undefined
157
- ? (existing[index]?.properties.opacity === undefined
158
- ? {}
159
- : { opacity: existing[index].properties.opacity })
160
- : { opacity: appearance.opacity ?? config.opacity })
143
+ ...resolveBarAppearance(config, existing[index]?.properties)
161
144
  };
162
145
  });
163
146
  }
@@ -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,
@@ -68,7 +69,13 @@ export function resolveLegacyCategoricalLegend(program) {
68
69
  return {
69
70
  mode: "legacyCategorical",
70
71
  compatibility,
71
- reservation: { gap: 18, width: 132 },
72
+ reservation: {
73
+ position: "right",
74
+ align: "center",
75
+ gap: 18,
76
+ width: 132,
77
+ height: 0
78
+ },
72
79
  legend: {
73
80
  field: config.field,
74
81
  mark: encodings[0].layer.mark.type,
@@ -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,29 @@ 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 position = prepared.reservation.position;
37
+ if (["top", "bottom"].includes(position)) {
38
+ const width = prepared.bounds.right - prepared.bounds.left;
39
+ const left = prepared.reservation.align === "left"
40
+ ? plot.x
41
+ : prepared.reservation.align === "right"
42
+ ? plot.x + plot.width - width
43
+ : plot.x + (plot.width - width) / 2;
49
44
  return {
50
- x: gradient.targetX - gradient.sourceX,
51
- y: gradient.targetY - gradient.sourceY
45
+ x: left - prepared.bounds.left,
46
+ y: layout.legend.y - prepared.bounds.top
47
+ };
48
+ }
49
+ const strips = prepared.source.graphicSpec.objects.colorGradientStrips;
50
+ if (strips?.items?.[0] !== undefined) {
51
+ const properties = strips.items[0].properties;
52
+ const length = prepared.source.guideConfigs.legend.gradient.gradient.length;
53
+ return {
54
+ x: layout.legend.x - (position === "left"
55
+ ? prepared.bounds.left
56
+ : properties.x),
57
+ y: plot.y + (plot.height - length) / 2 - properties.y
52
58
  };
53
59
  }
54
60
  const height = prepared.bounds.bottom - prepared.bounds.top;
@@ -79,6 +85,25 @@ function attachParentLegend(program, prepared, plot, layout) {
79
85
  return next;
80
86
  }
81
87
 
88
+ function assertSharedLegendFits(program, id, layout, plot) {
89
+ const bounds = resolveConcreteGraphicBounds(program.graphicSpec, id);
90
+ if (bounds === undefined) {
91
+ throw new Error("Facet shared legend requires measurable concrete bounds.");
92
+ }
93
+ const outside = bounds.left < 0 || bounds.right > layout.width ||
94
+ bounds.top < 0 || bounds.bottom > layout.height;
95
+ const overlapsPlot = Math.min(bounds.right, plot.x + plot.width) >
96
+ Math.max(bounds.left, plot.x) &&
97
+ Math.min(bounds.bottom, plot.y + plot.height) >
98
+ Math.max(bounds.top, plot.y);
99
+ if (outside || overlapsPlot) {
100
+ throw new Error(
101
+ "Facet shared legend requires more space than the composed Canvas provides."
102
+ );
103
+ }
104
+ return program;
105
+ }
106
+
82
107
  export function applyFacetGuideComposition(program, { layout, plot } = {}) {
83
108
  if (!isPlainObject(layout) || !Array.isArray(layout.children)) {
84
109
  throw new TypeError("Facet guide composition requires a resolved layout.");
@@ -119,8 +144,13 @@ export function applyFacetGuideComposition(program, { layout, plot } = {}) {
119
144
  }
120
145
  }
121
146
  if (prepared === undefined) return next;
147
+ let id;
122
148
  if (prepared.mode === "legacyCategorical") {
123
- return materializeLegacyCategoricalLegend(next, prepared, layout);
149
+ id = `${program.compositionSpec.id}-legend`;
150
+ next = materializeLegacyCategoricalLegend(next, prepared, layout);
151
+ } else {
152
+ id = `${program.compositionSpec.id}-shared-legend`;
153
+ next = attachParentLegend(next, prepared, plot, layout);
124
154
  }
125
- return attachParentLegend(next, prepared, plot, layout);
155
+ return assertSharedLegendFits(next, id, layout, plot);
126
156
  }
@@ -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",
@@ -145,6 +149,27 @@ function compatibleLegendChildren(program, source, inferred) {
145
149
  });
146
150
  }
147
151
 
152
+ function promotedLegendPlacement(source, kinds) {
153
+ const positioned = kinds.map(kind => source.guideConfigs.legend[kind])
154
+ .filter(config => config.position !== undefined);
155
+ const positions = new Set(positioned.map(config => config.position));
156
+ if (positions.size > 1) {
157
+ throw new Error(
158
+ "Shared facet legends must use one common edge position."
159
+ );
160
+ }
161
+ const aligns = new Set(positioned.map(config => config.align ?? "center"));
162
+ if (aligns.size > 1) {
163
+ throw new Error(
164
+ "Shared facet legends must use one common alignment."
165
+ );
166
+ }
167
+ return {
168
+ position: positioned[0]?.position ?? "right",
169
+ align: positioned[0]?.align ?? "center"
170
+ };
171
+ }
172
+
148
173
  export function prepareSharedFacetLegend(program) {
149
174
  if (program.compositionSpec.facet.guides.legend !== "shared") {
150
175
  return undefined;
@@ -172,6 +197,8 @@ export function prepareSharedFacetLegend(program) {
172
197
  throw new Error("Shared facet legend has no measurable concrete bounds.");
173
198
  }
174
199
  const gradient = kinds.includes("gradient");
200
+ const placement = promotedLegendPlacement(source, kinds);
201
+ const width = Math.ceil(bounds.right - bounds.left);
175
202
  return {
176
203
  mode: "promoted",
177
204
  source,
@@ -181,8 +208,10 @@ export function prepareSharedFacetLegend(program) {
181
208
  bounds,
182
209
  compatibility,
183
210
  reservation: {
211
+ ...placement,
184
212
  gap: gradient ? 24 : 18,
185
- width: gradient ? 112 : Math.max(132, Math.ceil(bounds.right - bounds.left))
213
+ width: Math.max(gradient ? 112 : 132, width),
214
+ height: Math.ceil(bounds.bottom - bounds.top)
186
215
  }
187
216
  };
188
217
  }
@@ -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
 
@@ -163,7 +175,9 @@ export function resolveFacetProgramLayout(program, preparedLegend) {
163
175
  sharedLegend: spec.facet.guides.legend === "shared",
164
176
  ...(preparedLegend === undefined ? {} : {
165
177
  sharedLegendGap: preparedLegend.reservation.gap,
166
- sharedLegendWidth: preparedLegend.reservation.width
178
+ sharedLegendWidth: preparedLegend.reservation.width,
179
+ sharedLegendHeight: preparedLegend.reservation.height,
180
+ sharedLegendPosition: preparedLegend.reservation.position
167
181
  })
168
182
  });
169
183
  const plots = spec.children.map(id => ({
@@ -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) &&
@@ -105,11 +108,19 @@ export function isIntentionallyEmptyArea(program, layer) {
105
108
  export function canMaterializeArc(_program, layer) {
106
109
  if (
107
110
  layer.mark?.type !== "arc" ||
108
- layer.encoding?.theta?.scale === undefined ||
109
- !["nominal", "ordinal"].includes(layer.encoding.theta.fieldType)
111
+ layer.encoding?.theta?.scale === undefined
110
112
  ) {
111
113
  return false;
112
114
  }
115
+ if (layer.encoding.theta.fieldType === "quantitative") {
116
+ return (
117
+ layer.encoding.theta.aggregate === undefined &&
118
+ layer.encoding?.radius === undefined
119
+ );
120
+ }
121
+ if (!["nominal", "ordinal"].includes(layer.encoding.theta.fieldType)) {
122
+ return false;
123
+ }
113
124
  if (["count", "sum"].includes(layer.encoding.theta.aggregate)) {
114
125
  return layer.encoding?.radius === undefined;
115
126
  }
@@ -159,7 +170,7 @@ export function canMaterializeText(program, layer) {
159
170
  if (layer.source !== undefined) {
160
171
  const source = findLayer(program, layer.source);
161
172
  return source !== undefined &&
162
- ["point", "bar", "rule", "rect"].includes(source.mark?.type) &&
173
+ ["point", "bar", "rule", "rect", "arc"].includes(source.mark?.type) &&
163
174
  Array.isArray(program.graphicSpec.objects[source.id]?.items);
164
175
  }
165
176
  return hasCartesianPositionScales(layer);
@@ -1,4 +1,7 @@
1
- import { POSITION_CHANNELS } from "../../core/vocabulary.js";
1
+ import {
2
+ POSITION_CHANNELS,
3
+ POSITION_ENCODING_CHANNELS
4
+ } from "../../core/vocabulary.js";
2
5
  import { getMarkMaterializationPolicy } from "./policies.js";
3
6
 
4
7
  export {
@@ -42,20 +45,40 @@ export function getSourceDependentMarkSteps(program, sourceId) {
42
45
  export function getPositionEncodingMaterializationSteps(program, layer, scaleId) {
43
46
  const policy = getMarkMaterializationPolicy(layer);
44
47
  if (policy === undefined) return [];
45
- const scale = { op: "rematerializeScale", args: { id: scaleId } };
46
48
  const complete = policy.canMaterialize(program, layer);
47
49
  const mark = getMarkRematerializationStep(layer);
50
+ const sharedConsumerMarks = (program.semanticSpec.layers ?? [])
51
+ .filter(candidate =>
52
+ candidate.id !== layer.id &&
53
+ POSITION_CHANNELS.some(channel =>
54
+ candidate.encoding?.[channel]?.scale === scaleId
55
+ )
56
+ )
57
+ .map(candidate => getMarkMaterializationStep(program, candidate))
58
+ .filter(step => step !== undefined);
59
+ const scale = {
60
+ op: "rematerializeScale",
61
+ args: sharedConsumerMarks.length === 0
62
+ ? { id: scaleId }
63
+ : { id: scaleId, marks: false }
64
+ };
48
65
  if (!complete) {
49
- if (policy.positionEncoding.incomplete === "scale") return [scale];
50
- return policy.positionEncoding.scaleFirst ? [scale, mark] : [mark];
66
+ if (policy.positionEncoding.incomplete === "scale") {
67
+ return [scale, ...sharedConsumerMarks];
68
+ }
69
+ return policy.positionEncoding.scaleFirst
70
+ ? [scale, mark, ...sharedConsumerMarks]
71
+ : [mark, ...sharedConsumerMarks];
51
72
  }
52
- return policy.positionEncoding.scaleFirst ? [scale, mark] : [mark];
73
+ return policy.positionEncoding.scaleFirst
74
+ ? [scale, mark, ...sharedConsumerMarks]
75
+ : [mark, ...sharedConsumerMarks];
53
76
  }
54
77
 
55
78
  export function getScaleConsumerMaterializationMode(layer, channel) {
56
79
  const policy = getMarkMaterializationPolicy(layer);
57
80
  if (policy === undefined) return "direct";
58
- if (POSITION_CHANNELS.includes(channel)) {
81
+ if (POSITION_ENCODING_CHANNELS.includes(channel)) {
59
82
  return policy.scaleApplication.position ?? policy.scaleApplication.default;
60
83
  }
61
84
  if (policy.scaleApplication.deferredChannels?.includes(channel)) {