Voronoi labels

Here a Voronoi diagram is used to label a scatterplot. The area of each Voronoi cell determines whether the associated label is visible: larger cells tend to have room to accommodate labels. The vector between the point and the cell’s centroid (orange) determines the label orientation: top, right, bottom or left.

const delaunay = d3.Delaunay.from(data);
const voronoi = delaunay.voronoi([-1, -1, width + 1, height + 1]);

const labels = [
  (text) => text.attr("text-anchor", "middle").attr("y", -6), // top
  (text) => text.attr("text-anchor", "start").attr("dy", "0.35em").attr("x", 6), // right
  (text) => text.attr("text-anchor", "middle").attr("dy", "0.71em").attr("y", 6), // bottom
  (text) => text.attr("text-anchor", "end").attr("dy", "0.35em").attr("x", -6) // left
];

const svg = d3.create("svg")
    .attr("viewBox", [0, 0, width, height])
    .attr("width", width)
    .attr("height", height)
    .attr("style", "max-width: 100%; height: auto;");

const cells = data.map((d, i) => [d, voronoi.cellPolygon(i)]);

svg.append("g")
    .attr("stroke", "orange")
  .selectAll("path")
  .data(cells)
  .join("path")
    .attr("d", ([d, cell]) => `M${d3.polygonCentroid(cell)}L${d}`);

svg.append("path")
    .attr("fill", "none")
    .attr("stroke", "#ccc")
    .attr("d", voronoi.render());

svg.append("path")
    .attr("d", delaunay.renderPoints(null, 2));

svg.append("g")
    .style("font", "10px sans-serif")
  .selectAll("text")
  .data(cells)
  .join("text")
    .each(function([[x, y], cell]) {
      const [cx, cy] = d3.polygonCentroid(cell);
      const angle = (Math.round(Math.atan2(cy - y, cx - x) / Math.PI * 2) + 5) % 4;
      d3.select(this).call(labels[angle]);
    })
    .attr("transform", ([d]) => `translate(${d})`)
    .attr("display", ([, cell]) => -d3.polygonArea(cell) > 1000 ? null : "none")
    .text((d, i) => i);

display(svg.node());
const width = 960;
const height = 600;

randomize; // re-run when the button is clicked

const rx = d3.randomNormal(width / 2, 80);
const ry = d3.randomNormal(height / 2, 80);
const rxy = () => ([rx(), ry()]);
const data = d3.range(200).map(rxy).filter(([x, y]) => 0 <= x && x <= width && 0 <= y && y <= height);

display(data);
✎ Suggest changes to this page