AeroViz 0.1.3__py3-none-any.whl → 0.1.4__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.

Files changed (121) hide show
  1. AeroViz/__init__.py +7 -5
  2. AeroViz/{config → data}/DEFAULT_DATA.csv +1 -1
  3. AeroViz/dataProcess/Chemistry/__init__.py +40 -40
  4. AeroViz/dataProcess/Chemistry/_calculate.py +15 -15
  5. AeroViz/dataProcess/Chemistry/_isoropia.py +72 -68
  6. AeroViz/dataProcess/Chemistry/_mass_volume.py +158 -161
  7. AeroViz/dataProcess/Chemistry/_ocec.py +109 -109
  8. AeroViz/dataProcess/Chemistry/_partition.py +19 -18
  9. AeroViz/dataProcess/Chemistry/_teom.py +9 -11
  10. AeroViz/dataProcess/Chemistry/isrpia.cnf +21 -0
  11. AeroViz/dataProcess/Optical/Angstrom_exponent.py +20 -0
  12. AeroViz/dataProcess/Optical/_IMPROVE.py +40 -41
  13. AeroViz/dataProcess/Optical/__init__.py +29 -44
  14. AeroViz/dataProcess/Optical/_absorption.py +21 -47
  15. AeroViz/dataProcess/Optical/_extinction.py +31 -25
  16. AeroViz/dataProcess/Optical/_mie.py +5 -7
  17. AeroViz/dataProcess/Optical/_mie_sd.py +89 -90
  18. AeroViz/dataProcess/Optical/_scattering.py +19 -20
  19. AeroViz/dataProcess/SizeDistr/__init__.py +39 -39
  20. AeroViz/dataProcess/SizeDistr/__merge.py +159 -158
  21. AeroViz/dataProcess/SizeDistr/_merge.py +155 -154
  22. AeroViz/dataProcess/SizeDistr/_merge_v1.py +162 -161
  23. AeroViz/dataProcess/SizeDistr/_merge_v2.py +153 -152
  24. AeroViz/dataProcess/SizeDistr/_merge_v3.py +327 -327
  25. AeroViz/dataProcess/SizeDistr/_merge_v4.py +273 -275
  26. AeroViz/dataProcess/SizeDistr/_size_distr.py +51 -51
  27. AeroViz/dataProcess/VOC/__init__.py +9 -9
  28. AeroViz/dataProcess/VOC/_potential_par.py +53 -55
  29. AeroViz/dataProcess/__init__.py +28 -6
  30. AeroViz/dataProcess/core/__init__.py +59 -65
  31. AeroViz/plot/__init__.py +7 -2
  32. AeroViz/plot/bar.py +126 -0
  33. AeroViz/plot/box.py +69 -0
  34. AeroViz/plot/distribution/distribution.py +421 -427
  35. AeroViz/plot/meteorology/meteorology.py +240 -292
  36. AeroViz/plot/optical/__init__.py +0 -1
  37. AeroViz/plot/optical/optical.py +230 -230
  38. AeroViz/plot/pie.py +198 -0
  39. AeroViz/plot/regression.py +196 -0
  40. AeroViz/plot/scatter.py +165 -0
  41. AeroViz/plot/templates/__init__.py +2 -4
  42. AeroViz/plot/templates/ammonium_rich.py +34 -0
  43. AeroViz/plot/templates/contour.py +25 -25
  44. AeroViz/plot/templates/corr_matrix.py +86 -93
  45. AeroViz/plot/templates/diurnal_pattern.py +28 -26
  46. AeroViz/plot/templates/koschmieder.py +59 -123
  47. AeroViz/plot/templates/metal_heatmap.py +135 -37
  48. AeroViz/plot/timeseries/__init__.py +1 -0
  49. AeroViz/plot/timeseries/template.py +47 -0
  50. AeroViz/plot/timeseries/timeseries.py +324 -264
  51. AeroViz/plot/utils/__init__.py +2 -1
  52. AeroViz/plot/utils/_color.py +57 -57
  53. AeroViz/plot/utils/_unit.py +48 -48
  54. AeroViz/plot/utils/plt_utils.py +92 -0
  55. AeroViz/plot/utils/sklearn_utils.py +49 -0
  56. AeroViz/plot/utils/units.json +5 -0
  57. AeroViz/plot/violin.py +80 -0
  58. AeroViz/process/__init__.py +17 -17
  59. AeroViz/process/core/DataProc.py +9 -9
  60. AeroViz/process/core/SizeDist.py +81 -81
  61. AeroViz/process/method/PyMieScatt_update.py +488 -488
  62. AeroViz/process/method/mie_theory.py +231 -229
  63. AeroViz/process/method/prop.py +40 -40
  64. AeroViz/process/script/AbstractDistCalc.py +103 -103
  65. AeroViz/process/script/Chemical.py +168 -167
  66. AeroViz/process/script/IMPACT.py +40 -40
  67. AeroViz/process/script/IMPROVE.py +152 -152
  68. AeroViz/process/script/Others.py +45 -45
  69. AeroViz/process/script/PSD.py +26 -26
  70. AeroViz/process/script/PSD_dry.py +69 -70
  71. AeroViz/process/script/retrieve_RI.py +50 -51
  72. AeroViz/rawDataReader/__init__.py +53 -58
  73. AeroViz/rawDataReader/config/supported_instruments.py +155 -0
  74. AeroViz/rawDataReader/core/__init__.py +233 -356
  75. AeroViz/rawDataReader/script/AE33.py +17 -18
  76. AeroViz/rawDataReader/script/AE43.py +18 -21
  77. AeroViz/rawDataReader/script/APS_3321.py +30 -30
  78. AeroViz/rawDataReader/script/Aurora.py +23 -24
  79. AeroViz/rawDataReader/script/BC1054.py +36 -40
  80. AeroViz/rawDataReader/script/EPA_vertical.py +37 -9
  81. AeroViz/rawDataReader/script/GRIMM.py +16 -23
  82. AeroViz/rawDataReader/script/IGAC.py +90 -0
  83. AeroViz/rawDataReader/script/MA350.py +32 -39
  84. AeroViz/rawDataReader/script/Minion.py +103 -0
  85. AeroViz/rawDataReader/script/NEPH.py +69 -74
  86. AeroViz/rawDataReader/script/SMPS_TH.py +25 -25
  87. AeroViz/rawDataReader/script/SMPS_aim11.py +32 -32
  88. AeroViz/rawDataReader/script/SMPS_genr.py +31 -31
  89. AeroViz/rawDataReader/script/Sunset_OCEC.py +60 -0
  90. AeroViz/rawDataReader/script/TEOM.py +30 -28
  91. AeroViz/rawDataReader/script/Table.py +13 -14
  92. AeroViz/rawDataReader/script/VOC.py +26 -0
  93. AeroViz/rawDataReader/script/__init__.py +18 -20
  94. AeroViz/tools/database.py +64 -66
  95. AeroViz/tools/dataclassifier.py +106 -106
  96. AeroViz/tools/dataprinter.py +51 -51
  97. AeroViz/tools/datareader.py +38 -38
  98. {AeroViz-0.1.3.dist-info → AeroViz-0.1.4.dist-info}/METADATA +5 -4
  99. AeroViz-0.1.4.dist-info/RECORD +112 -0
  100. AeroViz/plot/improve/__init__.py +0 -1
  101. AeroViz/plot/improve/improve.py +0 -240
  102. AeroViz/plot/optical/aethalometer.py +0 -77
  103. AeroViz/plot/templates/event_evolution.py +0 -65
  104. AeroViz/plot/templates/regression.py +0 -256
  105. AeroViz/plot/templates/scatter.py +0 -130
  106. AeroViz/plot/templates/templates.py +0 -398
  107. AeroViz/plot/utils/_decorator.py +0 -74
  108. AeroViz/rawDataReader/script/IGAC_TH.py +0 -104
  109. AeroViz/rawDataReader/script/IGAC_ZM.py +0 -90
  110. AeroViz/rawDataReader/script/OCEC_LCRES.py +0 -34
  111. AeroViz/rawDataReader/script/OCEC_RES.py +0 -28
  112. AeroViz/rawDataReader/script/VOC_TH.py +0 -30
  113. AeroViz/rawDataReader/script/VOC_ZM.py +0 -37
  114. AeroViz/rawDataReader/utils/__init__.py +0 -0
  115. AeroViz/rawDataReader/utils/config.py +0 -169
  116. AeroViz-0.1.3.dist-info/RECORD +0 -111
  117. /AeroViz/{config → data}/DEFAULT_PNSD_DATA.csv +0 -0
  118. /AeroViz/{config → rawDataReader/config}/__init__.py +0 -0
  119. {AeroViz-0.1.3.dist-info → AeroViz-0.1.4.dist-info}/LICENSE +0 -0
  120. {AeroViz-0.1.3.dist-info → AeroViz-0.1.4.dist-info}/WHEEL +0 -0
  121. {AeroViz-0.1.3.dist-info → AeroViz-0.1.4.dist-info}/top_level.txt +0 -0
