ggaction 0.0.13 → 0.0.15

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 (270) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +20 -11
  3. package/knowledge/action-cards.json +1 -1
  4. package/knowledge/intent-taxonomy.json +1 -1
  5. package/knowledge/mcp-resources.json +1 -1
  6. package/package.json +8 -3
  7. package/src/actions/basic.js +2 -2
  8. package/src/actions/boxPlots/components.js +14 -4
  9. package/src/actions/boxPlots/edit.js +31 -5
  10. package/src/actions/boxPlots/materialize.js +11 -3
  11. package/src/actions/boxPlots/options.js +48 -16
  12. package/src/actions/categoryOrder/index.js +15 -4
  13. package/src/actions/charts/area.js +5 -1
  14. package/src/actions/charts/bar.js +2 -1
  15. package/src/actions/charts/density.js +6 -2
  16. package/src/actions/charts/ecdf.js +4 -1
  17. package/src/actions/charts/endpoints.js +25 -3
  18. package/src/actions/charts/heatmap.js +2 -1
  19. package/src/actions/charts/histogram.js +4 -3
  20. package/src/actions/charts/horizon.js +3 -1
  21. package/src/actions/charts/interval-regression.js +5 -2
  22. package/src/actions/charts/line.js +3 -1
  23. package/src/actions/charts/parallel.js +8 -2
  24. package/src/actions/charts/pie.js +10 -2
  25. package/src/actions/charts/polar.js +15 -4
  26. package/src/actions/charts/radar.js +8 -2
  27. package/src/actions/charts/radial.js +10 -2
  28. package/src/actions/charts/rug-strip.js +6 -2
  29. package/src/actions/charts/scatter.js +3 -1
  30. package/src/actions/composition/actions.js +13 -4
  31. package/src/actions/coordinates/actions.js +1 -1
  32. package/src/actions/coordinates/edit.js +76 -0
  33. package/src/actions/data/bin.js +1 -1
  34. package/src/actions/data/bin2d.js +57 -185
  35. package/src/actions/data/complete.js +26 -0
  36. package/src/actions/data/computed.js +2 -2
  37. package/src/actions/data/create.js +2 -2
  38. package/src/actions/data/density.js +12 -20
  39. package/src/actions/data/derived.js +47 -17
  40. package/src/actions/data/edit.js +539 -0
  41. package/src/actions/data/filter.js +14 -2
  42. package/src/actions/data/impute.js +26 -0
  43. package/src/actions/data/index.js +14 -0
  44. package/src/actions/data/normalize.js +27 -0
  45. package/src/actions/data/regression.js +16 -21
  46. package/src/actions/data/revisionBindings.js +145 -0
  47. package/src/actions/data/shared.js +20 -4
  48. package/src/actions/data/summary.js +1 -1
  49. package/src/actions/data/timeUnit.js +4 -1
  50. package/src/actions/data/window.js +1 -1
  51. package/src/actions/encodings/channels.js +415 -0
  52. package/src/actions/encodings/color/index.js +2 -1
  53. package/src/actions/encodings/density.js +5 -2
  54. package/src/actions/encodings/histogram.js +3 -2
  55. package/src/actions/encodings/index.js +4 -0
  56. package/src/actions/encodings/offset.js +20 -13
  57. package/src/actions/encodings/position/apply.js +53 -7
  58. package/src/actions/encodings/position/index.js +10 -2
  59. package/src/actions/encodings/position/policies/bar.js +9 -0
  60. package/src/actions/encodings/position/resolve.js +24 -7
  61. package/src/actions/encodings/remove.js +9 -4
  62. package/src/actions/encodings/ruleAppearance.js +1 -23
  63. package/src/actions/encodings/seriesLayout.js +1 -0
  64. package/src/actions/encodings/shared.js +82 -14
  65. package/src/actions/encodings/stroke.js +226 -0
  66. package/src/actions/errorBands/create.js +23 -3
  67. package/src/actions/errorBands/edit.js +21 -6
  68. package/src/actions/errorBars/create.js +35 -7
  69. package/src/actions/errorBars/edit.js +19 -4
  70. package/src/actions/facets/actions.js +237 -57
  71. package/src/actions/facets/derive.js +91 -26
  72. package/src/actions/gradientPlots/components.js +10 -2
  73. package/src/actions/gradientPlots/edit.js +14 -4
  74. package/src/actions/gradientPlots/materialize.js +3 -1
  75. package/src/actions/gradientPlots/options.js +10 -2
  76. package/src/actions/guides/applicability.js +8 -2
  77. package/src/actions/guides/axes/labels.js +58 -12
  78. package/src/actions/guides/axes/lines.js +6 -2
  79. package/src/actions/guides/axes/parallel/resolve.js +6 -2
  80. package/src/actions/guides/axes/tickGroups.js +2 -1
  81. package/src/actions/guides/axes/ticks.js +6 -2
  82. package/src/actions/guides/axes/titles.js +11 -3
  83. package/src/actions/guides/grids/resolve.js +7 -2
  84. package/src/actions/guides/guides.js +19 -3
  85. package/src/actions/guides/legends/blocks.js +426 -0
  86. package/src/actions/guides/legends/categorical/actions.js +134 -48
  87. package/src/actions/guides/legends/categorical/components.js +17 -12
  88. package/src/actions/guides/legends/categorical/index.js +37 -33
  89. package/src/actions/guides/legends/categorical/layout.js +97 -15
  90. package/src/actions/guides/legends/categorical/options.js +2 -2
  91. package/src/actions/guides/legends/categorical/recipes.js +111 -20
  92. package/src/actions/guides/legends/categorical/resolve.js +12 -8
  93. package/src/actions/guides/legends/categorical/symbols.js +85 -30
  94. package/src/actions/guides/legends/continuous/common.js +30 -12
  95. package/src/actions/guides/legends/continuous/gradient.js +10 -7
  96. package/src/actions/guides/legends/continuous/index.js +2 -0
  97. package/src/actions/guides/legends/continuous/interval.js +8 -3
  98. package/src/actions/guides/legends/continuous/opacity.js +8 -5
  99. package/src/actions/guides/legends/continuous/stroke.js +525 -0
  100. package/src/actions/guides/legends/creation.js +14 -1
  101. package/src/actions/guides/legends/edit.js +158 -59
  102. package/src/actions/guides/legends/index.js +2 -0
  103. package/src/actions/guides/legends/lane.js +54 -19
  104. package/src/actions/guides/legends/lifecycle.js +19 -8
  105. package/src/actions/guides/legends/remove.js +13 -3
  106. package/src/actions/guides/legends/sampling.js +140 -0
  107. package/src/actions/guides/legends/size.js +93 -40
  108. package/src/actions/guides/legends/strokeWidth.js +34 -28
  109. package/src/actions/guides/legends/target.js +112 -1
  110. package/src/actions/guides/legends/transition.js +165 -16
  111. package/src/actions/guides/polar/axes/facade.js +4 -4
  112. package/src/actions/guides/polar/axes/labels.js +24 -5
  113. package/src/actions/guides/polar/axes/lines.js +10 -4
  114. package/src/actions/guides/polar/axes/shared.js +16 -0
  115. package/src/actions/guides/polar/axes/ticks.js +1 -1
  116. package/src/actions/guides/polar/axes/titles.js +1 -1
  117. package/src/actions/guides/polar/grids.js +1 -1
  118. package/src/actions/guides/polar/resolve.js +12 -8
  119. package/src/actions/index.js +14 -3
  120. package/src/actions/marks/arc/actions.js +77 -19
  121. package/src/actions/marks/area/actions.js +34 -9
  122. package/src/actions/marks/area/materialize.js +17 -1
  123. package/src/actions/marks/bar/create.js +11 -2
  124. package/src/actions/marks/bar/edit.js +20 -3
  125. package/src/actions/marks/bar/materialize.js +22 -2
  126. package/src/actions/marks/index.js +10 -1
  127. package/src/actions/marks/line/actions.js +37 -14
  128. package/src/actions/marks/line/materialize.js +29 -10
  129. package/src/actions/marks/point/create.js +8 -2
  130. package/src/actions/marks/point/edit.js +18 -6
  131. package/src/actions/marks/point/materialize.js +49 -15
  132. package/src/actions/marks/rect/actions.js +33 -12
  133. package/src/actions/marks/references.js +601 -30
  134. package/src/actions/marks/remove.js +71 -1
  135. package/src/actions/marks/rule/actions.js +47 -10
  136. package/src/actions/marks/shared.js +1 -1
  137. package/src/actions/marks/text/actions.js +74 -9
  138. package/src/actions/marks/text/index.js +4 -0
  139. package/src/actions/marks/text/layout.js +154 -6
  140. package/src/actions/marks/text/removal.js +173 -0
  141. package/src/actions/marks/text/selection.js +73 -0
  142. package/src/actions/marks/tick/actions.js +22 -5
  143. package/src/actions/primitives/editGraphics.js +26 -0
  144. package/src/actions/primitives/semantic.js +2 -1
  145. package/src/actions/primitives/semanticAction.js +83 -10
  146. package/src/actions/primitives/semanticValidation/guide.js +1 -1
  147. package/src/actions/primitives/semanticValidation/index.js +9 -1
  148. package/src/actions/primitives/semanticValidation/layer.js +5 -0
  149. package/src/actions/primitives/semanticValidation/scale.js +28 -7
  150. package/src/actions/regression/components.js +26 -11
  151. package/src/actions/regression/create.js +10 -2
  152. package/src/actions/regression/edit.js +21 -7
  153. package/src/actions/resources/index.js +7 -0
  154. package/src/actions/resources/remove.js +172 -0
  155. package/src/actions/scales/channels.js +212 -0
  156. package/src/actions/scales/consumers/common.js +1 -1
  157. package/src/actions/scales/consumers/families.js +3 -2
  158. package/src/actions/scales/consumers/index.js +19 -2
  159. package/src/actions/scales/consumers/seriesLayout.js +28 -14
  160. package/src/actions/scales/create.js +54 -22
  161. package/src/actions/scales/definitions.js +94 -40
  162. package/src/actions/scales/edit.js +66 -11
  163. package/src/actions/scales/editPolicy.js +81 -11
  164. package/src/actions/scales/index.js +6 -0
  165. package/src/actions/scales/materialize.js +4 -3
  166. package/src/actions/scales/offset.js +48 -0
  167. package/src/actions/scales/parallel.js +71 -0
  168. package/src/actions/scales/preview.js +111 -10
  169. package/src/actions/selection/actions.js +154 -30
  170. package/src/actions/selection/index.js +3 -0
  171. package/src/actions/theme/actions.js +68 -13
  172. package/src/actions/theme/composition.js +238 -0
  173. package/src/actions/theme/index.js +9 -2
  174. package/src/actions/theme/reconcile.js +315 -67
  175. package/src/actions/theme/state.js +160 -0
  176. package/src/actions/violinPlots/create.js +5 -2
  177. package/src/actions/violinPlots/edit.js +1 -0
  178. package/src/core/action.js +10 -0
  179. package/src/core/compositionState.js +6 -1
  180. package/src/core/programState.js +20 -0
  181. package/src/core/resourceReferences.js +332 -0
  182. package/src/core/vocabulary.js +16 -3
  183. package/src/grammar/bars/aggregate.js +4 -1
  184. package/src/grammar/bars/geometry.js +44 -0
  185. package/src/grammar/bin.js +54 -9
  186. package/src/grammar/complete.js +279 -0
  187. package/src/grammar/computed.js +265 -64
  188. package/src/grammar/coordinates.js +37 -0
  189. package/src/grammar/density.js +239 -37
  190. package/src/grammar/displayLabels.js +57 -0
  191. package/src/grammar/facets/dependencies.js +133 -1
  192. package/src/grammar/facets/guides.js +3 -0
  193. package/src/grammar/facets/index.js +7 -47
  194. package/src/grammar/facets/scales.js +15 -18
  195. package/src/grammar/impute.js +340 -0
  196. package/src/grammar/markLabelSelection.js +57 -0
  197. package/src/grammar/markLabels.js +18 -1
  198. package/src/grammar/normalize.js +366 -0
  199. package/src/grammar/pathSeries.js +26 -8
  200. package/src/grammar/pointShapes.js +13 -4
  201. package/src/grammar/polar.js +90 -8
  202. package/src/grammar/regression/index.js +1 -0
  203. package/src/grammar/regression/parameters.js +23 -0
  204. package/src/grammar/roundedRect.js +180 -0
  205. package/src/grammar/rules.js +1 -0
  206. package/src/grammar/scales/colorConsumers.js +20 -1
  207. package/src/grammar/scales/continuous.js +2 -2
  208. package/src/grammar/scales/definition.js +27 -4
  209. package/src/grammar/scales/index.js +1 -0
  210. package/src/grammar/scales/ordinal.js +12 -3
  211. package/src/grammar/scales/policies.js +2 -2
  212. package/src/grammar/scales/size.js +456 -0
  213. package/src/grammar/schemas/concreteGraphic.js +14 -2
  214. package/src/grammar/schemas/graphic.js +15 -0
  215. package/src/grammar/schemas/graphicBounds.js +179 -30
  216. package/src/grammar/schemas/semanticPath.js +7 -2
  217. package/src/grammar/statisticalReference.js +103 -0
  218. package/src/grammar/strokeStyle.js +77 -0
  219. package/src/grammar/summary.js +78 -16
  220. package/src/grammar/timeUnit.js +216 -5
  221. package/src/grammar/transformTopology.js +4 -0
  222. package/src/grammar/transforms.js +394 -28
  223. package/src/grammar/weightedStatistics.js +374 -0
  224. package/src/grammar/window.js +221 -16
  225. package/src/layout/aspect.js +80 -0
  226. package/src/layout/facets.js +59 -5
  227. package/src/layout/labels.js +343 -0
  228. package/src/materialization/bars/aggregate.js +25 -2
  229. package/src/materialization/bars/histogram.js +84 -24
  230. package/src/materialization/bars/ranged.js +19 -3
  231. package/src/materialization/bars/resolve.js +7 -4
  232. package/src/materialization/composition.js +3 -1
  233. package/src/materialization/coordinateBounds.js +204 -0
  234. package/src/materialization/dependencies.js +36 -0
  235. package/src/materialization/facetGuides/legacyCategorical.js +3 -0
  236. package/src/materialization/facetGuides/placement.js +8 -2
  237. package/src/materialization/facetGuides/preparation.js +12 -7
  238. package/src/materialization/facetHeaders.js +346 -0
  239. package/src/materialization/facets.js +15 -65
  240. package/src/materialization/guides/resources.js +26 -0
  241. package/src/materialization/legends.js +1 -1
  242. package/src/materialization/marks/index.js +11 -5
  243. package/src/materialization/marks/policies.js +2 -1
  244. package/src/materialization/rect.js +25 -12
  245. package/src/materialization/rectConfig.js +5 -1
  246. package/src/materialization/scales/map.js +5 -3
  247. package/src/materialization/scales/policies/binnedPosition.js +2 -2
  248. package/src/materialization/scales/policies/offset.js +50 -8
  249. package/src/materialization/scales/policies/series.js +4 -1
  250. package/src/materialization/scales/resolve.js +22 -6
  251. package/src/materialization/selection/items/arc.js +28 -4
  252. package/src/materialization/selection/items/bar.js +30 -3
  253. package/src/materialization/selection/items/common.js +19 -4
  254. package/src/materialization/selection/items/rect.js +28 -4
  255. package/src/materialization/selection/path.js +1 -1
  256. package/src/materialization/selection/point.js +3 -1
  257. package/src/materialization/text.js +265 -14
  258. package/src/renderers/canvas/circle.js +2 -3
  259. package/src/renderers/canvas/line.js +2 -3
  260. package/src/renderers/canvas/native.js +4 -4
  261. package/src/renderers/canvas/path.js +2 -3
  262. package/src/renderers/canvas/rect.js +2 -3
  263. package/src/renderers/canvas/stroke.js +11 -0
  264. package/src/renderers/svg.js +19 -0
  265. package/src/selectors/datasets.js +32 -1
  266. package/src/selectors/layers.js +4 -0
  267. package/src/theme/defaults.js +101 -0
  268. package/types/basic.d.ts +6 -0
  269. package/types/index.d.ts +85 -0
  270. package/types/program.d.ts +910 -117
