{"spec_id":"wordcloud-basic","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nwordcloud-basic: Basic Word Cloud\nLibrary: bokeh 3.9.2 | Python 3.13.14\nQuality: 93/100 | Updated: 2026-08-04\n\"\"\"\n\nimport os\nimport time\nfrom pathlib import Path\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, HoverTool, LabelSet\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\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\n# Color encodes topical category (a second, independent dimension) rather than\n# frequency, which is already shown by size - a size+color double-encoding of\n# the same variable was flagged as redundant in review. Each category gets one\n# fixed Imprint categorical color (see prompts/library/bokeh.md IMPRINT_PALETTE).\nCATEGORY_COLORS = {\n    \"ML & Algorithms\": \"#009E73\",\n    \"Data & Analytics\": \"#C475FD\",\n    \"Tools & Libraries\": \"#4467A3\",\n    \"Infra & Pipeline\": \"#BD8233\",\n}\n\n# Data: Technology terms with frequencies and topical category\nwords_data = [\n    (\"Python\", 100, \"Tools & Libraries\"),\n    (\"Data\", 95, \"Data & Analytics\"),\n    (\"Machine\", 92, \"ML & Algorithms\"),\n    (\"Learning\", 88, \"ML & Algorithms\"),\n    (\"Analytics\", 85, \"Data & Analytics\"),\n    (\"Visualization\", 82, \"Infra & Pipeline\"),\n    (\"Statistics\", 78, \"Data & Analytics\"),\n    (\"Algorithm\", 75, \"ML & Algorithms\"),\n    (\"Model\", 72, \"ML & Algorithms\"),\n    (\"Neural\", 70, \"ML & Algorithms\"),\n    (\"Network\", 68, \"ML & Algorithms\"),\n    (\"Cloud\", 65, \"Infra & Pipeline\"),\n    (\"API\", 62, \"Tools & Libraries\"),\n    (\"Framework\", 60, \"Tools & Libraries\"),\n    (\"Library\", 58, \"Tools & Libraries\"),\n    (\"Code\", 55, \"Tools & Libraries\"),\n    (\"Science\", 52, \"Data & Analytics\"),\n    (\"Analysis\", 50, \"Data & Analytics\"),\n    (\"Deep\", 48, \"ML & Algorithms\"),\n    (\"Tensor\", 46, \"ML & Algorithms\"),\n    (\"Deploy\", 44, \"Infra & Pipeline\"),\n    (\"Pipeline\", 42, \"Infra & Pipeline\"),\n    (\"Training\", 40, \"Infra & Pipeline\"),\n    (\"Metrics\", 38, \"Data & Analytics\"),\n    (\"Dataset\", 36, \"Data & Analytics\"),\n    (\"Vector\", 34, \"ML & Algorithms\"),\n    (\"Graph\", 32, \"Infra & Pipeline\"),\n    (\"Batch\", 30, \"Infra & Pipeline\"),\n    (\"Query\", 28, \"Tools & Libraries\"),\n    (\"Cache\", 26, \"Tools & Libraries\"),\n    (\"Index\", 24, \"Tools & Libraries\"),\n    (\"Schema\", 22, \"Tools & Libraries\"),\n    (\"Token\", 20, \"Tools & Libraries\"),\n    (\"Epoch\", 18, \"Infra & Pipeline\"),\n    (\"Layer\", 16, \"Infra & Pipeline\"),\n    (\"Cluster\", 14, \"Infra & Pipeline\"),\n    (\"Stream\", 12, \"Infra & Pipeline\"),\n    (\"Config\", 10, \"Tools & Libraries\"),\n]\n\ncanvas_width = 3200\ncanvas_height = 1800\n\nmin_freq = min(f for _, f, _ in words_data)\nmax_freq = max(f for _, f, _ in words_data)\nmin_size, max_size = 42, 150\n\nrotations = [0, 0, 90, -90]\n\nwords = []\nx_pos = []\ny_pos = []\nsizes = []\ncolors = []\nangles = []\nfrequencies = []\ncategories = []\nplaced_boxes = []\n\nfor i, (word, freq, category) in enumerate(words_data):\n    # Sub-linear (power 0.6) curve on the normalized frequency raises the\n    # size floor for the lowest-frequency words so they stay legible once\n    # downscaled to a gallery thumbnail, while the highest-frequency words\n    # still top out at max_size.\n    freq_normalized = (freq - min_freq) / (max_freq - min_freq)\n    size = int(min_size + freq_normalized**0.6 * (max_size - min_size))\n\n    if i < 5:\n        angle_deg = 0\n    else:\n        angle_deg = rotations[i % len(rotations)]\n    angle_rad = np.radians(angle_deg)\n\n    # bokeh text_font_size is set in CSS pt; headless Chrome renders pt at\n    # 1pt ~= 1.333 source-px, so the pt value must be converted to px before\n    # it's usable as a collision-box dimension.\n    px_size = size * 1.333\n    base_width = len(word) * px_size * 0.58\n    base_height = px_size * 1.2\n    if angle_deg != 0:\n        word_width = base_height\n        word_height = base_width\n    else:\n        word_width = base_width\n        word_height = base_height\n\n    cx, cy = canvas_width / 2, canvas_height / 2\n    spiral_angle = 0\n    radius = 0\n    padding = 7\n    found_x, found_y = cx, cy\n    found_box = (cx - word_width / 2, cy - word_height / 2, word_width, word_height)\n\n    for _ in range(25000):\n        test_x = cx + radius * 2.0 * np.cos(spiral_angle) - word_width / 2\n        test_y = cy + radius * np.sin(spiral_angle) - word_height / 2\n\n        margin_x = 20\n        margin_y = 35\n        if (\n            margin_x < test_x < canvas_width - word_width - margin_x\n            and margin_y < test_y < canvas_height - word_height - margin_y\n        ):\n            test_box = (test_x, test_y, word_width, word_height)\n\n            overlap = False\n            for pb in placed_boxes:\n                px, py, pw, ph = pb\n                if not (\n                    test_x + word_width + padding < px\n                    or px + pw + padding < test_x\n                    or test_y + word_height + padding < py\n                    or py + ph + padding < test_y\n                ):\n                    overlap = True\n                    break\n\n            if not overlap:\n                found_x = test_x + word_width / 2\n                found_y = test_y + word_height / 2\n                found_box = test_box\n                break\n\n        spiral_angle += 0.08\n        radius += 0.8\n\n    placed_boxes.append(found_box)\n    words.append(word)\n    x_pos.append(found_x)\n    y_pos.append(found_y)\n    sizes.append(size)\n    angles.append(angle_rad)\n    frequencies.append(freq)\n    categories.append(category)\n    colors.append(CATEGORY_COLORS[category])\n\np = figure(\n    width=canvas_width,\n    height=canvas_height,\n    title=\"wordcloud-basic · bokeh · anyplot.ai\",\n    x_range=(0, canvas_width),\n    y_range=(0, canvas_height),\n    tools=\"hover\",\n    toolbar_location=None,\n)\n\nhit_sizes = [s * 0.8 for s in sizes]\n\nsource = ColumnDataSource(\n    data={\n        \"x\": x_pos,\n        \"y\": y_pos,\n        \"text\": words,\n        \"size\": sizes,\n        \"hit_size\": hit_sizes,\n        \"color\": colors,\n        \"angle\": angles,\n        \"frequency\": frequencies,\n        \"category\": categories,\n    }\n)\n\np.scatter(x=\"x\", y=\"y\", size=\"hit_size\", source=source, fill_alpha=0, line_alpha=0)\n\nhover = p.select_one(HoverTool)\nhover.tooltips = [(\"Word\", \"@text\"), (\"Frequency\", \"@frequency\"), (\"Category\", \"@category\")]\nhover.mode = \"mouse\"\n\nsource.data[\"size\"] = [f\"{s}pt\" for s in sizes]\n\nlabels = LabelSet(\n    x=\"x\",\n    y=\"y\",\n    text=\"text\",\n    text_font_size=\"size\",\n    text_color=\"color\",\n    text_align=\"center\",\n    text_baseline=\"middle\",\n    text_font_style=\"bold\",\n    angle=\"angle\",\n    source=source,\n)\np.add_layout(labels)\n\np.axis.visible = False\np.grid.visible = False\np.outline_line_color = None\n\np.title.text_font_size = \"50pt\"\np.title.text_color = INK\np.title.align = \"center\"\n\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\n\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\nW, H = canvas_width, canvas_height\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()}\")\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"}