{"spec_id":"scatter-marginal","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nscatter-marginal: Scatter Plot with Marginal Distributions\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 94/100 | Updated: 2026-05-09\n\"\"\"\n\nimport os\nimport sys\nfrom pathlib import Path\n\n\ncurrent_dir = Path(__file__).parent\nsys.path = [p for p in sys.path if p != str(current_dir)]\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\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\"\n\n# Okabe-Ito palette\nBRAND = \"#009E73\"\n\n# Data - bivariate distribution with correlation\nnp.random.seed(42)\nn = 150\nx = np.random.randn(n) * 15 + 50\ny = 0.7 * x + np.random.randn(n) * 10 + 20\n\ndf = pd.DataFrame({\"X Value\": x, \"Y Value\": y})\n\n# Shared axis domains for proper alignment\nx_domain = [df[\"X Value\"].min() - 2, df[\"X Value\"].max() + 2]\ny_domain = [df[\"Y Value\"].min() - 2, df[\"Y Value\"].max() + 2]\n\n# Base chart\nbase = alt.Chart(df)\n\n# Main scatter plot with grid\nscatter = (\n    base.mark_circle(size=120, opacity=0.65, color=BRAND)\n    .encode(\n        x=alt.X(\"X Value:Q\", title=\"X Value (units)\", scale=alt.Scale(domain=x_domain)),\n        y=alt.Y(\"Y Value:Q\", title=\"Y Value (units)\", scale=alt.Scale(domain=y_domain)),\n        tooltip=[\"X Value:Q\", \"Y Value:Q\"],\n    )\n    .properties(width=1000, height=600)\n)\n\n# Top marginal histogram with matching X scale\ntop_hist = (\n    base.mark_bar(color=BRAND, opacity=0.5)\n    .encode(\n        x=alt.X(\n            \"X Value:Q\",\n            bin=alt.Bin(maxbins=25),\n            title=None,\n            scale=alt.Scale(domain=x_domain),\n            axis=alt.Axis(labels=False, ticks=False),\n        ),\n        y=alt.Y(\"count()\", title=None, axis=alt.Axis(labels=False, ticks=False)),\n    )\n    .properties(width=1000, height=120)\n)\n\n# Right marginal histogram with matching Y scale\nright_hist = (\n    base.mark_bar(color=BRAND, opacity=0.5)\n    .encode(\n        y=alt.Y(\n            \"Y Value:Q\",\n            bin=alt.Bin(maxbins=25),\n            title=None,\n            scale=alt.Scale(domain=y_domain),\n            axis=alt.Axis(labels=False, ticks=False),\n        ),\n        x=alt.X(\"count()\", title=None, axis=alt.Axis(labels=False, ticks=False)),\n    )\n    .properties(width=120, height=600)\n)\n\n# Combine: top histogram above, scatter with right histogram below\ncombined = (\n    alt.vconcat(top_hist, alt.hconcat(scatter, right_hist, spacing=5), spacing=5)\n    .properties(\n        background=PAGE_BG,\n        title=alt.Title(text=\"scatter-marginal · altair · anyplot.ai\", fontSize=28, anchor=\"middle\", color=INK),\n    )\n    .configure_axis(\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        gridColor=INK,\n        gridOpacity=0.10,\n        labelColor=INK_SOFT,\n        labelFontSize=16,\n        titleColor=INK,\n        titleFontSize=20,\n    )\n    .configure_view(fill=PAGE_BG, stroke=INK_SOFT, strokeWidth=1)\n    .configure_concat(spacing=5)\n)\n\n# Save outputs\ncombined.save(f\"plot-{THEME}.png\", scale_factor=3.0)\ncombined.save(f\"plot-{THEME}.html\")\n"}