import React , { useState , useEffect } from 'react' ; import * as d3 from 'd3' ; import { componentAtomTree , atom , selector } from '../../../types' ; import { useAppSelector , useAppDispatch } from '../../state-management/hooks' ; import { updateZoomState , selectZoomState , setDefaultZoom , } from '../../state-management/slices/ZoomSlice' ; interface AtomComponentVisualProps { componentAtomTree : componentAtomTree ; cleanedComponentAtomTree : componentAtomTree ; selectedRecoilValue : string [ ] ; atoms : atom ; selectors : selector ; setStr : React . Dispatch < React . SetStateAction < string [ ] > > ; setSelectedRecoilValue : React . Dispatch < React . SetStateAction < string [ ] > > ; } const AtomComponentVisual = ( { componentAtomTree , cleanedComponentAtomTree , selectedRecoilValue , atoms , selectors , setStr , setSelectedRecoilValue , } ) => { const zoomSelector = useAppSelector ( selectZoomState ) ; const { x , y , k } = zoomSelector ; const dispatch = useAppDispatch ( ) ; let width = 0 ; let height = 0 ; const [ rawToggle , setRawToggle ] = useState < boolean > ( false ) ; const [ hasSuspense , setHasSuspense ] = useState < boolean > ( false ) ; const [ atomList , setAtomList ] = useState ( Object . keys ( atoms ) ) ; const [ selectorList , setSelectorList ] = useState ( Object . keys ( selectors ) ) ; const [ showAtomMenu , setShowAtomMenu ] = useState < boolean > ( false ) ; const [ showSelectorMenu , setShowSelectorMenu ] = useState < boolean > ( false ) ; const [ atomButtonClicked , setAtomButtonClicked ] = useState < boolean > ( false ) ; const [ selectorButtonClicked , setSelectorButtonClicked ] = useState < boolean > ( false ) ; const [ bothButtonClicked , setBothButtonClicked ] = useState < boolean > ( false ) ; const [ isDropDownItem , setIsDropDownItem ] = useState < boolean > ( false ) ; useEffect ( ( ) => { height = document . querySelector ( '.Component' ) . clientHeight ; width = document . querySelector ( '.Component' ) . clientWidth ; document . getElementById ( 'canvas' ) . innerHTML = '' ; setHasSuspense ( false ) ; const svgContainer = d3 . select ( '#canvas' ) ; const g = svgContainer . append ( 'g' ) . attr ( 'id' , 'componentGraph' ) ; let i = 0 ; let duration = 750 ; let root ; let path ; const treeMap = d3 . tree ( ) . nodeSize ( [ height , width ] ) ; if ( ! rawToggle ) { root = d3 . hierarchy ( cleanedComponentAtomTree , function ( d ) { return d . children ; } ) ; } else { root = d3 . hierarchy ( componentAtomTree , function ( d ) { return d . children ; } ) ; } root . x0 = 10 ; root . y0 = width / 2 ; update ( root ) ; let zoom = d3 . zoom ( ) . on ( 'zoom' , zoomed ) ; svgContainer . call ( zoom . transform , d3 . zoomIdentity . translate ( x , y ) . scale ( k ) ) ; svgContainer . call ( d3 . zoom ( ) . scaleExtent ( [ 0.05 , 0.9 ] ) . on ( 'zoom' , zoomed ) ) ; function zoomed ( ) { g . attr ( 'transform' , d3 . event . transform ) . on ( 'mouseup' , dispatch ( updateZoomState ( d3 . zoomTransform ( d3 . select ( '#canvas' ) . node ( ) ) ) ) ) ; } function update ( source ) { treeMap ( root ) ; let nodes = root . descendants ( ) , links = root . descendants ( ) . slice ( 1 ) ; let node = g . selectAll ( 'g.node' ) . attr ( 'stroke-width' , 5 ) . data ( nodes , function ( d ) : number { return d . id || ( d . id = ++ i ) ; } ) ; const tooltip = d3 . select ( '.tooltipContainer' ) . append ( 'div' ) . attr ( 'class' , 'hoverInfo' ) . style ( 'opacity' , 0 ) ; let nodeEnter = node . enter ( ) . append ( 'g' ) . attr ( 'class' , 'node' ) . attr ( 'transform' , function ( ) : string { return `translate(${ source . y0 }, ${ source . x0 })` ; } ) . on ( 'click' , click ) . on ( 'mouseover' , function ( d , i ) : void { const atsel = [ ] ; if ( d . data . recoilNodes ) { for ( let x = 0 ; x < d . data . recoilNodes . length ; x ++ ) { atsel . push ( d . data . recoilNodes [ x ] ) ; } d3 . select ( this ) . transition ( ) . duration ( '50' ) . attr ( 'opacity' , '.85' ) ; const nodeData = formatAtomSelectorText ( atsel ) [ 0 ] ; const genHTML = ( obj ) : string => { let str = '' ; let htmlStr = '' ; for ( let key in obj ) { const curr = obj [ key ] ; if ( key === 'type' ) str += `${ curr }: ` ; if ( key === 'name' ) str += curr ; if ( key === 'info' ) { htmlStr += `<h3>${ str }</h3>` ; htmlStr += `<h5>Atomic Values</h5>` ; if ( typeof curr === 'string' ) htmlStr += `<p>title: ${ curr }</p>` ; else for ( let prop in curr ) { const title = prop ; const data = curr [ prop ] ; htmlStr += `<p>${ title }: ${ data }</p>` ; } } } return `<div>${ htmlStr }</div>` ; } ; tooltip . style ( 'opacity' , 1 ) . html ( genHTML ( nodeData ) ) . style ( 'left' , d3 . event . pageX + 15 + 'px' ) . style ( 'top' , d3 . event . pageY - 20 + 'px' ) ; } } ) . on ( 'mouseout' , function ( d , i ) : void { d3 . select ( this ) . transition ( ) . duration ( '50' ) . attr ( 'opacity' , '1' ) ; tooltip . style ( 'opacity' , 0 ) ; } ) ; nodeEnter . append ( 'circle' ) . attr ( 'class' , 'node' ) . attr ( 'r' , determineSize ) . attr ( 'fill' , colorComponents ) . style ( 'stroke' , borderColor ) . style ( 'stroke-width' , 15 ) ; nodeEnter . append ( 'text' ) . attr ( 'dy' , '.31em' ) . attr ( 'y' , ( d ) : number => ( d . data . recoilNodes ? 138 : - 75 ) ) . attr ( 'text-anchor' , function ( d ) : string { return d . children || d . _children ? 'middle' : 'middle' ; } ) . text ( ( d ) : string => d . data . name ) . style ( 'font-size' , `7.5rem` ) . style ( 'fill' , 'white' ) ; let nodeUpdate = nodeEnter . merge ( node ) ; nodeUpdate . attr ( 'transform' , function ( d ) : string { return `translate(${ d . y }, ${ d . x })` ; } ) ; nodeUpdate . select ( 'circle.node' ) . attr ( 'r' , determineSize ) . attr ( 'fill' , colorComponents ) . attr ( 'cursor' , 'pointer' ) . style ( 'stroke' , borderColor ) . style ( 'stroke-width' , 15 ) ; let nodeExit = node . exit ( ) . transition ( ) . duration ( duration ) . attr ( 'transform' , function ( d ) : string { return `translate(${ source . y }, ${ source . x })` ; } ) . remove ( ) ; let link = g . attr ( 'fill' , 'none' ) . attr ( 'stroke-width' , 5 ) . selectAll ( 'path.link' ) . data ( links , function ( d ) : number { return d . id ; } ) ; let linkEnter = link . enter ( ) . insert ( 'path' , 'g' ) . attr ( 'class' , 'link' ) . attr ( 'stroke' , '#646464' ) . attr ( 'stroke-width' , 5 ) . attr ( 'd' , function ( d ) : string { let o = { x : source . x0 , y : source . y0 } ; return diagonal ( o , o ) ; } ) ; let linkUpdate = linkEnter . merge ( link ) ; linkUpdate . transition ( ) . duration ( duration ) . attr ( 'stroke' , '#646464' ) . attr ( 'stroke-width' , 5 ) . attr ( 'd' , function ( d ) : string { return diagonal ( d , d . parent ) ; } ) ; let linkExit = link . exit ( ) . transition ( ) . duration ( duration ) . attr ( 'stroke' , '#646464' ) . attr ( 'stroke-width' , 5 ) . attr ( 'd' , function ( d ) : string { let o = { y : source . y , x : source . x } ; return diagonal ( o , o ) ; } ) . remove ( ) ; nodes . forEach ( function ( d ) : void { d . x0 = d . x ; d . y0 = d . y ; } ) ; function diagonal ( s , d ) { path = `M ${ s . y } ${ s . x }
            C ${ ( s . y + d . y ) / 2 } ${ s . x },
              ${ ( s . y + d . y ) / 2 } ${ d . x },
              ${ d . y } ${ d . x }` ; return path ; } function click ( d ) { if ( d . children ) { d . _children = d . children ; d . children = null ; } else { d . children = d . _children ; d . _children = null ; } update ( d ) ; const atsel = [ ] ; if ( d . data . recoilNodes ) { for ( let x = 0 ; x < d . data . recoilNodes . length ; x ++ ) { atsel . push ( d . data . recoilNodes [ x ] ) ; } setStr ( formatAtomSelectorText ( atsel ) ) ; } } node . call ( d3 . drag ( ) ) ; function formatMouseoverXValue ( recoilValue ) { if ( atoms . hasOwnProperty ( recoilValue ) ) { return - 30 ; } return - 150 ; } function formatAtomSelectorText ( atomOrSelector ) { const recoilData = [ ] ; for ( let i = 0 ; i < atomOrSelector . length ; i ++ ) { const data = { } ; const curr = atomOrSelector [ i ] ; data . type = atoms . hasOwnProperty ( curr ) ? 'atom' : 'selector' ; data . name = curr ; if ( data . type === 'atom' ) { data . info = atoms [ curr ] ; } else { data . info = selectors [ curr ] ; } recoilData . push ( data ) ; } return recoilData ; } function determineSize ( d ) { if ( d . data . recoilNodes && d . data . recoilNodes . length ) { if ( d . data . recoilNodes . includes ( selectedRecoilValue [ 0 ] ) ) { return 150 ; } return 100 ; } return 50 ; } function borderColor ( d ) { if ( d . data . wasSuspended ) setHasSuspense ( true ) ; return d . data . wasSuspended ? '#FF0000' : 'none' ; } function colorComponents ( d ) { if ( d . data . recoilNodes && d . data . recoilNodes . length ) { if ( d . data . recoilNodes . includes ( selectedRecoilValue [ 0 ] ) ) { return 'yellow' ; } let hasAtom = false ; let hasSelector = false ; for ( let i = 0 ; i < d . data . recoilNodes . length ; i ++ ) { if ( atoms . hasOwnProperty ( d . data . recoilNodes [ i ] ) ) { hasAtom = true ; } if ( selectors . hasOwnProperty ( d . data . recoilNodes [ i ] ) ) { hasSelector = true ; } } if ( hasAtom && hasSelector ) { return 'springgreen' ; } if ( hasAtom ) { return '#9580ff' ; } else { return '#ff80bf' ; } } return 'gray' ; } } } , [ componentAtomTree , rawToggle , selectedRecoilValue ] ) ; function openDropdown ( e ) { const target = e . target as Element ; if ( target . id === 'AtomP' ) { setAtomButtonClicked ( true ) ; setSelectorButtonClicked ( false ) ; setShowAtomMenu ( ! showAtomMenu ) ; setShowSelectorMenu ( false ) ; } else { setAtomButtonClicked ( false ) ; setSelectorButtonClicked ( true ) ; setShowSelectorMenu ( ! showSelectorMenu ) ; setShowAtomMenu ( false ) ; } } const resetNodes = ( ) => { setIsDropDownItem ( false ) ; setSelectedRecoilValue ( [ ] ) ; setShowSelectorMenu ( false ) ; setShowAtomMenu ( false ) ; setAtomButtonClicked ( false ) ; setSelectorButtonClicked ( false ) ; } ; return ( < div className = "AtomComponentVisual" > < svg id = "canvas" > < /svg> < button id = "fixedButton" style = { { color : rawToggle ? '#E6E6E6' : '#989898' , } } onClick = { ( ) => { setRawToggle ( ! rawToggle ) ; dispatch ( setDefaultZoom ( ) ) ; } } > < span > { rawToggle ? 'Collapse' : 'Expand' } < /span> < /button> < div className = "AtomNetworkLegend" > < div className = "AtomLegend" / > < button onClick = { isDropDownItem ? resetNodes : openDropdown } id = "AtomP" className = { atomButtonClicked ? 'AtomP atomSelected' : 'AtomP atomLegendDefault' } > ATOM < /button> { showAtomMenu && ( < div id = "atomDrop" className = "AtomDropDown" > { atomList . map ( ( atom , i ) => ( < div className = "dropDownButtonDiv" > < button id = { `atom-drop${ i }` } className = "atom-class atomDropDown" key = { i } onClick = { event => { if ( ! ( event . target as HTMLInputElement ) . classList . contains ( 'atomSelected' ) && ( event . target as HTMLInputElement ) . classList . contains ( 'atomNotSelected' ) ) { ( event . target as HTMLInputElement ) . classList . replace ( 'atomNotSelected' , 'atomSelected' ) ; } else if ( ! ( event . target as HTMLInputElement ) . classList . contains ( 'atomSelected' ) && ! ( event . target as HTMLInputElement ) . classList . contains ( 'atomNotSelected' ) ) { ( event . target as HTMLInputElement ) . classList . add ( 'atomSelected' ) ; } document . querySelectorAll ( '.atom-class' ) . forEach ( item => { if ( item . id !== `atom-drop${ i }` && item . classList . contains ( 'atomSelected' ) ) { item . classList . replace ( 'atomSelected' , 'atomNotSelected' ) ; } else if ( item . id !== `atom-drop${ i }` && ! item . classList . contains ( 'atomNotSelected' ) ) { item . classList . add ( 'atomNotSelected' ) ; } } ) ; setSelectedRecoilValue ( [ atom , 'atom' ] ) ; setIsDropDownItem ( true ) ; } } > { atom } < /button> < /div>))} < /div>)} < div className = "SelectorLegend" > < /div> < button onClick = { isDropDownItem ? resetNodes : openDropdown } id = "SelectorP" className = { selectorButtonClicked ? 'SelectorP selectorSelected' : 'SelectorP selectorLegendDefault' } > SELECTOR < /button> { showSelectorMenu && ( < div id = "selectorDrop" className = "SelectorDropDown" > { selectorList . map ( ( selector , i ) => ( < div className = "dropDownButtonDiv" > < button id = { `selector-drop${ i }` } className = "selector-class selectorDropDown" key = { i } onClick = { event => { if ( ! ( event . target as HTMLInputElement ) . classList . contains ( 'selectorSelected' ) && ( event . target as HTMLInputElement ) . classList . contains ( 'selectorNotSelected' ) ) { ( event . target as HTMLInputElement ) . classList . replace ( 'selectorNotSelected' , 'selectorSelected' ) ; } else if ( ! ( event . target as HTMLInputElement ) . classList . contains ( 'selectorSelected' ) && ! ( event . target as HTMLInputElement ) . classList . contains ( 'selectorNotSelected' ) ) { ( event . target as HTMLInputElement ) . classList . add ( 'selectorSelected' ) ; } document . querySelectorAll ( '.selector-class' ) . forEach ( item => { if ( item . id !== `selector-drop${ i }` && item . classList . contains ( 'selectorSelected' ) ) { item . classList . replace ( 'selectorSelected' , 'selectorNotSelected' ) ; } else if ( item . id !== `selector-drop${ i }` && ! item . classList . contains ( 'selectorNotSelected' ) ) { item . classList . add ( 'selectorNotSelected' ) ; } } ) ; setSelectedRecoilValue ( [ selector , 'selector' ] ) ; setIsDropDownItem ( true ) ; } } > { selector } < /button> < /div>))} < /div>)} < div className = "bothLegend" > < /div> < button className = "bothLegendDefault" > BOTH < /button> < div className = { hasSuspense ? 'suspenseLegend' : '' } > < /div> < p > { hasSuspense ? 'SUSPENSE' : '' } < /p> < div className = "tooltipContainer" > < /div> < /div> < /div>); } ; export default AtomComponentVisual ;