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 details)

Uploaded Source

Built Distribution

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

Uploaded Python 3

File details

Details for the file grot-0.2.1.tar.gz.

File metadata

  • Download URL: grot-0.2.1.tar.gz
  • Upload date:
  • Size: 19.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.1 CPython/3.10.7

File hashes

Hashes for grot-0.2.1.tar.gz
Algorithm Hash digest
SHA256 6288943645947b430658883ab753e315b5b4f02052a57ba613927620b11fb557
MD5 41a0ea5b4873b77276c5b720a9dcf39d
BLAKE2b-256 d000af8841fc36f65d73c776576cc157fc16128c90d83df1ce66304c144c81f8

See more details on using hashes here.

File details

Details for the file grot-0.2.1-py3-none-any.whl.

File metadata

  • Download URL: grot-0.2.1-py3-none-any.whl
  • Upload date:
  • Size: 9.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.1 CPython/3.10.7

File hashes

Hashes for grot-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 d54aa1ec0706b9eac23ded038a741474942c876915c62aeb5a21db263924cd18
MD5 da9cae7c810e52fd4b3ca6faafe6939f
BLAKE2b-256 57c5d9fef3170dc6c0de5c9ff8d5c6bc0a3de58b034b85b85026ed7a1c8fbec9

See more details on using hashes here.

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