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 pub avoid_ablines: Vec<(f64, f64)>,
94}
95
96impl Default for RepelParams {
97 fn default() -> Self {
98 RepelParams {
99 box_padding: 3.0,
100 point_padding: 2.0,
101 point_size: 3.0,
102 nudge_x: 0.0,
103 nudge_y: 0.0,
104 force: 1.0,
105 force_pull: 1.0,
106 max_iter: 2000,
107 max_time: std::time::Duration::from_millis(500),
108 max_overlaps: 10,
109 min_segment_length: 12.0,
110 seed: 42,
111 direction: RepelDirection::Both,
112 max_labels: 500,
113 segment_color: None,
114 segment_width: 0.6,
115 avoid_ablines: Vec::new(),
116 }
117 }
118}
119
120macro_rules! repel_builders {
121 ($t:ty) => {
122 impl $t {
123 pub fn box_padding(mut self, px: f64) -> Self {
125 self.repel.box_padding = px;
126 self
127 }
128 pub fn point_padding(mut self, px: f64) -> Self {
130 self.repel.point_padding = px;
131 self
132 }
133 pub fn point_size(mut self, px: f64) -> Self {
135 self.repel.point_size = px;
136 self
137 }
138 pub fn nudge(mut self, x: f64, y: f64) -> Self {
140 self.repel.nudge_x = x;
141 self.repel.nudge_y = y;
142 self
143 }
144 pub fn force(mut self, force: f64) -> Self {
146 self.repel.force = force;
147 self
148 }
149 pub fn force_pull(mut self, force: f64) -> Self {
151 self.repel.force_pull = force;
152 self
153 }
154 pub fn max_iter(mut self, n: usize) -> Self {
156 self.repel.max_iter = n;
157 self
158 }
159 pub fn max_time(mut self, t: std::time::Duration) -> Self {
161 self.repel.max_time = t;
162 self
163 }
164 pub fn max_overlaps(mut self, n: usize) -> Self {
166 self.repel.max_overlaps = n;
167 self
168 }
169 pub fn min_segment_length(mut self, px: f64) -> Self {
171 self.repel.min_segment_length = px;
172 self
173 }
174 pub fn seed(mut self, seed: u64) -> Self {
176 self.repel.seed = seed;
177 self
178 }
179 pub fn direction(mut self, d: RepelDirection) -> Self {
181 self.repel.direction = d;
182 self
183 }
184 pub fn max_labels(mut self, n: usize) -> Self {
186 self.repel.max_labels = n;
187 self
188 }
189 pub fn avoid_abline(mut self, slope: f64, intercept: f64) -> Self {
192 self.repel.avoid_ablines.push((slope, intercept));
193 self
194 }
195 pub fn segment_color(mut self, color: (u8, u8, u8)) -> Self {
197 self.repel.segment_color = Some(color);
198 self
199 }
200 }
201 };
202}
203
204pub struct GeomTextRepel {
208 pub size: f64,
209 pub color: (u8, u8, u8),
210 pub alpha: f64,
211 pub fontface: FontFace,
212 pub repel: RepelParams,
213}
214
215impl Default for GeomTextRepel {
216 fn default() -> Self {
217 GeomTextRepel {
218 size: 10.0,
219 color: (0, 0, 0),
220 alpha: 1.0,
221 fontface: FontFace::Plain,
222 repel: RepelParams::default(),
223 }
224 }
225}
226
227repel_builders!(GeomTextRepel);
228
229pub struct GeomLabelRepel {
232 pub size: f64,
233 pub color: (u8, u8, u8),
234 pub fill: (u8, u8, u8),
235 pub alpha: f64,
236 pub label_padding: f64,
238 pub fontface: FontFace,
239 pub repel: RepelParams,
240}
241
242impl Default for GeomLabelRepel {
243 fn default() -> Self {
244 GeomLabelRepel {
245 size: 10.0,
246 color: (0, 0, 0),
247 fill: (255, 255, 255),
248 alpha: 0.9,
249 label_padding: 3.0,
250 fontface: FontFace::Plain,
251 repel: RepelParams::default(),
252 }
253 }
254}
255
256repel_builders!(GeomLabelRepel);
257
258#[derive(Clone, Copy, Debug)]
260struct LabelBox {
261 cx: f64,
262 cy: f64,
263 hw: f64,
264 hh: f64,
265}
266
267impl LabelBox {
268 fn overlaps(&self, o: &LabelBox, pad: f64) -> bool {
269 (self.cx - o.cx).abs() < self.hw + o.hw + 2.0 * pad
270 && (self.cy - o.cy).abs() < self.hh + o.hh + 2.0 * pad
271 }
272
273 fn dist_to(&self, x: f64, y: f64) -> f64 {
275 let dx = ((x - self.cx).abs() - self.hw).max(0.0);
276 let dy = ((y - self.cy).abs() - self.hh).max(0.0);
277 dx.hypot(dy)
278 }
279
280 fn closest(&self, x: f64, y: f64) -> (f64, f64) {
282 (
283 x.clamp(self.cx - self.hw, self.cx + self.hw),
284 y.clamp(self.cy - self.hh, self.cy + self.hh),
285 )
286 }
287}
288
289pub(crate) struct RepelLayout {
292 boxes: Vec<LabelBox>,
293 kept: Vec<bool>,
294 dropped: usize,
295 capped: bool,
297}
298
299#[cfg(test)]
303pub(crate) fn layout(
304 anchors: &[(f64, f64)],
305 targets: &[(f64, f64)],
306 half: &[(f64, f64)],
307 points: &[(f64, f64)],
308 panel: &Rect,
309 p: &RepelParams,
310) -> RepelLayout {
311 layout_with_lines(anchors, targets, half, points, &[], panel, p)
312}
313
314fn segment_hits_box(a: (f64, f64), b: (f64, f64), bx: &LabelBox, pad: f64) -> bool {
316 let (dx, dy) = (b.0 - a.0, b.1 - a.1);
317 let (mut t0, mut t1) = (0.0f64, 1.0f64);
318 for (q, d, lo, hi) in [
319 (a.0, dx, bx.cx - bx.hw - pad, bx.cx + bx.hw + pad),
320 (a.1, dy, bx.cy - bx.hh - pad, bx.cy + bx.hh + pad),
321 ] {
322 if d == 0.0 {
323 if q < lo || q > hi {
324 return false;
325 }
326 continue;
327 }
328 let (mut ta, mut tb) = ((lo - q) / d, (hi - q) / d);
329 if ta > tb {
330 std::mem::swap(&mut ta, &mut tb);
331 }
332 t0 = t0.max(ta);
333 t1 = t1.min(tb);
334 if t0 > t1 {
335 return false;
336 }
337 }
338 true
339}
340
341pub(crate) fn layout_with_lines(
345 anchors: &[(f64, f64)],
346 targets: &[(f64, f64)],
347 half: &[(f64, f64)],
348 points: &[(f64, f64)],
349 lines: &[Seg],
350 panel: &Rect,
351 p: &RepelParams,
352) -> RepelLayout {
353 let lines: Vec<Seg> = lines
354 .iter()
355 .copied()
356 .filter(|(a, b)| {
357 [a.0, a.1, b.0, b.1].iter().all(|v| v.is_finite()) && (a.0 != b.0 || a.1 != b.1)
358 })
359 .collect();
360 let n = anchors.len();
361 let finite = |v: f64, d: f64| if v.is_finite() { v } else { d };
362 let box_pad = finite(p.box_padding, 0.0).max(0.0);
363 let pt_r = finite(p.point_size, 0.0).max(0.0) + finite(p.point_padding, 0.0).max(0.0);
364 let force = finite(p.force, 1.0).clamp(0.0, 100.0);
365 let pull = finite(p.force_pull, 1.0).clamp(0.0, 100.0);
366 let (x0, x1) = (panel.x, panel.x + panel.width);
367 let (y0, y1) = (panel.y, panel.y + panel.height);
368 let clamp_box = |b: &mut LabelBox| {
369 b.cx = if 2.0 * b.hw >= x1 - x0 {
371 (x0 + x1) / 2.0
372 } else {
373 b.cx.clamp(x0 + b.hw, x1 - b.hw)
374 };
375 b.cy = if 2.0 * b.hh >= y1 - y0 {
376 (y0 + y1) / 2.0
377 } else {
378 b.cy.clamp(y0 + b.hh, y1 - b.hh)
379 };
380 };
381
382 let blocked = |b: &LabelBox, along_x: bool, sign: f64| {
385 let eps = 1e-6;
386 if along_x {
387 (sign < 0.0 && b.cx - b.hw <= x0 + eps) || (sign > 0.0 && b.cx + b.hw >= x1 - eps)
388 } else {
389 (sign < 0.0 && b.cy - b.hh <= y0 + eps) || (sign > 0.0 && b.cy + b.hh >= y1 - eps)
390 }
391 };
392
393 let mut rng = crate::rng::SplitMix64::new(p.seed);
394 let mut boxes: Vec<LabelBox> = (0..n)
395 .map(|i| {
396 let mut b = LabelBox {
397 cx: targets[i].0 + rng.range_f64(-0.5, 0.5),
398 cy: targets[i].1 + rng.range_f64(-0.5, 0.5),
399 hw: half[i].0,
400 hh: half[i].1,
401 };
402 clamp_box(&mut b);
403 b
404 })
405 .collect();
406
407 let capped = n > p.max_labels;
408 let (move_x, move_y) = match p.direction {
409 RepelDirection::Both => (1.0, 1.0),
410 RepelDirection::X => (1.0, 0.0),
411 RepelDirection::Y => (0.0, 1.0),
412 };
413
414 let mut pts: Vec<(f64, f64)> = points
416 .iter()
417 .copied()
418 .filter(|(x, y)| x.is_finite() && y.is_finite())
419 .collect();
420 pts.sort_by(|a, b| a.0.total_cmp(&b.0));
421 let points_near = |pts: &[(f64, f64)], lo: f64, hi: f64| {
422 let start = pts.partition_point(|q| q.0 < lo);
423 let end = pts.partition_point(|q| q.0 <= hi);
424 start..end.max(start)
425 };
426
427 if !capped && n > 0 {
428 #[cfg(not(target_arch = "wasm32"))]
429 let started = std::time::Instant::now();
430 let mut order: Vec<usize> = (0..n).collect();
431 let mut delta = vec![(0.0f64, 0.0f64); n];
432 let mut hit = vec![false; n];
433 let pull_until = p.max_iter - p.max_iter / 4;
437 let max_step = 12.0;
440 let mut best: Option<(usize, Vec<LabelBox>)> = None;
445 for iter in 0..p.max_iter {
446 #[cfg(not(target_arch = "wasm32"))]
447 if iter % 16 == 15 && started.elapsed() >= p.max_time {
448 break;
449 }
450 for d in delta.iter_mut() {
451 *d = (0.0, 0.0);
452 }
453 hit.iter_mut().for_each(|h| *h = false);
454 let mut overlaps = 0usize;
455
456 order.sort_by(|&a, &b| {
458 (boxes[a].cx - boxes[a].hw).total_cmp(&(boxes[b].cx - boxes[b].hw))
459 });
460 for (oi, &i) in order.iter().enumerate() {
461 let bi = boxes[i];
462 for &j in &order[oi + 1..] {
463 let bj = boxes[j];
464 if bj.cx - bj.hw - box_pad >= bi.cx + bi.hw + box_pad {
465 break;
466 }
467 if !bi.overlaps(&bj, box_pad) {
468 continue;
469 }
470 overlaps += 1;
471 hit[i] = true;
472 hit[j] = true;
473 let (dx, dy) = (bi.cx - bj.cx, bi.cy - bj.cy);
474 let ox = bi.hw + bj.hw + 2.0 * box_pad - dx.abs();
475 let oy = bi.hh + bj.hh + 2.0 * box_pad - dy.abs();
476 let sx = if dx == 0.0 {
477 tie(&mut rng)
478 } else {
479 dx.signum()
480 };
481 let sy = if dy == 0.0 {
482 tie(&mut rng)
483 } else {
484 dy.signum()
485 };
486 let mut along_x = match p.direction {
489 RepelDirection::X => true,
490 RepelDirection::Y => false,
491 RepelDirection::Both => ox * move_x <= oy * move_y,
492 };
493 let s = |x: bool| if x { sx } else { sy };
494 let pinned = |x: bool| (blocked(&bi, x, s(x)), blocked(&bj, x, -s(x)));
498 if p.direction == RepelDirection::Both && pinned(along_x) == (true, true) {
499 along_x = !along_x;
500 }
501 let (pi, pj) = pinned(along_x);
502 let (ki, kj) = match (pi, pj) {
503 (true, false) => (0.0, force),
504 (false, true) => (force, 0.0),
505 _ => (0.5 * force, 0.5 * force),
506 };
507 if along_x {
508 delta[i].0 += sx * ox * ki;
509 delta[j].0 -= sx * ox * kj;
510 } else {
511 delta[i].1 += sy * oy * ki;
512 delta[j].1 -= sy * oy * kj;
513 }
514 }
515 }
516
517 let label_overlaps = overlaps;
518 let final_phase = iter >= pull_until;
519 let label_hit = if final_phase { hit.clone() } else { Vec::new() };
522 let free = |i: usize| !final_phase || !label_hit[i];
523
524 let point_force = if final_phase { 0.5 * force } else { force };
529 for i in 0..n {
530 if point_force == 0.0 {
531 break;
532 }
533 if !free(i) {
534 continue;
535 }
536 let b = boxes[i];
537 let r = pt_r + box_pad;
538 for q in &pts[points_near(&pts, b.cx - b.hw - r, b.cx + b.hw + r)] {
539 let (dx, dy) = (b.cx - q.0, b.cy - q.1);
540 let ox = b.hw + r - dx.abs();
541 let oy = b.hh + r - dy.abs();
542 if ox <= 0.0 || oy <= 0.0 {
543 continue;
544 }
545 overlaps += 1;
546 hit[i] = true;
547 let sx = if dx == 0.0 {
548 tie(&mut rng)
549 } else {
550 dx.signum()
551 };
552 let sy = if dy == 0.0 {
553 tie(&mut rng)
554 } else {
555 dy.signum()
556 };
557 let mut along_x = match p.direction {
558 RepelDirection::X => true,
559 RepelDirection::Y => false,
560 RepelDirection::Both => ox <= oy,
561 };
562 let (mut sx, mut sy, mut ox, mut oy) = (sx, sy, ox, oy);
563 if blocked(&b, along_x, if along_x { sx } else { sy }) {
568 let other = !along_x;
569 if p.direction == RepelDirection::Both
570 && !blocked(&b, other, if other { sx } else { sy })
571 {
572 along_x = other;
573 } else if along_x {
574 sx = -sx;
575 ox = 2.0 * (b.hw + r) - ox;
576 } else {
577 sy = -sy;
578 oy = 2.0 * (b.hh + r) - oy;
579 }
580 }
581 if along_x {
582 delta[i].0 += sx * ox * point_force;
583 } else {
584 delta[i].1 += sy * oy * point_force;
585 }
586 }
587 }
588
589 for i in 0..n {
592 if point_force == 0.0 {
593 break;
594 }
595 if !free(i) {
596 continue;
597 }
598 let b = boxes[i];
599 for &(a, e) in &lines {
600 if !segment_hits_box(a, e, &b, box_pad) {
601 continue;
602 }
603 overlaps += 1;
604 hit[i] = true;
605 let (lx, ly) = (e.0 - a.0, e.1 - a.1);
606 let len = lx.hypot(ly);
607 let (nx, ny) = (-ly / len, lx / len);
608 let d = (b.cx - a.0) * nx + (b.cy - a.1) * ny;
609 let side = if d == 0.0 { tie(&mut rng) } else { d.signum() };
610 let need = b.hw * nx.abs() + b.hh * ny.abs() + box_pad - d.abs();
611 if need > 0.0 {
612 delta[i].0 += side * nx * need * point_force;
613 delta[i].1 += side * ny * need * point_force;
614 }
615 }
616 }
617
618 if final_phase && label_overlaps == 0 {
619 let covered = overlaps - label_overlaps;
620 if best.as_ref().is_none_or(|(c, _)| covered < *c) {
621 best = Some((covered, boxes.clone()));
622 }
623 if covered == 0 {
624 break;
625 }
626 }
627
628 let mut moved = 0.0f64;
630 for i in 0..n {
631 let b = &mut boxes[i];
632 let k = if iter < pull_until && !hit[i] {
635 0.1 * pull
636 } else {
637 0.0
638 };
639 let sx = (targets[i].0 - b.cx) * k;
640 let sy = (targets[i].1 - b.cy) * k;
641 let cap = |v: f64| {
642 if v.is_finite() {
643 v.clamp(-max_step, max_step)
644 } else {
645 0.0
646 }
647 };
648 let (mx, my) = (cap(delta[i].0 + sx) * move_x, cap(delta[i].1 + sy) * move_y);
649 let (px, py) = (b.cx, b.cy);
650 b.cx += mx;
651 b.cy += my;
652 clamp_box(b);
653 moved = moved.max((b.cx - px).abs() + (b.cy - py).abs());
654 }
655 if (overlaps == 0 && moved < 0.05)
656 || (final_phase && label_overlaps == 0 && moved < 0.05)
657 {
658 break;
659 }
660 }
661 if let Some((_, b)) = best {
665 boxes = b;
666 }
667 }
668
669 let mut kept = vec![false; n];
672 let mut dropped = 0usize;
673 let mut kept_boxes: Vec<LabelBox> = Vec::new();
674 let limit = if capped { 0 } else { p.max_overlaps };
675 for i in 0..n {
676 let b = boxes[i];
677 let mut count = kept_boxes.iter().filter(|o| b.overlaps(o, 0.0)).count();
678 if count <= limit && !capped {
680 let r = pt_r;
681 count += pts[points_near(&pts, b.cx - b.hw - r, b.cx + b.hw + r)]
682 .iter()
683 .filter(|q| {
684 !(q.0 == anchors[i].0 && q.1 == anchors[i].1)
685 && (q.0 - b.cx).abs() < b.hw + r
686 && (q.1 - b.cy).abs() < b.hh + r
687 })
688 .count();
689 }
690 if count > limit {
691 dropped += 1;
692 } else {
693 kept[i] = true;
694 kept_boxes.push(b);
695 }
696 }
697
698 RepelLayout {
699 boxes,
700 kept,
701 dropped,
702 capped,
703 }
704}
705
706fn tie(rng: &mut crate::rng::SplitMix64) -> f64 {
708 if rng.next_u64() & 1 == 0 {
709 -1.0
710 } else {
711 1.0
712 }
713}
714
715type Seg = ((f64, f64), (f64, f64));
717
718struct Item {
720 row: usize,
721 anchor: (f64, f64),
722 target: (f64, f64),
723 text: String,
724}
725
726#[allow(clippy::too_many_arguments)]
728fn draw_repel(
729 name: &str,
730 data: &DataFrame,
731 coord: &dyn Coord,
732 scales: &ScaleSet,
733 theme: &Theme,
734 backend: &mut dyn DrawBackend,
735 size: f64,
736 color: (u8, u8, u8),
737 alpha: f64,
738 face: FontFace,
739 label: Option<((u8, u8, u8), f64)>,
740 p: &RepelParams,
741) -> Result<(), RenderError> {
742 let x_col = data
743 .column("x")
744 .ok_or(RenderError::MissingAesthetic("x".into()))?;
745 let y_col = data
746 .column("y")
747 .ok_or(RenderError::MissingAesthetic("y".into()))?;
748 let label_col = data
749 .column("label")
750 .ok_or(RenderError::MissingAesthetic("label".into()))?;
751 let color_col = data.column("color");
752 let fill_col = data.column("fill");
753
754 let panel = backend.plot_area();
755 let x_scale = scales.get(&Aesthetic::X);
756 let y_scale = scales.get(&Aesthetic::Y);
757 let size = if size.is_finite() && size > 0.0 {
758 size
759 } else {
760 10.0
761 };
762
763 let to_px = |xv: &Value, yv: &Value| -> (f64, f64) {
764 let nx = x_scale.map(|s| s.map(xv)).unwrap_or(0.5);
765 let ny = y_scale.map(|s| s.map(yv)).unwrap_or(0.5);
766 coord.transform((nx, ny), &panel)
767 };
768 let nudged = |v: &Value, d: f64, discrete: bool| -> Value {
769 match v.as_f64() {
770 Some(f) if d != 0.0 && d.is_finite() && !discrete => Value::Float(f + d),
771 _ => v.clone(),
772 }
773 };
774 let x_discrete = x_scale.map(|s| s.is_discrete()).unwrap_or(false);
775 let y_discrete = y_scale.map(|s| s.is_discrete()).unwrap_or(false);
776
777 let mut points = Vec::with_capacity(data.nrows());
778 let mut items: Vec<Item> = Vec::new();
779 for i in 0..data.nrows() {
780 if x_col[i].is_na() || y_col[i].is_na() {
781 continue;
782 }
783 let anchor = to_px(&x_col[i], &y_col[i]);
784 if !(anchor.0.is_finite() && anchor.1.is_finite()) {
785 continue;
786 }
787 points.push(anchor);
788 let text = if label_col[i].is_na() {
789 String::new()
790 } else {
791 label_col[i].to_group_key()
792 };
793 if text.trim().is_empty() {
794 continue;
795 }
796 let target = to_px(
797 &nudged(&x_col[i], p.nudge_x, x_discrete),
798 &nudged(&y_col[i], p.nudge_y, y_discrete),
799 );
800 let target = if target.0.is_finite() && target.1.is_finite() {
801 target
802 } else {
803 anchor
804 };
805 items.push(Item {
806 row: i,
807 anchor,
808 target,
809 text,
810 });
811 }
812 if items.is_empty() {
813 return Ok(());
814 }
815
816 let pad = label.map(|(_, pd)| pd.max(0.0)).unwrap_or(0.0);
817 let half: Vec<(f64, f64)> = items
818 .iter()
819 .map(|it| {
820 (
821 it.text.chars().count() as f64 * size * 0.3 + pad,
822 size * 0.5 + pad,
823 )
824 })
825 .collect();
826 let anchors: Vec<(f64, f64)> = items.iter().map(|it| it.anchor).collect();
827 let targets: Vec<(f64, f64)> = items.iter().map(|it| it.target).collect();
828 let mut lines = Vec::new();
831 if !p.avoid_ablines.is_empty() && !x_discrete && !y_discrete {
832 let xs: Vec<f64> = x_col
833 .iter()
834 .filter_map(|v| v.as_f64())
835 .filter(|v| v.is_finite())
836 .collect();
837 if let (Some(lo), Some(hi)) = (
838 xs.iter().copied().reduce(f64::min),
839 xs.iter().copied().reduce(f64::max),
840 ) {
841 let w = (hi - lo).max(1.0);
842 let (lo, hi) = (lo - 4.0 * w, hi + 4.0 * w);
843 for &(slope, intercept) in &p.avoid_ablines {
844 if !(slope.is_finite() && intercept.is_finite()) {
845 continue;
846 }
847 let at = |x: f64| to_px(&Value::Float(x), &Value::Float(intercept + slope * x));
848 lines.push((at(lo), at(hi)));
849 }
850 }
851 }
852 let lay = layout_with_lines(&anchors, &targets, &half, &points, &lines, &panel, p);
853
854 if lay.capped {
855 backend.warn(format!(
856 "geom_{name}: {} labels exceed the repel limit of {}; placed without \
857 repulsion (overlapping labels dropped)",
858 items.len(),
859 p.max_labels
860 ));
861 }
862 if lay.dropped > 0 {
863 backend.warn(format!(
864 "geom_{name}: {} unlabeled data point{} (too many overlaps). Consider \
865 increasing max_overlaps",
866 lay.dropped,
867 if lay.dropped == 1 { "" } else { "s" }
868 ));
869 }
870
871 let seg_min = if p.min_segment_length.is_finite() {
872 p.min_segment_length.max(0.0)
873 } else {
874 f64::INFINITY
875 };
876 let alpha = if alpha.is_finite() {
877 alpha.clamp(0.0, 1.0)
878 } else {
879 1.0
880 };
881
882 super::clear_mark(backend);
884 for (k, it) in items.iter().enumerate() {
885 if !lay.kept[k] {
886 continue;
887 }
888 let b = lay.boxes[k];
889 let d = b.dist_to(it.anchor.0, it.anchor.1);
890 if d <= seg_min || d <= 0.0 {
891 continue;
892 }
893 let (ex, ey) = b.closest(it.anchor.0, it.anchor.1);
894 let gap = if p.point_padding.is_finite() {
896 p.point_padding.max(0.0)
897 } else {
898 0.0
899 };
900 let (dx, dy) = (ex - it.anchor.0, ey - it.anchor.1);
901 let len = dx.hypot(dy);
902 if len <= gap {
903 continue;
904 }
905 let start = (it.anchor.0 + dx / len * gap, it.anchor.1 + dy / len * gap);
906 let c = p
907 .segment_color
908 .unwrap_or_else(|| row_color(scales, color_col, it.row, color));
909 backend.draw_line(
910 &[start, (ex, ey)],
911 &LineStyle {
912 color: c,
913 width: if p.segment_width.is_finite() {
914 p.segment_width.max(0.0)
915 } else {
916 0.6
917 },
918 alpha,
919 linetype: Linetype::Solid,
920 ..Default::default()
921 },
922 )?;
923 }
924
925 for (k, it) in items.iter().enumerate() {
926 if !lay.kept[k] {
927 continue;
928 }
929 let b = lay.boxes[k];
930 let c = row_color(scales, color_col, it.row, color);
931 if let Some((fill, _)) = label {
932 let f = fill_col
933 .and_then(|fc| scales.map_color(&Aesthetic::Fill, &fc[it.row]))
934 .unwrap_or(fill);
935 super::set_mark(
936 backend,
937 Some(it.text.clone()),
938 Some(super::tip_value(&x_col[it.row])),
939 super::series_key(data, it.row),
940 super::measured_value(data, it.row),
941 );
942 super::set_mark_formatted(
943 backend,
944 scales,
945 Some(&x_col[it.row]),
946 &Aesthetic::Y,
947 super::measured_raw(data, it.row),
948 );
949 backend.draw_rect(
950 (b.cx - b.hw, b.cy - b.hh),
951 (b.cx + b.hw, b.cy + b.hh),
952 &RectStyle {
953 fill: Some(f),
954 stroke: Some(c),
955 stroke_width: 0.5,
956 alpha,
957 clip: true,
958 corner_radius: 0.0,
959 },
960 )?;
961 super::clear_mark(backend);
962 }
963 backend.draw_text(
964 &it.text,
965 (b.cx, b.cy),
966 &TextStyle {
967 color: c,
968 size,
969 anchor: TextAnchor::Middle,
970 angle: 0.0,
971 family: super::resolve_font_family("", theme),
972 face,
973 bg_colour: None,
974 bg_r: 0.0,
975 },
976 )?;
977 }
978 Ok(())
979}
980
981fn row_color(
982 scales: &ScaleSet,
983 color_col: Option<&[Value]>,
984 row: usize,
985 fallback: (u8, u8, u8),
986) -> (u8, u8, u8) {
987 color_col
988 .and_then(|cc| scales.map_color(&Aesthetic::Color, &cc[row]))
989 .unwrap_or(fallback)
990}
991
992impl Geom for GeomTextRepel {
993 fn draw(
994 &self,
995 data: &DataFrame,
996 coord: &dyn Coord,
997 scales: &ScaleSet,
998 theme: &Theme,
999 backend: &mut dyn DrawBackend,
1000 ) -> Result<(), RenderError> {
1001 draw_repel(
1002 "text_repel",
1003 data,
1004 coord,
1005 scales,
1006 theme,
1007 backend,
1008 self.size,
1009 self.color,
1010 self.alpha,
1011 self.fontface,
1012 None,
1013 &self.repel,
1014 )
1015 }
1016
1017 fn required_aes(&self) -> Vec<Aesthetic> {
1018 vec![Aesthetic::X, Aesthetic::Y, Aesthetic::Label]
1019 }
1020 fn default_stat(&self) -> Box<dyn Stat> {
1021 Box::new(StatIdentity)
1022 }
1023 fn default_position(&self) -> Box<dyn Position> {
1024 Box::new(PositionIdentity)
1025 }
1026 fn default_params(&self) -> GeomParams {
1027 GeomParams::default()
1028 }
1029 fn name(&self) -> &str {
1030 "text_repel"
1031 }
1032
1033 fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
1034 if let (true, Some(ink)) = (self.color == (0, 0, 0), el.ink) {
1035 self.color = ink;
1036 }
1037 }
1038}
1039
1040impl Geom for GeomLabelRepel {
1041 fn draw(
1042 &self,
1043 data: &DataFrame,
1044 coord: &dyn Coord,
1045 scales: &ScaleSet,
1046 theme: &Theme,
1047 backend: &mut dyn DrawBackend,
1048 ) -> Result<(), RenderError> {
1049 draw_repel(
1050 "label_repel",
1051 data,
1052 coord,
1053 scales,
1054 theme,
1055 backend,
1056 self.size,
1057 self.color,
1058 self.alpha,
1059 self.fontface,
1060 Some((self.fill, self.label_padding)),
1061 &self.repel,
1062 )
1063 }
1064
1065 fn required_aes(&self) -> Vec<Aesthetic> {
1066 vec![Aesthetic::X, Aesthetic::Y, Aesthetic::Label]
1067 }
1068 fn default_stat(&self) -> Box<dyn Stat> {
1069 Box::new(StatIdentity)
1070 }
1071 fn default_position(&self) -> Box<dyn Position> {
1072 Box::new(PositionIdentity)
1073 }
1074 fn default_params(&self) -> GeomParams {
1075 GeomParams::default()
1076 }
1077 fn name(&self) -> &str {
1078 "label_repel"
1079 }
1080
1081 fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
1082 if let (true, Some(ink)) = (self.color == (0, 0, 0), el.ink) {
1083 self.color = ink;
1084 }
1085 if let (true, Some(paper)) = (self.fill == (255, 255, 255), el.paper) {
1086 self.fill = paper;
1087 }
1088 }
1089}
1090
1091#[cfg(test)]
1092mod tests {
1093 use super::*;
1094
1095 fn panel() -> Rect {
1096 Rect {
1097 x: 0.0,
1098 y: 0.0,
1099 width: 400.0,
1100 height: 300.0,
1101 }
1102 }
1103
1104 fn overlapping(lay: &RepelLayout) -> usize {
1105 let mut c = 0;
1106 for i in 0..lay.boxes.len() {
1107 for j in i + 1..lay.boxes.len() {
1108 if lay.kept[i] && lay.kept[j] && lay.boxes[i].overlaps(&lay.boxes[j], 0.0) {
1109 c += 1;
1110 }
1111 }
1112 }
1113 c
1114 }
1115
1116 #[test]
1117 fn coincident_labels_are_separated_deterministically() {
1118 let anchors = vec![(200.0, 150.0); 6];
1119 let half = vec![(20.0, 6.0); 6];
1120 let p = RepelParams::default();
1121 let a = layout(&anchors, &anchors, &half, &anchors, &panel(), &p);
1122 let b = layout(&anchors, &anchors, &half, &anchors, &panel(), &p);
1123 assert_eq!(overlapping(&a), 0);
1124 assert_eq!(a.dropped, 0);
1125 for (x, y) in a.boxes.iter().zip(&b.boxes) {
1126 assert_eq!((x.cx, x.cy), (y.cx, y.cy), "same seed, same layout");
1127 }
1128 let c = layout(
1129 &anchors,
1130 &anchors,
1131 &half,
1132 &anchors,
1133 &panel(),
1134 &RepelParams {
1135 seed: 7,
1136 ..RepelParams::default()
1137 },
1138 );
1139 assert_eq!(overlapping(&c), 0);
1140 }
1141
1142 #[test]
1143 fn crowded_top_edge_labels_end_clear_of_labels_and_points() {
1144 let pan = Rect {
1148 x: 0.0,
1149 y: 0.0,
1150 width: 440.0,
1151 height: 260.0,
1152 };
1153 let px = |v: f64| 10.0 + v / 33.0 * 420.0;
1154 let py = |p: f64| 17.0 + (100.0 - p) / 80.0 * 226.0;
1155 let pts: Vec<(f64, f64)> = [
1156 (1.0, 100.0),
1157 (2.0, 100.0),
1158 (4.0, 100.0),
1159 (4.4, 100.0),
1160 (5.0, 100.0),
1161 (7.0, 100.0),
1162 (7.4, 97.0),
1163 (6.0, 83.0),
1164 (9.0, 100.0),
1165 (16.0, 41.0),
1166 (16.5, 24.0),
1167 (18.0, 27.0),
1168 ]
1169 .iter()
1170 .map(|&(v, p)| (px(v), py(p)))
1171 .collect();
1172 let half = vec![(21.0, 6.0); pts.len()];
1173 let lay = layout(&pts, &pts, &half, &pts, &pan, &RepelParams::default());
1174 assert_eq!(overlapping(&lay), 0);
1175 assert_eq!(lay.dropped, 0);
1176 let r = RepelParams::default().point_size;
1178 let covered: Vec<usize> = (0..pts.len())
1179 .filter(|&i| {
1180 let b = lay.boxes[i];
1181 pts.iter().enumerate().any(|(j, q)| {
1182 j != i && (q.0 - b.cx).abs() < b.hw + r && (q.1 - b.cy).abs() < b.hh + r
1183 })
1184 })
1185 .collect();
1186 assert!(covered.is_empty(), "labels on other points: {covered:?}");
1187 }
1188
1189 #[test]
1190 fn labels_avoid_points_and_stay_inside() {
1191 let anchors: Vec<(f64, f64)> = (0..30)
1193 .map(|i| (5.0 + (i % 6) as f64 * 3.0, 5.0 + (i / 6) as f64 * 3.0))
1194 .collect();
1195 let half = vec![(15.0, 5.0); anchors.len()];
1196 let lay = layout(
1197 &anchors,
1198 &anchors,
1199 &half,
1200 &anchors,
1201 &panel(),
1202 &RepelParams {
1203 max_overlaps: usize::MAX,
1204 ..RepelParams::default()
1205 },
1206 );
1207 let pa = panel();
1208 for b in &lay.boxes {
1209 assert!(b.cx - b.hw >= pa.x - 1e-9 && b.cx + b.hw <= pa.x + pa.width + 1e-9);
1210 assert!(b.cy - b.hh >= pa.y - 1e-9 && b.cy + b.hh <= pa.y + pa.height + 1e-9);
1211 }
1212 assert_eq!(overlapping(&lay), 0, "all 30 labels separated");
1213 }
1214
1215 #[test]
1216 fn max_overlaps_zero_drops_what_cannot_be_placed() {
1217 let tiny = Rect {
1219 x: 0.0,
1220 y: 0.0,
1221 width: 60.0,
1222 height: 30.0,
1223 };
1224 let anchors = vec![(30.0, 15.0); 40];
1225 let half = vec![(20.0, 6.0); 40];
1226 let lay = layout(
1227 &anchors,
1228 &anchors,
1229 &half,
1230 &[],
1231 &tiny,
1232 &RepelParams {
1233 max_overlaps: 0,
1234 ..RepelParams::default()
1235 },
1236 );
1237 assert!(lay.dropped > 0);
1238 assert_eq!(overlapping(&lay), 0);
1239 assert_eq!(lay.kept.iter().filter(|k| **k).count() + lay.dropped, 40);
1240 }
1241
1242 #[test]
1243 fn direction_y_only_moves_vertically() {
1244 let anchors = vec![(100.0, 150.0), (101.0, 150.0), (102.0, 150.0)];
1245 let half = vec![(20.0, 6.0); 3];
1246 let lay = layout(
1247 &anchors,
1248 &anchors,
1249 &half,
1250 &[],
1251 &panel(),
1252 &RepelParams {
1253 direction: RepelDirection::Y,
1254 ..RepelParams::default()
1255 },
1256 );
1257 for (b, a) in lay.boxes.iter().zip(&anchors) {
1258 assert!((b.cx - a.0).abs() <= 0.5, "x stayed put");
1259 }
1260 assert_eq!(overlapping(&lay), 0);
1261 }
1262
1263 #[test]
1264 fn cap_skips_simulation() {
1265 let anchors: Vec<(f64, f64)> = (0..20).map(|i| (i as f64, 10.0)).collect();
1266 let half = vec![(5.0, 3.0); 20];
1267 let lay = layout(
1268 &anchors,
1269 &anchors,
1270 &half,
1271 &anchors,
1272 &panel(),
1273 &RepelParams {
1274 max_labels: 5,
1275 ..RepelParams::default()
1276 },
1277 );
1278 assert!(lay.capped);
1279 assert_eq!(overlapping(&lay), 0);
1280 }
1281
1282 #[test]
1283 fn non_finite_params_do_not_hang_or_panic() {
1284 let anchors = vec![(10.0, 10.0), (10.0, 10.0)];
1285 let half = vec![(5.0, 3.0); 2];
1286 let lay = layout(
1287 &anchors,
1288 &anchors,
1289 &half,
1290 &[(f64::NAN, 1.0)],
1291 &panel(),
1292 &RepelParams {
1293 force: f64::NAN,
1294 force_pull: f64::INFINITY,
1295 box_padding: f64::NAN,
1296 ..RepelParams::default()
1297 },
1298 );
1299 for b in &lay.boxes {
1300 assert!(b.cx.is_finite() && b.cy.is_finite());
1301 }
1302 }
1303
1304 #[test]
1305 fn labels_move_off_a_line() {
1306 let anchors = vec![(100.0, 100.0), (200.0, 200.0), (300.0, 300.0)];
1308 let half = vec![(20.0, 6.0); 3];
1309 let line = ((0.0, 0.0), (400.0, 400.0));
1310 let lay = layout_with_lines(
1311 &anchors,
1312 &anchors,
1313 &half,
1314 &anchors,
1315 &[line],
1316 &panel(),
1317 &RepelParams::default(),
1318 );
1319 for (k, b) in lay.boxes.iter().enumerate() {
1320 assert!(lay.kept[k]);
1321 assert!(
1322 !segment_hits_box(line.0, line.1, b, 0.0),
1323 "label {k} still on the line: {b:?}"
1324 );
1325 }
1326 }
1327
1328 #[test]
1329 fn segment_box_test() {
1330 let b = LabelBox {
1331 cx: 50.0,
1332 cy: 50.0,
1333 hw: 10.0,
1334 hh: 5.0,
1335 };
1336 assert!(segment_hits_box((0.0, 0.0), (100.0, 100.0), &b, 0.0));
1337 assert!(segment_hits_box((0.0, 50.0), (100.0, 50.0), &b, 0.0));
1338 assert!(!segment_hits_box((0.0, 70.0), (100.0, 70.0), &b, 0.0));
1339 assert!(segment_hits_box((0.0, 57.0), (100.0, 57.0), &b, 3.0));
1340 assert!(!segment_hits_box((0.0, 0.0), (30.0, 30.0), &b, 0.0));
1341 }
1342
1343 #[test]
1344 fn avoid_abline_in_a_plot() {
1345 let render = |avoid: bool| {
1346 let xs: Vec<Value> = (0..8).map(|i| Value::Float(i as f64)).collect();
1347 let labels: Vec<Value> = (0..8).map(|i| Value::Str(format!("point {i}"))).collect();
1348 crate::plot::GGPlot::new(vec![
1349 ("x".into(), xs.clone()),
1350 ("y".into(), xs),
1351 ("l".into(), labels),
1352 ])
1353 .aes(crate::aes::Aes::new().x("x").y("y").label("l"))
1354 .geom_point()
1355 .geom_text_repel_with(if avoid {
1356 GeomTextRepel::default().avoid_abline(1.0, 0.0)
1357 } else {
1358 GeomTextRepel::default()
1359 })
1360 .render_svg_native_with_size(400, 300)
1361 .unwrap()
1362 };
1363 let (with, without) = (render(true), render(false));
1364 assert_eq!(
1365 with.matches(">point ").count(),
1366 without.matches(">point ").count(),
1367 "no label dropped"
1368 );
1369 assert_ne!(with, without, "the line moved the labels");
1370 }
1371}