Basic Word Cloud — D3.js

A word cloud displays text data where word size represents frequency or importance. Words are arranged to fill available space, creating a visual summary of text content that highlights prominent terms and patterns. This visualization is ideal for quickly identifying the most common themes or keywords in a body of text.

Basic Word Cloud rendered with D3.js

JavaScript source (D3.js)

// anyplot.ai
// wordcloud-basic: Basic Word Cloud
// Library: d3 7.9.0 | JavaScript 22.23.1
// Quality: 88/100 | Created: 2026-08-04

//# anyplot-orientation: landscape
const t = window.ANYPLOT_TOKENS;
const { width, height } = window.ANYPLOT_SIZE;
const muted = window.ANYPLOT_THEME === "dark" ? "#A8A79F" : "#6B6A63";

// --- Data: term frequencies from a product-review survey, tagged by sentiment
const words = [
  { word: "reliable", frequency: 142, sentiment: "positive" },
  { word: "expensive", frequency: 118, sentiment: "negative" },
  { word: "intuitive", frequency: 104, sentiment: "positive" },
  { word: "support", frequency: 96, sentiment: "neutral" },
  { word: "battery", frequency: 90, sentiment: "neutral" },
  { word: "sluggish", frequency: 84, sentiment: "negative" },
  { word: "durable", frequency: 79, sentiment: "positive" },
  { word: "shipping", frequency: 74, sentiment: "negative" },
  { word: "design", frequency: 70, sentiment: "positive" },
  { word: "setup", frequency: 66, sentiment: "neutral" },
  { word: "value", frequency: 62, sentiment: "positive" },
  { word: "noisy", frequency: 58, sentiment: "negative" },
  { word: "warranty", frequency: 55, sentiment: "neutral" },
  { word: "lightweight", frequency: 52, sentiment: "positive" },
  { word: "confusing", frequency: 49, sentiment: "negative" },
  { word: "responsive", frequency: 47, sentiment: "positive" },
  { word: "packaging", frequency: 45, sentiment: "neutral" },
  { word: "overpriced", frequency: 43, sentiment: "negative" },
  { word: "sturdy", frequency: 41, sentiment: "positive" },
  { word: "manual", frequency: 39, sentiment: "neutral" },
  { word: "glitchy", frequency: 37, sentiment: "negative" },
  { word: "comfortable", frequency: 35, sentiment: "positive" },
  { word: "delayed", frequency: 33, sentiment: "negative" },
  { word: "helpful", frequency: 32, sentiment: "positive" },
  { word: "compact", frequency: 30, sentiment: "positive" },
  { word: "pricey", frequency: 29, sentiment: "negative" },
  { word: "smooth", frequency: 28, sentiment: "positive" },
  { word: "outdated", frequency: 27, sentiment: "negative" },
  { word: "friendly", frequency: 26, sentiment: "positive" },
  { word: "return", frequency: 25, sentiment: "neutral" },
  { word: "accurate", frequency: 24, sentiment: "positive" },
  { word: "clunky", frequency: 23, sentiment: "negative" },
  { word: "premium", frequency: 22, sentiment: "positive" },
  { word: "delivery", frequency: 21, sentiment: "neutral" },
  { word: "stylish", frequency: 20, sentiment: "positive" },
  { word: "refund", frequency: 19, sentiment: "negative" },
  { word: "efficient", frequency: 18, sentiment: "positive" },
  { word: "instructions", frequency: 17, sentiment: "neutral" },
  { word: "quiet", frequency: 16, sentiment: "positive" },
  { word: "fragile", frequency: 15, sentiment: "negative" },
];

const sentimentColor = (s) => (s === "positive" ? t.palette[0] : s === "negative" ? t.palette[4] : muted);

const fontSize = d3
  .scaleSqrt()
  .domain(d3.extent(words, (d) => d.frequency))
  .range([20, 80]);

// --- SVG mount ---------------------------------------------------------------
const svg = d3.select("#container").append("svg").attr("width", width).attr("height", height);

// --- Cloud placement (Vogel/Fermat sunflower spiral + rectangle collision) --
const bounds = { xMin: 30, xMax: width - 30, yMin: 96, yMax: height - 76 };
const cx = (bounds.xMin + bounds.xMax) / 2;
const cy = (bounds.yMin + bounds.yMax) / 2;
const halfW = cx - bounds.xMin;
const halfH = cy - bounds.yMin;
const squash = halfH / halfW; // keep the elliptical search inside the rectangular bounds
const pad = 4;
const placedRects = [];

