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    /// Straight lines labels keep clear of, as `(slope, intercept)` in the
91    /// same (scale) space as [`crate::geom::refline::GeomAbline`] — pass the
92    /// plot's ablines so a label isn't drawn across one.
93    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            /// Padding (px) around label boxes when testing overlaps.
124            pub fn box_padding(mut self, px: f64) -> Self {
125                self.repel.box_padding = px;
126                self
127            }
128            /// Padding (px) around the labelled points.
129            pub fn point_padding(mut self, px: f64) -> Self {
130                self.repel.point_padding = px;
131                self
132            }
133            /// Radius (px) of the points to avoid.
134            pub fn point_size(mut self, px: f64) -> Self {
135                self.repel.point_size = px;
136                self
137            }
138            /// Nudge the starting position (data units).
139            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            /// Repulsion strength.
145            pub fn force(mut self, force: f64) -> Self {
146                self.repel.force = force;
147                self
148            }
149            /// Spring strength back to the point.
150            pub fn force_pull(mut self, force: f64) -> Self {
151                self.repel.force_pull = force;
152                self
153            }
154            /// Maximum iterations.
155            pub fn max_iter(mut self, n: usize) -> Self {
156                self.repel.max_iter = n;
157                self
158            }
159            /// Wall-clock bound of the layout.
160            pub fn max_time(mut self, t: std::time::Duration) -> Self {
161                self.repel.max_time = t;
162                self
163            }
164            /// Drop labels overlapping more than `n` labels/points.
165            pub fn max_overlaps(mut self, n: usize) -> Self {
166                self.repel.max_overlaps = n;
167                self
168            }
169            /// Minimum label–point distance (px) that gets a segment.
170            pub fn min_segment_length(mut self, px: f64) -> Self {
171                self.repel.min_segment_length = px;
172                self
173            }
174            /// Seed of the deterministic tie-breaking jitter.
175            pub fn seed(mut self, seed: u64) -> Self {
176                self.repel.seed = seed;
177                self
178            }
179            /// Restrict movement to one axis.
180            pub fn direction(mut self, d: RepelDirection) -> Self {
181                self.repel.direction = d;
182                self
183            }
184            /// Label-count cap of the force layout.
185            pub fn max_labels(mut self, n: usize) -> Self {
186                self.repel.max_labels = n;
187                self
188            }
189            /// Keep labels clear of the line `y = intercept + slope * x`
190            /// (scale space, like `geom_abline`).
191            pub fn avoid_abline(mut self, slope: f64, intercept: f64) -> Self {
192                self.repel.avoid_ablines.push((slope, intercept));
193                self
194            }
195            /// Segment colour.
196            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
204/// Text labels repelled away from each other and from their points
205/// (`ggrepel::geom_text_repel`). Requires `x`, `y`, `label`; an empty / `NA`
206/// label is not drawn but its point is still avoided (as in ggrepel).
207pub 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
229/// Boxed labels repelled away from each other and from their points
230/// (`ggrepel::geom_label_repel`).
231pub struct GeomLabelRepel {
232    pub size: f64,
233    pub color: (u8, u8, u8),
234    pub fill: (u8, u8, u8),
235    pub alpha: f64,
236    /// Inner padding (px) between text and box border.
237    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/// Axis-aligned label box: centre and half extents (px).
259#[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    /// Distance from `(x, y)` to the box (0 inside).
274    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    /// Closest point of the box border/interior to `(x, y)`.
281    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
289/// Result of a layout: final boxes, which labels were kept, and how many
290/// were dropped for exceeding `max_overlaps`.
291pub(crate) struct RepelLayout {
292    boxes: Vec<LabelBox>,
293    kept: Vec<bool>,
294    dropped: usize,
295    /// The force layout was skipped because there were too many labels.
296    capped: bool,
297}
298
299/// Lay out `labels` (anchor point, half extents incl. any label padding) so
300/// they avoid each other and `points`, inside `panel`. Pure and
301/// deterministic for a given `params.seed` (unless `max_time` cuts it short).
302#[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
314/// Does the segment `a`–`b` pass through `bx` grown by `pad` (Liang–Barsky)?
315fn 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
341/// [`layout`], also keeping boxes clear of `lines` (pixel segments): a box a
342/// segment crosses is pushed along the segment's normal, to the side its
343/// centre is on.
344pub(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        // A box wider than the panel is centred on it.
370        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    // #475: whether a box already touches the panel edge it would be pushed
383    // towards (`sign` < 0: left / up), so the push would be clamped away.
384    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    // Points sorted by x for range queries.
415    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        // The spring is active for the first three quarters; the rest only
434        // separates, so the layout ends in a non-overlapping state whenever
435        // one is reachable.
436        let pull_until = p.max_iter - p.max_iter / 4;
437        // Largest step per iteration, against overshooting when many
438        // overlaps add up.
439        let max_step = 12.0;
440        // #475: the final phase keeps pushing labels that are clear of every
441        // other label off the points / lines they cover, and remembers the
442        // label-overlap-free layout that covers the fewest of them (that
443        // push can make two labels touch again before the phase ends).
444        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            // Box–box: sweep over boxes sorted by their left edge.
457            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                    // Separate along the axis of least overlap (respecting the
487                    // allowed direction).
488                    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                    // #475: a box pinned to the panel edge it would move
495                    // towards leaves the whole move to the other one; both
496                    // pinned on that axis, separate along the other.
497                    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            // Labels in an overlap with another label (the final phase moves
520            // those only apart from each other).
521            let label_hit = if final_phase { hit.clone() } else { Vec::new() };
522            let free = |i: usize| !final_phase || !label_hit[i];
523
524            // Box–point: push labels off every data point they cover. In the
525            // final phase only labels clear of every other label are pushed
526            // (a point that cannot be avoided must not keep two labels
527            // overlapping), at half strength.
528            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                    // #475: off a point towards a panel edge the box already
564                    // touches, the clamp would cancel the push and pin the
565                    // label on the point: go along the other axis, else
566                    // across to the point's other side.
567                    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            // Box–line: push a label a line crosses off it, along the line's
590            // normal (the same phases as the points).
591            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            // Spring back towards the target, then move and clamp.
629            let mut moved = 0.0f64;
630            for i in 0..n {
631                let b = &mut boxes[i];
632                // Pull back only labels that are currently free, so the spring
633                // never holds two labels in a residual overlap.
634                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        // The best label-overlap-free state of the final phase (its
662        // overlaps were counted, unlike those of the state after the last
663        // move).
664        if let Some((_, b)) = best {
665            boxes = b;
666        }
667    }
668
669    // Drop labels that still overlap too much: each label counts overlaps
670    // with already-kept labels and with points other than its own.
671    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        // Without a layout, covered points are not held against a label.
679        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
706/// Deterministic ±1 for coincident centres.
707fn 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
715/// A pixel line segment (start, end).
716type Seg = ((f64, f64), (f64, f64));
717
718/// One label to place.
719struct Item {
720    row: usize,
721    anchor: (f64, f64),
722    target: (f64, f64),
723    text: String,
724}
725
726/// Shared draw routine; `label` = `Some((fill, padding))` draws boxes.
727#[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    // `avoid_ablines` in pixels: the line between two far-apart x values
829    // (the data's own x range, widened) — the box test clips it.
830    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    // Segments first so labels paint over them.
883    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        // Leave a gap of `point_padding` at the point end.
895        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        // #475: a risk map's points crowd the top edge (y = 100 %); the
1145        // spring phase leaves labels on neighbouring points, and the final
1146        // separation-only phase used to stop as soon as labels were apart.
1147        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        // A label box touching another point's dot (radius point_size).
1177        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        // A dense cloud near the corner.
1192        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        // 40 labels in a panel that only fits a handful.
1218        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        // three labels sitting on the diagonal y = x
1307        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}