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
165 changes: 101 additions & 64 deletions plots/candlestick-basic/implementations/letsplot.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,21 @@
""" pyplots.ai
candlestick-basic: Basic Candlestick Chart
Library: letsplot 4.8.2 | Python 3.13.11
Quality: 92/100 | Created: 2025-12-23
Library: letsplot 4.8.2 | Python 3.14.3
Quality: 91/100 | Updated: 2026-02-24
"""

import cairosvg
import numpy as np
import pandas as pd
from lets_plot import (
LetsPlot,
aes,
element_blank,
element_text,
geom_rect,
geom_segment,
ggplot,
ggsize,
labs,
scale_x_continuous,
theme,
theme_minimal,
)
from lets_plot.export import ggsave
from lets_plot import * # noqa: F403


LetsPlot.setup_html()
LetsPlot.setup_html() # noqa: F405

Comment on lines +9 to 13
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file suppresses F405 repeatedly on many individual lines due to the from lets_plot import * import, which adds a lot of noise and makes the example harder to read. Other letsplot examples in this repo typically use a file-level # ruff: noqa: F405 (or explicit imports) to avoid per-line suppressions; consider switching to that approach here.

Copilot uses AI. Check for mistakes.
# Data - simulated 30 trading days of stock OHLC data
np.random.seed(42)
n_days = 30

dates = pd.date_range(start="2024-01-02", periods=n_days, freq="B") # Business days
dates = pd.date_range(start="2024-01-02", periods=n_days, freq="B")

# Generate realistic OHLC data with random walk
price = 100.0
Expand All @@ -52,60 +37,112 @@

df = pd.DataFrame({"date": dates, "open": opens, "high": highs, "low": lows, "close": closes})

# Calculate candlestick properties
df["bullish"] = df["close"] >= df["open"]
df["color"] = df["bullish"].map({True: "#22C55E", False: "#EF4444"}) # Green/Red
# Candlestick properties
df["direction"] = np.where(df["close"] >= df["open"], "Bullish", "Bearish")
df["body_low"] = df[["open", "close"]].min(axis=1)
df["body_high"] = df[["open", "close"]].max(axis=1)
df["x"] = range(len(df)) # Numeric x for positioning
df["x"] = range(len(df))
df["xmin"] = df["x"] - 0.35
df["xmax"] = df["x"] + 0.35
df["date_str"] = df["date"].dt.strftime("%b %d")

# 5-day simple moving average for trend storytelling
df["sma5"] = df["close"].rolling(window=5).mean()
sma_df = df.dropna(subset=["sma5"]).copy()

# Peak price focal point
peak_idx = int(df["high"].idxmax())
peak_x = df.loc[peak_idx, "x"]
peak_y = df.loc[peak_idx, "high"]
peak_df = pd.DataFrame({"x": [peak_x], "y": [peak_y], "label": [f"Peak ${peak_y:.0f}"]})

bull_color = "#2271B5" # Blue for bullish (up)
bear_color = "#D55E00" # Orange for bearish (down) — colorblind-safe
sma_color = "#555555"

# Tick label positions: every 5th trading day
tick_pos = list(range(0, n_days, 5))
tick_labels = [dates[i].strftime("%b %d") for i in tick_pos]

# Tooltip template for interactive HTML export
tip_fmt = (
layer_tooltips() # noqa: F405
.line("@date_str")
.line("Open|$@open")
.line("High|$@high")
.line("Low|$@low")
.line("Close|$@close")
)

