{"spec_id":"point-and-figure-basic","library":"matplotlib","language":"python","code":"\"\"\" anyplot.ai\npoint-and-figure-basic: Point and Figure Chart\nLibrary: matplotlib 3.10.9 | Python 3.13.13\nQuality: 86/100 | Updated: 2026-05-20\n\"\"\"\n\nimport os\nimport sys\n\n\n# Remove the script's directory from sys.path to prevent shadowing the installed matplotlib package\nif sys.path and sys.path[0] and \"implementations\" in sys.path[0]:\n    sys.path.pop(0)\n\nimport matplotlib.patches as mpatches\nimport matplotlib.patheffects as pe\nimport matplotlib.pyplot as plt\nimport numpy as np\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\n\n# imprint semantic anchors: green for rising (X), red for falling (O)\nX_COLOR = \"#009E73\"\nO_COLOR = \"#AE3030\"\n\n# Data: synthetic stock price with upward drift\nnp.random.seed(42)\nn_days = 300\nclose = 100.0 * np.cumprod(1 + np.random.normal(0.001, 0.02, n_days))\n\n# P&F parameters\nbox_size = 2.0\nreversal = 3\n\n# Build P&F columns\ncolumns = []\ncurrent_col = None\ncurrent_price = None\n\nfor price in close:\n    if current_col is None:\n        current_col = {\n            \"type\": \"X\",\n            \"start\": np.floor(price / box_size) * box_size,\n            \"end\": np.floor(price / box_size) * box_size,\n        }\n        current_price = current_col[\"end\"]\n        continue\n\n    if current_col[\"type\"] == \"X\":\n        new_high = np.floor(price / box_size) * box_size\n        if new_high > current_col[\"end\"]:\n            current_col[\"end\"] = new_high\n            current_price = new_high\n        elif price <= current_price - reversal * box_size:\n            columns.append(current_col.copy())\n            current_col = {\n                \"type\": \"O\",\n                \"start\": current_col[\"end\"] - box_size,\n                \"end\": np.ceil(price / box_size) * box_size,\n            }\n            current_price = current_col[\"end\"]\n    else:\n        new_low = np.ceil(price / box_size) * box_size\n        if new_low < current_col[\"end\"]:\n            current_col[\"end\"] = new_low\n            current_price = new_low\n        elif price >= current_price + reversal * box_size:\n            columns.append(current_col.copy())\n            current_col = {\n                \"type\": \"X\",\n                \"start\": current_col[\"end\"] + box_size,\n                \"end\": np.floor(price / box_size) * box_size,\n            }\n            current_price = current_col[\"end\"]\n\nif current_col is not None:\n    columns.append(current_col)\n\nn_cols = len(columns)\nall_prices = [p for col in columns for p in [col[\"start\"], col[\"end\"]]]\ny_min = min(all_prices) - 2 * box_size\ny_max = max(all_prices) + 2 * box_size\n\n# Find the most significant buy signal: X column exceeding prior X column peak by most boxes\nprev_x_high = None\nbest_breakout = None\nfor i, col in enumerate(columns):\n    if col[\"type\"] == \"X\":\n        col_high = max(col[\"start\"], col[\"end\"])\n        if prev_x_high is not None and col_high > prev_x_high:\n            excess = col_high - prev_x_high\n            if best_breakout is None or excess > best_breakout[3]:\n                best_breakout = (i, prev_x_high, col_high, excess)\n        prev_x_high = col_high\n\n# Trend line anchors\n# Support: from overall lowest low, rising 45° (one box per column to the right)\nall_lows = [(i, min(col[\"start\"], col[\"end\"])) for i, col in enumerate(columns)]\nlowest_col_idx, lowest_price = min(all_lows, key=lambda x: x[1])\n\n# Resistance: from top of first column, falling 45° (one box per column to the right)\nfirst_col_high = max(columns[0][\"start\"], columns[0][\"end\"])\n\n# Plot\nfig, ax = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\n# Draw X and O symbols using patheffects for crisp separation between dense symbols\nsymbol_stroke = [pe.withStroke(linewidth=2, foreground=PAGE_BG)]\nfor col_idx, col in enumerate(columns):\n    lo = min(col[\"start\"], col[\"end\"])\n    hi = max(col[\"start\"], col[\"end\"])\n    char = \"X\" if col[\"type\"] == \"X\" else \"O\"\n    color = X_COLOR if col[\"type\"] == \"X\" else O_COLOR\n    for box_price in np.arange(lo, hi + box_size / 2, box_size):\n        ax.text(\n            col_idx,\n            box_price,\n            char,\n            fontsize=8,\n            fontweight=\"bold\",\n            ha=\"center\",\n            va=\"center\",\n            color=color,\n            path_effects=symbol_stroke,\n        )\n\n# Support trend line: 45° upward from the overall lowest low\nsupport_x = np.array([0, n_cols - 1])\nsupport_y = np.array(\n    [lowest_price - lowest_col_idx * box_size, lowest_price + (n_cols - 1 - lowest_col_idx) * box_size]\n)\nax.plot(support_x, support_y, color=X_COLOR, linewidth=0.8, linestyle=\"--\", alpha=0.5, zorder=0)\n\n# Resistance trend line: 45° downward from the top of the first column\nresistance_x = np.array([0, n_cols - 1])\nresistance_y = np.array([first_col_high, first_col_high - (n_cols - 1) * box_size])\nax.plot(resistance_x, resistance_y, color=O_COLOR, linewidth=0.8, linestyle=\"--\", alpha=0.5, zorder=0)\n\n# Axes configuration\nax.set_xlim(-0.5, n_cols - 0.5)\nax.set_ylim(y_min, y_max)\n\n# Major ticks at $10 intervals (labeled + gridlines); minor ticks at $2 box intervals (tick marks only)\nmajor_ticks = np.arange(np.floor(y_min / 10) * 10, np.ceil(y_max / 10) * 10 + 10, 10)\nminor_ticks = np.arange(\n    np.floor(y_min / box_size) * box_size, np.ceil(y_max / box_size) * box_size + box_size, box_size\n)\nax.set_yticks(major_ticks)\nax.set_yticks(minor_ticks, minor=True)\nax.tick_params(axis=\"y\", which=\"major\", labelsize=8, colors=INK_SOFT, length=4)\nax.tick_params(axis=\"y\", which=\"minor\", length=2, colors=INK_SOFT, labelsize=0)\nax.tick_params(axis=\"x\", labelsize=8, colors=INK_SOFT)\n\n# Style\nax.yaxis.grid(True, which=\"major\", alpha=0.15, linewidth=0.6, color=INK)\nax.spines[\"top\"].set_visible(False)\nax.spines[\"right\"].set_visible(False)\nfor s in (\"left\", \"bottom\"):\n    ax.spines[s].set_color(INK_SOFT)\n\nax.set_xlabel(\"Column (Reversal Number)\", fontsize=10, color=INK)\nax.set_ylabel(\"Price ($)\", fontsize=10, color=INK)\nax.set_title(\"point-and-figure-basic · python · matplotlib · anyplot.ai\", fontsize=12, fontweight=\"medium\", color=INK)\n\n# Annotate the most prominent buy signal — X column breakout above prior X peak (narrative focal point)\nif best_breakout is not None:\n    b_col, _, b_col_high, _ = best_breakout\n    text_offset = 2.5 if b_col < n_cols * 0.65 else -3.5\n    ax.annotate(\n        \"Buy Signal\\n(Column Breakout)\",\n        xy=(b_col, b_col_high),\n        xytext=(b_col + text_offset, b_col_high + 4),\n        fontsize=7,\n        fontweight=\"bold\",\n        color=X_COLOR,\n        ha=\"center\",\n        arrowprops={\"arrowstyle\": \"->\", \"color\": X_COLOR, \"lw\": 0.8},\n        bbox={\"facecolor\": ELEVATED_BG, \"edgecolor\": X_COLOR, \"alpha\": 0.9, \"boxstyle\": \"round,pad=0.3\"},\n    )\n\n# Legend using color patches (avoids marker/text mismatch)\nx_patch = mpatches.Patch(facecolor=X_COLOR, label=\"Rising (X)\", edgecolor=PAGE_BG)\no_patch = mpatches.Patch(facecolor=O_COLOR, label=\"Falling (O)\", edgecolor=PAGE_BG)\nleg = ax.legend(handles=[x_patch, o_patch], fontsize=8, loc=\"upper left\")\nleg.get_frame().set_facecolor(ELEVATED_BG)\nleg.get_frame().set_edgecolor(INK_SOFT)\nplt.setp(leg.get_texts(), color=INK_SOFT)\n\nplt.tight_layout()\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, bbox_inches=\"tight\", facecolor=PAGE_BG)\n"}