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 mut rng = crate::rng::SplitMix64::new(p.seed);
383 let mut boxes: Vec<LabelBox> = (0..n)
384 .map(|i| {
385 let mut b = LabelBox {
386 cx: targets[i].0 + rng.range_f64(-0.5, 0.5),
387 cy: targets[i].1 + rng.range_f64(-0.5, 0.5),
388 hw: half[i].0,
389 hh: half[i].1,
390 };
391 clamp_box(&mut b);
392 b
393 })
394 .collect();
395
396 let capped = n > p.max_labels;
397 let (move_x, move_y) = match p.direction {
398 RepelDirection::Both => (1.0, 1.0),
399 RepelDirection::X => (1.0, 0.0),
400 RepelDirection::Y => (0.0, 1.0),
401 };
402
403 let mut pts: Vec<(f64, f64)> = points
405 .iter()
406 .copied()
407 .filter(|(x, y)| x.is_finite() && y.is_finite())
408 .collect();
409 pts.sort_by(|a, b| a.0.total_cmp(&b.0));
410 let points_near = |pts: &[(f64, f64)], lo: f64, hi: f64| {
411 let start = pts.partition_point(|q| q.0 < lo);
412 let end = pts.partition_point(|q| q.0 <= hi);
413 start..end.max(start)
414 };
415
416 if !capped && n > 0 {
417 #[cfg(not(target_arch = "wasm32"))]
418 let started = std::time::Instant::now();
419 let mut order: Vec<usize> = (0..n).collect();
420 let mut delta = vec![(0.0f64, 0.0f64); n];
421 let mut hit = vec![false; n];
422 let pull_until = p.max_iter - p.max_iter / 4;
426 let max_step = 12.0;
429 for iter in 0..p.max_iter {
430 #[cfg(not(target_arch = "wasm32"))]
431 if iter % 16 == 15 && started.elapsed() >= p.max_time {
432 break;
433 }
434 for d in delta.iter_mut() {
435 *d = (0.0, 0.0);
436 }
437 hit.iter_mut().for_each(|h| *h = false);
438 let mut overlaps = 0usize;
439
440 order.sort_by(|&a, &b| {
442 (boxes[a].cx - boxes[a].hw).total_cmp(&(boxes[b].cx - boxes[b].hw))
443 });
444 for (oi, &i) in order.iter().enumerate() {
445 let bi = boxes[i];
446 for &j in &order[oi + 1..] {
447 let bj = boxes[j];
448 if bj.cx - bj.hw - box_pad >= bi.cx + bi.hw + box_pad {
449 break;
450 }
451 if !bi.overlaps(&bj, box_pad) {
452 continue;
453 }
454 overlaps += 1;
455 hit[i] = true;
456 hit[j] = true;
457 let (dx, dy) = (bi.cx - bj.cx, bi.cy - bj.cy);
458 let ox = bi.hw + bj.hw + 2.0 * box_pad - dx.abs();
459 let oy = bi.hh + bj.hh + 2.0 * box_pad - dy.abs();
460 let sx = if dx == 0.0 {
461 tie(&mut rng)
462 } else {
463 dx.signum()
464 };
465 let sy = if dy == 0.0 {
466 tie(&mut rng)
467 } else {
468 dy.signum()
469 };
470 let along_x = match p.direction {
473 RepelDirection::X => true,
474 RepelDirection::Y => false,
475 RepelDirection::Both => ox * move_x <= oy * move_y,
476 };
477 let k = 0.5 * force;
478 if along_x {
479 delta[i].0 += sx * ox * k;
480 delta[j].0 -= sx * ox * k;
481 } else {
482 delta[i].1 += sy * oy * k;
483 delta[j].1 -= sy * oy * k;
484 }
485 }
486 }
487
488 let label_overlaps = overlaps;
489
490 let point_force = if iter < pull_until { force } else { 0.0 };
494 for i in 0..n {
495 if point_force == 0.0 {
496 break;
497 }
498 let b = boxes[i];
499 let r = pt_r + box_pad;
500 for q in &pts[points_near(&pts, b.cx - b.hw - r, b.cx + b.hw + r)] {
501 let (dx, dy) = (b.cx - q.0, b.cy - q.1);
502 let ox = b.hw + r - dx.abs();
503 let oy = b.hh + r - dy.abs();
504 if ox <= 0.0 || oy <= 0.0 {
505 continue;
506 }
507 overlaps += 1;
508 hit[i] = true;
509 let sx = if dx == 0.0 {
510 tie(&mut rng)
511 } else {
512 dx.signum()
513 };
514 let sy = if dy == 0.0 {
515 tie(&mut rng)
516 } else {
517 dy.signum()
518 };
519 let along_x = match p.direction {
520 RepelDirection::X => true,
521 RepelDirection::Y => false,
522 RepelDirection::Both => ox <= oy,
523 };
524 if along_x {
525 delta[i].0 += sx * ox * point_force;
526 } else {
527 delta[i].1 += sy * oy * point_force;
528 }
529 }
530 }
531
532 for i in 0..n {
535 if point_force == 0.0 {
536 break;
537 }
538 let b = boxes[i];
539 for &(a, e) in &lines {
540 if !segment_hits_box(a, e, &b, box_pad) {
541 continue;
542 }
543 overlaps += 1;
544 hit[i] = true;
545 let (lx, ly) = (e.0 - a.0, e.1 - a.1);
546 let len = lx.hypot(ly);
547 let (nx, ny) = (-ly / len, lx / len);
548 let d = (b.cx - a.0) * nx + (b.cy - a.1) * ny;
549 let side = if d == 0.0 { tie(&mut rng) } else { d.signum() };
550 let need = b.hw * nx.abs() + b.hh * ny.abs() + box_pad - d.abs();
551 if need > 0.0 {
552 delta[i].0 += side * nx * need * point_force;
553 delta[i].1 += side * ny * need * point_force;
554 }
555 }
556 }
557
558 let mut moved = 0.0f64;
560 for i in 0..n {
561 let b = &mut boxes[i];
562 let k = if iter < pull_until && !hit[i] {
565 0.1 * pull
566 } else {
567 0.0
568 };
569 let sx = (targets[i].0 - b.cx) * k;
570 let sy = (targets[i].1 - b.cy) * k;
571 let cap = |v: f64| {
572 if v.is_finite() {
573 v.clamp(-max_step, max_step)
574 } else {
575 0.0
576 }
577 };
578 let (mx, my) = (cap(delta[i].0 + sx) * move_x, cap(delta[i].1 + sy) * move_y);
579 let (px, py) = (b.cx, b.cy);
580 b.cx += mx;
581 b.cy += my;
582 clamp_box(b);
583 moved = moved.max((b.cx - px).abs() + (b.cy - py).abs());
584 }
585 if (overlaps == 0 && moved < 0.05) || (label_overlaps == 0 && iter >= pull_until) {
586 break;
587 }
588 }
589 }
590
591 let mut kept = vec![false; n];
594 let mut dropped = 0usize;
595 let mut kept_boxes: Vec<LabelBox> = Vec::new();
596 let limit = if capped { 0 } else { p.max_overlaps };
597 for i in 0..n {
598 let b = boxes[i];
599 let mut count = kept_boxes.iter().filter(|o| b.overlaps(o, 0.0)).count();
600 if count <= limit && !capped {
602 let r = pt_r;
603 count += pts[points_near(&pts, b.cx - b.hw - r, b.cx + b.hw + r)]
604 .iter()
605 .filter(|q| {
606 !(q.0 == anchors[i].0 && q.1 == anchors[i].1)
607 && (q.0 - b.cx).abs() < b.hw + r
608 && (q.1 - b.cy).abs() < b.hh + r
609 })
610 .count();
611 }
612 if count > limit {
613 dropped += 1;
614 } else {
615 kept[i] = true;
616 kept_boxes.push(b);
617 }
618 }
619
620 RepelLayout {
621 boxes,
622 kept,
623 dropped,
624 capped,
625 }
626}
627
628fn tie(rng: &mut crate::rng::SplitMix64) -> f64 {
630 if rng.next_u64() & 1 == 0 {
631 -1.0
632 } else {
633 1.0
634 }
635}
636
637type Seg = ((f64, f64), (f64, f64));
639
640struct Item {
642 row: usize,
643 anchor: (f64, f64),
644 target: (f64, f64),
645 text: String,
646}
647
648#[allow(clippy::too_many_arguments)]
650fn draw_repel(
651 name: &str,
652 data: &DataFrame,
653 coord: &dyn Coord,
654 scales: &ScaleSet,
655 theme: &Theme,
656 backend: &mut dyn DrawBackend,
657 size: f64,
658 color: (u8, u8, u8),
659 alpha: f64,
660 face: FontFace,
661 label: Option<((u8, u8, u8), f64)>,
662 p: &RepelParams,
663) -> Result<(), RenderError> {
664 let x_col = data
665 .column("x")
666 .ok_or(RenderError::MissingAesthetic("x".into()))?;
667 let y_col = data
668 .column("y")
669 .ok_or(RenderError::MissingAesthetic("y".into()))?;
670 let label_col = data
671 .column("label")
672 .ok_or(RenderError::MissingAesthetic("label".into()))?;
673 let color_col = data.column("color");
674 let fill_col = data.column("fill");
675
676 let panel = backend.plot_area();
677 let x_scale = scales.get(&Aesthetic::X);
678 let y_scale = scales.get(&Aesthetic::Y);
679 let size = if size.is_finite() && size > 0.0 {
680 size
681 } else {
682 10.0
683 };
684
685 let to_px = |xv: &Value, yv: &Value| -> (f64, f64) {
686 let nx = x_scale.map(|s| s.map(xv)).unwrap_or(0.5);
687 let ny = y_scale.map(|s| s.map(yv)).unwrap_or(0.5);
688 coord.transform((nx, ny), &panel)
689 };
690 let nudged = |v: &Value, d: f64, discrete: bool| -> Value {
691 match v.as_f64() {
692 Some(f) if d != 0.0 && d.is_finite() && !discrete => Value::Float(f + d),
693 _ => v.clone(),
694 }
695 };
696 let x_discrete = x_scale.map(|s| s.is_discrete()).unwrap_or(false);
697 let y_discrete = y_scale.map(|s| s.is_discrete()).unwrap_or(false);
698
699 let mut points = Vec::with_capacity(data.nrows());
700 let mut items: Vec<Item> = Vec::new();
701 for i in 0..data.nrows() {
702 if x_col[i].is_na() || y_col[i].is_na() {
703 continue;
704 }
705 let anchor = to_px(&x_col[i], &y_col[i]);
706 if !(anchor.0.is_finite() && anchor.1.is_finite()) {
707 continue;
708 }
709 points.push(anchor);
710 let text = if label_col[i].is_na() {
711 String::new()
712 } else {
713 label_col[i].to_group_key()
714 };
715 if text.trim().is_empty() {
716 continue;
717 }
718 let target = to_px(
719 &nudged(&x_col[i], p.nudge_x, x_discrete),
720 &nudged(&y_col[i], p.nudge_y, y_discrete),
721 );
722 let target = if target.0.is_finite() && target.1.is_finite() {
723 target
724 } else {
725 anchor
726 };
727 items.push(Item {
728 row: i,
729 anchor,
730 target,
731 text,
732 });
733 }
734 if items.is_empty() {
735 return Ok(());
736 }
737
738 let pad = label.map(|(_, pd)| pd.max(0.0)).unwrap_or(0.0);
739 let half: Vec<(f64, f64)> = items
740 .iter()
741 .map(|it| {
742 (
743 it.text.chars().count() as f64 * size * 0.3 + pad,
744 size * 0.5 + pad,
745 )
746 })
747 .collect();
748 let anchors: Vec<(f64, f64)> = items.iter().map(|it| it.anchor).collect();
749 let targets: Vec<(f64, f64)> = items.iter().map(|it| it.target).collect();
750 let mut lines = Vec::new();
753 if !p.avoid_ablines.is_empty() && !x_discrete && !y_discrete {
754 let xs: Vec<f64> = x_col
755 .iter()
756 .filter_map(|v| v.as_f64())
757 .filter(|v| v.is_finite())
758 .collect();
759 if let (Some(lo), Some(hi)) = (
760 xs.iter().copied().reduce(f64::min),
761 xs.iter().copied().reduce(f64::max),
762 ) {
763 let w = (hi - lo).max(1.0);
764 let (lo, hi) = (lo - 4.0 * w, hi + 4.0 * w);
765 for &(slope, intercept) in &p.avoid_ablines {
766 if !(slope.is_finite() && intercept.is_finite()) {
767 continue;
768 }
769 let at = |x: f64| to_px(&Value::Float(x), &Value::Float(intercept + slope * x));
770 lines.push((at(lo), at(hi)));
771 }
772 }
773 }
774 let lay = layout_with_lines(&anchors, &targets, &half, &points, &lines, &panel, p);
775
776 if lay.capped {
777 backend.warn(format!(
778 "geom_{name}: {} labels exceed the repel limit of {}; placed without \
779 repulsion (overlapping labels dropped)",
780 items.len(),
781 p.max_labels
782 ));
783 }
784 if lay.dropped > 0 {
785 backend.warn(format!(
786 "geom_{name}: {} unlabeled data point{} (too many overlaps). Consider \
787 increasing max_overlaps",
788 lay.dropped,
789 if lay.dropped == 1 { "" } else { "s" }
790 ));
791 }
792
793 let seg_min = if p.min_segment_length.is_finite() {
794 p.min_segment_length.max(0.0)
795 } else {
796 f64::INFINITY
797 };
798 let alpha = if alpha.is_finite() {
799 alpha.clamp(0.0, 1.0)
800 } else {
801 1.0
802 };
803
804 super::clear_mark(backend);
806 for (k, it) in items.iter().enumerate() {
807 if !lay.kept[k] {
808 continue;
809 }
810 let b = lay.boxes[k];
811 let d = b.dist_to(it.anchor.0, it.anchor.1);
812 if d <= seg_min || d <= 0.0 {
813 continue;
814 }
815 let (ex, ey) = b.closest(it.anchor.0, it.anchor.1);
816 let gap = if p.point_padding.is_finite() {
818 p.point_padding.max(0.0)
819 } else {
820 0.0
821 };
822 let (dx, dy) = (ex - it.anchor.0, ey - it.anchor.1);
823 let len = dx.hypot(dy);
824 if len <= gap {
825 continue;
826 }
827 let start = (it.anchor.0 + dx / len * gap, it.anchor.1 + dy / len * gap);
828 let c = p
829 .segment_color
830 .unwrap_or_else(|| row_color(scales, color_col, it.row, color));
831 backend.draw_line(
832 &[start, (ex, ey)],
833 &LineStyle {
834 color: c,
835 width: if p.segment_width.is_finite() {
836 p.segment_width.max(0.0)
837 } else {
838 0.6
839 },
840 alpha,
841 linetype: Linetype::Solid,
842 ..Default::default()
843 },
844 )?;
845 }
846
847 for (k, it) in items.iter().enumerate() {
848 if !lay.kept[k] {
849 continue;
850 }
851 let b = lay.boxes[k];
852 let c = row_color(scales, color_col, it.row, color);
853 if let Some((fill, _)) = label {
854 let f = fill_col
855 .and_then(|fc| scales.map_color(&Aesthetic::Fill, &fc[it.row]))
856 .unwrap_or(fill);
857 super::set_mark(
858 backend,
859 Some(it.text.clone()),
860 Some(super::tip_value(&x_col[it.row])),
861 super::series_key(data, it.row),
862 super::measured_value(data, it.row),
863 );
864 super::set_mark_formatted(
865 backend,
866 scales,
867 Some(&x_col[it.row]),
868 &Aesthetic::Y,
869 super::measured_raw(data, it.row),
870 );
871 backend.draw_rect(
872 (b.cx - b.hw, b.cy - b.hh),
873 (b.cx + b.hw, b.cy + b.hh),
874 &RectStyle {
875 fill: Some(f),
876 stroke: Some(c),
877 stroke_width: 0.5,
878 alpha,
879 clip: true,
880 corner_radius: 0.0,
881 },
882 )?;
883 super::clear_mark(backend);
884 }
885 backend.draw_text(
886 &it.text,
887 (b.cx, b.cy),
888 &TextStyle {
889 color: c,
890 size,
891 anchor: TextAnchor::Middle,
892 angle: 0.0,
893 family: super::resolve_font_family("", theme),
894 face,
895 bg_colour: None,
896 bg_r: 0.0,
897 },
898 )?;
899 }
900 Ok(())
901}
902
903fn row_color(
904 scales: &ScaleSet,
905 color_col: Option<&[Value]>,
906 row: usize,
907 fallback: (u8, u8, u8),
908) -> (u8, u8, u8) {
909 color_col
910 .and_then(|cc| scales.map_color(&Aesthetic::Color, &cc[row]))
911 .unwrap_or(fallback)
912}
913
914impl Geom for GeomTextRepel {
915 fn draw(
916 &self,
917 data: &DataFrame,
918 coord: &dyn Coord,
919 scales: &ScaleSet,
920 theme: &Theme,
921 backend: &mut dyn DrawBackend,
922 ) -> Result<(), RenderError> {
923 draw_repel(
924 "text_repel",
925 data,
926 coord,
927 scales,
928 theme,
929 backend,
930 self.size,
931 self.color,
932 self.alpha,
933 self.fontface,
934 None,
935 &self.repel,
936 )
937 }
938
939 fn required_aes(&self) -> Vec<Aesthetic> {
940 vec![Aesthetic::X, Aesthetic::Y, Aesthetic::Label]
941 }
942 fn default_stat(&self) -> Box<dyn Stat> {
943 Box::new(StatIdentity)
944 }
945 fn default_position(&self) -> Box<dyn Position> {
946 Box::new(PositionIdentity)
947 }
948 fn default_params(&self) -> GeomParams {
949 GeomParams::default()
950 }
951 fn name(&self) -> &str {
952 "text_repel"
953 }
954
955 fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
956 if let (true, Some(ink)) = (self.color == (0, 0, 0), el.ink) {
957 self.color = ink;
958 }
959 }
960}
961
962impl Geom for GeomLabelRepel {
963 fn draw(
964 &self,
965 data: &DataFrame,
966 coord: &dyn Coord,
967 scales: &ScaleSet,
968 theme: &Theme,
969 backend: &mut dyn DrawBackend,
970 ) -> Result<(), RenderError> {
971 draw_repel(
972 "label_repel",
973 data,
974 coord,
975 scales,
976 theme,
977 backend,
978 self.size,
979 self.color,
980 self.alpha,
981 self.fontface,
982 Some((self.fill, self.label_padding)),
983 &self.repel,
984 )
985 }
986
987 fn required_aes(&self) -> Vec<Aesthetic> {
988 vec![Aesthetic::X, Aesthetic::Y, Aesthetic::Label]
989 }
990 fn default_stat(&self) -> Box<dyn Stat> {
991 Box::new(StatIdentity)
992 }
993 fn default_position(&self) -> Box<dyn Position> {
994 Box::new(PositionIdentity)
995 }
996 fn default_params(&self) -> GeomParams {
997 GeomParams::default()
998 }
999 fn name(&self) -> &str {
1000 "label_repel"
1001 }
1002
1003 fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
1004 if let (true, Some(ink)) = (self.color == (0, 0, 0), el.ink) {
1005 self.color = ink;
1006 }
1007 if let (true, Some(paper)) = (self.fill == (255, 255, 255), el.paper) {
1008 self.fill = paper;
1009 }
1010 }
1011}
1012
1013#[cfg(test)]
1014mod tests {
1015 use super::*;
1016
1017 fn panel() -> Rect {
1018 Rect {
1019 x: 0.0,
1020 y: 0.0,
1021 width: 400.0,
1022 height: 300.0,
1023 }
1024 }
1025
1026 fn overlapping(lay: &RepelLayout) -> usize {
1027 let mut c = 0;
1028 for i in 0..lay.boxes.len() {
1029 for j in i + 1..lay.boxes.len() {
1030 if lay.kept[i] && lay.kept[j] && lay.boxes[i].overlaps(&lay.boxes[j], 0.0) {
1031 c += 1;
1032 }
1033 }
1034 }
1035 c
1036 }
1037
1038 #[test]
1039 fn coincident_labels_are_separated_deterministically() {
1040 let anchors = vec![(200.0, 150.0); 6];
1041 let half = vec![(20.0, 6.0); 6];
1042 let p = RepelParams::default();
1043 let a = layout(&anchors, &anchors, &half, &anchors, &panel(), &p);
1044 let b = layout(&anchors, &anchors, &half, &anchors, &panel(), &p);
1045 assert_eq!(overlapping(&a), 0);
1046 assert_eq!(a.dropped, 0);
1047 for (x, y) in a.boxes.iter().zip(&b.boxes) {
1048 assert_eq!((x.cx, x.cy), (y.cx, y.cy), "same seed, same layout");
1049 }
1050 let c = layout(
1051 &anchors,
1052 &anchors,
1053 &half,
1054 &anchors,
1055 &panel(),
1056 &RepelParams {
1057 seed: 7,
1058 ..RepelParams::default()
1059 },
1060 );
1061 assert_eq!(overlapping(&c), 0);
1062 }
1063
1064 #[test]
1065 fn labels_avoid_points_and_stay_inside() {
1066 let anchors: Vec<(f64, f64)> = (0..30)
1068 .map(|i| (5.0 + (i % 6) as f64 * 3.0, 5.0 + (i / 6) as f64 * 3.0))
1069 .collect();
1070 let half = vec![(15.0, 5.0); anchors.len()];
1071 let lay = layout(
1072 &anchors,
1073 &anchors,
1074 &half,
1075 &anchors,
1076 &panel(),
1077 &RepelParams {
1078 max_overlaps: usize::MAX,
1079 ..RepelParams::default()
1080 },
1081 );
1082 let pa = panel();
1083 for b in &lay.boxes {
1084 assert!(b.cx - b.hw >= pa.x - 1e-9 && b.cx + b.hw <= pa.x + pa.width + 1e-9);
1085 assert!(b.cy - b.hh >= pa.y - 1e-9 && b.cy + b.hh <= pa.y + pa.height + 1e-9);
1086 }
1087 assert_eq!(overlapping(&lay), 0, "all 30 labels separated");
1088 }
1089
1090 #[test]
1091 fn max_overlaps_zero_drops_what_cannot_be_placed() {
1092 let tiny = Rect {
1094 x: 0.0,
1095 y: 0.0,
1096 width: 60.0,
1097 height: 30.0,
1098 };
1099 let anchors = vec![(30.0, 15.0); 40];
1100 let half = vec![(20.0, 6.0); 40];
1101 let lay = layout(
1102 &anchors,
1103 &anchors,
1104 &half,
1105 &[],
1106 &tiny,
1107 &RepelParams {
1108 max_overlaps: 0,
1109 ..RepelParams::default()
1110 },
1111 );
1112 assert!(lay.dropped > 0);
1113 assert_eq!(overlapping(&lay), 0);
1114 assert_eq!(lay.kept.iter().filter(|k| **k).count() + lay.dropped, 40);
1115 }
1116
1117 #[test]
1118 fn direction_y_only_moves_vertically() {
1119 let anchors = vec![(100.0, 150.0), (101.0, 150.0), (102.0, 150.0)];
1120 let half = vec![(20.0, 6.0); 3];
1121 let lay = layout(
1122 &anchors,
1123 &anchors,
1124 &half,
1125 &[],
1126 &panel(),
1127 &RepelParams {
1128 direction: RepelDirection::Y,
1129 ..RepelParams::default()
1130 },
1131 );
1132 for (b, a) in lay.boxes.iter().zip(&anchors) {
1133 assert!((b.cx - a.0).abs() <= 0.5, "x stayed put");
1134 }
1135 assert_eq!(overlapping(&lay), 0);
1136 }
1137
1138 #[test]
1139 fn cap_skips_simulation() {
1140 let anchors: Vec<(f64, f64)> = (0..20).map(|i| (i as f64, 10.0)).collect();
1141 let half = vec![(5.0, 3.0); 20];
1142 let lay = layout(
1143 &anchors,
1144 &anchors,
1145 &half,
1146 &anchors,
1147 &panel(),
1148 &RepelParams {
1149 max_labels: 5,
1150 ..RepelParams::default()
1151 },
1152 );
1153 assert!(lay.capped);
1154 assert_eq!(overlapping(&lay), 0);
1155 }
1156
1157 #[test]
1158 fn non_finite_params_do_not_hang_or_panic() {
1159 let anchors = vec![(10.0, 10.0), (10.0, 10.0)];
1160 let half = vec![(5.0, 3.0); 2];
1161 let lay = layout(
1162 &anchors,
1163 &anchors,
1164 &half,
1165 &[(f64::NAN, 1.0)],
1166 &panel(),
1167 &RepelParams {
1168 force: f64::NAN,
1169 force_pull: f64::INFINITY,
1170 box_padding: f64::NAN,
1171 ..RepelParams::default()
1172 },
1173 );
1174 for b in &lay.boxes {
1175 assert!(b.cx.is_finite() && b.cy.is_finite());
1176 }
1177 }
1178
1179 #[test]
1180 fn labels_move_off_a_line() {
1181 let anchors = vec![(100.0, 100.0), (200.0, 200.0), (300.0, 300.0)];
1183 let half = vec![(20.0, 6.0); 3];
1184 let line = ((0.0, 0.0), (400.0, 400.0));
1185 let lay = layout_with_lines(
1186 &anchors,
1187 &anchors,
1188 &half,
1189 &anchors,
1190 &[line],
1191 &panel(),
1192 &RepelParams::default(),
1193 );
1194 for (k, b) in lay.boxes.iter().enumerate() {
1195 assert!(lay.kept[k]);
1196 assert!(
1197 !segment_hits_box(line.0, line.1, b, 0.0),
1198 "label {k} still on the line: {b:?}"
1199 );
1200 }
1201 }
1202
1203 #[test]
1204 fn segment_box_test() {
1205 let b = LabelBox {
1206 cx: 50.0,
1207 cy: 50.0,
1208 hw: 10.0,
1209 hh: 5.0,
1210 };
1211 assert!(segment_hits_box((0.0, 0.0), (100.0, 100.0), &b, 0.0));
1212 assert!(segment_hits_box((0.0, 50.0), (100.0, 50.0), &b, 0.0));
1213 assert!(!segment_hits_box((0.0, 70.0), (100.0, 70.0), &b, 0.0));
1214 assert!(segment_hits_box((0.0, 57.0), (100.0, 57.0), &b, 3.0));
1215 assert!(!segment_hits_box((0.0, 0.0), (30.0, 30.0), &b, 0.0));
1216 }
1217
1218 #[test]
1219 fn avoid_abline_in_a_plot() {
1220 let render = |avoid: bool| {
1221 let xs: Vec<Value> = (0..8).map(|i| Value::Float(i as f64)).collect();
1222 let labels: Vec<Value> = (0..8).map(|i| Value::Str(format!("point {i}"))).collect();
1223 crate::plot::GGPlot::new(vec![
1224 ("x".into(), xs.clone()),
1225 ("y".into(), xs),
1226 ("l".into(), labels),
1227 ])
1228 .aes(crate::aes::Aes::new().x("x").y("y").label("l"))
1229 .geom_point()
1230 .geom_text_repel_with(if avoid {
1231 GeomTextRepel::default().avoid_abline(1.0, 0.0)
1232 } else {
1233 GeomTextRepel::default()
1234 })
1235 .render_svg_native_with_size(400, 300)
1236 .unwrap()
1237 };
1238 let (with, without) = (render(true), render(false));
1239 assert_eq!(
1240 with.matches(">point ").count(),
1241 without.matches(">point ").count(),
1242 "no label dropped"
1243 );
1244 assert_ne!(with, without, "the line moved the labels");
1245 }
1246}