Skip to content

Custom scripts

Giorgio Bianchini edited this page Jan 4, 2023 · 4 revisions

Custom scripts are used to perform complex actions, as Further transformations, as Plot actions, or as individual macros that are only executed once. Custom scripts can be added to the plot as any other module, using the Custom script modules. These have two parameters: one is a brief description of the script (which has no actual effect), and the other is the source code of the custom script.

Custom further transformations

Custom further transformation scripts are implemented as static methods of a static class. Here is the default source code for such a script:

using PhyloTree;
using System.Collections.Generic;
using TreeViewer;
using System;

namespace a5fa7753275f5435cabe2e31a9644beee
{
    //Do not change class name
    public static class CustomCode
    {
        //Do not change method signature
        public static void PerformAction(ref TreeNode tree, TreeCollection trees, InstanceStateData stateData, Action<double> progressAction)
        {
            //TODO: do something with the tree
        }
    }
}

The name of the namespace can be changed, but the name of the class and the method signature must not be changed, otherwise the program will not be able to find the method.

The arguments for the PerformAction method are:

  • ref TreeNode tree: the tree that was returned by the previous Further transformation module (or the First transformed tree, if the custom script is the first Further transformation module). Note that this is passed with the ref keyword. The tree object is an instance of the TreeNode class from the TreeNode C# library.

  • TreeCollection trees: this object contains a collection of all the trees that were read from the input file. Depending on the Load file module that was used, the trees could reside in memory or on disk. This collection can be enumerated (e.g. using a for or foreach loop) to get the individual trees. The TreeCollection class also comes from the TreeNode C# library.

  • InstanceStateData stateData: this object contains additional data that can be accessed by the script (see below).

  • Action<double> progressAction: this object is a method that can be invoked with a single argument of type double (e.g. progressAction(0.5)) to provide feedback on long-running operations. If your code runs very quickly, you can ignore this parameter.

As other Further transformation modules, a custom script is expected to act on the tree object to alter the tree; since this parameter is passed with the ref keyword, it is also possible to assign a new value to the tree variable, and this will become the new tree object.

TreeNode objects can be manipulated using an object-oriented approach: each object represents an individual node in the tree; the whole tree is represented by the root node (even for unrooted trees, there is always a "root" node, which in this case has at least 3 children).

The tree topology can be navigated using the Parent and Children properties of a TreeNode object. Each node has a Parent, corresponding to its direct ancestor in the tree; this is null for the root node of the tree. Children is a list of TreeNode objects containing the direct descendants for a node; this is never null, but the Children of leaf nodes has a Count of 0.

Each TreeNode object also has a number of attributes: the Name, Length and Support attributes can be directly accessed as properties on the TreeNode object; other attributes can be accessed using the Attributes property, which is a dictionary associating string keys (i.e. attribute names) to object values (which can be either doubles in the case of numeric attributes such as Length or Support, or strings in the case of string attributes such as Name). The Attributes dictionary is case-insensitive (i.e. node.Attributes["ATTRIBUTE"] is the same as node.Attributes["attriBUTE"]). The Name, Length and Support attributes can also be accessed through the Attributes dictionary (i.e. node.Attributes["Length"] is the same as node.Length).

Each TreeNode object also has an Id property that can be used to identify the node. Note that these are not persistent, and are likely to have different values every time the script is called. Thus, to consistently identify a node in the tree, it is necessary to use methods that traverse the tree topology, such as the GetLastCommonAncestor method.

TreeNode objects can be manipulated in many ways; for example, the GetChildrenRecursive method can be used to get a list of all the direct and indirect descendants of a node, the GetLastCommonAncestor method can be used to get the LCA of the specified taxa, etc. See the documentation website for the TreeNode C# library for more details.

As an example, the following custom script can be applied to the test.tbi tree from this repository. It will get the last common ancestor of Taxon7 and Taxon15 and then go through all of its descendants, marking in green the leaves with an even number and in red the internal branches that have a descendant leaf that is a multiple of 4.

using PhyloTree;
using System.Collections.Generic;
using TreeViewer;
using System;

namespace abe47043606594e788b0bc6e8f8a4c41d
{
    //Do not change class name
    public static class CustomCode
    {
        //Do not change method signature
        public static void PerformAction(ref TreeNode tree, TreeCollection trees, InstanceStateData stateData, Action<double> progressAction)
        {
            // Get the last common ancestor of Taxon7 and Taxon15
            TreeNode LCAof7And15 = tree.GetLastCommonAncestor("Taxon7", "Taxon15");

            // Get a list of all the direct and indirect descendants of the ancestor. Note that this list also contains the LCAof7And15 node itself.
            List<TreeNode> descendants = LCAof7And15.GetChildrenRecursive();

            // Enumerate through the list of descendants.
            foreach (TreeNode node in descendants)
            {
                // In this case, the node is a leaf node.
                if (node.Children.Count == 0)
                {
                    // Get the name of the node. (string)node.Attributes["Name"] would return the same value.
                    string taxonName = node.Name;

                    // Get the number at the end of the name. First, we remove the "Taxon" part from the name of the node, and then we parse the result as an integer.
                    int taxonNumber = int.Parse(taxonName.Replace("Taxon", null));

                    // If the number is even (i.e. the remainder of its division by 2 is 0).
                    if (taxonNumber % 2 == 0)
                    {
                        // Set the Color of the node to "green".
                        node.Attributes["Color"] = "green";
                    }
                }
                // In this case, the node is an internal node.
                else
                {
                    // Get a list of the names of the leaf nodes that descend from this node.
                    List<string> descendantLeafNames = node.GetLeafNames();

                    // Enumerate through the leaf names.
                    foreach (string leafName in descendantLeafNames)
                    {
                        // Get the number at the end of the name.
                        int taxonNumber = int.Parse(leafName.Replace("Taxon", null));

                        // If the number is a multiple of 4 (i.e. the remainder of its division by 4 is 0).
                        if (taxonNumber % 4 == 0)
                        {
                            // Set the Color of the node to "red".
                            node.Attributes["Color"] = "red";
                        }
                    }
                }
            }
        }
    }
}

The result should be a plot similar to the following one:

You can insert breakpoints within the script to see when they are reached and how the values of the various variables change.

The stateData object contains a few interesting properties:

  • stateData.GraphBackgroundColour returns the current background colour for the tree plot.
  • stateData.Trees returns the collection of trees read from the tree file (same as the trees argument).
  • stateData.Tags is a Dictionary with string keys and object values that is used to store data that needs to be communicated between different modules. Note that this data is not saved with the tree file, and must computed again every time the tree is updated. For example, the Set up age distributions module stores the age distributions in this dictionary, so that they can be quickly retrieved by the corresponding Plot action module.
  • stateData.Attachments is a Dictionary with string keys and Attachment values that contains the attachment files that have been loaded by the user. Each file is identified by the name that the user has given to it. Interesting methods of the Attachment class are GetBytes and GetLines, which return the contents of the file as a byte array and as an array of strings, respectively. Attachments can be used to store additional data together with the tree.

