{"spec_id":"lollipop-basic","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nlollipop-basic: Basic Lollipop Chart\nLibrary: bokeh 3.9.1 | Python 3.13.14\nQuality: 91/100 | Updated: 2026-07-01\n\"\"\"\n\nimport sys\n\n\n# This file is named bokeh.py; without this fix Python would import itself\n# instead of the installed bokeh package when run directly.\nif sys.path and sys.path[0] not in (\"\", None):\n    sys.path.append(sys.path.pop(0))\n\nimport io\nimport os\nimport time\nfrom pathlib import Path\n\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, Label, NumeralTickFormatter, Range1d, Span\nfrom bokeh.plotting import figure\nfrom PIL import Image\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\n# Imprint palette + theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\nBRAND = \"#009E73\"  # Imprint palette position 1 — always first series\n\n# Data — monthly cloud infrastructure spend by service category (sorted descending)\ncategories = [\n    \"Compute\",\n    \"Storage\",\n    \"Networking\",\n    \"Database\",\n    \"Security\",\n    \"Analytics\",\n    \"AI / ML\",\n    \"IoT\",\n    \"DevOps\",\n    \"Monitoring\",\n]\nvalues = [94200, 67500, 48300, 38800, 28400, 22100, 18700, 12900, 9600, 6800]\navg_spend = sum(values) / len(values)\n\nlabels = [f\"${v // 1000}K\" for v in values]\n\nsource = ColumnDataSource(\n    data={\n        \"categories\": categories,\n        \"values\": values,\n        \"zeros\": [0] * len(values),\n        \"labels\": labels,\n        \"label_y\": [v + 2800 for v in values],\n    }\n)\n\n# Figure\np = figure(\n    width=3200,\n    height=1800,\n    x_range=categories,\n    y_range=Range1d(0, 108000),\n    title=\"lollipop-basic · python · bokeh · anyplot.ai\",\n    x_axis_label=\"Cloud Service\",\n    y_axis_label=\"Monthly Spend (USD)\",\n    toolbar_location=None,\n    min_border_bottom=160,\n    min_border_left=180,\n    min_border_top=110,\n    min_border_right=80,\n)\n\n# Average reference line + label\np.add_layout(Span(location=avg_spend, dimension=\"width\", line_color=INK_MUTED, line_width=3, line_dash=\"dashed\"))\np.add_layout(\n    Label(\n        x=2880,\n        y=avg_spend,  # screen x from plot-frame left; plot area = 3200-180-80=2940px wide\n        x_units=\"screen\",\n        text=f\"Avg: ${avg_spend / 1000:.1f}K\",\n        text_font_size=\"22pt\",\n        text_color=INK_MUTED,\n        text_align=\"right\",\n        text_baseline=\"bottom\",\n        y_offset=8,\n    )\n)\n\n# Stems\np.segment(x0=\"categories\", y0=\"zeros\", x1=\"categories\", y1=\"values\", source=source, line_width=4, color=BRAND)\n\n# Markers\np.scatter(x=\"categories\", y=\"values\", source=source, size=42, color=BRAND, line_color=PAGE_BG, line_width=3)\n\n# Value labels above each marker\np.text(\n    x=\"categories\",\n    y=\"label_y\",\n    text=\"labels\",\n    source=source,\n    text_font_size=\"24pt\",\n    text_align=\"center\",\n    text_baseline=\"bottom\",\n    text_color=INK_SOFT,\n)\n\n# Title\np.title.text_font_size = \"50pt\"\np.title.text_color = INK\np.title.text_font_style = \"bold\"\n\n# Axis label sizes\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# Axis label colors\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\n\n# Axis line and tick colors\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.xaxis.minor_tick_line_color = None\np.yaxis.minor_tick_line_color = None\np.xaxis.major_label_orientation = 0.5\n\n# Grid — y-axis only, subtle\np.xgrid.grid_line_color = None\np.ygrid.grid_line_color = INK\np.ygrid.grid_line_alpha = 0.10\n\n# Background and chrome\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = None  # L-spine only: x/y axis_line_color handle bottom+left\n\n# Y-axis tick format\np.yaxis.formatter = NumeralTickFormatter(format=\"$0,0\")\n\n# Save HTML (interactive catalog artifact)\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Screenshot with headless Chrome — Selenium 4 / Selenium Manager resolves the driver.\n# Chrome's headless viewport is ~143px shorter than --window-size due to internal\n# overhead; use an oversized window then crop to the exact canvas target.\nW, H = 3200, 1800\nCHROME_H = H + 200\nopts = Options()\nfor arg in (\n    \"--headless=new\",\n    \"--no-sandbox\",\n    \"--disable-dev-shm-usage\",\n    \"--disable-gpu\",\n    f\"--window-size={W},{CHROME_H}\",\n    \"--hide-scrollbars\",\n):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\ndriver.set_window_size(W, CHROME_H)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\n# The bokeh figure starts at (0, 0); crop screenshot to exact canvas dimensions\nImage.open(io.BytesIO(driver.get_screenshot_as_png())).crop((0, 0, W, H)).save(f\"plot-{THEME}.png\")\ndriver.quit()\n"}