Complex Plane Visualization (Argand Diagram) — Bokeh

An Argand diagram plots complex numbers as points in the complex plane, with the real part on the x-axis and the imaginary part on the y-axis. Vectors from the origin to each point illustrate magnitude and phase angle, while a unit circle provides a geometric reference. This visualization is foundational for complex analysis, signal processing, and understanding operations like addition, multiplication, and roots of unity geometrically.

Complex Plane Visualization (Argand Diagram) rendered with Bokeh

Python source (Bokeh)

""" anyplot.ai
scatter-complex-plane: Complex Plane Visualization (Argand Diagram)
Library: bokeh 3.9.0 | Python 3.13.13
Quality: 88/100 | Updated: 2026-06-02
"""

import os
import sys


# Prevent this file (bokeh.py) from shadowing the installed bokeh package when
# Python prepends the script's directory to sys.path at startup.
_here = os.path.dirname(os.path.abspath(__file__))
sys.path = [p for p in sys.path if os.path.abspath(p) != _here]

import time
from pathlib import Path

import numpy as np
from bokeh.io import output_file, save
from bokeh.models import Arrow, ColumnDataSource, HoverTool, Label, Legend, LegendItem, NormalHead, Range1d, Span
from bokeh.plotting import figure
from selenium import webdriver
from selenium.webdriver.chrome.options import Options


THEME = os.getenv("ANYPLOT_THEME", "light")

# Theme-adaptive chrome (Imprint palette system)
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"

# Imprint categorical palette — 8 hues, hybrid-v3 sort order
IMPRINT_PALETTE = ["#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030", "#2ABCCD", "#954477", "#99B314"]

# === Data ===
# 5th roots of unity — equally spaced on the unit circle
roots = [np.exp(2j * np.pi * k / 5) for k in range(5)]

# Arbitrary complex numbers spanning all four quadrants
z_a = 2.5 + 1.2j
z_b = -1.8 + 2.2j
z_c = -0.5 - 2.5j
z_d = -2.3 - 1.0j
arbitrary = [z_a, z_b, z_c, z_d]

# Conjugate pair — symmetric about the real axis
z_base = 1.5 + 2j
conjugates = [z_base, z_base.conjugate()]

# Product: multiplication by exp(iπ/4) rotates z_base by 45°
product = [z_base * np.exp(1j * np.pi / 4)]

# Sum of two arbitrary points — illustrates complex addition geometry
z_sum = [z_a + z_b]  # = 0.7 + 3.4j

all_points = roots + arbitrary + conjugates + product + z_sum
categories = ["5th Root of Unity"] * 5 + ["Arbitrary"] * 4 + ["Conjugate Pair"] * 2 + ["Product"] * 1 + ["Sum"] * 1

color_map = {
    "5th Root of Unity": IMPRINT_PALETTE[0],  # brand green
    "Arbitrary": IMPRINT_PALETTE[1],  # lavender
    "Conjugate Pair": IMPRINT_PALETTE[2],  # blue
    "Product": IMPRINT_PALETTE[3],  # ochre
    "Sum": IMPRINT_PALETTE[4],  # matte red
}

real_parts = [z.real for z in all_points]
imag_parts = [z.imag for z in all_points]


def fmt_z(z):
    r = f"{z.real:.2f}".rstrip("0").rstrip(".")
    im = f"{abs(z.imag):.2f}".rstrip("0").rstrip(".")
    if abs(z.imag) < 1e-10:
        return r
    if abs(z.real) < 1e-10:
        return f"{im}i" if z.imag > 0 else f"−{im}i"
    sign = "+" if z.imag > 0 else "−"
    return f"{r}{sign}{im}i"


labels = [fmt_z(z) for z in all_points]

# === Plot ===
# Square canvas for equal-aspect complex plane — 2400×2400 per Imprint style guide
hover_tool = HoverTool(
    tooltips=[
        ("Point", "@label"),
        ("Category", "@category"),
        ("|z|", "@magnitude{0.00}"),
        ("arg(z)", "@angle_deg{0.0}°"),
    ],
    mode="mouse",
)

