Skip to content

Instantly share code, notes, and snippets.

@proclamo-zz
Last active March 16, 2017 09:56
Show Gist options
  • Save proclamo-zz/0fc304040b7036eb7785 to your computer and use it in GitHub Desktop.
Save proclamo-zz/0fc304040b7036eb7785 to your computer and use it in GitHub Desktop.
Automatic labeling v1

Please, open in the new window to view the full example

This is the first version of a d3 plugin for automatic labeling. I called it labeling.

Each overlapped label rotates until no more overlaps. If one label reach the last position there is three options:

  • scale( factor ): the label scales factor times and run again the algorithm. scale can be called multiple times with different parameters.
  • legend(): transforms the label into a number and builds an internally legend which can be returned with the getLegend() function.
  • remove(): the label is removed.

The basic functioning is shown here:

d3.labeling()
  .select('.label')
  .align()

select can be a d3's valid string selector or a function which returns a d3 selection:

d3.labeling()
  .select(function() { 
     return d3.selectAll('.place-label')
       .sort(function(a,b) { 
           return d3.geo.area(b.geometry) - d3.geo.area(a.geometry); 
       })
  })
  .align();

In the example we have:

d3.labeling()
  .select('.place-label')		// className of the labels
  .legend()				// builds a legend
  .scale(.8)			// if no fit, scales down 0.8 times the label (or number)
  .scale(.5)			// idem but scales 0.5 times    
  .remove()				// else removes
  .align();      			// start
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.countries {
fill: #ccc;
stroke: #fff;
}
.place,
.place-label {
fill: #444;
}
text {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 10px;
pointer-events: none;
}
.map svg {
background-color: white;
}
.land {
opacity: .7;
stroke: #888;
}
div.legend {
position: absolute;
font-size: 10px;
top: 10px;
left: 950px;
}
div.legend ul {
list-style-type: none;
}
div.legend .counter {
font-weight: bold;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
<script type="text/javascript" src="labeling.js"></script>
<script>
var width = 960,
height = 500;
var projection = d3.geo.mercator()
.center([0,37])
.scale(450 / Math.PI)
.translate([width / 2, height / 2]);
var path = d3.geo.path()
.projection(projection);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
d3.json("world.json", function(error, world) {
var countries = topojson.feature(world, world.objects.countries),
geometries = countries.features;
svg.selectAll(".land")
.data(countries.features)
.enter()
.append("path")
.attr("class", "land")
.attr("d", path)
.style("fill", "#cacaca");
svg.selectAll(".place-label")
.data(geometries)
.enter()
.append("text")
.attr("class", "place-label")
.text(function(d) { return d.properties.name; })
.attr("x", function(d) { return path.centroid(d)[0]; })
.attr("y", function(d) { return path.centroid(d)[1]; });
var labeling = d3.labeling()
.select('.place-label')
.legend()
.scale(.8)
.scale(.5)
.remove()
.align();
var legend = d3.select('body')
.append('div')
.attr('class', 'legend')
.append('div')
.attr('class', 'counter')
.text("Visible countries: " + d3.selectAll('.place-label')[0].length + " of 177")
.append('ul');
legend.selectAll('.legend')
.data(labeling.getLegend())
.enter()
.append('li')
.text(function(d) { return d.key + ': ' + d.name; });
});
</script>
;
d3.labeling = function() {
var labeling = {}, className, labels, callbacks = [], callbackpos = -1,
updateLabels = false, _break = false, legend = [];
labeling.select = function(_className) {
className = _className;
labels = getLabels();
return labeling;
}
var getLabels = function() {
if (typeof className === "string") {
return svg.selectAll(className);
}
else {
return className.call(this);
}
}
labeling.remove = function() {
callbacks.push(labeling_remove);
updateLabels = true;
return labeling;
}
labeling.scale = function(_scaleFactor) {
var scaleFactor = _scaleFactor || .75;
// currying: https://medium.com/@kbrainwave/currying-in-javascript-ce6da2d324fe
var fn = function(label) {
return labeling_scale(label, scaleFactor);
}
callbacks.push(fn);
return labeling;
}
labeling.legend = function() {
callbacks.push(labeling_legend);
updateLabels = true;
return labeling;
}
var labeling_remove = function(label) {
label.remove();
}
var labeling_scale = function(label,factor) {
var x = +label.attr("x"), y = +label.attr("y"),
scaleFactor = factor < 1 ? 1 - factor : - (factor - 1),
transform = "translate(" + (x * scaleFactor) + "," + (y * scaleFactor) + ") scale(" + factor + ")";
label.attr("transform", transform);
}
var labeling_legend = function(label) {
legend.push(label.text());
label.text(legend.length);
}
var next = function(label) {
var box = getBox(label);
var pos = label.attr("data-pos") || 0;
pos++;
if (pos === 8) {
var calls = label.attr("data-calls") || -1;
calls++;
label.attr("data-calls", calls);
if (calls < callbacks.length) {
var fn = callbacks[calls];
fn.call(this, label);
}
pos = 0;
box = getBox(label);
// prevent infinite loop
if (!callbacks.length) {
_break = true;
}
}
label.attr("data-pos", pos);
var x = +label.attr("x"), y = +label.attr("y");
switch(pos) {
case 0:
case 1:
y += box.height/2;
break;
case 2:
case 3:
x -= box.width/2;
break;
case 4:
case 5:
y -= box.height/2;
break;
case 6:
case 7:
x += box.width/2;
break;
}
label.attr("x", x).attr("y", y);
}
var overlaps = function(a, b) {
return (
(a.left <= b.left && b.left <= a.right)
||
(a.left <= b.right && b.right <= a.right)
)
&&
(
(a.top <= b.top && b.top <= a.bottom)
||
(a.top <= b.bottom && b.bottom <= a.bottom)
);
}
var getBox = function(d) {
return d[0][0].getBoundingClientRect();
}
labeling.align = function() {
var overlappeds;
do {
overlappeds = 0;
labels.each(function() {
var current = this;
var box_text = getBox(d3.select(current));
var _overlappeds = labels[0].filter(function(d) {
if (d === current) return false;
return overlaps(box_text, getBox(d3.select(d)));
});
overlappeds += _overlappeds.length;
_overlappeds.forEach(function(el) {
next(d3.select(el));
});
});
if (updateLabels) {
labels = getLabels(className);
}
} while (overlappeds > 0 && !_break);
return labeling;
}
labeling.getLegend = function() {
return legend.map(function(d, i) {
return { key: i + 1, name: d }
});
}
return labeling;
}
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment