From f184ca34deb4e04f72780555b3481176a76d26d3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 15 Jan 2026 21:41:41 +0000 Subject: [PATCH 1/3] feat(bokeh): implement point-and-figure-basic --- .../implementations/bokeh.py | 214 ++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 plots/point-and-figure-basic/implementations/bokeh.py diff --git a/plots/point-and-figure-basic/implementations/bokeh.py b/plots/point-and-figure-basic/implementations/bokeh.py new file mode 100644 index 0000000000..92b1a65524 --- /dev/null +++ b/plots/point-and-figure-basic/implementations/bokeh.py @@ -0,0 +1,214 @@ +"""pyplots.ai +point-and-figure-basic: Point and Figure Chart +Library: bokeh | Python 3.13 +Quality: pending | Created: 2025-01-15 +""" + +import numpy as np +import pandas as pd +from bokeh.io import export_png, save +from bokeh.models import ColumnDataSource +from bokeh.plotting import figure + + +# Generate synthetic stock price data +np.random.seed(42) +n_days = 300 + +# Start price and generate realistic daily returns +start_price = 100 +daily_returns = np.random.normal(0.0005, 0.015, n_days) + +# Add some trending periods +daily_returns[50:80] += 0.003 # Uptrend +daily_returns[100:140] -= 0.004 # Downtrend +daily_returns[180:220] += 0.0035 # Uptrend +daily_returns[240:280] -= 0.003 # Downtrend + +close_prices = start_price * np.cumprod(1 + daily_returns) + +# Generate high/low based on close +volatility = np.abs(np.random.normal(0, 0.01, n_days)) +high_prices = close_prices * (1 + volatility) +low_prices = close_prices * (1 - volatility) + +dates = pd.date_range("2024-01-01", periods=n_days, freq="D") + +df = pd.DataFrame({"date": dates, "high": high_prices, "low": low_prices, "close": close_prices}) + +# Point and Figure calculation +box_size = 2.0 # Each box represents $2 +reversal = 3 # 3-box reversal + +# Calculate P&F columns +columns = [] +current_direction = None # 'X' for up, 'O' for down +current_column_start = None +current_column_end = None + +# Initialize with first price +first_price = df["close"].iloc[0] +box_start = np.floor(first_price / box_size) * box_size + +for _i, row in df.iterrows(): + price = row["close"] + box_price = np.floor(price / box_size) * box_size + + if current_direction is None: + # Initialize first direction based on next movement + current_column_start = box_start + current_column_end = box_start + # Wait for significant move + if price >= box_start + box_size: + current_direction = "X" + current_column_end = box_price + elif price <= box_start - box_size: + current_direction = "O" + current_column_end = box_price + else: + if current_direction == "X": + # In uptrend + if box_price >= current_column_end + box_size: + # Continue up + current_column_end = box_price + elif box_price <= current_column_end - reversal * box_size: + # Reversal down - save current column and start new O column + columns.append({"type": "X", "start": current_column_start, "end": current_column_end}) + current_direction = "O" + current_column_start = current_column_end - box_size + current_column_end = box_price + else: + # In downtrend + if box_price <= current_column_end - box_size: + # Continue down + current_column_end = box_price + elif box_price >= current_column_end + reversal * box_size: + # Reversal up - save current column and start new X column + columns.append({"type": "O", "start": current_column_start, "end": current_column_end}) + current_direction = "X" + current_column_start = current_column_end + box_size + current_column_end = box_price + +# Save the last column +if current_direction is not None: + columns.append({"type": current_direction, "start": current_column_start, "end": current_column_end}) + +# Prepare data for plotting +x_data = [] +o_data = [] + +for col_idx, col in enumerate(columns): + if col["type"] == "X": + start = min(col["start"], col["end"]) + end = max(col["start"], col["end"]) + boxes = np.arange(start, end + box_size / 2, box_size) + for box in boxes: + x_data.append({"col": col_idx, "price": box}) + else: + start = max(col["start"], col["end"]) + end = min(col["start"], col["end"]) + boxes = np.arange(end, start + box_size / 2, box_size) + for box in boxes: + o_data.append({"col": col_idx, "price": box}) + +# Create figure +p = figure( + width=4800, + height=2700, + title="point-and-figure-basic · bokeh · pyplots.ai", + x_axis_label="Column (Reversal)", + y_axis_label="Price ($)", +) + +# Style settings - scaled for 4800x2700 canvas +p.title.text_font_size = "48pt" +p.xaxis.axis_label_text_font_size = "36pt" +p.yaxis.axis_label_text_font_size = "36pt" +p.xaxis.major_label_text_font_size = "28pt" +p.yaxis.major_label_text_font_size = "28pt" + +# Background and grid +p.background_fill_color = "#fafafa" +p.grid.grid_line_alpha = 0.3 +p.grid.grid_line_dash = "dashed" +p.xgrid.grid_line_color = "#cccccc" +p.ygrid.grid_line_color = "#cccccc" + +# Plot X markers (bullish - green) +if x_data: + x_source = ColumnDataSource(data={"col": [d["col"] for d in x_data], "price": [d["price"] for d in x_data]}) + p.text( + x="col", + y="price", + text={"value": "X"}, + source=x_source, + text_font_size="48pt", + text_color="#2E7D32", + text_align="center", + text_baseline="middle", + text_font_style="bold", + ) + +# Plot O markers (bearish - red) +if o_data: + o_source = ColumnDataSource(data={"col": [d["col"] for d in o_data], "price": [d["price"] for d in o_data]}) + p.text( + x="col", + y="price", + text={"value": "O"}, + source=o_source, + text_font_size="48pt", + text_color="#C62828", + text_align="center", + text_baseline="middle", + text_font_style="bold", + ) + +# Add support trend line (45-degree ascending from low) +all_prices = [d["price"] for d in x_data] + [d["price"] for d in o_data] +if all_prices: + min_price = min(all_prices) + max_price = max(all_prices) + + # Find lowest point for support line + support_start_col = 0 + support_start_price = min_price - box_size + support_end_col = len(columns) - 1 + support_end_price = support_start_price + (support_end_col - support_start_col) * box_size + + if support_end_price <= max_price + 2 * box_size: + p.line( + x=[support_start_col, support_end_col], + y=[support_start_price, support_end_price], + line_width=5, + line_color="#306998", + line_dash="solid", + legend_label="Support Trend", + ) + + # Find highest point for resistance line + resistance_start_col = 0 + resistance_start_price = max_price + box_size + resistance_end_col = len(columns) - 1 + resistance_end_price = resistance_start_price - (resistance_end_col - resistance_start_col) * box_size + + if resistance_end_price >= min_price - 2 * box_size: + p.line( + x=[resistance_start_col, resistance_end_col], + y=[resistance_start_price, resistance_end_price], + line_width=5, + line_color="#FFD43B", + line_dash="solid", + legend_label="Resistance Trend", + ) + +# Legend styling +p.legend.location = "top_left" +p.legend.label_text_font_size = "28pt" +p.legend.background_fill_alpha = 0.8 +p.legend.glyph_height = 30 +p.legend.glyph_width = 30 + +# Save as PNG and HTML (interactive) +export_png(p, filename="plot.png") +save(p, filename="plot.html") From 9e65f24234b22ffa5930fadfd74cea4b35f6e5e3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 15 Jan 2026 21:41:57 +0000 Subject: [PATCH 2/3] chore(bokeh): add metadata for point-and-figure-basic --- .../metadata/bokeh.yaml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 plots/point-and-figure-basic/metadata/bokeh.yaml diff --git a/plots/point-and-figure-basic/metadata/bokeh.yaml b/plots/point-and-figure-basic/metadata/bokeh.yaml new file mode 100644 index 0000000000..53933a5fff --- /dev/null +++ b/plots/point-and-figure-basic/metadata/bokeh.yaml @@ -0,0 +1,19 @@ +# Per-library metadata for bokeh implementation of point-and-figure-basic +# Auto-generated by impl-generate.yml + +library: bokeh +specification_id: point-and-figure-basic +created: '2026-01-15T21:41:57Z' +updated: '2026-01-15T21:41:57Z' +generated_by: claude-opus-4-5-20251101 +workflow_run: 21047207311 +issue: 3755 +python_version: 3.13.11 +library_version: 3.8.2 +preview_url: https://storage.googleapis.com/pyplots-images/plots/point-and-figure-basic/bokeh/plot.png +preview_thumb: https://storage.googleapis.com/pyplots-images/plots/point-and-figure-basic/bokeh/plot_thumb.png +preview_html: https://storage.googleapis.com/pyplots-images/plots/point-and-figure-basic/bokeh/plot.html +quality_score: null +review: + strengths: [] + weaknesses: [] From 4e8e974c6710f4cd3131a423906b5878138a0c9a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 15 Jan 2026 21:45:15 +0000 Subject: [PATCH 3/3] chore(bokeh): update quality score 91 and review feedback for point-and-figure-basic --- .../implementations/bokeh.py | 6 +- .../metadata/bokeh.yaml | 209 +++++++++++++++++- 2 files changed, 205 insertions(+), 10 deletions(-) diff --git a/plots/point-and-figure-basic/implementations/bokeh.py b/plots/point-and-figure-basic/implementations/bokeh.py index 92b1a65524..ab918153f8 100644 --- a/plots/point-and-figure-basic/implementations/bokeh.py +++ b/plots/point-and-figure-basic/implementations/bokeh.py @@ -1,7 +1,7 @@ -"""pyplots.ai +""" pyplots.ai point-and-figure-basic: Point and Figure Chart -Library: bokeh | Python 3.13 -Quality: pending | Created: 2025-01-15 +Library: bokeh 3.8.2 | Python 3.13.11 +Quality: 91/100 | Created: 2026-01-15 """ import numpy as np diff --git a/plots/point-and-figure-basic/metadata/bokeh.yaml b/plots/point-and-figure-basic/metadata/bokeh.yaml index 53933a5fff..f2a06c4cc9 100644 --- a/plots/point-and-figure-basic/metadata/bokeh.yaml +++ b/plots/point-and-figure-basic/metadata/bokeh.yaml @@ -1,10 +1,7 @@ -# Per-library metadata for bokeh implementation of point-and-figure-basic -# Auto-generated by impl-generate.yml - library: bokeh specification_id: point-and-figure-basic created: '2026-01-15T21:41:57Z' -updated: '2026-01-15T21:41:57Z' +updated: '2026-01-15T21:45:14Z' generated_by: claude-opus-4-5-20251101 workflow_run: 21047207311 issue: 3755 @@ -13,7 +10,205 @@ library_version: 3.8.2 preview_url: https://storage.googleapis.com/pyplots-images/plots/point-and-figure-basic/bokeh/plot.png preview_thumb: https://storage.googleapis.com/pyplots-images/plots/point-and-figure-basic/bokeh/plot_thumb.png preview_html: https://storage.googleapis.com/pyplots-images/plots/point-and-figure-basic/bokeh/plot.html -quality_score: null +quality_score: 91 review: - strengths: [] - weaknesses: [] + strengths: + - Excellent implementation of the P&F algorithm with proper box size and reversal + logic + - Clean, readable code with proper data generation including realistic trending + periods + - Good visual clarity with appropriately sized X and O markers + - Both PNG and HTML exports for static and interactive viewing + - Proper trend line implementation with 45-degree ascending/descending lines + weaknesses: + - Legend only shows trend lines, missing explanation of X (bullish) and O (bearish) + meanings + - Grid lines not at exact box size intervals as suggested in spec + - Could add HoverTool for interactivity showing price/date information + - Minor empty space on left side of chart + image_description: The plot displays a Point and Figure chart with green "X" symbols + representing bullish (rising price) columns and red "O" symbols representing bearish + (falling price) columns. The chart shows 9 columns (numbered 0-8 on x-axis) with + prices ranging from approximately 78 to 122 on the y-axis. A blue ascending support + trend line starts from the lower left (~77) and slopes upward to the right (~95). + A yellow descending resistance trend line starts from the upper left (~123) and + slopes downward to the right (~105). The title reads "point-and-figure-basic · + bokeh · pyplots.ai". The legend shows "Support Trend" and "Resistance Trend" in + the top left. The background is light gray (#fafafa) with subtle dashed grid lines. + The X and O markers are appropriately sized and bold, making them clearly distinguishable. + criteria_checklist: + visual_quality: + score: 37 + max: 40 + items: + - id: VQ-01 + name: Text Legibility + score: 10 + max: 10 + passed: true + comment: 'All text perfectly readable: title at 48pt, axis labels at 36pt, + tick labels at 28pt' + - id: VQ-02 + name: No Overlap + score: 8 + max: 8 + passed: true + comment: No overlapping text or elements + - id: VQ-03 + name: Element Visibility + score: 8 + max: 8 + passed: true + comment: X and O markers at 48pt are optimal for data density + - id: VQ-04 + name: Color Accessibility + score: 5 + max: 5 + passed: true + comment: Green and red distinguishable; trend lines in blue and yellow provide + additional contrast + - id: VQ-05 + name: Layout Balance + score: 3 + max: 5 + passed: false + comment: Good proportions but some empty space on the left side of the chart + - id: VQ-06 + name: Axis Labels + score: 2 + max: 2 + passed: true + comment: Column (Reversal) and Price ($) are descriptive with units + - id: VQ-07 + name: Grid & Legend + score: 1 + max: 2 + passed: false + comment: Grid is subtle; legend glyph sizes could better represent actual + markers + spec_compliance: + score: 23 + max: 25 + items: + - id: SC-01 + name: Plot Type + score: 8 + max: 8 + passed: true + comment: Correct Point and Figure chart implementation + - id: SC-02 + name: Data Mapping + score: 5 + max: 5 + passed: true + comment: X-axis shows columns (reversals), Y-axis shows price - correct mapping + - id: SC-03 + name: Required Features + score: 4 + max: 5 + passed: false + comment: X's for rising, O's for falling, green/red colors, trend lines present; + grid lines not exactly at box size intervals + - id: SC-04 + name: Data Range + score: 3 + max: 3 + passed: true + comment: All data visible within axes + - id: SC-05 + name: Legend Accuracy + score: 1 + max: 2 + passed: false + comment: Legend only shows trend lines, not X/O meanings + - id: SC-06 + name: Title Format + score: 2 + max: 2 + passed: true + comment: 'Correct format: point-and-figure-basic · bokeh · pyplots.ai' + data_quality: + score: 19 + max: 20 + items: + - id: DQ-01 + name: Feature Coverage + score: 7 + max: 8 + passed: false + comment: Shows multiple columns of X's and O's, alternating trends, support/resistance + lines; could show a clearer breakout pattern + - id: DQ-02 + name: Realistic Context + score: 7 + max: 7 + passed: true + comment: Stock price data from $78-122 with realistic daily returns including + trending periods + - id: DQ-03 + name: Appropriate Scale + score: 5 + max: 5 + passed: true + comment: $2 box size with 3-box reversal is standard; 300 days of data is + appropriate + code_quality: + score: 9 + max: 10 + items: + - id: CQ-01 + name: KISS Structure + score: 3 + max: 3 + passed: true + comment: 'Linear script: imports → data generation → P&F calculation → plotting + → save' + - id: CQ-02 + name: Reproducibility + score: 3 + max: 3 + passed: true + comment: np.random.seed(42) set + - id: CQ-03 + name: Clean Imports + score: 2 + max: 2 + passed: true + comment: All imports are used + - id: CQ-04 + name: No Deprecated API + score: 1 + max: 1 + passed: true + comment: Uses current Bokeh API + - id: CQ-05 + name: Output Correct + score: 0 + max: 1 + passed: false + comment: Saves correctly but uses underscore prefix _i in loop which is unnecessary + library_features: + score: 3 + max: 5 + items: + - id: LF-01 + name: Distinctive Features + score: 3 + max: 5 + passed: false + comment: Uses ColumnDataSource, text glyphs, and HTML export; could utilize + HoverTool for interactive tooltips + verdict: APPROVED +impl_tags: + dependencies: [] + techniques: + - annotations + - html-export + patterns: + - data-generation + - columndatasource + - iteration-over-groups + dataprep: + - time-series + styling: + - grid-styling