{"spec_id":"streamgraph-basic","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nstreamgraph-basic: Basic Stream Graph\nLibrary: altair 6.2.2 | Python 3.13.14\nQuality: 90/100 | Updated: 2026-08-05\n\"\"\"\n\nimport os\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\n\n# Theme-adaptive chrome (Imprint palette)\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\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\"]\n\n# Data - Monthly streaming hours by music genre over two years\nnp.random.seed(42)\n\nmonths = pd.date_range(start=\"2022-01-01\", periods=24, freq=\"MS\")\ngenres = [\"Pop\", \"Rock\", \"Hip-Hop\", \"Electronic\", \"Jazz\", \"Classical\"]\n\n# Generate smooth, realistic streaming data for each genre - each genre gets its\n# own trend direction, seasonal phase, and amplitude so bands diverge rather than\n# swelling/shrinking in lockstep\nbase_by_genre = {\"Pop\": 150, \"Rock\": 100, \"Hip-Hop\": 120, \"Electronic\": 80, \"Jazz\": 40, \"Classical\": 30}\ntrend_by_genre = {\"Pop\": 25, \"Rock\": -20, \"Hip-Hop\": 35, \"Electronic\": 15, \"Jazz\": -5, \"Classical\": 5}\nphase_by_genre = {\n    \"Pop\": 0,\n    \"Rock\": np.pi / 3,\n    \"Hip-Hop\": np.pi / 2,\n    \"Electronic\": np.pi,\n    \"Jazz\": np.pi / 4,\n    \"Classical\": 3 * np.pi / 2,\n}\namplitude_by_genre = {\"Pop\": 30, \"Rock\": 15, \"Hip-Hop\": 25, \"Electronic\": 35, \"Jazz\": 10, \"Classical\": 8}\n\ndata = []\nfor genre in genres:\n    base = base_by_genre[genre]\n    trend = np.linspace(0, trend_by_genre[genre], 24)  # genre-specific growth or decline\n    seasonal = amplitude_by_genre[genre] * np.sin(np.linspace(0, 4 * np.pi, 24) + phase_by_genre[genre])\n    noise = np.random.randn(24).cumsum() * 5\n    values = base + trend + seasonal + noise\n    values = np.maximum(values, 10)  # Ensure positive values\n    for i, month in enumerate(months):\n        data.append({\"time\": month, \"category\": genre, \"value\": values[i]})\n\ndf = pd.DataFrame(data)\n\ntitle = \"streamgraph-basic · python · altair · anyplot.ai\"\n\n# Create streamgraph using area mark with center baseline (stack='center')\nchart = (\n    alt.Chart(df)\n    .mark_area(\n        interpolate=\"basis\",  # Basis spline for smooth flowing curves\n        opacity=0.9,\n    )\n    .encode(\n        x=alt.X(\"time:T\", title=\"Time\", axis=alt.Axis(format=\"%b %Y\", labelAngle=-45)),\n        y=alt.Y(\n            \"value:Q\",\n            title=\"Streaming Hours (millions)\",\n            stack=\"center\",  # Center baseline for streamgraph aesthetic\n            axis=alt.Axis(labels=False, ticks=False),  # Hide y-axis labels for streamgraph aesthetic\n        ),\n        color=alt.Color(\n            \"category:N\",\n            title=\"Genre\",\n            scale=alt.Scale(domain=genres, range=IMPRINT_PALETTE),\n            legend=alt.Legend(orient=\"right\"),\n        ),\n        order=alt.Order(\"category:N\"),\n        tooltip=[\"time:T\", \"category:N\", alt.Tooltip(\"value:Q\", format=\".1f\")],\n    )\n    .properties(\n        width=620, height=320, background=PAGE_BG, title=alt.Title(title, fontSize=16, anchor=\"middle\", color=INK)\n    )\n    .configure_view(continuousWidth=620, continuousHeight=320, fill=PAGE_BG, strokeWidth=0)\n    .configure_axis(\n        grid=False,\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        labelFontSize=10,\n        titleFontSize=12,\n    )\n    .configure_legend(\n        fillColor=ELEVATED_BG,\n        strokeColor=INK_SOFT,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        labelFontSize=10,\n        titleFontSize=10,\n    )\n)\n\n# Save as PNG, then pad to the exact canonical canvas (3200x1800)\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\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\n# Save interactive HTML version\nchart.interactive().save(f\"plot-{THEME}.html\")\n"}