Skip to content

Commit 8183965

Browse files
feat(matplotlib): implement point-and-figure-basic (#3876)
## Implementation: `point-and-figure-basic` - matplotlib Implements the **matplotlib** version of `point-and-figure-basic`. **File:** `plots/point-and-figure-basic/implementations/matplotlib.py` **Parent Issue:** #3755 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/21049143205)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 5dd4fe4 commit 8183965

2 files changed

Lines changed: 367 additions & 0 deletions

File tree

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
""" pyplots.ai
2+
point-and-figure-basic: Point and Figure Chart
3+
Library: matplotlib 3.10.8 | Python 3.13.11
4+
Quality: 91/100 | Created: 2026-01-15
5+
"""
6+
7+
import matplotlib.pyplot as plt
8+
import numpy as np
9+
from matplotlib.lines import Line2D
10+
11+
12+
# Generate synthetic price data simulating stock price movements
13+
np.random.seed(42)
14+
n_days = 300
15+
16+
# Starting price and random walk with trend
17+
base_price = 100
18+
returns = np.random.normal(0.001, 0.02, n_days) # Daily returns
19+
close = base_price * np.cumprod(1 + returns)
20+
21+
# Generate high/low around close
22+
volatility = np.abs(np.random.normal(0, 0.01, n_days))
23+
high = close * (1 + volatility)
24+
low = close * (1 - volatility)
25+
26+
# P&F chart parameters
27+
box_size = 2.0 # Price increment per box
28+
reversal = 3 # Boxes needed to reverse
29+
30+
# Build P&F columns from price data
31+
columns = [] # Each column: {'type': 'X' or 'O', 'start': price, 'end': price}
32+
current_col = None
33+
current_price = None
34+
35+
for i in range(len(close)):
36+
price = close[i]
37+
38+
if current_col is None:
39+
# Initialize first column based on price direction
40+
current_col = {
41+
"type": "X",
42+
"start": np.floor(price / box_size) * box_size,
43+
"end": np.floor(price / box_size) * box_size,
44+
}
45+
current_price = current_col["end"]
46+
continue
47+
48+
if current_col["type"] == "X":
49+
# In an X (up) column
50+
new_high = np.floor(price / box_size) * box_size
51+
if new_high > current_col["end"]:
52+
# Extend column up
53+
current_col["end"] = new_high
54+
current_price = new_high
55+
elif price <= current_price - reversal * box_size:
56+
# Reversal to O column
57+
columns.append(current_col.copy())
58+
new_start = current_col["end"] - box_size # Start one box below
59+
new_end = np.ceil(price / box_size) * box_size
60+
current_col = {"type": "O", "start": new_start, "end": new_end}
61+
current_price = new_end
62+
else:
63+
# In an O (down) column
64+
new_low = np.ceil(price / box_size) * box_size
65+
if new_low < current_col["end"]:
66+
# Extend column down
67+
current_col["end"] = new_low
68+
current_price = new_low
69+
elif price >= current_price + reversal * box_size:
70+
# Reversal to X column
71+
columns.append(current_col.copy())
72+
new_start = current_col["end"] + box_size # Start one box above
73+
new_end = np.floor(price / box_size) * box_size
74+
current_col = {"type": "X", "start": new_start, "end": new_end}
75+
current_price = new_end
76+
77+
# Append last column
78+
if current_col is not None:
79+
columns.append(current_col)
80+
81+
# Create figure
82+
fig, ax = plt.subplots(figsize=(16, 9))
83+
84+
# Define colors
85+
x_color = "#306998" # Python Blue for bullish
86+
o_color = "#D62728" # Red for bearish
87+
88+
# Plot each column
89+
for col_idx, col in enumerate(columns):
90+
if col["type"] == "X":
91+
# X column goes from start to end (upward)
92+
start = min(col["start"], col["end"])
93+
end = max(col["start"], col["end"])
94+
boxes = np.arange(start, end + box_size / 2, box_size)
95+
for box_price in boxes:
96+
ax.text(col_idx, box_price, "X", fontsize=14, fontweight="bold", ha="center", va="center", color=x_color)
97+
else:
98+
# O column goes from start to end (downward)
99+
start = max(col["start"], col["end"])
100+
end = min(col["start"], col["end"])
101+
boxes = np.arange(end, start + box_size / 2, box_size)
102+
for box_price in boxes:
103+
ax.text(col_idx, box_price, "O", fontsize=14, fontweight="bold", ha="center", va="center", color=o_color)
104+
105+
# Calculate y-axis limits from the data
106+
all_prices = []
107+
for col in columns:
108+
all_prices.extend([col["start"], col["end"]])
109+
y_min = min(all_prices) - 2 * box_size
110+
y_max = max(all_prices) + 2 * box_size
111+
112+
# Configure axes
113+
ax.set_xlim(-0.5, len(columns) - 0.5)
114+
ax.set_ylim(y_min, y_max)
115+
116+
# Grid at box size intervals
117+
ax.set_yticks(
118+
np.arange(np.floor(y_min / box_size) * box_size, np.ceil(y_max / box_size) * box_size + box_size, box_size)
119+
)
120+
ax.grid(True, alpha=0.3, linestyle="-", which="both")
121+
122+
# Labels and title
123+
ax.set_xlabel("Column (Reversal Number)", fontsize=20)
124+
ax.set_ylabel("Price ($)", fontsize=20)
125+
ax.set_title("point-and-figure-basic · matplotlib · pyplots.ai", fontsize=24)
126+
ax.tick_params(axis="both", labelsize=16)
127+
128+
# Add legend
129+
legend_elements = [
130+
Line2D(
131+
[0],
132+
[0],
133+
marker="$X$",
134+
color="w",
135+
markerfacecolor=x_color,
136+
markersize=20,
137+
label="Rising (X)",
138+
markeredgecolor=x_color,
139+
),
140+
Line2D(
141+
[0],
142+
[0],
143+
marker="$O$",
144+
color="w",
145+
markerfacecolor=o_color,
146+
markersize=20,
147+
label="Falling (O)",
148+
markeredgecolor=o_color,
149+
),
150+
]
151+
ax.legend(handles=legend_elements, fontsize=16, loc="upper left")
152+
153+
plt.tight_layout()
154+
plt.savefig("plot.png", dpi=300, bbox_inches="tight")
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
library: matplotlib
2+
specification_id: point-and-figure-basic
3+
created: '2026-01-15T22:54:30Z'
4+
updated: '2026-01-15T22:57:16Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 21049143205
7+
issue: 3755
8+
python_version: 3.13.11
9+
library_version: 3.10.8
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/point-and-figure-basic/matplotlib/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/point-and-figure-basic/matplotlib/plot_thumb.png
12+
preview_html: null
13+
quality_score: 91
14+
review:
15+
strengths:
16+
- Excellent P&F algorithm implementation correctly handling box sizes and 3-box
17+
reversals
18+
- Clean, readable code structure following KISS principles
19+
- Proper use of X-axis representing columns/reversals rather than time (key P&F
20+
feature)
21+
- Good color choices with blue for bullish and red for bearish
22+
- Grid lines aligned with box size intervals as recommended in spec
23+
- Appropriate text sizing for high-resolution output
24+
weaknesses:
25+
- Missing support/resistance trend lines mentioned in spec notes (45-degree lines
26+
connecting ascending lows/descending highs)
27+
- Legend marker style does not perfectly match the text characters displayed on
28+
chart
29+
image_description: The plot displays a Point and Figure (P&F) chart showing price
30+
movements over 13 columns (0-12). The chart uses blue "X" characters for rising
31+
price columns (bullish) and red "O" characters for falling price columns (bearish).
32+
The Y-axis shows "Price ($)" ranging from approximately $80 to $132 with grid
33+
lines at $2 intervals (matching the box size). The X-axis shows "Column (Reversal
34+
Number)" from 0 to 12. The title correctly follows the format "point-and-figure-basic
35+
· matplotlib · pyplots.ai". A legend in the upper left shows "Rising (X)" in blue
36+
and "Falling (O)" in red. The chart demonstrates alternating X and O columns showing
37+
price reversals, with an overall upward trend visible as prices rise from ~$100
38+
to ~$127 by the end.
39+
criteria_checklist:
40+
visual_quality:
41+
score: 37
42+
max: 40
43+
items:
44+
- id: VQ-01
45+
name: Text Legibility
46+
score: 10
47+
max: 10
48+
passed: true
49+
comment: Title at 24pt, axis labels at 20pt, tick labels at 16pt, all perfectly
50+
readable at full resolution
51+
- id: VQ-02
52+
name: No Overlap
53+
score: 8
54+
max: 8
55+
passed: true
56+
comment: No overlapping text or elements; X and O symbols are well-spaced
57+
within their columns
58+
- id: VQ-03
59+
name: Element Visibility
60+
score: 7
61+
max: 8
62+
passed: true
63+
comment: X and O symbols are clearly visible at fontsize=14 with bold weight;
64+
could be slightly larger for optimal visibility
65+
- id: VQ-04
66+
name: Color Accessibility
67+
score: 5
68+
max: 5
69+
passed: true
70+
comment: Blue and red provide good contrast and are distinguishable for colorblind
71+
users
72+
- id: VQ-05
73+
name: Layout Balance
74+
score: 5
75+
max: 5
76+
passed: true
77+
comment: Plot fills canvas appropriately with balanced margins
78+
- id: VQ-06
79+
name: Axis Labels
80+
score: 2
81+
max: 2
82+
passed: true
83+
comment: Price ($) includes units, Column (Reversal Number) is descriptive
84+
- id: VQ-07
85+
name: Grid & Legend
86+
score: 0
87+
max: 2
88+
passed: false
89+
comment: Legend marker style doesn't perfectly match the text characters displayed
90+
on chart
91+
spec_compliance:
92+
score: 23
93+
max: 25
94+
items:
95+
- id: SC-01
96+
name: Plot Type
97+
score: 8
98+
max: 8
99+
passed: true
100+
comment: Correct Point and Figure chart with X for rising and O for falling
101+
- id: SC-02
102+
name: Data Mapping
103+
score: 5
104+
max: 5
105+
passed: true
106+
comment: Y-axis correctly shows price, X-axis shows column/reversal number
107+
- id: SC-03
108+
name: Required Features
109+
score: 4
110+
max: 5
111+
passed: true
112+
comment: Has X/O symbols, box size intervals, grid lines. Missing trend lines
113+
from spec notes
114+
- id: SC-04
115+
name: Data Range
116+
score: 3
117+
max: 3
118+
passed: true
119+
comment: All data visible within axes limits with appropriate padding
120+
- id: SC-05
121+
name: Legend Accuracy
122+
score: 2
123+
max: 2
124+
passed: true
125+
comment: Legend correctly labels Rising (X) and Falling (O)
126+
- id: SC-06
127+
name: Title Format
128+
score: 1
129+
max: 2
130+
passed: true
131+
comment: Uses correct format with middle dot separator
132+
data_quality:
133+
score: 18
134+
max: 20
135+
items:
136+
- id: DQ-01
137+
name: Feature Coverage
138+
score: 7
139+
max: 8
140+
passed: true
141+
comment: Shows both bullish and bearish columns, multiple reversals, overall
142+
trend
143+
- id: DQ-02
144+
name: Realistic Context
145+
score: 7
146+
max: 7
147+
passed: true
148+
comment: Stock price simulation starting at $100 with realistic daily returns
149+
- id: DQ-03
150+
name: Appropriate Scale
151+
score: 4
152+
max: 5
153+
passed: true
154+
comment: Price range $80-132 is reasonable; box size of $2 is sensible
155+
code_quality:
156+
score: 10
157+
max: 10
158+
items:
159+
- id: CQ-01
160+
name: KISS Structure
161+
score: 3
162+
max: 3
163+
passed: true
164+
comment: 'Clean linear flow: imports, data generation, P&F algorithm, plot,
165+
save'
166+
- id: CQ-02
167+
name: Reproducibility
168+
score: 3
169+
max: 3
170+
passed: true
171+
comment: Uses np.random.seed(42)
172+
- id: CQ-03
173+
name: Clean Imports
174+
score: 2
175+
max: 2
176+
passed: true
177+
comment: All imports are used
178+
- id: CQ-04
179+
name: No Deprecated API
180+
score: 1
181+
max: 1
182+
passed: true
183+
comment: Uses current matplotlib API
184+
- id: CQ-05
185+
name: Output Correct
186+
score: 1
187+
max: 1
188+
passed: true
189+
comment: Saves as plot.png
190+
library_features:
191+
score: 3
192+
max: 5
193+
items:
194+
- id: LF-01
195+
name: Distinctive Features
196+
score: 3
197+
max: 5
198+
passed: true
199+
comment: Uses ax.text() for character placement and Line2D for custom legend
200+
markers
201+
verdict: APPROVED
202+
impl_tags:
203+
dependencies: []
204+
techniques:
205+
- annotations
206+
- custom-legend
207+
- manual-ticks
208+
patterns:
209+
- data-generation
210+
- iteration-over-groups
211+
dataprep: []
212+
styling:
213+
- grid-styling

0 commit comments

Comments
 (0)