├── .gitignore ├── .luacheckrc ├── CHANGELOG.md ├── Graphoon.lua ├── Graphoon ├── Edge.lua ├── Graph.lua ├── Node.lua └── init.lua ├── LICENSE.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Lua sources 2 | luac.out 3 | 4 | # luarocks build files 5 | *.src.rock 6 | *.zip 7 | *.tar.gz 8 | 9 | # Object files 10 | *.o 11 | *.os 12 | *.ko 13 | *.obj 14 | *.elf 15 | 16 | # Precompiled Headers 17 | *.gch 18 | *.pch 19 | 20 | # Libraries 21 | *.lib 22 | *.a 23 | *.la 24 | *.lo 25 | *.def 26 | *.exp 27 | 28 | # Shared objects (inc. Windows DLLs) 29 | *.dll 30 | *.so 31 | *.so.* 32 | *.dylib 33 | 34 | # Executables 35 | *.exe 36 | *.out 37 | *.app 38 | *.i*86 39 | *.x86_64 40 | *.hex 41 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | files['.luacheckrc'].global = false 2 | std = 'max+busted' 3 | 4 | globals = { 5 | 'love', 6 | 'getVersion', 7 | 'getTitle' 8 | } 9 | 10 | exclude_files = { 11 | './lua_install/*' 12 | } 13 | 14 | ignore = { 15 | '/self' 16 | } 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Version 1.0.1 ( 2016-01-12 ) 2 | - Fix [#1](https://github.com/rm-code/Graphoon/issues/1) - Adjusted force calculation and hopefully made it more stable 3 | 4 | ## Version 1.0.0 ( 2016-01-12 ) 5 | - Initial Release 6 | -------------------------------------------------------------------------------- /Graphoon.lua: -------------------------------------------------------------------------------- 1 | return require( (...) .. '.init' ); 2 | -------------------------------------------------------------------------------- /Graphoon/Edge.lua: -------------------------------------------------------------------------------- 1 | local current = (...):gsub('%.[^%.]+$', ''); 2 | 3 | local Edge = {}; 4 | 5 | function Edge.new( id, origin, target ) 6 | local self = {}; 7 | 8 | self.id = id; 9 | self.origin = origin; 10 | self.target = target; 11 | 12 | return self; 13 | end 14 | 15 | return Edge; 16 | -------------------------------------------------------------------------------- /Graphoon/Graph.lua: -------------------------------------------------------------------------------- 1 | local current = (...):gsub('%.[^%.]+$', ''); 2 | 3 | -- ------------------------------------------------ 4 | -- Required Modules 5 | -- ------------------------------------------------ 6 | 7 | local Node = require(current .. '.Node'); 8 | local Edge = require(current .. '.Edge'); 9 | 10 | -- ------------------------------------------------ 11 | -- Module 12 | -- ------------------------------------------------ 13 | 14 | local Graph = {}; 15 | 16 | function Graph.new() 17 | local self = {}; 18 | 19 | local nodes = {}; -- Contains all nodes in the graph. 20 | local edges = {}; -- Contains all edges in the graph. 21 | local edgeIDs = 0; -- Used to create a unique ID for new edges. 22 | 23 | local minX, maxX, minY, maxY; -- The boundaries of the graph. 24 | 25 | -- ------------------------------------------------ 26 | -- Local Functions 27 | -- ------------------------------------------------ 28 | 29 | --- 30 | -- (Re-)Sets the graph's boundaries to nil. 31 | -- 32 | local function resetBoundaries() 33 | minX, maxX, minY, maxY = nil, nil, nil, nil; 34 | end 35 | 36 | --- 37 | -- Updates the boundaries of the graph. 38 | -- This represents the rectangular area in which all nodes are contained. 39 | -- @param nx - The new x position to check. 40 | -- @param ny - The new y position to check. 41 | -- 42 | local function updateBoundaries( nx, ny ) 43 | return math.min( minX or nx, nx ), math.max( maxX or nx, nx ), math.min( minY or ny, ny ), math.max( maxY or ny, ny ); 44 | end 45 | 46 | --- 47 | -- Adds a new edge between two nodes. 48 | -- @param origin - The node from which the edge originates. 49 | -- @param target - The node to which the edge is pointing to. 50 | -- 51 | local function addEdge( origin, target ) 52 | for _, edge in pairs( edges ) do 53 | if edge.origin == origin and edge.target == target then 54 | error "Trying to connect nodes which are already connected."; 55 | end 56 | end 57 | 58 | assert( origin ~= target, "Tried to connect a node with itself." ); 59 | edges[edgeIDs] = Edge.new( edgeIDs, origin, target ); 60 | edgeIDs = edgeIDs + 1; 61 | end 62 | 63 | -- ------------------------------------------------ 64 | -- Public Functions 65 | -- ------------------------------------------------ 66 | 67 | --- 68 | -- Adds a node to the graph. 69 | -- @param id - The ID will be used to reference the Node inside of the graph. 70 | -- @param x - The x coordinate the Node should be spawned at (optional). 71 | -- @param y - The y coordinate the Node should be spawned at (optional). 72 | -- @param anchor - Wether the node should be locked in place or not (optional). 73 | -- @param ... - Additional parameters (useful when a custom Node class is used). 74 | -- 75 | function self:addNode( id, name, x, y, anchor, ... ) 76 | assert( not nodes[id], "Node IDs must be unique." ); 77 | nodes[id] = Node.new( id, name, x, y, anchor, ... ); 78 | return nodes[id]; 79 | end 80 | 81 | --- 82 | -- Removes a node from the graph. 83 | -- This will also remove all edges pointing to, or originating from this 84 | -- node. 85 | -- @param node - The node to remove from the graph. 86 | -- 87 | function self:removeNode( node ) 88 | nodes[node:getID()] = nil; 89 | 90 | self:removeEdges( node ); 91 | end 92 | 93 | --- 94 | -- Creates a graph from a .tgf formatted file. 95 | -- @param path (string) The path to the .tgf file to load. 96 | -- @param x (number) The x coordinate the nodes should be spawned at (optional). 97 | -- @param y (number) The y coordinate the nodes should be spawned at (optional). 98 | -- 99 | function self:loadTGF( path, x, y ) 100 | local dx = x or 0; 101 | local dy = y or 0; 102 | 103 | local n = {}; 104 | local e = {}; 105 | 106 | local target = n; 107 | for line in io.lines( path ) do 108 | -- '#' marks the definitions for edges in the .tgf file. 109 | if line == '#' then 110 | target = e; 111 | else 112 | target[#target + 1] = line; 113 | end 114 | end 115 | 116 | for _, line in ipairs( n ) do 117 | local tmp = {} 118 | for part in line:gmatch( '[^%s]+' ) do 119 | tmp[#tmp + 1] = part; 120 | end 121 | -- Add a slight random variation to the spawn coordinates to kick start 122 | -- the physics simulation. 123 | local rx = love.math.random( 2, 5 ); 124 | local ry = love.math.random( 2, 5 ); 125 | self:addNode( tmp[1], tmp[2], dx + rx, dy + ry, tmp[1] == '1' ); 126 | end 127 | 128 | for _, line in ipairs( e ) do 129 | local tmp = {} 130 | for part in line:gmatch( '[^%s]+' ) do 131 | tmp[#tmp + 1] = part; 132 | end 133 | self:connectIDs( tmp[1], tmp[2] ); 134 | end 135 | end 136 | 137 | --- 138 | -- Adds a new edge between two nodes. 139 | -- @param origin - The node from which the edge originates. 140 | -- @param target - The node to which the edge is pointing to. 141 | -- 142 | function self:connectNodes( origin, target ) 143 | addEdge( origin, target ); 144 | end 145 | 146 | --- 147 | -- Adds a new edge between two nodes referenced by their IDs. 148 | -- @param origin - The node id from which the edge originates. 149 | -- @param target - The node id to which the edge is pointing to. 150 | -- 151 | function self:connectIDs( originID, targetID ) 152 | assert( nodes[originID], string.format( "Tried to add an Edge to the nonexistent Node \"%s\".", originID )); 153 | assert( nodes[targetID], string.format( "Tried to add an Edge to the nonexistent Node \"%s\".", targetID )); 154 | addEdge( nodes[originID], nodes[targetID] ); 155 | end 156 | 157 | --- 158 | -- Removes all edges leading to, or originating from a node. 159 | -- @param node - The node to remove all edges from. 160 | -- 161 | function self:removeEdges( node ) 162 | for id, edge in pairs( edges ) do 163 | if edge.origin == node or edge.target == node then 164 | edges[id] = nil; 165 | end 166 | end 167 | end 168 | 169 | --- 170 | -- Updates the graph. 171 | -- @param dt - The delta time between frames. 172 | -- @param nodeCallback - A callback called on every node (optional). 173 | -- @param edgeCallback - A callback called on every edge (optional). 174 | -- 175 | function self:update( dt, nodeCallback, edgeCallback ) 176 | for _, edge in pairs( edges ) do 177 | edge.origin:attractTo( edge.target ); 178 | edge.target:attractTo( edge.origin ); 179 | 180 | if edgeCallback then 181 | edgeCallback( edge ); 182 | end 183 | end 184 | 185 | resetBoundaries(); 186 | 187 | for _, nodeA in pairs( nodes ) do 188 | if not nodeA:isAnchor() then 189 | for _, nodeB in pairs( nodes ) do 190 | if nodeA ~= nodeB then 191 | nodeA:repelFrom( nodeB ); 192 | end 193 | end 194 | nodeA:move( dt ); 195 | end 196 | 197 | if nodeCallback then 198 | nodeCallback( nodeA ); 199 | end 200 | 201 | minX, maxX, minY, maxY = updateBoundaries( nodeA:getPosition() ); 202 | end 203 | end 204 | 205 | --- 206 | -- Draws the graph. 207 | -- Takes two callback functions as a parameter. These will be called 208 | -- on each edge and node in the graph and will be used to wite a custom 209 | -- drawing function. 210 | -- @param nodeCallback - A callback called on every node. 211 | -- @param edgeCallback - A callback called on every edge. 212 | -- 213 | function self:draw( nodeCallback, edgeCallback ) 214 | for _, edge in pairs( edges ) do 215 | if not edgeCallback then break end 216 | edgeCallback( edge ); 217 | end 218 | 219 | for _, node in pairs( nodes ) do 220 | if not nodeCallback then break end 221 | nodeCallback( node ); 222 | end 223 | end 224 | 225 | --- 226 | -- Checks if a certain Node ID already exists. 227 | -- @param id - The id to check for. 228 | -- 229 | function self:hasNode( id ) 230 | return nodes[id] ~= nil; 231 | end 232 | 233 | --- 234 | -- Returns the node the id is pointing to. 235 | -- @param id - The id to check for. 236 | -- 237 | function self:getNode( id ) 238 | return nodes[id]; 239 | end 240 | 241 | --- 242 | -- Gets a node at a certain point in the graph. 243 | -- @param x - The x coordinate to check. 244 | -- @param y - The y coordinate to check. 245 | -- @param range - The range in which to check around the given coordinates. 246 | -- 247 | function self:getNodeAt(x, y, range) 248 | for _, node in pairs( nodes ) do 249 | local nx, ny = node:getPosition(); 250 | if x < nx + range and x > nx - range and y < ny + range and y > ny - range then 251 | return node; 252 | end 253 | end 254 | end 255 | 256 | --- 257 | -- Returns the graph's minimum and maxmimum x and y values. 258 | -- 259 | function self:getBoundaries() 260 | return minX, maxX, minY, maxY; 261 | end 262 | 263 | --- 264 | -- Returns the x and y coordinates of the graph's center. 265 | -- 266 | function self:getCenter() 267 | return ( maxX - minX ) * 0.5 + minX, ( maxY - minY ) * 0.5 + minY; 268 | end 269 | 270 | --- 271 | -- Turn a node into an anchor. 272 | -- Anchored nodes have fixed positions and can't be moved by the physical 273 | -- forces. 274 | -- @param id - The node's id. 275 | -- @param x - The x coordinate to anchor the node to. 276 | -- @param y - The y coordinate to anchor the node to. 277 | -- 278 | function self:setAnchor( id, x, y ) 279 | nodes[id]:setPosition( x, y ); 280 | nodes[id]:setAnchor( true ); 281 | end 282 | 283 | return self; 284 | end 285 | 286 | --- 287 | -- Replaces the default Edge class with a custom one. 288 | -- @param class - The custom Edge class to use. 289 | -- 290 | function Graph.setEdgeClass( class ) 291 | Edge = class; 292 | end 293 | 294 | --- 295 | -- Replaces the default Node class with a custom one. 296 | -- @param class - The custom Node class to use. 297 | -- 298 | function Graph.setNodeClass( class ) 299 | Node = class; 300 | end 301 | 302 | return Graph; 303 | -------------------------------------------------------------------------------- /Graphoon/Node.lua: -------------------------------------------------------------------------------- 1 | local current = (...):gsub('%.[^%.]+$', ''); 2 | 3 | local Node = {}; 4 | 5 | local FORCE_SPRING = 0.005; 6 | local FORCE_CHARGE = 200; 7 | 8 | local FORCE_MAX = 4; 9 | local NODE_SPEED = 128; 10 | local DAMPING_FACTOR = 0.95; 11 | 12 | local DEFAULT_MASS = 3; 13 | 14 | --- 15 | -- @param id - A unique id which will be used to reference this node. 16 | -- @param x - The x coordinate the Node should be spawned at (optional). 17 | -- @param y - The y coordinate the Node should be spawned at (optional). 18 | -- @param anchor - Wether the node should be locked in place or not (optional). 19 | -- 20 | function Node.new( id, name, x, y, anchor ) 21 | local self = {}; 22 | 23 | local px, py = x or 0, y or 0; 24 | local ax, ay = 0, 0; 25 | local vx, vy = 0, 0; 26 | local mass = DEFAULT_MASS; 27 | 28 | --- 29 | -- Clamps a value to a certain range. 30 | -- @param min 31 | -- @param val 32 | -- @param max 33 | -- 34 | local function clamp( min, val, max ) 35 | return math.max( min, math.min( val, max ) ); 36 | end 37 | 38 | --- 39 | -- Calculates the new xy-acceleration for this node. 40 | -- The values are clamped to keep the graph from "exploding". 41 | -- @param fx - The force to apply in x-direction. 42 | -- @param fy - The force to apply in y-direction. 43 | -- 44 | local function applyForce( fx, fy ) 45 | ax = clamp( -FORCE_MAX, ax + fx, FORCE_MAX ); 46 | ay = clamp( -FORCE_MAX, ay + fy, FORCE_MAX ); 47 | end 48 | 49 | --- 50 | -- Calculates the manhattan distance from the node's coordinates to the 51 | -- target coordinates. 52 | -- @param tx - The target coordinate in x-direction. 53 | -- @param ty - The target coordinate in y-direction. 54 | -- 55 | local function getManhattanDistance( tx, ty ) 56 | return px - tx, py - ty; 57 | end 58 | 59 | --- 60 | -- Calculates the actual distance vector between the node's current 61 | -- coordinates and the target coordinates based on the manhattan distance. 62 | -- @param dx - The horizontal distance. 63 | -- @param dy - The vertical distance. 64 | -- 65 | local function getRealDistance( dx, dy ) 66 | return math.sqrt( dx * dx + dy * dy ) + 0.1; 67 | end 68 | 69 | --- 70 | -- Attract this node to another node. 71 | -- @param node - The node to use for force calculation. 72 | -- 73 | function self:attractTo( node ) 74 | local dx, dy = getManhattanDistance( node:getPosition() ); 75 | local distance = getRealDistance( dx, dy ); 76 | dx = dx / distance; 77 | dy = dy / distance; 78 | 79 | local strength = -1 * FORCE_SPRING * distance * 0.5; 80 | applyForce( dx * strength, dy * strength ); 81 | end 82 | 83 | --- 84 | -- Repel this node from another node. 85 | -- @param node - The node to use for force calculation. 86 | -- 87 | function self:repelFrom( node ) 88 | local dx, dy = getManhattanDistance( node:getPosition() ); 89 | local distance = getRealDistance( dx, dy ); 90 | dx = dx / distance; 91 | dy = dy / distance; 92 | 93 | local strength = FORCE_CHARGE * (( mass * node:getMass() ) / ( distance * distance )); 94 | applyForce(dx * strength, dy * strength); 95 | end 96 | 97 | --- 98 | -- Update the node's position based on the calculated velocity and 99 | -- acceleration. 100 | -- @param dt - The delta time between frames. 101 | -- 102 | function self:move( dt ) 103 | vx = (vx + ax * dt * NODE_SPEED) * DAMPING_FACTOR; 104 | vy = (vy + ay * dt * NODE_SPEED) * DAMPING_FACTOR; 105 | px = px + vx; 106 | py = py + vy; 107 | ax, ay = 0, 0; 108 | end 109 | 110 | function self:getID() 111 | return id; 112 | end 113 | 114 | function self:getX() 115 | return px; 116 | end 117 | 118 | function self:getY() 119 | return py; 120 | end 121 | 122 | function self:getPosition() 123 | return px, py; 124 | end 125 | 126 | function self:setPosition( nx, ny ) 127 | px, py = nx, ny; 128 | end 129 | 130 | function self:setAnchor( nanchor ) 131 | anchor = nanchor; 132 | end 133 | 134 | function self:isAnchor() 135 | return anchor; 136 | end 137 | 138 | function self:setMass( nmass ) 139 | mass = nmass; 140 | end 141 | 142 | function self:getMass() 143 | return mass; 144 | end 145 | 146 | function self:getName() 147 | return name; 148 | end 149 | 150 | return self; 151 | end 152 | 153 | return Node; 154 | -------------------------------------------------------------------------------- /Graphoon/init.lua: -------------------------------------------------------------------------------- 1 | return { 2 | _VERSION = 'Graphoon v1.0.1', 3 | _DESCRIPTION = 'A force directed graph algorithm written in Lua.', 4 | _URL = 'https://github.com/rm-code/Graphoon', 5 | _LICENSE = [[ 6 | Copyright (c) 2015 - 2016 Robert Machmer 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining 9 | a copy of this software and associated documentation files (the 10 | "Software"), to deal in the Software without restriction, including 11 | without limitation the rights to use, copy, modify, merge, publish, 12 | distribute, sublicense, and/or sell copies of the Software, and to 13 | permit persons to whom the Software is furnished to do so, subject to 14 | the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be 17 | included in all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 23 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 24 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 25 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 26 | ]], 27 | 28 | Edge = require( (...):gsub('%.init$', '') .. '.Edge' ), 29 | Graph = require( (...):gsub('%.init$', '') .. '.Graph' ), 30 | Node = require( (...):gsub('%.init$', '') .. '.Node' ) 31 | }; 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 - 2016 Robert Machmer 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Graphoon 2 | [![Version](https://img.shields.io/badge/Version-1.0.1-blue.svg)](https://github.com/rm-code/Graphoon/releases/latest) 3 | [![License](http://img.shields.io/badge/Licence-MIT-brightgreen.svg)](LICENSE.md) 4 | 5 | A force directed graph algorithm written in Lua. 6 | 7 | [![example](https://cloud.githubusercontent.com/assets/11627131/12313149/44ea06a0-ba65-11e5-82d0-4867022fbdf5.gif)](http://rm-code.github.io/Graphoon/) 8 | 9 | ## Introduction 10 | 11 | _Graphoon_ emerged from the graph calculation code used in both [LoGiVi](https://github.com/rm-code/logivi) and [LoFiVi](https://github.com/rm-code/lofivi). 12 | 13 | A force directed graph layout is achieved by simulating physical forces, which push and pull each node in the graph until a nice layout is found. 14 | 15 | ## Basic Usage 16 | 17 | The basic idea is that you create a new graph object, to which you can then add nodes and edges. 18 | 19 | ```lua 20 | local GraphLibrary = require('Graphoon').Graph 21 | 22 | graph = GraphLibrary.new() 23 | graph:addNode( "Ash Williams" ) 24 | graph:addNode( "Necronomicon" ) 25 | graph:connectIDs( "Ash Williams", "Necronomicon" ) 26 | ``` 27 | 28 | By itself Graphoon only provides functionality for creating the graph and calculating the layout based on physical attraction and repulsion forces. 29 | 30 | It provides a ```draw``` and ```update``` function, which can be used to easily write your own rendering code. 31 | 32 | The ```draw``` function should be called with two callback functions. The first callback will be used for all nodes and the second one for all the edges. 33 | 34 | ```lua 35 | graph:draw( function( node ) 36 | local x, y = node:getPosition() 37 | drawCircle( 'fill', x, y, 10 ) 38 | end, 39 | function( edge ) 40 | local ox, oy = edge.origin:getPosition() 41 | local tx, ty = edge.target:getPosition() 42 | drawLine( ox, oy, tx, ty ) 43 | end) 44 | ``` 45 | 46 | At its simplest the force calculations can be updated via ```graph:update( dt )```, but the ```update``` function also can receive optional callbacks for both nodes and edges. 47 | 48 | ## Advanced usage 49 | 50 | ### Using anchors 51 | 52 | Anchors can be used to attach a node to a certain position on the screen. This can be useful if you want to center a certain node for example. 53 | 54 | This can either be done directly via the constructor of the node: 55 | 56 | ```lua 57 | -- Anchor the node to the center of the screen. 58 | graph:addNode( "Ash Williams", screenX * 0.5, screenY * 0.5, true ) 59 | ``` 60 | 61 | Or by using the ```setAnchor``` function: 62 | 63 | ```lua 64 | -- Invert anchor status 65 | node:setAnchor( not node:isAnchor(), mouseX, mouseY ) 66 | ``` 67 | 68 | ### Using custom classes for Nodes and Edges 69 | 70 | If you prefer to not touch the default classes, you can simply inherit from them and tell Graphoon to use your custom classes instead. 71 | 72 | ```lua 73 | local GraphLibraryNode = require('lib.libfdgraph.fd').Node 74 | 75 | local CustomNodeClass = {} 76 | 77 | -- You can pass additional arguments to your custom class. Just make sure the 78 | -- default parameters ar in the right order. 79 | function CustomNodeClass.new( id, x, y, anchor, ... ) 80 | local self = GraphLibraryNode.new( id, x, y, anchor ) 81 | 82 | -- ... Custom code 83 | end 84 | 85 | return CustomNodeClass 86 | ``` 87 | 88 | ```lua 89 | local GraphLibrary = require('Graphoon').Graph 90 | GraphLibrary.setNodeClass( require('CustomNodeClass') ) 91 | ``` 92 | --------------------------------------------------------------------------------