Control arrowheads dimension on a force-directed layout in d3.js - javascript

i've done a graph and now i want to add directed links.
I've a problem because i want the function that when i move the mouse over the link it gets bigger and when i click on the node it gets bigger and this two functions make a mess with my arrow on the links.
I've tryied many solutions found here on stackoverflow but nothing worked for me...Can somebody help me? Thanks
here my code:
<!DOCTYPE html>
<meta charset="utf-8">
<title>Modifying a force layout v4</title>
<style>
.link {
stroke: #3B3B3B;
/*stroke-width: 1px;*/
}
.node {
stroke: #000;
stroke-width: 1.5px;
}
.svg {
border:3px solid black;
border-radius:12px;
margin:auto;
}
#arrow {
fill:green;
}
</style>
<body>
Node:
<div id="log"></div>
Link:
<div id="log2"></div>
<script src="//d3js.org/d3.v4.js"></script>
<script>
var width = 960,
height = 500;
radius = 17;
var expnsed = false;
var color = d3.scaleOrdinal(d3.schemeCategory20);
var nodes = [],
links = [];
var charge = d3.forceManyBody().strength(-150);
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().distance(130).strength(.7))
.force("charge", charge)
// use forceX and forceY instead to change the relative positioning
// .force("centering", d3.forceCenter(width/2, height/2))
.force("x", d3.forceX(width/2))
.force("y", d3.forceY(height/2))
.on("tick", tick);
var svg = d3.select("body").append("svg").attr("class","svg")
.attr("width", width)
.attr("height", height);
svg.append("defs").append("marker")
.attr("id", "arrow")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 15)
.attr("refY", 0)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
//.attr("fill","red")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
var url = "https://api.myjson.com/bins/lj6ob";
d3.json(url, function(error, graph) {
if (error) throw error;
nodes = graph.nodes;
links=graph.links;
console.log("graph.links.length: "+links.length)
for (var i = 0; i < links.length; i++){
links[i].source = find(links[i].source);
links[i].target = find(links[i].target);
}
console.log("Link source: " + links[0].target)
start();
})
function find(name){
for(var i = 0; i < nodes.length; i++){
if (name == nodes[i].id){
console.log("name: " + name)
console.log("id: " + nodes[i].id)
return i;
}
}
}
function start() {
var nodeElements = svg.selectAll(".node").data(nodes, function(d){return d.id});
var linkElements = svg.selectAll(".line").data(links).attr("class","links");
//console.log(nodes)
nodeElements.enter().append("circle").attr("class", function(d) {return "node " + d.index; }).attr("r", 17).attr("fill", function(d) { return color(d.group); });
linkElements.enter().insert("line", ".node").attr("class", "link").attr("stroke-width", function(d) { return Math.sqrt(d.value)*2;});
d3.selectAll("line").attr("marker-end", "url(#arrow)");
nodeElements.exit().remove();
linkElements.exit().remove();
simulation.nodes(nodes)
simulation.force("link").links(links)
// NOTE: Very important to call both alphaTarget AND restart in conjunction
// Restart by itself will reset alpha (cooling of simulation)
// but won't reset the velocities of the nodes (inertia)
//remove alpha for slow incoming!!
//.alpha(1)
simulation.alpha(1).restart();
}
function tick() {
var nodeElements = svg.selectAll(".node");
var linkElements = svg.selectAll(".link");
linkElements.append("title")
.text(function(d) { return "value link: " + d.value; });
linkElements.on("mouseover", function(d) {
var g = d3.select(this); // The node
document.getElementById('log2').innerHTML = '<br> '+d.value;
g.attr("stroke-width", function(d) { return Math.sqrt(d.value)*4; })
});
linkElements.on("mouseout", function(d) {
var g = d3.select(this); // The node
g.attr("stroke-width", function(d) { return Math.sqrt(d.value)*2; })
});
nodeElements.append("title")
.text(function(d) {
return "node name: "+d.id + ", node group: "+d.group;
});
nodeElements.on("mouseout", function(d) {
var g = d3.select(this); // The node
g.attr("fill", function(d) { return color(d.group); })
})
.on("mouseover", function(d) {
document.getElementById('log').innerHTML = '<br> '+d.id;
var g = d3.select(this); // The node
g.attr("fill", "red")
});
nodeElements.on("click",click);
nodeElements.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
/*nodeElements.attr("cx", function(d,i) {return d.x; })
.attr("cy", function(d) { return d.y; })*/
nodeElements.attr("cx", function(d) { return d.x = Math.max(radius, Math.min(width - radius, d.x)); })
.attr("cy", function(d) { return d.y = Math.max(radius, Math.min(height - radius, d.y)); });
linkElements
.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;});
}
function click(d) {
d3.select(this).attr("r",
function(d){if (d3.select(this).attr("r")==17) {radius = 23;return "23"}else{radius = 17; return "17"}}
);
//expand();
var E = "E";
if(d.id == E && expand){
expand=false;
//expand_2();
}
};
function expand(){
console.log("expand")
b = createNode("A")
nodes.push(b);
start();
}
function expand_2(){
d3.json("nodes_2.json", function(error, graph) {
for(var i = 0; i < graph.nodes.length; i++){
var n = graph.nodes[i];
nodes.push(n);
}
for(var i = 0; i < graph.links.length; i++){
graph.links[i].source = find(graph.links[i].source)
graph.links[i].target = find(graph.links[i].target)
var l = graph.links[i];
links.push(l);
}
start();
})
}
function zoomed() {
g.attr("transform", d3.event.transform);
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
function createNode(id) {
return {id: id, x: width/2, y:height/2}
}
</script>

You can give the arrow a fixed size if you define markerUnits="userSpaceOnUse". To recreate your current sizes:
svg.append("defs").append("marker")
.attr("id", "arrow")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 15)
.attr("refY", 0)
.attr("markerUnits", "userSpaceOnUse")
.attr("markerWidth", 30)
.attr("markerHeight", 30)
.attr("orient", "auto")
If you want to resize the marker on hover, the best solution might be to have a second marker element arrow-hover that is bigger and that you temporarily exchange via CSS:
line {
marker-end: url(#arrow);
}
line:hover {
marker-end: url(#arrow-hover);
}

Related

D3 how to exclude links from Gooey effect

Problem: The Gooey effect applies to the links too. Which creates a teardrop shaped frame instead of an circle.
The snipped contains a dragged() function which allows the user to tear off node 1 from node 0. Further it is possible to connect node 1 with node 0 again with the help of dragging. The code isnĀ“t clean at all, as its a playground only.
Goal: How can I exclude the links from the Gooey effect in a way, that all links are displayed correctly and still achieve a proper circled shape. The shape of the Gooey effect can be manipulated by changing the -5 to -40, unfortunately it will hide the links completely:
.attr("values", "1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 50 -5")
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>D3v6 Playground</title>
<!-- call external d3.js framework -->
<script src="https://d3js.org/d3.v6.js"></script>
</head>
<style>
body {
overflow: hidden;
margin: 0px;
}
.canvas {
background-color: rgb(220, 220, 220);
}
.node {
cursor: pointer;
}
.node:hover {
stroke: red
}
.link {
fill: none;
cursor: default;
stroke: rgb(0, 0, 0);
stroke-width: 3px;
}
</style>
<body>
<svg id="svg"> </svg>
<script>
var graph = {
"nodes": [
{
"id": 0,
},
{
"id": 1,
},
{
"id": 2,
}
],
"links": [
{
"source": 1,
"target": 0,
},
{
"source": 2,
"target": 0,
},
]
}
var width = window.innerWidth
var height = window.innerHeight
var svg = d3.select("svg")
.attr("class", "canvas")
.attr("width", width)
.attr("height", height)
.call(d3.zoom().on("zoom", function (event) {
svg.attr("transform", event.transform)
}))
.append("g")
.style("filter", "url(#gooey)")
// remove zoom on dblclick listener
d3.select("svg").on("dblclick.zoom", null)
var linkContainer = svg.append("g").attr("class", "linkContainer")
var nodeContainer = svg.append("g").attr("class", "nodeContainer")
var isSpliced = false;
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function (d) {
return d.id;
}).distance(150))
.force("charge", d3.forceManyBody().strength(-50))
.force("center", d3.forceCenter(width / 2, height / 2))
.force("collision", d3.forceCollide().radius(30))
//###############################################
//######### SVG Filter for Gooey effect #########
//###############################################
var defs = svg.append("defs");
var filter = defs.append("filter").attr("id", "gooey");
filter.append("feGaussianBlur")
.attr("in", "SourceGraphic")
.attr("stdDeviation", "10")
.attr("result", "blur");
filter.append("feColorMatrix")
.attr("in", "blur")
.attr("mode", "matrix")
.attr("values", "1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 50 -5")
.attr("result", "gooey");
filter.append("feComposite")
.attr("in", "SourceGraphic")
.attr("in2", "gooey")
.attr("operator", "atop");
initialize()
//###############################################
//############## Initialization #################
//###############################################
function initialize() {
link = linkContainer.selectAll(".link")
.data(graph.links)
.join("line")
.attr("class", "link")
node = nodeContainer.selectAll(".node")
.data(graph.nodes, d => d.id)
.join("g")
.attr("class", "node")
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded)
)
node.selectAll("circle")
.data(d => [d])
.join("circle")
.attr("r", 30)
.style("fill", "white")
node.selectAll("text")
.data(d => [d])
.join("text")
.style("class", "icon")
.attr("font-family", "FontAwesome")
.attr("dominant-baseline", "central")
.attr("text-anchor", "middle")
.attr("font-size", 30)
.attr("fill", "black")
.attr("stroke-width", "0px")
.attr("pointer-events", "none")
.text((d) => {
return d.id
})
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation
.force("link")
.links(graph.links)
}
//###############################################
//############# Update Positions ################
//###############################################
function ticked() {
// update link positions
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;
});
// update node positions
node
.attr("transform", function (d) {
return "translate(" + d.x + ", " + d.y + ")";
});
}
//###############################################
//################ Drag Nodes ###################
//###############################################
var lineX;
var lineY;
var isPlugged = true;
function dragStarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
lineX = d.x
lineY = d.y
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
var root = graph.nodes.find(element => (element.id === 0))
var distance = (root.x - d.x) * 2 + (root.y - d.y) * 2
if (isPlugged && d.id === 1) {
var indexOfLink = graph.links.findIndex(element => (element.source.id === d.id))
if (distance < 0) {
distance = -distance
}
if (distance > 1000) {
graph.links.splice(indexOfLink, 1)
isPlugged = false;
}
} else {
if (distance < 0) {
distance = -distance
}
if (distance < 20) {
graph.links.push({ source: d.id, target: root.id })
isPlugged = true;
}
}
initialize()
}
function dragEnded(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = undefined;
d.fy = undefined;
link.filter((a) => {
return a.source.id === d.id
}).style("stroke", "black")
}
</script>
</body>
</html>
Just apply the style to nodeContainer instead of svg - see comments below:
var svg = d3.select("svg")
.attr("class", "canvas")
.attr("width", width)
.attr("height", height)
.call(d3.zoom().on("zoom", function (event) {
svg.attr("transform", event.transform)
}))
.append("g")
// <--------- remove the style here
// remove zoom on dblclick listener
d3.select("svg").on("dblclick.zoom", null)
var linkContainer = svg.append("g").attr("class", "linkContainer")
var nodeContainer = svg.append("g").attr("class", "nodeContainer")
.style("filter", "url(#gooey)") // <---------- add the style here
Your code updated:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>D3v6 Playground</title>
<!-- call external d3.js framework -->
<script src="https://d3js.org/d3.v6.js"></script>
</head>
<style>
body {
overflow: hidden;
margin: 0px;
}
.canvas {
background-color: rgb(220, 220, 220);
}
.node {
cursor: pointer;
}
.node:hover {
stroke: red
}
.link {
fill: none;
cursor: default;
stroke: rgb(0, 0, 0);
stroke-width: 3px;
}
</style>
<body>
<svg id="svg"> </svg>
<script>
var graph = {
"nodes": [
{
"id": 0,
},
{
"id": 1,
},
{
"id": 2,
}
],
"links": [
{
"source": 1,
"target": 0,
},
{
"source": 2,
"target": 0,
},
]
}
var width = window.innerWidth
var height = window.innerHeight
var svg = d3.select("svg")
.attr("class", "canvas")
.attr("width", width)
.attr("height", height)
.call(d3.zoom().on("zoom", function (event) {
svg.attr("transform", event.transform)
}))
.append("g")
// remove zoom on dblclick listener
d3.select("svg").on("dblclick.zoom", null)
var linkContainer = svg.append("g").attr("class", "linkContainer")
var nodeContainer = svg.append("g").attr("class", "nodeContainer")
.style("filter", "url(#gooey)")
var isSpliced = false;
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function (d) {
return d.id;
}).distance(150))
.force("charge", d3.forceManyBody().strength(-50))
.force("center", d3.forceCenter(width / 2, height / 2))
.force("collision", d3.forceCollide().radius(30))
//###############################################
//######### SVG Filter for Gooey effect #########
//###############################################
var defs = svg.append("defs");
var filter = defs.append("filter").attr("id", "gooey");
filter.append("feGaussianBlur")
.attr("in", "SourceGraphic")
.attr("stdDeviation", "10")
.attr("result", "blur");
filter.append("feColorMatrix")
.attr("in", "blur")
.attr("mode", "matrix")
.attr("values", "1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 50 -5")
.attr("result", "gooey");
filter.append("feComposite")
.attr("in", "SourceGraphic")
.attr("in2", "gooey")
.attr("operator", "atop");
initialize()
//###############################################
//############## Initialization #################
//###############################################
function initialize() {
link = linkContainer.selectAll(".link")
.data(graph.links)
.join("line")
.attr("class", "link")
node = nodeContainer.selectAll(".node")
.data(graph.nodes, d => d.id)
.join("g")
.attr("class", "node")
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded)
)
node.selectAll("circle")
.data(d => [d])
.join("circle")
.attr("r", 30)
.style("fill", "white")
node.selectAll("text")
.data(d => [d])
.join("text")
.style("class", "icon")
.attr("font-family", "FontAwesome")
.attr("dominant-baseline", "central")
.attr("text-anchor", "middle")
.attr("font-size", 30)
.attr("fill", "black")
.attr("stroke-width", "0px")
.attr("pointer-events", "none")
.text((d) => {
return d.id
})
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation
.force("link")
.links(graph.links)
}
//###############################################
//############# Update Positions ################
//###############################################
function ticked() {
// update link positions
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;
});
// update node positions
node
.attr("transform", function (d) {
return "translate(" + d.x + ", " + d.y + ")";
});
}
//###############################################
//################ Drag Nodes ###################
//###############################################
var lineX;
var lineY;
var isPlugged = true;
function dragStarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
lineX = d.x
lineY = d.y
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
var root = graph.nodes.find(element => (element.id === 0))
var distance = (root.x - d.x) * 2 + (root.y - d.y) * 2
if (isPlugged && d.id === 1) {
var indexOfLink = graph.links.findIndex(element => (element.source.id === d.id))
if (distance < 0) {
distance = -distance
}
if (distance > 1000) {
graph.links.splice(indexOfLink, 1)
isPlugged = false;
}
} else {
if (distance < 0) {
distance = -distance
}
if (distance < 20) {
graph.links.push({ source: d.id, target: root.id })
isPlugged = true;
}
}
initialize()
}
function dragEnded(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = undefined;
d.fy = undefined;
link.filter((a) => {
return a.source.id === d.id
}).style("stroke", "black")
}
</script>
</body>
</html>

