Skip to content

blitzarx1/egui_graphs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

424 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

build Crates.io docs.rs

egui_graphs

Graph visualization with rust, petgraph and egui in its DNA.

ezgif-7312f131a6515c6e

The project provides a GraphView for the egui framework, enabling easy visualization of interactive graphs in rust. The goal is to implement the very basic engine for graph visualization within egui, which can be easily extended and customized for your needs.

Check the web-demo for the comprehensive overview of the widget possibilities.

  • Build wasm or native;
  • Layouts and custom layout mechanism;
  • Zooming and panning;
  • Node and edge interaction reporting: click, double click, select, hover, drag;
  • Node and Edge labels;
  • Dark/Light theme support via egui context styles;
  • User stroke styling hooks (node & edge) for dynamic customization;

Table of Contents

Status

The project is not in active development. Feel free to fork it and tweak for your needs.

Examples

Basic setup example

The source code of the following steps can be found in the basic example.

Step 1: Setting up the BasicApp struct

First, let's define the BasicApp struct that will hold the graph.

pub struct BasicApp {
    g: egui_graphs::Graph,
}

Step 2: Implementing the new() function

Next, implement the new() function for the BasicApp struct.

impl BasicApp {
    fn new(_: &eframe::CreationContext<'_>) -> Self {
        let g = generate_graph();
        Self { g: egui_graphs::Graph::from(&g) }
    }
}

Step 3: Generating the graph

Create a helper function called generate_graph(). In this example, we create three nodes and three edges.

fn generate_graph() -> petgraph::StableGraph<(), ()> {
    let mut g = petgraph::StableGraph::new();

    let a = g.add_node(());
    let b = g.add_node(());
    let c = g.add_node(());

    g.add_edge(a, b, ());
    g.add_edge(b, c, ());
    g.add_edge(c, a, ());

    g
}

Step 4: Implementing the eframe::App trait

Now, lets implement the eframe::App trait for the BasicApp. In the ui() function, we create an egui::CentralPanel and show the egui_graphs::GraphView in it.

impl eframe::App for BasicApp {
    fn ui(&mut self, ui: &mut egui::Ui, _: &mut eframe::Frame) {
        egui::CentralPanel::default().show(ui, |ui| {
            egui_graphs::DefaultGraphView::new().show(ui, &mut self.g);
        });
    }
}

Step 5: Running the application

Finally, run the application using the eframe::run_native() function.

fn main() {
    eframe::run_native(
        "egui_graphs_basic_demo",
        eframe::NativeOptions::default(),
        Box::new(|cc| Ok(Box::new(BasicApp::new(cc)))),
    )
    .unwrap();
}

Basic example screenshot

You can further customize the appearance and behavior of your graph by modifying the settings or adding more nodes and edges as needed.

Custom renderers

Custom DisplayNode and DisplayEdge implementations receive a DrawContext. Use ctx.style.labels_always(), ctx.style.resolve_node_stroke(...), and ctx.style.resolve_edge_stroke(...) inside those renderers to preserve the graph-wide label and stroke settings. The public EdgeShapeBuilder, EdgeShapeProps, and TipProps types can be used to construct the same straight, curved, looped, and arrow-tipped edge geometry as the default renderer without copying its internals.

Features

Layouts

Built-in layouts with a pluggable API. The Layout trait powers layout selection and persistence; you can plug different algorithms or implement your own.

  • Random: quick scatter for any graph (default via DefaultGraphView).
  • Hierarchical: layered (ranked) layout.
  • Force-directed: Fruchterman–Reingold baseline with optional Extras (e.g., Center Gravity).

Quick start

// Default random layout
egui_graphs::DefaultGraphView::new().show(ui, &mut graph);

// Pick a specific layout (Hierarchical)
type L = egui_graphs::LayoutHierarchical;
type S = egui_graphs::LayoutStateHierarchical;
egui_graphs::GraphView::<S, L>::new().show(ui, &mut graph);

// Force‑Directed (FR) with Center Gravity
type L = egui_graphs::LayoutForceDirected<egui_graphs::FruchtermanReingoldWithCenterGravity>;
type S = egui_graphs::FruchtermanReingoldWithCenterGravityState;
egui_graphs::GraphView::<S, L>::new().show(ui, &mut graph);

In-depth: Force‑Directed layout

A naive O(n²) force-directed layout (Fruchterman–Reingold style) is included. It exposes adjustable simulation parameters (step size, damping, etc.). See the demo for a live tuning panel. Built-in options include the baseline Fruchterman–Reingold and an extended variant with composable “extras” (e.g., Center Gravity).

Select algorithm via the layout type parameter (public aliases):

use egui_graphs::{LayoutForceDirected, FruchtermanReingold, FruchtermanReingoldState};

