{"spec_id":"spiral-timeseries","library":"matplotlib","language":"python","code":"\"\"\" anyplot.ai\nspiral-timeseries: Spiral Time Series Chart\nLibrary: matplotlib 3.11.1 | Python 3.13.15\nQuality: 90/100 | Updated: 2026-08-17\n\"\"\"\n\nimport os\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nfrom matplotlib.collections import LineCollection\nfrom matplotlib.colors import LinearSegmentedColormap, TwoSlopeNorm\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\"\n\n# Data: Daily average temperatures over 5 years (Berlin-like climate)\nnp.random.seed(42)\nn_years = 5\nstart_year = 2019\ndays_per_year = 365\ntotal_days = n_years * days_per_year\n\nday_idx = np.arange(total_days)\nday_of_year = day_idx % days_per_year\n\nseasonal = -11.0 * np.cos(2 * np.pi * day_of_year / days_per_year)\nwarming_trend = 0.6 * day_idx / total_days\nnoise = np.random.normal(0, 3.5, total_days)\ntemperatures = 10.0 + seasonal + warming_trend + noise\n\n# Archimedean spiral: r = r0 + spacing * (cumulative_angle / 2π)\nr0 = 1.2\nrev_spacing = 1.4\ntotal_angle = day_idx * (2 * np.pi / days_per_year)\nr = r0 + rev_spacing * total_angle / (2 * np.pi)\n\n# Clockwise rotation, January starts at 12 o'clock\ntheta = -total_angle + np.pi / 2\nx = r * np.cos(theta)\ny = r * np.sin(theta)\n\nr_max = r0 + rev_spacing * n_years\n\n# Plot — square canvas suits the radially symmetric spiral (see default-style-guide.md)\nfig, ax = plt.subplots(figsize=(6, 6), dpi=400, facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\nax.set_aspect(\"equal\")\n\n# Spiral colored by temperature with the Imprint diverging colormap.\n# Temperature has a physically meaningful midpoint (freezing, 0°C) and a strong\n# domain color convention (cold→blue, hot→red), so imprint_div is oriented\n# blue→neutral→red rather than the library's default red→neutral→blue order.\nmidpoint = PAGE_BG\nimprint_div = LinearSegmentedColormap.from_list(\"imprint_div\", [\"#4467A3\", midpoint, \"#AE3030\"])\nnorm = TwoSlopeNorm(vcenter=0.0, vmin=temperatures.min(), vmax=temperatures.max())\n\npoints = np.column_stack([x, y]).reshape(-1, 1, 2)\nsegments = np.concatenate([points[:-1], points[1:]], axis=1)\nlc = LineCollection(segments, cmap=imprint_div, norm=norm, linewidth=2.5, alpha=0.95)\nlc.set_array(temperatures[:-1])\nax.add_collection(lc)\n\n# Month radial grid lines and outer labels\nmonth_names = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"]\nfor m in range(12):\n    m_angle = -2 * np.pi * m / 12 + np.pi / 2\n    ax.plot([0, r_max * np.cos(m_angle)], [0, r_max * np.sin(m_angle)], color=INK_SOFT, alpha=0.22, linewidth=0.8)\n    label_r = r_max + 0.62\n    cos_a, sin_a = np.cos(m_angle), np.sin(m_angle)\n    ha = \"left\" if cos_a > 0.1 else (\"right\" if cos_a < -0.1 else \"center\")\n    va = \"bottom\" if sin_a > 0.1 else (\"top\" if sin_a < -0.1 else \"center\")\n    ax.text(label_r * cos_a, label_r * sin_a, month_names[m], ha=ha, va=va, fontsize=12, color=INK_SOFT)\n\n# Concentric year-boundary rings and year labels, placed precisely at the Jan\n# start of each revolution (12 o'clock, straight above the ring)\nfor yi in range(n_years + 1):\n    ring_r = r0 + rev_spacing * yi\n    ring_theta = np.linspace(0, 2 * np.pi, 500)\n    ax.plot(\n        ring_r * np.cos(ring_theta),\n        ring_r * np.sin(ring_theta),\n        color=INK_SOFT,\n        alpha=0.22,\n        linewidth=0.8,\n        linestyle=\"--\",\n    )\n    if yi < n_years:\n        ax.text(\n            0, ring_r + 0.08, str(start_year + yi), ha=\"center\", va=\"bottom\", fontsize=12, fontweight=\"bold\", color=INK\n        )\n\n# Colorbar\nsm = plt.cm.ScalarMappable(cmap=imprint_div, norm=norm)\nsm.set_array([])\ncbar = fig.colorbar(sm, ax=ax, fraction=0.030, pad=0.04, aspect=25)\ncbar.set_label(\"Daily Temperature (°C)\", fontsize=15, color=INK)\ncbar.ax.tick_params(labelsize=11, colors=INK_SOFT, labelcolor=INK_SOFT)\ncbar.outline.set_edgecolor(INK_SOFT)\nplt.setp(cbar.ax.yaxis.get_ticklabels(), color=INK_SOFT)\n\n# Axis bounds, title, and cleanup\nmargin = r_max + 1.6\nax.set_xlim(-margin, margin)\nax.set_ylim(-margin, margin)\nax.axis(\"off\")\n\n# Title fontsize scales with title length off the 67-char baseline (see\n# prompts/plot-generator.md). The square canvas is narrower (2400px) than the\n# landscape baseline (3200px) the 12pt default targets, so the effective\n# baseline shrinks by the same 2400/3200 width ratio.\ntitle = \"spiral-timeseries · python · matplotlib · anyplot.ai\"\nsquare_baseline = round(12 * 2400 / 3200)\ntitle_fontsize = max(8, round(square_baseline * 67 / len(title))) if len(title) > 67 else square_baseline\nax.set_title(title, fontsize=title_fontsize, fontweight=\"medium\", color=INK, pad=16)\n\nplt.tight_layout()\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}