{"spec_id":"biplot-pca","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\nbiplot-pca: PCA Biplot with Scores and Loading Vectors\nLibrary: plotly 6.7.0 | Python 3.13.13\nQuality: 86/100 | Updated: 2026-05-17\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport plotly.graph_objects as go\nfrom sklearn.datasets import load_iris\nfrom sklearn.decomposition import PCA\nfrom sklearn.preprocessing import StandardScaler\n\n\n# Theme tokens\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\"\nGRID = \"rgba(26,26,23,0.10)\" if THEME == \"light\" else \"rgba(240,239,232,0.10)\"\n\n# Okabe-Ito palette (first series is always #009E73)\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\"]\n\n# Load data\niris = load_iris()\nX = iris.data\ny = iris.target\nfeature_names = iris.feature_names\ntarget_names = iris.target_names\n\n# Standardize and perform PCA\nscaler = StandardScaler()\nX_scaled = scaler.fit_transform(X)\npca = PCA(n_components=2)\nscores = pca.fit_transform(X_scaled)\nloadings = pca.components_.T * np.sqrt(pca.explained_variance_)\n\n# Variance explained\nvar_explained = pca.explained_variance_ratio_ * 100\n\n# Create figure\nfig = go.Figure()\n\n# Plot scores for each group\nfor i, target in enumerate(target_names):\n    mask = y == i\n    fig.add_trace(\n        go.Scatter(\n            x=scores[mask, 0],\n            y=scores[mask, 1],\n            mode=\"markers\",\n            marker={\"size\": 14, \"color\": IMPRINT[i], \"opacity\": 0.8, \"line\": {\"width\": 1, \"color\": PAGE_BG}},\n            name=target.capitalize(),\n            legendgroup=target,\n        )\n    )\n\n# Scale loadings for visibility (relative to score spread)\nscore_scale = max(np.abs(scores).max(axis=0))\nloading_scale = max(np.abs(loadings).max(axis=0))\nscale_factor = score_scale / loading_scale * 0.9\n\n# Plot loading arrows\narrow_color = INK_SOFT\nfor loading, name in zip(loadings, feature_names, strict=False):\n    x_end = loading[0] * scale_factor\n    y_end = loading[1] * scale_factor\n\n    # Arrow line\n    fig.add_trace(\n        go.Scatter(\n            x=[0, x_end],\n            y=[0, y_end],\n            mode=\"lines\",\n            line={\"color\": arrow_color, \"width\": 3},\n            showlegend=False,\n            hoverinfo=\"skip\",\n        )\n    )\n\n    # Arrowhead using annotation\n    fig.add_annotation(\n        x=x_end,\n        y=y_end,\n        ax=0,\n        ay=0,\n        xref=\"x\",\n        yref=\"y\",\n        axref=\"x\",\n        ayref=\"y\",\n        showarrow=True,\n        arrowhead=2,\n        arrowsize=1.5,\n        arrowwidth=3,\n        arrowcolor=arrow_color,\n    )\n\n    # Variable label with offset to avoid overlap\n    label_offsets = {\n        \"sepal length\": (0.15, 0.3),\n        \"sepal width\": (0.1, 0.25),\n        \"petal length\": (0.15, -0.35),\n        \"petal width\": (0.15, 0.15),\n    }\n    clean_name = name.replace(\" (cm)\", \"\")\n    dx, dy = label_offsets.get(clean_name, (0.1, 0.1))\n    offset_x = x_end + dx\n    offset_y = y_end + dy\n    xanchor = \"left\" if x_end > 0 else \"right\"\n    fig.add_annotation(\n        x=offset_x,\n        y=offset_y,\n        text=clean_name,\n        showarrow=False,\n        font={\"size\": 16, \"color\": arrow_color},\n        xanchor=xanchor,\n        yanchor=\"middle\",\n    )\n\n# Layout\nfig.update_layout(\n    title={\n        \"text\": \"biplot-pca · plotly · anyplot.ai\",\n        \"font\": {\"size\": 28, \"color\": INK},\n        \"x\": 0.5,\n        \"xanchor\": \"center\",\n    },\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAGE_BG,\n    font={\"color\": INK},\n    xaxis={\n        \"title\": {\"text\": f\"PC1 ({var_explained[0]:.1f}%)\", \"font\": {\"size\": 22, \"color\": INK}},\n        \"tickfont\": {\"size\": 18, \"color\": INK_SOFT},\n        \"zeroline\": True,\n        \"zerolinewidth\": 1,\n        \"zerolinecolor\": arrow_color,\n        \"gridcolor\": GRID,\n        \"gridwidth\": 1,\n        \"linecolor\": INK_SOFT,\n    },\n    yaxis={\n        \"title\": {\"text\": f\"PC2 ({var_explained[1]:.1f}%)\", \"font\": {\"size\": 22, \"color\": INK}},\n        \"tickfont\": {\"size\": 18, \"color\": INK_SOFT},\n        \"zeroline\": True,\n        \"zerolinewidth\": 1,\n        \"zerolinecolor\": arrow_color,\n        \"gridcolor\": GRID,\n        \"gridwidth\": 1,\n        \"linecolor\": INK_SOFT,\n        \"scaleanchor\": \"x\",\n        \"scaleratio\": 1,\n    },\n    legend={\n        \"font\": {\"size\": 18, \"color\": INK_SOFT},\n        \"x\": 0.02,\n        \"y\": 0.98,\n        \"xanchor\": \"left\",\n        \"yanchor\": \"top\",\n        \"bgcolor\": ELEVATED_BG,\n        \"bordercolor\": INK_SOFT,\n        \"borderwidth\": 1,\n    },\n    margin={\"l\": 80, \"r\": 80, \"t\": 100, \"b\": 80},\n)\n\n# Save outputs\nfig.write_image(f\"plot-{THEME}.png\", width=1600, height=900, scale=3)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}