Skip to content

Commit 8f7bf90

Browse files
feat(altair): implement candlestick-volume (#6879)
## Implementation: `candlestick-volume` - python/altair Implements the **python/altair** version of `candlestick-volume`. **File:** `plots/candlestick-volume/implementations/python/altair.py` **Parent Issue:** #3068 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/25953759519)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com>
1 parent ff8ddae commit 8f7bf90

2 files changed

Lines changed: 291 additions & 180 deletions

File tree

Lines changed: 129 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,28 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
candlestick-volume: Stock Candlestick Chart with Volume
3-
Library: altair 6.0.0 | Python 3.13.11
4-
Quality: 91/100 | Created: 2025-12-31
3+
Library: altair 6.1.0 | Python 3.13.13
4+
Quality: 86/100 | Updated: 2026-05-16
55
"""
66

7+
import os
8+
79
import altair as alt
810
import numpy as np
911
import pandas as pd
1012

1113

14+
# Theme tokens
15+
THEME = os.getenv("ANYPLOT_THEME", "light")
16+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
17+
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
18+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
19+
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
20+
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"
21+
22+
# Okabe-Ito palette
23+
BULLISH = "#009E73" # Okabe-Ito position 1 (bluish green)
24+
BEARISH = "#D55E00" # Okabe-Ito position 2 (vermillion)
25+
1226
# Data: Generate 60 days of realistic stock OHLC data with volume
1327
np.random.seed(42)
1428
n_days = 60
@@ -47,32 +61,65 @@
4761
# Determine if day is bullish (close >= open) or bearish
4862
df["direction"] = np.where(df["close"] >= df["open"], "bullish", "bearish")
4963

50-
# Color scheme for candlesticks and volume
51-
color_scale = alt.Scale(domain=["bullish", "bearish"], range=["#306998", "#FFD43B"])
52-
53-
# Base chart with shared x-axis selection
54-
base = alt.Chart(df).encode(x=alt.X("date:T", axis=alt.Axis(title="Date", labelFontSize=16, titleFontSize=20)))
64+
# Color scheme for candlesticks and volume (Okabe-Ito palette)
65+
color_scale = alt.Scale(domain=["bullish", "bearish"], range=[BULLISH, BEARISH])
5566

5667
# Candlestick wicks (high-low lines)
57-
wicks = base.mark_rule(strokeWidth=2).encode(
58-
y=alt.Y("low:Q", scale=alt.Scale(zero=False), axis=alt.Axis(title="Price ($)", labelFontSize=16, titleFontSize=20)),
59-
y2="high:Q",
60-
color=alt.Color("direction:N", scale=color_scale, legend=None),
68+
wicks = (
69+
alt.Chart(df)
70+
.mark_rule(strokeWidth=2)
71+
.encode(
72+
x=alt.X(
73+
"date:T",
74+
axis=alt.Axis(
75+
labelFontSize=18,
76+
titleFontSize=22,
77+
labelColor=INK_SOFT,
78+
titleColor=INK,
79+
domainColor=INK_SOFT,
80+
tickColor=INK_SOFT,
81+
gridColor=INK_MUTED,
82+
gridOpacity=0.15,
83+
),
84+
),
85+
y=alt.Y(
86+
"low:Q",
87+
scale=alt.Scale(zero=False),
88+
axis=alt.Axis(
89+
title="Price ($)",
90+
labelFontSize=18,
91+
titleFontSize=22,
92+
labelColor=INK_SOFT,
93+
titleColor=INK,
94+
domainColor=INK_SOFT,
95+
tickColor=INK_SOFT,
96+
gridColor=INK_MUTED,
97+
gridOpacity=0.15,
98+
),
99+
),
100+
y2="high:Q",
101+
color=alt.Color("direction:N", scale=color_scale, legend=None),
102+
)
61103
)
62104

63105
# Candlestick bodies (open-close bars)
64-
bodies = base.mark_bar(size=12).encode(
65-
y=alt.Y("open:Q", scale=alt.Scale(zero=False)),
66-
y2="close:Q",
67-
color=alt.Color("direction:N", scale=color_scale, legend=None),
68-
tooltip=[
69-
alt.Tooltip("date:T", title="Date"),
70-
alt.Tooltip("open:Q", title="Open", format="$.2f"),
71-
alt.Tooltip("high:Q", title="High", format="$.2f"),
72-
alt.Tooltip("low:Q", title="Low", format="$.2f"),
73-
alt.Tooltip("close:Q", title="Close", format="$.2f"),
74-
alt.Tooltip("volume:Q", title="Volume", format=","),
75-
],
106+
bodies = (
107+
alt.Chart(df)
108+
.mark_bar(size=12)
109+
.encode(
110+
x=alt.X("date:T"),
111+
y=alt.Y("open:Q", scale=alt.Scale(zero=False)),
112+
y2="close:Q",
113+
color=alt.Color("direction:N", scale=color_scale, legend=None),
114+
tooltip=[
115+
alt.Tooltip("date:T", title="Date", format="%Y-%m-%d"),
116+
alt.Tooltip("open:Q", title="Open", format="$.2f"),
117+
alt.Tooltip("high:Q", title="High", format="$.2f"),
118+
alt.Tooltip("low:Q", title="Low", format="$.2f"),
119+
alt.Tooltip("close:Q", title="Close", format="$.2f"),
120+
alt.Tooltip("volume:Q", title="Volume", format=","),
121+
],
122+
)
76123
)
77124

78125
# Combine wicks and bodies for candlestick chart
@@ -83,34 +130,71 @@
83130
alt.Chart(df)
84131
.mark_bar(size=12)
85132
.encode(
86-
x=alt.X("date:T", axis=alt.Axis(title="Date", labelFontSize=16, titleFontSize=20)),
87-
y=alt.Y("volume:Q", axis=alt.Axis(title="Volume", labelFontSize=16, titleFontSize=20, format="~s")),
133+
x=alt.X(
134+
"date:T",
135+
axis=alt.Axis(
136+
labelFontSize=18,
137+
titleFontSize=22,
138+
labelColor=INK_SOFT,
139+
titleColor=INK,
140+
domainColor=INK_SOFT,
141+
tickColor=INK_SOFT,
142+
gridColor=INK_MUTED,
143+
gridOpacity=0.15,
144+
),
145+
),
146+
y=alt.Y(
147+
"volume:Q",
148+
axis=alt.Axis(
149+
title="Volume",
150+
labelFontSize=18,
151+
titleFontSize=22,
152+
labelColor=INK_SOFT,
153+
titleColor=INK,
154+
domainColor=INK_SOFT,
155+
tickColor=INK_SOFT,
156+
gridColor=INK_MUTED,
157+
gridOpacity=0.15,
158+
format="~s",
159+
),
160+
),
88161
color=alt.Color("direction:N", scale=color_scale, legend=None),
89-
tooltip=[alt.Tooltip("date:T", title="Date"), alt.Tooltip("volume:Q", title="Volume", format=",")],
162+
tooltip=[
163+
alt.Tooltip("date:T", title="Date", format="%Y-%m-%d"),
164+
alt.Tooltip("volume:Q", title="Volume", format=","),
165+
],
90166
)
91167
.properties(width=1600, height=200)
92168
)
93169

94-
# Add legend separately
95-
legend_data = pd.DataFrame({"direction": ["bullish", "bearish"], "label": ["Bullish (Up)", "Bearish (Down)"]})
96-
legend = (
97-
alt.Chart(legend_data)
98-
.mark_point(size=300)
99-
.encode(y=alt.Y("label:N", axis=None), color=alt.Color("direction:N", scale=color_scale, legend=None))
100-
.properties(width=50, title="")
170+
# Combine candlestick and volume vertically with interactive features
171+
combined = (
172+
alt.vconcat(candlestick, volume, spacing=10)
173+
.properties(
174+
title=alt.Title("candlestick-volume · altair · anyplot.ai", fontSize=28, anchor="middle", color=INK),
175+
background=PAGE_BG,
176+
)
177+
.interactive()
101178
)
102179

103-
# Combine candlestick and volume vertically
104-
combined = alt.vconcat(candlestick, volume, spacing=10).properties(
105-
title=alt.Title("candlestick-volume · altair · pyplots.ai", fontSize=28, anchor="middle")
180+
# Configure chart with theme-adaptive styling
181+
chart = (
182+
combined.configure_axis(
183+
labelFontSize=18,
184+
titleFontSize=22,
185+
labelColor=INK_SOFT,
186+
titleColor=INK,
187+
domainColor=INK_SOFT,
188+
tickColor=INK_SOFT,
189+
gridColor=INK_MUTED,
190+
gridOpacity=0.15,
191+
)
192+
.configure_view(fill=PAGE_BG, strokeWidth=0)
193+
.configure_title(color=INK, fontSize=28)
106194
)
107195

108-
# Configure chart with appropriate styling
109-
chart = combined.configure_axis(labelFontSize=16, titleFontSize=20).configure_view(strokeWidth=0)
110-
111-
# Save as PNG (scale_factor=3 for 4800x2700)
112-
chart.save("plot.png", scale_factor=3.0)
196+
# Save as PNG with theme suffix (scale_factor=3 for 4800x2700)
197+
chart.save(f"plot-{THEME}.png", scale_factor=3.0)
113198

114-
# Save interactive HTML version
115-
chart_interactive = combined.configure_axis(labelFontSize=16, titleFontSize=20).configure_view(strokeWidth=0)
116-
chart_interactive.save("plot.html")
199+
# Save interactive HTML version with theme suffix
200+
chart.save(f"plot-{THEME}.html")

0 commit comments

Comments
 (0)