mpl-spaceplot 0.1.1__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.
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.4
2
+ Name: mpl-spaceplot
3
+ Version: 0.1.1
4
+ Summary: layout wrappers for matplotlib
5
+ Author-email: Florian Raths <raths.f@gmail.com>
6
+ License: MIT
7
+ Keywords: matplotlib,plotting,wrappers
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: matplotlib>=3.7
15
+ Requires-Dist: cmcrameri>=1.9
16
+ Dynamic: license-file
17
+
18
+ # spaceplot
@@ -0,0 +1,24 @@
1
+ mpl_spaceplot-0.1.1.dist-info/licenses/LICENSE,sha256=_vaCZw15E27W2FRa7fXnfzoCvRPLfJdvjZYnW-JgDXQ,1070
2
+ spaceplot/__init__.py,sha256=j8LDw-mYSuYJaTGrHKdWA0kxypSb0XMViWUGAmUppfQ,535
3
+ spaceplot/aligner.py,sha256=JZu9bVtM3Z7NlVB6BxeKRpw_XZU0zFWvEG0hsIs9N8I,2871
4
+ spaceplot/montage_plot.py,sha256=vRwvjza81VAJzaqN6EKbBcta6wIT9yo8hiPTXySX_84,9717
5
+ spaceplot/simulate_data.py,sha256=ojgaA3Rj_nWZup-UXsYHptQxx5FmgFiTbU0x22fibIg,2556
6
+ spaceplot/utils.py,sha256=8yA2k9-juq-jSmAzbBgM3z_rd_OHYPHe3Mn_2_h8B6o,1332
7
+ spaceplot/appearance/__init__.py,sha256=r8WmWsdbrdVlwG6wA2E-lTUAjXPh4KWmQN8TRd-u5R0,237
8
+ spaceplot/appearance/display.py,sha256=aW-I-RmxDw4-_RSoY54HnSby6HVMf68xEO5NxZqNkUA,7328
9
+ spaceplot/appearance/inline.py,sha256=_czkKGmHHu3EXGIYd1r0UwDMcP__bt1-Qq-Ir3z6V_o,1186
10
+ spaceplot/appearance/layout.py,sha256=WMgt_nXz9_9rw-uTBbcn-i9bTS_Rc8uUWIs2Q3a5L_k,7781
11
+ spaceplot/appearance/palettes.py,sha256=uVu-9EmP8m07jcUVu-7JgDKJTLbUpOmkrQHrfizCuno,7288
12
+ spaceplot/appearance/styles.py,sha256=YXZiVQ9UZqo3XFqKrlmAsDli7ZFtPWmawru4hAq0NtU,1480
13
+ spaceplot/appearance/tools.py,sha256=S6FXhACX5PUnfIbZ1ILloWdv_zeCrKW9wJk_4FRl3Vk,7406
14
+ spaceplot/decorators/__init__.py,sha256=EY9yo6QBmq9fmbsd7k2-BaimBnF4ldglgKDfAKa9vpc,208
15
+ spaceplot/decorators/decorators.py,sha256=S8ekPl7Wq1fV97GA9v2e53kutGIcJv2vZx6QdpaTIzk,12610
16
+ spaceplot/decorators/tools.py,sha256=KjtBjhJ0v2xtU-Es2J6n4EkwTawhYMaSWagBi4oonV4,2876
17
+ spaceplot/plotting/__init__.py,sha256=8fUm0IVPZCWvd-QY7Os2xQldxFGv3UsjCCzHm_xJXG8,134
18
+ spaceplot/plotting/plotting.py,sha256=Pt2AcNOpMHPhjO8qZCYjx0i-085j-Rw-DnRr_lwfFvk,5315
19
+ spaceplot/plotting/tools.py,sha256=4x9ciSWgVCHbmBfOUi0JWEgV9O0SHplVwkqsAvO1fyM,2359
20
+ spaceplot/resources/font_loader.py,sha256=xFJk9DdyucFTwvoj1q2TLdp0Cw43vnXoa2lXxkgt-GQ,270
21
+ mpl_spaceplot-0.1.1.dist-info/METADATA,sha256=9TlN5ihQi5Yo6zexDnRJ84n2JY5LBLFwD6W-DIL2ZFM,525
22
+ mpl_spaceplot-0.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
+ mpl_spaceplot-0.1.1.dist-info/top_level.txt,sha256=p7OWPWO7oZw08wku3-XvgUCcPpiMfIKe_sRVqHE82r8,10
24
+ mpl_spaceplot-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Florian Raths
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ spaceplot
spaceplot/__init__.py ADDED
@@ -0,0 +1,25 @@
1
+ import matplotlib.pyplot as plt
2
+
3
+ from . import aligner
4
+ from . import decorators as decs
5
+ from . import utils as ut
6
+ from .appearance import palettes as plts
7
+ from .appearance.display import Theme, display
8
+ from .appearance.layout import layout
9
+ from .montage_plot import montage_plot
10
+ from .plotting import plt_category, plt_continous, plt_image
11
+
12
+ __all__ = [
13
+ 'aligner',
14
+ 'decs',
15
+ 'ut',
16
+ 'plts',
17
+ 'Theme',
18
+ 'display',
19
+ 'layout',
20
+ 'montage_plot',
21
+ 'plt_category',
22
+ 'plt_continous',
23
+ 'plt_image',
24
+ 'plt',
25
+ ]
spaceplot/aligner.py ADDED
@@ -0,0 +1,112 @@
1
+ from typing import Literal
2
+
3
+ import numpy as np
4
+
5
+ aligns = Literal[
6
+ 'top_left',
7
+ 'top_right',
8
+ 'bottom_left',
9
+ 'bottom_right',
10
+ 'center_left',
11
+ 'center_right',
12
+ 'center_top',
13
+ 'center_bottom',
14
+ ]
15
+
16
+ align_map = {
17
+ 'c': 0.5,
18
+ 'l': 0,
19
+ 'r': 1,
20
+ 'b': 0,
21
+ 't': 1,
22
+ }
23
+
24
+ align_full_names = {
25
+ 'c': 'center',
26
+ 'l': 'left',
27
+ 'r': 'right',
28
+ 'b': 'bottom',
29
+ 't': 'top',
30
+ }
31
+
32
+
33
+ def translate_align(how: aligns, format: str = 'frac', xfact: float = None, yfact: float = None) -> tuple[float, float]:
34
+ if how is None and (xfact is None or yfact is None):
35
+ raise ValueError("Either 'how' must be provided or both 'xfact' and 'yfact' must be specified.")
36
+
37
+ if how is not None:
38
+ x_a, y_a = parse_alignment(how)
39
+ else:
40
+ x_a, y_a = 'c', 'c'
41
+
42
+ x = xfact if xfact else align_map[x_a]
43
+ y = yfact if yfact else align_map[y_a]
44
+
45
+ if format == 'name':
46
+ x = align_full_names[x_a] if xfact is None else xfact
47
+ y = align_full_names[y_a] if yfact is None else yfact
48
+
49
+ return x, y
50
+
51
+
52
+ def parse_alignment(inpt: aligns) -> tuple[aligns, aligns]:
53
+ """reutrns a tuple of (x, y) alignment based on the input string."""
54
+
55
+ def get_inpt_type(inpt):
56
+ if inpt in ['l', 'r']:
57
+ inpt_type = 'horizontal'
58
+ elif inpt in ['t', 'b']:
59
+ inpt_type = 'vertical'
60
+ else:
61
+ inpt_type = 'centered'
62
+ return inpt_type
63
+
64
+ if len(inpt) > 2:
65
+ if '_' in inpt:
66
+ parts = inpt.split('_')
67
+
68
+ if len(parts) == 2:
69
+ inpt = parts[0][0] + parts[1][0]
70
+ else:
71
+ raise ValueError('Input must be a two-part string separated by an underscore.')
72
+ else:
73
+ inpt = inpt[0]
74
+
75
+ x = y = None
76
+ if len(inpt) == 1:
77
+ in_type = get_inpt_type(inpt)
78
+ if in_type == 'horizontal':
79
+ x, y = inpt, 'c'
80
+ elif in_type == 'vertical':
81
+ x, y = 'c', inpt
82
+ else:
83
+ x = y = 'c'
84
+
85
+ elif len(inpt) == 2:
86
+ in_type_a = get_inpt_type(inpt[0])
87
+ in_type_b = get_inpt_type(inpt[1])
88
+
89
+ types = np.array([in_type_a, in_type_b])
90
+
91
+ if in_type_a == in_type_b:
92
+ raise ValueError('Both inputs cannot be of the same kind.')
93
+
94
+ h_idx = np.where(types == 'horizontal')[0]
95
+ v_idx = np.where(types == 'vertical')[0]
96
+
97
+ if len(h_idx) == 0:
98
+ x = 'c'
99
+ elif len(h_idx) == 1:
100
+ x = inpt[h_idx[0]]
101
+ else:
102
+ raise ValueError('More than one horizontal input provided.')
103
+ if len(v_idx) == 0:
104
+ y = 'c'
105
+ elif len(v_idx) == 1:
106
+ y = inpt[v_idx[0]]
107
+ else:
108
+ raise ValueError('More than one vertical input provided.')
109
+ else:
110
+ raise ValueError('Input must be a single character or a two-character string.')
111
+
112
+ return x, y
@@ -0,0 +1,8 @@
1
+ import importlib.resources as resources
2
+
3
+ from ..resources.font_loader import register_fonts
4
+
5
+ # register fonts supplied with package
6
+ path = resources.files('spaceplot.resources.fonts')
7
+ for cont in path.iterdir():
8
+ register_fonts(cont)
@@ -0,0 +1,178 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+ import matplotlib.pyplot as plt
6
+
7
+ from .. import utils
8
+ from . import inline, styles, tools
9
+
10
+
11
+ def display(
12
+ theme: str | Theme = 'default',
13
+ **kwargs,
14
+ ):
15
+ theme = Theme(source_theme=theme, **kwargs) if isinstance(theme, str) else theme
16
+ theme.apply()
17
+
18
+
19
+ @dataclass
20
+ class Theme:
21
+ source_theme: str = None
22
+ explicit_rcParams: dict = None
23
+ retina: bool = False
24
+ transparent: bool = False
25
+ figsize: tuple[float, float] = None
26
+ dpi: int = None
27
+ palette: list[str] = None
28
+ cmap: str = None
29
+ text_color: str = None
30
+ line_color: str = None
31
+ ticks: list[str] = None
32
+ minor_visible: bool = None
33
+ spines: bool = None
34
+ margins: float = None
35
+ grid: bool = None
36
+ grid_color: str = None
37
+ grid_alpha: float = None
38
+ grid_linestyle: str = None
39
+ grid_linewidth: float = None
40
+ tick_linewidth: tuple[float, float] = (None, None)
41
+ tick_pad: tuple[float, float] = (None, None)
42
+ spine_linewidth: float = None
43
+ tick_size: tuple[float, float] = (None, None)
44
+ font_family: str = None
45
+ font_size: int = None
46
+ fig_facecolor: str = None
47
+ axes_facecolor: str = None
48
+ labelsize: dict = field(default_factory=lambda: {'axes': None, 'figure': None, 'ticks': None})
49
+ titlesize: dict = field(default_factory=lambda: {'axes': None, 'figure': None})
50
+ titleweight: dict = field(default_factory=lambda: {'axes': None, 'figure': None})
51
+ labelweight: dict = field(default_factory=lambda: {'axes': None, 'figure': None})
52
+ axes_labelpad: float = None
53
+ axes_titlepad: float = None
54
+ inline_config: dict = field(default_factory=dict)
55
+
56
+ def parse_source_theme(self):
57
+ if isinstance(self.source_theme, str):
58
+ base_theme = styles.themes.get(self.source_theme, {})
59
+ return Theme(source_theme=base_theme)
60
+
61
+ elif isinstance(self.source_theme, dict):
62
+ return Theme(**self.source_theme)
63
+
64
+ else:
65
+ return self.source_theme
66
+
67
+ def reset_defaults(self):
68
+ plt.rcdefaults()
69
+
70
+ @property
71
+ def rcDict(self):
72
+ from cycler import cycler
73
+
74
+ tick_unset_val = None if self.ticks is None else False
75
+ tick_t, tick_b, tick_l, tick_r = tools.set_position(positions=self.ticks, unset_value=tick_unset_val)
76
+
77
+ spine_unset_val = None if self.spines is None else False
78
+ spine_t, spine_b, spine_l, spine_r = tools.set_position(positions=self.spines, unset_value=spine_unset_val)
79
+
80
+ prop_cycle = None if self.palette is None else cycler('color', self.palette)
81
+
82
+ rc_dict = {
83
+ key: value
84
+ for key, value in {
85
+ 'figure.figsize': self.figsize,
86
+ 'figure.dpi': self.dpi,
87
+ 'axes.prop_cycle': prop_cycle,
88
+ 'image.cmap': self.cmap,
89
+ 'xtick.color': self.line_color,
90
+ 'ytick.color': self.line_color,
91
+ 'axes.grid': self.grid,
92
+ 'axes3d.grid': self.grid,
93
+ 'polaraxes.grid': self.grid,
94
+ 'grid.alpha': self.grid_alpha,
95
+ 'grid.color': self.grid_color,
96
+ 'grid.linestyle': self.grid_linestyle,
97
+ 'grid.linewidth': self.grid_linewidth,
98
+ 'text.color': self.text_color,
99
+ 'axes.labelcolor': self.text_color,
100
+ 'axes.titlecolor': self.text_color,
101
+ 'ytick.labelcolor': self.text_color,
102
+ 'xtick.labelcolor': self.text_color,
103
+ 'axes.edgecolor': self.line_color,
104
+ 'axes.facecolor': self.axes_facecolor,
105
+ 'figure.facecolor': self.fig_facecolor,
106
+ 'font.family': self.font_family,
107
+ 'font.size': self.font_size,
108
+ 'figure.titlesize': self.titlesize.get('figure', None),
109
+ 'axes.titlesize': self.titlesize.get('axes', None),
110
+ 'figure.titleweight': self.titleweight.get('figure', None),
111
+ 'axes.titleweight': self.titleweight.get('axes', None),
112
+ 'figure.labelsize': self.labelsize.get('figure', None),
113
+ 'axes.labelsize': self.labelsize.get('axes', None),
114
+ 'xtick.labelsize': self.labelsize.get('ticks', None),
115
+ 'ytick.labelsize': self.labelsize.get('ticks', None),
116
+ 'axes.labelpad': self.axes_labelpad,
117
+ 'axes.titlepad': self.axes_titlepad,
118
+ 'figure.labelweight': self.labelweight.get('figure', None),
119
+ 'axes.labelweight': self.labelweight.get('axes', None),
120
+ 'axes.linewidth': self.spine_linewidth,
121
+ 'axes.xmargin': self.margins,
122
+ 'axes.ymargin': self.margins,
123
+ 'axes.zmargin': self.margins,
124
+ 'axes.spines.top': spine_t,
125
+ 'axes.spines.bottom': spine_b,
126
+ 'axes.spines.left': spine_l,
127
+ 'axes.spines.right': spine_r,
128
+ 'xtick.top': tick_t,
129
+ 'xtick.bottom': tick_b,
130
+ 'ytick.left': tick_l,
131
+ 'ytick.right': tick_r,
132
+ 'ytick.major.left': tick_l,
133
+ 'ytick.major.right': tick_r,
134
+ 'xtick.major.top': tick_t,
135
+ 'xtick.major.bottom': tick_b,
136
+ 'ytick.minor.left': tick_l,
137
+ 'ytick.minor.right': tick_r,
138
+ 'xtick.minor.top': tick_t,
139
+ 'xtick.minor.bottom': tick_b,
140
+ 'ytick.labelleft': tick_l,
141
+ 'ytick.labelright': tick_r,
142
+ 'xtick.labeltop': tick_t,
143
+ 'xtick.labelbottom': tick_b,
144
+ 'xtick.major.pad': utils.maj_min_args(self.tick_pad)[0],
145
+ 'xtick.minor.pad': utils.maj_min_args(self.tick_pad)[1],
146
+ 'ytick.major.pad': utils.maj_min_args(self.tick_pad)[0],
147
+ 'ytick.minor.pad': utils.maj_min_args(self.tick_pad)[1],
148
+ 'xtick.major.size': utils.maj_min_args(self.tick_size)[0],
149
+ 'xtick.minor.size': utils.maj_min_args(self.tick_size)[1],
150
+ 'ytick.major.size': utils.maj_min_args(self.tick_size)[0],
151
+ 'ytick.minor.size': utils.maj_min_args(self.tick_size)[1],
152
+ 'xtick.major.width': utils.maj_min_args(self.tick_linewidth)[0],
153
+ 'xtick.minor.width': utils.maj_min_args(self.tick_linewidth)[1],
154
+ 'ytick.major.width': utils.maj_min_args(self.tick_linewidth)[0],
155
+ 'ytick.minor.width': utils.maj_min_args(self.tick_linewidth)[1],
156
+ 'ytick.minor.visible': self.minor_visible,
157
+ 'xtick.minor.visible': self.minor_visible,
158
+ }.items()
159
+ if value is not None
160
+ }
161
+
162
+ if self.explicit_rcParams is not None:
163
+ rc_dict.update(self.explicit_rcParams)
164
+
165
+ if self.source_theme is not None:
166
+ source = self.parse_source_theme()
167
+ rc_dict = {**source.rcDict, **rc_dict}
168
+
169
+ return rc_dict
170
+
171
+ def apply(self):
172
+ if self.source_theme == 'default':
173
+ print('Theme: resetting to matplotlib defaults')
174
+ plt.rcdefaults()
175
+
176
+ plt.rcParams.update(self.rcDict)
177
+
178
+ inline.inline_config(retina=self.retina, transparent=self.transparent, **self.inline_config)
@@ -0,0 +1,45 @@
1
+ import matplotlib_inline.backend_inline as mpl_inline
2
+ from matplotlib import rcParams
3
+
4
+ rc_mapping = {
5
+ 'dpi': 'figure.dpi',
6
+ 'pad_inches': 'savefig.pad_inches',
7
+ 'facecolor': 'figure.facecolor',
8
+ 'bbox_inches': 'savefig.bbox',
9
+ }
10
+
11
+
12
+ def from_rc(key):
13
+ return rcParams[rc_mapping[key]] if key in rc_mapping else None
14
+
15
+
16
+ def inline_config(
17
+ retina: bool = None,
18
+ facecolor: str = 'rc',
19
+ dpi: int | str = 'rc',
20
+ pad_inches: float | str = 'rc',
21
+ bbox_inches: float | str = 'tight',
22
+ transparent: bool = False,
23
+ **kwargs,
24
+ ):
25
+ dpi = from_rc('dpi') if dpi == 'rc' else dpi
26
+ pad_inches = from_rc('pad_inches') if pad_inches == 'rc' else pad_inches
27
+ bbox_inches = from_rc('bbox_inches') if bbox_inches == 'rc' else bbox_inches
28
+ facecolor = from_rc('facecolor') if facecolor == 'rc' else facecolor
29
+
30
+ facecolor = 'none' if transparent else facecolor
31
+
32
+ if retina:
33
+ inl_format = 'retina'
34
+ dpi = dpi * 2
35
+ else:
36
+ inl_format = 'png'
37
+
38
+ mpl_inline.set_matplotlib_formats(
39
+ inl_format,
40
+ facecolor=facecolor,
41
+ bbox_inches=bbox_inches,
42
+ dpi=dpi,
43
+ pad_inches=pad_inches,
44
+ **kwargs,
45
+ )
@@ -0,0 +1,237 @@
1
+ from collections.abc import Iterable
2
+ from itertools import cycle
3
+
4
+ import numpy as np
5
+
6
+ from .. import decorators as decs
7
+ from .. import utils
8
+ from . import tools
9
+
10
+ major_grid_style = 'solid'
11
+ minor_grid_style = (0, (1, 2))
12
+
13
+
14
+ def layout(
15
+ axs,
16
+ *,
17
+ axis: str = 'both',
18
+ title: str = None,
19
+ x_label: str = None,
20
+ y_label: str = None,
21
+ abc: str | bool = None,
22
+ make_square: bool = None,
23
+ margins: float = None,
24
+ aspect: str | float | tuple = None,
25
+ ticks: tools._tick_vis = None,
26
+ grid: tools._grid_vis = None,
27
+ minor: bool = None,
28
+ spines: tools._tick_vis = None,
29
+ x_breaks: list[float] = None,
30
+ y_breaks: list[float] = None,
31
+ x_lims: list[float] = None,
32
+ y_lims: list[float] = None,
33
+ x_scale: str = None,
34
+ y_scale: str = None,
35
+ x_tick_labels: list[str] = None,
36
+ y_tick_labels: list[str] = None,
37
+ **kwargs,
38
+ ):
39
+ # decompose kwargs into title, label, tick, and grid settings
40
+ title_settings = utils.get_hook_dict(kwargs, 'title', remove_hook=True)
41
+ label_settings = utils.get_hook_dict(kwargs, 'label', remove_hook=True)
42
+
43
+ tick_settings = utils.get_hook_dict(kwargs, 'tick', remove_hook=True)
44
+ grid_settings = utils.get_hook_dict(kwargs, 'grid', remove_hook=False)
45
+ tick_settings.update(grid_settings)
46
+
47
+ # ensure axs is a list
48
+ if not isinstance(axs, Iterable):
49
+ axs = [axs]
50
+ if not isinstance(title, Iterable):
51
+ title = [title]
52
+
53
+ handle_abc_labels(axs, abc, **kwargs)
54
+
55
+ pairs = list(zip(axs, cycle(title)))
56
+
57
+ for ax, title in pairs:
58
+ # handle ticks, grid, and spine visibility
59
+ # NOTE when axis != 'both', ticks=True does weird things... seems to be a matplotlib issue
60
+ handle_tick_settings(ax, axis, ticks, minor, grid, tick_settings)
61
+
62
+ # handle other layout elements
63
+ handle_title(ax, title, title_settings)
64
+ handle_labels(ax, axis, x_label, y_label, label_settings)
65
+ handle_tick_labels(ax, x_tick_labels, y_tick_labels)
66
+
67
+ handle_spines(ax, spines)
68
+ handle_breaks(ax, x_breaks, y_breaks)
69
+ handle_scales(ax, x_scale, y_scale)
70
+ handle_lims(ax, x_lims, y_lims)
71
+
72
+ handle_aspect(ax, aspect)
73
+
74
+ # TODO when x_lim/y_lim are set, margins don't work as expected
75
+ handle_margins(ax, margins, make_square)
76
+
77
+ if make_square is True:
78
+ tools.axis_ratio(ax, yx_ratio=1, margins=margins, how='lims')
79
+
80
+
81
+ def handle_abc_labels(axs, abc=None, **kwargs):
82
+ if abc:
83
+ ax_labels = np.arange(1, len(axs) + 1)
84
+ if abc == 'ABC':
85
+ ax_labels = [chr(64 + num) for num in ax_labels]
86
+ elif abc == 'abc':
87
+ ax_labels = [chr(96 + num) for num in ax_labels]
88
+
89
+ abc_params = utils.get_hook_dict(kwargs, 'abc')
90
+ abc_params['loc'] = abc_params['loc'] if 'loc' in abc_params else 'tl'
91
+ abc_params['size'] = abc_params['size'] if 'size' in abc_params else 18
92
+ for i, ax in enumerate(axs):
93
+ decs.place_abc_label(
94
+ ax,
95
+ label=ax_labels[i],
96
+ pad=0.2,
97
+ **abc_params,
98
+ )
99
+
100
+
101
+ def handle_tick_grid_vis(ax, axis, ticks, minor, grid, tick_settings):
102
+ tools.set_minor_ticks_by_axis(ax, axis=axis)
103
+ minor = False if minor is None else minor
104
+
105
+ tools.set_tick_visibility(ax, axis=axis, ticks=ticks, minor=minor)
106
+
107
+ # set axis below if no grid zorder is specified to make sure grid lines are below other plot elements
108
+ ax_below = False if 'grid_zorder' in tick_settings else True
109
+ ax.set_axisbelow(ax_below)
110
+
111
+ maj_grid, min_grid = tools.set_grid_visibility(ax, axis=axis, grid=grid, minor=minor, apply=False)
112
+ tick_settings['gridOn'] = [maj_grid, min_grid]
113
+
114
+ # Set default grid style, since rcParams don't offer minor grid style
115
+ if 'grid_linestyle' not in tick_settings:
116
+ tick_settings['grid_linestyle'] = [major_grid_style, minor_grid_style]
117
+
118
+
119
+ def handle_text_element(getter, setter, text: str = None, params: dict = {}):
120
+ """Generic helper to get current text if needed and set it with params."""
121
+ if text is None and len(params) == 0:
122
+ return
123
+
124
+ if text is None and len(params) > 0:
125
+ text = getter()
126
+
127
+ setter(text, **params)
128
+
129
+
130
+ def handle_tick_settings(ax, axis, ticks, minor, grid, tick_settings):
131
+ if ticks is None and minor is None and grid is None and len(tick_settings) == 0:
132
+ return
133
+
134
+ # first all the visibility settings
135
+ handle_tick_grid_vis(ax, axis, ticks, minor, grid, tick_settings)
136
+
137
+ # tick (and grid) settings are applied separately for major and minor ticks
138
+ majmin_settings = {k: utils.maj_min_args(maj_min=v) for k, v in tick_settings.items()}
139
+
140
+ for i, which in enumerate(['major', 'minor']):
141
+ tick_settings_select = {k: v[i] for k, v in majmin_settings.items()}
142
+ ax.tick_params(axis=axis, which=which, **tick_settings_select)
143
+
144
+
145
+ def handle_spines(ax, spines):
146
+ if spines is not None:
147
+ tools.set_spine_visibility(ax, spines)
148
+
149
+
150
+ def handle_aspect(ax, aspect):
151
+ if aspect is not None:
152
+ aspect = [aspect] if not isinstance(aspect, (list, tuple)) else aspect
153
+ adjustable = None if len(aspect) < 2 else aspect[1]
154
+ aspect_params = {'aspect': aspect[0], 'adjustable': adjustable}
155
+ ax.set_aspect(**aspect_params)
156
+
157
+
158
+ def handle_breaks(ax, x_breaks, y_breaks):
159
+ if x_breaks is not None:
160
+ ax.set_xticks(x_breaks)
161
+
162
+ if y_breaks is not None:
163
+ ax.set_yticks(y_breaks)
164
+
165
+
166
+ def handle_scales(ax, x_scale, y_scale):
167
+ if y_scale is not None:
168
+ scale_params = tools.parse_scale(scale=y_scale)
169
+ ax.set_yscale(**scale_params)
170
+
171
+ if x_scale is not None:
172
+ scale_params = tools.parse_scale(scale=x_scale)
173
+ ax.set_xscale(**scale_params)
174
+
175
+
176
+ def handle_lims(ax, x_lims, y_lims):
177
+ if y_lims is not None:
178
+ ax.set_ylim(y_lims)
179
+
180
+ if x_lims is not None:
181
+ ax.set_xlim(x_lims)
182
+
183
+
184
+ def handle_title(ax, title, title_params):
185
+ if title is None and len(title_params) == 0:
186
+ return
187
+
188
+ if title is None:
189
+ title = ax.get_title()
190
+ title = None if len(title) == 0 else title
191
+
192
+ if title is not None or len(title_params) > 0:
193
+ handle_text_element(ax.get_title, ax.set_title, title, title_params)
194
+
195
+
196
+ def handle_labels(ax, axis, x_label, y_label, label_params):
197
+ if x_label is None and y_label is None and len(label_params) == 0:
198
+ return
199
+
200
+ loc_lookup = {
201
+ 'x': {'start': 'left', 'center': 'center', 'end': 'right'},
202
+ 'y': {'start': 'bottom', 'center': 'center', 'end': 'top'},
203
+ }
204
+
205
+ def normalize_params(axis_key: str, params: dict | None) -> dict:
206
+ params = params or {}
207
+ loc = params.get('loc')
208
+ if loc is not None:
209
+ try:
210
+ params['loc'] = loc_lookup[axis_key][loc]
211
+ except KeyError:
212
+ raise ValueError(
213
+ f"Invalid {axis_key} label loc '{loc}'. Valid options are {list(loc_lookup[axis_key])}."
214
+ )
215
+ return params
216
+
217
+ x_label_params = normalize_params('x', label_params.copy()) if axis in ['x', 'both'] else {}
218
+ y_label_params = normalize_params('y', label_params.copy()) if axis in ['y', 'both'] else {}
219
+
220
+ handle_text_element(ax.get_xlabel, ax.set_xlabel, x_label, x_label_params)
221
+ handle_text_element(ax.get_ylabel, ax.set_ylabel, y_label, y_label_params)
222
+
223
+
224
+ def handle_tick_labels(ax, x_tick_labels, y_tick_labels):
225
+ if x_tick_labels is not None:
226
+ ax.set_xticklabels(x_tick_labels)
227
+
228
+ if y_tick_labels is not None:
229
+ ax.set_yticklabels(y_tick_labels)
230
+
231
+
232
+ def handle_margins(ax, margins, make_square):
233
+ if margins is not None and not make_square:
234
+ xmargin, ymargin = utils.maj_min_args(margins)
235
+
236
+ ax.set_xmargin(xmargin)
237
+ ax.set_ymargin(ymargin)