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 +1 @@
1
- import{unionConcreteGraphicBounds as h}from"../../grammar/schemas/graphicBounds.js";function z(e,t,o,n){const r=[],s=(i,a,c,f)=>{if(c===void 0)return;const k=f.filter(L=>e.objects[L]!==void 0),b=h(e,k,n);b!==void 0&&r.push({id:i,kind:a,position:c,bounds:b})};s("chart title","title",o?.position,["chartTitle","chartSubtitle"]);for(const i of["x","y"])for(const a of I(i)){const c=a.slice(`${i}Axis`.length).toLowerCase();s(`${i}-axis ${c}`,"axis",t.axis?.[i]?.[c]?.position,[a])}const l=S(),d=t.legend??{},p=l.find(i=>i.family==="categorical"&&d[i.kind]!==void 0),g=p!==void 0&&d.size!==void 0&&d[p.kind].target===d.size.target&&(d.size.inheritPosition!==!1||d[p.kind].position===d.size.position),m=t.legendGroups??{};for(const[i,a]of Object.entries(m)){const c=l.filter(f=>d[f.kind]?.target===i).flatMap(f=>f.graphicIds);s(`${i} legend group`,"legend",a.placement.position??"free",c)}for(const i of l){const a=d[i.kind];if(m[a?.target]!==void 0||a===void 0||g&&i.kind==="size")continue;const c=g&&i===p?[...i.graphicIds,...l.find(f=>f.kind==="size").graphicIds]:i.graphicIds;s(`${i.kind}${g&&i===p?"+size":""} legend`,"legend",a.position,c)}return r}const y=Object.freeze(["Line","Ticks","Labels","Title"]),u=Object.freeze(["Symbols","SymbolLines","SymbolPoints","SymbolSwatches","Labels","Title","Background"]),O=Object.freeze({series:Object.freeze({semanticKind:"series",family:"categorical",rematerializeOp:void 0,graphicIds:Object.freeze(u.map(e=>`seriesLegend${e}`))}),color:Object.freeze({semanticKind:"color",family:"categorical",rematerializeOp:void 0,graphicIds:Object.freeze(u.map(e=>`colorLegend${e}`))}),stroke:Object.freeze({semanticKind:"stroke",family:"categorical",rematerializeOp:void 0,graphicIds:Object.freeze(u.map(e=>`strokeLegend${e}`))}),size:Object.freeze({semanticKind:"size",family:"size",rematerializeOp:"rematerializeSizeLegend",graphicIds:Object.freeze(["sizeLegendBackground","sizeLegendSymbols","sizeLegendLabels","sizeLegendTitle"])}),gradient:Object.freeze({semanticKind:"color",family:"continuous",rematerializeOp:"rematerializeGradientLegend",graphicIds:Object.freeze(["colorGradientBackground","colorGradientStrips","colorGradientTicks","colorGradientSymbols","colorGradientLabels","colorGradientTitle"])}),interval:Object.freeze({semanticKind:"color",family:"interval",rematerializeOp:"rematerializeIntervalLegend",graphicIds:Object.freeze(["colorLegendBackground","colorLegendSymbols","colorLegendLabels","colorLegendTitle"])}),strokeGradient:Object.freeze({semanticKind:"stroke",family:"continuous",rematerializeOp:"rematerializeStrokeGradientLegend",graphicIds:Object.freeze(["strokeGradientBackground","strokeGradientStrips","strokeGradientTicks","strokeGradientLabels","strokeGradientTitle"])}),strokeInterval:Object.freeze({semanticKind:"stroke",family:"interval",rematerializeOp:"rematerializeStrokeIntervalLegend",graphicIds:Object.freeze(["strokeIntervalBackground","strokeIntervalSymbols","strokeIntervalLabels","strokeIntervalTitle"])}),opacity:Object.freeze({semanticKind:"opacity",family:"continuous",rematerializeOp:"rematerializeOpacityLegend",graphicIds:Object.freeze(["opacityLegendBackground","opacityLegendSymbols","opacityLegendLabels","opacityLegendTitle"])}),strokeWidth:Object.freeze({semanticKind:"strokeWidth",family:"strokeWidth",rematerializeOp:"rematerializeStrokeWidthLegend",graphicIds:Object.freeze(["strokeWidthLegendBackground","strokeWidthLegendSymbols","strokeWidthLegendLabels","strokeWidthLegendTitle"])})});function I(e){const t=e==="radius"?"radial":e;return y.map(o=>`${t}Axis${o}`)}function j(e){return x(e).graphicIds}function x(e){const t=O[e];if(t===void 0)throw new Error(`Unknown legend graphic kind "${e}".`);return t}function S(){return Object.entries(O).map(([e,t])=>({kind:e,...t}))}function v(e){return[...new Set(e.flatMap(j))]}function C(e,t,o){if(t.inferredOffset!==!0||!["top","bottom"].includes(t.position)||t.layout==="legacy-bottom")return t.offset;const n=z(e.graphicSpec,e.guideConfigs,void 0,e.materializationConfigs.textMetrics).filter(s=>s.kind==="axis"&&s.position===t.position),r=t.position==="bottom"?Math.max(0,...n.map(s=>s.bounds.bottom-o.y-o.height)):Math.max(0,...n.map(s=>o.y-s.bounds.top));return t.offset+r}function G(e,t,o){if(t!=="size"||o.inheritPosition===!1)return o;const n=["series","color","stroke"].map(r=>e.guideConfigs.legend?.[r]).find(r=>r?.target===o.target);return n===void 0?o:{...o,position:n.position,offset:n.offset,inferredOffset:n.inferredOffset}}function $(e,t){return[e,t].some(n=>Object.entries(n.guideConfigs.legend??{}).some(([r,s])=>{const l=G(n,r,s);return l.inferredOffset===!0&&["top","bottom"].includes(l.position)&&l.layout!=="legacy-bottom"})||Object.values(n.guideConfigs.legendGroups??{}).some(r=>r.inferredOffset===!0&&["top","bottom"].includes(r.placement.position)))&&[e,t].some(n=>z(n.graphicSpec,n.guideConfigs,void 0,n.materializationConfigs.textMetrics).some(r=>r.kind==="axis"&&["top","bottom"].includes(r.position)))}export{v as allLegendGraphicIds,I as axisGraphicIds,G as effectiveLegendOffsetConfig,j as legendGraphicIds,S as legendResourcePolicies,x as legendResourcePolicy,$ as needsLegendAxisReflow,z as resolveGuideCollisionBlocks,C as resolveLegendAxisOffset};
1
+ import{axisGraphicIds as h}from"../../selectors/axes.js";import{localLegendPosition as m}from"../legendGroups.js";import{resolveConcreteGraphicBounds as G,unionConcreteGraphicBounds as v}from"../../grammar/schemas/graphicBounds.js";function y(e,t,s,o){const n=[],r=(i,a,l,c)=>{if(l===void 0)return;const p=c.filter(b=>e.objects[b]!==void 0),g=v(e,p,o);g!==void 0&&n.push({id:i,kind:a,position:l,bounds:g})};r("chart title","title",s?.position,["chartTitle","chartSubtitle"]);for(const[i,a]of Object.entries(t.axis??{})){const l=["line","ticks","labels","title"].map(p=>a[p]?.position).find(Boolean),c=["x","y"].includes(i)?i:["top","bottom"].includes(l)?"x":["left","right"].includes(l)?"y":void 0;if(c!==void 0)for(const[p,g]of["line","ticks","labels","title"].entries()){const b=h(c,i)[p],I=n.length;if(r(`${i}-axis ${g}`,"axis",a[g]?.position,[b]),n.length===I)continue;const z=n.at(-1);if(z.axisId=i,z.namedAxis=i!==c,["labels","title"].includes(g)){const x=e.objects[b];z.textBounds=x.items?.map(S=>G(e,S.id,o))??[z.bounds]}}}const f=T(),d=t.legend??{},u=f.find(i=>i.family==="categorical"&&d[i.kind]!==void 0),k=u!==void 0&&d.size!==void 0&&d[u.kind].target===d.size.target&&m(d[u.kind])===void 0&&m(d.size)===void 0&&(d.size.inheritPosition!==!1||d[u.kind].position===d.size.position),L=t.legendGroups??{};for(const[i,a]of Object.entries(L)){const l=f.filter(c=>d[c.kind]?.target===i&&m(d[c.kind])===void 0).flatMap(c=>c.graphicIds);r(`${i} legend group`,"legend",a.placement.position??"free",l)}for(const i of f){const a=d[i.kind];if(L[a?.target]!==void 0&&m(a)===void 0||a===void 0||k&&i.kind==="size")continue;const l=k&&i===u?[...i.graphicIds,...f.find(c=>c.kind==="size").graphicIds]:i.graphicIds;r(`${i.kind}${k&&i===u?"+size":""} legend`,"legend",m(a)??a.position,l)}return n}const O=Object.freeze(["Symbols","SymbolLines","SymbolPoints","SymbolSwatches","Labels","Title","Background"]),j=Object.freeze({colorSize:Object.freeze({semanticKind:"colorSize",family:"sampled",rematerializeOp:void 0,graphicIds:Object.freeze(["colorSizeLegendBackground","colorSizeLegendSymbols","colorSizeLegendLabels","colorSizeLegendTitle"])}),series:Object.freeze({semanticKind:"series",family:"categorical",rematerializeOp:void 0,graphicIds:Object.freeze(O.map(e=>`seriesLegend${e}`))}),color:Object.freeze({semanticKind:"color",family:"categorical",rematerializeOp:void 0,graphicIds:Object.freeze(O.map(e=>`colorLegend${e}`))}),stroke:Object.freeze({semanticKind:"stroke",family:"categorical",rematerializeOp:void 0,graphicIds:Object.freeze(O.map(e=>`strokeLegend${e}`))}),size:Object.freeze({semanticKind:"size",family:"size",rematerializeOp:"rematerializeSizeLegend",graphicIds:Object.freeze(["sizeLegendBackground","sizeLegendSymbols","sizeLegendLabels","sizeLegendTitle"])}),gradient:Object.freeze({semanticKind:"color",family:"continuous",rematerializeOp:"rematerializeGradientLegend",graphicIds:Object.freeze(["colorGradientBackground","colorGradientStrips","colorGradientTicks","colorGradientSymbols","colorGradientLabels","colorGradientTitle"])}),interval:Object.freeze({semanticKind:"color",family:"interval",rematerializeOp:"rematerializeIntervalLegend",graphicIds:Object.freeze(["colorLegendBackground","colorLegendSymbols","colorLegendLabels","colorLegendTitle"])}),strokeGradient:Object.freeze({semanticKind:"stroke",family:"continuous",rematerializeOp:"rematerializeStrokeGradientLegend",graphicIds:Object.freeze(["strokeGradientBackground","strokeGradientStrips","strokeGradientTicks","strokeGradientLabels","strokeGradientTitle"])}),strokeInterval:Object.freeze({semanticKind:"stroke",family:"interval",rematerializeOp:"rematerializeStrokeIntervalLegend",graphicIds:Object.freeze(["strokeIntervalBackground","strokeIntervalSymbols","strokeIntervalLabels","strokeIntervalTitle"])}),opacity:Object.freeze({semanticKind:"opacity",family:"continuous",rematerializeOp:"rematerializeOpacityLegend",graphicIds:Object.freeze(["opacityLegendBackground","opacityLegendSymbols","opacityLegendLabels","opacityLegendTitle"])}),strokeWidth:Object.freeze({semanticKind:"strokeWidth",family:"strokeWidth",rematerializeOp:"rematerializeStrokeWidthLegend",graphicIds:Object.freeze(["strokeWidthLegendBackground","strokeWidthLegendSymbols","strokeWidthLegendLabels","strokeWidthLegendTitle"])})});function B(e){return C(e).graphicIds}function C(e){const t=j[e];if(t===void 0)throw new Error(`Unknown legend graphic kind "${e}".`);return t}function T(){return Object.entries(j).map(([e,t])=>({kind:e,...t}))}function W(e){return[...new Set(e.flatMap(B))]}function w(e,t,s){if(t.inferredOffset!==!0||!["top","bottom"].includes(t.position)||t.layout==="legacy-bottom")return t.offset;const o=y(e.graphicSpec,e.guideConfigs,void 0,e.materializationConfigs.textMetrics).filter(r=>r.kind==="axis"&&r.position===t.position),n=t.position==="bottom"?Math.max(0,...o.map(r=>r.bounds.bottom-s.y-s.height)):Math.max(0,...o.map(r=>s.y-r.bounds.top));return t.offset+n}function K(e,t,s){if(t!=="size"||s.inheritPosition===!1)return s;const o=["series","color","stroke"].map(n=>e.guideConfigs.legend?.[n]).find(n=>n?.target===s.target);return o===void 0?s:{...s,position:o.position,offset:o.offset,inferredOffset:o.inferredOffset}}function M(e,t){return[e,t].some(o=>Object.entries(o.guideConfigs.legend??{}).some(([n,r])=>{const f=K(o,n,r);return f.inferredOffset===!0&&["top","bottom"].includes(f.position)&&f.layout!=="legacy-bottom"})||Object.values(o.guideConfigs.legendGroups??{}).some(n=>n.inferredOffset===!0&&["top","bottom"].includes(n.placement.position)))&&[e,t].some(o=>y(o.graphicSpec,o.guideConfigs,void 0,o.materializationConfigs.textMetrics).some(n=>n.kind==="axis"&&["top","bottom"].includes(n.position)))}export{W as allLegendGraphicIds,h as axisGraphicIds,K as effectiveLegendOffsetConfig,B as legendGraphicIds,T as legendResourcePolicies,C as legendResourcePolicy,M as needsLegendAxisReflow,y as resolveGuideCollisionBlocks,w as resolveLegendAxisOffset};
@@ -0,0 +1,28 @@
1
+ import { preserveGraphicPlacement } from './graphicHierarchy.js';
2
+ import { editGraphicProperties } from '../core/graphicProperties.js';
3
+
4
+ function borderCommands({x,y,width,height}) {
5
+ return [{op:'M',x,y},{op:'L',x:x+width,y},{op:'L',x:x+width,y:y+height},
6
+ {op:'L',x,y:y+height},{op:'Z'}];
7
+ }
8
+
9
+ export function materializeLegendBorder(program,id,bounds,border) {
10
+ const dashed=border.strokeDash?.length>0,type=dashed?'path':'rect';
11
+ let next=program;
12
+ if(program.graphicSpec.objects[id]?.type!==type) {
13
+ const placement=preserveGraphicPlacement(program,id);
14
+ next=next.editGraphics({target:id,remove:true}).createGraphics({id,type,...placement});
15
+ }
16
+ return editGraphicProperties(next,id,{
17
+ ...(dashed?{commands:borderCommands(bounds)}:bounds),
18
+ fill:border.background,stroke:border.color,strokeWidth:border.lineWidth,
19
+ ...(dashed?{strokeDash:border.strokeDash}:{})
20
+ });
21
+ }
22
+
23
+ export function editLegendBorderBounds(program,id,bounds) {
24
+ if(program.graphicSpec.objects[id]?.type==='path') {
25
+ return program.editGraphics({target:id,property:'commands',value:borderCommands(bounds)});
26
+ }
27
+ return editGraphicProperties(program,id,{x:bounds.x,y:bounds.y,width:bounds.width,height:bounds.height});
28
+ }
@@ -0,0 +1,76 @@
1
+ import{cloneAndFreeze}from'../core/immutable.js';
2
+ import{namespaceGraphicId}from'../selectors/graphicCapacity.js';
3
+
4
+ const clipPrefix=namespaceGraphicId('legendSampleClip','');
5
+ import{categoricalLegendSymbolId}from'../grammar/legendBlocks.js';
6
+ import{validateLegendClip}from'../grammar/legendClip.js';
7
+ import{hasRectRadius}from'../grammar/roundedRect.js';
8
+ import{resolvePrimitiveGraphicBounds}from'../grammar/schemas/graphicBounds.js';
9
+ import{translatedProperties}from'./graphicTranslation.js';
10
+
11
+ function roles(kind,config){
12
+ if(['series','color','stroke'].includes(kind))return config.symbol.layers.map(layer=>({
13
+ id:categoricalLegendSymbolId(config,layer.type),point:layer.type==='point',
14
+ type:layer.type==='line'?'line':layer.type==='point'?config.channels.includes('shape')?'collection':'circle':hasRectRadius(layer)?'collection':'rect'
15
+ }));
16
+ const prefix=kind==='gradient'?'colorGradient':`${kind}Legend`;
17
+ return [{id:`${prefix}Symbols`,type:'circle',point:true}];
18
+ }
19
+ function wrapped(owner){return owner?.children?.some(id=>id.startsWith(clipPrefix));}
20
+ export function hasLegendClipGraphics(program){
21
+ return Object.values(program.graphicSpec.objects).some(wrapped);
22
+ }
23
+ function copyGraphic(program,objects){return program._clone({graphicSpec:cloneAndFreeze({...program.graphicSpec,objects})});}
24
+ export function resetLegendClipGraphics(program){
25
+ if(!hasLegendClipGraphics(program))return program;
26
+ const objects={...program.graphicSpec.objects};
27
+ function remove(id){const object=objects[id];for(const child of object?.children??[])remove(child);delete objects[id];}
28
+ for(const[kind,config]of Object.entries(program.guideConfigs.legend??{})){
29
+ const descriptors=roles(kind,config);
30
+ if(!descriptors.some(role=>wrapped(objects[role.id])))continue;
31
+ for(const role of descriptors){
32
+ const owner=objects[role.id];if(!owner)continue;
33
+ for(const child of owner.children??[])remove(child);
34
+ objects[role.id]={type:role.type,items:[]};
35
+ }
36
+ }
37
+ return copyGraphic(program,objects);
38
+ }
39
+ function union(bounds){return bounds.reduce((a,b)=>a===undefined?b:{left:Math.min(a.left,b.left),right:Math.max(a.right,b.right),top:Math.min(a.top,b.top),bottom:Math.max(a.bottom,b.bottom)},undefined);}
40
+ function anchorY(type,p){
41
+ if(type==='circle')return p.y;
42
+ if(type==='rect')return p.y+p.height/2;
43
+ throw new Error('Path samples require their authored glyph anchor.');
44
+ }
45
+ export function applyLegendClipGraphics(program,{force=false,anchors={}}={}){
46
+ if(program.context.deferLegendClip===true&&!force)return program;
47
+ let next=program;
48
+ for(const[kind,config]of Object.entries(program.guideConfigs.legend??{})){
49
+ validateLegendClip(kind,config);if(config.clipHeight===undefined)continue;
50
+ if (["series","color","stroke"].includes(kind) && anchors[kind] === undefined && !force) continue;
51
+ const descriptors=roles(kind,config),owners=descriptors.map(role=>next.graphicSpec.objects[role.id]);
52
+ if(owners.some(wrapped)||owners.some(owner=>!owner?.items))continue;
53
+ const count=owners[0].items.length;if(count===0||owners.some(owner=>owner.items.length!==count))continue;
54
+ const objects={...next.graphicSpec.objects},primary=descriptors[0].id,children=[];
55
+ for(let index=0;index<count;index++){
56
+ const samples=owners.map((owner,j)=>({item:owner.items[index],type:owner.items[index].type??owner.type,role:descriptors[j]}));
57
+ const point=samples.find(sample=>sample.role.point),y=anchors[kind]?.[index]??anchorY(point.type,point.item.properties);
58
+ const b=union(samples.map(sample=>resolvePrimitiveGraphicBounds(sample.type,sample.item.properties)));
59
+ const x=b.left,top=y-config.clipHeight/2,id=namespaceGraphicId('legendSampleClip',namespaceGraphicId(`${primary}:${index}`,'window')),glyphs=[];
60
+ for(const sample of samples){
61
+ const glyph=namespaceGraphicId(sample.item.id,'glyph');glyphs.push(glyph);
62
+ objects[glyph]={type:sample.type,properties:translatedProperties(sample.type,sample.item.properties,-x,-top)};
63
+ }
64
+ objects[id]={type:'canvas',properties:{x,y:top,width:b.right-b.left,height:config.clipHeight},children:glyphs};children.push(id);
65
+ }
66
+ for(const descriptor of descriptors)objects[descriptor.id]={type:'collection',items:[],...(descriptor.id===primary?{children}:{})};
67
+ next=copyGraphic(next,objects);
68
+ }
69
+ return next;
70
+ }
71
+
72
+ export function restoreLegendClipContext(program, previous) {
73
+ const {deferLegendClip: _deferred, ...context} = program.context;
74
+ if (previous !== undefined) context.deferLegendClip = previous;
75
+ return program._clone({context});
76
+ }
@@ -1 +1 @@
1
- import{PLOT_LEGEND_POSITIONS as u}from"../layout/legend.js";function p(r,t){return r.guideConfigs.legendGroups?.[t]}function c(r,t){const n=p(r,t.target);if(n===void 0)return t;const o=n.placement.position,e=u.includes(o),i=e?t.direction==="horizontal"?"top":"right":o??(u.includes(t.position)?t.direction==="horizontal"?"top":"right":t.position??"right");return{...t,position:i,offset:0,align:"left",groupIntrinsic:!0,...e?{groupCorner:!0}:{},...i==="right"?{align:"center"}:{}}}export{c as intrinsicLegendConfig,p as legendGroup};
1
+ import{localLegendPosition as c,localLegendPlacement as s}from"../grammar/legendGroups.js";import"../core/vocabulary.js";import{PLOT_LEGEND_POSITIONS as l}from"../layout/legend.js";function u(n,t){return n.guideConfigs.legendGroups?.[t]}function f(n,t){const e=s(t),o=e===void 0?u(n,t.target)??(n.context.facetLegendIntrinsic===!0?{placement:{position:t.position}}:void 0):{placement:e};if(o===void 0)return t;const i=o.placement.position,r=l.includes(i),p=r?t.direction==="horizontal"?"top":"right":i??(l.includes(t.position)?t.direction==="horizontal"?"top":"right":t.position??"right");return{...t,...e===void 0?{}:{blockLocalPosition:e.position},position:p,offset:0,align:"left",groupIntrinsic:!0,...r?{groupCorner:!0}:{},...p==="right"?{align:"center"}:{}}}export{f as intrinsicLegendConfig,u as legendGroup,s as localLegendPlacement,c as localLegendPosition};
@@ -24,6 +24,6 @@ export function materializedLegendUsesScale(program, id) {
24
24
  const order = program.semanticSpec.guides.legend?.[kind]?.order;
25
25
  const linkedScale = order?.channel === undefined ? undefined
26
26
  : findLayer(program, config.target)?.encoding?.[order.channel]?.scale;
27
- return config.scale === id || config.scales?.includes(id) || linkedScale === id;
27
+ return config.colorScale === id || config.sizeScale === id || config.scale === id || config.scales?.includes(id) || linkedScale === id;
28
28
  });
