Quickstart: Rust

Eunoia is the pure-Rust core. Every other binding (JavaScript, R, Python, Julia) wraps the same engine. You describe a diagram with set sizes, pick a shape type, and fit. You get back fitted shapes plus goodness-of-fit metrics and, optionally, the region polygons to draw.

The diagram fitted by the first example below.

Install

cargo add eunoia

Or add it to Cargo.toml directly:

[dependencies]
eunoia = "1.7"

The crate is pure Rust with no system dependencies. Edition 2024, MSRV 1.88.

Fit a diagram

Building a diagram is two steps: describe what to draw with DiagramSpecBuilder (shape-agnostic), then fit it with Fitter<S>, choosing the shape type S at fit time.

use eunoia::geometry::shapes::Circle;
use eunoia::{DiagramSpecBuilder, Fitter, InputType};

let spec = DiagramSpecBuilder::new()
    .set("A", 5.0)
    .set("B", 3.0)
    .intersection(&["A", "B"], 1.5)
    .input_type(InputType::Exclusive)
    .build()
    .expect("valid spec");

let layout = Fitter::<Circle>::new(&spec).seed(42).fit().expect("fit");

.set names a set and its size; .intersection names the size of an overlap. Values are exclusive by default: the part of A outside B is 5, the overlap is 1.5. Pass InputType::Inclusive instead if your numbers are full set unions and eunoia will decompose them.

.seed(u64) makes the (stochastic, multi-restart) fit reproducible; omit it for a fresh layout each run.

Read the result

fit() returns a Layout<S> carrying the fitted shapes and fit metrics:

for circle in layout.shapes() {
    // Circle { center, radius, .. }; see geometry::shapes
    println!("{:?}", circle);
}

layout.loss();        // objective value (lower is better)
layout.stress();      // goodness-of-fit, à la venneuler
layout.diag_error();  // largest single-region error
layout.iterations();  // optimizer iterations of the kept restart

layout.requested();   // target per-region areas
layout.fitted();      // achieved per-region areas

Pick a different shape

The shape is the generic parameter on Fitter; the spec doesn’t change:

use eunoia::geometry::shapes::Ellipse;

let layout = Fitter::<Ellipse>::new(&spec).fit().expect("fit");

Circle, Ellipse, Square, and Rectangle all live in eunoia::geometry::shapes. Ellipses have more freedom than circles and often fit three or more sets noticeably better; see Shapes.

Regions for rendering

To draw the diagram, decompose it into the exclusive regions (A only, A&B, …) as polygons. Each region comes back as one or more polygons sampled at the given vertex count:

let regions = layout.region_polygons(&spec, 256);
for (combination, polygons) in &regions {
    // combination is a RegionMask; polygons are closed rings to fill/stroke
}

Complement (the “universe”)

To fit a bounding container whose leftover area matches a target (items outside every set), opt in with .complement:

let spec = DiagramSpecBuilder::new()
    .set("A", 5.0)
    .set("B", 3.0)
    .intersection(&["A", "B"], 1.5)
    .complement(4.0)
    .build()
    .expect("valid spec");

let layout = Fitter::<Circle>::new(&spec).fit().expect("fit");
layout.container(); // Some(rect): the jointly-fitted bounding box

See Complement for what this means.

Next steps

The full API (every builder method, optimizer, loss function, and shape trait) is on docs.rs/eunoia. The in-tree examples/ directory has runnable programs (e.g. plotting_demo.rs). For how the fit actually works, read the Fitter pipeline and Goodness of fit chapters.

Documentation for Eunoia v1.7.0