0

See attached image - How do I center align all the text in the blue rect? I've done it by eye so far, using x, but in real life I won't be able to do this as the text lengths will vary. The original rectangle was drawn in Inkscape.

image showing svg  with rectangle and text

The svg:

<g
  id="g2450">
  <rect
    width="30"
    height="40"
    stroke="#00ffff"
    fill-opacity="0"
    id="sensor-info"
  >
  </rect>
  <g
    id="sensor-info-text"
 transform="matrix(0.51584178,0,0,0.51641502,11.648419,22.062229)" 
  />
</g>

I then append the following in javascript:

let s = document.getElementById ('sensor-info-text');
s.innerHTML = `
  <text
    x="-20" y="-20"
    font-family="Verdana"
    font-size="12px"
    fill="#0000ff">
    <tspan >${sensor.Name}</tspan>
    <tspan x="-5" dy="20px" >${sensor.SetPoint + String.fromCharCode(176) + 'C'}</tspan>
    <tspan x="-5" dy="20px">${sensor.Measurement + String.fromCharCode(176) + 'C'}</tspan>
  </text>
`

I've tried svg text {dominant-baseline:middle;text-anchor:middle;} in the css, as suggested here: https://stackoverflow.com/questions/5546346/how-to-place-and-center-text-in-an-svg-rectangle, but the text "flies off" to the right if I unset e.g. x.

How do I proceed?

minisaurus
  • 1,099
  • 4
  • 17
  • 30
  • 1
    You need a invisible line to place the text on: https://stackoverflow.com/questions/5546346/how-to-place-and-center-text-in-an-svg-rectangle/71085150#71085150 – Danny '365CSI' Engelman Jul 11 '23 at 18:06
  • I suppose that you allways have 3 text lines: _sensor x_ and 2 other lines indicating temperatures. I also suppose that the svg has a viewBox attribute. In this case please try `` Observe the font size is much smaller than yours. For the you can use x="15" placing the tspan text in the middle of the rect. The dy attribute represents the distance relative to the previous tspan. You can try no dy for the first tspan and dy="15" for the next 2 – enxaneta Jul 11 '23 at 19:29
  • thanks @Danny'365CSI'Engelman, I'll give that a try and report back :) – minisaurus Jul 12 '23 at 15:26

3 Answers3

1

You need to calculate suitable x/y coordinates for your <text> elements according to your <rect> elements position.

TL;DR: SVG <text> elements are just a line of text

Sure, we can mimic a HTML block like appearance by adding <tspan> elements with y offsets:
It's still not a proper multiline text-compositing concept – just copy these pseudo-line-wrapped text and paste it to an editor: there won't be any new lines/breaks.

Keeping this (... frustrating fact) in mind:
dominant-baseline can only shift each text/tspan content's baselines.
It won't take the text elements total height as a reference – because we don't have a block like context.

At least we can calculate the bounding box of such a pseudo-text-block.

Here's an example of a labeling helper function:

let rect = document.querySelector("#sensor-info");
let rect2 = document.querySelector("#sensor-info2");
let rect3 = document.querySelector("#sensor-info3");

let text = `Sensor 1 \n 25° \n 22°`;
let text2 = `Sensor 2 \n 25°\n 29° \n 28° \n 22°`;
let text3 = `Sensor 3 \n 25°\n 29° \n 28° \n 22°`;

addTextToRect(rect, text, 5, 6);
addTextToRect(rect2, text2, 5, 6);
addTextToRect(rect3, text3, 5, 6);


function addTextToRect(rect, text, fontSize = 5, lineHeight = 6, seperator=" \n ") {
  let ns = "http://www.w3.org/2000/svg";
  let svg = rect.closest("svg");
  let { x, y, width, height } = getBBoxTransform(rect);

  // get rect centroid
  let cx = x + width / 2;
  let cy = y + height / 2;

  // split text in lines
  let lines = text.split(seperator);

  // calculate text y offset according to lines
  let offY = ((lines.length - 1) / 2) * lineHeight;

  let textEl = document.createElementNS(ns, "text");
  textEl.setAttribute("x", cx);
  textEl.setAttribute("y", cy - offY);
  textEl.setAttribute("text-anchor", "middle");
  textEl.setAttribute("dominant-baseline", "middle");
  textEl.setAttribute("font-size", fontSize);
  textEl.textContent = lines[0];

  //add lines
  for (let i = 1; i < lines.length; i++) {
    let tspan = document.createElementNS(ns, "tspan");
    tspan.textContent = lines[1];
    tspan.setAttribute("x", cx);
    tspan.setAttribute("dy", lineHeight);
    textEl.append(tspan);
  }

  svg.append(textEl);
}

