|
| 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. |
| 8 | +""" |
| 9 | + |
| 10 | +from typing import Optional |
| 11 | + |
| 12 | +import numpy as np |
| 13 | +import pandas as pd |
| 14 | +from highcharts_core.chart import Chart |
| 15 | +from highcharts_core.options import HighchartsOptions |
| 16 | +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() |
| 87 | + |
| 88 | + # Configure chart options |
| 89 | + chart.options = HighchartsOptions() |
| 90 | + |
| 91 | + # Title |
| 92 | + chart.options.title = { |
| 93 | + "text": title if title else None, |
| 94 | + "style": {"fontSize": "16px", "fontWeight": "bold"}, |
| 95 | + } |
| 96 | + |
| 97 | + # X-axis configuration |
| 98 | + if x_is_categorical: |
| 99 | + chart.options.x_axis = { |
| 100 | + "categories": x_values, |
| 101 | + "title": { |
| 102 | + "text": xlabel or x, |
| 103 | + "style": {"fontSize": "12px"}, |
| 104 | + }, |
| 105 | + "labels": {"style": {"fontSize": "11px"}}, |
| 106 | + "gridLineWidth": 1, |
| 107 | + "gridLineDashStyle": "Dot", |
| 108 | + "gridLineColor": "rgba(0, 0, 0, 0.1)", |
| 109 | + } |
| 110 | + else: |
| 111 | + chart.options.x_axis = { |
| 112 | + "title": { |
| 113 | + "text": xlabel or x, |
| 114 | + "style": {"fontSize": "12px"}, |
| 115 | + }, |
| 116 | + "labels": {"style": {"fontSize": "11px"}}, |
| 117 | + "gridLineWidth": 1, |
| 118 | + "gridLineDashStyle": "Dot", |
| 119 | + "gridLineColor": "rgba(0, 0, 0, 0.1)", |
| 120 | + } |
| 121 | + |
| 122 | + # Y-axis configuration |
| 123 | + chart.options.y_axis = { |
| 124 | + "title": { |
| 125 | + "text": ylabel or y, |
| 126 | + "style": {"fontSize": "12px"}, |
| 127 | + }, |
| 128 | + "labels": {"style": {"fontSize": "11px"}}, |
| 129 | + "gridLineWidth": 1, |
| 130 | + "gridLineDashStyle": "Dot", |
| 131 | + "gridLineColor": "rgba(0, 0, 0, 0.3)", |
| 132 | + } |
| 133 | + |
| 134 | + # Chart dimensions and background |
| 135 | + chart.options.chart = { |
| 136 | + "type": "line", |
| 137 | + "width": width, |
| 138 | + "height": height, |
| 139 | + "backgroundColor": "white", |
| 140 | + } |
| 141 | + |
| 142 | + # Plot options for line series |
| 143 | + plot_options: dict = { |
| 144 | + "line": { |
| 145 | + "lineWidth": linewidth, |
| 146 | + "connectNulls": False, |
| 147 | + "animation": False, |
| 148 | + } |
| 149 | + } |
| 150 | + |
| 151 | + # Handle marker configuration |
| 152 | + if marker: |
| 153 | + marker_config: dict = { |
| 154 | + "enabled": True, |
| 155 | + "radius": marker_size, |
| 156 | + "symbol": marker, |
| 157 | + } |
| 158 | + plot_options["line"]["marker"] = marker_config |
| 159 | + else: |
| 160 | + plot_options["line"]["marker"] = {"enabled": False} |
| 161 | + |
| 162 | + chart.options.plot_options = plot_options |
| 163 | + |
| 164 | + # Tooltip configuration |
| 165 | + chart.options.tooltip = { |
| 166 | + "shared": False, |
| 167 | + "useHTML": True, |
| 168 | + "headerFormat": "<b>{point.key}</b><br/>", |
| 169 | + "pointFormat": f"{ylabel or y}: <b>{{point.y:.2f}}</b>", |
| 170 | + } |
| 171 | + |
| 172 | + # Create line series |
| 173 | + line_series = LineSeries() |
| 174 | + |
| 175 | + # Set data based on x-axis type |
| 176 | + if x_is_categorical: |
| 177 | + line_series.data = y_values |
| 178 | + else: |
| 179 | + line_series.data = list(zip(x_values, y_values, strict=False)) |
| 180 | + |
| 181 | + line_series.name = ylabel or y |
| 182 | + |
| 183 | + # Apply color with alpha |
| 184 | + if alpha < 1.0: |
| 185 | + # Convert hex color to rgba |
| 186 | + if color.startswith("#"): |
| 187 | + r = int(color[1:3], 16) |
| 188 | + g = int(color[3:5], 16) |
| 189 | + b = int(color[5:7], 16) |
| 190 | + line_series.color = f"rgba({r}, {g}, {b}, {alpha})" |
| 191 | + else: |
| 192 | + line_series.color = color |
| 193 | + else: |
| 194 | + line_series.color = color |
| 195 | + |
| 196 | + chart.add_series(line_series) |
| 197 | + |
| 198 | + # Legend (hide for single series) |
| 199 | + chart.options.legend = {"enabled": False} |
| 200 | + |
| 201 | + # Disable credits |
| 202 | + chart.options.credits = {"enabled": False} |
| 203 | + |
| 204 | + return chart |
| 205 | + |
| 206 | + |
| 207 | +if __name__ == "__main__": |
| 208 | + # Sample data for testing - simulating monthly sales data |
| 209 | + np.random.seed(42) |
| 210 | + |
| 211 | + # Create sample data with 12 months |
| 212 | + months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] |
| 213 | + |
| 214 | + # Generate realistic sales trend with some variation |
| 215 | + base_sales = 100 |
| 216 | + trend = np.linspace(0, 50, 12) # Upward trend |
| 217 | + seasonal = 15 * np.sin(np.linspace(0, 2 * np.pi, 12)) # Seasonal variation |
| 218 | + noise = np.random.normal(0, 5, 12) # Random noise |
| 219 | + sales = base_sales + trend + seasonal + noise |
| 220 | + |
| 221 | + data = pd.DataFrame({"Month": months, "Sales": sales.round(1)}) |
| 222 | + |
| 223 | + # Create plot |
| 224 | + chart = create_plot( |
| 225 | + data, |
| 226 | + x="Month", |
| 227 | + y="Sales", |
| 228 | + title="Monthly Sales Performance", |
| 229 | + xlabel="Month", |
| 230 | + ylabel="Sales ($K)", |
| 231 | + color="#4A90D9", |
| 232 | + linewidth=2.5, |
| 233 | + ) |
| 234 | + |
| 235 | + # Export to PNG via Selenium screenshot |
| 236 | + import tempfile |
| 237 | + import time |
| 238 | + from pathlib import Path |
| 239 | + |
| 240 | + from selenium import webdriver |
| 241 | + from selenium.webdriver.chrome.options import Options |
| 242 | + |
| 243 | + # Generate HTML content |
| 244 | + html_str = chart.to_js_literal() |
| 245 | + html_content = f"""<!DOCTYPE html> |
| 246 | +<html> |
| 247 | +<head> |
| 248 | + <meta charset="utf-8"> |
| 249 | + <script src="https://code.highcharts.com/highcharts.js"></script> |
| 250 | +</head> |
| 251 | +<body style="margin:0;"> |
| 252 | + <div id="container" style="width: 1600px; height: 900px;"></div> |
| 253 | + <script>{html_str}</script> |
| 254 | +</body> |
| 255 | +</html>""" |
| 256 | + |
| 257 | + # Write temp HTML and take screenshot |
| 258 | + with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False) as f: |
| 259 | + f.write(html_content) |
| 260 | + temp_path = f.name |
| 261 | + |
| 262 | + chrome_options = Options() |
| 263 | + chrome_options.add_argument("--headless") |
| 264 | + chrome_options.add_argument("--no-sandbox") |
| 265 | + chrome_options.add_argument("--disable-dev-shm-usage") |
| 266 | + chrome_options.add_argument("--window-size=1600,900") |
| 267 | + |
| 268 | + driver = webdriver.Chrome(options=chrome_options) |
| 269 | + driver.get(f"file://{temp_path}") |
| 270 | + time.sleep(1) # Wait for chart to render |
| 271 | + driver.save_screenshot("plot.png") |
| 272 | + driver.quit() |
| 273 | + |
| 274 | + Path(temp_path).unlink() # Clean up temp file |
| 275 | + print("Plot saved to plot.png") |
0 commit comments