venn-helper
Version:
Area Proportional Venn and Euler Diagrams
1,367 lines (1,360 loc) • 47.5 kB
JavaScript
"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);
})();