ggaction 0.0.10 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +5 -3
  3. package/knowledge/action-card.schema.json +1 -0
  4. package/knowledge/action-cards.json +1 -10941
  5. package/knowledge/action-cards.schema.json +1 -0
  6. package/knowledge/intent-taxonomy.json +1 -183
  7. package/knowledge/intent-taxonomy.schema.json +1 -0
  8. package/knowledge/mcp-resources.json +1 -95
  9. package/knowledge/mcp-resources.schema.json +1 -0
  10. package/knowledge/task-packet.schema.json +1 -159
  11. package/knowledge/task-resolver.js +1 -1230
  12. package/package.json +18 -2
  13. package/src/actions/boxPlots/edit.js +45 -298
  14. package/src/actions/boxPlots/options.js +46 -77
  15. package/src/actions/boxPlots/resolve.js +1 -1
  16. package/src/actions/charts/bar.js +3 -0
  17. package/src/actions/charts/heatmap.js +73 -83
  18. package/src/actions/charts/index.js +1 -1
  19. package/src/actions/charts/parallel.js +35 -1
  20. package/src/actions/charts/shared.js +20 -0
  21. package/src/actions/composition/actions.js +46 -49
  22. package/src/actions/data/bin2d.js +105 -2
  23. package/src/actions/data/box.js +17 -17
  24. package/src/actions/data/density.js +42 -78
  25. package/src/actions/data/gradientProfile.js +22 -52
  26. package/src/actions/data/horizon.js +16 -41
  27. package/src/actions/data/index.js +10 -10
  28. package/src/actions/data/interval.js +16 -38
  29. package/src/actions/data/intervalEdit.js +55 -0
  30. package/src/actions/data/regression.js +7 -17
  31. package/src/actions/data/shared.js +47 -0
  32. package/src/actions/data/timeUnit.js +14 -38
  33. package/src/actions/data/window.js +14 -38
  34. package/src/actions/distributions/revision.js +154 -34
  35. package/src/actions/encodings/angle.js +9 -10
  36. package/src/actions/encodings/appearance.js +17 -51
  37. package/src/actions/encodings/density/resolve.js +3 -1
  38. package/src/actions/encodings/density.js +1 -15
  39. package/src/actions/encodings/horizon/resolve.js +13 -3
  40. package/src/actions/encodings/horizon.js +8 -7
  41. package/src/actions/encodings/index.js +9 -9
  42. package/src/actions/encodings/offset.js +41 -45
  43. package/src/actions/encodings/parallel.js +9 -10
  44. package/src/actions/encodings/pathOrder.js +6 -13
  45. package/src/actions/encodings/position/apply.js +30 -0
  46. package/src/actions/encodings/position/policies/arc.js +23 -2
  47. package/src/actions/encodings/position/policies/index.js +4 -4
  48. package/src/actions/encodings/position/policies/line.js +47 -17
  49. package/src/actions/encodings/position/resolve.js +36 -7
  50. package/src/actions/encodings/ranged.js +18 -23
  51. package/src/actions/encodings/remove.js +15 -25
  52. package/src/actions/encodings/ruleAppearance.js +9 -24
  53. package/src/actions/encodings/shared.js +37 -0
  54. package/src/actions/encodings/strokeDash.js +8 -23
  55. package/src/actions/encodings/text.js +9 -10
  56. package/src/actions/errorBands/create.js +105 -39
  57. package/src/actions/errorBands/edit.js +89 -93
  58. package/src/actions/errorBands/index.js +1 -2
  59. package/src/actions/errorBars/create.js +212 -30
  60. package/src/actions/errorBars/edit.js +91 -48
  61. package/src/actions/errorBars/index.js +1 -2
  62. package/src/actions/facets/actions.js +45 -69
  63. package/src/actions/facets/derive.js +38 -53
  64. package/src/actions/facets/replay.js +0 -1
  65. package/src/actions/gradientPlots/components.js +59 -48
  66. package/src/actions/gradientPlots/edit.js +35 -216
  67. package/src/actions/gradientPlots/index.js +11 -10
  68. package/src/actions/gradientPlots/materialize.js +21 -35
  69. package/src/actions/gradientPlots/options.js +5 -7
  70. package/src/actions/gradientPlots/paint.js +9 -6
  71. package/src/actions/gradientPlots/resolve.js +1 -1
  72. package/src/actions/guides/axes/index.js +3 -3
  73. package/src/actions/guides/axes/labels.js +68 -26
  74. package/src/actions/guides/axes/lines.js +22 -2
  75. package/src/actions/guides/axes/policy.js +28 -8
  76. package/src/actions/guides/axes/ticks.js +27 -9
  77. package/src/actions/guides/axes/titles.js +113 -65
  78. package/src/actions/guides/grids/resolve.js +14 -0
  79. package/src/actions/guides/guides.js +2 -4
  80. package/src/actions/guides/index.js +2 -2
  81. package/src/actions/guides/legends/categorical/actions.js +44 -68
  82. package/src/actions/guides/legends/categorical/components.js +6 -1
  83. package/src/actions/guides/legends/categorical/index.js +2 -2
  84. package/src/actions/guides/legends/categorical/layout.js +148 -175
  85. package/src/actions/guides/legends/categorical/recipes.js +5 -0
  86. package/src/actions/guides/legends/categorical/resolve.js +8 -1
  87. package/src/actions/guides/legends/continuous/common.js +117 -66
  88. package/src/actions/guides/legends/continuous/gradient.js +38 -37
  89. package/src/actions/guides/legends/continuous/interval.js +97 -76
  90. package/src/actions/guides/legends/continuous/opacity.js +28 -38
  91. package/src/actions/guides/legends/edit.js +14 -1
  92. package/src/actions/guides/legends/index.js +1 -1
  93. package/src/actions/guides/legends/lane.js +2 -1
  94. package/src/actions/guides/legends/remove.js +8 -0
  95. package/src/actions/guides/legends/size.js +36 -43
  96. package/src/actions/guides/legends/strokeWidth.js +20 -26
  97. package/src/actions/guides/polar/axes/labels.js +17 -1
  98. package/src/actions/guides/polar/axes/shared.js +7 -1
  99. package/src/actions/guides/polar/axes/titles.js +19 -2
  100. package/src/actions/guides/polar/grids.js +4 -0
  101. package/src/actions/guides/polar/resolve.js +6 -0
  102. package/src/actions/index.js +13 -13
  103. package/src/actions/intervals/resolve.js +87 -117
  104. package/src/actions/marks/arc/actions.js +31 -36
  105. package/src/actions/marks/area/actions.js +23 -31
  106. package/src/actions/marks/index.js +6 -6
  107. package/src/actions/marks/line/actions.js +33 -52
  108. package/src/actions/marks/line/materialize.js +7 -5
  109. package/src/actions/marks/point/materialize.js +38 -16
  110. package/src/actions/marks/remove.js +21 -0
  111. package/src/actions/marks/rule/actions.js +79 -54
  112. package/src/actions/marks/shared.js +8 -4
  113. package/src/actions/marks/text/actions.js +14 -6
  114. package/src/actions/marks/text/layout.js +21 -45
  115. package/src/actions/marks/tick/actions.js +22 -21
  116. package/src/actions/primitives/createGraphics.js +4 -0
  117. package/src/actions/primitives/editGraphics.js +4 -0
  118. package/src/actions/primitives/index.js +3 -3
  119. package/src/actions/primitives/semantic.js +1 -1
  120. package/src/actions/primitives/semanticValidation/index.js +2 -2
  121. package/src/actions/primitives/semanticValidation/layer.js +1 -1
  122. package/src/actions/regression/edit.js +3 -43
  123. package/src/actions/scales/consumers/families.js +24 -4
  124. package/src/actions/scales/consumers/index.js +1 -1
  125. package/src/actions/scales/consumers/seriesLayout.js +17 -0
  126. package/src/actions/scales/definitions.js +100 -103
  127. package/src/actions/scales/materialize.js +5 -0
  128. package/src/actions/selection/actions.js +79 -121
  129. package/src/actions/titles/actions.js +48 -94
  130. package/src/actions/titles/resolve.js +14 -27
  131. package/src/actions/violinPlots/create.js +37 -44
  132. package/src/core/textMetrics.js +74 -37
  133. package/src/core/validation.js +18 -0
  134. package/src/core/vocabulary.js +2 -2
  135. package/src/grammar/aggregate.js +48 -28
  136. package/src/grammar/arcs.js +170 -32
  137. package/src/grammar/areaSeries.js +14 -16
  138. package/src/grammar/bin2d.js +37 -23
  139. package/src/grammar/boxPlot.js +14 -13
  140. package/src/grammar/categoricalDensity.js +14 -14
  141. package/src/grammar/categoryOrder.js +8 -14
  142. package/src/grammar/curveCommands.js +87 -20
  143. package/src/grammar/density.js +162 -141
  144. package/src/grammar/facets/index.js +14 -0
  145. package/src/grammar/facets/scales.js +7 -4
  146. package/src/grammar/gradientProfile.js +12 -15
  147. package/src/grammar/histogram.js +145 -83
  148. package/src/grammar/horizon.js +60 -23
  149. package/src/grammar/interval.js +14 -13
  150. package/src/grammar/lineSeries.js +91 -61
  151. package/src/grammar/numeric.js +290 -0
  152. package/src/grammar/paint.js +3 -4
  153. package/src/grammar/palettes.js +21 -32
  154. package/src/grammar/pathCommands.js +6 -0
  155. package/src/grammar/polarPaths.js +5 -5
  156. package/src/grammar/positionCompatibility.js +1 -1
  157. package/src/grammar/regression/derive.js +48 -9
  158. package/src/grammar/regression/models.js +248 -69
  159. package/src/grammar/regression/parameters.js +8 -0
  160. package/src/grammar/rules.js +2 -0
  161. package/src/grammar/scales/color.js +35 -26
  162. package/src/grammar/scales/continuous.js +71 -24
  163. package/src/grammar/scales/definition.js +1 -1
  164. package/src/grammar/scales/discretized.js +42 -12
  165. package/src/grammar/scales/fields.js +21 -4
  166. package/src/grammar/scales/ordinal.js +139 -60
  167. package/src/grammar/scales/temporal.js +40 -11
  168. package/src/grammar/scales/transformed.js +199 -52
  169. package/src/grammar/schemas/graphicBounds.js +270 -79
  170. package/src/grammar/seriesLayout.js +50 -18
  171. package/src/grammar/ticks.js +123 -53
  172. package/src/grammar/timeUnit.js +18 -8
  173. package/src/grammar/transforms.js +4 -1
  174. package/src/grammar/window.js +70 -57
  175. package/src/index.js +1 -1
  176. package/src/layout/canvas.js +4 -8
  177. package/src/layout/composition.js +39 -40
  178. package/src/layout/facets.js +55 -30
  179. package/src/layout/labels.js +91 -15
  180. package/src/layout/legend.js +22 -20
  181. package/src/layout/legendLane.js +54 -40
  182. package/src/layout/title.js +7 -7
  183. package/src/materialization/bars/aggregate.js +14 -40
  184. package/src/materialization/bars/grouped.js +7 -18
  185. package/src/materialization/bars/histogram.js +2 -19
  186. package/src/materialization/bars/ranged.js +20 -15
  187. package/src/materialization/bars/resolve.js +21 -0
  188. package/src/materialization/facetGuides/legacyCategorical.js +9 -2
  189. package/src/materialization/facetGuides/placement.js +49 -19
  190. package/src/materialization/facetGuides/preparation.js +35 -6
  191. package/src/materialization/facets.js +42 -28
  192. package/src/materialization/guides/resources.js +2 -1
  193. package/src/materialization/marks/capabilities.js +15 -4
  194. package/src/materialization/marks/index.js +29 -6
  195. package/src/materialization/rowEncoding.js +57 -1
  196. package/src/materialization/scales/policies/bar.js +61 -102
  197. package/src/materialization/scales/policies/binnedPosition.js +34 -0
  198. package/src/materialization/scales/policies/offset.js +54 -0
  199. package/src/materialization/scales/policies/series.js +13 -7
  200. package/src/materialization/scales/resolve.js +6 -9
  201. package/src/materialization/selection/items/arc.js +16 -2
  202. package/src/materialization/selection/items/bar.js +4 -2
  203. package/src/materialization/selection/items/common.js +21 -1
  204. package/src/materialization/selection/items/path.js +19 -2
  205. package/src/materialization/selection/items/point.js +4 -16
  206. package/src/materialization/selection/items/tick.js +13 -21
  207. package/src/materialization/selection/point.js +11 -4
  208. package/src/materialization/selection/styles.js +19 -27
  209. package/src/materialization/text.js +4 -1
  210. package/src/mcp/adapter.js +8 -3
  211. package/src/renderers/canvas/fill.js +2 -2
  212. package/src/renderers/canvas/index.js +35 -2
  213. package/src/renderers/canvas/native.js +160 -0
  214. package/src/renderers/pdf.js +14 -0
  215. package/src/renderers/svg.js +37 -4
  216. package/types/index.d.ts +3 -0
  217. package/types/program.d.ts +523 -131
  218. package/types/svg.d.ts +1 -0
  219. package/src/actions/errorBands/components.js +0 -81
  220. package/src/actions/errorBands/options.js +0 -39
  221. package/src/actions/errorBands/resolve.js +0 -28
  222. package/src/actions/errorBars/components.js +0 -62
  223. package/src/actions/errorBars/options.js +0 -58
  224. package/src/actions/errorBars/resolve.js +0 -25
