Skip to content

ontograph

A module adding graphing functionality to ontopy.ontology

OntoGraph

A mixin class used by ontopy.ontology.Ontology that adds functionality for generating graph representations of the ontology.

Source code in ontopy/ontograph.py
class OntoGraph:
    """A mixin class used by ontopy.ontology.Ontology that adds
    functionality for generating graph representations of the ontology.
    """

    _default_style = {
        "graph": {
            "graph_type": "digraph",
            "rankdir": "RL",
            "fontsize": 8,
            # "fontname": "Bitstream Vera Sans", "splines": "ortho",
            # "engine": "neato",
        },
        "class": {
            "style": "filled",
            "fillcolor": "#ffffcc",
        },
        "defined_class": {
            "style": "filled",
            "fillcolor": "#ffc880",
        },
        "individuals": {},
        "is_a": {"arrowhead": "empty"},
        "equivalent_to": {
            "color": "green3",
        },
        "disjoint_with": {
            "color": "red",
        },
        "inverse_of": {
            "color": "orange",
        },
        # "other": {"color": "blue", },
        "relations": {
            "enclosing": {
                "color": "red",
                "arrowtail": "diamond",
                "dir": "back",
            },
            "has_subdimension": {
                "color": "red",
                "arrowtail": "diamond",
                "dir": "back",
                "style": "dashed",
            },
            "has_sign": {"color": "blue", "style": "dotted"},
            "has_property": {"color": "blue"},
            "has_unit": {"color": "magenta"},
            "has_type": {"color": "forestgreen"},
        },
        "other": {"color": "olivedrab"},
    }

    _uml_style = {
        "graph": {
            "graph_type": "digraph",
            "rankdir": "RL",
            "fontsize": 8,
            # "splines": "ortho",
        },
        "class": {
            # "shape": "record",
            "shape": "box",
            "fontname": "Bitstream Vera Sans",
            "style": "filled",
            "fillcolor": "#ffffe0",
        },
        "defined_class": {
            # "shape": "record",
            "shape": "box",
            "fontname": "Bitstream Vera Sans",
            "style": "filled",
            "fillcolor": "#ffc880",
        },
        "individuals": {},
        "is_a": {"arrowhead": "empty"},
        "equivalent_to": {"color": "green3"},
        "disjoint_with": {"color": "red", "arrowhead": "none"},
        "inverse_of": {"color": "orange", "arrowhead": "none"},
        # "other": {"color": "blue", "arrowtail": "diamond", "dir": "back"},
        "relations": {
            "enclosing": {
                "color": "red",
                "arrowtail": "diamond",
                "dir": "back",
            },
            "has_subdimension": {
                "color": "red",
                "arrowtail": "diamond",
                "dir": "back",
                "style": "dashed",
            },
            "has_sign": {"color": "blue", "style": "dotted"},
            "has_property": {"color": "blue"},
            "has_unit": {"color": "magenta"},
            "has_type": {"color": "forestgreen"},
        },
        "other": {"color": "blue"},
    }

    def get_dot_graph(  # pylint: disable=too-many-arguments,too-many-locals,too-many-branches
        self,
        root=None,
        graph=None,
        relations="is_a",
        leafs=None,
        parents=False,
        style=None,
        edgelabels=True,
        constraint=False,
    ):
        """Returns a pydot graph object for visualising the ontology.

        Parameters
        ----------
        root : None | string | owlready2.ThingClass instance
            Name or owlready2 entity of root node to plot subgraph
            below.  If `root` is None, all classes will be included in the
            subgraph.
        graph : None | pydot.Dot instance
            Pydot graph object to plot into.  If None, a new graph object
            is created using the keyword arguments.
        relations : True | str | sequence
            Sequence of relations to visualise.  If True, all relations are
            included.
        leafs : None | sequence
            A sequence of leaf node names for generating sub-graphs.
        parents : bool | str
            Whether to include parent nodes.  If `parents` is a string,
            only parent nodes down to the given name will included.
        style : None | dict | "uml"
            A dict mapping the name of the different graphical elements
            to dicts of pydot style settings. Supported graphical elements
            include:
              - graph : overall settings pydot graph
              - class : nodes for classes
              - individual : nodes for invididuals
              - is_a : edges for is_a relations
              - equivalent_to : edges for equivalent_to relations
              - disjoint_with : edges for disjoint_with relations
              - inverse_of : edges for inverse_of relations
              - relations : with relation names
                  XXX
              - other : edges for other relations and restrictions
            If style is None, a very simple default style is used.
            Some pre-defined styles can be selected by name (currently
            only "uml").
        edgelabels : bool | dict
            Whether to add labels to the edges of the generated graph.
            It is also possible to provide a dict mapping the
            full labels (with cardinality stripped off for restrictions)
            to some abbriviations.
        constraint : None | bool

        Note: This method requires pydot.
        """
        warnings.warn(
            """The ontopy.ontology.get_dot_graph() method is deprecated.
            Use ontopy.ontology.get_graph() instead.

            This requires that you install graphviz instead of the old
            pydot package.""",
            DeprecationWarning,
        )

        # FIXME - double inheritance leads to dublicated nodes. Make sure
        #         to only add a node once!

        if style is None or style == "default":
            style = self._default_style
        elif style == "uml":
            style = self._uml_style
        graph = self._get_dot_graph(
            root=root,
            graph=graph,
            relations=relations,
            leafs=leafs,
            style=style,
            edgelabels=edgelabels,
        )
        # Add parents
        # FIXME - factor out into a recursive function to support
        #         multiple inheritance

        if parents and root:
            root_entity = (
                self.get_by_label(root) if isinstance(root, str) else root
            )
            while True:
                parent = root_entity.is_a.first()
                if parent is None or parent is owlready2.Thing:
                    break
                label = asstring(parent)
                if self.is_defined(label):
                    node = pydot.Node(label, **style.get("defined_class", {}))
                    # If label contains a hyphen, the node name will
                    # be quoted (bug in pydot?).  To work around, set
                    # the name explicitly...
                    node.set_name(label)
                else:
                    node = pydot.Node(label, **style.get("class", {}))
                    node.set_name(label)
                graph.add_node(node)
                if relations is True or "is_a" in relations:
                    kwargs = style.get("is_a", {}).copy()
                    if isinstance(edgelabels, dict):
                        kwargs["label"] = edgelabels.get("is_a", "is_a")
                    elif edgelabels:
                        kwargs["label"] = "is_a"

                    rootnode = graph.get_node(asstring(root_entity))[0]
                    edge = pydot.Edge(rootnode, node, **kwargs)
                    graph.add_edge(edge)
                if isinstance(parents, str) and label == parents:
                    break
                root_entity = parent
        # Add edges
        for node in graph.get_nodes():
            try:
                entity = self.get_by_label(node.get_name())
            except (KeyError, NoSuchLabelError):
                continue
            # Add is_a edges
            targets = [
                e
                for e in entity.is_a
                if not isinstance(
                    e,
                    (
                        owlready2.ThingClass,
                        owlready2.ObjectPropertyClass,
                        owlready2.PropertyClass,
                    ),
                )
            ]

            self._get_dot_add_edges(
                graph,
                entity,
                targets,
                "relations",
                relations,
                # style=style.get('relations', style.get('other', {})),
                style=style.get("other", {}),
                edgelabels=edgelabels,
                constraint=constraint,
            )

            # Add equivalent_to edges
            if relations is True or "equivalent_to" in relations:
                self._get_dot_add_edges(
                    graph,
                    entity,
                    entity.equivalent_to,
                    "equivalent_to",
                    relations,
                    style.get("equivalent_to", {}),
                    edgelabels=edgelabels,
                    constraint=constraint,
                )

            # disjoint_with
            if hasattr(entity, "disjoints") and (
                relations is True or "disjoint_with" in relations
            ):
                self._get_dot_add_edges(
                    graph,
                    entity,
                    entity.disjoints(),
                    "disjoint_with",
                    relations,
                    style.get("disjoint_with", {}),
                    edgelabels=edgelabels,
                    constraint=constraint,
                )

            # Add inverse_of
            if (
                hasattr(entity, "inverse_property")
                and (relations is True or "inverse_of" in relations)
                and entity.inverse_property not in (None, entity)
            ):
                self._get_dot_add_edges(
                    graph,
                    entity,
                    [entity.inverse_property],
                    "inverse_of",
                    relations,
                    style.get("inverse_of", {}),
                    edgelabels=edgelabels,
                    constraint=constraint,
                )

        return graph

    def _get_dot_add_edges(  # pylint: disable=too-many-arguments,too-many-locals,too-many-branches
        self,
        graph,
        entity,
        targets,
        relation,
        relations,
        style,
        edgelabels=True,
        constraint=None,
    ):
        """Adds edges to `graph` for relations between `entity` and all
        members in `targets`.  `style` is a dict with options to pydot.Edge().
        """
        nodes = graph.get_node(asstring(entity))
        if not nodes:
            return
        node = nodes[0]

        for target in targets:
            entity_string = asstring(target)
            if isinstance(target, owlready2.ThingClass):
                pass
            elif isinstance(
                target, (owlready2.ObjectPropertyClass, owlready2.PropertyClass)
            ):
                label = asstring(target)
                nodes = graph.get_node(label)
                if nodes:
                    kwargs = style.copy()
                    if isinstance(edgelabels, dict):
                        kwargs["label"] = edgelabels.get(label, label)
                    elif edgelabels:
                        kwargs["label"] = label
                    edge = pydot.Edge(node, nodes[0], **kwargs)
                    if constraint is not None:
                        edge.set_constraint(constraint)
                    graph.add_edge(edge)
            elif isinstance(target, owlready2.Restriction):
                rname = asstring(target.property)
                rtype = owlready2.class_construct._restriction_type_2_label[  # pylint: disable=protected-access
                    target.type
                ]

                if relations or rname in relations:
                    vname = asstring(target.value)
                    others = graph.get_node(vname)

                    # Only proceede if there is only one node named `vname`
                    # and an edge to that node does not already exists
                    if (
                        len(others) == 1
                        and (node.get_name(), vname)
                        not in graph.obj_dict["edges"].keys()
                    ):
                        other = others[0]
                    else:
                        continue

                    if rtype in ("min", "max", "exactly"):
                        label = f"{rname} {rtype} {target.cardinality}"
                    else:
                        label = f"{rname} {rtype}"

                    kwargs = style.copy()
                    if isinstance(edgelabels, dict):
                        slabel = f"{rname} {rtype}"
                        kwargs["label"] = edgelabels.get(slabel, label) + "   "
                    elif edgelabels:
                        kwargs["label"] = label + "   "  # Add some extra space

                    edge = pydot.Edge(node, other, **kwargs)
                    if constraint is not None:
                        edge.set_constraint(constraint)
                    graph.add_edge(edge)

            elif hasattr(self, "_verbose") and self._verbose:
                print(
                    f"* get_dot_graph() * Ignoring: {node.get_name()} "
                    f"{relation} {entity_string}"
                )

    def _get_dot_graph(  # pylint: disable=too-many-arguments,too-many-locals,too-many-branches,too-many-statements
        self,
        root=None,
        graph=None,
        relations="is_a",
        leafs=None,
        style=None,
        visited=None,
        edgelabels=True,
    ):
        """Help method. See get_dot_graph(). `visited` is used to filter
        out circular dependencies.
        """
        if graph is None:
            kwargs = style.get("graph", {})
            kwargs.setdefault("newrank", True)
            graph = pydot.Dot(**kwargs)

        if relations is True:
            relations = ["is_a"] + list(self.get_relations())
        elif isinstance(relations, str):
            relations = [relations]
        relations = set(
            r
            if isinstance(r, str)
            else asstring(r)
            if len(r.label) == 1
            else r.name
            for r in relations
        )

        if visited is None:
            visited = set()

        if root is None:
            for root_cls in self.get_root_classes():
                self._get_dot_graph(
                    root=root_cls,
                    graph=graph,
                    relations=relations,
                    leafs=leafs,
                    style=style,
                    visited=visited,
                    edgelabels=edgelabels,
                )
            return graph
        if isinstance(root, (list, tuple, set)):
            for _ in root:
                self._get_dot_graph(
                    root=_,
                    graph=graph,
                    relations=relations,
                    leafs=leafs,
                    style=style,
                    visited=visited,
                    edgelabels=edgelabels,
                )
            return graph
        if isinstance(root, str):
            root = self.get_by_label(root)

        if root in visited:
            if hasattr(self, "_verbose") and self._verbose:
                warnings.warn(
                    f"Circular dependency of class {asstring(root)!r}"
                )
            return graph
        visited.add(root)

        label = asstring(root)
        nodes = graph.get_node(label)
        if nodes:
            if len(nodes) > 1:
                warnings.warn(
                    f"More than one node corresponding to label: {label}"
                )
            node = nodes[0]
        else:
            if self.is_individual(label):
                node = pydot.Node(label, **style.get("individual", {}))
                node.set_name(label)
            elif self.is_defined(label):
                node = pydot.Node(label, **style.get("defined_class", {}))
                node.set_name(label)
            else:
                node = pydot.Node(label, **style.get("class", {}))
                node.set_name(label)
            graph.add_node(node)

        if leafs and label in leafs:
            return graph

        for sub_cls in root.subclasses():
            label = asstring(sub_cls)
            if self.is_individual(label):
                subnode = pydot.Node(label, **style.get("individual", {}))
                subnode.set_name(label)
            elif self.is_defined(label):
                subnode = pydot.Node(label, **style.get("defined_class", {}))
                subnode.set_name(label)
            else:
                subnode = pydot.Node(label, **style.get("class", {}))
                subnode.set_name(label)
            graph.add_node(subnode)
            if relations is True or "is_a" in relations:
                kwargs = style.get("is_a", {}).copy()
                if isinstance(edgelabels, dict):
                    kwargs["label"] = edgelabels.get("is_a", "is_a")
                elif edgelabels:
                    kwargs["label"] = "is_a"
                edge = pydot.Edge(subnode, node, **kwargs)
                graph.add_edge(edge)
            self._get_dot_graph(
                root=sub_cls,
                graph=graph,
                relations=relations,
                leafs=leafs,
                style=style,
                visited=visited,
                edgelabels=edgelabels,
            )

        return graph

    def get_dot_relations_graph(self, graph=None, relations="is_a", style=None):
        """Returns a disjoined graph of all relations.

        This method simply calls get_dot_graph() with all root relations.
        All arguments are passed on.
        """
        rels = tuple(self.get_relations())
        roots = [
            relation
            for relation in rels
            if not any(_ in rels for _ in relation.is_a)
        ]
        return self.get_dot_graph(
            root=roots, graph=graph, relations=relations, style=style
        )

get_dot_graph(self, root=None, graph=None, relations='is_a', leafs=None, parents=False, style=None, edgelabels=True, constraint=False)

Returns a pydot graph object for visualising the ontology.

Parameters

root : None | string | owlready2.ThingClass instance Name or owlready2 entity of root node to plot subgraph below. If root is None, all classes will be included in the subgraph. graph : None | pydot.Dot instance Pydot graph object to plot into. If None, a new graph object is created using the keyword arguments. relations : True | str | sequence Sequence of relations to visualise. If True, all relations are included. leafs : None | sequence A sequence of leaf node names for generating sub-graphs. parents : bool | str Whether to include parent nodes. If parents is a string, only parent nodes down to the given name will included. style : None | dict | "uml" A dict mapping the name of the different graphical elements to dicts of pydot style settings. Supported graphical elements include: - graph : overall settings pydot graph - class : nodes for classes - individual : nodes for invididuals - is_a : edges for is_a relations - equivalent_to : edges for equivalent_to relations - disjoint_with : edges for disjoint_with relations - inverse_of : edges for inverse_of relations - relations : with relation names XXX - other : edges for other relations and restrictions If style is None, a very simple default style is used. Some pre-defined styles can be selected by name (currently only "uml"). edgelabels : bool | dict Whether to add labels to the edges of the generated graph. It is also possible to provide a dict mapping the full labels (with cardinality stripped off for restrictions) to some abbriviations. constraint : None | bool

