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
});