TreeGrapher + TreeNode | Create skill/progression trees, and any tree graph with ease! (Tree Data Structure included)

image image

Create skill trees, progression trees, and any tree graph with ease!


:circus_tent:Showcase/Examples::circus_tent:

🔥Skill Unlocking Gui🍃

🌲Tree Graph Visualizer🌲

🛠️Crafting Progression Tree🛠️

(This is not a part of the Playground.)


🍀Features:🍀

TreeGrapher allows you to graph Tree structures made with the TreeNode class.
It has multiple customizable settings, such as:

  1. Vertical Margin - Allows you to determine the vertical spacing between a node and its parent node UIs.
  2. Alignment - Allows you to change the alignment of the tree GUI (Left, Center, Right). Center is the default alignment.
  3. Edges Settings (settings for the lines connecting the graph widgets): Thickness, Transparency, Color, and ParallelToAxes.
    ParallelToAxes is the setting that allows you to decide whether you want diagonal edges or edges parallel to the X and Y axes.
  4. TreeNodeClass is a data structure that allows you to create Tree structures. It is generic, meaning it’s not limited to a single type and adapts to any data you provide as a parameter.
  5. Both classes should also work with the IntelliSense for most types (works with Roblox’s type system and autocomplete for you)
  6. You design the tree widget/node, and the tree is generated for you. You have direct access to each node’s GUI widget, which means you can also detect input, tween it, and basically do everything that you would do if you were scripting your own GUI.
  7. Plenty of “quality of life” - methods to save you time from having to add on your own. Such as Grapher:Redraw(…), Grapher:MountAndDraw(…), TreeNode:Traverse(…), TreeNode:IsLeaf(), TreeNode:IsRoot(), TreeNode:GetChildrenCount(), and plenty of others.
  8. TreeGrapher works using the scale sizing. This means it will attempt to fit the entire size of the canvas you provide. Works with ScrollingFrames too, but not with AutomaticSizing.

📋API Documentation📋

There is documentation for each method within the Modules as well. I’ve written it down here too:

TreeNodeClass

--[[

Create a new tree node with the given data. Data can be anything

]]

local rootData : any = ...

local root = TreeNode.new(rootData) -- There is a second, optional parameter for a parent if this is not the root node.

--[[

Returns the data stored in this node.

]]

root:GetData()

--[[

Adds the node as a child to the parent if the parent is not nil. If nil, it makes the node a root (no parent node).

]]

local parent = ...

root:SetParent(parent)

--[[

Returns the parent node of this node.

]]

local nodeParent = root:GetParent()

--[[

Returns true if this node is a root (no parent), false otherwise.

]]

local isRoot = root:IsRoot()

--[[

Adds a node as a child to this node.

]]

local newChildNode = ...

root:AddChild(newChildNode)

--[[

Adds all the nodes in the array to the children of this node.

]]

local arrayOfChildren = {...}

root:AddChildren(newChildNode)

--[[

Removes a child node from the children of this node.

childNode can either be the node itself or the index of the child.

newChildNode will become a root in the process.

]]

local childNode = ... -- child node or child index.

root:RemoveChild(newChildNode)

--[[

Returns a shallow copy of the array of child nodes of this node.

]]

local children = root:GetChildren()

--[[

Gets the number of children that this node has.

Use this instead of #node:GetChildren(), as this is more efficient.

]]

local childrenCount = root:GetChildrenCount()

--[[

Returns true if someNode is a child of thisNode, false otherwise.

"child" means a direct child of this node, and not a descendant.

]]

local someNode = ...

root:HasChild(someNode)

--[[

Returns true if this node is a leaf (has no children), false otherwise

]]

root:IsLeaf()

--[[

Calls doSomething on all the nodes in a pre-order (DFS) manner.

Passes the currentNode to the function.

]]

local doSomething = function(currentNode)

--Do something with the currentNode

...

end

root:PreOrderTraverse(doSomething)

--[[

Calls doSomething on all the nodes in a post-order (DFS) manner.

Passes the currentNode to the function.

]]

local doSomething = function(currentNode)

--Do something with the currentNode

...

end

root:PostOrderTraverse(doSomething)

--[[

Calls doSomething on all the nodes in level order (BFS) manner.

Passes the currentNode, and the node's level to the function.

]]

local doSomething = function(currentNode, level)

--Do something with the currentNode and level

...

end

root:LevelOrderTraverse(doSomething)

--[[

Calls doSomething on all nodes in the given order (PreOrder, PostOrder, or LevelOrder).

Passes the currentNode to the function.

]]

local traversalType = "PreOrder"

local doSomething = function(currentNode)

--Do something with the currentNode

...

end

root:Traverse(traversalType, doSomething)

TreeGrapher

local TreeGrapher = require(...)

local canvas = ... -- Path to the GuiObject you want to be the parent of the tree graph.

local tree = ... -- Reference to your TreeNode root.

local VERTICAL_MARGIN = 0.4 --This is the default vertical margin.

local ALIGNMENT = "Center" -- This is the default alignment type.

local EDGES_SETTINGS = {

    Thickness = 4; -- Thickness of the edges (in pixels).

    Transparency = 0; -- Transparency of the edges.

    Color = Color3.fromRGB(0,0,0) -- Color of the edges.

    ParallelToAxes = false; -- Whether the edges connecting the nodes will be parallel to X, Y axes or diagonal.

}

local grapher = TreeGrapher.new(tree, createWidget, EDGES_SETTINGS) -- Create a grapher object.

--[[

Prepares the TreeGrapher to display the tree on the parent GUI Object.

]]

grapher:Mount(canvas)

--[[

Draws this TreeGrapher's contained tree, given a verticalMargin. <br>

Throws an error and clears the canvas if it fails.

]]

