{"spec_id":"spiral-timeseries","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nspiral-timeseries: Spiral Time Series Chart\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 88/100 | Created: 2026-05-07\n\"\"\"\n\nimport os\nimport sys\n\n\n# Prevent the script's own filename from shadowing the altair package\nsys.path = [p for p in sys.path if p != os.path.dirname(os.path.abspath(__file__))]\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# Data: daily average temperatures over 5 years (one revolution per year)\nnp.random.seed(42)\nyears_list = [2019, 2020, 2021, 2022, 2023]\nrecords = []\nfor yr in years_list:\n    n_days = 366 if yr % 4 == 0 else 365\n    for doy in range(1, n_days + 1):\n        temp = 12.0 + 14.0 * np.cos(2 * np.pi * (doy - 15) / n_days) + (yr - 2019) * 0.08 + np.random.normal(0, 2.5)\n        records.append({\"year\": yr, \"doy\": doy, \"n_days\": n_days, \"temperature\": round(temp, 2)})\n\ndf = pd.DataFrame(records)\n\n# Spiral coordinates: Archimedean spiral, each year = one revolution\nBASE_R = 1.5\nSPACING = 1.2\nyear_order = {yr: i for i, yr in enumerate(years_list)}\ndf[\"year_idx\"] = df[\"year\"].map(year_order)\ndf[\"angle\"] = 2 * np.pi * (df[\"doy\"] - 1) / df[\"n_days\"] - np.pi / 2\ndf[\"radius\"] = BASE_R + SPACING * (df[\"year_idx\"] + (df[\"doy\"] - 1) / df[\"n_days\"])\ndf[\"x\"] = df[\"radius\"] * np.cos(df[\"angle\"])\ndf[\"y\"] = df[\"radius\"] * np.sin(df[\"angle\"])\n\n# Radial grid lines: one per month, from origin to outer edge\nmonth_names = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"]\nmonth_doys = [1, 32, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335]\nouter_r = BASE_R + SPACING * len(years_list)\nlabel_r = outer_r + 0.55\n\ngrid_rows = []\nfor i, doy in enumerate(month_doys):\n    angle = 2 * np.pi * (doy - 1) / 365 - np.pi / 2\n    grid_rows.append({\"x\": 0.0, \"y\": 0.0, \"seg\": i})\n    grid_rows.append({\"x\": outer_r * np.cos(angle), \"y\": outer_r * np.sin(angle), \"seg\": i})\n\ngrid_df = pd.DataFrame(grid_rows)\n\n# Month labels at outer rim\nmonth_label_rows = [\n    {\n        \"x\": label_r * np.cos(2 * np.pi * (doy - 1) / 365 - np.pi / 2),\n        \"y\": label_r * np.sin(2 * np.pi * (doy - 1) / 365 - np.pi / 2),\n        \"label\": name,\n    }\n    for name, doy in zip(month_names, month_doys, strict=True)\n]\nmonth_label_df = pd.DataFrame(month_label_rows)\n\n# Year labels at start of each revolution (Jan 1 = top of spiral)\nyear_label_rows = [{\"x\": 0.0, \"y\": -(BASE_R + SPACING * yi) - 0.25, \"label\": str(yr)} for yr, yi in year_order.items()]\nyear_label_df = pd.DataFrame(year_label_rows)\n\n# Equal-axis domain so spiral stays circular\ndomain_max = label_r + 0.6\n\n# Plot layers\ngrid_chart = (\n    alt.Chart(grid_df)\n    .mark_line(strokeWidth=0.7, opacity=0.20)\n    .encode(\n        x=alt.X(\"x:Q\", scale=alt.Scale(domain=[-domain_max, domain_max]), axis=None),\n        y=alt.Y(\"y:Q\", scale=alt.Scale(domain=[-domain_max, domain_max]), axis=None),\n        detail=\"seg:N\",\n        color=alt.value(INK_SOFT),\n    )\n)\n\nspiral_chart = (\n    alt.Chart(df)\n    .mark_circle(size=18)\n    .encode(\n        x=alt.X(\"x:Q\", scale=alt.Scale(domain=[-domain_max, domain_max]), axis=None),\n        y=alt.Y(\"y:Q\", scale=alt.Scale(domain=[-domain_max, domain_max]), axis=None),\n        color=alt.Color(\n            \"temperature:Q\",\n            scale=alt.Scale(scheme=\"viridis\"),\n            legend=alt.Legend(\n                title=\"Temp (°C)\",\n                titleFontSize=18,\n                labelFontSize=16,\n                gradientLength=220,\n                gradientThickness=18,\n                orient=\"right\",\n                titleColor=INK,\n                labelColor=INK_SOFT,\n            ),\n        ),\n        tooltip=[\n            alt.Tooltip(\"year:O\", title=\"Year\"),\n            alt.Tooltip(\"doy:Q\", title=\"Day\"),\n            alt.Tooltip(\"temperature:Q\", title=\"Temp (°C)\", format=\".1f\"),\n        ],\n    )\n)\n\nmonth_labels_chart = (\n    alt.Chart(month_label_df)\n    .mark_text(fontSize=17, fontWeight=\"normal\", align=\"center\", baseline=\"middle\")\n    .encode(\n        x=alt.X(\"x:Q\", scale=alt.Scale(domain=[-domain_max, domain_max]), axis=None),\n        y=alt.Y(\"y:Q\", scale=alt.Scale(domain=[-domain_max, domain_max]), axis=None),\n        text=\"label:N\",\n        color=alt.value(INK_SOFT),\n    )\n)\n\nyear_labels_chart = (\n    alt.Chart(year_label_df)\n    .mark_text(fontSize=15, fontWeight=\"bold\", align=\"center\", baseline=\"top\")\n    .encode(\n        x=alt.X(\"x:Q\", scale=alt.Scale(domain=[-domain_max, domain_max]), axis=None),\n        y=alt.Y(\"y:Q\", scale=alt.Scale(domain=[-domain_max, domain_max]), axis=None),\n        text=\"label:N\",\n        color=alt.value(INK),\n    )\n)\n\nchart = (\n    alt.layer(grid_chart, spiral_chart, month_labels_chart, year_labels_chart)\n    .properties(\n        width=1200,\n        height=1200,\n        background=PAGE_BG,\n        title=alt.TitleParams(\n            \"Daily Temperatures 2019–2023 · spiral-timeseries · altair · anyplot.ai\",\n            fontSize=24,\n            color=INK,\n            anchor=\"start\",\n            offset=12,\n        ),\n    )\n    .configure_view(fill=PAGE_BG, stroke=None)\n    .configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK)\n)\n\n# Save\nchart.save(f\"plot-{THEME}.png\", scale_factor=3.0)\nchart.save(f\"plot-{THEME}.html\")\n"}