Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions crates/egui_graphs/examples/circular_layout.rs
Original file line number Diff line number Diff line change
@@ -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<dyn eframe::App>, _>(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()),
);
});
}
}
190 changes: 190 additions & 0 deletions crates/egui_graphs/src/layouts/circular/layout.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
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, Default)]
pub enum SortOrder {
/// Alphabetical by label (ascending)
#[default]
Alphabetical,
/// Reverse alphabetical by label (descending)
ReverseAlphabetical,
/// No sorting - preserve insertion order
None,
}

/// 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<f32>,
}

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, 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 {
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<State> for Circular {
fn from_state(state: State) -> impl Layout<State> {
Self {
state,
sort_order: SortOrder::default(),
spacing: SpacingConfig::default(),
}
}

fn next<N, E, Ty, Ix, Dn, De>(&mut self, g: &mut Graph<N, E, Ty, Ix, Dn, De>, ui: &egui::Ui)
where
N: Clone,
E: Clone,
Ty: EdgeType,
Ix: IndexType,
Dn: DisplayNode<N, E, Ty, Ix>,
De: DisplayEdge<N, E, Ty, Ix, Dn>,
{
// 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()
}
}
3 changes: 3 additions & 0 deletions crates/egui_graphs/src/layouts/circular/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mod layout;

pub use layout::{Circular, SortOrder, SpacingConfig, State};
1 change: 1 addition & 0 deletions crates/egui_graphs/src/layouts/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod circular;
pub mod force_directed;
pub mod hierarchical;
pub mod random;
Expand Down
4 changes: 4 additions & 0 deletions crates/egui_graphs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down