paperplot-quantum 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.
paperplot/__init__.py ADDED
@@ -0,0 +1,77 @@
1
+ """paperplot — publication-correct matplotlib figures, journal by journal.
2
+
3
+ Quickstart::
4
+
5
+ import paperplot as pp
6
+
7
+ pp.use("aps") # set the journal once
8
+ fig, ax = pp.figure(width="single") # journal-sized, journal-styled
9
+ ax.plot(x, y)
10
+ pp.save(fig, "fig1.pdf") # embeds fonts, runs preflight()
11
+
12
+ Ships Physical Review (APS, incl. PRL/PRX/PRB), Nature, and IEEE, plus a "talk"
13
+ presentation target. matplotlib-only core; seaborn/IPython optional.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from .journals import FontScale, JournalSpec
19
+ from .layout import Width, Row, Grid
20
+ from .lint import Finding, Report, preflight
21
+ from .palettes import (
22
+ OKABE_ITO,
23
+ available_palettes,
24
+ cmap,
25
+ fills,
26
+ is_colorblind_safe,
27
+ palette,
28
+ register_palette,
29
+ strokes,
30
+ )
31
+ from .preview import grayscale_proof, preview_in_page, show
32
+ from .registry import available, get_spec
33
+ from .style import active, apply, rcparams, reset, style, use
34
+ from .core import (
35
+ clean_shared_axes,
36
+ despine,
37
+ figure,
38
+ panel_labels,
39
+ subplots,
40
+ composite,
41
+ )
42
+ from .plots import (
43
+ data_fit_band,
44
+ hist_filled,
45
+ hist_outline,
46
+ show_palettes,
47
+ swatches,
48
+ )
49
+ from .save import save
50
+ from .mplstyle import export_mplstyle, register_mplstyles, to_mplstyle_text
51
+
52
+ __version__ = "0.1.0"
53
+
54
+ __all__ = [
55
+ # types
56
+ "JournalSpec", "FontScale", "Width", "Report", "Finding",
57
+ "Row", "Grid",
58
+ # journal / style
59
+ "use", "style", "reset", "active", "apply", "rcparams",
60
+ "get_spec", "available",
61
+ # figures
62
+ "figure", "subplots", "composite", "save",
63
+ # helpers
64
+ "panel_labels", "despine", "clean_shared_axes",
65
+ # colors
66
+ "palette", "cmap", "register_palette", "OKABE_ITO", "fills", "strokes",
67
+ "available_palettes", "is_colorblind_safe",
68
+ # plot helpers
69
+ "hist_outline", "hist_filled", "data_fit_band", "swatches", "show_palettes",
70
+ # preview
71
+ "show", "preview_in_page", "grayscale_proof",
72
+ # mplstyle on-ramp
73
+ "export_mplstyle", "to_mplstyle_text", "register_mplstyles",
74
+ # lint
75
+ "preflight",
76
+ "__version__",
77
+ ]
paperplot/core.py ADDED
@@ -0,0 +1,228 @@
1
+ """The figure maker and axes helpers.
2
+
3
+ ``figure()`` (aliased ``subplots``) is the one function users touch first; it
4
+ sizes to the journal column and applies the spec's style. ``panel_labels`` /
5
+ ``despine`` / ``clean_shared_axes`` are the small layout helpers.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import string
11
+ from typing import Optional, Sequence
12
+
13
+ import importlib
14
+
15
+ import matplotlib.pyplot as plt
16
+ import numpy as np
17
+
18
+ # The public pp.style() function shadows the style submodule on the package, so
19
+ # reach the module via sys.modules rather than attribute lookup.
20
+ _style = importlib.import_module("paperplot.style")
21
+
22
+
23
+ def figure(width="single", *, aspect="golden", height=None, nrows=1, ncols=1,
24
+ journal=None, palette=None, serif=None, math=None, font_scale=None,
25
+ sharex=False, sharey=False,
26
+ height_ratios: Optional[Sequence[float]] = None,
27
+ width_ratios: Optional[Sequence[float]] = None,
28
+ constrained: bool = True, **subplot_kw):
29
+ """Create a journal-sized, journal-styled figure + axes.
30
+
31
+ Returns ``(fig, ax)`` for a 1x1 grid, else ``(fig, ndarray[Axes])`` — same
32
+ shape contract as ``plt.subplots``. ``serif``/``palette``/``math``/
33
+ ``font_scale`` left as ``None`` inherit whatever ``pp.use()`` set.
34
+ """
35
+ spec = _style.resolve_spec(journal)
36
+ _style.apply(spec, serif=serif, palette=palette, math=math,
37
+ font_scale=font_scale)
38
+
39
+ figsize = spec.figsize(width, aspect=aspect, height=height)
40
+
41
+ gridspec_kw = {}
42
+ if height_ratios is not None:
43
+ gridspec_kw["height_ratios"] = list(height_ratios)
44
+ if width_ratios is not None:
45
+ gridspec_kw["width_ratios"] = list(width_ratios)
46
+
47
+ fig, axes = plt.subplots(
48
+ nrows, ncols, figsize=figsize, sharex=sharex, sharey=sharey,
49
+ layout="constrained" if constrained else None,
50
+ gridspec_kw=gridspec_kw or None, **subplot_kw,
51
+ )
52
+ return fig, axes
53
+
54
+
55
+ subplots = figure # alias
56
+
57
+
58
+ def _flatten(axes):
59
+ if isinstance(axes, np.ndarray):
60
+ return list(axes.ravel())
61
+ if isinstance(axes, (list, tuple)):
62
+ return list(axes)
63
+ return [axes]
64
+
65
+
66
+ def _default_labels(n):
67
+ """a, b, …, z, aa, ab, … — enough letters for ``n`` panels (>26 safe)."""
68
+ import itertools
69
+ out = []
70
+ width = 1
71
+ while len(out) < n:
72
+ for combo in itertools.product(string.ascii_lowercase, repeat=width):
73
+ out.append("".join(combo))
74
+ if len(out) >= n:
75
+ break
76
+ width += 1
77
+ return out
78
+
79
+
80
+ def panel_labels(axes, labels=None, *, loc="outside", offset_pt=None,
81
+ nudge="first-col", size=None, weight="bold", fmt="{}",
82
+ color="black", bbox=False, reserve=None):
83
+ """Stamp ``a, b, c…`` panel letters on each axes.
84
+
85
+ Args:
86
+ loc: ``"outside"`` (above the top-left corner — the journal default) or
87
+ ``"inside"`` (just inside the top-left of the data area).
88
+ offset_pt: ``(dx, dy)`` nudge in points. Defaults to ``(-8, 2)`` outside
89
+ (letters sit over the left margin) / ``(3, -3)`` inside.
90
+ nudge: outside only — which columns shift left by ``dx``.
91
+ ``"first-col"`` (default) shifts only the outer-left column into the
92
+ margin, so inner columns don't widen the inter-column gap;
93
+ ``"all"`` shifts every label; ``"none"`` ignores ``dx``.
94
+ size: font size; defaults to the active journal's panel size.
95
+ bbox: draw a white background box behind each letter (good for
96
+ ``loc="inside"`` over data).
97
+ reserve: whether labels reserve layout space. Default ``True`` for
98
+ ``"outside"`` (so the top row never clips) and ``False`` for
99
+ ``"inside"`` (never clips anyway — no reflow).
100
+
101
+ ``annotation_clip`` is always off, so the axes never hide a label.
102
+ """
103
+ axs = _flatten(axes)
104
+ if labels is None:
105
+ labels = _default_labels(len(axs)) # never runs out past 26 panels
106
+ if size is None:
107
+ spec = _style.active()
108
+ size = spec.font_pt.panel if spec is not None else 9.0
109
+
110
+ inside = loc == "inside"
111
+ if offset_pt is None:
112
+ offset_pt = (3, -3) if inside else (-8, 2)
113
+ dx, dy = offset_pt
114
+ va = "top" if inside else "bottom"
115
+ if reserve is None:
116
+ reserve = not inside
117
+ box = (dict(boxstyle="square,pad=0.15", fc="white", ec="none", alpha=0.85)
118
+ if bbox else None)
119
+
120
+ out = []
121
+ for ax, lab in zip(axs, labels):
122
+ ddx = dx
123
+ if not inside and nudge != "all":
124
+ # left-shift only escapes into the OUTER margin; inner columns sit
125
+ # above their corner so they never widen the inter-column gap.
126
+ ss = ax.get_subplotspec()
127
+ if nudge == "none" or (ss is not None and ss.colspan.start != 0):
128
+ ddx = 0
129
+ ann = ax.annotate(
130
+ fmt.format(lab), xy=(0, 1), xycoords="axes fraction",
131
+ xytext=(ddx, dy), textcoords="offset points",
132
+ ha="left", va=va, fontsize=size, fontweight=weight, color=color,
133
+ annotation_clip=False, bbox=box)
134
+ ann.set_in_layout(reserve)
135
+ out.append(ann)
136
+ return out
137
+
138
+
139
+ def despine(axes, *, top=True, right=True, left=False, bottom=False):
140
+ """Hide the named spines (and their ticks). Inverse of the full-box default."""
141
+ sides = {"top": top, "right": right, "left": left, "bottom": bottom}
142
+ for ax in _flatten(axes):
143
+ for side, drop in sides.items():
144
+ if drop:
145
+ ax.spines[side].set_visible(False)
146
+ ax.tick_params(top=not top, right=not right,
147
+ left=not left, bottom=not bottom,
148
+ labeltop=False, labelright=False)
149
+
150
+
151
+ def clean_shared_axes(fig):
152
+ """On a shared grid, keep tick labels/axis labels only on the outer edge."""
153
+ for ax in fig.axes:
154
+ ss = ax.get_subplotspec()
155
+ if ss is None:
156
+ continue
157
+ if not ss.is_last_row():
158
+ ax.set_xlabel("")
159
+ ax.tick_params(labelbottom=False)
160
+ if not ss.is_first_col():
161
+ ax.set_ylabel("")
162
+ ax.tick_params(labelleft=False)
163
+
164
+
165
+ def _build_grid(fig, grid_spec):
166
+ """Builds the exact absolute coordinates for the grid."""
167
+ # 1. Compute total figure dimensions in inches
168
+ total_width = (grid_spec.margins["left"] +
169
+ grid_spec.ncols * grid_spec.col_width +
170
+ (grid_spec.ncols - 1) * grid_spec.wspace +
171
+ grid_spec.margins["right"])
172
+
173
+ total_height = grid_spec.margins["top"] + grid_spec.margins["bottom"]
174
+ for r in grid_spec.rows:
175
+ total_height += r.height + r.gap_above
176
+
177
+ # Resize figure to the exact required size
178
+ fig.set_size_inches(total_width, total_height)
179
+
180
+ axes_array = np.empty((len(grid_spec.rows), grid_spec.ncols), dtype=object)
181
+ row_names = [r.name for r in grid_spec.rows]
182
+
183
+ # 2. Compute column horizontal positions (relative 0.0 to 1.0)
184
+ lefts = []
185
+ for c in range(grid_spec.ncols):
186
+ x = grid_spec.margins["left"] + c * (grid_spec.col_width + grid_spec.wspace)
187
+ lefts.append(x / total_width)
188
+ w_frac = grid_spec.col_width / total_width
189
+
190
+ # 3. Compute row vertical positions from top to bottom
191
+ current_y = total_height - grid_spec.margins["top"]
192
+
193
+ for r_idx, r in enumerate(grid_spec.rows):
194
+ current_y -= r.gap_above
195
+ current_y -= r.height
196
+
197
+ y_frac = current_y / total_height
198
+ h_frac = r.height / total_height
199
+
200
+ for c_idx in range(grid_spec.ncols):
201
+ # Create exact axis position
202
+ ax = fig.add_axes([lefts[c_idx], y_frac, w_frac, h_frac])
203
+ axes_array[r_idx, c_idx] = ax
204
+
205
+ # Handle attachment semantics
206
+ if r.attached and r_idx > 0:
207
+ ax_above = axes_array[r_idx - 1, c_idx]
208
+ ax.sharex(ax_above)
209
+ ax_above.tick_params(labelbottom=False)
210
+ ax_above.set_xlabel("")
211
+
212
+ from .layout import GridAxesDict
213
+ return GridAxesDict(axes_array, row_names)
214
+
215
+
216
+ def composite(grid=None, journal=None, palette=None, serif=None, math=None, font_scale=None):
217
+ """Create a composite figure using the precise Grid absolute layout engine.
218
+
219
+ Returns ``(fig, ax_dict)`` where ax_dict acts as both a 2D ndarray and a dictionary
220
+ addressable by ``ax["row_name", col_index]``.
221
+ """
222
+ spec = _style.resolve_spec(journal)
223
+ _style.apply(spec, serif=serif, palette=palette, math=math, font_scale=font_scale)
224
+
225
+ # Create an empty figure with NO layout engine to prevent automatic squishing
226
+ fig = plt.figure(figsize=(1, 1), layout="none")
227
+ ax_dict = _build_grid(fig, grid)
228
+ return fig, ax_dict
@@ -0,0 +1,29 @@
1
+ % This is version 1.0, dated 22 June 2009, of the GUST Font License.
2
+ % (GUST is the Polish TeX Users Group, https://www.gust.org.pl)
3
+ %
4
+ % For the most recent version of this license see
5
+ % https://www.gust.org.pl/fonts/licenses/GUST-FONT-LICENSE.txt
6
+ % or
7
+ % https://tug.org/fonts/licenses/GUST-FONT-LICENSE.txt
8
+ %
9
+ % This work may be distributed and/or modified under the conditions
10
+ % of the LaTeX Project Public License, either version 1.3c of this
11
+ % license or (at your option) any later version.
12
+ %
13
+ % Please also observe the following clause:
14
+ % 1) it is requested, but not legally required, that derived works be
15
+ % distributed only after changing the names of the fonts comprising this
16
+ % work and given in an accompanying "manifest", and that the
17
+ % files comprising the Work, as listed in the manifest, also be given
18
+ % new names. Any exceptions to this request are also given in the
19
+ % manifest.
20
+ %
21
+ % We recommend the manifest be given in a separate file named
22
+ % MANIFEST-<fontid>.txt, where <fontid> is some unique identification
23
+ % of the font family. If a separate "readme" file accompanies the Work,
24
+ % we recommend a name of the form README-<fontid>.txt.
25
+ %
26
+ % The latest version of the LaTeX Project Public License is in
27
+ % https://www.latex-project.org/lppl.txt and version 1.3c or later
28
+ % is part of all distributions of LaTeX version 2006/05/20 or later.
29
+
@@ -0,0 +1,22 @@
1
+ # Bundled fonts
2
+
3
+ ## TeX Gyre Heros (`texgyreheros-*.otf`, version 2.004)
4
+
5
+ A free, metric-compatible substitute for **Helvetica**, bundled so paperplot
6
+ figures render in a Helvetica/Arial-like face on *any* machine — Arial and
7
+ Helvetica themselves are proprietary and cannot be redistributed.
8
+
9
+ - Family name in matplotlib: **`TeX Gyre Heros`**
10
+ - Version bundled: **2.004** (original, unmodified upstream OpenType releases)
11
+ - Source: GUST e-foundry — https://www.gust.org.pl/projects/e-foundry/tex-gyre/heros
12
+ - License: **GUST Font License (GFL)** — a free, LPPL-based license that permits
13
+ redistribution and bundling. Full verbatim text ships alongside these fonts in
14
+ [`GUST-FONT-LICENSE.txt`](GUST-FONT-LICENSE.txt)
15
+ (also: http://tug.org/fonts/licenses/GUST-FONT-LICENSE.txt).
16
+
17
+ Copyright (verbatim from the embedded font metadata):
18
+
19
+ > Copyright 2006, 2009 for TeX Gyre extensions by B. Jackowski and J.M. Nowacki
20
+ > (on behalf of TeX users groups).
21
+
22
+ The fonts are provided "as is" without warranty of any kind.
@@ -0,0 +1,123 @@
1
+ # paperplot journal specifications (data, not code).
2
+ # Edit the numbers here to update specs; bump `revision` when content changes.
3
+ schema_version = 1
4
+
5
+ # --- Physical Review (APS): PRL / PRX / PRB / PRA / PRApplied / PRResearch ---
6
+ [journal.aps]
7
+ name = "Physical Review"
8
+ revision = "2024-01"
9
+ page_height_mm = 235.0 # usable two-column text height on US-letter (REVTeX)
10
+ caption_reserve_mm = 25.0 # vertical space kept free for the caption on full-page floats
11
+ min_linewidth_pt = 0.5 # APS minimum line weight (0.18 mm)
12
+ page_body_pt = 10.0 # REVTeX manuscript body text size (for preview_in_page)
13
+ font_family = ["Arial", "Helvetica", "TeX Gyre Heros", "DejaVu Sans"]
14
+
15
+ [journal.aps.widths_mm]
16
+ single = 86.0 # 8.6 cm, 3 3/8 in
17
+ onehalf = 120.0 # advisory, not a fixed APS spec
18
+ double = 178.0 # 17.8 cm, 7 in
19
+
20
+ [journal.aps.font_pt]
21
+ base = 8.0
22
+ tick = 7.5
23
+ panel = 9.0
24
+ legend = 7.5
25
+
26
+ [journal.aps.rasterize_dpi]
27
+ line = 600
28
+ photo = 300
29
+
30
+ # Variants share APS geometry; they override only what differs.
31
+ [journal.aps.variants.prl]
32
+ revision = "2024-01+prl"
33
+
34
+ [journal.aps.variants.prx]
35
+ revision = "2024-01+prx"
36
+
37
+ [journal.aps.variants.prb]
38
+ revision = "2024-01+prb"
39
+
40
+ # --- Nature family ------------------------------------------------------------
41
+ [journal.nature]
42
+ name = "Nature"
43
+ revision = "2024-01"
44
+ page_height_mm = 170.0 # max figure height (leaves room for the legend)
45
+ caption_reserve_mm = 0.0 # the 170 mm cap already excludes legend space
46
+ min_linewidth_pt = 0.5
47
+ page_body_pt = 9.0 # Nature manuscript body text size (for preview_in_page)
48
+ font_family = ["Arial", "Helvetica", "TeX Gyre Heros", "DejaVu Sans"]
49
+
50
+ [journal.nature.widths_mm]
51
+ single = 89.0
52
+ onehalf = 120.0
53
+ double = 183.0
54
+
55
+ [journal.nature.font_pt]
56
+ base = 7.0 # Nature: max 7 pt for figure body text
57
+ tick = 6.5
58
+ panel = 8.0 # panel letters: 8 pt bold
59
+ legend = 6.5
60
+ min_warn_pt = 5.0 # Nature minimum text size
61
+ min_cap_height_mm = 1.3
62
+
63
+ [journal.nature.rasterize_dpi]
64
+ line = 600
65
+ photo = 300
66
+
67
+ # --- IEEE (Transactions / conference two-column) ------------------------------
68
+ [journal.ieee]
69
+ name = "IEEE"
70
+ revision = "2024-01"
71
+ page_height_mm = 235.0 # usable column text height (US-letter, IEEEtran)
72
+ caption_reserve_mm = 0.0
73
+ min_linewidth_pt = 0.5
74
+ page_body_pt = 10.0 # IEEEtran body text size (for preview_in_page)
75
+ # IEEE figures are commonly Times; pass serif=True. This is the sans fallback.
76
+ font_family = ["Arial", "Helvetica", "TeX Gyre Heros", "DejaVu Sans"]
77
+
78
+ [journal.ieee.widths_mm]
79
+ single = 88.9 # 3.5 in column
80
+ onehalf = 120.0
81
+ double = 181.9 # 7.16 in text width (2 x 3.5 in cols + 0.16 in gutter)
82
+
83
+ [journal.ieee.font_pt]
84
+ base = 8.0
85
+ tick = 7.0
86
+ panel = 9.0
87
+ legend = 7.0
88
+ min_warn_pt = 6.0
89
+ min_cap_height_mm = 1.5
90
+
91
+ [journal.ieee.rasterize_dpi]
92
+ line = 600
93
+ photo = 300
94
+
95
+ # --- Presentation target (slides / talks) -------------------------------------
96
+ # Not a journal: a sizing/typography target for projected figures. Larger type,
97
+ # thicker lines (derived from min_linewidth_pt), wider default. preflight's
98
+ # cap-height rule is irrelevant here, so the floor is set permissively.
99
+ [journal.talk]
100
+ name = "Presentation"
101
+ revision = "2024-01"
102
+ page_height_mm = 190.0 # ~16:9 slide content area
103
+ caption_reserve_mm = 0.0
104
+ min_linewidth_pt = 1.0 # thicker strokes survive projection
105
+ page_body_pt = 18.0
106
+ font_family = ["Arial", "Helvetica", "TeX Gyre Heros", "DejaVu Sans"]
107
+
108
+ [journal.talk.widths_mm]
109
+ single = 130.0 # half-slide figure
110
+ onehalf = 180.0
111
+ double = 240.0 # ~full-slide figure
112
+
113
+ [journal.talk.font_pt]
114
+ base = 18.0
115
+ tick = 16.0
116
+ panel = 20.0
117
+ legend = 16.0
118
+ min_warn_pt = 12.0
119
+ min_cap_height_mm = 0.0
120
+
121
+ [journal.talk.rasterize_dpi]
122
+ line = 600
123
+ photo = 200
paperplot/fonts.py ADDED
@@ -0,0 +1,45 @@
1
+ """Font registration.
2
+
3
+ Only redistributably-licensed faces would ever be *bundled* (DejaVu/Liberation/
4
+ TeX-Gyre Heros); Arial/Helvetica are runtime *preferences*, never shipped here.
5
+ If a ``data/fonts`` directory exists, its fonts are registered; otherwise this is
6
+ a silent no-op and matplotlib's resolution + the spec's fallback chain apply.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from importlib.resources import files
12
+
13
+ from matplotlib import font_manager
14
+
15
+ _done = False
16
+
17
+
18
+ def register_bundled(verbose: bool = False) -> int:
19
+ """Register any bundled fonts. Idempotent. Returns the count added."""
20
+ global _done
21
+ if _done:
22
+ return 0
23
+ _done = True
24
+ try:
25
+ font_dir = files("paperplot").joinpath("data", "fonts")
26
+ if not font_dir.is_dir():
27
+ return 0
28
+ added = 0
29
+ for path in font_manager.findSystemFonts(fontpaths=[str(font_dir)]):
30
+ font_manager.fontManager.addfont(path)
31
+ if verbose:
32
+ print(f"registered {path}")
33
+ added += 1
34
+ return added
35
+ except (FileNotFoundError, OSError):
36
+ return 0
37
+
38
+
39
+ def available_family(preferences) -> str | None:
40
+ """Return the first font family from ``preferences`` actually installed."""
41
+ installed = {f.name for f in font_manager.fontManager.ttflist}
42
+ for fam in preferences:
43
+ if fam in installed:
44
+ return fam
45
+ return None
paperplot/journals.py ADDED
@@ -0,0 +1,57 @@
1
+ """The journal spec data types: ``FontScale`` and ``JournalSpec``.
2
+
3
+ These are pure, frozen data. The only behavior is ``figsize`` (pure geometry,
4
+ delegated to :mod:`paperplot.layout`). Styling (``rcparams``) and validation
5
+ (``preflight``) live in their own modules as free functions so this stays plain,
6
+ testable data with no matplotlib-Figure coupling.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass, field
12
+ from typing import Mapping, Tuple
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class FontScale:
17
+ """Point sizes plus the APS cap-height floor."""
18
+
19
+ base: float = 8.0 # axis labels, title, default text
20
+ tick: float = 7.5 # tick labels
21
+ panel: float = 9.0 # (a)(b)(c) panel letters, bold
22
+ legend: float = 7.5
23
+ min_warn_pt: float = 7.0 # preflight warns below this (defaults pass)
24
+ min_cap_height_mm: float = 2.0 # APS rule (reported at info level)
25
+ cap_height_ratio: float = 0.72 # cap-height / em for sans faces
26
+
27
+ def cap_height_mm(self, pt: float) -> float:
28
+ """Approximate printed cap-height of ``pt`` text, in mm."""
29
+ return pt * (25.4 / 72.0) * self.cap_height_ratio
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class JournalSpec:
34
+ """Immutable, queryable description of one journal's figure rules."""
35
+
36
+ name: str
37
+ revision: str
38
+ widths_mm: Mapping[str, float]
39
+ page_height_mm: float
40
+ caption_reserve_mm: float
41
+ font_family: Tuple[str, ...]
42
+ font_pt: FontScale = field(default_factory=FontScale)
43
+ min_linewidth_pt: float = 0.5
44
+ page_body_pt: float = 10.0 # manuscript body text size (for preview_in_page)
45
+ rasterize_dpi: Mapping[str, int] = field(
46
+ default_factory=lambda: {"line": 600, "photo": 300}
47
+ )
48
+
49
+ def width_in(self, width="single") -> float:
50
+ from . import layout
51
+
52
+ return layout.width_in(self, width)
53
+
54
+ def figsize(self, width="single", aspect="golden", height=None):
55
+ from . import layout
56
+
57
+ return layout.figsize(self, width, aspect=aspect, height=height)