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
@@ -4,10 +4,14 @@ import { readQuantitativeField } from "../../grammar/scales/index.js";
4
4
  import { BAR_GRAINS, resolveBarGrain } from "../../grammar/bars/policy.js";
5
5
  import { resolveFacetScaleDomains } from "../../grammar/facets/scales.js";
6
6
  import {
7
+ applyLayerEmptyDataView,
7
8
  applyMaterializationPlan,
8
9
  planScaleGuideRematerialization
9
10
  } from "../../materialization/dependencies.js";
10
- import { getMarkMaterializationStep } from "../../materialization/marks/index.js";
11
+ import {
12
+ getLayerScaleIds,
13
+ getMarkMaterializationStep
14
+ } from "../../materialization/marks/index.js";
11
15
  import { buildMaterializationPlan } from "../../materialization/planner.js";
12
16
  import { requireDataset } from "../../selectors/datasets.js";
13
17
  import { requireLayer } from "../../selectors/layers.js";
@@ -56,7 +60,7 @@ function applySharedHistogramBoundaries(program, layerId, boundaries) {
56
60
  });
57
61
  }
58
62
 
59
- function deriveCellProgram(
63
+ function buildFacetCellCandidate(
60
64
  base,
61
65
  definition,
62
66
  cell,
@@ -81,12 +85,6 @@ function deriveCellProgram(
81
85
  });
82
86
  source = id;
83
87
  }
84
- if (requireDataset(child, cell.data).values.length === 0) {
85
- while (child.semanticSpec.layers.length > 0) {
86
- child = child.removeMark({ target: child.semanticSpec.layers[0].id });
87
- }
88
- return child;
89
- }
90
88
  const datasets = new Map([[definition.data, cell.data]]);
91
89
  for (const replay of definition.dependencies.replay) {
92
90
  const source = datasets.get(replay.source);
@@ -108,6 +106,7 @@ function deriveCellProgram(
108
106
  });
109
107
  datasets.set(replay.id, id);
110
108
  }
