Tskit: draw_svg(): converting svg to pdf loses text positioning and node circle size

Created on 11 Aug 2020  Â·  45Comments  Â·  Source: tskit-dev/tskit

Between tskit 0.2.3 and 0.3.0.b1, the svg output format has changed to use more advanced css styling. Unfortunately, it seems many svg readers/converters such as inkscape and rsvg-convert don't understand everything. In particular the text positioning is off and the node circles seem too small. I was unable to obtain an svg->pdf conversion that did the right thing for an svg created with 0.3.0.b1.

import os, tskit, msprime
ts = msprime.simulate(sample_size=5, random_seed=12345)
path = f"/tmp/tree-{tskit.__version__}"
ts.first().draw_svg(f"{path}.svg")
os.system(f"inkscape -o {path}.pdf {path}.svg")

tree-0.2.3.pdf
tree-0.3.0.b1.pdf

For the 0.3.0.b1 version, inkscape 1.0 (4035a4fb49, 2020-05-01) complains thusly:

** (org.inkscape.Inkscape:303264): WARNING **: 17:31:11.784: Unimplemented style property 121
** (org.inkscape.Inkscape:303264): WARNING **: 17:31:11.784: Unimplemented style property 13

tree-0.2.3.svg

<?xml version="1.0" encoding="utf-8" ?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink" baseProfile="full" height="200" version="1.1" width="200">
  <defs/>
  <g id="tree_0">
    <g fill="none" id="edges" stroke="black">
      <path d="M 62.5 134.05269443459176 V 30.0 H 103.75" id="edge_0_6"/>
      <path d="M 40.0 170.0 V 134.05269443459176 H 62.5" id="edge_0_4"/>
      <path d="M 85.0 136.95934339331913 V 134.05269443459176 H 62.5" id="edge_0_5"/>
      <path d="M 70.0 170.0 V 136.95934339331913 H 85.0" id="edge_0_0"/>
      <path d="M 100.0 170.0 V 136.95934339331913 H 85.0" id="edge_0_3"/>
      <path d="M 145.0 124.55471853350159 V 30.0 H 103.75" id="edge_0_7"/>
      <path d="M 130.0 170.0 V 124.55471853350159 H 145.0" id="edge_0_1"/>
      <path d="M 160.0 170.0 V 124.55471853350159 H 145.0" id="edge_0_2"/>
    </g>
    <g id="symbols">
      <g class="nodes">
        <circle cx="103.75" cy="30.0" id="node_0_8" r="3"/>
        <circle cx="62.5" cy="134.05269443459176" id="node_0_6" r="3"/>
        <circle cx="40.0" cy="170.0" id="node_0_4" r="3"/>
        <circle cx="85.0" cy="136.95934339331913" id="node_0_5" r="3"/>
        <circle cx="70.0" cy="170.0" id="node_0_0" r="3"/>
        <circle cx="100.0" cy="170.0" id="node_0_3" r="3"/>
        <circle cx="145.0" cy="124.55471853350159" id="node_0_7" r="3"/>
        <circle cx="130.0" cy="170.0" id="node_0_1" r="3"/>
        <circle cx="160.0" cy="170.0" id="node_0_2" r="3"/>
      </g>
      <g class="mutations" fill="red"/>
    </g>
    <g font-size="14" id="labels">
      <g class="nodes">
        <g text-anchor="start">
          <text x="90.0" y="131.95934339331913">5</text>
          <text x="150.0" y="119.55471853350159">7</text>
        </g>
        <g text-anchor="middle">
          <text x="103.75" y="25.0">8</text>
          <text x="40.0" y="190.0">4</text>
          <text x="70.0" y="190.0">0</text>
          <text x="100.0" y="190.0">3</text>
          <text x="130.0" y="190.0">1</text>
          <text x="160.0" y="190.0">2</text>
        </g>
        <g text-anchor="end">
          <text x="57.5" y="129.05269443459176">6</text>
        </g>
      </g>
      <g class="mutations" font-style="italic">
        <g text-anchor="start"/>
        <g text-anchor="end"/>
      </g>
    </g>
  </g>
</svg>