The remaining properties of the InstanceStateData class are mainly used to manipulate the modules that are currently active in the plot, and should not be used by custom scripts because they are meant for use by Action, Selection action, and Menu action modules.

  • stateData.TransformerModule() returns the Transformer module that is currently active.
  • stateData.GetTransformerModuleParameters() returns a Dictionary<string, object> containing the current values of the parameters for the Transformer module.
  • stateData.SetTransformerModule(TransformerModule module) changes the current Transformer module. An instance of the module can be obtained using the method TreeViewer.Modules.GetModule<TransformerModule>(TreeViewer.Modules.TransformerModules, "<module-id>");.
  • stateData.TransformerModuleParameterUpdater()(Dictionary<string, object> parametersToUpdate) changes the value of some parameters for the Transformer module. Note the double parentheses - this is because invoking TransformerModuleParameterUpdater returns an Action, which must itself be invoked to update the parameter values.

  • stateData.CoordinateModule() returns the Coordinates module that is currently active.
  • stateData.GetCoordinatesModuleParameters() returns a Dictionary<string, object> containing the current values of the parameters for the Coordinates module.
  • stateData.SetCoordinatesModule(CoordinatesModule module) changes the current Coordinates module. An instance of the module can be obtained using the method TreeViewer.Modules.GetModule<CoordinateModule>(TreeViewer.Modules.CoordinateModules, "<module-id>");.
  • stateData.CoordinatesModuleParameterUpdater()(Dictionary<string, object> parametersToUpdate) changes the value of some parameters for the Coordinates module. Note the double parentheses.

  • stateData.FurtherTransformationModules() returns a List of the Further transformation modules that are currently active.
  • stateData.GetFurtherTransformationModulesParamters(int moduleIndex) returns a Dictionary<string, object> containing the current values of the parameters for the specified Further transformation module (the index should be the index of the module in the List returned by stateData.FurtherTransformationModules()).
  • stateData.AddFurtherTransformationModule(FurtherTransformationModule module) adds a Further transformation module to the plot. An instance of the module can be obtained using the method TreeViewer.Modules.GetModule<FurtherTransformationModule>(TreeViewer.Modules.FurtherTransformationModules, "<module-id>");.
  • stateData.FurtherTransformationModulesParameterUpdater(int moduleIndex)(Dictionary<string, object> parametersToUpdate) changes the value of some parameters for the specified Further transformation module (the index should be the index of the module in the List returned by stateData.FurtherTransformationModules()). Note the double parentheses.
  • stateData.RemoveFurtherTransformationModule(int moduleIndex) removes the specified Further transformation module from the plot (the index should be the index of the module in the List returned by stateData.FurtherTransformationModules()).

  • stateData.PlottingModules() returns a List of the Plot action modules that are currently active.
  • stateData.GetPlottingModulesParameters(int moduleIndex) returns a Dictionary<string, object> containing the current values of the parameters for the specified Plot action module (the index should be the index of the module in the List returned by stateData.PlottingModules()).
  • stateData.AddPlottingModule(PlottingModule module) adds a Plot action module to the plot. An instance of the module can be obtained using the method TreeViewer.Modules.GetModule<PlottingModule>(TreeViewer.Modules.PlottingModules, "<module-id>");.
  • stateData.PlottingModulesParameterUpdater(int moduleIndex)(Dictionary<string, object> parametersToUpdate) changes the value of some parameters for the specified Plot action module (the index should be the index of the module in the List returned by stateData.PlottingModules()). Note the double parentheses.
  • stateData.RemovePlottingModule(int moduleIndex) removes the specified Further transformation module from the plot (the index should be the index of the module in the List returned by stateData.PlottingModules()).

  • stateData.GetSelectedNode() returns the currently selected node in the interface. This will return null if no node has been selected by the user. This also works in command-line mode, where the node is selected using the node command.
  • stateData.SetSelectedNode() sets the currently selected node.

  • stateData.TransformedTree returns the current final transformed tree.

Custom plot actions

Custom plot actions are also implemented as static methods of a static class. Here is the default source code for such a script:

using PhyloTree;
using System.Collections.Generic;
using VectSharp;
using TreeViewer;

namespace a928915c8384943c8bfc3e9a491881dc0
{
    //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)
        {
            Point topLeft = new Point();
            Point bottomRight = new Point();
            return new Point[] { topLeft, bottomRight };
        }
    }
}

The name of the namespace can be changed, but the name of the class and the method signature must not be changed, otherwise the program will not be able to find the method.

The arguments for the PerformPlotAction method are:

  • TreeNode tree: the final transformed tree.
  • Dictionary<string, Point> coordinates: the coordinates of the nodes, as computed by the Coordinates module. This dictionary associates the Id of nodes in the tree with their coordinates, represented as a Point.
  • Graphics graphics: the graphics surface on which the plot should be drawn. This is an instance of the VectSharp.Graphics class from the VectSharp library.
  • InstanceStateData stateData: this object contains additional data that can be accessed by the script (see above).

The method should return an array containing two Points, representing the top-left and bottom-right corner of a rectangle containing all the elements that have been plotted by the custom script. This is important mostly when the custom script draws things outside of the region occupied by the tree, as the page size needs to be updated to include these items as well.

At this stage, the tree and coordinates should not be changed by the script. While it is technically possible for the script to change the coordinates of some nodes or to modify the tree, this can lead to unexpected results: unlike the Further transformation modules, the Plot actions are not guaranteed to execute in the order they are shown in the interface, therefore these changes may not have the expected downstream effect.

The coordinates for a node in the tree can be obtained by using the Id property of the node as a key for the coordinates dictionary; this will return a Point corresponding to the coordinates that have been computed for that node by the Coordinates module.

The graphics object is used to draw plot elements. It has many methods that can be used to draw different kinds of elements; see the documentation for the VectSharp library and for the Graphics class to get a complete list.

For example, the following custom script draws red squares at every internal node and green circles at every leaf node in the tree:

