Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
a950843
refactor visualisations and fix/speed up matplotlib backend
M-Lampert Oct 7, 2025
99a1a3e
update matplotlib and tikz
M-Lampert Oct 8, 2025
94bf4f9
finalise static d3js
M-Lampert Oct 9, 2025
a0968c5
update configs
M-Lampert Oct 9, 2025
a560626
add automatic d3js layouting and margin config param
M-Lampert Oct 9, 2025
8ccb706
optimise temporal_edges
M-Lampert Oct 9, 2025
8cc156c
update d3js temporal graphs
M-Lampert Oct 9, 2025
8d672db
update curved edges
M-Lampert Oct 9, 2025
21553d5
efficiency improvements
M-Lampert Oct 9, 2025
17baaa3
fix issues with higher order and curved edges
M-Lampert Oct 10, 2025
bfb507f
update manim
M-Lampert Oct 10, 2025
3d3fae1
fix indexing
M-Lampert Oct 13, 2025
8ada504
update layout
M-Lampert Oct 13, 2025
a4a185c
add temporal network layouting
M-Lampert Oct 13, 2025
4ca903d
add networkx as dependency
M-Lampert Oct 14, 2025
66cfbdb
update manim backend
M-Lampert Oct 14, 2025
5b327ab
fix window and batch functions for temporal graphs
M-Lampert Oct 14, 2025
e7a9b3d
minor fixes
M-Lampert Oct 14, 2025
112e106
start documentation update
M-Lampert Oct 14, 2025
a447edd
update docs and minor fixes
M-Lampert Oct 15, 2025
8f019c2
fix documentation with underscore
M-Lampert Oct 15, 2025
7ac8873
update visualisation notebooks
M-Lampert Oct 15, 2025
f501e3b
documentation improvements
M-Lampert Oct 15, 2025
4162503
finish visualisation code reference
M-Lampert Oct 15, 2025
3e6835a
update plot dev tutorial
M-Lampert Oct 16, 2025
ca22411
fix existing tests
M-Lampert Oct 16, 2025
3ef7c81
update setup yaml
M-Lampert Oct 16, 2025
81a4401
fix tex setup
M-Lampert Oct 16, 2025
633bf1b
update tex action
M-Lampert Oct 16, 2025
7464f3a
fix
M-Lampert Oct 16, 2025
f824a2c
fix tex live setup
M-Lampert Oct 16, 2025
aba0434
set up optimized ffmpeg download
M-Lampert Oct 16, 2025
0ed67d7
improve apt installation
M-Lampert Oct 16, 2025
4cd31c6
add optional python deps
M-Lampert Oct 16, 2025
67c1eda
add tests and minor fixes
M-Lampert Oct 16, 2025
86c9896
update latex installation
M-Lampert Oct 20, 2025
fe1de79
fix ffmpeg installation
M-Lampert Oct 20, 2025
32feb37
-
M-Lampert Oct 20, 2025
be37d9c
fix ffmpeg
M-Lampert Oct 20, 2025
947b9c1
backend tests
M-Lampert Oct 20, 2025
a69dfb7
Merge branch 'main' into 115-use-indexmapping-as-node-labels-by-default
M-Lampert Oct 22, 2025
5f56033
Merge branch '115-use-indexmapping-as-node-labels-by-default' into 27…
M-Lampert Oct 23, 2025
62fc31d
add time-unfolded graph matplotlib
M-Lampert Oct 23, 2025
ec7675b
add unfolded visualisation to tikz
M-Lampert Oct 24, 2025
f056430
add unit-tests
M-Lampert Oct 24, 2025
17ec126
add unfolded graph to d3js backend
M-Lampert Oct 27, 2025
db8d83c
update docs
M-Lampert Oct 27, 2025
d8fec8f
fix temporal graph RGB assignment
M-Lampert Oct 27, 2025
73166bd
Merge branch '115-use-indexmapping-as-node-labels-by-default' into 27…
M-Lampert Oct 27, 2025
173a577
fix higher-order RGB assignment
M-Lampert Oct 27, 2025
4ccfe2b
fix tikz node border opacity
M-Lampert Oct 27, 2025
e5133e4
Merge branch '115-use-indexmapping-as-node-labels-by-default' into 27…
M-Lampert Oct 27, 2025
9d2c182
fix unit-test fail
M-Lampert Oct 27, 2025
9a725ec
Merge branch '115-use-indexmapping-as-node-labels-by-default' into 27…
M-Lampert Oct 27, 2025
42b1ba4
Merge branch 'main' into 270-implement-time-unfolded-visualization
M-Lampert Oct 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 45 additions & 6 deletions docs/reference/pathpyG/visualisations/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,12 @@ The default backend is `d3.js`, which is suitable for both static and temporal n
We currently support a total of four plotting backends, each with different capabilities making them suitable for different use cases.
The table below provides an overview of the supported backends and their available file formats:

