1"""
2Support for plotting vector fields.
3
4Presently this contains Quiver and Barb. Quiver plots an arrow in the
5direction of the vector, with the size of the arrow related to the
6magnitude of the vector.
7
8Barbs are like quiver in that they point along a vector, but
9the magnitude of the vector is given schematically by the presence of barbs
10or flags on the barb.
11
12This will also become a home for things such as standard
13deviation ellipses, which can and will be derived very easily from
14the Quiver code.
15"""
16
17import math
18
19import numpy as np
20from numpy import ma
21
22from matplotlib import _api, cbook, _docstring
23import matplotlib.artist as martist
24import matplotlib.collections as mcollections
25from matplotlib.patches import CirclePolygon
26import matplotlib.text as mtext
27import matplotlib.transforms as transforms
28
29
30_quiver_doc = """
31Plot a 2D field of arrows.
32
33Call signature::
34
35 quiver([X, Y], U, V, [C], **kwargs)
36
37*X*, *Y* define the arrow locations, *U*, *V* define the arrow directions, and
38*C* optionally sets the color.
39
40**Arrow length**
41
42The default settings auto-scales the length of the arrows to a reasonable size.
43To change this behavior see the *scale* and *scale_units* parameters.
44
45**Arrow shape**
46
47The arrow shape is determined by *width*, *headwidth*, *headlength* and
48*headaxislength*. See the notes below.
49
50**Arrow styling**
51
52Each arrow is internally represented by a filled polygon with a default edge
53linewidth of 0. As a result, an arrow is rather a filled area, not a line with
54a head, and `.PolyCollection` properties like *linewidth*, *edgecolor*,
55*facecolor*, etc. act accordingly.
56
57
58Parameters
59----------
60X, Y : 1D or 2D array-like, optional
61 The x and y coordinates of the arrow locations.
62
63 If not given, they will be generated as a uniform integer meshgrid based
64 on the dimensions of *U* and *V*.
65
66 If *X* and *Y* are 1D but *U*, *V* are 2D, *X*, *Y* are expanded to 2D
67 using ``X, Y = np.meshgrid(X, Y)``. In this case ``len(X)`` and ``len(Y)``
68 must match the column and row dimensions of *U* and *V*.
69
70U, V : 1D or 2D array-like
71 The x and y direction components of the arrow vectors. The interpretation
72 of these components (in data or in screen space) depends on *angles*.
73
74 *U* and *V* must have the same number of elements, matching the number of
75 arrow locations in *X*, *Y*. *U* and *V* may be masked. Locations masked
76 in any of *U*, *V*, and *C* will not be drawn.
77
78C : 1D or 2D array-like, optional
79 Numeric data that defines the arrow colors by colormapping via *norm* and
80 *cmap*.
81
82 This does not support explicit colors. If you want to set colors directly,
83 use *color* instead. The size of *C* must match the number of arrow
84 locations.
85
86angles : {'uv', 'xy'} or array-like, default: 'uv'
87 Method for determining the angle of the arrows.
88
89 - 'uv': Arrow direction in screen coordinates. Use this if the arrows
90 symbolize a quantity that is not based on *X*, *Y* data coordinates.
91
92 If *U* == *V* the orientation of the arrow on the plot is 45 degrees
93 counter-clockwise from the horizontal axis (positive to the right).
94
95 - 'xy': Arrow direction in data coordinates, i.e. the arrows point from
96 (x, y) to (x+u, y+v). Use this e.g. for plotting a gradient field.
97
98 - Arbitrary angles may be specified explicitly as an array of values
99 in degrees, counter-clockwise from the horizontal axis.
100
101 In this case *U*, *V* is only used to determine the length of the
102 arrows.
103
104 Note: inverting a data axis will correspondingly invert the
105 arrows only with ``angles='xy'``.
106
107pivot : {'tail', 'mid', 'middle', 'tip'}, default: 'tail'
108 The part of the arrow that is anchored to the *X*, *Y* grid. The arrow
109 rotates about this point.
110
111 'mid' is a synonym for 'middle'.
112
113scale : float, optional
114 Scales the length of the arrow inversely.
115
116 Number of data units per arrow length unit, e.g., m/s per plot width; a
117 smaller scale parameter makes the arrow longer. Default is *None*.
118
119 If *None*, a simple autoscaling algorithm is used, based on the average
120 vector length and the number of vectors. The arrow length unit is given by
121 the *scale_units* parameter.
122
123scale_units : {'width', 'height', 'dots', 'inches', 'x', 'y', 'xy'}, optional
124 If the *scale* kwarg is *None*, the arrow length unit. Default is *None*.
125
126 e.g. *scale_units* is 'inches', *scale* is 2.0, and ``(u, v) = (1, 0)``,
127 then the vector will be 0.5 inches long.
128
129 If *scale_units* is 'width' or 'height', then the vector will be half the
130 width/height of the axes.
131
132 If *scale_units* is 'x' then the vector will be 0.5 x-axis
133 units. To plot vectors in the x-y plane, with u and v having
134 the same units as x and y, use
135 ``angles='xy', scale_units='xy', scale=1``.
136
137units : {'width', 'height', 'dots', 'inches', 'x', 'y', 'xy'}, default: 'width'
138 Affects the arrow size (except for the length). In particular, the shaft
139 *width* is measured in multiples of this unit.
140
141 Supported values are:
142
143 - 'width', 'height': The width or height of the Axes.
144 - 'dots', 'inches': Pixels or inches based on the figure dpi.
145 - 'x', 'y', 'xy': *X*, *Y* or :math:`\\sqrt{X^2 + Y^2}` in data units.
146
147 The following table summarizes how these values affect the visible arrow
148 size under zooming and figure size changes:
149
150 ================= ================= ==================
151 units zoom figure size change
152 ================= ================= ==================
153 'x', 'y', 'xy' arrow size scales —
154 'width', 'height' — arrow size scales
155 'dots', 'inches' — —
156 ================= ================= ==================
157
158width : float, optional
159 Shaft width in arrow units. All head parameters are relative to *width*.
160
161 The default depends on choice of *units* above, and number of vectors;
162 a typical starting value is about 0.005 times the width of the plot.
163
164headwidth : float, default: 3
165 Head width as multiple of shaft *width*. See the notes below.
166
167headlength : float, default: 5
168 Head length as multiple of shaft *width*. See the notes below.
169
170headaxislength : float, default: 4.5
171 Head length at shaft intersection as multiple of shaft *width*.
172 See the notes below.
173
174minshaft : float, default: 1
175 Length below which arrow scales, in units of head length. Do not
176 set this to less than 1, or small arrows will look terrible!
177
178minlength : float, default: 1
179 Minimum length as a multiple of shaft width; if an arrow length
180 is less than this, plot a dot (hexagon) of this diameter instead.
181
182color : :mpltype:`color` or list :mpltype:`color`, optional
183 Explicit color(s) for the arrows. If *C* has been set, *color* has no
184 effect.
185
186 This is a synonym for the `.PolyCollection` *facecolor* parameter.
187
188Other Parameters
189----------------
190data : indexable object, optional
191 DATA_PARAMETER_PLACEHOLDER
192
193**kwargs : `~matplotlib.collections.PolyCollection` properties, optional
194 All other keyword arguments are passed on to `.PolyCollection`:
195
196 %(PolyCollection:kwdoc)s
197
198Returns
199-------
200`~matplotlib.quiver.Quiver`
201
202See Also
203--------
204.Axes.quiverkey : Add a key to a quiver plot.
205
206Notes
207-----
208
209**Arrow shape**
210
211The arrow is drawn as a polygon using the nodes as shown below. The values
212*headwidth*, *headlength*, and *headaxislength* are in units of *width*.
213
214.. image:: /_static/quiver_sizes.svg
215 :width: 500px
216
217The defaults give a slightly swept-back arrow. Here are some guidelines how to
218get other head shapes:
219
220- To make the head a triangle, make *headaxislength* the same as *headlength*.
221- To make the arrow more pointed, reduce *headwidth* or increase *headlength*
222 and *headaxislength*.
223- To make the head smaller relative to the shaft, scale down all the head
224 parameters proportionally.
225- To remove the head completely, set all *head* parameters to 0.
226- To get a diamond-shaped head, make *headaxislength* larger than *headlength*.
227- Warning: For *headaxislength* < (*headlength* / *headwidth*), the "headaxis"
228 nodes (i.e. the ones connecting the head with the shaft) will protrude out
229 of the head in forward direction so that the arrow head looks broken.
230""" % _docstring.interpd.params
231
232_docstring.interpd.update(quiver_doc=_quiver_doc)
233
234
235class QuiverKey(martist.Artist):
236 """Labelled arrow for use as a quiver plot scale key."""
237 halign = {'N': 'center', 'S': 'center', 'E': 'left', 'W': 'right'}
238 valign = {'N': 'bottom', 'S': 'top', 'E': 'center', 'W': 'center'}
239 pivot = {'N': 'middle', 'S': 'middle', 'E': 'tip', 'W': 'tail'}
240
241 def __init__(self, Q, X, Y, U, label,
242 *, angle=0, coordinates='axes', color=None, labelsep=0.1,
243 labelpos='N', labelcolor=None, fontproperties=None, **kwargs):
244 """
245 Add a key to a quiver plot.
246
247 The positioning of the key depends on *X*, *Y*, *coordinates*, and
248 *labelpos*. If *labelpos* is 'N' or 'S', *X*, *Y* give the position of
249 the middle of the key arrow. If *labelpos* is 'E', *X*, *Y* positions
250 the head, and if *labelpos* is 'W', *X*, *Y* positions the tail; in
251 either of these two cases, *X*, *Y* is somewhere in the middle of the
252 arrow+label key object.
253
254 Parameters
255 ----------
256 Q : `~matplotlib.quiver.Quiver`
257 A `.Quiver` object as returned by a call to `~.Axes.quiver()`.
258 X, Y : float
259 The location of the key.
260 U : float
261 The length of the key.
262 label : str
263 The key label (e.g., length and units of the key).
264 angle : float, default: 0
265 The angle of the key arrow, in degrees anti-clockwise from the
266 horizontal axis.
267 coordinates : {'axes', 'figure', 'data', 'inches'}, default: 'axes'
268 Coordinate system and units for *X*, *Y*: 'axes' and 'figure' are
269 normalized coordinate systems with (0, 0) in the lower left and
270 (1, 1) in the upper right; 'data' are the axes data coordinates
271 (used for the locations of the vectors in the quiver plot itself);
272 'inches' is position in the figure in inches, with (0, 0) at the
273 lower left corner.
274 color : :mpltype:`color`
275 Overrides face and edge colors from *Q*.
276 labelpos : {'N', 'S', 'E', 'W'}
277 Position the label above, below, to the right, to the left of the
278 arrow, respectively.
279 labelsep : float, default: 0.1
280 Distance in inches between the arrow and the label.
281 labelcolor : :mpltype:`color`, default: :rc:`text.color`
282 Label color.
283 fontproperties : dict, optional
284 A dictionary with keyword arguments accepted by the
285 `~matplotlib.font_manager.FontProperties` initializer:
286 *family*, *style*, *variant*, *size*, *weight*.
287 **kwargs
288 Any additional keyword arguments are used to override vector
289 properties taken from *Q*.
290 """
291 super().__init__()
292 self.Q = Q
293 self.X = X
294 self.Y = Y
295 self.U = U
296 self.angle = angle
297 self.coord = coordinates
298 self.color = color
299 self.label = label
300 self._labelsep_inches = labelsep
301
302 self.labelpos = labelpos
303 self.labelcolor = labelcolor
304 self.fontproperties = fontproperties or dict()
305 self.kw = kwargs
306 self.text = mtext.Text(
307 text=label,
308 horizontalalignment=self.halign[self.labelpos],
309 verticalalignment=self.valign[self.labelpos],
310 fontproperties=self.fontproperties)
311 if self.labelcolor is not None:
312 self.text.set_color(self.labelcolor)
313 self._dpi_at_last_init = None
314 self.zorder = Q.zorder + 0.1
315
316 @property
317 def labelsep(self):
318 return self._labelsep_inches * self.Q.axes.figure.dpi
319
320 def _init(self):
321 if True: # self._dpi_at_last_init != self.axes.figure.dpi
322 if self.Q._dpi_at_last_init != self.Q.axes.figure.dpi:
323 self.Q._init()
324 self._set_transform()
325 with cbook._setattr_cm(self.Q, pivot=self.pivot[self.labelpos],
326 # Hack: save and restore the Umask
327 Umask=ma.nomask):
328 u = self.U * np.cos(np.radians(self.angle))
329 v = self.U * np.sin(np.radians(self.angle))
330 self.verts = self.Q._make_verts([[0., 0.]],
331 np.array([u]), np.array([v]), 'uv')
332 kwargs = self.Q.polykw
333 kwargs.update(self.kw)
334 self.vector = mcollections.PolyCollection(
335 self.verts,
336 offsets=[(self.X, self.Y)],
337 offset_transform=self.get_transform(),
338 **kwargs)
339 if self.color is not None:
340 self.vector.set_color(self.color)
341 self.vector.set_transform(self.Q.get_transform())
342 self.vector.set_figure(self.get_figure())
343 self._dpi_at_last_init = self.Q.axes.figure.dpi
344
345 def _text_shift(self):
346 return {
347 "N": (0, +self.labelsep),
348 "S": (0, -self.labelsep),
349 "E": (+self.labelsep, 0),
350 "W": (-self.labelsep, 0),
351 }[self.labelpos]
352
353 @martist.allow_rasterization
354 def draw(self, renderer):
355 self._init()
356 self.vector.draw(renderer)
357 pos = self.get_transform().transform((self.X, self.Y))
358 self.text.set_position(pos + self._text_shift())
359 self.text.draw(renderer)
360 self.stale = False
361
362 def _set_transform(self):
363 self.set_transform(_api.check_getitem({
364 "data": self.Q.axes.transData,
365 "axes": self.Q.axes.transAxes,
366 "figure": self.Q.axes.figure.transFigure,
367 "inches": self.Q.axes.figure.dpi_scale_trans,
368 }, coordinates=self.coord))
369
370 def set_figure(self, fig):
371 super().set_figure(fig)
372 self.text.set_figure(fig)
373
374 def contains(self, mouseevent):
375 if self._different_canvas(mouseevent):
376 return False, {}
377 # Maybe the dictionary should allow one to
378 # distinguish between a text hit and a vector hit.
379 if (self.text.contains(mouseevent)[0] or
380 self.vector.contains(mouseevent)[0]):
381 return True, {}
382 return False, {}
383
384
385def _parse_args(*args, caller_name='function'):
386 """
387 Helper function to parse positional parameters for colored vector plots.
388
389 This is currently used for Quiver and Barbs.
390
391 Parameters
392 ----------
393 *args : list
394 list of 2-5 arguments. Depending on their number they are parsed to::
395
396 U, V
397 U, V, C
398 X, Y, U, V
399 X, Y, U, V, C
400
401 caller_name : str
402 Name of the calling method (used in error messages).
403 """
404 X = Y = C = None
405
406 nargs = len(args)
407 if nargs == 2:
408 # The use of atleast_1d allows for handling scalar arguments while also
409 # keeping masked arrays
410 U, V = np.atleast_1d(*args)
411 elif nargs == 3:
412 U, V, C = np.atleast_1d(*args)
413 elif nargs == 4:
414 X, Y, U, V = np.atleast_1d(*args)
415 elif nargs == 5:
416 X, Y, U, V, C = np.atleast_1d(*args)
417 else:
418 raise _api.nargs_error(caller_name, takes="from 2 to 5", given=nargs)
419
420 nr, nc = (1, U.shape[0]) if U.ndim == 1 else U.shape
421
422 if X is not None:
423 X = X.ravel()
424 Y = Y.ravel()
425 if len(X) == nc and len(Y) == nr:
426 X, Y = [a.ravel() for a in np.meshgrid(X, Y)]
427 elif len(X) != len(Y):
428 raise ValueError('X and Y must be the same size, but '
429 f'X.size is {X.size} and Y.size is {Y.size}.')
430 else:
431 indexgrid = np.meshgrid(np.arange(nc), np.arange(nr))
432 X, Y = [np.ravel(a) for a in indexgrid]
433 # Size validation for U, V, C is left to the set_UVC method.
434 return X, Y, U, V, C
435
436
437def _check_consistent_shapes(*arrays):
438 all_shapes = {a.shape for a in arrays}
439 if len(all_shapes) != 1:
440 raise ValueError('The shapes of the passed in arrays do not match')
441
442
443class Quiver(mcollections.PolyCollection):
444 """
445 Specialized PolyCollection for arrows.
446
447 The only API method is set_UVC(), which can be used
448 to change the size, orientation, and color of the
449 arrows; their locations are fixed when the class is
450 instantiated. Possibly this method will be useful
451 in animations.
452
453 Much of the work in this class is done in the draw()
454 method so that as much information as possible is available
455 about the plot. In subsequent draw() calls, recalculation
456 is limited to things that might have changed, so there
457 should be no performance penalty from putting the calculations
458 in the draw() method.
459 """
460
461 _PIVOT_VALS = ('tail', 'middle', 'tip')
462
463 @_docstring.Substitution(_quiver_doc)
464 def __init__(self, ax, *args,
465 scale=None, headwidth=3, headlength=5, headaxislength=4.5,
466 minshaft=1, minlength=1, units='width', scale_units=None,
467 angles='uv', width=None, color='k', pivot='tail', **kwargs):
468 """
469 The constructor takes one required argument, an Axes
470 instance, followed by the args and kwargs described
471 by the following pyplot interface documentation:
472 %s
473 """
474 self._axes = ax # The attr actually set by the Artist.axes property.
475 X, Y, U, V, C = _parse_args(*args, caller_name='quiver')
476 self.X = X
477 self.Y = Y
478 self.XY = np.column_stack((X, Y))
479 self.N = len(X)
480 self.scale = scale
481 self.headwidth = headwidth
482 self.headlength = float(headlength)
483 self.headaxislength = headaxislength
484 self.minshaft = minshaft
485 self.minlength = minlength
486 self.units = units
487 self.scale_units = scale_units
488 self.angles = angles
489 self.width = width
490
491 if pivot.lower() == 'mid':
492 pivot = 'middle'
493 self.pivot = pivot.lower()
494 _api.check_in_list(self._PIVOT_VALS, pivot=self.pivot)
495
496 self.transform = kwargs.pop('transform', ax.transData)
497 kwargs.setdefault('facecolors', color)
498 kwargs.setdefault('linewidths', (0,))
499 super().__init__([], offsets=self.XY, offset_transform=self.transform,
500 closed=False, **kwargs)
501 self.polykw = kwargs
502 self.set_UVC(U, V, C)
503 self._dpi_at_last_init = None
504
505 def _init(self):
506 """
507 Initialization delayed until first draw;
508 allow time for axes setup.
509 """
510 # It seems that there are not enough event notifications
511 # available to have this work on an as-needed basis at present.
512 if True: # self._dpi_at_last_init != self.axes.figure.dpi
513 trans = self._set_transform()
514 self.span = trans.inverted().transform_bbox(self.axes.bbox).width
515 if self.width is None:
516 sn = np.clip(math.sqrt(self.N), 8, 25)
517 self.width = 0.06 * self.span / sn
518
519 # _make_verts sets self.scale if not already specified
520 if (self._dpi_at_last_init != self.axes.figure.dpi
521 and self.scale is None):
522 self._make_verts(self.XY, self.U, self.V, self.angles)
523
524 self._dpi_at_last_init = self.axes.figure.dpi
525
526 def get_datalim(self, transData):
527 trans = self.get_transform()
528 offset_trf = self.get_offset_transform()
529 full_transform = (trans - transData) + (offset_trf - transData)
530 XY = full_transform.transform(self.XY)
531 bbox = transforms.Bbox.null()
532 bbox.update_from_data_xy(XY, ignore=True)
533 return bbox
534
535 @martist.allow_rasterization
536 def draw(self, renderer):
537 self._init()
538 verts = self._make_verts(self.XY, self.U, self.V, self.angles)
539 self.set_verts(verts, closed=False)
540 super().draw(renderer)
541 self.stale = False
542
543 def set_UVC(self, U, V, C=None):
544 # We need to ensure we have a copy, not a reference
545 # to an array that might change before draw().
546 U = ma.masked_invalid(U, copy=True).ravel()
547 V = ma.masked_invalid(V, copy=True).ravel()
548 if C is not None:
549 C = ma.masked_invalid(C, copy=True).ravel()
550 for name, var in zip(('U', 'V', 'C'), (U, V, C)):
551 if not (var is None or var.size == self.N or var.size == 1):
552 raise ValueError(f'Argument {name} has a size {var.size}'
553 f' which does not match {self.N},'
554 ' the number of arrow positions')
555
556 mask = ma.mask_or(U.mask, V.mask, copy=False, shrink=True)
557 if C is not None:
558 mask = ma.mask_or(mask, C.mask, copy=False, shrink=True)
559 if mask is ma.nomask:
560 C = C.filled()
561 else:
562 C = ma.array(C, mask=mask, copy=False)
563 self.U = U.filled(1)
564 self.V = V.filled(1)
565 self.Umask = mask
566 if C is not None:
567 self.set_array(C)
568 self.stale = True
569
570 def _dots_per_unit(self, units):
571 """Return a scale factor for converting from units to pixels."""
572 bb = self.axes.bbox
573 vl = self.axes.viewLim
574 return _api.check_getitem({
575 'x': bb.width / vl.width,
576 'y': bb.height / vl.height,
577 'xy': np.hypot(*bb.size) / np.hypot(*vl.size),
578 'width': bb.width,
579 'height': bb.height,
580 'dots': 1.,
581 'inches': self.axes.figure.dpi,
582 }, units=units)
583
584 def _set_transform(self):
585 """
586 Set the PolyCollection transform to go
587 from arrow width units to pixels.
588 """
589 dx = self._dots_per_unit(self.units)
590 self._trans_scale = dx # pixels per arrow width unit
591 trans = transforms.Affine2D().scale(dx)
592 self.set_transform(trans)
593 return trans
594
595 # Calculate angles and lengths for segment between (x, y), (x+u, y+v)
596 def _angles_lengths(self, XY, U, V, eps=1):
597 xy = self.axes.transData.transform(XY)
598 uv = np.column_stack((U, V))
599 xyp = self.axes.transData.transform(XY + eps * uv)
600 dxy = xyp - xy
601 angles = np.arctan2(dxy[:, 1], dxy[:, 0])
602 lengths = np.hypot(*dxy.T) / eps
603 return angles, lengths
604
605 # XY is stacked [X, Y].
606 # See quiver() doc for meaning of X, Y, U, V, angles.
607 def _make_verts(self, XY, U, V, angles):
608 uv = (U + V * 1j)
609 str_angles = angles if isinstance(angles, str) else ''
610 if str_angles == 'xy' and self.scale_units == 'xy':
611 # Here eps is 1 so that if we get U, V by diffing
612 # the X, Y arrays, the vectors will connect the
613 # points, regardless of the axis scaling (including log).
614 angles, lengths = self._angles_lengths(XY, U, V, eps=1)
615 elif str_angles == 'xy' or self.scale_units == 'xy':
616 # Calculate eps based on the extents of the plot
617 # so that we don't end up with roundoff error from
618 # adding a small number to a large.
619 eps = np.abs(self.axes.dataLim.extents).max() * 0.001
620 angles, lengths = self._angles_lengths(XY, U, V, eps=eps)
621
622 if str_angles and self.scale_units == 'xy':
623 a = lengths
624 else:
625 a = np.abs(uv)
626
627 if self.scale is None:
628 sn = max(10, math.sqrt(self.N))
629 if self.Umask is not ma.nomask:
630 amean = a[~self.Umask].mean()
631 else:
632 amean = a.mean()
633 # crude auto-scaling
634 # scale is typical arrow length as a multiple of the arrow width
635 scale = 1.8 * amean * sn / self.span
636
637 if self.scale_units is None:
638 if self.scale is None:
639 self.scale = scale
640 widthu_per_lenu = 1.0
641 else:
642 if self.scale_units == 'xy':
643 dx = 1
644 else:
645 dx = self._dots_per_unit(self.scale_units)
646 widthu_per_lenu = dx / self._trans_scale
647 if self.scale is None:
648 self.scale = scale * widthu_per_lenu
649 length = a * (widthu_per_lenu / (self.scale * self.width))
650 X, Y = self._h_arrows(length)
651 if str_angles == 'xy':
652 theta = angles
653 elif str_angles == 'uv':
654 theta = np.angle(uv)
655 else:
656 theta = ma.masked_invalid(np.deg2rad(angles)).filled(0)
657 theta = theta.reshape((-1, 1)) # for broadcasting
658 xy = (X + Y * 1j) * np.exp(1j * theta) * self.width
659 XY = np.stack((xy.real, xy.imag), axis=2)
660 if self.Umask is not ma.nomask:
661 XY = ma.array(XY)
662 XY[self.Umask] = ma.masked
663 # This might be handled more efficiently with nans, given
664 # that nans will end up in the paths anyway.
665
666 return XY
667
668 def _h_arrows(self, length):
669 """Length is in arrow width units."""
670 # It might be possible to streamline the code
671 # and speed it up a bit by using complex (x, y)
672 # instead of separate arrays; but any gain would be slight.
673 minsh = self.minshaft * self.headlength
674 N = len(length)
675 length = length.reshape(N, 1)
676 # This number is chosen based on when pixel values overflow in Agg
677 # causing rendering errors
678 # length = np.minimum(length, 2 ** 16)
679 np.clip(length, 0, 2 ** 16, out=length)
680 # x, y: normal horizontal arrow
681 x = np.array([0, -self.headaxislength,
682 -self.headlength, 0],
683 np.float64)
684 x = x + np.array([0, 1, 1, 1]) * length
685 y = 0.5 * np.array([1, 1, self.headwidth, 0], np.float64)
686 y = np.repeat(y[np.newaxis, :], N, axis=0)
687 # x0, y0: arrow without shaft, for short vectors
688 x0 = np.array([0, minsh - self.headaxislength,
689 minsh - self.headlength, minsh], np.float64)
690 y0 = 0.5 * np.array([1, 1, self.headwidth, 0], np.float64)
691 ii = [0, 1, 2, 3, 2, 1, 0, 0]
692 X = x[:, ii]
693 Y = y[:, ii]
694 Y[:, 3:-1] *= -1
695 X0 = x0[ii]
696 Y0 = y0[ii]
697 Y0[3:-1] *= -1
698 shrink = length / minsh if minsh != 0. else 0.
699 X0 = shrink * X0[np.newaxis, :]
700 Y0 = shrink * Y0[np.newaxis, :]
701 short = np.repeat(length < minsh, 8, axis=1)
702 # Now select X0, Y0 if short, otherwise X, Y
703 np.copyto(X, X0, where=short)
704 np.copyto(Y, Y0, where=short)
705 if self.pivot == 'middle':
706 X -= 0.5 * X[:, 3, np.newaxis]
707 elif self.pivot == 'tip':
708 # numpy bug? using -= does not work here unless we multiply by a
709 # float first, as with 'mid'.
710 X = X - X[:, 3, np.newaxis]
711 elif self.pivot != 'tail':
712 _api.check_in_list(["middle", "tip", "tail"], pivot=self.pivot)
713
714 tooshort = length < self.minlength
715 if tooshort.any():
716 # Use a heptagonal dot:
717 th = np.arange(0, 8, 1, np.float64) * (np.pi / 3.0)
718 x1 = np.cos(th) * self.minlength * 0.5
719 y1 = np.sin(th) * self.minlength * 0.5
720 X1 = np.repeat(x1[np.newaxis, :], N, axis=0)
721 Y1 = np.repeat(y1[np.newaxis, :], N, axis=0)
722 tooshort = np.repeat(tooshort, 8, 1)
723 np.copyto(X, X1, where=tooshort)
724 np.copyto(Y, Y1, where=tooshort)
725 # Mask handling is deferred to the caller, _make_verts.
726 return X, Y
727
728
729_barbs_doc = r"""
730Plot a 2D field of wind barbs.
731
732Call signature::
733
734 barbs([X, Y], U, V, [C], **kwargs)
735
736Where *X*, *Y* define the barb locations, *U*, *V* define the barb
737directions, and *C* optionally sets the color.
738
739All arguments may be 1D or 2D. *U*, *V*, *C* may be masked arrays, but masked
740*X*, *Y* are not supported at present.
741
742Barbs are traditionally used in meteorology as a way to plot the speed
743and direction of wind observations, but can technically be used to
744plot any two dimensional vector quantity. As opposed to arrows, which
745give vector magnitude by the length of the arrow, the barbs give more
746quantitative information about the vector magnitude by putting slanted
747lines or a triangle for various increments in magnitude, as show
748schematically below::
749
750 : /\ \
751 : / \ \
752 : / \ \ \
753 : / \ \ \
754 : ------------------------------
755
756The largest increment is given by a triangle (or "flag"). After those
757come full lines (barbs). The smallest increment is a half line. There
758is only, of course, ever at most 1 half line. If the magnitude is
759small and only needs a single half-line and no full lines or
760triangles, the half-line is offset from the end of the barb so that it
761can be easily distinguished from barbs with a single full line. The
762magnitude for the barb shown above would nominally be 65, using the
763standard increments of 50, 10, and 5.
764
765See also https://en.wikipedia.org/wiki/Wind_barb.
766
767Parameters
768----------
769X, Y : 1D or 2D array-like, optional
770 The x and y coordinates of the barb locations. See *pivot* for how the
771 barbs are drawn to the x, y positions.
772
773 If not given, they will be generated as a uniform integer meshgrid based
774 on the dimensions of *U* and *V*.
775
776 If *X* and *Y* are 1D but *U*, *V* are 2D, *X*, *Y* are expanded to 2D
777 using ``X, Y = np.meshgrid(X, Y)``. In this case ``len(X)`` and ``len(Y)``
778 must match the column and row dimensions of *U* and *V*.
779
780U, V : 1D or 2D array-like
781 The x and y components of the barb shaft.
782
783C : 1D or 2D array-like, optional
784 Numeric data that defines the barb colors by colormapping via *norm* and
785 *cmap*.
786
787 This does not support explicit colors. If you want to set colors directly,
788 use *barbcolor* instead.
789
790length : float, default: 7
791 Length of the barb in points; the other parts of the barb
792 are scaled against this.
793
794pivot : {'tip', 'middle'} or float, default: 'tip'
795 The part of the arrow that is anchored to the *X*, *Y* grid. The barb
796 rotates about this point. This can also be a number, which shifts the
797 start of the barb that many points away from grid point.
798
799barbcolor : :mpltype:`color` or color sequence
800 The color of all parts of the barb except for the flags. This parameter
801 is analogous to the *edgecolor* parameter for polygons, which can be used
802 instead. However this parameter will override facecolor.
803
804flagcolor : :mpltype:`color` or color sequence
805 The color of any flags on the barb. This parameter is analogous to the
806 *facecolor* parameter for polygons, which can be used instead. However,
807 this parameter will override facecolor. If this is not set (and *C* has
808 not either) then *flagcolor* will be set to match *barbcolor* so that the
809 barb has a uniform color. If *C* has been set, *flagcolor* has no effect.
810
811sizes : dict, optional
812 A dictionary of coefficients specifying the ratio of a given
813 feature to the length of the barb. Only those values one wishes to
814 override need to be included. These features include:
815
816 - 'spacing' - space between features (flags, full/half barbs)
817 - 'height' - height (distance from shaft to top) of a flag or full barb
818 - 'width' - width of a flag, twice the width of a full barb
819 - 'emptybarb' - radius of the circle used for low magnitudes
820
821fill_empty : bool, default: False
822 Whether the empty barbs (circles) that are drawn should be filled with
823 the flag color. If they are not filled, the center is transparent.
824
825rounding : bool, default: True
826 Whether the vector magnitude should be rounded when allocating barb
827 components. If True, the magnitude is rounded to the nearest multiple
828 of the half-barb increment. If False, the magnitude is simply truncated
829 to the next lowest multiple.
830
831barb_increments : dict, optional
832 A dictionary of increments specifying values to associate with
833 different parts of the barb. Only those values one wishes to
834 override need to be included.
835
836 - 'half' - half barbs (Default is 5)
837 - 'full' - full barbs (Default is 10)
838 - 'flag' - flags (default is 50)
839
840flip_barb : bool or array-like of bool, default: False
841 Whether the lines and flags should point opposite to normal.
842 Normal behavior is for the barbs and lines to point right (comes from wind
843 barbs having these features point towards low pressure in the Northern
844 Hemisphere).
845
846 A single value is applied to all barbs. Individual barbs can be flipped by
847 passing a bool array of the same size as *U* and *V*.
848
849Returns
850-------
851barbs : `~matplotlib.quiver.Barbs`
852
853Other Parameters
854----------------
855data : indexable object, optional
856 DATA_PARAMETER_PLACEHOLDER
857
858**kwargs
859 The barbs can further be customized using `.PolyCollection` keyword
860 arguments:
861
862 %(PolyCollection:kwdoc)s
863""" % _docstring.interpd.params
864
865_docstring.interpd.update(barbs_doc=_barbs_doc)
866
867
868class Barbs(mcollections.PolyCollection):
869 """
870 Specialized PolyCollection for barbs.
871
872 The only API method is :meth:`set_UVC`, which can be used to
873 change the size, orientation, and color of the arrows. Locations
874 are changed using the :meth:`set_offsets` collection method.
875 Possibly this method will be useful in animations.
876
877 There is one internal function :meth:`_find_tails` which finds
878 exactly what should be put on the barb given the vector magnitude.
879 From there :meth:`_make_barbs` is used to find the vertices of the
880 polygon to represent the barb based on this information.
881 """
882
883 # This may be an abuse of polygons here to render what is essentially maybe
884 # 1 triangle and a series of lines. It works fine as far as I can tell
885 # however.
886
887 @_docstring.interpd
888 def __init__(self, ax, *args,
889 pivot='tip', length=7, barbcolor=None, flagcolor=None,
890 sizes=None, fill_empty=False, barb_increments=None,
891 rounding=True, flip_barb=False, **kwargs):
892 """
893 The constructor takes one required argument, an Axes
894 instance, followed by the args and kwargs described
895 by the following pyplot interface documentation:
896 %(barbs_doc)s
897 """
898 self.sizes = sizes or dict()
899 self.fill_empty = fill_empty
900 self.barb_increments = barb_increments or dict()
901 self.rounding = rounding
902 self.flip = np.atleast_1d(flip_barb)
903 transform = kwargs.pop('transform', ax.transData)
904 self._pivot = pivot
905 self._length = length
906
907 # Flagcolor and barbcolor provide convenience parameters for
908 # setting the facecolor and edgecolor, respectively, of the barb
909 # polygon. We also work here to make the flag the same color as the
910 # rest of the barb by default
911
912 if None in (barbcolor, flagcolor):
913 kwargs['edgecolors'] = 'face'
914 if flagcolor:
915 kwargs['facecolors'] = flagcolor
916 elif barbcolor:
917 kwargs['facecolors'] = barbcolor
918 else:
919 # Set to facecolor passed in or default to black
920 kwargs.setdefault('facecolors', 'k')
921 else:
922 kwargs['edgecolors'] = barbcolor
923 kwargs['facecolors'] = flagcolor
924
925 # Explicitly set a line width if we're not given one, otherwise
926 # polygons are not outlined and we get no barbs
927 if 'linewidth' not in kwargs and 'lw' not in kwargs:
928 kwargs['linewidth'] = 1
929
930 # Parse out the data arrays from the various configurations supported
931 x, y, u, v, c = _parse_args(*args, caller_name='barbs')
932 self.x = x
933 self.y = y
934 xy = np.column_stack((x, y))
935
936 # Make a collection
937 barb_size = self._length ** 2 / 4 # Empirically determined
938 super().__init__(
939 [], (barb_size,), offsets=xy, offset_transform=transform, **kwargs)
940 self.set_transform(transforms.IdentityTransform())
941
942 self.set_UVC(u, v, c)
943
944 def _find_tails(self, mag, rounding=True, half=5, full=10, flag=50):
945 """
946 Find how many of each of the tail pieces is necessary.
947
948 Parameters
949 ----------
950 mag : `~numpy.ndarray`
951 Vector magnitudes; must be non-negative (and an actual ndarray).
952 rounding : bool, default: True
953 Whether to round or to truncate to the nearest half-barb.
954 half, full, flag : float, defaults: 5, 10, 50
955 Increments for a half-barb, a barb, and a flag.
956
957 Returns
958 -------
959 n_flags, n_barbs : int array
960 For each entry in *mag*, the number of flags and barbs.
961 half_flag : bool array
962 For each entry in *mag*, whether a half-barb is needed.
963 empty_flag : bool array
964 For each entry in *mag*, whether nothing is drawn.
965 """
966 # If rounding, round to the nearest multiple of half, the smallest
967 # increment
968 if rounding:
969 mag = half * np.around(mag / half)
970 n_flags, mag = divmod(mag, flag)
971 n_barb, mag = divmod(mag, full)
972 half_flag = mag >= half
973 empty_flag = ~(half_flag | (n_flags > 0) | (n_barb > 0))
974 return n_flags.astype(int), n_barb.astype(int), half_flag, empty_flag
975
976 def _make_barbs(self, u, v, nflags, nbarbs, half_barb, empty_flag, length,
977 pivot, sizes, fill_empty, flip):
978 """
979 Create the wind barbs.
980
981 Parameters
982 ----------
983 u, v
984 Components of the vector in the x and y directions, respectively.
985
986 nflags, nbarbs, half_barb, empty_flag
987 Respectively, the number of flags, number of barbs, flag for
988 half a barb, and flag for empty barb, ostensibly obtained from
989 :meth:`_find_tails`.
990
991 length
992 The length of the barb staff in points.
993
994 pivot : {"tip", "middle"} or number
995 The point on the barb around which the entire barb should be
996 rotated. If a number, the start of the barb is shifted by that
997 many points from the origin.
998
999 sizes : dict
1000 Coefficients specifying the ratio of a given feature to the length
1001 of the barb. These features include:
1002
1003 - *spacing*: space between features (flags, full/half barbs).
1004 - *height*: distance from shaft of top of a flag or full barb.
1005 - *width*: width of a flag, twice the width of a full barb.
1006 - *emptybarb*: radius of the circle used for low magnitudes.
1007
1008 fill_empty : bool
1009 Whether the circle representing an empty barb should be filled or
1010 not (this changes the drawing of the polygon).
1011
1012 flip : list of bool
1013 Whether the features should be flipped to the other side of the
1014 barb (useful for winds in the southern hemisphere).
1015
1016 Returns
1017 -------
1018 list of arrays of vertices
1019 Polygon vertices for each of the wind barbs. These polygons have
1020 been rotated to properly align with the vector direction.
1021 """
1022
1023 # These control the spacing and size of barb elements relative to the
1024 # length of the shaft
1025 spacing = length * sizes.get('spacing', 0.125)
1026 full_height = length * sizes.get('height', 0.4)
1027 full_width = length * sizes.get('width', 0.25)
1028 empty_rad = length * sizes.get('emptybarb', 0.15)
1029
1030 # Controls y point where to pivot the barb.
1031 pivot_points = dict(tip=0.0, middle=-length / 2.)
1032
1033 endx = 0.0
1034 try:
1035 endy = float(pivot)
1036 except ValueError:
1037 endy = pivot_points[pivot.lower()]
1038
1039 # Get the appropriate angle for the vector components. The offset is
1040 # due to the way the barb is initially drawn, going down the y-axis.
1041 # This makes sense in a meteorological mode of thinking since there 0
1042 # degrees corresponds to north (the y-axis traditionally)
1043 angles = -(ma.arctan2(v, u) + np.pi / 2)
1044
1045 # Used for low magnitude. We just get the vertices, so if we make it
1046 # out here, it can be reused. The center set here should put the
1047 # center of the circle at the location(offset), rather than at the
1048 # same point as the barb pivot; this seems more sensible.
1049 circ = CirclePolygon((0, 0), radius=empty_rad).get_verts()
1050 if fill_empty:
1051 empty_barb = circ
1052 else:
1053 # If we don't want the empty one filled, we make a degenerate
1054 # polygon that wraps back over itself
1055 empty_barb = np.concatenate((circ, circ[::-1]))
1056
1057 barb_list = []
1058 for index, angle in np.ndenumerate(angles):
1059 # If the vector magnitude is too weak to draw anything, plot an
1060 # empty circle instead
1061 if empty_flag[index]:
1062 # We can skip the transform since the circle has no preferred
1063 # orientation
1064 barb_list.append(empty_barb)
1065 continue
1066
1067 poly_verts = [(endx, endy)]
1068 offset = length
1069
1070 # Handle if this barb should be flipped
1071 barb_height = -full_height if flip[index] else full_height
1072
1073 # Add vertices for each flag
1074 for i in range(nflags[index]):
1075 # The spacing that works for the barbs is a little to much for
1076 # the flags, but this only occurs when we have more than 1
1077 # flag.
1078 if offset != length:
1079 offset += spacing / 2.
1080 poly_verts.extend(
1081 [[endx, endy + offset],
1082 [endx + barb_height, endy - full_width / 2 + offset],
1083 [endx, endy - full_width + offset]])
1084
1085 offset -= full_width + spacing
1086
1087 # Add vertices for each barb. These really are lines, but works
1088 # great adding 3 vertices that basically pull the polygon out and
1089 # back down the line
1090 for i in range(nbarbs[index]):
1091 poly_verts.extend(
1092 [(endx, endy + offset),
1093 (endx + barb_height, endy + offset + full_width / 2),
1094 (endx, endy + offset)])
1095
1096 offset -= spacing
1097
1098 # Add the vertices for half a barb, if needed
1099 if half_barb[index]:
1100 # If the half barb is the first on the staff, traditionally it
1101 # is offset from the end to make it easy to distinguish from a
1102 # barb with a full one
1103 if offset == length:
1104 poly_verts.append((endx, endy + offset))
1105 offset -= 1.5 * spacing
1106 poly_verts.extend(
1107 [(endx, endy + offset),
1108 (endx + barb_height / 2, endy + offset + full_width / 4),
1109 (endx, endy + offset)])
1110
1111 # Rotate the barb according the angle. Making the barb first and
1112 # then rotating it made the math for drawing the barb really easy.
1113 # Also, the transform framework makes doing the rotation simple.
1114 poly_verts = transforms.Affine2D().rotate(-angle).transform(
1115 poly_verts)
1116 barb_list.append(poly_verts)
1117
1118 return barb_list
1119
1120 def set_UVC(self, U, V, C=None):
1121 # We need to ensure we have a copy, not a reference to an array that
1122 # might change before draw().
1123 self.u = ma.masked_invalid(U, copy=True).ravel()
1124 self.v = ma.masked_invalid(V, copy=True).ravel()
1125
1126 # Flip needs to have the same number of entries as everything else.
1127 # Use broadcast_to to avoid a bloated array of identical values.
1128 # (can't rely on actual broadcasting)
1129 if len(self.flip) == 1:
1130 flip = np.broadcast_to(self.flip, self.u.shape)
1131 else:
1132 flip = self.flip
1133
1134 if C is not None:
1135 c = ma.masked_invalid(C, copy=True).ravel()
1136 x, y, u, v, c, flip = cbook.delete_masked_points(
1137 self.x.ravel(), self.y.ravel(), self.u, self.v, c,
1138 flip.ravel())
1139 _check_consistent_shapes(x, y, u, v, c, flip)
1140 else:
1141 x, y, u, v, flip = cbook.delete_masked_points(
1142 self.x.ravel(), self.y.ravel(), self.u, self.v, flip.ravel())
1143 _check_consistent_shapes(x, y, u, v, flip)
1144
1145 magnitude = np.hypot(u, v)
1146 flags, barbs, halves, empty = self._find_tails(
1147 magnitude, self.rounding, **self.barb_increments)
1148
1149 # Get the vertices for each of the barbs
1150
1151 plot_barbs = self._make_barbs(u, v, flags, barbs, halves, empty,
1152 self._length, self._pivot, self.sizes,
1153 self.fill_empty, flip)
1154 self.set_verts(plot_barbs)
1155
1156 # Set the color array
1157 if C is not None:
1158 self.set_array(c)
1159
1160 # Update the offsets in case the masked data changed
1161 xy = np.column_stack((x, y))
1162 self._offsets = xy
1163 self.stale = True
1164
1165 def set_offsets(self, xy):
1166 """
1167 Set the offsets for the barb polygons. This saves the offsets passed
1168 in and masks them as appropriate for the existing U/V data.
1169
1170 Parameters
1171 ----------
1172 xy : sequence of pairs of floats
1173 """
1174 self.x = xy[:, 0]
1175 self.y = xy[:, 1]
1176 x, y, u, v = cbook.delete_masked_points(
1177 self.x.ravel(), self.y.ravel(), self.u, self.v)
1178 _check_consistent_shapes(x, y, u, v)
1179 xy = np.column_stack((x, y))
1180 super().set_offsets(xy)
1181 self.stale = True