From: <dmk...@us...> - 2008-07-24 08:50:47
|
Revision: 5830 http://matplotlib.svn.sourceforge.net/matplotlib/?rev=5830&view=rev Author: dmkaplan Date: 2008-07-24 08:50:43 +0000 (Thu, 24 Jul 2008) Log Message: ----------- Committing rewrite of clabel inlining and label rotation code. Also, renaming ContourLabeler attributes to something like .labelAttribute. A few of the old attribute names have been maintained (in addition to new versions) for backward compatibility, but these should be remove in +1 releases. Added appropriate comments to CHANGELOG and API_CHANGES. Modified Paths: -------------- trunk/matplotlib/API_CHANGES trunk/matplotlib/CHANGELOG trunk/matplotlib/lib/matplotlib/blocking_input.py trunk/matplotlib/lib/matplotlib/contour.py Modified: trunk/matplotlib/API_CHANGES =================================================================== --- trunk/matplotlib/API_CHANGES 2008-07-24 08:06:45 UTC (rev 5829) +++ trunk/matplotlib/API_CHANGES 2008-07-24 08:50:43 UTC (rev 5830) @@ -8,6 +8,12 @@ Axes.patch, Axes.axesFrame renamed Axes.frame, Axes.get_frame, which returns Axes.patch, is deprecated. Examples and users guide updated +* Changes in the ContourLabeler attributes (clabel function) so that they + all have a form like .labelAttribute. The three attributes that are most + likely to be used by end users, .cl, .cl_xy and .cl_cvalues have been + maintained for the moment (in addition to their renamed versions), but they + are depricated and will eventually be removed. + Changes for 0.98.1 ================== Modified: trunk/matplotlib/CHANGELOG =================================================================== --- trunk/matplotlib/CHANGELOG 2008-07-24 08:06:45 UTC (rev 5829) +++ trunk/matplotlib/CHANGELOG 2008-07-24 08:50:43 UTC (rev 5830) @@ -1,3 +1,6 @@ +2008-07-24 Rewrite of a significant portion of the clabel code (class + ContourLabeler) to improve inlining. - DMK + 2008-07-22 Added Barbs polygon collection (similar to Quiver) for plotting wind barbs. Added corresponding helpers to Axes and pyplot as well. (examples/pylab_examples/barb_demo.py shows it off.) - RMM @@ -4,7 +7,7 @@ 2008-07-21 Added scikits.delaunay as matplotlib.delaunay. Added griddata function in matplotlib.mlab, with example (griddata_demo.py) in - pylab_examples. griddata function will use mpl_toolkits._natgrid + pylab_examples. griddata function will use mpl_toolkits._natgrid if installed (haven't yet created the toolkit). - JSW 2008-07-21 Re-introduced offset_copy that works in the context of the Modified: trunk/matplotlib/lib/matplotlib/blocking_input.py =================================================================== --- trunk/matplotlib/lib/matplotlib/blocking_input.py 2008-07-24 08:06:45 UTC (rev 5829) +++ trunk/matplotlib/lib/matplotlib/blocking_input.py 2008-07-24 08:50:43 UTC (rev 5830) @@ -19,8 +19,9 @@ import time import numpy as np + from matplotlib import path, verbose -from cbook import is_sequence_of_strings +from matplotlib.cbook import is_sequence_of_strings class BlockingInput(object): """ @@ -267,47 +268,39 @@ if event.inaxes == cs.ax: conmin,segmin,imin,xmin,ymin = cs.find_nearest_contour( - event.x, event.y, cs.label_indices)[:5] + event.x, event.y, cs.labelIndiceList)[:5] # Get index of nearest level in subset of levels used for labeling - lmin = cs.label_indices.index(conmin) + lmin = cs.labelIndiceList.index(conmin) + # Coordinates of contour paths = cs.collections[conmin].get_paths() lc = paths[segmin].vertices - # Figure out label rotation. This is very cludgy. - # Ideally, there would be one method in ContourLabeler - # that would figure out the best rotation for a label at a - # point, but the way automatic label rotation is done is - # quite mysterious to me and doesn't seem easy to - # generalize to non-automatic label placement. The method - # used below is not very robust! It basically looks one - # point before and one point after label location on - # contour and takes mean of angles of two vectors formed. - # This produces "acceptable" results, but not nearly as - # nice as automatic method. - ll = lc[max(0,imin-1):imin+2] # Get points around point - dd = np.diff(ll,axis=0) - rotation = np.mean( np.arctan2(dd[:,1], dd[:,0]) ) * 180 / np.pi - if rotation > 90: - rotation = rotation -180 - if rotation < -90: - rotation = 180 + rotation + # In pixel/screen space + slc = cs.ax.transData.transform(lc) - cs.add_label(xmin,ymin,rotation,cs.label_levels[lmin], - cs.label_cvalues[lmin]) + # Get label width for rotating labels and breaking contours + lw = cs.get_label_width(cs.labelLevelList[lmin], + cs.labelFmt, cs.labelFontSizeList[lmin]) + # Figure out label rotation. + rotation,nlc = cs.calc_label_rot_and_inline( + slc, imin, lw, lc if self.inline else [], + self.inline_spacing ) + + cs.add_label(xmin,ymin,rotation,cs.labelLevelList[lmin], + cs.labelCValueList[lmin]) + if self.inline: - # Get label width for breaking contours - lw = cs.get_label_width(cs.label_levels[lmin], - cs.fmt, cs.fslist[lmin]) - # Break contour - new=cs.break_linecontour(lc,rotation,lw,imin) - if len(new[0]): - paths[segmin] = path.Path(new[0]) - if len(new[1]): - paths.extend([path.Path(new[1])]) + # Remove old, not looping over paths so we can do this up front + paths.pop(segmin) + # Add paths if not empty or single point + for n in nlc: + if len(n)>1: + paths.append( path.Path(n) ) + self.fig.canvas.draw() else: # Remove event if not valid BlockingInput.pop(self) @@ -320,14 +313,21 @@ broken contour - once humpty-dumpty is broken, he can't be put back together. In inline mode, this does nothing. """ + # Remove this last event - not too important for clabel use + # since clabel normally doesn't have a maximum number of + # events, but best for cleanliness sake. + BlockingInput.pop(self) + if self.inline: pass else: self.cs.pop_label() self.cs.ax.figure.canvas.draw() - def __call__(self,inline,n=-1,timeout=-1): + def __call__(self,inline,inline_spacing=5,n=-1,timeout=-1): self.inline=inline + self.inline_spacing=inline_spacing + BlockingMouseInput.__call__(self,n=n,timeout=timeout, show_clicks=False) Modified: trunk/matplotlib/lib/matplotlib/contour.py =================================================================== --- trunk/matplotlib/lib/matplotlib/contour.py 2008-07-24 08:06:45 UTC (rev 5829) +++ trunk/matplotlib/lib/matplotlib/contour.py 2008-07-24 08:50:43 UTC (rev 5830) @@ -69,6 +69,12 @@ controls whether the underlying contour is removed or not. Default is *True*. + *inline_spacing*: + space in pixels to leave on each side of label when + placing inline. Defaults to 5. This spacing will be + exact for labels at locations where the contour is + straight, less so for labels on curved contours. + *fmt*: a format string for the label. Default is '%1.3f' Alternatively, this can be a dictionary matching contour @@ -108,11 +114,12 @@ fontsize = kwargs.get('fontsize', None) inline = kwargs.get('inline', 1) - self.fmt = kwargs.get('fmt', '%1.3f') + inline_spacing = kwargs.get('inline_spacing', 5) + self.labelFmt = kwargs.get('fmt', '%1.3f') _colors = kwargs.get('colors', None) # Detect if manual selection is desired and remove from argument list - self.manual_select=kwargs.get('manual',False) + self.labelManual=kwargs.get('manual',False) if len(args) == 0: levels = self.levels @@ -131,52 +138,59 @@ raise ValueError(msg) else: raise TypeError("Illegal arguments to clabel, see help(clabel)") - self.label_levels = levels - self.label_indices = indices + self.labelLevelList = levels + self.labelIndiceList = indices - self.fp = font_manager.FontProperties() + self.labelFontProps = font_manager.FontProperties() if fontsize == None: - font_size = int(self.fp.get_size_in_points()) + font_size = int(self.labelFontProps.get_size_in_points()) else: if type(fontsize) not in [int, float, str]: raise TypeError("Font size must be an integer number.") # Can't it be floating point, as indicated in line above? else: if type(fontsize) == str: - font_size = int(self.fp.get_size_in_points()) + font_size = int(self.labelFontProps.get_size_in_points()) else: - self.fp.set_size(fontsize) + self.labelFontProps.set_size(fontsize) font_size = fontsize - self.fslist = [font_size] * len(levels) + self.labelFontSizeList = [font_size] * len(levels) if _colors == None: - self.label_mappable = self - self.label_cvalues = np.take(self.cvalues, self.label_indices) + self.labelMappable = self + self.labelCValueList = np.take(self.cvalues, self.labelIndiceList) else: - cmap = colors.ListedColormap(_colors, N=len(self.label_levels)) - self.label_cvalues = range(len(self.label_levels)) - self.label_mappable = cm.ScalarMappable(cmap = cmap, - norm = colors.NoNorm()) + cmap = colors.ListedColormap(_colors, N=len(self.labelLevelList)) + self.labelCValueList = range(len(self.labelLevelList)) + self.labelMappable = cm.ScalarMappable(cmap = cmap, + norm = colors.NoNorm()) - #self.cl = [] # Initialized in ContourSet.__init__ - #self.cl_cvalues = [] # same - self.cl_xy = [] + #self.labelTexts = [] # Initialized in ContourSet.__init__ + #self.labelCValues = [] # same + self.labelXYs = [] - if self.manual_select: + if self.labelManual: print 'Select label locations manually using first mouse button.' print 'End manual selection with second mouse button.' if not inline: print 'Remove last label by clicking third mouse button.' blocking_contour_labeler = BlockingContourLabeler(self) - blocking_contour_labeler(inline) + blocking_contour_labeler(inline,inline_spacing) else: - self.labels(inline) + self.labels(inline,inline_spacing) - self.label_list = cbook.silent_list('text.Text', self.cl) - return self.label_list + # Hold on to some old attribute names. These are depricated and will + # be moved in the near future (sometime after 2008-08-01), but keeping + # for now for backwards compatibility + self.cl = self.labelTexts + self.cl_xy = self.labelXYs + self.cl_cvalues = self.labelCValues + self.labelTextsList = cbook.silent_list('text.Text', self.labelTexts) + return self.labelTextsList + def print_label(self, linecontour,labelwidth): "if contours are too short, don't plot a label" lcsize = len(linecontour) @@ -196,9 +210,9 @@ def too_close(self, x,y, lw): "if there's a label already nearby, find a better place" - if self.cl_xy != []: + if self.labelXYs != []: dist = [np.sqrt((x-loc[0]) ** 2 + (y-loc[1]) ** 2) - for loc in self.cl_xy] + for loc in self.labelXYs] for d in dist: if d < 1.2*lw: return 1 @@ -237,12 +251,35 @@ return lw + def get_real_label_width( self, lev, fmt, fsize ): + """ + This computes actual onscreen label width. + This uses some black magic to determine onscreen extent of non-drawn + label. This magic may not be very robust. + """ + # Find middle of axes + xx = np.mean( np.asarray(self.ax.axis()).reshape(2,2), axis=1 ) + # Temporarily create text object + t = text.Text( xx[0], xx[1] ) + self.set_label_props( t, self.get_text(lev,fmt), 'k' ) + + # Some black magic to get onscreen extent + # NOTE: This will only work for already drawn figures, as the canvas + # does not have a renderer otherwise. This is the reason this function + # can't be integrated into the rest of the code. + bbox = t.get_window_extent(renderer=self.ax.figure.canvas.renderer) + + # difference in pixel extent of image + lw = np.diff(bbox.corners()[0::2,0])[0] + + return lw + def set_label_props(self, label, text, color): "set the label properties - color, fontsize, text" label.set_text(text) label.set_color(color) - label.set_fontproperties(self.fp) + label.set_fontproperties(self.labelFontProps) label.set_clip_box(self.ax.bbox) def get_text(self, lev, fmt): @@ -255,85 +292,6 @@ else: return fmt%lev - def break_linecontour(self, linecontour, rot, labelwidth, ind): - "break a contour in two contours at the location of the label" - lcsize = len(linecontour) - hlw = int(labelwidth/2) - - #length of label in screen coords - ylabel = abs(hlw * np.sin(rot*np.pi/180)) - xlabel = abs(hlw * np.cos(rot*np.pi/180)) - - trans = self.ax.transData - - slc = trans.transform(linecontour) - x,y = slc[ind] - xx=slc[:,0].copy() - yy=slc[:,1].copy() - - #indices which are under the label - inds, = np.nonzero(((xx < x+xlabel) & (xx > x-xlabel)) & - ((yy < y+ylabel) & (yy > y-ylabel))) - - if len(inds) >0: - #if the label happens to be over the beginning of the - #contour, the entire contour is removed, i.e. - #indices to be removed are - #inds= [0,1,2,3,305,306,307] - #should rewrite this in a better way - linds, = np.nonzero(inds[1:]- inds[:-1] != 1) - if inds[0] == 0 and len(linds) != 0: - ii = inds[linds[0]] - lc1 =linecontour[ii+1:inds[ii+1]] - lc2 = [] - - else: - lc1=linecontour[:inds[0]] - lc2= linecontour[inds[-1]+1:] - - else: - lc1=linecontour[:ind] - lc2 = linecontour[ind+1:] - - - if rot <0: - new_x1, new_y1 = x-xlabel, y+ylabel - new_x2, new_y2 = x+xlabel, y-ylabel - else: - new_x1, new_y1 = x-xlabel, y-ylabel - new_x2, new_y2 = x+xlabel, y+ylabel - - inverse = trans.inverted() - new_x1d, new_y1d = inverse.transform_point((new_x1, new_y1)) - new_x2d, new_y2d = inverse.transform_point((new_x2, new_y2)) - new_xy1 = np.array(((new_x1d, new_y1d),)) - new_xy2 = np.array(((new_x2d, new_y2d),)) - - - if rot > 0: - if (len(lc1) > 0 and (lc1[-1][0] <= new_x1d) - and (lc1[-1][1] <= new_y1d)): - lc1 = np.concatenate((lc1, new_xy1)) - #lc1.append((new_x1d, new_y1d)) - - if (len(lc2) > 0 and (lc2[0][0] >= new_x2d) - and (lc2[0][1] >= new_y2d)): - lc2 = np.concatenate((new_xy2, lc2)) - #lc2.insert(0, (new_x2d, new_y2d)) - else: - if (len(lc1) > 0 and ((lc1[-1][0] <= new_x1d) - and (lc1[-1][1] >= new_y1d))): - lc1 = np.concatenate((lc1, new_xy1)) - #lc1.append((new_x1d, new_y1d)) - - if (len(lc2) > 0 and ((lc2[0][0] >= new_x2d) - and (lc2[0][1] <= new_y2d))): - lc2 = np.concatenate((new_xy2, lc2)) - #lc2.insert(0, (new_x2d, new_y2d)) - - return [lc1,lc2] - - def locate_label(self, linecontour, labelwidth): """find a good place to plot a label (relatively flat part of the contour) and the angle of rotation for the @@ -362,12 +320,6 @@ dist = np.add.reduce(([(abs(s)[i]/L[i]) for i in range(xsize)]),-1) x,y,ind = self.get_label_coords(dist, XX, YY, ysize, labelwidth) #print 'ind, x, y', ind, x, y - angle = np.arctan2(ylast - yfirst, xlast - xfirst).ravel() - rotation = angle[ind]*180/np.pi - if rotation > 90: - rotation = rotation -180 - if rotation < -90: - rotation = 180 + rotation # There must be a more efficient way... lc = [tuple(l) for l in linecontour] @@ -375,72 +327,181 @@ #print 'dind', dind #dind = list(linecontour).index((x,y)) - return x,y, rotation, dind + return x, y, dind + def calc_label_rot_and_inline( self, slc, ind, lw, lc=[], spacing=5 ): + """ + This function calculates the appropriate label rotation given + the linecontour coordinates in screen units, the index of the + label location and the label width. + + It will also break contour and calculate inlining if *lc* is + not empty. *spacing* is the space around the label in pixels + to leave empty. + + Do both of these tasks at once to avoid calling cbook.path_length + multiple times, which is relatively costly. + + The method used here involves calculating the path length + along the contour in pixel coordinates and then looking + approximately label width / 2 away from central point to + determine rotation and then to break contour if desired. + """ + + # Half the label width + hlw = lw/2.0 + + # Check if closed and, if so, rotate contour so label is at edge + closed = cbook.is_closed_polygon(slc) + if closed: + slc = np.r_[ slc[ind:-1], slc[:ind+1] ] + + if len(lc): # Rotate lc also if not empty + lc = np.r_[ lc[ind:-1], lc[:ind+1] ] + + ind = 0 + + # Path length in pixel space + pl = cbook.path_length(slc) + pl = pl-pl[ind] + + # Use linear interpolation to get points around label + xi = np.array( [ -hlw, hlw ] ) + if closed: # Look at end also for closed contours + dp = np.array([pl[-1],0]) + else: + dp = np.zeros_like(xi) + + ll = cbook.less_simple_linear_interpolation( pl, slc, dp+xi, + extrap=True ) + + # get vector in pixel space coordinates from one point to other + dd = np.diff( ll, axis=0 ).ravel() + + # Get angle of vector - must be calculated in pixel space for + # text rotation to work correctly + if np.all(dd==0): # Must deal with case of zero length label + rotation = 0.0 + else: + rotation = np.arctan2(dd[1], dd[0]) * 180.0 / np.pi + + # Fix angle so text is never upside-down + if rotation > 90: + rotation = rotation - 180.0 + if rotation < -90: + rotation = 180.0 + rotation + + # Break contour if desired + nlc = [] + if len(lc): + # Expand range by spacing + xi = dp + xi + np.array([-spacing,spacing]) + + # Get indices near points of interest + I = cbook.less_simple_linear_interpolation( + pl, np.arange(len(pl)), xi, extrap=False ) + + # If those indices aren't beyond contour edge, find x,y + if (not np.isnan(I[0])) and int(I[0])<>I[0]: + xy1 = cbook.less_simple_linear_interpolation( + pl, lc, [ xi[0] ] ) + + if (not np.isnan(I[1])) and int(I[1])<>I[1]: + xy2 = cbook.less_simple_linear_interpolation( + pl, lc, [ xi[1] ] ) + + # Make integer + I = [ np.floor(I[0]), np.ceil(I[1]) ] + + # Actually break contours + if closed: + # This will remove contour if shorter than label + if np.all(~np.isnan(I)): + nlc.append( np.r_[ xy2, lc[I[1]:I[0]+1], xy1 ] ) + else: + # These will remove pieces of contour if they have length zero + if not np.isnan(I[0]): + nlc.append( np.r_[ lc[:I[0]+1], xy1 ] ) + if not np.isnan(I[1]): + nlc.append( np.r_[ xy2, lc[I[1]:] ] ) + + # The current implementation removes contours completely + # covered by labels. Uncomment line below to keep + # original contour if this is the preferred behavoir. + #if not len(nlc): nlc = [ lc ] + + return (rotation,nlc) + + def add_label(self,x,y,rotation,lev,cvalue): dx,dy = self.ax.transData.inverted().transform_point((x,y)) t = text.Text(dx, dy, rotation = rotation, horizontalalignment='center', verticalalignment='center') - color = self.label_mappable.to_rgba(cvalue,alpha=self.alpha) + color = self.labelMappable.to_rgba(cvalue,alpha=self.alpha) - _text = self.get_text(lev,self.fmt) + _text = self.get_text(lev,self.labelFmt) self.set_label_props(t, _text, color) - self.cl.append(t) - self.cl_cvalues.append(cvalue) - self.cl_xy.append((x,y)) + self.labelTexts.append(t) + self.labelCValues.append(cvalue) + self.labelXYs.append((x,y)) # Add label to plot here - useful for manual mode label selection self.ax.add_artist(t) def pop_label(self,index=-1): '''Defaults to removing last label, but any index can be supplied''' - self.cl_cvalues.pop(index) - t = self.cl.pop(index) + self.labelCValues.pop(index) + t = self.labelTexts.pop(index) t.remove() - def labels(self, inline): + def labels(self, inline, inline_spacing): trans = self.ax.transData # A bit of shorthand for icon, lev, fsize, cvalue in zip( - self.label_indices, self.label_levels, self.fslist, - self.label_cvalues ): + self.labelIndiceList, self.labelLevelList, self.labelFontSizeList, + self.labelCValueList ): con = self.collections[icon] - lw = self.get_label_width(lev, self.fmt, fsize) + lw = self.get_label_width(lev, self.labelFmt, fsize) additions = [] paths = con.get_paths() for segNum, linepath in enumerate(paths): - linecontour = linepath.vertices - # for closed contours add one more point to - # avoid division by zero - if np.all(linecontour[0] == linecontour[-1]): - linecontour = np.concatenate((linecontour, - linecontour[1][np.newaxis,:])) - #linecontour.append(linecontour[1]) - # transfer all data points to screen coordinates - slc = trans.transform(linecontour) + lc = linepath.vertices # Line contour + slc0 = trans.transform(lc) # Line contour in screen coords + + # For closed polygons, add extra point to avoid division by + # zero in print_label and locate_label. Other than these + # functions, this is not necessary and should probably be + # eventually removed. + if cbook.is_closed_polygon( lc ): + slc = np.r_[ slc0, slc0[1:2,:] ] + else: + slc = slc0 + if self.print_label(slc,lw): - x,y, rotation, ind = self.locate_label(slc, lw) + x,y,ind = self.locate_label(slc, lw) + rotation,new=self.calc_label_rot_and_inline( + slc0, ind, lw, lc if inline else [], + inline_spacing ) + # Actually add the label self.add_label(x,y,rotation,lev,cvalue) - # Use break_linecontour to split contours for inlining + # If inline, add new contours if inline: - new = self.break_linecontour(linecontour, rotation, - lw, ind) - if len(new[0]): - paths[segNum] = path.Path(new[0]) - if len(new[1]): - additions.append(path.Path(new[1])) + for n in new: + # Add path if not empty or single point + if len(n)>1: additions.append( path.Path(n) ) - # After looping over all segments on a contour, append - # new paths to existing - paths.extend(additions) + # After looping over all segments on a contour, remove old + # paths and add new ones if inlining + if inline: + del paths[:] + paths.extend(additions) - class ContourSet(cm.ScalarMappable, ContourLabeler): """ Create and store a set of contour lines or filled regions. @@ -512,8 +573,8 @@ else: self.collections = cbook.silent_list('collections.LineCollection') # label lists must be initialized here - self.cl = [] - self.cl_cvalues = [] + self.labelTexts = [] + self.labelCValues = [] kw = {'cmap': cmap} if norm is not None: @@ -574,9 +635,9 @@ for color, collection in zip(tcolors, self.collections): collection.set_alpha(self.alpha) collection.set_color(color) - for label, cv in zip(self.cl, self.cl_cvalues): + for label, cv in zip(self.labelTexts, self.labelCValues): label.set_alpha(self.alpha) - label.set_color(self.label_mappable.to_rgba(cv)) + label.set_color(self.labelMappable.to_rgba(cv)) # add label colors cm.ScalarMappable.changed(self) This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |