Last active
May 11, 2016 08:21
-
-
Save kforeman/501685b4f7a4d22c1563b519718c4590 to your computer and use it in GitHub Desktop.
Labeled scatter
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
height: 560 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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