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
@@ -0,0 +1,204 @@
1
+ import { isSourceOwnedText } from "../grammar/text.js";
2
+ import { resolvePolarFrame } from "../grammar/polar.js";
3
+ import { resolveEffectiveBounds } from "../layout/aspect.js";
4
+ import { resolveGraphicBounds } from "../layout/canvas.js";
5
+ import { findCoordinate, requireCoordinate } from "../selectors/coordinates.js";
6
+ import { findSemanticScale } from "../selectors/scales.js";
7
+
8
+ const DATA_CHANNELS = Object.freeze(["x", "y"]);
9
+ const SECONDARY_CHANNELS = Object.freeze({ x2: "x", y2: "y" });
10
+
11
+ function samePair(left, right) {
12
+ return left.x === right.x && left.y === right.y;
13
+ }
14
+
15
+ export function resolveDataAspectScalePair(program, target) {
16
+ const coordinate = typeof target === "string"
17
+ ? requireCoordinate(program, target)
18
+ : target;
19
+ if (coordinate.type !== "cartesian") {
20
+ throw new Error("Coordinate data aspect requires a Cartesian coordinate.");
21
+ }
22
+ const layers = program.semanticSpec.layers.filter(layer =>
23
+ layer.coordinate === coordinate.id &&
24
+ !isSourceOwnedText(layer) &&
25
+ DATA_CHANNELS.some(channel => layer.encoding?.[channel] !== undefined)
26
+ );
27
+ if (layers.length === 0) {
28
+ throw new Error(
29
+ `Coordinate "${coordinate.id}" data aspect requires active x and y consumers.`
30
+ );
31
+ }
32
+ const pairs = layers.map(layer => {
33
+ for (const channel of DATA_CHANNELS) {
34
+ const encoding = layer.encoding?.[channel];
35
+ if (
36
+ encoding?.scale === undefined ||
37
+ encoding.fieldType !== "quantitative"
38
+ ) {
39
+ throw new Error(
40
+ `Coordinate "${coordinate.id}" data aspect requires quantitative x and y scale consumers.`
41
+ );
42
+ }
43
+ }
44
+ for (const [secondary, primary] of Object.entries(SECONDARY_CHANNELS)) {
45
+ const encoding = layer.encoding?.[secondary];
46
+ if (encoding !== undefined && (
47
+ encoding.fieldType !== "quantitative" ||
48
+ encoding.scale !== layer.encoding[primary].scale
49
+ )) {
50
+ throw new Error(
51
+ `Coordinate "${coordinate.id}" data aspect requires ${secondary} to share its quantitative ${primary} scale.`
52
+ );
53
+ }
54
+ }
55
+ return { x: layer.encoding.x.scale, y: layer.encoding.y.scale };
56
+ });
57
+ if (pairs.some(pair => !samePair(pair, pairs[0]))) {
58
+ throw new Error(
59
+ `Coordinate "${coordinate.id}" data aspect requires one consistent x/y scale pair.`
60
+ );
61
+ }
62
+ const scales = {
63
+ x: findSemanticScale(program, pairs[0].x),
64
+ y: findSemanticScale(program, pairs[0].y)
65
+ };
66
+ for (const channel of DATA_CHANNELS) {
67
+ if (scales[channel]?.type !== "linear") {
68
+ throw new Error(
69
+ `Coordinate data aspect requires a linear ${channel} scale.`
70
+ );
71
+ }
72
+ }
73
+ return Object.freeze({
74
+ x: pairs[0].x,
75
+ y: pairs[0].y
76
+ });
77
+ }
78
+
79
+ export function findDataAspectCoordinatesForScale(program, scaleId) {
80
+ return program.semanticSpec.coordinates.filter(coordinate => {
81
+ if (coordinate.aspect?.mode !== "data") return false;
82
+ const pair = resolveDataAspectScalePair(program, coordinate);
83
+ return pair.x === scaleId || pair.y === scaleId;
84
+ });
85
+ }
86
+
87
+ function sameNumericPair(left, right) {
88
+ return Array.isArray(left) && left.length === 2 &&
89
+ left.every((value, index) => Number.isFinite(value) &&
90
+ Math.abs(value - right[index]) <= 1e-9);
91
+ }
92
+
93
+ function validateExplicitDataRanges(program, pair, bounds) {
94
+ const expected = {
95
+ x: [bounds.x, bounds.x + bounds.width],
96
+ y: [bounds.y + bounds.height, bounds.y]
97
+ };
98
+ for (const channel of DATA_CHANNELS) {
99
+ const scale = findSemanticScale(program, pair[channel]);
100
+ if (scale.range === "auto") continue;
101
+ const directed = scale.reverse === true
102
+ ? [...expected[channel]].reverse()
103
+ : expected[channel];
104
+ if (!sameNumericPair(scale.range, directed)) {
105
+ throw new Error(
106
+ `Coordinate data aspect conflicts with explicit ${channel} scale range "${scale.id}".`
107
+ );
108
+ }
109
+ }
110
+ }
111
+
112
+ export function resolveCoordinateBounds(program, target, {
113
+ resolvedScales = program.resolvedScales
114
+ } = {}) {
115
+ const allocated = resolveGraphicBounds(program);
116
+ if (target === undefined) return allocated;
117
+ const coordinate = requireCoordinate(program, target);
118
+ const aspect = coordinate.aspect;
119
+ if (aspect === undefined || aspect === "auto") return allocated;
120
+ if (aspect.mode !== "data") {
121
+ return resolveEffectiveBounds(allocated, aspect);
122
+ }
123
+ const pair = resolveDataAspectScalePair(program, coordinate);
124
+ const x = resolvedScales[pair.x];
125
+ const y = resolvedScales[pair.y];
126
+ if (x?.type !== "linear" || y?.type !== "linear") {
127
+ throw new Error(
128
+ `Coordinate "${coordinate.id}" data aspect requires resolved linear x and y scales.`
129
+ );
130
+ }
131
+ const bounds = resolveEffectiveBounds(allocated, aspect, {
132
+ x: x.domain,
133
+ y: y.domain
134
+ });
135
+ validateExplicitDataRanges(program, pair, bounds);
136
+ return bounds;
137
+ }
138
+
139
+ export function resolveCoordinatePolarFrame(program, target, options) {
140
+ const coordinate = requireCoordinate(program, target);
141
+ if (coordinate.type !== "polar") {
142
+ throw new Error(`Coordinate "${coordinate.id}" is not Polar.`);
143
+ }
144
+ return resolvePolarFrame(
145
+ resolveCoordinateBounds(program, coordinate.id, options),
146
+ coordinate.polarFrame
147
+ );
148
+ }
149
+
150
+ function consumerCoordinateIds(consumers) {
151
+ return [...new Set(consumers.map(consumer => consumer.layer.coordinate))];
152
+ }
153
+
154
+ function sameBounds(left, right) {
155
+ return ["x", "y", "width", "height"].every(
156
+ property => Math.abs(left[property] - right[property]) <= 1e-9
157
+ );
158
+ }
159
+
160
+ export function resolveScaleConsumerBounds(program, consumers, options) {
161
+ const coordinateIds = consumerCoordinateIds(consumers);
162
+ if (coordinateIds.length === 0) return resolveGraphicBounds(program);
163
+ const allocated = resolveGraphicBounds(program);
164
+ const bounds = coordinateIds.map(id =>
165
+ id === undefined || findCoordinate(program, id) === undefined
166
+ ? allocated
167
+ : resolveCoordinateBounds(program, id, options)
168
+ );
169
+ if (bounds.slice(1).some(candidate => !sameBounds(candidate, bounds[0]))) {
170
+ throw new Error(
171
+ "A position scale cannot be shared across coordinates with different effective bounds."
172
+ );
173
+ }
174
+ return bounds[0];
175
+ }
176
+
177
+ export function resolveGuideCoordinateBounds(program, {
178
+ coordinate,
179
+ scale,
180
+ channel,
181
+ resolvedScales = program.resolvedScales
182
+ }) {
183
+ let target = coordinate;
184
+ if (target === undefined && scale !== undefined) {
185
+ const candidates = new Set();
186
+ for (const layer of program.semanticSpec.layers) {
187
+ if (isSourceOwnedText(layer)) continue;
188
+ if (layer.encoding?.[channel]?.scale === scale) {
189
+ candidates.add(layer.coordinate);
190
+ }
191
+ }
192
+ if (candidates.has(undefined)) {
193
+ throw new Error("Guide scale consumers require stored coordinates.");
194
+ }
195
+ if (candidates.size > 1) {
196
+ throw new Error("Guide bounds found no unique compatible coordinate.");
197
+ }
198
+ target = candidates.values().next().value;
199
+ }
200
+ if (target !== undefined && findCoordinate(program, target) === undefined) {
201
+ throw new Error(`Unknown coordinate "${target}".`);
202
+ }
203
+ return resolveCoordinateBounds(program, target, { resolvedScales });
204
+ }
@@ -19,6 +19,7 @@ import {
19
19
  planScaleGuideRematerialization
20
20
  } from "./scaleGuideDependencies.js";
