Skip to content

Commit

Permalink
feat(relations): graph visualisation of relations
Browse files Browse the repository at this point in the history
Adds a Graph tab to the tabbed relations template.
Graph tab shows a graphical representation of the all relations that
include the current object.
Nodes represent objects, each entity type deterministically gets a
unique colour.
All edges are directed from the object to the related objects.
  • Loading branch information
gythaogg committed Nov 13, 2024
1 parent 98e2ca3 commit c6b7e48
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 0 deletions.
10 changes: 10 additions & 0 deletions apis_core/relations/templates/relations/graph_relations.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{% load relations %}
{% relations_graph relations target as graph %}
{% if graph.error %}<div class="alert alert-warning" role="alert">{{ graph.error }}</div>{% endif %}
{% if graph.svg %}
<div class="d-flex justify-content-center"
style="overflow: auto;
max-width: 100%">
<div id="svg-container">{{ graph.svg|safe }}</div>
</div>
{% endif %}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
href="#reltab_{{ object.id }}_{{ target.name }}">{{ target.name | title }}</a>
</li>
{% endfor %}
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#reltab_graph">Graph</a>
</li>
</ul>
</div>
<div class="card-body">
Expand All @@ -25,6 +28,7 @@
{% for target in possible_targets %}
<div class="tab-pane" id="reltab_{{ object.id }}_{{ target.name }}">{% include "relations/list_relations.html" %}</div>
{% endfor %}
<div class="tab-pane" id="reltab_graph">{% include "relations/graph_relations.html" %}</div>
</div>
</div>
</div>
70 changes: 70 additions & 0 deletions apis_core/relations/templatetags/relations.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,73 @@ def relations_verbose_name_listview_url():
for relation in relation_classes
}
return sorted(ret.items())


@register.simple_tag(takes_context=True)
def relations_graph(context, relations, target=None):
from django.conf import settings

if "apis_core.relations" not in settings.INSTALLED_APPS:
return {
"error": "This has currently been configured only for apis_core.relations."
}

import hashlib

from pydot import Dot, Edge, Node

def get_colour(entity_type):
# TODO: Could this be better, by choosing from a proper matplotlib palette?
hash_value = int(hashlib.md5(entity_type.encode("utf-8")).hexdigest(), 16)
r = (hash_value >> 16) & 0xFF
g = (hash_value >> 8) & 0xFF
b = hash_value & 0xFF

# Format it into a hex color string
return f"#{r:02x}{g:02x}{b:02x}"

def get_node(obj):
node = Node(
obj.pk,
label="", # use tooltip instead
shape="circle",
style="filled",
splines=False,
fillcolor=get_colour(obj.__class__.__name__),
tooltip=str(rel.obj),
)
node.set_tooltip(str(obj))
node.set_URL(obj.get_absolute_url())
return node

graph = Dot(
graph_type="digraph",
layout="sfdp",
splines=True,
center=True,
# repulsiveforce=2.0, #
# K=1,
)

for rel in relations:
graph.add_node(get_node(rel.subj))
graph.add_node(get_node(rel.obj))

src = rel.subj.pk if rel.forward else rel.obj.pk
dest = rel.obj.pk if rel.forward else rel.subj.pk
edge_label = rel.name() if rel.forward else rel.reverse_name()

e = Edge(
src,
dest,
label=edge_label,
)
graph.add_edge(e)

graph_data = {}
try:
graph_data["dot"] = graph.to_string()
graph_data["svg"] = graph.create_svg().decode()
except Exception as e:
graph_data["error"] = f"{str(e)} occured while creating graph."
return graph_data

0 comments on commit c6b7e48

Please sign in to comment.