Source code for k1lib.cli.kgv

# AUTOGENERATED FILE! PLEASE DON'T EDIT HERE. EDIT THE SOURCE NOTEBOOKS INSTEAD
"""
This includes helper clis that make it quick to graph graphviz plots."""
__all__ = ["sketch", "nodes", "edges", "Graph", "InteractiveGraph"]
import re, k1lib, math, os, numpy as np, io, json, base64, unicodedata, inspect, time, html
from k1lib.cli.init import BaseCli; import k1lib.cli as cli, k1lib.cli.init as init
graphviz = k1lib.dep.graphviz
from collections import deque, defaultdict
settings = k1lib.settings.cli
_svgAutoInc = k1lib.AutoIncrement(prefix="_k1_svg_")
_preAutoInc = k1lib.AutoIncrement(prefix="_k1_svg_pre_")
_clusterAuto = k1lib.AutoIncrement(prefix="cluster_")
_idxAuto = k1lib.AutoIncrement(prefix="_idx_")
[docs]class sketch(BaseCli): # sketch _g = None; _name2Idx = None; ctxIdx = None # sketch
[docs] def __init__(self, **kwargs): # sketch """Similar to :class:`~k1lib.cli.utils.sketch`, which makes it easier to plot multiple graphs quickly, this makes it easier to plot node graphs a lot quicker than I have before. This cli configures the graph in general, but doesn't dive too much into the specifics. Example:: ["ab", "bc", "ca"] | (kgv.sketch(engine="sfdp") | kgv.edges()) [["ab", "bc", "ca"], ["b", "c", "d"]] | (kgv.sketch() | kgv.edges() + kgv.nodes()) Most of the complexities are in :class:`edges`, so check that class out for more comprehensive examples :param kwargs: keyword arguments passed into :class:`graphviz.Digraph`""" # sketch super().__init__(capture=True); self.kwargs = kwargs # sketch
@staticmethod # sketch def _guard(): # sketch if sketch._g is None: raise Exception("Context has not been setup yet, can't proceed to plot the edges/nodes. This could be because you're doing `data | kgv.edges()` directly. Instead, do `data | (kgv.sketch() | kgv.edges())`. The operation `kgv.sketch()` will initialize the graph") # sketch
[docs] def __ror__(self, it): # sketch sketch._g = g = graphviz.Digraph(**self.kwargs); sketch._idx2Popup = idx2Popup = {}; sketch._idx2Onclick = idx2Onclick = {} # sketch sketch._name2Idx = name2Idx = defaultdict(lambda: defaultdict(lambda: _idxAuto())) # sketch sketch._nodes = nodes = []; sketch._edges = edges = []; it | self.capturedSerial | cli.deref() # sketch sketch._g = None; sketch._idx2Popup = None; sketch._idx2Onclick = None; sketch._name2Idx = None; sketch._nodes = None; sketch._edges = None # sketch return Graph(g, name2Idx, idx2Popup, idx2Onclick, nodes, edges, self.kwargs) # sketch
def _jsF(self, meta): # sketch fIdx = init._jsFAuto(); dataIdx = init._jsDAuto(); ctxIdx = init._jsDAuto(); sketch.ctxIdx = ctxIdx # sketch header, _fIdx, _async = k1lib.kast.asyncGuard(self.capturedSerial._jsF(meta)) # sketch res = f"""\ {ctxIdx} = null;\n{header} {fIdx} = async ({dataIdx}) => {{ {ctxIdx} = []; const out = {'await ' if _async else ''}{_fIdx}({dataIdx}); const res = (await (await fetch("https://local.mlexps.com/routeServer/kapi_10-graphviz", {{ method: "POST", body: JSON.stringify({{ "obj": {ctxIdx}, "sketchKw": {json.dumps(self.kwargs)} }}), headers: {{ "Content-Type": "application/json" }} }})).json()); if (!res.success) throw new Error(res.reason); return atob(res.data); }}""", fIdx # sketch sketch.ctxIdx = None; return res # sketch
[docs]class nodes(BaseCli): # nodes
[docs] def __init__(self, postProcessF=None): # nodes """Plots out nodes of the graph. Example:: ["a", "b"] | kgv.nodes() # creates nodes with labels "a" and "b" [["s", "a"]] | kgv.nodes() # creates node with label "a" in subgraph with title "s" [["", "a"], ["", "b"]] | kgv.nodes() # creates nodes with labels "a" and "b", like the first line # displays popup window when user hovers over the node "a" [["", "a", {"popup": "Some <b>html</b> content"}]] | kgv.nodes() # executes function when user clicks on the node "a". "nodeId" is a string that looks like "_idx_23" that uniquely identifies the node. "context" is a dict with various objects from the generated js code. Mess around with it to see what's inside [["", "a", {"onclick": "(nodeId, context) => console.log('clicked')"}]] | kgv.nodes() Each row can be multiple different lengths, but only these configurations are allowed: - [name] - [group, name] - [group, name, kwargs] - [group, name, kwargs, extraData] See also: :class:`edges` :param postProcessF: optional function that will be executed over the whole table (N, 4)""" # nodes self.postProcessF = cli.aS(postProcessF or cli.iden()); self._ogPostProcessF = postProcessF # nodes
[docs] def __ror__(self, _it) -> "``5-column input``": # nodes if sketch._g is None: return _it | (sketch() | self) # nodes sketch._guard(); it = [] # nodes for row in _it: # nodes n = len(row) # nodes if n == 1: it.append(["", row[0], {}, None]) # nodes elif n == 2: it.append([*row, {}, None]) # nodes elif n == 3: it.append([*row, None]) # nodes elif n == 4: it.append(row) # nodes else: raise Exception(f"kgv.nodes() can only accept tables from 1 to 4 columns. Detected {n} columns instead") # nodes sketch._nodes.append((it, self._ogPostProcessF)); it = self.postProcessF(it); g = sketch._g; name2Idx = sketch._name2Idx # nodes for s1,n1,kw,_ in it: # subgraph, node, kwargs # nodes idx = name2Idx[s1][n1] # nodes if "popup" in kw: sketch._idx2Popup[idx] = kw["popup"]; kw = dict(kw); del kw["popup"] # nodes if "onclick" in kw: sketch._idx2Onclick[idx] = kw["onclick"]; kw = dict(kw); del kw["onclick"] # nodes g.node(idx, **{"label": n1, **kw}) # nodes return it # nodes
def _jsF(self, meta): # nodes fIdx = init._jsFAuto(); dataIdx = init._jsDAuto(); ctxIdx = sketch.ctxIdx; # nodes if ctxIdx is None: return (sketch() | self)._jsF(meta) # nodes h1,f1,a1 = k1lib.kast.asyncGuard(self.postProcessF._jsF(meta)) # nodes return f"""{h1} {fIdx} = {'async ' if a1 else ''}({dataIdx}) => {{ let it = []; for (const row of {dataIdx}) {{ const n = row.length; if (n === 1) it.push(["", row[0], {{}}, null]); else if (n === 2) it.push([...row, {{}}, null]); else if (n === 3) it.push([...row, null]); else if (n === 4) it.push(row); else throw new Error(`kgv.nodes() can only accept tables from 1 to 4 columns. Detected ${{n}} columns instead`) }} it = {'await ' if a1 else ''}{f1}(it); {ctxIdx}.push(["nodes", {{it, args: []}}]); return it; }} """, fIdx # nodes
def drawSimple(g, names, data, name2Idx): # data here is List[[name1, name2, kw]], names is List[name] # drawSimple for name in set(names): g.node(name2Idx[name], name); # drawSimple for name1, name2, kw in data: g.edge(name2Idx[name1], name2Idx[name2], **kw) # drawSimple
[docs]class edges(BaseCli): # edges blurb="Plots out edges of the graph" # edges
[docs] def __init__(self, postProcessF=None): # edges """Plots out edges of the graph. Example 1:: ["ab", "bc", "ca"] | kgv.edges() Result: .. image:: ../images/kgv_edges_1.png If you need to customize the graph on initialization, then you can use :class:`sketch` to capture related operations (like :class:`edges`), and inject your params in :class:`sketch`:: ["ab", "bc", "ca"] | (kgv.sketch(engine="sfdp") | kgv.edges()) Example 2:: [["a", "b", {"label": "3%"}], ["b", "c", {"color": "red"}], "ac", "cb"] | kgv.edges() Result: .. image:: ../images/kgv_edges_2.png Example 3:: [["group1", "a", "group1", "b", {"label": "3%"}], "ec", ["group1", "b", "", "c", {"color": "red"}], ["group1", "a", "", "c"], ["", "c", "group1", "b"], ["", "c", "group2", "d", {"color": "green"}] ] | kgv.edges() Result: .. image:: ../images/kgv_edges_3.png So the idea is, each row describes a single edge on the graph. Each row can be multiple different lengths, but only these configurations are allowed: - [name1, name2] - [name1, name2, kwargs] - [group1, name1, group2, name2] - [group1, name1, group2, name2, kwargs] - [group1, name1, group2, name2, kwargs, extraData] So if you don't need the complexity and just want to plot something out, you can just use the one at the top, but if you do want fancy features, then you can add those in the kwargs. The "extraData" is not used to graph at all, but is there so that you can put any random data there, potentially to be used to tweak the edges downstream by the postProcessF function. If there are multiple post process functions defined by multiple calls to :class:`edges`, then they will all be executed one after another, serially. See also: :class:`nodes`, :class:`sketch`. Check out a gallery of more examples at `kapi/10-graphviz <https://mlexps.com/kapi/10-graphviz/>`_. :param postProcessF: optional function that will be executed over the whole table (N, 6)""" # edges self.postProcessF = cli.aS(postProcessF or cli.iden()); self._ogPostProcessF = postProcessF # edges
[docs] def __ror__(self, _it) -> "``5-column input``": # edges if sketch._g is None: return _it | (sketch() | self) # edges sketch._guard(); it = [] # edges for row in _it: # edges n = len(row) # edges if n == 2: it.append(["", row[0], "", row[1], {}, None]) # edges elif n == 3: it.append(["", row[0], "", row[1], row[2], None]) # edges elif n == 4: it.append([*row, {}, None]) # edges elif n == 5: it.append([*row, None]) # edges elif n == 6: it.append(row) # edges else: raise Exception(f"kgv.edges() can only accept tables from 2 to 6 columns. Detected {n} columns instead") # edges sketch._edges.append((it, self._ogPostProcessF)); it = self.postProcessF(it); g = sketch._g; name2Idx = sketch._name2Idx # edges # grouping by segments and drawing their internals first # edges for segN, names in it | (cli.batched(2) | cli.head(2)).all() | cli.joinSt() | cli.groupBy(0, True) | cli.apply(cli.joinSt(), 1) | cli.deref(): # edges if segN: # edges with g.subgraph(name=_clusterAuto()) as subG: # edges subG.attr(label=f"{segN}"); drawSimple(subG, names, it | cli.filt(cli.op() == segN, [0, 2]) | cli.cut(1, 3, 4) | cli.deref(), name2Idx[segN]) # edges else: drawSimple(g, names, it | cli.filt(cli.op() == segN, [0, 2]) | cli.cut(1, 3, 4) | cli.deref(), name2Idx[""]) # edges # then draw external edges # edges for s1, n1, s2, n2, kw, _ in it | cli.filt(lambda x: x[0] != x[2]): g.edge(name2Idx[s1][n1], name2Idx[s2][n2], **kw) # edges return it # edges
def _jsF(self, meta): # edges fIdx = init._jsFAuto(); dataIdx = init._jsDAuto(); ctxIdx = sketch.ctxIdx; # edges if ctxIdx is None: return (sketch() | self)._jsF(meta) # edges h1,f1,a1 = k1lib.kast.asyncGuard(self.postProcessF._jsF(meta)) # edges return f"""{h1} {fIdx} = {'async ' if a1 else ''}({dataIdx}) => {{ // why not just pass dataIdx in directly? Well, in Python, the interface is that __ror__ should return a 6-column input, so here, gotta honor that, in case the user has some operation downstream of this let it = []; for (const row of {dataIdx}) {{ const n = row.length; if (n === 2) it.push(["", row[0], "", row[1], {{}}, null]); else if (n === 3) it.push(["", row[0], "", row[1], row[2], null]); else if (n === 4) it.push([...row, {{}}, null]); else if (n === 5) it.push([...row, null]); else if (n === 6) it.push(row); else throw new Error(`kgv.edges() can only accept tables from 2 to 6 columns. Detected ${{n}} columns instead`) }} it = {'await ' if a1 else ''}{f1}(it); {ctxIdx}.push(["edges", {{it, args: []}}]); return it; }} """, fIdx # edges
def _replaceIds(s) -> "[nodeIds, edgeIds, graphIds]": # _replaceIds s = s.replace("><", ">\n<"); parts = s.split("\n") # _replaceIds # these ids are: List[(auto generated id, unique id we're replacing it with)]: # _replaceIds # - Auto generated id is generated by graphviz, looks like "node1", "node2", "edge1", "edge2" # _replaceIds # - Unique id is generated by _idxAuto() for nodes, or _svgAutoInc() for edges and subgraphs # _replaceIds # replacing them so that we have control over the ids and so that they don't collide with other "node1" from other graphs # _replaceIds nodeIds = parts | cli.grep('class="node"', after=1) | cli.batched(2) | cli.apply(cli.op().split("title>")[1].strip("</"), 1) | cli.grep('<g id="(?P<g>node[0-9]+)', col=0, extract="g") | cli.deref() # _replaceIds edgeIds = parts | cli.grep('<g id="(?P<g>edge[0-9]+)"', extract="g") | cli.apply(lambda x: [x, _svgAutoInc()]) | cli.deref() # _replaceIds graphIds = parts | cli.grep('<g id="(?P<g>graph[0-9]+)"', extract="g") | cli.apply(lambda x: [x, _svgAutoInc()]) | cli.deref() # _replaceIds for x, y in [nodeIds, edgeIds, graphIds] | cli.joinSt(): s = s.replace(f'id="{x}"', f'id="{y}"') # _replaceIds # removes all <titles>, cause we've already extracted the modified ids, and titles show up as a tooltip, which is annoying # _replaceIds s = re.sub(r'<title>.*?</title>', '', s, flags=re.DOTALL) # _replaceIds return s, nodeIds, edgeIds, graphIds # _replaceIds
[docs]class Graph: # Graph
[docs] def __init__(self, g, name2Idx, idx2Popup, idx2Onclick, nodes, edges, sketchKw): # Graph """Wrapper around a :class:`graphviz.Graph` or :class:`graphviz.Digraph`. Internal graph object is available at ``self.g``. Not instantiated by end user, instead, this is returned by :class:`sketch`. This class's whole purpose is to implement a popup window for the nodes that require it. It analyzes the svg output of graphviz and compiles it to a form that is slightly interactive. This is the feedstock for more complex Graphs Also, "nodes" and "edges" contain all necessary information to reconstruct everything else like this:: g = [...] | kgv.edges() [g.edges, g.nodes] | (kgv.sketch() | kgv.edges() + kgv.nodes()) # regenerated graph You can do some extra filtering by adding a post process function to :class:`edges` and :class:`nodes`. :param name2Idx: looks like {'': {'c': '_idx_233', 'e': '_idx_234'}} :param idx2Popup: looks like {'_idx_113': 'some content'}. The content can be any complex html :param idx2Onclick: looks like {'_idx_113': '(nodeId) => console.log("something")'}. The string will be eval-ed when the node is clicked :param nodes: List[nodes, postProcessF], (cut(0) | joinSt()) is a bunch of edges, can be piped into nodes() again :param edges: List[edges, postProcessF], (cut(0) | joinSt()) is a bunch of edges, can be piped into edges() again""" # Graph self.g = g; self.name2Idx = name2Idx; self.idx2Popup = idx2Popup; self.idx2Onclick = idx2Onclick # Graph self.nodes = list([x[0] for x in nodes] | cli.joinSt()); self.nodesPostProcessF = cli.serial(*[cli.aS(x[1]) for x in nodes if x[1]]) # Graph self.edges = list([x[0] for x in edges] | cli.joinSt()); self.edgesPostProcessF = cli.serial(*[cli.aS(x[1]) for x in edges if x[1]]) # Graph self.startTime = round(time.time()*1000); self.sketchKw = sketchKw # Graph
[docs] def focus(self, name, depth=1) -> "InteractiveGraph": # Graph """Creates a complex interactive graph that users can click on a node to zoom in to its surroundings. Example:: g = ["ab", "bc", "ca", "cd"] | kgv.edges() g.focus("a") :param name: name of the node, like "a" or "subgraph1\\ue002b". The second example is just the subgraph name and node name joined together by "\\ue002" :param depth: all nodes <= depth away from the selected node will be displayed :param nodeF: function to run on all nodes after filtering out-of-focus nodes :param edgeF: function to run on all edges after filtering out-of-focus nodes""" # Graph return InteractiveGraph(self.nodes, self.edges, self.sketchKw, name, depth, self.nodesPostProcessF, self.edgesPostProcessF) # Graph
def _repr_mimebundle_(self, *args, **kwargs): return self.g._repr_mimebundle_(*args, **kwargs) # Graph def _repr_html_(self): return self._toHtml() # Graph def _toImg(self, **kw): return self.g | cli.toImg() # Graph def _toHtml(self): # Graph idx2Popup = self.idx2Popup; s, nodeIds, edgeIds, graphIds = _replaceIds(self.g | cli.toHtml()) # Graph a = [[x,list(y.items())] for x,y in self.name2Idx.items()] | cli.ungroup(False) | ~cli.apply(lambda x,y,z: [z, f"{x}\ue002{y}"]) | cli.aS(list) # Graph idx2Name = a | cli.toDict(); name2Idx = a | cli.cut(1, 0) | cli.toDict() # this name2Idx maps from "a\ue002b" to "_idx_233", different from self.name2Idx. I kinda just want a flat structure! # Graph a = nodeIds | cli.cut(1) | cli.apply(lambda idx: f"[{json.dumps(idx)}, {html.b64escape(idx2Popup.get(idx, None)) if idx2Popup.get(idx, None) else 'null'}]") | cli.join(", "); pre = f"{_preAutoInc()}_{self.startTime}" # Graph b = ", ".join([f"['{x}', {y}]" for x,y in self.idx2Onclick.items()]) # Graph inside = f"rect.x <= {pre}_mouseX && {pre}_mouseX < rect.x+rect.width && rect.y <= {pre}_mouseY && {pre}_mouseY < rect.y+rect.height" # Graph return f""" <div id="{pre}_wrapper" style="position: relative">{s}<div id="{pre}_popup" style="position: absolute; display: none; background: white; padding: 8px 12px; border-radius: 6px; box-shadow: 0 3px 5px rgb(0 0 0 / 0.3); z-index: 1000000"></div></div> <script> {pre}_idx2Name = {json.dumps(idx2Name)}; {pre}_name2Idx = {json.dumps(name2Idx)}; {pre}_nodeId_node_popup = ([{a}]).map(([x,y]) => [x, document.querySelector(`#${{x}}`), y]); {pre}_nodeId_node_onclick = ([{b}]).map(([x,y]) => [x, document.querySelector(`#${{x}}`), y]); {pre}_nodes = {pre}_nodeId_node_popup.map(([x,n,y]) => n); {pre}_activeNode = null; {pre}_popup = document.querySelector("#{pre}_popup"); {pre}_wrapper = document.querySelector("#{pre}_wrapper"); {pre}_nodeId2Popup = {{}}; for (const [x,n,y] of {pre}_nodeId_node_popup) {{ {pre}_nodeId2Popup[x] = y; }}; {pre}_context = {{nodeId_node_popup: {pre}_nodeId_node_popup, nodeId_node_onclick: {pre}_nodeId_node_onclick, nodes: {pre}_nodes, idx2Name: {pre}_idx2Name, name2Idx: {pre}_name2Idx}}; {pre}_mouseX = 0; {pre}_mouseY = 0; {pre}_wrapper.onmousemove = (e) => {{ {pre}_mouseX = e.clientX; {pre}_mouseY = e.clientY; }}; {pre}_adjustInterval = null; {pre}_wrapper.onclick = (e) => {{ const wRect = {pre}_wrapper.getBoundingClientRect(); for (const [nodeId, node, f] of {pre}_nodeId_node_onclick) {{ const rect = node.getBoundingClientRect(); if ({inside}) {{ f({pre}_idx2Name[nodeId], {pre}_context); break; }} }} }}; setInterval(() => {{ if ({pre}_activeNode) {{ const rect = {pre}_activeNode.getBoundingClientRect(); if (!({inside})) {{ clearInterval({pre}_adjustInterval); {pre}_adjustInterval = null; {pre}_activeNode = null; {pre}_popup.innerHTML = ""; {pre}_popup.style.display = "none"; }} }} if (!{pre}_activeNode) {{ // can't just do `if (activeNode) ... else ...` btw. Separated out for a reason const wRect = {pre}_wrapper.getBoundingClientRect(); for (const node of {pre}_nodes) {{ const rect = node.getBoundingClientRect(); if ({inside}) {{ const popup = {pre}_nodeId2Popup[node.id]; {pre}_activeNode = node; if (popup) {{ {pre}_popup.style.left = rect.x + rect.width/2 + 10 - wRect.x + "px"; {pre}_popup.style.top = 0; {pre}_popup.innerHTML = popup; {pre}_popup.style.display = "block"; (async () => {{ // popup might have <script> tags, so let's execute all of them await (new Promise(r => setTimeout(r, 30))); try {{ for (const script of {pre}_popup.getElementsByTagName("script")) eval(script.innerHTML); }} catch (e) {{ {pre}_popup.innerHTML = `<pre style="color: red">Error encountered:\n${{e}}</pre>`; }} }})(); if ({pre}_adjustInterval) clearInterval({pre}_adjustInterval); const adjustF = () => {{ if ({pre}_activeNode) {{ const pRect = {pre}_popup.getBoundingClientRect(); const t1 = rect.y + rect.height/2 + 10 - wRect.y; // "t" for "top" const t2 = wRect.height - pRect.height; {pre}_popup.style.top = ((t2 < 0) ? 0 : Math.min(t1, t2)) + "px"; }} }}; adjustF(); {pre}_adjustInterval = setInterval(adjustF, 100); }} break; }} }} }} }}, 30); console.log("k1.Graph '{pre}' loaded"); </script>""" # Graph return s # Graph
def _nodeJsonDump2(nodes): # overrides json.dumps(), just because I want to expose the function directly, instead of eval-ing it # _nodeJsonDump2 ans = [] # _nodeJsonDump2 for name, kw in nodes: # _nodeJsonDump2 skw = [f"{json.dumps(k)}: {json.dumps(v)}" for k,v in kw.items() if k != "onclick"] # _nodeJsonDump2 if "onclick" in kw: skw.append(f"'onclick': {kw['onclick']}") # _nodeJsonDump2 skw = "{" + ", ".join(skw) + "}"; ans.append(f"[{json.dumps(name)}, {skw}]") # _nodeJsonDump2 return "[" + ", ".join(ans) + "]" # _nodeJsonDump2
[docs]class InteractiveGraph: # InteractiveGraph def __init__(self, nodes, edges, sketchKw, focus:str, depth:int=1, nodeF=None, edgeF=None): # same signature as Graph # InteractiveGraph self.nodes = nodes; self.edges = edges; self.sketchKw = sketchKw; self.focus = focus if "\ue002" in focus else f"\ue002{focus}"; self.depth = depth # InteractiveGraph # refining the nodes and edges (turn [s1, n1] pairs into "s1\ue002n1") # InteractiveGraph self.rEdges = [[f"{s1}\ue002{n1}", f"{s2}\ue002{n2}", kw, extras] for s1,n1,s2,n2,kw,extras in edges] # InteractiveGraph self.rNodes = [[f"{s}\ue002{n}", kw, extras] for s,n,kw,extras in nodes]; self.startTime = round(time.time()*1000) # InteractiveGraph self.nodeF = cli.aS(nodeF or cli.iden()); self.edgeF = cli.aS(edgeF or cli.iden()); allNodeNames = self.rEdges | cli.cut(0, 1) | cli.joinSt() | cli.aS(set) # InteractiveGraph for name in allNodeNames - set(self.rNodes | cli.cut(0)): self.rNodes.append([name, {}, None]) # add node entries so that I can override the onclick field, to inject in the InteractiveGraph's callback # InteractiveGraph def _repr_html_(self): return self._toHtml() # InteractiveGraph def _toHtml(self): # InteractiveGraph pre = f"{_preAutoInc()}_{self.startTime}"; nodes = [[name, {**kw, "onclick": f"{pre}_ocb"}, extras] for name, kw, extras in self.rNodes] # InteractiveGraph name2Idx = self.rNodes | cli.cut(0) | cli.insId(begin=False) | cli.toDict() # InteractiveGraph inside = f"rect.x <= {pre}_mouseX && {pre}_mouseX < rect.x+rect.width && rect.y <= {pre}_mouseY && {pre}_mouseY < rect.y+rect.height" # InteractiveGraph h1,f1,a1 = k1lib.kast.asyncGuard(self.nodeF._jsF({})); h2,f2,a2 = k1lib.kast.asyncGuard(self.edgeF._jsF({})) # InteractiveGraph return f""" <button id="{pre}_homeBtn" style="padding: 4px 6px">Home</button><span style="margin-left: 8px">Focused on: </span><span id="{pre}_focusedOn">nothing</span>. <span style="margin-left: 6px">Click on any node to focus on it. Click home to view all nodes (might be big!)</span> <div id="{pre}_loading"></div> <div id="{pre}_wrapper" style="margin-top: 8px">(Rendering...)</div> <script> {h1}\n{h2} {pre}_nodes = {json.dumps(nodes)}; {pre}_edges = {json.dumps(self.rEdges)}; {pre}_sNodes = null; {pre}_sEdges = null; // selected nodes and edges to be rendered {pre}_name2Idx = {json.dumps(name2Idx)}; {pre}_wrapper = document.querySelector("#{pre}_wrapper"); {pre}_loading = document.querySelector("#{pre}_loading"); {pre}_focusedOn = document.querySelector("#{pre}_focusedOn"); async function {pre}_render() {{ // renders sNodes and sEdges using mlexps's demo, then inject into wrapper const nodes = {'await ' if a1 else ''}{f1}({pre}_nodes.map(([name, kw,extras], i) => [...name.split("\ue002"), kw, extras]).filter((e, i) => {pre}_sNodes[i])); const edges = {'await ' if a2 else ''}{f2}({pre}_edges.map(([n1,n2,kw,extras], i) => [...n1.split("\ue002"), ...n2.split("\ue002"), kw, extras]).filter((e, i) => {pre}_sEdges[i])); {pre}_loading.innerHTML = "Loading..."; const res = (await (await fetch("https://local.mlexps.com/routeServer/kapi_10-graphviz", {{ method: "POST", body: JSON.stringify({{ 'obj': [["edges", {{"it": edges}}], ["nodes", {{"it": nodes}}]], 'sketchKw': {json.dumps(self.sketchKw)} }}), headers: {{ "Content-Type": "application/json" }} }})).json()).data; {pre}_wrapper.innerHTML = atob(res); (async () => {{ await (new Promise(r => setTimeout(r, 100))); try {{ for (const script of {pre}_wrapper.getElementsByTagName("script")) eval(script.innerHTML); {pre}_loading.innerHTML = ""; }} catch (e) {{ {pre}_loading.innerHTML = `<pre style="color: red">Error encountered:\n${{e}}</pre>`; }} }})(); }} function {pre}_reset() {{ {pre}_sNodes = [...new Array({len(nodes)} ).keys()].map((x) => 1); {pre}_sEdges = [...new Array({len(self.rEdges)}).keys()].map((x) => 1); }}; {pre}_reset(); function {pre}_home() {{ {pre}_focusedOn.innerHTML = "nothing"; {pre}_sNodes = [...new Array({len(nodes)} ).keys()].map((x) => 1); {pre}_sEdges = [...new Array({len(self.rEdges)}).keys()].map((x) => 1); {pre}_render(); }}; setTimeout(() => {pre}_ocb({json.dumps(self.focus)}), 300); document.querySelector("#{pre}_homeBtn").onclick = {pre}_home; function {pre}_ocb(name, ctx) {{ // callback to inject into all. Forms sNodes and sEdges from nodes and edges {pre}_focusedOn.innerHTML = name.replace("\ue002", ">"); const depth = {self.depth}; let sNodes = new Set([name]); // set of node names that will be selected to display out for (let i = 0; i < depth; i++) {{ let newOnes = []; for (const [n1,n2,kw] of {pre}_edges) {{ if (sNodes.includes(n1)) newOnes.push(n2); if (sNodes.includes(n2)) newOnes.push(n1); }} for (const e of newOnes) sNodes.add(e); }} {pre}_sEdges = {pre}_edges.map(([n1,n2,kw]) => sNodes.includes(n1) && sNodes.includes(n2)); {pre}_sNodes = {pre}_nodes.map(([n,kw]) => sNodes.includes(n)); {pre}_render(); }} </script>""" # InteractiveGraph