Skip to content

Commit 2ad1cf8

Browse files
authored
Fix tikz/pdf and add latex to devcontainer (#287)
* fix typing issue in plot classes * add minimal latex installation to dev container * fix missing tikz-network.sty * fix tikz backend visualisations * remove latex packages * add ghostscript dependency
1 parent 5219be5 commit 2ad1cf8

10 files changed

Lines changed: 120 additions & 49 deletions

File tree

.devcontainer/Dockerfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ RUN apt-get -y install git
66
# For signed commits: https://code.visualstudio.com/remote/advancedcontainers/sharing-git-credentials#_sharing-gpg-keys
77
RUN apt install gnupg2 -y
88

9+
# Install dependencies for .svg support in tikz
10+
RUN apt update && apt install -y ghostscript
11+
912
# Install dependencies for manim
1013
RUN apt-get install -y build-essential python3-dev libcairo2-dev libpango1.0-dev ffmpeg
1114

.devcontainer/devcontainer.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,12 @@
3737
"all"
3838
],
3939
// Install pathpyG as editable python package
40-
"postCreateCommand": "pip install -e '.[dev,test,doc,vis]' && git config --global --add safe.directory /workspaces/pathpyG"
40+
"postCreateCommand": "pip install -e '.[dev,test,doc,vis]' && git config --global --add safe.directory /workspaces/pathpyG",
41+
"features": {
42+
"ghcr.io/prulloac/devcontainer-features/latex:1": {
43+
"scheme": "minimal",
44+
"mirror": "https://mirror.ctan.org/systems/texlive/tlnet/",
45+
"packages": "tikz-network,standalone,xcolor,xifthen,tools,ifmtarg,pgf,datatool,etoolbox,tracklang,amsmath,trimspaces,epstopdf-pkg,dvisvgm"
46+
}
47+
}
4148
}

