@@ -18,6 +18,7 @@ import { VerticalAlign } from 'types/text'
1818import { smartTransition } from 'utils/d3'
1919import { clamp , getNumber , getString , groupBy , isNumber } from 'utils/data'
2020import { getCSSVariableValueInPixels } from 'utils/misc'
21+ import { estimateStringPixelLength } from 'utils/text'
2122
2223// Config
2324import { SankeyDefaultConfig , SankeyConfigInterface } from './config'
@@ -26,12 +27,12 @@ import { SankeyDefaultConfig, SankeyConfigInterface } from './config'
2627import * as s from './style'
2728
2829// Local Types
29- import { SankeyInputLink , SankeyInputNode , SankeyLayout , SankeyLink , SankeyNode , SankeyZoomMode } from './types'
30+ import { SankeyInputLink , SankeyInputNode , SankeyLayout , SankeyLink , SankeyNode , SankeySubLabelPlacement , SankeyZoomMode } from './types'
3031
3132// Modules
3233import { createLinks , removeLinks , updateLinks } from './modules/link'
3334import { createNodes , NODE_SELECTION_RECT_DELTA , onNodeMouseOut , onNodeMouseOver , removeNodes , updateNodes } from './modules/node'
34- import { getLabelOrientation , requiredLabelSpace } from './modules/label'
35+ import { getLabelOrientation , SANKEY_LABEL_BLOCK_PADDING , SANKEY_LABEL_SPACING } from './modules/label'
3536
3637export class Sankey <
3738 N extends SankeyInputNode ,
@@ -47,6 +48,7 @@ export class Sankey<
4748 public g : Selection < SVGGElement , unknown , null , undefined >
4849 // eslint-disable-next-line @typescript-eslint/naming-convention
4950 private _gNode : SVGGElement & { __zoom : ZoomTransform }
51+ private _prevWidth : number | undefined = undefined
5052 private _extendedWidth : number | undefined = undefined
5153 private _extendedHeight : number | undefined = undefined
5254 private _extendedHeightIncreased : number | undefined = undefined
@@ -104,45 +106,71 @@ export class Sankey<
104106
105107 get bleed ( ) : Spacing {
106108 const { config, datamodel : { nodes, links } } = this
107- if ( ! nodes . length ) return { top : 0 , bottom : 0 , left : 0 , right : 0 }
109+ let bleed : Spacing = { top : 0 , bottom : 0 , left : 0 , right : 0 }
108110
109- const labelFontSize = config . labelFontSize ?? getCSSVariableValueInPixels ( 'var(--vis-sankey-label-font-size)' , this . element )
111+ if ( nodes . length ) {
112+ const labelFontSize = config . labelFontSize || getCSSVariableValueInPixels ( 'var(--vis-sankey-node-label-font-size)' , this . element )
113+ const subLabelFontSize = config . subLabelFontSize || getCSSVariableValueInPixels ( 'var(--vis-sankey-node-sublabel-font-size)' , this . element )
110114
111- // We pre-calculate sankey layout to get information about node labels placement and calculate bleed properly
112- // Potentially it can be a performance bottleneck for large layouts, but generally rendering of such layouts is much more computationally heavy
113- const sankeyProbeSize = 1000
114- this . _populateLinkAndNodeValues ( )
115- this . _sankey . size ( [ sankeyProbeSize , sankeyProbeSize ] )
116- this . _sankey ( { nodes, links } )
117- const layerSpacing = this . _getLayerSpacing ( nodes )
118- const labelSize = requiredLabelSpace ( clamp ( layerSpacing , 0 , config . labelMaxWidth ?? Infinity ) , labelFontSize )
119- const maxDepth = max ( nodes , d => d . depth )
120- const zeroDepthNodes = nodes . filter ( d => d . depth === 0 )
121- const maxDepthNodes = nodes . filter ( d => d . depth === maxDepth )
122-
123-
124- const left = zeroDepthNodes . some ( d => getLabelOrientation ( d , sankeyProbeSize , config . labelPosition ) === Position . Left ) ? labelSize . width : 0
125- const right = ( maxDepthNodes . some ( d => getString ( d , config . label ) ) &&
126- maxDepthNodes . some ( d => getLabelOrientation ( d , sankeyProbeSize , config . labelPosition ) === Position . Right ) )
127- ? labelSize . width
128- : 0
129-
130- const top = config . labelVerticalAlign === VerticalAlign . Top ? 0
131- : config . labelVerticalAlign === VerticalAlign . Bottom ? labelSize . height
132- : labelSize . height / 2
133-
134- const bottom = config . labelVerticalAlign === VerticalAlign . Top ? labelSize . height
135- : config . labelVerticalAlign === VerticalAlign . Bottom ? 0
136- : labelSize . height / 2
137-
138- const nodeSelectionBleed = config . selectedNodeIds ? 1 + NODE_SELECTION_RECT_DELTA : 0
139- const bleed = {
140- top : nodeSelectionBleed + top ,
141- bottom : nodeSelectionBleed + bottom ,
142- left : nodeSelectionBleed + left ,
143- right : nodeSelectionBleed + right ,
115+ // We pre-calculate sankey layout to get information about node labels placement and calculate bleed properly
116+ // Potentially it can be a performance bottleneck for large layouts, but generally rendering of such layouts is much more computationally heavy
117+ const sankeyProbeSize = 1000
118+ this . _populateLinkAndNodeValues ( )
119+ this . _sankey . size ( [ sankeyProbeSize , sankeyProbeSize ] )
120+ this . _sankey ( { nodes, links } )
121+ const requiredLabelHeight = labelFontSize * 2.5 + 2 * SANKEY_LABEL_BLOCK_PADDING // Assuming 2.5 lines per label
122+
123+ const maxLayer = max ( nodes , d => d . layer )
124+ const zeroLayerNodes = nodes . filter ( d => d . layer === 0 )
125+ const maxLayerNodes = nodes . filter ( d => d . layer === maxLayer )
126+ const layerSpacing = this . _getLayerSpacing ( nodes )
127+
128+ let left = 0
129+ const fallbackLabelMaxWidth = Math . min ( this . _width / 6 , layerSpacing )
130+ const labelMaxWidth = config . labelMaxWidth || fallbackLabelMaxWidth
131+ const labelHorizontalPadding = 2 * SANKEY_LABEL_SPACING + 2 * SANKEY_LABEL_BLOCK_PADDING
132+ const hasLabelsOnTheLeft = zeroLayerNodes . some ( d => getLabelOrientation ( d , sankeyProbeSize , config . labelPosition ) === Position . Left )
133+
134+ const estimateRequiredLabelWidth = ( d : SankeyNode < N , L > , config : SankeyConfigInterface < N , L > ) : number => {
135+ const inlineLabelAddWidth = 5 // Without this, the label anf sub-label will look too close to each other
136+ const tolerance = 1.1
137+ const isSublabelInline = config . subLabelPlacement === SankeySubLabelPlacement . Inline
138+ const labelText = `${ getString ( d , config . label ) ?? '' } ` // Stringify because theoretically it can be a number
139+ const sublabelText = `${ getString ( d , config . subLabel ) ?? '' } ` // Stringify because theoretically it can be a number
140+ const labelTextWidth = tolerance * estimateStringPixelLength ( labelText , labelFontSize )
141+ const sublabelTextWidth = tolerance * estimateStringPixelLength ( sublabelText , subLabelFontSize )
142+ return isSublabelInline ? inlineLabelAddWidth + ( labelTextWidth + sublabelTextWidth ) : Math . max ( labelTextWidth , sublabelTextWidth )
143+ }
144+ if ( hasLabelsOnTheLeft ) {
145+ const maxLeftLabelWidth = max ( zeroLayerNodes , d => estimateRequiredLabelWidth ( d , config ) )
146+ left = min ( [ labelMaxWidth , maxLeftLabelWidth ] ) + labelHorizontalPadding
147+ }
148+
149+ let right = 0
150+ const hasLabelsOnTheRight = maxLayerNodes . some ( d => getLabelOrientation ( d , sankeyProbeSize , config . labelPosition ) === Position . Right )
151+ if ( hasLabelsOnTheRight ) {
152+ const maxRightLabelWidth = max ( maxLayerNodes , d => estimateRequiredLabelWidth ( d , config ) )
153+ right = min ( [ labelMaxWidth , maxRightLabelWidth ] ) + labelHorizontalPadding
154+ }
155+
156+ const top = config . labelVerticalAlign === VerticalAlign . Top ? 0
157+ : config . labelVerticalAlign === VerticalAlign . Bottom ? requiredLabelHeight
158+ : requiredLabelHeight / 2
159+
160+ const bottom = config . labelVerticalAlign === VerticalAlign . Top ? requiredLabelHeight
161+ : config . labelVerticalAlign === VerticalAlign . Bottom ? 0
162+ : requiredLabelHeight / 2
163+
164+ const nodeSelectionRectBleed = config . selectedNodeIds ? 1 + NODE_SELECTION_RECT_DELTA : 0
165+ bleed = {
166+ top : nodeSelectionRectBleed + top ,
167+ bottom : nodeSelectionRectBleed + bottom ,
168+ left : left + ( hasLabelsOnTheLeft ? 0 : nodeSelectionRectBleed ) ,
169+ right : right + ( hasLabelsOnTheRight ? 0 : nodeSelectionRectBleed ) ,
170+ }
144171 }
145172
173+
146174 // Cache bleed for onZoom
147175 this . _bleedCached = bleed
148176
@@ -200,7 +228,9 @@ export class Sankey<
200228
201229 _render ( customDuration ?: number ) : void {
202230 const { config, datamodel : { nodes, links } } = this
203- const bleed = this . _bleedCached ?? this . bleed
231+ const wasResized = this . _prevWidth !== this . _width
232+ this . _prevWidth = this . _width
233+ const bleed = wasResized || ! this . _bleedCached ? this . bleed : this . _bleedCached
204234 const duration = isNumber ( customDuration ) ? customDuration : config . duration
205235
206236 if (
@@ -327,7 +357,7 @@ export class Sankey<
327357 const transform = event . transform
328358 const sourceEvent = event . sourceEvent as WheelEvent | MouseEvent | TouchEvent | undefined
329359 const zoomMode = config . zoomMode || SankeyZoomMode . XY
330- const bleed = this . _bleedCached
360+ const bleed = this . _bleedCached ?? this . bleed
331361
332362 // Zoom pivots
333363 const minX = min ( nodes , d => d . x0 ) ?? 0
@@ -441,7 +471,7 @@ export class Sankey<
441471
442472 private _prepareLayout ( ) : void {
443473 const { config, datamodel } = this
444- const bleed = this . _bleedCached
474+ const bleed = this . _bleedCached ?? this . bleed
445475 const isExtendedSize = this . sizing === Sizing . Extend
446476 const sankeyHeight = this . sizing === Sizing . Fit ? this . _height : this . _extendedHeight
447477 const sankeyWidth = this . sizing === Sizing . Fit ? this . _width : this . _extendedWidth
@@ -665,23 +695,25 @@ export class Sankey<
665695 const { config } = this
666696 if ( ! nodes ?. length ) return 0
667697
668- const firstNode = nodes [ 0 ]
669- const nextLayerNode = nodes . find ( d => d . layer === firstNode . layer + 1 )
670- return nextLayerNode ? nextLayerNode . x0 - ( firstNode . x0 + config . nodeWidth ) : this . _width - firstNode . x1
698+ const firstLayerNode = nodes . find ( d => d . layer === 0 )
699+ const nextLayerNode = nodes . find ( d => d . layer === firstLayerNode . layer + 1 )
700+ return nextLayerNode ? nextLayerNode . x0 - ( firstLayerNode . x0 + config . nodeWidth ) : this . _width - firstLayerNode . x1
671701 }
672702
673703 private _onNodeMouseOver ( d : SankeyNode < N , L > , event : MouseEvent ) : void {
674704 const { datamodel } = this
705+ const bleed = this . _bleedCached ?? this . bleed
675706 const nodeSelection = select < SVGGElement , SankeyNode < N , L > > ( event . currentTarget as SVGGElement )
676707 const sankeyWidth = this . sizing === Sizing . Fit ? this . _width : this . _extendedWidth
677- onNodeMouseOver ( d , datamodel . nodes , nodeSelection , this . config , sankeyWidth , this . _getLayerSpacing ( this . datamodel . nodes ) )
708+ onNodeMouseOver ( d , datamodel . nodes , nodeSelection , this . config , sankeyWidth , this . _getLayerSpacing ( this . datamodel . nodes ) , bleed )
678709 }
679710
680711 private _onNodeMouseOut ( d : SankeyNode < N , L > , event : MouseEvent ) : void {
681712 const { datamodel } = this
713+ const bleed = this . _bleedCached ?? this . bleed
682714 const nodeSelection = select < SVGGElement , SankeyNode < N , L > > ( event . currentTarget as SVGGElement )
683715 const sankeyWidth = this . sizing === Sizing . Fit ? this . _width : this . _extendedWidth
684- onNodeMouseOut ( d , datamodel . nodes , nodeSelection , this . config , sankeyWidth , this . _getLayerSpacing ( this . datamodel . nodes ) )
716+ onNodeMouseOut ( d , datamodel . nodes , nodeSelection , this . config , sankeyWidth , this . _getLayerSpacing ( this . datamodel . nodes ) , bleed )
685717 }
686718
687719 private _onNodeRectMouseOver ( d : SankeyNode < N , L > ) : void {
0 commit comments