refactor: update graph visualization and add ASCII representation (#3219)

* chore: Update dependencies in poetry files, adding grandalf 0.8.0 and upgrading several other packages to latest versions

* refactor: Add ASCII graph drawing functionality using grandalf library

* refactor: Improve Graph representation format and add ASCII visualization method to enhance readability and usability
This commit is contained in:
Gabriel Luiz Freitas Almeida 2024-08-07 16:45:10 -03:00 committed by GitHub
commit 72463d9b5d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 851 additions and 545 deletions

18
poetry.lock generated
View file

@ -3404,6 +3404,23 @@ test = ["aiofiles", "aiohttp (>=3.8.0,<4)", "aiohttp (>=3.9.0b0,<4)", "botocore
test-no-transport = ["aiofiles", "mock (==4.0.2)", "parse (==1.15.0)", "pytest (==7.4.2)", "pytest-asyncio (==0.21.1)", "pytest-console-scripts (==1.3.1)", "pytest-cov (==3.0.0)", "vcrpy (==4.4.0)"]
websockets = ["websockets (>=10,<12)"]
[[package]]
name = "grandalf"
version = "0.8"
description = "Graph and drawing algorithms framework"
optional = false
python-versions = "*"
files = [
{file = "grandalf-0.8-py3-none-any.whl", hash = "sha256:793ca254442f4a79252ea9ff1ab998e852c1e071b863593e5383afee906b4185"},
{file = "grandalf-0.8.tar.gz", hash = "sha256:2813f7aab87f0d20f334a3162ccfbcbf085977134a17a5b516940a93a77ea974"},
]
[package.dependencies]
pyparsing = "*"
[package.extras]
full = ["numpy", "ply"]
[[package]]
name = "graphql-core"
version = "3.2.3"
@ -5171,6 +5188,7 @@ emoji = "^2.12.0"
fastapi = "^0.111.0"
filelock = "^3.15.4"
firecrawl-py = "^0.0.16"
grandalf = "^0.8.0"
gunicorn = "^22.0.0"
httpx = "*"
jq = {version = "^1.7.0", markers = "sys_platform != \"win32\""}

View file

@ -0,0 +1,185 @@
"""
This code is adapted from the DVC project.
Original source:
https://github.com/iterative/dvc/blob/c5bac1c8cfdb2c0f54d52ac61ff754e6f583822a/dvc/dagascii.py
The original code has been modified to focus on drawing a Directed Acyclic Graph (DAG) in ASCII
using the `grandalf` library. Non-essential parts have been removed, and the code has been
refactored to suit this specific use case.
"""
import math
from grandalf.graphs import Edge as GrandalfEdge
from grandalf.graphs import Graph as GrandalfGraph
from grandalf.graphs import Vertex as GrandalfVertex
from grandalf.layouts import SugiyamaLayout
from grandalf.routing import EdgeViewer, route_with_lines
class VertexViewer:
"""Class to define vertex box boundaries that will be accounted for during graph building by grandalf."""
HEIGHT = 3 # top and bottom box edges + text
def __init__(self, name):
self._h = self.HEIGHT # top and bottom box edges + text
self._w = len(name) + 2 # right and left bottom edges + text
@property
def h(self):
return self._h
@property
def w(self):
return self._w
class AsciiCanvas:
"""Class for drawing in ASCII."""
def __init__(self, cols, lines):
assert cols > 1
assert lines > 1
self.cols = cols
self.lines = lines
self.canvas = [[" "] * cols for _ in range(lines)]
def get_lines(self):
return map("".join, self.canvas)
def draws(self):
return "\n".join(self.get_lines())
def draw(self):
"""Draws ASCII canvas on the screen."""
lines = self.get_lines()
print("\n".join(lines))
def point(self, x, y, char):
"""Create a point on ASCII canvas."""
assert len(char) == 1
assert 0 <= x < self.cols
assert 0 <= y < self.lines
self.canvas[y][x] = char
def line(self, x0, y0, x1, y1, char):
"""Create a line on ASCII canvas."""
if x0 > x1:
x1, x0 = x0, x1
y1, y0 = y0, y1
dx = x1 - x0
dy = y1 - y0
if dx == 0 and dy == 0:
self.point(x0, y0, char)
elif abs(dx) >= abs(dy):
for x in range(x0, x1 + 1):
y = y0 + int(round((x - x0) * dy / float(dx))) if dx else y0
self.point(x, y, char)
else:
for y in range(min(y0, y1), max(y0, y1) + 1):
x = x0 + int(round((y - y0) * dx / float(dy))) if dy else x0
self.point(x, y, char)
def text(self, x, y, text):
"""Print a text on ASCII canvas."""
for i, char in enumerate(text):
self.point(x + i, y, char)
def box(self, x0, y0, width, height):
"""Create a box on ASCII canvas."""
assert width > 1
assert height > 1
width -= 1
height -= 1
for x in range(x0, x0 + width):
self.point(x, y0, "-")
self.point(x, y0 + height, "-")
for y in range(y0, y0 + height):
self.point(x0, y, "|")
self.point(x0 + width, y, "|")
self.point(x0, y0, "+")
self.point(x0 + width, y0, "+")
self.point(x0, y0 + height, "+")
self.point(x0 + width, y0 + height, "+")
def build_sugiyama_layout(vertexes, edges):
vertexes = {v: GrandalfVertex(v) for v in vertexes}
edges = [GrandalfEdge(vertexes[s], vertexes[e]) for s, e in edges]
graph = GrandalfGraph(vertexes.values(), edges)
for vertex in vertexes.values():
vertex.view = VertexViewer(vertex.data)
minw = min([v.view.w for v in vertexes.values()])
for edge in edges:
edge.view = EdgeViewer()
sug = SugiyamaLayout(graph.C[0])
roots = [v for v in sug.g.sV if len(v.e_in()) == 0]
sug.init_all(roots=roots, optimize=True)
sug.yspace = VertexViewer.HEIGHT
sug.xspace = minw
sug.route_edge = route_with_lines
sug.draw()
return sug
def draw_graph(vertexes, edges, return_ascii=True):
"""Build a DAG and draw it in ASCII."""
sug = build_sugiyama_layout(vertexes, edges)
Xs = []
Ys = []
for vertex in sug.g.sV:
Xs.extend([vertex.view.xy[0] - vertex.view.w / 2.0, vertex.view.xy[0] + vertex.view.w / 2.0])
Ys.extend([vertex.view.xy[1], vertex.view.xy[1] + vertex.view.h])
for edge in sug.g.sE:
for x, y in edge.view._pts:
Xs.append(x)
Ys.append(y)
minx = min(Xs)
miny = min(Ys)
maxx = max(Xs)
maxy = max(Ys)
canvas_cols = int(math.ceil(maxx - minx)) + 1
canvas_lines = int(round(maxy - miny))
canvas = AsciiCanvas(canvas_cols, canvas_lines)
for edge in sug.g.sE:
assert len(edge.view._pts) > 1
for index in range(1, len(edge.view._pts)):
start = edge.view._pts[index - 1]
end = edge.view._pts[index]
canvas.line(
int(round(start[0] - minx)),
int(round(start[1] - miny)),
int(round(end[0] - minx)),
int(round(end[1] - miny)),
"*",
)
for vertex in sug.g.sV:
x = vertex.view.xy[0] - vertex.view.w / 2.0
y = vertex.view.xy[1]
canvas.box(int(round(x - minx)), int(round(y - miny)), vertex.view.w, vertex.view.h)
canvas.text(int(round(x - minx)) + 1, int(round(y - miny)) + 1, vertex.data)
if return_ascii:
return canvas.draws()
else:
canvas.draw()

View file

@ -1516,8 +1516,16 @@ class Graph:
def __repr__(self):
vertex_ids = [vertex.id for vertex in self.vertices]
edges_repr = "\n".join([f"{edge.source_id} --> {edge.target_id}" for edge in self.edges])
return f"Graph: \nNodes: {vertex_ids}\nConnections: \n{edges_repr}"
edges_repr = "\n".join([f" {edge.source_id} --> {edge.target_id}" for edge in self.edges])
return (
f"Graph Representation:\n"
f"----------------------\n"
f"Vertices ({len(vertex_ids)}):\n"
f" {', '.join(map(str, vertex_ids))}\n\n"
f"Edges ({len(self.edges)}):\n"
f"{edges_repr}"
)
def layered_topological_sort(
self,

File diff suppressed because it is too large Load diff

View file

@ -76,7 +76,7 @@ aiofiles = "^24.1.0"
setuptools = ">=70"
nanoid = "^2.0.0"
filelock = "^3.15.4"
grandalf = "^0.8.0"
[tool.poetry.extras]
deploy = ["celery", "redis", "flower"]
local = ["llama-cpp-python", "sentence-transformers", "ctransformers"]