Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: Graph visual #1223

Merged
merged 76 commits into from
May 19, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
1c1ce4e
Initial code for graph visual
Jul 26, 2015
b8a444f
Initial code for placing nodes and edges.
Jul 28, 2015
419fab1
Move GraphVisual to its own package
Jul 28, 2015
31879c9
Add __init__.py for graphs subpackage and add example
Jul 28, 2015
206ad4a
Use adjacency_mat.shape[0] to get the number of nodes.
Jul 28, 2015
7a5d4cf
Lower edge probability and set node border width to zero
Jul 28, 2015
afa2275
Fix a bug where all possible edges where drawn
Jul 28, 2015
c376e7c
Add arrow size property to GraphVisual
Jul 29, 2015
89b72b4
Create directed graph in GraphVisual Example
Jul 29, 2015
112df00
Use undirected graphs for now
Jul 30, 2015
e7dbbed
Add initial layout API code
Jul 30, 2015
cbef3d1
Add subpackage for graph layouts
Jul 30, 2015
464ffa8
Yield an np.array instead of list
Jul 31, 2015
8f2ffa2
Add random layout to layouts subpackage
Jul 31, 2015
e32529b
Refactor GraphVisual layout code
Jul 31, 2015
c30396c
Adapt GraphVisual example to the new layout system
Jul 31, 2015
faf8f49
Add a circular graph layout
Jul 31, 2015
0b728bb
Use circular layout in graph example
Jul 31, 2015
d285ecc
Refactor line vertex generation for graph layouts
Aug 3, 2015
6a4df1e
Add circular layout to the layout dictionary
Aug 3, 2015
3a04cfc
Arrow vertex generation can probably be vectorized
Aug 3, 2015
6495048
Install networkx for GraphVisual examples
Aug 3, 2015
de64a17
Initial code for a force directed graph layout
Aug 4, 2015
116fcd4
Remove unused import
Aug 4, 2015
d9d2c7d
Add new way of retreiving graph layouts
Aug 4, 2015
200b4d4
Alias spring_layout to the force directed layout
Aug 6, 2015
8018955
Reverse return values of animate_layout
Aug 6, 2015
2c4f411
Add function to normalize a set of coordinates
Aug 6, 2015
cdf7899
Fix some small things in the force_directed layout
Aug 6, 2015
69fb1b7
Update the graph example
Aug 6, 2015
c08f863
Improve documentation of graph visual related code
Aug 6, 2015
22a4138
Remove _layout_map from graph.py
Aug 6, 2015
5ddedc9
Improve docs and variable names of GraphVisual
Aug 6, 2015
e4b004d
Optimize
Aug 11, 2015
1c9a6f3
Improve performance of edge vertex generation
Aug 11, 2015
c460bfc
Remove debug code from rescale_layout
Aug 11, 2015
9978c02
Remove unused import
Aug 11, 2015
c4014a2
Improve performance of straight_line_vertices
Aug 11, 2015
e8e42fe
Add support for directed edges again
Aug 11, 2015
8610cae
Remove generators from line vertex generation code
Aug 12, 2015
91dbf47
Construct additional COO adjancency matrix
Aug 12, 2015
8f2ca07
Use T attribute to get transpose
Aug 12, 2015
c370225
`rescale_layout` operates in place
Aug 12, 2015
0757286
Make straight_line_vertices private
Aug 12, 2015
fc00e6a
Refactor a few function names
Aug 13, 2015
61b14fc
Let fruchterman_reingold derive from object
Aug 13, 2015
e271558
Improve docs of random and circular layout.
Aug 13, 2015
7071744
Improve Fruchterman-Reingold implementation
Aug 13, 2015
b6ff874
Use the same delta pos calculation method
Aug 13, 2015
f70838e
Remove unsed import
Aug 13, 2015
52cd97d
Fix PEP8 error in force_directed.py
Aug 13, 2015
2166f52
Update usage of layouts.get_layout
Aug 13, 2015
1de5353
Make sure AVAILABLE_LAYOUTS is a tuple
Aug 18, 2015
5586d90
Make rescale_layout private
Aug 18, 2015
ae02045
Use np.float32 instead of 'f32' as dtype
Aug 18, 2015
a08005a
Use the more obvious linspace instead of arange
Aug 18, 2015
6c3cf04
Make sure adjacency_mat is in COO format
Aug 18, 2015
e8be78f
Improve check if arrows are empty
Aug 19, 2015
aba87c6
Concatenate call fits on a single line
Aug 19, 2015
bca8c33
Improve documentation for fruchterman_reingold
Aug 19, 2015
c0d87b3
Use expanded if-else construction
Aug 19, 2015
113b332
Improve documentation of GraphVisual
Aug 19, 2015
219a3b8
Do not use pre-initialized delta array
Aug 19, 2015
29e237e
Do not index on the pos array
Aug 19, 2015
c2310e3
Use proper check to see if there are any arrows
Aug 19, 2015
064e8bb
WIP: Scene example and minor tweaks
larsoner Aug 22, 2015
78860e5
Add initial tests for the graph visual
Oct 19, 2015
6772979
Use np.float32 instead of 'f32'
Oct 19, 2015
2c2fcef
Add test for fruchterman-reingold layout
Oct 19, 2015
bc867be
PEP8 fix
Oct 19, 2015
502e23c
Fix possibility of non-initialized arrow-vbo
Oct 27, 2015
8df88fd
Remove fruchterman-reingold layout test
Oct 27, 2015
9b6422e
Remove dependency on networkx for layout tests
Oct 27, 2015
8f6c1e9
Use the same adjacency matrix as before
Oct 27, 2015
07cd175
FIX: Fix tests
larsoner May 19, 2016
2622212
FIX: Flake [size skip]
larsoner May 19, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ install:
if [ "${PYTHON}" == "2.6" ]; then
conda install --yes --quiet wxpython numpy$NUMPY > ${REDIRECT_TO};
else
conda install --yes --quiet ipython$IPYTHON ipython-notebook numpy$NUMPY > ${REDIRECT_TO};
conda install --yes --quiet ipython$IPYTHON ipython-notebook numpy$NUMPY networkx > ${REDIRECT_TO};
fi;
if [ "${PYTHON}" == "3.4" ]; then
pip install -q freetype-py husl pypng cassowary https://github.com/mpld3/mplexporter/zipball/master > ${REDIRECT_TO};
Expand Down
41 changes: 41 additions & 0 deletions examples/basics/scene/graph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Vispy Development Team.
# Distributed under the (new) BSD License. See LICENSE.txt for more info.

