Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 32bed39

Browse files
committed
Merge pull request matplotlib#1865 from rephorm/contour_label_pos
Fix manual contour label positions on sparse contours
2 parents ff6d7eb + b2c0352 commit 32bed39

File tree

6 files changed

+997
-60
lines changed

6 files changed

+997
-60
lines changed

doc/users/whats_new.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,16 @@ Andrew Dawson added the ability to add axes titles flush with the left and
100100
right sides of the top of the axes using a new keyword argument `loc` to
101101
:func:`~matplotlib.pyplot.title`.
102102

103+
Improved manual contour plot label positioning
104+
----------------------------------------------
105+
106+
Brian Mattern modified the manual contour plot label positioning code to
107+
interpolate along line segments and find the actual closest point on a
108+
contour to the requested position. Previously, the closest path vertex was
109+
used, which, in the case of straight contours was sometimes quite distant
110+
from the requested location. Much more precise label positioning is now
111+
possible.
112+
103113
.. _whats-new-1-2:
104114

105115
new in matplotlib-1.2

lib/matplotlib/contour.py

Lines changed: 130 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def get_rotation(self):
4949

5050

5151
class ContourLabeler:
52-
'''Mixin to provide labelling capability to ContourSet'''
52+
"""Mixin to provide labelling capability to ContourSet"""
5353

5454
def clabel(self, *args, **kwargs):
5555
"""
@@ -572,6 +572,18 @@ def add_label_near(self, x, y, inline=True, inline_spacing=5,
572572
conmin, segmin, imin, xmin, ymin = self.find_nearest_contour(
573573
x, y, self.labelIndiceList)[:5]
574574

575+
# The calc_label_rot_and_inline routine requires that (xmin,ymin)
576+
# be a vertex in the path. So, if it isn't, add a vertex here
577+
paths = self.collections[conmin].get_paths()
578+
lc = paths[segmin].vertices
579+
if transform:
580+
xcmin = transform.inverted().transform([xmin, ymin])
581+
else:
582+
xcmin = np.array([xmin, ymin])
583+
if not np.allclose(xcmin, lc[imin]):
584+
lc = np.r_[lc[:imin], np.array(xcmin)[None, :], lc[imin:]]
585+
paths[segmin] = mpath.Path(lc)
586+
575587
# Get index of nearest level in subset of levels used for labeling
576588
lmin = self.labelIndiceList.index(conmin)
577589

@@ -608,7 +620,7 @@ def add_label_near(self, x, y, inline=True, inline_spacing=5,
608620
paths.append(mpath.Path(n))
609621

610622
def pop_label(self, index=-1):
611-
'''Defaults to removing last label, but any index can be supplied'''
623+
"""Defaults to removing last label, but any index can be supplied"""
612624
self.labelCValues.pop(index)
613625
t = self.labelTexts.pop(index)
614626
t.remove()
@@ -621,8 +633,8 @@ def labels(self, inline, inline_spacing):
621633
add_label = self.add_label
622634

623635
for icon, lev, fsize, cvalue in zip(
624-
self.labelIndiceList, self.labelLevelList, self.labelFontSizeList,
625-
self.labelCValueList):
636+
self.labelIndiceList, self.labelLevelList,
637+
self.labelFontSizeList, self.labelCValueList):
626638

627639
con = self.collections[icon]
628640
trans = con.get_transform()
@@ -674,6 +686,64 @@ def labels(self, inline, inline_spacing):
674686
paths.extend(additions)
675687

676688

689+
def _find_closest_point_on_leg(p1, p2, p0):
690+
"""find closest point to p0 on line segment connecting p1 and p2"""
691+
692+
# handle degenerate case
693+
if np.all(p2 == p1):
694+
d = np.sum((p0 - p1)**2)
695+
return d, p1
696+
697+
d21 = p2 - p1
698+
d01 = p0 - p1
699+
700+
# project on to line segment to find closest point
701+
proj = np.dot(d01, d21) / np.dot(d21, d21)
702+
if proj < 0:
703+
proj = 0
704+
if proj > 1:
705+
proj = 1
706+
pc = p1 + proj * d21
707+
708+
# find squared distance
709+
d = np.sum((pc-p0)**2)
710+
711+
return d, pc
712+
713+
714+
def _find_closest_point_on_path(lc, point):
715+
"""
716+
lc: coordinates of vertices
717+
point: coordinates of test point
718+
"""
719+
720+
# find index of closest vertex for this segment
721+
ds = np.sum((lc - point[None, :])**2, 1)
722+
imin = np.argmin(ds)
723+
724+
dmin = np.inf
725+
xcmin = None
726+
legmin = (None, None)
727+
728+
closed = mlab.is_closed_polygon(lc)
729+
730+
# build list of legs before and after this vertex
731+
legs = []
732+
if imin > 0 or closed:
733+
legs.append(((imin-1) % len(lc), imin))
734+
if imin < len(lc) - 1 or closed:
735+
legs.append((imin, (imin+1) % len(lc)))
736+
737+
for leg in legs:
738+
d, xc = _find_closest_point_on_leg(lc[leg[0]], lc[leg[1]], point)
739+
if d < dmin:
740+
dmin = d
741+
xcmin = xc
742+
legmin = leg
743+
744+
return (dmin, xcmin, legmin)
745+
746+
677747
class ContourSet(cm.ScalarMappable, ContourLabeler):
678748
"""
679749
Store a set of contour lines or filled regions.
@@ -832,12 +902,13 @@ def __init__(self, ax, *args, **kwargs):
832902
paths = self._make_paths(segs, kinds)
833903
# Default zorder taken from Collection
834904
zorder = kwargs.get('zorder', 1)
835-
col = mcoll.PathCollection(paths,
836-
antialiaseds=(self.antialiased,),
837-
edgecolors='none',
838-
alpha=self.alpha,
839-
transform=self.get_transform(),
840-
zorder=zorder)
905+
col = mcoll.PathCollection(
906+
paths,
907+
antialiaseds=(self.antialiased,),
908+
edgecolors='none',
909+
alpha=self.alpha,
910+
transform=self.get_transform(),
911+
zorder=zorder)
841912
self.ax.add_collection(col)
842913
self.collections.append(col)
843914
else:
@@ -851,13 +922,14 @@ def __init__(self, ax, *args, **kwargs):
851922
zip(self.levels, tlinewidths, tlinestyles, self.allsegs):
852923
# Default zorder taken from LineCollection
853924
zorder = kwargs.get('zorder', 2)
854-
col = mcoll.LineCollection(segs,
855-
antialiaseds=aa,
856-
linewidths=width,
857-
linestyle=[lstyle],
858-
alpha=self.alpha,
859-
transform=self.get_transform(),
860-
zorder=zorder)
925+
col = mcoll.LineCollection(
926+
segs,
927+
antialiaseds=aa,
928+
linewidths=width,
929+
linestyle=[lstyle],
930+
alpha=self.alpha,
931+
transform=self.get_transform(),
932+
zorder=zorder)
861933
col.set_label('_nolegend_')
862934
self.ax.add_collection(col, False)
863935
self.collections.append(col)
@@ -902,29 +974,27 @@ def legend_elements(self, variable_name='x', str_format=str):
902974
n_levels = len(self.collections)
903975

