{"spec_id":"wordcloud-basic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// wordcloud-basic: Basic Word Cloud\n// Library: chartjs 4.4.7 | JavaScript 22.23.1\n// Quality: 86/100 | Created: 2026-08-04\n//# anyplot-orientation: landscape\n\n// Chart.js ships no native word-cloud chart type, and the chartjs-chart-wordcloud\n// plugin is not installed in this runtime. This snippet reaches core Chart.js\n// only: a chart-scoped custom plugin (a first-class, no-import Chart.js API,\n// distinct from a chartjs-chart-* package) draws the words directly onto the\n// chart's own canvas context inside afterDraw, using a deterministic Archimedean\n// spiral for placement — the same placement idea community word-cloud plugins\n// use internally, authored here without any external dependency.\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ---------------------------------------\n// Term frequencies mined from a customer-feedback corpus, grouped into themes.\nconst CATEGORIES = [\n  { key: \"performance\", label: \"Performance\" },\n  { key: \"usability\", label: \"Usability\" },\n  { key: \"support\", label: \"Support\" },\n  { key: \"pricing\", label: \"Pricing & Billing\" },\n  { key: \"features\", label: \"Features\" },\n];\n\nconst WORDS = [\n  { word: \"performance\", frequency: 145, category: \"performance\" },\n  { word: \"support\", frequency: 130, category: \"support\" },\n  { word: \"pricing\", frequency: 121, category: \"pricing\" },\n  { word: \"speed\", frequency: 118, category: \"performance\" },\n  { word: \"feature\", frequency: 109, category: \"features\" },\n  { word: \"interface\", frequency: 102, category: \"usability\" },\n  { word: \"usability\", frequency: 96, category: \"usability\" },\n  { word: \"security\", frequency: 95, category: \"features\" },\n  { word: \"response\", frequency: 92, category: \"support\" },\n  { word: \"crash\", frequency: 88, category: \"performance\" },\n  { word: \"integration\", frequency: 87, category: \"features\" },\n  { word: \"ticket\", frequency: 85, category: \"support\" },\n  { word: \"navigation\", frequency: 84, category: \"usability\" },\n  { word: \"subscription\", frequency: 79, category: \"pricing\" },\n  { word: \"latency\", frequency: 76, category: \"performance\" },\n  { word: \"cost\", frequency: 74, category: \"pricing\" },\n  { word: \"design\", frequency: 73, category: \"usability\" },\n  { word: \"reliability\", frequency: 71, category: \"performance\" },\n  { word: \"billing\", frequency: 68, category: \"pricing\" },\n  { word: \"workflow\", frequency: 67, category: \"usability\" },\n  { word: \"chat\", frequency: 66, category: \"support\" },\n  { word: \"analytics\", frequency: 65, category: \"features\" },\n  { word: \"mobile\", frequency: 63, category: \"features\" },\n  { word: \"documentation\", frequency: 61, category: \"support\" },\n  { word: \"onboarding\", frequency: 58, category: \"usability\" },\n  { word: \"plan\", frequency: 57, category: \"pricing\" },\n  { word: \"resolution\", frequency: 54, category: \"support\" },\n  { word: \"uptime\", frequency: 54, category: \"performance\" },\n  { word: \"dashboard\", frequency: 52, category: \"features\" },\n  { word: \"search\", frequency: 51, category: \"features\" },\n  { word: \"accessibility\", frequency: 49, category: \"usability\" },\n  { word: \"helpdesk\", frequency: 47, category: \"support\" },\n  { word: \"reporting\", frequency: 48, category: \"features\" },\n];\n\nconst FONT_FAMILY = \"'Helvetica Neue', Helvetica, Arial, sans-serif\";\nconst MIN_FONT_PX = 22;\nconst MAX_FONT_PX = 104;\nconst MIN_WEIGHT = 500;\nconst MAX_WEIGHT = 800;\nconst BOX_PAD = 6;\nconst AREA_MARGIN = 20;\n\nconst freqs = WORDS.map((w) => w.frequency);\nconst minFreq = Math.min(...freqs);\nconst maxFreq = Math.max(...freqs);\n\nfunction scale(freq) {\n  // sqrt scale keeps the size spread readable instead of letting the top\n  // word dwarf everything else.\n  const u = Math.sqrt((freq - minFreq) / (maxFreq - minFreq));\n  return {\n    fontSize: MIN_FONT_PX + (MAX_FONT_PX - MIN_FONT_PX) * u,\n    weight: Math.round(MIN_WEIGHT + (MAX_WEIGHT - MIN_WEIGHT) * u),\n  };\n}\n\nconst categoryColor = Object.fromEntries(\n  CATEGORIES.map((c, i) => [c.key, t.palette[i % t.palette.length]]),\n);\n\n// Largest-first placement packs the spiral center with the most important\n// words and lets smaller ones fill the remaining gaps.\nconst layoutQueue = WORDS.slice().sort((a, b) => b.frequency - a.frequency);\n\nfunction layoutWords(ctx, area) {\n  const centerX = (area.left + area.right) / 2;\n  const centerY = (area.top + area.bottom) / 2;\n  const maxRadius = Math.hypot(area.right - area.left, area.bottom - area.top) / 2;\n  const placed = [];\n  const positioned = [];\n\n  for (const entry of layoutQueue) {\n    const { fontSize, weight } = scale(entry.frequency);\n    ctx.font = `${weight} ${fontSize}px ${FONT_FAMILY}`;\n    const metrics = ctx.measureText(entry.word);\n    const boxW = metrics.width + BOX_PAD * 2;\n    const boxH = fontSize + BOX_PAD * 2;\n\n    let found = null;\n    const angleStep = 0.35;\n    const radiusStep = 2.0;\n    for (let step = 0, angle = 0, radius = 0; radius < maxRadius; step++, angle += angleStep, radius = radiusStep * angle) {\n      // Flatten the spiral vertically so it fills the wide landscape mount\n      // instead of drifting into a tall, narrow column.\n      const x = centerX + radius * Math.cos(angle);\n      const y = centerY + radius * Math.sin(angle) * 0.62;\n      const box = { left: x - boxW / 2, right: x + boxW / 2, top: y - boxH / 2, bottom: y + boxH / 2 };\n      if (box.left < area.left || box.right > area.right || box.top < area.top || box.bottom > area.bottom) continue;\n      const collides = (p) => p.left < box.right && p.right > box.left && p.top < box.bottom && p.bottom > box.top;\n      if (!placed.some(collides)) {\n        found = { x, y, box };\n        break;\n      }\n    }\n\n    if (!found) continue; // canvas full for this word's size; skip rather than overlap\n    placed.push(found.box);\n    positioned.push({ ...entry, x: found.x, y: found.y, fontSize, weight });\n  }\n\n  return positioned;\n}\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Word-cloud draw plugin ---------------------------------------------------\nconst LEGEND_ROW_H = 44;\n\nconst wordCloudPlugin = {\n  id: \"wordCloud\",\n  afterDraw(chart) {\n    const { ctx, chartArea } = chart;\n    ctx.save();\n\n    // Category legend: colored swatch + label, one row under the title.\n    ctx.font = `600 15px ${FONT_FAMILY}`;\n    ctx.textBaseline = \"middle\";\n    let lx = chartArea.left;\n    const ly = chartArea.top + LEGEND_ROW_H / 2;\n    for (const cat of CATEGORIES) {\n      const swatch = 14;\n      ctx.fillStyle = categoryColor[cat.key];\n      ctx.fillRect(lx, ly - swatch / 2, swatch, swatch);\n      lx += swatch + 8;\n      ctx.fillStyle = t.ink;\n      ctx.textAlign = \"left\";\n      ctx.fillText(cat.label, lx, ly + 1);\n      lx += ctx.measureText(cat.label).width + 26;\n    }\n\n    // Word cloud fills the region below the legend row, inset by a margin so\n    // words breathe away from the canvas frame instead of crowding the edges.\n    const area = {\n      left: chartArea.left + AREA_MARGIN,\n      right: chartArea.right - AREA_MARGIN,\n      top: chartArea.top + LEGEND_ROW_H + AREA_MARGIN,\n      bottom: chartArea.bottom - AREA_MARGIN,\n    };\n    const words = layoutWords(ctx, area);\n\n    ctx.textAlign = \"center\";\n    ctx.textBaseline = \"middle\";\n    for (const w of words) {\n      ctx.font = `${w.weight} ${w.fontSize}px ${FONT_FAMILY}`;\n      ctx.fillStyle = categoryColor[w.category];\n      ctx.fillText(w.word, w.x, w.y);\n    }\n\n    ctx.restore();\n  },\n};\n\n// --- Chart ---------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"scatter\",\n  data: { datasets: [{ data: [] }] },\n  plugins: [wordCloudPlugin],\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    plugins: {\n      title: {\n        display: true,\n        text: \"wordcloud-basic · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n      },\n      legend: { display: false },\n      tooltip: { enabled: false },\n    },\n    scales: {\n      x: { display: false, min: 0, max: 1, grid: { display: false } },\n      y: { display: false, min: 0, max: 1, grid: { display: false } },\n    },\n  },\n});\n"}