{"spec_id":"wordcloud-basic","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nwordcloud-basic: Basic Word Cloud\nLibrary: pygal 3.1.3 | Python 3.13.14\nQuality: 77/100 | Updated: 2026-08-04\n\"\"\"\n\nimport os\nimport sys\nimport xml.etree.ElementTree as ET\n\n\n# Avoid naming conflict with pygal.py script name\n# Remove current directory from path temporarily\ncwd = os.getcwd()\nsys.path = [p for p in sys.path if p not in (\"\", \".\", cwd)]\n\nimport cairosvg\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\n\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\"\n\n# Imprint palette (first series = #009E73)\nIMPRINT = (\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\")\n\n# Data: DevOps tooling adoption survey - term frequencies from a platform-engineering survey\nword_frequencies = {\n    \"Kubernetes\": 195,\n    \"Docker\": 178,\n    \"Terraform\": 162,\n    \"Ansible\": 148,\n    \"Helm\": 135,\n    \"Jenkins\": 122,\n    \"GitLab\": 110,\n    \"ArgoCD\": 98,\n    \"Prometheus\": 90,\n    \"Grafana\": 82,\n    \"Vault\": 75,\n    \"Consul\": 68,\n    \"CircleCI\": 60,\n    \"Chef\": 54,\n    \"Puppet\": 48,\n    \"GitOps\": 44,\n    \"Istio\": 40,\n    \"Envoy\": 36,\n    \"Fluentd\": 32,\n    \"Loki\": 29,\n    \"Kibana\": 26,\n    \"Vagrant\": 23,\n    \"Packer\": 20,\n    \"Nginx\": 18,\n    \"HAProxy\": 16,\n    \"Zabbix\": 14,\n    \"Nagios\": 12,\n    \"PagerDuty\": 10,\n}\n\n# Canvas dimensions (canonical landscape)\ncanvas_w = 3200\ncanvas_h = 1800\n\n# Scale frequencies to font sizes\nmin_freq = min(word_frequencies.values())\nmax_freq = max(word_frequencies.values())\nmin_size = 50\nmax_size = 187\n\n# Sort by frequency (largest first for better placement)\nsorted_words = sorted(word_frequencies.items(), key=lambda x: x[1], reverse=True)\nn_words = len(sorted_words)\n\n# Build word positions using spiral algorithm; opacity tiers by frequency add a\n# secondary depth cue beyond size alone (top tier fully opaque, tail eases back)\nword_data = []\nplaced_boxes = []\n\nfor i, (word, freq) in enumerate(sorted_words):\n    # Scale frequency to font size\n    size = int(min_size + (freq - min_freq) / (max_freq - min_freq) * (max_size - min_size))\n    opacity = round(0.7 + 0.3 * (freq - min_freq) / (max_freq - min_freq), 2)\n\n    # Estimate dimensions (generous width factor so bold glyphs keep a visible gap)\n    w = len(word) * size * 0.62\n    h = size * 1.2\n\n    # Spiral placement - centered with balanced distribution, biased below the title band\n    cx, cy = canvas_w / 2, canvas_h / 2 + 60\n\n    # Ellipse ratio narrows from wide (matches the 16:9 canvas for early, large\n    # words) toward near-circular for the tail, so late/small words reach the\n    # top-right/bottom-right voids instead of stacking against the horizontal bound\n    progress = i / max(n_words - 1, 1)\n    ellipse_x = 2.8 - 1.2 * progress\n    ellipse_y = 1.8 - 0.2 * progress\n\n    # Stagger each word's starting angle by the golden angle so consecutive\n    # spirals fan out in different directions instead of retracing the same\n    # path and piling into whichever gap opens first along it\n    angle = i * 2.399963\n    radius = 0\n    x, y = cx, cy\n    box = (cx - w / 2, cy - h / 2, w, h)\n\n    for _ in range(50000):\n        # Elliptical spiral\n        test_x = cx + radius * ellipse_x * np.cos(angle) - w / 2\n        test_y = cy + radius * ellipse_y * np.sin(angle) - h / 2\n\n        # Check bounds with margins for title and edges\n        if 67 < test_x < canvas_w - w - 67 and 150 < test_y < canvas_h - h - 67:\n            test_box = (test_x, test_y, w, h)\n            # Check for overlap with placed words\n            overlap = False\n            for pb in placed_boxes:\n                x1, y1, w1, h1 = test_box\n                x2, y2, w2, h2 = pb\n                padding = 40  # Padding to prevent clustering / guarantee a visible gap\n                if not (\n                    x1 + w1 + padding < x2 or x2 + w2 + padding < x1 or y1 + h1 + padding < y2 or y2 + h2 + padding < y1\n                ):\n                    overlap = True\n                    break\n            if not overlap:\n                x = test_x + w / 2\n                y = test_y + h / 2\n                box = test_box\n                break\n\n        angle += 0.06  # Slower angle progression for better spacing\n        radius += 2.33  # Moderate radius growth\n\n    placed_boxes.append(box)\n    word_data.append(\n        {\"word\": word, \"x\": x, \"y\": y, \"size\": size, \"opacity\": opacity, \"color\": IMPRINT[i % len(IMPRINT)]}\n    )\n\n# Recenter the whole cloud within the safe canvas area: the spiral settles\n# wherever overlap checks first succeed, which tends to drift the bounding\n# box off-center (e.g. a large left-side void with words crowded right).\n# Shifting every word by the same offset preserves all relative spacing and\n# the zero-overlap guarantee while balancing the leftover whitespace.\nsafe_left, safe_right = 67, canvas_w - 67\nsafe_top, safe_bottom = 150, canvas_h - 67\nbox_min_x = min(b[0] for b in placed_boxes)\nbox_max_x = max(b[0] + b[2] for b in placed_boxes)\nbox_min_y = min(b[1] for b in placed_boxes)\nbox_max_y = max(b[1] + b[3] for b in placed_boxes)\nshift_x = (safe_left + safe_right) / 2 - (box_min_x + box_max_x) / 2\nshift_y = (safe_top + safe_bottom) / 2 - (box_min_y + box_max_y) / 2\nfor item in word_data:\n    item[\"x\"] += shift_x\n    item[\"y\"] += shift_y\n\n# Minimal pygal chart as canvas, using pygal's own title rendering (idiomatic\n# high-level API) instead of hand-drawn SVG text; only the word placement below\n# needs manual SVG injection since pygal has no word-cloud primitive.\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    title_font_size=66,\n    title_font_family=\"sans-serif\",  # match the bold sans-serif word text below\n)\n\nchart = pygal.XY(\n    style=custom_style,\n    width=canvas_w,\n    height=canvas_h,\n    title=\"wordcloud-basic · pygal · anyplot.ai\",\n    show_legend=False,\n    show_x_labels=False,\n    show_y_labels=False,\n    show_x_guides=False,\n    show_y_guides=False,\n    show_dots=False,\n    stroke=False,\n    margin=0,\n)\n\n# Add dummy data (required for chart to render)\nchart.add(\"\", [(0, 0)])\n\n# Render SVG and manually inject the word cloud text elements (no pygal\n# primitive exists for freeform-positioned, variably-sized text)\nsvg_string = chart.render(is_unicode=True)\nroot = ET.fromstring(svg_string)\n\nfor item in word_data:\n    text_elem = ET.SubElement(root, \"text\")\n    text_elem.set(\"x\", str(int(item[\"x\"])))\n    text_elem.set(\"y\", str(int(item[\"y\"])))\n    text_elem.set(\"font-size\", str(item[\"size\"]))\n    text_elem.set(\"font-weight\", \"bold\")\n    text_elem.set(\"fill\", item[\"color\"])\n    text_elem.set(\"fill-opacity\", str(item[\"opacity\"]))\n    text_elem.set(\"text-anchor\", \"middle\")\n    text_elem.set(\"dominant-baseline\", \"middle\")\n    text_elem.set(\"font-family\", \"sans-serif\")\n    text_elem.text = item[\"word\"]\n\n# Write modified SVG\nmodified_svg = ET.tostring(root, encoding=\"unicode\")\nwith open(f\"plot-{THEME}.svg\", \"w\") as f:\n    f.write(modified_svg)\n\n# Render PNG using cairosvg\ncairosvg.svg2png(bytestring=modified_svg.encode(), write_to=f\"plot-{THEME}.png\")\n\n# Save as HTML for interactive viewing\nwith open(f\"plot-{THEME}.html\", \"w\") as f:\n    f.write(\n        f\"\"\"<!DOCTYPE html>\n<html>\n<head>\n    <title>wordcloud-basic · pygal · anyplot.ai</title>\n    <style>\n        body {{ margin: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; background: {PAGE_BG}; }}\n        svg {{ max-width: 100%; height: auto; }}\n    </style>\n</head>\n<body>\n{modified_svg}\n</body>\n</html>\"\"\"\n    )\n"}