chartjs-chart-venn
Version:
Chart.js module for venn diagrams
137 lines (128 loc) • 3.85 kB
text/typescript
import { layout, ICircle, IPoint } from '@upsetjs/venn.js';
import type { IVennDiagramLayout } from './layout';
import { pointAtCircle } from './math';
import type { ITextCircle, IBoundingBox } from './interfaces';
export function center(circles: readonly ICircle[]): { x: number; y: number } {
const sumX = circles.reduce((acc, a) => acc + a.x, 0);
const sumY = circles.reduce((acc, a) => acc + a.y, 0);
return {
x: sumX / circles.length,
y: sumY / circles.length,
};
}
function shiftPath(path: string | undefined, x: (v: number) => number, y: (v: number) => number) {
if (!path) {
return path;
}
const mapX = (v: string) => x(Number.parseFloat(v)).toString();
const mapY = (v: string) => y(Number.parseFloat(v)).toString();
const transformedPath = path
.split('\n')
.map((line) => {
const parts = line.trim().split(/[ ,]/);
if (parts[0] === 'M') {
return `${parts[0]} ${mapX(parts[1])} ${mapY(parts[2])}`;
}
if (parts[0] === 'A') {
return `${parts.slice(0, 6).join(' ')} ${mapX(parts[6])} ${mapY(parts[7])}`;
}
return line;
})
.join('\n');
return transformedPath;
}
function angleAtCircle(p: IPoint, c: IPoint) {
const x = p.x - c.x;
const y = p.y - c.y;
return (Math.atan2(y, x) / Math.PI) * 180;
}
interface IVennJSArc {
circle: { x: number; y: number; radius: number };
width: number;
p1: { x: number; y: number };
p2: { x: number; y: number };
}
export default function euler(
sets: readonly { sets: readonly string[]; value: number }[],
bb: IBoundingBox
): IVennDiagramLayout {
const r = layout(
sets.map((s) => ({ sets: s.sets, size: s.value })),
{
width: bb.width,
height: bb.height,
distinct: true,
}
);
const singleSets = r.filter((d) => d.data.sets.length === 1);
const setNames = singleSets.map((d) => d.data.sets[0]);
const setCircles = singleSets.map((d) => d.circles[0]);
const eulerCenter = center(singleSets.map((d) => d.circles[0]));
const setData = singleSets.map((d) => {
const c = d.circles[0];
const angle = angleAtCircle(c, eulerCenter);
return {
cx: c.x + bb.x,
cy: c.y + bb.y,
r: c.radius,
align: angle > 90 ? 'end' : 'start',
verticalAlign: 'bottom',
text: pointAtCircle(c.x + bb.x, c.y + bb.y, c.radius * 1.1, angle),
} as ITextCircle;
});
const asArc = (a: IVennJSArc) => ({
x2: a.p1.x + bb.x,
y2: a.p1.y + bb.y,
ref: setCircles.findIndex((d) => Math.abs(d.x - a.circle.x) < 0.05 && Math.abs(d.y - a.circle.y) < 0.05),
sweep: true,
large: a.width > a.circle.radius,
mode: 'i' as const,
});
return {
sets: setData,
intersections: r.map((d) => {
const { arcs } = d;
const text = {
x: d.text.x + bb.x,
y: d.text.y + bb.y,
};
const subSets = d.data.sets.map((subSet) => setNames.indexOf(subSet));
if (arcs.length === 0) {
return {
sets: subSets,
text,
x1: 0,
y1: 0,
arcs: [],
};
}
if (arcs.length === 1) {
const c = d.arcs[0].circle;
return {
sets: subSets,
text,
x1: d.arcs[0].p2.x + bb.x,
y1: c.y - c.radius + bb.y,
arcs: [asArc(d.arcs[0]), Object.assign(asArc(d.arcs[0]), { y2: c.y - c.radius + bb.y })],
path: shiftPath(
d.distinctPath || d.path,
(x) => x + bb.x,
(y) => y + bb.y
),
};
}
return {
text,
sets: subSets,
x1: d.arcs[0].p2.x + bb.x,
y1: d.arcs[0].p2.y + bb.y,
arcs: d.arcs.map((e) => asArc(e)),
path: shiftPath(
d.distinctPath || d.path,
(x) => x + bb.x,
(y) => y + bb.y
),
};
}),
};
}