|
<!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> |