{"spec_id":"streamgraph-basic","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\nstreamgraph-basic: Basic Stream Graph\nLibrary: plotly 6.9.0 | Python 3.13.14\nQuality: 91/100 | Updated: 2026-08-05\n\"\"\"\n\nimport sys\n\n\nsys.path.pop(0)\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nimport plotly.graph_objects as go\n\n\n# Theme tokens\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.15)\" if THEME == \"light\" else \"rgba(240,239,232,0.15)\"\n\n# Imprint palette — first series always #009E73\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\"]\n\n# Data - Monthly streaming hours by music genre over 2 years.\n# Each genre gets its own trend so the shape itself tells a story: Pop's\n# share visibly grows while Rock fades, Electronic pulses with summer\n# festival season, and Jazz/Classical stay steady — no annotations needed.\nnp.random.seed(42)\nmonths = pd.date_range(start=\"2022-01-01\", periods=24, freq=\"ME\")\ngenres = [\"Pop\", \"Rock\", \"Hip-Hop\", \"Electronic\", \"Jazz\", \"Classical\"]\n\ndata = {}\ndata[\"Pop\"] = 40 + np.linspace(0, 32, 24) + np.random.randn(24) * 3\ndata[\"Rock\"] = 38 - np.linspace(0, 16, 24) + np.random.randn(24) * 2.5\ndata[\"Hip-Hop\"] = 28 + np.linspace(0, 18, 24) + np.random.randn(24) * 2.5\ndata[\"Electronic\"] = 22 + 8 * np.sin(np.linspace(0, 4 * np.pi, 24) - np.pi / 2) + np.random.randn(24) * 2\ndata[\"Jazz\"] = 16 + np.random.randn(24) * 2\ndata[\"Classical\"] = 13 + np.random.randn(24) * 1.5\nfor genre in genres:\n    data[genre] = np.maximum(data[genre], 5)\n\ndf = pd.DataFrame(data, index=months)\nmonth_labels = months.strftime(\"%Y-%m\").tolist()\n\n# Calculate streamgraph layout (centered baseline)\nvalues_array = df.values.T  # Shape: (n_genres, n_time_points)\nn_genres, n_time = values_array.shape\n\ncumsum = np.vstack([np.zeros(n_time), np.cumsum(values_array, axis=0)])\ntotal = cumsum[-1]\noffset = total / 2\n\n# Plot\nfig = go.Figure()\n\nfor i, genre in enumerate(genres):\n    values = values_array[i]\n    y_lower = cumsum[i] - offset\n    y_upper = cumsum[i + 1] - offset\n    share = values / total * 100\n\n    x_fill = month_labels + month_labels[::-1]\n    y_fill = list(y_upper) + list(y_lower)[::-1]\n    customdata = np.stack([np.concatenate([values, values[::-1]]), np.concatenate([share, share[::-1]])], axis=-1)\n\n    if i == 0:\n        pop_y_upper = y_upper  # traced as a top-layer highlight after the loop\n\n    fig.add_trace(\n        go.Scatter(\n            x=x_fill,\n            y=y_fill,\n            fill=\"toself\",\n            fillcolor=IMPRINT[i],\n            opacity=1.0,\n            line={\"color\": IMPRINT[i], \"width\": 0.5, \"shape\": \"spline\", \"smoothing\": 1.0},\n            name=genre,\n            mode=\"none\",\n            customdata=customdata,\n            hovertemplate=f\"<b>{genre}</b> — %{{x}}<br>%{{customdata[0]:.0f}} hrs (%{{customdata[1]:.0f}}% share)<extra></extra>\",\n            hoveron=\"fills\",\n        )\n    )\n\n# Pop is the story's focal point: an ink-toned highlight line traces its crest\n# on top of every fill, giving it clear visual weight without dimming the\n# other streams (all keep full opacity — no subtle-opacity trick).\nfig.add_trace(\n    go.Scatter(\n        x=month_labels,\n        y=pop_y_upper,\n        mode=\"lines\",\n        line={\"color\": INK, \"width\": 2, \"shape\": \"spline\", \"smoothing\": 1.0},\n        showlegend=False,\n        hoverinfo=\"skip\",\n    )\n)\n\nsubtitle = f\"<span style='font-size:12px;color:{INK_SOFT}'>Monthly streaming hours by music genre, 2022–2023</span>\"\n\nfig.update_layout(\n    autosize=False,\n    width=800,\n    height=450,\n    title={\n        \"text\": f\"streamgraph-basic · python · plotly · anyplot.ai<br>{subtitle}\",\n        \"font\": {\"size\": 16, \"color\": INK},\n        \"x\": 0.5,\n        \"xanchor\": \"center\",\n    },\n    xaxis={\n        \"title\": {\"text\": \"Month\", \"font\": {\"size\": 12, \"color\": INK}},\n        \"tickfont\": {\"size\": 10, \"color\": INK_SOFT},\n        \"showgrid\": False,\n        \"showline\": True,\n        \"linecolor\": INK_SOFT,\n        \"mirror\": False,  # bottom spine only — no top spine\n        \"zeroline\": False,\n    },\n    yaxis={\n        \"showticklabels\": False,  # hide confusing centered-offset values\n        \"showgrid\": True,\n        \"gridcolor\": GRID,\n        \"gridwidth\": 1,\n        \"zeroline\": True,\n        \"zerolinecolor\": INK_SOFT,\n        \"zerolinewidth\": 1,\n        \"showline\": False,  # no left spine (y labels hidden anyway)\n        \"mirror\": False,\n    },\n    legend={\n        \"font\": {\"size\": 10, \"color\": INK_SOFT},\n        \"bgcolor\": ELEVATED_BG,\n        \"bordercolor\": INK_SOFT,\n        \"borderwidth\": 1,\n        \"orientation\": \"h\",\n        \"yanchor\": \"top\",\n        \"y\": -0.16,\n        \"xanchor\": \"center\",\n        \"x\": 0.5,\n    },\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAGE_BG,\n    hovermode=\"x unified\",\n    margin={\"l\": 60, \"r\": 40, \"t\": 90, \"b\": 100},\n)\n\n# Save\nfig.write_image(f\"plot-{THEME}.png\", width=800, height=450, scale=4)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}