{"spec_id":"polar-basic","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\npolar-basic: Basic Polar Chart\nLibrary: plotnine 0.15.7 | Python 3.13.14\nQuality: 83/100 | Updated: 2026-07-25\n\"\"\"\n\nimport math\nimport os\nimport sys\n\n\n# Prevent this file (plotnine.py) from shadowing the installed plotnine package\n_here = os.path.normpath(os.path.abspath(os.path.dirname(__file__)))\nsys.path = [p for p in sys.path if os.path.normpath(os.path.abspath(p or \".\")) != _here]\n\nimport numpy as np\nimport pandas as pd\nfrom plotnine import (\n    aes,\n    coord_fixed,\n    element_blank,\n    element_rect,\n    element_text,\n    geom_path,\n    geom_point,\n    geom_segment,\n    geom_text,\n    ggplot,\n    labs,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n)\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_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\nBRAND = \"#009E73\"  # Imprint palette position 1 — ALWAYS first series\n\n# Data - Hourly activity levels throughout the day (cyclical pattern)\nnp.random.seed(42)\nhours = np.arange(0, 24)\nbase_activity = 20 + 40 * np.sin((hours - 6) * np.pi / 12) ** 2\nactivity = base_activity + np.random.uniform(-8, 8, 24)\nactivity = np.clip(activity, 5, 100)\n\n# Convert hours to angles (0 hours = bottom, increasing counter-clockwise)\ntheta = hours * 2 * math.pi / 24 - math.pi / 2\n\n# Convert polar to Cartesian coordinates\nx = activity * np.cos(theta)\ny = activity * np.sin(theta)\n\ndf = pd.DataFrame({\"hour\": hours, \"activity\": activity, \"theta\": theta, \"x\": x, \"y\": y})\n\n# Close the loop by adding first point at end\ndf_closed = pd.concat([df, df.iloc[[0]]], ignore_index=True)\n\n# Peak point, highlighted below with a larger marker (visual emphasis, no text callout)\npeak_df = df.iloc[[int(np.argmax(activity))]]\n\n# Radial scale sized to the actual data range (+15% headroom) so the shape\n# fills most of the polar area instead of a fixed 0-100 scale compressing it.\ngrid_max = math.ceil(activity.max() * 1.15 / 10) * 10\ngrid_radii = [grid_max / 4, grid_max / 2, 3 * grid_max / 4, grid_max]\nspoke_radius = grid_max * 1.05\nhour_label_radius = grid_max * 1.22\naxis_limit = grid_max * 1.45\n\n# Circular gridlines\ngrid_rows = []\ngrid_angles = np.linspace(0, 2 * math.pi, 101)\nfor radius in grid_radii:\n    for angle in grid_angles:\n        grid_rows.append({\"x\": radius * np.cos(angle), \"y\": radius * np.sin(angle), \"radius\": radius})\n\ngrid_df = pd.DataFrame(grid_rows)\n\n# Radial spokes (every 3 hours = 8 spokes)\nspoke_rows = []\nspoke_hours = [0, 3, 6, 9, 12, 15, 18, 21]\nfor h in spoke_hours:\n    angle = h * 2 * math.pi / 24 - math.pi / 2\n    spoke_rows.append({\"x1\": 0, \"y1\": 0, \"x2\": spoke_radius * np.cos(angle), \"y2\": spoke_radius * np.sin(angle)})\n\nspoke_df = pd.DataFrame(spoke_rows)\n\n# Hour labels positioned outside the chart\nlabel_rows = []\nfor h in spoke_hours:\n    angle = h * 2 * math.pi / 24 - math.pi / 2\n    label_rows.append(\n        {\"label\": f\"{h:02d}:00\", \"x\": hour_label_radius * np.cos(angle), \"y\": hour_label_radius * np.sin(angle)}\n    )\n\nlabel_df = pd.DataFrame(label_rows)\n\n# Radius labels (activity level scale), placed off to the upper-left between\n# the 12:00 and 15:00 spokes — away from the 06:00/18:00 valleys on the\n# horizontal axis where low-activity points would otherwise crowd the labels.\n# Angle is kept roughly midway between the 12:00 (90°) and 15:00 (135°) spokes\n# so the outermost \"Activity Level\" title clears the \"12:00\" hour label instead\n# of sitting at nearly the same radius only 10° away from it.\nradius_label_angle = math.radians(114)\nradius_labels = [\n    {\"label\": str(int(r)), \"x\": r * math.cos(radius_label_angle) - 6, \"y\": r * math.sin(radius_label_angle)}\n    for r in grid_radii\n]\nradius_label_df = pd.DataFrame(radius_labels)\n\n# Radial axis title, placed just beyond the outermost ring on the same spoke\nradial_axis_label_df = pd.DataFrame(\n    [\n        {\n            \"label\": \"Activity Level\",\n            \"x\": (grid_max + 14) * math.cos(radius_label_angle) - 6,\n            \"y\": (grid_max + 14) * math.sin(radius_label_angle),\n        }\n    ]\n)\n\n# Plot — plotnine has no coord_polar (unlike ggplot2), so the circle is built\n# from Cartesian geoms: geom_path/geom_segment for the grid, geom_point/geom_text for data and labels.\nplot = (\n    ggplot()\n    # Circular gridlines\n    + geom_path(aes(x=\"x\", y=\"y\", group=\"radius\"), data=grid_df, color=INK_SOFT, size=0.4, alpha=0.4, linetype=\"dashed\")\n    # Radial spokes\n    + geom_segment(aes(x=\"x1\", y=\"y1\", xend=\"x2\", yend=\"y2\"), data=spoke_df, color=INK_SOFT, size=0.4, alpha=0.4)\n    # Data line\n    + geom_path(aes(x=\"x\", y=\"y\"), data=df_closed, color=BRAND, size=1.5, alpha=0.9)\n    # Data points\n    + geom_point(aes(x=\"x\", y=\"y\"), data=df, color=PAGE_BG, fill=BRAND, size=4, stroke=1.5)\n    # Peak activity hour, emphasized with a larger marker\n    + geom_point(aes(x=\"x\", y=\"y\"), data=peak_df, color=PAGE_BG, fill=BRAND, size=7, stroke=1.8)\n    # Hour labels\n    + geom_text(aes(x=\"x\", y=\"y\", label=\"label\"), data=label_df, size=11, color=INK_SOFT)\n    # Radius value labels\n    + geom_text(aes(x=\"x\", y=\"y\", label=\"label\"), data=radius_label_df, size=8, color=INK_MUTED, ha=\"left\")\n    # Radial axis title, describing what the radius numbers measure\n    + geom_text(\n        aes(x=\"x\", y=\"y\", label=\"label\"),\n        data=radial_axis_label_df,\n        size=8,\n        color=INK_MUTED,\n        ha=\"left\",\n        fontweight=\"bold\",\n    )\n    + coord_fixed(ratio=1)\n    + scale_x_continuous(limits=(-axis_limit, axis_limit))\n    + scale_y_continuous(limits=(-axis_limit, axis_limit))\n    + labs(title=\"Hourly Activity Levels · polar-basic · python · plotnine · anyplot.ai\")\n    + theme(\n        figure_size=(6, 6),\n        plot_title=element_text(size=12, ha=\"center\", color=INK),\n        axis_title=element_blank(),\n        axis_text=element_blank(),\n        axis_ticks=element_blank(),\n        axis_line=element_blank(),\n        panel_grid_major=element_blank(),\n        panel_grid_minor=element_blank(),\n        panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        plot_margin_top=0.01,\n        plot_margin_bottom=0.02,\n        plot_margin_left=0.02,\n        plot_margin_right=0.02,\n    )\n)\n\n# Save\nplot.save(f\"plot-{THEME}.png\", dpi=400, width=6, height=6, units=\"in\")\n"}