Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,25 @@ nothing #hide
```
![spring animation](spring_animation.mp4)

## Aligning Layouts

Any two-dimensional layout can have its principal axis aligned along a desired angle (default, zero angle), by nesting an "inner" layout into an [`Align`](@ref) layout.
For example, we may align the above `Spring` layout of the small cubical graph along the horizontal or vertical axes:

```@docs
Align
```
```@example layouts
g = smallgraph(:cubical)
f, ax, p = graphplot(g, layout=Align(Spring())) # horizontal alignment (zero angle by default)
hidedecorations!(ax); hidespines!(ax); ax.aspect = DataAspect(); f #hide
```

```@example layouts
f, ax, p = graphplot(g, layout=Align(Spring(), pi/2)) # vertical alignment
hidedecorations!(ax); hidespines!(ax); ax.aspect = DataAspect(); f #hide
```

## Stress Majorization
```@docs
Stress
Expand Down
3 changes: 2 additions & 1 deletion src/NetworkLayout.jl
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ macro addcall(expr::Expr)
@assert typedef isa Expr &&
typedef.head === :<: &&
typedef.args[2] isa Expr && # supertype
typedef.args[2].args[1] ∈ [:AbstractLayout, :IterativeLayout] "Macro musst be used on subtype of AbstractLayout"
typedef.args[2].args[1] ∈ [:AbstractLayout, :IterativeLayout] "Macro must be used on subtype of AbstractLayout"

if typedef.args[1] isa Symbol # no type parameters
name = typedef.args[1]
Expand All @@ -238,5 +238,6 @@ include("stress.jl")
include("spectral.jl")
include("shell.jl")
include("squaregrid.jl")
include("align.jl")

end
48 changes: 48 additions & 0 deletions src/align.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
export Align

"""
Align(inner_layout :: AbstractLayout{2, Ptype}, angle :: Ptype = zero(Ptype))

Align the vertex positions of `inner_layout` so that the principal axis of the resulting
layout makes an `angle` with the **x**-axis.
Also automatically centers the layout origin to its center of mass (average node position).

Only supports two-dimensional inner layouts.
"""
@addcall struct Align{Ptype, L <: AbstractLayout{2, Ptype}} <: AbstractLayout{2, Ptype}
inner_layout :: L
angle :: Ptype
function Align(inner_layout::L, angle::Real) where {L <: AbstractLayout{2, Ptype}} where Ptype
new{Ptype, L}(inner_layout, convert(Ptype, angle))
end
end
Align(inner_layout::AbstractLayout{2, Ptype}) where Ptype = Align(inner_layout, zero(Ptype))

function layout(algo::Align{Ptype, <:AbstractLayout{2, Ptype}}, adj_matrix::AbstractMatrix) where {Ptype}
# compute "inner" layout
rs = layout(algo.inner_layout, adj_matrix)

# align the "inner" layout to have its principal axis make `algo.angle` with x-axis
# step 1: compute covariance matrix for PCA analysis:
# C = ∑ᵢ (rᵢ - ⟨r⟩) (rᵢ - ⟨r⟩)ᵀ
# for vertex positions rᵢ, i = 1, …, N, and center of mass ⟨r⟩ = N⁻¹ ∑ᵢ rᵢ.
centerofmass = sum(rs) / length(rs)
C = zeros(SMatrix{2, 2, Ptype})
for r in rs
C += (r - centerofmass) * (r - centerofmass)'
end
vs = eigen(C).vectors

# step 2: pick principal axis (largest eigenvalue → last eigenvalue/vector)
axis = vs[:, end]
axis_angle = atan(axis[2], axis[1])

# step 3: rotate positions `rs` so that new axis is aligned with `algo.angle`
s, c = sincos(-axis_angle + algo.angle)
R = @SMatrix [c -s; s c] # [cos(θ) -sin(θ); sin(θ) cos(θ)]
for (i, r) in enumerate(rs)
rs[i] = Point2{Ptype}(R * (r-centerofmass)) :: Point2{Ptype}
end

return rs
end
20 changes: 20 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using NetworkLayout
using NetworkLayout: AbstractLayout, @addcall
using Graphs
using GeometryBasics
using DelimitedFiles: readdlm
Expand Down Expand Up @@ -465,4 +466,23 @@ jagmesh_adj = jagmesh()
@test ep[5][2] != [2]
end
end

@testset "Align" begin
@addcall struct Manual{Dim, Ptype} <: AbstractLayout{Dim, Ptype}
positions :: Vector{Point{Dim, Ptype}}
end
NetworkLayout.layout(algo::Manual, ::AbstractMatrix) = copy(algo.positions)

g = Graph(2); add_edge!(g, 1, 2)
pos = Align(Manual([Point2f(1, 2), Point2f(2, 3)]), 0.0)(g)
@test all(r->abs(r[2])<1e-12, pos)
@test norm(pos[1]-pos[2]) == norm(Point2f(1, 2)-Point2f(2, 3))

g = Graph(3); add_edge!(g, 1, 2); add_edge!(g, 2, 3); add_edge!(g, 3, 1)
pos = Align(Manual([Point2f(0, 4), Point2f(-1, -2), Point2f(1, -2)]), 0.0)(g)
@test pos ≈ [Point2f(4, 0), Point2f(-2, 1), Point2f(-2, -1)]

pos = Align(Manual([Point2f(0, 4), Point2f(-1, -2), Point2f(1, -2)]), π/2)(g)
@test pos ≈ [Point2f(0, 4), Point2f(-1, -2), Point2f(1, -2)]
end
end
Loading