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

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

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

Add windrose.

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