{"spec_id":"wordcloud-basic","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nwordcloud-basic: Basic Word Cloud\nLibrary: letsplot 4.11.0 | Python 3.13.14\nQuality: 85/100 | Updated: 2026-08-04\n\"\"\"\n\nimport math\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import (\n    LetsPlot,\n    aes,\n    element_blank,\n    element_rect,\n    element_text,\n    geom_text,\n    ggplot,\n    ggsize,\n    labs,\n    layer_tooltips,\n    scale_alpha_identity,\n    scale_color_manual,\n    scale_size_identity,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n    theme_void,\n)\nfrom lets_plot.export import ggsave\n\n\nLetsPlot.setup_html()\n\n# 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\"\n\n# Imprint palette (first series always #009E73)\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n\n# Data - Programming language popularity\nnp.random.seed(42)\nwords_data = {\n    \"Python\": 100,\n    \"JavaScript\": 92,\n    \"Java\": 85,\n    \"TypeScript\": 78,\n    \"SQL\": 75,\n    \"HTML\": 70,\n    \"CSS\": 68,\n    \"Rust\": 62,\n    \"Go\": 58,\n    \"C++\": 55,\n    \"Kotlin\": 52,\n    \"Swift\": 50,\n    \"Shell\": 48,\n    \"Ruby\": 45,\n    \"PHP\": 42,\n    \"Scala\": 38,\n    \"R\": 35,\n    \"Perl\": 32,\n    \"Dart\": 30,\n    \"Julia\": 28,\n    \"MATLAB\": 26,\n    \"Haskell\": 24,\n    \"Lua\": 22,\n    \"Clojure\": 20,\n    \"Elixir\": 18,\n    \"GraphQL\": 35,\n}\n\nwords = list(words_data.keys())\nfrequencies = list(words_data.values())\n\n# Sort by frequency (largest first for better placement)\nsorted_indices = np.argsort(frequencies)[::-1]\nwords = [words[i] for i in sorted_indices]\nfrequencies = [frequencies[i] for i in sorted_indices]\n\n# Canvas dimensions (data-space, independent of the exported pixel size) -\n# square domain so the naturally circular/blob-shaped spiral fills the frame\n# evenly on all sides (landscape left large empty side margins)\ncanvas_width = 150\ncanvas_height = 150\n\n# Scale font sizes for readability (mm, geom_text units) - floor raised so the\n# smallest-frequency words stay legible once scaled down to mobile widths\nmin_freq, max_freq = min(frequencies), max(frequencies)\nmin_size, max_size = 6.5, 14\n\nsizes = []\nfor freq in frequencies:\n    normalized = (freq - min_freq) / (max_freq - min_freq)\n    size = min_size + (normalized**0.6) * (max_size - min_size)\n    sizes.append(size)\n\n# Fade lower-frequency words slightly so the eye lands on the dominant terms first\nalphas = [0.55 + 0.45 * ((freq - min_freq) / (max_freq - min_freq)) for freq in frequencies]\n\n# Vertical rotation for a subset of words (skip the top 3 focal terms) - a\n# lets-plot geom_text `angle` aesthetic, the classic word-cloud variety cue\nangles = [90 if (i >= 3 and i % 5 == 2) else 0 for i in range(len(words))]\n\n# Spiral word placement with collision detection (rotation-aware bounding box)\nplaced = []\npositions_x = []\npositions_y = []\nchar_width_ratio = 0.62\n\nfor word, size, angle in zip(words, sizes, angles, strict=True):\n    raw_width = len(word) * size * char_width_ratio\n    raw_height = size * 1.05\n    word_width, word_height = (raw_height, raw_width) if angle == 90 else (raw_width, raw_height)\n\n    t = 0\n    step = 0.08\n    max_iterations = 4000\n    placed_word = False\n\n    while t < max_iterations and not placed_word:\n        r = 0.1 + t * 0.08\n        theta = t * 0.35\n        x = canvas_width / 2 + r * math.cos(theta) * 0.95\n        y = canvas_height / 2 + r * math.sin(theta)\n\n        margin = 3\n        if (\n            x - word_width / 2 < margin\n            or x + word_width / 2 > canvas_width - margin\n            or y - word_height / 2 < margin\n            or y + word_height / 2 > canvas_height - margin\n        ):\n            t += step\n            continue\n\n        collision = False\n        padding = 1.6\n        for px, py, pw, ph in placed:\n            if abs(x - px) < (word_width / 2 + pw / 2 + padding) and abs(y - py) < (word_height / 2 + ph / 2 + padding):\n                collision = True\n                break\n\n        if not collision:\n            placed.append((x, y, word_width, word_height))\n            positions_x.append(x)\n            positions_y.append(y)\n            placed_word = True\n        else:\n            t += step\n\n    if not placed_word:\n        positions_x.append(None)\n        positions_y.append(None)\n\n# Build dataframe with placed words\ndf_data = []\nfor word, freq, size, angle, alpha, x, y in zip(\n    words, frequencies, sizes, angles, alphas, positions_x, positions_y, strict=True\n):\n    if x is not None and y is not None:\n        df_data.append({\"word\": word, \"frequency\": freq, \"size\": size, \"angle\": angle, \"alpha\": alpha, \"x\": x, \"y\": y})\n\ndf = pd.DataFrame(df_data)\n\n# Assign Imprint colors to words\ndf[\"color\"] = [IMPRINT[i % len(IMPRINT)] for i in range(len(df))]\n\n# Plot with theme-adaptive chrome\nanyplot_theme = theme(\n    plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    plot_title=element_text(size=16, color=INK, hjust=0.5),\n    legend_position=\"none\",\n    axis_title=element_blank(),\n    axis_text=element_blank(),\n)\n\nword_tooltips = layer_tooltips().title(\"@word\").line(\"Frequency|@frequency\")\n\nplot = (\n    ggplot(df, aes(x=\"x\", y=\"y\", label=\"word\", size=\"size\", color=\"color\", angle=\"angle\", alpha=\"alpha\"))\n    + geom_text(fontface=\"bold\", tooltips=word_tooltips)\n    + scale_size_identity()\n    + scale_alpha_identity()\n    + scale_color_manual(values=df[\"color\"].unique(), guide=\"none\")\n    + scale_x_continuous(limits=(0, canvas_width), expand=[0, 0])\n    + scale_y_continuous(limits=(0, canvas_height), expand=[0, 0])\n    + labs(title=\"wordcloud-basic · letsplot · anyplot.ai\")\n    + theme_void()\n    + anyplot_theme\n    + ggsize(600, 600)\n)\n\n# Save\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}