29
29
  }
@@ -1,12 +1,10 @@
1
+ import { cartesianAxisOwners, axisTargetArgs } from "../selectors/axes.js";
2
+ import { axisGraphicIds } from "./guides/resources.js";
1
3
  import { isSourceOwnedText } from "../grammar/text.js";
2
4
  import { POSITION_ENCODING_CHANNELS } from "../core/vocabulary.js";
3
5
  import { materializedLegendUsesScale } from "./legends.js";
4
6
  import { buildMaterializationPlan } from "./planner.js";
5
7
 
6
- const CARTESIAN_AXES = Object.freeze([
7
- Object.freeze({ channel: "x", prefix: "X", lineObject: "xAxisLine" }),
8
- Object.freeze({ channel: "y", prefix: "Y", lineObject: "yAxisLine" })
9
- ]);
10
8
  const POLAR_AXES = Object.freeze([
11
9
  Object.freeze({ channel: "theta", prefix: "Theta" }),
12
10
  Object.freeze({ channel: "radius", prefix: "Radial" })
@@ -18,8 +16,8 @@ const GRIDS = Object.freeze([
18
16
  Object.freeze({ role: "radial", op: "rematerializeRadialGrid" })
19
17
  ]);
20
18
 
21
- export function planAxisTickGridRematerialization(program, channel) {
22
- const ticks = program.guideConfigs.axis?.[channel]?.ticks;
19
+ export function planAxisTickGridRematerialization(program, channel, axisId = channel) {
20
+ const ticks = program.guideConfigs.axis?.[axisId]?.ticks;
23
21
  const role = channel === "x" ? "vertical" : "horizontal";
24
22
  const config = program.guideConfigs.grid?.[role];
25
23
  return buildMaterializationPlan({ guides:
@@ -48,9 +46,7 @@ function usesRadialScale(program, id) {
48
46
 
49
47
  function semanticGuideUsesScale(program, id) {
50
48
  return [
51
- ...CARTESIAN_AXES.map(({ channel }) =>
52
- program.semanticSpec.guides.axis?.[channel]
53
- ),
49
+ ...cartesianAxisOwners(program).map(owner => owner.semantic),
54
50
  ...POLAR_AXES.map(({ channel }) =>
55
51
  program.semanticSpec.guides.axis?.[channel]
56
52
  ),
@@ -83,16 +79,19 @@ export function planScaleGuideRematerialization(program, id) {
83
79
  args: { target: guideConfigs.axis.parallel.axes.target }
84
80
  });
85
81
  }
86
- for (const axis of CARTESIAN_AXES) {
82
+ for (const owner of cartesianAxisOwners(program)) {
83
+ const axis = { channel: owner.channel, prefix: owner.channel.toUpperCase(),
84
+ lineObject: axisGraphicIds(owner.channel, owner.id)[0] };
85
+ const target = owner.named ? { args: axisTargetArgs(owner.id, owner.channel) } : {};
87
86
  if (
88
87
  objects[axis.lineObject] &&
89
- program.semanticSpec.guides.axis?.[axis.channel]?.scale === id
88
+ owner.scale === id
90
89
  ) {
91
- guides.push({ op: componentEditOperation(axis.prefix, "line") });
90
+ guides.push({ op: componentEditOperation(axis.prefix, "line"), ...target });
92
91
  }
93
92
  for (const component of ["ticks", "labels", "title"]) {
94
- if (guideConfigs.axis?.[axis.channel]?.[component]?.scale === id) {
95
- guides.push({ op: componentEditOperation(axis.prefix, component) });
93
+ if (guideConfigs.axis?.[owner.id]?.[component]?.scale === id) {
94
+ guides.push({ op: componentEditOperation(axis.prefix, component), ...target });
96
95
  }
97
96
  }
98
97
  }
@@ -22,8 +22,11 @@ export function mapScaleConsumerValues(values, resolvedScale, channel) {
22
22
  {
23
23
  type: resolvedScale.type,
24
24
  base: resolvedScale.base,
25
+ exponent: resolvedScale.exponent,
25
26
  constant: resolvedScale.constant,
26
27
  interpolation: resolvedScale.interpolate,
28
+ whitePoint: resolvedScale.whitePoint,
29
+ gamma: resolvedScale.gamma,
27
30
  midpoint: resolvedScale.midpoint,
28
31
  clamp: resolvedScale.clamp ?? false,
29
32
  ...(Object.hasOwn(resolvedScale, "unknown")
@@ -200,6 +200,8 @@ function resolveContinuousScale({
200
200
  }
201
201
  : {}),
202
202
  ...(isSequentialColor ? { interpolate: scale.interpolate ?? "rgb" } : {}),
203
+ ...(scale.whitePoint === undefined ? {} : {whitePoint: scale.whitePoint}),
204
+ ...(scale.gamma === undefined ? {} : {gamma: scale.gamma}),
203
205
  ...(Object.hasOwn(scale, "unknown") ? { unknown: scale.unknown } : {})
204
206
  };
205
207
  }
@@ -1,3 +1,5 @@
1
+ import { cartesianAxisOwners, axisTargetArgs } from "../selectors/axes.js";
2
+ import { axisGraphicIds } from "./guides/resources.js";
1
3
  import { applyMaterializationPlan, buildMaterializationPlan } from "./planner.js";
2
4
  import { withGuideLayoutTransaction } from "./guides/layout.js";
3
5
 
@@ -13,11 +15,17 @@ export function rematerializeTypography(program) {
13
15
  .map(layer => ({ op: "rematerializeTextMark", args: { id: layer.id } }));
14
16
  const guides = [];
15
17
  for (const [id, op] of [
16
- ["xAxisLabels", "editXAxisLabels"], ["yAxisLabels", "editYAxisLabels"],
17
- ["xAxisTitle", "editXAxisTitle"], ["yAxisTitle", "editYAxisTitle"],
18
18
  ["thetaAxisLabels", "editThetaAxisLabels"], ["radialAxisLabels", "editRadialAxisLabels"],
19
19
  ["thetaAxisTitle", "editThetaAxisTitle"], ["radialAxisTitle", "editRadialAxisTitle"]
20
20
  ]) if (exists(id, op)) guides.push({ op });
21
+ for (const owner of cartesianAxisOwners(program)) {
22
+ for (const [index, component] of [[2, "Labels"], [3, "Title"]]) {
23
+ const op = `edit${owner.channel.toUpperCase()}Axis${component}`;
24
+ if (exists(axisGraphicIds(owner.channel, owner.id)[index], op)) {
25
+ guides.push({ op, ...(owner.named ? { args: axisTargetArgs(owner.id, owner.channel) } : {}) });
26
+ }
27
+ }
28
+ }
21
29
  const parallel = program.guideConfigs.axis?.parallel?.axes;
22
30
  if (parallel !== undefined && typeof program.rematerializeParallelAxes === "function") {
23
31
  guides.push({ op: "rematerializeParallelAxes", args: { target: parallel.target } });
@@ -1,9 +1,34 @@
1
+ import { validateAuthoredFacetLegend } from "../grammar/facets/legendIntent.js";
2
+ import { AXIS_RESOURCE_CHANNELS } from "../core/vocabulary.js";
3
+ import { cartesianAxisOwners, assertCartesianAxisSideAvailable } from "../selectors/axes.js";
4
+ import { axisGraphicIds } from "../selectors/axes.js";
5
+
6
+
7
+
8
+ import {findLayer} from '../selectors/layers.js';
9
+ import {findCoordinate} from '../selectors/coordinates.js';
10
+ import {resolveColorSizeMapping,normalizeColorSizeSymbol} from '../grammar/colorSizeLegend.js';
11
+ import{validateLegendClip}from"../grammar/legendClip.js";
12
+ import {validateLegendLayerOpacity}from"../grammar/legendOpacity.js";
13
+ import { isSizeShapeChannels, resolveSizeShapeMapping } from "../grammar/legendCombinations.js";
14
+ import { describeLegendBlock } from "../grammar/legendBlocks.js";
15
+ import { localLegendPosition } from "../grammar/legendGroups.js";
16
+ import { normalizeLegendBorderDash } from "../grammar/legendBorder.js";
17
+ import { normalizeNumericDisplayLabelMap } from "../grammar/displayLabels.js";
18
+ import { isContinuousSizeScaleType } from "../grammar/scales/size.js";
19
+ import { validateGraphicItemLimit } from "../core/validation.js";
1
20
  import { LEGEND_TITLE_STYLE_KEYS, validateLegendTitleAlignment } from "../grammar/legendTitleStyle.js";
21
+ import { validateGuideDash, normalizeGuideDashMap } from "../grammar/guideDash.js";
22
+ import { validateAxisTextPlacement } from "../grammar/axisTextPlacement.js";
2
23
  import { validateAreaPaintConfig } from "../grammar/areaPaint.js";
3
24
  import { validateOptionObject, validateKeys, validatePositiveFinite, validateNonNegativeFinite, validateNonEmptyString, validateGeneratedItemLimit } from "../core/validation.js";
25
+ import { validateRegressionFitStatistics, requireRegressionStatisticsSource, regressionStatisticsRows } from "../grammar/regression/statistics.js";
26
+ import { resolveGraphicItemCapacity } from "../selectors/graphicCapacity.js";
4
27
  import { isContinuousColorScaleType } from "../grammar/scales/types.js";
28
+ import { normalizeColorInterpolationOptions } from "../grammar/scales/color.js";
29
+ import { normalizeTransformParameters } from "../grammar/scales/transformed.js";
5
30
  import { validateLegendGroupState } from "../grammar/legendGroups.js";
6
- import { validateLegendGradient } from "../grammar/legendGradient.js";
31
+ import { normalizeGradientDirection, validateLegendGradient } from "../grammar/legendGradient.js";
7
32
  import { normalizeTextMetricProfile } from "../core/textMetricProfile.js";
8
33
  import { isPlainObject } from "../core/immutable.js";
9
34
  import { validateUserId } from "../core/identifiers.js";
@@ -136,6 +161,12 @@ export function validateProgramState(program, path) {
136
161
  }
137
162
  resources.set(referenceKind, entries);
138
163
  for (const resource of entries.values()) {
164
+ if (kind === "scale") {
165
+ normalizeColorInterpolationOptions(resource.interpolate, resource);
166
+ if (resource.type === "pow") normalizeTransformParameters("pow", {
167
+ ...(resource.exponent === undefined ? {} : {exponent:resource.exponent})
168
+ });
169
+ }
139
170
  for (const [key, value] of Object.entries(resource)) {
140
171
  if (key !== "id") validateSemanticProperties(program, value, `${kind}[${resource.id}].${key}`);
141
172
  }
@@ -147,9 +178,121 @@ export function validateProgramState(program, path) {
147
178
  }
148
179
  for (const key of ["resolvedScales", "materializationConfigs", "context"]) requireObject(program[key], `${path}.${key}`);
149
180
  for (const key of ["marks", "guides"]) requireObject(program.materializationConfigs[key], `${path}.materializationConfigs.${key}`);
181
+ if (program.compositionSpec?.type === "facet" && isPlainObject(program.compositionSpec.facet.guides.legend)) {
182
+ validateAuthoredFacetLegend(program, program.compositionSpec.facet.guides.legend);
183
+ }
150
184
  if (program.materializationConfigs.textMetrics !== undefined) normalizeTextMetricProfile(program.materializationConfigs.textMetrics);
185
+ for (const config of [
186
+ ...Object.values(program.guideConfigs.axis ?? {}).map(axis => axis.ticks),
187
+ ...["horizontal", "vertical"].map(direction => program.guideConfigs.grid?.[direction])
188
+ ]) {
189
+ if (config?.strokeDash !== undefined) validateGuideDash(config.strokeDash);
190
+ if (config?.strokeDashMap !== undefined) normalizeGuideDashMap(config.strokeDashMap, { reset: false });
191
+ }
192
+ for (const owner of cartesianAxisOwners(program)) {
193
+ const channel = owner.id;
194
+ const axis = owner.config;
195
+ if (axis?.frame !== undefined) {
196
+ exactKeys(axis.frame, ["offset"], `${path}.guides.axis.${channel}.frame`);
197
+ validateNonNegativeFinite(axis.frame.offset, "Axis frame offset");
198
+ }
199
+ if (axis?.labels !== undefined) validateAxisTextPlacement(axis.labels);
200
+ if (axis?.title !== undefined) validateAxisTextPlacement(axis.title, { title: true });
201
+ }
202
+ for (const id of Object.keys(program.guideConfigs.axis ?? {})) {
203
+ if (!AXIS_RESOURCE_CHANNELS.includes(id) && !Object.hasOwn(program.semanticSpec.guides.axis ?? {}, id)) {
204
+ throw invalidSnapshot("Named axis configs require their semantic owner.", path);
205
+ }
206
+ }
207
+ for (const [id, semanticAxis] of Object.entries(program.semanticSpec.guides.axis ?? {})) {
208
+ if (AXIS_RESOURCE_CHANNELS.includes(id)) continue;
209
+ validateUserId(id, "Axis id");
210
+ validateKeys(semanticAxis, ["channel", "position", "scale", "coordinate", "title"], "Stored named axis");
211
+ if (!["x", "y"].includes(semanticAxis.channel) ||
212
+ !(semanticAxis.channel === "x" ? ["bottom", "top"] : ["left", "right"]).includes(semanticAxis.position)) {
213
+ throw invalidSnapshot("Named axes require a Cartesian channel and compatible physical side.", path);
214
+ }
215
+ validateUserId(semanticAxis.scale, "Axis scale id");
216
+ validateUserId(semanticAxis.coordinate, "Axis coordinate id");
217
+ if (resources.get("coordinate").get(semanticAxis.coordinate)?.type !== "cartesian") {
218
+ throw invalidSnapshot("Named Cartesian axes require a Cartesian coordinate.", path);
219
+ }
220
+ const config = program.guideConfigs.axis?.[id];
221
+ requireObject(config, `${path}.guides.axis.${id}`);
222
+ validateKeys(config, ["binding", "frame", "line", "ticks", "labels", "title"], "Stored named axis config");
223
+ const components = ["line", "ticks", "labels", "title"];
224
+ if (!components.some(component => config[component] !== undefined)) {
225
+ throw invalidSnapshot("Named axes require at least one retained component.", path);
226
+ }
227
+ for (const [index, component] of components.entries()) {
228
+ const stored = config[component];
229
+ const graphic = program.graphicSpec.objects[axisGraphicIds(semanticAxis.channel, id)[index]];
230
+ if ((program.compositionSpec === undefined && (stored !== undefined) !== (graphic !== undefined)) ||
231
+ (stored !== undefined && (stored.position !== semanticAxis.position ||
232
+ stored.scale !== undefined && stored.scale !== semanticAxis.scale))) {
233
+ throw invalidSnapshot("Named axis components must share their owner's side and scale.", path);
234
+ }
235
+ }
236
+ if ((semanticAxis.title !== undefined) !== (config.title !== undefined)) {
237
+ throw invalidSnapshot("Named axis title meaning requires its retained title component.", path);
238
+ }
239
+ assertCartesianAxisSideAvailable(program, { id, channel: semanticAxis.channel,
240
+ coordinate: semanticAxis.coordinate, position: semanticAxis.position });
241
+ }
151
242
  if (program.guideConfigs.legendGroups !== undefined) validateLegendGroupState(program.guideConfigs.legendGroups);
152
243
  for (const [kind, config] of Object.entries(program.guideConfigs.legend ?? {})) {
244
+ validateLegendClip(kind,config);
245
+ if (localLegendPosition(config) !== undefined) {
246
+ const descriptor = describeLegendBlock(program, kind, config);
247
+ for (const [key, override] of Object.entries(config.blockOverrides)) {
248
+ if (override.position !== undefined && key !== descriptor.key) throw invalidSnapshot("Local position requires its existing block owner.", path);
249
+ }
250
+ }
251
+ for(const layer of config.symbol?.layers??[])validateLegendLayerOpacity(layer.opacity);
252
+ if(kind==='colorSize') {
253
+ if(config.combine!=='color-size')throw invalidSnapshot('Color-size legend requires its combined owner.',path);
254
+ const layer=findLayer(program,config.target);
255
+ const mapping=resolveColorSizeMapping({layer,scales:program.resolvedScales,pointConfig:program.markConfigs[layer?.id],coordinate:findCoordinate(program,layer?.coordinate)});
256
+ for(const key of ['target','field','fieldType','colorScale','sizeScale','domain'])if(JSON.stringify(config[key])!==JSON.stringify(mapping[key]))throw invalidSnapshot('Stale color-size mapping.',path);
257
+ const guide=semantic.guides.legend?.colorSize;
258
+ if(guide?.colorScale!==mapping.colorScale||guide?.sizeScale!==mapping.sizeScale||guide.title!==config.title)throw invalidSnapshot('Stale color-size semantic owner.',path);
259
+ requireObject(config.symbol,path);normalizeColorSizeSymbol(config.symbol);
260
+ if(config.symbol.type!=='point'||!Object.hasOwn(config.symbol,'strokeWidth')||!Object.hasOwn(config.symbol,'opacity'))throw invalidSnapshot('Color-size symbol requires point type.',path);
261
+ const sample=config.sampling;requireObject(sample,path);
262
+ exactKeys(sample,sample.mode==='values'?['mode','count','values']:['mode','count'],path);
263
+ if(!['auto','values'].includes(sample.mode)||!Number.isInteger(sample.count)||sample.count<2)throw invalidSnapshot('Invalid color-size samples.',path);
264
+ validateGeneratedItemLimit(sample.count,'Color-size samples');
265
+ if(sample.mode==='values'){
266
+ requireArray(sample.values,path);
267
+ if(!sample.values.length||sample.values.length>100||sample.values.some((v,i)=>!Number.isFinite(v)||i>0&&v<=sample.values[i-1]||v<Math.min(...mapping.domain)||v>Math.max(...mapping.domain)||[mapping.colorScale,mapping.sizeScale].some(id=>program.resolvedScales[id].type==='log'&&v<=0)))throw invalidSnapshot('Invalid exact color-size samples.',path);
268
+ }
269
+ } else if (config.combine !== undefined) {
270
+ if (config.combine !== "size-shape" || kind !== "series" || !isSizeShapeChannels(config.channels)) throw invalidSnapshot("Invalid combined legend owner.", path);
271
+ const layer = semantic.layers.find(layer=>layer.id===config.target);
272
+ const definition = resolveSizeShapeMapping({layer,scales:program.resolvedScales,
273
+ order:semantic.guides.legend?.series?.order,rows:resolveDatasetReference(program,layer.data).values});
274
+ if (!Array.isArray(config.sizeAreas) || config.sizeAreas.length !== definition.sizeAreas.length ||
275
+ config.sizeAreas.some((area,index)=>!Object.is(area,definition.sizeAreas[index]))) throw invalidSnapshot("Stale combined legend size mapping.", path);
276
+ }
277
+ if (config.gradientDirection !== undefined) {
278
+ if (!["gradient", "strokeGradient"].includes(kind)) throw invalidSnapshot("Gradient direction requires a gradient owner.", path);
279
+ normalizeGradientDirection(config.gradientDirection, "Stored gradient direction");
280
+ }
281
+ if (["gradient", "strokeGradient"].includes(kind) && config.representation !== "symbols" && !["top", "bottom"].includes(config.titlePosition)) {
282
+ throw invalidSnapshot("Gradient titlePosition requires top or bottom.", path);
283
+ }
284
+ if (config.border && Object.hasOwn(config.border, "strokeDash")) {
285
+ normalizeLegendBorderDash(config.border.strokeDash, "Stored legend border strokeDash");
286
+ }
287
+ if (["size","colorSize"].includes(kind) && config.blockOverrides !== undefined) {
288
+ for (const [key, override] of Object.entries(config.blockOverrides)) {
289
+ if (override.labelMap === undefined) continue;
290
+ if (key !== (kind==='colorSize'?'["color","size"]':'["size"]') || !isContinuousSizeScaleType(program.resolvedScales[kind==='colorSize'?config.sizeScale:config.scale]?.type)) {
291
+ throw invalidSnapshot("Numeric display labels require a continuous size legend block.", path);
292
+ }
293
+ normalizeNumericDisplayLabelMap(override.labelMap, "Stored size legend labelMap");
294
+ }
295
+ }
153
296
  if (config.titleStyle !== undefined) {
154
297
  validateOptionObject(config.titleStyle, LEGEND_TITLE_STYLE_KEYS, "Stored legend title style");
155
298
  validateLegendTitleAlignment(config.titleStyle, config.titlePosition);
@@ -186,6 +329,27 @@ export function validateProgramState(program, path) {
186
329
  } else if (["sampling", "symbol", "columns", "direction", "layout"].some(key => config[key] !== undefined)) throw invalidSnapshot("Gradient color legend cannot store symbol representation settings.", path);
187
330
  }
188
331
 
332
+ const regressionFits = program.materializationConfigs.regressionFits ?? {};
333
+ requireObject(regressionFits, `${path}.materializationConfigs.regressionFits`);
334
+ for (const [id, cache] of Object.entries(regressionFits)) {
335
+ const dataset = resources.get("data").get(id);
336
+ validateRegressionFitStatistics(cache, dataset, resources.get("data").get(dataset?.source));
337
+ }
338
+ for (const dataset of semantic.datasets) {
339
+ if (dataset.transform?.[0]?.type !== "regressionStatistics") continue;
340
+ const fit = resources.get("data").get(dataset.source);
341
+ requireRegressionStatisticsSource(fit);
342
+ if (regressionFits[fit.id] === undefined) throw invalidSnapshot("Regression statistics requires its linear fit cache.", path);
343
+ if (dataset.values !== undefined) {
344
+ const expected = regressionStatisticsRows(regressionFits[fit.id], fit);
345
+ if (dataset.values.length !== expected.length || dataset.values.some((row, index) =>
346
+ !isPlainObject(row) || Object.keys(row).length !== Object.keys(expected[index]).length ||
347
+ Object.entries(expected[index]).some(([key, value]) => !Object.hasOwn(row, key) || !Object.is(row[key], value)))) {
348
+ throw invalidSnapshot("Regression statistics values do not match their linear fit cache.", path);
349
+ }
350
+ }
351
+ }
352
+
189
353
  const selections = program.materializationConfigs.selections ?? {};
190
354
  requireObject(selections, `${path}.materializationConfigs.selections`);
191
355
  for (const [id, selection] of Object.entries(selections)) {
@@ -205,6 +369,10 @@ export function validateProgramState(program, path) {
205
369
  requireObject(scale, `${path}.resolvedScales.${id}`);
206
370
  requireArray(scale.domain, `${path}.resolvedScales.${id}.domain`);
207
371
  requireArray(scale.range, `${path}.resolvedScales.${id}.range`);
372
+ normalizeColorInterpolationOptions(scale.interpolate, scale);
373
+ if (scale.type === "pow") normalizeTransformParameters("pow", {
374
+ ...(scale.exponent === undefined ? {} : {exponent:scale.exponent})
375
+ });
208
376
  if (!resources.get("scale").has(id) || scale.type !== resources.get("scale").get(id).type) {
209
377
  throw invalidSnapshot(`Resolved scale "${id}" has no matching semantic owner/type.`, path);
210
378
  }
@@ -229,4 +397,9 @@ export function validateProgramState(program, path) {
229
397
  for (const resource of resources.get(kind).values()) visit(resource);
230
398
  }
231
399
  validateGraphic(program.graphicSpec, `${path}.graphicSpec`);
400
+ for (const [id, graphic] of Object.entries(program.graphicSpec.objects)) {
401
+ if (graphic.items !== undefined) validateGraphicItemLimit(graphic.items.length, resolveGraphicItemCapacity(program, {
402
+ id, label: "Graphic collection length"
403
+ }));
404
+ }
232
405
  }
@@ -96,7 +96,7 @@ function migrateStateSchemas(state) {
96
96
  sourceSchema,
97
97
  dataset.transform?.[0] ?? {},
98
98
  dataset.values,
99
- { sourceId: dataset.source, ownerId: dataset.id }
99
+ { sourceId: dataset.source, ownerId: dataset.id, sourceTransform: byId.get(dataset.source)?.transform?.[0] }
100
100
  );
101
101
  }
102
102
  active.delete(id);
@@ -1,3 +1,5 @@
1
+ import { MAX_RASTER_DIMENSION, MAX_RASTER_PIXELS } from "./limits.js";
2
+ import { prepareIsolatedGlyphs, drawIsolatedGlyph } from "./isolated.js";
1
3
  import { resolveCanvasSolidPaint } from "./fill.js";
2
4
  import { annotateError } from "../../core/diagnostics.js";
3
5
  import { drawCircleGraphic } from "./circle.js";
@@ -40,8 +42,6 @@ const DRAWING_CONTEXT_METHODS = Object.freeze([
40
42
  "rotate",
41
43
  "fillText"
42
44
  ]);
43
- const MAX_RASTER_DIMENSION = 32_767;
44
- const MAX_RASTER_PIXELS = 16_777_216;
45
45
 
46
46
  function requireDrawingContext(context) {
47
47
  if (context === null || typeof context !== "object") {
@@ -144,9 +144,10 @@ export function resolveGraphicRenderTarget(graphicSpec) {
144
144
  });
145
145
  }
146
146
 
147
- export function drawResolvedGraphicSpec(target, context) {
147
+ export function drawResolvedGraphicSpec(target, context, options = {}) {
148
148
  requireDrawingContext(context);
149
149
  const { graphicSpec, canvasId, canvas, width, height } = target;
150
+ const isolated = options.isolated ?? prepareIsolatedGlyphs(target, context, options);
150
151
 
151
152
  fillCanvasBackground(context, canvasId, canvas, width, height);
152
153
 
@@ -162,14 +163,14 @@ export function drawResolvedGraphicSpec(target, context) {
162
163
  return;
163
164
  }
164
165
  if (!Array.isArray(object.items)) {
165
- drawConcreteGraphic(context, id, object);
166
+ drawConcreteGraphic(context, id, object, isolated);
166
167
  }
167
168
  },
168
169
  item({ id, object, owner }) {
169
170
  drawConcreteGraphic(context, id, {
170
171
  ...object,
171
172
  type: object.type ?? owner.type
172
- });
173
+ }, isolated);
173
174
  },
174
175
  exit({ id, object }) {
175
176
  if (
@@ -182,7 +183,7 @@ export function drawResolvedGraphicSpec(target, context) {
182
183
  });
183
184
  }
184
185
 
185
- export function render(program, context, { pixelRatio = 1 } = {}) {
186
+ export function renderCanvasProgram(program, context, { pixelRatio = 1 } = {}, createCanvas) {
186
187
  const graphicSpec = requireProgramGraphicSpec(program);
187
188
 
188
189
  requireCanvasContext(context);
@@ -220,6 +221,7 @@ export function render(program, context, { pixelRatio = 1 } = {}) {
220
221
  ), { code: "resource-limit", operation: "render", optionPath: "physicalPixels", limit: MAX_RASTER_PIXELS, actual: physicalWidth * physicalHeight });
221
222
  }
222
223
  preflightCanvasGraphicSpec(target, pixelRatio);
224
+ const isolated = prepareIsolatedGlyphs(target, context, {pixelRatio, createCanvas});
223
225
 
224
226
  context.canvas.width = physicalWidth;
225
227
  context.canvas.height = physicalHeight;
@@ -235,16 +237,20 @@ export function render(program, context, { pixelRatio = 1 } = {}) {
235
237
  try {
236
238
  context.scale(pixelRatio, pixelRatio);
237
239
  context.clearRect(0, 0, width, height);
238
- drawResolvedGraphicSpec(target, context);
240
+ drawResolvedGraphicSpec(target, context, {isolated, pixelRatio, createCanvas});
239
241
  } finally {
240
242
  context.restore();
241
243
  }
242
244
  }
243
245
 
244
- function drawConcreteGraphic(context, id, graphic) {
246
+ export function render(program, context, options) {
247
+ return renderCanvasProgram(program, context, options);
248
+ }
249
+
250
+ function drawConcreteGraphic(context, id, graphic, isolated) {
245
251
  if (graphic.type === "collection") {
246
252
  for (const item of graphic.items ?? []) {
247
- drawConcreteGraphic(context, item.id ?? id, item);
253
+ drawConcreteGraphic(context, item.id ?? id, item, isolated);
248
254
  }
249
255
  return;
250
256
  }
@@ -252,5 +258,9 @@ function drawConcreteGraphic(context, id, graphic) {
252
258
  if (draw === undefined) {
253
259
  throw new Error(`Canvas renderer does not support "${graphic.type}" yet.`);
254
260
  }
255
- draw(context, id, graphic);
261
+ if (["normal","difference"].includes(graphic.properties?.blend)) {
262
+ drawIsolatedGlyph(context, id, graphic, draw, isolated);
263
+ } else {
264
+ draw(context, id, graphic);
265
+ }
256
266
  }