Skip to main content

ggplot_rs/geom/
repel.rs

1//! Repelling text labels — `geom_text_repel` / `geom_label_repel` (R's
2//! ggrepel).
3//!
4//! Labels start at their data point (plus an optional nudge) and are pushed
5//! apart by a deterministic, seeded relaxation until they no longer overlap
6//! each other or the labelled points; a spring pulls each label back towards
7//! its point and every label stays inside the panel. Labels that still overlap
8//! more than `max_overlaps` other labels/points are dropped (reported as a
9//! render warning, like ggrepel's "unlabeled data points"). A segment connects
10//! a label to its point when it ended up farther away than
11//! `min_segment_length`.
12//!
13//! The layout runs in pixel space at draw time (text extents depend on the
14//! rendered size), costs O(n log n + overlapping pairs) per iteration thanks to
15//! a sweep over sorted box edges, and is bounded by `max_iter` and `max_time`.
16//! Above `max_labels` labels the simulation is skipped (greedy non-overlapping
17//! placement instead) with a warning.
18
19use 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/// Axis along which labels may move.
36#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
37pub enum RepelDirection {
38    /// Move freely (default).
39    #[default]
40    Both,
41    /// Only horizontally.
42    X,
43    /// Only vertically.
44    Y,
45}
46
47/// Shared layout parameters of [`GeomTextRepel`] / [`GeomLabelRepel`].
48#[derive(Clone, Debug)]
49pub struct RepelParams {
50    /// Padding (px) around each label's box when testing for overlaps
51    /// (ggrepel `box.padding`).
52    pub box_padding: f64,
53    /// Padding (px) around each data point (ggrepel `point.padding`); also
54    /// the gap left between a segment and its point.
55    pub point_padding: f64,
56    /// Radius (px) of the data points labels must avoid (match the point
57    /// layer's size; `GeomPoint`'s default is 3).
58    pub point_size: f64,
59    /// Horizontal nudge of the starting position, in data units (numeric x).
60    pub nudge_x: f64,
61    /// Vertical nudge of the starting position, in data units (numeric y).
62    pub nudge_y: f64,
63    /// Strength of the repulsion between overlapping boxes.
64    pub force: f64,
65    /// Strength of the spring pulling a label back to its point.
66    pub force_pull: f64,
67    /// Maximum relaxation iterations.
68    pub max_iter: usize,
69    /// Wall-clock bound for the relaxation (a safety net — the layout is
70    /// deterministic as long as it converges or hits `max_iter` first).
71    /// Ignored on `wasm32`, which has no monotonic clock.
72    pub max_time: std::time::Duration,
73    /// Drop labels that still overlap more than this many other labels or
74    /// points after the layout (ggrepel `max.overlaps`, default 10).
75    pub max_overlaps: usize,
76    /// Draw a segment when the label box is farther than this (px) from its
77    /// point (ggrepel `min.segment.length`).
78    pub min_segment_length: f64,
79    /// Seed of the tie-breaking jitter; equal seeds give equal layouts.
80    pub seed: u64,
81    /// Movement axis.
82    pub direction: RepelDirection,
83    /// Above this many labels the force layout is skipped (it is O(n²) in
84    /// the worst case) and labels are placed greedily, dropping overlaps.
85    pub max_labels: usize,
86    /// Segment colour; `None` = the label's text colour.
87    pub segment_color: Option<(u8, u8, u8)>,
88    /// Segment width (px).
89    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            /// Padding (px) around label boxes when testing overlaps.
119            pub fn box_padding(mut self, px: f64) -> Self {
120                self.repel.box_padding = px;
121                self
122            }
123            /// Padding (px) around the labelled points.
124            pub fn point_padding(mut self, px: f64) -> Self {
125                self.repel.point_padding = px;
126                self
127            }
128            /// Radius (px) of the points to avoid.
129            pub fn point_size(mut self, px: f64) -> Self {
130                self.repel.point_size = px;
131                self
132            }
133            /// Nudge the starting position (data units).
134            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            /// Repulsion strength.
140            pub fn force(mut self, force: f64) -> Self {
141                self.repel.force = force;
142                self
143            }
144            /// Spring strength back to the point.
145            pub fn force_pull(mut self, force: f64) -> Self {
146                self.repel.force_pull = force;
147                self
148            }
149            /// Maximum iterations.
150            pub fn max_iter(mut self, n: usize) -> Self {
151                self.repel.max_iter = n;
152                self
153            }
154            /// Wall-clock bound of the layout.
155            pub fn max_time(mut self, t: std::time::Duration) -> Self {
156                self.repel.max_time = t;
157                self
158            }
159            /// Drop labels overlapping more than `n` labels/points.
160            pub fn max_overlaps(mut self, n: usize) -> Self {
161                self.repel.max_overlaps = n;
162                self
163            }
164            /// Minimum label–point distance (px) that gets a segment.
165            pub fn min_segment_length(mut self, px: f64) -> Self {
166                self.repel.min_segment_length = px;
167                self
168            }
169            /// Seed of the deterministic tie-breaking jitter.
170            pub fn seed(mut self, seed: u64) -> Self {
171                self.repel.seed = seed;
172                self
173            }
174            /// Restrict movement to one axis.
175            pub fn direction(mut self, d: RepelDirection) -> Self {
176                self.repel.direction = d;
177                self
178            }
179            /// Label-count cap of the force layout.
180            pub fn max_labels(mut self, n: usize) -> Self {
181                self.repel.max_labels = n;
182                self
183            }
184            /// Segment colour.
185            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
193/// Text labels repelled away from each other and from their points
194/// (`ggrepel::geom_text_repel`). Requires `x`, `y`, `label`; an empty / `NA`
195/// label is not drawn but its point is still avoided (as in ggrepel).
196pub 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
218/// Boxed labels repelled away from each other and from their points
219/// (`ggrepel::geom_label_repel`).
220pub struct GeomLabelRepel {
221    pub size: f64,
222    pub color: (u8, u8, u8),
223    pub fill: (u8, u8, u8),
224    pub alpha: f64,
225    /// Inner padding (px) between text and box border.
226    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/// Axis-aligned label box: centre and half extents (px).
248#[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    /// Distance from `(x, y)` to the box (0 inside).
263    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    /// Closest point of the box border/interior to `(x, y)`.
270    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
278/// Result of a layout: final boxes, which labels were kept, and how many
279/// were dropped for exceeding `max_overlaps`.
280pub(crate) struct RepelLayout {
281    boxes: Vec<LabelBox>,
282    kept: Vec<bool>,
283    dropped: usize,
284    /// The force layout was skipped because there were too many labels.
285    capped: bool,
286}
287
288/// Lay out `labels` (anchor point, half extents incl. any label padding) so
289/// they avoid each other and `points`, inside `panel`. Pure and
290/// deterministic for a given `params.seed` (unless `max_time` cuts it short).
291pub(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        // A box wider than the panel is centred on it.
309        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    // Points sorted by x for range queries.
343    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        // The spring is active for the first three quarters; the rest only
362        // separates, so the layout ends in a non-overlapping state whenever
363        // one is reachable.
364        let pull_until = p.max_iter - p.max_iter / 4;
365        // Largest step per iteration, against overshooting when many
366        // overlaps add up.
367        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            // Box–box: sweep over boxes sorted by their left edge.
380            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                    // Separate along the axis of least overlap (respecting the
410                    // allowed direction).
411                    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            // Box–point: push labels off every data point they cover. The
430            // final phase only separates labels from each other (a point
431            // that cannot be avoided must not keep two labels overlapping).
432            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            // Spring back towards the target, then move and clamp.
472            let mut moved = 0.0f64;
473            for i in 0..n {
474                let b = &mut boxes[i];
475                // Pull back only labels that are currently free, so the spring
476                // never holds two labels in a residual overlap.
477                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    // Drop labels that still overlap too much: each label counts overlaps
505    // with already-kept labels and with points other than its own.
506    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        // Without a layout, covered points are not held against a label.
514        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
541/// Deterministic ±1 for coincident centres.
542fn 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
550/// One label to place.
551struct Item {
552    row: usize,
553    anchor: (f64, f64),
554    target: (f64, f64),
555    text: String,
556}
557
558/// Shared draw routine; `label` = `Some((fill, padding))` draws boxes.
559#[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    // Segments first so labels paint over them.
691    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        // Leave a gap of `point_padding` at the point end.
703        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        // A dense cloud near the corner.
951        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        // 40 labels in a panel that only fits a handful.
977        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}