# matplotlib Template Library > **⚠️ Before writing any code, read [`_rules.md`](references/_rules.md) — three non-negotiable rules on overlap, hierarchy, and color.** ## Environment Initialization (Must Execute Before Each Plot) ```python import matplotlib import matplotlib.pyplot as plt import matplotlib.ticker as mticker import numpy as np # ═══ Chinese Font Setup ═══ # SimHei is the default; other fonts available: # SimSun.ttf (Songti, formal docs), SimKai.ttf (Kaiti, artistic), # SarasaMonoSC-*.ttf (monospace CJK, code scenes) # Run `fc-list :lang=zh` for system fonts (PingFang SC, Heiti TC, etc.) # Font path: adjust for your system. Common locations: # macOS: '/System/Library/Fonts/Supplemental/SimHei.ttf' # Linux: '/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc' # Custom: './fonts/SimHei.ttf' import os SIMHEI_PATH = os.environ.get('SIMHEI_FONT', '/System/Library/Fonts/Supplemental/SimHei.ttf') matplotlib.font_manager.fontManager.addfont(SIMHEI_PATH) # ═══ Global Style ═══ plt.rcParams.update({ # Font 'font.sans-serif': ['SimHei'], 'axes.unicode_minus': False, # Background 'figure.facecolor': '#FFFFFF', 'axes.facecolor': '#FFFFFF', # Border: only keep left and bottom 'axes.edgecolor': '#E5E7EB', 'axes.linewidth': 0.8, 'axes.spines.top': False, 'axes.spines.right': False, # Grid: off by default 'axes.grid': False, # Ticks: hide tick marks 'xtick.major.size': 0, 'ytick.major.size': 0, 'xtick.labelsize': 9, 'ytick.labelsize': 9, # Title 'axes.labelsize': 10, 'axes.titlesize': 16, 'axes.titleweight': 'bold', 'axes.titlepad': 16, # Legend: no frame 'legend.frameon': False, 'legend.fontsize': 9, # Export 'figure.dpi': 200, 'savefig.dpi': 200, 'savefig.bbox': 'tight', 'savefig.facecolor': '#FFFFFF', 'savefig.pad_inches': 0.3, }) ``` ## Color Constants ```python # ─── Cool Colors (Business/Tech) ─── C_BLUE = '#3B82F6' C_CYAN = '#06B6D4' C_PURPLE = '#8B5CF6' C_AMBER = '#F59E0B' C_RED = '#EF4444' C_GREEN = '#10B981' COOL = [C_BLUE, C_CYAN, C_PURPLE, C_AMBER, C_RED, C_GREEN] # ─── Warm Colors (Warmth/Creative) ─── WARM = ['#F59E0B', '#EF4444', '#8B5CF6', '#3B82F6', '#10B981', '#EC4899'] # ─── Academic Grayscale ─── ACADEMIC = ['#111827', '#6B7280', '#9CA3AF', '#D1D5DB', '#E5E7EB', '#F3F4F6'] # ─── Colorblind-Safe (Paper Preferred) ─── CB_SAFE = ['#0077BB', '#33BBEE', '#009988', '#EE7733', '#CC3311', '#EE3377'] # ─── Grayscale ─── G900, G700, G500, G400, G300, G200, G100, G50 = \ '#111827', '#374151', '#6B7280', '#9CA3AF', '#D1D5DB', '#E5E7EB', '#F3F4F6', '#F9FAFB' # ─── Gain/Loss ─── POS = '#22C55E' NEG = '#EF4444' ``` ## Helper Functions ```python def clean_axis(ax, grid=True): """Clean axis: remove top and right borders, add faint grid""" ax.spines['top'].set_visible(False) ax.spines['right'].set_visible(False) if grid: ax.yaxis.grid(True, alpha=0.08, color=G300) ax.set_axisbelow(True) def add_value_labels(ax, bars, values, highlight_idx=None, fmt='{:,.0f}', offset_ratio=0.02): """Add value labels on top of bars, highlight bar in bold. ⚠️ Automatically extends Y-axis upper limit to ensure labels don't overflow chart area.""" max_val = max(values) for i, (bar, val) in enumerate(zip(bars, values)): is_hl = (i == highlight_idx) if highlight_idx is not None else False ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + max_val * offset_ratio, fmt.format(val), ha='center', va='bottom', fontsize=10 if is_hl else 9, color=G900 if is_hl else G400, fontweight='bold' if is_hl else 'normal') # ⚠️ Critical: extend Y-axis upper limit to leave enough space for labels (at least 15%) ax.set_ylim(0, max_val * 1.18) def save(fig, path, dpi=200): """Unified save — prefer constrained_layout, fallback to tight_layout""" if not fig.get_constrained_layout(): try: fig.tight_layout() except Exception: pass fig.savefig(path, dpi=dpi, facecolor='white', bbox_inches='tight') plt.close(fig) import os size_kb = os.path.getsize(path) / 1024 print(f'✅ {path} ({size_kb:.0f}KB)') ``` ### Label Text Avoidance (adjustText) When charts have multiple label annotations (e.g., scatter plot labels, box plot annotations), **must use adjustText library** to prevent text overlap: ```python # pip install adjustText from adjustText import adjust_text # Call after adding all annotations texts = [] for i, (x, y, label) in enumerate(zip(x_data, y_data, labels)): texts.append(ax.text(x, y, label, fontsize=9, color='#374151')) # Auto-avoidance — pass all text objects, adjustText will move them to avoid overlap adjust_text(texts, ax=ax, arrowprops=dict(arrowstyle='->', color='#9CA3AF', lw=0.8), force_text=(0.5, 0.8), # Force to push text away force_points=(0.3, 0.5), # Repulsion from data points expand=(1.2, 1.4)) # Expansion factor for text bbox ``` --- ## Template 1: Insight Bar Chart **Scenario**: Emphasize outstanding performance of one data item. Gray out others, highlight the focus. ```python def insight_bar(labels, values, highlight_idx, title, highlight_color=C_BLUE, save_path='insight_bar.png'): fig, ax = plt.subplots(figsize=(10, 6)) colors = [G200] * len(labels) colors[highlight_idx] = highlight_color bars = ax.bar(labels, values, color=colors, width=0.6, zorder=3, edgecolor='white', linewidth=0.5) add_value_labels(ax, bars, values, highlight_idx) ax.set_title(title, loc='left') # ⚠️ add_value_labels already sets ylim automatically, no need to repeat here clean_axis(ax) save(fig, save_path) ``` --- ## Template 2: Trend Comparison Line Chart **Scenario**: This year vs last year, actual vs target. Main line in colored solid, comparison line in gray dashed. ```python def trend_compare(x, y_main, y_ref, label_main, label_ref, title, color=C_BLUE, save_path='trend_compare.png'): fig, ax = plt.subplots(figsize=(12, 6)) # Comparison line (gray dashed, at bottom layer) ax.plot(x, y_ref, color=G300, linewidth=1.5, linestyle='--', zorder=2) ax.text(len(x)-0.5, y_ref[-1], label_ref, color=G400, fontsize=9, va='center') # Main line (colored solid + white-center dots) ax.plot(x, y_main, color=color, linewidth=2.5, marker='o', markersize=5, markerfacecolor='white', markeredgewidth=2, markeredgecolor=color, zorder=3) ax.text(len(x)-0.5, y_main[-1], f'{label_main} {y_main[-1]:,.0f}', color=color, fontsize=10, fontweight='bold', va='center') # Difference area ax.fill_between(range(len(x)), y_ref, y_main, alpha=0.06, color=color) ax.set_title(title, loc='left') ax.set_xticks(range(len(x))) ax.set_xticklabels(x) clean_axis(ax) save(fig, save_path) ``` --- ## Template 3: Grouped Bar Chart **Scenario**: Comparison of multiple categories across multiple dimensions. ```python def grouped_bar(labels, datasets, series_names, title, colors=None, save_path='grouped_bar.png'): if colors is None: colors = COOL[:len(datasets)] fig, ax = plt.subplots(figsize=(12, 6)) n = len(datasets) width = 0.7 / n x = np.arange(len(labels)) for i, (data, name, color) in enumerate(zip(datasets, series_names, colors)): offset = (i - n/2 + 0.5) * width ax.bar(x + offset, data, width=width*0.85, color=color, label=name, zorder=3, edgecolor='white', linewidth=0.3) ax.set_title(title, loc='left') ax.set_xticks(x) ax.set_xticklabels(labels) ax.set_ylim(0, max(max(d) for d in datasets) * 1.20) # Leave 20% space for labels ax.legend(loc='upper right', ncol=n) clean_axis(ax) save(fig, save_path) ``` --- ## Template 4: Horizontal Ranking Chart **Scenario**: Rankings / Top N. Progressive highlight for top items. ```python def ranking_bar(labels, values, title, top_n=3, color=C_BLUE, save_path='ranking.png'): from matplotlib.colors import to_rgba sorted_pairs = sorted(zip(labels, values), key=lambda x: x[1]) labels_s, values_s = zip(*sorted_pairs) fig, ax = plt.subplots(figsize=(10, max(6, len(labels)*0.45))) bar_colors = [G200] * len(labels_s) for i in range(len(labels_s) - top_n, len(labels_s)): progress = (i - (len(labels_s) - top_n)) / max(top_n - 1, 1) bar_colors[i] = to_rgba(color, 0.35 + 0.65 * progress) bars = ax.barh(range(len(labels_s)), values_s, color=bar_colors, height=0.6, zorder=3, edgecolor='white', linewidth=0.3) for i, (bar, val) in enumerate(zip(bars, values_s)): is_top = i >= len(labels_s) - top_n ax.text(bar.get_width() + max(values_s)*0.01, bar.get_y() + bar.get_height()/2, f'{val:,.0f}', va='center', fontsize=9, color=G900 if is_top else G400) ax.set_yticks(range(len(labels_s))) ax.set_yticklabels(labels_s) ax.set_title(title, loc='left') ax.spines['bottom'].set_visible(False) ax.xaxis.set_visible(False) save(fig, save_path) ``` --- ## Template 5: Donut Chart **Scenario**: Proportion distribution (max 5 slices, avoid if possible — bar charts are usually better). ```python def donut(labels, values, title, center_text=None, colors=None, save_path='donut.png'): if colors is None: colors = COOL[:len(labels)] fig, ax = plt.subplots(figsize=(8, 8)) wedges, _, autotexts = ax.pie( values, labels=None, colors=colors, autopct='%1.0f%%', startangle=90, pctdistance=0.78, wedgeprops=dict(width=0.35, edgecolor='white', linewidth=2)) for t in autotexts: t.set_fontsize(10) t.set_fontweight('bold') if center_text: ax.text(0, 0.06, str(center_text), ha='center', va='center', fontsize=28, fontweight='bold', color=G900) ax.text(0, -0.1, '总计', ha='center', va='center', fontsize=11, color=G500) ax.legend(wedges, labels, loc='center left', bbox_to_anchor=(1, 0.5), fontsize=10) ax.set_title(title, loc='center', pad=20) save(fig, save_path) ``` --- ## Template 6: Scatter Plot + Trend Line **Scenario**: Two-variable correlation analysis. ```python def scatter_trend(x, y, title, xlabel, ylabel, color=C_BLUE, save_path='scatter.png'): fig, ax = plt.subplots(figsize=(10, 7)) ax.scatter(x, y, c=color, s=50, alpha=0.6, edgecolors='white', linewidth=1, zorder=3) # Trend line z = np.polyfit(x, y, 1) p = np.poly1d(z) x_line = np.linspace(min(x), max(x), 100) ax.plot(x_line, p(x_line), color=G400, linewidth=1.5, linestyle='--', zorder=2, alpha=0.7) ax.set_title(title, loc='left') ax.set_xlabel(xlabel) ax.set_ylabel(ylabel) clean_axis(ax, grid=True) ax.xaxis.grid(True, alpha=0.08, color=G300) save(fig, save_path) ``` --- ## Template 7: Heatmap **Scenario**: Matrix data, correlations, time × category. ```python def heatmap(data, row_labels, col_labels, title, cmap_color=C_BLUE, save_path='heatmap.png'): from matplotlib.colors import LinearSegmentedColormap cmap = LinearSegmentedColormap.from_list('bc', ['#FFFFFF', cmap_color]) fig, ax = plt.subplots( figsize=(max(8, len(col_labels)*1.2), max(6, len(row_labels)*0.6))) arr = np.array(data) im = ax.imshow(arr, cmap=cmap, aspect='auto') vmax = arr.max() for i in range(len(row_labels)): for j in range(len(col_labels)): val = arr[i][j] color = 'white' if val > vmax * 0.6 else G700 ax.text(j, i, f'{val:.1f}', ha='center', va='center', fontsize=9, color=color) ax.set_xticks(range(len(col_labels))) ax.set_yticks(range(len(row_labels))) ax.set_xticklabels(col_labels) ax.set_yticklabels(row_labels) ax.set_title(title, loc='left', pad=16) cbar = fig.colorbar(im, ax=ax, fraction=0.03, pad=0.08, shrink=0.8) cbar.outline.set_visible(False) cbar.ax.tick_params(labelsize=8) # colorbar ticks should not be too large for spine in ax.spines.values(): spine.set_visible(True) spine.set_color(G200) save(fig, save_path) ``` --- ## Template 8: KPI Metric Cards **Scenario**: Dashboard top key number display. ```python def kpi_cards(metrics, save_path='kpi.png'): """ metrics: [{'label': '总收入', 'value': '12.8M', 'change': '+23%', 'positive': True}, ...] """ from matplotlib.patches import FancyBboxPatch n = len(metrics) fig, axes = plt.subplots(1, n, figsize=(3.8*n, 2.8)) if n == 1: axes = [axes] for ax, m in zip(axes, metrics): ax.set_xlim(0, 1); ax.set_ylim(0, 1); ax.axis('off') bg = FancyBboxPatch((0.05, 0.05), 0.9, 0.9, boxstyle='round,pad=0.05', facecolor=G50, edgecolor=G200, linewidth=0.8) ax.add_patch(bg) ax.text(0.5, 0.75, m['label'], ha='center', va='center', fontsize=10, color=G500) ax.text(0.5, 0.45, m['value'], ha='center', va='center', fontsize=24, fontweight='bold', color=G900) if 'change' in m: is_pos = m.get('positive', True) ax.text(0.5, 0.18, f'{"↑" if is_pos else "↓"} {m["change"]}', ha='center', va='center', fontsize=11, color=POS if is_pos else NEG, fontweight='bold') save(fig, save_path) ``` --- ## Template 9: Radar Chart **Scenario**: Multi-dimensional capability comparison (max 8 dimensions, max 3 groups). ```python def radar(categories, datasets, series_names, title, colors=None, save_path='radar.png'): if colors is None: colors = COOL[:len(datasets)] N = len(categories) angles = np.linspace(0, 2*np.pi, N, endpoint=False).tolist() angles += angles[:1] # Close the polygon fig, ax = plt.subplots(figsize=(8, 8), subplot_kw=dict(polar=True)) ax.set_theta_offset(np.pi / 2) ax.set_theta_direction(-1) ax.set_xticks(angles[:-1]) ax.set_xticklabels(categories, fontsize=10) ax.yaxis.set_visible(False) # Grid beautification ax.spines['polar'].set_color(G200) ax.grid(color=G200, linewidth=0.5, alpha=0.5) for data, name, color in zip(datasets, series_names, colors): vals = data + data[:1] # Close the polygon ax.plot(angles, vals, color=color, linewidth=2, label=name) ax.fill(angles, vals, color=color, alpha=0.08) ax.legend(loc='upper right', bbox_to_anchor=(1.3, 1.1)) ax.set_title(title, pad=30, fontsize=16, fontweight='bold') save(fig, save_path) ``` --- ## Template 10: Waterfall Chart **Scenario**: Show incremental changes from start to end value. ```python def waterfall(labels, values, title, save_path='waterfall.png'): """labels and values correspond, positive=increase negative=decrease, last item auto-treated as total""" fig, ax = plt.subplots(figsize=(12, 6)) cumulative = [0] for v in values[:-1]: cumulative.append(cumulative[-1] + v) bar_colors = [] for i, v in enumerate(values): if i == len(values) - 1: bar_colors.append(C_BLUE) # Total elif v >= 0: bar_colors.append(POS) # Increase else: bar_colors.append(NEG) # Decrease bottoms = [] for i, v in enumerate(values): if i == len(values) - 1: bottoms.append(0) # Total starts from 0 elif v >= 0: bottoms.append(cumulative[i]) else: bottoms.append(cumulative[i] + v) bars = ax.bar(labels, [abs(v) for v in values], bottom=bottoms, color=bar_colors, width=0.6, edgecolor='white', linewidth=0.5, zorder=3) # Connecting lines for i in range(len(values) - 2): y = cumulative[i+1] ax.plot([i+0.3, i+0.7], [y, y], color=G300, linewidth=0.8, zorder=2) # Value labels for i, (bar, val) in enumerate(zip(bars, values)): y_pos = bar.get_y() + bar.get_height() + max(abs(v) for v in values)*0.01 prefix = '+' if val > 0 and i < len(values)-1 else '' ax.text(bar.get_x()+bar.get_width()/2, y_pos, f'{prefix}{val:,.0f}', ha='center', va='bottom', fontsize=9, color=G700) ax.set_title(title, loc='left') clean_axis(ax) save(fig, save_path) ``` --- ## Template 11: Multi-Subplot Dashboard (GridSpec Precision Layout) **Scenario**: Combine multiple subplots into a dashboard. ⚠️ Max 4 subplots per canvas, split if exceeded. **Core Principle**: Use `GridSpec` for precise control of each subplot's position and spacing, don't rely on `tight_layout()`. ```python import matplotlib.gridspec as gridspec def dashboard(data_dict, title, save_path='dashboard.png'): """ 2x2 dashboard layout example. data_dict contains data needed for each subplot. """ # ⚠️ Use constrained_layout instead of tight_layout fig = plt.figure(figsize=(16, 12), constrained_layout=True) fig.suptitle(title, fontsize=20, fontweight='bold', y=0.98) # GridSpec: precise spacing control gs = gridspec.GridSpec(2, 2, figure=fig, wspace=0.35, # Column spacing (at least 0.3) hspace=0.35, # Row spacing (at least 0.3) left=0.08, right=0.92, top=0.92, bottom=0.08) # ─── Top-left: bar chart ─── ax1 = fig.add_subplot(gs[0, 0]) ax1.set_title('Quarterly Revenue', loc='left', fontsize=13, fontweight='bold') # ... binddata ... clean_axis(ax1) # ─── Top-right: line chart ─── ax2 = fig.add_subplot(gs[0, 1]) ax2.set_title('Monthly Trend', loc='left', fontsize=13, fontweight='bold') # ... bind data ... clean_axis(ax2) # ─── Bottom-left: pie chart ─── ax3 = fig.add_subplot(gs[1, 0]) ax3.set_title('Category Share', loc='left', fontsize=13, fontweight='bold') # ... bind data ... # ─── Bottom-right: scatter plot ─── ax4 = fig.add_subplot(gs[1, 1]) ax4.set_title('Conversion Analysis', loc='left', fontsize=13, fontweight='bold') # ... bind data ... clean_axis(ax4) fig.savefig(save_path, dpi=200, facecolor='white', bbox_inches='tight') plt.close(fig) ``` ### Dashboard Layout Golden Rules | Rule | Description | |------|-------------| | **Max 4 subplots** | Split into multiple charts if exceeded | | **Use `constrained_layout=True`** | Smarter than `tight_layout()`, auto-avoids labels | | **`wspace/hspace ≥ 0.3`** | Subplot spacing too small causes overlap | | **Independent title per subplot** | Use `ax.set_title()` instead of `fig.suptitle()` | | **Consistent font sizes** | Subtitle 13px, axis labels 10px, data labels 9px | | **Colorbar in separate column** | If a subplot needs colorbar, allocate `gs[0, 2]` separately | | **Don't mix legend and direct labels** | Use either all legends or all direct labels in a dashboard | ### Safe Layout with Colorbar ```python # When a subplot needs a colorbar, use 3-column layout, rightmost column for colorbar gs = gridspec.GridSpec(2, 3, figure=fig, width_ratios=[1, 1, 0.05], # Third column very narrow, dedicated to colorbar wspace=0.4, hspace=0.35) ax_heat = fig.add_subplot(gs[0, 1]) im = ax_heat.imshow(data, cmap='Blues') # Colorbar placed in its own subplot position, won't obscure any content cbar_ax = fig.add_subplot(gs[0, 2]) fig.colorbar(im, cax=cbar_ax) cbar_ax.set_ylabel('Value', fontsize=10) ``` ### Split Strategy for More Than 4 Subplots ```python # ❌ Wrong: 8 subplots crammed into one canvas fig, axes = plt.subplots(2, 4, figsize=(20, 10)) # Everything becomes unreadable # ✅ Correct: split into 2 figures, 4 subplots each # Figure 1: overview metrics fig1 = plt.figure(figsize=(16, 12), constrained_layout=True) # ... 4 subplots ... fig1.savefig('dashboard_overview.png', dpi=200) # Figure 2: detailed analysis fig2 = plt.figure(figsize=(16, 12), constrained_layout=True) # ... 4 subplots ... fig2.savefig('dashboard_detail.png', dpi=200) ```