{"spec_id":"point-and-figure-basic","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\npoint-and-figure-basic: Point and Figure Chart\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 93/100 | Updated: 2026-05-20\n\"\"\"\n\nimport os\nimport time\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nfrom bokeh.io import output_file, save\nfrom bokeh.models import BoxAnnotation, ColumnDataSource, FixedTicker, HoverTool, Range1d\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\n# Theme\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# Okabe-Ito palette positions used\nX_COLOR = \"#009E73\"  # position 1 — brand green, bullish X columns\nO_COLOR = \"#AE3030\"  # imprint red — bearish O columns\nSUPPORT_COLOR = \"#4467A3\"  # position 3 — blue, ascending support line\nRESISTANCE_COLOR = \"#BD8233\"  # position 4 — purple, descending resistance line\n\n# Data\nnp.random.seed(42)\nn_days = 300\nstart_price = 100\ndaily_returns = np.random.normal(0.0005, 0.015, n_days)\n\n# Add trending periods for visible breakout patterns\ndaily_returns[50:80] += 0.003\ndaily_returns[100:140] -= 0.004\ndaily_returns[180:220] += 0.0035\ndaily_returns[240:280] -= 0.003\n\nclose_prices = start_price * np.cumprod(1 + daily_returns)\nvolatility = np.abs(np.random.normal(0, 0.01, n_days))\nhigh_prices = close_prices * (1 + volatility)\nlow_prices = close_prices * (1 - volatility)\ndates = pd.date_range(\"2024-01-01\", periods=n_days, freq=\"D\")\ndf = pd.DataFrame({\"date\": dates, \"high\": high_prices, \"low\": low_prices, \"close\": close_prices})\n\n# Point and Figure algorithm\nbox_size = 2.0  # $2 per box\nreversal = 3  # 3-box reversal to start a new column\n\ncolumns = []\ncurrent_direction = None\ncurrent_column_start = None\ncurrent_column_end = None\n\nfirst_price = df[\"close\"].iloc[0]\nbox_start = np.floor(first_price / box_size) * box_size\n\nfor row in df.itertuples():\n    price = row.close\n    box_price = np.floor(price / box_size) * box_size\n\n    if current_direction is None:\n        current_column_start = box_start\n        current_column_end = box_start\n        if price >= box_start + box_size:\n            current_direction = \"X\"\n            current_column_end = box_price\n        elif price <= box_start - box_size:\n            current_direction = \"O\"\n            current_column_end = box_price\n    elif current_direction == \"X\":\n        if box_price >= current_column_end + box_size:\n            current_column_end = box_price\n        elif box_price <= current_column_end - reversal * box_size:\n            columns.append({\"type\": \"X\", \"start\": current_column_start, \"end\": current_column_end})\n            current_direction = \"O\"\n            current_column_start = current_column_end - box_size\n            current_column_end = box_price\n    else:\n        if box_price <= current_column_end - box_size:\n            current_column_end = box_price\n        elif box_price >= current_column_end + reversal * box_size:\n            columns.append({\"type\": \"O\", \"start\": current_column_start, \"end\": current_column_end})\n            current_direction = \"X\"\n            current_column_start = current_column_end + box_size\n            current_column_end = box_price\n\nif current_direction is not None:\n    columns.append({\"type\": current_direction, \"start\": current_column_start, \"end\": current_column_end})\n\n# Prepare plotting data\nx_cols, x_prices, x_labels = [], [], []\no_cols, o_prices, o_labels = [], [], []\n\nfor col_idx, col in enumerate(columns):\n    if col[\"type\"] == \"X\":\n        lo = min(col[\"start\"], col[\"end\"])\n        hi = max(col[\"start\"], col[\"end\"])\n        for box in np.arange(lo, hi + box_size / 2, box_size):\n            x_cols.append(col_idx)\n            x_prices.append(float(box))\n            x_labels.append(\"X\")\n    else:\n        lo = min(col[\"start\"], col[\"end\"])\n        hi = max(col[\"start\"], col[\"end\"])\n        for box in np.arange(lo, hi + box_size / 2, box_size):\n            o_cols.append(col_idx)\n            o_prices.append(float(box))\n            o_labels.append(\"O\")\n\nall_prices = x_prices + o_prices\nmin_price = min(all_prices)\nmax_price = max(all_prices)\n\n# Grid ticks at exact box-size intervals\nprice_ticks = list(\n    np.arange(np.floor(min_price / box_size) * box_size, np.ceil(max_price / box_size) * box_size + box_size, box_size)\n)\n\n# Plot\nhover = HoverTool(tooltips=[(\"Price\", \"@price{$0.00}\"), (\"Column\", \"@col\")])\n\np = figure(\n    width=3200,\n    height=1800,\n    title=\"point-and-figure-basic · python · bokeh · anyplot.ai\",\n    x_axis_label=\"Column (Reversal)\",\n    y_axis_label=\"Price ($)\",\n    toolbar_location=None,\n    tools=[hover],\n    x_range=Range1d(-0.5, len(columns) - 0.5),\n    min_border_bottom=160,\n    min_border_left=180,\n    min_border_top=110,\n    min_border_right=50,\n)\n\n# Font sizes for 3200×1800 canvas\np.title.text_font_size = \"50pt\"\np.xaxis.axis_label_text_font_size = \"42pt\"\np.yaxis.axis_label_text_font_size = \"42pt\"\np.xaxis.major_label_text_font_size = \"34pt\"\np.yaxis.major_label_text_font_size = \"34pt\"\n\n# Theme-adaptive chrome\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = None\np.title.text_color = INK\np.xaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_color = INK\np.xaxis.major_label_text_color = INK_SOFT\np.yaxis.major_label_text_color = INK_SOFT\np.xaxis.axis_line_color = INK_SOFT\np.yaxis.axis_line_color = INK_SOFT\np.xaxis.major_tick_line_color = INK_SOFT\np.yaxis.major_tick_line_color = INK_SOFT\n\n# Grid at exact box-size intervals\np.ygrid.ticker = FixedTicker(ticks=price_ticks)\np.xgrid.grid_line_color = INK\np.ygrid.grid_line_color = INK\np.xgrid.grid_line_alpha = 0.10\np.ygrid.grid_line_alpha = 0.10\n\n# Alternating column background shading via BoxAnnotation (Bokeh-native feature)\nfor col_idx, col in enumerate(columns):\n    fill = X_COLOR if col[\"type\"] == \"X\" else O_COLOR\n    p.add_layout(\n        BoxAnnotation(left=col_idx - 0.5, right=col_idx + 0.5, fill_color=fill, fill_alpha=0.05, line_color=None)\n    )\n\n# X markers — bullish rising price columns\nx_source = ColumnDataSource(data={\"col\": x_cols, \"price\": x_prices, \"label\": x_labels})\np.text(\n    x=\"col\",\n    y=\"price\",\n    text=\"label\",\n    source=x_source,\n    text_font_size=\"30pt\",\n    text_color=X_COLOR,\n    text_align=\"center\",\n    text_baseline=\"middle\",\n    text_font_style=\"bold\",\n    legend_label=\"X — Bullish\",\n)\n\n# O markers — bearish falling price columns\no_source = ColumnDataSource(data={\"col\": o_cols, \"price\": o_prices, \"label\": o_labels})\np.text(\n    x=\"col\",\n    y=\"price\",\n    text=\"label\",\n    source=o_source,\n    text_font_size=\"30pt\",\n    text_color=O_COLOR,\n    text_align=\"center\",\n    text_baseline=\"middle\",\n    text_font_style=\"bold\",\n    legend_label=\"O — Bearish\",\n)\n\n# Support trend line (45-degree ascending from lowest point)\nsupport_price_start = min_price - box_size\nsupport_price_end = support_price_start + (len(columns) - 1) * box_size\nif support_price_end <= max_price + 2 * box_size:\n    p.line(\n        x=[0, len(columns) - 1],\n        y=[support_price_start, support_price_end],\n        line_width=4,\n        line_color=SUPPORT_COLOR,\n        legend_label=\"Support\",\n    )\n\n# Resistance trend line (45-degree descending from highest point)\nresistance_price_start = max_price + box_size\nresistance_price_end = resistance_price_start - (len(columns) - 1) * box_size\nif resistance_price_end >= min_price - 2 * box_size:\n    p.line(\n        x=[0, len(columns) - 1],\n        y=[resistance_price_start, resistance_price_end],\n        line_width=4,\n        line_color=RESISTANCE_COLOR,\n        legend_label=\"Resistance\",\n    )\n\n# Legend\np.legend.location = \"top_left\"\np.legend.label_text_font_size = \"34pt\"\np.legend.background_fill_color = ELEVATED_BG\np.legend.border_line_color = INK_SOFT\np.legend.label_text_color = INK_SOFT\n\n# Save HTML\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Screenshot with headless Chrome (export_png unavailable in this env)\nW, H = 3200, 1800\nopts = Options()\nfor arg in (\n    \"--headless=new\",\n    \"--no-sandbox\",\n    \"--disable-dev-shm-usage\",\n    \"--disable-gpu\",\n    f\"--window-size={W},{H}\",\n    \"--hide-scrollbars\",\n):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\ndriver.set_window_size(W, H)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}