tree-0.3.0.b1.svg

<?xml version="1.0" encoding="utf-8" ?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink" baseProfile="full" height="200" version="1.1" width="200">
  <defs>
    <style type="text/css"><![CDATA[.axis {font-weight: bold}.tree, .axis {font-size: 14px; text-anchor:middle;}.edge {stroke: black; fill: none}.node > circle {r: 3px; fill: black; stroke: none}.tree text {dominant-baseline: middle}.mut > text.lft {transform: translateX(0.5em); text-anchor: start}.mut > text.rgt {transform: translateX(-0.5em); text-anchor: end}.root > text {transform: translateY(-0.8em)}.leaf > text {transform: translateY(1em)}.node > text.lft {transform: translate(0.5em, -0.5em); text-anchor: start}.node > text.rgt {transform: translate(-0.5em, -0.5em); text-anchor: end}.mut {fill: red; font-style: italic}]]></style>
  </defs>
  <g class="tree t0">
    <g class="node n8 root" transform="translate(111.25 30.0)">
      <g class="node n6 p8" transform="translate(-33.75 104.053)">
        <g class="leaf node n4 p6 sample" transform="translate(22.5 35.9473)">
          <path class="edge" d="M 0 0 V -35.9473 H -22.5"/>
          <circle cx="0" cy="0" r="1"/>
          <text>4</text>
        </g>
        <g class="node n5 p6" transform="translate(-22.5 2.90665)">
          <g class="leaf node n0 p5 sample" transform="translate(-15.0 33.0407)">
            <path class="edge" d="M 0 0 V -33.0407 H 15.0"/>
            <circle cx="0" cy="0" r="1"/>
            <text>0</text>
          </g>
          <g class="leaf node n3 p5 sample" transform="translate(15.0 33.0407)">
            <path class="edge" d="M 0 0 V -33.0407 H -15.0"/>
            <circle cx="0" cy="0" r="1"/>
            <text>3</text>
          </g>
          <path class="edge" d="M 0 0 V -2.90665 H 22.5"/>
          <circle cx="0" cy="0" r="1"/>
          <text class="rgt">5</text>
        </g>
        <path class="edge" d="M 0 0 V -104.053 H 33.75"/>
        <circle cx="0" cy="0" r="1"/>
        <text class="rgt">6</text>
      </g>
      <g class="node n7 p8" transform="translate(33.75 94.5547)">
        <g class="leaf node n1 p7 sample" transform="translate(-15.0 45.4453)">
          <path class="edge" d="M 0 0 V -45.4453 H 15.0"/>
          <circle cx="0" cy="0" r="1"/>
          <text>1</text>
        </g>
        <g class="leaf node n2 p7 sample" transform="translate(15.0 45.4453)">
          <path class="edge" d="M 0 0 V -45.4453 H -15.0"/>
          <circle cx="0" cy="0" r="1"/>
          <text>2</text>
        </g>
        <path class="edge" d="M 0 0 V -94.5547 H -33.75"/>
        <circle cx="0" cy="0" r="1"/>
        <text class="lft">7</text>
      </g>
      <circle cx="0" cy="0" r="1"/>
      <text>8</text>
    </g>
  </g>
</svg>

bug

Most helpful comment

IIUC, the main problem is we'd like a way to turn the SVGs into pdfs, right? Well, if chrome and firefox render the SVGs ok, perhaps we can use them to convert to pdf? I tested this just now, by asking chrome to print-to-pdf a jupyter notebook containing an SVG tree with custom CSS styling. It worked! And, I could then open the pdf in inkscape, and edit further. I also did

import os, tskit, msprime
ts = msprime.simulate(sample_size=5, random_seed=12345)
path = f"/tmp/tree-{tskit.__version__}"
ts.first().draw_svg(f"{path}.svg")

and then

chromium --headless --print-to-pdf=tree.pdf tree-0.3.0b2.svg

and got an SVG I could open up and edit in inkscape and, looked correct:
Screenshot from 2020-08-22 08-44-36

All 45 comments

Thanks @grahamgower, great to know this. This is a significant drawback of using CSS for styling for me - I'll use the code to generate PDFs much more often than I'll use it in the browser.