@@ -1,38 +1,39 @@
1
- from datetime import datetime
2
1
  from typing import Literal
3
2
 
4
3
  import matplotlib.pyplot as plt
4
+ import numpy as np
5
5
  from matplotlib.cm import ScalarMappable
6
6
  from matplotlib.pyplot import Figure, Axes
7
7
  from mpl_toolkits.axes_grid1.inset_locator import inset_axes
8
- from pandas import DataFrame, date_range, Timestamp
8
+ from pandas import DataFrame, date_range
9
9
 
10
10
  from AeroViz.plot.utils import *
11
11
 
12
- __all__ = ['timeseries', 'timeseries_template']
12
+ __all__ = ['timeseries', 'timeseries_stacked']
13
+
13
14
 
14
15
  default_bar_kws = dict(
15
- width=0.0417,
16
- edgecolor=None,
17
- linewidth=0,
18
- cmap='jet',
16
+ width=0.0417,
17
+ edgecolor=None,
18
+ linewidth=0,
19
+ cmap='jet',
19
20
  )
20
21
 
21
22
  default_scatter_kws = dict(
22
- marker='o',
23
- s=5,
24
- edgecolor=None,
25
- linewidths=0.3,
26
- alpha=0.9,
27
- cmap='jet',
23
+ marker='o',
24
+ s=5,
25
+ edgecolor=None,
26
+ linewidths=0.3,
27
+ alpha=0.9,
28
+ cmap='jet',
28
29
  )
