65.9K
CodeProject is changing. Read more.
Home

Full-Featured Visual Scripting Environment for R & Data Science

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (30 votes)

Aug 14, 2018

MIT

37 min read

viewsIcon

43948

downloadIcon

258

A visual scripting environment for R & data science

Index

Introduction

I am interested in video games. As a result, I tried to dig into the field of game development, driven by my curiosity. With that being said, it was inevitable for me not to notice the charms of the visual scripting tools that have been used by developers to make impressive games.

Personally, I have used multiple visual scripting environments, and I have admired how they can make the process easier sometimes.

Due to this ambition, I dedicate a year of my life to write my own visual scripting environment. It helped me generate relevant data science, Machine Learning and Artificial intelligence, related programs.

Few Notes

Even though the intention behind the creation of this project was to make a tool that can create tools for DataScience, this article won't be discussing the aspects of how this project relates to data science. We will only go through the technical parts of the article.

Reading a block of text that contains thousands of words will get you dead bored- that said, adding some touches to it may prove cool along the process of reading!

When reading, you'll face fruits! Yes, fruits. Fruits are used as references and they can help you get an idea about the section that you are reading.

: It refers to the overviews and abstractions. Usually, these parts don't focus much on the technical parts as much as they do focus on the user experience and the philosophy of user interface.

: It refers to the technical-ish parts that include codes, links, explanations and tests.

: It refers to the summaries and conclusions. You will, almost, see this apple at the end of every section you read.

: It refers to the code on Github.

These magical icons have been illustrated by Anastasia.

Inspiration

After I used a visual scripting tool such as Unreal Engine's blueprints, I enjoyed the fact that it renders the difficulty to write codes negligible.

However, visual scripting cannot make classical programming obsolete, because, sometimes, writing a few characters may prove easier than trying to link a bunch of nodes all together correctly.

All in all, visual scripting has its pros & cons- however, we won't be addressing any of those within this article.

Abstraction

As a visual scripting environment for Data Science, this project's main objective is to provide the user with the ability to create to perform multiple feats that are related to the field of Data Science, such as analyzing, extracting, mining and visualizing data, etc.

Generally, a visual scripting environment provides a bunch of built-in tools to help the user reach his sought results.

To be able to, visually, move, pan, zoom and linearly transform your nodes and the wires connecting them in a relevant way, and to get a suitable result, in the end, only then we can, appropriately, say we have created a visual scripting environment.

By default, such environments contain nodes, ports and connections. Each element of those has its own particular role and significance.

Why WPF

WPF is a relevant technology - I have already written an article about it in the past that I highly recommend.

Thanks to its powerful skinning and styling features, WPF makes it easier to achieve the desired UIs in less time. Writing a new control in WPF that needs 30 minutes to be ready, will take up to an hour or two if I had to use WinForms & GDI+.

Truth be told, the most crucial factor that has influenced my choice is that WPF uses hardware acceleration for rendering. Hence, the better the performance.

Architecture

The project is divided into multiple sections and slices, each part has its own merit and purpose.

Core

The core of the visual scripting environment is usually consolidated of a camera imitating control and some graphical elements that are manipulatable by that control.

Camera Control

The camera control is used to make surfing the environment easier, by making the user able to move, pan, zoom and resize the elements on the screen.

Before digging any deeper, we must first note that the mouse and its movement are the most crucial aspect of the Camera control, the mouse has multiple modes, and each mode has its own purposes and properties.

  • Nothing

    You guessed it right. In this state, the mouse's mode has no significant meaning.

  • Panning

    In this mode, the mouse's movement will be accompanied by the movement of all the nodes on the screen.

  • Selection

    This is the most common mode. Every selected element will move according to the movement of the mouse.

  • PreSelectionRectangle

    This activates when the mouse's left button is pressed down on a free space, once you drag it will trigger the SelectionRectangle mode.

  • SelectionRectangle

    Once you start selection elements and grouping them, a rectangle will follow the movements of your mouse and hover around the selected elements.

  • DraggingPort

    When you try to drag a wire out of a port, this mode will be activated.

  • ResizingComment

    Comments are elements, you can resize them. Once you do, this mode will be activated.

Camera: Move

Having the ability to move your elements within a specific two-dimensional space is so important. It will make you able to arrange elements for a better experience.

To be able to position your elements wherever you want in WPF, you must use a container that allows you to move your items based on some coordinates (X, Y). That said, the container that we will be using is the Canvas.

The canvas gives you the ability to set the coordinates of your UIElements- using one of these functions:

  • SetLeft
  • SetTop
  • SetRight
  • SetBottom

Within this section, will be only focusing only on SetLeft to set the X(horizontal axis) value, and SetTop to set the Y(vertical axis) value.

Saying that we want to move an element is equivalent to saying that we want to change its coordinates. Truth is, it is easier said than done. To avoid throwing blocks of code everywhere, I shall keep the article clean and optimized for a cool-read.

Here we have our UIElement, which is a cool looking rectangle, on a canvas:

As we can see, the coordinates of our rectangle are (X:2; Y:1).

To achieve the same result with, we can perform these two operations on a rectangle called box.

//Sets the Y value
Canvas.SetTop(box,1);

//Sets the X value
Canvas.SetLeft(box,2);

With all of that being said about the canvas and how it gives us the ability to move our UIElements within it, it is now easy to see that we can change our element's location wherever we want it to be.

For example, if we perform these two instructions:

//Sets the Y value
Canvas.SetTop(box,0); 

//Sets the X value 
Canvas.SetLeft(box,1);

Then we will perceive this result:

Now after understanding how the moving operation works, it is safe to dig deeper into the technical parts.

First, let's say that we will move our element based on our mouse's movements.

In order to perform a smooth linear transformation, you have got to respect three main events:

  • MouseDown

    Once the mouse is down, the location of the mouse must be stored as a point of origin.

  • MouseMove

    When the mouse moves, mathematical operations must be performed in order to calculate the distance between the point of origin(the ex-location of the mouse the moment the MouseDown event has been raised) and set the new coordinates of the element based on the interpreted results.

  • MouseUp

    This will indicate that no element is being selected nor moved.

Camera: Panning

Panning, as far as the definition goes, it is the operation of moving more than one element at the same moment, by the same value, and that is arguable.

We have two boxes, box1 and box2:

When we perform the panning operation, the two boxes will move based on the mouse's coordinates at the same moment.

Say we moved the mouse from right to left a little bit, this will be the result:

The subtle difference between moving and panning is that panning will move all the elements, with no exceptions, based on the mouse's coordinates. Technically speaking, we will iterate through all the elements of the canvas and edit their coordinates simultaneously.

Camera: Zoom

Zooming has two types, whether to zoom in or to zoom out. By zooming, we mean that we are going to scale our elements.

Though the fact is, it is fruitless to be talking about what zooming is in an era where, almost, everyone has a camera in his phone.

Here is our jelly box:

We won't be addressing its coordinates in this section of the article, they won't change.

Here is our box after zooming:

Technically, zoom-in and zoom-out are nothing but UI-friendly names. For example, there is no Cut & Paste, it is just about deleting the pasted data after the end of the operation. And that's exactly what we are doing in here, we are just scaling the elements to imitate the behaviour of zooming.

Code:

        private readonly double _zoomMax = 1.5;
        private readonly double _zoomMin = 0.7;
        private readonly double _zoomSpeed = 0.0005;
        private double _zoom = 0.9;

        protected virtual void HandleMouseWheel(object sender, MouseWheelEventArgs e)
        {
            _zoom += _zoomSpeed * e.Delta;
            if (_zoom < _zoomMin) _zoom = _zoomMin;
            if (_zoom > _zoomMax) _zoom = _zoomMax;
            var scaler = LayoutTransform as ScaleTransform;

            if (scaler == null)
            {
                scaler = new ScaleTransform(01, 01, Mouse.GetPosition(this).X, 
                                            Mouse.GetPosition(this).Y);
                LayoutTransform = scaler;
            }

            var animator = new DoubleAnimation
            {
                Duration = new Duration(TimeSpan.FromMilliseconds(500)),
                To = _zoom
            };
            scaler.BeginAnimation(ScaleTransform.ScaleXProperty, animator);
            scaler.BeginAnimation(ScaleTransform.ScaleYProperty, animator);

            MouseMode = MouseMode.Nothing;
            e.Handled = true;
        }

Camera: Theme

Having a grid-like style is a better-have asset. In fact, the grid will be so helpful when we want to arrange and adjust our items visually.

    <Style x:Key="VirtualControlStyle" TargetType="Canvas">
        <Setter Property="ScrollViewer.Visibility" Value="Visible" />
        <Setter Property="LayoutTransform">

            <Setter.Value>
                <MatrixTransform />
            </Setter.Value>
        </Setter>
        <Setter Property="Background">
            <Setter.Value>
                <DrawingBrush TileMode="Tile" Viewport="10,10,20,20"
                              ViewportUnits="Absolute">
                    <DrawingBrush.Drawing>
                        <DrawingGroup>
                            <GeometryDrawing Brush="#353535">
                                <GeometryDrawing.Geometry>
                                    <GeometryGroup>
                                        <RectangleGeometry Rect="0,0,50,50" />
                                    </GeometryGroup>
                                </GeometryDrawing.Geometry>
                                <GeometryDrawing.Pen>
                                    <Pen Brush="#FFE8E8E8" Thickness="0.1" />
                                </GeometryDrawing.Pen>
                            </GeometryDrawing>
                        </DrawingGroup>
                    </DrawingBrush.Drawing>
                </DrawingBrush>
            </Setter.Value>
        </Setter>
    </Style>

The implementation of the style will provide us with this result:

Camera

Children

Children are controls and UIElements that can be eventually added to the canvas. Hence, the name.

When we talk about children in this next we are already talking about Nodes, Ports, Comments and Wires, etc.

Nodes

Nodes: Abstraction

I classify nodes as sub-applications within the visual scripting environment. In fact, each node has its own purpose and each one of them is unique.

Nodes: Template

Each node has its own style and theme; however, they do all derive from the same template.

  • Title

    Each node has a title. The title is just a TextBlock that can be used to tell more about the node.

  • In-Execution Port

    The in-execution port is used to trigger the node and make it generate the code that will be eventually interpreted and compiled.

  • Out-Execution Port

    The out-execution port will trigger the node that it will connect to. If by any chance, it connects to no other node, then all the remaining nodes that are unlinked will be overlooked.

    An exception will be made for function, basic and spaghetti nodes.

  • Input Ports

    As the name suggests, everything you pass through an input port will be treated as data and will be eventually parsed. Usually, every input port has a control that is assigned to it. The role of such a control is to give the user the ability to enter his data without the need to import it from another node- not to mention that the sought data does not always exist in other nodes.

    • Control

      As we have already mentioned above, some input nodes may have custom controls to assure a better experience, here is an example of how I have used a CheckBox to create a logical node:

  • Output Ports

    Based on the input data, the generation of the code within the node will produce a relevant output that will be stored in the output port.

  • Additional Controls

    The additional controls, while being less generic, used and implement compared to the other components of a node, they are quite serviceable in some situations.

Nodes: Manipulation

Manipulating nodes is one of the most important aspects of the visual scripting environment, for the nodes being the most important assets.

There is a set of operations that we can perform to handle the nodes:

  • Create
  • Clone
  • Delete
  • Copy
  • Paste
  • Move
  • Zoom
  • Refresh
  • Add Execution Ports
  • Add Object Ports
  • Refresh
  • Search
  • De/Serialize
  • Hide

All these methods are already added to the superclass Node.

Nodes

Ports

Each port has only two possible states:

  • Linked
  • Unlinked

Execution Ports

Execution Ports: Abstraction

Execution ports are used to trigger and attach nodes to the main chain of executable functions.

We will see what a chain of executions is when we get into the connectors part.

Execution Ports: Template

The template of the execution ports is the simplest one amongst all the templates within this project; however, it varies based on the type of the port and whether is it linked or not.

That said, the execution port, as a control, is divided into two parts:

  • A pin
  • A TextBlock

The pin is a path.

