venn-helper
Version:
Area Proportional Venn and Euler Diagrams
1,063 lines (1,057 loc) • 32.7 kB
JavaScript
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
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 __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// 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
});
module.exports = __toCommonJS(index_exports);
// src/layout.ts
var import_fmin = require("fmin");
// 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 = require("fmin");
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;
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
bestInitialLayout,
chartVega,
circleArea,
circleCircleIntersection,
circleOverlap,
circlePath,
computeTextCentre,
computeTextCentres,
constrainedMDSLayout,
containedInCircles,
disjointCluster,
distance,
distanceFromIntersectArea,
getCenter,
getDistanceMatrices,
greedyLayout,
intersectionArea,
intersectionAreaPath,
lossFunction,
normalizeSolution,
scaleSolution,
venn
});