Skip to content

Commit 9eca40f

Browse files
feat(letsplot): implement candlestick-volume (#3098)
## Implementation: `candlestick-volume` - letsplot Implements the **letsplot** version of `candlestick-volume`. **File:** `plots/candlestick-volume/implementations/letsplot.py` **Parent Issue:** #3068 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20620318739)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 2af41e9 commit 9eca40f

2 files changed

Lines changed: 158 additions & 0 deletions

File tree

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
""" pyplots.ai
2+
candlestick-volume: Stock Candlestick Chart with Volume
3+
Library: letsplot 4.8.2 | Python 3.13.11
4+
Quality: 91/100 | Created: 2025-12-31
5+
"""
6+
7+
import numpy as np
8+
import pandas as pd
9+
from lets_plot import *
10+
11+
12+
LetsPlot.setup_html()
13+
14+
15+
# Format volume as human-readable (e.g., 5.0M instead of 5000000)
16+
def format_volume(val):
17+
if val >= 1_000_000:
18+
return f"{val / 1_000_000:.1f}M"
19+
elif val >= 1_000:
20+
return f"{val / 1_000:.0f}K"
21+
return str(int(val))
22+
23+
24+
# Data - 60 trading days of synthetic stock data
25+
np.random.seed(42)
26+
n_days = 60
27+
dates = pd.date_range("2024-01-02", periods=n_days, freq="B")
28+
29+
# Generate realistic price movement with trend and volatility
30+
returns = np.random.normal(0.001, 0.02, n_days)
31+
close_prices = 150 * np.cumprod(1 + returns)
32+
33+
# Generate OHLC from close prices
34+
open_prices = np.roll(close_prices, 1)
35+
open_prices[0] = 150
36+
high_prices = np.maximum(open_prices, close_prices) * (1 + np.abs(np.random.normal(0, 0.01, n_days)))
37+
low_prices = np.minimum(open_prices, close_prices) * (1 - np.abs(np.random.normal(0, 0.01, n_days)))
38+
39+
# Generate volume with some correlation to price movement
40+
base_volume = 5_000_000
41+
volatility = np.abs(close_prices - open_prices) / open_prices
42+
volume = base_volume * (1 + volatility * 10 + np.random.uniform(-0.3, 0.3, n_days))
43+
volume = volume.astype(int)
44+
45+
# Determine up/down days for coloring (shorter labels to avoid truncation)
46+
direction = ["Up Day" if c >= o else "Down Day" for c, o in zip(close_prices, open_prices)]
47+
48+
# Create date labels for x-axis (show every 10th trading day)
49+
date_labels = [d.strftime("%b %d") for d in dates]
50+
date_breaks = list(range(0, n_days, 10))
51+
date_tick_labels = [date_labels[i] for i in date_breaks]
52+
53+
df = pd.DataFrame(
54+
{
55+
"date": dates,
56+
"date_idx": range(n_days),
57+
"date_label": date_labels,
58+
"open": open_prices,
59+
"high": high_prices,
60+
"low": low_prices,
61+
"close": close_prices,
62+
"volume": volume,
63+
"direction": direction,
64+
}
65+
)
66+
67+
# Colorblind-safe colors (blue for up, orange for down)
68+
color_up = "#0077BB"
69+
color_down = "#EE7733"
70+
71+
# Create candlestick chart (main pane)
72+
candle_plot = (
73+
ggplot(df)
74+
# Wicks (high-low lines)
75+
+ geom_segment(aes(x="date_idx", xend="date_idx", y="low", yend="high", color="direction"), size=1.0)
76+
# Bodies (open-close rectangles)
77+
+ geom_segment(aes(x="date_idx", xend="date_idx", y="open", yend="close", color="direction"), size=5.0)
78+
+ scale_color_manual(values={"Up Day": color_up, "Down Day": color_down}, name="Direction")
79+
+ scale_x_continuous(breaks=date_breaks, labels=date_tick_labels)
80+
+ labs(title="candlestick-volume · letsplot · pyplots.ai", y="Price ($)", x="")
81+
# Enable crosshair cursor for precise reading
82+
+ coord_cartesian()
83+
+ theme_minimal()
84+
+ theme(
85+
plot_title=element_text(size=24),
86+
axis_title_y=element_text(size=20),
87+
axis_text_y=element_text(size=16),
88+
axis_text_x=element_blank(),
89+
legend_position=[0.5, 0.98],
90+
legend_justification=[0.5, 1.0],
91+
legend_direction="horizontal",
92+
legend_title=element_text(size=18),
93+
legend_text=element_text(size=16),
94+
panel_grid_major=element_line(color="#E5E7EB", size=0.5),
95+
panel_grid_minor=element_blank(),
96+
plot_margin=[40, 20, 5, 10],
97+
)
98+
+ ggsize(1600, 630)
99+
)
100+
101+
# Create volume breaks and labels for human-readable format
102+
vol_min, vol_max = df["volume"].min(), df["volume"].max()
103+
vol_breaks = [int(vol_min), int((vol_min + vol_max) / 2), int(vol_max)]
104+
vol_labels = [format_volume(v) for v in vol_breaks]
105+
106+
# Volume chart (lower pane)
107+
volume_plot = (
108+
ggplot(df)
109+
+ geom_bar(aes(x="date_idx", y="volume", fill="direction"), stat="identity", width=0.8)
110+
+ scale_fill_manual(values={"Up Day": color_up, "Down Day": color_down}, name="Direction")
111+
+ scale_x_continuous(breaks=date_breaks, labels=date_tick_labels)
112+
+ scale_y_continuous(breaks=vol_breaks, labels=vol_labels)
113+
+ labs(x="Date (2024)", y="Volume (shares)")
114+
# Enable crosshair cursor for precise reading (interactive HTML)
115+
+ coord_cartesian()
116+
+ theme_minimal()
117+
+ theme(
118+
axis_title=element_text(size=20),
119+
axis_text=element_text(size=16),
120+
legend_position="none",
121+
panel_grid_major=element_line(color="#E5E7EB", size=0.5),
122+
panel_grid_minor=element_blank(),
123+
)
124+
+ ggsize(1600, 270)
125+
)
126+
127+
# Use gggrid for dual-pane layout (replaces deprecated GGBunch)
128+
combined = gggrid([candle_plot, volume_plot], ncol=1, heights=[0.7, 0.3])
129+
130+
# Save outputs (path='' ensures files are saved in current directory)
131+
ggsave(combined, "plot.png", scale=3, path=".")
132+
ggsave(combined, "plot.html", path=".")
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
library: letsplot
2+
specification_id: candlestick-volume
3+
created: '2025-12-31T13:53:52Z'
4+
updated: '2025-12-31T14:44:41Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20620318739
7+
issue: 3068
8+
python_version: 3.13.11
9+
library_version: 4.8.2
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/candlestick-volume/letsplot/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/candlestick-volume/letsplot/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/candlestick-volume/letsplot/plot.html
13+
quality_score: 91
14+
review:
15+
strengths:
16+
- Excellent dual-pane layout using gggrid with proper 70/30 height ratio as specified
17+
- Colorblind-safe color scheme (blue/orange) applied consistently across both panes
18+
- Human-readable volume labels (3.9M, 6.1M, 8.3M) improve readability
19+
- Clean implementation of candlesticks using geom_segment for both wicks and bodies
20+
- Proper ggplot2 grammar of graphics style leveraging lets-plot strengths
21+
- Correct title format and descriptive axis labels with units
22+
weaknesses:
23+
- Helper function format_volume() violates KISS principle (code should be flat script)
24+
- Crosshair cursor mentioned in comments but only available in HTML output, not
25+
PNG
26+
- Minor visual gap between the two panes could be tighter

0 commit comments

Comments
 (0)