From 92cf8ee8be6d81616c767e5539960e336eaee314 Mon Sep 17 00:00:00 2001 From: Thomas Christensen Date: Fri, 28 Mar 2025 10:48:41 +0100 Subject: [PATCH 1/8] 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/8] 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 From deb109cbeeadb4c1d25a3f2cbbec71b4f83fb79e Mon Sep 17 00:00:00 2001 From: Thomas Christensen Date: Fri, 28 Mar 2025 16:05:42 +0100 Subject: [PATCH 3/8] recenter to center-of-mass origin before rotation --- src/NetworkLayout.jl | 2 +- src/align.jl | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/NetworkLayout.jl b/src/NetworkLayout.jl index 5c57638..257fb88 100644 --- a/src/NetworkLayout.jl +++ b/src/NetworkLayout.jl @@ -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] diff --git a/src/align.jl b/src/align.jl index 44cddd7..cf6415b 100644 --- a/src/align.jl +++ b/src/align.jl @@ -5,6 +5,7 @@ export Align 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. """ @@ -40,7 +41,7 @@ function layout(algo::Align{Ptype, <:AbstractLayout{2, Ptype}}, adj_matrix::Abst 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} + rs[i] = Point2{Ptype}(R * (r-centerofmass)) :: Point2{Ptype} end return rs From 8ad9b2aad058a095627213d891e8972efafcc3a7 Mon Sep 17 00:00:00 2001 From: Thomas Christensen Date: Fri, 28 Mar 2025 16:05:52 +0100 Subject: [PATCH 4/8] add tests --- test/runtests.jl | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/runtests.jl b/test/runtests.jl index 42efe2f..fba8b45 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -465,4 +465,21 @@ jagmesh_adj = jagmesh() @test ep[5][2] != [2] end end + + @testset "Align" begin + using NetworkLayout: AbstractLayout, @addcall + @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)] + end end From ab52fcb3ced374340b859a8704077098f1be5ba4 Mon Sep 17 00:00:00 2001 From: Thomas Christensen Date: Fri, 28 Mar 2025 16:07:01 +0100 Subject: [PATCH 5/8] add test of nonzero angle --- test/runtests.jl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/runtests.jl b/test/runtests.jl index fba8b45..8991082 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -481,5 +481,8 @@ jagmesh_adj = jagmesh() 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 From e8dfd4347c1089e024929a1de6c4004431282d81 Mon Sep 17 00:00:00 2001 From: Thomas Christensen Date: Fri, 28 Mar 2025 16:11:33 +0100 Subject: [PATCH 6/8] fix import --- test/runtests.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index 8991082..bfe74c0 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,4 +1,5 @@ using NetworkLayout +using NetworkLayout: AbstractLayout, @addcall using Graphs using GeometryBasics using DelimitedFiles: readdlm @@ -467,7 +468,6 @@ jagmesh_adj = jagmesh() end @testset "Align" begin - using NetworkLayout: AbstractLayout, @addcall @addcall struct Manual{Dim, Ptype} <: AbstractLayout{Dim, Ptype} positions :: Vector{Point{Dim, Ptype}} end From cf71d1732e6e3bc49e5e203ed07aaf2c6b213ce3 Mon Sep 17 00:00:00 2001 From: Thomas Christensen Date: Fri, 28 Mar 2025 16:35:47 +0100 Subject: [PATCH 7/8] fix docs; show both horizontal and vertical versions --- docs/src/index.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/src/index.md b/docs/src/index.md index a50e232..bb21e88 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -115,8 +115,11 @@ Align ```@example layouts g = smallgraph(:cubical) layout = Spring(Ptype=Float32) -f, ax, p = graphplot(g, layout=layout) # horizontal alignment (zero-angle by default) +f, ax, p = graphplot(g, layout=layout) # horizontal alignment (zero angle by default) hidedecorations!(ax); hidespines!(ax); ax.aspect = DataAspect(); f #hide +``` + +```@example layouts f, ax, p = graphplot(g, layout=Align(layout, pi/2)) # vertical alignment hidedecorations!(ax); hidespines!(ax); ax.aspect = DataAspect(); f #hide ``` From d4236484aa57ac4513313390414b090f561d667d Mon Sep 17 00:00:00 2001 From: Thomas Christensen Date: Mon, 31 Mar 2025 10:38:04 +0200 Subject: [PATCH 8/8] fix docs: forgot to use `Align` in shown figure --- docs/src/index.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/src/index.md b/docs/src/index.md index bb21e88..d7d80a0 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -114,13 +114,12 @@ Align ``` ```@example layouts g = smallgraph(:cubical) -layout = Spring(Ptype=Float32) -f, ax, p = graphplot(g, layout=layout) # horizontal alignment (zero angle by default) +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(layout, pi/2)) # vertical alignment +f, ax, p = graphplot(g, layout=Align(Spring(), pi/2)) # vertical alignment hidedecorations!(ax); hidespines!(ax); ax.aspect = DataAspect(); f #hide ```