Completed
Pull Request — master (#394)
by Ryan
01:25
created

get_interp_point()   A

Complexity

Conditions 4

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
c 1
b 0
f 0
dl 0
loc 17
rs 9.2
1
# Copyright (c) 2008-2015 MetPy Developers.
2
# Distributed under the terms of the BSD 3-Clause License.
3
# SPDX-License-Identifier: BSD-3-Clause
4
"""Functionality that we have upstreamed or will upstream into matplotlib."""
5
from __future__ import division
6
7
import inspect
8
9
# See if we should monkey-patch Barbs for better pivot
10
import matplotlib
11
if float(matplotlib.__version__[:3]) < 2.1:
12
    import numpy as np
13
    from numpy import ma
14
    import matplotlib.transforms as transforms
15
    from matplotlib.patches import CirclePolygon
16
    from matplotlib.quiver import Barbs
17
18
    def _make_barbs(self, u, v, nflags, nbarbs, half_barb, empty_flag, length,
19
                    pivot, sizes, fill_empty, flip):
20
        """Monkey-patch _make_barbs. Allows pivot to be a float value."""
21
        # These control the spacing and size of barb elements relative to the
22
        # length of the shaft
23
        spacing = length * sizes.get('spacing', 0.125)
24
        full_height = length * sizes.get('height', 0.4)
25
        full_width = length * sizes.get('width', 0.25)
26
        empty_rad = length * sizes.get('emptybarb', 0.15)
27
28
        # Controls y point where to pivot the barb.
29
        pivot_points = dict(tip=0.0, middle=-length / 2.)
30
31
        # Check for flip
32
        if flip:
33
            full_height = -full_height
34
35
        endx = 0.0
36
        try:
37
            endy = float(pivot)
38
        except ValueError:
39
            endy = pivot_points[pivot.lower()]
40
41
        # Get the appropriate angle for the vector components.  The offset is
42
        # due to the way the barb is initially drawn, going down the y-axis.
43
        # This makes sense in a meteorological mode of thinking since there 0
44
        # degrees corresponds to north (the y-axis traditionally)
45
        angles = -(ma.arctan2(v, u) + np.pi / 2)
46
47
        # Used for low magnitude.  We just get the vertices, so if we make it
48
        # out here, it can be reused.  The center set here should put the
49
        # center of the circle at the location(offset), rather than at the
50
        # same point as the barb pivot; this seems more sensible.
51
        circ = CirclePolygon((0, 0), radius=empty_rad).get_verts()
52
        if fill_empty:
53
            empty_barb = circ
54
        else:
55
            # If we don't want the empty one filled, we make a degenerate
56
            # polygon that wraps back over itself
57
            empty_barb = np.concatenate((circ, circ[::-1]))
58
59
        barb_list = []
60
        for index, angle in np.ndenumerate(angles):
61
            # If the vector magnitude is too weak to draw anything, plot an
62
            # empty circle instead
63
            if empty_flag[index]:
64
                # We can skip the transform since the circle has no preferred
65
                # orientation
66
                barb_list.append(empty_barb)
67
                continue
68
69
            poly_verts = [(endx, endy)]
70
            offset = length
71
72
            # Add vertices for each flag
73
            for _ in range(nflags[index]):
74
                # The spacing that works for the barbs is a little to much for
75
                # the flags, but this only occurs when we have more than 1
76
                # flag.
77
                if offset != length:
78
                    offset += spacing / 2.
79
                poly_verts.extend(
80
                    [[endx, endy + offset],
81
                     [endx + full_height, endy - full_width / 2 + offset],
82
                     [endx, endy - full_width + offset]])
83
84
                offset -= full_width + spacing
85
86
            # Add vertices for each barb.  These really are lines, but works
87
            # great adding 3 vertices that basically pull the polygon out and
88
            # back down the line
89
            for _ in range(nbarbs[index]):
90
                poly_verts.extend(
91
                    [(endx, endy + offset),
92
                     (endx + full_height, endy + offset + full_width / 2),
93
                     (endx, endy + offset)])
94
95
                offset -= spacing
96
97
            # Add the vertices for half a barb, if needed
98
            if half_barb[index]:
99
                # If the half barb is the first on the staff, traditionally it
100
                # is offset from the end to make it easy to distinguish from a
101
                # barb with a full one
102
                if offset == length:
103
                    poly_verts.append((endx, endy + offset))
104
                    offset -= 1.5 * spacing
105
                poly_verts.extend(
106
                    [(endx, endy + offset),
107
                     (endx + full_height / 2, endy + offset + full_width / 4),
108
                     (endx, endy + offset)])
109
110
            # Rotate the barb according the angle. Making the barb first and
111
            # then rotating it made the math for drawing the barb really easy.
112
            # Also, the transform framework makes doing the rotation simple.
113
            poly_verts = transforms.Affine2D().rotate(-angle).transform(
114
                poly_verts)
115
            barb_list.append(poly_verts)
116
117
        return barb_list
118
119
    # Replace existing method
120
    Barbs._make_barbs = _make_barbs
121
122
123
# See if we need to patch in our own scattertext implementation
124
from matplotlib.axes import Axes  # noqa: E402
125
if not hasattr(Axes, 'scattertext'):
126
    import matplotlib.cbook as cbook
127
    import matplotlib.transforms as mtransforms
128
    from matplotlib import rcParams
129
    from matplotlib.artist import allow_rasterization
130
    from matplotlib.text import Text
131
132
    def scattertext(self, x, y, texts, loc=(0, 0), **kw):
133
        """Add text to the axes.
134
135
        Add text in string `s` to axis at location `x`, `y`, data
136
        coordinates.
137
138
        Parameters
139
        ----------
140
        x, y : array_like, shape (n, )
141
            Input positions
142
143
        texts : array_like, shape (n, )
144
            Collection of text that will be plotted at each (x,y) location
145
146
        loc : length-2 tuple
147
            Offset (in screen coordinates) from x,y position. Allows
148
            positioning text relative to original point.
149
150
        Other parameters
151
        ----------------
152
        kwargs : `~matplotlib.text.TextCollection` properties.
153
            Other miscellaneous text parameters.
154
155
        Examples
156
        --------
157
        Individual keyword arguments can be used to override any given
158
        parameter::
159
160
            >>> scattertext(x, y, texts, fontsize=12)
161
162
        The default setting to to center the text at the specified x,y
163
        locations in data coordinates, and to take the data and format as
164
        float without any decimal places. The example below places the text
165
        above and to the right by 10 pixels, with 2 decimal places::
166
167
            >>> scattertext([0.25, 0.75], [0.25, 0.75], [0.5, 1.0],
168
            ...             loc=(10, 10))
169
        """
170
        # Start with default args and update from kw
171
        new_kw = {
172
            'verticalalignment': 'center',
173
            'horizontalalignment': 'center',
174
            'transform': self.transData,
175
            'clip_on': False}
176
        new_kw.update(kw)
177
178
        # Default to centered on point--special case it to keep transform
179
        # simpler.
180
        # t = new_kw['transform']
181
        # if loc == (0, 0):
182
        #     trans = t
183
        # else:
184
        #     x0, y0 = loc
185
        #     trans = t + mtransforms.Affine2D().translate(x0, y0)
186
        # new_kw['transform'] = trans
187
188
        # Handle masked arrays
189
        x, y, texts = cbook.delete_masked_points(x, y, texts)
190
191
        # If there is nothing left after deleting the masked points, return None
192
        if x.size == 0:
193
            return None
194
195
        # Make the TextCollection object
196
        text_obj = TextCollection(x, y, texts, offset=loc, **new_kw)
197
198
        # The margin adjustment is a hack to deal with the fact that we don't
199
        # want to transform all the symbols whose scales are in points
200
        # to data coords to get the exact bounding box for efficiency
201
        # reasons.  It can be done right if this is deemed important.
202
        # Also, only bother with this padding if there is anything to draw.
203
        if self._xmargin < 0.05:
204
            self.set_xmargin(0.05)
205
206
        if self._ymargin < 0.05:
207
            self.set_ymargin(0.05)
208
209
        # Add it to the axes and update range
210
        self.add_artist(text_obj)
211
        self.update_datalim(text_obj.get_datalim(self.transData))
212
        self.autoscale_view()
213
        return text_obj
214
215
    class TextCollection(Text):
216
        """Handle plotting a collection of text.
217
218
        Text Collection plots text with a collection of similar properties: font, color,
219
        and an offset relative to the x,y data location.
220
        """
221
222
        def __init__(self, x, y, text, offset=(0, 0), **kwargs):
223
            """Initialize an instance of `TextCollection`.
224
225
            This class encompasses drawing a collection of text values at a variety
226
            of locations.
227
228
            Parameters
229
            ----------
230
            x : array_like
231
                The x locations, in data coordinates, for the text
232
233
            y : array_like
234
                The y locations, in data coordinates, for the text
235
236
            text : array_like of str
237
                The string values to draw
238
239
            offset : (int, int)
240
                The offset x and y, in normalized coordinates, to draw the text relative
241
                to the data locations.
242
243
            kwargs : arbitrary keywords arguments
244
            """
245
            Text.__init__(self, **kwargs)
246
            self.x = x
247
            self.y = y
248
            self.text = text
249
            self.offset = offset
250
            if not hasattr(self, '_usetex'):  # Only needed for matplotlib 1.4 compatibility
251
                self._usetex = None
252
253
        def __str__(self):
254
            """Make a string representation of `TextCollection`."""
255
            return 'TextCollection'
256
257
        def get_datalim(self, transData):  # noqa: N803
258
            """Return the limits of the data.
259
260
            Parameters
261
            ----------
262
            transData : matplotlib.transforms.Transform
263
264
            Returns
265
            -------
266
            matplotlib.transforms.Bbox
267
                The bounding box of the data
268
            """
269
            full_transform = self.get_transform() - transData
270
            XY = full_transform.transform(np.vstack((self.x, self.y)).T)  # noqa: N806
271
            bbox = transforms.Bbox.null()
272
            bbox.update_from_data_xy(XY, ignore=True)
273
            return bbox
274
275
        @allow_rasterization
276
        def draw(self, renderer):
277
            """Draw the :class:`TextCollection` object to the given *renderer*."""
278
            if renderer is not None:
279
                self._renderer = renderer
280
            if not self.get_visible():
281
                return
282
            if not any(self.text):
283
                return
284
285
            renderer.open_group('text', self.get_gid())
286
287
            trans = self.get_transform()
288
            if self.offset != (0, 0):
289
                scale = self.axes.figure.dpi / 72
290
                xoff, yoff = self.offset
291
                trans += mtransforms.Affine2D().translate(scale * xoff,
292
                                                          scale * yoff)
293
294
            posx = self.convert_xunits(self.x)
295
            posy = self.convert_yunits(self.y)
296
            pts = np.vstack((posx, posy)).T
297
            pts = trans.transform(pts)
298
            canvasw, canvash = renderer.get_canvas_width_height()
299
300
            gc = renderer.new_gc()
301
            gc.set_foreground(self.get_color())
302
            gc.set_alpha(self.get_alpha())
303
            gc.set_url(self._url)
304
            self._set_gc_clip(gc)
305
306
            angle = self.get_rotation()
307
308
            for (posx, posy), t in zip(pts, self.text):
309
                # Skip empty strings--not only is this a performance gain, but it fixes
310
                # rendering with path effects below.
311
                if not t:
312
                    continue
313
314
                self._text = t  # hack to allow self._get_layout to work
315
                bbox, info, descent = self._get_layout(renderer)
316
                self._text = ''
317
318
                for line, _, x, y in info:
319
320
                    mtext = self if len(info) == 1 else None
321
                    x = x + posx
322
                    y = y + posy
323
                    if renderer.flipy():
324
                        y = canvash - y
325
                    clean_line, ismath = self.is_math_text(line)
326
327
                    if self.get_path_effects():
328
                        from matplotlib.patheffects import PathEffectRenderer
329
                        textrenderer = PathEffectRenderer(
330
                                            self.get_path_effects(), renderer)  # noqa: E126
331
                    else:
332
                        textrenderer = renderer
333
334
                    if self.get_usetex():
335
                        textrenderer.draw_tex(gc, x, y, clean_line,
336
                                              self._fontproperties, angle,
337
                                              mtext=mtext)
338
                    else:
339
                        textrenderer.draw_text(gc, x, y, clean_line,
340
                                               self._fontproperties, angle,
341
                                               ismath=ismath, mtext=mtext)
342
343
            gc.restore()
344
            renderer.close_group('text')
345
346
        def set_usetex(self, usetex):
347
            """
348
            Set this `Text` object to render using TeX (or not).
349
350
            If `None` is given, the option will be reset to use the value of
351
            `rcParams['text.usetex']`
352
            """
353
            if usetex is None:
354
                self._usetex = None
355
            else:
356
                self._usetex = bool(usetex)
357
            self.stale = True
358
359
        def get_usetex(self):
360
            """
361
            Return whether this `Text` object will render using TeX.
362
363
            If the user has not manually set this value, it will default to
364
            the value of `rcParams['text.usetex']`
365
            """
366
            if self._usetex is None:
367
                return rcParams['text.usetex']
368
            else:
369
                return self._usetex
370
371
    # Monkey-patch scattertext onto Axes
372
    Axes.scattertext = scattertext
373
374
375
# See if we need to add in the Tableau colors which were added in Matplotlib 2.0
376
import matplotlib.colors  # noqa: E402
377
if not hasattr(matplotlib.colors, 'TABLEAU_COLORS'):
378
    from collections import OrderedDict
379
380
    # These colors are from Tableau
381
    TABLEAU_COLORS = (
382
        ('blue', '#1f77b4'),
383
        ('orange', '#ff7f0e'),
384
        ('green', '#2ca02c'),
385
        ('red', '#d62728'),
386
        ('purple', '#9467bd'),
387
        ('brown', '#8c564b'),
388
        ('pink', '#e377c2'),
389
        ('gray', '#7f7f7f'),
390
        ('olive', '#bcbd22'),
391
        ('cyan', '#17becf'),
392
    )
393
394
    # Normalize name to "tab:<name>" to avoid name collisions.
395
    matplotlib.colors.TABLEAU_COLORS = OrderedDict(
396
        ('tab:' + name, value) for name, value in TABLEAU_COLORS)
397
398
    matplotlib.colors.cnames.update(matplotlib.colors.TABLEAU_COLORS)
399
400
401
# Backport interpolating around the cross-over point for fill_betweenx (Matplotlib #6560)
402
if 'interpolate' not in inspect.getfullargspec(Axes.fill_betweenx).args:
403
    from functools import reduce
404
405
    from matplotlib.cbook import STEP_LOOKUP_MAP
406
    import matplotlib.collections as mcoll
407
    import matplotlib.mlab as mlab
408
409
    def fill_betweenx(self, y, x1, x2=0, where=None,
410
                      step=None, interpolate=False, **kwargs):
411
        """
412
        Make filled polygons between two horizontal curves.
413
414
        Create a :class:`~matplotlib.collections.PolyCollection`
415
        filling the regions between *x1* and *x2* where
416
        ``where==True``
417
418
        Parameters
419
        ----------
420
        y : array
421
            An N-length array of the y data
422
423
        x1 : array
424
            An N-length array (or scalar) of the x data
425
426
        x2 : array, optional
427
            An N-length array (or scalar) of the x data
428
429
        where : array, optional
430
            If *None*, default to fill between everywhere.  If not *None*,
431
            it is a N length numpy boolean array and the fill will
432
            only happen over the regions where ``where==True``
433
434
        step : {'pre', 'post', 'mid'}, optional
435
            If not None, fill with step logic.
436
437
        interpolate : bool, optional
438
            If `True`, interpolate between the two lines to find the
439
            precise point of intersection.  Otherwise, the start and
440
            end points of the filled region will only occur on explicit
441
            values in the *x* array.
442
443
        Notes
444
        -----
445
446
        keyword args passed on to the
447
            :class:`~matplotlib.collections.PolyCollection`
448
449
        kwargs control the :class:`~matplotlib.patches.Polygon` properties:
450
451
        %(PolyCollection)s
452
453
        Examples
454
        --------
455
456
        .. plot:: mpl_examples/pylab_examples/fill_betweenx_demo.py
457
458
        See Also
459
        --------
460
461
            :meth:`fill_between`
462
                for filling between two sets of y-values
463
464
        """
465
        if not rcParams['_internal.classic_mode']:
466
            color_aliases = mcoll._color_aliases
467
            kwargs = cbook.normalize_kwargs(kwargs, color_aliases)
468
469
            if not any(c in kwargs for c in ('color', 'facecolors')):
470
                fc = self._get_patches_for_fill.get_next_color()
471
                kwargs['facecolors'] = fc
472
        # Handle united data, such as dates
473
        self._process_unit_info(ydata=y, xdata=x1, kwargs=kwargs)
474
        self._process_unit_info(xdata=x2)
475
476
        # Convert the arrays so we can work with them
477
        y = ma.masked_invalid(self.convert_yunits(y))
478
        x1 = ma.masked_invalid(self.convert_xunits(x1))
479
        x2 = ma.masked_invalid(self.convert_xunits(x2))
480
481
        for name, array in [('y', y), ('x1', x1), ('x2', x2)]:
482
            if array.ndim > 1:
483
                raise ValueError('Input passed into argument "{0:r}"'.format(name) +
484
                                 'is not 1-dimensional.')
485
486
        if x1.ndim == 0:
487
            x1 = np.ones_like(y) * x1
488
        if x2.ndim == 0:
489
            x2 = np.ones_like(y) * x2
490
491
        if where is None:
492
            where = np.ones(len(y), np.bool)
493
        else:
494
            where = np.asarray(where, np.bool)
495
496
        if not (y.shape == x1.shape == x2.shape == where.shape):
497
            raise ValueError('Argument dimensions are incompatible')
498
499
        mask = reduce(ma.mask_or, [ma.getmask(a) for a in (y, x1, x2)])
500
        if mask is not ma.nomask:
501
            where &= ~mask
502
503
        polys = []
504
        for ind0, ind1 in mlab.contiguous_regions(where):
505
            yslice = y[ind0:ind1]
506
            x1slice = x1[ind0:ind1]
507
            x2slice = x2[ind0:ind1]
508
            if step is not None:
509
                step_func = STEP_LOOKUP_MAP['steps-' + step]
510
                yslice, x1slice, x2slice = step_func(yslice, x1slice, x2slice)
511
512
            if not len(yslice):
513
                continue
514
515
            N = len(yslice)
516
            Y = np.zeros((2 * N + 2, 2), np.float)
517
            if interpolate:
518
                def get_interp_point(ind):
519
                    im1 = max(ind - 1, 0)
520
                    y_values = y[im1:ind + 1]
521
                    diff_values = x1[im1:ind + 1] - x2[im1:ind + 1]
522
                    x1_values = x1[im1:ind + 1]
523
524
                    if len(diff_values) == 2:
525
                        if np.ma.is_masked(diff_values[1]):
526
                            return x1[im1], y[im1]
527
                        elif np.ma.is_masked(diff_values[0]):
528
                            return x1[ind], y[ind]
529
530
                    diff_order = diff_values.argsort()
531
                    diff_root_y = np.interp(
532
                        0, diff_values[diff_order], y_values[diff_order])
533
                    diff_root_x = np.interp(diff_root_y, y_values, x1_values)
534
                    return diff_root_x, diff_root_y
535
536
                start = get_interp_point(ind0)
537
                end = get_interp_point(ind1)
538
            else:
539
                # the purpose of the next two lines is for when x2 is a
540
                # scalar like 0 and we want the fill to go all the way
541
                # down to 0 even if none of the x1 sample points do
542
                start = x2slice[0], yslice[0]
543
                end = x2slice[-1], yslice[-1]
544
545
            Y[0] = start
546
            Y[N + 1] = end
547
548
            Y[1:N + 1, 0] = x1slice
549
            Y[1:N + 1, 1] = yslice
550
            Y[N + 2:, 0] = x2slice[::-1]
551
            Y[N + 2:, 1] = yslice[::-1]
552
553
            polys.append(Y)
554
555
        collection = mcoll.PolyCollection(polys, **kwargs)
556
557
        # now update the datalim and autoscale
558
        X1Y = np.array([x1[where], y[where]]).T
559
        X2Y = np.array([x2[where], y[where]]).T
560
        self.dataLim.update_from_data_xy(X1Y, self.ignore_existing_data_limits,
561
                                         updatex=True, updatey=True)
562
        self.ignore_existing_data_limits = False
563
        self.dataLim.update_from_data_xy(X2Y, self.ignore_existing_data_limits,
564
                                         updatex=True, updatey=False)
565
        self.add_collection(collection, autolim=False)
566
        self.autoscale_view()
567
        return collection
568
569
    # Monkey patch in our fill_between function
570
    Axes.fill_betweenx = fill_betweenx
571