| Backend | Static Networks | Temporal Networks | Available File Formats|
|---------------|------------|-------------|--------------|
| **d3.js** | ✔️ | ✔️ | `html` |
| **manim** | ❌ | ✔️ | `mp4`, `gif` |
| **matplotlib**| ✔️ | ❌ | `png`, `jpg` |
| **tikz** | ✔️ | ❌ | `svg`, `pdf`, `tex`|
| Backend | Static Networks | Temporal Networks | Time-Unfolded Networks | Available File Formats|
|---------------|------------|-------------|--------------|-------------|
| **d3.js** | ✔️ | ✔️ | ✔️ | `html` |
| **manim** | ❌ | ✔️ | ❌ | `mp4`, `gif` |
| **matplotlib**| ✔️ | ❌ | ✔️ | `png`, `jpg` |
| **tikz** | ✔️ | ❌ | ✔️ | `svg`, `pdf`, `tex`|

#### Details

Expand Down Expand Up @@ -427,6 +427,44 @@ The layout algorithm can be any of the supported static layout algorithms descri
<img src="plot/manim_temporal_fa2.gif" alt="Manim Custom Properties Animation" width="650"/>
</div>

### Time-Unfolded Networks

For temporal networks, you can use the time-unfolded visualisation to show a static representation of the temporal network.
In this representation, each node is duplicated for each timestep, and edges are drawn between nodes at different timesteps to represent temporal interactions.
You can enable this visualisation by setting the "kind" argument to `"unfolded"` in the `pp.plot()` function call.
This visualisation is supported by all backends that support static networks, i.e. D3.js, Matplotlib, and TikZ.

!!! example "Time-Unfolded Visualisation of Temporal Networks"

In the example below, we create a time-unfolded visualisation of a temporal network using the `tikz` backend.
```python
import pathpyG as pp

# Example temporal network data
tedges = [
("a", "b", 1),
("a", "b", 2),
("b", "a", 3),
("b", "c", 3),
("d", "c", 4),
("a", "b", 4),
("c", "b", 4),
("c", "d", 5),
("b", "a", 5),
("c", "b", 6),
]
t = pp.TemporalGraph.from_edge_list(tedges)

# Create temporal plot and display inline
node_color = {"a": "red", ("a", 2): "darkred"}
edge_color = {("a", "b", 2): "blue"}
pp.plot(t, backend="tikz", kind="unfolded", node_size=12, node_color=node_color, edge_color=edge_color)
```
<img src="plot/unfolded_graph.svg" alt="Example TikZ Time-Unfolded Layout" width="650"/>

