Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
258 changes: 189 additions & 69 deletions plots/dumbbell-basic/implementations/python/highcharts.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
""" pyplots.ai
""" anyplot.ai
dumbbell-basic: Basic Dumbbell Chart
Library: highcharts unknown | Python 3.13.11
Quality: 91/100 | Created: 2025-12-23
Library: highcharts unknown | Python 3.14.4
Quality: 85/100 | Updated: 2026-04-26
"""

import base64
import json
import os
import tempfile
import time
import urllib.request
Expand All @@ -14,7 +16,20 @@
from selenium.webdriver.chrome.options import Options


# Data - Employee satisfaction scores before and after policy changes
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"
GRID = "rgba(26,26,23,0.10)" if THEME == "light" else "rgba(240,239,232,0.10)"
CONNECTOR = "rgba(26,26,23,0.40)" if THEME == "light" else "rgba(240,239,232,0.40)"

BEFORE_COLOR = "#009E73" # Okabe-Ito 1 — brand
AFTER_COLOR = "#D55E00" # Okabe-Ito 2

# Employee satisfaction scores (0-100) before and after policy changes.
# Mix of strong gains, modest gains, and two declines for full coverage.
categories = [
"Engineering",
"Sales",
Expand All @@ -23,90 +38,186 @@
"Human Resources",
"Finance",
"Operations",
"Research & Development",
"Research & Dev",
"Legal",
"IT Support",
]
before_scores = [65, 58, 72, 45, 68, 61, 53, 70]
after_scores = [82, 75, 85, 78, 80, 73, 71, 88]
before_scores = [65, 58, 72, 45, 68, 61, 53, 70, 76, 64]
after_scores = [82, 75, 85, 78, 80, 73, 71, 88, 68, 59]

# Sort by the magnitude of change (descending) to reveal patterns
changes = [after - before for before, after in zip(before_scores, after_scores, strict=True)]
sorted_data = sorted(
zip(categories, before_scores, after_scores, changes, strict=True), key=lambda x: x[3], reverse=True
)
categories = [item[0] for item in sorted_data]
before_scores = [item[1] for item in sorted_data]
after_scores = [item[2] for item in sorted_data]
# Sort by improvement (descending): biggest gains on top, declines at bottom.
changes = [a - b for b, a in zip(before_scores, after_scores, strict=True)]
order = sorted(range(len(categories)), key=lambda i: changes[i], reverse=True)
categories = [categories[i] for i in order]
before_scores = [before_scores[i] for i in order]
after_scores = [after_scores[i] for i in order]
changes = [changes[i] for i in order]

# Prepare data for dumbbell chart
# Build dumbbell points. To keep the green=before / orange=after semantics
# stable even when satisfaction drops (low marker is the smaller value, so
# "after" lands on it), we override per-point colors when before > after.
dumbbell_data = []
for i, (before, after) in enumerate(zip(before_scores, after_scores, strict=True)):
dumbbell_data.append({"x": i, "low": before, "high": after})
low = min(before, after)
high = max(before, after)
if before <= after:
dumbbell_data.append({"x": i, "low": low, "high": high})
else:
dumbbell_data.append(
{
"x": i,
"low": low,
"high": high,
"lowColor": AFTER_COLOR, # smaller value here is "after"
"color": BEFORE_COLOR, # larger value here is "before"
}
)

# Chart options for horizontal dumbbell chart
chart_options = {
"chart": {
"type": "dumbbell",
"inverted": True, # Horizontal orientation
"inverted": True,
"width": 4800,
"height": 2700,
"backgroundColor": "#ffffff",
"marginLeft": 400,
"marginBottom": 150,
"style": {"fontFamily": "Arial, sans-serif"},
"backgroundColor": PAGE_BG,
"marginLeft": 520,
"marginRight": 220,
"marginTop": 320,
"marginBottom": 200,
"spacingTop": 60,
"style": {"fontFamily": "Arial, sans-serif", "color": INK},
},
"title": {
"text": "Employee Satisfaction Before/After · dumbbell-basic · highcharts · pyplots.ai",
"style": {"fontSize": "52px", "fontWeight": "bold"},
"text": "dumbbell-basic · highcharts · anyplot.ai",
"align": "left",
"x": 80,
"style": {"fontSize": "56px", "fontWeight": "500", "color": INK},
},
"subtitle": {
"text": "Satisfaction scores before and after policy changes by department",
"style": {"fontSize": "34px", "color": "#666666"},
"text": "Employee satisfaction scores before and after policy changes (by department)",
"align": "left",
"x": 80,
"style": {"fontSize": "30px", "color": INK_SOFT},
},
"xAxis": {
"categories": categories,
"title": {"text": None}, # Categories are self-explanatory
"labels": {"style": {"fontSize": "32px"}},
"title": {"text": None},
"labels": {"style": {"fontSize": "32px", "color": INK}},
"lineColor": INK_SOFT,
"tickColor": INK_SOFT,
"tickWidth": 0,
"gridLineWidth": 0,
"minPadding": 0.05,
"maxPadding": 0.05,
"startOnTick": False,
"endOnTick": False,
},
"yAxis": {
"min": 40,
"min": 35,
"max": 95,
"title": {"text": "Satisfaction Score", "style": {"fontSize": "36px"}},
"labels": {"style": {"fontSize": "28px"}},
"gridLineColor": "#e0e0e0",
"gridLineDashStyle": "Dash",
"tickInterval": 10,
"title": {"text": "Satisfaction Score (0–100)", "style": {"fontSize": "36px", "color": INK}, "margin": 50},
"labels": {"style": {"fontSize": "30px", "color": INK_SOFT}},
"lineColor": INK_SOFT,
"tickColor": INK_SOFT,
"gridLineColor": GRID,
"gridLineWidth": 1,
"opposite": False,
},
"legend": {
"enabled": True,
"align": "right",
"verticalAlign": "top",
"layout": "vertical",
"x": -50,
"y": 100,
"itemStyle": {"fontSize": "28px"},
"symbolHeight": 20,
"symbolWidth": 40,
"layout": "horizontal",
"x": -20,
"y": 80,
"itemStyle": {"fontSize": "32px", "color": INK, "fontWeight": "500"},
"itemDistance": 70,
"backgroundColor": ELEVATED_BG,
"borderColor": GRID,
"borderWidth": 1,
"borderRadius": 6,
"padding": 22,
"symbolHeight": 30,
"symbolWidth": 30,
"symbolRadius": 15,
},
"plotOptions": {
"dumbbell": {
"connectorWidth": 5,
"connectorColor": "#888888",
"lowColor": "#306998", # Python Blue for "before"
"color": "#FFD43B", # Python Yellow for "after"
"marker": {"radius": 18},
"dataLabels": {
"enabled": True,
"style": {"fontSize": "24px", "fontWeight": "bold", "textOutline": "none"},
"y": 0,
},
}
"connectorWidth": 6,
"connectorColor": CONNECTOR,
"lowColor": BEFORE_COLOR,
"color": AFTER_COLOR,
"marker": {"radius": 24, "lineWidth": 3, "lineColor": PAGE_BG},
"lowMarker": {"radius": 24, "lineWidth": 3, "lineColor": PAGE_BG},
"dataLabels": [
{
"enabled": True,
"format": "{point.high}",
"align": "left",
"x": 38,
"verticalAlign": "middle",
"style": {"fontSize": "28px", "color": INK_SOFT, "fontWeight": "500", "textOutline": "none"},
},
{
"enabled": True,
"format": "{point.low}",
"align": "right",
"x": -38,
"verticalAlign": "middle",
"style": {"fontSize": "28px", "color": INK_SOFT, "fontWeight": "500", "textOutline": "none"},
},
],
},
"scatter": {"marker": {"radius": 18, "lineWidth": 2, "lineColor": PAGE_BG, "symbol": "circle"}},
},
"series": [
{
"type": "scatter",
"name": "Before",
"color": BEFORE_COLOR,
"data": [],
"showInLegend": True,
"marker": {"radius": 18, "symbol": "circle"},
"enableMouseTracking": False,
},
{
"type": "scatter",
"name": "After",
"color": AFTER_COLOR,
"data": [],
"showInLegend": True,
"marker": {"radius": 18, "symbol": "circle"},
"enableMouseTracking": False,
},
{
"type": "dumbbell",
"name": "Satisfaction change",
"data": dumbbell_data,
"lowColor": BEFORE_COLOR,
"color": AFTER_COLOR,
"showInLegend": False,
},
],
"credits": {"enabled": False},
"tooltip": {
"shared": False,
"useHTML": True,
"backgroundColor": ELEVATED_BG,
"borderColor": GRID,
"style": {"color": INK, "fontSize": "20px"},
"headerFormat": "<b>{point.key}</b><br/>",
"pointFormat": (
f"<span style='color:{BEFORE_COLOR}'>●</span> Before: <b>{{point.low}}</b><br/>"
f"<span style='color:{AFTER_COLOR}'>●</span> After: <b>{{point.high}}</b>"
),
},
"series": [{"name": "Before → After", "data": dumbbell_data, "lowColor": "#306998", "color": "#FFD43B"}],
}

# Download Highcharts JS and required modules for inline embedding
highcharts_url = "https://code.highcharts.com/highcharts.js"
highcharts_more_url = "https://code.highcharts.com/highcharts-more.js"
dumbbell_url = "https://code.highcharts.com/modules/dumbbell.js"
# Highcharts core + dumbbell module + highcharts-more (required by dumbbell).
highcharts_url = "https://cdn.jsdelivr.net/npm/highcharts@12/highcharts.js"
highcharts_more_url = "https://cdn.jsdelivr.net/npm/highcharts@12/highcharts-more.js"
dumbbell_url = "https://cdn.jsdelivr.net/npm/highcharts@12/modules/dumbbell.js"

with urllib.request.urlopen(highcharts_url, timeout=30) as response:
highcharts_js = response.read().decode("utf-8")
Expand All @@ -115,7 +226,6 @@
with urllib.request.urlopen(dumbbell_url, timeout=30) as response:
dumbbell_js = response.read().decode("utf-8")

# Generate HTML with inline scripts
chart_options_json = json.dumps(chart_options)
html_content = f"""<!DOCTYPE html>
<html>
Expand All @@ -125,26 +235,21 @@
<script>{highcharts_more_js}</script>
<script>{dumbbell_js}</script>
</head>
<body style="margin:0;">
<body style="margin:0; background:{PAGE_BG};">
<div id="container" style="width: 4800px; height: 2700px;"></div>
<script>
document.addEventListener('DOMContentLoaded', function() {{
Highcharts.chart('container', {chart_options_json});
}});
Highcharts.chart('container', {chart_options_json});
</script>
</body>
</html>"""

# Write temp HTML file
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
with open(f"plot-{THEME}.html", "w", encoding="utf-8") as f:
f.write(html_content)
temp_path = f.name

# Also save the HTML for interactive viewing
with open("plot.html", "w", encoding="utf-8") as f:
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
f.write(html_content)
temp_path = f.name

# Take screenshot with headless Chrome
chrome_options = Options()
chrome_options.add_argument("--headless")
chrome_options.add_argument("--no-sandbox")
Expand All @@ -153,10 +258,25 @@
chrome_options.add_argument("--window-size=4800,2700")

driver = webdriver.Chrome(options=chrome_options)
driver.execute_cdp_cmd(
"Emulation.setDeviceMetricsOverride", {"width": 4800, "height": 2700, "deviceScaleFactor": 1, "mobile": False}
)
driver.get(f"file://{temp_path}")
time.sleep(5)
driver.save_screenshot("plot.png")

# Full-page CDP capture so the y-axis title at the bottom is not clipped
# by Chrome's reduced rendering viewport.
result = driver.execute_cdp_cmd(
"Page.captureScreenshot",
{
"captureBeyondViewport": True,
"clip": {"x": 0, "y": 0, "width": 4800, "height": 2700, "scale": 1},
"format": "png",
},
)
with open(f"plot-{THEME}.png", "wb") as f:
f.write(base64.b64decode(result["data"]))

driver.quit()

# Clean up temp file
Path(temp_path).unlink()
Loading
Loading