toytree icon indicating copy to clipboard operation
toytree copied to clipboard

Return ToytreeMark.markers list with edge and node marks for optional legend construction

Open StuntsPT opened this issue 3 years ago • 4 comments

This is a help request, but I cannot seem to find in the docs how to do this. I want to add a legend to my tree, to explain what each mode colour means (they have different colours based on support value). I have looked at toytree's docs, and realized I had to add it via toyplot directly. So far so good. I dove into pyplot's docs, and found this: https://toyplot.readthedocs.io/en/stable/labels-and-legends.html#canvas-legends and this: https://toyplot.readthedocs.io/en/stable/toyplot.canvas.html#toyplot.canvas.Canvas.legend

This led me to the canvas object that is returned by

canvas, axes, mark = tre.draw()

In order to draw a legend I can use toyplot's API and call canvas.legend(). This requires (at least) a list of entries each being a tuple containing a custom string and a marker/mark object. If I prove it the mark object returned by tre.draw(), I can see the text string being draw in the canvas, but what I can't seem to figure out is how can I call each of the node styles present in my tree in order to label each of them with a custom string.

Can you please assist me in obtaining this information?

Thank you very much in advance!

StuntsPT avatar Jun 10 '21 00:06 StuntsPT

I have further figured out that mark.markers (from tre.draw()) is returning an empty list (is this intended?). Also, marker from canvas.legend([("text", marker)]) expects a string that looks like XML

>>> m2.markers[0]
<marker shape='s' mstyle='fill:rgb(0%,50.2%,0%);fill-opacity:1.0;opacity:1;stroke:rgb(0%,50.2%,0%);stroke-opac
ity:1.0'/>

But I can't find anything on mark from tre.draw() that looks like this. What am I missing?

StuntsPT avatar Jun 10 '21 01:06 StuntsPT

Ok, so I was unable to obtain a marker object from toytree, but I managed to work around the issue by using toyplot's marker.create() to create a marker object using the variables I had originally used to define the node markers in toytree like this:

markers = [toyplot.marker.create(shape="o", size=18, mstyle={"fill": x}) for x in my_palette]

legend_markers = list(zip(bootstraps, markers))

canvas.legend(legend_markers,
              corner=("top-right", 20, 170, 140))

Where my_palette and bootstraps contain the colours and intervals used on toytree nodes. If you find this important enough, I can easily add it to toytree's documentation (just let me know where I should place it).

StuntsPT avatar Jun 14 '21 15:06 StuntsPT

Hey @StuntsPT,

Great solution!

The legend creation in toyplot is not super intuitive. Saving the marker objects and passing them to the legend function is easiest, but not always doable when you have complex plots. Your approach of making custom marker objects is a great solution.

I was very back-and-forth when designing toytree in deciding whether to return portions of tree drawings as a collection of separate toyplot marker objects (e.g., a graph for the tree, text for the tips, scatterplot for nodes, etc.) versus creating a custom ToyTreeMark class object. In v1 I followed the first approach, whereas v2 does the latter. The custom marker object approach had some significant advantages for the size and speed of creation of the objects, and for setting margin spacing around them.

That being said, I think you are right that the mark.markers list is a clear place for storing the underlying toyplot markers that compose the nodes or edges, but its something that I never filled (it's just an inherited feature of the Mark class).

I definitely welcome any contributions to the docs or source, I'm really interested in getting more developers involved with toytree. Shoot me an email if you're interested and I can fill you in on some of the future plans.

I'll leave this issue open for now since I think filling the mark.markers list in render.py would be the best solution.

eaton-lab avatar Jun 14 '21 22:06 eaton-lab

Thanks!

The legend creation in toyplot is not super intuitive. Saving the marker objects and passing them to the legend function is easiest, but not always doable when you have complex plots. Your approach of making custom marker objects is a great solution.

It took me a while to get my head around it, but I managed a work around, that doesn't make me want to puke afterwards. =-)

I was very back-and-forth when designing toytree in deciding whether to return portions of tree drawings as a collection of separate toyplot marker objects (e.g., a graph for the tree, text for the tips, scatterplot for nodes, etc.) versus creating a custom ToyTreeMark class object. In v1 I followed the first approach, whereas v2 does the latter. The custom marker object approach had some significant advantages for the size and speed of creation of the objects, and for setting margin spacing around them.

Your v2 also seems like the best approach for extending the original markers if required in the future.

I definitely welcome any contributions to the docs or source, I'm really interested in getting more developers involved with toytree. Shoot me an email if you're interested and I can fill you in on some of the future plans.

Cool, expect an email as soon as I get some breathing space. =-)

That being said, I think you are right that the mark.markers list is a clear place for storing the underlying toyplot markers that compose the nodes or edges, but its something that I never filled (it's just an inherited feature of the Mark class). I'll leave this issue open for now since I think filling the mark.markers list in render.py would be the best solution.

In that case, maybe altering the issue title is reasonable in order to make it clear what is being addressed here?

StuntsPT avatar Jun 15 '21 21:06 StuntsPT