904976
for i, (collection, lower, upper) in enumerate(
905-
zip(self.collections,
906-
lowers, uppers)):
907-
patch = mpatches.Rectangle(
908-
(0, 0), 1, 1,
909-
facecolor=collection.get_facecolor()[0],
910-
hatch=collection.get_hatch(),
911-
alpha=collection.get_alpha(),
912-
)
913-
artists.append(patch)
914-
915-
lower = str_format(lower)
916-
upper = str_format(upper)
917-
918-
if i == 0 and self.extend in ('min', 'both'):
919-
labels.append(r'$%s \leq %s$' % (variable_name,
920-
lower))
921-
elif i == n_levels - 1 and self.extend in ('max', 'both'):
922-
labels.append(r'$%s > %s$' % (variable_name,
923-
upper))
924-
else:
925-
labels.append(r'$%s < %s \leq %s$' % (lower,
926-
variable_name,
927-
upper))
977+
zip(self.collections, lowers, uppers)):
978+
patch = mpatches.Rectangle(
979+
(0, 0), 1, 1,
980+
facecolor=collection.get_facecolor()[0],
981+
hatch=collection.get_hatch(),
982+
alpha=collection.get_alpha())
983+
artists.append(patch)
984+
985+
lower = str_format(lower)
986+
upper = str_format(upper)
987+
988+
if i == 0 and self.extend in ('min', 'both'):
989+
labels.append(r'$%s \leq %s$' % (variable_name,
990+
lower))
991+
elif i == n_levels - 1 and self.extend in ('max', 'both'):
992+
labels.append(r'$%s > %s$' % (variable_name,
993+
upper))
994+
else:
995+
labels.append(r'$%s < %s \leq %s$' % (lower,
996+
variable_name,
997+
upper))
928998
else:
929999
for collection, level in zip(self.collections, self.levels):
9301000

@@ -963,7 +1033,7 @@ def _process_args(self, *args, **kwargs):
9631033

9641034
# Check length of allkinds.
9651035
if (self.allkinds is not None and
966-
len(self.allkinds) != len(self.allsegs)):
1036+
len(self.allkinds) != len(self.allsegs)):
9671037
raise ValueError('allkinds has different length to allsegs')
9681038

9691039
# Determine x,y bounds and update axes data limits.
@@ -1032,7 +1102,7 @@ def changed(self):
10321102
cm.ScalarMappable.changed(self)
10331103

