NCCOOS Trac Projects: Top | Web | Platforms | Processing | Viz | Sprints | Sandbox | (Wind)

root/sodarplot/trunk/sodarplot/scintec/windrose.py

Revision 68 (checked in by cbc, 13 years ago)

Fix windrose legend label upper bound.

Line 
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 __version__ = '1.4cbc1'
5 __author__ = 'Lionel Roubeyrie'
6 __mail__ = 'lionel.roubeyrie@gmail.com'
7 __license__ = 'CeCILL-B'
8 __modified_by__ = 'Chris Calloway'
9 __modified_email__ = 'cbc@unc.edu'
10
11 import matplotlib
12 import matplotlib.cm as cm
13 import numpy as np
14 from matplotlib.patches import Rectangle, Polygon
15 from matplotlib.ticker import ScalarFormatter, AutoLocator
16 from matplotlib.text import Text, FontProperties
17 from matplotlib.projections.polar import PolarAxes
18 from numpy.lib.twodim_base import histogram2d
19 import matplotlib.pyplot as plt
20 from pylab import poly_between
21
22 RESOLUTION = 100
23 ZBASE = -1000 #The starting zorder for all drawing, negative to have the grid on
24
25 class WindroseAxes(PolarAxes):
26     """
27
28     Create a windrose axes
29
30     """
31
32     def __init__(self, *args, **kwargs):
33         """
34         See Axes base class for args and kwargs documentation
35         """
36        
37         #Uncomment to have the possibility to change the resolution directly
38         #when the instance is created
39         #self.RESOLUTION = kwargs.pop('resolution', 100)
40         PolarAxes.__init__(self, *args, **kwargs)
41         self.set_aspect('equal', adjustable='box', anchor='C')
42         self.radii_angle = 67.5
43         self.cla()
44
45
46     def cla(self):
47         """
48         Clear the current axes
49         """
50         PolarAxes.cla(self)
51
52         self.theta_angles = np.arange(0, 360, 45)
53         self.theta_labels = ['E', 'N-E', 'N', 'N-W', 'W', 'S-W', 'S', 'S-E']
54         self.set_thetagrids(angles=self.theta_angles, labels=self.theta_labels)
55
56         self._info = {'dir' : list(),
57                       'bins' : list(),
58                       'table' : list()}
59
60         self.patches_list = list()
61
62
63     def _colors(self, cmap, n):
64         '''
65         Returns a list of n colors based on the colormap cmap
66
67         '''
68         return [cmap(i) for i in np.linspace(0.0, 1.0, n)]
69
70
71     def set_radii_angle(self, **kwargs):
72         """
73         Set the radii labels angle
74         """
75
76         null = kwargs.pop('labels', None)
77         angle = kwargs.pop('angle', None)
78         if angle is None:
79             angle = self.radii_angle
80         self.radii_angle = angle
81         radii = np.linspace(0.1, self.get_rmax(), 6)
82         radii_labels = [ "%.1f" %r for r in radii ]
83         radii_labels[0] = "" #Removing label 0
84         null = self.set_rgrids(radii=radii, labels=radii_labels,
85                                angle=self.radii_angle, **kwargs)
86
87
88     def _update(self):
89         self.set_rmax(rmax=np.max(np.sum(self._info['table'], axis=0)))
90         self.set_radii_angle(angle=self.radii_angle)
91
92
93     def legend(self, loc='lower left', **kwargs):
94         """
95         Sets the legend location and her properties.
96         The location codes are
97
98           'best'         : 0,
99           'upper right'  : 1,
100           'upper left'   : 2,
101           'lower left'   : 3,
102           'lower right'  : 4,
103           'right'        : 5,
104           'center left'  : 6,
105           'center right' : 7,
106           'lower center' : 8,
107           'upper center' : 9,
108           'center'       : 10,
109
110         If none of these are suitable, loc can be a 2-tuple giving x,y
111         in axes coords, ie,
112
113           loc = (0, 1) is left top
114           loc = (0.5, 0.5) is center, center
115
116         and so on.  The following kwargs are supported:
117
118         isaxes=True           # whether this is an axes legend
119         prop = FontProperties(size='smaller')  # the font property
120         pad = 0.2             # the fractional whitespace inside the legend border
121         shadow                # if True, draw a shadow behind legend
122         labelsep = 0.005     # the vertical space between the legend entries
123         handlelen = 0.05     # the length of the legend lines
124         handletextsep = 0.02 # the space between the legend line and legend text
125         axespad = 0.02       # the border between the axes and legend edge
126         """
127
128         def get_handles():
129             handles = list()
130             for p in self.patches_list:
131                 if isinstance(p, matplotlib.patches.Polygon) or \
132                 isinstance(p, matplotlib.patches.Rectangle):
133                     color = p.get_facecolor()
134                 elif isinstance(p, matplotlib.lines.Line2D):
135                     color = p.get_color()
136                 else:
137                     raise AttributeError("Can't handle patches")
138                 handles.append(Rectangle((0, 0), 0.2, 0.2,
139                     facecolor=color, edgecolor='black'))
140             return handles
141
142         def get_labels():
143             labels = np.copy(self._info['bins'])
144             labels = ["[%.1f : %0.1f]" %(labels[i], labels[i+1]) \
145                       if label[i+1] != float('inf')
146                       else "[%.1f : inf]" % labels[i]
147                       for i in range(len(labels)-1)]
148             return labels
149
150         null = kwargs.pop('labels', None)
151         null = kwargs.pop('handles', None)
152         handles = get_handles()
153         labels = get_labels()
154         self.legend_ = matplotlib.legend.Legend(self, handles, labels,
155                                                 loc, **kwargs)
156         return self.legend_
157
158
159     def _init_plot(self, dir, var, **kwargs):
160         """
161         Internal method used by all plotting commands
162         """
163         #self.cla()
164         null = kwargs.pop('zorder', None)
165
166         #Init of the bins array if not set
167         bins = kwargs.pop('bins', None)
168         if bins is None:
169             bins = np.linspace(np.min(var), np.max(var), 6)
170         if isinstance(bins, int):
171             bins = np.linspace(np.min(var), np.max(var), bins)
172         bins = np.asarray(bins)
173         nbins = len(bins)
174
175         #Number of sectors
176         nsector = kwargs.pop('nsector', None)
177         if nsector is None:
178             nsector = 16
179
180         #Sets the colors table based on the colormap or the "colors" argument
181         colors = kwargs.pop('colors', None)
182         cmap = kwargs.pop('cmap', None)
183         if colors is not None:
184             if isinstance(colors, str):
185                 colors = [colors]*nbins
186             if isinstance(colors, (tuple, list)):
187                 if len(colors) != nbins:
188                     raise ValueError("colors and bins must have same length")
189         else:
190             if cmap is None:
191                 cmap = cm.jet
192             colors = self._colors(cmap, nbins)
193
194         #Building the angles list
195         angles = np.arange(0, -2*np.pi, -2*np.pi/nsector) + np.pi/2
196
197         normed = kwargs.pop('normed', False)
198         blowto = kwargs.pop('blowto', False)
199
200         #Set the global information dictionnary
201         self._info['dir'], self._info['bins'], self._info['table'] = histogram(dir, var, bins, nsector, normed, blowto)
202
203         return bins, nbins, nsector, colors, angles, kwargs
204
205
206     def contour(self, dir, var, **kwargs):
207         """
208         Plot a windrose in linear mode. For each var bins, a line will be
209         draw on the axes, a segment between each sector (center to center).
210         Each line can be formated (color, width, ...) like with standard plot
211         pylab command.
212
213         Mandatory:
214         * dir : 1D array - directions the wind blows from, North centred
215         * var : 1D array - values of the variable to compute. Typically the wind
216         speeds
217         Optional:
218         * nsector: integer - number of sectors used to compute the windrose
219         table. If not set, nsectors=16, then each sector will be 360/16=22.5°,
220         and the resulting computed table will be aligned with the cardinals
221         points.
222         * bins : 1D array or integer- number of bins, or a sequence of
223         bins variable. If not set, bins=6, then
224             bins=linspace(min(var), max(var), 6)
225         * blowto : bool. If True, the windrose will be pi rotated,
226         to show where the wind blow to (usefull for pollutant rose).
227         * colors : string or tuple - one string color ('k' or 'black'), in this
228         case all bins will be plotted in this color; a tuple of matplotlib
229         color args (string, float, rgb, etc), different levels will be plotted
230         in different colors in the order specified.
231         * cmap : a cm Colormap instance from matplotlib.cm.
232           - if cmap == None and colors == None, a default Colormap is used.
233
234         others kwargs : see help(pylab.plot)
235
236         """
237
238         bins, nbins, nsector, colors, angles, kwargs = self._init_plot(dir, var,
239                                                                        **kwargs)
240
241         #closing lines
242         angles = np.hstack((angles, angles[-1]-2*np.pi/nsector))
243         vals = np.hstack((self._info['table'],
244                          np.reshape(self._info['table'][:,0],
245                                    (self._info['table'].shape[0], 1))))
246        
247         offset = 0
248         for i in range(nbins):
249             val = vals[i,:] + offset
250             offset += vals[i, :]
251             zorder = ZBASE + nbins - i
252             patch = self.plot(angles, val, color=colors[i], zorder=zorder,
253                               **kwargs)
254             self.patches_list.extend(patch)
255         self._update()
256
257
258     def contourf(self, dir, var, **kwargs):
259         """
260         Plot a windrose in filled mode. For each var bins, a line will be
261         draw on the axes, a segment between each sector (center to center).
262         Each line can be formated (color, width, ...) like with standard plot
263         pylab command.
264
265         Mandatory:
266         * dir : 1D array - directions the wind blows from, North centred
267         * var : 1D array - values of the variable to compute. Typically the wind
268         speeds
269         Optional:
270         * nsector: integer - number of sectors used to compute the windrose
271         table. If not set, nsectors=16, then each sector will be 360/16=22.5°,
272         and the resulting computed table will be aligned with the cardinals
273         points.
274         * bins : 1D array or integer- number of bins, or a sequence of
275         bins variable. If not set, bins=6, then
276             bins=linspace(min(var), max(var), 6)
277         * blowto : bool. If True, the windrose will be pi rotated,
278         to show where the wind blow to (usefull for pollutant rose).
279         * colors : string or tuple - one string color ('k' or 'black'), in this
280         case all bins will be plotted in this color; a tuple of matplotlib
281         color args (string, float, rgb, etc), different levels will be plotted
282         in different colors in the order specified.
283         * cmap : a cm Colormap instance from matplotlib.cm.
284           - if cmap == None and colors == None, a default Colormap is used.
285
286         others kwargs : see help(pylab.plot)
287
288         """
289
290         bins, nbins, nsector, colors, angles, kwargs = self._init_plot(dir, var,
291                                                                        **kwargs)
292         null = kwargs.pop('facecolor', None)
293         null = kwargs.pop('edgecolor', None)
294        
295         #closing lines
296         angles = np.hstack((angles, angles[-1]-2*np.pi/nsector))
297         vals = np.hstack((self._info['table'],
298                          np.reshape(self._info['table'][:,0],
299                                    (self._info['table'].shape[0], 1))))
300         offset = 0
301         for i in range(nbins):
302             val = vals[i,:] + offset
303             offset += vals[i, :]
304             zorder = ZBASE + nbins - i
305             xs, ys = poly_between(angles, 0, val)
306             patch = self.fill(xs, ys, facecolor=colors[i],
307                               edgecolor=colors[i], zorder=zorder, **kwargs)
308             self.patches_list.extend(patch)
309
310
311     def bar(self, dir, var, **kwargs):
312         """
313         Plot a windrose in bar mode. For each var bins and for each sector,
314         a colored bar will be draw on the axes.
315
316         Mandatory:
317         * dir : 1D array - directions the wind blows from, North centred
318         * var : 1D array - values of the variable to compute. Typically the wind
319         speeds
320         Optional:
321         * nsector: integer - number of sectors used to compute the windrose
322         table. If not set, nsectors=16, then each sector will be 360/16=22.5°,
323         and the resulting computed table will be aligned with the cardinals
324         points.
325         * bins : 1D array or integer- number of bins, or a sequence of
326         bins variable. If not set, bins=6 between min(var) and max(var).
327         * blowto : bool. If True, the windrose will be pi rotated,
328         to show where the wind blow to (usefull for pollutant rose).
329         * colors : string or tuple - one string color ('k' or 'black'), in this
330         case all bins will be plotted in this color; a tuple of matplotlib
331         color args (string, float, rgb, etc), different levels will be plotted
332         in different colors in the order specified.
333         * cmap : a cm Colormap instance from matplotlib.cm.
334           - if cmap == None and colors == None, a default Colormap is used.
335         edgecolor : string - The string color each edge bar will be plotted.
336         Default : no edgecolor
337         * opening : float - between 0.0 and 1.0, to control the space between
338         each sector (1.0 for no space)
339
340         """
341
342         bins, nbins, nsector, colors, angles, kwargs = self._init_plot(dir, var,
343                                                                        **kwargs)
344         null = kwargs.pop('facecolor', None)
345         edgecolor = kwargs.pop('edgecolor', None)
346         if edgecolor is not None:
347             if not isinstance(edgecolor, str):
348                 raise ValueError('edgecolor must be a string color')
349         opening = kwargs.pop('opening', None)
350         if opening is None:
351             opening = 0.8
352         dtheta = 2*np.pi/nsector
353         opening = dtheta*opening
354
355         for j in range(nsector):
356             offset = 0
357             for i in range(nbins):
358                 if i > 0:
359                     offset += self._info['table'][i-1, j]
360                 val = self._info['table'][i, j]
361                 zorder = ZBASE + nbins - i
362                 patch = Rectangle((angles[j]-opening/2, offset), opening, val,
363                     facecolor=colors[i], edgecolor=edgecolor, zorder=zorder,
364                     **kwargs)
365                 self.add_patch(patch)
366                 if j == 0:
367                     self.patches_list.append(patch)
368         self._update()
369
370
371     def box(self, dir, var, **kwargs):
372         """
373         Plot a windrose in proportional bar mode. For each var bins and for each
374         sector, a colored bar will be draw on the axes.
375
376         Mandatory:
377         * dir : 1D array - directions the wind blows from, North centred
378         * var : 1D array - values of the variable to compute. Typically the wind
379         speeds
380         Optional:
381         * nsector: integer - number of sectors used to compute the windrose
382         table. If not set, nsectors=16, then each sector will be 360/16=22.5°,
383         and the resulting computed table will be aligned with the cardinals
384         points.
385         * bins : 1D array or integer- number of bins, or a sequence of
386         bins variable. If not set, bins=6 between min(var) and max(var).
387         * blowto : bool. If True, the windrose will be pi rotated,
388         to show where the wind blow to (usefull for pollutant rose).
389         * colors : string or tuple - one string color ('k' or 'black'), in this
390         case all bins will be plotted in this color; a tuple of matplotlib
391         color args (string, float, rgb, etc), different levels will be plotted
392         in different colors in the order specified.
393         * cmap : a cm Colormap instance from matplotlib.cm.
394           - if cmap == None and colors == None, a default Colormap is used.
395         edgecolor : string - The string color each edge bar will be plotted.
396         Default : no edgecolor
397
398         """
399
400         bins, nbins, nsector, colors, angles, kwargs = self._init_plot(dir, var,
401                                                                        **kwargs)
402         null = kwargs.pop('facecolor', None)
403         edgecolor = kwargs.pop('edgecolor', None)
404         if edgecolor is not None:
405             if not isinstance(edgecolor, str):
406                 raise ValueError('edgecolor must be a string color')
407         opening = np.linspace(0.0, np.pi/16, nbins)
408
409         for j in range(nsector):
410             offset = 0
411             for i in range(nbins):
412                 if i > 0:
413                     offset += self._info['table'][i-1, j]
414                 val = self._info['table'][i, j]
415                 zorder = ZBASE + nbins - i
416                 patch = Rectangle((angles[j]-opening[i]/2, offset), opening[i],
417                     val, facecolor=colors[i], edgecolor=edgecolor,
418                     zorder=zorder, **kwargs)
419                 self.add_patch(patch)
420                 if j == 0:
421                     self.patches_list.append(patch)
422         self._update()
423
424 def histogram(dir, var, bins, nsector, normed=False, blowto=False):
425     """
426     Returns an array where, for each sector of wind
427     (centred on the north), we have the number of time the wind comes with a
428     particular var (speed, polluant concentration, ...).
429     * dir : 1D array - directions the wind blows from, North centred
430     * var : 1D array - values of the variable to compute. Typically the wind
431         speeds
432     * bins : list - list of var category against we're going to compute the table
433     * nsector : integer - number of sectors
434     * normed : boolean - The resulting table is normed in percent or not.
435     * blowto : boolean - Normaly a windrose is computed with directions
436     as wind blows from. If true, the table will be reversed (usefull for
437     pollutantrose)
438
439     """
440
441     if len(var) != len(dir):
442         raise ValueError, "var and dir must have same length"
443
444     angle = 360./nsector
445
446     dir_bins = np.arange(-angle/2 ,360.+angle, angle, dtype=np.float)
447     dir_edges = dir_bins.tolist()
448     dir_edges.pop(-1)
449     dir_edges[0] = dir_edges.pop(-1)
450     dir_bins[0] = 0.
451
452     var_bins = bins.tolist()
453     var_bins.append(np.inf)
454
455     if blowto:
456         dir = dir + 180.
457         dir[dir>=360.] = dir[dir>=360.] - 360
458
459     table = histogram2d(x=var, y=dir, bins=[var_bins, dir_bins],
460                           normed=False)[0]
461     # add the last value to the first to have the table of North winds
462     table[:,0] = table[:,0] + table[:,-1]
463     # and remove the last col
464     table = table[:, :-1]
465     if normed:
466         table = table*100/table.sum()
467
468     return dir_edges, var_bins, table
469
470
471 def wrcontour(dir, var, **kwargs):
472     fig = plt.figure()
473     rect = [0.1, 0.1, 0.8, 0.8]
474     ax = WindroseAxes(fig, rect)
475     fig.add_axes(ax)
476     ax.contour(dir, var, **kwargs)
477     l = ax.legend(axespad=-0.10)
478     plt.setp(l.get_texts(), fontsize=8)
479     plt.draw()
480     plt.show()
481     return ax
482
483 def wrcontourf(dir, var, **kwargs):
484     fig = plt.figure()
485     rect = [0.1, 0.1, 0.8, 0.8]
486     ax = WindroseAxes(fig, rect)
487     fig.add_axes(ax)
488     ax.contourf(dir, var, **kwargs)
489     l = ax.legend(axespad=-0.10)
490     plt.setp(l.get_texts(), fontsize=8)
491     plt.draw()
492     plt.show()
493     return ax
494
495 def wrbox(dir, var, **kwargs):
496     fig = plt.figure()
497     rect = [0.1, 0.1, 0.8, 0.8]
498     ax = WindroseAxes(fig, rect)
499     fig.add_axes(ax)
500     ax.box(dir, var, **kwargs)
501     l = ax.legend(axespad=-0.10)
502     plt.setp(l.get_texts(), fontsize=8)
503     plt.draw()
504     plt.show()
505     return ax
506
507 def wrbar(dir, var, **kwargs):
508     fig = plt.figure()
509     rect = [0.1, 0.1, 0.8, 0.8]
510     ax = WindroseAxes(fig, rect)
511     fig.add_axes(ax)
512     ax.bar(dir, var, **kwargs)
513     l = ax.legend(axespad=-0.10)
514     plt.setp(l.get_texts(), fontsize=8)
515     plt.draw()
516     plt.show()
517     return ax
518
519 def clean(dir, var):
520     '''
521     Remove masked values in the two arrays, where if a direction data is masked,
522     the var data will also be removed in the cleaning process (and vice-versa)
523     '''
524     dirmask = dir.mask==False
525     varmask = var.mask==False
526     ind = dirmask*varmask
527     return dir[ind], var[ind]
528
529 if __name__=='__main__':
530     from pylab import figure, show, setp, random, grid, draw
531     vv=random(500)*6
532     dv=random(500)*360
533     fig = figure(figsize=(8, 8), dpi=80, facecolor='w', edgecolor='w')
534     rect = [0.1, 0.1, 0.8, 0.8]
535     ax = WindroseAxes(fig, rect, axisbg='w')
536     fig.add_axes(ax)
537
538 #    ax.contourf(dv, vv, bins=np.arange(0,8,1), cmap=cm.hot)
539 #    ax.contour(dv, vv, bins=np.arange(0,8,1), colors='k')
540 #    ax.bar(dv, vv, normed=True, opening=0.8, edgecolor='white')
541     ax.box(dv, vv, normed=True)
542     l = ax.legend(axespad=-0.10)
543     setp(l.get_texts(), fontsize=8)
544     draw()
545     #print ax._info
546     show()
547
548
549
550
551
552
Note: See TracBrowser for help on using the browser.