type L = LayoutForceDirected<FruchtermanReingold>;
type S = FruchtermanReingoldState;
egui_graphs::GraphView::<S, L>::new().show(ui, &mut graph);

Extras (composable add‑ons)

Use FruchtermanReingoldWithExtras<E> to apply base FR forces plus your extras each frame. Built-in extra: Center Gravity.

use egui_graphs::{
    LayoutForceDirected,
    FruchtermanReingoldWithCenterGravity,
    FruchtermanReingoldWithCenterGravityState,
};

type L = LayoutForceDirected<FruchtermanReingoldWithCenterGravity>;
type S = FruchtermanReingoldWithCenterGravityState;
let mut state = egui_graphs::get_layout_state::<S>(ui, None);
state.base.is_running = true;
state.extras.0.params.c = 0.2;
egui_graphs::set_layout_state(ui, state, None);
egui_graphs::GraphView::<S, L>::new().show(ui, &mut graph);
Author a custom extra

Implement ExtraForce and compose it through Extra<MyExtra, ENABLED> in a tuple. Extra-force parameters are persisted with layout state, so they must implement Serialize and Deserialize. The complete runnable version is in the custom_force example.

use egui::{Rect, Vec2};
use egui_graphs::{
    DisplayEdge, DisplayNode, Extra, ExtraForce, FruchtermanReingoldWithExtras,
    FruchtermanReingoldWithExtrasState, Graph, LayoutForceDirected,
};
use petgraph::EdgeType;
use serde::{Deserialize, Serialize};

#[derive(Debug, Default)]
struct PullToCenter;

#[derive(Clone, Debug, Default, Deserialize, Serialize)]
struct PullToCenterParams {
    strength: f32,
}

impl ExtraForce for PullToCenter {
    type Params = PullToCenterParams;

    fn apply<N, E, Ty, Ix, Dn, De>(
        params: &Self::Params,
        graph: &Graph<N, E, Ty, Ix, Dn, De>,
        indices: &[petgraph::stable_graph::NodeIndex<Ix>],
        displacements: &mut [Vec2],
        area: Rect,
        _k: f32,
    ) where
        N: Clone,
        E: Clone,
        Ty: EdgeType,
        Ix: petgraph::csr::IndexType,
        Dn: DisplayNode<N, E, Ty, Ix>,
        De: DisplayEdge<N, E, Ty, Ix, Dn>,
    {
        for (position, index) in indices.iter().enumerate() {
            let location = graph.g().node_weight(*index).unwrap().location();
            displacements[position] += (area.center() - location) * params.strength;
        }
    }
}

type Extras = (Extra<PullToCenter, true>, ());
type S = FruchtermanReingoldWithExtrasState<Extras>;
type L = LayoutForceDirected<FruchtermanReingoldWithExtras<Extras>>;
egui_graphs::GraphView::<S, L>::new().show(ui, &mut graph);

Composition is order-sensitive; each enabled extra accumulates into the shared displacement vector in tuple order.

GraphView response

GraphView::show returns a GraphViewResponse. Its response field is the standard egui::Response for the allocated graph area, while changes contains the graph-specific changes produced by that call:

let result = egui_graphs::DefaultGraphView::new().show(ui, &mut graph);

if result.response.hovered() {
    // The pointer is over this GraphView.
}

for change in result.changes {
    println!("{change:?}");
}

Changes are transient and ordered by occurrence. Repeated changes are kept as separate entries, and an interaction-free frame returns an empty vector. Variants involving nodes or edges use the graph's NodeIndex<Ix> or EdgeIndex<Ix> type; graph-wide changes such as pan and zoom do not depend on an entity index. Store or aggregate a batch in your application when it must outlive the current frame. See the graph_view_response example for a complete application.

Repository organization

Crates:

  • crates/egui_graphs – library crate published to crates.io
  • crates/demo-core – shared demo logic (not published)
  • crates/demo-web – WASM web demo (not published)

Build from the workspace root:

cargo build --workspace

Run locally

Run the web demo (WASM)

Prerequisites:

  • Rust toolchain
  • wasm target and trunk
rustup target add wasm32-unknown-unknown
cargo install trunk # if not installed

Serve locally:

cd crates/demo-web
trunk serve
# opens http://127.0.0.1:8080 (or similar)

Build static assets:

cd crates/demo-web
trunk build
# output in crates/demo-web/dist

Run any native example

From the workspace root, specify the package and the example name:

# demo example
cargo r --release --example demo

# another example (basic)
cargo r --release --example basic

# custom force-directed layout
cargo r --release --example custom_force

# inspect per-frame GraphView responses
cargo r --release --example graph_view_response

About

Interactive graph visualization widget for rust powered by egui and petgraph

Topics

Resources

License

Stars

687 stars

Watchers

7 watching

Forks

Contributors