{"spec_id":"scatter-marginal","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nscatter-marginal: Scatter Plot with Marginal Distributions\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-05-09\n\"\"\"\n\nimport io\nimport os\n\nimport numpy as np\nimport pygal\nfrom PIL import Image, ImageDraw, ImageFont\nfrom pygal.style import Style\n\n\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\n\n# Theme-adaptive colors from default-style-guide.md\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\n# Okabe-Ito palette\nBRAND = \"#009E73\"  # First categorical series\nSECONDARY = \"#C475FD\"  # For marginals if colored\n\n# Data - correlated bivariate data with realistic measurement context\nnp.random.seed(42)\nn_points = 150\nx = np.random.randn(n_points) * 15 + 50  # Measurement A in range ~10-90\ny = x * 0.6 + np.random.randn(n_points) * 12 + 20  # Measurement B correlated\n\n# Calculate correlation for annotation\ncorrelation = np.corrcoef(x, y)[0, 1]\n\n# Calculate histogram data for marginals\nn_bins = 10\nx_min, x_max = np.floor(x.min() / 5) * 5, np.ceil(x.max() / 5) * 5\ny_min, y_max = np.floor(y.min() / 5) * 5, np.ceil(y.max() / 5) * 5\n\nx_hist, x_edges = np.histogram(x, bins=n_bins, range=(x_min, x_max))\ny_hist, y_edges = np.histogram(y, bins=n_bins, range=(y_min, y_max))\n\n# Dimensions for layout\ntotal_width = 4800\ntotal_height = 2700\nmargin_plot_size = 450\ntitle_height = 100\ngap = 15\n\nscatter_width = total_width - margin_plot_size - gap * 3\nscatter_height = total_height - margin_plot_size - title_height - gap * 3\n\nleft_margin = 100\nbottom_margin = 80\ntop_margin = 20\nright_margin = 20\n\n# Custom style for main scatter - theme-adaptive\nscatter_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=(BRAND,),  # Okabe-Ito first series\n    title_font_size=48,\n    label_font_size=36,\n    major_label_font_size=32,\n    legend_font_size=32,\n    opacity=0.65,\n    opacity_hover=0.9,\n)\n\n# Custom style for marginal histograms - theme-adaptive, subtle color\nmarginal_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=(INK_SOFT,),  # Subtle gray for marginals\n    title_font_size=32,\n    label_font_size=32,\n    major_label_font_size=30,\n    legend_font_size=28,\n    opacity=0.6,\n)\n\n# Create main scatter plot\nscatter = pygal.XY(\n    width=scatter_width,\n    height=scatter_height,\n    style=scatter_style,\n    x_title=\"Measurement A (units)\",\n    y_title=\"Measurement B (units)\",\n    show_legend=False,\n    stroke=False,\n    dots_size=10,\n    show_x_guides=True,\n    show_y_guides=True,\n    x_label_rotation=0,\n    truncate_label=-1,\n    explicit_size=True,\n    margin_top=top_margin,\n    margin_right=right_margin,\n    margin_bottom=bottom_margin,\n    margin_left=left_margin,\n    range=(y_min - 5, y_max + 5),\n    xrange=(x_min - 5, x_max + 5),\n)\n\nscatter_points = [(float(xi), float(yi)) for xi, yi in zip(x, y, strict=True)]\nscatter.add(\"Data\", scatter_points)\n\n# Create top marginal histogram (X distribution)\nx_margin = pygal.Bar(\n    width=scatter_width,\n    height=margin_plot_size,\n    style=marginal_style,\n    show_legend=False,\n    show_x_labels=False,\n    show_y_labels=True,\n    show_y_guides=True,\n    show_x_guides=False,\n    margin_top=top_margin,\n    margin_right=right_margin,\n    margin_bottom=20,\n    margin_left=left_margin,\n    explicit_size=True,\n    spacing=2,\n)\nx_margin.add(\"X Distribution\", [float(h) for h in x_hist])\n\n# Create right marginal histogram (Y distribution)\ny_margin = pygal.HorizontalBar(\n    width=margin_plot_size,\n    height=scatter_height,\n    style=marginal_style,\n    show_legend=False,\n    show_x_labels=False,\n    show_y_labels=False,\n    show_y_guides=False,\n    show_x_guides=False,\n    margin_top=top_margin,\n    margin_right=30,\n    margin_bottom=bottom_margin,\n    margin_left=10,\n    explicit_size=True,\n    spacing=2,\n)\ny_margin.add(\"Y Distribution\", [float(h) for h in y_hist[::-1]])\n\n# Render each chart to PNG in memory\nscatter_png = scatter.render_to_png()\nx_margin_png = x_margin.render_to_png()\ny_margin_png = y_margin.render_to_png()\n\n# Open images\nscatter_img = Image.open(io.BytesIO(scatter_png))\nx_margin_img = Image.open(io.BytesIO(x_margin_png))\ny_margin_img = Image.open(io.BytesIO(y_margin_png))\n\n# Create final composite image with theme-adaptive background\nfinal_img = Image.new(\"RGB\", (total_width, total_height), PAGE_BG)\n\n# Calculate positions\nscatter_x = gap\nscatter_y = title_height + margin_plot_size + gap\nx_margin_x = gap\nx_margin_y = title_height\ny_margin_x = gap + scatter_width + gap\ny_margin_y = title_height + margin_plot_size + gap\n\n# Paste images\nfinal_img.paste(x_margin_img, (x_margin_x, x_margin_y))\nfinal_img.paste(y_margin_img, (y_margin_x, y_margin_y))\nfinal_img.paste(scatter_img, (scatter_x, scatter_y))\n\n# Add title and corner annotation\ndraw = ImageDraw.Draw(final_img)\ntitle_text = \"scatter-marginal · pygal · anyplot.ai\"\ntry:\n    title_font = ImageFont.truetype(\"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf\", 60)\n    stats_font = ImageFont.truetype(\"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf\", 36)\n    stats_font_bold = ImageFont.truetype(\"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf\", 42)\nexcept OSError:\n    title_font = ImageFont.load_default()\n    stats_font = ImageFont.load_default()\n    stats_font_bold = ImageFont.load_default()\n\n# Get text bounding box for centering title\nbbox = draw.textbbox((0, 0), title_text, font=title_font)\ntext_width = bbox[2] - bbox[0]\ntext_x = (total_width - text_width) // 2\ntext_y = 30\ndraw.text((text_x, text_y), title_text, fill=INK, font=title_font)\n\n# Add statistics in the corner space (top-right empty area)\ncorner_x = y_margin_x + 30\ncorner_y = title_height + 30\ncorner_width = margin_plot_size - 60\ncorner_height = margin_plot_size - 80\n\n# Draw subtle background for stats box with theme-adaptive colors\nelevated_bg = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nbox_border = INK_MUTED\n\nstats_box = [(corner_x, corner_y), (corner_x + corner_width, corner_y + corner_height)]\ndraw.rounded_rectangle(stats_box, radius=15, fill=elevated_bg, outline=box_border, width=2)\n\n# Add statistics text\nstats_title = \"Summary\"\ndraw.text((corner_x + 35, corner_y + 25), stats_title, fill=INK, font=stats_font_bold)\n\nstats_lines = [f\"n = {n_points}\", f\"r = {correlation:.3f}\", f\"A̅ = {np.mean(x):.1f}\", f\"B̅ = {np.mean(y):.1f}\"]\nline_y = corner_y + 85\nfor line in stats_lines:\n    draw.text((corner_x + 35, line_y), line, fill=INK_SOFT, font=stats_font)\n    line_y += 50\n\n# Save final image and HTML\nfinal_img.save(f\"plot-{THEME}.png\", \"PNG\")\n\n# Also save the scatter SVG as HTML for interactivity\nscatter_svg_full = scatter.render().decode(\"utf-8\")\nwith open(f\"plot-{THEME}.html\", \"w\", encoding=\"utf-8\") as f:\n    f.write(scatter_svg_full)\n"}