I'm going to mark it as a bug for now, but others might argue otherwise.

I'll second this as being bug-adjacent. I consider being able to accurately view the output in Inkscape as rather important. I often edit branch colors, etc., using that software.

For the record, krita also doesn't understand the CSS formatting.

On the plus side, if you take an svg from the beta version, modify it in inkscape, it displays the mods properly in a browser.
tree_screenshot

One possible fix is to have a use_css option which defaults to True. It would add complexity in that we'd have to do the positioning manually as well as using CSS. I'm not sure there's much option, as the CSS integration is very powerful for in-browser stuff. We could then document that people who wish to convert to PDF should set use_css=False.

Or, we could make use_css=False the default. It is a power-user feature.

Is the problem all CSS styling? Or just text positioning?

To the best of my understanding, inkscape/krita do not consider the inline css at all when loading a file. Likewise when saving to PDF.

Well, my version of Inkscape does pay attention to some stylesheets. The following is a red square in Inkscape 1.0, I think.

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
   xmlns="http://www.w3.org/2000/svg"
   xmlns:xlink="http://www.w3.org/1999/xlink"
   version="1.1"
   width="150"
   height="150">
  <style type="text/css">rect {fill: red}</style>
    <rect
     width="90"
     height="90"
     x="30"
     y="30"
     />
</svg>

So the node circle problem is simply that SVG 1.1 doesn't actually allow circle radii to be changed using css. That should be easy to fix, I think.

Inkscape 1 is a pretty bleeding edge requirement--not in Ubuntu 20.04 even,
but there is a PPA. It seems to be in RedHat. But good to know that this
may eventually work!

On Fri, Aug 21, 2020 at 12:58 PM Yan Wong notifications@github.com wrote:

Well, my version of Inkscape does pay attention to some stylesheets. The
following is a red square in Inkscape 1.0, I think.


xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1"
width="150"
height="150">

width="90"
height="90"
x="30"
y="30"
/>

—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
https://github.com/tskit-dev/tskit/issues/765#issuecomment-678465855,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/ABQ6OH456GXE6OIIDBBE3J3SB3GQXANCNFSM4P3FXE3Q
.

I've done some digging. I think the issue is twofold. Firstly, some properties such as (for circles) cx, cy, and r are "geometry properties", and can only be styled in CSS2 (which Inkscape doesn't yet support).

Secondly, from my testing, it seems that Inkscape doesn't support css transforms, although it doesn't mind SVG transforms. This explains the text positioning, and also, annoyingly, runs out another way we could have scaled the circles. E.g. Inkscape likes this <circle cx="0" cy="0" r="1" transform="scale(10)"/> but not this <circle cx="0" cy="0" r="1" style="transform: scale(10)"/>.

Given we can't set r in the css, and we can't apply a css transform, I'm trying to think of a way of adjusting e.g. all the circle sizes at once, without the need to change each one individually at each location that it is defined in the SVG file. I can't think of any (other than e.g. changing the stroke width, or something weird like that). Anyone have any ideas?

Inkscape 1 is a pretty bleeding edge requirement--not in Ubuntu 20.04 even, but there is a PPA. It seems to be in RedHat. But good to know that this may eventually work!

Does your Inkscape version display a red circle in my example, or a black one @molpopgen ?

red square.

Inkscape 1 is in the next Debian, it should be in Ubuntu before too long...

red square.

So I think your version will have roughly the same limitations as my Inkscape version 1.0. Seems like we should be able to implement a temporary fix to get this working in recent Inkscape versions. One possibility that springs to mind is that we can define a circle as an SVG group, and apply the single parameter or SVG transform there. The difficulty will be in how to expose to the user the ability to change this stuff - if all you have to do to is to change a stylesheet, we don't need to specify individual parameters such as symbol size, etc. But perhaps we can simply set some sensible defaults for the time being?

Inkscape 1 is in the next Debian, it should be in Ubuntu before too long...

1.0 is proposed for 20.10. Ubuntu package versions are now quite uncoupled from Debian.

Well, the advanced options of draw_svg() are still a work-in-progress, and Inkscape 1 will be more common by the time it's more finalized (eg to make styling easier without having to write the CSS yourself). People can still use the Tree.draw method to get the previous SVG figures. It might make sense to focus our efforts on SVG 1.1, which will be widely supported before too long, if it makes things a lot more elegant?

I think the discussion of Inkscape 1.0 is irrelevant here - it seems to support pretty much the same (SVG 1.1) as Kevin's version of Inkscape, which I guess is about 0.93.

Neither of them support the SVG2 properties that the browser-rendered SVG files allow, so to make the trees look pretty in Inkscape, we will need to do some hacking (until Inkscape supports CSS transforms). The question is how much do we want to allow the user to adjust the defaults, and how much do we say "we'll make it look nice in Inkscape, but (unlike browser-rendered graphics) we won't provide an easy way to change the defaults: you're on your own there".

I've got 0.92. One nice thing (mentioned above) is that it seems you can correctly edit the files in Inkscape and then display them in a browser with all of the fancy CSS bits showing. So the real problem is in creating static content where you edit in inkscape and output for lecture slides or whatnot, and the CSS bits don't show up. Although even for this, certain hacks may be good workarounds?

Oho - looks like we might be able to use entities to define some standard transformations. For example, this way of setting a global circle size works in my version of Inkscape - do you get big red node circles in your version, @molpopgen ?

<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"
[
  <!ENTITY sz "scale(10)">
]>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink" baseProfile="full" version="1.1" width="200">

    <style>.axis {font-weight: bold}.tree, .axis {font-size: 14px; text-anchor:middle;}.edge {stroke: black; fill: none} circle {fill: red;} .tree text {dominant-baseline: middle}.mut > text.lft {transform: translateX(0.5em); text-anchor: start}.mut > text.rgt {transform: translateX(-0.5em); text-anchor: end}.root > text {transform: translateY(-0.8em)}.leaf > text {transform: translateY(1em)}.node > text.lft {transform: translate(0.5em, -0.5em); text-anchor: start}.node > text.rgt {transform: translate(-0.5em, -0.5em); text-anchor: end}.mut {fill: red; font-style: italic}</style>
  <g class="tree t0">
    <g class="node n8 root" transform="translate(111.25 30.0)">
      <g class="node n6 p8" transform="translate(-33.75 104.053)">
        <g class="leaf node n4 p6 sample" transform="translate(22.5 35.9473)">
          <path class="edge" d="M 0 0 V -35.9473 H -22.5"/>
          <circle cx="0" cy="0" r="1" transform="&sz;"/>
          <text>4</text>
        </g>
        <g class="node n5 p6" transform="translate(-22.5 2.90665)">
          <g class="leaf node n0 p5 sample" transform="translate(-15.0 33.0407)">
            <path class="edge" d="M 0 0 V -33.0407 H 15.0"/>
            <circle cx="0" cy="0" r="1" transform="&sz;"/>
            <text>0</text>
          </g>
          <g class="leaf node n3 p5 sample" transform="translate(15.0 33.0407)">
            <path class="edge" d="M 0 0 V -33.0407 H -15.0"/>
            <circle cx="0" cy="0" r="1" transform="&sz;"/>
            <text>3</text>
          </g>
          <path class="edge" d="M 0 0 V -2.90665 H 22.5"/>
          <circle cx="0" cy="0" r="1" transform="&sz;"/>
          <text class="rgt">5</text>
        </g>
        <path class="edge" d="M 0 0 V -104.053 H 33.75"/>
        <circle cx="0" cy="0" r="1" transform="&sz;"/>
        <text class="rgt">6</text>
      </g>
      <g class="node n7 p8" transform="translate(33.75 94.5547)">
        <g class="leaf node n1 p7 sample" transform="translate(-15.0 45.4453)">
          <path class="edge" d="M 0 0 V -45.4453 H 15.0"/>
          <circle cx="0" cy="0" r="1" transform="&sz;"/>
          <text>1</text>
        </g>
        <g class="leaf node n2 p7 sample" transform="translate(15.0 45.4453)">
          <path class="edge" d="M 0 0 V -45.4453 H -15.0"/>
          <circle cx="0" cy="0" r="1" transform="&sz;"/>
          <text>2</text>
        </g>
        <path class="edge" d="M 0 0 V -94.5547 H -33.75"/>
        <circle cx="0" cy="0" r="1" transform="&sz;"/>
        <text class="lft">7</text>
      </g>
      <circle cx="0" cy="0" r="1" transform="&sz;"/>
      <text>8</text>
    </g>
  </g>
</svg>

yan

Success (?), along with the usual Inkscape issue where the bounding box just ain't right.

To digress a little, I ended up generating some tikz code to do what I wanted. I'll leave it here in case its useful to someone. Inkscape reads the resulting pdf just fine, and it also seems to be reasonably easy to convert to svg using dvisvgm (see pgfmanual.pdf regarding SVG output).

import string
import textwrap


def draw_tikz(tree, tree_height_scale="rank", aspect=16/9, scale=4, node_size=1):
    """
    Return a string containing latex/tikz commands to draw a tskit.Tree.

    Save the string to a file (e.g. tree.tex) and include it in your latex
    document with ``\input{tree.tex}``.
    """
    template = textwrap.dedent(
        """\
        \\begin{tikzpicture}[
            scale=$scale,
            edge/.style = {
                draw,
                thick,
            },
            node/.style = {
                circle,
                fill=red,
                line width=1,  % border
                % radius
                minimum size=$node_size mm,
                inner sep=0,
            },
            leaftext/.style = {
                below
            },
        ]
            \\foreach \\name/\\x/\\y in {$node_coords}
                \\node[node] (\\name) at (\\x, \\y) {};
            \\foreach \\a/\\b in {$edges}
                \\path[edge] (\\a) |- (\\b);
            \\foreach \\name/\\text in {$leaf_nodes_text}
                \\node[leaftext] at (\\name) {\\text};
        \\end{tikzpicture}\
        """
    )

    root_time = tree.time(tree.root)
    num_leaves = sum(1 for _ in tree.leaves())
    leaf_x_inc = aspect / num_leaves
    leaf_x = 0
    edges = []
    x_coords = dict()
    y_coords = dict()
    for node in tree.nodes(order="postorder"):
        if tree.is_leaf(node):
            x = leaf_x
            leaf_x += leaf_x_inc
        else:
            edges.extend([f"n{u}/n{node}" for u in tree.children(node)])
            children_x = [x_coords[u] for u in tree.children(node)]
            x = (children_x[0] + children_x[-1]) / 2
        x_coords[node] = x
        y_coords[node] = tree.time(node) / root_time

    if tree_height_scale == "rank":
        nodes = sorted(y_coords.keys(), key=y_coords.__getitem__)
        y_coords = {
            node: (i - num_leaves) / num_leaves if i > num_leaves else 0
            for i, node in enumerate(nodes, 1)
        }
    elif tree_height_scale != "time":
        raise ValueError(f"unknown tree_height_scale={tree_height_scale}")

    node_coords = [
        f"n{node}/{x_coords[node]}/{y_coords[node]}" for node in tree.nodes()
    ]

    return string.Template(template).substitute(
        scale=scale,
        node_size=node_size,
        node_coords=", ".join(node_coords),
        edges=", ".join(edges),
        leaf_nodes_text=", ".join(f"n{u}/{u}" for u in tree.leaves()),
    )


if __name__ == "__main__":
    import msprime

    ts = msprime.simulate(10, Ne=1000, random_seed=123)
    tree = ts.first()
    s = draw_tikz(tree)
    print(s)

Success (?), along with the usual Inkscape issue where the bounding box just ain't right.

Yes, I didn't bother to make it fit right, sorry. So I think the solution is to use XML entities to define some default transforms which get applied to text as transform attributes, and some default sizes which get applied to circles and squares. If any CSS styles are defined, they will overwrite the attributes, so we keep the previous behaviour. A suggestion is attached, which I think should work for @molpopgen 's Inkscape (it does for mine)

