Skip to content

Commit 19fdc38

Browse files
feat(highcharts): implement pie-basic (#371)
## Summary Implements `pie-basic` for **highcharts** library. **Parent Issue:** #206 **Sub-Issue:** #276 **Base Branch:** `plot/pie-basic` **Attempt:** 1/3 ## Implementation - `plots/highcharts/pie/pie-basic/default.py` ## Changes - Rewrote implementation to follow KISS principles (simple sequential script) - Removed function wrapper and class structure - Updated to 4800x2700 px dimensions per style guide - Added proper Selenium element screenshot for exact dimensions - Uses PyPlots color palette from style guide Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
1 parent b63611f commit 19fdc38

1 file changed

Lines changed: 105 additions & 256 deletions

File tree

Lines changed: 105 additions & 256 deletions
Original file line numberDiff line numberDiff line change
@@ -1,287 +1,136 @@
11
"""
22
pie-basic: Basic Pie Chart
33
Library: highcharts
4-
5-
A fundamental pie chart that visualizes proportions and percentages of categorical data
6-
as slices of a circular chart. Each slice represents a category's share of the whole.
7-
8-
Note: Highcharts requires a license for commercial use.
94
"""
105

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

1311
import pandas as pd
1412
from highcharts_core.chart import Chart
1513
from highcharts_core.options import HighchartsOptions
1614
from highcharts_core.options.series.pie import PieSeries
15+
from selenium import webdriver
16+
from selenium.webdriver.chrome.options import Options
17+
1718

19+
# Data (from spec)
20+
data = pd.DataFrame(
21+
{"category": ["Product A", "Product B", "Product C", "Product D", "Other"], "value": [35, 25, 20, 15, 5]}
22+
)
1823

1924
# Style guide colors
20-
PYPLOTS_COLORS = [
21-
"#306998", # Python Blue (Primary)
25+
COLORS = [
26+
"#306998", # Python Blue
2227
"#FFD43B", # Python Yellow
2328
"#DC2626", # Signal Red
2429
"#059669", # Teal Green
2530
"#8B5CF6", # Violet
2631
"#F97316", # Orange
2732
]
2833

29-
30-
def create_plot(
31-
data: pd.DataFrame,
32-
category: str,
33-
value: str,
34-
figsize: tuple[int, int] = (10, 8),
35-
title: Optional[str] = None,
36-
colors: Optional[list[str]] = None,
37-
startangle: float = 90,
38-
autopct: str = "%1.1f%%",
39-
explode: Optional[list[float]] = None,
40-
shadow: bool = False,
41-
labels: Optional[list[str]] = None,
42-
legend: bool = True,
43-
legend_loc: str = "best",
44-
width: int = 1600,
45-
height: int = 900,
46-
**kwargs,
47-
) -> Chart:
48-
"""
49-
Create a basic pie chart from DataFrame.
50-
51-
Args:
52-
data: Input DataFrame with categorical and numeric data
53-
category: Column name for category names (slice labels)
54-
value: Column name for numeric values (slice proportions)
55-
figsize: Figure size as (width, height) in inches (legacy, use width/height instead)
56-
title: Plot title
57-
colors: Custom color palette for slices (defaults to PyPlots style guide colors)
58-
startangle: Starting angle for first slice in degrees (from positive x-axis)
59-
autopct: Format string for percentage labels
60-
explode: Offset distances for each slice (0-0.1 typical)
61-
shadow: Add shadow effect for 3D appearance
62-
labels: Custom labels (defaults to category names)
63-
legend: Whether to display legend
64-
legend_loc: Legend location (e.g., 'best', 'right', 'left')
65-
width: Figure width in pixels (default: 1600)
66-
height: Figure height in pixels (default: 900)
67-
**kwargs: Additional parameters passed to chart options
68-
69-
Returns:
70-
Highcharts Chart object
71-
72-
Raises:
73-
ValueError: If data is empty or contains negative values
74-
KeyError: If required columns are not found in data
75-
76-
Example:
77-
>>> data = pd.DataFrame({
78-
... 'category': ['Product A', 'Product B', 'Product C'],
79-
... 'value': [35, 25, 40]
80-
... })
81-
>>> chart = create_plot(data, 'category', 'value', title='Market Share')
82-
"""
83-
# Input validation
84-
if data.empty:
85-
raise ValueError("Data cannot be empty")
86-
87-
for col in [category, value]:
88-
if col not in data.columns:
89-
available = ", ".join(data.columns.tolist())
90-
raise KeyError(f"Column '{col}' not found. Available: {available}")
91-
92-
# Validate non-negative values
93-
if (data[value] < 0).any():
94-
raise ValueError("Pie chart values must be non-negative")
95-
96-
# Check if all values sum to zero
97-
if data[value].sum() == 0:
98-
raise ValueError("Sum of values cannot be zero")
99-
100-
# Get colors (use provided or default to PyPlots style guide)
101-
slice_colors = colors if colors is not None else PYPLOTS_COLORS
102-
103-
# Get labels (use provided or default to category names)
104-
slice_labels = labels if labels is not None else data[category].tolist()
105-
106-
# Create chart with container ID for rendering
107-
chart = Chart(container="container")
108-
chart.options = HighchartsOptions()
109-
110-
# Chart configuration
111-
chart.options.chart = {"type": "pie", "width": width, "height": height, "backgroundColor": "#ffffff"}
112-
113-
# Title with style guide typography
114-
if title:
115-
chart.options.title = {
116-
"text": title,
117-
"style": {
118-
"fontSize": "20px",
119-
"fontWeight": "600",
120-
"fontFamily": "Inter, DejaVu Sans, Arial, Helvetica, sans-serif",
121-
},
122-
}
123-
else:
124-
chart.options.title = {"text": None}
125-
126-
# Build data points for pie series
127-
pie_data = []
128-
for i, (cat, val) in enumerate(zip(data[category].tolist(), data[value].tolist(), strict=True)):
129-
point = {
130-
"name": slice_labels[i] if i < len(slice_labels) else cat,
131-
"y": val,
132-
"color": slice_colors[i % len(slice_colors)],
133-
}
134-
135-
# Apply explode if provided
136-
if explode is not None and i < len(explode) and explode[i] > 0:
137-
point["sliced"] = True
138-
point["selected"] = True
139-
140-
pie_data.append(point)
141-
142-
# Create pie series
143-
series = PieSeries()
144-
series.data = pie_data
145-
series.name = value
146-
147-
# Configure data labels to show percentages
148-
# Parse autopct format for decimal places (e.g., '%1.1f%%' -> 1 decimal)
149-
decimal_places = 1
150-
if autopct and "." in autopct:
151-
try:
152-
decimal_places = int(autopct.split(".")[1][0])
153-
except (IndexError, ValueError):
154-
decimal_places = 1
155-
156-
# Pie series options
157-
series.show_in_legend = legend
158-
series.start_angle = startangle
159-
series.shadow = shadow
160-
161-
# Data labels configuration
162-
series.data_labels = {
163-
"enabled": True,
164-
"format": f"{{point.percentage:.{decimal_places}f}}%",
165-
"distance": 20,
166-
"style": {
167-
"fontSize": "14px",
168-
"fontWeight": "normal",
169-
"fontFamily": "Inter, DejaVu Sans, Arial, Helvetica, sans-serif",
170-
"textOutline": "2px white",
171-
},
172-
}
173-
174-
chart.add_series(series)
175-
176-
# Plot options for pie
177-
chart.options.plot_options = {
178-
"pie": {
179-
"allowPointSelect": True,
180-
"cursor": "pointer",
181-
"showInLegend": legend,
182-
"startAngle": startangle,
183-
"shadow": shadow,
184-
"center": ["50%", "50%"],
185-
"size": "75%",
186-
}
34+
# Create chart
35+
chart = Chart(container="container")
36+
chart.options = HighchartsOptions()
37+
38+
# Chart configuration (4800 x 2700 px per style guide)
39+
chart.options.chart = {"type": "pie", "width": 4800, "height": 2700, "backgroundColor": "#ffffff"}
40+
41+
# Title
42+
chart.options.title = {"text": "Market Share Distribution", "style": {"fontSize": "48px", "fontWeight": "600"}}
43+
44+
# Build pie data with colors
45+
pie_data = []
46+
for i, row in data.iterrows():
47+
pie_data.append({"name": row["category"], "y": row["value"], "color": COLORS[i % len(COLORS)]})
48+
49+
# Create pie series
50+
series = PieSeries()
51+
series.data = pie_data
52+
series.name = "Market Share"
53+
series.show_in_legend = True
54+
55+
# Data labels with percentages
56+
series.data_labels = {
57+
"enabled": True,
58+
"format": "{point.name}: {point.percentage:.1f}%",
59+
"distance": 40,
60+
"style": {"fontSize": "32px", "fontWeight": "normal", "textOutline": "3px white"},
61+
}
62+
63+
chart.add_series(series)
64+
65+
# Plot options for pie
66+
chart.options.plot_options = {
67+
"pie": {
68+
"allowPointSelect": True,
69+
"cursor": "pointer",
70+
"showInLegend": True,
71+
"center": ["50%", "50%"],
72+
"size": "70%",
18773
}
188-
189-
# Legend configuration
190-
if legend:
191-
# Map legend_loc to Highcharts position
192-
legend_config = {
193-
"enabled": True,
194-
"align": "right",
195-
"verticalAlign": "middle",
196-
"layout": "vertical",
197-
"itemStyle": {"fontSize": "16px", "fontFamily": "Inter, DejaVu Sans, Arial, Helvetica, sans-serif"},
198-
"backgroundColor": "#ffffff",
199-
"borderWidth": 1,
200-
"borderRadius": 5,
201-
}
202-
203-
if legend_loc in ["left"]:
204-
legend_config["align"] = "left"
205-
elif legend_loc in ["right"]:
206-
legend_config["align"] = "right"
207-
elif legend_loc in ["top", "upper center"]:
208-
legend_config["align"] = "center"
209-
legend_config["verticalAlign"] = "top"
210-
legend_config["layout"] = "horizontal"
211-
elif legend_loc in ["bottom", "lower center"]:
212-
legend_config["align"] = "center"
213-
legend_config["verticalAlign"] = "bottom"
214-
legend_config["layout"] = "horizontal"
215-
216-
chart.options.legend = legend_config
217-
else:
218-
chart.options.legend = {"enabled": False}
219-
220-
# Tooltip configuration
221-
chart.options.tooltip = {
222-
"pointFormat": "<b>{point.percentage:.1f}%</b><br/>Value: {point.y}",
223-
"style": {"fontSize": "14px", "fontFamily": "Inter, DejaVu Sans, Arial, Helvetica, sans-serif"},
224-
}
225-
226-
# Credits
227-
chart.options.credits = {"enabled": False}
228-
229-
return chart
230-
231-
232-
if __name__ == "__main__":
233-
import tempfile
234-
import time
235-
import urllib.request
236-
from pathlib import Path
237-
238-
from selenium import webdriver
239-
from selenium.webdriver.chrome.options import Options
240-
241-
# Sample data for testing (from spec)
242-
sample_data = pd.DataFrame(
243-
{"category": ["Product A", "Product B", "Product C", "Product D", "Other"], "value": [35, 25, 20, 15, 5]}
244-
)
245-
246-
# Create plot
247-
chart = create_plot(sample_data, "category", "value", title="Market Share Distribution")
248-
249-
# Download Highcharts JS (required for headless Chrome which can't load CDN)
250-
highcharts_url = "https://code.highcharts.com/highcharts.js"
251-
with urllib.request.urlopen(highcharts_url, timeout=30) as response:
252-
highcharts_js = response.read().decode("utf-8")
253-
254-
# Export to PNG via Selenium screenshot
255-
html_str = chart.to_js_literal()
256-
html_content = f"""<!DOCTYPE html>
74+
}
75+
76+
# Legend configuration
77+
chart.options.legend = {
78+
"enabled": True,
79+
"align": "right",
80+
"verticalAlign": "middle",
81+
"layout": "vertical",
82+
"itemStyle": {"fontSize": "32px"},
83+
}
84+
85+
# Tooltip
86+
chart.options.tooltip = {
87+
"pointFormat": "<b>{point.percentage:.1f}%</b><br/>Value: {point.y}",
88+
"style": {"fontSize": "28px"},
89+
}
90+
91+
# Disable credits
92+
chart.options.credits = {"enabled": False}
93+
94+
# Download Highcharts JS (required for headless Chrome)
95+
highcharts_url = "https://code.highcharts.com/highcharts.js"
96+
with urllib.request.urlopen(highcharts_url, timeout=30) as response:
97+
highcharts_js = response.read().decode("utf-8")
98+
99+
# Generate HTML with inline scripts
100+
html_str = chart.to_js_literal()
101+
html_content = f"""<!DOCTYPE html>
257102
<html>
258103
<head>
259104
<meta charset="utf-8">
260105
<script>{highcharts_js}</script>
261106
</head>
262107
<body style="margin:0;">
263-
<div id="container" style="width: 1600px; height: 900px;"></div>
108+
<div id="container" style="width: 4800px; height: 2700px;"></div>
264109
<script>{html_str}</script>
265110
</body>
266111
</html>"""
267112

268-
# Write temp HTML and take screenshot
269-
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
270-
f.write(html_content)
271-
temp_path = f.name
272-
273-
chrome_options = Options()
274-
chrome_options.add_argument("--headless")
275-
chrome_options.add_argument("--no-sandbox")
276-
chrome_options.add_argument("--disable-dev-shm-usage")
277-
chrome_options.add_argument("--disable-gpu")
278-
chrome_options.add_argument("--window-size=1600,900")
279-
280-
driver = webdriver.Chrome(options=chrome_options)
281-
driver.get(f"file:///{temp_path}")
282-
time.sleep(5) # Wait for chart to render
283-
driver.save_screenshot("plot.png")
284-
driver.quit()
285-
286-
Path(temp_path).unlink() # Clean up temp file
287-
print("Plot saved to plot.png")
113+
# Write temp HTML
114+
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
115+
f.write(html_content)
116+
temp_path = f.name
117+
118+
# Take screenshot with Selenium
119+
chrome_options = Options()
120+
chrome_options.add_argument("--headless")
121+
chrome_options.add_argument("--no-sandbox")
122+
chrome_options.add_argument("--disable-dev-shm-usage")
123+
chrome_options.add_argument("--disable-gpu")
124+
chrome_options.add_argument("--window-size=5000,3000")
125+
126+
driver = webdriver.Chrome(options=chrome_options)
127+
driver.get(f"file://{temp_path}")
128+
time.sleep(5) # Wait for chart to render
129+
130+
# Screenshot the chart container element for exact dimensions
131+
container = driver.find_element("id", "container")
132+
container.screenshot("plot.png")
133+
driver.quit()
134+
135+
# Clean up temp file
136+
Path(temp_path).unlink()

0 commit comments

Comments
 (0)