How to collapse groups of circle in D3js?

I'm creating a d3js graph where there are multiple nodes connected to multiple users. So far I've been able to set up the graph with a center node and then group them together. I want these groups to collapse when there's a click on the center node, somewhat similar to this example here.
My sample code for the same is here:
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }).strength(0.9))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2))
d3.json('demo.json', function(error, graph) {
if (error) throw error;
drawGraph("Artifacts");
function drawGraph(selectedValue) {
// create groups, links and nodes
groups = svg.append('g').attr('class', 'groups');
link = svg.append('g')
.attr('class', 'links')
.selectAll('line')
.data(graph.links)
.enter().append('line')
.attr('stroke-width', function (d) {
return Math.sqrt(d.value);
});
node = svg.append('g')
.attr('class', 'nodes')
.selectAll('circle')
.data(graph.nodes)
.enter().append('circle')
.attr('r', function (d) {
if (d.type == "agent") {
return 10
}
return 5
})
.attr('fill', function (d) {
if (selectedValue === "Artifacts") {
if (d.type == "Process") {
return "white"
} else if (d.type == "user") {
return "blue"
} else if (d.type == "File") {
return "green"
} else if (d.type == "File") {
return "green"
} else if (d.type == "agent") {
return color(d.group);
}
} else {
console.log(d.threatscore);
if(d.threatscore>=0&&d.threatscore<3){
if(d.type=="agent"){
return color(d.group);
}else {
return "green"
}
}
else if(d.threatscore>=3&&d.threatscore<6){
return "yellow"
}
if(d.threatscore>=6&&d.threatscore<9){
return "red"
}
}
})
// .attr('fill', function(d) { return color(d.group); })
.call(d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended));
var tip;
svg.on("click", function () {
if (tip) tip.remove();
});
node.on("click", function (d) {
d3.event.stopPropagation();
if (tip) tip.remove();
tip = svg.append("g")
.attr("transform", "translate(" + d.x + "," + d.y + ")");
var rect = tip.append("rect")
.style("fill", "white")
.style("stroke", "steelblue");
tip.append("text")
.text("Name: " + d.name)
.attr("dy", "1em")
.attr("x", 5);
tip.append("text")
.text("Type: " + d.type)
.attr("dy", "2em")
.attr("x", 5);
var con = graph.links
.filter(function (d1) {
return d1.source.id === d.id;
})
.map(function (d1) {
return d1.target.name;
})
tip.append("text")
.text("Connected to: " + con.join(","))
.attr("dy", "3em")
.attr("x", 5);
tip.append("text")
.text("Threat Score: " + d.threatscore)
.attr("dy", "4em")
.attr("x", 5);
tip.append("text")
.text("Labels: " + d.labels)
.attr("dy", "5em")
.attr("x", 5);
tip.append("text")
.text("Artifact ID: " + d.artifactid)
.attr("dy", "6em")
.attr("x", 5);
tip.append("text")
.html("More Information : <a href='dashboard#ajax/host_details.html?'" + d.artifactid + "'> " + d.artifactid + "</a>")
.attr("dy", "8em")
.attr("x", 5);
var bbox = tip.node().getBBox();
rect.attr("width", bbox.width + 5)
.attr("height", bbox.height + 5)
});
// count members of each group. Groups with less
// than 3 member will not be considered (creating
// a convex hull need 3 points at least)
groupIds = d3.set(graph.nodes.map(function (n) {
return +n.group;
}))
.values()
.map(function (groupId) {
return {
groupId: groupId,
count: graph.nodes.filter(function (n) {
return +n.group == groupId;
}).length
};
})
.filter(function (group) {
return group.count > 2;
})
.map(function (group) {
return group.groupId;
});
paths = groups.selectAll('.path_placeholder')
.data(groupIds, function (d) {
return +d;
})
.enter()
.append('g')
.attr('class', 'path_placeholder')
.append('path')
.attr('stroke', function (d) {
return color(d);
})
.attr('fill', function (d) {
return color(d);
})
.attr('opacity', 0);
paths
.transition()
.duration(2000)
.attr('opacity', 1);
// add interaction to the groups
groups.selectAll('.path_placeholder')
.call(d3.drag()
.on('start', group_dragstarted)
.on('drag', group_dragged)
.on('end', group_dragended)
);
node.append('title')
.text(function (d) {
return d.type + " - " + d.name;
});
simulation
.nodes(graph.nodes)
.on('tick', ticked)
.force('link')
// .force("link", d3.forceLink().distance(function(d) {return d.distance;}).strength(0.1))
.links(graph.links);
function ticked() {
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;
});
updateGroups();
}
}
});
// select nodes of the group, retrieve its positions
// and return the convex hull of the specified points
// (3 points as minimum, otherwise returns null)
var polygonGenerator = function(groupId) {
var node_coords = node
.filter(function(d) { return d.group == groupId; })
.data()
.map(function(d) { return [d.x, d.y]; });
console.log("Came here",node_coords)
return d3.polygonHull(node_coords);
};
function updateGroups() {
groupIds.forEach(function(groupId) {
var path = paths.filter(function(d) { return d == groupId;})
.attr('transform', 'scale(1) translate(0,0)')
.attr('d', function(d) {
polygon = polygonGenerator(d);
centroid = d3.polygonCentroid(polygon);
return valueline(
polygon.map(function(point) {
return [ point[0] - centroid[0], point[1] - centroid[1] ];
})
);
});
d3.select(path.node().parentNode).attr('transform', 'translate(' + centroid[0] + ',' + (centroid[1]) + ') scale(' + scaleFactor + ')');
});
}
// drag nodes
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
// drag groups
function group_dragstarted(groupId) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d3.select(this).select('path').style('stroke-width', 3);
}
function group_dragged(groupId) {
node
.filter(function(d) { return d.group == groupId; })
.each(function(d) {
d.x += d3.event.dx;
d.y += d3.event.dy;
})
}
function group_dragended(groupId) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d3.select(this).select('path').style('stroke-width', 1);
}
I want these groups to collapse into one on clicking the center node and then expanding on doing the same later.

