{"spec_id":"horizon-basic","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nhorizon-basic: Horizon Chart\nLibrary: altair 6.2.2 | Python 3.13.15\nQuality: 90/100 | Updated: 2026-08-18\n\"\"\"\n\nimport os\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image, ImageColor\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\"\n\n# Data: server metrics over 24 hours at 15-minute resolution\nnp.random.seed(42)\n\nn_points = 96\nhours = pd.date_range(\"2024-01-15 00:00\", periods=n_points, freq=\"15min\")\n\ndata_list = []\nservers = [\"Web Server 1\", \"Web Server 2\", \"Database\", \"Cache\", \"API Gateway\", \"Worker\"]\n\nfor server in servers:\n    t = np.linspace(0, 2 * np.pi, n_points)\n    if server == \"Database\":\n        base = 40 + 30 * np.sin(t - np.pi / 2) + np.random.randn(n_points) * 8\n    elif server == \"Cache\":\n        base = 25 + np.random.randn(n_points) * 5\n        base[40:45] += 40\n    elif server == \"Worker\":\n        base = 20 + 50 * (np.sin(t * 3) > 0.7) + np.random.randn(n_points) * 6\n    else:\n        base = 30 + 25 * np.sin(t - np.pi / 3) + np.random.randn(n_points) * 10\n\n    values = base - base.mean()\n\n    for hour, val in zip(hours, values, strict=True):\n        data_list.append({\"date\": hour, \"value\": val, \"series\": server})\n\ndf = pd.DataFrame(data_list)\n\n# Horizon bands: fold magnitude into 3 mirrored intensity bands per polarity.\n# band_height is computed per series (not globally) so low-variance series\n# like Cache still span the full Low/Medium/High range instead of being\n# flattened into \"Low\" by a shared global max.\nn_bands = 3\nFIXED_BAND_HEIGHT = 1.0\nintensity_labels = {0: \"Low\", 1: \"Medium\", 2: \"High\"}\n\nband_data = []\nfor _series, group in df.groupby(\"series\"):\n    local_band_height = group[\"value\"].abs().max() / n_bands\n    for _, row in group.iterrows():\n        val = row[\"value\"]\n        direction = \"positive\" if val >= 0 else \"negative\"\n        abs_val = abs(val)\n        for band in range(n_bands):\n            band_min = band * local_band_height\n            band_frac = max(0.0, min(abs_val - band_min, local_band_height) / local_band_height)\n            band_data.append(\n                {\n                    \"date\": row[\"date\"],\n                    \"series\": row[\"series\"],\n                    \"band\": band,\n                    \"value\": band_frac * FIXED_BAND_HEIGHT,\n                    \"label\": f\"{direction.capitalize()} {intensity_labels[band]}\",\n                }\n            )\n\nband_df = pd.DataFrame(band_data)\n\n\ndef _lerp_hex(c1, c2, t):\n    \"\"\"Linear-interpolate two hex colors — builds the imprint_div ramp below.\"\"\"\n    r1, g1, b1 = ImageColor.getrgb(c1)\n    r2, g2, b2 = ImageColor.getrgb(c2)\n    return \"#{:02X}{:02X}{:02X}\".format(round(r1 + (r2 - r1) * t), round(g1 + (g2 - g1) * t), round(b1 + (b2 - b1) * t))\n\n\n# imprint_div gradient: anchors #AE3030 red / #4467A3 blue, theme-adaptive\n# midpoint (PAGE_BG). The 3 intensity steps per polarity are interpolated\n# from the midpoint toward the anchor, so Low/Medium/High are built from the\n# documented gradient rather than hand-picked hexes.\npositive_colors = [_lerp_hex(PAGE_BG, \"#4467A3\", (i + 1) / n_bands) for i in range(n_bands)]\nnegative_colors = [_lerp_hex(PAGE_BG, \"#AE3030\", (i + 1) / n_bands) for i in range(n_bands)]\n\ncolor_scale = alt.Scale(\n    domain=[\"Positive Low\", \"Positive Medium\", \"Positive High\", \"Negative Low\", \"Negative Medium\", \"Negative High\"],\n    range=positive_colors + negative_colors,\n)\n\n# Canvas — landscape inner view sized so vl-convert's title/legend/facet-header\n# padding still lands the saved PNG within 3200x1800 at scale_factor=4.0.\nVIEW_W = 600\nROW_H = 44\n\nchart = (\n    alt.Chart(band_df)\n    .mark_area(clip=True)\n    .encode(\n        x=alt.X(\n            \"date:T\", title=\"Time (15-min intervals)\", axis=alt.Axis(format=\"%H:%M\", labelFontSize=10, titleFontSize=12)\n        ),\n        y=alt.Y(\"value:Q\", title=None, axis=None, stack=None, scale=alt.Scale(domain=[0, FIXED_BAND_HEIGHT])),\n        color=alt.Color(\n            \"label:N\",\n            scale=color_scale,\n            legend=alt.Legend(\n                title=\"Intensity\",\n                orient=\"right\",\n                titleFontSize=10,\n                labelFontSize=10,\n                symbolSize=100,\n                fillColor=ELEVATED_BG,\n                strokeColor=INK_SOFT,\n                labelColor=INK_SOFT,\n                titleColor=INK,\n            ),\n        ),\n        tooltip=[\"date:T\", \"series:N\", \"value:Q\"],\n        order=alt.Order(\"band:O\"),\n    )\n    .properties(width=VIEW_W, height=ROW_H)\n    .facet(\n        row=alt.Row(\n            \"series:N\",\n            title=None,\n            header=alt.Header(labelFontSize=10, labelAngle=0, labelAlign=\"left\", labelPadding=8, labelColor=INK),\n        )\n    )\n    .properties(\n        title=alt.Title(\"horizon-basic · python · altair · anyplot.ai\", fontSize=16, anchor=\"start\", offset=12),\n        background=PAGE_BG,\n    )\n    .configure_facet(spacing=4)\n    .configure_view(strokeWidth=0, fill=PAGE_BG)\n    .configure_axis(\n        domainColor=INK_SOFT, tickColor=INK_SOFT, gridColor=INK, gridOpacity=0.10, labelColor=INK_SOFT, titleColor=INK\n    )\n    .configure_title(color=INK)\n)\n\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\n# PAD-only to the canonical landscape target — never crop (would clip text).\nTW, TH = 3200, 1800\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TW or _h > TH:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}x{_h}, exceeds target {TW}x{TH}. \"\n        f\"Shrink chart .properties(width=, height=) values and re-render.\"\n    )\nif _w < TW or _h < TH:\n    _canvas = Image.new(\"RGB\", (TW, TH), PAGE_BG)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n\nchart.save(f\"plot-{THEME}.html\")\n"}