Skip to content

Commit 8ebe46b

Browse files
Add requested features (#186)
* 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]>
1 parent 52f4aae commit 8ebe46b

12 files changed

+299
-107
lines changed

.github/workflows/ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
fail-fast: false
1616
matrix:
1717
version:
18-
- '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'.
18+
- '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'.
1919
- '1' # Leave this line unchanged. '1' will automatically expand to the latest stable 1.x release of Julia.
2020
- 'nightly'
2121
os:

Project.toml

+4-6
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ version = "0.5.2"
55

66
[deps]
77
ArnoldiMethod = "ec485272-7323-5ecc-a04f-4719b315124d"
8-
ColorTypes = "3da002f7-5984-5a60-b8a6-cbb66c0b333f"
98
Colors = "5ae59095-9a9b-59fe-a467-6f913c188581"
109
Compose = "a81c6b42-2e10-5240-aca2-a61377ecd94b"
1110
DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab"
@@ -15,9 +14,8 @@ Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
1514
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
1615

1716
[compat]
18-
ArnoldiMethod = "0.0.4, 0.1, 0.2"
19-
ColorTypes = "0.9, 0.10, 0.11"
20-
Colors = "0.11, 0.12"
21-
Compose = "0.8, 0.9"
17+
ArnoldiMethod = "0.2"
18+
Colors = "0.12"
19+
Compose = "0.9"
2220
Graphs = "1.4"
23-
julia = "1.3"
21+
julia = "1.6"

README.md

+10-1
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,8 @@ draw(PDF("karate.pdf", 16cm, 16cm), gplot(g))
146146
draw(PNG("karate.png", 16cm, 16cm), gplot(g))
147147
# save to svg
148148
draw(SVG("karate.svg", 16cm, 16cm), gplot(g))
149+
# alternate way of saving to svg without loading Compose
150+
saveplot(gplot(g, plot_size = (16cm, 16cm)), "karate.svg")
149151
```
150152
# Graphs.jl integration
151153
```julia
@@ -160,6 +162,10 @@ gplot(h)
160162

161163
# Keyword Arguments
162164
+ `layout` Layout algorithm: `random_layout`, `circular_layout`, `spring_layout`, `shell_layout`, `stressmajorize_layout`, `spectral_layout`. Default: `spring_layout`
165+
+ `title` Plot title. Default: `""`
166+
+ `title_color` Plot title color. Default: `colorant"black"`
167+
+ `title_size` Plot title size. Default: `4.0`
168+
+ `font_family` Font family for all text. Default: `"Helvetica"`
163169
+ `NODESIZE` Max size for the nodes. Default: `3.0/sqrt(N)`
164170
+ `nodesize` Relative size for the nodes, can be a Vector. Default: `1.0`
165171
+ `nodelabel` Labels for the vertices, a Vector or nothing. Default: `nothing`
@@ -183,7 +189,10 @@ gplot(h)
183189
+ `arrowangleoffset` Angular width in radians for the arrows. Default: `π/9 (20 degrees)`
184190
+ `linetype` Type of line used for edges ("straight", "curve"). Default: "straight"
185191
+ `outangle` Angular width in radians for the edges (only used if `linetype = "curve`). Default: `π/5 (36 degrees)`
186-
192+
+ `background_color` Color for the plot background. Default: `nothing`
193+
+ `plot_size` Tuple of measures for width x height of plot area. Default: `(10cm, 10cm)`
194+
+ `leftpad, rightpad, toppad, bottompad` Padding for the plot margins. Default: `0mm`
195+
+ `pad` Padding for plot margins (overrides individual padding if given). Default: `nothing`
187196
# Reporting Bugs
188197

189198
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.

src/GraphPlot.jl

+3-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ export
1515
spring_layout,
1616
spectral_layout,
1717
shell_layout,
18-
stressmajorize_layout
18+
stressmajorize_layout,
19+
saveplot,
20+
mm, cm, inch
1921

2022
include("deprecations.jl")
2123

src/layout.jl

