From: <dmk...@us...> - 2008-07-21 12:29:44
|
Revision: 5799 http://matplotlib.svn.sourceforge.net/matplotlib/?rev=5799&view=rev Author: dmkaplan Date: 2008-07-21 12:26:35 +0000 (Mon, 21 Jul 2008) Log Message: ----------- Recommitting my changes to clabel to allow for manual labeling after fixing problems with indexing identified by Eric Firing on 19 July 2008. In addition, I have made the following changes: 1) Placed more comments in contour.py with the intention of eventually doing a rewrite or restructuring of that code. 2) Added two pylab_examples: contour_label_demo.py that tests out some of the more advanced things that can now be done with contour labeling, and ginput_manual_clabel.py that demonstrates some uses of ginput, waitforbuttonpress, and clabel(...,manual=True). The first of these has been integrated into backend_driver.py, but the second cannot be because it requires interaction. Modified Paths: -------------- trunk/matplotlib/examples/tests/backend_driver.py trunk/matplotlib/lib/matplotlib/blocking_input.py trunk/matplotlib/lib/matplotlib/contour.py Added Paths: ----------- trunk/matplotlib/examples/pylab_examples/contour_label_demo.py trunk/matplotlib/examples/pylab_examples/ginput_manual_clabel.py Added: trunk/matplotlib/examples/pylab_examples/contour_label_demo.py =================================================================== --- trunk/matplotlib/examples/pylab_examples/contour_label_demo.py (rev 0) +++ trunk/matplotlib/examples/pylab_examples/contour_label_demo.py 2008-07-21 12:26:35 UTC (rev 5799) @@ -0,0 +1,74 @@ +#!/usr/bin/env python +""" +Illustrate some of the more advanced things that one can do with +contour labels. + +See also contour_demo.py. +""" +import matplotlib +import numpy as np +import matplotlib.cm as cm +import matplotlib.mlab as mlab +import matplotlib.pyplot as plt + +matplotlib.rcParams['xtick.direction'] = 'out' +matplotlib.rcParams['ytick.direction'] = 'out' + +################################################## +# Define our surface +################################################## +delta = 0.025 +x = np.arange(-3.0, 3.0, delta) +y = np.arange(-2.0, 2.0, delta) +X, Y = np.meshgrid(x, y) +Z1 = mlab.bivariate_normal(X, Y, 1.0, 1.0, 0.0, 0.0) +Z2 = mlab.bivariate_normal(X, Y, 1.5, 0.5, 1, 1) +# difference of Gaussians +Z = 10.0 * (Z2 - Z1) + +################################################## +# Make contour labels using creative float classes +# Follows suggestion of Manuel Metz +################################################## +plt.figure() + +# Basic contour plot +CS = plt.contour(X, Y, Z) + +# Define a class that forces representation of float to look a certain way +# This remove trailing zero so '1.0' becomes '1' +class nf(float): + def __repr__(self): + str = '%.1f' % (self.__float__(),) + if str[-1]=='0': + return '%.0f' % self.__float__() + else: + return '%.1f' % self.__float__() + +# Recast levels to new class +CS.levels = [nf(val) for val in CS.levels ] + +# Label levels with specially formatted floats +plt.clabel(CS, CS.levels, inline=True, fmt='%r %%', fontsize=10) + +################################################## +# Label contours with arbitrary strings using a +# dictionary +################################################## +plt.figure() + +# Basic contour plot +CS = plt.contour(X, Y, Z) + +fmt = {} +strs = [ 'first', 'second', 'third', 'fourth', 'fifth', 'sixth', 'seventh' ] +for l,s in zip( CS.levels, strs ): + fmt[l] = s + +# Label every other level using strings +plt.clabel(CS,CS.levels[::2],inline=True,fmt=fmt,fontsize=10) + +################################################## +# Show the hole thing +################################################## +plt.show() Added: trunk/matplotlib/examples/pylab_examples/ginput_manual_clabel.py =================================================================== --- trunk/matplotlib/examples/pylab_examples/ginput_manual_clabel.py (rev 0) +++ trunk/matplotlib/examples/pylab_examples/ginput_manual_clabel.py 2008-07-21 12:26:35 UTC (rev 5799) @@ -0,0 +1,88 @@ +#!/usr/bin/env python +""" +This provides examples of uses of interactive functions, such as ginput, +waitforbuttonpress and manual clabel placement. + +This script must be run interactively using a backend that has a +graphical user interface (for example, from inside ipython using +GTKAgg backend, but not PS backend). +""" +import time +import matplotlib +import numpy as np +import matplotlib.cm as cm +import matplotlib.mlab as mlab +import matplotlib.pyplot as plt + +def tellme(s): + print s + plt.title(s,fontsize=16) + plt.draw() + +################################################## +# Define a triangle by clicking three points +################################################## +plt.clf() +plt.axis([-1.,1.,-1.,1.]) +plt.setp(plt.gca(),autoscale_on=False) + +tellme('You will define a triangle, click to begin') + +plt.waitforbuttonpress() + +happy = False +while not happy: + pts = [] + while len(pts) < 3: + tellme('Select 3 corners with mouse') + pts = np.asarray( plt.ginput(3,timeout=-1) ) + if len(pts) < 3: + tellme('Too few points, starting over') + time.sleep(1) # Wait a second + + ph = plt.fill( pts[:,0], pts[:,1], 'r', lw=2 ) + + tellme('Happy? Key click for yes, mouse click for no') + + happy = plt.waitforbuttonpress() + + # Get rid of fill + if not happy: + for p in ph: p.remove() + +################################################## +# Now contour according to distance from triangle +# corners - just an example +################################################## + +# Define a nice function of distance from individual pts +def f(x,y,pts): + z = np.zeros_like(x) + for p in pts: + z = z + 1/(np.sqrt((x-p[0])**2+(y-p[1])**2)) + return 1/z + +X,Y = np.meshgrid( np.linspace(-1,1,51), np.linspace(-1,1,51) ) +Z = f(X,Y,pts) + +CS = plt.contour( X, Y, Z, 20 ) + +tellme( 'Use mouse to select contour label locations, middle button to finish' ) +CL = plt.clabel( CS, manual=True ) + +################################################## +# Now do a zoom +################################################## +tellme( 'Now do a nested zoom, click to begin' ) +plt.waitforbuttonpress() + +happy = False +while not happy: + tellme( 'Select two corners of zoom, middle mouse button to finish' ) + pts = np.asarray( plt.ginput(2,timeout=-1) ) + + happy = len(pts) < 2 + if happy: break + + pts = np.sort(pts,axis=0) + plt.axis( pts.T.ravel() ) Modified: trunk/matplotlib/examples/tests/backend_driver.py =================================================================== --- trunk/matplotlib/examples/tests/backend_driver.py 2008-07-21 09:50:36 UTC (rev 5798) +++ trunk/matplotlib/examples/tests/backend_driver.py 2008-07-21 12:26:35 UTC (rev 5799) @@ -39,6 +39,7 @@ 'color_demo.py', 'cohere_demo.py', 'contour_demo.py', + 'contour_label_demo.py', 'contourf_demo.py', 'csd_demo.py', 'custom_ticker1.py', Modified: trunk/matplotlib/lib/matplotlib/blocking_input.py =================================================================== --- trunk/matplotlib/lib/matplotlib/blocking_input.py 2008-07-21 09:50:36 UTC (rev 5798) +++ trunk/matplotlib/lib/matplotlib/blocking_input.py 2008-07-21 12:26:35 UTC (rev 5799) @@ -5,11 +5,11 @@ creates a callable object to retrieve events in a blocking way for interactive sessions :class:`BlockingKeyMouseInput` - creates a callable object to retrieve key or mouse clicks in a blocking way for interactive sessions. + creates a callable object to retrieve key or mouse clicks in a blocking way for interactive sessions. Note: Subclass of BlockingInput. Used by waitforbuttonpress :class:`BlockingMouseInput` - creates a callable object to retrieve mouse clicks in a blocking way for interactive sessions. + creates a callable object to retrieve mouse clicks in a blocking way for interactive sessions. Note: Subclass of BlockingInput. Used by ginput :class:`BlockingContourLabeler` @@ -46,7 +46,7 @@ # This will extract info from events self.post_event() - + # Check if we have enough events already if len(self.events) >= self.n and self.n > 0: self.done = True @@ -84,7 +84,7 @@ """ Blocking call to retrieve n events """ - + assert isinstance(n, int), "Requires an integer argument" self.n = n @@ -94,7 +94,7 @@ # Ensure that the figure is shown self.fig.show() - + # connect the events to the on_event function call for n in self.eventslist: self.callbacks.append( self.fig.canvas.mpl_connect(n, self.on_event) ) @@ -124,7 +124,7 @@ blocking way. """ def __init__(self, fig): - BlockingInput.__init__(self, fig=fig, + BlockingInput.__init__(self, fig=fig, eventslist=('button_press_event',) ) def post_event(self): @@ -194,7 +194,7 @@ """ self.clicks.append((event.xdata,event.ydata)) - verbose.report("input %i: %f,%f" % + verbose.report("input %i: %f,%f" % (len(self.clicks),event.xdata, event.ydata)) # If desired plot up click @@ -209,7 +209,7 @@ removing the last click. """ self.clicks.pop(index) - + if self.show_clicks: mark = self.marks.pop(index) mark.remove() @@ -234,7 +234,7 @@ # Call base class to remove callbacks BlockingInput.cleanup(self) - + def __call__(self, n=1, timeout=30, show_clicks=True): """ Blocking call to retrieve n coordinate pairs through mouse @@ -261,11 +261,18 @@ This will be called if an event involving a button other than 2 or 3 occcurs. This will add a label to a contour. """ - if event.inaxes == self.cs.ax: - conmin,segmin,imin,xmin,ymin = self.cs.find_nearest_contour( - event.x, event.y)[:5] - paths = self.cs.collections[conmin].get_paths() + # Shorthand + cs = self.cs + + if event.inaxes == cs.ax: + conmin,segmin,imin,xmin,ymin = cs.find_nearest_contour( + event.x, event.y, cs.label_indices)[:5] + + # Get index of nearest level in subset of levels used for labeling + lmin = cs.label_indices.index(conmin) + + paths = cs.collections[conmin].get_paths() lc = paths[segmin].vertices # Figure out label rotation. This is very cludgy. @@ -287,15 +294,15 @@ if rotation < -90: rotation = 180 + rotation - self.cs.add_label(xmin,ymin,rotation,conmin) + cs.add_label(xmin,ymin,rotation,cs.label_levels[lmin], + cs.label_cvalues[lmin]) if self.inline: # Get label width for breaking contours - lw = self.cs.get_label_width(self.cs.label_levels[conmin], - self.cs.fmt, - self.cs.fslist[conmin]) + lw = cs.get_label_width(cs.label_levels[lmin], + cs.fmt, cs.fslist[lmin]) # Break contour - new=self.cs.break_linecontour(lc,rotation,lw,imin) + new=cs.break_linecontour(lc,rotation,lw,imin) if len(new[0]): paths[segmin] = path.Path(new[0]) if len(new[1]): @@ -304,7 +311,7 @@ self.fig.canvas.draw() else: # Remove event if not valid BlockingInput.pop(self) - + def button3(self,event): """ This will be called if button 3 is clicked. This will remove Modified: trunk/matplotlib/lib/matplotlib/contour.py =================================================================== --- trunk/matplotlib/lib/matplotlib/contour.py 2008-07-21 09:50:36 UTC (rev 5798) +++ trunk/matplotlib/lib/matplotlib/contour.py 2008-07-21 12:26:35 UTC (rev 5799) @@ -17,6 +17,9 @@ import matplotlib.text as text import matplotlib.cbook as cbook +# Import needed for adding manual selection capability to clabel +from matplotlib.blocking_input import BlockingContourLabeler + # We can't use a single line collection for contour because a line # collection can have only a single line style, and we want to be able to have # dashed negative contours, for example, and solid positive contours. @@ -68,16 +71,49 @@ *fmt*: a format string for the label. Default is '%1.3f' + Alternatively, this can be a dictionary matching contour + levels with arbitrary strings to use for each contour level + (i.e., fmt[level]=string) + *manual*: + if *True*, contour labels will be placed manually using + mouse clicks. Click the first button near a contour to + add a label, click the second button (or potentially both + mouse buttons at once) to finish adding labels. The third + button can be used to remove the last label added, but + only if labels are not inline. """ + + """" + NOTES on how this all works: + + clabel basically takes the input arguments and uses them to + add a list of "label specific" attributes to the ContourSet + object. These attributes currently include: label_indices, + label_levels, label_cvalues, fp (font properties), fslist + (fontsize list), label_mappable, cl (list of text objects of + labels), cl_xy (coordinates of labels), cl_cvalues (color + values of the actual labels). + + Note that these property names do not conform to the standards + set for coding matplotlib and I (DMK) eventually plan on + changing them so that they are clearer and conform to + standards. + + Once these attributes are set, clabel passes control to the + labels method (case of automatic label placement) or + BlockingContourLabeler (case of manual label placement. + """ + fontsize = kwargs.get('fontsize', None) inline = kwargs.get('inline', 1) self.fmt = 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) - if len(args) == 0: levels = self.levels indices = range(len(self.levels)) @@ -126,10 +162,16 @@ #self.cl_cvalues = [] # same self.cl_xy = [] - self.labels(inline) + if self.manual_select: + 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.' - for label in self.cl: - self.ax.add_artist(label) + blocking_contour_labeler = BlockingContourLabeler(self) + blocking_contour_labeler(inline) + else: + self.labels(inline) self.label_list = cbook.silent_list('text.Text', self.cl) return self.label_list @@ -141,10 +183,10 @@ if lcsize > 10 * labelwidth: return 1 - xmax = np.amax(np.array(linecontour)[:,0]) - xmin = np.amin(np.array(linecontour)[:,0]) - ymax = np.amax(np.array(linecontour)[:,1]) - ymin = np.amin(np.array(linecontour)[:,1]) + xmax = np.amax(linecontour[:,0]) + xmin = np.amin(linecontour[:,0]) + ymax = np.amax(linecontour[:,1]) + ymin = np.amin(linecontour[:,1]) lw = labelwidth if (xmax - xmin) > 1.2* lw or (ymax - ymin) > 1.2 * lw: @@ -180,12 +222,10 @@ if self.too_close(x,y, lw): continue else: - self.cl_xy.append((x,y)) return x,y, ind ind = adist[0] x, y = XX[ind][hysize], YY[ind][hysize] - self.cl_xy.append((x,y)) return x,y, ind def get_label_width(self, lev, fmt, fsize): @@ -193,7 +233,7 @@ if cbook.is_string_like(lev): lw = (len(lev)) * fsize else: - lw = (len(fmt%lev)) * fsize + lw = (len(self.get_text(lev,fmt))) * fsize return lw @@ -210,9 +250,11 @@ if cbook.is_string_like(lev): return lev else: - return fmt%lev + if isinstance(fmt,dict): + return fmt[lev] + 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) @@ -226,8 +268,8 @@ slc = trans.transform(linecontour) x,y = slc[ind] - xx= np.asarray(slc)[:,0].copy() - yy=np.asarray(slc)[:,1].copy() + xx=slc[:,0].copy() + yy=slc[:,1].copy() #indices which are under the label inds, = np.nonzero(((xx < x+xlabel) & (xx > x-xlabel)) & @@ -308,8 +350,8 @@ else: ysize = labelwidth - XX = np.resize(np.asarray(linecontour)[:,0],(xsize, ysize)) - YY = np.resize(np.asarray(linecontour)[:,1],(xsize, ysize)) + XX = np.resize(linecontour[:,0],(xsize, ysize)) + YY = np.resize(linecontour[:,1],(xsize, ysize)) #I might have fouled up the following: yfirst = YY[:,0].reshape(xsize, 1) ylast = YY[:,-1].reshape(xsize, 1) @@ -335,19 +377,38 @@ return x,y, rotation, dind + 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) + + _text = self.get_text(lev,self.fmt) + self.set_label_props(t, _text, color) + self.cl.append(t) + self.cl_cvalues.append(cvalue) + self.cl_xy.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) + t.remove() + def labels(self, inline): - levels = self.label_levels - fslist = self.fslist - trans = self.ax.transData - _colors = self.label_mappable.to_rgba(self.label_cvalues, - alpha=self.alpha) - fmt = self.fmt - for icon, lev, color, cvalue, fsize in zip(self.label_indices, - self.label_levels, - _colors, - self.label_cvalues, fslist): + 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 ): + con = self.collections[icon] - lw = self.get_label_width(lev, fmt, fsize) + lw = self.get_label_width(lev, self.fmt, fsize) additions = [] paths = con.get_paths() for segNum, linepath in enumerate(paths): @@ -356,28 +417,27 @@ # avoid division by zero if np.all(linecontour[0] == linecontour[-1]): linecontour = np.concatenate((linecontour, - linecontour[1][np.newaxis,:])) + linecontour[1][np.newaxis,:])) #linecontour.append(linecontour[1]) # transfer all data points to screen coordinates slc = trans.transform(linecontour) if self.print_label(slc,lw): x,y, rotation, ind = self.locate_label(slc, lw) - # transfer the location of the label back to - # data coordinates - dx,dy = trans.inverted().transform_point((x,y)) - t = text.Text(dx, dy, rotation = rotation, - horizontalalignment='center', - verticalalignment='center') - _text = self.get_text(lev,fmt) - self.set_label_props(t, _text, color) - self.cl.append(t) - self.cl_cvalues.append(cvalue) + + # Actually add the label + self.add_label(x,y,rotation,lev,cvalue) + + # Use break_linecontour to split contours for inlining if inline: - new = self.break_linecontour(linecontour, rotation, lw, ind) + 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])) + + # After looping over all segments on a contour, append + # new paths to existing paths.extend(additions) @@ -802,19 +862,8 @@ Use keyword args to control colors, linewidth, origin, cmap ... see below for more details. - *X*, *Y*, and *Z* may be arrays all with the same 2-D shape, or - *X* and *Y* can be 1-D while *Z* is 2-D. In the latter - case, the following must be true: + *X*, *Y*, and *Z* must be arrays with the same dimensions. - :: - - Z.shape == len(Y), len(X) - - Note that the first index of *Z*, the row number, corresponds - to the vertical coordinate on the page, while the second - index, the column number, corresponds to the horizontal - coordinate on the page. - *Z* may be a masked array, but filled contouring may not handle internal masked regions correctly. @@ -908,3 +957,70 @@ .. plot:: contour_demo.py """ + + def find_nearest_contour( self, x, y, indices=None, pixel=True ): + """ + Finds contour that is closest to a point. Defaults to + measuring distance in pixels (screen space - useful for manual + contour labeling), but this can be controlled via a keyword + argument. + + Returns a tuple containing the contour, segment, index of + segment, x & y of segment point and distance to minimum point. + + Call signature:: + + conmin,segmin,imin,xmin,ymin,dmin = find_nearest_contour( + self, x, y, indices=None, pixel=True ) + + Optional keyword arguments:: + + *indices*: + Indexes of contour levels to consider when looking for + nearest point. Defaults to using all levels. + + *pixel*: + If *True*, measure distance in pixel space, if not, measure + distance in axes space. Defaults to *True*. + + """ + + # This function uses a method that is probably quite + # inefficient based on converting each contour segment to + # pixel coordinates and then comparing the given point to + # those coordinates for each contour. This will probably be + # quite slow for complex contours, but for normal use it works + # sufficiently well that the time is not noticeable. + # Nonetheless, improvements could probably be made. + + if indices==None: + indices = range(len(self.levels)) + + dmin = 1e10 + conmin = None + segmin = None + xmin = None + ymin = None + + for icon in indices: + con = self.collections[icon] + paths = con.get_paths() + for segNum, linepath in enumerate(paths): + lc = linepath.vertices + + # transfer all data points to screen coordinates if desired + if pixel: + lc = self.ax.transData.transform(lc) + + ds = (lc[:,0]-x)**2 + (lc[:,1]-y)**2 + d = min( ds ) + if d < dmin: + dmin = d + conmin = icon + segmin = segNum + imin = mpl.mlab.find( ds == d )[0] + xmin = lc[imin,0] + ymin = lc[imin,1] + + return (conmin,segmin,imin,xmin,ymin,dmin) + This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |