Skip to content

Commit 1f854c2

Browse files
authored
Merge pull request #82 from JuliaGraphs/orientation
2 parents dfdb678 + d423648 commit 1f854c2

File tree

4 files changed

+89
-1
lines changed

4 files changed

+89
-1
lines changed

docs/src/index.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,25 @@ nothing #hide
104104
```
105105
![spring animation](spring_animation.mp4)
106106

107+
## Aligning Layouts
108+
109+
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.
110+
For example, we may align the above `Spring` layout of the small cubical graph along the horizontal or vertical axes:
111+
112+
```@docs
113+
Align
114+
```
115+
```@example layouts
116+
g = smallgraph(:cubical)
117+
f, ax, p = graphplot(g, layout=Align(Spring())) # horizontal alignment (zero angle by default)
118+
hidedecorations!(ax); hidespines!(ax); ax.aspect = DataAspect(); f #hide
119+
```
120+
121+
```@example layouts
122+
f, ax, p = graphplot(g, layout=Align(Spring(), pi/2)) # vertical alignment
123+
hidedecorations!(ax); hidespines!(ax); ax.aspect = DataAspect(); f #hide
124+
```
125+
107126
## Stress Majorization
108127
```@docs
109128
Stress

src/NetworkLayout.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ macro addcall(expr::Expr)
214214
@assert typedef isa Expr &&
215215
typedef.head === :<: &&
216216
typedef.args[2] isa Expr && # supertype
217-
typedef.args[2].args[1] [:AbstractLayout, :IterativeLayout] "Macro musst be used on subtype of AbstractLayout"
217+
typedef.args[2].args[1] [:AbstractLayout, :IterativeLayout] "Macro must be used on subtype of AbstractLayout"
218218

219219
if typedef.args[1] isa Symbol # no type parameters
220220
name = typedef.args[1]
@@ -238,5 +238,6 @@ include("stress.jl")
238238
include("spectral.jl")
239239
include("shell.jl")
240240
include("squaregrid.jl")
241+
include("align.jl")
241242

242243
end

src/align.jl

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
export Align
2+
3+
"""
4+
Align(inner_layout :: AbstractLayout{2, Ptype}, angle :: Ptype = zero(Ptype))
5+
6+
Align the vertex positions of `inner_layout` so that the principal axis of the resulting
7+
layout makes an `angle` with the **x**-axis.
8+
Also automatically centers the layout origin to its center of mass (average node position).
9+
10+
Only supports two-dimensional inner layouts.
11+
"""
12+
@addcall struct Align{Ptype, L <: AbstractLayout{2, Ptype}} <: AbstractLayout{2, Ptype}
13+
inner_layout :: L
14+
angle :: Ptype
15+
function Align(inner_layout::L, angle::Real) where {L <: AbstractLayout{2, Ptype}} where Ptype
16+
new{Ptype, L}(inner_layout, convert(Ptype, angle))
17+
end
18+
end
19+
Align(inner_layout::AbstractLayout{2, Ptype}) where Ptype = Align(inner_layout, zero(Ptype))
20+
21+
function layout(algo::Align{Ptype, <:AbstractLayout{2, Ptype}}, adj_matrix::AbstractMatrix) where {Ptype}
22+
# compute "inner" layout
23+
rs = layout(algo.inner_layout, adj_matrix)
24+
25+
# align the "inner" layout to have its principal axis make `algo.angle` with x-axis
26+
# step 1: compute covariance matrix for PCA analysis:
27+
# C = ∑ᵢ (rᵢ - ⟨r⟩) (rᵢ - ⟨r⟩)ᵀ
28+
# for vertex positions rᵢ, i = 1, …, N, and center of mass ⟨r⟩ = N⁻¹ ∑ᵢ rᵢ.
29+
centerofmass = sum(rs) / length(rs)
30+
C = zeros(SMatrix{2, 2, Ptype})
31+
for r in rs
32+
C += (r - centerofmass) * (r - centerofmass)'
33+
end
34+
vs = eigen(C).vectors
35+
36+
# step 2: pick principal axis (largest eigenvalue → last eigenvalue/vector)
37+
axis = vs[:, end]
38+
axis_angle = atan(axis[2], axis[1])
39+
40+
# step 3: rotate positions `rs` so that new axis is aligned with `algo.angle`
41+
s, c = sincos(-axis_angle + algo.angle)
42+
R = @SMatrix [c -s; s c] # [cos(θ) -sin(θ); sin(θ) cos(θ)]
43+
for (i, r) in enumerate(rs)
44+
rs[i] = Point2{Ptype}(R * (r-centerofmass)) :: Point2{Ptype}
45+
end
46+
47+
return rs
48+
end

test/runtests.jl

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using NetworkLayout
2+
using NetworkLayout: AbstractLayout, @addcall
23
using Graphs
34
using GeometryBasics
45
using DelimitedFiles: readdlm
@@ -465,4 +466,23 @@ jagmesh_adj = jagmesh()
465466
@test ep[5][2] != [2]
466467
end
467468
end
469+
470+
@testset "Align" begin
471+
@addcall struct Manual{Dim, Ptype} <: AbstractLayout{Dim, Ptype}
472+
positions :: Vector{Point{Dim, Ptype}}
473+
end
474+
NetworkLayout.layout(algo::Manual, ::AbstractMatrix) = copy(algo.positions)
475+
476+
g = Graph(2); add_edge!(g, 1, 2)
477+
pos = Align(Manual([Point2f(1, 2), Point2f(2, 3)]), 0.0)(g)
478+
@test all(r->abs(r[2])<1e-12, pos)
479+
@test norm(pos[1]-pos[2]) == norm(Point2f(1, 2)-Point2f(2, 3))
480+
481+
g = Graph(3); add_edge!(g, 1, 2); add_edge!(g, 2, 3); add_edge!(g, 3, 1)
482+
pos = Align(Manual([Point2f(0, 4), Point2f(-1, -2), Point2f(1, -2)]), 0.0)(g)
483+
@test pos [Point2f(4, 0), Point2f(-2, 1), Point2f(-2, -1)]
484+
485+
pos = Align(Manual([Point2f(0, 4), Point2f(-1, -2), Point2f(1, -2)]), π/2)(g)
486+
@test pos [Point2f(0, 4), Point2f(-1, -2), Point2f(1, -2)]
487+
end
468488
end

0 commit comments

Comments
 (0)