Skip to content

Commit 0f9b02c

Browse files
committed
venn diagram normalization code
Different algorithms can produce different looking output that is equally correct. This code attempts to remove the major differences by axis aligning the major sets, taking the mirror image if needed, and re-arranging all the disjoint clusters so that they are close to one another (in a rough grid pattern).
1 parent 3d281c9 commit 0f9b02c

File tree

7 files changed

+791
-25
lines changed

7 files changed

+791
-25
lines changed

src/diagram.js

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,19 @@
55
height = 350,
66
padding = 15,
77
duration = 1000,
8+
orientation = Math.PI / 2,
9+
normalize = true,
810
fontSize = null,
911
colours = d3.scale.category10(),
1012
layoutFunction = venn.venn;
1113

1214
function chart(selection) {
1315
selection.each(function(data) {
14-
// calculate circle position, scale to fit
15-
var circles = venn.scaleSolution(layoutFunction(data), width, height, padding);
16+
var solution = layoutFunction(data);
17+
if (normalize) {
18+
solution = venn.normalizeSolution(solution, orientation);
19+
}
20+
var circles = venn.scaleSolution(solution, width, height, padding);
1621
var textCentres = computeTextCentres(circles, data);
1722

1823
// draw out a svg
@@ -167,6 +172,17 @@
167172
return chart;
168173
};
169174

175+
chart.normalize = function(_) {
176+
if (!arguments.length) return normalize;
177+
normalize = _;
178+
return chart;
179+
};
180+
chart.orientation = function(_) {
181+
if (!arguments.length) return orientation;
182+
orientation = _;
183+
return chart;
184+
};
185+
170186
return chart;
171187
};
172188
// sometimes text doesn't fit inside the circle, if thats the case lets wrap

src/layout.js

Lines changed: 198 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,201 @@
288288
return output;
289289
};
290290

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+
291486
/** Scales a solution from venn.venn or venn.greedyLayout such that it fits in
292487
a rectangle of width/height - with padding around the borders. also
293488
centers the diagram in the available space at the same time */
@@ -300,19 +495,12 @@
300495
}
301496
}
302497

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-
311498
width -= 2*padding;
312499
height -= 2*padding;
313500

314-
var xRange = minMax('x'),
315-
yRange = minMax('y'),
501+
var bounds = getBoundingBox(circles),
502+
xRange = bounds.xRange,
503+
yRange = bounds.yRange,
316504
xScaling = width / (xRange.max - xRange.min),
317505
yScaling = height / (yRange.max - yRange.min),
318506
scaling = Math.min(yScaling, xScaling),

0 commit comments

Comments
 (0)