{"spec_id":"line-pca-variance-cumulative","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nline-pca-variance-cumulative: Cumulative Explained Variance for PCA Component Selection\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 94/100 | Updated: 2026-05-29\n\"\"\"\n\nimport importlib\nimport os\nimport sys\n\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\nfrom sklearn.datasets import load_wine\nfrom sklearn.decomposition import PCA\nfrom sklearn.preprocessing import StandardScaler\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\"\n\n# Imprint palette positions\nBRAND = \"#009E73\"  # position 1 — always first categorical series\nTHRESH_90 = \"#4467A3\"  # position 3 — blue\nTHRESH_95 = \"#BD8233\"  # position 4 — ochre\nELBOW_COLOR = \"#AE3030\"  # position 5 — matte red (semantic: key decision point)\n\n# Data — PCA on the Wine dataset (13 features)\nwine = load_wine()\nX_scaled = StandardScaler().fit_transform(wine.data)\npca = PCA().fit(X_scaled)\n\ncumulative_variance = np.cumsum(pca.explained_variance_ratio_) * 100\nn_components = np.arange(1, len(cumulative_variance) + 1)\n\ndf = pd.DataFrame({\"Component\": n_components, \"Cumulative Variance\": cumulative_variance})\n\n# Elbow point via kneedle method (max distance from diagonal)\nx_norm = (n_components - n_components[0]) / (n_components[-1] - n_components[0])\ny_norm = (cumulative_variance - cumulative_variance[0]) / (cumulative_variance[-1] - cumulative_variance[0])\nelbow_idx = int(np.argmax(np.abs(y_norm - x_norm)))\nelbow_component = n_components[elbow_idx]\nelbow_value = cumulative_variance[elbow_idx]\n\n# Threshold crossing points\nthresholds = pd.DataFrame({\"Threshold\": [90, 95], \"Label\": [\"90 %\", \"95 %\"]})\ncrossing_points = []\nfor thresh in [90, 95]:\n    idx = int(np.searchsorted(cumulative_variance, thresh))\n    if idx < len(cumulative_variance):\n        crossing_points.append(\n            {\n                \"Component\": idx + 1,\n                \"Cumulative Variance\": cumulative_variance[idx],\n                \"Label\": f\"{thresh} %\",\n                \"Annotation\": f\"{idx + 1} components\",\n            }\n        )\ncrossing_df = pd.DataFrame(crossing_points)\n\nelbow_df = pd.DataFrame(\n    [{\"Component\": elbow_component, \"Cumulative Variance\": elbow_value, \"Marker\": f\"Elbow (PC {elbow_component})\"}]\n)\n\n# Shared scales\ny_scale = alt.Scale(domain=[20, 105])\nx_scale = alt.Scale(domain=[0.5, len(cumulative_variance) + 0.5], nice=False)\nthreshold_scale = alt.Scale(domain=[\"90 %\", \"95 %\"], range=[THRESH_90, THRESH_95])\n\n# Area fill under curve — reduced opacity to preserve grid contrast\narea = (\n    alt.Chart(df)\n    .mark_area(opacity=0.05, color=BRAND)\n    .encode(\n        x=alt.X(\"Component:Q\", scale=x_scale),\n        y=alt.Y(\"Cumulative Variance:Q\", scale=y_scale),\n        y2=alt.value({\"expr\": \"height\"}),\n    )\n)\n\n# Cumulative variance line\nline = (\n    alt.Chart(df)\n    .mark_line(strokeWidth=3, color=BRAND, interpolate=\"monotone\")\n    .encode(\n        x=alt.X(\n            \"Component:Q\", title=\"Number of Components\", scale=x_scale, axis=alt.Axis(tickMinStep=1, titlePadding=10)\n        ),\n        y=alt.Y(\n            \"Cumulative Variance:Q\",\n            title=\"Cumulative Explained Variance (%)\",\n            scale=y_scale,\n            axis=alt.Axis(titlePadding=10, format=\".0f\"),\n        ),\n    )\n)\n\n# Data point markers\npoints = (\n    alt.Chart(df)\n    .mark_point(size=120, color=BRAND, filled=True, stroke=PAGE_BG, strokeWidth=1.5)\n    .encode(\n        x=alt.X(\"Component:Q\", scale=x_scale),\n        y=alt.Y(\"Cumulative Variance:Q\", scale=y_scale),\n        tooltip=[\n            alt.Tooltip(\"Component:Q\", title=\"Component\"),\n            alt.Tooltip(\"Cumulative Variance:Q\", format=\".1f\", title=\"Cumulative Variance (%)\"),\n        ],\n    )\n)\n\n# Interactive nearest-point selection (Altair's distinctive hover capability)\nnearest = alt.selection_point(nearest=True, on=\"pointerover\", fields=[\"Component\"], empty=False)\n\ninvisible_selector = (\n    alt.Chart(df)\n    .mark_point(size=300, opacity=0)\n    .encode(x=alt.X(\"Component:Q\", scale=x_scale), y=alt.Y(\"Cumulative Variance:Q\", scale=y_scale))\n    .add_params(nearest)\n)\n\nhighlight_point = (\n    alt.Chart(df)\n    .mark_point(size=180, color=BRAND, filled=True, stroke=INK, strokeWidth=2)\n    .encode(\n        x=alt.X(\"Component:Q\", scale=x_scale),\n        y=alt.Y(\"Cumulative Variance:Q\", scale=y_scale),\n        opacity=alt.condition(nearest, alt.value(1), alt.value(0)),\n    )\n)\n\nhover_rule = (\n    alt.Chart(df)\n    .mark_rule(color=INK_SOFT, strokeDash=[3, 3], strokeWidth=1, opacity=0.5)\n    .encode(x=alt.X(\"Component:Q\", scale=x_scale))\n    .transform_filter(nearest)\n)\n\n# Threshold reference lines\nthreshold_lines = (\n    alt.Chart(thresholds)\n    .mark_rule(strokeDash=[8, 5], strokeWidth=1.5, opacity=0.65)\n    .encode(\n        y=alt.Y(\"Threshold:Q\", scale=y_scale),\n        color=alt.Color(\n            \"Label:N\",\n            scale=threshold_scale,\n            legend=alt.Legend(\n                title=\"Threshold\",\n                titleFontSize=10,\n                titleFontWeight=\"bold\",\n                labelFontSize=10,\n                orient=\"right\",\n                symbolStrokeWidth=2,\n                symbolSize=100,\n                symbolDash=[8, 5],\n                offset=8,\n            ),\n        ),\n    )\n)\n\n# Threshold crossing markers\ncrossing_markers = (\n    alt.Chart(crossing_df)\n    .mark_point(shape=\"diamond\", size=180, filled=True, stroke=PAGE_BG, strokeWidth=1.5)\n    .encode(\n        x=alt.X(\"Component:Q\", scale=x_scale),\n        y=alt.Y(\"Cumulative Variance:Q\", scale=y_scale),\n        color=alt.Color(\"Label:N\", scale=threshold_scale, legend=None),\n        tooltip=[\n            alt.Tooltip(\"Component:Q\", title=\"Components needed\"),\n            alt.Tooltip(\"Cumulative Variance:Q\", format=\".1f\", title=\"Variance (%)\"),\n            alt.Tooltip(\"Label:N\", title=\"Threshold\"),\n        ],\n    )\n)\n\n# Threshold crossing annotations\ncrossing_labels = (\n    alt.Chart(crossing_df)\n    .mark_text(fontSize=11, fontWeight=\"bold\", dy=-13, align=\"center\")\n    .encode(\n        x=alt.X(\"Component:Q\", scale=x_scale),\n        y=alt.Y(\"Cumulative Variance:Q\", scale=y_scale),\n        text=alt.Text(\"Annotation:N\"),\n        color=alt.Color(\"Label:N\", scale=threshold_scale, legend=None),\n    )\n)\n\n# Elbow point marker\nelbow_marker = (\n    alt.Chart(elbow_df)\n    .mark_point(shape=\"triangle-up\", size=220, color=ELBOW_COLOR, filled=True, stroke=PAGE_BG, strokeWidth=1.5)\n    .encode(\n        x=alt.X(\"Component:Q\", scale=x_scale),\n        y=alt.Y(\"Cumulative Variance:Q\", scale=y_scale),\n        tooltip=[\n            alt.Tooltip(\"Component:Q\", title=\"Elbow at component\"),\n            alt.Tooltip(\"Cumulative Variance:Q\", format=\".1f\", title=\"Variance (%)\"),\n        ],\n    )\n)\n\n# Elbow label — dx=42 offsets right to clear the triangle marker (fixes overlap weakness)\nelbow_label = (\n    alt.Chart(elbow_df)\n    .mark_text(fontSize=12, fontWeight=\"bold\", color=ELBOW_COLOR, dy=-16, dx=42)\n    .encode(\n        x=alt.X(\"Component:Q\", scale=x_scale),\n        y=alt.Y(\"Cumulative Variance:Q\", scale=y_scale),\n        text=alt.Text(\"Marker:N\"),\n    )\n)\n\n# Title — 59 chars, under 67 baseline so no fontsize scaling needed\ntitle_str = \"line-pca-variance-cumulative · python · altair · anyplot.ai\"\n\n# Combine all layers\nchart = (\n    (\n        area\n        + threshold_lines\n        + hover_rule\n        + line\n        + points\n        + crossing_markers\n        + crossing_labels\n        + elbow_marker\n        + elbow_label\n        + invisible_selector\n        + highlight_point\n    )\n    .properties(\n        background=PAGE_BG,\n        width=620,\n        height=320,\n        padding={\"left\": 0, \"right\": 0, \"top\": 0, \"bottom\": 0},\n        title=alt.Title(text=title_str, fontSize=16, anchor=\"middle\", offset=12, color=INK),\n    )\n    .configure_view(fill=PAGE_BG, strokeWidth=0, continuousWidth=620, continuousHeight=320)\n    .configure_axis(\n        grid=True,\n        gridOpacity=0.12,\n        gridDash=[3, 3],\n        gridColor=INK,\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        labelFontSize=10,\n        titleFontSize=12,\n    )\n    .configure_title(color=INK)\n    .configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK)\n)\n\n# Save PNG\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\n# Pad PNG to exact 3200×1800 target (vl-convert lands slightly under with inner 620×320)\nTW, TH = 3200, 1800\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    _canvas = Image.new(\"RGB\", (TW, TH), PAGE_BG)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n\nchart.save(f\"plot-{THEME}.html\")\n"}