Skip to content

Commit 9e808f1

Browse files
feat(highcharts): implement line-basic
Implements a basic line plot for highcharts library following KISS principles. Uses LineSeries with markers and Python Blue color scheme (#306998). Renders to 4800x2700px PNG via Selenium/Chrome headless.
1 parent 27357ca commit 9e808f1

1 file changed

Lines changed: 93 additions & 235 deletions

File tree

Lines changed: 93 additions & 235 deletions
Original file line numberDiff line numberDiff line change
@@ -1,256 +1,114 @@
11
"""
2-
line-basic: Basic Line Chart
3-
Implementation for: highcharts
4-
Variant: default
5-
Python: 3.10+
6-
7-
Note: Highcharts requires a license for commercial use.
2+
line-basic: Basic Line Plot
3+
Library: highcharts
84
"""
95

10-
from typing import Optional
6+
import json
7+
import tempfile
8+
import time
9+
import urllib.request
10+
from pathlib import Path
1111

12-
import numpy as np
13-
import pandas as pd
1412
from highcharts_core.chart import Chart
1513
from highcharts_core.options import HighchartsOptions
1614
from highcharts_core.options.series.area import LineSeries
17-
18-
19-
def create_plot(
20-
data: pd.DataFrame,
21-
x: str,
22-
y: str,
23-
color: str = "#4A90D9",
24-
linewidth: float = 2.0,
25-
marker: Optional[str] = None,
26-
marker_size: float = 6,
27-
alpha: float = 1.0,
28-
title: Optional[str] = None,
29-
xlabel: Optional[str] = None,
30-
ylabel: Optional[str] = None,
31-
width: int = 1600,
32-
height: int = 900,
33-
**kwargs,
34-
) -> Chart:
35-
"""
36-
Create a basic line chart connecting data points in order using Highcharts.
37-
38-
Args:
39-
data: Input DataFrame with required columns
40-
x: Column name for x-axis values (numeric or categorical)
41-
y: Column name for y-axis values (numeric)
42-
color: Line color (default: "#4A90D9" - a pleasant blue)
43-
linewidth: Line thickness in pixels (default: 2.0)
44-
marker: Marker style at data points, e.g., 'circle', 'square' (default: None)
45-
marker_size: Size of markers if shown (default: 6)
46-
alpha: Line transparency 0.0-1.0 (default: 1.0)
47-
title: Plot title (optional)
48-
xlabel: Custom x-axis label (optional, defaults to x column name)
49-
ylabel: Custom y-axis label (optional, defaults to y column name)
50-
width: Figure width in pixels (default: 1600)
51-
height: Figure height in pixels (default: 900)
52-
**kwargs: Additional parameters for Highcharts configuration
53-
54-
Returns:
55-
Highcharts Chart object
56-
57-
Raises:
58-
ValueError: If data is empty
59-
KeyError: If required columns not found
60-
61-
Example:
62-
>>> data = pd.DataFrame({
63-
... 'Month': ['Jan', 'Feb', 'Mar', 'Apr', 'May'],
64-
... 'Sales': [100, 120, 115, 140, 160]
65-
... })
66-
>>> chart = create_plot(data, x='Month', y='Sales')
67-
"""
68-
# Input validation
69-
if data.empty:
70-
raise ValueError("Data cannot be empty")
71-
72-
# Check required columns
73-
for col in [x, y]:
74-
if col not in data.columns:
75-
available = ", ".join(data.columns)
76-
raise KeyError(f"Column '{col}' not found. Available columns: {available}")
77-
78-
# Prepare data
79-
x_values = data[x].tolist()
80-
y_values = data[y].tolist()
81-
82-
# Determine if x-axis is categorical or numeric
83-
x_is_categorical = not pd.api.types.is_numeric_dtype(data[x])
84-
85-
# Create chart
86-
chart = Chart(container="container")
87-
88-
# Configure chart options
89-
chart.options = HighchartsOptions()
90-
91-
# Title
92-
chart.options.title = {"text": title if title else None, "style": {"fontSize": "16px", "fontWeight": "bold"}}
93-
94-
# X-axis configuration
95-
if x_is_categorical:
96-
chart.options.x_axis = {
97-
"categories": x_values,
98-
"title": {"text": xlabel or x, "style": {"fontSize": "12px"}},
99-
"labels": {"style": {"fontSize": "11px"}},
100-
"gridLineWidth": 1,
101-
"gridLineDashStyle": "Dot",
102-
"gridLineColor": "rgba(0, 0, 0, 0.1)",
103-
}
104-
else:
105-
chart.options.x_axis = {
106-
"title": {"text": xlabel or x, "style": {"fontSize": "12px"}},
107-
"labels": {"style": {"fontSize": "11px"}},
108-
"gridLineWidth": 1,
109-
"gridLineDashStyle": "Dot",
110-
"gridLineColor": "rgba(0, 0, 0, 0.1)",
111-
}
112-
113-
# Y-axis configuration
114-
chart.options.y_axis = {
115-
"title": {"text": ylabel or y, "style": {"fontSize": "12px"}},
116-
"labels": {"style": {"fontSize": "11px"}},
117-
"gridLineWidth": 1,
118-
"gridLineDashStyle": "Dot",
119-
"gridLineColor": "rgba(0, 0, 0, 0.3)",
120-
}
121-
122-
# Chart dimensions and background
123-
chart.options.chart = {"type": "line", "width": width, "height": height, "backgroundColor": "white"}
124-
125-
# Plot options for line series
126-
plot_options: dict = {"line": {"lineWidth": linewidth, "connectNulls": False, "animation": False}}
127-
128-
# Handle marker configuration
129-
if marker:
130-
marker_config: dict = {"enabled": True, "radius": marker_size, "symbol": marker}
131-
plot_options["line"]["marker"] = marker_config
132-
else:
133-
plot_options["line"]["marker"] = {"enabled": False}
134-
135-
chart.options.plot_options = plot_options
136-
137-
# Tooltip configuration
138-
chart.options.tooltip = {
139-
"shared": False,
140-
"useHTML": True,
141-
"headerFormat": "<b>{point.key}</b><br/>",
142-
"pointFormat": f"{ylabel or y}: <b>{{point.y:.2f}}</b>",
143-
}
144-
145-
# Create line series
146-
line_series = LineSeries()
147-
148-
# Set data based on x-axis type
149-
if x_is_categorical:
150-
line_series.data = y_values
151-
else:
152-
line_series.data = list(zip(x_values, y_values, strict=False))
153-
154-
line_series.name = ylabel or y
155-
156-
# Apply color with alpha
157-
if alpha < 1.0:
158-
# Convert hex color to rgba
159-
if color.startswith("#"):
160-
r = int(color[1:3], 16)
161-
g = int(color[3:5], 16)
162-
b = int(color[5:7], 16)
163-
line_series.color = f"rgba({r}, {g}, {b}, {alpha})"
164-
else:
165-
line_series.color = color
166-
else:
167-
line_series.color = color
168-
169-
chart.add_series(line_series)
170-
171-
# Legend (hide for single series)
172-
chart.options.legend = {"enabled": False}
173-
174-
# Disable credits
175-
chart.options.credits = {"enabled": False}
176-
177-
return chart
178-
179-
180-
if __name__ == "__main__":
181-
# Sample data for testing - simulating monthly sales data
182-
np.random.seed(42)
183-
184-
# Create sample data with 12 months
185-
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
186-
187-
# Generate realistic sales trend with some variation
188-
base_sales = 100
189-
trend = np.linspace(0, 50, 12) # Upward trend
190-
seasonal = 15 * np.sin(np.linspace(0, 2 * np.pi, 12)) # Seasonal variation
191-
noise = np.random.normal(0, 5, 12) # Random noise
192-
sales = base_sales + trend + seasonal + noise
193-
194-
data = pd.DataFrame({"Month": months, "Sales": sales.round(1)})
195-
196-
# Create plot
197-
chart = create_plot(
198-
data,
199-
x="Month",
200-
y="Sales",
201-
title="Monthly Sales Performance",
202-
xlabel="Month",
203-
ylabel="Sales ($K)",
204-
color="#4A90D9",
205-
linewidth=2.5,
206-
)
207-
208-
# Export to PNG via Selenium screenshot
209-
import json
210-
import tempfile
211-
import time
212-
from pathlib import Path
213-
214-
import requests
215-
from selenium import webdriver
216-
from selenium.webdriver.chrome.options import Options
217-
218-
# Download Highcharts JS (CDN doesn't work with file:// protocol)
219-
hc_js = requests.get("https://code.highcharts.com/highcharts.js", timeout=30).text
220-
221-
# Get chart options as JSON (to_js_literal has data format bugs with line charts)
222-
opts_json = json.dumps(chart.options.to_dict())
223-
224-
html_content = f"""<!DOCTYPE html>
15+
from selenium import webdriver
16+
from selenium.webdriver.chrome.options import Options
17+
18+
19+
# Data
20+
time_values = ["1", "2", "3", "4", "5", "6", "7"]
21+
values = [10, 15, 13, 18, 22, 19, 25]
22+
23+
# Create chart with container
24+
chart = Chart(container="container")
25+
chart.options = HighchartsOptions()
26+
27+
# Chart configuration
28+
chart.options.chart = {
29+
"type": "line",
30+
"width": 4800,
31+
"height": 2700,
32+
"backgroundColor": "#ffffff",
33+
"style": {"fontFamily": "Arial, sans-serif"},
34+
}
35+
36+
# Title
37+
chart.options.title = {"text": "Basic Line Plot", "style": {"fontSize": "48px"}}
38+
39+
# Axes
40+
chart.options.x_axis = {
41+
"title": {"text": "Time", "style": {"fontSize": "40px"}},
42+
"labels": {"style": {"fontSize": "32px"}, "enabled": True},
43+
"categories": time_values,
44+
"gridLineWidth": 1,
45+
"gridLineColor": "#e0e0e0",
46+
"lineWidth": 2,
47+
"tickWidth": 2,
48+
}
49+
chart.options.y_axis = {
50+
"title": {"text": "Value", "style": {"fontSize": "40px"}},
51+
"labels": {"style": {"fontSize": "32px"}},
52+
"gridLineColor": "#e0e0e0",
53+
"lineWidth": 2,
54+
}
55+
56+
# Legend (not needed for single series)
57+
chart.options.legend = {"enabled": False}
58+
59+
# Disable credits
60+
chart.options.credits = {"enabled": False}
61+
62+
# Create and add series
63+
series = LineSeries()
64+
series.data = values
65+
series.name = "Value"
66+
series.color = "#306998"
67+
series.marker = {"enabled": True, "radius": 8, "fillColor": "#306998"}
68+
series.line_width = 4
69+
70+
chart.add_series(series)
71+
72+
# Download Highcharts JS for inline embedding
73+
highcharts_url = "https://code.highcharts.com/highcharts.js"
74+
with urllib.request.urlopen(highcharts_url, timeout=30) as response:
75+
highcharts_js = response.read().decode("utf-8")
76+
77+
# Generate HTML with inline scripts using JSON approach for reliability
78+
opts_json = json.dumps(chart.options.to_dict())
79+
html_content = f"""<!DOCTYPE html>
22580
<html>
22681
<head>
22782
<meta charset="utf-8">
228-
<script>{hc_js}</script>
83+
<script>{highcharts_js}</script>
22984
</head>
23085
<body style="margin:0;">
231-
<div id="container" style="width: 1600px; height: 900px;"></div>
86+
<div id="container" style="width: 4800px; height: 2700px;"></div>
23287
<script>
23388
Highcharts.chart('container', {opts_json});
23489
</script>
23590
</body>
23691
</html>"""
23792

238-
# Write temp HTML and take screenshot
239-
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
240-
f.write(html_content)
241-
temp_path = f.name
93+
# Write temp HTML and take screenshot
94+
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
95+
f.write(html_content)
96+
temp_path = f.name
97+
98+
chrome_options = Options()
99+
chrome_options.add_argument("--headless")
100+
chrome_options.add_argument("--no-sandbox")
101+
chrome_options.add_argument("--disable-dev-shm-usage")
102+
chrome_options.add_argument("--disable-gpu")
103+
chrome_options.add_argument("--window-size=4800,2800")
242104

243-
chrome_options = Options()
244-
chrome_options.add_argument("--headless")
245-
chrome_options.add_argument("--no-sandbox")
246-
chrome_options.add_argument("--disable-dev-shm-usage")
247-
chrome_options.add_argument("--window-size=1600,900")
105+
driver = webdriver.Chrome(options=chrome_options)
106+
driver.get(f"file://{temp_path}")
107+
time.sleep(5)
248108

249-
driver = webdriver.Chrome(options=chrome_options)
250-
driver.get(f"file:///{temp_path}")
251-
time.sleep(5) # Wait for chart to render
252-
driver.save_screenshot("plot.png")
253-
driver.quit()
109+
# Take screenshot of just the chart container element
110+
container = driver.find_element("id", "container")
111+
container.screenshot("plot.png")
112+
driver.quit()
254113

255-
Path(temp_path).unlink() # Clean up temp file
256-
print("Plot saved to plot.png")
114+
Path(temp_path).unlink()

0 commit comments

Comments
 (0)