acadplot 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- acadplot/__init__.py +70 -0
- acadplot/bar.py +428 -0
- acadplot/charts.py +376 -0
- acadplot/draw.py +126 -0
- acadplot/line.py +184 -0
- acadplot/plot.py +70 -0
- acadplot/styles.py +911 -0
- acadplot/utils.py +440 -0
- acadplot-0.1.0.dist-info/METADATA +840 -0
- acadplot-0.1.0.dist-info/RECORD +12 -0
- acadplot-0.1.0.dist-info/WHEEL +4 -0
- acadplot-0.1.0.dist-info/licenses/LICENSE +21 -0
acadplot/charts.py
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
from itertools import cycle
|
|
2
|
+
from typing import List, Optional, Sequence, Tuple
|
|
3
|
+
|
|
4
|
+
import matplotlib.pyplot as plt
|
|
5
|
+
from matplotlib.colors import to_rgba
|
|
6
|
+
from matplotlib.patches import Patch
|
|
7
|
+
|
|
8
|
+
from .draw import resolve_color_key
|
|
9
|
+
from .styles import apply_axis_style, apply_grid, get_current_style
|
|
10
|
+
from .utils import markers, new_alpha, save, styled_legend
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _prepare_axes(ax, fig_size):
|
|
14
|
+
if ax is None:
|
|
15
|
+
return plt.subplots(figsize=fig_size)
|
|
16
|
+
return ax.figure, ax
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _resolve_marker(marker_key: str | int):
|
|
20
|
+
if isinstance(marker_key, int):
|
|
21
|
+
return list(markers.values())[marker_key]
|
|
22
|
+
return markers[marker_key]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _resolve_text_sizes(style, font_size, label_size, tick_size, legend_size):
|
|
26
|
+
font_size_override = font_size
|
|
27
|
+
if font_size is None:
|
|
28
|
+
font_size = float(style["font_size"])
|
|
29
|
+
if label_size is None:
|
|
30
|
+
label_size = (
|
|
31
|
+
font_size_override
|
|
32
|
+
if font_size_override is not None
|
|
33
|
+
else float(style["label_size"])
|
|
34
|
+
)
|
|
35
|
+
if tick_size is None:
|
|
36
|
+
tick_size = (
|
|
37
|
+
font_size_override
|
|
38
|
+
if font_size_override is not None
|
|
39
|
+
else float(style["tick_size"])
|
|
40
|
+
)
|
|
41
|
+
if legend_size is None:
|
|
42
|
+
legend_size = (
|
|
43
|
+
font_size_override
|
|
44
|
+
if font_size_override is not None
|
|
45
|
+
else float(style["legend_size"])
|
|
46
|
+
)
|
|
47
|
+
return font_size, label_size, tick_size, legend_size
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _parse_xy_marker_series(series):
|
|
51
|
+
if len(series) == 5:
|
|
52
|
+
x, y, color_key, marker_key, series_label = series
|
|
53
|
+
return x, y, color_key, marker_key, series_label
|
|
54
|
+
if len(series) == 4:
|
|
55
|
+
x, y, marker_key, series_label = series
|
|
56
|
+
return x, y, None, marker_key, series_label
|
|
57
|
+
raise ValueError(
|
|
58
|
+
"Series entries must be (x, y, color, marker, label) or (x, y, marker, label)."
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _parse_errorbar_series(series):
|
|
63
|
+
if len(series) == 6:
|
|
64
|
+
x, y, yerr, color_key, marker_key, series_label = series
|
|
65
|
+
return x, y, yerr, color_key, marker_key, series_label
|
|
66
|
+
if len(series) == 5:
|
|
67
|
+
x, y, yerr, marker_key, series_label = series
|
|
68
|
+
return x, y, yerr, None, marker_key, series_label
|
|
69
|
+
raise ValueError(
|
|
70
|
+
"Errorbar entries must be (x, y, yerr, color, marker, label) or "
|
|
71
|
+
"(x, y, yerr, marker, label)."
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _parse_box_group(group):
|
|
76
|
+
if len(group) == 3:
|
|
77
|
+
values, color_key, group_label = group
|
|
78
|
+
return values, color_key, group_label
|
|
79
|
+
if len(group) == 2:
|
|
80
|
+
values, group_label = group
|
|
81
|
+
return values, None, group_label
|
|
82
|
+
raise ValueError("Box groups must be (values, color, label) or (values, label).")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _add_legend(
|
|
86
|
+
ax,
|
|
87
|
+
location: str,
|
|
88
|
+
legend_size: float,
|
|
89
|
+
ncols: int,
|
|
90
|
+
columnspacing: float,
|
|
91
|
+
legend_outside: bool | str,
|
|
92
|
+
):
|
|
93
|
+
return styled_legend(
|
|
94
|
+
ax,
|
|
95
|
+
location,
|
|
96
|
+
legend_size=legend_size,
|
|
97
|
+
ncols=ncols,
|
|
98
|
+
columnspacing=columnspacing,
|
|
99
|
+
legend_outside=legend_outside,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def plot_scatter(
|
|
104
|
+
series: List[Tuple],
|
|
105
|
+
location: str = "best",
|
|
106
|
+
fig_size: Optional[Tuple[float, float]] = None,
|
|
107
|
+
label: Tuple[str, str] = ("x-label", "y-label"),
|
|
108
|
+
ax=None,
|
|
109
|
+
font_size: Optional[float] = None,
|
|
110
|
+
label_size: Optional[float] = None,
|
|
111
|
+
tick_size: Optional[float] = None,
|
|
112
|
+
legend_size: Optional[float] = None,
|
|
113
|
+
marker_size: Optional[float] = None,
|
|
114
|
+
ncols: int = 1,
|
|
115
|
+
columnspacing: float = 0.5,
|
|
116
|
+
grid: Optional[str] = None,
|
|
117
|
+
fname: Optional[str] = "scatter_plot.pdf",
|
|
118
|
+
legend_outside: bool | str = False,
|
|
119
|
+
):
|
|
120
|
+
"""Plot one or more scatter series."""
|
|
121
|
+
style = get_current_style()
|
|
122
|
+
if fig_size is None:
|
|
123
|
+
fig_size = style["fig_size"]
|
|
124
|
+
_, label_size, tick_size, legend_size = _resolve_text_sizes(
|
|
125
|
+
style, font_size, label_size, tick_size, legend_size
|
|
126
|
+
)
|
|
127
|
+
if marker_size is None:
|
|
128
|
+
marker_size = 28.0 * float(style["marker_scale"])
|
|
129
|
+
|
|
130
|
+
fig, ax = _prepare_axes(ax, fig_size)
|
|
131
|
+
ax.set_prop_cycle(color=list(style["palette"]))
|
|
132
|
+
ax.set_xlabel(label[0], fontsize=label_size)
|
|
133
|
+
ax.set_ylabel(label[1], fontsize=label_size)
|
|
134
|
+
apply_grid(ax, grid or str(style["line_grid"]))
|
|
135
|
+
|
|
136
|
+
palette_iter = cycle(style["palette"])
|
|
137
|
+
for item in series:
|
|
138
|
+
x, y, color_key, marker_key, series_label = _parse_xy_marker_series(item)
|
|
139
|
+
color = resolve_color_key(color_key)
|
|
140
|
+
if color is None:
|
|
141
|
+
color = next(palette_iter)
|
|
142
|
+
marker, _ = _resolve_marker(marker_key)
|
|
143
|
+
ax.scatter(
|
|
144
|
+
x,
|
|
145
|
+
y,
|
|
146
|
+
s=marker_size,
|
|
147
|
+
marker=marker,
|
|
148
|
+
facecolors=new_alpha(to_rgba(color), 0.3),
|
|
149
|
+
edgecolors=color,
|
|
150
|
+
linewidths=float(style["marker_edge_width"]),
|
|
151
|
+
label=series_label,
|
|
152
|
+
zorder=3,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
_add_legend(ax, location, legend_size, ncols, columnspacing, legend_outside)
|
|
156
|
+
ax.tick_params(axis="both", labelsize=tick_size)
|
|
157
|
+
apply_axis_style(ax)
|
|
158
|
+
|
|
159
|
+
if fname:
|
|
160
|
+
save(fig, fname, close=False)
|
|
161
|
+
return fig, ax
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def plot_errorbar(
|
|
165
|
+
series: List[Tuple],
|
|
166
|
+
location: str = "best",
|
|
167
|
+
fig_size: Optional[Tuple[float, float]] = None,
|
|
168
|
+
label: Tuple[str, str] = ("x-label", "y-label"),
|
|
169
|
+
ax=None,
|
|
170
|
+
font_size: Optional[float] = None,
|
|
171
|
+
label_size: Optional[float] = None,
|
|
172
|
+
tick_size: Optional[float] = None,
|
|
173
|
+
legend_size: Optional[float] = None,
|
|
174
|
+
capsize: float = 2.5,
|
|
175
|
+
ncols: int = 1,
|
|
176
|
+
columnspacing: float = 0.5,
|
|
177
|
+
grid: Optional[str] = None,
|
|
178
|
+
fname: Optional[str] = "errorbar_plot.pdf",
|
|
179
|
+
legend_outside: bool | str = False,
|
|
180
|
+
):
|
|
181
|
+
"""Plot line series with error bars."""
|
|
182
|
+
style = get_current_style()
|
|
183
|
+
if fig_size is None:
|
|
184
|
+
fig_size = style["fig_size"]
|
|
185
|
+
_, label_size, tick_size, legend_size = _resolve_text_sizes(
|
|
186
|
+
style, font_size, label_size, tick_size, legend_size
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
fig, ax = _prepare_axes(ax, fig_size)
|
|
190
|
+
ax.set_prop_cycle(color=list(style["palette"]))
|
|
191
|
+
ax.set_xlabel(label[0], fontsize=label_size)
|
|
192
|
+
ax.set_ylabel(label[1], fontsize=label_size)
|
|
193
|
+
apply_grid(ax, grid or str(style["line_grid"]))
|
|
194
|
+
|
|
195
|
+
for item in series:
|
|
196
|
+
x, y, yerr, color_key, marker_key, series_label = _parse_errorbar_series(item)
|
|
197
|
+
color = resolve_color_key(color_key)
|
|
198
|
+
color_kwargs = {"color": color, "ecolor": color} if color is not None else {}
|
|
199
|
+
marker, marker_size = _resolve_marker(marker_key)
|
|
200
|
+
(line, _, _) = ax.errorbar(
|
|
201
|
+
x,
|
|
202
|
+
y,
|
|
203
|
+
yerr=yerr,
|
|
204
|
+
marker=marker,
|
|
205
|
+
markersize=marker_size * float(style["marker_scale"]),
|
|
206
|
+
markeredgewidth=float(style["marker_edge_width"]),
|
|
207
|
+
linewidth=float(style["line_width"]),
|
|
208
|
+
elinewidth=float(style["line_width"]) * 0.8,
|
|
209
|
+
capsize=capsize,
|
|
210
|
+
label=series_label,
|
|
211
|
+
zorder=3,
|
|
212
|
+
**color_kwargs,
|
|
213
|
+
)
|
|
214
|
+
resolved_color = line.get_color()
|
|
215
|
+
line.set_markerfacecolor(new_alpha(to_rgba(resolved_color), 0.3))
|
|
216
|
+
line.set_markeredgecolor(resolved_color)
|
|
217
|
+
|
|
218
|
+
_add_legend(ax, location, legend_size, ncols, columnspacing, legend_outside)
|
|
219
|
+
ax.tick_params(axis="both", labelsize=tick_size)
|
|
220
|
+
apply_axis_style(ax)
|
|
221
|
+
|
|
222
|
+
if fname:
|
|
223
|
+
save(fig, fname, close=False)
|
|
224
|
+
return fig, ax
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def plot_box(
|
|
228
|
+
groups: List[Tuple],
|
|
229
|
+
fig_size: Optional[Tuple[float, float]] = None,
|
|
230
|
+
label: Tuple[str, str] = ("Group", "Value"),
|
|
231
|
+
ax=None,
|
|
232
|
+
font_size: Optional[float] = None,
|
|
233
|
+
label_size: Optional[float] = None,
|
|
234
|
+
tick_size: Optional[float] = None,
|
|
235
|
+
legend_size: Optional[float] = None,
|
|
236
|
+
location: Optional[str] = None,
|
|
237
|
+
grid: Optional[str] = None,
|
|
238
|
+
fname: Optional[str] = "box_plot.pdf",
|
|
239
|
+
legend_outside: bool | str = False,
|
|
240
|
+
):
|
|
241
|
+
"""Plot grouped distributions as a box plot."""
|
|
242
|
+
style = get_current_style()
|
|
243
|
+
if fig_size is None:
|
|
244
|
+
fig_size = style["fig_size"]
|
|
245
|
+
_, label_size, tick_size, legend_size = _resolve_text_sizes(
|
|
246
|
+
style, font_size, label_size, tick_size, legend_size
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
values = []
|
|
250
|
+
labels = []
|
|
251
|
+
colors = []
|
|
252
|
+
palette_iter = cycle(style["palette"])
|
|
253
|
+
for group in groups:
|
|
254
|
+
group_values, color_key, group_label = _parse_box_group(group)
|
|
255
|
+
values.append(group_values)
|
|
256
|
+
labels.append(group_label)
|
|
257
|
+
color = resolve_color_key(color_key)
|
|
258
|
+
colors.append(color if color is not None else next(palette_iter))
|
|
259
|
+
|
|
260
|
+
fig, ax = _prepare_axes(ax, fig_size)
|
|
261
|
+
ax.set_xlabel(label[0], fontsize=label_size)
|
|
262
|
+
ax.set_ylabel(label[1], fontsize=label_size)
|
|
263
|
+
apply_grid(ax, grid or "major-y")
|
|
264
|
+
|
|
265
|
+
box = ax.boxplot(
|
|
266
|
+
values,
|
|
267
|
+
labels=labels,
|
|
268
|
+
patch_artist=True,
|
|
269
|
+
widths=0.55,
|
|
270
|
+
medianprops=dict(color=str(style["axis_color"])),
|
|
271
|
+
)
|
|
272
|
+
for patch, color in zip(box["boxes"], colors):
|
|
273
|
+
patch.set_facecolor(new_alpha(to_rgba(color), 0.28))
|
|
274
|
+
patch.set_edgecolor(color)
|
|
275
|
+
patch.set_linewidth(float(style["line_width"]))
|
|
276
|
+
for key in ("whiskers", "caps", "medians"):
|
|
277
|
+
for artist in box[key]:
|
|
278
|
+
artist.set_linewidth(float(style["line_width"]))
|
|
279
|
+
artist.set_color(str(style["axis_color"]))
|
|
280
|
+
|
|
281
|
+
if location is not None or legend_outside:
|
|
282
|
+
handles = [
|
|
283
|
+
Patch(facecolor=new_alpha(to_rgba(c), 0.28), edgecolor=c) for c in colors
|
|
284
|
+
]
|
|
285
|
+
styled_legend(
|
|
286
|
+
ax,
|
|
287
|
+
location or "best",
|
|
288
|
+
legend_size=legend_size,
|
|
289
|
+
legend_outside=legend_outside,
|
|
290
|
+
handles=handles,
|
|
291
|
+
labels=labels,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
ax.tick_params(axis="both", labelsize=tick_size)
|
|
295
|
+
apply_axis_style(ax)
|
|
296
|
+
|
|
297
|
+
if fname:
|
|
298
|
+
save(fig, fname, close=False)
|
|
299
|
+
return fig, ax
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def plot_heatmap(
|
|
303
|
+
matrix: Sequence[Sequence[float]],
|
|
304
|
+
fig_size: Optional[Tuple[float, float]] = None,
|
|
305
|
+
label: Tuple[str, str] = ("x-label", "y-label"),
|
|
306
|
+
ax=None,
|
|
307
|
+
xticklabels: Optional[Sequence[str]] = None,
|
|
308
|
+
yticklabels: Optional[Sequence[str]] = None,
|
|
309
|
+
cmap: str = "PuBuGn",
|
|
310
|
+
colorbar_label: Optional[str] = None,
|
|
311
|
+
annotate: bool = False,
|
|
312
|
+
fmt: str = ".2g",
|
|
313
|
+
font_size: Optional[float] = None,
|
|
314
|
+
label_size: Optional[float] = None,
|
|
315
|
+
tick_size: Optional[float] = None,
|
|
316
|
+
legend_size: Optional[float] = None,
|
|
317
|
+
fname: Optional[str] = "heatmap.pdf",
|
|
318
|
+
):
|
|
319
|
+
"""Plot a matrix heatmap with optional annotations and colorbar."""
|
|
320
|
+
style = get_current_style()
|
|
321
|
+
if fig_size is None:
|
|
322
|
+
fig_size = style["fig_size"]
|
|
323
|
+
_, label_size, tick_size, legend_size = _resolve_text_sizes(
|
|
324
|
+
style, font_size, label_size, tick_size, legend_size
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
fig, ax = _prepare_axes(ax, fig_size)
|
|
328
|
+
image = ax.imshow(matrix, cmap=cmap, aspect="auto", zorder=2)
|
|
329
|
+
ax.set_xlabel(label[0], fontsize=label_size)
|
|
330
|
+
ax.set_ylabel(label[1], fontsize=label_size)
|
|
331
|
+
|
|
332
|
+
if xticklabels is not None:
|
|
333
|
+
ax.set_xticks(range(len(xticklabels)))
|
|
334
|
+
ax.set_xticklabels(xticklabels, fontsize=tick_size)
|
|
335
|
+
if yticklabels is not None:
|
|
336
|
+
ax.set_yticks(range(len(yticklabels)))
|
|
337
|
+
ax.set_yticklabels(yticklabels, fontsize=tick_size)
|
|
338
|
+
|
|
339
|
+
if annotate:
|
|
340
|
+
colormap = plt.get_cmap(cmap)
|
|
341
|
+
for row_idx, row in enumerate(matrix):
|
|
342
|
+
for col_idx, value in enumerate(row):
|
|
343
|
+
cell_color = colormap(image.norm(value))
|
|
344
|
+
luminance = (
|
|
345
|
+
0.299 * cell_color[0]
|
|
346
|
+
+ 0.587 * cell_color[1]
|
|
347
|
+
+ 0.114 * cell_color[2]
|
|
348
|
+
)
|
|
349
|
+
annotation_color = (
|
|
350
|
+
"#F5F5F5" if luminance < 0.45 else str(style["text_color"])
|
|
351
|
+
)
|
|
352
|
+
ax.text(
|
|
353
|
+
col_idx,
|
|
354
|
+
row_idx,
|
|
355
|
+
format(value, fmt),
|
|
356
|
+
ha="center",
|
|
357
|
+
va="center",
|
|
358
|
+
fontsize=tick_size,
|
|
359
|
+
color=annotation_color,
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
colorbar = fig.colorbar(image, ax=ax, fraction=0.046, pad=0.04)
|
|
363
|
+
colorbar.ax.tick_params(labelsize=tick_size, colors=str(style["tick_color"]))
|
|
364
|
+
if colorbar_label is not None:
|
|
365
|
+
colorbar.set_label(
|
|
366
|
+
colorbar_label,
|
|
367
|
+
fontsize=legend_size,
|
|
368
|
+
color=str(style["axis_label_color"]),
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
ax.tick_params(axis="both", labelsize=tick_size)
|
|
372
|
+
apply_axis_style(ax)
|
|
373
|
+
|
|
374
|
+
if fname:
|
|
375
|
+
save(fig, fname, close=False)
|
|
376
|
+
return fig, ax
|
acadplot/draw.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
from matplotlib.colors import is_color_like, to_rgba
|
|
4
|
+
|
|
5
|
+
from .styles import get_current_style
|
|
6
|
+
from .utils import colors, markers, new_alpha, patterns
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def resolve_color_key(color_key: str | int | None) -> str | None:
|
|
10
|
+
"""Resolve an AcadPlot color key, raw Matplotlib color, or default cycle color."""
|
|
11
|
+
if color_key is None:
|
|
12
|
+
return None
|
|
13
|
+
if isinstance(color_key, int):
|
|
14
|
+
return list(colors.values())[color_key]
|
|
15
|
+
if color_key in colors:
|
|
16
|
+
return colors[color_key]
|
|
17
|
+
if is_color_like(color_key):
|
|
18
|
+
return color_key
|
|
19
|
+
raise ValueError(
|
|
20
|
+
f"Unknown color {color_key!r}. Use a theme color name, index, or Matplotlib color."
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def resolve_pattern_key(pattern_key: str | int | None) -> str | None:
|
|
25
|
+
"""Resolve an AcadPlot pattern preset, index, raw hatch string, or no hatch."""
|
|
26
|
+
if pattern_key is None:
|
|
27
|
+
return None
|
|
28
|
+
if isinstance(pattern_key, int):
|
|
29
|
+
return list(patterns.values())[pattern_key]
|
|
30
|
+
if pattern_key in patterns:
|
|
31
|
+
return patterns[pattern_key]
|
|
32
|
+
return pattern_key
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def draw(
|
|
36
|
+
ax,
|
|
37
|
+
x: List[float],
|
|
38
|
+
y: List[float],
|
|
39
|
+
color_key: str | int | None,
|
|
40
|
+
marker_key: str | int,
|
|
41
|
+
label: str,
|
|
42
|
+
):
|
|
43
|
+
"""Draw a line with markers on the given axes.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
ax: The axes to draw on.
|
|
47
|
+
x (List[float]): x values.
|
|
48
|
+
y (List[float]): y values.
|
|
49
|
+
color_key (str | int | None): Color name, index, raw Matplotlib color,
|
|
50
|
+
or None to use the active theme palette cycle.
|
|
51
|
+
marker_key (str | int): Marker name or index.
|
|
52
|
+
label (str): Label for the line.
|
|
53
|
+
"""
|
|
54
|
+
color = resolve_color_key(color_key)
|
|
55
|
+
|
|
56
|
+
if isinstance(marker_key, int):
|
|
57
|
+
marker = list(markers.values())[marker_key]
|
|
58
|
+
elif isinstance(marker_key, str):
|
|
59
|
+
marker = markers[marker_key]
|
|
60
|
+
|
|
61
|
+
style = get_current_style()
|
|
62
|
+
marker_style, marker_size = marker
|
|
63
|
+
color_kwargs = {"color": color} if color is not None else {}
|
|
64
|
+
|
|
65
|
+
(line,) = ax.plot(
|
|
66
|
+
x,
|
|
67
|
+
y,
|
|
68
|
+
marker=marker_style,
|
|
69
|
+
markersize=marker_size * float(style["marker_scale"]),
|
|
70
|
+
markeredgewidth=float(style["marker_edge_width"]),
|
|
71
|
+
linewidth=float(style["line_width"]),
|
|
72
|
+
label=label,
|
|
73
|
+
zorder=3,
|
|
74
|
+
**color_kwargs,
|
|
75
|
+
)
|
|
76
|
+
resolved_color = line.get_color()
|
|
77
|
+
line.set_markerfacecolor(new_alpha(to_rgba(resolved_color), 0.3))
|
|
78
|
+
line.set_markeredgecolor(resolved_color)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def draw_bar(
|
|
82
|
+
ax,
|
|
83
|
+
x: List[float],
|
|
84
|
+
y: List[float],
|
|
85
|
+
color_key: str | int | None,
|
|
86
|
+
label: str,
|
|
87
|
+
width: float = 0.35,
|
|
88
|
+
pattern_key: str | int | None = None,
|
|
89
|
+
):
|
|
90
|
+
"""Draw a bar on the given axes.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
ax: The axes to draw on.
|
|
94
|
+
x (List[float]): x positions.
|
|
95
|
+
y (List[float]): bar heights.
|
|
96
|
+
color_key (str | int | None): Color name, index, raw Matplotlib color,
|
|
97
|
+
or None to use the active theme palette cycle.
|
|
98
|
+
label (str): Label for the bar.
|
|
99
|
+
width (float): Bar width. Defaults to 0.35.
|
|
100
|
+
pattern_key (str | int | None): Hatch pattern name, index, raw Matplotlib
|
|
101
|
+
hatch string, or None.
|
|
102
|
+
"""
|
|
103
|
+
color = resolve_color_key(color_key)
|
|
104
|
+
pattern = resolve_pattern_key(pattern_key)
|
|
105
|
+
style = get_current_style()
|
|
106
|
+
bar_alpha = float(style["bar_alpha"])
|
|
107
|
+
bar_edge_color = str(style["bar_edge_color"])
|
|
108
|
+
color_kwargs = {"edgecolor": str(style["bar_edge_color"])}
|
|
109
|
+
if color is not None:
|
|
110
|
+
color_kwargs["facecolor"] = new_alpha(to_rgba(color), bar_alpha)
|
|
111
|
+
|
|
112
|
+
container = ax.bar(
|
|
113
|
+
x,
|
|
114
|
+
y,
|
|
115
|
+
width,
|
|
116
|
+
linewidth=float(style["bar_edge_width"]),
|
|
117
|
+
hatch=pattern,
|
|
118
|
+
label=label,
|
|
119
|
+
zorder=3,
|
|
120
|
+
**color_kwargs,
|
|
121
|
+
)
|
|
122
|
+
for patch in container.patches:
|
|
123
|
+
if color is None:
|
|
124
|
+
patch.set_facecolor(new_alpha(to_rgba(patch.get_facecolor()), bar_alpha))
|
|
125
|
+
patch.set_edgecolor(bar_edge_color)
|
|
126
|
+
patch.set_alpha(None)
|
acadplot/line.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
from typing import List, Optional, Tuple
|
|
2
|
+
|
|
3
|
+
import matplotlib.pyplot as plt
|
|
4
|
+
|
|
5
|
+
from .draw import draw
|
|
6
|
+
from .styles import (
|
|
7
|
+
apply_axis_style,
|
|
8
|
+
apply_grid,
|
|
9
|
+
configure_plot_style,
|
|
10
|
+
get_current_style,
|
|
11
|
+
)
|
|
12
|
+
from .utils import save, styled_legend
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _parse_line(line):
|
|
16
|
+
if len(line) == 5:
|
|
17
|
+
x, y, color_key, marker_key, line_label = line
|
|
18
|
+
return x, y, color_key, marker_key, line_label
|
|
19
|
+
if len(line) == 4:
|
|
20
|
+
x, y, marker_key, line_label = line
|
|
21
|
+
return x, y, None, marker_key, line_label
|
|
22
|
+
raise ValueError(
|
|
23
|
+
"Line entries must be (x, y, color, marker, label) or (x, y, marker, label)."
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _resolve_text_sizes(style, font_size, label_size, tick_size, legend_size):
|
|
28
|
+
font_size_override = font_size
|
|
29
|
+
if font_size is None:
|
|
30
|
+
font_size = float(style["font_size"])
|
|
31
|
+
if label_size is None:
|
|
32
|
+
label_size = (
|
|
33
|
+
font_size_override
|
|
34
|
+
if font_size_override is not None
|
|
35
|
+
else float(style["label_size"])
|
|
36
|
+
)
|
|
37
|
+
if tick_size is None:
|
|
38
|
+
tick_size = (
|
|
39
|
+
font_size_override
|
|
40
|
+
if font_size_override is not None
|
|
41
|
+
else float(style["tick_size"])
|
|
42
|
+
)
|
|
43
|
+
if legend_size is None:
|
|
44
|
+
legend_size = (
|
|
45
|
+
font_size_override
|
|
46
|
+
if font_size_override is not None
|
|
47
|
+
else float(style["legend_size"])
|
|
48
|
+
)
|
|
49
|
+
return font_size, label_size, tick_size, legend_size
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def plot_line(
|
|
53
|
+
lines: List[Tuple],
|
|
54
|
+
location: str,
|
|
55
|
+
fig_size: Optional[Tuple[float, float]] = None,
|
|
56
|
+
label: Tuple[str, str] = ("x-label", "y-label"),
|
|
57
|
+
ax=None,
|
|
58
|
+
xticks: Optional[List[float] | range] = None,
|
|
59
|
+
yticks: Optional[List[float] | range] = None,
|
|
60
|
+
xstart: Optional[float] = None,
|
|
61
|
+
ystart: Optional[float] = None,
|
|
62
|
+
font_size: Optional[float] = None,
|
|
63
|
+
label_size: Optional[float] = None,
|
|
64
|
+
tick_size: Optional[float] = None,
|
|
65
|
+
legend_size: Optional[float] = None,
|
|
66
|
+
ncols: int = 1,
|
|
67
|
+
columnspacing: float = 0.5,
|
|
68
|
+
grid: Optional[str] = None,
|
|
69
|
+
fname: Optional[str] = "plot.pdf",
|
|
70
|
+
legend_outside: bool | str = False,
|
|
71
|
+
):
|
|
72
|
+
"""Plot multiple lines with markers and a legend.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
lines (List[Tuple]): List of lines to plot, each defined by
|
|
76
|
+
(x values, y values, color name/index, marker name/index, label)
|
|
77
|
+
or (x values, y values, marker name/index, label). Omit color to
|
|
78
|
+
use the active theme palette cycle.
|
|
79
|
+
location (str): Location of the legend.
|
|
80
|
+
fig_size (Tuple[float, float], optional): Figure size. Defaults to the active style.
|
|
81
|
+
label (Tuple[str, str], optional): Labels for the x and y axes. Defaults to ("x-label", "y-label").
|
|
82
|
+
ax (Optional[plt.Axes], optional): Axes to plot on. Creates new if None. Defaults to None.
|
|
83
|
+
xticks (Optional[List[float] | range], optional): Custom x-axis ticks. Defaults to None.
|
|
84
|
+
yticks (Optional[List[float] | range], optional): Custom y-axis ticks. Defaults to None.
|
|
85
|
+
xstart (Optional[float], optional): Minimum x-axis value. Defaults to None.
|
|
86
|
+
ystart (Optional[float], optional): Minimum y-axis value. Defaults to None.
|
|
87
|
+
font_size (float, optional): Base font size for labels, ticks, and legend. Defaults to the active style.
|
|
88
|
+
label_size (float, optional): Axis label size. Defaults to font_size or the active style.
|
|
89
|
+
tick_size (float, optional): Tick label size. Defaults to font_size or the active style.
|
|
90
|
+
legend_size (float, optional): Legend text size. Defaults to font_size or the active style.
|
|
91
|
+
ncols (int, optional): Number of columns in the legend. Defaults to 1.
|
|
92
|
+
columnspacing (float, optional): Spacing between legend columns. Defaults to 0.5.
|
|
93
|
+
grid (str, optional): Grid preset: "major-y", "major", "major-minor", or "none".
|
|
94
|
+
fname (Optional[str], optional): Filename to save the plot. Defaults to "plot.pdf".
|
|
95
|
+
"""
|
|
96
|
+
style = get_current_style()
|
|
97
|
+
if fig_size is None:
|
|
98
|
+
fig_size = style["fig_size"]
|
|
99
|
+
font_size, label_size, tick_size, legend_size = _resolve_text_sizes(
|
|
100
|
+
style,
|
|
101
|
+
font_size,
|
|
102
|
+
label_size,
|
|
103
|
+
tick_size,
|
|
104
|
+
legend_size,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if ax is None:
|
|
108
|
+
fig, ax = plt.subplots(figsize=fig_size)
|
|
109
|
+
else:
|
|
110
|
+
fig = ax.figure
|
|
111
|
+
|
|
112
|
+
ax.set_prop_cycle(color=list(style["palette"]))
|
|
113
|
+
ax.set_xlabel(label[0], fontsize=label_size)
|
|
114
|
+
ax.set_ylabel(label[1], fontsize=label_size)
|
|
115
|
+
apply_grid(ax, grid or str(style["line_grid"]))
|
|
116
|
+
|
|
117
|
+
for line in lines:
|
|
118
|
+
draw(ax, *_parse_line(line))
|
|
119
|
+
|
|
120
|
+
styled_legend(
|
|
121
|
+
ax,
|
|
122
|
+
location,
|
|
123
|
+
legend_size=legend_size,
|
|
124
|
+
ncols=ncols,
|
|
125
|
+
columnspacing=columnspacing,
|
|
126
|
+
legend_outside=legend_outside,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if xticks is not None:
|
|
130
|
+
ax.set_xticks(xticks)
|
|
131
|
+
if yticks is not None:
|
|
132
|
+
ax.set_yticks(yticks)
|
|
133
|
+
|
|
134
|
+
ax.tick_params(axis="both", labelsize=tick_size)
|
|
135
|
+
apply_axis_style(ax)
|
|
136
|
+
|
|
137
|
+
if xstart is not None:
|
|
138
|
+
ax.set_xlim(left=xstart)
|
|
139
|
+
if ystart is not None:
|
|
140
|
+
ax.set_ylim(bottom=ystart)
|
|
141
|
+
|
|
142
|
+
if fname:
|
|
143
|
+
save(fig, fname, close=False)
|
|
144
|
+
|
|
145
|
+
return fig, ax
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
if __name__ == "__main__":
|
|
149
|
+
configure_plot_style()
|
|
150
|
+
data: List[Tuple[List[float], List[float], str | int, str | int, str]] = [
|
|
151
|
+
([10, 20, 30, 40, 50], [5, 10, 15, 20, 25], "blue", 8, "Method A"),
|
|
152
|
+
(
|
|
153
|
+
[10, 20, 30, 40, 50],
|
|
154
|
+
[6, 11, 14, 18, 22],
|
|
155
|
+
"orange",
|
|
156
|
+
0,
|
|
157
|
+
"Method B",
|
|
158
|
+
),
|
|
159
|
+
(
|
|
160
|
+
[10, 20, 30, 40, 50],
|
|
161
|
+
[7, 9, 13, 19, 24],
|
|
162
|
+
"green",
|
|
163
|
+
1,
|
|
164
|
+
"Method C",
|
|
165
|
+
),
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
# Single plot usage (unchanged)
|
|
169
|
+
plot_line(
|
|
170
|
+
data,
|
|
171
|
+
"upper left",
|
|
172
|
+
ystart=min(min(y) for _, y, _, _, _ in data),
|
|
173
|
+
yticks=range(0, 30, 5),
|
|
174
|
+
fname="examples/plot.png",
|
|
175
|
+
font_size=8,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Subplot usage example
|
|
179
|
+
fig, axes = plt.subplots(1, 2, figsize=(6, 2))
|
|
180
|
+
plot_line(data, "upper left", ax=axes[0], yticks=range(0, 30, 5), fname=None)
|
|
181
|
+
plot_line(data, "upper left", ax=axes[1], yticks=range(0, 30, 5), fname=None)
|
|
182
|
+
plt.tight_layout(pad=0.2)
|
|
183
|
+
plt.subplots_adjust(wspace=0.27) # Add horizontal space between subplots
|
|
184
|
+
save(fig, "examples/subplot.png")
|