29
30
 
30
31
  default_insert_kws = dict(
31
- width="1.5%",
32
- height="100%",
33
- loc='lower left',
34
- bbox_to_anchor=(1.01, 0, 1.2, 1),
35
- borderpad=0
32
+ width="1.5%",
33
+ height="100%",
34
+ loc='lower left',
35
+ bbox_to_anchor=(1.01, 0, 1.2, 1),
36
+ borderpad=0
36
37
  )
37
38
 
38
39
  default_plot_kws = dict()
@@ -41,262 +42,321 @@ default_cbar_kws = dict()
41
42
 
42
43
 
43
44
  def _scatter(ax, df, _y, _c, scatter_kws, cbar_kws, inset_kws):
44
- if _c is None or _c not in df.columns:
45
- scatter_kws.pop('cmap')
46
- ax.scatter(df.index, df[_y], **scatter_kws)
47
- else:
48
- ax.scatter(df.index, df[_y], c=df[_c], **scatter_kws)
49
- cax = inset_axes(ax, **inset_kws)
45
+ if _c is None or _c not in df.columns:
46
+ scatter_kws.pop('cmap')
47
+ ax.scatter(df.index, df[_y], **scatter_kws)
48
+ else:
49
+ ax.scatter(df.index, df[_y], c=df[_c], **scatter_kws)
50
+ cax = inset_axes(ax, **inset_kws)
50
51
 
51
- # Filter the children to find ScalarMappable objects
52
- mappable_objects = [child for child in ax.get_children() if isinstance(child, ScalarMappable)]
52
+ # Filter the children to find ScalarMappable objects
53
+ mappable_objects = [child for child in ax.get_children() if isinstance(child, ScalarMappable)]
53
54
 
54
- # Use the first mappable object for the colorbar
55
- if mappable_objects:
56
- plt.colorbar(mappable=mappable_objects[0], cax=cax, **cbar_kws)
57
- else:
58
- print("No mappable objects found.")
55
+ # Use the first mappable object for the colorbar
56
+ if mappable_objects:
57
+ plt.colorbar(mappable=mappable_objects[0], cax=cax, **cbar_kws)
58
+ else:
59
+ print("No mappable objects found.")
59
60
 
60
61
 
61
62
  def _bar(ax, df, _y, _c, bar_kws, cbar_kws, inset_kws):
62
- scalar_map, colors = Color.color_maker(df[_c].values, cmap=bar_kws.pop('cmap'))
63
- ax.bar(df.index, df[_y], color=scalar_map.to_rgba(colors), **bar_kws)
64
- cax = inset_axes(ax, **inset_kws)
65
- plt.colorbar(mappable=scalar_map, cax=cax, **cbar_kws)
63
+ scalar_map, colors = Color.color_maker(df[_c].values, cmap=bar_kws.pop('cmap'))
64
+ ax.bar(df.index, df[_y], color=scalar_map.to_rgba(colors), **bar_kws)
65
+ cax = inset_axes(ax, **inset_kws)
66
+ plt.colorbar(mappable=scalar_map, cax=cax, **cbar_kws)
66
67
 
67
68
 
68
69
  def _plot(ax, df, _y, _color, plot_kws):
69
- ax.plot(df.index, df[_y], color=_color, **plot_kws)
70
+ ax.plot(df.index, df[_y], color=_color, **plot_kws)
71
+
70
72
 
73
+ def process_timeseries_data(df, rolling=None, interpolate_limit=None):
74
+ # apply rolling window if specified
75
+ df = df.rolling(window=rolling, min_periods=1).mean(numeric_only=True) if rolling is not None else df
71
76
 
72
- def combine_legends(axes_list: list[Axes]) -> tuple[list, list]:
73
- return (
74
- [legend for axes in axes_list for legend in axes.get_legend_handles_labels()[0]],
75
- [label for axes in axes_list for label in axes.get_legend_handles_labels()[1]]
76
- )
77
+ # apply interpolation if specified
78
+ df = df.interpolate(method='time', limit=interpolate_limit) if interpolate_limit is not None else df
79
+ return df
77
80
 
78
81
 