How to make the extent of a brush match the size of the parent element?

I am trying to combine d3-brush with d3-zoom in a force-directed graph. Although I managed to combine the modules, it seems that I am not able to use the brush on the whole g element which contains the nodes.Thus, I am not able to select all the nodes in the graph.
I have already had a look at some blocks, e.g. D3 Selectable Force-Directed Graph,https://bl.ocks.org/mbostock/4566102, D3v4 Selectable, Draggable, Zoomable Force Directed Graph which try to combine the two modules [1, 3] or use the brush event in a force-directed graph [2]. Moreover, I have also read the d3-brush API. However, I can not achieve the desired behavior.
The user can select a region of points in the graph by holding the ctrl key and pressing the left mouse button (ctrl key + lmb).
Theoretically, the user should be able to select all the nodes within the g element which contains the nodes. In the next figure you can see the maximum brush extent. Nodes outside the brush extent can not be selected.
In the figure below, you can see the DOM hierarchy of the HTML document.
I am suspecting that the extent of the brush is not defined correctly, which causes this issue.If I apply the brush to the svgForce element and the nodes are also drawn directly on the svgForce element, then the brush extent matches the dimensions of the svg. However, in that case, the zoom does not work properly, because it is not bound to a g element.
svgForce.append("g")
.attr("class", "brush")
.call(d3.brush().on("brush", brushed));
Do you have any ideas what might be going wrong?
var width_network = 700,
height_network = 700,
gForce,
gMain,
zoom,
brush,
CIRCLE_RADIUS = 10,
link,
node,
nodeLabel,
gBrush;
var network = {"nodes":[{"name":"A"},{"name":"B"},{"name":"C"},{"name":"D"},{"name":"E"},{"name":"F"},{"name":"G"},{"name":"H"},{"name":"I"},{"name":"J"},{"name":"K"},{"name":"L"},{"name":"M"},{"name":"N"},{"name":"O"},{"name":"P"},{"name":"Q"},{"name":"R"},{"name":"S"},{"name":"T"},{"name":"U"},{"name":"V"},{"name":"W"},{"name":"X"},{"name":"Y"},{"name":"Z"},{"name":"AA"},{"name":"BB"},{"name":"CC"},{"name":"DD"},{"name":"EE"},{"name":"FF"},{"name":"GG"},{"name":"HH"},{"name":"II"},{"name":"JJ"},{"name":"KK"},{"name":"LL"},{"name":"MM"},{"name":"NN"},{"name":"OO"},{"name":"PP"},{"name":"QQ"},{"name":"RR"},{"name":"SS"},{"name":"TT"},{"name":"UU"},{"name":"VV"},{"name":"WW"},{"name":"XX"},{"name":"YY"},{"name":"ZZ"},{"name":"AAA"},{"name":"BBB"},{"name":"CCC"},{"name":"DDD"},{"name":"EEE"},{"name":"FFF"},{"name":"GGG"},{"name":"HHH"},{"name":"III"},{"name":"JJJ"},{"name":"KKK"},{"name":"LLL"},{"name":"MMM"},{"name":"NNN"},{"name":"OOO"},{"name":"PPP"},{"name":"QQQ"},{"name":"RRR"},{"name":"SSS"},{"name":"TTT"},{"name":"UUU"},{"name":"VVV"},{"name":"WWW"},{"name":"XXX"},{"name":"YYY"},{"name":"ZZZ"},{"name":"AAAA"},{"name":"BBBB"},{"name":"CCCC"},{"name":"DDDD"},{"name":"EEEE"},{"name":"FFFF"},{"name":"GGGG"},{"name":"HHHH"},{"name":"IIII"},{"name":"JJJJ"},{"name":"KKKK"},{"name":"LLLL"},{"name":"MMMM"},{"name":"NNNN"},{"name":"OOOO"},{"name":"PPPP"},{"name":"QQQQ"},{"name":"RRRR"},{"name":"SSSS"},{"name":"TTTT"},{"name":"UUUU"},{"name":"VVVV"},{"name":"WWWW"},{"name":"XXXX"},{"name":"YYYY"},{"name":"ZZZZ"},{"name":"A1"},{"name":"B2"},{"name":"C3"},{"name":"D4"},{"name":"E5"},{"name":"F6"},{"name":"G7"},{"name":"H8"},{"name":"I9"},{"name":"J10"},{"name":"K11"},{"name":"L12"},{"name":"M13"},{"name":"N14"},{"name":"O15"},{"name":"P16"},{"name":"Q17"},{"name":"R18"},{"name":"S19"},{"name":"T20"},{"name":"U21"},{"name":"V22"},{"name":"W23"},{"name":"X24"},{"name":"Y25"},{"name":"Z26"},{"name":"A27"},{"name":"B28"},{"name":"C29"},{"name":"D30"},{"name":"E31"},{"name":"F32"},{"name":"G33"},{"name":"H34"},{"name":"I35"},{"name":"J36"},{"name":"K37"},{"name":"L38"},{"name":"M39"},{"name":"N40"},{"name":"O41"},{"name":"P42"},{"name":"Q43"},{"name":"R44"},{"name":"S45"},{"name":"T46"},{"name":"U47"},{"name":"V48"},{"name":"W49"},{"name":"X50"},{"name":"Y51"},{"name":"Z52"},{"name":"A53"},{"name":"B54"},{"name":"C55"},{"name":"D56"},{"name":"E57"},{"name":"F58"},{"name":"G59"},{"name":"H60"},{"name":"I61"},{"name":"J62"},{"name":"K63"},{"name":"L64"},{"name":"M65"},{"name":"N66"},{"name":"O67"},{"name":"P68"},{"name":"Q69"},{"name":"R70"},{"name":"S71"},{"name":"T72"},{"name":"U73"},{"name":"V74"},{"name":"W75"},{"name":"X76"},{"name":"Y77"},{"name":"Z78"},{"name":"A79"},{"name":"B80"},{"name":"C81"},{"name":"D82"},{"name":"E83"},{"name":"F84"},{"name":"G85"},{"name":"H86"},{"name":"I87"},{"name":"J88"},{"name":"K89"},{"name":"L90"},{"name":"M91"},{"name":"N92"},{"name":"O93"},{"name":"P94"},{"name":"Q95"},{"name":"R96"},{"name":"S97"},{"name":"T98"},{"name":"U99"},{"name":"V100"},{"name":"W101"},{"name":"X102"},{"name":"Y103"},{"name":"Z104"},{"name":"A105"},{"name":"B106"},{"name":"C107"},{"name":"D108"},{"name":"E109"},{"name":"F110"},{"name":"G112"},{"name":"H113"},{"name":"I114"},{"name":"J115"},{"name":"K116"},{"name":"L117"},{"name":"M118"},{"name":"N119"},{"name":"O120"},{"name":"P121"},{"name":"Q123"},{"name":"R124"},{"name":"S125"},{"name":"T126"},{"name":"U127"},{"name":"V128"},{"name":"W129"},{"name":"X130"},{"name":"Y131"},{"name":"Z134"},{"name":"A135"},{"name":"B136"},{"name":"C137"},{"name":"D138"},{"name":"E139"},{"name":"F140"},{"name":"G141"}],"links":[{"source":0,"target":1,"value":375},{"source":0,"target":2,"value":27},{"source":0,"target":3,"value":15},{"source":0,"target":4,"value":8},{"source":0,"target":5,"value":6},{"source":0,"target":6,"value":4},{"source":0,"target":7,"value":3},{"source":0,"target":8,"value":3},{"source":0,"target":9,"value":2},{"source":0,"target":10,"value":2},{"source":0,"target":11,"value":2},{"source":0,"target":12,"value":2},{"source":0,"target":13,"value":2},{"source":0,"target":14,"value":2},{"source":0,"target":15,"value":1},{"source":0,"target":16,"value":1},{"source":0,"target":17,"value":1},{"source":0,"target":18,"value":1},{"source":0,"target":19,"value":1},{"source":0,"target":20,"value":1},{"source":0,"target":21,"value":1},{"source":0,"target":22,"value":1},{"source":0,"target":23,"value":87},{"source":0,"target":24,"value":24},{"source":0,"target":25,"value":20},{"source":0,"target":26,"value":20},{"source":0,"target":27,"value":19},{"source":0,"target":28,"value":17},{"source":0,"target":29,"value":12},{"source":0,"target":30,"value":6},{"source":0,"target":31,"value":5},{"source":0,"target":32,"value":5},{"source":0,"target":33,"value":4},{"source":0,"target":34,"value":4},{"source":0,"target":35,"value":3},{"source":0,"target":36,"value":3},{"source":0,"target":37,"value":3},{"source":0,"target":38,"value":3},{"source":0,"target":39,"value":3},{"source":0,"target":40,"value":3},{"source":0,"target":41,"value":2},{"source":0,"target":42,"value":2},{"source":0,"target":43,"value":2},{"source":0,"target":44,"value":2},{"source":0,"target":45,"value":1},{"source":0,"target":46,"value":1},{"source":0,"target":47,"value":1},{"source":0,"target":48,"value":1},{"source":0,"target":49,"value":1},{"source":0,"target":50,"value":1},{"source":0,"target":51,"value":1},{"source":0,"target":52,"value":1},{"source":0,"target":53,"value":1},{"source":0,"target":54,"value":1},{"source":0,"target":55,"value":34},{"source":0,"target":56,"value":13},{"source":0,"target":57,"value":8},{"source":0,"target":58,"value":8},{"source":0,"target":59,"value":5},{"source":0,"target":60,"value":5},{"source":0,"target":61,"value":4},{"source":0,"target":62,"value":4},{"source":0,"target":63,"value":3},{"source":0,"target":64,"value":3},{"source":0,"target":65,"value":3},{"source":0,"target":66,"value":2},{"source":0,"target":67,"value":2},{"source":0,"target":68,"value":2},{"source":0,"target":69,"value":2},{"source":0,"target":70,"value":2},{"source":0,"target":71,"value":2},{"source":0,"target":72,"value":2},{"source":0,"target":73,"value":1},{"source":0,"target":74,"value":1},{"source":0,"target":75,"value":1},{"source":0,"target":76,"value":1},{"source":0,"target":77,"value":1},{"source":0,"target":78,"value":1},{"source":0,"target":79,"value":1},{"source":0,"target":80,"value":1},{"source":0,"target":81,"value":1},{"source":0,"target":82,"value":1},{"source":0,"target":83,"value":1},{"source":0,"target":84,"value":1},{"source":0,"target":85,"value":1},{"source":0,"target":86,"value":1},{"source":0,"target":87,"value":1},{"source":0,"target":88,"value":1},{"source":0,"target":89,"value":1},{"source":0,"target":90,"value":1},{"source":0,"target":91,"value":1},{"source":0,"target":92,"value":1},{"source":0,"target":93,"value":1},{"source":0,"target":94,"value":1},{"source":0,"target":95,"value":11},{"source":0,"target":96,"value":7},{"source":0,"target":97,"value":6},{"source":0,"target":98,"value":3},{"source":0,"target":99,"value":3},{"source":0,"target":100,"value":2},{"source":0,"target":101,"value":1},{"source":0,"target":102,"value":1},{"source":0,"target":103,"value":1},{"source":0,"target":104,"value":1},{"source":0,"target":105,"value":1},{"source":0,"target":106,"value":1},{"source":0,"target":107,"value":1},{"source":0,"target":108,"value":1},{"source":0,"target":109,"value":1},{"source":0,"target":110,"value":1},{"source":0,"target":111,"value":1},{"source":0,"target":112,"value":1},{"source":0,"target":113,"value":1},{"source":0,"target":114,"value":1},{"source":0,"target":115,"value":1},{"source":0,"target":116,"value":1},{"source":0,"target":117,"value":9},{"source":0,"target":118,"value":7},{"source":0,"target":119,"value":1},{"source":0,"target":120,"value":1},{"source":0,"target":121,"value":1},{"source":0,"target":122,"value":1},{"source":0,"target":123,"value":8},{"source":0,"target":124,"value":4},{"source":0,"target":125,"value":2},{"source":0,"target":126,"value":2},{"source":0,"target":127,"value":2},{"source":0,"target":128,"value":1},{"source":0,"target":129,"value":4},{"source":0,"target":130,"value":4},{"source":0,"target":131,"value":3},{"source":0,"target":132,"value":2},{"source":0,"target":133,"value":2},{"source":0,"target":134,"value":1},{"source":0,"target":135,"value":1},{"source":0,"target":136,"value":1},{"source":0,"target":137,"value":8},{"source":0,"target":138,"value":3},{"source":0,"target":139,"value":2},{"source":0,"target":140,"value":1},{"source":0,"target":141,"value":1},{"source":0,"target":142,"value":4},{"source":0,"target":143,"value":3},{"source":0,"target":144,"value":2},{"source":0,"target":145,"value":2},{"source":0,"target":146,"value":2},{"source":0,"target":147,"value":1},{"source":0,"target":148,"value":6},{"source":0,"target":149,"value":3},{"source":0,"target":150,"value":1},{"source":0,"target":151,"value":1},{"source":0,"target":152,"value":1},{"source":0,"target":153,"value":1},{"source":0,"target":154,"value":1},{"source":0,"target":155,"value":6},{"source":0,"target":156,"value":2},{"source":0,"target":157,"value":2},{"source":0,"target":158,"value":1},{"source":0,"target":159,"value":1},{"source":0,"target":160,"value":1},{"source":0,"target":161,"value":2},{"source":0,"target":162,"value":1},{"source":0,"target":163,"value":1},{"source":0,"target":164,"value":1},{"source":0,"target":165,"value":1},{"source":0,"target":166,"value":1},{"source":0,"target":167,"value":1},{"source":0,"target":168,"value":1},{"source":0,"target":169,"value":5},{"source":0,"target":170,"value":2},{"source":0,"target":171,"value":1},{"source":0,"target":172,"value":2},{"source":0,"target":173,"value":2},{"source":0,"target":174,"value":1},{"source":0,"target":175,"value":1},{"source":0,"target":176,"value":1},{"source":0,"target":177,"value":1},{"source":0,"target":178,"value":2},{"source":0,"target":179,"value":2},{"source":0,"target":180,"value":1},{"source":0,"target":181,"value":1},{"source":0,"target":182,"value":1},{"source":0,"target":183,"value":1},{"source":0,"target":184,"value":1},{"source":0,"target":185,"value":1},{"source":0,"target":186,"value":1},{"source":0,"target":187,"value":1},{"source":0,"target":188,"value":1},{"source":0,"target":189,"value":1},{"source":0,"target":190,"value":2},{"source":0,"target":191,"value":1},{"source":0,"target":192,"value":1},{"source":0,"target":194},{"source":194,"target":193},{"source":0,"target":195},{"source":195,"target":193},{"source":0,"target":196},{"source":196,"target":193},{"source":0,"target":197,"value":27},{"source":0,"target":199},{"source":199,"target":198},{"source":0,"target":201},{"source":201,"target":200},{"source":0,"target":194},{"source":194,"target":202},{"source":0,"target":195},{"source":195,"target":202},{"source":0,"target":196},{"source":196,"target":202},{"source":0,"target":204},{"source":204,"target":203},{"source":0,"target":206},{"source":206,"target":205},{"source":0,"target":208},{"source":208,"target":207},{"source":0,"target":201},{"source":201,"target":209},{"source":0,"target":194},{"source":194,"target":210},{"source":0,"target":199},{"source":199,"target":211},{"source":0,"target":213},{"source":213,"target":212},{"source":0,"target":214,"value":2},{"source":0,"target":216},{"source":216,"target":215},{"source":0,"target":218},{"source":218,"target":217},{"source":0,"target":218},{"source":218,"target":219},{"source":0,"target":204},{"source":204,"target":220},{"source":0,"target":194},{"source":194,"target":221},{"source":0,"target":194},{"source":194,"target":222},{"source":0,"target":208},{"source":208,"target":223},{"source":0,"target":225},{"source":225,"target":224},{"source":0,"target":227},{"source":227,"target":226},{"source":0,"target":229},{"source":229,"target":228},{"source":0,"target":230,"value":1},{"source":0,"target":231,"value":1},{"source":0,"target":232,"value":1},{"source":0,"target":233,"value":1},{"source":0,"target":234,"value":1},{"source":0,"target":235,"value":1},{"source":0,"target":236,"value":1},{"source":0,"target":237,"value":1},{"source":0,"target":194},{"source":194,"target":238},{"source":0,"target":194},{"source":194,"target":239},{"source":0,"target":194},{"source":194,"target":240}]};
zoom = d3.zoom()
.scaleExtent([0.1, 10])
.on("zoom", zoomed);
var container = d3.select("#network_group");
var svgForce = container
.append("svg")
.attr("id", "network_svg")
.attr("width", width_network)
.attr("height", height_network);
gMain = svgForce.append("g")
.attr("class","gMain");
var rect = gMain.append('rect')
.attr('width', width_network)
.attr('height', height_network)
.style('fill', 'white');
gForce = gMain.append("g");
gMain.call(zoom)
.on("dblclick.zoom", null);
var simulation = d3.forceSimulation(network.nodes).force("charge", d3.forceManyBody().strength(-200))
.force("collision", d3.forceCollide().radius(CIRCLE_RADIUS*2))
.force("x", d3.forceX(width_network/2).strength(0.015))
.force("y", d3.forceY(height_network/2).strength(0.02))
.force("center", d3.forceCenter(width_network/2, height_network/2))
.force("link", d3.forceLink(network.links).distance(70));
var gBrushHolder = gForce.append('g');
var gBrush = null;
link = gForce.append("g")
.attr("class", "force_links")
.selectAll("line")
.data(network.links)
.enter()
.append("line")
.attr("stroke-width", 2);
// define nodes
node = gForce.append("g")
.attr("class", "force_nodes")
.selectAll("circle")
.data(network.nodes)
.enter()
.append("circle")
.attr("r", CIRCLE_RADIUS)
.on("mouseover", function (d) {
d3.select(this).append("title")
.text(function (d) {
return d.name;
});
})
.on("mouseout", function (d) {
d3.select(this).select("title").remove();
})
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
// define node labels
nodeLabel = gForce.append("g")
.attr("class", "label_nodes")
.selectAll("text")
.data(network.nodes)
.enter()
.append("text")
.text(function (d) {
return d.name;
})
.style("text-anchor", "start");
simulation.on("tick", ticked);
function ticked() {
node.attr("cx", function (d) {
return d.x;
})
.attr("cy", function (d) {
return 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;
});
nodeLabel.attr("x", function (d) {
return d.x;
})
.attr("y", function (d) {
return d.y - 13;
});
}
var brushMode = false;
var brushing = false;
var brush = d3.brush()
.extent([[0,0],[width_network,height_network]])
.on("start", brush_start)
.on("brush", brushed)
.on("end", brush_end);
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
if (d.fixed) {
d.fixed = false;
d.fx = null;
d.fy = null;
d3.select(this)
.style("fill","lightsteelblue")
.style("stroke", "#fff")
.style("stroke-width", "1.5px");
} else {
d3.select(this)
.style("stroke", "lightsteelblue")
.style("fill", "white")
.style("stroke-width", 9);
d.fixed = true;
}
}
function zoomed() {
var transform = d3.event.transform;
gForce.attr("transform", transform);
}
if (network.nodes.length > 80) {
zoom.translateTo(svgForce, (width_network - width_network * 0.4) / 2, (height_network - height_network * 0.4) / 2);
zoom.scaleTo(svgForce, 0.4);
}
function brush_start(){
brushing = true;
node.each(function(d) {
d.previouslySelected = ctrlKey && d.selected;
});
}
rect.on('click', () => {
node.each(function(d) {
d.selected = false;
d.previouslySelected = false;
});
node.classed("selected", false);
});
function brushed() {
if (!d3.event.sourceEvent) return;
if (!d3.event.selection) return;
var extent = d3.event.selection;
node.classed("selected", function(d) {
return d.selected = d.previouslySelected ^
(extent[0][0] <= d.x && d.x < extent[1][0]
&& extent[0][1] <= d.y && d.y < extent[1][1]);
});
}
function brush_end(){
if (!d3.event.sourceEvent) return;
if (!d3.event.selection) return;
if (!gBrush) return;
gBrush.call(brush.move, null);
if (!brushMode) {
// the shift key has been release before we ended our brushing
gBrush.remove();
gBrush = null;
}
brushing = false;
}
d3.select('body').on('keydown', keydown);
d3.select('body').on('keyup', keyup);
var ctrlKey;
function keydown() {
ctrlKey = d3.event.ctrlKey;
if (ctrlKey) {
if (gBrush)
return;
brushMode = true;
if (!gBrush) {
gBrush = gBrushHolder.append("g").attr("class", "brush");
gBrush.call(brush);
}
}
}
function keyup() {
ctrlKey = false;
brushMode = false;
if (!gBrush)
return;
if (!brushing) {
gBrush.remove();
gBrush = null;
}
}
.force_nodes {
fill: lightsteelblue;
}
.force_links {
stroke: gray;
}
.force_nodes .selected {
stroke: red;
}
.label_nodes text {
cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<div id="network_group"></div>
You can find a working version of the code also in this JSFiddle.
The problem with your code right now is that you're appending the brush group to a group that you later translate in the zoom function:
var gBrushHolder = gForce.append('g');
Instead of that, append the brush group to the main group:
var gBrushHolder = gMain.append('g');
Then, use a variable to track the zoom's translate and scale...
//declare it:
let currentZoom;
//update its value in the zoom function:
currentZoom = d3.event.transform;
Finally, use that value to get the SVG position of the nodes:
node.classed("selected", function(d) {
return d.selected = d.previouslySelected ^
(extent[0][0] <= (d.x * currentZoom.k + currentZoom.x) && (d.x * currentZoom.k + currentZoom.x) < extent[1][0] && extent[0][1] <= (d.y * currentZoom.k + currentZoom.y) && (d.y * currentZoom.k + currentZoom.y) < extent[1][1]);
});
Here is the updated JSFiddle: https://jsfiddle.net/uxqf7tp2/

JavaScript Code executing twice outside of functions

I am developing a site locally using D3 JS. When I do anything outside of any function it is happening twice, no matter where it is within the code. Example:
console.log("2x");
Console output is:
2x
2x
However if the code is inside any of the functions it only prints once. I noticed that next to the logs there is two different locations for their origin
Console output
2x site.js:3
2x site.js?v=twO2e-dF40DXz0Jm_X753ZBfaW8vwQs0ht7UrLyed5E:3
Inside a function the logs only originate from the longer string version. This affects any code outside a function, it seems to run twice...I've included the full code for reference if required.
I have found many similarly titled or tagged questions but all of them were due to the logging occurring in a loop or otherwise, I couldn't find any where it happened in the base code.
EDIT: My code does have two console.logs but that results in 4 prints in that case sorry for being unclear on that.
JavaScript
//Azibuda
console.log("2x");
//Get the SVG element
var svg = d3.select("svg");
var width = 960, height = 600;
var color = d3.scaleOrdinal(d3.schemeCategory20);
var link = svg.append("g").selectAll(".link");
var node = svg.append("g").selectAll(".node");
var label = svg.append("g").selectAll(".label");
//Begin the force simulation
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function (d) { return d.id; }).distance(50).strength(0.3))
.force("charge", d3.forceManyBody().strength(-15))
.force("center", d3.forceCenter(width / 2, height / 2));
//Highlight variables
var highlight_color = "blue";
var tHighlight = 0.05;
var config;
var linkedByIndex = {};
var linksAsString = {};
//Get the data
d3.json("/../../data.json", function (data) {
config = data;
if (!localStorage.graph)
{
localStorage.graph = JSON.stringify(data);
}
update();
});
function update() {
console.log(localStorage.graph);
//Create an array of source,target containing all links
config.links.forEach(function (d) {
linkedByIndex[d.source + "," + d.target] = true;
linkedByIndex[d.target + "," + d.source] = true;
//linksAsString[d.index] = d.source + "," + d.target;
});
var nodesAsString = {};
config.nodes.forEach(function (d) {
nodesAsString[d.index] = d.id + "," + d.radius;
});
//Draw links
link = link.data(config.links);
link.exit().remove();
link = link.enter().append("line")
.attr("class", "link")
.attr("stroke-width", 2)
.attr("stroke", "#888")
//.attr("opacity", function (d) { if (d.target.radius > 7) { return 1 }; return 0; })
.merge(link);
node = node.data(config.nodes);
node.exit().remove();
node = node.enter().append("circle")
.attr("class", "node")
.attr("r", function(d) { return d.radius; })
.attr("fill", function (d) { return color(d.id); })
.attr("stroke", "black")
// .attr("pointer-events", function (d) { if (d.radius <= 7) { return "none"; } return "visibleAll"; })
// .attr("opacity", function (d) { if (d.radius <= 7) { return 0; } return 1; })
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
.on("mouseover", mouseOver)
.on("mouseout", mouseOut)
.merge(node);
label = label.data(config.nodes);
label.exit().remove();
label = label.enter().append("text")
.attr("class", "label")
.attr("dx", function (d) { return d.radius * 1.25; })
.attr("dy", ".35em")
.attr("opacity", function (d) { if (d.radius <= 7) { return 0; } return 1; })
.attr("font-weight", "normal")
.style("font-size", 10)
.text(function (d) { return d.id; })
.merge(label);
//Add nodes to simulation
simulation
.nodes(config.nodes)
.on("tick", ticked);
//Add links to simulation
simulation.force("link")
.links(config.links);
simulation.alphaTarget(0.3).restart();
}
//Animating by ticks function
function ticked() {
node
.attr("cx", function (d) { return d.x = Math.max(d.radius, Math.min(width - d.radius, d.x)); })
.attr("cy", function (d) { return d.y = Math.max(d.radius, Math.min(height - d.radius, 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; });
label
.attr("x", function (d) { return d.x = Math.max(d.radius, Math.min(width - d.radius, d.x)); })
.attr("y", function (d) { return d.y = Math.max(d.radius, Math.min(height - d.radius, d.y)); });
}
//Using above array, check if two nodes are linked
function isConnected(node1, node2) {
return linkedByIndex[node1.id + "," + node2.id] || node1.index == node2.index;
}
//Highlight a node
function setHighlight(d) {
svg.style("cursor", "pointer");
//Set highlighted stroke around the current node, text and its links
node.style("stroke", function (tNode) {
return isConnected(d, tNode) ? highlight_color : "black";
});
label.style("font-weight", function (tNode) {
return isConnected(d, tNode) ? "bold" : "normal";
});
link.style("stroke", function (tNode) {
return tNode.source.index == d.index || tNode.target.index == d.index ? highlight_color : "#888";
});
}
//Drag/mousedown on a node
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
//Dragging a node
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
//Highlight/focus on held down node
setFocus(d);
setHighlight(d);
}
//End drag/mouseup on a node
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
//Mouse over on a node
function mouseOver(d) {
setFocus(d);
setHighlight(d);
}
//Mouse off of a node
function mouseOut(d) {
unFocus(d);
highlightOff();
}
//Turning off highlight
function highlightOff() {
svg.style("cursor", "default");
//Set node attributes back to normal
node.style("stroke", "black");
label.style("font-weight", "normal");
link.style("stroke", "#888");
}
//Focus on a node
function setFocus(d) {
//Set opacity of all non-connected nodes and their elements (text/links) to faded
node.style("opacity", function (tNode) {
return isConnected(d, tNode) ? 1 : tHighlight;
});
label.style("opacity", function (tNode) {
return isConnected(d, tNode) ? 1 : tHighlight;
});
link.style("opacity", function (tNode) {
return tNode.source.index == d.index || tNode.target.index == d.index ? 1 : tHighlight;
});
}
//Unfocus on a node (reset all to normal)
function unFocus(d) {
//node.style("opacity", function (d) { if (d.radius <= 7) { return 0; } return 1; });
//node.style("pointer-events", function (d) { if (d.radius <= 7) { return "none"; } return "visibleAll"; })
node.style("opacity", 1);
label.style("opacity", function (d) { if (d.radius <= 7) { return 0; } return 1; });
//link.style("opacity", function (d) { if (d.target.radius > 7) { return 1 }; return 0; });
link.style("opacity", 1);
}
function updateR()
{
console.log(config.nodes[2]);
config.nodes.splice(2, 1);
update();
}
var temp = JSON.parse(localStorage.graph);
//temp.nodes.push({"id":"Cheese", "radius":20});
//localStorage.graph = JSON.stringify(temp);
console.log("2x");
HTML
#{
ViewData["Title"] = "Home Page";
}
<link href="/css/geico-design-kit.css" rel="stylesheet">
<script src="~/js/geico-design-kit.bundle.js"></script>
<script src="~/js/d3.js"></script>
<script src="~/js/site.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
<style>
.link line {
stroke: #888;
}
text {
pointer-events: none;
font: 10px sans-serif;
}
</style>
<div class="col-md-12">
<div class="col-md-3">
<h2>Quick Links</h2>
<ul class="list list--unordered">
<li>Example Quick Links Here</li>
<li>Google</li>
<li>Google</li>
</ul>
</div>
</div>
<button onclick="updateR()" type="button" style="background-color:red">DO NOT PRESS!</button>
<svg id="container" width="960" height="600" style="border:1px solid black;"></svg>
<form id="nodes"></form>
If this ASP.NET view is using a layout view and the same file is referenced in that layout file, the rendered output would have multiple references.

