AeroViz 0.1.21__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.
Files changed (180) hide show
  1. AeroViz/__init__.py +13 -0
  2. AeroViz/__pycache__/__init__.cpython-312.pyc +0 -0
  3. AeroViz/data/DEFAULT_DATA.csv +1417 -0
  4. AeroViz/data/DEFAULT_PNSD_DATA.csv +1417 -0
  5. AeroViz/data/hysplit_example_data.txt +101 -0
  6. AeroViz/dataProcess/Chemistry/__init__.py +149 -0
  7. AeroViz/dataProcess/Chemistry/__pycache__/__init__.cpython-312.pyc +0 -0
  8. AeroViz/dataProcess/Chemistry/_calculate.py +557 -0
  9. AeroViz/dataProcess/Chemistry/_isoropia.py +150 -0
  10. AeroViz/dataProcess/Chemistry/_mass_volume.py +487 -0
  11. AeroViz/dataProcess/Chemistry/_ocec.py +172 -0
  12. AeroViz/dataProcess/Chemistry/isrpia.cnf +21 -0
  13. AeroViz/dataProcess/Chemistry/isrpia2.exe +0 -0
  14. AeroViz/dataProcess/Optical/PyMieScatt_update.py +577 -0
  15. AeroViz/dataProcess/Optical/_IMPROVE.py +452 -0
  16. AeroViz/dataProcess/Optical/__init__.py +281 -0
  17. AeroViz/dataProcess/Optical/__pycache__/PyMieScatt_update.cpython-312.pyc +0 -0
  18. AeroViz/dataProcess/Optical/__pycache__/__init__.cpython-312.pyc +0 -0
  19. AeroViz/dataProcess/Optical/__pycache__/mie_theory.cpython-312.pyc +0 -0
  20. AeroViz/dataProcess/Optical/_derived.py +518 -0
  21. AeroViz/dataProcess/Optical/_extinction.py +123 -0
  22. AeroViz/dataProcess/Optical/_mie_sd.py +912 -0
  23. AeroViz/dataProcess/Optical/_retrieve_RI.py +243 -0
  24. AeroViz/dataProcess/Optical/coefficient.py +72 -0
  25. AeroViz/dataProcess/Optical/fRH.pkl +0 -0
  26. AeroViz/dataProcess/Optical/mie_theory.py +260 -0
  27. AeroViz/dataProcess/README.md +271 -0
  28. AeroViz/dataProcess/SizeDistr/__init__.py +245 -0
  29. AeroViz/dataProcess/SizeDistr/__pycache__/__init__.cpython-312.pyc +0 -0
  30. AeroViz/dataProcess/SizeDistr/__pycache__/_size_dist.cpython-312.pyc +0 -0
  31. AeroViz/dataProcess/SizeDistr/_size_dist.py +810 -0
  32. AeroViz/dataProcess/SizeDistr/merge/README.md +93 -0
  33. AeroViz/dataProcess/SizeDistr/merge/__init__.py +20 -0
  34. AeroViz/dataProcess/SizeDistr/merge/_merge_v0.py +251 -0
  35. AeroViz/dataProcess/SizeDistr/merge/_merge_v0_1.py +246 -0
  36. AeroViz/dataProcess/SizeDistr/merge/_merge_v1.py +255 -0
  37. AeroViz/dataProcess/SizeDistr/merge/_merge_v2.py +244 -0
  38. AeroViz/dataProcess/SizeDistr/merge/_merge_v3.py +518 -0
  39. AeroViz/dataProcess/SizeDistr/merge/_merge_v4.py +422 -0
  40. AeroViz/dataProcess/SizeDistr/prop.py +62 -0
  41. AeroViz/dataProcess/VOC/__init__.py +14 -0
  42. AeroViz/dataProcess/VOC/__pycache__/__init__.cpython-312.pyc +0 -0
  43. AeroViz/dataProcess/VOC/_potential_par.py +108 -0
  44. AeroViz/dataProcess/VOC/support_voc.json +446 -0
  45. AeroViz/dataProcess/__init__.py +66 -0
  46. AeroViz/dataProcess/__pycache__/__init__.cpython-312.pyc +0 -0
  47. AeroViz/dataProcess/core/__init__.py +272 -0
  48. AeroViz/dataProcess/core/__pycache__/__init__.cpython-312.pyc +0 -0
  49. AeroViz/mcp_server.py +352 -0
  50. AeroViz/plot/__init__.py +13 -0
  51. AeroViz/plot/__pycache__/__init__.cpython-312.pyc +0 -0
  52. AeroViz/plot/__pycache__/bar.cpython-312.pyc +0 -0
  53. AeroViz/plot/__pycache__/box.cpython-312.pyc +0 -0
  54. AeroViz/plot/__pycache__/pie.cpython-312.pyc +0 -0
  55. AeroViz/plot/__pycache__/radar.cpython-312.pyc +0 -0
  56. AeroViz/plot/__pycache__/regression.cpython-312.pyc +0 -0
  57. AeroViz/plot/__pycache__/scatter.cpython-312.pyc +0 -0
  58. AeroViz/plot/__pycache__/violin.cpython-312.pyc +0 -0
  59. AeroViz/plot/bar.py +126 -0
  60. AeroViz/plot/box.py +69 -0
  61. AeroViz/plot/distribution/__init__.py +1 -0
  62. AeroViz/plot/distribution/__pycache__/__init__.cpython-312.pyc +0 -0
  63. AeroViz/plot/distribution/__pycache__/distribution.cpython-312.pyc +0 -0
  64. AeroViz/plot/distribution/distribution.py +576 -0
  65. AeroViz/plot/meteorology/CBPF.py +295 -0
  66. AeroViz/plot/meteorology/__init__.py +3 -0
  67. AeroViz/plot/meteorology/__pycache__/CBPF.cpython-312.pyc +0 -0
  68. AeroViz/plot/meteorology/__pycache__/__init__.cpython-312.pyc +0 -0
  69. AeroViz/plot/meteorology/__pycache__/hysplit.cpython-312.pyc +0 -0
  70. AeroViz/plot/meteorology/__pycache__/wind_rose.cpython-312.pyc +0 -0
  71. AeroViz/plot/meteorology/hysplit.py +93 -0
  72. AeroViz/plot/meteorology/wind_rose.py +77 -0
  73. AeroViz/plot/optical/__init__.py +1 -0
  74. AeroViz/plot/optical/__pycache__/__init__.cpython-312.pyc +0 -0
  75. AeroViz/plot/optical/__pycache__/optical.cpython-312.pyc +0 -0
  76. AeroViz/plot/optical/optical.py +388 -0
  77. AeroViz/plot/pie.py +210 -0
  78. AeroViz/plot/radar.py +184 -0
  79. AeroViz/plot/regression.py +200 -0
  80. AeroViz/plot/scatter.py +174 -0
  81. AeroViz/plot/templates/__init__.py +6 -0
  82. AeroViz/plot/templates/__pycache__/__init__.cpython-312.pyc +0 -0
  83. AeroViz/plot/templates/__pycache__/ammonium_rich.cpython-312.pyc +0 -0
  84. AeroViz/plot/templates/__pycache__/contour.cpython-312.pyc +0 -0
  85. AeroViz/plot/templates/__pycache__/corr_matrix.cpython-312.pyc +0 -0
  86. AeroViz/plot/templates/__pycache__/diurnal_pattern.cpython-312.pyc +0 -0
  87. AeroViz/plot/templates/__pycache__/koschmieder.cpython-312.pyc +0 -0
  88. AeroViz/plot/templates/__pycache__/metal_heatmap.cpython-312.pyc +0 -0
  89. AeroViz/plot/templates/ammonium_rich.py +34 -0
  90. AeroViz/plot/templates/contour.py +47 -0
  91. AeroViz/plot/templates/corr_matrix.py +267 -0
  92. AeroViz/plot/templates/diurnal_pattern.py +61 -0
  93. AeroViz/plot/templates/koschmieder.py +95 -0
  94. AeroViz/plot/templates/metal_heatmap.py +164 -0
  95. AeroViz/plot/timeseries/__init__.py +2 -0
  96. AeroViz/plot/timeseries/__pycache__/__init__.cpython-312.pyc +0 -0
  97. AeroViz/plot/timeseries/__pycache__/template.cpython-312.pyc +0 -0
  98. AeroViz/plot/timeseries/__pycache__/timeseries.cpython-312.pyc +0 -0
  99. AeroViz/plot/timeseries/template.py +47 -0
  100. AeroViz/plot/timeseries/timeseries.py +446 -0
  101. AeroViz/plot/utils/__init__.py +4 -0
  102. AeroViz/plot/utils/__pycache__/__init__.cpython-312.pyc +0 -0
  103. AeroViz/plot/utils/__pycache__/_color.cpython-312.pyc +0 -0
  104. AeroViz/plot/utils/__pycache__/_unit.cpython-312.pyc +0 -0
  105. AeroViz/plot/utils/__pycache__/plt_utils.cpython-312.pyc +0 -0
  106. AeroViz/plot/utils/__pycache__/sklearn_utils.cpython-312.pyc +0 -0
  107. AeroViz/plot/utils/_color.py +71 -0
  108. AeroViz/plot/utils/_unit.py +55 -0
  109. AeroViz/plot/utils/fRH.json +390 -0
  110. AeroViz/plot/utils/plt_utils.py +92 -0
  111. AeroViz/plot/utils/sklearn_utils.py +49 -0
  112. AeroViz/plot/utils/units.json +89 -0
  113. AeroViz/plot/violin.py +80 -0
  114. AeroViz/rawDataReader/FLOW.md +138 -0
  115. AeroViz/rawDataReader/__init__.py +220 -0
  116. AeroViz/rawDataReader/__pycache__/__init__.cpython-312.pyc +0 -0
  117. AeroViz/rawDataReader/config/__init__.py +0 -0
  118. AeroViz/rawDataReader/config/__pycache__/__init__.cpython-312.pyc +0 -0
  119. AeroViz/rawDataReader/config/__pycache__/supported_instruments.cpython-312.pyc +0 -0
  120. AeroViz/rawDataReader/config/supported_instruments.py +135 -0
  121. AeroViz/rawDataReader/core/__init__.py +658 -0
  122. AeroViz/rawDataReader/core/__pycache__/__init__.cpython-312.pyc +0 -0
  123. AeroViz/rawDataReader/core/__pycache__/logger.cpython-312.pyc +0 -0
  124. AeroViz/rawDataReader/core/__pycache__/pre_process.cpython-312.pyc +0 -0
  125. AeroViz/rawDataReader/core/__pycache__/qc.cpython-312.pyc +0 -0
  126. AeroViz/rawDataReader/core/__pycache__/report.cpython-312.pyc +0 -0
  127. AeroViz/rawDataReader/core/logger.py +171 -0
  128. AeroViz/rawDataReader/core/pre_process.py +308 -0
  129. AeroViz/rawDataReader/core/qc.py +961 -0
  130. AeroViz/rawDataReader/core/report.py +579 -0
  131. AeroViz/rawDataReader/script/AE33.py +173 -0
  132. AeroViz/rawDataReader/script/AE43.py +151 -0
  133. AeroViz/rawDataReader/script/APS.py +339 -0
  134. AeroViz/rawDataReader/script/Aurora.py +191 -0
  135. AeroViz/rawDataReader/script/BAM1020.py +90 -0
  136. AeroViz/rawDataReader/script/BC1054.py +161 -0
  137. AeroViz/rawDataReader/script/EPA.py +79 -0
  138. AeroViz/rawDataReader/script/GRIMM.py +68 -0
  139. AeroViz/rawDataReader/script/IGAC.py +140 -0
  140. AeroViz/rawDataReader/script/MA350.py +179 -0
  141. AeroViz/rawDataReader/script/Minion.py +218 -0
  142. AeroViz/rawDataReader/script/NEPH.py +199 -0
  143. AeroViz/rawDataReader/script/OCEC.py +173 -0
  144. AeroViz/rawDataReader/script/Q-ACSM.py +12 -0
  145. AeroViz/rawDataReader/script/SMPS.py +389 -0
  146. AeroViz/rawDataReader/script/TEOM.py +181 -0
  147. AeroViz/rawDataReader/script/VOC.py +106 -0
  148. AeroViz/rawDataReader/script/Xact.py +244 -0
  149. AeroViz/rawDataReader/script/__init__.py +28 -0
  150. AeroViz/rawDataReader/script/__pycache__/AE33.cpython-312.pyc +0 -0
  151. AeroViz/rawDataReader/script/__pycache__/AE43.cpython-312.pyc +0 -0
  152. AeroViz/rawDataReader/script/__pycache__/APS.cpython-312.pyc +0 -0
  153. AeroViz/rawDataReader/script/__pycache__/Aurora.cpython-312.pyc +0 -0
  154. AeroViz/rawDataReader/script/__pycache__/BAM1020.cpython-312.pyc +0 -0
  155. AeroViz/rawDataReader/script/__pycache__/BC1054.cpython-312.pyc +0 -0
  156. AeroViz/rawDataReader/script/__pycache__/EPA.cpython-312.pyc +0 -0
  157. AeroViz/rawDataReader/script/__pycache__/GRIMM.cpython-312.pyc +0 -0
  158. AeroViz/rawDataReader/script/__pycache__/IGAC.cpython-312.pyc +0 -0
  159. AeroViz/rawDataReader/script/__pycache__/MA350.cpython-312.pyc +0 -0
  160. AeroViz/rawDataReader/script/__pycache__/Minion.cpython-312.pyc +0 -0
  161. AeroViz/rawDataReader/script/__pycache__/NEPH.cpython-312.pyc +0 -0
  162. AeroViz/rawDataReader/script/__pycache__/OCEC.cpython-312.pyc +0 -0
  163. AeroViz/rawDataReader/script/__pycache__/Q-ACSM.cpython-312.pyc +0 -0
  164. AeroViz/rawDataReader/script/__pycache__/SMPS.cpython-312.pyc +0 -0
  165. AeroViz/rawDataReader/script/__pycache__/TEOM.cpython-312.pyc +0 -0
  166. AeroViz/rawDataReader/script/__pycache__/VOC.cpython-312.pyc +0 -0
  167. AeroViz/rawDataReader/script/__pycache__/Xact.cpython-312.pyc +0 -0
  168. AeroViz/rawDataReader/script/__pycache__/__init__.cpython-312.pyc +0 -0
  169. AeroViz/tools/__init__.py +2 -0
  170. AeroViz/tools/__pycache__/__init__.cpython-312.pyc +0 -0
  171. AeroViz/tools/__pycache__/database.cpython-312.pyc +0 -0
  172. AeroViz/tools/__pycache__/dataclassifier.cpython-312.pyc +0 -0
  173. AeroViz/tools/database.py +95 -0
  174. AeroViz/tools/dataclassifier.py +117 -0
  175. AeroViz/tools/dataprinter.py +58 -0
  176. aeroviz-0.1.21.dist-info/METADATA +294 -0
  177. aeroviz-0.1.21.dist-info/RECORD +180 -0
  178. aeroviz-0.1.21.dist-info/WHEEL +5 -0
  179. aeroviz-0.1.21.dist-info/licenses/LICENSE +21 -0
  180. aeroviz-0.1.21.dist-info/top_level.txt +1 -0
@@ -0,0 +1,446 @@
1
+ from typing import Literal
2
+
3
+ import matplotlib.pyplot as plt
4
+ import numpy as np
5
+ import pandas as pd
6
+ from matplotlib.cm import ScalarMappable
7
+ from matplotlib.pyplot import Figure, Axes
8
+ from mpl_toolkits.axes_grid1 import make_axes_locatable
9
+ from mpl_toolkits.axes_grid1.inset_locator import inset_axes
10
+ from pandas import DataFrame, date_range, Timedelta
11
+
12
+ from AeroViz.plot.utils import *
13
+
14
+ __all__ = ['timeseries', 'timeseries_stacked']
15
+
16
+ default_bar_kws = dict(
17
+ width=0.0417,
18
+ edgecolor=None,
19
+ linewidth=0,
20
+ cmap='jet',
21
+ )
22
+
23
+ default_scatter_kws = dict(
24
+ marker='o',
25
+ s=5,
26
+ edgecolor=None,
27
+ linewidths=0.3,
28
+ alpha=0.9,
29
+ cmap='jet',
30
+ )
31
+
32
+ default_insert_kws = dict(
33
+ width="1.5%",
34
+ height="100%",
35
+ loc='lower left',
36
+ bbox_to_anchor=(1.01, 0, 1.2, 1),
37
+ borderpad=0
38
+ )
39
+
40
+ default_plot_kws = dict()
41
+
42
+ default_cbar_kws = dict()
43
+
44
+
45
+ def _scatter(ax, df, _y, _c, scatter_kws, cbar_kws, inset_kws):
46
+ if _c is None or _c not in df.columns:
47
+ scatter_kws.pop('cmap')
48
+ ax.scatter(df.index, df[_y], **scatter_kws)
49
+ else:
50
+ ax.scatter(df.index, df[_y], c=df[_c], **scatter_kws)
51
+ cax = inset_axes(ax, **inset_kws)
52
+
53
+ # Filter the children to find ScalarMappable objects
54
+ mappable_objects = [child for child in ax.get_children() if isinstance(child, ScalarMappable)]
55
+
56
+ # Use the first mappable object for the colorbar
57
+ if mappable_objects:
58
+ plt.colorbar(mappable=mappable_objects[0], cax=cax, **cbar_kws)
59
+ else:
60
+ print("No mappable objects found.")
61
+
62
+
63
+ def _bar(ax, df, _y, _c, bar_kws, cbar_kws, inset_kws):
64
+ scalar_map, colors = Color.color_maker(df[_c].values, cmap=bar_kws.pop('cmap'))
65
+ ax.bar(df.index, df[_y], color=scalar_map.to_rgba(colors), **bar_kws)
66
+ cax = inset_axes(ax, **inset_kws)
67
+ plt.colorbar(mappable=scalar_map, cax=cax, **cbar_kws)
68
+
69
+
70
+ def _plot(ax, df, _y, _color, plot_kws):
71
+ ax.plot(df.index, df[_y], color=_color, **plot_kws)
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
+
108
+ def process_timeseries_data(df, rolling=None, interpolate_limit=None, full_time_index=None):
109
+ # 1. 先建立完整的時間索引
110
+ if full_time_index is None:
111
+ full_time_index = pd.date_range(start=df.index.min(), end=df.index.max(), freq='h') # 或其他適合的頻率
112
+
113
+ # 2. 重新索引,這會產生缺失值而不是丟棄時間點
114
+ df = df.reindex(full_time_index)
115
+
116
+ # apply interpolation if specified
117
+ df = df.interpolate(method='time', limit=interpolate_limit) if interpolate_limit is not None else df
118
+
119
+ # apply rolling window if specified
120
+ df = df.rolling(window=rolling, min_periods=1).mean(numeric_only=True) if rolling is not None else df
121
+
122
+ return df
123
+
124
+
125
+ @set_figure(autolayout=False)
126
+ def timeseries(df: DataFrame,
127
+ y: list[str] | str,
128
+ y2: list[str] | str = None,
129
+ yi: list[str] | str = None,
130
+ color: list[str] | str | None = None,
131
+ label: list[str] | str | None = None,
132
+ rolling: int | str | None = 3,
133
+ interpolate_limit: int | None = 6,
134
+ major_freq: str = '1MS',
135
+ minor_freq: str = '10d',
136
+ style: list[Literal['scatter', 'bar', 'line', 'arrow']] | str | None = None,
137
+ ax: Axes | None = None,
138
+ set_xaxis_visible: bool | None = None,
139
+ legend_loc: Literal['best', 'upper right', 'upper left', 'lower left', 'lower right'] = 'best',
140
+ legend_ncol: int = 1,
141
+ **kwargs
142
+ ) -> tuple[Figure, Axes]:
143
+ """
144
+ Plot the timeseries data with the option of scatterplot, barplot, and lineplot.
145
+
146
+ Parameters
147
+ -----------
148
+ df : DataFrame
149
+ The data to plot.
150
+ y : list[str] | str
151
+ The primary y-axis data columns.
152
+ y2 : list[str] | str, optional
153
+ The secondary y-axis data columns. Defaults to None.
154
+ yi : list[str] | str, optional
155
+ The components for percentage calculation. Defaults to None.
156
+ color : str, optional
157
+ The column for color mapping or the color. Defaults to None.
158
+ label : str, optional
159
+ The label for the legend. Defaults to None.
160
+ rolling : str | int | None, optional
161
+ Rolling window size for smoothing. Defaults to None.
162
+ interpolate_limit : int, optional
163
+ Interpolation limit for missing values. Defaults to None.
164
+ major_freq : str, optional
165
+ Frequency for x-axis ticks. Defaults to '1MS'.
166
+ minor_freq : str, optional
167
+ Frequency for x-axis minor ticks. Defaults to '10d'.
168
+ style : Literal['scatter', 'bar', 'line'] | None, optional
169
+ Style of the plot. Defaults to 'scatter'.
170
+ ax : Axes | None, optional
171
+ Matplotlib Axes object to plot on. Defaults to None.
172
+ set_xaxis_visible : bool | None, optional
173
+ Whether to set x-axis visibility. Defaults to None.
174
+ legend_loc : Literal['best', 'upper right', 'upper left', 'lower left', 'lower right'], optional
175
+ Location of the legend. Defaults to 'best'.
176
+ legend_ncol : int, optional
177
+ Number of columns in the legend. Defaults to 1.
178
+ **kwargs : Additional keyword arguments for customization.
179
+ fig_kws : dict, optional
180
+ Additional keyword arguments for the figure. Defaults to {}.
181
+ scatter_kws : dict, optional
182
+ Additional keyword arguments for the scatter plot. Defaults to {}.
183
+ bar_kws : dict, optional
184
+ Additional keyword arguments for the bar plot. Defaults to {}.
185
+ ax_plot_kws : dict, optional
186
+ Additional keyword arguments for the primary y-axis plot. Defaults to {}.
187
+ ax2_plot_kws : dict, optional
188
+ Additional keyword arguments for the secondary y-axis plot. Defaults to {}.
189
+ cbar_kws : dict, optional
190
+ Additional keyword arguments for the colorbar. Defaults to {}.
191
+ inset_kws : dict, optional
192
+ Additional keyword arguments for the inset axes. Defaults to {}.
193
+
194
+ Returns
195
+ -------
196
+ ax : AxesSubplot
197
+ Matplotlib AxesSubplot.
198
+
199
+ Example
200
+ -------
201
+ >>> timeseries(df, y='WS', color='WD', scatter_kws=dict(cmap='hsv'), cbar_kws=dict(ticks=[0, 90, 180, 270, 360]), ylim=[0, None])
202
+ """
203
+ # Set the time
204
+ try:
205
+ st_tm, fn_tm = df.index[0], df.index[-1]
206
+ except IndexError:
207
+ raise IndexError("The DataFrame is empty. Please provide a valid DataFrame.")
208
+
209
+ # calculate the percentage of each component
210
+ if yi is not None:
211
+ df_pct = df[yi].div(df[yi].sum(axis=1), axis=0) * 100
212
+ mean = [f"{_label} : {df[comp].mean():.2f}" for _label, comp in zip(label, yi)]
213
+ pct = [f"{_label} : {df_pct[comp].mean():.2f}%" for _label, comp in zip(label, yi)]
214
+ df_pct = process_timeseries_data(df_pct, rolling, interpolate_limit)
215
+
216
+ # process data
217
+ df = process_timeseries_data(df, rolling, interpolate_limit)
218
+
219
+ # Initialize figure and axis if not provided
220
+ fig, ax = plt.subplots(**{**{'figsize': (6, 2)}, **kwargs.get('fig_kws', {})}) if ax is None else (
221
+ ax.get_figure(), ax)
222
+
223
+ # Ensure y, y2, c, and style are lists
224
+ y = [y] if isinstance(y, str) else y
225
+ y2 = [y2] if isinstance(y2, str) else y2 if y2 is not None else []
226
+ color = [color] if isinstance(color, str) else color if color is not None else [None] * (len(y) + len(y2))
227
+ label = [label] if isinstance(label, str) else label if label is not None else [None] * (len(y) + len(y2))
228
+ style = [style] if isinstance(style, str) else style if style is not None else ['plot'] * (len(y) + len(y2))
229
+
230
+ for name, lst in [("c", color), ("style", style), ("label", label)]:
231
+ if len(lst) != len(y) + len(y2):
232
+ raise ValueError(f"The length of {name} must match the combined length of y and y2")
233
+
234
+ # Create a secondary y-axis if y2 is not empty
235
+ ax2 = ax.twinx() if y2 else None
236
+
237
+ # # Set color cycle
238
+ ax.set_prop_cycle(Color.color_cycle)
239
+ if y2:
240
+ ax2.set_prop_cycle(Color.color_cycle[len(y):])
241
+
242
+ if y2 and ('scatter' or 'bar') in style:
243
+ fig.subplots_adjust(right=0.8)
244
+
245
+ # for i, _c in enumerate(color):
246
+ # if _c is not None and _c in df.columns:
247
+ # style[i] = 'scatter'
248
+
249
+ for i, (_y, _c, _label, _style) in enumerate(zip(y, color, label, style)):
250
+ scatter_kws = {**default_scatter_kws, **{'label': Unit(_y)}, **kwargs.get('scatter_kws', {})}
251
+ bar_kws = {**default_bar_kws, **{'label': Unit(_y)}, **kwargs.get('bar_kws', {})}
252
+ plot_kws = {**default_plot_kws, **{'label': Unit(_y)}, **kwargs.get('plot_kws', {})}
253
+
254
+ if _style in ['scatter', 'bar', 'arrow']:
255
+ cbar_kws = {**default_cbar_kws, **{'label': Unit(_c), 'ticks': None}, **kwargs.get('cbar_kws', {})}
256
+ inset_kws = {**default_insert_kws, **{'bbox_transform': ax.transAxes}, **kwargs.get('inset_kws', {})}
257
+
258
+ if _style == 'scatter':
259
+ _scatter(ax, df, _y, _c, scatter_kws, cbar_kws, inset_kws)
260
+
261
+ elif _style == 'bar':
262
+ _bar(ax, df, _y, _c, bar_kws, cbar_kws, inset_kws)
263
+
264
+ elif _style == 'arrow':
265
+ _wind_arrow(ax, df, _y, _c, scatter_kws, cbar_kws, inset_kws)
266
+
267
+ else:
268
+ _plot(ax, df, _y, _c, plot_kws)
269
+
270
+ if y2:
271
+ for i, (_y, _c, _style) in enumerate(zip(y2, color[len(y):], style[len(y):])):
272
+ scatter_kws = {**default_scatter_kws, **{'label': Unit(_y)}, **kwargs.get('scatter_kws2', {})}
273
+ bar_kws = {**default_bar_kws, **{'label': Unit(_y)}, **kwargs.get('bar_kws2', {})}
274
+ plot_kws = {**default_plot_kws, **{'label': Unit(_y)}, **kwargs.get('plot_kws2', {})}
275
+
276
+ if _style in ['scatter', 'bar']:
277
+ cbar_kws = {**default_cbar_kws, **{'label': Unit(_c), 'ticks': None}, **kwargs.get('cbar_kws2', {})}
278
+ inset_kws = {**default_insert_kws, **{'bbox_transform': ax.transAxes}, **kwargs.get('inset_kws2', {})}
279
+
280
+ if _style == 'scatter':
281
+ _scatter(ax2, df, _y, _c, scatter_kws, cbar_kws, inset_kws)
282
+
283
+ elif _style == 'bar':
284
+ _bar(ax2, df, _y, _c, bar_kws, cbar_kws, inset_kws)
285
+
286
+ elif _style == 'arrow':
287
+ pass
288
+
289
+ else: # line plot
290
+ _plot(ax2, df, _y, _c, plot_kws)
291
+
292
+ # Combine legends from ax and ax2
293
+ ax.legend(*combine_legends([ax, ax2]), loc=legend_loc, ncol=legend_ncol)
294
+
295
+ else:
296
+ ax.legend(loc=legend_loc, ncol=legend_ncol)
297
+
298
+ if set_xaxis_visible is not None:
299
+ ax.axes.xaxis.set_visible(set_xaxis_visible)
300
+
301
+ ax.set(xlabel=kwargs.get('xlabel', ''),
302
+ ylabel=kwargs.get('ylabel', Unit(y) if isinstance(y, str) else Unit(y[0])),
303
+ xlim=kwargs.get('xlim', (st_tm, fn_tm)),
304
+ ylim=kwargs.get('ylim', (None, None)),
305
+ title=kwargs.get('title', '')
306
+ )
307
+
308
+ xticks = kwargs.get('xticks', date_range(start=st_tm, end=fn_tm, freq=major_freq))
309
+ minor_xticks = kwargs.get('minor_xticks', date_range(start=st_tm, end=fn_tm, freq=minor_freq))
310
+
311
+ ax.set_xticks(ticks=xticks, labels=xticks.strftime("%F"))
312
+ ax.set_xticks(minor_xticks, minor=True)
313
+
314
+ if y2:
315
+ ax2.set(ylim=kwargs.get('ylim2', (None, None)),
316
+ ylabel=kwargs.get('ylabel2', Unit(y2) if isinstance(y2, str) else Unit(y2[0]))
317
+ )
318
+
319
+ plt.show()
320
+
321
+ return fig, ax
322
+
323
+
324
+ @set_figure(figsize=(6, 3), fs=6, autolayout=False)
325
+ def timeseries_stacked(df,
326
+ y: list[str] | str,
327
+ yi: list[str] | str,
328
+ label: list[str] | str,
329
+ plot_type: Literal["absolute", "percentage", "both"] | str = 'both',
330
+ rolling: int | str | None = 4,
331
+ interpolate_limit: int | None = 4,
332
+ major_freq: str = '10d',
333
+ minor_freq: str = '1d',
334
+ support_df: DataFrame | None = None,
335
+ ax: Axes | None = None,
336
+ **kwargs
337
+ ) -> tuple[Figure, Axes]:
338
+ try:
339
+ st_tm, fn_tm = df.index[0], df.index[-1]
340
+ except IndexError:
341
+ raise IndexError("The DataFrame is empty. Please provide a valid DataFrame.")
342
+
343
+ if plot_type not in ['absolute', 'percentage', 'both']:
344
+ raise ValueError("plot_type must be one of 'absolute', 'percentage', or 'both'")
345
+
346
+ # calculate the percentage of each component
347
+ df = df.dropna()
348
+ df_pct = df[yi].div(df[yi].sum(axis=1), axis=0) * 100
349
+
350
+ mean = [f"{_label} : {df[comp].mean():.2f}" for _label, comp in zip(label, yi)]
351
+ pct = [f"{_label} : {df_pct[comp].mean():.2f}%" for _label, comp in zip(label, yi)]
352
+
353
+ full_time_index = pd.date_range(start=st_tm, end=fn_tm, freq='h')
354
+
355
+ # process data
356
+ df = process_timeseries_data(df, rolling, interpolate_limit, full_time_index)
357
+ df_pct = process_timeseries_data(df_pct, rolling, interpolate_limit, full_time_index)
358
+
359
+ # Set figure size based on plot_type
360
+ figsize = (7, 6) if plot_type == 'both' else (7, 3)
361
+ if plot_type == 'both':
362
+ fig, (ax1, ax2) = plt.subplots(2, 1, **{**{'figsize': figsize, 'dpi': 600}, **kwargs.get('fig_kws', {})})
363
+ else:
364
+ fig, ax1 = plt.subplots(1, 1, **{**{'figsize': figsize, 'dpi': 600}, **kwargs.get('fig_kws', {})})
365
+
366
+ plt.subplots_adjust(right=0.95)
367
+ width = 0.0417
368
+ color = Color.colors1
369
+
370
+ for name, lst in [("color", color), ("label", label)]:
371
+ if len(lst) != len(yi):
372
+ raise ValueError(f"The length of {name} must match the combined length of y and y2")
373
+
374
+ def plot_stacked_bars(ax, data, labels, is_percentage=False):
375
+ bottom = None
376
+ for i, (_column, _color, _label) in enumerate(zip(yi, color, labels)):
377
+ if i == 0:
378
+ bottom = data[_column] * 0
379
+ ax.bar(data.index, data[_column], color=_color, width=width, bottom=bottom, label=_label)
380
+ bottom += data[_column]
381
+
382
+ # Set axis properties
383
+ if kwargs.get('legend', True):
384
+ ax.legend(loc='upper left', ncol=2, prop={'weight': 'bold'}, bbox_to_anchor=(0.75, 0, 0.2, 1))
385
+
386
+ ylim = (0, 100) if is_percentage else kwargs.get('ylim', (None, None))
387
+ ylabel = 'Percentage (%)' if is_percentage else (
388
+ kwargs.get('ylabel', Unit(y) if isinstance(y, str) else Unit(y[0])))
389
+
390
+ ax.set(xlabel=kwargs.get('xlabel', ''),
391
+ xlim=kwargs.get('xlim', (st_tm, fn_tm)),
392
+ ylim=ylim,
393
+ title=kwargs.get('title', ''))
394
+
395
+ ax.set_ylabel(ylabel, fontsize=12)
396
+
397
+ # Set ticks
398
+ xticks = kwargs.get('xticks', date_range(start=st_tm, end=fn_tm, freq=major_freq))
399
+ yticks = kwargs.get('yticks', np.linspace(*ax.get_ylim(), num=6))
400
+ minor_xticks = kwargs.get('minor_xticks', date_range(start=st_tm, end=fn_tm, freq=minor_freq))
401
+
402
+ ax.set_xticks(ticks=xticks, labels=xticks.strftime("%F"))
403
+ ax.set_yticks(ticks=yticks, labels=[f'{tick:.0f}' for tick in yticks])
404
+ ax.set_xticks(minor_xticks, minor=True)
405
+
406
+ # Plot based on plot_type
407
+ if plot_type in ['absolute', 'both']:
408
+ plot_stacked_bars(ax1, df, mean, is_percentage=False)
409
+ if plot_type == 'absolute':
410
+ ax1.axes.xaxis.set_visible(True)
411
+
412
+ if support_df is not None: # 確保support_df存在
413
+ # 創建次要Y軸
414
+ ax_right = ax1.twinx()
415
+
416
+ support_df = process_timeseries_data(support_df, rolling, interpolate_limit, full_time_index)
417
+
418
+ # 繪製線圖在次要Y軸上
419
+ ax_right.plot(support_df.index, support_df['PM2.5'],
420
+ color='black', linewidth=1.5,
421
+ label=f'Measured $PM_{{2.5}}$')
422
+
423
+ # ax_right.plot(support_df.index, support_df['PM10'],
424
+ # color='gray', linewidth=1.5,
425
+ # label=f'Measured $PM_{{10}}$')
426
+
427
+ # 設置次要Y軸的標籤和格式
428
+ # ax_right.set_ylabel(Unit('PM2.5'), fontsize=12)
429
+ ax_right.set_ylim(0, 120)
430
+ ax_right.axes.yaxis.set_visible(False)
431
+
432
+ # ax_right.tick_params(axis='y', colors='black')
433
+ # ax_right.legend(loc='upper right', prop={'size': 12})
434
+
435
+ if plot_type in ['percentage', 'both']:
436
+ ax_pct = ax2 if plot_type == 'both' else ax1
437
+ plot_stacked_bars(ax_pct, df_pct, pct, is_percentage=True)
438
+
439
+ if plot_type == 'both':
440
+ pass
441
+ # ax1.axes.xaxis.set_visible(False)
442
+
443
+ plt.savefig('/Users/chanchihyu/Desktop/times_stacked.png', transparent=True)
444
+
445
+ plt.show()
446
+ return fig, ax1
@@ -0,0 +1,4 @@
1
+ from ._color import Color
2
+ from ._unit import Unit
3
+ from .plt_utils import *
4
+ from .sklearn_utils import *
@@ -0,0 +1,71 @@
1
+ import matplotlib.colors as plc
2
+ import matplotlib.pyplot as plt
3
+ import numpy as np
4
+ import seaborn as sns
5
+ from cycler import cycler
6
+ from matplotlib import colormaps
7
+
8
+ __all__ = ['Color']
9
+
10
+
11
+ class Color:
12
+ color_cycle = cycler(color=['b', 'g', 'r', 'c', 'm', 'y', 'k'])
13
+
14
+ linecolor = [{'line': '#1a56db', 'edge': '#0F50A6', 'face': '#5983D9'},
15
+ {'line': '#046c4e', 'edge': '#1B591F', 'face': '#538C4A'},
16
+ {'line': '#c81e1e', 'edge': '#f05252', 'face': '#f98080'}]
17
+
18
+ # colors = ['#FF3333', '#33FF33', '#FFFF33', '#5555FF', '#B94FFF', '#AAAAAA', '#748690'] # the last one is "unknown"
19
+
20
+ colors1 = ['#A65E58', '#A5BF6B', '#F2BF5E', '#3F83BF', '#B777C2', '#D1CFCB']
21
+ colors2 = ['#A65E58', '#A5BF6B', '#F2BF5E', '#3F83BF', '#B777C2', '#D1CFCB', '#96c8e6']
22
+ colors3 = ['#A65E58', '#A5BF6B', '#a6710d', '#F2BF5E', '#3F83BF', '#B777C2', '#D1CFCB', '#96c8e6'] # POC SOC
23
+
24
+ colors_mutiWater = ['#A65E58', '#c18e8a', '#A5BF6B', '#c5d6a0', '#F2BF5E', '#3F83BF', '#c089ca', '#d3acda',
25
+ '#D1CFCB']
26
+ colors_mutiWater2 = ['#A65E58', '#96c8e6', '#A5BF6B', '#96c8e6', '#F2BF5E', '#3F83BF', '#c089ca', '#96c8e6',
27
+ '#D1CFCB'] # water
28
+
29
+ color_choose = {'Clean': ['#1d4a9f', '#84a7e9'],
30
+ 'Transition': ['#4a9f1d', '#a7e984'],
31
+ 'Event': ['#9f1d4a', '#e984a7']}
32
+
33
+ paired = [plt.get_cmap('Paired')(i) for i in range(4)]
34
+
35
+ @staticmethod
36
+ def getColor(num: int = 6, cmap: str = 'jet_r'):
37
+ category_colors = plt.colormaps[cmap](np.linspace(0.1, 0.9, num))
38
+ return [plc.to_hex(category_colors[i]) for i in range(num)]
39
+
40
+ @staticmethod
41
+ def palplot(*args, **kwargs):
42
+ sns.palplot(*args, **kwargs)
43
+
44
+ @staticmethod
45
+ def adjust_opacity(colors: str | list[str], alpha: float):
46
+ if isinstance(colors, str):
47
+ colors = [colors]
48
+
49
+ adjusted_colors = []
50
+ for color in colors:
51
+ # 將顏色轉換為RGB表示
52
+ r, g, b = int(color[1:3], 16), int(color[3:5], 16), int(color[5:7], 16)
53
+ # 調整透明度
54
+ r_new = int(alpha * r + (1 - alpha) * 255)
55
+ g_new = int(alpha * g + (1 - alpha) * 255)
56
+ b_new = int(alpha * b + (1 - alpha) * 255)
57
+ # 轉換為新的色碼
58
+ new_color = '#{:02X}{:02X}{:02X}'.format(r_new, g_new, b_new)
59
+ adjusted_colors.append(new_color)
60
+ return adjusted_colors
61
+
62
+ @staticmethod
63
+ def color_maker(obj, cmap='Blues'):
64
+ colors = np.nan_to_num(obj, nan=0)
65
+ scalar_map = plt.cm.ScalarMappable(cmap=colormaps[cmap]) # create a scalar map for the colorbar
66
+ scalar_map.set_array(colors)
67
+ return scalar_map, colors
68
+
69
+
70
+ if __name__ == '__main__':
71
+ Color.palplot(Color.colors2)
@@ -0,0 +1,55 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ __all__ = ['Unit']
5
+
6
+
7
+ class Unit:
8
+ file_path = Path(__file__).parent / 'units.json'
9
+ data = None
10
+
11
+ def __new__(cls, unit: str):
12
+ cls.data = cls.load_jsonfile()
13
+ try:
14
+ value = cls.data[unit]
15
+ return r'${}$'.format(value.replace(' ', r'\ '))
16
+ except KeyError:
17
+ print(f"Attribute '{unit}' not found. Using default value.")
18
+ return r'${}$'.format(unit.replace(' ', r'\ ')) if unit is not None else 'None'
19
+
20
+ @classmethod
21
+ def load_jsonfile(cls):
22
+ """ 讀取 JSON 檔中數據并將其變成屬性 """
23
+ try:
24
+ with open(cls.file_path, 'r', encoding='utf-8') as f:
25
+ return json.load(f)
26
+
27
+ except FileNotFoundError:
28
+ print(f"JSON file '{cls.file_path}' not found.")
29
+ except json.JSONDecodeError:
30
+ print(f"Invalid JSON format in '{cls.file_path}'.")
31
+
32
+ @classmethod
33
+ def update_jsonfile(cls, key, value):
34
+ """ 更新JSON檔 """
35
+ with open(cls.file_path, 'r', encoding='utf-8') as f:
36
+ old_data = json.load(f)
37
+
38
+ old_data[key] = value
39
+
40
+ with open(cls.file_path, 'w', encoding='utf-8') as f:
41
+ json.dump(old_data, f, indent=4)
42
+
43
+ @classmethod
44
+ def del_jsonfile(cls, key):
45
+ """ 更新JSON檔 """
46
+ with open(cls.file_path, 'r', encoding='utf-8') as f:
47
+ old_data = json.load(f)
48
+
49
+ if key in old_data:
50
+ del old_data[key]
51
+
52
+ with open(cls.file_path, 'w', encoding='utf-8') as f:
53
+ json.dump(old_data, f, indent=4)
54
+ else:
55
+ print(f"Key '{key}' not found.")