# Plot
# Plot — unified color mapping for bull/bear direction
plot = (
ggplot()
# Wicks (high-low lines)
+ geom_segment(aes(x="x", xend="x", y="low", yend="high"), data=df, color="#666666", size=1)
# Bullish candle bodies (green)
+ geom_rect(
aes(xmin="xmin", xmax="xmax", ymin="body_low", ymax="body_high"),
data=df[df["bullish"]],
fill="#22C55E",
color="#22C55E",
size=0.5,
ggplot(df) # noqa: F405
# Wicks (high-low lines) colored by direction
+ geom_segment( # noqa: F405
aes(x="x", xend="x", y="low", yend="high", color="direction"), # noqa: F405
size=0.9,
tooltips=tip_fmt,
)
# Bearish candle bodies (red)
+ geom_rect(
aes(xmin="xmin", xmax="xmax", ymin="body_low", ymax="body_high"),
data=df[~df["bullish"]],
fill="#EF4444",
color="#EF4444",
# Candle bodies colored by direction
+ geom_rect( # noqa: F405
aes( # noqa: F405
xmin="xmin", xmax="xmax", ymin="body_low", ymax="body_high", fill="direction", color="direction"
),
size=0.5,
tooltips=tip_fmt,
)
# 5-day SMA trend line — guides the viewer through the price narrative
+ geom_line( # noqa: F405
aes(x="x", y="sma5"), # noqa: F405
data=sma_df,
color=sma_color,
size=1.0,
alpha=0.55,
linetype="dashed",
tooltips="none",
)
# Peak annotation — focal point for storytelling
+ geom_point( # noqa: F405
aes(x="x", y="y"), # noqa: F405
data=peak_df,
size=6,
shape=18,
color="#222222",
)
# Labels and theme
+ scale_x_continuous(
breaks=list(range(0, n_days, 5)), labels=[df["date"].iloc[i].strftime("%b %d") for i in range(0, n_days, 5)]
+ geom_text( # noqa: F405
aes(x="x", y="y", label="label"), # noqa: F405
data=peak_df,
size=13,
color="#222222",
nudge_y=1.8,
fontface="bold",
)
+ labs(x="Date", y="Price ($)", title="candlestick-basic · letsplot · pyplots.ai")
+ theme_minimal()
+ theme(
axis_title=element_text(size=20),
axis_text=element_text(size=16),
plot_title=element_text(size=24),
panel_grid_major_x=element_blank(),
panel_grid_minor=element_blank(),
+ scale_fill_manual(values={"Bullish": bull_color, "Bearish": bear_color}) # noqa: F405
+ scale_color_manual(values={"Bullish": bull_color, "Bearish": bear_color}) # noqa: F405
+ scale_x_continuous(breaks=tick_pos, labels=tick_labels, expand=[0.02, 0]) # noqa: F405
+ scale_y_continuous(expand=[0.12, 0]) # noqa: F405 — room for peak label
+ labs( # noqa: F405
x="Trading Day (Jan\u2013Feb 2024)",
y="Price ($)",
title="candlestick-basic \u00b7 letsplot \u00b7 pyplots.ai",
subtitle="Simulated 30-day equity prices \u2014 5-day moving average (dashed)",
)
+ ggsize(1600, 900)
+ theme_minimal() # noqa: F405
+ theme( # noqa: F405
axis_title=element_text(size=20, color="#333333"), # noqa: F405
axis_text=element_text(size=16, color="#555555"), # noqa: F405
plot_title=element_text(size=24, color="#1a1a1a", face="bold"), # noqa: F405
plot_subtitle=element_text(size=17, color="#666666"), # noqa: F405
panel_grid_major_x=element_line(color="#ececec", size=0.3), # noqa: F405
panel_grid_major_y=element_line(color="#e0e0e0", size=0.4), # noqa: F405
panel_grid_minor=element_blank(), # noqa: F405
axis_ticks=element_blank(), # noqa: F405
plot_background=element_rect(fill="white", color="white"), # noqa: F405
legend_position="none",
)
+ ggsize(1600, 900) # noqa: F405
)

# Save HTML for interactive version
ggsave(plot, "plot.html", path=".")

# Save SVG first, then convert to PNG
ggsave(plot, "plot.svg", path=".", w=1600, h=900, unit="px")

# Convert SVG to PNG using cairosvg
with open("plot.svg", "r") as f:
svg_content = f.read()

cairosvg.svg2png(bytestring=svg_content.encode("utf-8"), write_to="plot.png", output_width=4800, output_height=2700)
# Save
ggsave(plot, "plot.png", path=".", scale=3) # noqa: F405
ggsave(plot, "plot.html", path=".") # noqa: F405
Loading