@@ -1,4 +1,4 @@
1
- import { noOptions } from "../../../../core/validation.js";
1
+ import { validateOptionObject } from "../../../../core/validation.js";
2
2
  import { mapOrdinalValues } from "../../../../grammar/scales/index.js";
3
3
  import { DEFAULT_COLORS } from "../../../../theme/defaults.js";
4
4
  import { formatVisibleText } from "../../../../core/textMetrics.js";
@@ -10,25 +10,57 @@ import {
10
10
  import { resolveLegendItemLayout } from "../../../../layout/legendItems.js";
11
11
  import { createPointShapeGraphic } from "../../../../grammar/pointShapes.js";
12
12
  import { resolveConcreteGraphicBounds } from "../../../../grammar/schemas/graphicBounds.js";
13
+ import { resolveEffectiveLegendBlockConfig } from "../blocks.js";
14
+ import { resolveDisplayLabel } from "../../../../grammar/displayLabels.js";
15
+ import {
16
+ materializeRectItem,
17
+ requestedRectStyleDetails
18
+ } from "../../../../grammar/roundedRect.js";
19
+ import { requestedStrokeDetails } from "../../../../grammar/strokeStyle.js";
20
+
21
+ const CATEGORICAL_KINDS = Object.freeze(["series", "color", "stroke"]);
13
22
 
14
- export function activeConfig(program) {
15
- const kinds = ["series", "color"]
23
+ export function activeConfig(program, requested) {
24
+ if (requested !== undefined && !CATEGORICAL_KINDS.includes(requested)) {
25
+ throw new Error(`Unknown categorical legend kind "${requested}".`);
26
+ }
27
+ const kinds = CATEGORICAL_KINDS
16
28
  .filter(kind => program.guideConfigs.legend?.[kind] !== undefined);
29
+ if (requested !== undefined) {
30
+ if (!kinds.includes(requested)) {
31
+ throw new Error(`Missing categorical legend config "${requested}".`);
32
+ }
33
+ return {
34
+ kind: requested,
35
+ config: resolveEffectiveLegendBlockConfig(
36
+ program,
37
+ requested,
38
+ program.guideConfigs.legend[requested]
39
+ )
40
+ };
41
+ }
17
42
  if (kinds.length !== 1) {
18
43
  throw new Error("Legend component requires one categorical legend config.");
19
44
  }
20
45
  const kind = kinds[0];
21
- return { kind, config: program.guideConfigs.legend[kind] };
46
+ return {
47
+ kind,
48
+ config: resolveEffectiveLegendBlockConfig(
49
+ program,
50
+ kind,
51
+ program.guideConfigs.legend[kind]
52
+ )
53
+ };
22
54
  }
23
55
 
24
56
  function prefix(config) {
25
- return config.kind === "series" ? "seriesLegend" : "colorLegend";
57
+ return `${config.kind}Legend`;
26
58
  }
27
59
 
28
60
  export function symbolGraphic(config, type) {
29
61
  const onlyDefault = config.symbol.layers.length === 1 &&
30
62
  ((config.kind === "series" && type === "line") ||
31
- (config.kind === "color" && type === "swatch"));
63
+ (["color", "stroke"].includes(config.kind) && type === "swatch"));
32
64
  if (onlyDefault) return `${prefix(config)}Symbols`;
33
65
  const suffix = { line: "Lines", point: "Points", swatch: "Swatches" }[type];
34
66
  return `${prefix(config)}Symbol${suffix}`;
@@ -46,6 +78,14 @@ export function symbolWidth(config) {
46
78
  }));
