diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bd8d7a9..e3942e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: fail-fast: false matrix: version: - - '1.3' # Replace this with the minimum Julia version that your package supports. E.g. if your package requires Julia 1.5 or higher, change this to '1.5'. + - '1.6' # Replace this with the minimum Julia version that your package supports. E.g. if your package requires Julia 1.5 or higher, change this to '1.5'. - '1' # Leave this line unchanged. '1' will automatically expand to the latest stable 1.x release of Julia. - 'nightly' os: diff --git a/Project.toml b/Project.toml index f6ed3ec..fde45b0 100644 --- a/Project.toml +++ b/Project.toml @@ -5,7 +5,6 @@ version = "0.5.2" [deps] ArnoldiMethod = "ec485272-7323-5ecc-a04f-4719b315124d" -ColorTypes = "3da002f7-5984-5a60-b8a6-cbb66c0b333f" Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" Compose = "a81c6b42-2e10-5240-aca2-a61377ecd94b" DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab" @@ -15,9 +14,8 @@ Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" [compat] -ArnoldiMethod = "0.0.4, 0.1, 0.2" -ColorTypes = "0.9, 0.10, 0.11" -Colors = "0.11, 0.12" -Compose = "0.8, 0.9" +ArnoldiMethod = "0.2" +Colors = "0.12" +Compose = "0.9" Graphs = "1.4" -julia = "1.3" +julia = "1.6" diff --git a/README.md b/README.md index e5dd530..7e3485b 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,8 @@ draw(PDF("karate.pdf", 16cm, 16cm), gplot(g)) draw(PNG("karate.png", 16cm, 16cm), gplot(g)) # save to svg draw(SVG("karate.svg", 16cm, 16cm), gplot(g)) +# alternate way of saving to svg without loading Compose +saveplot(gplot(g, plot_size = (16cm, 16cm)), "karate.svg") ``` # Graphs.jl integration ```julia @@ -160,6 +162,10 @@ gplot(h) # Keyword Arguments + `layout` Layout algorithm: `random_layout`, `circular_layout`, `spring_layout`, `shell_layout`, `stressmajorize_layout`, `spectral_layout`. Default: `spring_layout` ++ `title` Plot title. Default: `""` ++ `title_color` Plot title color. Default: `colorant"black"` ++ `title_size` Plot title size. Default: `4.0` ++ `font_family` Font family for all text. Default: `"Helvetica"` + `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` @@ -183,7 +189,10 @@ gplot(h) + `arrowangleoffset` Angular width in radians for the arrows. Default: `π/9 (20 degrees)` + `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)` - ++ `background_color` Color for the plot background. Default: `nothing` ++ `plot_size` Tuple of measures for width x height of plot area. Default: `(10cm, 10cm)` ++ `leftpad, rightpad, toppad, bottompad` Padding for the plot margins. Default: `0mm` ++ `pad` Padding for plot margins (overrides individual padding if given). Default: `nothing` # Reporting Bugs Filing an issue to report a bug, counterintuitive behavior, or even to request a feature is extremely valuable in helping me prioritize what to work on, so don't hestitate. diff --git a/src/GraphPlot.jl b/src/GraphPlot.jl index c1738c1..b42189d 100644 --- a/src/GraphPlot.jl +++ b/src/GraphPlot.jl @@ -15,7 +15,9 @@ export spring_layout, spectral_layout, shell_layout, - stressmajorize_layout + stressmajorize_layout, + saveplot, + mm, cm, inch include("deprecations.jl") diff --git a/src/layout.jl b/src/layout.jl index 2a9956b..f588445 100644 --- a/src/layout.jl +++ b/src/layout.jl @@ -102,11 +102,11 @@ julia> locs_x, locs_y = spring_layout(g) ``` """ function spring_layout(g::AbstractGraph, - locs_x=2*rand(nv(g)).-1.0, - locs_y=2*rand(nv(g)).-1.0; + locs_x_in::AbstractVector{R1}=2*rand(nv(g)).-1.0, + locs_y_in::AbstractVector{R2}=2*rand(nv(g)).-1.0; C=2.0, MAXITER=100, - INITTEMP=2.0) + INITTEMP=2.0) where {R1 <: Real, R2 <: Real} nvg = nv(g) adj_matrix = adjacency_matrix(g) @@ -119,6 +119,10 @@ function spring_layout(g::AbstractGraph, force_x = zeros(nvg) force_y = zeros(nvg) + # Convert locs to float + locs_x = convert(Vector{Float64}, locs_x_in) + locs_y = convert(Vector{Float64}, locs_y_in) + # Iterate MAXITER times @inbounds for iter = 1:MAXITER # Calculate forces @@ -174,7 +178,7 @@ end using Random: MersenneTwister -function spring_layout(g::AbstractGraph, seed::Integer, kws...) +function spring_layout(g::AbstractGraph, seed::Integer; kws...) rng = MersenneTwister(seed) spring_layout(g, 2 .* rand(rng, nv(g)) .- 1.0, 2 .* rand(rng,nv(g)) .- 1.0; kws...) end @@ -205,20 +209,20 @@ function shell_layout(g, nlist::Union{Nothing, Vector{Vector{Int}}} = nothing) if nv(g) == 1 return [0.0], [0.0] end - if nlist == nothing + if isnothing(nlist) nlist = [collect(1:nv(g))] end radius = 0.0 if length(nlist[1]) > 1 radius = 1.0 end - locs_x = Float64[] - locs_y = Float64[] + locs_x = zeros(nv(g)) + locs_y = zeros(nv(g)) for nodes in nlist # Discard the extra angle since it matches 0 radians. θ = range(0, stop=2pi, length=length(nodes)+1)[1:end-1] - append!(locs_x, radius*cos.(θ)) - append!(locs_y, radius*sin.(θ)) + locs_x[nodes] = radius*cos.(θ) + locs_y[nodes] = radius*sin.(θ) radius += 1.0 end return locs_x, locs_y diff --git a/src/lines.jl b/src/lines.jl index 4589641..08a3cb8 100644 --- a/src/lines.jl +++ b/src/lines.jl @@ -1,10 +1,34 @@ """ Return lines and arrow heads """ -function graphline(g, locs_x, locs_y, nodesize::Vector{T}, arrowlength, angleoffset) where {T<:Real} - lines = Array{Vector{Tuple{Float64,Float64}}}(undef, ne(g)) - arrows = Array{Vector{Tuple{Float64,Float64}}}(undef, ne(g)) - for (e_idx, e) in enumerate(edges(g)) +function midpoint(pt1,pt2) + x = (pt1[1] + pt2[1]) / 2 + y = (pt1[2] + pt2[2]) / 2 + return x,y +end + +function interpolate_bezier(x::Vector,t) + #TODO: since this is only being used for `curve` which has 4 points (n = 3), the calculation can be simplified for this case. + n = length(x)-1 + x_loc = sum(binomial(n,i)*(1-t)^(n-i)*t^i*x[i+1][1] for i in 0:n) + y_loc = sum(binomial(n,i)*(1-t)^(n-i)*t^i*x[i+1][2] for i in 0:n) + return x_loc.value, y_loc.value +end + +interpolate_bezier(x::Compose.CurvePrimitive,t) = + interpolate_bezier([x.anchor0, x.ctrl0, x.ctrl1, x.anchor1], t) + +function interpolate_line(locs_x,locs_y,i,j,t) + x_loc = locs_x[i] + (locs_x[j]-locs_x[i])*t + y_loc = locs_y[i] + (locs_y[j]-locs_y[i])*t + return x_loc, y_loc +end + +function graphline(edge_list, locs_x, locs_y, nodesize::Vector{T}, arrowlength, angleoffset) where {T<:Real} + num_edges = length(edge_list) + lines = Array{Vector{Tuple{Float64,Float64}}}(undef, num_edges) + arrows = Array{Vector{Tuple{Float64,Float64}}}(undef, num_edges) + for (e_idx, e) in enumerate(edge_list) i = src(e) j = dst(e) Δx = locs_x[j] - locs_x[i] @@ -14,17 +38,24 @@ function graphline(g, locs_x, locs_y, nodesize::Vector{T}, arrowlength, angleoff starty = locs_y[i] + nodesize[i]*sin(θ) endx = locs_x[j] + nodesize[j]*cos(θ+π) endy = locs_y[j] + nodesize[j]*sin(θ+π) - lines[e_idx] = [(startx, starty), (endx, endy)] arr1, arr2 = arrowcoords(θ, endx, endy, arrowlength, angleoffset) + endx0, endy0 = midpoint(arr1, arr2) + e_idx2 = findfirst(==(Edge(j,i)), collect(edge_list)) #get index of reverse arc + if !isnothing(e_idx2) && e_idx2 < e_idx #only make changes if lines/arrows have already been defined for that arc + startx, starty = midpoint(arrows[e_idx2][[1,3]]...) #get midopint of reverse arc and use as new start point + lines[e_idx2][1] = (endx0, endy0) #update endpoint of reverse arc + end + lines[e_idx] = [(startx, starty), (endx0, endy0)] arrows[e_idx] = [arr1, (endx, endy), arr2] end lines, arrows end -function graphline(g::AbstractGraph{T}, locs_x, locs_y, nodesize::Real, arrowlength, angleoffset) where {T<:Integer} - lines = Array{Vector{Tuple{Float64,Float64}}}(undef, ne(g)) - arrows = Array{Vector{Tuple{Float64,Float64}}}(undef, ne(g)) - for (e_idx, e) in enumerate(edges(g)) +function graphline(edge_list, locs_x, locs_y, nodesize::Real, arrowlength, angleoffset) + num_edges = length(edge_list) + lines = Array{Vector{Tuple{Float64,Float64}}}(undef, num_edges) + arrows = Array{Vector{Tuple{Float64,Float64}}}(undef, num_edges) + for (e_idx, e) in enumerate(edge_list) i = src(e) j = dst(e) Δx = locs_x[j] - locs_x[i] @@ -34,16 +65,23 @@ function graphline(g::AbstractGraph{T}, locs_x, locs_y, nodesize::Real, arrowlen starty = locs_y[i] + nodesize*sin(θ) endx = locs_x[j] + nodesize*cos(θ+π) endy = locs_y[j] + nodesize*sin(θ+π) - lines[e_idx] = [(startx, starty), (endx, endy)] arr1, arr2 = arrowcoords(θ, endx, endy, arrowlength, angleoffset) + endx0, endy0 = midpoint(arr1, arr2) + e_idx2 = findfirst(==(Edge(j,i)), collect(edge_list)) #get index of reverse arc + if !isnothing(e_idx2) && e_idx2 < e_idx #only make changes if lines/arrows have already been defined for that arc + startx, starty = midpoint(arrows[e_idx2][[1,3]]...) #get midopint of reverse arc and use as new start point + lines[e_idx2][1] = (endx0, endy0) #update endpoint of reverse arc + end + lines[e_idx] = [(startx, starty), (endx0, endy0)] arrows[e_idx] = [arr1, (endx, endy), arr2] end lines, arrows end -function graphline(g::AbstractGraph{T}, locs_x, locs_y, nodesize::Vector{<:Real}) where {T<:Integer} - lines = Array{Vector{Tuple{Float64,Float64}}}(undef, ne(g)) - for (e_idx, e) in enumerate(edges(g)) +function graphline(edge_list, locs_x, locs_y, nodesize::Vector{T}) where {T<:Real} + num_edges = length(edge_list) + lines = Array{Vector{Tuple{Float64,Float64}}}(undef, num_edges) + for (e_idx, e) in enumerate(edge_list) i = src(e) j = dst(e) Δx = locs_x[j] - locs_x[i] @@ -58,9 +96,10 @@ function graphline(g::AbstractGraph{T}, locs_x, locs_y, nodesize::Vector{<:Real} lines end -function graphline(g::AbstractGraph{T}, locs_x, locs_y, nodesize::Real) where {T<:Integer} - lines = Array{Vector{Tuple{Float64,Float64}}}(undef, ne(g)) - for (e_idx, e) in enumerate(edges(g)) +function graphline(edge_list, locs_x, locs_y, nodesize::Real) + num_edges = length(edge_list) + lines = Array{Vector{Tuple{Float64,Float64}}}(undef, num_edges) + for (e_idx, e) in enumerate(edge_list) i = src(e) j = dst(e) Δx = locs_x[j] - locs_x[i] @@ -75,10 +114,11 @@ function graphline(g::AbstractGraph{T}, locs_x, locs_y, nodesize::Real) where {T return lines end -function graphcurve(g::AbstractGraph{T}, locs_x, locs_y, nodesize::Vector{<:Real}, arrowlength, angleoffset, outangle=pi/5) where {T<:Integer} - curves = Matrix{Tuple{Float64,Float64}}(undef, ne(g), 4) - arrows = Array{Vector{Tuple{Float64,Float64}}}(undef, ne(g)) - for (e_idx, e) in enumerate(edges(g)) +function graphcurve(edge_list, locs_x, locs_y, nodesize::Vector{T}, arrowlength, angleoffset, outangle=pi/5) where {T<:Real} + num_edges = length(edge_list) + curves = Matrix{Tuple{Float64,Float64}}(undef, num_edges, 4) + arrows = Array{Vector{Tuple{Float64,Float64}}}(undef, num_edges) + for (e_idx, e) in enumerate(edge_list) i = src(e) j = dst(e) Δx = locs_x[j] - locs_x[i] @@ -95,18 +135,20 @@ function graphcurve(g::AbstractGraph{T}, locs_x, locs_y, nodesize::Vector{<:Real d = 2 * π * nodesize[i] end - curves[e_idx, :] = curveedge(startx, starty, endx, endy, θ, outangle, d) - arr1, arr2 = arrowcoords(θ-outangle, endx, endy, arrowlength, angleoffset) + endx0 = (arr1[1] + arr2[1]) / 2 + endy0 = (arr1[2] + arr2[2]) / 2 + curves[e_idx, :] = curveedge(startx, starty, endx0, endy0, θ, outangle, d) arrows[e_idx] = [arr1, (endx, endy), arr2] end return curves, arrows end -function graphcurve(g, locs_x, locs_y, nodesize::Real, arrowlength, angleoffset, outangle=pi/5) - curves = Matrix{Tuple{Float64,Float64}}(undef, ne(g), 4) - arrows = Array{Vector{Tuple{Float64,Float64}}}(undef, ne(g)) - for (e_idx, e) in enumerate(edges(g)) +function graphcurve(edge_list, locs_x, locs_y, nodesize::Real, arrowlength, angleoffset, outangle=pi/5) + num_edges = length(edge_list) + curves = Matrix{Tuple{Float64,Float64}}(undef, num_edges, 4) + arrows = Array{Vector{Tuple{Float64,Float64}}}(undef, num_edges) + for (e_idx, e) in enumerate(edge_list) i = src(e) j = dst(e) Δx = locs_x[j] - locs_x[i] @@ -123,17 +165,19 @@ function graphcurve(g, locs_x, locs_y, nodesize::Real, arrowlength, angleoffset, d = 2 * π * nodesize end - curves[e_idx, :] = curveedge(startx, starty, endx, endy, θ, outangle, d) - arr1, arr2 = arrowcoords(θ-outangle, endx, endy, arrowlength, angleoffset) + endx0 = (arr1[1] + arr2[1]) / 2 + endy0 = (arr1[2] + arr2[2]) / 2 + curves[e_idx, :] = curveedge(startx, starty, endx0, endy0, θ, outangle, d) arrows[e_idx] = [arr1, (endx, endy), arr2] end return curves, arrows end -function graphcurve(g, locs_x, locs_y, nodesize::Real, outangle) - curves = Matrix{Tuple{Float64,Float64}}(undef, ne(g), 4) - for (e_idx, e) in enumerate(edges(g)) +function graphcurve(edge_list, locs_x, locs_y, nodesize::Real, outangle) + num_edges = length(edge_list) + curves = Matrix{Tuple{Float64,Float64}}(undef, num_edges, 4) + for (e_idx, e) in enumerate(edge_list) i = src(e) j = dst(e) Δx = locs_x[j] - locs_x[i] @@ -155,9 +199,10 @@ function graphcurve(g, locs_x, locs_y, nodesize::Real, outangle) return curves end -function graphcurve(g::AbstractGraph{T}, locs_x, locs_y, nodesize::Vector{<:Real}, outangle) where {T<:Integer} - curves = Matrix{Tuple{Float64,Float64}}(undef, ne(g), 4) - for (e_idx, e) in enumerate(edges(g)) +function graphcurve(edge_list, locs_x, locs_y, nodesize::Vector{T}, outangle) where {T<:Real} + num_edges = length(edge_list) + curves = Matrix{Tuple{Float64,Float64}}(undef, num_edges, 4) + for (e_idx, e) in enumerate(edge_list) i = src(e) j = dst(e) Δx = locs_x[j] - locs_x[i] @@ -201,3 +246,40 @@ function curveedge(x1, y1, x2, y2, θ, outangle, d; k=0.5) return [(x1,y1) (xc1, yc1) (xc2, yc2) (x2, y2)] end + +function build_curved_edges(edge_list, locs_x, locs_y, nodesize, arrowlengthfrac, arrowangleoffset, outangle) + if arrowlengthfrac > 0.0 + curves_cord, arrows_cord = graphcurve(edge_list, locs_x, locs_y, nodesize, arrowlengthfrac, arrowangleoffset, outangle) + curves = curve(curves_cord[:,1], curves_cord[:,2], curves_cord[:,3], curves_cord[:,4]) + carrows = polygon(arrows_cord) + else + curves_cord = graphcurve(edge_list, locs_x, locs_y, nodesize, outangle) + curves = curve(curves_cord[:,1], curves_cord[:,2], curves_cord[:,3], curves_cord[:,4]) + carrows = nothing + end + + return curves, carrows +end + +function build_straight_edges(edge_list, locs_x, locs_y, nodesize, arrowlengthfrac, arrowangleoffset) + if arrowlengthfrac > 0.0 + lines_cord, arrows_cord = graphline(edge_list, locs_x, locs_y, nodesize, arrowlengthfrac, arrowangleoffset) + lines = line(lines_cord) + larrows = polygon(arrows_cord) + else + lines_cord = graphline(edge_list, locs_x, locs_y, nodesize) + lines = line(lines_cord) + larrows = nothing + end + + return lines, larrows +end + +function build_straight_curved_edges(g, locs_x, locs_y, nodesize, arrowlengthfrac, arrowangleoffset, outangle) + edge_list1 = filter(e -> src(e) != dst(e), collect(edges(g))) + edge_list2 = filter(e -> src(e) == dst(e), collect(edges(g))) + lines, larrows = build_straight_edges(edge_list1, locs_x, locs_y, nodesize, arrowlengthfrac, arrowangleoffset) + curves, carrows = build_curved_edges(edge_list2, locs_x, locs_y, nodesize, arrowlengthfrac, arrowangleoffset, outangle) + + return lines, larrows, curves, carrows +end \ No newline at end of file diff --git a/src/plot.jl b/src/plot.jl index af438b7..d29b30a 100644 --- a/src/plot.jl +++ b/src/plot.jl @@ -23,6 +23,18 @@ Layout algorithm. Currently can be one of [`random_layout`, `spectral_layout`]. Default: `spring_layout` +`title` +Plot title. Default: `""` + +`title_color` +Plot title color. Default: `colorant"black"` + +`title_size` +Plot title size. Default: `4.0` + +`font_family` +Font family for all text. Default: `"Helvetica"` + `NODESIZE` Max size for the nodes. Default: `3.0/sqrt(N)` @@ -42,10 +54,10 @@ Distances for the node labels from center of nodes. Default: `0.0` Angle offset for the node labels. Default: `π/4.0` `NODELABELSIZE` -Largest fontsize for the vertice labels. Default: `4.0` +Largest fontsize for the vertex labels. Default: `4.0` `nodelabelsize` -Relative fontsize for the vertice labels, can be a Vector. Default: `1.0` +Relative fontsize for the vertex labels, can be a Vector. Default: `1.0` `nodefillc` Color to fill the nodes with, can be a Vector. Default: `colorant"turquoise"` @@ -94,9 +106,24 @@ Type of line used for edges ("straight", "curve"). Default: "straight" Angular width in radians for the edges (only used if `linetype = "curve`). Default: `π/5 (36 degrees)` +`background_color` +Color for the plot background. Default: `nothing` + +`plot_size` +Tuple of measures for width x height for plot area. Default: `(10cm, 10cm)` + +`leftpad, rightpad, toppad, bottompad` +Padding for the plot margins. Default: `0mm` + +`pad` +Padding for plot margins (overrides individual padding if given). Default: `nothing` """ function gplot(g::AbstractGraph{T}, - locs_x_in::Vector{R1}, locs_y_in::Vector{R2}; + locs_x_in::AbstractVector{R1}, locs_y_in::AbstractVector{R2}; + title = "", + title_color = colorant"black", + title_size = 4.0, + font_family = "Helvetica", nodelabel = nothing, nodelabelc = colorant"black", nodelabelsize = 1.0, @@ -120,20 +147,28 @@ function gplot(g::AbstractGraph{T}, arrowlengthfrac = is_directed(g) ? 0.1 : 0.0, arrowangleoffset = π / 9, linetype = "straight", - outangle = π / 5) where {T <:Integer, R1 <: Real, R2 <: Real} + outangle = π / 5, + background_color = nothing, + plot_size = (10cm, 10cm), + leftpad = 0mm, + rightpad = 0mm, + toppad = 0mm, + bottompad = 0mm, + pad = nothing + ) 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 + if !isnothing(nodelabel) && 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 - locs_x = Float64.(locs_x_in) - locs_y = Float64.(locs_y_in) + locs_x = convert(Vector{Float64}, locs_x_in) + locs_y = convert(Vector{Float64}, locs_y_in) # Scale to unit square min_x, max_x = extrema(locs_x) @@ -167,72 +202,92 @@ function gplot(g::AbstractGraph{T}, end # Create nodes - nodecircle = fill(0.4Compose.w, length(locs_x)) + nodecircle = fill(0.4*2.4, length(locs_x)) #40% of the width of the unit box 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 + for i = 1:length(locs_x) + nodecircle[i] *= nodesize + end + else + for i = 1:length(locs_x) + nodecircle[i] *= nodesize[i] + end + end nodes = circle(locs_x, locs_y, nodecircle) # Create node labels if provided texts = nothing - if nodelabel != nothing + if !isnothing(nodelabel) text_locs_x = deepcopy(locs_x) text_locs_y = deepcopy(locs_y) texts = text(text_locs_x .+ nodesize .* (nodelabeldist * cos(nodelabelangleoffset)), text_locs_y .- nodesize .* (nodelabeldist * sin(nodelabelangleoffset)), map(string, nodelabel), [hcenter], [vcenter]) end + + # Create lines and arrow heads + lines, larrows = nothing, nothing + curves, carrows = nothing, nothing + if linetype == "curve" + curves, carrows = build_curved_edges(edges(g), locs_x, locs_y, nodesize, arrowlengthfrac, arrowangleoffset, outangle) + elseif has_self_loops(g) + lines, larrows, curves, carrows = build_straight_curved_edges(g, locs_x, locs_y, nodesize, arrowlengthfrac, arrowangleoffset, outangle) + else + lines, larrows = build_straight_edges(edges(g), locs_x, locs_y, nodesize, arrowlengthfrac, arrowangleoffset) + end + # Create edge labels if provided edgetexts = nothing if !isempty(edgelabel) - edge_locs_x = zeros(R, NE) - edge_locs_y = zeros(R, NE) + edge_locs_x = zeros(R1, NE) + edge_locs_y = zeros(R2, NE) + self_loop_idx = 1 for (e_idx, e) in enumerate(edges(g)) - i = src(e) - 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 - + i, j = src(e), dst(e) + if linetype == "curve" + mid_x, mid_y = interpolate_bezier(curves.primitives[e_idx], 0.5) + elseif src(e) == dst(e) + mid_x, mid_y = interpolate_bezier(curves.primitives[self_loop_idx], 0.5) + self_loop_idx += 1 + else + mid_x, mid_y = interpolate_line(locs_x,locs_y,i,j,0.5) + end + edge_locs_x[e_idx] = mid_x + edgelabeldistx * NODESIZE + edge_locs_y[e_idx] = mid_y + edgelabeldisty * 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 - 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) - else - curves_cord = graphcurve(g, locs_x, locs_y, nodesize, outangle) - lines = curve(curves_cord[:,1], curves_cord[:,2], curves_cord[:,3], curves_cord[:,4]) - end - else - if arrowlengthfrac > 0.0 - lines_cord, arrows_cord = graphline(g, locs_x, locs_y, nodesize, arrowlengthfrac, arrowangleoffset) - lines = line(lines_cord) - arrows = line(arrows_cord) - else - lines_cord = graphline(g, locs_x, locs_y, nodesize) - lines = line(lines_cord) - end + # Set plot_size + if length(plot_size) != 2 || !isa(plot_size[1], Compose.AbsoluteLength) || !isa(plot_size[2], Compose.AbsoluteLength) + error("`plot_size` must be a Tuple of lengths") + end + Compose.set_default_graphic_size(plot_size...) + + # Plot title + title_offset = isempty(title) ? 0 : 0.1*title_size/4 #Fix title offset + title = text(0, -1.2 - title_offset/2, title, hcenter, vcenter) + + # Plot padding + if !isnothing(pad) + leftpad, rightpad, toppad, bottompad = pad, pad, pad, pad end - compose(context(units=UnitBox(-1.2, -1.2, +2.4, +2.4)), - compose(context(), texts, fill(nodelabelc), stroke(nothing), fontsize(nodelabelsize)), - compose(context(), nodes, fill(nodefillc), stroke(nodestrokec), linewidth(nodestrokelw)), - compose(context(), edgetexts, fill(edgelabelc), stroke(nothing), fontsize(edgelabelsize)), - compose(context(), arrows, stroke(edgestrokec), linewidth(edgelinewidth)), - compose(context(), lines, stroke(edgestrokec), fill(nothing), linewidth(edgelinewidth))) + # Plot area size + plot_area = (-1.2, -1.2 - title_offset, +2.4, +2.4 + title_offset) + + # Build figure + compose( + context(units=UnitBox(plot_area...; leftpad, rightpad, toppad, bottompad)), + compose(context(), title, fill(title_color), fontsize(title_size), font(font_family)), + compose(context(), texts, fill(nodelabelc), fontsize(nodelabelsize), font(font_family)), + compose(context(), nodes, fill(nodefillc), stroke(nodestrokec), linewidth(nodestrokelw)), + compose(context(), edgetexts, fill(edgelabelc), fontsize(edgelabelsize)), + compose(context(), larrows, fill(edgestrokec)), + compose(context(), carrows, fill(edgestrokec)), + compose(context(), lines, stroke(edgestrokec), linewidth(edgelinewidth)), + compose(context(), curves, stroke(edgestrokec), linewidth(edgelinewidth)), + compose(context(units=UnitBox(plot_area...)), rectangle(plot_area...), fill(background_color)) + ) end function gplot(g; layout::Function=spring_layout, keyargs...) @@ -284,3 +339,8 @@ function gplothtml(args...; keyargs...) close(output) open_file(filename) end + +function saveplot(gplot::Compose.Context, filename::String) + draw(SVG(filename), gplot) + return nothing +end \ No newline at end of file diff --git a/test/data/curve.png b/test/data/curve.png index 668ca01..608243c 100644 Binary files a/test/data/curve.png and b/test/data/curve.png differ diff --git a/test/data/karate_background_color.png b/test/data/karate_background_color.png new file mode 100644 index 0000000..430e108 Binary files /dev/null and b/test/data/karate_background_color.png differ diff --git a/test/data/karate_straight_directed.png b/test/data/karate_straight_directed.png index cc32e09..5ec1840 100644 Binary files a/test/data/karate_straight_directed.png and b/test/data/karate_straight_directed.png differ diff --git a/test/data/self_directed.png b/test/data/self_directed.png index 1eb1177..77a5d03 100644 Binary files a/test/data/self_directed.png and b/test/data/self_directed.png differ diff --git a/test/runtests.jl b/test/runtests.jl index f38f8d6..3c8e45e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -67,7 +67,7 @@ end @test test_images(VisualTest(plot_and_save1, refimg1), popup=!istravis) |> save_comparison |> success # test directed graph - plot_and_save2(fname) = plot_and_save(fname, g, arrowlengthfrac=0.02, nodelabel=nodelabel) + plot_and_save2(fname) = plot_and_save(fname, g, arrowlengthfrac=0.05, nodelabel=nodelabel, font_family="Sans") refimg2 = joinpath(datadir, "karate_straight_directed.png") @test test_images(VisualTest(plot_and_save2, refimg2), popup=!istravis) |> save_comparison |> success @@ -78,6 +78,11 @@ end plot_and_save3(fname) = plot_and_save(fname, g, nodelabel=nodelabel, nodefillc=nodefillc) refimg3 = joinpath(datadir, "karate_groups.png") @test test_images(VisualTest(plot_and_save3, refimg3), popup=!istravis) |> save_comparison |> success + + # test background color + plot_and_save4(fname) = plot_and_save(fname, g, background_color=colorant"lightyellow") + refimg4 = joinpath(datadir, "karate_background_color.png") + @test test_images(VisualTest(plot_and_save4, refimg4), popup=!istravis) |> save_comparison |> success end @testset "WheelGraph" begin @@ -93,7 +98,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", arrowlengthfrac=0.2, pad=5mm) refimg1 = joinpath(datadir, "curve.png") @test test_images(VisualTest(plot_and_save1, refimg1), popup=!istravis) |> save_comparison |> success @@ -102,8 +107,40 @@ 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", arrowlengthfrac=0.2, leftpad=20mm, toppad=3mm, bottompad=3mm) refimg2 = joinpath(datadir, "self_directed.png") @test test_images(VisualTest(plot_and_save2, refimg2), popup=!istravis) |> save_comparison |> success end + +@testset "Spring Layout" begin + g1 = path_digraph(3) + x1, y1 = spring_layout(g1, 0; C = 1) + @test all(isapprox.(x1, [1.0, -0.014799825222963192, -1.0])) + @test all(isapprox.(y1, [-1.0, 0.014799825222963303, 1.0])) +end + +@testset "Circular Layout" begin + #single node + g1 = SimpleGraph(1) + x1,y1 = circular_layout(g1) + @test iszero(x1) + @test iszero(y1) + #2 nodes + g2 = SimpleGraph(2) + x2,y2 = circular_layout(g2) + @test all(isapprox.(x2, [1.0, -1.0])) + @test all(isapprox.(y2, [0.0, 1.2246467991473532e-16])) +end + +@testset "Shell Layout" begin + #continuous nlist + g = SimpleGraph(6) + x1,y1 = shell_layout(g,[[1,2,3],[4,5,6]]) + @test all(isapprox.(x1, [1.0, -0.4999999999999998, -0.5000000000000004, 2.0, -0.9999999999999996, -1.0000000000000009])) + @test all(isapprox.(y1, [0.0, 0.8660254037844387, -0.8660254037844385, 0.0, 1.7320508075688774, -1.732050807568877])) + #skipping positions + x2,y2 = shell_layout(g,[[1,3,5],[2,4,6]]) + @test all(isapprox.(x2, [1.0, 2.0, -0.4999999999999998, -0.9999999999999996, -0.5000000000000004, -1.0000000000000009])) + @test all(isapprox.(y2, [0.0, 0.0, 0.8660254037844387, 1.7320508075688774, -0.8660254037844385, -1.732050807568877])) +end \ No newline at end of file