Skip to content

Instantly share code, notes, and snippets.

@thudfactor
Last active December 23, 2015 20:19
Show Gist options
  • Save thudfactor/6688739 to your computer and use it in GitHub Desktop.
Save thudfactor/6688739 to your computer and use it in GitHub Desktop.
Constraint relaxing on a scatterplot

This demonstrates relaxing constraints on label placement. Reload the example to get a different scatterplot. Click "relax" do show the labels reorganizing themselves to prevent stacking.

The relax routine checks collisions between each member of the data and every subsequent member of the data array. If there is a collision, the labels get nudged a bit in opposite directions, and the entire array is walked again. The walking continues until no collisions are found. The routine was inspired by http://bl.ocks.org/syntagmatic/4054247, but I've made some changes to allow more than one label to occupying the same vertical space. I've also removed the setTimeout() method, choosing a loop instead -- this blocks display while computing, but executes much faster.

This is a relatively naïve example intended to demonstrate the basic approach, not serve as a one-size-fits-all, robust solution. Other things to take into consideration: Things I've ignored: changing height or width of labels, boundaries of the charts, etc. The circumstances of your implementation may not require anything fancy here, but there are ways to augment the example to handle all of those edge cases. It becomes computationally expensive with many labels, but in that case you have more problems than processing time with your visualization.

<!DOCTYPE html>
<html>
<head>
<title>Constraint Relaxing</title>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<style>
.point {
fill: #333;
}
.label {
font-family: 'arial','helvetica','sans-serif';
font-size: 12px;
fill: #003939;
}
.line {
fill: none;
stroke: #b9b9b9;
}
</style>
</head>
<body>
<p><button id="relax">Relax</button></p>
<div id="canvas"></div>
<script>
// Typical D3 boilerplate here for creating
// a container.
var height = 420;
var width = 720;
var base = d3.select("#canvas").append("svg")
.attr({
width: width,
height: height,
}).append("g").attr("transform","translate(10,10)");
// I like to put different elements in their own
// groups. We will have points, lines, and labels.
// Source order determines visual stacking order.
var lines = base.append("g").attr("class","lines");
var points = base.append("g").attr("class","points");
var labels = base.append("g").attr("class","labels");
// Let's randomize some data. The height, width changes are to
// make sure there's room for labels and the points don't crowd
// the margins
var dataPoints = []
for (i = 0; i < 100; i++) {
dataPoints.push({
x: Math.floor(Math.random() * (width - 80)),
y: Math.floor(Math.random() * (height - 20)),
label: "Point " + i
});
}
// Precalculate natural label position based on data points,
// above and to the right of the dot. We'll handle relaxing
// later.
dataPoints.forEach(function(member){
member.labelX = member.x + 5;
member.labelY = member.y - 5;
});
// Place the points
var viewPoints = points.selectAll("point").data(dataPoints);
viewPoints.enter()
.append("circle")
.attr("class","point")
.attr({
cx: function(d) { return d.x },
cy: function(d) { return d.y },
r: 2
});
// Place the labels
var viewLabels = labels.selectAll("label").data(dataPoints);
viewLabels.enter()
.append("text")
.attr("class","label")
.text(function(d) { return d.label })
viewLabels.attr({
x: function(d) { return d.labelX },
y: function(d) { return d.labelY }
})
// Place the lines. Lines use the point position
// for the x1,y1 and the label position for the x2,y2.
var viewLines = lines.selectAll("line").data(dataPoints);
viewLines.enter()
.append("line").attr("class","line");
viewLines.attr({
x1: function(d) { return d.x},
y1: function(d) { return d.y},
x2: function(d) { return d.labelX},
y2: function(d) { return d.labelY}
});
// The constraint relaxing routine. This is based
// on http://bl.ocks.org/syntagmatic/4054247 ,
// but relaxing is done in one loop instead of
// using setTimeout() to tick the set.
var relax = function() {
again = true;
spacingX = 50;
spacingY = 12;
step = .5;
while(again) {
again = false
dataPoints.forEach(function(a,index) {
dataPoints.slice(index+1).forEach(function(b) {
// We're always moving along the Y axis, but
// we need to check the X-axis space, otherwise
// we end up with only one label occupying a
// y+space rank on the chart, and things
// get spread out pretty quickly.
dy = a.labelY - b.labelY;
dx = a.labelX - b.labelX;
if(Math.abs(dy) < spacingY && Math.abs(dx) < spacingX) {
sign = (dy > 0) ? 1 : -1;
deltaPos = sign*step;
a.labelY += deltaPos;
b.labelY -= deltaPos;
again = true;
}
});
});
}
viewLabels.data(dataPoints).transition().attr("y",function(d) { return d.labelY });
viewLines.data(dataPoints).transition().attr("y2",function(d) { return d.labelY });
}
d3.select("#relax").on("click",relax);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment