Skip to content

Commit 49ff747

Browse files
authored
Implement time unfolded visualization (#298)
* refactor visualisations and fix/speed up matplotlib backend * update matplotlib and tikz * finalise static d3js * update configs * add automatic d3js layouting and margin config param * optimise temporal_edges * update d3js temporal graphs * update curved edges * efficiency improvements * fix issues with higher order and curved edges * update manim * fix indexing * update layout * add temporal network layouting * add networkx as dependency * update manim backend * fix window and batch functions for temporal graphs * minor fixes * start documentation update * update docs and minor fixes * fix documentation with underscore * update visualisation notebooks * documentation improvements * finish visualisation code reference * update plot dev tutorial * fix existing tests * update setup yaml * fix tex setup * update tex action * fix * fix tex live setup * set up optimized ffmpeg download * improve apt installation * add optional python deps * add tests and minor fixes * update latex installation * fix ffmpeg installation * - * fix ffmpeg * backend tests * add time-unfolded graph matplotlib * add unfolded visualisation to tikz * add unit-tests * add unfolded graph to d3js backend * update docs * fix temporal graph RGB assignment * fix higher-order RGB assignment * fix tikz node border opacity * fix unit-test fail
1 parent 09466c6 commit 49ff747

22 files changed

Lines changed: 1745 additions & 79 deletions

docs/reference/pathpyG/visualisations/index.md

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,12 @@ The default backend is `d3.js`, which is suitable for both static and temporal n
6262
We currently support a total of four plotting backends, each with different capabilities making them suitable for different use cases.
6363
The table below provides an overview of the supported backends and their available file formats:
6464

65-
| Backend | Static Networks | Temporal Networks | Available File Formats|
66-
|---------------|------------|-------------|--------------|
67-
| **d3.js** | ✔️ | ✔️ | `html` |
68-
| **manim** || ✔️ | `mp4`, `gif` |
69-
| **matplotlib**| ✔️ || `png`, `jpg` |
70-
| **tikz** | ✔️ || `svg`, `pdf`, `tex`|
65+
| Backend | Static Networks | Temporal Networks | Time-Unfolded Networks | Available File Formats|
66+
|---------------|------------|-------------|--------------|-------------|
67+
| **d3.js** | ✔️ | ✔️ | ✔️ | `html` |
68+
| **manim** || ✔️ | | `mp4`, `gif` |
69+
| **matplotlib**| ✔️ || ✔️ | `png`, `jpg` |
70+
| **tikz** | ✔️ || ✔️ | `svg`, `pdf`, `tex`|
7171

7272
#### Details
7373

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

430+
### Time-Unfolded Networks
431+
432+
For temporal networks, you can use the time-unfolded visualisation to show a static representation of the temporal network.
433+
In this representation, each node is duplicated for each timestep, and edges are drawn between nodes at different timesteps to represent temporal interactions.
434+
You can enable this visualisation by setting the "kind" argument to `"unfolded"` in the `pp.plot()` function call.
435+
This visualisation is supported by all backends that support static networks, i.e. D3.js, Matplotlib, and TikZ.
436+
437+
!!! example "Time-Unfolded Visualisation of Temporal Networks"
438+
439+
In the example below, we create a time-unfolded visualisation of a temporal network using the `tikz` backend.
440+
```python
441+
import pathpyG as pp
442+
443+
# Example temporal network data
444+
tedges = [
445+
("a", "b", 1),
446+
("a", "b", 2),
447+
("b", "a", 3),
448+
("b", "c", 3),
449+
("d", "c", 4),
450+
("a", "b", 4),
451+
("c", "b", 4),
452+
("c", "d", 5),
453+
("b", "a", 5),
454+
("c", "b", 6),
455+
]
456+
t = pp.TemporalGraph.from_edge_list(tedges)
457+
458+
# Create temporal plot and display inline
459+
node_color = {"a": "red", ("a", 2): "darkred"}
460+
edge_color = {("a", "b", 2): "blue"}
461+
pp.plot(t, backend="tikz", kind="unfolded", node_size=12, node_color=node_color, edge_color=edge_color)
462+
```
463+
<img src="plot/unfolded_graph.svg" alt="Example TikZ Time-Unfolded Layout" width="650"/>
464+
465+
!!! tip "Customising Time-Unfolded Visualisations"
466+
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.
467+
430468
## Customisation Options
431469

432470
Below is full list of supported keyword arguments for each backend and their descriptions.
@@ -445,6 +483,7 @@ Below is full list of supported keyword arguments for each backend and their des
445483
| `layout_window_size` | ✔️ | ✔️ ||| Size of sliding window for temporal network layouts (int or tuple of int) |
446484
| `delta` | ✔️ | ✔️ ||| Duration of timestep in milliseconds (ms) |
447485
| `separator` | ✔️ | ✔️ | ✔️ | ✔️ | Separator for higher-order node labels |
486+
| `orientation` | ✔️ | ✔️ || ✔️ | Orientation of the time-unfolded network plot (`"up"`, `"down"`, `"left"`, or `"right"`) |
448487
| **Nodes** | | | | | |
449488
| `size` | ✔️ | ✔️ | ✔️ | ✔️ | Radius of nodes (uniform or per-node) |
450489
| `color` | ✔️ | ✔️ | ✔️ | ✔️ | Node fill color |

docs/reference/pathpyG/visualisations/plot/documentation_plots.ipynb

Lines changed: 173 additions & 1 deletion
Large diffs are not rendered by default.

docs/reference/pathpyG/visualisations/plot/unfolded_graph.svg

Lines changed: 140 additions & 0 deletions
Loading

docs/reference/pathpyG/visualisations/plot/unfolded_graph_d3js.html

Lines changed: 506 additions & 0 deletions
Large diffs are not rendered by default.
39.3 KB
Loading

docs/reference/pathpyG/visualisations/plot/unfolded_graph_tikz.svg

Lines changed: 136 additions & 0 deletions
Loading

src/pathpyG/pathpyG.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ curvature = 0.25 # Curvature for curved edges between nodes
3232
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.
3333
delta = 1000 # Time between frames in milliseconds
3434
separator = "->" # Separator for higher-order node labels
35+
orientation = "down" # Orientation of the time-unfolded plots. Options are "down", "up", "left", "right"
3536

3637
[visualisation.node]
3738
color = [36, 74, 92] # Node color in RGB from the pathpyG logo

src/pathpyG/visualisations/_d3js/__init__.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@
5151
5252
## Network Visualization with custom Images
5353
54+
With D3.js, you can easily use custom images for nodes by providing URLs or local paths.
55+
5456
```python
5557
import torch
5658
import pathpyG as pp
@@ -83,6 +85,29 @@
8385
- **Jupyter**: Direct display in notebook cells
8486
- **Web Apps**: Easy integration into existing websites
8587
88+
## Time-Unfolded Network
89+
90+
Below is an example of a time-unfolded network visualization using the D3.js backend.
91+
92+
```python
93+
import pathpyG as pp
94+
95+
# Example temporal network data
96+
tedges = [
97+
("a", "d", 1),
98+
("b", "c", 2),
99+
("b", "c", 3),
100+
("b", "a", 3),
101+
("d", "b", 4),
102+
103+
]
104+
t = pp.TemporalGraph.from_edge_list(tedges)
105+
106+
# Create temporal plot and display inline
107+
pp.plot(t, kind="unfolded", show_labels=False)
108+
```
109+
<iframe src="../plot/unfolded_graph_d3js.html" width="650" height="520"></iframe>
110+
86111
## Templates
87112
PathpyG uses HTML templates to generate D3.js visualizations located in the `templates` directory.
88113
Templates define the overall structure and include placeholders for dynamic content.

src/pathpyG/visualisations/_d3js/backend.py

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
- Both static and temporal network support
1111
- Jupyter notebook integration with inline display
1212
"""
13+
1314
from __future__ import annotations
1415

1516
import json
@@ -26,14 +27,16 @@
2627
from pathpyG.visualisations.pathpy_plot import PathPyPlot
2728
from pathpyG.visualisations.plot_backend import PlotBackend
2829
from pathpyG.visualisations.temporal_network_plot import TemporalNetworkPlot
29-
from pathpyG.visualisations.utils import rgb_to_hex, unit_str_to_float
30+
from pathpyG.visualisations.unfolded_network_plot import TimeUnfoldedNetworkPlot
31+
from pathpyG.visualisations.utils import in_jupyter_notebook, rgb_to_hex, unit_str_to_float
3032

3133
# create logger
3234
logger = logging.getLogger("root")
3335

3436
SUPPORTED_KINDS: dict[type, str] = {
3537
NetworkPlot: "static",
3638
TemporalNetworkPlot: "temporal",
39+
TimeUnfoldedNetworkPlot: "unfolded",
3740
}
3841

3942

@@ -62,7 +65,7 @@ class D3jsBackend(PlotBackend):
6265
6366
!!! info "Template Architecture"
6467
Uses modular templates for extensibility:
65-
68+
6669
- `styles.css`: Visual styling and responsive design
6770
- `setup.js`: Environment detection and D3.js loading
6871
- `network.js`: Core network visualization logic
@@ -105,12 +108,14 @@ def save(self, filename: str) -> None:
105108
106109
!!! tip "Deployment Ready"
107110
Generated HTML files are standalone and can be:
108-
111+
109112
- Opened directly in browsers
110113
- Served from web servers
111114
- Embedded in websites or documentation
112115
- Shared without additional dependencies
113116
"""
117+
# Default to the CDN version of d3js since browsers may block local scripts
118+
self.config["d3js_local"] = self.config.get("d3js_local", False)
114119
with open(filename, "w+") as new:
115120
new.write(self.to_html())
116121

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

@@ -149,16 +156,18 @@ def _prepare_data(self) -> dict:
149156
150157
!!! note "Data Structure"
151158
**Nodes**: Include uid, coordinates (xpos/ypos), and all attributes
152-
159+
153160
**Edges**: Include uid, source/target references, and styling
154161
"""
155162
node_data = self.data["nodes"].copy()
156-
node_data["uid"] = self.data["nodes"].index
163+
node_data["uid"] = self.data["nodes"].index.map(lambda x: f"({x[0]},{x[1]})" if isinstance(x, tuple) else str(x))
157164
node_data = node_data.rename(columns={"x": "xpos", "y": "ypos"})
165+
if self._kind == "unfolded":
166+
node_data["ypos"] = 1 - node_data["ypos"] # Invert y-axis for unfolded layout
158167
edge_data = self.data["edges"].copy()
159168
edge_data["uid"] = self.data["edges"].index.map(lambda x: f"{x[0]}-{x[1]}")
160-
edge_data["source"] = edge_data.index.get_level_values("source")
161-
edge_data["target"] = edge_data.index.get_level_values("target")
169+
edge_data["source"] = edge_data.index.to_frame()["source"].map(lambda x: f"({x[0]},{x[1]})" if isinstance(x, tuple) else str(x))
170+
edge_data["target"] = edge_data.index.to_frame()["target"].map(lambda x: f"({x[0]},{x[1]})" if isinstance(x, tuple) else str(x))
162171
data_dict = {
163172
"nodes": node_data.to_dict(orient="records"),
164173
"edges": edge_data.to_dict(orient="records"),
@@ -235,9 +244,8 @@ def to_html(self) -> str:
235244
os.path.normpath("_d3js/templates"),
236245
)
237246

238-
# get d3js version
239-
local = self.config.get("d3js_local", True)
240-
if local:
247+
# get d3js library path
248+
if self.config.get("d3js_local", False):
241249
d3js = os.path.join(template_dir, "d3.v7.min.js")
242250
else:
243251
d3js = "https://d3js.org/d3.v7.min.js"
@@ -305,9 +313,9 @@ def get_template(self, template_dir: str) -> str:
305313
306314
!!! info "Template Composition"
307315
**Core Template** (`network.js`): Base network visualization logic
308-
316+
309317
**Plot Templates**: Type-specific functionality:
310-
318+
311319
- `static.js`: Force simulation and interaction for static networks
312320
- `temporal.js`: Timeline controls and animation for temporal networks
313321
@@ -319,7 +327,9 @@ def get_template(self, template_dir: str) -> str:
319327
with open(os.path.join(template_dir, "network.js")) as template:
320328
js_template += template.read()
321329

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

325335
return js_template

src/pathpyG/visualisations/_matplotlib/__init__.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,32 @@
1717
pp.plot(g, backend="matplotlib")
1818
```
1919
<img src="../plot/network.png" alt="Example Matplotlib Backend Output" width="550"/>
20+
21+
## Time-Unfolded Network
22+
23+
We also support time-unfolded static visualizations of temporal networks using the matplotlib backend.
24+
The example uses the `node_opacity` parameter to highlight active nodes and edges at each time step.
25+
26+
```python
27+
import pathpyG as pp
28+
29+
# Example temporal network data
30+
tedges = [
31+
("a", "b", 1),
32+
("a", "b", 2),
33+
("b", "a", 3),
34+
("b", "c", 3),
35+
("d", "c", 4),
36+
("a", "b", 4),
37+
("c", "b", 4),
38+
]
39+
t = pp.TemporalGraph.from_edge_list(tedges)
40+
41+
# Create temporal plot and display inline
42+
node_opacity = {(node_id, time): 0.1 for node_id in t.nodes for time in range(t.data.time.max().item() + 2)}
43+
node_opacity.update({(source_id, time): 1.0 for source_id, target_id, time in t.temporal_edges})
44+
node_opacity.update({(target_id, time+1): 1.0 for source_id, target_id, time in t.temporal_edges})
45+
pp.plot(t, backend="matplotlib", kind="unfolded", node_size=12, node_opacity=node_opacity)
46+
```
47+
<img src="../plot/unfolded_graph_matplotlib.png" alt="Example Matplotlib Backend Time-Unfolded Output" width="550"/>
2048
"""

0 commit comments

Comments
 (0)