Skip to content

Commit 83661e5

Browse files
github-actions[bot]claudeMarkusNeusinger
authored
feat(plotnine): implement point-and-figure-basic (#7473)
## Implementation: `point-and-figure-basic` - python/plotnine Implements the **python/plotnine** version of `point-and-figure-basic`. **File:** `plots/point-and-figure-basic/implementations/python/plotnine.py` **Parent Issue:** #3755 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/26139439654)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com>
1 parent cd3793f commit 83661e5

3 files changed

Lines changed: 251 additions & 165 deletions

File tree

Lines changed: 83 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,30 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
point-and-figure-basic: Point and Figure Chart
3-
Library: plotnine 0.15.2 | Python 3.13.11
4-
Quality: 91/100 | Created: 2026-01-15
3+
Library: plotnine 0.15.4 | Python 3.13.13
4+
Quality: 83/100 | Updated: 2026-05-20
55
"""
66

7+
import os
8+
import sys
9+
10+
11+
# Work around filename shadowing the plotnine library
12+
sys.path.pop(0)
713
import numpy as np
814
import pandas as pd
915
from plotnine import (
1016
aes,
17+
coord_fixed,
1118
element_blank,
1219
element_line,
20+
element_rect,
1321
element_text,
22+
geom_point,
23+
geom_segment,
1424
geom_text,
1525
ggplot,
26+
guide_legend,
27+
guides,
1628
labs,
1729
scale_color_manual,
1830
scale_y_continuous,
@@ -21,30 +33,35 @@
2133
)
2234

2335

24-
# Generate synthetic stock price data
36+
THEME = os.getenv("ANYPLOT_THEME", "light")
37+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
38+
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
39+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
40+
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
41+
42+
X_COLOR = "#009E73" # Okabe-Ito position 1 - rising columns
43+
O_COLOR = "#D55E00" # Okabe-Ito position 2 - falling columns (colorblind-safe vs pure red)
44+
45+
# Data
2546
np.random.seed(42)
2647
n_days = 300
2748

28-
# Create trending price movement with volatility
2949
returns = np.random.normal(0.001, 0.02, n_days)
30-
# Add some trend periods
31-
returns[50:100] += 0.005 # Uptrend
32-
returns[120:160] -= 0.008 # Downtrend
33-
returns[180:250] += 0.004 # Uptrend
50+
returns[50:100] += 0.005
51+
returns[120:160] -= 0.008
52+
returns[180:250] += 0.004
3453

3554
price = 100 * np.cumprod(1 + returns)
3655
close = price
3756

38-
# Point and Figure parameters
39-
box_size = 2.0 # Each box represents $2 price movement
40-
reversal = 3 # 3-box reversal
57+
box_size = 2.0
58+
reversal = 3
4159

4260
# Build Point and Figure data
43-
pf_data = [] # List of (column_index, price_level, direction)
44-
current_direction = None # 'X' (up) or 'O' (down)
61+
pf_data = []
62+
current_direction = None
4563
current_column = 0
4664

47-
# Initialize with first price (quantize to box level)
4865
start_box = int(np.floor(close[0] / box_size))
4966
current_high_box = start_box
5067
current_low_box = start_box
@@ -53,7 +70,6 @@
5370
current_box = int(np.floor(close[i] / box_size))
5471

5572
if current_direction is None:
56-
# Determine initial direction
5773
if current_box > current_high_box:
5874
current_direction = "X"
5975
for b in range(current_low_box, current_box + 1):
@@ -66,73 +82,94 @@
6682
current_low_box = current_box
6783

6884
elif current_direction == "X":
69-
# Currently in an X (up) column
7085
if current_box > current_high_box:
71-
# Continue up
7286
for b in range(current_high_box + 1, current_box + 1):
7387
pf_data.append((current_column, b * box_size, "X"))
7488
current_high_box = current_box
7589
elif current_box <= current_high_box - reversal:
76-
# Reversal - start O column
7790
current_column += 1
7891
current_direction = "O"
7992
current_low_box = current_box
8093
for b in range(current_box, current_high_box):
8194
pf_data.append((current_column, b * box_size, "O"))
8295

8396
elif current_direction == "O":
84-
# Currently in an O (down) column
8597
if current_box < current_low_box:
86-
# Continue down
8798
for b in range(current_box, current_low_box):
8899
pf_data.append((current_column, b * box_size, "O"))
89100
current_low_box = current_box
90101
elif current_box >= current_low_box + reversal:
91-
# Reversal - start X column
92102
current_column += 1
93103
current_direction = "X"
94104
current_high_box = current_box
95105
for b in range(current_low_box + 1, current_box + 1):
96106
pf_data.append((current_column, b * box_size, "X"))
97107

98-
# Create DataFrame for plotting
99108
df = pd.DataFrame(pf_data, columns=["column", "price", "symbol"])
109+
# Use full label text as the color aesthetic value to avoid plotnine legend prefix bug
110+
df["direction"] = pd.Categorical(
111+
df["symbol"].map({"X": "Rising (X)", "O": "Falling (O)"}), categories=["Rising (X)", "Falling (O)"]
112+
)
100113

101-
# Map symbols to display characters
102-
df["display"] = df["symbol"].map({"X": "X", "O": "O"})
114+
# 45-degree support and resistance trend lines (one box per column)
115+
min_idx = df["price"].idxmin()
116+
max_idx = df["price"].idxmax()
117+
max_col = float(df["column"].max())
118+
support_col = float(df.loc[min_idx, "column"])
119+
support_price = float(df.loc[min_idx, "price"])
120+
resist_col = float(df.loc[max_idx, "column"])
121+
resist_price = float(df.loc[max_idx, "price"])
122+
123+
trend_lines = pd.DataFrame(
124+
{
125+
"x": [support_col, resist_col],
126+
"y": [support_price, resist_price],
127+
"xend": [max_col, max_col],
128+
"yend": [support_price + (max_col - support_col) * box_size, resist_price - (max_col - resist_col) * box_size],
129+
}
130+
)
103131

104-
# Create plot
132+
# Plot
105133
plot = (
106-
ggplot(df, aes(x="column", y="price", color="symbol", label="display"))
107-
+ geom_text(size=12, fontweight="bold")
108-
+ scale_color_manual(
109-
values={"X": "#2E8B57", "O": "#DC143C"}, # Green for X, Red for O
110-
labels={"X": "Rising (X)", "O": "Falling (O)"},
134+
ggplot(df, aes(x="column", y="price"))
135+
+ geom_segment(
136+
data=trend_lines,
137+
mapping=aes(x="x", y="y", xend="xend", yend="yend"),
138+
color=INK_SOFT,
139+
size=0.6,
140+
linetype="dashed",
141+
alpha=0.7,
142+
inherit_aes=False,
111143
)
144+
+ geom_text(mapping=aes(color="direction", label="symbol"), size=8, fontweight="bold", show_legend=False)
145+
+ geom_point(mapping=aes(color="direction"), size=0.01, alpha=0.01)
146+
+ coord_fixed(ratio=box_size)
147+
+ scale_color_manual(values={"Rising (X)": X_COLOR, "Falling (O)": O_COLOR}, name="Direction")
148+
+ guides(color=guide_legend(override_aes={"size": 3, "alpha": 1}))
112149
+ scale_y_continuous(
113150
breaks=np.arange(
114151
int(df["price"].min() / box_size) * box_size, int(df["price"].max() / box_size + 2) * box_size, box_size * 2
115152
)
116153
)
117-
+ labs(
118-
x="Column (Reversals)",
119-
y="Price Level ($)",
120-
title="point-and-figure-basic · plotnine · pyplots.ai",
121-
color="Direction",
122-
)
154+
+ labs(x="Column (Reversals)", y="Price Level ($)", title="point-and-figure-basic · python · plotnine · anyplot.ai")
123155
+ theme_minimal()
124156
+ theme(
125-
figure_size=(16, 9),
126-
text=element_text(size=14),
127-
axis_title=element_text(size=20),
128-
axis_text=element_text(size=16),
129-
plot_title=element_text(size=24),
130-
legend_text=element_text(size=16),
131-
legend_title=element_text(size=18),
157+
figure_size=(6, 6),
158+
text=element_text(size=7, color=INK),
159+
axis_title=element_text(size=10, color=INK),
160+
axis_text=element_text(size=8, color=INK_SOFT),
161+
plot_title=element_text(size=12, color=INK),
162+
legend_text=element_text(size=8, color=INK_SOFT),
163+
legend_title=element_text(size=9, color=INK),
164+
plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
165+
panel_background=element_rect(fill=PAGE_BG),
166+
legend_background=element_rect(fill=ELEVATED_BG, size=0),
167+
panel_border=element_blank(),
168+
axis_line=element_line(color=INK_SOFT, size=0.5),
132169
panel_grid_major_x=element_blank(),
133170
panel_grid_minor=element_blank(),
134-
panel_grid_major_y=element_line(color="#cccccc", size=0.5, alpha=0.5),
171+
panel_grid_major_y=element_line(color=INK, size=0.3, alpha=0.10),
135172
)
136173
)
137174

138-
plot.save("plot.png", dpi=300)
175+
plot.save(f"plot-{THEME}.png", dpi=400, width=6, height=6, units="in")

0 commit comments

Comments
 (0)