diff --git a/CHANGELOG.md b/CHANGELOG.md index 79aa2df..cc3655f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [v0.2.0] - forthcoming +## [v0.2.0] - 2022-05-09 ### Added diff --git a/Project.toml b/Project.toml index 6f4e0bb..44f6f4f 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Karnak" uuid = "cd156443-31ad-4f6f-850f-a93ee5f75905" authors = ["cormullion and contributors"] -version = "0.1.0" +version = "0.2.0" [deps] Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" diff --git a/README.md b/README.md index 64aa191..71ecf25 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,6 @@ There's a good selection of Julia packages for visualizing graphs: - [GraphMakie.jl](https://github.com/JuliaPlots/GraphMakie.jl): backend: Makie.jl -rather than this one. [docs-development-img]: https://img.shields.io/badge/docs-development-blue [docs-development-url]: http://cormullion.github.io/Karnak.jl/dev/ diff --git a/docs/src/assets/figures/graph-dependency-wallart.png b/docs/src/assets/figures/graph-dependency-wallart.png new file mode 100644 index 0000000..3ce2e44 Binary files /dev/null and b/docs/src/assets/figures/graph-dependency-wallart.png differ diff --git a/docs/src/assets/styles.css b/docs/src/assets/styles.css index 438fc3e..00d6e61 100644 --- a/docs/src/assets/styles.css +++ b/docs/src/assets/styles.css @@ -6,7 +6,7 @@ } pre, code { - font-family: JuliaMono !important; + font-family: JuliaMono-Light !important; font-feature-settings: "calt" 1; } @@ -16,7 +16,7 @@ p > a:after { } .schemename { - font-family: "JuliaMono"; + font-family: "JuliaMono-Light"; } .swatch { diff --git a/docs/src/basics.md b/docs/src/basics.md index 7de5cb1..7cffe2e 100644 --- a/docs/src/basics.md +++ b/docs/src/basics.md @@ -5,18 +5,20 @@ using Karnak, Luxor, Graphs, NetworkLayout, Colors, SimpleWeightedGraphs # Graph theory This section contains an introduction to basic graph theory -using the Graphs.jl package. You don't need any prior -knowledge of graphs, but you should be familiar with the basics -of programming in Julia. +using the +[Graphs.jl](https://github.com/JuliaGraphs/Graphs.jl) +package, illustrated with figures made with Karnak.jl. You +don't need any prior knowledge of graphs, but you should be +familiar with the basics of programming in Julia. !!! note All the figures in this manual are generated when the pages are built by Documenter.jl, and the code to draw them is included here. SVG is used because it's good for - line drawings, but you can use `drawgraph()` in any + line drawings, but you can use Karnak.jl in any Luxor environment, such as PNG - which is the - recommended format to use as the drawings get very + recommended format to use if the drawings get very complex, since large SVGs can tax web browsers. ## Graphs, vertices, and edges @@ -71,9 +73,11 @@ d # hide A typical graph consists of: -- vertices, which represent the things or entities, and +- _vertices_, which represent the things or entities, and -- edges, which describe how these things or entities connect and relate to each other +- _edges_, which describe how the things or entities connect and relate to each other + +Vertices are also called _nodes_ in the world of graph theory. The Graphs.jl package provides many ways to create graphs. We'll start off with this basic approach: @@ -97,9 +101,11 @@ We can easily add a number of new vertices: add_vertices!(g, 3) ``` -The graph has four now. We'll join pairs of vertices with an -edge. The four vertices we've made can be referred to with -`1`, `2`, `3`, and `4`: +The graph has four vertices now. We can refer to them +as `1`, `2`, `3`, and `4`. + +We'll join some pairs of vertices with an +edge: ```julia add_edge!(g, 1, 2) # join vertex 1 with vertex 2 @@ -108,7 +114,7 @@ add_edge!(g, 2, 3) add_edge!(g, 1, 4) ``` -(Vertices are always numberered from 1 to `n`.) +In Graphs.jl, vertices are always numbered from 1 to `n`. `g` is now a `{{4, 1} undirected simple Int64 graph}`. @@ -116,7 +122,6 @@ It's time to see some kind of visual representation of the graph we've made. ```@example graphsection -# packages to load: # using Karnak, Graphs, NetworkLayout, Colors g = Graph() @@ -133,7 +138,7 @@ add_edge!(g, 1, 4) end 600 300 ``` -This is just one of the many ways this graph can be represented visually. The coordinates of the vertices when drawn here are _not_ part of the graph's definition. The default styling uses the current Luxor color, with small circles marking the vertex positions. `drawgraph()` places the graphics for the graph on the current Luxor drawing. +This is just one of the many ways this graph can be represented visually. The locations of the vertices as drawn here are _not_ part of the graph's definition. The default styling uses the current Luxor color, with small circles marking the vertex positions. `drawgraph()` places the graphics for the graph on the current Luxor drawing. ## Undirected and directed graphs @@ -151,13 +156,13 @@ add_edge!(gd, 1, 4) # vertex 1 to vertex 4 add_edge!(gd, 4, 1) # vertex 4 to vertex 1 @drawsvg begin -background("grey10") -sethue("thistle1") -drawgraph(gd, vertexlabels = [1, 2, 3, 4]) + background("grey10") + sethue("thistle1") + drawgraph(gd, vertexlabels = [1, 2, 3, 4]) end 600 300 ``` -In this representation of our directed graph `gd`, we can now see the direction of the edges joining vertices. Vertices 1 and 4 are connected to each other. +In this representation of our directed graph `gd`, we can now see the direction of the edges joining vertices. ## Very simple graphs @@ -167,24 +172,24 @@ Creating graphs by typing the connections manually is tedious, so we can use fun g = Graph(10, 5) # 10 vertices, 5 edges d1 = @drawsvg begin -background("grey10") -sethue("gold") -drawgraph(g, vertexlabels = vertices(g)) + background("grey10") + sethue("gold") + drawgraph(g, vertexlabels = vertices(g)) end 400 300 gd = SimpleDiGraph(5, 3) # 5 vertices, 3, edges d2 = @drawsvg begin -background("grey10") -setline(0.5) -sethue("firebrick") -drawgraph(gd, vertexlabels = vertices(g)) + background("grey10") + setline(0.5) + sethue("firebrick") + drawgraph(gd, vertexlabels = vertices(g)) end 400 300 hcat(d1, d2) ``` -Neither of these graphs is **connected**. In a connected graph, every vertex is connected to every other via some path, a sequence of edges. +We can define how many vertices and edges the graph should have. An undirected graph with 10 vertices can have between 0 to 45 (`binomial(10, 2)`) edges, a directed graph up to 90. Neither of these graphs is **connected**. In a connected graph, every vertex is connected to every other via some path, a sequence of edges. ## Well-known graphs @@ -221,7 +226,7 @@ two groups. Each vertex in the first group is connected to one or more vertices in the second group. The next figure shows the **complete** version of a -bi-partite graph. So each vertex is connected to every other +bi-partite graph. Each vertex is connected to every other vertex in the other group. ```@example graphsection @@ -230,26 +235,29 @@ g = complete_bipartite_graph(N, N) H = 300 W = 550 @drawsvg begin -background("grey10") -pts = vcat( - between.(O + (-W/2, H/2), O + (W/2, H/2), range(0, 1, length=N)), - between.(O + (-W/2, -H/2), O + (W/2, -H/2), range(0, 1, length=N))) -sethue("aquamarine") -drawgraph(g, vertexlabels = 1:nv(g), layout = pts, edgestrokeweights=0.5) + background("grey10") + pts = vcat( + between.(O + (-W/2, H/2), O + (W/2, H/2), range(0, 1, length=N)), + between.(O + (-W/2, -H/2), O + (W/2, -H/2), range(0, 1, length=N))) + sethue("aquamarine") + drawgraph(g, vertexlabels = 1:nv(g), layout = pts, edgestrokeweights=0.5) end 600 400 ``` +We provided the required locations of the vertices on the +drawing to the `layout` keyword. + A **grid** graph doesn't need much explanation: ```@example graphsection M = 4 N = 5 -g = Graphs.grid([M, N]) # grid((m, n)) +g = Graphs.grid([M, N]) @drawsvg begin -background("grey10") -setline(0.5) -sethue("greenyellow") -drawgraph(g, vertexlabels = 1:nv(g), layout=stress) + background("grey10") + setline(0.5) + sethue("greenyellow") + drawgraph(g, vertexlabels = 1:nv(g), layout=stress) end 600 300 ``` @@ -273,6 +281,8 @@ g = wheel_graph(12) end 600 300 ``` +There are `star_digraph()` and `wheel_digraph()` DiGraph versions too. + ### Even more well-known graphs There are probably as many graphs as there are possible @@ -362,19 +372,21 @@ Julius Petersen, who first described it in 1898). ```@example graphsection @drawsvg begin -background("grey10") -pg = smallgraph(:petersen) -sethue("orange") -drawgraph(pg, vertexlabels = 1:nv(pg), layout = Shell(nlist=[6:10,])) + background("grey10") + pg = smallgraph(:petersen) + sethue("orange") + drawgraph(pg, vertexlabels = 1:nv(pg), layout = Shell(nlist=[6:10,])) end 600 300 ``` +Here's a cubical graph: + ```@example graphsection @drawsvg begin -background("grey10") -g = smallgraph(:cubical) -sethue("orange") -drawgraph(g, layout = Spring(Ptype=Float64)) + background("grey10") + g = smallgraph(:cubical) + sethue("orange") + drawgraph(g, layout = spring) end 600 300 ``` @@ -453,7 +465,7 @@ To add an edge, do: add_edge!(df, 1, 2) # from vertex 1 to 2 ``` -It's sometimes useful to be able to see these relationships between neighbors visually. +It's sometimes useful to be able to see these relationships between neighbors visually. This example looks for the neighbors of vertex 10: ```@example graphsection @drawsvg begin @@ -461,6 +473,7 @@ background("grey10") pg = smallgraph(:petersen) vertexofinterest = 10 + E = [] for (n, e) in enumerate(edges(pg)) if dst(e) == vertexofinterest || src(e) == vertexofinterest @@ -474,7 +487,8 @@ drawgraph(pg, vertexfillcolors = (v) -> ((v == vertexofinterest) || v ∈ neighbors(pg, vertexofinterest)) && colorant"rebeccapurple", vertexshapesizes = [v == vertexofinterest ? 20 : 10 for v in 1:nv(pg)], - edgestrokecolors = (e, f, t, s, d) -> (e ∈ E) ? colorant"firebrick" : colorant"thistle1" + edgestrokecolors = (e, f, t, s, d) -> (e ∈ E) ? + colorant"firebrick" : colorant"thistle1" ) end 600 300 ``` @@ -484,7 +498,7 @@ Other useful functions in Graphs.jl include `has_vertex(g, v)` and `has_edge(g, ### Degree The **degree** of a vertex is the number of edges that meet -at that vertex. This is shown in the figure by the vertex +at that vertex. This is shown in the figure below both in the vertex labels and also color-coded: ```@example graphsection @@ -506,7 +520,7 @@ end 600 300 ## Graphs as matrices -Graphs can be represented as matrices. In the world of graph theory, we'll meet the adjacency matrix and the incidence matrix (and there's an array called the adjacency list too). +Graphs can be represented as matrices - some say that graph theory is really the study of a particular set of matrices... We'll meet the adjacency matrix and the incidence matrix (and there's an array called the adjacency list too). ### Adjacency matrix @@ -550,10 +564,10 @@ m = [0 1 1 0 0; 0 0 1 1 0] @drawsvg begin -background("grey10") -hg = Graph(m) -sethue("palegreen") -drawgraph(hg, vertexlabels=1:nv(hg), layout=stress) + background("grey10") + hg = Graph(m) + sethue("palegreen") + drawgraph(hg, vertexlabels=1:nv(hg), layout=stress) end 800 400 ``` @@ -610,7 +624,8 @@ drawgraph(Graph(g), vertexshapesizes = 40, edgestrokeweights = 15, edgestrokecolors = colorant"gold", - vertexfillcolors = [colorant"#CB3C33", colorant"#389826", colorant"#9558B2"]) + vertexfillcolors = [colorant"#CB3C33", + colorant"#389826", colorant"#9558B2"]) end 600 250 ``` @@ -647,7 +662,7 @@ For example, this adjacency list: defines a graph with 20 vertices, such that vertex 1 has edges joining it to vertices 2, 5, and 7, and so on for each element of the whole array. -The `Graph()` function accepts an adjacency list, along with the number of edges. +The `Graph()` function accepts an adjacency list, preceded by the number of edges. ```@example graphsection g = Graph(30, [ @@ -673,9 +688,9 @@ g = Graph(30, [ [6, 16, 19]]) @drawsvg begin -background("grey10") -sethue("orange") -drawgraph(g, layout=stress) + background("grey10") + sethue("orange") + drawgraph(g, layout=stress) end 600 300 ``` @@ -690,7 +705,7 @@ Here, `fadjlist` is a forward adjacency list which defines how each vertex conne ## Paths, cycles, routes, and traversals -Graphs help us answer questions about connectivity and relationships. For example, thinking of a railway network as a graph, with the stations as vertices, and the tracks as edges, we want to ask questions such as "Can we get from A to B by train?", which therefore becomes the question "Are there sufficient edges between two vertices such that we can find a continuous path that joins them?". +Graphs help us answer questions about connectivity and relationships. For example, think of a railway network as a graph, with the stations as vertices, and the tracks as edges. We want to ask questions such as "Can we get from A to B by train?", which therefore becomes the question "Are there sufficient edges between vertices in the graph such that we can find a continuous path that joins them?". Graphs.jl has many features for traversing graphs and finding paths. We can look at just a few of them here. @@ -704,9 +719,9 @@ Graphs.jl has many features for traversing graphs and finding paths. We can look ### Paths and cycles -A path is a sequence of edges between some start vertex and some end vertex, such that a continuous unbroken route is available. +A **path** is a sequence of edges between some start vertex and some end vertex, such that a continuous unbroken route is available. -A cycle is a path where the start and end vertices are the same - a closed path. These are also called **circuits** in some sources. +A **cycle** is a path where the start and end vertices are the same - a closed path. These are also called circuits in some sources. The `cycle_basis()` function finds all the cycles in a graph (at least, it finds a **basis** of an undirected graph, which is a minimal collection of cycles that can be added to make all the cycles). The result is an array of arrays of vertex numbers. @@ -759,6 +774,44 @@ julia> cycles = cycle_basis(pg) end 600 300 ``` +For digraphs, you can use `simplecycles()`, to find every cycle. + +This example shows every cycle of a complete digraph `{4, 12}`. + +```@example graphsection + +sdg = complete_digraph(4) + +cycles = simplecycles(sdg) + +@drawsvg begin + background("grey10") + sethue("orange") + tiles = Tiler(600, 600, 4, 4) + for (pos, n) in tiles + cycle = cycles[n] + cycle_path = [Edge(cycle[i], cycle[i + 1]) for i in 1:length(cycle)-1] + @layer begin + translate(pos) + tilebox = BoundingBox(box(O, tiles.tilewidth, tiles.tileheight)) + text(string(cycle), halign=:center, boxbottomcenter(tilebox)) + sethue(HSV(rand(0:360), 0.6, 0.9)) + drawgraph(sdg, layout=squaregrid, + boundingbox = tilebox, + edgelist = cycle_path, + edgestrokeweights = 3, + vertexshapes = :none, + edgelines = (edgenumber, edgesrc, edgedest, from, to) -> + begin + line(from, to, :stroke) + end) + end + end +end 600 600 +``` + +There can be a lot of cycles in a graph. For example, a `complete_digraph(10)` has 1,110,073 cycles. Graphs.jl has tools for working with cycles efficiently. + ## Shortest paths: the A* algorithm One way to find the shortest path between two vertices is to use the `a_star()` function, and provide the graph, the start vertex, and the end vertex. The function returns a list of edges. @@ -812,29 +865,29 @@ end # find a route astar = a_star(g, 1, W * H) -sethue("grey30") +sethue("grey60") +setlinecap("square") drawgraph(g, - vertexshapesizes = 6, + vertexshapesizes = 0, layout=squaregrid, - edgegaps=2, edgestrokeweights = 12) -sethue("orange") +sethue("red") drawgraph(g, vertexshapes = :none, layout=squaregrid, edgelist=astar, edgegaps=0, - edgestrokeweights=2) + edgestrokeweights=5) end 600 600 ``` ## Visiting every vertex -Another feature of a graph that's useful to know: how to visit all vertices in a network just once. +Another feature of a graph that's useful to know: how to visit all vertices just once. -To do this, find a cycle that's the same length as the graph. However, there might be a lot of possibilities, since there could be many such cycles. Here's a way of finding a cycle that visits every vertex. `simplecycles()` finds all of them (there are 120 for this graph), so only the first one is used. +To do this, find a cycle that's the same length as the graph. However, there might be a lot of possibilities, since there could be many such cycles. Here's a way of finding a cycle that visits every vertex. `simplecycles()` finds all of them (there are 120 for this graph), so only the first one with the right length is used. ``` @example graphsection @drawsvg begin @@ -867,7 +920,7 @@ A well-known algorithm for finding the shortest path between graph vertices is n > I was just thinking about whether I could do this, and I > then designed the algorithm for the shortest path. -In Graphs.jl, this algorithm is available with `dijkstra_shortest_paths()`. After running this function, the result is an object with various pieces of information about all the shortest paths: this is a `DijkstraState` object, with fields containing things like distances and predecessor vertices. There's an `enumerate_paths()` function which can extract the vertex information for a specific path from the DijkstraState object. +In Graphs.jl, this algorithm is available with `dijkstra_shortest_paths()`. After running this function, the result is an object with various pieces of information about all the shortest paths: this is a `DijkstraState` object, with fields `parents`, `dists`, `predecessors`, `pathcounts`, `closest_vertices`. There's an `enumerate_paths()` function which can extract the vertex information for a specific path from the DijkstraState object. The following code animates the results of examining a grid graph using Dijkstra's algorithm. The shortest paths between the first vertex and every other vertex are drawn in a series of frames, one by one. @@ -1151,13 +1204,28 @@ In the following example, only three colors are needed such that no edge connect ```@example graphsection @drawsvg begin -background("grey10") -g = smallgraph(:octahedral) -gc = greedy_color(g) -dcolors = distinguishable_colors(gc.num_colors) -sethue("gold") -drawgraph(g, layout=stress, - vertexfillcolors = dcolors[gc.colors], - vertexshapesizes = 30) + background("grey10") + g = smallgraph(:octahedral) + gc = greedy_color(g) + dcolors = distinguishable_colors(gc.num_colors) + sethue("gold") + drawgraph(g, layout=stress, + vertexfillcolors = dcolors[gc.colors], + vertexshapesizes = 30) end 800 400 ``` + +whereas a complete graph might require many colors because there are so many connected vertices: + +```@example graphsection +@drawsvg begin + background("grey10") + g = complete_graph(20) + gc = greedy_color(g) + dcolors = distinguishable_colors(gc.num_colors) + sethue("grey50") + drawgraph(g, layout=stress, + vertexfillcolors = dcolors[gc.colors], + vertexshapesizes = 20) +end 600 300 +``` diff --git a/docs/src/examples.md b/docs/src/examples.md index 90a6521..9aec043 100644 --- a/docs/src/examples.md +++ b/docs/src/examples.md @@ -24,10 +24,12 @@ find(x::Int64) = stations[x] # Examples +This section contains a few examples showing how to use `drawgraph()` to visualize a few graphs. + ## The London Tube One real-world example of a small network is the London -Underground, known as "the Tube". The 250 or so stations in +Underground, known as “the Tube”. The 250 or so stations in the network can be modelled using a simple graph. ### Setup @@ -51,6 +53,7 @@ extrema_lat = extrema(tubedata.Latitude) extrema_long = extrema(tubedata.Longitude) # scale LatLong and flip in y to fit into current Luxor drawing + positions = @. Point( rescale(tubedata.Longitude, extrema_long..., -280, 280), rescale(tubedata.Latitude, extrema_lat..., 280, -280)) @@ -63,7 +66,7 @@ find(x::Int64) = stations[x] g = Graph(amatrix) ``` -The tube "map" is stored in `g`, as a `{267, 308} undirected simple Int64 graph`. +The tube “map” is stored in `g`, as a `{267, 308} undirected simple Int64 graph`. The `find()` functions are just a quick way to convert between station names and ID numbers: @@ -81,7 +84,7 @@ Most London residents and visitors are used to seeing the famous [Tube Map](http ![tube map](assets/figures/tubemap.png) -It's a design classic, hand-drawn by Harry Beck in 1931, and updated regularly +It’s a design classic, hand-drawn by Harry Beck in 1931, and updated regularly ever since. As an electrical engineer, Beck represented the sprawling London track network as a tidy circuit board. For Beck, the important thing about the map was to show the connections, rather than the accurate geography. @@ -238,8 +241,8 @@ Information about the required changes - at Victoria from the Piccadilly line to the Victoria Line, and at Warren Street from the Victoria Line to the Northern Line - is not part of the graph. Routes across the Tube network, like the -trains, follow the tracks (edges). The concept of "lines" -(Victoria, Circle, etc) isn't part of the graph structure, +trains, follow the tracks (edges). The concept of “lines” +(Victoria, Circle, etc) isn’t part of the graph structure, but a colorful layer imposed on top of the track network. ### Pandemic @@ -292,12 +295,12 @@ main() ## The JuliaGraphs logo -The logo for the JuliaGraphs package was easily drawn using +The ccurrent logo for the Graphs.jl package was easily drawn using Karnak. I wanted to use the graph coloring feature -(`greedy_color()`), but unfortunately it was too clever, -managing to color the graph using only two colors, instead +(`greedy_color()`), but unfortunately it was __too__ clever, +managing to color the graph using only two colors instead of the four I was hoping to use. ```@example @@ -368,10 +371,12 @@ end This example was originally developed by [Mathieu Besançon](https://github.com/matbesancon/lightgraphs_workshop) and presented as part of the workshop: __Analyzing Graphs -at Scale__, presented at JuliaCon 2020. You can watch the +at Scale__, at JuliaCon 2020. You can watch the video on [YouTube](https://youtu.be/K3z0kUOBy2Y). -The most important change is the renaming of LightGraphs.jl to Graphs.jl. Also, the way to access the list of packages might have changed between Julia v1.6 and v1.7. +The most important change is the renaming of LightGraphs.jl +to Graphs.jl. Also, the way to access the list of packages +might have changed between Julia v1.6 and v1.7. The code builds a dependency graph of the connections (ie which package depends on which package) for Julia packages @@ -399,7 +404,7 @@ using Colors ### Finding the general registry -On my computer, the registry is in its default location. You might need to modify the first line if yours is is another location: +On my computer, the registry is in its default location. You might need to modify these lines if yours is is another location: ```julia path_to_general = expanduser("~/.julia/registries/General") @@ -407,7 +412,7 @@ registry_file = Pkg.TOML.parsefile(joinpath(path_to_general, "Registry.toml")) packages_info = registry_file["packages"]; ``` -All we need is the name and path: +First we need the name and location of every package: ```julia # Julia v1.6? @@ -423,7 +428,7 @@ pkg_paths = map(values(Pkg.Registry.reachable_registries()[1].pkgs)) do d end ``` -The result in `pkg_paths` is a vector of tuples, containing the name and path of every package: +The result in `pkg_paths` is a vector of tuples, containing the name and location on disk of every package: ```julia 7495-element Vector{NamedTuple{(:name, :path), Tuple{String, String}}}: @@ -435,7 +440,7 @@ The result in `pkg_paths` is a vector of tuples, containing the name and path of ### Find packages that depend on a specific package -The function `find_direct_deps()` finds all the packages that directly depend on a specific named package. +The function `find_direct_deps()` finds all the packages (names and locations) that directly depend on a specific named package. ```julia function find_direct_deps(registry_path, pkg_paths, source) @@ -480,7 +485,7 @@ Colors.jl has 227 packages that depend on it. When Mathieu ran this code in 2020 ### Build a directed tree -The next function, `build_tree()`, will build a directed graph, to find out which packages depend on Colors.jl? Starting at the root package, which is the package we're interested in, the loop finds all its dependencies, then finds the dependencies of all of those dependent package, and continues doing this until it reaches packages that have no dependencies - the leaves at the tip of the tree's branches. +The next function, `build_tree()`, will build a directed graph of the dependencies on Colors.jl. Starting at the root package (Colors) the loop finds all its dependencies, then finds the dependencies of all of those dependent packages, and continues doing this until it reaches packages that have no dependencies. These are the "leaves" at the tip of the tree's branches. ```julia function build_tree(registry_path, pkg_paths, root) @@ -519,7 +524,7 @@ g = build_tree(path_to_general, pkg_paths, "Colors") {1375, 1374} directed Int64 metagraph with Float64 weights defined by :weight (default weight 1.0) ``` -Notice that there are 1375 nodes, and one less edge. The Colors.jl package is the root of the tree, and doesn't connect to anything. +Notice that there are 1375 nodes, but one less edge. The Colors.jl package is the root of the tree, and doesn't connect to anything else, in this analysis.) Of course, it depends on quite a few, but that's another graph story.) The result is a _directed metagraph_. In a metagraph, as implemented by MetaGraphs.jl, it's possible to add information to vertices using `set_prop()` and `get_prop()`. @@ -553,9 +558,9 @@ get_prop.(Ref(g), outneighbors(g, 1), :name) The `dijkstra_shortest_paths()` function finds the paths between the designated package and all its dependencies. -The returned value is a DijkstraState object, with fields `parents`, `dists`, `predecessors`, `pathcounts`, and `closest_vertices` +The returned value is a DijkstraState object, with fields `parents`, `dists`, `predecessors`, `pathcounts`, and `closest_vertices`. -Looking at the `dists` (distances), we see that one package is very close indeed at 0.0 - that's Colors.jl itself! +Looking at the `dists` (distances), we see that one package is very close indeed at 0.0 - that's Colors.jl itself. ```julia spath_result = dijkstra_shortest_paths(g, 1) @@ -644,7 +649,7 @@ full_graph = MetaDiGraph(length(all_packages)) {1375, 0} directed Int64 metagraph with Float64 weights defined by :weight (default weight 1.0) ``` -Assign names to the vertices: +Assigning names to the vertices: ```julia for v in vertices(full_graph) @@ -670,7 +675,7 @@ end ### Pagerank -This code computes the pagerank of the graph. It returns a long list of numbers, the centrality score for each vertex. +This code computes the _pagerank_ of the graph. It returns a long list of numbers, the centrality score for each vertex. ```julia ranks = pagerank(full_graph) @@ -733,6 +738,8 @@ get_prop.(Ref(full_graph), sorted_indices, :name) ### Most dependencies, most depended on +`indegree()` returns the number of edges which end at a vertex. For a package, this is another way of seeing how many other packages depend on it. + ```julia in_sorted_indices = sort(vertices(full_graph), by = i -> indegree(full_graph, i), rev = true) @@ -770,6 +777,8 @@ get_prop.(Ref(full_graph), in_sorted_indices, :name) "ElectronTests" ``` +`outdegree()` finds the number of edges which start at a vertex. + ```julia out_sorted_indices = sort(vertices(full_graph), by = i -> outdegree(full_graph, i), rev=true) @@ -862,7 +871,9 @@ get_prop.(Ref(full_graph), sorted_indices_betweenness, :name) "Colors" ``` -### Dependencies are cyclic? +### Is_cyclic + +`is_cyclic()` returns true if the graph contains a cycle. ```julia is_cyclic(full_graph) @@ -884,12 +895,19 @@ end ["Modia3D", "Modia"] ["RasterDataSources", "GeoData"] ["DSGE", "StateSpaceRoutines"] - ``` +For that first cycle: ImageCore.jl's Project.toml file has MosaicViews.jl in its `[deps]` section, and MosaicViews.jl has ImageCore.jl in the `[extras]` section of its Project.toml file. + ### Draw some graphs -Visualizations of graphs are sometimes (often?) better at communicating vague ideas such as complexity and shape: it's quite difficult to render a graph as rich as these to show the connections clearly while also showing all the labels such that they're easy to read. The solution may be to print out these graph representations and place them on a nearby wall. +Visualizations of graphs are sometimes (often?) better at communicating vague ideas such as complexity and shape. But it's quite difficult to render graphs as rich as these to show the connections clearly while also showing all the labels such that they're easy to read. + +The solution may be to print out these graph representations and place them on a nearby wall, although, with Julia's General Registry changing every day, it would be out of date before it was installed. + +![wall art office graph dependency](assets/figures/graph-dependency-wallart.png) + +The images above were made with the following code. ```julia @pdf begin @@ -923,8 +941,6 @@ Visualizations of graphs are sometimes (often?) better at communicating vague id end 2500 2500 "/tmp/graph-dependencies-colors.pdf" ``` -![package dependencies for Colors](assets/figures/graph-dependencies-colors.svg) - ```julia using ColorSchemes @@ -952,5 +968,3 @@ using ColorSchemes ) end 1200 1200 "/tmp/graph-dependencies-2.svg" ``` - -![package dependencies for Colors](assets/figures/graph-dependencies-colors-2.svg) diff --git a/docs/src/syntax.md b/docs/src/syntax.md index 58b1da2..929f4dc 100644 --- a/docs/src/syntax.md +++ b/docs/src/syntax.md @@ -14,9 +14,9 @@ The default display for graphs is: - current Luxor origin, scale and rotation -- current Luxor color +- current Luxor color for edges -- circles mark all vertices +- circles for all vertex shapes - no vertex labels @@ -24,17 +24,19 @@ The default display for graphs is: ```@example graphsection @drawsvg begin -background("grey10") -sethue("darkcyan") -g = complete_graph(10) -drawgraph(g) + background("grey10") + sethue("darkcyan") + g = complete_graph(10) + drawgraph(g) end 600 300 ``` To control the appearance of the graph, you supply values to -the keyword arguments. Apart from the general keywords `BoundingBox`, `layout`, `margin`, the keywords can be grouped into two categories: +the various keyword arguments. Apart from the general +keywords `BoundingBox`, `layout`, and `margin`, the keywords can +be grouped into two categories: -![](assets/figures/drawgraphkeywords.svg) +![two groups of keyword](assets/figures/drawgraphkeywords.svg) Most of these keyword arguments accept vectors, ranges, and scalar values, and some accept @@ -87,9 +89,10 @@ graph in layers. Remember to use the same layout algorithm. ## The BoundingBox The graphics for the graph are placed to fit inside the -current BoundingBox (ie the drawing), after allowing for the -margin (the default is 30). Pass a different -BoundingBox to the `boundingbox` keyword argument. +current BoundingBox (by default, the drawing), after +allowing for the margin (the default is 30). Pass a +different BoundingBox to the `boundingbox` keyword argument +to control the graph layout's size. ## Layout algorithms @@ -99,7 +102,19 @@ which is where you should look for information about the various algorithms that determine where vertices are positioned. -Here are some formulations which work: +You can choose a layout algorithm, or supply the vertex positions yourself. + +The main layout algorithms available are: + +- shell + +- spring + +- stress + +- squaregrid + +Here are some formulations which work as keywords in `drawgraph()`: ```julia layout = squaregrid @@ -121,15 +136,14 @@ layout = Shell(nlist=[6:10,]) layout = Stress(iterations = 100, weights = M) # M is matrix of weights layout = Spring(iterations = 200, initialtemp = 2.5) - ``` -Alternatively, pass a vector of points to the `layout` -keyword argument. Vertices will be placed on these points -(vertex 1 on point 1, etc...), rather than at points -suggested by the NetworkLayout functions. - -For example, in this next drawing, the two sets of points for a bipartite graph are generated beforehand. +Alternatively, you can pass a vector of points to the +`layout` keyword argument. Vertices will be placed on these +points (vertex 1 on point 1, etc...), rather than at points +suggested by the NetworkLayout algorithms. For example, in +this next drawing, the two sets of points for a bipartite +graph are generated beforehand. ```@example graphsection @drawsvg begin @@ -146,9 +160,11 @@ drawgraph(g, vertexlabels = 1:nv(g), layout = pts, end 600 300 ``` -The coordinates of the positions are returned by the `drawgraph()` function. +The calculated positions are returned by the `drawgraph()` function. + +Some of the layout algorithms allow you to poss _initial_ positions that are used by the algorithms as starting points. These can be supplied as xy pairs, rather than Luxor Points (which NetworkLayout won't accept). -Some of the layout algorithms allow you to poss _initial_ positions, not vertex locations. These can be supplied as xy pairs, rather than Luxor Points (which NetworkLayout won't accept). So, for example, the next example shows how the Stress algorithm refines the vertex positions on each iteration, after starting at each "grid location". +Here's a figure showing how the Stress algorithm refines the vertex positions on each iteration, after starting at each "grid location". ```@example graphsection G = smallgraph(:petersen) @@ -158,7 +174,7 @@ G = smallgraph(:petersen) initialpositions = [(pt.x, pt.y) for (pt, n) in Tiler(800, 800, 3, 3)] sethue("grey80") - circle.(Point.(initialpositions), 10, :stroke) + circle.(Point.(initialpositions), 6, :stroke) for i in 1:60 drawgraph(G, @@ -181,7 +197,7 @@ end ## The `vertexfunction` and `edgefunction` arguments -The two keyword arguments `vertexfunction` and `edgefunction` allow you to pass control over the drawing process completely to two functions, which can be anonymous functions. +The two keyword arguments `vertexfunction` and `edgefunction` allow you to pass control over the drawing process completely to these two functions, ignoring all the other keywords. ``` vertexfunction = my_vertexfunction(vertex, coordinates) @@ -225,9 +241,9 @@ end 800 600 ## Vertex labels and shapes -### The `vertexlabels` argument +### `vertexlabels` -Use `vertexlabels` to choose the text to associate with each vertex. Supply a range, array of strings or numbers, a string, or a function. +Use `vertexlabels` to choose the text to associate with each vertex. Supply a range, array of strings or numbers, a single string, or a function. This example draws all vertices, and numbers them from 1 to 6. @@ -237,13 +253,12 @@ This example draws all vertices, and numbers them from 1 to 6. ```@example graphsection @drawsvg begin -background("grey10") -g = smallgraph(:octahedral) -sethue("gold") -drawgraph(g, layout=stress, - vertexlabels = 1:nv(g), - vertexshapesizes = 10 - ) + background("grey10") + g = smallgraph(:octahedral) + sethue("gold") + drawgraph(g, layout=stress, + vertexlabels = 1:nv(g), + vertexshapesizes = 10) end 600 300 ``` @@ -435,21 +450,21 @@ drawgraph(g, end 600 600 ``` -Try playing with the following keyword arguments: +The following keyword arguments operate in a similar way: -- `vertexstrokeweights` +- `vertexstrokeweights` : Array | Range | :none -- `vertexlabeltextcolors` +- `vertexlabeltextcolors` : Array | Range | colorant -- `vertexlabelfontsizes` +- `vertexlabelfontsizes` : Array | Range | number -- `vertexlabelfontfaces` +- `vertexlabelfontfaces` : Array | string -- `vertexlabelrotations` +- `vertexlabelrotations` : Array | Range | number -- `vertexlabeloffsetangles` +- `vertexlabeloffsetangles` : Array | Range | number -- `vertexlabeloffsetdistances` +- `vertexlabeloffsetdistances` : Array | Range | number It's possible to specify the font faces for vertex labels, but it's difficult to demonstrate when the documentation is built on machines in the cloud with unknown typographical resources. But anyway: @@ -468,77 +483,9 @@ end 600 300 ## Edge options -### `edgelist` and `edgelines` - -A `Graphs.EdgeIterator` supplied to `edgelist` makes only those edges available for drawing. Otherwise, control which edges are to be drawn by supplying numbers (or a function) to `edgelines`. - -```@example graphsection -@drawsvg begin -background("grey10") -sethue("orange") -g = grid((15, 15)) -drawgraph(g, - layout = stress, - vertexshapes = :none, - edgelines = rand(1:ne(g), 30) -) -end 600 300 -``` - -`edgelist` is useful for drawing paths - a sequence of edges. - -```@example graphsection -@drawsvg begin -background("grey10") -g = grid((15, 15)) - -astar = a_star(g, 1, 15*15 - 1) - -sethue("orange") -drawgraph(g, - layout = stress, - vertexshapes = :none) - -sethue("cyan") -drawgraph(g, - layout = stress, - vertexshapes = :none, - edgestrokeweights = 5, - edgelist = astar) -end 600 300 -``` - -For more interesting arrows for edges, Luxor's arrows are available: - -```@example graphsection -@drawsvg begin -background("grey10") -g = star_digraph(12) -fontsize(20) -sethue("slateblue") -drawgraph(g, - layout=spring, - vertexshapes = 0, - edgestrokecolors = distinguishable_colors(ne(g)), - edgelines = (k, s, d, f, t) -> - arrow(f, between(f, t, .95), [10, -5], - linewidth=10, - arrowheadlength=40, - arrowheadangle=π/7, - decorate = () -> begin - sethue("purple") - circle(O, 15, :fill) - sethue("white") - text(string(k), angle = -getrotation(), halign=:center, valign=:middle) - end, - decoration=0.65) - ) -end 600 400 -``` - ### `edgefunction` -As with `vertexfunction`, the `edgefunction` keyword argument allows you to do anything you like when the edges are drawn, and overrides all other `edge-` keyword arguments. Here, the calculated coordinates of the graph and a path between two vertices are extracted into vectors for later Luxor-ious treatment. +As with `vertexfunction`, the `edgefunction` keyword argument allows you to do anything you like when the edges are drawn, and overrides all other `edge-` keyword arguments. Here, the calculated coordinates of the graph and a path between two vertices aren't drawn at first, just extracted into vectors for further processing. ```@example graphsection @drawsvg begin @@ -589,6 +536,75 @@ end 600 400 This keyword overrides the other `edge-` keywords. +### `edgelist` and `edgelines` + +A `Graphs.EdgeIterator` supplied to `edgelist` makes only the specified edges available for drawing. Otherwise, control which edges are to be drawn by supplying numbers (or a function) to `edgelines`. + +```@example graphsection +@drawsvg begin +background("grey10") +sethue("orange") +g = grid((15, 15)) +drawgraph(g, + layout = stress, + vertexshapes = :none, + edgelines = rand(1:ne(g), 30) +) +end 600 300 +``` + +`edgelist` is useful for drawing paths - a sequence of edges. For example, if you use `a_star()` to find the shortest path between two vertices, you can draw the edges with this keyword. It's useful to draw the graph twice, once with all edges, once with selected edges. + +```@example graphsection +@drawsvg begin +background("grey10") +g = grid((15, 15)) + +astar = a_star(g, 1, nv(g)) + +sethue("orange") +drawgraph(g, + layout = stress, + vertexshapes = :none) + +sethue("cyan") +drawgraph(g, + layout = stress, + vertexshapes = :none, + edgestrokeweights = 5, + edgelist = astar) +end 600 300 +``` + +For more interesting arrows for edges, Luxor's arrows are available: + +```@example graphsection +@drawsvg begin +background("grey10") +g = star_graph(12) +fontsize(20) +sethue("slateblue") +drawgraph(g, + layout=spring, + vertexshapes = 0, + vertexlabels = 1:nv(g), + vertexlabelfontsizes = 12, + edgestrokecolors = distinguishable_colors(ne(g)), + edgelines = (k, s, d, f, t) -> + arrow(f, between(f, t, .95), [20, -45], + linewidth = 5, + arrowheadlength = 15, + arrowheadangle = π/7, + decorate = () -> begin + sethue("purple") + circle(O, 15, :fill) + sethue("white") + text(string(k), angle = -getrotation(), halign = :center, valign=:middle) + end, + decoration = .7)) +end 600 400 +``` + ### Edge labels Use `edgelabels`, `edgelabelcolors`, `edgelabelrotations`, etc. to control the appearance of the labels alongside edges. @@ -634,7 +650,7 @@ end end 600 350 ``` -The `edgelabels` keyword argument can also accept a function with five arguments: `edge number`, `source`, `destination`, `from` point, and `to` point, and is able to annotate each edge with its graphical length in this particular layout. +The `edgelabels` keyword argument can also accept a function with five arguments: `edgenumber`, `source`, `destination`, `from` and `to`. In this example, the graphical distances between the two vertex positions provide the annotations for each edge. ```@example graphsection @drawsvg begin @@ -691,31 +707,35 @@ end 600 300 ### `edgelist` -This example draws the graph more than once; once with all the edges, and once with only the edges in `edgelist`, where `edgelist` is the path from vertex 15 to vertex 17, drawn in a pale translucent yellow. The path is marked with X marks the spot cyan-colored shapes. +This example draws the graph more than once; once with all the edges, once with only the edges in `edgelist`, where `edgelist` is the path from vertex 15 to vertex 17, drawn in a pale translucent yellow, and once to draw the vertices on the path "X marks the spot" cyan-colored crosses. ```@example graphsection @drawsvg begin background("grey10") g = smallgraph(:karate) sethue("slateblue") + drawgraph(g, layout = stress, - vertexlabels = 1:nv(g), - vertexshapes = :circle, - vertexshapesizes = 10, - vertexlabelfontsizes = 10) + vertexlabels = 1:nv(g), + vertexshapes = :circle, + vertexshapesizes = 10, + vertexlabelfontsizes = 10) + astar = a_star(g, 15, 17) + drawgraph(g, - layout=stress, - vertexshapes = :none, - edgelist = astar, - edgestrokecolors=RGBA(1, 1, 0, 0.5), - edgestrokeweights=10) + layout=stress, + vertexshapes = :none, + edgelist = astar, + edgestrokecolors=RGBA(1, 1, 0, 0.5), + edgestrokeweights=10) + drawgraph(g, - layout=stress, - edgelines=0, - vertexshapes = (v) -> v ∈ src.(astar) && polycross(O, 20, 4, 0.5, π/4, :fill), - vertexfillcolors = (v) -> v ∈ src.(astar) && colorant"cyan" - ) + layout=stress, + edgelines=0, + vertexshapes = (v) -> v ∈ src.(astar) && polycross(O, 20, 4, 0.5, π/4, :fill), + vertexfillcolors = (v) -> v ∈ src.(astar) && colorant"cyan" + ) end 600 600 ``` diff --git a/src/drawgraph.jl b/src/drawgraph.jl index f661d52..f5025a0 100644 --- a/src/drawgraph.jl +++ b/src/drawgraph.jl @@ -877,7 +877,7 @@ vertexfunction = (v, c) -> ngon(c[v], 30, 6, 0, :fill) `edgefunction(edgenumber, edgesrc, edgedest, from, to)` -> A function `edgefunction(edgenumber, from, to, edgesrc, edgedest)` that -completely specifies the appearance of every vertex. None +completely specifies the appearance of every edge. None of the other edge- keyword arguments are used. ## Vertex options @@ -889,9 +889,6 @@ the colors for vertex The text labels for each vertex. Vertex labels are not drawn by default. -`vertexstrokecolors(vertex)` -> -`vertexstrokecolors(vertex)` -> - `vertexshapes` : Array | Range | :circle | :square | :none | Function (vtx) -> Use shape for vertex. If function, `vtx` is vertex number, using current vertex rotation