{"spec_id":"scatter-complex-plane","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nscatter-complex-plane: Complex Plane Visualization (Argand Diagram)\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 88/100 | Updated: 2026-06-02\n\"\"\"\n\nimport importlib\nimport os\nimport sys\n\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\n\n# Drop script directory from sys.path so `altair` resolves the package, not this file\nsys.path[:] = [p for p in sys.path if os.path.abspath(p or \".\") != os.path.dirname(os.path.abspath(__file__))]\nalt = importlib.import_module(\"altair\")\n\n# Theme tokens (Imprint palette — theme-adaptive chrome)\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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint categorical palette — canonical order, theme-independent\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n\n# Data\nnp.random.seed(42)\n\n# 3rd roots of unity: e^(2πik/3) for k = 0, 1, 2\nn_roots = 3\nroots_of_unity = [np.exp(2j * np.pi * k / n_roots) for k in range(n_roots)]\n\n# Arbitrary complex numbers across all four quadrants\narbitrary_points = [2.5 + 1.5j, -1.8 + 2.2j, 1.0 - 2.0j, -0.5 - 1.5j, 2.0 + 0.5j]\n\n# Complex rotation: multiply z by e^(iπ/4) — rotation by π/4 radians\nz_original = 1.5 + 0.8j\nz_rotated = z_original * np.exp(1j * np.pi / 4)\n\n# Build all points with rectangular form labels\nall_points = []\npoint_sets = [\n    (roots_of_unity, [f\"ω{k}\" for k in range(n_roots)], \"Roots of Unity\"),\n    (arbitrary_points, [f\"z{chr(0x2081 + k)}\" for k in range(5)], \"Arbitrary\"),\n    ([z_original, z_rotated], [\"z\", \"z·e^(iπ/4)\"], \"Transformation\"),\n]\nfor pts, labels, cat in point_sets:\n    for lbl, z in zip(labels, pts, strict=True):\n        r, i = round(z.real, 2), round(z.imag, 2)\n        sign = \"+\" if i >= 0 else \"\"\n        all_points.append(\n            {\"real\": z.real, \"imaginary\": z.imag, \"label\": lbl, \"rect_form\": f\"{r}{sign}{i}i\", \"category\": cat}\n        )\n\ndf = pd.DataFrame(all_points)\ndf[\"annotation\"] = df[\"label\"] + \" = \" + df[\"rect_form\"]\n\n# Label offsets: push labels away from origin by quadrant\n# with per-point tuning to separate Q1 cluster (ω0, z, z₅)\noffsets = {\"dx\": [], \"dy\": [], \"align\": []}\nfor _, row in df.iterrows():\n    rx, iy = row[\"real\"], row[\"imaginary\"]\n    dx = 0.18 if rx >= 0 else -0.18\n    dy = 0.25 if iy >= 0 else -0.25\n    align = \"left\" if rx >= 0 else \"right\"\n    # ω0 = (1, 0) lies ON the x-axis — push label well above it to avoid the axis line\n    if row[\"label\"] == \"ω0\":\n        dx = 0.05\n        dy = 0.48\n        align = \"center\"\n    # z₁ (2.5+1.5i) is near the top-right legend — place below-left, right-aligned, to avoid both\n    if row[\"label\"] == \"z₁\":\n        dx = -0.20\n        dy = -0.30\n        align = \"right\"\n    # z₅ (2.0+0.5i) sits close to z (1.5+0.8i) in Q1 — push below x-axis entirely\n    if row[\"label\"] == \"z₅\":\n        dx = 0.15\n        dy = -0.60\n        align = \"left\"\n    # z·e^(iπ/4) — push further up-right to clear the point marker\n    if row[\"label\"] == \"z·e^(iπ/4)\":\n        dy = 0.35\n        dx = 0.24\n    offsets[\"dx\"].append(rx + dx)\n    offsets[\"dy\"].append(iy + dy)\n    offsets[\"align\"].append(align)\n\ndf[\"label_x\"] = offsets[\"dx\"]\ndf[\"label_y\"] = offsets[\"dy\"]\ndf[\"label_align\"] = offsets[\"align\"]\n\n# Unit circle parametric data (reference geometry)\ntheta = np.linspace(0, 2 * np.pi, 200)\ncircle_df = pd.DataFrame({\"x\": np.cos(theta), \"y\": np.sin(theta), \"order\": range(len(theta))})\n\n# Vector segments: origin (0,0) → each complex number\narrow_rows = []\nfor _, row in df.iterrows():\n    arrow_rows.append({\"x\": 0, \"y\": 0, \"group\": row[\"label\"], \"order\": 0, \"category\": row[\"category\"]})\n    arrow_rows.append(\n        {\"x\": row[\"real\"], \"y\": row[\"imaginary\"], \"group\": row[\"label\"], \"order\": 1, \"category\": row[\"category\"]}\n    )\narrow_df = pd.DataFrame(arrow_rows)\n\n# Arrowhead positions: pulled slightly back along the vector toward origin\nhead_offset = 0.08\narrowhead_rows = []\nfor _, row in df.iterrows():\n    rx, iy = row[\"real\"], row[\"imaginary\"]\n    mag = np.sqrt(rx**2 + iy**2)\n    scale = head_offset / mag if mag > 0 else 0\n    hx, hy = rx - scale * rx, iy - scale * iy\n    vega_angle = 90 - np.degrees(np.arctan2(iy, rx))\n    arrowhead_rows.append({\"x\": hx, \"y\": hy, \"angle\": vega_angle, \"category\": row[\"category\"]})\narrowhead_df = pd.DataFrame(arrowhead_rows)\n\n# Rotation arc: curved path from z to z·e^(iπ/4) at 55% of vector length\narc_start = np.arctan2(z_original.imag, z_original.real)\narc_end = arc_start + np.pi / 4\narc_theta = np.linspace(arc_start, arc_end, 40)\narc_r = abs(z_original) * 0.55\narc_df = pd.DataFrame({\"x\": arc_r * np.cos(arc_theta), \"y\": arc_r * np.sin(arc_theta), \"order\": range(40)})\n\n# Axis range — expanded slightly so z₁ annotation label (label_x≈2.68) stays within domain\naxis_limit = 2.75\n\n# Axis lines through origin (real = horizontal, imaginary = vertical)\naxis_line_data = pd.DataFrame(\n    {\n        \"x\": [-axis_limit, axis_limit, 0, 0],\n        \"y\": [0, 0, -axis_limit, axis_limit],\n        \"axis\": [\"real\", \"real\", \"imag\", \"imag\"],\n        \"order\": [0, 1, 0, 1],\n    }\n)\n\n# Color scale: Imprint positions 1→3 (green, lavender, blue)\ncat_domain = [\"Roots of Unity\", \"Arbitrary\", \"Transformation\"]\ncat_colors = [IMPRINT_PALETTE[0], IMPRINT_PALETTE[1], IMPRINT_PALETTE[2]]\ncolor_scale = alt.Scale(domain=cat_domain, range=cat_colors)\n\n# Interactive legend selection — click to highlight by category\nhighlight = alt.selection_point(fields=[\"category\"], bind=\"legend\")\nopacity_cond = alt.condition(highlight, alt.value(1.0), alt.value(0.25))\n\n# ── Layers ───────────────────────────────────────────────────────────────────\n\n# Axis lines through origin (structural reference)\naxes = (\n    alt.Chart(axis_line_data)\n    .mark_line(color=INK_MUTED, strokeWidth=1.5, opacity=0.6)\n    .encode(x=alt.X(\"x:Q\", axis=None), y=alt.Y(\"y:Q\", axis=None), detail=\"axis:N\", order=\"order:O\")\n)\n\n# Dashed unit circle reference\nunit_circle = (\n    alt.Chart(circle_df)\n    .mark_line(color=INK_SOFT, strokeWidth=2.0, strokeDash=[8, 6], opacity=0.5)\n    .encode(x=\"x:Q\", y=\"y:Q\", order=\"order:O\")\n)\n\n# Dashed arc showing the π/4 rotation angle\nrotation_arc = (\n    alt.Chart(arc_df)\n    .mark_line(color=IMPRINT_PALETTE[2], strokeWidth=2.5, strokeDash=[5, 3], opacity=0.75)\n    .encode(x=\"x:Q\", y=\"y:Q\", order=\"order:O\")\n)\n\n# \"π/4\" label at arc midpoint\narc_mid_angle = arc_start + np.pi / 8\narc_label_df = pd.DataFrame(\n    {\"x\": [arc_r * np.cos(arc_mid_angle) - 0.15], \"y\": [arc_r * np.sin(arc_mid_angle) + 0.16], \"text\": [\"π/4\"]}\n)\narc_label = (\n    alt.Chart(arc_label_df)\n    .mark_text(fontSize=12, fontStyle=\"italic\", fontWeight=\"bold\", color=INK_SOFT, opacity=0.9)\n    .encode(x=\"x:Q\", y=\"y:Q\", text=\"text:N\")\n)\n\n# Vector lines from origin to each complex number\nvectors = (\n    alt.Chart(arrow_df)\n    .mark_line(strokeWidth=2)\n    .encode(\n        x=\"x:Q\",\n        y=\"y:Q\",\n        detail=\"group:N\",\n        order=\"order:O\",\n        color=alt.Color(\"category:N\", scale=color_scale, legend=None),\n        opacity=opacity_cond,\n    )\n    .add_params(highlight)\n)\n\n# Triangular arrowheads — sized larger for full-resolution visibility\narrowheads = (\n    alt.Chart(arrowhead_df)\n    .mark_point(shape=\"triangle-up\", filled=True, size=400)\n    .encode(\n        x=\"x:Q\",\n        y=\"y:Q\",\n        angle=alt.Angle(\"angle:Q\"),\n        color=alt.Color(\"category:N\", scale=color_scale, legend=None),\n        opacity=opacity_cond,\n    )\n    .add_params(highlight)\n)\n\n# Shared axis configuration (applied to both x and y via the points layer)\naxis_cfg = {\n    \"tickCount\": 11,\n    \"labelFontSize\": 10,\n    \"titleFontSize\": 12,\n    \"gridDash\": [3, 3],\n    \"gridOpacity\": 0.12,\n    \"titleColor\": INK,\n    \"labelColor\": INK_SOFT,\n    \"domainColor\": INK_SOFT,\n    \"tickColor\": INK_SOFT,\n}\n\n# Scatter points with PAGE_BG stroke for definition on both themes\npoints = (\n    alt.Chart(df)\n    .mark_point(filled=True, size=250, stroke=PAGE_BG, strokeWidth=2)\n    .encode(\n        x=alt.X(\n            \"real:Q\", title=\"Real Axis\", scale=alt.Scale(domain=[-axis_limit, axis_limit]), axis=alt.Axis(**axis_cfg)\n        ),\n        y=alt.Y(\n            \"imaginary:Q\",\n            title=\"Imaginary Axis\",\n            scale=alt.Scale(domain=[-axis_limit, axis_limit]),\n            axis=alt.Axis(**axis_cfg),\n        ),\n        color=alt.Color(\n            \"category:N\",\n            scale=color_scale,\n            legend=alt.Legend(\n                title=\"Category\",\n                titleFontSize=10,\n                labelFontSize=10,\n                symbolType=\"circle\",\n                symbolSize=200,\n                symbolStrokeWidth=0,\n                orient=\"top-right\",\n                titleColor=INK,\n                labelColor=INK_SOFT,\n                fillColor=ELEVATED_BG,\n                strokeColor=INK_SOFT,\n            ),\n        ),\n        opacity=opacity_cond,\n        tooltip=[\n            alt.Tooltip(\"label:N\", title=\"Label\"),\n            alt.Tooltip(\"rect_form:N\", title=\"Value\"),\n            alt.Tooltip(\"category:N\", title=\"Category\"),\n        ],\n    )\n    .add_params(highlight)\n)\n\n# Point labels: label + rectangular form annotation\nannotations = (\n    alt.Chart(df)\n    .mark_text(fontSize=10, fontWeight=\"bold\", color=INK)\n    .encode(x=\"label_x:Q\", y=\"label_y:Q\", text=\"annotation:N\", opacity=opacity_cond)\n    .add_params(highlight)\n)\n\n# \"Re\" and \"Im\" italic labels at axis endpoints\naxis_labels_df = pd.DataFrame({\"x\": [axis_limit - 0.15, 0.22], \"y\": [-0.22, axis_limit - 0.10], \"text\": [\"Re\", \"Im\"]})\naxis_labels = (\n    alt.Chart(axis_labels_df)\n    .mark_text(fontSize=12, fontStyle=\"italic\", fontWeight=\"bold\", color=INK_MUTED)\n    .encode(x=\"x:Q\", y=\"y:Q\", text=\"text:N\")\n)\n\n# Compose all layers\ntitle_text = \"scatter-complex-plane · python · altair · anyplot.ai\"\nchart = (\n    alt.layer(axes, unit_circle, rotation_arc, arc_label, vectors, arrowheads, points, annotations, axis_labels)\n    .properties(\n        width=460,\n        height=460,\n        background=PAGE_BG,\n        title=alt.Title(\n            title_text,\n            fontSize=16,\n            anchor=\"middle\",\n            color=INK,\n            subtitle=\"Roots of unity, arbitrary points & rotation (z → z·e^(iπ/4)) in the complex plane\",\n            subtitleFontSize=12,\n            subtitleColor=INK_SOFT,\n        ),\n    )\n    .resolve_scale(color=\"independent\")\n    .configure_view(fill=PAGE_BG, strokeWidth=0, continuousWidth=460, continuousHeight=460)\n    .configure_axis(titlePadding=14)\n    .configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK)\n    .interactive()\n)\n\n# Save PNG and HTML (theme-suffixed filenames required by pipeline)\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\nchart.save(f\"plot-{THEME}.html\")\n\n# Pad PNG to exact 2400×2400 target (square canvas — equal aspect ratio spec)\nTW, TH = 2400, 2400\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TW or _h > TH:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}×{_h}, exceeds target {TW}×{TH}. \"\n        f\"Shrink chart .properties(width=, height=) values and re-render.\"\n    )\nif _w < TW or _h < TH:\n    _bg_rgb = tuple(int(PAGE_BG.lstrip(\"#\")[i : i + 2], 16) for i in (0, 2, 4))\n    _canvas = Image.new(\"RGB\", (TW, TH), _bg_rgb)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n"}