Skip to content

Commit

Permalink
Merge branch 'master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
Phlya committed Jun 23, 2020
2 parents d9532bc + 00802bc commit f6d1c77
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 76 deletions.
28 changes: 21 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,47 @@
<img src="https://github.com/Phlya/adjustText/blob/master/adjustText_logo.svg" width="183">

Ryan Neff: This version includes an improvement over the current package, by converting from Axes coordinates to display coordinates to allow for any underlying plot type, including polar graphs and 3D plots.
[![Documentation Status](https://readthedocs.org/projects/adjusttext/badge/?version=latest)](http://adjusttext.readthedocs.io/en/latest/?badge=latest)
[![Gitter chat](https://badges.gitter.im/gitterHQ/gitter.png)](https://gitter.im/adjustText/Lobby)
[![DOI](https://zenodo.org/badge/49349828.svg)](https://zenodo.org/badge/latestdoi/49349828)
[![PyPI version](https://badge.fury.io/py/adjustText.svg)](https://badge.fury.io/py/adjustText)

[![Documentation Status](https://readthedocs.org/projects/adjusttext/badge/?version=latest)](http://adjusttext.readthedocs.io/en/latest/?badge=latest) [![Gitter chat](https://badges.gitter.im/gitterHQ/gitter.png)](https://gitter.im/adjustText/Lobby)
# adjustText - automatic label placement for `matplotlib`

Inspired by **ggrepel** package for R/ggplot2 (https://github.com/slowkow/ggrepel)
![Alt text](figures/mtcars.gif "Labelled mtcars dataset")

## Brief description

The idea is that often when we want to label multiple points on a graph the text will start heavily overlapping with both other labels and data points. This can be a major problem requiring manual solution. However this can be largely automatized by smart placing of the labels (difficult) or iterative adjustment of their positions to minimize overlaps (relatively easy). This library (well... script) implements the latter option to help with matplotlib graphs. Usage is very straightforward with usually pretty good results with no tweaking (most important is to just make text slightly smaller than default and maybe the figure a little larger). However the algorithm itself is highly configurable for complicated plots.

There is a simple documentation now, thanks to Christophe Van Neste @beukueb!
## Getting started

http://adjusttext.readthedocs.io/en/latest/
### Installation

Should be installable from pypi:
```
pip install adjustText
```
Or with `conda`:
```
conda install -c phlya adjusttext
conda install -c conda-forge adjusttext
```

For the latest version from github:
```
pip install https://github.com/Phlya/adjustText/archive/master.zip
```

See [wiki] for some basic introduction, and more advanced usage examples [here].
### Documentation

[wiki]: https://github.com/Phlya/adjustText/wiki
[Wiki] has some basic introduction, and more advanced usage examples can be found [here].

Thanks to Christophe Van Neste @beukueb, **adjustText** has a simple documentation:
http://adjusttext.readthedocs.io/en/latest/

[Wiki]: https://github.com/Phlya/adjustText/wiki
[here]: https://github.com/Phlya/adjustText/blob/master/docs/source/Examples.ipynb

## Citing **adjustText**

To cite the library if you use it in scientific publications (or anywhere else, if you wish), please use the link to the GitHub repository (https://github.com/Phlya/adjustText) and a zenodo doi (see top of this page). Thank you!
216 changes: 150 additions & 66 deletions adjustText/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,52 @@
from itertools import product
import numpy as np
from operator import itemgetter
from matplotlib.path import get_path_collection_extents
import matplotlib

if sys.version_info >= (3, 0):
xrange = range

def get_bboxes_pathcollection(sc, ax):
"""Function to return a list of bounding boxes in data coordinates
for a scatter plot
Thank you to ImportanceOfBeingErnest
https://stackoverflow.com/a/55007838/1304161"""
# ax.figure.canvas.draw() # need to draw before the transforms are set.
transform = sc.get_transform()
transOffset = sc.get_offset_transform()
offsets = sc._offsets
paths = sc.get_paths()
transforms = sc.get_transforms()

if not transform.is_affine:
paths = [transform.transform_path_non_affine(p) for p in paths]
transform = transform.get_affine()
if not transOffset.is_affine:
offsets = transOffset.transform_non_affine(offsets)
transOffset = transOffset.get_affine()

if isinstance(offsets, np.ma.MaskedArray):
offsets = offsets.filled(np.nan)

bboxes = []

if len(paths) and len(offsets):
if len(paths) < len(offsets):
# for usual scatters you have one path, but several offsets
paths = [paths[0]]*len(offsets)
if len(transforms) < len(offsets):
# often you may have a single scatter size, but several offsets
transforms = [transforms[0]]*len(offsets)

for p, o, t in zip(paths, offsets, transforms):
result = get_path_collection_extents(
transform.frozen(), [p], [t],
[o], transOffset.frozen())
bboxes.append(result.inverse_transformed(ax.transData))

return bboxes

def get_text_position(text, ax=None):
ax = ax or plt.gca()
x, y = text.get_position()
Expand All @@ -29,7 +71,15 @@ def get_orig_coords(tupxy,ax=None):
def get_bboxes(objs, r, expand, ax):
if ax is None:
ax = plt.gca()
return [i.get_window_extent(r).expanded(*expand) for i in objs]
try:
return [i.get_window_extent(r).expanded(*expand).transformed(ax.\
transData.inverted()) for i in objs]
except (AttributeError, TypeError):
try:
if all([isinstance(obj, matplotlib.transforms.BboxBase) for obj in objs]):
return objs
except TypeError:
return get_bboxes_pathcollection(objs, ax)

def get_midpoint(bbox):
cx = (bbox.x0+bbox.x1)/2
Expand Down Expand Up @@ -357,7 +407,7 @@ def adjust_text(texts, x=None, y=None, add_objects=None, ax=None,
force_objects=(0.1, 0.25),
lim=500, precision=0.01,
only_move={'points':'xy', 'text':'xy', 'objects':'xy'},
text_from_text=True, text_from_points=True,
avoid_text=True, avoid_points=True, avoid_self=True,
save_steps=False, save_prefix='', save_format='png',
add_step_numbers=True, on_basemap=False,
*args, **kwargs):
Expand All @@ -374,67 +424,98 @@ def adjust_text(texts, x=None, y=None, add_objects=None, ax=None,
other and from points. In the end hides texts and substitutes them
with annotations to link them to the respective points.
Args:
texts (list): a list of text.Text objects to adjust
x (seq): x-coordinates of points to repel from; if not provided only
uses text coordinates
y (seq): y-coordinates of points to repel from; if not provided only
uses text coordinates
add_objects (list): a list of additional matplotlib objects to avoid;
they must have a .get_window_extent() method
ax (obj): axes object with the plot; if not provided is determined by
plt.gca()
expand_text (seq): a tuple/list/... with 2 multipliers (x, y) by which
to expand the bounding box of texts when repelling them from each other;
default (1.05, 1.2)
expand_points (seq): a tuple/list/... with 2 multipliers (x, y) by which
to expand the bounding box of texts when repelling them from points;
default (1.05, 1.2)
expand_objects (seq): a tuple/list/... with 2 multipliers (x, y) by which
to expand the bounding box of texts when repelling them from other objects;
default (1.05, 1.2)
expand_align (seq): a tuple/list/... with 2 multipliers (x, y) by which
to expand the bounding box of texts when autoaligning texts;
default (1.05, 1.2)
autoalign: If 'xy' or True, the best alignment of all texts will be
determined in all directions automatically before running the
iterative adjustment (overriding va and ha); if 'x', will only align
horizontally, if 'y', vertically; if False, do nothing (i.e.
preserve va and ha); default 'xy'
va (str): vertical alignment of texts; default 'center'
ha (str): horizontal alignment of texts; default 'center'
force_text (float): the repel force from texts is multiplied by this
value; default (0.1, 0.25)
force_points (float): the repel force from points is multiplied by this
value; default (0.2, 0.5)
force_objects (float): same as other forces, but for repelling
additional objects; default (0.1, 0.25)
lim (int): limit of number of iterations
precision (float): iterate until the sum of all overlaps along both x
and y are less than this amount, as a fraction of the total widths
and heights, respectively. May need to increase for complicated
situations; default 0.01
only_move (dict): a dict to restrict movement of texts to only certain
axis. Valid keys are 'points', 'text', and 'objects'. For each of
them valid values are 'x', 'y' and 'xy'. This way you can forbid
moving texts along either of the axes due to overlaps with points,
but let it happen if there is an overlap with texts:
only_move={'points':'y', 'text':'xy'}. Default: everything is allowed.
text_from_text (bool): whether to repel texts from each other; default
True
text_from_points (bool): whether to repel texts from points; default
True; can be helpful to switch off in extremely crowded plots
save_steps (bool): whether to save intermediate steps as images;
default False
save_prefix (str): a path and/or prefix to the saved steps; default ''
save_format (str): a format to save the steps into; default 'png
add_step_numbers (bool): whether to add step numbers as titles to the
images of saving steps
on_basemap (bool): whether your plot uses the basemap library, stops
labels going over the edge of the map; default False
args and kwargs: any arguments will be fed into ax.annotate after
all the optimization is done just for plotting the connecting arrows
if required
Parameters
----------
texts : list
A list of :obj:`matplotlib.text.Text` objects to adjust.
Other Parameters
----------------
x : array_like
x-coordinates of points to repel from; if not provided only uses text
coordinates.
y : array_like
y-coordinates of points to repel from; if not provided only uses text
coordinates
add_objects : list or PathCollection
a list of additional matplotlib objects to avoid; they must have a
`.get_window_extent()` method; alternatively, a PathCollection or a
list of Bbox objects.
ax : matplotlib axe, default is current axe (plt.gca())
axe object with the plot
expand_text : array_like, default (1.05, 1.2)
a tuple/list/... with 2 multipliers (x, y) by which to expand the
bounding box of texts when repelling them from each other.
expand_points : array_like, default (1.05, 1.2)
a tuple/list/... with 2 multipliers (x, y) by which to expand the
bounding box of texts when repelling them from points.
expand_objects : array_like, default (1.05, 1.2)
a tuple/list/... with 2 multipliers (x, y) by which to expand the
bounding box of texts when repelling them from other objects.
expand_align : array_like, default (1.05, 1.2)
a tuple/list/... with 2 multipliers (x, y) by which to expand the
bounding box of texts when autoaligning texts.
autoalign: str or boolean {'xy', 'x', 'y', True, False}, default 'xy'
Direction in wich the best alignement will be determined
- 'xy' or True, best alignment of all texts determined in all
directions automatically before running the iterative adjustment
(overriding va and ha),
- 'x', will only align horizontally,
- 'y', will only align vertically,
- False, do nothing (i.e. preserve va and ha)
va : str, default 'center'
vertical alignment of texts
ha : str, default 'center'
horizontal alignment of texts,
force_text : tuple, default (0.1, 0.25)
the repel force from texts is multiplied by this value
force_points : tuple, default (0.2, 0.5)
the repel force from points is multiplied by this value
force_objects : float, default (0.1, 0.25)
same as other forces, but for repelling additional objects
lim : int, default 500
limit of number of iterations
precision : float, default 0.01
iterate until the sum of all overlaps along both x and y are less than
this amount, as a fraction of the total widths and heights,
respectively. May need to increase for complicated situations.
only_move : dict, default {'points':'xy', 'text':'xy', 'objects':'xy'}
a dict to restrict movement of texts to only certain axes for certain
types of overlaps.
Valid keys are 'points', 'text', and 'objects'.
Valid values are '', 'x', 'y', and 'xy'.
For example, only_move={'points':'y', 'text':'xy', 'objects':'xy'}
forbids moving texts along the x axis due to overlaps with points.
avoid_text : bool, default True
whether to repel texts from each other.
avoid_points : bool, default True
whether to repel texts from points. Can be helpful to switch off in
extremely crowded plots.
avoid_self : bool, default True
whether to repel texts from its original positions.
save_steps : bool, default False
whether to save intermediate steps as images.
save_prefix : str, default ''
if `save_steps` is True, a path and/or prefix to the saved steps.
save_format : str, default 'png'
if `save_steps` is True, a format to save the steps into.
add_step_numbers : bool, default True
if `save_steps` is True, whether to add step numbers as titles to the
images of saving steps.
on_basemap : bool, default False
whether your plot uses the basemap library, stops labels going over the
edge of the map.
args and kwargs :
any arguments will be fed into obj:`ax.annotate` after all the
optimization is done just for plotting the connecting arrows if
required.
Return
------
int
Number of iteration
"""
plt.draw()
if ax is None:
Expand Down Expand Up @@ -470,7 +551,10 @@ def adjust_text(texts, x=None, y=None, add_objects=None, ax=None,

if x is None:
if y is None:
x, y = orig_x, orig_y
if avoid_self:
x, y = orig_x, orig_y
else:
x, y = [], []
else:
raise ValueError('Please specify both x and y, or neither')
if y is None:
Expand Down Expand Up @@ -519,13 +603,13 @@ def adjust_text(texts, x=None, y=None, add_objects=None, ax=None,
for i in xrange(lim):
# q1, q2 = [np.inf, np.inf], [np.inf, np.inf]

if text_from_text:
if avoid_text:
d_x_text, d_y_text, q1 = repel_text(texts, renderer=r, ax=ax,
expand=expand_text)
else:
d_x_text, d_y_text, q1 = [0]*len(texts), [0]*len(texts), (0, 0)

if text_from_points:
if avoid_points:
d_x_points, d_y_points, q2 = repel_text_from_points(x, y, texts,
ax=ax, renderer=r,
expand=expand_points)
Expand Down
5 changes: 3 additions & 2 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@
'sphinx.ext.viewcode',
'sphinx.ext.githubpages',
'nbsphinx',
'sphinx.ext.mathjax'
'sphinx.ext.mathjax',
'sphinx.ext.napoleon',
]

# Add any paths that contain templates here, relative to this directory.
Expand Down Expand Up @@ -160,7 +161,7 @@
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'adjustText', 'adjustText Documentation',
author, 'adjustText', 'One line description of project.',
author, 'adjustText', 'Auto adjust text for non-overlapping plot.',
'Miscellaneous'),
]

Expand Down
5 changes: 4 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,8 @@
author_email='flyamer@gmail.com',
url='https://github.com/Phlya/adjustText',
packages=['adjustText'],
install_requires=['numpy', 'matplotlib']
install_requires=['numpy', 'matplotlib'],
classifiers=[
'Framework :: Matplotlib',
'Topic :: Scientific/Engineering :: Visualization']
)

0 comments on commit f6d1c77

Please sign in to comment.