{"spec_id":"streamgraph-basic","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nstreamgraph-basic: Basic Stream Graph\nLibrary: bokeh 3.9.2 | Python 3.13.14\nQuality: 89/100 | Updated: 2026-08-05\n\"\"\"\n\nimport os\nimport time\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, HoverTool, Label, Legend, Range1d\nfrom bokeh.plotting import figure\nfrom scipy.interpolate import PchipInterpolator\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\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\"\n\n# Imprint palette — first series always #009E73\nCOLORS = [\"#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=\"ME\")\ncategories = [\"Pop\", \"Rock\", \"Hip-Hop\", \"Electronic\", \"Jazz\", \"Classical\"]\n\nn_points = len(months)\nbase = np.linspace(0, 4 * np.pi, n_points)\n\nraw = {\n    \"Pop\": 45 + 18 * np.sin(base) + np.random.randn(n_points) * 3,\n    \"Rock\": 38 + 12 * np.sin(base + 0.8) + np.random.randn(n_points) * 2.5,\n    \"Hip-Hop\": 35 + 22 * np.sin(base + 1.6) + np.random.randn(n_points) * 4,\n    \"Electronic\": 28 + 14 * np.sin(base + 2.4) + np.random.randn(n_points) * 2.5,\n    \"Jazz\": 18 + 10 * np.sin(base + 3.2) + np.random.randn(n_points) * 2,\n    \"Classical\": 14 + 6 * np.sin(base + 4.0) + np.random.randn(n_points) * 1.5,\n}\n\nfor cat in categories:\n    raw[cat] = np.maximum(raw[cat], 5)\n\ndf = pd.DataFrame(raw)\n\n# Symmetric baseline — center the stack around zero\nvalues = df[categories].values\ntotal = values.sum(axis=1)\nbaseline_offset = total / 2\n\ny_bottom = np.zeros_like(values)\ny_top = np.zeros_like(values)\nfor i in range(len(categories)):\n    if i == 0:\n        y_bottom[:, i] = -baseline_offset\n        y_top[:, i] = y_bottom[:, i] + values[:, i]\n    else:\n        y_bottom[:, i] = y_top[:, i - 1]\n        y_top[:, i] = y_bottom[:, i] + values[:, i]\n\n# Smooth interpolation for flowing curves.\n# PCHIP (monotone cubic Hermite) is shape-preserving and does not overshoot at\n# the series edges the way a high-degree polynomial fit (Runge phenomenon) does.\nx_numeric = np.arange(n_points)\nn_smooth = n_points * 10\nx_smooth = np.linspace(0, n_points - 1, n_smooth)\n\nmonths_smooth = pd.date_range(start=months.min(), end=months.max(), periods=n_smooth)\ny_bottom_smooth = np.zeros((n_smooth, len(categories)))\ny_top_smooth = np.zeros((n_smooth, len(categories)))\n\nfor i in range(len(categories)):\n    y_bottom_smooth[:, i] = PchipInterpolator(x_numeric, y_bottom[:, i])(x_smooth)\n    y_top_smooth[:, i] = PchipInterpolator(x_numeric, y_top[:, i])(x_smooth)\n\n# Extra headroom above/below the widest point of the stack so the bands don't\n# crowd the top/bottom plot edges.\nmax_disp = np.max(baseline_offset)\ny_limit = max_disp * 1.3\n\n# Plot\np = figure(\n    width=3200,\n    height=1800,\n    title=\"streamgraph-basic · python · bokeh · anyplot.ai\",\n    x_axis_label=\"Month\",\n    y_axis_label=\"Streaming Hours (relative)\",\n    x_axis_type=\"datetime\",\n    toolbar_location=None,\n    min_border_bottom=160,\n    min_border_left=180,\n    min_border_top=110,\n    min_border_right=50,\n)\np.y_range = Range1d(start=-y_limit, end=y_limit)\n\n# Font sizes for 3200×1800 px canvas\np.title.text_font_size = \"50pt\"\np.title.text_color = INK\np.xaxis.axis_label_text_font_size = \"42pt\"\np.yaxis.axis_label_text_font_size = \"42pt\"\np.xaxis.major_label_text_font_size = \"34pt\"\np.yaxis.major_label_text_font_size = \"34pt\"\n\n# Theme-adaptive chrome\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = None\np.xaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_color = INK\np.xaxis.major_label_text_color = INK_SOFT\np.yaxis.major_label_text_color = INK_SOFT\np.xaxis.axis_line_color = INK_SOFT\np.yaxis.axis_line_color = INK_SOFT\np.xaxis.major_tick_line_color = INK_SOFT\np.yaxis.major_tick_line_color = INK_SOFT\np.xgrid.grid_line_color = INK\np.ygrid.grid_line_color = INK\np.xgrid.grid_line_alpha = 0.10\np.ygrid.grid_line_alpha = 0.10\n\n# Draw streamgraph patches\nx_values = months_smooth.values\nlegend_items = []\nhover_renderers = []\n\nfor i, cat in enumerate(categories):\n    xs = np.concatenate([x_values, x_values[::-1]])\n    ys = np.concatenate([y_top_smooth[:, i], y_bottom_smooth[:, i][::-1]])\n\n    source = ColumnDataSource(data={\"x\": xs, \"y\": ys, \"genre\": [cat] * len(xs)})\n    renderer = p.patch(\n        x=\"x\", y=\"y\", source=source, fill_color=COLORS[i], fill_alpha=0.85, line_color=PAGE_BG, line_width=1\n    )\n    legend_items.append((cat, [renderer]))\n    hover_renderers.append(renderer)\n\n# HoverTool — shows genre name on hover\nhover = HoverTool(renderers=hover_renderers, tooltips=[(\"Genre\", \"@genre\")])\np.add_tools(hover)\n\n# Legend outside the plot area\nlegend = Legend(items=legend_items, location=\"center\")\nlegend.label_text_font_size = \"34pt\"\nlegend.label_text_color = INK_SOFT\nlegend.glyph_height = 44\nlegend.glyph_width = 44\nlegend.spacing = 15\nlegend.background_fill_color = ELEVATED_BG\nlegend.border_line_color = INK_SOFT\np.add_layout(legend, \"right\")\n\n# Focal-point callout on the genre with the highest peak streaming month —\n# gives the chart a storytelling anchor instead of leaving all six bands\n# equally weighted.\npeak_genre = \"Pop\"\npeak_idx = int(np.argmax(raw[peak_genre]))\npeak_x = months[peak_idx]\npeak_y = (y_top[peak_idx, 0] + y_bottom[peak_idx, 0]) / 2\n\npeak_marker_source = ColumnDataSource(data={\"x\": [peak_x], \"y\": [peak_y]})\np.scatter(x=\"x\", y=\"y\", source=peak_marker_source, size=22, fill_color=COLORS[0], line_color=INK, line_width=3)\n\npeak_label = Label(\n    x=peak_x,\n    y=peak_y,\n    x_offset=40,\n    y_offset=70,\n    text=f\"{peak_genre} — peak streaming month\",\n    text_font_size=\"28pt\",\n    text_color=INK,\n    background_fill_color=ELEVATED_BG,\n    background_fill_alpha=0.95,\n    border_line_color=INK_SOFT,\n    border_line_width=1,\n)\np.add_layout(peak_label)\n\n# Save interactive HTML\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Screenshot with headless Chrome (export_png unavailable in this environment)\nW, H = 3200, 1800\nopts = Options()\nfor arg in (\n    \"--headless=new\",\n    \"--no-sandbox\",\n    \"--disable-dev-shm-usage\",\n    \"--disable-gpu\",\n    f\"--window-size={W},{H}\",\n    \"--hide-scrollbars\",\n):\n    opts.add_argument(arg)\n\ndriver = webdriver.Chrome(options=opts)\ndriver.set_window_size(W, H)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ndriver.execute_cdp_cmd(\n    \"Emulation.setDeviceMetricsOverride\", {\"width\": W, \"height\": H, \"deviceScaleFactor\": 1, \"mobile\": False}\n)\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}