Skip to content
124 changes: 69 additions & 55 deletions plots/line-loss-training/implementations/python/highcharts.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
""" pyplots.ai
""" anyplot.ai
line-loss-training: Training Loss Curve
Library: highcharts unknown | Python 3.13.11
Quality: 92/100 | Created: 2025-12-31
Library: highcharts unknown | Python 3.13.13
Quality: 86/100 | Updated: 2026-05-14
"""

import os
import tempfile
import time
import urllib.request
Expand All @@ -17,6 +18,18 @@
from selenium.webdriver.chrome.options import Options


# 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.10)" if THEME == "light" else "rgba(240,239,232,0.10)"

# Okabe-Ito palette
BRAND = "#009E73" # Training loss (first series)
SECONDARY = "#D55E00" # Validation loss

# Data - Simulate training and validation loss over epochs
np.random.seed(42)
epochs = list(range(1, 51))
Expand All @@ -28,11 +41,10 @@
val_base = [2.5 * np.exp(-0.065 * e) + 0.30 for e in epochs]
noise = [np.random.randn() * 0.02 for _ in epochs]
val_loss = [v + n for v, n in zip(val_base, noise, strict=True)]
# Clear overfitting after epoch 28: validation loss increases while training keeps dropping
for i in range(28, 50):
val_loss[i] = val_loss[27] + (i - 27) * 0.35 / 22 + np.random.randn() * 0.015

# Find minimum validation loss epoch for annotation
# Find minimum validation loss epoch
min_val_idx = val_loss.index(min(val_loss))
min_val_epoch = epochs[min_val_idx]
min_val_loss_value = val_loss[min_val_idx]
Expand All @@ -41,113 +53,116 @@
chart = Chart(container="container")
chart.options = HighchartsOptions()

# Chart configuration for 4800x2700 px with increased bottom margin for x-axis labels
# Chart configuration
chart.options.chart = {
"type": "line",
"width": 4800,
"height": 2700,
"backgroundColor": "#ffffff",
"backgroundColor": PAGE_BG,
"style": {"fontFamily": "Arial, sans-serif"},
"marginBottom": 280,
"spacingBottom": 100,
"marginBottom": 100,
"spacingBottom": 50,
"spacingTop": 50,
}

# Title with larger font size for high resolution
# Title
chart.options.title = {
"text": "line-loss-training · highcharts · pyplots.ai",
"style": {"fontSize": "72px", "fontWeight": "bold"},
"text": "line-loss-training · highcharts · anyplot.ai",
"style": {"fontSize": "28px", "fontWeight": "medium", "color": INK},
}

# Subtitle indicating optimal stopping point
chart.options.subtitle = {
"text": f"Optimal stopping: Epoch {min_val_epoch} (Val Loss: {min_val_loss_value:.3f})",
"style": {"fontSize": "48px"},
"style": {"fontSize": "22px", "color": INK_SOFT},
}

# X-axis configuration with larger fonts
# X-axis configuration
chart.options.x_axis = {
"title": {"text": "Epoch", "style": {"fontSize": "48px"}, "margin": 30},
"labels": {"style": {"fontSize": "36px"}},
"lineWidth": 2,
"tickWidth": 2,
"title": {"text": "Epoch", "style": {"fontSize": "22px", "color": INK}, "margin": 15},
"labels": {"style": {"fontSize": "18px", "color": INK_SOFT}},
"lineColor": INK_SOFT,
"tickColor": INK_SOFT,
"gridLineColor": GRID,
"tickInterval": 5,
"min": 1,
"max": 50,
}

# Y-axis configuration
chart.options.y_axis = {
"title": {"text": "Cross-Entropy Loss", "style": {"fontSize": "48px"}},
"labels": {"style": {"fontSize": "36px"}},
"title": {"text": "Cross-Entropy Loss", "style": {"fontSize": "22px", "color": INK}},
"labels": {"style": {"fontSize": "18px", "color": INK_SOFT}},
"gridLineWidth": 1,
"gridLineColor": "#e0e0e0",
"gridLineColor": GRID,
"min": 0,
"max": 2.8,
}

# Legend configuration
# Legend configuration - moved to bottom for better layout
chart.options.legend = {
"enabled": True,
"itemStyle": {"fontSize": "42px"},
"layout": "vertical",
"align": "right",
"verticalAlign": "top",
"x": -50,
"y": 150,
"itemStyle": {"fontSize": "18px", "color": INK_SOFT},
"layout": "horizontal",
"align": "center",
"verticalAlign": "bottom",
"y": -40,
"borderWidth": 1,
"borderColor": "#e0e0e0",
"backgroundColor": "#ffffff",
"padding": 20,
"itemMarginTop": 10,
"itemMarginBottom": 10,
"borderColor": INK_SOFT,
"backgroundColor": ELEVATED_BG,
"padding": 12,
"itemMarginRight": 30,
}

# Plot options with increased line width for visibility at high resolution
# Plot options with appropriate line width for visibility
chart.options.plot_options = {
"line": {"lineWidth": 6, "marker": {"enabled": True, "radius": 12, "lineWidth": 3, "lineColor": "#ffffff"}}
"line": {"lineWidth": 3, "marker": {"enabled": True, "radius": 8, "lineWidth": 2, "lineColor": PAGE_BG}}
}

# Colorblind-safe colors (blue for training, yellow/gold for validation as per spec)
colors = ["#306998", "#FFD43B"]

# Add Training Loss series
# Add Training Loss series (first = brand color)
series1 = LineSeries()
series1.name = "Training Loss"
series1.data = [[e, t] for e, t in zip(epochs, train_loss, strict=True)]
series1.color = colors[0]
series1.color = BRAND
series1.marker = {"symbol": "circle"}
chart.add_series(series1)

# Add Validation Loss series
series2 = LineSeries()
series2.name = "Validation Loss"
series2.data = [[e, v] for e, v in zip(epochs, val_loss, strict=True)]
series2.color = colors[1]
series2.color = SECONDARY
series2.marker = {"symbol": "square"}
chart.add_series(series2)

# Download Highcharts JS for inline embedding
# Download Highcharts JS for inline embedding (required for file:// headless Chrome)
highcharts_url = "https://code.highcharts.com/highcharts.js"
with urllib.request.urlopen(highcharts_url, timeout=30) as response:
highcharts_js = response.read().decode("utf-8")

# Generate HTML with inline scripts
try:
req = urllib.request.Request(
highcharts_url, headers={"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36"}
)
with urllib.request.urlopen(req, timeout=60) as response:
highcharts_js = response.read().decode("utf-8")
except Exception as e:
raise RuntimeError(f"Failed to download Highcharts JS from {highcharts_url}: {e}") from e

# Generate HTML with INLINE scripts (critical for file:// headless Chrome)
html_str = chart.to_js_literal()
script_tag = f"<script>{highcharts_js}</script>"
html_content = f"""<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script>{highcharts_js}</script>
{script_tag}
</head>
<body style="margin:0;">
<body style="margin:0; background:{PAGE_BG};">
<div id="container" style="width: 4800px; height: 2700px;"></div>
<script>{html_str}</script>
</body>
</html>"""

# Save HTML version
with open("plot.html", "w", encoding="utf-8") as f:
# Save HTML version with theme suffix
with open(f"plot-{THEME}.html", "w", encoding="utf-8") as f:
f.write(html_content)

# Create PNG via headless Chrome
Expand All @@ -160,13 +175,12 @@
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,2800")
chrome_options.add_argument("--window-size=4800,2700")

driver = webdriver.Chrome(options=chrome_options)
driver.set_window_size(4800, 2700)
driver.get(f"file://{temp_path}")
time.sleep(5) # Wait for chart to render
driver.save_screenshot("plot.png")
time.sleep(10)
driver.save_screenshot(f"plot-{THEME}.png")
driver.quit()

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