Skip to content

Commit

Permalink
A very very rudimentary interactive graph visualization app.
Browse files Browse the repository at this point in the history
This loads a graph from a zip file and can produce subgraphs via webform
  • Loading branch information
thvitt committed Mar 25, 2019
1 parent b35b9eb commit f8e2f67
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 14 deletions.
53 changes: 53 additions & 0 deletions graphviewer/graphviewer.py
@@ -0,0 +1,53 @@
import subprocess

import networkx as nx
from flask import Flask, render_template, request, flash
from markupsafe import Markup

from macrogen import MacrogenesisInfo, write_dot
from macrogen.graphutils import remove_edges, simplify_timeline

app = Flask(__name__)
from pathlib import Path

info = MacrogenesisInfo('target/macrogenesis/macrogen-info.zip')

@app.route('/')
def hello_world():
node_str = request.args.get('nodes')
nodes = parse_nodes(node_str)
context = request.args.get('context', False)
abs_dates = request.args.get('abs_dates', False)
extra = parse_nodes(request.args.get('extra', ''))
induced_edges = request.args.get('induced_edges', False)
ignored_edges = request.args.get('ignored_edges', False)
tred = request.args.get('tred', False)
if nodes:
g = info.subgraph(*nodes, context=context, abs_dates=abs_dates, pathes=extra, keep_timeline=True)
if induced_edges:
g = info.base.subgraph(g.nodes).copy()
if not ignored_edges or tred:
g = remove_edges(g, lambda u,v,attr: attr.get('ignore', False))
if tred:
g = remove_edges(g, lambda u, v, attr: attr.get('delete', False))
g = simplify_timeline(g)
agraph = write_dot(g, target=None, highlight=nodes[0])
p = subprocess.run(['dot', '-T', 'svg'], input=agraph.to_string(), capture_output=True, timeout=30, encoding='UTF-8')
svg = Markup(p.stdout)
else:
svg = ':( Keine Knoten'



return render_template('form.html', svg=svg, **request.args)


def parse_nodes(node_str):
nodes = []
if node_str:
for node_spec in node_str.split(','):
try:
nodes.append(info.node(node_spec.strip()))
except KeyError:
... # flash("Knoten »%s« nicht gefunden", node_spec.strip())
return nodes
25 changes: 25 additions & 0 deletions graphviewer/templates/form.html
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Kontextgraph</title>
</head>
<body>

{{ svg }}

<form method="get">
<p><label for="nodes">Zentrale(r) Knoten: </label><input type="text" name="nodes" id="nodes" value="{{ nodes }}"></p>
<h3>Kontextknoten:</h3>
<p><input type="checkbox" name="context" id="context" {% if context %}checked{% endif %}> <label for="context">Nachbarknoten</label></p>
<p><input type="checkbox" name="abs_dates" id="abs_dates" {% if abs_dates %}checked{% endif %}> <label for="abs_dates">absolute Datierungen rechtfertigen</label></p>
<p><label for="extra">Pfade von/zu (falls verfügbar): </label><input type="text" name="extra" id="extra" value="{{ extra }}"></p>
<h3>Kantenauswahl und -Gestaltung:</h3>
<p><input type="checkbox" name="induced_edges" id="induced_edges" {% if induced_edges %}checked{% endif %}> <label for="induced_edges">alle induzierten Kanten</label></p>
<p><input type="checkbox" name="ignored_edges" id="ignored_edges" {% if ignored_edges %}checked{% endif %}> <label for="ignored_edges">ignorierte (graue) Kanten</label> </p>
<p><input type="checkbox" name="tred" id="tred" {% if tred %}checked{% endif %}> <label for="tred">Transitive Hülle</label> </p>
<p><input type="submit"></p>
</form>

</body>
</html>
7 changes: 4 additions & 3 deletions src/macrogen/graph.py
Expand Up @@ -364,14 +364,15 @@ def add_path(self, graph: nx.MultiDiGraph, source: Node, target: Node, weight='i
"""
try:
if edges_from is None:
edges_from = self.base
edges_from = self.dag
path = nx.shortest_path(edges_from, source, target, weight, method)
logger.info('Shortest path from %s to %s: %s', source, target, " → ".join(map(str, path)))
edges = expand_edges(edges_from, nx.utils.pairwise(path))
graph.add_edges_from(edges)
return path
except nx.NetworkXNoPath as e:
if must_exist:
raise e
return path

def subgraph(self, *nodes: Node, context: bool = True, path_to: Iterable[Node] = {}, abs_dates: bool=True,
path_from: Iterable[Node] = {}, pathes: Iterable[Node] = {}, keep_timeline=False) -> nx.MultiDiGraph:
Expand Down Expand Up @@ -416,7 +417,7 @@ def subgraph(self, *nodes: Node, context: bool = True, path_to: Iterable[Node] =
prev = max((d for d in self.closure.pred[node] if isinstance(d, date)), default=None)
if prev is not None and prev not in central_nodes:
self.add_path(subgraph, prev, node, edges_from=self.dag)
next_ = min((d for d in self.closure.succ[node] if isinstance(d, date)))
next_ = min((d for d in self.closure.succ[node] if isinstance(d, date)), default=None)
if next_ is not None and next not in central_nodes:
self.add_path(subgraph, node, next_, edges_from=self.dag)

Expand Down
24 changes: 13 additions & 11 deletions src/macrogen/visualize.py
Expand Up @@ -67,27 +67,25 @@ def _simplify_attrs(attrs):


def write_dot(graph: nx.MultiDiGraph, target='base_graph.dot', style=None,
highlight=None, record='auto', edge_labels=True):
highlight=None, record='auto', edge_labels=True) -> AGraph:
"""
Writes a properly styled graphviz file for the given graph.
Args:
graph: the subgraph to draw
target: dot file that should be written, may be a Path
target: dot file that should be written, may be a Path. If none, nothing is written but the AGraph returns
style (dict): rules for styling the graph
highlight: if a node, highlight that in the graph. If a tuple of nodes, highlight the shortest path(s) from the
first to the second node
record: record in the queue for `render_all`. If ``"auto"`` dependent on graph size
edge_labels (bool): Should we paint edge labels?
Returns:
None.
the AGraph, can be used to write the thing yourself.
"""
if style is None:
style = config.styles
logger.info('Writing %s ...', target)
target_path = Path(target)
target_path.parent.mkdir(exist_ok=True, parents=True)
try:
if record == 'auto' and config.render_node_limit >= 0:
record = graph.number_of_nodes() < config.render_node_limit
Expand Down Expand Up @@ -168,12 +166,16 @@ def write_dot(graph: nx.MultiDiGraph, target='base_graph.dot', style=None,
getattr(timeline, t + '_attr', {}).update(timeline_style[t])
logger.debug('timeline style: %s = %s', t, getattr(timeline, t + '_attr').items()) ## Doesn’t work

dotfilename = str(target)
agraph.write(dotfilename)
if record:
_render_queue.append(dotfilename)
else:
logger.warning('%s has not been queued for rendering', dotfilename)
if target is not None:
target_path = Path(target)
target_path.parent.mkdir(exist_ok=True, parents=True)
dotfilename = str(target)
agraph.write(dotfilename)
if record:
_render_queue.append(dotfilename)
else:
logger.warning('%s has not been queued for rendering', dotfilename)
return agraph


def render_file(filename):
Expand Down

0 comments on commit f8e2f67

Please sign in to comment.