{"spec_id":"horizon-basic","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\nhorizon-basic: Horizon Chart\nLibrary: plotnine 0.15.4 | Python 3.13.13\nQuality: 86/100 | Updated: 2026-05-07\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom plotnine import (\n    aes,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    facet_wrap,\n    geom_area,\n    ggplot,\n    labs,\n    scale_fill_manual,\n    scale_x_continuous,\n    theme,\n    theme_minimal,\n)\n\n\n# Theme colors - theme-adaptive chrome\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\"\nGRID_COLOR = INK\n\n# Data - Environmental temperature anomalies over 5 weeks for 6 weather stations\nnp.random.seed(42)\n\nn_points = 168  # 7 days * 24 hours, but we'll use 5 weeks = 840 hours. Using 168 for compact display\nn_series = 6\nstation_names = [\"Northern Ridge\", \"Coastal Bay\", \"Highland Peak\", \"Valley Floor\", \"Forest Edge\", \"Desert Plain\"]\n\n# Create time series data (hours over ~5 weeks)\nhours = np.arange(n_points)\n\n# Generate realistic temperature anomaly data (deviation from seasonal mean)\ndata_records = []\nfor i, name in enumerate(station_names):\n    # Daily temperature cycle (warmer during day, cooler at night) - shifted per station\n    daily_cycle = 8 * np.sin(2 * np.pi * hours / 24 + i * np.pi / 6)\n    # Weekly pattern (cooler at start of week, warmer mid-week for some stations)\n    weekly_pattern = 5 * np.sin(2 * np.pi * hours / 168 + i * 0.3)\n    # Random weather events (cold snaps, heat waves)\n    noise = np.random.randn(n_points) * 2\n    # Occasional extreme events\n    extremes = np.zeros(n_points)\n    event_indices = np.random.choice(n_points, size=4, replace=False)\n    extremes[event_indices] = np.random.choice([-1, 1], size=4) * np.random.uniform(12, 18, size=4)\n\n    values = daily_cycle + weekly_pattern + noise + extremes\n\n    for h, val in zip(hours, values, strict=True):\n        data_records.append({\"hour\": h, \"value\": val, \"station\": name})\n\ndf = pd.DataFrame(data_records)\n\n# Horizon chart parameters - fold values into bands\nn_bands = 3\nmax_val = df[\"value\"].abs().max()\nband_size = max_val / n_bands\n\n# Create horizon-folded data for visualization\nhorizon_records = []\n\n# Band labels with stronger contrast\nband_labels = {\"pos0\": \"+0-2°C\", \"pos1\": \"+2-4°C\", \"pos2\": \"+4°C+\", \"neg0\": \"-0-2°C\", \"neg1\": \"-2-4°C\", \"neg2\": \"-4°C–\"}\n\n# Band order for proper layering\nband_order = [\"pos0\", \"pos1\", \"pos2\", \"neg0\", \"neg1\", \"neg2\"]\n\nfor station in station_names:\n    station_data = df[df[\"station\"] == station]\n    values = station_data[\"value\"].values\n    hours_arr = station_data[\"hour\"].values\n\n    for band in range(n_bands):\n        low = band * band_size\n        high = (band + 1) * band_size\n\n        # Process positive values (anomaly warmer than baseline)\n        pos = np.clip(np.maximum(values, 0) - low, 0, band_size)\n        # Process negative values (anomaly colder than baseline) - mirror for display\n        neg = np.clip(np.maximum(-values, 0) - low, 0, band_size)\n\n        for h, pv, nv in zip(hours_arr, pos, neg, strict=True):\n            if pv > 0.01:\n                horizon_records.append(\n                    {\"hour\": h, \"value\": pv, \"station\": station, \"band\": f\"pos{band}\", \"sign\": \"positive\"}\n                )\n            if nv > 0.01:\n                horizon_records.append(\n                    {\"hour\": h, \"value\": nv, \"station\": station, \"band\": f\"neg{band}\", \"sign\": \"negative\"}\n                )\n\nhorizon_df = pd.DataFrame(horizon_records)\n\n# Set band as categorical with explicit order for proper layering\nhorizon_df[\"band\"] = pd.Categorical(horizon_df[\"band\"], categories=band_order, ordered=True)\n\n# Enhanced color scheme with stronger contrast\n# Warm (orange-red) for positive anomalies, cool (blue) for negative anomalies\ncolors = {\n    \"pos0\": \"#ffe8cc\",  # Very light orange\n    \"pos1\": \"#ffb366\",  # Medium orange\n    \"pos2\": \"#d97706\",  # Dark orange-red\n    \"neg0\": \"#cce5ff\",  # Very light blue\n    \"neg1\": \"#66b3ff\",  # Medium blue\n    \"neg2\": \"#0052cc\",  # Dark blue\n}\n\n# Create the horizon chart\nplot = (\n    ggplot(horizon_df, aes(x=\"hour\", y=\"value\", fill=\"band\"))\n    + geom_area(position=\"identity\", alpha=0.9, color=GRID_COLOR, size=0.15)\n    + scale_fill_manual(values=colors, labels=band_labels, breaks=band_order)\n    + facet_wrap(\"~station\", ncol=2)\n    + scale_x_continuous(\n        breaks=[0, 24, 48, 72, 96, 120, 144], labels=[\"Day 1\", \"Day 2\", \"Day 3\", \"Day 4\", \"Day 5\", \"Day 6\", \"Day 7\"]\n    )\n    + labs(\n        title=\"Temperature Anomalies by Station · horizon-basic · plotnine · pyplots.ai\",\n        x=\"Time (hours)\",\n        y=\"Temperature Deviation from Baseline (°C)\",\n        fill=\"Anomaly Range\",\n    )\n    + theme_minimal()\n    + theme(\n        figure_size=(16, 9),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG, color=None),\n        plot_title=element_text(size=24, weight=\"bold\", color=INK),\n        axis_title=element_text(size=20, color=INK),\n        axis_text_x=element_text(size=16, color=INK_SOFT),\n        axis_text_y=element_text(size=16, color=INK_SOFT),\n        strip_text=element_text(size=18, weight=\"bold\", color=INK),\n        legend_position=\"right\",\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n        legend_title=element_text(size=16, weight=\"bold\", color=INK),\n        legend_text=element_text(size=14, color=INK_SOFT),\n        panel_grid_major=element_line(color=GRID_COLOR, size=0.3, alpha=0.10),\n        panel_grid_minor=element_blank(),\n        axis_line=element_line(color=INK_SOFT, size=0.3),\n        axis_ticks=element_blank(),\n    )\n)\n\n# Save as PNG with theme-suffixed filename\nplot.save(f\"plot-{THEME}.png\", dpi=300, verbose=False)\n"}