722 lines
26 KiB
Python
722 lines
26 KiB
Python
|
# MIT License
|
||
|
|
||
|
# Copyright (c) 2017 Ilya Flyamer
|
||
|
|
||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||
|
# of this software and associated documentation files (the "Software"), to deal
|
||
|
# in the Software without restriction, including without limitation the rights
|
||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||
|
# copies of the Software, and to permit persons to whom the Software is
|
||
|
# furnished to do so, subject to the following conditions:
|
||
|
|
||
|
# The above copyright notice and this permission notice shall be
|
||
|
# included in all
|
||
|
# copies or substantial portions of the Software.
|
||
|
|
||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||
|
# SOFTWARE.
|
||
|
|
||
|
from __future__ import division
|
||
|
import sys
|
||
|
from matplotlib import pyplot as plt
|
||
|
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()
|
||
|
return (ax.xaxis.convert_units(x),
|
||
|
ax.yaxis.convert_units(y))
|
||
|
|
||
|
|
||
|
def get_bboxes(objs, r, expand, ax):
|
||
|
if ax is None:
|
||
|
ax = plt.gca()
|
||
|
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
|
||
|
cy = (bbox.y0+bbox.y1)/2
|
||
|
return cx, cy
|
||
|
|
||
|
|
||
|
def get_points_inside_bbox(x, y, bbox):
|
||
|
"""Return the indices of points inside the given bbox."""
|
||
|
x1, y1, x2, y2 = bbox.xmin, bbox.ymin, bbox.xmax, bbox.ymax
|
||
|
x_in = np.logical_and(x > x1, x < x2)
|
||
|
y_in = np.logical_and(y > y1, y < y2)
|
||
|
return np.asarray(np.nonzero(x_in & y_in)[0])
|
||
|
|
||
|
|
||
|
def get_renderer(fig):
|
||
|
try:
|
||
|
return fig.canvas.get_renderer()
|
||
|
except AttributeError:
|
||
|
return fig.canvas.renderer
|
||
|
|
||
|
|
||
|
def overlap_bbox_and_point(bbox, xp, yp):
|
||
|
"""Given a bbox that contains a given point, return the (x, y) displacement
|
||
|
necessary to make the bbox not overlap the point."""
|
||
|
cx, cy = get_midpoint(bbox)
|
||
|
|
||
|
dir_x = np.sign(cx-xp)
|
||
|
dir_y = np.sign(cy-yp)
|
||
|
|
||
|
if dir_x == -1:
|
||
|
dx = xp - bbox.xmax
|
||
|
elif dir_x == 1:
|
||
|
dx = xp - bbox.xmin
|
||
|
else:
|
||
|
dx = 0
|
||
|
|
||
|
if dir_y == -1:
|
||
|
dy = yp - bbox.ymax
|
||
|
elif dir_y == 1:
|
||
|
dy = yp - bbox.ymin
|
||
|
else:
|
||
|
dy = 0
|
||
|
return dx, dy
|
||
|
|
||
|
|
||
|
def move_texts(texts, delta_x, delta_y, bboxes=None, renderer=None, ax=None):
|
||
|
if ax is None:
|
||
|
ax = plt.gca()
|
||
|
if bboxes is None:
|
||
|
if renderer is None:
|
||
|
r = get_renderer(ax.get_figure())
|
||
|
else:
|
||
|
r = renderer
|
||
|
bboxes = get_bboxes(texts, r, (1, 1), ax=ax)
|
||
|
xmin, xmax = sorted(ax.get_xlim())
|
||
|
ymin, ymax = sorted(ax.get_ylim())
|
||
|
for i, (text, dx, dy) in enumerate(zip(texts, delta_x, delta_y)):
|
||
|
bbox = bboxes[i]
|
||
|
x1, y1, x2, y2 = bbox.xmin, bbox.ymin, bbox.xmax, bbox.ymax
|
||
|
if x1 + dx < xmin:
|
||
|
dx = 0
|
||
|
if x2 + dx > xmax:
|
||
|
dx = 0
|
||
|
if y1 + dy < ymin:
|
||
|
dy = 0
|
||
|
if y2 + dy > ymax:
|
||
|
dy = 0
|
||
|
|
||
|
x, y = get_text_position(text, ax=ax)
|
||
|
newx = x + dx
|
||
|
newy = y + dy
|
||
|
text.set_position((newx, newy))
|
||
|
|
||
|
|
||
|
def optimally_align_text(x, y, texts, expand=(1., 1.), add_bboxes=[],
|
||
|
renderer=None, ax=None,
|
||
|
direction='xy'):
|
||
|
"""
|
||
|
For all text objects find alignment that causes the least overlap with
|
||
|
points and other texts and apply it
|
||
|
"""
|
||
|
if ax is None:
|
||
|
ax = plt.gca()
|
||
|
if renderer is None:
|
||
|
r = get_renderer(ax.get_figure())
|
||
|
else:
|
||
|
r = renderer
|
||
|
xmin, xmax = sorted(ax.get_xlim())
|
||
|
ymin, ymax = sorted(ax.get_ylim())
|
||
|
bboxes = get_bboxes(texts, r, expand, ax=ax)
|
||
|
if 'x' not in direction:
|
||
|
ha = ['']
|
||
|
else:
|
||
|
ha = ['left', 'right', 'center']
|
||
|
if 'y' not in direction:
|
||
|
va = ['']
|
||
|
else:
|
||
|
va = ['bottom', 'top', 'center']
|
||
|
alignment = list(product(ha, va))
|
||
|
# coords = np.array(zip(x, y))
|
||
|
for i, text in enumerate(texts):
|
||
|
# tcoords = np.array(text.get_position()).T
|
||
|
# nonself_coords = coords[~np.all(coords==tcoords, axis=1)]
|
||
|
# nonself_x, nonself_y = np.split(nonself_coords, 2, axis=1)
|
||
|
counts = []
|
||
|
for h, v in alignment:
|
||
|
if h:
|
||
|
text.set_ha(h)
|
||
|
if v:
|
||
|
text.set_va(v)
|
||
|
bbox = text.get_window_extent(r).expanded(*expand).\
|
||
|
transformed(ax.transData.inverted())
|
||
|
c = len(get_points_inside_bbox(x, y, bbox))
|
||
|
intersections = [bbox.intersection(bbox, bbox2) if i != j else None
|
||
|
for j, bbox2 in enumerate(bboxes+add_bboxes)]
|
||
|
intersections = sum([abs(b.width*b.height) if b is not None else 0
|
||
|
for b in intersections])
|
||
|
# Check for out-of-axes position
|
||
|
bbox = text.get_window_extent(r).\
|
||
|
transformed(ax.transData.inverted())
|
||
|
x1, y1, x2, y2 = bbox.xmin, bbox.ymin, bbox.xmax, bbox.ymax
|
||
|
if x1 < xmin or x2 > xmax or y1 < ymin or y2 > ymax:
|
||
|
axout = 1
|
||
|
else:
|
||
|
axout = 0
|
||
|
counts.append((axout, c, intersections))
|
||
|
# Most important: prefer alignments that keep the text inside the axes.
|
||
|
# If tied, take the alignments that minimize the number of x, y points
|
||
|
# contained inside the text.
|
||
|
# Break any remaining ties by minimizing the total
|
||
|
# area of intersections
|
||
|
# with all text bboxes and other objects to avoid.
|
||
|
a, value = min(enumerate(counts), key=itemgetter(1))
|
||
|
if 'x' in direction:
|
||
|
text.set_ha(alignment[a][0])
|
||
|
if 'y' in direction:
|
||
|
text.set_va(alignment[a][1])
|
||
|
bboxes[i] = text.get_window_extent(r).expanded(*expand).\
|
||
|
transformed(ax.transData.inverted())
|
||
|
return texts
|
||
|
|
||
|
|
||
|
def repel_text(texts, renderer=None, ax=None, expand=(1.2, 1.2),
|
||
|
only_use_max_min=False, move=False):
|
||
|
"""
|
||
|
Repel texts from each other while expanding their bounding boxes by expand
|
||
|
(x, y), e.g. (1.2, 1.2) would multiply width and height by 1.2.
|
||
|
Requires a renderer to get the actual sizes of the text, and to that end
|
||
|
either one needs to be directly provided, or the axes have to be specified,
|
||
|
and the renderer is then got from the axes object.
|
||
|
"""
|
||
|
if ax is None:
|
||
|
ax = plt.gca()
|
||
|
if renderer is None:
|
||
|
r = get_renderer(ax.get_figure())
|
||
|
else:
|
||
|
r = renderer
|
||
|
bboxes = get_bboxes(texts, r, expand, ax=ax)
|
||
|
xmins = [bbox.xmin for bbox in bboxes]
|
||
|
xmaxs = [bbox.xmax for bbox in bboxes]
|
||
|
ymaxs = [bbox.ymax for bbox in bboxes]
|
||
|
ymins = [bbox.ymin for bbox in bboxes]
|
||
|
|
||
|
overlaps_x = np.zeros((len(bboxes), len(bboxes)))
|
||
|
overlaps_y = np.zeros_like(overlaps_x)
|
||
|
overlap_directions_x = np.zeros_like(overlaps_x)
|
||
|
overlap_directions_y = np.zeros_like(overlaps_y)
|
||
|
for i, bbox1 in enumerate(bboxes):
|
||
|
overlaps = get_points_inside_bbox(xmins*2+xmaxs*2, (ymins+ymaxs)*2,
|
||
|
bbox1) % len(bboxes)
|
||
|
overlaps = np.unique(overlaps)
|
||
|
for j in overlaps:
|
||
|
bbox2 = bboxes[j]
|
||
|
x, y = bbox1.intersection(bbox1, bbox2).size
|
||
|
overlaps_x[i, j] = x
|
||
|
overlaps_y[i, j] = y
|
||
|
direction = np.sign(bbox1.extents - bbox2.extents)[:2]
|
||
|
overlap_directions_x[i, j] = direction[0]
|
||
|
overlap_directions_y[i, j] = direction[1]
|
||
|
|
||
|
move_x = overlaps_x*overlap_directions_x
|
||
|
move_y = overlaps_y*overlap_directions_y
|
||
|
|
||
|
delta_x = move_x.sum(axis=1)
|
||
|
delta_y = move_y.sum(axis=1)
|
||
|
|
||
|
q = np.sum(overlaps_x), np.sum(overlaps_y)
|
||
|
if move:
|
||
|
move_texts(texts, delta_x, delta_y, bboxes, ax=ax)
|
||
|
return delta_x, delta_y, q
|
||
|
|
||
|
|
||
|
def repel_text_from_bboxes(add_bboxes, texts, renderer=None, ax=None,
|
||
|
expand=(1.2, 1.2), only_use_max_min=False,
|
||
|
move=False):
|
||
|
"""
|
||
|
Repel texts from other objects' bboxes while expanding their (texts')
|
||
|
bounding boxes by expand (x, y), e.g. (1.2, 1.2) would multiply width and
|
||
|
height by 1.2.
|
||
|
Requires a renderer to get the actual sizes of the text, and to that end
|
||
|
either one needs to be directly provided, or the axes have to be specified,
|
||
|
and the renderer is then got from the axes object.
|
||
|
"""
|
||
|
if ax is None:
|
||
|
ax = plt.gca()
|
||
|
if renderer is None:
|
||
|
r = get_renderer(ax.get_figure())
|
||
|
else:
|
||
|
r = renderer
|
||
|
|
||
|
bboxes = get_bboxes(texts, r, expand, ax=ax)
|
||
|
|
||
|
overlaps_x = np.zeros((len(bboxes), len(add_bboxes)))
|
||
|
overlaps_y = np.zeros_like(overlaps_x)
|
||
|
overlap_directions_x = np.zeros_like(overlaps_x)
|
||
|
overlap_directions_y = np.zeros_like(overlaps_y)
|
||
|
|
||
|
for i, bbox1 in enumerate(bboxes):
|
||
|
for j, bbox2 in enumerate(add_bboxes):
|
||
|
try:
|
||
|
x, y = bbox1.intersection(bbox1, bbox2).size
|
||
|
direction = np.sign(bbox1.extents - bbox2.extents)[:2]
|
||
|
overlaps_x[i, j] = x
|
||
|
overlaps_y[i, j] = y
|
||
|
overlap_directions_x[i, j] = direction[0]
|
||
|
overlap_directions_y[i, j] = direction[1]
|
||
|
except AttributeError:
|
||
|
pass
|
||
|
|
||
|
move_x = overlaps_x*overlap_directions_x
|
||
|
move_y = overlaps_y*overlap_directions_y
|
||
|
|
||
|
delta_x = move_x.sum(axis=1)
|
||
|
delta_y = move_y.sum(axis=1)
|
||
|
|
||
|
q = np.sum(overlaps_x), np.sum(overlaps_y)
|
||
|
if move:
|
||
|
move_texts(texts, delta_x, delta_y, bboxes, ax=ax)
|
||
|
return delta_x, delta_y, q
|
||
|
|
||
|
|
||
|
def repel_text_from_points(x, y, texts, renderer=None, ax=None,
|
||
|
expand=(1.2, 1.2), move=False):
|
||
|
"""
|
||
|
Repel texts from all points specified by x and y while expanding their
|
||
|
(texts'!) bounding boxes by expandby (x, y), e.g. (1.2, 1.2)
|
||
|
would multiply both width and height by 1.2.
|
||
|
Requires a renderer to get the actual sizes of the text, and to that end
|
||
|
either one needs to be directly provided, or the axes have to be specified,
|
||
|
and the renderer is then got from the axes object.
|
||
|
"""
|
||
|
assert len(x) == len(y)
|
||
|
if ax is None:
|
||
|
ax = plt.gca()
|
||
|
if renderer is None:
|
||
|
r = get_renderer(ax.get_figure())
|
||
|
else:
|
||
|
r = renderer
|
||
|
bboxes = get_bboxes(texts, r, expand, ax=ax)
|
||
|
|
||
|
# move_x[i,j] is the x displacement of the i'th
|
||
|
# text caused by the j'th point
|
||
|
move_x = np.zeros((len(bboxes), len(x)))
|
||
|
move_y = np.zeros((len(bboxes), len(x)))
|
||
|
for i, bbox in enumerate(bboxes):
|
||
|
xy_in = get_points_inside_bbox(x, y, bbox)
|
||
|
for j in xy_in:
|
||
|
xp, yp = x[j], y[j]
|
||
|
dx, dy = overlap_bbox_and_point(bbox, xp, yp)
|
||
|
|
||
|
move_x[i, j] = dx
|
||
|
move_y[i, j] = dy
|
||
|
|
||
|
delta_x = move_x.sum(axis=1)
|
||
|
delta_y = move_y.sum(axis=1)
|
||
|
q = np.sum(np.abs(move_x)), np.sum(np.abs(move_y))
|
||
|
if move:
|
||
|
move_texts(texts, delta_x, delta_y, bboxes, ax=ax)
|
||
|
return delta_x, delta_y, q
|
||
|
|
||
|
|
||
|
def repel_text_from_axes(texts, ax=None, bboxes=None, renderer=None,
|
||
|
expand=None):
|
||
|
if ax is None:
|
||
|
ax = plt.gca()
|
||
|
if renderer is None:
|
||
|
r = get_renderer(ax.get_figure())
|
||
|
else:
|
||
|
r = renderer
|
||
|
if expand is None:
|
||
|
expand = (1, 1)
|
||
|
if bboxes is None:
|
||
|
bboxes = get_bboxes(texts, r, expand=expand, ax=ax)
|
||
|
xmin, xmax = sorted(ax.get_xlim())
|
||
|
ymin, ymax = sorted(ax.get_ylim())
|
||
|
for i, bbox in enumerate(bboxes):
|
||
|
x1, y1, x2, y2 = bbox.xmin, bbox.ymin, bbox.xmax, bbox.ymax
|
||
|
dx, dy = 0, 0
|
||
|
if x1 < xmin:
|
||
|
dx = xmin - x1
|
||
|
if x2 > xmax:
|
||
|
dx = xmax - x2
|
||
|
if y1 < ymin:
|
||
|
dy = ymin - y1
|
||
|
if y2 > ymax:
|
||
|
dy = ymax - y2
|
||
|
if dx or dy:
|
||
|
x, y = get_text_position(texts[i], ax=ax)
|
||
|
newx, newy = x + dx, y + dy
|
||
|
texts[i].set_position((newx, newy))
|
||
|
return texts
|
||
|
|
||
|
|
||
|
def float_to_tuple(a):
|
||
|
try:
|
||
|
a = float(a)
|
||
|
return (a, a)
|
||
|
except TypeError:
|
||
|
assert len(a) == 2
|
||
|
try:
|
||
|
b = float(a[0]), float(a[1])
|
||
|
except TypeError:
|
||
|
raise TypeError('Force values must be castable to floats')
|
||
|
return b
|
||
|
|
||
|
|
||
|
def adjust_text(texts, x=None, y=None, add_objects=None, ax=None,
|
||
|
expand_text=(1.05, 1.2), expand_points=(1.05, 1.2),
|
||
|
expand_objects=(1.05, 1.2), expand_align=(1.05, 1.2),
|
||
|
autoalign='xy', va='center', ha='center',
|
||
|
force_text=(0.1, 0.25), force_points=(0.2, 0.5),
|
||
|
force_objects=(0.1, 0.25),
|
||
|
lim=500, precision=0.01,
|
||
|
only_move={'points': 'xy', 'text': 'xy', 'objects': 'xy'},
|
||
|
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):
|
||
|
"""Iteratively adjusts the locations of texts.
|
||
|
|
||
|
Call adjust_text the very last, after all plotting (especially
|
||
|
anything that can change the axes limits) has been done. This is
|
||
|
because to move texts the function needs to use the dimensions of
|
||
|
the axes, and without knowing the final size of the plots the
|
||
|
results will be completely nonsensical, or suboptimal.
|
||
|
|
||
|
First moves all texts that are outside the axes limits
|
||
|
inside. Then in each iteration moves all texts away from each
|
||
|
other and from points. In the end hides texts and substitutes them
|
||
|
with annotations to link them to the respective points.
|
||
|
|
||
|
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:
|
||
|
ax = plt.gca()
|
||
|
r = get_renderer(ax.get_figure())
|
||
|
orig_xy = [get_text_position(text, ax=ax) for text in texts]
|
||
|
orig_x = [xy[0] for xy in orig_xy]
|
||
|
orig_y = [xy[1] for xy in orig_xy]
|
||
|
force_objects = float_to_tuple(force_objects)
|
||
|
force_text = float_to_tuple(force_text)
|
||
|
force_points = float_to_tuple(force_points)
|
||
|
|
||
|
# xdiff = np.diff(ax.get_xlim())[0]
|
||
|
# ydiff = np.diff(ax.get_ylim())[0]
|
||
|
|
||
|
bboxes = get_bboxes(texts, r, (1.0, 1.0), ax)
|
||
|
sum_width = np.sum(list(map(lambda bbox: bbox.width, bboxes)))
|
||
|
sum_height = np.sum(list(map(lambda bbox: bbox.height, bboxes)))
|
||
|
if not any(list(map(lambda val: 'x' in val, only_move.values()))):
|
||
|
precision_x = np.inf
|
||
|
else:
|
||
|
precision_x = precision*sum_width
|
||
|
#
|
||
|
if not any(list(map(lambda val: 'y' in val, only_move.values()))):
|
||
|
precision_y = np.inf
|
||
|
else:
|
||
|
precision_y = precision*sum_height
|
||
|
|
||
|
if x is None:
|
||
|
if y is None:
|
||
|
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:
|
||
|
raise ValueError('Please specify both x and y, or neither')
|
||
|
if add_objects is None:
|
||
|
text_from_objects = False
|
||
|
add_bboxes = []
|
||
|
else:
|
||
|
try:
|
||
|
add_bboxes = get_bboxes(add_objects, r, (1, 1), ax)
|
||
|
except ValueError:
|
||
|
raise ValueError("Can't get bounding boxes from add_objects - is'\
|
||
|
it a flat list of matplotlib objects?")
|
||
|
return
|
||
|
text_from_objects = True
|
||
|
for text in texts:
|
||
|
text.set_va(va)
|
||
|
text.set_ha(ha)
|
||
|
if save_steps:
|
||
|
if add_step_numbers:
|
||
|
plt.title('Before')
|
||
|
plt.savefig('%s%s.%s' % (save_prefix,
|
||
|
'000a', save_format),
|
||
|
format=save_format, dpi=150)
|
||
|
elif on_basemap:
|
||
|
ax.draw(r)
|
||
|
|
||
|
if autoalign:
|
||
|
if autoalign is True:
|
||
|
autoalign = 'xy'
|
||
|
for i in range(2):
|
||
|
texts = optimally_align_text(x, y, texts, expand=expand_align,
|
||
|
add_bboxes=add_bboxes,
|
||
|
direction=autoalign, renderer=r,
|
||
|
ax=ax)
|
||
|
|
||
|
if save_steps:
|
||
|
if add_step_numbers:
|
||
|
plt.title('Autoaligned')
|
||
|
plt.savefig('%s%s.%s' % (save_prefix,
|
||
|
'000b', save_format),
|
||
|
format=save_format, dpi=150)
|
||
|
elif on_basemap:
|
||
|
ax.draw(r)
|
||
|
|
||
|
texts = repel_text_from_axes(texts, ax, renderer=r, expand=expand_points)
|
||
|
history = [(np.inf, np.inf)]*10
|
||
|
for i in xrange(lim):
|
||
|
# q1, q2 = [np.inf, np.inf], [np.inf, np.inf]
|
||
|
|
||
|
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 avoid_points:
|
||
|
d_x_points, d_y_points, q2 = repel_text_from_points(
|
||
|
x, y, texts,
|
||
|
ax=ax,
|
||
|
renderer=r,
|
||
|
expand=expand_points)
|
||
|
else:
|
||
|
d_x_points, d_y_points, q2 = [0]*len(texts), [0]*len(texts), (0, 0)
|
||
|
|
||
|
if text_from_objects:
|
||
|
d_x_objects, d_y_objects, q3 = repel_text_from_bboxes(
|
||
|
add_bboxes,
|
||
|
texts,
|
||
|
ax=ax,
|
||
|
renderer=r,
|
||
|
expand=expand_objects)
|
||
|
else:
|
||
|
d_x_objects, d_y_objects, q3 = [0]*len(texts), [0]*len(texts),\
|
||
|
(0, 0)
|
||
|
|
||
|
if only_move:
|
||
|
if 'text' in only_move:
|
||
|
if 'x' not in only_move['text']:
|
||
|
d_x_text = np.zeros_like(d_x_text)
|
||
|
if 'y' not in only_move['text']:
|
||
|
d_y_text = np.zeros_like(d_y_text)
|
||
|
if 'points' in only_move:
|
||
|
if 'x' not in only_move['points']:
|
||
|
d_x_points = np.zeros_like(d_x_points)
|
||
|
if 'y' not in only_move['points']:
|
||
|
d_y_points = np.zeros_like(d_y_points)
|
||
|
if 'objects' in only_move:
|
||
|
if 'x' not in only_move['objects']:
|
||
|
d_x_objects = np.zeros_like(d_x_objects)
|
||
|
if 'y' not in only_move['objects']:
|
||
|
d_y_objects = np.zeros_like(d_y_objects)
|
||
|
|
||
|
dx = (np.array(d_x_text) * force_text[0] +
|
||
|
np.array(d_x_points) * force_points[0] +
|
||
|
np.array(d_x_objects) * force_objects[0])
|
||
|
dy = (np.array(d_y_text) * force_text[1] +
|
||
|
np.array(d_y_points) * force_points[1] +
|
||
|
np.array(d_y_objects) * force_objects[1])
|
||
|
qx = np.sum([q[0] for q in [q1, q2, q3]])
|
||
|
qy = np.sum([q[1] for q in [q1, q2, q3]])
|
||
|
histm = np.max(np.array(history), axis=0)
|
||
|
history.pop(0)
|
||
|
history.append((qx, qy))
|
||
|
move_texts(texts, dx, dy,
|
||
|
bboxes=get_bboxes(texts, r, (1, 1), ax), ax=ax)
|
||
|
if save_steps:
|
||
|
if add_step_numbers:
|
||
|
plt.title(i+1)
|
||
|
plt.savefig('%s%s.%s' % (save_prefix,
|
||
|
'{0:03}'.format(i+1), save_format),
|
||
|
format=save_format, dpi=150)
|
||
|
elif on_basemap:
|
||
|
ax.draw(r)
|
||
|
# Stop if we've reached the precision threshold,
|
||
|
# or if the x and y displacement
|
||
|
# are both greater than the max over the last
|
||
|
# 10 iterations (suggesting a
|
||
|
# failure to converge)
|
||
|
if (qx < precision_x and qy < precision_y)\
|
||
|
or np.all([qx, qy] >= histm):
|
||
|
break
|
||
|
# Now adding arrows from texts to their original locations if required
|
||
|
if 'arrowprops' in kwargs:
|
||
|
bboxes = get_bboxes(texts, r, (1, 1), ax)
|
||
|
kwap = kwargs.pop('arrowprops')
|
||
|
for j, (bbox, text) in enumerate(zip(bboxes, texts)):
|
||
|
ap = {'patchA': text} # Ensure arrow is clipped by the text
|
||
|
ap.update(kwap) # Add arrowprops from kwargs
|
||
|
ax.annotate("", # Add an arrow from the text to the point
|
||
|
xy=(orig_xy[j]),
|
||
|
xytext=get_midpoint(bbox),
|
||
|
arrowprops=ap,
|
||
|
*args, **kwargs)
|
||
|
|
||
|
if save_steps:
|
||
|
if add_step_numbers:
|
||
|
plt.title(i+1)
|
||
|
plt.savefig('%s%s.%s' % (save_prefix,
|
||
|
'{0:03}'.format(i+1), save_format),
|
||
|
format=save_format, dpi=150)
|
||
|
elif on_basemap:
|
||
|
ax.draw(r)
|
||
|
|
||
|
return i+1
|