Skip to content

Commit 1af7274

Browse files
update(candlestick-basic): seaborn — comprehensive quality review (#4390)
## Summary Updated **seaborn** implementation for **candlestick-basic**. **Changes:** Comprehensive quality review — improved visual design, data quality, code patterns, and library-specific features. ## Test Plan - [x] Preview images uploaded to GCS staging - [x] Implementation file passes ruff format/check - [x] Metadata YAML updated with current versions - [ ] Automated review triggered --- Generated with [Claude Code](https://claude.com/claude-code) `/update` command --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 8b5ec3f commit 1af7274

File tree

2 files changed

+272
-188
lines changed

2 files changed

+272
-188
lines changed
Lines changed: 117 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,111 +1,152 @@
11
""" pyplots.ai
22
candlestick-basic: Basic Candlestick Chart
3-
Library: seaborn 0.13.2 | Python 3.13.11
4-
Quality: 90/100 | Created: 2025-12-23
3+
Library: seaborn 0.13.2 | Python 3.14.3
4+
Quality: 92/100 | Updated: 2026-02-24
55
"""
66

77
import matplotlib.pyplot as plt
88
import numpy as np
99
import pandas as pd
1010
import seaborn as sns
11-
from matplotlib.patches import Patch
12-
13-
14-
# Set seaborn style
15-
sns.set_theme(style="whitegrid")
11+
from matplotlib.collections import PatchCollection
12+
from matplotlib.lines import Line2D
13+
from matplotlib.patches import Patch, Rectangle
14+
15+
16+
# Seaborn theme and context
17+
sns.set_theme(
18+
style="whitegrid",
19+
rc={
20+
"axes.facecolor": "#f8f9fa",
21+
"figure.facecolor": "white",
22+
"grid.color": "#dee2e6",
23+
"text.color": "#212529",
24+
"axes.labelcolor": "#495057",
25+
"xtick.color": "#495057",
26+
"ytick.color": "#495057",
27+
},
28+
)
1629
sns.set_context("talk", font_scale=1.2)
1730

18-
# Data - 30 days of simulated stock OHLC data
31+
# Color palette via seaborn — blue/red scheme per spec, colorblind-safe
32+
candle_palette = sns.color_palette(["#306998", "#c0392b"])
33+
up_fill, down_fill = candle_palette[0], candle_palette[1]
34+
up_edge = sns.dark_palette(candle_palette[0], n_colors=4)[2]
35+
down_edge = sns.dark_palette(candle_palette[1], n_colors=4)[2]
36+
ma_palette = sns.color_palette(["#e67e22", "#8e44ad"])
37+
38+
# Data — 30 trading days with rally then selloff pattern
1939
np.random.seed(42)
2040
n_days = 30
21-
dates = pd.date_range("2024-01-02", periods=n_days, freq="B") # Business days
22-
23-
# Generate realistic OHLC data
24-
price = 150.0
41+
dates = pd.date_range("2024-01-02", periods=n_days, freq="B")
42+
43+
price = 145.0
44+
drift = np.concatenate(
45+
[
46+
np.linspace(0.4, 0.8, 12), # Uptrend phase
47+
np.linspace(-0.1, -0.6, 18), # Reversal and selloff
48+
]
49+
)
2550
opens, highs, lows, closes = [], [], [], []
26-
27-
for _ in range(n_days):
28-
# Daily volatility
29-
change = np.random.randn() * 3
30-
daily_range = abs(np.random.randn()) * 2 + 1
31-
51+
for i in range(n_days):
52+
change = drift[i] + np.random.randn() * 2.5
53+
volatility = abs(np.random.randn()) * 1.5 + 0.8
3254
open_price = price
3355
close_price = price + change
34-
35-
# High and low include the open/close range plus some extra
36-
high_price = max(open_price, close_price) + abs(np.random.randn()) * daily_range
37-
low_price = min(open_price, close_price) - abs(np.random.randn()) * daily_range
38-
56+
high_price = max(open_price, close_price) + abs(np.random.randn()) * volatility
57+
low_price = min(open_price, close_price) - abs(np.random.randn()) * volatility
3958
opens.append(open_price)
4059
highs.append(high_price)
4160
lows.append(low_price)
4261
closes.append(close_price)
43-
4462
price = close_price
4563

4664
df = pd.DataFrame({"date": dates, "open": opens, "high": highs, "low": lows, "close": closes})
47-
48-
# Determine bullish (up) vs bearish (down) days - use colorblind-safe colors
4965
df["bullish"] = df["close"] >= df["open"]
50-
# Colorblind-safe: blue for up, orange for down
51-
color_up = "#1f77b4" # Blue for bullish
52-
color_down = "#ff7f0e" # Orange for bearish
53-
df["color"] = df["bullish"].map({True: color_up, False: color_down})
54-
df["x"] = range(len(df))
66+
df["x"] = range(n_days)
5567

56-
# Create plot
68+
# Moving averages for trend storytelling
69+
df["5-Day MA"] = df["close"].rolling(window=5).mean()
70+
df["10-Day MA"] = df["close"].rolling(window=10).mean()
71+
72+
# Plot
5773
fig, ax = plt.subplots(figsize=(16, 9))
5874

59-
# Draw candlesticks manually with seaborn styling applied
60-
body_width = 0.6
75+
# Wicks
76+
wick_colors = [up_edge if b else down_edge for b in df["bullish"]]
77+
ax.vlines(df["x"], df["low"], df["high"], colors=wick_colors, linewidth=1.5)
6178

79+
# Candle bodies via PatchCollection
80+
body_width = 0.6
81+
rects, fcolors, ecolors = [], [], []
6282
for _, row in df.iterrows():
63-
x = row["x"]
64-
color = row["color"]
65-
66-
# Draw wick (high-low line) using seaborn's lineplot via dataframe
67-
wick_data = pd.DataFrame({"x": [x, x], "price": [row["low"], row["high"]]})
68-
sns.lineplot(data=wick_data, x="x", y="price", ax=ax, color=color, linewidth=2, legend=False)
69-
70-
# Draw candle body using matplotlib Rectangle for precise control
71-
body_bottom = min(row["open"], row["close"])
72-
body_height = abs(row["close"] - row["open"])
73-
# Ensure minimum body height for doji candles
74-
if body_height < 0.2:
75-
body_height = 0.2
76-
body_bottom = (row["open"] + row["close"]) / 2 - 0.1
77-
78-
rect = plt.Rectangle(
79-
(x - body_width / 2, body_bottom), body_width, body_height, facecolor=color, edgecolor=color, linewidth=1
80-
)
81-
ax.add_patch(rect)
82-
83-
# Set x-axis to show dates
84-
tick_positions = range(0, n_days, 5) # Show every 5th date
85-
tick_labels = [df["date"].iloc[i].strftime("%b %d") for i in tick_positions]
86-
ax.set_xticks(tick_positions)
87-
ax.set_xticklabels(tick_labels, rotation=0)
88-
89-
# Style the plot
83+
bottom = min(row["open"], row["close"])
84+
height = max(abs(row["close"] - row["open"]), 0.15)
85+
if abs(row["close"] - row["open"]) < 0.15:
86+
bottom = (row["open"] + row["close"]) / 2 - 0.075
87+
rects.append(Rectangle((row["x"] - body_width / 2, bottom), body_width, height))
88+
fcolors.append(up_fill if row["bullish"] else down_fill)
89+
ecolors.append(up_edge if row["bullish"] else down_edge)
90+
91+
bodies = PatchCollection(rects, facecolors=fcolors, edgecolors=ecolors, linewidths=0.8)
92+
ax.add_collection(bodies)
93+
94+
# Moving average overlays using seaborn lineplot
95+
ma_long = df[["x", "5-Day MA", "10-Day MA"]].melt(id_vars="x", var_name="Moving Average", value_name="Price").dropna()
96+
sns.lineplot(
97+
data=ma_long,
98+
x="x",
99+
y="Price",
100+
hue="Moving Average",
101+
palette={"5-Day MA": ma_palette[0], "10-Day MA": ma_palette[1]},
102+
linewidth=2.2,
103+
alpha=0.85,
104+
ax=ax,
105+
legend=False,
106+
)
107+
108+
# Peak annotation for data storytelling
109+
peak_idx = df["close"].idxmax()
110+
peak_row = df.loc[peak_idx]
111+
ax.annotate(
112+
f"Peak ${peak_row['close']:.0f}",
113+
xy=(peak_row["x"], peak_row["high"]),
114+
xytext=(peak_row["x"] + 3, peak_row["high"] + 2.0),
115+
fontsize=12,
116+
fontweight="bold",
117+
color="#495057",
118+
arrowprops={"arrowstyle": "->", "color": "#adb5bd", "lw": 1.5},
119+
bbox={"boxstyle": "round,pad=0.3", "facecolor": "white", "edgecolor": "#dee2e6", "alpha": 0.9},
120+
)
121+
122+
# X-axis date labels
123+
tick_idx = range(0, n_days, 5)
124+
ax.set_xticks(list(tick_idx))
125+
ax.set_xticklabels([dates[i].strftime("%b %d") for i in tick_idx])
126+
127+
# Style
90128
ax.set_xlabel("Date", fontsize=20)
91129
ax.set_ylabel("Price ($)", fontsize=20)
92-
ax.set_title("candlestick-basic · seaborn · pyplots.ai", fontsize=24)
93-
ax.tick_params(axis="both", labelsize=16)
94-
95-
# Add legend with colorblind-safe colors - positioned outside data area
96-
legend_elements = [Patch(facecolor=color_up, label="Bullish (Up)"), Patch(facecolor=color_down, label="Bearish (Down)")]
97-
ax.legend(handles=legend_elements, fontsize=14, loc="upper right", bbox_to_anchor=(0.99, 0.99))
98-
99-
# Subtle grid
100-
ax.grid(True, alpha=0.3, linestyle="--", axis="y")
130+
ax.set_title("candlestick-basic \u00b7 seaborn \u00b7 pyplots.ai", fontsize=24, fontweight="medium", pad=16)
131+
ax.tick_params(axis="both", labelsize=16, length=0)
132+
sns.despine(ax=ax)
133+
ax.yaxis.grid(True, alpha=0.15, linewidth=0.8)
134+
ax.xaxis.grid(False)
101135
ax.set_axisbelow(True)
102136

103-
# Set axis limits
104-
ax.set_xlim(-0.5, n_days - 0.5)
105-
y_min = df["low"].min()
106-
y_max = df["high"].max()
107-
y_padding = (y_max - y_min) * 0.1
108-
ax.set_ylim(y_min - y_padding, y_max + y_padding)
137+
# Combined legend
138+
legend_handles = [
139+
Patch(facecolor=up_fill, edgecolor=up_edge, label="Bullish"),
140+
Patch(facecolor=down_fill, edgecolor=down_edge, label="Bearish"),
141+
Line2D([0], [0], color=ma_palette[0], linewidth=2.2, label="5-Day MA"),
142+
Line2D([0], [0], color=ma_palette[1], linewidth=2.2, label="10-Day MA"),
143+
]
144+
ax.legend(handles=legend_handles, fontsize=14, loc="upper left", framealpha=0.9, edgecolor="#dee2e6")
145+
146+
# Axis limits
147+
ax.set_xlim(-0.8, n_days - 0.2)
148+
y_pad = (df["high"].max() - df["low"].min()) * 0.08
149+
ax.set_ylim(df["low"].min() - y_pad, df["high"].max() + y_pad * 2.5)
109150

110151
plt.tight_layout()
111152
plt.savefig("plot.png", dpi=300, bbox_inches="tight")

0 commit comments

Comments
 (0)