These are some ideas on how this Personal Library's node graph can be rendered.
Notes graph rendering styles
These are some ideas on how this Personal Library's node graph can be rendered.
Simple graph
Like
org-roam-ui
See
POC
It's well-explored, can represent a relationship graph easily and there's lots of tools to build it (like D3js).
The downside is that it's not really inviting. Can be intimidating.
Like a garden
If the graph is planar (can be represented in 2D without crossing relations) this could be a very interesting representation.
Connections between topics can be "paths" between regions of the garden. And the status of the notes (SOMETIMES, DONE) and it's time since edition can be represented on the state of different "tiles" of the garden.
Maybe it can be structured by building a graph, and overlaying a voronoi on top of it 🤔. If the graph can be used as starting point for next iterations it might be even possible to keep it reasonably stable on the parts that don't change.
[ 3/5 ] POCs
POC 1 - Base
Let's take the graph.json file and try to pass it through graphviz to check it it can be made planar easily.
import requests
url = 'http://localhost:8000/notes/graph.json'
g = requests.get(url).json()
f = open('graph.dot', 'wt')
f.write('digraph {\n')
for k, v in g.items():
print("_" + k.replace("-", "_") + "[label=\"" + v["title"].replace("\"", "'") + "\"];", file=f)
for k, v in g.items():
for link in v["links"]:
if link["target"].startswith("id:"):
link["target"] = link["target"][3:]
if '[' in link['target']:
# Special case, to be handled on org_rw
continue
t = "_" + link["target"].replace("-", "_")
print("_" + k.replace("-", "_") + "->" + t, file=f)
f.write('}\n')
# dot graph.dot -Tsvg graph.svg
f.close()
import subprocess
subprocess.call("fdp graph.dot -Tsvg -o graph.svg", shell=True)
return "graph.svg"
POC 2 - Hierarchical grouping
Implemented as part of
Works,but takes 3-4 minutes to render (lower maxiter
for quicker tests), also the result is quite messy.
Let's take the graph.json file and try to pass it through graphviz to check it it can be made planar easily.
from pprint import pprint
import logging
import requests
import copy
import subprocess
url = 'http://localhost:8000/notes/graph.json'
g = requests.get(url).json()
in_emacs_tree = {}
is_subcluster = set()
first_level_on_doc = {}
for k, v in g.items():
if v['depth'] == 1 and v.get("doc_id") and v.get("doc_id") != k:
if v['doc_id'] in first_level_on_doc:
logging.warning('Duplicate top-level with same doc-id: {} & {}'.format(
k, v['doc_id']
))
first_level_on_doc[v['doc_id']] = None
else:
first_level_on_doc[v['doc_id']] = k
for k, v in g.items():
for link in v["links"]:
if link["target"].startswith("id:"):
link["target"] = link["target"][3:]
if link.get('relation') == 'in':
t = link['target']
if t.startswith("id:"):
t = t[3:]
if '[' in t:
# Special case, to be handled on org_rw
continue
if t in first_level_on_doc:
t = first_level_on_doc[t]
if t not in in_emacs_tree:
in_emacs_tree[t] = set()
in_emacs_tree[t].add(k)
is_subcluster.add(k)
font_name = 'monospace'
with open('graph.dot', mode='wt') as f:
f.write('strict digraph {\n')
f.write('maxiter=10000\n')
f.write('splines=curved\n')
# f.write('splines=spline\n') # Not supported with edges to cluster
f.write('node[shape=rect, width=0.5, height=0.5]\n')
f.write('K=0.3\n')
f.write('edge[len = 1]\n')
def _ind(depth):
return " " * depth
def draw_subgraph(node_id, depth):
f.write(_ind(depth - 1) + "subgraph cluster_{} {{\n".format(node_id.replace("-", "_")))
f.write(_ind(depth) + 'URL="./{}.node.html"\n'.format(node_id))
f.write(_ind(depth) + 'class="{}"\n'.format('cluster-depth-' + str(depth - 1)))
f.write(_ind(depth) + "fontname=\"{}\"\n".format(font_name))
f.write(_ind(depth) + "label=\"{}\"\n".format(g[node_id]['title'].replace("\"", "'")))
f.write("\n")
# print("T: {}".format(in_emacs_tree), file=sys.stderr)
for k in in_emacs_tree[node_id]:
v = g[k]
if k in in_emacs_tree:
draw_subgraph(k, depth=depth + 1)
else:
print(_ind(depth) + "_" + k.replace("-", "_")
+ "[label=\"" + v["title"].replace("\"", "'") + "\", "
+ "URL=\"" + k + ".node.html\", "
+ "fontname=\"" + font_name + "\", "
+ "class=\"cluster-depth-" + str(depth) + "\""
+ "];", file=f)
f.write("\n" + _ind(depth - 1) + "}\n")
# draw_subgraph(reference_node, 1)
for node in in_emacs_tree.keys():
if node in first_level_on_doc:
continue
if node not in is_subcluster:
draw_subgraph(node, 1)
for k, v in g.items():
if k in first_level_on_doc or k in in_emacs_tree:
continue
print("_" + k.replace("-", "_")
+ "[label=\"" + v["title"].replace("\"", "'") + "\", "
+ "fontname=\"" + font_name + "\", "
+ "URL=\"" + k + ".node.html\"];", file=f)
for k, v in g.items():
if k in first_level_on_doc:
continue
link_src = '_' + k.replace("-", "_")
if k in in_emacs_tree:
link_src = 'cluster_{}'.format(k.replace("-", "_"))
for link in v["links"]:
if link.get('relation') == 'in':
continue
if link["target"].startswith("id:"):
link["target"] = link["target"][3:]
if '[' in link['target']:
# Special case, to be handled on org_rw
continue
if link['target'] not in g:
# Irrelevant
continue
if link['target'] in first_level_on_doc:
link['target'] = first_level_on_doc[link['target']]
if link['target'] in in_emacs_tree:
t = 'cluster_{}'.format(link['target'].replace("-", "_"))
else:
t = "_" + link["target"].replace("-", "_")
print(link_src + "->" + t, file=f)
f.write('}\n')
f.flush()
# subprocess.call(['fdp', 'graph.dot', '-Tsvg', '-o', 'graph.svg'])
# return 'graph.svg'
: None
POC 3 - Network X planarity checks
It is not a planar graph.
import requests
url = 'http://localhost:8000/notes/graph.json'
g = requests.get(url).json()
import networkx as nx
import matplotlib.pyplot as plt
fig = plt.figure()
fig.show()
graph = nx.DiGraph()
for k, v in g.items():
graph.add_node(k)
for k, v in g.items():
for link in v["links"]:
if link["target"].startswith("id:"):
link["target"] = link["target"][3:]
graph.add_edge(k, link["target"])
print("Planar?", nx.is_planar(graph))
# Cannot draw_planer, just draw_normal
nx.draw(graph, with_labels = False)
fig.canvas.draw()
input('Press enter to finish')
: Press enter to finish
POC 5 - Render as graph around a specific node
FDP rendering would be useful with better edge routing 🤔
Linking to the clusters makes it more aesthetically pleasing 🤔
Old code
import requests
url = 'http://localhost:8000/notes/graph.json'
reference_node = 'aa29be89-70e7-4465-91ed-361cf0ce62f2' # Emacs
# reference_node = '5aafa9e6-6344-49b8-86c2-7387d28a86ec' # Private server config
# reference_node = '6d82b501-ab92-4404-8a71-2b55a90eb814' # K8s
g = requests.get(url).json()
centered_graph = { reference_node: g[reference_node] }
del g[reference_node]
new_nodes = True
in_emacs_tree = {
reference_node: set(),
}
while new_nodes:
new_nodes = False
removed = set()
for k, v in g.items():
for link in v["links"]:
if link["target"].startswith("id:"):
link["target"] = link["target"][3:]
if link['target'] in centered_graph and link.get('relation') == 'in':
centered_graph[k] = v
for l in v["links"]:
if l.get('relation') == 'in':
t = l['target']
if t.startswith("id:"):
t = t[3:]
if '[' in t:
# Special case, to be handled on org_rw
continue
if t not in in_emacs_tree:
in_emacs_tree[t] = set()
in_emacs_tree[t].add(k)
v['links'] = [
l for l in v["links"]
if l.get('relation') != 'in'
]
removed.add(k)
new_nodes = True
break
for k in removed:
del g[k]
in_emacs = set(centered_graph.keys())
# One more round for the rest, not requiring "in"
for k, v in g.items():
for link in v["links"]:
if link["target"].startswith("id:"):
link["target"] = link["target"][3:]
if link['target'] in in_emacs:
centered_graph[k] = v
removed.add(k)
g = centered_graph
f = open('graph.dot', 'wt')
f.write('digraph {\n')
# f.write('bgcolor="#222222"\n')
# f.write('fontcolor="#ffffff"\n')
f.write('maxiter=1000\n')
f.write('splines=curved\n')
# f.write('splines=spline\n') # Not supported with edges to cluster
f.write('node[shape=rect]\n')
# f.write('edge[color="#ffffff"]\n')
def draw_subgraph(node_id):
f.write("subgraph cluster_{} {{\n".format(node_id.replace("-", "_")))
f.write('URL="./{}.node.html"\n'.format(node_id))
# f.write('color="#ffffff"\n')
f.write("label=\"{}\"\n".format(g[node_id]['title'].replace("\"", "'")))
f.write("\n")
# print("T: {}".format(in_emacs_tree), file=sys.stderr)
for k in in_emacs_tree[node_id]:
v = g[k]
print("_" + k.replace("-", "_") + "[label=\"" + v["title"].replace("\"", "'") + "\", URL=\"" + k + ".node.html\"];", file=f)
if k in in_emacs_tree:
draw_subgraph(k)
f.write("\n}")
draw_subgraph(reference_node)
for k, v in g.items():
if k not in in_emacs:
print("_" + k.replace("-", "_") + "[label=\"" + v["title"].replace("\"", "'") + "\", URL=\"" + k + ".node.html\"];", file=f)
for k, v in g.items():
for link in v["links"]:
if link["target"].startswith("id:"):
link["target"] = link["target"][3:]
if '[' in link['target']:
# Special case, to be handled on org_rw
continue
if link['target'] not in g:
# Irrelevant
continue
if link['target'] in in_emacs_tree:
t = 'cluster_{}'.format(link['target'].replace("-", "_"))
else:
t = "_" + link["target"].replace("-", "_")
print("_" + k.replace("-", "_") + "->" + t, file=f)
f.write('}\n')
# dot graph.dot -Tsvg graph.svg
f.close()
import subprocess
subprocess.call("fdp graph.dot -Tsvg -o graph.svg", shell=True)
# return "graph.svg"
Manually drawn
Manually draw a space in a Isometric style.