Generate flow graph in the DOT language.

        Args:
            engine: Layout command used. ['dot', 'neato', 'twopi', 'circo', 'fdp', 'sfdp', 'patchwork', 'osage']
            graph_attr: Mapping of (attribute, value) pairs for the graph.
            node_attr: Mapping of (attribute, value) pairs set for all nodes.
            edge_attr: Mapping of (attribute, value) pairs set for all edges.

        Returns: graphviz.Digraph <https://graphviz.readthedocs.io/en/stable/api.html#digraph>

def get_graphviz(self, engine="automatic", graph_attr=None, node_attr=None, edge_attr=None):
        """
        Generate flow graph in the DOT language.

        Args:
            engine: Layout command used. ['dot', 'neato', 'twopi', 'circo', 'fdp', 'sfdp', 'patchwork', 'osage']
            graph_attr: Mapping of (attribute, value) pairs for the graph.
            node_attr: Mapping of (attribute, value) pairs set for all nodes.
            edge_attr: Mapping of (attribute, value) pairs set for all edges.

        Returns: graphviz.Digraph <https://graphviz.readthedocs.io/en/stable/api.html#digraph>
        """
        self.allocate()

        from graphviz import Digraph
        fg = Digraph("flow", #filename="flow_%s.gv" % os.path.basename(self.relworkdir),
            engine="fdp" if engine == "automatic" else engine)

        # Set graph attributes.
        # https://www.graphviz.org/doc/info/
        #fg.attr(label="%s@%s" % (self.__class__.__name__, self.relworkdir))
        fg.attr(label=repr(self))
        #fg.attr(fontcolor="white", bgcolor='purple:pink')
        fg.attr(rankdir="LR", pagedir="BL")
        #fg.attr(constraint="false", pack="true", packMode="clust")
        fg.node_attr.update(color='lightblue2', style='filled')
        #fg.node_attr.update(ranksep='equally')

        # Add input attributes.
        if graph_attr is not None:
            fg.graph_attr.update(**graph_attr)
        if node_attr is not None:
            fg.node_attr.update(**node_attr)
        if edge_attr is not None:
            fg.edge_attr.update(**edge_attr)

        def node_kwargs(node):
            return dict(
                #shape="circle",
                color=node.color_hex,
                fontsize="8.0",
                label=(str(node) if not hasattr(node, "pos_str") else
                    node.pos_str + "\n" + node.__class__.__name__),
            )

        edge_kwargs = dict(arrowType="vee", style="solid")
        cluster_kwargs = dict(rankdir="LR", pagedir="BL", style="rounded", bgcolor="azure2")

        for work in self:
            # Build cluster with tasks.
            cluster_name = "cluster%s" % work.name
            with fg.subgraph(name=cluster_name) as wg:
                wg.attr(**cluster_kwargs)
                wg.attr(label="%s (%s)" % (work.__class__.__name__, work.name))
                #wg.attr(label=repr(work))
                #wg.attr(label="%s (%s)\n%s (%s)" % (
                #    work.__class__.__name__, work.name, work.relworkdir, work.node_id))
                for task in work:
                    wg.node(task.name, **node_kwargs(task))
                    # Connect children to task.
                    for child in task.get_children():
                        # Find file extensions required by this task
                        i = [dep.node for dep in child.deps].index(task)
                        edge_label = "+".join(child.deps[i].exts)
                        fg.edge(task.name, child.name, label=edge_label, color=task.color_hex,
                                **edge_kwargs)

        # Treat the case in which we have a work producing output for other tasks.
        for work in self:
            children = work.get_children()
            if not children: continue
            cluster_name = "cluster%s" % work.name
            seen = set()
            for child in children:
                # This is not needed, too much confusing
                #fg.edge(cluster_name, child.name, color=work.color_hex, **edge_kwargs)
                # Find file extensions required by work
                i = [dep.node for dep in child.deps].index(work)
                for ext in child.deps[i].exts:
                    out = "%s (%s)" % (ext, work.name)
                    fg.node(out)
                    fg.edge(out, child.name, **edge_kwargs)
                    key = (cluster_name, out)
                    if key not in seen:
                        seen.add(key)
                        fg.edge(cluster_name, out, color=work.color_hex, **edge_kwargs)

        # Treat the case in which we have a task that depends on external files.
        seen = set()
        for task in self.iflat_tasks():
            #print(task.get_parents())
            for node in (p for p in task.get_parents() if p.is_file):
                #print("parent file node", node)
                #infile = "%s (%s)" % (ext, work.name)
                infile = node.filepath
                if infile not in seen:
                    seen.add(infile)
                    fg.node(infile, **node_kwargs(node))

                fg.edge(infile, task.name, color=node.color_hex, **edge_kwargs)

        return fg