2323 * This replaces the G6 plugin's recompute-per-draw churn (the patched
2424 * updateBubbleSetsPath path coalescing, issue #7195) with an owned cache.
2525 */
26- import { DEFAULTS } from " ../config.js" ;
26+ import { DEFAULTS } from ' ../config.js' ;
2727import {
2828 nodeViewportRect ,
2929 computeOutlinePoints ,
@@ -32,10 +32,10 @@ import {
3232 idsKey ,
3333 positionsChecksum ,
3434 styleKey ,
35- } from " ./bubble_geometry.js" ;
35+ } from ' ./bubble_geometry.js' ;
3636
37- const LAYER_NAME = " bubbleSets" ;
38- const LABEL_LAYER_NAME = " bubbleSetsLabels" ;
37+ const LAYER_NAME = ' bubbleSets' ;
38+ const LABEL_LAYER_NAME = ' bubbleSetsLabels' ;
3939const OUTLINE_STROKE_WIDTH = 2 ;
4040// Extra screen-px gap (beyond half the font box) between the outline and a
4141// label drawn with labelCloseToPath: false.
@@ -65,23 +65,23 @@ class BubbleSetLayer {
6565 // createCanvasContext returns the Sigma instance (fluent API); the canvas
6666 // and 2d context land in sigma.elements / sigma.canvasContexts.
6767 sigma . createCanvasContext ( LAYER_NAME , {
68- afterLayer : " nodes" ,
69- style : { pointerEvents : " none" } ,
68+ afterLayer : ' nodes' ,
69+ style : { pointerEvents : ' none' } ,
7070 } ) ;
7171 this . canvas = sigma . getCanvases ( ) [ LAYER_NAME ] ;
72- this . ctx = this . canvas . getContext ( "2d" ) ;
72+ this . ctx = this . canvas . getContext ( '2d' ) ;
7373
7474 // Group labels paint on their own canvas stacked above sigma's node-label
7575 // layer so they always win the z-order contest against member labels.
7676 sigma . createCanvasContext ( LABEL_LAYER_NAME , {
77- afterLayer : " labels" ,
78- style : { pointerEvents : " none" } ,
77+ afterLayer : ' labels' ,
78+ style : { pointerEvents : ' none' } ,
7979 } ) ;
8080 this . labelCanvas = sigma . getCanvases ( ) [ LABEL_LAYER_NAME ] ;
81- this . labelCtx = this . labelCanvas . getContext ( "2d" ) ;
81+ this . labelCtx = this . labelCanvas . getContext ( '2d' ) ;
8282
8383 this . renderHandler = ( ) => this . scheduleRedraw ( ) ;
84- sigma . on ( " afterRender" , this . renderHandler ) ;
84+ sigma . on ( ' afterRender' , this . renderHandler ) ;
8585 }
8686
8787 /**
@@ -110,7 +110,7 @@ class BubbleSetLayer {
110110 if ( this . killed ) return ;
111111 this . killed = true ;
112112 if ( this . rafHandle !== null ) cancelAnimationFrame ( this . rafHandle ) ;
113- this . adapter . sigma . off ( " afterRender" , this . renderHandler ) ;
113+ this . adapter . sigma . off ( ' afterRender' , this . renderHandler ) ;
114114 // sigma.kill() may or may not remove custom layer canvases; remove() on
115115 // an already-detached node is a no-op, so drop ours defensively.
116116 this . canvas ?. remove ( ) ;
@@ -130,7 +130,7 @@ class BubbleSetLayer {
130130 #groupState( group ) {
131131 let state = this . groups . get ( group ) ;
132132 if ( ! state ) {
133- state = { members : new Map ( ) , avoidMembers : [ ] , opts : { } , membersKey : "" , avoidKey : "" } ;
133+ state = { members : new Map ( ) , avoidMembers : [ ] , opts : { } , membersKey : '' , avoidKey : '' } ;
134134 this . groups . set ( group , state ) ;
135135 }
136136 return state ;
@@ -182,7 +182,7 @@ class BubbleSetLayer {
182182 // re-renders on hover etc. without clearing custom layers).
183183 const signature =
184184 `${ width } x${ height } x${ dpr } |${ camera . x } ,${ camera . y } ,${ camera . ratio } ,${ camera . angle } ` +
185- `|${ active . map ( ( [ g ] ) => g ) . join ( "," ) } ` ;
185+ `|${ active . map ( ( [ g ] ) => g ) . join ( ',' ) } ` ;
186186 if ( ! outlinesChanged && signature === this . lastPaintSignature ) return ;
187187 this . lastPaintSignature = signature ;
188188
@@ -195,9 +195,7 @@ class BubbleSetLayer {
195195 #drawOutlines( active , sigma ) {
196196 for ( const [ group , state ] of active ) {
197197 // Reprojection assumes camera.angle === 0 (the app never rotates the camera).
198- const points = this . outlines
199- . get ( group )
200- . graphPoints . map ( ( p ) => sigma . graphToViewport ( p ) ) ;
198+ const points = this . outlines . get ( group ) . graphPoints . map ( ( p ) => sigma . graphToViewport ( p ) ) ;
201199 const defaults = this . cache . DEFAULTS . BUBBLE_GROUP_STYLE [ group ] ?? { } ;
202200 const drawn = this . #drawGroup( this . ctx , points , state , defaults ) ;
203201 // Labels paint on the top canvas (afterLayer: "labels") so they read
@@ -296,6 +294,16 @@ class BubbleSetLayer {
296294 canvas . width = width * dpr ;
297295 canvas . height = height * dpr ;
298296 }
297+ // Own the CSS display size too: sigma.createCanvasContext only sets
298+ // position:absolute, and sigma.resize() (the sole writer of element.style
299+ // width/height) runs once at construction — before this canvas exists — and
300+ // early-returns on unchanged dimensions. Left unset, the canvas displays at
301+ // its backing-store size (width*dpr CSS px), which is dpr* too large on a
302+ // >1 DPR display, so the group lands in the wrong place until a panel toggle
303+ // forces a real sigma resize. Setting it here keeps the overlay 1:1 with the
304+ // WebGL layers regardless of sigma's resize timing or the display's DPR.
305+ if ( canvas . style . width !== `${ width } px` ) canvas . style . width = `${ width } px` ;
306+ if ( canvas . style . height !== `${ height } px` ) canvas . style . height = `${ height } px` ;
299307 ctx . setTransform ( dpr , 0 , 0 , dpr , 0 , 0 ) ;
300308 ctx . clearRect ( 0 , 0 , width , height ) ;
301309 }
@@ -402,7 +410,7 @@ class BubbleSetLayer {
402410 // Reference-viewport → graph space (undo the ratio-1 mapping, then the
403411 // camera): the cache holds zoom-independent graph coords.
404412 return refPoints . map ( ( p ) =>
405- sigma . viewportToGraph ( { x : cx + ( p . x - cx ) / r , y : cy + ( p . y - cy ) / r } ) ,
413+ sigma . viewportToGraph ( { x : cx + ( p . x - cx ) / r , y : cy + ( p . y - cy ) / r } )
406414 ) ;
407415 }
408416
@@ -422,10 +430,10 @@ class BubbleSetLayer {
422430 ctx . save ( ) ;
423431 try {
424432 ctx . globalAlpha = opts . fillOpacity ?? defaults . fillOpacity ?? 0.25 ;
425- ctx . fillStyle = opts . fill ?? defaults . fill ?? " #403C53" ;
433+ ctx . fillStyle = opts . fill ?? defaults . fill ?? ' #403C53' ;
426434 ctx . fill ( path ) ;
427435 ctx . globalAlpha = opts . strokeOpacity ?? defaults . strokeOpacity ?? 1 ;
428- ctx . strokeStyle = opts . stroke ?? defaults . stroke ?? " #403C53" ;
436+ ctx . strokeStyle = opts . stroke ?? defaults . stroke ?? ' #403C53' ;
429437 ctx . lineWidth = OUTLINE_STROKE_WIDTH ;
430438 ctx . stroke ( path ) ;
431439 ctx . globalAlpha = 1 ;
@@ -442,9 +450,9 @@ class BubbleSetLayer {
442450 * outline tangent. labelOffsetX/Y stay additive in screen space.
443451 */
444452 #drawLabel( ctx , points , opts , defaults ) {
445- const text = opts . labelText ?? defaults . labelText ?? "" ;
453+ const text = opts . labelText ?? defaults . labelText ?? '' ;
446454 if ( ! text ) return ;
447- const placement = opts . labelPlacement ?? defaults . labelPlacement ?? " bottom" ;
455+ const placement = opts . labelPlacement ?? defaults . labelPlacement ?? ' bottom' ;
448456 const closeToPath = opts . labelCloseToPath ?? defaults . labelCloseToPath ?? true ;
449457 const autoRotate = opts . labelAutoRotate ?? defaults . labelAutoRotate ?? true ;
450458 const anchor = outlineLabelAnchor ( points , placement ) ;
@@ -459,12 +467,12 @@ class BubbleSetLayer {
459467 const y = anchor . y + anchor . ny * standoff + ( opts . labelOffsetY ?? 0 ) ;
460468
461469 ctx . font = `${ fontSize } px Arial, sans-serif` ;
462- ctx . textAlign = " center" ;
463- ctx . textBaseline = " middle" ;
470+ ctx . textAlign = ' center' ;
471+ ctx . textBaseline = ' middle' ;
464472 const textWidth = ctx . measureText ( text ) . width ;
465473
466474 // Rotation only makes sense hugging the path; "center" has no tangent.
467- const rotated = autoRotate && closeToPath && placement !== " center" ;
475+ const rotated = autoRotate && closeToPath && placement !== ' center' ;
468476 if ( rotated ) {
469477 ctx . save ( ) ;
470478 ctx . translate ( x , y ) ;
@@ -475,18 +483,18 @@ class BubbleSetLayer {
475483
476484 if ( opts . labelBackground ?? defaults . labelBackground ) {
477485 const radius = opts . labelBackgroundRadius ?? defaults . labelBackgroundRadius ?? 5 ;
478- ctx . fillStyle = opts . labelBackgroundFill ?? defaults . labelBackgroundFill ?? " #403C53" ;
486+ ctx . fillStyle = opts . labelBackgroundFill ?? defaults . labelBackgroundFill ?? ' #403C53' ;
479487 ctx . beginPath ( ) ;
480488 ctx . roundRect (
481489 lx - textWidth / 2 - padding ,
482490 ly - fontSize / 2 - padding ,
483491 textWidth + 2 * padding ,
484492 fontSize + 2 * padding ,
485- radius ,
493+ radius
486494 ) ;
487495 ctx . fill ( ) ;
488496 }
489- ctx . fillStyle = opts . labelFill ?? defaults . labelFill ?? " #fff" ;
497+ ctx . fillStyle = opts . labelFill ?? defaults . labelFill ?? ' #fff' ;
490498 ctx . fillText ( text , lx , ly ) ;
491499 if ( rotated ) ctx . restore ( ) ;
492500 }
0 commit comments