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
193 changes: 85 additions & 108 deletions plots/bar-basic/implementations/python/highcharts.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
""" pyplots.ai
""" anyplot.ai
bar-basic: Basic Bar Chart
Library: highcharts 1.10.3 | Python 3.14
Quality: 94/100 | Created: 2025-12-23
Library: highcharts unknown | Python 3.13.13
Quality: 94/100 | Updated: 2026-05-28
"""

import os
import re
import tempfile
import time
Expand All @@ -12,117 +13,111 @@

from highcharts_core.chart import Chart
from highcharts_core.options import HighchartsOptions
from highcharts_core.options.annotations import Annotation
from highcharts_core.options.series.bar import ColumnSeries
from PIL import Image
from selenium import webdriver
from selenium.webdriver.chrome.options import Options


# Data - Product sales by category (realistic retail scenario, sorted descending)
# Theme tokens
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"
GRID = "rgba(26,26,23,0.15)" if THEME == "light" else "rgba(240,239,232,0.15)"
ANYPLOT_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"

ANYPLOT_PALETTE = ["#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030", "#2ABCCD", "#954477", "#99B314"]
BRAND = ANYPLOT_PALETTE[0]

# Data — product sales by category, sorted descending
categories = ["Electronics", "Clothing", "Home & Garden", "Sports", "Books", "Toys"]
values = [4800, 3100, 2200, 1700, 950, 480]
avg_sales = sum(values) / len(values)

# Build chart using highcharts-core Python API
title = "bar-basic · python · highcharts · anyplot.ai" # 44 chars — no scaling

# Chart
chart = Chart(container="container")
chart.options = HighchartsOptions()

# Chart configuration
chart.options.chart = {
"type": "column",
"width": 4800,
"height": 2700,
"backgroundColor": "#ffffff",
"marginBottom": 280,
"marginTop": 140,
"width": 3200,
"height": 1800,
"backgroundColor": PAGE_BG,
"marginBottom": 220,
"marginTop": 100,
"marginLeft": 220,
"marginRight": 200,
"marginRight": 140,
"plotBorderWidth": 0,
"style": {"fontFamily": "Arial, Helvetica, sans-serif"},
}

# Title
chart.options.title = {
"text": "bar-basic \u00b7 highcharts \u00b7 pyplots.ai",
"style": {"fontSize": "52px", "fontWeight": "bold", "color": "#2c3e50"},
"margin": 50,
}
chart.options.title = {"text": title, "style": {"fontSize": "66px", "fontWeight": "bold", "color": INK}, "margin": 36}

# Subtitle for storytelling context
chart.options.subtitle = {
"text": "Electronics dominates with 4,800 units \u2014 10\u00d7 more than Toys",
"style": {"fontSize": "32px", "color": "#7f8c8d", "fontWeight": "normal"},
"margin": 30,
"text": "Electronics dominates with 4,800 units 10× more than Toys",
"style": {"fontSize": "44px", "color": INK_SOFT, "fontWeight": "normal"},
"margin": 24,
}

# X-axis
chart.options.x_axis = {
"categories": categories,
"title": {"text": "Product Category", "style": {"fontSize": "36px", "color": "#555555"}, "margin": 20},
"labels": {"style": {"fontSize": "30px", "color": "#555555"}},
"lineColor": "#cccccc",
"tickColor": "#cccccc",
"title": {"text": "Product Category", "style": {"fontSize": "56px", "color": INK}, "margin": 20},
"labels": {"style": {"fontSize": "44px", "color": INK_SOFT}},
"lineColor": INK_SOFT,
"tickColor": INK_SOFT,
"tickLength": 8,
"gridLineColor": GRID,
}

# Y-axis with plotLine showing average
chart.options.y_axis = {
"title": {"text": "Sales (Units)", "style": {"fontSize": "36px", "color": "#555555"}, "margin": 15},
"labels": {"style": {"fontSize": "28px", "color": "#555555"}, "format": "{value:,.0f}"},
"title": {"text": "Sales (Units)", "style": {"fontSize": "56px", "color": INK}, "margin": 15},
"labels": {"style": {"fontSize": "44px", "color": INK_SOFT}, "format": "{value:,.0f}"},
"max": 5200,
"endOnTick": False,
"tickInterval": 1000,
"gridLineColor": "#e8e8e8",
"gridLineColor": GRID,
"gridLineWidth": 1,
"gridLineDashStyle": "Dot",
"plotLines": [
{
"value": avg_sales,
"color": "#e74c3c",
"color": INK_SOFT,
"width": 3,
"dashStyle": "LongDash",
"zIndex": 5,
"label": {
"text": f"Average: {avg_sales:,.0f} units",
"text": f"Avg: {avg_sales:,.0f} units",
"align": "right",
"x": -30,
"x": -12,
"y": -14,
"style": {"fontSize": "26px", "color": "#e74c3c", "fontWeight": "bold", "fontStyle": "italic"},
"style": {"fontSize": "40px", "color": INK_SOFT, "fontWeight": "bold"},
},
}
],
}

# Tooltip with custom formatting
chart.options.tooltip = {
"headerFormat": '<span style="font-size:24px;font-weight:bold">{point.key}</span><br/>',
"pointFormat": '<span style="font-size:22px">Sales: <b>{point.y:,.0f}</b> units</span>',
"backgroundColor": "rgba(255, 255, 255, 0.95)",
"borderColor": "#306998",
"borderRadius": 8,
"borderWidth": 2,
"shadow": {"color": "rgba(0,0,0,0.1)", "offsetX": 1, "offsetY": 2, "width": 3},
"style": {"fontSize": "22px"},
"headerFormat": '<span style="font-size:44px;font-weight:bold">{point.key}</span><br/>',
"pointFormat": '<span style="font-size:40px">Sales: <b>{point.y:,.0f}</b> units</span>',
"backgroundColor": ELEVATED_BG,
"borderColor": INK_SOFT,
"borderRadius": 6,
"borderWidth": 1,
}

# Plot options for column styling
chart.options.plot_options = {
"column": {
"pointPadding": 0.12,
"borderWidth": 0,
"groupPadding": 0.08,
"borderRadius": 6,
"shadow": {"color": "rgba(0,0,0,0.08)", "offsetX": 2, "offsetY": 3, "width": 5},
}
"column": {"pointPadding": 0.12, "borderWidth": 0, "groupPadding": 0.08, "borderRadius": 4}
}

# Highlight top performer with darker shade, rest with standard Python Blue
data_points = [
{"y": values[0], "color": "#1a4971"} # Darker shade for top performer
]
for v in values[1:]:
data_points.append({"y": v, "color": "#306998"})
# Top performer in brand green; remaining bars in muted neutral with rank-fade opacity
data_points = [{"y": values[0], "color": BRAND}]
muted_opacities = [0.90, 0.78, 0.66, 0.54, 0.42]
for i, v in enumerate(values[1:]):
data_points.append({"y": v, "color": ANYPLOT_MUTED, "opacity": muted_opacities[i]})

# Create series using highcharts-core ColumnSeries
series = ColumnSeries.from_dict(
{
"data": data_points,
Expand All @@ -131,90 +126,72 @@
"dataLabels": {
"enabled": True,
"format": "{y:,.0f}",
"style": {"fontSize": "28px", "fontWeight": "bold", "color": "#2c3e50", "textOutline": "2px white"},
"style": {"fontSize": "44px", "fontWeight": "bold", "color": INK, "textOutline": f"2px {PAGE_BG}"},
"y": -8,
},
}
)
chart.add_series(series)

# Annotation callout on top performer
chart.options.annotations = [
Annotation.from_dict(
{
"labels": [
{
"point": {"x": 0, "y": 4800, "xAxis": 0, "yAxis": 0},
"text": "\u2b50 Top Seller",
"y": -45,
"style": {"fontSize": "26px", "fontWeight": "bold", "color": "#1a4971"},
}
],
"labelOptions": {
"backgroundColor": "rgba(255, 255, 255, 0.92)",
"borderColor": "#1a4971",
"borderWidth": 2,
"borderRadius": 8,
"padding": 12,
"shape": "callout",
},
"draggable": "",
}
)
]

# Disable legend (single series) and credits
chart.options.legend = {"enabled": False}
chart.options.credits = {"enabled": False}

# Download Highcharts JS for inline embedding (required for headless Chrome)
highcharts_url = "https://code.highcharts.com/highcharts.js"
annotations_url = "https://code.highcharts.com/modules/annotations.js"
with urllib.request.urlopen(highcharts_url, timeout=30) as response:
req = urllib.request.Request(
"https://code.highcharts.com/highcharts.js",
headers={"User-Agent": "Mozilla/5.0", "Referer": "https://www.highcharts.com/"},
)
with urllib.request.urlopen(req, timeout=30) as response:
highcharts_js = response.read().decode("utf-8")
with urllib.request.urlopen(annotations_url, timeout=30) as response:
annotations_js = response.read().decode("utf-8")

# Generate HTML using Chart.to_js_literal()
chart_js = chart.to_js_literal()
# Fix format strings: highcharts-core omits quotes around Highcharts format templates
# Wrap bare Highcharts format templates in single quotes (to_js_literal may omit them)
chart_js = re.sub(r"format: (\{[^}]+\})", r"format: '\1'", chart_js)

html_content = f"""<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script>{highcharts_js}</script>
<script>{annotations_js}</script>
</head>
<body style="margin:0;">
<div id="container" style="width: 4800px; height: 2700px;"></div>
<body style="margin:0; background:{PAGE_BG};">
<div id="container" style="width: 3200px; height: 1800px;"></div>
<script>{chart_js}</script>
</body>
</html>"""

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

# Save HTML for interactive viewing
with open("plot.html", "w", encoding="utf-8") as f:
# Write temp HTML and capture PNG via headless Chrome
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("--headless=new")
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-dev-shm-usage")
chrome_options.add_argument("--disable-gpu")
chrome_options.add_argument("--window-size=4800,2700")
chrome_options.add_argument("--hide-scrollbars")
chrome_options.add_argument("--window-size=3200,1800")

driver = webdriver.Chrome(options=chrome_options)
# CDP override is authoritative — --window-size alone loses ~139 px to Chrome chrome
driver.execute_cdp_cmd(
"Emulation.setDeviceMetricsOverride", {"width": 3200, "height": 1800, "deviceScaleFactor": 1, "mobile": False}
)
driver.get(f"file://{temp_path}")
time.sleep(5)
driver.save_screenshot("plot.png")
driver.save_screenshot(f"plot-{THEME}.png")
driver.quit()

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

# Pin to exact 3200×1800 to satisfy the post-render gate
_img = Image.open(f"plot-{THEME}.png").convert("RGB")
if _img.size != (3200, 1800):
_norm = Image.new("RGB", (3200, 1800), PAGE_BG)
_norm.paste(_img, ((3200 - _img.size[0]) // 2, (1800 - _img.size[1]) // 2))
_norm.save(f"plot-{THEME}.png")
Loading
Loading