<Style x:Key="ExecPin" TargetType="Path">
        <Setter Property="StrokeThickness" Value="2" />
        <Setter Property="Stretch" Value="Uniform" />
        <Setter Property="Data">
            <Setter.Value>
                <PathGeometry
                    Figures="m 42.333333 95.916667 h 21.166666 l 21.166667 
                             31.750003 -21.166667 31.75 H 42.333333 Z" />
            </Setter.Value>
        </Setter>
        <Style.Triggers>
            <Trigger Property="Path.IsMouseOver" Value="True">
                <Setter Property="Path.StrokeThickness" Value="3" />
                <Setter Property="Path.Effect">
                    <Setter.Value>
                        <BlurEffect Radius="1" />
                    </Setter.Value>
                </Setter>
            </Trigger>
        </Style.Triggers>
    </Style>

The TextBlock changes its location based on the type of the port. For example, if the port is an input port, then the TextBlock is located at right. Vice-Versa.

Execution Ports: Types

  • Input

    An input execution port is a port that can be used to trigger a node. It is more like someone ringing your bell and saying, "hey, it is time to wake up". Any method node that is not connected to any other node will not be executed.

  • Output

    An output execution port will connect its parent node with another node, and a connector of execution will be duly generated.

To add an execution port to your node, use the AddExecPort method.

//Adds an input execution port
AddExecPort(HelloNode, "port name", PortTypes.Input, "Text");
//Adds an output execution port
AddExecPort(HelloNode, "port name", PortTypes.Output, "Text");​​​​

Object Ports

Object Ports: Abstraction

Object ports are not as simple as execution ports; in fact, they are capable not only of storing data as objects, they even parse and interpret the values.

For example, there are nodes with no execution ports; despite that, they are capable or generating relevant results as a return value.

Object Ports: Template

The object ports' core template is quite similar to that of the execution ports; however, its pin's colour is variables based on the data that will be passing through that port.

Types of data and their corresponding colours:

Type Colour
Generic
Logical
Numeric
Character
Array, Factor, List or Matrix
DataFrame

In addition to the variance of the colour, the object ports can host a control to ease the user's experience. For more details, go back to the Nodes: Template section.

Object Ports: Types

Before we dig into the types of the object ports, we must mention that the object ports do have one more crucial criteria that have to be addressed. As a matter of fact, an object port can be connected to more than one object port.

Similar to the execution ports, there are only two types:

  • Input

    The input object ports are actually meant to store data, be it the data that has been attributed to it or the data that is carried within its internal control.

  • Output

    The output object ports are used to contain the return value of the whole operations that took place within the node's core.

To add an object port, you will have to call the AddObjectPort method.

//Add input port
AddObjectPort(this, "some text", PortTypes.Input, RTypes.Generic, false);

//Add input port with the ability to connect with more than one port
AddObjectPort(this, "some text", PortTypes.Input, RTypes.Generic, true);

//Add output port
AddObjectPort(this, "some text", PortTypes.Output, RTypes.Generic, false);

//Add port with an inner control (a textbox)
var tb = new TextBox();
AddObjectPort(this, "There is a textbox inside of me", 
              PortTypes.Input, RTypes.Generic, false, tb);

Object Ports: Events

  • DataChanged

    This event is so sensitive, once the data that is contained within an object port gets created, removed, updated or parsed, this event will be fired.

Ports

Connectors

Connectors are used to link nodes to each other. And because we have two types of ports, we'll respectively have two types of connectors.

A connector is composed of:

  • Host

    The host is actually the camera control that is hosting the parent nodes of the StartPort and the EndPort.

  • StartPort

    A connector has a starting port and an ending port. The StartPort is the port that the wire roots from.

  • EndPort

    As its name suggests, its functionalities and criteria are the opposite of that of the StartPort. It represents the end of the wire and the connection.

Connectors

Wires

Wires are used to help visualize the connected children. And each wire has its own colour based on the purpose it has to fulfil or the data that is being carried within.

Type Colour

Execution trigger

 
Generic
Logical
Numeric
Character
Array, Factor, List or Matrix

DataFrame

Truth be told, the wires are just an implementation of a parametric curve called Bézier curve.

(This gif has been copied from the wiki page of the Bézier curve that I have mentioned above.)

Visualizing a wire requires the existence of 4 coordinates to base our curve on. Each connector has a wire that is assigned to, which means that we can access the StartPort and the EndPort. That said, we can duly extract their coordinates for them being UIElements contained within our camera control themselves.

We can observe that the Wire class has some public properties. These properties will be, eventually, used to visualize a curve.

The curve is nothing but a path that has to be visualized, hence a style had to be created to accomplish that purpose.

<Style x:Key="WireStyle" TargetType="Path">
   <Setter Property="Stroke"
           Value="{Binding RelativeSource=
           {RelativeSource TemplatedParent}, 
           Path=Background}" />
   <Setter Property="StrokeThickness" Value="1.5" />
   <Setter Property="Data">
       <Setter.Value>
           <PathGeometry x:Name="CurveCore">
               <PathGeometry.Figures>
                   <PathFigureCollection>
                       <PathFigure
                           StartPoint="{Binding RelativeSource=
                           {RelativeSource TemplatedParent}, Path=StartPoint}">
                           <PathFigure.Segments>
                               <PathSegmentCollection>
                                   <BezierSegment
                                       Point1="{Binding RelativeSource=
                                       {RelativeSource TemplatedParent}, 
                                        Path=MiddlePoint1}"
                                       Point2="{Binding RelativeSource=
                                       {RelativeSource TemplatedParent}, 
                                        Path=MiddlePoint2}"
                                       Point3="{Binding RelativeSource=
                                       {RelativeSource TemplatedParent}, 
                                        Path=EndPoint}" />
                               </PathSegmentCollection>
                           </PathFigure.Segments>
                       </PathFigure>
                   </PathFigureCollection>
               </PathGeometry.Figures>
           </PathGeometry>
       </Setter.Value>
   </Setter>
lt;/Style>

Wires

Execution Connectors

Creating an execution connector will order the node to generate the code once its turn comes within the execution chain.

Say we have this graph:

Once we parse our graph, compile it and run it - this will be our final result:

This means that the order in which we linked the nodes, will be duly respected and instructions will be executed based on that custom order.

Executions Connectors: Operations

  • Create

    Creating an execution connector will be followed with the creation of a wire, and some notification within the engine to guard the ID of the connector for future usage (such as de/serialization).

    In addition to that, connectors cannot be created arbitrarily. That said, there are only two possible cases when you try to connect nodes. Either to have two linkable ports or not.

  • Delete

    Deleting an execution connector will be followed with the deletion of a wire, and some notification within the engine to tell that one node is not considered as an executable node anymore.

Executions Connectors: MiddleMan

By definition, a middleman is:

Quote:

a person who buys goods from producers and sells them to retailers or consumers.

True to its name, the MiddleMan algorithm I developed is an algorithm that performs the same task as the real middleman by taking the connector and change the nodes it connects based on some monospecific factor.

Imagine that we have a graph that contains dozens of nodes. Suddenly, you decide to add on more node in the middle of the graph; doing so will require you deleting multiple connectors and re-link the nodes. A better-avoided approach!

When such a situation occurs, a middleman will prove beneficial. Here is an example:

Explanation

Once you try to create an execution connector, the StartPort will be checked, if it already linked to another port, then a swap will occur. The former connector will be deleted, the EndPort will be linked to the previous StartPort - and the output execution port will become the StartPort of the connector that links it to the EndPort of the deleted connector. Feeling lost? Worry not, it is just a basic swapping operation.

Objects Connectors

Objects Connectors are used to help the user observe how the data is flowing in the visual scripting environment.

Creating and removing this kind of connectors is the same as the execution connectors; however, some tricks behind the scene do exist. We won't be digging into them for now.

Objects Connectors: Linkability

  • Linkable

    When the green tick appears, it means that the connecting operation is possible and that the data's type that is contained within the StartPort is the same as that of the EndPort.

  • UnLinkable

    When the red cross appears, it means that the connecting operation is impossible and that the data's type that is contained with StartPort is not the same as the of the EndPort. It may also mean that the types of the ports themselves are not compatible with each other.

  • Castable

    When the warning message appears, it means that the connecting operation is possible if and only if the data that is contained within the StartPort may be castable to another type that is compatible with the data's type that the EndPort can contain.

Objects Connectors: Spaghetti Divider

Graphs may get tedious and become hard to surf, analyze and even to look at. With that being mentioned, there is one approach that may help us customize the wires that may drive you crazy, and that approach is called spaghetti dividing.

Imagine we have this ugly graph in here:

We can easily notice that we can barely see where the wires are rooting from, not to mention that we can't decide whether we are handling the right piece of data or not. In such a condition, the spaghetti divider comes in to save the situation.

Now things have become clearer than ever.

Explanation

By performing a right-click on an object connector, select the "Divide" option, and it will divide the wire into two pieces that are linked to each other thanks to a pin that plays the role of a bridge. you can move that pin however you like to make it easier to visualize your graph.

Spaghetti Divider: Operations

  • Create

  • Delete

    Loyal to its main purpose, when you delete the flying pin, it won't result in the whole deletion of the connector.

    Once you create a spaghetti divider, it will conserve the details of the connector that it has divided into two parts. As a result, when you delete the magical pin, it will retrieve the previous state of the connector.

Objects Connectors: Flowing Data

Basically, this may be one of the most important parts of the project.

When the contents on the StartPort get modified, the DataChanged event gets triggered. As a consequence, the data that is inside the StartPort will be sent to the EndPort, such event will be executed N times, sequentially.

Virtual Control

Overview

The Virtual Control is, in fact, an inheritance of the Camera Control and it manages all the components we created above, such as the connectors, ports, etc.

Within the Virtual Control, all the components we created are considered as UIElements, in other words, handling them using the mechanism that the Camera is based on, is feasible.

Implementation of the Camera Control

    public class VirtualControl : CanvasCamera
    {
       public VirtualControl()
       {

       }
    }

Inheriting the properties of the Camera Control means that we will be able to apply the operations of moving, panning and zooming, etc.

Managment of Nodes

Nodes define the most crucial part of the project, thus, handling them with carefulness is a must. The virtual control is dedicated to taking care of every task that results in the manipulation of nodes.

Nodes, when hosted on the camera control, have two states. A node is either Selected or not. When selected, the node's border will glow in golden colour.

The distinction between a selected node and another is very important- for the selected nodes being the ones who will play the role of the operands in every node-related operation. For example, we can only copy the selected nodes.

Such operations include:

  • Move

    The only nodes you can move are the selected ones- that said, if you select multiple nodes, then all of those selected nodes will move simultaneously. This kind of operation is similar to that of the planning we discussed above.

  • Create

    The creation of new nodes consists of adding the new instanced node the list of nodes that are already hosted. Once that happens, the camera control will eventually consider it as one of its children.

  • Delete

    The process of deleting a node is not as easy as the creation of one. along the process of scripting, the user will work on a specific node, change its location, change its inner-data, link it to another node, add more ports to it, etc. This makes the deletion of a node tedious operation because the node is a part of the graph. For example, we have this node that has 21 connectors and data that is flowing through it, deleting it means the deletion of everything that is related to it.

    • To delete a node, just select it, and then press the delete button on your keyboard. Alternatively, you can use the context menu of the virtual control.

  • Copy

    Copying a node means that we will extract some kind of metadata and use it later to generate a clone of that copied node with respect to its characteristics. Truth is, we will serialize that node- we will get into the details later on in the de/serialization section.

    You can easily copy a node by performing a CTRL+V coupled-click or by using the context menu of the virtual control.

  • Paste

    Pasting a node means that we will try to exploit the copied metadata and use it to generate a new functional node. It is more like deserializing the metadata.

  • Cut

    There isn't much to talk about in here, the cutting operation is nothing but a sequential execution of the operations of copying and deleting.

  • Commenting

    Commenting on a zone of your graph is a salubrious practice. Truth is, it helps you organize your graph and divides your project into regions.

Management of Connections

Connections are used to connect ports and nodes to each other. There exist two types of connectors, ObjectsConnector and ExecutionConnector. Truth is, managing the connections is not solely the task of the virtual control, connections are managed via multiple players, such as nodes themselves. When a node moves, it triggers the events of the connectors which will make it change the coordinates of the wire.

With that being said, the virtual controls still plays a solid role when it comes to managing the connectors. You can create connections, divide them or even delete them.

Creating a connector requires calling the NodesManager static class- a class that contains multiple methods and functions that help the Virtual Control manage things.

For a starter, creating an execution connector, all you need to do is to call the CreateExecutionConnector method. This method takes the virtual control that hosts the parent nodes, the first and the second port as parameters.

NodesManager.CreateExecutionConnector(Host, portA, portB); 

Deleting a connector is a less tedious process, just call the Delete(); method and see the magic.

Connector.Delete();

Dividing a connector is not always possible for the division being a special operation that can only be performed if, and only if, the operand is an ObjectsConnector.

               if (StartPort.ParentNode.Types != NodeTypes.SpaghettiDivider &&
                    EndPort.ParentNode.Types != NodeTypes.SpaghettiDivider)
                    Task.Factory.StartNew(() =>
                    {
                        Wire.Dispatcher.BeginInvoke(DispatcherPriority.SystemIdle,
                            new Action(() =>
                            {
                                var divider = new SpaghettiDivider(Host, this, false);
                                Host.AddNode(divider, Mouse.GetPosition(Host).X, 
                                             Mouse.GetPosition(Host).Y);
                                e.Handled = true;
                            }));
                    });
                e.Handled = true;

Notes

Even though all the connectors and wires are hosted on the Virtual Control, the vControl only plays the role of the caller, by calling the functions and methods that are needed to manage the connections between ports & nodes.

VirtualControl

Comments

Commenting zones of your graph may prove useful, it is more like creating regions in C#. With that being said, what is, technically, a comment?

A comment is actually a rectangular component that has a flexible size. The moment of its creation, it calculates the coordinates and the area that is covered by the selected nodes.

The coordinates (X; Y) of the comment are the same as the first node of the selected ones. However, that is not the case for the width & height of the comment.

To determine the width and height of the comment based on all the nodes that we want to comment on, we need to go through all of them and compare their sizes and coordinates.

Technical Aspects

To find the genuine width and the height of the comment, we should call:

Width = max_Width(nodes) + 30;

Height = max_Height(nodes) + 40;

The prolongment of the size by 30 & 40 is not a mere act as it seems, we've done that to leave more space inside the comment so we won't have our nodes standing on the edge.

Searching for adequate width:

        private double max_Width(ObservableCollection<Node> nodes)
        {
            var maxwidth = nodes[0].ActualWidth;
            foreach (var node in nodes)
                if (node.ActualWidth + node.X > maxwidth + X)
                    maxwidth += node.ActualWidth + node.X - (maxwidth + X);
            return maxwidth;
        }

Height:

        private double max_Height(ObservableCollection<Node> nodes)
        {
            var maxheight = nodes[0].ActualHeight;
            foreach (var node in nodes)
                if (node.ActualHeight + node.Y > maxheight + Y)
                    maxheight += node.ActualHeight + node.Y - (maxheight + Y);
            return maxheight;
        }

Resize

Comments are flexible and resizable, you can resize the comment using the bottom-right handler.

When the left mouse button is down, the MouseMode will be set to a state that indicates that the event that must follow the movement of the mouse is the modification of the size of the comment.

            if (MouseMode == MouseMode.ResizingComment && 
                mouseEventArgs.LeftButton == MouseButtonState.Pressed)
            {
                Cursor = Cursors.SizeNWSE;
                var currentPoint = Mouse.GetPosition(this);
                if (currentPoint.Y - TempComment.Y > 0 && 
                    currentPoint.X - TempComment.X > 0)
                {
                    TempComment.Height =  currentPoint.Y - TempComment.Y;
                    TempComment.Width =  currentPoint.X - TempComment.X;
                    TempComment.LocateHandler();
                }
                else
                {
                    TempComment.Height = 32;
                    TempComment.Width = 32;
                    TempComment.LocateHandler();
                }
                return;
            }

Moving

The contents of a comment are nodes, and each comment works as a container. Once you move a container, its whole contents will be transferred all along.

        private void Comment_MouseDown(object sender, MouseButtonEventArgs e)
        {
            _host.MouseMode = MouseMode.Selection;
            _host.SelectedComment = this;
            _host.SelectedNodes.Clear();
            foreach (var node in _host.Nodes)
                if (node.X >= X &&
                    node.X + node.ActualWidth <= X + ActualWidth &&
                    node.Y >= Y &&
                    node.Y + node.ActualHeight <= Y + ActualHeight)
                    _host.SelectedNodes.Add(node);
        }

Uncomment (Deleting the Comment)

Being a UIElement, a comment can be duly deleted too when it is no more needed.

comment.Dispose();

Style

The style of the comment is quite basic. It is composed of a textbox, borders and a handler.

   <Style x:Key="Comment" TargetType="core:Comment">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate>
                    <Canvas Background="Transparent">

                        <Border
                            Background="#33FFFFFF"
                            Height="25"
                            Width="{Binding RelativeSource=
                            {RelativeSource TemplatedParent}, Path=Width}" />

                        <Border Background="#99B2B2B2"
                                Margin="0,0,0,4"
                                Width="{Binding RelativeSource=
                                {RelativeSource TemplatedParent}, Path=Width}"
                                Height="{Binding RelativeSource=
                                {RelativeSource TemplatedParent}, Path=Height}"
                                BorderBrush="#FF769954"
                                BorderThickness="2" />
                        <TextBox
                            FontSize="16"
                            Foreground="Black"
                            BorderBrush="Transparent"
                            Background="Transparent"
                            Width="{Binding RelativeSource=
                            {RelativeSource TemplatedParent}, Path=Width}"
                            Text="{Binding RelativeSource=
                            {RelativeSource TemplatedParent},
                             Path= Summary, Mode=TwoWay}" />
                        <StackPanel Name="CornerImage_Resize" Height="12" Width="12">
                            <StackPanel.Background>
                                <ImageBrush ImageSource=
                                 "../MediaResources/handle_resize.png" />
                            </StackPanel.Background>
                        </StackPanel>
                    </Canvas>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

The virtual control works as a container, it hosts all the elements we created and manages them adequately; however, it is not enough, we need more tools to help us make a pleasant visual scripting system.

Comments

Controls

Controls are magical tools that you can exploit to make perfect use of the visual scripting environment, these controls can help you manage variables that hold data, a list of nodes to add to your graph, etc.

Variables List

Variables play a solid role in each graph you create, they help you store the output of your operations.

To present the capabilities of the variables list, we will try to turn this formula into a graph representation.

Here, we can easily observe that the introduction of two special nodes, the Set node and the Get node. The Set node serves as a sub-application that redirects the output of the whole visual operations to the X variable. The Get node, on the other hand, serves as a data container, it returns the value of a specific variable.

Now that we understand how the variables list communicates and its purposes, we can safely dig into its depths.

Variables List: Add/Delete

Adding a variable means creating it and assigning two nodes (Set, Get) to it. By default, a variable's type is Generic and it is nameless.

Deleting a variable, on the other hand, is not that easy. Deleting a variable means deleting everything that is related to it- its related Get/Set nodes, for example.

Variables List: Exploit

One of the most beneficial features of the variables list is that it supports the Drag and Drop mechanism. In other words, you can easily drag your variable and drop it on your graph- then, watch the magic.

The truth is, the hovering element, that represents a visualization of the drag and drop mechanism is nothing but a window that has flexible properties. Those properties are based on the properties of the variable that we are intending to exploit.

Style

    <Style x:Key="VariableHoster" TargetType="controls:VariableHoster">
        <Setter Property="ShowInTaskbar" Value="False" />
        <Setter Property="WindowStyle" Value="None" />
        <Setter Property="ResizeMode" Value="NoResize" />
        <Setter Property="Height" Value="25" />
        <Setter Property="Width" Value="100" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate>
                    <Canvas Background="#353535">
                        <Border x:Name="GoldenBorder"
                                Width="{Binding RelativeSource=
                                {RelativeSource TemplatedParent},Path=ActualWidth}"
                                Height="{Binding RelativeSource=
                                {RelativeSource TemplatedParent},Path=ActualHeight}"
                                CornerRadius="4,4,4,4"
                                BorderThickness="2"
                                Background="{DynamicResource GlobalBackground}">
                            <Border.BorderBrush>
                                <RadialGradientBrush>
                                    <GradientStop Color="#FFFFB10C" Offset="0.215" />
                                    <GradientStop Color="#FF916E24" Offset="0" />
                                </RadialGradientBrush>
                            </Border.BorderBrush>
                        </Border>
                        <Border x:Name="Icon" Height="16" 
                        Width="16" Margin="5,3,0,0">
                            <Border.Background>
                                <ImageBrush ImageSource="{Binding RelativeSource=
                                            {RelativeSource Self},Path=Icon}" />
                            </Border.Background>
                        </Border>
                        <TextBlock x:Name="VarName" Margin="25,3,0,0" 
                                                    Foreground="AliceBlue" />
                    </Canvas>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

Variables List: Tracing

Tracing your variable means counting how many times it gets referenced.

For example, say that we used the variable y 3 times (2 setters, 1 getter), and the variable x, also 3 times (1 setter, 2 getters):

We should get these notes showing as tooltips:

AND

By passing the cursor on the degree-like icon, a tooltip will show. That tooltip will tell you how many times this variable has been used, and what was the type of usage.

VariablesList

Nodes Tree

The tree of nodes is the tool that will help you add nodes to the virtual control. The truth is, it is a combination of a dynamic TreeView. Its items do represent the metadata of every node we have.

In the process of the creation of this control, I, accidentally, managed to create the fastest filtering algorithm for WPF's TreeViews. That said, I & Dirk shared our expertise and released a better and a more optimized filtering algorithm. It can be found here: Advanced WPF TreeViews in C#/VB.Net Part 3 of n.

Nodes Tree: Template

The tree of nodes has two states, pre-expanded and post-expanded.

Pre-expanded

  • Green: The green zone represents the text box that can be eventually used to filter the tree.
  • Red: The red zone represents the categories of the nodes, and, of course, the nodes within.

Post-expanded

  • Blue: The blue zone represents the title of the node.
  • Green: The green zone represents the description or the tooltip that is related to the node.

Alternatively, we can notice the custom scrollbar on the right side of the tree.

Nodes Tree: Classification and Sorting

All the nodes are contained within a DLL file (as a form of flexibility that allows you create plugins/add-ons), and each node belongs to a specific category. With that being mentioned, the classification of the nodes within the tree will be based on the present categories, i.e., (Math, Arrays, Vectors, etc.).

Sorting the categories, on the other hand, is based on the number of the nodes that are contained inside.

The category of the node is set in the process of its creation. In its initial form, the tree of nodes will contain only the categories of the nodes- eventually, it will iterate through all the available nodes and place them respectively. The only category that is set by default, is the variables category, where all the variables will be hosted.

The magic behind building the categories:

        private void BuildCategories()
        {
            _categories = new List<Dictionary<int, string>>();

            foreach (var node in _pluginsManager.LoadedNodes)
                if (!_catNames.Contains(node.Category))
                    _catNames.Add(node.Category);
            if (VariablesTreeRootIndex() != -1)
                _catNames.Remove("Variables");
            for (var index = 0; index < _catNames.Count; index++)
            {
                var name = _catNames[index];
                var dic = new Dictionary<int, string> {{index, name}};
                _categories.Add(dic);
            }
            for (var index = 0; index < _categories.Count; index++)
            {
                var item = _categories[index];
                var name = item[index];
                if (name != null) Roots.Add(new NodeItem("", name));
            }
        }

The magic behind building node instances:

        private void BuildNodes()
        {
            Task.Factory.StartNew(() =>
            {
                Dispatcher.BeginInvoke(new Action(() =>
                {
                    foreach (var node in _pluginsManager.LoadedNodes)
                        for (var index = 0; index < Roots.Count; index++)
                            if (node.Category == Roots[index].NodeName)
                                Roots[index].Nodes.Add(new NodeItem("", node.Clone()));
                }));
            });
        }

Nodes Tree: Insertion

By insertion, we mean the creation of a new instance of a specific node, inserting it into the virtual control. That said, the process of insertion has two different states:

  • Generic Insertion: Occurs when you normally open the tree of nodes
  • Altered Insertion: Occurs when you open the tree of nodes by dragging a wire of a node

Generic Insertion

You can open the tree of nodes by performing a double-click, or by using the context menu of the virtual control. Eventually, you select the node that you are intending you add, and simply add it by press on the key [ENTER] or via a mouse-double-click.

