{"spec_id":"streamgraph-basic","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nstreamgraph-basic: Basic Stream Graph\nLibrary: letsplot 4.11.0 | Python 3.13.14\nQuality: 91/100 | Updated: 2026-08-05\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import *\nfrom lets_plot.export import ggsave\nfrom scipy.interpolate import make_interp_spline\n\n\nLetsPlot.setup_html()\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\"\nRULE = \"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\"]\n\n# Data — monthly streaming hours by music genre over two years\nnp.random.seed(42)\nn_months = 24\ngenres = [\"Pop\", \"Rock\", \"Hip-Hop\", \"Electronic\", \"Jazz\"]\nmonth_labels = pd.date_range(\"2023-01-01\", periods=n_months, freq=\"MS\").strftime(\"%b '%y\")\n\nraw_values = {}\nmonths_orig = np.arange(n_months, dtype=float)\nfor i, genre in enumerate(genres):\n    base = 100 + 50 * np.sin(np.linspace(0, 4 * np.pi, n_months) + i * 0.7)\n    trend = np.linspace(0, 25, n_months) * (1 if i % 2 == 0 else -0.6)\n    noise = np.random.randn(n_months) * 8\n    raw_values[genre] = np.clip(base + trend + noise, 25, None)\n\n# Smooth each series with a cubic spline for flowing curves\nn_interp = n_months * 8\nmonths_smooth = np.linspace(0, n_months - 1, n_interp)\nvalues_smooth = {}\nfor genre in genres:\n    spline = make_interp_spline(months_orig, raw_values[genre], k=3)\n    values_smooth[genre] = np.clip(spline(months_smooth), 10, None)\n\n# Compute streamgraph positions (symmetric baseline around zero)\nvalues_matrix = np.array([values_smooth[g] for g in genres])\ntotal_per_point = values_matrix.sum(axis=0)\nbaseline_offset = -total_per_point / 2\n\ndata = []\nfor t_idx, t in enumerate(months_smooth):\n    cumulative = baseline_offset[t_idx]\n    month_lbl = month_labels[int(round(t))]\n    for genre_idx, genre in enumerate(genres):\n        value = values_matrix[genre_idx, t_idx]\n        ymin = cumulative\n        ymax = cumulative + value\n        data.append({\"month\": t, \"genre\": genre, \"ymin\": ymin, \"ymax\": ymax, \"value\": value, \"month_label\": month_lbl})\n        cumulative = ymax\n\ndf = pd.DataFrame(data)\n\n# Focal point: the single month/genre combination with the highest streaming\n# volume, called out with a label — gives the reader a concrete entry point\n# into the data instead of five equally-weighted ribbons (DE-03)\npeak = df.loc[df[\"value\"].idxmax()]\npeak_y = (peak[\"ymin\"] + peak[\"ymax\"]) / 2\nrange_all = df[\"ymax\"].max() - df[\"ymin\"].min()\nlabel_direction = 1 if peak_y >= 0 else -1\npeak_df = pd.DataFrame(\n    {\n        \"month\": [peak[\"month\"]],\n        \"y\": [peak_y],\n        \"label_y\": [peak_y + label_direction * range_all * 0.16],\n        \"label\": [f\"{peak['genre']} peaks at {peak['value']:.0f} hrs — {peak['month_label']}\"],\n        \"fill\": [IMPRINT[genres.index(peak[\"genre\"])]],\n    }\n)\n\n# Plot\nanyplot_theme = theme(\n    plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    panel_background=element_rect(fill=PAGE_BG),\n    panel_grid_major_x=element_line(color=RULE, size=0.4, linetype=\"dashed\"),\n    panel_grid_major_y=element_blank(),\n    panel_grid_minor=element_blank(),\n    axis_title=element_text(color=INK, size=12),\n    axis_text=element_text(color=INK_SOFT, size=10),\n    axis_text_y=element_blank(),\n    axis_ticks_y=element_blank(),\n    axis_line=element_line(color=INK_SOFT),\n    plot_title=element_text(color=INK, size=16, face=\"bold\"),\n    plot_subtitle=element_text(color=INK_SOFT, size=11),\n    legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n    legend_text=element_text(color=INK_SOFT, size=10),\n    legend_title=element_text(color=INK, size=11),\n)\n\n# lets-plot-distinctive touch: custom interactive tooltips on hover (HTML\n# export) surfacing genre / month / hours per ribbon slice — not something a\n# static grammar-of-graphics library (e.g. plotnine) can offer (LM-02)\ntooltips = (\n    layer_tooltips()\n    .format(\"@value\", \".0f\")\n    .line(\"Genre|@genre\")\n    .line(\"Month|@month_label\")\n    .line(\"Hours|@value\")\n)\n\nplot = (\n    ggplot(df, aes(x=\"month\", fill=\"genre\"))\n    + geom_ribbon(\n        aes(ymin=\"ymin\", ymax=\"ymax\"),\n        alpha=0.9,\n        color=PAGE_BG,\n        size=0.6,\n        tooltips=tooltips,\n    )\n    + geom_point(\n        aes(x=\"month\", y=\"y\"),\n        data=peak_df,\n        color=INK,\n        fill=peak_df[\"fill\"].iloc[0],\n        shape=21,\n        size=3.5,\n        inherit_aes=False,\n    )\n    + geom_segment(\n        aes(x=\"month\", y=\"y\", xend=\"month\", yend=\"label_y\"),\n        data=peak_df,\n        color=INK_SOFT,\n        size=0.4,\n        inherit_aes=False,\n    )\n    + geom_label(\n        aes(x=\"month\", y=\"label_y\", label=\"label\"),\n        data=peak_df,\n        color=INK,\n        fill=ELEVATED_BG,\n        size=3.2,\n        label_padding=0.4,\n        hjust=\"inward\",\n        inherit_aes=False,\n    )\n    + scale_fill_manual(values=IMPRINT)\n    + scale_x_continuous(\n        breaks=[0, 6, 12, 18, 23], labels=[\"Jan '23\", \"Jul '23\", \"Jan '24\", \"Jul '24\", \"Dec '24\"]\n    )\n    + labs(\n        x=\"Month\",\n        y=\"\",\n        fill=\"Genre\",\n        title=\"streamgraph-basic · letsplot · anyplot.ai\",\n        subtitle=\"Monthly streaming hours by genre, 2023-2024\",\n    )\n    + ggsize(800, 450)\n    + theme_minimal()\n    + anyplot_theme\n)\n\n# Save\nggsave(plot, filename=f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(plot, filename=f\"plot-{THEME}.html\", path=\".\")\n"}