Skip to main content

merman_render/
venn.rs

1//! Source-backed Venn layout kernel and diagram adapter.
2//!
3//! This module ports the layout/geometry path used by `@upsetjs/venn.js@2.0.0` and the minimal
4//! `fmin@0.0.4` optimizer helpers it depends on. The diagram adapter is intentionally thin: it
5//! projects the core render model into the same helper layout surface that Mermaid consumes before
6//! SVG emission.
7
8use crate::model::{
9    Bounds as LayoutBounds, VennAreaLayout, VennCircleLayout, VennDiagramLayout,
10    VennTextAreaLayout, VennTextDebugCellLayout, VennTextNodeLayout,
11};
12use crate::{Error, Result};
13use indexmap::IndexMap;
14use merman_core::diagrams::venn::VennDiagramRenderModel;
15use ryu_js::Buffer;
16use std::collections::{HashMap, HashSet};
17use std::f64::consts::{PI, TAU};
18
19const SMALL: f64 = 1e-10;
20const REFERENCE_WIDTH: f64 = 1600.0;
21
22mod config;
23
24use config::VennConfigView;
25
26pub fn layout_venn_diagram(
27    semantic: &serde_json::Value,
28    diagram_title: Option<&str>,
29    effective_config: &serde_json::Value,
30) -> Result<VennDiagramLayout> {
31    let model: VennDiagramRenderModel = crate::json::from_value_ref(semantic)?;
32    layout_venn_diagram_typed(&model, diagram_title, effective_config)
33}
34
35pub fn layout_venn_diagram_typed(
36    model: &VennDiagramRenderModel,
37    diagram_title: Option<&str>,
38    effective_config: &serde_json::Value,
39) -> Result<VennDiagramLayout> {
40    let cfg = VennConfigView::new(effective_config).layout_settings();
41    let title = model
42        .title
43        .as_deref()
44        .map(str::trim)
45        .filter(|title| !title.is_empty())
46        .or_else(|| {
47            diagram_title
48                .map(str::trim)
49                .filter(|title| !title.is_empty())
50        });
51    let scale = cfg.width / REFERENCE_WIDTH;
52    let title_height = if title.is_some() { 48.0 * scale } else { 0.0 };
53    let diagram_height = (cfg.height - title_height).max(1.0);
54
55    let areas = model
56        .subsets
57        .iter()
58        .map(|subset| VennArea {
59            sets: subset.sets.clone(),
60            size: subset.size,
61            weight: None,
62            label: subset.label.clone(),
63        })
64        .collect::<Vec<_>>();
65    let layout_areas = if areas.is_empty() {
66        Vec::new()
67    } else {
68        compute_venn_layout(
69            &areas,
70            &VennLayoutOptions {
71                width: cfg.width,
72                height: diagram_height,
73                padding: cfg.padding,
74                ..Default::default()
75            },
76        )
77        .map_err(|err| Error::InvalidModel {
78            message: err.to_string(),
79        })?
80    };
81
82    let areas = layout_areas
83        .iter()
84        .map(|area| VennAreaLayout {
85            sets: area.data.sets.clone(),
86            size: area.data.size,
87            label: area.data.label.clone(),
88            text_x: area.text.x.floor(),
89            text_y: area.text.y.floor(),
90            text_disjoint: area.text.disjoint,
91            circles: area
92                .circles
93                .iter()
94                .map(|circle| VennCircleLayout {
95                    set: circle.set.clone(),
96                    x: circle.x,
97                    y: circle.y,
98                    radius: circle.radius,
99                })
100                .collect(),
101            path: area.path.clone(),
102            distinct_path: area.distinct_path.clone(),
103        })
104        .collect::<Vec<_>>();
105    let layout_by_key = layout_areas
106        .iter()
107        .map(|area| (stable_sets_key(&area.data.sets), area))
108        .collect::<HashMap<_, _>>();
109    let (text_areas, text_nodes) =
110        layout_text_nodes(model, &layout_by_key, scale, cfg.use_debug_layout);
111
112    Ok(VennDiagramLayout {
113        bounds: Some(LayoutBounds {
114            min_x: 0.0,
115            min_y: 0.0,
116            max_x: cfg.width,
117            max_y: cfg.height,
118        }),
119        width: cfg.width,
120        height: cfg.height,
121        diagram_height,
122        title_height,
123        scale,
124        padding: cfg.padding,
125        use_max_width: cfg.use_max_width,
126        use_debug_layout: cfg.use_debug_layout,
127        areas,
128        text_areas,
129        text_nodes,
130    })
131}
132
133fn layout_text_nodes(
134    model: &VennDiagramRenderModel,
135    layout_by_key: &HashMap<String, &VennLayoutArea>,
136    scale: f64,
137    use_debug_layout: bool,
138) -> (Vec<VennTextAreaLayout>, Vec<VennTextNodeLayout>) {
139    let mut nodes_by_area: IndexMap<
140        String,
141        Vec<&merman_core::diagrams::venn::VennTextNodeRenderModel>,
142    > = IndexMap::new();
143    for node in &model.text_nodes {
144        nodes_by_area
145            .entry(stable_sets_key(&node.sets))
146            .or_default()
147            .push(node);
148    }
149
150    let mut text_areas = Vec::new();
151    let mut text_nodes = Vec::new();
152    for (key, nodes) in nodes_by_area {
153        let Some(area) = layout_by_key.get(&key).copied() else {
154            continue;
155        };
156        if area.circles.is_empty() {
157            continue;
158        }
159
160        let center_x = area.text.x;
161        let center_y = area.text.y;
162        let min_circle_radius = area
163            .circles
164            .iter()
165            .map(|circle| circle.radius)
166            .fold(f64::INFINITY, f64::min);
167        let inner_radius_raw = area
168            .circles
169            .iter()
170            .map(|circle| {
171                circle.radius
172                    - ((center_x - circle.x).powi(2) + (center_y - circle.y).powi(2)).sqrt()
173            })
174            .fold(f64::INFINITY, f64::min);
175        let mut inner_radius = if inner_radius_raw.is_finite() {
176            inner_radius_raw.max(0.0)
177        } else {
178            0.0
179        };
180        if inner_radius == 0.0 && min_circle_radius.is_finite() {
181            inner_radius = min_circle_radius * 0.6;
182        }
183
184        let inner_width = (80.0 * scale).max(inner_radius * 2.0 * 0.95);
185        let inner_height = (60.0 * scale).max(inner_radius * 2.0 * 0.95);
186        let has_label = area
187            .data
188            .label
189            .as_deref()
190            .is_some_and(|label| !label.is_empty());
191        let label_offset_base = if has_label {
192            (32.0 * scale).min(inner_radius * 0.25)
193        } else {
194            0.0
195        };
196        let label_offset = label_offset_base + if nodes.len() <= 2 { 30.0 * scale } else { 0.0 };
197        let start_x = center_x - inner_width / 2.0;
198        let start_y = center_y - inner_height / 2.0 + label_offset;
199        let cols = (nodes.len() as f64).sqrt().ceil().max(1.0) as usize;
200        let rows = nodes.len().div_ceil(cols).max(1);
201        let cell_width = inner_width / cols as f64;
202        let cell_height = inner_height / rows as f64;
203
204        let mut debug_cells = Vec::new();
205        for (index, node) in nodes.iter().enumerate() {
206            let col = index % cols;
207            let row = index / cols;
208            let cell_x = start_x + cell_width * col as f64;
209            let cell_y = start_y + cell_height * row as f64;
210            if use_debug_layout {
211                debug_cells.push(VennTextDebugCellLayout {
212                    x: cell_x,
213                    y: cell_y,
214                    width: cell_width,
215                    height: cell_height,
216                });
217            }
218
219            let x = start_x + cell_width * (col as f64 + 0.5);
220            let y = start_y + cell_height * (row as f64 + 0.5);
221            let box_width = cell_width * 0.9;
222            let box_height = cell_height * 0.9;
223            text_nodes.push(VennTextNodeLayout {
224                sets: node.sets.clone(),
225                id: node.id.clone(),
226                label: node.label.clone(),
227                x: x - box_width / 2.0,
228                y: y - box_height / 2.0,
229                width: box_width,
230                height: box_height,
231            });
232        }
233
234        text_areas.push(VennTextAreaLayout {
235            sets: area.data.sets.clone(),
236            center_x,
237            center_y,
238            inner_radius,
239            font_size: 40.0 * scale,
240            debug_cells,
241        });
242    }
243
244    (text_areas, text_nodes)
245}
246
247fn stable_sets_key(sets: &[String]) -> String {
248    sets.join("|")
249}
250
251#[derive(Debug, Clone, PartialEq)]
252pub struct VennArea {
253    pub sets: Vec<String>,
254    pub size: f64,
255    pub weight: Option<f64>,
256    pub label: Option<String>,
257}
258
259impl VennArea {
260    pub fn new(sets: impl IntoIterator<Item = impl Into<String>>, size: f64) -> Self {
261        Self {
262            sets: sets.into_iter().map(Into::into).collect(),
263            size,
264            weight: None,
265            label: None,
266        }
267    }
268}
269
270#[derive(Debug, Clone, Copy, PartialEq)]
271pub struct VennPoint {
272    pub x: f64,
273    pub y: f64,
274}
275
276#[derive(Debug, Clone, PartialEq)]
277pub struct VennCircle {
278    pub set: String,
279    pub x: f64,
280    pub y: f64,
281    pub radius: f64,
282}
283
284#[derive(Debug, Clone, PartialEq)]
285pub struct VennArc {
286    pub circle: VennCircle,
287    pub width: f64,
288    pub p1: VennPoint,
289    pub p2: VennPoint,
290    pub large: bool,
291    pub sweep: bool,
292}
293
294#[derive(Debug, Clone, Copy, PartialEq)]
295pub struct VennTextPoint {
296    pub x: f64,
297    pub y: f64,
298    pub disjoint: bool,
299}
300
301#[derive(Debug, Clone, PartialEq)]
302pub struct VennLayoutArea {
303    pub data: VennArea,
304    pub text: VennTextPoint,
305    pub circles: Vec<VennCircle>,
306    pub arcs: Vec<VennArc>,
307    pub path: String,
308    pub distinct_path: String,
309}
310
311#[derive(Debug, Clone)]
312pub struct VennLayoutOptions {
313    pub width: f64,
314    pub height: f64,
315    pub padding: f64,
316    pub normalize: bool,
317    pub orientation: f64,
318    pub scale_to_fit: Option<f64>,
319    pub symmetrical_text_centre: bool,
320    pub distinct: bool,
321    pub round: Option<usize>,
322    pub max_iterations: usize,
323    pub restarts: usize,
324    pub random_seed: u64,
325}
326
327impl Default for VennLayoutOptions {
328    fn default() -> Self {
329        Self {
330            width: 600.0,
331            height: 350.0,
332            padding: 15.0,
333            normalize: true,
334            orientation: PI / 2.0,
335            scale_to_fit: None,
336            symmetrical_text_centre: false,
337            distinct: false,
338            // `diagram.js` helper defaults `round` to 2.
339            round: Some(2),
340            max_iterations: 500,
341            restarts: 10,
342            random_seed: 1,
343        }
344    }
345}
346
347#[derive(Debug, thiserror::Error)]
348pub enum VennLayoutError {
349    #[error("missing pairwise overlap information for Venn set `{0}`")]
350    MissingPairwiseOverlap(String),
351    #[error("Venn layout requires at least one single-set area")]
352    EmptySingleSetAreas,
353}
354
355pub type VennLayoutResult<T> = std::result::Result<T, VennLayoutError>;
356pub type VennSolution = IndexMap<String, VennCircle>;
357
358type VennResult<T> = VennLayoutResult<T>;
359type Solution = VennSolution;
360
361#[derive(Debug, Clone)]
362struct AreaStats {
363    area: f64,
364    arcs: Vec<VennArc>,
365}
366
367#[derive(Debug, Clone)]
368struct IntersectionPoint {
369    point: VennPoint,
370    parent_index: [usize; 2],
371    angle: f64,
372}
373
374#[derive(Debug, Clone, Copy)]
375struct Bounds {
376    x_min: f64,
377    x_max: f64,
378    y_min: f64,
379    y_max: f64,
380}
381
382#[derive(Debug, Clone)]
383struct OptimParams {
384    max_iterations: Option<usize>,
385    min_error_delta: Option<f64>,
386}
387
388#[derive(Debug, Clone)]
389struct OptimResult {
390    x: Vec<f64>,
391}
392
393#[derive(Debug, Clone)]
394struct CgState {
395    x: Vec<f64>,
396    fx: f64,
397    fxprime: Vec<f64>,
398}
399
400#[derive(Debug, Clone)]
401struct XorShift64Star {
402    state: u64,
403}
404
405impl XorShift64Star {
406    fn new(seed: u64) -> Self {
407        Self { state: seed.max(1) }
408    }
409
410    fn next_u64(&mut self) -> u64 {
411        let mut x = self.state;
412        x ^= x >> 12;
413        x ^= x << 25;
414        x ^= x >> 27;
415        self.state = x;
416        x.wrapping_mul(0x2545F4914F6CDD1D_u64)
417    }
418
419    fn next_f64_unit(&mut self) -> f64 {
420        let u = self.next_u64() >> 11;
421        (u as f64) / ((1u64 << 53) as f64)
422    }
423}
424
425pub fn compute_venn_layout(
426    data: &[VennArea],
427    options: &VennLayoutOptions,
428) -> VennLayoutResult<Vec<VennLayoutArea>> {
429    let mut solution = venn(data, options)?;
430    if options.normalize {
431        solution = normalize_solution(&solution, options.orientation);
432    }
433    let circles = scale_solution(
434        &solution,
435        options.width,
436        options.height,
437        options.padding,
438        options.scale_to_fit,
439    );
440    let text_centres = compute_text_centres(&circles, data, options.symmetrical_text_centre);
441
442    let mut helpers = Vec::with_capacity(data.len());
443    for area in data {
444        let area_circles: Vec<VennCircle> = area
445            .sets
446            .iter()
447            .filter_map(|set| circles.get(set).cloned())
448            .collect();
449        let arcs = intersection_area_arcs(&area_circles);
450        let path = arcs_to_path(&arcs, options.round);
451        let text = text_centres
452            .get(&sets_key(&area.sets))
453            .copied()
454            .unwrap_or(VennTextPoint {
455                x: 0.0,
456                y: 0.0,
457                disjoint: true,
458            });
459        helpers.push(VennLayoutArea {
460            data: area.clone(),
461            text,
462            circles: area_circles,
463            arcs,
464            path,
465            distinct_path: String::new(),
466        });
467    }
468
469    for i in 0..helpers.len() {
470        let mut distinct_path = helpers[i].path.clone();
471        for j in 0..helpers.len() {
472            if helpers[j].data.sets.len() > helpers[i].data.sets.len()
473                && helpers[i]
474                    .data
475                    .sets
476                    .iter()
477                    .all(|set| helpers[j].data.sets.contains(set))
478            {
479                distinct_path.push(' ');
480                distinct_path.push_str(&helpers[j].path);
481            }
482        }
483        helpers[i].distinct_path = distinct_path;
484    }
485
486    Ok(helpers)
487}
488
489pub fn venn(sets: &[VennArea], options: &VennLayoutOptions) -> VennLayoutResult<VennSolution> {
490    let areas = add_missing_areas(sets, options.distinct);
491    let mut circles = best_initial_layout(&areas, options)?;
492    let setids: Vec<String> = circles.keys().cloned().collect();
493    let mut initial = Vec::with_capacity(setids.len() * 2);
494    for setid in &setids {
495        let circle = &circles[setid];
496        initial.push(circle.x);
497        initial.push(circle.y);
498    }
499
500    let params = OptimParams {
501        max_iterations: Some(options.max_iterations),
502        min_error_delta: None,
503    };
504    let solution = nelder_mead(
505        |values| {
506            let mut current = Solution::new();
507            for (i, setid) in setids.iter().enumerate() {
508                let base = &circles[setid];
509                current.insert(
510                    setid.clone(),
511                    VennCircle {
512                        set: setid.clone(),
513                        x: values[2 * i],
514                        y: values[2 * i + 1],
515                        radius: base.radius,
516                    },
517                );
518            }
519            loss_function(&current, &areas)
520        },
521        &initial,
522        &params,
523    );
524
525    for (i, setid) in setids.iter().enumerate() {
526        if let Some(circle) = circles.get_mut(setid) {
527            circle.x = solution.x[2 * i];
528            circle.y = solution.x[2 * i + 1];
529        }
530    }
531    Ok(circles)
532}
533
534fn add_missing_areas(areas: &[VennArea], distinct: bool) -> Vec<VennArea> {
535    let mut out = areas.to_vec();
536
537    if distinct {
538        let mut count: HashMap<String, f64> = HashMap::new();
539        for area in &out {
540            for i in 0..area.sets.len() {
541                let si = &area.sets[i];
542                *count.entry(si.clone()).or_insert(0.0) += area.size;
543                for j in i + 1..area.sets.len() {
544                    let sj = &area.sets[j];
545                    *count.entry(format!("{si};{sj}")).or_insert(0.0) += area.size;
546                    *count.entry(format!("{sj};{si}")).or_insert(0.0) += area.size;
547                }
548            }
549        }
550        for area in &mut out {
551            if area.sets.len() < 3
552                && let Some(size) = count.get(&area.sets.join(";")).copied()
553            {
554                area.size = size;
555            }
556        }
557    }
558
559    let mut ids = Vec::new();
560    let mut pairs = HashSet::new();
561    for area in &out {
562        if area.sets.len() == 1 {
563            ids.push(area.sets[0].clone());
564        } else if area.sets.len() == 2 {
565            let a = &area.sets[0];
566            let b = &area.sets[1];
567            pairs.insert(format!("{a};{b}"));
568            pairs.insert(format!("{b};{a}"));
569        }
570    }
571
572    ids.sort();
573    for i in 0..ids.len() {
574        for j in i + 1..ids.len() {
575            let a = &ids[i];
576            let b = &ids[j];
577            if !pairs.contains(&format!("{a};{b}")) {
578                out.push(VennArea::new([a.clone(), b.clone()], 0.0));
579            }
580        }
581    }
582    out
583}
584
585pub fn distance_from_intersect_area(r1: f64, r2: f64, overlap: f64) -> f64 {
586    if r1.min(r2).powi(2) * PI <= overlap + SMALL {
587        return (r1 - r2).abs();
588    }
589    bisect(
590        |distance| circle_overlap(r1, r2, distance) - overlap,
591        0.0,
592        r1 + r2,
593        100,
594        1e-10,
595    )
596}
597
598fn best_initial_layout(areas: &[VennArea], options: &VennLayoutOptions) -> VennResult<Solution> {
599    let mut initial = greedy_layout(areas)?;
600    if areas.len() >= 8 {
601        let constrained = constrained_mds_layout(areas, options)?;
602        let constrained_loss = loss_function(&constrained, areas);
603        let greedy_loss = loss_function(&initial, areas);
604        if constrained_loss + 1e-8 < greedy_loss {
605            initial = constrained;
606        }
607    }
608    Ok(initial)
609}
610
611pub fn greedy_layout(areas: &[VennArea]) -> VennLayoutResult<VennSolution> {
612    let mut circles = Solution::new();
613    let mut set_overlaps: IndexMap<String, Vec<SetOverlap>> = IndexMap::new();
614
615    for area in areas {
616        if area.sets.len() == 1 {
617            let set = area.sets[0].clone();
618            circles.insert(
619                set.clone(),
620                VennCircle {
621                    set: set.clone(),
622                    x: 1e10,
623                    y: 1e10,
624                    radius: (area.size / PI).sqrt(),
625                },
626            );
627            set_overlaps.insert(set, Vec::new());
628        }
629    }
630    if circles.is_empty() {
631        return Err(VennLayoutError::EmptySingleSetAreas);
632    }
633
634    for area in areas.iter().filter(|a| a.sets.len() == 2) {
635        let left = &area.sets[0];
636        let right = &area.sets[1];
637        let Some(left_circle) = circles.get(left) else {
638            continue;
639        };
640        let Some(right_circle) = circles.get(right) else {
641            continue;
642        };
643        let mut weight = area.weight.unwrap_or(1.0);
644        if area.size + SMALL
645            >= (left_circle.radius * left_circle.radius * PI)
646                .min(right_circle.radius * right_circle.radius * PI)
647        {
648            weight = 0.0;
649        }
650        if let Some(overlaps) = set_overlaps.get_mut(left) {
651            overlaps.push(SetOverlap {
652                set: right.clone(),
653                size: area.size,
654                weight,
655            });
656        }
657        if let Some(overlaps) = set_overlaps.get_mut(right) {
658            overlaps.push(SetOverlap {
659                set: left.clone(),
660                size: area.size,
661                weight,
662            });
663        }
664    }
665
666    let mut most_overlapped: Vec<MostOverlapped> = set_overlaps
667        .iter()
668        .map(|(set, overlaps)| MostOverlapped {
669            set: set.clone(),
670            size: overlaps.iter().map(|o| o.size * o.weight).sum(),
671        })
672        .collect();
673    most_overlapped.sort_by(|a, b| b.size.total_cmp(&a.size));
674
675    let mut positioned = HashSet::new();
676    let first = most_overlapped[0].set.clone();
677    position_set(
678        &mut circles,
679        &mut positioned,
680        &first,
681        VennPoint { x: 0.0, y: 0.0 },
682    );
683
684    for item in most_overlapped.iter().skip(1) {
685        let set_index = &item.set;
686        let mut overlap: Vec<SetOverlap> = set_overlaps
687            .get(set_index)
688            .cloned()
689            .unwrap_or_default()
690            .into_iter()
691            .filter(|o| positioned.contains(&o.set))
692            .collect();
693        overlap.sort_by(|a, b| b.size.total_cmp(&a.size));
694        if overlap.is_empty() {
695            return Err(VennLayoutError::MissingPairwiseOverlap(set_index.clone()));
696        }
697
698        let set = circles[set_index].clone();
699        let mut points = Vec::new();
700        for j in 0..overlap.len() {
701            let p1 = &circles[&overlap[j].set];
702            let d1 = distance_from_intersect_area(set.radius, p1.radius, overlap[j].size);
703            points.push(VennPoint {
704                x: p1.x + d1,
705                y: p1.y,
706            });
707            points.push(VennPoint {
708                x: p1.x - d1,
709                y: p1.y,
710            });
711            points.push(VennPoint {
712                x: p1.x,
713                y: p1.y + d1,
714            });
715            points.push(VennPoint {
716                x: p1.x,
717                y: p1.y - d1,
718            });
719
720            for k in j + 1..overlap.len() {
721                let p2 = &circles[&overlap[k].set];
722                let d2 = distance_from_intersect_area(set.radius, p2.radius, overlap[k].size);
723                points.extend(circle_circle_intersection(
724                    &VennCircle {
725                        set: String::new(),
726                        x: p1.x,
727                        y: p1.y,
728                        radius: d1,
729                    },
730                    &VennCircle {
731                        set: String::new(),
732                        x: p2.x,
733                        y: p2.y,
734                        radius: d2,
735                    },
736                ));
737            }
738        }
739
740        let mut best_loss = 1e50;
741        let mut best_point = points[0];
742        for point in points {
743            if let Some(circle) = circles.get_mut(set_index) {
744                circle.x = point.x;
745                circle.y = point.y;
746            }
747            let local_loss = loss_function(&circles, areas);
748            if local_loss < best_loss {
749                best_loss = local_loss;
750                best_point = point;
751            }
752        }
753
754        position_set(&mut circles, &mut positioned, set_index, best_point);
755    }
756
757    Ok(circles)
758}
759
760#[derive(Debug, Clone)]
761struct SetOverlap {
762    set: String,
763    size: f64,
764    weight: f64,
765}
766
767#[derive(Debug, Clone)]
768struct MostOverlapped {
769    set: String,
770    size: f64,
771}
772
773fn position_set(
774    circles: &mut Solution,
775    positioned: &mut HashSet<String>,
776    set: &str,
777    point: VennPoint,
778) {
779    if let Some(circle) = circles.get_mut(set) {
780        circle.x = point.x;
781        circle.y = point.y;
782    }
783    positioned.insert(set.to_string());
784}
785
786fn constrained_mds_layout(areas: &[VennArea], options: &VennLayoutOptions) -> VennResult<Solution> {
787    let mut sets = Vec::new();
788    let mut setids = HashMap::new();
789    for area in areas {
790        if area.sets.len() == 1 {
791            setids.insert(area.sets[0].clone(), sets.len());
792            sets.push(area.clone());
793        }
794    }
795    if sets.is_empty() {
796        return Err(VennLayoutError::EmptySingleSetAreas);
797    }
798
799    let (mut distances, constraints) = get_distance_matrices(areas, &sets, &setids);
800    let norm =
801        norm2(&distances.iter().map(|row| norm2(row)).collect::<Vec<_>>()) / distances.len() as f64;
802    if !norm.is_finite() || norm == 0.0 {
803        return greedy_layout(areas);
804    }
805    for row in &mut distances {
806        for value in row {
807            *value /= norm;
808        }
809    }
810
811    let mut rng = XorShift64Star::new(options.random_seed);
812    let mut best: Option<CgState> = None;
813    for _ in 0..options.restarts {
814        let initial: Vec<f64> = (0..distances.len() * 2)
815            .map(|_| rng.next_f64_unit())
816            .collect();
817        let current = conjugate_gradient(
818            |x, fxprime| constrained_mds_gradient(x, fxprime, &distances, &constraints),
819            &initial,
820            Some(options.max_iterations),
821        );
822        if best.as_ref().is_none_or(|b| current.fx < b.fx) {
823            best = Some(current);
824        }
825    }
826
827    let positions = best
828        .map(|b| b.x)
829        .unwrap_or_else(|| vec![0.0; distances.len() * 2]);
830    let mut circles = Solution::new();
831    for (i, set) in sets.iter().enumerate() {
832        let setid = set.sets[0].clone();
833        circles.insert(
834            setid.clone(),
835            VennCircle {
836                set: setid,
837                x: positions[2 * i] * norm,
838                y: positions[2 * i + 1] * norm,
839                radius: (set.size / PI).sqrt(),
840            },
841        );
842    }
843    Ok(circles)
844}
845
846fn get_distance_matrices(
847    areas: &[VennArea],
848    sets: &[VennArea],
849    setids: &HashMap<String, usize>,
850) -> (Vec<Vec<f64>>, Vec<Vec<i8>>) {
851    let mut distances = vec![vec![0.0; sets.len()]; sets.len()];
852    let mut constraints = vec![vec![0; sets.len()]; sets.len()];
853
854    for current in areas.iter().filter(|a| a.sets.len() == 2) {
855        let Some(&left) = setids.get(&current.sets[0]) else {
856            continue;
857        };
858        let Some(&right) = setids.get(&current.sets[1]) else {
859            continue;
860        };
861        let r1 = (sets[left].size / PI).sqrt();
862        let r2 = (sets[right].size / PI).sqrt();
863        let distance = distance_from_intersect_area(r1, r2, current.size);
864        distances[left][right] = distance;
865        distances[right][left] = distance;
866
867        let mut constraint = 0;
868        if current.size + SMALL >= sets[left].size.min(sets[right].size) {
869            constraint = 1;
870        } else if current.size <= SMALL {
871            constraint = -1;
872        }
873        constraints[left][right] = constraint;
874        constraints[right][left] = constraint;
875    }
876
877    (distances, constraints)
878}
879
880fn constrained_mds_gradient(
881    x: &[f64],
882    fxprime: &mut [f64],
883    distances: &[Vec<f64>],
884    constraints: &[Vec<i8>],
885) -> f64 {
886    fxprime.fill(0.0);
887    let mut loss = 0.0;
888    for i in 0..distances.len() {
889        let xi = x[2 * i];
890        let yi = x[2 * i + 1];
891        for j in i + 1..distances.len() {
892            let xj = x[2 * j];
893            let yj = x[2 * j + 1];
894            let dij = distances[i][j];
895            let constraint = constraints[i][j];
896            let squared_distance = (xj - xi).powi(2) + (yj - yi).powi(2);
897            let distance = squared_distance.sqrt();
898            let delta = squared_distance - dij * dij;
899
900            if (constraint > 0 && distance <= dij) || (constraint < 0 && distance >= dij) {
901                continue;
902            }
903
904            loss += 2.0 * delta * delta;
905            fxprime[2 * i] += 4.0 * delta * (xi - xj);
906            fxprime[2 * i + 1] += 4.0 * delta * (yi - yj);
907            fxprime[2 * j] += 4.0 * delta * (xj - xi);
908            fxprime[2 * j + 1] += 4.0 * delta * (yj - yi);
909        }
910    }
911    loss
912}
913
914pub fn loss_function(circles: &VennSolution, overlaps: &[VennArea]) -> f64 {
915    let mut output = 0.0;
916    for area in overlaps {
917        if area.sets.len() == 1 {
918            continue;
919        }
920        let overlap = if area.sets.len() == 2 {
921            let Some(left) = circles.get(&area.sets[0]) else {
922                continue;
923            };
924            let Some(right) = circles.get(&area.sets[1]) else {
925                continue;
926            };
927            circle_overlap(left.radius, right.radius, distance(left, right))
928        } else {
929            let area_circles: Vec<VennCircle> = area
930                .sets
931                .iter()
932                .filter_map(|set| circles.get(set).cloned())
933                .collect();
934            intersection_area(&area_circles)
935        };
936        let weight = area.weight.unwrap_or(1.0);
937        output += weight * (overlap - area.size) * (overlap - area.size);
938    }
939    output
940}
941
942pub fn normalize_solution(solution: &VennSolution, orientation: f64) -> VennSolution {
943    let circles: Vec<VennCircle> = solution.values().cloned().collect();
944    let mut clusters: Vec<Cluster> = disjoint_cluster(circles)
945        .into_iter()
946        .map(|mut circles| {
947            orientate_circles(&mut circles, orientation);
948            let bounds = get_bounding_box(&circles);
949            let size = (bounds.x_max - bounds.x_min) * (bounds.y_max - bounds.y_min);
950            Cluster {
951                circles,
952                bounds,
953                size,
954            }
955        })
956        .collect();
957
958    if clusters.is_empty() {
959        return Solution::new();
960    }
961    clusters.sort_by(|a, b| b.size.total_cmp(&a.size));
962    let mut circles = clusters[0].circles.clone();
963    let mut return_bounds = clusters[0].bounds;
964    let spacing = (return_bounds.x_max - return_bounds.x_min) / 50.0;
965
966    let mut index = 1;
967    while index < clusters.len() {
968        add_cluster(
969            clusters.get(index),
970            true,
971            false,
972            &mut circles,
973            &return_bounds,
974            spacing,
975        );
976        add_cluster(
977            clusters.get(index + 1),
978            false,
979            true,
980            &mut circles,
981            &return_bounds,
982            spacing,
983        );
984        add_cluster(
985            clusters.get(index + 2),
986            true,
987            true,
988            &mut circles,
989            &return_bounds,
990            spacing,
991        );
992        index += 3;
993        return_bounds = get_bounding_box(&circles);
994    }
995
996    circles_to_solution(circles)
997}
998
999#[derive(Debug, Clone)]
1000struct Cluster {
1001    circles: Vec<VennCircle>,
1002    bounds: Bounds,
1003    size: f64,
1004}
1005
1006fn add_cluster(
1007    cluster: Option<&Cluster>,
1008    right: bool,
1009    bottom: bool,
1010    circles: &mut Vec<VennCircle>,
1011    return_bounds: &Bounds,
1012    spacing: f64,
1013) {
1014    let Some(cluster) = cluster else {
1015        return;
1016    };
1017    let bounds = cluster.bounds;
1018    let mut x_offset = if right {
1019        return_bounds.x_max - bounds.x_min + spacing
1020    } else {
1021        let mut offset = return_bounds.x_max - bounds.x_max;
1022        let centering =
1023            (bounds.x_max - bounds.x_min) / 2.0 - (return_bounds.x_max - return_bounds.x_min) / 2.0;
1024        if centering < 0.0 {
1025            offset += centering;
1026        }
1027        offset
1028    };
1029    let mut y_offset = if bottom {
1030        return_bounds.y_max - bounds.y_min + spacing
1031    } else {
1032        let mut offset = return_bounds.y_max - bounds.y_max;
1033        let centering =
1034            (bounds.y_max - bounds.y_min) / 2.0 - (return_bounds.y_max - return_bounds.y_min) / 2.0;
1035        if centering < 0.0 {
1036            offset += centering;
1037        }
1038        offset
1039    };
1040    if !x_offset.is_finite() {
1041        x_offset = 0.0;
1042    }
1043    if !y_offset.is_finite() {
1044        y_offset = 0.0;
1045    }
1046    for circle in &cluster.circles {
1047        let mut c = circle.clone();
1048        c.x += x_offset;
1049        c.y += y_offset;
1050        circles.push(c);
1051    }
1052}
1053
1054fn orientate_circles(circles: &mut [VennCircle], orientation: f64) {
1055    circles.sort_by(|a, b| b.radius.total_cmp(&a.radius));
1056    if let Some(largest) = circles.first().cloned() {
1057        for circle in circles.iter_mut() {
1058            circle.x -= largest.x;
1059            circle.y -= largest.y;
1060        }
1061    }
1062
1063    if circles.len() == 2 {
1064        let dist = distance(&circles[0], &circles[1]);
1065        if dist < (circles[1].radius - circles[0].radius).abs() {
1066            circles[1].x = circles[0].x + circles[0].radius - circles[1].radius - SMALL;
1067            circles[1].y = circles[0].y;
1068        }
1069    }
1070
1071    if circles.len() > 1 {
1072        let rotation = circles[1].x.atan2(circles[1].y) - orientation;
1073        let c = rotation.cos();
1074        let s = rotation.sin();
1075        for circle in circles.iter_mut() {
1076            let x = circle.x;
1077            let y = circle.y;
1078            circle.x = c * x - s * y;
1079            circle.y = s * x + c * y;
1080        }
1081    }
1082
1083    if circles.len() > 2 {
1084        let mut angle = circles[2].x.atan2(circles[2].y) - orientation;
1085        while angle < 0.0 {
1086            angle += TAU;
1087        }
1088        while angle > TAU {
1089            angle -= TAU;
1090        }
1091        if angle > PI {
1092            let slope = circles[1].y / (SMALL + circles[1].x);
1093            for circle in circles.iter_mut() {
1094                let d = (circle.x + slope * circle.y) / (1.0 + slope * slope);
1095                circle.x = 2.0 * d - circle.x;
1096                circle.y = 2.0 * d * slope - circle.y;
1097            }
1098        }
1099    }
1100}
1101
1102pub fn disjoint_cluster(circles: Vec<VennCircle>) -> Vec<Vec<VennCircle>> {
1103    let n = circles.len();
1104    let mut parent: Vec<usize> = (0..n).collect();
1105
1106    fn find(parent: &mut [usize], x: usize) -> usize {
1107        if parent[x] != x {
1108            parent[x] = find(parent, parent[x]);
1109        }
1110        parent[x]
1111    }
1112
1113    for i in 0..n {
1114        for j in i + 1..n {
1115            if distance(&circles[i], &circles[j]) + SMALL < circles[i].radius + circles[j].radius {
1116                let x_root = find(&mut parent, j);
1117                let y_root = find(&mut parent, i);
1118                parent[x_root] = y_root;
1119            }
1120        }
1121    }
1122
1123    let mut order = Vec::new();
1124    let mut grouped: IndexMap<usize, Vec<VennCircle>> = IndexMap::new();
1125    for (i, circle) in circles.iter().enumerate().take(n) {
1126        let root = find(&mut parent, i);
1127        if !grouped.contains_key(&root) {
1128            order.push(root);
1129        }
1130        grouped.entry(root).or_default().push(circle.clone());
1131    }
1132    order
1133        .into_iter()
1134        .filter_map(|root| grouped.shift_remove(&root))
1135        .collect()
1136}
1137
1138pub fn scale_solution(
1139    solution: &VennSolution,
1140    width: f64,
1141    height: f64,
1142    padding: f64,
1143    scale_to_fit: Option<f64>,
1144) -> VennSolution {
1145    let circles: Vec<VennCircle> = solution.values().cloned().collect();
1146    let width = width - 2.0 * padding;
1147    let height = height - 2.0 * padding;
1148    let bounds = get_bounding_box(&circles);
1149    if bounds.x_max == bounds.x_min || bounds.y_max == bounds.y_min {
1150        return solution.clone();
1151    }
1152
1153    let (x_scaling, y_scaling) = if let Some(scale_to_fit) = scale_to_fit {
1154        let to_scale_diameter = (scale_to_fit / PI).sqrt() * 2.0;
1155        (width / to_scale_diameter, height / to_scale_diameter)
1156    } else {
1157        (
1158            width / (bounds.x_max - bounds.x_min),
1159            height / (bounds.y_max - bounds.y_min),
1160        )
1161    };
1162    let scaling = y_scaling.min(x_scaling);
1163    let x_offset = (width - (bounds.x_max - bounds.x_min) * scaling) / 2.0;
1164    let y_offset = (height - (bounds.y_max - bounds.y_min) * scaling) / 2.0;
1165
1166    circles_to_solution(
1167        circles
1168            .into_iter()
1169            .map(|circle| VennCircle {
1170                set: circle.set,
1171                radius: scaling * circle.radius,
1172                x: padding + x_offset + (circle.x - bounds.x_min) * scaling,
1173                y: padding + y_offset + (circle.y - bounds.y_min) * scaling,
1174            })
1175            .collect(),
1176    )
1177}
1178
1179fn get_bounding_box(circles: &[VennCircle]) -> Bounds {
1180    let mut bounds = Bounds {
1181        x_min: f64::INFINITY,
1182        x_max: f64::NEG_INFINITY,
1183        y_min: f64::INFINITY,
1184        y_max: f64::NEG_INFINITY,
1185    };
1186    for circle in circles {
1187        bounds.x_min = bounds.x_min.min(circle.x - circle.radius);
1188        bounds.x_max = bounds.x_max.max(circle.x + circle.radius);
1189        bounds.y_min = bounds.y_min.min(circle.y - circle.radius);
1190        bounds.y_max = bounds.y_max.max(circle.y + circle.radius);
1191    }
1192    bounds
1193}
1194
1195fn circles_to_solution(circles: Vec<VennCircle>) -> Solution {
1196    let mut solution = Solution::new();
1197    for circle in circles {
1198        solution.insert(circle.set.clone(), circle);
1199    }
1200    solution
1201}
1202
1203pub fn intersection_area(circles: &[VennCircle]) -> f64 {
1204    intersection_area_stats(circles).area
1205}
1206
1207fn intersection_area_stats(circles: &[VennCircle]) -> AreaStats {
1208    if circles.is_empty() {
1209        return AreaStats {
1210            area: 0.0,
1211            arcs: Vec::new(),
1212        };
1213    }
1214
1215    let intersection_points = get_intersection_points(circles);
1216    let mut inner_points: Vec<IntersectionPoint> = intersection_points
1217        .into_iter()
1218        .filter(|p| contained_in_circles(p.point, circles))
1219        .collect();
1220
1221    let mut arc_area = 0.0;
1222    let mut polygon_area = 0.0;
1223    let mut arcs = Vec::new();
1224
1225    if inner_points.len() > 1 {
1226        let center = get_center(inner_points.iter().map(|p| p.point));
1227        for point in &mut inner_points {
1228            point.angle = (point.point.x - center.x).atan2(point.point.y - center.y);
1229        }
1230        inner_points.sort_by(|a, b| b.angle.total_cmp(&a.angle));
1231
1232        let mut p2 = inner_points[inner_points.len() - 1].clone();
1233        for p1 in &inner_points {
1234            polygon_area += (p2.point.x + p1.point.x) * (p1.point.y - p2.point.y);
1235            let mid_point = VennPoint {
1236                x: (p1.point.x + p2.point.x) / 2.0,
1237                y: (p1.point.y + p2.point.y) / 2.0,
1238            };
1239            let mut arc: Option<VennArc> = None;
1240
1241            for parent in p1.parent_index {
1242                if !p2.parent_index.contains(&parent) {
1243                    continue;
1244                }
1245                let circle = &circles[parent];
1246                let a1 = (p1.point.x - circle.x).atan2(p1.point.y - circle.y);
1247                let a2 = (p2.point.x - circle.x).atan2(p2.point.y - circle.y);
1248                let mut angle_diff = a2 - a1;
1249                if angle_diff < 0.0 {
1250                    angle_diff += TAU;
1251                }
1252                let a = a2 - angle_diff / 2.0;
1253                let mut width = distance_points(
1254                    mid_point,
1255                    VennPoint {
1256                        x: circle.x + circle.radius * a.sin(),
1257                        y: circle.y + circle.radius * a.cos(),
1258                    },
1259                );
1260                if width > circle.radius * 2.0 {
1261                    width = circle.radius * 2.0;
1262                }
1263                if arc.as_ref().is_none_or(|current| current.width > width) {
1264                    arc = Some(VennArc {
1265                        circle: circle.clone(),
1266                        width,
1267                        p1: p1.point,
1268                        p2: p2.point,
1269                        large: width > circle.radius,
1270                        sweep: true,
1271                    });
1272                }
1273            }
1274
1275            if let Some(arc) = arc {
1276                arc_area += circle_area(arc.circle.radius, arc.width);
1277                arcs.push(arc);
1278                p2 = p1.clone();
1279            }
1280        }
1281    } else {
1282        let mut smallest = &circles[0];
1283        for circle in circles.iter().skip(1) {
1284            if circle.radius < smallest.radius {
1285                smallest = circle;
1286            }
1287        }
1288
1289        let mut disjoint = false;
1290        for circle in circles {
1291            if distance(circle, smallest) > (smallest.radius - circle.radius).abs() {
1292                disjoint = true;
1293                break;
1294            }
1295        }
1296
1297        if disjoint {
1298            arc_area = 0.0;
1299            polygon_area = 0.0;
1300        } else {
1301            arc_area = smallest.radius * smallest.radius * PI;
1302            arcs.push(VennArc {
1303                circle: smallest.clone(),
1304                p1: VennPoint {
1305                    x: smallest.x,
1306                    y: smallest.y + smallest.radius,
1307                },
1308                p2: VennPoint {
1309                    x: smallest.x - SMALL,
1310                    y: smallest.y + smallest.radius,
1311                },
1312                width: smallest.radius * 2.0,
1313                large: true,
1314                sweep: true,
1315            });
1316        }
1317    }
1318
1319    polygon_area /= 2.0;
1320    AreaStats {
1321        area: arc_area + polygon_area,
1322        arcs,
1323    }
1324}
1325
1326fn contained_in_circles(point: VennPoint, circles: &[VennCircle]) -> bool {
1327    circles
1328        .iter()
1329        .all(|circle| distance_point_circle(point, circle) < circle.radius + SMALL)
1330}
1331
1332fn get_intersection_points(circles: &[VennCircle]) -> Vec<IntersectionPoint> {
1333    let mut out = Vec::new();
1334    for i in 0..circles.len() {
1335        for j in i + 1..circles.len() {
1336            for point in circle_circle_intersection(&circles[i], &circles[j]) {
1337                out.push(IntersectionPoint {
1338                    point,
1339                    parent_index: [i, j],
1340                    angle: 0.0,
1341                });
1342            }
1343        }
1344    }
1345    out
1346}
1347
1348pub fn circle_area(r: f64, width: f64) -> f64 {
1349    r * r * (1.0 - width / r).acos() - (r - width) * (width * (2.0 * r - width)).sqrt()
1350}
1351
1352pub fn distance(p1: &VennCircle, p2: &VennCircle) -> f64 {
1353    distance_points(
1354        VennPoint { x: p1.x, y: p1.y },
1355        VennPoint { x: p2.x, y: p2.y },
1356    )
1357}
1358
1359fn distance_point_circle(point: VennPoint, circle: &VennCircle) -> f64 {
1360    distance_points(
1361        point,
1362        VennPoint {
1363            x: circle.x,
1364            y: circle.y,
1365        },
1366    )
1367}
1368
1369fn distance_points(p1: VennPoint, p2: VennPoint) -> f64 {
1370    ((p1.x - p2.x).powi(2) + (p1.y - p2.y).powi(2)).sqrt()
1371}
1372
1373pub fn circle_overlap(r1: f64, r2: f64, d: f64) -> f64 {
1374    if d >= r1 + r2 {
1375        return 0.0;
1376    }
1377    if d <= (r1 - r2).abs() {
1378        return PI * r1.min(r2) * r1.min(r2);
1379    }
1380    let w1 = r1 - (d * d - r2 * r2 + r1 * r1) / (2.0 * d);
1381    let w2 = r2 - (d * d - r1 * r1 + r2 * r2) / (2.0 * d);
1382    circle_area(r1, w1) + circle_area(r2, w2)
1383}
1384
1385pub fn circle_circle_intersection(p1: &VennCircle, p2: &VennCircle) -> Vec<VennPoint> {
1386    let d = distance(p1, p2);
1387    let r1 = p1.radius;
1388    let r2 = p2.radius;
1389    if d >= r1 + r2 || d <= (r1 - r2).abs() {
1390        return Vec::new();
1391    }
1392
1393    let a = (r1 * r1 - r2 * r2 + d * d) / (2.0 * d);
1394    let h = (r1 * r1 - a * a).sqrt();
1395    let x0 = p1.x + (a * (p2.x - p1.x)) / d;
1396    let y0 = p1.y + (a * (p2.y - p1.y)) / d;
1397    let rx = -(p2.y - p1.y) * (h / d);
1398    let ry = -(p2.x - p1.x) * (h / d);
1399
1400    vec![
1401        VennPoint {
1402            x: x0 + rx,
1403            y: y0 - ry,
1404        },
1405        VennPoint {
1406            x: x0 - rx,
1407            y: y0 + ry,
1408        },
1409    ]
1410}
1411
1412fn get_center(points: impl IntoIterator<Item = VennPoint>) -> VennPoint {
1413    let mut count = 0usize;
1414    let mut center = VennPoint { x: 0.0, y: 0.0 };
1415    for point in points {
1416        center.x += point.x;
1417        center.y += point.y;
1418        count += 1;
1419    }
1420    if count > 0 {
1421        center.x /= count as f64;
1422        center.y /= count as f64;
1423    }
1424    center
1425}
1426
1427fn compute_text_centres(
1428    circles: &Solution,
1429    areas: &[VennArea],
1430    symmetrical_text_centre: bool,
1431) -> HashMap<String, VennTextPoint> {
1432    let overlapped = get_overlapping_circles(circles);
1433    let mut out = HashMap::new();
1434    for area in areas {
1435        let areaids: HashSet<&str> = area.sets.iter().map(String::as_str).collect();
1436        let mut exclude = HashSet::new();
1437        for set in &area.sets {
1438            if let Some(overlaps) = overlapped.get(set) {
1439                for overlap in overlaps {
1440                    exclude.insert(overlap.as_str());
1441                }
1442            }
1443        }
1444
1445        let mut interior = Vec::new();
1446        let mut exterior = Vec::new();
1447        for (setid, circle) in circles {
1448            if areaids.contains(setid.as_str()) {
1449                interior.push(circle.clone());
1450            } else if !exclude.contains(setid.as_str()) {
1451                exterior.push(circle.clone());
1452            }
1453        }
1454        out.insert(
1455            sets_key(&area.sets),
1456            compute_text_centre(&interior, &exterior, symmetrical_text_centre),
1457        );
1458    }
1459    out
1460}
1461
1462pub fn compute_text_centre(
1463    interior: &[VennCircle],
1464    exterior: &[VennCircle],
1465    symmetrical_text_centre: bool,
1466) -> VennTextPoint {
1467    if interior.is_empty() {
1468        return VennTextPoint {
1469            x: 0.0,
1470            y: 0.0,
1471            disjoint: true,
1472        };
1473    }
1474
1475    let mut points = Vec::new();
1476    for c in interior {
1477        points.push(VennPoint { x: c.x, y: c.y });
1478        points.push(VennPoint {
1479            x: c.x + c.radius / 2.0,
1480            y: c.y,
1481        });
1482        points.push(VennPoint {
1483            x: c.x - c.radius / 2.0,
1484            y: c.y,
1485        });
1486        points.push(VennPoint {
1487            x: c.x,
1488            y: c.y + c.radius / 2.0,
1489        });
1490        points.push(VennPoint {
1491            x: c.x,
1492            y: c.y - c.radius / 2.0,
1493        });
1494    }
1495
1496    let mut initial = points[0];
1497    let mut margin = circle_margin(points[0], interior, exterior);
1498    for point in points.into_iter().skip(1) {
1499        let m = circle_margin(point, interior, exterior);
1500        if m >= margin {
1501            initial = point;
1502            margin = m;
1503        }
1504    }
1505
1506    let solution = nelder_mead(
1507        |p| -circle_margin(VennPoint { x: p[0], y: p[1] }, interior, exterior),
1508        &[initial.x, initial.y],
1509        &OptimParams {
1510            max_iterations: Some(500),
1511            min_error_delta: Some(1e-10),
1512        },
1513    )
1514    .x;
1515
1516    let ret = VennTextPoint {
1517        x: if symmetrical_text_centre {
1518            0.0
1519        } else {
1520            solution[0]
1521        },
1522        y: solution[1],
1523        disjoint: false,
1524    };
1525
1526    let valid_interior = interior.iter().all(|circle| {
1527        distance_point_circle(VennPoint { x: ret.x, y: ret.y }, circle) <= circle.radius
1528    });
1529    let valid_exterior = exterior.iter().all(|circle| {
1530        distance_point_circle(VennPoint { x: ret.x, y: ret.y }, circle) >= circle.radius
1531    });
1532    if valid_interior && valid_exterior {
1533        return ret;
1534    }
1535
1536    if interior.len() == 1 {
1537        return VennTextPoint {
1538            x: interior[0].x,
1539            y: interior[0].y,
1540            disjoint: false,
1541        };
1542    }
1543
1544    let area_stats = intersection_area_stats(interior);
1545    if area_stats.arcs.is_empty() {
1546        return VennTextPoint {
1547            x: 0.0,
1548            y: -1000.0,
1549            disjoint: true,
1550        };
1551    }
1552    if area_stats.arcs.len() == 1 {
1553        return VennTextPoint {
1554            x: area_stats.arcs[0].circle.x,
1555            y: area_stats.arcs[0].circle.y,
1556            disjoint: false,
1557        };
1558    }
1559    if !exterior.is_empty() {
1560        return compute_text_centre(interior, &[], false);
1561    }
1562
1563    let center = get_center(area_stats.arcs.iter().map(|arc| arc.p1));
1564    VennTextPoint {
1565        x: center.x,
1566        y: center.y,
1567        disjoint: false,
1568    }
1569}
1570
1571fn circle_margin(current: VennPoint, interior: &[VennCircle], exterior: &[VennCircle]) -> f64 {
1572    let mut margin = interior[0].radius - distance_point_circle(current, &interior[0]);
1573    for circle in interior.iter().skip(1) {
1574        margin = margin.min(circle.radius - distance_point_circle(current, circle));
1575    }
1576    for circle in exterior {
1577        margin = margin.min(distance_point_circle(current, circle) - circle.radius);
1578    }
1579    margin
1580}
1581
1582fn get_overlapping_circles(circles: &Solution) -> HashMap<String, Vec<String>> {
1583    let mut out: HashMap<String, Vec<String>> = circles
1584        .keys()
1585        .map(|setid| (setid.clone(), Vec::new()))
1586        .collect();
1587    let ids: Vec<String> = circles.keys().cloned().collect();
1588    for i in 0..ids.len() {
1589        let ci = &ids[i];
1590        let a = &circles[ci];
1591        for cj in ids.iter().skip(i + 1) {
1592            let b = &circles[cj];
1593            let d = distance(a, b);
1594            if d + b.radius <= a.radius + SMALL {
1595                out.entry(cj.clone()).or_default().push(ci.clone());
1596            } else if d + a.radius <= b.radius + SMALL {
1597                out.entry(ci.clone()).or_default().push(cj.clone());
1598            }
1599        }
1600    }
1601    out
1602}
1603
1604fn intersection_area_arcs(circles: &[VennCircle]) -> Vec<VennArc> {
1605    if circles.is_empty() {
1606        return Vec::new();
1607    }
1608    intersection_area_stats(circles).arcs
1609}
1610
1611pub fn intersection_area_path(circles: &[VennCircle], round: Option<usize>) -> String {
1612    arcs_to_path(&intersection_area_arcs(circles), round)
1613}
1614
1615fn arcs_to_path(arcs: &[VennArc], round: Option<usize>) -> String {
1616    if arcs.is_empty() {
1617        return "M 0 0".to_string();
1618    }
1619    if arcs.len() == 1 {
1620        let circle = &arcs[0].circle;
1621        return circle_path(
1622            round_path_value(circle.x, round),
1623            round_path_value(circle.y, round),
1624            round_path_value(circle.radius, round),
1625        );
1626    }
1627
1628    let mut out = String::new();
1629    out.push_str("\nM ");
1630    push_js_number(&mut out, round_path_value(arcs[0].p2.x, round));
1631    out.push(' ');
1632    push_js_number(&mut out, round_path_value(arcs[0].p2.y, round));
1633    for arc in arcs {
1634        out.push_str(" \nA ");
1635        let radius = round_path_value(arc.circle.radius, round);
1636        push_js_number(&mut out, radius);
1637        out.push(' ');
1638        push_js_number(&mut out, radius);
1639        out.push_str(" 0 ");
1640        out.push(if arc.large { '1' } else { '0' });
1641        out.push(' ');
1642        out.push(if arc.sweep { '1' } else { '0' });
1643        out.push(' ');
1644        push_js_number(&mut out, round_path_value(arc.p1.x, round));
1645        out.push(' ');
1646        push_js_number(&mut out, round_path_value(arc.p1.y, round));
1647    }
1648    out
1649}
1650
1651fn circle_path(x: f64, y: f64, r: f64) -> String {
1652    let mut out = String::new();
1653    out.push_str("\nM ");
1654    push_js_number(&mut out, x);
1655    out.push(' ');
1656    push_js_number(&mut out, y);
1657    out.push_str(" \nm ");
1658    push_js_number(&mut out, -r);
1659    out.push_str(" 0 \na ");
1660    push_js_number(&mut out, r);
1661    out.push(' ');
1662    push_js_number(&mut out, r);
1663    out.push_str(" 0 1 0 ");
1664    push_js_number(&mut out, r * 2.0);
1665    out.push_str(" 0 \na ");
1666    push_js_number(&mut out, r);
1667    out.push(' ');
1668    push_js_number(&mut out, r);
1669    out.push_str(" 0 1 0 ");
1670    push_js_number(&mut out, -r * 2.0);
1671    out.push_str(" 0");
1672    out
1673}
1674
1675fn round_path_value(v: f64, round: Option<usize>) -> f64 {
1676    let Some(round) = round else {
1677        return v;
1678    };
1679    let factor = 10_f64.powi(round as i32);
1680    let out = ((v * factor) + 0.5).floor() / factor;
1681    if out == -0.0 { 0.0 } else { out }
1682}
1683
1684fn push_js_number(out: &mut String, mut v: f64) {
1685    if !v.is_finite() {
1686        out.push('0');
1687        return;
1688    }
1689    if v == -0.0 {
1690        v = 0.0;
1691    }
1692    let mut buf = Buffer::new();
1693    out.push_str(buf.format_finite(v));
1694}
1695
1696fn sets_key(sets: &[String]) -> String {
1697    sets.join(",")
1698}
1699
1700fn bisect<F>(mut f: F, mut a: f64, b: f64, max_iterations: usize, tolerance: f64) -> f64
1701where
1702    F: FnMut(f64) -> f64,
1703{
1704    let f_a = f(a);
1705    let f_b = f(b);
1706    let mut delta = b - a;
1707    if f_a * f_b > 0.0 {
1708        return a;
1709    }
1710    if f_a == 0.0 {
1711        return a;
1712    }
1713    if f_b == 0.0 {
1714        return b;
1715    }
1716    for _ in 0..max_iterations {
1717        delta /= 2.0;
1718        let mid = a + delta;
1719        let f_mid = f(mid);
1720        if f_mid * f_a >= 0.0 {
1721            a = mid;
1722        }
1723        if delta.abs() < tolerance || f_mid == 0.0 {
1724            return mid;
1725        }
1726    }
1727    a + delta
1728}
1729
1730fn nelder_mead<F>(mut f: F, x0: &[f64], parameters: &OptimParams) -> OptimResult
1731where
1732    F: FnMut(&[f64]) -> f64,
1733{
1734    let n = x0.len();
1735    let max_iterations = parameters.max_iterations.unwrap_or(n * 200);
1736    let non_zero_delta = 1.05;
1737    let zero_delta = 0.001;
1738    let min_error_delta = parameters.min_error_delta.unwrap_or(1e-6);
1739    let min_tolerance = parameters.min_error_delta.unwrap_or(1e-5);
1740    let rho = 1.0;
1741    let chi = 2.0;
1742    let psi = -0.5;
1743    let sigma = 0.5;
1744
1745    let mut simplex = Vec::with_capacity(n + 1);
1746    simplex.push(SimplexPoint {
1747        x: x0.to_vec(),
1748        fx: f(x0),
1749    });
1750    for i in 0..n {
1751        let mut point = x0.to_vec();
1752        point[i] = if point[i] != 0.0 {
1753            point[i] * non_zero_delta
1754        } else {
1755            zero_delta
1756        };
1757        let fx = f(&point);
1758        simplex.push(SimplexPoint { x: point, fx });
1759    }
1760
1761    let mut centroid = x0.to_vec();
1762    let mut reflected = x0.to_vec();
1763    let mut contracted = x0.to_vec();
1764    let mut expanded = x0.to_vec();
1765
1766    for _ in 0..max_iterations {
1767        simplex.sort_by(|a, b| a.fx.total_cmp(&b.fx));
1768        let mut max_diff: f64 = 0.0;
1769        if simplex.len() > 1 {
1770            for i in 0..n {
1771                max_diff = max_diff.max((simplex[0].x[i] - simplex[1].x[i]).abs());
1772            }
1773        }
1774        if (simplex[0].fx - simplex[n].fx).abs() < min_error_delta && max_diff < min_tolerance {
1775            break;
1776        }
1777
1778        for (i, centroid_value) in centroid.iter_mut().enumerate().take(n) {
1779            *centroid_value = 0.0;
1780            for point in simplex.iter().take(n) {
1781                *centroid_value += point.x[i];
1782            }
1783            *centroid_value /= n as f64;
1784        }
1785
1786        let worst = simplex[n].x.clone();
1787        weighted_sum(&mut reflected, 1.0 + rho, &centroid, -rho, &worst);
1788        let reflected_fx = f(&reflected);
1789        if reflected_fx < simplex[0].fx {
1790            weighted_sum(&mut expanded, 1.0 + chi, &centroid, -chi, &worst);
1791            let expanded_fx = f(&expanded);
1792            if expanded_fx < reflected_fx {
1793                update_simplex(&mut simplex[n], &expanded, expanded_fx);
1794            } else {
1795                update_simplex(&mut simplex[n], &reflected, reflected_fx);
1796            }
1797        } else if reflected_fx >= simplex[n - 1].fx {
1798            let mut should_reduce = false;
1799            if reflected_fx > simplex[n].fx {
1800                weighted_sum(&mut contracted, 1.0 + psi, &centroid, -psi, &worst);
1801                let contracted_fx = f(&contracted);
1802                if contracted_fx < simplex[n].fx {
1803                    update_simplex(&mut simplex[n], &contracted, contracted_fx);
1804                } else {
1805                    should_reduce = true;
1806                }
1807            } else {
1808                weighted_sum(
1809                    &mut contracted,
1810                    1.0 - psi * rho,
1811                    &centroid,
1812                    psi * rho,
1813                    &worst,
1814                );
1815                let contracted_fx = f(&contracted);
1816                if contracted_fx < reflected_fx {
1817                    update_simplex(&mut simplex[n], &contracted, contracted_fx);
1818                } else {
1819                    should_reduce = true;
1820                }
1821            }
1822
1823            if should_reduce {
1824                if sigma >= 1.0 {
1825                    break;
1826                }
1827                let best = simplex[0].x.clone();
1828                for point in simplex.iter_mut().skip(1) {
1829                    let current = point.x.clone();
1830                    weighted_sum(&mut point.x, 1.0 - sigma, &best, sigma, &current);
1831                    point.fx = f(&point.x);
1832                }
1833            }
1834        } else {
1835            update_simplex(&mut simplex[n], &reflected, reflected_fx);
1836        }
1837    }
1838
1839    simplex.sort_by(|a, b| a.fx.total_cmp(&b.fx));
1840    OptimResult {
1841        x: simplex[0].x.clone(),
1842    }
1843}
1844
1845#[derive(Debug, Clone)]
1846struct SimplexPoint {
1847    x: Vec<f64>,
1848    fx: f64,
1849}
1850
1851fn update_simplex(point: &mut SimplexPoint, value: &[f64], fx: f64) {
1852    point.x.copy_from_slice(value);
1853    point.fx = fx;
1854}
1855
1856fn conjugate_gradient<F>(mut f: F, initial: &[f64], max_iterations: Option<usize>) -> CgState
1857where
1858    F: FnMut(&[f64], &mut [f64]) -> f64,
1859{
1860    let mut current = CgState {
1861        x: initial.to_vec(),
1862        fx: 0.0,
1863        fxprime: initial.to_vec(),
1864    };
1865    let mut next = current.clone();
1866    let mut yk = initial.to_vec();
1867    let mut pk = current.fxprime.clone();
1868    let mut a = 1.0;
1869    let max_iterations = max_iterations.unwrap_or(initial.len() * 20);
1870
1871    current.fx = f(&current.x, &mut current.fxprime);
1872    scale(&mut pk, &current.fxprime, -1.0);
1873
1874    for _ in 0..max_iterations {
1875        a = wolfe_line_search(&mut f, &pk, &current, &mut next, a);
1876        if a == 0.0 {
1877            scale(&mut pk, &current.fxprime, -1.0);
1878        } else {
1879            weighted_sum(&mut yk, 1.0, &next.fxprime, -1.0, &current.fxprime);
1880            let delta_k = dot(&current.fxprime, &current.fxprime);
1881            let beta_k = (dot(&yk, &next.fxprime) / delta_k).max(0.0);
1882            let old_pk = pk.clone();
1883            weighted_sum(&mut pk, beta_k, &old_pk, -1.0, &next.fxprime);
1884            std::mem::swap(&mut current, &mut next);
1885        }
1886
1887        if norm2(&current.fxprime) <= 1e-5 {
1888            break;
1889        }
1890    }
1891
1892    current
1893}
1894
1895fn wolfe_line_search<F>(
1896    f: &mut F,
1897    pk: &[f64],
1898    current: &CgState,
1899    next: &mut CgState,
1900    mut a: f64,
1901) -> f64
1902where
1903    F: FnMut(&[f64], &mut [f64]) -> f64,
1904{
1905    let phi0 = current.fx;
1906    let phi_prime0 = dot(&current.fxprime, pk);
1907    let mut phi_old = phi0;
1908    let mut a0 = 0.0;
1909    let c1 = 1e-6;
1910    let c2 = 0.1;
1911    if a == 0.0 {
1912        a = 1.0;
1913    }
1914
1915    for iteration in 0..10 {
1916        weighted_sum(&mut next.x, 1.0, &current.x, a, pk);
1917        next.fx = f(&next.x, &mut next.fxprime);
1918        let phi = next.fx;
1919        let phi_prime = dot(&next.fxprime, pk);
1920        if phi > phi0 + c1 * a * phi_prime0 || (iteration > 0 && phi >= phi_old) {
1921            return zoom(
1922                f, pk, current, next, a0, a, phi_old, phi0, phi_prime0, c1, c2,
1923            );
1924        }
1925        if phi_prime.abs() <= -c2 * phi_prime0 {
1926            return a;
1927        }
1928        if phi_prime >= 0.0 {
1929            return zoom(f, pk, current, next, a, a0, phi, phi0, phi_prime0, c1, c2);
1930        }
1931        phi_old = phi;
1932        a0 = a;
1933        a *= 2.0;
1934    }
1935    a
1936}
1937
1938#[allow(clippy::too_many_arguments)]
1939fn zoom<F>(
1940    f: &mut F,
1941    pk: &[f64],
1942    current: &CgState,
1943    next: &mut CgState,
1944    mut a_lo: f64,
1945    mut a_high: f64,
1946    mut phi_lo: f64,
1947    phi0: f64,
1948    phi_prime0: f64,
1949    c1: f64,
1950    c2: f64,
1951) -> f64
1952where
1953    F: FnMut(&[f64], &mut [f64]) -> f64,
1954{
1955    for _ in 0..16 {
1956        let a = (a_lo + a_high) / 2.0;
1957        weighted_sum(&mut next.x, 1.0, &current.x, a, pk);
1958        next.fx = f(&next.x, &mut next.fxprime);
1959        let phi = next.fx;
1960        let phi_prime = dot(&next.fxprime, pk);
1961        if phi > phi0 + c1 * a * phi_prime0 || phi >= phi_lo {
1962            a_high = a;
1963        } else {
1964            if phi_prime.abs() <= -c2 * phi_prime0 {
1965                return a;
1966            }
1967            if phi_prime * (a_high - a_lo) >= 0.0 {
1968                a_high = a_lo;
1969            }
1970            a_lo = a;
1971            phi_lo = phi;
1972        }
1973    }
1974    0.0
1975}
1976
1977fn dot(a: &[f64], b: &[f64]) -> f64 {
1978    a.iter().zip(b).map(|(a, b)| a * b).sum()
1979}
1980
1981fn norm2(a: &[f64]) -> f64 {
1982    dot(a, a).sqrt()
1983}
1984
1985fn scale(ret: &mut [f64], value: &[f64], c: f64) {
1986    for (out, value) in ret.iter_mut().zip(value) {
1987        *out = value * c;
1988    }
1989}
1990
1991fn weighted_sum(ret: &mut [f64], w1: f64, v1: &[f64], w2: f64, v2: &[f64]) {
1992    for ((out, v1), v2) in ret.iter_mut().zip(v1).zip(v2) {
1993        *out = w1 * v1 + w2 * v2;
1994    }
1995}
1996
1997#[cfg(test)]
1998mod tests {
1999    use super::*;
2000
2001    fn c(set: &str, x: f64, y: f64, radius: f64) -> VennCircle {
2002        VennCircle {
2003            set: set.to_string(),
2004            x,
2005            y,
2006            radius,
2007        }
2008    }
2009
2010    fn area(sets: &[&str], size: f64) -> VennArea {
2011        VennArea::new(sets.iter().copied(), size)
2012    }
2013
2014    fn assert_close(actual: f64, expected: f64, tolerance: f64) {
2015        assert!(
2016            (actual - expected).abs() <= tolerance,
2017            "expected {actual} to be within {tolerance} of {expected}"
2018        );
2019    }
2020
2021    #[test]
2022    fn circle_area_matches_upstream_cases() {
2023        assert_close(circle_area(10.0, 0.0), 0.0, 1e-9);
2024        assert_close(circle_area(10.0, 10.0), (PI * 10.0 * 10.0) / 2.0, 1e-9);
2025        assert_close(circle_area(10.0, 20.0), PI * 10.0 * 10.0, 1e-9);
2026    }
2027
2028    #[test]
2029    fn circle_overlap_matches_upstream_cases() {
2030        assert_close(circle_overlap(10.0, 10.0, 200.0), 0.0, 1e-9);
2031        assert_close(circle_overlap(10.0, 10.0, 0.0), PI * 10.0 * 10.0, 1e-9);
2032        assert_close(circle_overlap(10.0, 5.0, 5.0), PI * 5.0 * 5.0, 1e-9);
2033    }
2034
2035    #[test]
2036    fn circle_circle_intersection_matches_upstream_cases() {
2037        assert!(
2038            circle_circle_intersection(&c("a", 0.0, 3.0, 10.0), &c("b", 3.0, 0.0, 20.0)).is_empty()
2039        );
2040        assert!(
2041            circle_circle_intersection(&c("a", 0.0, 0.0, 10.0), &c("b", 21.0, 0.0, 10.0))
2042                .is_empty()
2043        );
2044
2045        let points = circle_circle_intersection(&c("a", 0.0, 0.0, 10.0), &c("b", 10.0, 0.0, 10.0));
2046        assert_eq!(points.len(), 2);
2047        assert_close(points[0].x, 5.0, 1e-9);
2048        assert_close(points[1].x, 5.0, 1e-9);
2049        assert_close(points[0].y, -points[1].y, 1e-9);
2050
2051        let points = circle_circle_intersection(&c("a", 15.0, 5.0, 10.0), &c("b", 20.0, 0.0, 10.0));
2052        assert_eq!(points.len(), 2);
2053    }
2054
2055    #[test]
2056    fn intersection_area_matches_upstream_regressions() {
2057        let circles = vec![
2058            c("0", 0.909, 0.905, 0.548),
2059            c("1", 0.765, 0.382, 0.703),
2060            c("2", 0.63, 0.019, 0.449),
2061            c("3", 0.21, 0.755, 0.656),
2062            c("4", 0.276, 0.723, 1.145),
2063            c("5", 0.141, 0.585, 0.419),
2064        ];
2065        assert_eq!(intersection_area(&circles), 0.0);
2066
2067        let circles = vec![
2068            c("0", 0.426, 0.882, 0.944),
2069            c("1", 0.24, 0.685, 0.992),
2070            c("2", 0.01, 0.909, 1.161),
2071            c("3", 0.54, 0.475, 0.41),
2072        ];
2073        assert_close(intersection_area(&circles), 0.41 * 0.41 * PI, 1e-9);
2074
2075        let circles = vec![
2076            c("0", 0.501, 0.32, 0.629),
2077            c("1", 0.945, 0.022, 1.015),
2078            c("2", 0.021, 0.863, 0.261),
2079            c("3", 0.528, 0.09, 0.676),
2080        ];
2081        assert_close(intersection_area(&circles), 0.0008914, 0.0001);
2082
2083        let circles = vec![
2084            c("0", 9.154829758385864, 0.0, 8.481629223064205),
2085            c(
2086                "1",
2087                5.806079662851866,
2088                7.4438023223126795,
2089                15.274853405932202,
2090            ),
2091            c(
2092                "2",
2093                9.484491297623553,
2094                4.064806303558571,
2095                10.280023453913834,
2096            ),
2097            c(
2098                "3",
2099                10.56492833796709,
2100                3.0723147554880175,
2101                8.812923024107548,
2102            ),
2103        ];
2104        assert_close(intersection_area(&circles), 10.96362, 1e-5);
2105
2106        let circles = vec![
2107            c(
2108                "0",
2109                -0.0014183481763938425,
2110                0.0006071174738860746,
2111                510.3115834996166,
2112            ),
2113            c(
2114                "1",
2115                875.0163281608848,
2116                0.0007003612396158774,
2117                465.1793581792228,
2118            ),
2119            c(
2120                "2",
2121                462.7394999567192,
2122                387.9359963330729,
2123                172.62633992134658,
2124            ),
2125        ];
2126        assert!(!intersection_area(&circles).is_nan());
2127    }
2128
2129    #[test]
2130    fn greedy_layout_matches_upstream_zero_loss_cases() {
2131        let cases = vec![
2132            vec![
2133                area(&["0"], 0.7746543297103429),
2134                area(&["1"], 0.1311252856844238),
2135                area(&["2"], 0.2659942131443344),
2136                area(&["3"], 0.44600866168641723),
2137                area(&["0", "1"], 0.02051532092950205),
2138                area(&["0", "2"], 0.0),
2139                area(&["0", "3"], 0.0),
2140                area(&["1", "2"], 0.0),
2141                area(&["1", "3"], 0.07597023820511245),
2142                area(&["2", "3"], 0.0),
2143            ],
2144            vec![
2145                area(&["0"], 0.5299368855059736),
2146                area(&["1"], 0.03364187025606481),
2147                area(&["2"], 0.3121450394871512),
2148                area(&["3"], 0.0514397361783036),
2149                area(&["0", "1"], 0.013912447645582351),
2150                area(&["0", "2"], 0.005903647141469598),
2151                area(&["0", "3"], 0.0514397361783036),
2152                area(&["1", "2"], 0.012138157839477597),
2153                area(&["1", "3"], 0.008010688232481479),
2154                area(&["2", "3"], 0.0),
2155            ],
2156            vec![
2157                area(&["0"], 1.7288584050841396),
2158                area(&["1"], 0.040875831658950056),
2159                area(&["2"], 2.587146019782323),
2160                area(&["0", "1"], 0.040875831658950056),
2161                area(&["0", "2"], 0.5114617575187569),
2162                area(&["1", "2"], 0.040875831658950056),
2163            ],
2164        ];
2165
2166        for areas in cases {
2167            let circles = greedy_layout(&areas).expect("greedy layout");
2168            assert_close(loss_function(&circles, &areas), 0.0, 1e-8);
2169        }
2170    }
2171
2172    #[test]
2173    fn distance_from_intersect_area_round_trips_overlap() {
2174        for (r1, r2, overlap) in [
2175            (1.9544100476116797, 2.256758334191025, 11.0),
2176            (111.06512962798197, 113.32348546565727, 1218.0),
2177            (44.456564007075, 149.4335753619362, 2799.0),
2178            (592.89, 134.75, 56995.0),
2179            (139.50778247443944, 32.892784970851956, 3399.0),
2180            (4.886025119029199, 5.077706251929807, 75.0),
2181        ] {
2182            let d = distance_from_intersect_area(r1, r2, overlap);
2183            assert_close(circle_overlap(r1, r2, d), overlap, 1e-7);
2184        }
2185    }
2186
2187    #[test]
2188    fn normalize_solution_places_disjoint_circles_close_together() {
2189        let mut solution = Solution::new();
2190        solution.insert("0".to_string(), c("0", 0.0, 0.0, 0.5));
2191        solution.insert("1".to_string(), c("1", 1e10, 0.0, 1.5));
2192
2193        let normalized = normalize_solution(&solution, PI / 2.0);
2194        assert!(distance(&normalized["0"], &normalized["1"]) < 2.1);
2195    }
2196
2197    #[test]
2198    fn disjoint_clusters_match_upstream_case() {
2199        let input = vec![
2200            c(
2201                "0",
2202                0.8047033110633492,
2203                0.9396705999970436,
2204                0.47156485118903224,
2205            ),
2206            c(
2207                "1",
2208                0.7961132447235286,
2209                0.014027722179889679,
2210                0.14554832570720466,
2211            ),
2212            c(
2213                "2",
2214                0.28841276094317436,
2215                0.98081015329808,
2216                0.9851036085514352,
2217            ),
2218            c(
2219                "3",
2220                0.7689983483869582,
2221                0.2899463507346809,
2222                0.7210563338827342,
2223            ),
2224        ];
2225
2226        assert_eq!(disjoint_cluster(input).len(), 1);
2227    }
2228
2229    #[test]
2230    fn compute_text_centre_matches_upstream_cases() {
2231        let center = compute_text_centre(&[c("0", 0.0, 0.0, 1.0)], &[], false);
2232        assert_close(center.x, 0.0, 1e-9);
2233        assert_close(center.y, 0.0, 1e-9);
2234
2235        let center = compute_text_centre(&[c("0", 0.0, 0.0, 1.0)], &[c("1", 0.0, 1.0, 1.0)], false);
2236        assert_close(center.x, 0.0, 1e-4);
2237        assert_close(center.y, -0.5, 1e-6);
2238    }
2239
2240    #[test]
2241    fn compute_venn_layout_returns_typed_helper_surface() {
2242        let areas = vec![
2243            area(&["A"], 10.0),
2244            area(&["B"], 10.0),
2245            area(&["A", "B"], 3.0),
2246        ];
2247        let layout = compute_venn_layout(&areas, &VennLayoutOptions::default()).expect("layout");
2248        assert_eq!(layout.len(), 3);
2249        assert_eq!(layout[0].data.sets, vec!["A"]);
2250        assert!(!layout[0].path.is_empty());
2251        assert_eq!(layout[2].circles.len(), 2);
2252        assert!(!layout[2].text.disjoint);
2253    }
2254}