{"spec_id":"line-pca-variance-cumulative","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nline-pca-variance-cumulative: Cumulative Explained Variance for PCA Component Selection\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-05-29\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\n\n\n# Theme tokens — Imprint palette, theme-adaptive chrome\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\nIMPRINT_PALETTE = (\n    \"#009E73\",  # brand green — always first series\n    \"#C475FD\",  # lavender\n    \"#4467A3\",  # blue\n    \"#BD8233\",  # ochre\n    \"#AE3030\",\n    \"#2ABCCD\",\n    \"#954477\",\n    \"#99B314\",\n)\n\n# Data — realistic PCA variance ratios for a 13-feature Wine-like dataset\neigenvalues = np.array([4.73, 2.51, 1.45, 0.92, 0.85, 0.64, 0.55, 0.35, 0.29, 0.21, 0.17, 0.13, 0.08])\nexplained_variance_ratio = eigenvalues / eigenvalues.sum()\nindividual_variance = explained_variance_ratio * 100\ncumulative_variance = np.cumsum(explained_variance_ratio) * 100\nn_components = len(cumulative_variance)\ncomponent_labels = [str(i) for i in range(1, n_components + 1)]\n\n# Detect elbow: where slope drops below 35% of the initial slope\nslopes = np.diff(cumulative_variance)\nelbow_idx = int(np.argmax(slopes < slopes[0] * 0.35))\n\n# Threshold crossings\ncross_90 = int(np.searchsorted(cumulative_variance, 90))\ncross_95 = int(np.searchsorted(cumulative_variance, 95))\n\n# Style — Imprint palette, theme-adaptive tokens, 3200×1800 canvas sizing\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_SOFT,  # INK_SOFT (not INK_MUTED) for legible tick labels against green fill\n    colors=IMPRINT_PALETTE,\n    opacity=0.38,  # semi-transparent fill so y-axis labels remain legible beneath the green area\n    opacity_hover=1.0,\n    stroke_opacity=1.0,\n    stroke_opacity_hover=1.0,\n    stroke_width=2.5,\n    dot_opacity=1.0,\n    guide_stroke_color=INK_MUTED,\n    guide_stroke_dasharray=\"2,12\",  # longer gap — guides recede behind data\n    major_guide_stroke_color=INK_SOFT,\n    major_guide_stroke_dasharray=\"8,12\",  # longer gap for major guides at 90%/95%\n    title_font_size=66,\n    label_font_size=56,\n    major_label_font_size=44,\n    legend_font_size=44,\n    value_font_size=36,\n    tooltip_font_size=34,\n    font_family=\"Helvetica, Arial, sans-serif\",\n    title_font_family=\"Helvetica, Arial, sans-serif\",\n    label_font_family=\"Helvetica, Arial, sans-serif\",\n    legend_font_family=\"Helvetica, Arial, sans-serif\",\n    value_font_family=\"Helvetica, Arial, sans-serif\",\n    major_label_font_family=\"Helvetica, Arial, sans-serif\",\n    transition=\"200ms\",\n)\n\n# Chart\nchart = pygal.Line(\n    width=3200,\n    height=1800,\n    title=\"line-pca-variance-cumulative · python · pygal · anyplot.ai\",\n    x_title=\"Number of Components\",\n    y_title=\"Cumulative Explained Variance (%)\",\n    style=custom_style,\n    show_dots=True,\n    dots_size=10,\n    show_only_major_dots=False,\n    show_y_guides=True,\n    show_x_guides=False,\n    legend_at_bottom=True,\n    legend_at_bottom_columns=4,\n    legend_box_size=26,\n    truncate_legend=-1,\n    range=(0, 105),\n    secondary_range=(0, 55),  # individual variance secondary axis; wider margin ensures labels are visible\n    margin=30,\n    margin_bottom=90,\n    margin_left=220,  # wider left margin so y-axis labels sit clearly outside the green fill\n    margin_right=220,  # wider right margin for secondary-axis tick labels on right side\n    margin_top=40,\n    print_values=True,\n    print_values_position=\"top\",\n    value_formatter=lambda x: \"\",\n    spacing=20,\n    show_minor_x_labels=True,\n    tooltip_fancy_mode=True,\n    tooltip_border_radius=10,\n    interpolate=\"cubic\",\n    interpolation_precision=200,\n    human_readable=False,\n    no_data_text=\"No variance data\",\n)\n\n# Axes — custom y-tick positions highlight the decision thresholds\nchart.x_labels = component_labels\n# Mark the three decision-point components as major x-labels (bold, guided)\nchart.x_labels_major = [str(elbow_idx + 1), str(cross_90 + 1), str(cross_95 + 1)]\nchart.y_labels = [0, 20, 40, 60, 80, 90, 95, 100]\nchart.y_labels_major = [90, 95]  # major guide lines drawn at the two decision thresholds\n\n# Annotations at key decision points\nannotations = {\n    elbow_idx: lambda x: f\"Elbow: n={elbow_idx + 1} ({x:.0f}%)\",\n    cross_90: lambda x: f\"90%: n={cross_90 + 1}\",\n    cross_95: lambda x: f\"95%: n={cross_95 + 1}\",\n}\n\n# Cumulative variance — brand green filled area, thick primary stroke for visual hierarchy\ncumulative_values = [\n    {\"value\": round(v, 1), \"label\": f\"PC{i + 1}\", \"formatter\": annotations.get(i, lambda x: \"\")}\n    for i, v in enumerate(cumulative_variance)\n]\nchart.add(\n    \"Cumulative Variance\",\n    cumulative_values,\n    stroke_style={\"width\": 10, \"linecap\": \"round\", \"linejoin\": \"round\"},\n    fill=True,\n)\n\n# 90% threshold — lavender dashed, thinner to reinforce secondary role\nchart.add(\n    \"90% Threshold\",\n    [{\"value\": 90, \"label\": \"90% variance threshold\", \"formatter\": lambda x: \"\"} for _ in range(n_components)],\n    show_dots=False,\n    fill=False,\n    stroke_style={\"width\": 3, \"dasharray\": \"22, 12\", \"linecap\": \"round\"},\n)\n\n# 95% threshold — blue dotted, thinner for visual hierarchy\nchart.add(\n    \"95% Threshold\",\n    [{\"value\": 95, \"label\": \"95% variance threshold\", \"formatter\": lambda x: \"\"} for _ in range(n_components)],\n    show_dots=False,\n    fill=False,\n    stroke_style={\"width\": 3, \"dasharray\": \"8, 10\", \"linecap\": \"round\"},\n)\n\n# Individual variance on secondary Y-axis — ochre with larger dots for salience\n# Per-point formatters serve as secondary-axis labels for components with meaningful variance\nindividual_values = [\n    {\"value\": round(v, 1), \"label\": f\"PC{i + 1}: {v:.1f}%\", \"formatter\": lambda x: f\"{x:.1f}%\" if x > 3 else \"\"}\n    for i, v in enumerate(individual_variance)\n]\nchart.add(\n    \"Individual Variance (%)\",\n    individual_values,\n    secondary=True,\n    stroke_style={\"width\": 8, \"linecap\": \"round\", \"linejoin\": \"round\"},\n    show_dots=True,\n    dots_size=11,\n    fill=False,\n)\n\n# Save PNG and interactive HTML\nchart.render_to_png(f\"plot-{THEME}.png\")\nwith open(f\"plot-{THEME}.html\", \"wb\") as f:\n    f.write(chart.render())\n"}