From f5eb26c6aadee1b63211a3530e2c0e842b96dc7f Mon Sep 17 00:00:00 2001 From: Moritz Lampert Date: Wed, 1 Oct 2025 09:09:49 +0000 Subject: [PATCH 1/6] fix typing issue in plot classes --- src/pathpyG/core/temporal_graph.py | 22 +++++++++++++++++++-- src/pathpyG/visualisations/network_plots.py | 4 ++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/pathpyG/core/temporal_graph.py b/src/pathpyG/core/temporal_graph.py index 788cb0f2b..59ce7f8d1 100644 --- a/src/pathpyG/core/temporal_graph.py +++ b/src/pathpyG/core/temporal_graph.py @@ -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: diff --git a/src/pathpyG/visualisations/network_plots.py b/src/pathpyG/visualisations/network_plots.py index ec2bf6bf4..cd89d8f18 100644 --- a/src/pathpyG/visualisations/network_plots.py +++ b/src/pathpyG/visualisations/network_plots.py @@ -412,6 +412,7 @@ 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.""" @@ -419,8 +420,7 @@ def __init__(self, network: TemporalGraph, **kwargs: Any) -> None: 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, From ba86d3bc0454e5c03f907ce3b8b5bd6e852b0542 Mon Sep 17 00:00:00 2001 From: Moritz Lampert Date: Wed, 1 Oct 2025 10:02:39 +0000 Subject: [PATCH 2/6] add minimal latex installation to dev container --- .devcontainer/devcontainer.json | 8 +++++++- .devcontainer/latex-packages.txt | 13 +++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 .devcontainer/latex-packages.txt diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index efb4286c2..4c480ab93 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -37,5 +37,11 @@ "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 && xargs -a latex-packages.txt tlmgr install", + "features": { + "ghcr.io/prulloac/devcontainer-features/latex:1": { + "scheme": "minimal", + "mirror": "https://mirror.ctan.org/systems/texlive/tlnet/" + } + } } diff --git a/.devcontainer/latex-packages.txt b/.devcontainer/latex-packages.txt new file mode 100644 index 000000000..6a5e61fb3 --- /dev/null +++ b/.devcontainer/latex-packages.txt @@ -0,0 +1,13 @@ +tikz-network +standalone +xcolor +xifthen +tools +ifmtarg +pgf +datatool +etoolbox +tracklang +amsmath +trimspaces +epstopdf-pkg \ No newline at end of file From 7d70e86584c6bb4ca7a85373bc2b95423fe55277 Mon Sep 17 00:00:00 2001 From: Moritz Lampert Date: Wed, 1 Oct 2025 10:08:12 +0000 Subject: [PATCH 3/6] fix missing tikz-network.sty --- src/pathpyG/visualisations/_tikz/core.py | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/src/pathpyG/visualisations/_tikz/core.py b/src/pathpyG/visualisations/_tikz/core.py index e2f2c8873..7995c9acd 100644 --- a/src/pathpyG/visualisations/_tikz/core.py +++ b/src/pathpyG/visualisations/_tikz/core.py @@ -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.""" @@ -83,21 +83,9 @@ def compile_pdf(self) -> tuple: # 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() - # copy tikz-network to temporal directory - shutil.copy(tikz_dir, temp_dir) - # change to output dir os.chdir(temp_dir) @@ -115,9 +103,8 @@ 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") + except subprocess.CalledProcessError as e: + logger.error("latexmk compiler failed with output:\n%s", e.output.decode()) raise AttributeError finally: # change back to the current directory From d79617727d5453a187b662a2567bee2891665712 Mon Sep 17 00:00:00 2001 From: Moritz Lampert Date: Wed, 1 Oct 2025 17:26:38 +0000 Subject: [PATCH 4/6] fix tikz backend visualisations --- .devcontainer/devcontainer.json | 5 +- src/pathpyG/visualisations/_tikz/core.py | 94 ++++++++++++++----- .../visualisations/_tikz/network_plots.py | 4 +- .../_tikz/templates/network.tex | 4 +- .../visualisations/_tikz/templates/static.tex | 5 +- .../_tikz/templates/temporal.tex | 4 +- src/pathpyG/visualisations/plot.py | 1 + 7 files changed, 87 insertions(+), 30 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 4c480ab93..50ea557fa 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -37,11 +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 && xargs -a latex-packages.txt tlmgr install", + "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/" + "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" } } } diff --git a/src/pathpyG/visualisations/_tikz/core.py b/src/pathpyG/visualisations/_tikz/core.py index 7995c9acd..73c7f7c90 100644 --- a/src/pathpyG/visualisations/_tikz/core.py +++ b/src/pathpyG/visualisations/_tikz/core.py @@ -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) @@ -76,21 +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() + def compile_svg(self) -> tuple: + """Compile svg from tex.""" + temp_dir, current_dir, basename = self.prepare_compile() - # get temporal directory - temp_dir = tempfile.mkdtemp() + # 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 = [ @@ -105,13 +138,30 @@ def compile_pdf(self) -> tuple: 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 + 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.""" @@ -131,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, ) diff --git a/src/pathpyG/visualisations/_tikz/network_plots.py b/src/pathpyG/visualisations/_tikz/network_plots.py index fb4d3ea65..1aed15856 100644 --- a/src/pathpyG/visualisations/_tikz/network_plots.py +++ b/src/pathpyG/visualisations/_tikz/network_plots.py @@ -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() diff --git a/src/pathpyG/visualisations/_tikz/templates/network.tex b/src/pathpyG/visualisations/_tikz/templates/network.tex index a6496a3ec..5ce11fd7c 100644 --- a/src/pathpyG/visualisations/_tikz/templates/network.tex +++ b/src/pathpyG/visualisations/_tikz/templates/network.tex @@ -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} \ No newline at end of file diff --git a/src/pathpyG/visualisations/_tikz/templates/static.tex b/src/pathpyG/visualisations/_tikz/templates/static.tex index a6496a3ec..e926806cf 100644 --- a/src/pathpyG/visualisations/_tikz/templates/static.tex +++ b/src/pathpyG/visualisations/_tikz/templates/static.tex @@ -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} \ No newline at end of file diff --git a/src/pathpyG/visualisations/_tikz/templates/temporal.tex b/src/pathpyG/visualisations/_tikz/templates/temporal.tex index a6496a3ec..5ce11fd7c 100644 --- a/src/pathpyG/visualisations/_tikz/templates/temporal.tex +++ b/src/pathpyG/visualisations/_tikz/templates/temporal.tex @@ -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} \ No newline at end of file diff --git a/src/pathpyG/visualisations/plot.py b/src/pathpyG/visualisations/plot.py index b2c04cfdc..7e85b20d5 100644 --- a/src/pathpyG/visualisations/plot.py +++ b/src/pathpyG/visualisations/plot.py @@ -28,6 +28,7 @@ ".html": "d3js", ".tex": "tikz", ".pdf": "tikz", + ".svg": "tikz", ".png": "matplotlib", ".mp4": "manim", ".gif": "manim", From 12973b06df589971cbb62bd15ccddf2ca8aa698c Mon Sep 17 00:00:00 2001 From: Moritz Lampert Date: Wed, 1 Oct 2025 17:32:45 +0000 Subject: [PATCH 5/6] remove latex packages --- .devcontainer/latex-packages.txt | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 .devcontainer/latex-packages.txt diff --git a/.devcontainer/latex-packages.txt b/.devcontainer/latex-packages.txt deleted file mode 100644 index 6a5e61fb3..000000000 --- a/.devcontainer/latex-packages.txt +++ /dev/null @@ -1,13 +0,0 @@ -tikz-network -standalone -xcolor -xifthen -tools -ifmtarg -pgf -datatool -etoolbox -tracklang -amsmath -trimspaces -epstopdf-pkg \ No newline at end of file From 1d135ad4392d3ad2278ea107e144614b9ee63bc5 Mon Sep 17 00:00:00 2001 From: Moritz Lampert Date: Wed, 1 Oct 2025 17:35:46 +0000 Subject: [PATCH 6/6] add ghostscript dependency --- .devcontainer/Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 57b726447..08f566d09 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -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