import "./../style/visual.less" ; import "@babel/polyfill" ; import * as d3 from "d3" ; import * as _ from "lodash" ; import powerbi from "powerbi-visuals-api" ; import IViewport = powerbi . IViewport ; import IColorPalette = powerbi . extensibility . IColorPalette ; import IVisual = powerbi . extensibility . visual . IVisual ; import DataView = powerbi . DataView ; import DataViewMetadataColumn = powerbi . DataViewMetadataColumn ; import DataViewCategoryColumn = powerbi . DataViewCategoryColumn ; import DataViewValueColumn = powerbi . DataViewValueColumn ; import VisualTooltipDataItem = powerbi . extensibility . VisualTooltipDataItem ; import VisualUpdateOptions = powerbi . extensibility . visual . VisualUpdateOptions ; import EnumerateVisualObjectInstancesOptions = powerbi . EnumerateVisualObjectInstancesOptions ; import VisualObjectInstanceEnumeration = powerbi . VisualObjectInstanceEnumeration ; import VisualConstructorOptions = powerbi . extensibility . visual . VisualConstructorOptions ; import { pixelConverter as PixelConverter } from "powerbi-visuals-utils-typeutils" ; import * as SVGUtil from "powerbi-visuals-utils-svgutils" ; import ClassAndSelector = SVGUtil . CssConstants . ClassAndSelector ; import createClassAndSelector = SVGUtil . CssConstants . createClassAndSelector ; import { IMargin , manipulation } from "powerbi-visuals-utils-svgutils" ; import translate = manipulation . translate ; import { valueFormatter as vf , textMeasurementService as tms , textUtil } from "powerbi-visuals-utils-formattingutils" ; import textMeasurementService = tms . textMeasurementService ; import TextProperties = tms . TextProperties ; import IValueFormatter = vf . IValueFormatter ; import valueFormatter = vf . valueFormatter ; import { TooltipEventArgs , ITooltipServiceWrapper , createTooltipServiceWrapper } from "powerbi-visuals-utils-tooltiputils" ; import { ColorHelper } from "powerbi-visuals-utils-colorutils" ; import { ForceGraphColumns } from "./columns" ; import { ForceGraphSettings , LinkColorType } from "./settings" ; import { ForceGraphTooltipsFactory } from "./tooltipsFactory" ; import { ForceGraphData , ForceGraphNode , ForceGraphNodes , ForceGraphLink , LinkedByName , ITextRect } from "./dataInterfaces" ; import ISelectionManager = powerbi . extensibility . ISelectionManager ; import IVisualHost = powerbi . extensibility . visual . IVisualHost ; import ISelectionId = powerbi . extensibility . ISelectionId ; export class ForceGraph implements IVisual { private static Count : number = 0 ; private static VisualClassName : string = "forceGraph" ; private static getEvent = ( ) => require ( "d3" ) . event ; private static DefaultImage : string = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAHNDTTxAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAACuUExURQAAAMbGxvLy8sfHx/Hx8fLy8vHx8cnJycrKyvHx8fHx8cvLy/Ly8szMzM3NzfHx8dDQ0PHx8fLy8vHx8e/v79LS0tPT0/Ly8tTU1NXV1dbW1vHx8fHx8fDw8NjY2PT09PLy8vLy8vHx8fLy8vHx8fHx8enp6fDw8PLy8uPj4+Tk5OXl5fHx8b+/v/Pz8+bm5vHx8ejo6PLy8vHx8fLy8sTExPLy8vLy8sXFxfHx8YCtMbUAAAA6dFJOUwD/k/+b7/f///+r/////0z/w1RcEP//ZP///4fj/v8Yj3yXn/unDEhQ////YP9Y/8//aIMU/9+L/+fzC4s1AAAACXBIWXMAABcRAAAXEQHKJvM/AAABQElEQVQoU5WS61LCMBCFFymlwSPKVdACIgWkuNyL+P4v5ibZ0jKjP/xm0uw5ySa7mRItAhnMoIC5TwQZdCZiZjcoC8WU6EVsmZgzoqGdxafgvJAvjUXCb2M+0cXNsd/GDarZqSf7av3M2P1E3xhfLkPUvLD5joEYwVVJQXM6+9McWUwLf4nDTCQZAy96UoDjNI/jhl3xPLbQamu8xD7iaIsPKw7GJ7KZEnWLY3Gi8EFj5nqibXnwD5VEGjJXk5sbpLppfvvo1RazQVrhSopPK4TODrtnjS3dY4ic8KurruWQYF+UG60BacexTMyT2jlNg41dOmKvTpkUd/Jevy7ZxQ61ULRUpoododx8GeDPvIrktbFVdUsK6f8Na5VlVpjZJtowTXVy7kfXF5wCaV1tqXAFuIdWJu+JviaQzNzfQvQDGKRXXEmy83cAAAAASUVORK5CYII=" ; private static MinViewport : IViewport = { width : 1 , height : 1 } ; private static ImageViewport : IViewport = { width : 24 , height : 24 } ; private static ImagePosition : number = - 12 ; private static MinNodeWeight : number = 5 ; private static MinCharge : number = - 100 ; private static MaxCharge : number = - 0.1 ; private static MinDecimalPlaces : number = 0 ; private static MaxDecimalPlaces : number = 5 ; private static GravityFactor : number = 100 ; private static LinkDistance : number = 100 ; private static HoverOpacity : number = 0.3 ; private static DefaultOpacity : number = 1 ; private static DefaultLinkColor : string = "#bbb" ; private static DefaultLinkHighlightColor : string = "#f00" ; private static DefaultLinkThickness : string = "1.5px" ; private static LabelsFontFamily : string = "sans-serif" ; private static MinRangeValue : number = 1 ; private static MaxRangeValue : number = 10 ; private static DefaultValueOfExistingLink : number = 1 ; private static DefaultLinkType : string = "" ; private static MinWeight : number = 0 ; private static MaxWeight : number = 0 ; private static DefaultSourceType : string = "" ; private static DefaultTargetType : string = "" ; private static StartOffset : string = "25%" ; private static DefaultLinkFillColor : string = "#000" ; private static LinkTextAnchor : string = "middle" ; private static DefaultLabelX : number = 12 ; private static DefaultLabelDy : string = ".35em" ; private static DefaultLabelText : string = "" ; private static ResolutionFactor : number = 20 ; private static ResolutionFactorBoundByBox : number = 0.9 ; private static LinkSelector : ClassAndSelector = createClassAndSelector ( "link" ) ; private static LinkLabelHolderSelector : ClassAndSelector = createClassAndSelector ( "linklabelholder" ) ; private static LinkLabelSelector : ClassAndSelector = createClassAndSelector ( "linklabel" ) ; private static NodeSelector : ClassAndSelector = createClassAndSelector ( "node" ) ; private static NoAnimationLimit : number = 200 ; private selectionManager : ISelectionManager ; private host : IVisualHost ; private static get Href ( ) : string { return window . location . href . replace ( window . location . hash , "" ) ; } private static substractMargin ( viewport , margin ) { return { width : Math . max ( viewport . width - ( margin . left + margin . right ) , 0 ) , height : Math . max ( viewport . height - ( margin . top + margin . bottom ) , 0 ) } ; } private get settings ( ) : ForceGraphSettings { return this . data && this . data . settings ; } private defaultYPosition : number = - 6 ; private defaultYOffset : number = - 2 ; private container : d3 . Selection < any > ; private paths : d3 . Selection < ForceGraphLink > ; private nodes : d3 . Selection < ForceGraphNode > ; private forceLayout : d3 . layout . Force < ForceGraphLink , ForceGraphNode > ; private colorPalette : IColorPalette ; private colorHelper : ColorHelper ; private uniqieId : string = `_${ ForceGraph . Count ++ }_` ; private marginValue : IMargin ; private get margin ( ) : IMargin { return this . marginValue || { left : 0 , right : 0 , top : 0 , bottom : 0 } ; } private set margin ( value ) { this . marginValue = { ... value } ; this . viewportInValue = ForceGraph . substractMargin ( this . viewport , this . margin ) ; } private viewportValue : IViewport ; private get viewport ( ) : IViewport { return this . viewportValue || { ... ForceGraph . MinViewport } ; } private set viewport ( viewport ) { this . viewportValue = ForceGraph . getViewport ( viewport ) ; this . viewportInValue = ForceGraph . getViewport ( ForceGraph . substractMargin ( this . viewport , this . margin ) ) ; } private viewportInValue : IViewport ; private get viewportIn ( ) : IViewport { return this . viewportInValue || this . viewport ; } private data : ForceGraphData ; private tooltipServiceWrapper : ITooltipServiceWrapper ; constructor ( options ) { this . selectionManager = options . host . createSelectionManager ( ) ; this . host = options . host ; this . init ( options ) ; } private init ( options ) { const root = d3 . select ( options . element ) ; this . colorPalette = options . host . colorPalette ; this . colorHelper = new ColorHelper ( this . colorPalette ) ; this . tooltipServiceWrapper = createTooltipServiceWrapper ( options . host . tooltipService , options . element , undefined , ForceGraph . getEvent ) ; this . forceLayout = d3 . layout . force < ForceGraphLink , ForceGraphNode > ( ) ; this . forceLayout . drag ( ) . on ( "dragstart" , ( ( d ) => { this . forceLayout . stop ( ) ; d . isDrag = true ; this . fadeNode ( d ) ; } ) ) . on ( "dragend" , ( ( d ) => { this . forceLayout . tick ( ) ; this . forceLayout . resume ( ) ; d . isDrag = false ; this . fadeNode ( d ) ; } ) ) . on ( "drag" , ( d ) => { d . px += ( d3 . event as any ) . dx ; d . py += ( d3 . event as any ) . dy ; d . x += ( d3 . event as any ) . dx ; d . y += ( d3 . event as any ) . dy ; this . fadeNode ( d ) ; this . forceLayout . tick ( ) ; } ) ; const svg = root . append ( "svg" ) . attr ( { width : "100%" , height : "100%" } ) . classed ( ForceGraph . VisualClassName , true ) ; this . container = svg . append ( "g" ) . classed ( "chartContainer" , true ) ; } private static getViewport ( viewport ) { const { width , height } = viewport ; return { width : Math . max ( ForceGraph . MinViewport . width , width ) , height : Math . max ( ForceGraph . MinViewport . height , height ) } ; } private scale1to10 ( value ) { let scale = d3 . scale . linear ( ) . domain ( [ this . data . minFiles , this . data . maxFiles ] ) . rangeRound ( [ ForceGraph . MinRangeValue , ForceGraph . MaxRangeValue ] ) . clamp ( true ) ; return scale ( value ) ; } private getLinkColor ( link , colorPalette , colorHelper ) { if ( colorHelper . isHighContrast ) { return colorHelper . getThemeColor ( "foreground" ) ; } switch ( this . settings . links . colorLink ) { case LinkColorType . ByWeight : { return colorPalette . getColor ( this . scale1to10 ( link . weight ) . toString ( ) ) . value ; } case LinkColorType . ByLinkType : { return link . linkType && this . data . linkTypes [ link . linkType ] ? this . data . linkTypes [ link . linkType ] . color : ForceGraph . DefaultLinkColor ; } } return ForceGraph . DefaultLinkColor ; } public enumerateObjectInstances ( options ) { return ForceGraphSettings . enumerateObjectInstances ( this . settings , options ) ; } public static converter ( dataView , colorPalette , colorHelper , host ) { const settings = ForceGraph . parseSettings ( dataView , colorHelper ) ; const metadata = ForceGraphColumns . getMetadataColumns ( dataView ) ; const nodes = { } ; let minFiles = Number . MAX_VALUE ; let maxFiles = 0 ; const linkedByName = { } ; const links = [ ] ; const linkDataPoints = { } ; let linkTypeCount = 0 ; if ( ! metadata || ! metadata . Source || ! metadata . Target ) { return null ; } let categorical = ForceGraphColumns . getCategoricalColumns ( dataView ) ; if ( ! categorical || ! categorical . Source || ! categorical . Target || _ . isEmpty ( categorical . Source . source ) || _ . isEmpty ( categorical . Source . values ) || _ . isEmpty ( categorical . Target . source ) || _ . isEmpty ( categorical . Target . values ) ) { return null ; } let sourceCategories = categorical . Source . values , targetCategories = categorical . Target . values , sourceTypeCategories = ( categorical . SourceType || { values : [ ] } ) . values , targetTypeCategories = ( categorical . TargetType || { values : [ ] } ) . values , linkTypeCategories = ( categorical . LinkType || { values : [ ] } ) . values , weightValues = ( categorical . Weight && categorical . Weight [ 0 ] || { values : [ ] } ) . values ; let weightFormatter = null ; if ( metadata . Weight ) { let weightValue = settings . links . displayUnits ; if ( ! weightValue && categorical . Weight && categorical . Weight . length ) { weightValue = categorical . Weight [ 0 ] . maxLocal as number ; } weightFormatter = valueFormatter . create ( { format : valueFormatter . getFormatStringByColumn ( metadata . Weight , true ) , precision : settings . links . decimalPlaces , value : weightValue } ) ; } let sourceFormatter = valueFormatter . create ( { format : valueFormatter . getFormatStringByColumn ( metadata . Source , true ) , } ) ; let targetFormatter = valueFormatter . create ( { format : valueFormatter . getFormatStringByColumn ( metadata . Target , true ) , } ) ; for ( let i = 0 ; i < targetCategories . length ; i ++ ) { const source = sourceCategories [ i ] ; const target = targetCategories [ i ] ; const targetType = targetTypeCategories [ i ] ; const sourceType = sourceTypeCategories [ i ] ; const linkType = linkTypeCategories [ i ] ; const weight = weightValues [ i ] ; linkedByName [ `${ source },${ target }` ] = ForceGraph . DefaultValueOfExistingLink ; if ( ! nodes [ source ] ) { nodes [ source ] = { name : sourceFormatter . format ( source ) , hideLabel : false , image : sourceType || ForceGraph . DefaultSourceType , adj : { } , identity : host . createSelectionIdBuilder ( ) . withCategory ( categorical . Source , i ) . withMeasure ( < string > categorical . Source . values [ i ] ) . createSelectionId ( ) } ; } if ( ! nodes [ target ] ) { nodes [ target ] = { name : targetFormatter . format ( target ) , hideLabel : false , image : targetType || ForceGraph . DefaultTargetType , adj : { } , identity : host . createSelectionIdBuilder ( ) . withCategory ( categorical . Target , i ) . withMeasure ( < string > categorical . Target . values [ i ] ) . createSelectionId ( ) } ; } let sourceNode = nodes [ source ] , targetNode = nodes [ target ] ; sourceNode . adj [ targetNode . name ] = ForceGraph . DefaultValueOfExistingLink ; targetNode . adj [ sourceNode . name ] = ForceGraph . DefaultValueOfExistingLink ; const tooltipInfo = ForceGraphTooltipsFactory . build ( { Source : source , Target : target , Weight : weight , LinkType : linkType , SourceType : sourceType , TargetType : targetType } , dataView . metadata . columns ) ; let link = { source : sourceNode , target : targetNode , weight : Math . max ( metadata . Weight ? ( weight || ForceGraph . MinWeight ) : ForceGraph . MaxWeight , ForceGraph . MinWeight ) , formattedWeight : weight && weightFormatter . format ( weight ) , linkType : linkType || ForceGraph . DefaultLinkType , tooltipInfo : tooltipInfo } ; if ( metadata . LinkType && ! linkDataPoints [ linkType ] ) { const color = colorHelper . getHighContrastColor ( "foreground" , colorPalette . getColor ( ( linkTypeCount ++ ) . toString ( ) ) . value ) ; linkDataPoints [ linkType ] = { color , label : linkType , } ; } if ( link . weight < minFiles ) { minFiles = link . weight ; } if ( link . weight > maxFiles ) { maxFiles = link . weight ; } links . push ( link ) ; } return { nodes , links , minFiles , maxFiles , linkedByName , settings , linkTypes : linkDataPoints , formatter : targetFormatter } ; } private static parseSettings ( dataView , colorHelper ) { let settings = ForceGraphSettings . parse < ForceGraphSettings > ( dataView ) ; settings . size . charge = Math . min ( Math . max ( settings . size . charge , ForceGraph . MinCharge ) , ForceGraph . MaxCharge ) ; settings . links . decimalPlaces = settings . links . decimalPlaces && Math . min ( Math . max ( settings . links . decimalPlaces , ForceGraph . MinDecimalPlaces ) , ForceGraph . MaxDecimalPlaces ) ; settings . labels . color = colorHelper . getHighContrastColor ( "foreground" , settings . labels . color ) ; settings . nodes . fill = colorHelper . getHighContrastColor ( "foreground" , settings . nodes . fill ) ; settings . nodes . stroke = colorHelper . getHighContrastColor ( "background" , settings . nodes . stroke ) ; return settings ; } private isIntersect ( textRect1 , textRect2 ) { let intersectY = false ; let intersectX = false ; if ( textRect1 . y1 <= textRect2 . y1 && textRect2 . y1 <= textRect1 . y2 ) { intersectY = true ; } if ( textRect1 . y1 <= textRect2 . y2 && textRect2 . y2 <= textRect1 . y2 ) { intersectY = true ; } if ( textRect2 . y2 <= textRect1 . y1 && textRect1 . y1 <= textRect2 . y1 ) { intersectY = true ; } if ( textRect2 . y2 <= textRect1 . y2 && textRect1 . y2 <= textRect2 . y1 ) { intersectY = true ; } if ( textRect1 . x1 <= textRect2 . x1 && textRect2 . x1 <= textRect1 . x2 ) { intersectX = true ; } if ( textRect1 . x1 <= textRect2 . x2 && textRect2 . x2 <= textRect1 . x2 ) { intersectX = true ; } if ( textRect2 . x2 <= textRect1 . x1 && textRect1 . x1 <= textRect2 . x1 ) { intersectX = true ; } if ( textRect2 . x2 <= textRect1 . x2 && textRect1 . x2 <= textRect2 . x1 ) { intersectX = true ; } return intersectX && intersectY ; } public update ( options ) { if ( ! options || ! options . dataViews || ! options . dataViews [ 0 ] ) { return ; } this . data = ForceGraph . converter ( options . dataViews [ 0 ] , this . colorPalette , this . colorHelper , this . host ) ; if ( ! this . data ) { this . reset ( ) ; return ; } this . viewport = options . viewport ; let k = Math . sqrt ( Object . keys ( this . data . nodes ) . length / ( this . viewport . width * this . viewport . height ) ) ; this . reset ( ) ; this . forceLayout . gravity ( ForceGraph . GravityFactor * k ) . links ( this . data . links ) . size ( [ this . viewport . width , this . viewport . height ] ) . linkDistance ( ForceGraph . LinkDistance ) . charge ( this . settings . size . charge / k ) ; this . updateNodes ( ) ; let nodesNum = Object . keys ( this . data . nodes ) . length ; const theta = 1.4 ; if ( this . settings . animation . show && nodesNum <= ForceGraph . NoAnimationLimit ) { this . forceLayout . on ( "tick" , this . getForceTick ( ) ) ; this . forceLayout . theta ( theta ) . start ( ) ; this . setVisualData ( this . container , this . colorPalette , this . colorHelper ) ; } else { this . forceLayout . theta ( theta ) . start ( ) ; for ( let i = 0 ; i < nodesNum ; ++ i ) { this . forceLayout . tick ( ) ; } this . forceLayout . stop ( ) ; this . setVisualData ( this . container , this . colorPalette , this . colorHelper ) ; this . forceLayout . on ( "tick" , this . getForceTick ( ) ) ; } } private setVisualData ( svg , colorPalette , colorHelper ) { this . paths = svg . selectAll ( ForceGraph . LinkSelector . selectorName ) . data ( this . forceLayout . links ( ) ) . enter ( ) . append ( "path" ) . attr ( "id" , ( d , i ) => "linkid_" + this . uniqieId + i ) . attr ( "stroke-width" , ( link ) => { return this . settings . links . thickenLink ? this . scale1to10 ( link . weight ) : ForceGraph . DefaultLinkThickness ; } ) . classed ( ForceGraph . LinkSelector . className , true ) . style ( "stroke" , ( link ) => { return this . getLinkColor ( link , colorPalette , colorHelper ) ; } ) . style ( "fill" , ( link ) => { if ( this . settings . links . showArrow && link . source !== link . target ) { return this . getLinkColor ( link , colorPalette , colorHelper ) ; } } ) . on ( "mouseover" , ( ) => { return this . fadePath ( ForceGraph . HoverOpacity , colorHelper . getHighContrastColor ( "foreground" , ForceGraph . DefaultLinkHighlightColor ) , colorHelper . getHighContrastColor ( "foreground" , ForceGraph . DefaultLinkColor ) ) ; } ) . on ( "mouseout" , ( ) => { return this . fadePath ( ForceGraph . DefaultOpacity , colorHelper . getHighContrastColor ( "foreground" , ForceGraph . DefaultLinkColor ) , colorHelper . getHighContrastColor ( "foreground" , ForceGraph . DefaultLinkColor ) ) ; } ) ; this . tooltipServiceWrapper . addTooltip ( this . paths , ( eventArgs ) => { return eventArgs . data . tooltipInfo ; } ) ; if ( this . settings . links . showLabel ) { let linklabelholderUpdate = svg . selectAll ( ForceGraph . LinkLabelHolderSelector . selectorName ) . data ( this . forceLayout . links ( ) ) ; linklabelholderUpdate . enter ( ) . append ( "g" ) . classed ( ForceGraph . LinkLabelHolderSelector . className , true ) . append ( "text" ) . classed ( ForceGraph . LinkLabelSelector . className , true ) . attr ( "dy" , ( link ) => { return this . settings . links . thickenLink ? - this . scale1to10 ( link . weight ) + this . defaultYOffset : this . defaultYPosition ; } ) . attr ( "text-anchor" , ForceGraph . LinkTextAnchor ) . style ( "fill" , colorHelper . getHighContrastColor ( "foreground" , ForceGraph . DefaultLinkFillColor ) ) . append ( "textPath" ) . attr ( "xlink:href" , ( link , index ) => { return ForceGraph . Href + "#linkid_" + this . uniqieId + index ; } ) . attr ( "startOffset" , ForceGraph . StartOffset ) . text ( ( link ) => { return this . settings . links . colorLink === LinkColorType . ByLinkType ? link . linkType : link . formattedWeight ; } ) ; linklabelholderUpdate . exit ( ) . remove ( ) ; } let nodesNum = Object . keys ( this . data . nodes ) . length ; let selectionManager = this . selectionManager ; this . nodes = svg . selectAll ( ForceGraph . NodeSelector . selectorName ) . data ( this . forceLayout . nodes ( ) ) . enter ( ) . append ( "g" ) . attr ( "drag-resize-disabled" , true ) . classed ( ForceGraph . NodeSelector . className , true ) . on ( "mouseover" , ( node ) => { node . isOver = true ; this . fadeNode ( node ) ; } ) . on ( "mouseout" , ( node ) => { node . isOver = false ; this . fadeNode ( node ) ; } ) . on ( "click" , function ( d ) { selectionManager . select ( d . identity ) ; ( < Event > d3 . event ) . stopPropagation ( ) ; } ) ; if ( nodesNum <= ForceGraph . NoAnimationLimit ) { this . nodes . call ( this . forceLayout . drag ) ; } if ( ! this . settings . animation . show || nodesNum > ForceGraph . NoAnimationLimit ) { const viewport = this . viewportIn ; let maxWidth = viewport . width * ForceGraph . ResolutionFactor , maxHeight = viewport . height * ForceGraph . ResolutionFactor , limitX = x => Math . max ( ( viewport . width - maxWidth ) / 2 , Math . min ( ( viewport . width + maxWidth ) / 2 , x ) ) , limitY = y => Math . max ( ( viewport . height - maxHeight ) / 2 , Math . min ( ( viewport . height + maxHeight ) / 2 , y ) ) ; this . paths . attr ( "d" , ( link ) => { link . source . x = limitX ( link . source . x ) ; link . source . y = limitY ( link . source . y ) ; link . target . x = limitX ( link . target . x ) ; link . target . y = limitY ( link . target . y ) ; return this . settings && this . settings . links && this . settings . links . showArrow ? this . getPathWithArrow ( link ) : this . getPathWithoutArrow ( link ) ; } ) ; this . nodes . attr ( "transform" , ( node ) => translate ( limitX ( node . x ) , limitY ( node . y ) ) ) ; } if ( this . settings . nodes . displayImage ) { this . nodes . append ( "image" ) . attr ( "x" , PixelConverter . toString ( ForceGraph . ImagePosition ) ) . attr ( "y" , PixelConverter . toString ( ForceGraph . ImagePosition ) ) . attr ( "width" , PixelConverter . toString ( ForceGraph . ImageViewport . width ) ) . attr ( "height" , PixelConverter . toString ( ForceGraph . ImageViewport . height ) ) . attr ( "xlink:href" , ( node ) => { if ( node . image ) { return this . getImage ( node . image ) ; } else if ( this . settings . nodes . defaultImage ) { return this . getImage ( this . settings . nodes . defaultImage ) ; } return ForceGraph . DefaultImage ; } ) . attr ( "title" , ( node ) => node . name ) ; } else { this . nodes . append ( "circle" ) . attr ( "r" , ( node ) => { return isNaN ( node . weight ) || node . weight < ForceGraph . MinNodeWeight ? ForceGraph . MinNodeWeight : node . weight ; } ) . style ( "fill" , this . settings . nodes . fill ) . style ( "stroke" , this . settings . nodes . stroke ) ; } if ( this . settings . labels . show ) { this . nodes . append ( "text" ) . attr ( "x" , ForceGraph . DefaultLabelX ) . attr ( "dy" , ForceGraph . DefaultLabelDy ) . style ( "fill" , this . settings . labels . color ) . style ( "font-size" , PixelConverter . fromPoint ( this . settings . labels . fontSize ) ) . text ( ( node ) => { if ( node . name ) { if ( node . name . length > this . settings . nodes . nameMaxLength ) { return node . name . substr ( 0 , this . settings . nodes . nameMaxLength ) ; } else { return node . name ; } } else { return ForceGraph . DefaultLabelText ; } } ) ; } } private getImage ( image ) { return `${ this . settings . nodes . imageUrl }${ image }${ this . settings . nodes . imageExt }` ; } private reset ( ) { if ( this . container . empty ( ) ) { return ; } this . forceLayout . on ( "tick" , null ) ; this . forceLayout . stop ( ) ; this . container . selectAll ( "*" ) . remove ( ) ; } private updateNodes ( ) { let thePreviousNodes = this . forceLayout . nodes ( ) ; this . forceLayout . nodes ( d3 . values ( this . data . nodes ) ) ; this . forceLayout . nodes ( ) . forEach ( ( node , index ) => { if ( ! thePreviousNodes [ index ] ) { return ; } this . updateNodeAttributes ( node , thePreviousNodes [ index ] ) ; } ) ; } private updateNodeAttributes ( first , second ) { first . x = second . x ; first . y = second . y ; first . px = second . px ; first . py = second . py ; first . weight = second . weight ; } private getForceTick ( ) { const viewport = this . viewportIn ; const properties = { fontFamily : ForceGraph . LabelsFontFamily , fontSize : PixelConverter . fromPoint ( this . settings . labels . fontSize ) , text : this . data . formatter . format ( "" ) } ; const showArrow = this . settings && this . settings . links && this . settings . links . showArrow ; let resolutionFactor = ForceGraph . ResolutionFactor ; if ( this . settings . size . boundedByBox ) { resolutionFactor = ForceGraph . ResolutionFactorBoundByBox ; } const maxWidth = viewport . width * resolutionFactor , maxHeight = viewport . height * resolutionFactor , viewPortWidthDownLimit = ( viewport . width - maxWidth ) / 2 , viewPortHeightDownLimit = ( viewport . height - maxHeight ) / 2 , viewPortHeightUpLimit = ( viewport . height + maxHeight ) / 2 , viewPortWidthUpLimit = ( viewport . height + maxHeight ) / 2 , limitX = x => Math . max ( viewPortWidthDownLimit , Math . min ( viewPortWidthUpLimit , x ) ) , limitY = y => Math . max ( viewPortHeightDownLimit , Math . min ( viewPortWidthUpLimit , y ) ) ; return ( ) => { this . paths . attr ( "d" , ( link ) => { link . source . x = limitX ( link . source . x ) ; link . source . y = limitY ( link . source . y ) ; link . target . x = limitX ( link . target . x ) ; link . target . y = limitY ( link . target . y ) ; return showArrow ? this . getPathWithArrow ( link ) : this . getPathWithoutArrow ( link ) ; } ) ; this . nodes . attr ( "transform" , ( node ) => translate ( limitX ( node . x ) , limitY ( node . y ) ) ) ; if ( ! this . settings . labels . allowIntersection && this . settings . labels . show && Object . keys ( this . data . nodes ) . length <= ForceGraph . NoAnimationLimit ) { this . nodes . classed ( "hiddenLabel" , ( node ) => { properties . text = this . data . formatter . format ( node . name ) ; let curNodeTextRect = this . getTextRect ( properties , node . x , node . y ) ; node . hideLabel = false ; this . nodes . each ( ( otherNode ) => { properties . text = this . data . formatter . format ( otherNode . name ) ; let otherNodeTextRect = this . getTextRect ( properties , otherNode . x , otherNode . y ) ; if ( ! otherNode . hideLabel && node . name !== otherNode . name && this . isIntersect ( curNodeTextRect , otherNodeTextRect ) ) { node . hideLabel = true ; return ; } } ) ; return node . hideLabel ; } ) ; } } ; } private getTextRect ( properties , x , y ) { let textHeight = textMeasurementService . estimateSvgTextHeight ( properties ) ; let textWidth = textMeasurementService . measureSvgTextWidth ( properties ) ; let curTextUpperPointX = x + textWidth ; let curTextUpperPointY = y - textHeight ; return < ITextRect > { x1 : x , y1 : y , x2 : curTextUpperPointX , y2 : curTextUpperPointY } ; } private getPathWithArrow ( link ) { let dx = link . target . x - link . source . x , dy = link . target . y - link . source . y , dr = Math . sqrt ( dx * dx + dy * dy ) , theta = Math . atan2 ( dy , dx ) + Math . PI / 7.85 , d90 = Math . PI / 2 , dtxs = link . target . x - 6 * Math . cos ( theta ) , dtys = link . target . y - 6 * Math . sin ( theta ) ; if ( dr === 0 ) { return `M ${ link . source . x - 10 } ${ link . source . y - 10 } C ${ link . source . x - 50 } ${ link . source . y - 50 }, ${ link . source . x + 50 } ${ link . source . y - 50 }, ${ link . source . x + 10 } ${ link . source . y - 10 }` ; } return "M" + link . source . x + "," + link . source . y + "A" + dr + "," + dr + " 0 0 1," + link . target . x + "," + link . target . y + "A" + dr + "," + dr + " 0 0 0," + link . source . x + "," + link . source . y + "M" + dtxs + "," + dtys + "l" + ( 3.5 * Math . cos ( d90 - theta ) - 10 * Math . cos ( theta ) ) + "," + ( - 3.5 * Math . sin ( d90 - theta ) - 10 * Math . sin ( theta ) ) + "L" + ( dtxs - 3.5 * Math . cos ( d90 - theta ) - 10 * Math . cos ( theta ) ) + "," + ( dtys + 3.5 * Math . sin ( d90 - theta ) - 10 * Math . sin ( theta ) ) + "z" ; } private getPathWithoutArrow ( link ) { let dx = link . target . x - link . source . x , dy = link . target . y - link . source . y , dr = Math . sqrt ( dx * dx + dy * dy ) ; if ( dr === 0 ) { return `M ${ link . source . x - 10 } ${ link . source . y - 10 } C ${ link . source . x - 50 } ${ link . source . y - 50 }, ${ link . source . x + 50 } ${ link . source . y - 50 }, ${ link . source . x + 10 } ${ link . source . y - 10 }` ; } return "M" + link . source . x + "," + link . source . y + "A" + dr + "," + dr + " 0 0,1 " + link . target . x + "," + link . target . y ; } private fadePath ( opacity , highlightColor , defaultHighlightColor ) { if ( this . settings . links . colorLink !== LinkColorType . Interactive ) { return ; } return ( ) => { this . paths . style ( "stroke-opacity" , ( link ) => { return link . source === link . source && link . target === link . target ? ForceGraph . DefaultOpacity : opacity ; } ) . style ( "stroke" , ( link ) => { return link . source === link . source && link . target === link . target ? highlightColor : defaultHighlightColor ; } ) ; } ; } private isReachable ( a , b ) { if ( a . name === b . name || this . data . linkedByName [ a . name + "," + b . name ] ) { return true ; } let visited = { } ; for ( let name in this . data . nodes ) { visited [ name ] = false ; } visited [ a . name ] = true ; let stack = [ ] ; stack . push ( a . name ) ; while ( stack . length > 0 ) { let cur = stack . pop ( ) , node = this . data . nodes [ cur ] ; if ( node && node . adj ) { for ( let nb in node . adj ) { if ( nb === b . name ) { return true ; } if ( ! visited [ nb ] ) { visited [ nb ] = true ; stack . push ( nb ) ; } } } } return false ; } private fadeNode ( node ) { if ( ! this . settings || this . settings . links . colorLink !== LinkColorType . Interactive ) { return ; } let self = this , isHighlight = node . isOver || node . isDrag , opacity = isHighlight ? ForceGraph . HoverOpacity : ForceGraph . DefaultOpacity ; let highlight = isHighlight ? ForceGraph . DefaultLinkHighlightColor : ForceGraph . DefaultLinkColor ; this . nodes . style ( "stroke-opacity" , function ( otherNode ) { let thisOpacity = ( self . settings . nodes . highlightReachableLinks ? self . isReachable ( node , otherNode ) : self . areNodesConnected ( node , otherNode ) ) ? ForceGraph . DefaultOpacity : opacity ; this . setAttribute ( "fill-opacity" , thisOpacity ) ; return thisOpacity ; } ) ; this . paths . style ( "stroke-opacity" , ( link ) => ( this . settings . nodes . highlightReachableLinks ? this . isReachable ( node , link . source ) : ( link . source === node || link . target === node ) ) ? ForceGraph . DefaultOpacity : opacity ) ; this . paths . style ( "stroke" , ( link ) => { const color = ( this . settings . nodes . highlightReachableLinks ? this . isReachable ( node , link . source ) : ( link . source === node || link . target === node ) ) ? highlight : ForceGraph . DefaultLinkColor ; return this . colorHelper . getHighContrastColor ( "foreground" , color ) ; } ) ; } private areNodesConnected ( firstNode , secondNode ) { return this . data . linkedByName [ firstNode . name + "," + secondNode . name ] || this . data . linkedByName [ secondNode . name + "," + firstNode . name ] || firstNode . name === secondNode . name ; } public destroy ( ) { this . container . selectAll ( "*" ) . remove ( ) ; } }