I think

  1. We can use this approach as the default, without having to provide a "use_svg_1" parameter, as this will work perfectly well in browsers too.
  2. We should not give the user a default way to change these other than by providing alternative CSS styles (they can hack the SVG if need be - the definitions will all be at the top of the file).
  3. We should remove the current transform styles settings from the default stylesheet, and allow the attributes to set the default position. The slight down side to this is that if anyone wants to change the position of text or size of symbols, they will need to realise that whatever they do will override the default settings, rather than add to them. So e.g. if they want to rotate the bottom text by 90 degrees, they can't just apply the style .leaf > text {rotate(90deg)} but will need to recapitulate the downwards shift which is normally applied by default, i.e. .leaf > text {transform: translateY(12px) rotate(90deg)}. Of course rotation like this won't be visible in Inkscape, as the CSS transforms will be ignored, but you can't have everything.

Does this sound like an acceptable compromise? It makes the SVG a little larger, but I don't think we care.

<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"
[
  <!-- default settings for SVG 1.1, which can be overridden by CSS specs in SVG 2 -->
  <!ENTITY r "3"><!-- Default circle radius -->
  <!ENTITY tB "translate(0 12)"><!-- Bottom -->
  <!ENTITY tT "translate(0 -11)"><!-- Top -->
  <!ENTITY tR "translate(10 0)"><!-- Right -->
  <!ENTITY tL "translate(-10 0)"><!-- Left -->
  <!ENTITY tTL "translate(2 -8)"><!-- Top Left -->
  <!ENTITY tTR "translate(-2 -8)"><!-- Top Right -->
]>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink" baseProfile="full" height="200" version="1.1" width="200">
  <defs>
    <style type="text/css"><![CDATA[.axis {font-weight: bold}.tree, .axis {font-size: 14px; text-anchor:middle;}.edge {stroke: black; fill: none}.node > circle {fill: black; stroke: none}.tree text {dominant-baseline: middle}.mut > text.lft {text-anchor: start}.mut > text.rgt {text-anchor: end}.leaf > text {transform: translateY(12px) rotate(90deg)}.node > text.lft {text-anchor: start}.node > text.rgt {text-anchor: end}.mut {fill: red; font-style: italic}]]></style>
  </defs>
  <g class="tree t0">
    <g class="node n8 root" transform="translate(111.25 30.0)">
      <g class="node n6 p8" transform="translate(-33.75 104.053)">
        <g class="leaf node n4 p6 sample" transform="translate(22.5 35.9473)">
          <path class="edge" d="M 0 0 V -35.9473 H -22.5"/>
          <circle cx="0" cy="0" r="&r;"/>
          <text transform="&tB;">4</text>
        </g>
        <g class="node n5 p6" transform="translate(-22.5 2.90665)">
          <g class="leaf node n0 p5 sample" transform="translate(-15.0 33.0407)">
            <path class="edge" d="M 0 0 V -33.0407 H 15.0"/>
            <circle cx="0" cy="0" r="&r;"/>
            <text transform="&tB;">0</text>
          </g>
          <g class="leaf node n3 p5 sample" transform="translate(15.0 33.0407)">
            <path class="edge" d="M 0 0 V -33.0407 H -15.0"/>
            <circle cx="0" cy="0" r="&r;"/>
            <text transform="&tB;">3</text>
          </g>
          <path class="edge" d="M 0 0 V -2.90665 H 22.5"/>
          <circle cx="0" cy="0" r="&r;"/>
          <text class="rgt" transform="&tTR;">5</text>
        </g>
        <path class="edge" d="M 0 0 V -104.053 H 33.75"/>
        <circle cx="0" cy="0" r="&r;"/>
        <text class="rgt" transform="&tTR;">6</text>
      </g>
      <g class="node n7 p8" transform="translate(33.75 94.5547)">
        <g class="leaf node n1 p7 sample" transform="translate(-15.0 45.4453)">
          <path class="edge" d="M 0 0 V -45.4453 H 15.0"/>
          <circle cx="0" cy="0" r="&r;"/>
          <text transform="&tB;">1</text>
        </g>
        <g class="leaf node n2 p7 sample" transform="translate(15.0 45.4453)">
          <path class="edge" d="M 0 0 V -45.4453 H -15.0"/>
          <circle cx="0" cy="0" r="&r;"/>
          <text transform="&tB;">2</text>
        </g>
        <path class="edge" d="M 0 0 V -94.5547 H -33.75"/>
        <circle cx="0" cy="0" r="&r;"/>
        <text class="lft" transform="&tTL;">7</text>
      </g>
      <circle cx="0" cy="0" r="&r;"/>
      <text transform="&tT;">8</text>
    </g>
  </g>
