Graphs ====== .. toctree:: :hidden: api/understand.Graph api/understand.GraphContext api/understand.Node api/understand.Edge api/understand.Legend Understand comes with many built-in graphs such as call trees and control flow graphs. Graphs can also be defined through plugins, like the butterfly graph. Users can export graphs from the Python API. Discovery --------- Available graphs depend on the target: a :class:`Db `, an :class:`Ent `, or an :class:`Arch `. For example, the "Control Flow" graph is typically available for function entities whereas a "UML Class Diagram" is available for class entities. The "Architecture Graph" is only available for architectures. Use :meth:`Graph.list ` to discover available graphs. The :class:`Graph ` objects returned can be used to query information about the graph such as the :meth:`name ` and :meth:`variant `. Note that both the ``name`` and ``variant`` are necessary to uniquely identify a graph. Graphs support :class:`options `. Use :meth:`Graph.options ` followed by :meth:`Options.list ` to discover available options. List catalog graphs for the open project (pass a :class:`Db ` so availability matches the database):: import understand db = understand.open("/path/to/myproject.und") for g in understand.Graph.list(db): print(g.name(), "-", g.variant()) .. _graph-generation: Generation ---------- Call :meth:`db.draw() `, :meth:`ent.draw() `, or :meth:`arch.draw() ` to generate graphs for projects, entities, and architectures respectively. The ``graph`` and ``variant`` parameters are used to identify the graph to generate. If ``graph`` is an :class:`Graph ` object, both values are determined from the Graph object. If ``graph`` is a string with multiple variants and no variant is specified, a default variant is chosen. The draw command can save the graph to disk, return the graph as bytes or return a read-only :class:`GraphContext ` object. Use ``filename`` to save to disk (returning None) or ``format`` to return the graph. The ``filename`` and ``format`` parameters are mutually exclusive. When using ``filename``, the format is determined by the extension. Supported extensions and format arguments are jpg, png, svg, vsdx, vdx, and dot. Additionally, ``format`` can be ``"raw"`` to return a GraphContext instead of bytes. Note that while all graphs technically have dot output, not all dot files are meant to be used with `Graphviz `_. Graphs using custom Understand layout algorithms will have custom attributes in the dot files to support them. Graphs can have options. See the :class:`understand.Options ` class for how to format the ``options`` argument. For Relationship graphs use secondent=EntityUniqueName to indicate the second entity. If an error occurs, an UnderstandError will be raised. Some possible errors are: * Draw Too Large - failed to allocate memory for the image * Unknown Graph - the graph name did not match a known graph * Unsupported File Type - unsupported file format Save a graph to disk by passing a ``filename`` (extension selects the format):: import understand db = understand.open("/path/to/myproject.und") fn = next(db.ents("function ~unresolved ~unknown")) fn.draw("Called By", f"callby_{fn.name()}.png") Return a graph in memory with ``format`` (mutually exclusive with ``filename``). Use ``format="raw"`` for a read-only :class:`GraphContext `. Pass a :class:`Graph ` from :meth:`Graph.list ` so the name and variant match the catalog:: import understand db = understand.open("/path/to/myproject.und") g = understand.Graph.list(db)[0] png = db.draw(g, format="png") ctx = db.draw(g, format="raw") Plugin writing -------------- A graph plugin must have a `name` function and a `draw` function. If the `name` is used by other graphs, the `style` function should also be defined to provide the variant name. The `draw` function creates the graph. At least one `test_` function should be defined to indicate when the graph is available. The parameter `graph` passed to `init` and `draw` is an :class:`GraphContext ` object. It represents the root of the graph. Use the :meth:`cluster `, :meth:`node `, and :meth:`edge ` methods to add :class:`clusters `, :class:`nodes `, and :class:`edges ` to the graph. Each of those objects has a set method to change the Graphviz attributes. Default attributes can be set on the root graph context with the :meth:`default ` method. Nodes and edges can also be synced to :class:`entities `, :class:`references ` or file locations. Use :meth:`options ` to access an :class:`Options ` object. The options object can be used to define and retrieve custom options. You can also provide a legend for your graph using :meth:`legend ` to obtain a :class:`Legend ` object. Use :meth:`define ` to create entries. Use :meth:`set ` to update existing entries. The following sample script is a call tree. It can be run as a project level graph which graphs all functions in a database, or as an entity level graph showing the call tree from the initial entity. :: import understand def name(): """ Required, the name of the graph. """ return "Calls" def style(): """ Optional, the name as it appears in the graph variants drop down. This defaults to "Custom" """ return "Python Template" def test_global(db): """ Optional, return True if this graph is a project level graph If True, this graph will appear in the top level Graph Menu. """ return True def test_entity(ent): """ Optional, return True if the graph exists for the given entity If True, this graph will appear in the Graphical Views menu for the entity. """ return ent.kind().check("function ~unknown ~unresolved"); def test_architecture(arch): """ Optional, return True if the graph exists for the given architecture If True, this graph will appear in the Graphical Views menu for the architecture. """ return True; def init(graph, target): """ Initialize the graph This function is called once on graph creation. Use it to define the available graph options and/or a legend. """ # Define options through the options object graph.options().define("Fill", ["On","Off"], "Off"); # Use isinstance on the target to see if it's an entity, architecture, or # project level graph (if your plugin supports multiple). You can use the # target to customize available options. if isinstance(target, understand.Ent): graph.options().define("Depth", ["1", "2", "3"], "3") # Defining a legend is optional. You can add multiple entries to the legend. graph.legend().define("func", "roundedrect", "Function", "blue", "#FFFFFF") def grabNode(graph, nodes, ent): """ This is a custom function for this script to get a graphviz node """ if ent in nodes: node = nodes[ent] else: # passing an ent to the node object will automatically sync the entity. node = graph.node(ent.name(),ent) if ent.kind().check("unresolved"): # Set graph, node, and edge attributes with the set function. # See Graphviz documentation for available attributes. node.set("shape","octagon") node.set("color","gray") node.set("fillcolor","white") nodes[ent] = node return node def draw(graph, target): """ Draw the graph The second argument can be a database, architecture, or an entity depending which test functions return True. """ fore = "blue" back = "#FFFFFF" # Use options to lookup the current values if graph.options().lookup("Fill") == "On": fore = "#000000" back = "blue" # Use set to update the legend outside of init graph.legend().set("func","fore",fore) graph.legend().set("func","back",back) # Use set to change graph, node, and edge attributes graph.set("rankdir", "LR") # Use default to set the default attributes for graphs, nodes, and edges graph.default("color", fore, "node") graph.default("fillcolor", back, "node") graph.default("style","filled,rounded","node") graph.default("shape","box","node") # store the ent->graphviz node so that each entity node appears only # once no matter how many calls to it there are. nodes = dict() # avoid visiting any node more than once visited = set() # If the graph can be more than one type, use isinstance to determine the type depth = 1 curLevel = [] if isinstance(target, understand.Db): # This sample graphs all functions in the database for a project level # graph curLevel = target.ents("function ~unknown ~unresolved") elif isinstance(target, understand.Arch): # Graph all functions in the architecture for ent in target.ents(False): if ent.kind().check("function ~unknown ~unresolved"): curLevel.append(ent) elif ent.kind().check("file"): for ref in ent.refs("define", "function ~unknown ~unresolved", True): curLevel.append(ref.ent()) # Create clusters with cluster cluster = graph.cluster(target.name(), target) # add nodes to the cluster for ent in curLevel: grabNode(cluster, nodes, ent) else: # For an entity level graph, generate a calls tree curLevel.append(target) depth = int(graph.options().lookup("Depth")) # Loop over the levels of the tree while depth > 0: depth -= 1 nextLevel = [] for ent in curLevel: # avoid visiting nodes multiple times if ent in visited: continue visited.add(ent) # Get a graphviz node for the entity tail = grabNode(graph,nodes,ent) # Add edges for each call for ref in ent.refs("call",unique=True): headEnt = ref.ent() nextLevel.append(headEnt) head = grabNode(graph,nodes,headEnt) # create an edge edge = graph.edge(tail,head) # Use sync so that clicking on the edge will visit the reference edge.sync(ref) curLevel = nextLevel