├── LICENSE ├── README.md ├── index.html ├── label.png └── labeler.js /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Evan Wang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | D3-Labeler 2 | ========= 3 | 4 | A D3 plug-in for automatic label placement using simulated annealing that easily incorporates into existing D3 code, with syntax mirroring other D3 layouts. 5 | 6 | [View a demo here](http://tinker10.github.io/D3-Labeler/). 7 | 8 | Installation 9 | ------------ 10 | 11 | Download labeler.js. Include the plug-in within the relevant .html file with: 12 | ```html 13 | 14 | ``` 15 | 16 | Components of a labeling problem 17 | -------------------------------- 18 | 19 | ![label](label.png) 20 | 21 | Each *label* corresponds to an *anchor point*. A *leader line* may be used to help with the correspondence between the *label* and *anchor point*. None of the elements may cross the *graph boundary*. 22 | 23 | Usage 24 | ----------------- 25 | 26 | To automatically place labels, users declare a labeler (simulated annealing) layout, input label and anchor positions, the figure boundaries, and the number of Monte Carlo sweeps for simulated annealing. The general pattern is as follows: 27 | ```javascript 28 | var labels = d3.labeler() 29 | .label(label_array) 30 | .anchor(anchor_array) 31 | .width(w) 32 | .height(h) 33 | .start(nsweeps); 34 | ``` 35 | The default settings are: w = 1, h = 1, and nsweeps = 1000. The default label_array and anchor_array are empty arrays. Here we describe each term in more detail. 36 | 37 | d3.labeler() 38 | 39 | Start by declaring a labeling layout, the same as declaring any other D3 layout. 40 | 41 | labeler.label([label_array]) 42 | 43 | Each label has the following attributes: 44 | 45 | * x - the *x*-coordinate of the label. 46 | * y - the *y*-coordinate of the label. 47 | * width - the *width* of the label (approximating the label as a rectangle). 48 | * height - the *height* of the label (same approximation). 49 | * name - the label text. 50 | 51 | ```javascript 52 | var label_array = [{x: 10.2, y: 17.1, name: "Node 3", width: 18.0, height: 7.2}, ...] 53 | ``` 54 | 55 | Note that width and height can be easily measured using the SVG getBBox() method. The dimensions are used to calculate overlaps. 56 | 57 | ```javascript 58 | var index = 0; 59 | labels.each(function() { 60 | label_array[index].width = this.getBBox().width; 61 | label_array[index].height = this.getBBox().height; 62 | index += 1; 63 | }); 64 | ``` 65 | 66 | labeler.anchor([anchor_array]) 67 | 68 | Each anchor has the following attributes: 69 | 70 | * x - the *x*-coordinate of the anchor. 71 | * y - the *y*-coordinate of the anchor. 72 | * r - the anchor radius (assuming anchor is a circle). 73 | 74 | ```javascript 75 | var anchor_array = [{x: 5.3, y: 12.0, r: 7}, {x: 16.8, y: 23.5, r: 7}, ...] 76 | ``` 77 | 78 | labeler.width(w) 79 | 80 | labeler.height(h) 81 | 82 | The width and height are used to set the boundary conditions so that labels do not go outside the width and height of the figure. More specifically, Monte Carlo moves in which the labels cross the boundaries are rejected. If they are not specified, both the width and height default to 1. 83 | 84 | labeler.start(nsweeps) 85 | 86 | Finally, we specify the number of Monte Carlo sweeps for the optimization and run the simulated annealing procedure. The default for nsweeps is 1000. Note that one Monte Carlo sweep means that on average, each label is translated or rotated once. To obtain the actual number of Monte Carlo steps taken, multiply the number of sweeps by the number of labels. 87 | 88 | labeler.alt_energy(user_defined_energy) 89 | 90 | This function is constructed for expert users. The quality of the configuration is closely related to the energy function. The default energy function includes general labeling preferences (details below) and is suggested for most users. However, a user may wish to define his or her own energy function to suit individual preferences. 91 | ```javascript 92 | new_energy_function = function(index, label_array, anchor_array) { 93 | var ener = 0; 94 | // insert user-defined interaction energies here 95 | return ener; 96 | } 97 | ``` 98 | The newly constructed function must take as input an integer index, an array of labels label_array, and an array of anchors anchor_array. This function must also return an energy term that should correspond to the energy of a particular label, namely label_array[index]. One may wish calculate an energy of interaction for label_array[index] with all other labels and anchors. 99 | 100 | labeler.alt_schedule(user_defined_schedule) 101 | 102 | Similarly, an expert user may wish to include a custom cooling schedule used in the simulated annealing procedure. The default cooling schedule is linear. 103 | ```javascript 104 | new_cooling_schedule = function(currT, initialT, nsweeps) { 105 | // insert user-defined schedule here 106 | return updatedT; 107 | } 108 | ``` 109 | This function takes as input the current simulation temperature currT, the initial temperature initialT, and the total number of sweeps nsweeps and returns the updated temperature updatedT. The user defined functions can be included as follows: 110 | 111 | ```javascript 112 | var labels = d3.labeler() 113 | .label(label_array) 114 | .anchor(anchor_array) 115 | .width(w) 116 | .height(h) 117 | .alt_energy(new_energy_function) 118 | .alt_schedule(new_cooling_schedule) 119 | .start(nsweeps); 120 | ``` 121 | 122 | 123 | Default energy function details 124 | ------------------------------- 125 | 126 | In order to distinguish between the quality of different configurations in our search space, we need construct a function which takes as input a label configuration and outputs a score indicating the quality of the placements. In a labeling problem, the inputs are themselves functions of various parameters such as the amount of overlaps, distances between labels and their corresponding anchor points, and various stylistic preferences. This function, often an energy (also called cost or objective) function, is what we need to optimize. The default energy function includes penalties for: 127 | 128 | * Label-label overlaps 129 | * Label-anchor overlaps 130 | * Labels far from the corresponding anchor 131 | * Leader line intersections 132 | * Poorly oriented labels 133 | 134 | Author 135 | ------ 136 | * Evan Wang () 137 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | D3-Labeler 7 | 8 | 54 | 55 | 56 | 57 | 58 |
59 | 60 |

D3-Labeler

61 |

A D3 plug-in for automatic label placement using simulated annealing that easily incorporates into existing D3 code, with
syntax mirroring other D3 layouts.

62 | 63 |
64 | Number of labels:   |   65 | Uniform  Gaussian  |   66 | Number of MC sweeps:   |   67 | 68 |
69 | 70 |
71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /label.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinker10/D3-Labeler/6de86705a8bbdebb00282aaa61755686f7309b31/label.png -------------------------------------------------------------------------------- /labeler.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | d3.labeler = function() { 4 | var lab = [], 5 | anc = [], 6 | w = 1, // box width 7 | h = 1, // box width 8 | labeler = {}; 9 | 10 | var max_move = 5.0, 11 | max_angle = 0.5, 12 | acc = 0; 13 | rej = 0; 14 | 15 | // weights 16 | var w_len = 0.2, // leader line length 17 | w_inter = 1.0, // leader line intersection 18 | w_lab2 = 30.0, // label-label overlap 19 | w_lab_anc = 30.0; // label-anchor overlap 20 | w_orient = 3.0; // orientation bias 21 | 22 | // booleans for user defined functions 23 | var user_energy = false, 24 | user_schedule = false; 25 | 26 | var user_defined_energy, 27 | user_defined_schedule; 28 | 29 | energy = function(index) { 30 | // energy function, tailored for label placement 31 | 32 | var m = lab.length, 33 | ener = 0, 34 | dx = lab[index].x - anc[index].x, 35 | dy = anc[index].y - lab[index].y, 36 | dist = Math.sqrt(dx * dx + dy * dy), 37 | overlap = true, 38 | amount = 0 39 | theta = 0; 40 | 41 | // penalty for length of leader line 42 | if (dist > 0) ener += dist * w_len; 43 | 44 | // label orientation bias 45 | dx /= dist; 46 | dy /= dist; 47 | if (dx > 0 && dy > 0) { ener += 0 * w_orient; } 48 | else if (dx < 0 && dy > 0) { ener += 1 * w_orient; } 49 | else if (dx < 0 && dy < 0) { ener += 2 * w_orient; } 50 | else { ener += 3 * w_orient; } 51 | 52 | var x21 = lab[index].x, 53 | y21 = lab[index].y - lab[index].height + 2.0, 54 | x22 = lab[index].x + lab[index].width, 55 | y22 = lab[index].y + 2.0; 56 | var x11, x12, y11, y12, x_overlap, y_overlap, overlap_area; 57 | 58 | for (var i = 0; i < m; i++) { 59 | if (i != index) { 60 | 61 | // penalty for intersection of leader lines 62 | overlap = intersect(anc[index].x, lab[index].x, anc[i].x, lab[i].x, 63 | anc[index].y, lab[index].y, anc[i].y, lab[i].y); 64 | if (overlap) ener += w_inter; 65 | 66 | // penalty for label-label overlap 67 | x11 = lab[i].x; 68 | y11 = lab[i].y - lab[i].height + 2.0; 69 | x12 = lab[i].x + lab[i].width; 70 | y12 = lab[i].y + 2.0; 71 | x_overlap = Math.max(0, Math.min(x12,x22) - Math.max(x11,x21)); 72 | y_overlap = Math.max(0, Math.min(y12,y22) - Math.max(y11,y21)); 73 | overlap_area = x_overlap * y_overlap; 74 | ener += (overlap_area * w_lab2); 75 | } 76 | 77 | // penalty for label-anchor overlap 78 | x11 = anc[i].x - anc[i].r; 79 | y11 = anc[i].y - anc[i].r; 80 | x12 = anc[i].x + anc[i].r; 81 | y12 = anc[i].y + anc[i].r; 82 | x_overlap = Math.max(0, Math.min(x12,x22) - Math.max(x11,x21)); 83 | y_overlap = Math.max(0, Math.min(y12,y22) - Math.max(y11,y21)); 84 | overlap_area = x_overlap * y_overlap; 85 | ener += (overlap_area * w_lab_anc); 86 | 87 | } 88 | return ener; 89 | }; 90 | 91 | mcmove = function(currT) { 92 | // Monte Carlo translation move 93 | 94 | // select a random label 95 | var i = Math.floor(Math.random() * lab.length); 96 | 97 | // save old coordinates 98 | var x_old = lab[i].x; 99 | var y_old = lab[i].y; 100 | 101 | // old energy 102 | var old_energy; 103 | if (user_energy) {old_energy = user_defined_energy(i, lab, anc)} 104 | else {old_energy = energy(i)} 105 | 106 | // random translation 107 | lab[i].x += (Math.random() - 0.5) * max_move; 108 | lab[i].y += (Math.random() - 0.5) * max_move; 109 | 110 | // hard wall boundaries 111 | if (lab[i].x > w) lab[i].x = x_old; 112 | if (lab[i].x < 0) lab[i].x = x_old; 113 | if (lab[i].y > h) lab[i].y = y_old; 114 | if (lab[i].y < 0) lab[i].y = y_old; 115 | 116 | // new energy 117 | var new_energy; 118 | if (user_energy) {new_energy = user_defined_energy(i, lab, anc)} 119 | else {new_energy = energy(i)} 120 | 121 | // delta E 122 | var delta_energy = new_energy - old_energy; 123 | 124 | if (Math.random() < Math.exp(-delta_energy / currT)) { 125 | acc += 1; 126 | } else { 127 | // move back to old coordinates 128 | lab[i].x = x_old; 129 | lab[i].y = y_old; 130 | rej += 1; 131 | } 132 | 133 | }; 134 | 135 | mcrotate = function(currT) { 136 | // Monte Carlo rotation move 137 | 138 | // select a random label 139 | var i = Math.floor(Math.random() * lab.length); 140 | 141 | // save old coordinates 142 | var x_old = lab[i].x; 143 | var y_old = lab[i].y; 144 | 145 | // old energy 146 | var old_energy; 147 | if (user_energy) {old_energy = user_defined_energy(i, lab, anc)} 148 | else {old_energy = energy(i)} 149 | 150 | // random angle 151 | var angle = (Math.random() - 0.5) * max_angle; 152 | 153 | var s = Math.sin(angle); 154 | var c = Math.cos(angle); 155 | 156 | // translate label (relative to anchor at origin): 157 | lab[i].x -= anc[i].x 158 | lab[i].y -= anc[i].y 159 | 160 | // rotate label 161 | var x_new = lab[i].x * c - lab[i].y * s, 162 | y_new = lab[i].x * s + lab[i].y * c; 163 | 164 | // translate label back 165 | lab[i].x = x_new + anc[i].x 166 | lab[i].y = y_new + anc[i].y 167 | 168 | // hard wall boundaries 169 | if (lab[i].x > w) lab[i].x = x_old; 170 | if (lab[i].x < 0) lab[i].x = x_old; 171 | if (lab[i].y > h) lab[i].y = y_old; 172 | if (lab[i].y < 0) lab[i].y = y_old; 173 | 174 | // new energy 175 | var new_energy; 176 | if (user_energy) {new_energy = user_defined_energy(i, lab, anc)} 177 | else {new_energy = energy(i)} 178 | 179 | // delta E 180 | var delta_energy = new_energy - old_energy; 181 | 182 | if (Math.random() < Math.exp(-delta_energy / currT)) { 183 | acc += 1; 184 | } else { 185 | // move back to old coordinates 186 | lab[i].x = x_old; 187 | lab[i].y = y_old; 188 | rej += 1; 189 | } 190 | 191 | }; 192 | 193 | intersect = function(x1, x2, x3, x4, y1, y2, y3, y4) { 194 | // returns true if two lines intersect, else false 195 | // from http://paulbourke.net/geometry/lineline2d/ 196 | 197 | var mua, mub; 198 | var denom, numera, numerb; 199 | 200 | denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1); 201 | numera = (x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3); 202 | numerb = (x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3); 203 | 204 | /* Is the intersection along the the segments */ 205 | mua = numera / denom; 206 | mub = numerb / denom; 207 | if (!(mua < 0 || mua > 1 || mub < 0 || mub > 1)) { 208 | return true; 209 | } 210 | return false; 211 | } 212 | 213 | cooling_schedule = function(currT, initialT, nsweeps) { 214 | // linear cooling 215 | return (currT - (initialT / nsweeps)); 216 | } 217 | 218 | labeler.start = function(nsweeps) { 219 | // main simulated annealing function 220 | var m = lab.length, 221 | currT = 1.0, 222 | initialT = 1.0; 223 | 224 | for (var i = 0; i < nsweeps; i++) { 225 | for (var j = 0; j < m; j++) { 226 | if (Math.random() < 0.5) { mcmove(currT); } 227 | else { mcrotate(currT); } 228 | } 229 | currT = cooling_schedule(currT, initialT, nsweeps); 230 | } 231 | }; 232 | 233 | labeler.width = function(x) { 234 | // users insert graph width 235 | if (!arguments.length) return w; 236 | w = x; 237 | return labeler; 238 | }; 239 | 240 | labeler.height = function(x) { 241 | // users insert graph height 242 | if (!arguments.length) return h; 243 | h = x; 244 | return labeler; 245 | }; 246 | 247 | labeler.label = function(x) { 248 | // users insert label positions 249 | if (!arguments.length) return lab; 250 | lab = x; 251 | return labeler; 252 | }; 253 | 254 | labeler.anchor = function(x) { 255 | // users insert anchor positions 256 | if (!arguments.length) return anc; 257 | anc = x; 258 | return labeler; 259 | }; 260 | 261 | labeler.alt_energy = function(x) { 262 | // user defined energy 263 | if (!arguments.length) return energy; 264 | user_defined_energy = x; 265 | user_energy = true; 266 | return labeler; 267 | }; 268 | 269 | labeler.alt_schedule = function(x) { 270 | // user defined cooling_schedule 271 | if (!arguments.length) return cooling_schedule; 272 | user_defined_schedule = x; 273 | user_schedule = true; 274 | return labeler; 275 | }; 276 | 277 | return labeler; 278 | }; 279 | 280 | })(); 281 | 282 | --------------------------------------------------------------------------------