paulbrodersen / netgraph

Publication-quality network visualisations in python
GNU General Public License v3.0
660 stars 39 forks source link

Hyperlink or selectable text from annotations? #56

Closed a-arbabian closed 1 year ago

a-arbabian commented 1 year ago

Hey,

I'm currently using the library on a project and have a usecase for linking to a URL based on node attributes. I have the annotation showing on node click, but realized that the link is not selectable or clickable. Would it be possible to do either of those (hyperlinks would be preferred but selectable text is good too)?

Thanks

paulbrodersen commented 1 year ago

Hi, thanks for raising the issue, that is an interesting use case.

As you may know, netgraph uses matplotlib for drawing all visual elements, including text objects. However, matplotlib doesn't really do hyperlinks, as the imagined end-point is an image (and images are not text files, and hence don't support hyperlinks). Matplotlib does support hyperlinks when using the SVG backend (as SVG files are text files, ultimately) but the SVG backend is not interactive. As netgraph makes ample use of matplotlib's interactive events, netgraph cannot support matplotlib hyperlinks.

However, it is fairly straightforward to emulate hyperlinks using matplotlib pick events in combination with the (python built-in) webbrowser module. Below some examples.

#!/usr/bin/env python
import webbrowser
import matplotlib.pyplot as plt

from netgraph import InteractiveGraph

fig, ax = plt.subplots()

# Enable picking.
def on_pick(event):
    if isinstance(event.artist, plt.Text):
        webbrowser.open(event.artist.get_text())

fig.canvas.mpl_connect('pick_event', on_pick)

# Define some links.
node_labels = {
    0 : "https://stackoverflow.com/a/54121221/2912349",
    1 : "https://stackoverflow.com/a/69682368/2912349"
}

annotations = {(0, 1) : "https://matplotlib.org/stable/gallery/event_handling/pick_event_demo.html"}

# Create a graph.
g = InteractiveGraph([(0, 1)], node_labels=node_labels, node_label_offset=0.05, annotations=annotations, ax=ax)

# Enable pick events on text objects.
# By (matplotlib) default, pick events are disabled.
for text_object in g.node_label_artists.values():
    text_object.set_picker(True)

# Annotation text objects are created on-the-fly.
# We hence have to monkkeypatch the InteractiveGraph instance to enable the picker,
# whenever an annotation is created.

original_add_annotation = g._add_annotation

def new_add_annotation(artist, *args, **kwargs):
    original_add_annotation(artist, *args, **kwargs)
    text_object = g.artist_to_text_object[artist]
    text_object.set_picker(True)

g._add_annotation = new_add_annotation

plt.show()

################################################################################
# Naturally, hyperlinks need not be text objects.

from netgraph._artists import NodeArtist, EdgeArtist

fig, ax = plt.subplots()

def on_pick(event):
    if isinstance(event.artist, (NodeArtist, EdgeArtist)):
        if hasattr(event.artist, 'url'):
            webbrowser.open(event.artist.url)

fig.canvas.mpl_connect('pick_event', on_pick)

g = InteractiveGraph([(0, 1)], node_labels=True, ax=ax)

g.node_artists[0].set_picker(True)
g.node_artists[0].url = "www.google.com"

g.edge_artists[(0, 1)].set_picker(10) # increase pick radius for thin edges
g.edge_artists[(0, 1)].url = "www.stackoverflow.com"

plt.show()

I hope that addresses your issue. Make sure to install the latest version as I just fixed a bug that occurred when annotating edges (or change the edge annotation to a node annotation in the example above).

paulbrodersen commented 1 year ago

I have added some simple examples to the documentation here. I will close the issue for now, but feel free to re-open it if you have any further concerns.

a-arbabian commented 1 year ago

Wow thanks for the excellent write up! Will try this next week 👍

a-arbabian commented 1 year ago

@paulbrodersen I was able to get everything working as far as having annotations that hyperlink to the url that is the text of the annotation, like below: annotations = {0: dict(s="www.google.com")}

But what about an annotation with text equal to some info about the node, and another attribute url that is used for the hyperlink, sort of like this: annotations = {0: dict(s="ANNOTATION TEXT", url='www.google.com')}

I tried your snippet under "Naturally, hyperlinks need not be text objects." section but the annotations don't hyperlink out. I changed the on_pick() method to pass the event.artist.url to webbrowser.open() with no luck. Does anything else need to be changed? How can I (or do I need to) enable picking on the 'url' attribute of the annotation?

paulbrodersen commented 1 year ago

You probably forgot to enable the picker?

import webbrowser
import matplotlib.pyplot as plt

from netgraph import InteractiveGraph

def on_pick(event):
    webbrowser.open(event.artist.get_url())

fig, ax = plt.subplots()
g = InteractiveGraph([(0, 1)], node_labels=True, annotations={0 : dict(s='lorem ipsum', url='www.google.com', picker=True)})
fig.canvas.mpl_connect('pick_event', on_pick)
plt.show()
a-arbabian commented 1 year ago

Seemed to be an issue with webbrowser module, got it working now thanks!