{"spec_id":"scatter-complex-plane","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nscatter-complex-plane: Complex Plane Visualization (Argand Diagram)\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 88/100 | Updated: 2026-06-02\n\"\"\"\n\nimport os\nimport sys\n\n\n# Prevent this file (bokeh.py) from shadowing the installed bokeh package when\n# Python prepends the script's directory to sys.path at startup.\n_here = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if os.path.abspath(p) != _here]\n\nimport time\nfrom pathlib import Path\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import Arrow, ColumnDataSource, HoverTool, Label, Legend, LegendItem, NormalHead, Range1d, Span\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\n\n# Theme-adaptive chrome (Imprint palette system)\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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint categorical palette — 8 hues, hybrid-v3 sort order\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n\n# === Data ===\n# 5th roots of unity — equally spaced on the unit circle\nroots = [np.exp(2j * np.pi * k / 5) for k in range(5)]\n\n# Arbitrary complex numbers spanning all four quadrants\nz_a = 2.5 + 1.2j\nz_b = -1.8 + 2.2j\nz_c = -0.5 - 2.5j\nz_d = -2.3 - 1.0j\narbitrary = [z_a, z_b, z_c, z_d]\n\n# Conjugate pair — symmetric about the real axis\nz_base = 1.5 + 2j\nconjugates = [z_base, z_base.conjugate()]\n\n# Product: multiplication by exp(iπ/4) rotates z_base by 45°\nproduct = [z_base * np.exp(1j * np.pi / 4)]\n\n# Sum of two arbitrary points — illustrates complex addition geometry\nz_sum = [z_a + z_b]  # = 0.7 + 3.4j\n\nall_points = roots + arbitrary + conjugates + product + z_sum\ncategories = [\"5th Root of Unity\"] * 5 + [\"Arbitrary\"] * 4 + [\"Conjugate Pair\"] * 2 + [\"Product\"] * 1 + [\"Sum\"] * 1\n\ncolor_map = {\n    \"5th Root of Unity\": IMPRINT_PALETTE[0],  # brand green\n    \"Arbitrary\": IMPRINT_PALETTE[1],  # lavender\n    \"Conjugate Pair\": IMPRINT_PALETTE[2],  # blue\n    \"Product\": IMPRINT_PALETTE[3],  # ochre\n    \"Sum\": IMPRINT_PALETTE[4],  # matte red\n}\n\nreal_parts = [z.real for z in all_points]\nimag_parts = [z.imag for z in all_points]\n\n\ndef fmt_z(z):\n    r = f\"{z.real:.2f}\".rstrip(\"0\").rstrip(\".\")\n    im = f\"{abs(z.imag):.2f}\".rstrip(\"0\").rstrip(\".\")\n    if abs(z.imag) < 1e-10:\n        return r\n    if abs(z.real) < 1e-10:\n        return f\"{im}i\" if z.imag > 0 else f\"−{im}i\"\n    sign = \"+\" if z.imag > 0 else \"−\"\n    return f\"{r}{sign}{im}i\"\n\n\nlabels = [fmt_z(z) for z in all_points]\n\n# === Plot ===\n# Square canvas for equal-aspect complex plane — 2400×2400 per Imprint style guide\nhover_tool = HoverTool(\n    tooltips=[\n        (\"Point\", \"@label\"),\n        (\"Category\", \"@category\"),\n        (\"|z|\", \"@magnitude{0.00}\"),\n        (\"arg(z)\", \"@angle_deg{0.0}°\"),\n    ],\n    mode=\"mouse\",\n)\n\np = figure(\n    width=2400,\n    height=2400,\n    title=\"scatter-complex-plane · python · bokeh · anyplot.ai\",\n    x_axis_label=\"Real Axis\",\n    y_axis_label=\"Imaginary Axis\",\n    match_aspect=True,\n    tools=[hover_tool],\n    toolbar_location=None,  # omit for static PNG — toolbar offset shifts canvas height\n    min_border_bottom=160,  # room for 34pt x-tick labels + 42pt x-axis label\n    min_border_left=180,  # room for 34pt y-tick labels + 42pt y-axis label\n    min_border_top=110,  # room for 50pt title\n    min_border_right=60,\n)\n\np.x_range = Range1d(-4.2, 4.2)\np.y_range = Range1d(-4.2, 4.2)\n\n# Unit circle — dashed geometric reference, more prominent than previous\ntheta = np.linspace(0, 2 * np.pi, 300)\nunit_circle_r = p.line(\n    np.cos(theta).tolist(),\n    np.sin(theta).tolist(),\n    line_color=INK_MUTED,\n    line_dash=\"dashed\",\n    line_width=5,\n    line_alpha=0.85,\n)\n\n# Axes through origin\np.add_layout(Span(location=0, dimension=\"width\", line_color=INK_SOFT, line_width=2, line_alpha=0.7))\np.add_layout(Span(location=0, dimension=\"height\", line_color=INK_SOFT, line_width=2, line_alpha=0.7))\n\n# Focal-point annotation: dotted line connecting conjugate pair + callout label\np.line(\n    [z_base.real, z_base.real],\n    [z_base.imag, -z_base.imag],\n    line_color=IMPRINT_PALETTE[2],\n    line_dash=\"dotted\",\n    line_width=3,\n    line_alpha=0.5,\n)\np.add_layout(\n    Label(\n        x=z_base.real + 0.15,\n        y=0.2,\n        text=\"Conjugate pair: symmetric about Re-axis\",\n        text_font_size=\"21pt\",\n        text_color=IMPRINT_PALETTE[2],\n        text_font_style=\"italic\",\n        background_fill_color=ELEVATED_BG,\n        background_fill_alpha=0.85,\n        border_line_color=IMPRINT_PALETTE[2],\n        border_line_alpha=0.65,\n        padding=10,\n    )\n)\n\n# Vectors from origin to each complex number — visual hierarchy via alpha\nalpha_map = {\"5th Root of Unity\": 0.75, \"Arbitrary\": 0.55, \"Conjugate Pair\": 0.65, \"Product\": 0.70, \"Sum\": 0.70}\nfor i, (rx, iy) in enumerate(zip(real_parts, imag_parts, strict=True)):\n    cat = categories[i]\n    col = color_map[cat]\n    p.add_layout(\n        Arrow(\n            end=NormalHead(size=18, fill_color=col, line_color=col),\n            x_start=0,\n            y_start=0,\n            x_end=rx,\n            y_end=iy,\n            line_color=col,\n            line_width=2.5,\n            line_alpha=alpha_map[cat],\n        )\n    )\n\n# Scatter by category — ColumnDataSource for hover tooltips\nlegend_items = []\nfor cat_name, color in color_map.items():\n    idx = [i for i, c in enumerate(categories) if c == cat_name]\n    src = ColumnDataSource(\n        data={\n            \"x\": [real_parts[i] for i in idx],\n            \"y\": [imag_parts[i] for i in idx],\n            \"label\": [labels[i] for i in idx],\n            \"category\": [cat_name] * len(idx),\n            \"magnitude\": [abs(all_points[i]) for i in idx],\n            \"angle_deg\": [np.degrees(np.angle(all_points[i])) for i in idx],\n        }\n    )\n    r = p.scatter(x=\"x\", y=\"y\", source=src, size=26, color=color, line_color=\"white\", line_width=2.5, alpha=0.95)\n    legend_items.append(LegendItem(label=cat_name, renderers=[r]))\n\n# Point labels — radially offset from origin to reduce crowding near unit circle\nplaced = []\nfor i, (rx, iy, lbl) in enumerate(zip(real_parts, imag_parts, labels, strict=True)):\n    angle = np.arctan2(iy, rx)\n    cat = categories[i]\n    # Larger offset for roots of unity (clustered on the unit circle)\n    base_dist = 55 if cat == \"5th Root of Unity\" else 30\n    ox = base_dist * np.cos(angle)\n    oy = base_dist * np.sin(angle)\n    # Push left-half labels further left to clear axis\n    if rx < -0.5:\n        ox -= 12\n    # Raise near-horizontal labels above the data point\n    if abs(iy) < 0.3 and rx > 0:\n        oy += 18\n    # Stagger root labels at bottom to avoid overlap with tick marks\n    if cat == \"5th Root of Unity\":\n        if iy < -0.3:\n            oy -= 16\n        if abs(rx) < 0.5 and iy > 0:\n            oy += 14\n    # Nudge away from already-placed labels to reduce crowding\n    for px, py in placed:\n        dx = (rx + ox * 0.012) - px\n        dy = (iy + oy * 0.012) - py\n        if abs(dx) < 0.55 and abs(dy) < 0.55:\n            oy += 35 if iy >= py else -35\n    placed.append((rx + ox * 0.012, iy + oy * 0.012))\n    p.add_layout(Label(x=rx, y=iy, text=lbl, x_offset=ox, y_offset=oy, text_font_size=\"24pt\", text_color=INK_SOFT))\n\n# Legend — top-left, clean Imprint chrome\nlegend = Legend(\n    items=[LegendItem(label=\"Unit Circle\", renderers=[unit_circle_r])] + legend_items,\n    location=\"top_left\",\n    label_text_font_size=\"30pt\",\n    label_text_color=INK_SOFT,\n    glyph_width=36,\n    glyph_height=36,\n    spacing=10,\n    padding=20,\n    margin=20,\n    background_fill_alpha=0.92,\n    background_fill_color=ELEVATED_BG,\n    border_line_color=INK_SOFT,\n)\np.add_layout(legend)\n\n# Chrome styling — canonical Imprint font sizing for 2400×2400 bokeh canvas\np.title.text_font_size = \"50pt\"\np.title.text_font_style = \"normal\"\np.title.text_color = INK\n\np.xaxis.axis_label_text_font_size = \"42pt\"\np.yaxis.axis_label_text_font_size = \"42pt\"\np.xaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_color = INK\n\np.xaxis.major_label_text_font_size = \"34pt\"\np.yaxis.major_label_text_font_size = \"34pt\"\np.xaxis.major_label_text_color = INK_SOFT\np.yaxis.major_label_text_color = INK_SOFT\n\np.xaxis.axis_line_color = INK_SOFT\np.yaxis.axis_line_color = INK_SOFT\np.xaxis.minor_tick_line_color = None\np.yaxis.minor_tick_line_color = None\np.xaxis.major_tick_line_color = INK_SOFT\np.yaxis.major_tick_line_color = INK_SOFT\n\np.xgrid.grid_line_color = INK\np.ygrid.grid_line_color = INK\np.xgrid.grid_line_alpha = 0.12\np.ygrid.grid_line_alpha = 0.12\n\np.outline_line_color = None\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\n\n# === Save ===\n# HTML artifact (interactive — hover works without visible toolbar)\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# PNG via headless Chrome / Selenium — must match figure width/height exactly\nW, H = 2400, 2400\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)\n# CDP override ensures exact viewport size — window-size alone is eaten by browser chrome\ndriver.execute_cdp_cmd(\n    \"Emulation.setDeviceMetricsOverride\", {\"width\": W, \"height\": H, \"deviceScaleFactor\": 1, \"mobile\": False}\n)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n\n# Belt-and-braces: pin saved PNG to exact target dims so the post-render gate passes\nfrom PIL import Image as _PILImage\n\n\n_img = _PILImage.open(f\"plot-{THEME}.png\").convert(\"RGB\")\nif _img.size != (W, H):\n    _norm = _PILImage.new(\"RGB\", (W, H), PAGE_BG)\n    _norm.paste(_img, ((W - _img.size[0]) // 2, (H - _img.size[1]) // 2))\n    _norm.save(f\"plot-{THEME}.png\")\n"}