109
+ const reboundData = new Map();
111
110
  for (const layer of definition.dependencies.layers) {
112
111
  const data = datasets.get(layer.data);
113
112
  if (data === undefined) {
@@ -115,6 +114,14 @@ function deriveCellProgram(
115
114
  `Facet layer "${layer.id}" has no replayed dataset in cell "${cell.id}".`
116
115
  );
117
116
  }
117
+ reboundData.set(layer.id, data);
118
+ const statistical = child.markConfigs[layer.id]?.statisticalReference;
119
+ if (statistical !== undefined) {
120
+ child = child._withMarkConfig(layer.id, {
121
+ ...child.markConfigs[layer.id],
122
+ statisticalReference: { ...statistical, dataId: data }
123
+ });
124
+ }
118
125
  child = child.rebindLayerData({ id: layer.id, data });
119
126
  child = applySharedHistogramBoundaries(
120
127
  child,
@@ -134,20 +141,35 @@ function deriveCellProgram(
134
141
  }
135
142
  child = child.rebindGradientPlotProfile({ id, profile, source });
136
143
  }
137
- const scaleIds = [...new Set(child.semanticSpec.layers.flatMap(layer =>
138
- Object.values(layer.encoding ?? {})
139
- .map(encoding => encoding?.scale)
140
- .filter(id => id !== undefined)
141
- ))];
142
- return applyMaterializationPlan(child, buildMaterializationPlan({
144
+ const primaryLayers = definition.dependencies.primaryLayers ??
145
+ definition.dependencies.layers.map(layer => layer.id);
146
+ const populated = primaryLayers.some(id => {
147
+ const data = reboundData.get(id);
148
+ return data !== undefined && requireDataset(child, data).values.length > 0;
149
+ });
150
+ if (!populated) {
151
+ for (const layer of definition.dependencies.layers) {
152
+ child = applyLayerEmptyDataView(child, layer.id);
153
+ }
154
+ return { program: child, populated: false };
155
+ }
156
+ const scaleIds = [...new Set(child.semanticSpec.layers.flatMap(getLayerScaleIds))];
157
+ const program = applyMaterializationPlan(child, buildMaterializationPlan({
143
158
  scales: scaleIds.map(id => ({
144
159
  op: "rematerializeScale",
145
160
  args: { id, guides: false, marks: false }
146
161
  }))
147
162
  }));
163
+ return { program, populated: true };
148
164
  }
149
165
 
150
- function applyResolvedDomains(program, childId, resolution, baseResolved) {
166
+ function applyResolvedDomains(
167
+ program,
168
+ childId,
169
+ resolution,
170
+ baseResolved,
171
+ { marks: materializeMarks = true } = {}
172
+ ) {
151
173
  let next = program;
152
174
  for (const [id, scaleResolution] of Object.entries(resolution.scales)) {
153
175
  const current = next.resolvedScales[id];
@@ -157,19 +179,22 @@ function applyResolvedDomains(program, childId, resolution, baseResolved) {
157
179
  const shared = scaleResolution.policy === "shared"
158
180
  ? baseResolved?.[id]
159
181
  : undefined;
182
+ const semanticDomain = findSemanticScale(next, id)?.domain;
183
+ const domain = scaleResolution.childDomains[childId] ??
184
+ scaleResolution.domain ?? semanticDomain;
160
185
  next = next._withResolvedScale(id, {
161
186
  ...current,
162
187
  ...shared,
163
- domain: scaleResolution.childDomains[childId]
188
+ domain
164
189
  });
165
190
  }
166
- const marks = next.semanticSpec.layers.flatMap(layer => {
191
+ const marks = materializeMarks ? next.semanticSpec.layers.flatMap(layer => {
167
192
  const step = getMarkMaterializationStep(next, layer);
168
193
  if (step === undefined) return [];
169
- return ["bar", "line", "area"].includes(layer.mark?.type)
194
+ return ["bar", "line", "area", "arc"].includes(layer.mark?.type)
170
195
  ? [{ ...step, args: { ...step.args, scales: false } }]
171
196
  : [step];
172
- });
197
+ }) : [];
173
198
  const guides = Object.keys(resolution.scales).flatMap(id =>
174
199
  planScaleGuideRematerialization(next, id)
175
200
  );
@@ -179,6 +204,22 @@ function applyResolvedDomains(program, childId, resolution, baseResolved) {
179
204
  );
180
205
  }
181
206
 
207
+ function materializeFacetCell(
208
+ child,
209
+ childId,
210
+ resolution,
211
+ baseResolved,
212
+ populated
213
+ ) {
214
+ return applyResolvedDomains(
215
+ child,
216
+ childId,
217
+ resolution,
218
+ baseResolved,
219
+ { marks: populated }
220
+ );
221
+ }
222
+
182
223
  export function deriveFacetChildren(
183
224
  base,
184
225
  definition,
@@ -196,16 +237,23 @@ export function deriveFacetChildren(
196
237
  const bins = xPolicy === "shared"
197
238
  ? sharedHistogramBoundaries(template)
198
239
  : new Map();
199
- const independentlyResolved = Object.fromEntries(definition.cells.map(cell => [
240
+ const candidates = Object.fromEntries(definition.cells.map(cell => [
200
241
  cell.id,
201
- deriveCellProgram(template, definition, cell, bins, scales)
242
+ buildFacetCellCandidate(template, definition, cell, bins, scales)
202
243
  ]));
244
+ const independentlyResolved = Object.fromEntries(Object.entries(candidates).map(
245
+ ([id, candidate]) => [id, candidate.program]
246
+ ));
247
+ const populatedIds = new Set(Object.entries(candidates).flatMap(
248
+ ([id, candidate]) => candidate.populated ? [id] : []
249
+ ));
203
250
  return resolveFacetChildrenScales(
204
251
  template,
205
252
  definition.cells.map(cell => cell.id),
206
253
  independentlyResolved,
207
254
  scales,
208
- closeInheritedAction
255
+ closeInheritedAction,
256
+ populatedIds
209
257
  );
210
258
  }
211
259
 
@@ -214,14 +262,18 @@ export function resolveFacetChildrenScales(
214
262
  cellIds,
215
263
  independentlyResolved,
216
264
  scales = {},
217
- closeInheritedAction = false
265
+ closeInheritedAction = false,
266
+ populatedIds = undefined
218
267
  ) {
219
268
  const populated = Object.fromEntries(cellIds.flatMap(id => {
220
269
  const child = independentlyResolved[id];
221
270
  if (child === undefined) {
222
271
  throw new Error(`Facet derivation is missing child "${id}".`);
223
272
  }
224
- return child.semanticSpec.layers.length > 0 ? [[id, child]] : [];
273
+ const populated = populatedIds === undefined
274
+ ? child.semanticSpec.layers.length > 0
275
+ : populatedIds.has(id);
276
+ return populated ? [[id, child]] : [];
225
277
  }));
226
278
  if (Object.keys(populated).length === 0) {
227
279
  throw new Error("Facet derivation requires at least one populated cell.");
@@ -235,17 +287,30 @@ export function resolveFacetChildrenScales(
235
287
  scales,
236
288
  template.resolvedScales
237
289
  );
290
+ const emptyIds = cellIds.filter(id => !Object.hasOwn(populated, id));
291
+ const scalesById = new Map(template.semanticSpec.scales.map(scale => [scale.id, scale]));
292
+ for (const childId of emptyIds) {
293
+ const unresolved = Object.entries(resolution.scales).find(([id, value]) =>
294
+ value.policy === "independent" && scalesById.get(id)?.domain === "auto"
295
+ );
296
+ if (unresolved !== undefined) {
297
+ throw new Error(
298
+ `Facet child "${childId}" cannot resolve independent scale "${unresolved[0]}" from an empty partition.`
299
+ );
300
+ }
301
+ }
238
302
  const resolvedChildren = Object.fromEntries(
239
303
  cellIds.map(id => {
240
304
  const child = independentlyResolved[id];
241
305
  if (child.semanticSpec.layers.length === 0) {
242
306
  return [id, closeInheritedAction ? child._exitAction() : child];
243
307
  }
244
- const resolved = applyResolvedDomains(
308
+ const resolved = materializeFacetCell(
245
309
  child,
246
310
  id,
247
311
  resolution,
248
- template.resolvedScales
312
+ template.resolvedScales,
313
+ Object.hasOwn(populated, id)
249
314
  );
250
315
  return [id, closeInheritedAction ? resolved._exitAction() : resolved];
251
316
  })
@@ -2,6 +2,10 @@ import { assertGuideOptions, guideConflict } from "../guides/reuse.js";
2
2
  import { action } from "../../core/action.js";
3
3
  import { validateKeys } from "../../core/validation.js";
4
4
  import { GRADIENT_PROFILE_FIELDS } from "../../grammar/gradientProfile.js";
5
+ import {
6
+ requestedStrokeDetails,
7
+ STROKE_STYLE_PROPERTIES
8
+ } from "../../grammar/strokeStyle.js";
5
9
  import { resolveGraphicBounds } from "../../layout/canvas.js";
6
10
  import { findLayer } from "../../selectors/layers.js";
7
11
  import { createDensityLegendPaint } from "./paint.js";
@@ -9,7 +13,7 @@ import { createDensityLegendPaint } from "./paint.js";
9
13
  const CENTER_OPTIONS = Object.freeze([
10
14
  "id", "owner", "data", "category", "categoryType", "coordinate",
11
15
  "categoryScale", "measureScale", "orientation", "size", "stroke",
12
- "strokeWidth"
16
+ "strokeWidth", ...STROKE_STYLE_PROPERTIES
13
17
  ]);
14
18
  const LEGEND_OPTIONS = Object.freeze(["owner", "title", "position"]);
15
19
 
@@ -23,7 +27,11 @@ export const createGradientPlotCenter = action(
23
27
  const categoryAction = args.orientation === "vertical" ? "encodeX" : "encodeY";
24
28
  const measureAction = args.orientation === "vertical" ? "encodeY" : "encodeX";
25
29
  const spanOrientation = args.orientation === "vertical" ? "horizontal" : "vertical";
26
- let next = this.createRuleMark({ id: args.id, data: args.data });
30
+ let next = this.createRuleMark({
31
+ id: args.id,
32
+ data: args.data,
33
+ ...requestedStrokeDetails(args, "createGradientPlotCenter")
34
+ });
27
35
  next = next[categoryAction]({
28
36
  target: args.id,
29
37
  field: args.category,
@@ -7,6 +7,7 @@ import { findDataset } from "../../selectors/datasets.js";
7
7
  import { findLayer } from "../../selectors/layers.js";
8
8
  import { removeOwnedMark } from "../marks/remove.js";
9
9
  import { GRADIENT_PROFILE_FIELDS } from "../../grammar/gradientProfile.js";
10
+ import { requestedStrokeDetails } from "../../grammar/strokeStyle.js";
10
11
  import { removeGradientPlotLegend } from "./components.js";
11
12
  import {
12
13
  resolveDistributionRoles,
@@ -40,6 +41,13 @@ function removeCenter(program, id) {
40
41
  return removeOwnedMark(program, id, true);
41
42
  }
42
43
 
44
+ function editCenterStrokeDetails(program, id, center) {
45
+ const details = requestedStrokeDetails(center, "Gradient plot center");
46
+ return Object.keys(details).length === 0
47
+ ? program
48
+ : program.editRuleMark({ target: id, ...details });
49
+ }
50
+
43
51
  function roleCandidate(program, owner, current, args) {
44
52
  return {
45
53
  source: Object.hasOwn(args, "data")
@@ -219,10 +227,11 @@ export const editGradientPlot = action(
219
227
  orientation: candidate.orientation,
220
228
  size: spanSize,
221
229
  stroke: center.stroke,
222
- strokeWidth: center.strokeWidth
230
+ strokeWidth: center.strokeWidth,
231
+ ...requestedStrokeDetails(center, "Gradient plot center")
223
232
  });
224
233
  } else if (retainedCenter) {
225
- next = next
234
+ next = editCenterStrokeDetails(next, current.centerId, center)
226
235
  .encodeStroke({ target: current.centerId, value: center.stroke })
227
236
  .encodeStrokeWidth({
228
237
  target: current.centerId,
@@ -291,10 +300,11 @@ export const editGradientPlot = action(
291
300
  orientation: current.orientation,
292
301
  size: 1,
293
302
  stroke: center.stroke,
294
- strokeWidth: center.strokeWidth
303
+ strokeWidth: center.strokeWidth,
304
+ ...requestedStrokeDetails(center, "Gradient plot center")
295
305
  });
296
306
  } else if (center !== false) {
297
- next = next
307
+ next = editCenterStrokeDetails(next, current.centerId, center)
298
308
  .encodeStroke({ target: current.centerId, value: center.stroke })
299
309
  .encodeStrokeWidth({ target: current.centerId, value: center.strokeWidth });
300
310
  }
@@ -4,6 +4,7 @@ import { action } from "../../core/action.js";
4
4
  import { validateUserId } from "../../core/identifiers.js";
5
5
  import { isPlainObject } from "../../core/immutable.js";
6
6
  import { GRADIENT_PROFILE_FIELDS } from "../../grammar/gradientProfile.js";
7
+ import { requestedStrokeDetails } from "../../grammar/strokeStyle.js";
7
8
  import {
8
9
  mapContinuousScaleValues,
9
10
  mapOrdinalPositionValues
@@ -248,7 +249,8 @@ export const materializeGradientPlot = action(
248
249
  orientation,
249
250
  size: Math.max(1, categoryScale.bandwidth * config.width.band - 16),
250
251
  stroke: config.center.stroke,
251
- strokeWidth: config.center.strokeWidth
252
+ strokeWidth: config.center.strokeWidth,
253
+ ...requestedStrokeDetails(config.center, "Gradient plot center")
252
254
  });
253
255
  }
254
256
  const guides = config.guides;
@@ -5,6 +5,10 @@ import {
5
5
  validateUnitInterval
6
6
  } from "../../core/validation.js";
7
7
  import { normalizePalette } from "../../grammar/palettes.js";
8
+ import {
9
+ requestedStrokeDetails,
10
+ STROKE_STYLE_PROPERTIES
11
+ } from "../../grammar/strokeStyle.js";
8
12
 
9
13
  export const GRADIENT_PLOT_OPTIONS = Object.freeze([
10
14
  "id", "target", "data", "x", "y", "coordinate", "density", "width",
@@ -90,7 +94,7 @@ export function resolveGradientCenter(
90
94
  if (value === false) return false;
91
95
  const options = plain(
92
96
  value,
93
- ["type", "stroke", "strokeWidth"],
97
+ ["type", "stroke", "strokeWidth", ...STROKE_STYLE_PROPERTIES],
94
98
  "center",
95
99
  operation
96
100
  );
@@ -108,7 +112,11 @@ export function resolveGradientCenter(
108
112
  : validateNonNegativeFinite(
109
113
  options.strokeWidth,
110
114
  `${operation} center.strokeWidth`
111
- )
115
+ ),
116
+ ...requestedStrokeDetails(
117
+ { ...(previous ?? {}), ...options },
118
+ `${operation} center`
119
+ )
112
120
  });
113
121
  }
114
122
 
@@ -51,6 +51,8 @@ export function hasInferableLegend(program, layers = program.semanticSpec.layers
51
51
  isOpacityLegendLayer(layer) ||
52
52
  (layer.mark?.type === "point" &&
53
53
  layer.encoding?.size?.scale !== undefined) ||
54
+ (layer.mark?.type === "point" &&
55
+ layer.encoding?.stroke?.scale !== undefined) ||
54
56
  (layer.mark?.type === "point" &&
55
57
  layer.encoding?.color?.scale !== undefined &&
56
58
  (layer.encoding?.shape?.scale !== undefined ||
@@ -58,11 +60,15 @@ export function hasInferableLegend(program, layers = program.semanticSpec.layers
58
60
  findSemanticScale(program, layer.encoding.color.scale)?.type
59
61
  ))) ||
60
62
  (layer.mark?.type === "line" &&
61
- ["color", "strokeDash"].some(
63
+ ["color", "stroke", "strokeDash"].some(
62
64
  channel => layer.encoding?.[channel]?.scale !== undefined
63
65
  )) ||
64
66
  (["bar", "area", "arc", "rect"].includes(layer.mark?.type) &&
65
- layer.encoding?.color?.scale !== undefined)
67
+ ["color", "stroke"].some(
68
+ channel => layer.encoding?.[channel]?.scale !== undefined
69
+ )) ||
70
+ (["rule", "tick"].includes(layer.mark?.type) &&
71
+ layer.encoding?.stroke?.scale !== undefined)
66
72
  )
67
73
  );
68
74
  }
@@ -9,7 +9,7 @@ import {
9
9
  validateNonNegativeFinite,
10
10
  validatePositiveFinite
11
11
  } from "../../../core/validation.js";
12
- import { resolveGraphicBounds } from "../../../layout/canvas.js";
12
+ import { resolveGuideCoordinateBounds } from "../../../materialization/coordinateBounds.js";
13
13
  import {
14
14
  isTransformedScaleType,
15
15
  mapContinuousScaleValues,
@@ -38,11 +38,15 @@ import {
38
38
  import { resolveConcreteGraphicBounds } from
39
39
  "../../../grammar/schemas/graphicBounds.js";
40
40
  import { wrapText } from "../../../layout/text.js";
41
+ import {
42
+ normalizeDisplayLabelMap,
43
+ resolveDisplayLabel
44
+ } from "../../../grammar/displayLabels.js";
41
45
 
42
46
  const OPTIONS = [
43
47
  "scale", "position", "count", "values", "offset", "format", "color",
44
48
  "fontSize", "fontFamily", "fontWeight", "rotation", "maxWidth", "wrap",
45
- "lineHeight", "overlap"
49
+ "lineHeight", "overlap", "labelMap"
46
50
  ];
47
51
 
48
52
  const DEFAULTS = {
@@ -136,6 +140,24 @@ function validateConfig(channel, config) {
136
140
  }
137
141
  }
138
142
 
143
+ function normalizeAxisLabelMap(program, config, args, operation) {
144
+ if (!Object.hasOwn(args, "labelMap")) return config;
145
+ const scale = program.resolvedScales[config.scale];
146
+ if (!["ordinal", "band", "point"].includes(scale?.type)) {
147
+ throw new Error(`${operation} labelMap requires a categorical scale.`);
148
+ }
149
+ const next = { ...config };
150
+ if (args.labelMap === "auto") {
151
+ delete next.labelMap;
152
+ } else {
153
+ next.labelMap = normalizeDisplayLabelMap(
154
+ args.labelMap,
155
+ `${operation} labelMap`
156
+ );
157
+ }
158
+ return next;
159
+ }
160
+
139
161
  function assertTickCompatibility(ticks, config, operation) {
140
162
  if (!ticks) return;
141
163
  if (ticks.scale !== config.scale || ticks.mode !== config.mode) throw new Error(`${operation} conflicts with axis ticks.`);
@@ -198,8 +220,15 @@ function unionLabelBounds(records) {
198
220
 
199
221
  function resolve(program, channel, config) {
200
222
  const scale = program.resolvedScales[config.scale];
201
- const bounds = resolveGraphicBounds(program);
223
+ const bounds = resolveGuideCoordinateBounds(program, {
224
+ coordinate: program.semanticSpec.guides.axis?.[channel]?.coordinate,
225
+ scale: config.scale,
226
+ channel
227
+ });
202
228
  const discrete = ["ordinal", "band", "point"].includes(scale?.type);
229
+ if (config.labelMap !== undefined && !discrete) {
230
+ throw new Error("Axis labelMap requires a categorical scale.");
231
+ }
203
232
  if ((
204
233
  !["linear", "time", "ordinal", "band", "point"].includes(scale?.type) &&
205
234
  !isTransformedScaleType(scale?.type)
@@ -217,9 +246,7 @@ function resolve(program, channel, config) {
217
246
  const positions = discrete
218
247
  ? mapOrdinalPositionValues(values, scale)
219
248
  : mapContinuousScaleValues(values, scale);
220
- const text = scale.type === "time" && config.format === "auto"
221
- ? formatTimeTicks(values, scale.domain)
222
- : values.map(value => formatAxisValue(
249
+ const fallback = value => formatAxisValue(
223
250
  value,
224
251
  scale.type,
225
252
  config.format,
@@ -228,7 +255,12 @@ function resolve(program, channel, config) {
228
255
  : isTransformedScaleType(scale.type)
229
256
  ? formatTransformedTick(scale.type, item)
230
257
  : String(item)
231
- ));
258
+ );
259
+ const text = discrete
260
+ ? values.map(value => resolveDisplayLabel(value, config.labelMap, fallback))
261
+ : scale.type === "time" && config.format === "auto"
262
+ ? formatTimeTicks(values, scale.domain)
263
+ : values.map(fallback);
232
264
  const geometry = {
233
265
  values,
234
266
  ...resolveAxisLabelGeometry({
@@ -294,13 +326,14 @@ function makeEdit(channel) {
294
326
  const mode = Object.hasOwn(args, "values") || inferredValues !== undefined
295
327
  ? "values"
296
328
  : Object.hasOwn(args, "count") ? "count" : previous.mode;
297
- const config = normalizeLabelLayout({
329
+ let config = normalizeLabelLayout({
298
330
  ...previous,
299
331
  ...args,
300
332
  ...(inferredValues === undefined ? {} : { values: inferredValues }),
301
333
  ...(explicitMode ? { inferredValues: false } : {}),
302
334
  mode
303
335
  }, args, op);
336
+ config = normalizeAxisLabelMap(this, config, args, op);
304
337
  if (mode === "values") delete config.count; else delete config.values;
305
338
  validateConfig(channel, config);
306
339
  assertTickCompatibility(this.guideConfigs.axis?.[channel]?.ticks, config, op);
@@ -349,8 +382,15 @@ function makeCreate(channel) {
349
382
  const ticks = this.guideConfigs.axis?.[channel]?.ticks;
350
383
  const hasValues = Object.hasOwn(args, "values");
351
384
  const hasCount = Object.hasOwn(args, "count");
352
- const mode = hasValues ? "values" : hasCount ? "count" : ticks?.mode ?? "count";
353
- const config = normalizeLabelLayout({
385
+ const resolvedScale = this.resolvedScales[scale];
386
+ const inferredDiscreteValues = !hasValues && !hasCount && ticks === undefined &&
387
+ ["ordinal", "band", "point"].includes(resolvedScale?.type)
388
+ ? resolvedScale.domain
389
+ : undefined;
390
+ const mode = hasValues || inferredDiscreteValues !== undefined
391
+ ? "values"
392
+ : hasCount ? "count" : ticks?.mode ?? "count";
393
+ let config = normalizeLabelLayout({
354
394
  scale,
355
395
  position: defaultAxisPosition(channel),
356
396
  offset: channel === "x" ? 18 : 12,
@@ -362,10 +402,16 @@ function makeCreate(channel) {
362
402
  rotation: DEFAULTS.rotation,
363
403
  overlap: DEFAULTS.overlap,
364
404
  ...args,
365
- inferredValues: !hasValues && !hasCount && ticks?.inferredValues === true,
405
+ inferredValues: !hasValues && !hasCount &&
406
+ (ticks?.inferredValues === true || inferredDiscreteValues !== undefined),
366
407
  mode
367
408
  }, args, op);
368
- if (mode === "values") config.values ??= ticks?.values; else config.count ??= ticks?.count ?? DEFAULTS.count;
409
+ config = normalizeAxisLabelMap(this, config, args, op);
410
+ if (mode === "values") {
411
+ config.values ??= ticks?.values ?? inferredDiscreteValues;
412
+ } else {
413
+ config.count ??= ticks?.count ?? DEFAULTS.count;
414
+ }
369
415
  validateConfig(channel, config);
370
416
  assertTickCompatibility(ticks, config, op);
371
417
  resolve(this, channel, config);
@@ -7,7 +7,7 @@ import {
7
7
  validateNonEmptyString,
8
8
  validateNonNegativeFinite
9
9
  } from "../../../core/validation.js";
10
- import { resolveGraphicBounds } from "../../../layout/canvas.js";
10
+ import { resolveGuideCoordinateBounds } from "../../../materialization/coordinateBounds.js";
11
11
  import { DEFAULT_COLORS } from "../../../theme/defaults.js";
12
12
  import {
13
13
  defaultAxisPosition,
@@ -53,7 +53,11 @@ function resolveGeometry(program, channel, scaleId, position) {
53
53
 
54
54
  const resolvedScale = program.resolvedScales[scaleId];
55
55
  const range = resolvedScale?.range;
56
- const bounds = resolveGraphicBounds(program);
56
+ const bounds = resolveGuideCoordinateBounds(program, {
57
+ coordinate: program.semanticSpec.guides.axis?.[channel]?.coordinate,
58
+ scale: scaleId,
59
+ channel
60
+ });
57
61
 
58
62
  if (!Array.isArray(range) || range.length !== 2 || !range.every(Number.isFinite)) {
59
63
  throw new Error(`Axis line requires a resolved numeric scale "${scaleId}".`);
@@ -6,7 +6,7 @@ import {
6
6
  transformedTicks
7
7
  } from "../../../../grammar/scales/index.js";
8
8
  import { niceTicks } from "../../../../grammar/ticks.js";
9
- import { resolveGraphicBounds } from "../../../../layout/canvas.js";
9
+ import { resolveCoordinateBounds } from "../../../../materialization/coordinateBounds.js";
10
10
  import { findCoordinate } from "../../../../selectors/coordinates.js";
11
11
  import { findLayer } from "../../../../selectors/layers.js";
12
12
  import { formatAxisValue } from "../policy.js";
@@ -85,7 +85,11 @@ function componentValues(program, dimension, config) {
85
85
  }
86
86
 
87
87
  export function resolveStyledParallelAxes(program, dimensions) {
88
- const bounds = resolveGraphicBounds(program);
88
+ const stored = program.guideConfigs.axis?.parallel?.axes;
89
+ const layer = stored === undefined
90
+ ? undefined
91
+ : findLayer(program, stored.target);
92
+ const bounds = resolveCoordinateBounds(program, layer?.coordinate);
89
93
  const step = bounds.width / (dimensions.length - 1);
90
94
  const configs = resolveParallelAxisConfigs(program, dimensions);
91
95
  const titles = program.semanticSpec.guides.axis?.parallel?.titles ?? [];
@@ -30,7 +30,8 @@ const LABEL_OPTIONS = Object.freeze([
30
30
  "maxWidth",
31
31
  "wrap",
32
32
  "lineHeight",
33
- "overlap"
33
+ "overlap",
34
+ "labelMap"
34
35
  ]);
35
36
  const SHARED_CREATE = Object.freeze(["scale", "position", "count", "values"]);
36
37
  const SHARED_EDIT = Object.freeze(["position", "count", "values"]);
@@ -7,7 +7,7 @@ import {
7
7
  validateNonNegativeFinite,
8
8
  validateOptionObject
9
9
  } from "../../../core/validation.js";
10
- import { resolveGraphicBounds } from "../../../layout/canvas.js";
10
+ import { resolveGuideCoordinateBounds } from "../../../materialization/coordinateBounds.js";
11
11
  import {
12
12
  isTransformedScaleType,
13
13
  mapContinuousScaleValues,
@@ -69,7 +69,11 @@ function validateConfig(channel, config) {
69
69
 
70
70
  function geometry(program, channel, config) {
71
71
  const scale = program.resolvedScales[config.scale];
72
- const bounds = resolveGraphicBounds(program);
72
+ const bounds = resolveGuideCoordinateBounds(program, {
73
+ coordinate: program.semanticSpec.guides.axis?.[channel]?.coordinate,
74
+ scale: config.scale,
75
+ channel
76
+ });
73
77
  const discrete = ["ordinal", "band", "point"].includes(scale?.type);
74
78
  if ((
75
79
  !["linear", "time", "ordinal", "band", "point"].includes(scale?.type) &&
@@ -7,7 +7,7 @@ import {
7
7
  validateNonEmptyString,
8
8
  validateNonNegativeFinite
9
9
  } from "../../../core/validation.js";
10
- import { resolveGraphicBounds } from "../../../layout/canvas.js";
10
+ import { resolveGuideCoordinateBounds } from "../../../materialization/coordinateBounds.js";
11
11
  import {
12
12
  isTransformedScaleType,
13
13
  mapContinuousScaleValues,
@@ -50,7 +50,11 @@ function titleBounds(geometry, config, text) {
50
50
  }
51
51
 
52
52
  function inferredTitleOffset(program, channel, config) {
53
- const plot = resolveGraphicBounds(program);
53
+ const plot = resolveGuideCoordinateBounds(program, {
54
+ coordinate: program.semanticSpec.guides.axis?.[channel]?.coordinate,
55
+ scale: config.scale,
56
+ channel
57
+ });
54
58
  const canvas = findCanvasGraphic(program)?.properties;
55
59
  const labels = program.graphicSpec.objects[`${channel}AxisLabels`]
56
60
  ? resolveConcreteGraphicBounds(program.graphicSpec, `${channel}AxisLabels`)
@@ -151,7 +155,11 @@ export function inferAxisTitleText(program, channel, scaleId) {
151
155
 
152
156
  function resolveGeometry(program, channel, config) {
153
157
  const scale = program.resolvedScales[config.scale];
154
- const bounds = resolveGraphicBounds(program);
158
+ const bounds = resolveGuideCoordinateBounds(program, {
159
+ coordinate: program.semanticSpec.guides.axis?.[channel]?.coordinate,
160
+ scale: config.scale,
161
+ channel
162
+ });
155
163
  const discrete = ["ordinal", "band", "point"].includes(scale?.type);
156
164
  if ((
157
165
  !["linear", "time", "ordinal", "band", "point"].includes(scale?.type) &&