@@ -49,7 +49,7 @@ def get_rotation(self):
4949
5050
5151class 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+
677747class 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 :
0 commit comments