Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ RUN apt-get -y install git
# For signed commits: https://code.visualstudio.com/remote/advancedcontainers/sharing-git-credentials#_sharing-gpg-keys
RUN apt install gnupg2 -y

# Install dependencies for .svg support in tikz
RUN apt update && apt install -y ghostscript

# Install dependencies for manim
RUN apt-get install -y build-essential python3-dev libcairo2-dev libpango1.0-dev ffmpeg

Expand Down
9 changes: 8 additions & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,12 @@
"all"
],
// Install pathpyG as editable python package
"postCreateCommand": "pip install -e '.[dev,test,doc,vis]' && git config --global --add safe.directory /workspaces/pathpyG"
"postCreateCommand": "pip install -e '.[dev,test,doc,vis]' && git config --global --add safe.directory /workspaces/pathpyG",
"features": {
"ghcr.io/prulloac/devcontainer-features/latex:1": {
"scheme": "minimal",
"mirror": "https://mirror.ctan.org/systems/texlive/tlnet/",
"packages": "tikz-network,standalone,xcolor,xifthen,tools,ifmtarg,pgf,datatool,etoolbox,tracklang,amsmath,trimspaces,epstopdf-pkg,dvisvgm"
}
}
}
22 changes: 20 additions & 2 deletions src/pathpyG/core/temporal_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,26 @@ def from_edge_list(edge_list, num_nodes: Optional[int] = None, device: Optional[
)

@property
def temporal_edges(self) -> Generator[Tuple[int, int, int], None, None]:
"""Iterator that yields each edge as a tuple of source and destination node as well as the corresponding timestamp."""
def temporal_edges(self) -> list:
"""Return all temporal edges as a list of tuples (source, destination, timestamp).

Returns:
list: A list of tuples representing temporal edges in the format (source, destination, timestamp).

Examples:
Get the list of temporal edges:

>>> g = pp.TemporalGraph.from_edge_list([('a', 'b', 1), ('b', 'c', 2), ('c', 'a', 3)])
>>> print(g.temporal_edges)
[('a', 'b', 1), ('b', 'c', 2), ('c', 'a', 3)]

Iterate over temporal edges:
>>> for edge in g.temporal_edges:
>>> print(edge)
('a', 'b', 1)
('b', 'c', 2)
('c', 'a', 3)
"""
return [(*self.mapping.to_ids(e), t.item()) for e, t in zip(self.data.edge_index.t(), self.data.time)]

def to(self, device: torch.device) -> TemporalGraph:
Expand Down
113 changes: 75 additions & 38 deletions src/pathpyG/visualisations/_tikz/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@


class TikzPlot(PathPyPlot):
"""Base class for plotting d3js objects."""
"""Base class for plotting tikz objects."""

def __init__(self, **kwargs: Any) -> None:
"""Initialize plot class."""
Expand All @@ -52,20 +52,30 @@ def save(self, filename: str, **kwargs: Any) -> None:
shutil.copy(temp_file, filename)
# remove the temporal directory
shutil.rmtree(temp_dir)

elif filename.endswith("svg"):
# compile temporary svg
temp_file, temp_dir = self.compile_svg()
# Copy a file with new name
shutil.copy(temp_file, filename)
# remove the temporal directory
shutil.rmtree(temp_dir)
else:
raise NotImplementedError

def show(self, **kwargs: Any) -> None:
"""Show the plot on the device."""
# compile temporary pdf
temp_file, temp_dir = self.compile_pdf()
temp_file, temp_dir = self.compile_svg()

if config["environment"]["interactive"]:
from IPython.display import IFrame, display

# open the file in the notebook
display(IFrame(temp_file, width=600, height=300))
from IPython.display import SVG, display

# open the file, read the content and display it
# workaround because it is not possible to embed files in vs code
# https://github.com/microsoft/vscode-jupyter/discussions/13769
with open(temp_file, "r") as svg_file:
svg = SVG(svg_file.read())
display(svg)
else:
# open the file in the webbrowser
webbrowser.open(r"file:///" + temp_file)
Expand All @@ -76,33 +86,44 @@ def show(self, **kwargs: Any) -> None:
# remove the temporal directory
shutil.rmtree(temp_dir)

def compile_pdf(self) -> tuple:
"""Compile pdf from tex."""
# basename
basename = "default"
# get current directory
current_dir = os.getcwd()

# template directory
tikz_dir = str(
os.path.join(
os.path.dirname(os.path.dirname(__file__)),
os.path.normpath("templates"),
"tikz-network.sty",
)
)

# get temporal directory
temp_dir = tempfile.mkdtemp()
def compile_svg(self) -> tuple:
"""Compile svg from tex."""
temp_dir, current_dir, basename = self.prepare_compile()

# copy tikz-network to temporal directory
shutil.copy(tikz_dir, temp_dir)
# latex compiler
command = [
"latexmk",
"--interaction=nonstopmode",
basename + ".tex",
]
try:
subprocess.check_output(command, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e:
logger.error("latexmk compiler failed with output:\n%s", e.output.decode())
raise AttributeError from e

# dvisvgm command
command = [
"dvisvgm",
basename + ".dvi",
"-o",
basename + ".svg",
]
try:
subprocess.check_output(command, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e:
logger.error("dvisvgm command failed with output:\n%s", e.output.decode())
raise AttributeError from e
finally:
# change back to the current directory
os.chdir(current_dir)

# change to output dir
os.chdir(temp_dir)
# return the name of the folder and temp svg file
return os.path.join(temp_dir, basename + ".svg"), temp_dir

# save the tex file
self.save(basename + ".tex")
def compile_pdf(self) -> tuple:
"""Compile pdf from tex."""
temp_dir, current_dir, basename = self.prepare_compile()

# latex compiler
command = [
Expand All @@ -115,16 +136,32 @@ def compile_pdf(self) -> tuple:

try:
subprocess.check_output(command, stderr=subprocess.STDOUT)
except Exception:
# If compiler does not exist, try next in the list
logger.error("No latexmk compiler found")
raise AttributeError
except subprocess.CalledProcessError as e:
logger.error("latexmk compiler failed with output:\n%s", e.output.decode())
raise AttributeError from e
finally:
# change back to the current directory
os.chdir(current_dir)

# return the name of the folder and temp pdf file
return (os.path.join(temp_dir, basename + ".pdf"), temp_dir)
return os.path.join(temp_dir, basename + ".pdf"), temp_dir

def prepare_compile(self) -> tuple[str, str, str]:
"""Prepare compilation of tex to pdf or svg by saving the tex file."""
# basename
basename = "default"
# get current directory
current_dir = os.getcwd()

# get temporal directory
temp_dir = tempfile.mkdtemp()

# change to output dir
os.chdir(temp_dir)

# save the tex file
self.save(basename + ".tex")
return temp_dir, current_dir, basename

def to_tex(self) -> str:
"""Convert data to tex."""
Expand All @@ -144,8 +181,8 @@ def to_tex(self) -> str:
# fill template with data
tex = Template(tex_template).substitute(
classoptions=self.config.get("latex_class_options", ""),
width=self.config.get("width", "6cm"),
height=self.config.get("height", "6cm"),
width=self.config.get("width", "12cm"),
height=self.config.get("height", "12cm"),
tikz=data,
)

Expand Down
4 changes: 1 addition & 3 deletions src/pathpyG/visualisations/_tikz/network_plots.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,10 @@ def __init__(self, data: dict, **kwargs: Any) -> None:
super().__init__()
self.data = data
self.config = kwargs
self.config["width"] = self.config.pop("width", 6)
self.config["height"] = self.config.pop("height", 6)
self.generate()

def generate(self) -> None:
"""Clen up data."""
"""Clean up data."""
self._compute_node_data()
self._compute_edge_data()
self._update_layout()
Expand Down
4 changes: 3 additions & 1 deletion src/pathpyG/visualisations/_tikz/templates/network.tex
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
\documentclass[$classoptions]{standalone}
\usepackage[dvipsnames]{xcolor}
\usepackage{tikz-network}
\newcommand{\width}{$width}
\newcommand{\height}{$height}
\begin{document}
\begin{tikzpicture}
\tikzset{every node}=[font=\sffamily\bfseries]
\clip (0,0) rectangle ($width,$height);
\clip (-0.5*\width,-0.5*\height) rectangle (0.5*\width,0.5*\height);
$tikz
\end{tikzpicture}
\end{document}
5 changes: 4 additions & 1 deletion src/pathpyG/visualisations/_tikz/templates/static.tex
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
\documentclass[$classoptions]{standalone}
\usepackage[dvipsnames]{xcolor}
\usepackage{tikz-network}
\newcommand{\width}{$width}
\newcommand{\height}{$height}
\begin{document}
\begin{tikzpicture}
\tikzset{every node}=[font=\sffamily\bfseries]
\clip (0,0) rectangle ($width,$height);
\clip (-0.5*\width,-0.5*\height) rectangle (0.5*\width,0.5*\height);
\draw[draw,opacity=0] (-0.5*\width,-0.5*\height) rectangle (0.5*\width,0.5*\height);
$tikz
\end{tikzpicture}
\end{document}
4 changes: 3 additions & 1 deletion src/pathpyG/visualisations/_tikz/templates/temporal.tex
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
\documentclass[$classoptions]{standalone}
\usepackage[dvipsnames]{xcolor}
\usepackage{tikz-network}
\newcommand{\width}{$width}
\newcommand{\height}{$height}
\begin{document}
\begin{tikzpicture}
\tikzset{every node}=[font=\sffamily\bfseries]
\clip (0,0) rectangle ($width,$height);
\clip (-0.5*\width,-0.5*\height) rectangle (0.5*\width,0.5*\height);
$tikz
\end{tikzpicture}
\end{document}
4 changes: 2 additions & 2 deletions src/pathpyG/visualisations/network_plots.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,15 +412,15 @@ class TemporalNetworkPlot(NetworkPlot):
"""Network plot class for a temporal network."""

_kind = "temporal"
network: TemporalGraph

def __init__(self, network: TemporalGraph, **kwargs: Any) -> None:
"""Initialize network plot class."""
super().__init__(network, **kwargs)

def _get_edge_data(self, edges: dict, attributes: set, attr: defaultdict, categories: set) -> None:
"""Extract edge data from temporal network."""
# TODO: Fix typing issue with temporal graphs
for u, v, t in self.network.temporal_edges: # type: ignore
for u, v, t in self.network.temporal_edges:
uid = f"{u}-{v}-{t}"
edges[uid] = {
"uid": uid,
Expand Down
1 change: 1 addition & 0 deletions src/pathpyG/visualisations/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
".html": "d3js",
".tex": "tikz",
".pdf": "tikz",
".svg": "tikz",
".png": "matplotlib",
".mp4": "manim",
".gif": "manim",
Expand Down
Loading