Add counting of detached nodes
This commit is contained in:
parent
6feb20d3da
commit
41a5994bbd
7 changed files with 109 additions and 18 deletions
43
src/graph.rs
43
src/graph.rs
|
|
@ -30,6 +30,13 @@ pub struct Graph {
|
||||||
pub lowercase_keymap: HashMap<String, String>,
|
pub lowercase_keymap: HashMap<String, String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub meta: Meta,
|
pub meta: Meta,
|
||||||
|
#[serde(default)]
|
||||||
|
pub stats: Stats,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, Debug)]
|
||||||
|
pub struct Stats {
|
||||||
|
pub detached: HashMap<String, u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default, Debug)]
|
#[derive(Clone, Default, Debug)]
|
||||||
|
|
@ -160,8 +167,11 @@ impl Graph {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flag detached edges
|
// Flag detached edges
|
||||||
if !in_nodes.contains_key(&edge.to) {
|
if in_nodes.contains_key(&edge.to) {
|
||||||
new_edge.detached = false;
|
new_edge.detached = false;
|
||||||
|
} else {
|
||||||
|
new_edge.detached = true;
|
||||||
|
self.increment_detached(&key);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(e) = new_edges.get_mut(&connection_key) {
|
if let Some(e) = new_edges.get_mut(&connection_key) {
|
||||||
|
|
@ -171,14 +181,19 @@ impl Graph {
|
||||||
|
|
||||||
// Create connections for each link
|
// Create connections for each link
|
||||||
for link in &node.links {
|
for link in &node.links {
|
||||||
|
let detached = !in_nodes.contains_key(link);
|
||||||
new_edges.insert(
|
new_edges.insert(
|
||||||
link.clone(),
|
link.clone(),
|
||||||
Edge {
|
Edge {
|
||||||
from: key.clone(),
|
from: key.clone(),
|
||||||
to: link.clone(),
|
to: link.clone(),
|
||||||
detached: !in_nodes.clone().contains_key(link),
|
detached,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if detached {
|
||||||
|
self.increment_detached(&key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate empty titles with IDs
|
// Populate empty titles with IDs
|
||||||
|
|
@ -269,10 +284,34 @@ 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) {
|
pub fn map_lowercase_keys(&mut self) {
|
||||||
for key in self.nodes.keys() {
|
for key in self.nodes.keys() {
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,15 @@ pub struct Node {
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub connections: Option<HashMap<String, Edge>>,
|
pub connections: Option<HashMap<String, Edge>>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub stats: Stats,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Default, Eq, PartialEq, Debug)]
|
||||||
|
pub struct Stats {
|
||||||
|
pub outgoing: u32,
|
||||||
|
pub incoming: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Node {
|
impl Node {
|
||||||
|
|
@ -39,6 +48,7 @@ impl Node {
|
||||||
redirect: String::default(),
|
redirect: String::default(),
|
||||||
hidden: false,
|
hidden: false,
|
||||||
summary: String::default(),
|
summary: String::default(),
|
||||||
|
stats: Stats::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,13 @@ pub fn new(graph: &Graph) -> Router {
|
||||||
get(|| handlers::navigation::page("index.html"))
|
get(|| handlers::navigation::page("index.html"))
|
||||||
.post(handlers::navigation::search),
|
.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("/search", get(handlers::navigation::search))
|
||||||
|
.route("/redirect", get(handlers::navigation::redirect))
|
||||||
.route(
|
.route(
|
||||||
"/static/style.css",
|
"/static/style.css",
|
||||||
get(|| handlers::fixed::file("./static/style.css", "text/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")
|
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);
|
.fallback(handlers::error::not_found);
|
||||||
|
|
||||||
if graph.meta.config.about {
|
if graph.meta.config.about {
|
||||||
|
|
@ -47,8 +48,6 @@ pub fn new(graph: &Graph) -> Router {
|
||||||
}
|
}
|
||||||
|
|
||||||
if graph.meta.config.raw {
|
if graph.meta.config.raw {
|
||||||
router = router
|
|
||||||
.route("/data", get(|| handlers::navigation::page("data.html")));
|
|
||||||
if graph.meta.config.raw_json {
|
if graph.meta.config.raw_json {
|
||||||
router = router.route(
|
router = router.route(
|
||||||
"/graph/json",
|
"/graph/json",
|
||||||
|
|
|
||||||
|
|
@ -14,16 +14,27 @@ use crate::{
|
||||||
pub async fn page(template: &str) -> Response<Body> {
|
pub async fn page(template: &str) -> Response<Body> {
|
||||||
let mut context = tera::Context::default();
|
let mut context = tera::Context::default();
|
||||||
let graph = Graph::load();
|
let graph = Graph::load();
|
||||||
let root_node = graph.get_root().unwrap_or_default();
|
|
||||||
let nodes: Vec<Node> = graph.nodes.into_values().collect();
|
|
||||||
|
|
||||||
context.insert("nodes", &nodes);
|
context.insert("graph", &graph);
|
||||||
context.insert("root_node", &root_node);
|
|
||||||
context.insert("config", &graph.meta.config);
|
|
||||||
|
|
||||||
handlers::template::by_filename(template, &context, 500, None, false)
|
handlers::template::by_filename(template, &context, 500, None, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn data() -> Response<Body> {
|
||||||
|
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<Query>) -> Redirect {
|
pub async fn search(Form(query): Form<Query>) -> Redirect {
|
||||||
Redirect::permanent(format!("/node/{}", query.node).as_str())
|
Redirect::permanent(format!("/node/{}", query.node).as_str())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -539,10 +539,9 @@ text = """
|
||||||
- [ ] Example (implies metadata `has_example`)
|
- [ ] Example (implies metadata `has_example`)
|
||||||
- [ ] References/influences (implies metadata `has_references`)
|
- [ ] References/influences (implies metadata `has_references`)
|
||||||
- [ ] Meta-awareness
|
- [ ] Meta-awareness
|
||||||
- [ ] Detached edges
|
- [x] Detached edges
|
||||||
- [ ] Most linked to nodes
|
- [ ] Most linked to nodes
|
||||||
- [ ] Most linked from nodes
|
- [ ] Most linked from nodes
|
||||||
- [ ] Most linked to nonexistent nodes
|
|
||||||
- [ ] Most linked
|
- [ ] Most linked
|
||||||
- [ ] Rendering
|
- [ ] Rendering
|
||||||
- [ ] Sorting of tree, index list and drop-down navigation
|
- [ ] Sorting of tree, index list and drop-down navigation
|
||||||
|
|
|
||||||
|
|
@ -163,6 +163,17 @@ em#index-node-count {
|
||||||
font-size: 0.8em;
|
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) {
|
@media (prefers-color-scheme: dark) {
|
||||||
* {
|
* {
|
||||||
background-color: #222222;
|
background-color: #222222;
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,36 @@
|
||||||
{%- block body %}
|
{%- block body %}
|
||||||
<h1>Data</h1>
|
<h1>Data</h1>
|
||||||
|
|
||||||
|
<h2>Metadata</h2>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td>Detached edges</td> <td>{{ detached_count }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3>Detached edges</h3>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Expand to list all detached edges.</summary>
|
||||||
|
<ul>
|
||||||
|
{% for pair in detached_pairs %}
|
||||||
|
<li>{{ pair.0 }}: {{ pair.1 }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
{% if graph.meta.config.raw_toml or graph.meta.config.raw_json %}
|
||||||
|
<h2>Raw formats</h2>
|
||||||
<p>The raw data used to render this graph is available in the following formats:</p>
|
<p>The raw data used to render this graph is available in the following formats:</p>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
{% if config.raw_toml %}
|
{% if graph.meta.config.raw_toml %}
|
||||||
<li><a href="/graph/toml">TOML</a></li>
|
<li><a href="/graph/toml">TOML</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if config.raw_json %}
|
{% if graph.meta.config.raw_json %}
|
||||||
<li><a href="/graph/json">JSON</a></li>
|
<li><a href="/graph/json">JSON</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
{% endif %}
|
||||||
{%- endblock body %}
|
{%- endblock body %}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue