Library
User Friendly Interfaces
GraphPlayground.playground — Function
playground(g;
[link_options = (;iterations=1,distance=30),]
[center_options = NamedTuple(),]
[charge_options = NamedTuple(),]
[graphplot_options = NamedTuple(),]
[initial_iterations = 10,]
[labels = map(i->string(i), 1:nv(g)),]
[verbose = false,]
[kwargs...] )
playground(g, sim; kwargs...)Create a graph playground window from the graph g.
The value that is returned is a Playground object, which is a thin wrapper around the window, the simulation, and the Makie axis.
Optional parameters
link_options: These options are passed to theLinkForceconstructor. The default is(;iterations=1,distance=30)`. This is generally good. For grid graphs, set iterations higher to get a better layout.center_options: These options are passed to thePositionForceconstructor.charge_options: These options are passed to theManyBodyForceconstructor.graphplot_options: These options are passed to thegraphplot!function.initial_iterations: The number of layout iteration to run before the first display. The default is 10.labels: A list of strings to display for the node identifiers. By default these are the numeric node idsverbose: If true, additional information is shown in the lower left corner of the plot.
Examples
g = wheel_graph(10)
playground(g)For a grid, this often looks much better
g = grid([10,10])
playground(g;
link_options=(;iterations=10, strength=1, distance=20))See also
Force Simulator Interface
GraphPlayground.ForceSimulation — Type
ForceSimulation([PositionType,] nodes; [...])
ForceSimulation(positions, nodes; [rng,] [alpha,] [velocity_decay,] forcelist...)Create a force simulator for a set of positions. This evaluates and evolves the positions based on the forces applied. It is designed to be used with evaluating a dynamic force directed graph layout including attractive forces for edges and repulsive forces for nodes. But it may have other uses as well. For instance, collision forces can be used to simulate packed bubble charts with various radii.
Arguments
nodesis any array of nodes. This can be very simple, i.e. 1:n, or a list of objects. The objects must be used consistent with other forces involved.PositionTypeis the type of the positions. UsingPoint2fis recommended and the defaultpositionsis an array of initial positions. The position type is determined by the elements of
the array.
forcelistis a trailing list of forces. The names of these forces do not matter. The order is the order in which they are applied. While forcelist is not syntactically required, it is semantically required as otherwise the simulation will not do anything.
Optional Arguments
rngis a random number generator. This is used for the initial positions and for any random perturbations if there are degeneracies. The default is to use a deterministic generator so that the results are reproducible.alphais the cooling stepper. This is used to control the rate of convergence. SeeGraphPlayground.CoolingStepperfor more information.velocity_decayis the factor by which the velocities are decayed each step. Setting this to 1 will not decay the velocities. Setting it to 0 will stop all motion. The default is 0.6.
Usage
Here is an example that packs balls of different sizes into a region around the point (0,0).
radii = 1:10
sim = ForceSimulation(1:10;
collide=CollisionForce(radius=radii, iterations=3),
center=PositionForce(target=(0,0)))
initial_positions = copy(sim.positions)
step!(sim, 100) # run 100 steps
plot(sim.positions; markersize=(radii .- 0.5).*pi/1.11,
markerspace=:data, strokewidth=0.25, strokecolor=:white) # weird 1.11 to get the right size, add 0.05 Forces
The list of forces can have silly names if you wish. The names are not used other than for display. For example, this is entirely valid:
sim = ForceSimulation(1:10;
collide=CollisionForce(radius=radii, iterations=3),
push_nodes_to_middle=PositionForce(target=(0,0))
push_nodes_to_offset=PositionForce(target=(10,10)))Of course, that generates a very useless simulator.
Forces
LinkForce: This force applies a spring force to all edges in the graph. The force is proportional to the distance between the nodes.ManyBodyForce: This force applies a repulsive force between all nodes. The force is proportional to the inverse square of the distance between the nodes.PositionForce: This force applies a force to all nodes to move them to a target position. This is useful for centering the graph or pushing nodes to the edge.CollisionForce: This force applies a repulsive force between all positions. The force is proportional to the sum of the radii of the nodes.CenterForce: This force directly centers all the positions.
Data
The simulator maintains the following data that are useful:
positions: The current positions of the nodes.velocities: The current velocities of the nodes.
You can access these directly.
Methods
To fix a node in place, use fixnode!(sim, i, pos). To free a node, use freenode!(sim, i). To take a step, use step!(sim). To take multiple steps, use step!(sim, n).
See also
step!, fixnode!, freenode!, LinkForce, ManyBodyForce, PositionForce, CollisionForce, CenterForce
GraphPlayground.fixnode! — Function
fixnode!(sim::ForceSimulation, i, pos)Fix the position of a node in the simulation. This will prevent the node from moving. This importantly keeps the velocity of the node set to 0, which will prevent the node from updating other implicit positions.
GraphPlayground.freenode! — Function
freenode!(sim::ForceSimulation, i)Remove the fixed position of a node in the simulation. This will allow the node to move.
GraphPlayground.step! — Function
step!(sim) # take one step
step!(sim, n) # take n stepsTake a step of the force simulator. This will apply all forces in the order they were added to the simulator. The forces are applied to the positions and velocities. The velocities are then decayed by the velocity_decay factor.
See ForceSimulation for more information and an example.
GraphPlayground.LinkForce — Type
LinkForce(;edges)
LinkForce(;edges, [strength], [distance], [bias], [iterations], [random])A link force computes forces between nodes that emulate a strong connection. This is useful for graphs where the edges represent strong connections between nodes.
The force applied between two nodes is based on the strength of the link, the distance, the strength of the edge. The bias of the edge is used to determine how much the nodes should move.
For an edge between src, dst, let $d$ be the difference vector between the position of dst and src with their velocity corrections included. The total force is $f = \alpha \cdot s \cdot (||d|| - l) / ||d||$ where $l$ is the ideal distance and $s$ is the strength of the link. The force is applied to the velocity of the nodes proportional to the bias of the edge $\beta$
`vel[dst] -=` ``\beta f \cdot d``
`vel[src] +=` ``(1-\beta) f \cdot d``The bias is used to determine how much the nodes should move. If the bias is 0, then the update is exclusively provided to the src node. If the bias is 1, then the update is exclusively provided to the dst node.
Arguments
edges: An array of edge structures, where each edge structure containssrcanddstfields or can be indexed like a tuple with e[1], e[2] as the source and destination nodes.
Optional Arguments
strength: A function or array of values that determine the strength of the link between two nodes. By default, this is based on the number of edges between the nodes: 1/(min(degree(src), degree(dst))).distance: A function or array of values that determine the ideal distance between two nodes. By default, this is 30.0. But this can be a function that takes the edge index and returns a distance.bias: A function or array of values that determine the bias of the link between two nodes. This is designed to weight how much the nodes should move. It's designed to make it harder to move high degree nodes.iterations: The number of iterations to run the link force. The default is 1. Each iteration updates the velocity but not the positions. However, the velocity updates are included in the force calculations. So by running multiple iterations, the forces are more accurate. This is because we update the velocities in-place. Using large values here are most important for grids or graphs with a lot of structure.random: A random number generator. This is used for the random perturbations. The default is to use a deterministic generator so that the results are reproducible. I can't imagine why you would need to use this, but it's here in case someone needs to reproduce something strange.
Function inputs
An exmaple of using it with a function is the following
val = randn(10)
f = LinkForce(;edges=edges, strength=(i,e,src,dst)->val[src]*val[dst])This is called with
i: The index of the edge in the edges array.e: The edge structure.src: The source node.dst: The destination node.
This same structure is used for all strength, bias, and distance.
Usage
LinkForce is usually used as part of a ForceSimulation. Here, we setup something simple with two nodes at distance 1. But that want to be at distance 10 given the edge between them.
nodes = [1,2]
edgelist = [(1, 2)]
positions = [Point2f(0.0, 0.0), Point2f(1.0, 0.0)]
sim = ForceSimulation(positions, nodes;
link=LinkForce(edges=edgelist, strength=10, distance=10.0, bias=0.25))
iforce = sim.forces.link
# iforce is an `InitializedLinkForce` that's been linked to the simulation
GraphPlayground.force!(0.1, sim, iforce) # Assume alpha=0.1
sim.velocitiesThis example shows how the [LinkForce]computes the velocities of the nodes to move them away from each other. The reason the update is nonsymmetric is because of the bias. This says that we want to move node 1 more than node 2.
See also
GraphPlayground.ManyBodyForce — Type
ManyBodyForce()
ManyBodyForce(; [strength], [min_distance2], [max_distance2], [theta2], [random])Create a force defined by multiple bodies. This force is used to simulate the repulsion or attraction between nodes of the simulation. If you wish to apply to only a subset of nodes, you can set the strength to zero for the nodes you wish to ignore.
This computation is implemented with a space partitioning data structure (current a KDTree) to approximate the impact of distance forces using a far-field approximation (this is often called a Barnes-Hut approximation, but that doesn't help understand what is going on). Setting theta2 to zero will cause it to discard the approximation and compute the exact force. Reasonable values for theta2 are between 0.5 (better approximation) and 1.5 (poor approximation). (This is the square of the $\theta$ value commonly used in Barnes-Hut approximations.)
Arguments
strength: A constant, a function or array of values. The repulsive strength to use, defaults to -30, which is a repulsive force between nodes.min_distance2: A constant, that defines a minimum distance between nodes. If the distance between two nodes is less than this value, the force is increased a bit. The default is 1.0.max_distance2: A constant, that defines a maximum distance between nodes. If the distance between two nodes is greater than this value, the force is ignored. The default is Inf.theta2: A constant, that defines the accuracy of the approximation. The default is 0.81, which is a reasonable value for most simulations.random: A random number generator. This is used for the random perturbations.
GraphPlayground.CollisionForce — Type
CollisionForce([radius,] [strength])Create a collision force. This force is used to simulate collisions between nodes.
GraphPlayground.PositionForce — Type
PositionForce(;[target] [, strength])PositionForce represents a force that directions nodes of the simulation towards specific target positions.
Arguments
target: The target position of each node. This can be a single value or an array of values. The default is (0,0), which tries to center the positions.strength: The strength of the force, which is a real number. The default is 0.1.
See also
GraphPlayground.CenterForce — Type
CenterForce represents a centering adjustment in a force simulation. it has two parameters:
center: The center of the force, which can be anything resembling a pointstrength: The strength of the force, which is a real number
Note that CenterForce directly applies the force to the positions of the nodes in the simulation instead of updating their velocities.
Use PositionForce to apply a force to the velocities of the nodes instead. (Also, please don't combine PositionForce and CenterForce.)
Example
rad = 10*rand(100) sim = ForceSimulation(Point2f, eachindex(rad); center=CenterForce(center, strength=1.0), collide=CollisionForce(radius=rad) ) p = scatter(sim.positions, markersize=rad) for i in 1:100 step!(sim) p[:node_pos][] = sim.positions sleep(0.5) end
See also
GraphPlayground.CoolingStepper — Type
A model of the cooling step in d3-force. The stepper allows dynamic retargeting of the cooling factor, which is useful in simulations where you want to adjust behavior for user interaction or for incoming data.
Once the stepper has reached it's minimum value, it will return zero for all subsequent steps.
Usage: ```julia alpha = CoolingStepper() for i=1:10 println(step!(alpha)) end alpha.alphatarget = 0.5 for i=1:10 println(step!(alpha)) end alpha.alphatarget = 0.0 for i=1:10 println(step!(alpha)) end
Extra window help
GraphPlayground.Window — Function
Window(loop::Function, scene; [title="GraphPlayground", size=(800,800), kwargs...])Create a window based on a scene. The window will run the provided loop function ever frame. The loop function should take a single argument, which is the time since the window was opened. This function is a fairly thin wrapper around GLMakie.Screen and GLMakie.display_scene!, but makes it easier to abstract in the future.
Parameters
loop: A function that will be called every frame. The function should take a single argument, which is the time since the window was opened.scene: The scene to display in the window.title: The title of the window. Default is "GraphPlayground".size: The size of the window. Default is (800,800).kwargs: Additional keyword arguments to pass to the GLMakie.Screen constructor.
Example
This example shows a bunch of points that are going to be pushed away from each other in a simulation of a collision.
using GeometryBasics, GraphPlayground, GLMakie
scenesize = 500
n = 100
scene = Scene(camera=campixel!, size=(scenesize, scenesize))
pts = Observable((scenesize/2*rand(Point2f0, n)) .+ (scenesize/4)*Point2f(1,1))
radius = rand(10:20, n)
sim = ForceSimulation(pts[], eachindex(pts[]);
collide = CollisionForce(radius=radius .+ 2, iterations=3))
scatter!(scene, pts, markersize=pi*radius/1.11)
GraphPlayground.Window(scene;
title="Collision Simulation", size=(scenesize, scenesize),
focus_on_show = true) do _
step!(sim)
pts[] = sim.positions
end Index
GraphPlayground.CenterForceGraphPlayground.CollisionForceGraphPlayground.CoolingStepperGraphPlayground.ForceSimulationGraphPlayground.LinkForceGraphPlayground.ManyBodyForceGraphPlayground.PositionForceGraphPlayground.WindowGraphPlayground.fixnode!GraphPlayground.freenode!GraphPlayground.playgroundGraphPlayground.step!