Skip to content

Commit 1d98f20

Browse files
feat(plotnine): implement line-interactive (#2839)
## Implementation: `line-interactive` - plotnine Implements the **plotnine** version of `line-interactive`. **File:** `plots/line-interactive/implementations/plotnine.py` --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20603327438)* --------- 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 330fc71 commit 1d98f20

2 files changed

Lines changed: 199 additions & 0 deletions

File tree

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
""" pyplots.ai
2+
line-interactive: Interactive Line Chart with Hover and Zoom
3+
Library: plotnine 0.15.2 | Python 3.13.11
4+
Quality: 90/100 | Created: 2025-12-30
5+
"""
6+
7+
import numpy as np
8+
import pandas as pd
9+
from plotnine import (
10+
aes,
11+
annotate,
12+
element_line,
13+
element_rect,
14+
element_text,
15+
geom_line,
16+
geom_point,
17+
geom_rect,
18+
ggplot,
19+
guide_legend,
20+
guides,
21+
labs,
22+
scale_color_manual,
23+
scale_x_datetime,
24+
theme,
25+
theme_minimal,
26+
)
27+
28+
29+
# Data - Server response time metrics (realistic monitoring scenario)
30+
np.random.seed(42)
31+
n_points = 200
32+
33+
# Generate datetime index for one week of hourly data
34+
dates = pd.date_range("2024-01-01", periods=n_points, freq="h")
35+
36+
# Generate realistic server response times with patterns
37+
base_response = 120 # Base response time in ms
38+
daily_pattern = 30 * np.sin(2 * np.pi * np.arange(n_points) / 24)
39+
weekly_pattern = 15 * np.sin(2 * np.pi * np.arange(n_points) / 168)
40+
noise = np.random.normal(0, 10, n_points)
41+
trend = np.linspace(0, 20, n_points)
42+
43+
# Add spike anomalies
44+
response_times = base_response + daily_pattern + weekly_pattern + noise + trend
45+
spike_indices = [45, 120, 175]
46+
for idx in spike_indices:
47+
response_times[idx] += np.random.uniform(50, 100)
48+
49+
# Calculate bounds
50+
avg_response = np.mean(response_times)
51+
y_min = np.min(response_times) - 10
52+
y_max = np.max(response_times) + 50
53+
54+
# Create unified DataFrame with series type for legend
55+
df = pd.DataFrame({"datetime": dates, "response_time": response_times, "series": "Response Time"})
56+
57+
# Add anomaly points
58+
anomaly_df = pd.DataFrame(
59+
{"datetime": dates[spike_indices], "response_time": response_times[spike_indices], "series": "Anomaly Spike"}
60+
)
61+
62+
# Add average line
63+
avg_df = pd.DataFrame(
64+
{"datetime": [dates[0], dates[-1]], "response_time": [avg_response, avg_response], "series": "Average"}
65+
)
66+
67+
# Combine for legend
68+
combined_df = pd.concat([df, anomaly_df, avg_df], ignore_index=True)
69+
70+
# Demo hover tooltip
71+
demo_idx = 75
72+
demo_x = dates[demo_idx]
73+
demo_y = response_times[demo_idx]
74+
demo_date_str = demo_x.strftime("%Y-%m-%d %H:%M")
75+
76+
# Zoom region bounds
77+
zoom_start = dates[70]
78+
zoom_end = dates[100]
79+
80+
# Build plot
81+
plot = (
82+
ggplot(combined_df, aes(x="datetime", y="response_time", color="series"))
83+
# Zoom region highlight
84+
+ geom_rect(
85+
aes(xmin="xmin", xmax="xmax", ymin="ymin", ymax="ymax"),
86+
data=pd.DataFrame({"xmin": [zoom_start], "xmax": [zoom_end], "ymin": [y_min], "ymax": [y_max]}),
87+
inherit_aes=False,
88+
fill="#2A9D8F",
89+
alpha=0.15,
90+
)
91+
# Main time series line
92+
+ geom_line(data=df, size=1.8)
93+
# Scatter points for hover targets (larger for visibility)
94+
+ geom_point(data=df.iloc[::5], size=5, alpha=0.9)
95+
# Average reference line
96+
+ geom_line(data=avg_df, linetype="dashed", size=1.5)
97+
# Anomaly markers (large triangles)
98+
+ geom_point(data=anomaly_df, size=8, shape="^")
99+
# Color scale with explicit legend
100+
+ scale_color_manual(
101+
name="Data Series",
102+
values={"Response Time": "#306998", "Anomaly Spike": "#E63946", "Average": "#808080"},
103+
breaks=["Response Time", "Average", "Anomaly Spike"],
104+
)
105+
+ guides(color=guide_legend(override_aes={"size": 6}))
106+
# Demo tooltip box
107+
+ annotate(
108+
"rect",
109+
xmin=demo_x - pd.Timedelta(hours=8),
110+
xmax=demo_x + pd.Timedelta(hours=8),
111+
ymin=demo_y + 18,
112+
ymax=demo_y + 55,
113+
fill="#FFD43B",
114+
alpha=0.95,
115+
)
116+
+ annotate(
117+
"text",
118+
x=demo_x,
119+
y=demo_y + 36,
120+
label=f"Time: {demo_date_str}\nResponse: {demo_y:.1f} ms",
121+
size=11,
122+
fontweight="bold",
123+
color="#306998",
124+
)
125+
+ annotate("segment", x=demo_x, xend=demo_x, y=demo_y + 18, yend=demo_y + 4, color="#306998", size=1.2)
126+
# Zoom region label
127+
+ annotate(
128+
"text",
129+
x=dates[85],
130+
y=y_min + 12,
131+
label="Zoom Region\n(range selection)",
132+
size=10,
133+
color="#2A9D8F",
134+
fontweight="bold",
135+
)
136+
# Subtitle (positioned prominently below title area)
137+
+ annotate(
138+
"text",
139+
x=dates[50],
140+
y=y_max - 8,
141+
label="Static demonstration of interactive concepts: tooltips, zoom regions, anomaly markers",
142+
size=12,
143+
color="#444444",
144+
fontstyle="italic",
145+
ha="left",
146+
)
147+
# Labels
148+
+ labs(x="Time", y="Response Time (ms)", title="line-interactive · plotnine · pyplots.ai")
149+
+ scale_x_datetime(date_labels="%b %d\n%H:%M")
150+
+ theme_minimal()
151+
+ theme(
152+
figure_size=(16, 9),
153+
plot_title=element_text(size=24, weight="bold"),
154+
axis_title=element_text(size=20),
155+
axis_text=element_text(size=14),
156+
axis_text_x=element_text(rotation=30),
157+
panel_grid_major=element_line(color="#CCCCCC", size=0.5, alpha=0.3),
158+
panel_grid_minor=element_line(color="#EEEEEE", size=0.3, alpha=0.2),
159+
plot_background=element_rect(fill="white"),
160+
panel_background=element_rect(fill="white"),
161+
legend_title=element_text(size=16, weight="bold"),
162+
legend_text=element_text(size=14),
163+
legend_position=(0.85, 0.75),
164+
legend_direction="vertical",
165+
legend_key_size=20,
166+
legend_background=element_rect(fill="white", color="#CCCCCC", alpha=0.95),
167+
legend_box_margin=10,
168+
)
169+
)
170+
171+
# Save
172+
plot.save("plot.png", dpi=300, verbose=False)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
library: plotnine
2+
specification_id: line-interactive
3+
created: '2025-12-30T18:40:02Z'
4+
updated: '2025-12-30T19:02:16Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20603327438
7+
issue: 0
8+
python_version: 3.13.11
9+
library_version: 0.15.2
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/line-interactive/plotnine/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/line-interactive/plotnine/plot_thumb.png
12+
preview_html: null
13+
quality_score: 90
14+
review:
15+
strengths:
16+
- Excellent creative approach to demonstrating interactive concepts in a static
17+
library
18+
- Realistic server monitoring scenario with meaningful patterns (daily cycles, anomaly
19+
spikes)
20+
- Clean demonstration of tooltip, zoom region, and control instructions via annotations
21+
- Proper use of plotnine grammar with well-structured theming
22+
- Good color scheme and visual hierarchy
23+
- Appropriate text sizing for 4800x2700 output
24+
weaknesses:
25+
- Legend not rendering in output despite being configured (legend_position may need
26+
adjustment)
27+
- The size parameter in geom_line is deprecated in newer plotnine versions

0 commit comments

Comments
 (0)