using PhyloTree;
using System.Collections.Generic;
using VectSharp;
using TreeViewer;

namespace a928915c8384943c8bfc3e9a491881dc0
{
    //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)
        {
            // Get a list of all the nodes in the tree.
            List<TreeNode> nodes = tree.GetChildrenRecursive();

            // Enumerate through the list of nodes.
            foreach (TreeNode node in nodes)
            {
                // Get the coordinates of the point as they have been computed by the Coordinates module.
                Point nodeCoordinates = coordinates[node.Id];

                // If the node is a leaf node.
                if (node.Children.Count == 0)
                {
                    // Draw a circle centered at the node coordinates, with radius 5, and spanning angles from 0 to 2*pi (i.e. a whole circumference).
                    graphics.FillPath(new GraphicsPath().Arc(nodeCoordinates, 5, 0, 2 * System.Math.PI), Colours.Green);
                }
                else
                {
                    // Draw a square centered at the node coordinates, with width and height equal to 10.
                    graphics.FillRectangle(nodeCoordinates.X - 5, nodeCoordinates.Y - 5, 10, 10, Colours.Red);
                }
            }

            // All the things that we plotted are contained within the surface area of the tree, thus we do not need to actually determine the rectangle containing all of our points.
            Point topLeft = new Point();
            Point bottomRight = new Point();

            return new Point[] { topLeft, bottomRight };
        }
    }
}

When applied to the test.tbi tree from this repository, the result should be a plot similar to the following one:

Custom macro scripts

It is possible to create a custom script that is executed only once by using the Custom script button in the Actions tab. This will open a code editor window that contains the default code for such a script:

using System;
using System.Threading.Tasks;
using TreeViewer;

namespace ad01f1308013d489aa39be53588de067c
{
    //Do not change class name
    public static class CustomCode
    {
        //Do not change method signature
        public static async Task PerformAction(MainWindow parentWindow, Action<double> progressAction)
        {
            //TODO: do something
            //TODO: call progressAction with a value ranging from 0 to 1 to display progress
        }
    }
}

The code in the PerformAction method will be executed when the Run button at the top of the new window is clicked. This method has two parameters:

  • MainWindow parentWindow provides access to the window in which the Custom script button was clicked.
  • Action<double> progressAction can be used to report progress during execution of the script (this is useful e.g. in long-running scripts).

The properties of the parentWindow object can be used to access the tree(s) that are currently open in the window:

  • parentWindow.Trees is the collection of trees that were open (before the Transformer module has acted).
  • parentWindow.FirstTransformedTree is the tree produced by the Transformer module (e.g., the consensus tree), before the Further transformations have acted.
  • parentWindow.TransformedTree is the final transformed tree, after the Further transformations have acted.

You should treat these as read-only (even though you could actually change the value of these properties), because they are updated automatically when any changes to the relevant modules are made, thus any manual change applied through a script of this kind would be lost immediately.

For example, we can use this kind of script to compute the total length of the final transformed tree (i.e., the sum of all branch lengths in the tree) and display it in a dialog window:

using System;
using System.Threading.Tasks;
using TreeViewer;
// Additional using directives.
using System.Collections.Generic;
using PhyloTree;

namespace ad01f1308013d489aa39be53588de067c
{
    //Do not change class name
    public static class CustomCode
    {
        //Do not change method signature
        public static async Task PerformAction(MainWindow parentWindow, Action<double> progressAction)
        {
            // Get a list of all the nodes in the tree.
            List<TreeNode> nodes = parentWindow.TransformedTree.GetChildrenRecursive();

            // This variable will contain the total sum of branch lengths in the tree.
            double sum = 0;

            // Iterate over all the nodes in the list.
            foreach (TreeNode node in nodes)
            {
                // Check that the branch length is a valid number (e.g., the root node should have a length of not-a-number/NaN).
                if (!double.IsNaN(node.Length))
                {
                    // Add the length to the sum.
                    sum += node.Length;
                }
            }

            // Create a MessageBox to show the resulting value. If you started the program from a command line, you could
            // also use Console.WriteLine to write it to the terminal, but if you started the program normally from the
            // GUI this would have no effect.
            MessageBox box = new MessageBox(title: "output", text: "Total sum of branch lengths: " + sum.ToString());

            //Display the MessageBox and wait until it is closed.
            await box.ShowDialog(parentWindow);
        }
    }
}
Clone this wiki locally