oafuncs 0.0.59__py2.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.
oafuncs/oa_draw.py ADDED
@@ -0,0 +1,431 @@
1
+ #!/usr/bin/env python
2
+ # coding=utf-8
3
+ '''
4
+ Author: Liu Kun && 16031215@qq.com
5
+ Date: 2024-09-17 17:26:11
6
+ LastEditors: Liu Kun && 16031215@qq.com
7
+ LastEditTime: 2024-10-20 21:12:58
8
+ FilePath: \\Python\\My_Funcs\\OAFuncs\\OAFuncs\\oa_draw.py
9
+ Description:
10
+ EditPlatform: vscode
11
+ ComputerInfo: XPS 15 9510
12
+ SystemInfo: Windows 11
13
+ Python Version: 3.11
14
+ '''
15
+
16
+
17
+ import math
18
+ import warnings
19
+
20
+ import cartopy.crs as ccrs
21
+ import cartopy.feature as cfeature
22
+ import matplotlib as mpl
23
+ import matplotlib.pyplot as plt
24
+ import numpy as np
25
+ import xarray as xr
26
+ from cartopy.mpl.ticker import LatitudeFormatter, LongitudeFormatter
27
+ from mpl_toolkits.axes_grid1 import make_axes_locatable
28
+
29
+ __all__ = ['create_gif', 'xy2lonlat', 'plot_contourf',
30
+ 'plot_contourf_lonlat', 'plot_quiver', 'plot_contourf_cartopy']
31
+
32
+ warnings.filterwarnings('ignore')
33
+
34
+
35
+ # ** 将生成图片/已有图片制作成动图
36
+ def create_gif(image_list: list, gif_name: str, duration=0.2): # 制作动图,默认间隔0.2
37
+ '''
38
+ func : 制作动图,将已有图片拼接
39
+ description : Gif格式动图
40
+ param {*} image_list 图片列表
41
+ param {*} gif_name 动图名称(含路径)
42
+ param {*} duration 动图间隔
43
+ return {*} 自动保存至指定路径(包含于动图名称中)
44
+ example :
45
+ '''
46
+ import imageio.v2 as imageio
47
+ frames = []
48
+ for image_name in image_list:
49
+ frames.append(imageio.imread(image_name))
50
+ imageio.mimsave(gif_name, frames, format='GIF', duration=duration)
51
+ print('Gif制作完成!')
52
+ return
53
+
54
+
55
+ # ** 转化经/纬度刻度
56
+ def xy2lonlat(xy, lonlat='lon', decimal=2):
57
+ '''
58
+ param {*} xy : 经/纬度列表
59
+ param {*} lonlat : 'lon' or 'lat'
60
+ param {*} decimal : 小数位数
61
+ return {*} 转化后的经/纬度列表
62
+ example : xy2lonlat(x, lonlat='lon', decimal=2)
63
+ '''
64
+ def format_longitude(x_list):
65
+ out_list = []
66
+ for x in x_list:
67
+ if x > 180:
68
+ x -= 360
69
+ # degrees = int(abs(x))
70
+ degrees = round(abs(x), decimal)
71
+ direction = "E" if x >= 0 else "W"
72
+ out_list.append(
73
+ f"{degrees:.{decimal}f}°{direction}" if x != 0 and x != 180 else f"{degrees}°")
74
+ return out_list if len(out_list) > 1 else out_list[0]
75
+
76
+ def format_latitude(y_list):
77
+ out_list = []
78
+ for y in y_list:
79
+ if y > 90:
80
+ y -= 180
81
+ # degrees = int(abs(y))
82
+ degrees = round(abs(y), decimal)
83
+ direction = "N" if y >= 0 else "S"
84
+ out_list.append(
85
+ f"{degrees:.{decimal}f}°{direction}" if y != 0 else f"{degrees}°")
86
+ return out_list if len(out_list) > 1 else out_list[0]
87
+
88
+ if lonlat == 'lon':
89
+ return format_longitude(xy)
90
+ elif lonlat == 'lat':
91
+ return format_latitude(xy)
92
+
93
+
94
+ # ** 设置colorbar格式
95
+ class _MyFormatter(mpl.ticker.ScalarFormatter):
96
+ def __init__(self, cticks, fmt=None, useOffset=True, useMathText=True):
97
+ mpl.ticker.ScalarFormatter.__init__(self, useOffset, useMathText)
98
+ self.cticks = cticks
99
+ self.fmt = fmt
100
+ self.vmin = min(cticks)
101
+ self.vmax = max(cticks)
102
+ self.p_n = self.vmin < 0 and self.vmax > 0
103
+ self.magnitude_min = int(
104
+ math.modf(math.log10(min(abs(cticks[cticks != 0]))))[1])
105
+ self.magnitude_max = int(math.modf(math.log10(max(abs(cticks))))[1])
106
+ # print(self.vmin, self.vmax)
107
+
108
+ def __call__(self, x, pos):
109
+ if ((abs(x) < 1e-2) or (abs(x) > 1e4)) and x != 0:
110
+ # if self.magnitude_max - self.magnitude_min == 1 and (int(math.modf(math.log10(abs(x)))[1]) == self.magnitude_min):
111
+ # a, b = '{:.1e}'.format(x).split('e')
112
+ # b = int(b)
113
+ # return '${}{:.2f} \\times 10^{{{}}}$'.format(' ' if self.p_n and x > 0 else '', float(a)/10, b+1)
114
+ # else:
115
+ # return '${}{} \\times 10^{{{}}}$'.format(' ' if self.p_n and x > 0 else '', *'{:.2e}'.format(x).split('e'))
116
+ if self.magnitude_max - self.magnitude_min == 1 and (int(math.modf(math.log10(abs(x)))[1]) == self.magnitude_min):
117
+ a, b = '{:.1e}'.format(x).split('e')
118
+ a = float(a) / 10
119
+ b = int(b) + 1
120
+ else:
121
+ a, b = '{:.2e}'.format(x).split('e')
122
+ a = float(a)
123
+ b = int(b)
124
+ # return '${}{} \\times 10^{{{}}}$'.format(' ' if (self.p_n and x > 0) else '', a, b)
125
+ return '${}{:.2f}$'.format(' ' if (self.p_n and x > 0) else '', a)
126
+ elif x == 0:
127
+ return '0'
128
+ else:
129
+ return mpl.ticker.ScalarFormatter.__call__(self, x, pos)
130
+
131
+
132
+ # ** 绘制单张填色图
133
+ def plot_contourf(pic_data, picname=None, c_map='rainbow', minmax=None, labels=None, ticks_space=None, ticks=None, figsize=(12, 9)):
134
+ '''
135
+ func : 绘制填色等值线图,单张
136
+ description : 绘制单张填色等值线图,输入参数为横纵坐标、等值数据、图例标注等
137
+ param {*}pic_data : 填色等值线图的等值数据
138
+ param {*}picname : 图片保存的文件名(含路径)
139
+ param {*}c_map : 颜色映射,默认rainbow
140
+ param {*}minmax : 指定绘图的最大、小值,默认不指定
141
+ param {*}labels : x、y轴以及图例的标注,默认不标注
142
+ param {*}ticks_space : x、y轴刻度,以及所显示的标签,默认不显示
143
+ param {*}figsize : 图片大小,默认(12,9)
144
+ example : plot_contourf(pic_data, pictpath, var_name, c_map='bwr', labels=None, ticks_space=None, ticks=None, h=0, figsize=(12, 9))
145
+ '''
146
+ cmap = mpl.colormaps.get_cmap(c_map)
147
+ if minmax is not None:
148
+ value_min, value_max = minmax[0], minmax[1]
149
+ else:
150
+ value_min, value_max = pic_data.nanmin(), pic_data.nanmax()
151
+ v_bry = max(abs(value_min), abs(value_max))
152
+ flag = (value_min < 0) and (value_max > 0)
153
+ norm = mpl.colors.TwoSlopeNorm(
154
+ vmin=-1 * v_bry, vcenter=0, vmax=v_bry) if flag else mpl.colors.Normalize(vmin=value_min, vmax=value_max)
155
+ cticks = [num for num in np.linspace(-1 * v_bry if flag else value_min,
156
+ v_bry if flag else value_max, 9)] if value_min != value_max else None
157
+ levels = np.linspace(-1 * v_bry, v_bry,
158
+ 20) if flag else None if value_min == value_max else np.linspace(value_min, value_max, 20)
159
+
160
+ shape = np.array(pic_data).shape
161
+ x, y = np.meshgrid(np.arange(shape[1]), np.arange(shape[0]))
162
+
163
+ fig, ax = plt.subplots(figsize=figsize)
164
+ flag_lc = levels is not None and cticks is not None
165
+ CS = ax.contourf(x, y, pic_data, cmap=cmap, norm=norm, levels=levels, extend='both') if flag_lc else ax.contourf(
166
+ x, y, pic_data, cmap=cmap, norm=norm, extend='both')
167
+ cb = fig.colorbar(CS, ax=ax, orientation='vertical', shrink=1, format='%.3g', spacing='uniform', ticks=cticks) if cticks is not None else fig.colorbar(
168
+ CS, ax=ax, orientation='vertical', shrink=1, format='%.3g', spacing='uniform')
169
+ """%.3g采用的是自动调整格式,也可设置为%.3f,则改为3位小数"""
170
+
171
+ # 将格式化器设置为自定义的函数
172
+ class_cticks = np.array(cb.get_ticks())
173
+ # print(class_cticks)
174
+ cct = abs(class_cticks[class_cticks != 0])
175
+ if (min(cct) < 1e-2) or max(cct) > 1e4: # 判断是否需要采用科学计数法
176
+ cb.formatter = _MyFormatter(class_cticks)
177
+ cb.update_ticks()
178
+
179
+ if labels is not None:
180
+ cb.set_label(labels['c'])
181
+ plt.xlabel(labels['x'])
182
+ plt.ylabel(labels['y'])
183
+ if ticks_space is not None and ticks is not None:
184
+ plt.xticks(np.arange(0, len(x[0, :]) +
185
+ 1e-5, ticks_space['x']), ticks['x'])
186
+ plt.yticks(np.arange(0, len(y[:, 0]) +
187
+ 1e-5, ticks_space['y']), ticks['y'])
188
+
189
+ plt.title('Min: {:.3g}, Max: {:.3g}'.format(
190
+ pic_data.min(), pic_data.max()))
191
+
192
+ plt.savefig(
193
+ picname, bbox_inches='tight') if picname is not None else plt.show()
194
+ # plt.show()
195
+ plt.clf()
196
+ # 关闭当前figure
197
+ plt.close()
198
+
199
+
200
+ # ** 画等高线图,带经纬度坐标轴
201
+ def plot_contourf_lonlat(data, lon, lat, interval=5, picname=None, c_map='rainbow'):
202
+ '''
203
+ param {*} data : 二维数据
204
+ param {*} lon : 经度
205
+ param {*} lat : 纬度
206
+ param {*} interval : 经纬度间隔
207
+ param {*} picname : 图片保存的文件名(含路径)
208
+ param {*} c_map : 颜色映射,默认rainbow
209
+ return {*} 无返回值
210
+ '''
211
+ if len(lon.shape) == 2:
212
+ lon = lon[0, :]
213
+ if len(lat.shape) == 2:
214
+ lat = lat[:, 0]
215
+ # interval是经纬度间隔,单位为°
216
+ # 将lon,lat作为坐标轴刻度显示
217
+
218
+ def format_longitude(x):
219
+ if x > 180:
220
+ x -= 360
221
+ degrees = int(abs(x))
222
+ direction = "E" if x >= 0 else "W"
223
+ return f"{degrees}°{direction}" if x != 0 and x != 180 else f"{degrees}°"
224
+
225
+ def format_latitude(y):
226
+ if y > 90:
227
+ y -= 180
228
+ degrees = int(abs(y))
229
+ direction = "N" if y >= 0 else "S"
230
+ return f"{degrees}°{direction}" if y != 0 else f"{degrees}°"
231
+
232
+ plt.contourf(data, cmap=c_map)
233
+ x_space = int(len(lon) * interval / (lon[-1] - lon[0]))
234
+ y_space = int(len(lat) * interval / (lat[-1] - lat[0]))
235
+ plt.xticks(np.arange(0, len(lon), x_space), [
236
+ format_longitude(lon[i]) for i in range(0, len(lon), x_space)])
237
+ plt.yticks(np.arange(0, len(lat), y_space), [
238
+ format_latitude(lat[i]) for i in range(0, len(lat), y_space)])
239
+ plt.colorbar()
240
+ plt.savefig(
241
+ picname, bbox_inches='tight') if picname is not None else plt.show()
242
+ plt.close()
243
+
244
+
245
+ # ** 绘制矢量场
246
+ def plot_quiver(u, v, lon, lat, picname=None, cmap='coolwarm', scale=0.25, width=0.002, x_space=5, y_space=5):
247
+ '''
248
+ param {*} u : 二维数据
249
+ param {*} v : 二维数据
250
+ param {*} lon : 经度, 1D or 2D
251
+ param {*} lat : 纬度, 1D or 2D
252
+ param {*} picname : 图片保存的文件名(含路径)
253
+ param {*} cmap : 颜色映射,默认coolwarm
254
+ param {*} scale : 箭头的大小 / 缩小程度
255
+ param {*} width : 箭头的宽度
256
+ param {*} x_space : x轴间隔
257
+ param {*} y_space : y轴间隔
258
+ return {*} 无返回值
259
+ '''
260
+ # 创建新的网格位置变量(lat_c, lon_c)
261
+ if len(lon.shape) == 1 and len(lat.shape) == 1:
262
+ lon_c, lat_c = np.meshgrid(lon, lat)
263
+ else:
264
+ lon_c, lat_c = lon, lat
265
+
266
+ # 设置箭头的比例、颜色、宽度等参数
267
+ # scale = 0.25 # 箭头的大小 / 缩小程度
268
+ # color = '#E5D1FA'
269
+ # width = 0.002 # 箭头的宽度
270
+ # x_space = 1
271
+ # y_space = 1
272
+
273
+ # 计算矢量的大小
274
+ S = xr.DataArray(np.hypot(np.array(u), np.array(v)))
275
+
276
+ mean_S = S.nanmean()
277
+
278
+ # 使用 plt.quiver 函数绘制矢量图
279
+ # 通过设置 quiver 函数的 pivot 参数来指定箭头的位置
280
+ quiver_plot = plt.quiver(lon_c[::y_space, ::x_space],
281
+ lat_c[::y_space, ::x_space],
282
+ u[::y_space, ::x_space],
283
+ v[::y_space, ::x_space],
284
+ S[::y_space, ::x_space], # 矢量的大小,可以不要
285
+ pivot='middle',
286
+ scale=scale,
287
+ # color=color, # 矢量的颜色,单色
288
+ cmap=cmap, # 矢量的颜色,多色
289
+ width=width)
290
+ # plt.quiverkey(quiver_plot, X=0.90, Y=0.975, U=1, label='1 m/s', labelpos='E', fontproperties={'size': 10})
291
+ plt.quiverkey(quiver_plot, X=0.87, Y=0.975, U=mean_S,
292
+ label=f'{mean_S:.2f} m/s', labelpos='E', fontproperties={'size': 10})
293
+ plt.colorbar(quiver_plot)
294
+ plt.xlabel('X')
295
+ plt.ylabel('Y')
296
+
297
+ plt.savefig(
298
+ picname, bbox_inches='tight') if picname is not None else plt.show()
299
+ plt.clf()
300
+ plt.close()
301
+
302
+
303
+ def plot_contourf_cartopy(data, lon, lat, picname=None, cmap='rainbow', cn_fill_num=20, fig_size=(12, 9), title='Cartopy', land_color='green', ocean_color='lightgrey'):
304
+ '''
305
+ param {*} data : 二维数据
306
+ param {*} lon : 经度
307
+ param {*} lat : 纬度
308
+ param {*} picname : 图片保存的文件名(含路径)
309
+ param {*} cmap : 颜色映射,默认rainbow
310
+ param {*} cn_fill_num : 等值线数量
311
+ param {*} fig_size : 图片大小,默认(12,9)
312
+ param {*} title : 图片标题
313
+ param {*} land_color : 陆地颜色
314
+ param {*} ocean_color : 海洋颜色
315
+ return {*} 无返回值
316
+ '''
317
+ if len(lon.shape) == 2:
318
+ lon = lon[0, :]
319
+ if len(lat.shape) == 2:
320
+ lat = lat[:, 0]
321
+
322
+ data_max = np.nanmax(data)
323
+ data_min = np.nanmin(data)
324
+ levels = np.linspace(data_min, data_max, cn_fill_num)
325
+ cbar_ticks = np.linspace(data_min, data_max, 9)
326
+
327
+ fig = plt.figure(figsize=fig_size)
328
+ proj = ccrs.PlateCarree()
329
+ ax = fig.add_subplot(1, 1, 1, projection=proj)
330
+
331
+ ax.set_extent([lon[0], lon[-1], lat[0], lat[-1]], crs=proj)
332
+
333
+ ax.add_feature(cfeature.LAND, facecolor=land_color, edgecolor="k")
334
+ ax.add_feature(cfeature.COASTLINE, linewidth=0.5)
335
+ # ax.add_feature(cfeature.BORDERS, linewidth=0.5, linestyle=":") # 加载国界线
336
+
337
+ ax.set_xticks(np.arange(lon[0], lon[-1] + 1e-5, 5), crs=proj)
338
+ ax.set_yticks(np.arange(lat[0], lat[-1] + 1e-5, 5), crs=proj)
339
+ lon_formatter = LongitudeFormatter(zero_direction_label=True)
340
+ ax.xaxis.set_major_formatter(lon_formatter)
341
+ lat_formatter = LatitudeFormatter()
342
+ ax.yaxis.set_major_formatter(lat_formatter)
343
+ # plt.title(title, fontsize=16)
344
+
345
+ cmap = plt.get_cmap(cmap)
346
+ X, Y = np.meshgrid(lon, lat)
347
+
348
+ # Fill color for land and ocean
349
+ # ax.background_patch.set_facecolor(ocean_color)
350
+ ax.patch.set_facecolor(ocean_color)
351
+ ax.add_feature(cfeature.LAND, facecolor=land_color)
352
+
353
+ cticks = cbar_ticks
354
+ norm = mpl.colors.BoundaryNorm(cticks, cmap.N)
355
+
356
+ cnplot = ax.contourf(X, Y, data, levels=levels, cmap=cmap,
357
+ norm=norm, transform=proj, extend='both', alpha=1, zorder=0)
358
+ # cllevels = np.linspace(data_min, data_max, 9)
359
+ # clplot = ax.contour(X, Y, data, levels=levels[9::10], colors='k', linewidths=0.5, transform=proj, zorder=1, alpha=0.8, linestyle='--')
360
+ # 添加色标,并选择位置
361
+ divider = make_axes_locatable(ax)
362
+ location = 3
363
+ if location == 1: # 左侧
364
+ cax = divider.new_horizontal(
365
+ size="5%", pad=1, axes_class=plt.Axes, pack_start=True)
366
+ fig.add_axes(cax)
367
+ cbar = plt.colorbar(
368
+ cnplot, cax=cax, orientation='vertical', extend='both')
369
+ elif location == 2: # 下方
370
+ cax = divider.new_vertical(
371
+ size="5%", pad=0.3, axes_class=plt.Axes, pack_start=True)
372
+ fig.add_axes(cax)
373
+ cbar = plt.colorbar(
374
+ cnplot, cax=cax, orientation='horizontal', extend='both')
375
+ elif location == 3: # 右侧
376
+ cax = divider.new_horizontal(size="5%", pad=0.1, axes_class=plt.Axes)
377
+ fig.add_axes(cax)
378
+ # cbar = plt.colorbar(cnplot, cax=cax, orientation='vertical', extend='both', format='%.0f')
379
+ cbar = fig.colorbar(mpl.cm.ScalarMappable(cmap=cmap, norm=norm),
380
+ cax=cax, orientation='vertical', extend='both', format='%.3f')
381
+ cax.yaxis.set_ticks_position('right')
382
+ cax.yaxis.set_label_position('right')
383
+ else: # 上方
384
+ cax = divider.new_vertical(size="5%", pad=0.2, axes_class=plt.Axes)
385
+ fig.add_axes(cax)
386
+ cbar = plt.colorbar(
387
+ cnplot, cax=cax, orientation='horizontal', extend='both')
388
+ cbar.ax.tick_params(labelsize=10)
389
+ cbar.ax.xaxis.set_tick_params(direction='in', width=1, length=2)
390
+ # 添加cbar_ticks
391
+ # cbar.set_ticks(np.arange(round(levels[0]), round(levels[-1]), round((levels[-1]-levels[0])/9))) # 设置色标刻度
392
+ # cbar.set_ticks(cbar_ticks) # 设置色标刻度
393
+
394
+ cbar.set_ticks(cticks)
395
+ # cbar.ax.ticks = np.linspace(data_min, data_max, 8)
396
+ # cbar.set_ticks(np.arange(round(levels[0]), round(levels[-1]), round((levels[-1]-levels[0])/9))) # 设置色标刻度
397
+
398
+ # 单独设置label
399
+ cbar.set_label(title, fontsize=10,
400
+ weight='bold')
401
+ # cax.set_position([0.1, 0.2, 0.02, 0.6]) # 调整色标位置
402
+ fig.savefig(
403
+ picname, bbox_inches='tight', dpi=600) if picname is not None else plt.show()
404
+ plt.close()
405
+
406
+
407
+ if __name__ == '__main__':
408
+ # ** 绘制填色图
409
+ data = np.random.randn(100, 100)
410
+ picname = 'test.png'
411
+ plot_contourf(data, picname, c_map='rainbow', minmax=None,
412
+ labels=None, ticks_space=None, ticks=None, figsize=(12, 9))
413
+ # ** 绘制矢量场
414
+ u = np.random.randn(100, 100)
415
+ v = np.random.randn(100, 100)
416
+ lon = np.linspace(0, 360, 100)
417
+ lat = np.linspace(-90, 90, 100)
418
+ picname = 'test.png'
419
+ plot_quiver(u, v, lon, lat, picname, cmap='coolwarm',
420
+ scale=0.25, width=0.002, x_space=5, y_space=5)
421
+ # ** 绘制经纬度填色图
422
+ data = np.random.randn(100, 100)
423
+ lon = np.linspace(0, 360, 100)
424
+ lat = np.linspace(-90, 90, 100)
425
+ picname = 'test.png'
426
+ plot_contourf_lonlat(data, lon, lat, interval=5,
427
+ picname=picname, c_map='rainbow')
428
+ # ** 制作动图
429
+ image_list = ['test1.png', 'test2.png', 'test3.png']
430
+ gif_name = 'test.gif'
431
+ create_gif(image_list, gif_name, duration=0.2)