Skip to content

pl-graph element

Using the PyGraphviz library, create Graphviz DOT visualizations.

Sample elements

Screenshot of the pl-graph element using graphviz syntax

question.html
<pl-graph> digraph G { A -> B } </pl-graph>

Screenshot of the pl-graph element using a matrix

question.html
<pl-graph params-name="matrix" params-name-labels="labels"></pl-graph>
server.py
import prairielearn as pl
import numpy as np

def generate(data):
    mat = np.random.random((3, 3))
    mat = mat / np.linalg.norm(mat, 1, axis=0)
    data["params"]["labels"] = pl.to_json(["A", "B", "C"])
    data["params"]["matrix"] = pl.to_json(mat)

question.html
<pl-graph params-type="networkx" params-name="random-graph"></pl-graph>
server.py
import random
import string

import prairielearn as pl
import networkx as nx

def generate(data):
    random_graph = nx.gnm_random_graph(5, 6)

    for in_node, out_node, edge_data in random_graph.edges(data=True):
        edge_data["label"] = random.choice(string.ascii_lowercase)

    data["params"]["random-graph"] = pl.to_json(random_graph)

Customizations

Attribute Type Default Description
aria-label string A short text alternative describing the graph, read by screen readers. See the accessibility section.
aria-description string A longer, more detailed text description of the graph, read by screen readers. Requires aria-label. See the accessibility section.
directed boolean true Whether to treat edges in an adjacency matrix as directed or undirected. If set to false, then edges will be rendered as undirected. The input adjacency matrix must be symmetric if this is set to false.
directory string "." Directory where the source file is located. Can be "." (question directory), "clientFilesCourse", or "serverFilesCourse".
engine string dot The rendering engine to use; supports "circo", "dot", "fdp", "neato", "osage", and "twopi".
log-warnings boolean true Whether to log warnings that occur during Graphviz rendering.
negative-weights boolean false Whether to recognize negative weights in an adjacency matrix. If set to false, then all weights at most 0 are ignored (not counted as an edge). If set to true, then all weights that are not None are recognized.
params-name string The name of a parameter containing the data to use as input. Data type to use depends on params-type attribute.
params-name-labels string When using an adjacency matrix, the parameter that contains the labels for each node.
params-type string "adjacency-matrix" Which backend to use for rendering a graph from data. By default, only adjacency-matrix and networkx exist, but custom types can be added through extensions.
source-file-name string Name of the file to load graph content from. If provided, the file content will be used instead of the element's inner HTML. Useful for complex graphs with special characters like angle brackets in record-based nodes.
weights boolean When using an adjacency matrix, whether to show the edge weights. By default, will automatically show weights for stochastic matrices (when they are not binary 0/1).
weights-digits integer 2 When using an adjacency matrix, how many digits to show for the weights.
weights-presentation-type string "f" Number display format for the weights when using an adjacency matrix. If weights-presentation-type is "sigfig", each number is formatted using the to_precision module to digits significant figures. Otherwise, each number is formatted as {:.{digits}{presentation-type}}.

Migrating from deprecated attributes

The following deprecated attribute is still supported for backward compatibility:

Old syntax New syntax
params-name-matrix="<name>" params-name="<name>"

Details

Note that using networkx for rendering, attributes from the input networkx graph are retained when creating a Graphviz DOT visualization. As a result, it is possible to set node and edge properties such as color, line weight, as part of the input graph and have these reflected in the rendering. These include global properties of the graph, such as the rankdir used in rendering. See the Graphviz documentation on attributes for more information on what attributes are supported. The currently used Graphviz version is 2.44.0.

The source-file-name attribute is particularly useful when working with static graphs that contain special characters like angle brackets (<>), which are used in record-based nodes but can interfere with HTML parsing. By placing the graph content in an external file, you can avoid the need to escape these characters.

Accessibility

A pl-graph renders as an SVG. Graphviz adds a <title> to every node and edge, but those are hover tooltips, not a description of the graph as a whole, so a screen reader user gets no meaningful information by default. How you provide an accessible equivalent depends on how the graph is used.

Informational graphs

If the graph conveys information (rather than being the thing a student must interpret to answer), give it a text alternative with aria-label. When aria-label is set, the graph is exposed to assistive technologies as a single labeled image, which also hides Graphviz's per-node titles. Use aria-description for additional detail if needed, though the aria-description attribute has inconsistent screen reader support and requires aria-label to also be set.

For a static graph, describe it directly:

question.html
<pl-graph aria-label="Directed graph with an edge from A to B and from B to C">
  digraph G { A -> B -> C }
</pl-graph>

For a randomly generated graph, a fixed string like "a random graph" tells a screen reader user nothing about what was actually drawn. Generate the description from the same data so it always matches the graph:

server.py
import prairielearn as pl
import networkx as nx

def generate(data):
    graph = nx.gnm_random_graph(5, 6)
    data["params"]["graph"] = pl.to_json(graph)

    edges = ", ".join(f"{u} to {v}" for u, v in graph.edges())
    data["params"]["graph_alt"] = (
        f"Undirected graph with {graph.number_of_nodes()} nodes "
        f"and {graph.number_of_edges()} edges: {edges}."
    )
question.html
<pl-graph params-type="networkx" params-name="graph" aria-label="{{ params.graph_alt }}"></pl-graph>

Presenting the same information in an accessible format

Packing a large graph into a single aria-label string produces a long, unstructured announcement that is hard to navigate. Often the better option is to also present the data in a natively accessible format that every student benefits from — for example an adjacency matrix rendered with pl-matrix-latex, or an edge table:

question.html
<pl-graph
  params-type="networkx"
  params-name="graph"
  aria-label="Undirected graph; its edges are listed in the table below"
></pl-graph>

<table>
  <caption>
    Edges of the graph above
  </caption>
  <thead>
    <tr>
      <th scope="col">From</th>
      <th scope="col">To</th>
    </tr>
  </thead>
  <tbody>
    {{#params.edge_rows}}
    <tr>
      <td>{{from}}</td>
      <td>{{to}}</td>
    </tr>
    {{/params.edge_rows}}
  </tbody>
</table>

Graphs that are the question

If a student must read the graph to answer (for example, "give the adjacency matrix of the graph below"), a descriptive aria-label is not appropriate. It appears in the page source, so it would reveal the answer, and no text alternative can convey the graph in a form the student is expected to reason about. Such a question is not accessible to screen reader users as-is; provide an alternative version of the question instead.

Example implementations

Extension API

Custom values for params-type can be added with element extensions. Each custom type is defined as a function that takes as input the element and data values and returns processed DOT syntax as output.

A minimal type function can look something like:

def custom_type(element, data):
    return "graph { a -- b; }"

In order to register these custom types, your extension should define the global backends dictionary. This will map a value of params-type to your function above:

backends = {
    'my-custom-type': custom_type
}

This will automatically get picked up when the extension gets imported. If your extension needs extra attributes to be defined, you may optionally define the global optional_attribs array that contains a list of attributes that the element may use.

For a full implementation, check out the edge-inc-matrix extension in the exampleCourse.

See also