SVG <text> y position

Screenshot

I’m writing a population pyramid using d3 + react. I’m using d3 for calculations and react for DOM rendering. Everything is working out pretty great, except for positioning the y-axis labels. Ideally, the first label would be positioned either along the axis, or center aligned in the bandwidth for that range. Or put another way, center-aligned around the same meridian as its respective bar.

The following code represents the bar graph on the right and the text labels. The rendered text labels’ height is only 26px, and I understand the <text> element does not have a height attribute, and that the height of <text> elements can only be controlled with font size. However, I do expect that the y position of each label is derived from the y attribute. As you can see the y values for both the <text> and related <rect> elements are equal.

React

<g>
          {Array.from(ageGroups, ([k]) => (
            <text
              key={`yTick-${k}`}
              x={xStart - 12}
              y={yScale(k) as number}
              textAnchor="end"
              stroke="currentColor"
            >
              {k}
            </text>
          ))}
        </g>
        {/* bars */}
        {Array.from(ageGroups, ([k, v], i) => {
          return (
            <rect
              key={`${i}_bar_female`}
              x={xStart}
              y={yScale(k) as number}
              width={xScale(v.female) - xStart}
              height={yScale.bandwidth()}
              className={styles.red}
            />
          )
        })}

Markup

<svg preserveAspectRatio="xMinYMin meet" viewBox="0 0 255.33333333333334 214.66666666666666" role="img">
<rect x="54" y="186.96713615023472" width="0" height="24.92957746478873" class="Members_red__jJOEn"></rect>
  <g>
    <text x="42" y="20.7699530516432" text-anchor="end" stroke="currentColor">85-94</text>
    <text x="42" y="48.46948356807512" text-anchor="end" stroke="currentColor">75-84</text>
    <text x="42" y="76.16901408450704" text-anchor="end" stroke="currentColor">65-74</text>
    <text x="42" y="103.86854460093896" text-anchor="end" stroke="currentColor">55-64</text>
    <text x="42" y="131.56807511737088" text-anchor="end" stroke="currentColor">45-54</text>
    <text x="42" y="159.26760563380282" text-anchor="end" stroke="currentColor">35-44</text>
    <text x="42" y="186.96713615023472" text-anchor="end" stroke="currentColor">25-34</text>
  </g>
  <rect x="54" y="20.7699530516432" width="6.114942528735632" height="24.92957746478873" class="Members_red__jJOEn"></rect>
  <rect x="54" y="48.46948356807512" width="12.229885057471265" height="24.92957746478873" class="Members_red__jJOEn"></rect>
  <rect x="54" y="76.16901408450704" width="73.3793103448276" height="24.92957746478873" class="Members_red__jJOEn"></rect>
  <rect x="54" y="103.86854460093896" width="48.91954022988506" height="24.92957746478873" class="Members_red__jJOEn"></rect>
  <rect x="54" y="131.56807511737088" width="24.45977011494253" height="24.92957746478873" class="Members_red__jJOEn"></rect>
  <rect x="54" y="159.26760563380282" width="0" height="24.92957746478873" class="Members_red__jJOEn"></rect>
  <rect x="54" y="186.96713615023472" width="0" height="24.92957746478873" class="Members_red__jJOEn"></rect>
</svg>

I fiddled with this all day yesterday, trying nested SVGs, and adjusting different ways of rendering the size of the svg (viewBox v width/height), and even tried pulling the labels out into their own component. In all variations, the text labels are positioned closer to the top of the page than the bars. I saw a slight improvement increasing the min value that is passed to yScale, but it never truly aligned the text labels with the bars in the graph.

What am I missing here?

Here’s the d3 yScale var, where ageGroups type is Map<string, { // ... object properties}>

const yScale = scaleBand()
    .domain(Array.from(ageGroups, ([k]) => k))
    .range([0, height])
    .padding(0.1)

>Solution :

Don’t you want the y co-ordinate of the text to be in-between those of the bars?

The rect is only giving the y coordinate of one side of the bar: there is another y coordinate that is that y + the height. Presumably you want the text to be centred on the midpoint between those two y coordinates?

i.e.

y={(yScale(k)+ yScale.bandwidth()/2) as number}

That would be the simplest solution.

If that doesn’t work, look at the property dominant-baseline

Here is an example:

<svg width="500" height="100" style="outline: 1px solid blue; overflow:visible ">
<text x="0" y="0">SVG</text>
<text x="100" y="0" dominant-baseline="hanging">SVG (hanging)</text>
<text x="300" y="0" dominant-baseline="mathematical">SVG (mathematical)</text>
<text x="0" y="100" dominant-baseline="central">SVG (central)</text>
<text x="200" y="100" dominant-baseline="middle">SVG (middle)</text>
<text x="400" y="100" dominant-baseline="text-before-edge">SVG (text-before-edge)</text>
</svg>

Leave a Reply