@@ -49,7 +49,7 @@ def get_rotation(self):
49
49
50
50
51
51
class ContourLabeler :
52
- ''' Mixin to provide labelling capability to ContourSet'''
52
+ """ Mixin to provide labelling capability to ContourSet"""
53
53
54
54
def clabel (self , * args , ** kwargs ):
55
55
"""
@@ -572,6 +572,18 @@ def add_label_near(self, x, y, inline=True, inline_spacing=5,
572
572
conmin , segmin , imin , xmin , ymin = self .find_nearest_contour (
573
573
x , y , self .labelIndiceList )[:5 ]
574
574
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
+
575
587
# Get index of nearest level in subset of levels used for labeling
576
588
lmin = self .labelIndiceList .index (conmin )
577
589
@@ -608,7 +620,7 @@ def add_label_near(self, x, y, inline=True, inline_spacing=5,
608
620
paths .append (mpath .Path (n ))
609
621
610
622
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"""
612
624
self .labelCValues .pop (index )
613
625
t = self .labelTexts .pop (index )
614
626
t .remove ()
@@ -621,8 +633,8 @@ def labels(self, inline, inline_spacing):
621
633
add_label = self .add_label
622
634
623
635
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 ):
626
638
627
639
con = self .collections [icon ]
628
640
trans = con .get_transform ()
@@ -674,6 +686,64 @@ def labels(self, inline, inline_spacing):
674
686
paths .extend (additions )
675
687
676
688
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
+
677
747
class ContourSet (cm .ScalarMappable , ContourLabeler ):
678
748
"""
679
749
Store a set of contour lines or filled regions.
@@ -832,12 +902,13 @@ def __init__(self, ax, *args, **kwargs):
832
902
paths = self ._make_paths (segs , kinds )
833
903
# Default zorder taken from Collection
834
904
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 )
841
912
self .ax .add_collection (col )
842
913
self .collections .append (col )
843
914
else :
@@ -851,13 +922,14 @@ def __init__(self, ax, *args, **kwargs):
851
922
zip (self .levels , tlinewidths , tlinestyles , self .allsegs ):
852
923
# Default zorder taken from LineCollection
853
924
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 )
861
933
col .set_label ('_nolegend_' )
862
934
self .ax .add_collection (col , False )
863
935
self .collections .append (col )
@@ -902,29 +974,27 @@ def legend_elements(self, variable_name='x', str_format=str):
902
974
n_levels = len (self .collections )
903
975
904
976
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 ))
928
998
else :
929
999
for collection , level in zip (self .collections , self .levels ):
930
1000
@@ -963,7 +1033,7 @@ def _process_args(self, *args, **kwargs):
963
1033
964
1034
# Check length of allkinds.
965
1035
if (self .allkinds is not None and
966
- len (self .allkinds ) != len (self .allsegs )):
1036
+ len (self .allkinds ) != len (self .allsegs )):
967
1037
raise ValueError ('allkinds has different length to allsegs' )
968
1038
969
1039
# Determine x,y bounds and update axes data limits.
@@ -1032,7 +1102,7 @@ def changed(self):
1032
1102
cm .ScalarMappable .changed (self )
1033
1103
1034
1104
def _autolev (self , z , N ):
1035
- '''
1105
+ """
1036
1106
Select contour levels to span the data.
1037
1107
1038
1108
We need two more levels for filled contours than for
@@ -1041,7 +1111,7 @@ def _autolev(self, z, N):
1041
1111
a single contour boundary, say at z = 0, requires only
1042
1112
one contour line, but two filled regions, and therefore
1043
1113
three levels to provide boundaries for both regions.
1044
- '''
1114
+ """
1045
1115
if self .locator is None :
1046
1116
if self .logscale :
1047
1117
self .locator = ticker .LogLocator ()
@@ -1210,11 +1280,11 @@ def _process_linestyles(self):
1210
1280
return tlinestyles
1211
1281
1212
1282
def get_alpha (self ):
1213
- ''' returns alpha to be applied to all ContourSet artists'''
1283
+ """ returns alpha to be applied to all ContourSet artists"""
1214
1284
return self .alpha
1215
1285
1216
1286
def set_alpha (self , alpha ):
1217
- ''' sets alpha for all ContourSet artists'''
1287
+ """ sets alpha for all ContourSet artists"""
1218
1288
self .alpha = alpha
1219
1289
self .changed ()
1220
1290
@@ -1256,32 +1326,33 @@ def find_nearest_contour(self, x, y, indices=None, pixel=True):
1256
1326
if indices is None :
1257
1327
indices = range (len (self .levels ))
1258
1328
1259
- dmin = 1e10
1329
+ dmin = np . inf
1260
1330
conmin = None
1261
1331
segmin = None
1262
1332
xmin = None
1263
1333
ymin = None
1264
1334
1335
+ point = np .array ([x , y ])
1336
+
1265
1337
for icon in indices :
1266
1338
con = self .collections [icon ]
1267
1339
trans = con .get_transform ()
1268
1340
paths = con .get_paths ()
1341
+
1269
1342
for segNum , linepath in enumerate (paths ):
1270
1343
lc = linepath .vertices
1271
-
1272
1344
# transfer all data points to screen coordinates if desired
1273
1345
if pixel :
1274
1346
lc = trans .transform (lc )
1275
1347
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 )
1278
1349
if d < dmin :
1279
1350
dmin = d
1280
1351
conmin = icon
1281
1352
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 ]
1285
1356
1286
1357
return (conmin , segmin , imin , xmin , ymin , dmin )
1287
1358
@@ -1340,7 +1411,7 @@ def _process_args(self, *args, **kwargs):
1340
1411
# if the transform is not trans data, and some part of it
1341
1412
# contains transData, transform the xs and ys to data coordinates
1342
1413
if (t != self .ax .transData and
1343
- any (t .contains_branch_seperately (self .ax .transData ))):
1414
+ any (t .contains_branch_seperately (self .ax .transData ))):
1344
1415
trans_to_data = t - self .ax .transData
1345
1416
pts = (np .vstack ([x .flat , y .flat ]).T )
1346
1417
transformed_pts = trans_to_data .transform (pts )
@@ -1408,14 +1479,14 @@ def _contour_args(self, args, kwargs):
1408
1479
return (x , y , z )
1409
1480
1410
1481
def _check_xyz (self , args , kwargs ):
1411
- '''
1482
+ """
1412
1483
For functions like contour, check that the dimensions
1413
1484
of the input arrays match; if x and y are 1D, convert
1414
1485
them to 2D using meshgrid.
1415
1486
1416
1487
Possible change: I think we should make and use an ArgumentError
1417
1488
Exception class (here and elsewhere).
1418
- '''
1489
+ """
1419
1490
x , y = args [:2 ]
1420
1491
self .ax ._process_unit_info (xdata = x , ydata = y , kwargs = kwargs )
1421
1492
x = self .ax .convert_xunits (x )
@@ -1450,11 +1521,11 @@ def _check_xyz(self, args, kwargs):
1450
1521
1451
1522
if x .shape != z .shape :
1452
1523
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 ))
1454
1525
1455
1526
if y .shape != z .shape :
1456
1527
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 ))
1458
1529
1459
1530
else :
1460
1531
@@ -1463,7 +1534,7 @@ def _check_xyz(self, args, kwargs):
1463
1534
return x , y , z
1464
1535
1465
1536
def _initialize_x_y (self , z ):
1466
- '''
1537
+ """
1467
1538
Return X, Y arrays such that contour(Z) will match imshow(Z)
1468
1539
if origin is not None.
1469
1540
The center of pixel Z[i,j] depends on origin:
@@ -1474,7 +1545,7 @@ def _initialize_x_y(self, z):
1474
1545
as in imshow.
1475
1546
If origin is None and extent is not None, then extent
1476
1547
will give the minimum and maximum values of x and y.
1477
- '''
1548
+ """
1478
1549
if z .ndim != 2 :
1479
1550
raise TypeError ("Input must be a 2D array." )
1480
1551
else :
0 commit comments