Basic Count Plot — D3.js

A count plot displays the frequency of observations in each category of a categorical variable using vertical bars. Unlike a basic bar chart that requires pre-computed values, a count plot automatically counts occurrences from raw data. This makes it ideal for quick exploratory analysis of categorical distributions without manual aggregation.

Basic Count Plot rendered with D3.js

Renders

JavaScript source (D3.js)

// anyplot.ai
// count-basic: Basic Count Plot
// Library: d3 7.9.0 | JavaScript 22.23.1
// Quality: 95/100 | Created: 2026-08-11

const t = window.ANYPLOT_TOKENS;
const { width, height } = window.ANYPLOT_SIZE;
const margin = { top: 170, right: 60, bottom: 120, left: 130 };
const iw = width - margin.left - margin.right;
const ih = height - margin.top - margin.bottom;

// --- Data: raw per-respondent observations, counted (not pre-aggregated) ---
// Small LCG so results are deterministic without a browser RNG.
const lcg = (seed) => {
  let state = seed;
  return () => {
    state = (state * 1664525 + 1013904223) % 4294967296;
    return state / 4294967296;
  };
};
const random = lcg(42);

const languageWeights = [
  { language: "Python", weight: 28 },
  { language: "JavaScript", weight: 24 },
  { language: "TypeScript", weight: 16 },
  { language: "Java", weight: 12 },
  { language: "Go", weight: 9 },
  { language: "Rust", weight: 7 },
  { language: "C++", weight: 4 },
];
let cumulative = 0;
const cumulativeWeights = languageWeights.map((d) => {
  cumulative += d.weight;
  return { language: d.language, upTo: cumulative };
});
const totalWeight = cumulative;

const RESPONDENTS = 640;
const surveyResponses = Array.from({ length: RESPONDENTS }, () => {
  const r = random() * totalWeight;
  return cumulativeWeights.find((d) => r <= d.upTo).language;
});

const counts = Array.from(
  d3.rollup(
    surveyResponses,
    (v) => v.length,
    (d) => d,
  ),
  ([language, count]) => ({ language, count }),
).sort((a, b) => d3.descending(a.count, b.count));
const meanCount = d3.mean(counts, (d) => d.count);

// --- SVG mount ---------------------------------------------------------------
const svg = d3.select("#container").append("svg").attr("width", width).attr("height", height);
const g = svg.append("g").attr("transform", `translate(${margin.left},${margin.top})`);

// --- Scales --------------------------------------------------------------------
const x = d3
  .scaleBand()
  .domain(counts.map((d) => d.language))
  .range([0, iw])
  .padding(0.35);
const y = d3
  .scaleLinear()
  .domain([0, d3.max(counts, (d) => d.count)])
  .nice()
  .range([ih, 0]);

// --- Gridlines (y-axis only, subtle) --------------------------------------------
g.append("g")
  .selectAll("line")
  .data(y.ticks(6))
  .join("line")
  .attr("x1", 0)
  .attr("x2", iw)
  .attr("y1", (d) => y(d))
  .attr("y2", (d) => y(d))
  .attr("stroke", t.grid)
  .attr("stroke-width", 1);

// --- Axes ------------------------------------------------------------------------
const xAxis = g
  .append("g")
  .attr("transform", `translate(0,${ih})`)
  .call(d3.axisBottom(x).tickSize(0).tickPadding(14));
const yAxis = g
  .append("g")
  .call(d3.axisLeft(y).ticks(6).tickFormat(d3.format(",d")).tickSize(0).tickPadding(12));
for (const axis of [xAxis, yAxis]) {
  axis.selectAll("text").attr("fill", t.inkSoft).style("font-size", "16px");
  axis.select(".domain").remove();
}
yAxis.selectAll(".tick line").attr("stroke", t.grid);

// --- Bars ---------------------------------------------------------------------
// Opacity fades from the leading language (full strength) down through the
// long tail, giving the top result a visual focal point beyond sort order.
const emphasis = d3
  .scaleLinear()
  .domain([d3.min(counts, (d) => d.count), d3.max(counts, (d) => d.count)])
  .range([0.6, 1]);

