Skip to content

Commit

Permalink
Add requested features (#186)
Browse files Browse the repository at this point in the history
* Improve Readme and docstring
-distinguish args, kwargs
-add missing kwargs

* fix #166

clarify that Cairo is required to visualize in vscode

* Fix #175 to enable plotting to html

Checking for System OS was outdated

* Document the use of gplothtml in README

* Update open_file

Now matches gadfly.jl (https://github.com/GiovineItalia/Gadfly.jl/blob/master/src/open_file.jl)

* Minor changes:
-allow gplothtml to accept same arguments as gplot
-allow x/y-locs to be of different type (<: Real)

* add TagBot to repo

* Fix #172. set background color (`backgroundc` kwarg):
and minor changes to plots.jl
update README

* add compose child object for background (instead of main level)

* rename to `background_color` + add test

* fixed #149

* support changing plot size (fixes #94, #147)

uses Colorant.set_default_graphic_size

* update default `plot_size` to Compose.jl default

* Fix #107
-Allow add title
-Set title color and size
-Set font family for entire plot
-Remove unneded Compose objects (the ones with `nothing` as the arg)

* scale title margin with title font size

* Fix #160 make self-loop edges curved
behavior is regardless of the linetype

* add padding option for margins
-relevant for plots with curved self-loops and long node labels

* Fix #154

Multiple dispatch was messing up because types were not specified for spring layout and `kws` not preceeded by `;`

* update error msg

* update background rectangle to cover padded area

* add conversion to floats for input locations to avoid error when Ints are passed

* use float instead of Float64

* add tests for layouts

* update compat; remove ColorTypes

* avoid unnecessary allocation if locs are already Floats

Co-authored-by: Simon Schölly <[email protected]>

* avoid allocation in gplot if locs are floats.
fix typo

* update ci.yml to julia 1.6

* bug fix

* add `pad` kwarg to override indvidual paddings

* make lines more robust when self-loops involved

* remove deps to LinAlg and SparseArrays now that not needed for mixed edge styles.

* remove using statements

* Revert "remove deps to LinAlg and SparseArrays now that not needed for mixed edge styles."

This reverts commit c391c55.

* fix bug

when node size uses Width units and padding is used, the arrow ends get thrown off (instead, replace Compose.w with 2.4 (unit box width) to make sure that the right size is used regardless of the padding)

* Change arrows to triangles. Fixes point 2 in #150

* update ref. images in tests for new arrow types

* fixed bug in tests

* -update locs type in gplot and spring_layout
-update default plot_size to square plot

* fix visualization of double sided arcs

* fix bug when edge iterator passed to `graphline`

* fix bug on edgelabels

* closes #95 (add `saveplot`)
also uses Reexport to export Measures

* no need to use Reexport or Measures

* make edge label in center (even for direted)

* .

* add interpolation functions for edge labels

* fix #190

* add note on bezier curve interpolation

---------

Co-authored-by: Simon Schölly <[email protected]>
  • Loading branch information
hdavid16 and simonschoelly committed May 12, 2024
1 parent 52f4aae commit 8ebe46b
Show file tree
Hide file tree
Showing 12 changed files with 299 additions and 107 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
10 changes: 4 additions & 6 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Up @@ -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
Expand All @@ -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`
Expand All @@ -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
Expand Up @@ -15,7 +15,9 @@ export
spring_layout,
spectral_layout,
shell_layout,
stressmajorize_layout
stressmajorize_layout,
saveplot,
mm, cm, inch

include("deprecations.jl")

Expand Down
22 changes: 13 additions & 9 deletions src/layout.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
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]
Expand All @@ -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]
Expand All @@ -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]
Expand All @@ -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]
Expand All @@ -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]
Expand All @@ -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]
Expand All @@ -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]
Expand All @@ -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]
Expand Down Expand Up @@ -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
Loading

0 comments on commit 8ebe46b

Please sign in to comment.