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
@@ -1,3 +1,4 @@
1
+ import { synchronizeFacetLegendAppearance } from "../../materialization/facetGuides/owner.js";
1
2
  import { rematerializeTypography } from "../../materialization/typography.js";
2
3
  import { AXIS_LABEL_FONT_SIZES, AXIS_TITLE_FONT_SIZE, themeTokens } from "../../theme/defaults.js";
3
4
  import {
@@ -182,7 +183,7 @@ function componentRole(program, id, property) {
182
183
  }
183
184
 
184
185
  function graphicRole(program, id, property) {
185
- const axisPart = /Axis(Labels|Titles?)$/u.exec(id);
186
+ const axisPart = /(?:Axis|^axis_.+__)(Labels|Titles?)$/u.exec(id);
186
187
  const scoped = axisPart === null ? undefined : axisTextRole(property, axisPart[1] === "Labels" ? "labels" : "title");
187
188
  if (scoped !== undefined) return scoped;
188
189
  if (property === "fontFamily") return "fontFamily";
@@ -200,11 +201,11 @@ function graphicRole(program, id, property) {
200
201
  }
201
202
  return layer.mark?.type === "text" ? "text" : "mark";
202
203
  }
203
- if (/AxisLines?$/u.test(id) && property === "stroke") return "axis";
204
- if (/AxisTicks$/u.test(id) && property === "stroke") return "mutedText";
204
+ if (/(?:Axis|^axis_.+__)Lines?$/u.test(id) && property === "stroke") return "axis";
205
+ if (/(?:Axis|^axis_.+__)Ticks$/u.test(id) && property === "stroke") return "mutedText";
205
206
  if (/^parallelAxisLabels$/u.test(id) && property === "fill") return "axis";
206
- if (/AxisLabels$/u.test(id) && property === "fill") return "text";
207
- if (/AxisTitles?$/u.test(id) && property === "fill") return "axisTitle";
207
+ if (/(?:Axis|^axis_.+__)Labels$/u.test(id) && property === "fill") return "text";
208
+ if (/(?:Axis|^axis_.+__)Titles?$/u.test(id) && property === "fill") return "axisTitle";
208
209
  if (/Grid(?:Lines|Circles)?$/u.test(id) && property === "stroke") return "grid";
209
210
  if (id === "chartTitle" && property === "fill") return "strongText";
210
211
  if (id === "chartSubtitle" && property === "fill") return "mutedText";
@@ -676,12 +677,13 @@ function clearRemovedResources(overrides, node) {
676
677
  clearPrefix(overrides, `g:${mark[1]}.`);
677
678
  return;
678
679
  }
679
- const axis = /^guide\.axis\.(x|y|theta|radius)$/u
680
+ const axis = /^guide\.axis\.([A-Za-z0-9_-]+)$/u
680
681
  .exec(current.args.property);
681
682
  if (axis !== null) {
682
683
  const prefix = axis[1] === "radius" ? "radial" : axis[1];
684
+ const graphicPrefix = ["x", "y", "theta", "radius"].includes(axis[1]) ? `${prefix}Axis` : `axis_${axis[1]}__`;
683
685
  clearPrefix(overrides, `c:guides.axis.${axis[1]}.`);
684
- clearPrefix(overrides, `g:${prefix}Axis`);
686
+ clearPrefix(overrides, `g:${graphicPrefix}`);
685
687
  return;
686
688
  }
687
689
  const grid = /^guide\.grid\.([^\.]+)$/u.exec(current.args.property);
@@ -737,7 +739,23 @@ function clearRecreatedOverrides(overrides, op, args) {
737
739
  }
738
740
  }
739
741
 
740
- function addAxisOverrides(overrides, op, args) {
742
+ function axisOverrideId(node, channel, args) {
743
+ if (args.target !== undefined || args.id !== undefined) return args.target ?? args.id;
744
+ const ids = new Set();
745
+ const visit = entry => {
746
+ if (["createGraphics", "editGraphics"].includes(entry.op)) {
747
+ const id = entry.args.target ?? entry.args.id;
748
+ const named = /^axis_(.+)__(Line|Ticks|Labels|Title)$/u.exec(id ?? "");
749
+ if (named !== null) ids.add(named[1]);
750
+ else if (new RegExp(`^${channel}Axis(?:Line|Ticks|Labels|Title)$`).test(id ?? "")) ids.add(channel);
751
+ }
752
+ entry.children?.forEach(visit);
753
+ };
754
+ if (node !== undefined) visit(node);
755
+ return ids.size === 1 ? [...ids][0] : channel;
756
+ }
757
+
758
+ function addAxisOverrides(overrides, op, args, node) {
741
759
  const match = /^(?:create|edit)(X|Y|Theta|Radial)Axis(Line|Ticks|Labels|Title)$/u
742
760
  .exec(op);
743
761
  if (match === null) return;
@@ -748,23 +766,25 @@ function addAxisOverrides(overrides, op, args) {
748
766
  Radial: "radius"
749
767
  }[match[1]];
750
768
  const component = match[2].toLowerCase();
769
+ const axisId = axisOverrideId(node, channel, args);
751
770
  const idPrefix = match[1] === "Radial" ? "radial" : channel;
752
771
  if (Object.hasOwn(args, "color")) {
753
- overrides.add(`c:guides.axis.${channel}.${component}.color`);
772
+ overrides.add(`c:guides.axis.${axisId}.${component}.color`);
754
773
  overrides.add(
755
- `g:${idPrefix}Axis${match[2]}.${component === "line" || component === "ticks" ? "stroke" : "fill"}`
774
+ `g:${axisId === channel ? `${idPrefix}Axis` : `axis_${axisId}__`}${match[2]}.${component === "line" || component === "ticks" ? "stroke" : "fill"}`
756
775
  );
757
776
  }
758
777
  for (const property of ["fontFamily", "fontSize"]) {
759
778
  if (["labels", "title"].includes(component) && Object.hasOwn(args, property)) {
760
- overrides.add(`c:guides.axis.${channel}.${component}.${property}`);
761
- overrides.add(`g:${idPrefix}Axis${match[2]}.${property}`);
779
+ overrides.add(`c:guides.axis.${axisId}.${component}.${property}`);
780
+ overrides.add(`g:${axisId === channel ? `${idPrefix}Axis` : `axis_${axisId}__`}${match[2]}.${property}`);
762
781
  }
763
782
  }
764
783
  }
765
784
 
766
- function addCompleteAxisOverride(overrides, name, args) {
785
+ function addCompleteAxisOverride(overrides, name, args, node) {
767
786
  const channel = { X: "x", Y: "y", Theta: "theta", Radial: "radius" }[name];
787
+ const axisId = axisOverrideId(node, channel, args);
768
788
  const idPrefix = name === "Radial" ? "radial" : channel;
769
789
  const components = {
770
790
  line: args.line,
@@ -779,21 +799,21 @@ function addCompleteAxisOverride(overrides, name, args) {
779
799
  for (const [component, options] of Object.entries(components)) {
780
800
  const suffix = `${component[0].toUpperCase()}${component.slice(1)}`;
781
801
  if (Object.hasOwn(options ?? {}, "color")) {
782
- overrides.add(`c:guides.axis.${channel}.${component}.color`);
802
+ overrides.add(`c:guides.axis.${axisId}.${component}.color`);
783
803
  overrides.add(
784
- `g:${idPrefix}Axis${suffix}.${component === "line" || component === "ticks" ? "stroke" : "fill"}`
804
+ `g:${axisId === channel ? `${idPrefix}Axis` : `axis_${axisId}__`}${suffix}.${component === "line" || component === "ticks" ? "stroke" : "fill"}`
785
805
  );
786
806
  }
787
807
  for (const property of ["fontFamily", "fontSize"]) {
788
808
  if (["labels", "title"].includes(component) && Object.hasOwn(options ?? {}, property)) {
789
- overrides.add(`c:guides.axis.${channel}.${component}.${property}`);
790
- overrides.add(`g:${idPrefix}Axis${suffix}.${property}`);
809
+ overrides.add(`c:guides.axis.${axisId}.${component}.${property}`);
810
+ overrides.add(`g:${axisId === channel ? `${idPrefix}Axis` : `axis_${axisId}__`}${suffix}.${property}`);
791
811
  }
792
812
  }
793
813
  }
794
814
  }
795
815
 
796
- function addCompleteAxisOverrides(overrides, op, args) {
816
+ function addCompleteAxisOverrides(overrides, op, args, node) {
797
817
  if (args.guides?.axes !== null && typeof args.guides?.axes === "object") {
798
818
  addCompleteAxisOverrides(overrides, "createAxes", args.guides.axes);
799
819
  }
@@ -801,10 +821,10 @@ function addCompleteAxisOverrides(overrides, op, args) {
801
821
  addCompleteAxisOverrides(overrides, "createAxes", args.axes);
802
822
  }
803
823
  const match = /^(?:create|edit)(X|Y|Theta|Radial)Axis$/u.exec(op);
804
- if (match !== null) addCompleteAxisOverride(overrides, match[1], args);
824
+ if (match !== null) addCompleteAxisOverride(overrides, match[1], args, node);
805
825
  const group = /^(?:create|edit)(X|Y)AxisTicksAndLabels$/u.exec(op);
806
826
  if (group !== null) {
807
- addCompleteAxisOverride(overrides, group[1], { ticksAndLabels: args });
827
+ addCompleteAxisOverride(overrides, group[1], { target: args.target, ticksAndLabels: args }, node);
808
828
  }
809
829
  if (op !== "createAxes") return;
810
830
  for (const [option, name] of [
@@ -814,7 +834,7 @@ function addCompleteAxisOverrides(overrides, op, args) {
814
834
  ["radius", "Radial"]
815
835
  ]) {
816
836
  if (args[option] && typeof args[option] === "object") {
817
- addCompleteAxisOverride(overrides, name, args[option]);
837
+ for (const entry of Array.isArray(args[option]) ? args[option] : [args[option]]) addCompleteAxisOverride(overrides, name, entry);
818
838
  }
819
839
  }
820
840
  }
@@ -977,8 +997,8 @@ function explicitOverrides(program, node) {
977
997
  addErrorBarOverrides(overrides, program, node);
978
998
  addErrorBandOverrides(overrides, program, node);
979
999
  addRegressionOverrides(overrides, node);
980
- addAxisOverrides(overrides, op, args);
981
- addCompleteAxisOverrides(overrides, op, args);
1000
+ addAxisOverrides(overrides, op, args, node);
1001
+ addCompleteAxisOverrides(overrides, op, args, node);
982
1002
  addGridOverrides(overrides, op, args);
983
1003
  addParallelOverrides(overrides, program, op, args);
984
1004
  if (["createTitle", "editTitle"].includes(op)) {
@@ -999,6 +1019,11 @@ function explicitOverrides(program, node) {
999
1019
  if (op === "editLegendBlock") {
1000
1020
  addLegendBlockOverrides(overrides, program, args);
1001
1021
  }
1022
+ const parentLegendOptions = op === "editFacetGuides" ? args.legend
1023
+ : ["facet", "facetGrid", "repeatCharts"].includes(op) ? args.guides?.legend : undefined;
1024
+ if (parentLegendOptions !== null && typeof parentLegendOptions === "object") {
1025
+ addLegendOverrides(overrides, program, parentLegendOptions);
1026
+ }
1002
1027
  if (op === "editFacetHeaders") {
1003
1028
  addFacetOverrides(overrides, program, args);
1004
1029
  }
@@ -1084,6 +1109,8 @@ export function reconcileProgramTheme(program, { source, metadata }) {
1084
1109
  next = next._withMaterializationConfig(["facets"], configs.value.facets);
1085
1110
  }
1086
1111
 
1112
+ if (configs.changed) next = synchronizeFacetLegendAppearance(next);
1113
+
1087
1114
  const facetsChanged = configs.value.facets !==
1088
1115
  themed.materializationConfigs.facets;
1089
1116
  const typographyChanged = sourceTokens.fontFamily !== targetTokens.fontFamily ||
@@ -202,10 +202,10 @@ function validateFacetSpec(compositionSpec) {
202
202
  !isPlainObject(guides) ||
203
203
  Object.keys(guides).some(key => !["axes", "legend"].includes(key)) ||
204
204
  !["each", "outer"].includes(guides.axes) ||
205
- ![false, "shared"].includes(guides.legend)
205
+ (![false, "shared"].includes(guides.legend) && !isPlainObject(guides.legend))
206
206
  ) {
207
207
  throw new Error(
208
- 'compositionSpec.facet.guides requires axes "each" or "outer" and legend false or "shared".'
208
+ 'compositionSpec.facet.guides requires axes "each" or "outer" and legend false, "shared", or parent legend options.'
209
209
  );
210
210
  }
211
211
  }
@@ -0,0 +1 @@
1
+ function n(e,t,i){for(const[r,c]of Object.entries(i))e=e.editGraphics({target:t,property:r,value:c});return e}export{n as editGraphicProperties};
@@ -44,6 +44,10 @@ export function releaseNamedResourceOwnership(program, { kind, id, dataOwner })
44
44
  materializationConfigs,
45
45
  ["calculations", "datasets", dataOwner?.current ?? id]
46
46
  ).value;
47
+ materializationConfigs = removeMaterializationConfig(
48
+ materializationConfigs,
49
+ ["regressionFits", dataOwner?.current ?? id]
50
+ ).value;
47
51
  const context = program.context.currentData === id
48
52
  ? freezeOwned({ ...program.context, currentData: undefined })
49
53
  : program.context;
@@ -97,7 +97,7 @@ export function collectSemanticScaleReferences(program) {
97
97
  }
98
98
  }
99
99
  collectKnownKeys(program.semanticSpec.guides, [], (key, value, path) => {
100
- if (key === "scale") {
100
+ if (["scale", "colorScale", "sizeScale"].includes(key)) {
101
101
  append(references, reference(
102
102
  "scale", value, "semanticGuide", "guides", path
103
103
  ));
@@ -208,7 +208,7 @@ export function collectGuideConfigReferences(program) {
208
208
  const relativePath = parentPath.length === 0
209
209
  ? path
210
210
  : path.slice(parentPath.length);
211
- if (key === "scale") {
211
+ if (["scale", "colorScale", "sizeScale"].includes(key)) {
212
212
  append(references, reference(
213
213
  "scale", value, family, ownerId, relativePath
214
214
  ));
@@ -1,5 +1,6 @@
1
1
  import { annotateError } from "./diagnostics.js";
2
2
  import { isPlainObject } from "./immutable.js";
3
+ import { getMarkGraphicTypes } from "./vocabulary.js";
3
4
 
4
5
  export const MAX_GENERATED_ITEMS = 10_000;
5
6
  export const MAX_WORK_ITEMS = 10_000_000;
@@ -30,6 +31,14 @@ export function validateWorkLimit(value, label) {
30
31
  return validateGeneratedItemLimit(value, label, MAX_WORK_ITEMS);
31
32
  }
32
33
 
34
+ export function validateGraphicItemLimit(value, capacity) {
35
+ const point = capacity.pointOwned && getMarkGraphicTypes("point").includes(capacity.graphicType);
36
+ const label = point ? "Point mark item count" : capacity.label;
37
+ const maximum = point ? 50_000 : MAX_GENERATED_ITEMS;
38
+ if (point) validateGeneratedItemLimit(capacity.rawCount, label, maximum);
39
+ return validateGeneratedItemLimit(value, label, maximum);
40
+ }
41
+
33
42
  export function validateKeys(value, supported, label) {
34
43
  for (const key of Object.keys(value)) {
35
44
  if (!supported.includes(key)) {
@@ -220,6 +220,7 @@ export const LEGEND_CHANNELS = Object.freeze([
220
220
  ]);
221
221
 
222
222
  export const LEGEND_CONFIG_KINDS = Object.freeze([
223
+ "colorSize",
223
224
  "series",
224
225
  "color",
225
226
  "stroke",
@@ -239,3 +240,5 @@ export function includesVocabulary(vocabulary, value) {
239
240
  export const PLOT_LEGEND_POSITIONS = Object.freeze(["top-left", "top-right", "bottom-left", "bottom-right"]);
240
241
  export const LEGEND_POSITIONS = Object.freeze(["left", "right", "top", "bottom", ...PLOT_LEGEND_POSITIONS]);
241
242
  export const LEGEND_BLOCK_LAYOUTS = Object.freeze(["row", "column", "wrap"]);
243
+
244
+ export const AXIS_RESOURCE_CHANNELS = Object.freeze(["x", "y", "theta", "radius", "parallel"]);
@@ -0,0 +1 @@
1
+ import{resolveTextLines as o}from"../core/textMetrics.js";function a(e,{title:i=!1}={}){if(Object.hasOwn(e,"align")&&!["left","center","right"].includes(e.align))throw new TypeError("Axis text align must be left, center, or right.");if(i){if(Object.hasOwn(e,"baseline")&&!["top","hanging","middle","alphabetic","ideographic","bottom"].includes(e.baseline))throw new TypeError("Axis title baseline must be a supported text baseline.");for(const t of["offset","alongOffset"])if(Object.hasOwn(e,t)&&!Number.isFinite(e[t]))throw new TypeError(`Axis title ${t} must be a finite number.`)}}function f(e,i){if(!/[\r\n]/u.test(e))return;const t=i.baseline??"middle",n=o(e,{fontSize:i.fontSize,align:i.align??"center",baseline:t,...t==="middle"?{blockAlign:"middle"}:{}});if(["bottom","ideographic"].includes(t)){const s=(n.length-1)*i.fontSize*1.2;return n.map(r=>({...r,y:r.y-s}))}return n.length>1?n:void 0}export{f as axisTitleTextLines,a as validateAxisTextPlacement};
@@ -0,0 +1,39 @@
1
+ import {sameOrderedValues,validateOptionObject,validateUnitInterval,validateNonNegativeFinite,validateNonEmptyString} from '../core/validation.js';
2
+ import {isContinuousColorScaleType} from './scales/types.js';
3
+ import {isContinuousSizeScaleType} from './scales/size.js';
4
+
5
+ export function isColorSizeChannels(channels) {
6
+ return Array.isArray(channels)&&channels.length===2&&channels.includes('color')&&channels.includes('size');
7
+ }
8
+
9
+ export function resolveColorSizeMapping({layer,scales,pointConfig,coordinate}) {
10
+ const color=layer?.encoding?.color,size=layer?.encoding?.size;
11
+ const colorScale=scales[color?.scale],sizeScale=scales[size?.scale];
12
+ if(layer?.mark?.type!=='point'||coordinate?.type!=='cartesian'||
13
+ pointConfig?.shape!=='circle'||layer.encoding.shape!==undefined||
14
+ color?.fieldType!=='quantitative'||size?.fieldType!=='quantitative'||
15
+ typeof color.field!=='string'||color.field!==size.field||
16
+ color.aggregate!==size.aggregate||color.data!==size.data||
17
+ !isContinuousColorScaleType(colorScale?.type)||!isContinuousSizeScaleType(sizeScale?.type)) {
18
+ throw new Error('Color-size combination requires one Cartesian circle point owner with the same quantitative field and grain.');
19
+ }
20
+ const ends=scale=>[scale.domain?.[0],scale.domain?.at(-1)];
21
+ const domain=ends(colorScale),sizeDomain=ends(sizeScale);
22
+ if(!domain.every(Number.isFinite)||!sizeDomain.every(Number.isFinite)||
23
+ !sameOrderedValues(domain,sizeDomain)||domain[0]===domain[1]) {
24
+ throw new Error('Color-size combination requires identical finite numeric domain endpoints.');
25
+ }
26
+ return {target:layer.id,field:color.field,fieldType:'quantitative',colorScale:color.scale,sizeScale:size.scale,domain};
27
+ }
28
+
29
+ export function normalizeColorSizeSymbol(value,previous) {
30
+ if(value===undefined)return {...(previous??{type:'point',strokeWidth:0,opacity:1})};
31
+ if(value==='auto')return {type:'point',strokeWidth:0,opacity:1};
32
+ validateOptionObject(value,['type','stroke','strokeWidth','opacity'],'Color-size symbol');
33
+ if(value.type!==undefined&&value.type!=='point')throw new Error('Color-size symbols require type point.');
34
+ const symbol={type:'point',strokeWidth:0,opacity:1,...previous,...value};
35
+ if(symbol.stroke!==undefined)validateNonEmptyString(symbol.stroke,'Color-size symbol stroke');
36
+ validateNonNegativeFinite(symbol.strokeWidth,'Color-size symbol strokeWidth');
37
+ validateUnitInterval(symbol.opacity,'Color-size symbol opacity');
38
+ return symbol;
39
+ }
@@ -1,4 +1,5 @@
1
1
  import {parseTimeValue,validateParseTimeFormat} from "./parseTime.js";
2
+ import { calendarParts, validateTimeZone } from "./timeUnit.js";
2
3
  import { rejectUnknownProperties as rejectUnknownKeys } from "../core/validation.js";
3
4
  import { requireStringValue as requireField } from "../core/validation.js";
4
5
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
@@ -6,7 +7,7 @@ import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
6
7
  const TRANSFORM_KEYS = Object.freeze(["type", "as", "expression"]);
7
8
  const ARITHMETIC_BINARY = new Set(["add", "subtract", "multiply", "divide"]);
8
9
  const COMPARISONS = new Set(["eq", "neq", "lt", "lte", "gt", "gte"]);
9
- const NUMERIC_UNARY = new Set(["negate", "absolute", "log", "sqrt"]);
10
+ const NUMERIC_UNARY = new Set(["negate", "absolute", "log", "sqrt", "floor"]);
10
11
  const BOOLEAN_UNARY = new Set(["not", "isNull"]);
11
12
  const LOGICAL = new Set(["and", "or"]);
12
13
  const VARIADIC = new Set(["coalesce", "concat"]);
@@ -107,6 +108,14 @@ function inspectExpression(expression, depth, state, optionalPathRoot = false) {
107
108
  inspectExpression(expression.operand, depth + 1, state);
108
109
  return;
109
110
  }
111
+ if (expression.op === "datePart") {
112
+ rejectUnknownKeys(expression, ["op", "part", "operand", "timeZone"], "computed calendar expression");
113
+ requireNode(expression, "operand", expression.op);
114
+ if (expression.part !== "year") throw new Error("Computed datePart supports part year only.");
115
+ if (Object.hasOwn(expression, "timeZone")) validateTimeZone(expression.timeZone);
116
+ inspectExpression(expression.operand, depth + 1, state);
117
+ return;
118
+ }
110
119
  if (expression.op === "parseTime") {
111
120
  rejectUnknownKeys(expression, ["op", "operand", "format"], "computed time expression");
112
121
  requireNode(expression, "operand", expression.op);
@@ -339,6 +348,18 @@ function evaluate(expression, row, rowIndex) {
339
348
  ? value === null
340
349
  : !boolean(value, operation, rowIndex);
341
350
  }
351
+ if (operation === "datePart") {
352
+ const value = evaluate(expression.operand, row, rowIndex);
353
+ if (value === null) return null;
354
+ number(value, operation, rowIndex);
355
+ if (Math.abs(value) > 8_640_000_000_000_000) {
356
+ throw new RangeError(`Computed datePart timestamp is outside the supported Date range at row ${rowIndex}.`);
357
+ }
358
+ const timestamp = new Date(value).getTime();
359
+ return (expression.timeZone ?? "UTC") === "UTC"
360
+ ? new Date(timestamp).getUTCFullYear()
361
+ : calendarParts(timestamp, expression.timeZone).year;
362
+ }
342
363
  if (operation === "parseTime") {
343
364
  return parseTimeValue(evaluate(expression.operand, row, rowIndex), expression.format);
344
365
  }
@@ -360,6 +381,7 @@ function evaluate(expression, row, rowIndex) {
360
381
  }
361
382
  const result = operation === "negate" ? -value
362
383
  : operation === "absolute" ? Math.abs(value)
384
+ : operation === "floor" ? Math.floor(value)
363
385
  : operation === "log" ? Math.log(value)
364
386
  : Math.sqrt(value);
365
387
  if (!Number.isFinite(result)) {
@@ -1,6 +1,7 @@
1
1
  import { requireStringValue as requireName } from "../core/validation.js";
2
2
  import { annotateError } from "../core/diagnostics.js";
3
3
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
4
+ import { requireRegressionStatisticsSource, REGRESSION_STATISTIC_FIELDS } from "./regression/statistics.js";
4
5
 
5
6
  const STORAGE_TYPES = new Set([
6
7
  "number", "string", "boolean", "array", "object", "unknown", "mixed"
@@ -267,7 +268,7 @@ function generatedField(name, storageType, transform, role, inputs = []) {
267
268
  };
268
269
  }
269
270
 
270
- export function deriveTransformSchema(inputSchema, transform, values, { sourceId, ownerId } = {}) {
271
+ export function deriveTransformSchema(inputSchema, transform, values, { sourceId, ownerId, sourceTransform } = {}) {
271
272
  const input = validateDatasetSchema(inputSchema);
272
273
  const definition = { ...transform, __sourceId: sourceId, __ownerId: ownerId };
273
274
  const retain = ["filter", "markFilter", "sort", "computed", "timeUnit", "window", "normalize", "stack", "impute", "complete"];
@@ -300,6 +301,12 @@ export function deriveTransformSchema(inputSchema, transform, values, { sourceId
300
301
  generatedField(transform.as.count, "number", definition, "count", [transform.x, transform.y])
301
302
  ];
302
303
  if (transform.as.members !== undefined) push(generatedField(transform.as.members, "array", definition, "members"));
304
+ } else if (transform.type === "regressionStatistics") {
305
+ const fit = requireRegressionStatisticsSource({ transform: sourceTransform === undefined ? [] : [sourceTransform] });
306
+ fields = [
307
+ ...(fit.groupBy === undefined ? [] : [inheritedField(input, fit.groupBy)]),
308
+ ...REGRESSION_STATISTIC_FIELDS.map(name => generatedField(name, "number", definition, name))
309
+ ].filter(Boolean);
303
310
  } else if (transform.type === "regression") {
304
311
  fields = [inheritedField(input, transform.x), inheritedField(input, transform.y)].filter(Boolean);
305
312
  if (transform.groupBy !== undefined) fields.unshift(inheritedField(input, transform.groupBy));
@@ -367,7 +374,8 @@ export function deriveTransformSchema(inputSchema, transform, values, { sourceId
367
374
  if (planned.lineage === undefined && !["complete", "impute"].includes(transform.type)) {
368
375
  return planned;
369
376
  }
370
- return { ...planned, storageType: field.storageType, nullable: field.nullable, optional: field.optional };
377
+ return { ...planned, storageType: transform.type === "regressionStatistics" && planned.storageType === "number"
378
+ ? "number" : field.storageType, nullable: field.nullable, optional: field.optional };
371
379
  });
372
380
  for (const field of inferred.fields) {
373
381
  if (!fields.some(planned => planned.name === field.name)) fields.push(field);
@@ -44,6 +44,14 @@ export function normalizeDisplayLabelMap(value, label = "labelMap") {
44
44
  return Object.freeze(normalized);
45
45
  }
46
46
 
47
+ export function normalizeNumericDisplayLabelMap(value, label = "labelMap") {
48
+ const map = normalizeDisplayLabelMap(value, label);
49
+ if (map.some(entry => typeof entry.value !== "number" || !Number.isFinite(entry.value))) {
50
+ throw new TypeError(`${label} values must be finite numbers.`);
51
+ }
52
+ return map;
53
+ }
54
+
47
55
  export function resolveDisplayLabel(value, map, fallback) {
48
56
  if (map !== undefined) {
49
57
  for (const entry of map) {
@@ -3,13 +3,15 @@ import { cloneAndFreeze, isPlainObject } from "../../core/immutable.js";
3
3
  const LEGEND_FAMILIES = Object.freeze({
4
4
  series: "categorical",
5
5
  color: "categorical",
6
+ colorSize: "color-size",
6
7
  stroke: "categorical",
7
8
  gradient: "gradient",
8
9
  interval: "discretized",
9
10
  strokeGradient: "gradient",
10
11
  strokeInterval: "discretized",
11
12
  size: "size",
12
- opacity: "opacity"
13
+ opacity: "opacity",
14
+ strokeWidth: "stroke-width"
13
15
  });
14
16
 
15
17
  function requirePlacements(placements) {
@@ -72,25 +74,34 @@ function translatedBounds(placement, bounds, channel) {
72
74
  };
73
75
  }
74
76
 
77
+ export function resolveFacetOuterSides({ placements } = {}) {
78
+ const cells = requirePlacements(placements);
79
+ const edges = { top: new Map(), bottom: new Map(), left: new Map(), right: new Map() };
80
+ for (const cell of cells) {
81
+ for (const [side, key, coordinate, better] of [
82
+ ["top", cell.column, "row", (a, b) => a < b],
83
+ ["bottom", cell.column, "row", (a, b) => a > b],
84
+ ["left", cell.row, "column", (a, b) => a < b],
85
+ ["right", cell.row, "column", (a, b) => a > b]
86
+ ]) {
87
+ const previous = edges[side].get(key);
88
+ if (previous === undefined || better(cell[coordinate], previous[coordinate])) edges[side].set(key, cell);
89
+ }
90
+ }
91
+ return cloneAndFreeze(Object.fromEntries(Object.entries(edges).map(([side, owners]) => {
92
+ const ids = new Set([...owners.values()].map(cell => cell.id));
93
+ return [side, cells.filter(cell => ids.has(cell.id)).map(cell => cell.id)];
94
+ })));
95
+ }
96
+
75
97
  export function resolveFacetOuterAxes({ placements, axisBounds = {} } = {}) {
76
98
  const cells = requirePlacements(placements);
77
99
  if (!isPlainObject(axisBounds)) {
78
100
  throw new TypeError("Facet axisBounds must be a plain object.");
79
101
  }
80
- const bottomByColumn = new Map();
81
- const leftByRow = new Map();
82
- for (const cell of cells) {
83
- const bottom = bottomByColumn.get(cell.column);
84
- if (bottom === undefined || cell.row > bottom.row) {
85
- bottomByColumn.set(cell.column, cell);
86
- }
87
- const left = leftByRow.get(cell.row);
88
- if (left === undefined || cell.column < left.column) {
89
- leftByRow.set(cell.row, cell);
90
- }
91
- }
92
- const xOwners = new Set([...bottomByColumn.values()].map(cell => cell.id));
93
- const yOwners = new Set([...leftByRow.values()].map(cell => cell.id));
102
+ const sides = resolveFacetOuterSides({ placements: cells });
103
+ const xOwners = new Set(sides.bottom);
104
+ const yOwners = new Set(sides.left);
94
105
  return cloneAndFreeze({
95
106
  x: cells.filter(cell => xOwners.has(cell.id)).map(cell => cell.id),
96
107
  y: cells.filter(cell => yOwners.has(cell.id)).map(cell => cell.id),
@@ -129,7 +140,7 @@ function comparableConfig(config) {
129
140
  return stableValue(rest);
130
141
  }
131
142
 
132
- function sameValue(left, right) {
143
+ export function sameFacetGuideValue(left, right) {
133
144
  return JSON.stringify(stableValue(left)) === JSON.stringify(stableValue(right));
134
145
  }
135
146
 
@@ -152,12 +163,13 @@ function requireLegendChildren(children) {
152
163
  });
153
164
  }
154
165
 
155
- function scaleIds(config) {
156
- const ids = config.scales ?? (config.scale === undefined ? [] : [config.scale]);
166
+ function scaleIds(config, kind) {
167
+ const ids = kind === "colorSize" ? [config.colorScale, config.sizeScale]
168
+ : config.scales ?? (config.scale === undefined ? [] : [config.scale]);
157
169
  if (!Array.isArray(ids) || ids.length === 0 || !ids.every(id =>
158
170
  typeof id === "string" && id.length > 0
159
171
  )) {
160
- throw new Error(`Shared ${config.kind} legend requires resolved scale ids.`);
172
+ throw new Error(`Shared ${kind} legend requires resolved scale ids.`);
161
173
  }
162
174
  return ids;
163
175
  }
@@ -166,7 +178,7 @@ export function resolveSharedFacetLegends(children) {
166
178
  const normalized = requireLegendChildren(children);
167
179
  const first = normalized[0];
168
180
  for (const child of normalized.slice(1)) {
169
- if (!sameValue(first.guideConfigs.legendGroups, child.guideConfigs.legendGroups)) {
181
+ if (!sameFacetGuideValue(first.guideConfigs.legendGroups, child.guideConfigs.legendGroups)) {
170
182
  throw new Error(`Facet child "${child.id}" has incompatible legend group placement.`);
171
183
  }
172
184
  }
@@ -183,7 +195,7 @@ export function resolveSharedFacetLegends(children) {
183
195
  ) {
184
196
  throw new Error(`Facet child "${first.id}" has invalid ${kind} legend config.`);
185
197
  }
186
- const scales = scaleIds(config);
198
+ const scales = scaleIds(config, kind);
187
199
  const resolvedScales = Object.fromEntries(scales.map(id => {
188
200
  const scale = first.resolvedScales[id];
189
201
  if (!isPlainObject(scale)) {
@@ -195,14 +207,14 @@ export function resolveSharedFacetLegends(children) {
195
207
  const candidate = child.legends[kind];
196
208
  if (
197
209
  !isPlainObject(candidate) ||
198
- !sameValue(comparableConfig(config), comparableConfig(candidate))
210
+ !sameFacetGuideValue(comparableConfig(config), comparableConfig(candidate))
199
211
  ) {
200
212
  throw new Error(
201
213
  `Facet child "${child.id}" has an incompatible ${kind} legend config.`
202
214
  );
203
215
  }
204
216
  for (const id of scales) {
205
- if (!sameValue(resolvedScales[id], child.resolvedScales[id])) {
217
+ if (!sameFacetGuideValue(resolvedScales[id], child.resolvedScales[id])) {
206
218
  throw new Error(
207
219
  `Facet child "${child.id}" has an incompatible resolved legend scale "${id}".`
208
220
  );
@@ -213,7 +225,7 @@ export function resolveSharedFacetLegends(children) {
213
225
  });
214
226
  for (const child of normalized.slice(1)) {
215
227
  const childKinds = Object.keys(child.legends);
216
- if (!sameValue([...kinds].sort(), [...childKinds].sort())) {
228
+ if (!sameFacetGuideValue([...kinds].sort(), [...childKinds].sort())) {
217
229
  throw new Error(`Facet child "${child.id}" has incompatible legend kinds.`);
218
230
  }
219
231
  }
@@ -234,7 +246,7 @@ export function planFacetGuideOwnership({
234
246
  if (!["each", "outer"].includes(axes)) {
235
247
  throw new Error("Facet axes must be each or outer.");
236
248
  }
237
- if (![false, "shared"].includes(legend)) {
249
+ if (![false, "shared"].includes(legend) && !isPlainObject(legend)) {
238
250
  throw new Error("Facet legend must be false or shared.");
239
251
  }
240
252
  const byId = new Map(children.map(child => [child.id, child]));
@@ -242,23 +254,34 @@ export function planFacetGuideOwnership({
242
254
  throw new Error("Facet guide child ids must match occupied placements exactly.");
243
255
  }
244
256
  const outer = resolveFacetOuterAxes({ placements: cells });
245
- if (legend === "shared" && !isPlainObject(sharedLegends)) {
257
+ const sides = resolveFacetOuterSides({ placements: cells });
258
+ if (legend !== false && !isPlainObject(sharedLegends)) {
246
259
  throw new Error("Shared facet legend ownership requires a compatibility plan.");
247
260
  }
248
261
  return cloneAndFreeze({
249
262
  children: Object.fromEntries(cells.map(cell => {
250
263
  const descriptor = byId.get(cell.id);
251
264
  const presentAxes = descriptor.axes ?? [];
252
- const keepAxes = axes === "each"
253
- ? presentAxes
254
- : presentAxes.filter(channel => outer.children[cell.id][channel]);
265
+ const records = descriptor.axisOwners;
266
+ const removed = {};
267
+ const keepAxes = axes === "each" ? presentAxes : records === undefined
268
+ ? presentAxes.filter(channel => outer.children[cell.id][channel])
269
+ : presentAxes.filter(id => {
270
+ const owner = records.find(owner => owner.id === id);
271
+ if (owner === undefined) throw new Error("Facet axis descriptors must identify every retained owner.");
272
+ const components = Object.entries(owner.components);
273
+ const remove = components.filter(([, side]) => !sides[side]?.includes(cell.id)).map(([component]) => component);
274
+ if (remove.length > 0 && remove.length < components.length) removed[id] = remove;
275
+ return remove.length < components.length;
276
+ });
255
277
  return [cell.id, {
256
278
  keepAxes,
257
279
  removeAxes: presentAxes.filter(channel => !keepAxes.includes(channel)),
258
- removeLegends: legend === "shared" ? descriptor.legendKinds ?? [] : []
280
+ ...(records === undefined ? {} : { removeAxisComponents: removed }),
281
+ removeLegends: legend !== false ? descriptor.legendKinds ?? [] : []
259
282
  }];
260
283
  })),
261
- parent: legend === "shared"
284
+ parent: legend !== false
262
285
  ? { legends: sharedLegends.entries, promoteFrom: sharedLegends.source }
263
286
  : { legends: [] }
264
287
  });