ggaction 0.0.18 → 0.0.19

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 (155) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +6 -2
  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 +1 -1
  7. package/src/actions/canvas/fitting.js +6 -2
  8. package/src/actions/charts/heatmap.js +4 -3
  9. package/src/actions/data/derived.js +11 -1
  10. package/src/actions/data/edit.js +2 -0
  11. package/src/actions/data/index.js +4 -0
  12. package/src/actions/data/regression.js +24 -16
  13. package/src/actions/data/regressionStatistics.js +73 -0
  14. package/src/actions/data/shared.js +5 -2
  15. package/src/actions/distributions/revision.js +24 -18
  16. package/src/actions/encodings/remove.js +11 -6
  17. package/src/actions/encodings/shared.js +26 -24
  18. package/src/actions/facets/actions.js +18 -7
  19. package/src/actions/facets/guides.js +2 -1
  20. package/src/actions/guides/axes/axes.js +30 -11
  21. package/src/actions/guides/axes/axis.js +58 -8
  22. package/src/actions/guides/axes/components.js +13 -13
  23. package/src/actions/guides/axes/edit.js +36 -24
  24. package/src/actions/guides/axes/labels.js +37 -27
  25. package/src/actions/guides/axes/lines.js +22 -20
  26. package/src/actions/guides/axes/remove.js +10 -6
  27. package/src/actions/guides/axes/tickGroups.js +8 -7
  28. package/src/actions/guides/axes/ticks.js +31 -22
  29. package/src/actions/guides/axes/titles.js +53 -60
  30. package/src/actions/guides/facadeAxes.js +33 -24
  31. package/src/actions/guides/facadeLegend.js +3 -3
  32. package/src/actions/guides/grids/grid.js +1 -1
  33. package/src/actions/guides/grids/resolve.js +48 -51
  34. package/src/actions/guides/legends/blocks.js +43 -19
  35. package/src/actions/guides/legends/categorical/actions.js +30 -7
  36. package/src/actions/guides/legends/categorical/components.js +3 -10
  37. package/src/actions/guides/legends/categorical/index.js +2 -0
  38. package/src/actions/guides/legends/categorical/layout.js +9 -13
  39. package/src/actions/guides/legends/categorical/options.js +8 -2
  40. package/src/actions/guides/legends/categorical/recipes.js +7 -3
  41. package/src/actions/guides/legends/categorical/resolve.js +9 -2
  42. package/src/actions/guides/legends/categorical/symbols.js +22 -8
  43. package/src/actions/guides/legends/colorSize.js +110 -0
  44. package/src/actions/guides/legends/combine.js +37 -0
  45. package/src/actions/guides/legends/continuous/colorSymbols.js +1 -1
  46. package/src/actions/guides/legends/continuous/common.js +1 -1
  47. package/src/actions/guides/legends/continuous/gradient.js +27 -13
  48. package/src/actions/guides/legends/continuous/opacity.js +16 -11
  49. package/src/actions/guides/legends/creation.js +2 -0
  50. package/src/actions/guides/legends/edit.js +1 -1
  51. package/src/actions/guides/legends/focused.js +6 -5
  52. package/src/actions/guides/legends/lane.js +1 -1
  53. package/src/actions/guides/legends/lifecycle.js +2 -1
  54. package/src/actions/guides/legends/remove.js +6 -3
  55. package/src/actions/guides/legends/size.js +16 -6
  56. package/src/actions/guides/legends/strokeWidth.js +2 -0
  57. package/src/actions/guides/legends/target.js +2 -79
  58. package/src/actions/guides/legends/transition.js +24 -5
  59. package/src/actions/guides/tickValues.js +8 -5
  60. package/src/actions/marks/bar/edit.js +23 -12
  61. package/src/actions/marks/bar/materialize.js +6 -1
  62. package/src/actions/marks/point/materialize.js +5 -1
  63. package/src/actions/marks/remove.js +10 -10
  64. package/src/actions/primitives/createGraphics.js +3 -2
  65. package/src/actions/primitives/editGraphics.js +15 -11
  66. package/src/actions/primitives/graphicProperties.js +1 -6
  67. package/src/actions/primitives/semanticAction.js +27 -1
  68. package/src/actions/primitives/semanticValidation/guide.js +11 -1
  69. package/src/actions/primitives/semanticValidation/layer.js +4 -0
  70. package/src/actions/regression/edit.js +15 -3
  71. package/src/actions/scales/channels.js +2 -2
  72. package/src/actions/scales/create.js +3 -3
  73. package/src/actions/scales/definitions.js +1 -1
  74. package/src/actions/scales/edit.js +1 -1
  75. package/src/actions/scales/editPolicy.js +3 -0
  76. package/src/actions/scales/patch.js +1 -1
  77. package/src/actions/scales/quantitativeColor.js +8 -3
  78. package/src/actions/theme/reconcile.js +50 -23
  79. package/src/core/compositionState.js +2 -2
  80. package/src/core/graphicProperties.js +1 -0
  81. package/src/core/programState.js +4 -0
  82. package/src/core/resourceReferences.js +2 -2
  83. package/src/core/validation.js +9 -0
  84. package/src/core/vocabulary.js +3 -0
  85. package/src/grammar/axisTextPlacement.js +1 -0
  86. package/src/grammar/colorSizeLegend.js +39 -0
  87. package/src/grammar/computed.js +23 -1
  88. package/src/grammar/datasetSchema.js +10 -2
  89. package/src/grammar/displayLabels.js +8 -0
  90. package/src/grammar/facets/guides.js +54 -31
  91. package/src/grammar/facets/legendIntent.js +65 -0
  92. package/src/grammar/guideDash.js +1 -0
  93. package/src/grammar/legendBlocks.js +1 -0
  94. package/src/grammar/legendBorder.js +9 -0
  95. package/src/grammar/legendClip.js +19 -0
  96. package/src/grammar/legendCombinations.js +45 -0
  97. package/src/grammar/legendGradient.js +9 -0
  98. package/src/grammar/legendGroups.js +1 -1
  99. package/src/grammar/legendOpacity.js +6 -0
  100. package/src/grammar/pointShapes.js +16 -2
  101. package/src/grammar/regression/derive.js +9 -1
  102. package/src/grammar/regression/models.js +60 -0
  103. package/src/grammar/regression/parameters.js +9 -1
  104. package/src/grammar/regression/statistics.js +64 -0
  105. package/src/grammar/scales/color.js +1 -1
  106. package/src/grammar/scales/colorConsumers.js +1 -1
  107. package/src/grammar/scales/definition.js +1 -1
  108. package/src/grammar/scales/types.js +1 -1
  109. package/src/grammar/schemas/concreteGraphic.js +3 -0
  110. package/src/grammar/schemas/graphic.js +3 -0
  111. package/src/grammar/schemas/graphicBounds.js +31 -7
  112. package/src/grammar/schemas/semanticPath.js +17 -2
  113. package/src/grammar/summary.js +1 -1
  114. package/src/grammar/timeUnit.js +6 -4
  115. package/src/grammar/transformTopology.js +1 -0
  116. package/src/grammar/transforms.js +11 -1
  117. package/src/inspectionDescriptors.js +1 -1
  118. package/src/layout/guideCollisions.js +9 -1
  119. package/src/layout/legendGroup.js +1 -1
  120. package/src/materialization/compositionSnapshot.js +2 -7
  121. package/src/materialization/coordinateBounds.js +17 -4
  122. package/src/materialization/facetGuides/edit.js +30 -0
  123. package/src/materialization/facetGuides/layout.js +1 -1
  124. package/src/materialization/facetGuides/owner.js +125 -0
  125. package/src/materialization/facetGuides/ownerLayout.js +71 -0
  126. package/src/materialization/facetGuides/placement.js +52 -6
  127. package/src/materialization/facetGuides/preparation.js +7 -0
  128. package/src/materialization/facets.js +1 -1
  129. package/src/materialization/graphicHierarchy.js +5 -1
  130. package/src/materialization/graphicTranslation.js +73 -0
  131. package/src/materialization/guides/layout.js +1 -1
  132. package/src/materialization/guides/resources.js +1 -1
  133. package/src/materialization/legendBorder.js +28 -0
  134. package/src/materialization/legendClip.js +76 -0
  135. package/src/materialization/legendGroups.js +1 -1
  136. package/src/materialization/legends.js +1 -1
  137. package/src/materialization/scaleGuideDependencies.js +13 -14
  138. package/src/materialization/scales/map.js +3 -0
  139. package/src/materialization/scales/resolve.js +2 -0
  140. package/src/materialization/typography.js +10 -2
  141. package/src/persistence/validation.js +174 -1
  142. package/src/persistence.js +1 -1
  143. package/src/renderers/canvas/index.js +20 -10
  144. package/src/renderers/canvas/isolated.js +166 -0
  145. package/src/renderers/canvas/limits.js +3 -0
  146. package/src/renderers/canvas/rect.js +1 -1
  147. package/src/renderers/pdf.js +2 -2
  148. package/src/renderers/png.js +2 -2
  149. package/src/renderers/svg.js +10 -3
  150. package/src/selectors/axes.js +1 -0
  151. package/src/selectors/graphicCapacity.js +38 -0
  152. package/src/version.js +1 -1
  153. package/types/basic.d.ts +2 -0
  154. package/types/index.d.ts +5 -0
  155. package/types/program.d.ts +206 -61
