Quickstart: JavaScript

@jolars/eunoia is the WebAssembly build of the eunoia engine. You hand it set sizes, it fits an area-proportional Euler (or canonical Venn) diagram, and hands back plain JavaScript objects (circles, ellipses, polygons, or per-region pieces) that you draw however you like (SVG, Canvas, D3).

The diagram fitted by the first example below.

Install

npm install @jolars/eunoia

The package is an ES module backed by WebAssembly, so it needs a bundler that understands import-ed .wasm (Vite, webpack 5, Rollup, …). It ships its own TypeScript types.

Server-side rendering: call eunoia from the client (e.g. a dynamic import("@jolars/eunoia") inside onMount/useEffect) so the wasm module isn’t instantiated during the server render.

Without a bundler

For a plain <script type="module">, an Observable notebook, or a raw-file CDN, use the @jolars/eunoia/web entry instead. It is a single self-contained ES module with the WebAssembly inlined, so there is nothing for a bundler to resolve, but you must await init() once before calling euler/venn:

<script type="module">
  import { euler, init } from "https://esm.sh/@jolars/eunoia/web";

  await init(); // instantiate the embedded wasm once; idempotent

  const layout = euler({ sets: { A: 5, B: 3, "A&B": 1.5 } });
  console.log(layout.circles);
</script>

In an Observable notebook the same thing across two cells:

eunoia = import("https://esm.sh/@jolars/eunoia/web")
layout = (await eunoia.init(), eunoia.euler({ sets: { A: 5, B: 3, "A&B": 1.5 } }))

Apart from that one await init(), every function below behaves identically on the ./web entry.

Fit a diagram

import { euler } from "@jolars/eunoia";

const layout = euler({
  sets: { A: 5, B: 3, "A&B": 1.5 },
});

sets is keyed by combination expression: a single set ("A") or sets joined with & ("A&B"). By default the values are exclusive sizes (the part of A outside B is 5, the overlap is 1.5). Pass inputType: "inclusive" if your numbers are full set unions instead and eunoia will decompose them.

With no other options you get circles back:

layout.mode; // "shapes"
layout.shape; // "circle"
layout.circles;
// [
//   { label: "A", x, y, radius, labelAnchor: { x, y } },
//   { label: "B", x, y, radius, labelAnchor: { x, y } },
// ]
layout.metrics.stress; // goodness-of-fit, plus loss, fittedAreas, …

Choose what comes back

The output option decides the shape of the result. The Layout object is a union discriminated on mode:

outputYou getUse it when
"shapes" (default)circles/ellipses/squares/rectangles, the primitive paramsdrawing native <circle>/<ellipse>/<rect>
"polygons"the shapes plus polygons, one closed outline per setfilling/stroking exact set outlines as <path>
"regions"regions, the exclusive pieces (A only, A&B, …) + setAnchorsper-region fills, coloring, label placement

Every mode also carries metrics (loss, stress, fitted vs. target areas, …). See Goodness of fit for what those mean.

Render to SVG

In "shapes" mode the fields map straight onto SVG attributes. Coordinates are in abstract, centered units, so compute a bounding box and use it as the viewBox:

import { euler } from "@jolars/eunoia";

const palette = ["#4e79a7", "#f28e2b", "#59a14f"];

function renderCircles(circles, { size = 320, pad = 8 } = {}) {
  const minX = Math.min(...circles.map((c) => c.x - c.radius));
  const minY = Math.min(...circles.map((c) => c.y - c.radius));
  const maxX = Math.max(...circles.map((c) => c.x + c.radius));
  const maxY = Math.max(...circles.map((c) => c.y + c.radius));
  const w = maxX - minX;
  const h = maxY - minY;

  const shapes = circles
    .map(
      (c, i) =>
        `<circle cx="${c.x}" cy="${c.y}" r="${c.radius}" ` +
        `fill="${palette[i % palette.length]}" fill-opacity="0.5" />`,
    )
    .join("");

  const labels = circles
    .map(
      (c) =>
        `<text x="${c.labelAnchor.x}" y="${c.labelAnchor.y}" ` +
        `text-anchor="middle" dominant-baseline="central" ` +
        `font-size="${0.07 * Math.max(w, h)}">${c.label}</text>`,
    )
    .join("");

  return (
    `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" ` +
    `viewBox="${minX - pad} ${minY - pad} ${w + 2 * pad} ${h + 2 * pad}">` +
    `${shapes}${labels}</svg>`
  );
}

const layout = euler({ sets: { A: 5, B: 3, "A&B": 1.5 } });
document.body.innerHTML = renderCircles(layout.circles);

Or use the bundled serializer: the @jolars/eunoia/svg entry is wasm-free and DOM-free, and turns a Layout straight into SVG. Use toSvg for a complete document, or svgBody + viewBox when you want to own the <svg> element:

import { euler } from "@jolars/eunoia";
import { toSvg } from "@jolars/eunoia/svg";

const layout = euler({ sets: { A: 5, B: 3, "A&B": 1.5 }, output: "regions" });
document.body.innerHTML = toSvg(layout, { showLabels: true });

Because it never touches WebAssembly or the DOM, ./svg works in server-side rendering and from a CDN as-is.

ellipses carry semiMajor, semiMinor, and a rotation (radians); squares/rectangles carry side or width/height centered on x, y.

For exact overlap shading rather than translucent stacking, switch to output: "regions", give each region its own fill from its combination, and draw each piece’s outer ring (plus any holes) as one <path>. See the Fitter pipeline chapter for how regions are derived.

Pick a different shape

euler({ sets: { A: 5, B: 3, "A&B": 1.5 }, shape: "ellipse" });

shape is one of "circle", "ellipse", "square", "rectangle". Ellipses have more freedom than circles and often fit three or more sets noticeably better; see Shapes.

Venn diagrams

For a fixed, non-proportional Venn template (the classic overlapping outlines), use venn instead; it ignores set sizes and just lays out n sets:

import { venn } from "@jolars/eunoia";

const layout = venn({ n: 3 }); // ellipses, 1 ≤ n ≤ 5
layout.polygons; // one outline per set; output defaults to "polygons"

Pick the shape with shape. "ellipse" (default) is the only one that reaches n = 5; "circle" gives the classic 1–3-circle diagrams, and "square"/"rectangle" are axis-aligned. The non-ellipse shapes cap at n = 3:

venn({ n: 3, shape: "circle" }); // classic three-circle Venn

Labels

Interior labels come for free: each shape (and each region) carries a labelAnchor, and "regions" mode adds setAnchors. When a region is too small to hold its label, eunoia can place it outside with a leader line via placeLabelsForRegions. That involves measuring rendered text and (often) a resize loop, covered in Label placement.

All three entry points ship from the one package: @jolars/eunoia (the bundler default), @jolars/eunoia/web (bundler-less, init()), and @jolars/eunoia/svg (wasm-free SVG). For the complete option and return-type listing, see the TypeScript definitions shipped with the npm package.

Documentation for Eunoia v1.7.0