47
79
  }
48
80
 
81
+ export function categoricalLegendLabels(config) {
82
+ return config.domain.map(value => resolveDisplayLabel(
83
+ value,
84
+ config.labelMap,
85
+ formatVisibleText
86
+ ));
87
+ }
88
+
49
89
  function resolveSampleBounds(program, config, width) {
50
90
  const appearance = resolveAppearance(program, config);
51
91
  return config.domain.map((_, index) => {
@@ -53,21 +93,56 @@ function resolveSampleBounds(program, config, width) {
53
93
  if (layer.type === "line") {
54
94
  const x = (width - layer.length) / 2;
55
95
  return { type: "line", properties: { x1: x, x2: x + layer.length, y1: 0, y2: 0,
56
- strokeWidth: layer.lineWidth } };
96
+ stroke: layer.stroke ?? (config.channels.includes("stroke")
97
+ ? appearance.strokes[index]
98
+ : appearance.colors[index]),
99
+ strokeWidth: layer.lineWidth, opacity: layer.opacity,
100
+ ...requestedStrokeDetails(layer, "Legend line symbol") } };
57
101
  }
58
102
  if (layer.type === "swatch") {
59
- return { type: "rect", properties: { x: (width - layer.width) / 2, y: -layer.height / 2,
60
- width: layer.width, height: layer.height, strokeWidth: layer.strokeWidth } };
103
+ const details = requestedRectStyleDetails(layer, "Legend swatch symbol");
104
+ return materializeRectItem({ x: (width - layer.width) / 2, y: -layer.height / 2,
105
+ width: layer.width, height: layer.height, strokeWidth: layer.strokeWidth,
106
+ stroke: config.channels.includes("stroke")
107
+ ? appearance.strokes[index]
108
+ : layer.stroke,
109
+ opacity: layer.opacity,
110
+ ...requestedStrokeDetails(details, "Legend swatch symbol")
111
+ }, details.cornerRadius ?? 0);
61
112
  }
62
113
  if (config.channels.includes("shape")) {
63
114
  return createPointShapeGraphic({ shape: appearance.shapes[index], x: width / 2, y: 0,
64
115
  area: Math.PI * layer.size ** 2, fill: layer.fill ?? appearance.colors[index],
65
- stroke: layer.stroke, strokeWidth: layer.strokeWidth });
116
+ stroke: layer.stroke, strokeWidth: layer.strokeWidth, opacity: layer.opacity,
117
+ ...requestedStrokeDetails(layer, "Legend point symbol") });
66
118
  }
