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    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    // Points sorted by x for range queries.
404    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        // The spring is active for the first three quarters; the rest only
423        // separates, so the layout ends in a non-overlapping state whenever
424        // one is reachable.
425        let pull_until = p.max_iter - p.max_iter / 4;
426        // Largest step per iteration, against overshooting when many
427        // overlaps add up.
428        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            // Box–box: sweep over boxes sorted by their left edge.
441            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                    // Separate along the axis of least overlap (respecting the
471                    // allowed direction).
472                    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            // Box–point: push labels off every data point they cover. The
491            // final phase only separates labels from each other (a point
492            // that cannot be avoided must not keep two labels overlapping).
493            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            // Box–line: push a label a line crosses off it, along the line's
533            // normal (the same phases as the points).
534            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            // Spring back towards the target, then move and clamp.
559            let mut moved = 0.0f64;
560            for i in 0..n {
561                let b = &mut boxes[i];
562                // Pull back only labels that are currently free, so the spring
563                // never holds two labels in a residual overlap.
564                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    // Drop labels that still overlap too much: each label counts overlaps
592    // with already-kept labels and with points other than its own.
593    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        // Without a layout, covered points are not held against a label.
601        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
628/// Deterministic ±1 for coincident centres.
629fn 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
637/// A pixel line segment (start, end).
638type Seg = ((f64, f64), (f64, f64));
639
640/// One label to place.
641struct Item {
642    row: usize,
643    anchor: (f64, f64),
644    target: (f64, f64),
645    text: String,
646}
647
648/// Shared draw routine; `label` = `Some((fill, padding))` draws boxes.
649#[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    // `avoid_ablines` in pixels: the line between two far-apart x values
751    // (the data's own x range, widened) — the box test clips it.
752    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    // Segments first so labels paint over them.
805    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        // Leave a gap of `point_padding` at the point end.
817        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        // A dense cloud near the corner.
1067        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        // 40 labels in a panel that only fits a handful.
1093        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        // three labels sitting on the diagonal y = x
1182        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}