PageRenderTime 53ms CodeModel.GetById 27ms RepoModel.GetById 0ms app.codeStats 0ms

/metar/graphics.py

https://github.com/phobson/python-metar
Python | 237 lines | 183 code | 44 blank | 10 comment | 19 complexity | 011ba5bda92c9dd6e6c7a01fd431463d MD5 | raw file
  1. import numpy as np
  2. import matplotlib.pyplot as plt
  3. from matplotlib.ticker import FuncFormatter
  4. import matplotlib.dates as dates
  5. import pandas
  6. __all__ = ['hyetograph', 'rainClock', 'windRose', 'psychromograph',
  7. 'temperaturePlot']
  8. def _resampler(dataframe, col, freq, how='sum', fillna=None):
  9. rules = {
  10. '5min': ('5Min', 'line'),
  11. '5 min': ('5Min', 'line'),
  12. '5-min': ('5Min', 'line'),
  13. '5 minute': ('5Min', 'line'),
  14. '5-minute': ('5Min', 'line'),
  15. '15min': ('15Min', 'line'),
  16. '15 min': ('15Min', 'line'),
  17. '15-min': ('15Min', 'line'),
  18. '15 minute': ('15Min', 'line'),
  19. '15-minute': ('15Min', 'line'),
  20. '30min': ('30Min', 'line'),
  21. '30 min': ('30Min', 'line'),
  22. '30-min': ('30Min', 'line'),
  23. '30 minute': ('30Min', 'line'),
  24. '30-minute': ('30Min', 'line'),
  25. 'hour': ('H', 'line'),
  26. 'hourly': ('H', 'line'),
  27. 'day': ('D', 'line'),
  28. 'daily': ('D', 'line'),
  29. 'week': ('W', 'line'),
  30. 'weekly': ('W', 'line'),
  31. 'month': ('M', 'line'),
  32. 'monthly': ('M', 'line')
  33. }
  34. if freq not in list(rules.keys()):
  35. m = "freq should be in ['5-min', '15-'min', hourly', 'daily', 'weekly, 'monthly']"
  36. raise ValueError(m)
  37. rule = rules[freq.lower()][0]
  38. plotkind = rules[freq.lower()][1]
  39. data = dataframe[col].resample(how=how, rule=rule)
  40. if fillna is not None:
  41. data.fillna(value=fillna, inplace=True)
  42. return data, rule, plotkind
  43. def _plotter(dataframe, col, ylabel, freq='hourly', how='sum',
  44. ax=None, downward=False, fname=None, fillna=None):
  45. if not hasattr(dataframe, col):
  46. raise ValueError('input `dataframe` must have a `%s` column' % col)
  47. if ax is None:
  48. fig, ax = plt.subplots()
  49. else:
  50. fig = ax.figure
  51. data, rule, plotkind = _resampler(dataframe, col, freq=freq, how=how)
  52. data.plot(ax=ax, kind=plotkind)
  53. if rule == 'A':
  54. xformat = dates.DateFormatter('%Y')
  55. ax.xaxis.set_major_formatter(xformat)
  56. elif rule == 'M':
  57. xformat = dates.DateFormatter('%Y-%m')
  58. ax.xaxis.set_major_formatter(xformat)
  59. ax.tick_params(axis='x', labelsize=8)
  60. ax.set_xlabel('Date')
  61. ax.set_ylabel(ylabel)
  62. if downward:
  63. ax.invert_yaxis()
  64. if fname is not None:
  65. fig.tight_layout()
  66. fig.savefig(fname, dpi=300, bbox_inches='tight')
  67. return fig
  68. def hyetograph(dataframe, freq='hourly', ax=None, downward=True, col='Precip', fname=None):
  69. ylabel = '%s Rainfall Depth (in)' % freq.title()
  70. fig = _plotter(dataframe, col, ylabel, freq=freq, fillna=0,
  71. how='sum', ax=ax, downward=downward, fname=fname)
  72. return fig
  73. def psychromograph(dataframe, freq='hourly', ax=None, col='AtmPress', fname=None):
  74. ylabel = '%s Barometric Pressure (in Hg)' % freq.title()
  75. fig = _plotter(dataframe, col, ylabel, freq=freq,
  76. how='mean', ax=ax, fname=fname)
  77. return fig
  78. def temperaturePlot(dataframe, freq='hourly', ax=None, col='Temp', fname=None):
  79. ylabel = u'%s Temperature (\xB0C)' % freq.title()
  80. fig = _plotter(dataframe, col, ylabel, freq=freq,
  81. how='mean', ax=ax, fname=fname)
  82. return fig
  83. def rainClock(dataframe, raincol='Precip', fname=None):
  84. '''
  85. Mathematically dubious representation of the likelihood of rain at
  86. at any hour given that will rain.
  87. '''
  88. if not hasattr(dataframe, raincol):
  89. raise ValueError('input `dataframe` must have a `%s` column' % raincol)
  90. rainfall = dataframe[raincol]
  91. am_hours = np.arange(0, 12)
  92. am_hours[0] = 12
  93. rainhours = rainfall.index.hour
  94. rain_by_hour = []
  95. for hr in np.arange(24):
  96. selector = (rainhours == hr)
  97. total_depth = rainfall[selector].sum()
  98. num_obervations = rainfall[selector].count()
  99. rain_by_hour.append(total_depth/num_obervations)
  100. bar_width = 2*np.pi/12 * 0.8
  101. fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(7, 3),
  102. subplot_kw=dict(polar=True))
  103. theta = np.arange(0.0, 2*np.pi, 2*np.pi/12)
  104. ax1.bar(theta + 2*np.pi/12 * 0.1, rain_by_hour[:12],
  105. bar_width, color='DodgerBlue', linewidth=0.5)
  106. ax2.bar(theta + 2*np.pi/12 * 0.1, rain_by_hour[12:],
  107. bar_width, color='Crimson', linewidth=0.5)
  108. ax1.set_title('AM Hours')
  109. ax2.set_title('PM Hours')
  110. for ax in [ax1, ax2]:
  111. ax.set_theta_zero_location("N")
  112. ax.set_theta_direction('clockwise')
  113. ax.set_xticks(theta)
  114. ax.set_xticklabels(am_hours)
  115. ax.set_yticklabels([])
  116. if fname is not None:
  117. fig.tight_layout()
  118. fig.savefig(fname, dpi=300, bbox_inches='tight')
  119. return fig
  120. def windRose(dataframe, speedcol='WindSpd', dircol='WindDir', mph=False,
  121. fname=None):
  122. '''
  123. Plots a Wind Rose. Feed it a dataframe with 'WindSpd' (knots) and
  124. 'WindDir' degrees clockwise from north (columns)
  125. '''
  126. if not hasattr(dataframe, speedcol):
  127. raise ValueError('input `dataframe` must have a `%s` column' % speedcol)
  128. if not hasattr(dataframe, dircol):
  129. raise ValueError('input `dataframe` must have a `%s` column' % dircol)
  130. # set up the figure
  131. fig, ax1 = plt.subplots(figsize=(6, 6), subplot_kw=dict(polar=True))
  132. ax1.xaxis.grid(True, which='major', linestyle='-', alpha='0.125', zorder=0)
  133. ax1.yaxis.grid(True, which='major', linestyle='-', alpha='0.125', zorder=0)
  134. ax1.set_theta_zero_location("N")
  135. ax1.set_theta_direction('clockwise')
  136. # speed bins and colors
  137. speedBins = [40, 30, 20, 10, 5]
  138. colors = ['#990000', '#FF4719', '#FFCC00', '#579443', '#0066FF']
  139. # number of total and zero-wind observations
  140. total = np.float(dataframe.shape[0])
  141. if mph:
  142. factor = 1.15
  143. units = 'mph'
  144. else:
  145. factor = 1
  146. units = 'kt'
  147. calm = np.float(dataframe[dataframe[speedcol] == 0].shape[0])/total * 100
  148. # loop through the speed bins
  149. for spd, clr in zip(speedBins, colors):
  150. barLen = _get_wind_counts(dataframe, spd, speedcol, dircol, factor=factor)
  151. barLen = barLen/total
  152. barDir, barWidth = _convert_dir_to_left_radian(np.array(barLen.index))
  153. ax1.bar(barDir, barLen, width=barWidth, linewidth=0.50,
  154. edgecolor=(0.25, 0.25, 0.25), color=clr, alpha=0.8,
  155. label=r"<%d %s" % (spd, units))
  156. # format the plot's axes
  157. ax1.legend(loc='lower right', bbox_to_anchor=(1.10, -0.13), fontsize=8)
  158. ax1.set_xticklabels(['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'])
  159. ax1.xaxis.grid(True, which='major', color='k', alpha=0.5)
  160. ax1.yaxis.grid(True, which='major', color='k', alpha=0.5)
  161. ax1.yaxis.set_major_formatter(FuncFormatter(_pct_fmt))
  162. fig.text(0.05, 0.95, 'Calm Winds: %0.1f%%' % calm)
  163. #if calm >= 0.1:
  164. # ax1.set_ylim(ymin=np.floor(calm*10)/10.)
  165. if fname is not None:
  166. fig.tight_layout()
  167. fig.savefig(fname, dpi=300, bbox_inches='tight')
  168. return fig
  169. def _get_wind_counts(dataframe, maxSpeed, speedcol, dircol, factor=1):
  170. group = dataframe[dataframe[speedcol]*factor < maxSpeed].groupby(by=dircol)
  171. counts = group.size()
  172. return counts[counts.index != 0]
  173. def _pct_fmt(x, pos=0):
  174. return '%0.1f%%' % (100*x)
  175. def _convert_dir_to_left_radian(directions):
  176. N = directions.shape[0]
  177. barDir = directions * np.pi/180. - np.pi/N
  178. barWidth = [2 * np.pi / N]*N
  179. return barDir, barWidth
  180. def degrees2radians(degrees):
  181. return degrees * np.pi / 180.0
  182. def radians2degrees(radians):
  183. return radians * 180 / np.pi
  184. def avgDirection(directions):
  185. radians = degrees2radians(directions)
  186. cos = np.cos(radians).sum()
  187. sin = np.sin(radians).sum()
  188. degree = radians2degrees(np.arctan2(sin, cos))
  189. return degree if degree > 0 else degree + 360