How to append images as nodes in D3 force layout graph? - d3.js

So I'm working on freecodecamp's D3 force layout challenge : https://www.freecodecamp.com/challenges/show-national-contiguity-with-a-force-directed-graph
And as part of the challenge, I'm trying to append images of flags as the nodes in a force layout.
I've managed to append the flags and they are showing. When you click and drag on them, the links also move too. The problem is that they are stuck in the same position.
This is what I mean:
javascript (it's made within React):
createForceGraph() {
const { nodes, links } = this.state;
console.log(nodes);
console.log(links);
const w = 800;
const h = 500;
const margin = {
top: 30,
right: 30,
bottom: 80,
left: 80
};
const svg = d3.select('.chart')
.append('svg')
.attr('width', w)
.attr('height', h);
const simulation = d3.forceSimulation()
.force('link', d3.forceLink().id(function(d, i) { return i }).distance(1))
.force('charge', d3.forceManyBody().strength(1))
.force('center', d3.forceCenter(w / 2, h / 2))
.force('collision', d3.forceCollide(12));
const link = svg.append('g')
.attr('class', 'links')
.selectAll('line')
.data(links)
.enter()
.append('line')
.attr('stroke', 'black');
const node = d3.select('.nodes')
.selectAll('img')
.data(nodes)
.enter()
.append('img')
.attr('class', d => {
return `flag flag-${d.code}`;
})
.call(d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended));
simulation.nodes(nodes)
.on('tick', ticked);
simulation.force('link')
.links(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
.style("left", function(d) { return d.x + 'px'; })
.style("top", function(d) { return d.y + 'px'; });
}
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;
}
}
HTML:
<div>D3 Force-Directed Layout
<div className='chart'>
<div className='nodes'></div>
</div>
</div>

Maybe this can help you solve your problem?
https://bl.ocks.org/mbostock/950642

Related

How to create a custom element in d3