Updating D3 bubble radius with collide simulation

I've been wrestling very hard with D3 to try to make a simple bubble-chart using force collide that live-updates the bubble size.
I can get the chart to show on the first data update with force collide. However subsequent data calls freeze the chart and the sizes are never updated:
https://jsfiddle.net/d2zcfjfa/1/
node = svg.selectAll('g.node')
.data(root.children)
.enter()
.append('g')
.attr('class', 'node')
.append('circle')
.attr('r', function(d) { return d.r * 1.4; })
.attr('fill', function(d) { return color(d.data.name); })
.call(d3.drag()
.on("start", dragStart)
.on("drag", dragged)
.on("end", dragEnd));
var circleUpdate = node.select('circle')
.attr('r', function(d)
{
return d.r;
});
simulation.nodes(root.children);
I can get updates to work but only without using the collide simulation as seen here:
https://jsfiddle.net/rgdox7g7/1/
node = svg.selectAll('g.node')
.data(root.children)
.enter()
.append('g')
.attr('id', function(d) { return d.id; })
.attr('class', 'node')
.attr('transform', function(d)
{
return "translate(" + d.x + "," + d.y + ")";
});
var nodeUpdate = svg.selectAll('g.node')
.transition()
.duration(2000)
.ease(d3.easeLinear);
var circleUpdate = nodeUpdate.select('circle')
.attr('r', function(d)
{
return d.r;
});
node.append("circle")
.attr("r", function(d) { return d.r; })
.style('fill', function(d) { return color(d.data.name); });
Everything I have tried to mix these two solutions together simply does not work. I have scoured the internet for other examples and nothing I can find is helping. Can someone please help me understand what to do? I never thought D3 would be such a frustration!
stackoverflow: the place where you have to answer your own questions.
here is my working solution:
https://jsfiddle.net/zc0fgh6y/
var subscription = null;
var width = 600;
var height = 300;
var maxSpeed = 1000000;
var pack = d3.pack().size([width, height]).padding(0);
var svg = d3.select('svg');
var node = svg.selectAll("g.node");
var root;
var nodes = [];
var first = true;
var scaleFactor = 1.4;
var color = d3.interpolateHcl("#0faac3", "#dd2323");
var forceCollide = d3.forceCollide()
.strength(.8)
.radius(function(d)
{
return d.r;
}).iterations(10);
var simulationStart = d3.forceSimulation()
.force("forceX", d3.forceX(width/2).strength(.04))
.force("forceY", d3.forceY(height/2).strength(.2))
.force('collide', forceCollide)
.on('tick', ticked);
var simulation = d3.forceSimulation()
.force("forceX", d3.forceX(width/2).strength(.0005))
.force("forceY", d3.forceY(height/2).strength(.0025))
.force('collide', forceCollide)
.on('tick', ticked);
function ticked()
{
if (node)
{
node.attr('transform', function(d)
{
return "translate(" + d.x + "," + d.y + ")";
}).select('circle').attr('r', function(d)
{
return d.r;
});
}
}
function rand(min, max)
{
return Math.random() * (max - min) + min;
};
setInterval(function()
{
var hosts = [];
for (var i = 0; i < 100; i++)
{
hosts.push({name: i, cpu: rand(10,100), speed: rand(0,maxSpeed)});
}
root = d3.hierarchy({children: hosts})
.sum(function(d)
{
return d.cpu ? d.cpu : 0;
});
var leaves = pack(root).leaves().map(function(item)
{
return {
id: 'node-'+item.data.name,
name: item.data.name,
r: item.r * scaleFactor,
x: width/2,
y: height/2,
cpu: item.data.cpu,
speed: item.data.speed
};
});
for (var i = 0; i < leaves.length; i++)
{
if (nodes[i] && nodes[i].id == leaves[i].id)
{
var oldR = nodes[i].newR;
nodes[i].oldR = oldR;
nodes[i].newR = leaves[i].r;
nodes[i].cpu = leaves[i].cpu;
nodes[i].speed = leaves[i].speed;
}
else
{
nodes[i] = leaves[i];
//nodes[i].r = 1;
nodes[i].oldR = 1;//nodes[i].r;
nodes[i].newR = leaves[i].r;
}
}
if (first)
{
first = false;
node = node.data(nodes, function(d) { return d.id; });
node = node.enter()
.append('g')
.attr('class', 'node');
node.append("circle")
.style("fill", 'transparent');
node.append("text")
.attr("dy", "0.3em")
.style('fill', 'transparent')
.style("text-anchor", "middle")
.text(function(d)
{
return d.name;//.substring(0, d.r / 4);
});
// transition in size
node.transition()
.ease(d3.easePolyInOut)
.duration(950)
.tween('radius', function(d)
{
var that = d3.select(this);
var i = d3.interpolate(1, d.newR);
return function(t)
{
d.r = i(t);
that.attr('r', function(d)
{
return d.r;
});
simulationStart.nodes(nodes).alpha(1);
}
});
// fade in text color
node.select('text')
.transition()
.ease(d3.easePolyInOut)
.duration(950)
.style('fill', 'white');
// fade in circle size
node.select('circle')
.transition()
.ease(d3.easePolyInOut)
.duration(950)
.style('fill', function(d)
{
return color(d.speed / maxSpeed);
});
}
else
{
// transition to new size
node.transition()
.ease(d3.easeLinear)
.duration(950)
.tween('radius', function(d)
{
var that = d3.select(this);
var i = d3.interpolate(d.oldR, d.newR);
return function(t)
{
d.r = i(t);
that.attr('r', function(d)
{
return d.r;
});
simulation.nodes(nodes).alpha(1);
}
});
// transition to new color
node.select('circle')
.transition()
.ease(d3.easeLinear)
.duration(950)
.style('fill', function(d)
{
return color(d.speed / maxSpeed);
});
}
}, 1000);

Categories