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