Skip to content

Develop Custom Plot Functions

This tutorial guides you through the process of creating your own plotting functions in pathpyG.

The visualization framework of pathpyg is designed in such a way that is easy to extend it according your own needs.

For this tutorial we want to implement capabilities to plot histograms.

You will learn:

  • How to set up a generic plot function
  • How to convert pathpyG data to plot data
  • How to plot with d3js
  • How to plot with tikz
  • How to plot with matplotlib

Structure

Plotting commands and functions are located under /src/pathpyG/visualisation/

📁 visualisation
├── 📄 __init__.py
├── 📁 _d3js
   └── 📄 ...
├── 📁 _matplotlib
   └── 📄 ...
├── 📁 _tikz
   └── 📄 ...
├── 📄 layout.py
├── 📄 network_plots.py
├── 📄 plot.py
└── 📄 utils.py

Folders with _... indicate the supported backends. We will have a look at them later.

The layout.py file includes algorithms to calculate the positions of the nodes.

In the utils.py file are useful helper functions collected. E.g. among others a function that converts hex_to_rgb, rgb_to_hex, or a simple Colormap class. If your plot needs generic functions which might be helpful for other plots as well, this would be a good place to store them.

The network_plots.py file includes all plots related to network visualization. We will create in this tutorial a similar collection for histograms.

Finally, the plot.py file contains our generic PathPyPlot class which we will use to build our own class.

This abstract class has a property _kind which will specify the type of plot for the generic plot function. Similar to pandas we should be able to call:

pp.plot(graph, kind="hist")

This abstract class has two dict variables self.data and self.config. The self.data variable is used to store the data needed for the plot, while the self.config stores all the configurations passed to the plot.

Furthermore this class has three abstract methods we have to define later for our supported backends: generate to generate the plot, save to save the plot to a file, show to show the current plot.

Let's get started

In order to get started, we have to create a new python file where we will store our histogram plots. So let's generate a new file hist_plots.py

touch hist_plots.py

We start with creating a function which allows us later to plot a histogram.

This function will take a Graph object as input and has the parameters key and bins as well as a dict of kwargs for furthermore specifications.

We will use the key variable to define the data type of the histogram e.g. by='betweenes' to get the betweenes centrality plotted. With the bins parameters we will change the amount of bins in the histogram. all other options will by passed to the function as keyword arguments and can be backend specific.

"""Histogram plot classes."""
from __future__ import annotations

import logging

from typing import TYPE_CHECKING, Any

# pseudo load class for type checking
if TYPE_CHECKING:
    from pathpyG.core.Graph import Graph

# create logger
logger = logging.getLogger("pathpyG")


def hist(network: Graph, key: str = 'degree', bins: int = 10, **kwargs: Any) -> HistogramPlot:
    """Plot a histogram."""
    return HistogramPlot(network, key, bins, **kwargs)

pathpyG is using logging to print out messages and errors. It's a good habit to use it also for your plotting function.

Our hist function will be callable via the package. e.g. pp.hist(...). Itself it will return a plotting class which we have to create.

from pathpyG.visualisations.plot import PathPyPlot

class HistogramPlot(PathPyPlot):
    """Histogram plot class for a network properties."""

    _kind = "hist"

    def __init__(self, network: Graph, key: str = 'degree', bins: int = 10, **kwargs: Any) -> None:
        """Initialize network plot class."""
        super().__init__()
        self.network = network
        self.config = kwargs
        self.config['bins'] = bins
        self.config['key'] = key
        self.generate()

    def generate(self) -> None:
        """Generate the plot."""
        logger.debug("Generate histogram.")

The HistogramPlot plotting class is a child from our abstract PathPyPlot function. We will overwrite the abstract generate() function in order to get the data needed for our plot.

By convention we assume d3js will be the default plot backend, hence the final data generated by this function should provide the necessary data structure for this backend.

For other backends, this data might be needed to be converted e.g. keywords might be different. We will address this later in our tutorial.

Testing, Testing, Testing

Before we start developing our histogram plot, we should set up a test environment so that we can directly develop the unit test next to our plot function.

Therefore we are going to our testing folder an create a new test file.

cd ../../../tests/
touch test_hist.py

Now we can create a simple test environment with a simple graph and call our hist(...) function.

from pathpyG.core.Graph import Graph
from pathpyG.visualisations.hist_plots import hist


def test_hist_plot() -> None:
    """Test to plot a histogram."""
    net = Graph.from_edge_list([["a", "b"], ["b", "c"], ["a", "c"]])
    hist(net)

Note: If you only want to run this function and not all other test you can use:

pytest -s -k 'test_hist_plot'

Generating the plot data

To plot our histogram we first have to generate the required data from our graph.

In the future we might want to add more options for histograms, hence we use the match-case function form python.

    def generate(self) -> None:
        """Generate the plot."""
        logger.debug("Generate histogram.")

        data: dict = {}

        match self.config["key"]:
            case "indegrees":
                logger.debug("Generate data for in-degrees")
                data["values"] = list(self.network.degrees(mode="in").values())
            case "outdegrees":
                logger.debug("Generate data for out-degrees")
                data["values"] = list(self.network.degrees(mode="out").values())
            case _:
                logger.error(
                    f"The <{self.config['key']}> property",
                    "is currently not supported for hist plots.",
                )
                raise KeyError

        data["title"] = self.config["key"]
        self.data["data"] = data

First we initialize a dictionary data to store our values. In this case we are interested in the in and out-degrees of our graph, which are already implemented in pathpyG (state 2023-11-26).

If the keyword is not supported the function will raise a KeyError.

To provide a default title for our plot we also store the keyword in the data dict. If further data is required for the plot it can be stored here.

Finally, we add the data dict to our self.data variable of the plotting class. This variable will be used later in the backend classes.

With this our basic histogram plot function is finished. We are now able to call the plot function, get the data from our graph and create a data-set which can be passed down to the backend for visualization.

The matplotlib backend

Let's open the _matplotlib folder located under /src/pathpyG/visualisation/_matplotlib, where all matplotlib functions are stored.

📁 _matplotlib
├── 📄 __init__.py
├── 📄 core.py
└── 📄 network_plots.py

The _init_.py holds the configuration for the plot function, which we will modify later. The core.py file contains the generic MatplotlibPlot class, which provides save and show functionalities for our plots. We do not need to modify these functions. Instead, we have to generate a translation function from our generic data dict (see above) to a histogram in matplotlib. To do so, lets create first a new python file named hist_plots.py

cd _matplotlib
touch hist_plots.py

Here we will add our missing piece for a functional matplotlib plot.

"""Histogram plot classes."""
from __future__ import annotations

import logging

from typing import TYPE_CHECKING, Any

# pseudo load class for type checking
if TYPE_CHECKING:
    from pathpyG.core.Graph import Graph

# create logger
logger = logging.getLogger("pathpyG")


def hist(network: Graph, key: str = 'degree', bins: int = 10, **kwargs: Any) -> HistogramPlot:
    """Plot a histogram."""
    return HistogramPlot(network, key, bins, **kwargs)