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
112 changes: 65 additions & 47 deletions plots/ohlc-bar/implementations/python/pygal.py
Original file line number Diff line number Diff line change
@@ -1,73 +1,94 @@
""" pyplots.ai
""" anyplot.ai
ohlc-bar: OHLC Bar Chart
Library: pygal 3.1.0 | Python 3.13.11
Quality: 90/100 | Created: 2026-01-08
Library: pygal 3.1.0 | Python 3.13.13
Quality: 88/100 | Updated: 2026-05-17
"""

import os
import sys

import numpy as np
import pandas as pd
import pygal
from pygal.style import Style


# Data - Generate realistic stock price data
np.random.seed(42)
n_days = 40
# Work around file-name shadowing: temporarily remove current dir from path
_cwd = sys.path[0] if sys.path and sys.path[0] == "" or sys.path[0] == "." else None
if sys.path and (sys.path[0] == "" or sys.path[0] == "."):
sys.path.pop(0)
_script_dir = os.path.dirname(os.path.abspath(__file__))
while _script_dir in sys.path:
sys.path.remove(_script_dir)

import pygal # noqa: E402
from pygal.style import Style # noqa: E402


# Theme tokens
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"

# Starting price and generate daily returns
start_price = 150.0
daily_returns = np.random.normal(0.001, 0.02, n_days)
# Okabe-Ito palette
BRAND = "#009E73" # Bullish (up bars)
ACCENT = "#D55E00" # Bearish (down bars)

# Data - Generate realistic stock price data with different seed/start than plotnine
np.random.seed(77)
n_days = 45

# Different starting price and return distribution
start_price = 185.0
# Use exponential drift (trending upward) instead of simple normal returns
drift = 0.0005
volatility = 0.018
daily_returns = drift + np.random.normal(0, volatility, n_days)

# Generate OHLC data
dates = pd.date_range("2024-06-01", periods=n_days, freq="B") # Business days
dates = pd.date_range("2024-07-01", periods=n_days, freq="B") # Business days
closes = start_price * np.cumprod(1 + daily_returns)
opens = np.roll(closes, 1)
opens[0] = start_price

# Generate highs and lows based on volatility
daily_volatility = np.abs(np.random.normal(0.01, 0.005, n_days))
daily_volatility = np.abs(np.random.normal(0.012, 0.006, n_days))
highs = np.maximum(opens, closes) * (1 + daily_volatility)
lows = np.minimum(opens, closes) * (1 - daily_volatility)

# Custom style for 4800x2700 px canvas
# Use green for up bars, red for down bars
up_color = "#27AE60"
down_color = "#E74C3C"

# Custom style with theme-adaptive colors
custom_style = Style(
background="white",
plot_background="white",
foreground="#333333",
foreground_strong="#333333",
foreground_subtle="#999999",
guide_stroke_color="#CCCCCC",
background=PAGE_BG,
plot_background=PAGE_BG,
foreground=INK,
foreground_strong=INK,
foreground_subtle=INK_MUTED,
guide_stroke_color=INK_MUTED,
opacity=".95",
opacity_hover=".85",
colors=(up_color, down_color),
title_font_size=60,
label_font_size=32,
major_label_font_size=28,
legend_font_size=32,
value_font_size=28,
tooltip_font_size=24,
stroke_width=6,
colors=(BRAND, ACCENT),
title_font_size=28,
label_font_size=18,
major_label_font_size=16,
legend_font_size=16,
value_font_size=14,
tooltip_font_size=14,
stroke_width=3,
)

# Create date labels for x-axis (every 5th date for clarity)
date_labels = {i: dates[i].strftime("%b %d") for i in range(0, n_days, 5)}

# Create XY chart with legend near plot
# Create XY chart
chart = pygal.XY(
width=4800,
height=2700,
style=custom_style,
title="ohlc-bar · pygal · pyplots.ai",
x_title="Date (Jun-Aug 2024)",
x_title="Date (Jul-Sep 2024)",
y_title="Price ($)",
show_legend=True,
legend_at_bottom=True,
legend_at_bottom_columns=2,
legend_box_size=24,
legend_box_size=20,
show_x_guides=False,
show_y_guides=True,
truncate_label=-1,
Expand All @@ -78,16 +99,12 @@
x_labels_major_every=1,
)

# Tick width for open/close marks (slightly wider for visibility)
# Tick width for open/close marks
tick_width = 0.4

# Build OHLC bar segments - each bar needs:
# 1. Vertical line from low to high
# 2. Open tick (left horizontal)
# 3. Close tick (right horizontal)

# Separate up and down bars into different series
up_bars = [] # Each bar as list of points with None separators
# Build OHLC bar segments
# Each bar needs: vertical line (low to high), open tick (left), close tick (right)
up_bars = []
down_bars = []

for i in range(n_days):
Expand Down Expand Up @@ -118,10 +135,11 @@
else: # Down bar (bearish)
down_bars.extend(bar_points)

# Add series with descriptive legend labels
# Add series with descriptive labels
chart.add("Bullish (Close ≥ Open)", up_bars)
chart.add("Bearish (Close < Open)", down_bars)

# Save outputs
chart.render_to_png("plot.png")
chart.render_to_file("plot.html")
chart.render_to_png(f"plot-{THEME}.png")
with open(f"plot-{THEME}.html", "wb") as f:
f.write(chart.render())
Loading
Loading