Skip to content

Instantly share code, notes, and snippets.

@kforeman
Last active May 11, 2016 08:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kforeman/501685b4f7a4d22c1563b519718c4590 to your computer and use it in GitHub Desktop.
Save kforeman/501685b4f7a4d22c1563b519718c4590 to your computer and use it in GitHub Desktop.
Labeled scatter
height: 560
cause rate1990 rate2015 rate2040
A1 5 3 0.5
A2 5 2 4
A3 5 4 0.5
B1 5 7 10
B2 5 8 3
C1 5 10 10
C2 5 6 15
<!DOCTYPE html>
<html>
<head>
<!-- header/title stuff -->
<meta http-equiv='Content-Type' content='text/html;charset=utf-8'>
<title>d3 test</title>
<!-- load in d3 -->
<script src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<!-- load in labeler plugin -->
<script src="labeler.js" charset="utf-8"></script>
<!-- styling -->
<style>
body {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.dot {
stroke: #000;
opacity: 0.7;
}
</style>
</head>
<body>
<div id='chart'></div>
<script type="text/javascript">
var margin = {top: 20, right: 20, bottom: 40, left: 40},
width = 560 - margin.left - margin.right,
height = 560 - margin.top - margin.bottom;
var x = d3.scale.linear()
.range([0, width]);
var y = d3.scale.linear()
.range([height, 0]);
var size = d3.scale.sqrt()
.range([10,30]);
var color = d3.scale.category10();
var xAxis = d3.svg.axis()
.scale(x)
.tickFormat(d3.format(".0%"))
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.tickFormat(d3.format(".0%"))
.orient("left");
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.csv('data.csv', function(data) {
data.forEach(function(d) {
d.rate1990 = +d.rate1990;
d.rate2015 = +d.rate2015;
d.rate2040 = +d.rate2040;
d.change1 = (Math.log(d.rate2015) - Math.log(d.rate1990)) / (2015-1990);
d.change2 = (Math.log(d.rate2040) - Math.log(d.rate2015)) / (2040-2015);
d.group = d.cause.substr(0,1);
});
var xe = d3.extent(data, function(d) { return d.change1; }),
ye = d3.extent(data, function(d) { return d.change2; }),
mn = d3.min([xe[0],ye[0]]),
mx = d3.max([xe[1],ye[1]]);
mn *= (mn > 0 ? .9 : 1.11);
mx *= (mn < 0 ? .9 : 1.11);
x.domain([mn,mx]).nice();
y.domain([mn,mx]).nice();
size.domain(d3.extent(data, function(d) { return d.rate2015; }));
svg.append('line')
.attr('x1', x.range()[0])
.attr('y1', y.range()[0])
.attr('x2', x.range()[1])
.attr('y2', y.range()[1])
.style('stroke', 'black');
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.append("text")
.attr("class", "axis-label")
.attr("x", (margin.left + (width - margin.left - margin.right)/2))
.attr("y", margin.bottom)
.style("text-anchor", "middle")
.text("Rate of change 1990-2015");
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("class", "axis-label")
.attr("transform", "translate("+(-1*margin.left)+"," +(margin.top + (height - margin.top - margin.bottom)/2) +")rotate(-90)")
.attr("dy", ".71em")
.style("text-anchor", "middle")
.text("Rate of change 2015-2040")
svg.selectAll(".dot")
.data(data)
.enter().append("circle")
.attr("class", "dot")
.attr("r", function(d) { return size(d.rate2015); })
.attr("cx", function(d) { return x(d.change1); })
.attr("cy", function(d) { return y(d.change2); })
.style("fill", function(d) { return color(d.group); });
var legend = svg.selectAll(".legend")
.data(color.domain())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
legend.append("rect")
.attr("x", 6)
.attr("width", 18)
.attr("height", 18)
.style("fill", color);
legend.append("text")
.attr("x", 32)
.attr("y", 9)
.attr("dy", ".35em")
.text(function(d) { return d; });
var label_array = data.map(function(d) { return {
'x': x(d.change1),
'y': y(d.change2),
'name': d.cause
}; });
var anchor_array = data.map(function(d) { return {
'x': x(d.change1),
'y': y(d.change2),
'r': size(d.rate2015)*1.05
}; });
// Draw labels
labels = svg.selectAll(".label")
.data(label_array)
.enter()
.append("text")
.attr("class", "label")
.attr('text-anchor', 'start')
.text(function(d) { return d.name; })
.attr("x", function(d) { return (d.x); })
.attr("y", function(d) { return (d.y); })
.attr("fill", "black");
var index = 0;
labels.each(function() {
label_array[index].width = this.getBBox().width;
label_array[index].height = this.getBBox().height;
index += 1;
});
// Setup labels
var sim_ann = d3.labeler()
.label(label_array)
.anchor(anchor_array)
.width(width)
.height(height)
.start(1000);
labels
.transition()
.duration(300)
.attr("x", function(d) { return (d.x); })
.attr("y", function(d) { return (d.y); });
});
</script>
</body>
</html>
(function() {
d3.labeler = function() {
var lab = [],
anc = [],
w = 1, // box width
h = 1, // box width
labeler = {};
var max_move = 5.0,
max_angle = 0.5,
acc = 0;
rej = 0;
// weights
var w_len = 0.2, // leader line length
w_inter = 1.0, // leader line intersection
w_lab2 = 30.0, // label-label overlap
w_lab_anc = 30.0; // label-anchor overlap
w_orient = 3.0; // orientation bias
// booleans for user defined functions
var user_energy = false,
user_schedule = false;
var user_defined_energy,
user_defined_schedule;
energy = function(index) {
// energy function, tailored for label placement
var m = lab.length,
ener = 0,
dx = lab[index].x - anc[index].x,
dy = anc[index].y - lab[index].y,
dist = Math.sqrt(dx * dx + dy * dy),
overlap = true,
amount = 0
theta = 0;
// penalty for length of leader line
if (dist > 0) ener += dist * w_len;
// label orientation bias
dx /= dist;
dy /= dist;
if (dx > 0 && dy > 0) { ener += 0 * w_orient; }
else if (dx < 0 && dy > 0) { ener += 1 * w_orient; }
else if (dx < 0 && dy < 0) { ener += 2 * w_orient; }
else { ener += 3 * w_orient; }
var x21 = lab[index].x,
y21 = lab[index].y - lab[index].height + 2.0,
x22 = lab[index].x + lab[index].width,
y22 = lab[index].y + 2.0;
var x11, x12, y11, y12, x_overlap, y_overlap, overlap_area;
for (var i = 0; i < m; i++) {
if (i != index) {
// penalty for intersection of leader lines
overlap = intersect(anc[index].x, lab[index].x, anc[i].x, lab[i].x,
anc[index].y, lab[index].y, anc[i].y, lab[i].y);
if (overlap) ener += w_inter;
// penalty for label-label overlap
x11 = lab[i].x;
y11 = lab[i].y - lab[i].height + 2.0;
x12 = lab[i].x + lab[i].width;
y12 = lab[i].y + 2.0;
x_overlap = Math.max(0, Math.min(x12,x22) - Math.max(x11,x21));
y_overlap = Math.max(0, Math.min(y12,y22) - Math.max(y11,y21));
overlap_area = x_overlap * y_overlap;
ener += (overlap_area * w_lab2);
}
// penalty for label-anchor overlap
x11 = anc[i].x - anc[i].r;
y11 = anc[i].y - anc[i].r;
x12 = anc[i].x + anc[i].r;
y12 = anc[i].y + anc[i].r;
x_overlap = Math.max(0, Math.min(x12,x22) - Math.max(x11,x21));
y_overlap = Math.max(0, Math.min(y12,y22) - Math.max(y11,y21));
overlap_area = x_overlap * y_overlap;
ener += (overlap_area * w_lab_anc);
}
return ener;
};
mcmove = function(currT) {
// Monte Carlo translation move
// select a random label
var i = Math.floor(Math.random() * lab.length);
// save old coordinates
var x_old = lab[i].x;
var y_old = lab[i].y;
// old energy
var old_energy;
if (user_energy) {old_energy = user_defined_energy(i, lab, anc)}
else {old_energy = energy(i)}
// random translation
lab[i].x += (Math.random() - 0.5) * max_move;
lab[i].y += (Math.random() - 0.5) * max_move;
// hard wall boundaries
if (lab[i].x > w) lab[i].x = x_old;
if (lab[i].x < 0) lab[i].x = x_old;
if (lab[i].y > h) lab[i].y = y_old;
if (lab[i].y < 0) lab[i].y = y_old;
// new energy
var new_energy;
if (user_energy) {new_energy = user_defined_energy(i, lab, anc)}
else {new_energy = energy(i)}
// delta E
var delta_energy = new_energy - old_energy;
if (Math.random() < Math.exp(-delta_energy / currT)) {
acc += 1;
} else {
// move back to old coordinates
lab[i].x = x_old;
lab[i].y = y_old;
rej += 1;
}
};
mcrotate = function(currT) {
// Monte Carlo rotation move
// select a random label
var i = Math.floor(Math.random() * lab.length);
// save old coordinates
var x_old = lab[i].x;
var y_old = lab[i].y;
// old energy
var old_energy;
if (user_energy) {old_energy = user_defined_energy(i, lab, anc)}
else {old_energy = energy(i)}
// random angle
var angle = (Math.random() - 0.5) * max_angle;
var s = Math.sin(angle);
var c = Math.cos(angle);
// translate label (relative to anchor at origin):
lab[i].x -= anc[i].x
lab[i].y -= anc[i].y
// rotate label
var x_new = lab[i].x * c - lab[i].y * s,
y_new = lab[i].x * s + lab[i].y * c;
// translate label back
lab[i].x = x_new + anc[i].x
lab[i].y = y_new + anc[i].y
// hard wall boundaries
if (lab[i].x > w) lab[i].x = x_old;
if (lab[i].x < 0) lab[i].x = x_old;
if (lab[i].y > h) lab[i].y = y_old;
if (lab[i].y < 0) lab[i].y = y_old;
// new energy
var new_energy;
if (user_energy) {new_energy = user_defined_energy(i, lab, anc)}
else {new_energy = energy(i)}
// delta E
var delta_energy = new_energy - old_energy;
if (Math.random() < Math.exp(-delta_energy / currT)) {
acc += 1;
} else {
// move back to old coordinates
lab[i].x = x_old;
lab[i].y = y_old;
rej += 1;
}
};
intersect = function(x1, x2, x3, x4, y1, y2, y3, y4) {
// returns true if two lines intersect, else false
// from http://paulbourke.net/geometry/lineline2d/
var mua, mub;
var denom, numera, numerb;
denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);
numera = (x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3);
numerb = (x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3);
/* Is the intersection along the the segments */
mua = numera / denom;
mub = numerb / denom;
if (!(mua < 0 || mua > 1 || mub < 0 || mub > 1)) {
return true;
}
return false;
}
cooling_schedule = function(currT, initialT, nsweeps) {
// linear cooling
return (currT - (initialT / nsweeps));
}
labeler.start = function(nsweeps) {
// main simulated annealing function
var m = lab.length,
currT = 1.0,
initialT = 1.0;
for (var i = 0; i < nsweeps; i++) {
for (var j = 0; j < m; j++) {
if (Math.random() < 0.5) { mcmove(currT); }
else { mcrotate(currT); }
}
currT = cooling_schedule(currT, initialT, nsweeps);
}
};
labeler.width = function(x) {
// users insert graph width
if (!arguments.length) return w;
w = x;
return labeler;
};
labeler.height = function(x) {
// users insert graph height
if (!arguments.length) return h;
h = x;
return labeler;
};
labeler.label = function(x) {
// users insert label positions
if (!arguments.length) return lab;
lab = x;
return labeler;
};
labeler.anchor = function(x) {
// users insert anchor positions
if (!arguments.length) return anc;
anc = x;
return labeler;
};
labeler.alt_energy = function(x) {
// user defined energy
if (!arguments.length) return energy;
user_defined_energy = x;
user_energy = true;
return labeler;
};
labeler.alt_schedule = function(x) {
// user defined cooling_schedule
if (!arguments.length) return cooling_schedule;
user_defined_schedule = x;
user_schedule = true;
return labeler;
};
return labeler;
};
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment