diff --git a/.gitignore b/.gitignore index 0b8a543..a403117 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /Manifest.toml docs/build/* .DS_Store +*.pages diff --git a/CHANGELOG.md b/CHANGELOG.md index 6699223..0a7aa01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,13 @@ # Changelog -## [v0.4.0] - 2022-06-?? +## [v0.4.0] - 2022-12-25 ### Added ## Changed +- font choice better + ### Removed ### Deprecated diff --git a/Project.toml b/Project.toml index a85a089..04cc9db 100644 --- a/Project.toml +++ b/Project.toml @@ -16,7 +16,7 @@ Colors = "0.12" Graphs = "1" Luxor = "3" NetworkLayout = "0.4.4" -Reexport = "0.2, 1.0" +Reexport = "0.2, 1.0, 1.1, 1.2" SimpleWeightedGraphs = "1" julia = "1.5" diff --git a/docs/Project.toml b/docs/Project.toml index 32b164c..6af9adc 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -17,3 +17,4 @@ Graphs = "1.6" julia = "^1.5" Luxor = "3" NetworkLayout = "0.4.4" +SimpleWeightedGraphs = "1.2" diff --git a/docs/src/basics.md b/docs/src/basics.md index 2dda7c7..2c7dae3 100644 --- a/docs/src/basics.md +++ b/docs/src/basics.md @@ -17,7 +17,7 @@ familiar with the basics of programming in Julia. 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 + 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. @@ -824,7 +824,7 @@ It's useful to know how to visit all vertices just once. You can do this for DiGraphs if you can 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. This example uses `simplecycles()` to find all of them (there are over 400 for this graph), so only the first one with the right length is used. -``` @example graphsection +```@example graphsection @drawsvg begin background("grey10") g = complete_digraph(6) @@ -845,6 +845,58 @@ drawgraph(g, layout = spring, end 800 400 ``` +## Trees + +A tree is a connected graph with no cycles. A *rooted tree* is a tree graph in which one vertex has been designated as the root, or origin. Rooted tree graphs can be drawn using the Buchheim layout algorithm (named after the developer, Christoph Buchheim). + +In the next example, we start with a *binary tree*, in which each vertex has at most two vertices - but we add one more vertex so that it's no longer a binary tree. + +```@raw html +
Code for this figure +``` + +This code generates the figure below. + +```@example graphsection +using Karnak, Luxor, Graphs, NetworkLayout, Colors + +d = @drawsvg begin + background("grey10") + sethue("purple") + fontsize(12) + + bt = binary_tree(4) + g = SimpleDiGraph(collect(edges(bt))) + + # add another vertex + add_vertex!(g) + add_edge!(g, 7, 16) + + drawgraph(g, + layout=buchheim, + margin=20, + edgestrokeweights=2, + edgegaps=12, + vertexlabels = 1:nv(g), + vertexshapes=:circle, + vertexfillcolors=[RGB(Luxor.julia_red...), + RGB(Luxor.julia_purple...), + RGB(Luxor.julia_green...), + RGB(Luxor.julia_blue...)], + vertexshapesizes=12, + vertexlabeltextcolors=colorant"white", + ) +end 600 350 +``` + +```@raw html +
+``` + +```@example graphsection +d # hide +``` + ## 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. diff --git a/docs/src/examples.md b/docs/src/examples.md index 03dc43e..78261c7 100644 --- a/docs/src/examples.md +++ b/docs/src/examples.md @@ -24,7 +24,134 @@ find(x::Int64) = stations[x] # Examples -This section contains a few examples showing how to use `drawgraph()` to visualize a few graphs. +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") + fontface("JuliaMono-Black") + 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", + vertexlabeltextcolors = colorant"black") + 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. + +```@example +using Karnak, Graphs, NetworkLayout, InteractiveUtils + +add_numbered_vertex!(g) = add_vertex!(g) + +function build_type_tree(g, T, level=0) + add_numbered_vertex!(g) + push!(labels, T) + for t in subtypes(T) + build_type_tree(g, t, level + 1) + add_edge!(g, + findfirst(isequal(T), labels), + findfirst(isequal(t), labels)) + end +end + +function manhattanline(pt1, pt2) + mp = midpoint(pt1, pt2) + poly([pt1, + Point(pt1.x, mp.y), + Point(pt1.x, mp.y), + Point(pt2.x, mp.y), + Point(pt2.x, mp.y), + Point(pt2.x, pt2.y - get_fontsize()) + ], :stroke) +end + +g = DiGraph() +labels = [] +build_type_tree(g, Real) +labels = map(string, labels) + +@drawsvg begin + background("white") + sethue("black") + + fontsize(9) + + # adjust for centered labels + nodesizes = Float64[] + for l in eachindex(labels) + push!(nodesizes, textextents(string(labels[l]))[3] + + textextents(string(labels[mod1(l + 1, end)]))[3]) + end + drawgraph(g, margin=50, + layout=Buchheim(nodesize=nodesizes), + vertexfunction=(v, c) -> text(labels[v], c[v], halign=:center), + edgefunction=(n, s, d, f, t) -> manhattanline(f, t) + ) +end 1200 550 +``` + +It can be quite difficult to get the final output to look perfect. ## The London Tube diff --git a/examples/trees.jl b/examples/trees.jl deleted file mode 100644 index b4f7f8d..0000000 --- a/examples/trees.jl +++ /dev/null @@ -1,61 +0,0 @@ -using Karnak, Graphs, NetworkLayout, Colors - -# I started adapting David Sanders https://github.com/JuliaTeX/TreeView.jl -# but didn't finish - -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 - -g, labels = walk_tree(:(2 + sin(30))) -g, labels = walk_tree(:(begin g = DiGraph() - labels = Any[] - walk_tree!(g, labels, ex, show_call) - return (g, labels) -end)) - -@drawsvg begin - background("grey10") - sethue("gold") - drawgraph(g, - layout = buchheim, - vertexlabels = labels, - vertexshapes = :none, - edgefunction = (n, s, d, f, t) -> begin - - move(f) - line(t) - strokepath() - end, - edgegaps = 20, - vertexlabelfontsizes = 10, - vertexlabeltextcolors = colorant"gold") -end diff --git a/examples/typetree.jl b/examples/typetree.jl deleted file mode 100644 index 97244d9..0000000 --- a/examples/typetree.jl +++ /dev/null @@ -1,52 +0,0 @@ -using Karnak, Graphs, NetworkLayout - -#= -Output a tree graph of a Julia type - -This isn't yet working 100%, I still don't -understand the Buchheim layout yet... -=# - -add_numbered_vertex!(g) = add_vertex!(g) - -function build_type_tree(g, T, level=0) - add_numbered_vertex!(g) - push!(labels, T) - for t in subtypes(T) - build_type_tree(g, t, level+1) - add_edge!(g, - findfirst(isequal(T), labels), - findfirst(isequal(t), labels)) - end -end - -function manhattanline(pt1, pt2) - mp = midpoint(pt1, pt2) - poly([pt1, - Point(pt1.x, mp.y), - Point(pt1.x, mp.y), - Point(pt2.x, mp.y), - Point(pt2.x, mp.y), - Point(pt2.x, pt2.y - get_fontsize()) - ], :stroke) -end - -using Dates -g = DiGraph() -labels = [] -build_type_tree(g, Dates.AbstractTime) -labels = map(string, labels) - -@drawsvg begin - background("white") - sethue("black") - fontface("JuliaMono") - setline(1) - nodesizes = map(t -> textextents(string(t))[3], labels) - fontsize(10) - drawgraph(g, margin=50, - layout = Buchheim(nodesize = nodesizes), - vertexfunction = (v, c) -> text(labels[v], c[v], halign=:center), - edgefunction = (n, s, d, f, t) -> manhattanline(f, t) - ) -end 1200 600 diff --git a/src/drawgraph.jl b/src/drawgraph.jl index b5129c6..6695c2f 100644 --- a/src/drawgraph.jl +++ b/src/drawgraph.jl @@ -296,18 +296,22 @@ function _drawedgelabels(from, to; # set font face font_face = "" + usedefaultfont=true if isnothing(edgelabelfontfaces) # use default fontface elseif edgelabelfontfaces isa Array if !isempty(edgelabelfontfaces) if edgelabelfontfaces[mod1(edgenumber, end)] != :none font_face = edgelabelfontfaces[mod1(edgenumber, end)] + usedefaultfont=false end end elseif edgelabelfontfaces isa AbstractRange font_face = edgelabelfontfaces[mod1(edgenumber, end)] + usedefaultfont=false elseif edgelabelfontfaces isa AbstractString font_face = edgelabelfontfaces + usedefaultfont=false elseif edgelabelfontfaces == :none end @@ -321,7 +325,9 @@ function _drawedgelabels(from, to; str = string(collect(edgelabels)[mod1(edgenumber, end)]) if textcolor != :none setcolor(textcolor) - font_face != "" && fontface(font_face) + if !usedefaultfont + font_face != "" && fontface(font_face) + end fontsize(font_size) text(str, midpoint(from, to), halign=:center, angle=textrotation) end @@ -329,7 +335,9 @@ function _drawedgelabels(from, to; end elseif edgelabels isa Function @layer begin - font_face != "" && fontface(font_face) + if !usedefaultfont + font_face != "" && fontface(font_face) + end fontsize(font_size) edgelabels(edgenumber, edgesrc, edgedest, from, to) end @@ -341,7 +349,9 @@ function _drawedgelabels(from, to; @layer begin if textcolor != :none setcolor(textcolor) - font_face != "" && fontface(font_face) + if !usedefaultfont + font_face != "" && fontface(font_face) + end fontsize(font_size) text(edgelabel, midpoint(from, to), halign=:center, angle=textrotation) end @@ -619,18 +629,22 @@ function _drawvertexlabels(vertex, coordinates::Array{Point,1}; # set font face font_face = "" + usedefaultfont = true if isnothing(vertexlabelfontfaces) # use default fontsize elseif vertexlabelfontfaces isa Array if !isempty(vertexlabelfontfaces) if vertexlabelfontfaces[mod1(vertex, end)] != :none font_face = vertexlabelfontfaces[mod1(vertex, end)] + usedefaultfont = false end end elseif vertexlabelfontfaces isa AbstractRange font_face = vertexlabelfontfaces[mod1(vertex, end)] + usedefaultfont = false elseif vertexlabelfontfaces isa AbstractString font_face = vertexlabelfontfaces + usedefaultfont = false elseif vertexlabelfontfaces == :none end @@ -717,7 +731,9 @@ function _drawvertexlabels(vertex, coordinates::Array{Point,1}; rotate(textrotation) setcolor(textcolor) fontsize(font_size) - fontface(font_face) + if !usedefaultfont + font_face != "" && fontface(font_face) + end text(vertexlabel, halign=:center, valign=:middle, O + polar(textoffsetdistance, textoffsetangle)) end end @@ -972,7 +988,7 @@ The gaps from vertex center to arrow tip The colors of the label text -`edgelabelrotations`: A | Range | function edgelabelrotations = (n, s, d, f, t) -> angle +`edgelabelrotations`: Array | Range | function edgelabelrotations = (n, s, d, f, t) -> angle The rotation of the label text