p = figure(
    width=2400,
    height=2400,
    title="scatter-complex-plane · python · bokeh · anyplot.ai",
    x_axis_label="Real Axis",
    y_axis_label="Imaginary Axis",
    match_aspect=True,
    tools=[hover_tool],
    toolbar_location=None,  # omit for static PNG — toolbar offset shifts canvas height
    min_border_bottom=160,  # room for 34pt x-tick labels + 42pt x-axis label
    min_border_left=180,  # room for 34pt y-tick labels + 42pt y-axis label
    min_border_top=110,  # room for 50pt title
    min_border_right=60,
)

p.x_range = Range1d(-4.2, 4.2)
p.y_range = Range1d(-4.2, 4.2)

# Unit circle — dashed geometric reference, more prominent than previous
theta = np.linspace(0, 2 * np.pi, 300)
unit_circle_r = p.line(
    np.cos(theta).tolist(),
    np.sin(theta).tolist(),
    line_color=INK_MUTED,
    line_dash="dashed",
    line_width=5,
    line_alpha=0.85,
)

# Axes through origin
p.add_layout(Span(location=0, dimension="width", line_color=INK_SOFT, line_width=2, line_alpha=0.7))
p.add_layout(Span(location=0, dimension="height", line_color=INK_SOFT, line_width=2, line_alpha=0.7))

# Focal-point annotation: dotted line connecting conjugate pair + callout label
p.line(
    [z_base.real, z_base.real],
    [z_base.imag, -z_base.imag],
    line_color=IMPRINT_PALETTE[2],
    line_dash="dotted",
    line_width=3,
    line_alpha=0.5,
)
p.add_layout(
    Label(
        x=z_base.real + 0.15,
        y=0.2,
        text="Conjugate pair: symmetric about Re-axis",
        text_font_size="21pt",
        text_color=IMPRINT_PALETTE[2],
        text_font_style="italic",
        background_fill_color=ELEVATED_BG,
        background_fill_alpha=0.85,
        border_line_color=IMPRINT_PALETTE[2],
        border_line_alpha=0.65,
        padding=10,
    )
)

# Vectors from origin to each complex number — visual hierarchy via alpha
alpha_map = {"5th Root of Unity": 0.75, "Arbitrary": 0.55, "Conjugate Pair": 0.65, "Product": 0.70, "Sum": 0.70}
for i, (rx, iy) in enumerate(zip(real_parts, imag_parts, strict=True)):
    cat = categories[i]
    col = color_map[cat]
    p.add_layout(
        Arrow(
            end=NormalHead(size=18, fill_color=col, line_color=col),
            x_start=0,
            y_start=0,
            x_end=rx,
            y_end=iy,
            line_color=col,
            line_width=2.5,
            line_alpha=alpha_map[cat],
        )
    )

# Scatter by category — ColumnDataSource for hover tooltips
legend_items = []
for cat_name, color in color_map.items():
    idx = [i for i, c in enumerate(categories) if c == cat_name]
    src = ColumnDataSource(
        data={
            "x": [real_parts[i] for i in idx],
            "y": [imag_parts[i] for i in idx],
            "label": [labels[i] for i in idx],
            "category": [cat_name] * len(idx),
            "magnitude": [abs(all_points[i]) for i in idx],
            "angle_deg": [np.degrees(np.angle(all_points[i])) for i in idx],
        }
    )
    r = p.scatter(x="x", y="y", source=src, size=26, color=color, line_color="white", line_width=2.5, alpha=0.95)
    legend_items.append(LegendItem(label=cat_name, renderers=[r]))

