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/styles.py ADDED
@@ -0,0 +1,911 @@
1
+ from __future__ import annotations
2
+
3
+ from contextlib import contextmanager
4
+ from copy import deepcopy
5
+ from dataclasses import dataclass
6
+ import shutil
7
+ import subprocess
8
+ from typing import Iterator, Literal
9
+
10
+ import matplotlib.pyplot as plt
11
+ from matplotlib.colors import is_color_like, to_hex
12
+
13
+
14
+ GridPreset = Literal["major-y", "major", "major-minor", "none"]
15
+ LatexMode = bool | Literal["auto"]
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class LayoutProfile:
20
+ fig_size: tuple[float, float]
21
+ font_size: float
22
+ line_width: float
23
+ marker_scale: float
24
+ marker_edge_width: float
25
+ axes_linewidth: float
26
+ major_tick_width: float
27
+ minor_tick_width: float
28
+ grid_linewidth: float
29
+ minor_grid_linewidth: float
30
+ grid_alpha: float
31
+ minor_grid_alpha: float
32
+ bar_alpha: float
33
+ bar_edge_width: float
34
+ legend_frameon: bool
35
+ legend_framealpha: float
36
+ legend_frame_linewidth: float
37
+ line_grid: GridPreset
38
+ bar_grid: GridPreset
39
+ label_size: float | None = None
40
+ tick_size: float | None = None
41
+ legend_size: float | None = None
42
+ title_size: float | None = None
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class Theme:
47
+ colors: dict[str, str]
48
+ palette: tuple[str, ...]
49
+ grid_color: str
50
+ axis_color: str
51
+ axis_label_color: str
52
+ tick_color: str
53
+ legend_text_color: str
54
+ legend_edge_color: str
55
+ legend_face_color: str
56
+ text_color: str
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class FontPreset:
61
+ family: str
62
+ serif: tuple[str, ...]
63
+ sans_serif: tuple[str, ...]
64
+ monospace: tuple[str, ...]
65
+ latex_preamble: str
66
+ latex_packages: tuple[str, ...]
67
+
68
+
69
+ BASE_COLORS = {
70
+ # Muted, high-contrast academic colors. Yellow is explicit-only, not cycled.
71
+ "blue": "#2F3A8F",
72
+ "orange": "#A95F32",
73
+ "green": "#28745A",
74
+ "purple": "#7B3F76",
75
+ "brown": "#937860",
76
+ "yellow": "#B8A642",
77
+ "sky_blue": "#4F8EA8",
78
+ "gray": "#6B6B6B",
79
+ "red": "#B04A5A",
80
+ "pink": "#B56A8A",
81
+ "teal": "#3F7F7D",
82
+ "olive": "#536A3B",
83
+ "navy": "#25356F",
84
+ "maroon": "#8A3B57",
85
+ "lime": "#4F8A5B",
86
+ "cyan": "#4F8EA8",
87
+ "magenta": "#8A4D8F",
88
+ "dark_gray": "#404040",
89
+ "light_gray": "#D3D3D3",
90
+ }
91
+
92
+
93
+ LAYOUTS = {
94
+ "paper-1col": LayoutProfile(
95
+ fig_size=(5.5, 3.2),
96
+ font_size=7.5,
97
+ line_width=0.8,
98
+ marker_scale=1.0,
99
+ marker_edge_width=0.5,
100
+ axes_linewidth=0.5,
101
+ major_tick_width=0.48,
102
+ minor_tick_width=0.3,
103
+ grid_linewidth=0.3,
104
+ minor_grid_linewidth=0.2,
105
+ grid_alpha=0.28,
106
+ minor_grid_alpha=0.14,
107
+ bar_alpha=0.86,
108
+ bar_edge_width=0.65,
109
+ legend_frameon=True,
110
+ legend_framealpha=0.6,
111
+ legend_frame_linewidth=0.2,
112
+ line_grid="major",
113
+ bar_grid="major-y",
114
+ ),
115
+ "paper-2col": LayoutProfile(
116
+ fig_size=(3.35, 2.15),
117
+ font_size=9.2,
118
+ line_width=1.05,
119
+ marker_scale=1.22,
120
+ marker_edge_width=0.65,
121
+ axes_linewidth=0.65,
122
+ major_tick_width=0.6,
123
+ minor_tick_width=0.4,
124
+ grid_linewidth=0.36,
125
+ minor_grid_linewidth=0.24,
126
+ grid_alpha=0.24,
127
+ minor_grid_alpha=0.11,
128
+ bar_alpha=0.88,
129
+ bar_edge_width=0.75,
130
+ legend_frameon=True,
131
+ legend_framealpha=0.6,
132
+ legend_frame_linewidth=0.2,
133
+ line_grid="major",
134
+ bar_grid="major-y",
135
+ ),
136
+ "paper-2col-subplot": LayoutProfile(
137
+ fig_size=(3.35, 1.45),
138
+ font_size=5.9,
139
+ line_width=0.65,
140
+ marker_scale=0.68,
141
+ marker_edge_width=0.36,
142
+ axes_linewidth=0.42,
143
+ major_tick_width=0.38,
144
+ minor_tick_width=0.28,
145
+ grid_linewidth=0.22,
146
+ minor_grid_linewidth=0.16,
147
+ grid_alpha=0.2,
148
+ minor_grid_alpha=0.1,
149
+ bar_alpha=0.86,
150
+ bar_edge_width=0.5,
151
+ legend_frameon=True,
152
+ legend_framealpha=0.6,
153
+ legend_frame_linewidth=0.16,
154
+ line_grid="major-y",
155
+ bar_grid="major-y",
156
+ label_size=5.8,
157
+ tick_size=4.8,
158
+ legend_size=5.0,
159
+ title_size=5.8,
160
+ ),
161
+ "paper-2col-span": LayoutProfile(
162
+ fig_size=(6.8, 2.8),
163
+ font_size=8.0,
164
+ line_width=0.9,
165
+ marker_scale=1.05,
166
+ marker_edge_width=0.55,
167
+ axes_linewidth=0.55,
168
+ major_tick_width=0.5,
169
+ minor_tick_width=0.35,
170
+ grid_linewidth=0.35,
171
+ minor_grid_linewidth=0.22,
172
+ grid_alpha=0.26,
173
+ minor_grid_alpha=0.12,
174
+ bar_alpha=0.88,
175
+ bar_edge_width=0.7,
176
+ legend_frameon=True,
177
+ legend_framealpha=0.6,
178
+ legend_frame_linewidth=0.2,
179
+ line_grid="major",
180
+ bar_grid="major-y",
181
+ ),
182
+ "presentation": LayoutProfile(
183
+ fig_size=(7.2, 4.2),
184
+ font_size=12.0,
185
+ line_width=1.4,
186
+ marker_scale=1.45,
187
+ marker_edge_width=0.8,
188
+ axes_linewidth=0.8,
189
+ major_tick_width=0.75,
190
+ minor_tick_width=0.45,
191
+ grid_linewidth=0.5,
192
+ minor_grid_linewidth=0.3,
193
+ grid_alpha=0.24,
194
+ minor_grid_alpha=0.12,
195
+ bar_alpha=0.9,
196
+ bar_edge_width=0.9,
197
+ legend_frameon=True,
198
+ legend_framealpha=0.65,
199
+ legend_frame_linewidth=0.25,
200
+ line_grid="major",
201
+ bar_grid="major-y",
202
+ ),
203
+ }
204
+
205
+
206
+ THEMES = {
207
+ "classic": Theme(
208
+ colors=BASE_COLORS,
209
+ palette=(
210
+ "#2F3A8F",
211
+ "#28745A",
212
+ "#B04A5A",
213
+ "#4F8EA8",
214
+ "#7B3F76",
215
+ "#3F7F7D",
216
+ "#8A3B57",
217
+ "#A95F32",
218
+ "#536A3B",
219
+ "#6B6B6B",
220
+ ),
221
+ grid_color="#A9A9A9",
222
+ axis_color="#3A3A3A",
223
+ axis_label_color="#3F3F3F",
224
+ tick_color="#3F3F3F",
225
+ legend_text_color="#3F3F3F",
226
+ legend_edge_color="#DADADA",
227
+ legend_face_color="#FFFFFF",
228
+ text_color="#3F3F3F",
229
+ ),
230
+ "nature": Theme(
231
+ colors={
232
+ "blue": "#5A4A8A",
233
+ "orange": "#6FA9AA",
234
+ "green": "#737A73",
235
+ "purple": "#8E6BB0",
236
+ "brown": "#A67C52",
237
+ "yellow": "#DDAA33",
238
+ "sky_blue": "#8FC8C9",
239
+ "gray": "#7B7B7B",
240
+ "red": "#B35850",
241
+ "pink": "#C9A3D8",
242
+ "teal": "#2F7F86",
243
+ "olive": "#7B8D42",
244
+ "navy": "#4A3A70",
245
+ "maroon": "#7A3B46",
246
+ "lime": "#8DB67B",
247
+ "cyan": "#8FC8C9",
248
+ "magenta": "#9D74B6",
249
+ "dark_gray": "#4E4E4E",
250
+ "light_gray": "#D9D9D9",
251
+ },
252
+ palette=("#8E6BB0", "#2F7F86", "#737A73", "#5A4A8A", "#6FA9AA"),
253
+ grid_color="#A5A5A5",
254
+ axis_color="#3A3A3A",
255
+ axis_label_color="#3F3F3F",
256
+ tick_color="#3F3F3F",
257
+ legend_text_color="#3F3F3F",
258
+ legend_edge_color="#D8D8D8",
259
+ legend_face_color="#FFFFFF",
260
+ text_color="#3F3F3F",
261
+ ),
262
+ "colorblind": Theme(
263
+ colors={
264
+ "blue": "#0072B2",
265
+ "orange": "#E69F00",
266
+ "green": "#009E73",
267
+ "purple": "#CC79A7",
268
+ "brown": "#A6761D",
269
+ "yellow": "#F0E442",
270
+ "sky_blue": "#56B4E9",
271
+ "gray": "#8F8F8F",
272
+ "red": "#D55E00",
273
+ "pink": "#CC79A7",
274
+ "teal": "#009E73",
275
+ "olive": "#7F8C3A",
276
+ "navy": "#0072B2",
277
+ "maroon": "#8C3A3A",
278
+ "lime": "#7FBF7B",
279
+ "cyan": "#56B4E9",
280
+ "magenta": "#CC79A7",
281
+ "dark_gray": "#3F3F3F",
282
+ "light_gray": "#D4D4D4",
283
+ },
284
+ palette=(
285
+ "#0072B2",
286
+ "#D55E00",
287
+ "#009E73",
288
+ "#CC79A7",
289
+ "#56B4E9",
290
+ "#8C3A3A",
291
+ "#000000",
292
+ "#E69F00",
293
+ ),
294
+ grid_color="#A8A8A8",
295
+ axis_color="#3A3A3A",
296
+ axis_label_color="#3F3F3F",
297
+ tick_color="#3F3F3F",
298
+ legend_text_color="#3F3F3F",
299
+ legend_edge_color="#DADADA",
300
+ legend_face_color="#FFFFFF",
301
+ text_color="#3F3F3F",
302
+ ),
303
+ "mono": Theme(
304
+ colors={
305
+ "blue": "#111111",
306
+ "orange": "#333333",
307
+ "green": "#555555",
308
+ "purple": "#777777",
309
+ "brown": "#999999",
310
+ "yellow": "#BBBBBB",
311
+ "sky_blue": "#666666",
312
+ "gray": "#8C8C8C",
313
+ "red": "#222222",
314
+ "pink": "#AAAAAA",
315
+ "teal": "#444444",
316
+ "olive": "#606060",
317
+ "navy": "#181818",
318
+ "maroon": "#383838",
319
+ "lime": "#B0B0B0",
320
+ "cyan": "#707070",
321
+ "magenta": "#909090",
322
+ "dark_gray": "#303030",
323
+ "light_gray": "#D0D0D0",
324
+ },
325
+ palette=("#111111", "#333333", "#555555", "#777777", "#999999"),
326
+ grid_color="#B0B0B0",
327
+ axis_color="#3A3A3A",
328
+ axis_label_color="#3F3F3F",
329
+ tick_color="#3F3F3F",
330
+ legend_text_color="#3F3F3F",
331
+ legend_edge_color="#D4D4D4",
332
+ legend_face_color="#FFFFFF",
333
+ text_color="#3F3F3F",
334
+ ),
335
+ "warm": Theme(
336
+ colors={
337
+ "blue": "#3A6EA5",
338
+ "orange": "#C8792A",
339
+ "green": "#6E8B3D",
340
+ "purple": "#875A7B",
341
+ "brown": "#8B5E3C",
342
+ "yellow": "#D6A84F",
343
+ "sky_blue": "#6B9AC4",
344
+ "gray": "#8A837C",
345
+ "red": "#B55239",
346
+ "pink": "#B76E79",
347
+ "teal": "#5F8A8B",
348
+ "olive": "#827B3D",
349
+ "navy": "#304E6E",
350
+ "maroon": "#7B3F36",
351
+ "lime": "#A6B46A",
352
+ "cyan": "#7AA6A6",
353
+ "magenta": "#A05D83",
354
+ "dark_gray": "#4C4742",
355
+ "light_gray": "#D8D0C8",
356
+ },
357
+ palette=("#B55239", "#3A6EA5", "#6E8B3D", "#5F8A8B", "#875A7B"),
358
+ grid_color="#B5ACA3",
359
+ axis_color="#443E39",
360
+ axis_label_color="#4A4540",
361
+ tick_color="#4A4540",
362
+ legend_text_color="#4A4540",
363
+ legend_edge_color="#D9D0C8",
364
+ legend_face_color="#FFFFFF",
365
+ text_color="#4A4540",
366
+ ),
367
+ }
368
+
369
+
370
+ FONTS = {
371
+ "libertine": FontPreset(
372
+ family="serif",
373
+ serif=("Linux Libertine O", "Libertinus Serif", "Libertine", "DejaVu Serif"),
374
+ sans_serif=("DejaVu Sans",),
375
+ monospace=("DejaVu Sans Mono", "Courier New", "Courier"),
376
+ latex_preamble=r"\usepackage{libertine}\usepackage[libertine]{newtxmath}",
377
+ latex_packages=("libertine.sty", "newtxmath.sty"),
378
+ ),
379
+ "inconsolata": FontPreset(
380
+ family="monospace",
381
+ serif=("DejaVu Serif",),
382
+ sans_serif=("DejaVu Sans",),
383
+ monospace=("Inconsolata", "DejaVu Sans Mono", "Courier New", "Courier"),
384
+ latex_preamble=r"\usepackage[varqu]{zi4}",
385
+ latex_packages=("zi4.sty",),
386
+ ),
387
+ "serif": FontPreset(
388
+ family="serif",
389
+ serif=("DejaVu Serif", "Times New Roman", "Times"),
390
+ sans_serif=("DejaVu Sans",),
391
+ monospace=("DejaVu Sans Mono", "Courier New", "Courier"),
392
+ latex_preamble="",
393
+ latex_packages=(),
394
+ ),
395
+ "sans": FontPreset(
396
+ family="sans-serif",
397
+ serif=("DejaVu Serif",),
398
+ sans_serif=("DejaVu Sans", "Arial", "Helvetica"),
399
+ monospace=("DejaVu Sans Mono", "Courier New", "Courier"),
400
+ latex_preamble=r"\usepackage{sansmath}",
401
+ latex_packages=("sansmath.sty",),
402
+ ),
403
+ }
404
+
405
+
406
+ _VALID_GRIDS = {"major-y", "major", "major-minor", "none"}
407
+
408
+ TEXT_COLORS = {
409
+ "theme": None,
410
+ "default": None,
411
+ "dark": "#202020",
412
+ "black": "#111111",
413
+ "charcoal": "#2B2B2B",
414
+ "slate": "#344054",
415
+ "gray": "#555555",
416
+ "dimgray": "#696969",
417
+ "muted": "#4A4A4A",
418
+ "soft": "#3F3F3F",
419
+ "light": "#F5F5F5",
420
+ }
421
+
422
+
423
+ def _build_style(
424
+ layout: str,
425
+ theme: str,
426
+ font: str,
427
+ latex: LatexMode,
428
+ font_size: float | None = None,
429
+ label_size: float | None = None,
430
+ tick_size: float | None = None,
431
+ legend_size: float | None = None,
432
+ title_size: float | None = None,
433
+ scale: float = 1.0,
434
+ text_color: str | None = None,
435
+ ) -> dict[str, object]:
436
+ if layout not in LAYOUTS:
437
+ raise ValueError(
438
+ f"Unknown layout {layout!r}. Choose from: {', '.join(available_layouts())}."
439
+ )
440
+ if theme not in THEMES:
441
+ raise ValueError(
442
+ f"Unknown theme {theme!r}. Choose from: {', '.join(available_themes())}."
443
+ )
444
+ if font not in FONTS:
445
+ raise ValueError(f"Unknown font {font!r}. Choose from: {', '.join(FONTS)}.")
446
+
447
+ layout_profile = LAYOUTS[layout]
448
+ theme_profile = THEMES[theme]
449
+ font_profile = FONTS[font]
450
+ scale = float(scale)
451
+ if scale <= 0:
452
+ raise ValueError("scale must be a positive number.")
453
+ resolved_font_size = (
454
+ layout_profile.font_size if font_size is None else float(font_size)
455
+ ) * scale
456
+ if resolved_font_size <= 0:
457
+ raise ValueError("font_size must be a positive number.")
458
+ default_label_size = _scaled_size(
459
+ layout_profile.label_size, resolved_font_size, scale
460
+ )
461
+ default_tick_size = _scaled_size(
462
+ layout_profile.tick_size, resolved_font_size, scale
463
+ )
464
+ default_legend_size = _scaled_size(
465
+ layout_profile.legend_size, resolved_font_size, scale
466
+ )
467
+ default_title_size = _scaled_size(
468
+ layout_profile.title_size, resolved_font_size, scale
469
+ )
470
+ resolved_label_size = _resolve_size("label_size", label_size, default_label_size)
471
+ resolved_tick_size = _resolve_size("tick_size", tick_size, default_tick_size)
472
+ resolved_legend_size = _resolve_size(
473
+ "legend_size", legend_size, default_legend_size
474
+ )
475
+ resolved_title_size = _resolve_size("title_size", title_size, default_title_size)
476
+ resolved_text_color = _resolve_color(
477
+ "text_color", text_color, theme_profile.text_color
478
+ )
479
+ if text_color is None:
480
+ resolved_axis_label_color = theme_profile.axis_label_color
481
+ resolved_tick_color = theme_profile.tick_color
482
+ resolved_legend_text_color = theme_profile.legend_text_color
483
+ resolved_title_color = theme_profile.text_color
484
+ else:
485
+ resolved_axis_label_color = resolved_text_color
486
+ resolved_tick_color = resolved_text_color
487
+ resolved_legend_text_color = resolved_text_color
488
+ resolved_title_color = resolved_text_color
489
+
490
+ return {
491
+ "layout": layout,
492
+ "theme": theme,
493
+ "font": font,
494
+ "latex": _resolve_latex(latex, font),
495
+ "latex_requested": latex,
496
+ "scale": scale,
497
+ "fig_size": layout_profile.fig_size,
498
+ "font_size": resolved_font_size,
499
+ "font_size_override": font_size,
500
+ "label_size": resolved_label_size,
501
+ "label_size_override": label_size,
502
+ "tick_size": resolved_tick_size,
503
+ "tick_size_override": tick_size,
504
+ "legend_size": resolved_legend_size,
505
+ "legend_size_override": legend_size,
506
+ "title_size": resolved_title_size,
507
+ "title_size_override": title_size,
508
+ "font_family": font_profile.family,
509
+ "line_width": layout_profile.line_width * scale,
510
+ "marker_scale": layout_profile.marker_scale * scale,
511
+ "marker_edge_width": layout_profile.marker_edge_width * scale,
512
+ "axes_linewidth": layout_profile.axes_linewidth * scale,
513
+ "major_tick_width": layout_profile.major_tick_width * scale,
514
+ "minor_tick_width": layout_profile.minor_tick_width * scale,
515
+ "grid_color": theme_profile.grid_color,
516
+ "grid_linewidth": layout_profile.grid_linewidth * scale,
517
+ "minor_grid_linewidth": layout_profile.minor_grid_linewidth * scale,
518
+ "grid_alpha": layout_profile.grid_alpha,
519
+ "minor_grid_alpha": layout_profile.minor_grid_alpha,
520
+ "bar_alpha": layout_profile.bar_alpha,
521
+ "bar_edge_width": layout_profile.bar_edge_width * scale,
522
+ "bar_edge_color": "#000000",
523
+ "legend_frameon": layout_profile.legend_frameon,
524
+ "legend_framealpha": layout_profile.legend_framealpha,
525
+ "legend_frame_linewidth": layout_profile.legend_frame_linewidth * scale,
526
+ "line_grid": layout_profile.line_grid,
527
+ "bar_grid": layout_profile.bar_grid,
528
+ "colors": deepcopy(theme_profile.colors),
529
+ "palette": theme_profile.palette,
530
+ "axis_color": theme_profile.axis_color,
531
+ "axis_label_color": resolved_axis_label_color,
532
+ "tick_color": resolved_tick_color,
533
+ "legend_text_color": resolved_legend_text_color,
534
+ "legend_edge_color": theme_profile.legend_edge_color,
535
+ "legend_face_color": theme_profile.legend_face_color,
536
+ "text_color": resolved_text_color,
537
+ "text_color_override": text_color,
538
+ "title_color": resolved_title_color,
539
+ }
540
+
541
+
542
+ def _resolve_size(name: str, size: float | None, default: float) -> float:
543
+ if size is None:
544
+ return default
545
+ resolved = float(size)
546
+ if resolved <= 0:
547
+ raise ValueError(f"{name} must be a positive number.")
548
+ return resolved
549
+
550
+
551
+ def _scaled_size(size: float | None, fallback: float, scale: float) -> float:
552
+ if size is None:
553
+ return fallback
554
+ return float(size) * scale
555
+
556
+
557
+ def _resolve_color(name: str, color, default: str):
558
+ if isinstance(color, str) and color in TEXT_COLORS:
559
+ preset = TEXT_COLORS[color]
560
+ resolved = default if preset is None else preset
561
+ else:
562
+ resolved = default if color is None else color
563
+ if not is_color_like(resolved):
564
+ raise ValueError(f"{name} must be a valid Matplotlib color.")
565
+ return to_hex(resolved)
566
+
567
+
568
+ def _resolve_latex(latex: LatexMode, font: str) -> bool:
569
+ if latex == "auto":
570
+ return _latex_available(font)
571
+ if isinstance(latex, bool):
572
+ return latex
573
+ raise ValueError("latex must be True, False, or 'auto'.")
574
+
575
+
576
+ def _latex_available(font: str) -> bool:
577
+ if shutil.which("latex") is None:
578
+ return False
579
+ packages = FONTS[font].latex_packages
580
+ if not packages:
581
+ return True
582
+ if shutil.which("kpsewhich") is None:
583
+ return False
584
+ for package in packages:
585
+ result = subprocess.run(
586
+ ["kpsewhich", package],
587
+ stdout=subprocess.DEVNULL,
588
+ stderr=subprocess.DEVNULL,
589
+ timeout=2,
590
+ check=False,
591
+ )
592
+ if result.returncode != 0:
593
+ return False
594
+ return True
595
+
596
+
597
+ _CURRENT_STYLE = _build_style("paper-1col", "classic", "inconsolata", True)
598
+
599
+
600
+ def available_layouts() -> tuple[str, ...]:
601
+ """Return the names of available publication layout profiles."""
602
+ return tuple(LAYOUTS)
603
+
604
+
605
+ def available_themes() -> tuple[str, ...]:
606
+ """Return the names of available professional visual themes."""
607
+ return tuple(THEMES)
608
+
609
+
610
+ def available_fonts() -> tuple[str, ...]:
611
+ """Return the names of available font presets."""
612
+ return tuple(FONTS)
613
+
614
+
615
+ def available_text_colors() -> tuple[str, ...]:
616
+ """Return built-in text color preset names."""
617
+ return tuple(TEXT_COLORS)
618
+
619
+
620
+ def figure_size(layout: str | None = None) -> tuple[float, float]:
621
+ """Return the figure size for a layout, or for the active style."""
622
+ if layout is None:
623
+ return tuple(get_current_style()["fig_size"])
624
+ if layout not in LAYOUTS:
625
+ raise ValueError(
626
+ f"Unknown layout {layout!r}. Choose from: {', '.join(available_layouts())}."
627
+ )
628
+ return LAYOUTS[layout].fig_size
629
+
630
+
631
+ def get_current_style() -> dict[str, object]:
632
+ """Return a copy of the active AcadPlot style settings."""
633
+ return deepcopy(_CURRENT_STYLE)
634
+
635
+
636
+ def _sync_public_colors(style: dict[str, object]) -> None:
637
+ from . import utils
638
+
639
+ utils.colors.clear()
640
+ utils.colors.update(style["colors"])
641
+
642
+
643
+ def _apply_rcparams(style: dict[str, object]) -> None:
644
+ font = FONTS[str(style["font"])]
645
+ text_color = str(style["text_color"])
646
+ title_color = str(style["title_color"])
647
+ axis_color = str(style["axis_color"])
648
+ axis_label_color = str(style["axis_label_color"])
649
+ tick_color = str(style["tick_color"])
650
+
651
+ plt.rcParams.update(
652
+ {
653
+ "text.usetex": bool(style["latex"]),
654
+ "font.family": font.family,
655
+ "font.serif": list(font.serif),
656
+ "font.sans-serif": list(font.sans_serif),
657
+ "font.monospace": list(font.monospace),
658
+ "font.size": float(style["font_size"]),
659
+ "axes.linewidth": float(style["axes_linewidth"]),
660
+ "axes.labelsize": float(style["label_size"]),
661
+ "axes.titlesize": float(style["title_size"]),
662
+ "axes.labelcolor": axis_label_color,
663
+ "axes.titlecolor": title_color,
664
+ "axes.edgecolor": axis_color,
665
+ "axes.prop_cycle": plt.cycler(color=list(style["palette"])),
666
+ "text.color": text_color,
667
+ "xtick.labelsize": float(style["tick_size"]),
668
+ "ytick.labelsize": float(style["tick_size"]),
669
+ "xtick.color": tick_color,
670
+ "ytick.color": tick_color,
671
+ "xtick.major.width": float(style["major_tick_width"]),
672
+ "ytick.major.width": float(style["major_tick_width"]),
673
+ "xtick.minor.width": float(style["minor_tick_width"]),
674
+ "ytick.minor.width": float(style["minor_tick_width"]),
675
+ "legend.fontsize": float(style["legend_size"]),
676
+ "legend.labelcolor": str(style["legend_text_color"]),
677
+ "legend.facecolor": str(style["legend_face_color"]),
678
+ "legend.edgecolor": str(style["legend_edge_color"]),
679
+ "legend.frameon": bool(style["legend_frameon"]),
680
+ "legend.framealpha": float(style["legend_framealpha"]),
681
+ "figure.figsize": tuple(style["fig_size"]),
682
+ "figure.titlesize": float(style["title_size"]),
683
+ "figure.titleweight": "normal",
684
+ "grid.color": str(style["grid_color"]),
685
+ "grid.linewidth": float(style["grid_linewidth"]),
686
+ "grid.alpha": float(style["grid_alpha"]),
687
+ "pdf.fonttype": 42,
688
+ "ps.fonttype": 42,
689
+ "svg.fonttype": "none",
690
+ "savefig.dpi": 300,
691
+ }
692
+ )
693
+
694
+ if bool(style["latex"]):
695
+ plt.rcParams["text.latex.preamble"] = font.latex_preamble
696
+ else:
697
+ plt.rcParams["text.latex.preamble"] = ""
698
+
699
+
700
+ def configure_plot_style(
701
+ layout: str = "paper-1col",
702
+ theme: str = "classic",
703
+ font: str = "inconsolata",
704
+ latex: LatexMode = True,
705
+ font_size: float | None = None,
706
+ label_size: float | None = None,
707
+ tick_size: float | None = None,
708
+ legend_size: float | None = None,
709
+ title_size: float | None = None,
710
+ scale: float = 1.0,
711
+ text_color: str | None = None,
712
+ ) -> dict[str, object]:
713
+ """
714
+ Configure global AcadPlot style settings.
715
+
716
+ Declare this once near the top of a script to set publication layout,
717
+ theme colours, font family, and Matplotlib rendering defaults.
718
+ """
719
+ global _CURRENT_STYLE
720
+
721
+ _CURRENT_STYLE = _build_style(
722
+ layout,
723
+ theme,
724
+ font,
725
+ latex,
726
+ font_size=font_size,
727
+ label_size=label_size,
728
+ tick_size=tick_size,
729
+ legend_size=legend_size,
730
+ title_size=title_size,
731
+ scale=scale,
732
+ text_color=text_color,
733
+ )
734
+ _apply_rcparams(_CURRENT_STYLE)
735
+ _sync_public_colors(_CURRENT_STYLE)
736
+ return get_current_style()
737
+
738
+
739
+ @contextmanager
740
+ def use_style(
741
+ layout: str | None = None,
742
+ theme: str | None = None,
743
+ font: str | None = None,
744
+ latex: LatexMode | None = None,
745
+ font_size: float | None = None,
746
+ label_size: float | None = None,
747
+ tick_size: float | None = None,
748
+ legend_size: float | None = None,
749
+ title_size: float | None = None,
750
+ scale: float | None = None,
751
+ text_color: str | None = None,
752
+ ) -> Iterator[dict[str, object]]:
753
+ """Temporarily apply an AcadPlot style inside a ``with`` block."""
754
+ global _CURRENT_STYLE
755
+
756
+ previous_style = get_current_style()
757
+ previous_rcparams = plt.rcParams.copy()
758
+ resolved_font_size = (
759
+ previous_style["font_size_override"] if font_size is None else font_size
760
+ )
761
+ resolved_label_size = (
762
+ previous_style["label_size_override"] if label_size is None else label_size
763
+ )
764
+ resolved_tick_size = (
765
+ previous_style["tick_size_override"] if tick_size is None else tick_size
766
+ )
767
+ resolved_legend_size = (
768
+ previous_style["legend_size_override"] if legend_size is None else legend_size
769
+ )
770
+ resolved_title_size = (
771
+ previous_style["title_size_override"] if title_size is None else title_size
772
+ )
773
+ resolved_text_color = (
774
+ previous_style["text_color_override"] if text_color is None else text_color
775
+ )
776
+ resolved_scale = float(previous_style["scale"]) if scale is None else scale
777
+
778
+ try:
779
+ yield configure_plot_style(
780
+ layout=layout or str(previous_style["layout"]),
781
+ theme=theme or str(previous_style["theme"]),
782
+ font=font or str(previous_style["font"]),
783
+ latex=previous_style["latex_requested"] if latex is None else latex,
784
+ font_size=resolved_font_size,
785
+ label_size=resolved_label_size,
786
+ tick_size=resolved_tick_size,
787
+ legend_size=resolved_legend_size,
788
+ title_size=resolved_title_size,
789
+ scale=resolved_scale,
790
+ text_color=resolved_text_color,
791
+ )
792
+ finally:
793
+ _CURRENT_STYLE = previous_style
794
+ plt.rcParams.update(previous_rcparams)
795
+ _sync_public_colors(_CURRENT_STYLE)
796
+
797
+
798
+ def apply_axis_style(ax) -> None:
799
+ """Apply theme axis, tick, and label colors to an axes."""
800
+ style = get_current_style()
801
+ axis_color = str(style["axis_color"])
802
+ axis_label_color = str(style["axis_label_color"])
803
+ tick_color = str(style["tick_color"])
804
+ title_color = str(style["title_color"])
805
+
806
+ ax.xaxis.label.set_color(axis_label_color)
807
+ ax.yaxis.label.set_color(axis_label_color)
808
+ ax.title.set_color(title_color)
809
+ ax.tick_params(axis="both", colors=tick_color)
810
+ for spine in ax.spines.values():
811
+ spine.set_color(axis_color)
812
+
813
+
814
+ def despine(ax=None, sides: tuple[str, ...] = ("top", "right")):
815
+ """Hide selected axes spines."""
816
+ if ax is None:
817
+ ax = plt.gca()
818
+ for side in sides:
819
+ ax.spines[side].set_visible(False)
820
+ return ax
821
+
822
+
823
+ def format_axes(
824
+ ax=None,
825
+ *,
826
+ grid: GridPreset | None = None,
827
+ despine: bool | tuple[str, ...] = False,
828
+ label_size: float | None = None,
829
+ tick_size: float | None = None,
830
+ title_size: float | None = None,
831
+ ):
832
+ """Apply AcadPlot styling to a manually created Matplotlib axes."""
833
+ if ax is None:
834
+ ax = plt.gca()
835
+
836
+ style = get_current_style()
837
+ resolved_label_size = (
838
+ float(style["label_size"]) if label_size is None else float(label_size)
839
+ )
840
+ resolved_tick_size = (
841
+ float(style["tick_size"]) if tick_size is None else float(tick_size)
842
+ )
843
+ resolved_title_size = (
844
+ float(style["title_size"]) if title_size is None else float(title_size)
845
+ )
846
+
847
+ ax.xaxis.label.set_size(resolved_label_size)
848
+ ax.yaxis.label.set_size(resolved_label_size)
849
+ ax.title.set_size(resolved_title_size)
850
+ ax.tick_params(axis="both", labelsize=resolved_tick_size)
851
+ apply_axis_style(ax)
852
+
853
+ if grid is not None:
854
+ apply_grid(ax, grid)
855
+ if despine:
856
+ sides = ("top", "right") if despine is True else tuple(despine)
857
+ globals()["despine"](ax, sides=sides)
858
+ return ax
859
+
860
+
861
+ def apply_legend_style(legend) -> None:
862
+ """Apply theme legend text and frame colors."""
863
+ style = get_current_style()
864
+ for text in legend.get_texts():
865
+ text.set_color(str(style["legend_text_color"]))
866
+
867
+ frame = legend.get_frame()
868
+ frame.set_facecolor(str(style["legend_face_color"]))
869
+ frame.set_edgecolor(str(style["legend_edge_color"]))
870
+ frame.set_alpha(float(style["legend_framealpha"]))
871
+ frame.set_linewidth(float(style["legend_frame_linewidth"]))
872
+
873
+
874
+ def apply_grid(ax, grid: str) -> None:
875
+ """Apply one of AcadPlot's supported grid presets to an axes."""
876
+ if grid not in _VALID_GRIDS:
877
+ raise ValueError(
878
+ f"Unknown grid {grid!r}. Choose from: {', '.join(_VALID_GRIDS)}."
879
+ )
880
+
881
+ style = get_current_style()
882
+ ax.grid(False, which="both", axis="both")
883
+ ax.set_axisbelow(True)
884
+
885
+ if grid == "none":
886
+ return
887
+
888
+ common = {
889
+ "color": str(style["grid_color"]),
890
+ "linestyle": "-",
891
+ "linewidth": float(style["grid_linewidth"]),
892
+ "alpha": float(style["grid_alpha"]),
893
+ }
894
+
895
+ if grid == "major-y":
896
+ ax.grid(True, which="major", axis="y", **common)
897
+ return
898
+
899
+ ax.grid(True, which="major", axis="both", **common)
900
+
901
+ if grid == "major-minor":
902
+ ax.minorticks_on()
903
+ ax.grid(
904
+ True,
905
+ which="minor",
906
+ axis="both",
907
+ color=str(style["grid_color"]),
908
+ linestyle="-",
909
+ linewidth=float(style["minor_grid_linewidth"]),
910
+ alpha=float(style["minor_grid_alpha"]),
911
+ )