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.
- package/CHANGELOG.md +50 -0
- package/README.md +6 -2
- package/knowledge/action-cards.json +1 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/package.json +1 -1
- package/src/actions/canvas/fitting.js +6 -2
- package/src/actions/charts/heatmap.js +4 -3
- package/src/actions/data/derived.js +11 -1
- package/src/actions/data/edit.js +2 -0
- package/src/actions/data/index.js +4 -0
- package/src/actions/data/regression.js +24 -16
- package/src/actions/data/regressionStatistics.js +73 -0
- package/src/actions/data/shared.js +5 -2
- package/src/actions/distributions/revision.js +24 -18
- package/src/actions/encodings/remove.js +11 -6
- package/src/actions/encodings/shared.js +26 -24
- package/src/actions/facets/actions.js +18 -7
- package/src/actions/facets/guides.js +2 -1
- package/src/actions/guides/axes/axes.js +30 -11
- package/src/actions/guides/axes/axis.js +58 -8
- package/src/actions/guides/axes/components.js +13 -13
- package/src/actions/guides/axes/edit.js +36 -24
- package/src/actions/guides/axes/labels.js +37 -27
- package/src/actions/guides/axes/lines.js +22 -20
- package/src/actions/guides/axes/remove.js +10 -6
- package/src/actions/guides/axes/tickGroups.js +8 -7
- package/src/actions/guides/axes/ticks.js +31 -22
- package/src/actions/guides/axes/titles.js +53 -60
- package/src/actions/guides/facadeAxes.js +33 -24
- package/src/actions/guides/facadeLegend.js +3 -3
- package/src/actions/guides/grids/grid.js +1 -1
- package/src/actions/guides/grids/resolve.js +48 -51
- package/src/actions/guides/legends/blocks.js +43 -19
- package/src/actions/guides/legends/categorical/actions.js +30 -7
- package/src/actions/guides/legends/categorical/components.js +3 -10
- package/src/actions/guides/legends/categorical/index.js +2 -0
- package/src/actions/guides/legends/categorical/layout.js +9 -13
- package/src/actions/guides/legends/categorical/options.js +8 -2
- package/src/actions/guides/legends/categorical/recipes.js +7 -3
- package/src/actions/guides/legends/categorical/resolve.js +9 -2
- package/src/actions/guides/legends/categorical/symbols.js +22 -8
- package/src/actions/guides/legends/colorSize.js +110 -0
- package/src/actions/guides/legends/combine.js +37 -0
- package/src/actions/guides/legends/continuous/colorSymbols.js +1 -1
- package/src/actions/guides/legends/continuous/common.js +1 -1
- package/src/actions/guides/legends/continuous/gradient.js +27 -13
- package/src/actions/guides/legends/continuous/opacity.js +16 -11
- package/src/actions/guides/legends/creation.js +2 -0
- package/src/actions/guides/legends/edit.js +1 -1
- package/src/actions/guides/legends/focused.js +6 -5
- package/src/actions/guides/legends/lane.js +1 -1
- package/src/actions/guides/legends/lifecycle.js +2 -1
- package/src/actions/guides/legends/remove.js +6 -3
- package/src/actions/guides/legends/size.js +16 -6
- package/src/actions/guides/legends/strokeWidth.js +2 -0
- package/src/actions/guides/legends/target.js +2 -79
- package/src/actions/guides/legends/transition.js +24 -5
- package/src/actions/guides/tickValues.js +8 -5
- package/src/actions/marks/bar/edit.js +23 -12
- package/src/actions/marks/bar/materialize.js +6 -1
- package/src/actions/marks/point/materialize.js +5 -1
- package/src/actions/marks/remove.js +10 -10
- package/src/actions/primitives/createGraphics.js +3 -2
- package/src/actions/primitives/editGraphics.js +15 -11
- package/src/actions/primitives/graphicProperties.js +1 -6
- package/src/actions/primitives/semanticAction.js +27 -1
- package/src/actions/primitives/semanticValidation/guide.js +11 -1
- package/src/actions/primitives/semanticValidation/layer.js +4 -0
- package/src/actions/regression/edit.js +15 -3
- package/src/actions/scales/channels.js +2 -2
- package/src/actions/scales/create.js +3 -3
- package/src/actions/scales/definitions.js +1 -1
- package/src/actions/scales/edit.js +1 -1
- package/src/actions/scales/editPolicy.js +3 -0
- package/src/actions/scales/patch.js +1 -1
- package/src/actions/scales/quantitativeColor.js +8 -3
- package/src/actions/theme/reconcile.js +50 -23
- package/src/core/compositionState.js +2 -2
- package/src/core/graphicProperties.js +1 -0
- package/src/core/programState.js +4 -0
- package/src/core/resourceReferences.js +2 -2
- package/src/core/validation.js +9 -0
- package/src/core/vocabulary.js +3 -0
- package/src/grammar/axisTextPlacement.js +1 -0
- package/src/grammar/colorSizeLegend.js +39 -0
- package/src/grammar/computed.js +23 -1
- package/src/grammar/datasetSchema.js +10 -2
- package/src/grammar/displayLabels.js +8 -0
- package/src/grammar/facets/guides.js +54 -31
- package/src/grammar/facets/legendIntent.js +65 -0
- package/src/grammar/guideDash.js +1 -0
- package/src/grammar/legendBlocks.js +1 -0
- package/src/grammar/legendBorder.js +9 -0
- package/src/grammar/legendClip.js +19 -0
- package/src/grammar/legendCombinations.js +45 -0
- package/src/grammar/legendGradient.js +9 -0
- package/src/grammar/legendGroups.js +1 -1
- package/src/grammar/legendOpacity.js +6 -0
- package/src/grammar/pointShapes.js +16 -2
- package/src/grammar/regression/derive.js +9 -1
- package/src/grammar/regression/models.js +60 -0
- package/src/grammar/regression/parameters.js +9 -1
- package/src/grammar/regression/statistics.js +64 -0
- package/src/grammar/scales/color.js +1 -1
- package/src/grammar/scales/colorConsumers.js +1 -1
- package/src/grammar/scales/definition.js +1 -1
- package/src/grammar/scales/types.js +1 -1
- package/src/grammar/schemas/concreteGraphic.js +3 -0
- package/src/grammar/schemas/graphic.js +3 -0
- package/src/grammar/schemas/graphicBounds.js +31 -7
- package/src/grammar/schemas/semanticPath.js +17 -2
- package/src/grammar/summary.js +1 -1
- package/src/grammar/timeUnit.js +6 -4
- package/src/grammar/transformTopology.js +1 -0
- package/src/grammar/transforms.js +11 -1
- package/src/inspectionDescriptors.js +1 -1
- package/src/layout/guideCollisions.js +9 -1
- package/src/layout/legendGroup.js +1 -1
- package/src/materialization/compositionSnapshot.js +2 -7
- package/src/materialization/coordinateBounds.js +17 -4
- package/src/materialization/facetGuides/edit.js +30 -0
- package/src/materialization/facetGuides/layout.js +1 -1
- package/src/materialization/facetGuides/owner.js +125 -0
- package/src/materialization/facetGuides/ownerLayout.js +71 -0
- package/src/materialization/facetGuides/placement.js +52 -6
- package/src/materialization/facetGuides/preparation.js +7 -0
- package/src/materialization/facets.js +1 -1
- package/src/materialization/graphicHierarchy.js +5 -1
- package/src/materialization/graphicTranslation.js +73 -0
- package/src/materialization/guides/layout.js +1 -1
- package/src/materialization/guides/resources.js +1 -1
- package/src/materialization/legendBorder.js +28 -0
- package/src/materialization/legendClip.js +76 -0
- package/src/materialization/legendGroups.js +1 -1
- package/src/materialization/legends.js +1 -1
- package/src/materialization/scaleGuideDependencies.js +13 -14
- package/src/materialization/scales/map.js +3 -0
- package/src/materialization/scales/resolve.js +2 -0
- package/src/materialization/typography.js +10 -2
- package/src/persistence/validation.js +174 -1
- package/src/persistence.js +1 -1
- package/src/renderers/canvas/index.js +20 -10
- package/src/renderers/canvas/isolated.js +166 -0
- package/src/renderers/canvas/limits.js +3 -0
- package/src/renderers/canvas/rect.js +1 -1
- package/src/renderers/pdf.js +2 -2
- package/src/renderers/png.js +2 -2
- package/src/renderers/svg.js +10 -3
- package/src/selectors/axes.js +1 -0
- package/src/selectors/graphicCapacity.js +38 -0
- package/src/version.js +1 -1
- package/types/basic.d.ts +2 -0
- package/types/index.d.ts +5 -0
- package/types/program.d.ts +206 -61
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
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
|
|
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?.[
|
|
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
|
-
...
|
|
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
|
|
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
|
-
|
|
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?.[
|
|
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
|
}
|
package/src/persistence.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
}
|