Robert Hetland wrote:
Let me know if you would like to do a quick alpha test before you
commit. I'll help to put it through the paces..
-Rob.
Rob,
Thanks. Attached are a diff against svn and a test script to get you
started. If you apply the diff as a patch, you should be able to call
quiver2 from the pylab interface. Docstrings are provided. I made no
attempt to copy the kwarg part of the API from the old quiver; this one
is just too different. Most of the arg part of the API is the same,
except that the optional third or fifth argument is a mappable array
instead of a scale; I made the scale a kwarg, and it operates completely
differently from the one in the old quiver--but for good reason, I think.
The key idea for the scale is that the "units" kwarg establishes a unit
of measure ("arrow unit") for the arrows, and the scale gives the U,V
data units per arrow unit. For example, if units="inches" and you have
a 1 m/s velocity vector, then setting scale=2 will mean 2 m/s per inch,
and the vector will be half an inch (assuming the figure dpi value is
correct), regardless of how you change the window size or zoom. If
units="x", and your x-axis goes from 0 to 50 km, then you might set
scale=1/5.0 so that 1 m/s corresponds to 5 km along the x-axis; if you
zoom in, the vector will grow. If units="width" (present default, though
maybe not a good one), then the unit is the width of the plot, so you
might make scale=50, so that 1 m/s makes an arrow 1/50th the width of
the plot. Change the window width, and the arrow length changes along
with it. Zoom, and it does not change, however. In all cases, the
arrow direction remains constant, regardless of window or view limit
manipulations. (This is all because of John's transform magic--it is a
little hard to understand at first, but it certainly provides wonderful
functionality.)
There is a simple auto-scaling algorithm, so one does not have to
specify the scale kwarg initially.
Once quiver.py is in place, I will add related functionality such as
ellipses, so you can plot mean velocities and standard error ellipses,
for example.
One thing I have not looked into yet is what it will take to do all this
correctly with polar axes and with basemap, which I will need.
Eric
Index: lib/matplotlib/quiver.py
===================================================================
--- lib/matplotlib/quiver.py (revision 0)
+++ lib/matplotlib/quiver.py (revision 0)
@@ -0,0 +1,285 @@
+'''
+Support for plotting fields of arrows.
+
+Presently this contains a single class, Quiver, but it
+might make sense to consolidate other arrow plotting here.
+
+This will also become a home for things such as standard
+deviation ellipses, which can and will be derived very easily from
+the Quiver code.
+'''
+
+
+_quiver_doc = '''
+Plot a 2-D field of arrows.
+
+Function signatures:
+
+ quiver(U, V, *kw)
+ quiver(U, V, C, *kw)
+ quiver(X, Y, U, V, *kw)
+ quiver(X, Y, U, V, C, *kw)
+
+Arguments:
+
+ X, Y give the x and y coordinates of the arrow origins
+ U, V give the x and y components of the arrow vectors
+ C is an optional array used to map colors to the arrows
+
+ All arguments may be 1-D or 2-D arrays or sequences.
+ If X and Y are absent, they will be generated as a uniform grid.
+ If U and V are 2-D arrays but X and Y are 1-D, and if
+ len(X) and len(Y) match the column and row dimensions
+ of U, then X and Y will be expanded with meshgrid.
+
+Keyword arguments (default given first):
+
+ * units = 'width' | 'height' | 'dots' | 'inches' | 'x' | 'y'
+ arrow units; the arrow dimensions *except for length*
+ are in multiples of this unit.
+ * scale = None | float
+ data units per arrow unit, e.g. m/s per plot width;
+ a smaller scale parameter makes the arrow longer.
+
+ Arrow dimensions and scales can be in any of several units:
+
+ 'width' or 'height': the width or height of the axes
+ 'dots' or 'inches': pixels or inches, based on the figure dpi
+ 'x' or 'y': X or Y data units
+
+ In all cases the arrow aspect ratio is 1, so that if U==V the angle
+ of the arrow on the plot is 45 degrees CCW from the X-axis.
+
+ The arrows scale differently depending on the units, however.
+ For 'x' or 'y', the arrows get larger as one zooms in; for other
+ units, the arrow size is independent of the zoom state. For
+ 'width or 'height', the arrow size increases with the width and
+ height of the axes, respectively, when the the window is resized;
+ for 'dots' or 'inches', resizing does not change the arrows.
+
+
+ * width = 0.005 shaft width in arrow units; default depends on
+ choice of units, above, but is equivalent to
+ 0.005 times the width of the plot.
+ * headwidth = 3 head width as multiple of shaft width
+ * headlength = 5 head length as multiple of shaft width
+ * headaxislength = 4.5 head length at shaft intersection
+ * minshaft = 1 length below which arrow scales, in units
+ of head length. Do not set this to less
+ than 1, or small arrows will look terrible!
+
+ The defaults give a slightly swept-back arrow; to make the
+ head a triangle, make headaxislength the same as headlength.
+ To make the arrow more pointed, reduce headwidth or increase
+ headlength and headaxislength.
+ To make the head smaller relative to the shaft, scale down
+ all the head* parameters.
+ You will probably do best to leave minshaft alone.
+
+ * pivot = 'tail' | 'middle' | 'tip'
+ The part of the arrow that is at the grid point; the arrow
+ rotates about this point, hence the name 'pivot'.
+
+ * C = None | array, the same size as U, V. Setting this is the
+ same as using one of the signatures including C.
+ * color = 'k' | any matplotlib color spec or sequence of color specs.
+ This is a synonym for the PolyCollection facecolor kwarg.
+ If C has been set, either via arg or kwarg, it has no effect.
+
+ * All PolyCollection kwargs are valid, in the sense that they
+ will be passed on to the PolyCollection constructor.
+ In particular, one might want to use, for example:
+ linewidths = (1,), edgecolors = ('g')
+ to make the arrows have green outlines of unit width.
+
+
+'''
+
+
+from matplotlib.collections import PolyCollection
+from matplotlib.mlab import meshgrid
+from matplotlib import numerix as nx
+from matplotlib import transforms as T
+import math
+
+
+
+class Quiver(PolyCollection):
+ '''
+ Specialized PolyCollection for arrows.
+
+ The only API method is set_UVC(), which can be used
+ to change the size, orientation, and color of the
+ arrows; their locations are fixed when the class is
+ instantiated. Possibly this method will be useful
+ in animations.
+
+ Much of the work in this class is done in the draw()
+ method so that as much information as possible is available
+ about the plot. In subsequent draw() calls, recalculation
+ is limited to things that might have changed, so there
+ should be no performance penalty from putting the calculations
+ in the draw() method.
+ '''
+ def __init__(self, ax, *args, **kw):
+ self.ax = ax
+ X, Y, U, V, C = self._parse_args(*args)
+ self.X = X
+ self.Y = Y
+ self.scale = kw.pop('scale', None)
+ self.headwidth = kw.pop('headwidth', 3)
+ self.headlength = float(kw.pop('headlength', 5))
+ self.headaxislength = kw.pop('headaxislength', 4.5)
+ self.minshaft = kw.pop('minshaft', 1)
+ self.colorarray = kw.pop('C', None)
+ self.units = kw.pop('units', 'width')
+ self.width = kw.pop('width', None)
+ self.color = kw.pop('color', 'k')
+ self.pivot = kw.pop('pivot', 'tail')
+ kw.setdefault('facecolors', self.color)
+ kw.setdefault('linewidths', (0,))
+ PolyCollection.__init__(self, None, offsets=zip(X, Y),
+ transOffset=ax.transData, **kw)
+ self.set_UVC(U, V, C)
+ if self.colorarray is not None:
+ self.set_array(nx.ravel(self.colorarray))
+ #print 'set_array called in __init__'
+ self._initialized = False
+
+ __init__.__doc__ = '''
+ The constructor takes one required argument, an Axes
+ instance, followed by the args and kwargs described
+ by the following pylab interface documentation:
+ %s''' % _quiver_doc
+
+ def _parse_args(self, *args):
+ X, Y, U, V, C = [None]*5
+ args = list(args)
+ if len(args) == 3 or len(args) == 5:
+ C = nx.ravel(args.pop(-1))
+ #print 'in parse_args, C:', C
+ V = nx.asarray(args.pop(-1))
+ U = nx.asarray(args.pop(-1))
+ nn = nx.shape(U)
+ nc = nn[0]
+ nr = 1
+ if len(nn) > 1:
+ nr = nn[1]
+ if len(args) == 2:
+ X, Y = [nx.ravel(a) for a in args]
+ if len(X) == nc and len(Y) == nr:
+ X, Y = [nx.ravel(a) for a in meshgrid(X, Y)]
+ else:
+ X, Y = [nx.ravel(a) for a in meshgrid(nx.arange(nc), nx.arange(nr))]
+ return X, Y, U, V, C
+
+ def _init(self):
+ '''initialization delayed until first draw;
+ allow time for axes setup.
+ '''
+ if not self._initialized:
+ self._set_transform()
+
+ def draw(self, renderer):
+ self._init()
+ if self._new_UV:
+ verts = self._make_verts(self.U, self.V)
+ self.set_verts(verts)
+ self._new_UV = False
+ PolyCollection.draw(self, renderer)
+
+ def set_UVC(self, U, V, C=None):
+ self.U = nx.ravel(U)
+ self.V = nx.ravel(V)
+ if C is not None:
+ self.set_array(nx.ravel(C))
+ #print 'set_array called by set_UVC'
+ self._new_UV = True
+
+ def _set_transform(self):
+ ax = self.ax
+ if self.units in ('x', 'y'):
+ if self.units == 'x':
+ dx0 = ax.viewLim.ur().x() - ax.viewLim.ll().x()
+ dx1 = ax.bbox.ur().x() - ax.bbox.ll().x()
+ else:
+ dx0 = ax.viewLim.ur().y() - ax.viewLim.ll().y()
+ dx1 = ax.bbox.ur().y() - ax.bbox.ll().y()
+ dx = dx1/dx0
+ else:
+ if self.units == 'width':
+ dx = ax.bbox.ur().x() - ax.bbox.ll().x()
+ elif self.units == 'height':
+ dx = ax.bbox.ur().y() - ax.bbox.ll().y()
+ elif self.units == 'dots':
+ dx = T.Value(1)
+ elif self.units == 'inches':
+ dpi = ax.figure.dpi.get()
+ dx = T.Value(dpi)
+ else:
+ raise ValueError('unrecognized units')
+ bb = T.Bbox(T.origin(), T.Point(dx, dx))
+ trans = T.get_bbox_transform(T.unit_bbox(), bb)
+ self.set_transform(trans)
+ sx, sy = trans.inverse_xy_tup((ax.bbox.width(), ax.bbox.height()))
+ #print 'sx, sy', sx, sy
+ self.span = sx
+ if self.width is None:
+ self.width = 0.005 * self.span
+
+ def _make_verts(self, U, V):
+ uv = U+V*1j
+ a = nx.absolute(uv)
+ if self.scale is None:
+ scale = nx.amax(a) * math.sqrt(len(a)) # crude auto-scaling
+ scale = scale/self.span
+ self.scale = scale # So we can see what autoscaling does.
+ else:
+ scale = self.scale
+ #print 'scale, self.width', scale, self.width
+ #print 'original length', a
+ length = a/(scale*self.width)
+ #print 'scaled length', length
+ X, Y = self._h_arrows(length)
+ xy = (X+Y*1j) * nx.exp(1j*nx.angle(uv[...,nx.newaxis]))*self.width
+ return [zip(xyrow.real, xyrow.imag) for xyrow in xy]
+
+
+ def _h_arrows(self, length):
+ ''' length is in arrow width units '''
+ minsh = self.minshaft * self.headlength
+ N = len(length)
+ length = nx.reshape(length, (N,1))
+ x = nx.array([0, -self.headaxislength,
+ -self.headlength, 0], nx.Float32)
+ x = x + nx.array([0,1,1,1]) * length
+ y = 0.5 * nx.array([1, 1, self.headwidth, 0], nx.Float32)
+ y = nx.repeat(y[nx.NewAxis,:], N)
+ x0 = nx.array([0, minsh-self.headaxislength,
+ minsh-self.headlength, minsh], nx.Float32)
+ y0 = 0.5 * nx.array([1, 1, self.headwidth, 0], nx.Float32)
+ ii = [0,1,2,3,2,1,0]
+ X = nx.take(x, ii, 1)
+ Y = nx.take(y, ii, 1)
+ Y[:, 3:] *= -1
+ X0 = nx.take(x0, ii)
+ Y0 = nx.take(y0, ii)
+ Y0[3:] *= -1
+ shrink = length/minsh
+ X0 = shrink * X0[nx.NewAxis,:]
+ Y0 = shrink * Y0[nx.NewAxis,:]
+ short = nx.repeat(length < minsh, 7, 1)
+ #print 'short', length < minsh
+ X = nx.where(short, X0, X)
+ Y = nx.where(short, Y0, Y)
+ if self.pivot[:3] == 'mid':
+ X -= 0.5 * X[:,3, nx.NewAxis]
+ elif self.pivot[:3] == 'tip':
+ X = X - X[:,3, nx.NewAxis] #numpy bug? using -= does not
+ # work here unless we multiply
+ # by a float first, as with 'mid'.
+ return X, Y
+
+ quiver_doc = _quiver_doc
+
+
Index: lib/matplotlib/axes.py
===================================================================
--- lib/matplotlib/axes.py (revision 2428)
+++ lib/matplotlib/axes.py (working copy)
@@ -43,6 +43,8 @@
from transforms import PBox
from font_manager import FontProperties
+from quiver import Quiver
+
import matplotlib
if matplotlib._havedate:
@@ -3199,6 +3201,13 @@
self.add_artist(a)
return a
+ def quiver2(self, *args, **kw):
+ q = Quiver(self, *args, **kw)
+ self.add_collection(q)
+ self.update_datalim_numerix(q.X, q.Y)
+ self.autoscale_view()
+ return q
+ quiver2.__doc__ = Quiver.quiver_doc
def quiver(self, U, V, *args, **kwargs ):
"""
Index: lib/matplotlib/pylab.py
===================================================================
--- lib/matplotlib/pylab.py (revision 2428)
+++ lib/matplotlib/pylab.py (working copy)
@@ -1514,7 +1514,7 @@
draw_if_interactive()
### The following functions were autogenerated by the boilerplate.py
-### script. They are simple wrappers around the Axes methods of the
+### script. They are simple wrappers around Axes methods.
# This function was autogenerated by boilerplate.py. Do not edit as
# changes will be lost
@@ -2274,6 +2274,27 @@
# This function was autogenerated by boilerplate.py. Do not edit as
# changes will be lost
+def quiver2(*args, **kwargs):
+ # allow callers to override the hold state by passing hold=True|False
+ b = ishold()
+ h = popd(kwargs, 'hold', None)
+ if h is not None:
+ hold(h)
+ try:
+ ret = gca().quiver2(*args, **kwargs)
+ draw_if_interactive()
+ except:
+ hold(b)
+ raise
+ gci._current = ret
+ hold(b)
+ return ret
+if Axes.quiver2.__doc__ is not None:
+ quiver2.__doc__ = _shift_string(Axes.quiver2.__doc__) + """
+Addition kwargs: hold = [True|False] overrides default hold state"""
+
+# This function was autogenerated by boilerplate.py. Do not edit as
+# changes will be lost
def cla(*args, **kwargs):
ret = gca().cla(*args, **kwargs)
@@ -2504,3 +2525,4 @@
draw_if_interactive()
+
Index: lib/matplotlib/collections.py
===================================================================
--- lib/matplotlib/collections.py (revision 2428)
+++ lib/matplotlib/collections.py (working copy)
@@ -237,6 +237,10 @@
PatchCollection.__init__(self,**kwargs)
self._verts = verts
+ def set_verts(self, verts):
+ '''This allows one to delay initialization of the vertices.'''
+ self._verts = verts
+
def draw(self, renderer):
if not self.get_visible(): return
renderer.open_group('polycollection')
Index: boilerplate.py
===================================================================
--- boilerplate.py (revision 2428)
+++ boilerplate.py (working copy)
@@ -83,6 +83,7 @@
'stem',
'vlines',
'quiver',
+ 'quiver2',
)
_misccommands = (
@@ -101,6 +102,7 @@
'pcolormesh' : 'gci._current = ret',
'imshow' : 'gci._current = ret',
'spy2' : 'gci._current = ret',
+ 'quiver2' : 'gci._current = ret',
'specgram' : 'gci._current = ret[-1]',
}
'''
Demonstration of quiver2 function.
Warning: API and plotting methods may change.
Known problem: the plot autoscaling does not take into account
the arrows, so those on the boundaries are often out of the picture.
This is *not* an easy problem to solve in a perfectly general way.
'''
from pylab import *
X,Y = meshgrid( arange(0,2*pi,.2),arange(0,2*pi,.2) )
U = cos(X)
V = sin(Y)
figure()
quiver2( U, V)
title('Minimal arguments, no kwargs')
figure()
quiver2( X, Y, U, V, units='width')
title('scales with plot width, not view')
figure()
quiver2( X[::3, ::3], Y[::3, ::3], U[::3, ::3], V[::3, ::3],
pivot='mid', color='r', units='inches' )
plot( X[::3, ::3], Y[::3, ::3], 'k.')
title("pivot='mid'; every third arrow; units='inches'")
figure()
quiver2( X, Y, U, V, C=U+V, units='x', pivot='tip')
plot(X, Y, 'k.')
title("scales with x view; pivot='tip'")
figure()
quiver2( X[::3, ::3], Y[::3, ::3], U[::3, ::3], V[::3, ::3],
color='r', units='x',
linewidths=(2,), edgecolors=('k'), headaxislength=5 )
title("triangular head; scale with x view; black edges")
show()
_______________________________________________
Matplotlib-devel mailing list
Matplotlib-devel@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/matplotlib-devel