From 800c713552c64e3f2eac47cd18d010ece13070c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Raphael=20Diaz=20Sim=C3=B5es?= Date: Mon, 3 Nov 2025 05:24:15 +0400 Subject: [PATCH 1/3] Add circular layout --- .../egui_graphs/examples/circular_layout.rs | 71 ++++++ .../src/layouts/circular/layout.rs | 205 ++++++++++++++++++ .../egui_graphs/src/layouts/circular/mod.rs | 3 + crates/egui_graphs/src/layouts/mod.rs | 1 + crates/egui_graphs/src/lib.rs | 4 + 5 files changed, 284 insertions(+) create mode 100644 crates/egui_graphs/examples/circular_layout.rs create mode 100644 crates/egui_graphs/src/layouts/circular/layout.rs create mode 100644 crates/egui_graphs/src/layouts/circular/mod.rs diff --git a/crates/egui_graphs/examples/circular_layout.rs b/crates/egui_graphs/examples/circular_layout.rs new file mode 100644 index 0000000..1107457 --- /dev/null +++ b/crates/egui_graphs/examples/circular_layout.rs @@ -0,0 +1,71 @@ +use eframe::egui; +use egui_graphs::{ + DefaultEdgeShape, DefaultNodeShape, Graph, GraphView, LayoutCircular, LayoutStateCircular, + SettingsInteraction, SettingsStyle, +}; +use petgraph::{ + stable_graph::{DefaultIx, StableGraph}, + Directed, +}; + +fn main() -> eframe::Result<()> { + let native_options = eframe::NativeOptions::default(); + eframe::run_native( + "Circular Layout Example", + native_options, + Box::new(|_cc| Ok::, _>(Box::new(CircularExample::new()))), + ) +} + +struct CircularExample { + g: Graph<(), ()>, +} + +impl CircularExample { + fn new() -> Self { + let mut graph = StableGraph::new(); + + // Create some nodes + let nodes: Vec<_> = (0..8).map(|_| graph.add_node(())).collect(); + + // Add some edges in a ring + for i in 0..nodes.len() { + graph.add_edge(nodes[i], nodes[(i + 1) % nodes.len()], ()); + } + + let mut g = Graph::from(&graph); + + // Set labels + for (i, idx) in nodes.iter().enumerate() { + if let Some(node) = g.node_mut(*idx) { + node.set_label(format!("Node {}", i)); + } + } + + Self { g } + } +} + +impl eframe::App for CircularExample { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::CentralPanel::default().show(ctx, |ui| { + ui.heading("Circular Layout Example"); + ui.label("8 nodes arranged in a circle"); + + ui.add( + &mut GraphView::< + (), + (), + Directed, + DefaultIx, + DefaultNodeShape, + DefaultEdgeShape, + LayoutStateCircular, + LayoutCircular, + >::new(&mut self.g) + .with_interactions(&SettingsInteraction::new()) + .with_styles(&SettingsStyle::new()), + ); + }); + } +} diff --git a/crates/egui_graphs/src/layouts/circular/layout.rs b/crates/egui_graphs/src/layouts/circular/layout.rs new file mode 100644 index 0000000..38e0be2 --- /dev/null +++ b/crates/egui_graphs/src/layouts/circular/layout.rs @@ -0,0 +1,205 @@ +use egui; +use serde::{Deserialize, Serialize}; + +use crate::graph::Graph; +use crate::layouts::{Layout, LayoutState}; +use crate::{DisplayEdge, DisplayNode}; +use petgraph::graph::IndexType; +use petgraph::EdgeType; + +/// State for the circular layout algorithm +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct State { + applied: bool, +} + +impl LayoutState for State {} + +/// Sort order for circular layout nodes +#[derive(Debug, Clone)] +pub enum SortOrder { + /// Alphabetical by label (ascending) + Alphabetical, + /// Reverse alphabetical by label (descending) + ReverseAlphabetical, + /// No sorting - preserve insertion order + None, +} + +impl Default for SortOrder { + fn default() -> Self { + SortOrder::Alphabetical + } +} + +/// Configuration for spacing/radius of the circular layout +#[derive(Debug, Clone)] +pub struct SpacingConfig { + /// Base radius when there are few nodes + pub base_radius: f32, + /// Additional radius per node (for auto-scaling) + pub radius_per_node: f32, + /// If set, overrides the auto-calculated radius + pub fixed_radius: Option, +} + +impl Default for SpacingConfig { + fn default() -> Self { + Self { + base_radius: 50.0, + radius_per_node: 5.0, + fixed_radius: None, + } + } +} + +impl SpacingConfig { + /// Set the base radius for the circle + pub fn with_base_radius(mut self, base: f32) -> Self { + self.base_radius = base; + self + } + + /// Set the additional radius per node for auto-scaling + pub fn with_radius_per_node(mut self, per_node: f32) -> Self { + self.radius_per_node = per_node; + self + } + + /// Set a fixed radius, overriding auto-scaling + pub fn with_fixed_radius(mut self, radius: f32) -> Self { + self.fixed_radius = Some(radius); + self + } +} + +/// Circular layout arranges nodes in a circle. +/// +/// Nodes are positioned evenly around a circle with configurable: +/// +/// - Sort order (alphabetical, reverse, or insertion order) +/// - Spacing (auto-scaling or fixed radius) +/// +/// The layout applies once and preserves the circular arrangement. +#[derive(Debug, Clone)] +pub struct Circular { + state: State, + sort_order: SortOrder, + spacing: SpacingConfig, +} + +impl Default for Circular { + fn default() -> Self { + Self { + state: State::default(), + sort_order: SortOrder::default(), + spacing: SpacingConfig::default(), + } + } +} + +impl Circular { + /// Create a new circular layout with default configuration + pub fn new() -> Self { + Self::default() + } + + /// Set the sort order for nodes around the circle + pub fn with_sort_order(mut self, sort_order: SortOrder) -> Self { + self.sort_order = sort_order; + self + } + + /// Disable sorting, preserving insertion order + pub fn without_sorting(mut self) -> Self { + self.sort_order = SortOrder::None; + self + } + + /// Set custom spacing configuration + pub fn with_spacing(mut self, spacing: SpacingConfig) -> Self { + self.spacing = spacing; + self + } +} + +impl Layout for Circular { + fn from_state(state: State) -> impl Layout { + Self { + state, + sort_order: SortOrder::default(), + spacing: SpacingConfig::default(), + } + } + + fn next(&mut self, g: &mut Graph, ui: &egui::Ui) + where + N: Clone, + E: Clone, + Ty: EdgeType, + Ix: IndexType, + Dn: DisplayNode, + De: DisplayEdge, + { + // Only apply layout once + if self.state.applied { + return; + } + + // Collect all nodes with their indices and labels + let mut nodes: Vec<_> = g + .nodes_iter() + .map(|(idx, node)| (idx, node.label().to_string())) + .collect(); + + // Sort according to the configured sort order + match self.sort_order { + SortOrder::Alphabetical => { + nodes.sort_by(|a, b| a.1.cmp(&b.1)); + } + SortOrder::ReverseAlphabetical => { + nodes.sort_by(|a, b| b.1.cmp(&a.1)); + } + SortOrder::None => { + // Keep insertion order - no sorting + } + } + + let node_count = nodes.len(); + if node_count == 0 { + return; + } + + // Calculate center of the canvas + let rect = ui.available_rect_before_wrap(); + let center_x = rect.center().x; + let center_y = rect.center().y; + + // Calculate radius using configuration + let radius = if let Some(fixed) = self.spacing.fixed_radius { + fixed + } else { + self.spacing.base_radius + (node_count as f32) * self.spacing.radius_per_node + }; + + // Place nodes in a circle + for (i, (node_idx, _label)) in nodes.iter().enumerate() { + // Start at top (-π/2) and go clockwise + let angle = -std::f32::consts::PI / 2.0 + + (i as f32) * 2.0 * std::f32::consts::PI / (node_count as f32); + + let x = center_x + radius * angle.cos(); + let y = center_y + radius * angle.sin(); + + if let Some(node) = g.node_mut(*node_idx) { + node.set_location(egui::Pos2::new(x, y)); + } + } + + self.state.applied = true; + } + + fn state(&self) -> State { + self.state.clone() + } +} diff --git a/crates/egui_graphs/src/layouts/circular/mod.rs b/crates/egui_graphs/src/layouts/circular/mod.rs new file mode 100644 index 0000000..b08a81b --- /dev/null +++ b/crates/egui_graphs/src/layouts/circular/mod.rs @@ -0,0 +1,3 @@ +mod layout; + +pub use layout::{Circular, SortOrder, SpacingConfig, State}; diff --git a/crates/egui_graphs/src/layouts/mod.rs b/crates/egui_graphs/src/layouts/mod.rs index a2869ba..e065a82 100644 --- a/crates/egui_graphs/src/layouts/mod.rs +++ b/crates/egui_graphs/src/layouts/mod.rs @@ -1,3 +1,4 @@ +pub mod circular; pub mod force_directed; pub mod hierarchical; pub mod random; diff --git a/crates/egui_graphs/src/lib.rs b/crates/egui_graphs/src/lib.rs index f526137..7651059 100644 --- a/crates/egui_graphs/src/lib.rs +++ b/crates/egui_graphs/src/lib.rs @@ -21,6 +21,10 @@ pub use helpers::{ generate_simple_ungraph, node_size, to_graph, to_graph_custom, }; +pub use layouts::circular::{ + Circular as LayoutCircular, SortOrder as LayoutCircularSortOrder, + SpacingConfig as LayoutCircularSpacingConfig, State as LayoutStateCircular, +}; pub use layouts::force_directed::{ CenterGravity, CenterGravityParams, Extra, ForceAlgorithm, ForceDirected as LayoutForceDirected, FruchtermanReingold, FruchtermanReingoldState, From 2b714b54dfd92e5b7c0114bb99a34ff6d322014a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Raphael=20Diaz=20Sim=C3=B5es?= Date: Thu, 6 Nov 2025 21:33:26 +0400 Subject: [PATCH 2/3] Fix cargo clippy --- .../egui_graphs/src/layouts/circular/layout.rs | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/crates/egui_graphs/src/layouts/circular/layout.rs b/crates/egui_graphs/src/layouts/circular/layout.rs index 38e0be2..3e23f2f 100644 --- a/crates/egui_graphs/src/layouts/circular/layout.rs +++ b/crates/egui_graphs/src/layouts/circular/layout.rs @@ -17,8 +17,10 @@ impl LayoutState for State {} /// Sort order for circular layout nodes #[derive(Debug, Clone)] +#[derive(Default)] pub enum SortOrder { /// Alphabetical by label (ascending) + #[default] Alphabetical, /// Reverse alphabetical by label (descending) ReverseAlphabetical, @@ -26,11 +28,6 @@ pub enum SortOrder { None, } -impl Default for SortOrder { - fn default() -> Self { - SortOrder::Alphabetical - } -} /// Configuration for spacing/radius of the circular layout #[derive(Debug, Clone)] @@ -82,21 +79,13 @@ impl SpacingConfig { /// /// The layout applies once and preserves the circular arrangement. #[derive(Debug, Clone)] +#[derive(Default)] pub struct Circular { state: State, sort_order: SortOrder, spacing: SpacingConfig, } -impl Default for Circular { - fn default() -> Self { - Self { - state: State::default(), - sort_order: SortOrder::default(), - spacing: SpacingConfig::default(), - } - } -} impl Circular { /// Create a new circular layout with default configuration From 89b72aa0c329488ab32e420365b2f8c7421ea39a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Raphael=20Diaz=20Sim=C3=B5es?= Date: Fri, 7 Nov 2025 06:25:24 +0400 Subject: [PATCH 3/3] Run cargo fmt --- crates/egui_graphs/src/layouts/circular/layout.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/crates/egui_graphs/src/layouts/circular/layout.rs b/crates/egui_graphs/src/layouts/circular/layout.rs index 3e23f2f..c294995 100644 --- a/crates/egui_graphs/src/layouts/circular/layout.rs +++ b/crates/egui_graphs/src/layouts/circular/layout.rs @@ -16,8 +16,7 @@ pub struct State { impl LayoutState for State {} /// Sort order for circular layout nodes -#[derive(Debug, Clone)] -#[derive(Default)] +#[derive(Debug, Clone, Default)] pub enum SortOrder { /// Alphabetical by label (ascending) #[default] @@ -28,7 +27,6 @@ pub enum SortOrder { None, } - /// Configuration for spacing/radius of the circular layout #[derive(Debug, Clone)] pub struct SpacingConfig { @@ -78,15 +76,13 @@ impl SpacingConfig { /// - Spacing (auto-scaling or fixed radius) /// /// The layout applies once and preserves the circular arrangement. -#[derive(Debug, Clone)] -#[derive(Default)] +#[derive(Debug, Clone, Default)] pub struct Circular { state: State, sort_order: SortOrder, spacing: SpacingConfig, } - impl Circular { /// Create a new circular layout with default configuration pub fn new() -> Self {