!!! tip "Customising Time-Unfolded Visualisations"
In the time-unfolded visualisation, you can still customise node and edge properties as described in the [Node and Edge Customisation](#node-and-edge-customisation) section.

## Customisation Options

Below is full list of supported keyword arguments for each backend and their descriptions.
Expand All @@ -445,6 +483,7 @@ Below is full list of supported keyword arguments for each backend and their des
| `layout_window_size` | ✔️ | ✔️ | ❌ | ❌ | Size of sliding window for temporal network layouts (int or tuple of int) |
| `delta` | ✔️ | ✔️ | ❌ | ❌ | Duration of timestep in milliseconds (ms) |
| `separator` | ✔️ | ✔️ | ✔️ | ✔️ | Separator for higher-order node labels |
| `orientation` | ✔️ | ✔️ | ❌ | ✔️ | Orientation of the time-unfolded network plot (`"up"`, `"down"`, `"left"`, or `"right"`) |
| **Nodes** | | | | | |
| `size` | ✔️ | ✔️ | ✔️ | ✔️ | Radius of nodes (uniform or per-node) |
| `color` | ✔️ | ✔️ | ✔️ | ✔️ | Node fill color |
Expand Down
174 changes: 173 additions & 1 deletion docs/reference/pathpyG/visualisations/plot/documentation_plots.ipynb

Large diffs are not rendered by default.

140 changes: 140 additions & 0 deletions docs/reference/pathpyG/visualisations/plot/unfolded_graph.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
506 changes: 506 additions & 0 deletions docs/reference/pathpyG/visualisations/plot/unfolded_graph_d3js.html

Large diffs are not rendered by default.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
136 changes: 136 additions & 0 deletions docs/reference/pathpyG/visualisations/plot/unfolded_graph_tikz.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/pathpyG/pathpyG.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ curvature = 0.25 # Curvature for curved edges between nodes
layout_window_size = [-1, -1] # Window size for layout algorithms that use temporal information. Default is [-1, -1] meaning that all timestamps in both directions is used. If an integer is given, this defines the number of time steps that are used to compute the layout at a specific time step. If tuple of two integers is given, this defines the number of time steps before and after the current time step that are used to compute the layout at a specific time step.
delta = 1000 # Time between frames in milliseconds
separator = "->" # Separator for higher-order node labels
orientation = "down" # Orientation of the time-unfolded plots. Options are "down", "up", "left", "right"

[visualisation.node]
color = [36, 74, 92] # Node color in RGB from the pathpyG logo
Expand Down
25 changes: 25 additions & 0 deletions src/pathpyG/visualisations/_d3js/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@

## Network Visualization with custom Images

With D3.js, you can easily use custom images for nodes by providing URLs or local paths.

```python
import torch
import pathpyG as pp
Expand Down Expand Up @@ -83,6 +85,29 @@
- **Jupyter**: Direct display in notebook cells
- **Web Apps**: Easy integration into existing websites

## Time-Unfolded Network

Below is an example of a time-unfolded network visualization using the D3.js backend.

```python
import pathpyG as pp

# Example temporal network data
tedges = [
("a", "d", 1),
("b", "c", 2),
("b", "c", 3),
("b", "a", 3),
("d", "b", 4),

]
t = pp.TemporalGraph.from_edge_list(tedges)

# Create temporal plot and display inline
pp.plot(t, kind="unfolded", show_labels=False)
```
<iframe src="../plot/unfolded_graph_d3js.html" width="650" height="520"></iframe>

## Templates
PathpyG uses HTML templates to generate D3.js visualizations located in the `templates` directory.
Templates define the overall structure and include placeholders for dynamic content.
Expand Down
36 changes: 23 additions & 13 deletions src/pathpyG/visualisations/_d3js/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- Both static and temporal network support
- Jupyter notebook integration with inline display
"""

from __future__ import annotations

import json
Expand All @@ -26,14 +27,16 @@
from pathpyG.visualisations.pathpy_plot import PathPyPlot
from pathpyG.visualisations.plot_backend import PlotBackend
from pathpyG.visualisations.temporal_network_plot import TemporalNetworkPlot
from pathpyG.visualisations.utils import rgb_to_hex, unit_str_to_float
from pathpyG.visualisations.unfolded_network_plot import TimeUnfoldedNetworkPlot
from pathpyG.visualisations.utils import in_jupyter_notebook, rgb_to_hex, unit_str_to_float

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

SUPPORTED_KINDS: dict[type, str] = {
NetworkPlot: "static",
TemporalNetworkPlot: "temporal",
TimeUnfoldedNetworkPlot: "unfolded",
}


Expand Down Expand Up @@ -62,7 +65,7 @@ class D3jsBackend(PlotBackend):

!!! info "Template Architecture"
Uses modular templates for extensibility:

- `styles.css`: Visual styling and responsive design
- `setup.js`: Environment detection and D3.js loading
- `network.js`: Core network visualization logic
Expand Down Expand Up @@ -105,12 +108,14 @@ def save(self, filename: str) -> None:

!!! tip "Deployment Ready"
Generated HTML files are standalone and can be:

- Opened directly in browsers
- Served from web servers
- Embedded in websites or documentation
- Shared without additional dependencies
"""
# Default to the CDN version of d3js since browsers may block local scripts
self.config["d3js_local"] = self.config.get("d3js_local", False)
with open(filename, "w+") as new:
new.write(self.to_html())

Expand All @@ -125,6 +130,8 @@ def show(self) -> None:
Uses pathpyG config to detect interactive environment
and choose appropriate display method automatically.
"""
# Default to local d3js in Jupyter notebooks for offline use
self.config["d3js_local"] = self.config.get("d3js_local", False or in_jupyter_notebook())
if config["environment"]["interactive"]:
from IPython.display import display_html, HTML # noqa I001

Expand All @@ -149,16 +156,18 @@ def _prepare_data(self) -> dict:

!!! note "Data Structure"
**Nodes**: Include uid, coordinates (xpos/ypos), and all attributes

**Edges**: Include uid, source/target references, and styling
"""
node_data = self.data["nodes"].copy()
node_data["uid"] = self.data["nodes"].index
node_data["uid"] = self.data["nodes"].index.map(lambda x: f"({x[0]},{x[1]})" if isinstance(x, tuple) else str(x))
node_data = node_data.rename(columns={"x": "xpos", "y": "ypos"})
if self._kind == "unfolded":
node_data["ypos"] = 1 - node_data["ypos"] # Invert y-axis for unfolded layout
edge_data = self.data["edges"].copy()
edge_data["uid"] = self.data["edges"].index.map(lambda x: f"{x[0]}-{x[1]}")
edge_data["source"] = edge_data.index.get_level_values("source")
edge_data["target"] = edge_data.index.get_level_values("target")
edge_data["source"] = edge_data.index.to_frame()["source"].map(lambda x: f"({x[0]},{x[1]})" if isinstance(x, tuple) else str(x))
edge_data["target"] = edge_data.index.to_frame()["target"].map(lambda x: f"({x[0]},{x[1]})" if isinstance(x, tuple) else str(x))
data_dict = {
"nodes": node_data.to_dict(orient="records"),
"edges": edge_data.to_dict(orient="records"),
Expand Down Expand Up @@ -235,9 +244,8 @@ def to_html(self) -> str:
os.path.normpath("_d3js/templates"),
)

# get d3js version
local = self.config.get("d3js_local", True)
if local:
# get d3js library path
if self.config.get("d3js_local", False):
d3js = os.path.join(template_dir, "d3.v7.min.js")
else:
d3js = "https://d3js.org/d3.v7.min.js"
Expand Down Expand Up @@ -305,9 +313,9 @@ def get_template(self, template_dir: str) -> str:

!!! info "Template Composition"
**Core Template** (`network.js`): Base network visualization logic

**Plot Templates**: Type-specific functionality:

- `static.js`: Force simulation and interaction for static networks
- `temporal.js`: Timeline controls and animation for temporal networks

Expand All @@ -319,7 +327,9 @@ def get_template(self, template_dir: str) -> str:
with open(os.path.join(template_dir, "network.js")) as template:
js_template += template.read()

with open(os.path.join(template_dir, f"{self._kind}.js")) as template:
with open(
os.path.join(template_dir, "static.js" if self._kind == "unfolded" else f"{self._kind}.js")
) as template:
js_template += template.read()

return js_template
28 changes: 28 additions & 0 deletions src/pathpyG/visualisations/_matplotlib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,32 @@
pp.plot(g, backend="matplotlib")
```
<img src="../plot/network.png" alt="Example Matplotlib Backend Output" width="550"/>

## Time-Unfolded Network

We also support time-unfolded static visualizations of temporal networks using the matplotlib backend.
The example uses the `node_opacity` parameter to highlight active nodes and edges at each time step.

```python
import pathpyG as pp

# Example temporal network data
tedges = [
("a", "b", 1),
("a", "b", 2),
("b", "a", 3),
("b", "c", 3),
("d", "c", 4),
("a", "b", 4),
("c", "b", 4),
]
t = pp.TemporalGraph.from_edge_list(tedges)

# Create temporal plot and display inline
node_opacity = {(node_id, time): 0.1 for node_id in t.nodes for time in range(t.data.time.max().item() + 2)}
node_opacity.update({(source_id, time): 1.0 for source_id, target_id, time in t.temporal_edges})
node_opacity.update({(target_id, time+1): 1.0 for source_id, target_id, time in t.temporal_edges})
pp.plot(t, backend="matplotlib", kind="unfolded", node_size=12, node_opacity=node_opacity)
```
<img src="../plot/unfolded_graph_matplotlib.png" alt="Example Matplotlib Backend Time-Unfolded Output" width="550"/>
"""
54 changes: 42 additions & 12 deletions src/pathpyG/visualisations/_matplotlib/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@
from pathpyG.visualisations.network_plot import NetworkPlot
from pathpyG.visualisations.pathpy_plot import PathPyPlot
from pathpyG.visualisations.plot_backend import PlotBackend
from pathpyG.visualisations.unfolded_network_plot import TimeUnfoldedNetworkPlot
from pathpyG.visualisations.utils import unit_str_to_float

logger = logging.getLogger("root")

SUPPORTED_KINDS = {
NetworkPlot: "static",
TimeUnfoldedNetworkPlot: "unfolded",
}


Expand Down Expand Up @@ -139,16 +141,44 @@ def to_fig(self) -> tuple[plt.Figure, plt.Axes]:

# add node labels
if self.show_labels:
for label in self.data["nodes"].index:
x, y = self.data["nodes"].loc[label, ["x", "y"]]
# Annotate the node label with text in the center of the node
ax.annotate(
label,
(x, y),
fontsize=0.4 * self.data["nodes"]["size"].mean(),
ha="center",
va="center",
)
if self._kind == "static":
for label in self.data["nodes"].index:
x, y = self.data["nodes"].loc[[label], ["x", "y"]].values.flatten()
# Annotate the node label with text in the center of the node
ax.annotate(
label,
(x, y),
fontsize=0.4 * self.data["nodes"]["size"].mean(),
ha="center",
va="center",
)
elif self._kind == "unfolded":
# add labels at the starting nodes only
min_time = self.data["nodes"]["start"].min()
offset = 0.005 * self.data["nodes"]["size"].mean()
sign = 1 if self.config["orientation"] in ["down", "left"] else -1
label_df = self.data["nodes"][self.data["nodes"]["start"] == min_time]
for label in label_df.index:
x, y = label_df.loc[[label], ["x", "y"]].values.flatten()
ax.annotate(
label[0],
(x, y + offset * sign) if self.config["orientation"] in ["down", "up"] else (x + offset * sign, y),
fontsize=0.5 * self.data["nodes"]["size"].mean(),
ha="center",
va="center",
)

# add timestamps at the border
times = self.data["nodes"]["start"].unique()
for time in times[:-1]: # skip last time as it would be outside the plot
x, y = self.data["nodes"].iloc[time:time+2, :][["x", "y"]].values.mean(axis=0)
ax.annotate(
str(time),
(x - offset, y) if self.config["orientation"] in ["down", "up"] else (x, y - offset),
fontsize=0.5 * self.data["nodes"]["size"].mean(),
ha="center",
va="center",
)

# set limits
ax.set_xlim(-1 * self.config["margin"], 1 + (1*self.config["margin"]))
Expand Down Expand Up @@ -308,7 +338,7 @@ def get_bezier_curve(
direction_P2_P1 = (P1 - P2) / distance_P2_P1
P0_offset_dist = shorten + source_node_size
P2_offset_dist = shorten + target_node_size + (head_length * self.data["edges"]["size"].values[:, np.newaxis])
if np.any(distance_P2_P1/2 < P2_offset_dist):
if (not self.config["curved"]) or np.any(distance_P2_P1/2 < P2_offset_dist):
logger.warning("Arrowhead length is too long for some edges. Please reduce the edge size. Using non-curved edges instead.")
direction_P0_P2 = vec / dist
P0 += direction_P0_P2 * P0_offset_dist
Expand All @@ -326,7 +356,7 @@ def get_bezier_curve(
]
return vertices, codes

def get_arrowhead(self, vertices, head_length=0.01, head_width=0.02):
def get_arrowhead(self, vertices, head_length, head_width=0.02):
"""Generate triangular arrowhead paths for directed edges.

Creates proportional arrowheads at curve endpoints using tangent vectors
Expand Down
28 changes: 28 additions & 0 deletions src/pathpyG/visualisations/_tikz/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,34 @@
```
<img src="../plot/tikz_init_advanced.svg" alt="Example TikZ Custom Properties" width="550"/>

## Time-Unfolded Network Example

You can also create time-unfolded visualizations of temporal networks using the TikZ backend with all customization options from the temporal animations.
With the `orientation` parameter, you can control the layout direction of the time-unfolded graph.

```python
import pathpyG as pp

# Example temporal network data
tedges = [
("a", "b", 1),
("a", "b", 2),
("b", "a", 3),
("a", "b", 4),
("c", "b", 4),
("c", "d", 5),
("b", "a", 5),
("c", "b", 6),
]
t = pp.TemporalGraph.from_edge_list(tedges)

# Create temporal plot and display inline
node_color = {"a": "red", ("a", 2): "darkred"}
edge_color = {("a", "b", 2): "blue"}
pp.plot(t, backend="tikz", kind="unfolded", node_size=12, node_color=node_color, edge_color=edge_color, orientation="right")
```
<img src="../plot/unfolded_graph_tikz.svg" alt="Example TikZ Custom Properties" width="550"/>

## Templates

PathpyG uses LaTeX templates to generate TikZ visualizations. Templates define standalone LaTeX documents with placeholders for dynamic content.
Expand Down
Loading
Loading