Skip to content

Commit 035ef11

Browse files
feat(highcharts): implement strip-basic (#5671)
## Implementation: `strip-basic` - python/highcharts Implements the **python/highcharts** version of `strip-basic`. **File:** `plots/strip-basic/implementations/python/highcharts.py` **Parent Issue:** #975 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/25342407646)* --------- 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 0624985 commit 035ef11

2 files changed

Lines changed: 260 additions & 221 deletions

File tree

Lines changed: 81 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
strip-basic: Basic Strip Plot
3-
Library: highcharts unknown | Python 3.13.11
4-
Quality: 91/100 | Created: 2025-12-23
3+
Library: highcharts unknown | Python 3.13.13
4+
Quality: 89/100 | Updated: 2026-05-04
55
"""
66

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

1920

20-
# Data - student test scores by subject
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+
NEUTRAL = "#1A1A1A" if THEME == "light" else "#E8E8E0"
29+
30+
# Okabe-Ito palette — first series is always #009E73
31+
OKABE_ITO = ["#009E73", "#D55E00", "#0072B2", "#CC79A7"]
32+
33+
# Data — student test scores by subject
2134
np.random.seed(42)
2235
categories = ["Mathematics", "Science", "Literature", "History"]
23-
colors = ["#306998", "#FFD43B", "#9467BD", "#17BECF"]
2436

25-
# Generate realistic test score data (different distributions per subject)
2637
raw_data = {
2738
"Mathematics": np.concatenate(
2839
[
29-
np.random.normal(72, 12, 35), # Wider spread
30-
np.random.normal(90, 5, 10), # High performers
40+
np.random.normal(72, 12, 35),
41+
np.random.normal(90, 5, 10), # high-performer cluster (bimodal)
3142
]
3243
),
33-
"Science": np.random.normal(75, 10, 40), # Normal distribution
44+
"Science": np.random.normal(75, 10, 40),
3445
"Literature": np.concatenate(
3546
[
36-
np.random.normal(65, 8, 25), # Lower cluster
37-
np.random.normal(82, 6, 20), # Upper cluster (bimodal)
47+
np.random.normal(65, 8, 25), # lower cluster
48+
np.random.normal(82, 6, 20), # upper cluster (bimodal)
3849
]
3950
),
40-
"History": np.random.normal(78, 9, 38), # Slightly higher scores
51+
"History": np.random.normal(78, 9, 38),
4152
}
4253

43-
# Clip values to realistic range (0-100)
4454
for cat in categories:
4555
raw_data[cat] = np.clip(raw_data[cat], 30, 100)
4656

47-
# Create strip plot data with random jitter
48-
jitter_width = 0.25 # Moderate jitter as per spec recommendation (0.1-0.3)
57+
jitter_width = 0.25
4958
strip_data = []
5059
for cat_idx, cat in enumerate(categories):
5160
values = raw_data[cat]
52-
# Random horizontal jitter within jitter_width
5361
x_jitter = np.random.uniform(-jitter_width, jitter_width, len(values))
54-
5562
for val, x_off in zip(values, x_jitter, strict=True):
56-
strip_data.append({"x": cat_idx + x_off, "y": float(val), "category": cat, "color": colors[cat_idx]})
63+
strip_data.append({"x": cat_idx + x_off, "y": float(val), "category": cat})
5764

58-
# Calculate mean for each category (for reference markers)
5965
means = {cat: float(np.mean(raw_data[cat])) for cat in categories}
6066

61-
# Create chart
67+
# Plot
6268
chart = Chart(container="container")
6369
chart.options = HighchartsOptions()
6470

65-
# Chart configuration
6671
chart.options.chart = {
6772
"type": "scatter",
6873
"width": 4800,
6974
"height": 2700,
70-
"backgroundColor": "#ffffff",
75+
"backgroundColor": PAGE_BG,
7176
"marginBottom": 200,
77+
"style": {"fontFamily": "-apple-system, system-ui, sans-serif"},
7278
}
7379

74-
# Title
7580
chart.options.title = {
76-
"text": "strip-basic · highcharts · pyplots.ai",
77-
"style": {"fontSize": "72px", "fontWeight": "bold"},
81+
"text": "strip-basic · highcharts · anyplot.ai",
82+
"style": {"fontSize": "64px", "fontWeight": "bold", "color": INK},
7883
}
7984

80-
# Subtitle describing data
81-
chart.options.subtitle = {"text": "Student Test Scores by Subject", "style": {"fontSize": "48px"}}
85+
chart.options.subtitle = {"text": "Student Test Scores by Subject", "style": {"fontSize": "40px", "color": INK_SOFT}}
8286

83-
# X-axis (categorical)
8487
chart.options.x_axis = {
8588
"categories": categories,
86-
"title": {"text": "Subject", "style": {"fontSize": "48px"}},
87-
"labels": {"style": {"fontSize": "36px"}},
89+
"title": {"text": "Subject", "style": {"fontSize": "40px", "color": INK}},
90+
"labels": {"style": {"fontSize": "32px", "color": INK_SOFT}},
8891
"tickWidth": 0,
92+
"lineColor": INK_SOFT,
8993
"lineWidth": 2,
9094
"min": -0.5,
9195
"max": len(categories) - 0.5,
9296
"tickPositions": [0, 1, 2, 3],
9397
}
9498

95-
# Y-axis
9699
chart.options.y_axis = {
97-
"title": {"text": "Test Score", "style": {"fontSize": "48px"}},
98-
"labels": {"style": {"fontSize": "36px"}},
100+
"title": {"text": "Test Score (%)", "style": {"fontSize": "40px", "color": INK}},
101+
"labels": {"style": {"fontSize": "32px", "color": INK_SOFT}},
99102
"gridLineWidth": 1,
100-
"gridLineColor": "rgba(0, 0, 0, 0.1)",
101-
"gridLineDashStyle": "Dash",
103+
"gridLineColor": GRID,
104+
"lineColor": INK_SOFT,
102105
"min": 40,
103106
"max": 105,
104107
}
105108

106-
# Legend - position at top right to avoid cutting off
107109
chart.options.legend = {
108110
"enabled": True,
109111
"align": "right",
110112
"verticalAlign": "top",
111113
"layout": "vertical",
112114
"x": -50,
113115
"y": 100,
114-
"itemStyle": {"fontSize": "36px"},
116+
"itemStyle": {"fontSize": "30px", "color": INK_SOFT},
117+
"backgroundColor": ELEVATED_BG,
118+
"borderColor": INK_SOFT,
119+
"borderWidth": 1,
115120
}
116121

117-
# Credits
118122
chart.options.credits = {"enabled": False}
119123

120-
# Tooltip
121124
chart.options.tooltip = {
122125
"headerFormat": "",
123-
"pointFormat": "<b>{point.category}</b><br/>Score: {point.y:.1f}",
126+
"pointFormat": "<b>{series.name}</b><br/>Score: {point.y:.1f}%",
124127
"style": {"fontSize": "24px"},
125128
}
126129

127-
# Add scatter series for each category (for legend)
130+
# Add scatter series per category
128131
for cat_idx, cat in enumerate(categories):
129132
series = ScatterSeries()
130133
series.name = cat
131-
series.color = colors[cat_idx]
132-
series.data = [{"x": float(pt["x"]), "y": pt["y"], "category": cat} for pt in strip_data if pt["category"] == cat]
133-
series.marker = {
134-
"radius": 14,
135-
"symbol": "circle",
136-
"fillColor": colors[cat_idx],
137-
"fillOpacity": 0.6, # Transparency for overlapping points
138-
"lineWidth": 2,
139-
"lineColor": "#ffffff",
140-
}
134+
series.color = OKABE_ITO[cat_idx]
135+
series.data = [{"x": float(pt["x"]), "y": pt["y"]} for pt in strip_data if pt["category"] == cat]
136+
series.marker = {"radius": 12, "symbol": "circle", "fillOpacity": 0.65, "lineWidth": 1, "lineColor": PAGE_BG}
141137
chart.add_series(series)
142138

143-
# Add mean markers as horizontal line segments
139+
# Mean markers — adaptive neutral (Okabe-Ito position 8)
144140
mean_series = ScatterSeries()
145141
mean_series.name = "Mean"
146142
mean_series.data = [{"x": float(i), "y": means[cat]} for i, cat in enumerate(categories)]
147-
mean_series.marker = {"radius": 18, "symbol": "diamond", "fillColor": "#E74C3C", "lineWidth": 3, "lineColor": "#ffffff"}
148-
mean_series.color = "#E74C3C"
143+
mean_series.color = NEUTRAL
144+
mean_series.marker = {"radius": 20, "symbol": "diamond", "lineWidth": 3, "lineColor": PAGE_BG}
149145
chart.add_series(mean_series)
150146

151-
# Download Highcharts JS (required for headless Chrome)
152-
highcharts_url = "https://code.highcharts.com/highcharts.js"
147+
# Download Highcharts JS inline (headless Chrome cannot load CDN from file://)
148+
highcharts_url = "https://cdn.jsdelivr.net/npm/highcharts/highcharts.js"
153149
with urllib.request.urlopen(highcharts_url, timeout=30) as response:
154150
highcharts_js = response.read().decode("utf-8")
155151

156-
# Generate HTML with inline scripts
157-
html_str = chart.to_js_literal()
158-
html_content = f"""<!DOCTYPE html>
152+
js_literal = chart.to_js_literal()
153+
154+
html_inline = f"""<!DOCTYPE html>
159155
<html>
160156
<head>
161157
<meta charset="utf-8">
162158
<script>{highcharts_js}</script>
163159
</head>
164-
<body style="margin:0;">
165-
<div id="container" style="width: 4800px; height: 2700px;"></div>
166-
<script>{html_str}</script>
160+
<body style="margin:0; background:{PAGE_BG};">
161+
<div id="container" style="width:4800px; height:2700px;"></div>
162+
<script>{js_literal}</script>
163+
</body>
164+
</html>"""
165+
166+
# Site artifact — CDN keeps file size small for served pages
167+
html_cdn = f"""<!DOCTYPE html>
168+
<html>
169+
<head>
170+
<meta charset="utf-8">
171+
<script src="https://cdn.jsdelivr.net/npm/highcharts/highcharts.js"></script>
172+
</head>
173+
<body style="margin:0; background:{PAGE_BG};">
174+
<div id="container" style="width:100%; height:100vh;"></div>
175+
<script>{js_literal}</script>
167176
</body>
168177
</html>"""
169178

170-
# Write temp HTML and take screenshot
179+
with open(f"plot-{THEME}.html", "w", encoding="utf-8") as f:
180+
f.write(html_cdn)
181+
182+
# Save
171183
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
172-
f.write(html_content)
184+
f.write(html_inline)
173185
temp_path = f.name
174186

175187
chrome_options = Options()
176-
chrome_options.add_argument("--headless")
188+
chrome_options.add_argument("--headless=new")
177189
chrome_options.add_argument("--no-sandbox")
178190
chrome_options.add_argument("--disable-dev-shm-usage")
179191
chrome_options.add_argument("--disable-gpu")
180-
chrome_options.add_argument("--window-size=4800,2800")
192+
chrome_options.add_argument("--window-size=4800,2900")
193+
chrome_options.add_argument("--hide-scrollbars")
181194

182195
driver = webdriver.Chrome(options=chrome_options)
196+
driver.set_window_size(4800, 2900)
183197
driver.get(f"file://{temp_path}")
184198
time.sleep(5)
185-
186-
# Take screenshot of just the chart container element
187199
container = driver.find_element("id", "container")
188-
container.screenshot("plot.png")
200+
container.screenshot(f"plot-{THEME}.png")
189201
driver.quit()
190202

191203
Path(temp_path).unlink()
192-
193-
# Also save HTML for interactive version
194-
with open("plot.html", "w", encoding="utf-8") as f:
195-
interactive_html = f"""<!DOCTYPE html>
196-
<html>
197-
<head>
198-
<meta charset="utf-8">
199-
<script src="https://code.highcharts.com/highcharts.js"></script>
200-
</head>
201-
<body style="margin:0;">
202-
<div id="container" style="width: 100%; height: 100vh;"></div>
203-
<script>{html_str}</script>
204-
</body>
205-
</html>"""
206-
f.write(interactive_html)

0 commit comments

Comments
 (0)