{"spec_id":"wordcloud-basic","library":"echarts","language":"javascript","code":"// anyplot.ai\n// wordcloud-basic: Basic Word Cloud\n// Library: echarts 6.1.0 | JavaScript 22.23.1\n// Quality: 88/100 | Created: 2026-08-04\n\n//# anyplot-orientation: square\nconst t = window.ANYPLOT_TOKENS;\nconst size = window.ANYPLOT_SIZE;\nconst FONT_FAMILY =\n  '-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif';\n\n// --- Data (in-memory, deterministic) ---------------------------------------\n// Term frequencies extracted from a product-feedback survey, sorted by\n// frequency descending so array rank doubles as the Imprint color cycle.\nconst terms = [\n  [\"performance\", 132],\n  [\"reliability\", 118],\n  [\"support\", 104],\n  [\"usability\", 96],\n  [\"pricing\", 89],\n  [\"integration\", 82],\n  [\"security\", 76],\n  [\"documentation\", 71],\n  [\"dashboard\", 66],\n  [\"onboarding\", 61],\n  [\"scalability\", 57],\n  [\"customization\", 53],\n  [\"analytics\", 49],\n  [\"automation\", 46],\n  [\"mobile\", 43],\n  [\"api\", 40],\n  [\"backup\", 37],\n  [\"collaboration\", 35],\n  [\"reporting\", 33],\n  [\"workflow\", 31],\n  [\"notifications\", 29],\n  [\"templates\", 27],\n  [\"permissions\", 25],\n  [\"search\", 24],\n  [\"accessibility\", 22],\n  [\"localization\", 21],\n  [\"compliance\", 19],\n  [\"encryption\", 18],\n  [\"latency\", 17],\n  [\"uptime\", 16],\n  [\"throughput\", 15],\n  [\"caching\", 14],\n  [\"widgets\", 13],\n  [\"plugins\", 12],\n  [\"shortcuts\", 11],\n  [\"export\", 10],\n  [\"import\", 9],\n  [\"sync\", 8],\n];\n\n// --- Layout: font size + spiral placement -----------------------------------\n// ECharts has no native wordcloud series (and the echarts-wordcloud extension\n// package is disallowed), so fontSizeFor/measureWord/boxesOverlap/placeWords\n// hand-roll the frequency->size mapping and spiral collision layout that a\n// dedicated wordcloud library would otherwise provide.\nconst FONT_MIN = 23;\nconst FONT_MAX = 100;\nconst PAD = 5; // breathing room between word boxes\nconst freqs = terms.map(([, f]) => f);\nconst freqMin = Math.min(...freqs);\nconst freqMax = Math.max(...freqs);\n\nconst measureCanvas = document.createElement(\"canvas\");\nconst measureCtx = measureCanvas.getContext(\"2d\");\n\nfunction fontSizeFor(freq) {\n  // Cube-root curve (vs. sqrt) compresses the spread so low-frequency tail\n  // words land closer to FONT_MIN while the high-frequency words still peak\n  // near FONT_MAX, keeping every word legible at mobile preview scale.\n  const ratio = Math.cbrt((freq - freqMin) / (freqMax - freqMin));\n  return Math.round(FONT_MIN + (FONT_MAX - FONT_MIN) * ratio);\n}\n\nfunction measureWord(word, fontSize, weight) {\n  measureCtx.font = `${weight} ${fontSize}px ${FONT_FAMILY}`;\n  const m = measureCtx.measureText(word);\n  const ascent = m.actualBoundingBoxAscent || fontSize * 0.8;\n  const descent = m.actualBoundingBoxDescent || fontSize * 0.25;\n  return { width: m.width, height: ascent + descent };\n}\n\nfunction boxesOverlap(a, b, pad) {\n  return !(\n    a.x1 + pad < b.x0 ||\n    b.x1 + pad < a.x0 ||\n    a.y1 + pad < b.y0 ||\n    b.y1 + pad < a.y0\n  );\n}\n\n// Archimedean spiral placement (classic word-cloud layout): try the center\n// first, then spiral outward until the word's bounding box clears every\n// already-placed word and stays inside the cloud area.\nfunction placeWords(words, area) {\n  const placed = [];\n  const cx = area.x0 + area.w / 2;\n  const cy = area.y0 + area.h / 2;\n  // Radius grows by RING_STEP px every full revolution (STEPS_PER_RING\n  // samples per ring) — a fixed, predictable outward crawl so later words\n  // reliably reach the open space at the edges instead of stalling near\n  // the center once early attempts are exhausted.\n  const STEPS_PER_RING = 48;\n  const ANGLE_STEP = (2 * Math.PI) / STEPS_PER_RING;\n  const RING_STEP = 6;\n  const MAX_ATTEMPTS = 20000;\n\n  for (const w of words) {\n    let box = null;\n    for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {\n      const angle = attempt * ANGLE_STEP;\n      const radius = RING_STEP * Math.floor(attempt / STEPS_PER_RING);\n      const px = cx + radius * Math.cos(angle) - w.width / 2;\n      const py = cy + radius * Math.sin(angle) - w.height / 2;\n      const candidate = { x0: px, y0: py, x1: px + w.width, y1: py + w.height };\n      const inBounds =\n        candidate.x0 >= area.x0 &&\n        candidate.y0 >= area.y0 &&\n        candidate.x1 <= area.x0 + area.w &&\n        candidate.y1 <= area.y0 + area.h;\n      const collides = inBounds && placed.some((p) => boxesOverlap(candidate, p, PAD));\n      box = candidate; // best-effort fallback if attempts run out\n      if (inBounds && !collides) break;\n    }\n    placed.push({ ...box, word: w });\n  }\n  return placed;\n}\n\nconst cloudArea = { x0: 50, y0: 150, w: size.width - 100, h: size.height - 200 };\nconst sized = terms.map(([word, freq], rank) => ({\n  word,\n  freq,\n  rank,\n  width: 0,\n  height: 0,\n  fontSize: fontSizeFor(freq),\n  weight: rank < 6 ? 700 : 500,\n}));\nfor (const w of sized) {\n  const m = measureWord(w.word, w.fontSize, w.weight);\n  w.width = m.width;\n  w.height = m.height;\n}\nconst placed = placeWords(sized, cloudArea);\n\n// --- Graphic elements ---------------------------------------------------------\nconst wordElements = placed.map((p) => ({\n  type: \"text\",\n  left: p.x0,\n  top: p.y0,\n  z: 10,\n  style: {\n    text: p.word.word,\n    fontSize: p.word.fontSize,\n    fontWeight: p.word.weight,\n    fontFamily: FONT_FAMILY,\n    fill: t.palette[p.word.rank % t.palette.length],\n  },\n}));\n\n// --- Init + option --------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"wordcloud-basic · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    textStyle: { color: t.ink, fontSize: 22 },\n  },\n  graphic: { elements: wordElements },\n});\nchart.on(\"finished\", () => {\n  window.__anyplotReady = true;\n});\n"}