{"spec_id":"horizon-basic","library":"seaborn","language":"python","code":"\"\"\" anyplot.ai\nhorizon-basic: Horizon Chart\nLibrary: seaborn 0.13.2 | Python 3.13.15\nQuality: 85/100 | Updated: 2026-08-18\n\"\"\"\n\nimport os\n\nimport matplotlib.patches as mpatches\nimport numpy as np\nimport pandas as pd\nimport seaborn as sns\n\n\n# Theme tokens (see prompts/default-style-guide.md \"Background\" + \"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\"\n\n# Imprint palette — blue (gain) vs. matte red (loss) diverging pair; both are\n# CVD-distinguishable, unlike a green/red pairing that deuteranopes/protanopes\n# cannot tell apart.\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\nGAIN_BASE = IMPRINT_PALETTE[2]  # blue\nLOSS_BASE = IMPRINT_PALETTE[4]  # matte red — semantic anchor for loss\n\n# Data - stock price deviations from 20-day moving average (5 stocks over 90 trading days)\nnp.random.seed(42)\ntrading_days = 90\nstocks = [\"TECH\", \"FINANCE\", \"ENERGY\", \"HEALTHCARE\", \"RETAIL\"]\n\ndata = []\nfor stock_idx, stock in enumerate(stocks):\n    np.random.seed(42 + stock_idx)\n    if stock == \"TECH\":\n        base = 8 * np.sin(np.linspace(0, 4 * np.pi, trading_days)) + 3\n        noise = np.random.randn(trading_days) * 5\n        values = base + noise\n    elif stock == \"FINANCE\":\n        base = np.zeros(trading_days)\n        noise = np.random.randn(trading_days) * 6\n        volatility_spikes = np.random.choice([-8, 0, 8], trading_days, p=[0.15, 0.7, 0.15])\n        values = base + noise + volatility_spikes\n    elif stock == \"ENERGY\":\n        base = -5 * np.ones(trading_days)\n        trend = np.linspace(-5, 5, trading_days)\n        noise = np.random.randn(trading_days) * 4\n        values = base + trend + noise\n    elif stock == \"HEALTHCARE\":\n        base = 6 * np.cos(np.linspace(0, 3 * np.pi, trading_days))\n        noise = np.random.randn(trading_days) * 4\n        values = base + noise\n    else:\n        drift = np.linspace(-8, 8, trading_days)\n        noise = np.random.randn(trading_days) * 3\n        values = drift + noise\n\n    values = np.clip(values, -15, 15)\n    for day, v in enumerate(values):\n        data.append({\"day\": day, \"stock\": stock, \"deviation\": v})\n\ndf = pd.DataFrame(data)\n\n# Horizon chart parameters — 3 intensity bands per polarity, generated from the\n# Imprint gain/loss anchors via seaborn's own sequential-palette builder.\nn_bands = 3\nband_height = 15 / n_bands\ngain_colors = sns.light_palette(GAIN_BASE, n_colors=n_bands + 1)[1:]\nloss_colors = sns.light_palette(LOSS_BASE, n_colors=n_bands + 1)[1:]\n\n# Theme-adaptive chrome via seaborn's rc-based theme context\nsns.set_theme(\n    style=\"ticks\",\n    rc={\n        \"figure.facecolor\": PAGE_BG,\n        \"axes.facecolor\": PAGE_BG,\n        \"axes.edgecolor\": INK_SOFT,\n        \"axes.labelcolor\": INK,\n        \"text.color\": INK,\n        \"xtick.color\": INK_SOFT,\n        \"ytick.color\": INK_SOFT,\n        \"grid.color\": INK,\n        \"grid.alpha\": 0.15,\n        \"legend.facecolor\": ELEVATED_BG,\n        \"legend.edgecolor\": INK_SOFT,\n    },\n)\n\n# Plot — one row per stock via seaborn's FacetGrid, hand-filled with folded\n# horizon bands (see prompts/library/seaborn.md \"Canvas — hard rule\")\ng = sns.FacetGrid(df, row=\"stock\", row_order=stocks, sharex=True, sharey=True, despine=True)\ng.fig.set_size_inches(8, 4.5)\ng.fig.set_dpi(400)\ng.fig.subplots_adjust(hspace=0.06, top=0.82, bottom=0.34, left=0.24, right=0.97)\ng.set_titles(\"\")\n\ntick_positions = np.arange(0, trading_days, 15)\ntick_labels = [f\"Day {i}\" for i in tick_positions]\n\nfor idx, (stock, ax) in enumerate(zip(stocks, g.axes.flat, strict=True)):\n    stock_data = df[df[\"stock\"] == stock]\n    x = np.arange(len(stock_data))\n    values = stock_data[\"deviation\"].to_numpy()\n\n    ax.set_xlim(0, len(x))\n    ax.set_ylim(0, band_height)\n    ax.set_xticks(tick_positions)\n\n    gain_vals = np.maximum(values, 0)\n    loss_vals = np.abs(np.minimum(values, 0))\n\n    for band_idx in range(n_bands):\n        band_min = band_idx * band_height\n\n        loss_folded = np.clip(loss_vals - band_min, 0, band_height)\n        loss_mask = (loss_vals > band_min) & (values < 0)\n        ax.fill_between(\n            x, 0, np.where(loss_mask, loss_folded, np.nan), color=loss_colors[band_idx], alpha=0.95, linewidth=0\n        )\n\n        gain_folded = np.clip(gain_vals - band_min, 0, band_height)\n        gain_mask = (gain_vals > band_min) & (values > 0)\n        ax.fill_between(\n            x, 0, np.where(gain_mask, gain_folded, np.nan), color=gain_colors[band_idx], alpha=0.95, linewidth=0\n        )\n\n    ax.set_ylabel(stock, fontsize=16, rotation=0, ha=\"right\", va=\"center\", labelpad=15, color=INK)\n    ax.set_yticks([])\n    ax.grid(True, axis=\"x\", alpha=0.2, linewidth=0.8, color=INK_SOFT)\n    ax.set_axisbelow(True)\n    for spine in (\"top\", \"right\", \"left\"):\n        ax.spines[spine].set_visible(False)\n    ax.spines[\"bottom\"].set_visible(idx == len(stocks) - 1)\n    is_last = idx == len(stocks) - 1\n    ax.tick_params(axis=\"x\", labelsize=14, bottom=is_last, labelbottom=is_last)\n\n# X-axis formatting — labels/ticklabels only on the last facet, but every facet\n# shares the same tick_positions (set above) so gridlines align vertically across rows\nlast_ax = g.axes.flat[-1]\nlast_ax.set_xticklabels(tick_labels)\nlast_ax.set_xlabel(\"Trading Days (90-day period)\", fontsize=18, color=INK)\n\n# Title + subtitle clarifying what the bands measure\ng.fig.suptitle(\n    \"horizon-basic · python · seaborn · anyplot.ai\", fontsize=18, y=0.98, va=\"top\", fontweight=\"bold\", color=INK\n)\ng.fig.text(\n    0.5,\n    0.895,\n    \"Deviation from 20-Day Moving Average (percentage points)\",\n    ha=\"center\",\n    va=\"top\",\n    fontsize=13,\n    color=INK_SOFT,\n)\n\n# Legend — single row anchored below the x-axis label, entirely clear of every facet's data\nlegend_patches = [\n    mpatches.Patch(color=gain_colors[0], label=\"Gain 0-5 pp\"),\n    mpatches.Patch(color=gain_colors[1], label=\"Gain 5-10 pp\"),\n    mpatches.Patch(color=gain_colors[2], label=\"Gain 10-15 pp\"),\n    mpatches.Patch(color=loss_colors[0], label=\"Loss 0-5 pp\"),\n    mpatches.Patch(color=loss_colors[1], label=\"Loss 5-10 pp\"),\n    mpatches.Patch(color=loss_colors[2], label=\"Loss 10-15 pp\"),\n]\ng.fig.legend(\n    handles=legend_patches,\n    loc=\"lower center\",\n    bbox_to_anchor=(0.5, 0.01),\n    fontsize=15,\n    framealpha=0.95,\n    facecolor=ELEVATED_BG,\n    edgecolor=INK_SOFT,\n    ncol=3,\n    handlelength=1.4,\n    handletextpad=0.5,\n    columnspacing=1.3,\n)\n\n# Save\ng.fig.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}