{"spec_id":"spiral-timeseries","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\nspiral-timeseries: Spiral Time Series Chart\nLibrary: plotnine 0.15.8 | Python 3.13.15\nQuality: 87/100 | Updated: 2026-08-18\n\"\"\"\n\nimport os\nimport sys\n\n\n# Prevent this file from shadowing the plotnine package when imported by name\n_this_dir = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if p not in (\"\", \".\", _this_dir) and os.path.abspath(p) != _this_dir]\nif \"plotnine\" in sys.modules and getattr(sys.modules[\"plotnine\"], \"__file__\", \"\") == os.path.abspath(__file__):\n    del sys.modules[\"plotnine\"]\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_segment,\n    geom_text,\n    ggplot,\n    labs,\n    scale_color_gradient2,\n    theme,\n    theme_minimal,\n)\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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Data\nnp.random.seed(42)\nn_years = 5\ndays_per_year = 365\nstart_year = 2019\n\ntotal_days = n_years * days_per_year\nday_idx = np.arange(total_days, dtype=float)\ndoy = (day_idx % days_per_year) + 1\nyear_idx = (day_idx // days_per_year).astype(int)\n\n# Daily temperature with Northern Hemisphere seasonal pattern (°C)\n# Peak ~late July, trough ~late January\ntemperature = (\n    12.0 + 13.0 * np.sin(2 * np.pi * (doy - 80) / days_per_year) + 0.5 * year_idx + np.random.normal(0, 2.0, total_days)\n)\n\n# Archimedean spiral: clockwise from top (Jan 1 = 12 o'clock position)\nr_base = 1.8\nr_expand = 1.3  # radius increase per full revolution\ntheta = np.pi / 2 - 2 * np.pi * day_idx / days_per_year\nr = r_base + r_expand * (day_idx / days_per_year)\n\nspiral_df = pd.DataFrame({\"x\": r * np.cos(theta), \"y\": r * np.sin(theta), \"temperature\": temperature})\n\n# Radial grid lines at month boundaries\nmonth_doys = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]\nmonth_names = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"]\nmonth_angles = [np.pi / 2 - 2 * np.pi * d / days_per_year for d in month_doys]\n\nr_inner = r_base * 0.55\nr_outer = r_base + r_expand * n_years + 0.2\n\ngrid_df = pd.DataFrame(\n    {\n        \"x\": [r_inner * np.cos(a) for a in month_angles],\n        \"y\": [r_inner * np.sin(a) for a in month_angles],\n        \"xend\": [r_outer * np.cos(a) for a in month_angles],\n        \"yend\": [r_outer * np.sin(a) for a in month_angles],\n    }\n)\n\n# Month labels just beyond the outer spiral edge\nr_label = r_outer + 0.6\nlabel_df = pd.DataFrame(\n    {\n        \"x\": [r_label * np.cos(a) for a in month_angles],\n        \"y\": [r_label * np.sin(a) for a in month_angles],\n        \"label\": month_names,\n    }\n)\n\n# Year labels at Jan 1 of each revolution. The spiral's local radius barely\n# changes near the Jan boundary (the curve is nearly tangential there), so an\n# x-only offset at the same radius still sits on the ring. Nudge the label\n# radially inward into the empty gap between revolutions instead, plus a\n# small x offset to clear the Jan grid line.\nyear_label_df = pd.DataFrame(\n    {\n        \"x\": [-0.45] * n_years,\n        \"y\": [r_base + r_expand * yi - 0.55 * r_expand for yi in range(n_years)],\n        \"label\": [str(start_year + yi) for yi in range(n_years)],\n    }\n)\n\n# Symmetric plot bounds so the spiral sits centered rather than\n# drifting toward the year-label side\nextent = r_label + 0.7\n\n# Plot\nplot = (\n    ggplot(spiral_df, aes(\"x\", \"y\", color=\"temperature\"))\n    + geom_segment(\n        data=grid_df,\n        mapping=aes(x=\"x\", y=\"y\", xend=\"xend\", yend=\"yend\"),\n        color=INK_SOFT,\n        size=0.4,\n        alpha=0.35,\n        inherit_aes=False,\n    )\n    + geom_path(size=1.3)\n    + geom_text(data=label_df, mapping=aes(x=\"x\", y=\"y\", label=\"label\"), color=INK_SOFT, size=3.2, inherit_aes=False)\n    + geom_text(\n        data=year_label_df,\n        mapping=aes(x=\"x\", y=\"y\", label=\"label\"),\n        color=INK_MUTED,\n        size=2.8,\n        ha=\"right\",\n        inherit_aes=False,\n    )\n    + scale_color_gradient2(low=\"#4467A3\", mid=PAGE_BG, high=\"#AE3030\", midpoint=13, name=\"Temp (°C)\")\n    + coord_fixed(ratio=1, xlim=(-extent, extent), ylim=(-extent, extent))\n    + labs(title=\"spiral-timeseries · python · plotnine · anyplot.ai\")\n    + theme_minimal()\n    + theme(\n        figure_size=(6, 6),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n        panel_grid_major=element_blank(),\n        panel_grid_minor=element_blank(),\n        panel_border=element_blank(),\n        axis_title=element_blank(),\n        axis_text=element_blank(),\n        axis_ticks=element_blank(),\n        axis_line=element_blank(),\n        plot_title=element_text(color=INK, size=13),\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT, size=0.3),\n        legend_text=element_text(color=INK_SOFT, size=8),\n        legend_title=element_text(color=INK, size=9),\n        legend_position=\"right\",\n        legend_box_margin=9,\n    )\n)\n\n# Save\nplot.save(f\"plot-{THEME}.png\", dpi=400, width=6, height=6)\n"}