{"spec_id":"wordcloud-basic","library":"makie","language":"julia","code":"# anyplot.ai\n# wordcloud-basic: Basic Word Cloud\n# Library: makie 0.21.9 | Julia 1.11.9\n# Quality: 88/100 | Created: 2026-08-04\n\nusing CairoMakie\nusing Makie\nusing Colors\n\n# --- Theme tokens -----------------------------------------------------------\nconst THEME     = get(ENV, \"ANYPLOT_THEME\", \"light\")\nconst PAGE_BG   = THEME == \"light\" ? colorant\"#FAF8F1\" : colorant\"#1A1A17\"\nconst INK       = THEME == \"light\" ? colorant\"#1A1A17\" : colorant\"#F0EFE8\"\nconst INK_MUTED = THEME == \"light\" ? colorant\"#6B6A63\" : colorant\"#A8A79F\"\nconst IMPRINT_PALETTE = [\n    colorant\"#009E73\", colorant\"#C475FD\", colorant\"#4467A3\", colorant\"#BD8233\",\n    colorant\"#AE3030\", colorant\"#2ABCCD\", colorant\"#954477\", colorant\"#99B314\",\n]\n\n# --- Data: support-ticket term frequencies for a smart-thermostat app ------\nwords = [\n    \"app\", \"easy\", \"battery\", \"love\", \"setup\", \"temperature\", \"sync\", \"schedule\",\n    \"notifications\", \"intuitive\", \"energy\", \"savings\", \"reliable\", \"design\",\n    \"support\", \"helpful\", \"wifi\", \"remote\", \"control\", \"integration\", \"voice\",\n    \"assistant\", \"update\", \"features\", \"price\", \"interface\", \"compatibility\",\n    \"recommend\", \"sleek\", \"response\", \"connection\", \"slow\", \"laggy\", \"bugs\",\n    \"confusing\", \"glitch\", \"freeze\", \"restart\", \"unresponsive\", \"complicated\",\n    \"expensive\", \"manual\", \"disappointed\", \"refund\",\n]\nfrequencies = [\n    340, 290, 260, 245, 230, 210, 195, 180,\n    165, 155, 145, 138, 130, 122,\n    115, 108, 102, 96, 90, 85, 80,\n    75, 70, 66, 62, 58, 54,\n    50, 47, 44, 41, 38, 35, 33,\n    30, 28, 26, 24, 22, 20,\n    19, 18, 17, 16,\n]\n\norder = sortperm(frequencies; rev = true)\nwords = words[order]\nfrequencies = Float64.(frequencies[order])\n\n# Font size scaled by sqrt(frequency) so glyph AREA tracks word frequency\nmin_fs, max_fs = 22.0, 108.0\nscaled = sqrt.(frequencies)\nfontsizes = min_fs .+\n    (scaled .- minimum(scaled)) ./ (maximum(scaled) - minimum(scaled)) .* (max_fs - min_fs)\n\n# Color encodes sentiment polarity (not rank, which would just repeat the size\n# signal): satisfaction terms -> brand green, complaint terms -> matte red,\n# neutral/functional terms -> muted gray. This is the style guide's\n# sentiment/polarity semantic exception.\npositive_words = Set([\n    \"easy\", \"love\", \"intuitive\", \"savings\", \"reliable\", \"helpful\", \"recommend\", \"sleek\",\n])\nnegative_words = Set([\n    \"slow\", \"laggy\", \"bugs\", \"confusing\", \"glitch\", \"freeze\", \"restart\",\n    \"unresponsive\", \"complicated\", \"expensive\", \"disappointed\", \"refund\",\n])\ncolors = [\n    w in positive_words ? IMPRINT_PALETTE[1] :\n    w in negative_words ? IMPRINT_PALETTE[5] :\n    INK_MUTED\n    for w in words\n]\n\n# --- Plot --------------------------------------------------------------------\nfig = Figure(\n    resolution      = (1600, 900),\n    backgroundcolor = PAGE_BG,\n)\n\nax = Axis(\n    fig[1, 1];\n    title           = \"wordcloud-basic · julia · makie · anyplot.ai\",\n    titlesize       = 20,\n    titlecolor      = INK,\n    backgroundcolor = PAGE_BG,\n    aspect          = DataAspect(),\n)\nhidedecorations!(ax)\nhidespines!(ax)\nxlims!(ax, -800, 800)\nylims!(ax, -450, 450)\n\n# --- Word placement: Archimedean spiral search, no overlap ------------------\nfont = Makie.defaultfont()\npad = 6.0\nx_bound, y_bound = 760.0, 400.0\n\nplaced = Tuple{Float64,Float64,Float64,Float64}[]  # (cx, cy, halfwidth, halfheight)\n\noverlaps(cx, cy, hw, hh) = any(\n    abs(cx - px) < hw + phw && abs(cy - py) < hh + phh\n    for (px, py, phw, phh) in placed\n)\n\nfor i in eachindex(words)\n    wh = widths(Makie.text_bb(words[i], font, fontsizes[i]))\n    hw, hh = wh[1] / 2 + pad, wh[2] / 2 + pad\n\n    cx, cy = 0.0, 0.0\n    if !isempty(placed)\n        theta, radius = 0.0, 0.0\n        step = 0.28\n        tries = 0\n        while true\n            cx = radius * cos(theta)\n            cy = radius * sin(theta) * 0.6\n            fits_bounds = abs(cx) + hw <= x_bound && abs(cy) + hh <= y_bound\n            tries += 1\n            if (fits_bounds && !overlaps(cx, cy, hw, hh)) || tries >= 20000\n                break\n            end\n            theta += step\n            radius += 1.6 * step\n        end\n    end\n\n    push!(placed, (cx, cy, hw, hh))\n    text!(ax, cx, cy; text = words[i], fontsize = fontsizes[i],\n          color = colors[i], font = font, align = (:center, :center))\nend\n\n# --- Save -------------------------------------------------------------------\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}