Commit 04572376 authored by Tiago Peixoto's avatar Tiago Peixoto
Browse files

GraphWidget: Implement progressive drawing to improve interactive responsiveness

parent 406d7c4a
......@@ -28,8 +28,13 @@
#include "graph_properties.hh"
#include <iostream>
#include <array>
#include <chrono>
#include <unordered_map>
#ifdef HAVE_SPARSEHASH
#include SPARSEHASH_INCLUDE(dense_hash_map)
#endif
#include <cairommconfig.h>
#include <cairomm/context.h>
......@@ -42,6 +47,10 @@ using namespace std;
using namespace boost;
using namespace graph_tool;
#ifdef HAVE_SPARSEHASH
using google::dense_hash_map;
#endif
enum vertex_attr_t {
VERTEX_SHAPE = 100,
VERTEX_COLOR,
......@@ -118,7 +127,11 @@ enum edge_marker_t {
typedef pair<double, double> pos_t;
typedef std::tuple<double, double, double, double> color_t;
typedef std::unordered_map<int, boost::any> attrs_t;
#ifdef HAVE_SPARSEHASH
typedef dense_hash_map<int, boost::any, std::hash<int>> attrs_t;
#else
typedef std::unordered_map<int, boost::any> attrs_t;
#endif
typedef boost::mpl::map41<
boost::mpl::pair<boost::mpl::int_<VERTEX_SHAPE>, vertex_shape_t>,
......@@ -411,7 +424,7 @@ public:
template <class Value>
Value get(int k)
{
typeof(_attrs.begin()) iter = _attrs.find(k);
auto iter = _attrs.find(k);
if (iter != _attrs.end())
{
typedef DynamicPropertyMapWrap<Value, Descriptor, Converter> pmap_t;
......@@ -777,6 +790,13 @@ public:
color_t color, fillcolor;
double size, pw, aspect;
size = get_size(cr);
std::array<double, 4> clip;
cr.get_clip_extents(clip[0], clip[1], clip[2], clip[3]);
if ((_pos.first + 2 * size < clip[0] && _pos.second + 2 * size < clip[1]) ||
(_pos.first - 2 * size > clip[2] && _pos.second - 2 * size > clip[3]))
return;
aspect = _attrs.template get<double>(VERTEX_ASPECT);
if (!outline)
......@@ -997,7 +1017,7 @@ public:
EdgeShape(VertexShape& s, VertexShape& t, AttrDict<Descriptor> attrs)
: _s(s), _t(t), _attrs(attrs) {}
void draw(Cairo::Context& cr)
void draw(Cairo::Context& cr, double res = 0.)
{
pos_t pos_begin, pos_end;
......@@ -1030,6 +1050,11 @@ public:
marker_size = get_user_dist(cr, marker_size);
bool sloppy = _attrs.template get<uint8_t>(EDGE_SLOPPY);
if (marker_size < get_user_dist(cr, res))
{
sloppy = true;
}
pos_begin = _s.get_pos();
pos_end = _t.get_pos();
......@@ -1441,14 +1466,21 @@ private:
AttrDict<Descriptor> _attrs;
};
template <class Graph, class VertexIterator, class PosMap>
void draw_vertices(Graph&, pair<VertexIterator,VertexIterator> v_range,
template <class Graph, class VertexIterator, class PosMap, class Time>
void draw_vertices(Graph&, pair<VertexIterator, VertexIterator> v_range,
PosMap pos_map, attrs_t& attrs, attrs_t& defaults,
size_t offset, Time max_time, size_t& count,
Cairo::Context& cr)
{
typedef typename graph_traits<Graph>::vertex_descriptor vertex_t;
for(VertexIterator v = v_range.first; v != v_range.second; ++v)
{
if (count < offset)
{
count++;
continue;
}
pos_t pos;
if (pos_map[*v].size() >= 2)
{
......@@ -1457,18 +1489,29 @@ void draw_vertices(Graph&, pair<VertexIterator,VertexIterator> v_range,
}
VertexShape<vertex_t> vs(pos, AttrDict<vertex_t>(*v, attrs, defaults));
vs.draw(cr);
count++;
if (chrono::high_resolution_clock::now() > max_time)
break;
}
}
template <class Graph, class EdgeIterator, class PosMap>
template <class Graph, class EdgeIterator, class PosMap, class Time>
void draw_edges(Graph& g, pair<EdgeIterator, EdgeIterator> e_range,
PosMap pos_map, attrs_t& eattrs, attrs_t& edefaults,
attrs_t& vattrs, attrs_t& vdefaults, Cairo::Context& cr)
attrs_t& vattrs, attrs_t& vdefaults, double res, size_t offset,
Time max_time, size_t& count, Cairo::Context& cr)
{
typedef typename graph_traits<Graph>::vertex_descriptor vertex_t;
typedef typename graph_traits<Graph>::edge_descriptor edge_t;
for(EdgeIterator e = e_range.first; e != e_range.second; ++e)
{
if (count < offset)
{
count++;
continue;
}
vertex_t s, t;
s = source(*e, g);
t = target(*e, g);
......@@ -1486,7 +1529,10 @@ void draw_edges(Graph& g, pair<EdgeIterator, EdgeIterator> e_range,
}
if (spos == tpos && t != s)
{
count++;
continue;
}
VertexShape<vertex_t> ss(spos, AttrDict<vertex_t>(s, vattrs, vdefaults));
VertexShape<vertex_t> ts(tpos, AttrDict<vertex_t>(t, vattrs, vdefaults));
......@@ -1494,7 +1540,11 @@ void draw_edges(Graph& g, pair<EdgeIterator, EdgeIterator> e_range,
EdgeShape<edge_t,VertexShape<vertex_t> > es(ss, ts,
AttrDict<edge_t>(*e, eattrs,
edefaults));
es.draw(cr);
es.draw(cr, res);
count++;
if (chrono::high_resolution_clock::now() > max_time)
break;
}
}
......@@ -1546,29 +1596,31 @@ struct ordered_range
struct do_cairo_draw_edges
{
template <class Graph, class PosMap, class EdgeOrder>
void operator()(Graph& g, PosMap pos, EdgeOrder edge_order,
attrs_t& vattrs, attrs_t& eattrs, attrs_t& vdefaults,
attrs_t& edefaults, Cairo::Context& cr) const
template <class Graph, class PosMap, class EdgeOrder, class Time>
void operator()(Graph& g, PosMap pos, EdgeOrder edge_order, attrs_t& vattrs,
attrs_t& eattrs, attrs_t& vdefaults, attrs_t& edefaults,
double res, size_t offset, Time max_time, size_t& count,
Cairo::Context& cr) const
{
ordered_range<typename graph_traits<Graph>::edge_iterator>
edge_range(edges(g));
draw_edges(g, edge_range.get_range(edge_order), pos, eattrs,
edefaults, vattrs, vdefaults, cr);
draw_edges(g, edge_range.get_range(edge_order), pos, eattrs, edefaults,
vattrs, vdefaults, res, offset, max_time, count, cr);
}
};
struct do_cairo_draw_vertices
{
template <class Graph, class PosMap, class VertexOrder>
template <class Graph, class PosMap, class VertexOrder, class Time>
void operator()(Graph& g, PosMap pos, VertexOrder vertex_order,
attrs_t& vattrs, attrs_t&, attrs_t& vdefaults,
attrs_t&, Cairo::Context& cr) const
attrs_t& vattrs, attrs_t&, attrs_t& vdefaults, attrs_t&,
size_t offset, Time max_time, size_t& count,
Cairo::Context& cr) const
{
ordered_range<typename graph_traits<Graph>::vertex_iterator>
vertex_range(vertices(g));
draw_vertices(g, vertex_range.get_range(vertex_order), pos, vattrs,
vdefaults, cr);
vdefaults, offset, max_time, count, cr);
}
};
......@@ -1655,18 +1707,28 @@ struct populate_edge_attrs
};
void cairo_draw(GraphInterface& gi,
boost::any pos,
boost::any vorder,
boost::any eorder,
bool nodesfirst,
boost::python::dict ovattrs,
boost::python::dict oeattrs,
boost::python::dict ovdefaults,
boost::python::dict oedefaults,
boost::python::object ocr)
size_t cairo_draw(GraphInterface& gi,
boost::any pos,
boost::any vorder,
boost::any eorder,
bool nodesfirst,
boost::python::dict ovattrs,
boost::python::dict oeattrs,
boost::python::dict ovdefaults,
boost::python::dict oedefaults,
double res,
size_t offset,
int64_t max_time,
boost::python::object ocr)
{
attrs_t vattrs, eattrs, vdefaults, edefaults;
#ifdef HAVE_SPARSEHASH
vattrs.set_empty_key(numeric_limits<int>::max());
eattrs.set_empty_key(numeric_limits<int>::max());
vdefaults.set_empty_key(numeric_limits<int>::max());
edefaults.set_empty_key(numeric_limits<int>::max());
#endif
typedef graph_traits<GraphInterface::multigraph_t>::vertex_descriptor vertex_t;
populate_attrs<vertex_t, vertex_properties>(ovattrs, vattrs);
......@@ -1685,31 +1747,55 @@ void cairo_draw(GraphInterface& gi,
if (eorder.empty())
eorder = no_order();
size_t count = 0;
auto mtime = chrono::high_resolution_clock::now();
if (max_time < 0)
mtime = chrono::high_resolution_clock::time_point::max();
else
mtime += chrono::milliseconds(max_time);
Cairo::Context cr(PycairoContext_GET(ocr.ptr()));
if (nodesfirst)
run_action<graph_tool::detail::always_directed>()
(gi, std::bind(do_cairo_draw_vertices(), placeholders::_1,
placeholders::_2, placeholders::_3,
std::ref(vattrs), std::ref(eattrs), std::ref(vdefaults),
std::ref(edefaults), std::ref(cr)),
std::ref(edefaults), offset, mtime, std::ref(count),
std::ref(cr)),
vertex_scalar_vector_properties(),
vorder_t())(pos, vorder);
if (chrono::high_resolution_clock::now() > mtime)
return count;
run_action<graph_tool::detail::always_directed>()
(gi, std::bind(do_cairo_draw_edges(), placeholders::_1, placeholders::_2,
placeholders::_3, std::ref(vattrs), std::ref(eattrs),
std::ref(vdefaults), std::ref(edefaults), std::ref(cr)),
std::ref(vdefaults), std::ref(edefaults), res, offset,
mtime, std::ref(count), std::ref(cr)),
vertex_scalar_vector_properties(),
eorder_t())(pos, eorder);
if (!nodesfirst)
{
if (chrono::high_resolution_clock::now() > mtime)
return count;
run_action<graph_tool::detail::always_directed>()
(gi, std::bind(do_cairo_draw_vertices(), placeholders::_1,
placeholders::_2, placeholders::_3,
std::ref(vattrs), std::ref(eattrs), std::ref(vdefaults),
std::ref(edefaults), std::ref(cr)),
std::ref(edefaults), offset, mtime, std::ref(count),
std::ref(cr)),
vertex_scalar_vector_properties(),
vorder_t())(pos, vorder);
}
if (chrono::high_resolution_clock::now() < mtime)
count = 0;
return count;
}
struct do_apply_transforms
......
......@@ -427,9 +427,9 @@ def parse_props(prefix, args):
def cairo_draw(g, pos, cr, vprops=None, eprops=None, vorder=None, eorder=None,
nodesfirst=False, vcmap=default_cm,
ecmap=default_cm, loop_angle=float("nan"),
parallel_distance=None, fit_view=False, **kwargs):
nodesfirst=False, vcmap=default_cm, ecmap=default_cm,
loop_angle=float("nan"), parallel_distance=None, fit_view=False,
res=0, render_offset=0, max_render_time=-1, **kwargs):
r"""
Draw a graph to a :mod:`cairo` context.
......@@ -474,6 +474,15 @@ def cairo_draw(g, pos, cr, vprops=None, eprops=None, vorder=None, eorder=None,
specify the view in user coordinates.
bg_color : str or sequence (optional, default: ``None``)
Background color. The default is transparent.
res : float (optional, default: ``0.``):
If shape sizes fall below this value, simplified drawing is used.
render_offset : int (optional, default: ``0``):
If supplied, the rendering will skip the specified initial amount of
vertices / edges.
max_render_time : int (optional, default: ``-1``):
Maximum allowed time (in milliseconds) for rendering. If exceeded, the
rendering will return unfinished. If negative values are given, the
rendering will always complete.
vertex_* : :class:`~graph_tool.PropertyMap` or arbitrary types (optional, default: ``None``)
Parameters following the pattern ``vertex_<prop-name>`` specify the
vertex property with name ``<prop-name>``, as an alternative to the
......@@ -483,6 +492,11 @@ def cairo_draw(g, pos, cr, vprops=None, eprops=None, vorder=None, eorder=None,
property with name ``<prop-name>``, as an alternative to the ``eprops``
parameter.
Returns
-------
offset : int
The offset into the completed rendering. If this value is zero, the
rendering was complete.
"""
vprops = {} if vprops is None else copy.copy(vprops)
......@@ -551,11 +565,12 @@ def cairo_draw(g, pos, cr, vprops=None, eprops=None, vorder=None, eorder=None,
eprops["control_points"] = position_parallel_edges(g, pos, loop_angle,
parallel_distance)
g = GraphView(g, directed=True)
libgraph_tool_draw.cairo_draw(g._Graph__graph, _prop("v", g, pos),
_prop("v", g, vorder), _prop("e", g, eorder),
nodesfirst, vattrs, eattrs, vdefs, edefs, cr)
count = libgraph_tool_draw.cairo_draw(g._Graph__graph, _prop("v", g, pos),
_prop("v", g, vorder), _prop("e", g, eorder),
nodesfirst, vattrs, eattrs, vdefs, edefs, res,
render_offset, max_render_time, cr)
cr.restore()
return count
def color_contrast(color):
c = np.asarray(color)
......
......@@ -131,7 +131,8 @@ class GraphWidget(Gtk.DrawingArea):
eorder=None, nodesfirst=False, update_layout=False,
layout_K=1., multilevel=False, display_props=None,
display_props_size=11, fit_area=0.95, bg_color=None,
layout_callback=None, key_press_callback=None, **kwargs):
max_render_time=300, layout_callback=None,
key_press_callback=None, **kwargs):
r"""Interactive GTK+ widget displaying a given graph.
Parameters
......@@ -169,6 +170,9 @@ class GraphWidget(Gtk.DrawingArea):
Fraction of the drawing area to fit the graph initially.
bg_color : str or sequence (optional, default: ``None``)
Background color. The default is white.
max_render_time : int (optional, default: ``300``)
Maximum amount of time (in milliseconds) spent rendering portions of
the graph.
layout_callback : function (optional, default: ``Node``)
User-supplied callback to be called whenever the positions of the layout
have changed. It needs to have the following signature:
......@@ -260,6 +264,7 @@ class GraphWidget(Gtk.DrawingArea):
self.pointer = [0, 0]
self.picked = False
self.selected = g.new_vertex_property("bool")
self.sel_edge_filt = g.new_edge_property("bool", False)
self.srect = None
self.drag_begin = None
self.moved_picked = False
......@@ -275,7 +280,9 @@ class GraphWidget(Gtk.DrawingArea):
self.base_geometry = None
self.background = None
self.bg_color = bg_color if bg_color is not None else [1, 1, 1, 1]
self.surface_callback = None
self.regenerate_offset = 0
self.regenerate_max_time = max_render_time
self.max_render_time = max_render_time
self.layout_callback_id = None
self.layout_K = layout_K
......@@ -359,7 +366,7 @@ class GraphWidget(Gtk.DrawingArea):
self.layout_step *= 0.9
if self.vertex_matrix is not None:
self.vertex_matrix.update()
self.regenerate_surface(lazy=False)
self.regenerate_surface(reset=True, complete=True)
self.queue_draw()
ps = ungroup_vector_property(self.pos, [0, 1])
delta = np.sqrt((pos_temp[0].fa - ps[0].fa) ** 2 +
......@@ -390,7 +397,7 @@ class GraphWidget(Gtk.DrawingArea):
adjust_default_sizes(self.g, geometry, self.vprops,
self.eprops, force=True)
self.fit_to_window(ink=False)
self.regenerate_surface(lazy=False)
self.regenerate_surface(reset=True, complete=True)
except StopIteration:
self.g = self.ag
self.pos = self.apos
......@@ -410,48 +417,48 @@ class GraphWidget(Gtk.DrawingArea):
# Actual drawing
def regenerate_surface(self, lazy=True, timeout=350):
r"""Redraw the graph surface. If lazy is True, the actual redrawing will
be performed after the specified timeout."""
if lazy:
if self.surface_callback is not None:
gobject.source_remove(self.surface_callback)
f = lambda: self.regenerate_surface(lazy=False)
self.surface_callback = gobject.timeout_add(timeout, f)
else:
geometry = [self.get_allocated_width() * 3,
self.get_allocated_height() * 3]
def regenerate_surface(self, reset=False, complete=False):
r"""Redraw the graph surface."""
if reset:
self.regenerate_offset = 0
geometry = [self.get_allocated_width() * 3,
self.get_allocated_height() * 3]
if (self.base is None or self.base_geometry[0] != geometry[0] or
self.base_geometry[1] != geometry[1] or reset):
# self.base = cairo.ImageSurface(cairo.FORMAT_ARGB32,
# *geometry)
w = self.get_window()
if w is None:
return False
self.base = w.create_similar_surface(cairo.CONTENT_COLOR_ALPHA,
*geometry)
self.base_geometry = geometry
self.regenerate_offset = 0
m = cairo.Matrix()
m.translate(self.get_allocated_width(),
self.get_allocated_height())
self.smatrix = self.smatrix * m
self.tmatrix = self.tmatrix * self.smatrix
if (self.base is None or self.base_geometry[0] != geometry[0] or
self.base_geometry[1] != geometry[1]):
# self.base = cairo.ImageSurface(cairo.FORMAT_ARGB32,
# *geometry)
w = self.get_window()
if w is None:
return False
self.base = w.create_similar_surface(cairo.CONTENT_COLOR_ALPHA,
*geometry)
self.base_geometry = geometry
cr = cairo.Context(self.base)
cr.set_source_rgba(*self.bg_color)
cr.paint()
cr.set_matrix(self.tmatrix)
cairo_draw(self.g, self.pos, cr, self.vprops, self.eprops,
self.vorder, self.eorder, self.nodesfirst, **self.kwargs)
self.smatrix = cairo.Matrix()
self.smatrix.translate(-self.get_allocated_width(),
-self.get_allocated_height())
if self.surface_callback is not None:
gobject.source_remove(self.surface_callback)
self.surface_callback = None
self.queue_draw()
return False
cr = cairo.Context(self.base)
if self.regenerate_offset == 0:
cr.set_source_rgba(*self.bg_color)
cr.paint()
cr.set_matrix(self.tmatrix)
mtime = -1 if complete else self.regenerate_max_time
count = cairo_draw(self.g, self.pos, cr, self.vprops, self.eprops,
self.vorder, self.eorder, self.nodesfirst, res=1,
render_offset=self.regenerate_offset,
max_render_time=mtime,
**self.kwargs)
self.regenerate_offset = count
def draw(self, da, cr):
r"""Redraw the widget."""
......@@ -462,17 +469,24 @@ class GraphWidget(Gtk.DrawingArea):
if self.geometry is None:
adjust_default_sizes(self.g, geometry, self.vprops, self.eprops)
self.fit_to_window(ink=False)
self.regenerate_surface(lazy=False)
self.regenerate_surface()
self.geometry = geometry
cr.save()
cr.set_matrix(self.smatrix)
ul = self.pos_to_device((0, 0), surface=True, cr=cr)
lr = self.pos_to_device(self.base_geometry, surface=True, cr=cr)
c1 = self.pos_to_device((0, 0), surface=True, cr=cr)
c2 = self.pos_to_device((0, self.base_geometry[1]), surface=True, cr=cr)
c3 = self.pos_to_device((self.base_geometry[0], 0), surface=True, cr=cr)
c4 = self.pos_to_device(self.base_geometry, surface=True, cr=cr)
c = [c1, c2, c3, c4]
ul = [min([x[0] for x in c]), min([x[1] for x in c])]
lr = [max([x[0] for x in c]), max([x[1] for x in c])]
cr.restore()
if (ul[0] > 0 or lr[0] < geometry[0] or
ul[1] > 0 or lr[1] < geometry[1]):
self.regenerate_surface(reset=True)
elif self.regenerate_offset > 0:
self.regenerate_surface()
if self.background is None:
......@@ -513,10 +527,9 @@ class GraphWidget(Gtk.DrawingArea):
vprops["text_color"] = [1., 1., 1., 0.]
eprops = {}
eprops.update(self.eprops)
eprops["color"] = [1., 1., 1., 0.]
u = GraphView(self.g, vfilt=self.selected)
u = GraphView(self.g, vfilt=self.selected,
efilt=self.sel_edge_filt)
cr.save()
cr.set_matrix(self.tmatrix * self.smatrix)
......@@ -534,7 +547,7 @@ class GraphWidget(Gtk.DrawingArea):
cr.set_source_rgba(0, 0, 1, 0.3)
cr.fill()
if self.surface_callback is not None:
if self.regenerate_offset > 0:
icon = self.render_icon(Gtk.STOCK_EXECUTE, Gtk.IconSize.BUTTON)
Gdk.cairo_set_source_pixbuf(cr, icon, 10, 10)
cr.paint()
......@@ -560,6 +573,8 @@ class GraphWidget(Gtk.DrawingArea):
cr.set_source_rgba(0, 0, 0, 1.0)
cr.show_text(txt)
if self.regenerate_offset > 0:
self.queue_draw()
return False
# Position and transforms
......@@ -721,7 +736,7 @@ class GraphWidget(Gtk.DrawingArea):
self.pos, self.vprops,
self.eprops)
self.moved_picked = False
self.regenerate_surface(timeout=100)
self.regenerate_surface(complete=True)
self.queue_draw()
elif event.button == 2:
self.panning = None
......@@ -789,6 +804,14 @@ class GraphWidget(Gtk.DrawingArea):
def scroll_event(self, widget, event):
r"""Handle scrolling."""
self.regenerate_max_time = 50
def restore_render_time():
self.regenerate_max_time = self.max_render_time
return False
self.surface_callback = gobject.timeout_add(2000, restore_render_time)
state = event.state
angle = 0
......@@ -812,16 +835,16 @@ class GraphWidget(Gtk.DrawingArea):
# keep centered
if zoom != 1:
center = self.pointer
cpos = self.pos_from_device(center, surface=True)
cpos = self.pos_from_device(center)
m = cairo.Matrix()
m.scale(zoom, zoom)
self.smatrix = self.smatrix.multiply(m)
self.tmatrix = self.tmatrix.multiply(m)
ncpos = self.pos_from_device(center, surface=True)
self.smatrix.translate(ncpos[0] - cpos[0],
ncpos = self.pos_from_device(center)
self.tmatrix.translate(ncpos[0] - cpos[0],
ncpos[1] - cpos[1])
self.regenerate_surface()
self.regenerate_surface(reset=True)
if angle != 0:
if not isinstance(self.picked, PropertyMap):
center = (self.pointer[0], self.pointer[1])
......@@ -830,7 +853,6 @@ class GraphWidget(Gtk.DrawingArea):
m.rotate(angle)
m.translate(-center[0], -center[1])
self.smatrix = self.smatrix.multiply(m)
self.regenerate_surface() <