The code is based on Denise's one on bl.ocks. How can I add to the existing code text and an icon to a node? I have almost got it by appending the needed elements but the problem is that circles appear at (0, 0) coordinate.
A picture is worth a thousand words,
and my target is having inside the nodes and where the nodes need to be, a text in the middle and an icon,
This is my current code, that works perfectly as long as the commented part is not uncommented (that's what I have tried to do)
let translateVar = [0,0];
let scaleVar = 1;
let radius = 50;
function create_pan_zoomable_svg(html_element, width, height) {
let svg = d3.select("body")
.append("svg")
.attr("width", "100%")
.attr("height", "100%")
.style("background-color", "#eeeeee")
.call(_zoom).on("dblclick.zoom", null)
.append("g");
d3.select("#zoom_in").on('click', function() { _zoom.scaleBy(svg, 2)});
d3.select("#zoom_out").on('click', function() { _zoom.scaleBy(svg, 0.5)});
create_marker(svg);
initialize_link_node(svg);
return svg;
}
var _zoom = d3.zoom()
.on("zoom", function() {
translateVar[0] = d3.event.transform.x;
translateVar[1] = d3.event.transform.y;
scaleVar = d3.event.transform.k;
svg.attr('transform', 'translate(' + translateVar[0] + ',' + translateVar[1] + ') scale(' + scaleVar + ')');
});
function create_marker(svg) {
let defs = svg.append("defs");
defs.append("marker")
.attr("id", "arrow")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 40)
.attr("refY", 0)
.attr("markerWidth", 8)
.attr("markerHeight", 8)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
}
function getScreenInfo() {
return {
width : ($(window).width()-translateVar[0])/scaleVar,
height : ($(window).height()-translateVar[1])/scaleVar,
centerx : (($(window).width()-translateVar[0])/scaleVar)/2,
centery : (($(window).height()-translateVar[1])/scaleVar)/2
};
}
let link, node, simulation;
function initialize_link_node(svg) {
let currentScreen = getScreenInfo();
simulation = d3.forceSimulation()
.force("link", d3.forceLink()
.id(function(d) { return d.id; }))
.force("charge", d3.forceManyBody()
.strength(function(d) { return -20000;}))
.force("center", d3.forceCenter(currentScreen.centerx, currentScreen.centery));
link = svg.append("g").selectAll(".link");
node = svg.append("g").selectAll(".node");
}
function spawn_nodes(svg, graph, around_node, filtering_options) {
node = node.data(theData.nodes, function(d) {
return d.id;
});
let newNode = node.enter().append("g");
newNode = newNode.append("circle")
.attr("class", "node")
.attr("r", radius)
.attr("fill", function(d) {
return colors[d.type]
})
.attr("id", function(d) {
return d.id
});
/****************************************************
newNode.append("text")
.text( function(d) {
return d.id;
})
.style("fill", "red")
.style("text-anchor", "middle");
newNode.append("svg:image")
.attr("href", function(d) {
return dict_type_icon[d.type];
}).attr("width", "30")
.attr("height", "30");**************************/
newNode.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended)
);
node = node.merge(newNode);
link = link.data(theData.edges, function(d) {
return d.id;
});
let newLink = link.enter().append("line").attr("class", "link").attr("marker-end", "url(#arrow)");
newLink.append("title").text(function(d) {
return d.labeled;
});
link = link.merge(newLink);
node = node.merge(newNode);
simulation.nodes(theData.nodes).on("tick", ticked);
simulation.force("link").links(theData.edges);
simulation.alpha(1).alphaTarget(0).restart();
}
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 ticked() {
let currentScreen = getScreenInfo();
node
.attr("cx", function(d) {
return d.x = Math.max(radius, Math.min(currentScreen.width - radius, d.x));
})
.attr("cy", function(d) { return d.y = Math.max(radius, Math.min(currentScreen.height - 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; });
}
When I uncomment the circles appear at (0,0) point as you may have seen in the image.
I would appreciate if you could help me! Thank you.
Right now, you're turning newNode into a circles' selection:
newNode = newNode.append("circle")
//etc...
So, in order to keep it as a groups' selection, change that line to:
newNode.append("circle")
//etc...
Then, uncomment the commented part.
Finally, since that now node.merge(newNode) will be a groups' selection, change the cx and cy in ticked function to translate:
node.attr("transform", function(d) {
return "translate(" + (d.x = Math.max(radius, Math.min(currentScreen.width - radius, d.x))) +
"," + (d.y = Math.max(radius, Math.min(currentScreen.height - radius, d.y))) + ")";
});

Add different images for nodes in d3 v4 network graph

I know it's there're a few topics that discuss the issue but non of the found results worked for me. I'm trying to modify the classic d3 network graph Les miserables example (d3v4 version HERE: https://bl.ocks.org/mbostock/4062045) to add different images for different nodes - the relative path of the file being given as one of the node attributes, eg.
{"id": "Valjean", "group": 1, img: "images/male.png"},
What I'm trying to achieve is similar to this:https://bl.ocks.org/mbostock/950642 but made in d3v4, and different images for different nodes.
All examples that I found (also this promissing code snippet, which unfortunately doesnt't work for me) point me to similar approach, which looks more or less like this(both in d3 v4 and v3):
node.append("image")
.attr("xlink:href", function(d) { return d.img })
.attr("x", -8)
.attr("y", -8)
.attr("width", 16)
.attr("height", 16);
However, despite a few hours spent I can not make it work. Any ideas?
Here's a quick example which combines your v4 example with an image based on your v3 example:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.links line {
stroke: #999;
stroke-opacity: 0.6;
}
.nodes circle {
stroke: #fff;
stroke-width: 1.5px;
}
</style>
<svg width="960" height="600"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
var color = d3.scaleOrdinal(d3.schemeCategory20);
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) {
return d.id;
}))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
d3.json("https://jsonblob.com/api/9f7e2c48-8ccd-11e7-8b46-ef38640909a4", function(error, graph) {
if (error) throw error;
var 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);
});
var node = svg.append("g")
.attr("class", "nodes")
.selectAll("image")
.data(graph.nodes)
.enter().append("image")
.attr("xlink:href", "https://github.com/favicon.ico")
.attr("x", -8)
.attr("y", -8)
.attr("width", 16)
.attr("height", 16)
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
node.append("title")
.text(function(d) {
return d.id;
});
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation.force("link")
.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("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
}
});
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;
}
</script> 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;
}
</script>
Adding in your images should then be as simple as changing and .attr("xlink:href" to:
.attr("xlink:href", function(d){
return d.img;
)};

d3 v4 force layout with boundary

I'm trying to create a forceSimulation in d3 v4 which does not let the nodes float outside the boundries of the svg in the same way that this example has it for d3 v3 https://bl.ocks.org/mbostock/1129492.
Have tried a few different things in simulation.on("tick", ticked) to no avail. My codePen is below. Any ideas on how to achieve this?
https://codepen.io/mtsvelik/pen/rzxVrE
//Read the data from the mis element
var graph = document.getElementById('json').innerHTML;
graph = JSON.parse(graph);
render(graph);
function render(graph){
// Dimensions of sunburst.
var radius = 6;
var maxValue = d3.max(graph.links, function(d, i, data) {
return d.value;
});
//sub-in max-value from
d3.select("div").html('<form class="force-control" ng-if="formControl">Link threshold 0 <input type="range" id="thersholdSlider" name="points" value="0" min="0" max="'+ maxValue +'">'+ maxValue +'</form>');
document.getElementById("thersholdSlider").onchange = function() {threshold(this.value);};
var svg = d3.select("svg");
var width = svg.attr("width");
var height = svg.attr("height");
console.log(graph);
var graphRec = JSON.parse(JSON.stringify(graph)); //Add this line
//graphRec = graph; //Add this line
console.log(graphRec);
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }))
.force("charge", d3.forceManyBody().strength(Number(-1000 + (1.25*graph.links.length)))) //default force is -30, making weaker to increase size of chart
.force("center", d3.forceCenter(width / 2, height / 2));
var link = svg.append("g")
.attr("class", "links")
.selectAll("line")
.data(graph.links)
.enter().append("line")
.attr("class", "link")
.attr("stroke-width", function(d) { return Math.sqrt(d.value); });
var node = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(graph.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", radius)
.attr("fill", function(d) { return d.color; })
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
node.append("title")
.text(function(d) { return d.id; });
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation.force("link")
.links(graph.links);
console.log(link.data(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; });
}
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 threshold(thresh) {
thresh = Number(thresh);
graph.links.splice(0, graph.links.length);
for (var i = 0; i < graphRec.links.length; i++) {
if (graphRec.links[i].value > thresh) {graph.links.push(graphRec.links[i]);}
}
console.log(graph.links);
/*var threshold_links = graph.links.filter(function(d){ return (d.value > thresh);});
console.log(graph.links);
restart(threshold_links);*/
restart();
}
//Restart the visualisation after any node and link changes
// function restart(threshold_links) {
function restart() {
//DATA JOIN
//link = link.data(threshold_links);
link = link.data(graph.links);
console.log(link);
//EXIT
link.exit().remove();
console.log(link);
// ENTER - https://bl.ocks.org/colbenkharrl/21b3808492b93a21de841bc5ceac4e47
// Create new links as needed.
link = link.enter().append("line")
.attr("class", "link")
.attr("stroke-width", function(d) { return Math.sqrt(d.value); }).merge(link);
console.log(link);
// DATA JOIN
node = node.data(graph.nodes);
/*
// EXIT
node.exit().remove();
// ENTER
node = node.enter().append("circle")
.attr("class", "node")
.attr("r", radius)
.attr("fill", function(d) {return d.color;})
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended)
)
.merge(node);
node.append("title")
.text(function(d) { return d.id; });
*/
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation.force("link")
.links(graph.links);
simulation.alphaTarget(0.3).restart();
}
}
In the tick function restrict the nodes to move out from the boundary:
node
.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)));
})
//now update the links.
working code here
You can also use d3.forceBoundary that allows you to set a boundary with a strength. In your code
import it
<script src="https://unpkg.com/d3-force-boundary#0.0.1/dist/d3-force-boundary.min.js"></script>
then
var simulation = d3.forceSimulation()
.force("boundary", forceBoundary(0,0,width, height))
.force("link", d3.forceLink().id(function(d) { return d.id; }))
.force("charge", d3.forceManyBody().strength(Number(-1000 + (1.25*graph.links.length)))) //default force is -30, making weaker to increase size of chart
.force("center", d3.forceCenter(width / 2, height / 2));
your pen fixed https://codepen.io/duto_guerra/pen/XWXagqm

