Skip to content

Commit 020d909

Browse files
feat(highcharts): implement line-annotated-events (#3045)
## Implementation: `line-annotated-events` - highcharts Implements the **highcharts** version of `line-annotated-events`. **File:** `plots/line-annotated-events/implementations/highcharts.py` **Parent Issue:** #2997 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20617493187)* --------- 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 f96dc53 commit 020d909

2 files changed

Lines changed: 260 additions & 0 deletions

File tree

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
""" pyplots.ai
2+
line-annotated-events: Annotated Line Plot with Event Markers
3+
Library: highcharts unknown | Python 3.13.11
4+
Quality: 92/100 | Created: 2025-12-31
5+
"""
6+
7+
import tempfile
8+
import time
9+
import urllib.request
10+
from datetime import datetime, timedelta
11+
from pathlib import Path
12+
13+
import numpy as np
14+
from highcharts_core.chart import Chart
15+
from highcharts_core.options import HighchartsOptions
16+
from highcharts_core.options.series.area import LineSeries
17+
from highcharts_core.options.series.scatter import ScatterSeries
18+
from selenium import webdriver
19+
from selenium.webdriver.chrome.options import Options
20+
21+
22+
# Data - Simulated daily product metrics over 6 months with event annotations
23+
np.random.seed(42)
24+
25+
# Generate dates for 180 days
26+
start_date = datetime(2024, 1, 1)
27+
dates = [start_date + timedelta(days=i) for i in range(180)]
28+
timestamps = [int(d.timestamp() * 1000) for d in dates] # Highcharts uses milliseconds
29+
30+
# Generate realistic user growth data with trend and seasonality
31+
base = 10000
32+
trend = np.linspace(0, 5000, 180)
33+
seasonal = 500 * np.sin(np.linspace(0, 4 * np.pi, 180))
34+
noise = np.random.randn(180) * 300
35+
values = base + trend + seasonal + noise
36+
37+
# Define significant events to annotate
38+
events = [
39+
(datetime(2024, 1, 15), "Feature A Launch"),
40+
(datetime(2024, 2, 20), "Marketing Campaign"),
41+
(datetime(2024, 3, 25), "App Redesign"),
42+
(datetime(2024, 4, 30), "Partnership Deal"),
43+
(datetime(2024, 5, 28), "Mobile Update"),
44+
]
45+
event_timestamps = [int(e[0].timestamp() * 1000) for e in events]
46+
event_labels = [e[1] for e in events]
47+
48+
# Find y-values at event dates for scatter markers
49+
event_y_values = []
50+
for evt_date, _ in events:
51+
idx = (evt_date - start_date).days
52+
if 0 <= idx < len(values):
53+
event_y_values.append(round(values[idx]))
54+
else:
55+
event_y_values.append(None)
56+
57+
# Create chart with container specified
58+
chart = Chart(container="container")
59+
chart.options = HighchartsOptions()
60+
61+
# Chart configuration - explicit size for proper rendering
62+
chart.options.chart = {
63+
"type": "line",
64+
"width": 4800,
65+
"height": 2700,
66+
"backgroundColor": "#ffffff",
67+
"style": {"fontFamily": "Arial, sans-serif"},
68+
}
69+
70+
# Title
71+
chart.options.title = {
72+
"text": "line-annotated-events · highcharts · pyplots.ai",
73+
"style": {"fontSize": "48px", "fontWeight": "bold", "color": "#333333"},
74+
}
75+
76+
# Subtitle
77+
chart.options.subtitle = {
78+
"text": "Daily Active Users with Key Milestones",
79+
"style": {"fontSize": "32px", "color": "#666666"},
80+
}
81+
82+
# Build plot lines for events with alternating label positions
83+
plot_lines = []
84+
for i, (ts, label) in enumerate(zip(event_timestamps, event_labels, strict=False)):
85+
plot_lines.append(
86+
{
87+
"value": ts,
88+
"color": "#FFD43B",
89+
"width": 5,
90+
"dashStyle": "Dash",
91+
"zIndex": 5,
92+
"label": {
93+
"text": label,
94+
"rotation": 0,
95+
"style": {"fontSize": "28px", "fontWeight": "bold", "color": "#333333"},
96+
"y": 25 if i % 2 == 0 else 70,
97+
"x": 12,
98+
"align": "left",
99+
},
100+
}
101+
)
102+
103+
# X-axis (datetime)
104+
chart.options.x_axis = {
105+
"type": "datetime",
106+
"title": {"text": "Date", "style": {"fontSize": "40px", "color": "#333333"}},
107+
"labels": {"style": {"fontSize": "32px", "color": "#333333"}},
108+
"dateTimeLabelFormats": {"day": "%e %b", "week": "%e %b", "month": "%b '%y", "year": "%Y"},
109+
"plotLines": plot_lines,
110+
"lineColor": "#333333",
111+
"lineWidth": 2,
112+
"tickColor": "#333333",
113+
"tickInterval": 30 * 24 * 3600 * 1000, # Monthly ticks
114+
}
115+
116+
# Y-axis
117+
chart.options.y_axis = {
118+
"title": {"text": "Daily Active Users", "style": {"fontSize": "40px", "color": "#333333"}},
119+
"labels": {"style": {"fontSize": "32px", "color": "#333333"}},
120+
"gridLineWidth": 1,
121+
"gridLineColor": "#E0E0E0",
122+
"gridLineDashStyle": "Dash",
123+
"lineColor": "#333333",
124+
"lineWidth": 2,
125+
}
126+
127+
# Legend configuration
128+
chart.options.legend = {
129+
"enabled": True,
130+
"itemStyle": {"fontSize": "32px", "color": "#333333"},
131+
"align": "center",
132+
"verticalAlign": "bottom",
133+
"y": 30,
134+
"symbolRadius": 6,
135+
"symbolWidth": 24,
136+
}
137+
138+
# Plot options for styling
139+
chart.options.plot_options = {
140+
"line": {"lineWidth": 5, "marker": {"enabled": False}},
141+
"scatter": {"marker": {"symbol": "circle", "radius": 16, "lineWidth": 4, "lineColor": "#333333"}},
142+
}
143+
144+
# Credits
145+
chart.options.credits = {"enabled": False}
146+
147+
# Add main line series
148+
line_series = LineSeries()
149+
line_series.name = "Daily Active Users"
150+
line_series.data = [[ts, round(val)] for ts, val in zip(timestamps, values, strict=False)]
151+
line_series.color = "#306998"
152+
line_series.line_width = 5
153+
line_series.marker = {"enabled": False}
154+
155+
chart.add_series(line_series)
156+
157+
# Add event marker series (scatter points on the line at event dates)
158+
event_series = ScatterSeries()
159+
event_series.name = "Key Events"
160+
event_series.data = [
161+
{"x": ts, "y": yval} for ts, yval in zip(event_timestamps, event_y_values, strict=False) if yval is not None
162+
]
163+
event_series.color = "#FFD43B"
164+
event_series.marker = {"symbol": "circle", "radius": 18, "lineWidth": 4, "lineColor": "#333333", "fillColor": "#FFD43B"}
165+
event_series.z_index = 10
166+
167+
chart.add_series(event_series)
168+
169+
# Download Highcharts JS for inline embedding
170+
highcharts_url = "https://code.highcharts.com/highcharts.js"
171+
with urllib.request.urlopen(highcharts_url, timeout=30) as response:
172+
highcharts_js = response.read().decode("utf-8")
173+
174+
# Generate chart JS code
175+
chart_js = chart.to_js_literal()
176+
177+
# Build HTML with inline scripts - use string concatenation to avoid f-string issues
178+
# with JavaScript template literals containing braces
179+
html_parts = [
180+
"<!DOCTYPE html>",
181+
"<html>",
182+
"<head>",
183+
' <meta charset="utf-8">',
184+
" <style>",
185+
" * { margin: 0; padding: 0; }",
186+
" body { background: #ffffff; }",
187+
" #container { width: 4800px; height: 2700px; }",
188+
" </style>",
189+
" <script>",
190+
highcharts_js,
191+
" </script>",
192+
"</head>",
193+
"<body>",
194+
' <div id="container"></div>',
195+
" <script>",
196+
chart_js,
197+
" </script>",
198+
"</body>",
199+
"</html>",
200+
]
201+
html_content = "\n".join(html_parts)
202+
203+
# Write temp HTML
204+
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
205+
f.write(html_content)
206+
temp_path = f.name
207+
208+
# Save HTML file for interactive version
209+
with open("plot.html", "w", encoding="utf-8") as f:
210+
f.write(html_content)
211+
212+
# Take screenshot with headless Chrome
213+
chrome_options = Options()
214+
chrome_options.add_argument("--headless=new")
215+
chrome_options.add_argument("--no-sandbox")
216+
chrome_options.add_argument("--disable-dev-shm-usage")
217+
chrome_options.add_argument("--disable-gpu")
218+
chrome_options.add_argument("--window-size=5000,3000")
219+
chrome_options.add_argument("--force-device-scale-factor=1")
220+
221+
driver = webdriver.Chrome(options=chrome_options)
222+
driver.set_window_size(5000, 3000)
223+
driver.get(f"file://{temp_path}")
224+
time.sleep(6) # Wait for chart to render
225+
226+
# Find the chart container and take screenshot of it
227+
container = driver.find_element("id", "container")
228+
container.screenshot("plot.png")
229+
driver.quit()
230+
231+
# Clean up temp file
232+
Path(temp_path).unlink()
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
library: highcharts
2+
specification_id: line-annotated-events
3+
created: '2025-12-31T11:00:38Z'
4+
updated: '2025-12-31T11:11:16Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20617493187
7+
issue: 2997
8+
python_version: 3.13.11
9+
library_version: unknown
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/line-annotated-events/highcharts/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/line-annotated-events/highcharts/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/line-annotated-events/highcharts/plot.html
13+
quality_score: 92
14+
review:
15+
strengths:
16+
- Excellent implementation of event annotation pattern using plotLines with alternating
17+
label positions
18+
- Clean colorblind-safe palette with blue line and yellow event markers
19+
- Proper use of Highcharts datetime axis with appropriate label formatting
20+
- Well-structured code following KISS principles with proper Selenium screenshot
21+
workflow
22+
- Realistic business scenario with 5 distinct milestones across 180-day period
23+
weaknesses:
24+
- Legend text could be larger for better visibility at full resolution
25+
- Could leverage Highcharts native annotation module for more sophisticated event
26+
labels
27+
- Event marker scatter points could have tooltips showing event details for better
28+
interactivity in HTML output

0 commit comments

Comments
 (0)