diff --git a/README.md b/README.md index e5dd530..f34804c 100644 --- a/README.md +++ b/README.md @@ -127,9 +127,22 @@ locs_x, locs_y = shell_layout(g, nlist) gplot(g, locs_x, locs_y, nodelabel=nodelabel) ``` +### stress majorize layout +```julia +gplot(g, layout=stressmajorize_layout) +``` + +### community layout +```julia +community_id = rand(1:3, nv(g)) #membership for each node +node_c = [colorant"red",colorant"yellow",colorant"blue"] #colors for each community +locs_x, locs_y = community_layout(g,community_id) +gplot(g, locs_x, locs_y, nodefillc = node_c[community_id]) +``` + ## Curve edge ```julia -gplot(g, linetype="curve") +gplot(g, linetype=:curve) ``` ## Show plot @@ -159,29 +172,29 @@ gplot(h) + `locs_x, locs_y` Locations of the nodes (will be normalized and centered). If not specified, will be obtained from `layout` kwarg. # Keyword Arguments -+ `layout` Layout algorithm: `random_layout`, `circular_layout`, `spring_layout`, `shell_layout`, `stressmajorize_layout`, `spectral_layout`. Default: `spring_layout` -+ `NODESIZE` Max size for the nodes. Default: `3.0/sqrt(N)` ++ `layout` Layout algorithm: `random_layout`, `circular_layout`, `spring_layout`, `shell_layout`, `stressmajorize_layout`, `spectral_layout`, `community_layout`. Default: `spring_layout` ++ `max_nodesize` Max size for the nodes. Default: `3.0/sqrt(N)` + `nodesize` Relative size for the nodes, can be a Vector. Default: `1.0` + `nodelabel` Labels for the vertices, a Vector or nothing. Default: `nothing` + `nodelabelc` Color for the node labels, can be a Vector. Default: `colorant"black"` + `nodelabeldist` Distances for the node labels from center of nodes. Default: `0.0` -+ `nodelabelangleoffset` Angle offset for the node labels. Default: `π/4.0` -+ `NODELABELSIZE` Largest fontsize for the vertice labels. Default: `4.0` ++ `nodelabelangleoffset` Angle offset for the node labels (only used when `nodelabeldist` is not zero). Default: `π/4.0` ++ `max_nodelabelsize` Largest fontsize for the vertice labels. Default: `4.0` + `nodelabelsize` Relative fontsize for the vertice labels, can be a Vector. Default: `1.0` + `nodefillc` Color to fill the nodes with, can be a Vector. Default: `colorant"turquoise"` + `nodestrokec` Color for the nodes stroke, can be a Vector. Default: `nothing` + `nodestrokelw` Line width for the nodes stroke, can be a Vector. Default: `0.0` -+ `edgelabel` Labels for the edges, a Vector or nothing. Default: `[]` ++ `edgelabel` Labels for the edges, a Vector or nothing. Default: `nothing` + `edgelabelc` Color for the edge labels, can be a Vector. Default: `colorant"black"` + `edgelabeldistx, edgelabeldisty` Distance for the edge label from center of edge. Default: `0.0` -+ `EDGELABELSIZE` Largest fontsize for the edge labels. Default: `4.0` ++ `max_edgelabelsize` Largest fontsize for the edge labels. Default: `4.0` + `edgelabelsize` Relative fontsize for the edge labels, can be a Vector. Default: `1.0` -+ `EDGELINEWIDTH` Max line width for the edges. Default: `0.25/sqrt(N)` ++ `max_edgelinewidth` Max line width for the edges. Default: `0.25/sqrt(N)` + `edgelinewidth` Relative line width for the edges, can be a Vector. Default: `1.0` + `edgestrokec` Color for the edge strokes, can be a Vector. Default: `colorant"lightgray"` + `arrowlengthfrac` Fraction of line length to use for arrows. Equal to 0 for undirected graphs. Default: `0.1` for the directed graphs + `arrowangleoffset` Angular width in radians for the arrows. Default: `π/9 (20 degrees)` -+ `linetype` Type of line used for edges ("straight", "curve"). Default: "straight" ++ `linetype` Type of line used for edges (:straight, :curve). Default: :straight + `outangle` Angular width in radians for the edges (only used if `linetype = "curve`). Default: `π/5 (36 degrees)` # Reporting Bugs diff --git a/src/GraphPlot.jl b/src/GraphPlot.jl index c1738c1..3472fb8 100644 --- a/src/GraphPlot.jl +++ b/src/GraphPlot.jl @@ -17,16 +17,11 @@ export shell_layout, stressmajorize_layout -include("deprecations.jl") - # layout algorithms include("layout.jl") -include("stress.jl") # ploting utilities -include("shape.jl") include("lines.jl") include("plot.jl") -include("collapse_plot.jl") end # module diff --git a/src/collapse_plot.jl b/src/collapse_plot.jl deleted file mode 100644 index 2e1e062..0000000 --- a/src/collapse_plot.jl +++ /dev/null @@ -1,96 +0,0 @@ -using GraphPlot - -function collapse_graph(g::AbstractGraph, membership::Vector{Int}) - nb_comm = maximum(membership) - - collapsed_edge_weights = Vector{Dict{Int,Float64}}(undef, nb_comm) - for i=1:nb_comm - collapsed_edge_weights[i] = Dict{Int,Float64}() - end - - for e in edges(g) - u = src(e) - v = dst(e) - u_comm = membership[u] - v_comm = membership[v] - - # for special case of undirected network - if !is_directed(g) - u_comm, v_comm = minmax(u_comm, v_comm) - end - - if haskey(collapsed_edge_weights[u_comm], v_comm) - collapsed_edge_weights[u_comm][v_comm] += 1 - else - collapsed_edge_weights[u_comm][v_comm] = 1 - end - end - - collapsed_graph = SimpleGraph(nb_comm) - collapsed_weights = Float64[] - - for u=1:nb_comm - for (v,w) in collapsed_edge_weights[u] - add_edge!(collapsed_graph, u, v) - push!(collapsed_weights, w) - end - end - - return collapsed_graph, collapsed_weights -end - -function community_layout(g::AbstractGraph, membership::Vector{Int}) - N = length(membership) - lx = zeros(N) - ly = zeros(N) - comms = Dict{Int,Vector{Int}}() - for (idx,lbl) in enumerate(membership) - if haskey(comms, lbl) - push!(comms[lbl], idx) - else - comms[lbl] = Int[idx] - end - end - h, w = collapse_graph(g, membership) - clx, cly = spring_layout(h) - for (lbl, nodes) in comms - θ = range(0, stop=2pi, length=(length(nodes) + 1))[1:end-1] - for (idx, node) in enumerate(nodes) - lx[node] = 1.8*length(nodes)/N*cos(θ[idx]) + clx[lbl] - ly[node] = 1.8*length(nodes)/N*sin(θ[idx]) + cly[lbl] - end - end - return lx, ly -end - -function collapse_layout(g::AbstractGraph, membership::Vector{Int}) - sg = Graphs.SimpleGraph(nv(g)) - for e in edges(g) - u = src(e) - v = dst(e) - Graphs.add_edge!(sg, u, v) - end - N = length(membership) - lx = zeros(N) - ly = zeros(N) - comms = Dict{Int,Vector{Int}}() - for (idx,lbl) in enumerate(membership) - if haskey(comms, lbl) - push!(comms[lbl], idx) - else - comms[lbl] = Int[idx] - end - end - h, w = collapse_graph(g, membership) - clx, cly = spring_layout(h) - for (lbl, nodes) in comms - subg = sg[nodes] - sublx, subly = spring_layout(subg) - θ = range(0, stop=2pi, length=(length(nodes) + 1))[1:end-1] - for (idx, node) in enumerate(nodes) - lx[node] = 1.8*length(nodes)/N*sublx[idx] + clx[lbl] - ly[node] = 1.8*length(nodes)/N*subly[idx] + cly[lbl] - end - end - return lx, ly -end diff --git a/src/deprecations.jl b/src/deprecations.jl deleted file mode 100644 index 4e07ac3..0000000 --- a/src/deprecations.jl +++ /dev/null @@ -1,106 +0,0 @@ -using Base: depwarn - - -function _nv(g) - depwarn("`GraphPlot._nv(g)` is deprectated. Use `Graphs.nv(g)` instead.", :_nv) - return Graphs.nv(g) -end - -function _ne(g) - depwarn("`GraphPlot._ne(g)` is deprectated. Use `Graphs.ne(g)` instead.", :_ne) - return Graphs.ne(g) -end - -function _vertices(g) - depwarn("`GraphPlot._vertices(g)` is deprectated. Use `Graphs.vertices(g)` instead.", :_vertices) - return Graphs.vertices(g) -end - -function _edges(g) - depwarn("`GraphPlot._edges(g)` is deprectated. Use `Graphs.edges(g)` instead.", :_edges) - return Graphs.edges(g) -end - -function _src_index(e, g) - depwarn("`GraphPlot._src_index(g)` is deprectated. Use `Graphs.src(e)` instead.", :_src_index) - return Graphs.src(e) -end - -function _dst_index(e, g) - depwarn("`GraphPlot._dst_index(g)` is deprectated. Use `Graphs.dst(e)` instead.", :_dst_index) - return Graphs.dst(e) -end - -function _adjacency_matrix(g) - depwarn("`GraphPlot._adjacency_matrix(g)` is deprectated. Use `Graphs.adjacency_matrix(g)` instead.", :_adjacency_matrix) - return Graphs.adjacency_matrix(g) -end - -function _is_directed(g) - depwarn("`GraphPlot._is_directed(g)` is deprectated. Use `Graphs.is_directed(g)` instead.", :_is_directed) - return Graphs.is_directed(g) -end - -function _laplacian_matrix(g) - depwarn("`GraphPlot._laplacian_matrix(g)` is deprectated. Use `Graphs.laplacian_matrix(g)` instead.", :_laplacian_matrix) - return Graphs.laplacian_matrix(g) -end - - -""" -read some famous graphs - -**Paramenters** - -*graphname* -Currently, `graphname` can be one of ["karate", "football", "dolphins", -"netscience", "polbooks", "power", "cond-mat"] - -**Return** -a graph - -**Example** - julia> g = graphfamous("karate") -""" -function graphfamous(graphname::AbstractString) - depwarn(""" - `graphfamous` has been deprecated and will be removed in the future. Consider the package `GraphIO.jl` for loading graphs. - """, :graphfamous) - file = joinpath(dirname(@__DIR__), "data", graphname*".dat") - readedgelist(file) -end -export graphfamous - -using DelimitedFiles: readdlm -"""read graph from in edgelist format""" -function readedgelist(filename; is_directed::Bool=false, start_index::Int=0, delim::Char=' ') - depwarn(""" - `graphfamous` has been deprecated and will be removed in the future. Consider the package `GraphIO.jl` for loading graphs. - """, :graphfamous) - es = readdlm(filename, delim, Int) - es = unique(es, dims=1) - if start_index == 0 - es = es .+ 1 - end - N = maximum(es) - if is_directed - g = DiGraph(N) - for i=1:size(es,1) - add_edge!(g, es[i,1], es[i,2]) - end - return g - else - for i=1:size(es,1) - if es[i,1] > es[i,2] - es[i,1], es[i,2] = es[i,2], es[i,1] - end - end - es = unique(es, dims=1) - g = Graph(N) - for i=1:size(es,1) - add_edge!(g, es[i,1], es[i,2]) - end - return g - end -end -export readedgelist diff --git a/src/layout.jl b/src/layout.jl index 2a9956b..5507b89 100644 --- a/src/layout.jl +++ b/src/layout.jl @@ -1,7 +1,7 @@ using SparseArrays: SparseMatrixCSC, sparse using ArnoldiMethod: SR using Base: OneTo -using LinearAlgebra: eigen +using LinearAlgebra: eigen, norm """ Position nodes uniformly at random in the unit square. @@ -284,3 +284,288 @@ function _spectral(A::SparseMatrixCSC) index = sortperm(real(eigenvalues))[2:3] return real(eigenvectors[:, index[1]]), real(eigenvectors[:, index[2]]) end + +# This layout algorithm is copy from [IainNZ](https://github.com/IainNZ)'s [GraphLayout.jl](https://github.com/IainNZ/GraphLayout.jl) +@doc """ +Compute graph layout using stress majorization + +Inputs: + + δ: Matrix of pairwise distances + p: Dimension of embedding (default: 2) + w: Matrix of weights. If not specified, defaults to + w[i,j] = δ[i,j]^-2 if δ[i,j] is nonzero, or 0 otherwise + X0: Initial guess for the layout. Coordinates are given in rows. + If not specified, default to random matrix of Gaussians + +Additional optional keyword arguments control the convergence of the algorithm +and the additional output as requested: + + maxiter: Maximum number of iterations. Default: 400size(X0, 1)^2 + abstols: Absolute tolerance for convergence of stress. + The iterations terminate if the difference between two + successive stresses is less than abstol. + Default: √(eps(eltype(X0)) + reltols: Relative tolerance for convergence of stress. + The iterations terminate if the difference between two + successive stresses relative to the current stress is less than + reltol. Default: √(eps(eltype(X0)) + abstolx: Absolute tolerance for convergence of layout. + The iterations terminate if the Frobenius norm of two successive + layouts is less than abstolx. Default: √(eps(eltype(X0)) + verbose: If true, prints convergence information at each iteration. + Default: false + returnall: If true, returns all iterates and their associated stresses. + If false (default), returns the last iterate + +Output: + + The final layout X, with coordinates given in rows, unless returnall=true. + +Reference: + + The main equation to solve is (8) of: + + @incollection{ + author = {Emden R Gansner and Yehuda Koren and Stephen North}, + title = {Graph Drawing by Stress Majorization} + year={2005}, + isbn={978-3-540-24528-5}, + booktitle={Graph Drawing}, + seriesvolume={3383}, + series={Lecture Notes in Computer Science}, + editor={Pach, J\'anos}, + doi={10.1007/978-3-540-31843-9_25}, + publisher={Springer Berlin Heidelberg}, + pages={239--250}, + } +""" +function stressmajorize_layout(g::AbstractGraph, + p::Int=2, + w=nothing, + X0=randn(nv(g), p); + maxiter = 400size(X0, 1)^2, + abstols=√(eps(eltype(X0))), + reltols=√(eps(eltype(X0))), + abstolx=√(eps(eltype(X0))), + verbose = false) + + @assert size(X0, 2)==p + δ = fill(1.0, nv(g), nv(g)) + + if w == nothing + w = δ.^-2 + w[.!isfinite.(w)] .= 0 + end + + @assert size(X0, 1)==size(δ, 1)==size(δ, 2)==size(w, 1)==size(w, 2) + Lw = weightedlaplacian(w) + pinvLw = pinv(Lw) + newstress = stress(X0, δ, w) + Xs = Matrix[X0] + stresses = [newstress] + iter = 0 + for outer iter = 1:maxiter + #TODO the faster way is to drop the first row and col from the iteration + X = pinvLw * (LZ(X0, δ, w)*X0) + @assert all(isfinite.(X)) + newstress, oldstress = stress(X, δ, w), newstress + verbose && @info("""Iteration $iter + Change in coordinates: $(norm(X - X0)) + Stress: $newstress (change: $(newstress-oldstress)) + """) + push!(Xs, X) + push!(stresses, newstress) + abs(newstress - oldstress) < reltols * newstress && break + abs(newstress - oldstress) < abstols && break + norm(X - X0) < abstolx && break + X0 = X + end + iter == maxiter && @warn("Maximum number of iterations reached without convergence") + + Xs[end][:,1], Xs[end][:,2] +end + +@doc """ +Stress function to majorize + +Input: + X: A particular layout (coordinates in rows) + d: Matrix of pairwise distances + w: Weights for each pairwise distance + +See (1) of Reference +""" +function stress(X, d=fill(1.0, size(X, 1), size(X, 1)), w=nothing) + s = 0.0 + n = size(X, 1) + if w==nothing + w = d.^-2 + w[!isfinite.(w)] = 0 + end + @assert n==size(d, 1)==size(d, 2)==size(w, 1)==size(w, 2) + for j=1:n, i=1:j-1 + s += w[i, j] * (norm(X[i,:] - X[j,:]) - d[i,j])^2 + end + @assert isfinite(s) + return s +end + +@doc """ +Compute weighted Laplacian given ideal weights w + +Lʷ defined in (4) of the Reference +""" +function weightedlaplacian(w) + n = LinearAlgebra.checksquare(w) + T = eltype(w) + Lw = zeros(T, n, n) + for i=1:n + D = zero(T) + for j=1:n + i==j && continue + Lw[i, j] = -w[i, j] + D += w[i, j] + end + Lw[i, i] = D + end + return Lw +end + +@doc """ +Computes L^Z defined in (5) of the Reference + +Input: Z: current layout (coordinates) + d: Ideal distances (default: all 1) + w: weights (default: d.^-2) +""" +function LZ(Z, d, w) + n = size(Z, 1) + L = zeros(n, n) + for i=1:n + D = 0.0 + for j=1:n + i==j && continue + nrmz = norm(Z[i,:] - Z[j,:]) + nrmz==0 && continue + δ = w[i, j] * d[i, j] + L[i, j] = -δ/nrmz + D -= -δ/nrmz + end + L[i, i] = D + end + @assert all(isfinite.(L)) + L +end + +""" +Community layout for graphs with pre-defined community assignments + +**Parameters** + +*g* +a graph + +*membership* +`Vector` indicating the membership (`Int`) of each node + +**Examples** +``` +julia> g = smallgraph(:karate) +julia> member_id = rand(1:5, nv(g)) +julia> locs_x, locs_y = spectral_layout(g, member_id) +``` +""" +function community_layout(g::AbstractGraph, membership::Vector{Int}=collect(vertices(g))) + N = length(membership) + lx = zeros(N) + ly = zeros(N) + comms = Dict{Int,Vector{Int}}() + for (idx,lbl) in enumerate(membership) + if haskey(comms, lbl) + push!(comms[lbl], idx) + else + comms[lbl] = Int[idx] + end + end + h = collapse_graph(g, membership)[1] + clx, cly = spring_layout(h) + for (lbl, nodes) in comms + θ = range(0, stop=2pi, length=(length(nodes) + 1))[1:end-1] + for (idx, node) in enumerate(nodes) + lx[node] = 1.8*length(nodes)/N*cos(θ[idx]) + clx[lbl] + ly[node] = 1.8*length(nodes)/N*sin(θ[idx]) + cly[lbl] + end + end + return lx, ly +end + +function collapse_layout(g::AbstractGraph, membership::Vector{Int}) + sg = Graphs.SimpleGraph(nv(g)) + for e in edges(g) + u = src(e) + v = dst(e) + Graphs.add_edge!(sg, u, v) + end + N = length(membership) + lx = zeros(N) + ly = zeros(N) + comms = Dict{Int,Vector{Int}}() + for (idx,lbl) in enumerate(membership) + if haskey(comms, lbl) + push!(comms[lbl], idx) + else + comms[lbl] = Int[idx] + end + end + h = collapse_graph(g, membership)[1] + clx, cly = spring_layout(h) + for (lbl, nodes) in comms + subg = sg[nodes] + sublx, subly = spring_layout(subg) + for (idx, node) in enumerate(nodes) + lx[node] = 1.8*length(nodes)/N*sublx[idx] + clx[lbl] + ly[node] = 1.8*length(nodes)/N*subly[idx] + cly[lbl] + end + end + return lx, ly +end + +function collapse_graph(g::AbstractGraph, membership::Vector{Int}) + nb_comm = maximum(membership) + + collapsed_edge_weights = Vector{Dict{Int,Float64}}(undef, nb_comm) + for i=1:nb_comm + collapsed_edge_weights[i] = Dict{Int,Float64}() + end + + for e in edges(g) + u = src(e) + v = dst(e) + u_comm = membership[u] + v_comm = membership[v] + + # for special case of undirected network + if !is_directed(g) + u_comm, v_comm = minmax(u_comm, v_comm) + end + + if haskey(collapsed_edge_weights[u_comm], v_comm) + collapsed_edge_weights[u_comm][v_comm] += 1 + else + collapsed_edge_weights[u_comm][v_comm] = 1 + end + end + + collapsed_graph = SimpleGraph(nb_comm) + collapsed_weights = Float64[] + + for u=1:nb_comm + for (v,w) in collapsed_edge_weights[u] + add_edge!(collapsed_graph, u, v) + push!(collapsed_weights, w) + end + end + + return collapsed_graph, collapsed_weights +end \ No newline at end of file diff --git a/src/lines.jl b/src/lines.jl index 4589641..8b1a857 100644 --- a/src/lines.jl +++ b/src/lines.jl @@ -180,7 +180,7 @@ function graphcurve(g::AbstractGraph{T}, locs_x, locs_y, nodesize::Vector{<:Real end # this function is copy from [IainNZ](https://github.com/IainNZ)'s [GraphLayout.jl](https://github.com/IainNZ/GraphLayout.jl) -function arrowcoords(θ, endx, endy, arrowlength, angleoffset=20.0/180.0*π) +function arrowcoords(θ, endx, endy, arrowlength, angleoffset=π/9) arr1x = endx - arrowlength*cos(θ+angleoffset) arr1y = endy - arrowlength*sin(θ+angleoffset) arr2x = endx - arrowlength*cos(θ-angleoffset) diff --git a/src/pienode.jl b/src/pienode.jl deleted file mode 100644 index dbafe5b..0000000 --- a/src/pienode.jl +++ /dev/null @@ -1,41 +0,0 @@ -# to do -mutable struct PIENODE - x::Float64 - y::Float64 - r::Float64 - prop::Vector{Float64} - colors - strokes -end - -function pie(pn::PIENODE) - p = [0.0;2pi*pn.prop/sum(pn.prop)] - θ = cumsum(p) - s = Vector[] - for i=1:length(θ)-1 - push!(s, sector(pn.x, pn.y, pn.r, θ[i], θ[i+1])) - end - compose(context(),path(s), fill(pn.colors), stroke(pn.strokes)) -end - -function sector(x, y, r, θ1, θ2) - if θ2-θ1<=pi - [:M, x+r*cos(θ1),y-r*sin(θ1), :A, r, r, 0, false, false, x+r*cos(θ2), y-r*sin(θ2), :L, x, y, :Z] - else - [ - :M, x+r*cos(θ1),y-r*sin(θ1), - :A, r, r, 0, false, false, x+r*cos(θ1+pi), y-r*sin(θ1+pi), - :A, r, r, 0, false, false, x+r*cos(θ2), y-r*sin(θ2), - :L, x, y, - :Z - ] - end -end - -function sector(x::Vector{T}, y::Vector{T}, r::Vector{T}, θ1::Vector{T}, θ2::Vector{T}) where {T} - s = Vector[] - for i=1:length(x) - push!(s, sector(x[i],y[i],r[i],θ1[i],θ2[i])) - end - s -end diff --git a/src/plot.jl b/src/plot.jl index af438b7..fe6e553 100644 --- a/src/plot.jl +++ b/src/plot.jl @@ -20,10 +20,10 @@ be obtained from `layout` kwarg. `layout` Layout algorithm. Currently can be one of [`random_layout`, `circular_layout`, `spring_layout`, `shell_layout`, `stressmajorize_layout`, -`spectral_layout`]. +`spectral_layout`, `community_layout`]. Default: `spring_layout` -`NODESIZE` +`max_nodesize` Max size for the nodes. Default: `3.0/sqrt(N)` `nodesize` @@ -39,10 +39,10 @@ Color for the node labels, can be a Vector. Default: `colorant"black"` Distances for the node labels from center of nodes. Default: `0.0` `nodelabelangleoffset` -Angle offset for the node labels. Default: `π/4.0` +Angle offset for the node labels (only used when `nodelabeldist` is not zero). Default: `π/4.0` -`NODELABELSIZE` -Largest fontsize for the vertice labels. Default: `4.0` +`max_nodelabelsize` +Largest fontsize for the vertex labels. Default: `4.0` `nodelabelsize` Relative fontsize for the vertice labels, can be a Vector. Default: `1.0` @@ -57,7 +57,7 @@ Color for the nodes stroke, can be a Vector. Default: `nothing` Line width for the nodes stroke, can be a Vector. Default: `0.0` `edgelabel` -Labels for the edges, a Vector or nothing. Default: `[]` +Labels for the edges, a Vector or nothing. Default: `nothing` `edgelabelc` Color for the edge labels, can be a Vector. Default: `colorant"black"` @@ -65,13 +65,13 @@ Color for the edge labels, can be a Vector. Default: `colorant"black"` `edgelabeldistx, edgelabeldisty` Distance for the edge label from center of edge. Default: `0.0` -`EDGELABELSIZE` +`max_edgelabelsize` Largest fontsize for the edge labels. Default: `4.0` `edgelabelsize` Relative fontsize for the edge labels, can be a Vector. Default: `1.0` -`EDGELINEWIDTH` +`max_edgelinewidth` Max line width for the edges. Default: `0.25/sqrt(N)` `edgelinewidth` @@ -88,7 +88,7 @@ Equal to 0 for undirected graphs. Default: `0.1` for the directed graphs Angular width in radians for the arrows. Default: `π/9 (20 degrees)` `linetype` -Type of line used for edges ("straight", "curve"). Default: "straight" +Type of line used for edges (:straight, :curve). Default: :straight `outangle` Angular width in radians for the edges (only used if `linetype = "curve`). @@ -100,37 +100,31 @@ function gplot(g::AbstractGraph{T}, nodelabel = nothing, nodelabelc = colorant"black", nodelabelsize = 1.0, - NODELABELSIZE = 4.0, + max_nodelabelsize = 4.0, nodelabeldist = 0.0, nodelabelangleoffset = π / 4.0, - edgelabel = [], + edgelabel = nothing, edgelabelc = colorant"black", edgelabelsize = 1.0, - EDGELABELSIZE = 4.0, + max_edgelabelsize = 4.0, edgestrokec = colorant"lightgray", edgelinewidth = 1.0, - EDGELINEWIDTH = 3.0 / sqrt(nv(g)), + max_edgelinewidth = 3.0 / sqrt(nv(g)), edgelabeldistx = 0.0, edgelabeldisty = 0.0, nodesize = 1.0, - NODESIZE = 0.25 / sqrt(nv(g)), + max_nodesize = 0.25 / sqrt(nv(g)), nodefillc = colorant"turquoise", nodestrokec = nothing, nodestrokelw = 0.0, arrowlengthfrac = is_directed(g) ? 0.1 : 0.0, arrowangleoffset = π / 9, - linetype = "straight", + linetype = :straight, outangle = π / 5) where {T <:Integer, R1 <: Real, R2 <: Real} - length(locs_x_in) != length(locs_y_in) && error("Vectors must be same length") - N = nv(g) - NE = ne(g) - if nodelabel != nothing && length(nodelabel) != N - error("Must have one label per node (or none)") - end - if !isempty(edgelabel) && length(edgelabel) != NE - error("Must have one label per edge (or none)") - end + @assert length(locs_x_in) == length(locs_y_in) == nv(g) "Position vectors must be of the same length as the number of nodes" + @assert isnothing(nodelabel) || length(nodelabel) == nv(g) "`nodelabel` must either be `nothing` or a vector of the same length as the number of nodes" + @assert isnothing(edgelabel) || length(edgelabel) == ne(g) "`edgelabel` must either be `nothing` or a vector of the same length as the number of edges" locs_x = Float64.(locs_x_in) locs_y = Float64.(locs_y_in) @@ -148,35 +142,18 @@ function gplot(g::AbstractGraph{T}, map!(z -> scaler(z, min_x, max_x), locs_x, locs_x) map!(z -> scaler(z, min_y, max_y), locs_y, locs_y) - # Determine sizes - #NODESIZE = 0.25/sqrt(N) - #LINEWIDTH = 3.0/sqrt(N) - - max_nodesize = NODESIZE / maximum(nodesize) - nodesize *= max_nodesize - max_edgelinewidth = EDGELINEWIDTH / maximum(edgelinewidth) - edgelinewidth *= max_edgelinewidth - max_edgelabelsize = EDGELABELSIZE / maximum(edgelabelsize) - edgelabelsize *= max_edgelabelsize - max_nodelabelsize = NODELABELSIZE / maximum(nodelabelsize) - nodelabelsize *= max_nodelabelsize + # Scale sizes + nodesize *= (max_nodesize / maximum(nodesize)) + edgelinewidth *= (max_edgelinewidth / maximum(edgelinewidth)) + edgelabelsize *= (max_edgelabelsize / maximum(edgelabelsize)) + nodelabelsize *= (max_nodelabelsize / maximum(nodelabelsize)) max_nodestrokelw = maximum(nodestrokelw) - if max_nodestrokelw > 0.0 - max_nodestrokelw = EDGELINEWIDTH / max_nodestrokelw - nodestrokelw *= max_nodestrokelw + if !iszero(max_nodestrokelw) + nodestrokelw *= (max_edgelinewidth / max_nodestrokelw) end # Create nodes - nodecircle = fill(0.4Compose.w, length(locs_x)) - if isa(nodesize, Real) - for i = 1:length(locs_x) - nodecircle[i] *= nodesize - end - else - for i = 1:length(locs_x) - nodecircle[i] *= nodesize[i] - end - end + nodecircle = fill(0.4Compose.w, nv(g)) .* nodesize nodes = circle(locs_x, locs_y, nodecircle) # Create node labels if provided @@ -190,7 +167,7 @@ function gplot(g::AbstractGraph{T}, end # Create edge labels if provided edgetexts = nothing - if !isempty(edgelabel) + if !isnothing(edgelabel) edge_locs_x = zeros(R, NE) edge_locs_y = zeros(R, NE) for (e_idx, e) in enumerate(edges(g)) @@ -198,17 +175,16 @@ function gplot(g::AbstractGraph{T}, j = dst(e) mid_x = (locs_x[i]+locs_x[j]) / 2.0 mid_y = (locs_y[i]+locs_y[j]) / 2.0 - edge_locs_x[e_idx] = (is_directed(g) ? (mid_x+locs_x[j]) / 2.0 : mid_x) + edgelabeldistx * NODESIZE - edge_locs_y[e_idx] = (is_directed(g) ? (mid_y+locs_y[j]) / 2.0 : mid_y) + edgelabeldisty * NODESIZE - + edge_locs_x[e_idx] = (is_directed(g) ? (mid_x+locs_x[j]) / 2.0 : mid_x) + edgelabeldistx * max_nodesize + edge_locs_y[e_idx] = (is_directed(g) ? (mid_y+locs_y[j]) / 2.0 : mid_y) + edgelabeldisty * max_nodesize end edgetexts = text(edge_locs_x, edge_locs_y, map(string, edgelabel), [hcenter], [vcenter]) end # Create lines and arrow heads lines, arrows = nothing, nothing - if linetype == "curve" - if arrowlengthfrac > 0.0 + if linetype == :curve + if !iszero(arrowlengthfrac) curves_cord, arrows_cord = graphcurve(g, locs_x, locs_y, nodesize, arrowlengthfrac, arrowangleoffset, outangle) lines = curve(curves_cord[:,1], curves_cord[:,2], curves_cord[:,3], curves_cord[:,4]) arrows = line(arrows_cord) @@ -217,7 +193,7 @@ function gplot(g::AbstractGraph{T}, lines = curve(curves_cord[:,1], curves_cord[:,2], curves_cord[:,3], curves_cord[:,4]) end else - if arrowlengthfrac > 0.0 + if !iszero(arrowlengthfrac) lines_cord, arrows_cord = graphline(g, locs_x, locs_y, nodesize, arrowlengthfrac, arrowangleoffset) lines = line(lines_cord) arrows = line(arrows_cord) @@ -227,12 +203,13 @@ function gplot(g::AbstractGraph{T}, end end + #build plot compose(context(units=UnitBox(-1.2, -1.2, +2.4, +2.4)), - compose(context(), texts, fill(nodelabelc), stroke(nothing), fontsize(nodelabelsize)), + compose(context(), texts, fill(nodelabelc), fontsize(nodelabelsize)), compose(context(), nodes, fill(nodefillc), stroke(nodestrokec), linewidth(nodestrokelw)), - compose(context(), edgetexts, fill(edgelabelc), stroke(nothing), fontsize(edgelabelsize)), + compose(context(), edgetexts, fill(edgelabelc), fontsize(edgelabelsize)), compose(context(), arrows, stroke(edgestrokec), linewidth(edgelinewidth)), - compose(context(), lines, stroke(edgestrokec), fill(nothing), linewidth(edgelinewidth))) + compose(context(), lines, stroke(edgestrokec), linewidth(edgelinewidth))) end function gplot(g; layout::Function=spring_layout, keyargs...) diff --git a/src/shape.jl b/src/shape.jl deleted file mode 100644 index a3e6549..0000000 --- a/src/shape.jl +++ /dev/null @@ -1,9 +0,0 @@ -# draw a regular n-gon -function ngon(x, y, r, n=3, θ=0.0) - αs = linspace(0,2pi,n+1)[1:end-1] - [(r*cos(α-θ)+x, r*sin(α-θ)+y) for α in αs] -end - -function ngon(xs::Vector, ys::Vector, rs::Vector, ns::Vector=fill(3,length(xs)), θs::Vector=zeros(length(xs))) - [ngon(xs[i], ys[i], rs[i], ns[i], θs[i]) for i=1:length(xs)] -end diff --git a/src/stress.jl b/src/stress.jl deleted file mode 100644 index f09c708..0000000 --- a/src/stress.jl +++ /dev/null @@ -1,175 +0,0 @@ -using LinearAlgebra - -# This layout algorithm is copy from [IainNZ](https://github.com/IainNZ)'s [GraphLayout.jl](https://github.com/IainNZ/GraphLayout.jl) -@doc """ -Compute graph layout using stress majorization - -Inputs: - - δ: Matrix of pairwise distances - p: Dimension of embedding (default: 2) - w: Matrix of weights. If not specified, defaults to - w[i,j] = δ[i,j]^-2 if δ[i,j] is nonzero, or 0 otherwise - X0: Initial guess for the layout. Coordinates are given in rows. - If not specified, default to random matrix of Gaussians - -Additional optional keyword arguments control the convergence of the algorithm -and the additional output as requested: - - maxiter: Maximum number of iterations. Default: 400size(X0, 1)^2 - abstols: Absolute tolerance for convergence of stress. - The iterations terminate if the difference between two - successive stresses is less than abstol. - Default: √(eps(eltype(X0)) - reltols: Relative tolerance for convergence of stress. - The iterations terminate if the difference between two - successive stresses relative to the current stress is less than - reltol. Default: √(eps(eltype(X0)) - abstolx: Absolute tolerance for convergence of layout. - The iterations terminate if the Frobenius norm of two successive - layouts is less than abstolx. Default: √(eps(eltype(X0)) - verbose: If true, prints convergence information at each iteration. - Default: false - returnall: If true, returns all iterates and their associated stresses. - If false (default), returns the last iterate - -Output: - - The final layout X, with coordinates given in rows, unless returnall=true. - -Reference: - - The main equation to solve is (8) of: - - @incollection{ - author = {Emden R Gansner and Yehuda Koren and Stephen North}, - title = {Graph Drawing by Stress Majorization} - year={2005}, - isbn={978-3-540-24528-5}, - booktitle={Graph Drawing}, - seriesvolume={3383}, - series={Lecture Notes in Computer Science}, - editor={Pach, J\'anos}, - doi={10.1007/978-3-540-31843-9_25}, - publisher={Springer Berlin Heidelberg}, - pages={239--250}, - } -""" -function stressmajorize_layout(g::AbstractGraph, - p::Int=2, - w=nothing, - X0=randn(nv(g), p); - maxiter = 400size(X0, 1)^2, - abstols=√(eps(eltype(X0))), - reltols=√(eps(eltype(X0))), - abstolx=√(eps(eltype(X0))), - verbose = false, - returnall = false) - - @assert size(X0, 2)==p - δ = fill(1.0, nv(g), nv(g)) - - if w == nothing - w = δ.^-2 - w[.!isfinite.(w)] .= 0 - end - - @assert size(X0, 1)==size(δ, 1)==size(δ, 2)==size(w, 1)==size(w, 2) - Lw = weightedlaplacian(w) - pinvLw = pinv(Lw) - newstress = stress(X0, δ, w) - Xs = Matrix[X0] - stresses = [newstress] - iter = 0 - for outer iter = 1:maxiter - #TODO the faster way is to drop the first row and col from the iteration - X = pinvLw * (LZ(X0, δ, w)*X0) - @assert all(isfinite.(X)) - newstress, oldstress = stress(X, δ, w), newstress - verbose && @info("""Iteration $iter - Change in coordinates: $(norm(X - X0)) - Stress: $newstress (change: $(newstress-oldstress)) - """) - push!(Xs, X) - push!(stresses, newstress) - abs(newstress - oldstress) < reltols * newstress && break - abs(newstress - oldstress) < abstols && break - norm(X - X0) < abstolx && break - X0 = X - end - iter == maxiter && @warn("Maximum number of iterations reached without convergence") - #returnall ? (Xs, stresses) : Xs[end] - Xs[end][:,1], Xs[end][:,2] -end - -@doc """ -Stress function to majorize - -Input: - X: A particular layout (coordinates in rows) - d: Matrix of pairwise distances - w: Weights for each pairwise distance - -See (1) of Reference -""" -function stress(X, d=fill(1.0, size(X, 1), size(X, 1)), w=nothing) - s = 0.0 - n = size(X, 1) - if w==nothing - w = d.^-2 - w[!isfinite.(w)] = 0 - end - @assert n==size(d, 1)==size(d, 2)==size(w, 1)==size(w, 2) - for j=1:n, i=1:j-1 - s += w[i, j] * (norm(X[i,:] - X[j,:]) - d[i,j])^2 - end - @assert isfinite(s) - return s -end - -@doc """ -Compute weighted Laplacian given ideal weights w - -Lʷ defined in (4) of the Reference -""" -function weightedlaplacian(w) - n = LinearAlgebra.checksquare(w) - T = eltype(w) - Lw = zeros(T, n, n) - for i=1:n - D = zero(T) - for j=1:n - i==j && continue - Lw[i, j] = -w[i, j] - D += w[i, j] - end - Lw[i, i] = D - end - return Lw -end - -@doc """ -Computes L^Z defined in (5) of the Reference - -Input: Z: current layout (coordinates) - d: Ideal distances (default: all 1) - w: weights (default: d.^-2) -""" -function LZ(Z, d, w) - n = size(Z, 1) - L = zeros(n, n) - for i=1:n - D = 0.0 - for j=1:n - i==j && continue - nrmz = norm(Z[i,:] - Z[j,:]) - nrmz==0 && continue - δ = w[i, j] * d[i, j] - L[i, j] = -δ/nrmz - D -= -δ/nrmz - end - L[i, i] = D - end - @assert all(isfinite.(L)) - L -end diff --git a/test/runtests.jl b/test/runtests.jl index f38f8d6..4014ed5 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -93,7 +93,7 @@ end add_edge!(g2, 1,2) add_edge!(g2, 2,1) - plot_and_save1(fname) = plot_and_save(fname, g2, linetype="curve") + plot_and_save1(fname) = plot_and_save(fname, g2, linetype=:curve) refimg1 = joinpath(datadir, "curve.png") @test test_images(VisualTest(plot_and_save1, refimg1), popup=!istravis) |> save_comparison |> success @@ -102,7 +102,7 @@ end add_edge!(g3, 1,2) add_edge!(g3, 2,1) - plot_and_save2(fname) = plot_and_save(fname, g3, linetype="curve") + plot_and_save2(fname) = plot_and_save(fname, g3, linetype=:curve) refimg2 = joinpath(datadir, "self_directed.png") @test test_images(VisualTest(plot_and_save2, refimg2), popup=!istravis) |> save_comparison |> success