Developing your own Plots¶
Overview
Add a new histogram plot to pathpyG’s visualisation stack, wire it into pp.plot(...), and render it with Matplotlib. This guide explains the data-prep vs. rendering split and shows the minimal pieces to implement.
This tutorial shows how to add a new plotting capability to pathpyG’s visualisation backend by implementing a histogram plot. You’ll learn how plot types, backends, and configuration work together, and how to add a new plot into the public pp.plot(...) entry point.
What you’ll do
- Understand the new visualisation architecture
- Implement a new
HistogramPlotthat prepares data - Wire it into the plot orchestrator and select backends
- Add Matplotlib rendering support for the new type
- Use and (optionally) test your new plot
Scope
This guide focuses on Matplotlib for rendering histograms (a natural fit). You can add other backends later following the same pattern.
Visualisation architecture at a glance¶
The visualisation module is built around two core abstractions and a single entry point:
-
PathPyPlotprepares data/config for rendering. Subclass it for each plot type. -
PlotBackendrenders a givenPathPyPlotusing a concrete engine (Matplotlib, TikZ, d3.js, Manim). -
plot(...)is the public API. It chooses a plot class (kind) and a backend (by argument or filename extension), instantiates both, then saves or shows.
Reference
See the module overview for supported backends, formats, and styling options. For existing plot types, see NetworkPlot (static) and TemporalNetworkPlot (temporal). For existing backends, see e.g. MatplotlibBackend which we will be using.
Define a new plot type: HistogramPlot¶
Start by creating a new subclass of PathPyPlot (e.g., in src/pathpyG/visualisations/histogram_plot.py). Its job is to:
- Accept the input object(s) (typically a
Graph) and user options - Compute or collect the values to be binned
- Populate
self.datawith a clean, backend-agnostic structure - Update
self.configwith plot configuration (bins, labels, etc.)
Minimal class attributes
Inputs: graph: Graph, key: str (what to measure), bins: int | sequence, plus style options via **kwargs.
Data format (suggested):
- self.data["hist_values"]: list[float | int] — the values to bin
- optionally precomputed bins/edges (if you want backend-agnostic binning)
- self.config should include title, xlabel, ylabel, and bins
Example outline:¶
# src/pathpyG/visualisations/histogram_plot.py
from __future__ import annotations
import logging
from typing import Any
from pathpyG.visualisations.pathpy_plot import PathPyPlot
from pathpyG.core.graph import Graph
logger = logging.getLogger("root")
class HistogramPlot(PathPyPlot):
"""Prepare data for histogram visualisation.
Collects values from a Graph according to `key` and exposes them in
`self.data["hist_values"]` for backends to render.
"""
_kind = "histogram"
def __init__(self, graph: Graph, key: str = "degree", bins: int | list[int] = 10, **kwargs: Any) -> None:
super().__init__()
self.graph = graph
# merge kwargs into config; ensure required fields are present
self.config.update({
"bins": bins,
"title": kwargs.pop("title", f"{key.title()} distribution"),
"xlabel": kwargs.pop("xlabel", key),
"ylabel": kwargs.pop("ylabel", "count"),
})
self.key = key
self.config.update(kwargs)
self.generate()
def generate(self) -> None:
# Compute values to bin based on `key`
if self.key in ("degree", "degrees"):
values = list(self.graph.degrees().values())
elif self.key in ("in_degree", "indegree", "in-degrees"):
values = list(self.graph.degrees(mode="in").values())
elif self.key in ("out_degree", "outdegree", "out-degrees"):
values = list(self.graph.degrees(mode="out").values())
else:
logger.error(f"Histogram key '{self.key}' not supported.")
raise KeyError(self.key)
self.data["hist_values"] = values
Note
- Keep the class small: gather values and fill
self.data/self.config. - Choose names that are clear for backends (
hist_values,bins, labels).
Add the new plot to the public API¶
plot(...) uses the PLOT_CLASSES mapping to instantiate the right plot class for a given kind. Extend it with your new class:
# src/pathpyG/visualisations/plot_function.py
from pathpyG.visualisations.histogram_plot import HistogramPlot
PLOT_CLASSES: dict = {
"static": NetworkPlot,
"temporal": TemporalNetworkPlot,
"histogram": HistogramPlot, # add this line
}
Usage
Backend selection
plot(...) auto-selects a backend from the filename extension if you omit backend. For histograms, prefer PNG via Matplotlib by passing filename="...png" or backend="matplotlib".
Add Matplotlib support for HistogramPlot¶
Backends validate supported plot types. The Matplotlib backend currently supports NetworkPlot and renders nodes/edges. We’ll extend it to also support HistogramPlot.
Implementation approach:
- Add
HistogramPlottoSUPPORTED_KINDSso the backend accepts the plot type. - Branch in
to_fig()(or factor out into a helper) to draw a histogram when the plot is aHistogramPlot.
Sketch of the required changes (condensed for illustration):
# src/pathpyG/visualisations/_matplotlib/backend.py
from pathpyG.visualisations.histogram_plot import HistogramPlot
SUPPORTED_KINDS = {
NetworkPlot: "static",
HistogramPlot: "histogram", # add support
}
class MatplotlibBackend(PlotBackend):
...
def to_fig(self) -> tuple[plt.Figure, plt.Axes]:
# If histogram: render using ax.hist
if self._kind == "histogram":
return self._to_fig_histogram()
# Else: existing network rendering
return self._to_fig_network()
def _to_fig_histogram(self) -> tuple[plt.Figure, plt.Axes]:
fig, ax = plt.subplots(
figsize=(unit_str_to_float(self.config["width"], "in"), unit_str_to_float(self.config["height"], "in")),
dpi=150,
)
ax.set_axis_on()
ax.hist(self.data["hist_values"], bins=self.config.get("bins", 10), color=rgb_to_hex(self.config["node"]["color"]), alpha=0.9)
ax.set_title(self.config.get("title", "Histogram"))
ax.set_xlabel(self.config.get("xlabel", "value"))
ax.set_ylabel(self.config.get("ylabel", "count"))
return fig, ax
def _to_fig_network(self) -> tuple[plt.Figure, plt.Axes]:
# move existing implementation of `to_fig` here
...
Tips
- Reuse
unit_str_to_floatso sizing behaves like other plots. - Use a default color from
self.config["node"]["color"]for consistency. - Keep the new code path fully separate from the network drawing code to avoid regressions.
If you want web or LaTeX histograms
The current d3.js and TikZ backends are tailored to network visualisation (they expect nodes/edges in self.data). To add histogram support there, you would:
- Create a new JS or TeX template for histograms
- Extend the backend to accept
HistogramPlotand dispatch to the new template
Start with Matplotlib first — it's a good starting point.
Try it out¶
Once you’ve added the HistogramPlot, updated PLOT_CLASSES, and extended the Matplotlib backend as shown, you can create and save a histogram in a single call:
import pathpyG as pp
g = pp.Graph.from_edge_list([("a", "b"), ("b", "c"), ("a", "c"), ("c", "d")])
pp.plot(
g,
kind="histogram",
backend="matplotlib", # or infer via filename extension
key="degree",
bins=5,
title="Node Degree Distribution",
filename="degree_hist.png",
)
In notebooks, omit filename to show inline.
Testing (optional but recommended)¶
Create a small unit test to exercise the new path end-to-end:
# tests/visualisations/test_histogram.py
import pathpyG as pp
def test_histogram_plot_matplotlib(tmp_path):
g = pp.Graph.from_edge_list([("a", "b"), ("b", "c"), ("a", "c")])
out = tmp_path / "deg_hist.png"
pp.plot(g, kind="histogram", backend="matplotlib", key="degree", bins=3, filename=str(out))
assert out.exists()
Where to look for guidance and consistency¶
- Backends: see other backends like
Matplotlibandd3.jsfor how plot instances are validated and rendered. - Plot classes: study
NetworkPlotandTemporalNetworkPlotto understand howPathPyPlotsubclasses fillself.dataandself.config. - The module overview explains backend selection, saving, and common styling options.
Recap¶
- New plots are
PathPyPlotsubclasses that prepare data and config. - Register your plot in
PLOT_CLASSESsopp.plot(..., kind=...)can instantiate it. - Extend at least one backend to render your plot type. For histograms, Matplotlib is a clean first target.
- Keep a small, clear data contract between your plot class and backend rendering.
With this, you have a clean, maintainable path to add new visualisations to pathpyG while leveraging the unified pp.plot(...) API and existing backend infrastructure.