{"spec_id":"horizon-basic","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\nhorizon-basic: Horizon Chart\nLibrary: plotly 6.7.0 | Python 3.13.13\nQuality: 92/100 | Updated: 2026-05-07\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport plotly.graph_objects as go\nfrom plotly.subplots import make_subplots\n\n\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 = \"rgba(26,26,23,0.10)\" if THEME == \"light\" else \"rgba(240,239,232,0.10)\"\n\n# Data: Server metrics over 24 hours for 6 servers\nnp.random.seed(42)\nn_points = 200\nn_series = 6\nseries_names = [\"Server A\", \"Server B\", \"Server C\", \"Server D\", \"Server E\", \"Server F\"]\n\n# Generate time points over 24 hours\nhours = np.linspace(0, 24, n_points)\n\n# Generate realistic CPU usage patterns with variations from baseline (50%)\ndata = []\nfor i, name in enumerate(series_names):\n    # Different patterns for each server\n    base = np.sin(hours * np.pi / 12 + i * 0.5) * 15  # Daily cycle\n    noise = np.cumsum(np.random.randn(n_points) * 0.5)  # Random walk\n    spikes = np.random.choice([0, 1], n_points, p=[0.95, 0.05]) * np.random.randn(n_points) * 20\n    values = base + noise + spikes\n    data.append({\"series\": name, \"hours\": hours, \"values\": values})\n\n# Horizon chart parameters\nn_bands = 3\ncolors_pos = [\"#a6cee3\", \"#1f78b4\", \"#033860\"]  # Light to dark blue\ncolors_neg = [\"#fb9a99\", \"#e31a1c\", \"#67000d\"]  # Light to dark red\n\n# Create subplots - one row per series\nfig = make_subplots(rows=n_series, cols=1, shared_xaxes=True, vertical_spacing=0.02, row_heights=[1] * n_series)\n\n# Calculate global max for consistent band sizing\nall_values = np.concatenate([d[\"values\"] for d in data])\nband_size = np.max(np.abs(all_values)) / n_bands\n\n# Build horizon chart for each series\nfor row_idx, series_data in enumerate(data, 1):\n    values = series_data[\"values\"]\n    x = series_data[\"hours\"]\n    name = series_data[\"series\"]\n\n    # Create bands for positive values (folded)\n    for band in range(n_bands):\n        band_min = band * band_size\n        band_max = (band + 1) * band_size\n\n        # Clip positive values to this band\n        y_pos = np.clip(values, band_min, band_max) - band_min\n        y_pos = np.where(values > band_min, y_pos, 0)\n\n        fig.add_trace(\n            go.Scatter(\n                x=x,\n                y=y_pos,\n                fill=\"tozeroy\",\n                fillcolor=colors_pos[band],\n                line=dict(width=0),\n                mode=\"lines\",\n                showlegend=False,\n                hoverinfo=\"skip\",\n            ),\n            row=row_idx,\n            col=1,\n        )\n\n    # Create bands for negative values (folded, mirrored to positive)\n    for band in range(n_bands):\n        band_min = band * band_size\n        band_max = (band + 1) * band_size\n\n        # Clip negative values (absolute) to this band and mirror\n        neg_values = np.abs(np.minimum(values, 0))\n        y_neg = np.clip(neg_values, band_min, band_max) - band_min\n        y_neg = np.where(neg_values > band_min, y_neg, 0)\n\n        fig.add_trace(\n            go.Scatter(\n                x=x,\n                y=y_neg,\n                fill=\"tozeroy\",\n                fillcolor=colors_neg[band],\n                line=dict(width=0),\n                mode=\"lines\",\n                showlegend=False,\n                hoverinfo=\"skip\",\n            ),\n            row=row_idx,\n            col=1,\n        )\n\n    # Add series label with larger font size\n    fig.add_annotation(\n        x=0.5,\n        y=band_size * 0.7,\n        xref=f\"x{row_idx}\" if row_idx > 1 else \"x\",\n        yref=f\"y{row_idx}\" if row_idx > 1 else \"y\",\n        text=name,\n        showarrow=False,\n        font=dict(size=20, color=INK),\n        xanchor=\"left\",\n    )\n\n# Update layout with theme-aware colors\nfig.update_layout(\n    title=dict(text=\"horizon-basic · plotly · anyplot.ai\", font=dict(size=28, color=INK), x=0.5, xanchor=\"center\"),\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAGE_BG,\n    font=dict(color=INK),\n    showlegend=False,\n    margin=dict(l=100, r=50, t=100, b=80),\n)\n\n# Update x-axes with theme-aware colors and larger tick font\nfig.update_xaxes(\n    title_text=\"Hour of Day\",\n    title_font=dict(size=22, color=INK),\n    tickfont=dict(size=18, color=INK_SOFT),\n    gridcolor=GRID,\n    linecolor=INK_SOFT,\n    row=n_series,\n    col=1,\n)\n\n# Update y-axes - hide tick labels but keep consistent range\nfor i in range(1, n_series + 1):\n    fig.update_yaxes(\n        range=[0, band_size], showticklabels=False, showgrid=False, zeroline=False, linecolor=INK_SOFT, row=i, col=1\n    )\n\n# Add more prominent legend for color interpretation\nfig.add_annotation(\n    x=0.98,\n    y=1.02,\n    xref=\"paper\",\n    yref=\"paper\",\n    text=\"<b>Positive:</b> Blue (light→dark) | <b>Negative:</b> Red (light→dark)\",\n    showarrow=False,\n    font=dict(size=16, color=INK),\n    xanchor=\"right\",\n)\n\n# Save as PNG (4800x2700) and HTML with theme-suffixed filenames\nfig.write_image(f\"plot-{THEME}.png\", width=1600, height=900, scale=3)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}