import { getDegree } from '../utils/graph' ; import { LayoutOptionBase , Node } from '../../types' ; export interface BBox { x1 : number ; y1 : number ; x2 ? : number ; y2 ? : number ; w : number ; h : number ; } interface NodeValue { value : number ; node : Node ; } interface Level extends Array < NodeValue > { dTheta ? : number ; r ? : number ; } interface ConcentricConfig extends LayoutOptionBase { /** 同心圆的布局范围，默认为当前画布的宽高范围 */ boundingBox : BBox ; /** 节点间的距离，默认为60 */ minNodeSpacing : number ; /** 每层的节点度数范围 */ levelWidth : ( nodes : Node [ ] , maxDegree : number ) => number ; counterclockwise : boolean ; clockwise : boolean ; concentric : ( node : Node ) => number ; avoidOverlap : boolean ; sweep ? : number ; equidistant : boolean ; startAngle : number ; } type PartialConcentricOption = Partial < Exclude < ConcentricConfig , keyof LayoutOptionBase > > ; export type ConcentricOption = PartialConcentricOption & LayoutOptionBase ; export const makeBoundingBox = ( bb ) : BBox | undefined => { if ( bb . x2 != null && bb . y2 != null && bb . x2 >= bb . x1 && bb . y2 >= bb . y1 ) { return { x1 : bb . x1 , y1 : bb . y1 , x2 : bb . x2 , y2 : bb . y2 , w : bb . x2 - bb . x1 , h : bb . y2 - bb . y1 , } ; } if ( bb . w >= 0 && bb . h >= 0 ) { return { x1 : bb . x1 , y1 : bb . y1 , x2 : bb . x1 + bb . w , y2 : bb . y1 + bb . h , w : bb . w , h : bb . h , } ; } } ; const defaults = { fit : true , padding : 30 , startAngle : ( 3 / 2 ) * Math . PI , sweep : undefined , clockwise : true , equidistant : false , minNodeSpacing : 10 , boundingBox : undefined , avoidOverlap : true , nodeDimensionsIncludeLabels : false , height : undefined , width : undefined , spacingFactor : undefined , concentric ( node ) { return node . degree ( ) ; } , levelWidth ( nodes ) { return nodes . maxDegree ( ) / 4 ; } , animate : false , animationDuration : 500 , animationEasing : undefined , ready : undefined , stop : undefined , } ; class ConcentricLayout { options : ConcentricOption ; constructor ( options ) { const { edges } = options . data ! ; this . options = { ... defaults , ... { concentric ( node ) { return getDegree ( node , edges as any ) ; } , } , ... options , } as ConcentricOption ; } run ( ) { const { options } = this ; const clockwise = options . counterclockwise !== undefined ? ! options . counterclockwise : options . clockwise ; const { nodes } = this . options . data ; const bb = makeBoundingBox ( options . boundingBox ? options . boundingBox : { x1 : 0 , y1 : 0 , w : options . width , h : options . height , } ) ; const center = { x : bb ! . x1 + bb ! . w / 2 , y : bb ! . y1 + bb ! . h / 2 , } ; let maxNodeSize = 0 ; const nodeValues = nodes . map ( ( node ) => { return { value : options . concentric ! ( node ) , node , } ; } ) . sort ( ( a , b ) => { return b . value - a . value ; } ) ; maxNodeSize = 50 ; const maxDegree = nodeValues [ 0 ] . value ; const levelWidth = options . levelWidth ! ( nodes , maxDegree ) ; const levels = [ [ ] ] ; let currentLevel = levels [ 0 ] ; nodeValues . forEach ( ( node ) => { if ( currentLevel . length > 0 ) { const firstItem = currentLevel [ 0 ] . value ; const diff = Math . abs ( firstItem - node . value ) ; if ( diff >= levelWidth ) { currentLevel = [ ] ; levels . push ( currentLevel ) ; } } currentLevel . push ( node ) ; } ) ; let minDist = maxNodeSize + options . minNodeSpacing ! ; if ( ! options . avoidOverlap ) { const firstLvlHasMulti = levels . length > 0 && levels [ 0 ] . length > 1 ; const maxR = Math . min ( bb ! . w , bb ! . h ) / 2 - minDist ; const rStep = maxR / ( levels . length + ( firstLvlHasMulti ? 1 : 0 ) ) ; minDist = Math . min ( minDist , rStep ) ; } let r = 0 ; levels . forEach ( ( level ) => { const sweep = options . sweep === undefined ? 2 * Math . PI - ( 2 * Math . PI ) / level . length : options . sweep ; level . dTheta = sweep / Math . max ( 1 , level . length - 1 ) ; const { dTheta } = level ; if ( level . length > 1 && options . avoidOverlap ) { const dcos = Math . cos ( dTheta ) - Math . cos ( 0 ) ; const dsin = Math . sin ( dTheta ) - Math . sin ( 0 ) ; const rMin = Math . sqrt ( ( minDist * minDist ) / ( dcos * dcos + dsin * dsin ) ) ; r = Math . max ( rMin , r ) ; } level . r = r ; r += minDist ; } ) ; if ( options . equidistant ) { let rDeltaMax = 0 ; let radius = 0 ; for ( let i = 0 ; i < levels . length ; i ++ ) { const level = levels [ i ] ; const rDelta = level . r ! - radius ; rDeltaMax = Math . max ( rDeltaMax , rDelta ) ; } radius = 0 ; for ( let i = 0 ; i < levels . length ; i ++ ) { const level = levels [ i ] ; if ( i === 0 ) { radius = level . r ! ; } level . r = radius ; radius += rDeltaMax ; } } const pos = { } ; const outerR = [ ... levels ] . pop ( ) ! . r ; const result = [ ] ; for ( let i = 0 ; i < levels . length ; i ++ ) { const level = levels [ i ] ; const { dTheta } = level ; const { r : radius } = level ; for ( let j = 0 ; j < level . length ; j ++ ) { const val = level [ j ] ; const theta = options . startAngle ! + ( clockwise ? 1 : - 1 ) * dTheta ! * j ; const p = { x : center . x + radius ! * Math . cos ( theta ) , y : center . y + radius ! * Math . sin ( theta ) , } ; pos [ val . node . data . id ] = p ; result . push ( { ... val . node , data : val . node . data , degree : val . value , layout : { ... val . node . layout , concentric : { outerR , center , theta , } , } , x : p . x , y : p . y , } ) ; } } const { edges } = options . data ; return { outerR , levels , nodes : result , edges , } ; } } export default ConcentricLayout ;