Table of contents

Notes graph rendering styles

%3 cluster_85415cfe_da14_461d_bf1c_af66b4826e03 Notes graph rendering styles cluster_383ba669_501a_4586_b39a_a604f8e15fa4 Like a garden cluster_edf31e68_937c_4bee_a3c7_56c9adf6c4da [3/5] POCs cluster_ba8caab6_c3d7_4aa1_80f6_7ad7245aa23e POC 5 - Render as graph around a specific node _958dadad_be6c_41f0_a6af_4e0bbc410729 Manually drawn _0967e4bb_62f1_4e12_b106_205e5d8e483e Old code _b7149b7c_5fe4_4e84_831e_58cd14714300 POC 4 - Find planar net _f971bf69_eab3_43e5_b84b_a495e389ab64 POC 1 - Base _65601528_5704_4a7c_a7d7_7dc1daa3a667 POC 3 - Network X planarity checks _8835a03c_2a36_4495_8cc8_cd2816a6a5ab POC 2 - Hierarchical grouping _8835a03c_2a36_4495_8cc8_cd2816a6a5ab->__0:cluster_ba8caab6_c3d7_4aa1_80f6_7ad7245aa23e _128526aa_83d0_413b_b43e_84ee40861100 Simple graph _0873a209_b9df_4bf3_b65e_2f425c5a2adc Librarian / Personal Library _23a793f3_a58e_4de4_9728_3770551906a0 Custom Graphviz render plugin _23a793f3_a58e_4de4_9728_3770551906a0->__1:cluster_ba8caab6_c3d7_4aa1_80f6_7ad7245aa23e _c0b2e5ee_dae4_4e8a_a0e8_f9c10a7114f8 ZeldaLikeInformationSystem _e9f4ec85_2527_4541_8fae_4d88791f3c32 MiniCubes __2:cluster_85415cfe_da14_461d_bf1c_af66b4826e03->_0873a209_b9df_4bf3_b65e_2f425c5a2adc __3:cluster_383ba669_501a_4586_b39a_a604f8e15fa4->_c0b2e5ee_dae4_4e8a_a0e8_f9c10a7114f8 __4:cluster_383ba669_501a_4586_b39a_a604f8e15fa4->_e9f4ec85_2527_4541_8fae_4d88791f3c32

These are some ideas on how this Personal Library's node graph can be rendered.

Simple graph

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.

SOMETIME

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

DONE

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"

TODO

POC 2 - Hierarchical grouping

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

DONE

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

DONE

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"

SOMETIME

Manually drawn

Manually draw a space in a Isometric style.