grapher:Draw(VERTICAL_MARGIN, ALIGNMENT)

--[[

Prepares the TreeGrapher to display the tree on the parent GUI Object.

Draws this TreeGrapher's contained tree, given a verticalMargin. <br>

Throws an error and clears the canvas if it fails.
If n is the number of nodes in the tree, then Draw(...) runs in O(n) linear time.
]]

grapher:MountAndDraw(canvas, VERTICAL_MARGIN, ALIGNMENT)

--[[

Clears the graph and lets go of the resources used in creating it.

]]

grapher:Clear()

--[[

Redraws the tree contained in the grapher. This is useful if you have a dynamically changing tree.

This method draws the changed tree on the same canvas with the same settings.

]]

grapher:Redraw()

--[[

Makes the graph invisible, without clearing it.

]]

grapher:HideGraph()

--[[

Makes the graph visible if it was hidden.

]]

grapher:ShowGraph()

--[[

Makes the graph visible or invisible, depending on the parameter (boolean)

]]

local visible : boolean = true

grapher:SetVisiblity(visible)

--[[

Replaces the current edge settings with the new edge settings and redraws the graph.

]]

local NEW_EDGE_SETTINGS = {

    Thickness = 2;

    Transparency = 0.2;

    Color = Color3.fromRGB(255,0,0);

    ParallelToAxes = true;

}

grapher:SetEdgeSettings(NEW_EDGE_SETTINGS)

--[[

Changes the alignment type of the graph and redraws it.

]]

grapher:SetAlignment("Right")

--[[

Changes the vertical margin of the graph to the new vertical margin given and redraws it.

]]

grapher:SetVerticalMargin(0.8)

--[[

Clears the current graph and draws a new graph with the new tree given.

]]

local newTree = ... -- Some other tree.

grapher:ReplaceTree(newTree)


🏁Get Started:🏁

  1. Firstly, get the source from Roblox or GitHub.
    I recommend placing it in a folder under ReplicatedStorage, so that it is accessible both from the server and the client.
  2. Require both TreeNode and TreeGrapher from a local script, create a tree, and render it.
    Here’s a sample script that does exactly that. Feel free to mess around with the settings and see the result:
local TreeNodeClass = require(...) -- Path to the TreeNode Module
local TreeGrapher = require(...) -- Path to the TreeGrapher Module
-- Create a tree. I choose my tree data to contain a string, which is the node name.
local treeRoot : TreeNodeClass.TreeNode<string> = TreeNodeClass.new("Root")

local childA: TreeNodeClass.TreeNode<string> = TreeNodeClass.new("ChildA", treeRoot )
local childA1: TreeNodeClass.TreeNode<string> = TreeNodeClass.new("ChildA1", childA)
local childA2: TreeNodeClass.TreeNode<string> = TreeNodeClass.new("ChildA2", childA)

local childB: TreeNodeClass.TreeNode<string> = TreeNodeClass.new("ChildB", treeRoot )
local childB1: TreeNodeClass.TreeNode<string> = TreeNodeClass.new("ChildB1", childB)

--Set up the Grapher:
local VERTICAL_MARGIN = 0.4 -- This is the default margin. Feel free to do trial and error and see what works best for you.
local ALIGNMENT_TYPE = "Center"
-- Try these settings, adjust, and see what works for you:
local EDGES_SETTINGS : TreeGrapher.EdgeSettings = {
    Thickness = 4;
    Transparency = 0;
    Color = Color3.fromRGB(255, 255, 0);
    ParallelToAxes = false;
}
local canvas = script.Parent -- Where you want the tree GUI to be parented to.
--[[

This is the function that will create the node widgets. 
You are given the data of the node, and also the node object itself 
(in case you need to access the parent/children of the node)
]]
local function createNodeButton(nodeData, node)
	--Create the widget
	local btn = Instance.new("TextButton")
	btn.Name = nodeData -- In this case, nodeData is the name we've given the node.
	btn.Text = nodeData
	btn.TextScaled = true
	btn.Size = UDim2.fromScale(1, 1)

	--Add any input events to it:
	btn.Activated:Connect(function()
		print(nodeData .. " was clicked on!")
	end)
	--Design it the way you like it:
	local uiCorner = Instance.new("UICorner")
	uiCorner.CornerRadius = UDim.new(0.5, 0)
	uiCorner.Parent = btn

	local aspectRatio = Instance.new("UIAspectRatioConstraint")
	aspectRatio.AspectRatio = 1
	aspectRatio.Parent = btn
	return btn
end
-- Create a grapher, mount and draw the tree:
local grapher : TreeGrapher.TreeGrapher<string> = TreeGrapher.new(treeRoot, createNodeButton, EDGES_SETTINGS)
grapher:MountAndDraw(canvas, VERTICAL_MARGIN, ALIGNMENT_TYPE)

You should get a result similar to this:


Tutorials:

There aren't any video tutorials just yet, however, I am working on making some as soon as possible!

Lastly

  • This Module is free for anyone to use, please do not sell this version (the open-sourced one). Any forks/modifications, however, are no problem.
  • You are allowed to modify/fork it. However, you must attach the original MIT license (from GitHub) to your fork. Feel free to post them under this thread.
  • This Module may be updated in the future, the current version is 1.0.
  • I would love to see what you make with it! Feel free to post it in this thread.
  • If you have any feedback/suggestions/ideas, please let me know! I’ll gladly consider them.
  • If you have any questions, feel free to ask and I will reply when I see them :slight_smile:

Thank you for reading, and I hope you’ve found this useful!
@LightningLion58

16 Likes

one simple word to describe what I thought the moment I saw this post : “yoink”

I’ve been much too lazy to make my own proper tree graph, so this is great

2 Likes