plotstyle 0.1.0a1__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.
Files changed (60) hide show
  1. plotstyle/__init__.py +121 -0
  2. plotstyle/_utils/__init__.py +0 -0
  3. plotstyle/_utils/io.py +113 -0
  4. plotstyle/_utils/warnings.py +86 -0
  5. plotstyle/_version.py +24 -0
  6. plotstyle/cli/__init__.py +0 -0
  7. plotstyle/cli/main.py +553 -0
  8. plotstyle/color/__init__.py +42 -0
  9. plotstyle/color/_rendering.py +86 -0
  10. plotstyle/color/accessibility.py +286 -0
  11. plotstyle/color/data/okabe_ito.json +5 -0
  12. plotstyle/color/data/safe_grayscale.json +7 -0
  13. plotstyle/color/data/tol_bright.json +5 -0
  14. plotstyle/color/data/tol_muted.json +5 -0
  15. plotstyle/color/data/tol_vibrant.json +5 -0
  16. plotstyle/color/grayscale.py +284 -0
  17. plotstyle/color/palettes.py +259 -0
  18. plotstyle/core/__init__.py +0 -0
  19. plotstyle/core/export.py +418 -0
  20. plotstyle/core/figure.py +394 -0
  21. plotstyle/core/migrate.py +579 -0
  22. plotstyle/core/style.py +394 -0
  23. plotstyle/engine/__init__.py +0 -0
  24. plotstyle/engine/fonts.py +309 -0
  25. plotstyle/engine/latex.py +287 -0
  26. plotstyle/engine/rcparams.py +352 -0
  27. plotstyle/integrations/__init__.py +0 -0
  28. plotstyle/integrations/seaborn.py +305 -0
  29. plotstyle/preview/__init__.py +50 -0
  30. plotstyle/preview/gallery.py +337 -0
  31. plotstyle/preview/print_size.py +304 -0
  32. plotstyle/py.typed +0 -0
  33. plotstyle/specs/__init__.py +304 -0
  34. plotstyle/specs/_templates.toml +48 -0
  35. plotstyle/specs/acs.toml +36 -0
  36. plotstyle/specs/cell.toml +35 -0
  37. plotstyle/specs/elsevier.toml +35 -0
  38. plotstyle/specs/ieee.toml +35 -0
  39. plotstyle/specs/nature.toml +35 -0
  40. plotstyle/specs/plos.toml +35 -0
  41. plotstyle/specs/prl.toml +35 -0
  42. plotstyle/specs/schema.py +1095 -0
  43. plotstyle/specs/science.toml +35 -0
  44. plotstyle/specs/springer.toml +35 -0
  45. plotstyle/specs/units.py +761 -0
  46. plotstyle/specs/wiley.toml +35 -0
  47. plotstyle/validation/__init__.py +94 -0
  48. plotstyle/validation/checks/__init__.py +95 -0
  49. plotstyle/validation/checks/_base.py +149 -0
  50. plotstyle/validation/checks/colors.py +394 -0
  51. plotstyle/validation/checks/dimensions.py +166 -0
  52. plotstyle/validation/checks/export.py +205 -0
  53. plotstyle/validation/checks/lines.py +147 -0
  54. plotstyle/validation/checks/typography.py +200 -0
  55. plotstyle/validation/report.py +293 -0
  56. plotstyle-0.1.0a1.dist-info/METADATA +271 -0
  57. plotstyle-0.1.0a1.dist-info/RECORD +60 -0
  58. plotstyle-0.1.0a1.dist-info/WHEEL +4 -0
  59. plotstyle-0.1.0a1.dist-info/entry_points.txt +2 -0
  60. plotstyle-0.1.0a1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,205 @@
