For d3 force layouts that include drag functionality with d3-drag, it seems that the functions called on each drag event modify d.fx/d.fy, eg:
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
The drag start event often bases d.fx/d.fy on d.x/d.y while the end event sets d.fx/d.fy to null.
Where does d.fx/d.fy come from and why does it get used on elements that are being dragged? Is this built into d3 or d3-force in some way? Where is it assigned to the element being dragged?
d3 force layout and node.fx/fy
Within a d3 force simulation, a node's fx/fy properties can be used to set a fixed position for that node. If the fx/fy values are undefined or null, the nodes is free to move around. If they are set, the x/y properties of the node will always be set to match the fx/fy properties:
At the end of each tick, after the application of any forces, a node
with a defined node.fx has node.x reset to this value and node.vx set
to zero; likewise, a node with a defined node.fy has node.y reset to
this value and node.vy set to zero. To unfix a node that was
previously fixed, set node.fx and node.fy to null, or delete these
properties. (docs)
These fx/fy properties are used to fix nodes in general, not just during drag events.
Application to drag events in a d3 force layout:
In a d3 force simulation the position of each node is updated on every tick. The tick fires repeatedly throughout the simulation to keep the nodes position updated, it does so fast enough to appear to animate the nodes movement.
While dragging you want to keep the node's position where the mouse is. During a drag, each time the mouse is moved, the drag event fires. It doesn't fire continuously unless the mouse moves.
When dragging we don't want to apply a force to the node being dragged: we want the node to follow the mouse (we generally also don't want to freeze the rest of the nodes by stopping the simulation during drags).
In order to remove the effects of the force layout on the dragged node, we can set the node.fx/fy properties so that the force doesn't pull the nodes away from the mouse position. When the drag is complete, we want to unset (using null) those values so the force will position the node again.
In the snippet below two force layouts are presented. Each will behave differently:
In the red layout nodes have there fx/fy properties set to the mouse position during the drag.
In the blue layout nodes simply have their x/y properties set to the mouse position during the drag.
In the red layout the force won't re-position a node during a drag. In the blue layout the force will continue to act upon a node during a drag. In the blue example both drag and force continuously place the node based on their individual rules, though normally tick events will generally place the node frequently enough that a drag may not be very visible. Try dragging the blue node a bit then don't move the mouse - it'll drift according to the force layout only:
In both examples the drag functions still update the force layout regarding the position of the dragged node
var data1 ={ "nodes": [{"id": "A"},{"id": "B"},{"id": "C"},{"id":"D"}], "links": [{"source": "A", "target": "B"}, {"source": "B", "target": "C"}, {"source": "C", "target": "A"}, {"source": "D", "target": "A"}] }
var data2 ={ "nodes": [{"id": "A"},{"id": "B"},{"id": "C"},{"id":"D"}], "links": [{"source": "A", "target": "B"}, {"source": "B", "target": "C"}, {"source": "C", "target": "A"}, {"source": "D", "target": "A"}] }
var height = 250; var width = 400;
var svg = d3.select("body").append("svg")
.attr("width",width)
.attr("height",height);
// FIRST SIMULATION
var simulation1 = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }).distance(50))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 3, height / 2));
var link1 = svg.append("g")
.selectAll("line")
.data(data1.links)
.enter().append("line")
.attr("stroke","black");
var node1 = svg.append("g")
.selectAll("circle")
.data(data1.nodes)
.enter().append("circle")
.attr("r", 10)
.call(d3.drag()
.on("drag", dragged1)
.on("end", dragended1))
.attr("fill","crimson");
simulation1.nodes(data1.nodes)
.on("tick", ticked1)
.alphaDecay(0)
.force("link")
.links(data1.links);
function ticked1() {
link1
.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; });
node1
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
function dragged1(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended1(d) {
d.fx = null;
d.fy = null;
}
// SECOND SIMULATION
var simulation2 = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }).distance(50))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width * 2 / 3, height / 2));
var link2 = svg.append("g")
.selectAll("line")
.data(data2.links)
.enter().append("line")
.attr("stroke","black");
var node2 = svg.append("g")
.selectAll("circle")
.data(data2.nodes)
.enter().append("circle")
.attr("r", 10)
.call(d3.drag()
.on("drag", dragged2))
.attr("fill","steelblue");
simulation2.nodes(data2.nodes)
.on("tick", ticked2)
.alphaDecay(0)
.force("link")
.links(data2.links);
function ticked2() {
link2
.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; });
node2
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
function dragged2(d) {
d.x = d3.event.x;
d.y = d3.event.y;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
The d in the drag functions being an individual node in the nodes data array (the node being dragged), from which the force layout bases its calculations and where it updates positions
Also, some drag started events may use d.fx = d.x, this will simply set the node's position to its current position (as I do above), you could also use the mouse's current position without any noticeable difference.
Related
I am trying to create bubble chart.(I'm new to D3.js).
First, I tried to change the code by referring to this site(https://www.d3-graph-gallery.com/graph/circularpacking_drag.html) to make a bubble chart.
the following code is original one.
// set the dimensions and margins of the graph
var width = 450
var height = 450
// append the svg object to the body of the page
var svg = d3.select("#my_dataviz")
.append("svg")
.attr("width", 450)
.attr("height", 450)
// create dummy data -> just one element per circle
var data = [{
"name": "A"
}, {
"name": "B"
}, {
"name": "C"
}, {
"name": "D"
}, {
"name": "E"
}, {
"name": "F"
}, {
"name": "G"
}, {
"name": "H"
}]
// Initialize the circle: all located at the center of the svg area
var node = svg.append("g")
.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("r", 25)
.attr("cx", width / 2)
.attr("cy", height / 2)
.style("fill", "#19d3a2")
.style("fill-opacity", 0.3)
.attr("stroke", "#b3a2c8")
.style("stroke-width", 4)
.call(d3.drag() // call specific function when circle is dragged
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
// Features of the forces applied to the nodes:
var simulation = d3.forceSimulation()
.force("center", d3.forceCenter().x(width / 2).y(height / 2)) // Attraction to the center of the svg area
.force("charge", d3.forceManyBody().strength(1)) // Nodes are attracted one each other of value is > 0
.force("collide", d3.forceCollide().strength(.1).radius(30).iterations(1)) // Force that avoids circle overlapping
// Apply these forces to the nodes and update their positions.
// Once the force algorithm is happy with positions ('alpha' value is low enough), simulations will stop.
simulation
.nodes(data)
.on("tick", function(d) {
node
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
});
// What happens when a circle is dragged?
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(.03).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(.03);
d.fx = null;
d.fy = null;
}
<div id="my_dataviz"></div>
<script src="https://d3js.org/d3.v5.js"></script>
And in this chart, since there was no attraction function(attract to center),
I tried to add the following code. (I referred to the following site https://blockbuilder.org/ericsoco/d2d49d95d2f75552ac64f0125440b35e)
.force('attract', d3.forceAttract()
.target([width/2, height/2])
.strength(0.01))
However, it is not working.And it changes like the following image.
Could anyone advice me why this happen?
The image you get from d3.forceAttract not existing in d3 v5, as you can see from the console. You can use something like d3.forceRadial, however to add an attraction towards the center:
// set the dimensions and margins of the graph
var width = 450
var height = 450
// append the svg object to the body of the page
var svg = d3.select("#my_dataviz")
.append("svg")
.attr("width", 450)
.attr("height", 450)
// create dummy data -> just one element per circle
var data = [{
"name": "A"
}, {
"name": "B"
}, {
"name": "C"
}, {
"name": "D"
}, {
"name": "E"
}, {
"name": "F"
}, {
"name": "G"
}, {
"name": "H"
}]
// Initialize the circle: all located at the center of the svg area
var node = svg.append("g")
.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("r", 25)
.attr("cx", width / 2)
.attr("cy", height / 2)
.style("fill", "#19d3a2")
.style("fill-opacity", 0.3)
.attr("stroke", "#b3a2c8")
.style("stroke-width", 4)
.call(d3.drag() // call specific function when circle is dragged
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
// Features of the forces applied to the nodes:
var simulation = d3.forceSimulation()
.force("center", d3.forceCenter().x(width / 2).y(height / 2)) // Attraction to the center of the svg area
.force("charge", d3.forceManyBody().strength(1)) // Nodes are attracted one each other of value is > 0
.force("collide", d3.forceCollide().strength(.1).radius(30).iterations(1)) // Force that avoids circle overlapping
.force('attract', d3.forceRadial(0, width / 2, height / 2).strength(0.05))
// Apply these forces to the nodes and update their positions.
// Once the force algorithm is happy with positions ('alpha' value is low enough), simulations will stop.
simulation
.nodes(data)
.on("tick", function(d) {
node
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
});
// What happens when a circle is dragged?
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(.03).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(.03);
d.fx = null;
d.fy = null;
}
<div id="my_dataviz"></div>
<script src="https://d3js.org/d3.v5.js"></script>
I would like to draw a simple graph with d3.js like this one
I failed to find out the graph layout in both in v5 API documentation and on the Internet.
All examples use d3.force layout from d3-force. Although it looks impressive, I am not looking for a graph layout with physicall simulation.
Is there a simpler layout or is the best way to use d3.force layout with switched off physics ?
There isn't any need to use the d3.forceSimulation, that ultimately is just one of the many D3 layouts that are on offer. If you look at Mike Bostocks example Force Directed Graph then the modifications you need are as follows:
Define a set of nodes - basically an array of things to represent the circles
Append the svg:circle elements to the DOM using .data(nodes)
Defined a set of links
Append the svg:line elements to the DOM using .data(links)
This is essentially using D3 for it's pure DOM data binding, and not using a pre-built layout.
You can copy any of the force example really - all it's doing underneath is setting an x and a y position underneath. If you've already got those, just position your circles at those points.
All the parts to do with simulation and the ticked event you can remove from the example.
Here is an example providing a static layout:
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
var color = d3.scaleOrdinal(d3.schemeCategory20);
const layout = (graph) => {
return new Promise((resolve) => {
var simulation = d3
.forceSimulation()
.alphaMin(0.3)
.force("link", d3.forceLink().id(function(d) {
return d.id;
}))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2))
.on("end", resolve(graph));
simulation.nodes(graph.nodes);
simulation.force("link").links(graph.links);
});
};
const render = (graph) => {
var link = d3.select("#target").append("g")
.attr("class", "links")
.selectAll("line")
.data(graph.links)
.enter().append("line")
.attr("stroke-width", function(d) { return Math.sqrt(d.value); })
.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; });
var node = d3.select("#target").append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(graph.nodes)
.enter().append("circle")
.attr("r", 5)
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("fill", function(d) { return color(d.group); });
}
d3.json("https://gist.githubusercontent.com/mbostock/4062045/raw/5916d145c8c048a6e3086915a6be464467391c62/miserables.json", function(error, graph) {
if (error) throw error;
layout(graph)
.then((graph) => render(graph));
});
.links line {
stroke: #999;
stroke-opacity: 0.6;
}
.nodes circle {
stroke: #fff;
stroke-width: 1.5px;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="960" height="600">
<g id="target" transform="translate(480, 300)">
</g>
</svg>
EDIT
To change the distance between the nodes you need to change some of the forces in play. The docs are really good - specifically what you want to change is https://github.com/d3/d3-force#many-body
So modify the charge force to read:
.force("charge", d3.forceManyBody().strength(-charge))
You'll need to define the charge, and tweak it a little until the force gives you a good layout.
I've been trying to implement a D3 force directed graph function to give a visual representation to my data, So far I've been successful in getting the nodes to display on the screen and have the individual nodes draggable, however I have been unsuccessful in getting the whole network to pan and zoom.
I've looked at countless examples online, but haven't really been able to figure out what I'm doing wrong.
Could someone point me in the right direction please, using d3 version 4
function selectableForceDirectedGraph(){
var width = d3.select('svg').attr('width');
var height = d3.select('svg').attr('height');
var color = d3.scaleOrdinal(d3.schemeCategory20);
var svg = d3.select("svg")
.attr('width',width)
.attr('height',height);
var container = svg.append("g")
.on("zoom",zoomed)
.on("start",dragstarted)
.on("drag",dragged)
.on("end",dragended);
var json_nodes = _dict['one']['nodes'];
var json_links = _dict['one']['links'];
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));
var link = svg.append("g")
.attr("class", "links")
.selectAll("line")
.data(json_links)
.enter().append("line")
.attr("stroke-width", function(d) { return Math.sqrt(d.value); })
.style("marker-end","url(#suit)");
var node = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(json_nodes)
.enter().append("circle")
.attr("r", 5)
.attr("fill", function(d) { return color(d.group); })
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
node.append("title")
.text(function(d) { return d.id; });
simulation
.nodes(json_nodes)
.on("tick", ticked);
simulation.force("link")
.links(json_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;
}
//Zoom functions
function zoomed(){
container.attr("transform","translate(" + d3.event.translate + ")scale(" +d3.event.scale + ")");
}
}
The HTML doc that this function is called from has an SVG instantiated (hence why the SVG is selected, not appended like in most other examples)
The two variables "json_nodes" and "json_links" take formatted nodes and links (like what you'd see in a JSON file) from a text file and passes them as the data (wanted to do this offline so when I go overseas). The format for the data is like so:
nodes:[{"id": "Name", "group": integer},...],
links:[{"source": "Name", "target": "Name", "value": integer},...]
I apologise if this is a repeated question, I haven't been able to find any really intuitive help with this.
After some intense staring at the screen for a couple hours, I realised a number of little housekeeping tips to make mine, and hopefully to any other programmers new to D3, applications easier to understand and less prone to errors.
As it turns out from my last example, I was trying to perform zoom functions when the zoom variable containing the D3 .zoom() method hadn't been added to the links variable (as I have pointed out below). Once I had done this, everything worked perfectly.
I've also added some comments as to make the improve the readability of the code from the original question, these changes make it easier to understand and much easier to build upon (much like the inheritance with python classes that I'm familiar with).
So hopefully my little moment of frustration is useful to someone in the future, until the next question, happy debugging :)
Mr Incompetent.
function selectableForceDirectedGraph(){
var width = d3.select('svg').attr('width');
var height = d3.select('svg').attr('height');
var color = d3.scaleOrdinal(d3.schemeCategory20);
//As the height and width have already been set, no need to reset them.
var svg = d3.select("svg");
//This is the container group for the zoom
var container = svg.append("g")
.attr("class","everything");
//see the above question for explanation for purpose of these variable.
var json_nodes = _dict['one']['nodes'];
var json_links = _dict['one']['links'];
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));
//drawing lines for the links
var link = container.append("g")
.attr("class", "links")
.selectAll("line")
.data(json_links)
.enter().append("line")
.attr("stroke-width", function(d) { return Math.sqrt(d.value); })
.style("marker-end","url(#suit)");
//draw the circles for the nodes
var node = container.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(json_nodes)
.enter().append("circle")
.attr("r", 5)
.attr("fill", function(d) { return color(d.group); });
//HOUSE KEEPING NOTE: add handlers for drag and zoom as to prevent DRY
var drag_controls = d3.drag()
.on("start",dragstarted)
.on("drag",dragged)
.on("end",dragended);
drag_controls(node); //adding the drag event handlers to the nodes
var zoom_controls = d3.zoom()
.on("zoom",zoomed);
zoom_controls(svg); //adding the zoom event handler to the svg container
node.append("title")
.text(function(d) { return d.id; });
simulation
.nodes(json_nodes)
.on("tick", ticked);
simulation.force("link")
.links(json_links);
function ticked() {
link //updates the link positions
.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 //update the node positions
.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;
}
//Zoom functions
function zoomed(){
container.attr("transform",d3.event.transform)
}
}
How to apply force repulsion on map's labels so they find their right places automatically ?
Bostock' "Let's Make a Map"
Mike Bostock's Let's Make a Map (screenshot below). By default, labels are put at the point's coordinates and polygons/multipolygons's path.centroid(d) + a simple left or right align, so they frequently enter in conflict.
Handmade label placements
One improvement I met requires to add an human made IF fixes, and to add as many as needed, such :
.attr("dy", function(d){ if(d.properties.name==="Berlin") {return ".9em"} })
The whole become increasingly dirty as the number of labels to reajust increase :
//places's labels: point objects
svg.selectAll(".place-label")
.data(topojson.object(de, de.objects.places).geometries)
.enter().append("text")
.attr("class", "place-label")
.attr("transform", function(d) { return "translate(" + projection(d.coordinates) + ")"; })
.attr("dy", ".35em")
.text(function(d) { if (d.properties.name!=="Berlin"&&d.properties.name!=="Bremen"){return d.properties.name;} })
.attr("x", function(d) { return d.coordinates[0] > -1 ? 6 : -6; })
.style("text-anchor", function(d) { return d.coordinates[0] > -1 ? "start" : "end"; });
//districts's labels: polygons objects.
svg.selectAll(".subunit-label")
.data(topojson.object(de, de.objects.subunits).geometries)
.enter().append("text")
.attr("class", function(d) { return "subunit-label " + d.properties.name; })
.attr("transform", function(d) { return "translate(" + path.centroid(d) + ")"; })
.attr("dy", function(d){
//handmade IF
if( d.properties.name==="Sachsen"||d.properties.name==="Thüringen"|| d.properties.name==="Sachsen-Anhalt"||d.properties.name==="Rheinland-Pfalz")
{return ".9em"}
else if(d.properties.name==="Brandenburg"||d.properties.name==="Hamburg")
{return "1.5em"}
else if(d.properties.name==="Berlin"||d.properties.name==="Bremen")
{return "-1em"}else{return ".35em"}}
)
.text(function(d) { return d.properties.name; });
Need for better solution
That's just not manageable for larger maps and sets of labels. How to add force repulsions to these both classes: .place-label and .subunit-label?
This issue is quite a brain storming as I haven't deadline on this, but I'am quite curious about it. I was thinking about this question as a basic D3js implementation of Migurski/Dymo.py. Dymo.py's README.md documentation set a large set of objectives, from which to select the core needs and functions (20% of the work, 80% of the result).
Initial placement: Bostock give a good start with left/right positionning relative to the geopoint.
Inter-labels repulsion: different approach are possible, Lars & Navarrc proposed one each,
Labels annihilation: A label annihilation function when one label's overall repulsion is too intense, since squeezed between other labels, with the priority of annihilation being either random or based on a population data value, which we can get via NaturalEarth's .shp file.
[Luxury] Label-to-dots repulsion: with fixed dots and mobile labels. But this is rather a luxury.
I ignore if label repulsion will work across layers and classes of labels. But getting countries labels and cities labels not overlapping may be a luxury as well.
In my opinion, the force layout is unsuitable for the purpose of placing labels on a map. The reason is simple -- labels should be as close as possible to the places they label, but the force layout has nothing to enforce this. Indeed, as far as the simulation is concerned, there is no harm in mixing up labels, which is clearly not desirable for a map.
There could be something implemented on top of the force layout that has the places themselves as fixed nodes and attractive forces between the place and its label, while the forces between labels would be repulsive. This would likely require a modified force layout implementation (or several force layouts at the same time), so I'm not going to go down that route.
My solution relies simply on collision detection: for each pair of labels, check if they overlap. If this is the case, move them out of the way, where the direction and magnitude of the movement is derived from the overlap. This way, only labels that actually overlap are moved at all, and labels only move a little bit. This process is iterated until no movement occurs.
The code is somewhat convoluted because checking for overlap is quite messy. I won't post the entire code here, it can be found in this demo (note that I've made the labels much larger to exaggerate the effect). The key bits look like this:
function arrangeLabels() {
var move = 1;
while(move > 0) {
move = 0;
svg.selectAll(".place-label")
.each(function() {
var that = this,
a = this.getBoundingClientRect();
svg.selectAll(".place-label")
.each(function() {
if(this != that) {
var b = this.getBoundingClientRect();
if(overlap) {
// determine amount of movement, move labels
}
}
});
});
}
}
The whole thing is far from perfect -- note that some labels are quite far away from the place they label, but the method is universal and should at least avoid overlap of labels.
One option is to use the force layout with multiple foci. Each foci must be located in the feature's centroid, set up the label to be attracted only by the corresponding foci. This way, each label will tend to be near of the feature's centroid, but the repulsion with other labels may avoid the overlapping issue.
For comparison:
M. Bostock's "Lets Make a Map" tutorial (resulting map),
my gist for an Automatic Labels Placement version (resulting map) implementing the foci's strategy.
The relevant code:
// Place and label location
var foci = [],
labels = [];
// Store the projected coordinates of the places for the foci and the labels
places.features.forEach(function(d, i) {
var c = projection(d.geometry.coordinates);
foci.push({x: c[0], y: c[1]});
labels.push({x: c[0], y: c[1], label: d.properties.name})
});
// Create the force layout with a slightly weak charge
var force = d3.layout.force()
.nodes(labels)
.charge(-20)
.gravity(0)
.size([width, height]);
// Append the place labels, setting their initial positions to
// the feature's centroid
var placeLabels = svg.selectAll('.place-label')
.data(labels)
.enter()
.append('text')
.attr('class', 'place-label')
.attr('x', function(d) { return d.x; })
.attr('y', function(d) { return d.y; })
.attr('text-anchor', 'middle')
.text(function(d) { return d.label; });
force.on("tick", function(e) {
var k = .1 * e.alpha;
labels.forEach(function(o, j) {
// The change in the position is proportional to the distance
// between the label and the corresponding place (foci)
o.y += (foci[j].y - o.y) * k;
o.x += (foci[j].x - o.x) * k;
});
// Update the position of the text element
svg.selectAll("text.place-label")
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; });
});
force.start();
While ShareMap-dymo.js may work, it does not appear to be very well documented. I have found a library that works for the more general case, is well documented and also uses simulated annealing: D3-Labeler
I've put together a usage sample with this jsfiddle.The D3-Labeler sample page uses 1,000 iterations. I have found this is rather unnecessary and that 50 iterations seems to work quite well - this is very fast even for a few hundred data points. I believe there is room for improvement both in the way this library integrates with D3 and in terms of efficiency, but I wouldn't have been able to get this far on my own. I'll update this thread should I find the time to submit a PR.
Here is the relevant code (see the D3-Labeler link for further documentation):
var label_array = [];
var anchor_array = [];
//Create circles
svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("id", function(d){
var text = getRandomStr();
var id = "point-" + text;
var point = { x: xScale(d[0]), y: yScale(d[1]) }
var onFocus = function(){
d3.select("#" + id)
.attr("stroke", "blue")
.attr("stroke-width", "2");
};
var onFocusLost = function(){
d3.select("#" + id)
.attr("stroke", "none")
.attr("stroke-width", "0");
};
label_array.push({x: point.x, y: point.y, name: text, width: 0.0, height: 0.0, onFocus: onFocus, onFocusLost: onFocusLost});
anchor_array.push({x: point.x, y: point.y, r: rScale(d[1])});
return id;
})
.attr("fill", "green")
.attr("cx", function(d) {
return xScale(d[0]);
})
.attr("cy", function(d) {
return yScale(d[1]);
})
.attr("r", function(d) {
return rScale(d[1]);
});
//Create labels
var labels = svg.selectAll("text")
.data(label_array)
.enter()
.append("text")
.attr("class", "label")
.text(function(d) {
return d.name;
})
.attr("x", function(d) {
return d.x;
})
.attr("y", function(d) {
return d.y;
})
.attr("font-family", "sans-serif")
.attr("font-size", "11px")
.attr("fill", "black")
.on("mouseover", function(d){
d3.select(this).attr("fill","blue");
d.onFocus();
})
.on("mouseout", function(d){
d3.select(this).attr("fill","black");
d.onFocusLost();
});
var links = svg.selectAll(".link")
.data(label_array)
.enter()
.append("line")
.attr("class", "link")
.attr("x1", function(d) { return (d.x); })
.attr("y1", function(d) { return (d.y); })
.attr("x2", function(d) { return (d.x); })
.attr("y2", function(d) { return (d.y); })
.attr("stroke-width", 0.6)
.attr("stroke", "gray");
var index = 0;
labels.each(function() {
label_array[index].width = this.getBBox().width;
label_array[index].height = this.getBBox().height;
index += 1;
});
d3.labeler()
.label(label_array)
.anchor(anchor_array)
.width(w)
.height(h)
.start(50);
labels
.transition()
.duration(800)
.attr("x", function(d) { return (d.x); })
.attr("y", function(d) { return (d.y); });
links
.transition()
.duration(800)
.attr("x2",function(d) { return (d.x); })
.attr("y2",function(d) { return (d.y); });
For a more in depth look at how D3-Labeler works, see "A D3 plug-in for automatic label placement using simulated
annealing"
Jeff Heaton's "Artificial Intelligence for Humans, Volume 1" also does an excellent job at explaining the simulated annealing process.
You might be interested in the d3fc-label-layout component (for D3v5) that is designed exactly for this purpose. The component provides a mechanism for arranging child components based on their rectangular bounding boxes. You can apply either a greedy or simulated annealing strategy in order to minimise overlaps.
Here's a code snippet which demonstrates how to apply this layout component to Mike Bostock's map example:
const labelPadding = 2;
// the component used to render each label
const textLabel = layoutTextLabel()
.padding(labelPadding)
.value(d => d.properties.name);
// a strategy that combines simulated annealing with removal
// of overlapping labels
const strategy = layoutRemoveOverlaps(layoutGreedy());
// create the layout that positions the labels
const labels = layoutLabel(strategy)
.size((d, i, g) => {
// measure the label and add the required padding
const textSize = g[i].getElementsByTagName('text')[0].getBBox();
return [textSize.width + labelPadding * 2, textSize.height + labelPadding * 2];
})
.position(d => projection(d.geometry.coordinates))
.component(textLabel);
// render!
svg.datum(places.features)
.call(labels);
And this is a small screenshot of the result:
You can see a complete example here:
http://bl.ocks.org/ColinEberhardt/389c76c6a544af9f0cab
Disclosure: As discussed in the comment below, I am a core contributor of this project, so clearly I am somewhat biased. Full credit to the other answers to this question which gave us inspiration!
For 2D case
here are some examples that do something very similar:
one http://bl.ocks.org/1691430
two http://bl.ocks.org/1377729
thanks Alexander Skaburskis who brought this up here
For 1D case
For those who search a solution to a similar problem in 1-D i can share my sandbox JSfiddle where i try to solve it. It's far from perfect but it kind of doing the thing.
Left: The sandbox model, Right: an example usage
Here is the code snippet which you can run by pressing the button in the end of the post, and also the code itself. When running, click on the field to position the fixed nodes.
var width = 700,
height = 500;
var mouse = [0,0];
var force = d3.layout.force()
.size([width*2, height])
.gravity(0.05)
.chargeDistance(30)
.friction(0.2)
.charge(function(d){return d.fixed?0:-1000})
.linkDistance(5)
.on("tick", tick);
var drag = force.drag()
.on("dragstart", dragstart);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.on("click", function(){
mouse = d3.mouse(d3.select(this).node()).map(function(d) {
return parseInt(d);
});
graph.links.forEach(function(d,i){
var rn = Math.random()*200 - 100;
d.source.fixed = true;
d.source.px = mouse[0];
d.source.py = mouse[1] + rn;
d.target.y = mouse[1] + rn;
})
force.resume();
d3.selectAll("circle").classed("fixed", function(d){ return d.fixed});
});
var link = svg.selectAll(".link"),
node = svg.selectAll(".node");
var graph = {
"nodes": [
{"x": 469, "y": 410},
{"x": 493, "y": 364},
{"x": 442, "y": 365},
{"x": 467, "y": 314},
{"x": 477, "y": 248},
{"x": 425, "y": 207},
{"x": 402, "y": 155},
{"x": 369, "y": 196},
{"x": 350, "y": 148},
{"x": 539, "y": 222},
{"x": 594, "y": 235},
{"x": 582, "y": 185}
],
"links": [
{"source": 0, "target": 1},
{"source": 2, "target": 3},
{"source": 4, "target": 5},
{"source": 6, "target": 7},
{"source": 8, "target": 9},
{"source": 10, "target": 11}
]
}
function tick() {
graph.nodes.forEach(function (d) {
if(d.fixed) return;
if(d.x<mouse[0]) d.x = mouse[0]
if(d.x>mouse[0]+50) d.x--
})
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 dblclick(d) {
d3.select(this).classed("fixed", d.fixed = false);
}
function dragstart(d) {
d3.select(this).classed("fixed", d.fixed = true);
}
force
.nodes(graph.nodes)
.links(graph.links)
.start();
link = link.data(graph.links)
.enter().append("line")
.attr("class", "link");
node = node.data(graph.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", 10)
.on("dblclick", dblclick)
.call(drag);
.link {
stroke: #ccc;
stroke-width: 1.5px;
}
.node {
cursor: move;
fill: #ccc;
stroke: #000;
stroke-width: 1.5px;
opacity: 0.5;
}
.node.fixed {
fill: #f00;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<body></body>
I want some of the nodes in my force-directed layout to ignore all forces and stay in fixed positions based on an attribute of the node, while still being able to be dragged and exert repulsion on other nodes and maintain their link lines.
I thought it would be as simple as this:
force.on("tick", function() {
vis.selectAll("g.node")
.attr("transform", function(d) {
return (d.someAttribute == true) ?
"translate(" + d.xcoordFromAttribute + "," + d.ycoordFromAttribute +")" :
"translate(" + d.x + "," + d.y + ")"
});
});
I have also tried to manually set the node's x and y attributes each tick, but then the links continue to float out to where the node would be if it was affected by the force.
Obviously I have a basic misunderstanding of how this is supposed to work. How can I fix nodes in a position, while keeping links and still allowing for them to be draggable?
Set d.fixed on the desired nodes to true, and initialize d.x and d.y to the desired position. These nodes will then still be part of the simulation, and you can use the normal display code (e.g., setting a transform attribute); however, because they are marked as fixed, they can only be moved by dragging and not by the simulation.
See the force layout documentation for more details (v3 docs, current docs), and also see how the root node is positioned in this example.
Fixed nodes in force layout for d3v4 and d4v5
In d3v3 d.fixed will fix nodes at d.x and d.y; however, in d3v4/5 this method no longer is supported. The d3 documentation states:
To fix a node in a given position, you may specify two additional
properties:
fx - the node’s fixed x-position
fy - the node’s fixed y-position
At the end of each tick, after the application of any forces, a node
with a defined node.fx has node.x reset to this value and node.vx set
to zero; likewise, a node with a defined node.fy has node.y reset to
this value and node.vy set to zero. To unfix a node that was
previously fixed, set node.fx and node.fy to null, or delete these
properties.
You can set fx and fy attributes for the force nodes in your data source, or you can add and remove fx and fy values dynamically. The snippet below sets these properties at the end of drag events, just drag a node to fix its position:
var data ={
"nodes":
[{"id": "A"},{"id": "B"},{"id": "C"},{"id":"D"}],
"links":
[{"source": "A", "target": "B"},
{"source": "B", "target": "C"},
{"source": "C", "target": "A"},
{"source": "D", "target": "A"}]
}
var height = 250;
var width = 400;
var svg = d3.select("body").append("svg")
.attr("width",width)
.attr("height",height);
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }).distance(50))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
var link = svg.append("g")
.selectAll("line")
.data(data.links)
.enter().append("line")
.attr("stroke","black");
var node = svg.append("g")
.selectAll("circle")
.data(data.nodes)
.enter().append("circle")
.attr("r", 5)
.call(d3.drag()
.on("drag", dragged)
.on("end", dragended));
simulation
.nodes(data.nodes)
.on("tick", ticked)
.alphaDecay(0);
simulation.force("link")
.links(data.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 dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.6.0/d3.min.js"></script>
d3v6 changes to event listners
In the above snippet, the drag events use the form
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
Where d is the datum of the node being dragged. In d3v6, the form is now:
function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
or:
function dragged(event,d) {
d.fx = event.x;
d.fy = event.y;
}
The event is now passed directly to the listener, the second parameter passed to the event listener is the datum. Here's the canonical example on Observable.