Note: This method requires pydot.

Source code in ontopy/ontograph.py
def get_dot_graph(  # pylint: disable=too-many-arguments,too-many-locals,too-many-branches
    self,
    root=None,
    graph=None,
    relations="is_a",
    leafs=None,
    parents=False,
    style=None,
    edgelabels=True,
    constraint=False,
):
    """Returns a pydot graph object for visualising the ontology.

    Parameters
    ----------
    root : None | string | owlready2.ThingClass instance
        Name or owlready2 entity of root node to plot subgraph
        below.  If `root` is None, all classes will be included in the
        subgraph.
    graph : None | pydot.Dot instance
        Pydot graph object to plot into.  If None, a new graph object
        is created using the keyword arguments.
    relations : True | str | sequence
        Sequence of relations to visualise.  If True, all relations are
        included.
    leafs : None | sequence
        A sequence of leaf node names for generating sub-graphs.
    parents : bool | str
        Whether to include parent nodes.  If `parents` is a string,
        only parent nodes down to the given name will included.
    style : None | dict | "uml"
        A dict mapping the name of the different graphical elements
        to dicts of pydot style settings. Supported graphical elements
        include:
          - graph : overall settings pydot graph
          - class : nodes for classes
          - individual : nodes for invididuals
          - is_a : edges for is_a relations
          - equivalent_to : edges for equivalent_to relations
          - disjoint_with : edges for disjoint_with relations
          - inverse_of : edges for inverse_of relations
          - relations : with relation names
              XXX
          - other : edges for other relations and restrictions
        If style is None, a very simple default style is used.
        Some pre-defined styles can be selected by name (currently
        only "uml").
    edgelabels : bool | dict
        Whether to add labels to the edges of the generated graph.
        It is also possible to provide a dict mapping the
        full labels (with cardinality stripped off for restrictions)
        to some abbriviations.
    constraint : None | bool

    Note: This method requires pydot.
    """
    warnings.warn(
        """The ontopy.ontology.get_dot_graph() method is deprecated.
        Use ontopy.ontology.get_graph() instead.

        This requires that you install graphviz instead of the old
        pydot package.""",
        DeprecationWarning,
    )

    # FIXME - double inheritance leads to dublicated nodes. Make sure
    #         to only add a node once!

    if style is None or style == "default":
        style = self._default_style
    elif style == "uml":
        style = self._uml_style
    graph = self._get_dot_graph(
        root=root,
        graph=graph,
        relations=relations,
        leafs=leafs,
        style=style,
        edgelabels=edgelabels,
    )
    # Add parents
    # FIXME - factor out into a recursive function to support
    #         multiple inheritance

    if parents and root:
        root_entity = (
            self.get_by_label(root) if isinstance(root, str) else root
        )
        while True:
            parent = root_entity.is_a.first()
            if parent is None or parent is owlready2.Thing:
                break
            label = asstring(parent)
            if self.is_defined(label):
                node = pydot.Node(label, **style.get("defined_class", {}))
                # If label contains a hyphen, the node name will
                # be quoted (bug in pydot?).  To work around, set
                # the name explicitly...
                node.set_name(label)
            else:
                node = pydot.Node(label, **style.get("class", {}))
                node.set_name(label)
            graph.add_node(node)
            if relations is True or "is_a" in relations:
                kwargs = style.get("is_a", {}).copy()
                if isinstance(edgelabels, dict):
                    kwargs["label"] = edgelabels.get("is_a", "is_a")
                elif edgelabels:
                    kwargs["label"] = "is_a"

                rootnode = graph.get_node(asstring(root_entity))[0]
                edge = pydot.Edge(rootnode, node, **kwargs)
                graph.add_edge(edge)
            if isinstance(parents, str) and label == parents:
                break
            root_entity = parent
    # Add edges
    for node in graph.get_nodes():
        try:
            entity = self.get_by_label(node.get_name())
        except (KeyError, NoSuchLabelError):
            continue
        # Add is_a edges
        targets = [
            e
            for e in entity.is_a
            if not isinstance(
                e,
                (
                    owlready2.ThingClass,
                    owlready2.ObjectPropertyClass,
                    owlready2.PropertyClass,
                ),
            )
        ]

        self._get_dot_add_edges(
            graph,
            entity,
            targets,
            "relations",
            relations,
            # style=style.get('relations', style.get('other', {})),
            style=style.get("other", {}),
            edgelabels=edgelabels,
            constraint=constraint,
        )

        # Add equivalent_to edges
        if relations is True or "equivalent_to" in relations:
            self._get_dot_add_edges(
                graph,
                entity,
                entity.equivalent_to,
                "equivalent_to",
                relations,
                style.get("equivalent_to", {}),
                edgelabels=edgelabels,
                constraint=constraint,
            )

        # disjoint_with
        if hasattr(entity, "disjoints") and (
            relations is True or "disjoint_with" in relations
        ):
            self._get_dot_add_edges(
                graph,
                entity,
                entity.disjoints(),
                "disjoint_with",
                relations,
                style.get("disjoint_with", {}),
                edgelabels=edgelabels,
                constraint=constraint,
            )

        # Add inverse_of
        if (
            hasattr(entity, "inverse_property")
            and (relations is True or "inverse_of" in relations)
            and entity.inverse_property not in (None, entity)
        ):
            self._get_dot_add_edges(
                graph,
                entity,
                [entity.inverse_property],
                "inverse_of",
                relations,
                style.get("inverse_of", {}),
                edgelabels=edgelabels,
                constraint=constraint,
            )

    return graph

