diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fde7a3..7297970 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [v0.6.0] - forthcoming +## [v1.0.0] - 2023-10-06 12:23:14 ### Added diff --git a/Project.toml b/Project.toml index d4bab64..e0bf2ce 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.6.0" +version = "1.0.0" [deps] Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" @@ -15,7 +15,7 @@ SimpleWeightedGraphs = "47aef6b3-ad0c-573a-a1e2-d07658019622" [compat] Colors = "0.10, 0.11, 0.12" DelimitedFiles = "1" -Graphs = "1" +Graphs = "1.7, 1.8, 1.9" Luxor = "3" NetworkLayout = ">=0.4.4" Reexport = "0.2, 1.0, 1.1, 1.2" diff --git a/docs/Manifest.toml b/docs/Manifest.toml index f75436c..a220359 100644 --- a/docs/Manifest.toml +++ b/docs/Manifest.toml @@ -2,7 +2,7 @@ julia_version = "1.9.2" manifest_format = "2.0" -project_hash = "c43ef599cc680a647fc8c44dedd4464108dad1bc" +project_hash = "ef3c9186cf034673db73cb73bbf83b59f51ae062" [[deps.ANSIColoredPrinters]] git-tree-sha1 = "574baf8110975760d391c710b6341da1afa48d8c" diff --git a/docs/Project.toml b/docs/Project.toml index ac4f362..1b4ab19 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -6,17 +6,15 @@ DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" Karnak = "cd156443-31ad-4f6f-850f-a93ee5f75905" -Luxor = "ae8d54c2-7ccd-5906-9d76-62fc9837b5bc" NetworkLayout = "46757867-2c16-5918-afeb-47bfcb05e46a" SimpleWeightedGraphs = "47aef6b3-ad0c-573a-a1e2-d07658019622" [compat] CSV = "0.10" Colors = "0.10, 0.11, 0.12" -DataFrames = "1.4" +DataFrames = "1.4, 1.5, 1.6" Documenter = "1" -Graphs = "1.7" -Luxor = "3" +Graphs = "1.7, 1.8, 1.9" NetworkLayout = ">=0.4.4" SimpleWeightedGraphs = "1.2" julia = "1.6" diff --git a/docs/src/basics.md b/docs/src/basics.md index 9135700..cd28d08 100644 --- a/docs/src/basics.md +++ b/docs/src/basics.md @@ -1,5 +1,5 @@ ```@setup graphsection -using Karnak, Luxor, Graphs, NetworkLayout, Colors, SimpleWeightedGraphs +using Karnak, Graphs, NetworkLayout, Colors, SimpleWeightedGraphs ``` # Graph theory @@ -15,11 +15,8 @@ familiar with the basics of programming in Julia. 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 Karnak.jl in any - Luxor environment, such as PNG - which is the - recommended format to use if the drawings get very - complex, since large SVGs can tax web browsers. + them is included here. To run the examples, you'll need the packages + `Karnak`, `Graphs`, `NetworkLayout`, `Colors`, and possibly `SimpleWeightedGraphs`. ## Graphs, vertices, and edges @@ -33,7 +30,7 @@ the relationships between things in the network. This code generates the figure below. ```@example graphsection -using Karnak, Luxor, Graphs, NetworkLayout, Colors, SimpleWeightedGraphs +using Karnak, Graphs, NetworkLayout, Colors, SimpleWeightedGraphs d = @drawsvg begin background("grey10") sethue("yellow") @@ -126,7 +123,7 @@ It's time to see some kind of visual representation of the graph we've made. ```@example graphsection -# using Karnak, Graphs, NetworkLayout, Colors +using Karnak, Graphs g = Graph() add_vertices!(g, 4) @@ -144,6 +141,14 @@ end 600 300 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. +!!! note + + SVG is used in this manual because it's a good format for + line drawings, but you can also use Karnak.jl to create PDF or PNG. + See the [Luxor documentation](http://juliagraphics.github.io/Luxor.jl/stable/) for details. + PNG is a good choice if the graphics get very + complex, since large SVGs can tax web browsers. + ## Undirected and directed graphs We'll meet two main types of graph, **undirected** and **directed**. In our undirected graph `g` above, vertex 1 and vertex 2 are _neighbors_, connected with an edge, but there's no way to specify or see a direction for that connection. For example, if the graph was modelling people making financial transactions, we couldn't tell whether the person at vertex 1 sent money to the person at vertex 2, or received money from them. @@ -151,33 +156,13 @@ We'll meet two main types of graph, **undirected** and **directed**. In our undi In Graphs.jl, we can create directed graphs with `DiGraph()` (also `SimpleDiGraph()`). ```@example graphsection -gd = DiGraph() -add_vertices!(gd, 4) -add_edge!(gd, 1, 2) -add_edge!(gd, 1, 3) -add_edge!(gd, 2, 3) -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]) -end 600 300 -``` - -In this representation of our directed graph `gd`, we can now see the direction of the edges joining vertices. - -With the default drawing settings, the two edges connecting vertices 1 and 4 are drawn so that they overlap exactly, and you can't easily differentiate them. An easy way to see both edges at the same time is to specify a small amount of curvature with the `edgecurvature` keyword: - -```@example graphsection -gd = DiGraph() # hide -add_vertices!(gd, 4) # hide -add_edge!(gd, 1, 2) # hide -add_edge!(gd, 1, 3) # hide -add_edge!(gd, 2, 3) # hide -add_edge!(gd, 1, 4) # vertex 1 to vertex 4 # hide -add_edge!(gd, 4, 1) # vertex 4 to vertex 1 # hide +gd = DiGraph() +add_vertices!(gd, 4) +add_edge!(gd, 1, 2) +add_edge!(gd, 1, 3) +add_edge!(gd, 2, 3) +add_edge!(gd, 1, 4) # vertex 1 to vertex 4 +add_edge!(gd, 4, 1) # vertex 4 to vertex 1 @drawsvg begin background("grey10") @@ -186,34 +171,9 @@ add_edge!(gd, 4, 1) # vertex 4 to vertex 1 # hide end 600 300 ``` -Another option would be to make use of Luxor.jl’s `arrow()` functions, which would allow you to add all kinds of pointless graphics to edges, such as this hue-encoded `¾`-of-the-shaft-length version: +!!! note -```@example graphsection -gd = DiGraph() # hide -add_vertices!(gd, 4) # hide -add_edge!(gd, 1, 2) # hide -add_edge!(gd, 1, 3) # hide -add_edge!(gd, 2, 3) # hide -add_edge!(gd, 1, 4) # vertex 1 to vertex 4 # hide -add_edge!(gd, 4, 1) # vertex 4 to vertex 1 # hide - -@drawsvg begin # hide - background("grey10") # hide - sethue("thistle1") # hide - drawgraph(gd, vertexlabels = [1, 2, 3, 4], # hide - edgefunction = (n, s, d, f, t) -> begin # hide - arrow(f, t, [10, 10], # hide - decoration = 0.75, # hide - decorate = () -> begin # hide - sethue(HSB(60n, 0.7, 0.8)) # hide - ngon(O, 10, 3, 0, :fill) # hide # hide - end, # hide - arrowheadfunction= (f, t, a) -> () # hide - ) # hide - end # hide - ) # hide -end 600 300 # hide -``` + In this representation of our directed graph `gd`, we can see the direction of the edges joining the vertices. The `edgecurvature` keyword has been used to specify a small amount of curvature for each edge. Otherwise, with the default drawing settings, the two edges connecting vertices 1 and 4 would have been drawn overlapping, and difficult to distiguish at a glance. ## Very simple graphs @@ -240,9 +200,9 @@ end 400 300 hcat(d1, d2) ``` -Neither of these two graphs is **connected**. In a connected graph, every vertex is connected to every other via some path, a sequence of edges. +Neither of these two graphs is a **connected** graph. 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. +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 edges. ## Well-known graphs @@ -270,7 +230,7 @@ g = complete_digraph(N) background("grey10") setline(0.5) sethue("orange") - drawgraph(g, vertexlabels = vertices(g)) + drawgraph(g, vertexlabels = vertices(g), edgecurvature = 2) end 600 300 ``` @@ -297,8 +257,7 @@ W = 550 end 600 400 ``` -We provided the required locations of the vertices on the -drawing to the `layout` keyword. +Here, we calculated the coordinates of the vertices and passed the resulting `pts` to the `layout` keyword. A **grid** graph doesn't need much explanation: @@ -422,7 +381,7 @@ Some of the graphs in this figure would benefit from individual ‘tuning’ of the various layout parameters. Here's a larger view of the Petersen graph (named after -Julius Petersen, who first described it in 1898). +Danish mathematician Julius Petersen, who first described it in 1898). ```@example graphsection @drawsvg begin @@ -531,7 +490,7 @@ To add an edge: add_edge!(pg1, 10, 11) # join 10 to 11 ``` -It's sometimes useful to be able to see these relationships between neighbors visually. This example looks for the neighbors of vertex 10: +It's sometimes useful to be able to see these relationships between neighbors visually. This example looks for the neighbors of vertex 10 and draws them in thick red lines: ```@example graphsection @drawsvg begin @@ -541,21 +500,24 @@ pg = smallgraph(:petersen) vertexofinterest = 10 -E = [] +E = Int[] for (n, e) in enumerate(edges(pg)) if dst(e) == vertexofinterest || src(e) == vertexofinterest push!(E, n) end end +edgewts = [dst(e) ∈ E ? 4 : 1 for e in edges(pg)] + drawgraph(pg, vertexlabels = 1:nv(pg), layout = Shell(nlist=[6:10,]), vertexfillcolors = (v) -> ((v == vertexofinterest) || - v ∈ neighbors(pg, vertexofinterest)) && colorant"rebeccapurple", + 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"red" : colorant"thistle1", + edgestrokeweights = edgewts ) end 600 300 ``` @@ -1319,9 +1281,9 @@ end 800 600 ## Graph coloring -A simple **graph coloring** is a way of coloring the vertices or edges of a graph so that no two adjacent vertices or edges are the same color. The `greedy_color()` function finds a random graph coloring for a graph. The total number of colors, and an array of integers representing the colors, are returned in fields of the returned value. +A simple **graph coloring** is a way of coloring the vertices of a graph so that no two adjacent vertices are the same color. The `greedy_color()` function finds a random graph coloring for a graph. The total number of colors, and an array of integers representing the colors, are returned in fields `num_colors` and `colors` (as integers between 1 and `n`). -In the following example, only three colors are needed such that no edge connects two vertices with the same color. +In the following example, only three colors are needed such that no edge connects two vertices with the same color. Colors.jl has a `distinguishable_colors()` function that finds `n` colors which look sufficiently different: ```@example graphsection @drawsvg begin @@ -1336,7 +1298,7 @@ In the following example, only three colors are needed such that no edge connect end 800 400 ``` -whereas a complete graph might require many colors because there are so many connected vertices: +Here `gc.num_colors` is 3. However, a complete graph might require many colors because there are so many connected vertices. For example, `gc.num_colors` is now 20: ```@example graphsection @drawsvg begin diff --git a/docs/src/examples.md b/docs/src/examples.md index 6f88700..788340b 100644 --- a/docs/src/examples.md +++ b/docs/src/examples.md @@ -26,75 +26,6 @@ find(x::Int64) = stations[x] This chapter contains a few examples showing how to use `drawgraph()` to visualize a few graphs. -## Julia source tree - -This example takes a Julia expression and displays it as a tree. - -```@example -using Karnak, Graphs, NetworkLayout, Colors - -# shamelessly stolen from Professor David Sanders' Tree ! - -add_numbered_vertex!(g) = (add_vertex!(g); top = nv(g)) - -function walk_tree!(g, labels, ex, show_call = true) - top_vertex = add_numbered_vertex!(g) - where_start = 1 # which argument to start with - if !(show_call) && ex.head == :call - f = ex.args[1] # the function name - push!(labels, f) - where_start = 2 # drop "call" from tree - else - push!(labels, ex.head) - end - for i in where_start:length(ex.args) - if isa(ex.args[i], Expr) - child = walk_tree!(g, labels, ex.args[i], show_call) - add_edge!(g, top_vertex, child) - else - n = add_numbered_vertex!(g) - add_edge!(g, top_vertex, n) - push!(labels, ex.args[i]) - end - end - return top_vertex -end - -function walk_tree(ex::Expr, show_call = false) - g = DiGraph() - labels = Any[] - walk_tree!(g, labels, ex, show_call) - return (g, labels) -end - -# build graph and labels -expression = :(2 + sin(30) * cos(15) / 2π - log(-1.02^exp(-1))) - -g, labels = walk_tree(expression) - -@drawsvg begin - background("grey10") - sethue("gold") - drawgraph(g, - margin=60, - layout = buchheim, - vertexlabels = labels, - vertexshapes = :circle, - vertexshapesizes = 20, - edgefunction = (n, s, d, f, t) -> begin - move(f) - line(t) - strokepath() - end, - vertexlabelfontsizes = 15, - vertexlabelfontfaces = "Courier-Bold", - vertexlabeltextcolors = colorant"black") - fontface("Courier-Bold") - fontsize(15) - text(string(expression), boxbottomcenter() + (0, -20), halign=:center) -end -``` - ## Julia type tree This example tries to draw a type hierarchy diagram. The Buchheim layout algorithm can take a list of “vertex widths” that are normalized and then used to assign sufficient space for each label. @@ -115,6 +46,9 @@ function build_type_tree(g, T, level=0) add_numbered_vertex!(g) push!(labels, T) for t in subtypes(T) + if occursin(".", string(t)) # only Base + continue + end build_type_tree(g, t, level + 1) add_edge!(g, findfirst(isequal(T), labels), @@ -142,27 +76,29 @@ labels = map(string, labels) dg = @drawsvg begin background("grey20") - fontsize(8) - fontface("JuliaMono-Black") + fontsize(15) + fontface("JuliaMono-Bold") setline(1) sethue("gold") nodesizes = Float64[] for l in eachindex(labels) - labelwidth = textextents(string(labels[l]))[3] + tx = textextents(string(labels[l])) + labelwidth = tx[3] push!(nodesizes, labelwidth) end drawgraph(g, margin=50, layout=Buchheim(nodesize=nodesizes), vertexfunction=(v, c) -> begin w = nodesizes[v] - bbox = BoundingBox(box(c[v] + (0.8w/2, 0), 0.8w, 12)) + bbox = BoundingBox(box(c[v], w/2, get_fontsize())) + # box @layer begin - setopacity(0.8) sethue("white") - box(bbox, 5, action=:fillpreserve) + box(bbox, 2, action=:fillpreserve) sethue("gold") strokepath() end + #text @layer begin sethue("black") textfit(labels[v], bbox) @@ -170,7 +106,7 @@ dg = @drawsvg begin end, edgefunction=(n, s, d, f, t) -> manhattanline(f, t) ) -end 800 500 +end 1000 550 nothing # hide ``` @@ -182,7 +118,76 @@ nothing # hide dg # hide ``` -There are still a few problems with this visualization. +This graph could do with a bit more tweaking. + +## Julia source tree + +This example takes a Julia expression and displays it as a tree. + +```@example +using Karnak, Graphs, NetworkLayout, Colors + +# shamelessly stolen from Professor David Sanders' Tree ! + +add_numbered_vertex!(g) = (add_vertex!(g); top = nv(g)) + +function walk_tree!(g, labels, ex, show_call = true) + top_vertex = add_numbered_vertex!(g) + where_start = 1 # which argument to start with + if !(show_call) && ex.head == :call + f = ex.args[1] # the function name + push!(labels, f) + where_start = 2 # drop "call" from tree + else + push!(labels, ex.head) + end + for i in where_start:length(ex.args) + if isa(ex.args[i], Expr) + child = walk_tree!(g, labels, ex.args[i], show_call) + add_edge!(g, top_vertex, child) + else + n = add_numbered_vertex!(g) + add_edge!(g, top_vertex, n) + push!(labels, ex.args[i]) + end + end + return top_vertex +end + +function walk_tree(ex::Expr, show_call = false) + g = DiGraph() + labels = Any[] + walk_tree!(g, labels, ex, show_call) + return (g, labels) +end + +# build graph and labels +expression = :(2 + sin(30) * cos(15) / 2π - log(-1.02^exp(-1))) + +g, labels = walk_tree(expression) + +@drawsvg begin + background("grey10") + sethue("gold") + drawgraph(g, + margin=60, + layout = buchheim, + vertexlabels = labels, + vertexshapes = :circle, + vertexshapesizes = 20, + edgefunction = (n, s, d, f, t) -> begin + move(f) + line(t) + strokepath() + end, + vertexlabelfontsizes = 15, + vertexlabelfontfaces = "JuliaMono-Bold", # probably won't be available for docs + vertexlabeltextcolors = colorant"black") + fontface("JuliaMono-Bold") + fontsize(15) + text(string(expression), boxbottomcenter() + (0, -20), halign=:center) +end +``` ## LayeredLayouts.jl @@ -691,9 +696,11 @@ and presented as part of the workshop: __Analyzing Graphs 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 changes since the video was made are: + +- the renaming of LightGraphs.jl to Graphs.jl + +- the way to access the list of packages has changed The code builds a dependency graph of the connections (ie which package depends on which package) for Julia packages @@ -732,20 +739,20 @@ packages_info = registry_file["packages"]; First we need the name and location of every package: ```julia -# Julia v1.6? +# Julia <= v1.6 pkg_paths = map(values(packages_info)) do d (name = d["name"], path = d["path"]) end ``` ```julia -# Julia v1.7? +# Julia >= v1.7 pkg_paths = map(values(Pkg.Registry.reachable_registries()[1].pkgs)) do d (name = d.name, path = d.path) end ``` -The result in `pkg_paths` is a vector of tuples, containing the name and location on disk of every package: +The result in `pkg_paths` is a vector of tuples, containing the name and location of every package: ```julia 7495-element Vector{NamedTuple{(:name, :path), Tuple{String, String}}}: @@ -1281,7 +1288,7 @@ For that first cycle: ImageCore.jl's Project.toml file has MosaicViews.jl in its 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. +The solution may be to print out these graph representations and stick them on a nearby wall, although, with Julia's General Registry changing every day, it would be out of date before the glue dries. ![wall art office graph dependency](assets/figures/graph-dependency-wallart.png) diff --git a/docs/src/syntax.md b/docs/src/syntax.md index ba3f68e..76b627b 100644 --- a/docs/src/syntax.md +++ b/docs/src/syntax.md @@ -742,7 +742,34 @@ drawgraph(g, end 600 300 ``` -For more interesting arrows for edges, Luxor's arrows are available: +For more interesting arrows for edges, Luxor's arrows are available, and you can define functions to create all kinds of graphical deatil: + +```@example graphsection +gd = DiGraph() +add_vertices!(gd, 4) +add_edge!(gd, 1, 2) +add_edge!(gd, 1, 3) +add_edge!(gd, 2, 3) +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], + edgefunction = (n, s, d, f, t) -> begin + arrow(f, t, [10, 10], + decoration = 0.75, + decorate = () -> begin + sethue(HSB(60n, 0.7, 0.8)) + ngon(O, 10, 3, 0, :fill) + end, + arrowheadfunction= (f, t, a) -> () + ) + end + ) +end 600 300 +``` ```@example graphsection @drawsvg begin @@ -925,7 +952,7 @@ end 600 600 ### `edgecurvature` and `edgecaps` -`edgecurvature` determines the curvature of the edges, and `edgegaps` sets the distance between the tip of the arrowhead and the vertex position. +`edgecurvature` determines the curvature of the edges, and `edgegaps` sets the distance between the tip of the arrowhead and the vertex position. Units, as everywhere in Karnak and Luxor, are points/pixels (1 point is 0.3527mm). ```@example graphsection g = grid((3, 3)) diff --git a/examples/example1.jl b/examples/example1.jl index fdbfd43..1f80c7a 100644 --- a/examples/example1.jl +++ b/examples/example1.jl @@ -1,5 +1,7 @@ using Luxor, Karnak, Graphs, Colors, NetworkLayout +# this was the first test page + function test1() Drawing(600, 600, :png) origin() diff --git a/examples/konigsberg.jl b/examples/konigsberg.jl index 943c38c..3842bfa 100644 --- a/examples/konigsberg.jl +++ b/examples/konigsberg.jl @@ -14,10 +14,14 @@ seven bridges. The problem was to devise a walk through the city that would cross each of those bridges once and only once. -# I'm not sure I've set this up correctly yet... +(wikipedia) =# +# This code trys to find cycles that visit every island. + +# I'm not sure I've set this up correctly... + konigsberg_al = [ [2, 4], # 1 [3, 5, 1], # 2 @@ -85,7 +89,7 @@ for (pos, n) in tiles boundingbox=BoundingBox(box(O, tiles.tilewidth, tiles.tilewidth)), edgelist = vertexlist_to_edgelist(cycle), edgestrokeweights = 3, - edgestrokecolors = colorant"magenta", + edgestrokecolors = colorant"purple", edgegaps=0) end end