|
288 | 288 | return output;
|
289 | 289 | };
|
290 | 290 |
|
| 291 | + // orientates a bunch of circles to point in orientation |
| 292 | + function orientateCircles(circles, orientation) { |
| 293 | + // sort circles by size |
| 294 | + circles.sort(function (a, b) { return b.radius - a.radius; }); |
| 295 | + |
| 296 | + var i; |
| 297 | + // shift circles so largest circle is at (0, 0) |
| 298 | + if (circles.length > 0) { |
| 299 | + var largestX = circles[0].x, |
| 300 | + largestY = circles[0].y; |
| 301 | + |
| 302 | + for (i = 0; i < circles.length; ++i) { |
| 303 | + circles[i].x -= largestX; |
| 304 | + circles[i].y -= largestY; |
| 305 | + } |
| 306 | + } |
| 307 | + |
| 308 | + // rotate circles so that second largest is at an angle of 'orientation' |
| 309 | + // from largest |
| 310 | + if (circles.length > 1) { |
| 311 | + var rotation = Math.atan2(circles[1].x, circles[1].y) - orientation, |
| 312 | + c = Math.cos(rotation), |
| 313 | + s = Math.sin(rotation), x, y; |
| 314 | + |
| 315 | + for (i = 0; i < circles.length; ++i) { |
| 316 | + x = circles[i].x; |
| 317 | + y = circles[i].y; |
| 318 | + circles[i].x = c * x - s * y; |
| 319 | + circles[i].y = s * x + c * y; |
| 320 | + } |
| 321 | + } |
| 322 | + |
| 323 | + // mirror solution if third solution is above plane specified by |
| 324 | + // first two circles |
| 325 | + if (circles.length > 2) { |
| 326 | + var angle = Math.atan2(circles[2].x, circles[2].y) - orientation; |
| 327 | + while (angle < 0) { angle += 2* Math.PI; } |
| 328 | + while (angle > 2*Math.PI) { angle -= 2* Math.PI; } |
| 329 | + if (angle > Math.PI) { |
| 330 | + var slope = circles[1].y / (1e-10 + circles[1].x); |
| 331 | + for (i = 0; i < circles.length; ++i) { |
| 332 | + var d = (circles[i].x + slope * circles[i].y) / (1 + slope*slope); |
| 333 | + circles[i].x = 2 * d - circles[i].x; |
| 334 | + circles[i].y = 2 * d * slope - circles[i].y; |
| 335 | + } |
| 336 | + } |
| 337 | + } |
| 338 | + } |
| 339 | + |
| 340 | + venn.disjointCluster = function(circles) { |
| 341 | + // union-find clustering to get disjoint sets |
| 342 | + circles.map(function(circle) { circle.parent = circle; }); |
| 343 | + |
| 344 | + // path compression step in union find |
| 345 | + function find(circle) { |
| 346 | + if (circle.parent !== circle) { |
| 347 | + circle.parent = find(circle.parent); |
| 348 | + } |
| 349 | + return circle.parent; |
| 350 | + } |
| 351 | + |
| 352 | + function union(x, y) { |
| 353 | + var xRoot = find(x), yRoot = find(y); |
| 354 | + xRoot.parent = yRoot; |
| 355 | + } |
| 356 | + |
| 357 | + // get the union of all overlapping sets |
| 358 | + for (var i = 0; i < circles.length; ++i) { |
| 359 | + for (var j = i + 1; j < circles.length; ++j) { |
| 360 | + var maxDistance = circles[i].radius + circles[j].radius; |
| 361 | + if (venn.distance(circles[i], circles[j]) + 1e-10 < maxDistance) { |
| 362 | + union(circles[j], circles[i]); |
| 363 | + } |
| 364 | + } |
| 365 | + } |
| 366 | + |
| 367 | + // find all the disjoint clusters and group them together |
| 368 | + var disjointClusters = {}, setid; |
| 369 | + for (i = 0; i < circles.length; ++i) { |
| 370 | + setid = find(circles[i]).parent.setid; |
| 371 | + if (!(setid in disjointClusters)) { |
| 372 | + disjointClusters[setid] = []; |
| 373 | + } |
| 374 | + disjointClusters[setid].push(circles[i]); |
| 375 | + } |
| 376 | + |
| 377 | + // cleanup bookkeeping |
| 378 | + circles.map(function(circle) { delete circle.parent; }); |
| 379 | + |
| 380 | + // return in more usable form |
| 381 | + var ret = []; |
| 382 | + for (setid in disjointClusters) { |
| 383 | + if (disjointClusters.hasOwnProperty(setid)) { |
| 384 | + ret.push(disjointClusters[setid]); |
| 385 | + } |
| 386 | + } |
| 387 | + return ret; |
| 388 | + }; |
| 389 | + |
| 390 | + function getBoundingBox(circles) { |
| 391 | + var minMax = function(d) { |
| 392 | + var hi = Math.max.apply(null, circles.map( |
| 393 | + function(c) { return c[d] + c.radius; } )), |
| 394 | + lo = Math.min.apply(null, circles.map( |
| 395 | + function(c) { return c[d] - c.radius;} )); |
| 396 | + return {max:hi, min:lo}; |
| 397 | + }; |
| 398 | + |
| 399 | + return {xRange: minMax('x'), yRange: minMax('y')}; |
| 400 | + } |
| 401 | + |
| 402 | + venn.normalizeSolution = function(solution, orientation) { |
| 403 | + orientation = orientation || Math.PI/2; |
| 404 | + |
| 405 | + // work with a list instead of a dictionary, and take a copy so we |
| 406 | + // don't mutate input |
| 407 | + var circles = [], i, setid; |
| 408 | + for (setid in solution) { |
| 409 | + if (solution.hasOwnProperty(setid)) { |
| 410 | + var previous = solution[setid]; |
| 411 | + circles.push({x: previous.x, |
| 412 | + y: previous.y, |
| 413 | + radius: previous.radius, |
| 414 | + setid: setid}); |
| 415 | + } |
| 416 | + } |
| 417 | + |
| 418 | + // get all the disjoint clusters |
| 419 | + var clusters = venn.disjointCluster(circles); |
| 420 | + |
| 421 | + // orientate all disjoint sets, get sizes |
| 422 | + for (i = 0; i < clusters.length; ++i) { |
| 423 | + orientateCircles(clusters[i], orientation); |
| 424 | + var bounds = getBoundingBox(clusters[i]); |
| 425 | + clusters[i].size = (bounds.xRange.max - bounds.xRange.min) * (bounds.yRange.max - bounds.yRange.min); |
| 426 | + clusters[i].bounds = bounds; |
| 427 | + } |
| 428 | + clusters.sort(function(a, b) { return b.size - a.size; }); |
| 429 | + |
| 430 | + // orientate the largest at 0,0, and get the bounds |
| 431 | + circles = clusters[0]; |
| 432 | + var returnBounds = circles.bounds; |
| 433 | + |
| 434 | + var spacing = (returnBounds.xRange.max - returnBounds.xRange.min)/50; |
| 435 | + |
| 436 | + function addCluster(cluster, right, bottom) { |
| 437 | + if (!cluster) return; |
| 438 | + |
| 439 | + var bounds = cluster.bounds, xOffset, yOffset, centreing; |
| 440 | + |
| 441 | + if (right) { |
| 442 | + xOffset = returnBounds.xRange.max - bounds.xRange.min + spacing; |
| 443 | + } else { |
| 444 | + xOffset = returnBounds.xRange.max - bounds.xRange.max - spacing; |
| 445 | + centreing = (bounds.xRange.max - bounds.xRange.min) / 2 - |
| 446 | + (returnBounds.xRange.max - returnBounds.xRange.min) / 2; |
| 447 | + if (centreing < 0) xOffset += centreing; |
| 448 | + } |
| 449 | + |
| 450 | + if (bottom) { |
| 451 | + yOffset = returnBounds.yRange.max - bounds.yRange.min + spacing; |
| 452 | + } else { |
| 453 | + yOffset = returnBounds.yRange.max - bounds.yRange.max - spacing; |
| 454 | + centreing = (bounds.yRange.max - bounds.yRange.min) / 2 - |
| 455 | + (returnBounds.yRange.max - returnBounds.yRange.min) / 2; |
| 456 | + if (centreing < 0) yOffset += centreing; |
| 457 | + } |
| 458 | + |
| 459 | + for (var j = 0; j < cluster.length; ++j) { |
| 460 | + cluster[j].x += xOffset; |
| 461 | + cluster[j].y += yOffset; |
| 462 | + circles.push(cluster[j]); |
| 463 | + } |
| 464 | + } |
| 465 | + |
| 466 | + var index = 1; |
| 467 | + while (index < clusters.length) { |
| 468 | + addCluster(clusters[index], true, false); |
| 469 | + addCluster(clusters[index+1], false, true); |
| 470 | + addCluster(clusters[index+2], true, true); |
| 471 | + index += 3; |
| 472 | + |
| 473 | + // have one cluster (in top left). lay out next three relative |
| 474 | + // to it in a grid |
| 475 | + returnBounds = getBoundingBox(circles); |
| 476 | + } |
| 477 | + |
| 478 | + // convert back to solution form |
| 479 | + var ret = {}; |
| 480 | + for (i = 0; i < circles.length; ++i) { |
| 481 | + ret[circles[i].setid] = circles[i]; |
| 482 | + } |
| 483 | + return ret; |
| 484 | + }; |
| 485 | + |
291 | 486 | /** Scales a solution from venn.venn or venn.greedyLayout such that it fits in
|
292 | 487 | a rectangle of width/height - with padding around the borders. also
|
293 | 488 | centers the diagram in the available space at the same time */
|
|
300 | 495 | }
|
301 | 496 | }
|
302 | 497 |
|
303 |
| - var minMax = function(d) { |
304 |
| - var hi = Math.max.apply(null, circles.map( |
305 |
| - function(c) { return c[d] + c.radius; } )), |
306 |
| - lo = Math.min.apply(null, circles.map( |
307 |
| - function(c) { return c[d] - c.radius;} )); |
308 |
| - return {max:hi, min:lo}; |
309 |
| - }; |
310 |
| - |
311 | 498 | width -= 2*padding;
|
312 | 499 | height -= 2*padding;
|
313 | 500 |
|
314 |
| - var xRange = minMax('x'), |
315 |
| - yRange = minMax('y'), |
| 501 | + var bounds = getBoundingBox(circles), |
| 502 | + xRange = bounds.xRange, |
| 503 | + yRange = bounds.yRange, |
316 | 504 | xScaling = width / (xRange.max - xRange.min),
|
317 | 505 | yScaling = height / (yRange.max - yRange.min),
|
318 | 506 | scaling = Math.min(yScaling, xScaling),
|
|
0 commit comments