Skip to content

Commit 6541fd1

Browse files
feat(highcharts): implement area-basic (#378)
## Summary Implements `area-basic` for **highcharts** library. **Parent Issue:** #201 **Sub-Issue:** #339 **Base Branch:** `plot/area-basic` **Attempt:** 1/3 ## Implementation - `plots/highcharts/area/area-basic/default.py` ## Changes - Simple sequential script following KISS principle (no functions, classes, or type hints) - Uses Python Blue (#306998) as primary color per style guide - 4800x2700 px output per style guide - Inline Highcharts JS for reliable headless Chrome rendering - Proper `container="container"` specification for chart rendering - Subtle grid with 10% opacity - Appropriate font sizes for the large canvas (48px title, 40px axis titles, 32px labels) Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
1 parent f99f09f commit 6541fd1

1 file changed

Lines changed: 80 additions & 218 deletions

File tree

Lines changed: 80 additions & 218 deletions
Original file line numberDiff line numberDiff line change
@@ -1,235 +1,97 @@
11
"""
22
area-basic: Basic Area Chart
3-
Implementation for: highcharts
4-
Variant: default
5-
Python: 3.10+
6-
7-
Note: Highcharts requires a license for commercial use.
3+
Library: highcharts
84
"""
95

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

12-
import pandas as pd
1311
from highcharts_core.chart import Chart
1412
from highcharts_core.options import HighchartsOptions
1513
from highcharts_core.options.series.area import AreaSeries
16-
17-
18-
def create_plot(
19-
data: pd.DataFrame,
20-
x: str,
21-
y: str,
22-
title: Optional[str] = None,
23-
xlabel: Optional[str] = None,
24-
ylabel: Optional[str] = None,
25-
color: str = "#4682B4",
26-
fill_opacity: float = 0.5,
27-
line_width: int = 2,
28-
width: int = 1600,
29-
height: int = 900,
30-
**kwargs,
31-
) -> Chart:
32-
"""
33-
Create a basic area chart showing magnitude and trends using Highcharts.
34-
35-
Args:
36-
data: Input DataFrame with required columns
37-
x: Column name for x-axis values
38-
y: Column name for y-axis values (numeric)
39-
title: Plot title (optional)
40-
xlabel: Custom x-axis label (optional, defaults to x column name)
41-
ylabel: Custom y-axis label (optional, defaults to y column name)
42-
color: Fill and line color (default: "#4682B4" steelblue)
43-
fill_opacity: Transparency level for the fill area (default: 0.5)
44-
line_width: Width of the line on top of the area (default: 2)
45-
width: Figure width in pixels (default: 1600)
46-
height: Figure height in pixels (default: 900)
47-
**kwargs: Additional parameters for Highcharts configuration
48-
49-
Returns:
50-
Highcharts Chart object
51-
52-
Raises:
53-
ValueError: If data is empty
54-
KeyError: If required columns not found
55-
56-
Example:
57-
>>> data = pd.DataFrame({
58-
... 'month': [1, 2, 3, 4, 5, 6],
59-
... 'sales': [100, 150, 130, 180, 200, 220]
60-
... })
61-
>>> chart = create_plot(data, x='month', y='sales')
62-
"""
63-
# Input validation
64-
if data.empty:
65-
raise ValueError("Data cannot be empty")
66-
67-
# Check required columns
68-
for col in [x, y]:
69-
if col not in data.columns:
70-
available = ", ".join(data.columns)
71-
raise KeyError(f"Column '{col}' not found. Available columns: {available}")
72-
73-
# Prepare data - sort by x values for proper area rendering
74-
sorted_data = data[[x, y]].dropna().sort_values(by=x)
75-
x_values = sorted_data[x].tolist()
76-
y_values = sorted_data[y].tolist()
77-
78-
# Determine if x-axis should be categorical or numeric
79-
x_is_numeric = pd.api.types.is_numeric_dtype(data[x])
80-
81-
# Create chart
82-
chart = Chart()
83-
84-
# Configure chart options
85-
chart.options = HighchartsOptions()
86-
87-
# Title
88-
chart.options.title = {
89-
"text": title or "Area Chart",
90-
"style": {"fontSize": "16px", "fontWeight": "bold"},
91-
}
92-
93-
# X-axis configuration
94-
if x_is_numeric:
95-
chart.options.x_axis = {
96-
"title": {"text": xlabel or x},
97-
"gridLineWidth": 1,
98-
"gridLineDashStyle": "Dot",
99-
"gridLineColor": "rgba(0, 0, 0, 0.1)",
100-
}
101-
else:
102-
# Categorical x-axis
103-
chart.options.x_axis = {
104-
"categories": x_values,
105-
"title": {"text": xlabel or x},
106-
"gridLineWidth": 1,
107-
"gridLineDashStyle": "Dot",
108-
"gridLineColor": "rgba(0, 0, 0, 0.1)",
109-
}
110-
111-
# Y-axis
112-
chart.options.y_axis = {
113-
"title": {"text": ylabel or y},
114-
"gridLineWidth": 1,
115-
"gridLineDashStyle": "Dot",
116-
"gridLineColor": "rgba(0, 0, 0, 0.1)",
117-
"min": 0, # Area charts typically start from 0
118-
}
119-
120-
# Plot options for area series
121-
chart.options.plot_options = {
122-
"area": {
123-
"fillOpacity": fill_opacity,
124-
"lineWidth": line_width,
125-
"marker": {
126-
"enabled": True,
127-
"radius": 4,
128-
"fillColor": color,
129-
"lineWidth": 1,
130-
"lineColor": "#ffffff",
131-
},
132-
}
133-
}
134-
135-
# Tooltip
136-
chart.options.tooltip = {
137-
"shared": False,
138-
"useHTML": True,
139-
"headerFormat": "<b>{point.key}</b><br/>",
140-
"pointFormat": f"<span>{ylabel or y}: {{point.y}}</span>",
141-
}
142-
143-
# Chart dimensions and type
144-
chart.options.chart = {
145-
"type": "area",
146-
"width": width,
147-
"height": height,
148-
"backgroundColor": "white",
149-
}
150-
151-
# Create area series
152-
area_series = AreaSeries()
153-
area_series.name = ylabel or y
154-
area_series.color = color
155-
156-
# Set data based on x-axis type
157-
if x_is_numeric:
158-
# For numeric x-axis, use [x, y] pairs
159-
area_series.data = list(zip(x_values, y_values, strict=True))
160-
else:
161-
# For categorical x-axis, use y values only (categories are set on x-axis)
162-
area_series.data = y_values
163-
164-
chart.add_series(area_series)
165-
166-
# Legend
167-
chart.options.legend = {
168-
"enabled": False, # Single series, no legend needed
169-
}
170-
171-
# Credits
172-
chart.options.credits = {"enabled": False}
173-
174-
return chart
175-
176-
177-
if __name__ == "__main__":
178-
import tempfile
179-
import time
180-
from pathlib import Path
181-
182-
from selenium import webdriver
183-
from selenium.webdriver.chrome.options import Options
184-
185-
# Sample data for testing - monthly sales data
186-
sample_data = pd.DataFrame({
187-
"Month": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
188-
"Sales": [120, 150, 170, 160, 180, 220, 250, 240, 210, 190, 230, 280],
189-
})
190-
191-
# Create plot with categorical x-axis
192-
chart = create_plot(
193-
sample_data,
194-
x="Month",
195-
y="Sales",
196-
title="Monthly Sales Performance",
197-
ylabel="Sales ($K)",
198-
xlabel="Month",
199-
color="#4682B4",
200-
fill_opacity=0.4,
201-
)
202-
203-
# Generate HTML content
204-
html_str = chart.to_js_literal()
205-
html_content = f"""<!DOCTYPE html>
14+
from selenium import webdriver
15+
from selenium.webdriver.chrome.options import Options
16+
17+
18+
# Data - Monthly sales data
19+
sales = [100, 150, 130, 180, 200, 220, 195, 240, 260, 245, 280, 310]
20+
21+
# Create chart
22+
chart = Chart(container="container")
23+
chart.options = HighchartsOptions()
24+
25+
# Chart configuration
26+
chart.options.chart = {"type": "area", "width": 4800, "height": 2700, "backgroundColor": "#ffffff"}
27+
28+
# Title
29+
chart.options.title = {"text": "Basic Area Chart", "style": {"fontSize": "48px"}}
30+
31+
# Axes
32+
chart.options.x_axis = {
33+
"title": {"text": "Month", "style": {"fontSize": "40px"}},
34+
"categories": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
35+
"labels": {"style": {"fontSize": "32px"}},
36+
"gridLineWidth": 1,
37+
"gridLineColor": "rgba(0, 0, 0, 0.1)",
38+
}
39+
40+
chart.options.y_axis = {
41+
"title": {"text": "Sales", "style": {"fontSize": "40px"}},
42+
"labels": {"style": {"fontSize": "32px"}},
43+
"gridLineWidth": 1,
44+
"gridLineColor": "rgba(0, 0, 0, 0.1)",
45+
}
46+
47+
# Legend styling
48+
chart.options.legend = {"itemStyle": {"fontSize": "32px"}}
49+
50+
# Area series
51+
series = AreaSeries()
52+
series.data = sales
53+
series.name = "Monthly Sales"
54+
series.color = "#306998" # Python Blue
55+
series.fill_opacity = 0.5
56+
series.line_width = 4
57+
58+
chart.add_series(series)
59+
60+
# Download Highcharts JS for inline embedding
61+
highcharts_url = "https://code.highcharts.com/highcharts.js"
62+
with urllib.request.urlopen(highcharts_url, timeout=30) as response:
63+
highcharts_js = response.read().decode("utf-8")
64+
65+
# Generate HTML with inline scripts
66+
html_str = chart.to_js_literal()
67+
html_content = f"""<!DOCTYPE html>
20668
<html>
20769
<head>
20870
<meta charset="utf-8">
209-
<script src="https://code.highcharts.com/highcharts.js"></script>
71+
<script>{highcharts_js}</script>
21072
</head>
21173
<body style="margin:0;">
212-
<div id="container" style="width: 1600px; height: 900px;"></div>
74+
<div id="container" style="width: 4800px; height: 2700px;"></div>
21375
<script>{html_str}</script>
21476
</body>
21577
</html>"""
21678

217-
# Write temp HTML and take screenshot
218-
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False) as f:
219-
f.write(html_content)
220-
temp_path = f.name
221-
222-
chrome_options = Options()
223-
chrome_options.add_argument("--headless")
224-
chrome_options.add_argument("--no-sandbox")
225-
chrome_options.add_argument("--disable-dev-shm-usage")
226-
chrome_options.add_argument("--window-size=1600,900")
227-
228-
driver = webdriver.Chrome(options=chrome_options)
229-
driver.get(f"file://{temp_path}")
230-
time.sleep(1) # Wait for chart to render
231-
driver.save_screenshot("plot.png")
232-
driver.quit()
233-
234-
Path(temp_path).unlink() # Clean up temp file
235-
print("Plot saved to plot.png")
79+
# Write temp HTML and take screenshot
80+
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
81+
f.write(html_content)
82+
temp_path = f.name
83+
84+
chrome_options = Options()
85+
chrome_options.add_argument("--headless")
86+
chrome_options.add_argument("--no-sandbox")
87+
chrome_options.add_argument("--disable-dev-shm-usage")
88+
chrome_options.add_argument("--disable-gpu")
89+
chrome_options.add_argument("--window-size=4800,2700")
90+
91+
driver = webdriver.Chrome(options=chrome_options)
92+
driver.get(f"file://{temp_path}")
93+
time.sleep(5) # Wait for chart to render
94+
driver.save_screenshot("plot.png")
95+
driver.quit()
96+
97+
Path(temp_path).unlink() # Clean up temp file

0 commit comments

Comments
 (0)