g.selectAll("rect")
  .data(counts)
  .join("rect")
  .attr("x", (d) => x(d.language))
  .attr("y", (d) => y(d.count))
  .attr("width", x.bandwidth())
  .attr("height", (d) => ih - y(d.count))
  .attr("fill", t.palette[0])
  .attr("fill-opacity", (d) => emphasis(d.count))
  .attr("stroke", t.ink)
  .attr("stroke-opacity", 0.15)
  .attr("stroke-width", 1);

// --- Mean reference line (secondary encoding beyond sort/opacity) --------------
// A dashed amber rule at the across-category mean lets the eye split the long
// tail into above-/below-average languages at a glance. Drawn before the count
// labels so their page-bg halo (below) can mask the line where they land close
// to it, instead of the line cutting through the digits.
g.append("line")
  .attr("x1", 0)
  .attr("x2", iw)
  .attr("y1", y(meanCount))
  .attr("y2", y(meanCount))
  .attr("stroke", t.amber)
  .attr("stroke-width", 2)
  .attr("stroke-dasharray", "6,5");

g.append("text")
  .attr("x", iw)
  .attr("y", y(meanCount) - 10)
  .attr("text-anchor", "end")
  .attr("fill", t.amber)
  .style("font-size", "14px")
  .style("font-weight", "600")
  .text(`Mean = ${d3.format(",.0f")(meanCount)}`);

// --- Count labels above bars ---------------------------------------------------
// A page-bg halo (paint-order: stroke) keeps the mean line above from visually
// clashing with labels that land close to it (e.g. Java at 87 vs. mean 91).
g.selectAll(".count-label")
  .data(counts)
  .join("text")
  .attr("class", "count-label")
  .attr("x", (d) => x(d.language) + x.bandwidth() / 2)
  .attr("y", (d) => y(d.count) - 14)
  .attr("text-anchor", "middle")
  .attr("fill", t.ink)
  .attr("stroke", t.pageBg)
  .attr("stroke-width", 6)
  .attr("stroke-linejoin", "round")
  .style("paint-order", "stroke")
  .style("font-size", "16px")
  .style("font-weight", "600")
  .text((d) => d.count);

// --- Leading-category annotation (d3-shape symbol generator) -------------------
// A small amber diamond over the top bar calls out the mode without adding a
// second data color, exercising d3.symbol alongside the rollup/scale/axis idiom.
const leader = counts[0];
const leaderX = x(leader.language) + x.bandwidth() / 2;
const leaderY = y(leader.count) - 40;
g.append("path")
  .attr("d", d3.symbol().type(d3.symbolDiamond).size(140)())
  .attr("transform", `translate(${leaderX},${leaderY})`)
  .attr("fill", t.amber);
g.append("text")
  .attr("x", leaderX)
  .attr("y", leaderY - 14)
  .attr("text-anchor", "middle")
  .attr("fill", t.amber)
  .style("font-size", "14px")
  .style("font-weight", "600")
  .text("Most common");

// --- Axis titles -----------------------------------------------------------------
g.append("text")
  .attr("x", iw / 2)
  .attr("y", ih + 76)
  .attr("text-anchor", "middle")
  .attr("fill", t.ink)
  .style("font-size", "18px")
  .text("Primary Programming Language");

g.append("text")
  .attr("transform", "rotate(-90)")
  .attr("x", -ih / 2)
  .attr("y", -92)
  .attr("text-anchor", "middle")
  .attr("fill", t.ink)
  .style("font-size", "18px")
  .text("Number of Respondents");

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

svg
  .append("text")
  .attr("x", width / 2)
  .attr("y", 88)
  .attr("text-anchor", "middle")
  .attr("fill", t.inkSoft)
  .style("font-size", "17px")
  .text(`Developer survey · n = ${RESPONDENTS} respondents · sorted by frequency`);

Part of Basic Count Plot on anyplot.ai.

Other implementations