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 +77 -0
- paperplot/core.py +228 -0
- paperplot/data/fonts/GUST-FONT-LICENSE.txt +29 -0
- paperplot/data/fonts/LICENSE.md +22 -0
- paperplot/data/fonts/texgyreheros-bold.otf +0 -0
- paperplot/data/fonts/texgyreheros-bolditalic.otf +0 -0
- paperplot/data/fonts/texgyreheros-italic.otf +0 -0
- paperplot/data/fonts/texgyreheros-regular.otf +0 -0
- paperplot/data/journals.toml +123 -0
- paperplot/fonts.py +45 -0
- paperplot/journals.py +57 -0
- paperplot/layout.py +133 -0
- paperplot/lint.py +156 -0
- paperplot/mplstyle.py +103 -0
- paperplot/palettes.py +190 -0
- paperplot/plots.py +310 -0
- paperplot/preview.py +250 -0
- paperplot/registry.py +111 -0
- paperplot/save.py +99 -0
- paperplot/style.py +194 -0
- paperplot_quantum-0.1.0.dist-info/METADATA +281 -0
- paperplot_quantum-0.1.0.dist-info/RECORD +27 -0
- paperplot_quantum-0.1.0.dist-info/WHEEL +5 -0
- paperplot_quantum-0.1.0.dist-info/licenses/LICENSE +21 -0
- paperplot_quantum-0.1.0.dist-info/licenses/paperplot/data/fonts/GUST-FONT-LICENSE.txt +29 -0
- paperplot_quantum-0.1.0.dist-info/licenses/paperplot/data/fonts/LICENSE.md +22 -0
- paperplot_quantum-0.1.0.dist-info/top_level.txt +1 -0
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.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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)
|