Skip to main content

Graphviz syntax wrapper. Draw graphs with pure python.

Project description

Grot

Grot is a noun and means arrowhead in polish language.

Makes graphviz usage simpler

Much less headache. Gets you faster into the point.

Hello Grot

To generate a graph you need to import Grot class, create its instance and define nodes and edges. While g.edge() call, you can pass unlimited number of nodes or plain strings (creates implicit node).

If you don't connect given node (as unconnected) it's going to float somewhere around.

def example_01():
    import os
    from grot import Grot

    this_dir_path = os.path.dirname(__file__)  # if run in console - remove 'directory' parameter below
    out_dir_path = os.path.join(this_dir_path, "out")

    g = Grot(name="example_01", format="png", directory=out_dir_path, graph_attrs={"rankdir": "LR"})

    one = g.node("It is\neaiser")
    two = g.node("graphs", color="#8a9bac")
    ignored = g.node("Node floats when\nunconnected", color="#da3080")

    g.edge(one, "to define", two)
    g.render()

Source of this example is in examples/example_01.py.

It generates raw dot-syntax text file in: examples/out/example_01.gv. And the final graph file is in: examples/out/example_01.gv.png:

Rendered example image to be shown in gitlab)

Branches

One call of g.edge(node1, node2, node3, ...) creates single chain of arrows. To make a branch, you need to call g.edge once again.

def example_02a():
    from grot import Grot

    this_dir_path = os.path.dirname(__file__)  # if run in console - remove 'directory' parameter below
    out_dir_path = os.path.join(this_dir_path, "out")
    g = Grot(name="example_02a", format="png", directory=out_dir_path, graph_attrs={"rankdir": "LR"})

    one = g.node("One")
    two = g.node("Two")
    three = g.node("Three")
    four = g.node("Four")
    five = g.node("Five")
    six = g.node("Six")
    seven = g.node("Seven")

    g.edge(one, two, three, four, "A")
    g.edge(two, five, six, "B")
    g.edge(two, seven, "C")

    g.render()

Source of this example is in examples/example_02.py.

It generates raw dot-syntax text file in: examples/out/example_02a.gv. And the final graph file is in: examples/out/example_02a.gv.png:

Rendered example image to be shown in gitlab)

Branches - syntax variant

You don't have to assign nodes to variables. However it's a good practice to do so. You can define nodes while g.edge(...) call. Here node two is assigned to a local variable, because we refer it several times.

def example_02b():
    from grot import Grot

    this_dir_path = os.path.dirname(__file__)  # if run in console - remove 'directory' parameter below
    out_dir_path = os.path.join(this_dir_path, "out")

    g = Grot(name="example_02b", format="png", directory=out_dir_path, graph_attrs={"rankdir": "LR"})

    two = g.node("Two")
    g.edge(g.node("One"), two, g.node("Three"), g.node("Four"), "A")
    g.edge(two, g.node("Five"), g.node("Six"), "B")
    g.edge(two, g.node("Seven"), "C")

    g.render()

Source of this example is in examples/example_02.py.

It generates raw dot-syntax text file in: examples/out/example_02b.gv. And the final graph file is in: examples/out/example_02b.gv.png:

Rendered example image to be shown in gitlab)

Branches

One call of g.edge(node1, node2, node3, ...) creates single chain of arrows. To make a branch, you need to call g.edge once again.

