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

1"""Plot style presets for different output contexts. 

2 

3This module provides comprehensive style presets for publication-quality, 

4presentation, screen viewing, and print output. 

5 

6 

7Example: 

8 >>> from tracekit.visualization.styles import apply_style_preset 

9 >>> with apply_style_preset("publication"): 

10 ... plot_waveform(signal) 

11 

12References: 

13 matplotlib rcParams customization 

14 Publication and presentation best practices 

15""" 

16 

17from __future__ import annotations 

18 

19from contextlib import contextmanager 

20from dataclasses import dataclass, field 

21from typing import TYPE_CHECKING, Any 

22 

23if TYPE_CHECKING: 

24 from collections.abc import Iterator 

25 

26try: 

27 import matplotlib.pyplot as plt 

28 

29 HAS_MATPLOTLIB = True 

30except ImportError: 

31 HAS_MATPLOTLIB = False 

32 

33 

34@dataclass 

35class StylePreset: 

36 """Style preset configuration for plots. 

37 

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 """ 

55 

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) 

71 

72 

73# Predefined style presets 

74 

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) 

103 

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) 

129 

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) 

151 

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) 

177 

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} 

185 

186 

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. 

194 

195 : Provide comprehensive style presets for common use cases 

196 with support for custom overrides. 

197 

198 Args: 

199 preset: Preset name or StylePreset object 

200 overrides: Dictionary of rcParams to override 

201 

202 Yields: 

203 None (use as context manager) 

204 

205 Raises: 

206 ValueError: If preset name is unknown 

207 ImportError: If matplotlib is not available 

208 

209 Example: 

210 >>> with apply_style_preset("publication"): 

211 ... fig, ax = plt.subplots() 

212 ... ax.plot(x, y) 

213 ... plt.savefig("figure.pdf") 

214 

215 >>> # With overrides 

216 >>> with apply_style_preset("screen", overrides={"font.size": 14}): 

217 ... plot_waveform(signal) 

218 

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

225 

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 

233 

234 # Build rcParams dictionary 

235 rc_dict = _preset_to_rcparams(preset_obj) 

236 

237 # Apply overrides 

238 if overrides: 

239 rc_dict.update(overrides) 

240 

241 # Apply as context 

242 with plt.rc_context(rc_dict): 

243 yield 

244 

245 

246def _preset_to_rcparams(preset: StylePreset) -> dict[str, Any]: 

247 """Convert StylePreset to matplotlib rcParams dictionary. 

248 

249 Args: 

250 preset: StylePreset object 

251 

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 } 

269 

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 

273 

274 # Merge with additional rcparams 

275 rc.update(preset.rcparams) 

276 

277 return rc 

278 

279 

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. 

286 

287 : Support custom presets with inheritance and override. 

288 

289 Args: 

290 name: Name for custom preset 

291 base_preset: Base preset to inherit from 

292 **kwargs: Attributes to override 

293 

294 Returns: 

295 Custom StylePreset object 

296 

297 Raises: 

298 ValueError: If base_preset is unknown 

299 

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

309 

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

315 

316 # Get base preset 

317 base = PRESETS[base_preset] 

318 

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 } 

337 

338 return StylePreset(**preset_dict) 

339 

340 

341def register_preset(preset: StylePreset) -> None: 

342 """Register custom preset in global registry. 

343 

344 Args: 

345 preset: StylePreset to register 

346 

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 

354 

355 

356def list_presets() -> list[str]: 

357 """Get list of available preset names. 

358 

359 Returns: 

360 List of preset names 

361 

362 Example: 

363 >>> presets = list_presets() 

364 >>> print(presets) 

365 ['publication', 'presentation', 'screen', 'print'] 

366 """ 

367 return list(PRESETS.keys()) 

368 

369 

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]