import EventEmitter from '@antv/event-emitter' ; import { ICanvas , IGroup , Point } from '@antv/g-base' ; import { ext } from '@antv/matrix-util' ; import { clone , deepMix , each , isPlainObject , isString } from '@antv/util' ; import { getDegree , getAdjMatrix as getAdjacentMatrix , Stack , floydWarshall , } from '@antv/algorithm' ; import { IAbstractGraph } from '../interface/graph' ; import { IEdge , INode , ICombo } from '../interface/item' ; import { GraphAnimateConfig , GraphOptions , EdgeConfig , GraphData , Item , ITEM_TYPE , ModelConfig , NodeConfig , NodeMap , Padding , TreeGraphData , ComboConfig , ModeOption , ModeType , States , ComboTree , HullCfg , IG6GraphEvent , IPoint , FitViewRules , } from '../types' ; import { lerp , move } from '../util/math' ; import { dataValidation , singleDataValidation } from '../util/validation' ; import Global from '../global' ; import { ItemController , ModeController , StateController , ViewController } from './controller' ; import { plainCombosToTrees , traverseTree , reconstructTree , traverseTreeUp } from '../util/graphic' ; import Hull from '../item/hull' ; const { transform } = ext ; const NODE = 'node' ; export interface PrivateGraphOption extends GraphOptions { data : GraphData ; event : boolean ; nodes : NodeConfig [ ] ; edges : EdgeConfig [ ] ; vedges : EdgeConfig [ ] ; combos : ComboConfig [ ] ; itemMap : NodeMap ; callback : ( ) => void ; /**
     * 格式：
     * {
     *  hover: [Node, Node],
     *  selected: [Node]
     * }
     */ states : States ; } export default abstract class AbstractGraph extends EventEmitter implements IAbstractGraph { protected animating : boolean ; protected cfg : GraphOptions & { [ key ] : any ; } ; public destroyed : boolean ; protected undoStack : Stack ; protected redoStack : Stack ; constructor ( cfg ) { super ( ) ; this . cfg = deepMix ( this . getDefaultCfg ( ) , cfg ) ; this . init ( ) ; this . animating = false ; this . destroyed = false ; if ( this . cfg . enabledStack ) { this . undoStack = new Stack ( this . cfg . maxStep ) ; this . redoStack = new Stack ( this . cfg . maxStep ) ; } } protected init ( ) { this . initCanvas ( ) ; const viewController = new ViewController ( this ) ; const modeController = new ModeController ( this ) ; const itemController = new ItemController ( this ) ; const stateController = new StateController ( this ) ; this . set ( { viewController , modeController , itemController , stateController , } ) ; this . initLayoutController ( ) ; this . initEventController ( ) ; this . initGroups ( ) ; /** 初始化插件 */ this . initPlugins ( ) ; } protected abstract initLayoutController ( ) ; protected abstract initEventController ( ) ; /**
     * initCanvas 需要在 PC 和 Mobile 分别实现
     */ protected abstract initCanvas ( ) ; /**
     * initPlugins 需要在 PC 和 Mobile 分别实现
     */ protected abstract initPlugins ( ) ; protected initGroups ( ) { const canvas = this . get ( 'canvas' ) ; if ( ! canvas ) return ; const el = canvas . get ( 'el' ) ; const { id = 'g6' } = el || { } ; const group = canvas . addGroup ( { id : `${ id }-root` , className : Global . rootContainerClassName , } ) ; if ( this . get ( 'groupByTypes' ) ) { const edgeGroup = group . addGroup ( { id : `${ id }-edge` , className : Global . edgeContainerClassName , } ) ; const nodeGroup = group . addGroup ( { id : `${ id }-node` , className : Global . nodeContainerClassName , } ) ; const comboGroup = group . addGroup ( { id : `${ id }-combo` , className : Global . comboContainerClassName , } ) ; comboGroup . toBack ( ) ; this . set ( { nodeGroup , edgeGroup , comboGroup } ) ; } const delegateGroup = group . addGroup ( { id : `${ id }-delegate` , className : Global . delegateContainerClassName , } ) ; this . set ( { delegateGroup } ) ; this . set ( 'group' , group ) ; } public getDefaultCfg ( ) { return { /**
             * Container could be dom object or dom id
             */ container : undefined , /**
             * Canvas width
             * unit pixel if undefined force fit width
             */ width : undefined , /**
             * Canvas height
             * unit pixel if undefined force fit height
             */ height : undefined , type { string } renderer : 'canvas' , /**
             * control graph behaviors
             */ modes : { } , /**
             * 注册插件
             */ plugins : [ ] , /**
             * source data
             */ data : { } , /**
             * Fit view padding (client scale)
             */ fitViewPadding : 10 , /**
             * Minimum scale size
             */ minZoom : 0.2 , /**
             * Maxmum scale size
             */ maxZoom : 10 , /**
             *  capture events
             */ event : true , /**
             * group node & edges into different graphic groups
             */ groupByTypes : true , /**
             * determine if it's a directed graph
             */ directed : false , /**
             * when data or shape changed, should canvas draw automatically
             */ autoPaint : true , /**
             * store all the node instances
             */ nodes : [ ] , /**
             * store all the edge instances
             */ edges : [ ] , /**
             * store all the combo instances
             */ combos : [ ] , /**
             * store all the edge instances which are virtual edges related to collapsed combo
             */ vedges : [ ] , /**
             * all the instances indexed by id
             */ itemMap : { } , /**
             * 边直接连接到节点的中心，不再考虑锚点
             */ linkCenter : false , /**
             * 默认的节点配置，data 上定义的配置会覆盖这些配置。例如：
             * defaultNode: {
             *  type: 'rect',
             *  size: [60, 40],
             *  style: {
             *    //... 样式配置项
             *  }
             * }
             * 若数据项为 { id: 'node', x: 100, y: 100 }
             * 实际创建的节点模型是 { id: 'node', x: 100, y: 100， type: 'rect', size: [60, 40] }
             * 若数据项为 { id: 'node', x: 100, y: 100, type: 'circle' }
             * 实际创建的节点模型是 { id: 'node', x: 100, y: 100， type: 'circle', size: [60, 40] }
             */ defaultNode : { } , /**
             * 默认边配置，data 上定义的配置会覆盖这些配置。用法同 defaultNode
             */ defaultEdge : { } , /**
             * 节点默认样式，也可以添加状态样式
             * 例如：
             * const graph = new G6.Graph({
             *  nodeStateStyles: {
             *    selected: { fill: '#ccc', stroke: '#666' },
             *    active: { lineWidth: 2 }
             *  },
             *  ...
             * });
             *
             */ nodeStateStyles : { } , /**
             * 边默认样式，用法同nodeStateStyle
             */ edgeStateStyles : { } , /**
             * graph 状态
             */ states : { } , /**
             * 是否启用全局动画
             */ animate : false , /**
             * 动画设置,仅在 animate 为 true 时有效
             */ animateCfg : { /**
                 * 帧回调函数，用于自定义节点运动路径，为空时线性运动
                 */ onFrame : undefined , /**
                 * 动画时长(ms)
                 */ duration : 500 , /**
                 * 指定动画动效
                 */ easing : 'easeLinear' , } , callback : undefined , enabledStack : false , maxStep : 10 , tooltips : [ ] , } ; } param key param val public set < T = any > ( key , val ? ) { if ( isPlainObject ( key ) ) { this . cfg = { ... this . cfg , ... key } ; } else { this . cfg [ key ] = val ; } return this ; } param key public get ( key ) { return this . cfg ?. [ key ] ; } return public getGroup ( ) { return this . get ( 'group' ) ; } return public getContainer ( ) { return this . get ( 'container' ) ; } return public getMinZoom ( ) { return this . get ( 'minZoom' ) ; } return public setMinZoom ( ratio ) { return this . set ( 'minZoom' , ratio ) ; } param maxZoom public getMaxZoom ( ) { return this . get ( 'maxZoom' ) ; } param maxZoom public setMaxZoom ( ratio ) { return this . set ( 'maxZoom' , ratio ) ; } return public getWidth ( ) { return this . get ( 'width' ) ; } return public getHeight ( ) { return this . get ( 'height' ) ; } param { string | Item } item param { string [ ] } states public clearItemStates ( item , states ? ) { if ( isString ( item ) ) { item = this . findById ( item ) ; } const itemController = this . get ( 'itemController' ) ; if ( ! states ) { states = item . get < string [ ] > ( 'states' ) ; } itemController . clearItemStates ( item , states ) ; const stateController = this . get ( 'stateController' ) ; stateController . updateStates ( item , states , false ) ; } param { function } nodeFn public node ( nodeFn ) { if ( typeof nodeFn === 'function' ) { this . set ( 'nodeMapper' , nodeFn ) ; } } param { function } edgeFn public edge ( edgeFn ) { if ( typeof edgeFn === 'function' ) { this . set ( 'edgeMapper' , edgeFn ) ; } } param comboFn public combo ( comboFn ) { if ( typeof comboFn === 'function' ) { this . set ( 'comboMapper' , comboFn ) ; } } param id public findById ( id ) { return this . get ( 'itemMap' ) [ id ] ; } param { ITEM_TYPE } type param { ( item : T , index : number ) => T } fn return { T } public find < T extends Item > ( type , fn ) { let result ; const items = this . get ( `${ type }s` ) ; each ( items , ( item , i ) => { if ( fn ( item , i ) ) { result = item ; return result ; } } ) ; return result ; } param { string } type param { string } fn return { array } public findAll < T extends Item > ( type , fn ) { const result = [ ] ; each ( this . get ( `${ type }s` ) , ( item , i ) => { if ( fn ( item , i ) ) { result . push ( item ) ; } } ) ; return result ; } param { string } type param { string } state return { object } public findAllByState < T extends Item > ( type , state ) { return this . findAll ( type , item => item . hasState ( state ) ) ; } private getAnimateCfgWithCallback ( { animateCfg , callback } ) { let animateConfig ; if ( ! animateCfg ) { animateConfig = { duration : 500 , callback } ; } else { animateConfig = clone ( animateCfg ) ; if ( animateCfg . callback ) { const animateCfgCallback = animateCfg . callback ; animateConfig . callback = ( ) => { callback ( ) ; animateCfgCallback ( ) ; } ; } else { animateConfig . callback = callback ; } } return animateConfig ; } param dx param dy param { boolean } animate param { GraphAnimateConfig } animateCfg public translate ( dx , dy , animate ? , animateCfg ? ) { const group = this . get ( 'group' ) ; let matrix = clone ( group . getMatrix ( ) ) ; if ( ! matrix ) { matrix = [ 1 , 0 , 0 , 0 , 1 , 0 , 0 , 0 , 1 ] ; } if ( animate ) { const animateConfig = this . getAnimateCfgWithCallback ( { animateCfg , callback : ( ) => this . emit ( 'viewportchange' , { action : 'translate' , matrix : group . getMatrix ( ) } ) } ) ; move ( group , { x : group . getCanvasBBox ( ) . x + dx , y : group . getCanvasBBox ( ) . y + dy } , animate , animateConfig || { duration : 500 , easing : 'easeCubic' } ) ; } else { matrix = transform ( matrix , [ [ 't' , dx , dy ] ] ) ; group . setMatrix ( matrix ) ; this . emit ( 'viewportchange' , { action : 'translate' , matrix } ) ; this . autoPaint ( ) ; } } param { number } x param { number } y param { boolean } animate param { GraphAnimateConfig } animateCfg public moveTo ( x , y , animate ? , animateCfg ? ) { const group = this . get ( 'group' ) ; move ( group , { x , y } , animate , animateCfg || { duration : 500 , easing : 'easeCubic' , } ) ; this . emit ( 'viewportchange' , { action : 'move' , matrix : group . getMatrix ( ) } ) ; } param { object } padding param { FitViewRules } rules public fitView ( padding ? , rules ? ) { if ( padding ) { this . set ( 'fitViewPadding' , padding ) ; } const viewController = this . get ( 'viewController' ) ; if ( rules ) { viewController . fitViewByRules ( rules ) ; } else { viewController . fitView ( ) ; } this . autoPaint ( ) ; } /**
     * 调整视口适应视图，不缩放，仅将图 bbox 中心对齐到画布中心
     */ public fitCenter ( ) { const viewController = this . get ( 'viewController' ) ; viewController . fitCenter ( ) ; this . autoPaint ( ) ; } param { string | ModeOption | ModeType [ ] } behaviors param { string | string [ ] } modes return { Graph } public addBehaviors ( behaviors , modes ) { const modeController = this . get ( 'modeController' ) ; modeController . manipulateBehaviors ( behaviors , modes , true ) ; return this ; } param { string | ModeOption | ModeType [ ] } behaviors param { string | string [ ] } modes return { Graph } public removeBehaviors ( behaviors , modes ) { const modeController = this . get ( 'modeController' ) ; modeController . manipulateBehaviors ( behaviors , modes , false ) ; return this ; } param { string | ModeOption | ModeType } behavior param { string | string [ ] } modes return { Graph } public updateBehavior ( behavior , newCfg , mode ? ) { const modeController = this . get ( 'modeController' ) ; modeController . updateBehavior ( behavior , newCfg , mode ) ; return this ; } param ratio param center param { boolean } animate param { GraphAnimateConfig } animateCfg return { boolean } public zoom ( ratio , center ? , animate ? , animateCfg ? ) { const group = this . get ( 'group' ) ; let matrix = clone ( group . getMatrix ( ) ) ; const minZoom = this . get ( 'minZoom' ) ; const maxZoom = this . get ( 'maxZoom' ) ; if ( ! matrix ) { matrix = [ 1 , 0 , 0 , 0 , 1 , 0 , 0 , 0 , 1 ] ; } if ( center ) { matrix = transform ( matrix , [ [ 't' , - center . x , - center . y ] , [ 's' , ratio , ratio ] , [ 't' , center . x , center . y ] , ] ) ; } else { matrix = transform ( matrix , [ [ 's' , ratio , ratio ] ] ) ; } if ( ( minZoom && matrix [ 0 ] < minZoom ) || ( maxZoom && matrix [ 0 ] > maxZoom ) ) { return false ; } if ( animate ) { let aniMatrix = clone ( group . getMatrix ( ) ) ; if ( ! aniMatrix ) { aniMatrix = [ 1 , 0 , 0 , 0 , 1 , 0 , 0 , 0 , 1 ] ; } const initialRatio = aniMatrix [ 0 ] ; const targetRatio = initialRatio * ratio ; const animateConfig = this . getAnimateCfgWithCallback ( { animateCfg , callback : ( ) => this . emit ( 'viewportchange' , { action : 'zoom' , matrix : group . getMatrix ( ) } ) } ) ; group . animate ( ( ratio ) => { if ( ratio === 1 ) { aniMatrix = matrix ; } else { const scale = lerp ( initialRatio , targetRatio , ratio ) / aniMatrix [ 0 ] ; if ( center ) { aniMatrix = transform ( aniMatrix , [ [ 't' , - center . x , - center . y ] , [ 's' , scale , scale ] , [ 't' , center . x , center . y ] ] ) ; } else { aniMatrix = transform ( aniMatrix , [ [ 's' , scale , scale ] ] ) ; } } return { matrix : aniMatrix } ; } , animateConfig ) ; } else { group . setMatrix ( matrix ) ; this . emit ( 'viewportchange' , { action : 'zoom' , matrix } ) ; this . autoPaint ( ) ; } return true ; } param { number } toRatio param { Point } center param { boolean } animate param { GraphAnimateConfig } animateCfg return { boolean } public zoomTo ( toRatio , center ? , animate ? , animateCfg ? ) { const ratio = toRatio / this . getZoom ( ) ; return this . zoom ( ratio , center , animate , animateCfg ) ; } param { Item } item param { boolean } animate param { GraphAnimateConfig } animateCfg public focusItem ( item , animate ? , animateCfg ? ) { const viewController = this . get ( 'viewController' ) ; let isAnimate = false ; if ( animate ) isAnimate = true ; else if ( animate === undefined ) isAnimate = this . get ( 'animate' ) ; let curAniamteCfg = { } as GraphAnimateConfig ; if ( animateCfg ) curAniamteCfg = animateCfg ; else if ( animateCfg === undefined ) curAniamteCfg = this . get ( 'animateCfg' ) ; viewController . focus ( item , isAnimate , curAniamteCfg ) ; this . autoPaint ( ) ; } internal public autoPaint ( ) { if ( this . get ( 'autoPaint' ) ) { this . paint ( ) ; } } /**
     * 仅画布重新绘制
     */ public paint ( ) { this . emit ( 'beforepaint' ) ; this . get ( 'canvas' ) . draw ( ) ; this . emit ( 'afterpaint' ) ; } param { number } clientX param { number } clientY return { Point } public getPointByClient ( clientX , clientY ) { const viewController = this . get ( 'viewController' ) ; return viewController . getPointByClient ( clientX , clientY ) ; } param { number } x param { number } y return { Point } public getClientByPoint ( x , y ) { const viewController = this . get ( 'viewController' ) ; return viewController . getClientByPoint ( x , y ) ; } param { number } canvasX param { number } canvasY return { object } public getPointByCanvas ( canvasX , canvasY ) { const viewController = this . get ( 'viewController' ) ; return viewController . getPointByCanvas ( canvasX , canvasY ) ; } param { number } x param { number } y return { object } public getCanvasByPoint ( x , y ) { const viewController = this . get ( 'viewController' ) ; return viewController . getCanvasByPoint ( x , y ) ; } return { object } public getGraphCenterPoint ( ) { const bbox = this . get ( 'group' ) . getCanvasBBox ( ) ; return { x : ( bbox . minX + bbox . maxX ) / 2 , y : ( bbox . minY + bbox . maxY ) / 2 , } ; } return { object } public getViewPortCenterPoint ( ) { return this . getPointByCanvas ( this . get ( 'width' ) / 2 , this . get ( 'height' ) / 2 ) ; } param { Item } item param { boolean } stack public showItem ( item , stack = true ) { const itemController = this . get ( 'itemController' ) ; const object = itemController . changeItemVisibility ( item , true ) ; if ( stack && this . get ( 'enabledStack' ) ) { const id = object . getID ( ) ; const type = object . getType ( ) ; const before = { } ; const after = { } ; switch ( type ) { case 'node' : before . nodes = [ { id , visible : false } ] ; after . nodes = [ { id , visible : true } ] ; break ; case 'edge' : before . nodes = [ { id , visible : false } ] ; after . edges = [ { id , visible : true } ] ; break ; case 'combo' : before . nodes = [ { id , visible : false } ] ; after . combos = [ { id , visible : true } ] ; break ; default : break ; } this . pushStack ( 'visible' , { before , after } ) ; } } param { Item } item param { boolean } stack public hideItem ( item , stack = true ) { const itemController = this . get ( 'itemController' ) ; const object = itemController . changeItemVisibility ( item , false ) ; if ( stack && this . get ( 'enabledStack' ) ) { const id = object . getID ( ) ; const type = object . getType ( ) ; const before = { } ; const after = { } ; switch ( type ) { case 'node' : before . nodes = [ { id , visible : true } ] ; after . nodes = [ { id , visible : false } ] ; break ; case 'edge' : before . nodes = [ { id , visible : true } ] ; after . edges = [ { id , visible : false } ] ; break ; case 'combo' : before . nodes = [ { id , visible : true } ] ; after . combos = [ { id , visible : false } ] ; break ; default : break ; } this . pushStack ( 'visible' , { before , after } ) ; } } param { string | object } item public refreshItem ( item ) { const itemController = this . get ( 'itemController' ) ; itemController . refreshItem ( item ) ; } param { boolean } auto public setAutoPaint ( auto ) { const self = this ; self . set ( 'autoPaint' , auto ) ; const canvas = self . get ( 'canvas' ) ; canvas . set ( 'autoDraw' , auto ) ; } param { Item } item param { boolean } stack public remove ( item , stack = true ) { this . removeItem ( item , stack ) ; } param { Item } item param { boolean } stack public removeItem ( item , stack = true ) { let nodeItem = item ; if ( isString ( item ) ) nodeItem = this . findById ( item as string ) ; if ( ! nodeItem && isString ( item ) ) { console . warn ( 'The item to be removed does not exist!' ) ; } else if ( nodeItem ) { let type = '' ; if ( ( nodeItem as Item ) . getType ) type = ( nodeItem as Item ) . getType ( ) ; if ( stack && this . get ( 'enabledStack' ) ) { const deletedModel = { ... ( nodeItem as Item ) . getModel ( ) , itemType : type , } ; const before = { } ; switch ( type ) { case 'node' : { before . nodes = [ deletedModel as NodeConfig ] ; before . edges = [ ] ; const edges = ( nodeItem as INode ) . getEdges ( ) ; for ( let i = edges . length - 1 ; i >= 0 ; i -- ) { before . edges . push ( { ... edges [ i ] . getModel ( ) , itemType : 'edge' , } ) ; } break ; } case 'edge' : before . edges = [ deletedModel as EdgeConfig ] ; break ; case 'combo' : before . combos = [ deletedModel as ComboConfig ] ; break ; default : break ; } this . pushStack ( 'delete' , { before , after : { } , } ) ; } if ( type === 'node' ) { const model = ( nodeItem as INode ) . getModel ( ) ; if ( model . comboId ) { this . updateComboTree ( nodeItem as INode , undefined , false ) ; } } const itemController = this . get ( 'itemController' ) ; itemController . removeItem ( nodeItem ) ; if ( type === 'combo' ) { const newComboTrees = reconstructTree ( this . get ( 'comboTrees' ) ) ; this . set ( 'comboTrees' , newComboTrees ) ; } } } param { ITEM_TYPE } type param { ModelConfig } model param { boolean } stack param { boolean } sortCombo return { Item } public addItem ( type , model , stack = true , sortCombo = true ) { const currentComboSorted = this . get ( 'comboSorted' ) ; this . set ( 'comboSorted' , currentComboSorted && ! sortCombo ) ; const itemController = this . get ( 'itemController' ) ; if ( ! singleDataValidation ( type , model ) ) { return false ; } if ( model . id && this . findById ( model . id as string ) ) { console . warn ( `This item exists already. Be sure the id %c${ model . id }%c is unique.` , 'font-size: 20px; color: red;' , '' ) ; return ; } let item ; let comboTrees = this . get ( 'comboTrees' ) ; if ( ! comboTrees ) comboTrees = [ ] ; if ( type === 'combo' ) { const itemMap = this . get ( 'itemMap' ) ; let foundParent = false ; comboTrees . forEach ( ( ctree ) => { if ( foundParent ) return ; traverseTreeUp < ComboTree > ( ctree , child => { if ( model . parentId === child . id ) { foundParent = true ; const newCombo = { id : model . id as string , depth : child . depth + 2 , ... model , } ; if ( child . children ) child . children . push ( newCombo ) ; else child . children = [ newCombo ] ; model . depth = newCombo . depth ; item = itemController . addItem ( type , model ) as ICombo ; } const childItem = itemMap [ child . id ] ; if ( foundParent && childItem && childItem . getType && childItem . getType ( ) === 'combo' ) { itemController . updateCombo ( childItem , child . children ) ; } return true ; } ) ; } ) ; if ( ! foundParent ) { const newCombo = { id : model . id as string , depth : 0 , ... model , } ; model . depth = newCombo . depth ; comboTrees . push ( newCombo ) ; item = itemController . addItem ( type , model ) as ICombo ; } this . set ( 'comboTrees' , comboTrees ) ; } else if ( type === 'node' && isString ( model . comboId ) && comboTrees ) { const parentCombo = this . findById ( model . comboId as string ) ; if ( parentCombo && parentCombo . getType && parentCombo . getType ( ) !== 'combo' ) { console . warn ( `'${ model . comboId }' is not a id of a combo in the graph, the node will be added without combo.` ) ; } item = itemController . addItem ( type , model ) ; const itemMap = this . get ( 'itemMap' ) ; let foundParent = false , foundNode = false ; ( comboTrees || [ ] ) . forEach ( ( ctree ) => { if ( foundNode || foundParent ) return ; traverseTreeUp < ComboTree > ( ctree , child => { if ( child . id === model . id ) { foundNode = true ; return false ; } if ( model . comboId === child . id && ! foundNode ) { foundParent = true ; const cloneNode = clone ( model ) ; cloneNode . itemType = 'node' ; if ( child . children ) child . children . push ( cloneNode as any ) ; else child . children = [ cloneNode as any ] ; cloneNode . depth = child . depth + 1 ; } if ( foundParent && itemMap [ child . id ] . getType && itemMap [ child . id ] . getType ( ) === 'combo' ) { itemController . updateCombo ( itemMap [ child . id ] , child . children ) ; } return true ; } ) ; } ) ; } else { item = itemController . addItem ( type , model ) ; } if ( ( type === 'node' && model . comboId ) || ( type === 'combo' && model . parentId ) ) { const parentCombo = this . findById ( ( model . comboId as string ) || ( model . parentId as string ) ) as ICombo ; if ( parentCombo && parentCombo . getType && parentCombo . getType ( ) === 'combo' ) parentCombo . addChild ( item ) ; } const combos = this . get ( 'combos' ) ; if ( combos && combos . length > 0 ) { this . sortCombos ( ) ; } this . autoPaint ( ) ; if ( stack && this . get ( 'enabledStack' ) ) { const addedModel = { ... item . getModel ( ) , itemType : type , } ; const after = { } ; switch ( type ) { case 'node' : after . nodes = [ addedModel ] ; break ; case 'edge' : after . edges = [ addedModel ] ; break ; case 'combo' : after . combos = [ addedModel ] ; break ; default : break ; } this . pushStack ( 'add' , { before : { } , after , } ) ; } return item ; } param { ITEM_TYPE } type param { ModelConfig } model param { boolean } stack return { Item } public add ( type , model , stack = true , sortCombo = true ) { return this . addItem ( type , model , stack , sortCombo ) ; } param { Item } item param { Partial < NodeConfig > | EdgeConfig } cfg public updateItem ( item , cfg , stack = true ) { const itemController = this . get ( 'itemController' ) ; let currentItem ; if ( isString ( item ) ) { currentItem = this . findById ( item as string ) ; } else { currentItem = item ; } const UnupdateModel = clone ( currentItem . getModel ( ) ) ; let type = '' ; if ( currentItem . getType ) type = currentItem . getType ( ) ; const states = [ ... currentItem . getStates ( ) ] ; if ( type === 'combo' ) { each ( states , state => this . setItemState ( currentItem , state , false ) ) ; } itemController . updateItem ( currentItem , cfg ) ; if ( type === 'combo' ) { each ( states , state => this . setItemState ( currentItem , state , true ) ) ; } if ( stack && this . get ( 'enabledStack' ) ) { const before = { nodes : [ ] , edges : [ ] , combos : [ ] } ; const after = { nodes : [ ] , edges : [ ] , combos : [ ] } ; const afterModel = { id : UnupdateModel . id , ... cfg , } ; switch ( type ) { case 'node' : before . nodes . push ( UnupdateModel ) ; after . nodes . push ( afterModel ) ; break ; case 'edge' : before . edges . push ( UnupdateModel ) ; after . edges . push ( afterModel ) ; break ; case 'combo' : before . combos . push ( UnupdateModel ) ; after . combos . push ( afterModel ) ; break ; default : break ; } if ( type === 'node' ) { before . nodes . push ( UnupdateModel ) ; } this . pushStack ( 'update' , { before , after } ) ; } } param { Item } item param { Partial < NodeConfig > | EdgeConfig } cfg param { boolean } stack public update ( item , cfg , stack = true ) { this . updateItem ( item , cfg , stack ) ; } param { Item } item param { string } state param { string | boolean } value public setItemState ( item , state , value ) { if ( isString ( item ) ) { item = this . findById ( item as string ) ; } const itemController = this . get ( 'itemController' ) ; itemController . setItemState ( item , state , value ) ; const stateController = this . get ( 'stateController' ) ; if ( isString ( value ) ) { stateController . updateState ( item , `${ state }:${ value }` , true ) ; } else { stateController . updateState ( item , state , value ) ; } } param { Item } item param state public priorityState ( item , state ) { const itemController = this . get ( 'itemController' ) ; itemController . priorityState ( item , state ) ; } param { GraphData } data public data ( data ? ) { dataValidation ( data ) ; this . set ( 'data' , data ) ; } /**
     * 根据data接口的数据渲染视图
     */ public render ( ) { const self = this ; this . set ( 'comboSorted' , false ) ; const data = this . get ( 'data' ) ; if ( this . get ( 'enabledStack' ) ) { this . clearStack ( ) ; } if ( ! data ) { throw new Error ( 'data must be defined first' ) ; } const { nodes = [ ] , edges = [ ] , combos = [ ] } = data ; this . clear ( true ) ; this . emit ( 'beforerender' ) ; each ( nodes , ( node ) => { self . add ( 'node' , node , false , false ) ; } ) ; if ( combos && combos . length !== 0 ) { const comboTrees = plainCombosToTrees ( combos , nodes ) ; this . set ( 'comboTrees' , comboTrees ) ; self . addCombos ( combos ) ; } each ( edges , ( edge ) => { self . add ( 'edge' , edge , false , false ) ; } ) ; const animate = self . get ( 'animate' ) ; if ( self . get ( 'fitView' ) || self . get ( 'fitCenter' ) ) { self . set ( 'animate' , false ) ; } const layoutController = self . get ( 'layoutController' ) ; if ( layoutController ) { layoutController . layout ( success ) ; if ( this . destroyed ) return ; } else { if ( self . get ( 'fitView' ) ) { self . fitView ( ) ; } if ( self . get ( 'fitCenter' ) ) { self . fitCenter ( ) ; } self . emit ( 'afterrender' ) ; self . set ( 'animate' , animate ) ; } function success ( ) { if ( self . get ( 'fitView' ) ) { self . fitView ( ) ; } else if ( self . get ( 'fitCenter' ) ) { self . fitCenter ( ) ; } self . autoPaint ( ) ; self . emit ( 'afterrender' ) ; if ( self . get ( 'fitView' ) || self . get ( 'fitCenter' ) ) { self . set ( 'animate' , animate ) ; } } if ( ! this . get ( 'groupByTypes' ) ) { if ( combos && combos . length !== 0 ) { this . sortCombos ( ) ; } else { if ( data . nodes && data . edges && data . nodes . length < data . edges . length ) { const nodesArr = this . getNodes ( ) ; nodesArr . forEach ( node => { node . toFront ( ) ; } ) ; } else { const edgesArr = this . getEdges ( ) ; edgesArr . forEach ( edge => { edge . toBack ( ) ; } ) ; } } } if ( this . get ( 'enabledStack' ) ) { this . pushStack ( 'render' ) ; } } Param public read ( data ) { this . data ( data ) ; this . render ( ) ; } protected diffItems ( type , items , models ) { const self = this ; let item ; const itemMap = this . get ( 'itemMap' ) ; each ( models , model => { item = itemMap [ model . id ] ; if ( item ) { if ( self . get ( 'animate' ) && type === NODE ) { let containerMatrix = item . getContainer ( ) . getMatrix ( ) ; if ( ! containerMatrix ) containerMatrix = [ 1 , 0 , 0 , 0 , 1 , 0 , 0 , 0 , 1 ] ; item . set ( 'originAttrs' , { x : containerMatrix [ 6 ] , y : containerMatrix [ 7 ] , } ) ; } self . updateItem ( item , model , false ) ; } else { item = self . addItem ( type , model , false ) ; } if ( item ) ( items as { [ key ] : any [ ] ; } ) [ `${ type }s` ] . push ( item ) ; } ) ; } param { GraphData | TreeGraphData } data param { boolean } 是否入栈 return { object } public changeData ( data ? , stack = true ) { const self = this ; if ( ! dataValidation ( data ) ) { return this ; } if ( stack && this . get ( 'enabledStack' ) ) { this . pushStack ( 'changedata' , { before : self . save ( ) , after : data , } ) ; } this . set ( 'comboSorted' , false ) ; this . removeHulls ( ) ; this . getNodes ( ) . map ( node => self . clearItemStates ( node ) ) ; this . getEdges ( ) . map ( edge => self . clearItemStates ( edge ) ) ; const canvas = this . get ( 'canvas' ) ; const localRefresh = canvas . get ( 'localRefresh' ) ; canvas . set ( 'localRefresh' , false ) ; if ( ! self . get ( 'data' ) ) { self . data ( data ) ; self . render ( ) ; } const itemMap = this . get ( 'itemMap' ) ; const items = { nodes : [ ] , edges : [ ] , } ; const combosData = ( data as GraphData ) . combos ; if ( combosData ) { const comboTrees = plainCombosToTrees ( combosData , ( data as GraphData ) . nodes ) ; this . set ( 'comboTrees' , comboTrees ) ; } else { this . set ( 'comboTrees' , [ ] ) ; } this . diffItems ( 'node' , items , ( data as GraphData ) . nodes ! ) ; each ( itemMap , ( item , id ) => { itemMap [ id ] . getModel ( ) . depth = 0 ; if ( item . getType && item . getType ( ) === 'edge' ) return ; if ( item . getType && item . getType ( ) === 'combo' ) { delete itemMap [ id ] ; item . destroy ( ) ; } else if ( items . nodes . indexOf ( item ) < 0 ) { delete itemMap [ id ] ; self . remove ( item , false ) ; } } ) ; const comboItems = this . getCombos ( ) ; const combosLength = comboItems . length ; for ( let i = combosLength - 1 ; i >= 0 ; i -- ) { if ( comboItems [ i ] . destroyed ) { comboItems . splice ( i , 1 ) ; } } if ( combosData ) { self . addCombos ( combosData ) ; if ( ! this . get ( 'groupByTypes' ) ) { this . sortCombos ( ) ; } } this . diffItems ( 'edge' , items , ( data as GraphData ) . edges ! ) ; each ( itemMap , ( item , id ) => { if ( item . getType && ( item . getType ( ) === 'node' || item . getType ( ) === 'combo' ) ) return ; if ( items . edges . indexOf ( item ) < 0 ) { delete itemMap [ id ] ; self . remove ( item , false ) ; } } ) ; this . set ( { nodes : items . nodes , edges : items . edges } ) ; const layoutController = this . get ( 'layoutController' ) ; if ( layoutController ) { layoutController . changeData ( ) ; if ( self . get ( 'animate' ) && ! layoutController . getLayoutType ( ) ) { self . positionsAnimate ( ) ; } else { self . autoPaint ( ) ; } } setTimeout ( ( ) => { canvas . set ( 'localRefresh' , localRefresh ) ; } , 16 ) ; return this ; } param { ComboConfig [ ] } combos protected addCombos ( combos ) { const self = this ; const comboTrees = self . get ( 'comboTrees' ) ; const itemController = this . get ( 'itemController' ) ; itemController . addCombos ( comboTrees , combos ) ; } param combo param children public createCombo ( combo , children ) { this . set ( 'comboSorted' , false ) ; let comboId = '' ; let comboConfig ; if ( ! combo ) return ; if ( isString ( combo ) ) { comboId = combo ; comboConfig = { id : combo , } ; } else { comboId = combo . id ; if ( ! comboId ) { console . warn ( 'Create combo failed. Please assign a unique string id for the adding combo.' ) ; return ; } comboConfig = combo ; } const trees = children . map ( elementId => { const item = this . findById ( elementId ) ; const model = item . getModel ( ) ; let type = '' ; if ( item . getType ) type = item . getType ( ) ; const cItem = { id : item . getID ( ) , itemType : type as 'node' | 'combo' , } ; if ( type === 'combo' ) { ( cItem as ComboConfig ) . parentId = comboId ; model . parentId = comboId ; } else if ( type === 'node' ) { ( cItem as NodeConfig ) . comboId = comboId ; model . comboId = comboId ; } return cItem ; } ) ; comboConfig . children = trees ; this . addItem ( 'combo' , comboConfig , false ) ; this . set ( 'comboSorted' , false ) ; const comboTrees = this . get ( 'comboTrees' ) ; ( comboTrees || [ ] ) . forEach ( ctree => { traverseTreeUp < ComboTree > ( ctree , child => { if ( child . id === comboId ) { child . itemType = 'combo' ; child . children = trees as ComboTree [ ] ; return false ; } return true ; } ) ; } ) ; if ( comboTrees ) { this . sortCombos ( ) ; } } param { String | INode | ICombo } combo public uncombo ( combo ) { const self = this ; let comboItem = combo as ICombo ; if ( isString ( combo ) ) { comboItem = this . findById ( combo ) as ICombo ; } if ( ! comboItem || ( comboItem . getType && comboItem . getType ( ) !== 'combo' ) ) { console . warn ( 'The item is not a combo!' ) ; return ; } const parentId = comboItem . getModel ( ) . parentId ; let comboTrees = self . get ( 'comboTrees' ) ; if ( ! comboTrees ) comboTrees = [ ] ; const itemMap = this . get ( 'itemMap' ) ; const comboId = comboItem . get ( 'id' ) ; let treeToBeUncombo ; let brothers = [ ] ; const comboItems = this . get ( 'combos' ) ; const parentItem = this . findById ( parentId as string ) as ICombo ; comboTrees . forEach ( ctree => { if ( treeToBeUncombo ) return ; traverseTreeUp < ComboTree > ( ctree , subtree => { if ( subtree . id === comboId ) { treeToBeUncombo = subtree ; const edges = comboItem . getEdges ( ) ; edges . forEach ( edge => { this . removeItem ( edge , false ) ; } ) ; const index = comboItems . indexOf ( comboItem ) ; comboItems . splice ( index , 1 ) ; delete itemMap [ comboId ] ; comboItem . destroy ( ) ; this . emit ( 'afterremoveitem' , { item : comboItem , type : 'combo' } ) ; } if ( parentId && treeToBeUncombo && subtree . id === parentId ) { parentItem . removeCombo ( comboItem ) ; brothers = subtree . children ; const index = brothers . indexOf ( treeToBeUncombo ) ; if ( index !== - 1 ) { brothers . splice ( index , 1 ) ; } treeToBeUncombo . children ?. forEach ( child => { const item = this . findById ( child . id ) as ICombo | INode ; const childModel = item . getModel ( ) ; if ( item . getType && item . getType ( ) === 'combo' ) { child . parentId = parentId ; delete child . comboId ; childModel . parentId = parentId ; delete childModel . comboId ; } else if ( item . getType && item . getType ( ) === 'node' ) { child . comboId = parentId ; childModel . comboId = parentId ; } parentItem . addChild ( item ) ; brothers . push ( child ) ; } ) ; return false ; } return true ; } ) ; } ) ; if ( ! parentId && treeToBeUncombo ) { const index = comboTrees . indexOf ( treeToBeUncombo ) ; comboTrees . splice ( index , 1 ) ; treeToBeUncombo . children ?. forEach ( child => { child . parentId = undefined ; const childModel = this . findById ( child . id ) . getModel ( ) ; delete childModel . parentId ; delete childModel . comboId ; if ( child . itemType !== 'node' ) comboTrees . push ( child ) ; } ) ; } } /**
     * 根据节点的 bbox 更新所有 combos 的绘制，包括 combos 的位置和范围
     */ public updateCombos ( ) { const self = this ; const comboTrees = this . get ( 'comboTrees' ) ; const itemController = self . get ( 'itemController' ) ; const itemMap = self . get ( 'itemMap' ) ; ( comboTrees || [ ] ) . forEach ( ( ctree ) => { traverseTreeUp < ComboTree > ( ctree , child => { if ( ! child ) { return true ; } const childItem = itemMap [ child . id ] ; if ( childItem && childItem . getType && childItem . getType ( ) === 'combo' ) { const states = [ ... childItem . getStates ( ) ] ; each ( states , state => this . setItemState ( childItem , state , false ) ) ; itemController . updateCombo ( childItem , child . children ) ; each ( states , state => this . setItemState ( childItem , state , true ) ) ; } return true ; } ) ; } ) ; self . sortCombos ( ) ; } param { String | ICombo } combo public updateCombo ( combo ) { const self = this ; let comboItem = combo as ICombo ; let comboId ; if ( isString ( combo ) ) { comboItem = this . findById ( combo ) as ICombo ; } if ( ! comboItem || ( comboItem . getType && comboItem . getType ( ) !== 'combo' ) ) { console . warn ( 'The item to be updated is not a combo!' ) ; return ; } comboId = comboItem . get ( 'id' ) ; const comboTrees = this . get ( 'comboTrees' ) ; const itemController = self . get ( 'itemController' ) ; const itemMap = self . get ( 'itemMap' ) ; ( comboTrees || [ ] ) . forEach ( ( ctree ) => { traverseTreeUp < ComboTree > ( ctree , child => { if ( ! child ) { return true ; } const childItem = itemMap [ child . id ] ; if ( comboId === child . id && childItem && childItem . getType && childItem . getType ( ) === 'combo' ) { const states = [ ... childItem . getStates ( ) ] ; each ( states , state => { if ( childItem . getStateStyle ( state ) ) { this . setItemState ( childItem , state , false ) ; } } ) ; itemController . updateCombo ( childItem , child . children ) ; each ( states , state => { if ( childItem . getStateStyle ( state ) ) { this . setItemState ( childItem , state , true ) ; } } ) ; if ( comboId ) comboId = child . parentId ; } return true ; } ) ; } ) ; } param { String | INode | ICombo } item param { string | undefined } parentId public updateComboTree ( item , parentId ? , stack = true ) { const self = this ; this . set ( 'comboSorted' , false ) ; let uItem ; if ( isString ( item ) ) { uItem = self . findById ( item ) as INode | ICombo ; } else { uItem = item as INode | ICombo ; } const model = uItem . getModel ( ) ; const oldParentId = ( model . comboId as string ) || ( model . parentId as string ) ; let type = '' ; if ( uItem . getType ) type = uItem . getType ( ) ; if ( parentId && type === 'combo' ) { const comboTrees = this . get ( 'comboTrees' ) ; let valid = true ; let itemSubTree ; ( comboTrees || [ ] ) . forEach ( ctree => { if ( itemSubTree ) return ; traverseTree ( ctree , subTree => { if ( itemSubTree ) return ; if ( subTree . id === uItem . getID ( ) ) { itemSubTree = subTree ; } return true ; } ) ; } ) ; traverseTree ( itemSubTree , subTree => { if ( subTree . id === parentId ) { valid = false ; return false ; } return true ; } ) ; if ( ! valid ) { console . warn ( 'Failed to update the combo tree! The parentId points to a descendant of the combo!' ) ; return ; } } if ( stack && this . get ( 'enabledStack' ) ) { const beforeData = { } , afterData = { } ; if ( type === 'combo' ) { beforeData . combos = [ { id : model . id , parentId : ( model as ComboConfig ) . parentId , } , ] ; afterData . combos = [ { id : model . id , parentId , } , ] ; } else if ( type === 'node' ) { beforeData . nodes = [ { id : model . id , parentId : ( model as NodeConfig ) . comboId , } , ] ; afterData . nodes = [ { id : model . id , parentId , } , ] ; } this . pushStack ( 'updateComboTree' , { before : beforeData , after : afterData , } ) ; } if ( model . parentId || model . comboId ) { const combo = this . findById ( ( model . parentId || model . comboId ) as string ) as ICombo ; if ( combo ) { combo . removeChild ( uItem ) ; } } if ( type === 'combo' ) { model . parentId = parentId ; } else if ( type === 'node' ) { model . comboId = parentId ; } if ( parentId ) { const parentCombo = this . findById ( parentId ) as ICombo ; if ( parentCombo ) { parentCombo . addChild ( uItem as ICombo | INode ) ; } } if ( oldParentId ) { const parentCombo = this . findById ( oldParentId ) as ICombo ; if ( parentCombo ) { parentCombo . removeChild ( uItem as ICombo | INode ) ; } } const newComboTrees = reconstructTree ( this . get ( 'comboTrees' ) , model . id , parentId ) ; this . set ( 'comboTrees' , newComboTrees ) ; this . updateCombos ( ) ; } return { object } public save ( ) { const nodes = [ ] ; const edges = [ ] ; const combos = [ ] ; each ( this . get ( 'nodes' ) , ( node ) => { nodes . push ( node . getModel ( ) as NodeConfig ) ; } ) ; each ( this . get ( 'edges' ) , ( edge ) => { edges . push ( edge . getModel ( ) as EdgeConfig ) ; } ) ; each ( this . get ( 'combos' ) , ( combo ) => { combos . push ( combo . getModel ( ) as ComboConfig ) ; } ) ; return { nodes , edges , combos } ; } param { number } width param { number } height return { object } public changeSize ( width , height ) { const viewController = this . get ( 'viewController' ) ; viewController . changeSize ( width , height ) ; return this ; } /**
     * 当源数据在外部发生变更时，根据新数据刷新视图。但是不刷新节点位置
     */ public refresh ( ) { const self = this ; self . emit ( 'beforegraphrefresh' ) ; if ( self . get ( 'animate' ) ) { self . positionsAnimate ( ) ; } else { const nodes = self . get ( 'nodes' ) ; const edges = self . get ( 'edges' ) ; const vedges = self . get ( 'edges' ) ; each ( nodes , ( node ) => { node . refresh ( ) ; } ) ; each ( edges , ( edge ) => { edge . refresh ( ) ; } ) ; each ( vedges , ( vedge ) => { vedge . refresh ( ) ; } ) ; } self . emit ( 'aftergraphrefresh' ) ; self . autoPaint ( ) ; } return { INode } public getNodes ( ) { return this . get ( 'nodes' ) ; } return { IEdge } public getEdges ( ) { return this . get ( 'edges' ) ; } /**
     * 获取图中所有的 combo 实例
     */ public getCombos ( ) { return this . get ( 'combos' ) ; } param comboId public getComboChildren ( combo ) { if ( isString ( combo ) ) { combo = this . findById ( combo ) as ICombo ; } if ( ! combo || ( combo . getType && combo . getType ( ) !== 'combo' ) ) { console . warn ( 'The combo does not exist!' ) ; return ; } return combo . getChildren ( ) ; } /**
     * 根据 graph 上的 animateCfg 进行视图中节点位置动画接口
     */ public positionsAnimate ( ) { const self = this ; self . emit ( 'beforeanimate' ) ; const animateCfg = self . get ( 'animateCfg' ) ; const { onFrame } = animateCfg ; const nodes = self . getNodes ( ) ; const toNodes = nodes . map ( node => { const model = node . getModel ( ) ; return { id : model . id , x : model . x , y : model . y , } ; } ) ; if ( self . isAnimating ( ) ) { self . stopAnimate ( ) ; } const canvas = self . get ( 'canvas' ) ; canvas . animate ( ( ratio ) => { each ( toNodes , data => { const node = self . findById ( data . id ) ; if ( ! node || node . destroyed ) { return ; } let originAttrs = node . get ( 'originAttrs' ) ; const model = node . get ( 'model' ) ; if ( ! originAttrs ) { let containerMatrix = node . getContainer ( ) . getMatrix ( ) ; if ( ! containerMatrix ) containerMatrix = [ 1 , 0 , 0 , 0 , 1 , 0 , 0 , 0 , 1 ] ; originAttrs = { x : containerMatrix [ 6 ] , y : containerMatrix [ 7 ] , } ; node . set ( 'originAttrs' , originAttrs ) ; } if ( onFrame ) { const attrs = onFrame ( node , ratio , data , originAttrs ) ; node . set ( 'model' , Object . assign ( model , attrs ) ) ; } else { model . x = originAttrs . x + ( data . x - originAttrs . x ) * ratio ; model . y = originAttrs . y + ( data . y - originAttrs . y ) * ratio ; } } ) ; self . refreshPositions ( ) ; } , { duration : animateCfg . duration , easing : animateCfg . easing , callback : ( ) => { each ( nodes , ( node ) => { node . set ( 'originAttrs' , null ) ; } ) ; if ( animateCfg . callback ) { animateCfg . callback ( ) ; } self . emit ( 'afteranimate' ) ; self . animating = false ; } , } ) ; } /**
     * 当节点位置在外部发生改变时，刷新所有节点位置，重计算边
     */ public refreshPositions ( ) { const self = this ; self . emit ( 'beforegraphrefreshposition' ) ; const nodes = self . get ( 'nodes' ) ; const edges = self . get ( 'edges' ) ; const vedges = self . get ( 'vedges' ) ; const combos = self . get ( 'combos' ) ; let model ; const updatedNodes = { } ; each ( nodes , ( node ) => { model = node . getModel ( ) as NodeConfig ; const originAttrs = node . get ( 'originAttrs' ) ; if ( originAttrs && model . x === originAttrs . x && model . y === originAttrs . y ) { return ; } const changed = node . updatePosition ( { x : model . x ! , y : model . y ! } ) ; updatedNodes [ model . id ] = changed ; if ( model . comboId ) updatedNodes [ model . comboId ] = updatedNodes [ model . comboId ] || changed ; } ) ; if ( combos && combos . length !== 0 ) { self . updateCombos ( ) ; } each ( edges , ( edge ) => { const sourceModel = edge . getSource ( ) . getModel ( ) ; const target = edge . getTarget ( ) ; if ( ! isPlainObject ( target ) ) { const targetModel = ( target as INode | ICombo ) . getModel ( ) ; if ( updatedNodes [ sourceModel . id as string ] || updatedNodes [ targetModel . id as string ] || edge . getModel ( ) . isComboEdge ) { edge . refresh ( ) ; } } } ) ; each ( vedges , ( vedge ) => { vedge . refresh ( ) ; } ) ; self . emit ( 'aftergraphrefreshposition' ) ; self . autoPaint ( ) ; } public stopAnimate ( ) { this . get ( 'canvas' ) . stopAnimate ( ) ; } public isAnimating ( ) { return this . animating ; } return { number } public getZoom ( ) { const matrix = this . get ( 'group' ) . getMatrix ( ) ; return matrix ? matrix [ 0 ] : 1 ; } return { string } public getCurrentMode ( ) { const modeController = this . get ( 'modeController' ) ; return modeController . getMode ( ) ; } param { string } mode return { object } public setMode ( mode ) { const modeController = this . get ( 'modeController' ) ; modeController . setMode ( mode ) ; return this ; } return { object } public clear ( avoidEmit = false ) { this . get ( 'canvas' ) ?. clear ( ) ; this . initGroups ( ) ; this . set ( { itemMap : { } , nodes : [ ] , edges : [ ] , groups : [ ] , combos : [ ] , comboTrees : [ ] } ) ; if ( ! avoidEmit ) this . emit ( 'afterrender' ) ; return this ; } param { object } cfg param { 'center' | 'begin' } align param { IPoint } alignPoint public updateLayout ( cfg , align ? , alignPoint ? ) { const layoutController = this . get ( 'layoutController' ) ; if ( isString ( cfg ) ) { cfg = { type : cfg , } ; } if ( align ) { let toPoint = alignPoint ; if ( ! toPoint ) { if ( align === 'begin' ) toPoint = { x : 0 , y : 0 } ; else toPoint = { x : this . getWidth ( ) / 2 , y : this . getHeight ( ) / 2 } ; } toPoint = this . getPointByCanvas ( toPoint . x , toPoint . y ) ; const forceTypes = [ 'force' , 'gForce' , 'fruchterman' ] ; if ( forceTypes . includes ( cfg . type ) || ( ! cfg . type && forceTypes . includes ( layoutController ?. layoutType ) ) ) { cfg . center = [ toPoint . x , toPoint . y ] ; } else { this . once ( 'afterlayout' , e => { const matrix = this . getGroup ( ) . getMatrix ( ) || [ 1 , 0 , 0 , 0 , 1 , 0 , 0 , 0 , 1 ] ; toPoint . x = toPoint . x * matrix [ 0 ] + matrix [ 6 ] ; toPoint . y = toPoint . y * matrix [ 0 ] + matrix [ 7 ] ; const { minX , maxX , minY , maxY } = this . getGroup ( ) . getCanvasBBox ( ) ; const bboxPoint = { x : ( minX + maxX ) / 2 , y : ( minY + maxY ) / 2 } ; if ( align === 'begin' ) { bboxPoint . x = minX ; bboxPoint . y = minY ; } this . translate ( toPoint . x - bboxPoint . x , toPoint . y - bboxPoint . y ) ; } ) ; } } const oriLayoutCfg = this . get ( 'layout' ) ; const layoutCfg = { } ; Object . assign ( layoutCfg , oriLayoutCfg , cfg ) ; this . set ( 'layout' , layoutCfg ) ; if ( layoutController . isLayoutTypeSame ( layoutCfg ) && layoutCfg . gpuEnabled === oriLayoutCfg . gpuEnabled ) { layoutController . updateLayoutCfg ( layoutCfg ) ; } else { layoutController . changeLayout ( layoutCfg ) ; } } /**
     * 销毁布局，changeData 时不会再使用原来的布局方法对新数据进行布局
     */ public destroyLayout ( ) { const layoutController = this . get ( 'layoutController' ) ; layoutController . destroyLayout ( ) ; } /**
     * 重新以当前示例中配置的属性进行一次布局
     */ public layout ( ) { const layoutController = this . get ( 'layoutController' ) ; const layoutCfg = this . get ( 'layout' ) ; if ( ! layoutCfg || ! layoutController ) return ; if ( layoutCfg . workerEnabled ) { layoutController . layout ( ) ; return ; } if ( layoutController . layoutMethod ) { layoutController . relayout ( true ) ; } else { layoutController . layout ( ) ; } } param { string | ICombo } combo public collapseCombo ( combo ) { if ( isString ( combo ) ) { combo = this . findById ( combo ) as ICombo ; } if ( ! combo ) { console . warn ( 'The combo to be collapsed does not exist!' ) ; return ; } this . emit ( 'beforecollapseexpandcombo' , { action : 'expand' , item : combo } ) ; const comboModel = combo . getModel ( ) ; const itemController = this . get ( 'itemController' ) ; itemController . collapseCombo ( combo ) ; comboModel . collapsed = true ; const edges = this . getEdges ( ) . concat ( this . get ( 'vedges' ) ) ; let cnodes = [ ] ; let ccombos = [ ] ; const comboTrees = this . get ( 'comboTrees' ) ; let found = false ; ( comboTrees || [ ] ) . forEach ( ctree => { if ( found ) return ; traverseTree ( ctree , subTree => { if ( found && subTree . depth <= comboModel . depth ) return false ; if ( comboModel . id === subTree . id ) found = true ; if ( found ) { const item = this . findById ( subTree . id ) as ICombo ; if ( item && item . getType && item . getType ( ) === 'combo' ) { cnodes = cnodes . concat ( item . getNodes ( ) ) ; ccombos = ccombos . concat ( item . getCombos ( ) ) ; } } return true ; } ) ; } ) ; const edgeWeightMap = { } ; const addedVEdges = [ ] ; edges . forEach ( edge => { if ( edge . isVisible ( ) && ! edge . getModel ( ) . isVEdge ) return ; let source = edge . getSource ( ) ; let target = edge . getTarget ( ) ; if ( ( ( cnodes . includes ( source ) || ccombos . includes ( source ) ) && ! cnodes . includes ( target ) && ! ccombos . includes ( target ) ) || source . getModel ( ) . id === comboModel . id ) { const edgeModel = edge . getModel ( ) ; if ( edgeModel . isVEdge ) { this . removeItem ( edge , false ) ; return ; } let targetModel = target . getModel ( ) ; while ( ! target . isVisible ( ) ) { target = this . findById ( ( targetModel . parentId as string ) || ( targetModel . comboId as string ) ) as ICombo ; if ( ! target || ( ! targetModel . parentId && ! targetModel . comboId ) ) return ; targetModel = target . getModel ( ) ; } const targetId = targetModel . id ; if ( edgeWeightMap [ `${ comboModel . id }-${ targetId }` ] ) { edgeWeightMap [ `${ comboModel . id }-${ targetId }` ] += edgeModel . size || 1 ; return ; } const vedge = this . addItem ( 'vedge' , { source : comboModel . id , target : targetId , isVEdge : true , } , false ) ; edgeWeightMap [ `${ comboModel . id }-${ targetId }` ] = edgeModel . size || 1 ; addedVEdges . push ( vedge ) ; } else if ( ( ! cnodes . includes ( source ) && ! ccombos . includes ( source ) && ( cnodes . includes ( target ) || ccombos . includes ( target ) ) ) || target . getModel ( ) . id === comboModel . id ) { const edgeModel = edge . getModel ( ) ; if ( edgeModel . isVEdge ) { this . removeItem ( edge , false ) ; return ; } let sourceModel = source . getModel ( ) ; while ( ! source . isVisible ( ) ) { source = this . findById ( ( sourceModel . parentId as string ) || ( sourceModel . comboId as string ) ) as ICombo ; if ( ! source || ( ! sourceModel . parentId && ! sourceModel . comboId ) ) return ; sourceModel = source . getModel ( ) ; } const sourceId = sourceModel . id ; if ( edgeWeightMap [ `${ sourceId }-${ comboModel . id }` ] ) { edgeWeightMap [ `${ sourceId }-${ comboModel . id }` ] += edgeModel . size || 1 ; return ; } const vedge = this . addItem ( 'vedge' , { target : comboModel . id , source : sourceId , isVEdge : true , } , false ) ; edgeWeightMap [ `${ sourceId }-${ comboModel . id }` ] = edgeModel . size || 1 ; addedVEdges . push ( vedge ) ; } } ) ; addedVEdges . forEach ( vedge => { const vedgeModel = vedge . getModel ( ) ; this . updateItem ( vedge , { size : edgeWeightMap [ `${ vedgeModel . source }-${ vedgeModel . target }` ] , } , false ) ; } ) ; this . emit ( 'aftercollapseexpandcombo' , { action : 'collapse' , item : combo } ) ; } param { string | ICombo } combo public expandCombo ( combo ) { if ( isString ( combo ) ) { combo = this . findById ( combo ) as ICombo ; } if ( ! combo || ( combo . getType && combo . getType ( ) !== 'combo' ) ) { console . warn ( 'The combo to be collapsed does not exist!' ) ; return ; } this . emit ( 'beforecollapseexpandcombo' , { action : 'expand' , item : combo } ) ; const comboModel = combo . getModel ( ) ; const itemController = this . get ( 'itemController' ) ; itemController . expandCombo ( combo ) ; comboModel . collapsed = false ; const edges = this . getEdges ( ) . concat ( this . get ( 'vedges' ) ) ; let cnodes = [ ] ; let ccombos = [ ] ; const comboTrees = this . get ( 'comboTrees' ) ; let found = false ; ( comboTrees || [ ] ) . forEach ( ctree => { if ( found ) return ; traverseTree ( ctree , subTree => { if ( found && subTree . depth <= comboModel . depth ) return false ; if ( comboModel . id === subTree . id ) found = true ; if ( found ) { const item = this . findById ( subTree . id ) as ICombo ; if ( item && item . getType && item . getType ( ) === 'combo' ) { cnodes = cnodes . concat ( item . getNodes ( ) ) ; ccombos = ccombos . concat ( item . getCombos ( ) ) ; } } return true ; } ) ; } ) ; const edgeWeightMap = { } ; const addedVEdges = { } ; edges . forEach ( edge => { if ( edge . isVisible ( ) && ! edge . getModel ( ) . isVEdge ) return ; let source = edge . getSource ( ) ; let target = edge . getTarget ( ) ; let sourceId = source . get ( 'id' ) ; let targetId = target . get ( 'id' ) ; if ( ( ( cnodes . includes ( source ) || ccombos . includes ( source ) ) && ! cnodes . includes ( target ) && ! ccombos . includes ( target ) ) || sourceId === comboModel . id ) { if ( edge . getModel ( ) . isVEdge ) { this . removeItem ( edge , false ) ; return ; } let targetModel = target . getModel ( ) ; while ( ! target . isVisible ( ) ) { target = this . findById ( ( targetModel . comboId as string ) || ( targetModel . parentId as string ) ) as ICombo ; if ( ! target || ( ! targetModel . parentId && ! targetModel . comboId ) ) { return ; } targetModel = target . getModel ( ) ; } targetId = targetModel . id ; let sourceModel = source . getModel ( ) ; while ( ! source . isVisible ( ) ) { source = this . findById ( ( sourceModel . comboId as string ) || ( sourceModel . parentId as string ) ) as ICombo ; if ( ! source || ( ! sourceModel . parentId && ! sourceModel . comboId ) ) { return ; } if ( sourceModel . comboId === comboModel . id || sourceModel . parentId === comboModel . id ) { break ; } sourceModel = source . getModel ( ) ; } sourceId = sourceModel . id ; if ( targetId ) { const vedgeId = `${ sourceId }-${ targetId }` ; if ( edgeWeightMap [ vedgeId ] ) { edgeWeightMap [ vedgeId ] += edge . getModel ( ) . size || 1 ; this . updateItem ( addedVEdges [ vedgeId ] , { size : edgeWeightMap [ vedgeId ] , } , false ) ; return ; } const vedge = this . addItem ( 'vedge' , { source : sourceId , target : targetId , isVEdge : true , } , false ) ; edgeWeightMap [ vedgeId ] = edge . getModel ( ) . size || 1 ; addedVEdges [ vedgeId ] = vedge ; } } else if ( ( ! cnodes . includes ( source ) && ! ccombos . includes ( source ) && ( cnodes . includes ( target ) || ccombos . includes ( target ) ) ) || targetId === comboModel . id ) { if ( edge . getModel ( ) . isVEdge ) { this . removeItem ( edge , false ) ; return ; } let sourceModel = source . getModel ( ) ; while ( ! source . isVisible ( ) ) { source = this . findById ( ( sourceModel . comboId as string ) || ( sourceModel . parentId as string ) ) as ICombo ; if ( ! source || ( ! sourceModel . parentId && ! sourceModel . comboId ) ) { return ; } sourceModel = source . getModel ( ) ; } sourceId = sourceModel . id ; let targetModel = target . getModel ( ) ; while ( ! target . isVisible ( ) ) { target = this . findById ( ( targetModel . comboId as string ) || ( targetModel . parentId as string ) ) as ICombo ; if ( ! target || ( ! targetModel . parentId && ! targetModel . comboId ) ) { return ; } if ( targetModel . comboId === comboModel . id || targetModel . parentId === comboModel . id ) { break ; } targetModel = target . getModel ( ) ; } targetId = targetModel . id ; if ( sourceId ) { const vedgeId = `${ sourceId }-${ targetId }` ; if ( edgeWeightMap [ vedgeId ] ) { edgeWeightMap [ vedgeId ] += edge . getModel ( ) . size || 1 ; this . updateItem ( addedVEdges [ vedgeId ] , { size : edgeWeightMap [ vedgeId ] , } , false ) ; return ; } const vedge = this . addItem ( 'vedge' , { target : targetId , source : sourceId , isVEdge : true , } , false ) ; edgeWeightMap [ vedgeId ] = edge . getModel ( ) . size || 1 ; addedVEdges [ vedgeId ] = vedge ; } } else if ( ( cnodes . includes ( source ) || ccombos . includes ( source ) ) && ( cnodes . includes ( target ) || ccombos . includes ( target ) ) ) { if ( source . isVisible ( ) && target . isVisible ( ) ) { edge . show ( ) ; } } } ) ; this . emit ( 'aftercollapseexpandcombo' , { action : 'expand' , item : combo } ) ; } public collapseExpandCombo ( combo ) { if ( isString ( combo ) ) { combo = this . findById ( combo ) as ICombo ; } if ( ! combo || ( combo . getType && combo . getType ( ) !== 'combo' ) ) return ; const comboModel = combo . getModel ( ) ; let parentItem = this . findById ( comboModel . parentId as string ) ; while ( parentItem ) { const parentModel = parentItem . getModel ( ) ; if ( parentModel . collapsed ) { console . warn ( `Fail to expand the combo since it's ancestor combo is collapsed.` ) ; parentItem = undefined ; return ; } parentItem = this . findById ( parentModel . parentId as string ) ; } const collapsed = comboModel . collapsed ; if ( collapsed ) { this . expandCombo ( combo ) ; } else { this . collapseCombo ( combo ) ; } this . updateCombo ( combo ) ; } param { GraphData } data protected sortCombos ( ) { const comboSorted = this . get ( 'comboSorted' ) ; if ( comboSorted ) return ; this . set ( 'comboSorted' , true ) ; const depthMap = [ ] ; const dataDepthMap = { } ; const comboTrees = this . get ( 'comboTrees' ) ; ( comboTrees || [ ] ) . forEach ( cTree => { traverseTree ( cTree , child => { if ( depthMap [ child . depth ] ) depthMap [ child . depth ] . push ( child . id ) ; else depthMap [ child . depth ] = [ child . id ] ; dataDepthMap [ child . id ] = child . depth ; return true ; } ) ; } ) ; const edges = this . getEdges ( ) . concat ( this . get ( 'vedges' ) ) ; ( edges || [ ] ) . forEach ( edgeItem => { const edge = edgeItem . getModel ( ) ; const sourceDepth = dataDepthMap [ edge . source as string ] || 0 ; const targetDepth = dataDepthMap [ edge . target as string ] || 0 ; const depth = Math . max ( sourceDepth , targetDepth ) ; if ( depthMap [ depth ] ) depthMap [ depth ] . push ( edge . id ) ; else depthMap [ depth ] = [ edge . id ] ; } ) ; depthMap . forEach ( array => { if ( ! array || ! array . length ) return ; for ( let i = array . length - 1 ; i >= 0 ; i -- ) { const item = this . findById ( array [ i ] ) ; if ( item ) item . toFront ( ) ; } } ) ; } param { ( string | INode ) } node returns { INode [ ] } memberof public getNeighbors ( node , type ? ) { let item = node as INode ; if ( isString ( node ) ) { item = this . findById ( node ) as INode ; } return item . getNeighbors ( type ) ; } param { ( string | INode ) } node param { ( 'in' | 'out' | 'total' | 'all' | undefined ) } 度数类型 returns { Number | Object } memberof public getNodeDegree ( node , type = undefined , refresh = false ) { let item = node as INode ; if ( isString ( node ) ) { item = this . findById ( node ) as INode ; } let degrees = this . get ( 'degrees' ) ; if ( ! degrees || refresh ) { degrees = getDegree ( this . save ( ) as any ) ; this . set ( 'degrees' , degrees ) ; } const nodeDegrees = degrees [ item . getID ( ) ] ; let res = 0 ; if ( ! nodeDegrees ) { return 0 ; } switch ( type ) { case 'in' : res = nodeDegrees . inDegree ; break ; case 'out' : res = nodeDegrees . outDegree ; break ; case 'all' : res = nodeDegrees ; break ; default : res = nodeDegrees . degree ; break ; } return res ; } public getUndoStack ( ) { return this . undoStack ; } public getRedoStack ( ) { return this . redoStack ; } /**
     * 获取 undo 和 redo 栈的数据
     */ public getStackData ( ) { if ( ! this . get ( 'enabledStack' ) ) { return null ; } return { undoStack : this . undoStack . toArray ( ) , redoStack : this . redoStack . toArray ( ) , } ; } /**
     * 清空 undo stack & redo stack
     */ public clearStack ( ) { if ( this . get ( 'enabledStack' ) ) { this . undoStack . clear ( ) ; this . redoStack . clear ( ) ; } } param action param data param stackType public pushStack ( action = 'update' , data ? , stackType = 'undo' ) { if ( ! this . get ( 'enabledStack' ) ) { console . warn ( '请先启用 undo & redo 功能，在实例化 Graph 时候配置 enabledStack: true !' ) ; return ; } const stackData = data ? clone ( data ) : { before : { } , after : clone ( this . save ( ) ) , } ; if ( stackType === 'redo' ) { this . redoStack . push ( { action , data : stackData , } ) ; } else { this . undoStack . push ( { action , data : stackData , } ) ; } this . emit ( 'stackchange' , { undoStack : this . undoStack , redoStack : this . redoStack , } ) ; } param { boolean } cache param { boolean } directed returns { Matrix } memberof public getAdjMatrix ( cache = true , directed ? ) { if ( directed === undefined ) directed = this . get ( 'directed' ) ; let currentAdjMatrix = this . get ( 'adjMatrix' ) ; if ( ! currentAdjMatrix || ! cache ) { currentAdjMatrix = getAdjacentMatrix ( this . save ( ) as any , directed ) ; this . set ( 'adjMatrix' , currentAdjMatrix ) ; } return currentAdjMatrix ; } param { boolean } cache param { boolean } directed returns { Matrix } memberof public getShortestPathMatrix ( cache = true , directed ? ) { if ( directed === undefined ) directed = this . get ( 'directed' ) ; let currentAdjMatrix = this . get ( 'adjMatrix' ) ; let currentShourtestPathMatrix = this . get ( 'shortestPathMatrix' ) ; if ( ! currentAdjMatrix || ! cache ) { currentAdjMatrix = getAdjacentMatrix ( this . save ( ) as any , directed ) ; this . set ( 'adjMatrix' , currentAdjMatrix ) ; } if ( ! currentShourtestPathMatrix || ! cache ) { currentShourtestPathMatrix = floydWarshall ( this . save ( ) as any , directed ) ; this . set ( 'shortestPathMatrix' , currentShourtestPathMatrix ) ; } return currentShourtestPathMatrix ; } /**
     * 重新定义监听函数，复写参数类型
     */ public on < T = IG6GraphEvent > ( eventName , callback , once ? ) { return super . on ( eventName , callback , once ) ; } /**
     * 销毁画布
     */ public destroy ( ) { this . clear ( ) ; this . clearStack ( ) ; this . get ( 'itemController' ) ?. destroy ( ) ; this . get ( 'modeController' ) ?. destroy ( ) ; this . get ( 'viewController' ) ?. destroy ( ) ; this . get ( 'stateController' ) ?. destroy ( ) ; this . get ( 'canvas' ) ?. destroy ( ) ; ( this . cfg as any ) = null ; this . destroyed = true ; this . redoStack = null ; this . undoStack = null ; } param cfg public createHull ( cfg ) { if ( ! cfg . members || cfg . members . length < 1 ) { console . warn ( 'Create hull failed! The members is empty.' ) ; return ; } let parent = this . get ( 'hullGroup' ) ; let hullMap = this . get ( 'hullMap' ) ; if ( ! hullMap ) { hullMap = { } ; this . set ( 'hullMap' , hullMap ) ; } if ( ! parent || parent . get ( 'destroyed' ) ) { parent = this . get ( 'group' ) . addGroup ( { id : 'hullGroup' , } ) ; parent . toBack ( ) ; this . set ( 'hullGroup' , parent ) ; } if ( hullMap [ cfg . id ] ) { console . warn ( 'Existed hull id.' ) ; return hullMap [ cfg . id ] ; } const group = parent . addGroup ( { id : `${ cfg . id }-container` , } ) ; const hull = new Hull ( this , { ... cfg , group , } ) ; const hullId = hull . id ; hullMap [ hullId ] = hull ; return hull ; } return { [ key : string ] public getHulls ( ) { return this . get ( 'hullMap' ) ; } return public getHullById ( hullId ) { return this . get ( 'hullMap' ) [ hullId ] ; } public removeHull ( hull ) { let hullInstance ; if ( isString ( hull ) ) { hullInstance = this . getHullById ( hull ) ; } else { hullInstance = hull ; } delete this . get ( 'hullMap' ) ?. [ hullInstance . id ] ; hullInstance . destroy ( ) ; } public removeHulls ( ) { const hulls = this . getHulls ( ) ; if ( ! hulls || ! Object . keys ( hulls ) . length ) return ; Object . keys ( hulls ) . forEach ( key => { const hull = hulls [ key ] ; hull . destroy ( ) ; } ) ; this . set ( 'hullMap' , { } ) ; } }