67
119
  return { type: "circle", properties: { x: width / 2, y: 0, radius: layer.size,
68
- strokeWidth: layer.strokeWidth } };
120
+ stroke: config.channels.includes("stroke")
121
+ ? appearance.strokes[index]
122
+ : layer.stroke,
123
+ strokeWidth: layer.strokeWidth, opacity: layer.opacity,
124
+ ...requestedStrokeDetails(layer, "Legend point symbol") } };
69
125
  });
70
- return resolveConcreteGraphicBounds({ objects: { sample: { type: "collection", items } }, order: ["sample"] }, "sample");
126
+ return items.map((item, itemIndex) => {
127
+ const bounds = resolveConcreteGraphicBounds({
128
+ objects: { sample: item },
129
+ order: ["sample"]
130
+ }, "sample");
131
+ if (config.symbol.layers[itemIndex].type !== "line") return bounds;
132
+ const extent = config.symbol.layers[itemIndex].lineWidth / 2;
133
+ return {
134
+ ...bounds,
135
+ left: bounds.left - extent,
136
+ right: bounds.right + extent
137
+ };
138
+ }).reduce((result, bounds) => result === undefined
139
+ ? bounds
140
+ : {
141
+ left: Math.min(result.left, bounds.left),
142
+ right: Math.max(result.right, bounds.right),
143
+ top: Math.min(result.top, bounds.top),
144
+ bottom: Math.max(result.bottom, bounds.bottom)
145
+ }, undefined);
71
146
  });
72
147
  }
73
148
 
