Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- graph op `visualizer`: documentation for specifying a document as root was
wrong
- graph op `visualizer`: speed up execution by skipping already processed tokens
and ignoring pointing relations to find reachable nodes

## [0.60.0] - 2026-10-06

Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ git2 = { version = "0.20.2", default-features = false }
glob = "0.3"
graphannis = "4.2.0"
graphannis-core = "4.2.0"
graphviz-rust = "0.9.7"
graphviz-rust = "0.9.9"
indicatif = "0.17"
itertools = "0.12"
lazy_static = "1.4.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ expression: result_dot
digraph G {
subgraph token {
rank=same
6[shape=box label="single_sentence/zossen#t1\n \nannis:layer=default_layer\nannis:node_type=node\ndefault_ns:pos=ART\nannis:tok=Die"]
7[shape=box label="single_sentence/zossen#t2\n \nannis:layer=default_layer\nannis:node_type=node\ndefault_ns:pos=NN\nannis:tok=Jugendlichen\nannis:tok-whitespace-before= "]
9[shape=box label="single_sentence/zossen#t3\n \nannis:layer=default_layer\nannis:node_type=node\ndefault_ns:pos=APPR\nannis:tok=in\nannis:tok-whitespace-before= "]
10[shape=box label="single_sentence/zossen#t4\n \nannis:layer=default_layer\nannis:node_type=node\ndefault_ns:pos=NE\nannis:tok=Zossen\nannis:tok-whitespace-before= "]
11[shape=box label="single_sentence/zossen#t5\n \nannis:layer=default_layer\nannis:node_type=node\ndefault_ns:pos=VMFIN\nannis:tok=wollen\nannis:tok-whitespace-before= "]
13[shape=box label="single_sentence/zossen#t6\n \nannis:layer=default_layer\nannis:node_type=node\ndefault_ns:pos=ART\nannis:tok=ein\nannis:tok-whitespace-before= "]
14[shape=box label="single_sentence/zossen#t7\n \nannis:layer=default_layer\nannis:node_type=node\ndefault_ns:pos=NN\nannis:tok=Musikcafé\nannis:tok-whitespace-before= "]
15[shape=box label="single_sentence/zossen#t8\n \nannis:layer=default_layer\nannis:node_type=node\ndefault_ns:pos=$.\nannis:tok=.\nannis:tok-whitespace-before= "]
6[shape=box label="single_sentence/zossen#t1\n \nannis:tok=Die\nannis:layer=default_layer\nannis:node_type=node\ndefault_ns:pos=ART"]
7[shape=box label="single_sentence/zossen#t2\n \nannis:tok=Jugendlichen\nannis:layer=default_layer\nannis:node_type=node\ndefault_ns:pos=NN\nannis:tok-whitespace-before= "]
9[shape=box label="single_sentence/zossen#t3\n \nannis:tok=in\nannis:layer=default_layer\nannis:node_type=node\ndefault_ns:pos=APPR\nannis:tok-whitespace-before= "]
10[shape=box label="single_sentence/zossen#t4\n \nannis:tok=Zossen\nannis:layer=default_layer\nannis:node_type=node\ndefault_ns:pos=NE\nannis:tok-whitespace-before= "]
11[shape=box label="single_sentence/zossen#t5\n \nannis:tok=wollen\nannis:layer=default_layer\nannis:node_type=node\ndefault_ns:pos=VMFIN\nannis:tok-whitespace-before= "]
13[shape=box label="single_sentence/zossen#t6\n \nannis:tok=ein\nannis:layer=default_layer\nannis:node_type=node\ndefault_ns:pos=ART\nannis:tok-whitespace-before= "]
14[shape=box label="single_sentence/zossen#t7\n \nannis:tok=Musikcafé\nannis:layer=default_layer\nannis:node_type=node\ndefault_ns:pos=NN\nannis:tok-whitespace-before= "]
15[shape=box label="single_sentence/zossen#t8\n \nannis:tok=.\nannis:layer=default_layer\nannis:node_type=node\ndefault_ns:pos=$.\nannis:tok-whitespace-before= "]
}
2[shape=box label="single_sentence/zossen#text\n \nannis:node_type=datasource"]
1[shape=box label="single_sentence/zossen\n \nannis:doc=zossen\nannis:node_type=corpus"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ expression: result_dot
digraph G {
subgraph token {
rank=same
6[shape=box label="single_sentence/zossen#t1\n \nannis:layer=default_layer\nannis:node_type=node\ndefault_ns:pos=ART\nannis:tok=Die"]
7[shape=box label="single_sentence/zossen#t2\n \nannis:layer=default_layer\nannis:node_type=node\ndefault_ns:pos=NN\nannis:tok=Jugendlichen\nannis:tok-whitespace-before= "]
9[shape=box label="single_sentence/zossen#t3\n \nannis:layer=default_layer\nannis:node_type=node\ndefault_ns:pos=APPR\nannis:tok=in\nannis:tok-whitespace-before= "]
10[shape=box label="single_sentence/zossen#t4\n \nannis:layer=default_layer\nannis:node_type=node\ndefault_ns:pos=NE\nannis:tok=Zossen\nannis:tok-whitespace-before= "]
6[shape=box label="single_sentence/zossen#t1\n \nannis:tok=Die\nannis:layer=default_layer\nannis:node_type=node\ndefault_ns:pos=ART"]
7[shape=box label="single_sentence/zossen#t2\n \nannis:tok=Jugendlichen\nannis:layer=default_layer\nannis:node_type=node\ndefault_ns:pos=NN\nannis:tok-whitespace-before= "]
9[shape=box label="single_sentence/zossen#t3\n \nannis:tok=in\nannis:layer=default_layer\nannis:node_type=node\ndefault_ns:pos=APPR\nannis:tok-whitespace-before= "]
10[shape=box label="single_sentence/zossen#t4\n \nannis:tok=Zossen\nannis:layer=default_layer\nannis:node_type=node\ndefault_ns:pos=NE\nannis:tok-whitespace-before= "]
}
2[shape=box label="single_sentence/zossen#text\n \nannis:node_type=datasource"]
1[shape=box label="single_sentence/zossen\n \nannis:doc=zossen\nannis:node_type=corpus"]
Expand Down
109 changes: 81 additions & 28 deletions src/manipulator/visualize.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
use super::Manipulator;
use crate::{
StepID,
util::{CorpusGraphHelper, token_helper::TokenHelper},
progress::ProgressReporter,
util::{
CorpusGraphHelper,
token_helper::{TOKEN_KEY, TokenHelper},
},
};
use anyhow::{Context, Result};
use facet::Facet;
Expand Down Expand Up @@ -139,7 +143,11 @@ impl Default for Visualize {
}

impl Visualize {
fn create_graph(&self, graph: &AnnotationGraph) -> Result<Graph> {
fn create_graph(
&self,
graph: &AnnotationGraph,
mut progress: ProgressReporter,
) -> Result<Graph> {
let mut output = Graph::DiGraph {
id: Id::Plain("G".to_string()),
strict: false,
Expand All @@ -149,12 +157,17 @@ impl Visualize {
let token_helper = TokenHelper::new(graph)?;

let parent_id = self.get_root_node_name(graph)?;

let all_token = token_helper.get_ordered_token(&parent_id, None)?;
let included_token = if self.limit_tokens {
all_token.into_iter().take(self.token_limit).collect_vec()
} else {
all_token
};
progress.info(format!(
"visualizing {} token from {parent_id}",
included_token.len()
))?;

let mut subgraph = subgraph!("token"; attr!("rank", "same"));
for t in included_token.iter() {
Expand All @@ -164,28 +177,43 @@ impl Visualize {

// Add all other nodes that are somehow connected to the included token and the document
let all_components = graph.get_all_components(None, None);
let all_gs = all_components

let all_non_pointing_gs = all_components
.iter()
.filter(|c| c.get_type() != AnnotationComponentType::Pointing)
.filter_map(|c| graph.get_graphstorage(c))
.collect_vec();
let all_edge_container =
UnionEdgeContainer::new(all_gs.iter().map(|gs| gs.as_edgecontainer()).collect_vec());

let mut included_nodes: HashSet<graphannis_core::types::NodeID> =
included_token.iter().copied().collect();
// Iterate over all non-pointing components to find connected nodes
let edge_container = UnionEdgeContainer::new(
all_non_pointing_gs
.iter()
.map(|gs| gs.as_edgecontainer())
.collect_vec(),
);

let mut included_nodes = HashSet::new();

progress = progress.with_total_work(included_token.len())?;
for t in included_token {
for step in dfs::CycleSafeDFS::new(&all_edge_container, t, 1, usize::MAX) {
let n = step?.node;
if !token_helper.is_token(n)? && included_nodes.insert(n) {
output.add_stmt(self.create_node_stmt(n, graph)?);
if included_nodes.insert(t) {
for step in dfs::CycleSafeDFS::new(&edge_container, t, 1, usize::MAX) {
let step = step?;
let n = step.node;

if !token_helper.is_token(n)? && included_nodes.insert(n) {
output.add_stmt(self.create_node_stmt(n, graph)?);
}
}
}
for step in dfs::CycleSafeDFS::new_inverse(&all_edge_container, t, 1, usize::MAX) {
let n = step?.node;
if !token_helper.is_token(n)? && included_nodes.insert(n) {
output.add_stmt(self.create_node_stmt(n, graph)?);
for step in dfs::CycleSafeDFS::new_inverse(&edge_container, t, 1, usize::MAX) {
let n = step?.node;
if !token_helper.is_token(n)? && included_nodes.insert(n) {
output.add_stmt(self.create_node_stmt(n, graph)?);
}
}
}

progress.worked(1)?;
}
// Add all datasource nodes if they are connected to the included documents have not been already added
let part_of_gs = graph
Expand Down Expand Up @@ -268,15 +296,30 @@ impl Visualize {
.unwrap_or_else(|| Cow::Owned(n.to_string()));

let annos = input.get_node_annos().get_annotations_for_item(&n)?;
let annos = annos
.into_iter()
.filter(|a| &a.key != NODE_NAME_KEY.as_ref())
.sorted()
.collect_vec();

let anno_string = annos
let mut displayed_annos = Vec::new();
// if annis::tok is part of the annotations, put it at the beginning of the list
if let Some(tok_anno) = annos.iter().find(|a| &a.key == TOKEN_KEY.as_ref()) {
displayed_annos.push(tok_anno.clone());
}
// Add all remaining annotations
displayed_annos.extend(
annos
.into_iter()
.filter(|a| &a.key != NODE_NAME_KEY.as_ref() && &a.key != TOKEN_KEY.as_ref())
.sorted(),
);

let anno_string = displayed_annos
.into_iter()
.map(|a| format!("{}:{}={}", a.key.ns, a.key.name, a.val))
.map(|a| {
format!(
"{}:{}={}",
a.key.ns,
a.key.name,
a.val.replace("\"", "\\\"")
)
})
.join("\\n");

let label = format!("\"{node_name}\\n \\n{anno_string}\"");
Expand Down Expand Up @@ -358,19 +401,29 @@ impl Manipulator for Visualize {
&self,
graph: &mut graphannis::AnnotationGraph,
workflow_directory: &std::path::Path,
_step_id: StepID,
_tx: Option<crate::workflow::StatusSender>,
step_id: StepID,
tx: Option<crate::workflow::StatusSender>,
) -> Result<(), Box<dyn std::error::Error>> {
// let progress = ProgressReporter::new_unknown_total_work(tx, step_id)?;

let output = self.create_graph(graph)?;
let output = self.create_graph(
graph,
ProgressReporter::new_unknown_total_work(tx.clone(), step_id.clone())?,
)?;
let progress = ProgressReporter::new_unknown_total_work(tx, step_id)?;

if let Some(file_path) = &self.output_dot {
progress.info(format!(
"writing visualizer output DOT file {}",
file_path.to_string_lossy()
))?;
let graph_dot = output.print(&mut PrinterContext::default());
std::fs::write(workflow_directory.join(file_path), graph_dot)?;
}

if let Some(file_path) = &self.output_svg {
progress.info(format!(
"writing visualizer output SVG file {}",
file_path.to_string_lossy()
))?;
let graph_svg = exec(
output,
&mut PrinterContext::default(),
Expand Down
13 changes: 13 additions & 0 deletions src/progress.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,19 @@ impl ProgressReporter {
Ok(reporter)
}

/// Converts an existing `ProgressReporter` to a new one with the given amount of total work.
/// Any progress information is removed and the work starts at 0 again.
pub fn with_total_work(self, total_work: usize) -> Result<ProgressReporter, AnnattoError> {
let reporter = ProgressReporter {
state: self.state,
step_id: self.step_id,
total_work: Some(total_work),
};
// Send a first status report so any listener can get the total number of steps to perform
reporter.worked(0)?;
Ok(reporter)
}

pub fn new_unknown_total_work(
tx: Option<StatusSender>,
step_id: StepID,
Expand Down
Loading