dvc: pipeline show: create ASCII visualizations for DAG

Signed-off-by: Ruslan Kuprieiev <[email protected]>
This commit is contained in:
Ruslan Kuprieiev
2018-08-19 21:01:12 +03:00
parent 00d53f50b1
commit c378e4b3d2
5 changed files with 181 additions and 1 deletions
+5
View File
@@ -755,6 +755,11 @@ def parse_args(argv=None):
default=False,
help='Print output files instead of '
'paths to DVC files.')
pipeline_show_parser.add_argument(
'--ascii',
action='store_true',
default=False,
help='Output DAG as ASCII.')
pipeline_show_parser.add_argument(
'targets',
nargs='+',
+51 -1
View File
@@ -2,6 +2,7 @@ import os
from dvc.exceptions import DvcException
from dvc.command.base import CmdBase
from dvc.dagascii import Dagascii
class CmdPipelineShow(CmdBase):
@@ -23,10 +24,59 @@ class CmdPipelineShow(CmdBase):
else:
self.project.logger.info(n)
def _show_ascii(self, target, commands, outs):
import networkx
from dvc.stage import Stage
stage = Stage.load(self.project, target)
node = os.path.relpath(stage.path, self.project.root_dir)
pipelines = list(filter(lambda g: node in g.nodes(),
self.project.pipelines()))
if len(pipelines) == 0:
return
assert len(pipelines) == 1
G = pipelines[0]
stages = networkx.get_node_attributes(G, 'stage')
nodes = set()
edges = []
for e in G.edges():
from_stage = stages[e[0]]
to_stage = stages[e[1]]
if commands:
if to_stage.cmd is None:
continue
nodes.add(from_stage.cmd)
nodes.add(to_stage.cmd)
edges.append((from_stage.cmd, to_stage.cmd))
elif outs:
for from_out in from_stage.outs:
for to_out in to_stage.outs:
nodes.add(from_out.rel_path)
nodes.add(to_out.rel_path)
edges.append((from_out.rel_path,
to_out.rel_path))
else:
nodes.add(from_stage.relpath)
nodes.add(to_stage.relpath)
edges.append((from_stage.relpath, to_stage.relpath))
d = Dagascii(nodes, edges)
d.draw()
def run(self, unlock=False):
for target in self.args.targets:
try:
self._show(target, self.args.commands, self.args.outs)
if self.args.ascii:
self._show_ascii(target,
self.args.commands,
self.args.outs)
else:
self._show(target,
self.args.commands,
self.args.outs)
except DvcException as ex:
msg = 'Failed to show pipeline for \'{}\''.format(target)
self.project.logger.error(msg, ex)
+121
View File
@@ -0,0 +1,121 @@
import math
from asciicanvas.asciicanvas import AsciiCanvas
from asciicanvas.style import Style
from grandalf.graphs import Vertex, Edge, Graph
from grandalf.layouts import SugiyamaLayout
from grandalf.routing import route_with_lines, EdgeViewer
class Dagascii(object):
def __init__(self, vertexes, edges):
#
# Just a reminder about naming conventions:
# +------------X
# |
# |
# |
# |
# Y
#
V = {v: Vertex(" {} ".format(v)) for v in vertexes}
# NOTE: reverting edges to correctly orientate the graph
E = [Edge(V[e], V[s]) for s, e in edges]
V = V.values()
g = Graph(V, E)
class VertexViewer(object):
h = 3 # top and bottom box edges + text
def __init__(self, name):
self.w = len(name) + 2 # right and left bottom edges + text
for v in V:
v.view = VertexViewer(v.data)
# NOTE: determine min box length to create the best layout
minw = min([v.view.w for v in V])
for e in E:
e.view = EdgeViewer()
sug = SugiyamaLayout(g.C[0])
gr = g.C[0]
r = list(filter(lambda x: len(x.e_in()) == 0, gr.sV))
sug.init_all(roots=r, optimize=True)
sug.yspace = 2*VertexViewer.h
sug.xspace = minw
sug.route_edge = route_with_lines
sug.draw()
self.sug = sug
def draw(self):
# NOTE: coordinates might me negative, so we need to shift
# everything to the positive plane before we actually draw it.
Xs = []
Ys = []
for v in self.sug.g.sV:
# NOTE: moving boxes w/2 to the left
Xs.append(v.view.xy[0] - v.view.w/2.0)
Xs.append(v.view.xy[0] + v.view.w/2.0)
Ys.append(v.view.xy[1])
Ys.append(v.view.xy[1] + v.view.h)
for e in self.sug.g.sE:
for xy in e.view._pts:
Xs.append(xy[0])
Ys.append(xy[1])
minx = min(Xs)
miny = min(Ys)
maxx = max(Xs)
maxy = max(Ys)
canvas_cols = int(math.ceil(math.ceil(maxx) - math.floor(minx))) + 1
canvas_lines = int(round(maxy - miny))
canvas = AsciiCanvas(canvas_cols, canvas_lines)
# NOTE: first draw edges so that node boxes could overwrite them
for e in self.sug.g.sE:
assert len(e.view._pts) > 1
for index in range(1, len(e.view._pts)):
start = e.view._pts[index - 1]
end = e.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))
assert start_x >= 0
assert start_y >= 0
assert end_x >= 0
assert end_y >= 0
canvas.add_line(start_x, start_y,
end_x, end_y,
style=Style('*'))
for v in self.sug.g.sV:
# NOTE: moving boxes w/2 to the left
x = v.view.xy[0] - v.view.w/2.0
y = v.view.xy[1]
canvas.add_nine_patch_rect(int(round(x - minx)),
int(round(y - miny)),
v.view.w,
v.view.h)
canvas.add_text(int(round(x - minx)) + 1,
int(round(y - miny)) + 1,
v.data)
canvas.print_out()
+2
View File
@@ -22,3 +22,5 @@ reflink==0.2.0
requests>=2.18.4
wheel>=0.31.1
futures>=3.2.0; python_version == "2.7"
grandalf==0.55555
asciicanvas==0.0.3
+2
View File
@@ -23,6 +23,8 @@ install_requires = [
"jsonpath-rw==1.4.0",
"reflink==0.2.0",
"requests>=2.18.4",
"grandalf==0.55555",
"asciicanvas==0.0.3",
]
# Extra dependencies for remote integrations