paperplotter 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.
- examples/Data_Analysis_Utils/data_analysis_utils_example.png +0 -0
- examples/Data_Analysis_Utils/data_analysis_utils_example.py +81 -0
- examples/Data_Analysis_Utils/utility_functions_example.png +0 -0
- examples/Data_Analysis_Utils/utility_functions_example.py +82 -0
- examples/Domain_Specific_Plots/bifurcation_diagram_example.png +0 -0
- examples/Domain_Specific_Plots/bifurcation_diagram_example.py +70 -0
- examples/Domain_Specific_Plots/concentration_map_example.png +0 -0
- examples/Domain_Specific_Plots/concentration_map_example.py +54 -0
- examples/Domain_Specific_Plots/domain_specific_plots.png +0 -0
- examples/Domain_Specific_Plots/domain_specific_plots_example.py +104 -0
- examples/Domain_Specific_Plots/learning_curve_example.png +0 -0
- examples/Domain_Specific_Plots/learning_curve_example.py +86 -0
- examples/Domain_Specific_Plots/phasor_diagram_example.png +0 -0
- examples/Domain_Specific_Plots/phasor_diagram_example.py +49 -0
- examples/Domain_Specific_Plots/power_timeseries_example.png +0 -0
- examples/Domain_Specific_Plots/power_timeseries_example.py +65 -0
- examples/Features_Customization/advanced_customization.py +70 -0
- examples/Features_Customization/advanced_customization_figure.png +0 -0
- examples/Features_Customization/cleanup_demonstration.py +62 -0
- examples/Features_Customization/cleanup_demonstration_figure.png +0 -0
- examples/Features_Customization/composite_figure.png +0 -0
- examples/Features_Customization/composite_figure_example.py +88 -0
- examples/Features_Customization/error_handling_test.py +75 -0
- examples/Features_Customization/feature_expansion_example.py +84 -0
- examples/Features_Customization/feature_expansion_figure.png +0 -0
- examples/Features_Customization/fig_annotation_example.png +0 -0
- examples/Features_Customization/fig_annotation_example.py +72 -0
- examples/Features_Customization/fig_text_example.png +0 -0
- examples/Features_Customization/fig_text_example.py +51 -0
- examples/Features_Customization/global_controls_example.py +66 -0
- examples/Features_Customization/global_controls_figure.png +0 -0
- examples/Features_Customization/heatmap_colorbar_example.py +68 -0
- examples/Features_Customization/heatmap_default_figure.png +0 -0
- examples/Features_Customization/heatmap_shared_colorbar_figure.png +0 -0
- examples/Features_Customization/highlighting_example.png +0 -0
- examples/Features_Customization/highlighting_example.py +62 -0
- examples/Features_Customization/multi_plot_grid.py +91 -0
- examples/Features_Customization/multi_plot_grid_figure.png +0 -0
- examples/Features_Customization/resources/placeholder_image.png +0 -0
- examples/Layout/advanced_layout_example.py +70 -0
- examples/Layout/advanced_layout_figure.png +0 -0
- examples/Layout/aspect_ratio_example.py +72 -0
- examples/Layout/aspect_ratio_mosaic.png +0 -0
- examples/Layout/aspect_ratio_simple_grid.png +0 -0
- examples/Layout/block_span_example.py +64 -0
- examples/Layout/block_span_figure.png +0 -0
- examples/Layout/declarative_nested_layout.png +0 -0
- examples/Layout/declarative_nested_layout_example.py +91 -0
- examples/Layout/row_span_example.py +68 -0
- examples/Layout/row_span_figure.png +0 -0
- examples/Styles_Aesthetics/aesthetic_and_processing_example.png +0 -0
- examples/Styles_Aesthetics/aesthetic_and_processing_example.py +85 -0
- examples/Styles_Aesthetics/statistical_annotation_example.png +0 -0
- examples/Styles_Aesthetics/statistical_annotation_example.py +65 -0
- examples/Styles_Aesthetics/style_gallery_example.py +64 -0
- examples/Styles_Aesthetics/style_gallery_flat.png +0 -0
- examples/Styles_Aesthetics/style_gallery_nord.png +0 -0
- examples/Styles_Aesthetics/style_gallery_presentation.png +0 -0
- examples/Styles_Aesthetics/style_gallery_publication.png +0 -0
- examples/Styles_Aesthetics/style_gallery_solarized_light.png +0 -0
- paperplot/__init__.py +30 -0
- paperplot/core.py +254 -0
- paperplot/exceptions.py +35 -0
- paperplot/mixins/__init__.py +1 -0
- paperplot/mixins/domain.py +331 -0
- paperplot/mixins/generic.py +95 -0
- paperplot/mixins/modifiers.py +504 -0
- paperplot/styles/flat.mplstyle +40 -0
- paperplot/styles/nord.mplstyle +44 -0
- paperplot/styles/presentation.mplstyle +41 -0
- paperplot/styles/publication.mplstyle +24 -0
- paperplot/styles/solarized_light.mplstyle +44 -0
- paperplot/utils.py +510 -0
- paperplotter-0.1.4.dist-info/METADATA +163 -0
- paperplotter-0.1.4.dist-info/RECORD +78 -0
- paperplotter-0.1.4.dist-info/WHEEL +5 -0
- paperplotter-0.1.4.dist-info/licenses/LICENSE +21 -0
- paperplotter-0.1.4.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
# paperplot/mixins/modifiers.py
|
|
2
|
+
|
|
3
|
+
from typing import Optional, Union, List
|
|
4
|
+
import matplotlib.pyplot as plt
|
|
5
|
+
import matplotlib.image as mpimg
|
|
6
|
+
import logging
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
class ModifiersMixin:
|
|
10
|
+
"""
|
|
11
|
+
包含用于修改、装饰和收尾图表的方法的 Mixin 类。
|
|
12
|
+
"""
|
|
13
|
+
def __init__(self, *args, **kwargs):
|
|
14
|
+
super().__init__(*args, **kwargs) #确保调用父类的__init__
|
|
15
|
+
self._draw_on_save_queue = []
|
|
16
|
+
|
|
17
|
+
def set_title(self, tag: Union[str, int], label: str, **kwargs) -> 'Plotter':
|
|
18
|
+
"""
|
|
19
|
+
为指定子图设置标题。
|
|
20
|
+
"""
|
|
21
|
+
self._get_ax_by_tag(tag).set_title(label, **kwargs)
|
|
22
|
+
return self
|
|
23
|
+
|
|
24
|
+
def set_xlabel(self, tag: Union[str, int], label: str, **kwargs) -> 'Plotter':
|
|
25
|
+
"""
|
|
26
|
+
为指定子图设置X轴标签。
|
|
27
|
+
"""
|
|
28
|
+
self._get_ax_by_tag(tag).set_xlabel(label, **kwargs)
|
|
29
|
+
return self
|
|
30
|
+
|
|
31
|
+
def set_ylabel(self, tag: Union[str, int], label: str, **kwargs) -> 'Plotter':
|
|
32
|
+
"""
|
|
33
|
+
为指定子图设置Y轴标签。
|
|
34
|
+
"""
|
|
35
|
+
self._get_ax_by_tag(tag).set_ylabel(label, **kwargs)
|
|
36
|
+
return self
|
|
37
|
+
|
|
38
|
+
def set_xlim(self, tag: Union[str, int], *args, **kwargs) -> 'Plotter':
|
|
39
|
+
"""
|
|
40
|
+
为指定子图设置X轴的显示范围。
|
|
41
|
+
"""
|
|
42
|
+
self._get_ax_by_tag(tag).set_xlim(*args, **kwargs)
|
|
43
|
+
return self
|
|
44
|
+
|
|
45
|
+
def set_ylim(self, tag: Union[str, int], *args, **kwargs) -> 'Plotter':
|
|
46
|
+
"""
|
|
47
|
+
为指定子图设置Y轴的显示范围。
|
|
48
|
+
"""
|
|
49
|
+
self._get_ax_by_tag(tag).set_ylim(*args, **kwargs)
|
|
50
|
+
return self
|
|
51
|
+
|
|
52
|
+
def tick_params(self, tag: Union[str, int], axis: str = 'both', **kwargs) -> 'Plotter':
|
|
53
|
+
"""
|
|
54
|
+
为指定子图的刻度线、刻度标签和网格线设置参数。
|
|
55
|
+
"""
|
|
56
|
+
self._get_ax_by_tag(tag).tick_params(axis=axis, **kwargs)
|
|
57
|
+
return self
|
|
58
|
+
|
|
59
|
+
def set_legend(self, tag: Union[str, int], **kwargs) -> 'Plotter':
|
|
60
|
+
"""
|
|
61
|
+
为指定子图添加图例。
|
|
62
|
+
"""
|
|
63
|
+
self._get_ax_by_tag(tag).legend(**kwargs)
|
|
64
|
+
return self
|
|
65
|
+
|
|
66
|
+
def set_suptitle(self, title: str, **kwargs):
|
|
67
|
+
"""
|
|
68
|
+
为整个画布(Figure)设置一个主标题。
|
|
69
|
+
"""
|
|
70
|
+
self.fig.suptitle(title, **kwargs)
|
|
71
|
+
return self
|
|
72
|
+
|
|
73
|
+
def fig_add_text(self, x: float, y: float, text: str, **kwargs) -> 'Plotter':
|
|
74
|
+
"""
|
|
75
|
+
在整个画布(Figure)的指定位置添加文本。
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
x (float): 文本的X坐标,范围从0到1(图的左下角为(0,0),右上角为(1,1))。
|
|
79
|
+
y (float): 文本的Y坐标,范围从0到1。
|
|
80
|
+
text (str): 要添加的文本内容。
|
|
81
|
+
**kwargs: 其他传递给 `matplotlib.figure.Figure.text` 的关键字参数。
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Plotter: 返回Plotter实例以支持链式调用。
|
|
85
|
+
"""
|
|
86
|
+
self.fig.text(x, y, text, **kwargs)
|
|
87
|
+
return self
|
|
88
|
+
|
|
89
|
+
def fig_add_line(self, x_coords: List[float], y_coords: List[float], **kwargs) -> 'Plotter':
|
|
90
|
+
"""
|
|
91
|
+
在整个画布(Figure)上绘制一条线。
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
x_coords (List[float]): 线的X坐标列表,范围从0到1(图的左下角为(0,0),右上角为(1,1))。
|
|
95
|
+
y_coords (List[float]): 线的Y坐标列表,范围从0到1。
|
|
96
|
+
**kwargs: 其他传递给 `matplotlib.lines.Line2D` 的关键字参数。
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Plotter: 返回Plotter实例以支持链式调用。
|
|
100
|
+
"""
|
|
101
|
+
line = plt.Line2D(x_coords, y_coords, transform=self.fig.transFigure, **kwargs)
|
|
102
|
+
self.fig.add_artist(line)
|
|
103
|
+
return self
|
|
104
|
+
|
|
105
|
+
def fig_add_box(self, tags: Union[str, int, List[Union[str, int]]], padding: float = 0.01, **kwargs) -> 'Plotter':
|
|
106
|
+
"""
|
|
107
|
+
在整个画布(Figure)上,围绕一个或多个指定的子图绘制一个矩形框。
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
tags (Union[str, int, List[Union[str, int]]]):
|
|
111
|
+
一个或多个子图的tag,这些子图将被框选。
|
|
112
|
+
padding (float, optional):
|
|
113
|
+
矩形框相对于子图边界的额外填充(以Figure坐标为单位)。默认为0.01。
|
|
114
|
+
**kwargs: 其他传递给 `matplotlib.patches.Rectangle` 的关键字参数。
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Plotter: 返回Plotter实例以支持链式调用。
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
TagNotFoundError: 如果指定的tag未找到。
|
|
121
|
+
"""
|
|
122
|
+
self.fig.canvas.draw() # 强制重绘以获取准确的坐标
|
|
123
|
+
|
|
124
|
+
if not isinstance(tags, list):
|
|
125
|
+
tags = [tags]
|
|
126
|
+
|
|
127
|
+
min_x, min_y, max_x, max_y = 1.0, 1.0, 0.0, 0.0
|
|
128
|
+
|
|
129
|
+
for tag in tags:
|
|
130
|
+
ax = self._get_ax_by_tag(tag)
|
|
131
|
+
bbox = ax.get_position() # Bounding box in figure coordinates
|
|
132
|
+
|
|
133
|
+
min_x = min(min_x, bbox.x0)
|
|
134
|
+
min_y = min(min_y, bbox.y0)
|
|
135
|
+
max_x = max(max_x, bbox.x1)
|
|
136
|
+
max_y = max(max_y, bbox.y1)
|
|
137
|
+
|
|
138
|
+
# Apply padding
|
|
139
|
+
min_x -= padding
|
|
140
|
+
min_y -= padding
|
|
141
|
+
max_x += padding
|
|
142
|
+
max_y += padding
|
|
143
|
+
|
|
144
|
+
width = max_x - min_x
|
|
145
|
+
height = max_y - min_y
|
|
146
|
+
|
|
147
|
+
# Default kwargs for the rectangle
|
|
148
|
+
kwargs.setdefault('facecolor', 'none')
|
|
149
|
+
kwargs.setdefault('edgecolor', 'black')
|
|
150
|
+
kwargs.setdefault('linewidth', 1.5)
|
|
151
|
+
kwargs.setdefault('linestyle', '--')
|
|
152
|
+
kwargs.setdefault('clip_on', False) # Ensure the box is drawn even if it slightly extends beyond figure limits
|
|
153
|
+
|
|
154
|
+
rect = plt.Rectangle((min_x, min_y), width, height,
|
|
155
|
+
transform=self.fig.transFigure, **kwargs)
|
|
156
|
+
self.fig.add_artist(rect)
|
|
157
|
+
return self
|
|
158
|
+
|
|
159
|
+
def _draw_fig_boundary_box(self, padding: float = 0.02, **kwargs):
|
|
160
|
+
"""
|
|
161
|
+
[私有] 实际执行绘制画布边框的逻辑。
|
|
162
|
+
"""
|
|
163
|
+
all_tags = list(self.tag_to_ax.keys())
|
|
164
|
+
if not all_tags:
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
# Default kwargs for the boundary box
|
|
168
|
+
kwargs.setdefault('facecolor', 'none')
|
|
169
|
+
kwargs.setdefault('edgecolor', 'black')
|
|
170
|
+
kwargs.setdefault('linewidth', 1)
|
|
171
|
+
kwargs.setdefault('clip_on', False)
|
|
172
|
+
|
|
173
|
+
# Re-use the logic from fig_add_box, but don't return self
|
|
174
|
+
self.fig.canvas.draw()
|
|
175
|
+
min_x, min_y, max_x, max_y = 1.0, 1.0, 0.0, 0.0
|
|
176
|
+
for tag in all_tags:
|
|
177
|
+
ax = self._get_ax_by_tag(tag)
|
|
178
|
+
bbox = ax.get_position()
|
|
179
|
+
min_x = min(min_x, bbox.x0)
|
|
180
|
+
min_y = min(min_y, bbox.y0)
|
|
181
|
+
max_x = max(max_x, bbox.x1)
|
|
182
|
+
max_y = max(max_y, bbox.y1)
|
|
183
|
+
|
|
184
|
+
min_x -= padding
|
|
185
|
+
min_y -= padding
|
|
186
|
+
max_x += padding
|
|
187
|
+
max_y += padding
|
|
188
|
+
|
|
189
|
+
width = max_x - min_x
|
|
190
|
+
height = max_y - min_y
|
|
191
|
+
|
|
192
|
+
rect = plt.Rectangle((min_x, min_y), width, height,
|
|
193
|
+
transform=self.fig.transFigure, **kwargs)
|
|
194
|
+
self.fig.add_artist(rect)
|
|
195
|
+
|
|
196
|
+
def fig_add_boundary_box(self, padding: float = 0.02, **kwargs) -> 'Plotter':
|
|
197
|
+
"""
|
|
198
|
+
请求在整个画布(Figure)上,围绕所有子图的组合边界框绘制一个矩形边框。
|
|
199
|
+
实际的绘制操作将延迟到调用 .save() 方法时执行,以确保所有其他元素都已就位。
|
|
200
|
+
"""
|
|
201
|
+
self._draw_on_save_queue.append(
|
|
202
|
+
{'func': self._draw_fig_boundary_box, 'kwargs': {'padding': padding, **kwargs}}
|
|
203
|
+
)
|
|
204
|
+
return self
|
|
205
|
+
|
|
206
|
+
def fig_add_label(self, tags: Union[str, int, List[Union[str, int]]], text: str, position: str = 'top_left', padding: float = 0.01, **kwargs) -> 'Plotter':
|
|
207
|
+
"""
|
|
208
|
+
在整个画布(Figure)上,相对于一个或多个指定的子图放置一个文本标签。
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
tags (Union[str, int, List[Union[str, int]]]):
|
|
212
|
+
一个或多个子图的tag,标签的位置将相对于这些子图的组合边界框。
|
|
213
|
+
text (str): 要添加的标签文本内容。
|
|
214
|
+
position (str, optional):
|
|
215
|
+
标签相对于组合边界框的相对位置。
|
|
216
|
+
可选值:'top_left', 'top_right', 'bottom_left', 'bottom_right',
|
|
217
|
+
'center', 'top_center', 'bottom_center', 'left_center', 'right_center'。
|
|
218
|
+
默认为 'top_left'。
|
|
219
|
+
padding (float, optional):
|
|
220
|
+
标签文本与组合边界框之间的额外间距(以Figure坐标为单位)。默认为0.01。
|
|
221
|
+
**kwargs: 其他传递给 `matplotlib.figure.Figure.text` 的关键字参数。
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
Plotter: 返回Plotter实例以支持链式调用。
|
|
225
|
+
|
|
226
|
+
Raises:
|
|
227
|
+
TagNotFoundError: 如果指定的tag未找到。
|
|
228
|
+
ValueError: 如果`position`参数无效。
|
|
229
|
+
"""
|
|
230
|
+
self.fig.canvas.draw() # 强制重绘以获取准确的坐标
|
|
231
|
+
|
|
232
|
+
if not isinstance(tags, list):
|
|
233
|
+
tags = [tags]
|
|
234
|
+
|
|
235
|
+
min_x, min_y, max_x, max_y = 1.0, 1.0, 0.0, 0.0
|
|
236
|
+
|
|
237
|
+
for tag in tags:
|
|
238
|
+
ax = self._get_ax_by_tag(tag)
|
|
239
|
+
bbox = ax.get_position() # Bounding box in figure coordinates
|
|
240
|
+
|
|
241
|
+
min_x = min(min_x, bbox.x0)
|
|
242
|
+
min_y = min(min_y, bbox.y0)
|
|
243
|
+
max_x = max(max_x, bbox.x1)
|
|
244
|
+
max_y = max(max_y, bbox.y1)
|
|
245
|
+
|
|
246
|
+
# Calculate center and corners of the combined bounding box
|
|
247
|
+
center_x = (min_x + max_x) / 2
|
|
248
|
+
center_y = (min_y + max_y) / 2
|
|
249
|
+
|
|
250
|
+
x, y, ha, va = center_x, center_y, 'center', 'center' # Default to center
|
|
251
|
+
|
|
252
|
+
if position == 'top_left':
|
|
253
|
+
x, y, ha, va = min_x - padding, max_y + padding, 'right', 'bottom'
|
|
254
|
+
elif position == 'top_right':
|
|
255
|
+
x, y, ha, va = max_x + padding, max_y + padding, 'left', 'bottom'
|
|
256
|
+
elif position == 'bottom_left':
|
|
257
|
+
x, y, ha, va = min_x - padding, min_y - padding, 'right', 'top'
|
|
258
|
+
elif position == 'bottom_right':
|
|
259
|
+
x, y, ha, va = max_x + padding, min_y - padding, 'left', 'top'
|
|
260
|
+
elif position == 'top_center':
|
|
261
|
+
x, y, ha, va = center_x, max_y + padding, 'center', 'bottom'
|
|
262
|
+
elif position == 'bottom_center':
|
|
263
|
+
x, y, ha, va = center_x, min_y - padding, 'center', 'top'
|
|
264
|
+
elif position == 'left_center':
|
|
265
|
+
x, y, ha, va = min_x - padding, center_y, 'right', 'center'
|
|
266
|
+
elif position == 'right_center':
|
|
267
|
+
x, y, ha, va = max_x + padding, center_y, 'left', 'center'
|
|
268
|
+
elif position == 'center':
|
|
269
|
+
pass # Already default
|
|
270
|
+
else:
|
|
271
|
+
raise ValueError(f"Invalid position: {position}. Must be one of 'top_left', 'top_right', 'bottom_left', 'bottom_right', 'center', 'top_center', 'bottom_center', 'left_center', 'right_center'.")
|
|
272
|
+
|
|
273
|
+
kwargs.setdefault('ha', ha)
|
|
274
|
+
kwargs.setdefault('va', va)
|
|
275
|
+
kwargs.setdefault('fontsize', 12)
|
|
276
|
+
kwargs.setdefault('weight', 'bold')
|
|
277
|
+
|
|
278
|
+
self.fig.text(x, y, text, **kwargs)
|
|
279
|
+
return self
|
|
280
|
+
|
|
281
|
+
def add_global_legend(self, tags: list = None, remove_sub_legends: bool = True, **kwargs):
|
|
282
|
+
"""
|
|
283
|
+
创建一个作用于整个画布的全局图例。
|
|
284
|
+
"""
|
|
285
|
+
handles, labels = [], []
|
|
286
|
+
ax_to_process = []
|
|
287
|
+
|
|
288
|
+
target_tags = tags if tags is not None else self.tag_to_ax.keys()
|
|
289
|
+
|
|
290
|
+
for tag in target_tags:
|
|
291
|
+
ax = self._get_ax_by_tag(tag)
|
|
292
|
+
h, l = ax.get_legend_handles_labels()
|
|
293
|
+
if h and l:
|
|
294
|
+
handles.extend(h)
|
|
295
|
+
labels.extend(l)
|
|
296
|
+
ax_to_process.append(ax)
|
|
297
|
+
|
|
298
|
+
from collections import OrderedDict
|
|
299
|
+
by_label = OrderedDict(zip(labels, handles))
|
|
300
|
+
|
|
301
|
+
if by_label:
|
|
302
|
+
self.fig.legend(by_label.values(), by_label.keys(), **kwargs)
|
|
303
|
+
|
|
304
|
+
if remove_sub_legends:
|
|
305
|
+
for ax in ax_to_process:
|
|
306
|
+
if ax.get_legend() is not None:
|
|
307
|
+
ax.get_legend().remove()
|
|
308
|
+
|
|
309
|
+
return self
|
|
310
|
+
|
|
311
|
+
def add_twinx(self, tag: str, **kwargs) -> plt.Axes:
|
|
312
|
+
"""
|
|
313
|
+
为一个已存在的子图创建一个共享X轴但拥有独立Y轴的“双Y轴”图。
|
|
314
|
+
"""
|
|
315
|
+
ax1 = self._get_ax_by_tag(tag)
|
|
316
|
+
ax2 = ax1.twinx(**kwargs)
|
|
317
|
+
return ax2
|
|
318
|
+
|
|
319
|
+
def add_hline(self, tag: str, y: float, **kwargs) -> 'Plotter':
|
|
320
|
+
"""
|
|
321
|
+
在指定子图上添加一条水平参考线。
|
|
322
|
+
"""
|
|
323
|
+
ax = self._get_ax_by_tag(tag)
|
|
324
|
+
ax.axhline(y, **kwargs)
|
|
325
|
+
return self
|
|
326
|
+
|
|
327
|
+
def add_vline(self, tag: str, x: float, **kwargs) -> 'Plotter':
|
|
328
|
+
"""
|
|
329
|
+
在指定子图上添加一条垂直参考线。
|
|
330
|
+
"""
|
|
331
|
+
ax = self._get_ax_by_tag(tag)
|
|
332
|
+
ax.axvline(x, **kwargs)
|
|
333
|
+
return self
|
|
334
|
+
|
|
335
|
+
def add_text(self, tag: str, x: float, y: float, text: str, **kwargs) -> 'Plotter':
|
|
336
|
+
"""
|
|
337
|
+
在指定子图的数据坐标系上添加文本。
|
|
338
|
+
"""
|
|
339
|
+
ax = self._get_ax_by_tag(tag)
|
|
340
|
+
ax.text(x, y, text, **kwargs)
|
|
341
|
+
return self
|
|
342
|
+
|
|
343
|
+
def add_patch(self, tag: str, patch_object) -> 'Plotter':
|
|
344
|
+
"""
|
|
345
|
+
将一个Matplotlib的Patch对象添加到指定子图。
|
|
346
|
+
"""
|
|
347
|
+
ax = self._get_ax_by_tag(tag)
|
|
348
|
+
ax.add_patch(patch_object)
|
|
349
|
+
return self
|
|
350
|
+
|
|
351
|
+
def add_highlight_box(self, tag: Union[str, int], x_range: tuple[float, float], y_range: tuple[float, float], **kwargs) -> 'Plotter':
|
|
352
|
+
"""
|
|
353
|
+
在指定子图上,根据数据坐标绘制一个高亮矩形区域。
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
tag (Union[str, int]): 目标子图的tag。
|
|
357
|
+
x_range (tuple[float, float]): 高亮区域的X轴范围 (xmin, xmax),使用数据坐标。
|
|
358
|
+
y_range (tuple[float, float]): 高亮区域的Y轴范围 (ymin, ymax),使用数据坐标。
|
|
359
|
+
**kwargs: 其他传递给 `matplotlib.patches.Rectangle` 的关键字参数。
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
Plotter: 返回Plotter实例以支持链式调用。
|
|
363
|
+
"""
|
|
364
|
+
ax = self._get_ax_by_tag(tag)
|
|
365
|
+
|
|
366
|
+
width = x_range[1] - x_range[0]
|
|
367
|
+
height = y_range[1] - y_range[0]
|
|
368
|
+
|
|
369
|
+
# 设置高亮框的默认样式
|
|
370
|
+
kwargs.setdefault('facecolor', 'yellow')
|
|
371
|
+
kwargs.setdefault('alpha', 0.3)
|
|
372
|
+
kwargs.setdefault('edgecolor', 'none')
|
|
373
|
+
kwargs.setdefault('zorder', 0) # 将高亮框置于底层
|
|
374
|
+
|
|
375
|
+
rect = plt.Rectangle((x_range[0], y_range[0]), width, height, **kwargs)
|
|
376
|
+
ax.add_patch(rect)
|
|
377
|
+
return self
|
|
378
|
+
|
|
379
|
+
def add_inset_image(self, host_tag: Union[str, int], image_path: str, rect: List[float], **kwargs) -> 'Plotter':
|
|
380
|
+
"""
|
|
381
|
+
在指定子图内部嵌入一张图片。
|
|
382
|
+
"""
|
|
383
|
+
host_ax = self._get_ax_by_tag(host_tag)
|
|
384
|
+
|
|
385
|
+
try:
|
|
386
|
+
img = mpimg.imread(image_path)
|
|
387
|
+
except FileNotFoundError:
|
|
388
|
+
raise FileNotFoundError(f"Image file not found at: {image_path}")
|
|
389
|
+
|
|
390
|
+
inset_ax = host_ax.inset_axes(rect)
|
|
391
|
+
inset_ax.imshow(img, **kwargs)
|
|
392
|
+
inset_ax.axis('off')
|
|
393
|
+
|
|
394
|
+
return self
|
|
395
|
+
|
|
396
|
+
def hide_axes(self, x: bool = False, y: bool = False) -> 'Plotter':
|
|
397
|
+
"""
|
|
398
|
+
隐藏所有子图的X轴或Y轴。
|
|
399
|
+
"""
|
|
400
|
+
for ax in self.axes:
|
|
401
|
+
if x:
|
|
402
|
+
ax.get_xaxis().set_visible(False)
|
|
403
|
+
if y:
|
|
404
|
+
ax.get_yaxis().set_visible(False)
|
|
405
|
+
return self
|
|
406
|
+
|
|
407
|
+
def cleanup(self, share_y_on_rows: list[int] = None, share_x_on_cols: list[int] = None, align_labels: bool = True, auto_share: Union[bool, str] = False):
|
|
408
|
+
"""
|
|
409
|
+
根据指定对行或列进行坐标轴共享和清理。
|
|
410
|
+
"""
|
|
411
|
+
try:
|
|
412
|
+
if isinstance(self.layout, tuple):
|
|
413
|
+
n_rows, n_cols = self.layout
|
|
414
|
+
else:
|
|
415
|
+
n_rows = len(self.layout)
|
|
416
|
+
n_cols = len(self.layout[0]) if n_rows > 0 else 0
|
|
417
|
+
except:
|
|
418
|
+
n_rows, n_cols = 1, len(self.axes)
|
|
419
|
+
|
|
420
|
+
# Implement auto_share logic
|
|
421
|
+
if auto_share is True or auto_share == 'y':
|
|
422
|
+
if share_y_on_rows is None:
|
|
423
|
+
share_y_on_rows = list(range(n_rows))
|
|
424
|
+
|
|
425
|
+
if auto_share is True or auto_share == 'x':
|
|
426
|
+
if share_x_on_cols is None:
|
|
427
|
+
share_x_on_cols = list(range(n_cols))
|
|
428
|
+
|
|
429
|
+
ax_map = {(i // n_cols, i % n_cols): ax for i, ax in enumerate(self.axes) if i < n_rows * n_cols}
|
|
430
|
+
|
|
431
|
+
if share_y_on_rows:
|
|
432
|
+
for row_idx in share_y_on_rows:
|
|
433
|
+
row_axes = [ax_map.get((row_idx, col_idx)) for col_idx in range(n_cols)]
|
|
434
|
+
row_axes = [ax for ax in row_axes if ax]
|
|
435
|
+
if not row_axes or len(row_axes) < 2: continue
|
|
436
|
+
leader_ax = row_axes[0]
|
|
437
|
+
for follower_ax in row_axes[1:]:
|
|
438
|
+
follower_ax.sharey(leader_ax)
|
|
439
|
+
follower_ax.tick_params(axis='y', labelleft=False)
|
|
440
|
+
follower_ax.set_ylabel("")
|
|
441
|
+
|
|
442
|
+
if share_x_on_cols:
|
|
443
|
+
for col_idx in share_x_on_cols:
|
|
444
|
+
col_axes = [ax_map.get((row_idx, col_idx)) for row_idx in range(n_rows)]
|
|
445
|
+
col_axes = [ax for ax in col_axes if ax]
|
|
446
|
+
if not col_axes or len(col_axes) < 2: continue
|
|
447
|
+
leader_ax = col_axes[-1]
|
|
448
|
+
for follower_ax in col_axes[:-1]:
|
|
449
|
+
follower_ax.sharex(leader_ax)
|
|
450
|
+
follower_ax.tick_params(axis='x', labelbottom=False)
|
|
451
|
+
follower_ax.set_xlabel("")
|
|
452
|
+
|
|
453
|
+
if align_labels:
|
|
454
|
+
try:
|
|
455
|
+
self.fig.align_labels()
|
|
456
|
+
except Exception:
|
|
457
|
+
pass
|
|
458
|
+
return self
|
|
459
|
+
|
|
460
|
+
def cleanup_heatmaps(self, tags: List[Union[str, int]]) -> 'Plotter':
|
|
461
|
+
"""
|
|
462
|
+
为指定的一组热图创建共享的颜色条。
|
|
463
|
+
"""
|
|
464
|
+
if not tags or not isinstance(tags, list):
|
|
465
|
+
raise ValueError("'tags' must be a list of heatmap tags.")
|
|
466
|
+
|
|
467
|
+
mappables = [self.tag_to_mappable.get(tag) for tag in tags]
|
|
468
|
+
mappables = [m for m in mappables if m]
|
|
469
|
+
if not mappables:
|
|
470
|
+
raise ValueError("No valid heatmaps found for the given tags.")
|
|
471
|
+
|
|
472
|
+
try:
|
|
473
|
+
global_vmin = min(m.get_clim()[0] for m in mappables)
|
|
474
|
+
global_vmax = max(m.get_clim()[1] for m in mappables)
|
|
475
|
+
except (AttributeError, IndexError):
|
|
476
|
+
raise ValueError("Could not retrieve color limits from the provided heatmap tags.")
|
|
477
|
+
|
|
478
|
+
for m in mappables:
|
|
479
|
+
m.set_clim(vmin=global_vmin, vmax=global_vmax)
|
|
480
|
+
|
|
481
|
+
from mpl_toolkits.axes_grid1 import make_axes_locatable
|
|
482
|
+
last_ax = self._get_ax_by_tag(tags[-1])
|
|
483
|
+
divider = make_axes_locatable(last_ax)
|
|
484
|
+
cax = divider.append_axes("right", size="5%", pad=0.1)
|
|
485
|
+
self.fig.colorbar(mappables[-1], cax=cax)
|
|
486
|
+
return self
|
|
487
|
+
|
|
488
|
+
def save(self, filename: str, **kwargs) -> None:
|
|
489
|
+
"""
|
|
490
|
+
将当前图形保存到文件。
|
|
491
|
+
在保存前,会先执行所有通过 `_draw_on_save_queue` 队列请求的延迟绘图操作。
|
|
492
|
+
"""
|
|
493
|
+
# 执行所有延迟的绘图操作
|
|
494
|
+
for task in self._draw_on_save_queue:
|
|
495
|
+
task['func'](**task['kwargs'])
|
|
496
|
+
|
|
497
|
+
# 清空队列
|
|
498
|
+
self._draw_on_save_queue.clear()
|
|
499
|
+
|
|
500
|
+
defaults = {'dpi': 300, 'bbox_inches': 'tight'}
|
|
501
|
+
defaults.update(kwargs)
|
|
502
|
+
self.fig.savefig(filename, **defaults)
|
|
503
|
+
plt.close(self.fig)
|
|
504
|
+
logger.info(f"Figure saved to {filename}")
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# styles/flat.mplstyle
|
|
2
|
+
# A modern, flat design style with a bright, friendly palette.
|
|
3
|
+
|
|
4
|
+
# Font (a clean, modern sans-serif)
|
|
5
|
+
font.family : sans-serif
|
|
6
|
+
font.sans-serif : Helvetica, Arial, sans-serif
|
|
7
|
+
font.size : 12
|
|
8
|
+
axes.labelsize : 14
|
|
9
|
+
axes.titlesize : 16
|
|
10
|
+
legend.fontsize : 11
|
|
11
|
+
xtick.labelsize : 10
|
|
12
|
+
ytick.labelsize : 10
|
|
13
|
+
|
|
14
|
+
# Lines
|
|
15
|
+
lines.linewidth : 2.0
|
|
16
|
+
lines.markersize : 6
|
|
17
|
+
|
|
18
|
+
# Axes
|
|
19
|
+
axes.facecolor : "#f0f0f0" # Light gray background for the plot area
|
|
20
|
+
axes.edgecolor : "#b0b0b0"
|
|
21
|
+
axes.labelcolor : "#333333"
|
|
22
|
+
axes.titlecolor : "#333333"
|
|
23
|
+
axes.grid : True
|
|
24
|
+
axes.axisbelow : True
|
|
25
|
+
axes.spines.top : False
|
|
26
|
+
axes.spines.right : False
|
|
27
|
+
|
|
28
|
+
# Ticks
|
|
29
|
+
xtick.color : "#333333"
|
|
30
|
+
ytick.color : "#333333"
|
|
31
|
+
|
|
32
|
+
# Grid
|
|
33
|
+
grid.color : white
|
|
34
|
+
grid.linestyle : -
|
|
35
|
+
|
|
36
|
+
# Figure
|
|
37
|
+
figure.facecolor : white # White background for the figure
|
|
38
|
+
|
|
39
|
+
# Color Cycle (from a flat UI palette)
|
|
40
|
+
axes.prop_cycle: cycler(color=["#3498db", "#2ecc71", "#e74c3c", "#f1c40f", "#9b59b6", "#1abc9c"])
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# styles/nord.mplstyle
|
|
2
|
+
# A cool, arctic, north-bluish color scheme from the Nord palette.
|
|
3
|
+
|
|
4
|
+
# Font
|
|
5
|
+
font.family : sans-serif
|
|
6
|
+
font.sans-serif : Segoe UI, Arial, sans-serif
|
|
7
|
+
font.size : 12
|
|
8
|
+
axes.labelsize : 14
|
|
9
|
+
axes.titlesize : 16
|
|
10
|
+
legend.fontsize : 11
|
|
11
|
+
xtick.labelsize : 10
|
|
12
|
+
ytick.labelsize : 10
|
|
13
|
+
|
|
14
|
+
# Lines
|
|
15
|
+
lines.linewidth : 2.0
|
|
16
|
+
lines.markersize : 6
|
|
17
|
+
|
|
18
|
+
# Axes (light text on dark background)
|
|
19
|
+
axes.facecolor : "#3B4252" # nord1
|
|
20
|
+
axes.edgecolor : "#D8DEE9" # nord4
|
|
21
|
+
axes.labelcolor : "#ECEFF4" # nord6
|
|
22
|
+
axes.titlecolor : "#ECEFF4" # nord6
|
|
23
|
+
axes.grid : True
|
|
24
|
+
axes.axisbelow : True
|
|
25
|
+
axes.spines.top : False
|
|
26
|
+
axes.spines.right : False
|
|
27
|
+
|
|
28
|
+
# Ticks
|
|
29
|
+
xtick.color : "#D8DEE9" # nord4
|
|
30
|
+
ytick.color : "#D8DEE9" # nord4
|
|
31
|
+
|
|
32
|
+
# Grid
|
|
33
|
+
grid.color : "#4C566A" # nord3
|
|
34
|
+
grid.linestyle : --
|
|
35
|
+
|
|
36
|
+
# Figure
|
|
37
|
+
figure.facecolor : "#2E3440" # nord0
|
|
38
|
+
|
|
39
|
+
# Legend
|
|
40
|
+
legend.frameon : False
|
|
41
|
+
legend.labelcolor : "#ECEFF4"
|
|
42
|
+
|
|
43
|
+
# Color Cycle (the "aurora" colors)
|
|
44
|
+
axes.prop_cycle: cycler(color=["#BF616A", "#D08770", "#EBCB8B", "#A3BE8C", "#B48EAD"])
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# styles/presentation.mplstyle
|
|
2
|
+
# 一个适合屏幕演示的、高对比度的深色主题
|
|
3
|
+
|
|
4
|
+
# Fonts (使用无衬线字体,字号更大)
|
|
5
|
+
font.family : sans-serif
|
|
6
|
+
font.sans-serif : Arial, Helvetica, sans-serif
|
|
7
|
+
font.size : 14
|
|
8
|
+
axes.labelsize : 16
|
|
9
|
+
axes.titlesize : 18
|
|
10
|
+
legend.fontsize : 12
|
|
11
|
+
xtick.labelsize : 12
|
|
12
|
+
ytick.labelsize : 12
|
|
13
|
+
|
|
14
|
+
# Lines
|
|
15
|
+
lines.linewidth : 2.5
|
|
16
|
+
lines.markersize : 8
|
|
17
|
+
|
|
18
|
+
# Axes (坐标轴和标签使用白色)
|
|
19
|
+
axes.labelcolor : white
|
|
20
|
+
axes.titlecolor : white
|
|
21
|
+
axes.edgecolor : white
|
|
22
|
+
axes.grid : True
|
|
23
|
+
axes.axisbelow : True
|
|
24
|
+
grid.color : "#444444" # 深灰色网格
|
|
25
|
+
grid.linestyle : -
|
|
26
|
+
|
|
27
|
+
# Ticks (刻度使用白色)
|
|
28
|
+
xtick.color : white
|
|
29
|
+
ytick.color : white
|
|
30
|
+
|
|
31
|
+
# Figure (画布和子图背景设为深色)
|
|
32
|
+
figure.facecolor : "#1e1e1e" # 深灰色背景
|
|
33
|
+
axes.facecolor : "#2a2a2a" # 稍亮的子图背景
|
|
34
|
+
|
|
35
|
+
# Spines (隐藏顶部和右侧的轴线)
|
|
36
|
+
axes.spines.top : False
|
|
37
|
+
axes.spines.right : False
|
|
38
|
+
|
|
39
|
+
# Legend (图例背景透明)
|
|
40
|
+
legend.frameon : False
|
|
41
|
+
legend.labelcolor : white
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Fonts
|
|
2
|
+
font.family : serif
|
|
3
|
+
font.serif : Times New Roman
|
|
4
|
+
font.size : 10
|
|
5
|
+
axes.labelsize : 12
|
|
6
|
+
legend.fontsize : 9
|
|
7
|
+
xtick.labelsize : 8
|
|
8
|
+
ytick.labelsize : 8
|
|
9
|
+
|
|
10
|
+
# Lines
|
|
11
|
+
lines.linewidth : 1.5
|
|
12
|
+
lines.markersize : 4
|
|
13
|
+
|
|
14
|
+
# Axes
|
|
15
|
+
axes.grid : True
|
|
16
|
+
axes.axisbelow : True
|
|
17
|
+
grid.color : 0.8
|
|
18
|
+
grid.linestyle : --
|
|
19
|
+
axes.spines.top : False
|
|
20
|
+
axes.spines.right : False
|
|
21
|
+
|
|
22
|
+
# Figure
|
|
23
|
+
figure.dpi : 300
|
|
24
|
+
figure.facecolor : white
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# styles/solarized_light.mplstyle
|
|
2
|
+
# A light theme based on the popular Solarized palette.
|
|
3
|
+
|
|
4
|
+
# Font
|
|
5
|
+
font.family : sans-serif
|
|
6
|
+
font.sans-serif : Segoe UI, DejaVu Sans, sans-serif
|
|
7
|
+
font.size : 12
|
|
8
|
+
axes.labelsize : 14
|
|
9
|
+
axes.titlesize : 16
|
|
10
|
+
legend.fontsize : 11
|
|
11
|
+
xtick.labelsize : 10
|
|
12
|
+
ytick.labelsize : 10
|
|
13
|
+
|
|
14
|
+
# Lines
|
|
15
|
+
lines.linewidth : 2.0
|
|
16
|
+
lines.markersize : 7
|
|
17
|
+
|
|
18
|
+
# Axes
|
|
19
|
+
axes.facecolor : "#fdf6e3" # base3
|
|
20
|
+
axes.edgecolor : "#93a1a1" # base1
|
|
21
|
+
axes.labelcolor : "#586e75" # base01
|
|
22
|
+
axes.titlecolor : "#073642" # base02
|
|
23
|
+
axes.grid : True
|
|
24
|
+
axes.axisbelow : True
|
|
25
|
+
axes.spines.top : False
|
|
26
|
+
axes.spines.right : False
|
|
27
|
+
|
|
28
|
+
# Ticks
|
|
29
|
+
xtick.color : "#657b83" # base00
|
|
30
|
+
ytick.color : "#657b83" # base00
|
|
31
|
+
|
|
32
|
+
# Grid
|
|
33
|
+
grid.color : "#eee8d5" # base2
|
|
34
|
+
grid.linestyle : -
|
|
35
|
+
|
|
36
|
+
# Figure
|
|
37
|
+
figure.facecolor : "#fdf6e3" # base3
|
|
38
|
+
|
|
39
|
+
# Legend
|
|
40
|
+
legend.framealpha : 0.5
|
|
41
|
+
legend.facecolor : "#eee8d5" # base2
|
|
42
|
+
|
|
43
|
+
# Color Cycle (the accent colors)
|
|
44
|
+
axes.prop_cycle: cycler(color=["#268bd2", "#859900", "#dc322f", "#d33682", "#6c71c4", "#2aa198", "#b58900"])
|