1
+ """Export-readiness validation checks.
2
+
3
+ This module registers :func:`check_export_settings`, which verifies that
4
+ Matplotlib's global ``rcParams`` are configured for publication-quality output.
5
+
6
+ Checks performed
7
+ ----------------
8
+ 1. **PDF font embedding** (``pdf.fonttype``) — must be ``42`` (TrueType
9
+ embedding). Setting ``3`` embeds fonts as Type 3 outlines, which many
10
+ journal submission systems reject.
11
+ 2. **PostScript font embedding** (``ps.fonttype``) — same requirement as PDF.
12
+ 3. **Save DPI** (``savefig.dpi``) — must be at least the minimum DPI
13
+ specified in the journal's export spec. A value of ``"figure"`` (the
14
+ Matplotlib default) is treated as ``WARN`` because the resolution depends
15
+ on the figure's screen DPI setting, which is typically 72-100 DPI — below
16
+ the 300+ DPI required for print.
17
+
18
+ Why rcParams and not Figure properties?
19
+ ----------------------------------------
20
+ Font embedding and output DPI are controlled globally via ``rcParams`` in
21
+ Matplotlib, not per-figure. Checking them here ensures that the entire
22
+ export pipeline is configured correctly, not just the figure layout.
23
+
24
+ Example
25
+ -------
26
+ >>> from plotstyle.validation.checks.export import check_export_settings
27
+ >>> results = check_export_settings(fig, spec)
28
+ >>> [r.check_name for r in results]
29
+ ['export.pdf_fonttype', 'export.ps_fonttype', 'export.dpi']
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ from typing import TYPE_CHECKING, Any
35
+
36
+ import matplotlib as mpl
37
+
38
+ from plotstyle.validation.checks._base import check
39
+ from plotstyle.validation.report import CheckResult, CheckStatus
40
+
41
+ if TYPE_CHECKING:
42
+ from matplotlib.figure import Figure
43
+
44
+ from plotstyle.specs.schema import JournalSpec
45
+
46
+ # ---------------------------------------------------------------------------
47
+ # Constants
48
+ # ---------------------------------------------------------------------------
49
+
50
+ # TrueType font embedding value for pdf.fonttype and ps.fonttype.
51
+ # Type 42 embeds complete TrueType outlines; Type 3 embeds bitmaps/outlines
52
+ # that many PDF pre-flight tools and journal submission systems reject.
53
+ _TRUETYPE_FONTTYPE: int = 42
54
+
55
+
56
+ # ---------------------------------------------------------------------------
57
+ # Registered check
58
+ # ---------------------------------------------------------------------------
59
+
60
+
61
+ @check
62
+ def check_export_settings(fig: Figure, spec: JournalSpec) -> list[CheckResult]:
63
+ """Validate Matplotlib export rcParams against journal requirements.
64
+
65
+ Inspects three global ``rcParams`` that control the quality and
66
+ compatibility of saved figures:
67
+
68
+ - ``pdf.fonttype`` — must be :data:`_TRUETYPE_FONTTYPE` (42).
69
+ - ``ps.fonttype`` — must be :data:`_TRUETYPE_FONTTYPE` (42).
70
+ - ``savefig.dpi`` — must be a numeric value ≥ ``spec.export.min_dpi``.
71
+
72
+ Args:
73
+ fig: The :class:`~matplotlib.figure.Figure` being validated. Not used
74
+ by this check (export settings are global), but required by the
75
+ :data:`~plotstyle.validation.checks._base.CheckFunc` interface.
76
+ spec: Journal specification providing ``export.min_dpi`` and
77
+ ``metadata.name`` for error messages.
78
+
79
+ Returns
80
+ -------
81
+ A list of exactly three :class:`~plotstyle.validation.report.CheckResult`
82
+ objects in the order: ``pdf_fonttype``, ``ps_fonttype``, ``dpi``.
83
+
84
+ Example:
85
+ >>> import matplotlib as mpl
86
+ >>> mpl.rcParams["pdf.fonttype"] = 42
87
+ >>> mpl.rcParams["ps.fonttype"] = 42
88
+ >>> mpl.rcParams["savefig.dpi"] = 300
89
+ >>> results = check_export_settings(fig, spec)
90
+ >>> all(r.status.value == "PASS" for r in results)
91
+ True
92
+
93
+ Notes
94
+ -----
95
+ - ``fig`` is explicitly discarded (``_ = fig``) to make it clear this
96
+ is intentional rather than an oversight. The variable binding is
97
+ kept to satisfy the :data:`~plotstyle.validation.checks._base.CheckFunc`
98
+ interface contract.
99
+ - A ``savefig.dpi`` value of ``"figure"`` — Matplotlib's default —
100
+ produces a ``WARN`` rather than a ``FAIL`` because the actual DPI
101
+ is not deterministic at validation time; it depends on the screen or
102
+ backend DPI when :meth:`~matplotlib.figure.Figure.savefig` is called.
103
+ """
104
+ # `fig` is intentionally unused; this check inspects global rcParams only.
105
+ _ = fig
106
+
107
+ results: list[CheckResult] = []
108
+
109
+ # ------------------------------------------------------------------
110
+ # 1. PDF font embedding
111
+ # ------------------------------------------------------------------
112
+ pdf_fonttype: Any = mpl.rcParams.get("pdf.fonttype")
113
+
114
+ if pdf_fonttype == _TRUETYPE_FONTTYPE:
115
+ results.append(
116
+ CheckResult(
117
+ status=CheckStatus.PASS,
118
+ check_name="export.pdf_fonttype",
119
+ message=f"pdf.fonttype = {_TRUETYPE_FONTTYPE} (TrueType fonts will be embedded in PDF output).",
120
+ )
121
+ )
122
+ else:
123
+ results.append(
124
+ CheckResult(
125
+ status=CheckStatus.FAIL,
126
+ check_name="export.pdf_fonttype",
127
+ message=(
128
+ f"pdf.fonttype = {pdf_fonttype!r}; must be {_TRUETYPE_FONTTYPE} "
129
+ "for TrueType font embedding. Type 3 fonts are rejected by "
130
+ "many journal submission systems."
131
+ ),
132
+ fix_suggestion=(
133
+ f"Call plotstyle.use() to apply all required rcParams, or set "
134
+ f"mpl.rcParams['pdf.fonttype'] = {_TRUETYPE_FONTTYPE} manually."
135
+ ),
136
+ )
137
+ )
138
+
139
+ # ------------------------------------------------------------------
140
+ # 2. PostScript font embedding
141
+ # ------------------------------------------------------------------
142
+ ps_fonttype: Any = mpl.rcParams.get("ps.fonttype")
143
+
144
+ if ps_fonttype == _TRUETYPE_FONTTYPE:
145
+ results.append(
146
+ CheckResult(
147
+ status=CheckStatus.PASS,
148
+ check_name="export.ps_fonttype",
149
+ message=f"ps.fonttype = {_TRUETYPE_FONTTYPE} (TrueType fonts will be embedded in PostScript output).",
150
+ )
151
+ )
152
+ else:
153
+ results.append(
154
+ CheckResult(
155
+ status=CheckStatus.FAIL,
156
+ check_name="export.ps_fonttype",
157
+ message=(
158
+ f"ps.fonttype = {ps_fonttype!r}; must be {_TRUETYPE_FONTTYPE} "
159
+ "for TrueType font embedding in PostScript/EPS output."
160
+ ),
161
+ fix_suggestion=(
162
+ f"Call plotstyle.use() to apply all required rcParams, or set "
163
+ f"mpl.rcParams['ps.fonttype'] = {_TRUETYPE_FONTTYPE} manually."
164
+ ),
165
+ )
166
+ )
167
+
168
+ # ------------------------------------------------------------------
169
+ # 3. Save DPI
170
+ # ------------------------------------------------------------------
171
+ required_dpi: float = spec.export.min_dpi
172
+ savefig_dpi: Any = mpl.rcParams.get("savefig.dpi", "figure")
173
+
174
+ if isinstance(savefig_dpi, (int, float)) and savefig_dpi >= required_dpi:
175
+ results.append(
176
+ CheckResult(
177
+ status=CheckStatus.PASS,
178
+ check_name="export.dpi",
179
+ message=(
180
+ f"savefig.dpi = {savefig_dpi} meets the "
181
+ f"{spec.metadata.name} minimum of {required_dpi} DPI."
182
+ ),
183
+ )
184
+ )
185
+ else:
186
+ # A non-numeric value (e.g., "figure") is a warning rather than a
187
+ # hard failure because the actual DPI is resolved at save time and
188
+ # may be overridden by an explicit dpi= kwarg in savefig().
189
+ results.append(
190
+ CheckResult(
191
+ status=CheckStatus.WARN,
192
+ check_name="export.dpi",
193
+ message=(
194
+ f"savefig.dpi = {savefig_dpi!r}; "
195
+ f"{spec.metadata.name} requires ≥ {required_dpi} DPI. "
196
+ "The figure may be saved at insufficient resolution."
197
+ ),
198
+ fix_suggestion=(
199
+ f"Use plotstyle.savefig() which enforces {required_dpi} DPI, "
200
+ f"or set mpl.rcParams['savefig.dpi'] = {int(required_dpi)}."
201
+ ),
202
+ )
203
+ )
204
+
205
+ return results
@@ -0,0 +1,147 @@
1
+ """Line weight validation check.
2
+
3
+ This module registers :func:`check_line_weights`, which verifies that every
4
+ :class:`~matplotlib.lines.Line2D` object and every axis spine in a figure
5
+ meets the journal's minimum line weight specification.
6
+
7
+ Why line weights matter
8
+ -----------------------
9
+ Many journals (particularly IEEE and physical-sciences publishers) specify a
10
+ minimum line weight — typically 0.5 pt — to ensure that figure elements remain
11
+ visible after the half-tone or rasterisation processes applied during printing.
12
+ Lines below this threshold can disappear or appear as artefacts in the final
13
+ publication.
14
+
15
+ Scope
16
+ -----
17
+ The check covers:
18
+
19
+ - **Line2D objects** — from ``ax.plot``, ``ax.step``, ``ax.axhline``, etc.
20
+ - **Axis spines** — the box borders drawn around each axes (``top``,
21
+ ``bottom``, ``left``, ``right``).
22
+
23
+ Intentionally excluded: tick marks, grid lines, legend borders, and
24
+ collection/patch edges, because these are controlled separately and their
25
+ weight requirements vary more across journals.
26
+
27
+ Example
28
+ -------
29
+ >>> from plotstyle.validation.checks.lines import check_line_weights
30
+ >>> results = check_line_weights(fig, spec)
31
+ >>> results[0].check_name
32
+ 'lines.min_weight'
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ from typing import TYPE_CHECKING
38
+
39
+ from plotstyle.validation.checks._base import check
40
+ from plotstyle.validation.report import CheckResult, CheckStatus
41
+
42
+ if TYPE_CHECKING:
43
+ from matplotlib.figure import Figure
44
+
45
+ from plotstyle.specs.schema import JournalSpec
46
+
47
+ # Maximum number of violation examples to include in the result message
48
+ # before truncating with an implicit "and N more…" (not shown; the user is
49
+ # expected to inspect the figure systematically once they know a problem exists).
50
+ _MAX_VIOLATION_EXAMPLES: int = 5
51
+
52
+
53
+ @check
54
+ def check_line_weights(fig: Figure, spec: JournalSpec) -> list[CheckResult]:
55
+ """Validate line and spine widths against the journal's minimum requirement.
56
+
57
+ Iterates over every :class:`~matplotlib.lines.Line2D` and axis spine in
58
+ *fig* and records any whose ``linewidth`` falls below
59
+ ``spec.line.min_weight_pt``.
60
+
61
+ Args:
62
+ fig: The :class:`~matplotlib.figure.Figure` to inspect.
63
+ spec: Journal specification providing ``line.min_weight_pt`` and
64
+ ``metadata.name`` for error messages.
65
+
66
+ Returns
67
+ -------
68
+ A list containing exactly one :class:`~plotstyle.validation.report.CheckResult`
69
+ with check name ``"lines.min_weight"``. The status is:
70
+
71
+ - ``PASS`` — all inspected elements meet the minimum line weight.
72
+ - ``FAIL`` — one or more elements are below the minimum, with up to
73
+ :data:`_MAX_VIOLATION_EXAMPLES` offending names and widths listed.
74
+
75
+ Example:
76
+ >>> import matplotlib.pyplot as plt
77
+ >>> fig, ax = plt.subplots()
78
+ >>> ax.plot([0, 1], [0, 1], lw=0.3) # below most journal minimums
79
+ >>> results = check_line_weights(fig, spec)
80
+ >>> results[0].is_failure
81
+ True
82
+
83
+ Notes
84
+ -----
85
+ - Line labels are read from
86
+ :meth:`~matplotlib.lines.Line2D.get_label`. Auto-generated legend
87
+ labels (``"_line0"``, ``"_collection0"``, etc.) start with
88
+ ``"_"``, but the check reports them as ``"(unlabeled)"`` only when
89
+ the label is empty or ``None`` — the underscore-prefixed auto-labels
90
+ are included as-is so the user can identify the artist.
91
+ - The tolerance applied to width comparisons is strict (``<`` rather
92
+ than ``<=``) so that a line *exactly* at the minimum passes.
93
+ """
94
+ min_linewidth: float = spec.line.min_weight_pt
95
+ violations: list[str] = []
96
+
97
+ for ax in fig.get_axes():
98
+ # --- Plotted Line2D objects ---
99
+ for line in ax.get_lines():
100
+ lw = line.get_linewidth()
101
+ if lw < min_linewidth:
102
+ # Prefer the user-set label; fall back to a generic descriptor.
103
+ label = line.get_label() or "(unlabeled line)"
104
+ violations.append(f"{label!r}: {lw:.2f}pt")
105
+
106
+ # --- Axis spines (frame borders) ---
107
+ for spine_name, spine in ax.spines.items():
108
+ lw = spine.get_linewidth()
109
+ if lw < min_linewidth:
110
+ violations.append(f"spine '{spine_name}': {lw:.2f}pt")
111
+
112
+ if violations:
113
+ # Show only the first few violations to keep the message readable.
114
+ truncated = violations[:_MAX_VIOLATION_EXAMPLES]
115
+ suffix = (
116
+ f" … and {len(violations) - _MAX_VIOLATION_EXAMPLES} more."
117
+ if len(violations) > _MAX_VIOLATION_EXAMPLES
118
+ else ""
119
+ )
120
+ return [
121
+ CheckResult(
122
+ status=CheckStatus.FAIL,
123
+ check_name="lines.min_weight",
124
+ message=(
125
+ f"{len(violations)} element(s) below the "
126
+ f"{spec.metadata.name} minimum of {min_linewidth}pt: "
127
+ f"{'; '.join(truncated)}{suffix}"
128
+ ),
129
+ fix_suggestion=(
130
+ f"Set linewidth ≥ {min_linewidth}pt on all plotted lines. "
131
+ "Apply globally with "
132
+ f"mpl.rcParams['lines.linewidth'] = {min_linewidth}, or "
133
+ "per-line with the lw= keyword argument."
134
+ ),
135
+ )
136
+ ]
137
+
138
+ return [
139
+ CheckResult(
140
+ status=CheckStatus.PASS,
141
+ check_name="lines.min_weight",
142
+ message=(
143
+ f"All plotted lines and spines meet the "
144
+ f"{spec.metadata.name} minimum line weight of {min_linewidth}pt."
145
+ ),
146
+ )
147
+ ]
@@ -0,0 +1,200 @@
1
+ """Typography validation check — font size compliance.
2
+
3
+ This module registers :func:`check_typography`, which verifies that every
4
+ visible text element in a figure falls within the minimum and maximum font
5
+ sizes specified by the target journal.
6
+
7
+ Text elements inspected
8
+ -----------------------
9
+ - Figure-level text (``fig.texts`` — suptitle, annotations added directly to
10
+ the figure).
11
+ - Axes titles (``ax.title``).
12
+ - Axis labels (``ax.xaxis.label``, ``ax.yaxis.label``).
13
+ - Tick labels (``ax.get_xticklabels()``, ``ax.get_yticklabels()``).
14
+ - Legend text entries (from every legend attached to an axes).
15
+
16
+ Empty and whitespace-only text artists are skipped because they contribute no
17
+ visible content and should not be penalised for having a default font size that
18
+ might fall outside the permitted range.
19
+
20
+ Why font size matters
21
+ ---------------------
22
+ Journals enforce minimum font sizes (typically 6-7 pt) to ensure legibility
23
+ after reduction to column width during typesetting. Maximum sizes prevent
24
+ labels from dominating the figure area and clashing with the journal's body
25
+ text.
26
+
27
+ Example
28
+ -------
29
+ >>> from plotstyle.validation.checks.typography import check_typography
30
+ >>> results = check_typography(fig, spec)
31
+ >>> results[0].check_name
32
+ 'typography.font_size'
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ from typing import TYPE_CHECKING
38
+
39
+ from plotstyle.validation.checks._base import check
40
+ from plotstyle.validation.report import CheckResult, CheckStatus
41
+
42
+ if TYPE_CHECKING:
43
+ from matplotlib.figure import Figure
44
+ from matplotlib.text import Text
45
+
46
+ from plotstyle.specs.schema import JournalSpec
47
+
48
+ # Maximum number of violation examples to include in the result message.
49
+ _MAX_VIOLATION_EXAMPLES: int = 5
50
+
51
+
52
+ # ---------------------------------------------------------------------------
53
+ # Internal helpers
54
+ # ---------------------------------------------------------------------------
55
+
56
+
57
+ def _gather_text_artists(fig: Figure) -> list[Text]:
58
+ """Collect all relevant :class:`~matplotlib.text.Text` artists from *fig*.
59
+
60
+ Includes figure-level text and all per-axes text elements that carry
61
+ meaningful content labels: titles, axis labels, tick labels, and legend
62
+ entries.
63
+
64
+ Args:
65
+ fig: The figure to inspect.
66
+
67
+ Returns
68
+ -------
69
+ Flat list of :class:`~matplotlib.text.Text` instances in the order:
70
+ figure texts → (for each axes) title, x-label, y-label, x-tick
71
+ labels, y-tick labels, legend texts.
72
+
73
+ Notes
74
+ -----
75
+ - Axes with empty titles or labels are included; the caller
76
+ (:func:`check_typography`) is responsible for skipping
77
+ whitespace-only entries.
78
+ - The list may contain duplicates if an artist is somehow shared
79
+ across axes; in practice this does not occur in normal figure
80
+ construction.
81
+ """
82
+ texts: list[Text] = list(fig.texts) # copy to avoid mutating fig.texts
83
+
84
+ for ax in fig.get_axes():
85
+ texts.append(ax.title)
86
+ texts.append(ax.xaxis.label)
87
+ texts.append(ax.yaxis.label)
88
+ texts.extend(ax.get_xticklabels())
89
+ texts.extend(ax.get_yticklabels())
90
+
91
+ legend = ax.get_legend()
92
+ if legend is not None:
93
+ texts.extend(legend.get_texts())
94
+
95
+ return texts
96
+
97
+
98
+ # ---------------------------------------------------------------------------
99
+ # Registered check
100
+ # ---------------------------------------------------------------------------
101
+
102
+
103
+ @check
104
+ def check_typography(fig: Figure, spec: JournalSpec) -> list[CheckResult]:
105
+ """Validate all visible text elements against journal font size limits.
106
+
107
+ Gathers every :class:`~matplotlib.text.Text` artist in *fig* (via
108
+ :func:`_gather_text_artists`), skips empty/whitespace-only entries, and
109
+ checks whether each artist's font size is within
110
+ ``[spec.typography.min_font_pt, spec.typography.max_font_pt]``.
111
+
112
+ Args:
113
+ fig: The :class:`~matplotlib.figure.Figure` to inspect. Should be
114
+ fully composed before calling (tick labels may not be finalised
115
+ until the figure is rendered or the layout engine has run).
116
+ spec: Journal specification providing ``typography.min_font_pt``,
117
+ ``typography.max_font_pt``, and ``metadata.name``.
118
+
119
+ Returns
120
+ -------
121
+ A list containing exactly one :class:`~plotstyle.validation.report.CheckResult`
122
+ with check name ``"typography.font_size"``. Status is:
123
+
124
+ - ``PASS`` — all non-empty text elements are within the allowed range.
125
+ - ``FAIL`` — one or more text elements fall outside the range, with
126
+ up to :data:`_MAX_VIOLATION_EXAMPLES` specific violations reported.
127
+
128
+ Example:
129
+ >>> import matplotlib.pyplot as plt
130
+ >>> fig, ax = plt.subplots()
131
+ >>> ax.set_xlabel("Time (s)", fontsize=4) # below most minimums
132
+ >>> results = check_typography(fig, spec)
133
+ >>> results[0].is_failure
134
+ True
135
+
136
+ Notes
137
+ -----
138
+ - Font size is read via :meth:`~matplotlib.text.Text.get_fontsize`,
139
+ which returns the *effective* size in points after resolving any
140
+ relative size strings (``"small"``, ``"large"``, etc.).
141
+ - Tick labels are computed from the axes' locator/formatter when the
142
+ figure is drawn; calling this check before ``fig.canvas.draw()`` may
143
+ return empty tick-label strings, causing them to be skipped. For
144
+ accurate tick-label validation, call ``fig.canvas.draw()`` or use
145
+ ``constrained_layout=True`` before validation.
146
+ """
147
+ min_pt: float = spec.typography.min_font_pt
148
+ max_pt: float = spec.typography.max_font_pt
149
+
150
+ violations: list[str] = []
151
+
152
+ for text in _gather_text_artists(fig):
153
+ content = text.get_text()
154
+
155
+ # Skip artists that carry no visible content.
156
+ if not content or not content.strip():
157
+ continue
158
+
159
+ font_size: float = text.get_fontsize()
160
+
161
+ if font_size < min_pt:
162
+ violations.append(f"{content!r}: {font_size:.1f}pt (< {min_pt}pt min)")
163
+ elif font_size > max_pt:
164
+ violations.append(f"{content!r}: {font_size:.1f}pt (> {max_pt}pt max)")
165
+
166
+ if violations:
167
+ truncated = violations[:_MAX_VIOLATION_EXAMPLES]
168
+ suffix = (
169
+ f" … and {len(violations) - _MAX_VIOLATION_EXAMPLES} more."
170
+ if len(violations) > _MAX_VIOLATION_EXAMPLES
171
+ else ""
172
+ )
173
+ return [
174
+ CheckResult(
175
+ status=CheckStatus.FAIL,
176
+ check_name="typography.font_size",
177
+ message=(
178
+ f"{len(violations)} text element(s) outside the "
179
+ f"{spec.metadata.name} range of {min_pt}-{max_pt}pt: "
180
+ f"{'; '.join(truncated)}{suffix}"
181
+ ),
182
+ fix_suggestion=(
183
+ f"Set all font sizes to {min_pt}-{max_pt}pt for "
184
+ f"{spec.metadata.name} compliance. Apply globally with "
185
+ f"mpl.rcParams['font.size'] = {min_pt}, or per-element "
186
+ "via the fontsize= keyword argument."
187
+ ),
188
+ )
189
+ ]
190
+
191
+ return [
192
+ CheckResult(
193
+ status=CheckStatus.PASS,
194
+ check_name="typography.font_size",
195
+ message=(
196
+ f"All text elements are within the "
197
+ f"{spec.metadata.name} range of {min_pt}-{max_pt}pt."
198
+ ),
199
+ )
200
+ ]