From 92cf8ee8be6d81616c767e5539960e336eaee314 Mon Sep 17 00:00:00 2001 From: Thomas Christensen Date: Fri, 28 Mar 2025 10:48:41 +0100 Subject: [PATCH 1/2] implement `Align` (fix #79) --- docs/src/index.md | 17 ++++++++++++++++ src/NetworkLayout.jl | 1 + src/align.jl | 47 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+) create mode 100644 src/align.jl 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..04c6fb0 --- /dev/null +++ b/src/align.jl @@ -0,0 +1,47 @@ +export Align + +""" + Align(inner_layout :: AbstractLayout{2}, angle :: Real = zero(Float64)) + +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. +""" +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 From aabbbbbadc139de0e3f04453e3a7a94b7c8c31f6 Mon Sep 17 00:00:00 2001 From: Thomas Christensen Date: Fri, 28 Mar 2025 10:54:17 +0100 Subject: [PATCH 2/2] minor nits & use `@addcall` --- src/align.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/align.jl b/src/align.jl index 04c6fb0..44cddd7 100644 --- a/src/align.jl +++ b/src/align.jl @@ -1,14 +1,14 @@ export Align """ - Align(inner_layout :: AbstractLayout{2}, angle :: Real = zero(Float64)) + 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. """ -struct Align{Ptype, L <: AbstractLayout{2, Ptype}} <: AbstractLayout{2, Ptype} +@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