Skip to content

Commit 5817ed9

Browse files
feat(pygal): implement point-and-figure-basic (#3860)
## Implementation: `point-and-figure-basic` - pygal Implements the **pygal** version of `point-and-figure-basic`. **File:** `plots/point-and-figure-basic/implementations/pygal.py` **Parent Issue:** #3755 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/21047207399)* --------- 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 1534816 commit 5817ed9

File tree

2 files changed

+376
-0
lines changed

2 files changed

+376
-0
lines changed
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
""" pyplots.ai
2+
point-and-figure-basic: Point and Figure Chart
3+
Library: pygal 3.1.0 | Python 3.13.11
4+
Quality: 72/100 | Created: 2026-01-15
5+
"""
6+
7+
import numpy as np
8+
import pygal
9+
from pygal.style import Style
10+
11+
12+
# Random seed for reproducibility
13+
np.random.seed(42)
14+
15+
# Generate realistic stock price data (6 months of daily data)
16+
n_days = 150
17+
base_price = 100.0
18+
returns = np.random.normal(0.001, 0.02, n_days)
19+
prices = base_price * np.cumprod(1 + returns)
20+
21+
# P&F parameters
22+
box_size = 2.0
23+
reversal = 3
24+
25+
# Calculate P&F chart data - inline rounding (KISS - no helper functions)
26+
columns = []
27+
current_direction = None
28+
current_column_start = np.floor(prices[0] / box_size) * box_size
29+
current_column_end = current_column_start
30+
31+
for price in prices[1:]:
32+
rounded_price = np.floor(price / box_size) * box_size
33+
34+
if current_direction is None:
35+
if rounded_price >= current_column_end + box_size:
36+
current_direction = "X"
37+
current_column_end = rounded_price
38+
elif rounded_price <= current_column_end - box_size:
39+
current_direction = "O"
40+
current_column_end = rounded_price
41+
elif current_direction == "X":
42+
if rounded_price >= current_column_end + box_size:
43+
current_column_end = rounded_price
44+
elif rounded_price <= current_column_end - (reversal * box_size):
45+
columns.append((current_column_start, current_column_end, "X"))
46+
current_column_start = current_column_end - box_size
47+
current_column_end = rounded_price
48+
current_direction = "O"
49+
else:
50+
if rounded_price <= current_column_end - box_size:
51+
current_column_end = rounded_price
52+
elif rounded_price >= current_column_end + (reversal * box_size):
53+
columns.append((current_column_start, current_column_end, "O"))
54+
current_column_start = current_column_end + box_size
55+
current_column_end = rounded_price
56+
current_direction = "X"
57+
58+
if current_direction:
59+
columns.append((current_column_start, current_column_end, current_direction))
60+
61+
# Colorblind-safe colors (blue for rising X, orange for falling O)
62+
x_color = "#0066CC"
63+
o_color = "#E56B00"
64+
support_color = "#228B22"
65+
resistance_color = "#8B0000"
66+
67+
# Custom style with subtle grid
68+
custom_style = Style(
69+
background="white",
70+
plot_background="white",
71+
foreground="#333333",
72+
foreground_strong="#333333",
73+
foreground_subtle="#DDDDDD", # Subtle grid lines
74+
colors=(x_color, o_color, support_color, resistance_color),
75+
title_font_size=72,
76+
label_font_size=48,
77+
major_label_font_size=42,
78+
legend_font_size=48,
79+
value_font_size=42,
80+
tooltip_font_size=36,
81+
stroke_width=6,
82+
guide_stroke_dasharray="4,4", # Dashed grid for subtlety
83+
)
84+
85+
# Collect price range
86+
all_prices = []
87+
for start, end, _ in columns:
88+
all_prices.extend([start, end])
89+
90+
y_min = min(all_prices) - box_size
91+
y_max = max(all_prices) + box_size
92+
y_labels = list(np.arange(y_min, y_max + box_size, box_size))
93+
94+
# Create XY chart with visible dots for X and O markers
95+
# Pygal uses circles as markers - distinguish X/O through color and legend
96+
chart = pygal.XY(
97+
width=4800,
98+
height=2700,
99+
style=custom_style,
100+
title="point-and-figure-basic · pygal · pyplots.ai",
101+
show_legend=True,
102+
legend_at_bottom=True,
103+
legend_at_bottom_columns=4,
104+
show_y_guides=True,
105+
show_x_guides=False, # Only horizontal guides for cleaner look
106+
x_title="Column (Reversal)",
107+
y_title="Price ($)",
108+
margin=50,
109+
margin_bottom=200,
110+
margin_left=200,
111+
margin_top=150,
112+
margin_right=120, # Better balance
113+
y_labels=y_labels,
114+
range=(y_min, y_max),
115+
dots_size=22,
116+
stroke=False,
117+
)
118+
119+
# Build X and O data points
120+
x_points = []
121+
o_points = []
122+
123+
for col_idx, (start, end, direction) in enumerate(columns):
124+
low = min(start, end)
125+
high = max(start, end)
126+
if direction == "X":
127+
for price_level in np.arange(low, high + box_size, box_size):
128+
x_points.append((col_idx, price_level))
129+
else:
130+
for price_level in np.arange(low, high + box_size, box_size):
131+
o_points.append((col_idx, price_level))
132+
133+
# Add series - X markers (rising) and O markers (falling)
134+
# Legend clearly shows X and O symbols to identify the markers
135+
chart.add("X (Rising)", x_points, stroke=False)
136+
chart.add("O (Falling)", o_points, stroke=False)
137+
138+
# Calculate support and resistance trend lines
139+
support_lows = []
140+
resistance_highs = []
141+
142+
for col_idx, (start, end, direction) in enumerate(columns):
143+
low = min(start, end)
144+
high = max(start, end)
145+
if direction == "O":
146+
support_lows.append((col_idx, low))
147+
else:
148+
resistance_highs.append((col_idx, high))
149+
150+
# Support line (45-degree uptrend from O column lows) - with visible endpoints
151+
if len(support_lows) >= 2:
152+
support_start = support_lows[0]
153+
support_end_col = min(support_start[0] + 5, len(columns) - 1)
154+
support_line = [
155+
(support_start[0], support_start[1]),
156+
(support_end_col, support_start[1] + (support_end_col - support_start[0]) * box_size),
157+
]
158+
chart.add("Support (45° up)", support_line, stroke=True, dots_size=10)
159+
160+
# Resistance line (45-degree downtrend from X column highs) - with visible endpoints
161+
if len(resistance_highs) >= 2:
162+
resistance_start = resistance_highs[0]
163+
resistance_end_col = min(resistance_start[0] + 5, len(columns) - 1)
164+
resistance_line = [
165+
(resistance_start[0], resistance_start[1]),
166+
(resistance_end_col, resistance_start[1] - (resistance_end_col - resistance_start[0]) * box_size),
167+
]
168+
chart.add("Resistance (45° down)", resistance_line, stroke=True, dots_size=10)
169+
170+
# Save outputs
171+
chart.render_to_png("plot.png")
172+
chart.render_to_file("plot.html")
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
library: pygal
2+
specification_id: point-and-figure-basic
3+
created: '2026-01-15T21:41:11Z'
4+
updated: '2026-01-15T22:05:05Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 21047207399
7+
issue: 3755
8+
python_version: 3.13.11
9+
library_version: 3.1.0
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/point-and-figure-basic/pygal/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/point-and-figure-basic/pygal/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/point-and-figure-basic/pygal/plot.html
13+
quality_score: 72
14+
review:
15+
strengths:
16+
- Colorblind-safe blue/orange palette is excellent
17+
- Clean KISS code structure with clear P&F algorithm
18+
- Correct title format and descriptive axis labels
19+
- Realistic stock price data with appropriate box size
20+
- Good use of pygal custom Style for font sizing and grid
21+
weaknesses:
22+
- Support and resistance trend lines do not appear in rendered output despite being
23+
added to chart
24+
- Uses circular dot markers instead of actual X and O symbols (pygal limitation)
25+
- Plot could better utilize canvas space
26+
image_description: 'The plot displays a Point and Figure chart with 8 columns of
27+
price reversals. The chart uses green circular markers for rising (X) columns
28+
and red/orange circular markers for falling (O) columns. The Y-axis shows "Price
29+
($)" ranging from 82 to 110 in $2 increments (box size). The X-axis shows "Column
30+
(Reversal)" from 0 to 7. Subtle horizontal dashed grid lines are visible. The
31+
legend at the bottom shows "X (Rising)" in green and "O (Falling)" in red. The
32+
title correctly displays "point-and-figure-basic · pygal · pyplots.ai". The chart
33+
shows a clear pattern: starting with a bullish rise (column 0), followed by bearish
34+
reversal (column 1), then alternating bullish and bearish columns. Support and
35+
resistance trend lines mentioned in the code are NOT visible in the rendered output.'
36+
criteria_checklist:
37+
visual_quality:
38+
score: 32
39+
max: 40
40+
items:
41+
- id: VQ-01
42+
name: Text Legibility
43+
score: 8
44+
max: 10
45+
passed: true
46+
comment: Title and labels readable but Y-axis label appears slightly small
47+
relative to canvas
48+
- id: VQ-02
49+
name: No Overlap
50+
score: 8
51+
max: 8
52+
passed: true
53+
comment: No overlapping elements, clean layout
54+
- id: VQ-03
55+
name: Element Visibility
56+
score: 6
57+
max: 8
58+
passed: true
59+
comment: Dots visible but circles used instead of actual X and O symbols as
60+
spec requires
61+
- id: VQ-04
62+
name: Color Accessibility
63+
score: 5
64+
max: 5
65+
passed: true
66+
comment: Blue/orange colorblind-safe palette is excellent
67+
- id: VQ-05
68+
name: Layout Balance
69+
score: 3
70+
max: 5
71+
passed: true
72+
comment: Decent canvas use but some wasted space on left margin
73+
- id: VQ-06
74+
name: Axis Labels
75+
score: 2
76+
max: 2
77+
passed: true
78+
comment: 'Descriptive labels with units: Price ($), Column (Reversal)'
79+
- id: VQ-07
80+
name: Grid & Legend
81+
score: 0
82+
max: 2
83+
passed: false
84+
comment: Support/resistance trend lines from code do not appear in output
85+
spec_compliance:
86+
score: 18
87+
max: 25
88+
items:
89+
- id: SC-01
90+
name: Plot Type
91+
score: 6
92+
max: 8
93+
passed: true
94+
comment: Correct P&F concept but uses circles instead of X/O symbols per spec
95+
- id: SC-02
96+
name: Data Mapping
97+
score: 5
98+
max: 5
99+
passed: true
100+
comment: X-axis shows columns (reversals), Y-axis shows price correctly
101+
- id: SC-03
102+
name: Required Features
103+
score: 2
104+
max: 5
105+
passed: false
106+
comment: Missing visible support/resistance trend lines that spec requires
107+
- id: SC-04
108+
name: Data Range
109+
score: 3
110+
max: 3
111+
passed: true
112+
comment: All data visible within range
113+
- id: SC-05
114+
name: Legend Accuracy
115+
score: 2
116+
max: 2
117+
passed: true
118+
comment: Legend correctly identifies rising and falling series
119+
- id: SC-06
120+
name: Title Format
121+
score: 2
122+
max: 2
123+
passed: true
124+
comment: 'Correct format: point-and-figure-basic · pygal · pyplots.ai'
125+
data_quality:
126+
score: 17
127+
max: 20
128+
items:
129+
- id: DQ-01
130+
name: Feature Coverage
131+
score: 6
132+
max: 8
133+
passed: true
134+
comment: Shows bullish and bearish columns with reversals, but trend lines
135+
missing
136+
- id: DQ-02
137+
name: Realistic Context
138+
score: 7
139+
max: 7
140+
passed: true
141+
comment: Stock price data is realistic ($82-$110 range, plausible movements)
142+
- id: DQ-03
143+
name: Appropriate Scale
144+
score: 4
145+
max: 5
146+
passed: true
147+
comment: $2 box size and 3-box reversal are appropriate for the price range
148+
code_quality:
149+
score: 10
150+
max: 10
151+
items:
152+
- id: CQ-01
153+
name: KISS Structure
154+
score: 3
155+
max: 3
156+
passed: true
157+
comment: 'Linear flow: imports, data generation, P&F calculation, chart creation,
158+
save'
159+
- id: CQ-02
160+
name: Reproducibility
161+
score: 3
162+
max: 3
163+
passed: true
164+
comment: Uses np.random.seed(42)
165+
- id: CQ-03
166+
name: Clean Imports
167+
score: 2
168+
max: 2
169+
passed: true
170+
comment: Only numpy, pygal, and Style are imported
171+
- id: CQ-04
172+
name: No Deprecated API
173+
score: 1
174+
max: 1
175+
passed: true
176+
comment: Current pygal API usage
177+
- id: CQ-05
178+
name: Output Correct
179+
score: 1
180+
max: 1
181+
passed: true
182+
comment: Saves as plot.png and plot.html
183+
library_features:
184+
score: 3
185+
max: 5
186+
items:
187+
- id: LF-01
188+
name: Distinctive Features
189+
score: 3
190+
max: 5
191+
passed: true
192+
comment: Uses pygal XY chart, custom Style, SVG native with PNG export, but
193+
trend lines with stroke=True not rendering properly
194+
verdict: APPROVED
195+
impl_tags:
196+
dependencies: []
197+
techniques:
198+
- html-export
199+
patterns:
200+
- data-generation
201+
- iteration-over-groups
202+
dataprep: []
203+
styling:
204+
- grid-styling

0 commit comments

Comments
 (0)