1
|
|
|
# Copyright (c) 2017 MetPy Developers. |
2
|
|
|
# Distributed under the terms of the BSD 3-Clause License. |
3
|
|
|
# SPDX-License-Identifier: BSD-3-Clause |
4
|
|
|
""" |
5
|
|
|
Meteogram |
6
|
|
|
========= |
7
|
|
|
|
8
|
|
|
Plots time series data as a meteogram. |
9
|
|
|
""" |
10
|
|
|
|
11
|
|
|
import datetime as dt |
12
|
|
|
|
13
|
|
|
import matplotlib as mpl |
14
|
|
|
import matplotlib.pyplot as plt |
15
|
|
|
import numpy as np |
16
|
|
|
|
17
|
|
|
from metpy.calc import dewpoint_rh |
18
|
|
|
from metpy.cbook import get_test_data |
19
|
|
|
from metpy.units import units |
20
|
|
|
|
21
|
|
|
|
22
|
|
|
def calc_mslp(t, p, h): |
23
|
|
|
return p * (1 - (0.0065 * h) / (t + 0.0065 * h + 273.15)) ** (-5.257) |
24
|
|
|
|
25
|
|
|
|
26
|
|
|
# Make meteogram plot |
27
|
|
|
class Meteogram(object): |
28
|
|
|
""" Plot a time series of meteorological data from a particular station as a |
29
|
|
|
meteogram with standard variables to visualize, including thermodynamic, |
30
|
|
|
kinematic, and pressure. The functions below control the plotting of each |
31
|
|
|
variable. |
32
|
|
|
TO DO: Make the subplot creation dynamic so the number of rows is not |
33
|
|
|
static as it is currently. """ |
34
|
|
|
|
35
|
|
|
def __init__(self, fig, dates, probeid, time=None, axis=0): |
36
|
|
|
""" |
37
|
|
|
Required input: |
38
|
|
|
fig: figure object |
39
|
|
|
dates: array of dates corresponding to the data |
40
|
|
|
probeid: ID of the station |
41
|
|
|
Optional Input: |
42
|
|
|
time: Time the data is to be plotted |
43
|
|
|
axis: number that controls the new axis to be plotted (FOR FUTURE) |
44
|
|
|
""" |
45
|
|
|
if not time: |
46
|
|
|
time = dt.datetime.utcnow() |
47
|
|
|
self.start = dates[0] |
48
|
|
|
self.fig = fig |
49
|
|
|
self.end = dates[-1] |
50
|
|
|
self.axis_num = 0 |
51
|
|
|
self.dates = mpl.dates.date2num(dates) |
52
|
|
|
self.time = time.strftime('%Y-%m-%d %H:%M UTC') |
53
|
|
|
self.title = 'Latest Ob Time: {0}\nProbe ID: {1}'.format(self.time, probeid) |
54
|
|
|
|
55
|
|
|
def plot_winds(self, ws, wd, wsmax, plot_range=None): |
56
|
|
|
""" |
57
|
|
|
Required input: |
58
|
|
|
ws: Wind speeds (knots) |
59
|
|
|
wd: Wind direction (degrees) |
60
|
|
|
wsmax: Wind gust (knots) |
61
|
|
|
Optional Input: |
62
|
|
|
plot_range: Data range for making figure (list of (min,max,step)) |
63
|
|
|
""" |
64
|
|
|
# PLOT WIND SPEED AND WIND DIRECTION |
65
|
|
|
self.ax1 = fig.add_subplot(4, 1, 1) |
66
|
|
|
ln1 = self.ax1.plot(self.dates, ws, label='Wind Speed') |
67
|
|
|
plt.fill_between(self.dates, ws, 0) |
68
|
|
|
self.ax1.set_xlim(self.start, self.end) |
69
|
|
|
if not plot_range: |
70
|
|
|
plot_range = [0, 20, 1] |
71
|
|
|
plt.ylabel('Wind Speed (knots)', multialignment='center') |
72
|
|
|
self.ax1.set_ylim(plot_range[0], plot_range[1], plot_range[2]) |
73
|
|
|
plt.grid(b=True, which='major', axis='y', color='k', linestyle='--', linewidth=0.5) |
74
|
|
|
ln2 = self.ax1.plot(self.dates, |
75
|
|
|
wsmax, |
76
|
|
|
'.r', |
77
|
|
|
label='3-sec Wind Speed Max') |
78
|
|
|
plt.setp(self.ax1.get_xticklabels(), visible=True) |
79
|
|
|
ax7 = self.ax1.twinx() |
80
|
|
|
ln3 = ax7.plot(self.dates, |
81
|
|
|
wd, |
82
|
|
|
'.k', |
83
|
|
|
linewidth=0.5, |
84
|
|
|
label='Wind Direction') |
85
|
|
|
plt.ylabel('Wind\nDirection\n(degrees)', multialignment='center') |
86
|
|
|
plt.ylim(0, 360) |
87
|
|
|
plt.yticks(np.arange(45, 405, 90), ['NE', 'SE', 'SW', 'NW']) |
88
|
|
|
lns = ln1 + ln2 + ln3 |
89
|
|
|
labs = [l.get_label() for l in lns] |
90
|
|
|
plt.gca().xaxis.set_major_formatter(mpl.dates.DateFormatter('%d/%H UTC')) |
91
|
|
|
ax7.legend(lns, labs, loc='upper center', |
92
|
|
|
bbox_to_anchor=(0.5, 1.2), ncol=3, prop={'size': 12}) |
93
|
|
|
|
94
|
|
|
def plot_thermo(self, t, td, plot_range=None): |
95
|
|
|
""" |
96
|
|
|
Required input: |
97
|
|
|
T: Temperature (deg F) |
98
|
|
|
TD: Dewpoint (deg F) |
99
|
|
|
Optional Input: |
100
|
|
|
plot_range: Data range for making figure (list of (min,max,step)) |
101
|
|
|
""" |
102
|
|
|
# PLOT TEMPERATURE AND DEWPOINT |
103
|
|
|
if not plot_range: |
104
|
|
|
plot_range = [10, 90, 2] |
105
|
|
|
self.ax2 = fig.add_subplot(4, 1, 2, sharex=self.ax1) |
106
|
|
|
ln4 = self.ax2.plot(self.dates, |
107
|
|
|
t, |
108
|
|
|
'r-', |
109
|
|
|
label='Temperature') |
110
|
|
|
plt.fill_between(self.dates, |
111
|
|
|
t, |
112
|
|
|
td, |
113
|
|
|
color='r') |
114
|
|
|
plt.setp(self.ax2.get_xticklabels(), visible=True) |
115
|
|
|
plt.ylabel('Temperature\n(F)', multialignment='center') |
116
|
|
|
plt.grid(b=True, which='major', axis='y', color='k', linestyle='--', linewidth=0.5) |
117
|
|
|
self.ax2.set_ylim(plot_range[0], plot_range[1], plot_range[2]) |
118
|
|
|
ln5 = self.ax2.plot(self.dates, |
119
|
|
|
td, |
120
|
|
|
'g-', |
121
|
|
|
label='Dewpoint') |
122
|
|
|
plt.fill_between(self.dates, |
123
|
|
|
td, |
124
|
|
|
plt.ylim()[0], |
125
|
|
|
color='g') |
126
|
|
|
ax_twin = self.ax2.twinx() |
127
|
|
|
# ax_twin.set_ylim(20,90,2) |
128
|
|
|
ax_twin.set_ylim(plot_range[0], plot_range[1], plot_range[2]) |
129
|
|
|
lns = ln4 + ln5 |
130
|
|
|
labs = [l.get_label() for l in lns] |
131
|
|
|
plt.gca().xaxis.set_major_formatter(mpl.dates.DateFormatter('%d/%H UTC')) |
132
|
|
|
|
133
|
|
|
self.ax2.legend(lns, labs, loc='upper center', |
134
|
|
|
bbox_to_anchor=(0.5, 1.2), ncol=2, prop={'size': 12}) |
135
|
|
|
|
136
|
|
|
def plot_rh(self, rh, plot_range=None): |
137
|
|
|
""" |
138
|
|
|
Required input: |
139
|
|
|
RH: Relative humidity (%) |
140
|
|
|
Optional Input: |
141
|
|
|
plot_range: Data range for making figure (list of (min,max,step)) |
142
|
|
|
""" |
143
|
|
|
# PLOT RELATIVE HUMIDITY |
144
|
|
|
if not plot_range: |
145
|
|
|
plot_range = [0, 100, 4] |
146
|
|
|
self.ax3 = fig.add_subplot(4, 1, 3, sharex=self.ax1) |
147
|
|
|
self.ax3.plot(self.dates, |
148
|
|
|
rh, |
149
|
|
|
'g-', |
150
|
|
|
label='Relative Humidity') |
151
|
|
|
self.ax3.legend(loc='upper center', bbox_to_anchor=(0.5, 1.22), prop={'size': 12}) |
152
|
|
|
plt.setp(self.ax3.get_xticklabels(), visible=True) |
153
|
|
|
plt.grid(b=True, which='major', axis='y', color='k', linestyle='--', linewidth=0.5) |
154
|
|
|
self.ax3.set_ylim(plot_range[0], plot_range[1], plot_range[2]) |
155
|
|
|
plt.fill_between(self.dates, rh, plt.ylim()[0], color='g') |
156
|
|
|
plt.ylabel('Relative Humidity\n(%)', multialignment='center') |
157
|
|
|
plt.gca().xaxis.set_major_formatter(mpl.dates.DateFormatter('%d/%H UTC')) |
158
|
|
|
axtwin = self.ax3.twinx() |
159
|
|
|
axtwin.set_ylim(plot_range[0], plot_range[1], plot_range[2]) |
160
|
|
|
|
161
|
|
|
def plot_pressure(self, p, plot_range=None): |
162
|
|
|
""" |
163
|
|
|
Required input: |
164
|
|
|
P: Mean Sea Level Pressure (hPa) |
165
|
|
|
Optional Input: |
166
|
|
|
plot_range: Data range for making figure (list of (min,max,step)) |
167
|
|
|
""" |
168
|
|
|
# PLOT PRESSURE |
169
|
|
|
if not plot_range: |
170
|
|
|
plot_range = [970, 1030, 2] |
171
|
|
|
self.ax4 = fig.add_subplot(4, 1, 4, sharex=self.ax1) |
172
|
|
|
self.ax4.plot(self.dates, |
173
|
|
|
p, |
174
|
|
|
'm', |
175
|
|
|
label='Mean Sea Level Pressure') |
176
|
|
|
plt.ylabel('Mean Sea\nLevel Pressure\n(mb)', multialignment='center') |
177
|
|
|
plt.ylim(plot_range[0], plot_range[1], plot_range[2]) |
178
|
|
|
axtwin = self.ax4.twinx() |
179
|
|
|
axtwin.set_ylim(plot_range[0], plot_range[1], plot_range[2]) |
180
|
|
|
plt.fill_between(self.dates, p, plt.ylim()[0], color='m') |
181
|
|
|
plt.gca().xaxis.set_major_formatter(mpl.dates.DateFormatter('%d/%H UTC')) |
182
|
|
|
self.ax4.legend(loc='upper center', bbox_to_anchor=(0.5, 1.2), prop={'size': 12}) |
183
|
|
|
plt.grid(b=True, which='major', axis='y', color='k', linestyle='--', linewidth=0.5) |
184
|
|
|
plt.setp(self.ax4.get_xticklabels(), visible=True) |
185
|
|
|
# OTHER OPTIONAL AXES TO PLOT |
186
|
|
|
# plot_irradiance |
187
|
|
|
# plot_precipitation |
188
|
|
|
|
189
|
|
|
|
190
|
|
|
# set the starttime and endtime for plotting, 24 hour range |
191
|
|
|
endtime = dt.datetime(2016, 3, 31, 22, 0, 0, 0) |
192
|
|
|
starttime = endtime - dt.timedelta(hours=24) |
193
|
|
|
|
194
|
|
|
# Height of the station to calculate MSLP |
195
|
|
|
hgt_example = 292. |
196
|
|
|
|
197
|
|
|
|
198
|
|
|
# Parse dates from .csv file, knowing their format as a string and convert to datetime |
199
|
|
|
def parse_date(date): |
200
|
|
|
return dt.datetime.strptime(date.decode('ascii'), '%Y-%m-%d %H:%M:%S') |
201
|
|
|
|
202
|
|
|
|
203
|
|
|
testdata = np.genfromtxt(get_test_data('timeseries.csv', False), names=True, dtype=None, |
204
|
|
|
usecols=list(range(1, 8)), |
205
|
|
|
converters={'DATE': parse_date}, delimiter=',') |
206
|
|
|
|
207
|
|
|
# Temporary variables for ease |
208
|
|
|
temp = testdata['T'] |
209
|
|
|
pres = testdata['P'] |
210
|
|
|
rh = testdata['RH'] |
211
|
|
|
ws = testdata['WS'] |
212
|
|
|
wsmax = testdata['WSMAX'] |
213
|
|
|
wd = testdata['WD'] |
214
|
|
|
date = testdata['DATE'] |
215
|
|
|
|
216
|
|
|
# ID For Plotting on Meteogram |
217
|
|
|
probe_id = '0102A' |
218
|
|
|
|
219
|
|
|
data = {'wind_speed': (np.array(ws) * units('m/s')).to(units('knots')), |
220
|
|
|
'wind_speed_max': (np.array(wsmax) * units('m/s')).to(units('knots')), |
221
|
|
|
'wind_direction': np.array(wd) * units('degrees'), |
222
|
|
|
'dewpoint': dewpoint_rh((np.array(temp) * units('degC')).to(units('K')), |
223
|
|
|
np.array(rh) / 100.).to(units('degF')), |
224
|
|
|
'air_temperature': (np.array(temp) * units('degC')).to(units('degF')), |
225
|
|
|
'mean_slp': calc_mslp(np.array(temp), np.array(pres), hgt_example) * units('hPa'), |
226
|
|
|
'relative_humidity': np.array(rh), 'times': np.array(date)} |
227
|
|
|
|
228
|
|
|
fig = plt.figure(figsize=(20, 16)) |
229
|
|
|
meteogram = Meteogram(fig, data['times'], probe_id) |
230
|
|
|
meteogram.plot_winds(data['wind_speed'], data['wind_direction'], data['wind_speed_max']) |
231
|
|
|
meteogram.plot_thermo(data['air_temperature'], data['dewpoint']) |
232
|
|
|
meteogram.plot_rh(data['relative_humidity']) |
233
|
|
|
meteogram.plot_pressure(data['mean_slp']) |
234
|
|
|
fig.subplots_adjust(hspace=0.5) |
235
|
|
|
plt.show() |
236
|
|
|
|