Coverage for src / tracekit / visualization / render.py: 94%
38 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"""Visualization rendering functions for DPI-aware output.
3This module provides DPI-aware rendering configuration for adapting
4plot quality and parameters based on target output device (screen vs print).
7Example:
8 >>> from tracekit.visualization.render import configure_dpi_rendering
9 >>> config = configure_dpi_rendering("publication")
10 >>> fig = plt.figure(dpi=config['dpi'], figsize=config['figsize'])
12References:
13 - matplotlib DPI scaling best practices
14 - Print quality standards (300-600 DPI)
15"""
17from __future__ import annotations
19from typing import Any, Literal
21RenderPreset = Literal["screen", "print", "publication"]
24def configure_dpi_rendering(
25 preset: RenderPreset = "screen",
26 *,
27 custom_dpi: int | None = None,
28 dpi: int | None = None,
29 figsize: tuple[float, float] = (10, 6),
30 baseline_dpi: float = 96.0,
31) -> dict[str, Any]:
32 """Configure DPI-aware rendering parameters.
34 Adapts plot rendering quality and parameters based on target DPI
35 for print (300-600 DPI) versus screen (72-96 DPI) with export presets.
37 Args:
38 preset: Rendering preset ("screen", "print", "publication").
39 custom_dpi: Custom DPI override (ignores preset).
40 dpi: Alias for custom_dpi.
41 figsize: Figure size in inches (width, height).
42 baseline_dpi: Baseline DPI for scaling calculations (default 96).
44 Returns:
45 Dictionary with rendering configuration:
46 - dpi: Target DPI
47 - figsize: Figure size
48 - font_scale: Font size scale factor
49 - line_scale: Line width scale factor
50 - marker_scale: Marker size scale factor
51 - antialias: Whether to enable anti-aliasing
52 - format: Recommended file format
53 - style_params: Additional matplotlib rcParams
55 Raises:
56 ValueError: If preset is invalid.
58 Example:
59 >>> config = configure_dpi_rendering("print")
60 >>> plt.rcParams.update(config['style_params'])
61 >>> fig = plt.figure(dpi=config['dpi'], figsize=config['figsize'])
63 References:
64 VIS-017: DPI-Aware Rendering
65 """
66 # Define preset configurations
67 presets = {
68 "screen": {
69 "dpi": 96,
70 "font_family": "sans-serif",
71 "antialias": True,
72 "format": "png",
73 "description": "Screen display (96 DPI)",
74 },
75 "print": {
76 "dpi": 300,
77 "font_family": "sans-serif",
78 "antialias": False,
79 "format": "pdf",
80 "description": "Print output (300 DPI)",
81 },
82 "publication": {
83 "dpi": 600,
84 "font_family": "serif",
85 "antialias": False,
86 "format": "pdf",
87 "description": "Publication quality (600 DPI)",
88 },
89 }
91 # Handle dpi alias
92 if dpi is not None and custom_dpi is None:
93 custom_dpi = dpi
95 if preset not in presets and custom_dpi is None:
96 raise ValueError(f"Invalid preset: {preset}. Must be one of {list(presets.keys())}")
98 # Get preset configuration
99 if custom_dpi is not None:
100 target_dpi = custom_dpi
101 preset_config = {
102 "font_family": "sans-serif",
103 "antialias": True,
104 "format": "png" if target_dpi <= 150 else "pdf",
105 "description": f"Custom ({target_dpi} DPI)",
106 }
107 else:
108 preset_config = presets[preset]
109 target_dpi = preset_config["dpi"] # type: ignore[assignment]
111 # Calculate scale factors based on DPI
112 # Scale factor = target_dpi / baseline_dpi
113 scale = target_dpi / baseline_dpi
115 # Font size scaling: proportional to DPI
116 # Baseline font sizes at 96 DPI: default 10pt
117 font_scale = scale
119 # Line width scaling: proportional to DPI
120 # Baseline line width at 96 DPI: 1.0 pt
121 line_scale = scale
123 # Marker size scaling: proportional to DPI
124 # Baseline marker size at 96 DPI: 6.0 pt
125 marker_scale = scale
127 # Build matplotlib rcParams for this preset
128 style_params = {
129 "figure.dpi": target_dpi,
130 "savefig.dpi": target_dpi,
131 "font.family": preset_config["font_family"],
132 "font.size": 10 * font_scale,
133 "axes.titlesize": 12 * font_scale,
134 "axes.labelsize": 10 * font_scale,
135 "xtick.labelsize": 9 * font_scale,
136 "ytick.labelsize": 9 * font_scale,
137 "legend.fontsize": 9 * font_scale,
138 "lines.linewidth": 1.0 * line_scale,
139 "lines.markersize": 6.0 * marker_scale,
140 "patch.linewidth": 1.0 * line_scale,
141 "grid.linewidth": 0.5 * line_scale,
142 "axes.linewidth": 0.8 * line_scale,
143 "xtick.major.width": 0.8 * line_scale,
144 "ytick.major.width": 0.8 * line_scale,
145 "xtick.minor.width": 0.6 * line_scale,
146 "ytick.minor.width": 0.6 * line_scale,
147 }
149 # Anti-aliasing settings
150 if preset_config["antialias"]:
151 style_params["lines.antialiased"] = True
152 style_params["patch.antialiased"] = True
153 style_params["text.antialiased"] = True
154 else:
155 # Disable for high-DPI print (cleaner output)
156 style_params["lines.antialiased"] = False
157 style_params["patch.antialiased"] = False
158 style_params["text.antialiased"] = False
160 # Publication-specific settings
161 if preset == "publication":
162 style_params["font.family"] = "serif"
163 style_params["mathtext.fontset"] = "cm" # Computer Modern for LaTeX
164 style_params["axes.grid"] = True
165 style_params["grid.alpha"] = 0.3
166 style_params["axes.axisbelow"] = True
168 return {
169 "dpi": target_dpi,
170 "figsize": figsize,
171 "font_scale": font_scale,
172 "line_scale": line_scale,
173 "marker_scale": marker_scale,
174 "antialias": preset_config["antialias"],
175 "format": preset_config["format"],
176 "style_params": style_params,
177 "description": preset_config["description"],
178 "preset": preset if custom_dpi is None else "custom",
179 }
182def apply_rendering_config(config: dict[str, Any]) -> None:
183 """Apply rendering configuration to matplotlib rcParams.
185 Args:
186 config: Configuration dictionary from configure_dpi_rendering().
188 Raises:
189 ImportError: If matplotlib is not available.
191 Example:
192 >>> config = configure_dpi_rendering("print")
193 >>> apply_rendering_config(config)
194 """
195 try:
196 import matplotlib.pyplot as plt
198 plt.rcParams.update(config["style_params"])
199 except ImportError:
200 raise ImportError("matplotlib is required for rendering configuration") # noqa: B904
203__all__ = [
204 "RenderPreset",
205 "apply_rendering_config",
206 "configure_dpi_rendering",
207]