arklumpus / TreeViewer

Cross-platform software to draw phylogenetic trees
GNU Affero General Public License v3.0
182 stars 8 forks source link

Shades on taxa or monophyletic clades #5

Closed Dichromatism closed 3 years ago

Dichromatism commented 3 years ago

Hi! I am trying to use TreeViewer and have a question on color shades on clusters. Are there any modules that can draw color shades (or big "blocks") onto taxa or monophyletic clades? Which I mean is to draw a rectangular block consisting all of the branches or the names of one clade in a rectangular tree, aiming to emphasize. Sometimes I think it is necessary to draw different colored shades especially for there are a lot of different taxa. Thank you!

arklumpus commented 3 years ago

Hi! Sorry for the long post, I though I might as well be thorough 😅

If I understand correctly what you want to do (I assume we are talking about trees in "rooted" style), I would say you have multiple options here:

  1. This is probably the easiest:

    • Use the Add attribute module to add an attribute containing the highlight colour to the LCAs of the groups you want to highlight (make sure to enable the option to propagate the attribute to all children). You can do this also by selecting the node and adding the attribute using the selection panel.
    • Add one or more additional Branches modules, set the branch colour so that it picks up the value from your new attribute, and increase the branch width. Finally, move this module above the original Branches so that it is drawn behind it.
    • You can do multiple "layers" for nested groups, you just need to use a different attribute and a different Branches module for each layer.

    The result should be something like this (sorry about the image quality, GitHub does not let me upload an SVG file):

  2. You could use the Group labels module the way it is "meant" to be used:

    • As before, use the Add attribute module to add an attribute with the highlight colour to the various groups. This time, use the same attribute even for nested groups, but make sure that the attribute for the larger group (e.g. Group 3) is set before the attribute for the smaller group (e.g. Group 2).
    • Now, add another attribute to each LCA containing the name of the group (this time you don't need the attribute to propagate).
    • Add a Group labels module and set it up to pick up the group name and highlight colour from the respective attributes. Adjust the layout attributes to position the labels in the right place.

    One of the tutorials does something very similar and has a lot more detail: Displaying the names of the groups

    The result should be something like this:

  1. Similar to the above, but "hacks" the Group labels module in a creative way:

    • As in 2., use the Add attribute module to add an attribute with the highlight colour to the groups. Use the same attribute for all groups.
    • Then add another attribute with the group name to the LCAs. In this case, use a different attribute for each group; the value of the attribute (as long as it is not empty) does not matter because we are not really going to draw the group names (e.g. you could add attribute Group1 with value Group 1 to the LCA of group 1, attribute Group2 with value Group 2 to the LCA of group 2, etc).
    • Add a Group labels module and set it up to pick up the highlight colour from the attribute you set in the first step, and the group name from the attribute associated to the first group. Then, set the text colour to transparent (so that the text is not drawn) and fiddle with the layout parameters so that the rectangle is the right size.
    • Rinse and repeat for each group.

    The result should be something like this:

  1. Finally, you could go Rambo and use a Custom script Plot action to draw the rectangles. I think this is actually a good problem to start working with custom scripts in TreeViewer, because it is not too hard but not entirely trivial either. Here is a commented code example:

    using PhyloTree;
    using System.Collections.Generic;
    using VectSharp;
    using TreeViewer;
    using System;
    
    namespace adf9bf2c0252d4c83949458a07eadc17a
    {
        //Do not change class name
        public static class CustomCode
        {
            //Do not change method signature
            public static Point[] PerformPlotAction(TreeNode tree, Dictionary<string, Point> coordinates, Graphics graphics, InstanceStateData stateData)
            {
                //Define the groups we want to highlight by their common ancestor (you can suppy more taxon names, if needed)
                TreeNode lcaOfGroup1 = tree.GetLastCommonAncestor("Taxon1", "Taxon5");
                TreeNode lcaOfGroup2 = tree.GetLastCommonAncestor("Taxon16", "Taxon18");
                TreeNode lcaOfGroup3 = tree.GetLastCommonAncestor("Taxon11", "Taxon18");
    
                //Highlight each group. Since group 2 is a subset of group 3, you want to highlight group 3 first (so that the
                //highlight for group 2 is drawn after, i.e. over, the highlight for group 3)
                //The arguments we are passing to the "HighlightGroup" function (defined below) are:
                // * The node to highlight
                // * The colour with which to highlight it
                // * The dictionary containing the coordinates of all the nodes in the tree (as it was supplied to us by the
                //   Coordinates module)
                // * The graphics surface on which the plot is drawn
                HighlightGroup(lcaOfGroup1, Colour.FromRgb(216, 245, 255), coordinates, graphics);
                HighlightGroup(lcaOfGroup3, Colour.FromRgb(225, 182, 150), coordinates, graphics);
                HighlightGroup(lcaOfGroup2, Colour.FromRgb(170, 227, 206), coordinates, graphics);
    
                //This bit would be useful to update the bounds of the whole tree plot; since the things we are drawing are
                //contained within the area of the tree anyways, we do not need to bother with this.
                Point topLeft = new Point();
                Point bottomRight = new Point();
                return new Point[] { topLeft, bottomRight };
            }
    
            //Function to highlight a group in a certain colour
            private static void HighlightGroup(TreeNode lca, Colour colour, Dictionary<string, Point> coordinates, Graphics graphics)
            {
                //First of all, determine the bounds of the area occupied by the specified group
    
                //Initialise variables
                double minX = double.MaxValue;
                double maxX = double.MinValue;
                double minY = double.MaxValue;
                double maxY = double.MinValue;
    
                //Iterate over the descendants of the LCA (LCA included)
                foreach (TreeNode descendant in lca.GetChildrenRecursiveLazy())
                {
                    //Get the coordinates of the node
                    Point pt = coordinates[descendant.Id];
    
                    //Update the bounds
                    minX = Math.Min(minX, pt.X);
                    maxX = Math.Max(maxX, pt.X);
                    minY = Math.Min(minY, pt.Y);
                    maxY = Math.Max(maxY, pt.Y);
                }
    
                //Now we know that all descendants of the specified node are contained in a rectangular area whose top-left
                //corner is at (minX, minY) and whose width is maxX - minX and height is maxY - minY. We just need to draw the
                //rectangle
    
                //Margins so that the rectangle is not too tight
                double marginLeft = 10;
                double marginRight = 0.5;
                double marginY = 7;
    
                //Fill the rectangle with the specified colour.
                graphics.FillRectangle(minX - marginLeft, minY - marginY, maxX - minX + marginLeft + marginRight, maxY - minY + 2 * marginY, colour);
            }
        }
    }

    Of course, this gives you much more flexibility, because now you are not limited to rectangles and can tweak every feature of the plot. You can also use this approach for trees in "unrooted" or "circular" style (naturally, in this case, you will want to use something more clever than a bounding rectangle shape). In the demo for VectSharp you can find a lot more examples of what you can do on a Graphics object.

    The result should be something like this:

In this ZIP file you can find the tree files I used to generate these plots, so that you can get an idea of how to set up the various modules.

Let me know if this makes sense or something is not clear!

Dichromatism commented 3 years ago

Thank's a lot. I have tried and learnt your advises and checked your nexus files. I think they are useful to me. Maybe I need more time on your VectSharp module and try to draw a highlighted "circular" tree. Besides, I am appreciate for your brilliant work.

arklumpus commented 3 years ago

Glad you like my program!

The approach to highlight groups in a circular tree is essentially the same actually (i.e., use the Group labels modules and tweak the distance and height so that it looks nice). For example, this is equivalent to option 3 above:

Adapting the custom script is slightly more complicated, because we need to convert the Cartesian coordinates to polar and back, but it's just a maths problem. Here is how you could do this (you just need to replace the HighlightGroup method, the rest remains the same):

//Function to highlight a group in a certain colour
private static void HighlightGroup(TreeNode lca, Colour colour, Dictionary<string, Point> coordinates, Graphics graphics)
{
    //First of all, determine the bounds of the area occupied by the specified group
    //In this case we need to work with polar coordinates (i.e. r and theta)

    //Get a list of all the leaves that descend from the node
    List<TreeNode> leaves = lca.GetLeaves();

    //Coordinates of the "first" leaf in the group
    Point pt1 = coordinates[leaves[0].Id];

    //This holds the angle of the "first" leaf in the group. We use the Atan2 function to convert from Cartesian
    //coordinates to polar coordinates
    double theta1 = Math.Atan2(pt1.Y, pt1.X);

    //Same thing for the "last" leaf in the group
    Point pt2 = coordinates[leaves[leaves.Count - 1].Id];
    double theta2 = Math.Atan2(pt2.Y, pt2.X);

    //Make sure that theta2 is greater than theta1 (this might not be the case if the group straddles the angle at PI/180°)
    if (theta2 < theta1)
    {
        theta2 += 2 * Math.PI;
    }

    //r for the leaves; we want to pick the one corresponding to the leaf that is furthest away from the centre, so
    //we iterate over all the leaves
    double rOuter = double.MinValue;
    foreach (TreeNode leaf in leaves)
    {
        Point pt = coordinates[leaf.Id];
        double r = Math.Sqrt(pt.X * pt.X + pt.Y * pt.Y);

        rOuter = Math.Max(r, rOuter);
    }

    //Compute r for the LCA (we don't care about theta)
    Point ptLCA = coordinates[lca.Id];
    double rInner = Math.Sqrt(ptLCA.X * ptLCA.X + ptLCA.Y * ptLCA.Y);

    //Now we know that all descendants of the specified node are contained within a circular crown going from rInner
    //to rOuter, in a sector going from theta1 to theta2. We just need to draw it.

    //Margins so that the shade is not too tight
    double marginInner = 10;
    double marginOuter = 0.5;
    double marginTheta = 0.1;

    rInner -= marginInner;
    rOuter += marginOuter;
    theta1 -= marginTheta;
    theta2 += marginTheta;

    //We can draw the shape using a GraphicsPath
    GraphicsPath path = new GraphicsPath();

    //Starting point - we need to convert back the polar coordinates to cartesian
    path.MoveTo(rInner * Math.Cos(theta1), rInner * Math.Sin(theta1));

    //Inner arc from theta1 to theta2
    path.Arc(new Point(0, 0), rInner, theta1, theta2);

    //Line to the outer arc
    path.LineTo(rOuter * Math.Cos(theta2), rOuter * Math.Sin(theta2));

    //Outer arc from theta2 to theta1
    path.Arc(new Point(0, 0), rOuter, theta2, theta1);

    //Close the path, drawing the last segment from the outer arc to the inner arc
    path.Close();

    //Fill the path with the specified colour.
    graphics.FillPath(path, colour);
}

In this ZIP file you can find the equivalent of examples 3 and 4 above adapted for a circular tree.

Also, I forgot to say this earlier, but you can obviously combine e.g. 2 and 3 or 2 and 4 to show the group names as well as highlighting the groups.

Dichromatism commented 3 years ago

Thank you! Your answer is very clear and I can draw trees following your comment.

The idea of combining 2 and 4 is just what I want and it works very well!