Iteratively adjust text position in matplotlib plots to minimize overlaps
npx @tessl/cli install tessl/pypi-adjusttext@1.3.0A Python library for iteratively adjusting text position in matplotlib plots to minimize overlaps between text annotations and data points. Inspired by the ggrepel R package, adjustText implements an intelligent algorithm that automatically repositions matplotlib text labels to improve plot readability without manual intervention.
pip install adjustTextfrom adjustText import adjust_textImport with version:
from adjustText import adjust_text, __version__Import utility functions:
from adjustText import overlap_intervalsAlternative import:
import adjustText
# Access: adjustText.adjust_text()import matplotlib.pyplot as plt
import numpy as np
from adjustText import adjust_text
# Create sample data
x = np.random.randn(20)
y = np.random.randn(20)
# Create scatter plot
fig, ax = plt.subplots()
ax.scatter(x, y)
# Create text labels
texts = []
for i, (xi, yi) in enumerate(zip(x, y)):
texts.append(ax.text(xi, yi, f'Point {i}', fontsize=9))
# Automatically adjust text positions to avoid overlaps
adjust_text(texts, ax=ax)
plt.show()Advanced usage with additional objects to avoid:
import matplotlib.pyplot as plt
import numpy as np
from adjustText import adjust_text
# Create data and plot
x = np.random.randn(15)
y = np.random.randn(15)
fig, ax = plt.subplots(figsize=(10, 8))
scatter = ax.scatter(x, y, s=100, c='red', alpha=0.7)
# Add some additional objects to avoid
objects = [scatter]
# Create text labels
texts = []
for i, (xi, yi) in enumerate(zip(x, y)):
texts.append(ax.text(xi, yi, f'Label {i}', fontsize=10))
# Adjust text positions avoiding both other texts and scatter points
adjust_text(texts,
objects=objects,
expand=(1.1, 1.3),
force_text=(0.05, 0.25),
force_static=(0.02, 0.25),
ax=ax)
plt.show()Automatically adjusts the positions of matplotlib text objects to minimize overlaps using an iterative physics-based algorithm.
def adjust_text(
texts,
x=None,
y=None,
objects=None,
target_x=None,
target_y=None,
avoid_self=True,
prevent_crossings=True,
force_text=(0.1, 0.2),
force_static=(0.1, 0.2),
force_pull=(0.01, 0.01),
force_explode=(0.1, 0.5),
pull_threshold=10,
expand=(1.05, 1.2),
max_move=(10, 10),
explode_radius="auto",
ensure_inside_axes=True,
expand_axes=False,
only_move={"text": "xy", "static": "xy", "explode": "xy", "pull": "xy"},
ax=None,
min_arrow_len=5,
time_lim=None,
iter_lim=None,
*args,
**kwargs
):
"""
Iteratively adjusts the locations of texts to minimize overlaps.
Must be called after all plotting is complete, as the function needs
the final axes dimensions to work correctly.
Parameters:
- texts: List of matplotlib.text.Text objects to adjust
- x, y: Array-like coordinates of points to repel from (optional, with avoid_self=True original text positions are added)
- objects: List of matplotlib objects to avoid or PathCollection/list of Bbox objects (must have get_window_extent() method)
- target_x, target_y: Array-like coordinates to connect adjusted texts to (optional, defaults to original text positions)
- avoid_self: Whether to repel texts from their original positions (bool, default True)
- prevent_crossings: Whether to prevent arrows from crossing each other (bool, default True, experimental)
- force_text: Multiplier for text-text repulsion forces (tuple[float, float] | float, default (0.1, 0.2))
- force_static: Multiplier for text-object repulsion forces (tuple[float, float] | float, default (0.1, 0.2))
- force_pull: Multiplier for pull-back-to-origin forces (tuple[float, float] | float, default (0.01, 0.01))
- force_explode: Multiplier for initial explosion forces (tuple[float, float] | float, default (0.1, 0.5))
- pull_threshold: Distance threshold for pull-back forces in display units (float, default 10)
- expand: Multipliers for expanding text bounding boxes (tuple[float, float], default (1.05, 1.2))
- max_move: Maximum movement per iteration in display units (tuple[int, int] | int | None, default (10, 10))
- explode_radius: Initial explosion radius in display units or "auto" (str | float, default "auto")
- ensure_inside_axes: Whether to keep texts inside axes boundaries (bool, default True)
- expand_axes: Whether to expand axes to fit all texts (bool, default False)
- only_move: Movement restrictions per force type (dict, default {"text": "xy", "static": "xy", "explode": "xy", "pull": "xy"})
- ax: Matplotlib axes object (matplotlib.axes.Axes | None, uses plt.gca() if None)
- min_arrow_len: Minimum arrow length to draw in display units (float, default 5)
- time_lim: Maximum time for adjustment in seconds (float | None, default None)
- iter_lim: Maximum number of iterations (int | None, default None)
- args, kwargs: Additional arguments passed to FancyArrowPatch for arrow styling
Returns:
- texts: List of adjusted text objects (list)
- patches: List of arrow patches connecting texts to targets (list of FancyArrowPatch or Annotation objects)
"""Access the package version:
__version__: str
# Package version string (e.g., "1.3.0")The package exports utility functions for advanced use cases:
def overlap_intervals(starts1, ends1, starts2, ends2, closed=False, sort=False):
"""
Find overlapping intervals between two sets of intervals.
Parameters:
- starts1, ends1: First set of interval coordinates (numpy.ndarray)
- starts2, ends2: Second set of interval coordinates (numpy.ndarray)
- closed: Whether to treat intervals as closed (bool)
- sort: Whether to sort results (bool)
Returns:
- overlap_ids: Array of overlapping interval pair indices (numpy.ndarray)
"""def arange_multi(starts, stops=None, lengths=None):
"""
Create concatenated ranges of integers for multiple start/length pairs.
Parameters:
- starts: Start values for each range (numpy.ndarray)
- stops: Stop values for each range (numpy.ndarray, optional)
- lengths: Length values for each range (numpy.ndarray, optional)
Returns:
- concat_ranges: Concatenated ranges (numpy.ndarray)
Notes:
Either stops or lengths must be provided, but not both.
"""The following functions are technically importable from the main module but are primarily intended for internal use:
def get_renderer(fig):
"""
Get a renderer for the given figure.
Parameters:
- fig: Matplotlib figure object
Returns:
- renderer: Figure renderer object
Raises:
- ValueError: If unable to determine renderer
"""def intersect(seg1, seg2):
"""
Check if two line segments intersect.
Parameters:
- seg1: First line segment as (x1, y1, x2, y2) tuple
- seg2: Second line segment as (x3, y3, x4, y4) tuple
Returns:
- intersects: Whether segments intersect (bool)
"""def get_bboxes(objs, r=None, expand=(1, 1), ax=None):
"""
Get bounding boxes for matplotlib objects.
Parameters:
- objs: List of objects or PathCollection to get bboxes from
- r: Renderer (optional, deduced from ax if None)
- expand: Expansion factors for bboxes (x, y) (tuple, default (1, 1))
- ax: Axes object (optional, uses current axes if None)
Returns:
- bboxes: List of bounding box objects
"""Note: These functions are not part of the stable public API and may change in future versions. They are documented here for completeness as they are technically accessible via import.
# Force specification types
ForceValue = tuple[float, float] | float
# Movement restriction values
MovementRestriction = str # "x", "y", "xy", "x+", "x-", "y+", "y-"
# Only move dictionary type
OnlyMoveDict = dict[str, MovementRestriction] # Keys: "text", "static", "explode", "pull"The adjustText algorithm works in several phases:
ensure_inside_axes=TrueThe algorithm is highly configurable through force multipliers, movement restrictions, and termination conditions, making it suitable for both simple automatic adjustment and fine-tuned control for complex visualizations.
The library is designed to be called as the final step in plot creation, after all other plotting operations are complete, ensuring optimal text positioning based on the final plot layout.