| (function() { |
| |
| d3.labeler = function() { |
| var lab = [], |
| anc = [], |
| w = 1, // box width |
| h = 1, // box width |
| update, |
| labeler = {}; |
| |
| var max_move = 5.0/20, |
| 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.start2 = 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.start = function(nsweeps) { |
| // main simulated annealing function |
| if(nsweeps <= 0) |
| return; |
| |
| var m = lab.length, |
| currT = 1.0, |
| initialT = 1.0; |
| |
| for (var j = 0; j < m; j++) { |
| if (Math.random() < 0.5) { mcmove(currT); } |
| else { mcrotate(currT); } |
| } |
| update(); |
| currT = cooling_schedule(currT, initialT, nsweeps); |
| setTimeout(labeler.start(nsweeps-1), 2); |
| }; |
| |
| 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.update = function(x) { |
| // users insert label positions |
| if (!arguments.length) return null; |
| update = 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; |
| }; |
| |
| })(); |
| |