Altered Insertion

This part is relatively genuine. Instead of adding a node, then linking its ports to other nodes- you can, simply, drag the wire that you are intending to link and wait for the tree of nodes to show you all the available options.

The linking operation will be performed automatically.

Magically, the two insertion approaches do have the same code-behind:

        private void InsertNode(Node node)
        {
            Task.Factory.StartNew(() =>
            {
                Application.Current.Dispatcher.BeginInvoke
                            (DispatcherPriority.Render, new Action(() =>
                {
                    _host.AddNode(node, Canvas.GetLeft(this), Canvas.GetTop(this));

                    node.Dispatcher.BeginInvoke
                         (DispatcherPriority.Loaded, new Action(() =>
                    {
                        if (_host.TemExecPort != null && node.InExecPorts.Count > 0)
                            if (_host.TemExecPort.ConnectedConnectors.Count > 0)
                            {
                                string id1 = Guid.NewGuid().ToString(), 
                                       id2 = Guid.NewGuid().ToString();

                                var thirdNode = 
                                _host.TemExecPort.ConnectedConnectors[0].
                                EndPort.ParentNode;
                                NodesManager.CreateExecutionConnector
                                (_host, _host.TemExecPort, node.InExecPorts[0],
                                    id1);
                                NodesManager.CreateExecutionConnector
                                             (_host, node.OutExecPorts[0],
                                    thirdNode.InExecPorts[0], id2);
                            }
                            else
                            {
                                NodesManager.CreateExecutionConnector
                                (_host, _host.TemExecPort, node.InExecPorts[0]);
                            }
                        else if (_host.TemObjectPort != null && 
                                  node.InputPorts.Count > 0)
                            NodesManager.CreateObjectConnector
                                 (_host, _host.TemObjectPort, node.InputPorts[0]);
                    }));
                }));
            });
        }

Calling the above function and the creation of the node:

                    var hostedNode = ((NodeItem) _tv.SelectedItem)?.HostedNode;
                    if (hostedNode != null)
                    {
                        var node = hostedNode.Clone();
                        InsertNode(node);
                        Remove();
                    }

The abilities of the tree of nodes are not limited to that extent, it can also manage the variables.

We are referring to the X & Y variables in this screenshot.

NodesTree

InnerMessageBox

This control works like an internal notification, tooltip and an indicator. It has an icon that serves as a sign. We have already discussed the role of this component above when we talked about the linking possibilities.

        public enum InnerMessageIcon
        {
            Correct,
            Warning,
            False
        }

InnerMessageBox

Search Window

Every node has a built-in function that returns a TreeViewItem if it contains metadata that matches the item that we are looking for. It returns null if it does not.

           public virtual FoundItem Search(string key)
        {
            if (IsCollapsed) return null;
            var KEY = key.ToUpper();
            var fi = new FoundItem();
            if (Title.ToUpper().Contains(KEY))
            {
                fi.foundNode = this;
                fi.Hint = Title;
                fi.Type = ItemTypes.Node;
            }
            foreach (var port in InputPorts)
                if (port.IsVisible)
                {
                    if (port.Control is TextBox)
                    {
                        if (((TextBox) port.Control).Text.ToUpper().Contains(KEY))
                        {
                            if (fi.Hint != Title)
                                fi.Hint = Title;
                            fi.Items.Add(new FoundItem
                            {
                                Hint = $"   In input :{(port.Control as TextBox).Text}",
                                Type = ItemTypes.Port,
                                foundNode = this
                            });
                        }
                    }
                    else
                    {
                        var textBox = port.Control as UnrealControlsCollection.TextBox;
                        if (textBox == null || 
                            !textBox.Text.ToUpper().Contains(KEY)) continue;
                        var box = port.Control as UnrealControlsCollection.TextBox;
                        if (box != null)
                            if (fi.Hint != Title)
                                fi.Hint = Title;
                        fi.Items.Add(new FoundItem(port.Background)
                        {
                            Hint = $"In input :{box.Text}",
                            Type = ItemTypes.Port,
                            foundNode = this,
                            Brush = port.StrokeBrush
                        });
                    }
                }
                else
                {
                    var textBox = port.Control as UnrealControlsCollection.TextBox;
                    if (textBox == null || 
                       !textBox.Text.ToUpper().Contains(KEY)) continue;
                    var box = port.Control as UnrealControlsCollection.TextBox;
                    if (box != null)
                        if (fi.Hint != Title)
                            fi.Hint = Title;
                    fi.Items.Add(new FoundItem(port.Background)
                    {
                        Hint = $"In input :{box.Text}",
                        Type = ItemTypes.Port,
                        foundNode = this,
                        Brush = port.StrokeBrush
                    });
                }
            foreach (var port in OutputPorts)
                if (port.IsVisible)
                {
                    if (port.Control is TextBox)
                    {
                        if (((TextBox) port.Control).Text.ToUpper().Contains(KEY))
                        {
                            if (fi.Hint != Title)
                                fi.Hint = Title;
                            fi.Items.Add(new FoundItem
                            {
                                Hint = $"   In input :{(port.Control as TextBox).Text}",
                                Type = ItemTypes.Port,
                                foundNode = this
                            });
                        }
                    }
                    else
                    {
                        var textBox = port.Control as UnrealControlsCollection.TextBox;
                        if (textBox == null || 
                           !textBox.Text.ToUpper().Contains(KEY)) continue;
                        var box = port.Control as UnrealControlsCollection.TextBox;
                        if (box != null)
                            if (fi.Hint != Title)
                                fi.Hint = Title;
                        fi.Items.Add(new FoundItem(port.Background)
                        {
                            Hint = $"In input :{box.Text}",
                            Type = ItemTypes.Port,
                            foundNode = this,
                            Brush = port.StrokeBrush
                        });
                    }
                }
                else
                {
                    var textBox = port.Control as UnrealControlsCollection.TextBox;
                    if (textBox == null || 
                       !textBox.Text.ToUpper().Contains(KEY)) continue;
                    var box = port.Control as UnrealControlsCollection.TextBox;
                    if (box != null)
                        if (fi.Hint != Title)
                            fi.Hint = Title;
                    fi.Items.Add(new FoundItem(port.Background)
                    {
                        Hint = $"In input :{box.Text}",
                        Type = ItemTypes.Port,
                        foundNode = this,
                        Brush = port.StrokeBrush
                    });
                }
            if (fi.Hint != Title)
                return null;
            fi.foundNode = this;
            return fi;
        }

Once we launch the search operation, the algorithm will iterate through all the nodes and check if they contain a similar metadata within. If yes, then the TreeViewItem that will return will be hosted on the Search Window.

The FoundItem will operate as the standalone component itself. By double-clicking on it, it will take you to the node where the data is contained.

     public class FoundItem : TreeViewItem
    {
        private readonly Path ctrl = 
        new Path {Width = 15, Height = 15, Margin = new Thickness(0, -2, 0, 0)};

        private readonly TextBlock hint =
            new TextBlock {Foreground = Brushes.WhiteSmoke, 
                           Background = Brushes.Transparent};

        private string _hint;

        private Brush brush;

        public ItemTypes Type;

        public FoundItem(Brush b = null)
        {
            IsExpanded = true;
            Loaded += (s, e) =>
            {
                MouseDoubleClick += FoundItem_MouseDoubleClick;

                var sp = new StackPanel {Orientation = Orientation.Horizontal, 
                                         MaxHeight = 20};
                sp.Children.Add(ctrl);
                sp.Children.Add(hint);
                Header = sp;
                if (Type == ItemTypes.Port)
                {
                    ctrl.Style = FindResource("ObjectPin") as Style;
                    ctrl.Stroke = b;
                }
                else
                {
                    ctrl.Style = FindResource("ExecPin") as Style;
                }
            };
        }

        public string Hint
        {
            get { return _hint; }
            set
            {
                _hint = value;
                if (hint != null) hint.Text = value;
            }
        }

        public Brush Brush
        {
            get { return brush; }
            set
            {
                brush = value;
                ctrl.Stroke = value;
                ctrl.Fill = value;
            }
        }

        public Node foundNode { get; set; }

        private void FoundItem_MouseDoubleClick(object sender, MouseButtonEventArgs e)
        {
            foundNode.Host.GoForNode(foundNode);
        }
    }

The GoForNode is a function that takes a node as a parameter. It moves all the UIElements that are rendered on the VirtualControl in order to put the selected node in the centre of the screen.

         public void GoForNode(Node node)
        {
            node.X = Math.Truncate(node.X);
            node.Y = Math.Truncate(node.Y);
            var origin = BasisOrigin;
            origin.X -= node.ActualWidth - 10;
            origin.X = Math.Truncate(origin.X);
            origin.Y = Math.Truncate(origin.Y);
            var difference = Point.Subtract(origin, new Point(node.X, node.Y));
            var timer = new DispatcherTimer {Interval = new TimeSpan(0, 0, 0, 0, 01)};
            foreach (var n in Nodes)
            {
                if (difference.X < 0)
                    n.X += difference.X;
                else
                    n.X += difference.X;
                if (difference.Y < 0)
                    n.Y += difference.Y;
                else
                    n.Y += difference.Y;
                NeedsRefresh = true;
            }
            difference.X = 10;
            difference.Y = 0;
            if (difference.X != 0 || difference.Y != 0)
                timer.Start();
            timer.Tick += (s, e) =>
            {
                if (difference.X == 0 && difference.Y == 0)
                    timer.Stop();
                foreach (var n in Nodes)
                {
                    if (difference.X > 0)
                        n.X++;
                    else
                        n.X--;
                    if (difference.Y > 0)
                        n.Y++;
                    else
                        n.Y--;
                }
                if (difference.X > 0)
                    difference.X--;
                else
                    difference.X++;
                if (difference.Y > 0)
                    difference.Y--;
                else
                    difference.Y++;
            };
            SelectedNodes.Clear();
            SelectedNodes.Add(node);
        }

Search

Contents Browser

The contents browser plays the role of a files explorer that is adaptive to the visual scripting environment.

Overview

The contents browser, while not being stable yet, is a tool that can be used like a bridge between the visual scripting environment and the files on the hard drive. It plays the same role as the Solution Explorer of Visual Studio but in a more genuine way.

Template

The template is quite generic, and it is divided into three sections.

  1. The tools bar

    The tools bar is a panel that contains a few buttons that allow you to create a new file/folder, import an existing file, and save all the changes you have made. Also, it supports undo/redo operations.

  2. Folders explorer

    The folders explorer is a tree that represents all the folder within a specific path.

  3. Contents explorer

    Every file and folder will show in the contents explorer. Not only that, it classifies the files based on their extensions by giving them specific colours that would help us distinguish the difference.

    i.e.:

    Folders, on the other hand, are more flexible as well. In fact, a folder is a combination of two paths (a.k.a., 2D shapes). You can easily customize a folder, by changing its colours and shapes.

    1. Folder's head

      The folder, as we have already mentioned above, is divided into two components. The header of the folder and the core.

    2. Folder's core

      The core of the folder is the part of the path that will have its colour changed eventually.

      Changing the colour of the folder can be done easily. Here are two samples:

  4. Search bar

    Nothing fancy nor sophisticated is related to this part. It is a bar that can filter the files that you are exploring based on the standard search patterns of windows.

    Example: "*.txt* to show only the files which have '.txt' as an extension.

Styles

