diff --git a/plots/ohlc-bar/implementations/python/highcharts.py b/plots/ohlc-bar/implementations/python/highcharts.py index 6210e0a9c8..7949d2dce8 100644 --- a/plots/ohlc-bar/implementations/python/highcharts.py +++ b/plots/ohlc-bar/implementations/python/highcharts.py @@ -1,10 +1,11 @@ -""" pyplots.ai +""" anyplot.ai ohlc-bar: OHLC Bar Chart -Library: highcharts unknown | Python 3.13.11 -Quality: 91/100 | Created: 2026-01-09 +Library: highcharts unknown | Python 3.13.13 +Quality: 88/100 | Updated: 2026-05-23 """ import json +import os import tempfile import time import urllib.request @@ -12,18 +13,29 @@ from pathlib import Path import numpy as np +from PIL import Image from selenium import webdriver from selenium.webdriver.chrome.options import Options -# Data - 50 trading days of simulated stock prices +# 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)" + +# Semantic exception: finance — green=bullish/up, red=bearish/down (anyplot positions 1 and 3) +UP_COLOR = "#009E73" +DOWN_COLOR = "#B71D27" + +# Data — 50 trading days of simulated stock prices np.random.seed(42) -# Start price and generate OHLC data start_price = 150.0 n_days = 50 -# Generate realistic stock movements opens = [start_price] highs = [] lows = [] @@ -34,9 +46,8 @@ if i > 0: opens.append(open_price) - # Daily volatility daily_range = abs(np.random.randn() * 2) + 0.5 - direction = np.random.choice([-1, 1], p=[0.48, 0.52]) # Slight bullish bias + direction = np.random.choice([-1, 1], p=[0.48, 0.52]) close_price = open_price + direction * np.random.rand() * daily_range high_price = max(open_price, close_price) + abs(np.random.randn() * 0.8) @@ -48,110 +59,133 @@ opens = [round(o, 2) for o in opens] -# Generate dates (trading days, skip weekends) +# 5-day SMA for trend layer +sma_period = 5 +sma_closes = [] +for i in range(sma_period - 1, n_days): + avg = sum(closes[i - sma_period + 1 : i + 1]) / sma_period + sma_closes.append(round(avg, 2)) + +# Generate trading dates (skip weekends) start_date = datetime(2024, 6, 1) dates = [] current_date = start_date while len(dates) < n_days: - if current_date.weekday() < 5: # Monday to Friday + if current_date.weekday() < 5: dates.append(current_date) current_date += timedelta(days=1) -# Format data for Highcharts: [timestamp, open, high, low, close] +# Format as Highcharts timestamps: [timestamp_ms, open, high, low, close] ohlc_data = [] for i in range(n_days): - timestamp = int(dates[i].timestamp() * 1000) # JavaScript timestamp in ms + timestamp = int(dates[i].timestamp() * 1000) ohlc_data.append([timestamp, opens[i], highs[i], lows[i], closes[i]]) -# Chart options for Highcharts Stock OHLC chart -# Using colorblind-safe palette: Python Blue for down bars, Python Yellow for up bars +sma_data = [] +for i in range(sma_period - 1, n_days): + timestamp = int(dates[i].timestamp() * 1000) + sma_data.append([timestamp, sma_closes[i - (sma_period - 1)]]) + +# Net-change subtitle for data storytelling +net_change = closes[-1] - opens[0] +pct_change = net_change / opens[0] * 100 +sign = "+" if net_change >= 0 else "" +subtitle_text = f"Jun–Aug 2024 · Net change: {sign}${net_change:.2f} / {sign}{pct_change:.1f}%" + +title = "ohlc-bar · python · highcharts · anyplot.ai" + chart_options = { "chart": { "type": "ohlc", - "width": 4800, - "height": 2700, - "backgroundColor": "#ffffff", - "marginBottom": 220, - "marginLeft": 250, + "width": 3200, + "height": 1800, + "backgroundColor": PAGE_BG, + "plotBorderWidth": 0, + "marginBottom": 160, + "marginLeft": 200, "marginRight": 80, "marginTop": 150, - "style": {"fontFamily": "Arial, sans-serif"}, - }, - "title": { - "text": "ohlc-bar · highcharts · pyplots.ai", - "style": {"fontSize": "72px", "fontWeight": "bold", "color": "#333333"}, - "y": 60, + "style": {"fontFamily": "Arial, sans-serif", "color": INK}, }, + "title": {"text": title, "style": {"fontSize": "66px", "fontWeight": "600", "color": INK}, "y": 55}, + "subtitle": {"text": subtitle_text, "style": {"fontSize": "36px", "color": INK_SOFT}, "y": 110}, "xAxis": { "type": "datetime", - "title": {"text": "Date", "style": {"fontSize": "52px", "color": "#333333"}, "margin": 30}, - "labels": { - "style": {"fontSize": "36px", "color": "#333333"}, - "format": "{value:%b %d}", - "y": 45, - "step": 3, # Show every 3rd label to prevent overlap - }, + "title": {"text": "Date", "style": {"fontSize": "56px", "color": INK}, "margin": 25}, + "labels": {"style": {"fontSize": "44px", "color": INK_SOFT}, "format": "{value:%b %d}", "y": 40, "step": 3}, "gridLineWidth": 1, - "gridLineColor": "rgba(0, 0, 0, 0.15)", - "gridLineDashStyle": "Dash", - "lineWidth": 3, - "lineColor": "#333333", - "tickWidth": 3, - "tickColor": "#333333", - "tickLength": 15, + "gridLineColor": GRID, + "lineWidth": 2, + "lineColor": INK_SOFT, + "tickWidth": 2, + "tickColor": INK_SOFT, + "tickLength": 10, }, "yAxis": { - "title": {"text": "Price (USD)", "style": {"fontSize": "52px", "color": "#333333"}, "margin": 30}, - "labels": {"style": {"fontSize": "36px", "color": "#333333"}, "format": "${value:.0f}", "x": -15}, + "title": {"text": "Price (USD)", "style": {"fontSize": "56px", "color": INK}, "margin": 25}, + "labels": {"style": {"fontSize": "44px", "color": INK_SOFT}, "format": "${value:.0f}", "x": -15}, "gridLineWidth": 1, - "gridLineColor": "rgba(0, 0, 0, 0.15)", - "gridLineDashStyle": "Dash", - "lineWidth": 3, - "lineColor": "#333333", - "opposite": False, # Keep Y-axis on left side only + "gridLineColor": GRID, + "lineWidth": 2, + "lineColor": INK_SOFT, + "opposite": False, + }, + "legend": { + "enabled": True, + "itemStyle": {"color": INK_SOFT, "fontSize": "36px", "fontWeight": "normal"}, + "backgroundColor": "transparent", + "borderWidth": 0, + "align": "right", + "verticalAlign": "top", + "layout": "vertical", + "x": -80, + "y": 160, }, - "legend": {"enabled": False}, "tooltip": { "split": False, - "style": {"fontSize": "28px"}, + "style": {"fontSize": "32px"}, "headerFormat": "{point.x:%b %d, %Y}
", "pointFormat": "Open: ${point.open:.2f}
" + "High: ${point.high:.2f}
" + "Low: ${point.low:.2f}
" + "Close: ${point.close:.2f}", }, - "plotOptions": { - "ohlc": { - # Colorblind-safe: Python Yellow for up bars, Python Blue for down bars - "color": "#306998", # Python Blue for down bars (close < open) - "upColor": "#FFD43B", # Python Yellow for up bars (close > open) - "lineWidth": 5, # Bar line width - visible at large size - } - }, + "plotOptions": {"ohlc": {"color": DOWN_COLOR, "upColor": UP_COLOR, "lineWidth": 4}}, "rangeSelector": {"enabled": False}, "navigator": {"enabled": False}, "scrollbar": {"enabled": False}, "credits": {"enabled": False}, - "series": [{"type": "ohlc", "name": "Stock Price", "data": ohlc_data}], + "series": [ + {"type": "ohlc", "name": "Stock Price", "data": ohlc_data}, + { + "type": "line", + "name": "5-day SMA", + "data": sma_data, + "color": INK_SOFT, + "lineWidth": 2, + "dashStyle": "Dash", + "enableMouseTracking": False, + "marker": {"enabled": False}, + }, + ], } -# Download Highstock JS (includes OHLC support) -highstock_url = "https://code.highcharts.com/stock/highstock.js" -with urllib.request.urlopen(highstock_url, timeout=30) as response: +# Download Highstock JS (OHLC type lives in Highstock) +highstock_url = "https://cdn.jsdelivr.net/npm/highcharts/highstock.js" +req = urllib.request.Request(highstock_url, headers={"User-Agent": "Mozilla/5.0"}) +with urllib.request.urlopen(req, timeout=30) as response: highstock_js = response.read().decode("utf-8") -# Generate chart options JSON chart_options_json = json.dumps(chart_options) -# Generate HTML with inline scripts html_content = f""" - -
+ +