10341104
def _autolev(self, z, N):
1035-
'''
1105+
"""
10361106
Select contour levels to span the data.
10371107
10381108
We need two more levels for filled contours than for
@@ -1041,7 +1111,7 @@ def _autolev(self, z, N):
10411111
a single contour boundary, say at z = 0, requires only
10421112
one contour line, but two filled regions, and therefore
10431113
three levels to provide boundaries for both regions.
1044-
'''
1114+
"""
10451115
if self.locator is None:
10461116
if self.logscale:
10471117
self.locator = ticker.LogLocator()
@@ -1210,11 +1280,11 @@ def _process_linestyles(self):
12101280
return tlinestyles
12111281

12121282
def get_alpha(self):
1213-
'''returns alpha to be applied to all ContourSet artists'''
1283+
"""returns alpha to be applied to all ContourSet artists"""
12141284
return self.alpha
12151285

12161286
def set_alpha(self, alpha):
1217-
'''sets alpha for all ContourSet artists'''
1287+
"""sets alpha for all ContourSet artists"""
12181288
self.alpha = alpha
12191289
self.changed()
12201290

@@ -1256,32 +1326,33 @@ def find_nearest_contour(self, x, y, indices=None, pixel=True):
12561326
if indices is None:
12571327
indices = range(len(self.levels))
12581328

1259-
dmin = 1e10
1329+
dmin = np.inf
12601330
conmin = None
12611331
segmin = None
12621332
xmin = None
12631333
ymin = None
12641334

1335+
point = np.array([x, y])
1336+
12651337
for icon in indices:
12661338
con = self.collections[icon]
12671339
trans = con.get_transform()
12681340
paths = con.get_paths()
1341+
12691342
for segNum, linepath in enumerate(paths):
12701343
lc = linepath.vertices
1271-
12721344
# transfer all data points to screen coordinates if desired
12731345
if pixel:
12741346
lc = trans.transform(lc)
12751347

1276-
ds = (lc[:, 0] - x) ** 2 + (lc[:, 1] - y) ** 2
1277-
d = min(ds)
1348+
d, xc, leg = _find_closest_point_on_path(lc, point)
12781349
if d < dmin:
12791350
dmin = d
12801351
conmin = icon
12811352
segmin = segNum
1282-
imin = mpl.mlab.find(ds == d)[0]
1283-
xmin = lc[imin, 0]
1284-
ymin = lc[imin, 1]
1353+
imin = leg[1]
1354+
xmin = xc[0]
1355+
ymin = xc[1]
12851356

12861357
return (conmin, segmin, imin, xmin, ymin, dmin)
12871358

@@ -1340,7 +1411,7 @@ def _process_args(self, *args, **kwargs):
13401411
# if the transform is not trans data, and some part of it
13411412
# contains transData, transform the xs and ys to data coordinates
13421413
if (t != self.ax.transData and
1343-
any(t.contains_branch_seperately(self.ax.transData))):
1414+
any(t.contains_branch_seperately(self.ax.transData))):
13441415
trans_to_data = t - self.ax.transData
13451416
pts = (np.vstack([x.flat, y.flat]).T)
13461417
transformed_pts = trans_to_data.transform(pts)
@@ -1408,14 +1479,14 @@ def _contour_args(self, args, kwargs):
14081479
return (x, y, z)
14091480

14101481
def _check_xyz(self, args, kwargs):
1411-
'''
1482+
"""
14121483
For functions like contour, check that the dimensions
14131484
of the input arrays match; if x and y are 1D, convert
14141485
them to 2D using meshgrid.
14151486
14161487
Possible change: I think we should make and use an ArgumentError
14171488
Exception class (here and elsewhere).
1418-
'''
1489+
"""
14191490
x, y = args[:2]
14201491
self.ax._process_unit_info(xdata=x, ydata=y, kwargs=kwargs)
14211492
x = self.ax.convert_xunits(x)
@@ -1450,11 +1521,11 @@ def _check_xyz(self, args, kwargs):
14501521

14511522
if x.shape != z.shape:
14521523
raise TypeError("Shape of x does not match that of z: found "
1453-
"{0} instead of {1}.".format(x.shape, z.shape))
1524+
"{0} instead of {1}.".format(x.shape, z.shape))
14541525

14551526
if y.shape != z.shape:
14561527
raise TypeError("Shape of y does not match that of z: found "
1457-
"{0} instead of {1}.".format(y.shape, z.shape))
1528+
"{0} instead of {1}.".format(y.shape, z.shape))
14581529

14591530
else:
14601531

@@ -1463,7 +1534,7 @@ def _check_xyz(self, args, kwargs):
14631534
return x, y, z
14641535

14651536
def _initialize_x_y(self, z):
1466-
'''
1537+
"""
14671538
Return X, Y arrays such that contour(Z) will match imshow(Z)
14681539
if origin is not None.
14691540
The center of pixel Z[i,j] depends on origin:
@@ -1474,7 +1545,7 @@ def _initialize_x_y(self, z):
14741545
as in imshow.
14751546
If origin is None and extent is not None, then extent
14761547
will give the minimum and maximum values of x and y.
1477-
'''
1548+
"""
14781549
if z.ndim != 2:
14791550
raise TypeError("Input must be a 2D array.")
14801551
else:
Binary file not shown.

0 commit comments

Comments
 (0)