ExplorerItem

     <Style x:Key="ContentBrowserElement" TargetType="{x:Type ListViewItem}">
        <Setter Property="HorizontalAlignment" Value="Left" />
        <Setter Property="FocusVisualStyle" Value="{x:Null}" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type ListViewItem}">
                    <Grid HorizontalAlignment="Left" VerticalAlignment="Top">
                        <Border x:Name="border" 
                                BorderBrush="{x:Null}" BorderThickness="1"
                                HorizontalAlignment="Stretch" 
                                VerticalAlignment="Stretch" CornerRadius="2.5" />
                        <WrapPanel HorizontalAlignment="Stretch" 
                                   VerticalAlignment="Stretch">
                            <ContentPresenter Margin="3,3,5,30" />
                        </WrapPanel>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    <Style x:Key="ExplorerItem" TargetType="controls:ExplorerItem">

        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate>
                    <Border x:Name="ExplorerItemContainer" CornerRadius="8">
                        <StackPanel Orientation="Vertical" Margin="3" 
                         Background="{Binding BackColor}" Width="68"
                                    Height="100"
                                    ClipToBounds="True">
                            <Canvas x:Name="Image" Visibility="Hidden">
                                <Rectangle Fill="{Binding ItemColor}" 
                                           Opacity=".4" Canvas.Left="2"
                                           x:Name="SelectionBox" 
                                           Height="64" Width="64" 
                                           StrokeThickness="1">
                                    <Rectangle.Effect>
                                        <BlurEffect Radius="3" />
                                    </Rectangle.Effect>
                                </Rectangle>

                                <Rectangle Canvas.Left="2" Canvas.Top="57" 
                                           Height="6.4" Width="64"
                                           Fill="{Binding ItemColor}" 
                                           RadiusY="4" RadiusX="4" />
                                <Rectangle x:Name="Thumbnail" Width="42" 
                                           Height="42" Canvas.Left="12" 
                                           Canvas.Top="6" />
                            </Canvas>
                            <Grid x:Name="Folder" 
                            ClipToBounds="True" Margin="0,12,0,0"
                                  Visibility="Hidden" Width="52" Height="52">
                                <StackPanel Orientation="Vertical">
                                    <Path Stretch="Uniform" 
                                          StrokeThickness="1.33333337">
                                        <Path.Fill>
                                            <LinearGradientBrush>
                                                <GradientStop Color="#FF141F2B" 
                                                              Offset="0.774" />
                                                <GradientStop Color="#FF979797" />
                                            </LinearGradientBrush>
                                        </Path.Fill>
                                        <Path.Data>
                                            <PathGeometry
                                                Figures="m 36.08425 225.37304 
                                                c 3.687503 -0.38572 5.000001 
                                                -1.13954 5.000001 -2.87168 0 
                                                -4.92432 4.279726 -14.59278 
                                                8.064553 -18.21887 2.169627 
                                                -2.07864 6.743628 -5.31508 
                                                10.164449 -7.19208 9.241516 
                                                -5.07084 10.814177 -8.05027 
                                                12.940261 -24.5155 1.607543 
                                                -12.44945 2.399082 -15.09711 
                                                5.751785 -19.23949 2.138244 
                                                -2.64187 6.255744 -6.04563 9.150001 
                                                -7.56391 5.140936 -2.69685 7.276218 
                                                -2.77019 92.59562 -3.18018 95.45312 
                                                -0.45869 98.15761 -0.28718 106.152 
                                                6.73198 7.0916 6.22651 8.7382 10.6307 
                                                9.68435 25.90307 0.73396 11.84719 
                                                1.26586 14.14613 3.87724 16.75775 
                                                l 3.02658 3.02687 163.62993 0.66666 
                                                c 162.75799 0.66312 163.65832 0.68152 
                                                168.96324 3.45406 6.35662 3.3222 
                                                12.57175 12.61305 13.60857 20.3431 
                                                0.69946 5.21488 0.91185 5.41624 
                                                6.22446 5.90136 3.02518 0.27624 
                                                -136.0997 0.50623 -309.16637 
                                                0.51107 -173.06667 0.005 
                                                -312.41667 -0.22655 -309.66667 
                                                -0.51421 z"
                                                FillRule="NonZero" />
                                        </Path.Data>
                                    </Path>
                                    <Path Margin="0,-2,0,0" Stretch="Uniform" 
                                          StrokeThickness="1.33333337">
                                        <Path.Fill>
                                            <LinearGradientBrush EndPoint="0.5,1" 
                                                 StartPoint="0.5,0">
                                                <GradientStop Color="Black" 
                                                 Offset="1.5" />
                                                <GradientStop Color="#FF6E6E6E" 
                                                 Offset="0.029" />
                                                <GradientStop Color="#FF000102" />
                                            </LinearGradientBrush>
                                        </Path.Fill>
                                        <Path.Data>
                                            <PathGeometry
                                                Figures="m 35.46142 662.88211 
                                                         c -5.645595 -6.04364 -6.953389 
                                                         -12.93947 -7.582867 -39.98349 
                                                         -0.31759 -13.64451 -1.179782 
                                                         -51.50821 -1.915981 -84.14154 
                                                         -1.96164 -86.95319 -5.166926 
                                                         -162.45026 -9.992734 -235.36814 
                                                         -1.271839 -19.21746 -1.512834 
                                                         -30.87022 -0.697253 -33.71399 
                                                         2.083099 -7.26333 6.312426 
                                                         -12.8508 12.339504 -16.302 
                                                         l 5.732545 -3.28255 314.483266 
                                                         -0.34436 c 218.55465 -0.2393 
                                                         316.08585 0.0872 319.73701 
                                                         1.07034 6.94134 1.8691 
                                                         15.21772 10.23518 17.12215 
                                                         17.30772 1.12319 4.17118 
                                                         0.98421 13.3284 -0.57271 
                                                         37.73698 -5.17222 81.08726 
                                                         -8.05125 149.04533 -10.06429 
                                                         237.56267 -2.32833 102.38191 
                                                         -2.43281 105.12652 -4.2054 
                                                         110.47999 -0.93725 2.83066 
                                                         -3.32391 6.88066 -5.30367 
                                                         9 l -3.59956 3.85334 H 
                                                         350.01131 39.081194 Z"
                                                FillRule="NonZero" />
                                        </Path.Data>
                                        <Path.Effect>
                                            <DropShadowEffect BlurRadius="5" 
                                             Color="Black" />
                                        </Path.Effect>
                                    </Path>
                                </StackPanel>
                            </Grid>
                            <Label x:Name="Tag" BorderBrush="Transparent" 
                                                VerticalAlignment="Center"
                                  Background="Transparent" HorizontalAlignment="Center">
                                <TextBlock Foreground="{Binding Foreground}" 
                                           VerticalAlignment="Center" MaxWidth="65"
                                           MaxHeight="40" TextWrapping="Wrap" 
                                           Background="Transparent"
                                           TextTrimming="CharacterEllipsis" 
                                           HorizontalAlignment="Center"
                                           Text="{Binding ItemName}" />

                            </Label>
                            <TextBox Visibility="Collapsed" x:Name="RenameBox" 
                                     Text="{Binding ItemName}"
                                     Background="Transparent" Foreground="WhiteSmoke"
                                     TextWrapping="Wrap" AcceptsReturn="True" 
                                     VerticalScrollBarVisibility="Disabled"
                                     HorizontalAlignment="Center" MaxWidth="64" 
                                     MaxHeight="40"
                                     BorderBrush="White" BorderThickness="1">
                                <TextBox.Effect>
                                    <DropShadowEffect BlurRadius="10" Color="White" />
                                </TextBox.Effect>
                            </TextBox>
                        </StackPanel>
                        <Border.ContextMenu>
                            <ContextMenu />
                        </Border.ContextMenu>
                    </Border>
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsMouseOver" Value="True">
                            <Setter TargetName="ExplorerItemContainer" 
                                    Property="Background">
                                <Setter.Value>
                                    <DrawingBrush>
                                        <DrawingBrush.Drawing>
                                            <DrawingGroup>

                                                <GeometryDrawing>
                                                    <GeometryDrawing.Brush>
                                                        <LinearGradientBrush 
                                                         StartPoint="0,0" EndPoint="0,1"
                                                         SpreadMethod="Pad">
                                                            <GradientStop 
                                                             Color="#FF6C6C6C" Offset="1" />
                                                            <GradientStop 
                                                             Color="#22FFFFFF" Offset="0" />
                                                        </LinearGradientBrush>
                                                    </GeometryDrawing.Brush>
                                                    <GeometryDrawing.Geometry>
                                                        <RectangleGeometry 
                                                         Rect="0,0 1,0.48" />
                                                    </GeometryDrawing.Geometry>
                                                </GeometryDrawing>
                                            </DrawingGroup>
                                        </DrawingBrush.Drawing>
                                    </DrawingBrush>
                                </Setter.Value>
                            </Setter>
                        </Trigger>
                        <DataTrigger Binding="{Binding IsSelected}" Value="True">
                            <Setter TargetName="ExplorerItemContainer" 
                                    Property="Border.Background">
                                <Setter.Value>
                                    <LinearGradientBrush StartPoint="0,0" EndPoint="0,1"
                                                         SpreadMethod="Pad">
                                        <GradientStop Color="#99FFE909" Offset="1" />
                                        <GradientStop Color="#FF232000" Offset="0" />
                                    </LinearGradientBrush>
                                </Setter.Value>
                            </Setter>
                            <Setter TargetName="ExplorerItemContainer" 
                                    Property="Border.BorderThickness" Value="2" />

                            <Setter TargetName="ExplorerItemContainer" 
                                    Property="Border.BorderBrush">
                                <Setter.Value>
                                    <LinearGradientBrush StartPoint="0,0" EndPoint="0,1"
                                                         SpreadMethod="Pad">
                                        <GradientStop Color="#99FFB109" Offset="0" />
                                        <GradientStop Color="#CC383302" Offset="1" />
                                    </LinearGradientBrush>
                                </Setter.Value>
                            </Setter>

                        </DataTrigger>
                    </ControlTemplate.Triggers>

                </ControlTemplate>
            </Setter.Value>
        </Setter>
        <Setter Property="ToolTip">

            <Setter.Value>
                <Grid>
                    <Border CornerRadius="5">
                        <Border.Background>
                            <LinearGradientBrush>
                                <GradientStop Color="#99CD3800" Offset="0.763" />
                                <GradientStop Color="#FFCB8C1F" Offset="0.028" />
                            </LinearGradientBrush>
                        </Border.Background>
                        <StackPanel Orientation="Vertical" Background="#353535" 
                                                           Margin="3,3,3,3">
                            <StackPanel Orientation="Horizontal">
                                <TextBlock Text="File Name:   " 
                                           Foreground="Gray" FontSize="13" />
                                <TextBlock Foreground="WhiteSmoke" 
                                           Text="{Binding ItemName}" FontSize="13" />
                            </StackPanel>
                            <Border BorderBrush="Azure" BorderThickness="0,1,0,0" 
                                    Margin="0,8" Background="#FF09172B" />
                            <StackPanel Orientation="Horizontal">
                                <TextBlock Text="Type:   " Foreground="Gray" />
                                <TextBlock Foreground="WhiteSmoke" 
                                           Text="{Binding Extension}" />
                            </StackPanel>
                            <StackPanel Orientation="Horizontal">
                                <TextBlock Text="Size:    " Foreground="Gray" />
                                <TextBlock Foreground="WhiteSmoke" 
                                           Text="{Binding Size}" />
                            </StackPanel>
                            <StackPanel Orientation="Horizontal">
                                <TextBlock Text="Path:   " Foreground="Gray" />
                                <TextBlock Foreground="WhiteSmoke" 
                                           Text="{Binding Path}" MaxWidth="300"
                                           Margin="0,0,3,0" TextWrapping="Wrap" />
                            </StackPanel>
                        </StackPanel>
                    </Border>
                </Grid>
            </Setter.Value>
        </Setter>
    </Style>
    <Style TargetType="ToolTip">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="ToolTip">
                    <ContentPresenter />
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

Folder (Header):

     <Path Stretch="Uniform" StrokeThickness="1.33333337">
                                        <Path.Fill>
                                            <LinearGradientBrush>
                                                <GradientStop Color="#FF141F2B" 
                                                              Offset="0.774" />
                                                <GradientStop Color="#FF979797" />
                                            </LinearGradientBrush>
                                        </Path.Fill>
                                        <Path.Data>
                                            <PathGeometry
                                                Figures="m 36.08425 225.37304 c 
                                                3.687503 -0.38572 5.000001 -1.13954 
                                                5.000001 -2.87168 0 -4.92432 4.279726 
                                                -14.59278 8.064553 -18.21887 2.169627 
                                                -2.07864 6.743628 -5.31508 10.164449 
                                                -7.19208 9.241516 -5.07084 10.814177 
                                                -8.05027 12.940261 -24.5155 1.607543 
                                                -12.44945 2.399082 -15.09711 5.751785 
                                                -19.23949 2.138244 -2.64187 6.255744 
                                                -6.04563 9.150001 -7.56391 5.140936 
                                                -2.69685 7.276218 -2.77019 92.59562 
                                                -3.18018 95.45312 -0.45869 98.15761 
                                                -0.28718 106.152 6.73198 7.0916 
                                                6.22651 8.7382 10.6307 9.68435 
                                                25.90307 0.73396 11.84719 1.26586 
                                                14.14613 3.87724 16.75775 l 3.02658 
                                                3.02687 163.62993 0.66666 c 162.75799 
                                                0.66312 163.65832 0.68152 168.96324 
                                                3.45406 6.35662 3.3222 12.57175 
                                                12.61305 13.60857 20.3431 0.69946 
                                                5.21488 0.91185 5.41624 6.22446 
                                                5.90136 3.02518 0.27624 -136.0997 
                                                0.50623 -309.16637 0.51107 -173.06667 
                                                0.005 -312.41667 -0.22655 -309.66667 
                                                -0.51421 z"
                                                FillRule="NonZero" />
                                        </Path.Data>
    </Path>

Folder (Core):

     <Path Margin="0,-2,0,0" Stretch="Uniform" 
     StrokeThickness="1.33333337">
                                        <Path.Fill>
                                            <LinearGradientBrush EndPoint="0.5,1" 
                                                 StartPoint="0.5,0">
                                                <GradientStop Color="Black" 
                                                              Offset="1.5" />
                                                <GradientStop Color="#FF6E6E6E" 
                                                              Offset="0.029" />
                                                <GradientStop Color="#FF000102" />
                                            </LinearGradientBrush>
                                        </Path.Fill>
                                        <Path.Data>
                                            <PathGeometry
                                                Figures="m 35.46142 662.88211 
                                                c -5.645595 -6.04364 -6.953389 
                                                -12.93947 -7.582867 -39.98349 
                                                -0.31759 -13.64451 -1.179782 -51.50821 
                                                -1.915981 -84.14154 -1.96164 -86.95319
                                                -5.166926 -162.45026 -9.992734 -235.36814 
                                                -1.271839 -19.21746 -1.512834 -30.87022 
                                                -0.697253 -33.71399 2.083099 -7.26333 
                                                6.312426 -12.8508 12.339504 -16.302 l 
                                                5.732545 -3.28255 314.483266 -0.34436 
                                                c 218.55465 -0.2393 316.08585 0.0872 
                                                319.73701 1.07034 6.94134 1.8691 
                                                15.21772 10.23518 17.12215 17.30772 
                                                1.12319 4.17118 0.98421 13.3284 
                                                -0.57271 37.73698 -5.17222 81.08726 
                                                -8.05125 149.04533 -10.06429 237.56267 
                                                -2.32833 102.38191 -2.43281 105.12652 
                                                -4.2054 110.47999 -0.93725 2.83066 
                                                -3.32391 6.88066 -5.30367 9 l 
                                                -3.59956 3.85334 H 350.01131 
                                                39.081194 Z"
                                                FillRule="NonZero" />
                                        </Path.Data>
                                        <Path.Effect>
                                            <DropShadowEffect BlurRadius="5" 
                                                              Color="Black" />
                                        </Path.Effect>
    </Path>

Implementation

This file explorer has been duly created to integrate with the system fully. It enables you to explore the files that can be used by a data scientist. Not only that, you can generate nodes based on those files within.

ContentsBrowser

Performance Gauge

A performance gauge is a tool that I have made out of fun. However, adding it to the project has been proven useful. It helps me keep track of how many resources the visual scripting environment is using.

Style

      <Style x:Key="Gauge" TargetType="controls:PerformanceGauge">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate>
                    <Grid Width="270" Height="30">
                        <Path Style="{DynamicResource ToolTipHint}" 
                              HorizontalAlignment="Left" Name="Spy"
                              Stretch="Uniform" StrokeThickness="1.33333325">
                            <Path.ToolTip>
                                <Border Background="#424242" 
                                        BorderBrush="Gray" CornerRadius="3,3,3,3"
                                        BorderThickness="2">
                                    <StackPanel MaxWidth="300" Margin="3">
                                        <TextBlock Foreground="Gold" FontSize="14" 
                                                   Text="Performance gauge"
                                                   FontFamily="Segoe UI Semibold" />
                                        <TextBlock Foreground="AliceBlue"
                                                   Text="This gauge will help you 
                                                   maintain the performaance of your 
                                                   running project."
                                                   TextWrapping="Wrap" />
                                    </StackPanel>
                                </Border>
                            </Path.ToolTip>
                        </Path>
                        <Grid Height="30" Width="180">
                            <Path xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                                           Name="Gauge"
                                  StrokeThickness="3" StrokeLineJoin="Miter" 
                                  StrokeStartLineCap="Flat"
                                  StrokeEndLineCap="Flat" Stretch="Fill" 
                                  Fill="#FF353535">
                                <Path.Stroke>
                                    <LinearGradientBrush EndPoint="0.5,1" 
                                         StartPoint="0.5,0">
                                        <GradientStop Color="#FF202934" 
                                         Offset="0.623" />
                                        <GradientStop
                                            Color="{Binding RelativeSource=
                                            {RelativeSource TemplatedParent},
                                            Path=CoreColor}" />
                                    </LinearGradientBrush>
                                </Path.Stroke>
                                <Path.Data>
                                    <PathGeometry
                                        Figures="m 10 2.519685 h 240 v 30 h 40 v 
                                        -30 H 790 L 660 152.51969 H 290 v -30 h 
                                        -40 v 30 H 10 Z"
                                        FillRule="NonZero" />
                                </Path.Data>
                            </Path>
                            <TextBlock Text="{Binding RelativeSource=
                             {RelativeSource TemplatedParent},Path=Ram}"
                                       Foreground="WhiteSmoke" Margin="8" 
                                       FontFamily="Segoe WP Semibold" />
                            <StackPanel Orientation="Horizontal" 
                             RenderTransformOrigin="0.444,0.5" Height="30"
                                        VerticalAlignment="Top" 
                                        HorizontalAlignment="Left"
                                        Width="180" Margin="70,1,0,0">
                                <Path xmlns:x=
                                 "http://schemas.microsoft.com/winfx/2006/xaml" 
                                x:Name="Cold" Fill="Black"
                                      Height="15" Stretch="Fill" 
                                      Width="15" HorizontalAlignment="Left"
                                      Stroke="#FFBFBFBF">
                                    <Path.Data>
                                        <PathGeometry
                                            Figures="M 64.00 416.00l 96.00 
                                            96.00l 256.00-256.00L 160.00 0.00L 
                                            64.00 96.00l 160.00 160.00L 64.00 416.00z"
                                            FillRule="NonZero" />
                                    </Path.Data>
                                    <Path.ToolTip>
                                        <Border Background="#424242" 
                                        BorderBrush="Gray" 
                                        CornerRadius="3,3,3,3"
                                                BorderThickness="2">
                                            <StackPanel MaxWidth="300" 
                                            Margin="3">
                                                <TextBlock Foreground="#FF00A2FF" 
                                                FontSize="14" Text="Cold state"
                                                FontFamily="Segoe UI Semibold" />
                                                <TextBlock Foreground="AliceBlue"
                                                 Text="This level of performance 
                                                 indicates that your project 
                                                 is running in the best possible state."
                                                 TextWrapping="Wrap" />
                                                <TextBlock Foreground="Green"
                                                           Text="You don't need 
                                                           to take any actions." />

                                            </StackPanel>
                                        </Border>
                                    </Path.ToolTip>
                                </Path>
                                <Path xmlns:x=
                                 "http://schemas.microsoft.com/winfx/2006/xaml" 
                                x:Name="Cool"
                                      Fill="#FF000000" Height="15" 
                                      Stretch="Fill" Width="15" 
                                      Margin="-4,0,0,0"
                                      HorizontalAlignment="Left" 
                                      Stroke="#FFBFBFBF">
                                    <Path.Data>
                                        <PathGeometry
                                            Figures="M 64.00 416.00l 96.00 
                                            96.00l 256.00-256.00L 160.00 0.00L 
                                            64.00 96.00l 160.00 160.00L 64.00 416.00z"
                                            FillRule="NonZero" />
                                    </Path.Data>
                                    <Path.ToolTip>
                                        <Border Background="#424242" 
                                        BorderBrush="Gray" CornerRadius="3,3,3,3"
                                                BorderThickness="2">
                                            <StackPanel MaxWidth="300" 
                                            Margin="3">
                                                <TextBlock Foreground="#FF0074FF" 
                                                FontSize="14" Text="Cool state"
                                                FontFamily="Segoe UI Semibold" />
                                                <TextBlock Foreground="AliceBlue"
                                                 Text="This level of performance 
                                                 indicates that your project is running 
                                                 in the near-best possible state."
                                                 TextWrapping="Wrap" />
                                                <TextBlock Foreground="Green"
                                                           Text="You don't need to 
                                                           take any actions." />

                                            </StackPanel>
                                        </Border>
                                    </Path.ToolTip>
                                </Path>
                                <Path xmlns:x=
                                 "http://schemas.microsoft.com/winfx/2006/xaml" 
                                x:Name="Good"
                                      Fill="#FF000000" Height="15" 
                                      Stretch="Fill" Width="15" 
                                      Margin="-4,0,0,0"
                                      HorizontalAlignment="Left" 
                                      Stroke="#FFBFBFBF">
                                    <Path.Data>
                                        <PathGeometry
                                            Figures="M 64.00 416.00l 96.00 
                                            96.00l 256.00-256.00L 160.00 0.00L 
                                            64.00 96.00l 160.00 160.00L 64.00 416.00z"
                                            FillRule="NonZero" />
                                    </Path.Data>
                                    <Path.ToolTip>
                                        <Border Background="#424242" 
                                        BorderBrush="Gray" CornerRadius="3,3,3,3"
                                                BorderThickness="2">
                                            <StackPanel MaxWidth="300" 
                                            Margin="3">
                                                <TextBlock Foreground="#FF0074FF" 
                                                FontSize="14" Text="Good state"
                                                FontFamily="Segoe UI Semibold" />
                                                <TextBlock Foreground="AliceBlue"
                                                 Text="This level of performance 
                                                 indicates that your project is running 
                                                 in the near-best possible state."
                                                 TextWrapping="Wrap" />
                                                <TextBlock Foreground="Green"
                                                           Text="You don't need 
                                                           to take any actions." />

                                            </StackPanel>
                                        </Border>
                                    </Path.ToolTip>
                                </Path>
                                <Path xmlns:x=
                                 "http://schemas.microsoft.com/winfx/2006/xaml" 
                                x:Name="Normal"
                                      Fill="#FF000000" Height="15" 
                                      Stretch="Fill" Width="15" 
                                      Margin="-4,0,0,0"
                                      HorizontalAlignment="Left" 
                                      Stroke="#FFBFBFBF">
                                    <Path.Data>
                                        <PathGeometry
                                            Figures="M 64.00 416.00l 96.00 
                                            96.00l 256.00-256.00L 160.00 0.00L 
                                            64.00 96.00l 160.00 160.00L 64.00 416.00z"
                                            FillRule="NonZero" />
                                    </Path.Data>
                                    <Path.ToolTip>
                                        <Border Background="#424242" 
                                        BorderBrush="Gray" CornerRadius="3,3,3,3"
                                                BorderThickness="2">
                                            <StackPanel MaxWidth="300" 
                                            Margin="3">
                                                <TextBlock Foreground="WhiteSmoke" 
                                                FontSize="14" Text="Normal state"
                                                FontFamily="Segoe UI Semibold" />
                                                <TextBlock Foreground="AliceBlue"
                                                Text="This level of performance 
                                                indicates that your project is 
                                                running in the normal state."
                                                TextWrapping="Wrap" />
                                                <TextBlock Foreground="Green"
                                                           Text="You don't need 
                                                           to take any actions." />

                                            </StackPanel>
                                        </Border>
                                    </Path.ToolTip>
                                </Path>
                                <Path xmlns:x=
                                 "http://schemas.microsoft.com/winfx/2006/xaml" 
                                x:Name="Heating"
                                      Fill="#FF000000" Height="15" 
                                      Stretch="Fill" Width="15" 
                                      Margin="-4,0,0,0"
                                      HorizontalAlignment="Left" 
                                      Stroke="#FFBFBFBF">
                                    <Path.Data>
                                        <PathGeometry
                                            Figures="M 64.00 416.00l 96.00 
                                            96.00l 256.00-256.00L 160.00 0.00L 
                                            64.00 96.00l 160.00 160.00L 64.00 416.00z"
                                            FillRule="NonZero" />
                                    </Path.Data>
                                    <Path.ToolTip>
                                        <Border Background="#424242" 
                                        BorderBrush="Gray" CornerRadius="3,3,3,3"
                                                BorderThickness="2">
                                            <StackPanel MaxWidth="300" 
                                            Margin="3">
                                                <TextBlock Foreground="#FFDC3E36" 
                                                FontSize="14" Text="Heating state"
                                                FontFamily="Segoe UI Semibold" />
                                                <TextBlock Foreground="AliceBlue"
                                                Text="This level of performance 
                                                indicates that your project is consuming 
                                                too much RAM. You can take some actions
                                                to reduce the amount of memory 
                                                your project is using."
                                                TextWrapping="Wrap" />
                                                <TextBlock Foreground="Gray"
                                                Text="Reduce the number of nodes 
                                                you are using." />
                                                <TextBlock Foreground="Gray"
                                                 Text="Divide your set of nodes 
                                                 into multiple functions." />
                                                <TextBlock Foreground="Gray"
                                                 Text="Load instructions 
                                                 via the 'Load' node." />
                                                <TextBlock Foreground="Gray"
                                                           Text="Write native code 
                                                           instead on using nodes." />

                                            </StackPanel>
                                        </Border>
                                    </Path.ToolTip>
                                </Path>
                                <Path x:Name="Hot"
                                      Fill="#FF000000" Height="15" 
                                      Stretch="Fill" Width="15" Margin="-4,0,0,0"
                                      HorizontalAlignment="Left" Stroke="#FFBFBFBF">
                                    <Path.Data>
                                        <PathGeometry
                                            Figures="M 64.00 416.00l 96.00 
                                                     96.00l 256.00-256.00L 
                                            160.00 0.00L 64.00 96.00l 160.00 
                                            160.00L 64.00 416.00z"
                                            FillRule="NonZero" />
                                    </Path.Data>
                                    <Path.ToolTip>
                                        <Border Background="#424242" 
                                        BorderBrush="Gray" CornerRadius="3,3,3,3"
                                                BorderThickness="2">
                                            <StackPanel MaxWidth="300" Margin="3">
                                                <TextBlock Foreground="#FFDA0808" 
                                                FontSize="14" Text="Hot state"
                                                FontFamily="Segoe UI Semibold" />
                                                <TextBlock Foreground="AliceBlue"
                                                 Text="This level of performance 
                                                 indicates that your project is consuming 
                                                 too much RAM. You can take some actions
                                                 to reduce the amount of memory 
                                                 your project is using."
                                                 TextWrapping="Wrap" />
                                                <TextBlock Foreground="Gray"
                                                 Text="Reduce the number of nodes 
                                                 you are using." />
                                                <TextBlock Foreground="Gray"
                                                 Text="Divide your set of nodes 
                                                 into multiple functions." />
                                                <TextBlock Foreground="Gray"
                                                 Text="Load instructions 
                                                 via the 'Load' node." />
                                                <TextBlock Foreground="Gray"
                                                 Text="Write native code 
                                                 instead on using nodes." />
                                            </StackPanel>
                                        </Border>
                                    </Path.ToolTip>
                                </Path>
                                <Path xmlns:x=
                                 "http://schemas.microsoft.com/winfx/2006/xaml" 
                                x:Name="Risky"
                                      Fill="#FF000000" Height="15" 
                                      Stretch="Fill" Width="15" 
                                      Margin="-4,0,0,0"
                                      HorizontalAlignment="Left" Stroke="#FFBFBFBF">
                                    <Path.Data>
                                        <PathGeometry
                                            Figures="M 64.00 416.00l 96.00 
                                            96.00l 256.00-256.00L 160.00 0.00L 
                                            64.00 96.00l 160.00 160.00L 64.00 416.00z"
                                            FillRule="NonZero" />
                                    </Path.Data>
                                    <Path.ToolTip>
                                        <Border Background="#424242" 
                                        BorderBrush="Gray" CornerRadius="3,3,3,3"
                                                BorderThickness="2">
                                            <StackPanel MaxWidth="300" 
                                            Margin="3">
                                                <TextBlock Foreground="#FFF50909" 
                                                FontSize="14" Text="Risky state"
                                                FontFamily="Segoe UI Semibold" />
                                                <TextBlock Foreground="AliceBlue"
                                                 Text="This level of performance 
                                                 indicates that your project is consuming 
                                                 too much RAM. You can take some actions
                                                 to reduce the amount of memory 
                                                 your project is using."
                                                 TextWrapping="Wrap" />
                                                <TextBlock Foreground="Gray"
                                                 Text="Reduce the number of nodes 
                                                 you are using." />
                                                <TextBlock Foreground="Gray"
                                                 Text="Divide your set of nodes 
                                                 into multiple functions." />
                                                <TextBlock Foreground="Gray"
                                                 Text="Load instructions via the 
                                                 'Load' node." />
                                                <TextBlock Foreground="Gray"
                                                 Text="Write native code 
                                                 instead on using nodes." />
                                            </StackPanel>
                                        </Border>
                                    </Path.ToolTip>
                                </Path>
                            </StackPanel>
                        </Grid>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

