#! /usr/bin/env python
# -*- coding: utf-8 -*-
#
# graph_tool -- a general graph manipulation python module
#
# Copyright (C) 2006-2024 Tiago de Paula Peixoto <tiago@skewed.de>
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation; either version 3 of the License, or (at your option) any
# later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import warnings
import numpy
from collections.abc import Iterable
from .. topology import shortest_distance, is_bipartite
from .. import _check_prop_scalar, perfect_prop_hash
try:
import cairo
except ImportError:
msg = "Error importing cairo. Graph drawing will not work."
warnings.warn(msg, RuntimeWarning)
raise
default_cm = None
try:
import matplotlib.artist
import matplotlib.backends.backend_cairo
import matplotlib.cm
import matplotlib.colors
from matplotlib.cbook import flatten
default_clrs = list(matplotlib.cm.tab20.colors) + \
list(matplotlib.cm.tab20b.colors)
default_cm = matplotlib.cm.colors.ListedColormap(default_clrs)
default_cm_bin = matplotlib.cm.colors.ListedColormap([[1, 1, 1, 1],
[0, 0, 0, 1]])
has_draw_inline = 'inline' in matplotlib.get_backend()
color_converter = matplotlib.colors.ColorConverter()
except ImportError:
msg = "Error importing matplotlib module. Graph drawing will not work."
warnings.warn(msg, RuntimeWarning)
raise
try:
import IPython.display
except ImportError:
pass
import numpy as np
import gzip
import bz2
import zipfile
import copy
import io
from collections import defaultdict
from scipy.stats import rankdata
from .. import Graph, GraphView, PropertyMap, ungroup_vector_property,\
group_vector_property, _prop, _check_prop_vector, map_property_values
from .. generation import label_parallel_edges, label_self_loops
from .. dl_import import dl_import
dl_import("from . import libgraph_tool_draw")
try:
from .libgraph_tool_draw import vertex_attrs, edge_attrs, vertex_shape,\
edge_marker
except ImportError:
msg = "Error importing cairo-based drawing library. " + \
"Was graph-tool compiled with cairomm support?"
warnings.warn(msg, RuntimeWarning)
from .. draw import sfdp_layout, random_layout, _avg_edge_distance, \
radial_tree_layout, prop_to_size
from .. generation import graph_union
from .. topology import shortest_path
_vdefaults = {
"shape": "circle",
"color": (0.6, 0.6, 0.6, 0.8),
"fill_color": (0.6470588235294118, 0.058823529411764705, 0.08235294117647059, 0.8),
"size": 5,
"aspect": 1.,
"rotation": 0.,
"anchor": 1,
"pen_width": 0.8,
"halo": 0,
"halo_color": [0., 0., 1., 0.5],
"halo_size": 1.5,
"text": "",
"text_color": [0., 0., 0., 1.],
"text_position": -1.,
"text_rotation": 0.,
"text_offset": [0., 0.],
"text_out_width": .1,
"text_out_color": [0., 0., 0., 0.],
"font_family": "serif",
"font_slant": cairo.FONT_SLANT_NORMAL,
"font_weight": cairo.FONT_WEIGHT_NORMAL,
"font_size": 12.,
"surface": None,
"pie_fractions": [0.75, 0.25],
"pie_colors": [default_cm(x) for x in range(10)]
}
_edefaults = {
"color": (0.1796875, 0.203125, 0.2109375, 0.8),
"pen_width": 1,
"start_marker": "none",
"mid_marker": "none",
"end_marker": "none",
"marker_size": 4.,
"mid_marker_pos": .5,
"control_points": [],
"gradient": [],
"dash_style": [],
"text": "",
"text_color": (0., 0., 0., 1.),
"text_distance": 5,
"text_parallel": True,
"text_out_width": .1,
"text_out_color": [0., 0., 0., 0.],
"font_family": "serif",
"font_slant": cairo.FONT_SLANT_NORMAL,
"font_weight": cairo.FONT_WEIGHT_NORMAL,
"font_size": 12.,
"sloppy": False,
"seamless": False
}
_vtypes = {
"shape": "int",
"color": "vector<double>",
"fill_color": "vector<double>",
"size": "double",
"aspect": "double",
"rotation": "double",
"anchor": "double",
"pen_width": "double",
"halo": "bool",
"halo_color": "vector<double>",
"halo_size": "double",
"text": "string",
"text_color": "vector<double>",
"text_position": "double",
"text_rotation": "double",
"text_offset": "vector<double>",
"text_out_width": "double",
"text_out_color": "vector<double>",
"font_family": "string",
"font_slant": "int",
"font_weight": "int",
"font_size": "double",
"surface": "object",
"pie_fractions": "vector<double>",
"pie_colors": "vector<double>"
}
_etypes = {
"color": "vector<double>",
"pen_width": "double",
"start_marker": "int",
"mid_marker": "int",
"end_marker": "int",
"marker_size": "double",
"mid_marker_pos": "double",
"control_points": "vector<double>",
"gradient": "vector<double>",
"dash_style": "vector<double>",
"text": "string",
"text_color": "vector<double>",
"text_distance": "double",
"text_parallel": "bool",
"text_out_width": "double",
"text_out_color": "vector<double>",
"font_family": "string",
"font_slant": "int",
"font_weight": "int",
"font_size": "double",
"sloppy": "bool",
"seamless": "bool"
}
for k in list(_vtypes.keys()):
_vtypes[getattr(vertex_attrs, k)] = _vtypes[k]
for k in list(_etypes.keys()):
_etypes[getattr(edge_attrs, k)] = _etypes[k]
def shape_from_prop(shape, enum):
if isinstance(shape, PropertyMap):
g = shape.get_graph()
if shape.key_type() == "v":
prop = g.new_vertex_property("int")
descs = g.vertices()
else:
descs = g.edges()
prop = g.new_edge_property("int")
if shape.value_type() == "string":
def conv(x):
return int(getattr(enum, x))
map_property_values(shape, prop, conv)
else:
rg = (min(enum.values.keys()),
max(enum.values.keys()))
g.copy_property(shape, prop)
if prop.fa.min() < rg[0]:
prop.fa += rg[0]
prop.fa -= rg[0]
prop.fa %= rg[1] - rg[0] + 1
prop.fa += rg[0]
return prop
if isinstance(shape, str):
return int(getattr(enum, shape))
else:
return shape
raise ValueError("Invalid value for attribute %s: %s" %
(repr(enum), repr(shape)))
def open_file(name, mode="r"):
name = os.path.expanduser(name)
base, ext = os.path.splitext(name)
if ext == ".gz":
out = gzip.GzipFile(name, mode)
name = base
elif ext == ".bz2":
out = bz2.BZ2File(name, mode)
name = base
elif ext == ".zip":
out = zipfile.ZipFile(name, mode)
name = base
else:
out = open(name, mode)
fmt = os.path.splitext(name)[1].replace(".", "")
return out, fmt
def get_file_fmt(name):
name = os.path.expanduser(name)
base, ext = os.path.splitext(name)
if ext == ".gz":
name = base
elif ext == ".bz2":
name = base
elif ext == ".zip":
name = base
fmt = os.path.splitext(name)[1].replace(".", "")
return fmt
def surface_from_prop(surface):
if isinstance(surface, PropertyMap):
if surface.key_type() == "v":
prop = surface.get_graph().new_vertex_property("object")
descs = surface.get_graph().vertices()
else:
descs = surface.get_graph().edges()
prop = surface.get_graph().new_edge_property("object")
surface_map = {}
for v in descs:
if surface.value_type() == "string":
if surface[v] not in surface_map:
sfc = gen_surface(surface[v])
surface_map[surface[v]] = sfc
prop[v] = surface_map[surface[v]]
elif surface.value_type() == "python::object":
if isinstance(surface[v], cairo.Surface):
prop[v] = surface[v]
elif surface[v] is not None:
raise ValueError("Invalid value type for surface property: " +
str(type(surface[v])))
else:
raise ValueError("Invalid value type for surface property: " +
surface.value_type())
return prop
if isinstance(surface, str):
return gen_surface(surface)
elif isinstance(surface, cairo.Surface) or surface is None:
return surface
raise ValueError("Invalid value for attribute surface: " + repr(surface))
def centered_rotation(g, pos, text_pos=True):
x, y = ungroup_vector_property(pos, [0, 1])
cm = (x.fa.mean(), y.fa.mean())
dx = x.fa - cm[0]
dy = y.fa - cm[1]
angle = g.new_vertex_property("double")
angle.fa = numpy.arctan2(dy, dx)
pi = numpy.pi
angle.fa += 2 * pi
angle.fa %= 2 * pi
if text_pos:
idx = (angle.a > pi / 2 ) * (angle.a < 3 * pi / 2)
tpos = g.new_vertex_property("double")
angle.a[idx] += pi
tpos.a[idx] = pi
return angle, tpos
return angle
def choose_cm(prop, cm):
is_bin = False
if prop.value_type() in ["double", "long double"]:
is_seq = True
else:
ph = perfect_prop_hash([prop])[0]
if ph.fa.max() >= default_cm.N:
is_seq = True
else:
is_seq = False
if ph.fa.max() == 1:
is_bin = True
if cm is not None:
return cm, is_seq
if prop.key_type() == "e":
if is_bin:
cm = matplotlib.cm.RdGy
is_seq = True
elif not is_seq:
cm = default_cm
else:
cm = matplotlib.cm.binary
else:
if is_bin:
cm = default_cm_bin
elif not is_seq:
cm = default_cm
else:
cm = matplotlib.cm.magma
return cm, is_seq
def _convert(attr, val, cmap, vnorm, pmap_default=False, g=None, k=None):
try:
cmap, alpha = cmap
except TypeError:
alpha = None
if attr == vertex_attrs.shape:
new_val = shape_from_prop(val, vertex_shape)
if pmap_default and not isinstance(val, PropertyMap):
new_val = g.new_vertex_property("int", new_val)
return new_val
elif attr == vertex_attrs.surface:
new_val = surface_from_prop(val)
if pmap_default and not isinstance(val, PropertyMap):
new_val = g.new_vertex_property("python::object", new_val)
return new_val
elif attr in [edge_attrs.start_marker, edge_attrs.mid_marker,
edge_attrs.end_marker]:
new_val = shape_from_prop(val, edge_marker)
if pmap_default and not isinstance(val, PropertyMap):
new_val = g.new_edge_property("int", new_val)
return new_val
elif attr in [vertex_attrs.pie_colors]:
if isinstance(val, PropertyMap):
if val.value_type() in ["vector<double>", "vector<long double>"]:
return val
if val.value_type() in ["vector<int16_t>", "vector<int32_t>",
"vector<int64_t>", "vector<bool>"]:
g = val.get_graph()
new_val = g.new_vertex_property("vector<double>")
rg = [numpy.inf, -numpy.inf]
for v in g.vertices():
for x in val[v]:
rg[0] = min(x, rg[0])
rg[1] = max(x, rg[1])
if rg[0] == rg[1]:
rg[1] = 1
if cmap is None:
cmap = default_cm
map_property_values(val, new_val,
lambda y: flatten([cmap((x - rg[0]) / (rg[1] - rg[0]),
alpha=alpha) for x in y]))
return new_val
if val.value_type() == "vector<string>":
g = val.get_graph()
new_val = g.new_vertex_property("vector<double>")
map_property_values(val, new_val,
lambda y: flatten([color_converter.to_rgba(x) for x in y]))
return new_val
if val.value_type() == "python::object":
try:
g = val.get_graph()
new_val = g.new_vertex_property("vector<double>")
def conv(y):
try:
new_val[v] = [float(x) for x in flatten(y)]
except ValueError:
new_val[v] = flatten([color_converter.to_rgba(x) for x in y])
map_property_values(val, new_val, conv)
return new_val
except ValueError:
pass
else:
try:
new_val = [float(x) for x in flatten(val)]
except ValueError:
try:
new_val = flatten(color_converter.to_rgba(x) for x in val)
new_val = list(new_val)
except ValueError:
pass
if pmap_default:
val_a = numpy.zeros((g.num_vertices(), len(new_val)))
for i in range(len(new_val)):
val_a[:, i] = new_val[i]
return g.new_vertex_property("vector<double>", val_a)
else:
return new_val
elif attr in [vertex_attrs.color, vertex_attrs.fill_color,
vertex_attrs.text_color, vertex_attrs.text_out_color,
vertex_attrs.halo_color, edge_attrs.color,
edge_attrs.text_color, edge_attrs.text_out_color]:
if isinstance(val, list):
new_val = val
elif isinstance(val, (tuple, np.ndarray)):
new_val = list(val)
elif isinstance(val, str):
new_val = list(color_converter.to_rgba(val))
elif isinstance(val, PropertyMap):
if val.value_type() in ["vector<double>", "vector<long double>"]:
new_val = val
elif val.value_type() in ["int16_t", "int32_t", "int64_t", "double",
"long double", "unsigned long",
"unsigned int", "bool"]:
cmap, is_seq = choose_cm(val, cmap)
g = val.get_graph()
if val.value_type() in ["int16_t", "int32_t", "int64_t",
"unsigned long", "unsigned int"]:
if not is_seq:
nval = val.copy()
nval.fa = rankdata(val.fa, method='dense') - 1
else:
nval = val
else:
nval = val
try:
vrange = [nval.fa.min(), nval.fa.max()]
except (AttributeError, ValueError):
#vertex index
vrange = [int(g.vertex(0, use_index=False)),
int(g.vertex(g.num_vertices() - 1,
use_index=False))]
if vnorm is None:
if not is_seq:
cnorm = lambda x : x % cmap.N
else:
cnorm = matplotlib.colors.Normalize(vmin=vrange[0],
vmax=vrange[1])
else:
cnorm = vnorm
cnorm(vrange) # for auto-scale, if needed
g = val.get_graph()
if val.key_type() == "v":
prop = g.new_vertex_property("vector<double>")
else:
prop = g.new_edge_property("vector<double>")
map_property_values(nval, prop, lambda x: cmap(cnorm(x),
alpha=alpha))
new_val = prop
elif val.value_type() == "string":
g = val.get_graph()
if val.key_type() == "v":
prop = g.new_vertex_property("vector<double>")
else:
prop = g.new_edge_property("vector<double>")
map_property_values(val, prop,
lambda x: color_converter.to_rgba(x))
new_val = prop
else:
raise ValueError("Invalid value for attribute %s: %s" %
(repr(attr), repr(val)))
if pmap_default and not isinstance(val, PropertyMap):
if attr in [vertex_attrs.color, vertex_attrs.fill_color,
vertex_attrs.text_color, vertex_attrs.halo_color]:
val_a = numpy.zeros((g.num_vertices(),len(new_val)))
for i in range(len(new_val)):
val_a[:, i] = new_val[i]
return g.new_vertex_property("vector<double>", val_a)
else:
val_a = numpy.zeros((g.num_edges(), len(new_val)))
for i in range(len(new_val)):
val_a[:,i] = new_val[i]
return g.new_edge_property("vector<double>", val_a)
else:
return new_val
if pmap_default and not isinstance(val, PropertyMap):
if k == "v":
new_val = g.new_vertex_property(_vtypes[attr], val=val)
else:
new_val = g.new_edge_property(_etypes[attr], val=val)
return new_val
return val
def _attrs(attrs, d, g, cmap, vnorm):
nattrs = {}
defaults = {}
for k, v in attrs.items():
try:
if d == "v":
attr = getattr(vertex_attrs, k)
else:
attr = getattr(edge_attrs, k)
except AttributeError:
warnings.warn("Unknown attribute: " + str(k), UserWarning)
continue
if isinstance(v, PropertyMap):
nattrs[int(attr)] = _prop(d, g, _convert(attr, v, cmap, vnorm))
else:
defaults[int(attr)] = _convert(attr, v, cmap, vnorm)
return nattrs, defaults
def _convert_props(props, d, g, cmap, vnorm, pmap_default=False):
nprops = {}
for k, v in props.items():
try:
if d == "v":
attr = getattr(vertex_attrs, k)
else:
attr = getattr(edge_attrs, k)
nprops[k] = _convert(attr, v, cmap, vnorm, pmap_default=pmap_default,
g=g, k=d)
except AttributeError:
kind = "vertex" if d == k else "edge"
warnings.warn(f"Unknown {kind} attribute: " + str(k), UserWarning)
continue
return nprops
def get_attr(attr, d, attrs, defaults):
if attr in attrs:
p = attrs[attr]
else:
p = defaults[attr]
if isinstance(p, PropertyMap):
return p[d]
else:
return p
def position_parallel_edges(g, pos, loop_angle=float("nan"),
parallel_distance=1):
lp = label_parallel_edges(GraphView(g, directed=False))
ll = label_self_loops(g)
if isinstance(loop_angle, PropertyMap):
angle = loop_angle
else:
angle = g.new_vertex_property("double", float(loop_angle))
g = GraphView(g, directed=True)
if ((len(lp.fa) == 0 or lp.fa.max() == 0) and
(len(ll.fa) == 0 or ll.fa.max() == 0)):
return []
else:
spline = g.new_edge_property("vector<double>")
libgraph_tool_draw.put_parallel_splines(g._Graph__graph,
_prop("v", g, pos),
_prop("e", g, lp),
_prop("e", g, spline),
_prop("v", g, angle),
parallel_distance)
return spline
def parse_props(prefix, args):
props = {}
others = {}
for k, v in list(args.items()):
if v is None:
continue
if k.startswith(prefix + "_"):
props[k.replace(prefix + "_", "")] = v
else:
others[k] = v
return props, others
[docs]
def cairo_draw(g, pos, cr, vprops=None, eprops=None, vorder=None, eorder=None,
nodesfirst=False, vcmap=None, vcnorm=None, ecmap=None,
ecnorm=None, loop_angle=numpy.nan, parallel_distance=None, res=0,
max_render_time=-1, **kwargs):
r"""Draw a graph to a :mod:`cairo` context.
Parameters
----------
g : :class:`~graph_tool.Graph`
Graph to be drawn.
pos : :class:`~graph_tool.VertexPropertyMap`
Vector-valued vertex property map containing the x and y coordinates of
the vertices.
cr : :class:`~cairo.Context`
A :class:`~cairo.Context` instance.
vprops : dict (optional, default: ``None``)
Dictionary with the vertex properties. Individual properties may also be
given via the ``vertex_<prop-name>`` parameters, where ``<prop-name>`` is
the name of the property.
eprops : dict (optional, default: ``None``)
Dictionary with the edge properties. Individual properties may also be
given via the ``edge_<prop-name>`` parameters, where ``<prop-name>`` is
the name of the property.
vorder : :class:`~graph_tool.VertexPropertyMap` (optional, default: ``None``)
If provided, defines the relative order in which the vertices are drawn.
eorder : :class:`~graph_tool.EdgePropertyMap` (optional, default: ``None``)
If provided, defines the relative order in which the edges are drawn.
nodesfirst : bool (optional, default: ``False``)
If ``True``, the vertices are drawn first, otherwise the edges are.
vcmap : :class:`matplotlib.colors.Colormap` or tuple (optional, default: ``None``)
Vertex color map. Optionally, this may be a
(:class:`matplotlib.colors.Colormap`, alpha) tuple.
vcnorm : :class:`matplotlib.colors.Normalize` (optional, default: ``None``)
An object which, when called, normalizes vertex property values into the
:math:`[0.0, 1.0]` interval, before passing to the ``vcmap`` colormap.
ecmap : :class:`matplotlib.colors.Colormap` or tuple (optional, default: ``None``)
Edge color map. Optionally, this may be a
(:class:`matplotlib.colors.Colormap`, alpha) tuple.
ecnorm : :class:`matplotlib.colors.Normalize` (optional, default: ``None``)
An object which, when called, normalizes edge property values into the
:math:`[0.0, 1.0]` interval, before passing to the ``ecmap`` colormap.
loop_angle : float or :class:`~graph_tool.EdgePropertyMap` (optional, default: ``numpy.nan``)
Angle used to draw self-loops. If ``nan`` is given, they will be placed
radially from the center of the layout.
parallel_distance : float (optional, default: ``None``)
Distance used between parallel edges. If not provided, it will be
determined automatically.
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.
max_render_time : int (optional, default: ``-1``):
If nonnegative, this function will return an iterator that will perform
part of the drawing at each step, so that each iteration takes at most
``max_render_time`` milliseconds.
vertex_* : :class:`~graph_tool.VertexPropertyMap` 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
``vprops`` parameter.
edge_* : :class:`~graph_tool.EdgePropertyMap` or arbitrary types (optional, default: ``None``)
Parameters following the pattern ``edge_<prop-name>`` specify the edge
property with name ``<prop-name>``, as an alternative to the ``eprops``
parameter.
Returns
-------
iterator :
If ``max_render_time`` is nonnegative, this will be an iterator that will
perform part of the drawing at each step, so that each iteration takes
at most ``max_render_time`` milliseconds.
"""
if vorder is not None:
_check_prop_scalar(vorder, name="vorder")
vprops = {} if vprops is None else copy.copy(vprops)
eprops = {} if eprops is None else copy.copy(eprops)
props, kwargs = parse_props("vertex", kwargs)
vprops.update(props)
props, kwargs = parse_props("edge", kwargs)
eprops.update(props)
for k in kwargs:
warnings.warn("Unknown parameter: " + k, UserWarning)
cr.save()
if "control_points" not in eprops:
if parallel_distance is None:
parallel_distance = vprops.get("size", _vdefaults["size"])
if isinstance(parallel_distance, PropertyMap):
parallel_distance = parallel_distance.fa.mean()
parallel_distance /= 1.5
M = cr.get_matrix()
scale = transform_scale(M, 1,)
parallel_distance /= scale
eprops["control_points"] = position_parallel_edges(g, pos, loop_angle,
parallel_distance)
if g.is_directed() and "end_marker" not in eprops:
eprops["end_marker"] = "arrow"
if vprops.get("text_position", None) == "centered":
angle, tpos = centered_rotation(g, pos, text_pos=True)
vprops["text_position"] = tpos
vprops["text_rotation"] = angle
toffset = vprops.get("text_offset", None)
if toffset is not None:
if not isinstance(toffset, PropertyMap):
toffset = g.new_vp("vector<double>", val=toffset)
xo, yo = ungroup_vector_property(toffset, [0, 1])
xo.a[tpos.a == numpy.pi] *= -1
toffset = group_vector_property([xo, yo])
vprops["text_offset"] = toffset
vattrs, vdefaults = _attrs(vprops, "v", g, vcmap, vcnorm)
eattrs, edefaults = _attrs(eprops, "e", g, ecmap, ecnorm)
vdefs = _attrs(_vdefaults, "v", g, vcmap, vcnorm)[1]
vdefs.update(vdefaults)
edefs = _attrs(_edefaults, "e", g, ecmap, ecnorm)[1]
edefs.update(edefaults)
if "control_points" not in eprops:
if parallel_distance is None:
parallel_distance = _defaults
eprops["control_points"] = position_parallel_edges(g, pos, loop_angle,
parallel_distance)
generator = 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,
max_render_time, cr)
if max_render_time >= 0:
def gen():
for count in generator:
yield count
cr.restore()
return gen()
else:
for count in generator:
pass
cr.restore()
def color_contrast(color):
c = np.asarray(color)
y = c[0] * .299 + c[1] * .587 + c[2] * .114
if y < .5:
c[:3] = 1
else:
c[:3] = 0
return c
def auto_colors(g, bg, pos, back):
if not isinstance(bg, PropertyMap):
if isinstance(bg, str):
bg = color_converter.to_rgba(bg)
bg = g.new_vertex_property("vector<double>", val=bg)
if not isinstance(pos, PropertyMap):
if pos == "centered":
pos = 0
pos = g.new_vertex_property("double", pos)
bg_a = bg.get_2d_array(range(4))
bgc_pos = numpy.zeros((g.num_vertices(), 5))
for i in range(4):
bgc_pos[:, i] = bg_a[i, :]
bgc_pos[:, 4] = pos.fa
bgc_pos = g.new_vertex_property("vector<double>", bgc_pos)
def conv(x):
bgc = x[:4]
p = x[4]
if p < 0:
return color_contrast(bgc)
else:
return color_contrast(back)
c = g.new_vertex_property("vector<double>")
map_property_values(bgc_pos, c, conv)
return c
[docs]
def graph_draw(g, pos=None, vprops=None, eprops=None, vorder=None, eorder=None,
nodesfirst=False, output_size=(600, 600), fit_view=True,
fit_view_ink=None, adjust_aspect=True, ink_scale=1,
inline=has_draw_inline, inline_scale=2, mplfig=None,
yflip=True, output=None, fmt="auto", bg_color=None,
antialias=None, **kwargs):
r"""Draw a graph to screen or to a file using :mod:`cairo`.
Parameters
----------
g : :class:`~graph_tool.Graph`
Graph to be drawn.
pos : :class:`~graph_tool.VertexPropertyMap` (optional, default: ``None``)
Vector-valued vertex property map containing the x and y coordinates of
the vertices. If not given, it will be computed using :func:`sfdp_layout`.
vprops : dict (optional, default: ``None``)
Dictionary with the vertex properties. Individual properties may also be
given via the ``vertex_<prop-name>`` parameters, where ``<prop-name>`` is
the name of the property.
eprops : dict (optional, default: ``None``)
Dictionary with the edge properties. Individual properties may also be
given via the ``edge_<prop-name>`` parameters, where ``<prop-name>`` is
the name of the property.
vorder : :class:`~graph_tool.VertexPropertyMap` (optional, default: ``None``)
If provided, defines the relative order in which the vertices are drawn.
eorder : :class:`~graph_tool.EdgePropertyMap` (optional, default: ``None``)
If provided, defines the relative order in which the edges are drawn.
nodesfirst : bool (optional, default: ``False``)
If ``True``, the vertices are drawn first, otherwise the edges are.
output_size : tuple of scalars (optional, default: ``(600,600)``)
Size of the drawing canvas. The units will depend on the output format
(pixels for the screen, points for PDF, etc).
fit_view : bool, float or tuple (optional, default: ``True``)
If ``True``, the layout will be scaled to fit the entire clip region.
If a float value is given, it will be interpreted as ``True``, and in
addition the viewport will be scaled out by that factor. If a tuple
value is given, it should have four values ``(x, y, w, h)`` that
specify the view in user coordinates.
fit_view_ink : bool (optional, default: ``None``)
If ``True``, and ``fit_view == True`` the drawing will be performed once
to figure out the bounding box, before the actual drawing is
made. Otherwise, only the vertex positions will be used for this
purpose. If the value is ``None``, then it will be assumed ``True`` for
networks of size 10,000 nodes or less, otherwise it will be assumed
``False``.
adjust_aspect : bool (optional, default: ``True``)
If ``True``, and ``fit_view == True`` the output size will be decreased
in the width or height to remove empty spaces.
ink_scale : float (optional, default: ``1.``)
Scale all sizes and widths by this factor.
inline : bool (optional, default: ``False``)
If ``True`` and an `IPython notebook <http://ipython.org/notebook>`_ is
being used, an inline version of the drawing will be returned.
inline_scale : float (optional, default: ``2.``):
Resolution scaling factor for inline images.
mplfig : :mod:`matplotlib` container object (optional, default: ``None``)
The ``mplfig`` object needs to have an ``add_artist()`` function. This
can for example be a :class:`matplotlib.figure.Figure` or
:class:`matplotlib.axes.Axes`. Only the cairo backend is supported; use
``switch_backend('cairo')``. If this option is used, a
:class:`~graph_tool.draw.GraphArtist` object is returned.
yflip : bool (optional, default: ``True``)
If ``mplfig is not None``, and ``fit_view != False``, then the y
direction of the axis will be flipped, reproducing the same output as
when ``mplfig is None``.
output : string or file object (optional, default: ``None``)
Output file name (or object). If not given, the graph will be displayed via
:func:`interactive_window`.
fmt : string (default: ``"auto"``)
Output file format. Possible values are ``"auto"``, ``"ps"``, ``"pdf"``,
``"svg"``, and ``"png"``. If the value is ``"auto"``, the format is
guessed from the ``output`` parameter.
bg_color : str or sequence (optional, default: ``None``)
Background color. The default is transparent.
antialias : :class:`cairo.Antialias` (optional, default: ``None``)
If supplied, this will set the antialising mode of the cairo context.
vertex_* : :class:`~graph_tool.VertexPropertyMap` 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
``vprops`` parameter.
edge_* : :class:`~graph_tool.EdgePropertyMap` or arbitrary types (optional, default: ``None``)
Parameters following the pattern ``edge_<prop-name>`` specify the edge
property with name ``<prop-name>``, as an alternative to the ``eprops``
parameter.
**kwargs
Any extra parameters are passed to :func:`~graph_tool.draw.interactive_window`,
:class:`~graph_tool.draw.GraphWindow`, :class:`~graph_tool.draw.GraphWidget`
and :func:`~graph_tool.draw.cairo_draw`.
Returns
-------
pos : :class:`~graph_tool.VertexPropertyMap`
Vector vertex property map with the x and y coordinates of the vertices.
selected : :class:`~graph_tool.VertexPropertyMap` (optional, only if ``output is None``)
Boolean-valued vertex property map marking the vertices which were
selected interactively.
Notes
-----
.. rubric:: List of vertex properties
.. table::
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| Name | Description | Accepted types | Default Value |
+================+===================================================+========================+==================================+
| shape | The vertex shape. Can be one of the following | ``str`` or ``int`` | ``"circle"`` |
| | strings: "circle", "triangle", "square", | | |
| | "pentagon", "hexagon", "heptagon", "octagon" | | |
| | "double_circle", "double_triangle", | | |
| | "double_square", "double_pentagon", | | |
| | "double_hexagon", "double_heptagon", | | |
| | "double_octagon", "pie", "none". | | |
| | Optionally, this might take a numeric value | | |
| | corresponding to position in the list above. | | |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| color | Color used to stroke the lines of the vertex. | ``str`` or list of | ``[0., 0., 0., 1]`` |
| | | ``floats`` | |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| fill_color | Color used to fill the interior of the vertex. | ``str`` or list of | ``[0.640625, 0, 0, 0.9]`` |
| | | ``floats`` | |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| size | The size of the vertex, in the default units of | ``float`` or ``int`` | ``5`` |
| | the output format (normally either pixels or | | |
| | points). | | |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| aspect | The aspect ratio of the vertex. | ``float`` or ``int`` | ``1.0`` |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| rotation | Angle (in radians) to rotate the vertex. | ``float`` | ``0.`` |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| anchor | Specifies how the edges anchor to the vertices. | ``int`` | ``1`` |
| | If `0`, the anchor is at the center of the vertex,| | |
| | otherwise it is at the border. | | |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| pen_width | Width of the lines used to draw the vertex, in | ``float`` or ``int`` | ``0.8`` |
| | the default units of the output format (normally | | |
| | either pixels or points). | | |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| halo | Whether to draw a circular halo around the | ``bool`` | ``False`` |
| | vertex. | | |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| halo_color | Color used to draw the halo. | ``str`` or list of | ``[0., 0., 1., 0.5]`` |
| | | ``floats`` | |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| halo_size | Relative size of the halo. | ``float`` | ``1.5`` |
| | | | |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| text | Text to draw together with the vertex. | ``str`` | ``""`` |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| text_color | Color used to draw the text. If the value is | ``str`` or list of | ``"auto"`` |
| | ``"auto"``, it will be computed based on | ``floats`` | |
| | fill_color to maximize contrast. | | |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| text_position | Position of the text relative to the vertex. | ``float`` or ``int`` | ``-1`` |
| | If the passed value is positive, it will | or ``"centered"`` | |
| | correspond to an angle in radians, which will | | |
| | determine where the text will be placed outside | | |
| | the vertex. If the value is negative, the text | | |
| | will be placed inside the vertex. If the value is | | |
| | ``-1``, the vertex size will be automatically | | |
| | increased to accommodate the text. The special | | |
| | value ``"centered"`` positions the texts rotated | | |
| | radially around the center of mass. | | |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| text_offset | Text position offset. | list of ``float`` | ``[0.0, 0.0]`` |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| text_rotation | Angle of rotation (in radians) for the text. | ``float`` | ``0.0`` |
| | The center of rotation is the position of the | | |
| | vertex. | | |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| text_out_color | Color used to draw the text outline. | ``str`` or list of | ``[0,0,0,0]`` |
| | | ``floats`` | |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| text_out_width | Width of the text outline. | ``float`` | ``1.`` |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| font_family | Font family used to draw the text. | ``str`` | ``"serif"`` |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| font_slant | Font slant used to draw the text. | ``cairo.FONT_SLANT_*`` | :data:`cairo.FONT_SLANT_NORMAL` |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| font_weight | Font weight used to draw the text. | ``cairo.FONT_WEIGHT_*``| :data:`cairo.FONT_WEIGHT_NORMAL` |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| font_size | Font size used to draw the text. | ``float`` or ``int`` | ``12`` |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| surface | The cairo surface used to draw the vertex. If | :class:`cairo.Surface` | ``None`` |
| | the value passed is a string, it is interpreted | or ``str`` | |
| | as an image file name to be loaded. | | |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| pie_fractions | Fractions of the pie sections for the vertices if | list of ``int`` or | ``[0.75, 0.25]`` |
| | ``shape=="pie"``. | ``float`` | |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| pie_colors | Colors used in the pie sections if | list of strings or | ``('b','g','r','c','m','y','k')``|
| | ``shape=="pie"``. | ``float``. | |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
.. rubric:: List of edge properties
.. table::
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| Name | Description | Accepted types | Default Value |
+================+===================================================+========================+==================================+
| color | Color used to stroke the edge lines. | ``str`` or list of | ``[0.179, 0.203, 0.210, 0.8]`` |
| | | floats | |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| pen_width | Width of the line used to draw the edge, in | ``float`` or ``int`` | ``1.0`` |
| | the default units of the output format (normally | | |
| | either pixels or points). | | |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| start_marker, | Edge markers. Can be one of "none", "arrow", | ``str`` or ``int`` | ``none`` |
| mid_marker, | "circle", "square", "diamond", or "bar". | | |
| end_marker | Optionally, this might take a numeric value | | |
| | corresponding to position in the list above. | | |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| mid_marker_pos | Relative position of the middle marker. | ``float`` | ``0.5`` |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| marker_size | Size of edge markers, in units appropriate to the | ``float`` or ``int`` | ``4`` |
| | output format (normally either pixels or points). | | |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| control_points | Control points of a Bézier spline used to draw | sequence of ``floats`` | ``[]`` |
| | the edge. Each spline segment requires 6 values | | |
| | corresponding to the (x,y) coordinates of the two | | |
| | intermediary control points and the final point. | | |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| gradient | Stop points of a linear gradient used to stroke | sequence of ``floats`` | ``[]`` |
| | the edge. Each group of 5 elements is interpreted | | |
| | as ``[o, r, g, b, a]`` where ``o`` is the offset | | |
| | in the range [0, 1] and the remaining values | | |
| | specify the colors. | | |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| dash_style | Dash pattern is specified by an array of positive | sequence of ``floats`` | ``[]`` |
| | values. Each value provides the length of | | |
| | alternate "on" and "off" portions of the stroke. | | |
| | The last value specifies an offset into the | | |
| | pattern at which the stroke begins. | | |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| text | Text to draw next to the edges. | ``str`` | ``""`` |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| text_color | Color used to draw the text. | ``str`` or list of | ``[0., 0., 0., 1.]`` |
| | | ``floats`` | |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| text_distance | Distance from the edge and its text. | ``float`` or ``int`` | ``4`` |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| text_parallel | If ``True`` the text will be drawn parallel to | ``bool`` | ``True`` |
| | the edges. | | |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| text_out_color | Color used to draw the text outline. | ``str`` or list of | ``[0,0,0,0]`` |
| | | ``floats`` | |
+--------------- +---------------------------------------------------+------------------------+----------------------------------+
| text_out_width | Width of the text outline. | ``float`` | ``1.`` |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| font_family | Font family used to draw the text. | ``str`` | ``"serif"`` |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| font_slant | Font slant used to draw the text. | ``cairo.FONT_SLANT_*`` | :data:`cairo.FONT_SLANT_NORMAL` |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| font_weight | Font weight used to draw the text. | ``cairo.FONT_WEIGHT_*``| :data:`cairo.FONT_WEIGHT_NORMAL` |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
| font_size | Font size used to draw the text. | ``float`` or ``int`` | ``12`` |
+----------------+---------------------------------------------------+------------------------+----------------------------------+
Examples
--------
.. testcode::
:hide:
np.random.seed(42)
gt.seed_rng(42)
from numpy import sqrt
>>> g = gt.price_network(1500)
>>> deg = g.degree_property_map("in")
>>> deg.a = 4 * (sqrt(deg.a) * 0.5 + 0.4)
>>> ebet = gt.betweenness(g)[1]
>>> ebet.a /= ebet.a.max() / 10.
>>> eorder = ebet.copy()
>>> eorder.a *= -1
>>> pos = gt.sfdp_layout(g)
>>> control = g.new_edge_property("vector<double>")
>>> for e in g.edges():
... d = sqrt(sum((pos[e.source()].a - pos[e.target()].a) ** 2)) / 5
... control[e] = [0.3, d, 0.7, d]
>>> gt.graph_draw(g, pos=pos, vertex_size=deg, vertex_fill_color=deg, vorder=deg,
... edge_color=ebet, eorder=eorder, edge_pen_width=ebet,
... edge_control_points=control, # some curvy edges
... output="graph-draw.pdf")
<...>
.. testcleanup::
conv_png("graph-draw.pdf")
.. figure:: graph-draw.png
:align: center
:width: 80%
SFDP force-directed layout of a Price network with 1500 nodes. The
vertex size and color indicate the degree, and the edge color and width
the edge betweenness centrality.
"""
vprops = vprops.copy() if vprops is not None else {}
eprops = eprops.copy() if eprops is not None else {}
props, kwargs = parse_props("vertex", kwargs)
props = _convert_props(props, "v", g, kwargs.get("vcmap", None),
kwargs.get("vcnorm", None))
vprops.update(props)
props, kwargs = parse_props("edge", kwargs)
props = _convert_props(props, "e", g, kwargs.get("ecmap", None),
kwargs.get("ecnorm", None))
eprops.update(props)
if pos is None:
if (g.num_vertices() > 2 and output is None and
not inline and kwargs.get("update_layout", True) and
mplfig is None):
L = np.sqrt(g.num_vertices())
pos = random_layout(g, [L, L])
if g.num_vertices() > 1000:
if "multilevel" not in kwargs:
kwargs["multilevel"] = True
if "layout_K" not in kwargs:
kwargs["layout_K"] = _avg_edge_distance(g, pos) / 10
else:
pos = sfdp_layout(g)
else:
pos = g.own_property(pos)
_check_prop_vector(pos, name="pos", floating=True)
if output is None and not inline and mplfig is None:
if "layout_K" not in kwargs:
kwargs["layout_K"] = _avg_edge_distance(g, pos)
if "update_layout" not in kwargs:
kwargs["update_layout"] = False
if "pen_width" in eprops and "marker_size" not in eprops:
pw = eprops["pen_width"]
if isinstance(pw, PropertyMap):
pw = pw.copy("double")
pw.fa *= 2.75
eprops["marker_size"] = pw
else:
eprops["marker_size"] = pw * 2.75
if "text" in eprops and "text_distance" not in eprops and "pen_width" in eprops:
pw = eprops["pen_width"]
if isinstance(pw, PropertyMap):
pw = pw.copy("double")
pw.fa *= 2
eprops["text_distance"] = pw
else:
eprops["text_distance"] = pw * 2
if "text" in vprops and ("text_color" not in vprops or vprops["text_color"] == "auto"):
vcmap = kwargs.get("vcmap", None)
bg = _convert(vertex_attrs.fill_color,
vprops.get("fill_color", _vdefaults["fill_color"]),
vcmap, kwargs.get("vcnorm", None))
vprops["text_color"] = auto_colors(g, bg,
vprops.get("text_position",
_vdefaults["text_position"]),
bg_color if bg_color is not None else [1., 1., 1., 1.])
if mplfig is not None:
ax = None
if isinstance(mplfig, matplotlib.figure.Figure):
ax = mplfig.gca()
elif isinstance(mplfig, matplotlib.axes.Axes):
ax = mplfig
else:
ax = mplfig
x, y = ungroup_vector_property(pos, [0, 1])
l, r = x.a.min(), x.a.max()
b, t = y.a.min(), y.a.max()
adjust_default_sizes(g, (r - l, t - b), vprops, eprops, min_pen_width=0)
if ink_scale != 1:
scale_ink(ink_scale, vprops, eprops, min_pen_width=0)
artist = GraphArtist(g, pos, ax, vprops, eprops, vorder=vorder,
eorder=eorder, nodesfirst=nodesfirst, **kwargs)
ax.add_artist(artist)
return artist
output_file = output
if inline and output is None:
if fmt == "auto":
if output is None:
fmt = "png"
else:
fmt = get_file_fmt(output)
output = io.BytesIO()
if output is None:
if ink_scale != 1:
scale_ink(ink_scale, vprops, eprops)
return interactive_window(g, pos, vprops, eprops, vorder, eorder,
nodesfirst, geometry=output_size,
fit_view=fit_view, bg_color=bg_color,
**kwargs)
else:
adjust_default_sizes(g, output_size, vprops, eprops)
if ink_scale != 1:
scale_ink(ink_scale, vprops, eprops)
if inline and fmt != "svg":
output_size = [int(x * inline_scale) for x in output_size]
scale_ink(inline_scale, vprops, eprops)
if fit_view != False:
try:
x, y, w, h = fit_view
zoom = min(output_size[0] / w, output_size[1] / h)
except TypeError:
pad = fit_view if fit_view is not True else 0.9
output_size = list(output_size)
if fit_view_ink is None:
fit_view_ink = g.num_vertices() <= 1000
if fit_view_ink:
x, y, zoom = fit_to_view_ink(g, pos, output_size, vprops,
eprops, adjust_aspect, pad=pad)
else:
x, y, zoom = fit_to_view(get_bb(g, pos), output_size,
adjust_aspect=adjust_aspect,
pad=pad)
else:
x, y, zoom = 0, 0, 1
if isinstance(output, str):
out, auto_fmt = open_file(output, mode="wb")
else:
out = output
if fmt == "auto":
raise ValueError("File format must be specified.")
if fmt == "auto":
fmt = auto_fmt
if fmt == "pdf":
srf = cairo.PDFSurface(out, output_size[0], output_size[1])
elif fmt == "ps":
srf = cairo.PSSurface(out, output_size[0], output_size[1])
elif fmt == "eps":
srf = cairo.PSSurface(out, output_size[0], output_size[1])
srf.set_eps(True)
elif fmt == "svg":
srf = cairo.SVGSurface(out, output_size[0], output_size[1])
srf.restrict_to_version(cairo.SVG_VERSION_1_2)
elif fmt == "png":
srf = cairo.ImageSurface(cairo.FORMAT_ARGB32, output_size[0],
output_size[1])
else:
raise ValueError("Invalid format type: " + fmt)
cr = cairo.Context(srf)
if antialias is not None:
cr.set_antialias(antialias)
cr.scale(zoom, zoom)
cr.translate(-x, -y)
if bg_color is not None:
if isinstance(bg_color, str):
bg_color = matplotlib.colors.to_rgba(bg_color)
cr.set_source_rgba(bg_color[0], bg_color[1],
bg_color[2], bg_color[3])
cr.paint()
cairo_draw(g, pos, cr, vprops, eprops, vorder, eorder,
nodesfirst, **kwargs)
srf.flush()
if fmt == "png":
srf.write_to_png(out)
elif fmt == "svg":
srf.finish()
del cr
if inline and output_file is None:
img = None
if fmt == "png":
img = IPython.display.Image(data=out.getvalue(),
width=int(output_size[0]/inline_scale),
height=int(output_size[1]/inline_scale))
elif fmt == "svg":
img = IPython.display.SVG(data=out.getvalue())
elif img is None:
inl_out = io.BytesIO()
inl_srf = cairo.ImageSurface(cairo.FORMAT_ARGB32,
output_size[0],
output_size[1])
inl_cr = cairo.Context(inl_srf)
inl_cr.set_source_surface(srf, 0, 0)
inl_cr.paint()
inl_srf.write_to_png(inl_out)
del inl_srf
img = IPython.display.Image(data=inl_out.getvalue(),
width=int(output_size[0]/inline_scale),
height=int(output_size[1]/inline_scale))
srf.finish()
IPython.display.display(img)
del srf
return pos
def adjust_default_sizes(g, geometry, vprops, eprops, force=False,
min_pen_width=0.05):
if "size" not in vprops or force:
A = geometry[0] * geometry[1]
N = max(g.num_vertices(), 1)
vprops["size"] = np.sqrt(A / N) / 3.5
if "pen_width" not in vprops or force:
size = vprops["size"]
if isinstance(vprops["size"], PropertyMap):
size = vprops["size"].fa.mean()
vprops["pen_width"] = max(size / 10, min_pen_width)
if "pen_width" not in eprops or force:
eprops["pen_width"] = max(size / 10, min_pen_width)
if "marker_size" not in eprops or force:
eprops["marker_size"] = size * 0.8
if "font_size" not in vprops or force:
size = vprops["size"]
if isinstance(vprops["size"], PropertyMap):
size = vprops["size"].fa.mean()
vprops["font_size"] = size * .6
if "font_size" not in eprops or force:
size = vprops["size"]
if isinstance(vprops["size"], PropertyMap):
size = vprops["size"].fa.mean()
eprops["font_size"] = size * .6
def scale_ink(scale, vprops, eprops, copy=True, min_pen_width=0.05):
vink_props = ["size", "pen_width", "font_size", "text_out_width"]
eink_props = ["marker_size", "pen_width", "font_size", "text_distance",
"text_out_width"]
for ink_props, props, defaults in zip([vink_props, eink_props],
[vprops, eprops],
[_vdefaults, _edefaults]):
for p in ink_props:
if p not in props:
props[p] = defaults[p]
if isinstance(props[p], PropertyMap):
if copy:
props[p] = props[p].copy()
props[p].fa *= scale
if p == "pen_width":
x = props[p].fa
x[x<min_pen_width] = min_pen_width
props[p].fa = x
else:
if copy:
props[p] = props[p] * scale
else:
props[p] *= scale
if p == "pen_width":
props[p] = max(props[p],
min_pen_width)
def get_bb(g, pos):
pos_x, pos_y = ungroup_vector_property(pos, [0, 1])
x_range = [pos_x.fa.min(), pos_x.fa.max()]
y_range = [pos_y.fa.min(), pos_y.fa.max()]
return x_range[0], y_range[0], x_range[1] - x_range[0], y_range[1] - y_range[0]
def fit_to_view(rec, output_size, adjust_aspect=False, pad=.9):
x, y, w, h = rec
if adjust_aspect:
if h > w:
output_size[0] = int(round(float(output_size[1] * w / h)))
else:
output_size[1] = int(round(float(output_size[0] * h / w)))
zoom = max(w / output_size[0], h / output_size[1])
if zoom == 0:
zoom = 1
else:
zoom = 1 / zoom
x -= (output_size[0] / zoom - w) / 2
y -= (output_size[1] / zoom - h) / 2
zoom *= pad
x -= (1-pad) / 2 * output_size[0] / zoom
y -= (1-pad) / 2 * output_size[1] / zoom
return x, y, zoom
def fit_to_view_ink(g, pos, output_size, vprops, eprops, adjust_aspect=False,
pad=0.9):
x, y, zoom = fit_to_view(get_bb(g, pos), output_size, pad=pad)
srf = cairo.RecordingSurface(cairo.Content.COLOR_ALPHA,
cairo.Rectangle(-output_size[0] * 5,
-output_size[1] * 5,
output_size[0] * 10,
output_size[1] * 10))
cr = cairo.Context(srf)
cr.scale(zoom, zoom)
cr.translate(-x, -y)
# work around cairo bug with small line widths
def min_lw(lw):
if isinstance(lw, PropertyMap):
lw = lw.copy()
x = lw.fa
x[x < 0.05] = 0.1
lw.fa = x
else:
lw = max(lw, 0.1)
return lw
vprops = dict(vprops, pen_width=min_lw(vprops.get("pen_width", 0.)))
eprops = dict(eprops, pen_width=min_lw(eprops.get("pen_width", 0.)))
cairo_draw(g, pos, cr, vprops, eprops)
bb = list(srf.ink_extents())
bb[0], bb[1] = cr.device_to_user(bb[0], bb[1])
bb[2], bb[3] = cr.device_to_user_distance(bb[2], bb[3])
x, y, zoom = fit_to_view(bb, output_size,
adjust_aspect=adjust_aspect, pad=pad)
return x, y, zoom
def transform_scale(M, scale):
p = M.transform_distance(scale / np.sqrt(2),
scale / np.sqrt(2))
return np.sqrt(p[0] ** 2 + p[1] ** 2)
[docs]
def get_hierarchy_control_points(g, t, tpos, beta=0.8, cts=None, is_tree=True,
max_depth=None):
r"""Return the Bézier spline control points for the edges in ``g``, given
the hierarchical structure encoded in graph `t`.
Parameters
----------
g : :class:`~graph_tool.Graph`
Graph to be drawn.
t : :class:`~graph_tool.Graph`
Directed graph containing the hierarchy of ``g``. It must be a directed
tree with a single root. The direction of the edges point from the root
to the leaves, and the vertices in ``t`` with index in the range
:math:`[0, N-1]`, with :math:`N` being the number of vertices in ``g``,
must correspond to the respective vertex in ``g``.
tpos : :class:`~graph_tool.VertexPropertyMap`
Vector-valued vertex property map containing the x and y coordinates of
the vertices in graph ``t``.
beta : ``float`` (optional, default: ``0.8`` or :class:`~graph_tool.EdgePropertyMap`)
Edge bundling strength. For ``beta == 0`` the edges are straight lines,
and for ``beta == 1`` they strictly follow the hierarchy. This can be
optionally an edge property map, which specified a different bundling
strength for each edge.
cts : :class:`~graph_tool.EdgePropertyMap` (optional, default: ``None``)
Edge property map of type ``vector<double>`` where the control points
will be stored.
is_tree : ``bool`` (optional, default: ``True``)
If ``True``, ``t`` must be a directed tree, otherwise it can be any
connected graph.
max_depth : ``int`` (optional, default: ``None``)
If supplied, only the first ``max_depth`` bottom levels of the hierarchy
will be used.
Returns
-------
cts : :class:`~graph_tool.EdgePropertyMap`
Vector-valued edge property map containing the Bézier spline control
points for the edges in ``g``.
Notes
-----
This is an implementation of the edge-bundling algorithm described in
[holten-hierarchical-2006]_.
Examples
--------
.. testsetup:: nested_cts
gt.seed_rng(42)
np.random.seed(42)
.. doctest:: nested_cts
>>> g = gt.collection.data["netscience"]
>>> g = gt.GraphView(g, vfilt=gt.label_largest_component(g))
>>> state = gt.minimize_nested_blockmodel_dl(g)
>>> t = gt.get_hierarchy_tree(state)[0]
>>> tpos = pos = gt.radial_tree_layout(t, t.vertex(t.num_vertices() - 1, use_index=False), weighted=True)
>>> cts = gt.get_hierarchy_control_points(g, t, tpos)
>>> pos = g.own_property(tpos)
>>> b = state.levels[0].b
>>> shape = b.copy()
>>> shape.a %= 14
>>> gt.graph_draw(g, pos=pos, vertex_fill_color=b, vertex_shape=shape, edge_control_points=cts,
... edge_color=[0, 0, 0, 0.3], vertex_anchor=0, output="netscience_nested_mdl.pdf")
<...>
.. testcleanup:: nested_cts
conv_png("netscience_nested_mdl.pdf")
.. figure:: netscience_nested_mdl.png
:align: center
:width: 80%
Block partition of a co-authorship network, which minimizes the description
length of the network according to the nested (degree-corrected) stochastic blockmodel.
References
----------
.. [holten-hierarchical-2006] Holten, D. "Hierarchical Edge Bundles:
Visualization of Adjacency Relations in Hierarchical Data.", IEEE
Transactions on Visualization and Computer Graphics 12, no. 5, 741–748
(2006). :doi:`10.1109/TVCG.2006.147`
"""
if cts is None:
cts = g.new_edge_property("vector<double>")
if cts.value_type() != "vector<double>":
raise ValueError("cts property map must be of type 'vector<double>' not '%s' " % cts.value_type())
u = GraphView(g, directed=True)
tu = GraphView(t, directed=True)
if not isinstance(beta, PropertyMap):
beta = u.new_edge_property("double", beta)
else:
beta = beta.copy("double")
if max_depth is None:
max_depth = t.num_vertices()
tu = GraphView(tu, skip_vfilt=True)
tpos = tu.own_property(tpos)
libgraph_tool_draw.get_cts(u._Graph__graph,
tu._Graph__graph,
_prop("v", tu, tpos),
_prop("e", u, beta),
_prop("e", u, cts),
is_tree, max_depth)
return cts
#
# The functions and classes below depend on GTK
# =============================================
#
try:
import gi
try:
gi.require_version('Gtk', '3.0')
except ValueError:
raise ImportError
from gi.repository import Gtk, Gdk, GdkPixbuf
from gi.repository import GObject as gobject
from .gtk_draw import *
except (ImportError, RuntimeError) as e:
msg = "Error importing Gtk module: %s; GTK+ drawing will not work." % str(e)
warnings.warn(msg, RuntimeWarning)
def gen_surface(name):
fobj, fmt = open_file(name)
if fmt in ["png", "PNG"]:
sfc = cairo.ImageSurface.create_from_png(fobj)
return sfc
else:
pixbuf = GdkPixbuf.Pixbuf.new_from_file(name)
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, pixbuf.get_width(),
pixbuf.get_height())
cr = cairo.Context(surface)
Gdk.cairo_set_source_pixbuf(cr, pixbuf, 0, 0)
cr.paint()
return surface
#
# matplotlib
# ==========
#
[docs]
class GraphArtist(matplotlib.artist.Artist):
def __init__(self, g, pos, ax, vprops=None, eprops=None, raster=False,
**kwargs):
""":class:`matplotlib.artist.Artist` specialization that draws
:class:`~graph_tool.Graph` instances in a :mod:`matplotlib` figure.
.. warning::
Vector drawing is available only on cairo-based backends. For other
backends, rasterization will be used.
Parameters
----------
g : :class:`~graph_tool.Graph`
Graph to be drawn.
pos : :class:`~graph_tool.VertexPropertyMap`
Vector-valued vertex property map containing the x and y coordinates
of the vertices.
ax : :class:`matplotlib.axes.Axes`
A :class:`matplotlib.axes.Axes` instance where the graph will be drawn..
vprops : dict (optional, default: ``None``)
Dictionary with the vertex properties.
eprops : dict (optional, default: ``None``)
Dictionary with the edge properties.
**kwargs : dict (optional, default: ``None``)
Remaining options to pass to :class:`~graph_tool.draw.cairo_draw`.
"""
matplotlib.artist.Artist.__init__(self)
self.g = g
self.pos = pos
self.ax = ax
self.vprops = vprops if vprops is not None else {}
self.eprops = eprops if eprops is not None else {}
self.raster = raster
self.kwargs = kwargs.copy()
[docs]
def set_raster(self, raster=True):
"""If ``raster == True``, then the graph will be rasterized even if a
cairo-based backend is being used."""
self.raster = raster
[docs]
def fit_view(self, scale=1, margin=.1, yflip=False, keep_aspect=True,
set_axis_aspect=False):
"""Set axis limits to fit the graph, optionally scaling it via the
``scale`` parameter, and adding a relative margin given by ``margin``.
If ``yflip is True``, the y axis is flipped upside down. If
``keep_aspect is True``, the aspect ratio is preserved via the data
limits. Otherwise, if ``set_axis_aspect is True``, the aspect of the
axis will be changed to match the data aspect.
"""
x, y = ungroup_vector_property(self.pos, [0, 1])
l, r = x.fa.min(), x.fa.max()
b, t = y.fa.min(), y.fa.max()
w = r - l
h = t - b
if keep_aspect:
ll, ur = (self.ax.get_position() *
self.ax.get_figure().get_size_inches())
dw, dh = ur - ll
a = dh / dw
ar = h / w
if ar > a:
nw = h / a
l -= (nw - w) / 2
r += (nw - w) / 2
w = nw
else:
nh = w * a
b -= (nh - h) / 2
t += (nh - h) / 2
h = nh
elif set_axis_aspect:
self.ax.set_aspect(h / w)
w *= scale
h *= scale
self.ax.set_xlim(l - w * margin, r + w * margin)
if yflip:
self.ax.set_ylim(t + h * margin, b - h * margin)
else:
self.ax.set_ylim(b - h * margin, t + h * margin)
[docs]
def draw_to_cairo(self, ctx, transform, mag=1):
"""Draw to a cairo context."""
ctx.save()
pos = self.pos.copy()
eprops = dict(self.eprops)
vprops = dict(self.vprops)
# clip region
l, r = self.ax.get_xlim()
b, t = self.ax.get_ylim()
l, b = transform((l, b))
r, t = transform((r, t))
ctx.new_path()
ctx.rectangle(min(l, r), min(b, t), abs(r-l), abs(t-b))
ctx.clip()
d = (self.ax.transData.transform((1, 1)) -
self.ax.transData.transform((0, 0)))
scale_ink(np.mean(np.abs(d)) * mag, vprops, eprops, min_pen_width=0)
pos = pos.t(transform)
# transform edge control points, if present
cp = eprops.get("control_points", None)
if isinstance(cp, PropertyMap):
ctx.save()
cp = cp.copy()
for e in self.g.edges():
s = self.pos[e.source()].a
t = self.pos[e.target()].a
a = np.arctan2(t[1] - s[1],
t[0] - s[0])
l = np.sqrt((t[1] - s[1]) ** 2 +
(t[0] - s[0]) ** 2)
c = cp[e]
for i in range(len(c) // 2):
x = c.a[i*2:(i+1)*2]
ctx.identity_matrix()
ctx.translate(s[0], s[1])
ctx.rotate(a)
ctx.scale(l, 1)
x = ctx.user_to_device(x[0], x[1])
x = transform(x)
c.a[i*2:(i+1)*2] = x
s = pos[e.source()].a
t = pos[e.target()].a
a = np.arctan2(t[1] - s[1],
t[0] - s[0])
l = np.sqrt((t[1] - s[1]) ** 2 +
(t[0] - s[0]) ** 2)
for i in range(len(c) // 2):
x = c.a[i*2:(i+1)*2]
ctx.identity_matrix()
ctx.scale(1/l, 1)
ctx.rotate(-a)
ctx.translate(-s[0], -s[1])
x = ctx.user_to_device(x[0], x[1])
c.a[i*2:(i+1)*2] = x
ctx.restore()
eprops["control_points"] = cp
cairo_draw(self.g, pos, ctx, vprops, eprops, **self.kwargs)
ctx.restore()
[docs]
def draw(self, renderer):
width, height = renderer.get_canvas_width_height()
if (isinstance(renderer,
matplotlib.backends.backend_cairo.RendererCairo) and
not self.raster):
ctx = renderer.gc.ctx
if not isinstance(ctx, cairo.Context):
ctx = _UNSAFE_cairocffi_context_to_pycairo(ctx)
# Cairo coordinates are flipped in the y direction
def transform(x):
x = self.ax.transData.transform(x)
return (x[0], height - x[1])
self.draw_to_cairo(ctx, transform)
else:
l, r = self.ax.get_xlim()
b, t = self.ax.get_ylim()
l, b = self.ax.transData.transform((l, b))
r, t = self.ax.transData.transform((r, t))
mag = renderer.get_image_magnification()
width, height = int(abs(r-l) * mag), int(abs(t-b) * mag)
img = cairo.ImageSurface(cairo.Format.ARGB32, width, height)
ctx = cairo.Context(img)
def transform(x):
x = self.ax.transData.transform(x)
x = ((x[0] - l) * width / (r - l),
(x[1] - b) * height / (t - b))
return (x[0], height - x[1])
self.draw_to_cairo(ctx, transform, mag)
img.flush()
buf = img.get_data()
im = np.ndarray(shape=(height, width, 4), dtype=np.uint8,
buffer=buf)
im[:, :, [0, 2]] = im[:, :, [2, 0]] # fix endianess (swap R and B)
im = im[::-1, :, :] # flip y direction
gc = renderer.new_gc()
renderer.draw_image(gc, l, b, im)
#
# Drawing hierarchies
# ===================
#
[docs]
def draw_hierarchy(state, pos=None, layout="radial", beta=0.8, node_weight=None,
vprops=None, eprops=None, hvprops=None, heprops=None,
subsample_edges=None, rel_order="degree", deg_size=True,
vsize_scale=1, hsize_scale=1, hshortcuts=0, hide=0,
bip_aspect=1., empty_branches=False, **kwargs):
r"""Draw a nested block model state in a circular hierarchy layout with edge
bundling.
Parameters
----------
state : :class:`~graph_tool.inference.NestedBlockState`
Nested block state to be drawn.
pos : :class:`~graph_tool.VertexPropertyMap` (optional, default: ``None``)
If supplied, this specifies a vertex property map with the positions of
the vertices in the layout.
layout : ``str`` or :class:`~graph_tool.VertexPropertyMap` (optional, default: ``"radial"``)
If ``layout == "radial"`` :func:`~graph_tool.draw.radial_tree_layout`
will be used. If ``layout == "sfdp"``, the hierarchy tree will be
positioned using :func:`~graph_tool.draw.sfdp_layout`. If ``layout ==
"bipartite"`` a bipartite layout will be used. If instead a
:class:`~graph_tool.VertexPropertyMap` is provided, it must correspond to the
position of the hierarchy tree.
beta : ``float`` (optional, default: ``.8``)
Edge bundling strength.
node_weight : :class:`~graph_tool.VertexPropertyMap` (optional, default: ``None``)
If provided, this specifies a vertex property map with the relative angular
section that each vertex occupies. This value is ignored if
``layout != "radial"``.
vprops : dict (optional, default: ``None``)
Dictionary with the vertex properties. Individual properties may also be
given via the ``vertex_<prop-name>`` parameters, where ``<prop-name>`` is
the name of the property. See :func:`~graph_tool.draw.graph_draw` for
details.
eprops : dict (optional, default: ``None``)
Dictionary with the edge properties. Individual properties may also be
given via the ``edge_<prop-name>`` parameters, where ``<prop-name>`` is
the name of the property. See :func:`~graph_tool.draw.graph_draw` for
details.
hvprops : dict (optional, default: ``None``)
Dictionary with the vertex properties for the *hierarchy tree*.
Individual properties may also be given via the ``hvertex_<prop-name>``
parameters, where ``<prop-name>`` is the name of the property. See
:func:`~graph_tool.draw.graph_draw` for details.
heprops : dict (optional, default: ``None``)
Dictionary with the edge properties for the *hierarchy tree*. Individual
properties may also be given via the ``hedge_<prop-name>`` parameters,
where ``<prop-name>`` is the name of the property. See
:func:`~graph_tool.draw.graph_draw` for details.
subsample_edges : ``int`` or list of :class:`~graph_tool.Edge` instances (optional, default: ``None``)
If provided, only this number of random edges will be drawn. If the
value is a list, it should include the edges that are to be drawn.
rel_order : ``str`` or ``None`` or :class:`~graph_tool.VertexPropertyMap` (optional, default: ``"degree"``)
If ``degree``, the vertices will be ordered according to degree inside
each group, and the relative ordering of the hierarchy branches. If
instead a :class:`~graph_tool.VertexPropertyMap` is provided, its value will
be used for the relative ordering.
deg_size : ``bool`` (optional, default: ``True``)
If ``True``, the (total) node degrees will be used for the default
vertex sizes..
vsize_scale : ``float`` (optional, default: ``1.``)
Multiplicative factor for the default vertex sizes.
hsize_scale : ``float`` (optional, default: ``1.``)
Multiplicative factor for the default sizes of the hierarchy nodes.
hshortcuts : ``int`` (optional, default: ``0``)
Include shortcuts to the number of upper layers in the hierarchy
determined by this parameter.
hide : ``int`` (optional, default: ``0``)
Hide upper levels of the hierarchy.
bip_aspect : ``float`` (optional, default: ``1.``)
If ``layout == "bipartite"``, this will define the aspect ratio of layout.
empty_branches : ``bool`` (optional, default: ``False``)
If ``empty_branches == False``, dangling branches at the upper layers
will be pruned.
vertex_* : :class:`~graph_tool.VertexPropertyMap` 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
``vprops`` parameter. See :func:`~graph_tool.draw.graph_draw` for
details.
edge_* : :class:`~graph_tool.EdgePropertyMap` or arbitrary types (optional, default: ``None``)
Parameters following the pattern ``edge_<prop-name>`` specify the edge
property with name ``<prop-name>``, as an alternative to the ``eprops``
parameter. See :func:`~graph_tool.draw.graph_draw` for details.
hvertex_* : :class:`~graph_tool.VertexPropertyMap` or arbitrary types (optional, default: ``None``)
Parameters following the pattern ``hvertex_<prop-name>`` specify the
vertex property with name ``<prop-name>``, as an alternative to the
``hvprops`` parameter. See :func:`~graph_tool.draw.graph_draw` for
details.
hedge_* : :class:`~graph_tool.EdgePropertyMap` or arbitrary types (optional, default: ``None``)
Parameters following the pattern ``hedge_<prop-name>`` specify the edge
property with name ``<prop-name>``, as an alternative to the ``heprops``
parameter. See :func:`~graph_tool.draw.graph_draw` for details.
**kwargs :
All remaining keyword arguments will be passed to the
:func:`~graph_tool.draw.graph_draw` function.
Returns
-------
pos : :class:`~graph_tool.VertexPropertyMap`
This is a vertex property map with the positions of
the vertices in the layout.
t : :class:`~graph_tool.Graph`
This is a the hierarchy tree used in the layout.
tpos : :class:`~graph_tool.VertexPropertyMap`
This is a vertex property map with the positions of
the hierarchy tree in the layout.
Examples
--------
.. testsetup:: draw_hierarchy
gt.seed_rng(42)
np.random.seed(42)
.. doctest:: draw_hierarchy
>>> g = gt.collection.data["celegansneural"]
>>> state = gt.minimize_nested_blockmodel_dl(g)
>>> gt.draw_hierarchy(state, output="celegansneural_nested_mdl.pdf")
(...)
.. testcleanup:: draw_hierarchy
conv_png("celegansneural_nested_mdl.pdf")
.. figure:: celegansneural_nested_mdl.png
:align: center
:width: 80%
Hierarchical block partition of the C. elegans neural network, which
minimizes the description length of the network according to the nested
(degree-corrected) stochastic blockmodel.
References
----------
.. [holten-hierarchical-2006] Holten, D. "Hierarchical Edge Bundles:
Visualization of Adjacency Relations in Hierarchical Data.", IEEE
Transactions on Visualization and Computer Graphics 12, no. 5, 741–748
(2006). :doi:`10.1109/TVCG.2006.147`
"""
g = state.g
overlap = state.levels[0].overlap
if overlap:
ostate = state.levels[0]
bv, bcin, bcout, bc = ostate.get_overlap_blocks()
be = ostate.get_edge_blocks()
orig_state = state
state = state.copy()
b = ostate.get_majority_blocks()
state.levels[0] = BlockState(g, b=b)
else:
b = state.levels[0].b
if subsample_edges is not None:
emask = g.new_edge_property("bool", False)
if isinstance(subsample_edges, int):
eidx = g.edge_index.copy("int").fa.copy()
numpy.random.shuffle(eidx)
emask = g.new_edge_property("bool")
emask.a[eidx[:subsample_edges]] = True
else:
for e in subsample_edges:
emask[e] = True
g = GraphView(g, efilt=emask)
t, tb, tvorder = get_hierarchy_tree(state,
empty_branches=empty_branches)
if layout == "radial":
if rel_order == "degree":
rel_order = g.degree_property_map("total")
vorder = t.own_property(rel_order.copy())
if pos is not None:
x, y = ungroup_vector_property(pos, [0, 1])
x.fa -= x.fa.mean()
y.fa -= y.fa.mean()
angle = g.new_vertex_property("double")
angle.fa = (numpy.arctan2(y.fa, x.fa) + 2 * numpy.pi) % (2 * numpy.pi)
vorder = t.own_property(angle)
if node_weight is not None:
node_weight = t.own_property(node_weight.copy())
node_weight.a[node_weight.a == 0] = 1
tpos = radial_tree_layout(t, root=t.vertex(t.num_vertices() - 1,
use_index=False),
node_weight=node_weight,
rel_order=vorder,
rel_order_leaf=True)
elif layout == "bipartite":
tpos = get_bip_hierachy_pos(state, aspect=bip_aspect,
node_weight=node_weight)
tpos = t.own_property(tpos)
elif layout == "sfdp":
if pos is None:
tpos = sfdp_layout(t)
else:
x, y = ungroup_vector_property(pos, [0, 1])
x.fa -= x.fa.mean()
y.fa -= y.fa.mean()
K = numpy.sqrt(x.fa.std() + y.fa.std()) / 10
tpos = t.new_vertex_property("vector<double>")
for v in t.vertices():
if int(v) < g.num_vertices(True):
tpos[v] = [x[v], y[v]]
else:
tpos[v] = [0, 0]
pin = t.new_vertex_property("bool")
pin.a[:g.num_vertices(True)] = True
tpos = sfdp_layout(t, K=K, pos=tpos, pin=pin, multilevel=False)
else:
tpos = t.own_property(layout)
hvvisible = t.new_vertex_property("bool", True)
if hide is None:
L = len([s for s in state.levels if s.get_nonempty_B() > 0])
hide = len(state.levels) - L
if hide > 0:
root = t.vertex(t.num_vertices(True) - 1)
dist = shortest_distance(t, source=root)
hvvisible.fa = dist.fa >= hide
pos = g.own_property(tpos.copy())
cts = get_hierarchy_control_points(g, t, tpos, beta,
max_depth=len(state.levels) - hshortcuts)
vprops_orig = vprops
eprops_orig = eprops
hvprops_orig = vprops
heprops_orig = eprops
kwargs_orig = kwargs
vprops = vprops.copy() if vprops is not None else {}
eprops = eprops.copy() if eprops is not None else {}
props, kwargs = parse_props("vertex", kwargs)
vprops.update(props)
vprops.setdefault("fill_color", b)
vprops.setdefault("color", b)
vprops.setdefault("shape", _vdefaults["shape"] if not overlap else "pie")
output_size = kwargs.get("output_size", (600, 600))
if kwargs.get("mplfig", None) is not None:
x, y = ungroup_vector_property(pos, [0, 1])
w = x.a.max() - x.a.min()
h = y.a.max() - y.a.min()
output_size = (w, h)
s = numpy.mean(output_size) / (4 * numpy.sqrt(g.num_vertices()))
vprops.setdefault("size", prop_to_size(g.degree_property_map("total"), s/5, s))
adjust_default_sizes(g, output_size, vprops, eprops)
if vprops.get("text_position", None) == "centered":
angle, text_pos = centered_rotation(g, pos, text_pos=True)
vprops["text_position"] = text_pos
vprops["text_rotation"] = angle
toffset = vprops.get("text_offset", None)
if toffset is not None:
if not isinstance(toffset, PropertyMap):
toffset = g.new_vp("vector<double>", val=toffset)
xo, yo = ungroup_vector_property(toffset, [0, 1])
xo.a[text_pos.a == numpy.pi] *= -1
toffset = group_vector_property([xo, yo])
vprops["text_offset"] = toffset
self_loops = label_self_loops(g, mark_only=True)
if self_loops.fa.max() > 0:
parallel_distance = vprops.get("size", _vdefaults["size"])
if isinstance(parallel_distance, PropertyMap):
parallel_distance = parallel_distance.fa.mean()
cts_p = position_parallel_edges(g, pos, numpy.nan,
parallel_distance)
gu = GraphView(g, efilt=self_loops)
for e in gu.edges():
cts[e] = cts_p[e]
vprops = _convert_props(vprops, "v", g, kwargs.get("vcmap", None),
kwargs.get("vcnorm", None), pmap_default=True)
props, kwargs = parse_props("edge", kwargs)
eprops.update(props)
eprops.setdefault("control_points", cts)
eprops.setdefault("pen_width", _edefaults["pen_width"])
eprops.setdefault("color", list(_edefaults["color"][:-1]) + [.6])
eprops.setdefault("end_marker", "arrow" if g.is_directed() else "none")
eprops = _convert_props(eprops, "e", g, kwargs.get("ecmap", None),
kwargs.get("ecnorm", None), pmap_default=True)
hvprops = hvprops.copy() if hvprops is not None else {}
heprops = heprops.copy() if heprops is not None else {}
props, kwargs = parse_props("hvertex", kwargs)
hvprops.update(props)
blue = list(color_converter.to_rgba("#729fcf"))
blue[-1] = .6
hvprops.setdefault("fill_color", blue)
hvprops.setdefault("color", [1, 1, 1, 0])
hvprops.setdefault("shape", "square")
hvprops.setdefault("size", s)
if hvprops.get("text_position", None) == "centered":
angle, text_pos = centered_rotation(t, tpos, text_pos=True)
hvprops["text_position"] = text_pos
hvprops["text_rotation"] = angle
toffset = hvprops.get("text_offset", None)
if toffset is not None:
if not isinstance(toffset, PropertyMap):
toffset = t.new_vp("vector<double>", val=toffset)
xo, yo = ungroup_vector_property(toffset, [0, 1])
xo.a[text_pos.a == numpy.pi] *= -1
toffset = group_vector_property([xo, yo])
hvprops["text_offset"] = toffset
hvprops = _convert_props(hvprops, "v", t, kwargs.get("vcmap", None),
kwargs.get("vcnorm", None), pmap_default=True)
props, kwargs = parse_props("hedge", kwargs)
heprops.update(props)
heprops.setdefault("color", blue)
heprops.setdefault("end_marker", "arrow")
heprops.setdefault("marker_size", s * .8)
heprops.setdefault("pen_width", s / 10)
heprops = _convert_props(heprops, "e", t, kwargs.get("ecmap", None),
kwargs.get("ecnorm", None), pmap_default=True)
vcmap = kwargs.get("vcmap", None)
ecmap = kwargs.get("ecmap", vcmap)
B = state.levels[0].get_B()
if overlap and "pie_fractions" not in vprops:
vprops["pie_fractions"] = bc.copy("vector<double>")
if "pie_colors" not in vprops:
vertex_pie_colors = g.new_vertex_property("vector<double>")
nodes = defaultdict(list)
def conv(k):
clrs = [vcmap(r / (B - 1) if B > 1 else 0) for r in k]
return [item for l in clrs for item in l]
map_property_values(bv, vertex_pie_colors, conv)
vprops["pie_colors"] = vertex_pie_colors
gradient = eprops.get("gradient", None)
if gradient is None:
gradient = g.new_edge_property("double")
gradient = group_vector_property([gradient])
ecolor = eprops.get("ecolor", _edefaults["color"])
eprops["gradient"] = gradient
if overlap:
for e in g.edges(): # ******** SLOW *******
r, s = be[e]
if not g.is_directed() and e.source() > e.target():
r, s = s, r
gradient[e] = [0] + list(vcmap(r / (B - 1))) + \
[1] + list(vcmap(s / (B - 1)))
if isinstance(ecolor, PropertyMap):
gradient[e][4] = gradient[e][9] = ecolor[e][3]
else:
gradient[e][4] = gradient[e][9] = ecolor[3]
t_orig = t
t = GraphView(t,
vfilt=lambda v: int(v) >= g.num_vertices(True) and hvvisible[v])
t_vprops = {}
t_eprops = {}
props = []
for k in set(list(vprops.keys()) + list(hvprops.keys())):
t_vprops[k] = (vprops.get(k, None), hvprops.get(k, None))
props.append(t_vprops[k])
for k in set(list(eprops.keys()) + list(heprops.keys())):
t_eprops[k] = (eprops.get(k, None), heprops.get(k, None))
props.append(t_eprops[k])
props.append((pos, tpos))
props.append((g.vertex_index, tb))
props.append((b, None))
if "eorder" in kwargs:
eorder = kwargs["eorder"]
props.append((eorder,
t.new_ep(eorder.value_type(),
eorder.fa.max() + 1)))
u, props = graph_union(GraphView(g, directed=True), t, props=props)
for k in set(list(vprops.keys()) + list(hvprops.keys())):
t_vprops[k] = props.pop(0)
for k in set(list(eprops.keys()) + list(heprops.keys())):
t_eprops[k] = props.pop(0)
pos = props.pop(0)
tb = props.pop(0)
b = props.pop(0)
if "eorder" in kwargs:
eorder = props.pop(0)
def update_cts(widget, gg, picked, pos, vprops, eprops):
vmask = gg.vertex_index.copy("int")
u = GraphView(gg, directed=False, vfilt=vmask.fa < g.num_vertices(True))
cts = eprops["control_points"]
get_hierarchy_control_points(u, t_orig, pos, beta, cts=cts,
max_depth=len(state.levels) - hshortcuts)
def draw_branch(widget, gg, key_id, picked, pos, vprops, eprops):
if key_id == ord('b'):
if picked is not None and not isinstance(picked, PropertyMap) and int(picked) > g.num_vertices(True):
p = shortest_path(t_orig, source=t_orig.vertex(t_orig.num_vertices(True) - 1),
target=picked)[0]
l = len(state.levels) - max(len(p), 1)
bstack = state.get_bstack()
bs = [s.vp["b"].a for s in bstack[:l+1]]
bs[-1][:] = 0
if not overlap:
b = state.project_level(l).b
u = GraphView(g, vfilt=b.a == tb[picked])
u.vp["b"] = state.levels[0].b
u = Graph(u, prune=True)
b = u.vp["b"]
bs[0] = b.a
else:
be = orig_state.project_level(l).get_edge_blocks()
emask = g.new_edge_property("bool")
for e in g.edges():
rs = be[e]
if rs[0] == tb[picked] and rs[1] == tb[picked]:
emask[e] = True
u = GraphView(g, efilt=emask)
d = u.degree_property_map("total")
u = GraphView(u, vfilt=d.fa > 0)
u.ep["be"] = orig_state.levels[0].get_edge_blocks()
u = Graph(u, prune=True)
be = u.ep["be"]
s = OverlapBlockState(u, b=be)
bs[0] = s.b.a.copy()
nstate = NestedBlockState(u, bs=bs,
base_type=type(state.levels[0]),
state_args=state.state_args)
kwargs_ = kwargs_orig.copy()
if "no_main" in kwargs_:
del kwargs_["no_main"]
draw_hierarchy(nstate, beta=beta, vprops=vprops_orig,
eprops=eprops_orig, hvprops=hvprops_orig,
heprops=heprops_orig,
subsample_edges=subsample_edges,
deg_order=deg_order, empty_branches=False,
no_main=True, **kwargs_)
if key_id == ord('r'):
if layout == "radial":
x, y = ungroup_vector_property(pos, [0, 1])
x.fa -= x.fa.mean()
y.fa -= y.fa.mean()
angle = t_orig.new_vertex_property("double")
angle.fa = (numpy.arctan2(y.fa, x.fa) + 2 * numpy.pi) % (2 * numpy.pi)
tpos = radial_tree_layout(t_orig,
root=t_orig.vertex(t_orig.num_vertices(True) - 1),
rel_order=angle)
gg.copy_property(gg.own_property(tpos), pos)
update_cts(widget, gg, picked, pos, vprops, eprops)
if widget.vertex_matrix is not None:
widget.vertex_matrix.update()
widget.picked = None
widget.selected.fa = False
widget.fit_to_window()
widget.regenerate_surface(reset=True)
widget.queue_draw()
if ("output" not in kwargs and not kwargs.get("inline", has_draw_inline) and
kwargs.get("mplfig", None) is None):
kwargs["layout_callback"] = update_cts
kwargs["key_press_callback"] = draw_branch
if "eorder" in kwargs:
kwargs["eorder"] = eorder
vorder = kwargs.pop("vorder", None)
if vorder is None:
vorder = g.degree_property_map("total")
tvorder = u.own_property(tvorder)
tvorder.fa[:g.num_vertices()] = vorder.fa
for k, v in kwargs.items():
if isinstance(v, PropertyMap) and v.get_graph().base is not u.base:
kwargs[k] = u.own_property(v.copy())
ret = graph_draw(u, pos, vprops=t_vprops, eprops=t_eprops, vorder=tvorder,
**kwargs)
if isinstance(ret, PropertyMap):
ret = g.own_property(ret)
return ret, t_orig, tpos
def get_bip_hierachy_pos(state, aspect=1., node_weight=None):
if state.levels[0].overlap:
g = state.g
ostate = state.levels[0]
bv, bcin, bcout, bc = ostate.get_overlap_blocks()
be = ostate.get_edge_blocks()
n_r = zeros(ostate.get_B())
b = g.new_vertex_property("int")
for v in g.vertices():
i = bc[v].a.argmax()
b[v] = bv[v][i]
n_r[b[v]] += 1
orphans = [r for r in range(ostate.get_B()) if n_r[r] == 0]
for v in g.vertices():
for r in orphans:
b[v] = r
orig_state = state
state = state.copy()
state.levels[0] = BlockState(g, b=b)
g = state.g
deg = g.degree_property_map("total")
t, tb, order = get_hierarchy_tree(state)
root = t.vertex(t.num_vertices(True) - 1)
if root.out_degree() > 2:
clabel = is_bipartite(g, partition=True)[1].copy("int")
if state.levels[0].overlap:
ostate = OverlapBlockState(g, b=clabel)
ostate = orig_state.copy(clabel=clabel)
bc = ostate.propagate_clabel(len(state.levels) - 2)
else:
state = state.copy(clabel=clabel)
bc = state.propagate_clabel(len(state.levels) - 2)
ps = list(root.out_neighbors())
t.clear_vertex(root)
p1 = t.add_vertex()
p2 = t.add_vertex()
t.add_edge(root, p1)
t.add_edge(root, p2)
for p in ps:
if bc.a[tb[p]] == 0:
t.add_edge(p2, p)
else:
t.add_edge(p1, p)
w = t.new_vertex_property("double")
for v in t.vertices():
if v.in_degree() == 0:
break
if v.out_degree() == 0:
w[v] = 1 if node_weight is None else node_weight[v]
parent, = v.in_neighbors()
w[parent] += w[v]
pos = t.new_vertex_property("vector<double>")
pos[root] = (0., 0.)
p1, p2 = root.out_neighbors()
if ((w[p1] == w[p2] and p1.out_degree() > p2.out_degree()) or
w[p1] > w[p2]):
p1, p2 = p2, p1
L = len(state.levels)
pos[p1] = (-1 / L * .5 * aspect, 0)
pos[p2] = (+1 / L * .5 * aspect, 0)
for i, p in enumerate([p1, p2]):
roots = [p]
while len(roots) > 0:
nroots = []
for r in roots:
cw = pos[r][1] - w[r] / (2. * w[p])
for v in sorted(r.out_neighbors(), key=lambda a: order[a]):
pos[v] = (0, 0)
if i == 0:
pos[v][0] = pos[r][0] - 1 / L * .5 * aspect
else:
pos[v][0] = pos[r][0] + 1 / L * .5 * aspect
pos[v][1] = cw + w[v] / (2. * w[p])
cw += w[v] / w[p]
nroots.append(v)
roots = nroots
return pos
# Handle cairo contexts from cairocffi
try:
import cairocffi
import ctypes
pycairo_aux = ctypes.PyDLL(os.path.dirname(os.path.abspath(__file__)) + "/libgt_pycairo_aux.so")
pycairo_aux.gt_PycairoContext_FromContext.restype = ctypes.c_void_p
pycairo_aux.gt_PycairoContext_FromContext.argtypes = 3 * [ctypes.c_void_p]
ctypes.pythonapi.PyList_Append.argtypes = 2 * [ctypes.c_void_p]
except ImportError:
pass
def _UNSAFE_cairocffi_context_to_pycairo(cairocffi_context):
# Sanity check. Continuing with another type would probably segfault.
if not isinstance(cairocffi_context, cairocffi.Context):
raise TypeError('Expected a cairocffi.Context, got %r'
% cairocffi_context)
# Create a reference for PycairoContext_FromContext to take ownership of.
cairocffi.cairo.cairo_reference(cairocffi_context._pointer)
# Casting the pointer to uintptr_t (the integer type as wide as a pointer)
# gets the context’s integer address.
# On CPython id(cairo.Context) gives the address to the Context type,
# as expected by PycairoContext_FromContext.
address = pycairo_aux.gt_PycairoContext_FromContext(
int(cairocffi.ffi.cast('uintptr_t', cairocffi_context._pointer)),
id(cairo.Context),
None)
assert address
# This trick uses Python’s C API
# to get a reference to a Python object from its address.
temp_list = []
assert ctypes.pythonapi.PyList_Append(id(temp_list), address) == 0
return temp_list[0]
# Bottom imports to avoid circular dependency issues
from .. inference import get_hierarchy_tree, NestedBlockState, BlockState, \
OverlapBlockState