{"spec_id":"spiral-timeseries","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nspiral-timeseries: Spiral Time Series Chart\nLibrary: letsplot 4.9.0 | Python 3.13.13\nQuality: 85/100 | Created: 2026-05-07\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import (\n    LetsPlot,\n    aes,\n    coord_fixed,\n    element_rect,\n    element_text,\n    geom_path,\n    geom_segment,\n    geom_text,\n    ggplot,\n    ggsize,\n    labs,\n    scale_color_viridis,\n    theme,\n    theme_void,\n)\nfrom lets_plot.export import ggsave\n\n\nLetsPlot.setup_html()\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\n# 5 years of synthetic daily average temperatures (northern hemisphere)\nnp.random.seed(42)\ndates = pd.date_range(\"2019-01-01\", \"2023-12-31\", freq=\"D\")\nn = len(dates)\n\ndoy = dates.day_of_year.values\nyear_num = (dates.year - 2019).values\ndays_in_yr = np.array([366 if y % 4 == 0 else 365 for y in dates.year])\nfrac_year = (doy - 1) / days_in_yr\n\ntemp = 15.0 * np.sin(2 * np.pi * (doy - 80) / days_in_yr) + 12.0 + np.random.normal(0, 2.0, n)\n\n# Archimedean spiral: clockwise from top (Jan 1 = 12 o'clock)\ninner_r = 1.5\nspacing = 1.0\nangle = np.pi / 2 - 2 * np.pi * (year_num + frac_year)\nr = inner_r + spacing * (year_num + frac_year)\ndf = pd.DataFrame({\"x\": r * np.cos(angle), \"y\": r * np.sin(angle), \"temp\": temp})\n\n# Radial grid lines at each month start\ngrid_rows = []\nfor ms in pd.date_range(\"2019-01-01\", \"2023-12-01\", freq=\"MS\"):\n    yr = ms.year - 2019\n    frac_m = (ms.day_of_year - 1) / (366 if ms.year % 4 == 0 else 365)\n    ang = np.pi / 2 - 2 * np.pi * (yr + frac_m)\n    r_end = inner_r + spacing * (yr + frac_m)\n    grid_rows.append({\"x\": 0.0, \"y\": 0.0, \"xend\": r_end * np.cos(ang), \"yend\": r_end * np.sin(ang)})\ngrid_df = pd.DataFrame(grid_rows)\n\n# Year labels slightly left of the top spoke\nang_lbl = np.pi / 2 + 0.18\nyear_df = pd.DataFrame(\n    [\n        {\n            \"x\": (inner_r + spacing * yr) * np.cos(ang_lbl),\n            \"y\": (inner_r + spacing * yr) * np.sin(ang_lbl),\n            \"label\": str(2019 + yr),\n        }\n        for yr in range(5)\n    ]\n)\n\n# Month labels on the outer ring using 2020 (leap year) reference angles\nMONTHS = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"]\nouter_r = inner_r + spacing * 5 + 0.65\nmonth_rows = []\nfor i, mname in enumerate(MONTHS):\n    mid = pd.Timestamp(2020, i + 1, 15)\n    frac_m = (mid.day_of_year - 1) / 366\n    ang_m = np.pi / 2 - 2 * np.pi * frac_m\n    month_rows.append({\"x\": outer_r * np.cos(ang_m), \"y\": outer_r * np.sin(ang_m), \"label\": mname})\nmonth_df = pd.DataFrame(month_rows)\n\n# Theme: void base + custom chrome\nanyplot_theme = theme_void() + theme(\n    plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    panel_background=element_rect(fill=PAGE_BG),\n    plot_title=element_text(color=INK, size=24),\n    legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n    legend_text=element_text(color=INK_SOFT, size=16),\n    legend_title=element_text(color=INK, size=16),\n)\n\ntitle = \"Daily Temperatures 2019–2023 · spiral-timeseries · letsplot · anyplot.ai\"\n\nplot = (\n    ggplot()\n    + geom_segment(\n        data=grid_df, mapping=aes(x=\"x\", y=\"y\", xend=\"xend\", yend=\"yend\"), color=INK_SOFT, alpha=0.25, size=0.5\n    )\n    + geom_path(data=df, mapping=aes(x=\"x\", y=\"y\", color=\"temp\"), size=2.5)\n    + scale_color_viridis(name=\"Temp (°C)\")\n    + geom_text(data=year_df, mapping=aes(x=\"x\", y=\"y\", label=\"label\"), color=INK, size=14)\n    + geom_text(data=month_df, mapping=aes(x=\"x\", y=\"y\", label=\"label\"), color=INK_SOFT, size=11)\n    + coord_fixed()\n    + labs(title=title)\n    + anyplot_theme\n    + ggsize(900, 900)\n)\n\nggsave(plot, filename=f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(plot, filename=f\"plot-{THEME}.html\", path=\".\")\n"}