Page Menu
Home
c4science
Search
Configure Global Search
Log In
Files
F80545172
ForceGraph.js
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Subscribers
None
File Metadata
Details
File Info
Storage
Attached
Created
Sun, Sep 1, 07:59
Size
20 KB
Mime Type
text/x-c
Expires
Tue, Sep 3, 07:59 (1 d, 21 h)
Engine
blob
Format
Raw Data
Handle
20395051
Attached To
R13029 webapp_nextjs
ForceGraph.js
View Options
import
React
,
{
useRef
,
useEffect
,
useState
}
from
'react'
;
import
*
as
d3
from
'd3'
;
import
'./ForceGraph.css'
var
colorConceptsPeople
=
{
'concept'
:
'#8eb9fc'
,
'object'
:
'#e0805a'
,
'secondaryConcept'
:
'#b0caff'
}
function
ForceGraph
({
dataDisplayed
,
tableColumns
,
searchEntity
})
{
const
svgRef
=
useRef
(
null
);
const
nodeGroupRef
=
useRef
(
null
);
const
linkGroupRef
=
useRef
(
null
);
const
simulationRef
=
useRef
(
null
);
const
[
conceptThreshold
,
setConceptThreshold
]
=
useState
(
1
);
// Initialize to show all nodes initially
const
[
objectThreshold
,
setObjectThreshold
]
=
useState
(
1
);
const
[
showLinks
,
setShowLinks
]
=
useState
(
true
);
const
[
nodeSize
,
setNodeSize
]
=
useState
(
1
);
const
[
maxStrength
,
setMaxStrength
]
=
useState
(
1
);
const
[
minRadius
,
setMinRadius
]
=
useState
(
5
);
const
[
maxRadius
,
setMaxRadius
]
=
useState
(
20
);
function
findNeighbors
(
node
)
{
return
linkGroupRef
.
current
.
data
().
reduce
((
neighbors
,
link
)
=>
{
if
(
link
.
target
.
id
===
node
.
id
)
{
neighbors
.
push
(
link
.
source
.
id
)
}
else
if
(
link
.
source
.
id
===
node
.
id
)
{
neighbors
.
push
(
link
.
target
.
id
)
}
return
neighbors
;
},
[
node
.
id
]);
// include the node itself
}
// console.log("dataDisplayed", dataDisplayed)
// console.log("tableColumns", tableColumns)
useEffect
(()
=>
{
// setMaxStrength(d3.max(nodes, d => d.size));
setMinRadius
(
5
*
Math
.
sqrt
(
nodeSize
));
setMaxRadius
(
20
*
Math
.
sqrt
(
nodeSize
));
},
[
nodeSize
,
maxStrength
])
const
maxConceptRank
=
tableColumns
.
length
;
// Assuming maxConceptRank holds the max rank for concept nodes
const
maxObjectRank
=
dataDisplayed
.
length
;
// Assuming maxObjectRank holds the max rank for object nodes
const
redrawGraph
=
()
=>
{
if
(
nodeGroupRef
.
current
===
null
)
return
;
nodeGroupRef
.
current
.
selectAll
(
'.nodeGroup'
)
.
style
(
"display"
,
d
=>
{
if
(
d
.
type
===
'concept'
&&
d
.
rank
>
conceptThreshold
)
return
"none"
;
if
(
d
.
type
===
'object'
&&
d
.
rank
>
objectThreshold
)
return
"none"
;
return
"block"
;
});
nodeGroupRef
.
current
.
selectAll
(
"circle"
)
.
attr
(
"r"
,
d
=>
d
.
size
)
nodeGroupRef
.
current
.
selectAll
(
'text'
)
.
text
(
d
=>
{
// console.log('d', d)
if
(
d
.
type
===
'concept'
)
{
return
d
.
id
.
split
(
"_"
).
join
(
" "
);
}
return
d
.
name
;
})
.
call
(
wrap
,
10
)
}
function
wrap
(
text
,
width
)
{
text
.
each
(
function
()
{
var
gnodeParent
=
d3
.
select
(
this
.
parentNode
);
// console.log("gnodeParent", gnodeParent)
var
gnodeParentData
=
gnodeParent
.
_groups
[
0
][
0
].
__data__
;
// console.log("gnodeParentData", gnodeParentData)
var
text
=
d3
.
select
(
this
),
words
=
text
.
text
().
split
(
" "
).
reverse
(),
nwords
=
words
.
length
,
word
,
line
=
[],
lineNumber
=
0
,
lineHeight
=
1.1
,
// ems
x
=
text
.
attr
(
"x"
),
y
=
text
.
attr
(
"y"
),
dy
=
-
(
nwords
+
1
/
2
)
*
lineHeight
/
2
,
//parseFloat(text.attr("dy")),
tspan
=
text
.
text
(
null
)
.
append
(
"tspan"
)
.
attr
(
"x"
,
x
)
.
attr
(
"y"
,
y
)
.
attr
(
"dy"
,
dy
+
"em"
);
while
(
word
=
words
.
pop
())
{
// console.log("word", word)
line
.
push
(
word
);
tspan
.
text
(
line
.
join
(
" "
));
if
(
tspan
.
node
().
getComputedTextLength
()
>
width
)
{
line
.
pop
();
tspan
.
text
(
line
.
join
(
" "
));
line
=
[
word
];
// create a span with d3
tspan
=
text
.
append
(
"tspan"
)
.
text
(
word
)
.
attr
(
"font-weight"
,
"bold"
)
.
attr
(
"font-size"
,
parseInt
(
gnodeParentData
.
size
/
3
)
+
"px"
)
.
attr
(
"dx"
,
function
()
{
return
-
this
.
getComputedTextLength
()
/
2
})
.
attr
(
"x"
,
0
)
.
attr
(
"y"
,
0
)
.
attr
(
"dy"
,
++
lineNumber
*
lineHeight
+
dy
+
"em"
);
}
}
});
}
function
computeNodeSize
(
nodes
,
links
)
{
// Initialize a strength to 0 for each node
nodes
.
forEach
(
node
=>
{
node
.
size
=
1
;
});
// This assumes you have defined your nodes and links from the data as before.
const
nodeById
=
new
Map
(
nodes
.
map
(
node
=>
[
node
.
id
,
node
]));
links
.
forEach
(
link
=>
{
link
.
source
=
nodeById
.
get
(
link
.
source
);
link
.
target
=
nodeById
.
get
(
link
.
target
);
});
// Iterate over links to sum the strength to the linked nodes
links
.
forEach
(
link
=>
{
link
.
source
.
size
+=
link
.
strength
;
link
.
target
.
size
+=
link
.
strength
;
});
// First, let's separate nodes by their type
const
conceptNodes
=
nodes
.
filter
(
node
=>
node
.
type
===
'concept'
);
const
objectNodes
=
nodes
.
filter
(
node
=>
node
.
type
===
'object'
);
// Then, let's sort the nodes in descending order based on their size
conceptNodes
.
sort
((
a
,
b
)
=>
b
.
size
-
a
.
size
);
objectNodes
.
sort
((
a
,
b
)
=>
b
.
size
-
a
.
size
);
// Finally, assign the rank based on the sorted position
conceptNodes
.
forEach
((
node
,
index
)
=>
{
node
.
rank
=
index
;
});
objectNodes
.
forEach
((
node
,
index
)
=>
{
node
.
rank
=
index
;
});
const
localMinRadius
=
5
*
Math
.
sqrt
(
nodeSize
);
const
localMaxRadius
=
20
*
Math
.
sqrt
(
nodeSize
);
const
localMaxStrength
=
d3
.
max
(
nodes
,
d
=>
d
.
size
);
const
radiusScale
=
d3
.
scaleLinear
()
.
domain
([
0
,
localMaxStrength
])
.
range
([
localMinRadius
,
localMaxRadius
]);
nodes
.
forEach
(
node
=>
{
node
.
size
=
radiusScale
(
node
.
size
);
});
}
useEffect
(()
=>
{
if
(
!
dataDisplayed
||
dataDisplayed
.
length
===
0
)
return
;
const
width
=
800
;
const
height
=
600
;
const
nodes
=
[];
const
links
=
[];
// Create nodes from dataDisplayed
dataDisplayed
.
forEach
((
item
)
=>
{
// console.log("item", item)
let
myNode
=
{
id
:
item
.
ID
,
type
:
"object"
};
if
(
searchEntity
===
'courses'
)
{
myNode
.
name
=
item
.
SubjectName
;
}
else
if
(
searchEntity
===
'labs'
)
{
myNode
.
name
=
item
.
ID
;
}
else
if
(
searchEntity
===
'publications'
)
{
myNode
.
name
=
item
.
Title
;
}
else
if
([
'research'
,
'teaching'
].
includes
(
searchEntity
))
{
myNode
.
name
=
item
.
FullName
;
}
nodes
.
push
(
myNode
);
tableColumns
.
forEach
((
column
)
=>
{
if
(
item
[
column
]
>
0
)
{
// Create node if doesn't exist
if
(
!
nodes
.
some
(
node
=>
node
.
id
===
column
))
{
nodes
.
push
({
id
:
column
,
type
:
"concept"
});
}
// Create link
links
.
push
({
source
:
item
.
ID
,
target
:
column
,
strength
:
item
[
column
]});
}
});
});
computeNodeSize
(
nodes
,
links
);
// Setup simulation
simulationRef
.
current
=
d3
.
forceSimulation
(
nodes
)
.
force
(
"link"
,
d3
.
forceLink
(
links
)
.
id
(
d
=>
d
.
id
)
.
strength
(
d
=>
d
.
strength
)
)
.
force
(
"charge"
,
d3
.
forceManyBody
().
strength
(
function
(
node
)
{
if
(
node
.
type
===
'concept'
)
{
// return -10;
return
-
1
*
(
node
.
size
)
**
(
1.1
)
*
10
}
else
{
return
-
node
.
size
*
5
// return 0;
}
}
))
.
force
(
"radialCenter"
,
d3
.
forceRadial
(
0
,
width
/
2
,
height
/
2
).
strength
(
function
(
d
)
{
return
0.15
;
}))
.
force
(
"center"
,
d3
.
forceCenter
(
width
/
2
,
height
/
2
)
.
strength
(
1
)
)
.
force
(
'collide'
,
d3
.
forceCollide
().
radius
(
d
=>
{
console
.
log
(
'd.size'
,
d
.
size
)
return
d
.
size
})
.
strength
(
1.5
)
)
;
// Warm-up the simulation
for
(
let
i
=
0
;
i
<
300
;
i
++
)
{
simulationRef
.
current
.
tick
();
}
const
svg
=
d3
.
select
(
svgRef
.
current
)
.
attr
(
"width"
,
width
)
.
attr
(
"height"
,
height
);
svg
.
selectAll
(
"*"
).
remove
();
const
zoomGroup
=
svg
.
append
(
'g'
)
.
attr
(
'id'
,
'zoomGroup'
);
// Create links (lines)
linkGroupRef
.
current
=
zoomGroup
.
append
(
"g"
)
.
attr
(
"stroke"
,
"#999"
)
.
attr
(
"stroke-opacity"
,
0.6
)
.
selectAll
(
"line"
)
.
data
(
links
)
.
join
(
"line"
)
.
attr
(
"stroke-width"
,
d
=>
Math
.
sqrt
(
d
.
value
));
// Create node groups
nodeGroupRef
.
current
=
zoomGroup
.
append
(
"g"
)
// .attr("stroke", "#fff")
// .attr("stroke-width", 1.5)
.
selectAll
(
".nodeGroup"
)
.
data
(
nodes
)
.
join
(
"g"
)
.
attr
(
'class'
,
'nodeGroup'
)
.
call
(
drag
(
simulationRef
.
current
));
// Within each node group, create a circle
nodeGroupRef
.
current
.
append
(
"circle"
)
.
attr
(
"r"
,
d
=>
d
.
size
)
.
attr
(
"fill"
,
d
=>
colorConceptsPeople
[
d
.
type
]);
// Within each node group, create a text element for the node's name
nodeGroupRef
.
current
.
append
(
'text'
)
// .attr('text-anchor', 'middle') // Centers the text on the node
.
attr
(
'fill'
,
'black'
)
.
text
(
d
=>
{
// console.log('d', d)
if
(
d
.
type
===
'concept'
)
{
return
d
.
id
.
split
(
"_"
).
join
(
" "
);
}
return
d
.
name
;
})
.
call
(
wrap
,
10
);
// Adjust the width (10 in this case) to fit your requirements
// Update the tick function
simulationRef
.
current
.
on
(
"tick"
,
()
=>
{
linkGroupRef
.
current
.
attr
(
"x1"
,
d
=>
d
.
source
.
x
)
.
attr
(
"y1"
,
d
=>
d
.
source
.
y
)
.
attr
(
"x2"
,
d
=>
d
.
target
.
x
)
.
attr
(
"y2"
,
d
=>
d
.
target
.
y
);
nodeGroupRef
.
current
.
each
(
function
(
d
)
{
// .each() is used to access the data for each node
d
.
x
=
Math
.
max
(
5
,
Math
.
min
(
width
-
5
,
d
.
x
));
d
.
y
=
Math
.
max
(
5
,
Math
.
min
(
height
-
5
,
d
.
y
));
}).
attr
(
"transform"
,
d
=>
`
translate
(
$
{
d
.
x
},
$
{
d
.
y
})
`
);
linkGroupRef
.
current
.
attr
(
"x1"
,
d
=>
d
.
source
.
x
)
.
attr
(
"y1"
,
d
=>
d
.
source
.
y
)
.
attr
(
"x2"
,
d
=>
d
.
target
.
x
)
.
attr
(
"y2"
,
d
=>
d
.
target
.
y
);
nodeGroupRef
.
current
.
attr
(
"transform"
,
d
=>
`
translate
(
$
{
Math
.
max
(
5
,
Math
.
min
(
width
-
5
,
d
.
x
))},
$
{
Math
.
max
(
5
,
Math
.
min
(
height
-
5
,
d
.
y
))})
`
);
});
function
zoomed
(
event
)
{
zoomGroup
.
attr
(
'transform'
,
event
.
transform
);
}
const
zoom
=
d3
.
zoom
()
.
scaleExtent
([
0.1
,
10
])
// This defines the range of the zoom (0.1x to 10x).
.
on
(
'zoom'
,
zoomed
);
svg
.
call
(
zoom
);
zoomGroup
.
attr
(
'transform'
,
'translate(0, 0) scale(1)'
);
function
drag
(
simulation
)
{
function
dragstarted
(
event
,
d
)
{
if
(
!
event
.
active
)
simulation
.
alphaTarget
(
0
).
alphaMin
(
-
1
).
restart
();
d
.
fx
=
d
.
x
;
d
.
fy
=
d
.
y
;
}
function
dragged
(
event
,
d
)
{
d
.
fx
=
event
.
x
;
d
.
fy
=
event
.
y
;
}
function
dragended
(
event
,
d
)
{
if
(
!
event
.
active
)
simulation
.
alphaTarget
(
0
);
// If you want to make the node fixed after dragging, comment out the next two lines.
// d.fx = null;
// d.fy = null;
}
return
d3
.
drag
()
.
on
(
"start"
,
dragstarted
)
.
on
(
"drag"
,
dragged
)
.
on
(
"end"
,
dragended
);
}
;
let
isNodeFocused
=
false
;
// to keep track of node focus state
nodeGroupRef
.
current
.
on
(
'click'
,
function
(
event
,
d
)
{
event
.
stopPropagation
();
// Prevent the graph from being reset when clicking on a node
if
(
isNodeFocused
)
{
// Reset opacities for nodes and links
nodeGroupRef
.
current
.
attr
(
'opacity'
,
1
);
linkGroupRef
.
current
.
attr
(
'opacity'
,
1
);
isNodeFocused
=
false
;
}
else
{
const
neighbors
=
findNeighbors
(
d
);
nodeGroupRef
.
current
.
attr
(
'opacity'
,
node
=>
neighbors
.
includes
(
node
.
id
)
?
1
:
0.1
);
linkGroupRef
.
current
.
attr
(
'opacity'
,
link
=>
d
.
id
===
link
.
source
.
id
||
d
.
id
===
link
.
target
.
id
?
1
:
0.1
);
isNodeFocused
=
true
;
}
});
if
(
svgRef
.
current
)
{
// SVG click event to reset opacities
d3
.
select
(
svgRef
.
current
).
on
(
'click'
,
function
(
event
)
{
if
(
!
isNodeFocused
)
return
;
// Reset opacities for nodes and links
nodeGroupRef
.
current
.
attr
(
'opacity'
,
1
);
linkGroupRef
.
current
.
attr
(
'opacity'
,
1
);
isNodeFocused
=
false
;
});
}
},
[
dataDisplayed
,
tableColumns
]);
useEffect
(()
=>
{
const
updateNodeLinkVisibility
=
()
=>
{
// Update node visibility
const
nodeSelection
=
d3
.
select
(
svgRef
.
current
).
selectAll
(
'.nodeGroup'
)
.
style
(
"display"
,
d
=>
{
if
(
d
.
type
===
'concept'
&&
d
.
rank
>=
conceptThreshold
)
return
"none"
;
if
(
d
.
type
===
'object'
&&
d
.
rank
>=
objectThreshold
)
return
"none"
;
return
"block"
;
});
// Update link visibility based on node visibility
d3
.
select
(
svgRef
.
current
).
selectAll
(
'line'
)
// Assuming links are represented as line elements
.
style
(
"display"
,
d
=>
{
if
(
!
showLinks
)
return
"none"
;
// Check visibility for source and target nodes
const
sourceNode
=
nodeSelection
.
filter
(
n
=>
n
.
id
===
d
.
source
.
id
).
node
();
const
targetNode
=
nodeSelection
.
filter
(
n
=>
n
.
id
===
d
.
target
.
id
).
node
();
if
(
sourceNode
&&
targetNode
)
{
// If either source or target node is hidden, hide the link
if
(
sourceNode
.
style
.
display
===
"none"
||
targetNode
.
style
.
display
===
"none"
)
{
return
"none"
;
}
}
return
"block"
;
});
}
if
(
svgRef
.
current
)
{
updateNodeLinkVisibility
()
}
},
[
conceptThreshold
,
objectThreshold
,
dataDisplayed
,
showLinks
]);
useEffect
(()
=>
{
if
(
nodeGroupRef
.
current
&&
linkGroupRef
.
current
)
{
console
.
log
(
'nodeGroupRef.current'
,
nodeGroupRef
.
current
.
data
())
console
.
log
(
'linkGroupRef.current'
,
linkGroupRef
.
current
.
data
())
computeNodeSize
(
nodeGroupRef
.
current
.
data
(),
linkGroupRef
.
current
.
selectAll
(
"line"
).
data
())
simulationRef
.
current
.
force
(
"charge"
,
d3
.
forceManyBody
().
strength
(
node
=>
{
if
(
node
.
type
===
'concept'
)
{
return
-
1
*
(
node
.
size
)
**
(
1.1
)
*
100
;
}
else
{
return
-
node
.
size
*
5
;
}
}))
.
force
(
'collide'
,
d3
.
forceCollide
().
radius
(
d
=>
d
.
size
).
strength
(
1.5
));
}
},
[
nodeSize
])
const
handleIncrease
=
(
type
)
=>
{
if
(
type
===
'concept'
&&
conceptThreshold
<
maxConceptRank
)
{
setConceptThreshold
(
prev
=>
prev
+
1
);
redrawGraph
();
}
else
if
(
type
===
'object'
&&
objectThreshold
<
maxObjectRank
)
{
setObjectThreshold
(
prev
=>
prev
+
1
);
redrawGraph
();
}
else
if
(
type
===
'size'
&&
nodeSize
<
10
)
{
setNodeSize
(
prev
=>
prev
+
1
);
redrawGraph
();
}
// console.log('conceptThreshold', conceptThreshold)
// console.log('objectThreshold', objectThreshold)
}
const
handleDecrease
=
(
type
)
=>
{
if
(
type
===
'concept'
&&
conceptThreshold
>
1
)
{
setConceptThreshold
(
prev
=>
prev
-
1
);
redrawGraph
();
}
else
if
(
type
===
'object'
&&
objectThreshold
>
1
)
{
setObjectThreshold
(
prev
=>
prev
-
1
);
redrawGraph
();
}
else
if
(
type
===
'size'
&&
nodeSize
>
1
)
{
setNodeSize
(
prev
=>
prev
-
1
);
redrawGraph
();
}
}
const
handleShowLinksChange
=
()
=>
{
setShowLinks
(
!
showLinks
);
// Logic to show/hide links in your graph here.
};
const
handleRecomputePositioning
=
()
=>
{
// Logic to recompute graph positioning here.
simulationRef
.
current
.
alpha
(
1
).
restart
();
for
(
let
i
=
0
;
i
<
300
;
i
++
)
{
simulationRef
.
current
.
tick
();
}
};
useEffect
(()
=>
{
redrawGraph
();
},
[
nodeSize
,
maxStrength
])
return
(
<
div
style
=
{{
display
:
'flex'
,
height
:
'100vh'
}}
>
{
/* Container div with flex layout */
}
{
/* Sliders container */
}
<
div
style
=
{{
flex
:
'0 0 20%'
,
padding
:
'10px'
}}
>
{
/* Taking up 20% of container width */
}
<
label
style
=
{{
margin
:
'0 10px'
}}
>
Number
of
Nodes
:
<
/label>
<
div
style
=
{{
display
:
'flex'
,
alignItems
:
'center'
}}
>
<
button
className
=
'plusMinusButton'
onClick
=
{()
=>
handleDecrease
(
'concept'
)}
>-<
/button>
<
button
className
=
'plusMinusButton'
onClick
=
{()
=>
handleIncrease
(
'concept'
)}
>+<
/button>
<
input
id
=
'conceptSlider'
type
=
"range"
min
=
"1"
max
=
{
maxConceptRank
}
value
=
{
conceptThreshold
}
onChange
=
{(
e
)
=>
{
setConceptThreshold
(
parseInt
(
e
.
target
.
value
));
redrawGraph
();
}}
/>
{
conceptThreshold
}
<
/div>
<
div
style
=
{{
display
:
'flex'
,
alignItems
:
'center'
}}
>
<
button
className
=
'plusMinusButton'
onClick
=
{()
=>
handleDecrease
(
'object'
)}
>-<
/button>
<
button
className
=
'plusMinusButton'
onClick
=
{()
=>
handleIncrease
(
'object'
)}
>+<
/button>
<
input
id
=
'objectSlider'
type
=
"range"
min
=
"1"
max
=
{
maxObjectRank
}
// Assuming maxObjectRank holds the max rank for object nodes
value
=
{
objectThreshold
}
onChange
=
{(
e
)
=>
{
setObjectThreshold
(
parseInt
(
e
.
target
.
value
));
redrawGraph
();
}}
/>
{
objectThreshold
}
<
/div>
<
br
/>
<
label
style
=
{{
margin
:
'0 10px'
}}
>
Size
of
Nodes
:
<
/label>
<
div
style
=
{{
display
:
'flex'
,
alignItems
:
'center'
}}
>
<
button
className
=
'plusMinusButton'
onClick
=
{()
=>
handleDecrease
(
'size'
)}
>-<
/button>
<
button
className
=
'plusMinusButton'
onClick
=
{()
=>
handleIncrease
(
'size'
)}
>+<
/button>
<
input
id
=
'sizeSlider'
type
=
"range"
min
=
'1'
max
=
{
10
}
// Assuming maxObjectRank holds the max rank for object nodes
value
=
{
nodeSize
}
onChange
=
{(
e
)
=>
{
setNodeSize
(
parseInt
(
e
.
target
.
value
));
redrawGraph
();
}}
/>
{
nodeSize
}
<
/div>
<
div
style
=
{{
margin
:
'10px 0'
}}
>
<
input
type
=
"checkbox"
id
=
'showLinksCheckbox'
checked
=
{
showLinks
}
onChange
=
{
handleShowLinksChange
}
/>
<
label
htmlFor
=
'showLinksCheckbox'
>
Show
Links
<
/label>
<
/div>
<
div
style
=
{{
margin
:
'10px 0'
}}
>
<
button
onClick
=
{
handleRecomputePositioning
}
>
Recompute
Positioning
<
/button>
<
/div>
<
/div>
{
/* SVG container */
}
<
div
style
=
{{
flex
:
'1'
,
overflow
:
'hidden'
}}
>
{
/* Taking up remaining space */
}
<
svg
ref
=
{
svgRef
}
><
/svg>
<
/div>
<
/div>
);
}
export
default
ForceGraph
;
Event Timeline
Log In to Comment