diff --git a/tools/rxgraph/CMakeLists.txt b/tools/rxgraph/CMakeLists.txt
new file mode 100644
index 00000000..f8f1c9cc
--- /dev/null
+++ b/tools/rxgraph/CMakeLists.txt
@@ -0,0 +1,30 @@
+cmake_minimum_required(VERSION 2.4.6)
+include($ENV{ROS_ROOT}/core/rosbuild/rosbuild.cmake)
+
+# Set the build type. Options are:
+# Coverage : w/ debug symbols, w/o optimization, w/ code-coverage
+# Debug : w/ debug symbols, w/o optimization
+# Release : w/o debug symbols, w/ optimization
+# RelWithDebInfo : w/ debug symbols, w/ optimization
+# MinSizeRel : w/o debug symbols, w/ optimization, stripped binaries
+#set(ROS_BUILD_TYPE RelWithDebInfo)
+
+rosbuild_init()
+
+#set the default path for built executables to the "bin" directory
+set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)
+#set the default path for built libraries to the "lib" directory
+set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)
+
+#uncomment if you have defined messages
+#rosbuild_genmsg()
+#uncomment if you have defined services
+#rosbuild_gensrv()
+
+#common commands for building c++ executables and libraries
+#rosbuild_add_library(${PROJECT_NAME} src/example.cpp)
+#target_link_libraries(${PROJECT_NAME} another_library)
+#rosbuild_add_boost_directories()
+#rosbuild_link_boost(${PROJECT_NAME} thread)
+#rosbuild_add_executable(example examples/example.cpp)
+#target_link_libraries(example ${PROJECT_NAME})
diff --git a/tools/rxgraph/Makefile b/tools/rxgraph/Makefile
new file mode 100644
index 00000000..b75b928f
--- /dev/null
+++ b/tools/rxgraph/Makefile
@@ -0,0 +1 @@
+include $(shell rospack find mk)/cmake.mk
\ No newline at end of file
diff --git a/tools/rxgraph/manifest.xml b/tools/rxgraph/manifest.xml
new file mode 100644
index 00000000..8c897fd0
--- /dev/null
+++ b/tools/rxgraph/manifest.xml
@@ -0,0 +1,18 @@
+
+
+
+ rxgraph
+
+
+ Ken Conley
+ BSD
+
+ http://ros.org/wiki/rxgraph
+
+
+
+
+
+
+
+
diff --git a/tools/rxgraph/src/rxgraph/__init__.py b/tools/rxgraph/src/rxgraph/__init__.py
new file mode 100644
index 00000000..c0f46d16
--- /dev/null
+++ b/tools/rxgraph/src/rxgraph/__init__.py
@@ -0,0 +1,33 @@
+# Software License Agreement (BSD License)
+#
+# Copyright (c) 2010, Willow Garage, Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following
+# disclaimer in the documentation and/or other materials provided
+# with the distribution.
+# * Neither the name of Willow Garage, Inc. nor the names of its
+# contributors may be used to endorse or promote products derived
+# from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# Revision $Id: rxgraph.py 8782 2010-03-22 21:44:43Z kwc $
diff --git a/tools/rxgraph/src/rxgraph/dotcode.py b/tools/rxgraph/src/rxgraph/dotcode.py
new file mode 100644
index 00000000..775c2826
--- /dev/null
+++ b/tools/rxgraph/src/rxgraph/dotcode.py
@@ -0,0 +1,175 @@
+# Software License Agreement (BSD License)
+#
+# Copyright (c) 2008, Willow Garage, Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following
+# disclaimer in the documentation and/or other materials provided
+# with the distribution.
+# * Neither the name of Willow Garage, Inc. nor the names of its
+# contributors may be used to endorse or promote products derived
+# from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# Revision $Id: rxgraph.py 8782 2010-03-22 21:44:43Z kwc $
+
+import roslib.names
+import rosgraph.graph
+
+ORIENTATIONS = ['LR', 'TB', 'RL', 'BT']
+
+INIT_DOTCODE = """
+digraph G { initializing [label="initializing..."]; }
+"""
+
+# node/node connectivity
+NODE_NODE_GRAPH = "node_node"
+# node/topic connections where an actual network connection exists
+NODE_TOPIC_GRAPH = "node_topic"
+# all node/topic connections, even if no actual network connection
+NODE_TOPIC_ALL_GRAPH = "node_topic_all"
+
+def safe_dotcode_name(name):
+ """
+ encode the name for dotcode symbol-safe syntax
+ """
+ # not terribly efficient or sophisticated
+ ret = name.replace('/', '_')
+ ret = ret.replace(' ', '_')
+ ret = ret.replace('-', '_')
+ return ret.replace('.', '_')
+
+def _edge_to_dot(e):
+ if e.label:
+ return ' %s->%s [label="%s"]'%(safe_dotcode_name(e.start), safe_dotcode_name(e.end), e.label)
+ else:
+ return ' %s->%s'%(safe_dotcode_name(e.start), safe_dotcode_name(e.end))
+
+def _generate_node_dotcode(node, g, quiet):
+ if node in g.bad_nodes:
+ bn = g.bad_nodes[node]
+ if bn.type == rosgraph.graph.BadNode.DEAD:
+ return ' %s [color="red", shape="doublecircle", label="%s", URL="node:%s"];'%(
+ safe_dotcode_name(node), node, node)
+ else:
+ return ' %s [color="orange", shape="doublecircle", label="%s", URL="node:%s"];'%(
+ safe_dotcode_name(node), node, node, node)
+ else:
+ return ' %s [label="%s", URL="node:%s"];'%(safe_dotcode_name(node), node, node)
+
+QUIET_NAMES = ['/diag_agg', '/runtime_logger', '/pr2_dashboard', '/rviz', '/rosout', '/cpu_monitor', '/monitor','/hd_monitor', '/rxloggerlevel', '/clock']
+def _quiet_filter(name):
+ # ignore viewers
+ for n in QUIET_NAMES:
+ if n in name:
+ return False
+ return True
+
+def _quiet_filter_edge(edge):
+ for quiet_label in ['/time', '/clock', '/rosout']:
+ if quiet_label == edge.label:
+ return False
+ return _quiet_filter(edge.start) and _quiet_filter(edge.end)
+
+def generate_namespaces(g, graph_mode, quiet=False):
+ """
+ Determine the namespaces of the nodes being displayed
+ """
+ namespaces = []
+ if graph_mode == NODE_NODE_GRAPH:
+ nodes = g.nn_nodes
+ if quiet:
+ nodes = [n for n in nodes if not n in QUIET_NAMES]
+ namespaces = list(set([roslib.names.namespace(n) for n in nodes]))
+
+ elif graph_mode == NODE_TOPIC_GRAPH or \
+ graph_mode == NODE_TOPIC_ALL_GRAPH:
+ nn_nodes = g.nn_nodes
+ nt_nodes = g.nt_nodes
+ if quiet:
+ nn_nodes = [n for n in nn_nodes if not n in QUIET_NAMES]
+ nt_nodes = [n for n in nt_nodes if not n in QUIET_NAMES]
+ if nn_nodes or nt_nodes:
+ namespaces = [roslib.names.namespace(n) for n in nn_nodes]
+ namespaces.extend([roslib.names.namespace(n) for n in nt_nodes])
+
+ return list(set(namespaces))
+
+def _filter_edges(edges, nodes):
+ return [e for e in edges if e.start in nodes or e.end in nodes]
+
+def generate_dotcode(g, ns_filter, graph_mode, orientation, quiet=False):
+ """
+ @param g: Graph instance
+ @param ns_filter: namespace filter (must be canonicalized with trailing '/')
+ @type ns_filter: string
+ @param graph_mode str: NODE_NODE_GRAPH | NODE_TOPIC_GRAPH | NODE_TOPIC_ALL_GRAPH
+ @type graph_mode: str
+ @param orientation: rankdir value (see ORIENTATIONS dict)
+ @return: dotcode generated from graph singleton
+ @rtype: str
+ """
+ #print "generate_dotcode", graph_mode
+ if ns_filter:
+ name_filter = ns_filter[:-1]
+
+ # create the node definitions
+ if graph_mode == NODE_NODE_GRAPH:
+ nodes = g.nn_nodes
+ if quiet:
+ nodes = [n for n in nodes if not n in QUIET_NAMES]
+ if ns_filter and ns_filter != '/':
+ nodes = [n for n in nodes if n.startswith(ns_filter) or n == name_filter]
+ if nodes:
+ nodes_str = '\n'.join([_generate_node_dotcode(n, g, quiet) for n in nodes])
+ else:
+ nodes_str = ' empty;'
+
+ elif graph_mode == NODE_TOPIC_GRAPH or \
+ graph_mode == NODE_TOPIC_ALL_GRAPH:
+ nn_nodes = g.nn_nodes
+ nt_nodes = g.nt_nodes
+ if quiet:
+ nn_nodes = [n for n in nn_nodes if not n in QUIET_NAMES]
+ nt_nodes = [n for n in nt_nodes if not n in QUIET_NAMES]
+ if nn_nodes or nt_nodes:
+ nodes_str = '\n'.join([_generate_node_dotcode(n, g, quiet) for n in nn_nodes])
+ nodes_str += '\n'.join([' %s [shape=box,label="%s",URL="topic:%s"];'%(
+ safe_dotcode_name(n), rosgraph.graph.node_topic(n), rosgraph.graph.node_topic(n)) for n in nt_nodes])
+ else:
+ nodes_str = ' empty;'
+ nodes = list(nn_nodes) + list(nt_nodes)
+
+ # create the edge definitions
+ if graph_mode == NODE_NODE_GRAPH:
+ edges = g.nn_edges
+ elif graph_mode == NODE_TOPIC_GRAPH:
+ edges = g.nt_edges
+ else:
+ edges = g.nt_all_edges
+ if quiet:
+ edges = filter(_quiet_filter_edge, edges)
+
+ edges = _filter_edges(edges, nodes)
+ edges_str = '\n'.join([_edge_to_dot(e) for e in edges])
+ return "digraph G {\n rankdir=%(orientation)s;\n%(nodes_str)s\n%(edges_str)s}\n"%vars()
+
diff --git a/tools/rxgraph/src/rxgraph/rxgraph.py b/tools/rxgraph/src/rxgraph/rxgraph.py
new file mode 100644
index 00000000..c930390a
--- /dev/null
+++ b/tools/rxgraph/src/rxgraph/rxgraph.py
@@ -0,0 +1,187 @@
+#!/usr/bin/env python
+# Software License Agreement (BSD License)
+#
+# Copyright (c) 2008, Willow Garage, Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following
+# disclaimer in the documentation and/or other materials provided
+# with the distribution.
+# * Neither the name of Willow Garage, Inc. nor the names of its
+# contributors may be used to endorse or promote products derived
+# from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# Revision $Id: rxgraph.py 8782 2010-03-22 21:44:43Z kwc $
+
+from __future__ import with_statement
+
+import roslib; roslib.load_manifest('rxgraph')
+
+import sys
+import time
+import threading
+import traceback
+import logging
+from optparse import OptionParser
+
+import rosgraph.graph
+import rxgraph.dotcode
+from rxgraph.dotcode import generate_dotcode, generate_namespaces, NODE_NODE_GRAPH, NODE_TOPIC_GRAPH
+from rxgraph.viewer import RxGraphViewerFrame
+
+import roslib.roslogging
+
+# have to import later than others due to xdot calling wxversion
+import wx
+
+class DotUpdate(threading.Thread):
+ """Thread to control update of dot file"""
+
+ def __init__(self, graph, viewer, output_file=None):
+ threading.Thread.__init__(self, name="DotUpdate")
+ self.viewer = viewer
+ self.graph = graph
+ self.output_file = None
+
+ def run(self):
+ viewer = self.viewer
+ current_ns_filter = viewer.ns_filter
+ g = self.graph
+ output_file = self.output_file
+ last_graph_mode = NODE_NODE_GRAPH
+ quiet = False
+
+ orientation = rxgraph.dotcode.ORIENTATIONS[0]
+
+ g.set_master_stale(5.0)
+ g.set_node_stale(5.0)
+
+ GUPDATE_INTERVAL = 0.5
+ last_gupdate = time.time() - GUPDATE_INTERVAL
+ try:
+ while not is_shutdown():
+
+ # throttle calls to g.update(). we want fast refresh
+ # on changes to the viewer's ns_filter, less so on the
+ # graph polling.
+ now = time.time()
+ if now - last_gupdate >= GUPDATE_INTERVAL:
+ changed = g.update()
+ last_gupdate = now
+
+ graph_mode = NODE_TOPIC_GRAPH if viewer.topic_boxes else NODE_NODE_GRAPH
+
+ changed |= viewer.ns_filter != current_ns_filter
+ changed |= quiet != viewer.quiet
+ changed |= graph_mode != last_graph_mode
+
+ quiet = viewer.quiet
+ last_graph_mode = graph_mode
+
+ if changed:
+ current_ns_filter = viewer.ns_filter
+ dotcode = generate_dotcode(g, current_ns_filter, graph_mode, orientation, quiet)
+
+ #compute path-combo box
+ namespaces = generate_namespaces(g, graph_mode, quiet)
+ viewer.update_namespaces(namespaces)
+
+ viewer.set_dotcode(dotcode)
+
+ # store dotcode if requested
+ if output_file:
+ with file(output_file, 'w') as f:
+ f.write(dotcode)
+
+ #information still changing, shorter yield
+ time.sleep(0.1)
+ else:
+ # no new information, let bad nodes update
+ g.bad_update()
+ time.sleep(0.1)
+
+ changed = False
+
+ except wx.PyDeadObjectError:
+ # shutdown
+ pass
+ except:
+ traceback.print_exc()
+
+_is_shutdown = False
+def is_shutdown():
+ return _is_shutdown
+
+def set_shutdown(is_shutdown):
+ global _is_shutdown
+ _is_shutdown = is_shutdown
+
+def rxgraph_main():
+ parser = OptionParser(usage="usage: rxgraph [options]")
+ parser.add_option("-o", "--dot",
+ dest="output_file", default=None,
+ help="ouput graph as graphviz dot file", metavar="DOTFILE")
+ parser.add_option("--nodens",
+ dest="node_ns", default=None,
+ help="only show nodes in specified namespace")
+ parser.add_option("--topicns",
+ dest="topic_ns", default=None,
+ help="only show topics in specified namespace")
+
+ options, args = parser.parse_args()
+ if args:
+ parser.error("invalid arguments")
+
+
+ import subprocess
+ try:
+ subprocess.check_call(['dot', '-V'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ except:
+ print >> sys.stderr, "Graphviz does not appear to be installed on your system. Please run:\n\n\trosdep install rosgraph\n\nto install the necessary dependencies on your system"
+ sys.exit(1)
+
+
+ roslib.roslogging.configure_logging('rxgraph', logging.DEBUG, additional=['rospy', 'roslib', 'rosgraph'])
+ try:
+ # make gtk play nice with Python threads
+ #gtk.gdk.threads_init()
+
+ graph = rosgraph.graph.Graph(options.node_ns, options.topic_ns)
+
+ app = wx.App()
+ frame = RxGraphViewerFrame()
+ frame.set_dotcode(rxgraph.dotcode.INIT_DOTCODE)
+
+ DotUpdate(graph, frame, output_file=options.output_file).start()
+
+ frame.Show()
+ app.MainLoop()
+
+ except KeyboardInterrupt:
+ pass
+ finally:
+ set_shutdown(True)
+
+
+if __name__ == '__main__':
+ rxgraph_main()
diff --git a/tools/rxgraph/src/rxgraph/viewer.py b/tools/rxgraph/src/rxgraph/viewer.py
new file mode 100644
index 00000000..7620e132
--- /dev/null
+++ b/tools/rxgraph/src/rxgraph/viewer.py
@@ -0,0 +1,191 @@
+#!/usr/bin/env python
+# Copyright (c) 2010, Willow Garage, Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# * Neither the name of the Willow Garage, Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# Author: Jonathan Bohren
+
+import roslib; roslib.load_manifest('rxgraph')
+import rospy
+
+import sys
+import xdot
+import wx
+
+DOTCODE= """digraph G {
+ rankdir=LR;
+ _included_talker [label="/included/talker"];
+ _included_wg2_talker [label="/included/wg2/talker"];
+ _included2_talker [label="/included2/talker"];
+ _included2_listener [label="/included2/listener"];
+ _included_wg2_listener [label="/included/wg2/listener"];
+ _rosout [label="/rosout"];
+ _included_listener [label="/included/listener"];
+ _wg_listener [label="/wg/listener"];
+ _wg_talker2 [label="/wg/talker2"];
+ _wg_talker1 [label="/wg/talker1"];
+ _included2_wg2_talker [label="/included2/wg2/talker"];
+ _included2_wg2_listener [label="/included2/wg2/listener"];
+ _wg_talker1->_rosout [label="/rosout"]
+ _included2_wg2_listener->_rosout [label="/rosout"]
+ _wg_talker2->_wg_listener [label="/wg/hello"]
+ _wg_talker2->_rosout [label="/rosout"]
+ _included2_wg2_talker->_included2_wg2_listener [label="/included2/wg2/chatter"]
+ _included2_listener->_rosout [label="/rosout"]
+ _included_wg2_listener->_rosout [label="/rosout"]
+ _wg_talker1->_wg_listener [label="/wg/hello"]
+ _included2_talker->_included2_listener [label="/included2/chatter"]
+ _wg_listener->_rosout [label="/rosout"]
+ _included_wg2_talker->_included_wg2_listener [label="/included/wg2/chatter"]
+ _included_talker->_included_listener [label="/included/chatter"]
+ _included_wg2_talker->_rosout [label="/rosout"]
+ _included2_talker->_rosout [label="/rosout"]
+ _included2_wg2_talker->_rosout [label="/rosout"]
+ _included_talker->_rosout [label="/rosout"]
+ _included_listener->_rosout [label="/rosout"]}
+"""
+
+class RxGraphViewerFrame(wx.Frame):
+ def __init__(self):
+ wx.Frame.__init__(self, None, -1, "rxgraph", size=(720,480))
+
+ # TODO: convert to read-only property
+ self.ns_filter = None
+ self.quiet = False
+ self.topic_boxes = False
+
+ self._needs_refresh = False
+
+ vbox = wx.BoxSizer(wx.VERTICAL)
+
+ graph_view = wx.Panel(self, -1)
+ gv_vbox = wx.BoxSizer(wx.VERTICAL)
+ graph_view.SetSizer(gv_vbox)
+
+ # Construct toolbar
+ toolbar = wx.ToolBar(graph_view, -1)
+
+ toolbar.AddControl(wx.StaticText(toolbar,-1,"Path: "))
+
+ self._ns_combo = wx.ComboBox(toolbar, -1, style=wx.CB_DROPDOWN)
+ self._ns_combo .Bind(wx.EVT_COMBOBOX, self.set_path)
+ self._ns_combo.Append('/')
+ self._ns_combo.SetValue('/')
+ self._namespaces = ['/']
+
+ # display options
+ quiet_check = wx.CheckBox(toolbar, -1, label="Quiet")
+ quiet_check.Bind(wx.EVT_CHECKBOX, self.set_quiet_check)
+ topic_check = wx.CheckBox(toolbar, -1, label="All topics")
+ topic_check.Bind(wx.EVT_CHECKBOX, self.set_topic_boxes)
+
+ toolbar.AddControl(self._ns_combo)
+
+ toolbar.AddControl(wx.StaticText(toolbar,-1," "))
+ toolbar.AddControl(quiet_check)
+ toolbar.AddControl(wx.StaticText(toolbar,-1," "))
+ toolbar.AddControl(topic_check)
+
+ toolbar.AddControl(wx.StaticText(toolbar,-1," "))
+ toolbar.AddLabelTool(wx.ID_HELP, 'Help',
+ wx.ArtProvider.GetBitmap(wx.ART_HELP,wx.ART_OTHER,(16,16)) )
+ toolbar.Realize()
+
+ self.Bind(wx.EVT_TOOL, self.ShowControlsDialog, id=wx.ID_HELP)
+
+ # Create dot graph widget
+ self._widget = xdot.wxxdot.WxDotWindow(graph_view, -1)
+ self._widget.set_dotcode(DOTCODE)
+ self._widget.zoom_to_fit()
+
+ vbox.Add(graph_view, 1, wx.EXPAND | wx.ALL)
+
+ gv_vbox.Add(toolbar, 0, wx.EXPAND)
+ gv_vbox.Add(self._widget, 1, wx.EXPAND)
+
+ self.SetSizer(vbox)
+ self.Center()
+
+ self.Bind(wx.EVT_IDLE,self.OnIdle)
+
+ # user callback for select
+ self._widget.register_select_callback(self.select_cb)
+
+ def OnIdle(self, event):
+ if self._needs_refresh:
+ self.Refresh()
+ self._needs_refresh = False
+
+ def select_cb(self, *args):
+ print args[0].item.url
+
+ def set_quiet_check(self, event):
+ self.quiet = event.Checked()
+
+ def set_topic_boxes(self, event):
+ self.topic_boxes = event.Checked()
+
+ def set_path(self, event):
+ self.ns_filter = self._ns_combo.GetValue()
+ self._needs_zoom = True
+
+ def update_namespaces(self, namespaces):
+ # unfortunately this routine will not alphanumerically sort
+ curr = self._namespaces
+ new_filters = [n for n in namespaces if n not in curr]
+ for f in new_filters:
+ self._ns_combo.Append(f)
+ curr.extend(new_filters)
+
+ def ShowControlsDialog(self,event):
+ dial = wx.MessageDialog(None,
+ "Pan: Arrow Keys\nZoom: PageUp / PageDown\nZoom To Fit: F\n",
+ 'Keyboard Controls', wx.OK)
+ dial.ShowModal()
+
+ def set_dotcode(self, dotcode, zoom=True):
+ if self._widget.set_dotcode(dotcode, None):
+ self.SetTitle('rxgraph')
+ if zoom or self._needs_zoom:
+ self._widget.zoom_to_fit()
+ self._needs_zoom = False
+ self._needs_refresh = True
+ wx.PostEvent(self.GetEventHandler(), wx.IdleEvent())
+
+
+def main():
+ app = wx.App()
+
+ frame = RxGraphViewerFrame()
+ frame.Show()
+
+ # for testing only
+ frame.set_dotcode(DOTCODE)
+
+ app.MainLoop()
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/xdot/CMakeLists.txt b/tools/xdot/CMakeLists.txt
new file mode 100644
index 00000000..f8f1c9cc
--- /dev/null
+++ b/tools/xdot/CMakeLists.txt
@@ -0,0 +1,30 @@
+cmake_minimum_required(VERSION 2.4.6)
+include($ENV{ROS_ROOT}/core/rosbuild/rosbuild.cmake)
+
+# Set the build type. Options are:
+# Coverage : w/ debug symbols, w/o optimization, w/ code-coverage
+# Debug : w/ debug symbols, w/o optimization
+# Release : w/o debug symbols, w/ optimization
+# RelWithDebInfo : w/ debug symbols, w/ optimization
+# MinSizeRel : w/o debug symbols, w/ optimization, stripped binaries
+#set(ROS_BUILD_TYPE RelWithDebInfo)
+
+rosbuild_init()
+
+#set the default path for built executables to the "bin" directory
+set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)
+#set the default path for built libraries to the "lib" directory
+set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)
+
+#uncomment if you have defined messages
+#rosbuild_genmsg()
+#uncomment if you have defined services
+#rosbuild_gensrv()
+
+#common commands for building c++ executables and libraries
+#rosbuild_add_library(${PROJECT_NAME} src/example.cpp)
+#target_link_libraries(${PROJECT_NAME} another_library)
+#rosbuild_add_boost_directories()
+#rosbuild_link_boost(${PROJECT_NAME} thread)
+#rosbuild_add_executable(example examples/example.cpp)
+#target_link_libraries(example ${PROJECT_NAME})
diff --git a/tools/xdot/Makefile b/tools/xdot/Makefile
new file mode 100644
index 00000000..b75b928f
--- /dev/null
+++ b/tools/xdot/Makefile
@@ -0,0 +1 @@
+include $(shell rospack find mk)/cmake.mk
\ No newline at end of file
diff --git a/tools/xdot/dot_viewer.py b/tools/xdot/dot_viewer.py
new file mode 100755
index 00000000..21b3b0c2
--- /dev/null
+++ b/tools/xdot/dot_viewer.py
@@ -0,0 +1,74 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2010, Willow Garage, Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# * Neither the name of the Willow Garage, Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# Author: Jonathan Bohren
+
+import roslib; roslib.load_manifest('xdot')
+import rospy
+
+import sys
+import xdot
+import wx
+
+def main():
+ import optparse
+
+ parser = optparse.OptionParser(
+ usage='\n\t%prog [file]')
+ parser.add_option(
+ '-f', '--filter',
+ type='choice', choices=('dot', 'neato', 'twopi', 'circo', 'fdp'),
+ dest='filter', default='dot',
+ help='graphviz filter: dot, neato, twopi, circo, or fdp [default: %default]')
+
+ (options, args) = parser.parse_args(sys.argv[1:])
+ if len(args) > 1:
+ parser.error('incorrect number of arguments')
+
+ app = wx.App()
+
+ frame = xdot.wxxdot.WxDotFrame()
+ frame.set_filter(options.filter)
+
+ """Sample mouse event."""
+ #def print_cb(item,event):
+ # print "MOUSE_EVENT: "+str(item)
+ #frame.widget.register_select_callback(print_cb)
+
+ frame.Show()
+ if len(args) >= 1:
+ if args[0] == '-':
+ frame.set_dotcode(sys.stdin.read())
+ else:
+ frame.open_file(args[0])
+
+ app.MainLoop()
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/xdot/mainpage.dox b/tools/xdot/mainpage.dox
new file mode 100644
index 00000000..1a632aa7
--- /dev/null
+++ b/tools/xdot/mainpage.dox
@@ -0,0 +1,26 @@
+/**
+\mainpage
+\htmlinclude manifest.html
+
+\b xdot is ...
+
+
+
+
+\section codeapi Code API
+
+
+
+
+*/
diff --git a/tools/xdot/manifest.xml b/tools/xdot/manifest.xml
new file mode 100644
index 00000000..14373979
--- /dev/null
+++ b/tools/xdot/manifest.xml
@@ -0,0 +1,20 @@
+
+
+ XDot is an interactive viewer for graphs written in Graphviz's dot
+ language.
+ This package adds front-end capabilities to XDot including WX Widget
+ support and a mechanism for receiving callbacks when nodes are clicked.
+
+ Jose Fonseca, ROS package and WX frontend by Jonathan Bohren
+ LGPL/BSD
+
+ http://code.google.com/p/jrfonseca/wiki/XDot
+
+
+
+
+
+
+
+
+
diff --git a/tools/xdot/src/xdot/__init__.py b/tools/xdot/src/xdot/__init__.py
new file mode 100644
index 00000000..fa9b6d61
--- /dev/null
+++ b/tools/xdot/src/xdot/__init__.py
@@ -0,0 +1,2 @@
+
+import wxxdot
diff --git a/tools/xdot/src/xdot/wxxdot.py b/tools/xdot/src/xdot/wxxdot.py
new file mode 100644
index 00000000..09212e24
--- /dev/null
+++ b/tools/xdot/src/xdot/wxxdot.py
@@ -0,0 +1,575 @@
+#!/usr/bin/env python
+#
+# wxpython widgets for using Jose Fonseca's cairo graphviz visualizer
+# Copyright (c) 2010, Willow Garage, Inc.
+#
+# Source modified from Jose Fonseca's XDot pgtk widgets. That code is
+# Copyright 2008 Jose Fonseca
+#
+# 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 .
+
+from xdot import *
+
+__all__ = ['WxDotWindow', 'WxDotFrame']
+
+# We need to get the wx version with built-in cairo support
+import wxversion
+wxversion.select("2.8")
+import wx
+import wx.lib.wxcairo as wxcairo
+
+# This is a crazy hack to get this to work on 64-bit systems
+if 'wxMac' in wx.PlatformInfo:
+ pass # Implement if necessary
+elif 'wxMSW' in wx.PlatformInfo:
+ pass # Implement if necessary
+elif 'wxGTK' in wx.PlatformInfo:
+ import ctypes
+ gdkLib = wx.lib.wxcairo._findGDKLib()
+ gdkLib.gdk_cairo_create.restype = ctypes.c_void_p
+
+class WxDragAction(object):
+ def __init__(self, dot_widget):
+ self.dot_widget = dot_widget
+
+ def on_button_press(self, event):
+ x,y = event.GetPositionTuple()
+ self.startmousex = self.prevmousex = x
+ self.startmousey = self.prevmousey = y
+ self.start()
+
+ def on_motion_notify(self, event):
+ x,y = event.GetPositionTuple()
+ deltax = self.prevmousex - x
+ deltay = self.prevmousey - y
+ self.drag(deltax, deltay)
+ self.prevmousex = x
+ self.prevmousey = y
+
+ def on_button_release(self, event):
+ x,y = event.GetPositionTuple()
+ self.stopmousex = x
+ self.stopmousey = y
+ self.stop()
+
+ def draw(self, cr):
+ pass
+
+ def start(self):
+ pass
+
+ def drag(self, deltax, deltay):
+ pass
+
+ def stop(self):
+ pass
+
+ def abort(self):
+ pass
+
+class WxNullAction(WxDragAction):
+ def on_motion_notify(self, event):
+ pass
+
+class WxPanAction(WxDragAction):
+ def start(self):
+ self.dot_widget.set_cursor(wx.CURSOR_SIZING)
+
+ def drag(self, deltax, deltay):
+ self.dot_widget.x += deltax / self.dot_widget.zoom_ratio
+ self.dot_widget.y += deltay / self.dot_widget.zoom_ratio
+ self.dot_widget.Refresh()
+
+ def stop(self):
+ self.dot_widget.set_cursor(wx.CURSOR_ARROW)
+
+ abort = stop
+
+class WxZoomAction(WxDragAction):
+ def drag(self, deltax, deltay):
+ self.dot_widget.zoom_ratio *= 1.005 ** (deltax + deltay)
+ self.dot_widget.zoom_to_fit_on_resize = False
+ self.dot_widget.Refresh()
+
+ def stop(self):
+ self.dot_widget.Refresh()
+
+class WxZoomAreaAction(WxDragAction):
+ def drag(self, deltax, deltay):
+ self.dot_widget.Refresh()
+
+ def draw(self, cr):
+ cr.save()
+ cr.set_source_rgba(.5, .5, 1.0, 0.25)
+ cr.rectangle(self.startmousex, self.startmousey,
+ self.prevmousex - self.startmousex,
+ self.prevmousey - self.startmousey)
+ cr.fill()
+ cr.set_source_rgba(.5, .5, 1.0, 1.0)
+ cr.set_line_width(1)
+ cr.rectangle(self.startmousex - .5, self.startmousey - .5,
+ self.prevmousex - self.startmousex + 1,
+ self.prevmousey - self.startmousey + 1)
+ cr.stroke()
+ cr.restore()
+
+ def stop(self):
+ x1, y1 = self.dot_widget.window2graph(self.startmousex,
+ self.startmousey)
+ x2, y2 = self.dot_widget.window2graph(self.stopmousex,
+ self.stopmousey)
+ self.dot_widget.zoom_to_area(x1, y1, x2, y2)
+
+ def abort(self):
+ self.dot_widget.Refresh()
+
+class WxDotWindow(wx.Panel):
+ """wxpython Frame that draws dot graphs."""
+ filter = 'dot'
+
+ def __init__(self, parent, id):
+ """constructor"""
+ wx.Panel.__init__(self, parent, id)
+
+ self.graph = Graph()
+ self.openfilename = None
+
+ self.x, self.y = 0.0, 0.0
+ self.zoom_ratio = 1.0
+ self.zoom_to_fit_on_resize = False
+ self.animation = NoAnimation(self)
+ self.drag_action = WxNullAction(self)
+ self.presstime = None
+ self.highlight = None
+
+ # Bind events
+ self.Bind(wx.EVT_PAINT, self.OnPaint)
+ self.Bind(wx.EVT_SIZE, self.OnResize)
+
+ self.Bind(wx.EVT_MOUSEWHEEL, self.OnScroll)
+
+ self.Bind(wx.EVT_MOUSE_EVENTS, self.OnMouse)
+
+ self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
+
+ # Callback register
+ self.select_cbs = []
+ self.dc = None
+ self.ctx = None
+
+ ### User callbacks
+ def register_select_callback(self, cb):
+ self.select_cbs.append(cb)
+
+ ### Event handlers
+ def OnResize(self, event):
+ self.Refresh()
+
+ def OnPaint(self, event):
+ """Redraw the graph."""
+ dc = wx.PaintDC(self)
+
+ #print dc
+ ctx = wxcairo.ContextFromDC(dc)
+ ctx = pangocairo.CairoContext(ctx)
+
+ # Get widget size
+ width, height = self.GetSize()
+ #width,height = self.dc.GetSizeTuple()
+
+ ctx.rectangle(0,0,width,height)
+ ctx.clip()
+
+ ctx.set_source_rgba(1.0, 1.0, 1.0, 1.0)
+ ctx.paint()
+
+ ctx.save()
+ ctx.translate(0.5*width, 0.5*height)
+
+ ctx.scale(self.zoom_ratio, self.zoom_ratio)
+ ctx.translate(-self.x, -self.y)
+ self.graph.draw(ctx, highlight_items=self.highlight)
+ ctx.restore()
+
+ self.drag_action.draw(ctx)
+
+ def OnScroll(self, event):
+ """Zoom the view."""
+ if event.GetWheelRotation() > 0:
+ self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT,
+ pos=(event.GetX(), event.GetY()))
+ else:
+ self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT,
+ pos=(event.GetX(), event.GetY()))
+
+ def OnKeyDown(self, event):
+ """Process key down event."""
+ key = event.GetKeyCode()
+ if key == wx.WXK_LEFT:
+ self.x -= self.POS_INCREMENT/self.zoom_ratio
+ self.Refresh()
+ if key == wx.WXK_RIGHT:
+ self.x += self.POS_INCREMENT/self.zoom_ratio
+ self.Refresh()
+ if key == wx.WXK_UP:
+ self.y -= self.POS_INCREMENT/self.zoom_ratio
+ self.Refresh()
+ if key == wx.WXK_DOWN:
+ self.y += self.POS_INCREMENT/self.zoom_ratio
+ self.Refresh()
+ if key == wx.WXK_PAGEUP:
+ self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT)
+ self.Refresh()
+ if key == wx.WXK_PAGEDOWN:
+ self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT)
+ self.Refresh()
+ if key == wx.WXK_ESCAPE:
+ self.drag_action.abort()
+ self.drag_action = WxNullAction(self)
+ if key == ord('F'):
+ self.zoom_to_fit()
+ if key == ord('R'):
+ self.reload()
+ if key == ord('Q'):
+ self.reload()
+ exit(0)
+
+ event.Skip()
+
+ ### Helper functions
+ def get_current_pos(self):
+ """Get the current graph position."""
+ return self.x, self.y
+
+ def set_current_pos(self, x, y):
+ """Set the current graph position."""
+ self.x = x
+ self.y = y
+ self.Refresh()
+
+ def set_highlight(self, items):
+ """Set a number of items to be hilighted."""
+ if self.highlight != items:
+ self.highlight = items
+ self.Refresh()
+
+ ### Cursor manipulation
+ def set_cursor(self, cursor_type):
+ self.cursor = wx.StockCursor(cursor_type)
+ self.SetCursor(self.cursor)
+
+ ### Zooming methods
+ def zoom_image(self, zoom_ratio, center=False, pos=None):
+ """Zoom the graph."""
+ if center:
+ self.x = self.graph.width/2
+ self.y = self.graph.height/2
+ elif pos is not None:
+ width, height = self.GetSize()
+ x, y = pos
+ x -= 0.5*width
+ y -= 0.5*height
+ self.x += x / self.zoom_ratio - x / zoom_ratio
+ self.y += y / self.zoom_ratio - y / zoom_ratio
+ self.zoom_ratio = zoom_ratio
+ self.zoom_to_fit_on_resize = False
+ self.Refresh()
+
+ def zoom_to_area(self, x1, y1, x2, y2):
+ """Zoom to an area of the graph."""
+ width, height = self.GetSize()
+ area_width = abs(x1 - x2)
+ area_height = abs(y1 - y2)
+ self.zoom_ratio = min(
+ float(width)/float(area_width),
+ float(height)/float(area_height)
+ )
+ self.zoom_to_fit_on_resize = False
+ self.x = (x1 + x2) / 2
+ self.y = (y1 + y2) / 2
+ self.Refresh()
+
+ def zoom_to_fit(self):
+ """Zoom to fit the size of the graph."""
+ width,height = self.GetSize()
+ x = self.ZOOM_TO_FIT_MARGIN
+ y = self.ZOOM_TO_FIT_MARGIN
+ width -= 2 * self.ZOOM_TO_FIT_MARGIN
+ height -= 2 * self.ZOOM_TO_FIT_MARGIN
+
+ if float(self.graph.width) > 0 and float(self.graph.height) > 0 and width > 0 and height > 0:
+ zoom_ratio = min(
+ float(width)/float(self.graph.width),
+ float(height)/float(self.graph.height)
+ )
+ self.zoom_image(zoom_ratio, center=True)
+ self.zoom_to_fit_on_resize = True
+
+ ZOOM_INCREMENT = 1.25
+ ZOOM_TO_FIT_MARGIN = 12
+
+ def on_zoom_in(self, action):
+ self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT)
+
+ def on_zoom_out(self, action):
+ self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT)
+
+ def on_zoom_fit(self, action):
+ self.zoom_to_fit()
+
+ def on_zoom_100(self, action):
+ self.zoom_image(1.0)
+
+ POS_INCREMENT = 100
+
+ def get_drag_action(self, event):
+ """Get a drag action for this click."""
+ # Grab the button
+ button = event.GetButton()
+ # Grab modifier keys
+ control_down = event.ControlDown()
+ alt_down = event.AltDown()
+ shift_down = event.ShiftDown()
+
+ drag = event.Dragging()
+ motion = event.Moving()
+
+ # Get the correct drag action for this click
+ if button in (wx.MOUSE_BTN_LEFT, wx.MOUSE_BTN_MIDDLE): # left or middle button
+ if control_down:
+ if shift_down:
+ return WxZoomAreaAction(self)
+ else:
+ return WxZoomAction(self)
+ else:
+ return WxPanAction(self)
+
+ return WxNullAction(self)
+
+ def OnMouse(self, event):
+ x,y = event.GetPositionTuple()
+
+ item = None
+
+ # Get the item
+ if not event.Dragging():
+ item = self.get_url(x, y)
+ if item is None:
+ item = self.get_jump(x, y)
+
+ if item is not None:
+ self.set_cursor(wx.CURSOR_HAND)
+ self.set_highlight(item.highlight)
+
+ for cb in self.select_cbs:
+ cb(item,event)
+ else:
+ self.set_cursor(wx.CURSOR_ARROW)
+ self.set_highlight(None)
+
+ if item is None:
+ if event.ButtonDown():
+ self.animation.stop()
+ self.drag_action.abort()
+
+ # Get the drag action
+ self.drag_action = self.get_drag_action(event)
+ self.drag_action.on_button_press(event)
+
+ self.pressx = x
+ self.pressy = y
+
+ if event.Dragging() or event.Moving():
+ self.drag_action.on_motion_notify(event)
+
+ if event.ButtonUp():
+ self.drag_action.on_button_release(event)
+ self.drag_action = WxNullAction(self)
+
+ event.Skip()
+
+
+ def on_area_size_allocate(self, area, allocation):
+ if self.zoom_to_fit_on_resize:
+ self.zoom_to_fit()
+
+ def animate_to(self, x, y):
+ self.animation = ZoomToAnimation(self, x, y)
+ self.animation.start()
+
+ def window2graph(self, x, y):
+ "Get the x,y coordinates in the graph from the x,y coordinates in the window."""
+ width, height = self.GetSize()
+ x -= 0.5*width
+ y -= 0.5*height
+ x /= self.zoom_ratio
+ y /= self.zoom_ratio
+ x += self.x
+ y += self.y
+ return x, y
+
+ def get_url(self, x, y):
+ x, y = self.window2graph(x, y)
+ return self.graph.get_url(x, y)
+
+ def get_jump(self, x, y):
+ x, y = self.window2graph(x, y)
+ return self.graph.get_jump(x, y)
+
+ def set_filter(self, filter):
+ self.filter = filter
+
+ def set_dotcode(self, dotcode, filename=''):
+ if isinstance(dotcode, unicode):
+ dotcode = dotcode.encode('utf8')
+ p = subprocess.Popen(
+ [self.filter, '-Txdot'],
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ shell=False,
+ universal_newlines=True
+ )
+ xdotcode, error = p.communicate(dotcode)
+ if p.returncode != 0:
+ print "ERROR PARSING DOT CODE", error
+ dialog = gtk.MessageDialog(type=gtk.MESSAGE_ERROR,
+ message_format=error,
+ buttons=gtk.BUTTONS_OK)
+ dialog.set_title('Dot Viewer')
+ dialog.run()
+ dialog.destroy()
+ return False
+ try:
+ self.set_xdotcode(xdotcode)
+ except ParseError, ex:
+ print "ERROR PARSING XDOT CODE"
+ dialog = gtk.MessageDialog(type=gtk.MESSAGE_ERROR,
+ message_format=str(ex),
+ buttons=gtk.BUTTONS_OK)
+ dialog.set_title('Dot Viewer')
+ dialog.run()
+ dialog.destroy()
+ return False
+ else:
+ self.openfilename = filename
+ return True
+
+ def set_xdotcode(self, xdotcode):
+ """Set xdot code."""
+ #print xdotcode
+ parser = XDotParser(xdotcode)
+ self.graph = parser.parse()
+ self.highlight = None
+ #self.zoom_image(self.zoom_ratio, center=True)
+
+ def reload(self):
+ if self.openfilename is not None:
+ try:
+ fp = file(self.openfilename, 'rt')
+ self.set_dotcode(fp.read(), self.openfilename)
+ fp.close()
+ except IOError:
+ pass
+
+
+class WxDotFrame(wx.Frame):
+ def __init__(self):
+ wx.Frame.__init__(self, None, -1, "Dot Viewer", size=(512,512))
+
+ vbox = wx.BoxSizer(wx.VERTICAL)
+
+ # Construct toolbar
+ toolbar = wx.ToolBar(self, -1)
+ toolbar.AddLabelTool(wx.ID_OPEN, 'Open File',
+ wx.ArtProvider.GetBitmap(wx.ART_FOLDER_OPEN,wx.ART_OTHER,(16,16)))
+ toolbar.AddLabelTool(wx.ID_HELP, 'Help',
+ wx.ArtProvider.GetBitmap(wx.ART_HELP,wx.ART_OTHER,(16,16)) )
+ toolbar.Realize()
+
+ self.Bind(wx.EVT_TOOL, self.DoOpenFile, id=wx.ID_OPEN)
+ self.Bind(wx.EVT_TOOL, self.ShowControlsDialog, id=wx.ID_HELP)
+
+ # Create dot widge
+ self.widget = WxDotWindow(self, -1)
+
+ # Add elements to sizer
+ vbox.Add(toolbar, 0, wx.EXPAND)
+ vbox.Add(self.widget, 100, wx.EXPAND | wx.ALL)
+
+ self.SetSizer(vbox)
+ self.Center()
+
+ def ShowControlsDialog(self,event):
+ dial = wx.MessageDialog(None,
+ "\
+Pan: Arrow Keys\n\
+Zoom: PageUp / PageDown\n\
+Zoom To Fit: F\n\
+Refresh: R",
+ 'Keyboard Controls', wx.OK)
+ dial.ShowModal()
+
+ def DoOpenFile(self,event):
+ wcd = 'All files (*)|*|GraphViz Dot Files(*.dot)|*.dot|'
+ dir = os.getcwd()
+ open_dlg = wx.FileDialog(self, message='Choose a file', defaultDir=dir, defaultFile='',
+ wildcard=wcd, style=wx.OPEN|wx.CHANGE_DIR)
+ if open_dlg.ShowModal() == wx.ID_OK:
+ path = open_dlg.GetPath()
+
+ try:
+ self.open_file(path)
+
+ except IOError, error:
+ dlg = wx.MessageDialog(self, 'Error opening file\n' + str(error))
+ dlg.ShowModal()
+
+ except UnicodeDecodeError, error:
+ dlg = wx.MessageDialog(self, 'Error opening file\n' + str(error))
+ dlg.ShowModal()
+
+ open_dlg.Destroy()
+
+ def OnExit(self, event):
+ pass
+
+ def set_dotcode(self, dotcode, filename=''):
+ if self.widget.set_dotcode(dotcode, filename):
+ self.SetTitle(os.path.basename(filename) + ' - Dot Viewer')
+ self.widget.zoom_to_fit()
+
+ def set_xdotcode(self, xdotcode, filename=''):
+ if self.widget.set_xdotcode(xdotcode):
+ self.SetTitle(os.path.basename(filename) + ' - Dot Viewer')
+ self.widget.zoom_to_fit()
+
+ def open_file(self, filename):
+ try:
+ fp = file(filename, 'rt')
+ self.set_dotcode(fp.read(), filename)
+ fp.close()
+ except IOError, ex:
+ """
+ dlg = gtk.MessageDialog(type=gtk.MESSAGE_ERROR,
+ message_format=str(ex),
+ buttons=gtk.BUTTONS_OK)
+ dlg.set_title('Dot Viewer')
+ dlg.run()
+ dlg.destroy()
+ """
+
+ def set_filter(self, filter):
+ self.widget.set_filter(filter)
+
diff --git a/tools/xdot/src/xdot/xdot.py b/tools/xdot/src/xdot/xdot.py
new file mode 100755
index 00000000..564a1b05
--- /dev/null
+++ b/tools/xdot/src/xdot/xdot.py
@@ -0,0 +1,2163 @@
+#!/usr/bin/env python
+#
+# Copyright 2008 Jose Fonseca
+#
+# 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 .
+#
+
+'''Visualize dot graphs via the xdot format.'''
+
+__author__ = "Jose Fonseca"
+
+__version__ = "0.4"
+
+
+import os
+import sys
+import subprocess
+import math
+import colorsys
+import time
+import re
+
+import gobject
+import gtk
+import gtk.gdk
+import gtk.keysyms
+import cairo
+import pango
+import pangocairo
+
+
+# See http://www.graphviz.org/pub/scm/graphviz-cairo/plugin/cairo/gvrender_cairo.c
+
+# For pygtk inspiration and guidance see:
+# - http://mirageiv.berlios.de/
+# - http://comix.sourceforge.net/
+
+
+class Pen:
+ """Store pen attributes."""
+
+ def __init__(self):
+ # set default attributes
+ self.color = (0.0, 0.0, 0.0, 1.0)
+ self.fillcolor = (0.0, 0.0, 0.0, 1.0)
+ self.linewidth = 1.0
+ self.fontsize = 14.0
+ self.fontname = "Times-Roman"
+ self.dash = ()
+
+ def copy(self):
+ """Create a copy of this pen."""
+ pen = Pen()
+ pen.__dict__ = self.__dict__.copy()
+ return pen
+
+ def highlighted(self):
+ pen = self.copy()
+ pen.color = (1, 0, 0, 1)
+ pen.fillcolor = (1, .8, .8, 1)
+ return pen
+
+
+class Shape:
+ """Abstract base class for all the drawing shapes."""
+
+ def __init__(self):
+ pass
+
+ def draw(self, cr, highlight=False):
+ """Draw this shape with the given cairo context"""
+ raise NotImplementedError
+
+ def select_pen(self, highlight):
+ if highlight:
+ if not hasattr(self, 'highlight_pen'):
+ self.highlight_pen = self.pen.highlighted()
+ return self.highlight_pen
+ else:
+ return self.pen
+
+
+class TextShape(Shape):
+
+ #fontmap = pangocairo.CairoFontMap()
+ #fontmap.set_resolution(72)
+ #context = fontmap.create_context()
+
+ LEFT, CENTER, RIGHT = -1, 0, 1
+
+ def __init__(self, pen, x, y, j, w, t):
+ Shape.__init__(self)
+ self.pen = pen.copy()
+ self.x = x
+ self.y = y
+ self.j = j
+ self.w = w
+ self.t = t
+
+ def draw(self, cr, highlight=False):
+
+ try:
+ layout = self.layout
+ except AttributeError:
+ layout = cr.create_layout()
+
+ # set font options
+ # see http://lists.freedesktop.org/archives/cairo/2007-February/009688.html
+ context = layout.get_context()
+ fo = cairo.FontOptions()
+ fo.set_antialias(cairo.ANTIALIAS_DEFAULT)
+ fo.set_hint_style(cairo.HINT_STYLE_NONE)
+ fo.set_hint_metrics(cairo.HINT_METRICS_OFF)
+ try:
+ pangocairo.context_set_font_options(context, fo)
+ except TypeError:
+ # XXX: Some broken pangocairo bindings show the error
+ # 'TypeError: font_options must be a cairo.FontOptions or None'
+ pass
+
+ # set font
+ font = pango.FontDescription()
+ font.set_family(self.pen.fontname)
+ font.set_absolute_size(self.pen.fontsize*pango.SCALE)
+ layout.set_font_description(font)
+
+ # set text
+ layout.set_text(self.t)
+
+ # cache it
+ self.layout = layout
+ else:
+ cr.update_layout(layout)
+
+ descent = 2 # XXX get descender from font metrics
+
+ width, height = layout.get_size()
+ width = float(width)/pango.SCALE
+ height = float(height)/pango.SCALE
+ # we know the width that dot thinks this text should have
+ # we do not necessarily have a font with the same metrics
+ # scale it so that the text fits inside its box
+ if width > self.w:
+ f = self.w / width
+ width = self.w # equivalent to width *= f
+ height *= f
+ descent *= f
+ else:
+ f = 1.0
+
+ if self.j == self.LEFT:
+ x = self.x
+ elif self.j == self.CENTER:
+ x = self.x - 0.5*width
+ elif self.j == self.RIGHT:
+ x = self.x - width
+ else:
+ assert 0
+
+ y = self.y - height + descent
+
+ cr.move_to(x, y)
+
+ cr.save()
+ cr.scale(f, f)
+ cr.set_source_rgba(*self.select_pen(highlight).color)
+ cr.show_layout(layout)
+ cr.restore()
+
+ if 0: # DEBUG
+ # show where dot thinks the text should appear
+ cr.set_source_rgba(1, 0, 0, .9)
+ if self.j == self.LEFT:
+ x = self.x
+ elif self.j == self.CENTER:
+ x = self.x - 0.5*self.w
+ elif self.j == self.RIGHT:
+ x = self.x - self.w
+ cr.move_to(x, self.y)
+ cr.line_to(x+self.w, self.y)
+ cr.stroke()
+
+
+class EllipseShape(Shape):
+
+ def __init__(self, pen, x0, y0, w, h, filled=False):
+ Shape.__init__(self)
+ self.pen = pen.copy()
+ self.x0 = x0
+ self.y0 = y0
+ self.w = w
+ self.h = h
+ self.filled = filled
+
+ def draw(self, cr, highlight=False):
+ cr.save()
+ cr.translate(self.x0, self.y0)
+ cr.scale(self.w, self.h)
+ cr.move_to(1.0, 0.0)
+ cr.arc(0.0, 0.0, 1.0, 0, 2.0*math.pi)
+ cr.restore()
+ pen = self.select_pen(highlight)
+ if self.filled:
+ cr.set_source_rgba(*pen.fillcolor)
+ cr.fill()
+ else:
+ cr.set_dash(pen.dash)
+ cr.set_line_width(pen.linewidth)
+ cr.set_source_rgba(*pen.color)
+ cr.stroke()
+
+
+class PolygonShape(Shape):
+
+ def __init__(self, pen, points, filled=False):
+ Shape.__init__(self)
+ self.pen = pen.copy()
+ self.points = points
+ self.filled = filled
+
+ def draw(self, cr, highlight=False):
+ x0, y0 = self.points[-1]
+ cr.move_to(x0, y0)
+ for x, y in self.points:
+ cr.line_to(x, y)
+ cr.close_path()
+ pen = self.select_pen(highlight)
+ if self.filled:
+ cr.set_source_rgba(*pen.fillcolor)
+ cr.fill_preserve()
+ cr.fill()
+ else:
+ cr.set_dash(pen.dash)
+ cr.set_line_width(pen.linewidth)
+ cr.set_source_rgba(*pen.color)
+ cr.stroke()
+
+
+class LineShape(Shape):
+
+ def __init__(self, pen, points):
+ Shape.__init__(self)
+ self.pen = pen.copy()
+ self.points = points
+
+ def draw(self, cr, highlight=False):
+ x0, y0 = self.points[0]
+ cr.move_to(x0, y0)
+ for x1, y1 in self.points[1:]:
+ cr.line_to(x1, y1)
+ pen = self.select_pen(highlight)
+ cr.set_dash(pen.dash)
+ cr.set_line_width(pen.linewidth)
+ cr.set_source_rgba(*pen.color)
+ cr.stroke()
+
+
+class BezierShape(Shape):
+
+ def __init__(self, pen, points, filled=False):
+ Shape.__init__(self)
+ self.pen = pen.copy()
+ self.points = points
+ self.filled = filled
+
+ def draw(self, cr, highlight=False):
+ x0, y0 = self.points[0]
+ cr.move_to(x0, y0)
+ for i in xrange(1, len(self.points), 3):
+ x1, y1 = self.points[i]
+ x2, y2 = self.points[i + 1]
+ x3, y3 = self.points[i + 2]
+ cr.curve_to(x1, y1, x2, y2, x3, y3)
+ pen = self.select_pen(highlight)
+ if self.filled:
+ cr.set_source_rgba(*pen.fillcolor)
+ cr.fill_preserve()
+ cr.fill()
+ else:
+ cr.set_dash(pen.dash)
+ cr.set_line_width(pen.linewidth)
+ cr.set_source_rgba(*pen.color)
+ cr.stroke()
+
+
+class CompoundShape(Shape):
+
+ def __init__(self, shapes):
+ Shape.__init__(self)
+ self.shapes = shapes
+
+ def draw(self, cr, highlight=False):
+ for shape in self.shapes:
+ shape.draw(cr, highlight=highlight)
+
+
+class Url(object):
+
+ def __init__(self, item, url, highlight=None):
+ self.item = item
+ self.url = url
+ if highlight is None:
+ highlight = set([item])
+ self.highlight = highlight
+
+
+class Jump(object):
+
+ def __init__(self, item, x, y, highlight=None, url=None):
+ self.item = item
+ self.x = x
+ self.y = y
+ if highlight is None:
+ highlight = set([item])
+ self.highlight = highlight
+ self.url = url
+
+
+class Element(CompoundShape):
+ """Base class for graph nodes and edges."""
+
+ def __init__(self, shapes):
+ CompoundShape.__init__(self, shapes)
+
+ def get_url(self, x, y):
+ return None
+
+ def get_jump(self, x, y):
+ return None
+
+
+class Node(Element):
+
+ def __init__(self, x, y, w, h, shapes, url):
+ Element.__init__(self, shapes)
+
+ self.x = x
+ self.y = y
+
+ self.x1 = x - 0.5*w
+ self.y1 = y - 0.5*h
+ self.x2 = x + 0.5*w
+ self.y2 = y + 0.5*h
+
+ self.url = url
+
+ def is_inside(self, x, y):
+ return self.x1 <= x and x <= self.x2 and self.y1 <= y and y <= self.y2
+
+ def get_url(self, x, y):
+ if self.url is None:
+ return None
+ #print (x, y), (self.x1, self.y1), "-", (self.x2, self.y2)
+ if self.is_inside(x, y):
+ return Url(self, self.url)
+ return None
+
+ def get_jump(self, x, y):
+ if self.is_inside(x, y):
+ return Jump(self, self.x, self.y)
+ return None
+
+
+def square_distance(x1, y1, x2, y2):
+ deltax = x2 - x1
+ deltay = y2 - y1
+ return deltax*deltax + deltay*deltay
+
+
+class Edge(Element):
+
+ def __init__(self, src, dst, points, shapes, url):
+ Element.__init__(self, shapes)
+ self.src = src
+ self.dst = dst
+ self.points = points
+ self.url = url
+
+ RADIUS = 10
+
+ def get_jump(self, x, y):
+ if square_distance(x, y, *self.points[0]) <= self.RADIUS*self.RADIUS:
+ return Jump(self, self.dst.x, self.dst.y, highlight=set([self, self.dst]),url=self.url)
+ if square_distance(x, y, *self.points[-1]) <= self.RADIUS*self.RADIUS:
+ return Jump(self, self.src.x, self.src.y, highlight=set([self, self.src]),url=self.url)
+ return None
+
+
+class Graph(Shape):
+
+ def __init__(self, width=1, height=1, shapes=(), nodes=(), edges=()):
+ Shape.__init__(self)
+
+ self.width = width
+ self.height = height
+ self.shapes = shapes
+ self.nodes = nodes
+ self.edges = edges
+
+ def get_size(self):
+ return self.width, self.height
+
+ def draw(self, cr, highlight_items=None):
+ if highlight_items is None:
+ highlight_items = ()
+ cr.set_source_rgba(0.0, 0.0, 0.0, 1.0)
+
+ cr.set_line_cap(cairo.LINE_CAP_BUTT)
+ cr.set_line_join(cairo.LINE_JOIN_MITER)
+
+ for shape in self.shapes:
+ shape.draw(cr)
+ for edge in self.edges:
+ edge.draw(cr, highlight=(edge in highlight_items))
+ for node in self.nodes:
+ node.draw(cr, highlight=(node in highlight_items))
+
+ def get_url(self, x, y):
+ for node in self.nodes:
+ url = node.get_url(x, y)
+ if url is not None:
+ return url
+ return None
+
+ def get_jump(self, x, y):
+ for edge in self.edges:
+ jump = edge.get_jump(x, y)
+ if jump is not None:
+ return jump
+ for node in self.nodes:
+ jump = node.get_jump(x, y)
+ if jump is not None:
+ return jump
+ return None
+
+
+class XDotAttrParser:
+ """Parser for xdot drawing attributes.
+ See also:
+ - http://www.graphviz.org/doc/info/output.html#d:xdot
+ """
+
+ def __init__(self, parser, buf):
+ self.parser = parser
+ self.buf = self.unescape(buf)
+ self.pos = 0
+
+ self.pen = Pen()
+ self.shapes = []
+
+ def __nonzero__(self):
+ return self.pos < len(self.buf)
+
+ def unescape(self, buf):
+ buf = buf.replace('\\"', '"')
+ buf = buf.replace('\\n', '\n')
+ return buf
+
+ def read_code(self):
+ pos = self.buf.find(" ", self.pos)
+ res = self.buf[self.pos:pos]
+ self.pos = pos + 1
+ while self.pos < len(self.buf) and self.buf[self.pos].isspace():
+ self.pos += 1
+ return res
+
+ def read_number(self):
+ return int(self.read_code())
+
+ def read_float(self):
+ return float(self.read_code())
+
+ def read_point(self):
+ x = self.read_number()
+ y = self.read_number()
+ return self.transform(x, y)
+
+ def read_text(self):
+ num = self.read_number()
+ pos = self.buf.find("-", self.pos) + 1
+ self.pos = pos + num
+ res = self.buf[pos:self.pos]
+ while self.pos < len(self.buf) and self.buf[self.pos].isspace():
+ self.pos += 1
+ return res
+
+ def read_polygon(self):
+ n = self.read_number()
+ p = []
+ for i in range(n):
+ x, y = self.read_point()
+ p.append((x, y))
+ return p
+
+ def read_color(self):
+ # See http://www.graphviz.org/doc/info/attrs.html#k:color
+ c = self.read_text()
+ c1 = c[:1]
+ if c1 == '#':
+ hex2float = lambda h: float(int(h, 16)/255.0)
+ r = hex2float(c[1:3])
+ g = hex2float(c[3:5])
+ b = hex2float(c[5:7])
+ try:
+ a = hex2float(c[7:9])
+ except (IndexError, ValueError):
+ a = 1.0
+ return r, g, b, a
+ elif c1.isdigit() or c1 == ".":
+ # "H,S,V" or "H S V" or "H, S, V" or any other variation
+ h, s, v = map(float, c.replace(",", " ").split())
+ r, g, b = colorsys.hsv_to_rgb(h, s, v)
+ a = 1.0
+ return r, g, b, a
+ else:
+ return self.lookup_color(c)
+
+ def lookup_color(self, c):
+ try:
+ color = gtk.gdk.color_parse(c)
+ except ValueError:
+ pass
+ else:
+ s = 1.0/65535.0
+ r = color.red*s
+ g = color.green*s
+ b = color.blue*s
+ a = 1.0
+ return r, g, b, a
+
+ try:
+ dummy, scheme, index = c.split('/')
+ r, g, b = brewer_colors[scheme][int(index)]
+ except (ValueError, KeyError):
+ pass
+ else:
+ s = 1.0/255.0
+ r = r*s
+ g = g*s
+ b = b*s
+ a = 1.0
+ return r, g, b, a
+
+ sys.stderr.write("unknown color '%s'\n" % c)
+ return None
+
+ def parse(self):
+ s = self
+
+ while s:
+ op = s.read_code()
+ if op == "c":
+ color = s.read_color()
+ if color is not None:
+ self.handle_color(color, filled=False)
+ elif op == "C":
+ color = s.read_color()
+ if color is not None:
+ self.handle_color(color, filled=True)
+ elif op == "S":
+ # http://www.graphviz.org/doc/info/attrs.html#k:style
+ style = s.read_text()
+ if style.startswith("setlinewidth("):
+ lw = style.split("(")[1].split(")")[0]
+ lw = float(lw)
+ self.handle_linewidth(lw)
+ elif style in ("solid", "dashed"):
+ self.handle_linestyle(style)
+ elif op == "F":
+ size = s.read_float()
+ name = s.read_text()
+ self.handle_font(size, name)
+ elif op == "T":
+ x, y = s.read_point()
+ j = s.read_number()
+ w = s.read_number()
+ t = s.read_text()
+ self.handle_text(x, y, j, w, t)
+ elif op == "E":
+ x0, y0 = s.read_point()
+ w = s.read_number()
+ h = s.read_number()
+ self.handle_ellipse(x0, y0, w, h, filled=True)
+ elif op == "e":
+ x0, y0 = s.read_point()
+ w = s.read_number()
+ h = s.read_number()
+ self.handle_ellipse(x0, y0, w, h, filled=False)
+ elif op == "L":
+ points = self.read_polygon()
+ self.handle_line(points)
+ elif op == "B":
+ points = self.read_polygon()
+ self.handle_bezier(points, filled=False)
+ elif op == "b":
+ points = self.read_polygon()
+ self.handle_bezier(points, filled=True)
+ elif op == "P":
+ points = self.read_polygon()
+ self.handle_polygon(points, filled=True)
+ elif op == "p":
+ points = self.read_polygon()
+ self.handle_polygon(points, filled=False)
+ else:
+ sys.stderr.write("unknown xdot opcode '%s'\n" % op)
+ break
+
+ return self.shapes
+
+ def transform(self, x, y):
+ return self.parser.transform(x, y)
+
+ def handle_color(self, color, filled=False):
+ if filled:
+ self.pen.fillcolor = color
+ else:
+ self.pen.color = color
+
+ def handle_linewidth(self, linewidth):
+ self.pen.linewidth = linewidth
+
+ def handle_linestyle(self, style):
+ if style == "solid":
+ self.pen.dash = ()
+ elif style == "dashed":
+ self.pen.dash = (6, ) # 6pt on, 6pt off
+
+ def handle_font(self, size, name):
+ self.pen.fontsize = size
+ self.pen.fontname = name
+
+ def handle_text(self, x, y, j, w, t):
+ self.shapes.append(TextShape(self.pen, x, y, j, w, t))
+
+ def handle_ellipse(self, x0, y0, w, h, filled=False):
+ if filled:
+ # xdot uses this to mean "draw a filled shape with an outline"
+ self.shapes.append(EllipseShape(self.pen, x0, y0, w, h, filled=True))
+ self.shapes.append(EllipseShape(self.pen, x0, y0, w, h))
+
+ def handle_line(self, points):
+ self.shapes.append(LineShape(self.pen, points))
+
+ def handle_bezier(self, points, filled=False):
+ if filled:
+ # xdot uses this to mean "draw a filled shape with an outline"
+ self.shapes.append(BezierShape(self.pen, points, filled=True))
+ self.shapes.append(BezierShape(self.pen, points))
+
+ def handle_polygon(self, points, filled=False):
+ if filled:
+ # xdot uses this to mean "draw a filled shape with an outline"
+ self.shapes.append(PolygonShape(self.pen, points, filled=True))
+ self.shapes.append(PolygonShape(self.pen, points))
+
+
+EOF = -1
+SKIP = -2
+
+
+class ParseError(Exception):
+
+ def __init__(self, msg=None, filename=None, line=None, col=None):
+ self.msg = msg
+ self.filename = filename
+ self.line = line
+ self.col = col
+
+ def __str__(self):
+ return ':'.join([str(part) for part in (self.filename, self.line, self.col, self.msg) if part != None])
+
+
+class Scanner:
+ """Stateless scanner."""
+
+ # should be overriden by derived classes
+ tokens = []
+ symbols = {}
+ literals = {}
+ ignorecase = False
+
+ def __init__(self):
+ flags = re.DOTALL
+ if self.ignorecase:
+ flags |= re.IGNORECASE
+ self.tokens_re = re.compile(
+ '|'.join(['(' + regexp + ')' for type, regexp, test_lit in self.tokens]),
+ flags
+ )
+
+ def next(self, buf, pos):
+ if pos >= len(buf):
+ return EOF, '', pos
+ mo = self.tokens_re.match(buf, pos)
+ if mo:
+ text = mo.group()
+ type, regexp, test_lit = self.tokens[mo.lastindex - 1]
+ pos = mo.end()
+ if test_lit:
+ type = self.literals.get(text, type)
+ return type, text, pos
+ else:
+ c = buf[pos]
+ return self.symbols.get(c, None), c, pos + 1
+
+
+class Token:
+
+ def __init__(self, type, text, line, col):
+ self.type = type
+ self.text = text
+ self.line = line
+ self.col = col
+
+
+class Lexer:
+
+ # should be overriden by derived classes
+ scanner = None
+ tabsize = 8
+
+ newline_re = re.compile(r'\r\n?|\n')
+
+ def __init__(self, buf = None, pos = 0, filename = None, fp = None):
+ if fp is not None:
+ try:
+ fileno = fp.fileno()
+ length = os.path.getsize(fp.name)
+ import mmap
+ except:
+ # read whole file into memory
+ buf = fp.read()
+ pos = 0
+ else:
+ # map the whole file into memory
+ if length:
+ # length must not be zero
+ buf = mmap.mmap(fileno, length, access = mmap.ACCESS_READ)
+ pos = os.lseek(fileno, 0, 1)
+ else:
+ buf = ''
+ pos = 0
+
+ if filename is None:
+ try:
+ filename = fp.name
+ except AttributeError:
+ filename = None
+
+ self.buf = buf
+ self.pos = pos
+ self.line = 1
+ self.col = 1
+ self.filename = filename
+
+ def next(self):
+ while True:
+ # save state
+ pos = self.pos
+ line = self.line
+ col = self.col
+
+ type, text, endpos = self.scanner.next(self.buf, pos)
+ assert pos + len(text) == endpos
+ self.consume(text)
+ type, text = self.filter(type, text)
+ self.pos = endpos
+
+ if type == SKIP:
+ continue
+ elif type is None:
+ msg = 'unexpected char '
+ if text >= ' ' and text <= '~':
+ msg += "'%s'" % text
+ else:
+ msg += "0x%X" % ord(text)
+ raise ParseError(msg, self.filename, line, col)
+ else:
+ break
+ return Token(type = type, text = text, line = line, col = col)
+
+ def consume(self, text):
+ # update line number
+ pos = 0
+ for mo in self.newline_re.finditer(text, pos):
+ self.line += 1
+ self.col = 1
+ pos = mo.end()
+
+ # update column number
+ while True:
+ tabpos = text.find('\t', pos)
+ if tabpos == -1:
+ break
+ self.col += tabpos - pos
+ self.col = ((self.col - 1)//self.tabsize + 1)*self.tabsize + 1
+ pos = tabpos + 1
+ self.col += len(text) - pos
+
+
+class Parser:
+
+ def __init__(self, lexer):
+ self.lexer = lexer
+ self.lookahead = self.lexer.next()
+
+ def match(self, type):
+ if self.lookahead.type != type:
+ raise ParseError(
+ msg = 'unexpected token %r' % self.lookahead.text,
+ filename = self.lexer.filename,
+ line = self.lookahead.line,
+ col = self.lookahead.col)
+
+ def skip(self, type):
+ while self.lookahead.type != type:
+ self.consume()
+
+ def consume(self):
+ token = self.lookahead
+ self.lookahead = self.lexer.next()
+ return token
+
+
+ID = 0
+STR_ID = 1
+HTML_ID = 2
+EDGE_OP = 3
+
+LSQUARE = 4
+RSQUARE = 5
+LCURLY = 6
+RCURLY = 7
+COMMA = 8
+COLON = 9
+SEMI = 10
+EQUAL = 11
+PLUS = 12
+
+STRICT = 13
+GRAPH = 14
+DIGRAPH = 15
+NODE = 16
+EDGE = 17
+SUBGRAPH = 18
+
+
+class DotScanner(Scanner):
+
+ # token regular expression table
+ tokens = [
+ # whitespace and comments
+ (SKIP,
+ r'[ \t\f\r\n\v]+|'
+ r'//[^\r\n]*|'
+ r'/\*.*?\*/|'
+ r'#[^\r\n]*',
+ False),
+
+ # Alphanumeric IDs
+ (ID, r'[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*', True),
+
+ # Numeric IDs
+ (ID, r'-?(?:\.[0-9]+|[0-9]+(?:\.[0-9]*)?)', False),
+
+ # String IDs
+ (STR_ID, r'"[^"\\]*(?:\\.[^"\\]*)*"', False),
+
+ # HTML IDs
+ (HTML_ID, r'<[^<>]*(?:<[^<>]*>[^<>]*)*>', False),
+
+ # Edge operators
+ (EDGE_OP, r'-[>-]', False),
+ ]
+
+ # symbol table
+ symbols = {
+ '[': LSQUARE,
+ ']': RSQUARE,
+ '{': LCURLY,
+ '}': RCURLY,
+ ',': COMMA,
+ ':': COLON,
+ ';': SEMI,
+ '=': EQUAL,
+ '+': PLUS,
+ }
+
+ # literal table
+ literals = {
+ 'strict': STRICT,
+ 'graph': GRAPH,
+ 'digraph': DIGRAPH,
+ 'node': NODE,
+ 'edge': EDGE,
+ 'subgraph': SUBGRAPH,
+ }
+
+ ignorecase = True
+
+
+class DotLexer(Lexer):
+
+ scanner = DotScanner()
+
+ def filter(self, type, text):
+ # TODO: handle charset
+ if type == STR_ID:
+ text = text[1:-1]
+
+ # line continuations
+ text = text.replace('\\\r\n', '')
+ text = text.replace('\\\r', '')
+ text = text.replace('\\\n', '')
+
+ text = text.replace('\\r', '\r')
+ text = text.replace('\\n', '\n')
+ text = text.replace('\\t', '\t')
+ text = text.replace('\\', '')
+
+ type = ID
+
+ elif type == HTML_ID:
+ text = text[1:-1]
+ type = ID
+
+ return type, text
+
+
+class DotParser(Parser):
+
+ def __init__(self, lexer):
+ Parser.__init__(self, lexer)
+ self.graph_attrs = {}
+ self.node_attrs = {}
+ self.edge_attrs = {}
+
+ def parse(self):
+ self.parse_graph()
+ self.match(EOF)
+
+ def parse_graph(self):
+ if self.lookahead.type == STRICT:
+ self.consume()
+ self.skip(LCURLY)
+ self.consume()
+ while self.lookahead.type != RCURLY:
+ self.parse_stmt()
+ self.consume()
+
+ def parse_subgraph(self):
+ id = None
+ if self.lookahead.type == SUBGRAPH:
+ self.consume()
+ if self.lookahead.type == ID:
+ id = self.lookahead.text
+ self.consume()
+ if self.lookahead.type == LCURLY:
+ self.consume()
+ while self.lookahead.type != RCURLY:
+ self.parse_stmt()
+ self.consume()
+ return id
+
+ def parse_stmt(self):
+ if self.lookahead.type == GRAPH:
+ self.consume()
+ attrs = self.parse_attrs()
+ self.graph_attrs.update(attrs)
+ self.handle_graph(attrs)
+ elif self.lookahead.type == NODE:
+ self.consume()
+ self.node_attrs.update(self.parse_attrs())
+ elif self.lookahead.type == EDGE:
+ self.consume()
+ self.edge_attrs.update(self.parse_attrs())
+ elif self.lookahead.type in (SUBGRAPH, LCURLY):
+ self.parse_subgraph()
+ else:
+ id = self.parse_node_id()
+ if self.lookahead.type == EDGE_OP:
+ self.consume()
+ node_ids = [id, self.parse_node_id()]
+ while self.lookahead.type == EDGE_OP:
+ node_ids.append(self.parse_node_id())
+ attrs = self.parse_attrs()
+ for i in range(0, len(node_ids) - 1):
+ self.handle_edge(node_ids[i], node_ids[i + 1], attrs)
+ elif self.lookahead.type == EQUAL:
+ self.consume()
+ self.parse_id()
+ else:
+ attrs = self.parse_attrs()
+ self.handle_node(id, attrs)
+ if self.lookahead.type == SEMI:
+ self.consume()
+
+ def parse_attrs(self):
+ attrs = {}
+ while self.lookahead.type == LSQUARE:
+ self.consume()
+ while self.lookahead.type != RSQUARE:
+ name, value = self.parse_attr()
+ attrs[name] = value
+ if self.lookahead.type == COMMA:
+ self.consume()
+ self.consume()
+ return attrs
+
+ def parse_attr(self):
+ name = self.parse_id()
+ if self.lookahead.type == EQUAL:
+ self.consume()
+ value = self.parse_id()
+ else:
+ value = 'true'
+ return name, value
+
+ def parse_node_id(self):
+ node_id = self.parse_id()
+ if self.lookahead.type == COLON:
+ self.consume()
+ port = self.parse_id()
+ if self.lookahead.type == COLON:
+ self.consume()
+ compass_pt = self.parse_id()
+ else:
+ compass_pt = None
+ else:
+ port = None
+ compass_pt = None
+ # XXX: we don't really care about port and compass point values when parsing xdot
+ return node_id
+
+ def parse_id(self):
+ self.match(ID)
+ id = self.lookahead.text
+ self.consume()
+ return id
+
+ def handle_graph(self, attrs):
+ pass
+
+ def handle_node(self, id, attrs):
+ pass
+
+ def handle_edge(self, src_id, dst_id, attrs):
+ pass
+
+
+class XDotParser(DotParser):
+
+ def __init__(self, xdotcode):
+ lexer = DotLexer(buf = xdotcode)
+ DotParser.__init__(self, lexer)
+
+ self.nodes = []
+ self.edges = []
+ self.shapes = []
+ self.node_by_name = {}
+ self.top_graph = True
+
+ def handle_graph(self, attrs):
+ if self.top_graph:
+ try:
+ bb = attrs['bb']
+ except KeyError:
+ return
+
+ if not bb:
+ return
+
+ xmin, ymin, xmax, ymax = map(float, bb.split(","))
+
+ self.xoffset = -xmin
+ self.yoffset = -ymax
+ self.xscale = 1.0
+ self.yscale = -1.0
+ # FIXME: scale from points to pixels
+
+ self.width = xmax - xmin
+ self.height = ymax - ymin
+
+ self.top_graph = False
+
+ for attr in ("_draw_", "_ldraw_", "_hdraw_", "_tdraw_", "_hldraw_", "_tldraw_"):
+ if attr in attrs:
+ parser = XDotAttrParser(self, attrs[attr])
+ self.shapes.extend(parser.parse())
+
+ def handle_node(self, id, attrs):
+ try:
+ pos = attrs['pos']
+ except KeyError:
+ return
+
+ x, y = self.parse_node_pos(pos)
+ w = float(attrs['width'])*72
+ h = float(attrs['height'])*72
+ shapes = []
+ for attr in ("_draw_", "_ldraw_"):
+ if attr in attrs:
+ parser = XDotAttrParser(self, attrs[attr])
+ shapes.extend(parser.parse())
+ url = attrs.get('URL', None)
+ node = Node(x, y, w, h, shapes, url)
+ self.node_by_name[id] = node
+ if shapes:
+ self.nodes.append(node)
+
+ def handle_edge(self, src_id, dst_id, attrs):
+ try:
+ pos = attrs['pos']
+ except KeyError:
+ return
+
+ points = self.parse_edge_pos(pos)
+ shapes = []
+ for attr in ("_draw_", "_ldraw_", "_hdraw_", "_tdraw_", "_hldraw_", "_tldraw_"):
+ if attr in attrs:
+ parser = XDotAttrParser(self, attrs[attr])
+ shapes.extend(parser.parse())
+ url = attrs.get('URL', None)
+ if shapes:
+ src = self.node_by_name[src_id]
+ dst = self.node_by_name[dst_id]
+ self.edges.append(Edge(src, dst, points, shapes, url))
+
+ def parse(self):
+ DotParser.parse(self)
+
+ return Graph(self.width, self.height, self.shapes, self.nodes, self.edges)
+
+ def parse_node_pos(self, pos):
+ x, y = pos.split(",")
+ return self.transform(float(x), float(y))
+
+ def parse_edge_pos(self, pos):
+ points = []
+ for entry in pos.split(' '):
+ fields = entry.split(',')
+ try:
+ x, y = fields
+ except ValueError:
+ # TODO: handle start/end points
+ continue
+ else:
+ points.append(self.transform(float(x), float(y)))
+ return points
+
+ def transform(self, x, y):
+ # XXX: this is not the right place for this code
+ x = (x + self.xoffset)*self.xscale
+ y = (y + self.yoffset)*self.yscale
+ return x, y
+
+
+class Animation(object):
+
+ step = 0.03 # seconds
+
+ def __init__(self, dot_widget):
+ self.dot_widget = dot_widget
+ self.timeout_id = None
+
+ def start(self):
+ self.timeout_id = gobject.timeout_add(int(self.step * 1000), self.tick)
+
+ def stop(self):
+ self.dot_widget.animation = NoAnimation(self.dot_widget)
+ if self.timeout_id is not None:
+ gobject.source_remove(self.timeout_id)
+ self.timeout_id = None
+
+ def tick(self):
+ self.stop()
+
+
+class NoAnimation(Animation):
+
+ def start(self):
+ pass
+
+ def stop(self):
+ pass
+
+
+class LinearAnimation(Animation):
+
+ duration = 0.6
+
+ def start(self):
+ self.started = time.time()
+ Animation.start(self)
+
+ def tick(self):
+ t = (time.time() - self.started) / self.duration
+ self.animate(max(0, min(t, 1)))
+ return (t < 1)
+
+ def animate(self, t):
+ pass
+
+
+class MoveToAnimation(LinearAnimation):
+
+ def __init__(self, dot_widget, target_x, target_y):
+ Animation.__init__(self, dot_widget)
+ self.source_x = dot_widget.x
+ self.source_y = dot_widget.y
+ self.target_x = target_x
+ self.target_y = target_y
+
+ def animate(self, t):
+ sx, sy = self.source_x, self.source_y
+ tx, ty = self.target_x, self.target_y
+ self.dot_widget.x = tx * t + sx * (1-t)
+ self.dot_widget.y = ty * t + sy * (1-t)
+ self.dot_widget.queue_draw()
+
+
+class ZoomToAnimation(MoveToAnimation):
+
+ def __init__(self, dot_widget, target_x, target_y):
+ MoveToAnimation.__init__(self, dot_widget, target_x, target_y)
+ self.source_zoom = dot_widget.zoom_ratio
+ self.target_zoom = self.source_zoom
+ self.extra_zoom = 0
+
+ middle_zoom = 0.5 * (self.source_zoom + self.target_zoom)
+
+ distance = math.hypot(self.source_x - self.target_x,
+ self.source_y - self.target_y)
+ rect = self.dot_widget.get_allocation()
+ visible = min(rect.width, rect.height) / self.dot_widget.zoom_ratio
+ visible *= 0.9
+ if distance > 0:
+ desired_middle_zoom = visible / distance
+ self.extra_zoom = min(0, 4 * (desired_middle_zoom - middle_zoom))
+
+ def animate(self, t):
+ a, b, c = self.source_zoom, self.extra_zoom, self.target_zoom
+ self.dot_widget.zoom_ratio = c*t + b*t*(1-t) + a*(1-t)
+ self.dot_widget.zoom_to_fit_on_resize = False
+ MoveToAnimation.animate(self, t)
+
+
+class DragAction(object):
+
+ def __init__(self, dot_widget):
+ self.dot_widget = dot_widget
+
+ def on_button_press(self, event):
+ self.startmousex = self.prevmousex = event.x
+ self.startmousey = self.prevmousey = event.y
+ self.start()
+
+ def on_motion_notify(self, event):
+ if event.is_hint:
+ x, y, state = event.window.get_pointer()
+ else:
+ x, y, state = event.x, event.y, event.state
+ deltax = self.prevmousex - x
+ deltay = self.prevmousey - y
+ self.drag(deltax, deltay)
+ self.prevmousex = x
+ self.prevmousey = y
+
+ def on_button_release(self, event):
+ self.stopmousex = event.x
+ self.stopmousey = event.y
+ self.stop()
+
+ def draw(self, cr):
+ pass
+
+ def start(self):
+ pass
+
+ def drag(self, deltax, deltay):
+ pass
+
+ def stop(self):
+ pass
+
+ def abort(self):
+ pass
+
+
+class NullAction(DragAction):
+
+ def on_motion_notify(self, event):
+ if event.is_hint:
+ x, y, state = event.window.get_pointer()
+ else:
+ x, y, state = event.x, event.y, event.state
+ dot_widget = self.dot_widget
+ item = dot_widget.get_url(x, y)
+ if item is None:
+ item = dot_widget.get_jump(x, y)
+ if item is not None:
+ dot_widget.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.HAND2))
+ dot_widget.set_highlight(item.highlight)
+ else:
+ dot_widget.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.ARROW))
+ dot_widget.set_highlight(None)
+
+
+class PanAction(DragAction):
+
+ def start(self):
+ self.dot_widget.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.FLEUR))
+
+ def drag(self, deltax, deltay):
+ self.dot_widget.x += deltax / self.dot_widget.zoom_ratio
+ self.dot_widget.y += deltay / self.dot_widget.zoom_ratio
+ self.dot_widget.queue_draw()
+
+ def stop(self):
+ self.dot_widget.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.ARROW))
+
+ abort = stop
+
+
+class ZoomAction(DragAction):
+
+ def drag(self, deltax, deltay):
+ self.dot_widget.zoom_ratio *= 1.005 ** (deltax + deltay)
+ self.dot_widget.zoom_to_fit_on_resize = False
+ self.dot_widget.queue_draw()
+
+ def stop(self):
+ self.dot_widget.queue_draw()
+
+
+class ZoomAreaAction(DragAction):
+
+ def drag(self, deltax, deltay):
+ self.dot_widget.queue_draw()
+
+ def draw(self, cr):
+ cr.save()
+ cr.set_source_rgba(.5, .5, 1.0, 0.25)
+ cr.rectangle(self.startmousex, self.startmousey,
+ self.prevmousex - self.startmousex,
+ self.prevmousey - self.startmousey)
+ cr.fill()
+ cr.set_source_rgba(.5, .5, 1.0, 1.0)
+ cr.set_line_width(1)
+ cr.rectangle(self.startmousex - .5, self.startmousey - .5,
+ self.prevmousex - self.startmousex + 1,
+ self.prevmousey - self.startmousey + 1)
+ cr.stroke()
+ cr.restore()
+
+ def stop(self):
+ x1, y1 = self.dot_widget.window2graph(self.startmousex,
+ self.startmousey)
+ x2, y2 = self.dot_widget.window2graph(self.stopmousex,
+ self.stopmousey)
+ self.dot_widget.zoom_to_area(x1, y1, x2, y2)
+
+ def abort(self):
+ self.dot_widget.queue_draw()
+
+
+class DotWidget(gtk.DrawingArea):
+ """PyGTK widget that draws dot graphs."""
+
+ __gsignals__ = {
+ 'expose-event': 'override',
+ 'clicked' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING, gtk.gdk.Event))
+ }
+
+ filter = 'dot'
+
+ def __init__(self):
+ gtk.DrawingArea.__init__(self)
+
+ self.graph = Graph()
+ self.openfilename = None
+
+ self.set_flags(gtk.CAN_FOCUS)
+
+ self.add_events(gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK)
+ self.connect("button-press-event", self.on_area_button_press)
+ self.connect("button-release-event", self.on_area_button_release)
+ self.add_events(gtk.gdk.POINTER_MOTION_MASK | gtk.gdk.POINTER_MOTION_HINT_MASK | gtk.gdk.BUTTON_RELEASE_MASK)
+ self.connect("motion-notify-event", self.on_area_motion_notify)
+ self.connect("scroll-event", self.on_area_scroll_event)
+ self.connect("size-allocate", self.on_area_size_allocate)
+
+ self.connect('key-press-event', self.on_key_press_event)
+
+ self.x, self.y = 0.0, 0.0
+ self.zoom_ratio = 1.0
+ self.zoom_to_fit_on_resize = False
+ self.animation = NoAnimation(self)
+ self.drag_action = NullAction(self)
+ self.presstime = None
+ self.highlight = None
+
+ def set_filter(self, filter):
+ self.filter = filter
+
+ def set_dotcode(self, dotcode, filename=''):
+ if isinstance(dotcode, unicode):
+ dotcode = dotcode.encode('utf8')
+ p = subprocess.Popen(
+ [self.filter, '-Txdot'],
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ shell=False,
+ universal_newlines=True
+ )
+ xdotcode, error = p.communicate(dotcode)
+ if p.returncode != 0:
+ print "UNABLE TO SHELL TO DOT", error
+ dialog = gtk.MessageDialog(type=gtk.MESSAGE_ERROR,
+ message_format=error,
+ buttons=gtk.BUTTONS_OK)
+ dialog.set_title('Dot Viewer')
+ dialog.run()
+ dialog.destroy()
+ return False
+ try:
+ self.set_xdotcode(xdotcode)
+ except ParseError, ex:
+ dialog = gtk.MessageDialog(type=gtk.MESSAGE_ERROR,
+ message_format=str(ex),
+ buttons=gtk.BUTTONS_OK)
+ dialog.set_title('Dot Viewer')
+ dialog.run()
+ dialog.destroy()
+ return False
+ else:
+ self.openfilename = filename
+ return True
+
+ def set_xdotcode(self, xdotcode):
+ #print xdotcode
+ parser = XDotParser(xdotcode)
+ self.graph = parser.parse()
+ self.zoom_image(self.zoom_ratio, center=True)
+
+ def reload(self):
+ if self.openfilename is not None:
+ try:
+ fp = file(self.openfilename, 'rt')
+ self.set_dotcode(fp.read(), self.openfilename)
+ fp.close()
+ except IOError:
+ pass
+
+ def do_expose_event(self, event):
+ cr = self.window.cairo_create()
+
+ # set a clip region for the expose event
+ cr.rectangle(
+ event.area.x, event.area.y,
+ event.area.width, event.area.height
+ )
+ cr.clip()
+
+ cr.set_source_rgba(1.0, 1.0, 1.0, 1.0)
+ cr.paint()
+
+ cr.save()
+ rect = self.get_allocation()
+ cr.translate(0.5*rect.width, 0.5*rect.height)
+ cr.scale(self.zoom_ratio, self.zoom_ratio)
+ cr.translate(-self.x, -self.y)
+
+ self.graph.draw(cr, highlight_items=self.highlight)
+ cr.restore()
+
+ self.drag_action.draw(cr)
+
+ return False
+
+ def get_current_pos(self):
+ return self.x, self.y
+
+ def set_current_pos(self, x, y):
+ self.x = x
+ self.y = y
+ self.queue_draw()
+
+ def set_highlight(self, items):
+ if self.highlight != items:
+ self.highlight = items
+ self.queue_draw()
+
+ def zoom_image(self, zoom_ratio, center=False, pos=None):
+ if center:
+ self.x = self.graph.width/2
+ self.y = self.graph.height/2
+ elif pos is not None:
+ rect = self.get_allocation()
+ x, y = pos
+ x -= 0.5*rect.width
+ y -= 0.5*rect.height
+ self.x += x / self.zoom_ratio - x / zoom_ratio
+ self.y += y / self.zoom_ratio - y / zoom_ratio
+ self.zoom_ratio = zoom_ratio
+ self.zoom_to_fit_on_resize = False
+ self.queue_draw()
+
+ def zoom_to_area(self, x1, y1, x2, y2):
+ rect = self.get_allocation()
+ width = abs(x1 - x2)
+ height = abs(y1 - y2)
+ self.zoom_ratio = min(
+ float(rect.width)/float(width),
+ float(rect.height)/float(height)
+ )
+ self.zoom_to_fit_on_resize = False
+ self.x = (x1 + x2) / 2
+ self.y = (y1 + y2) / 2
+ self.queue_draw()
+
+ def zoom_to_fit(self):
+ rect = self.get_allocation()
+ rect.x += self.ZOOM_TO_FIT_MARGIN
+ rect.y += self.ZOOM_TO_FIT_MARGIN
+ rect.width -= 2 * self.ZOOM_TO_FIT_MARGIN
+ rect.height -= 2 * self.ZOOM_TO_FIT_MARGIN
+ zoom_ratio = min(
+ float(rect.width)/float(self.graph.width),
+ float(rect.height)/float(self.graph.height)
+ )
+ self.zoom_image(zoom_ratio, center=True)
+ self.zoom_to_fit_on_resize = True
+
+ ZOOM_INCREMENT = 1.25
+ ZOOM_TO_FIT_MARGIN = 12
+
+ def on_zoom_in(self, action):
+ self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT)
+
+ def on_zoom_out(self, action):
+ self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT)
+
+ def on_zoom_fit(self, action):
+ self.zoom_to_fit()
+
+ def on_zoom_100(self, action):
+ self.zoom_image(1.0)
+
+ POS_INCREMENT = 100
+
+ def on_key_press_event(self, widget, event):
+ if event.keyval == gtk.keysyms.Left:
+ self.x -= self.POS_INCREMENT/self.zoom_ratio
+ self.queue_draw()
+ return True
+ if event.keyval == gtk.keysyms.Right:
+ self.x += self.POS_INCREMENT/self.zoom_ratio
+ self.queue_draw()
+ return True
+ if event.keyval == gtk.keysyms.Up:
+ self.y -= self.POS_INCREMENT/self.zoom_ratio
+ self.queue_draw()
+ return True
+ if event.keyval == gtk.keysyms.Down:
+ self.y += self.POS_INCREMENT/self.zoom_ratio
+ self.queue_draw()
+ return True
+ if event.keyval == gtk.keysyms.Page_Up:
+ self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT)
+ self.queue_draw()
+ return True
+ if event.keyval == gtk.keysyms.Page_Down:
+ self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT)
+ self.queue_draw()
+ return True
+ if event.keyval == gtk.keysyms.Escape:
+ self.drag_action.abort()
+ self.drag_action = NullAction(self)
+ return True
+ if event.keyval == gtk.keysyms.r:
+ self.reload()
+ return True
+ if event.keyval == gtk.keysyms.q:
+ gtk.main_quit()
+ return True
+ return False
+
+ def get_drag_action(self, event):
+ state = event.state
+ if event.button in (1, 2): # left or middle button
+ if state & gtk.gdk.CONTROL_MASK:
+ return ZoomAction
+ elif state & gtk.gdk.SHIFT_MASK:
+ return ZoomAreaAction
+ else:
+ return PanAction
+ return NullAction
+
+ def on_area_button_press(self, area, event):
+ self.animation.stop()
+ self.drag_action.abort()
+ action_type = self.get_drag_action(event)
+ self.drag_action = action_type(self)
+ self.drag_action.on_button_press(event)
+ self.presstime = time.time()
+ self.pressx = event.x
+ self.pressy = event.y
+ return False
+
+ def is_click(self, event, click_fuzz=4, click_timeout=1.0):
+ assert event.type == gtk.gdk.BUTTON_RELEASE
+ if self.presstime is None:
+ # got a button release without seeing the press?
+ return False
+ # XXX instead of doing this complicated logic, shouldn't we listen
+ # for gtk's clicked event instead?
+ deltax = self.pressx - event.x
+ deltay = self.pressy - event.y
+ return (time.time() < self.presstime + click_timeout
+ and math.hypot(deltax, deltay) < click_fuzz)
+
+ def on_area_button_release(self, area, event):
+ self.drag_action.on_button_release(event)
+ self.drag_action = NullAction(self)
+ if event.button == 1 and self.is_click(event):
+ x, y = int(event.x), int(event.y)
+ url = self.get_url(x, y)
+ if url is not None:
+ self.emit('clicked', unicode(url.url), event)
+ else:
+ jump = self.get_jump(x, y)
+ if jump is not None:
+ self.animate_to(jump.x, jump.y)
+
+ return True
+ if event.button == 1 or event.button == 2:
+ return True
+ return False
+
+ def on_area_scroll_event(self, area, event):
+ if event.direction == gtk.gdk.SCROLL_UP:
+ self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT,
+ pos=(event.x, event.y))
+ return True
+ if event.direction == gtk.gdk.SCROLL_DOWN:
+ self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT,
+ pos=(event.x, event.y))
+ return True
+ return False
+
+ def on_area_motion_notify(self, area, event):
+ self.drag_action.on_motion_notify(event)
+ return True
+
+ def on_area_size_allocate(self, area, allocation):
+ if self.zoom_to_fit_on_resize:
+ self.zoom_to_fit()
+
+ def animate_to(self, x, y):
+ self.animation = ZoomToAnimation(self, x, y)
+ self.animation.start()
+
+ def window2graph(self, x, y):
+ rect = self.get_allocation()
+ x -= 0.5*rect.width
+ y -= 0.5*rect.height
+ x /= self.zoom_ratio
+ y /= self.zoom_ratio
+ x += self.x
+ y += self.y
+ return x, y
+
+ def get_url(self, x, y):
+ x, y = self.window2graph(x, y)
+ return self.graph.get_url(x, y)
+
+ def get_jump(self, x, y):
+ x, y = self.window2graph(x, y)
+ return self.graph.get_jump(x, y)
+
+
+class DotWindow(gtk.Window):
+
+ ui = '''
+
+
+
+
+
+
+
+
+
+
+
+ '''
+
+ def __init__(self):
+ gtk.Window.__init__(self)
+
+ self.graph = Graph()
+
+ window = self
+
+ window.set_title('Dot Viewer')
+ window.set_default_size(512, 512)
+ vbox = gtk.VBox()
+ window.add(vbox)
+
+ self.widget = DotWidget()
+
+ # Create a UIManager instance
+ uimanager = self.uimanager = gtk.UIManager()
+
+ # Add the accelerator group to the toplevel window
+ accelgroup = uimanager.get_accel_group()
+ window.add_accel_group(accelgroup)
+
+ # Create an ActionGroup
+ actiongroup = gtk.ActionGroup('Actions')
+ self.actiongroup = actiongroup
+
+ # Create actions
+ actiongroup.add_actions((
+ ('Open', gtk.STOCK_OPEN, None, None, None, self.on_open),
+ ('Reload', gtk.STOCK_REFRESH, None, None, None, self.on_reload),
+ ('ZoomIn', gtk.STOCK_ZOOM_IN, None, None, None, self.widget.on_zoom_in),
+ ('ZoomOut', gtk.STOCK_ZOOM_OUT, None, None, None, self.widget.on_zoom_out),
+ ('ZoomFit', gtk.STOCK_ZOOM_FIT, None, None, None, self.widget.on_zoom_fit),
+ ('Zoom100', gtk.STOCK_ZOOM_100, None, None, None, self.widget.on_zoom_100),
+ ))
+
+ # Add the actiongroup to the uimanager
+ uimanager.insert_action_group(actiongroup, 0)
+
+ # Add a UI descrption
+ uimanager.add_ui_from_string(self.ui)
+
+ # Create a Toolbar
+ toolbar = uimanager.get_widget('/ToolBar')
+ vbox.pack_start(toolbar, False)
+
+ vbox.pack_start(self.widget)
+
+ self.set_focus(self.widget)
+
+ self.show_all()
+
+ def update(self, filename):
+ import os
+ if not hasattr(self, "last_mtime"):
+ self.last_mtime = None
+
+ current_mtime = os.stat(filename).st_mtime
+ if current_mtime != self.last_mtime:
+ self.last_mtime = current_mtime
+ self.open_file(filename)
+
+ return True
+
+ def set_filter(self, filter):
+ self.widget.set_filter(filter)
+
+ def set_dotcode(self, dotcode, filename=''):
+ if self.widget.set_dotcode(dotcode, filename):
+ self.set_title(os.path.basename(filename) + ' - Dot Viewer')
+ self.widget.zoom_to_fit()
+
+ def set_xdotcode(self, xdotcode, filename=''):
+ if self.widget.set_xdotcode(xdotcode):
+ self.set_title(os.path.basename(filename) + ' - Dot Viewer')
+ self.widget.zoom_to_fit()
+
+ def open_file(self, filename):
+ try:
+ fp = file(filename, 'rt')
+ self.set_dotcode(fp.read(), filename)
+ fp.close()
+ except IOError, ex:
+ dlg = gtk.MessageDialog(type=gtk.MESSAGE_ERROR,
+ message_format=str(ex),
+ buttons=gtk.BUTTONS_OK)
+ dlg.set_title('Dot Viewer')
+ dlg.run()
+ dlg.destroy()
+
+ def on_open(self, action):
+ chooser = gtk.FileChooserDialog(title="Open dot File",
+ action=gtk.FILE_CHOOSER_ACTION_OPEN,
+ buttons=(gtk.STOCK_CANCEL,
+ gtk.RESPONSE_CANCEL,
+ gtk.STOCK_OPEN,
+ gtk.RESPONSE_OK))
+ chooser.set_default_response(gtk.RESPONSE_OK)
+ filter = gtk.FileFilter()
+ filter.set_name("Graphviz dot files")
+ filter.add_pattern("*.dot")
+ chooser.add_filter(filter)
+ filter = gtk.FileFilter()
+ filter.set_name("All files")
+ filter.add_pattern("*")
+ chooser.add_filter(filter)
+ if chooser.run() == gtk.RESPONSE_OK:
+ filename = chooser.get_filename()
+ chooser.destroy()
+ self.open_file(filename)
+ else:
+ chooser.destroy()
+
+ def on_reload(self, action):
+ self.widget.reload()
+
+
+def main():
+ import optparse
+
+ parser = optparse.OptionParser(
+ usage='\n\t%prog [file]',
+ version='%%prog %s' % __version__)
+ parser.add_option(
+ '-f', '--filter',
+ type='choice', choices=('dot', 'neato', 'twopi', 'circo', 'fdp'),
+ dest='filter', default='dot',
+ help='graphviz filter: dot, neato, twopi, circo, or fdp [default: %default]')
+
+ (options, args) = parser.parse_args(sys.argv[1:])
+ if len(args) > 1:
+ parser.error('incorrect number of arguments')
+
+ win = DotWindow()
+ win.connect('destroy', gtk.main_quit)
+ win.set_filter(options.filter)
+ if len(args) >= 1:
+ if args[0] == '-':
+ win.set_dotcode(sys.stdin.read())
+ else:
+ win.open_file(args[0])
+ gobject.timeout_add(1000, win.update, args[0])
+ gtk.main()
+
+
+# Apache-Style Software License for ColorBrewer software and ColorBrewer Color
+# Schemes, Version 1.1
+#
+# Copyright (c) 2002 Cynthia Brewer, Mark Harrower, and The Pennsylvania State
+# University. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions as source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+#
+# 2. The end-user documentation included with the redistribution, if any,
+# must include the following acknowledgment:
+#
+# This product includes color specifications and designs developed by
+# Cynthia Brewer (http://colorbrewer.org/).
+#
+# Alternately, this acknowledgment may appear in the software itself, if and
+# wherever such third-party acknowledgments normally appear.
+#
+# 3. The name "ColorBrewer" must not be used to endorse or promote products
+# derived from this software without prior written permission. For written
+# permission, please contact Cynthia Brewer at cbrewer@psu.edu.
+#
+# 4. Products derived from this software may not be called "ColorBrewer",
+# nor may "ColorBrewer" appear in their name, without prior written
+# permission of Cynthia Brewer.
+#
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED WARRANTIES,
+# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+# FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CYNTHIA
+# BREWER, MARK HARROWER, OR THE PENNSYLVANIA STATE UNIVERSITY BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+brewer_colors = {
+ 'accent3': [(127, 201, 127), (190, 174, 212), (253, 192, 134)],
+ 'accent4': [(127, 201, 127), (190, 174, 212), (253, 192, 134), (255, 255, 153)],
+ 'accent5': [(127, 201, 127), (190, 174, 212), (253, 192, 134), (255, 255, 153), (56, 108, 176)],
+ 'accent6': [(127, 201, 127), (190, 174, 212), (253, 192, 134), (255, 255, 153), (56, 108, 176), (240, 2, 127)],
+ 'accent7': [(127, 201, 127), (190, 174, 212), (253, 192, 134), (255, 255, 153), (56, 108, 176), (240, 2, 127), (191, 91, 23)],
+ 'accent8': [(127, 201, 127), (190, 174, 212), (253, 192, 134), (255, 255, 153), (56, 108, 176), (240, 2, 127), (191, 91, 23), (102, 102, 102)],
+ 'blues3': [(222, 235, 247), (158, 202, 225), (49, 130, 189)],
+ 'blues4': [(239, 243, 255), (189, 215, 231), (107, 174, 214), (33, 113, 181)],
+ 'blues5': [(239, 243, 255), (189, 215, 231), (107, 174, 214), (49, 130, 189), (8, 81, 156)],
+ 'blues6': [(239, 243, 255), (198, 219, 239), (158, 202, 225), (107, 174, 214), (49, 130, 189), (8, 81, 156)],
+ 'blues7': [(239, 243, 255), (198, 219, 239), (158, 202, 225), (107, 174, 214), (66, 146, 198), (33, 113, 181), (8, 69, 148)],
+ 'blues8': [(247, 251, 255), (222, 235, 247), (198, 219, 239), (158, 202, 225), (107, 174, 214), (66, 146, 198), (33, 113, 181), (8, 69, 148)],
+ 'blues9': [(247, 251, 255), (222, 235, 247), (198, 219, 239), (158, 202, 225), (107, 174, 214), (66, 146, 198), (33, 113, 181), (8, 81, 156), (8, 48, 107)],
+ 'brbg10': [(84, 48, 5), (0, 60, 48), (140, 81, 10), (191, 129, 45), (223, 194, 125), (246, 232, 195), (199, 234, 229), (128, 205, 193), (53, 151, 143), (1, 102, 94)],
+ 'brbg11': [(84, 48, 5), (1, 102, 94), (0, 60, 48), (140, 81, 10), (191, 129, 45), (223, 194, 125), (246, 232, 195), (245, 245, 245), (199, 234, 229), (128, 205, 193), (53, 151, 143)],
+ 'brbg3': [(216, 179, 101), (245, 245, 245), (90, 180, 172)],
+ 'brbg4': [(166, 97, 26), (223, 194, 125), (128, 205, 193), (1, 133, 113)],
+ 'brbg5': [(166, 97, 26), (223, 194, 125), (245, 245, 245), (128, 205, 193), (1, 133, 113)],
+ 'brbg6': [(140, 81, 10), (216, 179, 101), (246, 232, 195), (199, 234, 229), (90, 180, 172), (1, 102, 94)],
+ 'brbg7': [(140, 81, 10), (216, 179, 101), (246, 232, 195), (245, 245, 245), (199, 234, 229), (90, 180, 172), (1, 102, 94)],
+ 'brbg8': [(140, 81, 10), (191, 129, 45), (223, 194, 125), (246, 232, 195), (199, 234, 229), (128, 205, 193), (53, 151, 143), (1, 102, 94)],
+ 'brbg9': [(140, 81, 10), (191, 129, 45), (223, 194, 125), (246, 232, 195), (245, 245, 245), (199, 234, 229), (128, 205, 193), (53, 151, 143), (1, 102, 94)],
+ 'bugn3': [(229, 245, 249), (153, 216, 201), (44, 162, 95)],
+ 'bugn4': [(237, 248, 251), (178, 226, 226), (102, 194, 164), (35, 139, 69)],
+ 'bugn5': [(237, 248, 251), (178, 226, 226), (102, 194, 164), (44, 162, 95), (0, 109, 44)],
+ 'bugn6': [(237, 248, 251), (204, 236, 230), (153, 216, 201), (102, 194, 164), (44, 162, 95), (0, 109, 44)],
+ 'bugn7': [(237, 248, 251), (204, 236, 230), (153, 216, 201), (102, 194, 164), (65, 174, 118), (35, 139, 69), (0, 88, 36)],
+ 'bugn8': [(247, 252, 253), (229, 245, 249), (204, 236, 230), (153, 216, 201), (102, 194, 164), (65, 174, 118), (35, 139, 69), (0, 88, 36)],
+ 'bugn9': [(247, 252, 253), (229, 245, 249), (204, 236, 230), (153, 216, 201), (102, 194, 164), (65, 174, 118), (35, 139, 69), (0, 109, 44), (0, 68, 27)],
+ 'bupu3': [(224, 236, 244), (158, 188, 218), (136, 86, 167)],
+ 'bupu4': [(237, 248, 251), (179, 205, 227), (140, 150, 198), (136, 65, 157)],
+ 'bupu5': [(237, 248, 251), (179, 205, 227), (140, 150, 198), (136, 86, 167), (129, 15, 124)],
+ 'bupu6': [(237, 248, 251), (191, 211, 230), (158, 188, 218), (140, 150, 198), (136, 86, 167), (129, 15, 124)],
+ 'bupu7': [(237, 248, 251), (191, 211, 230), (158, 188, 218), (140, 150, 198), (140, 107, 177), (136, 65, 157), (110, 1, 107)],
+ 'bupu8': [(247, 252, 253), (224, 236, 244), (191, 211, 230), (158, 188, 218), (140, 150, 198), (140, 107, 177), (136, 65, 157), (110, 1, 107)],
+ 'bupu9': [(247, 252, 253), (224, 236, 244), (191, 211, 230), (158, 188, 218), (140, 150, 198), (140, 107, 177), (136, 65, 157), (129, 15, 124), (77, 0, 75)],
+ 'dark23': [(27, 158, 119), (217, 95, 2), (117, 112, 179)],
+ 'dark24': [(27, 158, 119), (217, 95, 2), (117, 112, 179), (231, 41, 138)],
+ 'dark25': [(27, 158, 119), (217, 95, 2), (117, 112, 179), (231, 41, 138), (102, 166, 30)],
+ 'dark26': [(27, 158, 119), (217, 95, 2), (117, 112, 179), (231, 41, 138), (102, 166, 30), (230, 171, 2)],
+ 'dark27': [(27, 158, 119), (217, 95, 2), (117, 112, 179), (231, 41, 138), (102, 166, 30), (230, 171, 2), (166, 118, 29)],
+ 'dark28': [(27, 158, 119), (217, 95, 2), (117, 112, 179), (231, 41, 138), (102, 166, 30), (230, 171, 2), (166, 118, 29), (102, 102, 102)],
+ 'gnbu3': [(224, 243, 219), (168, 221, 181), (67, 162, 202)],
+ 'gnbu4': [(240, 249, 232), (186, 228, 188), (123, 204, 196), (43, 140, 190)],
+ 'gnbu5': [(240, 249, 232), (186, 228, 188), (123, 204, 196), (67, 162, 202), (8, 104, 172)],
+ 'gnbu6': [(240, 249, 232), (204, 235, 197), (168, 221, 181), (123, 204, 196), (67, 162, 202), (8, 104, 172)],
+ 'gnbu7': [(240, 249, 232), (204, 235, 197), (168, 221, 181), (123, 204, 196), (78, 179, 211), (43, 140, 190), (8, 88, 158)],
+ 'gnbu8': [(247, 252, 240), (224, 243, 219), (204, 235, 197), (168, 221, 181), (123, 204, 196), (78, 179, 211), (43, 140, 190), (8, 88, 158)],
+ 'gnbu9': [(247, 252, 240), (224, 243, 219), (204, 235, 197), (168, 221, 181), (123, 204, 196), (78, 179, 211), (43, 140, 190), (8, 104, 172), (8, 64, 129)],
+ 'greens3': [(229, 245, 224), (161, 217, 155), (49, 163, 84)],
+ 'greens4': [(237, 248, 233), (186, 228, 179), (116, 196, 118), (35, 139, 69)],
+ 'greens5': [(237, 248, 233), (186, 228, 179), (116, 196, 118), (49, 163, 84), (0, 109, 44)],
+ 'greens6': [(237, 248, 233), (199, 233, 192), (161, 217, 155), (116, 196, 118), (49, 163, 84), (0, 109, 44)],
+ 'greens7': [(237, 248, 233), (199, 233, 192), (161, 217, 155), (116, 196, 118), (65, 171, 93), (35, 139, 69), (0, 90, 50)],
+ 'greens8': [(247, 252, 245), (229, 245, 224), (199, 233, 192), (161, 217, 155), (116, 196, 118), (65, 171, 93), (35, 139, 69), (0, 90, 50)],
+ 'greens9': [(247, 252, 245), (229, 245, 224), (199, 233, 192), (161, 217, 155), (116, 196, 118), (65, 171, 93), (35, 139, 69), (0, 109, 44), (0, 68, 27)],
+ 'greys3': [(240, 240, 240), (189, 189, 189), (99, 99, 99)],
+ 'greys4': [(247, 247, 247), (204, 204, 204), (150, 150, 150), (82, 82, 82)],
+ 'greys5': [(247, 247, 247), (204, 204, 204), (150, 150, 150), (99, 99, 99), (37, 37, 37)],
+ 'greys6': [(247, 247, 247), (217, 217, 217), (189, 189, 189), (150, 150, 150), (99, 99, 99), (37, 37, 37)],
+ 'greys7': [(247, 247, 247), (217, 217, 217), (189, 189, 189), (150, 150, 150), (115, 115, 115), (82, 82, 82), (37, 37, 37)],
+ 'greys8': [(255, 255, 255), (240, 240, 240), (217, 217, 217), (189, 189, 189), (150, 150, 150), (115, 115, 115), (82, 82, 82), (37, 37, 37)],
+ 'greys9': [(255, 255, 255), (240, 240, 240), (217, 217, 217), (189, 189, 189), (150, 150, 150), (115, 115, 115), (82, 82, 82), (37, 37, 37), (0, 0, 0)],
+ 'oranges3': [(254, 230, 206), (253, 174, 107), (230, 85, 13)],
+ 'oranges4': [(254, 237, 222), (253, 190, 133), (253, 141, 60), (217, 71, 1)],
+ 'oranges5': [(254, 237, 222), (253, 190, 133), (253, 141, 60), (230, 85, 13), (166, 54, 3)],
+ 'oranges6': [(254, 237, 222), (253, 208, 162), (253, 174, 107), (253, 141, 60), (230, 85, 13), (166, 54, 3)],
+ 'oranges7': [(254, 237, 222), (253, 208, 162), (253, 174, 107), (253, 141, 60), (241, 105, 19), (217, 72, 1), (140, 45, 4)],
+ 'oranges8': [(255, 245, 235), (254, 230, 206), (253, 208, 162), (253, 174, 107), (253, 141, 60), (241, 105, 19), (217, 72, 1), (140, 45, 4)],
+ 'oranges9': [(255, 245, 235), (254, 230, 206), (253, 208, 162), (253, 174, 107), (253, 141, 60), (241, 105, 19), (217, 72, 1), (166, 54, 3), (127, 39, 4)],
+ 'orrd3': [(254, 232, 200), (253, 187, 132), (227, 74, 51)],
+ 'orrd4': [(254, 240, 217), (253, 204, 138), (252, 141, 89), (215, 48, 31)],
+ 'orrd5': [(254, 240, 217), (253, 204, 138), (252, 141, 89), (227, 74, 51), (179, 0, 0)],
+ 'orrd6': [(254, 240, 217), (253, 212, 158), (253, 187, 132), (252, 141, 89), (227, 74, 51), (179, 0, 0)],
+ 'orrd7': [(254, 240, 217), (253, 212, 158), (253, 187, 132), (252, 141, 89), (239, 101, 72), (215, 48, 31), (153, 0, 0)],
+ 'orrd8': [(255, 247, 236), (254, 232, 200), (253, 212, 158), (253, 187, 132), (252, 141, 89), (239, 101, 72), (215, 48, 31), (153, 0, 0)],
+ 'orrd9': [(255, 247, 236), (254, 232, 200), (253, 212, 158), (253, 187, 132), (252, 141, 89), (239, 101, 72), (215, 48, 31), (179, 0, 0), (127, 0, 0)],
+ 'paired10': [(166, 206, 227), (106, 61, 154), (31, 120, 180), (178, 223, 138), (51, 160, 44), (251, 154, 153), (227, 26, 28), (253, 191, 111), (255, 127, 0), (202, 178, 214)],
+ 'paired11': [(166, 206, 227), (106, 61, 154), (255, 255, 153), (31, 120, 180), (178, 223, 138), (51, 160, 44), (251, 154, 153), (227, 26, 28), (253, 191, 111), (255, 127, 0), (202, 178, 214)],
+ 'paired12': [(166, 206, 227), (106, 61, 154), (255, 255, 153), (177, 89, 40), (31, 120, 180), (178, 223, 138), (51, 160, 44), (251, 154, 153), (227, 26, 28), (253, 191, 111), (255, 127, 0), (202, 178, 214)],
+ 'paired3': [(166, 206, 227), (31, 120, 180), (178, 223, 138)],
+ 'paired4': [(166, 206, 227), (31, 120, 180), (178, 223, 138), (51, 160, 44)],
+ 'paired5': [(166, 206, 227), (31, 120, 180), (178, 223, 138), (51, 160, 44), (251, 154, 153)],
+ 'paired6': [(166, 206, 227), (31, 120, 180), (178, 223, 138), (51, 160, 44), (251, 154, 153), (227, 26, 28)],
+ 'paired7': [(166, 206, 227), (31, 120, 180), (178, 223, 138), (51, 160, 44), (251, 154, 153), (227, 26, 28), (253, 191, 111)],
+ 'paired8': [(166, 206, 227), (31, 120, 180), (178, 223, 138), (51, 160, 44), (251, 154, 153), (227, 26, 28), (253, 191, 111), (255, 127, 0)],
+ 'paired9': [(166, 206, 227), (31, 120, 180), (178, 223, 138), (51, 160, 44), (251, 154, 153), (227, 26, 28), (253, 191, 111), (255, 127, 0), (202, 178, 214)],
+ 'pastel13': [(251, 180, 174), (179, 205, 227), (204, 235, 197)],
+ 'pastel14': [(251, 180, 174), (179, 205, 227), (204, 235, 197), (222, 203, 228)],
+ 'pastel15': [(251, 180, 174), (179, 205, 227), (204, 235, 197), (222, 203, 228), (254, 217, 166)],
+ 'pastel16': [(251, 180, 174), (179, 205, 227), (204, 235, 197), (222, 203, 228), (254, 217, 166), (255, 255, 204)],
+ 'pastel17': [(251, 180, 174), (179, 205, 227), (204, 235, 197), (222, 203, 228), (254, 217, 166), (255, 255, 204), (229, 216, 189)],
+ 'pastel18': [(251, 180, 174), (179, 205, 227), (204, 235, 197), (222, 203, 228), (254, 217, 166), (255, 255, 204), (229, 216, 189), (253, 218, 236)],
+ 'pastel19': [(251, 180, 174), (179, 205, 227), (204, 235, 197), (222, 203, 228), (254, 217, 166), (255, 255, 204), (229, 216, 189), (253, 218, 236), (242, 242, 242)],
+ 'pastel23': [(179, 226, 205), (253, 205, 172), (203, 213, 232)],
+ 'pastel24': [(179, 226, 205), (253, 205, 172), (203, 213, 232), (244, 202, 228)],
+ 'pastel25': [(179, 226, 205), (253, 205, 172), (203, 213, 232), (244, 202, 228), (230, 245, 201)],
+ 'pastel26': [(179, 226, 205), (253, 205, 172), (203, 213, 232), (244, 202, 228), (230, 245, 201), (255, 242, 174)],
+ 'pastel27': [(179, 226, 205), (253, 205, 172), (203, 213, 232), (244, 202, 228), (230, 245, 201), (255, 242, 174), (241, 226, 204)],
+ 'pastel28': [(179, 226, 205), (253, 205, 172), (203, 213, 232), (244, 202, 228), (230, 245, 201), (255, 242, 174), (241, 226, 204), (204, 204, 204)],
+ 'piyg10': [(142, 1, 82), (39, 100, 25), (197, 27, 125), (222, 119, 174), (241, 182, 218), (253, 224, 239), (230, 245, 208), (184, 225, 134), (127, 188, 65), (77, 146, 33)],
+ 'piyg11': [(142, 1, 82), (77, 146, 33), (39, 100, 25), (197, 27, 125), (222, 119, 174), (241, 182, 218), (253, 224, 239), (247, 247, 247), (230, 245, 208), (184, 225, 134), (127, 188, 65)],
+ 'piyg3': [(233, 163, 201), (247, 247, 247), (161, 215, 106)],
+ 'piyg4': [(208, 28, 139), (241, 182, 218), (184, 225, 134), (77, 172, 38)],
+ 'piyg5': [(208, 28, 139), (241, 182, 218), (247, 247, 247), (184, 225, 134), (77, 172, 38)],
+ 'piyg6': [(197, 27, 125), (233, 163, 201), (253, 224, 239), (230, 245, 208), (161, 215, 106), (77, 146, 33)],
+ 'piyg7': [(197, 27, 125), (233, 163, 201), (253, 224, 239), (247, 247, 247), (230, 245, 208), (161, 215, 106), (77, 146, 33)],
+ 'piyg8': [(197, 27, 125), (222, 119, 174), (241, 182, 218), (253, 224, 239), (230, 245, 208), (184, 225, 134), (127, 188, 65), (77, 146, 33)],
+ 'piyg9': [(197, 27, 125), (222, 119, 174), (241, 182, 218), (253, 224, 239), (247, 247, 247), (230, 245, 208), (184, 225, 134), (127, 188, 65), (77, 146, 33)],
+ 'prgn10': [(64, 0, 75), (0, 68, 27), (118, 42, 131), (153, 112, 171), (194, 165, 207), (231, 212, 232), (217, 240, 211), (166, 219, 160), (90, 174, 97), (27, 120, 55)],
+ 'prgn11': [(64, 0, 75), (27, 120, 55), (0, 68, 27), (118, 42, 131), (153, 112, 171), (194, 165, 207), (231, 212, 232), (247, 247, 247), (217, 240, 211), (166, 219, 160), (90, 174, 97)],
+ 'prgn3': [(175, 141, 195), (247, 247, 247), (127, 191, 123)],
+ 'prgn4': [(123, 50, 148), (194, 165, 207), (166, 219, 160), (0, 136, 55)],
+ 'prgn5': [(123, 50, 148), (194, 165, 207), (247, 247, 247), (166, 219, 160), (0, 136, 55)],
+ 'prgn6': [(118, 42, 131), (175, 141, 195), (231, 212, 232), (217, 240, 211), (127, 191, 123), (27, 120, 55)],
+ 'prgn7': [(118, 42, 131), (175, 141, 195), (231, 212, 232), (247, 247, 247), (217, 240, 211), (127, 191, 123), (27, 120, 55)],
+ 'prgn8': [(118, 42, 131), (153, 112, 171), (194, 165, 207), (231, 212, 232), (217, 240, 211), (166, 219, 160), (90, 174, 97), (27, 120, 55)],
+ 'prgn9': [(118, 42, 131), (153, 112, 171), (194, 165, 207), (231, 212, 232), (247, 247, 247), (217, 240, 211), (166, 219, 160), (90, 174, 97), (27, 120, 55)],
+ 'pubu3': [(236, 231, 242), (166, 189, 219), (43, 140, 190)],
+ 'pubu4': [(241, 238, 246), (189, 201, 225), (116, 169, 207), (5, 112, 176)],
+ 'pubu5': [(241, 238, 246), (189, 201, 225), (116, 169, 207), (43, 140, 190), (4, 90, 141)],
+ 'pubu6': [(241, 238, 246), (208, 209, 230), (166, 189, 219), (116, 169, 207), (43, 140, 190), (4, 90, 141)],
+ 'pubu7': [(241, 238, 246), (208, 209, 230), (166, 189, 219), (116, 169, 207), (54, 144, 192), (5, 112, 176), (3, 78, 123)],
+ 'pubu8': [(255, 247, 251), (236, 231, 242), (208, 209, 230), (166, 189, 219), (116, 169, 207), (54, 144, 192), (5, 112, 176), (3, 78, 123)],
+ 'pubu9': [(255, 247, 251), (236, 231, 242), (208, 209, 230), (166, 189, 219), (116, 169, 207), (54, 144, 192), (5, 112, 176), (4, 90, 141), (2, 56, 88)],
+ 'pubugn3': [(236, 226, 240), (166, 189, 219), (28, 144, 153)],
+ 'pubugn4': [(246, 239, 247), (189, 201, 225), (103, 169, 207), (2, 129, 138)],
+ 'pubugn5': [(246, 239, 247), (189, 201, 225), (103, 169, 207), (28, 144, 153), (1, 108, 89)],
+ 'pubugn6': [(246, 239, 247), (208, 209, 230), (166, 189, 219), (103, 169, 207), (28, 144, 153), (1, 108, 89)],
+ 'pubugn7': [(246, 239, 247), (208, 209, 230), (166, 189, 219), (103, 169, 207), (54, 144, 192), (2, 129, 138), (1, 100, 80)],
+ 'pubugn8': [(255, 247, 251), (236, 226, 240), (208, 209, 230), (166, 189, 219), (103, 169, 207), (54, 144, 192), (2, 129, 138), (1, 100, 80)],
+ 'pubugn9': [(255, 247, 251), (236, 226, 240), (208, 209, 230), (166, 189, 219), (103, 169, 207), (54, 144, 192), (2, 129, 138), (1, 108, 89), (1, 70, 54)],
+ 'puor10': [(127, 59, 8), (45, 0, 75), (179, 88, 6), (224, 130, 20), (253, 184, 99), (254, 224, 182), (216, 218, 235), (178, 171, 210), (128, 115, 172), (84, 39, 136)],
+ 'puor11': [(127, 59, 8), (84, 39, 136), (45, 0, 75), (179, 88, 6), (224, 130, 20), (253, 184, 99), (254, 224, 182), (247, 247, 247), (216, 218, 235), (178, 171, 210), (128, 115, 172)],
+ 'puor3': [(241, 163, 64), (247, 247, 247), (153, 142, 195)],
+ 'puor4': [(230, 97, 1), (253, 184, 99), (178, 171, 210), (94, 60, 153)],
+ 'puor5': [(230, 97, 1), (253, 184, 99), (247, 247, 247), (178, 171, 210), (94, 60, 153)],
+ 'puor6': [(179, 88, 6), (241, 163, 64), (254, 224, 182), (216, 218, 235), (153, 142, 195), (84, 39, 136)],
+ 'puor7': [(179, 88, 6), (241, 163, 64), (254, 224, 182), (247, 247, 247), (216, 218, 235), (153, 142, 195), (84, 39, 136)],
+ 'puor8': [(179, 88, 6), (224, 130, 20), (253, 184, 99), (254, 224, 182), (216, 218, 235), (178, 171, 210), (128, 115, 172), (84, 39, 136)],
+ 'puor9': [(179, 88, 6), (224, 130, 20), (253, 184, 99), (254, 224, 182), (247, 247, 247), (216, 218, 235), (178, 171, 210), (128, 115, 172), (84, 39, 136)],
+ 'purd3': [(231, 225, 239), (201, 148, 199), (221, 28, 119)],
+ 'purd4': [(241, 238, 246), (215, 181, 216), (223, 101, 176), (206, 18, 86)],
+ 'purd5': [(241, 238, 246), (215, 181, 216), (223, 101, 176), (221, 28, 119), (152, 0, 67)],
+ 'purd6': [(241, 238, 246), (212, 185, 218), (201, 148, 199), (223, 101, 176), (221, 28, 119), (152, 0, 67)],
+ 'purd7': [(241, 238, 246), (212, 185, 218), (201, 148, 199), (223, 101, 176), (231, 41, 138), (206, 18, 86), (145, 0, 63)],
+ 'purd8': [(247, 244, 249), (231, 225, 239), (212, 185, 218), (201, 148, 199), (223, 101, 176), (231, 41, 138), (206, 18, 86), (145, 0, 63)],
+ 'purd9': [(247, 244, 249), (231, 225, 239), (212, 185, 218), (201, 148, 199), (223, 101, 176), (231, 41, 138), (206, 18, 86), (152, 0, 67), (103, 0, 31)],
+ 'purples3': [(239, 237, 245), (188, 189, 220), (117, 107, 177)],
+ 'purples4': [(242, 240, 247), (203, 201, 226), (158, 154, 200), (106, 81, 163)],
+ 'purples5': [(242, 240, 247), (203, 201, 226), (158, 154, 200), (117, 107, 177), (84, 39, 143)],
+ 'purples6': [(242, 240, 247), (218, 218, 235), (188, 189, 220), (158, 154, 200), (117, 107, 177), (84, 39, 143)],
+ 'purples7': [(242, 240, 247), (218, 218, 235), (188, 189, 220), (158, 154, 200), (128, 125, 186), (106, 81, 163), (74, 20, 134)],
+ 'purples8': [(252, 251, 253), (239, 237, 245), (218, 218, 235), (188, 189, 220), (158, 154, 200), (128, 125, 186), (106, 81, 163), (74, 20, 134)],
+ 'purples9': [(252, 251, 253), (239, 237, 245), (218, 218, 235), (188, 189, 220), (158, 154, 200), (128, 125, 186), (106, 81, 163), (84, 39, 143), (63, 0, 125)],
+ 'rdbu10': [(103, 0, 31), (5, 48, 97), (178, 24, 43), (214, 96, 77), (244, 165, 130), (253, 219, 199), (209, 229, 240), (146, 197, 222), (67, 147, 195), (33, 102, 172)],
+ 'rdbu11': [(103, 0, 31), (33, 102, 172), (5, 48, 97), (178, 24, 43), (214, 96, 77), (244, 165, 130), (253, 219, 199), (247, 247, 247), (209, 229, 240), (146, 197, 222), (67, 147, 195)],
+ 'rdbu3': [(239, 138, 98), (247, 247, 247), (103, 169, 207)],
+ 'rdbu4': [(202, 0, 32), (244, 165, 130), (146, 197, 222), (5, 113, 176)],
+ 'rdbu5': [(202, 0, 32), (244, 165, 130), (247, 247, 247), (146, 197, 222), (5, 113, 176)],
+ 'rdbu6': [(178, 24, 43), (239, 138, 98), (253, 219, 199), (209, 229, 240), (103, 169, 207), (33, 102, 172)],
+ 'rdbu7': [(178, 24, 43), (239, 138, 98), (253, 219, 199), (247, 247, 247), (209, 229, 240), (103, 169, 207), (33, 102, 172)],
+ 'rdbu8': [(178, 24, 43), (214, 96, 77), (244, 165, 130), (253, 219, 199), (209, 229, 240), (146, 197, 222), (67, 147, 195), (33, 102, 172)],
+ 'rdbu9': [(178, 24, 43), (214, 96, 77), (244, 165, 130), (253, 219, 199), (247, 247, 247), (209, 229, 240), (146, 197, 222), (67, 147, 195), (33, 102, 172)],
+ 'rdgy10': [(103, 0, 31), (26, 26, 26), (178, 24, 43), (214, 96, 77), (244, 165, 130), (253, 219, 199), (224, 224, 224), (186, 186, 186), (135, 135, 135), (77, 77, 77)],
+ 'rdgy11': [(103, 0, 31), (77, 77, 77), (26, 26, 26), (178, 24, 43), (214, 96, 77), (244, 165, 130), (253, 219, 199), (255, 255, 255), (224, 224, 224), (186, 186, 186), (135, 135, 135)],
+ 'rdgy3': [(239, 138, 98), (255, 255, 255), (153, 153, 153)],
+ 'rdgy4': [(202, 0, 32), (244, 165, 130), (186, 186, 186), (64, 64, 64)],
+ 'rdgy5': [(202, 0, 32), (244, 165, 130), (255, 255, 255), (186, 186, 186), (64, 64, 64)],
+ 'rdgy6': [(178, 24, 43), (239, 138, 98), (253, 219, 199), (224, 224, 224), (153, 153, 153), (77, 77, 77)],
+ 'rdgy7': [(178, 24, 43), (239, 138, 98), (253, 219, 199), (255, 255, 255), (224, 224, 224), (153, 153, 153), (77, 77, 77)],
+ 'rdgy8': [(178, 24, 43), (214, 96, 77), (244, 165, 130), (253, 219, 199), (224, 224, 224), (186, 186, 186), (135, 135, 135), (77, 77, 77)],
+ 'rdgy9': [(178, 24, 43), (214, 96, 77), (244, 165, 130), (253, 219, 199), (255, 255, 255), (224, 224, 224), (186, 186, 186), (135, 135, 135), (77, 77, 77)],
+ 'rdpu3': [(253, 224, 221), (250, 159, 181), (197, 27, 138)],
+ 'rdpu4': [(254, 235, 226), (251, 180, 185), (247, 104, 161), (174, 1, 126)],
+ 'rdpu5': [(254, 235, 226), (251, 180, 185), (247, 104, 161), (197, 27, 138), (122, 1, 119)],
+ 'rdpu6': [(254, 235, 226), (252, 197, 192), (250, 159, 181), (247, 104, 161), (197, 27, 138), (122, 1, 119)],
+ 'rdpu7': [(254, 235, 226), (252, 197, 192), (250, 159, 181), (247, 104, 161), (221, 52, 151), (174, 1, 126), (122, 1, 119)],
+ 'rdpu8': [(255, 247, 243), (253, 224, 221), (252, 197, 192), (250, 159, 181), (247, 104, 161), (221, 52, 151), (174, 1, 126), (122, 1, 119)],
+ 'rdpu9': [(255, 247, 243), (253, 224, 221), (252, 197, 192), (250, 159, 181), (247, 104, 161), (221, 52, 151), (174, 1, 126), (122, 1, 119), (73, 0, 106)],
+ 'rdylbu10': [(165, 0, 38), (49, 54, 149), (215, 48, 39), (244, 109, 67), (253, 174, 97), (254, 224, 144), (224, 243, 248), (171, 217, 233), (116, 173, 209), (69, 117, 180)],
+ 'rdylbu11': [(165, 0, 38), (69, 117, 180), (49, 54, 149), (215, 48, 39), (244, 109, 67), (253, 174, 97), (254, 224, 144), (255, 255, 191), (224, 243, 248), (171, 217, 233), (116, 173, 209)],
+ 'rdylbu3': [(252, 141, 89), (255, 255, 191), (145, 191, 219)],
+ 'rdylbu4': [(215, 25, 28), (253, 174, 97), (171, 217, 233), (44, 123, 182)],
+ 'rdylbu5': [(215, 25, 28), (253, 174, 97), (255, 255, 191), (171, 217, 233), (44, 123, 182)],
+ 'rdylbu6': [(215, 48, 39), (252, 141, 89), (254, 224, 144), (224, 243, 248), (145, 191, 219), (69, 117, 180)],
+ 'rdylbu7': [(215, 48, 39), (252, 141, 89), (254, 224, 144), (255, 255, 191), (224, 243, 248), (145, 191, 219), (69, 117, 180)],
+ 'rdylbu8': [(215, 48, 39), (244, 109, 67), (253, 174, 97), (254, 224, 144), (224, 243, 248), (171, 217, 233), (116, 173, 209), (69, 117, 180)],
+ 'rdylbu9': [(215, 48, 39), (244, 109, 67), (253, 174, 97), (254, 224, 144), (255, 255, 191), (224, 243, 248), (171, 217, 233), (116, 173, 209), (69, 117, 180)],
+ 'rdylgn10': [(165, 0, 38), (0, 104, 55), (215, 48, 39), (244, 109, 67), (253, 174, 97), (254, 224, 139), (217, 239, 139), (166, 217, 106), (102, 189, 99), (26, 152, 80)],
+ 'rdylgn11': [(165, 0, 38), (26, 152, 80), (0, 104, 55), (215, 48, 39), (244, 109, 67), (253, 174, 97), (254, 224, 139), (255, 255, 191), (217, 239, 139), (166, 217, 106), (102, 189, 99)],
+ 'rdylgn3': [(252, 141, 89), (255, 255, 191), (145, 207, 96)],
+ 'rdylgn4': [(215, 25, 28), (253, 174, 97), (166, 217, 106), (26, 150, 65)],
+ 'rdylgn5': [(215, 25, 28), (253, 174, 97), (255, 255, 191), (166, 217, 106), (26, 150, 65)],
+ 'rdylgn6': [(215, 48, 39), (252, 141, 89), (254, 224, 139), (217, 239, 139), (145, 207, 96), (26, 152, 80)],
+ 'rdylgn7': [(215, 48, 39), (252, 141, 89), (254, 224, 139), (255, 255, 191), (217, 239, 139), (145, 207, 96), (26, 152, 80)],
+ 'rdylgn8': [(215, 48, 39), (244, 109, 67), (253, 174, 97), (254, 224, 139), (217, 239, 139), (166, 217, 106), (102, 189, 99), (26, 152, 80)],
+ 'rdylgn9': [(215, 48, 39), (244, 109, 67), (253, 174, 97), (254, 224, 139), (255, 255, 191), (217, 239, 139), (166, 217, 106), (102, 189, 99), (26, 152, 80)],
+ 'reds3': [(254, 224, 210), (252, 146, 114), (222, 45, 38)],
+ 'reds4': [(254, 229, 217), (252, 174, 145), (251, 106, 74), (203, 24, 29)],
+ 'reds5': [(254, 229, 217), (252, 174, 145), (251, 106, 74), (222, 45, 38), (165, 15, 21)],
+ 'reds6': [(254, 229, 217), (252, 187, 161), (252, 146, 114), (251, 106, 74), (222, 45, 38), (165, 15, 21)],
+ 'reds7': [(254, 229, 217), (252, 187, 161), (252, 146, 114), (251, 106, 74), (239, 59, 44), (203, 24, 29), (153, 0, 13)],
+ 'reds8': [(255, 245, 240), (254, 224, 210), (252, 187, 161), (252, 146, 114), (251, 106, 74), (239, 59, 44), (203, 24, 29), (153, 0, 13)],
+ 'reds9': [(255, 245, 240), (254, 224, 210), (252, 187, 161), (252, 146, 114), (251, 106, 74), (239, 59, 44), (203, 24, 29), (165, 15, 21), (103, 0, 13)],
+ 'set13': [(228, 26, 28), (55, 126, 184), (77, 175, 74)],
+ 'set14': [(228, 26, 28), (55, 126, 184), (77, 175, 74), (152, 78, 163)],
+ 'set15': [(228, 26, 28), (55, 126, 184), (77, 175, 74), (152, 78, 163), (255, 127, 0)],
+ 'set16': [(228, 26, 28), (55, 126, 184), (77, 175, 74), (152, 78, 163), (255, 127, 0), (255, 255, 51)],
+ 'set17': [(228, 26, 28), (55, 126, 184), (77, 175, 74), (152, 78, 163), (255, 127, 0), (255, 255, 51), (166, 86, 40)],
+ 'set18': [(228, 26, 28), (55, 126, 184), (77, 175, 74), (152, 78, 163), (255, 127, 0), (255, 255, 51), (166, 86, 40), (247, 129, 191)],
+ 'set19': [(228, 26, 28), (55, 126, 184), (77, 175, 74), (152, 78, 163), (255, 127, 0), (255, 255, 51), (166, 86, 40), (247, 129, 191), (153, 153, 153)],
+ 'set23': [(102, 194, 165), (252, 141, 98), (141, 160, 203)],
+ 'set24': [(102, 194, 165), (252, 141, 98), (141, 160, 203), (231, 138, 195)],
+ 'set25': [(102, 194, 165), (252, 141, 98), (141, 160, 203), (231, 138, 195), (166, 216, 84)],
+ 'set26': [(102, 194, 165), (252, 141, 98), (141, 160, 203), (231, 138, 195), (166, 216, 84), (255, 217, 47)],
+ 'set27': [(102, 194, 165), (252, 141, 98), (141, 160, 203), (231, 138, 195), (166, 216, 84), (255, 217, 47), (229, 196, 148)],
+ 'set28': [(102, 194, 165), (252, 141, 98), (141, 160, 203), (231, 138, 195), (166, 216, 84), (255, 217, 47), (229, 196, 148), (179, 179, 179)],
+ 'set310': [(141, 211, 199), (188, 128, 189), (255, 255, 179), (190, 186, 218), (251, 128, 114), (128, 177, 211), (253, 180, 98), (179, 222, 105), (252, 205, 229), (217, 217, 217)],
+ 'set311': [(141, 211, 199), (188, 128, 189), (204, 235, 197), (255, 255, 179), (190, 186, 218), (251, 128, 114), (128, 177, 211), (253, 180, 98), (179, 222, 105), (252, 205, 229), (217, 217, 217)],
+ 'set312': [(141, 211, 199), (188, 128, 189), (204, 235, 197), (255, 237, 111), (255, 255, 179), (190, 186, 218), (251, 128, 114), (128, 177, 211), (253, 180, 98), (179, 222, 105), (252, 205, 229), (217, 217, 217)],
+ 'set33': [(141, 211, 199), (255, 255, 179), (190, 186, 218)],
+ 'set34': [(141, 211, 199), (255, 255, 179), (190, 186, 218), (251, 128, 114)],
+ 'set35': [(141, 211, 199), (255, 255, 179), (190, 186, 218), (251, 128, 114), (128, 177, 211)],
+ 'set36': [(141, 211, 199), (255, 255, 179), (190, 186, 218), (251, 128, 114), (128, 177, 211), (253, 180, 98)],
+ 'set37': [(141, 211, 199), (255, 255, 179), (190, 186, 218), (251, 128, 114), (128, 177, 211), (253, 180, 98), (179, 222, 105)],
+ 'set38': [(141, 211, 199), (255, 255, 179), (190, 186, 218), (251, 128, 114), (128, 177, 211), (253, 180, 98), (179, 222, 105), (252, 205, 229)],
+ 'set39': [(141, 211, 199), (255, 255, 179), (190, 186, 218), (251, 128, 114), (128, 177, 211), (253, 180, 98), (179, 222, 105), (252, 205, 229), (217, 217, 217)],
+ 'spectral10': [(158, 1, 66), (94, 79, 162), (213, 62, 79), (244, 109, 67), (253, 174, 97), (254, 224, 139), (230, 245, 152), (171, 221, 164), (102, 194, 165), (50, 136, 189)],
+ 'spectral11': [(158, 1, 66), (50, 136, 189), (94, 79, 162), (213, 62, 79), (244, 109, 67), (253, 174, 97), (254, 224, 139), (255, 255, 191), (230, 245, 152), (171, 221, 164), (102, 194, 165)],
+ 'spectral3': [(252, 141, 89), (255, 255, 191), (153, 213, 148)],
+ 'spectral4': [(215, 25, 28), (253, 174, 97), (171, 221, 164), (43, 131, 186)],
+ 'spectral5': [(215, 25, 28), (253, 174, 97), (255, 255, 191), (171, 221, 164), (43, 131, 186)],
+ 'spectral6': [(213, 62, 79), (252, 141, 89), (254, 224, 139), (230, 245, 152), (153, 213, 148), (50, 136, 189)],
+ 'spectral7': [(213, 62, 79), (252, 141, 89), (254, 224, 139), (255, 255, 191), (230, 245, 152), (153, 213, 148), (50, 136, 189)],
+ 'spectral8': [(213, 62, 79), (244, 109, 67), (253, 174, 97), (254, 224, 139), (230, 245, 152), (171, 221, 164), (102, 194, 165), (50, 136, 189)],
+ 'spectral9': [(213, 62, 79), (244, 109, 67), (253, 174, 97), (254, 224, 139), (255, 255, 191), (230, 245, 152), (171, 221, 164), (102, 194, 165), (50, 136, 189)],
+ 'ylgn3': [(247, 252, 185), (173, 221, 142), (49, 163, 84)],
+ 'ylgn4': [(255, 255, 204), (194, 230, 153), (120, 198, 121), (35, 132, 67)],
+ 'ylgn5': [(255, 255, 204), (194, 230, 153), (120, 198, 121), (49, 163, 84), (0, 104, 55)],
+ 'ylgn6': [(255, 255, 204), (217, 240, 163), (173, 221, 142), (120, 198, 121), (49, 163, 84), (0, 104, 55)],
+ 'ylgn7': [(255, 255, 204), (217, 240, 163), (173, 221, 142), (120, 198, 121), (65, 171, 93), (35, 132, 67), (0, 90, 50)],
+ 'ylgn8': [(255, 255, 229), (247, 252, 185), (217, 240, 163), (173, 221, 142), (120, 198, 121), (65, 171, 93), (35, 132, 67), (0, 90, 50)],
+ 'ylgn9': [(255, 255, 229), (247, 252, 185), (217, 240, 163), (173, 221, 142), (120, 198, 121), (65, 171, 93), (35, 132, 67), (0, 104, 55), (0, 69, 41)],
+ 'ylgnbu3': [(237, 248, 177), (127, 205, 187), (44, 127, 184)],
+ 'ylgnbu4': [(255, 255, 204), (161, 218, 180), (65, 182, 196), (34, 94, 168)],
+ 'ylgnbu5': [(255, 255, 204), (161, 218, 180), (65, 182, 196), (44, 127, 184), (37, 52, 148)],
+ 'ylgnbu6': [(255, 255, 204), (199, 233, 180), (127, 205, 187), (65, 182, 196), (44, 127, 184), (37, 52, 148)],
+ 'ylgnbu7': [(255, 255, 204), (199, 233, 180), (127, 205, 187), (65, 182, 196), (29, 145, 192), (34, 94, 168), (12, 44, 132)],
+ 'ylgnbu8': [(255, 255, 217), (237, 248, 177), (199, 233, 180), (127, 205, 187), (65, 182, 196), (29, 145, 192), (34, 94, 168), (12, 44, 132)],
+ 'ylgnbu9': [(255, 255, 217), (237, 248, 177), (199, 233, 180), (127, 205, 187), (65, 182, 196), (29, 145, 192), (34, 94, 168), (37, 52, 148), (8, 29, 88)],
+ 'ylorbr3': [(255, 247, 188), (254, 196, 79), (217, 95, 14)],
+ 'ylorbr4': [(255, 255, 212), (254, 217, 142), (254, 153, 41), (204, 76, 2)],
+ 'ylorbr5': [(255, 255, 212), (254, 217, 142), (254, 153, 41), (217, 95, 14), (153, 52, 4)],
+ 'ylorbr6': [(255, 255, 212), (254, 227, 145), (254, 196, 79), (254, 153, 41), (217, 95, 14), (153, 52, 4)],
+ 'ylorbr7': [(255, 255, 212), (254, 227, 145), (254, 196, 79), (254, 153, 41), (236, 112, 20), (204, 76, 2), (140, 45, 4)],
+ 'ylorbr8': [(255, 255, 229), (255, 247, 188), (254, 227, 145), (254, 196, 79), (254, 153, 41), (236, 112, 20), (204, 76, 2), (140, 45, 4)],
+ 'ylorbr9': [(255, 255, 229), (255, 247, 188), (254, 227, 145), (254, 196, 79), (254, 153, 41), (236, 112, 20), (204, 76, 2), (153, 52, 4), (102, 37, 6)],
+ 'ylorrd3': [(255, 237, 160), (254, 178, 76), (240, 59, 32)],
+ 'ylorrd4': [(255, 255, 178), (254, 204, 92), (253, 141, 60), (227, 26, 28)],
+ 'ylorrd5': [(255, 255, 178), (254, 204, 92), (253, 141, 60), (240, 59, 32), (189, 0, 38)],
+ 'ylorrd6': [(255, 255, 178), (254, 217, 118), (254, 178, 76), (253, 141, 60), (240, 59, 32), (189, 0, 38)],
+ 'ylorrd7': [(255, 255, 178), (254, 217, 118), (254, 178, 76), (253, 141, 60), (252, 78, 42), (227, 26, 28), (177, 0, 38)],
+ 'ylorrd8': [(255, 255, 204), (255, 237, 160), (254, 217, 118), (254, 178, 76), (253, 141, 60), (252, 78, 42), (227, 26, 28), (177, 0, 38)],
+}
+
+
+if __name__ == '__main__':
+ main()