Adding a background to text in d3

When working with d3.js you can get into a situation where you have a text element that’s hard to read because of content in the background. Unfortunately you can’t add a background to the text element, but you can automatically add a rect with the same bounding box and position behind the text element. This rect element CAN have a fill to give your text the surface that separates it from the surrounding content.

I’ve created a handy helper method that you can use to automagically give your text nodes the necessary background.

The addTextBackground helper method

import * as d3 from "d3";
 
interface AddTextBackgroundParams {
    // Target text element
    text: d3.Selection<any>;
    // Element the target was appended to
    container: d3.Selection<any>;
    // Flip the x/y axes of the bounding box, useful when the text element has a rotate transform applied
    invertBoundingBox?: boolean;
    // Number of pixels to shift the background rect on the X axis
    offsetX?: number;
    // Number of pixels to shift the background rect on the Y axis
    offsetY?: number;
}
 
export function addTextBackground({
    text,
    container,
    invertBoundingBox = false,
    offsetX = 0,
    offsetY = 0,
}: AddTextBackgroundParams) {
    const padding = 4; // pixels of padding around text
    if (!text.node()) return;
    let bbox = (text.node() as SVGGraphicsElement).getBBox();
    if (invertBoundingBox)
        bbox = {
            ...bbox,
            x: bbox.y,
            y: bbox.x,
            height: bbox.width,
            width: bbox.height,
        };
    // insert a rect beneath the text to provide a background
    container
        // You might be able to use the text variable here instead of the specific query "text.LabelPrimary"
        // For my purposes, I always wanted to base it from the LabelPrimary
        .insert("rect", "text.LabelPrimary")
        .attr("x", bbox.x - padding + offsetX)
        .attr("y", bbox.y - padding + offsetY)
        .attr("width", bbox.width + padding * 2)
        .attr("height", bbox.height + padding * 2)
        .style("fill", "white");
 

Using the addTextBackground helper

const container = d3.select(this);
const yAxisLabel = container.selectAll('.YAxisLabel').data([0]);
const newYAxisLabel = yAxisLabel
  .enter()
  .append('g')
  .classed('YAxisLabel', true);
newYAxisLabel
  .append('text')
  .attr('text-anchor', 'middle')
  .attr('transform', 'rotate(-90)')
  .classed('LabelPrimary', true);
  .text('some text goes here');
 
// This specific selector is to prevent selecting other LabelPrimary elements
const primaryText = container.selectAll('.YAxisLabel .LabelPrimary');
addTextBackground({
  text: primaryText,
  container: newYAxisLabel,
  invertBoundingBox: true, // this text is rotated so invert the box
});