def example_tree():
    import os
    from typing import List, Optional

    from airium import Airium
    from grot import Grot, NodeVisit, Node

    this_dir_path = os.path.dirname(__file__)  # if run in console - remove 'directory' parameter below
    out_dir_path = os.path.join(this_dir_path, "out")

    font_face = "Monospace"  # "Courier New", "Helvetica"
    g = Grot(
        name="example_tree",
        format="png",
        directory=out_dir_path,
        graph_attrs={
            "fontname": font_face,
            "layout": "neato",
            "overlap": "scalexy",
            "mode": "ipsep",
            "sep": "+5",
        },
        node_attrs={
            "fontname": font_face,
            "penwidth": "1.1",
            "shape": "box",
            "width": "0",
            "height": "0",
            "margin": "0",
        },
        edge_attrs={
            "fontname": font_face,
            "penwidth": "0.80",
        },
    )

    def html_table_label(
        own_name: str, own_type: str, own_path: str, children: list[NodeVisit], fg_color: str, bg_color=str
    ) -> str:
        a = Airium(base_indent="")

        with a.table(border="0", bgcolor=bg_color):
            shaded_color = "#757575"
            with a.tr():
                with a.td(align="left").font(color=fg_color, **{"point-size": "29"}):
                    a(f" <b><u>{own_name}</u></b>")
                with a.td(align="right").font(color=fg_color):
                    types_ = " | ".join(sorted(set(c.type for c in children)))
                    types_ = f" [{types_[30:]}...]" if len(types_) > 36 else f" [{types_}]"
                    a(f"{own_type}{types_}")
            with a.tr():
                with a.td(colspan=2, align="center").font(color=shaded_color):
                    a(f"<i>{own_path}</i>")

            for visit in children:
                type_annotation = f'<font color="{shaded_color}"><b>{visit.type}</b> '
                sub_types = " | ".join(sorted(NodeVisit.children_types(visit.value)))
                if len(sub_types) > 36:
                    type_annotation += f"[{sub_types[:30]}...]"
                elif sub_types:
                    type_annotation += f"[{sub_types}]"
                type_annotation += "</font>"
                right = repr(visit.value)
                if len(right) < 55 and "<" not in right:
                    left = f"{visit.key}: {type_annotation}"
                    right = f" = {right}"
                else:
                    left = f"{visit.key}:"
                    right = type_annotation

                with a.tr():
                    a.td(align="left").font(_t=left, color=fg_color)
                    a.td(align="right").font(_t=right, color=fg_color)

            if not children:
                a.tr().td(colspan=2).font(_t="No children", color=fg_color)
        return str(a)

    def draw_node(node_object, path=("root",)) -> Optional[Node]:
        children: List[NodeVisit] = []
        for visit in NodeVisit.iter_tree_nodes(node_object):
            visit: NodeVisit
            visit.graph_node_id = draw_node(visit.value, (*path, visit.key))
            children.append(visit)

        if not children:
            return  # draw only containers not leafs
        fg_col = "#111111" if len(path) < 2 else "#112233" if len(path) % 2 else "#334433"
        bg_col = "#eeeeee" if len(path) < 2 else "#ddeedd" if len(path) % 2 else "#eee6dd"
        table_body: str = html_table_label(
            own_name=path[-1],
            own_type=type(node_object).__name__,
            own_path=NodeVisit.join_path_str(*path),
            children=children,
            fg_color=fg_col,
            bg_color=bg_col,
        )

        own_node: Node = g.html_node(table_body)

        for visit in children:
            if visit.graph_node_id:
                g.edge(own_node, visit.graph_node_id)

        return own_node

    my_nested_structure = {
        "web-app": {
            "servlet": [
                {
                    "servlet-name": "cofaxCDS",
                    "servlet-class": "org.cofax.cds.CDSServlet",
                    "init-param": {
                        "configGlossary:installationAt": "Philadelphia, PA",
                        "configGlossary:poweredBy": "Cofax",
                        "dataStoreLogFile": "/usr/local/tomcat/logs/datastore.log",
                        "dataStoreConnUsageLimit": 100,
                        "dataStoreLogLevel": "debug",
                        "maxUrlLength": 500,
                    },
                },
                {
                    "servlet-name": "cofaxEmail",
                    "servlet-class": "org.cofax.cds.EmailServlet",
                    "init-param": {"mailHost": "mail1", "mailHostOverride": "mail2"},
                },
                {"servlet-name": "cofaxAdmin", "servlet-class": "org.cofax.cds.AdminServlet"},
                {"servlet-name": "fileServlet", "servlet-class": "org.cofax.cds.FileServlet"},
                {
                    "servlet-name": "cofaxTools",
                    "servlet-class": "org.cofax.cms.CofaxToolsServlet",
                    "init-param": {
                        "templatePath": "toolstemplates/",
                        "log": 1,
                        "logLocation": "/usr/local/tomcat/logs/CofaxTools.log",
                        "lookInContext": 1,
                        "adminGroupID": 4,
                        "betaServer": True,
                    },
                },
            ],
            "servlet-mapping": {
                "cofaxCDS": "/",
                "cofaxEmail": "/cofaxutil/aemail/*",
                "cofaxAdmin": "/admin/*",
                "fileServlet": "/static/*",
                "cofaxTools": "/tools/*",
            },
            "taglib": {"taglib-uri": "cofax.tld", "taglib-location": "/WEB-INF/tlds/cofax.tld"},
        }
    }
    draw_node(my_nested_structure)

    g.render()

Source of this example is in examples/example_tree.py.

It generates raw dot-syntax text file in: examples/out/example_tree.gv. And the final graph file is in: examples/out/example_tree.gv.png:

Rendered example image to be shown in gitlab)

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

grot-0.2.1.tar.gz (19.5 kB view hashes)

Uploaded Source

Built Distribution

grot-0.2.1-py3-none-any.whl (9.4 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page