|
| 1 | +""" pyplots.ai |
| 2 | +line-interactive: Interactive Line Chart with Hover and Zoom |
| 3 | +Library: matplotlib 3.10.8 | Python 3.13.11 |
| 4 | +Quality: 88/100 | Created: 2025-12-30 |
| 5 | +""" |
| 6 | + |
| 7 | +import matplotlib.pyplot as plt |
| 8 | +import mplcursors |
| 9 | +import numpy as np |
| 10 | +import pandas as pd |
| 11 | + |
| 12 | + |
| 13 | +# Data - Server response time metrics (realistic monitoring scenario) |
| 14 | +np.random.seed(42) |
| 15 | +n_points = 200 |
| 16 | + |
| 17 | +# Generate datetime index for one week of hourly data |
| 18 | +dates = pd.date_range("2024-01-01", periods=n_points, freq="h") |
| 19 | + |
| 20 | +# Generate realistic server response times with patterns |
| 21 | +base_response = 120 # Base response time in ms |
| 22 | +daily_pattern = 30 * np.sin(2 * np.pi * np.arange(n_points) / 24) # Daily cycle |
| 23 | +weekly_pattern = 15 * np.sin(2 * np.pi * np.arange(n_points) / 168) # Weekly cycle |
| 24 | +noise = np.random.normal(0, 10, n_points) |
| 25 | +trend = np.linspace(0, 20, n_points) # Slight upward trend |
| 26 | + |
| 27 | +# Add some spike anomalies |
| 28 | +response_times = base_response + daily_pattern + weekly_pattern + noise + trend |
| 29 | +spike_indices = [45, 120, 175] |
| 30 | +for idx in spike_indices: |
| 31 | + response_times[idx] += np.random.uniform(50, 100) |
| 32 | + |
| 33 | +# Create the figure with interactive backend capabilities |
| 34 | +fig, ax = plt.subplots(figsize=(16, 9)) |
| 35 | + |
| 36 | +# Main line plot |
| 37 | +(line,) = ax.plot(dates, response_times, color="#306998", linewidth=2.5, label="Response Time", zorder=2) |
| 38 | + |
| 39 | +# Add scatter points for hover targets (every 5th point for better interactivity) |
| 40 | +scatter = ax.scatter( |
| 41 | + dates[::5], |
| 42 | + response_times[::5], |
| 43 | + color="#306998", |
| 44 | + s=100, |
| 45 | + alpha=0.8, |
| 46 | + edgecolors="white", |
| 47 | + linewidths=1.5, |
| 48 | + zorder=3, |
| 49 | + label="Data Points", |
| 50 | +) |
| 51 | + |
| 52 | +# Setup mplcursors for interactive hover tooltips on scatter points |
| 53 | +cursor = mplcursors.cursor(scatter, hover=True) |
| 54 | + |
| 55 | + |
| 56 | +@cursor.connect("add") |
| 57 | +def on_add(sel): |
| 58 | + """Format hover tooltip with datetime and value.""" |
| 59 | + idx = sel.index * 5 # Map back to original index |
| 60 | + date_str = dates[idx].strftime("%Y-%m-%d %H:%M") |
| 61 | + val = response_times[idx] |
| 62 | + sel.annotation.set_text(f"Time: {date_str}\nResponse: {val:.1f} ms") |
| 63 | + sel.annotation.get_bbox_patch().set(facecolor="#FFD43B", alpha=0.95) |
| 64 | + sel.annotation.set_fontsize(14) |
| 65 | + sel.annotation.set_fontweight("bold") |
| 66 | + |
| 67 | + |
| 68 | +# Highlight anomaly spikes with visible annotations |
| 69 | +for i, idx in enumerate(spike_indices): |
| 70 | + ax.scatter( |
| 71 | + dates[idx], |
| 72 | + response_times[idx], |
| 73 | + color="#E63946", |
| 74 | + s=250, |
| 75 | + edgecolors="#306998", |
| 76 | + linewidths=2.5, |
| 77 | + zorder=5, |
| 78 | + marker="^", |
| 79 | + ) |
| 80 | + offset_y = 20 if i % 2 == 0 else -35 |
| 81 | + ax.annotate( |
| 82 | + f"Anomaly: {response_times[idx]:.0f} ms", |
| 83 | + xy=(dates[idx], response_times[idx]), |
| 84 | + xytext=(0, offset_y), |
| 85 | + textcoords="offset points", |
| 86 | + fontsize=13, |
| 87 | + fontweight="bold", |
| 88 | + color="white", |
| 89 | + ha="center", |
| 90 | + bbox=dict(boxstyle="round,pad=0.4", facecolor="#E63946", alpha=0.95, edgecolor="#306998", linewidth=1.5), |
| 91 | + arrowprops=dict(arrowstyle="-", color="#E63946", lw=2), |
| 92 | + ) |
| 93 | + |
| 94 | +# Add a reference line for average response time |
| 95 | +avg_response = np.mean(response_times) |
| 96 | +ax.axhline( |
| 97 | + y=avg_response, color="#808080", linestyle="--", linewidth=2, alpha=0.7, label=f"Average: {avg_response:.0f} ms" |
| 98 | +) |
| 99 | + |
| 100 | +# Highlight zoom region to demonstrate range selection capability |
| 101 | +zoom_start, zoom_end = 70, 100 |
| 102 | +ax.axvspan(dates[zoom_start], dates[zoom_end], alpha=0.2, color="#2A9D8F", zorder=1) |
| 103 | +ax.annotate( |
| 104 | + "Zoom Region\n(use scroll/drag)", |
| 105 | + xy=(dates[85], ax.get_ylim()[0] + 10), |
| 106 | + fontsize=12, |
| 107 | + ha="center", |
| 108 | + va="bottom", |
| 109 | + color="#2A9D8F", |
| 110 | + fontweight="bold", |
| 111 | +) |
| 112 | + |
| 113 | +# Style the plot |
| 114 | +ax.set_xlabel("Time", fontsize=20) |
| 115 | +ax.set_ylabel("Response Time (ms)", fontsize=20) |
| 116 | +ax.set_title("line-interactive · matplotlib · pyplots.ai", fontsize=24, fontweight="bold", pad=15) |
| 117 | + |
| 118 | +# Configure tick parameters |
| 119 | +ax.tick_params(axis="both", labelsize=16) |
| 120 | + |
| 121 | +# Format x-axis for better date display |
| 122 | +fig.autofmt_xdate(rotation=30) |
| 123 | + |
| 124 | +# Add subtle grid (reduced alpha for busy chart) |
| 125 | +ax.grid(True, alpha=0.2, linestyle="--", zorder=1) |
| 126 | + |
| 127 | +# Add compact legend |
| 128 | +ax.legend(fontsize=14, loc="upper left", framealpha=0.95) |
| 129 | + |
| 130 | +# Add toolbar hint for navigation |
| 131 | +fig.text( |
| 132 | + 0.5, |
| 133 | + 0.01, |
| 134 | + "Interactive Controls: Hover points for values • Scroll to zoom • Click-drag to pan • Home to reset", |
| 135 | + ha="center", |
| 136 | + va="bottom", |
| 137 | + fontsize=12, |
| 138 | + color="#555555", |
| 139 | + style="italic", |
| 140 | + bbox=dict(boxstyle="round,pad=0.4", facecolor="#f0f0f0", alpha=0.9, edgecolor="#cccccc"), |
| 141 | +) |
| 142 | + |
| 143 | +# Ensure proper layout with extra bottom margin for footer |
| 144 | +plt.tight_layout() |
| 145 | +plt.subplots_adjust(bottom=0.15) |
| 146 | + |
| 147 | +# Simulate a hover state by programmatically adding an annotation at one point |
| 148 | +# This demonstrates the hover tooltip that mplcursors provides |
| 149 | +demo_idx = 15 # Show tooltip on a representative point |
| 150 | +demo_x, demo_y = dates[demo_idx * 5], response_times[demo_idx * 5] |
| 151 | +demo_date_str = dates[demo_idx * 5].strftime("%Y-%m-%d %H:%M") |
| 152 | +ax.annotate( |
| 153 | + f"Time: {demo_date_str}\nResponse: {demo_y:.1f} ms", |
| 154 | + xy=(demo_x, demo_y), |
| 155 | + xytext=(30, 30), |
| 156 | + textcoords="offset points", |
| 157 | + fontsize=14, |
| 158 | + fontweight="bold", |
| 159 | + color="#306998", |
| 160 | + bbox=dict(boxstyle="round,pad=0.5", facecolor="#FFD43B", alpha=0.95, edgecolor="#306998", linewidth=2), |
| 161 | + arrowprops=dict(arrowstyle="->", color="#306998", lw=2, connectionstyle="arc3,rad=0.2"), |
| 162 | + zorder=10, |
| 163 | +) |
| 164 | + |
| 165 | +# Save the plot |
| 166 | +plt.savefig("plot.png", dpi=300, bbox_inches="tight") |
0 commit comments