#!/usr/bin/env python3 """ Create a beautiful grayscale visualization of benchmark results Minimal aesthetic matching the site design """ import json import matplotlib.pyplot as plt import matplotlib as mpl import numpy as np # Configure matplotlib for beautiful output # Try to use Noto Sans Mono if available try: import matplotlib.font_manager as fm # Find Noto Sans Mono fonts = fm.findSystemFonts(fontpaths=None, fontext='ttf') noto_fonts = [f for f in fonts if 'NotoSansMono' in f or 'Noto Sans Mono' in f] if noto_fonts: fm.fontManager.addfont(noto_fonts[0]) mpl.rcParams['font.family'] = 'Noto Sans Mono' else: mpl.rcParams['font.family'] = 'monospace' except: mpl.rcParams['font.family'] = 'monospace' mpl.rcParams['font.size'] = 10 mpl.rcParams['axes.linewidth'] = 0.5 mpl.rcParams['xtick.major.width'] = 0.5 mpl.rcParams['ytick.major.width'] = 0.5 def load_summary(): """Load the summary data""" with open('summary.json', 'r') as f: return json.load(f) def create_visualization(): """Create the benchmark visualization""" data = load_summary() # Extract average relative performance relative = data['relative_performance'] # Calculate averages across Python versions operations = ['creation_per_object', 'modification_per_operation', 'serialization_per_object'] operation_labels = ['creation', 'modification', 'serialization'] # Prepare data dataclass_perf = [1.0, 1.0, 1.0] # baseline pydantic_perf = [] sqlalchemy_perf = [] for op in operations: pyd_avg = np.mean([relative[v][op]['pydantic'] for v in relative]) sql_avg = np.mean([relative[v][op]['sqlalchemy'] for v in relative]) pydantic_perf.append(pyd_avg) sqlalchemy_perf.append(sql_avg) # Create figure with more space for better layout fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8), gridspec_kw={'height_ratios': [3, 2], 'hspace': 0.5}) # Main comparison: dataclasses vs pydantic x = np.arange(len(operations)) width = 0.35 # Colors: light gray for baseline, dark gray for pydantic color_dataclass = '#e0e0e0' color_pydantic = '#404040' color_sqlalchemy = '#000000' # Top plot: Direct comparison bars1 = ax1.barh(x - width/2, dataclass_perf, width, label='dataclasses', color=color_dataclass, edgecolor='black', linewidth=0.5) bars2 = ax1.barh(x + width/2, pydantic_perf, width, label='pydantic', color=color_pydantic, edgecolor='black', linewidth=0.5) # Highlight the serialization advantage ax1.annotate('3x faster!', xy=(0.3, 2 + width/2), xytext=(0.15, 2.6), arrowprops=dict(arrowstyle='->', color='black', lw=0.5), fontsize=9, ha='center') ax1.set_xlabel('relative performance (lower is faster)', fontsize=10) ax1.set_yticks(x) ax1.set_yticklabels(operation_labels) ax1.set_xlim(0, 2) ax1.legend(loc='lower right', frameon=False, fontsize=9) ax1.set_title('dataclasses vs pydantic: comparable performance', fontsize=11, pad=10) # Remove top and right spines ax1.spines['top'].set_visible(False) ax1.spines['right'].set_visible(False) # Add vertical line at 1.0 for reference ax1.axvline(x=1.0, color='gray', linestyle=':', linewidth=0.5, alpha=0.5) # Bottom plot: Properly grouped horizontal bars # Create positions for grouped bars bar_height = 0.22 # Height of each bar group_gap = 0.6 # Gap between operation groups within_group_gap = 0.03 # Small gap between bars in same group # Calculate y positions for each bar y_positions = [] y_labels = [] y_label_positions = [] # Reverse the order to match top pane (serialization, modification, creation from top to bottom) reversed_labels = list(reversed(operation_labels)) for i, op_label in enumerate(reversed_labels): # Start position for this group group_start = i * (3 * bar_height + 2 * within_group_gap + group_gap) # Positions for the three bars in this group (top to bottom: dataclasses, pydantic, sqlalchemy) y_positions.extend([ group_start + 0 * (bar_height + within_group_gap), # dataclasses group_start + 1 * (bar_height + within_group_gap), # pydantic group_start + 2 * (bar_height + within_group_gap), # sqlalchemy ]) # Label position at center of group y_label_positions.append(group_start + bar_height + within_group_gap) y_labels.append(op_label) # Plot bars for each library (reversing data order to match reversed labels) reversed_dataclass = list(reversed(dataclass_perf)) reversed_pydantic = list(reversed(pydantic_perf)) reversed_sqlalchemy = list(reversed(sqlalchemy_perf)) for i in range(len(operations)): base_idx = i * 3 ax2.barh(y_positions[base_idx], reversed_dataclass[i], bar_height, color=color_dataclass, edgecolor='black', linewidth=0.5) ax2.barh(y_positions[base_idx + 1], reversed_pydantic[i], bar_height, color=color_pydantic, edgecolor='black', linewidth=0.5) ax2.barh(y_positions[base_idx + 2], reversed_sqlalchemy[i], bar_height, color=color_sqlalchemy, edgecolor='black', linewidth=0.5) ax2.set_xlabel('relative performance - log scale (lower is faster)', fontsize=10) ax2.set_yticks(y_label_positions) ax2.set_yticklabels(y_labels) ax2.set_ylim(-0.5, max(y_positions) + 0.5) ax2.set_xscale('log') ax2.set_xlim(0.1, 200) ax2.set_title('sqlalchemy: different purpose, different scale', fontsize=11, pad=10) # Add text annotation for SQLAlchemy (position it near the bottom creation sqlalchemy bar) ax2.text(150, max(y_positions) - 0.3, 'includes database\noperations', fontsize=8, ha='right', va='bottom', style='italic') # Remove top and right spines ax2.spines['top'].set_visible(False) ax2.spines['right'].set_visible(False) # Add legend in bottom right for consistency ax2.legend(['dataclasses', 'pydantic', 'sqlalchemy'], loc='lower right', frameon=False, fontsize=9) # Overall title fig.suptitle('python data model performance', fontsize=12, y=0.98) # Add note at bottom fig.text(0.5, 0.02, 'benchmarked across python 3.10-3.14 with 10,000 iterations', ha='center', fontsize=8, style='italic', color='#666666') # Save with high quality and transparent background plt.savefig('benchmark_visualization.png', dpi=150, bbox_inches='tight', facecolor='none', transparent=True, edgecolor='none') plt.savefig('benchmark_visualization.svg', format='svg', bbox_inches='tight', facecolor='none', transparent=True, edgecolor='none') print("Visualization saved as benchmark_visualization.png and .svg") if __name__ == "__main__": create_visualization()