Skip to content

Commit aeef6b0

Browse files
feat(matplotlib): implement line-interactive (#2821)
## Implementation: `line-interactive` - matplotlib Implements the **matplotlib** version of `line-interactive`. **File:** `plots/line-interactive/implementations/matplotlib.py` --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20602449294)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 6529163 commit aeef6b0

2 files changed

Lines changed: 182 additions & 0 deletions

File tree

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
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")
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
library: matplotlib
2+
specification_id: line-interactive
3+
created: '2025-12-30T17:49:35Z'
4+
updated: '2025-12-30T18:28:44Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20602449294
7+
issue: 0
8+
python_version: 3.13.11
9+
library_version: 3.10.8
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/line-interactive/matplotlib/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/line-interactive/matplotlib/plot_thumb.png
12+
preview_html: null
13+
quality_score: 88
14+
review:
15+
strengths: []
16+
weaknesses: []

0 commit comments

Comments
 (0)