zk-graph-view 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026, Juan Diego García <reclusive_coeditor@proton.me>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,101 @@
1
+ Metadata-Version: 2.4
2
+ Name: zk-graph-view
3
+ Version: 0.1.0
4
+ Summary: Visualize zk graphs using python
5
+ Requires-Python: >=3.13
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE
8
+ Requires-Dist: pyvis
9
+ Requires-Dist: colorir
10
+ Requires-Dist: networkx
11
+ Requires-Dist: matplotlib
12
+ Requires-Dist: numpy
13
+ Requires-Dist: typing
14
+ Requires-Dist: scipy
15
+ Requires-Dist: halo
16
+ Dynamic: license-file
17
+
18
+ # zk-graph-view
19
+
20
+ Visualize your Zettelkasten graph from [`zk`](https://github.com/zk-org/zk) as an interactive HTML network.
21
+
22
+ `zk-graph-view` consumes the output of `zk graph --format=json` and renders it using `pyvis`.
23
+
24
+ [![Watch the demo](assets/demo.gif)](https://github.com/cyberSapoPerro/zk-graph-view/releases/tag/v0.1.0/demo.mp4)
25
+
26
+ ---
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ git clone https://github.com/cyberSapoPerro/zk-graph-view.git
32
+ cd zk-graph-view
33
+ pipx install -e .
34
+ ````
35
+
36
+ > Using `pipx` is recommended to isolate the CLI tool.
37
+
38
+ ---
39
+
40
+ ## Requirements
41
+
42
+ * [`zk`](https://github.com/zk-org/zk) installed and configured
43
+ * Python 3.13+
44
+
45
+ ---
46
+
47
+ ## Usage
48
+
49
+ Run the tool inside a valid `zk` notebook directory:
50
+
51
+ ```bash
52
+ zk-graph-view
53
+ ```
54
+
55
+ This will internally call:
56
+
57
+ ```bash
58
+ zk graph --format=json
59
+ ```
60
+
61
+ and generate an interactive HTML visualization.
62
+
63
+ ### Static Graph
64
+
65
+ For large graphs (1k+ notes) or when you need a static image, use the `--static` flag:
66
+
67
+ ```bash
68
+ zk-graph-view --static -o graph.png
69
+ ```
70
+
71
+ This renders a high-resolution PNG using matplotlib with the same tag-based coloring and node sizing. The layout is computed automatically (Kamada-Kawai for small graphs, spring layout for larger ones).
72
+
73
+ ![Static Graph Example](assets/static_graph_example.png)
74
+
75
+ ---
76
+
77
+ ## Configuration
78
+
79
+ You can integrate `zk-graph-view` as a `zk` alias for convenience.
80
+
81
+ Add this to your global config (`~/.config/zk/config.toml`) or a notebook-specific config:
82
+
83
+ ```toml
84
+ [alias]
85
+ vis = "zk-graph-view"
86
+ ```
87
+
88
+ Then run:
89
+
90
+ ```bash
91
+ zk vis
92
+ ```
93
+
94
+ ---
95
+
96
+ ## Notes
97
+
98
+ * The tool **must be executed inside a directory containing a `.zk/` folder**.
99
+ * Ensure your notes are properly tagged if you rely on tags for visualization or filtering.
100
+ * Interactive mode outputs HTML files; static mode outputs PNG images.
101
+ * Use `--help` to see all available options.
@@ -0,0 +1,84 @@
1
+ # zk-graph-view
2
+
3
+ Visualize your Zettelkasten graph from [`zk`](https://github.com/zk-org/zk) as an interactive HTML network.
4
+
5
+ `zk-graph-view` consumes the output of `zk graph --format=json` and renders it using `pyvis`.
6
+
7
+ [![Watch the demo](assets/demo.gif)](https://github.com/cyberSapoPerro/zk-graph-view/releases/tag/v0.1.0/demo.mp4)
8
+
9
+ ---
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ git clone https://github.com/cyberSapoPerro/zk-graph-view.git
15
+ cd zk-graph-view
16
+ pipx install -e .
17
+ ````
18
+
19
+ > Using `pipx` is recommended to isolate the CLI tool.
20
+
21
+ ---
22
+
23
+ ## Requirements
24
+
25
+ * [`zk`](https://github.com/zk-org/zk) installed and configured
26
+ * Python 3.13+
27
+
28
+ ---
29
+
30
+ ## Usage
31
+
32
+ Run the tool inside a valid `zk` notebook directory:
33
+
34
+ ```bash
35
+ zk-graph-view
36
+ ```
37
+
38
+ This will internally call:
39
+
40
+ ```bash
41
+ zk graph --format=json
42
+ ```
43
+
44
+ and generate an interactive HTML visualization.
45
+
46
+ ### Static Graph
47
+
48
+ For large graphs (1k+ notes) or when you need a static image, use the `--static` flag:
49
+
50
+ ```bash
51
+ zk-graph-view --static -o graph.png
52
+ ```
53
+
54
+ This renders a high-resolution PNG using matplotlib with the same tag-based coloring and node sizing. The layout is computed automatically (Kamada-Kawai for small graphs, spring layout for larger ones).
55
+
56
+ ![Static Graph Example](assets/static_graph_example.png)
57
+
58
+ ---
59
+
60
+ ## Configuration
61
+
62
+ You can integrate `zk-graph-view` as a `zk` alias for convenience.
63
+
64
+ Add this to your global config (`~/.config/zk/config.toml`) or a notebook-specific config:
65
+
66
+ ```toml
67
+ [alias]
68
+ vis = "zk-graph-view"
69
+ ```
70
+
71
+ Then run:
72
+
73
+ ```bash
74
+ zk vis
75
+ ```
76
+
77
+ ---
78
+
79
+ ## Notes
80
+
81
+ * The tool **must be executed inside a directory containing a `.zk/` folder**.
82
+ * Ensure your notes are properly tagged if you rely on tags for visualization or filtering.
83
+ * Interactive mode outputs HTML files; static mode outputs PNG images.
84
+ * Use `--help` to see all available options.
@@ -0,0 +1,22 @@
1
+ [project]
2
+ name = "zk-graph-view"
3
+ version = "0.1.0"
4
+ description = "Visualize zk graphs using python"
5
+ readme = "README.md"
6
+ requires-python = ">=3.13"
7
+ dependencies = [
8
+ "pyvis",
9
+ "colorir",
10
+ "networkx",
11
+ "matplotlib",
12
+ "numpy",
13
+ "typing",
14
+ "scipy",
15
+ "halo",
16
+ ]
17
+
18
+ [project.scripts]
19
+ zk-graph-view = "zk_graph_view.cli:main"
20
+
21
+ [tool.setuptools.packages.find]
22
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,92 @@
1
+ import os
2
+ import sys
3
+ import subprocess
4
+ import json
5
+ from typing import Any, Dict
6
+
7
+
8
+ def ensure_zk_dir_exist() -> None:
9
+ """Ensure a .zk directory exists in the current directory.
10
+
11
+ Exits with an error message if not found.
12
+ """
13
+ if not os.path.isdir(".zk"):
14
+ print(
15
+ "Error: .zk directory not found in the current directory", file=sys.stderr
16
+ )
17
+ sys.exit(1)
18
+
19
+
20
+ def get_json_from_cli() -> Dict[str, Any]:
21
+ """Get zk graph data by calling the zk CLI.
22
+
23
+ Runs ``zk graph --format=json`` and parses the output.
24
+
25
+ Returns:
26
+ Parsed JSON data from the zk graph command.
27
+ """
28
+ result = subprocess.run(
29
+ ["zk", "graph", "--format=json"],
30
+ capture_output=True,
31
+ text=True,
32
+ check=True,
33
+ )
34
+ data = json.loads(result.stdout)
35
+ return data
36
+
37
+
38
+ def get_json_from_input_path(input_path: str) -> Dict[str, Any]:
39
+ """Load zk graph data from a JSON file.
40
+
41
+ Args:
42
+ input_path: Path to the JSON file.
43
+
44
+ Returns:
45
+ Parsed JSON data from the file.
46
+ """
47
+ with open(input_path) as f:
48
+ data = json.load(f)
49
+ return data
50
+
51
+
52
+ def transform_json_data(json_data: Dict[str, Any]) -> Dict[str, Any]:
53
+ """Transform raw zk graph JSON into a structured format.
54
+
55
+ Normalizes note paths by removing .md suffixes, computes backlink counts,
56
+ and adds a singular ``tag`` key to each note derived from its ``tags`` list.
57
+
58
+ Args:
59
+ json_data: Raw graph data with ``notes`` and ``links`` keys.
60
+
61
+ Returns:
62
+ Dict with ``notes`` (each containing ``filenameStem``, ``title``, ``tag``,
63
+ ``backlinks``, and original keys) and ``links`` (with ``sourcePath`` and
64
+ ``targetPath`` cleaned of .md suffix).
65
+ """
66
+ backlinks: Dict[str, int] = {}
67
+ for note in json_data["notes"]:
68
+ note_id = note["filenameStem"]
69
+ backlinks[note_id] = sum(
70
+ 1
71
+ for link in json_data["links"]
72
+ if note_id == link["targetPath"].replace(".md", "")
73
+ )
74
+
75
+ notes = [
76
+ {
77
+ **note,
78
+ "tag": note["tags"][0] if note["tags"] else "untagged",
79
+ "backlinks": backlinks[note["filenameStem"]],
80
+ }
81
+ for note in json_data["notes"]
82
+ ]
83
+
84
+ links = [
85
+ {
86
+ "sourcePath": link["sourcePath"].replace(".md", ""),
87
+ "targetPath": link["targetPath"].replace(".md", ""),
88
+ }
89
+ for link in json_data["links"]
90
+ ]
91
+
92
+ return {"notes": notes, "links": links}
@@ -0,0 +1,88 @@
1
+ import argparse
2
+ import networkx as nx
3
+ from halo import Halo
4
+
5
+ from zk_graph_view.api import (
6
+ ensure_zk_dir_exist,
7
+ get_json_from_cli,
8
+ get_json_from_input_path,
9
+ transform_json_data,
10
+ )
11
+ from zk_graph_view.graph import make_interactive_graph, make_static_graph
12
+
13
+
14
+ def main():
15
+ ensure_zk_dir_exist()
16
+
17
+ parser = argparse.ArgumentParser(
18
+ description="Visualize your zk graph. Run inside a zk directory.",
19
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
20
+ )
21
+
22
+ parser.add_argument(
23
+ "-i",
24
+ "--input",
25
+ metavar="FILE",
26
+ help="Path to input JSON file (if omitted, uses `zk graph --format=json`)",
27
+ )
28
+
29
+ parser.add_argument(
30
+ "-c",
31
+ "--colors",
32
+ metavar="PALETTE",
33
+ default="carnival",
34
+ help="Color palette name (see colorir docs: https://colorir.readthedocs.io/en/latest/builtin_palettes.html)",
35
+ )
36
+
37
+ parser.add_argument(
38
+ "-o",
39
+ "--output",
40
+ metavar="FILE",
41
+ help="Path to output file (HTML for interactive, image for static)",
42
+ )
43
+
44
+ parser.add_argument(
45
+ "--static",
46
+ action="store_true",
47
+ help="Render a static graph instead of interactive",
48
+ )
49
+
50
+ args = parser.parse_args()
51
+
52
+ input_path = args.input
53
+ output_path = args.output
54
+ colors = args.colors
55
+
56
+ if input_path:
57
+ data = get_json_from_input_path(input_path)
58
+ else:
59
+ data = get_json_from_cli()
60
+
61
+ if not args.static:
62
+ with Halo(text="Generating interactive graph", spinner="dots"):
63
+ make_interactive_graph(data, palette=colors, output_path=output_path)
64
+ else:
65
+ data = transform_json_data(data)
66
+
67
+ G = nx.DiGraph()
68
+ for note in data["notes"]:
69
+ G.add_node(note["filenameStem"])
70
+ for link in data["links"]:
71
+ G.add_edge(link["sourcePath"], link["targetPath"])
72
+
73
+ if len(G) < 1500:
74
+ try:
75
+ layout = nx.kamada_kawai_layout(G)
76
+ except Exception:
77
+ layout = nx.spring_layout(G, seed=42)
78
+ else:
79
+ layout = nx.spring_layout(G, seed=42)
80
+
81
+ with Halo(text="Generating static graph", spinner="dots"):
82
+ make_static_graph(
83
+ data, palette=colors, layout=layout, output_path=output_path
84
+ )
85
+
86
+
87
+ if __name__ == "__main__":
88
+ main()
@@ -0,0 +1,292 @@
1
+ import tempfile
2
+ import webbrowser
3
+
4
+ import colorir as cl
5
+ import matplotlib.pyplot as plt
6
+ import networkx as nx
7
+ import numpy as np
8
+ from pyvis.network import Network
9
+
10
+ from .api import transform_json_data
11
+ from typing import Any, Dict, List, Optional
12
+
13
+
14
+ def build_color_map(unique_tags: List[str], palette: str) -> Dict[str, cl.Hex]:
15
+ """Build a color map from a list of tags and a palette name.
16
+
17
+ Args:
18
+ unique_tags: List of unique tag names.
19
+ palette: Name of a Colorir palette to use for coloring.
20
+
21
+ Returns:
22
+ Dict mapping each tag to a Hex color, including "untagged" as gray.
23
+ """
24
+ palette_obj = cl.StackPalette.load(
25
+ palette, palettes_dir=cl.config.USR_PALETTES_DIR
26
+ ).resize(len(unique_tags))
27
+ color_map = {tag: color for tag, color in zip(unique_tags, palette_obj)}
28
+ color_map["untagged"] = cl.Hex("#808080")
29
+ return color_map
30
+
31
+
32
+ def build_ordered_tags(unique_tags: List[str]) -> List[str]:
33
+ """Build ordered list of tags with untagged last."""
34
+ return [t for t in unique_tags if t != "untagged"] + ["untagged"]
35
+
36
+
37
+ def make_interactive_graph(
38
+ data: Dict[str, Any],
39
+ palette: str,
40
+ output_path: Optional[str] = None,
41
+ ) -> None:
42
+ """Render an interactive note graph using Pyvis.
43
+
44
+ Transforms raw zk graph data, then builds a directed graph with nodes
45
+ colored by tag and sized by backlink count.
46
+
47
+ Args:
48
+ data: Raw graph data with ``notes`` and ``links`` keys.
49
+ palette: Name of a Colorir palette to use for tag-based coloring.
50
+ output_path: If provided, saves the graph at this path; otherwise
51
+ uses a temporary file.
52
+
53
+ Returns:
54
+ None
55
+ """
56
+ data = transform_json_data(data)
57
+
58
+ net = Network(height="100vh", width="100%", directed=True, cdn_resources="remote")
59
+
60
+ unique_tags: List[str] = list({note["tag"] for note in data["notes"]})
61
+ color_map = build_color_map(unique_tags, palette)
62
+ ordered_tags = build_ordered_tags(unique_tags)
63
+
64
+ for note in data["notes"]:
65
+ net.add_node(
66
+ note["filenameStem"],
67
+ label=note["title"],
68
+ color=color_map[note["tag"]],
69
+ size=10 + 10 * np.log(note["backlinks"] + 1),
70
+ shape="dot",
71
+ )
72
+
73
+ for link in data["links"]:
74
+ net.add_edge(link["sourcePath"], link["targetPath"])
75
+
76
+ if output_path is None:
77
+ with tempfile.NamedTemporaryFile(suffix=".html") as f:
78
+ html_path = f.name
79
+ else:
80
+ html_path = output_path
81
+ net.write_html(html_path)
82
+
83
+ def build_legend_html(
84
+ color_map: Dict[str, cl.Hex], note_tags: Dict[str, str], ordered_tags: List[str]
85
+ ) -> str:
86
+ rows = ""
87
+ for tag in ordered_tags:
88
+ color = color_map[tag]
89
+ rows += f"""
90
+ <tr>
91
+ <td style="padding: 4px;">
92
+ <div id="legend-{tag}" style="
93
+ width: 15px;
94
+ height: 15px;
95
+ background-color: {color};
96
+ border-radius: 3px;
97
+ cursor: pointer;
98
+ opacity: 1;
99
+ transition: opacity 0.2s;
100
+ " onclick="toggleTag('{tag}')"></div>
101
+ </td>
102
+ <td style="padding: 4px; cursor: pointer;" onclick="toggleTag('{tag}')">{tag}</td>
103
+ </tr>
104
+ """
105
+
106
+ legend = f"""
107
+ <div style="
108
+ position: fixed;
109
+ top: 10px;
110
+ right: 10px;
111
+ background: white;
112
+ border: 1px solid #ccc;
113
+ border-radius: 8px;
114
+ padding: 12px;
115
+ z-index: 9999;
116
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
117
+ font-size: 13px;
118
+ user-select: none;
119
+ min-width: 140px;
120
+ ">
121
+ <b style="display: block; margin-bottom: 8px; color: #333;">Tags</b>
122
+ <span style="font-size: 11px; color: #888;">click to filter</span>
123
+ <table style="margin-top: 6px; border-collapse: collapse;">
124
+ {rows}
125
+ </table>
126
+ <div style="display: flex; gap: 6px; margin-top: 10px; padding-top: 10px; border-top: 1px solid #eee;">
127
+ <button onclick="hideAllTags()" style="
128
+ flex: 1;
129
+ padding: 5px 10px;
130
+ border: 1px solid #ddd;
131
+ border-radius: 4px;
132
+ background: #f5f5f5;
133
+ cursor: pointer;
134
+ font-size: 12px;
135
+ transition: background 0.15s;
136
+ "
137
+ onmouseover="this.style.background='#e8e8e8'"
138
+ onmouseout="this.style.background='#f5f5f5'">Hide All</button>
139
+ <button onclick="showAllTags()" style="
140
+ flex: 1;
141
+ padding: 5px 10px;
142
+ border: 1px solid #ddd;
143
+ border-radius: 4px;
144
+ background: #f5f5f5;
145
+ cursor: pointer;
146
+ font-size: 12px;
147
+ transition: background 0.15s;
148
+ "
149
+ onmouseover="this.style.background='#e8e8e8'"
150
+ onmouseout="this.style.background='#f5f5f5'">Show All</button>
151
+ </div>
152
+ </div>
153
+ <script>
154
+ var hiddenTags = {{}};
155
+ var nodeTags = {note_tags};
156
+
157
+ function toggleTag(tag) {{
158
+ hiddenTags[tag] = !hiddenTags[tag];
159
+ var el = document.getElementById('legend-' + tag);
160
+ el.style.opacity = hiddenTags[tag] ? '0.3' : '1';
161
+
162
+ for (var nodeId in nodeTags) {{
163
+ if (nodeTags[nodeId] === tag) {{
164
+ var node = network.body.data.nodes.get(nodeId);
165
+ if (node) {{
166
+ network.body.data.nodes.update({{
167
+ id: nodeId,
168
+ hidden: hiddenTags[tag]
169
+ }});
170
+ }}
171
+ }}
172
+ }}
173
+ }}
174
+
175
+ function showAllTags() {{
176
+ for (var tag in hiddenTags) {{
177
+ if (hiddenTags[tag]) {{
178
+ hiddenTags[tag] = false;
179
+ var el = document.getElementById('legend-' + tag);
180
+ if (el) el.style.opacity = '1';
181
+
182
+ for (var nodeId in nodeTags) {{
183
+ if (nodeTags[nodeId] === tag) {{
184
+ var node = network.body.data.nodes.get(nodeId);
185
+ if (node) {{
186
+ network.body.data.nodes.update({{
187
+ id: nodeId,
188
+ hidden: false
189
+ }});
190
+ }}
191
+ }}
192
+ }}
193
+ }}
194
+ }}
195
+ }}
196
+
197
+ function hideAllTags() {{
198
+ for (var tag in nodeTags) {{
199
+ if (!hiddenTags[nodeTags[tag]]) {{
200
+ hiddenTags[nodeTags[tag]] = true;
201
+ var el = document.getElementById('legend-' + nodeTags[tag]);
202
+ if (el) el.style.opacity = '0.3';
203
+
204
+ for (var nodeId in nodeTags) {{
205
+ if (nodeTags[nodeId] === nodeTags[tag]) {{
206
+ var node = network.body.data.nodes.get(nodeId);
207
+ if (node) {{
208
+ network.body.data.nodes.update({{
209
+ id: nodeId,
210
+ hidden: true
211
+ }});
212
+ }}
213
+ }}
214
+ }}
215
+ }}
216
+ }}
217
+ }}
218
+ </script>
219
+ """
220
+ return legend
221
+
222
+ note_tags = {note["filenameStem"]: note["tag"] for note in data["notes"]}
223
+ legend_html = build_legend_html(color_map, note_tags, ordered_tags)
224
+ with open(html_path, "r+") as f:
225
+ html = f.read()
226
+ html = html.replace("</body>", legend_html + "\n</body>")
227
+ f.seek(0)
228
+ f.write(html)
229
+ f.truncate()
230
+
231
+ webbrowser.open(f"file://{html_path}")
232
+
233
+
234
+ def make_static_graph(
235
+ data: Dict[str, Any],
236
+ palette: str,
237
+ layout: Dict[str, tuple[float, float]],
238
+ output_path: Optional[str] = None,
239
+ ) -> None:
240
+ """Render a static note graph using a pre-computed layout.
241
+
242
+ Uses the transformed data to build a directed graph and renders it
243
+ with matplotlib using the provided node positions.
244
+
245
+ Args:
246
+ data: Raw graph data with ``notes`` and ``links`` keys.
247
+ palette: Name of a Colorir palette to use for tag-based coloring.
248
+ layout: Dict mapping node IDs to (x, y) position tuples.
249
+ output_path: Path to save the graph image. Required.
250
+
251
+ Returns:
252
+ None. Saves the graph to ``output_path``.
253
+ """
254
+ if not output_path:
255
+ raise ValueError("output_path is required for static graph")
256
+
257
+ data = transform_json_data(data)
258
+
259
+ G = nx.DiGraph()
260
+
261
+ unique_tags: List[str] = list({note["tag"] for note in data["notes"]})
262
+ color_map = build_color_map(unique_tags, palette)
263
+
264
+ for note in data["notes"]:
265
+ G.add_node(
266
+ note["filenameStem"],
267
+ label=note["title"],
268
+ size=10 + 10 * np.log(note["backlinks"] + 1),
269
+ )
270
+
271
+ for link in data["links"]:
272
+ G.add_edge(link["sourcePath"], link["targetPath"])
273
+
274
+ node_colors = [str(color_map[note["tag"]]) for note in data["notes"]]
275
+ node_sizes = [
276
+ (10 + 10 * np.log(note["backlinks"] + 1)) * 20 for note in data["notes"]
277
+ ]
278
+
279
+ plt.figure(figsize=(14, 12))
280
+
281
+ nx.draw_networkx_nodes(G, layout, node_color=node_colors, node_size=node_sizes)
282
+ nx.draw_networkx_edges(
283
+ G, layout, arrows=True, arrowstyle="->", arrowsize=8, alpha=0.4
284
+ )
285
+
286
+ for tag, color in color_map.items():
287
+ plt.scatter([], [], c=str(color), label=tag)
288
+ plt.legend(title="Tags", loc="best")
289
+
290
+ plt.axis("off")
291
+
292
+ plt.savefig(output_path, bbox_inches="tight", dpi=300)
@@ -0,0 +1,101 @@
1
+ Metadata-Version: 2.4
2
+ Name: zk-graph-view
3
+ Version: 0.1.0
4
+ Summary: Visualize zk graphs using python
5
+ Requires-Python: >=3.13
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE
8
+ Requires-Dist: pyvis
9
+ Requires-Dist: colorir
10
+ Requires-Dist: networkx
11
+ Requires-Dist: matplotlib
12
+ Requires-Dist: numpy
13
+ Requires-Dist: typing
14
+ Requires-Dist: scipy
15
+ Requires-Dist: halo
16
+ Dynamic: license-file
17
+
18
+ # zk-graph-view
19
+
20
+ Visualize your Zettelkasten graph from [`zk`](https://github.com/zk-org/zk) as an interactive HTML network.
21
+
22
+ `zk-graph-view` consumes the output of `zk graph --format=json` and renders it using `pyvis`.
23
+
24
+ [![Watch the demo](assets/demo.gif)](https://github.com/cyberSapoPerro/zk-graph-view/releases/tag/v0.1.0/demo.mp4)
25
+
26
+ ---
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ git clone https://github.com/cyberSapoPerro/zk-graph-view.git
32
+ cd zk-graph-view
33
+ pipx install -e .
34
+ ````
35
+
36
+ > Using `pipx` is recommended to isolate the CLI tool.
37
+
38
+ ---
39
+
40
+ ## Requirements
41
+
42
+ * [`zk`](https://github.com/zk-org/zk) installed and configured
43
+ * Python 3.13+
44
+
45
+ ---
46
+
47
+ ## Usage
48
+
49
+ Run the tool inside a valid `zk` notebook directory:
50
+
51
+ ```bash
52
+ zk-graph-view
53
+ ```
54
+
55
+ This will internally call:
56
+
57
+ ```bash
58
+ zk graph --format=json
59
+ ```
60
+
61
+ and generate an interactive HTML visualization.
62
+
63
+ ### Static Graph
64
+
65
+ For large graphs (1k+ notes) or when you need a static image, use the `--static` flag:
66
+
67
+ ```bash
68
+ zk-graph-view --static -o graph.png
69
+ ```
70
+
71
+ This renders a high-resolution PNG using matplotlib with the same tag-based coloring and node sizing. The layout is computed automatically (Kamada-Kawai for small graphs, spring layout for larger ones).
72
+
73
+ ![Static Graph Example](assets/static_graph_example.png)
74
+
75
+ ---
76
+
77
+ ## Configuration
78
+
79
+ You can integrate `zk-graph-view` as a `zk` alias for convenience.
80
+
81
+ Add this to your global config (`~/.config/zk/config.toml`) or a notebook-specific config:
82
+
83
+ ```toml
84
+ [alias]
85
+ vis = "zk-graph-view"
86
+ ```
87
+
88
+ Then run:
89
+
90
+ ```bash
91
+ zk vis
92
+ ```
93
+
94
+ ---
95
+
96
+ ## Notes
97
+
98
+ * The tool **must be executed inside a directory containing a `.zk/` folder**.
99
+ * Ensure your notes are properly tagged if you rely on tags for visualization or filtering.
100
+ * Interactive mode outputs HTML files; static mode outputs PNG images.
101
+ * Use `--help` to see all available options.
@@ -0,0 +1,13 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/zk_graph_view/__init__.py
5
+ src/zk_graph_view/api.py
6
+ src/zk_graph_view/cli.py
7
+ src/zk_graph_view/graph.py
8
+ src/zk_graph_view.egg-info/PKG-INFO
9
+ src/zk_graph_view.egg-info/SOURCES.txt
10
+ src/zk_graph_view.egg-info/dependency_links.txt
11
+ src/zk_graph_view.egg-info/entry_points.txt
12
+ src/zk_graph_view.egg-info/requires.txt
13
+ src/zk_graph_view.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ zk-graph-view = zk_graph_view.cli:main
@@ -0,0 +1,8 @@
1
+ pyvis
2
+ colorir
3
+ networkx
4
+ matplotlib
5
+ numpy
6
+ typing
7
+ scipy
8
+ halo
@@ -0,0 +1 @@
1
+ zk_graph_view