{"spec_id":"point-and-figure-basic","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\npoint-and-figure-basic: Point and Figure Chart\nLibrary: plotly 6.7.0 | Python 3.13.13\nQuality: 92/100 | Updated: 2026-05-20\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport plotly.graph_objects as go\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\"\nGRID = \"rgba(26,26,23,0.10)\" if THEME == \"light\" else \"rgba(240,239,232,0.10)\"\n\nCOLOR_X = \"#009E73\"  # Okabe-Ito pos 1 — X (Rising), colorblind-safe\nCOLOR_O = \"#AE3030\"  # imprint red — O (Falling)\n\n# Data — synthetic stock price with distinct trend phases\nnp.random.seed(42)\nn_days = 300\n\ninitial_price = 100.0\nreturns = np.random.normal(0.001, 0.02, n_days)\nreturns[50:100] += 0.003\nreturns[120:150] -= 0.004\nreturns[180:230] += 0.002\nreturns[250:280] -= 0.003\n\nclose = initial_price * np.cumprod(1 + returns)\n\n# P&F parameters\nbox_size = 2.0\nreversal = 3\n\n# Build P&F columns\ncolumns = []\ncurrent_direction = None\ncurrent_column = []\ncolumn_index = 0\ncurrent_price = np.floor(close[0] / box_size) * box_size\n\nfor i in range(1, len(close)):\n    price = close[i]\n\n    if current_direction is None:\n        price_boxes = np.floor(price / box_size) * box_size\n        if price_boxes > current_price + box_size:\n            current_direction = \"X\"\n            for p in np.arange(current_price, price_boxes + box_size, box_size):\n                current_column.append(p)\n            current_price = price_boxes\n        elif price_boxes < current_price - box_size:\n            current_direction = \"O\"\n            for p in np.arange(current_price, price_boxes - box_size, -box_size):\n                current_column.append(p)\n            current_price = price_boxes\n    else:\n        price_boxes = np.floor(price / box_size) * box_size\n\n        if current_direction == \"X\":\n            if price_boxes > current_price:\n                for p in np.arange(current_price + box_size, price_boxes + box_size, box_size):\n                    current_column.append(p)\n                current_price = price_boxes\n            elif price_boxes <= current_price - reversal * box_size:\n                if current_column:\n                    columns.append((current_column.copy(), \"X\", column_index))\n                    column_index += 1\n                current_direction = \"O\"\n                current_column = []\n                for p in np.arange(current_price - box_size, price_boxes - box_size, -box_size):\n                    current_column.append(p)\n                current_price = price_boxes\n        else:\n            if price_boxes < current_price:\n                for p in np.arange(current_price - box_size, price_boxes - box_size, -box_size):\n                    current_column.append(p)\n                current_price = price_boxes\n            elif price_boxes >= current_price + reversal * box_size:\n                if current_column:\n                    columns.append((current_column.copy(), \"O\", column_index))\n                    column_index += 1\n                current_direction = \"X\"\n                current_column = []\n                for p in np.arange(current_price + box_size, price_boxes + box_size, box_size):\n                    current_column.append(p)\n                current_price = price_boxes\n\nif current_column:\n    columns.append((current_column.copy(), current_direction, column_index))\n\n# Plot\nfig = go.Figure()\n\n# X and O symbols for each column\nfor prices, direction, col_idx in columns:\n    if not prices:\n        continue\n    color = COLOR_X if direction == \"X\" else COLOR_O\n    symbol = \"X\" if direction == \"X\" else \"O\"\n    label = \"Rising\" if direction == \"X\" else \"Falling\"\n    fig.add_trace(\n        go.Scatter(\n            x=[col_idx] * len(prices),\n            y=prices,\n            mode=\"text\",\n            text=[symbol] * len(prices),\n            textfont={\"size\": 11, \"color\": color, \"family\": \"Arial Black\"},\n            showlegend=False,\n            hovertemplate=f\"Column: {col_idx}<br>Price: $%{{y:.0f}}<br>{label}<extra></extra>\",\n        )\n    )\n\n# Compute chart bounds\nall_prices = [p for prices, _, _ in columns for p in prices]\nmin_price = min(all_prices) - box_size * 2 if all_prices else 80\nmax_price = max(all_prices) + box_size * 2 if all_prices else 140\nmax_col_idx = columns[-1][2] if columns else 0\n\n# Support trend line: from O column with lowest minimum, ascending 45° (1 box per column)\no_bottoms = [(col_idx, min(prices)) for prices, direction, col_idx in columns if direction == \"O\" and prices]\nif o_bottoms and max_col_idx > 0:\n    support_col, support_price = min(o_bottoms, key=lambda t: t[1])\n    if support_col < max_col_idx:\n        fig.add_trace(\n            go.Scatter(\n                x=[support_col, max_col_idx],\n                y=[support_price, support_price + (max_col_idx - support_col) * box_size],\n                mode=\"lines\",\n                line={\"color\": INK_SOFT, \"width\": 1.5, \"dash\": \"dash\"},\n                name=\"Support (45°)\",\n                showlegend=True,\n            )\n        )\n\n# Resistance trend line: from X column with highest maximum, descending 45°\nx_tops = [(col_idx, max(prices)) for prices, direction, col_idx in columns if direction == \"X\" and prices]\nif x_tops and max_col_idx > 0:\n    resist_col, resist_price = max(x_tops, key=lambda t: t[1])\n    if resist_col < max_col_idx:\n        fig.add_trace(\n            go.Scatter(\n                x=[resist_col, max_col_idx],\n                y=[resist_price, resist_price - (max_col_idx - resist_col) * box_size],\n                mode=\"lines\",\n                line={\"color\": INK_SOFT, \"width\": 1.5, \"dash\": \"dot\"},\n                name=\"Resistance (45°)\",\n                showlegend=True,\n            )\n        )\n\n# Legend entries — square markers avoid the text-mode legend rendering bug\nfig.add_trace(\n    go.Scatter(\n        x=[None],\n        y=[None],\n        mode=\"markers\",\n        marker={\"symbol\": \"square\", \"size\": 12, \"color\": COLOR_X},\n        name=\"X (Rising)\",\n        showlegend=True,\n    )\n)\nfig.add_trace(\n    go.Scatter(\n        x=[None],\n        y=[None],\n        mode=\"markers\",\n        marker={\"symbol\": \"square\", \"size\": 12, \"color\": COLOR_O},\n        name=\"O (Falling)\",\n        showlegend=True,\n    )\n)\n\n# Style\nfig.update_layout(\n    title={\n        \"text\": \"point-and-figure-basic · python · plotly · anyplot.ai\",\n        \"font\": {\"size\": 16, \"color\": INK},\n        \"x\": 0.5,\n        \"xanchor\": \"center\",\n    },\n    xaxis={\n        \"title\": {\"text\": \"Column (Reversal)\", \"font\": {\"size\": 12, \"color\": INK}},\n        \"tickfont\": {\"size\": 10, \"color\": INK_SOFT},\n        \"showgrid\": True,\n        \"gridwidth\": 1,\n        \"gridcolor\": GRID,\n        \"linecolor\": INK_SOFT,\n        \"zeroline\": False,\n        \"dtick\": 5,\n    },\n    yaxis={\n        \"title\": {\"text\": \"Price ($)\", \"font\": {\"size\": 12, \"color\": INK}},\n        \"tickfont\": {\"size\": 10, \"color\": INK_SOFT},\n        \"showgrid\": True,\n        \"gridwidth\": 1,\n        \"gridcolor\": GRID,\n        \"linecolor\": INK_SOFT,\n        \"dtick\": 10,\n        \"range\": [min_price, max_price],\n    },\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAGE_BG,\n    font={\"color\": INK},\n    legend={\n        \"bgcolor\": ELEVATED_BG,\n        \"bordercolor\": INK_SOFT,\n        \"borderwidth\": 1,\n        \"font\": {\"size\": 10, \"color\": INK_SOFT},\n        \"x\": 1.02,\n        \"y\": 0.98,\n        \"xanchor\": \"left\",\n        \"yanchor\": \"top\",\n    },\n    margin={\"l\": 70, \"r\": 160, \"t\": 70, \"b\": 100},\n)\n\nfig.add_annotation(\n    text=f\"Box Size: ${box_size:.0f} | Reversal: {reversal} boxes\",\n    xref=\"paper\",\n    yref=\"paper\",\n    x=0.5,\n    y=-0.13,\n    showarrow=False,\n    font={\"size\": 10, \"color\": INK_SOFT},\n    xanchor=\"center\",\n)\n\n# Save\nfig.write_image(f\"plot-{THEME}.png\", width=600, height=600, scale=4)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}