{"spec_id":"scatter-3d","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nscatter-3d: 3D Scatter Plot\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 93/100 | Updated: 2026-05-08\n\"\"\"\n\nimport os\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\n\n\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\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\"]\n\n# Data - create 3D clusters to demonstrate spatial relationships\nnp.random.seed(42)\n\nn_points = 150\n\n# Create three distinct clusters in 3D space\nclusters = []\ncenters = [(2.5, 2.5, 2.5), (-2.5, -1.5, 1.0), (0.5, 0.0, -2.5)]\n\nfor i, (cx, cy, cz) in enumerate(centers):\n    n_cluster = n_points // 3\n    x = np.random.randn(n_cluster) * 0.8 + cx\n    y = np.random.randn(n_cluster) * 0.8 + cy\n    z = np.random.randn(n_cluster) * 0.8 + cz\n    clusters.append(pd.DataFrame({\"x\": x, \"y\": y, \"z\": z, \"cluster\": f\"Cluster {i + 1}\"}))\n\ndf = pd.concat(clusters, ignore_index=True)\n\n# 3D to 2D isometric projection (elevation=25°, azimuth=35°)\nelev_rad = np.radians(25)\nazim_rad = np.radians(35)\n\n# Rotation around z-axis (azimuth)\ndf[\"x_rot\"] = df[\"x\"] * np.cos(azim_rad) - df[\"y\"] * np.sin(azim_rad)\ndf[\"y_rot\"] = df[\"x\"] * np.sin(azim_rad) + df[\"y\"] * np.cos(azim_rad)\n\n# Rotation around x-axis (elevation) and project to 2D\ndf[\"x_proj\"] = df[\"x_rot\"]\ndf[\"z_proj\"] = df[\"y_rot\"] * np.sin(elev_rad) + df[\"z\"] * np.cos(elev_rad)\n\n# Calculate depth for point ordering (painters algorithm)\ndf[\"depth\"] = df[\"y_rot\"] * np.cos(elev_rad) - df[\"z\"] * np.sin(elev_rad)\n\n# Normalize depth for opacity (further points slightly more transparent)\ndepth_min = df[\"depth\"].min()\ndepth_max = df[\"depth\"].max()\ndf[\"opacity\"] = 0.65 + 0.35 * (df[\"depth\"] - depth_min) / (depth_max - depth_min + 1e-6)\n\n# Scatter chart with clusters\nscatter = (\n    alt.Chart(df)\n    .mark_circle(size=280, strokeWidth=1.5, stroke=PAGE_BG)\n    .encode(\n        x=alt.X(\"x_proj:Q\", axis=alt.Axis(title=\"X Axis\", labelFontSize=18, titleFontSize=22)),\n        y=alt.Y(\"z_proj:Q\", axis=alt.Axis(title=\"Z Axis\", labelFontSize=18, titleFontSize=22)),\n        color=alt.Color(\n            \"cluster:N\",\n            scale=alt.Scale(domain=[\"Cluster 1\", \"Cluster 2\", \"Cluster 3\"], range=IMPRINT),\n            legend=alt.Legend(title=\"Cluster\", titleFontSize=20, labelFontSize=16, orient=\"top-right\", offset=10),\n        ),\n        opacity=alt.Opacity(\"opacity:Q\", legend=None),\n        order=alt.Order(\"depth:Q\", sort=\"ascending\"),\n        tooltip=[\n            alt.Tooltip(\"x:Q\", title=\"X\", format=\".2f\"),\n            alt.Tooltip(\"y:Q\", title=\"Y\", format=\".2f\"),\n            alt.Tooltip(\"z:Q\", title=\"Z\", format=\".2f\"),\n            alt.Tooltip(\"cluster:N\", title=\"Cluster\"),\n        ],\n    )\n)\n\n# Add pan and zoom interactivity\npan_zoom = scatter.interactive()\n\n# Final chart\nchart = (\n    pan_zoom.properties(\n        width=1600,\n        height=900,\n        background=PAGE_BG,\n        title=alt.Title(text=\"scatter-3d · altair · anyplot.ai\", fontSize=28),\n    )\n    .configure_view(fill=PAGE_BG, stroke=INK_SOFT)\n    .configure_axis(\n        domainColor=INK_SOFT, tickColor=INK_SOFT, gridColor=INK, gridOpacity=0.10, labelColor=INK_SOFT, titleColor=INK\n    )\n    .configure_title(color=INK)\n    .configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK)\n)\n\n# Save outputs\nchart.save(f\"plot-{THEME}.png\", scale_factor=3.0)\nchart.save(f\"plot-{THEME}.html\")\n"}