#!/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 et al"
import os
import sys
import subprocess
import math
import colorsys
import time
import re
import optparse
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
def search_text(self, regexp):
return False
class TextShape(Shape):
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()
def search_text(self, regexp):
return regexp.search(self.t) is not None
class ImageShape(Shape):
def __init__(self, pen, x0, y0, w, h, path):
Shape.__init__(self)
self.pen = pen.copy()
self.x0 = x0
self.y0 = y0
self.w = w
self.h = h
self.path = path
def draw(self, cr, highlight=False):
cr2 = gtk.gdk.CairoContext(cr)
pixbuf = gtk.gdk.pixbuf_new_from_file(self.path)
sx = float(self.w)/float(pixbuf.get_width())
sy = float(self.h)/float(pixbuf.get_height())
cr.save()
cr.translate(self.x0, self.y0 - self.h)
cr.scale(sx, sy)
cr2.set_source_pixbuf(pixbuf, 0, 0)
cr2.paint()
cr.restore()
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)
def search_text(self, regexp):
for shape in self.shapes:
if shape.search_text(regexp):
return True
return False
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):
self.item = item
self.x = x
self.y = y
if highlight is None:
highlight = set([item])
self.highlight = highlight
class Element(CompoundShape):
"""Base class for graph nodes and edges."""
def __init__(self, shapes):
CompoundShape.__init__(self, shapes)
def is_inside(self, x, y):
return False
def get_url(self, x, y):
return None
def get_jump(self, x, y):
return None
class Node(Element):
def __init__(self, id, x, y, w, h, shapes, url):
Element.__init__(self, shapes)
self.id = id
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
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 __repr__(self):
return "" % self.id
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):
Element.__init__(self, shapes)
self.src = src
self.dst = dst
self.points = points
RADIUS = 10
def is_inside_begin(self, x, y):
return square_distance(x, y, *self.points[0]) <= self.RADIUS*self.RADIUS
def is_inside_end(self, x, y):
return square_distance(x, y, *self.points[-1]) <= self.RADIUS*self.RADIUS
def is_inside(self, x, y):
if self.is_inside_begin(x, y):
return True
if self.is_inside_end(x, y):
return True
return False
def get_jump(self, x, y):
if self.is_inside_begin(x, y):
return Jump(self, self.dst.x, self.dst.y, highlight=set([self, self.dst]))
if self.is_inside_end(x, y):
return Jump(self, self.src.x, self.src.y, highlight=set([self, self.src]))
return None
def __repr__(self):
return " %s>" % (self.src, self.dst)
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_element(self, x, y):
for node in self.nodes:
if node.is_inside(x, y):
return node
for edge in self.edges:
if edge.is_inside(x, y):
return edge
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
BOLD = 1
ITALIC = 2
UNDERLINE = 4
SUPERSCRIPT = 8
SUBSCRIPT = 16
STRIKE_THROUGH = 32
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 = buf
self.pos = 0
self.pen = Pen()
self.shapes = []
def __nonzero__(self):
return self.pos < len(self.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_int(self):
return int(self.read_code())
def read_float(self):
return float(self.read_code())
def read_point(self):
x = self.read_float()
y = self.read_float()
return self.transform(x, y)
def read_text(self):
num = self.read_int()
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_int()
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
elif c1 == "[":
sys.stderr.write('warning: color gradients not supported yet\n')
return None
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("warning: 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", "dotted"):
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_int()
w = s.read_float()
t = s.read_text()
self.handle_text(x, y, j, w, t)
elif op == "t":
f = s.read_int()
self.handle_font_characteristics(f)
elif op == "E":
x0, y0 = s.read_point()
w = s.read_float()
h = s.read_float()
self.handle_ellipse(x0, y0, w, h, filled=True)
elif op == "e":
x0, y0 = s.read_point()
w = s.read_float()
h = s.read_float()
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)
elif op == "I":
x0, y0 = s.read_point()
w = s.read_float()
h = s.read_float()
path = s.read_text()
self.handle_image(x0, y0, w, h, path)
else:
sys.stderr.write("error: unknown xdot opcode '%s'\n" % op)
sys.exit(1)
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
elif style == "dotted":
self.pen.dash = (2, 4) # 2pt on, 4pt off
def handle_font(self, size, name):
self.pen.fontsize = size
self.pen.fontname = name
def handle_font_characteristics(self, flags):
# TODO
if flags != 0:
sys.stderr.write("warning: font characteristics not supported yet\n" % op)
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_image(self, x0, y0, w, h, path):
self.shapes.append(ImageShape(self.pen, x0, y0, w, h, path))
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 = next(self.lexer)
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 = next(self.lexer)
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', '')
# quotes
text = text.replace('\\"', '"')
# layout engines recognize other escape codes (many non-standard)
# but we don't translate them here
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):
XDOTVERSION = '1.6'
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:
# Check xdot version
try:
xdotversion = attrs['xdotversion']
except KeyError:
pass
else:
if float(xdotversion) > float(self.XDOTVERSION):
sys.stderr.write('warning: xdot version %s, but supported is %s\n' % (xdotversion, self.XDOTVERSION))
# Parse bounding box
try:
bb = attrs['bb']
except KeyError:
return
if bb:
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 = max(xmax - xmin, 1)
self.height = max(ymax - ymin, 1)
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.get('width', 0))*72
h = float(attrs.get('height', 0))*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(id, 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())
if shapes:
src = self.node_by_name[src_id]
dst = self.node_by_name[dst_id]
self.edges.append(Edge(src, dst, points, shapes))
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.last_mtime = None
gobject.timeout_add(1000, self.update)
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 run_filter(self, dotcode):
if not self.filter:
return dotcode
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)
sys.stderr.write(error)
if p.returncode != 0:
dialog = gtk.MessageDialog(type=gtk.MESSAGE_ERROR,
message_format=error,
buttons=gtk.BUTTONS_OK)
dialog.set_title('Dot Viewer')
dialog.run()
dialog.destroy()
return None
return xdotcode
def set_dotcode(self, dotcode, filename=None):
self.openfilename = None
if isinstance(dotcode, unicode):
dotcode = dotcode.encode('utf8')
xdotcode = self.run_filter(dotcode)
if xdotcode is None:
return False
try:
self.set_xdotcode(xdotcode)
except ParseError as 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:
if filename is None:
self.last_mtime = None
else:
self.last_mtime = os.stat(filename).st_mtime
self.openfilename = filename
return True
def set_xdotcode(self, 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 update(self):
if self.openfilename is not None:
current_mtime = os.stat(self.openfilename).st_mtime
if current_mtime != self.last_mtime:
self.last_mtime = current_mtime
self.reload()
return True
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):
# Constrain zoom ratio to a sane range to prevent numeric instability.
zoom_ratio = min(zoom_ratio, 1E4)
zoom_ratio = max(zoom_ratio, 1E-6)
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)
if width == 0 and height == 0:
self.zoom_ratio *= self.ZOOM_INCREMENT
else:
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 in (gtk.keysyms.Page_Up,
gtk.keysyms.plus,
gtk.keysyms.equal,
gtk.keysyms.KP_Add):
self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT)
self.queue_draw()
return True
if event.keyval in (gtk.keysyms.Page_Down,
gtk.keysyms.minus,
gtk.keysyms.KP_Subtract):
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.f:
win = widget.get_toplevel()
find_toolitem = win.uimanager.get_widget('/ToolBar/Find')
textentry = find_toolitem.get_children()
win.set_focus(textentry[0])
return True
if event.keyval == gtk.keysyms.q:
gtk.main_quit()
return True
if event.keyval == gtk.keysyms.p:
self.on_print()
return True
return False
print_settings = None
def on_print(self, action=None):
print_op = gtk.PrintOperation()
if self.print_settings != None:
print_op.set_print_settings(self.print_settings)
print_op.connect("begin_print", self.begin_print)
print_op.connect("draw_page", self.draw_page)
res = print_op.run(gtk.PRINT_OPERATION_ACTION_PRINT_DIALOG, self.parent.parent)
if res == gtk.PRINT_OPERATION_RESULT_APPLY:
print_settings = print_op.get_print_settings()
def begin_print(self, operation, context):
operation.set_n_pages(1)
return True
def draw_page(self, operation, context, page_nr):
cr = context.get_cairo_context()
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)
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_click(self, element, event):
"""Override this method in subclass to process
click events. Note that element can be None
(click on empty space)."""
return False
def on_area_button_release(self, area, event):
self.drag_action.on_button_release(event)
self.drag_action = NullAction(self)
x, y = int(event.x), int(event.y)
if self.is_click(event):
el = self.get_element(x, y)
if self.on_click(el, event):
return True
if event.button == 1:
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_element(self, x, y):
x, y = self.window2graph(x, y)
return self.graph.get_element(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 FindMenuToolAction(gtk.Action):
__gtype_name__ = "FindMenuToolAction"
def __init__(self, *args, **kw):
gtk.Action.__init__(self, *args, **kw)
self.set_tool_item_type(gtk.ToolItem)
class DotWindow(gtk.Window):
ui = '''
'''
base_title = 'Dot Viewer'
def __init__(self, widget=None):
gtk.Window.__init__(self)
self.graph = Graph()
window = self
window.set_title(self.base_title)
window.set_default_size(512, 512)
vbox = gtk.VBox()
window.add(vbox)
self.widget = widget or 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),
('Print', gtk.STOCK_PRINT, None, None, "Prints the currently visible part of the graph", self.widget.on_print),
('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),
))
find_action = FindMenuToolAction("Find", None,
"Find a node by name", None)
actiongroup.add_action(find_action)
# 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.last_open_dir = "."
self.set_focus(self.widget)
# Add Find text search
find_toolitem = uimanager.get_widget('/ToolBar/Find')
self.textentry = gtk.Entry(max=20)
self.textentry.set_icon_from_stock(0, gtk.STOCK_FIND)
find_toolitem.add(self.textentry)
self.textentry.set_activates_default(True)
self.textentry.connect ("activate", self.textentry_activate, self.textentry);
self.textentry.connect ("changed", self.textentry_changed, self.textentry);
self.show_all()
def find_text(self, entry_text):
found_items = []
dot_widget = self.widget
regexp = re.compile(entry_text)
for node in dot_widget.graph.nodes:
if node.search_text(regexp):
found_items.append(node)
return found_items
def textentry_changed(self, widget, entry):
entry_text = entry.get_text()
dot_widget = self.widget
if not entry_text:
dot_widget.set_highlight(None)
return
found_items = self.find_text(entry_text)
dot_widget.set_highlight(found_items)
def textentry_activate(self, widget, entry):
entry_text = entry.get_text()
dot_widget = self.widget
if not entry_text:
dot_widget.set_highlight(None)
return;
found_items = self.find_text(entry_text)
dot_widget.set_highlight(found_items)
if(len(found_items) == 1):
dot_widget.animate_to(found_items[0].x, found_items[0].y)
def set_filter(self, filter):
self.widget.set_filter(filter)
def set_dotcode(self, dotcode, filename=None):
if self.widget.set_dotcode(dotcode, filename):
self.update_title(filename)
self.widget.zoom_to_fit()
def set_xdotcode(self, xdotcode, filename=None):
if self.widget.set_xdotcode(xdotcode):
self.update_title(filename)
self.widget.zoom_to_fit()
def update_title(self, filename=None):
if filename is None:
self.set_title(self.base_title)
else:
self.set_title(os.path.basename(filename) + ' - ' + self.base_title)
def open_file(self, filename):
try:
fp = file(filename, 'rt')
self.set_dotcode(fp.read(), filename)
fp.close()
except IOError as ex:
dlg = gtk.MessageDialog(type=gtk.MESSAGE_ERROR,
message_format=str(ex),
buttons=gtk.BUTTONS_OK)
dlg.set_title(self.base_title)
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)
chooser.set_current_folder(self.last_open_dir)
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()
self.last_open_dir = chooser.get_current_folder()
chooser.destroy()
self.open_file(filename)
else:
chooser.destroy()
def on_reload(self, action):
self.widget.reload()
class OptionParser(optparse.OptionParser):
def format_epilog(self, formatter):
# Prevent stripping the newlines in epilog message
# http://stackoverflow.com/questions/1857346/python-optparse-how-to-include-additional-info-in-usage-output
return self.epilog
def main():
parser = OptionParser(
usage='\n\t%prog [file]',
epilog='''
Shortcuts:
Up, Down, Left, Right scroll
PageUp, +, = zoom in
PageDown, - zoom out
R reload dot file
F find
Q quit
P print
Escape halt animation
Ctrl-drag zoom in/out
Shift-drag zooms an area
'''
)
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]')
parser.add_option(
'-n', '--no-filter',
action='store_const', const=None, dest='filter',
help='assume input is already filtered into xdot format (use e.g. dot -Txdot)')
(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) == 0:
if not sys.stdin.isatty():
win.set_dotcode(sys.stdin.read())
else:
if args[0] == '-':
win.set_dotcode(sys.stdin.read())
else:
win.open_file(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()