1use 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 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(¤t, &areas)
520 },
521 &initial,
522 ¶ms,
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(¤t.sets[0]) else {
856 continue;
857 };
858 let Some(&right) = setids.get(¤t.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, ¢roid, -rho, &worst);
1788 let reflected_fx = f(&reflected);
1789 if reflected_fx < simplex[0].fx {
1790 weighted_sum(&mut expanded, 1.0 + chi, ¢roid, -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, ¢roid, -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 ¢roid,
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, ¤t);
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(¤t.x, &mut current.fxprime);
1872 scale(&mut pk, ¤t.fxprime, -1.0);
1873
1874 for _ in 0..max_iterations {
1875 a = wolfe_line_search(&mut f, &pk, ¤t, &mut next, a);
1876 if a == 0.0 {
1877 scale(&mut pk, ¤t.fxprime, -1.0);
1878 } else {
1879 weighted_sum(&mut yk, 1.0, &next.fxprime, -1.0, ¤t.fxprime);
1880 let delta_k = dot(¤t.fxprime, ¤t.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(¤t.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(¤t.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, ¤t.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, ¤t.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}