get_dot_relations_graph(self, graph=None, relations='is_a', style=None)

Returns a disjoined graph of all relations.

This method simply calls get_dot_graph() with all root relations. All arguments are passed on.

Source code in ontopy/ontograph.py
def get_dot_relations_graph(self, graph=None, relations="is_a", style=None):
    """Returns a disjoined graph of all relations.

    This method simply calls get_dot_graph() with all root relations.
    All arguments are passed on.
    """
    rels = tuple(self.get_relations())
    roots = [
        relation
        for relation in rels
        if not any(_ in rels for _ in relation.is_a)
    ]
    return self.get_dot_graph(
        root=roots, graph=graph, relations=relations, style=style
    )

get_figsize(graph)

Returns figure size (width, height) in points of figures for the current pydot graph object graph.

Source code in ontopy/ontograph.py
def get_figsize(graph):
    """Returns figure size (width, height) in points of figures for the
    current pydot graph object `graph`."""
    with tempfile.TemporaryDirectory() as tmpdir:
        tmpfile = os.path.join(tmpdir, "graph.svg")
        graph.write_svg(tmpfile)
        xml = ET.parse(tmpfile)
        svg = xml.getroot()
        width = svg.attrib["width"]
        height = svg.attrib["height"]
        if not width.endswith("pt"):
            # ensure that units are in points
            raise ValueError(
                "The width attribute should always be given in 'pt', "
                f"but it is: {width}"
            )

    def asfloat(string):
        return float(re.match(r"^[\d.]+", string).group())

    return asfloat(width), asfloat(height)
Back to top