+13-9
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,11 @@ julia> locs_x, locs_y = spring_layout(g)
102102
```
103103
"""
104104
function spring_layout(g::AbstractGraph,
105-
locs_x=2*rand(nv(g)).-1.0,
106-
locs_y=2*rand(nv(g)).-1.0;
105+
locs_x_in::AbstractVector{R1}=2*rand(nv(g)).-1.0,
106+
locs_y_in::AbstractVector{R2}=2*rand(nv(g)).-1.0;
107107
C=2.0,
108108
MAXITER=100,
109-
INITTEMP=2.0)
109+
INITTEMP=2.0) where {R1 <: Real, R2 <: Real}
110110

111111
nvg = nv(g)
112112
adj_matrix = adjacency_matrix(g)
@@ -119,6 +119,10 @@ function spring_layout(g::AbstractGraph,
119119
force_x = zeros(nvg)
120120
force_y = zeros(nvg)
121121

122+
# Convert locs to float
123+
locs_x = convert(Vector{Float64}, locs_x_in)
124+
locs_y = convert(Vector{Float64}, locs_y_in)
125+
122126
# Iterate MAXITER times
123127
@inbounds for iter = 1:MAXITER
124128
# Calculate forces
@@ -174,7 +178,7 @@ end
174178

175179
using Random: MersenneTwister
176180

177-
function spring_layout(g::AbstractGraph, seed::Integer, kws...)
181+
function spring_layout(g::AbstractGraph, seed::Integer; kws...)
178182
rng = MersenneTwister(seed)
179183
spring_layout(g, 2 .* rand(rng, nv(g)) .- 1.0, 2 .* rand(rng,nv(g)) .- 1.0; kws...)
180184
end
@@ -205,20 +209,20 @@ function shell_layout(g, nlist::Union{Nothing, Vector{Vector{Int}}} = nothing)
205209
if nv(g) == 1
206210
return [0.0], [0.0]
207211
end
208-
if nlist == nothing
212+
if isnothing(nlist)
209213
nlist = [collect(1:nv(g))]
210214
end
211215
radius = 0.0
212216
if length(nlist[1]) > 1
213217
radius = 1.0
214218
end
215-
locs_x = Float64[]
216-
locs_y = Float64[]
219+
locs_x = zeros(nv(g))
220+
locs_y = zeros(nv(g))
217221
for nodes in nlist
218222
# Discard the extra angle since it matches 0 radians.
219223
θ = range(0, stop=2pi, length=length(nodes)+1)[1:end-1]
220-
append!(locs_x, radius*cos.(θ))
221-
append!(locs_y, radius*sin.(θ))
224+
locs_x[nodes] = radius*cos.(θ)
225+
locs_y[nodes] = radius*sin.(θ)
222226
radius += 1.0
223227
end
224228
return locs_x, locs_y

src/lines.jl

+116-34
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,34 @@
11
"""
22
Return lines and arrow heads
33
"""
4-
function graphline(g, locs_x, locs_y, nodesize::Vector{T}, arrowlength, angleoffset) where {T<:Real}
5-
lines = Array{Vector{Tuple{Float64,Float64}}}(undef, ne(g))
6-
arrows = Array{Vector{Tuple{Float64,Float64}}}(undef, ne(g))
7-
for (e_idx, e) in enumerate(edges(g))
4+
function midpoint(pt1,pt2)
5+
x = (pt1[1] + pt2[1]) / 2
6+
y = (pt1[2] + pt2[2]) / 2
7+
return x,y
8+
end
9+
10+
function interpolate_bezier(x::Vector,t)
11+
#TODO: since this is only being used for `curve` which has 4 points (n = 3), the calculation can be simplified for this case.
12+
n = length(x)-1
13+
x_loc = sum(binomial(n,i)*(1-t)^(n-i)*t^i*x[i+1][1] for i in 0:n)
14+
y_loc = sum(binomial(n,i)*(1-t)^(n-i)*t^i*x[i+1][2] for i in 0:n)
15+
return x_loc.value, y_loc.value
16+
end
17+
18+
interpolate_bezier(x::Compose.CurvePrimitive,t) =
19+
interpolate_bezier([x.anchor0, x.ctrl0, x.ctrl1, x.anchor1], t)
20+
21+
function interpolate_line(locs_x,locs_y,i,j,t)
22+
x_loc = locs_x[i] + (locs_x[j]-locs_x[i])*t
23+
y_loc = locs_y[i] + (locs_y[j]-locs_y[i])*t
24+
return x_loc, y_loc
25+
end
26+
27+
function graphline(edge_list, locs_x, locs_y, nodesize::Vector{T}, arrowlength, angleoffset) where {T<:Real}
28+
num_edges = length(edge_list)
29+
lines = Array{Vector{Tuple{Float64,Float64}}}(undef, num_edges)
30+
arrows = Array{Vector{Tuple{Float64,Float64}}}(undef, num_edges)
31+
for (e_idx, e) in enumerate(edge_list)
832
i = src(e)
933
j = dst(e)
1034
Δx = locs_x[j] - locs_x[i]
@@ -14,17 +38,24 @@ function graphline(g, locs_x, locs_y, nodesize::Vector{T}, arrowlength, angleoff
1438
starty = locs_y[i] + nodesize[i]*sin(θ)
1539
endx = locs_x[j] + nodesize[j]*cos+π)
1640
endy = locs_y[j] + nodesize[j]*sin+π)
17-
lines[e_idx] = [(startx, starty), (endx, endy)]
1841
arr1, arr2 = arrowcoords(θ, endx, endy, arrowlength, angleoffset)
42+
endx0, endy0 = midpoint(arr1, arr2)
43+
e_idx2 = findfirst(==(Edge(j,i)), collect(edge_list)) #get index of reverse arc
44+
if !isnothing(e_idx2) && e_idx2 < e_idx #only make changes if lines/arrows have already been defined for that arc
45+
startx, starty = midpoint(arrows[e_idx2][[1,3]]...) #get midopint of reverse arc and use as new start point
46+
lines[e_idx2][1] = (endx0, endy0) #update endpoint of reverse arc
47+
end
48+
lines[e_idx] = [(startx, starty), (endx0, endy0)]
1949
arrows[e_idx] = [arr1, (endx, endy), arr2]
2050
end
2151
lines, arrows
2252
end
2353

24-
function graphline(g::AbstractGraph{T}, locs_x, locs_y, nodesize::Real, arrowlength, angleoffset) where {T<:Integer}
25-
lines = Array{Vector{Tuple{Float64,Float64}}}(undef, ne(g))
26-
arrows = Array{Vector{Tuple{Float64,Float64}}}(undef, ne(g))
27-
for (e_idx, e) in enumerate(edges(g))
54+
function graphline(edge_list, locs_x, locs_y, nodesize::Real, arrowlength, angleoffset)
55+
num_edges = length(edge_list)
56+
lines = Array{Vector{Tuple{Float64,Float64}}}(undef, num_edges)
57+
arrows = Array{Vector{Tuple{Float64,Float64}}}(undef, num_edges)
58+
for (e_idx, e) in enumerate(edge_list)
2859
i = src(e)
2960
j = dst(e)
3061
Δx = locs_x[j] - locs_x[i]
@@ -34,16 +65,23 @@ function graphline(g::AbstractGraph{T}, locs_x, locs_y, nodesize::Real, arrowlen
3465
starty = locs_y[i] + nodesize*sin(θ)
3566
endx = locs_x[j] + nodesize*cos+π)
3667
endy = locs_y[j] + nodesize*sin+π)
37-
lines[e_idx] = [(startx, starty), (endx, endy)]
3868
arr1, arr2 = arrowcoords(θ, endx, endy, arrowlength, angleoffset)
69+
endx0, endy0 = midpoint(arr1, arr2)
70+
e_idx2 = findfirst(==(Edge(j,i)), collect(edge_list)) #get index of reverse arc
71+
if !isnothing(e_idx2) && e_idx2 < e_idx #only make changes if lines/arrows have already been defined for that arc
72+
startx, starty = midpoint(arrows[e_idx2][[1,3]]...) #get midopint of reverse arc and use as new start point
73+
lines[e_idx2][1] = (endx0, endy0) #update endpoint of reverse arc
74+
end
75+
lines[e_idx] = [(startx, starty), (endx0, endy0)]
3976
arrows[e_idx] = [arr1, (endx, endy), arr2]
4077
end
4178
lines, arrows
4279
end
4380

44-
function graphline(g::AbstractGraph{T}, locs_x, locs_y, nodesize::Vector{<:Real}) where {T<:Integer}
45-
lines = Array{Vector{Tuple{Float64,Float64}}}(undef, ne(g))
46-
for (e_idx, e) in enumerate(edges(g))
81+
function graphline(edge_list, locs_x, locs_y, nodesize::Vector{T}) where {T<:Real}
82+
num_edges = length(edge_list)
83+
lines = Array{Vector{Tuple{Float64,Float64}}}(undef, num_edges)
84+
for (e_idx, e) in enumerate(edge_list)
4785
i = src(e)
4886
j = dst(e)
4987
Δx = locs_x[j] - locs_x[i]
@@ -58,9 +96,10 @@ function graphline(g::AbstractGraph{T}, locs_x, locs_y, nodesize::Vector{<:Real}
5896
lines
5997
end
6098

61-
function graphline(g::AbstractGraph{T}, locs_x, locs_y, nodesize::Real) where {T<:Integer}
62-
lines = Array{Vector{Tuple{Float64,Float64}}}(undef, ne(g))
63-
for (e_idx, e) in enumerate(edges(g))
99+
function graphline(edge_list, locs_x, locs_y, nodesize::Real)
100+
num_edges = length(edge_list)
101+
lines = Array{Vector{Tuple{Float64,Float64}}}(undef, num_edges)
102+
for (e_idx, e) in enumerate(edge_list)
64103
i = src(e)
65104
j = dst(e)
66105
Δx = locs_x[j] - locs_x[i]
@@ -75,10 +114,11 @@ function graphline(g::AbstractGraph{T}, locs_x, locs_y, nodesize::Real) where {T
75114
return lines
76115
end
77116

78-
function graphcurve(g::AbstractGraph{T}, locs_x, locs_y, nodesize::Vector{<:Real}, arrowlength, angleoffset, outangle=pi/5) where {T<:Integer}
79-
curves = Matrix{Tuple{Float64,Float64}}(undef, ne(g), 4)
80-
arrows = Array{Vector{Tuple{Float64,Float64}}}(undef, ne(g))
81-
for (e_idx, e) in enumerate(edges(g))
117+
function graphcurve(edge_list, locs_x, locs_y, nodesize::Vector{T}, arrowlength, angleoffset, outangle=pi/5) where {T<:Real}
118+
num_edges = length(edge_list)
119+
curves = Matrix{Tuple{Float64,Float64}}(undef, num_edges, 4)
120+
arrows = Array{Vector{Tuple{Float64,Float64}}}(undef, num_edges)
121+
for (e_idx, e) in enumerate(edge_list)
82122
i = src(e)
83123
j = dst(e)
84124
Δx = locs_x[j] - locs_x[i]
@@ -95,18 +135,20 @@ function graphcurve(g::AbstractGraph{T}, locs_x, locs_y, nodesize::Vector{<:Real
95135
d = 2 * π * nodesize[i]
96136
end
97137

98-
curves[e_idx, :] = curveedge(startx, starty, endx, endy, θ, outangle, d)
99-
100138
arr1, arr2 = arrowcoords-outangle, endx, endy, arrowlength, angleoffset)
139+
endx0 = (arr1[1] + arr2[1]) / 2
140+
endy0 = (arr1[2] + arr2[2]) / 2
141+
curves[e_idx, :] = curveedge(startx, starty, endx0, endy0, θ, outangle, d)
101142
arrows[e_idx] = [arr1, (endx, endy), arr2]
102143
end
103144
return curves, arrows
104145
end
105146

106-
function graphcurve(g, locs_x, locs_y, nodesize::Real, arrowlength, angleoffset, outangle=pi/5)
107-
curves = Matrix{Tuple{Float64,Float64}}(undef, ne(g), 4)
108-
arrows = Array{Vector{Tuple{Float64,Float64}}}(undef, ne(g))
109-
for (e_idx, e) in enumerate(edges(g))
147+
function graphcurve(edge_list, locs_x, locs_y, nodesize::Real, arrowlength, angleoffset, outangle=pi/5)
148+
num_edges = length(edge_list)
149+
curves = Matrix{Tuple{Float64,Float64}}(undef, num_edges, 4)
150+
arrows = Array{Vector{Tuple{Float64,Float64}}}(undef, num_edges)
151+
for (e_idx, e) in enumerate(edge_list)
110152
i = src(e)
111153
j = dst(e)
112154
Δx = locs_x[j] - locs_x[i]
@@ -123,17 +165,19 @@ function graphcurve(g, locs_x, locs_y, nodesize::Real, arrowlength, angleoffset,
123165
d = 2 * π * nodesize
124166
end
125167

126-
curves[e_idx, :] = curveedge(startx, starty, endx, endy, θ, outangle, d)
127-
128168
arr1, arr2 = arrowcoords-outangle, endx, endy, arrowlength, angleoffset)
169+
endx0 = (arr1[1] + arr2[1]) / 2
170+
endy0 = (arr1[2] + arr2[2]) / 2
171+
curves[e_idx, :] = curveedge(startx, starty, endx0, endy0, θ, outangle, d)
129172
arrows[e_idx] = [arr1, (endx, endy), arr2]
130173
end
131174
return curves, arrows
132175
end
133176

134-
function graphcurve(g, locs_x, locs_y, nodesize::Real, outangle)
135-
curves = Matrix{Tuple{Float64,Float64}}(undef, ne(g), 4)
136-
for (e_idx, e) in enumerate(edges(g))
177+
function graphcurve(edge_list, locs_x, locs_y, nodesize::Real, outangle)
178+
num_edges = length(edge_list)
179+
curves = Matrix{Tuple{Float64,Float64}}(undef, num_edges, 4)
180+
for (e_idx, e) in enumerate(edge_list)
137181
i = src(e)
138182
j = dst(e)
139183
Δx = locs_x[j] - locs_x[i]
@@ -155,9 +199,10 @@ function graphcurve(g, locs_x, locs_y, nodesize::Real, outangle)
155199
return curves
156200
end
157201

158-
function graphcurve(g::AbstractGraph{T}, locs_x, locs_y, nodesize::Vector{<:Real}, outangle) where {T<:Integer}
159-
curves = Matrix{Tuple{Float64,Float64}}(undef, ne(g), 4)
160-
for (e_idx, e) in enumerate(edges(g))
202+
function graphcurve(edge_list, locs_x, locs_y, nodesize::Vector{T}, outangle) where {T<:Real}
203+
num_edges = length(edge_list)
204+
curves = Matrix{Tuple{Float64,Float64}}(undef, num_edges, 4)
205+
for (e_idx, e) in enumerate(edge_list)
161206
i = src(e)
162207
j = dst(e)
163208
Δx = locs_x[j] - locs_x[i]
@@ -201,3 +246,40 @@ function curveedge(x1, y1, x2, y2, θ, outangle, d; k=0.5)
201246

202247
return [(x1,y1) (xc1, yc1) (xc2, yc2) (x2, y2)]
203248
end
249+
250+
function build_curved_edges(edge_list, locs_x, locs_y, nodesize, arrowlengthfrac, arrowangleoffset, outangle)
251+
if arrowlengthfrac > 0.0
252+
curves_cord, arrows_cord = graphcurve(edge_list, locs_x, locs_y, nodesize, arrowlengthfrac, arrowangleoffset, outangle)
253+
curves = curve(curves_cord[:,1], curves_cord[:,2], curves_cord[:,3], curves_cord[:,4])
254+
carrows = polygon(arrows_cord)
255+
else
256+
curves_cord = graphcurve(edge_list, locs_x, locs_y, nodesize, outangle)
257+
curves = curve(curves_cord[:,1], curves_cord[:,2], curves_cord[:,3], curves_cord[:,4])
258+
carrows = nothing
259+
end
260+
261+
return curves, carrows
262+
end
263+
264+
function build_straight_edges(edge_list, locs_x, locs_y, nodesize, arrowlengthfrac, arrowangleoffset)
265+
if arrowlengthfrac > 0.0
266+
lines_cord, arrows_cord = graphline(edge_list, locs_x, locs_y, nodesize, arrowlengthfrac, arrowangleoffset)
267+
lines = line(lines_cord)
268+
larrows = polygon(arrows_cord)
269+
else
270+
lines_cord = graphline(edge_list, locs_x, locs_y, nodesize)
271+
lines = line(lines_cord)
272+
larrows = nothing
273+
end
274+
275+
return lines, larrows
276+
end
277+
278+
function build_straight_curved_edges(g, locs_x, locs_y, nodesize, arrowlengthfrac, arrowangleoffset, outangle)
279+
edge_list1 = filter(e -> src(e) != dst(e), collect(edges(g)))
280+
edge_list2 = filter(e -> src(e) == dst(e), collect(edges(g)))
281+
lines, larrows = build_straight_edges(edge_list1, locs_x, locs_y, nodesize, arrowlengthfrac, arrowangleoffset)
282+
curves, carrows = build_curved_edges(edge_list2, locs_x, locs_y, nodesize, arrowlengthfrac, arrowangleoffset, outangle)
283+
284+
return lines, larrows, curves, carrows
285+
end

0 commit comments

Comments
 (0)