|
1 | | -""" pyplots.ai |
| 1 | +""" anyplot.ai |
2 | 2 | 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 |
5 | 5 | """ |
6 | 6 |
|
| 7 | +import os |
| 8 | +import sys |
| 9 | + |
| 10 | + |
| 11 | +# Work around filename shadowing the plotnine library |
| 12 | +sys.path.pop(0) |
7 | 13 | import numpy as np |
8 | 14 | import pandas as pd |
9 | 15 | from plotnine import ( |
10 | 16 | aes, |
| 17 | + coord_fixed, |
11 | 18 | element_blank, |
12 | 19 | element_line, |
| 20 | + element_rect, |
13 | 21 | element_text, |
| 22 | + geom_point, |
| 23 | + geom_segment, |
14 | 24 | geom_text, |
15 | 25 | ggplot, |
| 26 | + guide_legend, |
| 27 | + guides, |
16 | 28 | labs, |
17 | 29 | scale_color_manual, |
18 | 30 | scale_y_continuous, |
|
21 | 33 | ) |
22 | 34 |
|
23 | 35 |
|
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 |
25 | 46 | np.random.seed(42) |
26 | 47 | n_days = 300 |
27 | 48 |
|
28 | | -# Create trending price movement with volatility |
29 | 49 | 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 |
34 | 53 |
|
35 | 54 | price = 100 * np.cumprod(1 + returns) |
36 | 55 | close = price |
37 | 56 |
|
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 |
41 | 59 |
|
42 | 60 | # 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 |
45 | 63 | current_column = 0 |
46 | 64 |
|
47 | | -# Initialize with first price (quantize to box level) |
48 | 65 | start_box = int(np.floor(close[0] / box_size)) |
49 | 66 | current_high_box = start_box |
50 | 67 | current_low_box = start_box |
|
53 | 70 | current_box = int(np.floor(close[i] / box_size)) |
54 | 71 |
|
55 | 72 | if current_direction is None: |
56 | | - # Determine initial direction |
57 | 73 | if current_box > current_high_box: |
58 | 74 | current_direction = "X" |
59 | 75 | for b in range(current_low_box, current_box + 1): |
|
66 | 82 | current_low_box = current_box |
67 | 83 |
|
68 | 84 | elif current_direction == "X": |
69 | | - # Currently in an X (up) column |
70 | 85 | if current_box > current_high_box: |
71 | | - # Continue up |
72 | 86 | for b in range(current_high_box + 1, current_box + 1): |
73 | 87 | pf_data.append((current_column, b * box_size, "X")) |
74 | 88 | current_high_box = current_box |
75 | 89 | elif current_box <= current_high_box - reversal: |
76 | | - # Reversal - start O column |
77 | 90 | current_column += 1 |
78 | 91 | current_direction = "O" |
79 | 92 | current_low_box = current_box |
80 | 93 | for b in range(current_box, current_high_box): |
81 | 94 | pf_data.append((current_column, b * box_size, "O")) |
82 | 95 |
|
83 | 96 | elif current_direction == "O": |
84 | | - # Currently in an O (down) column |
85 | 97 | if current_box < current_low_box: |
86 | | - # Continue down |
87 | 98 | for b in range(current_box, current_low_box): |
88 | 99 | pf_data.append((current_column, b * box_size, "O")) |
89 | 100 | current_low_box = current_box |
90 | 101 | elif current_box >= current_low_box + reversal: |
91 | | - # Reversal - start X column |
92 | 102 | current_column += 1 |
93 | 103 | current_direction = "X" |
94 | 104 | current_high_box = current_box |
95 | 105 | for b in range(current_low_box + 1, current_box + 1): |
96 | 106 | pf_data.append((current_column, b * box_size, "X")) |
97 | 107 |
|
98 | | -# Create DataFrame for plotting |
99 | 108 | 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 | +) |
100 | 113 |
|
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 | +) |
103 | 131 |
|
104 | | -# Create plot |
| 132 | +# Plot |
105 | 133 | 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, |
111 | 143 | ) |
| 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})) |
112 | 149 | + scale_y_continuous( |
113 | 150 | breaks=np.arange( |
114 | 151 | int(df["price"].min() / box_size) * box_size, int(df["price"].max() / box_size + 2) * box_size, box_size * 2 |
115 | 152 | ) |
116 | 153 | ) |
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") |
123 | 155 | + theme_minimal() |
124 | 156 | + 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), |
132 | 169 | panel_grid_major_x=element_blank(), |
133 | 170 | 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), |
135 | 172 | ) |
136 | 173 | ) |
137 | 174 |
|
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