The gauge has seven possible states when it comes to memory usage.

  • Cold

  • Cool

  • Good

  • Normal

  • Heating

  • Hot

  • Risky

Note that the suggestions that show up when you use more memory resources are not actually applicable.

Code-behind:

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;
using System.Windows.Threading;
using VisualSR.Properties;

namespace VisualSR.Controls
{
    public enum PerfomanceState
    {
        Cold,
        Cool,
        Good,
        Normal,
        Heating,
        Hot,
        Risky
    }

    public class PerformanceGauge : Control, INotifyPropertyChanged
    {
        private readonly DispatcherTimer _backgroundRamCollector = new DispatcherTimer();

        private readonly PerformanceCounter _pc = new PerformanceCounter
        {
            CategoryName = "Process",
            CounterName = "Working Set - Private"
        };

        private readonly Process _proc = Process.GetCurrentProcess();
        private Path _cold;
        private Path _cool;
        private Color _coreColor;
        private Path _good;
        private Brush _heatColor;
        private Path _heating;
        private Path _hot;
        private double _memsize;
        private Path _normal;

        private Path[] _paths;
        private string _ram = "...";
        private Path _risky;
        public PerfomanceState States = PerfomanceState.Cold;

        public PerformanceGauge()
        {
            Style = FindResource("Gauge") as Style;
            ApplyTemplate();
            Loaded += (s, e) =>
            {
                _cold = Template.FindName("Cold", this) as Path;
                _cool = Template.FindName("Cool", this) as Path;
                _good = Template.FindName("Good", this) as Path;
                _normal = Template.FindName("Normal", this) as Path;
                _heating = Template.FindName("Heating", this) as Path;
                _hot = Template.FindName("Hot", this) as Path;
                _risky = Template.FindName("Risky", this) as Path;
                _paths = new[] {_cold, _cool, _good, _normal, _heating, _hot, _risky};
            };
            _backgroundRamCollector.Interval = new TimeSpan(0, 0, 0, 3);
            _backgroundRamCollector.IsEnabled = true;
            _backgroundRamCollector.Start();
            _backgroundRamCollector.Tick += (ts, te) => CountRam();
        }

        public Color CoreColor
        {
            get { return _coreColor; }
            set
            {
                _coreColor = value;
                OnPropertyChanged();
            }
        }

        public Brush HeatColor
        {
            get { return _heatColor; }
            set
            {
                _heatColor = value;
                OnPropertyChanged();
            }
        }

