amos
amos

Reputation: 5402

How do I align svg foreignObject with a text element?

I want to add a checkbox with a label to my d3 svg. I'd like them to be in separate g elements because for some reason, the font rendering doesn't match the rest of the svg if the label is in the foreignObject (irrelevant to my question, although I'd be happy with an explanation there to that too).

However, I can't figure out how to get them to align vertically. Yes, I could introduce some fudge factor, but it seems like that would only align for one particular size of the svg, which can change. How do I do it?

let y=40;
svg.append("g")
    .append("foreignObject")
    .attr("x", "4ex")
    .attr("y", y)
    .attr("width", 75)
    .attr("height", 25)
    .append("xhtml:body")
    .html("<form><input type=checkbox id=check/>text1</form>");
//seems I must render this outside the foreignObject in order to match font rendering of the svg
svg.append("g")
    .append("text")
    .attr("y", y)
    .attr("height", 25)
    .attr("x", "7ex")
    .text("text2")

That is, I'd like the text to line up with the checkbox as if the text had been in the foreignObject's html. It seems like I should be able to just set the same value of y for both.

misaligned elements

http://jsfiddle.net/48rdrp6a/2/

If it's a matter of setting some vertical-align, I can't figure out where to put it.

Upvotes: 1

Views: 4318

Answers (1)

mmlr
mmlr

Reputation: 2145

The main problem is that the y attribute on the foreignObject specifies the top of the box, while the y attribute on the text specifies the baseline of the text.

In the given example the foreignObject will therefore start at 40 and grow downwards to 65, while the text will have its baseline start at 40 with the ascends and descends going upwards and downwards from 40 respectively.

This behaviour can be changed through the alignment-baseline property. Using before-edge moves the EM box so that y specifies the top edge for the text as well, making both elements compatible.

Additional to that fundamental difference there is a problem with browser default styling. The body in the foreignObject will have a browser specific default styling, which may offset the form and checkbox, bringing it out of alignment again. In Chrome this for example adds 8px of margin.

var svg = d3.select("#chart").append("svg")
  .attr("width", 200)
  .attr("height", 200);

let y = 40;
svg.append("g")
  .append("foreignObject")
  .attr("x", "4ex")
  .attr("y", y)
  .attr("width", 75)
  .attr("height", 25)
  .append("xhtml:body")
  .html("<form><input type=checkbox id=check/></form>");

svg.append("g")
  .append("text")
  .attr("y", y)
  .attr("height", 25)
  .attr("x", "6ex")
  .text("text2")

// svg repositioning
$("svg").css({
  top: 200,
  left: 200,
  position: 'absolute'
});
#chart {
  border: 1px solid green;
}

svg {
  background: #AABBFF;
  border: 2px solid #8899EE;
}

text {
  alignment-baseline: before-edge;
}

foreignObject body {
  all: unset;
  display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

<div id="chart"></div>

I have used CSS to set the alignment-baseline, but using the corresponding attribute would have worked as well.

For counteracting the body default styling I've used a combination of all: unset to remove all browser styling and then restoring display: block.

Upvotes: 3

Related Questions