style_configurator.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. #!/usr/bin/env python3
  2. """
  3. Matplotlib Style Configurator
  4. Interactive utility to configure matplotlib style preferences and generate
  5. custom style sheets. Creates a preview of the style and optionally saves
  6. it as a .mplstyle file.
  7. Usage:
  8. python style_configurator.py [--preset PRESET] [--output FILE] [--preview]
  9. Presets:
  10. publication, presentation, web, dark, minimal
  11. """
  12. import numpy as np
  13. import matplotlib.pyplot as plt
  14. from matplotlib.gridspec import GridSpec
  15. import argparse
  16. import os
  17. # Predefined style presets
  18. STYLE_PRESETS = {
  19. 'publication': {
  20. 'figure.figsize': (8, 6),
  21. 'figure.dpi': 100,
  22. 'savefig.dpi': 300,
  23. 'savefig.bbox': 'tight',
  24. 'font.family': 'sans-serif',
  25. 'font.sans-serif': ['Arial', 'Helvetica'],
  26. 'font.size': 11,
  27. 'axes.labelsize': 12,
  28. 'axes.titlesize': 14,
  29. 'axes.linewidth': 1.5,
  30. 'axes.grid': False,
  31. 'axes.spines.top': False,
  32. 'axes.spines.right': False,
  33. 'lines.linewidth': 2,
  34. 'lines.markersize': 8,
  35. 'xtick.labelsize': 10,
  36. 'ytick.labelsize': 10,
  37. 'xtick.direction': 'in',
  38. 'ytick.direction': 'in',
  39. 'xtick.major.size': 6,
  40. 'ytick.major.size': 6,
  41. 'xtick.major.width': 1.5,
  42. 'ytick.major.width': 1.5,
  43. 'legend.fontsize': 10,
  44. 'legend.frameon': True,
  45. 'legend.framealpha': 1.0,
  46. 'legend.edgecolor': 'black',
  47. },
  48. 'presentation': {
  49. 'figure.figsize': (12, 8),
  50. 'figure.dpi': 100,
  51. 'savefig.dpi': 150,
  52. 'font.size': 16,
  53. 'axes.labelsize': 20,
  54. 'axes.titlesize': 24,
  55. 'axes.linewidth': 2,
  56. 'lines.linewidth': 3,
  57. 'lines.markersize': 12,
  58. 'xtick.labelsize': 16,
  59. 'ytick.labelsize': 16,
  60. 'legend.fontsize': 16,
  61. 'axes.grid': True,
  62. 'grid.alpha': 0.3,
  63. },
  64. 'web': {
  65. 'figure.figsize': (10, 6),
  66. 'figure.dpi': 96,
  67. 'savefig.dpi': 150,
  68. 'font.size': 11,
  69. 'axes.labelsize': 12,
  70. 'axes.titlesize': 14,
  71. 'lines.linewidth': 2,
  72. 'axes.grid': True,
  73. 'grid.alpha': 0.2,
  74. 'grid.linestyle': '--',
  75. },
  76. 'dark': {
  77. 'figure.facecolor': '#1e1e1e',
  78. 'figure.edgecolor': '#1e1e1e',
  79. 'axes.facecolor': '#1e1e1e',
  80. 'axes.edgecolor': 'white',
  81. 'axes.labelcolor': 'white',
  82. 'text.color': 'white',
  83. 'xtick.color': 'white',
  84. 'ytick.color': 'white',
  85. 'grid.color': 'gray',
  86. 'grid.alpha': 0.3,
  87. 'axes.grid': True,
  88. 'legend.facecolor': '#1e1e1e',
  89. 'legend.edgecolor': 'white',
  90. 'savefig.facecolor': '#1e1e1e',
  91. },
  92. 'minimal': {
  93. 'figure.figsize': (10, 6),
  94. 'axes.spines.top': False,
  95. 'axes.spines.right': False,
  96. 'axes.spines.left': False,
  97. 'axes.spines.bottom': False,
  98. 'axes.grid': False,
  99. 'xtick.bottom': True,
  100. 'ytick.left': True,
  101. 'axes.axisbelow': True,
  102. 'lines.linewidth': 2.5,
  103. 'font.size': 12,
  104. }
  105. }
  106. def generate_preview_data():
  107. """Generate sample data for style preview."""
  108. np.random.seed(42)
  109. x = np.linspace(0, 10, 100)
  110. y1 = np.sin(x) + 0.1 * np.random.randn(100)
  111. y2 = np.cos(x) + 0.1 * np.random.randn(100)
  112. scatter_x = np.random.randn(100)
  113. scatter_y = 2 * scatter_x + np.random.randn(100)
  114. categories = ['A', 'B', 'C', 'D', 'E']
  115. bar_values = [25, 40, 30, 55, 45]
  116. return {
  117. 'x': x, 'y1': y1, 'y2': y2,
  118. 'scatter_x': scatter_x, 'scatter_y': scatter_y,
  119. 'categories': categories, 'bar_values': bar_values
  120. }
  121. def create_style_preview(style_dict=None):
  122. """Create a preview figure demonstrating the style."""
  123. if style_dict:
  124. plt.rcParams.update(style_dict)
  125. data = generate_preview_data()
  126. fig = plt.figure(figsize=(14, 10))
  127. gs = GridSpec(2, 2, figure=fig, hspace=0.3, wspace=0.3)
  128. # Line plot
  129. ax1 = fig.add_subplot(gs[0, 0])
  130. ax1.plot(data['x'], data['y1'], label='sin(x)', marker='o', markevery=10)
  131. ax1.plot(data['x'], data['y2'], label='cos(x)', linestyle='--')
  132. ax1.set_xlabel('X axis')
  133. ax1.set_ylabel('Y axis')
  134. ax1.set_title('Line Plot')
  135. ax1.legend()
  136. ax1.grid(True, alpha=0.3)
  137. # Scatter plot
  138. ax2 = fig.add_subplot(gs[0, 1])
  139. colors = np.sqrt(data['scatter_x']**2 + data['scatter_y']**2)
  140. scatter = ax2.scatter(data['scatter_x'], data['scatter_y'],
  141. c=colors, cmap='viridis', alpha=0.6, s=50)
  142. ax2.set_xlabel('X axis')
  143. ax2.set_ylabel('Y axis')
  144. ax2.set_title('Scatter Plot')
  145. cbar = plt.colorbar(scatter, ax=ax2)
  146. cbar.set_label('Distance')
  147. ax2.grid(True, alpha=0.3)
  148. # Bar chart
  149. ax3 = fig.add_subplot(gs[1, 0])
  150. bars = ax3.bar(data['categories'], data['bar_values'],
  151. edgecolor='black', linewidth=1)
  152. # Color bars with gradient
  153. colors = plt.cm.viridis(np.linspace(0.2, 0.8, len(bars)))
  154. for bar, color in zip(bars, colors):
  155. bar.set_facecolor(color)
  156. ax3.set_xlabel('Categories')
  157. ax3.set_ylabel('Values')
  158. ax3.set_title('Bar Chart')
  159. ax3.grid(True, axis='y', alpha=0.3)
  160. # Multiple line plot with fills
  161. ax4 = fig.add_subplot(gs[1, 1])
  162. ax4.plot(data['x'], data['y1'], label='Signal 1', linewidth=2)
  163. ax4.fill_between(data['x'], data['y1'] - 0.2, data['y1'] + 0.2,
  164. alpha=0.3, label='±1 std')
  165. ax4.plot(data['x'], data['y2'], label='Signal 2', linewidth=2)
  166. ax4.fill_between(data['x'], data['y2'] - 0.2, data['y2'] + 0.2,
  167. alpha=0.3)
  168. ax4.set_xlabel('X axis')
  169. ax4.set_ylabel('Y axis')
  170. ax4.set_title('Time Series with Uncertainty')
  171. ax4.legend()
  172. ax4.grid(True, alpha=0.3)
  173. fig.suptitle('Style Preview', fontsize=16, fontweight='bold')
  174. return fig
  175. def save_style_file(style_dict, filename):
  176. """Save style dictionary as .mplstyle file."""
  177. with open(filename, 'w') as f:
  178. f.write("# Custom matplotlib style\n")
  179. f.write("# Generated by style_configurator.py\n\n")
  180. # Group settings by category
  181. categories = {
  182. 'Figure': ['figure.'],
  183. 'Font': ['font.'],
  184. 'Axes': ['axes.'],
  185. 'Lines': ['lines.'],
  186. 'Markers': ['markers.'],
  187. 'Ticks': ['tick.', 'xtick.', 'ytick.'],
  188. 'Grid': ['grid.'],
  189. 'Legend': ['legend.'],
  190. 'Savefig': ['savefig.'],
  191. 'Text': ['text.'],
  192. }
  193. for category, prefixes in categories.items():
  194. category_items = {k: v for k, v in style_dict.items()
  195. if any(k.startswith(p) for p in prefixes)}
  196. if category_items:
  197. f.write(f"# {category}\n")
  198. for key, value in sorted(category_items.items()):
  199. # Format value appropriately
  200. if isinstance(value, (list, tuple)):
  201. value_str = ', '.join(str(v) for v in value)
  202. elif isinstance(value, bool):
  203. value_str = str(value)
  204. else:
  205. value_str = str(value)
  206. f.write(f"{key}: {value_str}\n")
  207. f.write("\n")
  208. print(f"Style saved to {filename}")
  209. def print_style_info(style_dict):
  210. """Print information about the style."""
  211. print("\n" + "="*60)
  212. print("STYLE CONFIGURATION")
  213. print("="*60)
  214. categories = {
  215. 'Figure Settings': ['figure.'],
  216. 'Font Settings': ['font.'],
  217. 'Axes Settings': ['axes.'],
  218. 'Line Settings': ['lines.'],
  219. 'Grid Settings': ['grid.'],
  220. 'Legend Settings': ['legend.'],
  221. }
  222. for category, prefixes in categories.items():
  223. category_items = {k: v for k, v in style_dict.items()
  224. if any(k.startswith(p) for p in prefixes)}
  225. if category_items:
  226. print(f"\n{category}:")
  227. for key, value in sorted(category_items.items()):
  228. print(f" {key}: {value}")
  229. print("\n" + "="*60 + "\n")
  230. def list_available_presets():
  231. """Print available style presets."""
  232. print("\nAvailable style presets:")
  233. print("-" * 40)
  234. descriptions = {
  235. 'publication': 'Optimized for academic publications',
  236. 'presentation': 'Large fonts for presentations',
  237. 'web': 'Optimized for web display',
  238. 'dark': 'Dark background theme',
  239. 'minimal': 'Minimal, clean style',
  240. }
  241. for preset, desc in descriptions.items():
  242. print(f" {preset:15s} - {desc}")
  243. print("-" * 40 + "\n")
  244. def interactive_mode():
  245. """Run interactive mode to customize style settings."""
  246. print("\n" + "="*60)
  247. print("MATPLOTLIB STYLE CONFIGURATOR - Interactive Mode")
  248. print("="*60)
  249. list_available_presets()
  250. preset = input("Choose a preset to start from (or 'custom' for default): ").strip().lower()
  251. if preset in STYLE_PRESETS:
  252. style_dict = STYLE_PRESETS[preset].copy()
  253. print(f"\nStarting from '{preset}' preset")
  254. else:
  255. style_dict = {}
  256. print("\nStarting from default matplotlib style")
  257. print("\nCommon settings you might want to customize:")
  258. print(" 1. Figure size")
  259. print(" 2. Font sizes")
  260. print(" 3. Line widths")
  261. print(" 4. Grid settings")
  262. print(" 5. Color scheme")
  263. print(" 6. Done, show preview")
  264. while True:
  265. choice = input("\nSelect option (1-6): ").strip()
  266. if choice == '1':
  267. width = input(" Figure width (inches, default 10): ").strip() or '10'
  268. height = input(" Figure height (inches, default 6): ").strip() or '6'
  269. style_dict['figure.figsize'] = (float(width), float(height))
  270. elif choice == '2':
  271. base = input(" Base font size (default 12): ").strip() or '12'
  272. style_dict['font.size'] = float(base)
  273. style_dict['axes.labelsize'] = float(base) + 2
  274. style_dict['axes.titlesize'] = float(base) + 4
  275. elif choice == '3':
  276. lw = input(" Line width (default 2): ").strip() or '2'
  277. style_dict['lines.linewidth'] = float(lw)
  278. elif choice == '4':
  279. grid = input(" Enable grid? (y/n): ").strip().lower()
  280. style_dict['axes.grid'] = grid == 'y'
  281. if style_dict['axes.grid']:
  282. alpha = input(" Grid transparency (0-1, default 0.3): ").strip() or '0.3'
  283. style_dict['grid.alpha'] = float(alpha)
  284. elif choice == '5':
  285. print(" Theme options: 1=Light, 2=Dark")
  286. theme = input(" Select theme (1-2): ").strip()
  287. if theme == '2':
  288. style_dict.update(STYLE_PRESETS['dark'])
  289. elif choice == '6':
  290. break
  291. return style_dict
  292. def main():
  293. """Main function."""
  294. parser = argparse.ArgumentParser(
  295. description='Matplotlib style configurator',
  296. formatter_class=argparse.RawDescriptionHelpFormatter,
  297. epilog="""
  298. Examples:
  299. # Show available presets
  300. python style_configurator.py --list
  301. # Preview a preset
  302. python style_configurator.py --preset publication --preview
  303. # Save a preset as .mplstyle file
  304. python style_configurator.py --preset publication --output my_style.mplstyle
  305. # Interactive mode
  306. python style_configurator.py --interactive
  307. """
  308. )
  309. parser.add_argument('--preset', type=str, choices=list(STYLE_PRESETS.keys()),
  310. help='Use a predefined style preset')
  311. parser.add_argument('--output', type=str,
  312. help='Save style to .mplstyle file')
  313. parser.add_argument('--preview', action='store_true',
  314. help='Show style preview')
  315. parser.add_argument('--list', action='store_true',
  316. help='List available presets')
  317. parser.add_argument('--interactive', action='store_true',
  318. help='Run in interactive mode')
  319. args = parser.parse_args()
  320. if args.list:
  321. list_available_presets()
  322. # Also show currently available matplotlib styles
  323. print("\nBuilt-in matplotlib styles:")
  324. print("-" * 40)
  325. for style in sorted(plt.style.available):
  326. print(f" {style}")
  327. return
  328. if args.interactive:
  329. style_dict = interactive_mode()
  330. elif args.preset:
  331. style_dict = STYLE_PRESETS[args.preset].copy()
  332. print(f"Using '{args.preset}' preset")
  333. else:
  334. print("No preset or interactive mode specified. Showing default preview.")
  335. style_dict = {}
  336. if style_dict:
  337. print_style_info(style_dict)
  338. if args.output:
  339. save_style_file(style_dict, args.output)
  340. if args.preview or args.interactive:
  341. print("Creating style preview...")
  342. fig = create_style_preview(style_dict if style_dict else None)
  343. if args.output:
  344. preview_filename = args.output.replace('.mplstyle', '_preview.png')
  345. plt.savefig(preview_filename, dpi=150, bbox_inches='tight')
  346. print(f"Preview saved to {preview_filename}")
  347. plt.show()
  348. if __name__ == "__main__":
  349. main()