function getBBoxTransform(el) {
  let parent = el.farthestViewportElement;
  let bb = el.getBBox();
  // check transformations
  let matrix = parent.getScreenCTM().inverse().multiply(el.getScreenCTM());
  let { a, b, c, d, e, f } = matrix;
  let matrixStr = [a, b, c, d, e, f]
    .map((val) => {
      return +val.toFixed(8);
    })
    .join("");

  // no transformations - return bbox
  if (matrixStr === "100100") {
    console.log("default bbox");
    return bb;
  } else {
    let ns = "http://www.w3.org/2000/svg";
    let svg = el.closest("svg");
    
    // transform bbox
    let p0 = {x: bb.x,y: bb.y};
    let p1 = {x: bb.x + bb.width,y: bb.y};
    let p2 = {x: bb.x, y: bb.y + bb.height};
    let p3 = {x: bb.x+bb.width, y: bb.y + bb.height};
    
    let pts = [p0, p1, p2, p3];
    let ptsTrans = [];
    
    let xAll = [];
    let yAll = [];
    pts.forEach((p) => {
      let pt = svg.createSVGPoint();
      pt.x = p.x;
      pt.y = p.y;
      pt = pt.matrixTransform(matrix);
      xAll.push(pt.x)
      yAll.push(pt.y)
    });
    
    let xMin = Math.min(...xAll);
    let xMax = Math.max(...xAll);
    let yMin = Math.min(...yAll);
    let yMax = Math.max(...yAll);
    
    bb = {
      x: xMin,
      y: yMin,
      width: xMax-xMin,
      height: yMax-yMin
    };
    
    //renderPoint(svg, [bb.x, bb.y], 'green')
    //renderPoint(svg, [bb.x, bb.y+bb.height], 'magenta')
  }
  return bb;
}
<svg viewBox="0 0 100 100">
  <g id="g2450" transform="matrix(0.51584178,0,0,0.51641502,11.648419,22.062229)">
    <rect width="30" height="40" stroke="#00ffff" fill-opacity="0" id="sensor-info" />
    <rect transform="translate(40 -40) scale(1.2)" x="50" y="50" width="50" height="100" stroke="#00ffff" fill-opacity="0" id="sensor-info2" />
  </g>
  <rect transform="rotate(45 50 50)" x="30" y="50" width="30" height="30" stroke="#00ffff" fill-opacity="0" id="sensor-info3" />
</svg>

How it works

  • get bounding box of a <rect> and calculate a centroid/center point via custom getBBoxTransform(el) helper. This function will calculate a bounding box also respecting transformations (e.g inherited from parent groups)

  • generate <tspan> elements for pseudo-lines (by splitting text content according to a defined seperator like " \n "

  • count lines and calculate the <text> y attribute value

    let offY = ((lines.length - 1) / 2) * lineHeight;
    textEl.setAttribute("y", cy - offY);
    

You'll need to adjust font-sizes to get the desired result.

»Lost in transformations«

It's always nice to have readable x/y values – unnecessary transformations can complicate this.
Sure, transforms can be pretty handy – especially for animating. Unfortunately, a lot of graphic applications and data visualization libraries tend to introduce way too many – sometimes none-sense transformations resulting in highly incomprehensible positioning attributes/property values.

**Concider un-grouping some elements ** (unnecessary groups are annoying as well) to strip/reduce transformations. See "Removing transforms in SVG files"

herrstrietzel
  • 11,541
  • 2
  • 12
  • 34
0

This alternative works for the first <rect> from HerrStrietzel his answer because the <path> and <text> are injected in the <g> where the transform is applied.

Does not work for the second <rect> where the transform is applied on the rect itself.

document.querySelector("svg")
.querySelectorAll("rect").forEach(rect=>{
  let {x,y,width,height} = rect.getBBox();
  let dx = width-x;
  let dy = height-y;
  let d = `M${x} ${y+dy/2}H${width}`;
  let text = "Sensor1";
  let id = new Date()/1;
  console.log(x,y,width,height,dx,dy,"\n",d);
  rect.insertAdjacentHTML("afterend",`<path stroke="red" id="P${id}" d="${d}"/>
  <text>
    <textPath href="#P${id}"
              font-size="9px"
              startoffset="50%" 
              dominant-baseline="middle"
              text-anchor="middle">${text}</textPath>
  </text>`);
})
<svg viewBox="0 0 100 100">
  <g id="g2450" transform="matrix(0.51584178,0,0,0.51641502,11.648419,.062229)">

    <rect width="30" height="40" stroke="#00ffff" fill-opacity="0" id="sensor-info" />

    <rect transform="translate(40 -40) scale(1.2)" x="50" y="50" width="50" height="100" stroke="#00ffff" fill-opacity="0" id="sensor-info2" />

  </g>
</svg>
Danny '365CSI' Engelman
  • 16,526
  • 2
  • 32
  • 49
0

The best and simplest for me was the earlier tip in the comments from Danny' 365CSI' Engelman referencning this answer; i.e. creating an invisible line to then draw the text "along".

I did this:

<svg >
  <rect x="00" y="00" width="30" height="40" fill="aqua"></rect>
  <path id="p1" pathLength="2" d="M0 8h 30" stroke="none"/>
  <path id="p2" pathLength="2" d="M0 20h 30" stroke="none"/>
  <path id="p3" pathLength="2" d="M0 31h 30" stroke="none"/>
  <text>
    <textPath href="#p1" startoffset="1" text-anchor="middle" dominant-baseline="middle" font-family="Verdana" font-size="8" fill="blue" >GT11</textPath>
    <textPath href="#p3" startoffset="1" text-anchor="middle" dominant-baseline="middle" font-family="Verdana" font-size="8" fill="blue" >21&#176;C</textPath>
    <textPath href="#p2" startoffset="1" text-anchor="middle" dominant-baseline="middle" font-family="Verdana" font-size="8" fill="fuschia" >26&#176;C</textPath>
  </text>
</svg>

Works a treat, thank you for the help.

minisaurus
  • 1,099
  • 4
  • 17
  • 30