@@ -1,26 +1,77 @@
1
1
  import { action } from "../../core/action.js";
2
2
  import { validateUserId } from "../../core/identifiers.js";
3
- import { validateKeys } from "../../core/validation.js";
4
- import { findLayer, resolveEligibleLayer } from "../../selectors/layers.js";
5
- import { planIntervalEdit } from "../data/intervalEdit.js";
6
3
  import {
7
- ERROR_BAR_APPEARANCE_OPTIONS,
8
- ERROR_BAR_EDIT_OPTIONS,
9
- resolveErrorBarAppearance
10
- } from "./options.js";
4
+ validateKeys,
5
+ validatePositiveFinite
6
+ } from "../../core/validation.js";
7
+ import {
8
+ normalizeStrokeDashPattern,
9
+ validateOpacityValue
10
+ } from "../../grammar/scales/index.js";
11
+ import {
12
+ validateRuleStroke,
13
+ validateRuleStrokeWidth
14
+ } from "../../grammar/ruleAppearance.js";
15
+ import { findLayer } from "../../selectors/layers.js";
16
+ import {
17
+ applyIntervalRevision,
18
+ ownOptions,
19
+ planIntervalEdit,
20
+ releaseIntervalRevision,
21
+ resolveIntervalOwner
22
+ } from "../data/intervalEdit.js";
23
+
24
+ export const ERROR_BAR_EDIT_OPTIONS = Object.freeze([
25
+ "target", "caps", "capSize", "stroke", "strokeWidth",
26
+ "strokeDash", "opacity", "statistics"
27
+ ]);
28
+
29
+ export const ERROR_BAR_APPEARANCE_OPTIONS = Object.freeze(
30
+ ERROR_BAR_EDIT_OPTIONS.filter(option =>
31
+ option !== "target" && option !== "statistics"
32
+ )
33
+ );
34
+
35
+ export function resolveErrorBarAppearance(args, { defaults, operation }) {
36
+ validateKeys(args, ERROR_BAR_EDIT_OPTIONS, operation);
37
+ const caps = args.caps ?? defaults.caps;
38
+ if (typeof caps !== "boolean") {
39
+ throw new TypeError(`${operation} caps must be a boolean.`);
40
+ }
41
+ const capSize = args.capSize ?? defaults.capSize;
42
+ validatePositiveFinite(capSize, `${operation} capSize`);
43
+ const stroke = validateRuleStroke(
44
+ args.stroke ?? defaults.stroke,
45
+ `${operation} stroke`
46
+ );
47
+ const strokeWidth = validateRuleStrokeWidth(
48
+ args.strokeWidth ?? defaults.strokeWidth,
49
+ `${operation} strokeWidth`
50
+ );
51
+ const strokeDash = args.strokeDash ?? defaults.strokeDash;
52
+ normalizeStrokeDashPattern(strokeDash);
53
+ const opacity = validateOpacityValue(
54
+ args.opacity ?? defaults.opacity,
55
+ `${operation} opacity`
56
+ );
57
+ return { caps, capSize, stroke, strokeWidth, strokeDash, opacity };
58
+ }
11
59
 