79
- @set_figure(fs=8, autolayout=False)
82
+ @set_figure(autolayout=False)
80
83
  def timeseries(df: DataFrame,
81
- y: list[str] | str,
82
- y2: list[str] | str = None,
83
- c: list[str] | str = None,
84
- # color: list[str] | str = None,
85
- rolling: str | int | None = None,
86
- times: list[datetime | Timestamp | str] = None,
87
- freq: str = '1MS',
88
- style: list[Literal['scatter', 'bar', 'line']] | str | None = None,
89
- ax: Axes | None = None,
90
- set_xaxis_visible: bool | None = None,
91
- legend_loc: Literal['best', 'upper right', 'upper left', 'lower left', 'lower right'] = 'best',
92
- legend_ncol: int = 1,
93
- **kwargs
94
- ) -> tuple[Figure, Axes]:
95
- """
96
- Plot the timeseries data with the option of scatterplot, barplot, and lineplot.
97
-
98
- Parameters
99
- -----------
100
- df : DataFrame
101
- The data to plot.
102
- y : list[str] | str
103
- The primary y-axis data columns.
104
- y2 : list[str] | str, optional
105
- The secondary y-axis data columns. Defaults to None.
106
- c : str, optional
107
- The column for color mapping or the color. Defaults to None.
108
- rolling : str | int | None, optional
109
- Rolling window size for smoothing. Defaults to None.
110
- times : tuple[datetime, datetime] | tuple[Timestamp, Timestamp], optional
111
- Time range for the data. Defaults to None.
112
- freq : str, optional
113
- Frequency for x-axis ticks. Defaults to '2MS'.
114
- style : Literal['scatter', 'bar', 'line'] | None, optional
115
- Style of the plot. Defaults to 'scatter'.
116
- ax : Axes | None, optional
117
- Matplotlib Axes object to plot on. Defaults to None.
118
- set_xaxis_visible : bool | None, optional
119
- Whether to set x-axis visibility. Defaults to None.
120
- legend_loc : Literal['best', 'upper right', 'upper left', 'lower left', 'lower right'], optional
121
- Location of the legend. Defaults to 'best'.
122
- legend_ncol : int, optional
123
- Number of columns in the legend. Defaults to 1.
124
- **kwargs : Additional keyword arguments for customization.
125
- fig_kws : dict, optional
126
- Additional keyword arguments for the figure. Defaults to {}.
127
- scatter_kws : dict, optional
128
- Additional keyword arguments for the scatter plot. Defaults to {}.
129
- bar_kws : dict, optional
130
- Additional keyword arguments for the bar plot. Defaults to {}.
131
- ax_plot_kws : dict, optional
132
- Additional keyword arguments for the primary y-axis plot. Defaults to {}.
133
- ax2_plot_kws : dict, optional
134
- Additional keyword arguments for the secondary y-axis plot. Defaults to {}.
135
- cbar_kws : dict, optional
136
- Additional keyword arguments for the colorbar. Defaults to {}.
137
- inset_kws : dict, optional
138
- Additional keyword arguments for the inset axes. Defaults to {}.
139
-
140
- Returns
141
- -------
142
- ax : AxesSubplot
143
- Matplotlib AxesSubplot.
144
-
145
- Example
146
- -------
147
- >>> timeseries(df, y='WS', c='WD', scatter_kws=dict(cmap='hsv'), cbar_kws=dict(ticks=[0, 90, 180, 270, 360]), ylim=[0, None])
148
- """
149
- # Set the time
150
-
151
- if times is not None:
152
- st_tm, fn_tm = map(Timestamp, times)
153
- else:
154
- try:
155
- st_tm, fn_tm = df.index[0], df.index[-1]
156
- except IndexError:
157
- raise IndexError("The DataFrame is empty. Please provide a valid DataFrame.")
158
-
159
- # Apply rolling window if specified
160
- df = df.loc[st_tm:fn_tm] if rolling is None else (
161
- df.loc[st_tm:fn_tm].rolling(window=rolling, min_periods=1).mean(numeric_only=True))
162
-
163
- # Initialize figure and axis if not provided
164
- fig, ax = plt.subplots(**{**{'figsize': (6, 2)}, **kwargs.get('fig_kws', {})}) if ax is None else (
165
- ax.get_figure(), ax)
166
-
167
- # Ensure y, y2, c, and style are lists
168
- y = [y] if isinstance(y, str) else y
169
- y2 = [y2] if isinstance(y2, str) else y2 if y2 is not None else []
170
- c = [c] if isinstance(c, str) else c if c is not None else [None] * (len(y) + len(y2))
171
- style = [style] if isinstance(style, str) else style if style is not None else ['plot'] * (len(y) + len(y2))
172
-
173
- if len(c) != len(y) + len(y2):
174
- raise ValueError("The length of c must match the combined length of y and y2")
175
-
176
- if len(style) != len(y) + len(y2):
177
- raise ValueError("The length of style must match the combined length of y and y2")
178
-
179
- # Create a secondary y-axis if y2 is not empty
180
- ax2 = ax.twinx() if y2 else None
181
-
182
- # # Set color cycle
183
- ax.set_prop_cycle(Color.color_cycle)
184
- if y2:
185
- ax2.set_prop_cycle(Color.color_cycle[len(y):])
186
-
187
- if y2 and ('scatter' or 'bar') in style:
188
- fig.subplots_adjust(right=0.8)
189
-
190
- for i, _c in enumerate(c):
191
- if _c is not None and _c in df.columns:
192
- style[i] = 'scatter'
193
-
194
- for i, (_y, _c, _style) in enumerate(zip(y, c, style)):
195
- scatter_kws = {**default_scatter_kws, **{'label': Unit(_y)}, **kwargs.get('scatter_kws', {})}
196
- bar_kws = {**default_bar_kws, **{'label': Unit(_y)}, **kwargs.get('bar_kws', {})}
197
- plot_kws = {**default_plot_kws, **{'label': Unit(_y)}, **kwargs.get('plot_kws', {})}
198
-
199
- if _style in ['scatter', 'bar']:
200
- cbar_kws = {**default_cbar_kws, **{'label': Unit(_c), 'ticks': None}, **kwargs.get('cbar_kws', {})}
201
- inset_kws = {**default_insert_kws, **{'bbox_transform': ax.transAxes}, **kwargs.get('inset_kws', {})}
202
-
203
- if _style == 'scatter':
204
- _scatter(ax, df, _y, _c, scatter_kws, cbar_kws, inset_kws)
205
-
206
- elif _style == 'bar':
207
- _bar(ax, df, _y, _c, bar_kws, cbar_kws, inset_kws)
208
-
209
- else:
210
- _plot(ax, df, _y, _c, plot_kws)
211
-
212
- if y2:
213
- for i, (_y, _c, _style) in enumerate(zip(y2, c[len(y):], style[len(y):])):
214
- scatter_kws = {**default_scatter_kws, **{'label': Unit(_y)}, **kwargs.get('scatter_kws2', {})}
215
- bar_kws = {**default_bar_kws, **{'label': Unit(_y)}, **kwargs.get('bar_kws2', {})}
216
- plot_kws = {**default_plot_kws, **{'label': Unit(_y)}, **kwargs.get('plot_kws2', {})}
217
-
218
- if _style in ['scatter', 'bar']:
219
- cbar_kws = {**default_cbar_kws, **{'label': Unit(_c), 'ticks': None}, **kwargs.get('cbar_kws2', {})}
220
- inset_kws = {**default_insert_kws, **{'bbox_transform': ax.transAxes}, **kwargs.get('inset_kws2', {})}
221
-
222
- if _style == 'scatter':
223
- _scatter(ax2, df, _y, _c, scatter_kws, cbar_kws, inset_kws)
224
-
225
- elif _style == 'bar':
226
- _bar(ax2, df, _y, _c, bar_kws, cbar_kws, inset_kws)
227
-
228
- else: # line plot
229
- _plot(ax2, df, _y, _c, plot_kws)
230
-
231
- # Combine legends from ax and ax2
232
- ax.legend(*combine_legends([ax, ax2]), loc=legend_loc, ncol=legend_ncol)
233
-
234
- else:
235
- ax.legend(loc=legend_loc, ncol=legend_ncol)
236
-
237
- if set_xaxis_visible is not None:
238
- ax.axes.xaxis.set_visible(set_xaxis_visible)
239
-
240
- ax.set(xlabel=kwargs.get('xlabel', ''),
241
- ylabel=kwargs.get('ylabel', Unit(y) if isinstance(y, str) else Unit(y[0])),
242
- xticks=kwargs.get('xticks', date_range(start=st_tm, end=fn_tm, freq=freq).strftime("%F")),
243
- yticks=kwargs.get('yticks', ax.get_yticks()),
244
- xticklabels=kwargs.get('xticklabels', date_range(start=st_tm, end=fn_tm, freq=freq).strftime("%F")),
245
- yticklabels=kwargs.get('yticklabels', ax.get_yticklabels()),
246
- xlim=kwargs.get('xlim', (st_tm, fn_tm)),
247
- ylim=kwargs.get('ylim', (None, None)),
248
- title=kwargs.get('title', '')
249
- )
250
-
251
- if y2:
252
- ax2.set(ylabel=kwargs.get('ylabel2', Unit(y2) if isinstance(y2, str) else Unit(y2[0])),
253
- yticks=kwargs.get('yticks2', ax2.get_yticks()),
254
- yticklabels=kwargs.get('yticklabels2', ax2.get_yticklabels()),
255
- ylim=kwargs.get('ylim2', (None, None)))
256
-
257
- plt.show()
258
-
259
- return fig, ax
260
-
261
-
262
- @set_figure(fs=8, autolayout=False)
263
- def timeseries_template(df: DataFrame) -> tuple[Figure, Axes]:
264
- fig, ax = plt.subplots(5, 1, figsize=(len(df.index) * 0.01, 4))
265
- (ax1, ax2, ax3, ax4, ax5) = ax
266
-
267
- timeseries(df,
268
- y=['Extinction', 'Scattering', 'Absorption'],
269
- rolling=30,
270
- ax=ax1,
271
- ylabel='Coefficient',
272
- ylim=[0., None],
273
- set_xaxis_visible=False,
274
- legend_ncol=3,
275
- )
276
-
277
- # Temp, RH
278
- timeseries(df,
279
- y='AT',
280
- y2='RH',
281
- rolling=30,
282
- ax=ax2,
283
- ax_plot_kws=dict(color='r'),
284
- ax2_plot_kws=dict(color='b'),
285
- ylim=[10, 30],
286
- ylim2=[20, 100],
287
- set_xaxis_visible=False,
288
- legend_ncol=2,
289
- )
290
-
291
- timeseries(df, y='WS', c='WD', style='scatter', ax=ax3, scatter_kws=dict(cmap='hsv'),
292
- cbar_kws=dict(ticks=[0, 90, 180, 270, 360]),
293
- ylim=[0, None], set_xaxis_visible=False)
294
-
295
- timeseries(df, y='VC', c='PBLH', style='bar', ax=ax4, bar_kws=dict(cmap='Blues'), set_xaxis_visible=False,
296
- ylim=[0, 5000])
297
-
298
- timeseries(df, y='PM25', c='PM1/PM25', style='scatter', ax=ax5, ylim=[0, None])
299
-
300
- plt.show()
301
-
302
- return fig, ax
84
+ y: list[str] | str,
85
+ y2: list[str] | str = None,
86
+ yi: list[str] | str = None,
87
+ color: list[str] | str | None = None,
88
+ label: list[str] | str | None = None,
89
+ rolling: int | str | None = 3,
90
+ interpolate_limit: int | None = 6,
91
+ major_freq: str = '1MS',
92
+ minor_freq: str = '10d',
93
+ style: list[Literal['scatter', 'bar', 'line']] | str | None = None,
94
+ ax: Axes | None = None,
95
+ set_xaxis_visible: bool | None = None,
96
+ legend_loc: Literal['best', 'upper right', 'upper left', 'lower left', 'lower right'] = 'best',
97
+ legend_ncol: int = 1,
98
+ **kwargs
99
+ ) -> tuple[Figure, Axes]:
100
+ """
101
+ Plot the timeseries data with the option of scatterplot, barplot, and lineplot.
102
+
103
+ Parameters
104
+ -----------
105
+ df : DataFrame
106
+ The data to plot.
107
+ y : list[str] | str
108
+ The primary y-axis data columns.
109
+ y2 : list[str] | str, optional
110
+ The secondary y-axis data columns. Defaults to None.
111
+ yi : list[str] | str, optional
112
+ The components for percentage calculation. Defaults to None.
113
+ color : str, optional
114
+ The column for color mapping or the color. Defaults to None.
115
+ label : str, optional
116
+ The label for the legend. Defaults to None.
117
+ rolling : str | int | None, optional
118
+ Rolling window size for smoothing. Defaults to None.
119
+ interpolate_limit : int, optional
120
+ Interpolation limit for missing values. Defaults to None.
121
+ major_freq : str, optional
122
+ Frequency for x-axis ticks. Defaults to '1MS'.
123
+ minor_freq : str, optional
124
+ Frequency for x-axis minor ticks. Defaults to '10d'.
125
+ style : Literal['scatter', 'bar', 'line'] | None, optional
126
+ Style of the plot. Defaults to 'scatter'.
127
+ ax : Axes | None, optional
128
+ Matplotlib Axes object to plot on. Defaults to None.
129
+ set_xaxis_visible : bool | None, optional
130
+ Whether to set x-axis visibility. Defaults to None.
131
+ legend_loc : Literal['best', 'upper right', 'upper left', 'lower left', 'lower right'], optional
132
+ Location of the legend. Defaults to 'best'.
133
+ legend_ncol : int, optional
134
+ Number of columns in the legend. Defaults to 1.
135
+ **kwargs : Additional keyword arguments for customization.
136
+ fig_kws : dict, optional
137
+ Additional keyword arguments for the figure. Defaults to {}.
138
+ scatter_kws : dict, optional
139
+ Additional keyword arguments for the scatter plot. Defaults to {}.
140
+ bar_kws : dict, optional
141
+ Additional keyword arguments for the bar plot. Defaults to {}.
142
+ ax_plot_kws : dict, optional
143
+ Additional keyword arguments for the primary y-axis plot. Defaults to {}.
144
+ ax2_plot_kws : dict, optional
145
+ Additional keyword arguments for the secondary y-axis plot. Defaults to {}.
146
+ cbar_kws : dict, optional
147
+ Additional keyword arguments for the colorbar. Defaults to {}.
148
+ inset_kws : dict, optional
149
+ Additional keyword arguments for the inset axes. Defaults to {}.
150
+
151
+ Returns
152
+ -------
153
+ ax : AxesSubplot
154
+ Matplotlib AxesSubplot.
155
+
156
+ Example
157
+ -------
158
+ >>> timeseries(df, y='WS', color='WD', scatter_kws=dict(cmap='hsv'), cbar_kws=dict(ticks=[0, 90, 180, 270, 360]), ylim=[0, None])
159
+ """
160
+ # Set the time
161
+ try:
162
+ st_tm, fn_tm = df.index[0], df.index[-1]
163
+ except IndexError:
164
+ raise IndexError("The DataFrame is empty. Please provide a valid DataFrame.")
165
+
166
+ # calculate the percentage of each component
167
+ if yi is not None:
168
+ df_pct = df[yi].div(df[yi].sum(axis=1), axis=0) * 100
169
+ mean = [f"{_label} : {df[comp].mean():.2f}" for _label, comp in zip(label, yi)]
170
+ pct = [f"{_label} : {df_pct[comp].mean():.2f}%" for _label, comp in zip(label, yi)]
171
+ df_pct = process_timeseries_data(df_pct, rolling, interpolate_limit)
172
+
173
+ # process data
174
+ df = process_timeseries_data(df, rolling, interpolate_limit)
175
+
176
+ # Initialize figure and axis if not provided
177
+ fig, ax = plt.subplots(**{**{'figsize': (6, 2)}, **kwargs.get('fig_kws', {})}) if ax is None else (
178
+ ax.get_figure(), ax)
179
+
180
+ # Ensure y, y2, c, and style are lists
181
+ y = [y] if isinstance(y, str) else y
182
+ y2 = [y2] if isinstance(y2, str) else y2 if y2 is not None else []
183
+ color = [color] if isinstance(color, str) else color if color is not None else [None] * (len(y) + len(y2))
184
+ label = [label] if isinstance(label, str) else label if label is not None else [None] * (len(y) + len(y2))
185
+ style = [style] if isinstance(style, str) else style if style is not None else ['plot'] * (len(y) + len(y2))
186
+
187
+ for name, lst in [("c", color), ("style", style), ("label", label)]:
188
+ if len(lst) != len(y) + len(y2):
189
+ raise ValueError(f"The length of {name} must match the combined length of y and y2")
190
+
191
+ # Create a secondary y-axis if y2 is not empty
192
+ ax2 = ax.twinx() if y2 else None
193
+
194
+ # # Set color cycle
195
+ ax.set_prop_cycle(Color.color_cycle)
196
+ if y2:
197
+ ax2.set_prop_cycle(Color.color_cycle[len(y):])
198
+
199
+ if y2 and ('scatter' or 'bar') in style:
200
+ fig.subplots_adjust(right=0.8)
201
+
202
+ for i, _c in enumerate(color):
203
+ if _c is not None and _c in df.columns:
204
+ style[i] = 'scatter'
205
+
206
+ for i, (_y, _c, _label, _style) in enumerate(zip(y, color, label, style)):
207
+ scatter_kws = {**default_scatter_kws, **{'label': Unit(_y)}, **kwargs.get('scatter_kws', {})}
208
+ bar_kws = {**default_bar_kws, **{'label': Unit(_y)}, **kwargs.get('bar_kws', {})}
209
+ plot_kws = {**default_plot_kws, **{'label': Unit(_y)}, **kwargs.get('plot_kws', {})}
210
+
211
+ if _style in ['scatter', 'bar']:
212
+ cbar_kws = {**default_cbar_kws, **{'label': Unit(_c), 'ticks': None}, **kwargs.get('cbar_kws', {})}
213
+ inset_kws = {**default_insert_kws, **{'bbox_transform': ax.transAxes}, **kwargs.get('inset_kws', {})}
214
+
215
+ if _style == 'scatter':
216
+ _scatter(ax, df, _y, _c, scatter_kws, cbar_kws, inset_kws)
217
+
218
+ elif _style == 'bar':
219
+ _bar(ax, df, _y, _c, bar_kws, cbar_kws, inset_kws)
220
+
221
+ else:
222
+ _plot(ax, df, _y, _c, plot_kws)
223
+
224
+ if y2:
225
+ for i, (_y, _c, _style) in enumerate(zip(y2, color[len(y):], style[len(y):])):
226
+ scatter_kws = {**default_scatter_kws, **{'label': Unit(_y)}, **kwargs.get('scatter_kws2', {})}
227
+ bar_kws = {**default_bar_kws, **{'label': Unit(_y)}, **kwargs.get('bar_kws2', {})}
228
+ plot_kws = {**default_plot_kws, **{'label': Unit(_y)}, **kwargs.get('plot_kws2', {})}
229
+
230
+ if _style in ['scatter', 'bar']:
231
+ cbar_kws = {**default_cbar_kws, **{'label': Unit(_c), 'ticks': None}, **kwargs.get('cbar_kws2', {})}
232
+ inset_kws = {**default_insert_kws, **{'bbox_transform': ax.transAxes}, **kwargs.get('inset_kws2', {})}
233
+
234
+ if _style == 'scatter':
235
+ _scatter(ax2, df, _y, _c, scatter_kws, cbar_kws, inset_kws)
236
+
237
+ elif _style == 'bar':
238
+ _bar(ax2, df, _y, _c, bar_kws, cbar_kws, inset_kws)
239
+
240
+ else: # line plot
241
+ _plot(ax2, df, _y, _c, plot_kws)
242
+
243
+ # Combine legends from ax and ax2
244
+ ax.legend(*combine_legends([ax, ax2]), loc=legend_loc, ncol=legend_ncol)
245
+
246
+ else:
247
+ ax.legend(loc=legend_loc, ncol=legend_ncol)
248
+
249
+ if set_xaxis_visible is not None:
250
+ ax.axes.xaxis.set_visible(set_xaxis_visible)
251
+
252
+ ax.set(xlabel=kwargs.get('xlabel', ''),
253
+ ylabel=kwargs.get('ylabel', Unit(y) if isinstance(y, str) else Unit(y[0])),
254
+ xlim=kwargs.get('xlim', (st_tm, fn_tm)),
255
+ ylim=kwargs.get('ylim', (None, None)),
256
+ title=kwargs.get('title', '')
257
+ )
258
+
259
+ xticks = kwargs.get('xticks', date_range(start=st_tm, end=fn_tm, freq=major_freq))
260
+ minor_xticks = kwargs.get('minor_xticks', date_range(start=st_tm, end=fn_tm, freq=minor_freq))
261
+
262
+ ax.set_xticks(ticks=xticks, labels=xticks.strftime("%F"))
263
+ ax.set_xticks(minor_xticks, minor=True)
264
+
265
+ if y2:
266
+ ax2.set(ylim=kwargs.get('ylim2', (None, None)),
267
+ ylabel=kwargs.get('ylabel2', Unit(y2) if isinstance(y2, str) else Unit(y2[0]))
268
+ )
269
+
270
+ plt.show()
271
+
272
+ return fig, ax
273
+
274
+
275
+ @set_figure(autolayout=False)
276
+ def timeseries_stacked(df,
277
+ y: list[str] | str,
278
+ yi: list[str] | str,
279
+ label: list[str] | str,
280
+ rolling: int | str | None = 6,
281
+ interpolate_limit: int | None = 6,
282
+ major_freq: str = '1MS',
283
+ minor_freq: str = '10d',
284
+ ax: Axes | None = None,
285
+ legend_ncol: int = 1,
286
+ **kwargs
287
+ ) -> tuple[Figure, Axes]:
288
+ try:
289
+ st_tm, fn_tm = df.index[0], df.index[-1]
290
+ except IndexError:
291
+ raise IndexError("The DataFrame is empty. Please provide a valid DataFrame.")
292
+
293
+ # calculate the percentage of each component
294
+ df_pct = df[yi].div(df[yi].sum(axis=1), axis=0) * 100
295
+ mean = [f"{_label} : {df[comp].mean():.2f}" for _label, comp in zip(label, yi)]
296
+ pct = [f"{_label} : {df_pct[comp].mean():.2f}%" for _label, comp in zip(label, yi)]
297
+
298
+ # process data
299
+ df = process_timeseries_data(df, rolling, interpolate_limit)
300
+ df_pct = process_timeseries_data(df_pct, rolling, interpolate_limit)
301
+
302
+ fig, (ax1, ax2) = plt.subplots(2, 1, **{**{'figsize': (12, 3)}, **kwargs.get('fig_kws', {})})
303
+
304
+ width = 0.0417
305
+ color = Color.colors1
306
+
307
+ for name, lst in [("color", color), ("label", label)]:
308
+ if len(lst) != len(yi):
309
+ raise ValueError(f"The length of {name} must match the combined length of y and y2")
310
+
311
+ bottom = None # 初始化堆疊位置
312
+ for i, (_column, _color, _label) in enumerate(zip(yi, color, mean)):
313
+ if i == 0:
314
+ bottom = df[_column] * 0 # 第一個堆疊底部為零
315
+ ax1.bar(df.index, df[_column], color=_color, width=width, bottom=bottom, label=_label)
316
+ bottom += df[_column] # 更新堆疊底部位置
317
+
318
+ ax1.legend(loc='upper left', ncol=legend_ncol, prop={'weight': 'bold'}, bbox_to_anchor=(1, 0, 1.2, 1))
319
+
320
+ ax1.axes.xaxis.set_visible(False)
321
+
322
+ ax1.set(xlabel=kwargs.get('xlabel', ''),
323
+ ylabel=kwargs.get('ylabel', Unit(y) if isinstance(y, str) else Unit(y[0])),
324
+ xlim=kwargs.get('xlim', (st_tm, fn_tm)),
325
+ ylim=kwargs.get('ylim', (None, None)),
326
+ title=kwargs.get('title', ''),
327
+ )
328
+
329
+ xticks = kwargs.get('xticks', date_range(start=st_tm, end=fn_tm, freq=major_freq))
330
+ yticks = kwargs.get('yticks', np.linspace(*ax1.get_ylim(), num=6))
331
+ minor_xticks = kwargs.get('minor_xticks', date_range(start=st_tm, end=fn_tm, freq=minor_freq))
332
+
333
+ ax1.set_xticks(ticks=xticks, labels=xticks.strftime("%F"))
334
+ ax1.set_yticks(ticks=yticks, labels=[f'{tick:.0f}' for tick in yticks])
335
+ ax1.set_xticks(minor_xticks, minor=True)
336
+
337
+ # ax2
338
+ bottom = None # 初始化堆疊位置
339
+ for i, (_column, _color, _label) in enumerate(zip(yi, color, pct)):
340
+ if i == 0:
341
+ bottom = df_pct[_column] * 0 # 第一個堆疊底部為零
342
+ ax2.bar(df_pct.index, df_pct[_column], color=_color, width=width, bottom=bottom, label=_label)
343
+ bottom += df_pct[_column] # 更新堆疊底部位置
344
+
345
+ ax2.legend(loc='upper left', ncol=legend_ncol, prop={'weight': 'bold'}, bbox_to_anchor=(1, 0, 1.2, 1))
346
+
347
+ ax2.set(xlabel=kwargs.get('xlabel', ''),
348
+ ylabel=kwargs.get('ylabel', 'Percentage (%)'),
349
+ xlim=kwargs.get('xlim', (st_tm, fn_tm)),
350
+ ylim=(0, 100),
351
+ title=kwargs.get('title', '')
352
+ )
353
+
354
+ xticks = kwargs.get('xticks', date_range(start=st_tm, end=fn_tm, freq=major_freq))
355
+ yticks = kwargs.get('yticks', np.linspace(*ax2.get_ylim(), num=6))
356
+ minor_xticks = kwargs.get('minor_xticks', date_range(start=st_tm, end=fn_tm, freq=minor_freq))
357
+
358
+ ax2.set_xticks(ticks=xticks, labels=xticks.strftime("%F"))
359
+ ax2.set_yticks(ticks=yticks, labels=[f'{tick:.0f}' for tick in yticks])
360
+ ax2.set_xticks(minor_xticks, minor=True)
361
+
362
+ return fig, ax1
@@ -1,3 +1,4 @@
1
1
  from ._color import Color
2
- from ._decorator import set_figure
3
2
  from ._unit import Unit
3
+ from .plt_utils import *
4
+ from .sklearn_utils import *