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/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")