        public string Ram
        {
            get { return _ram; }
            set
            {
                _ram = value;

                OnPropertyChanged();
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        private void ReBuild()
        {
            var x = _memsize;
            if (x <= 200)
            {
                if (!Equals(HeatColor, Brushes.Cyan))
                {
                    States = PerfomanceState.Cold;
                    HeatColor = Brushes.Cyan;
                    CoreColor = Colors.Cyan;
                    Highlighter(0, 1, HeatColor);
                    Highlighter(1, 7, Brushes.Black);
                }
            }
            else if (x <= 300 && x > 200)
            {
                if (!Equals(HeatColor, Brushes.DarkCyan))
                {
                    States = PerfomanceState.Cool;
                    HeatColor = Brushes.DarkCyan;
                    CoreColor = Colors.DarkCyan;
                    Highlighter(0, 2, HeatColor);
                    Highlighter(3, 7, Brushes.Black);
                }
            }
            else if (x <= 600 && x > 300)
            {
                if (!Equals(HeatColor, Brushes.CadetBlue))
                {
                    States = PerfomanceState.Good;
                    HeatColor = Brushes.CadetBlue;
                    CoreColor = Colors.CadetBlue;
                    Highlighter(0, 3, HeatColor);
                    Highlighter(4, 7, Brushes.Black);
                }
            }
            else if (x <= 900 && x > 600)
            {
                if (!Equals(HeatColor, Brushes.ForestGreen))
                {
                    States = PerfomanceState.Normal;
                    HeatColor = Brushes.ForestGreen;
                    CoreColor = Colors.ForestGreen;
                    Highlighter(0, 4, HeatColor);
                    Highlighter(5, 7, Brushes.Black);
                }
            }
            else if (x <= 1500 && x > 900)
            {
                if (!Equals(HeatColor, Brushes.HotPink))
                {
                    States = PerfomanceState.Heating;
                    HeatColor = Brushes.HotPink;
                    CoreColor = Colors.HotPink;
                    Highlighter(0, 5, HeatColor);
                    Highlighter(6, 7, Brushes.Black);
                }
            }
            else if (x <= 2000 && x > 1500)
            {
                if (!Equals(HeatColor, Brushes.Firebrick))
                {
                    States = PerfomanceState.Hot;
                    HeatColor = Brushes.Firebrick;
                    CoreColor = Colors.Firebrick;
                    Highlighter(0, 6, HeatColor);
                    Highlighter(7, 7, Brushes.Black);
                }
            }
            else if (x > 3000)
            {
                if (!Equals(HeatColor, Brushes.Firebrick))
                {
                    States = PerfomanceState.Risky;
                    HeatColor = Brushes.Red;
                    CoreColor = Colors.Red;
                    Highlighter(0, 7, HeatColor);
                }
            }
            OnPropertyChanged();
        }

        private void CountRam()
        {
            Task.Factory.StartNew(() =>
            {
                Application.Current.Dispatcher.BeginInvoke
                            (DispatcherPriority.Background, new Action(() =>
                {
                    //This code can be used to mimick a real-life experience
                    //memsize += 1;
                    //Ram = memsize + " MB";
                    //ReBuild();
                    _pc.InstanceName = _proc.ProcessName;
                    _memsize = Convert.ToDouble(_pc.NextValue() / 1048576);
                    Ram = _memsize.ToString("#.0") + " MB";
                    ReBuild();
                }));
            });
        }

        private void Highlighter(int begin, int end, Brush brush)
        {
            for (var i = begin; i < end; i++)
                if (_paths[i] != null)
                    _paths[i].Fill = brush;
        }

        [NotifyPropertyChangedInvocator]
        protected virtual void OnPropertyChanged(string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Note

The gauge is simply made for fun-related and light reasons.

PerformanceGauge

Tools

Tools, some may call them utilities, are used to help the developer perform multiple tasks that are not actually intended to handle a specific task within the product- they are, in fact, used to handle multiple tasks.

Magic Laboratory

Nothing magical in here, it is just math. The lab of magic is a class that contains a lot of functions that result in animations, 2D transformations, 2D effects, etc.

The presence of the magic lab can be felt easily whenever you perform any operation. It supports mouse effects when you click on the virtual control, blurring the wires when you are trying to create another one, etc.

MagicLab

Cipher (Serialization) | Save/Load Operations

Working on a graph, then being unable to save it to work on it, later on, won't be a cool event. Thus, having the ability to store all your graphs, nodes and metadata in a way that would allow you to continue on them later is needed.

I have already written an ARTICLE about serialization and deserialization. It contains the ins-outs of the process, and it ends up with a valid de/serialization algorithm.

The approach of saving/loading I ended up with is based on saving the metadata that is related to every UIElement that is being rendered- they type, title, contents (data, inputs, connections, etc.).

i.e., the following node:

will have its metadata converted into JSON:

{
   "ExecutionConnectors":[
   ],
   "Nodes":[
      {
         "InnerData":[
         ],
         "InputData":[
            "A lonely node."
         ],
         "OutputData":[
         ],
         "Name":"Nodes.Nodes.R.Basics.Print",
         "Id":"a9c51559-2094-45be-8417-08873cec72a8",
         "NodeType":"Method",
         "X":154.00001899999998,
         "Y":113
      }
   ],
   "ObjectConnectors":[
   ],
   "SpaghettiDividers":[
   ]
}

Code-behind:

        public static string SerializeToString<T>(T toSerialize)
        {
            return new JavaScriptSerializer().Serialize(toSerialize);
        }

The function takes the object, which is the VirtualControl, with all its contents, as a parameter in our case, and returns a JSON string that can be eventually stored in a file.

The object that will be passed is a class that will represent all the contents of the VirtualControl.

     public class VirtualControlData
    {
        private readonly List<VolatileConnector> VolatileConnectors = 
                                                 new List<VolatileConnector>();
        public List<ExecutionConnectorProperties> ExecutionConnectors = 
                                      new List<ExecutionConnectorProperties>();
        public List<NodeProperties> Nodes = new List<NodeProperties>();
        public List<ObjectConnectorProperties> ObjectConnectors = 
                                   new List<ObjectConnectorProperties>();
        public List<SpaghDividerProperties> SpaghettiDividers = 
                                new List<SpaghDividerProperties>();

        public VirtualControlData()
        {
        }

        public VirtualControlData(VirtualControl vc)
        {
            for (var index = vc.Nodes.Count - 1; index >= 0; index--)
            {
                var node = vc.Nodes[index];
                if (node.Types != NodeTypes.Root && 
                    node.Types != NodeTypes.SpaghettiDivider)
                {
                    Nodes.Add(new NodeProperties(node));
                }
                else if (node.Types == NodeTypes.SpaghettiDivider)
                {
                    SpaghettiDividers.Add(new SpaghDividerProperties(node));
                    VolatileConnectors.Add(new VolatileConnector
                    {
                        X = node.X,
                        Y = node.Y,
                        Connector = ((SpaghettiDivider) node).ObjectConnector
                    });
                    (node as SpaghettiDivider).Delete();
                }
            }
            foreach (var conn in vc.ObjectConnectors)
                ObjectConnectors.Add(new ObjectConnectorProperties(conn));
            foreach (var conn in vc.ExecutionConnectors)
                ExecutionConnectors.Add(new ExecutionConnectorProperties(conn));

            foreach (var conn in VolatileConnectors)
            {
                var divider = new SpaghettiDivider(vc, conn.Connector, false);
                for (var index = 0; index < vc.ObjectConnectors.Count; index++)
                {
                    var c = vc.ObjectConnectors[index];
                    if (c == conn.Connector)
                        c.Delete();
                }
                conn.Connector.EndPort.ConnectedConnectors.ClearConnectors();
                vc.AddNode(divider, conn.X, conn.Y);
            }
        }

        private struct VolatileConnector
        {
            public double X;
            public double Y;
            public ObjectsConnector Connector;
        }
    }

The deserialization process does the exact opposite, it takes a string as a parameter and returns an object based on the passed serialized data.

         public static T DeSerializeFromString<T>(string data) where T : new()
        {
            return new JavaScriptSerializer().Deserialize<T>(data);
        }

The serialization algorithm also takes care of other aspects, such as connectors, spaghetti dividers and comments.

For example, take this simple graph:

Here, we can see that there are more present elements compared to the other case. In here, we can observe the presence of two connectors, flowing data, and executable nodes.

{
   "ExecutionConnectors":[
      {
         "StartNode_ID":"0",
         "EndNode_ID":"a9c51559-2094-45be-8417-08873cec72a8",
         "StartPort_Index":0,
         "EndPort_Index":0
      }
   ],
   "Nodes":[
      {
         "InnerData":[

         ],
         "InputData":[

         ],
         "OutputData":[
            "Not lonely anymore"
         ],
         "Name":"Nodes.Nodes.Math.Vector1D",
         "Id":"cb7e5497-2886-4a58-adab-487932c99464",
         "NodeType":"Basic",
         "X":414.0000399999999,
         "Y":439
      },
      {
         "InnerData":[

         ],
         "InputData":[
            "Not lonely anymore"
         ],
         "OutputData":[

         ],
         "Name":"Nodes.Nodes.R.Basics.Print",
         "Id":"a9c51559-2094-45be-8417-08873cec72a8",
         "NodeType":"Method",
         "X":675.00006299999984,
         "Y":362
      }
   ],
   "ObjectConnectors":[
      {
         "StartNode_ID":"cb7e5497-2886-4a58-adab-487932c99464",
         "EndNode_ID":"a9c51559-2094-45be-8417-08873cec72a8",
         "StartPort_Index":0,
         "EndPort_Index":0
      }
   ],
   "SpaghettiDividers":[

   ]
}

Important Note

We can easily notice that the connectors somehow link to a node that has the ID 0. The weird part is that we have no data about any node that has the ID 0 in the array nodes in the JSON file.

Actually, that's because the node that we are referring to in this case is the start node. It is always there, it never vanishes and it gets created once we launch the visual scripting environment.

You cannot delete the Start node.

public override void Delete()
        {
            //Not a chance :-)
        }

Other Tools

Other tools, such as the tools that are used to calculate units, measure memory usage, perform operations on texts and so on and so forth, do exist. However, we are not going to dig deep into them for them being generic ones.

Cipher

Plugins

Github reference: https://github.com/alaabenfatma/VisualSR/tree/master/Nodes

Plugins, true to their nature, are made to extend the capacities of the visual scripting environment by containing more tools and stuff which would give the user more control over the application.

The plugins do contain nodes, and nodes serve as tools.

To work with plugins in a relevant way, I used MEF, which is a framework that helps developers create their own extensions, plugins and add-ons.

For example, I made an extension that contains a node that can perform some OCR (Optical character recognition) operations.

To make such a thing, you first need to add the library as a referenced assembly and then set up the core of the node and build it up.

using System.ComponentModel.Composition;
using VisualSR.Controls;
using VisualSR.Core;

namespace Tesseract
{
    [Export(typeof(Node))]
    public class OCR : Node
    {
        private readonly UnrealControlsCollection.TextBox _tb = 
                                       new UnrealControlsCollection.TextBox();
        private readonly VirtualControl Host;
        private readonly UnrealControlsCollection.TextBox lang = 
                                       new UnrealControlsCollection.TextBox();

        [ImportingConstructor]
        public OCR([Import("host")] VirtualControl host, 
               [Import("bool")] bool spontaneousAddition = false) : base(
            host, NodeTypes.Basic,
            spontaneousAddition)
        {
            Title = "Optical character recognition - OCR";
            Description = "Allows to convert scanned images, 
                           faxes, screenshots, PDF documents and ebooks to text";
            Host = host;
            Width += 50;
            Category = "Tesseract";
            AddObjectPort(this, "File Path", PortTypes.Input, 
                          RTypes.Character, false, _tb);
            AddObjectPort(this, "Engine", PortTypes.Input, 
                          RTypes.Character, false, lang);
            AddObjectPort(this, "       return", PortTypes.Output, 
                                        RTypes.Character, false);

            _tb.TextChanged += (s, e) => { InputPorts[0].Data.Value = _tb.Text; };
            InputPorts[0].DataChanged += (s, e) =>
            {
                if (_tb.Text != InputPorts[0].Data.Value)
                    _tb.Text = InputPorts[0].Data.Value;
                GenerateCode();
            };
            InputPorts[1].DataChanged += (s, e) =>
            {
                if (lang.Text != InputPorts[2].Data.Value)
                    lang.Text = InputPorts[2].Data.Value;
                GenerateCode();
            };

            Width = ActualWidth + 90;
        }

        public override string GenerateCode()
        {
            var value = InputPorts?[0].Data.Value;
            OutputPorts[0].Data.Value = $"tesseract::ocr('{value}', 
                           engine = tesseract('{InputPorts?[1].Data.Value}')";
            return "# OCR Generation process, TARGET :" + value;
        }

        public override Node Clone()
        {
            var node = new OCR(Host);
            return node;
        }
    }
}

The node actually generates an R code. The R code eventually makes use of the Tesseract Library of Google.

Adding a new plugins does not require the reboot of the visual scripting environment, the tree of nodes will check the presence and the absence of plugins each time it launches.

That said, if we delete the Tesseract plugins, and try to add it again, we won't be able to do so for it going to be unfound.

PluginsManager

Code Generation

Do you remember the fresh year computer science courses, where they spam your timetable with LinkedList related classes? I bet you do. Well, here they do come in handy.

When we try to generate the whole code that will be eventually executed, we will go through every method node and extract its code.

For example, say we have this simple graph that is made of a combination of digits, and a plotting node:

Once we generate the code and compile this graph, we will end up with this generated code in R:

#Artificial code. 
setwd("C:\\Users\\ABF\\Desktop\\demo\\")
#Generated a vector of characters : c(55,99,22,42)
plot(c(55,99,22,42))

NOTE: I chose the path. The setwd works as a directory path setter.

Eventually, the generated code will be sent to the R compiler that installed on your computer and will be compiled and consecutively executed.

Every node has a GenerateCode() function that returns a string that will be eventually appended to the main code that starts from the Start node.

        public static string Code(Node root)
        {
            var codeBuilder = new StringBuilder();
            codeBuilder.Append(root.GenerateCode());
            codeBuilder.AppendLine();
            if (root.OutExecPorts.Count <= 0) return codeBuilder.ToString();
            if (root.OutExecPorts[0].ConnectedConnectors.Count <= 0) 
                return codeBuilder.ToString();
            var nextNode = 
                root.OutExecPorts[0].ConnectedConnectors[0].EndPort.ParentNode;
            var stillHasMoreNodes = true;
            while (stillHasMoreNodes)
            {
                codeBuilder.Append(nextNode.GenerateCode());
                codeBuilder.AppendLine();
                if (nextNode.OutExecPorts[0].ConnectedConnectors.Count > 0)
                    nextNode = 
                    nextNode.OutExecPorts[0].ConnectedConnectors[0].EndPort.ParentNode;
                else
                    stillHasMoreNodes = false;
            }
            return codeBuilder.ToString();
        }

There are other few cases where the nodes are more complicated for them having multiple out-Execution ports. As an example, we can take the Loop node.

Looking at this graph, we used a variable named i, and we used it as a counter. Eventually, we did set up the range from 0 to 10 and did set up the instructions will be executed during the process of iteration from 0 to 10. The IDLE state refers to the instruction that will be executed once the loop is finished.

Final code:

#Artificial code. 
setwd("C:\\Users\\ABF\\Desktop\\demo\\")
for(i in 0:10){
print(i)
}
print('It is over') 

The magic behind the loop nodes is that they make use of the function that generates code out of a sequence of nodes by setting the first node that links to the instructions port a root.

        public override string GenerateCode()
        {
            var sb = new StringBuilder();
            sb.Append($"for({InputPorts?[0].Data.Value} in 
                      {InputPorts?[1].Data.Value})"+"{");
            sb.AppendLine();
            if (OutExecPorts[1].ConnectedConnectors.Count > 0)
                sb.AppendLine(CodeMiner.Code(OutExecPorts[1].
                              ConnectedConnectors[0].EndPort.ParentNode));
            sb.AppendLine();
            sb.Append("}");
            return sb.ToString();
        }

Future

The visual scripting environment has been designed to help data scientists work on their project in a lighter environment, especially if they are not a big fan of coding.

The project will be distributed under the MIT license, and, hopefully, there will be a lot of contributions and updates to make it more stable and professional.

Important Notes

This project is not:

  • commercial

    The project is nothing but an experiment I started working on in high school.

  • a company

    It is a personal project, made it only for educational purposes.

  • business-oriented

    It is only for studies related purposes.

  • profitable

    The project is only for learning, it is a non-profit project. And it will never be profitable.

The author is not:

  • responsible for any misuse of the project
  • responsible for any damage caused by the project

History

  • 14th August, 2018: Initial version