{"spec_id":"residual-plot","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nresidual-plot: Residual Plot\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 95/100 | Updated: 2026-05-10\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Okabe-Ito palette\nIMPRINT = (\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\")\n\n# Data - Linear regression example with heteroscedasticity\nnp.random.seed(42)\nn_points = 100\n\n# Generate fitted values (x-axis) - house price predictions in $1000s\nfitted_values = np.linspace(150, 500, n_points)\n\n# Generate residuals with heteroscedasticity and outliers\nbase_residuals = np.random.normal(0, 20, n_points)\nheteroscedasticity = (fitted_values / 500) * np.random.normal(0, 15, n_points)\nresiduals = base_residuals + heteroscedasticity\n\n# Add outliers\noutlier_indices = [15, 45, 78]\nresiduals[outlier_indices] = [85, -75, 90]\n\n# Calculate standard deviation for reference bands\nstd_residuals = np.std(residuals)\nupper_band = 2 * std_residuals\nlower_band = -2 * std_residuals\n\n# Identify outliers (beyond 2 standard deviations)\noutlier_mask = np.abs(residuals) > 2 * std_residuals\n\n# Custom style for 4800x2700 canvas with theme-adaptive tokens\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_MUTED,\n    colors=IMPRINT,\n    title_font_size=28,\n    label_font_size=22,\n    major_label_font_size=20,\n    legend_font_size=18,\n    value_font_size=14,\n    stroke_width=3,\n    guide_stroke_color=INK_MUTED,\n    guide_stroke_dasharray=\"3, 3\",\n)\n\n# Create XY scatter chart for residual plot\nchart = pygal.XY(\n    width=4800,\n    height=2700,\n    style=custom_style,\n    title=\"residual-plot · pygal · anyplot.ai\",\n    x_title=\"Fitted Values - Predicted Price ($1000s)\",\n    y_title=\"Residuals - Actual minus Predicted ($1000s)\",\n    show_legend=True,\n    legend_at_bottom=True,\n    legend_at_bottom_columns=5,\n    show_x_guides=True,\n    show_y_guides=True,\n    stroke=False,\n    dots_size=12,\n    truncate_legend=-1,\n    x_label_rotation=0,\n    xrange=(140, 510),\n    range=(-100, 110),\n)\n\n# Set explicit x-axis labels\nchart.x_labels = [150, 200, 250, 300, 350, 400, 450, 500]\n\n# Prepare data points - separate normal and outlier points\nnormal_points = [(float(fitted_values[i]), float(residuals[i])) for i in range(n_points) if not outlier_mask[i]]\noutlier_points = [(float(fitted_values[i]), float(residuals[i])) for i in range(n_points) if outlier_mask[i]]\n\n# Add data series\nchart.add(\"Residuals\", normal_points)\nchart.add(\"Outliers (>2σ)\", outlier_points)\n\n# Add zero reference line\nzero_line_points = [(float(x), 0.0) for x in np.linspace(150, 500, 50)]\nchart.add(\"Zero Reference (Perfect Fit)\", zero_line_points, stroke=True, show_dots=False, stroke_style={\"width\": 5})\n\n# Add +2σ reference band line\nupper_band_points = [(float(x), float(upper_band)) for x in np.linspace(150, 500, 50)]\nchart.add(\n    \"+2σ Threshold\", upper_band_points, stroke=True, show_dots=False, stroke_style={\"width\": 3, \"dasharray\": \"10, 8\"}\n)\n\n# Add -2σ reference band line\nlower_band_points = [(float(x), float(lower_band)) for x in np.linspace(150, 500, 50)]\nchart.add(\n    \"-2σ Threshold\", lower_band_points, stroke=True, show_dots=False, stroke_style={\"width\": 3, \"dasharray\": \"10, 8\"}\n)\n\n# Save as PNG and HTML\nchart.render_to_png(f\"plot-{THEME}.png\")\nwith open(f\"plot-{THEME}.html\", \"wb\") as f:\n    f.write(chart.render())\n"}