How to modify a d3 force layout with voronoi polygons to trigger events on grouped elements?

The goal is to combine d3 force simulation, g elements, and voronoi polygons to make trigger events on nodes easier, such as dragging, mouseovers, tooltips and so on with a graph that can be dynamically modified. This follows the d3 Circle Dragging IV example.
In the following code, when adding the clip path attribute to the g element and clippath elements:
Why does dragging not trigger on the cells?
Why do the nodes become obscured and the
paths lose their styles on edges?
How can this be fixed to drag the nodes and trigger events on them like mouseovers?
var data = [
{
"index" : 0,
"vx" : 0,
"vy" : 0,
"x" : 842,
"y" : 106
},
{
"index" : 1,
"vx" : 0,
"vy" : 0,
"x" : 839,
"y" : 56
},
{
"index" : 2,
"vx" : 0,
"vy" : 0,
"x" : 771,
"y" : 72
}
]
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
var simulation = d3.forceSimulation(data)
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2))
.on("tick", ticked);
var nodes = svg.append("g").attr("class", "nodes"),
node = nodes.selectAll("g"),
paths = svg.append("g").attr("class", "paths"),
path = paths.selectAll("path");
var voronoi = d3.voronoi()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.extent([[0, 0], [width, height]]);
var update = function() {
node = nodes.selectAll("g").data(data);
var nodeEnter = node.enter()
.append("g")
.attr("class", "node")
.attr("clip-path", function(d, i) { return "url(#clip-" + i + ")"; });
nodeEnter.append("circle");
nodeEnter.append("text")
.text(function(d, i) { return i; });
node.merge(nodeEnter);
path = paths.selectAll(".path")
.data(data)
.enter().append("clipPath")
.attr("id", function(d, i) { return "clip-" + i; })
.append("path")
.attr("class", "path");
simulation.nodes(data);
simulation.restart();
}();
function ticked() {
var node = nodes.selectAll("g");
var diagram = voronoi(node.data()).polygons();
paths.selectAll("path")
.data(diagram)
.enter()
.append("clipPath")
.attr("id", function(d, i) { return "clip-" + i; })
.append("path")
.attr("class", "path");
paths.selectAll("path")
.attr("d", function(d) { return d == null ? null : "M" + d.join("L") + "Z"; });
node.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
node
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")" });
}
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;
}
svg {
border: 1px solid #888888;
}
circle {
r: 3;
cursor: move;
fill: black;
}
.node {
pointer-events: all;
}
path {
fill: none;
stroke: #999;
pointer-events: all;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.1/d3.js"></script>
<svg width="400" height="400"></svg>
(Separate question, but nesting the paths in the g elements as in the Circle Dragging IV element causes undesired positioning of the paths off to the side of the graph.)
In a related question, using polygons instead of paths and clippaths, I can get the dragging to work, but am trying to use the clippath version as a comparison and not sure what are the differences, other than clippath seems to be preferred by Mike Bostock (d3 creator).
If the goal is:
is to combine d3 force simulation, g elements, and voronoi polygons to
make trigger events on nodes easier, such as dragging, mouseovers,
tooltips and so on with a graph that can be dynamically updated.
I'm going to step back a bit from the specifics of your code and try to get to the goal. I will use two primary sources (one which you reference) in this attempt to get there (and I may be way off base in doing so).
Source one: Mike Bostock's block circle dragging example.
Source two: Mike Bostock's Force-directed Graph example.
I hope that this approach at least helps to get to your goal (I took it partly as I was having difficulty with your snippet). It should be useful as a minimal example and proof of concept.
As with you, I'll use the circle dragging example as the foundation, and then I'll try to incorporate the force-directed example.
The key portions of the force directed graph that need to be imported are defining the simulation:
var simulation = d3.forceSimulation()
Assigning the nodes:
simulation
.nodes(circle)
.on("tick", ticked);
( .nodes(graph.nodes) in original )
Instructing what to do on tick:
force.nodes(circles)
.on('tick',ticked);
The ticked function:
function ticked() {
circle.selectAll('circle')
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
( we don't need the link portion, and we want to update the circles (rather than a variable named node )
And the portions that fall in the drag events.
If we import all that into a snippet (combining drag events, adding a ticked function, and we get:
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
radius = 32;
var simulation = d3.forceSimulation()
.force("charge", d3.forceManyBody())
var circles = d3.range(20).map(function() {
return {
x: Math.round(Math.random() * (width - radius * 2) + radius),
y: Math.round(Math.random() * (height - radius * 2) + radius)
};
});
var color = d3.scaleOrdinal()
.range(d3.schemeCategory20);
var voronoi = d3.voronoi()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.extent([[-1, -1], [width + 1, height + 1]]);
var circle = svg.selectAll("g")
.data(circles)
.enter().append("g")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
var cell = circle.append("path")
.data(voronoi.polygons(circles))
.attr("d", renderCell)
.attr("id", function(d, i) { return "cell-" + i; });
circle.append("clipPath")
.attr("id", function(d, i) { return "clip-" + i; })
.append("use")
.attr("xlink:href", function(d, i) { return "#cell-" + i; });
circle.append("circle")
.attr("clip-path", function(d, i) { return "url(#clip-" + i + ")"; })
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", radius)
.style("fill", function(d, i) { return color(i); });
simulation
.nodes(circles)
.on("tick", ticked);
function ticked() {
circle.selectAll('circle')
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
function dragstarted(d) {
d3.select(this).raise().classed("active", true);
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d3.select(this).select("circle").attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
cell = cell.data(voronoi.polygons(circles)).attr("d", renderCell);
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d, i) {
d3.select(this).classed("active", false);
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
function renderCell(d) {
return d == null ? null : "M" + d.join("L") + "Z";
}
path {
pointer-events: all;
fill: none;
stroke: #666;
stroke-opacity: 0.2;
}
.active circle {
stroke: #000;
stroke-width: 2px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>
<svg width="600" height="400"></svg>
The obvious problem is that the cells don't update unless there is a drag. To solve this we just need to take the line that updates the cells on drag and put it in the ticked function so it updates on tick:
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
radius = 32;
var simulation = d3.forceSimulation()
.force("charge", d3.forceManyBody())
var circles = d3.range(20).map(function() {
return {
x: Math.round(Math.random() * (width - radius * 2) + radius),
y: Math.round(Math.random() * (height - radius * 2) + radius)
};
});
var color = d3.scaleOrdinal()
.range(d3.schemeCategory20);
var voronoi = d3.voronoi()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.extent([[-1, -1], [width + 1, height + 1]]);
var circle = svg.selectAll("g")
.data(circles)
.enter().append("g")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
var cell = circle.append("path")
.data(voronoi.polygons(circles))
.attr("d", renderCell)
.attr("id", function(d, i) { return "cell-" + i; });
circle.append("clipPath")
.attr("id", function(d, i) { return "clip-" + i; })
.append("use")
.attr("xlink:href", function(d, i) { return "#cell-" + i; });
circle.append("circle")
.attr("clip-path", function(d, i) { return "url(#clip-" + i + ")"; })
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", radius)
.style("fill", function(d, i) { return color(i); });
circle.append("text")
.attr("clip-path", function(d, i) { return "url(#clip-" + i + ")"; })
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; })
.attr("dy", '0.35em')
.attr("text-anchor", function(d) { return 'middle'; })
.attr("opacity", 0.6)
.style("font-size", "1.8em")
.style("font-family", "Sans-Serif")
.text(function(d, i) { return i; })
simulation
.nodes(circles)
.on("tick", ticked);
function ticked() {
circle.selectAll('circle')
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
circle.selectAll('text')
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; });
cell = cell.data(voronoi.polygons(circles)).attr("d", renderCell);
}
function dragstarted(d) {
d3.select(this).raise().classed("active", true);
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d3.select(this).select("circle").attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
cell = cell.data(voronoi.polygons(circles)).attr("d", renderCell);
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d, i) {
d3.select(this).classed("active", false);
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
function renderCell(d) {
return d == null ? null : "M" + d.join("L") + "Z";
}
path {
pointer-events: all;
fill: none;
stroke: #666;
stroke-opacity: 0.2;
}
.active circle {
stroke: #000;
stroke-width: 2px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>
<svg width="600" height="400"></svg>
update: updating nodes:
Adding and removing nodes is where it got complicated for me at least. The primary issue was that the code above rearranged the svg groups with d3.selection.raise() on drag events, which could mess up my clip path ordering if using only the data element increment. Likewise with removing items from within the middle of the array, this would cause pairing issues between cells, groups, and circles. This pairing was the primary challenge - along with ensuring any appended nodes were in the proper parent and in the right order.
To solve the pairing issues, I used a new property in the data to use as an identifier, rather than the increment. Secondly, I do a couple specific manipulations of the cells when adding: ensuring they are in the right parent and that the cell appears above the circle in the DOM (using d3.selection.lower()).
Note: I haven't managed a good way to remove a circle and keep the voronoi working with a typical update cycle, so I've just recreated for each removal - and since as far as I know the Voronoi is recalculated every tick, this shouldn't be an issue.
The result is (click to remove/add, click the button to toggle remove/add):
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
radius = 32;
var n = 0;
var circles = d3.range(15).map(function() {
return {
n: n++,
x: Math.round(Math.random() * (width - radius * 2) + radius),
y: Math.round(Math.random() * (height - radius * 2) + radius)
};
});
// control add/remove
var addNew = false;
d3.select('#control').append('input')
.attr('type','button')
.attr('value', addNew ? "Add" : "Remove")
.on('click', function(d) {
addNew = !addNew;
d3.select(this).attr('value', addNew ? "Add" : "Remove")
d3.selectAll('g').on('click', (addNew) ? add : remove);
});
var color = d3.scaleOrdinal()
.range(d3.schemeCategory20);
var voronoi = d3.voronoi()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.extent([[-1, -1], [width + 1, height + 1]]);
var circle = svg.selectAll("g")
.data(circles)
.enter().append("g")
.attr('id',function(d) { return 'g-'+d.n })
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
.on('click', (addNew) ? add : remove);
var cell = circle.append("path")
.data(voronoi.polygons(circles))
.attr("d", renderCell)
.attr("class","cell")
.attr("id", function(d) { return "cell-" + d.data.n; });
circle.append("clipPath")
.attr("id", function(d) { return "clip-" + d.n; })
.append("use")
.attr("xlink:href", function(d) { return "#cell-" + d.n; });
circle.append("circle")
.attr("clip-path", function(d) { return "url(#clip-" + d.n + ")"; })
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", radius)
.style("fill", function(d) { return color(d.n); });
circle.append("text")
.attr("clip-path", function(d, i) { return "url(#clip-" + i + ")"; })
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; })
.attr("dy", '0.35em')
.attr("text-anchor", function(d) { return 'middle'; })
.attr("opacity", 0.6)
.style("font-size", "1.8em")
.style("font-family", "Sans-Serif")
.text(function(d) { return d.n; })
var simulation = d3.forceSimulation()
.nodes(circles)
.force('charge', d3.forceManyBody());
simulation.nodes(circles)
.on('tick',ticked);
function ticked() {
circle.selectAll('circle')
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
circle.selectAll('text')
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; });
cell = cell.data(voronoi.polygons(circles)).attr("d", renderCell);
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d3.select(this).select("circle").attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
cell = cell.data(voronoi.polygons(circles)).attr("d", renderCell);
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 remove () {
d3.select(this).raise();
var id = d3.select(this).attr('id').split('-')[1];
id = +id;
// Get the clicked item:
var index = circles.map(function(d) {
return d.n;
}).indexOf(id);
circles.splice(index,1);
// Update circle data:
var circle = svg.selectAll("g")
.data(circles);
circle.exit().remove();
circle.selectAll("clipPath").exit().remove();
circle.selectAll("circle").exit().remove();
circle.selectAll("text").exit().remove();
//// Update voronoi:
d3.selectAll('.cell').remove();
cell = circle.append("path")
.data(voronoi.polygons(circles))
.attr("d", renderCell)
.attr("class","cell")
.attr("id", function(d) { return "cell-" + d.data.n; });
simulation.nodes(circles)
.on('tick',ticked);
}
function add() {
// Add circle to circles:
var coord = d3.mouse(this);
var newIndex = d3.max(circles, function(d) { return d.n; }) + 1;
circles.push({x: coord[0], y: coord[1], n: newIndex });
// Enter and Append:
circle = svg.selectAll("g").data(circles).enter()
var newCircle = circle.append("g")
.attr('id',function(d) { return 'g-'+d.n })
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
.on('click',add)
cell = circle.selectAll("path")
.data(voronoi.polygons(circles)).enter();
cell.select('#g-'+newIndex).append('path')
.attr("d", renderCell)
.attr("class","cell")
.attr("id", function(d) { return "cell-" + d.data.n; });
newCircle.data(circles).enter();
newCircle.append("clipPath")
.attr("id", function(d) { return "clip-" + d.n; })
.append("use")
.attr("xlink:href", function(d) { return "#cell-" + d.n; });
newCircle.append("circle")
.attr("clip-path", function(d) { return "url(#clip-" + d.n + ")"; })
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", radius)
.style("fill", function(d) { return color(d.n); });
newCircle.append("text")
.attr("clip-path", function(d) { return "url(#clip-" + d.n + ")"; })
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; })
.attr("dy", '0.35em')
.attr("text-anchor", function(d) { return 'middle'; })
.attr("opacity", 0.6)
.style("font-size", "1.8em")
.style("font-family", "Sans-Serif")
.text(function(d) { return d.n; })
cell = d3.selectAll('.cell');
d3.select("#cell-"+newIndex).lower(); // ensure the path is above the circle in svg.
simulation.nodes(circles)
.on('tick',ticked);
}
function renderCell(d) {
return d == null ? null : "M" + d.join("L") + "Z";
}
.cell {
pointer-events: all;
fill: none;
stroke: #666;
stroke-opacity: 0.2;
}
.active circle {
stroke: #000;
stroke-width: 2px;
}
svg {
background: #eeeeee;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<div id="control"> </div>
<svg width="960" height="500"></svg>
In terms of specific parts of your question, I found that the dragging and clip path issues in the first two bullets of your question were a largely problem of pairing clip paths, cells, and circles as well as finding the right manner in which to add new elements to the chart - which I hope I demonstrated above.
I hope this is last snippet is closer to the specific problems you were encountering, and I hope that the code above is clear - but it likely went from the clear and concise Bostockian to some other lower standard.
Block version.
Why does dragging not trigger on the cells?
Because if the cell attribute has fill:none, then it must have pointer-events:all.
Why do the nodes become obscured and the paths lose their styles on
edges?
Because the clip path was targeting the g elements position instead of the circles position.
How can this be fixed to drag the nodes and trigger events on
them like mouseovers?
use path attr pointer-events: all, path { pointer-events: all; }
select the desired child element such as circle, or text, in the drag or tick event for positioning parent.select(child).attr('d' function(d) { ..do stuff.. });
use node id's for references to simplify data array updates or deletions node.data(data, function(d) { return d.id; })
Thanks Andrew Reid for your help.
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
color = d3.scaleOrdinal(d3.schemeCategory10);
var a = {id: "a"},
b = {id: "b"},
c = {id: "c"},
data = [a, b, c],
links = [];
var simulation = d3.forceSimulation(data)
.force("charge", d3.forceManyBody().strength(-10))
.force("link", d3.forceLink(links).distance(200))
.force("center", d3.forceCenter(width / 2, height / 2))
.alphaTarget(1)
.on("tick", ticked);
var link = svg.append("g").attr("class", "links").selectAll(".link"),
node = svg.append("g").attr("class", "nodes").selectAll(".node");
var voronoi = d3.voronoi()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.extent([[-1, 1], [width + 1, height + 1]]);
update();
d3.timeout(function() {
links.push({source: a, target: b}); // Add a-b.
links.push({source: b, target: c}); // Add b-c.
links.push({source: c, target: a}); // Add c-a.
update();
}, 1000);
d3.interval(function() {
data.pop(); // Remove c.
links.pop(); // Remove c-a.
links.pop(); // Remove b-c.
update();
}, 5000, d3.now());
d3.interval(function() {
data.push(c); // Re-add c.
links.push({source: b, target: c}); // Re-add b-c.
links.push({source: c, target: a}); // Re-add c-a.
update();
}, 5000, d3.now() + 1000);
function update() {
node = node.data(data, function(d) { return d.id; });
node.exit().remove();
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.on("mouseover", mouseover)
.on("mouseout", mouseout);
nodeEnter.append("circle").attr("fill", function(d) { return color(d.id); }).attr("r", 8);
nodeEnter.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) { return d.id; });
nodeEnter.append("path").attr("class", "path");
nodeEnter.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
node = node.merge(nodeEnter);
// Apply the general update pattern to the links.
link = link.data(links, function(d) { return d.source.id + "-" + d.target.id; });
link.exit().remove();
link = link.enter().append("line").merge(link);
// Update and restart the simulation.
simulation.nodes(data);
simulation.force("link").links(links);
simulation.alpha(1).restart();
}
function mouseover(d) {
d3.select(this).raise().classed("active", true);
}
function mouseout(d) {
d3.select(this).raise().classed("active", false);
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d3.select(this).select("circle").attr("cx", d.fx = d3.event.x).attr("cy", d.fy = d3.event.y);
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
function ticked() {
node.select("circle")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
node.select("path")
.data(voronoi.polygons(data))
.attr("d", function(d) { return d == null ? null : "M" + d.join("L") + "Z"; });
node.select("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; });
}
path {
pointer-events: all;
fill: none;
stroke: #666;
stroke-opacity: 0.2;
}
.active path {
fill: #111;
opacity: 0.05;
}
.active text {
visibility: visible;
}
.active circle {
stroke: #000;
stroke-width: 1.5px;
}
svg {
border: 1px solid #888;
}
.links {
stroke: #000;
stroke-width: 1.5;
}
.nodes {
stroke-width: 1.5;
}
text {
pointer-events: none;
font: 1.8em sans-serif;
visibility: hidden;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>
<svg width="400" height="400"></svg>

d3 force layout node rotates even after release of mouse click in IE11 windows 10

When user drag node by mouse click and release mouse click, node is rotating continuously on mouse event as shown in
JS Fiddle Example
This issue is reproduce on IE11 in windows 10.
What is the solution to this issue?
var width = 500;
var height = 500;
var nodeWidth = 40;
var nodeHeight = 40;
var circleRadius = 5;
var diagramLayout;
var graphData = {"nodes":[{"uid":"Term20","name":"Term20","image":"images/Term.png"},{"uid":"glossforArrow","name":"glossforArrow","image":"images/Glossary.png"},{"uid":"Term43","name":"Term43","image":"images/Term.png"},{"uid":"Term1","name":"Term43","image":"images/Term.png"},{"uid":"Term2","name":"Term43","image":"images/Term.png"}],"links":[{"source":"glossforArrow","target":"Term20","direction":"output","label":"Owned Terms"},{"source":"glossforArrow","target":"Term43","direction":"output","label":"Owned Terms"},{"source":"glossforArrow","target":"Term1","direction":"output","label":"Owned Terms"},{"source":"glossforArrow","target":"Term2","direction":"output","label":"Owned Terms"}]};
forceInitialize(graphData)
function forceInitialize(graphData){
diagramLayout = d3.select("#diagramLayout")
.attr("id", "diagramLayout")//set id
.attr("width", width)//set width
.attr("height", height)//set height
.append("g")
.attr("transform", "translate(" + 20 + "," + 20 + ")")
markerRefx = 35;
simulation = d3.forceSimulation();
alphaMulti = 1;
simulation.force("link", d3.forceLink().id(function(d) { return d.uid; }).distance(70).strength(0))
.force("charge", d3.forceManyBody().distanceMin(20).distanceMax(50))
.force("centre", d3.forceCenter(width/2 , height/2))
.force("x", d3.forceX(2))
.force("y", d3.forceY(10))
.force("collide", d3.forceCollide().radius(function(d) { return 80; }).iterations(2))
simulation.on('end', function() {
simulation.force("link", d3.forceLink().id(function(d) { return d.uid; }).distance(30).strength(0.0).iterations(10))
.force("x", d3.forceX().strength(0))
.force("y", d3.forceX().strength(0))
});
force(graphData);
}
//Force Layout
function force(graphData) {
var linkEnter = diagramLayout.selectAll(".links");
linkEnter = linkEnter.data(graphData.links)
.enter().append("g")
.attr("class", "links")
var link = linkEnter.append("line")
.attr("stroke-width", function(d) { return Math.sqrt(2); })
.attr("stroke-opacity", "0.3")
.attr("stroke", "#000" )
var node = diagramLayout.selectAll(".node");
node = node.data(graphData.nodes, function(d) { return d.uid; });
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.attr("height", nodeHeight)
.attr("width", nodeWidth)
.on('contextmenu', menuCall);
var nodeIcon = nodeEnter.append("rect")
.attr("class", "rect")
.attr("x", -20)
.attr("y", -20)
.attr("rx", 10)
.attr("width", 40)
.attr("height", 40)
.attr("stroke-width", function(d) { return Math.sqrt(2); })
.attr("stroke-opacity", "0.3")
.attr("stroke", "#000" )
.attr("fill", "blue" )
nodeIcon.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
simulation
.nodes(graphData.nodes)
.on("tick", ticked);
setTimeout(function tick(){
simulation.tick();
if(simulation.alpha() >= .005);
setTimeout(tick, 0);
}, 0);
simulation.force("link")
.links(graphData.links);
simulation.restart();
function ticked(e) {
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; });
nodeEnter.attr("transform", function(d) {d.fixed = true; return "translate(" + d.x + "," + d.y + ")"; });
}
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) {
d3.select(this).classed("fixed", d.fixed = false);
d3.selectAll(".node").fixed = true;
}
function menuCall(di,i) {
}
}

Resources