Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 172 additions & 0 deletions plots/point-and-figure-basic/implementations/pygal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
""" pyplots.ai
point-and-figure-basic: Point and Figure Chart
Library: pygal 3.1.0 | Python 3.13.11
Quality: 72/100 | Created: 2026-01-15
"""

import numpy as np
import pygal
from pygal.style import Style


# Random seed for reproducibility
np.random.seed(42)

# Generate realistic stock price data (6 months of daily data)
n_days = 150
base_price = 100.0
returns = np.random.normal(0.001, 0.02, n_days)
prices = base_price * np.cumprod(1 + returns)

# P&F parameters
box_size = 2.0
reversal = 3

# Calculate P&F chart data - inline rounding (KISS - no helper functions)
columns = []
current_direction = None
current_column_start = np.floor(prices[0] / box_size) * box_size
current_column_end = current_column_start

for price in prices[1:]:
rounded_price = np.floor(price / box_size) * box_size

if current_direction is None:
if rounded_price >= current_column_end + box_size:
current_direction = "X"
current_column_end = rounded_price
elif rounded_price <= current_column_end - box_size:
current_direction = "O"
current_column_end = rounded_price
elif current_direction == "X":
if rounded_price >= current_column_end + box_size:
current_column_end = rounded_price
elif rounded_price <= current_column_end - (reversal * box_size):
columns.append((current_column_start, current_column_end, "X"))
current_column_start = current_column_end - box_size
current_column_end = rounded_price
current_direction = "O"
else:
if rounded_price <= current_column_end - box_size:
current_column_end = rounded_price
elif rounded_price >= current_column_end + (reversal * box_size):
columns.append((current_column_start, current_column_end, "O"))
current_column_start = current_column_end + box_size
current_column_end = rounded_price
current_direction = "X"

if current_direction:
columns.append((current_column_start, current_column_end, current_direction))

# Colorblind-safe colors (blue for rising X, orange for falling O)
x_color = "#0066CC"
o_color = "#E56B00"
support_color = "#228B22"
resistance_color = "#8B0000"

# Custom style with subtle grid
custom_style = Style(
background="white",
plot_background="white",
foreground="#333333",
foreground_strong="#333333",
foreground_subtle="#DDDDDD", # Subtle grid lines
colors=(x_color, o_color, support_color, resistance_color),
title_font_size=72,
label_font_size=48,
major_label_font_size=42,
legend_font_size=48,
value_font_size=42,
tooltip_font_size=36,
stroke_width=6,
guide_stroke_dasharray="4,4", # Dashed grid for subtlety
)

# Collect price range
all_prices = []
for start, end, _ in columns:
all_prices.extend([start, end])

y_min = min(all_prices) - box_size
y_max = max(all_prices) + box_size
y_labels = list(np.arange(y_min, y_max + box_size, box_size))

# Create XY chart with visible dots for X and O markers
# Pygal uses circles as markers - distinguish X/O through color and legend
chart = pygal.XY(
width=4800,
height=2700,
style=custom_style,
title="point-and-figure-basic · pygal · pyplots.ai",
show_legend=True,
legend_at_bottom=True,
legend_at_bottom_columns=4,
show_y_guides=True,
show_x_guides=False, # Only horizontal guides for cleaner look
x_title="Column (Reversal)",
y_title="Price ($)",
margin=50,
margin_bottom=200,
margin_left=200,
margin_top=150,
margin_right=120, # Better balance
y_labels=y_labels,
range=(y_min, y_max),
dots_size=22,
stroke=False,
)

# Build X and O data points
x_points = []
o_points = []

for col_idx, (start, end, direction) in enumerate(columns):
low = min(start, end)
high = max(start, end)
if direction == "X":
for price_level in np.arange(low, high + box_size, box_size):
x_points.append((col_idx, price_level))
else:
for price_level in np.arange(low, high + box_size, box_size):
o_points.append((col_idx, price_level))

# Add series - X markers (rising) and O markers (falling)
# Legend clearly shows X and O symbols to identify the markers
chart.add("X (Rising)", x_points, stroke=False)
chart.add("O (Falling)", o_points, stroke=False)

# Calculate support and resistance trend lines
support_lows = []
resistance_highs = []

for col_idx, (start, end, direction) in enumerate(columns):
low = min(start, end)
high = max(start, end)
if direction == "O":
support_lows.append((col_idx, low))
else:
resistance_highs.append((col_idx, high))

# Support line (45-degree uptrend from O column lows) - with visible endpoints
if len(support_lows) >= 2:
support_start = support_lows[0]
support_end_col = min(support_start[0] + 5, len(columns) - 1)
support_line = [
(support_start[0], support_start[1]),
(support_end_col, support_start[1] + (support_end_col - support_start[0]) * box_size),
]
chart.add("Support (45° up)", support_line, stroke=True, dots_size=10)

# Resistance line (45-degree downtrend from X column highs) - with visible endpoints
if len(resistance_highs) >= 2:
resistance_start = resistance_highs[0]
resistance_end_col = min(resistance_start[0] + 5, len(columns) - 1)
resistance_line = [
(resistance_start[0], resistance_start[1]),
(resistance_end_col, resistance_start[1] - (resistance_end_col - resistance_start[0]) * box_size),
]
chart.add("Resistance (45° down)", resistance_line, stroke=True, dots_size=10)

# Save outputs
chart.render_to_png("plot.png")
chart.render_to_file("plot.html")
204 changes: 204 additions & 0 deletions plots/point-and-figure-basic/metadata/pygal.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
library: pygal
specification_id: point-and-figure-basic
created: '2026-01-15T21:41:11Z'
updated: '2026-01-15T22:05:05Z'
generated_by: claude-opus-4-5-20251101
workflow_run: 21047207399
issue: 3755
python_version: 3.13.11
library_version: 3.1.0
preview_url: https://storage.googleapis.com/pyplots-images/plots/point-and-figure-basic/pygal/plot.png
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/point-and-figure-basic/pygal/plot_thumb.png
preview_html: https://storage.googleapis.com/pyplots-images/plots/point-and-figure-basic/pygal/plot.html
quality_score: 72
review:
strengths:
- Colorblind-safe blue/orange palette is excellent
- Clean KISS code structure with clear P&F algorithm
- Correct title format and descriptive axis labels
- Realistic stock price data with appropriate box size
- Good use of pygal custom Style for font sizing and grid
weaknesses:
- Support and resistance trend lines do not appear in rendered output despite being
added to chart
- Uses circular dot markers instead of actual X and O symbols (pygal limitation)
- Plot could better utilize canvas space
image_description: 'The plot displays a Point and Figure chart with 8 columns of
price reversals. The chart uses green circular markers for rising (X) columns
and red/orange circular markers for falling (O) columns. The Y-axis shows "Price
($)" ranging from 82 to 110 in $2 increments (box size). The X-axis shows "Column
(Reversal)" from 0 to 7. Subtle horizontal dashed grid lines are visible. The
legend at the bottom shows "X (Rising)" in green and "O (Falling)" in red. The
title correctly displays "point-and-figure-basic · pygal · pyplots.ai". The chart
shows a clear pattern: starting with a bullish rise (column 0), followed by bearish
reversal (column 1), then alternating bullish and bearish columns. Support and
resistance trend lines mentioned in the code are NOT visible in the rendered output.'
criteria_checklist:
visual_quality:
score: 32
max: 40
items:
- id: VQ-01
name: Text Legibility
score: 8
max: 10
passed: true
comment: Title and labels readable but Y-axis label appears slightly small
relative to canvas
- id: VQ-02
name: No Overlap
score: 8
max: 8
passed: true
comment: No overlapping elements, clean layout
- id: VQ-03
name: Element Visibility
score: 6
max: 8
passed: true
comment: Dots visible but circles used instead of actual X and O symbols as
spec requires
- id: VQ-04
name: Color Accessibility
score: 5
max: 5
passed: true
comment: Blue/orange colorblind-safe palette is excellent
- id: VQ-05
name: Layout Balance
score: 3
max: 5
passed: true
comment: Decent canvas use but some wasted space on left margin
- id: VQ-06
name: Axis Labels
score: 2
max: 2
passed: true
comment: 'Descriptive labels with units: Price ($), Column (Reversal)'
- id: VQ-07
name: Grid & Legend
score: 0
max: 2
passed: false
comment: Support/resistance trend lines from code do not appear in output
spec_compliance:
score: 18
max: 25
items:
- id: SC-01
name: Plot Type
score: 6
max: 8
passed: true
comment: Correct P&F concept but uses circles instead of X/O symbols per spec
- id: SC-02
name: Data Mapping
score: 5
max: 5
passed: true
comment: X-axis shows columns (reversals), Y-axis shows price correctly
- id: SC-03
name: Required Features
score: 2
max: 5
passed: false
comment: Missing visible support/resistance trend lines that spec requires
- id: SC-04
name: Data Range
score: 3
max: 3
passed: true
comment: All data visible within range
- id: SC-05
name: Legend Accuracy
score: 2
max: 2
passed: true
comment: Legend correctly identifies rising and falling series
- id: SC-06
name: Title Format
score: 2
max: 2
passed: true
comment: 'Correct format: point-and-figure-basic · pygal · pyplots.ai'
data_quality:
score: 17
max: 20
items:
- id: DQ-01
name: Feature Coverage
score: 6
max: 8
passed: true
comment: Shows bullish and bearish columns with reversals, but trend lines
missing
- id: DQ-02
name: Realistic Context
score: 7
max: 7
passed: true
comment: Stock price data is realistic ($82-$110 range, plausible movements)
- id: DQ-03
name: Appropriate Scale
score: 4
max: 5
passed: true
comment: $2 box size and 3-box reversal are appropriate for the price range
code_quality:
score: 10
max: 10
items:
- id: CQ-01
name: KISS Structure
score: 3
max: 3
passed: true
comment: 'Linear flow: imports, data generation, P&F calculation, chart creation,
save'
- id: CQ-02
name: Reproducibility
score: 3
max: 3
passed: true
comment: Uses np.random.seed(42)
- id: CQ-03
name: Clean Imports
score: 2
max: 2
passed: true
comment: Only numpy, pygal, and Style are imported
- id: CQ-04
name: No Deprecated API
score: 1
max: 1
passed: true
comment: Current pygal API usage
- id: CQ-05
name: Output Correct
score: 1
max: 1
passed: true
comment: Saves as plot.png and plot.html
library_features:
score: 3
max: 5
items:
- id: LF-01
name: Distinctive Features
score: 3
max: 5
passed: true
comment: Uses pygal XY chart, custom Style, SVG native with PNG export, but
trend lines with stroke=True not rendering properly
verdict: APPROVED
impl_tags:
dependencies: []
techniques:
- html-export
patterns:
- data-generation
- iteration-over-groups
dataprep: []
styling:
- grid-styling