Skip to content

bigomics/plotly.repel

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

43 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

plotly.repel

Automatically position and repel labels in plotly charts — the ggrepel experience for interactive plots

R package version License: GPL-3.0

Why plotly.repel?

Overlapping labels are the curse of dense scatter plots. In static charts, ggrepel solves this beautifully. But the Plotly community has been asking for equivalent functionality in interactive charts since at least 2019 — see plotly.js#4674 — with no official solution.

plotly.repel fills that gap. It gives you the familiar add_text_repel() / add_label_repel() API (ggrepel ergonomics you already know) and runs a fast placement solver in the browser at render time. Labels stay readable through zoom, pan, resize, and animation — because they're placed fresh every time the viewport changes, within a tight millisecond budget that keeps interactions smooth.

Installation

# Install from GitHub
remotes::install_github("bigomics/plotly.repel")

Quick start

library(plotly)
library(plotly.repel)

df <- data.frame(
  x    = c(1, 1.05, 1.02, 0.98, 1.5, 2.0),
  y    = c(2, 2.02, 2.05, 1.97, 2.5, 3.0),
  gene = c("TP53", "EGFR", "MYC", "PTEN", "BRCA1", "KRAS")
)

plot_ly(df, x = ~x, y = ~y, type = "scatter", mode = "markers") |>
  add_text_repel(x = ~x, y = ~y, text = ~gene)

Tuning knobs

Most defaults work well out of the box, but every knob is exposed when you need it:

add_text_repel(
  p,
  x = ~x, y = ~y, text = ~gene,
  force         = 1,       # repulsion strength between labels
  force_pull    = 1,       # spring pull toward each label's anchor point
  box_padding   = 0.35,    # gap around the text box (data units)
  point_padding = 0.2,     # gap around the data point (data units)
  direction     = "both",  # constrain repulsion: "both" | "x" | "y"
  max_time_ms   = 25,      # solver time budget in ms — raise for dense charts
  max_iter      = 200,     # hard iteration cap
  max_overlaps  = 10,      # drop labels when this many overlaps remain unsolvable
  seed          = 42,      # fix starting positions for reproducible output
  on            = c("render", "zoom", "resize")  # re-solve on these events
)

Core functions

add_text_repel()

Plain text labels with a thin connector line from label to point. Drop-in equivalent of ggrepel::geom_text_repel().

add_text_repel(p, x = ~x, y = ~y, text = ~label)

add_label_repel()

Labels with a filled background box — equivalent of ggrepel::geom_label_repel(). Useful for labels that need to stand out against a busy background.

add_label_repel(
  p,
  x = ~x, y = ~y, text = ~label,
  label = list(
    bgcolor     = "rgba(255, 255, 255, 0.9)",
    bordercolor = "rgba(0, 0, 0, 0.25)",
    borderwidth = 1,
    borderpad   = 3
  )
)

Advanced features

Show only selected labels

Pass a logical vector or an expression string to visibility to show only the labels you care about:

# Keep only high-priority hits (expression evaluated per-label in the browser)
add_text_repel(p, x = ~x, y = ~y, text = ~gene,
               priority   = ~abs(log2fc),
               visibility = "priority > 1.5")

# Or pass a pre-computed logical vector from R
keep <- abs(df$log2fc) > 1.5
add_text_repel(p, x = ~x, y = ~y, text = ~gene, visibility = keep)

Priority-based culling

When there are more labels than fit comfortably, priority controls which ones survive:

add_text_repel(p, x = ~x, y = ~y, text = ~gene,
               priority     = ~abs(log2fc),
               max_overlaps = 5)

Custom segment and font styling

add_text_repel(p, x = ~x, y = ~y, text = ~gene,
               segment = list(color = "rgba(120, 120, 120, 0.6)", width = 1.5),
               font    = list(size = 13, color = "navy"))

Event triggers

# Static: solve once at render, don't reposition on interaction
add_text_repel(p, x = ~x, y = ~y, text = ~gene, on = "render")

# Dynamic: re-solve on zoom and resize but not pan
add_text_repel(p, x = ~x, y = ~y, text = ~gene,
               on = c("render", "zoom", "resize"))

Getting started — full tutorial

The mtcars vignette walks through a complete example: starting from bare plotly markers, adding repelled labels, filtering with visibility, tuning with priority, and polishing with styling options. Copy-paste ready with the built-in mtcars dataset.

How it works (briefly)

Because plotly charts are interactive, label positions must be recalculated whenever the viewport changes. plotly.repel injects a small JavaScript solver that runs in the browser at render time and re-solves on the events you configure with the on parameter. Placement is constrained by a time budget (max_time_ms) to keep interactions snappy, with an iteration cap (max_iter) as a safety net.

Comparison to ggrepel

Feature ggrepel plotly.repel
Interactive charts
Zoom / pan updates
Animation support
API style geom_text_repel() add_text_repel()
Where placement runs R (once) Browser (per viewport)

Browser compatibility

Requires modern browsers with JavaScript support:

  • Chrome 90+
  • Firefox 88+
  • Safari 14+
  • Edge 90+

License

GPL-3.0 © Santiago Cano Muniz

About

R package for repelling overlapping text labels for plotly

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors