Skip to content

Add requested features #186

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 59 commits into from
May 12, 2024
Merged
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
3368e89
Improve Readme and docstring
hdavid16 Jul 21, 2022
febb53b
fix #166
hdavid16 Jul 21, 2022
9bffdd9
Fix #175 to enable plotting to html
hdavid16 Jul 21, 2022
080a1a4
Document the use of gplothtml in README
hdavid16 Jul 21, 2022
3d32d07
Update open_file
hdavid16 Jul 21, 2022
2c64095
Minor changes:
hdavid16 Jul 25, 2022
222fb26
add TagBot to repo
hdavid16 Jul 26, 2022
dfcdf19
Merge branch 'JuliaGraphs:master' into master
hdavid16 Jul 26, 2022
14d62b4
Fix #172. set background color (`backgroundc` kwarg):
hdavid16 Jul 26, 2022
8274db2
add compose child object for background (instead of main level)
hdavid16 Jul 27, 2022
074ce86
rename to `background_color` + add test
hdavid16 Jul 27, 2022
4c1f900
fixed #149
hdavid16 Jul 27, 2022
4244133
support changing plot size (fixes #94, #147)
hdavid16 Jul 28, 2022
8347d86
update default `plot_size` to Compose.jl default
hdavid16 Aug 2, 2022
36a8005
Fix #107
hdavid16 Aug 2, 2022
c2f7079
scale title margin with title font size
hdavid16 Aug 2, 2022
f1f2760
Fix #160 make self-loop edges curved
hdavid16 Aug 2, 2022
f0015ca
add padding option for margins
hdavid16 Aug 2, 2022
4f72c07
Fix #154
hdavid16 Aug 2, 2022
6a3eaeb
Merge pull request #1 from hdavid16/fix_shell_layout
hdavid16 Aug 5, 2022
4a1002d
Merge pull request #2 from hdavid16/fix_spring_layout
hdavid16 Aug 5, 2022
bbdc0d3
Merge pull request #3 from hdavid16/add_title
hdavid16 Aug 5, 2022
d2e7f22
Merge branch 'up' into self-loops-curve
hdavid16 Aug 5, 2022
4c16357
Merge pull request #4 from hdavid16/self-loops-curve
hdavid16 Aug 5, 2022
37e7b81
update error msg
hdavid16 Aug 5, 2022
68cbd0a
Merge branch 'up' into set_size
hdavid16 Aug 5, 2022
935aae9
Merge pull request #5 from hdavid16/set_size
hdavid16 Aug 5, 2022
04fa157
Merge branch 'up' into background_color
hdavid16 Aug 5, 2022
3366a89
Merge pull request #7 from hdavid16/background_color
hdavid16 Aug 5, 2022
af16ff1
update background rectangle to cover padded area
hdavid16 Aug 5, 2022
c16f8ee
add conversion to floats for input locations to avoid error when Ints…
hdavid16 Aug 9, 2022
fb074d9
use float instead of Float64
hdavid16 Aug 9, 2022
905f710
add tests for layouts
hdavid16 Aug 9, 2022
9aafb86
update compat; remove ColorTypes
hdavid16 Aug 11, 2022
26cdd26
avoid unnecessary allocation if locs are already Floats
hdavid16 Aug 11, 2022
a7c7b11
avoid allocation in gplot if locs are floats.
hdavid16 Aug 11, 2022
d3fbc96
update ci.yml to julia 1.6
hdavid16 Aug 12, 2022
d6c10c9
bug fix
hdavid16 Aug 12, 2022
29f9eb0
add `pad` kwarg to override indvidual paddings
hdavid16 Aug 22, 2022
37ee922
make lines more robust when self-loops involved
hdavid16 Aug 22, 2022
c391c55
remove deps to LinAlg and SparseArrays now that not needed for mixed …
hdavid16 Aug 23, 2022
b86848e
remove using statements
hdavid16 Aug 23, 2022
efb5e05
Revert "remove deps to LinAlg and SparseArrays now that not needed fo…
hdavid16 Aug 23, 2022
ee69de0
fix bug
hdavid16 Aug 30, 2022
1cb2399
Change arrows to triangles. Fixes point 2 in #150
hdavid16 Aug 30, 2022
98f7d3e
update ref. images in tests for new arrow types
hdavid16 Aug 30, 2022
d9716fc
fixed bug in tests
hdavid16 Aug 30, 2022
419655a
-update locs type in gplot and spring_layout
hdavid16 Sep 27, 2022
0f9ca82
fix visualization of double sided arcs
hdavid16 Sep 27, 2022
788cc5e
fix bug when edge iterator passed to `graphline`
hdavid16 Sep 27, 2022
f44fd7b
fix bug on edgelabels
hdavid16 Sep 29, 2022
db61499
closes #95 (add `saveplot`)
hdavid16 Sep 29, 2022
e8a608d
no need to use Reexport or Measures
hdavid16 Sep 29, 2022
75ec07f
make edge label in center (even for direted)
hdavid16 Oct 13, 2022
9bb185c
.
hdavid16 Oct 13, 2022
3bafc1c
add interpolation functions for edge labels
hdavid16 Jan 24, 2023
d117fc2
fix #190
hdavid16 Jan 24, 2023
7fdcb3f
Merge pull request #8 from hdavid16/edge_label_middle
hdavid16 Jan 24, 2023
246eab6
add note on bezier curve interpolation
hdavid16 Jan 24, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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:
10 changes: 4 additions & 6 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -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"
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 3 additions & 1 deletion src/GraphPlot.jl
Original file line number Diff line number Diff line change
@@ -15,7 +15,9 @@ export
spring_layout,
spectral_layout,
shell_layout,
stressmajorize_layout
stressmajorize_layout,
saveplot,
mm, cm, inch

include("deprecations.jl")

22 changes: 13 additions & 9 deletions src/layout.jl
Original file line number Diff line number Diff line change
@@ -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
150 changes: 116 additions & 34 deletions src/lines.jl
Original file line number Diff line number Diff line change
@@ -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
164 changes: 112 additions & 52 deletions src/plot.jl
Original file line number Diff line number Diff line change
@@ -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
Binary file modified test/data/curve.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/data/karate_background_color.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/data/karate_straight_directed.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/data/self_directed.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
43 changes: 40 additions & 3 deletions test/runtests.jl
Original file line number Diff line number Diff line change
@@ -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