const overlaps = (r) =>
  placedRects.some((p) => !(r.x + r.w < p.x || r.x > p.x + p.w || r.y + r.h < p.y || r.y > p.y + p.h));

const GOLDEN_ANGLE = 2.399963;
const SPIRAL_STEP = 5;

const findSpot = (bw, bh) => {
  const maxRadius = Math.hypot(halfW, halfH) + Math.max(bw, bh);
  const maxSteps = Math.ceil((maxRadius / SPIRAL_STEP) ** 2);
  for (let step = 0; step < maxSteps; step++) {
    const angle = step * GOLDEN_ANGLE;
    const radius = SPIRAL_STEP * Math.sqrt(step);
    const x = cx + radius * Math.cos(angle);
    const y = cy + radius * Math.sin(angle) * squash;
    const rect = { x: x - bw / 2 - pad, y: y - bh / 2 - pad, w: bw + 2 * pad, h: bh + 2 * pad };
    if (rect.x < bounds.xMin || rect.x + rect.w > bounds.xMax) continue;
    if (rect.y < bounds.yMin || rect.y + rect.h > bounds.yMax) continue;
    if (!overlaps(rect)) return { x, y, rect };
  }
  return null;
};

// Largest (most frequent) terms placed first, near the center.
const ranked = [...words].sort((a, b) => b.frequency - a.frequency);

const cloud = svg.append("g");
const placed = [];
for (const d of ranked) {
  const label = cloud
    .append("text")
    .attr("font-size", `${fontSize(d.frequency)}px`)
    .attr("font-weight", d.frequency >= fontSize.domain()[1] * 0.55 ? 700 : 500)
    .attr("text-anchor", "middle")
    .attr("dominant-baseline", "central")
    .attr("fill", sentimentColor(d.sentiment))
    .text(d.word);

  const box = label.node().getBBox();
  const spot = findSpot(box.width, box.height);
  if (!spot) {
    label.remove();
    continue;
  }
  label.attr("x", spot.x).attr("y", spot.y);
  placedRects.push(spot.rect);
  placed.push({ label, x: spot.x, y: spot.y });
}

// Stretch the placed word mass outward from its own center so it fills more
// of the reserved bounds, scaling each axis independently and clamping so no
// rect crosses the original bounds. Because every rect scales from the same
// center by a factor >= 1, pairwise gaps only grow, so this never introduces
// a new overlap between previously non-overlapping words.
if (placed.length) {
  const minX = d3.min(placedRects, (r) => r.x);
  const maxX = d3.max(placedRects, (r) => r.x + r.w);
  const minY = d3.min(placedRects, (r) => r.y);
  const maxY = d3.max(placedRects, (r) => r.y + r.h);
  const bcx = (minX + maxX) / 2;
  const bcy = (minY + maxY) / 2;
  const halfBw = (maxX - minX) / 2;
  const halfBh = (maxY - minY) / 2;
  const sx = Math.max(1, Math.min((bounds.xMax - bcx) / halfBw, (bcx - bounds.xMin) / halfBw));
  const sy = Math.max(1, Math.min((bounds.yMax - bcy) / halfBh, (bcy - bounds.yMin) / halfBh));
  for (const p of placed) {
    p.label.attr("x", bcx + (p.x - bcx) * sx).attr("y", bcy + (p.y - bcy) * sy);
  }
}

// --- Title ---------------------------------------------------------------
svg
  .append("text")
  .attr("x", width / 2)
  .attr("y", 46)
  .attr("text-anchor", "middle")
  .attr("fill", t.ink)
  .style("font-size", "22px")
  .style("font-weight", "600")
  .text("wordcloud-basic · javascript · d3 · anyplot.ai");

// --- Legend: sentiment color key ------------------------------------------
const legendItems = [
  { label: "Positive", color: t.palette[0] },
  { label: "Negative", color: t.palette[4] },
  { label: "Neutral", color: muted },
];
const legend = svg
  .append("g")
  .attr("transform", `translate(${width / 2 - 170}, ${height - 34})`);
legendItems.forEach((item, i) => {
  const g = legend.append("g").attr("transform", `translate(${i * 130}, 0)`);
  g.append("rect").attr("width", 14).attr("height", 14).attr("rx", 3).attr("fill", item.color);
  g.append("text")
    .attr("x", 22)
    .attr("y", 11)
    .attr("fill", t.inkSoft)
    .style("font-size", "14px")
    .text(item.label);
});

Part of Basic Word Cloud on anyplot.ai.

Other implementations