"""
This example demonstrates how to visualise a NetworkX graph using a VisPy
Graph.
"""

import sys

import networkx as nx

from vispy import app, scene
from vispy.visuals.graphs import layouts


canvas = scene.SceneCanvas(title='Simple NetworkX Graph', size=(600, 600),
bgcolor='white', show=True)
view = canvas.central_widget.add_view('panzoom')

graph = nx.adjacency_matrix(
nx.fast_gnp_random_graph(500, 0.005, directed=True))
layout = layouts.get_layout('force_directed', iterations=100)

visual = scene.visuals.Graph(
graph, layout=layout, line_color='black', arrow_type="stealth",
arrow_size=30, node_symbol="disc", node_size=20,
face_color=(1, 0, 0, 0.2), border_width=0.0, animate=True, directed=False,
parent=view.scene)


@canvas.events.draw.connect
def on_draw(event):
if not visual.animate_layout():
canvas.update()

if __name__ == '__main__':
if sys.flags.interactive != 1:
app.run()
4 changes: 2 additions & 2 deletions examples/basics/visuals/arrows_quiver.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ def on_resize(self, event):
self.visual.transforms.configure(canvas=self, viewport=vp)

def rotate_arrows(self, point_towards):
direction_vectors = (self.grid_coords -
point_towards).astype(np.float32)
direction_vectors = (self.grid_coords - point_towards).astype(
np.float32)
norms = np.sqrt(np.sum(direction_vectors**2, axis=-1))
direction_vectors[:, 0] /= norms
direction_vectors[:, 1] /= norms
Expand Down
59 changes: 59 additions & 0 deletions examples/basics/visuals/graph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Vispy Development Team.
# Distributed under the (new) BSD License. See LICENSE.txt for more info.

"""
This example demonstrates how to visualise a NetworkX graph using the
GraphVisual.
"""

import sys

import networkx as nx

from vispy import app, visuals
from vispy.visuals.graphs import layouts
from vispy.visuals.transforms import STTransform


class Canvas(app.Canvas):
def __init__(self):
app.Canvas.__init__(self, title="Simple NetworkX Graph",
keys="interactive", size=(600, 600))

graph = nx.adjacency_matrix(
nx.fast_gnp_random_graph(500, 0.005, directed=True))
layout = layouts.get_layout('force_directed', iterations=100)

self.visual = visuals.GraphVisual(
graph, layout=layout, line_color='black', arrow_type="stealth",
arrow_size=30, node_symbol="disc", node_size=20,
face_color=(1, 0, 0, 0.5), border_width=0.0, animate=True,
directed=False)

self.visual.transform = STTransform(self.visual_size, (20, 20))
self.show()

@property
def visual_size(self):
return self.physical_size[0] - 40, self.physical_size[1] - 40

def on_resize(self, event):
self.visual.transform.scale = self.visual_size
vp = (0, 0, self.physical_size[0], self.physical_size[1])
self.context.set_viewport(*vp)
self.visual.transforms.configure(canvas=self, viewport=vp)

def on_draw(self, event):
self.context.clear('white')
self.visual.draw()
if not self.visual.animate_layout():
self.update()


if __name__ == '__main__':
win = Canvas()

if sys.flags.interactive != 1:
app.run()
1 change: 1 addition & 0 deletions vispy/scene/visuals.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ def generate_docstring(subclass, clsname):
Compound = create_visual_node(visuals.CompoundVisual)
Cube = create_visual_node(visuals.CubeVisual)
Ellipse = create_visual_node(visuals.EllipseVisual)
Graph = create_visual_node(visuals.GraphVisual)
GridLines = create_visual_node(visuals.GridLinesVisual)
GridMesh = create_visual_node(visuals.GridMeshVisual)
Histogram = create_visual_node(visuals.HistogramVisual)
Expand Down
1 change: 1 addition & 0 deletions vispy/visuals/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,4 @@
from .xyz_axis import XYZAxisVisual # noqa
from .border import _BorderVisual # noqa
from .colorbar import ColorBarVisual # noqa
from .graphs import GraphVisual # noqa
1 change: 1 addition & 0 deletions vispy/visuals/graphs/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .graph import GraphVisual # noqa
212 changes: 212 additions & 0 deletions vispy/visuals/graphs/graph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Vispy Development Team.
# Distributed under the (new) BSD License. See LICENSE.txt for more info.
"""
Graph Visual
============

This visual can be used to visualise graphs or networks.
"""

from ..visual import CompoundVisual
from ..line import ArrowVisual
from ..markers import MarkersVisual
from . import layouts


class GraphVisual(CompoundVisual):
"""Visual for displaying graphs or networks.

Parameters
----------
adjacency_mat : array or sparse
The adjacency matrix of the graph.
directed : bool
Whether the graph is directed or not. If True, then this visual will
draw arrows for the directed edges.
line_color : str or :class:`vispy.color.colormap.ColorMap`
The color to use for the edges.
line_width : number
The edge thickness.
arrow_type : str
The kind of arrow head to use. See :class:`vispy.visuals.ArrowHead`
for more information.
arrow_size : number
The size of the arrow head.
node_symbol : string
The marker to use for nodes. See
:class:`vispy.visuals.MarkersVisual` for more information.
node_size : number
The size of the node
border_color : str or :class:`vispy.color.colormap.ColorMap`
The border color for nodes.
face_color : str or :class:`vispy.color.colormap.ColorMap`
The face color for nodes.
border_width : number
The border size for nodes.

See Also
--------
ArrowVisual, MarkersVisual

"""

_arrow_attributes = ('arrow_type', 'arrow_size')
_arrow_kwargs = ('line_color', 'line_width')
_node_kwargs = ('node_symbol', 'node_size', 'border_color', 'face_color',
'border_width')

