diff --git a/docs/src/index.md b/docs/src/index.md index 6e9ed50..a50e232 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -104,6 +104,23 @@ 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) +layout = Spring(Ptype=Float32) +f, ax, p = graphplot(g, layout=layout) # horizontal alignment (zero-angle by default) +hidedecorations!(ax); hidespines!(ax); ax.aspect = DataAspect(); f #hide +f, ax, p = graphplot(g, layout=Align(layout, pi/2)) # vertical alignment +hidedecorations!(ax); hidespines!(ax); ax.aspect = DataAspect(); f #hide +``` + ## Stress Majorization ```@docs Stress diff --git a/src/NetworkLayout.jl b/src/NetworkLayout.jl index 55dec9e..5c57638 100644 --- a/src/NetworkLayout.jl +++ b/src/NetworkLayout.jl @@ -238,5 +238,6 @@ include("stress.jl") include("spectral.jl") include("shell.jl") include("squaregrid.jl") +include("align.jl") end diff --git a/src/align.jl b/src/align.jl new file mode 100644 index 0000000..44cddd7 --- /dev/null +++ b/src/align.jl @@ -0,0 +1,47 @@ +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. + +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) :: Point2{Ptype} + end + + return rs +end \ No newline at end of file