Skip to content

Commit c8313e6

Browse files
feat(highcharts): implement box-horizontal (#6406)
## Implementation: `box-horizontal` - python/highcharts Implements the **python/highcharts** version of `box-horizontal`. **File:** `plots/box-horizontal/implementations/python/highcharts.py` **Parent Issue:** #2548 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/25706930503)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com>
1 parent 2f34828 commit c8313e6

2 files changed

Lines changed: 227 additions & 171 deletions

File tree

Lines changed: 80 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
box-horizontal: Horizontal Box Plot
3-
Library: highcharts unknown | Python 3.13.11
4-
Quality: 92/100 | Created: 2025-12-30
3+
Library: highcharts unknown | Python 3.13.13
4+
Quality: 84/100 | Updated: 2026-05-12
55
"""
66

7+
import os
78
import tempfile
89
import time
910
import urllib.request
@@ -17,29 +18,51 @@
1718
from selenium.webdriver.chrome.options import Options
1819

1920

20-
# Data - Response times by service type (realistic scenario)
21+
# Theme tokens
22+
THEME = os.getenv("ANYPLOT_THEME", "light")
23+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
24+
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
25+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
26+
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
27+
GRID = "rgba(26,26,23,0.10)" if THEME == "light" else "rgba(240,239,232,0.10)"
28+
BRAND = "#009E73" # Okabe-Ito position 1
29+
30+
# Data - Response times by service type with explicit outliers
2131
np.random.seed(42)
2232

2333
categories = ["API Gateway", "Database Query", "File Upload", "Authentication", "Payment Processing"]
2434

2535
# Generate data with different distributions for each service
26-
# Each category needs: [low, q1, median, q3, high]
36+
# Include explicit outliers to better demonstrate box plot capabilities
2737
data = []
2838
for cat in categories:
2939
if cat == "API Gateway":
30-
values = np.random.normal(50, 15, 200) # Fast, consistent
40+
values = np.random.normal(50, 15, 200)
3141
elif cat == "Database Query":
32-
values = np.random.normal(120, 40, 200) # Moderate with variation
42+
values = np.random.normal(120, 40, 200)
3343
elif cat == "File Upload":
34-
values = np.random.normal(250, 80, 200) # Slow, high variance
44+
values = np.random.normal(250, 80, 200)
3545
elif cat == "Authentication":
36-
values = np.random.normal(30, 8, 200) # Very fast
46+
values = np.random.normal(30, 8, 200)
3747
else: # Payment Processing
38-
values = np.random.normal(180, 50, 200) # Moderate-slow
48+
values = np.random.normal(180, 50, 200)
3949

4050
# Ensure positive values
4151
values = np.maximum(values, 5)
4252

53+
# Add explicit outliers to better demonstrate box plot
54+
outlier_indices = np.random.choice(len(values), size=3, replace=False)
55+
if cat == "API Gateway":
56+
values[outlier_indices] = [150, 160, 170]
57+
elif cat == "Database Query":
58+
values[outlier_indices] = [320, 350, 380]
59+
elif cat == "File Upload":
60+
values[outlier_indices] = [600, 620, 640]
61+
elif cat == "Authentication":
62+
values[outlier_indices] = [120, 130, 140]
63+
else: # Payment Processing
64+
values[outlier_indices] = [450, 480, 510]
65+
4366
# Calculate quartiles
4467
q1, median, q3 = np.percentile(values, [25, 50, 75])
4568
iqr = q3 - q1
@@ -56,67 +79,67 @@
5679
# Chart configuration - use full 4800x2700 with proper margins
5780
chart.options.chart = {
5881
"type": "boxplot",
59-
"inverted": True, # This makes it horizontal
82+
"inverted": True,
6083
"width": 4800,
6184
"height": 2700,
62-
"backgroundColor": "#ffffff",
63-
"marginLeft": 450, # Extra space for category labels and axis title
64-
"marginBottom": 220, # Extra space for value axis labels and title
85+
"backgroundColor": PAGE_BG,
86+
"marginLeft": 450,
87+
"marginBottom": 220,
6588
"marginTop": 180,
6689
"marginRight": 120,
6790
}
6891

6992
# Title
7093
chart.options.title = {
71-
"text": "box-horizontal · highcharts · pyplots.ai",
72-
"style": {"fontSize": "64px", "fontWeight": "bold"},
73-
"y": 60,
94+
"text": "box-horizontal · highcharts · anyplot.ai",
95+
"style": {"fontSize": "28px", "fontWeight": "600", "color": INK},
96+
"y": 40,
7497
}
7598

7699
# Subtitle for context
77100
chart.options.subtitle = {
78101
"text": "Response Time Distribution by Service Type",
79-
"style": {"fontSize": "42px", "color": "#666666"},
80-
"y": 120,
102+
"style": {"fontSize": "22px", "color": INK_SOFT},
103+
"y": 90,
81104
}
82105

83106
# X-axis (categories - shown on left due to inverted)
84107
chart.options.x_axis = {
85108
"categories": categories,
86-
"title": {"text": "Service Type", "style": {"fontSize": "42px"}, "margin": 20},
87-
"labels": {"style": {"fontSize": "36px"}, "x": -10},
88-
"lineWidth": 2,
89-
"lineColor": "#333333",
109+
"title": {"text": "Service Type", "style": {"fontSize": "22px", "color": INK}},
110+
"labels": {"style": {"fontSize": "18px", "color": INK_SOFT}},
111+
"lineColor": INK_SOFT,
112+
"tickColor": INK_SOFT,
90113
}
91114

92115
# Y-axis (values - shown on bottom due to inverted)
93116
chart.options.y_axis = {
94-
"title": {"text": "Response Time (ms)", "style": {"fontSize": "42px"}, "margin": 20},
95-
"labels": {"style": {"fontSize": "32px"}, "y": 30},
96-
"gridLineColor": "#e0e0e0",
117+
"title": {"text": "Response Time (ms)", "style": {"fontSize": "22px", "color": INK}},
118+
"labels": {"style": {"fontSize": "18px", "color": INK_SOFT}},
119+
"gridLineColor": GRID,
97120
"gridLineWidth": 1,
98121
"min": 0,
99-
"lineWidth": 2,
100-
"lineColor": "#333333",
122+
"lineColor": INK_SOFT,
123+
"tickColor": INK_SOFT,
101124
}
102125

103126
# Legend
104127
chart.options.legend = {"enabled": False}
105128

106-
# Plot options for boxplot styling
129+
# Plot options for boxplot styling using Okabe-Ito palette
107130
chart.options.plot_options = {
108131
"boxplot": {
109-
"fillColor": "rgba(48, 105, 152, 0.7)", # Python Blue with transparency
110-
"color": "#306998", # Box outline color
111-
"lineWidth": 4,
112-
"medianColor": "#FFD43B", # Python Yellow for median
113-
"medianWidth": 8,
114-
"stemColor": "#306998",
115-
"stemWidth": 4,
116-
"whiskerColor": "#306998",
132+
"fillColor": BRAND,
133+
"color": BRAND,
134+
"lineWidth": 3,
135+
"medianColor": INK,
136+
"medianWidth": 4,
137+
"stemColor": BRAND,
138+
"stemWidth": 2,
139+
"whiskerColor": BRAND,
117140
"whiskerLength": "40%",
118-
"whiskerWidth": 4,
119-
"pointWidth": 80,
141+
"whiskerWidth": 2,
142+
"pointWidth": 70,
120143
}
121144
}
122145

@@ -128,13 +151,21 @@
128151
chart.add_series(series)
129152

130153
# Download Highcharts JS files (required for headless Chrome)
154+
headers = {
155+
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36",
156+
"Accept": "text/javascript",
157+
"Referer": "https://www.highcharts.com/",
158+
}
159+
131160
highcharts_url = "https://code.highcharts.com/highcharts.js"
132-
with urllib.request.urlopen(highcharts_url, timeout=30) as response:
161+
highcharts_req = urllib.request.Request(highcharts_url, headers=headers)
162+
with urllib.request.urlopen(highcharts_req, timeout=30) as response:
133163
highcharts_js = response.read().decode("utf-8")
134164

135165
# BoxPlot requires highcharts-more.js
136166
highcharts_more_url = "https://code.highcharts.com/highcharts-more.js"
137-
with urllib.request.urlopen(highcharts_more_url, timeout=30) as response:
167+
highcharts_more_req = urllib.request.Request(highcharts_more_url, headers=headers)
168+
with urllib.request.urlopen(highcharts_more_req, timeout=30) as response:
138169
highcharts_more_js = response.read().decode("utf-8")
139170

140171
# Generate HTML with INLINE scripts
@@ -146,21 +177,21 @@
146177
<script>{highcharts_js}</script>
147178
<script>{highcharts_more_js}</script>
148179
</head>
149-
<body style="margin:0; padding:0; overflow:hidden;">
180+
<body style="margin:0; padding:0; overflow:hidden; background:{PAGE_BG};">
150181
<div id="container" style="width: 4800px; height: 2700px;"></div>
151182
<script>{html_str}</script>
152183
</body>
153184
</html>"""
154185

186+
# Write HTML for interactive version (theme-suffixed)
187+
with open(f"plot-{THEME}.html", "w", encoding="utf-8") as f:
188+
f.write(html_content)
189+
155190
# Write temp HTML and take screenshot
156191
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
157192
f.write(html_content)
158193
temp_path = f.name
159194

160-
# Also save HTML for interactive version
161-
with open("plot.html", "w", encoding="utf-8") as f:
162-
f.write(html_content)
163-
164195
chrome_options = Options()
165196
chrome_options.add_argument("--headless")
166197
chrome_options.add_argument("--no-sandbox")
@@ -172,10 +203,8 @@
172203
driver = webdriver.Chrome(options=chrome_options)
173204
driver.set_window_size(4800, 2700)
174205
driver.get(f"file://{temp_path}")
175-
time.sleep(5) # Wait for chart to render
176-
driver.save_screenshot("plot.png")
206+
time.sleep(5)
207+
driver.save_screenshot(f"plot-{THEME}.png")
177208
driver.quit()
178209

179-
Path(temp_path).unlink() # Clean up temp file
180-
181-
print("Generated plot.png and plot.html")
210+
Path(temp_path).unlink()

0 commit comments

Comments
 (0)