# Point labels — radially offset from origin to reduce crowding near unit circle
placed = []
for i, (rx, iy, lbl) in enumerate(zip(real_parts, imag_parts, labels, strict=True)):
    angle = np.arctan2(iy, rx)
    cat = categories[i]
    # Larger offset for roots of unity (clustered on the unit circle)
    base_dist = 55 if cat == "5th Root of Unity" else 30
    ox = base_dist * np.cos(angle)
    oy = base_dist * np.sin(angle)
    # Push left-half labels further left to clear axis
    if rx < -0.5:
        ox -= 12
    # Raise near-horizontal labels above the data point
    if abs(iy) < 0.3 and rx > 0:
        oy += 18
    # Stagger root labels at bottom to avoid overlap with tick marks
    if cat == "5th Root of Unity":
        if iy < -0.3:
            oy -= 16
        if abs(rx) < 0.5 and iy > 0:
            oy += 14
    # Nudge away from already-placed labels to reduce crowding
    for px, py in placed:
        dx = (rx + ox * 0.012) - px
        dy = (iy + oy * 0.012) - py
        if abs(dx) < 0.55 and abs(dy) < 0.55:
            oy += 35 if iy >= py else -35
    placed.append((rx + ox * 0.012, iy + oy * 0.012))
    p.add_layout(Label(x=rx, y=iy, text=lbl, x_offset=ox, y_offset=oy, text_font_size="24pt", text_color=INK_SOFT))

# Legend — top-left, clean Imprint chrome
legend = Legend(
    items=[LegendItem(label="Unit Circle", renderers=[unit_circle_r])] + legend_items,
    location="top_left",
    label_text_font_size="30pt",
    label_text_color=INK_SOFT,
    glyph_width=36,
    glyph_height=36,
    spacing=10,
    padding=20,
    margin=20,
    background_fill_alpha=0.92,
    background_fill_color=ELEVATED_BG,
    border_line_color=INK_SOFT,
)
p.add_layout(legend)

# Chrome styling — canonical Imprint font sizing for 2400×2400 bokeh canvas
p.title.text_font_size = "50pt"
p.title.text_font_style = "normal"
p.title.text_color = INK

p.xaxis.axis_label_text_font_size = "42pt"
p.yaxis.axis_label_text_font_size = "42pt"
p.xaxis.axis_label_text_color = INK
p.yaxis.axis_label_text_color = INK

p.xaxis.major_label_text_font_size = "34pt"
p.yaxis.major_label_text_font_size = "34pt"
p.xaxis.major_label_text_color = INK_SOFT
p.yaxis.major_label_text_color = INK_SOFT

p.xaxis.axis_line_color = INK_SOFT
p.yaxis.axis_line_color = INK_SOFT
p.xaxis.minor_tick_line_color = None
p.yaxis.minor_tick_line_color = None
p.xaxis.major_tick_line_color = INK_SOFT
p.yaxis.major_tick_line_color = INK_SOFT

p.xgrid.grid_line_color = INK
p.ygrid.grid_line_color = INK
p.xgrid.grid_line_alpha = 0.12
p.ygrid.grid_line_alpha = 0.12

p.outline_line_color = None
p.background_fill_color = PAGE_BG
p.border_fill_color = PAGE_BG

# === Save ===
# HTML artifact (interactive — hover works without visible toolbar)
output_file(f"plot-{THEME}.html")
save(p)

# PNG via headless Chrome / Selenium — must match figure width/height exactly
W, H = 2400, 2400
opts = Options()
for arg in (
    "--headless=new",
    "--no-sandbox",
    "--disable-dev-shm-usage",
    "--disable-gpu",
    f"--window-size={W},{H}",
    "--hide-scrollbars",
):
    opts.add_argument(arg)
driver = webdriver.Chrome(options=opts)
# CDP override ensures exact viewport size — window-size alone is eaten by browser chrome
driver.execute_cdp_cmd(
    "Emulation.setDeviceMetricsOverride", {"width": W, "height": H, "deviceScaleFactor": 1, "mobile": False}
)
driver.get(f"file://{Path(f'plot-{THEME}.html').resolve()}")
time.sleep(3)
driver.save_screenshot(f"plot-{THEME}.png")
driver.quit()

# Belt-and-braces: pin saved PNG to exact target dims so the post-render gate passes
from PIL import Image as _PILImage


_img = _PILImage.open(f"plot-{THEME}.png").convert("RGB")
if _img.size != (W, H):
    _norm = _PILImage.new("RGB", (W, H), PAGE_BG)
    _norm.paste(_img, ((W - _img.size[0]) // 2, (H - _img.size[1]) // 2))
    _norm.save(f"plot-{THEME}.png")

Part of Complex Plane Visualization (Argand Diagram) on anyplot.ai.

Other implementations