Skip to content

Commit

Permalink
ENH: Add option to use Graphviz to draw the network (#13)
Browse files Browse the repository at this point in the history
* ENH: Add option to use Graphviz to draw the network

Adds a `--use-graphviz` flag to create and render the network using
Graphviz instead of plotly. Requires that the Graphviz application be
installed.

On Mac, Graphviz can be installed from homebrew with:

    brew install graphviz

Updatesthe macro to use Graphviz
  • Loading branch information
achabotl authored Aug 7, 2019
1 parent 5c1e78b commit 223d88a
Show file tree
Hide file tree
Showing 6 changed files with 350 additions and 144 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ Then install zkviz with:
pip install zkviz
```

If [Graphviz](https://graphviz.org/download/) is installed on your computer,
zkviz can use it to draw the network. It is not a Python package so it needs to
be installed independently. If you're on a Mac and have
[Homebrew](https://brew.sh) installed, you can install Graphviz from a Terminal
with:

```sh
brew install graphviz
```

## Usage

To execute zkviz from the Terminal, you either need to add the zkviz
Expand Down Expand Up @@ -58,6 +68,12 @@ You can also pass a list of files to zkviz:
~/envs/zkviz/bin/zkviz "~/Notes/201906021303 the state of affairs.md" "~/Notes/201901021232 Journey to the center of the earth.md"
```

To use Graphviz to generate the visualization, add the `--use-graphviz` option:

```sh
~/envs/zkviz/bin/zkviz --notes-dir ~/Notes --use-graphviz
```

## Using zkviz with Keyboard Maestro

The `keyboard-maestro` folder includes a [Keyboard Maestro](https://www.keyboardmaestro.com)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@
<key>StyledText</key>
<data>
cnRmZAAAAAADAAAAAgAAAAcAAABU
WFQucnRmAQAAAC6sAwAAKwAAAAEA
AACkAwAAe1xydGYxXGFuc2lcYW5z
WFQucnRmAQAAAC4kBAAAKwAAAAEA
AAAcBAAAe1xydGYxXGFuc2lcYW5z
aWNwZzEyNTJcY29jb2FydGYxNjcx
XGNvY29hc3VicnRmNTAwCntcZm9u
XGNvY29hc3VicnRmNjAwCntcZm9u
dHRibFxmMFxmbmlsXGZjaGFyc2V0
MCBIZWx2ZXRpY2FOZXVlO30Ke1xj
b2xvcnRibDtccmVkMjU1XGdyZWVu
Expand All @@ -43,29 +43,35 @@
bGVjdGUgaW4gVGhlIEFyY2hpdmUg
YXBwIFsyXS5cClwKRm9yIHRoaXMg
bWFjcm8gdG8gd29yayBwcm9wZXJs
eSwgcGxlYXNlIHNldCB0aGUgbmV4
dCB0d28gdmFyaWFibGVzOlwKXAot
IFpLVklaX0FSQ0hJVkVfUEFUSCBp
cyB0aGUgcGF0aCB0byB3aGVyZSB5
b3VyIGFyY2hpdmUgbm90ZXMgYXJl
IHN0b3JlLiBGb3IgZXhhbXBsZSwg
fi9Ecm9wYm94L05vdGVzXAotIFpL
VklaX1BBVEggaXMgdGhlIGFic29s
dXRlIHBhdGggdG8gdGhlIHprdml6
IGV4ZWN1dGFibGUuIFBsZWFzZSBz
ZWUgWzFdIGZvciBpbnN0cnVjdGlv
bnMgb2YgaG93IHRvIGluc3RhbGwg
aXQuIFRoZSBwYXRoIGJlbG93IHVz
ZXMgdGhlIG9uZSB5b3UnZCBoYXZl
IGlmIHlvdSBmb2xsb3dlZCB0aGUg
aW5zdHJ1Y3Rpb25zIGRpcmVjdGx5
LlwKXApbMV06IGh0dHBzOi8vZ2l0
aHViLmNvbS9aZXR0ZWxrYXN0ZW4t
TWV0aG9kL3prdml6XApbMl06IGh0
dHBzOi8vemV0dGVsa2FzdGVuLmRl
L3RoZS1hcmNoaXZlL30BAAAAIwAA
AAEAAAAHAAAAVFhULnJ0ZhAAAADe
TQhdtgEAAAAAAAAAAAAA
eSwgcGxlYXNlIHNldCB0aGVzZSB2
YXJpYWJsZXM6XApcCi0gWktWSVpf
QVJDSElWRV9QQVRIIGlzIHRoZSBw
YXRoIHRvIHdoZXJlIHlvdXIgYXJj
aGl2ZSBub3RlcyBhcmUgc3RvcmUu
IEZvciBleGFtcGxlLCB+L0Ryb3Bi
b3gvTm90ZXNcCi0gWktWSVpfUEFU
SCBpcyB0aGUgYWJzb2x1dGUgcGF0
aCB0byB0aGUgemt2aXogZXhlY3V0
YWJsZS4gUGxlYXNlIHNlZSBbMV0g
Zm9yIGluc3RydWN0aW9ucyBvZiBo
b3cgdG8gaW5zdGFsbCBpdC4gVGhl
IHBhdGggYmVsb3cgdXNlcyB0aGUg
b25lIHlvdSdkIGhhdmUgaWYgeW91
IGZvbGxvd2VkIHRoZSBpbnN0cnVj
dGlvbnMgZGlyZWN0bHkuXAotIFpL
VklaX1VTRV9HUkFQSFZJWiBzZXQg
dG8gMSBpZiB5b3Ugd2FudCB0byB1
c2UgR3JhcGh2aXogdG8gZ2VuZXJh
dGUgdGhlIHZpc3VhbGl6YXRpb24u
IE9yIHVzZSAwIGlmIHlvdSB3YW50
IHRvIHVzZSBQbG90bHlcClwKXApb
MV06IGh0dHBzOi8vZ2l0aHViLmNv
bS9aZXR0ZWxrYXN0ZW4tTWV0aG9k
L3prdml6XApbMl06IGh0dHBzOi8v
emV0dGVsa2FzdGVuLmRlL3RoZS1h
cmNoaXZlL30BAAAAIwAAAAEAAAAH
AAAAVFhULnJ0ZhAAAAA/NkpdtgEA
AAAAAAAAAAAA
</data>
<key>Title</key>
<string>README</string>
Expand All @@ -90,6 +96,16 @@
<key>Variable</key>
<string>ZKVIZ_PATH</string>
</dict>
<dict>
<key>ActionName</key>
<string>Choose whether to use Graphviz or not to render the visualization [0 or 1]</string>
<key>MacroActionType</key>
<string>SetVariableToText</string>
<key>Text</key>
<string>0</string>
<key>Variable</key>
<string>ZKVIZ_USE_GRAPHVIZ</string>
</dict>
<dict>
<key>ActionName</key>
<string>Notify that listing all notes might take a little while</string>
Expand Down Expand Up @@ -189,8 +205,6 @@
<string>Window</string>
<key>IncludeStdErr</key>
<true/>
<key>IsDisclosed</key>
<false/>
<key>MacroActionType</key>
<string>ExecuteShellScript</string>
<key>Path</key>
Expand All @@ -207,15 +221,25 @@ import subprocess
# Get path to the zkviz executable
zkviz = os.environ['KMVAR_ZKVIZ_PATH']

# Check if `/usr/local/bin` is in the PATH, which is needed for Graphviz.
# If not, append it.
if '/usr/loca/bin/' not in os.environ['PATH']:
os.environ['PATH'] += ':/usr/local/bin'

# Use graphviz or not
use_graphviz = int(os.environ['KMVAR_ZKVIZ_USE_GRAPHVIZ'])

# Get the list of paths using environment variables.
filenames = os.environ['KMVAR_ZKVIZ_ZETTEL_FILENAMES'].strip().splitlines()

# Create the output file next to your zettels. Otherwise, we run into a permission issue.
archive_path = os.path.expanduser(os.environ['KMVAR_ZKVIZ_ARCHIVE_PATH'])
output_filename = os.path.join(archive_path, 'zettel-network.html')

# Build the arguments to
# Build the arguments to execute
args = [zkviz, '--output', output_filename] + filenames
if use_graphviz:
args.insert(1, '--use-graphviz')

# Capture error messages and display them if something happens. Otherwise,
# this should print any output.
Expand All @@ -236,7 +260,7 @@ except subprocess.CalledProcessError as exc:
<key>CreationDate</key>
<real>582430104.35251606</real>
<key>ModificationDate</key>
<real>582518133.38314497</real>
<real>586837783.08286202</real>
<key>Name</key>
<string>Generate network visualization of notes selected in The Archive using zkviz</string>
<key>Triggers</key>
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def find_version(*file_paths):
url="https://github.com/Zettelkasten-Method/zkviz",
packages=setuptools.find_packages(),
install_requires=[
'graphviz',
'plotly',
'numpy',
'scipy',
Expand Down
87 changes: 87 additions & 0 deletions zkviz/graphviz.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
from textwrap import fill

from graphviz import Digraph


class NetworkGraphviz:

def __init__(self, name='Zettelkasten', engine='sfdp', shape='record'):
"""
Build network to visualize with Graphviz.
Parameters
----------
name : str (optional)
Name of the network. Default is "Zettelkasten".
engine : str
Layout engine used by Graphviz. The default is 'sfdp', which is a
good default. See the graphviz documentation for alternatives.
shape : str {record, plaintext}
The shape, or style, of each node. The default is "record".
"""
self.name = name
self.engine = engine
self.graph = Digraph(comment=self.name, engine=self.engine)
self.shape = shape

def wrap_title(self, text, width=30):
"""
Wrap the title to be a certain width.
Parameters
----------
text : str
The text to wrap.
width : int
The text width, in characters.
"""
return fill(text, width)

def add_node(self, node_id, title):
"""
Add a node to the network.
Parameters
----------
node_id : str, or int
A unique identifier for the node, typically the zettel ID.
title : str
The text label for each node, typically the zettel title.
"""
if self.shape == 'plaintext':
label = self.wrap_title("{} {}".format(node_id, title)).strip()
elif self.shape == 'record':
# Wrap in {} so the elements are stacked vertically
label = "{" + '|'.join([node_id, self.wrap_title(title)]) + "}"

self.graph.node(node_id, label, shape=self.shape)

def add_edge(self, source, target):
"""
Add a node (a zettel) to the network.
Parameters
----------
source : str or int
The ID of the source zettel.
target : str or int
The ID of the target (cited) zettel.
"""
self.graph.edge(source, target)

def render(self, output, view=True):
"""
Render the network to disk.
Parameters
----------
output : str
Name of the output file.
view : bool
Show the network using the default PDF viewer. Default is True.
"""
self.graph.render(output, view=view)
Loading

0 comments on commit 223d88a

Please sign in to comment.