facett-graph 0.1.6

facett — graph (node/edge) viewer component, on facett-core
Documentation
//! **facett-graph** — the graph (node/edge) viewer component, on
//! [`facett_core`]. Build a [`Scene`] from a labelled edge list (the common
//! shape: a graph query, a dataflow DAG) and draw it. Source-agnostic — the
//! caller turns its Arrow/Cypher/whatever into edges first.

use std::collections::{BTreeMap, HashMap};

pub use facett_core::{Edge, Facet, FacetCaps, Layout, Node, Scene, Theme, draw, hash_color, set_theme, theme};

mod depgraph;
pub use depgraph::{DepEdge, DepGraphView, DepNode, draw_arrow};

/// Build a [`Scene`] from a labelled edge list — each row is
/// `(src_id, dst_id, src_label, dst_label)`. Distinct ids become nodes, coloured
/// per label (FNV-hashed). The reusable "graph from edges" adapter korp's Graph
/// and Pipelines views both want.
pub fn scene_from_labeled_edges<I>(rows: I) -> Scene
where
    I: IntoIterator<Item = (i64, i64, String, String)>,
{
    let mut scene = Scene::new();
    let mut idx: HashMap<i64, usize> = HashMap::new();
    for (s, d, sl, dl) in rows {
        let si = *idx.entry(s).or_insert_with(|| scene.node(sl.clone(), hash_color(&sl)));
        let di = *idx.entry(d).or_insert_with(|| scene.node(dl.clone(), hash_color(&dl)));
        scene.edge(si, di);
    }
    scene
}

/// A simple graph-view widget: a [`Scene`] + a [`Layout`]. Implements [`Facet`],
/// so it drops into a `FacetDeck` and exposes `state_json` for free.
pub struct GraphView {
    pub scene: Scene,
    pub layout: Layout,
    pub empty_hint: String,
    pub title: String,
}

impl GraphView {
    pub fn new(scene: Scene) -> Self {
        Self { scene, layout: Layout::default(), empty_hint: "empty".into(), title: "graph".into() }
    }
    pub fn empty_hint(mut self, hint: impl Into<String>) -> Self {
        self.empty_hint = hint.into();
        self
    }
    pub fn with_title(mut self, title: impl Into<String>) -> Self {
        self.title = title.into();
        self
    }
    pub fn show(&self, ui: &mut egui::Ui) {
        draw(ui, &self.scene, self.layout, &self.empty_hint);
    }
    /// Per-label node counts (for the introspection).
    pub fn label_counts(&self) -> BTreeMap<String, usize> {
        let mut m = BTreeMap::new();
        for n in &self.scene.nodes {
            *m.entry(n.label.clone()).or_insert(0) += 1;
        }
        m
    }
}

impl Facet for GraphView {
    fn title(&self) -> &str {
        &self.title
    }
    fn ui(&mut self, ui: &mut egui::Ui) {
        self.show(ui);
    }
    fn state_json(&self) -> serde_json::Value {
        serde_json::json!({
            "nodes": self.scene.nodes.len(),
            "edges": self.scene.edges.len(),
            "labels": self.label_counts(),
        })
    }
    /// Painted via [`draw`], which reads the active [`Theme`] (edges/labels/empty
    /// hint) from the context; its canvas takes the host's `available_size`.
    fn caps(&self) -> FacetCaps {
        FacetCaps::NONE.themeable().resizable()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn builds_scene_from_labeled_edges() {
        let scene = scene_from_labeled_edges(vec![
            (1, 2, "Person".into(), "Company".into()),
            (1, 3, "Person".into(), "Address".into()),
        ]);
        assert_eq!(scene.nodes.len(), 3, "1, 2, 3 distinct");
        assert_eq!(scene.edges.len(), 2);
        assert_eq!(scene.nodes[0].label, "Person");
    }
}