diff --git a/client/gulpfile.js b/client/gulpfile.js index 6624ea5aba6d..f0393e438b1e 100644 --- a/client/gulpfile.js +++ b/client/gulpfile.js @@ -34,9 +34,8 @@ const STATIC_PLUGIN_BUILD_IDS = [ "scatterplot", "tiffviewer", "ts_visjs", - "venn", ]; -const INSTALL_PLUGIN_BUILD_IDS = ["cytoscape", "ngl", "msa", "openlayers", "vizarr"]; // todo: derive from XML +const INSTALL_PLUGIN_BUILD_IDS = ["cytoscape", "ngl", "msa", "openlayers", "venn", "vizarr"]; // todo: derive from XML const DIST_PLUGIN_BUILD_IDS = ["new_user"]; const PLUGIN_BUILD_IDS = Array.prototype.concat(DIST_PLUGIN_BUILD_IDS, STATIC_PLUGIN_BUILD_IDS); diff --git a/config/plugins/visualizations/venn/config/venn.xml b/config/plugins/visualizations/venn/config/venn.xml index a46fa31a9aa0..25e140440fa3 100644 --- a/config/plugins/visualizations/venn/config/venn.xml +++ b/config/plugins/visualizations/venn/config/venn.xml @@ -13,8 +13,11 @@ dataset_id - - + + + + + key @@ -28,5 +31,5 @@ data_column true - + \ No newline at end of file diff --git a/config/plugins/visualizations/venn/package.json b/config/plugins/visualizations/venn/package.json deleted file mode 100644 index 2ed9970cabb5..000000000000 --- a/config/plugins/visualizations/venn/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "visualization", - "version": "0.2.0", - "keywords": [ - "galaxy", - "visualization" - ], - "license": "AFL-3.0", - "dependencies": { - "@galaxyproject/charts": "^0.1.0", - "babel-preset-env": "^1.6.1", - "backbone": "^1.3.3", - "bootstrap": "^3.3.7", - "d3": "^3.4.5", - "jquery": "^3.1.1", - "underscore": "^1.13.1" - }, - "scripts": { - "build": "parcel build src/script.js --dist-dir static" - }, - "devDependencies": { - "parcel": "2.11.0" - } -} diff --git a/config/plugins/visualizations/venn/src/script.js b/config/plugins/visualizations/venn/src/script.js deleted file mode 100644 index bd24659752c1..000000000000 --- a/config/plugins/visualizations/venn/src/script.js +++ /dev/null @@ -1,93 +0,0 @@ -import * as Venn from "./venn"; -import * as d3 from "d3"; -import _ from "underscore"; -window.d3 = d3; -import { request as requestDatasets } from "@galaxyproject/charts/lib/utilities/datasets"; - -function _combinations( current, remaining, results ) { - _.each( remaining, function( value, index ) { - var new_current = current.slice(); - var new_remaining = remaining.slice(); - new_remaining.splice( 0, index + 1 ); - new_current.push( value ); - results.push( new_current ); - _combinations( new_current, new_remaining, results ); - }); -}; - -window.bundleEntries = window.bundleEntries || {}; -window.bundleEntries.load = function (options) { - requestDatasets({ - root : options.root, - chart : options.chart, - dataset_id : options.chart.get( 'dataset_id' ), - dataset_groups : options.chart.groups, - success : function( result ) { - var group_keys = []; - var group_values = []; - var all_values = {}; - var set_size = {}; - var group_ids = []; - _.each( result, function( group, i ) { - var group_index = {}; - _.each( group.values, function( d ) { - all_values[ d.observation ] = group_index[ d.observation ] = true; - }); - group_keys.push( group.key ); - group_values.push( group_index ); - group_ids.push( i ); - }); - var combos = []; - _combinations( [], group_ids, combos ); - var sets = []; - _.each( combos, function( c ) { - var size = 0; - for ( var value in all_values ) { - var found = 0; - _.each( c, function( group_id ) { - if ( group_values[ group_id ][ value ] ) { - found++; - } - }); - if ( found == c.length ) { - size++; - } - } - if ( size > 0 ) { - var set_labels = []; - _.each( c, function( id ) { - set_labels.push( group_keys[ id ]); - }); - sets.push( { sets: set_labels, size: size } ); - } - }); - $('#' + options.target).append("") - .attr("id", "svg-" + options.target) - .css("height", "100%") - .css("width", "100%"); - var svg = d3.select( '#svg-' + options.target ).datum( sets ).call( Venn.VennDiagram() ); - var tooltip = null; - svg.selectAll( 'g' ) - .on( 'mouseover', function( d, i ) { - Venn.sortAreas( svg, d ); - tooltip = d3.select( 'body' ).append( 'div' ).attr( 'class', 'venntooltip' ); - tooltip.transition().duration( 400 ).style( 'opacity', .9 ); - tooltip.text(d.size ); - var selection = d3.select( this ).transition( 'tooltip' ).duration( 400 ); - selection.select( 'path' ) - .style( 'stroke-width', 3 ) - .style( 'fill-opacity', d.sets.length == 1 ? .4 : .1 ) - .style( 'stroke-opacity', 1 ); - }) - .on( 'mousemove', function() { - tooltip.style( 'left', ( d3.event.pageX ) + 'px') - .style( 'top', ( d3.event.pageY - 28 ) + 'px'); - }) - .on( 'mouseout', function( d, i ) { - tooltip.remove(); - }); - options.chart.state( 'ok', 'Venn diagram drawn.' ); - options.process.resolve(); - } - }); -} diff --git a/config/plugins/visualizations/venn/src/venn.js b/config/plugins/visualizations/venn/src/venn.js deleted file mode 100644 index 58a29e784b3f..000000000000 --- a/config/plugins/visualizations/venn/src/venn.js +++ /dev/null @@ -1,1789 +0,0 @@ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : - typeof define === 'function' && define.amd ? define(['exports'], factory) : - factory((global.venn = {}),global.d3,global.d3); -}(this, function (exports) { 'use strict'; - - /** finds the zeros of a function, given two starting points (which must - * have opposite signs */ - function bisect(f, a, b, parameters) { - parameters = parameters || {}; - var maxIterations = parameters.maxIterations || 100, - tolerance = parameters.tolerance || 1e-10, - fA = f(a), - fB = f(b), - delta = b - a; - - if (fA * fB > 0) { - throw "Initial bisect points must have opposite signs"; - } - - if (fA === 0) return a; - if (fB === 0) return b; - - for (var i = 0; i < maxIterations; ++i) { - delta /= 2; - var mid = a + delta, - fMid = f(mid); - - if (fMid * fA >= 0) { - a = mid; - } - - if ((Math.abs(delta) < tolerance) || (fMid === 0)) { - return mid; - } - } - return a + delta; - } - - // need some basic operations on vectors, rather than adding a dependency, - // just define here - function zeros(x) { var r = new Array(x); for (var i = 0; i < x; ++i) { r[i] = 0; } return r; } - function zerosM(x,y) { return zeros(x).map(function() { return zeros(y); }); } - - function dot(a, b) { - var ret = 0; - for (var i = 0; i < a.length; ++i) { - ret += a[i] * b[i]; - } - return ret; - } - - function norm2(a) { - return Math.sqrt(dot(a, a)); - } - - function multiplyBy(a, c) { - for (var i = 0; i < a.length; ++i) { - a[i] *= c; - } - } - - function weightedSum(ret, w1, v1, w2, v2) { - for (var j = 0; j < ret.length; ++j) { - ret[j] = w1 * v1[j] + w2 * v2[j]; - } - } - - /** minimizes a function using the downhill simplex method */ - function fmin(f, x0, parameters) { - parameters = parameters || {}; - - var maxIterations = parameters.maxIterations || x0.length * 200, - nonZeroDelta = parameters.nonZeroDelta || 1.1, - zeroDelta = parameters.zeroDelta || 0.001, - minErrorDelta = parameters.minErrorDelta || 1e-6, - minTolerance = parameters.minErrorDelta || 1e-5, - rho = parameters.rho || 1, - chi = parameters.chi || 2, - psi = parameters.psi || -0.5, - sigma = parameters.sigma || 0.5, - callback = parameters.callback, - maxDiff, - temp; - - // initialize simplex. - var N = x0.length, - simplex = new Array(N + 1); - simplex[0] = x0; - simplex[0].fx = f(x0); - for (var i = 0; i < N; ++i) { - var point = x0.slice(); - point[i] = point[i] ? point[i] * nonZeroDelta : zeroDelta; - simplex[i+1] = point; - simplex[i+1].fx = f(point); - } - - var sortOrder = function(a, b) { return a.fx - b.fx; }; - - var centroid = x0.slice(), - reflected = x0.slice(), - contracted = x0.slice(), - expanded = x0.slice(); - - for (var iteration = 0; iteration < maxIterations; ++iteration) { - simplex.sort(sortOrder); - if (callback) { - callback(simplex); - } - - maxDiff = 0; - for (i = 0; i < N; ++i) { - maxDiff = Math.max(maxDiff, Math.abs(simplex[0][i] - simplex[1][i])); - } - - if ((Math.abs(simplex[0].fx - simplex[N].fx) < minErrorDelta) && - (maxDiff < minTolerance)) { - break; - } - - // compute the centroid of all but the worst point in the simplex - for (i = 0; i < N; ++i) { - centroid[i] = 0; - for (var j = 0; j < N; ++j) { - centroid[i] += simplex[j][i]; - } - centroid[i] /= N; - } - - // reflect the worst point past the centroid and compute loss at reflected - // point - var worst = simplex[N]; - weightedSum(reflected, 1+rho, centroid, -rho, worst); - reflected.fx = f(reflected); - - // if the reflected point is the best seen, then possibly expand - if (reflected.fx <= simplex[0].fx) { - weightedSum(expanded, 1+chi, centroid, -chi, worst); - expanded.fx = f(expanded); - if (expanded.fx < reflected.fx) { - temp = simplex[N]; - simplex[N] = expanded; - expanded = temp; - } else { - temp = simplex[N]; - simplex[N] = reflected; - reflected = temp; - } - } - - // if the reflected point is worse than the second worst, we need to - // contract - else if (reflected.fx >= simplex[N-1].fx) { - var shouldReduce = false; - - if (reflected.fx > worst.fx) { - // do an inside contraction - weightedSum(contracted, 1+psi, centroid, -psi, worst); - contracted.fx = f(contracted); - if (contracted.fx < worst.fx) { - temp = simplex[N]; - simplex[N] = contracted; - contracted = temp; - } else { - shouldReduce = true; - } - } else { - // do an outside contraction - weightedSum(contracted, 1-psi * rho, centroid, psi*rho, worst); - contracted.fx = f(contracted); - if (contracted.fx <= reflected.fx) { - temp = simplex[N]; - simplex[N] = contracted; - contracted = temp; - } else { - shouldReduce = true; - } - } - - if (shouldReduce) { - // do reduction. doesn't actually happen that often - for (i = 1; i < simplex.length; ++i) { - weightedSum(simplex[i], 1 - sigma, simplex[0], sigma, simplex[i]); - simplex[i].fx = f(simplex[i]); - } - } - } else { - temp = simplex[N]; - simplex[N] = reflected; - reflected = temp; - } - - } - - simplex.sort(sortOrder); - return {f : simplex[0].fx, - solution : simplex[0]}; - } - - function minimizeConjugateGradient(f, initial, params) { - // allocate all memory up front here, keep out of the loop for perfomance - // reasons - var current = {x: initial.slice(), fx: 0, fxprime: initial.slice()}, - next = {x: initial.slice(), fx: 0, fxprime: initial.slice()}, - yk = initial.slice(), - pk, temp, - a = 1, - maxIterations; - - params = params || {}; - maxIterations = params.maxIterations || initial.length * 5; - - current.fx = f(current.x, current.fxprime); - pk = current.fxprime.slice(); - multiplyBy(pk, -1); - - for (var i = 0; i < maxIterations; ++i) { - if (params.history) { - params.history.push({x: current.x.slice(), - fx: current.fx, - fxprime: current.fxprime.slice()}); - } - - a = wolfeLineSearch(f, pk, current, next, a); - if (!a) { - // faiiled to find point that satifies wolfe conditions. - // reset direction for next iteration - for (var j = 0; j < pk.length; ++j) { - pk[j] = -1 * current.fxprime[j]; - } - } else { - // update direction using Polak–Ribiere CG method - weightedSum(yk, 1, next.fxprime, -1, current.fxprime); - - var delta_k = dot(current.fxprime, current.fxprime), - beta_k = Math.max(0, dot(yk, next.fxprime) / delta_k); - - weightedSum(pk, beta_k, pk, -1, next.fxprime); - - temp = current; - current = next; - next = temp; - } - - if (norm2(current.fxprime) <= 1e-5) { - break; - } - } - - if (params.history) { - params.history.push({x: current.x.slice(), - fx: current.fx, - fxprime: current.fxprime.slice()}); - } - - return current; - } - - var c1 = 1e-6; - var c2 = 0.1; - /// searches along line 'pk' for a point that satifies the wolfe conditions - /// See 'Numerical Optimization' by Nocedal and Wright p59-60 - function wolfeLineSearch(f, pk, current, next, a) { - var phi0 = current.fx, phiPrime0 = dot(current.fxprime, pk), - phi = phi0, phi_old = phi0, - phiPrime = phiPrime0, - a0 = 0; - - a = a || 1; - - function zoom(a_lo, a_high, phi_lo) { - for (var iteration = 0; iteration < 16; ++iteration) { - a = (a_lo + a_high)/2; - weightedSum(next.x, 1.0, current.x, a, pk); - phi = next.fx = f(next.x, next.fxprime); - phiPrime = dot(next.fxprime, pk); - - if ((phi > (phi0 + c1 * a * phiPrime0)) || - (phi >= phi_lo)) { - a_high = a; - - } else { - if (Math.abs(phiPrime) <= -c2 * phiPrime0) { - return a; - } - - if (phiPrime * (a_high - a_lo) >=0) { - a_high = a_lo; - } - - a_lo = a; - phi_lo = phi; - } - } - - return 0; - } - - for (var iteration = 0; iteration < 10; ++iteration) { - weightedSum(next.x, 1.0, current.x, a, pk); - phi = next.fx = f(next.x, next.fxprime); - phiPrime = dot(next.fxprime, pk); - if ((phi > (phi0 + c1 * a * phiPrime0)) || - (iteration && (phi >= phi_old))) { - return zoom(a0, a, phi_old); - } - - if (Math.abs(phiPrime) <= -c2 * phiPrime0) { - return a; - } - - if (phiPrime >= 0 ) { - return zoom(a, a0, phi); - } - - phi_old = phi; - a0 = a; - a *= 2; - } - - return 0; - } - - var SMALL = 1e-10; - - /** Returns the intersection area of a bunch of circles (where each circle - is an object having an x,y and radius property) */ - function intersectionArea(circles, stats) { - // get all the intersection points of the circles - var intersectionPoints = getIntersectionPoints(circles); - - // filter out points that aren't included in all the circles - var innerPoints = intersectionPoints.filter(function (p) { - return containedInCircles(p, circles); - }); - - var arcArea = 0, polygonArea = 0, arcs = [], i; - - // if we have intersection points that are within all the circles, - // then figure out the area contained by them - if (innerPoints.length > 1) { - // sort the points by angle from the center of the polygon, which lets - // us just iterate over points to get the edges - var center = getCenter(innerPoints); - for (i = 0; i < innerPoints.length; ++i ) { - var p = innerPoints[i]; - p.angle = Math.atan2(p.x - center.x, p.y - center.y); - } - innerPoints.sort(function(a,b) { return b.angle - a.angle;}); - - // iterate over all points, get arc between the points - // and update the areas - var p2 = innerPoints[innerPoints.length - 1]; - for (i = 0; i < innerPoints.length; ++i) { - var p1 = innerPoints[i]; - - // polygon area updates easily ... - polygonArea += (p2.x + p1.x) * (p1.y - p2.y); - - // updating the arc area is a little more involved - var midPoint = {x : (p1.x + p2.x) / 2, - y : (p1.y + p2.y) / 2}, - arc = null; - - for (var j = 0; j < p1.parentIndex.length; ++j) { - if (p2.parentIndex.indexOf(p1.parentIndex[j]) > -1) { - // figure out the angle halfway between the two points - // on the current circle - var circle = circles[p1.parentIndex[j]], - a1 = Math.atan2(p1.x - circle.x, p1.y - circle.y), - a2 = Math.atan2(p2.x - circle.x, p2.y - circle.y); - - var angleDiff = (a2 - a1); - if (angleDiff < 0) { - angleDiff += 2*Math.PI; - } - - // and use that angle to figure out the width of the - // arc - var a = a2 - angleDiff/2, - width = distance(midPoint, { - x : circle.x + circle.radius * Math.sin(a), - y : circle.y + circle.radius * Math.cos(a) - }); - - // pick the circle whose arc has the smallest width - if ((arc === null) || (arc.width > width)) { - arc = { circle : circle, - width : width, - p1 : p1, - p2 : p2}; - } - } - } - - if (arc !== null) { - arcs.push(arc); - arcArea += circleArea(arc.circle.radius, arc.width); - p2 = p1; - } - } - } else { - // no intersection points, is either disjoint - or is completely - // overlapped. figure out which by examining the smallest circle - var smallest = circles[0]; - for (i = 1; i < circles.length; ++i) { - if (circles[i].radius < smallest.radius) { - smallest = circles[i]; - } - } - - // make sure the smallest circle is completely contained in all - // the other circles - var disjoint = false; - for (i = 0; i < circles.length; ++i) { - if (distance(circles[i], smallest) > Math.abs(smallest.radius - circles[i].radius)) { - disjoint = true; - break; - } - } - - if (disjoint) { - arcArea = polygonArea = 0; - - } else { - arcArea = smallest.radius * smallest.radius * Math.PI; - arcs.push({circle : smallest, - p1: { x: smallest.x, y : smallest.y + smallest.radius}, - p2: { x: smallest.x - SMALL, y : smallest.y + smallest.radius}, - width : smallest.radius * 2 }); - } - } - - polygonArea /= 2; - if (stats) { - stats.area = arcArea + polygonArea; - stats.arcArea = arcArea; - stats.polygonArea = polygonArea; - stats.arcs = arcs; - stats.innerPoints = innerPoints; - stats.intersectionPoints = intersectionPoints; - } - - return arcArea + polygonArea; - } - - /** returns whether a point is contained by all of a list of circles */ - function containedInCircles(point, circles) { - for (var i = 0; i < circles.length; ++i) { - if (distance(point, circles[i]) > circles[i].radius + SMALL) { - return false; - } - } - return true; - } - - /** Gets all intersection points between a bunch of circles */ - function getIntersectionPoints(circles) { - var ret = []; - for (var i = 0; i < circles.length; ++i) { - for (var j = i + 1; j < circles.length; ++j) { - var intersect = circleCircleIntersection(circles[i], - circles[j]); - for (var k = 0; k < intersect.length; ++k) { - var p = intersect[k]; - p.parentIndex = [i,j]; - ret.push(p); - } - } - } - return ret; - } - - function circleIntegral(r, x) { - var y = Math.sqrt(r * r - x * x); - return x * y + r * r * Math.atan2(x, y); - } - - /** Returns the area of a circle of radius r - up to width */ - function circleArea(r, width) { - return circleIntegral(r, width - r) - circleIntegral(r, -r); - } - - /** euclidean distance between two points */ - function distance(p1, p2) { - return Math.sqrt((p1.x - p2.x) * (p1.x - p2.x) + - (p1.y - p2.y) * (p1.y - p2.y)); - } - - - /** Returns the overlap area of two circles of radius r1 and r2 - that - have their centers separated by distance d. Simpler faster - circle intersection for only two circles */ - function circleOverlap(r1, r2, d) { - // no overlap - if (d >= r1 + r2) { - return 0; - } - - // completely overlapped - if (d <= Math.abs(r1 - r2)) { - return Math.PI * Math.min(r1, r2) * Math.min(r1, r2); - } - - var w1 = r1 - (d * d - r2 * r2 + r1 * r1) / (2 * d), - w2 = r2 - (d * d - r1 * r1 + r2 * r2) / (2 * d); - return circleArea(r1, w1) + circleArea(r2, w2); - } - - /** Given two circles (containing a x/y/radius attributes), - returns the intersecting points if possible. - note: doesn't handle cases where there are infinitely many - intersection points (circles are equivalent):, or only one intersection point*/ - function circleCircleIntersection(p1, p2) { - var d = distance(p1, p2), - r1 = p1.radius, - r2 = p2.radius; - - // if to far away, or self contained - can't be done - if ((d >= (r1 + r2)) || (d <= Math.abs(r1 - r2))) { - return []; - } - - var a = (r1 * r1 - r2 * r2 + d * d) / (2 * d), - h = Math.sqrt(r1 * r1 - a * a), - x0 = p1.x + a * (p2.x - p1.x) / d, - y0 = p1.y + a * (p2.y - p1.y) / d, - rx = -(p2.y - p1.y) * (h / d), - ry = -(p2.x - p1.x) * (h / d); - - return [{x: x0 + rx, y : y0 - ry }, - {x: x0 - rx, y : y0 + ry }]; - } - - /** Returns the center of a bunch of points */ - function getCenter(points) { - var center = {x: 0, y: 0}; - for (var i =0; i < points.length; ++i ) { - center.x += points[i].x; - center.y += points[i].y; - } - center.x /= points.length; - center.y /= points.length; - return center; - } - - /** given a list of set objects, and their corresponding overlaps. - updates the (x, y, radius) attribute on each set such that their positions - roughly correspond to the desired overlaps */ - function venn(areas, parameters) { - parameters = parameters || {}; - parameters.maxIterations = parameters.maxIterations || 500; - var initialLayout = parameters.initialLayout || bestInitialLayout; - - // add in missing pairwise areas as having 0 size - areas = addMissingAreas(areas); - - // initial layout is done greedily - var circles = initialLayout(areas); - - // transform x/y coordinates to a vector to optimize - var initial = [], setids = [], setid; - for (setid in circles) { - if (circles.hasOwnProperty(setid)) { - initial.push(circles[setid].x); - initial.push(circles[setid].y); - setids.push(setid); - } - } - - // optimize initial layout from our loss function - var totalFunctionCalls = 0; - var solution = fmin( - function(values) { - totalFunctionCalls += 1; - var current = {}; - for (var i = 0; i < setids.length; ++i) { - var setid = setids[i]; - current[setid] = {x: values[2 * i], - y: values[2 * i + 1], - radius : circles[setid].radius, - // size : circles[setid].size - }; - } - return lossFunction(current, areas); - }, - initial, - parameters); - - // transform solution vector back to x/y points - var positions = solution.solution; - for (var i = 0; i < setids.length; ++i) { - setid = setids[i]; - circles[setid].x = positions[2 * i]; - circles[setid].y = positions[2 * i + 1]; - } - - return circles; - } - - var SMALL$1 = 1e-10; - - /** Returns the distance necessary for two circles of radius r1 + r2 to - have the overlap area 'overlap' */ - function distanceFromIntersectArea(r1, r2, overlap) { - // handle complete overlapped circles - if (Math.min(r1, r2) * Math.min(r1,r2) * Math.PI <= overlap + SMALL$1) { - return Math.abs(r1 - r2); - } - - return bisect(function(distance) { - return circleOverlap(r1, r2, distance) - overlap; - }, 0, r1 + r2); - } - - /** Missing pair-wise intersection area data can cause problems: - treating as an unknown means that sets will be laid out overlapping, - which isn't what people expect. To reflect that we want disjoint sets - here, set the overlap to 0 for all missing pairwise set intersections */ - function addMissingAreas(areas) { - areas = areas.slice(); - - // two circle intersections that aren't defined - var ids = [], pairs = {}, i, j, a, b; - for (i = 0; i < areas.length; ++i) { - var area = areas[i]; - if (area.sets.length == 1) { - ids.push(area.sets[0]); - } else if (area.sets.length == 2) { - a = area.sets[0]; - b = area.sets[1]; - pairs[[a, b]] = true; - pairs[[b, a]] = true; - } - } - ids.sort(function(a, b) { return a > b; }); - - for (i = 0; i < ids.length; ++i) { - a = ids[i]; - for (j = i + 1; j < ids.length; ++j) { - b = ids[j]; - if (!([a, b] in pairs)) { - areas.push({'sets': [a, b], - 'size': 0}); - } - } - } - return areas; - } - - /// Returns two matrices, one of the euclidean distances between the sets - /// and the other indicating if there are subset or disjoint set relationships - function getDistanceMatrices(areas, sets, setids) { - // initialize an empty distance matrix between all the points - var distances = zerosM(sets.length, sets.length), - constraints = zerosM(sets.length, sets.length); - - // compute required distances between all the sets such that - // the areas match - areas.filter(function(x) { return x.sets.length == 2; }) - .map(function(current) { - var left = setids[current.sets[0]], - right = setids[current.sets[1]], - r1 = Math.sqrt(sets[left].size / Math.PI), - r2 = Math.sqrt(sets[right].size / Math.PI), - distance = distanceFromIntersectArea(r1, r2, current.size); - - distances[left][right] = distances[right][left] = distance; - - // also update constraints to indicate if its a subset or disjoint - // relationship - var c = 0; - if (current.size + 1e-10 >= Math.min(sets[left].size, - sets[right].size)) { - c = 1; - } else if (current.size <= 1e-10) { - c = -1; - } - constraints[left][right] = constraints[right][left] = c; - }); - - return {distances: distances, constraints: constraints}; - } - - /// computes the gradient and loss simulatenously for our constrained MDS optimizer - function constrainedMDSGradient(x, fxprime, distances, constraints) { - var loss = 0, i; - for (i = 0; i < fxprime.length; ++i) { - fxprime[i] = 0; - } - - for (i = 0; i < distances.length; ++i) { - var xi = x[2 * i], yi = x[2 * i + 1]; - for (var j = i + 1; j < distances.length; ++j) { - var xj = x[2 * j], yj = x[2 * j + 1], - dij = distances[i][j], - constraint = constraints[i][j]; - - var squaredDistance = (xj - xi) * (xj - xi) + (yj - yi) * (yj - yi), - distance = Math.sqrt(squaredDistance), - delta = squaredDistance - dij * dij; - - if (((constraint > 0) && (distance <= dij)) || - ((constraint < 0) && (distance >= dij))) { - continue; - } - - loss += 2 * delta * delta; - - fxprime[2*i] += 4 * delta * (xi - xj); - fxprime[2*i + 1] += 4 * delta * (yi - yj); - - fxprime[2*j] += 4 * delta * (xj - xi); - fxprime[2*j + 1] += 4 * delta * (yj - yi); - } - } - return loss; - } - - /// takes the best working variant of either constrained MDS or greedy - function bestInitialLayout(areas, params) { - var initial = greedyLayout(areas, params); - - // greedylayout is sufficient for all 2/3 circle cases. try out - // constrained MDS for higher order problems, take its output - // if it outperforms. (greedy is aesthetically better on 2/3 circles - // since it axis aligns) - if (areas.length >= 8) { - var constrained = constrainedMDSLayout(areas, params), - constrainedLoss = lossFunction(constrained, areas), - greedyLoss = lossFunction(initial, areas); - - if (constrainedLoss + 1e-8 < greedyLoss) { - initial = constrained; - } - } - return initial; - } - - /// use the constrained MDS variant to generate an initial layout - function constrainedMDSLayout(areas, params) { - params = params || {}; - var restarts = params.restarts || 10; - - // bidirectionally map sets to a rowid (so we can create a matrix) - var sets = [], setids = {}, i; - for (i = 0; i < areas.length; ++i ) { - var area = areas[i]; - if (area.sets.length == 1) { - setids[area.sets[0]] = sets.length; - sets.push(area); - } - } - - var matrices = getDistanceMatrices(areas, sets, setids), - distances = matrices.distances, - constraints = matrices.constraints; - - // keep distances bounded, things get messed up otherwise. - // TODO: proper preconditioner? - var norm = norm2(distances.map(norm2))/(distances.length); - distances = distances.map(function (row) { - return row.map(function (value) { return value / norm; });}); - - var obj = function(x, fxprime) { - return constrainedMDSGradient(x, fxprime, distances, constraints); - }; - - var best, current; - for (i = 0; i < restarts; ++i) { - var initial = zeros(distances.length*2).map(Math.random); - - current = minimizeConjugateGradient(obj, initial, params); - if (!best || (current.fx < best.fx)) { - best = current; - } - } - var positions = best.x; - - // translate rows back to (x,y,radius) coordinates - var circles = {}; - for (i = 0; i < sets.length; ++i) { - var set = sets[i]; - circles[set.sets[0]] = { - x: positions[2*i] * norm, - y: positions[2*i + 1] * norm, - radius: Math.sqrt(set.size / Math.PI) - }; - } - - if (params.history) { - for (i = 0; i < params.history.length; ++i) { - multiplyBy(params.history[i].x, norm); - } - } - return circles; - } - - /** Lays out a Venn diagram greedily, going from most overlapped sets to - least overlapped, attempting to position each new set such that the - overlapping areas to already positioned sets are basically right */ - function greedyLayout(areas) { - // define a circle for each set - var circles = {}, setOverlaps = {}, set; - for (var i = 0; i < areas.length; ++i) { - var area = areas[i]; - if (area.sets.length == 1) { - set = area.sets[0]; - circles[set] = {x: 1e10, y: 1e10, - rowid: circles.length, - size: area.size, - radius: Math.sqrt(area.size / Math.PI)}; - setOverlaps[set] = []; - } - } - areas = areas.filter(function(a) { return a.sets.length == 2; }); - - // map each set to a list of all the other sets that overlap it - for (i = 0; i < areas.length; ++i) { - var current = areas[i]; - var weight = current.hasOwnProperty('weight') ? current.weight : 1.0; - var left = current.sets[0], right = current.sets[1]; - - // completely overlapped circles shouldn't be positioned early here - if (current.size + SMALL$1 >= Math.min(circles[left].size, - circles[right].size)) { - weight = 0; - } - - setOverlaps[left].push ({set:right, size:current.size, weight:weight}); - setOverlaps[right].push({set:left, size:current.size, weight:weight}); - } - - // get list of most overlapped sets - var mostOverlapped = []; - for (set in setOverlaps) { - if (setOverlaps.hasOwnProperty(set)) { - var size = 0; - for (i = 0; i < setOverlaps[set].length; ++i) { - size += setOverlaps[set][i].size * setOverlaps[set][i].weight; - } - - mostOverlapped.push({set: set, size:size}); - } - } - - // sort by size desc - function sortOrder(a,b) { - return b.size - a.size; - } - mostOverlapped.sort(sortOrder); - - // keep track of what sets have been laid out - var positioned = {}; - function isPositioned(element) { - return element.set in positioned; - } - - // adds a point to the output - function positionSet(point, index) { - circles[index].x = point.x; - circles[index].y = point.y; - positioned[index] = true; - } - - // add most overlapped set at (0,0) - positionSet({x: 0, y: 0}, mostOverlapped[0].set); - - // get distances between all points. TODO, necessary? - // answer: probably not - // var distances = venn.getDistanceMatrices(circles, areas).distances; - for (i = 1; i < mostOverlapped.length; ++i) { - var setIndex = mostOverlapped[i].set, - overlap = setOverlaps[setIndex].filter(isPositioned); - set = circles[setIndex]; - overlap.sort(sortOrder); - - if (overlap.length === 0) { - // this shouldn't happen anymore with addMissingAreas - throw "ERROR: missing pairwise overlap information"; - } - - var points = []; - for (var j = 0; j < overlap.length; ++j) { - // get appropriate distance from most overlapped already added set - var p1 = circles[overlap[j].set], - d1 = distanceFromIntersectArea(set.radius, p1.radius, - overlap[j].size); - - // sample positions at 90 degrees for maximum aesthetics - points.push({x : p1.x + d1, y : p1.y}); - points.push({x : p1.x - d1, y : p1.y}); - points.push({y : p1.y + d1, x : p1.x}); - points.push({y : p1.y - d1, x : p1.x}); - - // if we have at least 2 overlaps, then figure out where the - // set should be positioned analytically and try those too - for (var k = j + 1; k < overlap.length; ++k) { - var p2 = circles[overlap[k].set], - d2 = distanceFromIntersectArea(set.radius, p2.radius, - overlap[k].size); - - var extraPoints = circleCircleIntersection( - { x: p1.x, y: p1.y, radius: d1}, - { x: p2.x, y: p2.y, radius: d2}); - - for (var l = 0; l < extraPoints.length; ++l) { - points.push(extraPoints[l]); - } - } - } - - // we have some candidate positions for the set, examine loss - // at each position to figure out where to put it at - var bestLoss = 1e50, bestPoint = points[0]; - for (j = 0; j < points.length; ++j) { - circles[setIndex].x = points[j].x; - circles[setIndex].y = points[j].y; - var loss = lossFunction(circles, areas); - if (loss < bestLoss) { - bestLoss = loss; - bestPoint = points[j]; - } - } - - positionSet(bestPoint, setIndex); - } - - return circles; - } - - /** Given a bunch of sets, and the desired overlaps between these sets - computes - the distance from the actual overlaps to the desired overlaps. Note that - this method ignores overlaps of more than 2 circles */ - function lossFunction(sets, overlaps) { - var output = 0; - - function getCircles(indices) { - return indices.map(function(i) { return sets[i]; }); - } - - for (var i = 0; i < overlaps.length; ++i) { - var area = overlaps[i], overlap; - if (area.sets.length == 1) { - continue; - } else if (area.sets.length == 2) { - var left = sets[area.sets[0]], - right = sets[area.sets[1]]; - overlap = circleOverlap(left.radius, right.radius, - distance(left, right)); - } else { - overlap = intersectionArea(getCircles(area.sets)); - } - - var weight = area.hasOwnProperty('weight') ? area.weight : 1.0; - output += weight * (overlap - area.size) * (overlap - area.size); - } - - return output; - } - - // orientates a bunch of circles to point in orientation - function orientateCircles(circles, orientation, orientationOrder) { - if (orientationOrder === null) { - circles.sort(function (a, b) { return b.radius - a.radius; }); - } else { - circles.sort(orientationOrder); - } - - var i; - // shift circles so largest circle is at (0, 0) - if (circles.length > 0) { - var largestX = circles[0].x, - largestY = circles[0].y; - - for (i = 0; i < circles.length; ++i) { - circles[i].x -= largestX; - circles[i].y -= largestY; - } - } - - // rotate circles so that second largest is at an angle of 'orientation' - // from largest - if (circles.length > 1) { - var rotation = Math.atan2(circles[1].x, circles[1].y) - orientation, - c = Math.cos(rotation), - s = Math.sin(rotation), x, y; - - for (i = 0; i < circles.length; ++i) { - x = circles[i].x; - y = circles[i].y; - circles[i].x = c * x - s * y; - circles[i].y = s * x + c * y; - } - } - - // mirror solution if third solution is above plane specified by - // first two circles - if (circles.length > 2) { - var angle = Math.atan2(circles[2].x, circles[2].y) - orientation; - while (angle < 0) { angle += 2* Math.PI; } - while (angle > 2*Math.PI) { angle -= 2* Math.PI; } - if (angle > Math.PI) { - var slope = circles[1].y / (1e-10 + circles[1].x); - for (i = 0; i < circles.length; ++i) { - var d = (circles[i].x + slope * circles[i].y) / (1 + slope*slope); - circles[i].x = 2 * d - circles[i].x; - circles[i].y = 2 * d * slope - circles[i].y; - } - } - } - } - - function disjointCluster(circles) { - // union-find clustering to get disjoint sets - circles.map(function(circle) { circle.parent = circle; }); - - // path compression step in union find - function find(circle) { - if (circle.parent !== circle) { - circle.parent = find(circle.parent); - } - return circle.parent; - } - - function union(x, y) { - var xRoot = find(x), yRoot = find(y); - xRoot.parent = yRoot; - } - - // get the union of all overlapping sets - for (var i = 0; i < circles.length; ++i) { - for (var j = i + 1; j < circles.length; ++j) { - var maxDistance = circles[i].radius + circles[j].radius; - if (distance(circles[i], circles[j]) + 1e-10 < maxDistance) { - union(circles[j], circles[i]); - } - } - } - - // find all the disjoint clusters and group them together - var disjointClusters = {}, setid; - for (i = 0; i < circles.length; ++i) { - setid = find(circles[i]).parent.setid; - if (!(setid in disjointClusters)) { - disjointClusters[setid] = []; - } - disjointClusters[setid].push(circles[i]); - } - - // cleanup bookkeeping - circles.map(function(circle) { delete circle.parent; }); - - // return in more usable form - var ret = []; - for (setid in disjointClusters) { - if (disjointClusters.hasOwnProperty(setid)) { - ret.push(disjointClusters[setid]); - } - } - return ret; - } - - function getBoundingBox(circles) { - var minMax = function(d) { - var hi = Math.max.apply(null, circles.map( - function(c) { return c[d] + c.radius; } )), - lo = Math.min.apply(null, circles.map( - function(c) { return c[d] - c.radius;} )); - return {max:hi, min:lo}; - }; - - return {xRange: minMax('x'), yRange: minMax('y')}; - } - - function normalizeSolution(solution, orientation, orientationOrder) { - if (orientation === null){ - orientation = Math.PI/2; - } - - // work with a list instead of a dictionary, and take a copy so we - // don't mutate input - var circles = [], i, setid; - for (setid in solution) { - if (solution.hasOwnProperty(setid)) { - var previous = solution[setid]; - circles.push({x: previous.x, - y: previous.y, - radius: previous.radius, - setid: setid}); - } - } - - // get all the disjoint clusters - var clusters = disjointCluster(circles); - - // orientate all disjoint sets, get sizes - for (i = 0; i < clusters.length; ++i) { - orientateCircles(clusters[i], orientation, orientationOrder); - var bounds = getBoundingBox(clusters[i]); - clusters[i].size = (bounds.xRange.max - bounds.xRange.min) * (bounds.yRange.max - bounds.yRange.min); - clusters[i].bounds = bounds; - } - clusters.sort(function(a, b) { return b.size - a.size; }); - - // orientate the largest at 0,0, and get the bounds - circles = clusters[0]; - var returnBounds = circles.bounds; - - var spacing = (returnBounds.xRange.max - returnBounds.xRange.min)/50; - - function addCluster(cluster, right, bottom) { - if (!cluster) return; - - var bounds = cluster.bounds, xOffset, yOffset, centreing; - - if (right) { - xOffset = returnBounds.xRange.max - bounds.xRange.min + spacing; - } else { - xOffset = returnBounds.xRange.max - bounds.xRange.max; - centreing = (bounds.xRange.max - bounds.xRange.min) / 2 - - (returnBounds.xRange.max - returnBounds.xRange.min) / 2; - if (centreing < 0) xOffset += centreing; - } - - if (bottom) { - yOffset = returnBounds.yRange.max - bounds.yRange.min + spacing; - } else { - yOffset = returnBounds.yRange.max - bounds.yRange.max; - centreing = (bounds.yRange.max - bounds.yRange.min) / 2 - - (returnBounds.yRange.max - returnBounds.yRange.min) / 2; - if (centreing < 0) yOffset += centreing; - } - - for (var j = 0; j < cluster.length; ++j) { - cluster[j].x += xOffset; - cluster[j].y += yOffset; - circles.push(cluster[j]); - } - } - - var index = 1; - while (index < clusters.length) { - addCluster(clusters[index], true, false); - addCluster(clusters[index+1], false, true); - addCluster(clusters[index+2], true, true); - index += 3; - - // have one cluster (in top left). lay out next three relative - // to it in a grid - returnBounds = getBoundingBox(circles); - } - - // convert back to solution form - var ret = {}; - for (i = 0; i < circles.length; ++i) { - ret[circles[i].setid] = circles[i]; - } - return ret; - } - - /** Scales a solution from venn.venn or venn.greedyLayout such that it fits in - a rectangle of width/height - with padding around the borders. also - centers the diagram in the available space at the same time */ - function scaleSolution(solution, width, height, padding) { - var circles = [], setids = []; - for (var setid in solution) { - if (solution.hasOwnProperty(setid)) { - setids.push(setid); - circles.push(solution[setid]); - } - } - - width -= 2*padding; - height -= 2*padding; - - var bounds = getBoundingBox(circles), - xRange = bounds.xRange, - yRange = bounds.yRange, - xScaling = width / (xRange.max - xRange.min), - yScaling = height / (yRange.max - yRange.min), - scaling = Math.min(yScaling, xScaling), - - // while we're at it, center the diagram too - xOffset = (width - (xRange.max - xRange.min) * scaling) / 2, - yOffset = (height - (yRange.max - yRange.min) * scaling) / 2; - - var scaled = {}; - for (var i = 0; i < circles.length; ++i) { - var circle = circles[i]; - scaled[setids[i]] = { - radius: scaling * circle.radius, - x: padding + xOffset + (circle.x - xRange.min) * scaling, - y: padding + yOffset + (circle.y - yRange.min) * scaling, - }; - } - - return scaled; - } - - /*global console:true*/ - - function VennDiagram() { - var width = 600, - height = 350, - padding = 15, - duration = 1000, - orientation = Math.PI / 2, - normalize = true, - wrap = true, - styled = true, - fontSize = null, - orientationOrder = null, - - // mimic the behaviour of d3.scale.category10 from the previous - // version of d3 - colourMap = {}, - - // so this is the same as d3.schemeCategory10, which is only defined in d3 4.0 - // since we can support older versions of d3 as long as we don't force this, - // I'm hackily redefining below. TODO: remove this and change to d3.schemeCategory10 - colourScheme = ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf"], - colourIndex = 0, - colours = function(key) { - if (key in colourMap) { - return colourMap[key]; - } - var ret = colourMap[key] = colourScheme[colourIndex]; - colourIndex += 1; - if (colourIndex >= colourScheme.length) { - colourIndex = 0; - } - return ret; - }, - layoutFunction = venn; - - function chart(selection) { - var data = selection.datum(); - var solution = layoutFunction(data); - if (normalize) { - solution = normalizeSolution(solution, - orientation, - orientationOrder); - } - var circles = scaleSolution(solution, width, height, padding); - var textCentres = computeTextCentres(circles, data); - - // create svg if not already existing - selection.selectAll("svg").data([circles]).enter().append("svg"); - - var svg = selection.select("svg") - .attr("width", width) - .attr("height", height); - - // to properly transition intersection areas, we need the - // previous circles locations. load from elements - var previous = {}, hasPrevious = false; - svg.selectAll("g path").each(function (d) { - var path = d3.select(this).attr("d"); - if ((d.sets.length == 1) && path) { - hasPrevious = true; - previous[d.sets[0]] = circleFromPath(path); - } - }); - - // interpolate intersection area paths between previous and - // current paths - var pathTween = function(d) { - return function(t) { - var c = d.sets.map(function(set) { - var start = previous[set], end = circles[set]; - if (!start) { - start = {x : width/2, y : height/2, radius : 1}; - } - if (!end) { - end = {x : width/2, y : height/2, radius : 1}; - } - return {'x' : start.x * (1 - t) + end.x * t, - 'y' : start.y * (1 - t) + end.y * t, - 'radius' : start.radius * (1 - t) + end.radius * t}; - }); - return intersectionAreaPath(c); - }; - }; - - // update data, joining on the set ids - var nodes = svg.selectAll("g") - .data(data, function(d) { return d.sets; }); - - // create new nodes - var enter = nodes.enter() - .append('g') - .attr("class", function(d) { - return "venn-area venn-" + - (d.sets.length == 1 ? "circle" : "intersection"); - }) - .attr("data-venn-sets", function(d) { - return d.sets.join("_"); - }); - - var enterPath = enter.append("path"), - enterText = enter.append("text") - .attr("class", "label") - .text(function (d) { return label(d); } ) - .attr("text-anchor", "middle") - .attr("dy", ".35em") - .attr("x", width/2) - .attr("y", height/2); - - - // apply minimal style if wanted - if (styled) { - enterPath.style("fill-opacity", "0") - .filter(function (d) { return d.sets.length == 1; } ) - .style("fill", function(d) { return colours(label(d)); }) - .style("fill-opacity", ".25"); - - enterText - .style("fill", function(d) { return d.sets.length == 1 ? colours(label(d)) : "#444"; }); - } - - // update existing, using pathTween if necessary - var update = selection; - if (hasPrevious) { - update = selection.transition("venn").duration(duration); - update.selectAll("path") - .attrTween("d", pathTween); - } else { - update.selectAll("path") - .attr("d", function(d) { - return intersectionAreaPath(d.sets.map(function (set) { return circles[set]; })); - }); - } - - var updateText = update.selectAll("text") - .filter(function (d) { return d.sets in textCentres; }) - .text(function (d) { return label(d); } ) - .attr("x", function(d) { return Math.floor(textCentres[d.sets].x);}) - .attr("y", function(d) { return Math.floor(textCentres[d.sets].y);}); - - if (wrap) { - if (hasPrevious) { - // d3 4.0 uses 'on' for events on transitions, - // but d3 3.0 used 'each' instead. switch appropiately - if ('on' in updateText) { - updateText.on("end", wrapText(circles, label)); - } else { - updateText.each("end", wrapText(circles, label)); - } - } else { - updateText.each(wrapText(circles, label)); - } - } - - // remove old - var exit = nodes.exit().transition('venn').duration(duration).remove(); - exit.selectAll("path") - .attrTween("d", pathTween); - - var exitText = exit.selectAll("text") - .attr("x", width/2) - .attr("y", height/2); - - // if we've been passed a fontSize explicitly, use it to - // transition - if (fontSize !== null) { - enterText.style("font-size", "0px"); - updateText.style("font-size", fontSize); - exitText.style("font-size", "0px"); - } - - - return {'circles': circles, - 'textCentres': textCentres, - 'nodes': nodes, - 'enter': enter, - 'update': update, - 'exit': exit}; - } - - function label(d) { - if (d.label) { - return d.label; - } - if (d.sets.length == 1) { - return '' + d.sets[0]; - } - } - - chart.wrap = function(_) { - if (!arguments.length) return wrap; - wrap = _; - return chart; - }; - - chart.width = function(_) { - if (!arguments.length) return width; - width = _; - return chart; - }; - - chart.height = function(_) { - if (!arguments.length) return height; - height = _; - return chart; - }; - - chart.padding = function(_) { - if (!arguments.length) return padding; - padding = _; - return chart; - }; - - chart.colours = function(_) { - if (!arguments.length) return colours; - colours = _; - return chart; - }; - - chart.fontSize = function(_) { - if (!arguments.length) return fontSize; - fontSize = _; - return chart; - }; - - chart.duration = function(_) { - if (!arguments.length) return duration; - duration = _; - return chart; - }; - - chart.layoutFunction = function(_) { - if (!arguments.length) return layoutFunction; - layoutFunction = _; - return chart; - }; - - chart.normalize = function(_) { - if (!arguments.length) return normalize; - normalize = _; - return chart; - }; - - chart.styled = function(_) { - if (!arguments.length) return styled; - styled = _; - return chart; - }; - - chart.orientation = function(_) { - if (!arguments.length) return orientation; - orientation = _; - return chart; - }; - - chart.orientationOrder = function(_) { - if (!arguments.length) return orientationOrder; - orientationOrder = _; - return chart; - }; - - return chart; - } - // sometimes text doesn't fit inside the circle, if thats the case lets wrap - // the text here such that it fits - // todo: looks like this might be merged into d3 ( - // https://github.com/mbostock/d3/issues/1642), - // also worth checking out is - // http://engineering.findthebest.com/wrapping-axis-labels-in-d3-js/ - // this seems to be one of those things that should be easy but isn't - function wrapText(circles, labeller) { - return function() { - var text = d3.select(this), - data = text.datum(), - width = circles[data.sets[0]].radius || 50, - label = labeller(data) || ''; - - var words = label.split(/\s+/).reverse(), - maxLines = 3, - minChars = (label.length + words.length) / maxLines, - word = words.pop(), - line = [word], - joined, - lineNumber = 0, - lineHeight = 1.1, // ems - tspan = text.text(null).append("tspan").text(word); - - while (true) { - word = words.pop(); - if (!word) break; - line.push(word); - joined = line.join(" "); - tspan.text(joined); - if (joined.length > minChars && tspan.node().getComputedTextLength() > width) { - line.pop(); - tspan.text(line.join(" ")); - line = [word]; - tspan = text.append("tspan").text(word); - lineNumber++; - } - } - - var initial = 0.35 - lineNumber * lineHeight / 2, - x = text.attr("x"), - y = text.attr("y"); - - text.selectAll("tspan") - .attr("x", x) - .attr("y", y) - .attr("dy", function(d, i) { - return (initial + i * lineHeight) + "em"; - }); - }; - } - - function circleMargin(current, interior, exterior) { - var margin = interior[0].radius - distance(interior[0], current), i, m; - for (i = 1; i < interior.length; ++i) { - m = interior[i].radius - distance(interior[i], current); - if (m <= margin) { - margin = m; - } - } - - for (i = 0; i < exterior.length; ++i) { - m = distance(exterior[i], current) - exterior[i].radius; - if (m <= margin) { - margin = m; - } - } - return margin; - } - - // compute the center of some circles by maximizing the margin of - // the center point relative to the circles (interior) after subtracting - // nearby circles (exterior) - function computeTextCentre(interior, exterior) { - // get an initial estimate by sampling around the interior circles - // and taking the point with the biggest margin - var points = [], i; - for (i = 0; i < interior.length; ++i) { - var c = interior[i]; - points.push({x: c.x, y: c.y}); - points.push({x: c.x + c.radius/2, y: c.y}); - points.push({x: c.x - c.radius/2, y: c.y}); - points.push({x: c.x, y: c.y + c.radius/2}); - points.push({x: c.x, y: c.y - c.radius/2}); - } - var initial = points[0], margin = circleMargin(points[0], interior, exterior); - for (i = 1; i < points.length; ++i) { - var m = circleMargin(points[i], interior, exterior); - if (m >= margin) { - initial = points[i]; - margin = m; - } - } - - // maximize the margin numerically - var solution = fmin( - function(p) { return -1 * circleMargin({x: p[0], y: p[1]}, interior, exterior); }, - [initial.x, initial.y], - {maxIterations:500, minErrorDelta:1e-10}).solution; - var ret = {x: solution[0], y: solution[1]}; - - // check solution, fallback as needed (happens if fully overlapped - // etc) - var valid = true; - for (i = 0; i < interior.length; ++i) { - if (distance(ret, interior[i]) > interior[i].radius) { - valid = false; - break; - } - } - - for (i = 0; i < exterior.length; ++i) { - if (distance(ret, exterior[i]) < exterior[i].radius) { - valid = false; - break; - } - } - - if (!valid) { - if (interior.length == 1) { - ret = {x: interior[0].x, y: interior[0].y}; - } else { - var areaStats = {}; - intersectionArea(interior, areaStats); - - if (areaStats.arcs.length === 0) { - ret = {'x': 0, 'y': -1000, disjoint:true}; - - } else if (areaStats.arcs.length == 1) { - ret = {'x': areaStats.arcs[0].circle.x, - 'y': areaStats.arcs[0].circle.y}; - - } else if (exterior.length) { - // try again without other circles - ret = computeTextCentre(interior, []); - - } else { - // take average of all the points in the intersection - // polygon. this should basically never happen - // and has some issues: - // https://github.com/benfred/venn.js/issues/48#issuecomment-146069777 - ret = getCenter(areaStats.arcs.map(function (a) { return a.p1; })); - } - } - } - - return ret; - } - - // given a dictionary of {setid : circle}, returns - // a dictionary of setid to list of circles that completely overlap it - function getOverlappingCircles(circles) { - var ret = {}, circleids = []; - for (var circleid in circles) { - circleids.push(circleid); - ret[circleid] = []; - } - for (var i = 0; i < circleids.length; i++) { - var a = circles[circleids[i]]; - for (var j = i + 1; j < circleids.length; ++j) { - var b = circles[circleids[j]], - d = distance(a, b); - - if (d + b.radius <= a.radius + 1e-10) { - ret[circleids[j]].push(circleids[i]); - - } else if (d + a.radius <= b.radius + 1e-10) { - ret[circleids[i]].push(circleids[j]); - } - } - } - return ret; - } - - function computeTextCentres(circles, areas) { - var ret = {}, overlapped = getOverlappingCircles(circles); - for (var i = 0; i < areas.length; ++i) { - var area = areas[i].sets, areaids = {}, exclude = {}; - for (var j = 0; j < area.length; ++j) { - areaids[area[j]] = true; - var overlaps = overlapped[area[j]]; - // keep track of any circles that overlap this area, - // and don't consider for purposes of computing the text - // centre - for (var k = 0; k < overlaps.length; ++k) { - exclude[overlaps[k]] = true; - } - } - - var interior = [], exterior = []; - for (var setid in circles) { - if (setid in areaids) { - interior.push(circles[setid]); - } else if (!(setid in exclude)) { - exterior.push(circles[setid]); - } - } - var centre = computeTextCentre(interior, exterior); - ret[area] = centre; - if (centre.disjoint && (areas[i].size > 0)) { - console.log("WARNING: area " + area + " not represented on screen"); - } - } - return ret; - } - - // sorts all areas in the venn diagram, so that - // a particular area is on top (relativeTo) - and - // all other areas are so that the smallest areas are on top - function sortAreas(div, relativeTo) { - - // figure out sets that are completly overlapped by relativeTo - var overlaps = getOverlappingCircles(div.selectAll("svg").datum()); - var exclude = {}; - for (var i = 0; i < relativeTo.sets.length; ++i) { - var check = relativeTo.sets[i]; - for (var setid in overlaps) { - var overlap = overlaps[setid]; - for (var j = 0; j < overlap.length; ++j) { - if (overlap[j] == check) { - exclude[setid] = true; - break; - } - } - } - } - - // checks that all sets are in exclude; - function shouldExclude(sets) { - for (var i = 0; i < sets.length; ++i) { - if (!(sets[i] in exclude)) { - return false; - } - } - return true; - } - - // need to sort div's so that Z order is correct - div.selectAll("g").sort(function (a, b) { - // highest order set intersections first - if (a.sets.length != b.sets.length) { - return a.sets.length - b.sets.length; - } - - if (a == relativeTo) { - return shouldExclude(b.sets) ? -1 : 1; - } - if (b == relativeTo) { - return shouldExclude(a.sets) ? 1 : -1; - } - - // finally by size - return b.size - a.size; - }); - } - - function circlePath(x, y, r) { - var ret = []; - ret.push("\nM", x, y); - ret.push("\nm", -r, 0); - ret.push("\na", r, r, 0, 1, 0, r *2, 0); - ret.push("\na", r, r, 0, 1, 0,-r *2, 0); - return ret.join(" "); - } - - // inverse of the circlePath function, returns a circle object from an svg path - function circleFromPath(path) { - var tokens = path.split(' '); - return {'x' : parseFloat(tokens[1]), - 'y' : parseFloat(tokens[2]), - 'radius' : -parseFloat(tokens[4]) - }; - } - - /** returns a svg path of the intersection area of a bunch of circles */ - function intersectionAreaPath(circles) { - var stats = {}; - intersectionArea(circles, stats); - var arcs = stats.arcs; - - if (arcs.length === 0) { - return "M 0 0"; - - } else if (arcs.length == 1) { - var circle = arcs[0].circle; - return circlePath(circle.x, circle.y, circle.radius); - - } else { - // draw path around arcs - var ret = ["\nM", arcs[0].p2.x, arcs[0].p2.y]; - for (var i = 0; i < arcs.length; ++i) { - var arc = arcs[i], r = arc.circle.radius, wide = arc.width > r; - ret.push("\nA", r, r, 0, wide ? 1 : 0, 1, - arc.p1.x, arc.p1.y); - } - return ret.join(" "); - } - } - - exports.fmin = fmin; - exports.minimizeConjugateGradient = minimizeConjugateGradient; - exports.bisect = bisect; - exports.intersectionArea = intersectionArea; - exports.circleCircleIntersection = circleCircleIntersection; - exports.circleOverlap = circleOverlap; - exports.circleArea = circleArea; - exports.distance = distance; - exports.circleIntegral = circleIntegral; - exports.venn = venn; - exports.greedyLayout = greedyLayout; - exports.scaleSolution = scaleSolution; - exports.normalizeSolution = normalizeSolution; - exports.bestInitialLayout = bestInitialLayout; - exports.lossFunction = lossFunction; - exports.disjointCluster = disjointCluster; - exports.distanceFromIntersectArea = distanceFromIntersectArea; - exports.VennDiagram = VennDiagram; - exports.wrapText = wrapText; - exports.computeTextCentres = computeTextCentres; - exports.computeTextCentre = computeTextCentre; - exports.sortAreas = sortAreas; - exports.circlePath = circlePath; - exports.circleFromPath = circleFromPath; - exports.intersectionAreaPath = intersectionAreaPath; - -})); \ No newline at end of file diff --git a/config/plugins/visualizations/venn/static/logo.png b/config/plugins/visualizations/venn/static/logo.png deleted file mode 100644 index 3d0b70624c8d..000000000000 Binary files a/config/plugins/visualizations/venn/static/logo.png and /dev/null differ diff --git a/config/plugins/visualizations/venn/static/logo.svg b/config/plugins/visualizations/venn/static/logo.svg new file mode 100644 index 000000000000..2d851c8a414c --- /dev/null +++ b/config/plugins/visualizations/venn/static/logo.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/config/plugins/visualizations/venn/static/script.css b/config/plugins/visualizations/venn/static/script.css deleted file mode 100644 index 235c2555cee7..000000000000 --- a/config/plugins/visualizations/venn/static/script.css +++ /dev/null @@ -1,14 +0,0 @@ -.venntooltip { - position: absolute; - text-align: center; - width: 128px; - height: 22px; - background: #333; - color: #ddd; - border: 0px; - border-radius: 8px; - opacity: 0; -} -.venn-area > .label { - font-size: 1em; -} \ No newline at end of file