Skip to main content

pedant_graph/analysis/
layout.rs

1//! What something drawing or ranking a graph would otherwise derive itself.
2//!
3//! One typed projection of facts this crate already owns: the raw identities and
4//! their containment, the partition that declares each node, the selected
5//! degrees, and the same bounded betweenness result the public metric answers
6//! with. It decides nothing about how a graph looks — there is no coordinate,
7//! no route, no ordering, and no drawing state here, and the version-1 graph
8//! object stays exactly as it was.
9//!
10//! The bounded pass runs first, so a graph beyond the caller's stated budget
11//! refuses with that pass's own refusal, unchanged, before any metadata exists.
12
13use crate::edge::{GraphCertainty, GraphEdgeKind};
14use crate::graph::CodeGraph;
15use crate::id::{GraphEdgeId, GraphNodeId, index_of, position};
16
17use super::betweenness::{self, BetweennessCentrality};
18use super::degree::{self, DegreeCentrality};
19use super::error::GraphAnalysisError;
20use super::index::{AnalysisContext, SelectedAdjacency, SelectedIndexes};
21
22/// What one node is, is inside, and measures as.
23#[derive(Clone, Copy, Debug)]
24pub struct LayoutNodeMetadata {
25    node: GraphNodeId,
26    parent: Option<GraphNodeId>,
27    partition: GraphNodeId,
28    incoming: u32,
29    outgoing: u32,
30    raw: f64,
31    normalized: f64,
32}
33
34impl LayoutNodeMetadata {
35    /// The node this metadata describes.
36    pub fn node(self) -> GraphNodeId {
37        self.node
38    }
39
40    /// The node containing it, if anything does.
41    pub fn containment_parent(self) -> Option<GraphNodeId> {
42        self.parent
43    }
44
45    /// The partition root that declares it.
46    pub fn partition(self) -> GraphNodeId {
47        self.partition
48    }
49
50    /// How many selected edges arrive at it.
51    pub fn incoming_degree(self) -> u32 {
52        self.incoming
53    }
54
55    /// How many selected edges leave it.
56    pub fn outgoing_degree(self) -> u32 {
57        self.outgoing
58    }
59
60    /// The summed share of shortest routes passing through it.
61    pub fn raw_betweenness(self) -> f64 {
62        self.raw
63    }
64
65    /// That share against every ordered pair a graph this size can state.
66    pub fn normalized_betweenness(self) -> f64 {
67        self.normalized
68    }
69}
70
71/// One selected edge, restated with both of its ends and its own evidence.
72#[derive(Clone, Copy, Debug)]
73pub struct LayoutEdgeMetadata {
74    edge: GraphEdgeId,
75    source: GraphNodeId,
76    target: GraphNodeId,
77    kind: GraphEdgeKind,
78    certainty: GraphCertainty,
79}
80
81impl LayoutEdgeMetadata {
82    /// The raw edge this metadata describes.
83    pub fn edge(self) -> GraphEdgeId {
84        self.edge
85    }
86
87    /// The node it leaves.
88    pub fn source(self) -> GraphNodeId {
89        self.source
90    }
91
92    /// The node it reaches.
93    pub fn target(self) -> GraphNodeId {
94        self.target
95    }
96
97    /// What relation it states.
98    pub fn kind(self) -> GraphEdgeKind {
99        self.kind
100    }
101
102    /// How certain that relation is.
103    pub fn certainty(self) -> GraphCertainty {
104        self.certainty
105    }
106}
107
108/// Every admitted node and every selected edge, described once.
109#[derive(Debug)]
110pub struct LayoutAssistMetadata {
111    nodes: Box<[LayoutNodeMetadata]>,
112    edges: Box<[LayoutEdgeMetadata]>,
113}
114
115impl LayoutAssistMetadata {
116    /// Every admitted node, in raw node-id order.
117    pub fn nodes(&self) -> &[LayoutNodeMetadata] {
118        &self.nodes
119    }
120
121    /// Every selected edge, in raw edge-id order.
122    pub fn edges(&self) -> &[LayoutEdgeMetadata] {
123        &self.edges
124    }
125}
126
127/// Describe every admitted node and selected edge, once the bounded pass admits
128/// the work.
129pub(crate) fn layout_assist(
130    analysis: &AnalysisContext<'_>,
131) -> Result<LayoutAssistMetadata, GraphAnalysisError> {
132    let weights = betweenness::betweenness(analysis)?;
133    let degrees = degree::degree(analysis);
134    Ok(LayoutAssistMetadata {
135        nodes: described(analysis, (&degrees, &weights)),
136        edges: connections(analysis),
137    })
138}
139
140/// Every node's metadata, in raw node-id order.
141fn described(
142    analysis: &AnalysisContext<'_>,
143    measured: (&[DegreeCentrality], &[BetweennessCentrality]),
144) -> Box<[LayoutNodeMetadata]> {
145    let (degrees, weights) = measured;
146    let indexes = analysis.indexes();
147    degrees
148        .iter()
149        .zip(weights)
150        .zip(analysis.declared_partition().owners())
151        .map(|((degree, weight), partition)| node_metadata(indexes, (*degree, *weight), *partition))
152        .collect()
153}
154
155/// One node beside what contains it, what declares it, and what measures it.
156fn node_metadata(
157    indexes: &SelectedIndexes,
158    measured: (DegreeCentrality, BetweennessCentrality),
159    partition: GraphNodeId,
160) -> LayoutNodeMetadata {
161    let (degree, weight) = measured;
162    LayoutNodeMetadata {
163        node: degree.node(),
164        parent: indexes.parent(degree.node()),
165        partition,
166        incoming: degree.incoming(),
167        outgoing: degree.outgoing(),
168        raw: weight.raw(),
169        normalized: weight.normalized(),
170    }
171}
172
173/// Every selected edge's metadata, in raw edge-id order.
174///
175/// The shared adjacency lists hold the selection, so the walk is over what was
176/// indexed; each entry's raw record is read back for the evidence it carries.
177fn connections(analysis: &AnalysisContext<'_>) -> Box<[LayoutEdgeMetadata]> {
178    let indexes = analysis.indexes();
179    let graph = analysis.graph();
180    let mut found: Vec<LayoutEdgeMetadata> = Vec::with_capacity(index_of(indexes.selected_edges()));
181    found.extend(
182        (0..indexes.node_count())
183            .map(|at| GraphNodeId::new(position(at)))
184            .flat_map(|node| leaving(graph, indexes, node)),
185    );
186    found.sort_unstable_by_key(|stated| stated.edge);
187    found.into_boxed_slice()
188}
189
190/// The metadata of every selected edge leaving one node.
191fn leaving<'graph>(
192    graph: &'graph CodeGraph,
193    indexes: &'graph SelectedIndexes,
194    node: GraphNodeId,
195) -> impl Iterator<Item = LayoutEdgeMetadata> + 'graph {
196    indexes
197        .outgoing(node)
198        .iter()
199        .filter_map(move |entry| edge_metadata(graph, node, *entry))
200}
201
202/// One selected edge, restated from the raw record its identity names.
203///
204/// The identity came from the adjacency list the graph's own edges were indexed
205/// into, so the lookup answers for every entry; asking through the graph rather
206/// than storing a second copy of the evidence is what keeps the raw record the
207/// one place it is stated.
208fn edge_metadata(
209    graph: &CodeGraph,
210    source: GraphNodeId,
211    entry: SelectedAdjacency,
212) -> Option<LayoutEdgeMetadata> {
213    let found = graph.edge(entry.edge())?;
214    Some(LayoutEdgeMetadata {
215        edge: entry.edge(),
216        source,
217        target: entry.node(),
218        kind: found.kind(),
219        certainty: found.certainty(),
220    })
221}