|
1 | 1 | """ |
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 |
8 | 4 | """ |
9 | 5 |
|
10 | | -from typing import Optional |
| 6 | +import json |
| 7 | +import tempfile |
| 8 | +import time |
| 9 | +import urllib.request |
| 10 | +from pathlib import Path |
11 | 11 |
|
12 | | -import numpy as np |
13 | | -import pandas as pd |
14 | 12 | from highcharts_core.chart import Chart |
15 | 13 | from highcharts_core.options import HighchartsOptions |
16 | 14 | 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> |
225 | 80 | <html> |
226 | 81 | <head> |
227 | 82 | <meta charset="utf-8"> |
228 | | - <script>{hc_js}</script> |
| 83 | + <script>{highcharts_js}</script> |
229 | 84 | </head> |
230 | 85 | <body style="margin:0;"> |
231 | | - <div id="container" style="width: 1600px; height: 900px;"></div> |
| 86 | + <div id="container" style="width: 4800px; height: 2700px;"></div> |
232 | 87 | <script> |
233 | 88 | Highcharts.chart('container', {opts_json}); |
234 | 89 | </script> |
235 | 90 | </body> |
236 | 91 | </html>""" |
237 | 92 |
|
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") |
242 | 104 |
|
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) |
248 | 108 |
|
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() |
254 | 113 |
|
255 | | - Path(temp_path).unlink() # Clean up temp file |
256 | | - print("Plot saved to plot.png") |
| 114 | +Path(temp_path).unlink() |
0 commit comments