src/pathpyG/core/temporal_graph.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,26 @@ def from_edge_list(edge_list, num_nodes: Optional[int] = None, device: Optional[
8989
)
9090

9191
@property
92-
def temporal_edges(self) -> Generator[Tuple[int, int, int], None, None]:
93-
"""Iterator that yields each edge as a tuple of source and destination node as well as the corresponding timestamp."""
92+
def temporal_edges(self) -> list:
93+
"""Return all temporal edges as a list of tuples (source, destination, timestamp).
94+
95+
Returns:
96+
list: A list of tuples representing temporal edges in the format (source, destination, timestamp).
97+
98+
Examples:
99+
Get the list of temporal edges:
100+
101+
>>> g = pp.TemporalGraph.from_edge_list([('a', 'b', 1), ('b', 'c', 2), ('c', 'a', 3)])
102+
>>> print(g.temporal_edges)
103+
[('a', 'b', 1), ('b', 'c', 2), ('c', 'a', 3)]
104+
105+
Iterate over temporal edges:
106+
>>> for edge in g.temporal_edges:
107+
>>> print(edge)
108+
('a', 'b', 1)
109+
('b', 'c', 2)
110+
('c', 'a', 3)
111+
"""
94112
return [(*self.mapping.to_ids(e), t.item()) for e, t in zip(self.data.edge_index.t(), self.data.time)]
95113

96114
def to(self, device: torch.device) -> TemporalGraph:

src/pathpyG/visualisations/_tikz/core.py

Lines changed: 75 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828

2929

3030
class TikzPlot(PathPyPlot):
31-
"""Base class for plotting d3js objects."""
31+
"""Base class for plotting tikz objects."""
3232

3333
def __init__(self, **kwargs: Any) -> None:
3434
"""Initialize plot class."""
@@ -52,20 +52,30 @@ def save(self, filename: str, **kwargs: Any) -> None:
5252
shutil.copy(temp_file, filename)
5353
# remove the temporal directory
5454
shutil.rmtree(temp_dir)
55-
55+
elif filename.endswith("svg"):
56+
# compile temporary svg
57+
temp_file, temp_dir = self.compile_svg()
58+
# Copy a file with new name
59+
shutil.copy(temp_file, filename)
60+
# remove the temporal directory
61+
shutil.rmtree(temp_dir)
5662
else:
5763
raise NotImplementedError
5864

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

6470
if config["environment"]["interactive"]:
65-
from IPython.display import IFrame, display
66-
67-
# open the file in the notebook
68-
display(IFrame(temp_file, width=600, height=300))
71+
from IPython.display import SVG, display
72+
73+
# open the file, read the content and display it
74+
# workaround because it is not possible to embed files in vs code
75+
# https://github.com/microsoft/vscode-jupyter/discussions/13769
76+
with open(temp_file, "r") as svg_file:
77+
svg = SVG(svg_file.read())
78+
display(svg)
6979
else:
7080
# open the file in the webbrowser
7181
webbrowser.open(r"file:///" + temp_file)
@@ -76,33 +86,44 @@ def show(self, **kwargs: Any) -> None:
7686
# remove the temporal directory
7787
shutil.rmtree(temp_dir)
7888

79-
def compile_pdf(self) -> tuple:
80-
"""Compile pdf from tex."""
81-
# basename
82-
basename = "default"
83-
# get current directory
84-
current_dir = os.getcwd()
85-
86-
# template directory
87-
tikz_dir = str(
88-
os.path.join(
89-
os.path.dirname(os.path.dirname(__file__)),
90-
os.path.normpath("templates"),
91-
"tikz-network.sty",
92-
)
93-
)
94-
95-
# get temporal directory
96-
temp_dir = tempfile.mkdtemp()
89+
def compile_svg(self) -> tuple:
90+
"""Compile svg from tex."""
91+
temp_dir, current_dir, basename = self.prepare_compile()
9792

98-
# copy tikz-network to temporal directory
99-
shutil.copy(tikz_dir, temp_dir)
93+
# latex compiler
94+
command = [
95+
"latexmk",
96+
"--interaction=nonstopmode",
97+
basename + ".tex",
98+
]
99+
try:
100+
subprocess.check_output(command, stderr=subprocess.STDOUT)
101+
except subprocess.CalledProcessError as e:
102+
logger.error("latexmk compiler failed with output:\n%s", e.output.decode())
103+
raise AttributeError from e
104+
105+
# dvisvgm command
106+
command = [
107+
"dvisvgm",
108+
basename + ".dvi",
109+
"-o",
110+
basename + ".svg",
111+
]
112+
try:
113+
subprocess.check_output(command, stderr=subprocess.STDOUT)
114+
except subprocess.CalledProcessError as e:
115+
logger.error("dvisvgm command failed with output:\n%s", e.output.decode())
116+
raise AttributeError from e
117+
finally:
118+
# change back to the current directory
119+
os.chdir(current_dir)
100120

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

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

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

116137
try:
117138
subprocess.check_output(command, stderr=subprocess.STDOUT)
118-
except Exception:
119-
# If compiler does not exist, try next in the list
120-
logger.error("No latexmk compiler found")
121-
raise AttributeError
139+
except subprocess.CalledProcessError as e:
140+
logger.error("latexmk compiler failed with output:\n%s", e.output.decode())
141+
raise AttributeError from e
122142
finally:
123143
# change back to the current directory
124144
os.chdir(current_dir)
125145

126146
# return the name of the folder and temp pdf file
127-
return (os.path.join(temp_dir, basename + ".pdf"), temp_dir)
147+
return os.path.join(temp_dir, basename + ".pdf"), temp_dir
148+
149+
def prepare_compile(self) -> tuple[str, str, str]:
150+
"""Prepare compilation of tex to pdf or svg by saving the tex file."""
151+
# basename
152+
basename = "default"
153+
# get current directory
154+
current_dir = os.getcwd()
155+
156+
# get temporal directory
157+
temp_dir = tempfile.mkdtemp()
158+
159+
# change to output dir
160+
os.chdir(temp_dir)
161+
162+
# save the tex file
163+
self.save(basename + ".tex")
164+
return temp_dir, current_dir, basename
128165

129166
def to_tex(self) -> str:
130167
"""Convert data to tex."""
@@ -144,8 +181,8 @@ def to_tex(self) -> str:
144181
# fill template with data
145182
tex = Template(tex_template).substitute(
146183
classoptions=self.config.get("latex_class_options", ""),
147-
width=self.config.get("width", "6cm"),
148-
height=self.config.get("height", "6cm"),
184+
width=self.config.get("width", "12cm"),
185+
height=self.config.get("height", "12cm"),
149186
tikz=data,
150187
)
151188

src/pathpyG/visualisations/_tikz/network_plots.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,10 @@ def __init__(self, data: dict, **kwargs: Any) -> None:
3232
super().__init__()
3333
self.data = data
3434
self.config = kwargs
35-
self.config["width"] = self.config.pop("width", 6)
36-
self.config["height"] = self.config.pop("height", 6)
3735
self.generate()
3836

3937
def generate(self) -> None:
40-
"""Clen up data."""
38+
"""Clean up data."""
4139
self._compute_node_data()
4240
self._compute_edge_data()
4341
self._update_layout()
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
\documentclass[$classoptions]{standalone}
22
\usepackage[dvipsnames]{xcolor}
33
\usepackage{tikz-network}
4+
\newcommand{\width}{$width}
5+
\newcommand{\height}{$height}
46
\begin{document}
57
\begin{tikzpicture}
68
\tikzset{every node}=[font=\sffamily\bfseries]
7-
\clip (0,0) rectangle ($width,$height);
9+
\clip (-0.5*\width,-0.5*\height) rectangle (0.5*\width,0.5*\height);
810
$tikz
911
\end{tikzpicture}
1012
\end{document}
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
\documentclass[$classoptions]{standalone}
22
\usepackage[dvipsnames]{xcolor}
33
\usepackage{tikz-network}
4+
\newcommand{\width}{$width}
5+
\newcommand{\height}{$height}
46
\begin{document}
57
\begin{tikzpicture}
68
\tikzset{every node}=[font=\sffamily\bfseries]
7-
\clip (0,0) rectangle ($width,$height);
9+
\clip (-0.5*\width,-0.5*\height) rectangle (0.5*\width,0.5*\height);
10+
\draw[draw,opacity=0] (-0.5*\width,-0.5*\height) rectangle (0.5*\width,0.5*\height);
811
$tikz
912
\end{tikzpicture}
1013
\end{document}
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
\documentclass[$classoptions]{standalone}
22
\usepackage[dvipsnames]{xcolor}
33
\usepackage{tikz-network}
4+
\newcommand{\width}{$width}
5+
\newcommand{\height}{$height}
46
\begin{document}
57
\begin{tikzpicture}
68
\tikzset{every node}=[font=\sffamily\bfseries]
7-
\clip (0,0) rectangle ($width,$height);
9+
\clip (-0.5*\width,-0.5*\height) rectangle (0.5*\width,0.5*\height);
810
$tikz
911
\end{tikzpicture}
1012
\end{document}

src/pathpyG/visualisations/network_plots.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -412,15 +412,15 @@ class TemporalNetworkPlot(NetworkPlot):
412412
"""Network plot class for a temporal network."""
413413

414414
_kind = "temporal"
415+
network: TemporalGraph
415416

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

420421
def _get_edge_data(self, edges: dict, attributes: set, attr: defaultdict, categories: set) -> None:
421422
"""Extract edge data from temporal network."""
422-
# TODO: Fix typing issue with temporal graphs
423-
for u, v, t in self.network.temporal_edges: # type: ignore
423+
for u, v, t in self.network.temporal_edges:
424424
uid = f"{u}-{v}-{t}"
425425
edges[uid] = {
426426
"uid": uid,

src/pathpyG/visualisations/plot.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
".html": "d3js",
2929
".tex": "tikz",
3030
".pdf": "tikz",
31+
".svg": "tikz",
3132
".png": "matplotlib",
3233
".mp4": "manim",
3334
".gif": "manim",

0 commit comments

Comments
 (0)