{"spec_id":"pictogram-basic","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\npictogram-basic: Pictogram Chart (Isotype Visualization)\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 92/100 | Updated: 2026-06-03\n\"\"\"\n\nimport base64\nimport os\nimport sys\nimport time\nfrom pathlib import Path\n\n\n# Prevent this file (bokeh.py) from shadowing the real bokeh package\n_script_dir = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if os.path.abspath(p) != _script_dir]\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import (\n    BoxAnnotation,\n    ColumnDataSource,\n    CustomJSTickFormatter,\n    FixedTicker,\n    HoverTool,\n    Label,\n    Range1d,\n    Title,\n)\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\n# Theme tokens — Imprint palette, see prompts/default-style-guide.md\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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint categorical palette — canonical order, position 1 always first\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\"]\n\n# Data — fruit production (thousands of tonnes), icon = 5k tonnes\ncategories = [\"Apples\", \"Oranges\", \"Bananas\", \"Grapes\", \"Mangoes\"]\nvalues = [35, 23, 17, 28, 11]\nicon_value = 5\n\n# Sort by value descending, assign Imprint colors in ordinal rank order\nsorted_data = sorted(zip(categories, values, strict=True), key=lambda x: x[1], reverse=True)\ncategories = [d[0] for d in sorted_data]\nvalues = [d[1] for d in sorted_data]\ncolors = IMPRINT_PALETTE[: len(categories)]\n\n# Build icon grid positions\nfull_x, full_y, full_c, full_cat, full_val = [], [], [], [], []\nbg_x, bg_y = [], []\nwedge_x, wedge_y, wedge_c, wedge_end = [], [], [], []\nn_cats = len(categories)\nradius = 0.40\n\nfor i, (cat, val, color) in enumerate(zip(categories, values, colors, strict=True)):\n    n_full = int(val // icon_value)\n    remainder = val % icon_value\n    fraction = remainder / icon_value\n    y = n_cats - 1 - i\n\n    for j in range(n_full):\n        full_x.append(j)\n        full_y.append(y)\n        full_c.append(color)\n        full_cat.append(cat)\n        full_val.append(f\"{val}k tonnes\")\n\n    if fraction > 0:\n        bg_x.append(n_full)\n        bg_y.append(y)\n        wedge_x.append(n_full)\n        wedge_y.append(y)\n        wedge_c.append(color)\n        wedge_end.append(np.pi / 2 - fraction * 2 * np.pi)\n\n# Figure — canonical landscape 3200×1800, toolbar_location=None is mandatory\nmax_icons = max(int(np.ceil(v / icon_value)) for v in values)\n\nTITLE = \"pictogram-basic · python · bokeh · anyplot.ai\"\ntitle_n = len(TITLE)\ntitle_fs = f\"{round(50 * min(1.0, 67 / title_n))}pt\"\n\np = figure(\n    width=3200,\n    height=1800,\n    x_range=Range1d(-0.65, max_icons + 0.65),\n    y_range=Range1d(-0.85, n_cats - 0.15),\n    toolbar_location=None,\n    title=TITLE,\n    min_border_left=240,\n    min_border_bottom=120,\n    min_border_top=110,\n    min_border_right=230,\n)\n\n# Subtitle\nsubtitle = Title(\n    text=\"Fruit Production — Annual output in thousands of tonnes\",\n    text_font_size=\"28pt\",\n    text_color=INK_SOFT,\n    align=\"center\",\n)\np.add_layout(subtitle, \"above\")\n\n# Alternating row background bands for readability\nfor i in range(n_cats):\n    if i % 2 == 0:\n        band = BoxAnnotation(bottom=i - 0.48, top=i + 0.48, fill_color=ELEVATED_BG, fill_alpha=0.6, level=\"underlay\")\n        p.add_layout(band)\n\n# Full icons — circles with page-background stroke for definition\nfull_source = ColumnDataSource(\n    data={\"x\": full_x, \"y\": full_y, \"color\": full_c, \"category\": full_cat, \"value\": full_val}\n)\ncircles = p.circle(\n    x=\"x\", y=\"y\", radius=radius, source=full_source, color=\"color\", alpha=0.90, line_color=PAGE_BG, line_width=3\n)\n\n# Interactive hover tooltip (Bokeh HTML output feature)\nhover = HoverTool(renderers=[circles], tooltips=[(\"Category\", \"@category\"), (\"Production\", \"@value\")])\np.add_tools(hover)\n\n# Partial icon backgrounds (muted ghost circles)\nif bg_x:\n    bg_source = ColumnDataSource(data={\"x\": bg_x, \"y\": bg_y})\n    p.circle(\n        x=\"x\", y=\"y\", radius=radius, source=bg_source, color=INK_SOFT, alpha=0.18, line_color=PAGE_BG, line_width=3\n    )\n\n    # Partial icon wedge fills\n    wedge_source = ColumnDataSource(data={\"x\": wedge_x, \"y\": wedge_y, \"color\": wedge_c, \"end_angle\": wedge_end})\n    p.wedge(\n        x=\"x\",\n        y=\"y\",\n        radius=radius,\n        start_angle=np.pi / 2,\n        end_angle=\"end_angle\",\n        direction=\"clock\",\n        source=wedge_source,\n        color=\"color\",\n        alpha=0.90,\n        line_color=PAGE_BG,\n        line_width=3,\n    )\n\n# Value labels on the right of each row\nfor i, (_cat, val) in enumerate(zip(categories, values, strict=True)):\n    y = n_cats - 1 - i\n    n_icons = int(np.ceil(val / icon_value))\n    val_label = Label(\n        x=n_icons + 0.18,\n        y=y,\n        text=f\"{val:,}k\",\n        text_font_size=\"28pt\",\n        text_font_style=\"bold\" if i == 0 else \"normal\",\n        text_color=INK if i == 0 else INK_SOFT,\n        text_baseline=\"middle\",\n        text_align=\"left\",\n        x_units=\"data\",\n        y_units=\"data\",\n    )\n    p.add_layout(val_label)\n\n# Legend key\nlegend_label = Label(\n    x=0,\n    y=-0.62,\n    text=\"Each ● = 5,000 tonnes\",\n    text_font_size=\"26pt\",\n    text_color=INK_MUTED,\n    x_units=\"data\",\n    y_units=\"data\",\n)\np.add_layout(legend_label)\n\n# Title styling\np.title.text_font_size = title_fs\np.title.align = \"center\"\np.title.text_color = INK\n\n# Y-axis: category labels\ncat_labels = [categories[n_cats - 1 - i] for i in range(n_cats)]\np.yaxis.ticker = FixedTicker(ticks=list(range(n_cats)))\np.yaxis.formatter = CustomJSTickFormatter(args={\"labels\": cat_labels}, code=\"return labels[tick] || '';\")\np.yaxis.major_label_text_font_size = \"34pt\"\np.yaxis.major_label_text_font_style = \"bold\"\np.yaxis.major_label_text_color = INK\n\n# X-axis: hidden (icon count readable from row widths)\np.xaxis.visible = False\n\n# Remove spines and grid — clean isotype layout\np.outline_line_color = None\np.grid.grid_line_color = None\np.xaxis.axis_line_color = None\np.yaxis.axis_line_color = None\np.yaxis.major_tick_line_color = None\np.yaxis.minor_tick_line_color = None\n\n# Theme-adaptive background\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\n\n# Save interactive HTML, then screenshot with Selenium (export_png unavailable on this runner)\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\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)\ndriver = webdriver.Chrome(options=opts)\ndriver.set_window_size(W, H)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\n\n# Use CDP clip to get exactly W×H pixels regardless of Chrome viewport overhead\npng_data = driver.execute_cdp_cmd(\n    \"Page.captureScreenshot\", {\"format\": \"png\", \"clip\": {\"x\": 0, \"y\": 0, \"width\": W, \"height\": H, \"scale\": 1}}\n)\nwith open(f\"plot-{THEME}.png\", \"wb\") as f:\n    f.write(base64.b64decode(png_data[\"data\"]))\ndriver.quit()\n"}