{"spec_id":"point-and-figure-basic","library":"altair","language":"python","code":"\"\"\" anyplot.ai\npoint-and-figure-basic: Point and Figure Chart\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-05-20\n\"\"\"\n\nimport os\nimport sys\n\n\n# Prevent the script's own directory from shadowing the 'altair' package\nsys.path = [p for p in sys.path if not p.endswith(\"/python\")]\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\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\"  # Okabe-Ito position 1 — X columns (bullish)\nBEAR_COLOR = \"#AE3030\"  # imprint red — O columns (bearish)\n\n# Data\nnp.random.seed(42)\nn_days = 300\ndates = pd.date_range(\"2024-01-01\", periods=n_days, freq=\"D\")\nreturns = np.random.normal(0.0005, 0.02, n_days)\nclose = 100 * np.cumprod(1 + returns)\n\n# Point and Figure parameters\nbox_size = 2.0  # $2 per box\nreversal = 3  # 3-box reversal\n\n# Build P&F columns\npf_data = []\ncol = 0\ndirection = None\ncurrent_price = close[0]\ncol_start = round(current_price / box_size) * box_size\n\nfor i in range(1, len(close)):\n    price = close[i]\n\n    if direction is None:\n        if price >= col_start + box_size:\n            direction = \"X\"\n            boxes_up = int((price - col_start) / box_size)\n            for b in range(boxes_up + 1):\n                pf_data.append({\"column\": col, \"price\": col_start + b * box_size, \"symbol\": \"X\", \"dir\": \"bullish\"})\n            current_price = col_start + boxes_up * box_size\n        elif price <= col_start - box_size:\n            direction = \"O\"\n            boxes_down = int((col_start - price) / box_size)\n            for b in range(boxes_down + 1):\n                pf_data.append({\"column\": col, \"price\": col_start - b * box_size, \"symbol\": \"O\", \"dir\": \"bearish\"})\n            current_price = col_start - boxes_down * box_size\n    elif direction == \"X\":\n        if price >= current_price + box_size:\n            boxes_up = int((price - current_price) / box_size)\n            for b in range(1, boxes_up + 1):\n                pf_data.append({\"column\": col, \"price\": current_price + b * box_size, \"symbol\": \"X\", \"dir\": \"bullish\"})\n            current_price += boxes_up * box_size\n        elif price <= current_price - reversal * box_size:\n            col += 1\n            boxes_down = int((current_price - price) / box_size)\n            new_start = current_price - box_size\n            for b in range(boxes_down):\n                pf_data.append({\"column\": col, \"price\": new_start - b * box_size, \"symbol\": \"O\", \"dir\": \"bearish\"})\n            current_price = new_start - (boxes_down - 1) * box_size\n            direction = \"O\"\n    else:\n        if price <= current_price - box_size:\n            boxes_down = int((current_price - price) / box_size)\n            for b in range(1, boxes_down + 1):\n                pf_data.append({\"column\": col, \"price\": current_price - b * box_size, \"symbol\": \"O\", \"dir\": \"bearish\"})\n            current_price -= boxes_down * box_size\n        elif price >= current_price + reversal * box_size:\n            col += 1\n            boxes_up = int((price - current_price) / box_size)\n            new_start = current_price + box_size\n            for b in range(boxes_up):\n                pf_data.append({\"column\": col, \"price\": new_start + b * box_size, \"symbol\": \"X\", \"dir\": \"bullish\"})\n            current_price = new_start + (boxes_up - 1) * box_size\n            direction = \"X\"\n\npf_df = pd.DataFrame(pf_data)\nmax_col = int(pf_df[\"column\"].max())\n\n# 45-degree support line: ascends from lowest O price\no_df = pf_df[pf_df[\"symbol\"] == \"O\"]\nsup_col = int(o_df.loc[o_df[\"price\"].idxmin(), \"column\"])\nsup_price = float(o_df[\"price\"].min())\nsupport_df = pd.DataFrame(\n    {\n        \"column\": list(range(sup_col, max_col + 1)),\n        \"price\": [sup_price + (c - sup_col) * box_size for c in range(sup_col, max_col + 1)],\n    }\n)\n\n# 45-degree resistance line: descends from highest X price\nx_df = pf_df[pf_df[\"symbol\"] == \"X\"]\nres_col = int(x_df.loc[x_df[\"price\"].idxmax(), \"column\"])\nres_price = float(x_df[\"price\"].max())\nresist_df = pd.DataFrame(\n    {\n        \"column\": list(range(res_col, max_col + 1)),\n        \"price\": [res_price - (c - res_col) * box_size for c in range(res_col, max_col + 1)],\n    }\n)\n\n# Plot\nTITLE = \"point-and-figure-basic · python · altair · anyplot.ai\"\nSUBTITLE = f\"Box Size: ${box_size:.0f} | Reversal: {reversal} boxes\"\n\npf_marks = (\n    alt.Chart(pf_df)\n    .mark_text(fontSize=18, fontWeight=\"bold\")\n    .encode(\n        x=alt.X(\"column:O\", title=\"Column (Reversals)\", axis=alt.Axis(labelFontSize=10, titleFontSize=12)),\n        y=alt.Y(\n            \"price:Q\",\n            title=\"Price ($)\",\n            scale=alt.Scale(zero=False),\n            axis=alt.Axis(labelFontSize=10, titleFontSize=12, format=\"$.0f\"),\n        ),\n        text=\"symbol:N\",\n        color=alt.Color(\n            \"dir:N\",\n            scale=alt.Scale(domain=[\"bullish\", \"bearish\"], range=[BULL_COLOR, BEAR_COLOR]),\n            legend=alt.Legend(title=\"Direction\"),\n        ),\n        tooltip=[\n            alt.Tooltip(\"column:O\", title=\"Column\"),\n            alt.Tooltip(\"price:Q\", title=\"Price\", format=\"$.2f\"),\n            alt.Tooltip(\"symbol:N\", title=\"Symbol\"),\n        ],\n    )\n)\n\nsupport_layer = (\n    alt.Chart(support_df)\n    .mark_line(strokeDash=[5, 3], strokeWidth=1.5, color=\"#4467A3\", opacity=0.7)\n    .encode(x=\"column:O\", y=\"price:Q\")\n)\n\nresist_layer = (\n    alt.Chart(resist_df)\n    .mark_line(strokeDash=[5, 3], strokeWidth=1.5, color=\"#BD8233\", opacity=0.7)\n    .encode(x=\"column:O\", y=\"price:Q\")\n)\n\nchart = (\n    alt.layer(pf_marks, support_layer, resist_layer)\n    .properties(\n        width=800,\n        height=450,\n        background=PAGE_BG,\n        title=alt.Title(TITLE, fontSize=16, subtitle=SUBTITLE, subtitleFontSize=11),\n    )\n    .interactive()\n    .configure_view(fill=PAGE_BG, stroke=INK_SOFT)\n    .configure_axis(\n        domainColor=INK_SOFT, tickColor=INK_SOFT, gridColor=INK, gridOpacity=0.12, labelColor=INK_SOFT, titleColor=INK\n    )\n    .configure_title(color=INK, subtitleColor=INK_SOFT)\n    .configure_legend(\n        labelFontSize=10,\n        titleFontSize=12,\n        fillColor=ELEVATED_BG,\n        strokeColor=INK_SOFT,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n    )\n)\n\n# Save\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\nchart.save(f\"plot-{THEME}.html\")\n"}