UNPKG

venn-helper

Version:

Area Proportional Venn and Euler Diagrams

1,367 lines (1,360 loc) 47.5 kB
"use strict"; var venn2 = (() => { var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __commonJS = (cb, mod) => function __require() { return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // node_modules/fmin/build/fmin.js var require_fmin = __commonJS({ "node_modules/fmin/build/fmin.js"(exports, module) { "use strict"; (function(global, factory) { typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define(["exports"], factory) : factory(global.fmin = global.fmin || {}); })(exports, function(exports2) { "use strict"; function bisect2(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; } function zeros2(x) { var r = new Array(x); for (var i = 0; i < x; ++i) { r[i] = 0; } return r; } function zerosM2(x, y) { return zeros2(x).map(function() { return zeros2(y); }); } function dot(a, b) { var ret = 0; for (var i = 0; i < a.length; ++i) { ret += a[i] * b[i]; } return ret; } function norm22(a) { return Math.sqrt(dot(a, a)); } function scale2(ret, value, c) { for (var i = 0; i < value.length; ++i) { ret[i] = value[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]; } } function nelderMead3(f, x0, parameters) { parameters = parameters || {}; var maxIterations = parameters.maxIterations || x0.length * 200, nonZeroDelta = parameters.nonZeroDelta || 1.05, zeroDelta = parameters.zeroDelta || 1e-3, minErrorDelta = parameters.minErrorDelta || 1e-6, minTolerance = parameters.minErrorDelta || 1e-5, rho = parameters.rho !== void 0 ? parameters.rho : 1, chi = parameters.chi !== void 0 ? parameters.chi : 2, psi = parameters.psi !== void 0 ? parameters.psi : -0.5, sigma = parameters.sigma !== void 0 ? parameters.sigma : 0.5, maxDiff; var N = x0.length, simplex = new Array(N + 1); simplex[0] = x0; simplex[0].fx = f(x0); simplex[0].id = 0; 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); simplex[i + 1].id = i + 1; } function updateSimplex(value) { for (var i2 = 0; i2 < value.length; i2++) { simplex[N][i2] = value[i2]; } simplex[N].fx = value.fx; } 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 (parameters.history) { var sortedSimplex = simplex.map(function(x) { var state = x.slice(); state.fx = x.fx; state.id = x.id; return state; }); sortedSimplex.sort(function(a, b) { return a.id - b.id; }); parameters.history.push({ x: simplex[0].slice(), fx: simplex[0].fx, simplex: sortedSimplex }); } 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; } for (i = 0; i < N; ++i) { centroid[i] = 0; for (var j = 0; j < N; ++j) { centroid[i] += simplex[j][i]; } centroid[i] /= N; } var worst = simplex[N]; weightedSum(reflected, 1 + rho, centroid, -rho, worst); reflected.fx = f(reflected); if (reflected.fx < simplex[0].fx) { weightedSum(expanded, 1 + chi, centroid, -chi, worst); expanded.fx = f(expanded); if (expanded.fx < reflected.fx) { updateSimplex(expanded); } else { updateSimplex(reflected); } } else if (reflected.fx >= simplex[N - 1].fx) { var shouldReduce = false; if (reflected.fx > worst.fx) { weightedSum(contracted, 1 + psi, centroid, -psi, worst); contracted.fx = f(contracted); if (contracted.fx < worst.fx) { updateSimplex(contracted); } else { shouldReduce = true; } } else { weightedSum(contracted, 1 - psi * rho, centroid, psi * rho, worst); contracted.fx = f(contracted); if (contracted.fx < reflected.fx) { updateSimplex(contracted); } else { shouldReduce = true; } } if (shouldReduce) { if (sigma >= 1) break; for (i = 1; i < simplex.length; ++i) { weightedSum(simplex[i], 1 - sigma, simplex[0], sigma, simplex[i]); simplex[i].fx = f(simplex[i]); } } } else { updateSimplex(reflected); } } simplex.sort(sortOrder); return { fx: simplex[0].fx, x: simplex[0] }; } function wolfeLineSearch(f, pk, current, next, a, c1, c2) { var phi0 = current.fx, phiPrime0 = dot(current.fxprime, pk), phi = phi0, phi_old = phi0, phiPrime = phiPrime0, a0 = 0; a = a || 1; c1 = c1 || 1e-6; c2 = c2 || 0.1; function zoom(a_lo, a_high, phi_lo) { for (var iteration2 = 0; iteration2 < 16; ++iteration2) { a = (a_lo + a_high) / 2; weightedSum(next.x, 1, 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, 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 a; } function conjugateGradient2(f, initial, params) { 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 * 20; current.fx = f(current.x, current.fxprime); pk = current.fxprime.slice(); scale2(pk, current.fxprime, -1); for (var i = 0; i < maxIterations; ++i) { a = wolfeLineSearch(f, pk, current, next, a); if (params.history) { params.history.push({ x: current.x.slice(), fx: current.fx, fxprime: current.fxprime.slice(), alpha: a }); } if (!a) { scale2(pk, current.fxprime, -1); } else { 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 (norm22(current.fxprime) <= 1e-5) { break; } } if (params.history) { params.history.push({ x: current.x.slice(), fx: current.fx, fxprime: current.fxprime.slice(), alpha: a }); } return current; } function gradientDescent(f, initial, params) { params = params || {}; var maxIterations = params.maxIterations || initial.length * 100, learnRate = params.learnRate || 1e-3, current = { x: initial.slice(), fx: 0, fxprime: initial.slice() }; for (var i = 0; i < maxIterations; ++i) { current.fx = f(current.x, current.fxprime); if (params.history) { params.history.push({ x: current.x.slice(), fx: current.fx, fxprime: current.fxprime.slice() }); } weightedSum(current.x, 1, current.x, -learnRate, current.fxprime); if (norm22(current.fxprime) <= 1e-5) { break; } } return current; } function gradientDescentLineSearch(f, initial, params) { params = params || {}; var current = { x: initial.slice(), fx: 0, fxprime: initial.slice() }, next = { x: initial.slice(), fx: 0, fxprime: initial.slice() }, maxIterations = params.maxIterations || initial.length * 100, learnRate = params.learnRate || 1, pk = initial.slice(), c1 = params.c1 || 1e-3, c2 = params.c2 || 0.1, temp, functionCalls = []; if (params.history) { var inner = f; f = function(x, fxprime) { functionCalls.push(x.slice()); return inner(x, fxprime); }; } current.fx = f(current.x, current.fxprime); for (var i = 0; i < maxIterations; ++i) { scale2(pk, current.fxprime, -1); learnRate = wolfeLineSearch(f, pk, current, next, learnRate, c1, c2); if (params.history) { params.history.push({ x: current.x.slice(), fx: current.fx, fxprime: current.fxprime.slice(), functionCalls, learnRate, alpha: learnRate }); functionCalls = []; } temp = current; current = next; next = temp; if (learnRate === 0 || norm22(current.fxprime) < 1e-5) break; } return current; } exports2.bisect = bisect2; exports2.nelderMead = nelderMead3; exports2.conjugateGradient = conjugateGradient2; exports2.gradientDescent = gradientDescent; exports2.gradientDescentLineSearch = gradientDescentLineSearch; exports2.zeros = zeros2; exports2.zerosM = zerosM2; exports2.norm2 = norm22; exports2.weightedSum = weightedSum; exports2.scale = scale2; }); } }); // src/index.ts var index_exports = {}; __export(index_exports, { bestInitialLayout: () => bestInitialLayout, chartVega: () => chartVega, circleArea: () => circleArea, circleCircleIntersection: () => circleCircleIntersection, circleOverlap: () => circleOverlap, circlePath: () => circlePath, computeTextCentre: () => computeTextCentre, computeTextCentres: () => computeTextCentres, constrainedMDSLayout: () => constrainedMDSLayout, containedInCircles: () => containedInCircles, disjointCluster: () => disjointCluster, distance: () => distance, distanceFromIntersectArea: () => distanceFromIntersectArea, getCenter: () => getCenter, getDistanceMatrices: () => getDistanceMatrices, greedyLayout: () => greedyLayout, intersectionArea: () => intersectionArea, intersectionAreaPath: () => intersectionAreaPath, lossFunction: () => lossFunction, normalizeSolution: () => normalizeSolution, scaleSolution: () => scaleSolution, venn: () => venn }); // src/layout.ts var import_fmin = __toESM(require_fmin(), 1); // src/circle-intersection.ts var SMALL = 1e-10; function intersectionArea(circles) { var intersectionPoints = getIntersectionPoints(circles); var innerPoints = intersectionPoints.filter(function(p) { return containedInCircles(p, circles); }); var arcArea = 0, polygonArea = 0, arcs = [], i; if (innerPoints.length > 1) { var center = getCenter(innerPoints); innerPoints = innerPoints.map((p) => ({ ...p, angle: Math.atan2(p.x - center.x, p.y - center.y) })).sort((p12, p22) => p22.angle - p12.angle); var p2 = innerPoints[innerPoints.length - 1]; for (i = 0; i < innerPoints.length; ++i) { var p1 = innerPoints[i]; polygonArea += (p2.x + p1.x) * (p1.y - p2.y); 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) { 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; } var a = a2 - angleDiff / 2, width = distance(midPoint, { x: circle.x + circle.radius * Math.sin(a), y: circle.y + circle.radius * Math.cos(a) }); if (width > circle.radius * 2) { width = circle.radius * 2; } if (arc === null || arc.width > width) { arc = { circle, width, p1, p2 }; } } } if (arc !== null) { arcs.push(arc); arcArea += circleArea(arc.circle.radius, arc.width); p2 = p1; } } } else { var smallest = circles[0]; for (i = 1; i < circles.length; ++i) { const compare = circles[i]; if (compare.radius < smallest.radius) { smallest = compare; } } var disjoint = false; for (i = 0; i < circles.length; ++i) { const circle2 = circles[i]; if (distance(circle2, smallest) > Math.abs(smallest.radius - circle2.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; return { overlap: arcArea + polygonArea, stats: { area: arcArea + polygonArea, arcArea, polygonArea, arcs, innerPoints, intersectionPoints } }; } function containedInCircles(point, circles) { for (var i = 0; i < circles.length; ++i) { const circle = circles[i]; if (distance(point, circle) > circle.radius + SMALL) { return false; } } return true; } function getIntersectionPoints(circles) { var ret = []; for (var i = 0; i < circles.length; ++i) { for (var j = i + 1; j < circles.length; ++j) { const c1 = circles[i]; const c2 = circles[j]; if (!c1 || !c2) continue; var intersect = circleCircleIntersection(c1, c2); for (var k = 0; k < intersect.length; ++k) { let p = intersect[k]; if (!p) continue; ret.push({ ...p, parentIndex: [i, j] }); } } } return ret; } function circleArea(r, width) { return r * r * Math.acos(1 - width / r) - (r - width) * Math.sqrt(width * (2 * r - width)); } function distance(p1, p2) { return Math.sqrt( (p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y) ); } function circleOverlap(r1, r2, d) { if (d >= r1 + r2) { return 0; } 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); } function circleCircleIntersection(p1, p2) { var d = distance(p1, p2), r1 = p1.radius, r2 = p2.radius; 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 } ]; } function getCenter(points) { const center = points.reduce( (sum, p) => { return { x: sum.x + p.x, y: sum.y + p.y }; }, { x: 0, y: 0 } ); center.x /= points.length; center.y /= points.length; return center; } // src/layout.ts function venn(areas, parameters = {}) { parameters = parameters || {}; parameters.maxIterations = parameters.maxIterations || 500; var initialLayout = parameters.initialLayout || bestInitialLayout; var loss = parameters.lossFunction || lossFunction; areas = addMissingAreas(areas); var circles = initialLayout(areas, parameters); var initial = [], setids = [], setid; for (const setid2 of Object.keys(circles)) { const circle = circles[setid2]; if (!circle) continue; initial.push(circle.x); initial.push(circle.y); setids.push(setid2); } var totalFunctionCalls = 0; var solution = (0, import_fmin.nelderMead)( function(values) { totalFunctionCalls += 1; var current = {}; for (var i2 = 0; i2 < setids.length; ++i2) { var setid2 = setids[i2]; const circle = circles[setid2]; current[setid2] = { x: values[2 * i2], y: values[2 * i2 + 1], radius: circle.radius, size: circle.size, rowid: circle.rowid }; } return loss(current, areas); }, initial, parameters ); var positions = solution.x; for (var i = 0; i < setids.length; ++i) { setid = setids[i]; const circle = circles[setid]; if (!circle) continue; circle.x = positions[2 * i]; circle.y = positions[2 * i + 1]; } return circles; } var SMALL2 = 1e-10; function distanceFromIntersectArea(r1, r2, overlap) { if (Math.min(r1, r2) * Math.min(r1, r2) * Math.PI <= overlap + SMALL2) { return Math.abs(r1 - r2); } return (0, import_fmin.bisect)( function(distance2) { return circleOverlap(r1, r2, distance2) - overlap; }, 0, r1 + r2 ); } function addMissingAreas(areas) { areas = areas.slice(); var ids = [], pairs = {}, i, j, a, b; for (const area of areas) { 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((a2, b2) => a2.localeCompare(b2)); 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; } function getDistanceMatrices(areas, sets, setids) { var distances = (0, import_fmin.zerosM)(sets.length, sets.length), constraints = (0, import_fmin.zerosM)(sets.length, sets.length); 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), distance2 = distanceFromIntersectArea(r1, r2, current.size); distances[left][right] = distances[right][left] = distance2; 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, constraints }; } 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), distance2 = Math.sqrt(squaredDistance), delta = squaredDistance - dij * dij; if (constraint > 0 && distance2 <= dij || constraint < 0 && distance2 >= 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; } function bestInitialLayout(areas, params) { var initial = greedyLayout(areas, params); var loss = params.lossFunction || lossFunction; if (areas.length >= 8) { var constrained = constrainedMDSLayout(areas, params), constrainedLoss = loss(constrained, areas), greedyLoss = loss(initial, areas); if (constrainedLoss + 1e-8 < greedyLoss) { initial = constrained; } } return initial; } function constrainedMDSLayout(areas, params) { params = params || {}; var restarts = params.restarts || 10; var sets = [], setids = {}, i; for (i = 0; i < areas.length; ++i) { var area = areas[i]; if (area?.sets.length === 1) { if (area.sets[0]) setids[area.sets[0]] = sets.length; sets.push(area); } } var matrices = getDistanceMatrices(areas, sets, setids), distances = matrices.distances, constraints = matrices.constraints; var norm = (0, import_fmin.norm2)(distances.map(import_fmin.norm2)) / distances.length; distances = distances.map(function(row) { return row.map(function(value) { return value / norm; }); }); var best, current; for (i = 0; i < restarts; ++i) { var initial = (0, import_fmin.zeros)(distances.length * 2).map(Math.random); current = (0, import_fmin.conjugateGradient)( (x, fxprime) => constrainedMDSGradient(x, fxprime, distances, constraints), initial, params ); if (!best || current.fx < best.fx) { best = current; } } var positions = best?.x; var circles = {}; for (i = 0; i < sets.length; ++i) { var set = sets[i]; const setName = set?.sets[0]; if (!set || !setName) continue; circles[setName] = { x: positions[2 * i] * norm, y: positions[2 * i + 1] * norm, radius: Math.sqrt(set.size / Math.PI), size: set.size, rowid: Object.keys(circles).length }; } if (params.history) { for (const step of params.history) { (0, import_fmin.scale)(step.x, norm); } } return circles; } function greedyLayout(areas, params) { var loss = params && params.lossFunction ? params.lossFunction : lossFunction; 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: Object.keys(circles).length, size: area.size, radius: Math.sqrt(area.size / Math.PI) }; setOverlaps[set] = []; } } areas = areas.filter(function(a) { return a.sets.length == 2; }); for (i = 0; i < areas.length; ++i) { var current = areas[i]; var weight = current.weight ? current.weight : 1; var left = current.sets[0], right = current.sets[1]; const leftCircle = circles[left]; const rightCircle = circles[right]; if (!leftCircle.size || !rightCircle.size) continue; if (current.size + SMALL2 >= Math.min(leftCircle.size, rightCircle.size)) { weight = 0; } setOverlaps[left]?.push({ set: right, size: current.size, weight }); setOverlaps[right]?.push({ set: left, size: current.size, weight }); } var mostOverlapped = []; for (set in setOverlaps) { const overlaps = setOverlaps[set]; if (overlaps) { var size = 0; for (i = 0; i < overlaps.length; ++i) { const overlap2 = overlaps[i]; if (!overlap2) continue; size += overlap2.size * overlap2.weight; } mostOverlapped.push({ set, size }); } } mostOverlapped.sort((a, b) => b.size - a.size); var positioned = {}; function isPositioned(element) { return element.set in positioned; } function positionSet(point, index) { const circle = circles[index]; if (circle) { circle.x = point.x; circle.y = point.y; } positioned[index] = true; } positionSet({ x: 0, y: 0 }, mostOverlapped[0]?.set ?? ""); for (i = 1; i < mostOverlapped.length; ++i) { var setIndex = mostOverlapped[i]?.set; var overlap = setIndex ? setOverlaps[setIndex]?.filter(isPositioned) : null; const set2 = setIndex ? circles[setIndex] : null; overlap?.sort((a, b) => b.size - a.size); if (overlap?.length === 0) { throw "ERROR: missing pairwise overlap information"; } if (!overlap) { throw "ERROR: missing pairwise overlap information"; } var points = []; for (var j = 0; j < overlap.length; ++j) { const item = overlap[j]; if (!item) continue; var p1 = circles[overlap[j].set]; if (!p1) continue; const d1 = distanceFromIntersectArea(set2.radius, p1.radius, item.size); 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 }); for (var k = j + 1; k < overlap.length; ++k) { var p2 = circles[overlap[k].set], d2 = distanceFromIntersectArea( set2.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]); } } } 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 localLoss = loss(circles, areas); if (localLoss < bestLoss) { bestLoss = localLoss; bestPoint = points[j]; } } if (bestPoint && setIndex) positionSet(bestPoint, setIndex); } return circles; } function lossFunction(sets, overlaps) { var output = 0; function getCircles(indices) { return indices.map(function(i2) { return sets[i2]; }); } for (var i = 0; i < overlaps.length; ++i) { var area = overlaps[i], overlap; if (!area || area.sets.length === 1) continue; 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)).overlap; } var weight = area.weight ? area.weight : 1; output += weight * (overlap - area.size) * (overlap - area.size); } return output; } function orientateCircles(circles, orientation, orientationOrder) { if (orientationOrder === null) { circles.sort(function(a, b) { return b.radius - a.radius; }); } else { circles.sort(orientationOrder); } var i; 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; } } if (circles.length == 2) { const c1 = circles[0]; const c2 = circles[1]; if (c1 && c2) { var dist = distance(c1, c2); if (dist < Math.abs(c2.radius - c1.radius)) { c2.x = c1.x + c1.radius - c2.radius - 1e-10; c2.y = c2.y; } } } if (circles.length > 1) { var rotation = Math.atan2(circles[1].x, circles[1].y) - (orientation ?? 0), c = Math.cos(rotation), s = Math.sin(rotation), x, y; for (i = 0; i < circles.length; ++i) { const circle = circles[i]; if (!circle) continue; x = circle.x; y = circle.y; circle.x = c * x - s * y; circle.y = s * x + c * y; } } if (circles.length > 2) { var angle = Math.atan2(circles[2].x, circles[2].y) - (orientation ?? 0); 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) { const circle = circles[i]; if (!circle) continue; var d = (circle.x + slope * circle.y) / (1 + slope * slope); circle.x = 2 * d - circle.x; circle.y = 2 * d * slope - circle.y; } } } } function disjointCluster(circles) { circles.map(function(circle) { circle.parent = circle; }); function find(circle) { if (!circle) return void 0; if (circle.parent !== circle) { circle.parent = find(circle.parent); } return circle.parent; } function union(x, y) { var xRoot = find(x), yRoot = find(y); if (!xRoot) return; xRoot.parent = yRoot; } for (var i = 0; i < circles.length; ++i) { for (var j = i + 1; j < circles.length; ++j) { const c1 = circles[i]; const c2 = circles[j]; if (!c1 || !c2) continue; var maxDistance = c1.radius + c2.radius; if (distance(c1, c2) + 1e-10 < maxDistance) { union(c2, c1); } } } var disjointClusters = {}, setid; for (i = 0; i < circles.length; ++i) { const circle = circles[i]; if (!circle) continue; const parentCircle = find(circles[i]); const setid2 = parentCircle?.parent?.setid; if (!setid2) continue; if (!(setid2 in disjointClusters)) { disjointClusters[setid2] = []; } if (disjointClusters[setid2]) disjointClusters[setid2].push(circle); } circles.map(function(circle) { delete circle.parent; }); var ret = []; for (setid in disjointClusters) { const cluster = disjointClusters[setid]; if (cluster) { ret.push(cluster); } } 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; } var circles = [], i, setid; for (setid in solution) { var previous = solution[setid]; if (!previous) continue; circles.push({ ...previous, setid }); } var clusters = disjointCluster(circles); for (i = 0; i < clusters.length; ++i) { const cluster = clusters[i]; if (!cluster) continue; orientateCircles(cluster, orientation, orientationOrder); var bounds = getBoundingBox(cluster); cluster.size = (bounds.xRange.max - bounds.xRange.min) * (bounds.yRange.max - bounds.yRange.min); cluster.bounds = bounds; } clusters.sort(function(a, b) { if (!a.size || !b.size) return 0; return b.size - a.size; }); let largestCluster = clusters[0]; var returnBounds = largestCluster.bounds; var spacing = (returnBounds.xRange.max - returnBounds.xRange.min) / 50; function addCluster(cluster, right, bottom) { if (!cluster) return; if (!cluster.size || !cluster.bounds) return; var bounds2 = cluster.bounds, xOffset, yOffset, centreing; if (right) { xOffset = returnBounds.xRange.max - bounds2.xRange.min + spacing; } else { xOffset = returnBounds.xRange.max - bounds2.xRange.max; centreing = (bounds2.xRange.max - bounds2.xRange.min) / 2 - (returnBounds.xRange.max - returnBounds.xRange.min) / 2; if (centreing < 0) xOffset += centreing; } if (bottom) { yOffset = returnBounds.yRange.max - bounds2.yRange.min + spacing; } else { yOffset = returnBounds.yRange.max - bounds2.yRange.max; centreing = (bounds2.yRange.max - bounds2.yRange.min) / 2 - (returnBounds.yRange.max - returnBounds.yRange.min) / 2; if (centreing < 0) yOffset += centreing; } for (var j = 0; j < cluster.length; ++j) { const circle = cluster[j]; if (!circle) continue; circle.x += xOffset; circle.y += yOffset; circles.push(circle); } } 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; returnBounds = getBoundingBox(circles); } var ret = {}; for (const circle of circles) { ret[circle.setid] = circle; } return ret; } function scaleSolution(solution, width, height, padding) { var circles = [], setids = []; for (var setid in solution) { const circle2 = solution[setid]; if (circle2) { setids.push(setid); circles.push(circle2); } } width -= 2 * padding; height -= 2 * padding; var bounds = getBoundingBox(circles), xRange = bounds.xRange, yRange = bounds.yRange; if (xRange.max == xRange.min || yRange.max == yRange.min) { console.log("not scaling solution: zero size detected"); return solution; } var xScaling = width / (xRange.max - xRange.min), yScaling = height / (yRange.max - yRange.min), scaling = Math.min(yScaling, xScaling), 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]; const id = setids[i]; if (!circle || !id) continue; scaled[id] = { ...circle, radius: scaling * circle.radius, x: padding + xOffset + (circle.x - xRange.min) * scaling, y: padding + yOffset + (circle.y - yRange.min) * scaling }; } return scaled; } // src/diagram.ts var import_fmin2 = __toESM(require_fmin(), 1); function chartVega(data, options) { const filteredData = data.filter( (datum) => datum.size !== 0 && datum.sets.length > 0 ); let circles = {}; let textCenters = {}; if (filteredData.length > 0) { let solution = venn(filteredData); if (options.normalize) { solution = normalizeSolution(solution, options.orientation); } circles = scaleSolution( solution, options.width, options.height, options.padding ); textCenters = computeTextCentres(circles, filteredData); } const intersections = filteredData.map((datum) => { if (datum.sets.length <= 1) return null; return { sets: datum, path: intersectionAreaPath(datum.sets.map((set) => circles[set])), text: datum.label || datum.sets.join("\u2229") }; }).filter(Boolean); const circlesData = Object.entries(circles).map(([key, circle]) => ({ set: key, x: circle.x, y: circle.y, size: Math.pow(circle.radius * 2, 2), text: key, textX: textCenters[key].x, textY: textCenters[key].y })); const schema = { $schema: "https://vega.github.io/schema/vega/v5.json", width: options.width, height: options.height, padding: options.padding, data: [ { name: "circles", values: circlesData }, { name: "intersections", values: intersections } ], scales: [ { name: "color", type: "ordinal", domain: { data: "circles", field: "set" }, range: "category" } ], marks: [ { type: "symbol", from: { data: "circles" }, encode: { enter: { x: { field: "x" }, y: { field: "y" }, size: { field: "size" }, shape: { value: "circle" }, fillOpacity: { value: 0.3 }, fill: { scale: "color", field: "set" }, tooltip: [{ field: "text", type: "quantitative" }] }, hover: { fillOpacity: { value: 0.5 } }, update: { fillOpacity: { value: 0.3 } } } }, { type: "path", from: { data: "intersections" }, encode: { enter: { path: { field: "path" }, fill: { value: "grey" }, fillOpacity: { value: 0 }, tooltip: [{ field: "text", type: "quantitative" }] }, hover: { stroke: { value: "black" }, strokeWidth: { value: 1 }, fill: { value: "grey" } }, update: { strokeWidth: { value: 0 } } } }, { type: "text", from: { data: "circles" }, encode: { enter: { x: { field: "textX" }, y: { field: "textY" }, text: { field: "text" }, fontSize: { value: 14 }, fill: { scale: "color", field: "set" }, fontWeight: { value: "normal" } } } } ] }; return { circles: circlesData, intersections, schema }; } function intersectionAreaPath(circles) { const { stats } = intersectionArea(circles); var arcs = stats.arcs; if (arcs.length === 0) { return "M 0 0"; } if (arcs.length === 1) { var circle = arcs[0].circle; return circlePath(circle.x, circle.y, circle.radius); } var ret = ["\nM", arcs[0].p2.x, arcs[0].p2.y]; for (const arc of arcs) { const r = arc.circle.radius; const wide = arc.width > r; ret.push("\nA", r, r, 0, wide ? 1 : 0, 1, arc.p1.x, arc.p1.y); } return ret.join(" "); } function circlePath(x, y, r) { var ret = []; ret.push("\nM", x.toString(), y.toString()); 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(" "); } 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]]; if (!overlaps) continue; 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.toString()] = centre; if (centre.disjoint && areas[i].size > 0) { console.log("WARNING: area " + area + " not represented on screen"); } } return ret; } function getOverlappingCircles(circles) { var ret = {}, circleids = Object.keys(circles); circleids.forEach((id) => ret[id] = []); 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 computeTextCentre(interior, exterior) { var points = [], i; for (i = 0; i < interior.length; ++i) { var c = interior[i]; if (!c) continue; 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; } } var solution = (0, import_fmin2.nelderMead)( function(p) { return -1 * circleMargin({ x: p[0], y: p[1] }, interior, exterior); }, [initial.x, initial.y], { maxIterations: 500, minErrorDelta: 1e-10 } ).x; var ret = { x: solution[0], y: solution[1] }; 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 { const { stats: areaStats } = intersectionArea(interior); if (areaStats.arcs.length === 0) { ret = { x: 0, y: -1e3, 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) { ret = computeTextCentre(interior, []); } else { ret = getCenter( areaStats.arcs.map(function(a) { return a.p1; }) ); } } } return ret; } 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; } return __toCommonJS(index_exports); })();