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.
- zk_graph_view-0.1.0/LICENSE +21 -0
- zk_graph_view-0.1.0/PKG-INFO +101 -0
- zk_graph_view-0.1.0/README.md +84 -0
- zk_graph_view-0.1.0/pyproject.toml +22 -0
- zk_graph_view-0.1.0/setup.cfg +4 -0
- zk_graph_view-0.1.0/src/zk_graph_view/__init__.py +0 -0
- zk_graph_view-0.1.0/src/zk_graph_view/api.py +92 -0
- zk_graph_view-0.1.0/src/zk_graph_view/cli.py +88 -0
- zk_graph_view-0.1.0/src/zk_graph_view/graph.py +292 -0
- zk_graph_view-0.1.0/src/zk_graph_view.egg-info/PKG-INFO +101 -0
- zk_graph_view-0.1.0/src/zk_graph_view.egg-info/SOURCES.txt +13 -0
- zk_graph_view-0.1.0/src/zk_graph_view.egg-info/dependency_links.txt +1 -0
- zk_graph_view-0.1.0/src/zk_graph_view.egg-info/entry_points.txt +2 -0
- zk_graph_view-0.1.0/src/zk_graph_view.egg-info/requires.txt +8 -0
- zk_graph_view-0.1.0/src/zk_graph_view.egg-info/top_level.txt +1 -0
|
@@ -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
|
+
[](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
|
+

|
|
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
|
+
[](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
|
+

|
|
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"]
|
|
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
|
+
[](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
|
+

|
|
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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
zk_graph_view
|