diff --git a/examples/graph_animations/example0.md b/examples/graph_animations/example0.md new file mode 100644 index 000000000..aed378059 --- /dev/null +++ b/examples/graph_animations/example0.md @@ -0,0 +1,123 @@ +## Graph Animation Demos + +The demo serves to list the features that I am going to implement for graph animation using Javis. The example links at the end will take you to use cases that I think are worth considering before implementing the API. + +### List of features + +* Graph type invariance - The user should have the flexibility in terms of the type of the graph. The default graph type supported would be from LightGraphs.jl +* Add/Remove edges or nodes - Upon such changes the layout should change automatically to take into account the new arrangement +* Utilities to update the graph + 1. `addNode!` - add a node on the canvas + 2. `addEdge!` - add an edge on the canvas + 3. `changeNodeProperty!` - Update drawing style of node(s) on canvas + 4. `changeEdgeProperty!` - Update drawing style of edge(s) on canvas + 5. `updateGraph!` - Takes in the updated input graph object and updates the drawing properties of nodes and edges correspondingly +* Animation tools on graph + 1. `animate_inneighbors` - Incoming neighbors (for a directed graph) + 2. `animate_outneighbors` - Outgoing neighbors (for a directed graph) + 3. `animate_neighbors` - All neighbors + 4. `highlightNode` - Highlight node(s) using flicker animation of a node property + 5. `highlightEdge` - Highlight edge(s) using flicker animation of an edge property + 6. `animatePath` - Animate a path on the graph + 7. `bfs` - Animate bfs at a node + 8. `dfs` - Animate dfs at a node + +### Examples + +1. [Graph Traversal](example1.md) +2. [Depth First Search](example2.md) +2. [Shortest Path](example3.md) +3. [Cycle Detection]() +4. [Minimum Spanning Tree]() +5. [Bipartite Matching]() +6. [Strongly connected components]() +7. [Graph Coloring]() +8. [Gradient backpropagation]() + +### Reference implementation till now + +The struct definitions of `GraphAnimation`, `GraphNode` & `GraphEdge` are provided in [Graphs.jl](../../src/structs/Graphs.jl) + +### Updates + +#### 22 June +Completed: +* ~~A way to create graphs using `Object(1:100, JGraph(...)`~~ + * Solved temporarily using multiple dispatch on the object constructor and `@Object` macro that expands the tuple `(draw_func, metadata)` returned by `JGraph`. +* ~~How to provide layout options to users.~~ + * Provide 2 layout options for now and keep a `none` mode so that user can specify his own layout. +* ~~How to access reference to node/edge objects from the parent graph.~~ + * Keep a lightweight adjacency list in the parent with nodes and edges having an attribute to store their position in the ordering list. + +Working on: +* How to make a graph node customizable. Having predefined drawing functions like `drawNode(shape="circle", radius=12, text="node", text_align="top_left")` with lots of options does not seem extendible. + * Use a plugin mechanism to let the user write their own drawing functions for certain node properties while reuse other default options. +* Add nodes to a graph object. The usual method of `Object(1:100, GraphNode(...))` has a problem of how to register a graph node object to a graph. + * Using a macro syntax `@Graph g 1:100 GraphNode(...)` to register a created node object with the parent graph. + +Approach(s) thought of (or used): +* To provide custom drawing options, provide an interface like +```julia +@Graph g 1:100 GraphNode(12, [draw_shape(:square, 12), draw_text(:inside, "123"), fill(:image, "./img.png"), custom_border()]) +``` +This requires custom functions to adhere to some rules and export some parameters to other functions. For example, if nodes are drawn as square return `text_bbx` to support the option `:inside` of `draw_text`. Similarly to have your own custom border you can use `border_bbx` from a drawing function to create a border around the node. + +Stuck on: +* How to manage keywords arguments passed to different drawing functions. For example, an object passes all the change keyword arguments to the drawing function, but to support node drawing functions like `draw_shape(:square, 12)` or custom functions like `star(...)` which may return something similar to `args(...; length=12)` only specific keyword armguents need to be supplied. + +#### 29 June +Completed: +* A demo to do a basic graph animation. +* Add custom node shapes (square and circle), node borders (square and circular borders), node filling (color), node annotation (text within a box) +```julia +@Graph g 1:100 GraphNode(12, [draw_shape(:square, 12), draw_text(:inside, "123"), fill(:color, "red"), border("yellow")]) +``` +* The predefined functions above like `draw_shape`, `draw_text` etc. each return a function having some keyword arguments to be used by this draw function. + * These options need to be provided by the user or exposed by some draw function like `draw_shape`--exposes-->`:text_box`--usedby-->`draw_text`. + * The issue was how to identify and compile this pool of keywords into a single draw function. + +Working on: +* Extending the options available for node drawing configuration. +* Edge drawing functions and line animation options. + * Need to handle self-loops and curves in graph. + * Animate a line generated from source to destination. + +Approach(s) thought of (or used): +* Add a regular polygon option for node draw shape and add compatible support for it for borders and text box. + +Stuck on: +* Aligning text on straight edges depending on the direction of edge. +* Approximate area to draw self loop edges to prevent clutter. + +#### 6th July +Completed: +* Node drawing configurations and demo. + * Divided node property into shape, text, fill & border. +* Animating line (curved lines) from a source to destination node + +Working on: +* Edge drawing properties :- shape, style, label, arrowheads. + * Shape will provide a clip over the edge which maybe a curved or straight line. Shape also includes line width, end offsets and curvature. + * Styles incorporate features like color blends and dash type. + * Arrow deals with options to set arrows on the edge. + * Label/text allows positioning text boxes/latex relative to the edge + +Stuck on: +* For both nodes and edges, the `node_shape` and `edge_shape` function was supposed to provide a clip around the edge and any custom function provided by the user would be clipped within that region. `:clip` action does not work as expected on a line. +* How to return a edge outline for edges of different shapes? For example, for a line it an be 2 points for a circle it can be 3 points etc. This is required when positioning labels/glyphs with relative positioning on the edge. + +#### 13th July +Completed: +* Add self-loops to graph + +Plans for this week: +* complete implementation for `edge_label`, `edge_arrow`, `edge_style` +* complete `dynamic` mode for graph creation +* complete graph animation utlities highlight, change property, update graph, animate* +* Update documentation for prevailing code +* Add unit tests for node and edge drawing functions +* Update examples-(1, 2, 3) with working code + +Stuck on (Backlog for now): +* Self-loop egde orientation +* Edge flickering when using `Luxor.text` \ No newline at end of file diff --git a/examples/graph_animations/example1.md b/examples/graph_animations/example1.md new file mode 100644 index 000000000..2fef68784 --- /dev/null +++ b/examples/graph_animations/example1.md @@ -0,0 +1,193 @@ +## Graph traversal using BFS + +### Points covered +1. Graph creation +2. Algorithm explanation through two different visualisations + +### Graph Creation + +The graph object can be created/initialized by the use of a LightGraph typed object or any arbitrary graph types. In the latter case, there is a need to specify some additional accessibility functions to make use of some advanced visualisation features. It is discussed in later examples. This one covers how to do it with the use of a simple adacency list. It is supposed to be the most simplest way one can use the API with almost a zero learning curve. + +**Graph representation** +```julia +graph = [[2, 3, 4, 5], + [6, 7], + [8], + [], + [], + [], + [], + []] +``` +The input graph can be of any Julia data type. This does not impose any restriction on the type of graph but for advanced features it requires some extra work. An alternative to this is using `LightGraphs.jl` for which there are many convenience functions available. Its usage is covered in [Example 2](example2.md). + +**Graph initialization** +```julia +# Parameters - Graph object | is directed? | width | height | starting position +ga = GraphAnimation(graph, true, 300, 300, O) +``` + +**Node registration** +```julia +nodes = [Object(@Frames(prev_start()+5, stop=100), GraphNode(i, drawNode; animate_on=:scale, fill_color="yellow", border_color="black", text=string(i), text_valign=:middle, text_halign=:center)) for i in range(1, 8; step=1)] + +# TODO: Need to find the best way to map drawing arguments like text_align (specified before) into the drawing function. Using a dictionary, seems a good idea. +function drawNode(draw_opts) + sethue(draw_opts[:fill_color]) + circle(draw_opts[:position], 5, :fill) + sethue(draw_opts[:border_color]) + circle(draw_opts[:position], 5, :stroke) + text(draw_opts[:text], draw_opts[:position], valign = draw_opts[:text_valign], halign = draw_opts[:text_halign]) +end +``` +The set of drawing options (like `border_color`) that can be supported depends solely on the user provided parameters and the drawing function used. The utility functions like `highlightNode` take as input one of these drawing parameters and a new value for it and perform highlighting operations on them. + +In [Example 3](example3.md), I will demonstrate how to map these drawing options to node properties which are part of the input graph object. That will help animate changes in node properties simultaneously without any additional coding. + +The additional option `animate_on` controls the appearance of the node on the canvas. The same option is used during removal of the nodes/edges. + +The options available are: +* `:opacity` - both nodes and edges +* `:scale` - only for nodes +* `:line_width` - only for edges +* `:length` - only for edges + +The result is eight balls drawn on the canvas at fixed locations unaltered by changes in the graph by addition/deletion of new nodes. + +**Edge registration** + +```julia +edges=[] +for (index, node) in enumerate(graph) + for j in node + push!(edges, Object(@Frames(prev_start()+5, stop=100), GraphEdge(index, j[1], drawEdge; animate_on=:length, color="black"))) + end +end + +# Need to provide custom drawEdge functions to account for self-loops, curved edges etc. +function drawEdge(opts) + sethue(opts[:color]) + line(opts[:position1], opts[:position2], :stroke) +end +``` + +### Graph visualisation + +The nodes already visited need to be marked and a data structure like queue providing FIFO access is needed to store the order of traversal of nodes. +```julia +using DataStructures +# vis[x] indicates if a node is visited and Q is a queue data structure +vis=[false for i in 1:8] +Q=Queue{Int}() +``` + +The algorithm can be explained in two ways: + +**Using simple coloring** +* Use a different fill color to convey the new nodes in the queue +* Change the color of visited nodes permanently + +```julia +enqueue!(Q, 1) +# The frames argument is optional here. +changeNodeProperty!(ga, 1, :fill_color, "green") +while !isempty(Q) + i=dequeue!(Q) + vis[i]=true + changeNodeProperty!(ga, i, :fill_color, "blue") + for j in neighbors(i) + if !vis[j] + enqueue!(Q, j) + changeNodeProperty!(ga, j, :fill_color, "green") + end + end +end +``` + +**Using node color or border color highlighting** +* Use a different fill or border color to convey the currently highlighted node +* Change the color of visited nodes permanently + +```julia +# vis[x] indicates if a node is visited and Q is a queue data structure +highlightNode(ga, 1, :fill_color, "white") # Flicker between original yellow and white color for some default number of frames +vis[1]=true +changeNodeProperty!(ga, 1, :fill_color, "orange") +enqueue!(Q, 1) +while !Q.empty() + i=dequeue!(Q) + for j in neighbors(i) + if !vis[j] + highlightNode(ga, j, :fill_color, "white") + vis[j]=true + changeNodeProperty!(ga, j, :fill_color, "orange") + enqueue!(Q, j) + end + end +end +``` + +## Full Code + +```julia +using Javis, DataStructures + +function ground(args...) + background("white") + sethue("black") +end + +function drawNode(draw_opts) + sethue(draw_opts[:fill_color]) + circle(draw_opts[:position], 5, :fill) + sethue(draw_opts[:border_color]) + circle(draw_opts[:position], 5, :stroke) + text(draw_opts[:text], draw_opts[:position], valign = draw_opts[:text_valign], halign = draw_opts[:text_halign]) +end + +function drawEdge(opts) + sethue(opts[:color]) + line(opts[:position1], opts[:position2], :stroke) +end + +graph = [[2, 3, 4, 5], + [6, 7], + [8], + [], + [], + [], + [], + []] + +video=Video(300, 300) +Background(1:100, ground) + +ga = GraphAnimation(graph, true, 300, 300, O) +nodes = [Object(@Frames(prev_start()+5, stop=100), GraphNode(i, drawNode; animate_on=:scale, fill_color="yellow", border_color="black", text=string(i), text_valign=:middle, text_halign=:center)) for i in range(1, 8; step=1)] + +edges=[] +for (index, node) in enumerate(graph) + for j in node + push!(edges, Object(@Frames(prev_start()+5, stop=100), GraphEdge(index, j[1], drawEdge; animate_on=:length, color="black"))) + end +end + +vis=[false for i in 1:8] +Q=Queue{Int}() +enqueue!(Q, 1) +changeNodeProperty!(ga, 1, :fill_color, "green") + +while !isempty(Q) + i=dequeue!(Q) + vis[i]=true + changeNodeProperty!(ga, i, :fill_color, "blue") + for j in neighbors(i) + if !vis[j] + enqueue!(Q, j) + changeNodeProperty!(ga, j, :fill_color, "green") + end + end +end + +render(video; pathname="example1.gif") +``` diff --git a/examples/graph_animations/example2.md b/examples/graph_animations/example2.md new file mode 100644 index 000000000..68726c57a --- /dev/null +++ b/examples/graph_animations/example2.md @@ -0,0 +1,196 @@ +## Depth First Search + +### Points covered +1. Graph creation using `LightGraphs.jl` +2. Demonstrate additional utility functions + +### Graph creation + +In the previous example, it was shown how a graph can be created and animated for a simple graph data type. To simplify a lot of details one can provide a known graph type i.e. from JuliaGraphs. The candidate types for this are: +* `SimpleGraph` +* `SimpleDiGraph` +* `SimpleWeightedGraph` +* `SimpleWeightedDiGraph` + +Here I cover the case when it is represented using `SimpleGraph` from the LightGraphs package. + +```julia +using LightGraphs +g = SimpleGraph(6) +add_edge!(g, 1, 2) +add_edge!(g, 1, 3) +add_edge!(g, 2, 4) +add_edge!(g, 3, 5) +add_edge!(g, 4, 6) + +ag, nodes, edges = create_graph(g, 300, 300; layout=:spring, mode=:static) +``` +`ag` - A Javis object storing some useful meta-data corresponding to the graph. Only the translate operation is supported on it as of now. + +`nodes` - list of references to node objects in the order of node id + +`edges` - list of references to edge objects in the order of creation + +The `layout` defines how the nodes shall be arranged on the canvas. The `mode` argument determines whether the graph is to be considered a static or a dynamic graph. + +#### Static graphs +* The layout computation is done only once when the entire graph is known. +* Updates to nodes/edge properties in the input graph are not animated i.e. the final graph state is used for the animation + +#### Dynamic graphs +* Addition of each new node leads to recomputation of new layout +* Group animations of nodes/edges are separated across frames using a predefined ordering +* Updates to edges and nodes through properties are animated with the help of action transitions leading to visualisation for time evolving graphs + +*Note - Dynamic layout will be much more computationally intensive than static layout* + +**Adding or removing nodes and edges** + +New nodes and edges can be added to the canvas or existing nodes deleted after the graph creation. +```julia +removeNode!(ag, 2) +addNode!(ag, 7) +addEdge!(ag, 1, 7) +``` +or +```julia +addEdge!(ag, 1, 7, 20) +``` +The last argument is the edge weight. This mimics the `add_edge!` function provided by the LightGraph interface, where the `weight` parameter is valid if the internal graph type is a `SimpleWeightedGraph`. + + +### Visualisation and demo utility functions + +Rendering the video at this stage would just draw and animate a plain graph based on the underlying `LightGraph` type. It picks up reasonable defaults for these animations for e.g. if the graph is of type `SimpleWeightedGraph` the edge weights are simply centered on the lines drawn. + +```julia +current=1 +dst=6 +path=[] +visited=[false for i in 1:nv(g)] +num_visited=0 +``` + +The `path` variable stores the path to the destination node. The additional variable `num_visited` is needed to organize the animation of the call to the utility functions one after another. Once a relative way to define action frames across different objects is available this variable won't be necessary. + +```julia +function dfs_and_animate(node, path) + if node==dst + highlightNode(GFrames(20+num_visited*10, 100), ag, current, :border_color, "red") + return + end + # Highlight the current node + highlightNode(GFrames(20+num_visited*10, 100), ag, current, :border_color, "yellow") + visited[current]=true + num_visited+=1 + push!(path, current) + # Change node color when highlighting effect ends + changeNodeProperty!(@Frames(prev_end(), stop=parent_end()), ag, current, :color, "blue") + for nb in neighbors(g, current) + if visited[nb] + continue + end + highlightEdge(GFrames(20+num_visited*10, 100), ag, current, nb, :color, "green") + num_visited+=1 + dfs_and_animate(nb, path) + end + # Highlight again to indicate return to parent node + highlightNode(GFrames(20+num_visited*10, 100), ag, current, :border_color, "yellow") + num_visited+=1 + pop!(path) +end + +dfs_and_animate(1, path) +``` + +Note - Even though the drawing property `:color` was not explicitly defined by passing it to a drawing function, these are provided as defaults for every node and edges in the case the graph is created using `create_graph`. + +The default drawing properties provided are - +* `fill_color` - for nodes +* `border_color` - for nodes +* `radius` - for nodes +* `color` - for edges +* `weights` - this property is only available for edges in weighted graphs +* `opacity`, `scale` & `line_width` - defined in [example 1](example1.md) + +If frame management becomes an issue, it is possible to skip it completely and let Javis handle the frame management, again through the use of reasonable defaults. For example, skipping frames in `highlightNode` schedules the node highlighting after the end of the previous animation specified on the graph. This animation could be any of the graph utility functions with the exception of `changeNodeProperty` and `changeEdgeProperty` for which the starting frame is used as reference. To avail of this default option, the `default_keyframes=true` option needs to be passed in `create_graph`. + +Most utility functions like `changeNodeProperty` or `highlightNode` or `animate_*` use Javis actions underneath in the implementations which makes it easy to arrange them via frames. + +**Using `animate_neighbors`** + +Animating neighboring nodes and edges can be simplified by using the `animate_neighbors` utlity function + +```julia +animate_neighbors(ag, 1; animate_node_on=(:fill_color, "green"), animate_edge_on=(:color, "red")) +``` +This internally makes a call to `changeNodeProperty` and `changeEdgeProperty`. If the keyword arguments are not provided, simple switching highlighting is used for the animation. + +**Using `animate_path`** + +After the traversal has been done and the shortest path found, show the path by animating it. + +```julia +# change back the graph to its original form +changeNodeProperty!(ag, (node)->true, :fill_color, "white") +changeEdgeProperty!(ag, (nodes...)->true, :color, "black") +animate_path(ag, path; animate_node_on=(:fill_color, "green"), animate_edge_on=(:color, "red")) +``` + +In the default case, when nothing is specified about how to animate nodes/edges in a path simple on/off highlighting is used on the universal property `:opacity`. + +## Full Code + +```julia +using LightGraphs +g = SimpleGraph(6) +add_edge!(g, 1, 2) +add_edge!(g, 1, 3) +add_edge!(g, 2, 4) +add_edge!(g, 3, 5) +add_edge!(g, 4, 6) + +video=Video(300, 300) +Background(1:100, ground) + +ag, nodes, edges = create_graph(g, 300, 300; layout=:spring, mode=:static) + +current=1 +dest=6 +path=[] +visited=[false for i in 1:nv(g)] +num_visited=0 + +function dfs_and_animate(node, path) + if node==dest + highlightNode(GFrames(20+num_visited*10, 100), ag, current, :border_color, "red") + return + end + # Highlight the current node + highlightNode(GFrames(20+num_visited*10, 100), ag, current, :border_color, "yellow") + visited[current]=true + num_visited+=1 + push!(path, current) + # Change node color when highlighting effect ends + changeNodeProperty!(@Frames(prev_end(), stop=parent_end()), ag, current, :color, "blue") + for nb in neighbors(g, current) + if visited[nb] + continue + end + highlightEdge(GFrames(20+num_visited*10, 100), ag, current, nb, :color, "green") + num_visited+=1 + dfs_and_animate(nb, path) + end + # Highlight again to indicate return to parent node + highlightNode(GFrames(20+num_visited*10, 100), ag, current, :border_color, "yellow") + num_visited+=1 + pop!(path) +end + +dfs_and_animate(1, path) +changeNodeProperty!(ag, (node)->true, :fill_color, "white") +changeEdgeProperty!(ag, (nodes...)->true, :color, "black") +animate_path(ag, path; animate_node_on=(:fill_color, "green"), animate_edge_on=(:color, "red")) + +render(video; pathname="example2.md") +``` diff --git a/examples/graph_animations/example3.md b/examples/graph_animations/example3.md new file mode 100644 index 000000000..617fc3c45 --- /dev/null +++ b/examples/graph_animations/example3.md @@ -0,0 +1,239 @@ +## Minimum cost path problem + +In this example, we shall cover visualising the minimum-cost path finding algorithm for weighted graphs. The graph shall be represented using a custom data type. However, one could use the `MetaGraph` package from `JuliaGraphs`, where extra node properties can be stored and managed. + +### Djikstra's Algorithm + +This is one of the most famous graph algorithms concieved by computer scientist Edsger W. Dijkstra in 1956. It exists in many variants i.e. finding the shortest path from a node to another node in a weighted graph or to all other nodes. In this demo we will look at the the former variant only. + +The algorithm proceeds as follows: +1. Mark all nodes as unvisited and group them into an *unvisited* set. +2. Assign an initial weight of infinity to all nodes except the initial node which gets a weight 0. Mark this node as currrent. +3. For every unvisited neighbour of the current node, update its weight to the minimum of its current value and the new tentative weight computed. The tentative weight is computed as the sum of the parent node value and the edge weight between the two nodes. +4. Mark the current node as visited and remove it from unvisited set. +5. When the destination node is marked visited, stop. +6. Otherwise, mark the node from the nieghbouring set with the minimum weight as current and repeat step 3. + +### Demo + +**Graph creation** + +Create an example graph using a custom data type. For simplicity, the graph is assumed to be connected. +```julia +# The weights are used later in the algorithm. Storing it here avoids requiring any additional data structures. The neighbor list elements are read as (node_id, edge_weight) +g = [Dict(:weight=>0, :neighbors=>[(2, 3), (3, 5)]), + Dict(:weight=>1000, :neighbors=>[(4, 1), (5, 2)]), + Dict(:weight=>1000, :neighbors=>[(5, 1), (4, 2)]), + Dict(:weight=>1000, :neighbors=>[(6, 1)]), + Dict(:weight=>1000, :neighbors=>[(4, 3)]), + Dict(:weight=>1000, :neighbors=>[])] +``` + +(*Visualisation coming up*) + +**Register graph in Javis** +```julia +wg = GraphAnimation(g, false, 300, 300, O; + node_attribute_fn=(g, node, attr) -> n(g, node, attr), + edge_attribute_fn=(g, node1, node2, attr) -> e(g, node1, node2)) + +function n(g, node, attr) + if attr==:weight && g[node][attr]==1000 + return "inf" + else + return g[node][attr] + end +end + +function e(g, node1, node2) + for j in g[node1][:neighbors] + if j[1]==node2 + return j[2] + end + end +end +``` + +**Create nodes and edges** + +Once the graph has been registered, we need to register nodes and edges to it. This can be skipped if the base graph representation was one of the known graph types listed in [example 2](example2.md). However, this way of doing it provides much more flexibility to customise the drawing function, frame management, etc. + +```julia +nodes = [Object(@Frames(prev_start()+5, stop=100), GraphNode(i, drawNode; animate_on=:scale, property_style_map=Dict(:weight=>:weight), fill_color="white", border_color="black", text=string(i))) for i in range(1, 6; step=1)] + +function drawNode(opts) + sethue(opts[:fill_color]) + circle(opts[:position], 5, :fill) + sethue(opts[:border_color]) + circle(opts[:position], 5, :stroke) + text(opts[:text], opts[:position], valign = :middle, halign = :center) + text(opts[:weight], opts[:position]+(0, 8), valign = :middle, halign = :center) +end + +edges=[] +for (index, node) in enumerate(g) + for j in node[:neighbors] + push!(edges, Object(@Frames(prev_start()+5, stop=100), GraphEdge(index, j[1], drawEdge; animate_on=:scale, property_style_map=Dict(:weight=>:line_width, :weight=>:text), color="black"))) + end +end + +# Need to provide custom drawEdge functions to account for self-loops, curved edges etc. +function drawEdge() + sethue(opts[:color]) + setline(opts[:line_width]) + line(opts[:position1], opts[:position2], :stroke) + # Now add the edge weight aligned to the edge line/curve + translate((opts[:position1]+opts[:position2])/2) + rotate(slope(opts[:position1], opts[:position2])) + translate(Point(0, 3)) + text(opts[:text], O, valign = :middle, halign = :center) + return O +end +``` +The value for property `:line_width` is tracked internally from the node property `:weight`. The extremas of the value of the property `:weight` is scaled and clipped between sensible line widths. Finding these extremum points are where the node and edge attribute functions are used for. + +These limits get updated every time the utility function `updateGraph` gets called after changing the node properties. + +**Visualise the algorithm** + +To keep track of the neighboring node set, a `SortedDict` shall be used. To distinguish visited nodes, a `visited` data structure will be kept. +```julia +st=SortedDict([index=>first(w)[2] for (index, w) in enumerate(g)]) +visited=[false for i in 1:6] +``` + +The states of the nodes are represented as `white` -> unmarked, `yellow highlighted` -> current & `green` -> marked. Currently all nodes are colored white by default. + +```julia +# Make all edges translucent +changeEdgeProperty(wg, (node1, node2) -> true, :opacity, 0.5) +current=1 +dst=6 +while !isempty(st) + current=first(st) + if current[1]==dst + highlightNode(wg, current[1], :border_color, "red") + break + end + highlightNode(wg, current[1], :border_color, "yellow") + visited[current[1]]=true + delete!(st, current[1]) + changeNodeProperty!(wg, current[1], :fill_color, "green") + for j in g[current[1]][:neighbors] + if !visited[j[1]] + highlightEdge(wg, current[1], j[1], :color, "yellow") + st[j[1]]=min(st[j[1]], current[2]+j[2]) + changeNodeProperty!(wg, j[1], :weight, string(st[j[1]])) + end + end +end +``` + +**Weight update using `updateGraph`** + +Changing the node weight drawn on the canvas can also be done in a different way. It is not very expressive for this example, but when updates to node properties like `:weight` in the graph has to be made in bulk, using `updateGraph` is simpler. + +Additionally, for known drawing properties like `:line_width` for edges easing transitions are provided between the 2 different states. + +The changes to the above algorithm would be +```julia + st[j[1]]=min(st[j[1]], current[2]+j[2]) + g[j[1]][:weight]=st[j[1]] + end + updateGraph!(wg, g) + end +``` + +## Full Code + +```julia +using Javis + +function n(g, node, attr) + if attr==:weight && g[node][attr]==1000 + return "inf" + else + return g[node][attr] + end +end + +function e(g, node1, node2) + for j in g[node1][:neighbors] + if j[1]==node2 + return j[2] + end + end +end + +function drawEdge() + sethue(opts[:color]) + setline(opts[:line_width]) + line(opts[:position1], opts[:position2], :stroke) + # Now add the edge weight aligned to the edge line/curve + translate((opts[:position1]+opts[:position2])/2) + rotate(slope(opts[:position1], opts[:position2])) + translate(Point(0, 3)) + text(opts[:text], O, valign = :middle, halign = :center) + return O +end + +function drawNode(opts) + sethue(opts[:fill_color]) + circle(opts[:position], 5, :fill) + sethue(opts[:border_color]) + circle(opts[:position], 5, :stroke) + text(opts[:text], opts[:position], valign = :middle, halign = :center) + text(opts[:weight], opts[:position]+(0, 8), valign = :middle, halign = :center) +end + +g = [Dict(:weight=>0, :neighbors=>[(2, 3), (3, 5)]), + Dict(:weight=>1000, :neighbors=>[(4, 1), (5, 2)]), + Dict(:weight=>1000, :neighbors=>[(5, 1), (4, 2)]), + Dict(:weight=>1000, :neighbors=>[(6, 1)]), + Dict(:weight=>1000, :neighbors=>[(4, 3)]), + Dict(:weight=>1000, :neighbors=>[])] + +video =Video(300, 300) +Background(1:100, ground) + +wg = GraphAnimation(g, false, 300, 300, O; + node_attribute_fn=(g, node, attr) -> n(g, node, attr), + edge_attribute_fn=(g, node1, node2, attr) -> e(g, node1, node2)) + +nodes = [Object(@Frames(prev_start()+5, stop=100), GraphNode(i, drawNode; animate_on=:scale, property_style_map=Dict(:weight=>:weight), fill_color="white", border_color="black", text=string(i))) for i in range(1, 6; step=1)] + +edges=[] +for (index, node) in enumerate(g) + for j in node[:neighbors] + push!(edges, Object(@Frames(prev_start()+5, stop=100), GraphEdge(index, j[1], drawEdge; animate_on=:scale, property_style_map=Dict(:weight=>:line_width, :weight=>:text), color="black"))) + end +end + +st=SortedDict([index=>first(w)[2] for (index, w) in enumerate(g)]) +visited=[false for i in 1:6] + +changeEdgeProperty(wg, (node1, node2) -> true, :opacity, 0.5) +current=1 +dest=6 +while !isempty(st) + current=first(st) + if current[1]==dest + highlightNode(wg, current[1], :border_color, "red") + break + end + highlightNode(wg, current[1], :border_color, "yellow") + visited[current[1]]=true + delete!(st, current[1]) + changeNodeProperty!(wg, current[1], :fill_color, "green") + for j in g[current[1]][:neighbors] + if !visited[j[1]] + highlightEdge(wg, current[1], j[1], :color, "yellow") + st[j[1]]=min(st[j[1]], current[2]+j[2]) + g[j[1]][:weight]=st[j[1]] + end + updateGraph!(wg, g) + end +end + +render(video; pathname="example3.md") +``` diff --git a/src/structs/Graphs.jl b/src/structs/Graphs.jl new file mode 100644 index 000000000..776936e33 --- /dev/null +++ b/src/structs/Graphs.jl @@ -0,0 +1,98 @@ +""" + GraphAnimation + +Maintain information for the graph object to be drawn and animated on the canvas. +""" +struct GraphAnimation + reference_graph + width::Int + height::Int + mode::Symbol + layout::Symbol + start_pos::Union{Point,Object} + node_attribute_fn::Function + edge_attribute_fn::Function + adjacency_list::AbstractGraph + ordering::Vector{AbstractObject} + edge_weight_limits::Dict{Symbol,Tuple{Real,Real}} + node_weight_limits::Dict{Symbol,Tuple{Real,Real}} +end + +GraphAnimation(reference_graph, directed::bool) = + GraphAnimation(reference_graph, directed, 300, 300, O) +GraphAnimation(width::Int, height::Int, start_pos::Union{Point,Object}) = + GraphAnimation(LightGraphs.SimpleGraph(), false, width::Int, height::Int, start_pos) + +function GraphAnimation( + reference_graph, + directed::bool, + width::Int, + height::Int, + start_pos::Union{Point,Object}; + mode::Symbol = :static, + layout::Symbol = :spring, + node_attribute_fn::Function = (args...) -> nothing, + edge_attribute_fn::Function = (args...) -> nothing, +) + +end + +function _graph_animation_object(mode) + if mode == :static + # Invoke the graph layout generation algorithm here + # That is when the entire graph is already created + # After that change the start positions of nodes using the info from ordering list + end + for j in CURRENT_GRAPH[1].ordering + # Get object type from some object specific field like metadata + if j.metadata.type == :graph_node + for style in keys(get(j.metadata, weight_style_map, Dict())) + if style in keys(CURRENT_GRAPH[1].node_weight_limits) + # Update the limits for this style property on node + end + end + elseif j.metadata.type == :graph_node + # Do the same computation for edge styles + end + end + # Now update the node and edge object drawing parameters like scale, opacity, + # layout weights etc. +end + +struct GraphNode + node_id + properties_to_style_map::Dict{Any,Symbol} + draw_fn::Function +end + +# For nodes store drawing options as part of the Javis object itself +GraphNode(node_id, draw::Function) = GraphNode(node_id, draw; Dict{Symbol,Symbol}()) + +function GraphNode( + node_id, + draw::Function; + animate_on::Symbol = :opacity, + property_style_map::Dict{Symbol,Symbol}, + kwargs..., +) + # Register new node with Graph Animation metadata + # IF mode is static simply call the draw function and return + # ELSE recalculate the network layout based on the current graph structure + # and update the positions of nodes through easing translations +end + +struct GraphEdge + from_node + to_node + properties_to_style_map::Dict{Any,Symbol} + draw_fn::Function +end + +function GraphEdge( + node1, + node2, + draw::Function; + animate_on::Symbol = :opacity, + property_style_map::Dict{Symbol,Symbol}, + kwargs..., +) end