{"spec_id":"point-and-figure-basic","library":"seaborn","language":"python","code":"\"\"\" anyplot.ai\npoint-and-figure-basic: Point and Figure Chart\nLibrary: seaborn 0.13.2 | Python 3.13.13\nQuality: 88/100 | Updated: 2026-05-20\n\"\"\"\n\nimport os\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nimport seaborn as sns\nfrom matplotlib.lines import Line2D\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\"\nBULL_COLOR = \"#009E73\"  # imprint green — X columns (bullish)\nBEAR_COLOR = \"#AE3030\"  # imprint red — O columns (bearish)\n\nsns.set_theme(\n    style=\"ticks\",\n    rc={\n        \"figure.facecolor\": PAGE_BG,\n        \"axes.facecolor\": PAGE_BG,\n        \"axes.edgecolor\": INK_SOFT,\n        \"axes.labelcolor\": INK,\n        \"text.color\": INK,\n        \"xtick.color\": INK_SOFT,\n        \"ytick.color\": INK_SOFT,\n        \"grid.color\": INK,\n        \"grid.alpha\": 0.10,\n        \"legend.facecolor\": ELEVATED_BG,\n        \"legend.edgecolor\": INK_SOFT,\n    },\n)\n\n# Synthetic biotech stock over 300 trading days\nnp.random.seed(42)\nn_days = 300\n\nbase_price = 80.0\nreturns = np.random.normal(0.001, 0.018, n_days)\nreturns[40:90] += 0.006\nreturns[130:180] -= 0.006\nreturns[230:270] += 0.005\n\nprices = base_price * np.exp(np.cumsum(returns))\nclose_prices = pd.Series(prices)\n\n# Point and Figure — 3-box reversal, $2 box size\nbox_size = 2.0\nreversal = 3\n\npnf_columns = []\ncurrent_direction = None\ncurrent_col_boxes = []\ncol_index = 0\nfirst_box = round(close_prices.iloc[0] / box_size) * box_size\n\nfor price in close_prices:\n    rounded_price = round(price / box_size) * box_size\n\n    if current_direction is None:\n        current_col_boxes = [first_box]\n        if rounded_price > first_box:\n            current_direction = \"X\"\n            while current_col_boxes[-1] + box_size <= rounded_price:\n                current_col_boxes.append(current_col_boxes[-1] + box_size)\n        elif rounded_price < first_box:\n            current_direction = \"O\"\n            while current_col_boxes[-1] - box_size >= rounded_price:\n                current_col_boxes.append(current_col_boxes[-1] - box_size)\n        continue\n\n    if current_direction == \"X\":\n        top_box = max(current_col_boxes)\n        if rounded_price >= top_box + box_size:\n            while current_col_boxes[-1] + box_size <= rounded_price:\n                current_col_boxes.append(current_col_boxes[-1] + box_size)\n        elif rounded_price <= top_box - reversal * box_size:\n            pnf_columns.append((col_index, list(current_col_boxes), \"X\"))\n            col_index += 1\n            start_box = top_box - box_size\n            current_col_boxes = [start_box]\n            current_direction = \"O\"\n            while current_col_boxes[-1] - box_size >= rounded_price:\n                current_col_boxes.append(current_col_boxes[-1] - box_size)\n    else:\n        bottom_box = min(current_col_boxes)\n        if rounded_price <= bottom_box - box_size:\n            while current_col_boxes[-1] - box_size >= rounded_price:\n                current_col_boxes.append(current_col_boxes[-1] - box_size)\n        elif rounded_price >= bottom_box + reversal * box_size:\n            pnf_columns.append((col_index, list(current_col_boxes), \"O\"))\n            col_index += 1\n            start_box = bottom_box + box_size\n            current_col_boxes = [start_box]\n            current_direction = \"X\"\n            while current_col_boxes[-1] + box_size <= rounded_price:\n                current_col_boxes.append(current_col_boxes[-1] + box_size)\n\nif current_col_boxes:\n    pnf_columns.append((col_index, current_col_boxes, current_direction or \"X\"))\n\n# Build DataFrame with series labels for seaborn hue encoding\nplot_rows = []\nfor col_idx, boxes, direction in pnf_columns:\n    series = \"X — Rising\" if direction == \"X\" else \"O — Falling\"\n    for box in boxes:\n        plot_rows.append({\"column\": col_idx, \"price\": box, \"series\": series})\n\nplot_df = pd.DataFrame(plot_rows)\n\n# 45-degree trend line anchors\no_bottoms = [(ci, min(boxes)) for ci, boxes, d in pnf_columns if d == \"O\"]\nx_tops = [(ci, max(boxes)) for ci, boxes, d in pnf_columns if d == \"X\"]\n\n# Plot\nfig, ax = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\nprice_min = plot_df[\"price\"].min() - box_size\nprice_max = plot_df[\"price\"].max() + box_size\ncol_max = int(plot_df[\"column\"].max())\n\n# Seaborn hue + style dual-encoding: direction → color (palette) + marker shape\nsns.scatterplot(\n    data=plot_df,\n    x=\"column\",\n    y=\"price\",\n    hue=\"series\",\n    style=\"series\",\n    hue_order=[\"X — Rising\", \"O — Falling\"],\n    style_order=[\"X — Rising\", \"O — Falling\"],\n    palette={\"X — Rising\": BULL_COLOR, \"O — Falling\": BEAR_COLOR},\n    markers={\"X — Rising\": \"X\", \"O — Falling\": \"o\"},\n    s=200,\n    linewidth=2.5,\n    legend=False,\n    ax=ax,\n)\n\n# 45-degree support trend line — ascending from the lowest O-column bottom\nif o_bottoms:\n    supp_col, supp_price = min(o_bottoms, key=lambda t: t[1])\n    x_end = min(col_max + 0.5, supp_col + (price_max - supp_price) / box_size)\n    if x_end > supp_col:\n        ax.plot(\n            [supp_col, x_end],\n            [supp_price, supp_price + (x_end - supp_col) * box_size],\n            \"--\",\n            color=BULL_COLOR,\n            alpha=0.55,\n            linewidth=1.5,\n            zorder=1,\n        )\n        ax.annotate(\n            \"Support\",\n            xy=(supp_col, supp_price),\n            xytext=(4, -12),\n            textcoords=\"offset points\",\n            fontsize=7,\n            color=BULL_COLOR,\n            alpha=0.85,\n        )\n\n# 45-degree resistance trend line — descending from the highest X-column top\nif x_tops:\n    res_col, res_price = max(x_tops, key=lambda t: t[1])\n    x_end = min(col_max + 0.5, res_col + (res_price - price_min) / box_size)\n    if x_end > res_col:\n        ax.plot(\n            [res_col, x_end],\n            [res_price, res_price - (x_end - res_col) * box_size],\n            \"--\",\n            color=BEAR_COLOR,\n            alpha=0.55,\n            linewidth=1.5,\n            zorder=1,\n        )\n        ax.annotate(\n            \"Resistance\",\n            xy=(res_col, res_price),\n            xytext=(4, 4),\n            textcoords=\"offset points\",\n            fontsize=7,\n            color=BEAR_COLOR,\n            alpha=0.85,\n        )\n\n# Style\nax.set_xlabel(\"Column (Reversals)\", fontsize=10, color=INK)\nax.set_ylabel(\"Price ($)\", fontsize=10, color=INK)\nax.set_title(\"point-and-figure-basic · python · seaborn · anyplot.ai\", fontsize=12, fontweight=\"medium\", color=INK)\nax.tick_params(axis=\"both\", labelsize=8, colors=INK_SOFT)\n\nyticks = np.arange(int(price_min / box_size) * box_size, price_max + box_size, box_size * 2)\nax.set_yticks(yticks)\nax.set_ylim(price_min, price_max)\nax.set_xlim(-0.5, col_max + 0.5)\n\nax.yaxis.grid(True, alpha=0.10, linewidth=0.8, color=INK)\nax.spines[\"top\"].set_visible(False)\nax.spines[\"right\"].set_visible(False)\nax.spines[\"left\"].set_color(INK_SOFT)\nax.spines[\"bottom\"].set_color(INK_SOFT)\n\n# Legend: hollow-O convention + trend line entries\nlegend_handles = [\n    Line2D([0], [0], marker=\"X\", color=BULL_COLOR, markersize=8, linewidth=0, markeredgewidth=2.5),\n    Line2D(\n        [0], [0], marker=\"o\", color=BEAR_COLOR, markersize=8, linewidth=0, markerfacecolor=\"none\", markeredgewidth=2.5\n    ),\n    Line2D([0], [0], linestyle=\"--\", color=BULL_COLOR, alpha=0.7, linewidth=1.5),\n    Line2D([0], [0], linestyle=\"--\", color=BEAR_COLOR, alpha=0.7, linewidth=1.5),\n]\nlegend_labels = [\"X — Rising\", \"O — Falling\", \"Support (45°)\", \"Resistance (45°)\"]\nleg = ax.legend(\n    handles=legend_handles,\n    labels=legend_labels,\n    loc=\"upper left\",\n    fontsize=8,\n    frameon=True,\n    facecolor=ELEVATED_BG,\n    edgecolor=INK_SOFT,\n)\nfor text in leg.get_texts():\n    text.set_color(INK)\n\nplt.tight_layout()\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, bbox_inches=\"tight\", facecolor=PAGE_BG)\n"}