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

1"""Visualization rendering functions for DPI-aware output. 

2 

3This module provides DPI-aware rendering configuration for adapting 

4plot quality and parameters based on target output device (screen vs print). 

5 

6 

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']) 

11 

12References: 

13 - matplotlib DPI scaling best practices 

14 - Print quality standards (300-600 DPI) 

15""" 

16 

17from __future__ import annotations 

18 

19from typing import Any, Literal 

20 

21RenderPreset = Literal["screen", "print", "publication"] 

22 

23 

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. 

33 

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. 

36 

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). 

43 

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 

54 

55 Raises: 

56 ValueError: If preset is invalid. 

57 

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']) 

62 

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 } 

90 

91 # Handle dpi alias 

92 if dpi is not None and custom_dpi is None: 

93 custom_dpi = dpi 

94 

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())}") 

97 

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] 

110 

111 # Calculate scale factors based on DPI 

112 # Scale factor = target_dpi / baseline_dpi 

113 scale = target_dpi / baseline_dpi 

114 

115 # Font size scaling: proportional to DPI 

116 # Baseline font sizes at 96 DPI: default 10pt 

117 font_scale = scale 

118 

119 # Line width scaling: proportional to DPI 

120 # Baseline line width at 96 DPI: 1.0 pt 

121 line_scale = scale 

122 

123 # Marker size scaling: proportional to DPI 

124 # Baseline marker size at 96 DPI: 6.0 pt 

125 marker_scale = scale 

126 

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 } 

148 

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 

159 

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 

167 

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 } 

180 

181 

182def apply_rendering_config(config: dict[str, Any]) -> None: 

183 """Apply rendering configuration to matplotlib rcParams. 

184 

185 Args: 

186 config: Configuration dictionary from configure_dpi_rendering(). 

187 

188 Raises: 

189 ImportError: If matplotlib is not available. 

190 

191 Example: 

192 >>> config = configure_dpi_rendering("print") 

193 >>> apply_rendering_config(config) 

194 """ 

195 try: 

196 import matplotlib.pyplot as plt 

197 

198 plt.rcParams.update(config["style_params"]) 

199 except ImportError: 

200 raise ImportError("matplotlib is required for rendering configuration") # noqa: B904 

201 

202 

203__all__ = [ 

204 "RenderPreset", 

205 "apply_rendering_config", 

206 "configure_dpi_rendering", 

207]