21
21
  import { planLayoutRematerialization } from "./layout.js";
22
+ import { requireCoordinate } from "../selectors/coordinates.js";
22
23
 
23
24
  export function planCanvasRematerialization(program) {
24
25
  const marks = [];
@@ -61,6 +62,41 @@ export function planCanvasRematerialization(program) {
61
62
  return buildMaterializationPlan({ scales, marks, guides, layout });
62
63
  }
63
64
 
65
+ export function planCoordinateRematerialization(program, target) {
66
+ requireCoordinate(program, target);
67
+ const layers = program.semanticSpec.layers.filter(
68
+ layer => layer.coordinate === target
69
+ );
70
+ const scaleIds = [...new Set(layers.flatMap(layer => [
71
+ ...["x", "y", "x2", "y2", "theta", "radius"].map(
72
+ channel => layer.encoding?.[channel]?.scale
73
+ ),
74
+ ...(layer.encoding?.parallel?.dimensions ?? []).map(
75
+ dimension => dimension.scale
76
+ )
77
+ ]).filter(id => id !== undefined))];
78
+ const directMarks = layers
79
+ .map(layer => getMarkMaterializationStep(program, layer))
80
+ .filter(step => step !== undefined);
81
+ const marks = [
82
+ ...directMarks,
83
+ ...directMarks.flatMap(step =>
84
+ getSourceDependentMarkSteps(program, step.args.id)
85
+ )
86
+ ];
87
+ return buildMaterializationPlan({
88
+ scales: scaleIds.map(id => ({
89
+ op: "rematerializeScale",
90
+ args: { id, guides: false, marks: false }
91
+ })),
92
+ marks: reusePlannedMarkScales(program, marks, scaleIds),
93
+ guides: scaleIds.flatMap(id =>
94
+ planScaleGuideRematerialization(program, id)
95
+ ),
96
+ layout: planLayoutRematerialization(program)
97
+ });
98
+ }
99
+
64
100
  export function planLayerDataRematerialization(program, id) {
65
101
  const layer = requireLayer(program, id);
66
102
  const scaleIds = [...new Set(getLayerScaleIds(layer))];
@@ -29,6 +29,9 @@ function collection(program, id, items) {
29
29
  }
30
30
 
31
31
  export function resolveLegacyCategoricalLegend(program) {
32
+ if (program.semanticSpec.layers.some(layer =>
33
+ layer.encoding?.stroke?.scale !== undefined
34
+ )) return undefined;
32
35
  const encodings = program.semanticSpec.layers.flatMap(layer =>
33
36
  layer.encoding?.color === undefined
34
37
  ? []
@@ -46,10 +46,16 @@ function legendTranslation(prepared, plot, layout) {
46
46
  y: layout.legend.y - prepared.bounds.top
47
47
  };
48
48
  }
49
- const strips = prepared.source.graphicSpec.objects.colorGradientStrips;
49
+ const gradientKind = prepared.kinds?.find(kind =>
50
+ ["gradient", "strokeGradient"].includes(kind)
51
+ );
52
+ const prefix = gradientKind === "strokeGradient"
53
+ ? "strokeGradient"
54
+ : "colorGradient";
55
+ const strips = prepared.source.graphicSpec.objects[`${prefix}Strips`];
50
56
  if (strips?.items?.[0] !== undefined) {
51
57
  const properties = strips.items[0].properties;
52
- const length = prepared.source.guideConfigs.legend.gradient.gradient.length;
58
+ const length = prepared.source.guideConfigs.legend[gradientKind].gradient.length;
53
59
  return {
54
60
  x: layout.legend.x - (position === "left"
55
61
  ? prepared.bounds.left
@@ -35,7 +35,7 @@ function legendCandidates(program) {
35
35
  const layer = color[0];
36
36
  return {
37
37
  target: layer.id,
38
- channels: ["color", "shape", "strokeDash"].filter(
38
+ channels: ["color", "stroke", "shape", "strokeDash"].filter(
39
39
  channel => layer.encoding?.[channel]?.scale !== undefined
40
40
  )
41
41
  };
@@ -45,7 +45,7 @@ function legendCandidates(program) {
45
45
  "Shared facet legend requires one unambiguous color legend target."
46
46
  );
47
47
  }
48
- for (const channel of ["shape", "strokeDash", "size", "opacity"]) {
48
+ for (const channel of ["stroke", "shape", "strokeDash", "size", "opacity"]) {
49
49
  const candidates = layers.filter(
50
50
  layer => layer.encoding?.[channel]?.scale !== undefined
51
51
  );
@@ -105,9 +105,14 @@ function prepareAutoLegendSource(child) {
105
105
  }
106
106
  const scale = findSemanticScale(
107
107
  source,
108
- findLayer(source, request.target)?.encoding?.color?.scale
108
+ findLayer(source, request.target)?.encoding?.[
109
+ request.channels.includes("color") ? "color" : "stroke"
110
+ ]?.scale
109
111
  );
110
112
  const gradient = scale?.type === "sequential";
113
+ const gradientPrefix = request.channels.includes("color")
114
+ ? "colorGradient"
115
+ : "strokeGradient";
111
116
  source = source.createLegend({
112
117
  target: request.target,
113
118
  channels: request.channels,
@@ -122,7 +127,7 @@ function prepareAutoLegendSource(child) {
122
127
  const resolved = source.resolvedScales[scale.id];
123
128
  source = source
124
129
  .editGraphics({
125
- target: "colorGradientLabels",
130
+ target: `${gradientPrefix}Labels`,
126
131
  property: "text",
127
132
  value: formatDistinctNumericSamples(
128
133
  continuousValues(resolved.domain, 5),
@@ -130,9 +135,9 @@ function prepareAutoLegendSource(child) {
130
135
  )
131
136
  })
132
137
  .editGraphics({
133
- target: "colorGradientTitle",
138
+ target: `${gradientPrefix}Title`,
134
139
  property: "y",
135
- value: source.graphicSpec.objects.colorGradientTitle.properties.y + 8
140
+ value: source.graphicSpec.objects[`${gradientPrefix}Title`].properties.y + 8
136
141
  });
137
142
  }
138
143
  return source;
@@ -196,7 +201,7 @@ export function prepareSharedFacetLegend(program) {
196
201
  if (bounds === undefined) {
197
202
  throw new Error("Shared facet legend has no measurable concrete bounds.");
198
203
  }
199
- const gradient = kinds.includes("gradient");
204
+ const gradient = kinds.some(kind => ["gradient", "strokeGradient"].includes(kind));
200
205
  const placement = promotedLegendPlacement(source, kinds);
201
206
  const width = Math.ceil(bounds.right - bounds.left);
202
207
  return {
@@ -0,0 +1,346 @@
1
+ import { isPlainObject } from "../core/immutable.js";
2
+ import {
3
+ formatVisibleText,
4
+ measureTextWidth,
5
+ resolveTextBounds,
6
+ textBoundsIntersect
7
+ } from "../core/textMetrics.js";
8
+ import { resolveDisplayLabel } from "../grammar/displayLabels.js";
9
+ import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from "../theme/defaults.js";
10
+
11
+ const SIDES = Object.freeze(["top", "right", "bottom", "left"]);
12
+ const ZERO_SIDES = Object.freeze({ top: 0, right: 0, bottom: 0, left: 0 });
13
+
14
+ export function createDefaultFacetHeaders() {
15
+ return {
16
+ mode: "legacy",
17
+ common: {
18
+ fontSize: 12,
19
+ fontFamily: DEFAULT_FONT_FAMILY,
20
+ fontWeight: 600,
21
+ color: DEFAULT_COLORS.strongText,
22
+ offset: 10,
23
+ align: "center"
24
+ },
25
+ row: { side: "left" },
26
+ column: { side: "top" }
27
+ };
28
+ }
29
+
30
+ export function normalizeFacetHeadersConfig(value) {
31
+ if (!isPlainObject(value)) {
32
+ throw new TypeError("Facet headers config must be a plain object.");
33
+ }
34
+ if (value.common === undefined && value.mode === undefined) {
35
+ return {
36
+ ...createDefaultFacetHeaders(),
37
+ common: {
38
+ ...createDefaultFacetHeaders().common,
39
+ ...value
40
+ }
41
+ };
42
+ }
43
+ if (!["legacy", "roles"].includes(value.mode) ||
44
+ !isPlainObject(value.common) || !isPlainObject(value.row) ||
45
+ !isPlainObject(value.column)) {
46
+ throw new Error("Facet headers config must contain canonical mode and role objects.");
47
+ }
48
+ return {
49
+ mode: value.mode,
50
+ common: { ...value.common },
51
+ row: { ...value.row },
52
+ column: { ...value.column }
53
+ };
54
+ }
55
+
56
+ export function resolveFacetHeaderConfig(headers, role) {
57
+ const normalized = normalizeFacetHeadersConfig(headers);
58
+ return role === "all"
59
+ ? { ...normalized.common }
60
+ : { ...normalized.common, ...normalized[role] };
61
+ }
62
+
63
+ function mappedText(value, config) {
64
+ return resolveDisplayLabel(value, config.labelMap, formatVisibleText);
65
+ }
66
+
67
+ function gridCellById(spec) {
68
+ return new Map((spec.facet.grid?.cells ?? []).map(cell => [cell.id, cell]));
69
+ }
70
+
71
+ function legacyDescriptors(program, headers) {
72
+ const spec = program.compositionSpec;
73
+ const config = resolveFacetHeaderConfig(headers, "all");
74
+ const cells = gridCellById(spec);
75
+ return spec.children.map((id, index) => {
76
+ const cell = cells.get(id);
77
+ const text = cell === undefined
78
+ ? mappedText(spec.facet.values[index], config)
79
+ : `${mappedText(cell.rowValue, config)} · ${mappedText(cell.columnValue, config)}`;
80
+ return { topology: "legacy", role: "column", id, text, config };
81
+ });
82
+ }
83
+
84
+ function uniqueGridGroups(spec, role) {
85
+ const coordinate = role === "column" ? "column" : "row";
86
+ const valueKey = role === "column" ? "columnValue" : "rowValue";
87
+ const groups = new Map();
88
+ for (const cell of spec.facet.grid.cells) {
89
+ const existing = groups.get(cell[coordinate]);
90
+ if (existing === undefined) {
91
+ groups.set(cell[coordinate], {
92
+ coordinate: cell[coordinate],
93
+ value: cell[valueKey],
94
+ ids: [cell.id]
95
+ });
96
+ } else {
97
+ if (existing.value !== cell[valueKey]) {
98
+ throw new Error(`Facet ${role} header values conflict at coordinate ${cell[coordinate]}.`);
99
+ }
100
+ existing.ids.push(cell.id);
101
+ }
102
+ }
103
+ return [...groups.values()].sort((left, right) =>
104
+ left.coordinate - right.coordinate
105
+ );
106
+ }
107
+
108
+ function roleDescriptors(program, headers) {
109
+ const spec = program.compositionSpec;
110
+ const column = resolveFacetHeaderConfig(headers, "column");
111
+ if (spec.facet.grid === undefined) {
112
+ return spec.children.map((id, index) => ({
113
+ topology: "cell-column",
114
+ role: "column",
115
+ id,
116
+ ids: [id],
117
+ row: Math.floor(index / spec.columns),
118
+ text: mappedText(spec.facet.values[index], column),
119
+ config: column
120
+ }));
121
+ }
122
+ const row = resolveFacetHeaderConfig(headers, "row");
123
+ const columns = uniqueGridGroups(spec, "column").map(group => ({
124
+ topology: "grid-column",
125
+ role: "column",
126
+ ids: group.ids,
127
+ coordinate: group.coordinate,
128
+ text: mappedText(group.value, column),
129
+ config: column
130
+ }));
131
+ const rows = uniqueGridGroups(spec, "row").map(group => ({
132
+ topology: "grid-row",
133
+ role: "row",
134
+ ids: group.ids,
135
+ coordinate: group.coordinate,
136
+ text: mappedText(group.value, row),
137
+ config: row
138
+ }));
139
+ return [...columns, ...rows];
140
+ }
141
+
142
+ function laneThickness(descriptors, role) {
143
+ const visible = descriptors.filter(item => item.role === role && item.text !== "");
144
+ if (visible.length === 0) return 0;
145
+ if (role === "column") {
146
+ return Math.max(...visible.map(item => item.config.fontSize + item.config.offset));
147
+ }
148
+ return Math.max(...visible.map(item => measureTextWidth(item.text, item.config) +
149
+ item.config.offset));
150
+ }
151
+
152
+ export function prepareFacetHeaders(program, headersConfig) {
153
+ const headers = normalizeFacetHeadersConfig(headersConfig);
154
+ const descriptors = headers.mode === "legacy"
155
+ ? legacyDescriptors(program, headers)
156
+ : roleDescriptors(program, headers);
157
+ const rows = program.compositionSpec.facet.grid === undefined
158
+ ? Math.ceil(program.compositionSpec.children.length / program.compositionSpec.columns)
159
+ : Math.max(...program.compositionSpec.facet.grid.cells.map(cell => cell.row)) + 1;
160
+ const headerLayout = {
161
+ outer: { ...ZERO_SIDES },
162
+ cellRows: { top: Array(rows).fill(0), bottom: Array(rows).fill(0) }
163
+ };
164
+ if (headers.mode === "roles") {
165
+ const column = resolveFacetHeaderConfig(headers, "column");
166
+ const columnThickness = laneThickness(descriptors, "column");
167
+ if (program.compositionSpec.facet.grid === undefined) {
168
+ for (const descriptor of descriptors) {
169
+ if (descriptor.text !== "") {
170
+ headerLayout.cellRows[column.side][descriptor.row] = Math.max(
171
+ headerLayout.cellRows[column.side][descriptor.row],
172
+ descriptor.config.fontSize + descriptor.config.offset
173
+ );
174
+ }
175
+ }
176
+ } else {
177
+ headerLayout.outer[column.side] = columnThickness;
178
+ const row = resolveFacetHeaderConfig(headers, "row");
179
+ headerLayout.outer[row.side] = laneThickness(descriptors, "row");
180
+ }
181
+ }
182
+ return { headers, descriptors, headerLayout };
183
+ }
184
+
185
+ function horizontalAnchor(bounds, align) {
186
+ if (align === "start") return { x: bounds.left, textAlign: "left" };
187
+ if (align === "end") return { x: bounds.right, textAlign: "right" };
188
+ return { x: (bounds.left + bounds.right) / 2, textAlign: "center" };
189
+ }
190
+
191
+ function verticalAnchor(bounds, align) {
192
+ if (align === "start") return { y: bounds.top, textBaseline: "top" };
193
+ if (align === "end") return { y: bounds.bottom, textBaseline: "bottom" };
194
+ return { y: (bounds.top + bounds.bottom) / 2, textBaseline: "middle" };
195
+ }
196
+
197
+ function union(items) {
198
+ return {
199
+ left: Math.min(...items.map(item => item.left)),
200
+ right: Math.max(...items.map(item => item.right)),
201
+ top: Math.min(...items.map(item => item.top)),
202
+ bottom: Math.max(...items.map(item => item.bottom))
203
+ };
204
+ }
205
+
206
+ function placedPlot(cell, plot) {
207
+ return {
208
+ left: cell.x + plot.x,
209
+ right: cell.x + plot.x + plot.width,
210
+ top: cell.y + plot.y,
211
+ bottom: cell.y + plot.y + plot.height
212
+ };
213
+ }
214
+
215
+ function placedCell(cell) {
216
+ return {
217
+ left: cell.x,
218
+ right: cell.x + cell.width,
219
+ top: cell.y,
220
+ bottom: cell.y + cell.height
221
+ };
222
+ }
223
+
224
+ function roleItem(descriptor, cells, plots) {
225
+ const selectedCells = descriptor.ids.map(id => cells.get(id));
226
+ const missingCell = descriptor.ids.find((id, index) =>
227
+ selectedCells[index] === undefined
228
+ );
229
+ const missingPlot = descriptor.ids.find(id => plots.get(id) === undefined);
230
+ if (missingCell !== undefined || missingPlot !== undefined) {
231
+ throw new Error(
232
+ `Facet header requires cell and plot bounds for "${missingCell ?? missingPlot}".`
233
+ );
234
+ }
235
+ const selectedPlots = descriptor.ids.map((id, index) =>
236
+ placedPlot(selectedCells[index], plots.get(id))
237
+ );
238
+ const plot = union(selectedPlots);
239
+ const snapshots = union(selectedCells.map(placedCell));
240
+ const config = descriptor.config;
241
+ let anchor;
242
+ if (descriptor.role === "column") {
243
+ anchor = horizontalAnchor(plot, config.align);
244
+ const top = config.side === "top";
245
+ anchor.y = top
246
+ ? snapshots.top - config.offset - config.fontSize / 2
247
+ : snapshots.bottom + config.offset + config.fontSize / 2;
248
+ anchor.textBaseline = "middle";
249
+ } else {
250
+ anchor = verticalAnchor(plot, config.align);
251
+ const left = config.side === "left";
252
+ anchor.x = left
253
+ ? snapshots.left - config.offset
254
+ : snapshots.right + config.offset;
255
+ anchor.textAlign = left ? "right" : "left";
256
+ }
257
+ return { descriptor, snapshots, properties: anchor };
258
+ }
259
+
260
+ function legacyItem(descriptor, cell, plot) {
261
+ if (cell === undefined || plot === undefined) {
262
+ throw new Error(`Facet header requires cell and plot bounds for "${descriptor.id}".`);
263
+ }
264
+ return {
265
+ descriptor,
266
+ snapshots: placedCell(cell),
267
+ properties: {
268
+ ...horizontalAnchor(placedPlot(cell, plot), descriptor.config.align),
269
+ y: cell.y + descriptor.config.offset,
270
+ textBaseline: "middle"
271
+ }
272
+ };
273
+ }
274
+
275
+ function styledItem(item) {
276
+ const { config } = item.descriptor;
277
+ return {
278
+ type: "text",
279
+ properties: {
280
+ ...item.properties,
281
+ text: item.descriptor.text,
282
+ fill: config.color,
283
+ fontSize: config.fontSize,
284
+ fontFamily: config.fontFamily,
285
+ fontWeight: config.fontWeight
286
+ }
287
+ };
288
+ }
289
+
290
+ function assertRoleHeadersFit(records, items, layout) {
291
+ const bounds = items.map((item, index) => records[index].descriptor.text === ""
292
+ ? undefined
293
+ : resolveTextBounds(item.properties));
294
+ for (const [index, current] of bounds.entries()) {
295
+ if (current === undefined) continue;
296
+ if (current.left < -1e-9 || current.right > layout.width + 1e-9 ||
297
+ current.top < -1e-9 || current.bottom > layout.height + 1e-9 ||
298
+ bounds.some((other, otherIndex) => otherIndex < index &&
299
+ other !== undefined && textBoundsIntersect(other, current)) ||
300
+ layout.children.some(cell => textBoundsIntersect(current, placedCell(cell)))) {
301
+ throw new Error(
302
+ "Facet role headers require sufficient non-overlapping reserved space."
303
+ );
304
+ }
305
+ }
306
+ }
307
+
308
+ function assertLegacyHeadersFit(items, layout, plotById) {
309
+ const previous = [];
310
+ for (let index = 0; index < items.length; index += 1) {
311
+ if (items[index].properties.text === "") continue;
312
+ const bounds = resolveTextBounds(items[index].properties);
313
+ const cell = layout.children[index];
314
+ const plot = plotById.get(cell.id);
315
+ if (bounds.left < 0 || bounds.right > layout.width ||
316
+ bounds.top < 0 || bounds.bottom > layout.height ||
317
+ previous.some(item => textBoundsIntersect(item, bounds)) ||
318
+ textBoundsIntersect(bounds, placedPlot(cell, plot))) {
319
+ throw new Error(
320
+ "Facet headers require sufficient non-overlapping space above every child plot."
321
+ );
322
+ }
323
+ previous.push(bounds);
324
+ }
325
+ }
326
+
327
+ export function materializeFacetHeaders(program, layout, plots, prepared) {
328
+ const cells = new Map(layout.children.map(cell => [cell.id, cell]));
329
+ const plotById = new Map(plots.map(plot => [plot.id, plot]));
330
+ const records = prepared.descriptors.map(descriptor => {
331
+ if (descriptor.topology === "legacy") {
332
+ return legacyItem(descriptor, cells.get(descriptor.id), plotById.get(descriptor.id));
333
+ }
334
+ return roleItem(descriptor, cells, plotById);
335
+ });
336
+ const items = records.map(styledItem);
337
+ if (prepared.headers.mode === "legacy") {
338
+ assertLegacyHeadersFit(items, layout, plotById);
339
+ } else {
340
+ assertRoleHeadersFit(records, items, layout);
341
+ }
342
+ const id = `${program.compositionSpec.id}-headers`;
343
+ return program
344
+ .createGraphics({ id, type: "collection", parent: "canvas" })
345
+ .editGraphics({ target: id, property: "items", value: items });
346
+ }