@@ -75,7 +150,7 @@ export function resolveLayout(program, config) {
75
150
  const { plot, canvas } = resolveContinuousBounds(program,
76
151
  "Legend layout requires Canvas bounds, width, and height.");
77
152
  const width = symbolWidth(config);
78
- const layout = resolveLegendItemLayout(plot, config, config.domain.map(formatVisibleText), {
153
+ const layout = resolveLegendItemLayout(plot, config, categoricalLegendLabels(config), {
79
154
  width, height: 0, itemBounds: resolveSampleBounds(program, config, width)
80
155
  }, canvas);
81
156
  assertLegendBoundsInsideCanvas(layout.bounds, canvas, "Categorical legend layout", config);
@@ -86,19 +161,26 @@ export function resolveLayout(program, config) {
86
161
 
87
162
  export function resolveAppearance(program, config) {
88
163
  let colors = config.domain.map(() => DEFAULT_COLORS.mark);
164
+ let strokes = config.domain.map(() => "white");
89
165
  let dashes = config.domain.map(() => []);
90
166
  let shapes = config.domain.map(() => "circle");
91
167
  for (let index = 0; index < config.channels.length; index += 1) {
92
168
  const scale = program.resolvedScales[config.scales[index]];
93
169
  const values = mapOrdinalValues(config.domain, scale.domain, scale.range);
94
170
  if (config.channels[index] === "color") colors = values;
171
+ if (config.channels[index] === "stroke") strokes = values;
95
172
  if (config.channels[index] === "strokeDash") dashes = values;
96
173
  if (config.channels[index] === "shape") shapes = values;
97
174
  }
98
- return { colors, dashes, shapes };
175
+ return { colors, strokes, dashes, shapes };
99
176
  }
100
177
 
101
- export { noOptions };
178
+ export function noOptions(args, operation) {
179
+ validateOptionObject(args, ["kind"], operation);
180
+ if (args.kind !== undefined && !CATEGORICAL_KINDS.includes(args.kind)) {
181
+ throw new Error(`Unknown categorical legend kind "${args.kind}".`);
182
+ }
183
+ }
102
184
 
103
185
  export function layerFor(config, type) {
104
186
  const layer = config.symbol.layers.find(item => item.type === type);
@@ -88,7 +88,7 @@ function normalizeBorder(border) {
88
88
  return normalized;
89
89
  }
90
90
 
91
- export function normalizeOptions(args, kind) {
91
+ export function normalizeOptions(args, kind, { internalSymbol = false } = {}) {
92
92
  validateOptionObject(args, OPTIONS, "createLegend");
93
93
  if (Object.hasOwn(args, "labels")) {
94
94
  validateObject(args.labels, TEXT_OPTIONS, "createLegend.labels");
@@ -191,7 +191,7 @@ export function normalizeOptions(args, kind) {
191
191
  offset,
192
192
  titlePosition,
193
193
  title: args.title,
194
- symbol: normalizeRecipe(args.symbol, kind),
194
+ symbol: normalizeRecipe(args.symbol, kind, { internal: internalSymbol }),
195
195
  labels,
196
196
  titleStyle,
197
197
  itemGap,
@@ -5,6 +5,15 @@ import {
5
5
  positive,
6
6
  validateKeys
7
7
  } from "./validation.js";
8
+ import {
9
+ requestedRectStyleDetails,
10
+ RECT_STYLE_PROPERTIES
11
+ } from "../../../../grammar/roundedRect.js";
12
+ import {
13
+ requestedStrokeDetails,
14
+ STROKE_STYLE_PROPERTIES
15
+ } from "../../../../grammar/strokeStyle.js";
16
+ import { findLayerMatching } from "../../../../selectors/layers.js";
8
17
 
9
18
  const LAYER_OPTIONS = Object.freeze({
10
19
  line: Object.freeze(["type", "length", "lineWidth"]),
@@ -24,19 +33,101 @@ const LAYER_OPTIONS = Object.freeze({
24
33
  "strokeWidth"
25
34
  ])
26
35
  });
36
+ const INTERNAL_LAYER_OPTIONS = Object.freeze({
37
+ line: Object.freeze([...LAYER_OPTIONS.line, ...STROKE_STYLE_PROPERTIES]),
38
+ point: Object.freeze([...LAYER_OPTIONS.point, ...STROKE_STYLE_PROPERTIES]),
39
+ swatch: Object.freeze([...LAYER_OPTIONS.swatch, ...RECT_STYLE_PROPERTIES])
40
+ });
27
41
 
28
- export function resolveLegendSymbol(program, layer, channels, requested) {
42
+ export function resolveLegendSymbol(program, layer, channels, requested, kind) {
29
43
  if (requested !== undefined && requested !== "auto") return requested;
30
- if (layer.mark?.type !== "point" || !channels?.includes("shape")) return undefined;
31
- const color = channels.includes("color") ? layer.encoding?.color : undefined;
32
- const hasMatchingLine = color?.scale !== undefined && program.semanticSpec.layers.some(candidate =>
33
- candidate.mark?.type === "line" && candidate.encoding?.color?.field === color.field &&
34
- candidate.encoding?.color?.scale === color.scale
35
- );
36
- return { layers: [
37
- ...(hasMatchingLine ? [{ type: "line", length: 32, lineWidth: 3 }] : []),
38
- { type: "point", size: Math.sqrt(64 / Math.PI), stroke: "white", strokeWidth: 0 }
39
- ] };
44
+ const config = program.markConfigs[layer.id] ?? {};
45
+ const bar = config.barAppearance ?? {};
46
+ const styleOwner = layer.mark.type === "bar" ? bar : config;
47
+ const strokeDetails = requestedStrokeDetails(styleOwner, "Legend source");
48
+ const rectDetails = ["bar", "rect"].includes(layer.mark.type)
49
+ ? requestedRectStyleDetails(styleOwner, "Legend source")
50
+ : strokeDetails;
51
+ if (channels?.length === 1 && channels[0] === "stroke") {
52
+ const strokeWidth = bar.strokeWidth ?? config.strokeWidth ?? ({
53
+ point: 1,
54
+ line: 2,
55
+ area: 1,
56
+ bar: 0.5,
57
+ rect: 1,
58
+ arc: 1,
59
+ rule: 2,
60
+ tick: 2
61
+ }[layer.mark.type] ?? 1);
62
+ if (["line", "rule", "tick"].includes(layer.mark.type)) {
63
+ return { layers: [{
64
+ type: "line",
65
+ length: 32,
66
+ lineWidth: strokeWidth,
67
+ ...strokeDetails
68
+ }] };
69
+ }
70
+ if (layer.mark.type === "point") {
71
+ return { layers: [{
72
+ type: "point",
73
+ size: 5,
74
+ fill: config.fill ?? "#4c78a8",
75
+ stroke: "white",
76
+ strokeWidth,
77
+ ...strokeDetails
78
+ }] };
79
+ }
80
+ return { layers: [{
81
+ type: "swatch",
82
+ width: 14,
83
+ height: 12,
84
+ stroke: "white",
85
+ strokeWidth,
86
+ ...rectDetails
87
+ }] };
88
+ }
89
+ if (layer.mark?.type === "point" && channels?.includes("shape")) {
90
+ const color = channels.includes("color") ? layer.encoding?.color : undefined;
91
+ const matchingLine = color?.scale === undefined
92
+ ? undefined
93
+ : findLayerMatching(program, candidate =>
94
+ candidate.mark?.type === "line" &&
95
+ candidate.encoding?.color?.field === color.field &&
96
+ candidate.encoding?.color?.scale === color.scale
97
+ );
98
+ const lineDetails = matchingLine === undefined
99
+ ? {}
100
+ : requestedStrokeDetails(
101
+ program.markConfigs[matchingLine.id] ?? {},
102
+ "Legend line source"
103
+ );
104
+ return { layers: [
105
+ ...(matchingLine !== undefined ? [{
106
+ type: "line",
107
+ length: 32,
108
+ lineWidth: 3,
109
+ ...lineDetails
110
+ }] : []),
111
+ {
112
+ type: "point",
113
+ size: Math.sqrt(64 / Math.PI),
114
+ stroke: "white",
115
+ strokeWidth: 0,
116
+ ...strokeDetails
117
+ }
118
+ ] };
119
+ }
120
+ if (Object.keys(rectDetails).length === 0) return undefined;
121
+ return kind === "series"
122
+ ? { layers: [{ type: "line", length: 32, lineWidth: 2, ...strokeDetails }] }
123
+ : { layers: [{
124
+ type: "swatch",
125
+ width: 14,
126
+ height: 12,
127
+ stroke: "white",
128
+ strokeWidth: 0.5,
129
+ ...rectDetails
130
+ }] };
40
131
  }
41
132
 
42
133
  function defaultRecipe(kind) {
@@ -53,20 +144,20 @@ function defaultRecipe(kind) {
53
144
  };
54
145
  }
55
146
 
56
- function normalizeLayer(layer) {
147
+ function normalizeLayer(layer, internal) {
57
148
  if (!isPlainObject(layer) || !Object.hasOwn(LAYER_OPTIONS, layer.type)) {
58
149
  throw new Error("Legend symbol layer type must be line, point, or swatch.");
59
150
  }
60
151
  validateKeys(
61
152
  layer,
62
- LAYER_OPTIONS[layer.type],
153
+ internal ? INTERNAL_LAYER_OPTIONS[layer.type] : LAYER_OPTIONS[layer.type],
63
154
  `createLegend.symbol.${layer.type}`
64
155
  );
65
156
  if (layer.type === "line") {
66
157
  const normalized = { length: 32, lineWidth: 2, ...layer };
67
158
  positive(normalized.length, "Legend line symbol length");
68
159
  nonNegative(normalized.lineWidth, "Legend line symbol lineWidth");
69
- return normalized;
160
+ return { ...normalized, ...requestedStrokeDetails(normalized, "Legend line symbol") };
70
161
  }
71
162
  if (layer.type === "point") {
72
163
  const normalized = {
@@ -85,7 +176,7 @@ function normalizeLayer(layer) {
85
176
  if (normalized.fill !== undefined) {
86
177
  nonEmptyString(normalized.fill, "Legend point symbol fill");
87
178
  }
88
- return normalized;
179
+ return { ...normalized, ...requestedStrokeDetails(normalized, "Legend point symbol") };
89
180
  }
90
181
  const normalized = {
91
182
  width: 14,
@@ -98,10 +189,10 @@ function normalizeLayer(layer) {
98
189
  positive(normalized.height, "Legend swatch height");
99
190
  nonEmptyString(normalized.stroke, "Legend swatch stroke");
100
191
  nonNegative(normalized.strokeWidth, "Legend swatch strokeWidth");
101
- return normalized;
192
+ return { ...normalized, ...requestedRectStyleDetails(normalized, "Legend swatch symbol") };
102
193
  }
103
194
 
104
- export function normalizeRecipe(symbol, kind) {
195
+ export function normalizeRecipe(symbol, kind, { internal = false } = {}) {
105
196
  if (symbol === undefined || symbol === "auto") return defaultRecipe(kind);
106
197
  if (!isPlainObject(symbol)) {
107
198
  throw new TypeError('createLegend.symbol must be "auto" or a plain object.');
@@ -118,17 +209,17 @@ export function normalizeRecipe(symbol, kind) {
118
209
  "Legend symbol recipe supports at most one layer per type."
119
210
  );
120
211
  }
121
- layers = symbol.layers.map(normalizeLayer);
212
+ layers = symbol.layers.map(layer => normalizeLayer(layer, internal));
122
213
  } else if (kind === "series") {
123
214
  validateKeys(symbol, ["length", "lineWidth"], "createLegend.symbol");
124
- layers = [normalizeLayer({ type: "line", ...symbol })];
215
+ layers = [normalizeLayer({ type: "line", ...symbol }, internal)];
125
216
  } else {
126
217
  validateKeys(
127
218
  symbol,
128
219
  ["width", "height", "stroke", "strokeWidth"],
129
220
  "createLegend.symbol"
130
221
  );
131
- layers = [normalizeLayer({ type: "swatch", ...symbol })];
222
+ layers = [normalizeLayer({ type: "swatch", ...symbol }, internal)];
132
223
  }
133
224
 
134
225
  const types = layers.map(layer => layer.type);
@@ -17,8 +17,11 @@ function isCategoricalTarget(layer) {
17
17
  channel => layer.encoding?.[channel]?.scale !== undefined
18
18
  );
19
19
  }
20
- return ["bar", "area", "arc", "rect"].includes(layer?.mark?.type) &&
21
- layer.encoding?.color?.scale !== undefined;
20
+ return ["bar", "area", "arc", "rect", "rule", "tick"].includes(
21
+ layer?.mark?.type
22
+ ) && ["color", "stroke"].some(
23
+ channel => layer.encoding?.[channel]?.scale !== undefined
24
+ );
22
25
  }
23
26
 
24
27
  export function resolveTarget(program, requested) {
@@ -45,6 +48,7 @@ export function resolveTarget(program, requested) {
45
48
  export const sameValues = sameOrderedValues;
46
49
 
47
50
  function resolveLegendKind(layer, requestedChannels) {
51
+ if (sameValues(requestedChannels, ["stroke"])) return "stroke";
48
52
  if (["bar", "area", "arc", "rect"].includes(layer.mark.type)) return "color";
49
53
  if (
50
54
  layer.mark.type === "point" &&
@@ -78,9 +82,9 @@ function resolveOrdinalScales(program, scaleIds) {
78
82
  }
79
83
 
80
84
  export function resolveDefinition(program, layer, requestedChannels, requestedTitle, order) {
81
- const channels = requestedChannels ?? (["line", "point"].includes(layer.mark.type)
82
- ? CHANNELS.filter(channel => layer.encoding?.[channel]?.scale !== undefined)
83
- : ["color"]);
85
+ const channels = requestedChannels ?? CHANNELS.filter(
86
+ channel => layer.encoding?.[channel]?.scale !== undefined
87
+ );
84
88
  const kind = resolveLegendKind(layer, channels);
85
89
  if (
86
90
  !Array.isArray(channels) ||
@@ -92,8 +96,8 @@ export function resolveDefinition(program, layer, requestedChannels, requestedTi
92
96
  "Legend channels must be a non-empty unique color/strokeDash/shape array."
93
97
  );
94
98
  }
95
- if (kind === "color" && !sameValues(channels, ["color"])) {
96
- throw new Error("Color legends currently support only the color channel.");
99
+ if (["color", "stroke"].includes(kind) && !sameValues(channels, [kind])) {
100
+ throw new Error(`${kind} legends currently support only the ${kind} channel.`);
97
101
  }
98
102
 
99
103
  const encodings = channels.map(channel => {
@@ -138,7 +142,7 @@ export function resolveCurrentDefinition(program, config) {
138
142
  if (guide === undefined) {
139
143
  throw new Error("Legend rematerialization requires semantic guide state.");
140
144
  }
141
- const channels = config.kind === "series" ? guide.channels : ["color"];
145
+ const channels = config.kind === "series" ? guide.channels : [config.kind];
142
146
  return resolveDefinition(
143
147
  program,
144
148
  layer,
@@ -11,6 +11,18 @@ import {
11
11
  } from "./layout.js";
12
12
  import { resolveCategoricalLegendPlacement } from "../lifecycle.js";
13
13
  import { createPointShapeGraphic } from "../../../../grammar/pointShapes.js";
14
+ import {
15
+ materializeRectItem,
16
+ requestedRectStyleDetails
17
+ } from "../../../../grammar/roundedRect.js";
18
+ import { requestedStrokeDetails } from "../../../../grammar/strokeStyle.js";
19
+ import { replaceGraphicItems } from "../../../primitives/editGraphics.js";
20
+
21
+ function applyLayerOpacity(program, id, opacity) {
22
+ return opacity === undefined
23
+ ? program
24
+ : program.editGraphics({ target: id, property: "opacity", value: opacity });
25
+ }
14
26
 
15
27
  function makeEditSymbol(type) {
16
28
  const suffix = { line: "Lines", point: "Points", swatch: "Swatches" }[type];
@@ -19,20 +31,23 @@ function makeEditSymbol(type) {
19
31
  { op, description: `Rematerialize categorical legend ${type} symbols.` },
20
32
  function (args = {}) {
21
33
  noOptions(args, op);
22
- const { config } = activeConfig(this);
34
+ const { config } = activeConfig(this, args.kind);
23
35
  const layer = layerFor(config, type);
24
36
  const id = symbolGraphic(config, type);
25
37
  const dynamicPoint = type === "point" && config.channels.includes("shape");
26
- const expected = dynamicPoint
38
+ const styledSwatch = type === "swatch" && Object.hasOwn(layer, "cornerRadius");
39
+ const expected = dynamicPoint || styledSwatch
27
40
  ? "collection"
28
41
  : { line: "line", point: "circle", swatch: "rect" }[type];
29
- if (this.graphicSpec.objects[id]?.type !== expected) {
42
+ const actual = this.graphicSpec.objects[id]?.type;
43
+ const compatibleStyledSwatch = styledSwatch && ["rect", "collection"].includes(actual);
44
+ if (actual !== expected && !compatibleStyledSwatch) {
30
45
  throw new Error(`${op} requires existing ${type} symbols.`);
31
46
  }
32
47
  const layout = resolveLayout(this, config);
33
48
  const appearance = resolveAppearance(this, config);
34
49
  let next = this;
35
- if (!dynamicPoint) {
50
+ if (!dynamicPoint && !styledSwatch) {
36
51
  next = next.editGraphics({
37
52
  target: id,
38
53
  property: "length",
@@ -43,7 +58,7 @@ function makeEditSymbol(type) {
43
58
  const x1 = layout.symbolX.map(
44
59
  value => value + (symbolWidth(config) - layer.length) / 2
45
60
  );
46
- return next
61
+ next = next
47
62
  .editGraphics({ target: id, property: "x1", value: x1 })
48
63
  .editGraphics({ target: id, property: "y1", value: layout.itemY })
49
64
  .editGraphics({
@@ -52,9 +67,21 @@ function makeEditSymbol(type) {
52
67
  value: x1.map(value => value + layer.length)
53
68
  })
54
69
  .editGraphics({ target: id, property: "y2", value: layout.itemY })
55
- .editGraphics({ target: id, property: "stroke", value: appearance.colors })
70
+ .editGraphics({
71
+ target: id,
72
+ property: "stroke",
73
+ value: layer.stroke ?? (config.channels.includes("stroke")
74
+ ? appearance.strokes
75
+ : appearance.colors)
76
+ })
56
77
  .editGraphics({ target: id, property: "strokeWidth", value: layer.lineWidth })
57
78
  .editGraphics({ target: id, property: "strokeDash", value: appearance.dashes });
79
+ for (const [property, value] of Object.entries(
80
+ requestedStrokeDetails(layer, "Legend line symbol")
81
+ )) {
82
+ next = next.editGraphics({ target: id, property, value });
83
+ }
84
+ return applyLayerOpacity(next, id, layer.opacity);
58
85
  }
59
86
  if (type === "point") {
60
87
  const x = layout.symbolX.map(value => value + symbolWidth(config) / 2);
@@ -67,8 +94,12 @@ function makeEditSymbol(type) {
67
94
  y: layout.itemY[index],
68
95
  area: Math.PI * layer.size ** 2,
69
96
  fill,
70
- stroke: layer.stroke,
71
- strokeWidth: layer.strokeWidth
97
+ stroke: config.channels.includes("stroke")
98
+ ? appearance.strokes[index]
99
+ : layer.stroke,
100
+ strokeWidth: layer.strokeWidth,
101
+ opacity: layer.opacity,
102
+ ...requestedStrokeDetails(layer, "Legend point symbol")
72
103
  });
73
104
  });
74
105
  return next.editGraphics({
@@ -77,7 +108,7 @@ function makeEditSymbol(type) {
77
108
  value: items
78
109
  });
79
110
  }
80
- return next
111
+ next = next
81
112
  .editGraphics({ target: id, property: "x", value: x })
82
113
  .editGraphics({ target: id, property: "y", value: layout.itemY })
83
114
  .editGraphics({ target: id, property: "radius", value: layer.size })
@@ -86,24 +117,47 @@ function makeEditSymbol(type) {
86
117
  property: "fill",
87
118
  value: layer.fill ?? appearance.colors
88
119
  })
89
- .editGraphics({ target: id, property: "stroke", value: layer.stroke })
120
+ .editGraphics({
121
+ target: id,
122
+ property: "stroke",
123
+ value: config.channels.includes("stroke")
124
+ ? appearance.strokes
125
+ : layer.stroke
126
+ })
90
127
  .editGraphics({ target: id, property: "strokeWidth", value: layer.strokeWidth });
128
+ for (const [property, value] of Object.entries(
129
+ requestedStrokeDetails(layer, "Legend point symbol")
130
+ )) {
131
+ next = next.editGraphics({ target: id, property, value });
132
+ }
133
+ return applyLayerOpacity(next, id, layer.opacity);
91
134
  }
92
135
  const x = layout.symbolX.map(
93
136
  value => value + (symbolWidth(config) - layer.width) / 2
94
137
  );
95
- return next
96
- .editGraphics({ target: id, property: "x", value: x })
97
- .editGraphics({
98
- target: id,
99
- property: "y",
100
- value: layout.itemY.map(value => value - layer.height / 2)
101
- })
102
- .editGraphics({ target: id, property: "width", value: layer.width })
103
- .editGraphics({ target: id, property: "height", value: layer.height })
104
- .editGraphics({ target: id, property: "fill", value: appearance.colors })
105
- .editGraphics({ target: id, property: "stroke", value: layer.stroke })
106
- .editGraphics({ target: id, property: "strokeWidth", value: layer.strokeWidth });
138
+ const details = requestedRectStyleDetails(layer, "Legend swatch symbol");
139
+ const fills = layer.fill === undefined
140
+ ? appearance.colors
141
+ : config.domain.map(() => layer.fill);
142
+ const strokes = config.channels.includes("stroke")
143
+ ? appearance.strokes
144
+ : config.domain.map(() => layer.stroke);
145
+ return replaceGraphicItems(
146
+ next,
147
+ id,
148
+ "rect",
149
+ config.domain.map((_, index) => materializeRectItem({
150
+ x: x[index],
151
+ y: layout.itemY[index] - layer.height / 2,
152
+ width: layer.width,
153
+ height: layer.height,
154
+ fill: fills[index],
155
+ stroke: strokes[index],
156
+ strokeWidth: layer.strokeWidth,
157
+ ...(layer.opacity === undefined ? {} : { opacity: layer.opacity }),
158
+ ...requestedStrokeDetails(details, "Legend swatch symbol")
159
+ }, details.cornerRadius ?? 0))
160
+ );
107
161
  }
108
162
  );
109
163
  }
@@ -115,13 +169,14 @@ function makeCreateSymbol(type, edit) {
115
169
  { op, description: `Create categorical legend ${type} symbols.` },
116
170
  function (args = {}) {
117
171
  noOptions(args, op);
118
- const { config } = activeConfig(this);
119
- layerFor(config, type);
172
+ const { config } = activeConfig(this, args.kind);
173
+ const layer = layerFor(config, type);
120
174
  const id = symbolGraphic(config, type);
121
175
  if (this.graphicSpec.objects[id] !== undefined) {
122
176
  throw new Error(`${op} requires missing ${type} symbols.`);
123
177
  }
124
- const graphicType = type === "point" && config.channels.includes("shape")
178
+ const graphicType = type === "point" && config.channels.includes("shape") ||
179
+ type === "swatch" && (layer.cornerRadius ?? 0) > 0
125
180
  ? "collection"
126
181
  : { line: "line", point: "circle", swatch: "rect" }[type];
127
182
  return this
@@ -136,7 +191,7 @@ function makeCreateSymbol(type, edit) {
136
191
  ? {}
137
192
  : { length: config.domain.length })
138
193
  })
139
- [edit]();
194
+ [edit]({ kind: config.kind });
140
195
  }
141
196
  );
142
197
  }
@@ -161,7 +216,7 @@ export const createLegendSymbols = action(
161
216
  { op: "createLegendSymbols", description: "Create layered legend symbols." },
162
217
  function (args = {}) {
163
218
  noOptions(args, "createLegendSymbols");
164
- const { config } = activeConfig(this);
219
+ const { config } = activeConfig(this, args.kind);
165
220
  let next = this;
166
221
  for (const layer of config.symbol.layers) {
167
222
  const operation = {
@@ -169,7 +224,7 @@ export const createLegendSymbols = action(
169
224
  point: "createLegendSymbolPoints",
170
225
  swatch: "createLegendSymbolSwatches"
171
226
  }[layer.type];
172
- next = next[operation]();
227
+ next = next[operation]({ kind: config.kind });
173
228
  }
174
229
  return next;
175
230
  }
@@ -179,7 +234,7 @@ export const rematerializeLegendSymbols = action(
179
234
  { op: "rematerializeLegendSymbols", description: "Rematerialize layered legend symbols." },
180
235
  function (args = {}) {
181
236
  noOptions(args, "rematerializeLegendSymbols");
182
- const { config } = activeConfig(this);
237
+ const { config } = activeConfig(this, args.kind);
183
238
  let next = this;
184
239
  for (const layer of config.symbol.layers) {
185
240
  const operation = {
@@ -187,7 +242,7 @@ export const rematerializeLegendSymbols = action(
187
242
  point: "rematerializeLegendSymbolPoints",
188
243
  swatch: "rematerializeLegendSymbolSwatches"
189
244
  }[layer.type];
190
- next = next[operation]();
245
+ next = next[operation]({ kind: config.kind });
191
246
  }
192
247
  return next;
193
248
  }