_arrow_kw_trans = dict(line_color='color', line_width='width')
_node_kw_trans = dict(node_symbol='symbol', node_size='size',
border_color='edge_color', border_width='edge_width')

def __init__(self, adjacency_mat=None, directed=False, layout=None,
animate=False, line_color=None, line_width=None,
arrow_type=None, arrow_size=None, node_symbol=None,
node_size=None, border_color=None, face_color=None,
border_width=None):

self._edges = ArrowVisual(method='gl', connect='segments')
self._nodes = MarkersVisual()

self._arrow_data = {}
self._node_data = {}

self._adjacency_mat = None

self._layout = None
self._layout_iter = None
self.layout = layout

self._directed = directed
self.directed = directed

self._animate = False
self.animate = animate

CompoundVisual.__init__(self, [self._edges, self._nodes])

self.set_data(adjacency_mat, line_color=line_color,
line_width=line_width, arrow_type=arrow_type,
arrow_size=arrow_size, node_symbol=node_symbol,
node_size=node_size, border_color=border_color,
face_color=face_color, border_width=border_width)

@property
def adjacency_matrix(self):
return self._adjacency_mat

@property
def layout(self):
return self._layout

@layout.setter
def layout(self, value):
if type(value) == str:
self._layout = layouts.get_layout(value)
else:
assert callable(value)
self._layout = value

self._layout_iter = None

@property
def directed(self):
return self._directed

@directed.setter
def directed(self, value):
self._directed = bool(value)

@property
def animate(self):
return self._animate

@animate.setter
def animate(self, value):
self._animate = bool(value)

def animate_layout(self):
if self._layout_iter is None:
if self._adjacency_mat is None:
raise ValueError("No adjacency matrix set yet. An adjacency "
"matrix is required to calculate the layout.")

self._layout_iter = iter(self._layout(self._adjacency_mat,
self._directed))

try:
node_vertices, line_vertices, arrows = next(self._layout_iter)
except StopIteration:
return True

self._nodes.set_data(pos=node_vertices, **self._node_data)
self._edges.set_data(pos=line_vertices, arrows=arrows,
**self._arrow_data)

return False

def set_final_layout(self):
if self._layout_iter is None:
if self._adjacency_mat is None:
raise ValueError("No adjacency matrix set yet. An adjacency "
"matrix is required to calculate the layout.")

self._layout_iter = iter(self._layout(self._adjacency_mat,
self._directed))

# Calculate the final position of the nodes and lines
node_vertices = None
line_vertices = None
arrows = None
for node_vertices, line_vertices, arrows in self._layout_iter:
pass

self._nodes.set_data(pos=node_vertices, **self._node_data)
self._edges.set_data(pos=line_vertices, arrows=arrows,
**self._arrow_data)

def reset_layout(self):
self._layout_iter = None

def set_data(self, adjacency_mat=None, **kwargs):
if adjacency_mat is not None:
if adjacency_mat.shape[0] != adjacency_mat.shape[1]:
raise ValueError("Adjacency matrix should be square.")

self._adjacency_mat = adjacency_mat

for k in self._arrow_attributes:
if k in kwargs:
translated = (self._arrow_kw_trans[k] if k in
self._arrow_kw_trans else k)

setattr(self._edges, translated, kwargs.pop(k))

arrow_kwargs = {}
for k in self._arrow_kwargs:
if k in kwargs:
translated = (self._arrow_kw_trans[k] if k in
self._arrow_kw_trans else k)

arrow_kwargs[translated] = kwargs.pop(k)

node_kwargs = {}
for k in self._node_kwargs:
if k in kwargs:
translated = (self._node_kw_trans[k] if k in
self._node_kw_trans else k)

node_kwargs[translated] = kwargs.pop(k)

if len(kwargs) > 0:
raise TypeError("%s.set_data() got invalid keyword arguments: %S"
% (self.__class__.__name__, list(kwargs.keys())))

# The actual data is set in GraphVisual.animate_layout or
# GraphVisual.set_final_layout
self._arrow_data = arrow_kwargs
self._node_data = node_kwargs

if not self._animate:
self.set_final_layout()