Source code for langchain_core.runnables.graph_ascii
"""Draws DAG in ASCII.Adapted from https://github.com/iterative/dvc/blob/main/dvc/dagascii.py"""importmathimportosfromtypingimportAny,Mapping,Sequencefromlangchain_core.runnables.graphimportEdgeasLangEdge
[docs]classVertexViewer:"""Class to define vertex box boundaries that will be accounted for during graph building by grandalf. Args: name (str): name of the vertex. """HEIGHT=3# top and bottom box edges + text"""Height of the box."""
[docs]def__init__(self,name:str)->None:self._h=self.HEIGHT# top and bottom box edges + textself._w=len(name)+2# right and left bottom edges + text
@propertydefh(self)->int:"""Height of the box."""returnself._h@propertydefw(self)->int:"""Width of the box."""returnself._w
[docs]classAsciiCanvas:"""Class for drawing in ASCII. Args: cols (int): number of columns in the canvas. Should be > 1. lines (int): number of lines in the canvas. Should be > 1. """TIMEOUT=10
[docs]defdraw(self)->str:"""Draws ASCII canvas on the screen."""lines=map("".join,self.canvas)returnos.linesep.join(lines)
[docs]defpoint(self,x:int,y:int,char:str)->None:"""Create a point on ASCII canvas. Args: x (int): x coordinate. Should be >= 0 and < number of columns in the canvas. y (int): y coordinate. Should be >= 0 an < number of lines in the canvas. char (str): character to place in the specified point on the canvas. """assertlen(char)==1assertx>=0assertx<self.colsasserty>=0asserty<self.linesself.canvas[y][x]=char
[docs]defline(self,x0:int,y0:int,x1:int,y1:int,char:str)->None:"""Create a line on ASCII canvas. Args: x0 (int): x coordinate where the line should start. y0 (int): y coordinate where the line should start. x1 (int): x coordinate where the line should end. y1 (int): y coordinate where the line should end. char (str): character to draw the line with. """ifx0>x1:x1,x0=x0,x1y1,y0=y0,y1dx=x1-x0dy=y1-y0ifdx==0anddy==0:self.point(x0,y0,char)elifabs(dx)>=abs(dy):forxinrange(x0,x1+1):ifdx==0:y=y0else:y=y0+int(round((x-x0)*dy/float(dx)))self.point(x,y,char)elify0<y1:foryinrange(y0,y1+1):ifdy==0:x=x0else:x=x0+int(round((y-y0)*dx/float(dy)))self.point(x,y,char)else:foryinrange(y1,y0+1):ifdy==0:x=x0else:x=x1+int(round((y-y1)*dx/float(dy)))self.point(x,y,char)
[docs]deftext(self,x:int,y:int,text:str)->None:"""Print a text on ASCII canvas. Args: x (int): x coordinate where the text should start. y (int): y coordinate where the text should start. text (str): string that should be printed. """fori,charinenumerate(text):self.point(x+i,y,char)
[docs]defbox(self,x0:int,y0:int,width:int,height:int)->None:"""Create a box on ASCII canvas. Args: x0 (int): x coordinate of the box corner. y0 (int): y coordinate of the box corner. width (int): box width. height (int): box height. """assertwidth>1assertheight>1width-=1height-=1forxinrange(x0,x0+width):self.point(x,y0,"-")self.point(x,y0+height,"-")foryinrange(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(vertices:Mapping[str,str],edges:Sequence[LangEdge])->Any:try:fromgrandalf.graphsimportEdge,Graph,Vertex# type: ignore[import]fromgrandalf.layoutsimportSugiyamaLayout# type: ignore[import]fromgrandalf.routingimport(# type: ignore[import]EdgeViewer,route_with_lines,)exceptImportErrorasexc:raiseImportError("Install grandalf to draw graphs: `pip install grandalf`.")fromexc## Just a reminder about naming conventions:# +------------X# |# |# |# |# Y#vertices_={id:Vertex(f" {data} ")forid,datainvertices.items()}edges_=[Edge(vertices_[s],vertices_[e],data=cond)fors,e,_,condinedges]vertices_list=vertices_.values()graph=Graph(vertices_list,edges_)forvertexinvertices_list:vertex.view=VertexViewer(vertex.data)# NOTE: determine min box length to create the best layoutminw=min(v.view.wforvinvertices_list)foredgeinedges_:edge.view=EdgeViewer()sug=SugiyamaLayout(graph.C[0])graph=graph.C[0]roots=list(filter(lambdax:len(x.e_in())==0,graph.sV))sug.init_all(roots=roots,optimize=True)sug.yspace=VertexViewer.HEIGHTsug.xspace=minwsug.route_edge=route_with_linessug.draw()returnsug
[docs]defdraw_ascii(vertices:Mapping[str,str],edges:Sequence[LangEdge])->str:"""Build a DAG and draw it in ASCII. Args: vertices (list): list of graph vertices. edges (list): list of graph edges. Returns: str: ASCII representation Example: >>> vertices = [1, 2, 3, 4] >>> edges = [(1, 2), (2, 3), (2, 4), (1, 4)] >>> print(draw(vertices, edges)) +---+ +---+ | 3 | | 4 | +---+ *+---+ * ** * * ** * * * * +---+ * | 2 | * +---+ * * * * * ** +---+ | 1 | +---+ """# NOTE: coordinates might me negative, so we need to shift# everything to the positive plane before we actually draw it.Xs=[]Ys=[]sug=_build_sugiyama_layout(vertices,edges)forvertexinsug.g.sV:# NOTE: moving boxes w/2 to the leftXs.append(vertex.view.xy[0]-vertex.view.w/2.0)Xs.append(vertex.view.xy[0]+vertex.view.w/2.0)Ys.append(vertex.view.xy[1])Ys.append(vertex.view.xy[1]+vertex.view.h)foredgeinsug.g.sE:forx,yinedge.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(math.ceil(maxx)-math.floor(minx)))+1canvas_lines=int(round(maxy-miny))canvas=AsciiCanvas(canvas_cols,canvas_lines)# NOTE: first draw edges so that node boxes could overwrite themforedgeinsug.g.sE:assertlen(edge.view._pts)>1forindexinrange(1,len(edge.view._pts)):start=edge.view._pts[index-1]end=edge.view._pts[index]start_x=int(round(start[0]-minx))start_y=int(round(start[1]-miny))end_x=int(round(end[0]-minx))end_y=int(round(end[1]-miny))assertstart_x>=0assertstart_y>=0assertend_x>=0assertend_y>=0canvas.line(start_x,start_y,end_x,end_y,"."ifedge.dataelse"*")forvertexinsug.g.sV:# NOTE: moving boxes w/2 to the leftx=vertex.view.xy[0]-vertex.view.w/2.0y=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)returncanvas.draw()