diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..4bfe59e --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +enable-beta-ecosystems: true +updates: + - package-ecosystem: "julia" + directory: "/" + schedule: + interval: "weekly" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d2f3ff1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,47 @@ +name: CI +on: + pull_request: + branches: + - master + push: + branches: + - master + tags: '*' +jobs: + test: + name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + version: + - '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: + - ubuntu-latest + - macos-latest + arch: + - x64 + steps: + - uses: actions/checkout@v2 + - uses: julia-actions/setup-julia@v1 + with: + version: ${{ matrix.version }} + arch: ${{ matrix.arch }} + - uses: actions/cache@v4 + env: + cache-name: cache-artifacts + with: + path: ~/.julia/artifacts + key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} + restore-keys: | + ${{ runner.os }}-test-${{ env.cache-name }}- + ${{ runner.os }}-test- + ${{ runner.os }}- + - uses: julia-actions/julia-buildpkg@v1 + - uses: julia-actions/julia-runtest@v1 + - uses: julia-actions/julia-processcoverage@v1 + - uses: codecov/codecov-action@v1 + with: + file: lcov.info diff --git a/.github/workflows/downgrade.yml b/.github/workflows/downgrade.yml new file mode 100644 index 0000000..e3a2b80 --- /dev/null +++ b/.github/workflows/downgrade.yml @@ -0,0 +1,29 @@ +name: Downgrade +on: + pull_request: + branches: [master, main] + paths-ignore: + - 'docs/**' + push: + branches: [master, main] + paths-ignore: + - 'docs/**' +env: + PYTHON: ~ +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + version: ['1.10'] + steps: + - uses: actions/checkout@v5 + - uses: julia-actions/setup-julia@v1 + with: + version: ${{ matrix.version }} + - uses: julia-actions/julia-downgrade-compat@v2 + with: + skip: Pkg,TOML,InteractiveUtils,Random,LinearAlgebra + - uses: julia-actions/cache@v2 + - uses: julia-actions/julia-buildpkg@v1 + - uses: julia-actions/julia-runtest@v1 diff --git a/.github/workflows/tagbot.yml b/.github/workflows/tagbot.yml new file mode 100644 index 0000000..32a9dd8 --- /dev/null +++ b/.github/workflows/tagbot.yml @@ -0,0 +1,20 @@ +name: TagBot +on: + issue_comment: + types: + - created + workflow_dispatch: + inputs: + lookback: + default: 3 +permissions: + contents: write +jobs: + TagBot: + if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot' + runs-on: ubuntu-latest + steps: + - uses: JuliaRegistries/TagBot@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + ssh: ${{ secrets.DOCUMENTER_KEY }} \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index e69cfa0..0000000 --- a/.travis.yml +++ /dev/null @@ -1,19 +0,0 @@ -language: julia -os: - - linux - - osx -julia: - - 1.0 - - nightly - -matrix: - allow_failures: - - julia: nightly - -notifications: - email: false -# uncomment the following lines to override the default test script - -after_success: - # push coverage results to Codecov - - julia -e 'using Pkg; cd(Pkg.dir("GraphPlot")); Pkg.add("Coverage"); using Coverage; Codecov.submit(Codecov.process_folder())' diff --git a/Project.toml b/Project.toml index 5a9c680..fa53586 100644 --- a/Project.toml +++ b/Project.toml @@ -1,33 +1,22 @@ name = "GraphPlot" uuid = "a2cc645c-3eea-5389-862e-a155d0052231" authors = ["JuliaGraphs"] -version = "0.4.2" +version = "0.6.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" -LightGraphs = "093fc24a-ae57-5d10-9952-331d41423f4d" +Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" [compat] -ArnoldiMethod = "0.0.4" -ColorTypes = "0.9, 0.10" -Colors = "0.11, 0.12" -Compose = "0.7, 0.8" -LightGraphs = "1.1" -VisualRegressionTests = "0.2, 0.3" -julia = "1" - -[extras] -Cairo = "159f3aea-2a34-519c-b102-8c37f9878175" -ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" -VisualRegressionTests = "34922c18-7c2a-561c-bac1-01e79b2c4c92" - -[targets] -test = ["Test", "Cairo", "ImageMagick", "VisualRegressionTests"] +ArnoldiMethod = "0.4" +Colors = "0.12, 0.13" +Compose = "0.9" +DelimitedFiles = "1" +Graphs = "1.4" +julia = "1.6" diff --git a/README.md b/README.md index e80acc5..a226273 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # GraphPlot -[![Build Status](https://travis-ci.org/JuliaGraphs/GraphPlot.jl.svg?branch=master)](https://travis-ci.org/JuliaGraphs/GraphPlot.jl) -[![Julia Build](http://pkg.julialang.org/badges/GraphPlot_0.6.svg)](http://pkg.julialang.org/?pkg=GraphPlot) +[![CI](https://github.com/JuliaGraphs/GraphPlot.jl/actions/workflows/ci.yml/badge.svg)](https://github.com/JuliaGraphs/GraphPlot.jl/actions/workflows/ci.yml) +[![version](https://juliahub.com/docs/GraphPlot/version.svg)](https://juliahub.com/ui/Packages/GraphPlot/bUwXr) Graph layout and visualization algorithms based on [Compose.jl](https://github.com/dcjones/Compose.jl) and inspired by [GraphLayout.jl](https://github.com/IainNZ/GraphLayout.jl). @@ -14,7 +14,7 @@ Other layout algorithms are wrapped from [NetworkX](https://github.com/networkx/ # Getting Started From the Julia REPL the latest version can be installed with -```{execute="false"} +```julia Pkg.add("GraphPlot") ``` GraphPlot is then loaded with @@ -25,7 +25,7 @@ using GraphPlot # Usage ## karate network ```julia -using LightGraphs: smallgraph +using Graphs: smallgraph g = smallgraph(:karate) gplot(g) @@ -33,8 +33,8 @@ gplot(g) ## Add node label ```julia -using LightGraphs -nodelabel = [1:nv(g)] +using Graphs +nodelabel = 1:nv(g) gplot(g, nodelabel=nodelabel) ``` @@ -47,7 +47,7 @@ gplot(g, nodelabel=nodelabel, nodelabeldist=1.5, nodelabelangleoffset=π/4) ## Control the node size ```julia # nodes size proportional to their degree -nodesize = [LightGraphs.out_degree(g, v) for v in LightGraphs.vertices(g)] +nodesize = [Graphs.outdegree(g, v) for v in Graphs.vertices(g)] gplot(g, nodesize=nodesize) ``` @@ -76,13 +76,13 @@ gplot(g, nodelabelsize=nodelabelsize, nodesize=nodesize, nodelabel=nodelabel) ## Draw edge labels ```julia -edgelabel = [1:LightGraphs.ne(g)] +edgelabel = 1:Graphs.ne(g) gplot(g, edgelabel=edgelabel, nodelabel=nodelabel) ``` ## Adjust edge labels ```julia -edgelabel = [1:LightGraphs.ne(g)] +edgelabel = 1:Graphs.ne(g) gplot(g, edgelabel=edgelabel, nodelabel=nodelabel, edgelabeldistx=0.5, edgelabeldisty=0.5) ``` @@ -99,7 +99,7 @@ gplot(g, nodefillc=nodefillc) ## Different layout ### spring layout (default) -This is the defaut layout and will be chosen if no layout is specified. The [default parameters to the spring layout algorithm](https://github.com/JuliaGraphs/GraphPlot.jl/blob/master/src/layout.jl#L78) can be changed by supplying an anonymous function, e.g., if nodes appear clustered too tightly together, try +This is the defaut layout and will be chosen if no layout is specified. The [default parameters to the spring layout algorithm](https://github.com/JuliaGraphs/GraphPlot.jl/blob/master/src/layout.jl#L78) can be changed by supplying an anonymous function, e.g., if nodes appear clustered too tightly together, try ```julia layout=(args...)->spring_layout(args...; C=20) gplot(g, layout=layout, nodelabel=nodelabel) @@ -121,8 +121,8 @@ gplot(g, layout=spectral_layout) ### shell layout ```julia nlist = Vector{Vector{Int}}(undef, 2) # two shells -nlist[1] = [1:5] # first shell -nlist[2] = [6:nv(g)] # second shell +nlist[1] = 1:5 # first shell +nlist[2] = 6:nv(g) # second shell locs_x, locs_y = shell_layout(g, nlist) gplot(g, locs_x, locs_y, nodelabel=nodelabel) ``` @@ -132,38 +132,67 @@ gplot(g, locs_x, locs_y, nodelabel=nodelabel) gplot(g, linetype="curve") ``` +## Show plot + +When using an IDE such as VSCode, `Cairo.jl` is required to visualize the plot inside the IDE. +When using the REPL, `gplothtml` will allow displaying the plot on a browser. + ## Save to figure -```{execute="false"} -using Cairo, Compose +```julia +using Compose # save to pdf draw(PDF("karate.pdf", 16cm, 16cm), gplot(g)) # save to png 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") ``` -# LightGraphs integration +# Graphs.jl integration ```julia -using LightGraphs +using Graphs h = watts_strogatz(50, 6, 0.3) gplot(h) ``` # Arguments -+ `G` graph to plot -+ `layout` Optional. layout algorithm. Currently can choose from -[random_layout, circular_layout, spring_layout, stressmajorize_layout, -shell_layout, spectral_layout]. -Default: `spring_layout` -+ `nodelabel` Optional. Labels for the vertices. Default: `nothing` -+ `nodefillc` Optional. Color to fill the nodes with. -Default: `colorant"turquoise"` -+ `nodestrokec` Color for the node stroke. -Default: `nothing` -+ `arrowlengthfrac` Fraction of line length to use for arrows. -Set to 0 for no arrows. Default: 0 for undirected graph and 0.1 for directed graph -+ `arrowangleoffset` angular width in radians for the arrows. Default: `π/9` (20 degrees) - ++ `G` Graph to draw ++ `locs_x, locs_y` Locations of the nodes (will be normalized and centered). If not specified, will be obtained from `layout` kwarg. + +# Keyword Arguments ++ `layout` Layout algorithm: `random_layout`, `circular_layout`, `spring_layout`, `shell_layout`, `stressmajorize_layout`, `spectral_layout`. Default: `spring_layout` ++ `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` ++ `nodelabelc` Color for the node labels, can be a Vector. Default: `colorant"black"` ++ `nodelabeldist` Distances for the node labels from center of nodes. Default: `0.0` ++ `nodelabelangleoffset` Angle offset for the node labels. Default: `π/4.0` ++ `NODELABELSIZE` Largest fontsize for the vertice labels. Default: `4.0` ++ `nodelabelsize` Relative fontsize for the vertice labels, can be a Vector. Default: `1.0` ++ `nodefillc` Color to fill the nodes with, can be a Vector. Default: `colorant"turquoise"` ++ `nodestrokec` Color for the nodes stroke, can be a Vector. Default: `nothing` ++ `nodestrokelw` Line width for the nodes stroke, can be a Vector. Default: `0.0` ++ `edgelabel` Labels for the edges, a Vector or nothing. Default: `[]` ++ `edgelabelc` Color for the edge labels, can be a Vector. Default: `colorant"black"` ++ `edgelabeldistx, edgelabeldisty` Distance for the edge label from center of edge. Default: `0.0` ++ `EDGELABELSIZE` Largest fontsize for the edge labels. Default: `4.0` ++ `edgelabelsize` Relative fontsize for the edge labels, can be a Vector. Default: `1.0` ++ `EDGELINEWIDTH` Max line width for the edges. Default: `0.25/sqrt(N)` ++ `edgelinewidth` Relative line width for the edges, can be a Vector. Default: `1.0` ++ `edgestrokec` Color for the edge strokes, can be a Vector. Default: `colorant"lightgray"` ++ `arrowlengthfrac` Fraction of line length to use for arrows. Equal to 0 for undirected graphs. Default: `0.1` for the directed graphs ++ `arrowangleoffset` Angular width in radians for the arrows. Default: `π/9 (20 degrees)` ++ `linetype` Type of line used for edges ("straight", "curve"). Default: "straight" ++ `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/REQUIRE b/REQUIRE deleted file mode 100644 index 5a68070..0000000 --- a/REQUIRE +++ /dev/null @@ -1,6 +0,0 @@ -julia 0.7 -ArnoldiMethod -Colors -ColorTypes -Compose 0.7.0 -LightGraphs 1.1.0 diff --git a/examples/graphplot.ipynb b/examples/graphplot.ipynb index 3ef10eb..f1143ee 100644 --- a/examples/graphplot.ipynb +++ b/examples/graphplot.ipynb @@ -49,7 +49,7 @@ "outputs": [], "source": [ "# load needed package firstly\n", - "using LightGraphs\n", + "using Graphs\n", "using GraphPlot" ] }, @@ -12127,7 +12127,7 @@ } }, "source": [ - "# Native LightGraphs integration" + "# Native Graphs.jl integration" ] }, { diff --git a/src/GraphPlot.jl b/src/GraphPlot.jl index d1f58a6..b42189d 100644 --- a/src/GraphPlot.jl +++ b/src/GraphPlot.jl @@ -1,7 +1,7 @@ module GraphPlot using Compose # for plotting features -using LightGraphs +using Graphs const gadflyjs = joinpath(dirname(Base.source_path()), "gadfly.js") @@ -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/collapse_plot.jl b/src/collapse_plot.jl index 19d0f9a..2e1e062 100644 --- a/src/collapse_plot.jl +++ b/src/collapse_plot.jl @@ -64,11 +64,11 @@ function community_layout(g::AbstractGraph, membership::Vector{Int}) end function collapse_layout(g::AbstractGraph, membership::Vector{Int}) - lightg = LightGraphs.SimpleGraph(nv(g)) + sg = Graphs.SimpleGraph(nv(g)) for e in edges(g) u = src(e) v = dst(e) - LightGraphs.add_edge!(lightg, u, v) + Graphs.add_edge!(sg, u, v) end N = length(membership) lx = zeros(N) @@ -84,7 +84,7 @@ function collapse_layout(g::AbstractGraph, membership::Vector{Int}) h, w = collapse_graph(g, membership) clx, cly = spring_layout(h) for (lbl, nodes) in comms - subg = lightg[nodes] + subg = sg[nodes] sublx, subly = spring_layout(subg) θ = range(0, stop=2pi, length=(length(nodes) + 1))[1:end-1] for (idx, node) in enumerate(nodes) diff --git a/src/deprecations.jl b/src/deprecations.jl index 35802f0..4e07ac3 100644 --- a/src/deprecations.jl +++ b/src/deprecations.jl @@ -1,49 +1,49 @@ using Base: depwarn -function _nv(g) - depwarn("`GraphPlot._nv(g)` is deprectated. Use `LightGraphs.nv(g)` instead.", :_nv) - return LightGraphs.nv(g) +function _nv(g) + depwarn("`GraphPlot._nv(g)` is deprectated. Use `Graphs.nv(g)` instead.", :_nv) + return Graphs.nv(g) end -function _ne(g) - depwarn("`GraphPlot._ne(g)` is deprectated. Use `LightGraphs.ne(g)` instead.", :_ne) - return LightGraphs.ne(g) +function _ne(g) + depwarn("`GraphPlot._ne(g)` is deprectated. Use `Graphs.ne(g)` instead.", :_ne) + return Graphs.ne(g) end -function _vertices(g) - depwarn("`GraphPlot._vertices(g)` is deprectated. Use `LightGraphs.vertices(g)` instead.", :_vertices) - return LightGraphs.vertices(g) +function _vertices(g) + depwarn("`GraphPlot._vertices(g)` is deprectated. Use `Graphs.vertices(g)` instead.", :_vertices) + return Graphs.vertices(g) end -function _edges(g) - depwarn("`GraphPlot._edges(g)` is deprectated. Use `LightGraphs.edges(g)` instead.", :_edges) - return LightGraphs.edges(g) +function _edges(g) + depwarn("`GraphPlot._edges(g)` is deprectated. Use `Graphs.edges(g)` instead.", :_edges) + return Graphs.edges(g) end -function _src_index(e, g) - depwarn("`GraphPlot._src_index(g)` is deprectated. Use `LightGraphs.src(e)` instead.", :_src_index) - return LightGraphs.src(e) +function _src_index(e, g) + depwarn("`GraphPlot._src_index(g)` is deprectated. Use `Graphs.src(e)` instead.", :_src_index) + return Graphs.src(e) end -function _dst_index(e, g) - depwarn("`GraphPlot._dst_index(g)` is deprectated. Use `LightGraphs.dst(e)` instead.", :_dst_index) - return LightGraphs.dst(e) +function _dst_index(e, g) + depwarn("`GraphPlot._dst_index(g)` is deprectated. Use `Graphs.dst(e)` instead.", :_dst_index) + return Graphs.dst(e) end -function _adjacency_matrix(g) - depwarn("`GraphPlot._adjacency_matrix(g)` is deprectated. Use `LightGraphs.adjacency_matrix(g)` instead.", :_adjacency_matrix) - return LightGraphs.adjacency_matrix(g) +function _adjacency_matrix(g) + depwarn("`GraphPlot._adjacency_matrix(g)` is deprectated. Use `Graphs.adjacency_matrix(g)` instead.", :_adjacency_matrix) + return Graphs.adjacency_matrix(g) end -function _is_directed(g) - depwarn("`GraphPlot._is_directed(g)` is deprectated. Use `LightGraphs.is_directed(g)` instead.", :_is_directed) - return LightGraphs.is_directed(g) +function _is_directed(g) + depwarn("`GraphPlot._is_directed(g)` is deprectated. Use `Graphs.is_directed(g)` instead.", :_is_directed) + return Graphs.is_directed(g) end -function _laplacian_matrix(g) - depwarn("`GraphPlot._laplacian_matrix(g)` is deprectated. Use `LightGraphs.laplacian_matrix(g)` instead.", :_laplacian_matrix) - return LightGraphs.laplacian_matrix(g) +function _laplacian_matrix(g) + depwarn("`GraphPlot._laplacian_matrix(g)` is deprectated. Use `Graphs.laplacian_matrix(g)` instead.", :_laplacian_matrix) + return Graphs.laplacian_matrix(g) end diff --git a/src/layout.jl b/src/layout.jl index d3a7035..87b32e2 100644 --- a/src/layout.jl +++ b/src/layout.jl @@ -2,6 +2,7 @@ using SparseArrays: SparseMatrixCSC, sparse using ArnoldiMethod: SR using Base: OneTo using LinearAlgebra: eigen +using Random: AbstractRNG, default_rng """ Position nodes uniformly at random in the unit square. @@ -83,6 +84,14 @@ where C is a parameter we can adjust *g* a graph +*locs_x_in* +x coordinates of the initial locations. If not provided they are sampled +from [-1, 1]. Can be modified. + +*locs_y_in* +y coordinates of the initial locations. If not provided they are sampled +from [-1, 1]. Can be modified. + *C* Constant to fiddle with density of resulting layout @@ -93,7 +102,8 @@ Number of iterations we apply the forces Initial "temperature", controls movement per iteration *seed* -Integer seed for pseudorandom generation of locations (default = 0). +Either an `Integer` seed or an `Random.AbstractRNG` for generation of initial locations. +If neither is provided `Random.default_rng()` is used. **Examples** ``` @@ -102,13 +112,20 @@ 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}, + locs_y_in::AbstractVector{R2}; C=2.0, MAXITER=100, - INITTEMP=2.0) - + INITTEMP=2.0) where {R1 <: Real, R2 <: Real} nvg = nv(g) + + if length(locs_x_in) != nvg + throw(ArgumentError("The length of locs_x_in does not equal the number of vertices")) + end + if length(locs_y_in) != nvg + throw(ArgumentError("The length of locs_y_in does not equal the number of vertices")) + end + adj_matrix = adjacency_matrix(g) # The optimal distance bewteen vertices @@ -119,6 +136,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,9 +195,16 @@ 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...) + spring_layout(g, rng; kws...) +end + +function spring_layout(g::AbstractGraph, rng::AbstractRNG=default_rng(); kws...) + nvg = nv(g) + locs_x_in = 2.0 * rand(rng, nvg) .- 1.0 + locs_y_in = 2.0 * rand(rng, nvg) .- 1.0 + spring_layout(g, locs_x_in, locs_y_in; kws...) end """ @@ -195,9 +223,9 @@ Vector of Vector, Vector of node Vector for each shell. **Examples** ``` julia> g = smallgraph(:karate) -julia> nlist = Array{Vector{Int}}(2) -julia> nlist[1] = [1:5] -julia> nlist[2] = [6:num_vertiecs(g)] +julia> nlist = Vector{Vector{Int}}() +julia> push!(nlist, collect(1:5)) +julia> push!(nlist, collect(6:nv(g))) julia> locs_x, locs_y = shell_layout(g, nlist) ``` """ @@ -205,20 +233,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 @@ -242,7 +270,7 @@ the edge weight. If None, then all edge weights are 1. **Examples** ``` julia> g = smallgraph(:karate) -julia> weight = rand(num_edges(g)) +julia> weight = rand(ne(g)) julia> locs_x, locs_y = spectral_layout(g, weight) ``` """ @@ -280,7 +308,7 @@ function _spectral(A::SparseMatrixCSC) data = vec(sum(A, dims=1)) D = sparse(Base.OneTo(length(data)), Base.OneTo(length(data)), data) L = D - A - eigenvalues, eigenvectors = LightGraphs.LinAlg.eigs(L, nev=3, which=SR()) + eigenvalues, eigenvectors = Graphs.LinAlg.eigs(L, nev=3, which=SR()) index = sortperm(real(eigenvalues))[2:3] return real(eigenvectors[:, index[1]]), real(eigenvectors[:, index[2]]) end 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 73b34a8..d29b30a 100644 --- a/src/plot.jl +++ b/src/plot.jl @@ -10,83 +10,120 @@ a Compose tree of the graph layout `G` Graph to draw +`locs_x, locs_y` +Locations of the nodes. Can be any units you want, +but will be normalized and centered anyway. If not provided, will +be obtained from `layout` kwarg. + +**Keyword Arguments** + `layout` -Optional. Layout algorithm. Currently can be one of [`random_layout`, +Layout algorithm. Currently can be one of [`random_layout`, `circular_layout`, `spring_layout`, `shell_layout`, `stressmajorize_layout`, `spectral_layout`]. Default: `spring_layout` -`locs_x, locs_y` -Locations of the nodes. Can be any units you want, -but will be normalized and centered anyway +`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` -Optional. Max size for the nodes. Default: `3.0/sqrt(N)` +Max size for the nodes. Default: `3.0/sqrt(N)` `nodesize` -Optional. Relative size for the nodes, can be a Vector. Default: `1.0` +Relative size for the nodes, can be a Vector. Default: `1.0` `nodelabel` -Optional. Labels for the vertices, a Vector or nothing. Default: `nothing` +Labels for the vertices, a Vector or nothing. Default: `nothing` `nodelabelc` -Optional. Color for the node labels, can be a Vector. Default: `colorant"black"` +Color for the node labels, can be a Vector. Default: `colorant"black"` `nodelabeldist` -Optional. Distances for the node labels from center of nodes. Default: `0.0` +Distances for the node labels from center of nodes. Default: `0.0` `nodelabelangleoffset` -Optional. Angle offset for the node labels. Default: `π/4.0` +Angle offset for the node labels. Default: `π/4.0` `NODELABELSIZE` -Optional. Largest fontsize for the vertice labels. Default: `4.0` +Largest fontsize for the vertex labels. Default: `4.0` `nodelabelsize` -Optional. 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` -Optional. Color to fill the nodes with, can be a Vector. Default: `colorant"turquoise"` +Color to fill the nodes with, can be a Vector. Default: `colorant"turquoise"` `nodestrokec` -Optional. Color for the nodes stroke, can be a Vector. Default: `nothing` +Color for the nodes stroke, can be a Vector. Default: `nothing` `nodestrokelw` -Optional. Line width for the nodes stroke, can be a Vector. Default: `0.0` +Line width for the nodes stroke, can be a Vector. Default: `0.0` `edgelabel` -Optional. Labels for the edges, a Vector or nothing. Default: `[]` +Labels for the edges, a Vector or nothing. Default: `[]` `edgelabelc` -Optional. Color for the edge labels, can be a Vector. Default: `colorant"black"` +Color for the edge labels, can be a Vector. Default: `colorant"black"` `edgelabeldistx, edgelabeldisty` -Optional. Distance for the edge label from center of edge. Default: `0.0` +Distance for the edge label from center of edge. Default: `0.0` `EDGELABELSIZE` -Optional. Largest fontsize for the edge labels. Default: `4.0` +Largest fontsize for the edge labels. Default: `4.0` `edgelabelsize` -Optional. Relative fontsize for the edge labels, can be a Vector. Default: `1.0` +Relative fontsize for the edge labels, can be a Vector. Default: `1.0` `EDGELINEWIDTH` -Optional. Max line width for the edges. Default: `0.25/sqrt(N)` +Max line width for the edges. Default: `0.25/sqrt(N)` `edgelinewidth` -Optional. Relative line width for the edges, can be a Vector. Default: `1.0` +Relative line width for the edges, can be a Vector. Default: `1.0` `edgestrokec` -Optional. Color for the edge strokes, can be a Vector. Default: `colorant"lightgray"` +Color for the edge strokes, can be a Vector. Default: `colorant"lightgray"` `arrowlengthfrac` -Optional. Fraction of line length to use for arrows. +Fraction of line length to use for arrows. Equal to 0 for undirected graphs. Default: `0.1` for the directed graphs `arrowangleoffset` -Optional. Angular width in radians for the arrows. Default: `π/9 (20 degrees)` +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 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{R}, locs_y_in::Vector{R}; + 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, @@ -108,28 +145,40 @@ function gplot(g::AbstractGraph{T}, nodestrokec = nothing, nodestrokelw = 0.0, arrowlengthfrac = is_directed(g) ? 0.1 : 0.0, - arrowangleoffset = π / 9.0, + arrowangleoffset = π / 9, linetype = "straight", - outangle = pi/5) where {T <:Integer, R <: 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) min_y, max_y = extrema(locs_y) function scaler(z, a, b) - 2.0 * ((z - a) / (b - a)) - 1.0 + if (a - b) == 0.0 + return 0.5 + else + return 2.0 * ((z - a) / (b - a)) - 1.0 + end end map!(z -> scaler(z, min_x, max_x), locs_x, locs_x) map!(z -> scaler(z, min_y, max_y), locs_y, locs_y) @@ -153,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...) @@ -227,11 +296,11 @@ end # take from [Gadfly.jl](https://github.com/dcjones/Gadfly.jl) function open_file(filename) - if Sys.KERNEL == :Darwin + if Sys.isapple() #apple run(`open $(filename)`) - elseif Sys.KERNEL == :Linux || Sys.KERNEL == :FreeBSD + elseif Sys.islinux() || Sys.isbsd() #linux run(`xdg-open $(filename)`) - elseif Sys.KERNEL == :Windows + elseif Sys.iswindows() #windows run(`$(ENV["COMSPEC"]) /c start $(filename)`) else @warn("Showing plots is not supported on OS $(string(Sys.KERNEL))") @@ -239,13 +308,13 @@ function open_file(filename) end # taken from [Gadfly.jl](https://github.com/dcjones/Gadfly.jl) -function gplothtml(g; layout::Function=spring_layout, keyargs...) +function gplothtml(args...; keyargs...) filename = string(tempname(), ".html") output = open(filename, "w") plot_output = IOBuffer() draw(SVGJS(plot_output, Compose.default_graphic_width, - Compose.default_graphic_width, false), gplot(g, layout(g)...; keyargs...)) + Compose.default_graphic_width, false), gplot(args...; keyargs...)) plotsvg = String(take!(plot_output)) write(output, @@ -270,3 +339,8 @@ function gplothtml(g; layout::Function=spring_layout, 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/Project.toml b/test/Project.toml new file mode 100644 index 0000000..b601d06 --- /dev/null +++ b/test/Project.toml @@ -0,0 +1,10 @@ +[deps] +Cairo = "159f3aea-2a34-519c-b102-8c37f9878175" +ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +VisualRegressionTests = "34922c18-7c2a-561c-bac1-01e79b2c4c92" + +[compat] +VisualRegressionTests = "< 1.1.0" # TODO currently visual regression tests are broken for version >= 1.1.0 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 4c21138..7f9ffed 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,13 +1,14 @@ - +@info "Importing test packages..." # This should fix an error, see : https://github.com/JuliaIO/ImageMagick.jl/issues/133 (Sys.islinux() || Sys.iswindows()) && import ImageMagick using GraphPlot -using LightGraphs +using GraphPlot.Graphs using Cairo -using Colors -using Compose +using GraphPlot.Colors +using GraphPlot.Compose using Random +using StableRNGs: StableRNG using Test using VisualRegressionTests using ImageMagick @@ -17,9 +18,10 @@ istravis = "TRAVIS" ∈ keys(ENV) datadir = joinpath(@__DIR__, "data") +@info "Starting tests..." -# TODO smallgraph(:karate) has already been added to LightGraphs +# TODO smallgraph(:karate) has already been added to Graphs # but as there hasn't been any new version tagged, we relay on this instead karate_edges = Edge.([ 1 => 2, 1 => 3, 1 => 4, 1 => 5, 1 => 6, 1 => 7, @@ -39,70 +41,142 @@ karate_edges = Edge.([ # graphs to test #g = smallgraph(:karate) g = SimpleGraph(karate_edges) -h = LightGraphs.WheelGraph(10) +h = Graphs.wheel_graph(10) test_layout(g::AbstractGraph; kws...) = spring_layout(g, 2017, kws...) # plot and save function for visual regression tests -function plot_and_save(fname, g; gplot_kwargs...) - draw(PNG(fname, 8inch, 8inch), gplot(g; layout=test_layout, gplot_kwargs...)) -end - -function save_comparison(result::VisualTestResult) - grid = hcat(result.refImage, result.testImage) - path = joinpath(datadir, string(basename(result.refFilename)[1:end-length(".png")], "-comparison.png")) - ImageMagick.save(path, grid) - return result +# TODO visual regression tests are currently broken for higher Julia versions +@static if VERSION < v"1.7" + function plot_and_save(fname, g; gplot_kwargs...) + draw(PNG(fname, 8inch, 8inch), gplot(g; layout=test_layout, gplot_kwargs...)) + end + + function save_comparison(result::VisualTestResult) + grid = hcat(result.refImage, result.testImage) + path = joinpath(datadir, string(basename(result.refFilename)[1:end-length(".png")], "-comparison.png")) + ImageMagick.save(path, grid) + return result + end end -@testset "Karate Net" begin - # auxiliary variables - nodelabel = collect(1:nv(g)) - nodesize = outdegree(g) .* 1.0 +@static if VERSION < v"1.7" + @testset "Karate Net" begin + # auxiliary variables + nodelabel = collect(1:nv(g)) + nodesize = outdegree(g) .* 1.0 # test nodesize - plot_and_save1(fname) = plot_and_save(fname, g, nodesize=nodesize.^0.3, nodelabel=nodelabel, nodelabelsize=nodesize.^0.3) - refimg1 = joinpath(datadir, "karate_different_nodesize.png") - @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) - refimg2 = joinpath(datadir, "karate_straight_directed.png") - @test test_images(VisualTest(plot_and_save2, refimg2), popup=!istravis) |> save_comparison |> success - - # test node membership - membership = [1,1,1,1,1,1,1,1,2,1,1,1,1,1,2,2,1,1,2,1,2,1,2,2,2,2,2,2,2,2,2,2,2,2] - nodecolor = [colorant"lightseagreen", colorant"orange"] - nodefillc = nodecolor[membership] - 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 + plot_and_save1(fname) = plot_and_save(fname, g, nodesize=nodesize.^0.3, nodelabel=nodelabel, nodelabelsize=nodesize.^0.3) + refimg1 = joinpath(datadir, "karate_different_nodesize.png") + @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.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 + + # test node membership + membership = [1,1,1,1,1,1,1,1,2,1,1,1,1,1,2,2,1,1,2,1,2,1,2,2,2,2,2,2,2,2,2,2,2,2] + nodecolor = [colorant"lightseagreen", colorant"orange"] + nodefillc = nodecolor[membership] + + 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 end -@testset "WheelGraph" begin - # default options - plot_and_save1(fname) = plot_and_save(fname, h) - refimg1 = joinpath(datadir, "wheel10.png") - @test test_images(VisualTest(plot_and_save1, refimg1), popup=!istravis) |> save_comparison |> success +@static if VERSION < v"1.7" + @testset "WheelGraph" begin + # default options + plot_and_save1(fname) = plot_and_save(fname, h) + refimg1 = joinpath(datadir, "wheel10.png") + @test test_images(VisualTest(plot_and_save1, refimg1), popup=!istravis) |> save_comparison |> success + end end -@testset "Curves" begin +@static if VERSION < v"1.7" + @testset "Curves" begin + + g2 = DiGraph(2) + add_edge!(g2, 1,2) + add_edge!(g2, 2,1) - g2 = DiGraph(2) - add_edge!(g2, 1,2) - add_edge!(g2, 2,1) + 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 - plot_and_save1(fname) = plot_and_save(fname, g2, linetype="curve") - refimg1 = joinpath(datadir, "curve.png") - @test test_images(VisualTest(plot_and_save1, refimg1), popup=!istravis) |> save_comparison |> success + g3 = DiGraph(2) + add_edge!(g3, 1,1) + add_edge!(g3, 1,2) + add_edge!(g3, 2,1) - g3 = DiGraph(2) - add_edge!(g3, 1,1) - add_edge!(g3, 1,2) - add_edge!(g3, 2,1) + 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 +end + +@testset "Spring Layout" begin + g1 = path_digraph(3) + g2 = smallgraph(:house) + + # Neither seed nor initial locations provided + x1, y1 = spring_layout(g1; MAXITER=10) + @test length(x1) == nv(g1) + @test length(y1) == nv(g1) + + # Using a seed + x2, y2 = spring_layout(g1, 0; C = 1) + @test length(x2) == nv(g1) + @test length(y2) == nv(g1) + + # Using a rng + rng = StableRNG(123) + x3, y3 = spring_layout(g2, rng; INITTEMP = 7.5) + @test x3 ≈ [0.6417685918857294, -1.0, 1.0, -0.5032029640625139, 0.585415479582793] + @test y3 ≈ [-1.0, -0.7760280912987298, 0.06519424728464562, 0.2702599482349506, 1.0] + + # Using initial locations + locs_x_in = 1:5 + locs_y_in = [-1.0, 2.0, 0.3, 0.4, -0.5] + x4, y4 = spring_layout(g2, locs_x_in, locs_y_in) + @test x4 ≈ [-1.0, -0.4030585026962391, -0.050263101475789274, 0.5149349966578818, 1.0] + @test y4 ≈ [-0.03307638042475203, 1.0, -0.8197758901868164, 0.15834883764718155, -1.0] + + # Providing initial locations with the wrong lengths should throw an ArgumentError + @test_throws ArgumentError("The length of locs_x_in does not equal the number of vertices") spring_layout(g1, 1:5, [1,2,3]) + @test_throws ArgumentError("The length of locs_y_in does not equal the number of vertices") spring_layout(g2, 1:5, [1,2,3]) +end - plot_and_save2(fname) = plot_and_save(fname, g3, linetype="curve") - refimg2 = joinpath(datadir, "self_directed.png") - @test test_images(VisualTest(plot_and_save2, refimg2), popup=!istravis) |> save_comparison |> success +@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