Skip to content

Commit 1f8f00f

Browse files
feat(letsplot): implement point-and-figure-basic (#3864)
## Implementation: `point-and-figure-basic` - letsplot Implements the **letsplot** version of `point-and-figure-basic`. **File:** `plots/point-and-figure-basic/implementations/letsplot.py` **Parent Issue:** #3755 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/21047207459)* --------- 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 07690bf commit 1f8f00f

2 files changed

Lines changed: 335 additions & 0 deletions

File tree

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
""" pyplots.ai
2+
point-and-figure-basic: Point and Figure Chart
3+
Library: letsplot 4.8.2 | Python 3.13.11
4+
Quality: 91/100 | Created: 2026-01-15
5+
"""
6+
7+
import numpy as np
8+
import pandas as pd
9+
from lets_plot import (
10+
LetsPlot,
11+
aes,
12+
element_blank,
13+
element_text,
14+
geom_text,
15+
ggplot,
16+
ggsize,
17+
labs,
18+
scale_color_manual,
19+
scale_x_continuous,
20+
scale_y_continuous,
21+
theme,
22+
theme_minimal,
23+
)
24+
from lets_plot.export import ggsave
25+
26+
27+
LetsPlot.setup_html()
28+
29+
# Generate synthetic stock price data
30+
np.random.seed(42)
31+
n_days = 300
32+
33+
# Create realistic price movement with trends
34+
returns = np.random.normal(0.001, 0.02, n_days)
35+
returns[50:100] += 0.005 # Uptrend
36+
returns[150:200] -= 0.004 # Downtrend
37+
returns[250:280] += 0.006 # Uptrend
38+
39+
prices = 100 * np.cumprod(1 + returns)
40+
41+
# Point and Figure parameters
42+
box_size = 2.0 # Each box represents $2
43+
reversal = 3 # 3-box reversal
44+
45+
# Build Point and Figure chart data (inline)
46+
pnf_rows = [] # List of (column_index, price_level, symbol, direction)
47+
current_box = int(prices[0] / box_size) * box_size
48+
direction = None
49+
column_idx = 0
50+
51+
for price in prices:
52+
price_box = int(price / box_size) * box_size
53+
54+
if direction is None:
55+
# First move determines direction
56+
if price_box > current_box:
57+
direction = "up"
58+
for b in range(int(current_box / box_size), int(price_box / box_size) + 1):
59+
pnf_rows.append((column_idx, b * box_size, "X", "up"))
60+
current_box = price_box
61+
elif price_box < current_box:
62+
direction = "down"
63+
for b in range(int(price_box / box_size), int(current_box / box_size) + 1):
64+
pnf_rows.append((column_idx, b * box_size, "O", "down"))
65+
current_box = price_box
66+
67+
elif direction == "up":
68+
if price_box > current_box:
69+
# Continue up
70+
for b in range(int(current_box / box_size) + 1, int(price_box / box_size) + 1):
71+
pnf_rows.append((column_idx, b * box_size, "X", "up"))
72+
current_box = price_box
73+
elif price_box <= current_box - reversal * box_size:
74+
# Reversal down
75+
column_idx += 1
76+
for b in range(int(price_box / box_size), int(current_box / box_size)):
77+
pnf_rows.append((column_idx, b * box_size, "O", "down"))
78+
current_box = price_box
79+
direction = "down"
80+
81+
elif direction == "down":
82+
if price_box < current_box:
83+
# Continue down
84+
for b in range(int(price_box / box_size), int(current_box / box_size)):
85+
pnf_rows.append((column_idx, b * box_size, "O", "down"))
86+
current_box = price_box
87+
elif price_box >= current_box + reversal * box_size:
88+
# Reversal up
89+
column_idx += 1
90+
for b in range(int(current_box / box_size) + 1, int(price_box / box_size) + 1):
91+
pnf_rows.append((column_idx, b * box_size, "X", "up"))
92+
current_box = price_box
93+
direction = "up"
94+
95+
# Convert to DataFrame for plotting
96+
df_pnf = pd.DataFrame(pnf_rows, columns=["column", "price", "symbol", "direction"])
97+
df_pnf = df_pnf.drop_duplicates(subset=["column", "price"])
98+
99+
# Create the plot using geom_text to display X and O symbols
100+
plot = (
101+
ggplot(df_pnf, aes(x="column", y="price", label="symbol", color="direction"))
102+
+ geom_text(size=16, fontface="bold")
103+
+ scale_color_manual(values={"up": "#16a34a", "down": "#dc2626"})
104+
+ scale_x_continuous(name="Column (Reversal Number)")
105+
+ scale_y_continuous(name="Price ($)")
106+
+ labs(title="point-and-figure-basic · letsplot · pyplots.ai")
107+
+ theme_minimal()
108+
+ theme(
109+
plot_title=element_text(size=24, face="bold"),
110+
axis_title=element_text(size=20),
111+
axis_text=element_text(size=16),
112+
legend_position="none",
113+
panel_grid_major_x=element_blank(),
114+
panel_grid_minor_x=element_blank(),
115+
)
116+
+ ggsize(1600, 900)
117+
)
118+
119+
# Save PNG (scale 3x for 4800 × 2700 px)
120+
ggsave(plot, "plot.png", path=".", scale=3)
121+
122+
# Save HTML for interactivity
123+
ggsave(plot, "plot.html", path=".")
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
library: letsplot
2+
specification_id: point-and-figure-basic
3+
created: '2026-01-15T21:43:30Z'
4+
updated: '2026-01-15T21:47:33Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 21047207459
7+
issue: 3755
8+
python_version: 3.13.11
9+
library_version: 4.8.2
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/point-and-figure-basic/letsplot/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/point-and-figure-basic/letsplot/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/point-and-figure-basic/letsplot/plot.html
13+
quality_score: 91
14+
review:
15+
strengths:
16+
- Excellent implementation of the P&F algorithm with correct box size and 3-box
17+
reversal logic
18+
- Clean use of lets-plot ggplot2-style grammar with appropriate aesthetics mapping
19+
- Perfect title format and axis labeling with descriptive names and units
20+
- Good color choices with green/red that also include shape distinction (X/O) for
21+
accessibility
22+
- Proper handling of column-based (not time-based) X-axis which is key to P&F charts
23+
- High-resolution export with scale=3 for 4800x2700 output
24+
weaknesses:
25+
- Grid styling could be improved - horizontal grid is visible but could use more
26+
subtle alpha
27+
- The X and O symbols could be slightly larger/bolder for better visual impact at
28+
the target resolution
29+
- Spec mentions adding support/resistance trend lines which are not implemented
30+
(optional per spec)
31+
image_description: The plot displays a Point and Figure (P&F) chart with green X
32+
symbols representing rising price columns (bullish) and red O symbols representing
33+
falling price columns (bearish). The chart shows price movement from approximately
34+
$80 to $155 across 23 columns (reversal numbers 0-22). The X-axis is labeled "Column
35+
(Reversal Number)" and Y-axis is labeled "Price ($)". The title correctly displays
36+
"point-and-figure-basic · letsplot · pyplots.ai". The chart demonstrates clear
37+
alternating columns of X's and O's with appropriate spacing, showing multiple
38+
trends including uptrends and downtrends. The overall layout is clean with a minimal
39+
theme, subtle horizontal grid lines, and no legend (appropriately hidden since
40+
color coding is self-explanatory).
41+
criteria_checklist:
42+
visual_quality:
43+
score: 36
44+
max: 40
45+
items:
46+
- id: VQ-01
47+
name: Text Legibility
48+
score: 10
49+
max: 10
50+
passed: true
51+
comment: Title, axis labels, and tick marks are all clearly readable at full
52+
size
53+
- id: VQ-02
54+
name: No Overlap
55+
score: 8
56+
max: 8
57+
passed: true
58+
comment: No overlapping text or symbols
59+
- id: VQ-03
60+
name: Element Visibility
61+
score: 6
62+
max: 8
63+
passed: true
64+
comment: X and O symbols are visible and appropriately sized, though could
65+
be slightly bolder
66+
- id: VQ-04
67+
name: Color Accessibility
68+
score: 5
69+
max: 5
70+
passed: true
71+
comment: Green and red are distinguishable, X vs O shape provides secondary
72+
distinction
73+
- id: VQ-05
74+
name: Layout Balance
75+
score: 5
76+
max: 5
77+
passed: true
78+
comment: Good use of canvas space, plot fills majority of area with balanced
79+
margins
80+
- id: VQ-06
81+
name: Axis Labels
82+
score: 2
83+
max: 2
84+
passed: true
85+
comment: Price ($) and Column (Reversal Number) are descriptive with units
86+
- id: VQ-07
87+
name: Grid & Legend
88+
score: 0
89+
max: 2
90+
passed: false
91+
comment: Legend hidden appropriately but horizontal grid could be more subtle
92+
spec_compliance:
93+
score: 25
94+
max: 25
95+
items:
96+
- id: SC-01
97+
name: Plot Type
98+
score: 8
99+
max: 8
100+
passed: true
101+
comment: Correct Point and Figure chart with X and O columns
102+
- id: SC-02
103+
name: Data Mapping
104+
score: 5
105+
max: 5
106+
passed: true
107+
comment: Columns represent reversals, Y-axis shows price levels
108+
- id: SC-03
109+
name: Required Features
110+
score: 5
111+
max: 5
112+
passed: true
113+
comment: Uses X for rising, O for falling, green for bullish, red for bearish,
114+
3-box reversal
115+
- id: SC-04
116+
name: Data Range
117+
score: 3
118+
max: 3
119+
passed: true
120+
comment: Full price range visible from ~$80 to ~$155
121+
- id: SC-05
122+
name: Legend Accuracy
123+
score: 2
124+
max: 2
125+
passed: true
126+
comment: No legend needed, color/symbol mapping is standard P&F convention
127+
- id: SC-06
128+
name: Title Format
129+
score: 2
130+
max: 2
131+
passed: true
132+
comment: Correctly formatted as point-and-figure-basic · letsplot · pyplots.ai
133+
data_quality:
134+
score: 18
135+
max: 20
136+
items:
137+
- id: DQ-01
138+
name: Feature Coverage
139+
score: 7
140+
max: 8
141+
passed: true
142+
comment: Shows both uptrends and downtrends, multiple reversals, but could
143+
show more distinct support/resistance patterns
144+
- id: DQ-02
145+
name: Realistic Context
146+
score: 7
147+
max: 7
148+
passed: true
149+
comment: Stock price simulation with realistic price movements and trends
150+
- id: DQ-03
151+
name: Appropriate Scale
152+
score: 4
153+
max: 5
154+
passed: true
155+
comment: Price range $80-155 is realistic for stocks, box size of $2 is appropriate
156+
code_quality:
157+
score: 9
158+
max: 10
159+
items:
160+
- id: CQ-01
161+
name: KISS Structure
162+
score: 3
163+
max: 3
164+
passed: true
165+
comment: 'Linear flow: imports → data generation → P&F algorithm → plot →
166+
save'
167+
- id: CQ-02
168+
name: Reproducibility
169+
score: 3
170+
max: 3
171+
passed: true
172+
comment: Uses np.random.seed(42)
173+
- id: CQ-03
174+
name: Clean Imports
175+
score: 2
176+
max: 2
177+
passed: true
178+
comment: All imports are used
179+
- id: CQ-04
180+
name: No Deprecated API
181+
score: 1
182+
max: 1
183+
passed: true
184+
comment: Uses current lets-plot API
185+
- id: CQ-05
186+
name: Output Correct
187+
score: 0
188+
max: 1
189+
passed: false
190+
comment: Saves to plot.png but path argument is explicit rather than implicit
191+
library_features:
192+
score: 3
193+
max: 5
194+
items:
195+
- id: LF-01
196+
name: Distinctive Features
197+
score: 3
198+
max: 5
199+
passed: true
200+
comment: Uses geom_text for symbol rendering, ggplot grammar, theme customization,
201+
and ggsave with scale parameter
202+
verdict: APPROVED
203+
impl_tags:
204+
dependencies: []
205+
techniques:
206+
- html-export
207+
patterns:
208+
- data-generation
209+
- iteration-over-groups
210+
dataprep: []
211+
styling:
212+
- grid-styling

0 commit comments

Comments
 (0)