</svg>

Yes, I didn't bother to make it fit right, sorry.

Not an issue. The command line is smart enough to distinguish the drawing from the page:

inkscape  --export-dpi=300 --export-area-drawing --export-png="test.png" hotdamn.svg 

I get this out of your most recent example w/Inkscape 0.92:

yan2

That's what you want, right?

Depends, as I don't know if it is what your code says. I have no idea what it means, and I just copy/pasted it into a file.

But the look is correct, I think?

Yeah, that looks great! Thanks @hyanwong! I was worried this would require a solution that discarded all the brilliant flexibility you'd built in, but this seems to do the job nicely.

I'm just checking in I'm allowed to include the <!ENTITY r "3"> definitions within the <svg> tag, so we can embed the svg e.g. in Jupiter notebooks. You don't happened to know, do you @grahamgower ?

(NB - recursive definition attacks: https://www.fortinet.com/blog/threat-research/scalable-vector-graphics-attack-surface-anatomy - yay!)

Yes, definitely looks good.

On Sat, Aug 22, 2020, 6:44 AM Yan Wong notifications@github.com wrote:

But the look is correct, I think?

—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/tskit-dev/tskit/issues/765#issuecomment-678642991,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/ABQ6OH26BCCZZQA4HEGSWD3SB7DQZANCNFSM4P3FXE3Q
.

I'm just checking in I'm allowed to include the <!ENTITY r "3"> definitions _within_ the <svg> tag, so we can embed the svg e.g. in Jupiter notebooks. You don't happened to know, do you @grahamgower ?

Sorry, no, this is well outside my expertise.

(NB - recursive definition attacks: https://www.fortinet.com/blog/threat-research/scalable-vector-graphics-attack-surface-anatomy - yay!)

So this must be why github, stackoverflow, etc. don't permit svg images (in user uploaded content)?

So this must be why github, stackoverflow, etc. don't permit svg images (in user uploaded content)?

I think probably not - it should be easy to limit recursion etc. I think it's just that, because it's essentially a language to interpret, there are problems with both compatibility and other possible attack vectors. And (sadly) it's not so common that it's worth the effort to plug all the holes.

[EDIT - this left here for reference. I think the solution I describe a few comments further down is the most pragmatic approach]

So (annoyingly/predictably), it's a bit more complex, and I think some decisions will have to be made here. Please jump in with suggestions. The (rather involved) explanation is below.

The main issue is that to position the labels below or to the right etc of the nodes, I think we should define a small set of transforms and reuse them. That means we can apply the same transform e.g. to all the labels that are marked as leaves, rather than redeclaring the same transformation over and over again, every time we draw a leaf. If we go down the redeclaring route, then it becomes pretty tricky to allow users to adjust the position of all the leaf labels in one go (we'd need to keep the state ourselves, and therefore have a bespoke way to set it).

In SVG 2, you can store a transform in the CSS style file, which is how the code works currently. However, in SVG 1.1 (i.e. Inkscape) I can't find a way to store SVG transforms for reuse (you can store complete graphical elements for reuse via the <use> tag, but I can't see how that would work for labels which have different text content).

My solution (above) was to define a transform via the XML <!ENTITY ...> method. Unfortunately, that can only be used at the top of a standalone document. That's fine when editing a standalone SVG file (e.g. in Inkscape), but not so good if you are embedding the file e.g. in an HTML page (or presumably a Jupiter notebook).

One slightly nasty possibility is to have a switch that creates either an SVG1.1 standalone file (with an <xml> header) or an SVG2 file that can be either be standalone or embedded in a page (but which would not look nice in Inkscape). Thoughts?

It almost sounds like it might almost be easier to implement the required features in inkscape.

IIUC, the main problem is we'd like a way to turn the SVGs into pdfs, right? Well, if chrome and firefox render the SVGs ok, perhaps we can use them to convert to pdf? I tested this just now, by asking chrome to print-to-pdf a jupyter notebook containing an SVG tree with custom CSS styling. It worked! And, I could then open the pdf in inkscape, and edit further. I also did

import os, tskit, msprime
ts = msprime.simulate(sample_size=5, random_seed=12345)
path = f"/tmp/tree-{tskit.__version__}"
ts.first().draw_svg(f"{path}.svg")

and then

chromium --headless --print-to-pdf=tree.pdf tree-0.3.0b2.svg

and got an SVG I could open up and edit in inkscape and, looked correct:
Screenshot from 2020-08-22 08-44-36

There are two desired outcomes. One is the conversion to png/pdf. The other is to make the rendering of the svg file match its intent with all reasonably common viewers.

The other is to make the rendering of the svg file match its intent with all reasonably common viewers.

Well, the goal of supporting "all reasonably common viewers" has to be balanced against the goal of having a spiffy tree visualization tool. If we can do this stuff much better with SVGs that work with web browsers only, then that seems OK to me.

Thanks for the testing @petrelharp - I think that makes everything a lot easier. I guess we could close this issue (or at least not mark it as a bug), but @molpopgen's point is a fair one. So here's my suggestion:

We hard-code some sensible defaults for the transforms directly into each label (and similarly for point sizes), e.g. each label will look like <text class="lft" transform="translate(-10 0)">2</text>, and remove those transforms from the css. This will make the file slightly larger (each label will add another 30 bytes to the file) but will mean that the SVG should look the same in Inkscape / SVG1.1 viewers as in browsers. However, if users want to adjust either the label positions/rotations or the symbol sizes via tskit commands, they will have to do it by changing the css stylesheet (which will override the hard-coded defaults). This particular class of user transformation won't appear to have an effect in Inkscape (although other adjustments like changing colours will work). We document this, and say that if any Inkscape users want e.g. rotated labels they will have to go via the chromium route.

I think this is a very reasonable compromise - shall I code it up?

[addendum: it would also be trivial to add an extra parameter to draw_svg() to specify symbol_size - I suggest we do this as I need to specify a default value somewhere anyway, so we might as well make it user-adjustable]

Well, the goal of supporting "all reasonably common viewers" has to be balanced against the goal of having a spiffy tree visualization tool. If we can do this stuff much better with SVGs that work with web browsers only, then that seems OK to me.

I think the surprise, for me at least, was that the changes introduced a regression, breaking existing renderings. This stuff is hard, though. (R doesn't get it right, neither.)

We hard-code some sensible defaults for the transforms directly into each label (and similarly for point sizes)... We document this, and say that if any Inkscape users want e.g. rotated labels they will have to go via the chromium route.

I think this is a very reasonable compromise - shall I code it up?

Now coded up in #790. You can test it using something like:

python3 -c 'import msprime; print(msprime.simulate(6, mutation_rate=3, recombination_rate=1, random_seed=2).draw_svg())' > ts.svg

To see a partially Inkscape-incompatible SVG, and test the chromium thing, try:

python3 -c 'import msprime; print(msprime.simulate(6, mutation_rate=3, recombination_rate=1, random_seed=2).draw_svg(style=".tree .node.leaf > .lab {transform: translateY(0.5em) rotate(90deg); text-anchor: start} .tree .node.leaf > .sym {fill: blue; r: 5}.tree .node:not(.sample) > .lab {visibility: hidden}"))' > ts.svg

Also I just realised that it would be possible to e.g. rotate text labels for Inkscape by passing something like {id: {'transform':'rotate(90)'} for id in ts.samples()} as the node_label_attrs param

Was this page helpful?
0 / 5 - 0 ratings

Related issues

daniel-goldstein picture daniel-goldstein  Â·  8Comments

jeromekelleher picture jeromekelleher  Â·  4Comments

bhaller picture bhaller  Â·  6Comments

petrelharp picture petrelharp  Â·  6Comments

jeromekelleher picture jeromekelleher  Â·  8Comments