{"spec_id":"polar-scatter","library":"altair","language":"python","code":"\"\"\" anyplot.ai\npolar-scatter: Polar Scatter Plot\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 92/100 | Updated: 2026-05-09\n\"\"\"\n\nimport os\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 (positions 1, 2, 3 for three categories)\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\"]\n\n# Data - Synthetic wind measurements with prevailing directions\nnp.random.seed(42)\nn_points = 120\n\n# Create realistic wind data with prevailing directions (NW and SE)\nangles_nw = np.random.normal(315, 25, n_points // 3)  # NW winds\nangles_se = np.random.normal(135, 30, n_points // 3)  # SE winds\nangles_other = np.random.uniform(0, 360, n_points - 2 * (n_points // 3))  # Other directions\nangles_deg = np.concatenate([angles_nw, angles_se, angles_other]) % 360\n\n# Wind speeds (m/s) - higher speeds for prevailing directions\nspeeds = np.zeros(n_points)\nspeeds[: n_points // 3] = np.random.gamma(4, 2.5, n_points // 3)  # NW - stronger\nspeeds[n_points // 3 : 2 * n_points // 3] = np.random.gamma(3, 2, n_points // 3)  # SE - moderate\nspeeds[2 * n_points // 3 :] = np.random.gamma(2, 1.5, n_points - 2 * (n_points // 3))  # Other - weaker\nspeeds = np.clip(speeds, 1, 20)\n\n# Time of day categories\ntime_of_day = np.array(\n    [\"Morning\"] * (n_points // 3) + [\"Afternoon\"] * (n_points // 3) + [\"Evening\"] * (n_points - 2 * (n_points // 3))\n)\n\n# Convert polar to Cartesian for Altair (doesn't have native polar support)\nangles_rad = np.deg2rad(angles_deg)\nx = speeds * np.cos(angles_rad)\ny = speeds * np.sin(angles_rad)\n\ndf = pd.DataFrame({\"x\": x, \"y\": y, \"speed\": speeds, \"direction\": angles_deg, \"time_of_day\": time_of_day})\n\n# Create polar gridlines (circles)\nmax_radius = 20\nradii = [5, 10, 15, 20]\ncircle_points = []\nfor r in radii:\n    theta = np.linspace(0, 2 * np.pi, 100)\n    for i, t in enumerate(theta):\n        circle_points.append({\"x\": r * np.cos(t), \"y\": r * np.sin(t), \"radius\": r, \"order\": i})\ncircles_df = pd.DataFrame(circle_points)\n\n# Create angular gridlines (spokes)\nspoke_angles = [0, 45, 90, 135, 180, 225, 270, 315]\nspoke_points = []\nfor angle in spoke_angles:\n    rad = np.deg2rad(angle)\n    spoke_points.append({\"x\": 0, \"y\": 0, \"angle\": angle, \"group\": angle, \"order\": 0})\n    spoke_points.append(\n        {\"x\": max_radius * np.cos(rad), \"y\": max_radius * np.sin(rad), \"angle\": angle, \"group\": angle, \"order\": 1}\n    )\nspokes_df = pd.DataFrame(spoke_points)\n\n# Create angle labels\nlabel_offset = max_radius * 1.12\nangle_labels = []\ndirection_names = {\n    0: \"E (0°)\",\n    45: \"NE (45°)\",\n    90: \"N (90°)\",\n    135: \"NW (135°)\",\n    180: \"W (180°)\",\n    225: \"SW (225°)\",\n    270: \"S (270°)\",\n    315: \"SE (315°)\",\n}\nfor angle, name in direction_names.items():\n    rad = np.deg2rad(angle)\n    angle_labels.append({\"x\": label_offset * np.cos(rad), \"y\": label_offset * np.sin(rad), \"label\": name})\nlabels_df = pd.DataFrame(angle_labels)\n\n# Create radius labels\nradius_labels = [{\"x\": r + 0.5, \"y\": 0.5, \"label\": f\"{r} m/s\"} for r in radii]\nradius_labels_df = pd.DataFrame(radius_labels)\n\n# Circular gridlines\ngrid_circles = (\n    alt.Chart(circles_df)\n    .mark_line(strokeWidth=1, opacity=0.10)\n    .encode(\n        x=alt.X(\"x:Q\", axis=None),\n        y=alt.Y(\"y:Q\", axis=None),\n        detail=\"radius:N\",\n        order=\"order:Q\",\n        color=alt.value(INK_SOFT),\n    )\n)\n\n# Radial spokes\ngrid_spokes = (\n    alt.Chart(spokes_df)\n    .mark_line(strokeWidth=1, opacity=0.10)\n    .encode(\n        x=alt.X(\"x:Q\", axis=None),\n        y=alt.Y(\"y:Q\", axis=None),\n        detail=\"group:N\",\n        order=\"order:Q\",\n        color=alt.value(INK_SOFT),\n    )\n)\n\n# Angle labels\nangle_text = (\n    alt.Chart(labels_df)\n    .mark_text(fontSize=18, fontWeight=\"bold\")\n    .encode(x=alt.X(\"x:Q\", axis=None), y=alt.Y(\"y:Q\", axis=None), text=\"label:N\", color=alt.value(INK_SOFT))\n)\n\n# Radius labels\nradius_text = (\n    alt.Chart(radius_labels_df)\n    .mark_text(fontSize=14, align=\"left\")\n    .encode(x=alt.X(\"x:Q\", axis=None), y=alt.Y(\"y:Q\", axis=None), text=\"label:N\", color=alt.value(INK_SOFT))\n)\n\n# Data points with Okabe-Ito colors\npoints = (\n    alt.Chart(df)\n    .mark_point(size=220, filled=True, opacity=0.8)\n    .encode(\n        x=alt.X(\"x:Q\", axis=None, scale=alt.Scale(domain=[-25, 25])),\n        y=alt.Y(\"y:Q\", axis=None, scale=alt.Scale(domain=[-25, 25])),\n        color=alt.Color(\n            \"time_of_day:N\",\n            scale=alt.Scale(domain=[\"Morning\", \"Afternoon\", \"Evening\"], range=IMPRINT),\n            title=\"Time of Day\",\n        ),\n        tooltip=[\n            alt.Tooltip(\"direction:Q\", title=\"Direction (°)\", format=\".1f\"),\n            alt.Tooltip(\"speed:Q\", title=\"Wind Speed (m/s)\", format=\".1f\"),\n            alt.Tooltip(\"time_of_day:N\", title=\"Time of Day\"),\n        ],\n    )\n)\n\n# Combine all layers with theme-adaptive styling\nchart = (\n    alt.layer(grid_circles, grid_spokes, angle_text, radius_text, points)\n    .properties(\n        width=1600,\n        height=1600,\n        background=PAGE_BG,\n        title=alt.Title(\"polar-scatter · altair · anyplot.ai\", fontSize=28, anchor=\"middle\", color=INK),\n    )\n    .configure_view(strokeWidth=0, fill=PAGE_BG)\n    .configure_legend(\n        titleFontSize=22,\n        labelFontSize=18,\n        symbolSize=220,\n        orient=\"right\",\n        fillColor=ELEVATED_BG,\n        strokeColor=INK_SOFT,\n        titleColor=INK,\n        labelColor=INK_SOFT,\n    )\n    .interactive()\n)\n\n# Save as PNG and HTML with theme-suffixed filenames\nchart.save(f\"plot-{THEME}.png\", scale_factor=3.0)\nchart.save(f\"plot-{THEME}.html\")\n"}