{"spec_id":"point-and-figure-basic","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\npoint-and-figure-basic: Point and Figure Chart\nLibrary: letsplot 4.9.0 | Python 3.13.13\nQuality: 80/100 | Updated: 2026-05-20\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import (\n    LetsPlot,\n    aes,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_abline,\n    geom_text,\n    ggplot,\n    ggsize,\n    labs,\n    layer_tooltips,\n    scale_color_manual,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n    theme_minimal,\n)\nfrom lets_plot.export import ggsave\n\n\nLetsPlot.setup_html()\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\n\n# Data\nnp.random.seed(42)\nn_days = 300\n\nreturns = np.random.normal(0.001, 0.02, n_days)\nreturns[50:100] += 0.005  # Uptrend\nreturns[150:200] -= 0.004  # Downtrend\nreturns[250:280] += 0.006  # Uptrend\n\nprices = 100 * np.cumprod(1 + returns)\n\n# Point and Figure algorithm (box_size=$2, 3-box reversal)\nbox_size = 2.0\nreversal = 3\n\npnf_rows = []\ncurrent_box = int(prices[0] / box_size) * box_size\ndirection = None\ncolumn_idx = 0\n\nfor price in prices:\n    price_box = int(price / box_size) * box_size\n\n    if direction is None:\n        if price_box > current_box:\n            direction = \"up\"\n            for b in range(int(current_box / box_size), int(price_box / box_size) + 1):\n                pnf_rows.append((column_idx, b * box_size, \"X\", \"up\"))\n            current_box = price_box\n        elif price_box < current_box:\n            direction = \"down\"\n            for b in range(int(price_box / box_size), int(current_box / box_size) + 1):\n                pnf_rows.append((column_idx, b * box_size, \"O\", \"down\"))\n            current_box = price_box\n\n    elif direction == \"up\":\n        if price_box > current_box:\n            for b in range(int(current_box / box_size) + 1, int(price_box / box_size) + 1):\n                pnf_rows.append((column_idx, b * box_size, \"X\", \"up\"))\n            current_box = price_box\n        elif price_box <= current_box - reversal * box_size:\n            column_idx += 1\n            for b in range(int(price_box / box_size), int(current_box / box_size)):\n                pnf_rows.append((column_idx, b * box_size, \"O\", \"down\"))\n            current_box = price_box\n            direction = \"down\"\n\n    elif direction == \"down\":\n        if price_box < current_box:\n            for b in range(int(price_box / box_size), int(current_box / box_size)):\n                pnf_rows.append((column_idx, b * box_size, \"O\", \"down\"))\n            current_box = price_box\n        elif price_box >= current_box + reversal * box_size:\n            column_idx += 1\n            for b in range(int(current_box / box_size) + 1, int(price_box / box_size) + 1):\n                pnf_rows.append((column_idx, b * box_size, \"X\", \"up\"))\n            current_box = price_box\n            direction = \"up\"\n\ndf_pnf = pd.DataFrame(pnf_rows, columns=[\"column\", \"price\", \"symbol\", \"direction\"])\ndf_pnf = df_pnf.drop_duplicates(subset=[\"column\", \"price\"])\n\n# 45-degree trend lines: 1 box per column in chart coordinates\nmin_idx = df_pnf[\"price\"].idxmin()\nmin_price_val = df_pnf.loc[min_idx, \"price\"]\nmin_col_val = df_pnf.loc[min_idx, \"column\"]\n\nmax_idx = df_pnf[\"price\"].idxmax()\nmax_price_val = df_pnf.loc[max_idx, \"price\"]\nmax_col_val = df_pnf.loc[max_idx, \"column\"]\n\n# Ascending support: anchored at chart minimum, slope = +box_size\nsupport_intercept = min_price_val - box_size * min_col_val\n# Descending resistance: anchored at chart maximum, slope = -box_size\nresistance_intercept = max_price_val + box_size * max_col_val\n\n# Plot\nplot = (\n    ggplot(df_pnf, aes(x=\"column\", y=\"price\", label=\"symbol\", color=\"direction\"))\n    + geom_abline(slope=box_size, intercept=support_intercept, color=INK_SOFT, linetype=\"dashed\", size=0.8)\n    + geom_abline(slope=-box_size, intercept=resistance_intercept, color=INK_SOFT, linetype=\"dashed\", size=0.8)\n    + geom_text(\n        size=12,\n        fontface=\"bold\",\n        tooltips=layer_tooltips().line(\"Column: @column\").line(\"Price: $@price\").line(\"Signal: @symbol\"),\n    )\n    + scale_color_manual(values={\"up\": \"#009E73\", \"down\": \"#AE3030\"})  # imprint red for down\n    + scale_x_continuous(name=\"Column (Reversal Number)\")\n    + scale_y_continuous(name=\"Price ($)\")\n    + labs(title=\"point-and-figure-basic · python · letsplot · anyplot.ai\")\n    + theme_minimal()\n    + theme(\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n        plot_title=element_text(size=16, color=INK),\n        axis_title=element_text(size=12, color=INK),\n        axis_text=element_text(size=10, color=INK_SOFT),\n        axis_line=element_line(color=INK_SOFT),\n        legend_position=\"none\",\n        panel_grid_major_y=element_line(color=INK_SOFT, size=0.2),\n        panel_grid_minor_y=element_blank(),\n        panel_grid_major_x=element_blank(),\n        panel_grid_minor_x=element_blank(),\n    )\n    + ggsize(800, 450)\n)\n\n# Save\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}