12
60
  const REMATERIALIZE_OPTIONS = Object.freeze(["id"]);
13
61
 
62
+ export function errorBarCaps(config) {
63
+ return [
64
+ [config.lowerCapId, config.fields?.lower ?? config.lowerField],
65
+ [config.upperCapId, config.fields?.upper ?? config.upperField]
66
+ ];
67
+ }
68
+
14
69
  function resolveOwner(program, requested) {
15
- const target = requested === undefined
16
- ? undefined
17
- : validateUserId(requested, "Error-bar id");
18
- return resolveEligibleLayer(program, {
19
- target,
70
+ return resolveIntervalOwner(program, requested, {
71
+ idLabel: "Error-bar id",
20
72
  label: "error bar",
21
- predicate: layer =>
22
- layer.mark?.type === "rule" &&
23
- program.markConfigs[layer.id]?.errorBar !== undefined
73
+ mark: "rule",
74
+ config: "errorBar"
24
75
  });
25
76
  }
26
77
 
@@ -43,6 +94,16 @@ function capArgs(config, id, intervalField) {
43
94
  coordinate: config.coordinate,
44
95
  positionScale: config.positionScale,
45
96
  intervalScale: config.intervalScale,
97
+ ...(config.offset === undefined
98
+ ? {}
99
+ : {
100
+ offsetChannel: config.offset.channel,
101
+ offsetField: config.offset.field,
102
+ offsetFieldType: config.offset.fieldType,
103
+ offsetScale: config.offset.scale,
104
+ offsetPaddingInner: config.offset.paddingInner,
105
+ offsetPaddingOuter: config.offset.paddingOuter
106
+ }),
46
107
  capSize: config.capSize,
47
108
  stroke: config.stroke,
48
109
  strokeWidth: config.strokeWidth,
@@ -82,10 +143,7 @@ export const rematerializeErrorBar = action(
82
143
  throw new Error(`Unknown error bar "${id}".`);
83
144
  }
84
145
  let next = rematerializeRuleAppearance(this, id, config);
85
- const capDefinitions = [
86
- [config.lowerCapId, config.lowerField],
87
- [config.upperCapId, config.upperField]
88
- ];
146
+ const capDefinitions = errorBarCaps(config);
89
147
  if (!config.caps) {
90
148
  for (const [capId] of capDefinitions) next = removeCap(next, capId);
91
149
  return next;
@@ -118,22 +176,20 @@ export const editErrorBar = action(
118
176
  }
119
177
  const owner = resolveOwner(this, args.target);
120
178
  const current = this.markConfigs[owner.id].errorBar;
121
- const appearanceArgs = Object.fromEntries(
122
- ERROR_BAR_APPEARANCE_OPTIONS
123
- .filter(key => Object.hasOwn(args, key))
124
- .map(key => [key, args[key]])
125
- );
126
- const appearance = resolveErrorBarAppearance(appearanceArgs, {
179
+ const appearance = resolveErrorBarAppearance(
180
+ ownOptions(args, ERROR_BAR_APPEARANCE_OPTIONS), {
127
181
  defaults: current,
128
182
  operation: "editErrorBar"
129
- });
183
+ }
184
+ );
130
185
  const interval = Object.hasOwn(args, "statistics")
131
186
  ? planIntervalEdit(this, {
132
187
  owner: owner.id,
133
188
  data: current.data,
134
189
  consumers: [
135
190
  owner.id,
136
- ...[current.lowerCapId, current.upperCapId]
191
+ ...errorBarCaps(current)
192
+ .map(([id]) => id)
137
193
  .filter(id => findLayer(this, id) !== undefined)
138
194
  ],
139
195
  statistics: args.statistics,
@@ -141,27 +197,14 @@ export const editErrorBar = action(
141
197
  })
142
198
  : { changed: false };
143
199
 
144
- const applyEdit = program => {
145
- let next = program;
146
- if (interval.changed) {
147
- next = next.createIntervalData(interval.dataArgs);
148
- for (const rebind of interval.revision.rebinds) {
149
- next = next.rebindLayerData(rebind);
150
- }
200
+ const next = applyIntervalRevision(this, interval);
201
+ return releaseIntervalRevision(next._withMarkConfig(owner.id, {
202
+ ...next.markConfigs[owner.id],
203
+ errorBar: {
204
+ ...current,
205
+ ...appearance,
206
+ ...(interval.changed ? { data: interval.revision.id } : {})
151
207
  }
152
- next = next._withMarkConfig(owner.id, {
153
- ...next.markConfigs[owner.id],
154
- errorBar: {
155
- ...current,
156
- ...appearance,
157
- ...(interval.changed ? { data: interval.revision.id } : {})
158
- }
159
- }).rematerializeErrorBar({ id: owner.id });
160
- return interval.changed
161
- ? next.releaseDerivedData(interval.revision.release)
162
- : next;
163
- };
164
- if (interval.changed) applyEdit(this);
165
- return applyEdit(this);
208
+ }).rematerializeErrorBar({ id: owner.id }), interval);
166
209
  }
167
210
  );
@@ -1,5 +1,4 @@
1
- import { createErrorBarCap } from "./components.js";
2
- import { createErrorBar } from "./create.js";
1
+ import { createErrorBar, createErrorBarCap } from "./create.js";
3
2
  import { editErrorBar, rematerializeErrorBar } from "./edit.js";
4
3
 
5
4
  export function registerErrorBarActions(ProgramClass) {
@@ -7,9 +7,10 @@ import {
7
7
  validatePositiveFinite
8
8
  } from "../../core/validation.js";
9
9
  import { resolveFacetDefinition } from "../../grammar/facets/index.js";
10
- import { normalizeFacetScalePolicies } from
11
- "../../grammar/facets/scales.js";
12
- import { FACET_SCALE_CHANNELS } from "../../grammar/facets/scales.js";
10
+ import {
11
+ FACET_SCALE_CHANNELS,
12
+ normalizeFacetScalePolicies
13
+ } from "../../grammar/facets/scales.js";
13
14
  import { resolveFacetLayout } from "../../layout/facets.js";
14
15
  import { compositionChildDescriptor } from
15
16
  "../../materialization/composition.js";
@@ -17,6 +18,7 @@ import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from "../../theme/defaults.js";
17
18
  import { deriveFacetChildren } from "./derive.js";
18
19
  import { replayDerivedData } from "./replay.js";
19
20
  import { composeFacetGuides } from "./guides.js";
21
+ import { applyCompositionState } from "../composition/actions.js";
20
22
 
21
23
  const FACET_OPTIONS = Object.freeze([
22
24
  "id", "field", "data", "columns", "gap", "align", "padding", "scales",
@@ -48,25 +50,6 @@ function normalizeGuides(guides) {
48
50
  return { axes, legend };
49
51
  }
50
52
 
51
- function normalizeHeaderPatch(args, previous) {
52
- validateOptionObject(args, HEADER_OPTIONS, "editFacetHeaders", {
53
- allowEmpty: false,
54
- emptyMessage: "editFacetHeaders requires at least one change."
55
- });
56
- const next = { ...previous, ...args };
57
- validatePositiveFinite(next.fontSize, "Facet header fontSize");
58
- validateNonEmptyString(next.fontFamily, "Facet header fontFamily");
59
- validateNonEmptyString(next.color, "Facet header color");
60
- validateNonNegativeFinite(next.offset, "Facet header offset");
61
- if (!(
62
- (typeof next.fontWeight === "string" && next.fontWeight.length > 0) ||
63
- Number.isFinite(next.fontWeight)
64
- )) {
65
- throw new TypeError("Facet header fontWeight must be a non-empty string or number.");
66
- }
67
- return next;
68
- }
69
-
70
53
  function requireFacetProgram(program, operation) {
71
54
  program._assertCompositionProgram(operation);
72
55
  if (program.compositionSpec.type !== "facet") {
@@ -74,16 +57,6 @@ function requireFacetProgram(program, operation) {
74
57
  }
75
58
  }
76
59
 
77
- function currentFacetDefinition(program) {
78
- const current = program.compositionSpec;
79
- return resolveFacetDefinition(program.semanticSpec, {
80
- id: current.id,
81
- data: current.facet.data,
82
- field: current.facet.field,
83
- values: current.facet.values
84
- });
85
- }
86
-
87
60
  function facetUnitTemplate(program) {
88
61
  const seed = program.children[program.compositionSpec.children[0]];
89
62
  if (seed === undefined) {
@@ -106,20 +79,19 @@ function facetUnitTemplate(program) {
106
79
  });
107
80
  }
108
81
 
109
- function applicableScaleRequest(program, channels) {
110
- return Object.fromEntries(FACET_SCALE_CHANNELS.flatMap(channel =>
82
+ function rederiveFacet(program, { scales, guides }) {
83
+ const current = program.compositionSpec;
84
+ const definition = resolveFacetDefinition(program.semanticSpec, {
85
+ id: current.id,
86
+ data: current.facet.data,
87
+ field: current.facet.field,
88
+ values: current.facet.values
89
+ });
90
+ const request = Object.fromEntries(FACET_SCALE_CHANNELS.flatMap(channel =>
111
91
  program.semanticSpec.layers.some(
112
92
  layer => layer.encoding?.[channel]?.scale !== undefined
113
- )
114
- ? [[channel, channels[channel]]]
115
- : []
93
+ ) ? [[channel, scales[channel]]] : []
116
94
  ));
117
- }
118
-
119
- function rederiveFacet(program, { scales, guides }) {
120
- const current = program.compositionSpec;
121
- const definition = currentFacetDefinition(program);
122
- const request = applicableScaleRequest(program, scales);
123
95
  const normalized = normalizeFacetScalePolicies(program.semanticSpec, request);
124
96
  const derived = deriveFacetChildren(facetUnitTemplate(program), definition, {
125
97
  closeInheritedAction: true,
@@ -134,14 +106,10 @@ function rederiveFacet(program, { scales, guides }) {
134
106
  guides
135
107
  }
136
108
  };
137
- let next = program._withCompositionState({
109
+ return applyCompositionState(program, {
138
110
  children: derived.children,
139
111
  compositionSpec
140
- });
141
- for (const id of compositionSpec.children) {
142
- next = next.useProgram({ id });
143
- }
144
- return next.materializeComposition();
112
+ }, compositionSpec.children);
145
113
  }
146
114
 
147
115
  export const facet = action(
@@ -189,18 +157,16 @@ export const facet = action(
189
157
  guides
190
158
  }
191
159
  };
192
- let next = this
193
- ._withCompositionState({
160
+ return applyCompositionState(
161
+ this._withMaterializationConfig(["facets", definition.id], {
162
+ headers: DEFAULT_HEADERS
163
+ }),
164
+ {
194
165
  children: derived.children,
195
166
  compositionSpec
196
- })
197
- ._withMaterializationConfig(["facets", definition.id], {
198
- headers: DEFAULT_HEADERS
199
- });
200
- for (const id of compositionSpec.children) {
201
- next = next.useProgram({ id });
202
- }
203
- return next.materializeComposition();
167
+ },
168
+ compositionSpec.children
169
+ );
204
170
  }
205
171
  );
206
172
 
@@ -217,7 +183,23 @@ export const editFacetHeaders = action(
217
183
  if (!isPlainObject(config?.headers)) {
218
184
  throw new Error(`Facet "${id}" requires header configuration.`);
219
185
  }
220
- const headers = normalizeHeaderPatch(args, config.headers);
186
+ validateOptionObject(args, HEADER_OPTIONS, "editFacetHeaders", {
187
+ allowEmpty: false,
188
+ emptyMessage: "editFacetHeaders requires at least one change."
189
+ });
190
+ const headers = { ...config.headers, ...args };
191
+ validatePositiveFinite(headers.fontSize, "Facet header fontSize");
192
+ validateNonEmptyString(headers.fontFamily, "Facet header fontFamily");
193
+ validateNonEmptyString(headers.color, "Facet header color");
194
+ validateNonNegativeFinite(headers.offset, "Facet header offset");
195
+ if (!(
196
+ (typeof headers.fontWeight === "string" && headers.fontWeight.length > 0) ||
197
+ Number.isFinite(headers.fontWeight)
198
+ )) {
199
+ throw new TypeError(
200
+ "Facet header fontWeight must be a non-empty string or number."
201
+ );
202
+ }
221
203
  return this
222
204
  ._withMaterializationConfig(["facets", id], { ...config, headers })
223
205
  .materializeComposition();
@@ -252,14 +234,10 @@ export const editFacetScales = action(
252
234
  )) {
253
235
  throw new Error("editFacetScales requires at least one channel policy change.");
254
236
  }
255
- const request = applicableScaleRequest(this, scales);
256
- const normalized = normalizeFacetScalePolicies(this.semanticSpec, request);
257
- const applyEdit = program => rederiveFacet(program, {
258
- scales: normalized.channels,
237
+ return rederiveFacet(this, {
238
+ scales,
259
239
  guides: current.guides
260
240
  });
261
- applyEdit(this);
262
- return applyEdit(this);
263
241
  }
264
242
  );
265
243
 
@@ -277,12 +255,10 @@ export const editFacetGuides = action(
277
255
  });
278
256
  const current = this.compositionSpec.facet;
279
257
  const guides = normalizeGuides({ ...current.guides, ...args });
280
- const applyEdit = program => rederiveFacet(program, {
258
+ return rederiveFacet(this, {
281
259
  scales: current.scales,
282
260
  guides
283
261
  });
284
- applyEdit(this);
285
- return applyEdit(this);
286
262
  }
287
263
  );
288
264
 
@@ -56,40 +56,6 @@ function applySharedHistogramBoundaries(program, layerId, boundaries) {
56
56
  });
57
57
  }
58
58
 
59
- function replayDatasetId(cell, owner) {
60
- return `${cell.id}-${owner}-data`;
61
- }
62
-
63
- function rebindReplayedGradientPlots(child, base, datasets) {
64
- let next = child;
65
- for (const [id, markConfig] of Object.entries(base.markConfigs)) {
66
- const gradientPlot = markConfig.gradientPlot;
67
- if (gradientPlot?.materialized !== true) continue;
68
- const profile = datasets.get(gradientPlot.profileId);
69
- const source = datasets.get(gradientPlot.source);
70
- if (profile === undefined || source === undefined) {
71
- throw new Error(
72
- `Facet gradient plot "${id}" is missing replayed profile dependencies.`
73
- );
74
- }
75
- next = next.rebindGradientPlotProfile({ id, profile, source });
76
- }
77
- return next;
78
- }
79
-
80
- function cellMaterializationPlan(program) {
81
- const scaleIds = [...new Set(program.semanticSpec.layers.flatMap(layer =>
82
- Object.values(layer.encoding ?? {})
83
- .map(encoding => encoding?.scale)
84
- .filter(id => id !== undefined)
85
- ))];
86
- const scales = scaleIds.map(id => ({
87
- op: "rematerializeScale",
88
- args: { id, guides: false, marks: false }
89
- }));
90
- return buildMaterializationPlan({ scales });
91
- }
92
-
93
59
  function deriveCellProgram(
94
60
  base,
95
61
  definition,
@@ -111,7 +77,7 @@ function deriveCellProgram(
111
77
  `Facet replay source "${replay.source}" is not available in cell "${cell.id}".`
112
78
  );
113
79
  }
114
- const id = replayDatasetId(cell, replay.id);
80
+ const id = `${cell.id}-${replay.id}-data`;
115
81
  const policy = findTransformPolicy(replay.transform.type);
116
82
  const transform = policy?.facetReplayTransform?.(
117
83
  replay.transform,
@@ -138,8 +104,29 @@ function deriveCellProgram(
138
104
  histogramBoundaries.get(layer.id)
139
105
  );
140
106
  }
141
- child = rebindReplayedGradientPlots(child, base, datasets);
142
- return applyMaterializationPlan(child, cellMaterializationPlan(child));
107
+ for (const [id, markConfig] of Object.entries(base.markConfigs)) {
108
+ const gradientPlot = markConfig.gradientPlot;
109
+ if (gradientPlot?.materialized !== true) continue;
110
+ const profile = datasets.get(gradientPlot.profileId);
111
+ const source = datasets.get(gradientPlot.source);
112
+ if (profile === undefined || source === undefined) {
113
+ throw new Error(
114
+ `Facet gradient plot "${id}" is missing replayed profile dependencies.`
115
+ );
116
+ }
117
+ child = child.rebindGradientPlotProfile({ id, profile, source });
118
+ }
119
+ const scaleIds = [...new Set(child.semanticSpec.layers.flatMap(layer =>
120
+ Object.values(layer.encoding ?? {})
121
+ .map(encoding => encoding?.scale)
122
+ .filter(id => id !== undefined)
123
+ ))];
124
+ return applyMaterializationPlan(child, buildMaterializationPlan({
125
+ scales: scaleIds.map(id => ({
126
+ op: "rematerializeScale",
127
+ args: { id, guides: false, marks: false }
128
+ }))
129
+ }));
143
130
  }
144
131
 
145
132
  function applyResolvedDomains(program, childId, resolution, baseResolved) {
@@ -186,14 +173,14 @@ export function deriveFacetChildren(
186
173
  const template = stripTitle && base.semanticSpec.title.text !== undefined
187
174
  ? base.removeTitle()
188
175
  : base;
189
- const resolutionRequest = scales ?? {};
190
- const xPolicy = resolutionRequest.x ?? "shared";
176
+ scales ??= {};
177
+ const xPolicy = scales.x ?? "shared";
191
178
  const bins = xPolicy === "shared"
192
179
  ? sharedHistogramBoundaries(template)
193
180
  : new Map();
194
181
  const independentlyResolved = Object.fromEntries(definition.cells.map(cell => [
195
182
  cell.id,
196
- deriveCellProgram(template, definition, cell, bins, resolutionRequest)
183
+ deriveCellProgram(template, definition, cell, bins, scales)
197
184
  ]));
198
185
  const resolution = resolveFacetScaleDomains(
199
186
  template.semanticSpec,
@@ -201,14 +188,19 @@ export function deriveFacetChildren(
201
188
  id,
202
189
  child.resolvedScales
203
190
  ])),
204
- resolutionRequest,
191
+ scales,
205
192
  template.resolvedScales
206
193
  );
207
194
  const resolvedChildren = Object.fromEntries(
208
- Object.entries(independentlyResolved).map(([id, child]) => [
209
- id,
210
- applyResolvedDomains(child, id, resolution, template.resolvedScales)
211
- ])
195
+ Object.entries(independentlyResolved).map(([id, child]) => {
196
+ const resolved = applyResolvedDomains(
197
+ child,
198
+ id,
199
+ resolution,
200
+ template.resolvedScales
201
+ );
202
+ return [id, closeInheritedAction ? resolved._exitAction() : resolved];
203
+ })
212
204
  );
213
205
  const sharedScales = Object.fromEntries(
214
206
  Object.entries(resolution.scales)
@@ -216,14 +208,7 @@ export function deriveFacetChildren(
216
208
  .map(([id]) => [id, resolvedChildren[definition.cells[0].id].resolvedScales[id]])
217
209
  );
218
210
  return freezeOwned({
219
- children: freezeOwned(Object.fromEntries(
220
- Object.entries(resolvedChildren).map(([id, child]) => [
221
- id,
222
- closeInheritedAction
223
- ? child._exitAction()
224
- : child
225
- ])
226
- )),
211
+ children: freezeOwned(resolvedChildren),
227
212
  sharedScales: freezeOwned(sharedScales),
228
213
  resolution
229
214
  });
@@ -3,7 +3,6 @@ import { isPlainObject } from "../../core/immutable.js";
3
3
  import { validateUserId } from "../../core/identifiers.js";
4
4
  import { validateKeys } from "../../core/validation.js";
5
5
  import { findTransformPolicy } from "../../grammar/transforms.js";
6
- import { hasDataset } from "../../selectors/datasets.js";
7
6
 
8
7
  const REPLAY_OPTIONS = Object.freeze(["id", "source", "transform"]);
9
8
 
@@ -65,6 +65,14 @@ function requireLegendOwner(program, owner) {
65
65
  return { layer, config };
66
66
  }
67
67
 
68
+ function editProperties(program, target, properties) {
69
+ let next = program;
70
+ for (const [property, value] of Object.entries(properties)) {
71
+ next = next.editGraphics({ target, property, value });
72
+ }
73
+ return next;
74
+ }
75
+
68
76
  export const rematerializeGradientPlotLegend = action(
69
77
  {
70
78
  op: "rematerializeGradientPlotLegend",
@@ -82,35 +90,33 @@ export const rematerializeGradientPlotLegend = action(
82
90
  const y = bounds.y + 50;
83
91
  const width = 22;
84
92
  const height = Math.min(170, Math.max(80, bounds.height - 110));
85
- return this
86
- .editGraphics({ target: ids.strip, property: "x", value: x })
87
- .editGraphics({ target: ids.strip, property: "y", value: y })
88
- .editGraphics({ target: ids.strip, property: "width", value: width })
89
- .editGraphics({ target: ids.strip, property: "height", value: height })
90
- .editGraphics({
91
- target: ids.strip,
92
- property: "fill",
93
- value: createDensityLegendPaint(config.gradient.opacity)
94
- })
95
- .editGraphics({ target: ids.strip, property: "stroke", value: "#cbd5e1" })
96
- .editGraphics({ target: ids.strip, property: "strokeWidth", value: 1 })
97
- .editGraphics({ target: ids.labels, property: "x", value: x + width + 10 })
98
- .editGraphics({ target: ids.labels, property: "y", value: [y + height, y] })
99
- .editGraphics({ target: ids.labels, property: "text", value: ["Low", "High"] })
100
- .editGraphics({ target: ids.labels, property: "fill", value: "#64748b" })
101
- .editGraphics({ target: ids.labels, property: "fontSize", value: 11 })
102
- .editGraphics({ target: ids.labels, property: "fontFamily", value: "sans-serif" })
103
- .editGraphics({ target: ids.labels, property: "textAlign", value: "left" })
104
- .editGraphics({ target: ids.labels, property: "textBaseline", value: "middle" })
105
- .editGraphics({ target: ids.title, property: "x", value: x })
106
- .editGraphics({ target: ids.title, property: "y", value: y - 18 })
107
- .editGraphics({ target: ids.title, property: "text", value: config.guides.legend.title })
108
- .editGraphics({ target: ids.title, property: "fill", value: "#334155" })
109
- .editGraphics({ target: ids.title, property: "fontSize", value: 12 })
110
- .editGraphics({ target: ids.title, property: "fontFamily", value: "sans-serif" })
111
- .editGraphics({ target: ids.title, property: "fontWeight", value: 600 })
112
- .editGraphics({ target: ids.title, property: "textAlign", value: "left" })
113
- .editGraphics({ target: ids.title, property: "textBaseline", value: "middle" });
93
+ let next = editProperties(this, ids.strip, {
94
+ x, y, width, height,
95
+ fill: createDensityLegendPaint(config.gradient.opacity),
96
+ stroke: "#cbd5e1",
97
+ strokeWidth: 1
98
+ });
99
+ next = editProperties(next, ids.labels, {
100
+ x: x + width + 10,
101
+ y: [y + height, y],
102
+ text: ["Low", "High"],
103
+ fill: "#64748b",
104
+ fontSize: 11,
105
+ fontFamily: "sans-serif",
106
+ textAlign: "left",
107
+ textBaseline: "middle"
108
+ });
109
+ return editProperties(next, ids.title, {
110
+ x,
111
+ y: y - 18,
112
+ text: config.guides.legend.title,
113
+ fill: "#334155",
114
+ fontSize: 12,
115
+ fontFamily: "sans-serif",
116
+ fontWeight: 600,
117
+ textAlign: "left",
118
+ textBaseline: "middle"
119
+ });
114
120
  }
115
121
  );
116
122
 
@@ -130,24 +136,29 @@ export const createGradientPlotLegend = action(
130
136
  }
131
137
  const ids = legendIds(args.owner);
132
138
  const densityScale = `${args.owner}Density`;
133
- let next = this
134
- .editSemantic({ property: `scale[${densityScale}].type`, value: "sequential" })
135
- .editSemantic({
136
- property: `scale[${densityScale}].domain`,
137
- value: this.markConfigs[args.owner].gradientPlot.intensityDomain
138
- })
139
- .editSemantic({
140
- property: `scale[${densityScale}].range`,
141
- value: { palette: { name: "greys" } }
142
- })
143
- .editSemantic({ property: "guide.legend.color.scale", value: densityScale })
144
- .editSemantic({
145
- property: "guide.legend.color.title",
146
- value: args.title ?? "Relative density"
147
- })
148
- .createGraphics({ id: ids.strip, type: "rect", parent: "canvas" })
149
- .createGraphics({ id: ids.labels, type: "text", length: 2, parent: "canvas" })
150
- .createGraphics({ id: ids.title, type: "text", parent: "canvas" });
139
+ const title = args.title ?? "Relative density";
140
+ let next = this;
141
+ for (const [property, value] of [
142
+ [`scale[${densityScale}].type`, "sequential"],
143
+ [`scale[${densityScale}].domain`, this.markConfigs[args.owner].gradientPlot.intensityDomain],
144
+ [`scale[${densityScale}].range`, { palette: { name: "greys" } }],
145
+ ["guide.legend.color.scale", densityScale],
146
+ ["guide.legend.color.title", title]
147
+ ]) {
148
+ next = next.editSemantic({ property, value });
149
+ }
150
+ for (const [id, type, length] of [
151
+ [ids.strip, "rect"],
152
+ [ids.labels, "text", 2],
153
+ [ids.title, "text"]
154
+ ]) {
155
+ next = next.createGraphics({
156
+ id,
157
+ type,
158
+ ...(length === undefined ? {} : { length }),
159
+ parent: "canvas"
160
+ });
161
+ }
151
162
  next = next._withMarkConfig(args.owner, {
152
163
  ...next.markConfigs[args.owner],
153
164
  gradientPlot: {
@@ -155,7 +166,7 @@ export const createGradientPlotLegend = action(
155
166
  guides: {
156
167
  ...next.markConfigs[args.owner].gradientPlot.guides,
157
168
  legend: {
158
- title: args.title ?? "Relative density",
169
+ title,
159
170
  position: args.position ?? "right"
160
171
  }
161
172
  }