Coverage for src / tracekit / visualization / styles.py: 92%
60 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 23:04 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 23:04 +0000
1"""Plot style presets for different output contexts.
3This module provides comprehensive style presets for publication-quality,
4presentation, screen viewing, and print output.
7Example:
8 >>> from tracekit.visualization.styles import apply_style_preset
9 >>> with apply_style_preset("publication"):
10 ... plot_waveform(signal)
12References:
13 matplotlib rcParams customization
14 Publication and presentation best practices
15"""
17from __future__ import annotations
19from contextlib import contextmanager
20from dataclasses import dataclass, field
21from typing import TYPE_CHECKING, Any
23if TYPE_CHECKING:
24 from collections.abc import Iterator
26try:
27 import matplotlib.pyplot as plt
29 HAS_MATPLOTLIB = True
30except ImportError:
31 HAS_MATPLOTLIB = False
34@dataclass
35class StylePreset:
36 """Style preset configuration for plots.
38 Attributes:
39 name: Preset name
40 dpi: Target DPI (dots per inch)
41 font_family: Font family (serif, sans-serif, monospace)
42 font_size: Base font size in points
43 line_width: Default line width in points
44 marker_size: Default marker size
45 figure_facecolor: Figure background color
46 axes_facecolor: Axes background color
47 axes_edgecolor: Axes edge color
48 grid_color: Grid line color
49 grid_alpha: Grid line transparency
50 grid_linestyle: Grid line style
51 use_latex: Use LaTeX for text rendering
52 tight_layout: Use tight layout
53 rcparams: Additional matplotlib rcParams
54 """
56 name: str
57 dpi: int = 96
58 font_family: str = "sans-serif"
59 font_size: int = 10
60 line_width: float = 1.0
61 marker_size: float = 6.0
62 figure_facecolor: str = "white"
63 axes_facecolor: str = "white"
64 axes_edgecolor: str = "black"
65 grid_color: str = "#B0B0B0"
66 grid_alpha: float = 0.3
67 grid_linestyle: str = "-"
68 use_latex: bool = False
69 tight_layout: bool = True
70 rcparams: dict[str, Any] = field(default_factory=dict)
73# Predefined style presets
75PUBLICATION_PRESET = StylePreset(
76 name="publication",
77 dpi=600,
78 font_family="serif",
79 font_size=10,
80 line_width=0.8,
81 marker_size=4.0,
82 figure_facecolor="white",
83 axes_facecolor="white",
84 axes_edgecolor="black",
85 grid_color="#808080",
86 grid_alpha=0.3,
87 grid_linestyle=":",
88 use_latex=False, # LaTeX optional - requires system install
89 tight_layout=True,
90 rcparams={
91 "axes.linewidth": 0.8,
92 "xtick.major.width": 0.8,
93 "ytick.major.width": 0.8,
94 "xtick.minor.width": 0.6,
95 "ytick.minor.width": 0.6,
96 "lines.antialiased": True,
97 "patch.antialiased": True,
98 "savefig.dpi": 600,
99 "savefig.format": "pdf",
100 "savefig.bbox": "tight",
101 },
102)
104PRESENTATION_PRESET = StylePreset(
105 name="presentation",
106 dpi=96,
107 font_family="sans-serif",
108 font_size=18,
109 line_width=2.5,
110 marker_size=10.0,
111 figure_facecolor="white",
112 axes_facecolor="white",
113 axes_edgecolor="black",
114 grid_color="#CCCCCC",
115 grid_alpha=0.5,
116 grid_linestyle="-",
117 use_latex=False,
118 tight_layout=True,
119 rcparams={
120 "axes.linewidth": 2.0,
121 "xtick.major.width": 2.0,
122 "ytick.major.width": 2.0,
123 "xtick.major.size": 8,
124 "ytick.major.size": 8,
125 "lines.antialiased": True,
126 "savefig.dpi": 150,
127 },
128)
130SCREEN_PRESET = StylePreset(
131 name="screen",
132 dpi=96,
133 font_family="sans-serif",
134 font_size=10,
135 line_width=1.2,
136 marker_size=6.0,
137 figure_facecolor="white",
138 axes_facecolor="white",
139 axes_edgecolor="#333333",
140 grid_color="#B0B0B0",
141 grid_alpha=0.3,
142 grid_linestyle="-",
143 use_latex=False,
144 tight_layout=True,
145 rcparams={
146 "axes.linewidth": 1.0,
147 "lines.antialiased": True,
148 "patch.antialiased": True,
149 },
150)
152PRINT_PRESET = StylePreset(
153 name="print",
154 dpi=300,
155 font_family="serif",
156 font_size=11,
157 line_width=1.2,
158 marker_size=5.0,
159 figure_facecolor="white",
160 axes_facecolor="white",
161 axes_edgecolor="black",
162 grid_color="#707070",
163 grid_alpha=0.3,
164 grid_linestyle=":",
165 use_latex=False,
166 tight_layout=True,
167 rcparams={
168 "axes.linewidth": 1.0,
169 "xtick.major.width": 1.0,
170 "ytick.major.width": 1.0,
171 "lines.antialiased": False, # Sharper lines for print
172 "patch.antialiased": False,
173 "savefig.dpi": 300,
174 "savefig.format": "pdf",
175 },
176)
178# Registry of available presets
179PRESETS: dict[str, StylePreset] = {
180 "publication": PUBLICATION_PRESET,
181 "presentation": PRESENTATION_PRESET,
182 "screen": SCREEN_PRESET,
183 "print": PRINT_PRESET,
184}
187@contextmanager
188def apply_style_preset(
189 preset: str | StylePreset,
190 *,
191 overrides: dict[str, Any] | None = None,
192) -> Iterator[None]:
193 """Apply style preset as context manager.
195 : Provide comprehensive style presets for common use cases
196 with support for custom overrides.
198 Args:
199 preset: Preset name or StylePreset object
200 overrides: Dictionary of rcParams to override
202 Yields:
203 None (use as context manager)
205 Raises:
206 ValueError: If preset name is unknown
207 ImportError: If matplotlib is not available
209 Example:
210 >>> with apply_style_preset("publication"):
211 ... fig, ax = plt.subplots()
212 ... ax.plot(x, y)
213 ... plt.savefig("figure.pdf")
215 >>> # With overrides
216 >>> with apply_style_preset("screen", overrides={"font.size": 14}):
217 ... plot_waveform(signal)
219 References:
220 VIS-024: Plot Style Presets
221 matplotlib style sheets and rcParams
222 """
223 if not HAS_MATPLOTLIB: 223 ↛ 224line 223 didn't jump to line 224 because the condition on line 223 was never true
224 raise ImportError("matplotlib is required for style presets")
226 # Get preset object
227 if isinstance(preset, str):
228 if preset not in PRESETS:
229 raise ValueError(f"Unknown preset: {preset}. Available: {list(PRESETS.keys())}")
230 preset_obj = PRESETS[preset]
231 else:
232 preset_obj = preset
234 # Build rcParams dictionary
235 rc_dict = _preset_to_rcparams(preset_obj)
237 # Apply overrides
238 if overrides:
239 rc_dict.update(overrides)
241 # Apply as context
242 with plt.rc_context(rc_dict):
243 yield
246def _preset_to_rcparams(preset: StylePreset) -> dict[str, Any]:
247 """Convert StylePreset to matplotlib rcParams dictionary.
249 Args:
250 preset: StylePreset object
252 Returns:
253 Dictionary of rcParams
254 """
255 rc = {
256 "figure.dpi": preset.dpi,
257 "font.family": preset.font_family,
258 "font.size": preset.font_size,
259 "lines.linewidth": preset.line_width,
260 "lines.markersize": preset.marker_size,
261 "figure.facecolor": preset.figure_facecolor,
262 "axes.facecolor": preset.axes_facecolor,
263 "axes.edgecolor": preset.axes_edgecolor,
264 "grid.color": preset.grid_color,
265 "grid.alpha": preset.grid_alpha,
266 "grid.linestyle": preset.grid_linestyle,
267 "figure.autolayout": preset.tight_layout,
268 }
270 # LaTeX rendering
271 if preset.use_latex: 271 ↛ 272line 271 didn't jump to line 272 because the condition on line 271 was never true
272 rc["text.usetex"] = True
274 # Merge with additional rcparams
275 rc.update(preset.rcparams)
277 return rc
280def create_custom_preset(
281 name: str,
282 base_preset: str = "screen",
283 **kwargs: Any,
284) -> StylePreset:
285 """Create custom preset by inheriting from base preset.
287 : Support custom presets with inheritance and override.
289 Args:
290 name: Name for custom preset
291 base_preset: Base preset to inherit from
292 **kwargs: Attributes to override
294 Returns:
295 Custom StylePreset object
297 Raises:
298 ValueError: If base_preset is unknown
300 Example:
301 >>> custom = create_custom_preset(
302 ... "my_style",
303 ... base_preset="publication",
304 ... font_size=12,
305 ... line_width=1.5
306 ... )
307 >>> with apply_style_preset(custom):
308 ... plot_data()
310 References:
311 VIS-024: Plot Style Presets with inheritance
312 """
313 if base_preset not in PRESETS: 313 ↛ 314line 313 didn't jump to line 314 because the condition on line 313 was never true
314 raise ValueError(f"Unknown base_preset: {base_preset}")
316 # Get base preset
317 base = PRESETS[base_preset]
319 # Create copy with overrides
320 preset_dict = {
321 "name": name,
322 "dpi": kwargs.get("dpi", base.dpi),
323 "font_family": kwargs.get("font_family", base.font_family),
324 "font_size": kwargs.get("font_size", base.font_size),
325 "line_width": kwargs.get("line_width", base.line_width),
326 "marker_size": kwargs.get("marker_size", base.marker_size),
327 "figure_facecolor": kwargs.get("figure_facecolor", base.figure_facecolor),
328 "axes_facecolor": kwargs.get("axes_facecolor", base.axes_facecolor),
329 "axes_edgecolor": kwargs.get("axes_edgecolor", base.axes_edgecolor),
330 "grid_color": kwargs.get("grid_color", base.grid_color),
331 "grid_alpha": kwargs.get("grid_alpha", base.grid_alpha),
332 "grid_linestyle": kwargs.get("grid_linestyle", base.grid_linestyle),
333 "use_latex": kwargs.get("use_latex", base.use_latex),
334 "tight_layout": kwargs.get("tight_layout", base.tight_layout),
335 "rcparams": kwargs.get("rcparams", base.rcparams.copy()),
336 }
338 return StylePreset(**preset_dict)
341def register_preset(preset: StylePreset) -> None:
342 """Register custom preset in global registry.
344 Args:
345 preset: StylePreset to register
347 Example:
348 >>> custom = create_custom_preset("my_style", base_preset="publication")
349 >>> register_preset(custom)
350 >>> with apply_style_preset("my_style"):
351 ... plot_data()
352 """
353 PRESETS[preset.name] = preset
356def list_presets() -> list[str]:
357 """Get list of available preset names.
359 Returns:
360 List of preset names
362 Example:
363 >>> presets = list_presets()
364 >>> print(presets)
365 ['publication', 'presentation', 'screen', 'print']
366 """
367 return list(PRESETS.keys())
370__all__ = [
371 "PRESENTATION_PRESET",
372 "PRESETS",
373 "PRINT_PRESET",
374 "PUBLICATION_PRESET",
375 "SCREEN_PRESET",
376 "StylePreset",
377 "apply_style_preset",
378 "create_custom_preset",
379 "list_presets",
380 "register_preset",
381]