{"spec_id":"line-cycle-seasonal","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nline-cycle-seasonal: Cycle Plot (Seasonal Subseries)\nLibrary: letsplot 4.10.1 | Python 3.13.13\nQuality: 86/100 | Created: 2026-06-15\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import (\n    LetsPlot,\n    aes,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_line,\n    geom_segment,\n    geom_text,\n    ggplot,\n    ggsize,\n    labs,\n    scale_x_continuous,\n    theme,\n)\nfrom lets_plot.export import ggsave\n\n\nLetsPlot.setup_html()\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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\nGRID_LINE = \"#D8D6D0\" if THEME == \"light\" else \"#3A3A36\"\n\nBRAND = \"#009E73\"  # Imprint palette position 1 — always first series\n\n# Data: monthly mean temperature (°C) for a temperate European city, 1991–2020\nnp.random.seed(42)\nyears = np.arange(1991, 2021)  # 30 years\nmonth_names = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"]\n\n# Seasonal baseline + gentle warming trend (0.045 °C/year → ~1.3 °C over 30 years)\nseasonal_base = np.array([-1.0, 1.2, 5.5, 10.8, 15.6, 19.4, 21.3, 20.9, 16.7, 11.2, 5.0, 0.8])\nwarming = 0.045\n\nrecords = []\nfor m_idx in range(12):\n    for y_idx, year in enumerate(years):\n        temp = seasonal_base[m_idx] + warming * y_idx + np.random.normal(0, 0.7)\n        records.append(\n            {\n                \"month_num\": m_idx + 1,\n                \"month\": month_names[m_idx],\n                \"year\": year,\n                \"y_idx\": y_idx,\n                \"temperature\": round(temp, 2),\n            }\n        )\n\ndf = pd.DataFrame(records)\n\n# x-positions: each month group occupies GROUP_WIDTH units (30 pts + 2-unit gap)\nGROUP_WIDTH = 32\ndf[\"x_pos\"] = (df[\"month_num\"] - 1) * GROUP_WIDTH + df[\"y_idx\"]\n\n# Per-month mean reference line endpoints\nmeans = df.groupby(\"month_num\")[\"temperature\"].mean().reset_index()\nmeans[\"x_start\"] = (means[\"month_num\"] - 1) * GROUP_WIDTH - 0.5\nmeans[\"x_end\"] = means[\"x_start\"] + 30.0\nmeans[\"x_center\"] = (means[\"month_num\"] - 1) * GROUP_WIDTH + 14.5\nmeans[\"label\"] = means[\"temperature\"].round(1).astype(str) + \"°C\"\nmeans[\"label_y\"] = means[\"temperature\"] + 0.8\n\n# Subtle vertical dividers between month groups\ndivider_xs = [(m * GROUP_WIDTH - 1.0) for m in range(1, 12)]\ny_lo = df[\"temperature\"].min() - 1.5\ny_hi = df[\"temperature\"].max() + 1.5\ndividers = pd.DataFrame({\"x\": divider_xs, \"xend\": divider_xs, \"y\": [y_lo] * 11, \"yend\": [y_hi] * 11})\n\n# X-axis tick at center of each month group\ntick_breaks = [(m - 1) * GROUP_WIDTH + 14.5 for m in range(1, 13)]\n\ntitle = \"line-cycle-seasonal · python · letsplot · anyplot.ai\"\n# 52 chars < 67 baseline → title_size stays at default 16\ntitle_size = 16\n\nanyplot_theme = theme(\n    plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    panel_background=element_rect(fill=PAGE_BG),\n    panel_border=element_blank(),\n    panel_grid_major=element_line(color=GRID_LINE, size=0.25),\n    panel_grid_minor=element_blank(),\n    axis_title=element_text(color=INK, size=12),\n    axis_text=element_text(color=INK_SOFT, size=10),\n    axis_line=element_line(color=INK_SOFT),\n    plot_title=element_text(color=INK, size=title_size),\n    legend_position=\"none\",\n)\n\nplot = (\n    ggplot()\n    # Subtle vertical dividers between month groups\n    + geom_segment(data=dividers, mapping=aes(x=\"x\", xend=\"xend\", y=\"y\", yend=\"yend\"), color=INK_MUTED, size=0.4)\n    # Subseries: chronological line per month\n    + geom_line(data=df, mapping=aes(x=\"x_pos\", y=\"temperature\", group=\"month_num\"), color=BRAND, size=0.9)\n    # Seasonal mean reference lines (key visual for comparing seasons)\n    + geom_segment(\n        data=means, mapping=aes(x=\"x_start\", xend=\"x_end\", y=\"temperature\", yend=\"temperature\"), color=INK, size=1.5\n    )\n    # Mean temperature annotations — letsplot geom_text labels each season's average\n    + geom_text(\n        data=means,\n        mapping=aes(x=\"x_center\", y=\"label_y\", label=\"label\"),\n        color=INK_SOFT,\n        size=3.5,\n        vjust=0.5,\n        hjust=0.5,\n    )\n    + scale_x_continuous(breaks=tick_breaks, labels=month_names)\n    + labs(x=\"Month\", y=\"Temperature (°C)\", title=title)\n    + anyplot_theme\n    + ggsize(800, 450)\n)\n\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}