Custom scripts - arklumpus/TreeViewer GitHub Wiki
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 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 theref
keyword. The tree object is an instance of theTreeNode
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 afor
orforeach
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 typedouble
(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 double
s in the case of numeric attributes such as Length
or Support
, or string
s 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 thetrees
argument). -
stateData.Tags
is aDictionary
withstring
keys andobject
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, theSet 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 aDictionary
withstring
keys andAttachment
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 theAttachment
class areGetBytes
andGetLines
, which return the contents of the file as abyte
array and as an array ofstring
s, 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 aDictionary<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 methodTreeViewer.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 invokingTransformerModuleParameterUpdater
returns anAction
, which must itself be invoked to update the parameter values.
-
stateData.CoordinateModule()
returns the Coordinates module that is currently active. -
stateData.GetCoordinatesModuleParameters()
returns aDictionary<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 methodTreeViewer.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 aList
of the Further transformation modules that are currently active. -
stateData.GetFurtherTransformationModulesParamters(int moduleIndex)
returns aDictionary<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 theList
returned bystateData.FurtherTransformationModules()
). -
stateData.AddFurtherTransformationModule(FurtherTransformationModule module)
adds a Further transformation module to the plot. An instance of the module can be obtained using the methodTreeViewer.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 theList
returned bystateData.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 theList
returned bystateData.FurtherTransformationModules()
).
-
stateData.PlottingModules()
returns aList
of the Plot action modules that are currently active. -
stateData.GetPlottingModulesParameters(int moduleIndex)
returns aDictionary<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 theList
returned bystateData.PlottingModules()
). -
stateData.AddPlottingModule(PlottingModule module)
adds a Plot action module to the plot. An instance of the module can be obtained using the methodTreeViewer.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 theList
returned bystateData.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 theList
returned bystateData.PlottingModules()
).
-
stateData.GetSelectedNode()
returns the currently selected node in the interface. This will returnnull
if no node has been selected by the user. This also works in command-line mode, where the node is selected using thenode
command. -
stateData.SetSelectedNode()
sets the currently selected node.
-
stateData.TransformedTree
returns the current final transformed tree.
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 theId
of nodes in the tree with their coordinates, represented as aPoint
. -
Graphics graphics
: the graphics surface on which the plot should be drawn. This is an instance of theVectSharp.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 Point
s, 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:
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 theCustom 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);
}
}
}