|
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
|
|
|
|