@@ -35,9 +35,11 @@ export function requireDerivedDataset(program, id, type) {
35
35
  return { id: validatedId, dataset, source, transform: dataset.transform[0] };
36
36
  }
37
37
 
38
- export function derivedMaterializer(op, description, type, derive, resolve) {
38
+ export function derivedMaterializer(op, description, type, derive, resolve, { before, after } = {}) {
39
39
  return action({ op, description }, function (args = {}) {
40
40
  validateKeys(args, MATERIALIZE_OPTIONS, op);
41
+ const existing = before?.(this, args);
42
+ if (existing !== undefined) return existing;
41
43
  const { id, source, transform } = requireDerivedDataset(
42
44
  this,
43
45
  args.id,
@@ -51,6 +53,7 @@ export function derivedMaterializer(op, description, type, derive, resolve) {
51
53
  const values = Array.isArray(result) ? result : result.values;
52
54
  const schema = deriveTransformSchema(source.schema, transform, values, {
53
55
  sourceId: source.id,
56
+ sourceTransform: source.transform?.[0],
54
57
  ownerId: id
55
58
  });
56
59
  validateRowsAgainstSchema(values, schema, `Derived dataset "${id}"`);
@@ -75,7 +78,7 @@ export function derivedMaterializer(op, description, type, derive, resolve) {
75
78
  report
76
79
  );
77
80
  }
78
- return next;
81
+ return after === undefined ? next : after(next, { id, source, transform, result });
79
82
  });
80
83
  }
81
84
 
@@ -1,3 +1,4 @@
1
+ import { cartesianAxisOwners, axisTargetArgs } from "../../selectors/axes.js";
1
2
  import { DEFAULT_TICK_COUNT } from "../guides/tickValues.js";
2
3
  import { findLayer } from "../../selectors/layers.js";
3
4
  import { findSemanticScale } from "../../selectors/scales.js";
@@ -296,39 +297,44 @@ export function clearCartesianPositions(program, id) {
296
297
  return next;
297
298
  }
298
299
 
299
- function rebindAxis(program, channel, {
300
+ function rebindAxis(program, channel, options) {
301
+ return cartesianAxisOwners(program).filter(owner => owner.channel === channel)
302
+ .reduce((next, owner) => rebindAxisOwner(next, channel, owner.id, options), program);
303
+ }
304
+
305
+ function rebindAxisOwner(program, channel, axisId, {
300
306
  previousScale,
301
307
  nextScale,
302
308
  previousTitle,
303
309
  nextTitle
304
310
  }) {
305
- const semantic = program.semanticSpec.guides.axis?.[channel];
311
+ const semantic = program.semanticSpec.guides.axis?.[axisId];
306
312
  if (semantic?.scale !== previousScale) return program;
307
313
  let next = program.editSemantic({
308
- property: `guide.axis.${channel}.scale`,
314
+ property: `guide.axis.${axisId}.scale`,
309
315
  value: nextScale
310
316
  });
311
317
  if (semantic.title === previousTitle) {
312
318
  next = next.editSemantic({
313
- property: `guide.axis.${channel}.title`,
319
+ property: `guide.axis.${axisId}.title`,
314
320
  value: nextTitle
315
321
  });
316
322
  }
317
323
  const scale = next.resolvedScales[nextScale];
318
324
  const ticks = normalizeTickConfig(
319
- next.guideConfigs.axis?.[channel]?.ticks,
325
+ next.guideConfigs.axis?.[axisId]?.ticks,
320
326
  scale
321
327
  );
322
328
  if (ticks !== undefined) {
323
- next = next._withGuideConfig(channel, "ticks", {
329
+ next = next._withGuideConfig(axisId, "ticks", {
324
330
  ...ticks,
325
331
  scale: nextScale
326
332
  });
327
333
  }
328
- const labels = next.guideConfigs.axis?.[channel]?.labels;
334
+ const labels = next.guideConfigs.axis?.[axisId]?.labels;
329
335
  if (labels !== undefined) {
330
- const tickConfig = next.guideConfigs.axis[channel].ticks;
331
- next = next._withGuideConfig(channel, "labels", {
336
+ const tickConfig = next.guideConfigs.axis[axisId].ticks;
337
+ next = next._withGuideConfig(axisId, "labels", {
332
338
  ...labels,
333
339
  scale: nextScale,
334
340
  mode: tickConfig.mode,
@@ -338,30 +344,30 @@ function rebindAxis(program, channel, {
338
344
  : { count: tickConfig.count })
339
345
  });
340
346
  if (tickConfig.mode === "values") {
341
- const config = next.guideConfigs.axis[channel].labels;
347
+ const config = next.guideConfigs.axis[axisId].labels;
342
348
  if (Object.hasOwn(config, "count")) {
343
349
  const { count: _count, ...withoutCount } = config;
344
- next = next._withGuideConfig(channel, "labels", withoutCount);
350
+ next = next._withGuideConfig(axisId, "labels", withoutCount);
345
351
  }
346
352
  } else {
347
- const config = next.guideConfigs.axis[channel].labels;
353
+ const config = next.guideConfigs.axis[axisId].labels;
348
354
  if (Object.hasOwn(config, "values")) {
349
355
  const { values: _values, ...withoutValues } = config;
350
- next = next._withGuideConfig(channel, "labels", withoutValues);
356
+ next = next._withGuideConfig(axisId, "labels", withoutValues);
351
357
  }
352
358
  }
353
359
  }
354
- const title = next.guideConfigs.axis?.[channel]?.title;
360
+ const title = next.guideConfigs.axis?.[axisId]?.title;
355
361
  if (title !== undefined) {
356
- next = next._withGuideConfig(channel, "title", {
362
+ next = next._withGuideConfig(axisId, "title", {
357
363
  ...title,
358
364
  scale: nextScale
359
365
  });
360
366
  }
361
367
  const methods = axisMethods(channel);
362
- if (ticks !== undefined) next = next[methods.ticks]();
363
- if (labels !== undefined) next = next[methods.labels]();
364
- if (title !== undefined) next = next[methods.title]();
368
+ if (ticks !== undefined) next = next[methods.ticks](axisId === channel ? undefined : axisTargetArgs(axisId, channel));
369
+ if (labels !== undefined) next = next[methods.labels](axisId === channel ? undefined : axisTargetArgs(axisId, channel));
370
+ if (title !== undefined) next = next[methods.title](axisId === channel ? undefined : axisTargetArgs(axisId, channel));
365
371
  return next;
366
372
  }
367
373
 
@@ -1,3 +1,4 @@
1
+ import { cartesianAxisOwners, axisTargetArgs } from "../../selectors/axes.js";
1
2
  import { closedAction } from "../../core/action.js";
2
3
  import { validateUserId } from "../../core/identifiers.js";
3
4
 
@@ -41,9 +42,9 @@ const REMOVE_AXIS = Object.freeze({
41
42
  radius: "removeRadialAxis"
42
43
  });
43
44
  const SPECIALIZED_LEGEND_KIND = Object.freeze({
44
- color: Object.freeze(["gradient", "interval"]),
45
+ color: Object.freeze(["gradient", "interval", "colorSize"]),
45
46
  stroke: Object.freeze(["strokeGradient", "strokeInterval"]),
46
- size: Object.freeze(["size"]),
47
+ size: Object.freeze(["size", "colorSize"]),
47
48
  opacity: Object.freeze(["opacity"]),
48
49
  strokeWidth: Object.freeze(["strokeWidth"])
49
50
  });
@@ -144,10 +145,14 @@ function cleanupPositionGuides(program, removed) {
144
145
  ) {
145
146
  continue;
146
147
  }
147
- const axis = next.semanticSpec.guides.axis?.[definition.guideChannel];
148
- if (axis?.scale === scale) {
149
- next = next[REMOVE_AXIS[definition.guideChannel]]({
150
- scale,
148
+ const owners = ["x", "y"].includes(definition.guideChannel)
149
+ ? cartesianAxisOwners(next).filter(owner => owner.channel === definition.guideChannel)
150
+ : [{ id: definition.guideChannel, channel: definition.guideChannel,
151
+ semantic: next.semanticSpec.guides.axis?.[definition.guideChannel] }];
152
+ for (const owner of owners) {
153
+ const axis = owner.semantic;
154
+ if (axis?.scale === scale) next = next[REMOVE_AXIS[definition.guideChannel]]({
155
+ ...axisTargetArgs(owner.id, owner.channel), scale,
151
156
  ...(axis.coordinate === undefined ? {} : { coordinate: axis.coordinate })
152
157
  });
153
158
  }
@@ -1,3 +1,4 @@
1
+ import { cartesianAxisOwners } from "../../selectors/axes.js";
1
2
  import { isSourceOwnedText } from "../../grammar/text.js";
2
3
  import { validateUserId } from "../../core/identifiers.js";
3
4
  import { findDataset } from "../../selectors/datasets.js";
@@ -80,7 +81,7 @@ export function applyEncodingScale(
80
81
  for (const property of [
81
82
  "domain", "range", "nice", "zero", "clamp", "reverse",
82
83
  "base", "exponent", "constant", "paddingInner", "paddingOuter",
83
- "padding", "align", "unknown", "interpolate", "radialMapping"
84
+ "padding", "align", "unknown", "interpolate", "whitePoint", "gamma", "radialMapping"
84
85
  ]) {
85
86
  if (Object.hasOwn(options, property)) patch[property] = definition[property];
86
87
  }
@@ -102,26 +103,26 @@ export function rebindPositionGuides(
102
103
  if (definition?.guideChannel === undefined) return program;
103
104
  if (previousScale === undefined || previousScale === nextScale) return program;
104
105
 
105
- const axis = program.semanticSpec.guides.axis?.[definition.guideChannel];
106
+ const owners = ["x", "y"].includes(definition.guideChannel)
107
+ ? cartesianAxisOwners(program).filter(owner => owner.channel === definition.guideChannel)
108
+ : [{ id: definition.guideChannel, semantic: program.semanticSpec.guides.axis?.[definition.guideChannel] }];
106
109
  const direction = definition.gridDirection;
107
110
  const grid = program.semanticSpec.guides.grid?.[direction];
108
- const ownsAxis = axis?.scale === previousScale;
109
- const refreshesReboundAxis = axis?.scale === nextScale;
110
111
  const ownsGrid = grid?.scale === previousScale;
111
- if (!ownsAxis && !ownsGrid && !refreshesReboundAxis) return program;
112
-
113
- const remaining = program.semanticSpec.layers.some(layer =>
114
- layer.id !== target && !isSourceOwnedText(layer) && layer.encoding?.[channel]?.scale === previousScale
115
- );
116
- if ((ownsAxis || ownsGrid) && remaining) {
117
- throw new Error(
118
- `Cannot rebind ${channel} guides from shared scale "${previousScale}" while it has other consumers.`
119
- );
112
+ const affected = owners.filter(owner => [previousScale, nextScale].includes(owner.semantic?.scale));
113
+ if (!affected.length && !ownsGrid) return program;
114
+ const remaining = program.semanticSpec.layers.some(layer => layer.id !== target &&
115
+ !isSourceOwnedText(layer) && layer.encoding?.[channel]?.scale === previousScale);
116
+ if ((affected.some(owner => owner.semantic?.scale === previousScale) || ownsGrid) && remaining) {
117
+ throw new Error(`Cannot rebind ${channel} guides from shared scale "${previousScale}" while it has other consumers.`);
120
118
  }
121
-
122
119
  let next = program;
120
+ for (const owner of affected) {
121
+ const axisId = owner.id;
122
+ const ownsAxis = owner.semantic?.scale === previousScale;
123
+ const refreshesReboundAxis = owner.semantic?.scale === nextScale;
123
124
  if (ownsAxis || refreshesReboundAxis) {
124
- const previousAxisConfig = next.guideConfigs.axis?.[definition.guideChannel];
125
+ const previousAxisConfig = next.guideConfigs.axis?.[axisId];
125
126
  const previousTicks = previousAxisConfig?.ticks;
126
127
  const previousLabels = previousAxisConfig?.labels;
127
128
  const labelsFollowTicks = previousTicks !== undefined &&
@@ -135,13 +136,13 @@ export function rebindPositionGuides(
135
136
  ));
136
137
  if (ownsAxis) {
137
138
  next = next.editSemantic({
138
- property: `guide.axis.${definition.guideChannel}.scale`,
139
+ property: `guide.axis.${axisId}.scale`,
139
140
  value: nextScale
140
141
  });
141
142
  for (const component of ["line", "ticks", "labels", "title"]) {
142
- const config = next.guideConfigs.axis?.[definition.guideChannel]?.[component];
143
+ const config = next.guideConfigs.axis?.[axisId]?.[component];
143
144
  if (config?.scale === previousScale) {
144
- next = next._withGuideConfig(definition.guideChannel, component, {
145
+ next = next._withGuideConfig(axisId, component, {
145
146
  ...config,
146
147
  scale: nextScale
147
148
  });
@@ -151,7 +152,7 @@ export function rebindPositionGuides(
151
152
  const resolved = next.resolvedScales[nextScale] ??
152
153
  findSemanticScale(next, nextScale);
153
154
  const discrete = ["ordinal", "band", "point"].includes(resolved?.type);
154
- let ticks = next.guideConfigs.axis?.[definition.guideChannel]?.ticks;
155
+ let ticks = next.guideConfigs.axis?.[axisId]?.ticks;
155
156
  if (ticks !== undefined && discrete && ticks.mode !== "values") {
156
157
  ticks = {
157
158
  ...ticks,
@@ -160,7 +161,7 @@ export function rebindPositionGuides(
160
161
  inferredValues: true
161
162
  };
162
163
  delete ticks.count;
163
- next = next._withGuideConfig(definition.guideChannel, "ticks", ticks);
164
+ next = next._withGuideConfig(axisId, "ticks", ticks);
164
165
  } else if (
165
166
  ticks !== undefined &&
166
167
  !discrete &&
@@ -174,18 +175,18 @@ export function rebindPositionGuides(
174
175
  inferredValues: true
175
176
  };
176
177
  delete ticks.values;
177
- next = next._withGuideConfig(definition.guideChannel, "ticks", ticks);
178
+ next = next._withGuideConfig(axisId, "ticks", ticks);
178
179
  } else if (
179
180
  ticks !== undefined &&
180
181
  discrete &&
181
182
  ticks.inferredValues === true
182
183
  ) {
183
184
  ticks = { ...ticks, values: resolved.domain };
184
- next = next._withGuideConfig(definition.guideChannel, "ticks", ticks);
185
+ next = next._withGuideConfig(axisId, "ticks", ticks);
185
186
  }
186
187
  if (previousLabels !== undefined && labelsFollowTicks && ticks !== undefined) {
187
188
  const labels = {
188
- ...next.guideConfigs.axis[definition.guideChannel].labels,
189
+ ...next.guideConfigs.axis[axisId].labels,
189
190
  mode: ticks.mode,
190
191
  inferredValues: ticks.inferredValues === true,
191
192
  ...(ticks.mode === "values"
@@ -195,12 +196,13 @@ export function rebindPositionGuides(
195
196
  if (ticks.mode === "values") delete labels.count;
196
197
  else delete labels.values;
197
198
  next = next._withGuideConfig(
198
- definition.guideChannel,
199
+ axisId,
199
200
  "labels",
200
201
  labels
201
202
  );
202
203
  }
203
204
  }
205
+ }
204
206
  if (ownsGrid) {
205
207
  next = next
206
208
  .editSemantic({
@@ -60,7 +60,7 @@ const HEADER_STYLE_OPTIONS = Object.freeze([
60
60
  "fontSize", "fontFamily", "fontWeight", "color", "offset", "align"
61
61
  ]);
62
62
 
63
- function normalizeGuides(guides) {
63
+ function normalizeGuides(guides, allowStored = false) {
64
64
  if (guides === undefined) return { axes: "each", legend: false };
65
65
  validateOptionObject(guides, GUIDE_OPTIONS, "facet.guides");
66
66
  const axes = guides.axes ?? "each";
@@ -68,8 +68,11 @@ function normalizeGuides(guides) {
68
68
  throw new Error('facet guides.axes must be "each" or "outer".');
69
69
  }
70
70
  const legend = guides.legend ?? false;
71
- if (legend !== false && legend !== "shared") {
72
- throw new Error('facet guides.legend must be false or "shared".');
71
+ if (legend !== false && legend !== "shared" && !isPlainObject(legend)) {
72
+ throw new Error('facet guides.legend must be false, "shared", or legend options.');
73
+ }
74
+ if (!allowStored && isPlainObject(legend) && Object.hasOwn(legend, "type")) {
75
+ throw new Error("Facet legend options do not accept internal type tags.");
73
76
  }
74
77
  return { axes, legend };
75
78
  }
@@ -617,7 +620,8 @@ export const editFacetGuides = /* @__PURE__ */ action(
617
620
  emptyMessage: "editFacetGuides requires at least one guide policy."
618
621
  });
619
622
  const current = this.compositionSpec.facet;
620
- const guides = normalizeGuides({ ...current.guides, ...args });
623
+ if (Object.hasOwn(args, "legend")) normalizeGuides({ legend: args.legend });
624
+ const guides = normalizeGuides({ ...current.guides, ...args }, true);
621
625
  const family = resolveFacetFamily(this.semanticSpec).family;
622
626
  if (family !== "cartesian" && guides.axes === "outer") {
623
627
  throw new Error("Polar and Parallel facets do not support outer axes.");
@@ -688,6 +692,8 @@ export const editFacetSource = /* @__PURE__ */ closedAction(
688
692
  const current = this.compositionSpec;
689
693
  const facetConfig = this.materializationConfigs.facets?.[current.id];
690
694
  const base = adoptUnitState(args.program, this);
695
+ const authored = isPlainObject(current.facet.guides.legend);
696
+ const guides = authored ? { ...current.facet.guides, legend: false } : current.facet.guides;
691
697
  let revised;
692
698
  const scales = usedFacetScalePolicies(base, current.facet.scales);
693
699
  if (current.facet.grid !== undefined) {
@@ -702,7 +708,7 @@ export const editFacetSource = /* @__PURE__ */ closedAction(
702
708
  align: current.align,
703
709
  padding: current.padding,
704
710
  scales,
705
- guides: current.facet.guides
711
+ guides
706
712
  });
707
713
  } else if (current.facet.repeat !== undefined) {
708
714
  revised = base.repeatCharts({
@@ -714,7 +720,7 @@ export const editFacetSource = /* @__PURE__ */ closedAction(
714
720
  align: current.align,
715
721
  padding: current.padding,
716
722
  scales,
717
- guides: current.facet.guides
723
+ guides
718
724
  });
719
725
  } else {
720
726
  revised = base.facet({
@@ -728,9 +734,14 @@ export const editFacetSource = /* @__PURE__ */ closedAction(
728
734
  align: current.align,
729
735
  padding: current.padding,
730
736
  scales,
731
- guides: current.facet.guides
737
+ guides
732
738
  });
733
739
  }
740
+ if (authored) {
741
+ const spec = revised.compositionSpec;
742
+ revised = revised._withCompositionState({ children: revised.children,
743
+ compositionSpec: { ...spec, facet: { ...spec.facet, guides: current.facet.guides } } });
744
+ }
734
745
  if (facetConfig !== undefined) {
735
746
  revised = revised
736
747
  ._withMaterializationConfig(["facets", current.id], facetConfig)
@@ -1,3 +1,4 @@
1
+ import { synchronizeFacetColorSizeMeaning } from "../primitives/semanticAction.js";
1
2
  import { closedAction } from "../../core/action.js";
2
3
 
3
4
  import { applyFacetGuideComposition } from
@@ -10,6 +11,6 @@ export const composeFacetGuides = /* @__PURE__ */ closedAction(
10
11
  scope: "composition"
11
12
  }, ["layout", "plot"],
12
13
  function ({ layout, plot } = {}) {
13
- return applyFacetGuideComposition(this, { layout, plot });
14
+ return applyFacetGuideComposition(this, { layout, plot, synchronizeColorSizeMeaning: synchronizeFacetColorSizeMeaning });
14
15
  }
15
16
  );
@@ -1,3 +1,4 @@
1
+ import { withGuideLayoutTransaction } from "../../../materialization/guides/layout.js";
1
2
  import { isSourceOwnedText } from "../../../grammar/text.js";
2
3
  import { action } from "../../../core/action.js";
3
4
  import { isPlainObject } from "../../../core/immutable.js";
@@ -15,6 +16,13 @@ const COORDINATE_API_TYPES = new Set([
15
16
  ]);
16
17
 
17
18
  function validateAxisOption(value, channel) {
19
+ if (["x", "y"].includes(channel) && Array.isArray(value)) {
20
+ if (value.length === 0 || value.some(entry => !isPlainObject(entry) || !Object.hasOwn(entry, "id"))) {
21
+ throw new TypeError(`createAxes ${channel} arrays require nonempty named axis descriptors.`);
22
+ }
23
+ for (const entry of value) validateUserId(entry.id, "Axis id");
24
+ return;
25
+ }
18
26
  if (value !== undefined && value !== false && !isPlainObject(value)) {
19
27
  throw new TypeError(`createAxes ${channel} must be false or a plain object.`);
20
28
  }
@@ -287,23 +295,34 @@ const createAxes = /* @__PURE__ */ action(
287
295
  coordinateDescriptor,
288
296
  cartesianLayers
289
297
  );
290
- const x = resolveAxisArgs(coordinate.layers, "x", args.x);
291
- const y = resolveAxisArgs(coordinate.layers, "y", args.y);
298
+ const resolve = (channel, value) => Array.isArray(value)
299
+ ? value.map(entry => resolveAxisArgs(coordinate.layers, channel, entry))
300
+ : resolveAxisArgs(coordinate.layers, channel, value);
301
+ const x = resolve("x", args.x);
302
+ const y = resolve("y", args.y);
292
303
 
293
304
  if (x === undefined && y === undefined) {
294
305
  throw new Error("createAxes requires at least one selected axis.");
295
306
  }
296
307
 
297
- let program = this;
298
-
299
- if (x !== undefined) {
300
- program = program.createXAxis({ ...x, coordinate: coordinate.id });
301
- }
302
- if (y !== undefined) {
303
- program = program.createYAxis({ ...y, coordinate: coordinate.id });
308
+ const apply = source => {
309
+ let program = source;
310
+ for (const [channel, option] of [["x", x], ["y", y]]) {
311
+ if (option === undefined) continue;
312
+ for (const entry of Array.isArray(option) ? option : [option]) {
313
+ program = program[`create${channel.toUpperCase()}Axis`]({ ...entry, coordinate: coordinate.id });
314
+ }
315
+ }
316
+ return program;
317
+ };
318
+ if (Array.isArray(x) || Array.isArray(y)) {
319
+ const ids = [...(Array.isArray(x) ? x : x === undefined ? [] : [x]),
320
+ ...(Array.isArray(y) ? y : y === undefined ? [] : [y])].map((entry, index) => entry.id ?? (index === 0 ? "x" : "y"));
321
+ if (new Set(ids).size !== ids.length) throw new Error("createAxes requires unique axis ids across its descriptors.");
322
+ withGuideLayoutTransaction(this, apply);
323
+ return withGuideLayoutTransaction(this, apply);
304
324
  }
305
-
306
- return program;
325
+ return apply(this);
307
326
  }
308
327
  );
309
328
 
@@ -1,21 +1,27 @@
1
+ import { AXIS_RESOURCE_CHANNELS } from "../../../core/vocabulary.js";
2
+ import { cartesianAxisOwners, axisTargetArgs, assertCartesianAxisSideAvailable } from "../../../selectors/axes.js";
3
+ import { axisGraphicIds } from "../../../materialization/guides/resources.js";
4
+ import { defaultAxisPosition, validateAxisPosition } from "./policy.js";
1
5
  import { isSourceOwnedText } from "../../../grammar/text.js";
2
6
  import { withGuideLayoutValidation } from "../../../materialization/guides/layout.js";
3
7
  import { action } from "../../../core/action.js";
4
8
  import { isPlainObject } from "../../../core/immutable.js";
5
9
  import { validateUserId } from "../../../core/identifiers.js";
6
- import { validateKeys } from "../../../core/validation.js";
10
+ import { validateKeys, validateNonNegativeFinite } from "../../../core/validation.js";
7
11
  import { hasCoordinate } from "../../../selectors/index.js";
8
12
  import { validateEnabledAxisComponents } from "./components.js";
9
13
  import { requireSemanticScale } from "../../../selectors/scales.js";
10
14
  import { requireCoordinate } from "../../../selectors/coordinates.js";
11
15
 
12
16
  const TOP_OPTIONS = Object.freeze([
17
+ "id",
13
18
  "scale",
14
19
  "coordinate",
15
20
  "position",
16
21
  "line",
17
22
  "ticksAndLabels",
18
- "title"
23
+ "title",
24
+ "offset"
19
25
  ]);
20
26
  const LINE_OPTIONS = Object.freeze(["color", "lineWidth"]);
21
27
  const TICK_GROUP_OPTIONS = Object.freeze([
@@ -32,7 +38,8 @@ const TITLE_OPTIONS = Object.freeze([
32
38
  "color",
33
39
  "fontSize",
34
40
  "fontFamily",
35
- "fontWeight"
41
+ "fontWeight",
42
+ "alongOffset", "align", "baseline"
36
43
  ]);
37
44
 
38
45
  function validateNested(value, supported, label) {
@@ -45,6 +52,7 @@ function validateNested(value, supported, label) {
45
52
 
46
53
  export function validateAxisArgs(args, operation) {
47
54
  validateKeys(args, TOP_OPTIONS, operation);
55
+ if (Object.hasOwn(args, "offset")) validateNonNegativeFinite(args.offset, "Axis frame offset");
48
56
 
49
57
  validateEnabledAxisComponents(args, operation);
50
58
  if (Object.hasOwn(args, "line") && args.line !== false) {
@@ -84,10 +92,52 @@ function makeCreateAxis(channel) {
84
92
  },
85
93
  withGuideLayoutValidation(function (args = {}) {
86
94
  validateAxisArgs(args, operation.create);
87
- const shared = {};
95
+ const axisId = validateUserId(args.id ?? channel, "Axis id");
96
+ if (AXIS_RESOURCE_CHANNELS.includes(axisId) && axisId !== channel) {
97
+ throw new Error(`Axis id "${axisId}" is reserved for another axis channel.`);
98
+ }
99
+ const named = axisId !== channel;
100
+ let source = this;
101
+ if (named) {
102
+ if (cartesianAxisOwners(this).some(owner => owner.id === axisId) ||
103
+ axisGraphicIds(channel, axisId).some(id => this.graphicSpec.objects[id] !== undefined)) {
104
+ throw new Error(`Axis "${axisId}" requires a missing resource and missing concrete components.`);
105
+ }
106
+ const layers = this.semanticSpec.layers.filter(layer => !isSourceOwnedText(layer) &&
107
+ layer.encoding?.[channel]?.scale !== undefined &&
108
+ (args.coordinate === undefined || layer.coordinate === args.coordinate) &&
109
+ (args.scale === undefined || layer.encoding[channel].scale === args.scale));
110
+ const scales = [...new Set(layers.map(layer => layer.encoding[channel].scale))];
111
+ if (args.scale === undefined && scales.length !== 1) throw new Error("Named axes require an explicit scale when the channel binding is ambiguous.");
112
+ const scale = args.scale ?? scales[0];
113
+ const coordinates = [...new Set(layers.map(layer => layer.coordinate))];
114
+ if (args.coordinate === undefined && coordinates.length !== 1) throw new Error("Named axes require an explicit coordinate when the binding is ambiguous.");
115
+ const coordinate = args.coordinate ?? coordinates[0];
116
+ if (requireCoordinate(this, coordinate).type !== "cartesian") throw new Error("Named Cartesian axes require a Cartesian coordinate.");
117
+ const position = validateAxisPosition(channel, args.position ?? defaultAxisPosition(channel));
118
+ assertCartesianAxisSideAvailable(this, { id: axisId, channel, coordinate, position });
119
+ args = { ...args, scale, coordinate, position };
120
+ source = source.editSemantic({ property: `guide.axis.${axisId}.channel`, value: channel })
121
+ .editSemantic({ property: `guide.axis.${axisId}.position`, value: position })
122
+ .editSemantic({ property: `guide.axis.${axisId}.scale`, value: scale })
123
+ .editSemantic({ property: `guide.axis.${axisId}.coordinate`, value: coordinate });
124
+ } else if (cartesianAxisOwners(this).some(owner => owner.named)) {
125
+ const scale = args.scale ?? channel;
126
+ const coordinates = [...new Set(this.semanticSpec.layers.filter(layer =>
127
+ layer.encoding?.[channel]?.scale === scale).map(layer => layer.coordinate))];
128
+ const coordinate = args.coordinate ?? (coordinates.length === 1 ? coordinates[0] : undefined);
129
+ assertCartesianAxisSideAvailable(this, { id: axisId, channel, coordinate,
130
+ position: validateAxisPosition(channel, args.position ?? defaultAxisPosition(channel)) });
131
+ if (!cartesianAxisOwners(this).some(owner => owner.id === channel)) {
132
+ source = source.editSemantic({ property: `guide.axis.${axisId}.scale`, value: scale });
133
+ if (coordinate !== undefined) source = source.editSemantic({ property: `guide.axis.${axisId}.coordinate`, value: coordinate });
134
+ }
135
+ }
136
+ const shared = axisTargetArgs(axisId, channel);
88
137
  if (Object.hasOwn(args, "scale")) shared.scale = args.scale;
89
138
  if (Object.hasOwn(args, "position")) shared.position = args.position;
90
- let next = this;
139
+ let next = Object.hasOwn(args, "offset")
140
+ ? source._withGuideConfig(axisId, "frame", { offset: args.offset }) : source;
91
141
 
92
142
  if (Object.hasOwn(args, "coordinate")) {
93
143
  const coordinate = validateUserId(args.coordinate, "Coordinate id");
@@ -116,11 +166,11 @@ function makeCreateAxis(channel) {
116
166
  }
117
167
 
118
168
  next = next.editSemantic({
119
- property: `guide.axis.${channel}.coordinate`,
169
+ property: `guide.axis.${axisId}.coordinate`,
120
170
  value: coordinate
121
171
  });
122
- if (standalone) next = next._withGuideConfig(channel, "binding", { standalone: true })
123
- .editSemantic({ property: `guide.axis.${channel}.scale`, value: scale })
172
+ if (standalone) next = next._withGuideConfig(axisId, "binding", { standalone: true })
173
+ .editSemantic({ property: `guide.axis.${axisId}.scale`, value: scale })
124
174
  .rematerializeScale({ id: scale, guides: false, marks: false });
125
175
  }
126
176
 
@@ -2,8 +2,8 @@ import { axisGraphicIds } from "../../../materialization/guides/resources.js";
2
2
 
3
3
  export const AXIS_COMPONENTS = Object.freeze(["line", "ticks", "labels", "title"]);
4
4
 
5
- export function axisComponentId(channel, component) {
6
- return axisGraphicIds(channel)[AXIS_COMPONENTS.indexOf(component)];
5
+ export function axisComponentId(channel, component, axisId = channel) {
6
+ return axisGraphicIds(channel, axisId)[AXIS_COMPONENTS.indexOf(component)];
7
7
  }
8
8
 
9
9
  export function axisComponentDisabled(args, component) {
@@ -17,27 +17,27 @@ export function validateEnabledAxisComponents(args, operation) {
17
17
  }
18
18
  }
19
19
 
20
- export function hasAxisComponent(program, channel, component) {
21
- return program.graphicSpec.objects[axisComponentId(channel, component)] !== undefined ||
22
- program.guideConfigs.axis?.[channel]?.[component] !== undefined ||
20
+ export function hasAxisComponent(program, channel, component, axisId = channel) {
21
+ return program.graphicSpec.objects[axisComponentId(channel, component, axisId)] !== undefined ||
22
+ program.guideConfigs.axis?.[axisId]?.[component] !== undefined ||
23
23
  (component === "title" &&
24
- program.semanticSpec.guides.axis?.[channel]?.title !== undefined);
24
+ program.semanticSpec.guides.axis?.[axisId]?.title !== undefined);
25
25
  }
26
26
 
27
- export function assertRemovableAxisComponent(program, channel, component, operation) {
28
- if (!hasAxisComponent(program, channel, component)) {
27
+ export function assertRemovableAxisComponent(program, channel, component, operation, axisId = channel) {
28
+ if (!hasAxisComponent(program, channel, component, axisId)) {
29
29
  throw new Error(`${operation}.${component} requires an existing ${channel}-axis ${component}.`);
30
30
  }
31
31
  }
32
32
 
33
- export function removeAxisComponent(program, channel, component) {
34
- const id = axisComponentId(channel, component);
33
+ export function removeAxisComponent(program, channel, component, axisId = channel) {
34
+ const id = axisComponentId(channel, component, axisId);
35
35
  let next = program;
36
- if (component === "title" && next.semanticSpec.guides.axis?.[channel]?.title !== undefined) {
37
- next = next.editSemantic({ property: `guide.axis.${channel}.title`, remove: true });
36
+ if (component === "title" && next.semanticSpec.guides.axis?.[axisId]?.title !== undefined) {
37
+ next = next.editSemantic({ property: `guide.axis.${axisId}.title`, remove: true });
38
38
  }
39
39
  if (next.graphicSpec.objects[id] !== undefined) {
40
40
  next = next.editGraphics({ target: id, remove: true });
41
41
  }
42
- return next._withoutMaterializationConfig(["guides", "axis", channel, component]);
42
+ return next._withoutMaterializationConfig(["guides", "axis", axisId, component]);
43
43
  }