Related
I am thoroughly stumped and would love a helping hand from anyone who can kick me in the right direction. I'm trying to create groups g and a single rectangle inside them. The join works great for the g parent but it does not create the children. What am I missing? (I'm thinking in joins!)
I've tried replacing the join with an enter().append('g') to no avail either, so I'm missing something.
Here is a jsfiddle.
var svg = d3.select("div#canvas")
.append("svg")
.attr("width", '100%')
.attr("height", '100%');
let NodeData = [
{
'x': 20,
'y': 20,
'id': 'abc',
'fill': '#D4C2F1'
},
{
'x': 20,
'y': 80,
'id': 'def',
'fill': '#B3D2C5'
},
];
function updateNodes(data) {
var groups = svg.selectAll('g').data(data)
.join('g')
.attr('node-id', function (d) { return d.id; })
.attr('transform', function (d) { return `translate(${d.x}, ${d.y})` });
var rects = groups.selectAll('rect')
.data(function (d) { return d; })
.join('rect')
.attr('x', 0)
.attr('y', 0)
.attr('width', 80)
.attr('height', 20)
.attr('stroke', '#666666')
.attr('fill', function (d) { return d.fill; });
}
updateNodes(NodeData);
selection.data() requires an array (or function that returns an array). You are not passing an array to .data() when trying to create the child rects. You are passing an object - an individual item in the original data array, so no elements are entered.
To fix this you can simply use:
var rects = groups.selectAll('rect')
.data(function (d) { return [d]; })
Updated fiddle,
But, this is not the best approach if you are just passing the parent's datum as is to a single child. You don't need to use a nested enter/update/exit cycle, you can just append to the parents:
var groups = svg.selectAll('g').data(data)
.join('g')
.attr('node-id', function (d) { return d.id; })
.attr('transform', function (d) { return `translate(${d.x}, ${d.y})` });
var rects = groups.append("rect")
.attr('x', 0)
.attr('y', 0)
.attr('width', 80)
.attr('height', 20)
.attr('stroke', '#666666')
.attr('fill', function (d) { return d.fill; });
Modified fiddle
The new child elements (rects) inherit the bound data of their parents, as you can see here:
var svg = d3.select("div#canvas")
.append("svg")
.attr("width", '100%')
.attr("height", '100%');
let NodeData = [
{
'x': 20,
'y': 20,
'id': 'abc',
'fill': '#D4C2F1'
},
{
'x': 20,
'y': 80,
'id': 'def',
'fill': '#B3D2C5'
},
];
function updateNodes(data) {
var groups = svg.selectAll('g').data(data)
.join('g')
.attr('node-id', function (d) { return d.id; })
.attr('transform', function (d) { return `translate(${d.x}, ${d.y})` });
var rects = groups.append('rect')
.attr('x', 0)
.attr('y', 0)
.attr('width', 80)
.attr('height', 20)
.attr('stroke', '#666666')
.attr('fill', function (d) { return d.fill; });
rects.each(function(d) {
console.log(d);
})
}
updateNodes(NodeData);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.9.0/d3.min.js"></script>
<div id="canvas" style="border: 1px solid #D9D9D9; width: 100%; height: 600px; margin-top: 6px"></div>
First, let me accept that i am new to d3.js. I am fairly accustomed to R. I have been doing plot in d3.js, thanks to the stack overflow community and some may d3.js example. I came across r2d3 as a mode to connect R and d3.js plots and hence have used the same to build a connection.
I have created a plot in d3.js and wanted to connect it with the R Shiny output. I am able to connect the plot to the R Shiny. But the plot is always coming out of the shiny plot area.
This is How my plot looks in the R Shiny area :
My Existing d3 plot in R Shiny
Requesting your suggestions on the following :
How to fix the d3.js plot within the R Shiny Area.
My Ui.R code is as below :
column(d3Output("clplot"),width = 12)
My server code is as below :
output$clplot <-r2d3::renderD3(
r2d3(data = cl_d3(),script="d3/cl_dilip_v1.js", d3_version = "5")
)
The js code is attached as below :
var margin = { top: 20, right: 20, bottom: 30, left: 50 },
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom;
var jsondata = [{ "promotedprice": 100, "nonpromotedprice": 350, "avgprice": 230, "x_value": 80, "brand": "Brand1" }, { "promotedprice": 99, "nonpromotedprice": 170, "avgprice": 130, "x_value": 140, "brand": "Brand2" }, { "promotedprice": 47, "nonpromotedprice": 147, "avgprice": 80, "x_value": 200, "brand": "Brand3" }, { "promotedprice": 100, "nonpromotedprice": 250, "avgprice": 220, "x_value": 260, "brand": "Brand4" }, { "promotedprice": 99, "nonpromotedprice": 170, "avgprice": 130, "x_value": 320, "brand": "Brand5" }];
// Creating the colour Category
var color = d3.scaleOrdinal(d3.schemeCategory10);
// Creating the 1st Comapartment
var svg = d3.select("body").append("svg")
.attr("width", 400)
.attr("height", 450);
var div = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
// Attach the Promoted Price Rectangle
var g = svg.selectAll("rect")
.data(data)
.enter()
.append("g")
.classed('rect', false)
.on("mouseover", function (d) {
div.transition()
.duration(200)
.style("opacity", .9);
div.html(formatTime(d.x_value) + "<br/>" + d.nonpromotedprice);
//.style("left", (d3.event.pageX) + "px")
//.style("top", (d3.event.pageY - 28) + "px");
})
.on("mouseout", function (d) {
div.transition()
.duration(0)
.style("opacity", 0);
})
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
var accent = d3.scaleOrdinal(d3.schemeAccent);
// Line for the 1st Block
g.append("line") // attach a line
.style("stroke", "#E6EAEE")
.style("stroke-width", 17) // colour the line
.attr("x1", function (d) { return d.x_value; }) // x position of the first end of the line
.attr("y1", function (d) { return d.nonpromotedprice; }) // y position of the first end of the line
.attr("x2", function (d) { return d.x_value; }) // x position of the second end of the line
.attr("y2", function (d) { return d.promotedprice; });
// Promoted Price Rectangle for the 1st Block
g.append("rect")
.attr("width", 24)
.attr("height", 13)
.attr("x", function (d) { return d.x_value - 12; })
.attr("y", function (d) { return d.promotedprice; })
.attr("fill", function (d) { return color(d.x_value); })
// Non Promoted Price Rectangle for the 1st Block
g.append("rect")
.attr("width", 24)
.attr("height", 13)
.attr("x", function (d) { return d.x_value - 12; })
.attr("y", function (d) { return d.nonpromotedprice; })
.attr("fill", function (d) { return color(d.x_value); })
// Average Price Rectangle for the 1st Block
g.append("rect")
.attr("width", 24)
.attr("height", 13)
.attr("x", function (d) { return d.x_value - 12; })
.attr("y", function (d) { return d.avgprice; })
.attr("fill", function (d) { return color(d.x_value); });
// Graph X- Axis and Title Text for 1st svg
var y_scale = d3.scaleLinear()
//.domain([d3.min(function (d) { return d.promotedprice }), d3.max(function (d) { return d.nonpromotedprice; })])
.range([370, 0]);
var y_axis = d3.axisLeft()
.scale(y_scale);
y_scale.domain([0, d3.max(data, function (d) { return d.nonpromotedprice; })]).nice();
g.append("g")
.attr("class", "grid")
.attr("transform", "translate(0, 40)")
.attr("fill", "lightgrey")
.attr("stroke-width", 0.15)
.attr("stroke-opacity", 0.2)
//.attr("shape-rendering", crispEdges)
//stroke-opacity: 0.7;shape-rendering: crispEdges;
.call(y_axis
.tickSize(-420)
.tickFormat(""))
;
// PEPSICO AS-IS BRAND CALL OUT
g.append("rect")
.attr("width", 38)
.attr("height", 20)
.attr("x", function (d) { return d.x_value - 2; })
.attr("y", function (d) { return d.promotedprice; })
.attr("fill", function (d) { return color(d.x_value); })
.attr("transform", "translate(" + -15 + "," + -40 + ")");
g.append("text")
//.classed('rotation', true)
//.attr('x', (d,i)=> xScale(i))
.attr("x", function (d) { return d.x_value - 13; })
.attr("y", function (d) { return d.promotedprice - 28; })
.attr("dy", ".35em")
.text(function (d) { return d.brand; })
.style("font-family", "arial")
.style("font-size", 8)
.attr("stroke-width", 0.15)
.attr("fill", "white");
function dragstarted(d) {
d3.select(this).raise().classed("active", true);
}
function dragged(d) {
d3.select(this).select("rect").attr("y", d.y = d3.event.y);
//.attr("x", d.x = d3.event.x)
}
function dragended(d)
{
d3.select(this).classed("active", false);
}
Try to delete this part of code:
var svg = d3.select("body").append("svg")
.attr("width", 400)
.attr("height", 450);
I'm trying to get the values for x and y to make a circles using d3.js v4. With the following code I manage to create the chart like behavior of the circles, but when I try to run the same code in v4 it doesn't work anymore. I know that there are some differences in the update to v4 but I didn't find any information about it. So i was wondering if someone can help me to run this code in d3.js v4.
Here is the code using v3 (it will break using v4):
var svg = d3.select('body').append('svg')
.attr('width', 250)
.attr('height', 250);
//render the data
function render(data) {
//Bind
var circles = svg.selectAll('circle').data(data);
//Enter
circles.enter().append('circle')
.attr('r', 10);
//Update
circles
.attr('cx', function(d) {
return d.x;
})
.attr('cy', function(d) {
return d.y;
});
//Exit
circles.exit().remove();
}
var myObjects = [{
x: 100,
y: 100
}, {
x: 130,
y: 120
}, {
x: 80,
y: 180
}, {
x: 180,
y: 80
}, {
x: 180,
y: 40
}];
render(myObjects);
<script src='https://d3js.org/d3.v3.min.js'></script>
This is the expected behaviour, and I've explained this before in this answer (not a duplicate, though).
What happened is that Mike Bostock, D3 creator, introduced a magic behaviour in D3 v2, which he kept in D3 v3.x, but decided to abandon in D3 v4.x. To read more about that, have a look here: What Makes Software Good? This is what he says:
D3 2.0 introduced a change: appending to the enter selection would now copy entering elements into the update selection [...] D3 4.0 removes the magic of enter.append. (In fact, D3 4.0 removes the distinction between enter and normal selections entirely: there is now only one class of selection.)
Let's see it.
Here is your code with D3 v3:
var svg = d3.select('body').append('svg')
.attr('width', 250)
.attr('height', 250);
//render the data
function render(data) {
//Bind
var circles = svg.selectAll('circle').data(data);
//Enter
circles.enter().append('circle')
.attr('r', 10);
//Update
circles
.attr('cx', function(d) {
return d.x;
})
.attr('cy', function(d) {
return d.y;
});
//Exit
circles.exit().remove();
}
var myObjects = [{
x: 100,
y: 100
}, {
x: 130,
y: 120
}, {
x: 80,
y: 180
}, {
x: 180,
y: 80
}, {
x: 180,
y: 40
}];
render(myObjects);
<script src='https://d3js.org/d3.v3.min.js'></script>
Now the same code, with D3 v4. It will "break":
var svg = d3.select('body').append('svg')
.attr('width', 250)
.attr('height', 250);
//render the data
function render(data) {
//Bind
var circles = svg.selectAll('circle').data(data);
//Enter
circles.enter().append('circle')
.attr('r', 10);
//Update
circles
.attr('cx', function(d) {
return d.x;
})
.attr('cy', function(d) {
return d.y;
});
//Exit
circles.exit().remove();
}
var myObjects = [{
x: 100,
y: 100
}, {
x: 130,
y: 120
}, {
x: 80,
y: 180
}, {
x: 180,
y: 80
}, {
x: 180,
y: 40
}];
render(myObjects);
<script src='https://d3js.org/d3.v4.min.js'></script>
By "break" I mean the circles will be appended, but they will not receive the x and y properties in the "enter" selection, and they will default to zero. That's why you see all circles at the top left corner.
Solution: merge the selections:
circles.enter().append('circle')
.attr('r', 10)
.merge(circles) //from now on, enter + update
.attr('cx', function(d) {
return d.x;
})
.attr('cy', function(d) {
return d.y;
});
According to the API, merge()...
... is commonly used to merge the enter and update selections after a data-join. After modifying the entering and updating elements separately, you can merge the two selections and perform operations on both without duplicate code.
Here is the code with merge():
var svg = d3.select('body').append('svg')
.attr('width', 250)
.attr('height', 250);
//render the data
function render(data) {
//Bind
var circles = svg.selectAll('circle').data(data);
//Enter
circles.enter().append('circle')
.attr('r', 10)
.merge(circles) //from now on, enter + update
.attr('cx', function(d) {
return d.x;
})
.attr('cy', function(d) {
return d.y;
});
//Exit
circles.exit().remove();
}
var myObjects = [{
x: 100,
y: 100
}, {
x: 130,
y: 120
}, {
x: 80,
y: 180
}, {
x: 180,
y: 80
}, {
x: 180,
y: 40
}];
render(myObjects);
<script src='https://d3js.org/d3.v4.min.js'></script>
Update pattern was changed in d3v4.
Excerpt from the documentation:
In addition, selection.append no longer merges entering nodes into the
update selection; use selection.merge to combine enter and update
after a data join.
You should rewrite your code this way:
var svg = d3.select('body').append('svg')
.attr('width', 250)
.attr('height', 250);
//render the data
function render(data){
//Bind
var circles = svg.selectAll('circle').data(data);
//Enter
circles.enter().append('circle')
.attr('r', 10).merge(circles) // <== !!!
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; });
//Exit
circles.exit().remove();
}
var myObjects = [
{x: 100, y: 100},
{x: 130, y: 120},
{x: 80, y: 180},
{x: 180, y: 80},
{x: 180, y: 40}
];
render(myObjects);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.11.0/d3.min.js"></script>
There are many cases online how to plot couple of lines in d3 if you add svg object only once, such as
svg.selectAll("line")
.data(dataset)
.enter().append("line")
.style("stroke", "black") // colour the line
.attr("x1", function(d) { console.log(d); return xScale(d.x1); })
.attr("y1", function(d) { return yScale(d.y1); })
.attr("x2", function(d) { return xScale(d.x2); })
.attr("y2", function(d) { return yScale(d.y2); });
This plot create one line. I want to create many different lines in an array smth like
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
for (a_ind=1; a_ind<3; a_ind++){
dataset_a=dataset.filter(function(d) { return (d.a==a_ind)})
svg.selectAll("line")
.data(dataset_a) - //!!! using new dataset in each cycle
.enter().append("line")
.style("stroke", "black") // colour the line
.attr("x1", function(d) { console.log(d); return xScale(d.x1); })
.attr("y1", function(d) { return yScale(d.y1); })
.attr("x2", function(d) { return xScale(d.x2); })
.attr("y2", function(d) { return yScale(d.y2); });
}
I was told it's impossible. Or maybe there is the way? And also how to access then line from dataset_a if i want to delete it with the click of the mouse?
Well, if you want to plot lines, I suggest that you append...<line>s!
The thing with a D3 enter selection is quite simple: the number of appended elements is the number of objects in the data array that doesn't match any element.
So, you just need a data array with several objects. For instance, let's create 50 of them:
var data = d3.range(50).map(function(d) {
return {
x1: Math.random() * 300,
x2: Math.random() * 300,
y1: Math.random() * 150,
y2: Math.random() * 150,
}
});
And, as in the below demo I'm selecting null, all of them will be in the enter selection. Here is the demo:
var svg = d3.select("svg");
var data = d3.range(50).map(function(d) {
return {
x1: Math.random() * 300,
x2: Math.random() * 300,
y1: Math.random() * 150,
y2: Math.random() * 150,
}
});
var color = d3.scaleOrdinal(d3.schemeCategory20);
var lines = svg.selectAll(null)
.data(data)
.enter()
.append("line")
.attr("x1", function(d) {
return d.x1
})
.attr("x2", function(d) {
return d.x2
})
.attr("y1", function(d) {
return d.y1
})
.attr("y2", function(d) {
return d.y2
})
.style("stroke", function(_, i) {
return color(i)
})
.style("stroke-width", 1);
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
Finally, a tip: as this is JavaScript you can use for loops anywhere you want. However, do not use for loops to append elements in a D3 code. It's unnecessary and not idiomatic.
That being said, whoever told you that it is impossible was wrong, it's clearly possible. Here is a demo (but don't do that, it's a very cumbersome and ugly code):
var svg = d3.select("svg");
var data = d3.range(50).map(function(d, i) {
return {
x1: Math.random() * 300,
x2: Math.random() * 300,
y1: Math.random() * 150,
y2: Math.random() * 150,
id: "id" + i
}
});
var color = d3.scaleOrdinal(d3.schemeCategory20);
for (var i = 0; i < data.length; i++) {
var filteredData = data.filter(function(d) {
return d.id === "id" + i
});
var lines = svg.selectAll(null)
.data(filteredData)
.enter()
.append("line")
.attr("x1", function(d) {
return d.x1
})
.attr("x2", function(d) {
return d.x2
})
.attr("y1", function(d) {
return d.y1
})
.attr("y2", function(d) {
return d.y2
})
.style("stroke", function() {
return color(i)
})
.style("stroke-width", 1);
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
I would do something like this. Make each data set (1 data set per line), an array inside the final data array .enter().append() will then work properly. To remove the line on click, I added an event handler that will select the line just clicked and remove it.
var data = [[dataset_a], [dataset_b], [dataset_c], [dataset_d], [dataset_e]];
var xValue = function(d){return d.x;}
var yValue = function(d){return d.y;}
var lineFunction = d3.line()
.x(function(d) { return xScale(xValue(d)); })
.y(function(d) { return yScale(yValue(d)); });
var lines = d3.select("svg").selectAll("path")
lines.data(data)
.enter().append("path")
.attr("d", lineFunction)
.on("click", function(d){
d3.select(this).remove();
});
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>