AeroViz 0.1.7__py3-none-any.whl → 0.1.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of AeroViz might be problematic. Click here for more details.

@@ -0,0 +1,260 @@
1
+ from typing import Sequence, Literal
2
+
3
+ import numpy as np
4
+ import pandas as pd
5
+ from numpy import exp, log, log10, sqrt, pi
6
+
7
+ from .PyMieScatt_update import AutoMieQ
8
+
9
+
10
+ def Mie_Q(m: complex,
11
+ wavelength: float,
12
+ dp: float | Sequence[float]
13
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
14
+ """
15
+ Calculate Mie scattering efficiency (Q) for given spherical particle diameter(s).
16
+
17
+ Parameters
18
+ ----------
19
+ m : complex
20
+ The complex refractive index of the particles.
21
+ wavelength : float
22
+ The wavelength of the incident light (in nm).
23
+ dp : float | Sequence[float]
24
+ Particle diameters (in nm), can be a single value or Sequence object.
25
+
26
+ Returns
27
+ -------
28
+ Q_ext : ndarray
29
+ The Mie extinction efficiency for each particle diameter.
30
+ Q_sca : ndarray
31
+ The Mie scattering efficiency for each particle diameter.
32
+ Q_abs : ndarray
33
+ The Mie absorption efficiency for each particle diameter.
34
+
35
+ Examples
36
+ --------
37
+ >>> Q_ext, Q_sca, Q_abs = Mie_Q(m=complex(1.5, 0.02), wavelength=550, dp=[100, 200, 300, 400])
38
+ """
39
+ # Ensure dp is a numpy array
40
+ dp = np.atleast_1d(dp)
41
+
42
+ # Transpose for proper unpacking
43
+ Q_ext, Q_sca, Q_abs, g, Q_pr, Q_back, Q_ratio = np.array([AutoMieQ(m, wavelength, _dp) for _dp in dp]).T
44
+
45
+ return Q_ext, Q_sca, Q_abs
46
+
47
+
48
+ def Mie_MEE(m: complex,
49
+ wavelength: float,
50
+ dp: float | Sequence[float],
51
+ density: float
52
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
53
+ """
54
+ Calculate mass extinction efficiency and other parameters.
55
+
56
+ Parameters
57
+ ----------
58
+ m : complex
59
+ The complex refractive index of the particles.
60
+ wavelength : float
61
+ The wavelength of the incident light.
62
+ dp : float | Sequence[float]
63
+ List of particle sizes or a single value.
64
+ density : float
65
+ The density of particles.
66
+
67
+ Returns
68
+ -------
69
+ MEE : ndarray
70
+ The mass extinction efficiency for each particle diameter.
71
+ MSE : ndarray
72
+ The mass scattering efficiency for each particle diameter.
73
+ MAE : ndarray
74
+ The mass absorption efficiency for each particle diameter.
75
+
76
+ Examples
77
+ --------
78
+ >>> MEE, MSE, MAE = Mie_MEE(m=complex(1.5, 0.02), wavelength=550, dp=[100, 200, 300, 400], density=1.2)
79
+ """
80
+ Q_ext, Q_sca, Q_abs = Mie_Q(m, wavelength, dp)
81
+
82
+ MEE = (3 * Q_ext) / (2 * density * dp) * 1000
83
+ MSE = (3 * Q_sca) / (2 * density * dp) * 1000
84
+ MAE = (3 * Q_abs) / (2 * density * dp) * 1000
85
+
86
+ return MEE, MSE, MAE
87
+
88
+
89
+ def Mie_PESD(m: complex,
90
+ wavelength: float = 550,
91
+ dp: float | Sequence[float] = None,
92
+ ndp: float | Sequence[float] = None,
93
+ lognormal: bool = False,
94
+ dp_range: tuple = (1, 2500),
95
+ geoMean: float = 200,
96
+ geoStdDev: float = 2,
97
+ numberOfParticles: float = 1e6,
98
+ numberOfBins: int = 167,
99
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
100
+ """
101
+ Simultaneously calculate "extinction distribution" and "integrated results" using the Mie_Q method.
102
+
103
+ Parameters
104
+ ----------
105
+ m : complex
106
+ The complex refractive index of the particles.
107
+ wavelength : float
108
+ The wavelength of the incident light.
109
+ dp : float | Sequence[float]
110
+ Particle sizes.
111
+ ndp : float | Sequence[float]
112
+ Number concentration from SMPS or APS in the units of dN/dlogdp.
113
+ lognormal : bool, optional
114
+ Whether to use lognormal distribution for ndp. Default is False.
115
+ dp_range : tuple, optional
116
+ Range of particle sizes. Default is (1, 2500) nm.
117
+ geoMean : float, optional
118
+ Geometric mean of the particle size distribution. Default is 200 nm.
119
+ geoStdDev : float, optional
120
+ Geometric standard deviation of the particle size distribution. Default is 2.
121
+ numberOfParticles : float, optional
122
+ Number of particles. Default is 1e6.
123
+ numberOfBins : int, optional
124
+ Number of bins for the lognormal distribution. Default is 167.
125
+
126
+ Returns
127
+ -------
128
+ ext_dist : ndarray
129
+ The extinction distribution for the given data.
130
+ sca_dist : ndarray
131
+ The scattering distribution for the given data.
132
+ abs_dist : ndarray
133
+ The absorption distribution for the given data.
134
+
135
+ Notes
136
+ -----
137
+ return in "dext/dlogdp", please make sure input the dNdlogdp data.
138
+
139
+ Examples
140
+ --------
141
+ >>> Ext, Sca, Abs = Mie_PESD(m=complex(1.5, 0.02), wavelength=550, dp=[100, 200, 500, 1000], ndp=[100, 50, 30, 20])
142
+ """
143
+ if lognormal:
144
+ dp = np.logspace(log10(dp_range[0]), log10(dp_range[1]), numberOfBins)
145
+
146
+ ndp = numberOfParticles * (1 / (log(geoStdDev) * sqrt(2 * pi)) *
147
+ exp(-(log(dp) - log(geoMean)) ** 2 / (2 * log(geoStdDev) ** 2)))
148
+
149
+ # dN / dlogdp
150
+ ndp = np.atleast_1d(ndp)
151
+
152
+ Q_ext, Q_sca, Q_abs = Mie_Q(m, wavelength, dp)
153
+
154
+ # The 1e-6 here is so that the final value is the same as the unit 1/10^6m.
155
+ Ext = Q_ext * (pi / 4 * dp ** 2) * ndp * 1e-6
156
+ Sca = Q_sca * (pi / 4 * dp ** 2) * ndp * 1e-6
157
+ Abs = Q_abs * (pi / 4 * dp ** 2) * ndp * 1e-6
158
+
159
+ return Ext, Sca, Abs
160
+
161
+
162
+ def internal(dist: pd.Series,
163
+ dp: float | Sequence[float],
164
+ wavelength: float = 550,
165
+ result_type: Literal['extinction', 'scattering', 'absorption'] = 'extinction'
166
+ ) -> np.ndarray:
167
+ """
168
+ Calculate the extinction distribution by internal mixing model.
169
+
170
+ Parameters
171
+ ----------
172
+ dist : pd.Series
173
+ Particle size distribution data.
174
+ dp : float | Sequence[float]
175
+ Diameter(s) of the particles, either a single value or a sequence.
176
+ wavelength : float, optional
177
+ Wavelength of the incident light, default is 550 nm.
178
+ result_type : {'extinction', 'scattering', 'absorption'}, optional
179
+ Type of result to calculate, defaults to 'extinction'.
180
+
181
+ Returns
182
+ -------
183
+ np.ndarray
184
+ Extinction distribution calculated based on the internal mixing model.
185
+ """
186
+ ext_dist, sca_dist, abs_dist = Mie_PESD(m=complex(dist['n_amb'], dist['k_amb']),
187
+ wavelength=wavelength,
188
+ dp=dp,
189
+ ndp=np.array(dist[:np.size(dp)]))
190
+
191
+ if result_type == 'extinction':
192
+ return ext_dist
193
+ elif result_type == 'scattering':
194
+ return sca_dist
195
+ else:
196
+ return abs_dist
197
+
198
+
199
+ # return dict(ext=ext_dist, sca=sca_dist, abs=abs_dist)
200
+
201
+
202
+ def external(dist: pd.Series,
203
+ dp: float | Sequence[float],
204
+ wavelength: float = 550,
205
+ result_type: Literal['extinction', 'scattering', 'absorption'] = 'extinction'
206
+ ) -> np.ndarray:
207
+ """
208
+ Calculate the extinction distribution by external mixing model.
209
+
210
+ Parameters
211
+ ----------
212
+ dist : pd.Series
213
+ Particle size distribution data.
214
+ dp : float | Sequence[float]
215
+ Diameter(s) of the particles, either a single value or a sequence.
216
+ wavelength : float, optional
217
+ Wavelength of the incident light, default is 550 nm.
218
+ result_type : {'extinction', 'scattering', 'absorption'}, optional
219
+ Type of result to calculate, defaults to 'extinction'.
220
+
221
+ Returns
222
+ -------
223
+ np.ndarray
224
+ Extinction distribution calculated based on the external mixing model.
225
+ """
226
+ refractive_dic = {'AS_volume_ratio': complex(1.53, 0.00),
227
+ 'AN_volume_ratio': complex(1.55, 0.00),
228
+ 'OM_volume_ratio': complex(1.54, 0.00),
229
+ 'Soil_volume_ratio': complex(1.56, 0.01),
230
+ 'SS_volume_ratio': complex(1.54, 0.00),
231
+ 'EC_volume_ratio': complex(1.80, 0.54),
232
+ 'ALWC_volume_ratio': complex(1.33, 0.00)}
233
+
234
+ ndp = np.array(dist[:np.size(dp)])
235
+ mie_results = (
236
+ Mie_PESD(refractive_dic[_specie], wavelength, dp, dist[_specie] / (1 + dist['ALWC_volume_ratio']) * ndp) for
237
+ _specie in refractive_dic)
238
+
239
+ ext_dist, sca_dist, abs_dist = (np.sum([res[0] for res in mie_results], axis=0),
240
+ np.sum([res[1] for res in mie_results], axis=0),
241
+ np.sum([res[2] for res in mie_results], axis=0))
242
+
243
+ if result_type == 'extinction':
244
+ return ext_dist
245
+ elif result_type == 'scattering':
246
+ return sca_dist
247
+ else:
248
+ return abs_dist
249
+
250
+
251
+ def core_shell():
252
+ pass
253
+
254
+
255
+ def sensitivity():
256
+ pass
257
+
258
+
259
+ if __name__ == '__main__':
260
+ result = Mie_Q(m=complex(1.5, 0.02), wavelength=550, dp=[100., 200.])
@@ -6,13 +6,14 @@ import numpy as np
6
6
  # from PyMieScatt import ScatteringFunction
7
7
  from matplotlib.pyplot import Figure, Axes
8
8
 
9
+ from AeroViz.plot.optical.PyMieScatt_update import ScatteringFunction
10
+ from AeroViz.plot.optical.mie_theory import Mie_Q, Mie_MEE, Mie_PESD
9
11
  from AeroViz.plot.utils import *
10
- from temp.process.method.mie_theory import Mie_Q, Mie_MEE, Mie_PESD
11
12
 
12
13
  __all__ = ['Q_plot',
13
14
  'RI_couple',
14
15
  'RRI_2D',
15
- # 'scattering_phase',
16
+ 'scattering_phase',
16
17
  'response_surface',
17
18
  ]
18
19
 
@@ -252,59 +253,59 @@ def RRI_2D(mode: Literal["ext", "sca", "abs"] = 'ext',
252
253
  return fig, ax
253
254
 
254
255
 
255
- # @set_figure
256
- # def scattering_phase(m: complex = 1.55 + 0.01j,
257
- # wave: float = 600,
258
- # dp: float = 200) -> tuple[Figure, Axes]:
259
- # """
260
- # Generate a polar plot to visualize the scattering phase function.
261
- #
262
- # Parameters
263
- # ----------
264
- # m : complex, optional
265
- # The complex refractive index of the scattering medium. Default is 1.55 + 0.01j.
266
- # wave : float, optional
267
- # The wavelength of the incident light in nanometers. Default is 600 nm.
268
- # dp : float, optional
269
- # The particle diameter in nanometers. Default is 200 nm.
270
- #
271
- # Returns
272
- # -------
273
- # ax : Axes
274
- # Matplotlib Axes object containing the generated polar plot.
275
- #
276
- # Examples
277
- # --------
278
- # Example usage of the scattering_phase function:
279
- #
280
- # >>> ax = scattering_phase(m=1.55 + 0.01j, wave=600, dp=200)
281
- # """
282
- # theta, _SL, _SR, _SU = ScatteringFunction(m, wave, dp)
283
- #
284
- # SL = np.append(_SL, _SL[::-1])
285
- # SR = np.append(_SR, _SR[::-1])
286
- # SU = np.append(_SU, _SU[::-1])
287
- #
288
- # angles = ['0', '60', '120', '180', '240', '300']
289
- #
290
- # fig, ax = plt.subplots(subplot_kw={'projection': 'polar'})
291
- #
292
- # theta = np.linspace(0, 2 * np.pi, len(SL))
293
- #
294
- # plt.thetagrids(range(0, 360, int(360 / len(angles))), angles)
295
- #
296
- # plt.plot(theta, SL, '-', linewidth=2, color='#115162', label='SL')
297
- # plt.fill(theta, SL, '#afe0f5', alpha=0.5)
298
- # plt.plot(theta, SR, '-', linewidth=2, color='#7FAE80', label='SR')
299
- # plt.fill(theta, SR, '#b5e6c5', alpha=0.5)
300
- # plt.plot(theta, SU, '-', linewidth=2, color='#621129', label='SU')
301
- # plt.fill(theta, SU, '#f5afbd', alpha=0.5)
302
- #
303
- # plt.legend(loc='best', bbox_to_anchor=(1, 0, 0.2, 1), prop={'weight': 'bold'})
304
- # plt.title(r'$\bf Scattering\ phase\ function$')
305
- #
306
- # plt.show()
307
- # return fig, ax
256
+ @set_figure
257
+ def scattering_phase(m: complex = 1.55 + 0.01j,
258
+ wave: float = 600,
259
+ dp: float = 200) -> tuple[Figure, Axes]:
260
+ """
261
+ Generate a polar plot to visualize the scattering phase function.
262
+
263
+ Parameters
264
+ ----------
265
+ m : complex, optional
266
+ The complex refractive index of the scattering medium. Default is 1.55 + 0.01j.
267
+ wave : float, optional
268
+ The wavelength of the incident light in nanometers. Default is 600 nm.
269
+ dp : float, optional
270
+ The particle diameter in nanometers. Default is 200 nm.
271
+
272
+ Returns
273
+ -------
274
+ ax : Axes
275
+ Matplotlib Axes object containing the generated polar plot.
276
+
277
+ Examples
278
+ --------
279
+ Example usage of the scattering_phase function:
280
+
281
+ >>> ax = scattering_phase(m=1.55 + 0.01j, wave=600, dp=200)
282
+ """
283
+ theta, _SL, _SR, _SU = ScatteringFunction(m, wave, dp)
284
+
285
+ SL = np.append(_SL, _SL[::-1])
286
+ SR = np.append(_SR, _SR[::-1])
287
+ SU = np.append(_SU, _SU[::-1])
288
+
289
+ angles = ['0', '60', '120', '180', '240', '300']
290
+
291
+ fig, ax = plt.subplots(subplot_kw={'projection': 'polar'})
292
+
293
+ theta = np.linspace(0, 2 * np.pi, len(SL))
294
+
295
+ plt.thetagrids(range(0, 360, int(360 / len(angles))), angles)
296
+
297
+ plt.plot(theta, SL, '-', linewidth=2, color='#115162', label='SL')
298
+ plt.fill(theta, SL, '#afe0f5', alpha=0.5)
299
+ plt.plot(theta, SR, '-', linewidth=2, color='#7FAE80', label='SR')
300
+ plt.fill(theta, SR, '#b5e6c5', alpha=0.5)
301
+ plt.plot(theta, SU, '-', linewidth=2, color='#621129', label='SU')
302
+ plt.fill(theta, SU, '#f5afbd', alpha=0.5)
303
+
304
+ plt.legend(loc='best', bbox_to_anchor=(1, 0, 0.2, 1), prop={'weight': 'bold'})
305
+ plt.title(r'$\bf Scattering\ phase\ function$')
306
+
307
+ plt.show()
308
+ return fig, ax
308
309
 
309
310
 
310
311
  @set_figure
@@ -368,7 +369,7 @@ def response_surface(real_range=(1.33, 1.7),
368
369
  ax.plot_surface(real, gmd, ext, rstride=1, cstride=1, cmap=plt.get_cmap('jet'), edgecolor='none')
369
370
 
370
371
  ax.set(xlabel='Real part (n)', ylabel='GMD (nm)', zlabel=Unit('Extinction'),
371
- title='Sensitive tests of Extinction')
372
+ title='Sensitive tests of extinction')
372
373
 
373
374
  ax.zaxis.get_offset_text().set_visible(False)
374
375
  exponent = math.floor(math.log10(np.max(ext)))
@@ -381,8 +382,8 @@ def response_surface(real_range=(1.33, 1.7),
381
382
 
382
383
 
383
384
  if __name__ == '__main__':
384
- # Q_plot(['AS', 'AN', 'OM', 'Soil', 'SS', 'BC'], x='dp', y='MEE')
385
- # Q_plot(['AS', 'AN', 'OM', 'Soil', 'SS', 'BC'], x='dp', y='Q')
385
+ Q_plot(['AS', 'AN', 'OM', 'Soil', 'SS', 'BC'], x='dp', y='MEE')
386
+ Q_plot(['AS', 'AN', 'OM', 'Soil', 'SS', 'BC'], x='dp', y='Q')
386
387
 
387
- # RI_couple()
388
+ RI_couple()
388
389
  response_surface()
@@ -15,30 +15,47 @@ def diurnal_pattern(df: DataFrame,
15
15
  ax: Axes | None = None,
16
16
  **kwargs
17
17
  ) -> tuple[Figure, Axes]:
18
- if 'hour' or 'Hour' not in df.columns:
18
+ if 'hour' not in df.columns and 'Hour' not in df.columns:
19
19
  df['Hour'] = df.index.hour
20
20
 
21
21
  Hour = range(0, 24)
22
22
  mean = df.groupby('Hour')[y].mean()
23
23
  std = df.groupby('Hour')[y].std() * std_area
24
24
 
25
- fig, ax = plt.subplots(**kwargs.get('fig_kws', {})) if ax is None else (ax.get_figure(), ax)
25
+ fig, ax = plt.subplots() if ax is None else (ax.get_figure(), ax)
26
26
 
27
27
  # Plot Diurnal pattern
28
- ax.plot(Hour, mean, 'blue')
29
- ax.fill_between(Hour, y1=mean + std, y2=mean - std, alpha=0.2, color='blue', edgecolor=None)
28
+ ax.plot(Hour, mean, 'blue', zorder=3)
29
+ ax.fill_between(Hour, y1=mean + std, y2=mean - std, alpha=0.2, color='blue', edgecolor=None, zorder=2)
30
+
31
+ # Plot Boxplot for each hour
32
+ bp = ax.boxplot([df[df['Hour'] == h][y].dropna() for h in Hour],
33
+ positions=Hour,
34
+ widths=0.5,
35
+ patch_artist=True,
36
+ showfliers=False,
37
+ zorder=1)
38
+
39
+ # Customize boxplot colors
40
+ for element in ['boxes', 'whiskers', 'fliers', 'means', 'medians', 'caps']:
41
+ plt.setp(bp[element], color='gray')
42
+
43
+ for patch in bp['boxes']:
44
+ patch.set(facecolor='lightgray', alpha=0.5)
30
45
 
31
46
  ax.set(xlabel=kwargs.get('xlabel', 'Hours'),
32
47
  ylabel=kwargs.get('ylabel', Unit(y)),
33
- xlim=kwargs.get('xlim', (0, 23)),
48
+ xlim=kwargs.get('xlim', (-0.5, 23.5)),
34
49
  ylim=kwargs.get('ylim', (None, None)),
35
- xticks=kwargs.get('xticks', [0, 4, 8, 12, 16, 20]))
50
+ xticks=kwargs.get('xticks', range(0, 24, 4)),
51
+ xticklabels=kwargs.get('xticklabels', range(0, 24, 4)))
36
52
 
37
53
  ax.tick_params(axis='both', which='major')
38
54
  ax.tick_params(axis='x', which='minor')
39
55
  ax.xaxis.set_minor_locator(AutoMinorLocator())
40
56
  ax.ticklabel_format(axis='y', style='sci', scilimits=(-2, 3), useMathText=True)
41
57
 
58
+ plt.tight_layout()
42
59
  plt.show()
43
60
 
44
- return fig, ax
61
+ return fig, ax
@@ -2,7 +2,7 @@ import matplotlib.pyplot as plt
2
2
  from matplotlib.pyplot import Figure, Axes
3
3
  from pandas import DataFrame
4
4
 
5
- from AeroViz.plot.timeseries import timeseries
5
+ from AeroViz.plot.timeseries.timeseries import timeseries
6
6
 
7
7
 
8
8
  def timeseries_template(df: DataFrame) -> tuple[Figure, Axes]:
@@ -40,7 +40,7 @@ def timeseries_template(df: DataFrame) -> tuple[Figure, Axes]:
40
40
  timeseries(df, y='VC', color='PBLH', style='bar', ax=ax4, bar_kws=dict(cmap='Blues'), set_xaxis_visible=False,
41
41
  ylim=[0, 5000])
42
42
 
43
- timeseries(df, y='PM25', color='PM1/PM25', style='scatter', ax=ax5, ylim=[0, None])
43
+ timeseries(df, y='PM2.5', color='PM1/PM25', style='scatter', ax=ax5, ylim=[0, None])
44
44
 
45
45
  plt.show()
46
46
 
@@ -4,8 +4,9 @@ import matplotlib.pyplot as plt
4
4
  import numpy as np
5
5
  from matplotlib.cm import ScalarMappable
6
6
  from matplotlib.pyplot import Figure, Axes
7
+ from mpl_toolkits.axes_grid1 import make_axes_locatable
7
8
  from mpl_toolkits.axes_grid1.inset_locator import inset_axes
8
- from pandas import DataFrame, date_range
9
+ from pandas import DataFrame, date_range, Timedelta
9
10
 
10
11
  from AeroViz.plot.utils import *
11
12
 
@@ -70,6 +71,40 @@ def _plot(ax, df, _y, _color, plot_kws):
70
71
  ax.plot(df.index, df[_y], color=_color, **plot_kws)
71
72
 
72
73
 
74
+ def _wind_arrow(ax, df, y, c, scatter_kws, cbar_kws, inset_kws):
75
+ """
76
+ Plot wind arrows on a scatter plot.
77
+
78
+ :param ax: matplotlib axes
79
+ :param df: pandas DataFrame
80
+ :param y: column name for wind speed
81
+ :param c: column name for wind direction
82
+ :param scatter_kws: keyword arguments for scatter plot
83
+ :param cbar_kws: keyword arguments for colorbar
84
+ :param inset_kws: keyword arguments for inset axes
85
+ """
86
+ # First, create a scatter plot
87
+ sc = ax.scatter(df.index, df[y], c=df[c], **scatter_kws)
88
+
89
+ # Add colorbar
90
+ divider = make_axes_locatable(ax)
91
+ cax = divider.append_axes("right", size="2%", pad=0.05)
92
+ plt.colorbar(sc, cax=cax, **cbar_kws)
93
+
94
+ # Add wind arrows
95
+ for idx, row in df.iterrows():
96
+ wind_speed = row[y]
97
+ wind_dir = np.radians(row[c])
98
+ dx = np.sin(wind_dir) * wind_speed / 20 # Scale factor can be adjusted
99
+ dy = np.cos(wind_dir) * wind_speed / 20
100
+ ax.annotate('', xy=(idx + 10 * dx * Timedelta(hours=5), wind_speed + 4 * dy),
101
+ xytext=(idx - 10 * dx * Timedelta(hours=5), wind_speed - 4 * dy),
102
+ arrowprops=dict(arrowstyle='->', color='k', linewidth=0.5))
103
+
104
+ # Set the x-axis limit to show all data points
105
+ # ax.set_xlim(df.index.min() - datetime.timedelta(days=1), df.index.max())
106
+
107
+
73
108
  def process_timeseries_data(df, rolling=None, interpolate_limit=None):
74
109
  # apply rolling window if specified
75
110
  df = df.rolling(window=rolling, min_periods=1).mean(numeric_only=True) if rolling is not None else df
@@ -90,7 +125,7 @@ def timeseries(df: DataFrame,
90
125
  interpolate_limit: int | None = 6,
91
126
  major_freq: str = '1MS',
92
127
  minor_freq: str = '10d',
93
- style: list[Literal['scatter', 'bar', 'line']] | str | None = None,
128
+ style: list[Literal['scatter', 'bar', 'line', 'arrow']] | str | None = None,
94
129
  ax: Axes | None = None,
95
130
  set_xaxis_visible: bool | None = None,
96
131
  legend_loc: Literal['best', 'upper right', 'upper left', 'lower left', 'lower right'] = 'best',
@@ -199,16 +234,16 @@ def timeseries(df: DataFrame,
199
234
  if y2 and ('scatter' or 'bar') in style:
200
235
  fig.subplots_adjust(right=0.8)
201
236
 
202
- for i, _c in enumerate(color):
203
- if _c is not None and _c in df.columns:
204
- style[i] = 'scatter'
237
+ # for i, _c in enumerate(color):
238
+ # if _c is not None and _c in df.columns:
239
+ # style[i] = 'scatter'
205
240
 
206
241
  for i, (_y, _c, _label, _style) in enumerate(zip(y, color, label, style)):
207
242
  scatter_kws = {**default_scatter_kws, **{'label': Unit(_y)}, **kwargs.get('scatter_kws', {})}
208
243
  bar_kws = {**default_bar_kws, **{'label': Unit(_y)}, **kwargs.get('bar_kws', {})}
209
244
  plot_kws = {**default_plot_kws, **{'label': Unit(_y)}, **kwargs.get('plot_kws', {})}
210
245
 
211
- if _style in ['scatter', 'bar']:
246
+ if _style in ['scatter', 'bar', 'arrow']:
212
247
  cbar_kws = {**default_cbar_kws, **{'label': Unit(_c), 'ticks': None}, **kwargs.get('cbar_kws', {})}
213
248
  inset_kws = {**default_insert_kws, **{'bbox_transform': ax.transAxes}, **kwargs.get('inset_kws', {})}
214
249
 
@@ -218,6 +253,9 @@ def timeseries(df: DataFrame,
218
253
  elif _style == 'bar':
219
254
  _bar(ax, df, _y, _c, bar_kws, cbar_kws, inset_kws)
220
255
 
256
+ elif _style == 'arrow':
257
+ _wind_arrow(ax, df, _y, _c, scatter_kws, cbar_kws, inset_kws)
258
+
221
259
  else:
222
260
  _plot(ax, df, _y, _c, plot_kws)
223
261
 
@@ -237,6 +275,9 @@ def timeseries(df: DataFrame,
237
275
  elif _style == 'bar':
238
276
  _bar(ax2, df, _y, _c, bar_kws, cbar_kws, inset_kws)
239
277
 
278
+ elif _style == 'arrow':
279
+ pass
280
+
240
281
  else: # line plot
241
282
  _plot(ax2, df, _y, _c, plot_kws)
242
283
 
@@ -15,7 +15,7 @@ SUPPORTED_INSTRUMENTS = [
15
15
 
16
16
 
17
17
  def RawDataReader(instrument_name: str,
18
- path: Path,
18
+ path: Path | str,
19
19
  reset: bool = False,
20
20
  qc: bool | str = True,
21
21
  qc_freq: str | None = None,
@@ -87,7 +87,7 @@ def RawDataReader(instrument_name: str,
87
87
 
88
88
  if start and end:
89
89
  if end.hour == 0 and end.minute == 0 and end.second == 0:
90
- end = end.replace(hour=23)
90
+ end = end.replace(hour=23, minute=59, second=59)
91
91
  else:
92
92
  raise ValueError("Both start and end times must be provided.")
93
93
  if end <= start:
@@ -98,7 +98,7 @@ def RawDataReader(instrument_name: str,
98
98
  Timedelta(mean_freq)
99
99
  except ValueError:
100
100
  raise ValueError(
101
- f"Invalid mean_freq: '{mean_freq}'. It should be a valid frequency string (e.g., '1H', '30min', '1D').")
101
+ f"Invalid mean_freq: '{mean_freq}'. It should be a valid frequency string (e.g., '1h', '30min', '1D').")
102
102
 
103
103
  # Instantiate the class and return the instance
104
104
  reader_module = instrument_class_map[instrument_name].Reader(