1use crate::aes::Aesthetic;
20use crate::coord::Coord;
21use crate::data::{DataFrame, Value};
22use crate::position::identity::PositionIdentity;
23use crate::position::Position;
24use crate::render::backend::{
25 DrawBackend, FontFace, LineStyle, Linetype, RectStyle, TextAnchor, TextStyle,
26};
27use crate::render::{Rect, RenderError};
28use crate::scale::ScaleSet;
29use crate::stat::identity::StatIdentity;
30use crate::stat::Stat;
31use crate::theme::Theme;
32
33use super::{Geom, GeomParams};
34
35#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
37pub enum RepelDirection {
38 #[default]
40 Both,
41 X,
43 Y,
45}
46
47#[derive(Clone, Debug)]
49pub struct RepelParams {
50 pub box_padding: f64,
53 pub point_padding: f64,
56 pub point_size: f64,
59 pub nudge_x: f64,
61 pub nudge_y: f64,
63 pub force: f64,
65 pub force_pull: f64,
67 pub max_iter: usize,
69 pub max_time: std::time::Duration,
73 pub max_overlaps: usize,
76 pub min_segment_length: f64,
79 pub seed: u64,
81 pub direction: RepelDirection,
83 pub max_labels: usize,
86 pub segment_color: Option<(u8, u8, u8)>,
88 pub segment_width: f64,
90}
91
92impl Default for RepelParams {
93 fn default() -> Self {
94 RepelParams {
95 box_padding: 3.0,
96 point_padding: 2.0,
97 point_size: 3.0,
98 nudge_x: 0.0,
99 nudge_y: 0.0,
100 force: 1.0,
101 force_pull: 1.0,
102 max_iter: 2000,
103 max_time: std::time::Duration::from_millis(500),
104 max_overlaps: 10,
105 min_segment_length: 12.0,
106 seed: 42,
107 direction: RepelDirection::Both,
108 max_labels: 500,
109 segment_color: None,
110 segment_width: 0.6,
111 }
112 }
113}
114
115macro_rules! repel_builders {
116 ($t:ty) => {
117 impl $t {
118 pub fn box_padding(mut self, px: f64) -> Self {
120 self.repel.box_padding = px;
121 self
122 }
123 pub fn point_padding(mut self, px: f64) -> Self {
125 self.repel.point_padding = px;
126 self
127 }
128 pub fn point_size(mut self, px: f64) -> Self {
130 self.repel.point_size = px;
131 self
132 }
133 pub fn nudge(mut self, x: f64, y: f64) -> Self {
135 self.repel.nudge_x = x;
136 self.repel.nudge_y = y;
137 self
138 }
139 pub fn force(mut self, force: f64) -> Self {
141 self.repel.force = force;
142 self
143 }
144 pub fn force_pull(mut self, force: f64) -> Self {
146 self.repel.force_pull = force;
147 self
148 }
149 pub fn max_iter(mut self, n: usize) -> Self {
151 self.repel.max_iter = n;
152 self
153 }
154 pub fn max_time(mut self, t: std::time::Duration) -> Self {
156 self.repel.max_time = t;
157 self
158 }
159 pub fn max_overlaps(mut self, n: usize) -> Self {
161 self.repel.max_overlaps = n;
162 self
163 }
164 pub fn min_segment_length(mut self, px: f64) -> Self {
166 self.repel.min_segment_length = px;
167 self
168 }
169 pub fn seed(mut self, seed: u64) -> Self {
171 self.repel.seed = seed;
172 self
173 }
174 pub fn direction(mut self, d: RepelDirection) -> Self {
176 self.repel.direction = d;
177 self
178 }
179 pub fn max_labels(mut self, n: usize) -> Self {
181 self.repel.max_labels = n;
182 self
183 }
184 pub fn segment_color(mut self, color: (u8, u8, u8)) -> Self {
186 self.repel.segment_color = Some(color);
187 self
188 }
189 }
190 };
191}
192
193pub struct GeomTextRepel {
197 pub size: f64,
198 pub color: (u8, u8, u8),
199 pub alpha: f64,
200 pub fontface: FontFace,
201 pub repel: RepelParams,
202}
203
204impl Default for GeomTextRepel {
205 fn default() -> Self {
206 GeomTextRepel {
207 size: 10.0,
208 color: (0, 0, 0),
209 alpha: 1.0,
210 fontface: FontFace::Plain,
211 repel: RepelParams::default(),
212 }
213 }
214}
215
216repel_builders!(GeomTextRepel);
217
218pub struct GeomLabelRepel {
221 pub size: f64,
222 pub color: (u8, u8, u8),
223 pub fill: (u8, u8, u8),
224 pub alpha: f64,
225 pub label_padding: f64,
227 pub fontface: FontFace,
228 pub repel: RepelParams,
229}
230
231impl Default for GeomLabelRepel {
232 fn default() -> Self {
233 GeomLabelRepel {
234 size: 10.0,
235 color: (0, 0, 0),
236 fill: (255, 255, 255),
237 alpha: 0.9,
238 label_padding: 3.0,
239 fontface: FontFace::Plain,
240 repel: RepelParams::default(),
241 }
242 }
243}
244
245repel_builders!(GeomLabelRepel);
246
247#[derive(Clone, Copy, Debug)]
249struct LabelBox {
250 cx: f64,
251 cy: f64,
252 hw: f64,
253 hh: f64,
254}
255
256impl LabelBox {
257 fn overlaps(&self, o: &LabelBox, pad: f64) -> bool {
258 (self.cx - o.cx).abs() < self.hw + o.hw + 2.0 * pad
259 && (self.cy - o.cy).abs() < self.hh + o.hh + 2.0 * pad
260 }
261
262 fn dist_to(&self, x: f64, y: f64) -> f64 {
264 let dx = ((x - self.cx).abs() - self.hw).max(0.0);
265 let dy = ((y - self.cy).abs() - self.hh).max(0.0);
266 dx.hypot(dy)
267 }
268
269 fn closest(&self, x: f64, y: f64) -> (f64, f64) {
271 (
272 x.clamp(self.cx - self.hw, self.cx + self.hw),
273 y.clamp(self.cy - self.hh, self.cy + self.hh),
274 )
275 }
276}
277
278pub(crate) struct RepelLayout {
281 boxes: Vec<LabelBox>,
282 kept: Vec<bool>,
283 dropped: usize,
284 capped: bool,
286}
287
288pub(crate) fn layout(
292 anchors: &[(f64, f64)],
293 targets: &[(f64, f64)],
294 half: &[(f64, f64)],
295 points: &[(f64, f64)],
296 panel: &Rect,
297 p: &RepelParams,
298) -> RepelLayout {
299 let n = anchors.len();
300 let finite = |v: f64, d: f64| if v.is_finite() { v } else { d };
301 let box_pad = finite(p.box_padding, 0.0).max(0.0);
302 let pt_r = finite(p.point_size, 0.0).max(0.0) + finite(p.point_padding, 0.0).max(0.0);
303 let force = finite(p.force, 1.0).clamp(0.0, 100.0);
304 let pull = finite(p.force_pull, 1.0).clamp(0.0, 100.0);
305 let (x0, x1) = (panel.x, panel.x + panel.width);
306 let (y0, y1) = (panel.y, panel.y + panel.height);
307 let clamp_box = |b: &mut LabelBox| {
308 b.cx = if 2.0 * b.hw >= x1 - x0 {
310 (x0 + x1) / 2.0
311 } else {
312 b.cx.clamp(x0 + b.hw, x1 - b.hw)
313 };
314 b.cy = if 2.0 * b.hh >= y1 - y0 {
315 (y0 + y1) / 2.0
316 } else {
317 b.cy.clamp(y0 + b.hh, y1 - b.hh)
318 };
319 };
320
321 let mut rng = crate::rng::SplitMix64::new(p.seed);
322 let mut boxes: Vec<LabelBox> = (0..n)
323 .map(|i| {
324 let mut b = LabelBox {
325 cx: targets[i].0 + rng.range_f64(-0.5, 0.5),
326 cy: targets[i].1 + rng.range_f64(-0.5, 0.5),
327 hw: half[i].0,
328 hh: half[i].1,
329 };
330 clamp_box(&mut b);
331 b
332 })
333 .collect();
334
335 let capped = n > p.max_labels;
336 let (move_x, move_y) = match p.direction {
337 RepelDirection::Both => (1.0, 1.0),
338 RepelDirection::X => (1.0, 0.0),
339 RepelDirection::Y => (0.0, 1.0),
340 };
341
342 let mut pts: Vec<(f64, f64)> = points
344 .iter()
345 .copied()
346 .filter(|(x, y)| x.is_finite() && y.is_finite())
347 .collect();
348 pts.sort_by(|a, b| a.0.total_cmp(&b.0));
349 let points_near = |pts: &[(f64, f64)], lo: f64, hi: f64| {
350 let start = pts.partition_point(|q| q.0 < lo);
351 let end = pts.partition_point(|q| q.0 <= hi);
352 start..end.max(start)
353 };
354
355 if !capped && n > 0 {
356 #[cfg(not(target_arch = "wasm32"))]
357 let started = std::time::Instant::now();
358 let mut order: Vec<usize> = (0..n).collect();
359 let mut delta = vec![(0.0f64, 0.0f64); n];
360 let mut hit = vec![false; n];
361 let pull_until = p.max_iter - p.max_iter / 4;
365 let max_step = 12.0;
368 for iter in 0..p.max_iter {
369 #[cfg(not(target_arch = "wasm32"))]
370 if iter % 16 == 15 && started.elapsed() >= p.max_time {
371 break;
372 }
373 for d in delta.iter_mut() {
374 *d = (0.0, 0.0);
375 }
376 hit.iter_mut().for_each(|h| *h = false);
377 let mut overlaps = 0usize;
378
379 order.sort_by(|&a, &b| {
381 (boxes[a].cx - boxes[a].hw).total_cmp(&(boxes[b].cx - boxes[b].hw))
382 });
383 for (oi, &i) in order.iter().enumerate() {
384 let bi = boxes[i];
385 for &j in &order[oi + 1..] {
386 let bj = boxes[j];
387 if bj.cx - bj.hw - box_pad >= bi.cx + bi.hw + box_pad {
388 break;
389 }
390 if !bi.overlaps(&bj, box_pad) {
391 continue;
392 }
393 overlaps += 1;
394 hit[i] = true;
395 hit[j] = true;
396 let (dx, dy) = (bi.cx - bj.cx, bi.cy - bj.cy);
397 let ox = bi.hw + bj.hw + 2.0 * box_pad - dx.abs();
398 let oy = bi.hh + bj.hh + 2.0 * box_pad - dy.abs();
399 let sx = if dx == 0.0 {
400 tie(&mut rng)
401 } else {
402 dx.signum()
403 };
404 let sy = if dy == 0.0 {
405 tie(&mut rng)
406 } else {
407 dy.signum()
408 };
409 let along_x = match p.direction {
412 RepelDirection::X => true,
413 RepelDirection::Y => false,
414 RepelDirection::Both => ox * move_x <= oy * move_y,
415 };
416 let k = 0.5 * force;
417 if along_x {
418 delta[i].0 += sx * ox * k;
419 delta[j].0 -= sx * ox * k;
420 } else {
421 delta[i].1 += sy * oy * k;
422 delta[j].1 -= sy * oy * k;
423 }
424 }
425 }
426
427 let label_overlaps = overlaps;
428
429 let point_force = if iter < pull_until { force } else { 0.0 };
433 for i in 0..n {
434 if point_force == 0.0 {
435 break;
436 }
437 let b = boxes[i];
438 let r = pt_r + box_pad;
439 for q in &pts[points_near(&pts, b.cx - b.hw - r, b.cx + b.hw + r)] {
440 let (dx, dy) = (b.cx - q.0, b.cy - q.1);
441 let ox = b.hw + r - dx.abs();
442 let oy = b.hh + r - dy.abs();
443 if ox <= 0.0 || oy <= 0.0 {
444 continue;
445 }
446 overlaps += 1;
447 hit[i] = true;
448 let sx = if dx == 0.0 {
449 tie(&mut rng)
450 } else {
451 dx.signum()
452 };
453 let sy = if dy == 0.0 {
454 tie(&mut rng)
455 } else {
456 dy.signum()
457 };
458 let along_x = match p.direction {
459 RepelDirection::X => true,
460 RepelDirection::Y => false,
461 RepelDirection::Both => ox <= oy,
462 };
463 if along_x {
464 delta[i].0 += sx * ox * point_force;
465 } else {
466 delta[i].1 += sy * oy * point_force;
467 }
468 }
469 }
470
471 let mut moved = 0.0f64;
473 for i in 0..n {
474 let b = &mut boxes[i];
475 let k = if iter < pull_until && !hit[i] {
478 0.1 * pull
479 } else {
480 0.0
481 };
482 let sx = (targets[i].0 - b.cx) * k;
483 let sy = (targets[i].1 - b.cy) * k;
484 let cap = |v: f64| {
485 if v.is_finite() {
486 v.clamp(-max_step, max_step)
487 } else {
488 0.0
489 }
490 };
491 let (mx, my) = (cap(delta[i].0 + sx) * move_x, cap(delta[i].1 + sy) * move_y);
492 let (px, py) = (b.cx, b.cy);
493 b.cx += mx;
494 b.cy += my;
495 clamp_box(b);
496 moved = moved.max((b.cx - px).abs() + (b.cy - py).abs());
497 }
498 if (overlaps == 0 && moved < 0.05) || (label_overlaps == 0 && iter >= pull_until) {
499 break;
500 }
501 }
502 }
503
504 let mut kept = vec![false; n];
507 let mut dropped = 0usize;
508 let mut kept_boxes: Vec<LabelBox> = Vec::new();
509 let limit = if capped { 0 } else { p.max_overlaps };
510 for i in 0..n {
511 let b = boxes[i];
512 let mut count = kept_boxes.iter().filter(|o| b.overlaps(o, 0.0)).count();
513 if count <= limit && !capped {
515 let r = pt_r;
516 count += pts[points_near(&pts, b.cx - b.hw - r, b.cx + b.hw + r)]
517 .iter()
518 .filter(|q| {
519 !(q.0 == anchors[i].0 && q.1 == anchors[i].1)
520 && (q.0 - b.cx).abs() < b.hw + r
521 && (q.1 - b.cy).abs() < b.hh + r
522 })
523 .count();
524 }
525 if count > limit {
526 dropped += 1;
527 } else {
528 kept[i] = true;
529 kept_boxes.push(b);
530 }
531 }
532
533 RepelLayout {
534 boxes,
535 kept,
536 dropped,
537 capped,
538 }
539}
540
541fn tie(rng: &mut crate::rng::SplitMix64) -> f64 {
543 if rng.next_u64() & 1 == 0 {
544 -1.0
545 } else {
546 1.0
547 }
548}
549
550struct Item {
552 row: usize,
553 anchor: (f64, f64),
554 target: (f64, f64),
555 text: String,
556}
557
558#[allow(clippy::too_many_arguments)]
560fn draw_repel(
561 name: &str,
562 data: &DataFrame,
563 coord: &dyn Coord,
564 scales: &ScaleSet,
565 theme: &Theme,
566 backend: &mut dyn DrawBackend,
567 size: f64,
568 color: (u8, u8, u8),
569 alpha: f64,
570 face: FontFace,
571 label: Option<((u8, u8, u8), f64)>,
572 p: &RepelParams,
573) -> Result<(), RenderError> {
574 let x_col = data
575 .column("x")
576 .ok_or(RenderError::MissingAesthetic("x".into()))?;
577 let y_col = data
578 .column("y")
579 .ok_or(RenderError::MissingAesthetic("y".into()))?;
580 let label_col = data
581 .column("label")
582 .ok_or(RenderError::MissingAesthetic("label".into()))?;
583 let color_col = data.column("color");
584 let fill_col = data.column("fill");
585
586 let panel = backend.plot_area();
587 let x_scale = scales.get(&Aesthetic::X);
588 let y_scale = scales.get(&Aesthetic::Y);
589 let size = if size.is_finite() && size > 0.0 {
590 size
591 } else {
592 10.0
593 };
594
595 let to_px = |xv: &Value, yv: &Value| -> (f64, f64) {
596 let nx = x_scale.map(|s| s.map(xv)).unwrap_or(0.5);
597 let ny = y_scale.map(|s| s.map(yv)).unwrap_or(0.5);
598 coord.transform((nx, ny), &panel)
599 };
600 let nudged = |v: &Value, d: f64, discrete: bool| -> Value {
601 match v.as_f64() {
602 Some(f) if d != 0.0 && d.is_finite() && !discrete => Value::Float(f + d),
603 _ => v.clone(),
604 }
605 };
606 let x_discrete = x_scale.map(|s| s.is_discrete()).unwrap_or(false);
607 let y_discrete = y_scale.map(|s| s.is_discrete()).unwrap_or(false);
608
609 let mut points = Vec::with_capacity(data.nrows());
610 let mut items: Vec<Item> = Vec::new();
611 for i in 0..data.nrows() {
612 if x_col[i].is_na() || y_col[i].is_na() {
613 continue;
614 }
615 let anchor = to_px(&x_col[i], &y_col[i]);
616 if !(anchor.0.is_finite() && anchor.1.is_finite()) {
617 continue;
618 }
619 points.push(anchor);
620 let text = if label_col[i].is_na() {
621 String::new()
622 } else {
623 label_col[i].to_group_key()
624 };
625 if text.trim().is_empty() {
626 continue;
627 }
628 let target = to_px(
629 &nudged(&x_col[i], p.nudge_x, x_discrete),
630 &nudged(&y_col[i], p.nudge_y, y_discrete),
631 );
632 let target = if target.0.is_finite() && target.1.is_finite() {
633 target
634 } else {
635 anchor
636 };
637 items.push(Item {
638 row: i,
639 anchor,
640 target,
641 text,
642 });
643 }
644 if items.is_empty() {
645 return Ok(());
646 }
647
648 let pad = label.map(|(_, pd)| pd.max(0.0)).unwrap_or(0.0);
649 let half: Vec<(f64, f64)> = items
650 .iter()
651 .map(|it| {
652 (
653 it.text.chars().count() as f64 * size * 0.3 + pad,
654 size * 0.5 + pad,
655 )
656 })
657 .collect();
658 let anchors: Vec<(f64, f64)> = items.iter().map(|it| it.anchor).collect();
659 let targets: Vec<(f64, f64)> = items.iter().map(|it| it.target).collect();
660 let lay = layout(&anchors, &targets, &half, &points, &panel, p);
661
662 if lay.capped {
663 backend.warn(format!(
664 "geom_{name}: {} labels exceed the repel limit of {}; placed without \
665 repulsion (overlapping labels dropped)",
666 items.len(),
667 p.max_labels
668 ));
669 }
670 if lay.dropped > 0 {
671 backend.warn(format!(
672 "geom_{name}: {} unlabeled data point{} (too many overlaps). Consider \
673 increasing max_overlaps",
674 lay.dropped,
675 if lay.dropped == 1 { "" } else { "s" }
676 ));
677 }
678
679 let seg_min = if p.min_segment_length.is_finite() {
680 p.min_segment_length.max(0.0)
681 } else {
682 f64::INFINITY
683 };
684 let alpha = if alpha.is_finite() {
685 alpha.clamp(0.0, 1.0)
686 } else {
687 1.0
688 };
689
690 super::clear_mark(backend);
692 for (k, it) in items.iter().enumerate() {
693 if !lay.kept[k] {
694 continue;
695 }
696 let b = lay.boxes[k];
697 let d = b.dist_to(it.anchor.0, it.anchor.1);
698 if d <= seg_min || d <= 0.0 {
699 continue;
700 }
701 let (ex, ey) = b.closest(it.anchor.0, it.anchor.1);
702 let gap = if p.point_padding.is_finite() {
704 p.point_padding.max(0.0)
705 } else {
706 0.0
707 };
708 let (dx, dy) = (ex - it.anchor.0, ey - it.anchor.1);
709 let len = dx.hypot(dy);
710 if len <= gap {
711 continue;
712 }
713 let start = (it.anchor.0 + dx / len * gap, it.anchor.1 + dy / len * gap);
714 let c = p
715 .segment_color
716 .unwrap_or_else(|| row_color(scales, color_col, it.row, color));
717 backend.draw_line(
718 &[start, (ex, ey)],
719 &LineStyle {
720 color: c,
721 width: if p.segment_width.is_finite() {
722 p.segment_width.max(0.0)
723 } else {
724 0.6
725 },
726 alpha,
727 linetype: Linetype::Solid,
728 ..Default::default()
729 },
730 )?;
731 }
732
733 for (k, it) in items.iter().enumerate() {
734 if !lay.kept[k] {
735 continue;
736 }
737 let b = lay.boxes[k];
738 let c = row_color(scales, color_col, it.row, color);
739 if let Some((fill, _)) = label {
740 let f = fill_col
741 .and_then(|fc| scales.map_color(&Aesthetic::Fill, &fc[it.row]))
742 .unwrap_or(fill);
743 super::set_mark(
744 backend,
745 Some(it.text.clone()),
746 Some(super::tip_value(&x_col[it.row])),
747 super::series_key(data, it.row),
748 super::measured_value(data, it.row),
749 );
750 super::set_mark_formatted(
751 backend,
752 scales,
753 Some(&x_col[it.row]),
754 &Aesthetic::Y,
755 super::measured_raw(data, it.row),
756 );
757 backend.draw_rect(
758 (b.cx - b.hw, b.cy - b.hh),
759 (b.cx + b.hw, b.cy + b.hh),
760 &RectStyle {
761 fill: Some(f),
762 stroke: Some(c),
763 stroke_width: 0.5,
764 alpha,
765 clip: true,
766 corner_radius: 0.0,
767 },
768 )?;
769 super::clear_mark(backend);
770 }
771 backend.draw_text(
772 &it.text,
773 (b.cx, b.cy),
774 &TextStyle {
775 color: c,
776 size,
777 anchor: TextAnchor::Middle,
778 angle: 0.0,
779 family: super::resolve_font_family("", theme),
780 face,
781 },
782 )?;
783 }
784 Ok(())
785}
786
787fn row_color(
788 scales: &ScaleSet,
789 color_col: Option<&[Value]>,
790 row: usize,
791 fallback: (u8, u8, u8),
792) -> (u8, u8, u8) {
793 color_col
794 .and_then(|cc| scales.map_color(&Aesthetic::Color, &cc[row]))
795 .unwrap_or(fallback)
796}
797
798impl Geom for GeomTextRepel {
799 fn draw(
800 &self,
801 data: &DataFrame,
802 coord: &dyn Coord,
803 scales: &ScaleSet,
804 theme: &Theme,
805 backend: &mut dyn DrawBackend,
806 ) -> Result<(), RenderError> {
807 draw_repel(
808 "text_repel",
809 data,
810 coord,
811 scales,
812 theme,
813 backend,
814 self.size,
815 self.color,
816 self.alpha,
817 self.fontface,
818 None,
819 &self.repel,
820 )
821 }
822
823 fn required_aes(&self) -> Vec<Aesthetic> {
824 vec![Aesthetic::X, Aesthetic::Y, Aesthetic::Label]
825 }
826 fn default_stat(&self) -> Box<dyn Stat> {
827 Box::new(StatIdentity)
828 }
829 fn default_position(&self) -> Box<dyn Position> {
830 Box::new(PositionIdentity)
831 }
832 fn default_params(&self) -> GeomParams {
833 GeomParams::default()
834 }
835 fn name(&self) -> &str {
836 "text_repel"
837 }
838
839 fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
840 if let (true, Some(ink)) = (self.color == (0, 0, 0), el.ink) {
841 self.color = ink;
842 }
843 }
844}
845
846impl Geom for GeomLabelRepel {
847 fn draw(
848 &self,
849 data: &DataFrame,
850 coord: &dyn Coord,
851 scales: &ScaleSet,
852 theme: &Theme,
853 backend: &mut dyn DrawBackend,
854 ) -> Result<(), RenderError> {
855 draw_repel(
856 "label_repel",
857 data,
858 coord,
859 scales,
860 theme,
861 backend,
862 self.size,
863 self.color,
864 self.alpha,
865 self.fontface,
866 Some((self.fill, self.label_padding)),
867 &self.repel,
868 )
869 }
870
871 fn required_aes(&self) -> Vec<Aesthetic> {
872 vec![Aesthetic::X, Aesthetic::Y, Aesthetic::Label]
873 }
874 fn default_stat(&self) -> Box<dyn Stat> {
875 Box::new(StatIdentity)
876 }
877 fn default_position(&self) -> Box<dyn Position> {
878 Box::new(PositionIdentity)
879 }
880 fn default_params(&self) -> GeomParams {
881 GeomParams::default()
882 }
883 fn name(&self) -> &str {
884 "label_repel"
885 }
886
887 fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
888 if let (true, Some(ink)) = (self.color == (0, 0, 0), el.ink) {
889 self.color = ink;
890 }
891 if let (true, Some(paper)) = (self.fill == (255, 255, 255), el.paper) {
892 self.fill = paper;
893 }
894 }
895}
896
897#[cfg(test)]
898mod tests {
899 use super::*;
900
901 fn panel() -> Rect {
902 Rect {
903 x: 0.0,
904 y: 0.0,
905 width: 400.0,
906 height: 300.0,
907 }
908 }
909
910 fn overlapping(lay: &RepelLayout) -> usize {
911 let mut c = 0;
912 for i in 0..lay.boxes.len() {
913 for j in i + 1..lay.boxes.len() {
914 if lay.kept[i] && lay.kept[j] && lay.boxes[i].overlaps(&lay.boxes[j], 0.0) {
915 c += 1;
916 }
917 }
918 }
919 c
920 }
921
922 #[test]
923 fn coincident_labels_are_separated_deterministically() {
924 let anchors = vec![(200.0, 150.0); 6];
925 let half = vec![(20.0, 6.0); 6];
926 let p = RepelParams::default();
927 let a = layout(&anchors, &anchors, &half, &anchors, &panel(), &p);
928 let b = layout(&anchors, &anchors, &half, &anchors, &panel(), &p);
929 assert_eq!(overlapping(&a), 0);
930 assert_eq!(a.dropped, 0);
931 for (x, y) in a.boxes.iter().zip(&b.boxes) {
932 assert_eq!((x.cx, x.cy), (y.cx, y.cy), "same seed, same layout");
933 }
934 let c = layout(
935 &anchors,
936 &anchors,
937 &half,
938 &anchors,
939 &panel(),
940 &RepelParams {
941 seed: 7,
942 ..RepelParams::default()
943 },
944 );
945 assert_eq!(overlapping(&c), 0);
946 }
947
948 #[test]
949 fn labels_avoid_points_and_stay_inside() {
950 let anchors: Vec<(f64, f64)> = (0..30)
952 .map(|i| (5.0 + (i % 6) as f64 * 3.0, 5.0 + (i / 6) as f64 * 3.0))
953 .collect();
954 let half = vec![(15.0, 5.0); anchors.len()];
955 let lay = layout(
956 &anchors,
957 &anchors,
958 &half,
959 &anchors,
960 &panel(),
961 &RepelParams {
962 max_overlaps: usize::MAX,
963 ..RepelParams::default()
964 },
965 );
966 let pa = panel();
967 for b in &lay.boxes {
968 assert!(b.cx - b.hw >= pa.x - 1e-9 && b.cx + b.hw <= pa.x + pa.width + 1e-9);
969 assert!(b.cy - b.hh >= pa.y - 1e-9 && b.cy + b.hh <= pa.y + pa.height + 1e-9);
970 }
971 assert_eq!(overlapping(&lay), 0, "all 30 labels separated");
972 }
973
974 #[test]
975 fn max_overlaps_zero_drops_what_cannot_be_placed() {
976 let tiny = Rect {
978 x: 0.0,
979 y: 0.0,
980 width: 60.0,
981 height: 30.0,
982 };
983 let anchors = vec![(30.0, 15.0); 40];
984 let half = vec![(20.0, 6.0); 40];
985 let lay = layout(
986 &anchors,
987 &anchors,
988 &half,
989 &[],
990 &tiny,
991 &RepelParams {
992 max_overlaps: 0,
993 ..RepelParams::default()
994 },
995 );
996 assert!(lay.dropped > 0);
997 assert_eq!(overlapping(&lay), 0);
998 assert_eq!(lay.kept.iter().filter(|k| **k).count() + lay.dropped, 40);
999 }
1000
1001 #[test]
1002 fn direction_y_only_moves_vertically() {
1003 let anchors = vec![(100.0, 150.0), (101.0, 150.0), (102.0, 150.0)];
1004 let half = vec![(20.0, 6.0); 3];
1005 let lay = layout(
1006 &anchors,
1007 &anchors,
1008 &half,
1009 &[],
1010 &panel(),
1011 &RepelParams {
1012 direction: RepelDirection::Y,
1013 ..RepelParams::default()
1014 },
1015 );
1016 for (b, a) in lay.boxes.iter().zip(&anchors) {
1017 assert!((b.cx - a.0).abs() <= 0.5, "x stayed put");
1018 }
1019 assert_eq!(overlapping(&lay), 0);
1020 }
1021
1022 #[test]
1023 fn cap_skips_simulation() {
1024 let anchors: Vec<(f64, f64)> = (0..20).map(|i| (i as f64, 10.0)).collect();
1025 let half = vec![(5.0, 3.0); 20];
1026 let lay = layout(
1027 &anchors,
1028 &anchors,
1029 &half,
1030 &anchors,
1031 &panel(),
1032 &RepelParams {
1033 max_labels: 5,
1034 ..RepelParams::default()
1035 },
1036 );
1037 assert!(lay.capped);
1038 assert_eq!(overlapping(&lay), 0);
1039 }
1040
1041 #[test]
1042 fn non_finite_params_do_not_hang_or_panic() {
1043 let anchors = vec![(10.0, 10.0), (10.0, 10.0)];
1044 let half = vec![(5.0, 3.0); 2];
1045 let lay = layout(
1046 &anchors,
1047 &anchors,
1048 &half,
1049 &[(f64::NAN, 1.0)],
1050 &panel(),
1051 &RepelParams {
1052 force: f64::NAN,
1053 force_pull: f64::INFINITY,
1054 box_padding: f64::NAN,
1055 ..RepelParams::default()
1056 },
1057 );
1058 for b in &lay.boxes {
1059 assert!(b.cx.is_finite() && b.cy.is_finite());
1060 }
1061 }
1062}