From 41a5994bbd65fec2f5998f0950a5a6e10dd8f9b2 Mon Sep 17 00:00:00 2001 From: jutty Date: Tue, 13 Jan 2026 15:32:36 -0300 Subject: [PATCH] Add counting of detached nodes --- src/graph.rs | 43 +++++++++++++++++++++++++++++-- src/graph/node.rs | 10 +++++++ src/router.rs | 13 +++++----- src/router/handlers/navigation.rs | 21 +++++++++++---- static/graph.toml | 3 +-- static/style.css | 11 ++++++++ templates/data.html | 26 +++++++++++++++++-- 7 files changed, 109 insertions(+), 18 deletions(-) diff --git a/src/graph.rs b/src/graph.rs index 47361c4..ef443e0 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -30,6 +30,13 @@ pub struct Graph { pub lowercase_keymap: HashMap, #[serde(default)] pub meta: Meta, + #[serde(default)] + pub stats: Stats, +} + +#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, Debug)] +pub struct Stats { + pub detached: HashMap, } #[derive(Clone, Default, Debug)] @@ -160,8 +167,11 @@ impl Graph { } // Flag detached edges - if !in_nodes.contains_key(&edge.to) { + if in_nodes.contains_key(&edge.to) { new_edge.detached = false; + } else { + new_edge.detached = true; + self.increment_detached(&key); } if let Some(e) = new_edges.get_mut(&connection_key) { @@ -171,14 +181,19 @@ impl Graph { // Create connections for each link for link in &node.links { + let detached = !in_nodes.contains_key(link); new_edges.insert( link.clone(), Edge { from: key.clone(), to: link.clone(), - detached: !in_nodes.clone().contains_key(link), + detached, }, ); + + if detached { + self.increment_detached(&key); + } } // Populate empty titles with IDs @@ -269,11 +284,35 @@ impl Graph { }, ); } + } else { + if let Some(destination) = anchor.destination() + && !anchor.external() + { + self.stats + .detached + .entry( + destination + .trim_start_matches("/node/") + .to_string(), + ) + .and_modify(|count| { + *count = count.saturating_add(1) + }) + .or_insert(1); + } } } } } + fn increment_detached(&mut self, node_id: &str) { + self.stats + .detached + .entry(node_id.to_string()) + .and_modify(|count| *count = count.saturating_add(1)) + .or_insert(1); + } + pub fn map_lowercase_keys(&mut self) { for key in self.nodes.keys() { self.lowercase_keymap diff --git a/src/graph/node.rs b/src/graph/node.rs index b4d8bb9..6d2071f 100644 --- a/src/graph/node.rs +++ b/src/graph/node.rs @@ -23,6 +23,15 @@ pub struct Node { #[serde(skip_serializing_if = "Option::is_none")] pub connections: Option>, + + #[serde(default)] + pub stats: Stats, +} + +#[derive(Serialize, Deserialize, Clone, Default, Eq, PartialEq, Debug)] +pub struct Stats { + pub outgoing: u32, + pub incoming: u32, } impl Node { @@ -39,6 +48,7 @@ impl Node { redirect: String::default(), hidden: false, summary: String::default(), + stats: Stats::default(), } } } diff --git a/src/router.rs b/src/router.rs index b799470..c84ffa8 100644 --- a/src/router.rs +++ b/src/router.rs @@ -18,8 +18,13 @@ pub fn new(graph: &Graph) -> Router { get(|| handlers::navigation::page("index.html")) .post(handlers::navigation::search), ) - .route("/redirect", get(handlers::navigation::redirect)) + .route( + "/node/{node_id}", + get(handlers::graph::node).post(handlers::graph::node), + ) + .route("/data", get(handlers::navigation::data)) .route("/search", get(handlers::navigation::search)) + .route("/redirect", get(handlers::navigation::redirect)) .route( "/static/style.css", get(|| handlers::fixed::file("./static/style.css", "text/css")), @@ -30,10 +35,6 @@ pub fn new(graph: &Graph) -> Router { handlers::fixed::file("./static/favicon.svg", "image/svg+xml") }), ) - .route( - "/node/{node_id}", - get(handlers::graph::node).post(handlers::graph::node), - ) .fallback(handlers::error::not_found); if graph.meta.config.about { @@ -47,8 +48,6 @@ pub fn new(graph: &Graph) -> Router { } if graph.meta.config.raw { - router = router - .route("/data", get(|| handlers::navigation::page("data.html"))); if graph.meta.config.raw_json { router = router.route( "/graph/json", diff --git a/src/router/handlers/navigation.rs b/src/router/handlers/navigation.rs index 35ccd7a..924ec0b 100644 --- a/src/router/handlers/navigation.rs +++ b/src/router/handlers/navigation.rs @@ -14,16 +14,27 @@ use crate::{ pub async fn page(template: &str) -> Response { let mut context = tera::Context::default(); let graph = Graph::load(); - let root_node = graph.get_root().unwrap_or_default(); - let nodes: Vec = graph.nodes.into_values().collect(); - context.insert("nodes", &nodes); - context.insert("root_node", &root_node); - context.insert("config", &graph.meta.config); + context.insert("graph", &graph); handlers::template::by_filename(template, &context, 500, None, false) } +pub async fn data() -> Response { + let mut context = tera::Context::default(); + let graph = Graph::load(); + + let mut detached_pairs: Vec<(String, u32)> = + graph.stats.detached.clone().into_iter().collect(); + detached_pairs.sort_by(|a, b| b.1.cmp(&a.1)); + + context.insert("graph", &graph); + context.insert("detached_count", &graph.stats.detached.len()); + context.insert("detached_pairs", &detached_pairs); + + handlers::template::by_filename("data.html", &context, 500, None, false) +} + pub async fn search(Form(query): Form) -> Redirect { Redirect::permanent(format!("/node/{}", query.node).as_str()) } diff --git a/static/graph.toml b/static/graph.toml index 9c5f93a..f47c1d3 100644 --- a/static/graph.toml +++ b/static/graph.toml @@ -539,10 +539,9 @@ text = """ - [ ] Example (implies metadata `has_example`) - [ ] References/influences (implies metadata `has_references`) - [ ] Meta-awareness - - [ ] Detached edges + - [x] Detached edges - [ ] Most linked to nodes - [ ] Most linked from nodes - - [ ] Most linked to nonexistent nodes - [ ] Most linked - [ ] Rendering - [ ] Sorting of tree, index list and drop-down navigation diff --git a/static/style.css b/static/style.css index e7b39f1..aed772c 100644 --- a/static/style.css +++ b/static/style.css @@ -163,6 +163,17 @@ em#index-node-count { font-size: 0.8em; } +table { + margin: auto; + border-collapse: collapse; + border: 0.5px dotted #666; +} + +td, th { + padding: 10px; + border: 0.5px dotted #666; +} + @media (prefers-color-scheme: dark) { * { background-color: #222222; diff --git a/templates/data.html b/templates/data.html index e9aa513..41a525b 100644 --- a/templates/data.html +++ b/templates/data.html @@ -6,14 +6,36 @@ {%- block body %}

Data

+

Metadata

+ + + + + +
Detached edges {{ detached_count }}
+ +

Detached edges

+ +
+Expand to list all detached edges. +
    + {% for pair in detached_pairs %} +
  • {{ pair.0 }}: {{ pair.1 }}
  • + {% endfor %} +
+
+ +{% if graph.meta.config.raw_toml or graph.meta.config.raw_json %} +

Raw formats

The raw data used to render this graph is available in the following formats:

    - {% if config.raw_toml %} + {% if graph.meta.config.raw_toml %}
  • TOML
  • {% endif %} - {% if config.raw_json %} + {% if graph.meta.config.raw_json %}
  • JSON
  • {% endif %}
+{% endif %} {%- endblock body %}