Built with blockbuilder.org
forked from eyaler's block: Force-Directed Graph with Drag/Zoom/Pan/Center/Resize/Labels/Shapes/Filter/Highlight
xxxxxxxxxx
<meta charset="utf-8">
<style>
body {
overflow:hidden;
margin:0;
}
text {
font-family: "Helvetica", "sans", "sans-serif";
pointer-events: none;
}
#user{
color: "gray";
}
#rCount{
color: #d62728;
}
#cCount{
color: #759FB8;
}
#coCount{
color: #ff8914;
}
</style>
<body>
<script src="https://d3js.org/d3.v3.min.js"></script>
<!-- search input -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script type='text/javascript' src="https://code.jquery.com/ui/1.11.3/jquery-ui.min.js"> </script>
<script type='text/javascript' src="https://code.jquery.com/ui/1.11.3/themes/smoothness/jquery-ui.css"> </script>
<div class="ui-widget">
<input id="search" placeholder="search by name">
<button type="button" onclick="searchNode()">Find</button>
<select id="circlesize" style="float:right">
<option value="total">Total Interactions</option>
<option value="discussions">Discussions</option>
<option value="replies">Replies</option>
<option value="comments">Comments</option>
<option value="connections">Connections</option>
</select>
</div>
<div class="report" style="text-align:center">
<h2 style="color:gray; font-family:Helvetica"><span id="user"> . </span><span id="dCount"></span> Discussions . <span id="rCount"></span> Replies . <span id="coCount"></span> Comments . <span id="cCount"></span> Connections<span id="user"> . </span></h2>
</div>
<div id="graph"></div>
<script>
var optArray = []; //place holder for search names
var w = window.innerWidth,
h = window.innerHeight;
var focus_node = null,
highlight_node = null;
var text_center = false;
var highlight_color = "#759FB8";
var highlight_trans = 0.1;
var size = d3.scale.linear()
.range([20,120]);
var thickness = d3.scale.linear()
.range([1, 20]);
var force = d3.layout.force()
.linkDistance(300)
.charge(-2000)
.size([w,h]);
var default_comments_color = "#ff8914";
var default_replies_color ="#d62728";
var default_link_color = "#dddddd";
var nominal_base_node_size = 8;
var nominal_text_size = 10;
var max_text_size = 24;
var nominal_stroke = 4.86;
var max_stroke = 5.5;
var max_base_node_size = 41;
var min_zoom = .2;
var max_zoom = 7;
var svg = d3.select("#graph").append("svg");
var zoom = d3.behavior.zoom().scaleExtent([min_zoom,max_zoom])
var g = svg.append("g");
svg.style("cursor","move");
var totalDiscussions = 0,
totalReplies = 0,
totalConnections = 0,
totalComments = 0;
d3.json("graph.json", function(error, graph) {
var linkedByIndex = {};
graph.links.forEach(function(d) {
linkedByIndex[d.source + "," + d.target] = true;
});
function isConnected(a, b) {
return linkedByIndex[a.index + "," + b.index] || linkedByIndex[b.index + "," + a.index] || a.index == b.index;
}
function hasConnections(a) {
for (var property in linkedByIndex) {
s = property.split(",");
if ((s[0] == a.index || s[1] == a.index) && linkedByIndex[property]) return true;
}
return false;
}
//set size domain based on input value
size.domain([1, d3.max(graph.nodes, function(d) { return +d.total; })]);
thickness.domain([1, d3.max(graph.links, function(d) { return +d.value; })]);
// collect all the node names for search auto-complete
for (var i = 0; i < graph.nodes.length; i++) {
optArray.push(graph.nodes[i].id);
}
optArray = optArray.sort();
// assign number of total discussions
totalDiscussions = graph.totalDiscussions;
totalComments = graph.totalComments;
// calculate total replies
graph.links.forEach(function(d){totalReplies+=d['value']});
// calculate total people
totalConnections = graph.nodes.length;
updateReport();
// align nodes along a diagonal line to minimize number of iterations
var n = graph.nodes.Length;
graph.nodes.forEach(function(d, i) {
d.x = d.y = w / n * i;
});
force
.nodes(graph.nodes)
.links(graph.links)
.start();
// add lines between people
var link = g.selectAll(".link")
.data(graph.links)
.enter().append("line")
.attr("class", "link")
.style("stroke-width", function(d){return thickness(d.value)})
.style("stroke", default_link_color)
var node = g.selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag)
// add circle clip
var clipPath = node.append("clipPath")
.attr("id", function(d){return "clipCircle_" + d.id})
.append("circle")
.attr("r", function(d){return size(d.total)/2});
var image = node.append("image")
.attr("xlink:href", function(d){return d.img + "?width=" + parseInt(2 * size(d.total)) + "&height=" + parseInt(2 * size(d.total)) + "&crop=1%3A1"})
.attr("x", function(d){return -size(d.total)/2})
.attr("y", function(d){return -size(d.total)/2})
.attr("width", function(d){return size(d.total)})
.attr("height", function(d){return size(d.total)})
.attr("clip-path", function(d){return "url(#clipCircle_" + d.id +")"});
var repliesArc = node.append("path")
.attr("class", "replyPath")
.style("fill", default_replies_color)
.attr("d", d3.svg.arc()
.innerRadius(function(d){return size(d.total)/2})
.outerRadius(function(d){return size(d.total)/2 + 7})
.startAngle(Math.PI)
.endAngle(calculateRepliesAngle)
);
var commentsArc = node.append("path")
.attr("class", "commentPath")
.style("fill", default_comments_color)
.attr("d", d3.svg.arc()
.innerRadius(function(d){return size(d.total)/2})
.outerRadius(function(d){return size(d.total)/2 + 7})
.startAngle(calculateCommentsAngleStart)
.endAngle(calculateCommentsAngleEnd)
);
var text = g.selectAll(".text")
.data(graph.nodes)
.enter().append("text")
.attr("dy", ".35em")
.attr("class", "text")
.style("font-size", nominal_text_size + "px");
if (text_center)
text.text(function(d) { return d.id; })
.style("text-anchor", "middle");
else
text.attr("dx", function(d) {return (size(d.total)/2||nominal_base_node_size);})
.text(function(d) { return '\u2002'+d.id; });
function updateReport(d){
if (d=== undefined){
d3.select("span#user").text(' . ');
d3.select("span#dCount").text(totalDiscussions);
d3.select("span#rCount").text(totalReplies);
d3.select("span#cCount").text(totalConnections);
d3.select("span#coCount").text(totalComments);
}else{
d3.select("span#user").text(d.id + ' > ');
d3.select("span#dCount").text(d.discussions);
d3.select("span#rCount").text(d.replies);
d3.select("span#cCount").text(d.connections);
d3.select("span#coCount").text(d.comments);
}
}
//set events
node
.on("mouseover", function(d){set_highlight(d);})
.on("mousedown", function(d) {
d3.event.stopPropagation(); //so user can move the node around
focus_node = d;
set_focus(d);
if (highlight_node === null) set_highlight(d)
})
.on("mouseout", function(d) {exit_highlight();});
d3.select(window).on("mouseup",
function() {
if (focus_node!==null)
{
focus_node = null;
if (highlight_trans<1)
{
updateReport();
commentsArc.style("opacity", 1);
repliesArc.style("opacity", 1);
image.style("opacity", 1);
text.style("opacity", 1);
link.style("opacity", 1);
}
}
if (highlight_node === null) exit_highlight();
});
// when user has mouse down on one of the circles
function set_focus(d){
if (highlight_trans <1){
updateReport(d);
commentsArc.style("opacity", function(o) {
return isConnected(d, o) ? 1 : highlight_trans;
});
repliesArc.style("opacity", function(o) {
return isConnected(d, o) ? 1 : highlight_trans;
});
image.style("opacity", function(o) {
return isConnected(d, o) ? 1 : 2 * highlight_trans;
});
text.style("opacity", function(o) {
return isConnected(d, o) ? 1 : highlight_trans;
});
link.style("opacity", function(o) {
return o.source.index == d.index || o.target.index == d.index ? 1 : highlight_trans;
});
}
}
function set_highlight(d)
{
svg.style("cursor","pointer");
updateReport(d);
if (focus_node!==null) d = focus_node;
highlight_node = d;
if (highlight_color!="white")
{
text.style("font-weight", function(o) {
return isConnected(d, o) ? "bold" : "normal";});
link.style("stroke", function(o) {
return o.source.index == d.index || o.target.index == d.index ? highlight_color:default_link_color;
});
}
}
function exit_highlight(){
updateReport();
highlight_node = null;
if (focus_node===null)
{
svg.style("cursor","move");
if (highlight_color!="white")
{
text.style("font-weight", "normal");
link.style("stroke", default_link_color);
}
}
}
zoom.on("zoom", function() {
g.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
});
svg.call(zoom);
resize();
d3.select(window).on("resize", resize);
force.on("tick", function() {
node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
text.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
link.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
});
function resize() {
var width = window.innerWidth, height = window.innerHeight;
svg.attr("width", width).attr("height", height);
force.size([force.size()[0]+(width-w)/zoom.scale(),force.size()[1]+(height-h)/zoom.scale()]).resume();
w = width;
h = height;
}
});
function calculateRepliesAngle(d){
var fraction = d.replies/d.total;
return Math.PI + fraction * 2 * Math.PI;
}
function calculateCommentsAngleStart(d){
return calculateRepliesAngle(d);
}
function calculateCommentsAngleEnd(d){
var fraction = d.comments/d.total;
return calculateRepliesAngle(d) + fraction * 2 * Math.PI;
}
// update for resizing nodes
d3.select("#circlesize")
.on("change", function(d) {
var sizedBy = d3.select(this).property("value");
resizeNodes(sizedBy);
});
function resizeNodes(parameter){
// add circle clip
nodes = d3.selectAll(".node");
//set size domain based on input value
size.domain([1, d3.max(nodes.data(), function(d) { return +d[parameter]; })]);
nodes.selectAll("circle")
.attr("r", function(d){return size(d[parameter])/2});
nodes.selectAll("Image")
.attr("xlink:href", function(d){return d.img + "?width=" + parseInt(2 * size(d[parameter])) + "&height=" + parseInt(2 * size(d[parameter])) + "&crop=1%3A1"})
.attr("x", function(d){return -size(d[parameter])/2})
.attr("y", function(d){return -size(d[parameter])/2})
.attr("width", function(d){return size(d[parameter])})
.attr("height", function(d){return size(d[parameter])});
nodes.selectAll(".replyPath")
.attr("d", d3.svg.arc()
.innerRadius(function(d){return size(d[parameter])/2})
.outerRadius(function(d){return size(d[parameter])/2 + 7})
.startAngle(Math.PI)
.endAngle(calculateRepliesAngle));
nodes.selectAll(".commentPath")
.attr("d", d3.svg.arc()
.innerRadius(function(d){return size(d[parameter])/2})
.outerRadius(function(d){return size(d[parameter])/2 + 7})
.startAngle(calculateCommentsAngleStart)
.endAngle(calculateCommentsAngleEnd)
);
d3.selectAll(".text")
.attr("dx", function(d){return size(d[parameter])/2;});
force.start();
}
// assign optArray to search box
// Search box is modified from this post > https://www.coppelia.io/2014/07/an-a-to-z-of-extra-features-for-the-d3-force-layout/
$(function () {
$("#search").autocomplete({
source: optArray
});
});
function searchNode() {
//find the node
var selectedVal = document.getElementById('search').value;
svg.selectAll(".node")
.filter(function (d) { return d.id != selectedVal;})
.style("opacity", highlight_trans/2)
.transition()
.duration(5000)
.style("opacity", 1);
svg.selectAll(".link, .text, .replyPath, .commentPath")
.style("opacity", highlight_trans/2)
.transition()
.duration(5000)
.style("opacity", 1);
}
function isNumber(n) { return !isNaN(parseFloat(n)) && isFinite(n);}
</script>
Modified http://d3js.org/d3.v3.min.js to a secure url
Modified http://code.jquery.com/ui/1.11.3/jquery-ui.min.js to a secure url
Modified http://code.jquery.com/ui/1.11.3/themes/smoothness/jquery-ui.css to a secure url
https://d3js.org/d3.v3.min.js
https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js
https://code.jquery.com/ui/1.11.3/jquery-ui.min.js
https://code.jquery.com/ui/1.11.3/themes/smoothness/jquery-ui.css