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